Skip to content

Commit

Permalink
feat: doppler effect
Browse files Browse the repository at this point in the history
  • Loading branch information
mbillingr committed Jul 20, 2018
1 parent b294eed commit 92d2ea3
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 44 deletions.
13 changes: 12 additions & 1 deletion src/bformat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ pub struct Bweights {
}

impl Bweights {
/// Weights that correspond to a omnidirectional source
pub fn omni_source() -> Self {
Bweights {
w: 1.0 / 2f32.sqrt(),
x: 0.0,
y: 0.0,
z: 0.0,
}
}

/// Compute weights that correspond to a sound source at given position.
pub fn from_position(pos: [f32; 3]) -> Self {
let dist = (pos[0] * pos[0] + pos[1] * pos[1] + pos[1] * pos[2]).sqrt();
Expand All @@ -109,7 +119,8 @@ impl Bweights {
/// microphone, `p==0` to a bi-directional microphone, and `p==0.5` to a cardioid microphone
/// (https://en.wikipedia.org/wiki/Microphone#Polar_patterns).
pub fn virtual_microphone(direction: [f32; 3], p: f32) -> Self {
let l = (direction[0] * direction[0] + direction[1] * direction[1]
let l = (direction[0] * direction[0]
+ direction[1] * direction[1]
+ direction[1] * direction[2])
.sqrt();
Bweights {
Expand Down
6 changes: 3 additions & 3 deletions src/bmixer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::time::Duration;
use rodio::{Sample, Source};

use bformat::Bformat;
use bstream::{self, Bstream, BstreamController};
use bstream::{self, Bstream, SoundController};

/// Construct a 3D sound mixer and associated sound composer.
pub fn bmixer(sample_rate: u32) -> (BstreamMixer, Arc<BmixerComposer>) {
Expand Down Expand Up @@ -102,11 +102,11 @@ impl BmixerComposer {
/// Add a single-channel `Source` to the sound scene at a position relative to the listener
///
/// Returns a controller object that can be used to control the source during playback.
pub fn play<I>(&self, input: I, pos: [f32; 3]) -> Arc<BstreamController>
pub fn play<I>(&self, input: I) -> SoundController
where
I: Source<Item = f32> + Send + 'static,
{
let (bstream, sound_ctl) = bstream::bstream(input, pos);
let (bstream, sound_ctl) = bstream::bstream(input);

self.pending_streams
.lock()
Expand Down
138 changes: 105 additions & 33 deletions src/bstream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,31 @@ use bformat::{Bformat, Bweights};
///
/// The input source must produce `f32` samples and is expected to have exactly one channel.
pub fn bstream<I: Source<Item = f32> + Send + 'static>(
source: I,
pos: [f32; 3],
) -> (Bstream, Arc<BstreamController>) {
source: I) -> (Bstream, SoundController) {
assert_eq!(source.channels(), 1);

let controller = Arc::new(BstreamController {
let bridge = Arc::new(BstreamBridge {
commands: Mutex::new(Vec::new()),
pending_commands: AtomicBool::new(false),
stopped: AtomicBool::new(false),
});

let controller = SoundController {
bridge: bridge.clone(),
position: [0.0, 0.0, 0.0],
velocity: [0.0, 0.0, 0.0],
doppler_factor: 1.0,
speed_of_sound: 343.5, // m/s in air
};

let stream = Bstream {
input: Box::new(source),
bweights: Bweights::from_position(pos),
controller: controller.clone(),
bweights: Bweights::omni_source(),
speed: 1.0,
sampling_offset: 0.0,
previous_sample: 0.0,
next_sample: 0.0,
bridge: bridge,
};

(stream, controller)
Expand All @@ -38,9 +48,15 @@ pub fn bstream<I: Source<Item = f32> + Send + 'static>(
pub struct Bstream {
input: Box<Source<Item = f32> + Send>,
bweights: Bweights,
controller: Arc<BstreamController>,
speed: f32,
sampling_offset: f32,
previous_sample: f32,
next_sample: f32,
bridge: Arc<BstreamBridge>,
}

impl Bstream {}

impl Source for Bstream {
#[inline(always)]
fn current_frame_len(&self) -> Option<usize> {
Expand Down Expand Up @@ -68,60 +84,116 @@ impl Iterator for Bstream {
type Item = Bformat;

fn next(&mut self) -> Option<Self::Item> {
if self.controller.pending_commands.load(Ordering::SeqCst) {
let mut commands = self.controller.commands.lock().unwrap();
let mut new_pos = None;
if self.bridge.pending_commands.load(Ordering::SeqCst) {
let mut commands = self.bridge.commands.lock().unwrap();

for cmd in commands.drain(..) {
match cmd {
Command::SetPos(p) => new_pos = Some(p),
Command::SetWeights(bw) => self.bweights = bw,
Command::SetSpeed(s) => self.speed = s,
Command::Stop => {
self.controller.stopped.store(true, Ordering::SeqCst);
self.bridge.stopped.store(true, Ordering::SeqCst);
return None;
}
}
}

if let Some(pos) = new_pos {
self.bweights = Bweights::from_position(pos);
}

self.controller
self.bridge
.pending_commands
.store(false, Ordering::SeqCst);
}
match self.input.next() {
Some(x) => Some(self.bweights.scale(x)),
None => {
self.controller.stopped.store(true, Ordering::SeqCst);
None
}

while self.sampling_offset >= 1.0 {
match self.input.next() {
Some(x) => {
self.previous_sample = self.next_sample;
self.next_sample = x;
}
None => {
self.bridge.stopped.store(true, Ordering::SeqCst);
return None;
}
};
self.sampling_offset -= 1.0;
}

let x = self.next_sample * self.sampling_offset
+ self.previous_sample * (1.0 - self.sampling_offset);

self.sampling_offset += self.speed;
Some(self.bweights.scale(x))
}
}

enum Command {
SetPos([f32; 3]),
SetWeights(Bweights),
SetSpeed(f32),
Stop,
}

/// Controls playback and position of spatial audio source
pub struct BstreamController {
/// Bridges a Bstream and its controller across threads
pub struct BstreamBridge {
commands: Mutex<Vec<Command>>,
pending_commands: AtomicBool,
stopped: AtomicBool,
}

impl BstreamController {
/// Set source position
pub fn set_position(&self, pos: [f32; 3]) {
self.commands.lock().unwrap().push(Command::SetPos(pos));
self.pending_commands.store(true, Ordering::SeqCst);
/// Controls playback and position of a spatial audio source
pub struct SoundController {
bridge: Arc<BstreamBridge>,
position: [f32; 3],
velocity: [f32; 3],
doppler_factor: f32,
speed_of_sound: f32,
}

impl SoundController {
/// Set source position relative to listener
pub fn set_position(&mut self, pos: [f32; 3]) {
self.position = pos;
let weights = Bweights::from_position(pos);
let rate = self.doppler_rate();
{
let mut cmds = self.bridge.commands.lock().unwrap();
cmds.push(Command::SetSpeed(rate));
cmds.push(Command::SetWeights(weights));
}
self.bridge.pending_commands.store(true, Ordering::SeqCst);
}

/// Set source velocity relative to listener
pub fn set_velocity(&mut self, vel: [f32; 3]) {
self.velocity = vel;
let rate = self.doppler_rate();
{
let mut cmds = self.bridge.commands.lock().unwrap();
cmds.push(Command::SetSpeed(rate));
}
self.bridge.pending_commands.store(true, Ordering::SeqCst);
}

/// Stop playback
pub fn stop(&self) {
self.commands.lock().unwrap().push(Command::Stop);
self.pending_commands.store(true, Ordering::SeqCst);
self.bridge.commands.lock().unwrap().push(Command::Stop);
self.bridge.pending_commands.store(true, Ordering::SeqCst);
}

/// Set doppler factor
pub fn set_doppler_factor(&mut self, factor: f32) {
self.doppler_factor = factor;
}

/// compute doppler rate
fn doppler_rate(&self) -> f32 {
let dist = (self.position[0] * self.position[0]
+ self.position[1] * self.position[1]
+ self.position[1] * self.position[2])
.sqrt();

let relative_velocity = (self.position[0] * self.velocity[0]
+ self.position[1] * self.velocity[1]
+ self.position[1] * self.velocity[2]) / dist;

self.speed_of_sound / (self.speed_of_sound + self.doppler_factor * relative_velocity)
}
}
58 changes: 51 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Compose and play 3D audio.
//! ## Compose and play 3D audio.
//!
//! The library provides 3D sound scene support on top of [`rodio`](https://crates.io/crates/rodio).
//! It allows positioning and moving sound sources freely in 3D space around a virtual listener,
Expand All @@ -14,6 +14,30 @@
//! and played through a `rodio` sink. Although at the moment only stereo output is supported, the
//! *B-format* abstraction should make it easy to implement arbitrary speaker configurations in the
//! future.
//!
//! ## Usage Example
//!
//! ```
//! use std::thread::sleep;
//! use std::time::Duration;
//! use ambisonic::{rodio, AmbisonicBuilder};
//!
//! let scene = AmbisonicBuilder::default().build();
//!
//! let source = rodio::source::SineWave::new(440);
//! let mut sound = scene.play(source);
//!
//! // exaggerate doppler effect
//! sound.set_doppler_factor(10.0);
//!
//! // move sound from right to left
//! sound.set_velocity([-1.0, 0.0, 0.0]);
//! for i in 0..1000 {
//! sound.set_position([50.0 - i as f32 / 10.0, 1.0, 0.0]);
//! sleep(Duration::from_millis(10));
//! }
//! sound.set_velocity([0.0, 0.0, 0.0]);
//! ```

extern crate cpal;
pub extern crate rodio;
Expand All @@ -26,7 +50,7 @@ mod renderer;
use std::sync::Arc;

use bmixer::BmixerComposer;
use bstream::BstreamController;
pub use bstream::SoundController;

/// A builder object for creating `Ambisonic` contexts
pub struct AmbisonicBuilder {
Expand Down Expand Up @@ -98,11 +122,11 @@ impl Ambisonic {
///
/// Returns a controller object that can be used to control the source during playback.
#[inline(always)]
pub fn play<I>(&self, input: I, pos: [f32; 3]) -> Arc<BstreamController>
pub fn play<I>(&self, input: I) -> SoundController
where
I: rodio::Source<Item = f32> + Send + 'static,
{
self.composer.play(input, pos)
self.composer.play(input)
}
}

Expand All @@ -117,22 +141,42 @@ mod tests {
let engine = AmbisonicBuilder::new().build();

let source = rodio::source::SineWave::new(440);
let first = engine.play(source, [1.0, 0.0, 0.0]);
let mut first = engine.play(source);

first.set_position([1.0, 0.0, 0.0]);
sleep(Duration::from_millis(1000));

let source = rodio::source::SineWave::new(330);
let second = engine.play(source, [-1.0, 0.0, 0.0]);
let mut second = engine.play(source);

second.set_position([-1.0, 0.0, 0.0]);
sleep(Duration::from_millis(1000));

first.stop();
second.set_position([0.0, 1.0, 0.0]);

sleep(Duration::from_millis(1000));

drop(engine);

sleep(Duration::from_millis(1000));
}

#[test]
fn move_sound() {
let scene = AmbisonicBuilder::default().build();

let source = rodio::source::SineWave::new(440);
let mut sound = scene.play(source);

// exaggerate doppler effect
sound.set_doppler_factor(10.0);

// move sound from right to left
sound.set_velocity([-1.0, 0.0, 0.0]);
for i in 0..1000 {
sound.set_position([50.0 - i as f32 / 10.0, 1.0, 0.0]);
sleep(Duration::from_millis(10));
}
sound.set_velocity([0.0, 0.0, 0.0]);
}
}
Empty file added tools/output.txt
Empty file.
Empty file added tools/plot_output.py
Empty file.

0 comments on commit 92d2ea3

Please sign in to comment.