# Binaural Audio FX (Pseudo-binaural)

**URL:** <https://in-thread.sonic-pi.net/t/binaural-audio-fx-pseudo-binaural/9719>\
**Category:** Creations & Ideas\
**Created:** [June 5, 2025, 4:31am UTC](https://in-thread.sonic-pi.net/t/binaural-audio-fx-pseudo-binaural/9719 "2025-06-05T04:31:54Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![Doffu](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/doffu/32/2044_2.png) [@Doffu](https://in-thread.sonic-pi.net/u/Doffu)\
**Post date:** [June 5, 2025, 4:31am UTC](https://in-thread.sonic-pi.net/t/binaural-audio-fx-pseudo-binaural/9719/1 "2025-06-05T04:31:54Z")

</div>

Tonight I tried building an effect that emulates binaural audio. I made multiple versions starting with something pseudo-binaural and iterating on it to get it more realistic. This seems to be as far as I can take it, but maybe someone can recommend further improvements.

First attempt does no delay between the ears, just using panning and a couple LPFs to mimic how the sound might change when moved around the 3D space:

```ruby
use_bpm 60

# Calculate volume attenuation based on distance
define :distance_attenuation do |x, y, z, intensity=1.0|
  d = Math.sqrt(x **2 + y** 2 + z**2)
  max_dist = Math.sqrt(3)
  norm_d = d / max_dist
  amp = Math.exp(-norm_d * intensity * 5)
  return [amp, 0.01].max
end

# Map z to LPF cutoff frequency
define :cutoff_z do |z|
  if z < 0
    ratio = (z + 1.0001)
    cutoff = 40 + Math.log10(ratio * 10 + 1) * 90
  else
    ratio = (1.0001 - z)
    cutoff = 100 + Math.log10(ratio * 10 + 1) * 30
  end
  return [cutoff, 130.9].min
end

# Map y to LPF cutoff frequency
define :cutoff_y do |y|
  if y < 0
    # y in [-1, 0] → cutoff 80 to 130
    ratio = (y + 1.0001)
    cutoff = 50 + Math.log10(ratio * 10 + 1) * 80 # 80 = 130 - 50, scaled logarithmically
  else
    # y in [0, 1] → cutoff 130 to 110
    ratio = (1.0001 - y)
    cutoff = 110 + Math.log10(ratio * 10 + 1) * 20
  end
  return [cutoff, 130.9].min
end

live_loop :random_3d_sample do
  puts tick
  x = rrand(-1.0, 1.0)
  y = rrand(-1.0, 1.0)
  z = rrand(-1.0, 1.0)
  
  pan_pos = [[x, -1.0].max, 1.0].min
  intensity = 1.0
  amp = distance_attenuation(x, y, z, intensity)
  
  lpf_cutoff_y = cutoff_y(y)
  lpf_cutoff_z = cutoff_z(z)
  
  with_fx :rlpf, cutoff: lpf_cutoff_y, res: 0 do
    #with_fx :lpf, cutoff: lpf_cutoff_y do
    with_fx :lpf, cutoff: lpf_cutoff_z do
      with_fx :pan, pan: pan_pos do
        sample :elec_beep, amp: amp
      end
    end
  end
  
  print "x:", x.round(2),
    " y:", y.round(2),
    " z:", z.round(2),
    " amp:", amp.round(2),
    " pan:", pan_pos.round(2),
    " cut_y:", lpf_cutoff_y.round(1),
    " cut_z:", lpf_cutoff_z.round(1),
    " intensity:", intensity
  
  sleep 0.25
end

```

Next, added delay and amp level difference to get it more spatial feeling. Also switched one LPF to a RLPF to give it a distinct timbre from the other filter:

```ruby
# BINAURAL MODEL V2 - includes ITD + ILD (this is still not true delay accounting for human ear spacing).
use_bpm 60

define :distance_attenuation do |x, y, z, intensity=1.0|
  d = Math.sqrt(x **2 + y** 2 + z**2)
  max_dist = Math.sqrt(3)
  norm_d = d / max_dist
  amp = Math.exp(-norm_d * intensity * 5)
  return [amp, 0.01].max
end

define :cutoff_z do |z|
  if z < 0
    ratio = (z + 1.0001)
    cutoff = 40 + Math.log10(ratio * 10 + 1) * 90
  else
    ratio = (1.0001 - z)
    cutoff = 100 + Math.log10(ratio * 10 + 1) * 30
  end
  return [cutoff, 130.9].min
end

define :cutoff_y do |y|
  if y < 0
    ratio = (y + 1.0001)
    cutoff = 50 + Math.log10(ratio * 10 + 1) * 80
  else
    ratio = (1.0001 - y)
    cutoff = 110 + Math.log10(ratio * 10 + 1) * 20
  end
  return [cutoff, 130.9].min
end

live_loop :random_3d_sample do
  puts tick
  x = rrand(-1.0, 1.0) 
  y = rrand(-1.0, 1.0) 
  z = rrand(-1.0, 1.0) 
  
  intensity = 1.0
  amp = distance_attenuation(x, y, z, intensity)
  lpf_cutoff_y = cutoff_y(y)
  lpf_cutoff_z = cutoff_z(z)
  
  # Interaural Time Difference
  interaural_delay = [x.abs * 0.01, 0.001].max
  delay_left = x < 0 ? 0.001 : interaural_delay
  delay_right = x > 0 ? 0.001 : interaural_delay
  
  # Interaural Level Difference
  left_amp = amp * (1.0 - x) / 2.0
  right_amp = amp * (1.0 + x) / 2.0
  
  with_fx :rlpf, cutoff: lpf_cutoff_y, res: 0 do
    with_fx :lpf, cutoff: lpf_cutoff_z do
      
      # Left Ear
      with_fx :echo, phase: delay_left, mix: 0.4, decay: 0.05 do
        with_fx :pan, pan: -1 do
          sample :elec_beep, amp: left_amp
        end
      end
      
      # Right Ear
      with_fx :echo, phase: delay_right, mix: 0.4, decay: 0.05 do
        with_fx :pan, pan: 1 do
          sample :elec_beep, amp: right_amp
        end
      end
      
    end
  end
  
  print "x:", x.round(2),
    " y:", y.round(2),
    " z:", z.round(2),
    " amp_total:", amp.round(2),
    " left_amp:", left_amp.round(2),
    " right_amp:", right_amp.round(2),
    " cut_y:", lpf_cutoff_y.round(1),
    " cut_z:", lpf_cutoff_z.round(1),
    " delay_L:", delay_left.round(4),
    " delay_R:", delay_right.round(4)
  
  sleep 0.25
end

```

Tweaked this by making variables for ear spacing and speed of sound to better calculate delay for each ear:

```ruby
# BINAURAL MODEL V3 - includes ITD + ILD + delay based on ear spacing (Still doesn't fully model ear shape, pinna effect, and other reverb cues).
use_bpm 60

EAR_DISTANCE = 0.215 # Average human ear spacing in meters
SPEED_OF_SOUND = 343.0 # Speed of sound in m/s
MIN_DELAY = 0.001 # Minimal allowed delay for :echo phase (> 0)

define :distance_attenuation do |x, y, z, intensity=1.0|
  d = Math.sqrt(x **2 + y** 2 + z**2)
  max_dist = Math.sqrt(3)
  norm_d = d / max_dist
  amp = Math.exp(-norm_d * intensity * 5)
  return [amp, 0.01].max
end

define :cutoff_z do |z|
  if z < 0
    ratio = (z + 1.0001)
    cutoff = 40 + Math.log10(ratio * 10 + 1) * 90
  else
    ratio = (1.0001 - z)
    cutoff = 100 + Math.log10(ratio * 10 + 1) * 30
  end
  return [cutoff, 130.9].min
end

define :cutoff_y do |y|
  if y < 0
    ratio = (y + 1.0001)
    cutoff = 50 + Math.log10(ratio * 10 + 1) * 80
  else
    ratio = (1.0001 - y)
    cutoff = 110 + Math.log10(ratio * 10 + 1) * 20
  end
  return [cutoff, 130.9].min
end

live_loop :random_3d_sample do
  puts tick
  x = rrand(-1.0, 1.0)
  y = rrand(-1.0, 1.0)
  z = rrand(-1.0, 1.0)
  
  intensity = 1.0
  amp = distance_attenuation(x, y, z, intensity)
  lpf_cutoff_y = cutoff_y(y)
  lpf_cutoff_z = cutoff_z(z)
  
  # Calculate Interaural Time Difference (seconds)
  itd = (EAR_DISTANCE / SPEED_OF_SOUND) * x.abs
  
  # Assign delay times, replace zero delays with MIN_DELAY
  delay_left = x < 0 ? MIN_DELAY : itd
  delay_right = x > 0 ? MIN_DELAY : itd
  delay_left = [delay_left, MIN_DELAY].max
  delay_right = [delay_right, MIN_DELAY].max
  
  # Interaural Level Difference with a power curve for more natural attenuation
  left_amp = amp * (x < 0 ? 1.0 : (1.0 - x)**1.5)
  right_amp = amp * (x > 0 ? 1.0 : (1.0 + x)**1.5)
  
  with_fx :rlpf, cutoff: lpf_cutoff_y, res: 0 do
    with_fx :lpf, cutoff: lpf_cutoff_z do
      
      # Left Ear
      with_fx :echo, phase: delay_left, mix: 0.4, decay: 0.05 do
        with_fx :pan, pan: -1 do
          sample :ambi_choir, amp: left_amp
        end
      end
      
      # Right Ear
      with_fx :echo, phase: delay_right, mix: 0.4, decay: 0.05 do
        with_fx :pan, pan: 1 do
          sample :ambi_choir, amp: right_amp
        end
      end
      
    end
  end
  
  print "x:", x.round(2),
    " y:", y.round(2),
    " z:", z.round(2),
    " amp_total:", amp.round(2),
    " left_amp:", left_amp.round(2),
    " right_amp:", right_amp.round(2),
    " cut_y:", lpf_cutoff_y.round(1),
    " cut_z:", lpf_cutoff_z.round(1),
    " delay_L:", delay_left.round(5),
    " delay_R:", delay_right.round(5)
  
  sleep 0.5
end

```

This last bit of code is almost the same, I just added in some math to rotate the sound source in a circle around the listener at ear level to better show off the effect:

```ruby
# BINAURAL MODEL V3_RotatingSound - includes ITD + ILD + delay based on ear spacing (Still doesn't fully model ear shape, pinna effect, and other reverb cues).
# With math for making sound rotate around the listener.
use_bpm 60

EAR_DISTANCE = 0.215 # Average human ear spacing in meters
SPEED_OF_SOUND = 343.0 # Speed of sound in m/s
MIN_DELAY = 0.001 # Minimal allowed delay for :echo phase (> 0)

define :distance_attenuation do |x, y, z, intensity=1.0|
  d = Math.sqrt(x **2 + y** 2 + z**2)
  max_dist = Math.sqrt(3)
  norm_d = d / max_dist
  amp = Math.exp(-norm_d * intensity * 5)
  return [amp, 0.01].max
end

define :cutoff_z do |z|
  if z < 0
    ratio = (z + 1.0001)
    cutoff = 40 + Math.log10(ratio * 10 + 1) * 90
  else
    ratio = (1.0001 - z)
    cutoff = 100 + Math.log10(ratio * 10 + 1) * 30
  end
  return [cutoff, 130.9].min
end

define :cutoff_y do |y|
  if y < 0
    ratio = (y + 1.0001)
    cutoff = 50 + Math.log10(ratio * 10 + 1) * 80
  else
    ratio = (1.0001 - y)
    cutoff = 110 + Math.log10(ratio * 10 + 1) * 20
  end
  return [cutoff, 130.9].min
end

live_loop :random_3d_sample do
  # Use a tick to get an angle in radians
  angle = tick * (Math::PI / 16) # Adjust step size for speed of rotation
  
  x = Math.cos(angle) # Moves from 1 to -1 in cosine wave
  y = Math.sin(angle) # Moves from 0 to 1 to 0 to -1 in sine wave
  z = 0 # Ear level
  
  intensity = 1.0
  amp = distance_attenuation(x, y, z, intensity)
  lpf_cutoff_y = cutoff_y(y)
  lpf_cutoff_z = cutoff_z(z)
  
  # Calculate Interaural Time Difference (seconds)
  itd = (EAR_DISTANCE / SPEED_OF_SOUND) * x.abs
  
  # Assign delay times, replace zero delays with MIN_DELAY
  delay_left = x < 0 ? MIN_DELAY : itd
  delay_right = x > 0 ? MIN_DELAY : itd
  delay_left = [delay_left, MIN_DELAY].max
  delay_right = [delay_right, MIN_DELAY].max
  
  # Interaural Level Difference with a power curve for more natural attenuation
  left_amp = amp * (x < 0 ? 1.0 : (1.0 - x)**1.5)
  right_amp = amp * (x > 0 ? 1.0 : (1.0 + x)**1.5)
  
  with_fx :rlpf, cutoff: lpf_cutoff_y, res: 0 do
    with_fx :lpf, cutoff: lpf_cutoff_z do
      
      # Left Ear
      with_fx :echo, phase: delay_left, mix: 0.4, decay: 0.05 do
        with_fx :pan, pan: -1 do
          sample :ambi_choir, amp: left_amp
        end
      end
      
      # Right Ear
      with_fx :echo, phase: delay_right, mix: 0.4, decay: 0.05 do
        with_fx :pan, pan: 1 do
          sample :ambi_choir, amp: right_amp
        end
      end
      
    end
  end
  
  print "x:", x.round(2),
    " y:", y.round(2),
    " z:", z.round(2),
    " amp_total:", amp.round(2),
    " left_amp:", left_amp.round(2),
    " right_amp:", right_amp.round(2),
    " cut_y:", lpf_cutoff_y.round(1),
    " cut_z:", lpf_cutoff_z.round(1),
    " delay_L:", delay_left.round(5),
    " delay_R:", delay_right.round(5)
  
  sleep 0.5
end

```

[https://doffu.net/wp-content/uploads/2025/06/Binaural-RotatingSound.mp3](https://doffu.net/wp-content/uploads/2025/06/Binaural-RotatingSound.mp3)

It would be really cool if there was a built-in binaural effect within Sonic Pi… would make for some awesome soundscaping.

---

<div class="post-metadata">

**Author:** ![ethancrawford](https://avatars.discourse-cdn.com/v4/letter/e/a88e4f/32.png) [@ethancrawford](https://in-thread.sonic-pi.net/u/ethancrawford)\
**Post date:** [June 5, 2025, 4:53am UTC](https://in-thread.sonic-pi.net/t/binaural-audio-fx-pseudo-binaural/9719/2 "2025-06-05T04:53:02Z")

</div>

Perhaps not _quite_ the same, but it has been on my todo list to introduce a new FX for Sonic Pi called `:haas`, which adds a [haas effect](https://en.wikipedia.org/wiki/Precedence_effect).

---

<div class="post-metadata">

**Author:** ![Doffu](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/doffu/32/2044_2.png) [@Doffu](https://in-thread.sonic-pi.net/u/Doffu)\
**Post date:** [June 5, 2025, 4:58am UTC](https://in-thread.sonic-pi.net/t/binaural-audio-fx-pseudo-binaural/9719/3 "2025-06-05T04:58:56Z")

</div>

That would be awesome. I think that may be an important factor in making this more realistic. There’s a lot of other things I was researching like how the ear reflect and absorbs sound, shadowing from the head, etc… It seems like a fairly complicated thing to simulate so I’m glad I could at least get something feeling baseline realistic.

---

<div class="post-metadata">

**Author:** ![Doffu](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/doffu/32/2044_2.png) [@Doffu](https://in-thread.sonic-pi.net/u/Doffu)\
**Post date:** [June 5, 2025, 5:01am UTC](https://in-thread.sonic-pi.net/t/binaural-audio-fx-pseudo-binaural/9719/4 "2025-06-05T05:01:12Z")

</div>

I’m also now just wondering if my sound sample should be monophonic or whether this would have any bearing on the end result. Might have to play around with that too.

---

<div class="post-metadata">

**Author:** ![ethancrawford](https://avatars.discourse-cdn.com/v4/letter/e/a88e4f/32.png) [@ethancrawford](https://in-thread.sonic-pi.net/u/ethancrawford)\
**Post date:** [June 5, 2025, 5:07am UTC](https://in-thread.sonic-pi.net/t/binaural-audio-fx-pseudo-binaural/9719/5 "2025-06-05T05:07:00Z")

</div>

I haven’t tried the haas effect with a true stereo input - just a duplicated mono signal. Might be worth a try? I’m guessing the more similar the signals are, the more pronounced the effect 🤷‍♂️
