1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
//! ## 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,
//! and playing the resulting spatial mix in real-time over a sound card.
//!
//! `ambisonic` is built around the concept of an intermediate representation of the sound field,
//! called *B-format*. The *B-format* describes what the listener should hear, independent of
//! their audio playback equipment. This leads to a clear separation of audio scene composition and
//! rendering. For details, see [Wikipedia](https://en.wikipedia.org/wiki/Ambisonics).
//!
//! In its current state, the library allows spatial composition of single-channel `rodio` sources
//! into a first-order *B-format* stream. The chosen renderer then decodes the *B-format* stream
//! into audio signals for playback.
//!
//! Currently, the following renderers are available:
//!
//! - Stereo: simple and efficient playback on two stereo speakers or headphones
//! - HRTF: realistic 3D sound over headphones using head related transfer functions
//!
//! 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);
//! sound.set_position([50.0, 1.0, 0.0]);
//!
//! // move sound from right to left
//! sound.set_velocity([-10.0, 0.0, 0.0]);
//! for i in 0..1000 {
//!     sound.adjust_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;
extern crate rand;
pub extern crate rodio;

mod bformat;
mod bmixer;
mod bstream;
mod renderer;
pub mod sources;

use std::f32;
use std::sync::Arc;

use bmixer::BmixerComposer;
pub use bstream::SoundController;
pub use renderer::{HrtfConfig, StereoConfig};

/// Configure playback parameters
pub enum PlaybackConfiguration {
    /// Stereo playback
    Stereo(StereoConfig),

    /// Headphone playback using head related transfer functions
    Hrtf(HrtfConfig),
}

impl Default for PlaybackConfiguration {
    fn default() -> Self {
        PlaybackConfiguration::Stereo(StereoConfig::default())
    }
}

impl From<StereoConfig> for PlaybackConfiguration {
    fn from(cfg: StereoConfig) -> Self {
        PlaybackConfiguration::Stereo(cfg)
    }
}

impl From<HrtfConfig> for PlaybackConfiguration {
    fn from(cfg: HrtfConfig) -> Self {
        PlaybackConfiguration::Hrtf(cfg)
    }
}

/// A builder object for creating `Ambisonic` contexts
pub struct AmbisonicBuilder {
    device: Option<rodio::Device>,
    sample_rate: u32,
    config: PlaybackConfiguration,
}

impl AmbisonicBuilder {
    /// Create a new builder with default settings
    pub fn new() -> Self {
        Self::default()
    }

    /// Build the ambisonic context
    pub fn build(self) -> Ambisonic {
        let device = self.device
            .unwrap_or_else(|| rodio::default_output_device().unwrap());
        let sink = rodio::Sink::new(&device);

        let (mixer, controller) = bmixer::bmixer(self.sample_rate);

        match self.config {
            PlaybackConfiguration::Stereo(cfg) => {
                let output = renderer::BstreamStereoRenderer::new(mixer, cfg);
                sink.append(output);
            }

            PlaybackConfiguration::Hrtf(cfg) => {
                let output = renderer::BstreamHrtfRenderer::new(mixer, cfg);
                sink.append(output);
            }
        }

        Ambisonic {
            sink,
            composer: controller,
        }
    }

    /// Select device (defaults to `rodio::default_output_device()`
    pub fn with_device(self, device: rodio::Device) -> Self {
        AmbisonicBuilder {
            device: Some(device),
            ..self
        }
    }

    /// Set sample rate fo the ambisonic mix
    pub fn with_sample_rate(self, sample_rate: u32) -> Self {
        AmbisonicBuilder {
            sample_rate,
            ..self
        }
    }

    /// Set playback configuration
    pub fn with_config(self, config: PlaybackConfiguration) -> Self {
        AmbisonicBuilder { config, ..self }
    }
}

impl Default for AmbisonicBuilder {
    fn default() -> Self {
        AmbisonicBuilder {
            device: None,
            sample_rate: 48000,
            config: PlaybackConfiguration::default(),
        }
    }
}

/// High-level Ambisonic Context.
///
/// Stops playing all sounds when dropped.
pub struct Ambisonic {
    // disable warning that `sink` is unused. We need it to keep the audio alive.
    #[allow(dead_code)]
    sink: rodio::Sink,
    composer: Arc<BmixerComposer>,
}

impl Ambisonic {
    /// 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.
    #[inline(always)]
    pub fn play<I>(&self, input: I) -> SoundController
    where
        I: rodio::Source<Item = f32> + Send + 'static,
    {
        self.composer.play(input)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::thread::sleep;
    use std::time::Duration;

    #[test]
    fn it_works() {
        let engine = AmbisonicBuilder::new().build();

        let source = rodio::source::SineWave::new(440);
        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 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);
        sound.set_position([50.0, 1.0, 0.0]);

        // move sound from right to left
        sound.set_velocity([-10.0, 0.0, 0.0]);
        for i in 0..1000 {
            sound.adjust_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]);
    }

    #[test]
    fn bench() {
        use rodio::Source;

        let scene = AmbisonicBuilder::default().build();

        let mut f: u64 = 1;
        for _ in 0..850 {
            f = (f + f * f * 7 + f * f * f * 3 + 1) % 800;
            let source = rodio::source::SineWave::new(440).amplify(0.001);
            let _ = scene.play(source);
        }

        sleep(Duration::from_secs(10));
    }

    #[test]
    fn hrir() {
        let cfg = HrtfConfig::default();
        let scene = AmbisonicBuilder::default().with_config(cfg.into()).build();

        let source = sources::Noise::new(48000);

        let mut sound = scene.play(source);

        for i in 0..1000 {
            sound.adjust_position([(500 - i) as f32 / 10.0, 1.0, 0.0]);
            sleep(Duration::from_millis(10));
        }
    }
}