├── .gitattributes ├── .github └── workflows │ ├── clippy.yml │ └── run-tests.yml ├── .gitignore ├── .prettierrc.json ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── changelog.md ├── crates ├── benchmarks │ ├── Cargo.toml │ └── benches │ │ └── benchmarks.rs ├── examples │ ├── assets │ │ ├── blip.ogg │ │ ├── drums.ogg │ │ ├── dynamic │ │ │ ├── arp.ogg │ │ │ ├── bass.ogg │ │ │ ├── drums.ogg │ │ │ ├── lead.ogg │ │ │ └── pad.ogg │ │ ├── score.ogg │ │ └── sine.wav │ ├── dynamic-music │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── ghost-noise │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── metronome │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── score-counter │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── seamless-loop-with-intro │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── simple-sound-playback │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── spatial-audio │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs └── kira │ ├── Cargo.toml │ ├── src │ ├── backend.rs │ ├── backend │ │ ├── cpal.rs │ │ ├── cpal │ │ │ ├── desktop.rs │ │ │ ├── desktop │ │ │ │ ├── renderer_with_cpu_usage.rs │ │ │ │ ├── stream_manager.rs │ │ │ │ └── stream_manager │ │ │ │ │ └── send_on_drop.rs │ │ │ ├── error.rs │ │ │ └── wasm.rs │ │ ├── mock.rs │ │ ├── renderer.rs │ │ ├── resources.rs │ │ └── resources │ │ │ ├── clocks.rs │ │ │ ├── listeners.rs │ │ │ ├── mixer.rs │ │ │ ├── modulators.rs │ │ │ └── test.rs │ ├── clock.rs │ ├── clock │ │ ├── clock_speed.rs │ │ ├── handle.rs │ │ ├── test.rs │ │ ├── time.rs │ │ └── time │ │ │ └── test.rs │ ├── command.rs │ ├── decibels.rs │ ├── effect.rs │ ├── effect │ │ ├── compressor.rs │ │ ├── compressor │ │ │ ├── builder.rs │ │ │ └── handle.rs │ │ ├── delay.rs │ │ ├── delay │ │ │ ├── builder.rs │ │ │ └── handle.rs │ │ ├── distortion.rs │ │ ├── distortion │ │ │ ├── builder.rs │ │ │ └── handle.rs │ │ ├── eq_filter.rs │ │ ├── eq_filter │ │ │ ├── builder.rs │ │ │ └── handle.rs │ │ ├── filter.rs │ │ ├── filter │ │ │ ├── builder.rs │ │ │ └── handle.rs │ │ ├── panning_control.rs │ │ ├── panning_control │ │ │ ├── builder.rs │ │ │ └── handle.rs │ │ ├── reverb.rs │ │ ├── reverb │ │ │ ├── all_pass.rs │ │ │ ├── builder.rs │ │ │ ├── comb.rs │ │ │ └── handle.rs │ │ ├── volume_control.rs │ │ └── volume_control │ │ │ ├── builder.rs │ │ │ └── handle.rs │ ├── error.rs │ ├── frame.rs │ ├── info.rs │ ├── lib.rs │ ├── listener.rs │ ├── listener │ │ └── handle.rs │ ├── manager.rs │ ├── manager │ │ └── settings.rs │ ├── mix.rs │ ├── modulator.rs │ ├── modulator │ │ ├── lfo.rs │ │ ├── lfo │ │ │ ├── builder.rs │ │ │ ├── handle.rs │ │ │ └── test.rs │ │ ├── tweener.rs │ │ └── tweener │ │ │ ├── builder.rs │ │ │ ├── handle.rs │ │ │ └── test.rs │ ├── panning.rs │ ├── parameter.rs │ ├── parameter │ │ └── test.rs │ ├── playback_rate.rs │ ├── playback_state_manager.rs │ ├── semitones.rs │ ├── sound.rs │ ├── sound │ │ ├── error.rs │ │ ├── playback_position.rs │ │ ├── playback_position │ │ │ └── test.rs │ │ ├── static_sound.rs │ │ ├── static_sound │ │ │ ├── data.rs │ │ │ ├── data │ │ │ │ ├── from_file.rs │ │ │ │ └── test.rs │ │ │ ├── handle.rs │ │ │ ├── settings.rs │ │ │ ├── sound.rs │ │ │ └── sound │ │ │ │ ├── resampler.rs │ │ │ │ └── test.rs │ │ ├── streaming.rs │ │ ├── streaming │ │ │ ├── data.rs │ │ │ ├── data │ │ │ │ └── test.rs │ │ │ ├── decoder.rs │ │ │ ├── decoder │ │ │ │ ├── mock.rs │ │ │ │ └── symphonia.rs │ │ │ ├── handle.rs │ │ │ ├── settings.rs │ │ │ ├── sound.rs │ │ │ └── sound │ │ │ │ ├── decode_scheduler.rs │ │ │ │ └── test.rs │ │ ├── symphonia.rs │ │ ├── transport.rs │ │ └── transport │ │ │ └── test.rs │ ├── start_time.rs │ ├── test_helpers.rs │ ├── track.rs │ ├── track │ │ ├── main.rs │ │ ├── main │ │ │ ├── builder.rs │ │ │ └── handle.rs │ │ ├── send.rs │ │ ├── send │ │ │ ├── builder.rs │ │ │ └── handle.rs │ │ ├── sub.rs │ │ └── sub │ │ │ ├── builder.rs │ │ │ ├── handle.rs │ │ │ ├── spatial_builder.rs │ │ │ └── spatial_handle.rs │ ├── tween.rs │ ├── tween │ │ └── tweenable.rs │ └── value.rs │ └── tests │ ├── change_sample_rate.rs │ ├── streaming_sound_stops_on_error.rs │ └── sync_send.rs ├── readme.md ├── rustfmt.toml └── todo.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Clippy 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | clippy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: install cpal dependencies 15 | run: sudo apt-get install libasound2-dev 16 | - name: install wasm32 target 17 | run: rustup target add wasm32-unknown-unknown 18 | - name: clippy (desktop - no features) 19 | uses: actions-rs/clippy-check@v1 20 | with: 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | args: -p kira --no-default-features 23 | name: desktop - no features 24 | - name: clippy (desktop - with cpal feature) 25 | uses: actions-rs/clippy-check@v1 26 | with: 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | args: -p kira --no-default-features --features=cpal 29 | name: desktop - with cpal feature 30 | - name: clippy (desktop - with mp3 feature) 31 | uses: actions-rs/clippy-check@v1 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | args: -p kira --no-default-features --features=mp3 35 | name: desktop - with mp3 feature 36 | - name: clippy (desktop - with all features) 37 | uses: actions-rs/clippy-check@v1 38 | with: 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | args: -p kira --all-features 41 | name: desktop - with all features 42 | - name: clippy (wasm32 - no features) 43 | uses: actions-rs/clippy-check@v1 44 | with: 45 | token: ${{ secrets.GITHUB_TOKEN }} 46 | args: -p kira --no-default-features --target=wasm32-unknown-unknown 47 | name: wasm32 - no features 48 | - name: clippy (wasm32 - with cpal feature) 49 | uses: actions-rs/clippy-check@v1 50 | with: 51 | token: ${{ secrets.GITHUB_TOKEN }} 52 | args: -p kira --no-default-features --features=cpal --target=wasm32-unknown-unknown 53 | name: wasm32 - with cpal feature 54 | - name: clippy (wasm32 - with mp3 feature) 55 | uses: actions-rs/clippy-check@v1 56 | with: 57 | token: ${{ secrets.GITHUB_TOKEN }} 58 | args: -p kira --no-default-features --features=mp3 --target=wasm32-unknown-unknown 59 | name: wasm32 - with mp3 feature 60 | - name: clippy (wasm32 - with all features) 61 | uses: actions-rs/clippy-check@v1 62 | with: 63 | token: ${{ secrets.GITHUB_TOKEN }} 64 | args: -p kira --all-features 65 | name: wasm32 - with all features 66 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: install cpal dependencies 15 | run: sudo apt-get install libasound2-dev 16 | - name: run tests (desktop - all features) 17 | run: cargo test -p kira --all-features 18 | - name: run tests (desktop - no features) 19 | run: cargo test -p kira --no-default-features --lib 20 | - name: run tests (desktop - with cpal feature) 21 | run: cargo test -p kira --no-default-features --features=cpal --lib 22 | - name: run tests (desktop - with mp3 feature) 23 | run: cargo test -p kira --no-default-features --features=mp3 --lib 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "always" 3 | } 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/kira", 4 | "crates/benchmarks", 5 | "crates/examples/dynamic-music", 6 | "crates/examples/ghost-noise", 7 | "crates/examples/metronome", 8 | "crates/examples/score-counter", 9 | "crates/examples/seamless-loop-with-intro", 10 | "crates/examples/simple-sound-playback", 11 | "crates/examples/spatial-audio", 12 | ] 13 | resolver = "2" 14 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2021 Andrew Minnich 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /crates/benchmarks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benchmarks" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | kira = { path = "../kira" } 10 | 11 | [dev-dependencies] 12 | criterion = "0.5.1" 13 | 14 | [[bench]] 15 | name = "benchmarks" 16 | harness = false 17 | -------------------------------------------------------------------------------- /crates/benchmarks/benches/benchmarks.rs: -------------------------------------------------------------------------------- 1 | use std::{f32::consts::TAU, sync::Arc}; 2 | 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | use kira::{ 5 | backend::mock::{MockBackend, MockBackendSettings}, 6 | sound::static_sound::{StaticSoundData, StaticSoundSettings}, 7 | track::MainTrackBuilder, 8 | AudioManager, AudioManagerSettings, Frame, 9 | }; 10 | 11 | fn create_test_sound(num_samples: usize) -> StaticSoundData { 12 | const SAMPLE_RATE: u32 = 48_000; 13 | let mut frames = vec![]; 14 | let mut phase = 0.0; 15 | for _ in 0..num_samples { 16 | frames.push(Frame::from_mono((phase * TAU).sin())); 17 | phase += 440.0 / SAMPLE_RATE as f32; 18 | } 19 | StaticSoundData { 20 | sample_rate: SAMPLE_RATE, 21 | frames: Arc::from(frames), 22 | settings: StaticSoundSettings::new().loop_region(0.0..), 23 | slice: None, 24 | } 25 | } 26 | 27 | fn sounds(c: &mut Criterion) { 28 | // a simple test case where many sounds are being played at once 29 | c.bench_function("simple", |b| { 30 | const SAMPLE_RATE: u32 = 48_000; 31 | const NUM_SOUNDS: usize = 5000; 32 | let mut manager = AudioManager::::new(AudioManagerSettings { 33 | backend_settings: MockBackendSettings { 34 | sample_rate: SAMPLE_RATE, 35 | }, 36 | main_track_builder: MainTrackBuilder::new().sound_capacity(NUM_SOUNDS), 37 | ..Default::default() 38 | }) 39 | .unwrap(); 40 | let sound_data = create_test_sound(SAMPLE_RATE as usize); 41 | for _ in 0..NUM_SOUNDS { 42 | manager.play(sound_data.clone()).unwrap(); 43 | } 44 | manager.backend_mut().on_start_processing(); 45 | b.iter(|| manager.backend_mut().process()); 46 | }); 47 | 48 | // similar to "simple", but also periodically calls the 49 | // on_start_processing callback to measure its relative 50 | // impact on the performance 51 | c.bench_function("with on_start_processing callback", |b| { 52 | const SAMPLE_RATE: u32 = 48_000; 53 | const NUM_SOUNDS: usize = 5000; 54 | let mut manager = AudioManager::::new(AudioManagerSettings { 55 | backend_settings: MockBackendSettings { 56 | sample_rate: SAMPLE_RATE, 57 | }, 58 | main_track_builder: MainTrackBuilder::new().sound_capacity(NUM_SOUNDS), 59 | ..Default::default() 60 | }) 61 | .unwrap(); 62 | let sound_data = create_test_sound(SAMPLE_RATE as usize); 63 | for _ in 0..NUM_SOUNDS { 64 | manager.play(sound_data.clone()).unwrap(); 65 | } 66 | b.iter(|| { 67 | manager.backend_mut().on_start_processing(); 68 | manager.backend_mut().process(); 69 | }); 70 | }); 71 | } 72 | 73 | criterion_group!(benches, sounds); 74 | criterion_main!(benches); 75 | -------------------------------------------------------------------------------- /crates/examples/assets/blip.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/kira/6f6ff10726634d407226c3f0bd3613fd020c8da7/crates/examples/assets/blip.ogg -------------------------------------------------------------------------------- /crates/examples/assets/drums.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/kira/6f6ff10726634d407226c3f0bd3613fd020c8da7/crates/examples/assets/drums.ogg -------------------------------------------------------------------------------- /crates/examples/assets/dynamic/arp.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/kira/6f6ff10726634d407226c3f0bd3613fd020c8da7/crates/examples/assets/dynamic/arp.ogg -------------------------------------------------------------------------------- /crates/examples/assets/dynamic/bass.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/kira/6f6ff10726634d407226c3f0bd3613fd020c8da7/crates/examples/assets/dynamic/bass.ogg -------------------------------------------------------------------------------- /crates/examples/assets/dynamic/drums.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/kira/6f6ff10726634d407226c3f0bd3613fd020c8da7/crates/examples/assets/dynamic/drums.ogg -------------------------------------------------------------------------------- /crates/examples/assets/dynamic/lead.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/kira/6f6ff10726634d407226c3f0bd3613fd020c8da7/crates/examples/assets/dynamic/lead.ogg -------------------------------------------------------------------------------- /crates/examples/assets/dynamic/pad.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/kira/6f6ff10726634d407226c3f0bd3613fd020c8da7/crates/examples/assets/dynamic/pad.ogg -------------------------------------------------------------------------------- /crates/examples/assets/score.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/kira/6f6ff10726634d407226c3f0bd3613fd020c8da7/crates/examples/assets/score.ogg -------------------------------------------------------------------------------- /crates/examples/assets/sine.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/kira/6f6ff10726634d407226c3f0bd3613fd020c8da7/crates/examples/assets/sine.wav -------------------------------------------------------------------------------- /crates/examples/dynamic-music/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dynamic-music" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | kira = { path = "../../kira" } 8 | -------------------------------------------------------------------------------- /crates/examples/dynamic-music/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::stdin, time::Duration}; 2 | 3 | use kira::{ 4 | backend::DefaultBackend, 5 | effect::{filter::FilterBuilder, reverb::ReverbBuilder}, 6 | modulator::tweener::TweenerBuilder, 7 | sound::static_sound::{StaticSoundData, StaticSoundSettings}, 8 | track::TrackBuilder, 9 | AudioManager, AudioManagerSettings, Decibels, Easing, Mapping, Mix, Tween, Value, 10 | }; 11 | 12 | fn main() -> Result<(), Box> { 13 | let mut manager = AudioManager::::new(AudioManagerSettings::default())?; 14 | // create the tweener that will be used to change multiple parameters at once 15 | let mut underwater_tweener = manager.add_modulator(TweenerBuilder { initial_value: 0.0 })?; 16 | // create a mixer track for the lead instrument. this has a filter and reverb that become 17 | // more prominent as the tweener value reaches 1.0. 18 | let mut lead_track = manager.add_sub_track( 19 | TrackBuilder::new() 20 | .with_effect(FilterBuilder::new().cutoff(Value::from_modulator( 21 | &underwater_tweener, 22 | Mapping { 23 | input_range: (0.0, 1.0), 24 | output_range: (20_000.0, 2000.0), 25 | easing: Easing::Linear, 26 | }, 27 | ))) 28 | .with_effect(ReverbBuilder::new().mix(Value::from_modulator( 29 | &underwater_tweener, 30 | Mapping { 31 | input_range: (0.0, 1.0), 32 | output_range: (Mix::DRY, Mix(1.0 / 3.0)), 33 | easing: Easing::Linear, 34 | }, 35 | ))), 36 | )?; 37 | // set a loop region (used for all the sounds, since they're the same length) 38 | let music_duration = 21.0 + 1.0 / 3.0; 39 | let common_sound_settings = 40 | StaticSoundSettings::new().loop_region(music_duration / 2.0..music_duration); 41 | // load the sounds, linking the volumes to the tweener when appropriate 42 | let arp = StaticSoundData::from_file("crates/examples/assets/dynamic/arp.ogg")? 43 | .with_settings(common_sound_settings); 44 | let bass = StaticSoundData::from_file("crates/examples/assets/dynamic/bass.ogg")? 45 | .with_settings(common_sound_settings) 46 | .volume(Value::from_modulator( 47 | &underwater_tweener, 48 | Mapping { 49 | input_range: (0.0, 1.0), 50 | output_range: (Decibels::IDENTITY, Decibels::SILENCE), 51 | easing: Easing::Linear, 52 | }, 53 | )); 54 | let drums = StaticSoundData::from_file("crates/examples/assets/dynamic/drums.ogg")? 55 | .with_settings(common_sound_settings) 56 | .volume(Value::from_modulator( 57 | &underwater_tweener, 58 | Mapping { 59 | input_range: (0.0, 1.0), 60 | output_range: (Decibels::IDENTITY, Decibels::SILENCE), 61 | easing: Easing::Linear, 62 | }, 63 | )); 64 | let lead = StaticSoundData::from_file("crates/examples/assets/dynamic/lead.ogg")? 65 | .with_settings(common_sound_settings); 66 | let pad = StaticSoundData::from_file("crates/examples/assets/dynamic/pad.ogg")? 67 | .with_settings(common_sound_settings) 68 | .volume(Value::from_modulator( 69 | &underwater_tweener, 70 | Mapping { 71 | input_range: (0.0, 1.0), 72 | output_range: (Decibels::SILENCE, Decibels::IDENTITY), 73 | easing: Easing::Linear, 74 | }, 75 | )); 76 | // play the sounds 77 | manager.play(arp)?; 78 | manager.play(bass)?; 79 | manager.play(drums)?; 80 | lead_track.play(lead)?; 81 | manager.play(pad)?; 82 | 83 | println!("Press enter to switch music variations"); 84 | let mut underwater = false; 85 | loop { 86 | wait_for_enter_press()?; 87 | underwater = !underwater; 88 | underwater_tweener.set( 89 | if underwater { 1.0 } else { 0.0 }, 90 | Tween { 91 | duration: Duration::from_secs(3), 92 | ..Default::default() 93 | }, 94 | ); 95 | if underwater { 96 | println!("submerging..."); 97 | } else { 98 | println!("resurfacing..."); 99 | } 100 | } 101 | } 102 | 103 | fn wait_for_enter_press() -> Result<(), Box> { 104 | stdin().read_line(&mut "".into())?; 105 | Ok(()) 106 | } 107 | -------------------------------------------------------------------------------- /crates/examples/ghost-noise/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ghost-noise" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | kira = { path = "../../kira" } 8 | -------------------------------------------------------------------------------- /crates/examples/ghost-noise/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::stdin}; 2 | 3 | use kira::{ 4 | backend::DefaultBackend, modulator::lfo::LfoBuilder, sound::static_sound::StaticSoundData, 5 | AudioManager, AudioManagerSettings, Easing, Mapping, Semitones, Value, 6 | }; 7 | 8 | fn main() -> Result<(), Box> { 9 | let mut manager = AudioManager::::new(AudioManagerSettings::default())?; 10 | let amplitude_lfo = manager.add_modulator(LfoBuilder::new().frequency(0.093))?; 11 | let frequency_lfo = manager.add_modulator(LfoBuilder::new().frequency(0.038))?; 12 | let playback_rate_lfo = manager.add_modulator( 13 | LfoBuilder::new() 14 | .amplitude(Value::from_modulator( 15 | &litude_lfo, 16 | Mapping { 17 | input_range: (-1.0, 1.0), 18 | output_range: (0.5, 1.5), 19 | easing: Easing::Linear, 20 | }, 21 | )) 22 | .frequency(Value::from_modulator( 23 | &frequency_lfo, 24 | Mapping { 25 | input_range: (-1.0, 1.0), 26 | output_range: (1.0, 4.0), 27 | easing: Easing::Linear, 28 | }, 29 | )), 30 | )?; 31 | manager.play( 32 | StaticSoundData::from_file("crates/examples/assets/sine.wav")? 33 | .volume(1.0 / 3.0) 34 | .loop_region(..) 35 | .playback_rate(Value::from_modulator( 36 | &playback_rate_lfo, 37 | Mapping { 38 | input_range: (-1.0, 1.0), 39 | output_range: (Semitones(56.0).into(), Semitones(64.0).into()), 40 | easing: Easing::Linear, 41 | }, 42 | )), 43 | )?; 44 | 45 | println!("oooOOOOooOOOOooo"); 46 | wait_for_enter_press()?; 47 | Ok(()) 48 | } 49 | 50 | fn wait_for_enter_press() -> Result<(), Box> { 51 | stdin().read_line(&mut "".into())?; 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /crates/examples/metronome/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metronome" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | kira = { path = "../../kira" } 8 | -------------------------------------------------------------------------------- /crates/examples/metronome/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::stdin, time::Duration}; 2 | 3 | use kira::{ 4 | backend::DefaultBackend, 5 | clock::{ClockSpeed, ClockTime}, 6 | sound::static_sound::StaticSoundData, 7 | AudioManager, AudioManagerSettings, 8 | }; 9 | 10 | fn main() -> Result<(), Box> { 11 | let mut manager = AudioManager::::new(AudioManagerSettings::default())?; 12 | let sound_data = StaticSoundData::from_file("crates/examples/assets/blip.ogg")?; 13 | let mut clock = manager.add_clock(ClockSpeed::TicksPerMinute(120.0))?; 14 | // queue up the first 2 metronome clicks 15 | manager.play(sound_data.playback_rate(2.0).start_time(ClockTime { 16 | clock: clock.id(), 17 | ticks: 0, 18 | fraction: 0.0, 19 | }))?; 20 | manager.play(sound_data.playback_rate(1.0).start_time(ClockTime { 21 | clock: clock.id(), 22 | ticks: 1, 23 | fraction: 0.0, 24 | }))?; 25 | 26 | println!("Press enter to start the metronome"); 27 | wait_for_enter_press()?; 28 | clock.start(); 29 | 30 | let mut previous_clock_time = clock.time(); 31 | loop { 32 | std::thread::sleep(Duration::from_millis(10)); 33 | let current_clock_time = clock.time(); 34 | if current_clock_time.ticks > previous_clock_time.ticks { 35 | // whenever the clock ticks, queue up a metronome click for the next tick 36 | let playback_rate = if is_next_tick_beginning_of_measure(current_clock_time) { 37 | 2.0 38 | } else { 39 | 1.0 40 | }; 41 | manager.play( 42 | sound_data 43 | .playback_rate(playback_rate) 44 | .start_time(clock.time() + 1), 45 | )?; 46 | previous_clock_time = current_clock_time; 47 | } 48 | } 49 | } 50 | 51 | fn is_next_tick_beginning_of_measure(current_clock_time: ClockTime) -> bool { 52 | (current_clock_time.ticks + 1) % 4 == 0 53 | } 54 | 55 | fn wait_for_enter_press() -> Result<(), Box> { 56 | stdin().read_line(&mut "".into())?; 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /crates/examples/score-counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "score-counter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | kira = { path = "../../kira" } 8 | -------------------------------------------------------------------------------- /crates/examples/score-counter/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::stdin}; 2 | 3 | use kira::{ 4 | backend::DefaultBackend, sound::static_sound::StaticSoundData, AudioManager, 5 | AudioManagerSettings, 6 | }; 7 | 8 | fn main() -> Result<(), Box> { 9 | let mut manager = AudioManager::::new(AudioManagerSettings::default())?; 10 | let sound_data = StaticSoundData::from_file("crates/examples/assets/score.ogg")? 11 | .playback_rate(1.5) 12 | .loop_region(..0.06); 13 | 14 | loop { 15 | println!("Press enter to start a looping score counter sound"); 16 | wait_for_enter_press()?; 17 | let mut sound = manager.play(sound_data.clone())?; 18 | 19 | println!("Press enter to finish the score counter"); 20 | wait_for_enter_press()?; 21 | sound.set_loop_region(None); 22 | } 23 | } 24 | 25 | fn wait_for_enter_press() -> Result<(), Box> { 26 | stdin().read_line(&mut "".into())?; 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /crates/examples/seamless-loop-with-intro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "seamless-loop-with-intro" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | kira = { path = "../../kira" } 8 | -------------------------------------------------------------------------------- /crates/examples/seamless-loop-with-intro/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::stdin}; 2 | 3 | use kira::{ 4 | backend::DefaultBackend, sound::static_sound::StaticSoundData, AudioManager, 5 | AudioManagerSettings, 6 | }; 7 | 8 | fn main() -> Result<(), Box> { 9 | let mut manager = AudioManager::::new(AudioManagerSettings::default())?; 10 | manager.play( 11 | StaticSoundData::from_file("crates/examples/assets/drums.ogg")?.loop_region(3.6..6.0), 12 | )?; 13 | 14 | println!("Press enter to exit"); 15 | wait_for_enter_press()?; 16 | 17 | Ok(()) 18 | } 19 | 20 | fn wait_for_enter_press() -> Result<(), Box> { 21 | stdin().read_line(&mut "".into())?; 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /crates/examples/simple-sound-playback/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-sound-playback" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | kira = { path = "../../kira" } 8 | -------------------------------------------------------------------------------- /crates/examples/simple-sound-playback/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, io::stdin}; 2 | 3 | use kira::{ 4 | backend::DefaultBackend, sound::static_sound::StaticSoundData, AudioManager, 5 | AudioManagerSettings, 6 | }; 7 | 8 | fn main() -> Result<(), Box> { 9 | let mut manager = AudioManager::::new(AudioManagerSettings::default())?; 10 | let sound_data = StaticSoundData::from_file("crates/examples/assets/blip.ogg")?; 11 | 12 | println!("Press enter to play a sound"); 13 | loop { 14 | wait_for_enter_press()?; 15 | manager.play(sound_data.clone())?; 16 | } 17 | } 18 | 19 | fn wait_for_enter_press() -> Result<(), Box> { 20 | stdin().read_line(&mut "".into())?; 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /crates/examples/spatial-audio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spatial-audio" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | glam = { version = "0.27", features = ["mint"] } 10 | kira = { path = "../../kira" } 11 | macroquad = { version = "0.4.13", default-features = false } 12 | -------------------------------------------------------------------------------- /crates/examples/spatial-audio/src/main.rs: -------------------------------------------------------------------------------- 1 | use kira::{ 2 | backend::DefaultBackend, 3 | effect::{filter::FilterBuilder, reverb::ReverbBuilder}, 4 | sound::static_sound::StaticSoundData, 5 | track::{SendTrackBuilder, SpatialTrackBuilder}, 6 | AudioManager, AudioManagerSettings, Decibels, Easing, Mapping, Mix, Tween, Value, 7 | }; 8 | use macroquad::prelude::*; 9 | 10 | const MOVE_SPEED: f32 = 6.0; 11 | const LOOK_SPEED: f32 = 0.005; 12 | const WORLD_UP: Vec3 = vec3(0.0, 1.0, 0.0); 13 | const SPATIAL_TRACK_POSITION: Vec3 = vec3(0.0, 1.0, -6.0); 14 | 15 | fn conf() -> Conf { 16 | Conf { 17 | window_title: String::from("Macroquad"), 18 | window_width: 1260, 19 | window_height: 768, 20 | fullscreen: false, 21 | ..Default::default() 22 | } 23 | } 24 | 25 | #[macroquad::main(conf)] 26 | async fn main() { 27 | let mut camera_controller = CameraController::new(); 28 | 29 | let mut last_mouse_position: Vec2 = mouse_position().into(); 30 | 31 | let mut audio_manager = 32 | AudioManager::::new(AudioManagerSettings::default()).unwrap(); 33 | let mut listener = audio_manager 34 | .add_listener(camera_controller.position, camera_controller.orientation()) 35 | .unwrap(); 36 | let reverb_send = audio_manager 37 | .add_send_track( 38 | SendTrackBuilder::new().with_effect(ReverbBuilder::new().mix(Mix::WET).damping(0.5)), 39 | ) 40 | .unwrap(); 41 | let mut spatial_track = audio_manager 42 | .add_spatial_sub_track( 43 | &listener, 44 | SPATIAL_TRACK_POSITION, 45 | SpatialTrackBuilder::new() 46 | .with_effect( 47 | FilterBuilder::new().cutoff(Value::FromListenerDistance(Mapping { 48 | input_range: (0.0, 100.0), 49 | output_range: (18000.0, 2000.0), 50 | easing: Easing::Linear, 51 | })), 52 | ) 53 | .with_send( 54 | &reverb_send, 55 | Value::FromListenerDistance(Mapping { 56 | input_range: (0.0, 100.0), 57 | output_range: (Decibels(-12.0), Decibels(24.0)), 58 | easing: Easing::Linear, 59 | }), 60 | ), 61 | ) 62 | .unwrap(); 63 | spatial_track 64 | .play( 65 | StaticSoundData::from_file("crates/examples/assets/blip.ogg") 66 | .unwrap() 67 | .loop_region(..), 68 | ) 69 | .unwrap(); 70 | 71 | loop { 72 | let delta_time = get_frame_time(); 73 | 74 | if is_key_pressed(KeyCode::Escape) { 75 | break; 76 | } 77 | 78 | let mouse_position: Vec2 = mouse_position().into(); 79 | let mouse_delta = mouse_position - last_mouse_position; 80 | last_mouse_position = mouse_position; 81 | camera_controller.update(delta_time, mouse_delta); 82 | listener.set_position(camera_controller.position, Tween::default()); 83 | listener.set_orientation(camera_controller.orientation(), Tween::default()); 84 | 85 | clear_background(LIGHTGRAY); 86 | 87 | // Going 3d! 88 | 89 | set_camera(&camera_controller.camera()); 90 | 91 | draw_grid(20, 1., BLACK, GRAY); 92 | 93 | draw_cube_wires(SPATIAL_TRACK_POSITION, vec3(2., 2., 2.), GREEN); 94 | 95 | // Back to screen space, render some text 96 | 97 | set_default_camera(); 98 | 99 | next_frame().await 100 | } 101 | } 102 | 103 | struct CameraController { 104 | position: Vec3, 105 | yaw: f32, 106 | pitch: f32, 107 | } 108 | 109 | impl CameraController { 110 | fn new() -> Self { 111 | Self { 112 | position: vec3(0.0, 1.0, 0.0), 113 | yaw: 0.0, 114 | pitch: 0.0, 115 | } 116 | } 117 | 118 | fn update(&mut self, delta_time: f32, mouse_delta: Vec2) { 119 | if is_key_down(KeyCode::Up) || is_key_down(KeyCode::W) { 120 | self.position += self.front() * delta_time * MOVE_SPEED; 121 | } 122 | if is_key_down(KeyCode::Down) || is_key_down(KeyCode::S) { 123 | self.position -= self.front() * delta_time * MOVE_SPEED; 124 | } 125 | if is_key_down(KeyCode::Left) || is_key_down(KeyCode::A) { 126 | self.position -= self.right() * delta_time * MOVE_SPEED; 127 | } 128 | if is_key_down(KeyCode::Right) || is_key_down(KeyCode::D) { 129 | self.position += self.right() * delta_time * MOVE_SPEED; 130 | } 131 | self.yaw -= mouse_delta.x * LOOK_SPEED; 132 | self.pitch -= mouse_delta.y * LOOK_SPEED; 133 | self.pitch = self.pitch.clamp(-1.5, 1.5); 134 | } 135 | 136 | fn orientation(&self) -> Quat { 137 | Quat::from_euler(EulerRot::XYZ, self.pitch, self.yaw, 0.0) 138 | } 139 | 140 | fn camera(&self) -> Camera3D { 141 | Camera3D { 142 | position: self.position, 143 | target: self.position + self.front(), 144 | up: WORLD_UP, 145 | ..Default::default() 146 | } 147 | } 148 | 149 | fn front(&self) -> Vec3 { 150 | (Quat::from_euler(EulerRot::XYZ, self.pitch, self.yaw, 0.0) * Vec3::new(0.0, 0.0, -1.0)) 151 | .normalize() 152 | } 153 | 154 | fn right(&self) -> Vec3 { 155 | self.front().cross(WORLD_UP).normalize() 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /crates/kira/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kira" 3 | version = "0.10.7" 4 | authors = ["Andrew Minnich "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | description = "Expressive audio library for games" 8 | repository = "https://github.com/tesselode/kira" 9 | keywords = ["gamedev", "audio", "music"] 10 | categories = ["game-development", "multimedia::audio"] 11 | readme = "../../README.md" 12 | 13 | [dependencies] 14 | assert_no_alloc = { version = "1.1.2", optional = true } 15 | atomic-arena = "0.1.1" 16 | glam = { version = "0.30.0", features = ["mint"] } 17 | mint = "0.5.9" 18 | paste = "1.0.14" 19 | rtrb = "0.3.1" 20 | serde = { version = "1.0.164", features = ["derive"], optional = true } 21 | symphonia = { version = "0.5.0", optional = true, default-features = false } 22 | triple_buffer = "8.1.0" 23 | 24 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.cpal] 25 | version = "0.15.1" 26 | optional = true 27 | 28 | [target.'cfg(target_arch = "wasm32")'.dependencies.cpal] 29 | version = "0.15.1" 30 | optional = true 31 | features = ["wasm-bindgen"] 32 | 33 | [target.'cfg(target_arch = "wasm32")'.dependencies] 34 | send_wrapper = "0.6.0" 35 | 36 | [features] 37 | default = ["cpal", "mp3", "ogg", "flac", "wav"] 38 | mp3 = ["symphonia", "symphonia/mp3"] 39 | ogg = ["symphonia", "symphonia/ogg", "symphonia/vorbis"] 40 | flac = ["symphonia", "symphonia/flac"] 41 | wav = ["symphonia", "symphonia/wav", "symphonia/pcm"] 42 | android_shared_stdcxx = ["cpal/oboe-shared-stdcxx"] 43 | 44 | [dev-dependencies] 45 | approx = "0.5.1" 46 | 47 | # docs.rs-specific configuration 48 | [package.metadata.docs.rs] 49 | # document all features 50 | all-features = true 51 | # defines the configuration attribute `docsrs` 52 | rustdoc-args = ["--cfg", "docsrs"] 53 | -------------------------------------------------------------------------------- /crates/kira/src/backend.rs: -------------------------------------------------------------------------------- 1 | //! Communication between Kira and a low-level audio API. 2 | 3 | #[cfg(feature = "cpal")] 4 | pub mod cpal; 5 | pub mod mock; 6 | mod renderer; 7 | pub(crate) mod resources; 8 | 9 | pub use renderer::*; 10 | 11 | #[cfg(feature = "cpal")] 12 | /// The default backend used by [`AudioManager`](crate::AudioManager)s. 13 | /// 14 | /// If the `cpal` feature is enabled, this will be the cpal backend. Otherwise, 15 | /// it will be the mock backend. 16 | pub type DefaultBackend = cpal::CpalBackend; 17 | #[cfg(not(feature = "cpal"))] 18 | /// The default backend used by [`AudioManager`](crate::AudioManager)s. 19 | /// 20 | /// If the `cpal` feature is enabled, this will be the cpal backend. Otherwise, 21 | /// it will be the mock backend. 22 | pub type DefaultBackend = mock::MockBackend; 23 | 24 | /// Connects a [`Renderer`] to a lower level audio API. 25 | pub trait Backend: Sized { 26 | /// Settings for this backend. 27 | type Settings; 28 | 29 | /// Errors that can occur when using this backend. 30 | type Error; 31 | 32 | /// Starts the backend and returns itself and the initial sample rate. 33 | fn setup( 34 | settings: Self::Settings, 35 | internal_buffer_size: usize, 36 | ) -> Result<(Self, u32), Self::Error>; 37 | 38 | /// Sends the renderer to the backend to start audio playback. 39 | fn start(&mut self, renderer: Renderer) -> Result<(), Self::Error>; 40 | } 41 | -------------------------------------------------------------------------------- /crates/kira/src/backend/cpal.rs: -------------------------------------------------------------------------------- 1 | //! Plays audio using [cpal](https://crates.io/crates/cpal). 2 | 3 | #![cfg_attr(docsrs, doc(cfg(feature = "cpal")))] 4 | 5 | mod error; 6 | use cpal::{BufferSize, Device}; 7 | pub use error::*; 8 | 9 | /// Settings for the cpal backend. 10 | pub struct CpalBackendSettings { 11 | /// The output audio device to use. If [`None`], the default output 12 | /// device will be used. 13 | pub device: Option, 14 | /// The buffer size used by the device. If it is set to [`BufferSize::Default`], 15 | /// the default buffer size for the device will be used. Note that the default 16 | /// buffer size might be surprisingly large, leading to latency issues. If 17 | /// a lower latency is desired, consider using [`BufferSize::Fixed`] in accordance 18 | /// with the [`cpal::SupportedBufferSize`] range provided by the [`cpal::SupportedStreamConfig`] 19 | /// API. 20 | pub buffer_size: BufferSize, 21 | } 22 | 23 | impl Default for CpalBackendSettings { 24 | fn default() -> Self { 25 | Self { 26 | device: None, 27 | buffer_size: BufferSize::Default, 28 | } 29 | } 30 | } 31 | 32 | #[cfg(target_arch = "wasm32")] 33 | mod wasm; 34 | #[cfg(target_arch = "wasm32")] 35 | pub use wasm::CpalBackend; 36 | 37 | #[cfg(not(target_arch = "wasm32"))] 38 | mod desktop; 39 | #[cfg(not(target_arch = "wasm32"))] 40 | pub use desktop::CpalBackend; 41 | -------------------------------------------------------------------------------- /crates/kira/src/backend/cpal/desktop.rs: -------------------------------------------------------------------------------- 1 | mod renderer_with_cpu_usage; 2 | mod stream_manager; 3 | 4 | use std::sync::Mutex; 5 | 6 | use renderer_with_cpu_usage::RendererWithCpuUsage; 7 | use rtrb::Consumer; 8 | use stream_manager::{StreamManager, StreamManagerController}; 9 | 10 | use crate::backend::{Backend, Renderer}; 11 | use cpal::{ 12 | traits::{DeviceTrait, HostTrait}, 13 | BufferSize, Device, StreamConfig, StreamError, 14 | }; 15 | 16 | use super::{CpalBackendSettings, Error}; 17 | 18 | enum State { 19 | Empty, 20 | Uninitialized { 21 | device: Device, 22 | config: StreamConfig, 23 | }, 24 | Initialized { 25 | stream_manager_controller: StreamManagerController, 26 | }, 27 | } 28 | 29 | /// A backend that uses [cpal](https://crates.io/crates/cpal) to 30 | /// connect a [`Renderer`] to the operating system's audio driver. 31 | pub struct CpalBackend { 32 | state: State, 33 | /// Whether the device was specified by the user. 34 | custom_device: bool, 35 | buffer_size: BufferSize, 36 | cpu_usage_consumer: Option>>, 37 | } 38 | 39 | impl CpalBackend { 40 | /** 41 | Returns the oldest reported CPU usage in the queue. 42 | 43 | The formula for the CPU usage is time elapsed / time allotted, where 44 | - time elapsed is the amount of time it took to fill the audio buffer 45 | requested by the OS 46 | - time allotted is the maximum amount of time Kira could take to process 47 | audio and still finish in time to avoid audio stuttering (num frames / sample 48 | rate) 49 | */ 50 | #[cfg_attr(docsrs, doc(cfg(not(wasm32))))] 51 | pub fn pop_cpu_usage(&mut self) -> Option { 52 | self.cpu_usage_consumer 53 | .as_mut() 54 | .unwrap() 55 | .get_mut() 56 | .unwrap() 57 | .pop() 58 | .ok() 59 | } 60 | 61 | /// Returns the oldest available stream error in the queue. 62 | #[cfg_attr(docsrs, doc(cfg(not(wasm32))))] 63 | pub fn pop_error(&mut self) -> Option { 64 | if let State::Initialized { 65 | stream_manager_controller, 66 | } = &mut self.state 67 | { 68 | stream_manager_controller.pop_handled_error() 69 | } else { 70 | None 71 | } 72 | } 73 | 74 | /** 75 | Returns the number of unhandled stream errors discarded because of overcrowding. 76 | This increases either: 77 | 78 | 1. When `cpal` produces errors faster than they are polled by `kira`, or 79 | 2. When `kira` produces errors faster than they are polled by [`Self::pop_error`]. 80 | */ 81 | #[must_use] 82 | #[cfg_attr(docsrs, doc(cfg(not(wasm32))))] 83 | pub fn num_stream_errors_discarded(&self) -> Option { 84 | if let State::Initialized { 85 | stream_manager_controller, 86 | } = &self.state 87 | { 88 | Some(stream_manager_controller.num_stream_errors_discarded()) 89 | } else { 90 | None 91 | } 92 | } 93 | } 94 | 95 | impl Backend for CpalBackend { 96 | type Settings = CpalBackendSettings; 97 | 98 | type Error = Error; 99 | 100 | fn setup( 101 | settings: Self::Settings, 102 | _internal_buffer_size: usize, 103 | ) -> Result<(Self, u32), Self::Error> { 104 | let host = cpal::default_host(); 105 | 106 | let (device, custom_device) = if let Some(device) = settings.device { 107 | (device, true) 108 | } else { 109 | ( 110 | host.default_output_device() 111 | .ok_or(Error::NoDefaultOutputDevice)?, 112 | false, 113 | ) 114 | }; 115 | 116 | let config = device.default_output_config()?.config(); 117 | let sample_rate = config.sample_rate.0; 118 | Ok(( 119 | Self { 120 | state: State::Uninitialized { device, config }, 121 | custom_device, 122 | buffer_size: settings.buffer_size, 123 | cpu_usage_consumer: None, 124 | }, 125 | sample_rate, 126 | )) 127 | } 128 | 129 | fn start(&mut self, renderer: Renderer) -> Result<(), Self::Error> { 130 | let state = std::mem::replace(&mut self.state, State::Empty); 131 | if let State::Uninitialized { device, config } = state { 132 | let (renderer, cpu_usage_consumer) = RendererWithCpuUsage::new(renderer); 133 | self.state = State::Initialized { 134 | stream_manager_controller: StreamManager::start( 135 | renderer, 136 | device, 137 | config, 138 | self.custom_device, 139 | self.buffer_size, 140 | )?, 141 | }; 142 | self.cpu_usage_consumer = Some(Mutex::new(cpu_usage_consumer)); 143 | } else { 144 | panic!("Cannot initialize the backend multiple times") 145 | } 146 | Ok(()) 147 | } 148 | } 149 | 150 | impl Drop for CpalBackend { 151 | fn drop(&mut self) { 152 | if let State::Initialized { 153 | stream_manager_controller, 154 | } = &self.state 155 | { 156 | stream_manager_controller.stop(); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /crates/kira/src/backend/cpal/desktop/renderer_with_cpu_usage.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{Deref, DerefMut}, 3 | time::Instant, 4 | }; 5 | 6 | use rtrb::{Consumer, Producer, RingBuffer}; 7 | 8 | use crate::backend::Renderer; 9 | 10 | const CPU_USAGE_RINGBUFFER_CAPACITY: usize = 100; 11 | 12 | pub struct RendererWithCpuUsage { 13 | renderer: Renderer, 14 | cpu_usage_producer: Producer, 15 | } 16 | 17 | impl RendererWithCpuUsage { 18 | pub fn new(renderer: Renderer) -> (Self, Consumer) { 19 | let (cpu_usage_producer, cpu_usage_consumer) = 20 | RingBuffer::new(CPU_USAGE_RINGBUFFER_CAPACITY); 21 | ( 22 | Self { 23 | renderer, 24 | cpu_usage_producer, 25 | }, 26 | cpu_usage_consumer, 27 | ) 28 | } 29 | 30 | pub fn process(&mut self, out: &mut [f32], num_channels: u16, sample_rate: u32) { 31 | let allotted_time = out.len() as f32 / num_channels as f32 / sample_rate as f32; 32 | let start_time = Instant::now(); 33 | self.renderer.process(out, num_channels); 34 | let end_time = Instant::now(); 35 | let process_duration = end_time - start_time; 36 | let cpu_usage = process_duration.as_secs_f32() / allotted_time; 37 | self.cpu_usage_producer.push(cpu_usage).ok(); 38 | } 39 | } 40 | 41 | impl Deref for RendererWithCpuUsage { 42 | type Target = Renderer; 43 | 44 | fn deref(&self) -> &Self::Target { 45 | &self.renderer 46 | } 47 | } 48 | 49 | impl DerefMut for RendererWithCpuUsage { 50 | fn deref_mut(&mut self) -> &mut Self::Target { 51 | &mut self.renderer 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/kira/src/backend/cpal/desktop/stream_manager/send_on_drop.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use rtrb::{Consumer, Producer, RingBuffer}; 4 | 5 | /// Wraps `T` so that when it's dropped, it gets sent 6 | /// back through a thread channel. 7 | /// 8 | /// This allows us to retrieve the data after a closure 9 | /// that takes ownership of the data is dropped because of, 10 | /// for instance, a cpal error. 11 | pub struct SendOnDrop { 12 | data: Option, 13 | producer: Producer, 14 | } 15 | 16 | impl SendOnDrop { 17 | pub fn new(data: T) -> (Self, Consumer) { 18 | let (producer, consumer) = RingBuffer::new(1); 19 | ( 20 | Self { 21 | data: Some(data), 22 | producer, 23 | }, 24 | consumer, 25 | ) 26 | } 27 | } 28 | 29 | impl Deref for SendOnDrop { 30 | type Target = T; 31 | 32 | fn deref(&self) -> &Self::Target { 33 | self.data.as_ref().unwrap() 34 | } 35 | } 36 | 37 | impl DerefMut for SendOnDrop { 38 | fn deref_mut(&mut self) -> &mut Self::Target { 39 | self.data.as_mut().unwrap() 40 | } 41 | } 42 | 43 | impl Drop for SendOnDrop { 44 | fn drop(&mut self) { 45 | self.producer 46 | .push(self.data.take().unwrap()) 47 | .expect("send on drop producer full"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/kira/src/backend/cpal/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | use cpal::{BuildStreamError, DefaultStreamConfigError, PlayStreamError}; 4 | 5 | /// Errors that can occur when using the cpal backend. 6 | #[derive(Debug)] 7 | pub enum Error { 8 | /// A default audio output device could not be determined. 9 | NoDefaultOutputDevice, 10 | /// An error occurred when getting the default output configuration. 11 | DefaultStreamConfigError(DefaultStreamConfigError), 12 | /// An error occurred when building the audio stream. 13 | BuildStreamError(BuildStreamError), 14 | /// An error occurred when starting the audio stream. 15 | PlayStreamError(PlayStreamError), 16 | } 17 | 18 | impl Display for Error { 19 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 20 | match self { 21 | Error::NoDefaultOutputDevice => { 22 | f.write_str("Cannot find the default audio output device") 23 | } 24 | Error::DefaultStreamConfigError(error) => error.fmt(f), 25 | Error::BuildStreamError(error) => error.fmt(f), 26 | Error::PlayStreamError(error) => error.fmt(f), 27 | } 28 | } 29 | } 30 | 31 | impl std::error::Error for Error { 32 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 33 | match self { 34 | Error::DefaultStreamConfigError(error) => Some(error), 35 | Error::BuildStreamError(error) => Some(error), 36 | Error::PlayStreamError(error) => Some(error), 37 | _ => None, 38 | } 39 | } 40 | } 41 | 42 | impl From for Error { 43 | fn from(v: DefaultStreamConfigError) -> Self { 44 | Self::DefaultStreamConfigError(v) 45 | } 46 | } 47 | 48 | impl From for Error { 49 | fn from(v: BuildStreamError) -> Self { 50 | Self::BuildStreamError(v) 51 | } 52 | } 53 | 54 | impl From for Error { 55 | fn from(v: PlayStreamError) -> Self { 56 | Self::PlayStreamError(v) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/kira/src/backend/cpal/wasm.rs: -------------------------------------------------------------------------------- 1 | use crate::backend::{Backend, Renderer}; 2 | use cpal::{ 3 | traits::{DeviceTrait, HostTrait, StreamTrait}, 4 | Device, Stream, StreamConfig, 5 | }; 6 | use send_wrapper::SendWrapper; 7 | 8 | use super::{CpalBackendSettings, Error}; 9 | 10 | enum State { 11 | Empty, 12 | Uninitialized { 13 | device: Device, 14 | config: StreamConfig, 15 | }, 16 | Initialized { 17 | _stream: Stream, 18 | }, 19 | } 20 | 21 | /// A backend that uses [cpal](https://crates.io/crates/cpal) to 22 | /// connect a [`Renderer`] to the operating system's audio driver. 23 | pub struct CpalBackend { 24 | state: SendWrapper, 25 | } 26 | 27 | impl Backend for CpalBackend { 28 | type Settings = CpalBackendSettings; 29 | 30 | type Error = Error; 31 | 32 | fn setup( 33 | settings: Self::Settings, 34 | _internal_buffer_size: usize, 35 | ) -> Result<(Self, u32), Self::Error> { 36 | let host = cpal::default_host(); 37 | let device = if let Some(device) = settings.device { 38 | device 39 | } else { 40 | host.default_output_device() 41 | .ok_or(Error::NoDefaultOutputDevice)? 42 | }; 43 | let config = device.default_output_config()?.config(); 44 | let sample_rate = config.sample_rate.0; 45 | Ok(( 46 | Self { 47 | state: SendWrapper::new(State::Uninitialized { device, config }), 48 | }, 49 | sample_rate, 50 | )) 51 | } 52 | 53 | fn start(&mut self, mut renderer: Renderer) -> Result<(), Self::Error> { 54 | if let State::Uninitialized { device, config } = 55 | std::mem::replace(&mut *self.state, State::Empty) 56 | { 57 | let channels = config.channels; 58 | let stream = device.build_output_stream( 59 | &config, 60 | move |data: &mut [f32], _| { 61 | renderer.on_start_processing(); 62 | renderer.process(data, channels); 63 | }, 64 | move |_| {}, 65 | None, 66 | )?; 67 | stream.play()?; 68 | self.state = SendWrapper::new(State::Initialized { _stream: stream }); 69 | } else { 70 | panic!("Cannot initialize the backend multiple times") 71 | } 72 | Ok(()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/kira/src/backend/mock.rs: -------------------------------------------------------------------------------- 1 | //! Useful for testing and benchmarking. 2 | 3 | use std::sync::Mutex; 4 | 5 | use super::{Backend, Renderer}; 6 | 7 | enum State { 8 | Uninitialized, 9 | Initialized { renderer: Mutex }, 10 | } 11 | 12 | /// Settings for the mock backend. 13 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 14 | pub struct MockBackendSettings { 15 | /// The sample rate that the [`Renderer`] should run at. 16 | pub sample_rate: u32, 17 | } 18 | 19 | impl Default for MockBackendSettings { 20 | fn default() -> Self { 21 | Self { sample_rate: 1 } 22 | } 23 | } 24 | 25 | /// A backend that does not connect to any lower-level 26 | /// audio APIs, but allows manually calling 27 | /// [`Renderer::on_start_processing`] and [`Renderer::process`]. 28 | /// 29 | /// This is useful for testing and benchmarking. 30 | pub struct MockBackend { 31 | sample_rate: u32, 32 | state: State, 33 | frames: Vec, 34 | } 35 | 36 | impl MockBackend { 37 | /// Changes the sample rate of the [`Renderer`]. 38 | pub fn set_sample_rate(&mut self, sample_rate: u32) { 39 | self.sample_rate = sample_rate; 40 | if let State::Initialized { renderer } = &mut self.state { 41 | renderer 42 | .get_mut() 43 | .expect("mutex poisoned") 44 | .on_change_sample_rate(sample_rate); 45 | } 46 | } 47 | 48 | /// Calls the [`on_start_processing`](Renderer::on_start_processing) 49 | /// callback of the [`Renderer`]. 50 | pub fn on_start_processing(&mut self) { 51 | if let State::Initialized { renderer } = &mut self.state { 52 | renderer 53 | .get_mut() 54 | .expect("mutex poisoned") 55 | .on_start_processing(); 56 | } else { 57 | panic!("backend is not initialized") 58 | } 59 | } 60 | 61 | /// Calls the [`process`](Renderer::process) callback of the [`Renderer`]. 62 | pub fn process(&mut self) { 63 | if let State::Initialized { renderer } = &mut self.state { 64 | renderer 65 | .get_mut() 66 | .expect("mutex poisoned") 67 | .process(&mut self.frames, 2) 68 | } else { 69 | panic!("backend is not initialized") 70 | } 71 | } 72 | } 73 | 74 | impl Backend for MockBackend { 75 | type Settings = MockBackendSettings; 76 | 77 | type Error = (); 78 | 79 | fn setup( 80 | settings: Self::Settings, 81 | internal_buffer_size: usize, 82 | ) -> Result<(Self, u32), Self::Error> { 83 | Ok(( 84 | Self { 85 | sample_rate: settings.sample_rate, 86 | state: State::Uninitialized, 87 | frames: vec![0.0; internal_buffer_size * 2], 88 | }, 89 | settings.sample_rate, 90 | )) 91 | } 92 | 93 | fn start(&mut self, renderer: Renderer) -> Result<(), Self::Error> { 94 | self.state = State::Initialized { 95 | renderer: Mutex::new(renderer), 96 | }; 97 | Ok(()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/kira/src/backend/renderer.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | atomic::{AtomicU32, Ordering}, 3 | Arc, 4 | }; 5 | 6 | use crate::Frame; 7 | 8 | use super::resources::Resources; 9 | 10 | #[derive(Debug)] 11 | pub(crate) struct RendererShared { 12 | pub(crate) sample_rate: AtomicU32, 13 | } 14 | 15 | impl RendererShared { 16 | #[must_use] 17 | pub fn new(sample_rate: u32) -> Self { 18 | Self { 19 | sample_rate: AtomicU32::new(sample_rate), 20 | } 21 | } 22 | } 23 | 24 | /// Produces [`Frame`]s of audio data to be consumed by a 25 | /// low-level audio API. 26 | /// 27 | /// You will probably not need to interact with [`Renderer`]s 28 | /// directly unless you're writing a [`Backend`](super::Backend). 29 | pub struct Renderer { 30 | dt: f64, 31 | shared: Arc, 32 | resources: Resources, 33 | internal_buffer_size: usize, 34 | temp_buffer: Vec, 35 | } 36 | 37 | impl Renderer { 38 | #[must_use] 39 | pub(crate) fn new( 40 | shared: Arc, 41 | internal_buffer_size: usize, 42 | resources: Resources, 43 | ) -> Self { 44 | Self { 45 | dt: 1.0 / shared.sample_rate.load(Ordering::SeqCst) as f64, 46 | shared, 47 | resources, 48 | internal_buffer_size, 49 | temp_buffer: vec![Frame::ZERO; internal_buffer_size], 50 | } 51 | } 52 | 53 | /// Called by the backend when the sample rate of the 54 | /// audio output changes. 55 | pub fn on_change_sample_rate(&mut self, sample_rate: u32) { 56 | self.dt = 1.0 / sample_rate as f64; 57 | self.shared.sample_rate.store(sample_rate, Ordering::SeqCst); 58 | self.resources.mixer.on_change_sample_rate(sample_rate); 59 | } 60 | 61 | /// Called by the backend when it's time to process 62 | /// a new batch of samples. 63 | pub fn on_start_processing(&mut self) { 64 | self.resources.mixer.on_start_processing(); 65 | self.resources.clocks.on_start_processing(); 66 | self.resources.listeners.on_start_processing(); 67 | self.resources.modulators.on_start_processing(); 68 | } 69 | 70 | /// Produces the next [`Frame`]s of audio. 71 | pub fn process(&mut self, out: &mut [f32], num_channels: u16) { 72 | for chunk in out.chunks_mut(self.internal_buffer_size * num_channels as usize) { 73 | self.process_chunk(chunk, num_channels); 74 | } 75 | } 76 | 77 | fn process_chunk(&mut self, chunk: &mut [f32], num_channels: u16) { 78 | let num_frames = chunk.len() / num_channels as usize; 79 | 80 | self.resources.modulators.process( 81 | self.dt * num_frames as f64, 82 | &self.resources.clocks, 83 | &self.resources.listeners, 84 | ); 85 | self.resources.clocks.update( 86 | self.dt * num_frames as f64, 87 | &self.resources.modulators, 88 | &self.resources.listeners, 89 | ); 90 | self.resources.listeners.update( 91 | self.dt * num_frames as f64, 92 | &self.resources.clocks, 93 | &self.resources.modulators, 94 | ); 95 | 96 | self.resources.mixer.process( 97 | &mut self.temp_buffer[..num_frames], 98 | self.dt, 99 | &self.resources.clocks, 100 | &self.resources.modulators, 101 | &self.resources.listeners, 102 | ); 103 | 104 | // convert from frames to requested number of channels 105 | for (i, channels) in chunk.chunks_mut(num_channels.into()).enumerate() { 106 | let mut frame = self.temp_buffer[i]; 107 | frame.left = frame.left.clamp(-1.0, 1.0); 108 | frame.right = frame.right.clamp(-1.0, 1.0); 109 | if num_channels == 1 { 110 | channels[0] = (frame.left + frame.right) / 2.0; 111 | } else { 112 | channels[0] = frame.left; 113 | channels[1] = frame.right; 114 | /* 115 | if there's more channels, send silence to them. if we don't, 116 | we might get bad sounds outputted to those channels. 117 | (https://github.com/tesselode/kira/issues/50) 118 | */ 119 | for channel in channels.iter_mut().skip(2) { 120 | *channel = 0.0; 121 | } 122 | } 123 | } 124 | self.temp_buffer.fill(Frame::ZERO); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/kira/src/backend/resources/clocks.rs: -------------------------------------------------------------------------------- 1 | use crate::{clock::Clock, info::Info}; 2 | 3 | use super::{ 4 | listeners::Listeners, modulators::Modulators, ResourceController, 5 | SelfReferentialResourceStorage, 6 | }; 7 | 8 | pub(crate) struct Clocks(pub(crate) SelfReferentialResourceStorage); 9 | 10 | impl Clocks { 11 | #[must_use] 12 | pub(crate) fn new(capacity: usize) -> (Self, ResourceController) { 13 | let (storage, controller) = SelfReferentialResourceStorage::new(capacity); 14 | (Self(storage), controller) 15 | } 16 | 17 | pub(crate) fn on_start_processing(&mut self) { 18 | self.0 19 | .remove_and_add(|clock| clock.shared().is_marked_for_removal()); 20 | for (_, clock) in &mut self.0 { 21 | clock.on_start_processing(); 22 | } 23 | } 24 | 25 | pub(crate) fn update(&mut self, dt: f64, modulators: &Modulators, listeners: &Listeners) { 26 | self.0.for_each(|clock, others| { 27 | clock.update( 28 | dt, 29 | &Info::new( 30 | others, 31 | &modulators.0.resources, 32 | &listeners.0.resources, 33 | None, 34 | ), 35 | ); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/kira/src/backend/resources/listeners.rs: -------------------------------------------------------------------------------- 1 | use crate::{info::Info, listener::Listener}; 2 | 3 | use super::{ 4 | clocks::Clocks, modulators::Modulators, ResourceController, SelfReferentialResourceStorage, 5 | }; 6 | 7 | pub(crate) struct Listeners(pub(crate) SelfReferentialResourceStorage); 8 | 9 | impl Listeners { 10 | #[must_use] 11 | pub(crate) fn new(capacity: usize) -> (Self, ResourceController) { 12 | let (storage, controller) = SelfReferentialResourceStorage::new(capacity); 13 | (Self(storage), controller) 14 | } 15 | 16 | pub(crate) fn on_start_processing(&mut self) { 17 | self.0 18 | .remove_and_add(|listener| listener.shared.is_marked_for_removal()); 19 | for (_, listener) in &mut self.0 { 20 | listener.on_start_processing(); 21 | } 22 | } 23 | 24 | pub(crate) fn update(&mut self, dt: f64, clocks: &Clocks, modulators: &Modulators) { 25 | self.0.for_each(|listener, others| { 26 | listener.update( 27 | dt, 28 | &Info::new(&clocks.0.resources, &modulators.0.resources, others, None), 29 | ); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/kira/src/backend/resources/mixer.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | frame::Frame, 3 | info::Info, 4 | track::{MainTrack, MainTrackBuilder, MainTrackHandle, SendTrack, Track}, 5 | }; 6 | 7 | use super::{ 8 | clocks::Clocks, listeners::Listeners, modulators::Modulators, ResourceController, 9 | ResourceStorage, 10 | }; 11 | 12 | pub(crate) struct Mixer { 13 | main_track: MainTrack, 14 | sub_tracks: ResourceStorage, 15 | send_tracks: ResourceStorage, 16 | temp_buffer: Vec, 17 | } 18 | 19 | impl Mixer { 20 | #[must_use] 21 | pub fn new( 22 | sub_track_capacity: usize, 23 | send_track_capacity: usize, 24 | sample_rate: u32, 25 | internal_buffer_size: usize, 26 | main_track_builder: MainTrackBuilder, 27 | ) -> ( 28 | Self, 29 | ResourceController, 30 | ResourceController, 31 | MainTrackHandle, 32 | ) { 33 | let (mut main_track, main_track_handle) = main_track_builder.build(internal_buffer_size); 34 | main_track.init_effects(sample_rate); 35 | let (sub_tracks, sub_track_controller) = ResourceStorage::new(sub_track_capacity); 36 | let (send_tracks, send_track_controller) = ResourceStorage::new(send_track_capacity); 37 | ( 38 | Self { 39 | main_track, 40 | sub_tracks, 41 | send_tracks, 42 | temp_buffer: vec![Frame::ZERO; internal_buffer_size], 43 | }, 44 | sub_track_controller, 45 | send_track_controller, 46 | main_track_handle, 47 | ) 48 | } 49 | 50 | pub fn on_change_sample_rate(&mut self, sample_rate: u32) { 51 | self.main_track.on_change_sample_rate(sample_rate); 52 | for (_, track) in &mut self.sub_tracks { 53 | track.on_change_sample_rate(sample_rate); 54 | } 55 | for (_, track) in &mut self.send_tracks { 56 | track.on_change_sample_rate(sample_rate); 57 | } 58 | } 59 | 60 | pub fn on_start_processing(&mut self) { 61 | self.sub_tracks 62 | .remove_and_add(|track| track.should_be_removed()); 63 | for (_, track) in &mut self.sub_tracks { 64 | track.on_start_processing(); 65 | } 66 | self.send_tracks 67 | .remove_and_add(|track| track.shared().is_marked_for_removal()); 68 | for (_, track) in &mut self.send_tracks { 69 | track.on_start_processing(); 70 | } 71 | self.main_track.on_start_processing(); 72 | } 73 | 74 | pub fn process( 75 | &mut self, 76 | out: &mut [Frame], 77 | dt: f64, 78 | clocks: &Clocks, 79 | modulators: &Modulators, 80 | listeners: &Listeners, 81 | ) { 82 | for (_, track) in &mut self.sub_tracks { 83 | track.process( 84 | &mut self.temp_buffer[..out.len()], 85 | dt, 86 | clocks, 87 | modulators, 88 | listeners, 89 | None, 90 | &mut self.send_tracks, 91 | ); 92 | for (summed_out, sound_out) in out.iter_mut().zip(self.temp_buffer.iter().copied()) { 93 | *summed_out += sound_out; 94 | } 95 | self.temp_buffer.fill(Frame::ZERO); 96 | } 97 | let info = Info::new( 98 | &clocks.0.resources, 99 | &modulators.0.resources, 100 | &listeners.0.resources, 101 | None, 102 | ); 103 | for (_, track) in &mut self.send_tracks { 104 | track.process(&mut self.temp_buffer[..out.len()], dt, &info); 105 | for (summed_out, sound_out) in out.iter_mut().zip(self.temp_buffer.iter().copied()) { 106 | *summed_out += sound_out; 107 | } 108 | self.temp_buffer.fill(Frame::ZERO); 109 | } 110 | self.main_track.process(out, dt, &info); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /crates/kira/src/backend/resources/modulators.rs: -------------------------------------------------------------------------------- 1 | use crate::{info::Info, modulator::Modulator}; 2 | 3 | use super::{ 4 | clocks::Clocks, listeners::Listeners, ResourceController, SelfReferentialResourceStorage, 5 | }; 6 | 7 | pub(crate) struct Modulators(pub(crate) SelfReferentialResourceStorage>); 8 | 9 | impl Modulators { 10 | #[must_use] 11 | pub fn new(capacity: usize) -> (Self, ResourceController>) { 12 | let (storage, controller) = SelfReferentialResourceStorage::new(capacity); 13 | (Self(storage), controller) 14 | } 15 | 16 | pub fn on_start_processing(&mut self) { 17 | self.0.remove_and_add(|modulator| modulator.finished()); 18 | for (_, modulator) in &mut self.0 { 19 | modulator.on_start_processing(); 20 | } 21 | } 22 | 23 | pub fn process(&mut self, dt: f64, clocks: &Clocks, listeners: &Listeners) { 24 | self.0.for_each(|modulator, others| { 25 | modulator.update( 26 | dt, 27 | &Info::new(&clocks.0.resources, others, &listeners.0.resources, None), 28 | ); 29 | }); 30 | } 31 | } 32 | 33 | struct DummyModulator; 34 | 35 | impl Modulator for DummyModulator { 36 | fn update(&mut self, _dt: f64, _info: &Info) {} 37 | 38 | fn value(&self) -> f64 { 39 | 0.0 40 | } 41 | 42 | fn finished(&self) -> bool { 43 | false 44 | } 45 | } 46 | 47 | impl Default for Box { 48 | fn default() -> Self { 49 | Box::new(DummyModulator) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/kira/src/backend/resources/test.rs: -------------------------------------------------------------------------------- 1 | use crate::{backend::resources::SelfReferentialResourceStorage, ResourceLimitReached}; 2 | 3 | use super::ResourceStorage; 4 | 5 | #[test] 6 | fn resource_storage() { 7 | let (mut storage, mut controller) = ResourceStorage::new(2); 8 | 9 | // add 10 | let one = controller.insert(1).unwrap(); 11 | let two = controller.insert(2).unwrap(); 12 | assert_eq!(controller.insert(3), Err(ResourceLimitReached)); 13 | storage.remove_and_add(|_| false); 14 | 15 | // get 16 | assert_eq!(storage.get_mut(one), Some(&mut 1)); 17 | assert_eq!(storage.get_mut(two), Some(&mut 2)); 18 | 19 | // iter 20 | assert_eq!( 21 | storage.iter_mut().collect::>(), 22 | vec![(two, &mut 2), (one, &mut 1)] 23 | ); 24 | 25 | // remove 26 | storage.remove_and_add(|&x| x < 2); 27 | assert_eq!(storage.get_mut(one), None); 28 | assert_eq!(storage.get_mut(two), Some(&mut 2)); 29 | assert_eq!(storage.iter_mut().collect::>(), vec![(two, &mut 2)]); 30 | 31 | // re-add 32 | let three = controller.insert(3).unwrap(); 33 | assert_eq!(controller.insert(4), Err(ResourceLimitReached)); 34 | storage.remove_and_add(|_| false); 35 | assert_eq!(storage.get_mut(two), Some(&mut 2)); 36 | assert_eq!(storage.get_mut(three), Some(&mut 3)); 37 | assert_eq!( 38 | storage.iter_mut().collect::>(), 39 | vec![(three, &mut 3), (two, &mut 2)] 40 | ); 41 | } 42 | 43 | #[test] 44 | fn self_referential_resource_storage() { 45 | let (mut storage, mut controller) = SelfReferentialResourceStorage::new(2); 46 | 47 | // add 48 | let one = controller.insert(1).unwrap(); 49 | let two = controller.insert(2).unwrap(); 50 | assert_eq!(controller.insert(3), Err(ResourceLimitReached)); 51 | storage.remove_and_add(|_| false); 52 | 53 | // get 54 | assert_eq!(storage.get_mut(one), Some(&mut 1)); 55 | assert_eq!(storage.get_mut(two), Some(&mut 2)); 56 | 57 | // iter 58 | assert_eq!( 59 | storage.iter_mut().collect::>(), 60 | vec![(two, &mut 2), (one, &mut 1)] 61 | ); 62 | 63 | // remove 64 | storage.remove_and_add(|&x| x < 2); 65 | assert_eq!(storage.get_mut(one), None); 66 | assert_eq!(storage.get_mut(two), Some(&mut 2)); 67 | assert_eq!(storage.iter_mut().collect::>(), vec![(two, &mut 2)]); 68 | 69 | // re-add 70 | let three = controller.insert(3).unwrap(); 71 | assert_eq!(controller.insert(4), Err(ResourceLimitReached)); 72 | storage.remove_and_add(|_| false); 73 | assert_eq!(storage.get_mut(two), Some(&mut 2)); 74 | assert_eq!(storage.get_mut(three), Some(&mut 3)); 75 | assert_eq!( 76 | storage.iter_mut().collect::>(), 77 | vec![(three, &mut 3), (two, &mut 2)] 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /crates/kira/src/clock/clock_speed.rs: -------------------------------------------------------------------------------- 1 | use crate::{tween::Tweenable, Value}; 2 | 3 | /// The rate that a [clock](crate::clock) ticks at. 4 | #[derive(Debug, Clone, Copy, PartialEq)] 5 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 6 | pub enum ClockSpeed { 7 | /// The clock ticks every x seconds. 8 | SecondsPerTick(f64), 9 | /// The clock ticks x times per second. 10 | TicksPerSecond(f64), 11 | /// The clock ticks x times per minute. 12 | TicksPerMinute(f64), 13 | } 14 | 15 | impl ClockSpeed { 16 | /// Returns the [`ClockSpeed`] as a number of seconds between each tick. 17 | #[must_use] 18 | pub fn as_seconds_per_tick(&self) -> f64 { 19 | match self { 20 | ClockSpeed::SecondsPerTick(seconds_per_tick) => *seconds_per_tick, 21 | ClockSpeed::TicksPerSecond(ticks_per_second) => 1.0 / *ticks_per_second, 22 | ClockSpeed::TicksPerMinute(ticks_per_minute) => 60.0 / *ticks_per_minute, 23 | } 24 | } 25 | 26 | /// Returns the [`ClockSpeed`] as a number of ticks per second. 27 | #[must_use] 28 | pub fn as_ticks_per_second(&self) -> f64 { 29 | match self { 30 | ClockSpeed::SecondsPerTick(seconds_per_tick) => 1.0 / *seconds_per_tick, 31 | ClockSpeed::TicksPerSecond(ticks_per_second) => *ticks_per_second, 32 | ClockSpeed::TicksPerMinute(ticks_per_minute) => *ticks_per_minute / 60.0, 33 | } 34 | } 35 | 36 | /// Returns the [`ClockSpeed`] as a number of ticks per minute. 37 | #[must_use] 38 | pub fn as_ticks_per_minute(&self) -> f64 { 39 | match self { 40 | ClockSpeed::SecondsPerTick(seconds_per_tick) => 60.0 / *seconds_per_tick, 41 | ClockSpeed::TicksPerSecond(ticks_per_second) => *ticks_per_second * 60.0, 42 | ClockSpeed::TicksPerMinute(ticks_per_minute) => *ticks_per_minute, 43 | } 44 | } 45 | } 46 | 47 | impl Tweenable for ClockSpeed { 48 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 49 | match b { 50 | ClockSpeed::SecondsPerTick(b) => ClockSpeed::SecondsPerTick(Tweenable::interpolate( 51 | a.as_seconds_per_tick(), 52 | b, 53 | amount, 54 | )), 55 | ClockSpeed::TicksPerSecond(b) => ClockSpeed::TicksPerSecond(Tweenable::interpolate( 56 | a.as_ticks_per_second(), 57 | b, 58 | amount, 59 | )), 60 | ClockSpeed::TicksPerMinute(b) => ClockSpeed::TicksPerMinute(Tweenable::interpolate( 61 | a.as_ticks_per_minute(), 62 | b, 63 | amount, 64 | )), 65 | } 66 | } 67 | } 68 | 69 | impl From for Value { 70 | fn from(clock_speed: ClockSpeed) -> Self { 71 | Value::Fixed(clock_speed) 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | #[test] 77 | #[allow(clippy::float_cmp)] 78 | fn test() { 79 | const SECONDS_PER_TICK: f64 = 0.5; 80 | const TICKS_PER_SECOND: f64 = 2.0; 81 | const TICKS_PER_MINUTE: f64 = 120.0; 82 | 83 | assert_eq!( 84 | ClockSpeed::SecondsPerTick(SECONDS_PER_TICK).as_seconds_per_tick(), 85 | SECONDS_PER_TICK 86 | ); 87 | assert_eq!( 88 | ClockSpeed::SecondsPerTick(SECONDS_PER_TICK).as_ticks_per_second(), 89 | TICKS_PER_SECOND 90 | ); 91 | assert_eq!( 92 | ClockSpeed::SecondsPerTick(SECONDS_PER_TICK).as_ticks_per_minute(), 93 | TICKS_PER_MINUTE 94 | ); 95 | 96 | assert_eq!( 97 | ClockSpeed::TicksPerSecond(TICKS_PER_SECOND).as_seconds_per_tick(), 98 | SECONDS_PER_TICK 99 | ); 100 | assert_eq!( 101 | ClockSpeed::TicksPerSecond(TICKS_PER_SECOND).as_ticks_per_second(), 102 | TICKS_PER_SECOND 103 | ); 104 | assert_eq!( 105 | ClockSpeed::TicksPerSecond(TICKS_PER_SECOND).as_ticks_per_minute(), 106 | TICKS_PER_MINUTE 107 | ); 108 | 109 | assert_eq!( 110 | ClockSpeed::TicksPerMinute(TICKS_PER_MINUTE).as_seconds_per_tick(), 111 | SECONDS_PER_TICK 112 | ); 113 | assert_eq!( 114 | ClockSpeed::TicksPerMinute(TICKS_PER_MINUTE).as_ticks_per_second(), 115 | TICKS_PER_SECOND 116 | ); 117 | assert_eq!( 118 | ClockSpeed::TicksPerMinute(TICKS_PER_MINUTE).as_ticks_per_minute(), 119 | TICKS_PER_MINUTE 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /crates/kira/src/clock/handle.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::Ordering, Arc}; 2 | 3 | use crate::command::handle_param_setters; 4 | 5 | use super::{ClockId, ClockShared, ClockSpeed, ClockTime, CommandWriters}; 6 | 7 | /// Controls a clock. 8 | /// 9 | /// When a [`ClockHandle`] is dropped, the corresponding clock 10 | /// will be removed. 11 | #[derive(Debug)] 12 | pub struct ClockHandle { 13 | pub(crate) id: ClockId, 14 | pub(crate) shared: Arc, 15 | pub(crate) command_writers: CommandWriters, 16 | } 17 | 18 | impl ClockHandle { 19 | /// Returns the unique identifier for the clock. 20 | #[must_use] 21 | pub fn id(&self) -> ClockId { 22 | self.id 23 | } 24 | 25 | /// Returns `true` if the clock is currently ticking 26 | /// and `false` if not. 27 | #[must_use] 28 | pub fn ticking(&self) -> bool { 29 | self.shared.ticking() 30 | } 31 | 32 | /// Returns the current time of the clock. 33 | #[must_use] 34 | pub fn time(&self) -> ClockTime { 35 | ClockTime { 36 | clock: self.id, 37 | ticks: self.shared.ticks(), 38 | fraction: self.shared.fractional_position(), 39 | } 40 | } 41 | 42 | handle_param_setters! { 43 | /// Sets the speed of the clock. 44 | speed: ClockSpeed, 45 | } 46 | 47 | /// Starts or resumes the clock. 48 | pub fn start(&mut self) { 49 | self.command_writers.set_ticking.write(true) 50 | } 51 | 52 | /// Pauses the clock. 53 | pub fn pause(&mut self) { 54 | self.command_writers.set_ticking.write(false) 55 | } 56 | 57 | /// Stops and resets the clock. 58 | pub fn stop(&mut self) { 59 | self.command_writers.set_ticking.write(false); 60 | self.command_writers.reset.write(()); 61 | // store 0 as the current time so any reads that happen 62 | // immediately after a stop don't errantly get the previous 63 | // clock time 64 | self.shared.ticks.store(0, Ordering::SeqCst); 65 | self.shared 66 | .fractional_position 67 | .store(0.0f64.to_bits(), Ordering::SeqCst); 68 | } 69 | } 70 | 71 | impl Drop for ClockHandle { 72 | fn drop(&mut self) { 73 | self.shared.mark_for_removal(); 74 | } 75 | } 76 | 77 | impl From<&ClockHandle> for ClockId { 78 | fn from(handle: &ClockHandle) -> Self { 79 | handle.id() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/kira/src/decibels.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, AddAssign, Sub, SubAssign}; 2 | 3 | use crate::{tween::Tweenable, Value}; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 6 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 7 | #[cfg_attr(feature = "serde", serde(transparent))] 8 | /// Represents a change in volume. 9 | /// 10 | /// Higher values increase the volume and lower values decrease it. 11 | /// Setting the volume of a sound to -60dB or lower makes it silent. 12 | pub struct Decibels(pub f32); 13 | 14 | impl Decibels { 15 | /// The minimum decibel value at which a sound is considered 16 | /// silent. 17 | pub const SILENCE: Self = Self(-60.0); 18 | /// The decibel value that produces no change in volume. 19 | pub const IDENTITY: Self = Self(0.0); 20 | 21 | /// Converts decibels to amplitude, a linear volume measurement. 22 | /// 23 | /// This returns a number from `0.0`-`1.0` that you can multiply 24 | /// a singal by to change its volume. 25 | pub fn as_amplitude(self) -> f32 { 26 | // adding a special case for db == 0.0 improves 27 | // performance in the sound playback benchmarks 28 | // by about 7% 29 | if self == Self(0.0) { 30 | return 1.0; 31 | } 32 | if self <= Self::SILENCE { 33 | return 0.0; 34 | } 35 | 10.0f32.powf(self.0 / 20.0) 36 | } 37 | } 38 | 39 | impl Default for Decibels { 40 | fn default() -> Self { 41 | Self::IDENTITY 42 | } 43 | } 44 | 45 | impl Tweenable for Decibels { 46 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 47 | Self(Tweenable::interpolate(a.0, b.0, amount)) 48 | } 49 | } 50 | 51 | impl From for Decibels { 52 | fn from(value: f32) -> Self { 53 | Self(value) 54 | } 55 | } 56 | 57 | impl From for Value { 58 | fn from(value: f32) -> Self { 59 | Value::Fixed(Decibels(value)) 60 | } 61 | } 62 | 63 | impl From for Value { 64 | fn from(value: Decibels) -> Self { 65 | Value::Fixed(value) 66 | } 67 | } 68 | 69 | impl Add for Decibels { 70 | type Output = Decibels; 71 | 72 | fn add(self, rhs: Decibels) -> Self::Output { 73 | Self(self.0 + rhs.0) 74 | } 75 | } 76 | 77 | impl AddAssign for Decibels { 78 | fn add_assign(&mut self, rhs: Decibels) { 79 | self.0 += rhs.0; 80 | } 81 | } 82 | 83 | impl Sub for Decibels { 84 | type Output = Decibels; 85 | 86 | fn sub(self, rhs: Decibels) -> Self::Output { 87 | Self(self.0 - rhs.0) 88 | } 89 | } 90 | 91 | impl SubAssign for Decibels { 92 | fn sub_assign(&mut self, rhs: Decibels) { 93 | self.0 -= rhs.0; 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | #[test] 99 | #[allow(clippy::float_cmp)] 100 | fn test() { 101 | /// A table of dB values to the corresponding amplitudes. 102 | // Data gathered from https://www.silisoftware.com/tools/db.php 103 | const TEST_CALCULATIONS: [(Decibels, f32); 6] = [ 104 | (Decibels::IDENTITY, 1.0), 105 | (Decibels(3.0), 1.4125376), 106 | (Decibels(12.0), 3.9810717), 107 | (Decibels(-3.0), 0.70794576), 108 | (Decibels(-12.0), 0.25118864), 109 | (Decibels::SILENCE, 0.0), 110 | ]; 111 | 112 | for (decibels, amplitude) in TEST_CALCULATIONS { 113 | assert!((decibels.as_amplitude() - amplitude).abs() < 0.00001); 114 | } 115 | 116 | // test some special cases 117 | assert_eq!((Decibels::SILENCE - Decibels(100.0)).as_amplitude(), 0.0); 118 | } 119 | -------------------------------------------------------------------------------- /crates/kira/src/effect.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Modifies audio signals. 3 | 4 | Any type that implements [`EffectBuilder`] can be added to a mixer track by 5 | using [`TrackBuilder::add_effect`](crate::track::TrackBuilder::add_effect). Kira 6 | comes with a number of commonly used effects. 7 | 8 | If needed, you can create custom effects by implementing the [`EffectBuilder`] 9 | and [`Effect`] traits. 10 | */ 11 | 12 | pub mod compressor; 13 | pub mod delay; 14 | pub mod distortion; 15 | pub mod eq_filter; 16 | pub mod filter; 17 | pub mod panning_control; 18 | pub mod reverb; 19 | pub mod volume_control; 20 | 21 | use crate::{frame::Frame, info::Info}; 22 | 23 | /// Configures an effect. 24 | pub trait EffectBuilder { 25 | /// Allows the user to control the effect from gameplay code. 26 | type Handle; 27 | 28 | /// Creates the effect and a handle to the effect. 29 | #[must_use] 30 | fn build(self) -> (Box, Self::Handle); 31 | } 32 | 33 | /// Receives input audio from a mixer track and outputs modified audio. 34 | /// 35 | /// For performance reasons, avoid allocating and deallocating memory in any methods 36 | /// of this trait besides [`on_change_sample_rate`](Effect::on_change_sample_rate). 37 | #[allow(unused_variables)] 38 | pub trait Effect: Send { 39 | /// Called when the effect is first sent to the renderer. 40 | fn init(&mut self, sample_rate: u32, internal_buffer_size: usize) {} 41 | 42 | /// Called when the sample rate of the renderer is changed. 43 | fn on_change_sample_rate(&mut self, sample_rate: u32) {} 44 | 45 | /// Called whenever a new batch of audio samples is requested by the backend. 46 | /// 47 | /// This is a good place to put code that needs to run fairly frequently, 48 | /// but not for every single audio sample. 49 | fn on_start_processing(&mut self) {} 50 | 51 | /// Transforms a slice of input [`Frame`]s. 52 | /// 53 | /// `dt` is the time between each frame (in seconds). 54 | fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info); 55 | } 56 | -------------------------------------------------------------------------------- /crates/kira/src/effect/compressor.rs: -------------------------------------------------------------------------------- 1 | //! Reduces (or increases) the dynamic range of audio. 2 | 3 | // Loosely based on https://www.musicdsp.org/en/latest/Effects/204-simple-compressor-class-c.html 4 | 5 | mod builder; 6 | mod handle; 7 | 8 | pub use builder::*; 9 | pub use handle::*; 10 | 11 | use std::time::Duration; 12 | 13 | use crate::{ 14 | command::{read_commands_into_parameters, ValueChangeCommand}, 15 | command_writers_and_readers, 16 | frame::Frame, 17 | info::Info, 18 | Decibels, Mix, Parameter, 19 | }; 20 | 21 | use super::Effect; 22 | 23 | struct Compressor { 24 | command_readers: CommandReaders, 25 | threshold: Parameter, 26 | ratio: Parameter, 27 | attack_duration: Parameter, 28 | release_duration: Parameter, 29 | makeup_gain: Parameter, 30 | mix: Parameter, 31 | envelope_follower: [f32; 2], 32 | } 33 | 34 | impl Compressor { 35 | #[must_use] 36 | fn new(builder: CompressorBuilder, command_readers: CommandReaders) -> Self { 37 | Self { 38 | command_readers, 39 | threshold: Parameter::new(builder.threshold, CompressorBuilder::DEFAULT_THRESHOLD), 40 | ratio: Parameter::new(builder.ratio, CompressorBuilder::DEFAULT_RATIO), 41 | attack_duration: Parameter::new( 42 | builder.attack_duration, 43 | CompressorBuilder::DEFAULT_ATTACK_DURATION, 44 | ), 45 | release_duration: Parameter::new( 46 | builder.release_duration, 47 | CompressorBuilder::DEFAULT_RELEASE_DURATION, 48 | ), 49 | makeup_gain: Parameter::new( 50 | builder.makeup_gain, 51 | CompressorBuilder::DEFAULT_MAKEUP_GAIN, 52 | ), 53 | mix: Parameter::new(builder.mix, CompressorBuilder::DEFAULT_MIX), 54 | envelope_follower: [0.0; 2], 55 | } 56 | } 57 | } 58 | 59 | impl Effect for Compressor { 60 | fn on_start_processing(&mut self) { 61 | read_commands_into_parameters!( 62 | self, 63 | threshold, 64 | ratio, 65 | attack_duration, 66 | release_duration, 67 | makeup_gain, 68 | mix, 69 | ); 70 | } 71 | 72 | fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) { 73 | self.threshold.update(dt * input.len() as f64, info); 74 | self.ratio.update(dt * input.len() as f64, info); 75 | self.attack_duration.update(dt * input.len() as f64, info); 76 | self.release_duration.update(dt * input.len() as f64, info); 77 | self.makeup_gain.update(dt * input.len() as f64, info); 78 | self.mix.update(dt * input.len() as f64, info); 79 | 80 | let threshold = self.threshold.value() as f32; 81 | let ratio = self.ratio.value() as f32; 82 | let attack_duration = self.attack_duration.value(); 83 | let release_duration = self.release_duration.value(); 84 | 85 | let num_frames = input.len(); 86 | for (i, frame) in input.iter_mut().enumerate() { 87 | let time_in_chunk = (i + 1) as f64 / num_frames as f64; 88 | let makeup_gain = self.makeup_gain.interpolated_value(time_in_chunk); 89 | let mix = self.mix.interpolated_value(time_in_chunk).0.clamp(0.0, 1.0); 90 | 91 | let input_decibels = [ 92 | 20.0 * frame.left.abs().log10(), 93 | 20.0 * frame.right.abs().log10(), 94 | ]; 95 | let over_decibels = input_decibels.map(|input| (input - threshold).max(0.0)); 96 | for (i, envelope_follower) in self.envelope_follower.iter_mut().enumerate() { 97 | let duration = if *envelope_follower > over_decibels[i] { 98 | release_duration 99 | } else { 100 | attack_duration 101 | }; 102 | let speed = (-1.0 / (duration.as_secs_f64() / dt)).exp(); 103 | *envelope_follower = 104 | over_decibels[i] + speed as f32 * (*envelope_follower - over_decibels[i]); 105 | } 106 | let gain_reduction = self 107 | .envelope_follower 108 | .map(|envelope_follower| envelope_follower * ((1.0 / ratio) - 1.0)); 109 | let amplitude = 110 | gain_reduction.map(|gain_reduction| 10.0f32.powf(gain_reduction / 20.0)); 111 | let makeup_gain_linear = 10.0f32.powf(makeup_gain.0 / 20.0); 112 | let output = Frame { 113 | left: amplitude[0] * frame.left, 114 | right: amplitude[1] * frame.right, 115 | } * makeup_gain_linear; 116 | 117 | *frame = output * mix.sqrt() + *frame * (1.0 - mix).sqrt() 118 | } 119 | } 120 | } 121 | 122 | command_writers_and_readers! { 123 | set_threshold: ValueChangeCommand, 124 | set_ratio: ValueChangeCommand, 125 | set_attack_duration: ValueChangeCommand, 126 | set_release_duration: ValueChangeCommand, 127 | set_makeup_gain: ValueChangeCommand, 128 | set_mix: ValueChangeCommand, 129 | } 130 | -------------------------------------------------------------------------------- /crates/kira/src/effect/compressor/handle.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{command::handle_param_setters, Decibels, Mix}; 4 | 5 | use super::CommandWriters; 6 | 7 | /// Controls a compressor. 8 | #[derive(Debug)] 9 | pub struct CompressorHandle { 10 | pub(super) command_writers: CommandWriters, 11 | } 12 | 13 | impl CompressorHandle { 14 | handle_param_setters! { 15 | /// Sets the volume above which volume will start to be decreased (in decibels). 16 | threshold: f64, 17 | 18 | /// Sets how much the signal will be compressed. 19 | /// 20 | /// A ratio of `2.0` (or 2 to 1) means an increase of 3dB will 21 | /// become an increase of 1.5dB. Ratios between `0.0` and `1.0` 22 | /// will actually expand the audio. 23 | ratio: f64, 24 | 25 | /// Sets how much time it takes for the volume attenuation to ramp up once 26 | /// the input volume exceeds the threshold. 27 | attack_duration: Duration, 28 | 29 | /// Sets how much time it takes for the volume attenuation to relax once 30 | /// the input volume dips below the threshold. 31 | release_duration: Duration, 32 | 33 | /// Sets the amount to change the volume after processing (in dB). 34 | /// 35 | /// This can be used to compensate for the decrease in volume resulting 36 | /// from compression. This is only applied to the wet signal, nto the 37 | /// dry signal. 38 | makeup_gain: Decibels, 39 | 40 | /// Sets how much dry (unprocessed) signal should be blended 41 | /// with the wet (processed) signal. 42 | mix: Mix, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/kira/src/effect/delay.rs: -------------------------------------------------------------------------------- 1 | //! Adds echoes to a sound. 2 | 3 | mod builder; 4 | mod handle; 5 | 6 | pub use builder::*; 7 | pub use handle::*; 8 | 9 | use std::time::Duration; 10 | 11 | use crate::{ 12 | command::{read_commands_into_parameters, ValueChangeCommand}, 13 | command_writers_and_readers, 14 | frame::Frame, 15 | info::Info, 16 | Decibels, Mix, Parameter, 17 | }; 18 | 19 | use super::Effect; 20 | 21 | struct Delay { 22 | command_readers: CommandReaders, 23 | delay_time: Duration, 24 | feedback: Parameter, 25 | mix: Parameter, 26 | buffer: Vec, 27 | feedback_effects: Vec>, 28 | temp_buffer: Vec, 29 | } 30 | 31 | impl Delay { 32 | /// Creates a new delay effect. 33 | #[must_use] 34 | fn new(builder: DelayBuilder, command_readers: CommandReaders) -> Self { 35 | Self { 36 | command_readers, 37 | delay_time: builder.delay_time, 38 | feedback: Parameter::new(builder.feedback, Decibels(-6.0)), 39 | mix: Parameter::new(builder.mix, Mix(0.5)), 40 | buffer: Vec::with_capacity(0), 41 | feedback_effects: builder.feedback_effects, 42 | temp_buffer: vec![], 43 | } 44 | } 45 | } 46 | 47 | impl Effect for Delay { 48 | fn init(&mut self, sample_rate: u32, internal_buffer_size: usize) { 49 | let delay_time_frames = (self.delay_time.as_secs_f64() * sample_rate as f64) as usize; 50 | self.buffer = vec![Frame::ZERO; delay_time_frames]; 51 | self.temp_buffer = vec![Frame::ZERO; internal_buffer_size]; 52 | for effect in &mut self.feedback_effects { 53 | effect.init(sample_rate, internal_buffer_size); 54 | } 55 | } 56 | 57 | fn on_change_sample_rate(&mut self, sample_rate: u32) { 58 | let delay_time_frames = (self.delay_time.as_secs_f64() * sample_rate as f64) as usize; 59 | self.buffer = vec![Frame::ZERO; delay_time_frames]; 60 | for effect in &mut self.feedback_effects { 61 | effect.on_change_sample_rate(sample_rate); 62 | } 63 | } 64 | 65 | fn on_start_processing(&mut self) { 66 | read_commands_into_parameters!(self, feedback, mix); 67 | for effect in &mut self.feedback_effects { 68 | effect.on_start_processing(); 69 | } 70 | } 71 | 72 | fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) { 73 | self.feedback.update(dt * input.len() as f64, info); 74 | self.mix.update(dt * input.len() as f64, info); 75 | 76 | for input in input.chunks_mut(self.buffer.len()) { 77 | let num_frames = input.len(); 78 | 79 | // read from the beginning of the buffer and apply effects and feedback gain 80 | self.temp_buffer[..input.len()].copy_from_slice(&self.buffer[..input.len()]); 81 | for effect in &mut self.feedback_effects { 82 | effect.process(&mut self.temp_buffer[..input.len()], dt, info); 83 | } 84 | for (i, frame) in self.temp_buffer[..input.len()].iter_mut().enumerate() { 85 | let time_in_chunk = (i + 1) as f64 / num_frames as f64; 86 | let feedback = self.feedback.interpolated_value(time_in_chunk); 87 | *frame *= feedback.as_amplitude(); 88 | } 89 | 90 | // write input + read buffer to the end of the buffer 91 | self.buffer.copy_within(input.len().., 0); 92 | let write_range = self.buffer.len() - input.len()..; 93 | for ((out, input), read) in self.buffer[write_range] 94 | .iter_mut() 95 | .zip(input.iter()) 96 | .zip(&mut self.temp_buffer[..input.len()]) 97 | { 98 | *out = *input + *read; 99 | } 100 | 101 | // output mix of input and read buffer 102 | for (i, frame) in input.iter_mut().enumerate() { 103 | let time_in_chunk = (i + 1) as f64 / num_frames as f64; 104 | let mix = self.mix.interpolated_value(time_in_chunk).0.clamp(0.0, 1.0); 105 | *frame = self.temp_buffer[i] * mix.sqrt() + *frame * (1.0 - mix).sqrt() 106 | } 107 | } 108 | } 109 | } 110 | 111 | command_writers_and_readers! { 112 | set_feedback: ValueChangeCommand, 113 | set_mix: ValueChangeCommand, 114 | } 115 | -------------------------------------------------------------------------------- /crates/kira/src/effect/delay/builder.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{ 4 | effect::{Effect, EffectBuilder}, 5 | Decibels, Mix, Value, 6 | }; 7 | 8 | use super::{command_writers_and_readers, Delay, DelayHandle}; 9 | 10 | /// Configures a delay effect. 11 | pub struct DelayBuilder { 12 | /// The amount of time the input audio is delayed by. 13 | pub(super) delay_time: Duration, 14 | /// The amount of feedback. 15 | pub(super) feedback: Value, 16 | /// Effects that should be applied in the feedback loop. 17 | pub(super) feedback_effects: Vec>, 18 | /// How much dry (unprocessed) signal should be blended 19 | /// with the wet (processed) signal. 20 | pub(super) mix: Value, 21 | } 22 | 23 | impl DelayBuilder { 24 | /// Creates a new [`DelayBuilder`] with the default settings. 25 | #[must_use] 26 | pub fn new() -> Self { 27 | Self::default() 28 | } 29 | 30 | /// Sets the amount of time the input audio is delayed by. 31 | #[must_use = "This method consumes self and returns a modified DelayBuilder, so the return value should be used"] 32 | pub fn delay_time(self, delay_time: Duration) -> Self { 33 | Self { delay_time, ..self } 34 | } 35 | 36 | /// Sets the amount of feedback. 37 | #[must_use = "This method consumes self and returns a modified DelayBuilder, so the return value should be used"] 38 | pub fn feedback(self, feedback: impl Into>) -> Self { 39 | Self { 40 | feedback: feedback.into(), 41 | ..self 42 | } 43 | } 44 | 45 | /// Adds an effect to the feedback loop. 46 | pub fn add_feedback_effect(&mut self, builder: B) -> B::Handle { 47 | let (effect, handle) = builder.build(); 48 | self.feedback_effects.push(effect); 49 | handle 50 | } 51 | 52 | /// Adds an effect to the feedback loop and returns the [`DelayBuilder`]. 53 | /// 54 | /// If you need a handle to the newly added effect, use [`DelayBuilder::add_feedback_effect`]. 55 | pub fn with_feedback_effect(mut self, builder: B) -> Self { 56 | self.add_feedback_effect(builder); 57 | self 58 | } 59 | 60 | /// Sets how much dry (unprocessed) signal should be blended 61 | /// with the wet (processed) signal. `0.0` means only the dry 62 | /// signal will be heard. `1.0` means only the wet signal will 63 | /// be heard. 64 | #[must_use = "This method consumes self and returns a modified DelayBuilder, so the return value should be used"] 65 | pub fn mix(self, mix: impl Into>) -> Self { 66 | Self { 67 | mix: mix.into(), 68 | ..self 69 | } 70 | } 71 | } 72 | 73 | impl Default for DelayBuilder { 74 | fn default() -> Self { 75 | Self { 76 | delay_time: Duration::from_millis(500), 77 | feedback: Value::Fixed(Decibels(-6.0)), 78 | feedback_effects: vec![], 79 | mix: Value::Fixed(Mix(0.5)), 80 | } 81 | } 82 | } 83 | 84 | impl EffectBuilder for DelayBuilder { 85 | type Handle = DelayHandle; 86 | 87 | fn build(self) -> (Box, Self::Handle) { 88 | let (command_writers, command_readers) = command_writers_and_readers(); 89 | ( 90 | Box::new(Delay::new(self, command_readers)), 91 | DelayHandle { command_writers }, 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /crates/kira/src/effect/delay/handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{command::handle_param_setters, Decibels, Mix}; 2 | 3 | use super::CommandWriters; 4 | 5 | /// Controls a delay effect. 6 | #[derive(Debug)] 7 | pub struct DelayHandle { 8 | pub(super) command_writers: CommandWriters, 9 | } 10 | 11 | impl DelayHandle { 12 | handle_param_setters! { 13 | /// Sets the amount of feedback. 14 | feedback: Decibels, 15 | 16 | /// Sets how much dry (unprocessed) signal should be blended 17 | /// with the wet (processed) signal. 18 | mix: Mix, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/kira/src/effect/distortion.rs: -------------------------------------------------------------------------------- 1 | //! Makes a sound harsher and noisier. 2 | 3 | mod builder; 4 | mod handle; 5 | 6 | pub use builder::*; 7 | pub use handle::*; 8 | 9 | use crate::{ 10 | command::{read_commands_into_parameters, ValueChangeCommand}, 11 | command_writers_and_readers, 12 | effect::Effect, 13 | frame::Frame, 14 | info::Info, 15 | Decibels, Mix, Parameter, 16 | }; 17 | 18 | /// Different types of distortion. 19 | #[derive(Debug, Copy, Clone, PartialEq)] 20 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 21 | pub enum DistortionKind { 22 | /// The signal will be clamped to the -1.0 to 1.0 range. 23 | /// 24 | /// This creates a harsh distortion when the signal leaves 25 | /// the -1.0 to 1.0 range. 26 | HardClip, 27 | /// The signal will be kept in the -1.0 to 1.0 range, 28 | /// and the slope will gradually decrease as it reaches 29 | /// -1.0 or 1.0. 30 | /// 31 | /// This creates a smoother distortion that gradually 32 | /// becomes more prominent as the signal becomes louder. 33 | SoftClip, 34 | } 35 | 36 | impl Default for DistortionKind { 37 | fn default() -> Self { 38 | Self::HardClip 39 | } 40 | } 41 | 42 | struct Distortion { 43 | command_readers: CommandReaders, 44 | kind: DistortionKind, 45 | drive: Parameter, 46 | mix: Parameter, 47 | } 48 | 49 | impl Effect for Distortion { 50 | fn on_start_processing(&mut self) { 51 | if let Some(kind) = self.command_readers.set_kind.read() { 52 | self.kind = kind; 53 | } 54 | read_commands_into_parameters!(self, drive, mix); 55 | } 56 | 57 | fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) { 58 | self.drive.update(dt * input.len() as f64, info); 59 | self.mix.update(dt * input.len() as f64, info); 60 | 61 | let num_frames = input.len(); 62 | for (i, frame) in input.iter_mut().enumerate() { 63 | let time_in_chunk = (i + 1) as f64 / num_frames as f64; 64 | let drive = self.drive.interpolated_value(time_in_chunk).as_amplitude(); 65 | let mix = self.mix.interpolated_value(time_in_chunk).0.clamp(0.0, 1.0); 66 | 67 | let mut output = *frame * drive; 68 | output = match self.kind { 69 | DistortionKind::HardClip => { 70 | Frame::new(output.left.clamp(-1.0, 1.0), output.right.clamp(-1.0, 1.0)) 71 | } 72 | DistortionKind::SoftClip => Frame::new( 73 | output.left / (1.0 + output.left.abs()), 74 | output.right / (1.0 + output.right.abs()), 75 | ), 76 | }; 77 | output /= drive; 78 | 79 | *frame = output * mix.sqrt() + *frame * (1.0 - mix).sqrt() 80 | } 81 | } 82 | } 83 | 84 | command_writers_and_readers! { 85 | set_kind: DistortionKind, 86 | set_drive: ValueChangeCommand, 87 | set_mix: ValueChangeCommand, 88 | } 89 | -------------------------------------------------------------------------------- /crates/kira/src/effect/distortion/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | effect::{Effect, EffectBuilder}, 3 | Decibels, Mix, Parameter, Value, 4 | }; 5 | 6 | use super::{command_writers_and_readers, handle::DistortionHandle, Distortion, DistortionKind}; 7 | 8 | /// Configures a distortion effect. 9 | #[derive(Debug, Copy, Clone, PartialEq)] 10 | pub struct DistortionBuilder { 11 | /// The kind of distortion to use. 12 | pub kind: DistortionKind, 13 | /// The factor to multiply the signal by before applying 14 | /// the distortion. 15 | pub drive: Value, 16 | /// How much dry (unprocessed) signal should be blended 17 | /// with the wet (processed) signal. 18 | pub mix: Value, 19 | } 20 | 21 | impl DistortionBuilder { 22 | /// Creates a new [`DistortionBuilder`] with the default settings. 23 | #[must_use] 24 | pub fn new() -> Self { 25 | Self::default() 26 | } 27 | 28 | /// Sets the kind of distortion to use. 29 | #[must_use = "This method consumes self and returns a modified DistortionBuilder, so the return value should be used"] 30 | pub fn kind(self, kind: DistortionKind) -> Self { 31 | Self { kind, ..self } 32 | } 33 | 34 | /// Sets the factor to multiply the signal by before applying 35 | /// the distortion. 36 | #[must_use = "This method consumes self and returns a modified DistortionBuilder, so the return value should be used"] 37 | pub fn drive(self, drive: impl Into>) -> Self { 38 | Self { 39 | drive: drive.into(), 40 | ..self 41 | } 42 | } 43 | 44 | /// Sets how much dry (unprocessed) signal should be blended 45 | /// with the wet (processed) signal. `0.0` means only the dry 46 | /// signal will be heard. `1.0` means only the wet signal will 47 | /// be heard. 48 | #[must_use = "This method consumes self and returns a modified DistortionBuilder, so the return value should be used"] 49 | pub fn mix(self, mix: impl Into>) -> Self { 50 | Self { 51 | mix: mix.into(), 52 | ..self 53 | } 54 | } 55 | } 56 | 57 | impl Default for DistortionBuilder { 58 | fn default() -> Self { 59 | Self { 60 | kind: Default::default(), 61 | drive: Value::Fixed(Decibels::IDENTITY), 62 | mix: Value::Fixed(Mix::WET), 63 | } 64 | } 65 | } 66 | 67 | impl EffectBuilder for DistortionBuilder { 68 | type Handle = DistortionHandle; 69 | 70 | fn build(self) -> (Box, Self::Handle) { 71 | let (command_writers, command_readers) = command_writers_and_readers(); 72 | ( 73 | Box::new(Distortion { 74 | command_readers, 75 | kind: self.kind, 76 | drive: Parameter::new(self.drive, Decibels::IDENTITY), 77 | mix: Parameter::new(self.mix, Mix::WET), 78 | }), 79 | DistortionHandle { command_writers }, 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/kira/src/effect/distortion/handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{command::handle_param_setters, Decibels, Mix}; 2 | 3 | use super::{CommandWriters, DistortionKind}; 4 | 5 | /// Controls a distortion effect. 6 | #[derive(Debug)] 7 | pub struct DistortionHandle { 8 | pub(super) command_writers: CommandWriters, 9 | } 10 | 11 | impl DistortionHandle { 12 | /// Sets the kind of distortion to use. 13 | pub fn set_kind(&mut self, kind: DistortionKind) { 14 | self.command_writers.set_kind.write(kind) 15 | } 16 | 17 | handle_param_setters! { 18 | /// Sets how much distortion should be applied. 19 | drive: Decibels, 20 | 21 | /// Sets how much dry (unprocessed) signal should be blended 22 | /// with the wet (processed) signal. 23 | mix: Mix, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/kira/src/effect/eq_filter/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{effect::EffectBuilder, Decibels, Value}; 2 | 3 | use super::{command_writers_and_readers, EqFilter, EqFilterHandle, EqFilterKind}; 4 | 5 | /// Configures an EQ filter. 6 | pub struct EqFilterBuilder { 7 | /// The shape of the frequency adjustment curve. 8 | pub kind: EqFilterKind, 9 | /// The "center" or "corner" of the frequency range to adjust in Hz 10 | /// (for bell or shelf curves, respectively). 11 | pub frequency: Value, 12 | /// The volume adjustment for frequencies in the specified range (in decibels). 13 | pub gain: Value, 14 | /// The width of the frequency range to adjust. 15 | /// 16 | /// A higher Q value results in a narrower range of frequencies being adjusted. 17 | /// The value should be greater than `0.0`. 18 | pub q: Value, 19 | } 20 | 21 | impl EqFilterBuilder { 22 | /// Creates a new `EqFilterBuilder`. 23 | #[must_use] 24 | pub fn new( 25 | kind: EqFilterKind, 26 | frequency: impl Into>, 27 | gain: impl Into>, 28 | q: impl Into>, 29 | ) -> Self { 30 | Self { 31 | kind, 32 | frequency: frequency.into(), 33 | gain: gain.into(), 34 | q: q.into(), 35 | } 36 | } 37 | } 38 | 39 | impl EffectBuilder for EqFilterBuilder { 40 | type Handle = EqFilterHandle; 41 | 42 | fn build(self) -> (Box, Self::Handle) { 43 | let (command_writers, command_readers) = command_writers_and_readers(); 44 | ( 45 | Box::new(EqFilter::new(self, command_readers)), 46 | EqFilterHandle { command_writers }, 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/kira/src/effect/eq_filter/handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{command::handle_param_setters, Decibels}; 2 | 3 | use super::{CommandWriters, EqFilterKind}; 4 | 5 | /// Controls an EQ filter. 6 | #[derive(Debug)] 7 | pub struct EqFilterHandle { 8 | pub(super) command_writers: CommandWriters, 9 | } 10 | 11 | impl EqFilterHandle { 12 | /// Sets the shape of the frequency adjustment curve. 13 | pub fn set_kind(&mut self, kind: EqFilterKind) { 14 | self.command_writers.set_kind.write(kind) 15 | } 16 | 17 | handle_param_setters! { 18 | /// Sets the "center" or "corner" of the frequency range to adjust in Hz 19 | /// (for bell or shelf curves, respectively). 20 | frequency: f64, 21 | 22 | /// Sets the volume adjustment for frequencies in the specified range (in decibels). 23 | gain: Decibels, 24 | 25 | /// Sets the width of the frequency range to adjust. 26 | /// 27 | /// A higher Q value results in a narrower range of frequencies being adjusted. 28 | /// The value should be greater than `0.0`. 29 | q: f64, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/kira/src/effect/filter.rs: -------------------------------------------------------------------------------- 1 | //! Removes frequencies from a sound. 2 | 3 | mod builder; 4 | mod handle; 5 | 6 | pub use builder::*; 7 | pub use handle::*; 8 | 9 | use std::f64::consts::PI; 10 | 11 | use crate::{ 12 | command::{read_commands_into_parameters, ValueChangeCommand}, 13 | command_writers_and_readers, 14 | effect::Effect, 15 | frame::Frame, 16 | info::Info, 17 | Mix, Parameter, 18 | }; 19 | 20 | // This filter code is based on the filter code from baseplug: 21 | // https://github.com/wrl/baseplug/blob/trunk/examples/svf/svf_simper.rs 22 | 23 | /// The frequencies that the filter will remove. 24 | #[derive(Debug, Copy, Clone, PartialEq)] 25 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 26 | pub enum FilterMode { 27 | /// Removes frequencies above the cutoff frequency. 28 | LowPass, 29 | /// Removes frequencies above and below the cutoff frequency. 30 | BandPass, 31 | /// Removes frequencies below the cutoff frequency. 32 | HighPass, 33 | /// Removes frequencies around the cutoff frequency. 34 | Notch, 35 | } 36 | 37 | struct Filter { 38 | command_readers: CommandReaders, 39 | mode: FilterMode, 40 | cutoff: Parameter, 41 | resonance: Parameter, 42 | mix: Parameter, 43 | ic1eq: Frame, 44 | ic2eq: Frame, 45 | } 46 | 47 | impl Filter { 48 | /// Creates a new filter. 49 | #[must_use] 50 | fn new(builder: FilterBuilder, command_readers: CommandReaders) -> Self { 51 | Self { 52 | command_readers, 53 | mode: builder.mode, 54 | cutoff: Parameter::new(builder.cutoff, 1000.0), 55 | resonance: Parameter::new(builder.resonance, 0.0), 56 | mix: Parameter::new(builder.mix, Mix(1.0)), 57 | ic1eq: Frame::ZERO, 58 | ic2eq: Frame::ZERO, 59 | } 60 | } 61 | } 62 | 63 | impl Effect for Filter { 64 | fn on_start_processing(&mut self) { 65 | if let Some(mode) = self.command_readers.set_mode.read() { 66 | self.mode = mode; 67 | } 68 | read_commands_into_parameters!(self, cutoff, resonance, mix); 69 | } 70 | 71 | fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) { 72 | self.cutoff.update(dt * input.len() as f64, info); 73 | self.resonance.update(dt * input.len() as f64, info); 74 | self.mix.update(dt * input.len() as f64, info); 75 | 76 | let num_frames = input.len(); 77 | for (i, frame) in input.iter_mut().enumerate() { 78 | let time_in_chunk = (i + 1) as f64 / num_frames as f64; 79 | let cutoff = self.cutoff.interpolated_value(time_in_chunk); 80 | let resonance = self 81 | .resonance 82 | .interpolated_value(time_in_chunk) 83 | .clamp(0.0, 1.0); 84 | let mix = self.mix.interpolated_value(time_in_chunk).0.clamp(0.0, 1.0); 85 | 86 | let sample_rate = 1.0 / dt; 87 | let g = (PI * (cutoff / sample_rate).clamp(0.0001, 0.5)).tan(); 88 | let k = 2.0 - (1.9 * resonance); 89 | let a1 = 1.0 / (1.0 + (g * (g + k))); 90 | let a2 = g * a1; 91 | let a3 = g * a2; 92 | let v3 = *frame - self.ic2eq; 93 | let v1 = (self.ic1eq * (a1 as f32)) + (v3 * (a2 as f32)); 94 | let v2 = self.ic2eq + (self.ic1eq * (a2 as f32)) + (v3 * (a3 as f32)); 95 | self.ic1eq = (v1 * 2.0) - self.ic1eq; 96 | self.ic2eq = (v2 * 2.0) - self.ic2eq; 97 | let output = match self.mode { 98 | FilterMode::LowPass => v2, 99 | FilterMode::BandPass => v1, 100 | FilterMode::HighPass => *frame - v1 * (k as f32) - v2, 101 | FilterMode::Notch => *frame - v1 * (k as f32), 102 | }; 103 | *frame = output * mix.sqrt() + *frame * (1.0 - mix).sqrt() 104 | } 105 | } 106 | } 107 | 108 | command_writers_and_readers!( 109 | set_mode: FilterMode, 110 | set_cutoff: ValueChangeCommand, 111 | set_resonance: ValueChangeCommand, 112 | set_mix: ValueChangeCommand, 113 | ); 114 | -------------------------------------------------------------------------------- /crates/kira/src/effect/filter/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | effect::{Effect, EffectBuilder}, 3 | Mix, Value, 4 | }; 5 | 6 | use super::{command_writers_and_readers, Filter, FilterHandle, FilterMode}; 7 | 8 | /// Configures a filter effect. 9 | #[derive(Debug, Copy, Clone, PartialEq)] 10 | pub struct FilterBuilder { 11 | /// The frequencies that the filter will remove. 12 | pub mode: FilterMode, 13 | /// The cutoff frequency of the filter (in hertz). 14 | pub cutoff: Value, 15 | /// The resonance of the filter. 16 | /// 17 | /// The resonance is a feedback effect that produces 18 | /// a distinctive "ringing" sound. 19 | pub resonance: Value, 20 | /// How much dry (unprocessed) signal should be blended 21 | /// with the wet (processed) signal. 22 | pub mix: Value, 23 | } 24 | 25 | impl FilterBuilder { 26 | /// Creates a new [`FilterBuilder`] with the default settings. 27 | #[must_use] 28 | pub fn new() -> Self { 29 | Self::default() 30 | } 31 | 32 | /// Sets the frequencies that the filter will remove. 33 | #[must_use = "This method consumes self and returns a modified FilterBuilder, so the return value should be used"] 34 | pub fn mode(self, mode: FilterMode) -> Self { 35 | Self { mode, ..self } 36 | } 37 | 38 | /// Sets the cutoff frequency of the filter (in hertz). 39 | #[must_use = "This method consumes self and returns a modified FilterBuilder, so the return value should be used"] 40 | pub fn cutoff(self, cutoff: impl Into>) -> Self { 41 | Self { 42 | cutoff: cutoff.into(), 43 | ..self 44 | } 45 | } 46 | 47 | /// Sets the resonance of the filter. 48 | #[must_use = "This method consumes self and returns a modified FilterBuilder, so the return value should be used"] 49 | pub fn resonance(self, resonance: impl Into>) -> Self { 50 | Self { 51 | resonance: resonance.into(), 52 | ..self 53 | } 54 | } 55 | 56 | /// Sets how much dry (unprocessed) signal should be blended 57 | /// with the wet (processed) signal. `0.0` means only the dry 58 | /// signal will be heard. `1.0` means only the wet signal will 59 | /// be heard. 60 | #[must_use = "This method consumes self and returns a modified FilterBuilder, so the return value should be used"] 61 | pub fn mix(self, mix: impl Into>) -> Self { 62 | Self { 63 | mix: mix.into(), 64 | ..self 65 | } 66 | } 67 | } 68 | 69 | impl Default for FilterBuilder { 70 | fn default() -> Self { 71 | Self { 72 | mode: FilterMode::LowPass, 73 | cutoff: Value::Fixed(1000.0), 74 | resonance: Value::Fixed(0.0), 75 | mix: Value::Fixed(Mix::WET), 76 | } 77 | } 78 | } 79 | 80 | impl EffectBuilder for FilterBuilder { 81 | type Handle = FilterHandle; 82 | 83 | fn build(self) -> (Box, Self::Handle) { 84 | let (command_writers, command_readers) = command_writers_and_readers(); 85 | ( 86 | Box::new(Filter::new(self, command_readers)), 87 | FilterHandle { command_writers }, 88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crates/kira/src/effect/filter/handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{command::handle_param_setters, Mix}; 2 | 3 | use super::{CommandWriters, FilterMode}; 4 | 5 | /// Controls a filter effect. 6 | #[derive(Debug)] 7 | pub struct FilterHandle { 8 | pub(super) command_writers: CommandWriters, 9 | } 10 | 11 | impl FilterHandle { 12 | /// Sets the frequencies that the filter will remove. 13 | pub fn set_mode(&mut self, mode: FilterMode) { 14 | self.command_writers.set_mode.write(mode) 15 | } 16 | 17 | handle_param_setters! { 18 | /// Sets the cutoff frequency of the filter (in hertz). 19 | cutoff: f64, 20 | 21 | /// Sets the resonance of the filter. 22 | resonance: f64, 23 | 24 | /// Sets how much dry (unprocessed) signal should be blended 25 | /// with the wet (processed) signal. 26 | mix: Mix, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/kira/src/effect/panning_control.rs: -------------------------------------------------------------------------------- 1 | //! Adjusts the panning of audio. 2 | 3 | mod builder; 4 | mod handle; 5 | 6 | pub use builder::*; 7 | pub use handle::*; 8 | 9 | use crate::{ 10 | command::{read_commands_into_parameters, ValueChangeCommand}, 11 | command_writers_and_readers, 12 | frame::Frame, 13 | info::Info, 14 | Panning, Parameter, 15 | }; 16 | 17 | use super::Effect; 18 | 19 | struct PanningControl { 20 | command_readers: CommandReaders, 21 | panning: Parameter, 22 | } 23 | 24 | impl PanningControl { 25 | #[must_use] 26 | fn new(builder: PanningControlBuilder, command_readers: CommandReaders) -> Self { 27 | Self { 28 | command_readers, 29 | panning: Parameter::new(builder.0, Panning::CENTER), 30 | } 31 | } 32 | } 33 | 34 | impl Effect for PanningControl { 35 | fn on_start_processing(&mut self) { 36 | read_commands_into_parameters!(self, panning); 37 | } 38 | 39 | fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) { 40 | self.panning.update(dt * input.len() as f64, info); 41 | let num_frames = input.len(); 42 | for (i, frame) in input.iter_mut().enumerate() { 43 | let time_in_chunk = (i + 1) as f64 / num_frames as f64; 44 | *frame = frame.panned(self.panning.interpolated_value(time_in_chunk)) 45 | } 46 | } 47 | } 48 | 49 | command_writers_and_readers! { 50 | set_panning: ValueChangeCommand, 51 | } 52 | -------------------------------------------------------------------------------- /crates/kira/src/effect/panning_control/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{effect::EffectBuilder, Panning, Value}; 2 | 3 | use super::{command_writers_and_readers, PanningControl, PanningControlHandle}; 4 | 5 | /// Configures a panning control effect. 6 | #[derive(Debug, Copy, Clone, PartialEq)] 7 | pub struct PanningControlBuilder(pub Value); 8 | 9 | impl Default for PanningControlBuilder { 10 | fn default() -> Self { 11 | Self(Value::Fixed(Panning::CENTER)) 12 | } 13 | } 14 | 15 | impl EffectBuilder for PanningControlBuilder { 16 | type Handle = PanningControlHandle; 17 | 18 | fn build(self) -> (Box, Self::Handle) { 19 | let (command_writers, command_readers) = command_writers_and_readers(); 20 | ( 21 | Box::new(PanningControl::new(self, command_readers)), 22 | PanningControlHandle { command_writers }, 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/kira/src/effect/panning_control/handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{command::handle_param_setters, Panning}; 2 | 3 | use super::CommandWriters; 4 | 5 | /// Controls a panning control effect. 6 | #[derive(Debug)] 7 | pub struct PanningControlHandle { 8 | pub(super) command_writers: CommandWriters, 9 | } 10 | 11 | impl PanningControlHandle { 12 | handle_param_setters! { 13 | /// Sets the panning adjustment to apply to input audio. 14 | panning: Panning, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/kira/src/effect/reverb/all_pass.rs: -------------------------------------------------------------------------------- 1 | const FEEDBACK: f32 = 0.5; 2 | 3 | #[derive(Debug)] 4 | pub struct AllPassFilter { 5 | buffer: Vec, 6 | current_index: usize, 7 | } 8 | 9 | impl AllPassFilter { 10 | #[must_use] 11 | pub fn new(buffer_size: usize) -> Self { 12 | Self { 13 | buffer: vec![0.0; buffer_size], 14 | current_index: 0, 15 | } 16 | } 17 | 18 | #[must_use] 19 | pub fn process(&mut self, input: f32) -> f32 { 20 | let buffer_output = self.buffer[self.current_index]; 21 | let output = -input + buffer_output; 22 | self.buffer[self.current_index] = input + buffer_output * FEEDBACK; 23 | self.current_index += 1; 24 | self.current_index %= self.buffer.len(); 25 | output 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/kira/src/effect/reverb/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | effect::{Effect, EffectBuilder}, 3 | Mix, Value, 4 | }; 5 | 6 | use super::{command_writers_and_readers, Reverb, ReverbHandle}; 7 | 8 | /// Configures a reverb effect. 9 | #[derive(Debug, Copy, Clone, PartialEq)] 10 | pub struct ReverbBuilder { 11 | /// How much the room reverberates. A higher value will 12 | /// result in a bigger sounding room. 1.0 gives an infinitely 13 | /// reverberating room. 14 | pub feedback: Value, 15 | /// How quickly high frequencies disappear from the reverberation. 16 | pub damping: Value, 17 | /// The stereo width of the reverb effect (0.0 being fully mono, 18 | /// 1.0 being fully stereo). 19 | pub stereo_width: Value, 20 | /// How much dry (unprocessed) signal should be blended 21 | /// with the wet (processed) signal. 22 | pub mix: Value, 23 | } 24 | 25 | impl ReverbBuilder { 26 | /// Creates a new [`ReverbBuilder`] with the default settings. 27 | #[must_use] 28 | pub fn new() -> Self { 29 | Self::default() 30 | } 31 | 32 | /// Sets how much the room reverberates. A higher value will 33 | /// result in a bigger sounding room. 1.0 gives an infinitely 34 | /// reverberating room. 35 | #[must_use = "This method consumes self and returns a modified ReverbBuilder, so the return value should be used"] 36 | pub fn feedback(self, feedback: impl Into>) -> Self { 37 | Self { 38 | feedback: feedback.into(), 39 | ..self 40 | } 41 | } 42 | 43 | /// Sets how quickly high frequencies disappear from the reverberation. 44 | #[must_use = "This method consumes self and returns a modified ReverbBuilder, so the return value should be used"] 45 | pub fn damping(self, damping: impl Into>) -> Self { 46 | Self { 47 | damping: damping.into(), 48 | ..self 49 | } 50 | } 51 | 52 | /// Sets the stereo width of the reverb effect (0.0 being fully mono, 53 | /// 1.0 being fully stereo). 54 | #[must_use = "This method consumes self and returns a modified ReverbBuilder, so the return value should be used"] 55 | pub fn stereo_width(self, stereo_width: impl Into>) -> Self { 56 | Self { 57 | stereo_width: stereo_width.into(), 58 | ..self 59 | } 60 | } 61 | 62 | /// Sets how much dry (unprocessed) signal should be blended 63 | /// with the wet (processed) signal. `0.0` means only the dry 64 | /// signal will be heard. `1.0` means only the wet signal will 65 | /// be heard. 66 | #[must_use = "This method consumes self and returns a modified ReverbBuilder, so the return value should be used"] 67 | pub fn mix(self, mix: impl Into>) -> Self { 68 | Self { 69 | mix: mix.into(), 70 | ..self 71 | } 72 | } 73 | } 74 | 75 | impl Default for ReverbBuilder { 76 | fn default() -> Self { 77 | Self { 78 | feedback: Value::Fixed(0.9), 79 | damping: Value::Fixed(0.1), 80 | stereo_width: Value::Fixed(1.0), 81 | mix: Value::Fixed(Mix(0.5)), 82 | } 83 | } 84 | } 85 | 86 | impl EffectBuilder for ReverbBuilder { 87 | type Handle = ReverbHandle; 88 | 89 | fn build(self) -> (Box, Self::Handle) { 90 | let (command_writers, command_readers) = command_writers_and_readers(); 91 | ( 92 | Box::new(Reverb::new(self, command_readers)), 93 | ReverbHandle { command_writers }, 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /crates/kira/src/effect/reverb/comb.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct CombFilter { 3 | filter_store: f32, 4 | buffer: Vec, 5 | current_index: usize, 6 | } 7 | 8 | impl CombFilter { 9 | #[must_use] 10 | pub fn new(buffer_size: usize) -> Self { 11 | Self { 12 | filter_store: 0.0, 13 | buffer: vec![0.0; buffer_size], 14 | current_index: 0, 15 | } 16 | } 17 | 18 | #[must_use] 19 | pub fn process(&mut self, input: f32, feedback: f32, damp: f32) -> f32 { 20 | let output = self.buffer[self.current_index]; 21 | self.filter_store = output * (1.0 - damp) + self.filter_store * damp; 22 | self.buffer[self.current_index] = input + self.filter_store * feedback; 23 | self.current_index += 1; 24 | self.current_index %= self.buffer.len(); 25 | output 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/kira/src/effect/reverb/handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{command::handle_param_setters, Mix}; 2 | 3 | use super::CommandWriters; 4 | 5 | /// Controls a reverb effect. 6 | #[derive(Debug)] 7 | pub struct ReverbHandle { 8 | pub(super) command_writers: CommandWriters, 9 | } 10 | 11 | impl ReverbHandle { 12 | handle_param_setters! { 13 | /// Sets how much the room reverberates. A higher value will 14 | /// result in a bigger sounding room. 1.0 gives an infinitely 15 | /// reverberating room. 16 | feedback: f64, 17 | 18 | /// Sets how quickly high frequencies disappear from the reverberation. 19 | damping: f64, 20 | 21 | /// Sets the stereo width of the reverb effect (0.0 being fully mono, 22 | /// 1.0 being fully stereo). 23 | stereo_width: f64, 24 | 25 | /// Sets how much dry (unprocessed) signal should be blended 26 | /// with the wet (processed) signal. 27 | mix: Mix, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/kira/src/effect/volume_control.rs: -------------------------------------------------------------------------------- 1 | //! Adjusts the volume of audio. 2 | 3 | mod builder; 4 | mod handle; 5 | 6 | pub use builder::*; 7 | pub use handle::*; 8 | 9 | use crate::{ 10 | command::{read_commands_into_parameters, ValueChangeCommand}, 11 | command_writers_and_readers, 12 | frame::Frame, 13 | info::Info, 14 | Decibels, Parameter, 15 | }; 16 | 17 | use super::Effect; 18 | 19 | struct VolumeControl { 20 | command_readers: CommandReaders, 21 | volume: Parameter, 22 | } 23 | 24 | impl VolumeControl { 25 | #[must_use] 26 | fn new(builder: VolumeControlBuilder, command_readers: CommandReaders) -> Self { 27 | Self { 28 | command_readers, 29 | volume: Parameter::new(builder.0, Decibels::IDENTITY), 30 | } 31 | } 32 | } 33 | 34 | impl Effect for VolumeControl { 35 | fn on_start_processing(&mut self) { 36 | read_commands_into_parameters!(self, volume); 37 | } 38 | 39 | fn process(&mut self, input: &mut [Frame], dt: f64, info: &Info) { 40 | self.volume.update(dt * input.len() as f64, info); 41 | let num_frames = input.len(); 42 | for (i, frame) in input.iter_mut().enumerate() { 43 | let time_in_chunk = (i + 1) as f64 / num_frames as f64; 44 | *frame *= self.volume.interpolated_value(time_in_chunk).as_amplitude(); 45 | } 46 | } 47 | } 48 | 49 | command_writers_and_readers! { 50 | set_volume: ValueChangeCommand, 51 | } 52 | -------------------------------------------------------------------------------- /crates/kira/src/effect/volume_control/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{effect::EffectBuilder, Decibels, Value}; 2 | 3 | use super::{command_writers_and_readers, VolumeControl, VolumeControlHandle}; 4 | 5 | /// Configures a volume control effect. 6 | #[derive(Debug, Copy, Clone, PartialEq)] 7 | pub struct VolumeControlBuilder(pub Value); 8 | 9 | impl VolumeControlBuilder { 10 | /// Creates a new [`VolumeControlBuilder`]. 11 | #[must_use] 12 | pub fn new(volume: impl Into>) -> Self { 13 | Self(volume.into()) 14 | } 15 | } 16 | 17 | impl Default for VolumeControlBuilder { 18 | fn default() -> Self { 19 | Self(Value::Fixed(Decibels::IDENTITY)) 20 | } 21 | } 22 | 23 | impl EffectBuilder for VolumeControlBuilder { 24 | type Handle = VolumeControlHandle; 25 | 26 | fn build(self) -> (Box, Self::Handle) { 27 | let (command_writers, command_readers) = command_writers_and_readers(); 28 | ( 29 | Box::new(VolumeControl::new(self, command_readers)), 30 | VolumeControlHandle { command_writers }, 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/kira/src/effect/volume_control/handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{command::handle_param_setters, Decibels}; 2 | 3 | use super::CommandWriters; 4 | 5 | /// Controls a volume control effect. 6 | #[derive(Debug)] 7 | pub struct VolumeControlHandle { 8 | pub(super) command_writers: CommandWriters, 9 | } 10 | 11 | impl VolumeControlHandle { 12 | handle_param_setters! { 13 | /// Sets the volume adjustment to apply to input audio. 14 | volume: Decibels, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/kira/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt::{Display, Formatter}, 4 | }; 5 | 6 | /// Errors that can occur when playing a sound. 7 | #[derive(Debug)] 8 | pub enum PlaySoundError { 9 | /// Could not play a sound because the maximum number of sounds has been reached. 10 | SoundLimitReached, 11 | /// An error occurred when initializing the sound. 12 | IntoSoundError(E), 13 | } 14 | 15 | impl Display for PlaySoundError { 16 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 17 | match self { 18 | PlaySoundError::SoundLimitReached => f.write_str( 19 | "Could not play a sound because the maximum number of sounds has been reached.", 20 | ), 21 | PlaySoundError::IntoSoundError(_) => { 22 | f.write_str("An error occurred when initializing the sound.") 23 | } 24 | } 25 | } 26 | } 27 | 28 | impl Error for PlaySoundError {} 29 | 30 | /// An error that is returned when a resource cannot be added because the 31 | /// maximum capacity for that resource has been reached. 32 | /// 33 | /// You can adjust these capacities using [`Capacities`](crate::Capacities). 34 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 35 | pub struct ResourceLimitReached; 36 | 37 | impl Display for ResourceLimitReached { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | f.write_str("Could not add a resource because the maximum capacity for that resource has been reached") 40 | } 41 | } 42 | 43 | impl Error for ResourceLimitReached {} 44 | -------------------------------------------------------------------------------- /crates/kira/src/frame.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | f32::consts::SQRT_2, 3 | ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, 4 | }; 5 | 6 | use crate::Panning; 7 | 8 | /// A stereo audio sample. 9 | #[derive(Debug, Copy, Clone, PartialEq, Default)] 10 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 11 | pub struct Frame { 12 | /// The sample for the left channel. 13 | pub left: f32, 14 | /// The sample for the right channel. 15 | pub right: f32, 16 | } 17 | 18 | impl Frame { 19 | /// A [`Frame`] with both the left and right samples 20 | /// set to `0.0`. 21 | pub const ZERO: Frame = Frame { 22 | left: 0.0, 23 | right: 0.0, 24 | }; 25 | 26 | /// Creates a frame with the given left and right values. 27 | #[must_use] 28 | pub fn new(left: f32, right: f32) -> Self { 29 | Self { left, right } 30 | } 31 | 32 | /// Creates a frame with both the left and right channels set 33 | /// to the same value. 34 | #[must_use] 35 | pub fn from_mono(value: f32) -> Self { 36 | Self::new(value, value) 37 | } 38 | 39 | /// Pans a frame to the left or right. 40 | /// 41 | /// A panning of -1.0 represents a hard left panning, a panning of 1.0 42 | /// represents a hard right panning. 43 | #[allow(clippy::float_cmp)] 44 | #[must_use = "This method returns a new Frame and does not mutate the original value"] 45 | pub fn panned(self, panning: Panning) -> Self { 46 | // adding a special case for center panning improves 47 | // performance in the sound playback benchmarks by 48 | // about 3% 49 | if panning == Panning::CENTER { 50 | return self; 51 | } 52 | let panning = panning.0.clamp(-1.0, 1.0); 53 | let left_right_mix = (panning + 1.0) * 0.5; 54 | Self::new( 55 | self.left * (1.0 - left_right_mix).sqrt(), 56 | self.right * left_right_mix.sqrt(), 57 | ) * SQRT_2 58 | } 59 | 60 | /// Returns the frame mixed down to mono. 61 | #[must_use = "This method returns a new Frame and does not mutate the original value"] 62 | pub fn as_mono(self) -> Self { 63 | Self::from_mono((self.left + self.right) / 2.0) 64 | } 65 | } 66 | 67 | impl Add for Frame { 68 | type Output = Self; 69 | 70 | fn add(self, rhs: Self) -> Self::Output { 71 | Self::new(self.left + rhs.left, self.right + rhs.right) 72 | } 73 | } 74 | 75 | impl AddAssign for Frame { 76 | fn add_assign(&mut self, rhs: Self) { 77 | self.left += rhs.left; 78 | self.right += rhs.right; 79 | } 80 | } 81 | 82 | impl Sub for Frame { 83 | type Output = Self; 84 | 85 | fn sub(self, rhs: Self) -> Self::Output { 86 | Self::new(self.left - rhs.left, self.right - rhs.right) 87 | } 88 | } 89 | 90 | impl SubAssign for Frame { 91 | fn sub_assign(&mut self, rhs: Self) { 92 | self.left -= rhs.left; 93 | self.right -= rhs.right; 94 | } 95 | } 96 | 97 | impl Mul for Frame { 98 | type Output = Self; 99 | 100 | fn mul(self, rhs: f32) -> Self::Output { 101 | Self::new(self.left * rhs, self.right * rhs) 102 | } 103 | } 104 | 105 | impl MulAssign for Frame { 106 | fn mul_assign(&mut self, rhs: f32) { 107 | self.left *= rhs; 108 | self.right *= rhs; 109 | } 110 | } 111 | 112 | impl Div for Frame { 113 | type Output = Self; 114 | 115 | fn div(self, rhs: f32) -> Self::Output { 116 | Self::new(self.left / rhs, self.right / rhs) 117 | } 118 | } 119 | 120 | impl DivAssign for Frame { 121 | fn div_assign(&mut self, rhs: f32) { 122 | self.left /= rhs; 123 | self.right /= rhs; 124 | } 125 | } 126 | 127 | impl Neg for Frame { 128 | type Output = Self; 129 | 130 | fn neg(self) -> Self::Output { 131 | Self::new(-self.left, -self.right) 132 | } 133 | } 134 | 135 | /// Given a previous frame, a current frame, the two next frames, 136 | /// and a position `x` from 0.0 to 1.0 between the current frame 137 | /// and next frame, get an approximated frame. 138 | // This is the 4-point, 3rd-order Hermite interpolation x-form 139 | // algorithm from "Polynomial Interpolators for High-Quality 140 | // Resampling of Oversampled Audio" by Olli Niemitalo, p. 43: 141 | // http://yehar.com/blog/wp-content/uploads/2009/08/deip.pdf 142 | #[must_use] 143 | pub fn interpolate_frame( 144 | previous: Frame, 145 | current: Frame, 146 | next_1: Frame, 147 | next_2: Frame, 148 | fraction: f32, 149 | ) -> Frame { 150 | let c0 = current; 151 | let c1 = (next_1 - previous) * 0.5; 152 | let c2 = previous - current * 2.5 + next_1 * 2.0 - next_2 * 0.5; 153 | let c3 = (next_2 - previous) * 0.5 + (current - next_1) * 1.5; 154 | ((c3 * fraction + c2) * fraction + c1) * fraction + c0 155 | } 156 | -------------------------------------------------------------------------------- /crates/kira/src/listener.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Types related to spatial listeners. 3 | 4 | To create a listener, use [`AudioManager::add_listener`](crate::AudioManager::add_listener). 5 | 6 | For more information, see the documentation on [spatial mixer tracks](crate::track#spatial-tracks). 7 | */ 8 | 9 | mod handle; 10 | 11 | use atomic_arena::Key; 12 | pub use handle::*; 13 | 14 | use std::sync::{ 15 | atomic::{AtomicBool, Ordering}, 16 | Arc, 17 | }; 18 | 19 | use glam::{Quat, Vec3}; 20 | 21 | use crate::{ 22 | command::{read_commands_into_parameters, ValueChangeCommand}, 23 | command_writers_and_readers, 24 | info::Info, 25 | Parameter, Value, 26 | }; 27 | 28 | /// A unique identifier for a listener. 29 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 30 | pub struct ListenerId(pub(crate) Key); 31 | 32 | pub(crate) struct Listener { 33 | pub shared: Arc, 34 | pub position: Parameter, 35 | pub orientation: Parameter, 36 | pub command_readers: CommandReaders, 37 | } 38 | 39 | impl Listener { 40 | pub fn new( 41 | id: ListenerId, 42 | position: Value, 43 | orientation: Value, 44 | ) -> (Self, ListenerHandle) { 45 | let shared = Arc::new(ListenerShared::new()); 46 | let (command_writers, command_readers) = command_writers_and_readers(); 47 | ( 48 | Self { 49 | shared: shared.clone(), 50 | position: Parameter::new(position, Vec3::ZERO), 51 | orientation: Parameter::new(orientation, Quat::IDENTITY), 52 | command_readers, 53 | }, 54 | ListenerHandle { 55 | id, 56 | shared, 57 | command_writers, 58 | }, 59 | ) 60 | } 61 | 62 | pub fn on_start_processing(&mut self) { 63 | read_commands_into_parameters!(self, position, orientation); 64 | } 65 | 66 | pub(crate) fn update(&mut self, dt: f64, info: &Info) { 67 | self.position.update(dt, info); 68 | self.orientation.update(dt, info); 69 | } 70 | } 71 | 72 | impl Default for Listener { 73 | fn default() -> Self { 74 | let (_, command_readers) = command_writers_and_readers(); 75 | Self { 76 | shared: Arc::new(ListenerShared::new()), 77 | position: Parameter::new(Value::Fixed(Vec3::ZERO), Vec3::ZERO), 78 | orientation: Parameter::new(Value::Fixed(Quat::IDENTITY), Quat::IDENTITY), 79 | command_readers, 80 | } 81 | } 82 | } 83 | 84 | #[derive(Debug)] 85 | pub(crate) struct ListenerShared { 86 | removed: AtomicBool, 87 | } 88 | 89 | impl ListenerShared { 90 | pub fn new() -> Self { 91 | Self { 92 | removed: AtomicBool::new(false), 93 | } 94 | } 95 | 96 | #[must_use] 97 | pub fn is_marked_for_removal(&self) -> bool { 98 | self.removed.load(Ordering::SeqCst) 99 | } 100 | 101 | pub fn mark_for_removal(&self) { 102 | self.removed.store(true, Ordering::SeqCst); 103 | } 104 | } 105 | 106 | command_writers_and_readers! { 107 | set_position: ValueChangeCommand, 108 | set_orientation: ValueChangeCommand, 109 | } 110 | -------------------------------------------------------------------------------- /crates/kira/src/listener/handle.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{command::ValueChangeCommand, Tween, Value}; 4 | 5 | use super::{CommandWriters, ListenerId, ListenerShared}; 6 | 7 | /// Controls a listener. 8 | /// 9 | /// When a [`ListenerHandle`] is dropped, the corresponding 10 | /// listener will be removed. 11 | #[derive(Debug)] 12 | pub struct ListenerHandle { 13 | pub(crate) id: ListenerId, 14 | pub(crate) shared: Arc, 15 | pub(crate) command_writers: CommandWriters, 16 | } 17 | 18 | impl ListenerHandle { 19 | /// Gets the unique identifier for this listener. 20 | pub fn id(&self) -> ListenerId { 21 | self.id 22 | } 23 | 24 | /// Sets the location of the listener in the spatial scene. 25 | pub fn set_position(&mut self, position: impl Into>>, tween: Tween) { 26 | let position: Value> = position.into(); 27 | self.command_writers.set_position.write(ValueChangeCommand { 28 | target: position.to_(), 29 | tween, 30 | }) 31 | } 32 | 33 | /// Sets the rotation of the listener. 34 | /// 35 | /// An unrotated listener should face in the negative Z direction with 36 | /// positive X to the right and positive Y up. 37 | pub fn set_orientation( 38 | &mut self, 39 | orientation: impl Into>>, 40 | tween: Tween, 41 | ) { 42 | let orientation: Value> = orientation.into(); 43 | self.command_writers 44 | .set_orientation 45 | .write(ValueChangeCommand { 46 | target: orientation.to_(), 47 | tween, 48 | }) 49 | } 50 | } 51 | 52 | impl Drop for ListenerHandle { 53 | fn drop(&mut self) { 54 | self.shared.mark_for_removal(); 55 | } 56 | } 57 | 58 | impl From<&ListenerHandle> for ListenerId { 59 | fn from(value: &ListenerHandle) -> Self { 60 | value.id() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/kira/src/manager/settings.rs: -------------------------------------------------------------------------------- 1 | use crate::track::MainTrackBuilder; 2 | 3 | use crate::backend::Backend; 4 | 5 | /// Specifies how many of each resource type an audio context 6 | /// can have. 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 8 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 9 | pub struct Capacities { 10 | /// The maximum number of mixer sub-tracks that can exist at a time. 11 | pub sub_track_capacity: usize, 12 | /// The maximum number of mixer send tracks that can exist at a time. 13 | pub send_track_capacity: usize, 14 | /// The maximum number of clocks that can exist at a time. 15 | pub clock_capacity: usize, 16 | /// The maximum number of modulators that can exist at a time. 17 | pub modulator_capacity: usize, 18 | /// The maximum number of listeners that can exist at a time. 19 | pub listener_capacity: usize, 20 | } 21 | 22 | impl Default for Capacities { 23 | fn default() -> Self { 24 | Self { 25 | sub_track_capacity: 128, 26 | send_track_capacity: 16, 27 | clock_capacity: 8, 28 | modulator_capacity: 16, 29 | listener_capacity: 8, 30 | } 31 | } 32 | } 33 | 34 | /// Settings for an [`AudioManager`](super::AudioManager). 35 | pub struct AudioManagerSettings { 36 | /// Specifies how many of each resource type an audio context 37 | /// can have. 38 | pub capacities: Capacities, 39 | /// Configures the main mixer track. 40 | pub main_track_builder: MainTrackBuilder, 41 | /// Determines how often modulators and clocks will be updated (in samples). 42 | /// 43 | /// At the default size of 128 samples, at a sample rate of 44100hz, 44 | /// modulators and clocks will update about every 3 milliseconds. 45 | /// 46 | /// Decreasing this value increases the precision of clocks and modulators 47 | /// at the expense of higher CPU usage. 48 | pub internal_buffer_size: usize, 49 | /// Configures the backend. 50 | pub backend_settings: B::Settings, 51 | } 52 | 53 | impl Default for AudioManagerSettings 54 | where 55 | B::Settings: Default, 56 | { 57 | fn default() -> Self { 58 | Self { 59 | capacities: Capacities::default(), 60 | main_track_builder: MainTrackBuilder::default(), 61 | internal_buffer_size: 128, 62 | backend_settings: B::Settings::default(), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/kira/src/mix.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{ 2 | Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign, 3 | }; 4 | 5 | use crate::{tween::Tweenable, Value}; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 8 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 9 | #[cfg_attr(feature = "serde", serde(transparent))] 10 | /** 11 | An amount to blend the "dry" and "wet" outputs from an effect. 12 | 13 | The "dry" signal is the audio before the effect is applied. 14 | The "wet" signal is the audio after the effect is applied. 15 | 16 | Valid mix values range from `0.0` to `1.0`, where `0.0` is 17 | the dry signal only, `1.0` is the wet signal only, and `0.5` 18 | is an equal mix of both. 19 | */ 20 | pub struct Mix(pub f32); 21 | 22 | impl Mix { 23 | /// Only output the dry signal. 24 | pub const DRY: Self = Self(0.0); 25 | /// Only output the wet signal. 26 | pub const WET: Self = Self(1.0); 27 | } 28 | 29 | impl Tweenable for Mix { 30 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 31 | Self(Tweenable::interpolate(a.0, b.0, amount)) 32 | } 33 | } 34 | 35 | impl From for Mix { 36 | fn from(value: f32) -> Self { 37 | Self(value) 38 | } 39 | } 40 | 41 | impl From for Value { 42 | fn from(value: f32) -> Self { 43 | Self::Fixed(Mix(value)) 44 | } 45 | } 46 | 47 | impl From for Value { 48 | fn from(value: Mix) -> Self { 49 | Self::Fixed(value) 50 | } 51 | } 52 | 53 | impl Add for Mix { 54 | type Output = Mix; 55 | 56 | fn add(self, rhs: Mix) -> Self::Output { 57 | Self(self.0 + rhs.0) 58 | } 59 | } 60 | 61 | impl AddAssign for Mix { 62 | fn add_assign(&mut self, rhs: Mix) { 63 | self.0 += rhs.0; 64 | } 65 | } 66 | 67 | impl Sub for Mix { 68 | type Output = Mix; 69 | 70 | fn sub(self, rhs: Mix) -> Self::Output { 71 | Self(self.0 - rhs.0) 72 | } 73 | } 74 | 75 | impl SubAssign for Mix { 76 | fn sub_assign(&mut self, rhs: Mix) { 77 | self.0 -= rhs.0; 78 | } 79 | } 80 | 81 | impl Mul for Mix { 82 | type Output = Mix; 83 | 84 | fn mul(self, rhs: f32) -> Self::Output { 85 | Self(self.0 * rhs) 86 | } 87 | } 88 | 89 | impl MulAssign for Mix { 90 | fn mul_assign(&mut self, rhs: f32) { 91 | self.0 *= rhs; 92 | } 93 | } 94 | 95 | impl Div for Mix { 96 | type Output = Mix; 97 | 98 | fn div(self, rhs: f32) -> Self::Output { 99 | Self(self.0 / rhs) 100 | } 101 | } 102 | 103 | impl DivAssign for Mix { 104 | fn div_assign(&mut self, rhs: f32) { 105 | self.0 /= rhs; 106 | } 107 | } 108 | 109 | impl Neg for Mix { 110 | type Output = Mix; 111 | 112 | fn neg(self) -> Self::Output { 113 | Self(-self.0) 114 | } 115 | } 116 | 117 | impl Rem for Mix { 118 | type Output = Mix; 119 | 120 | fn rem(self, rhs: f32) -> Self::Output { 121 | Self(self.0 % rhs) 122 | } 123 | } 124 | 125 | impl RemAssign for Mix { 126 | fn rem_assign(&mut self, rhs: f32) { 127 | self.0 %= rhs; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /crates/kira/src/modulator/lfo.rs: -------------------------------------------------------------------------------- 1 | //! Oscillates back and forth. 2 | 3 | #[cfg(test)] 4 | mod test; 5 | 6 | mod builder; 7 | mod handle; 8 | 9 | pub use builder::*; 10 | pub use handle::*; 11 | 12 | use std::{ 13 | f64::consts::TAU, 14 | sync::{ 15 | atomic::{AtomicBool, Ordering}, 16 | Arc, 17 | }, 18 | }; 19 | 20 | use crate::{ 21 | command::{read_commands_into_parameters, ValueChangeCommand}, 22 | command_writers_and_readers, 23 | info::Info, 24 | Parameter, 25 | }; 26 | 27 | use super::Modulator; 28 | 29 | struct Lfo { 30 | waveform: Waveform, 31 | frequency: Parameter, 32 | amplitude: Parameter, 33 | offset: Parameter, 34 | command_readers: CommandReaders, 35 | shared: Arc, 36 | phase: f64, 37 | value: f64, 38 | } 39 | 40 | impl Lfo { 41 | #[must_use] 42 | fn new(builder: &LfoBuilder, command_readers: CommandReaders, shared: Arc) -> Self { 43 | Self { 44 | waveform: builder.waveform, 45 | frequency: Parameter::new(builder.frequency, 2.0), 46 | amplitude: Parameter::new(builder.amplitude, 1.0), 47 | offset: Parameter::new(builder.offset, 0.0), 48 | command_readers, 49 | shared, 50 | phase: builder.starting_phase / TAU, 51 | value: 0.0, 52 | } 53 | } 54 | } 55 | 56 | impl Modulator for Lfo { 57 | fn on_start_processing(&mut self) { 58 | read_commands_into_parameters!(self, frequency, amplitude, offset); 59 | if let Some(waveform) = self.command_readers.set_waveform.read() { 60 | self.waveform = waveform; 61 | } 62 | if let Some(phase) = self.command_readers.set_phase.read() { 63 | self.phase = phase / TAU; 64 | } 65 | } 66 | 67 | fn update(&mut self, dt: f64, info: &Info) { 68 | self.frequency.update(dt, info); 69 | self.amplitude.update(dt, info); 70 | self.offset.update(dt, info); 71 | self.phase += dt * self.frequency.value(); 72 | self.phase %= 1.0; 73 | self.value = self.offset.value() + self.amplitude.value() * self.waveform.value(self.phase); 74 | } 75 | 76 | fn value(&self) -> f64 { 77 | self.value 78 | } 79 | 80 | fn finished(&self) -> bool { 81 | self.shared.removed.load(Ordering::SeqCst) 82 | } 83 | } 84 | 85 | /// Describes an oscillation pattern. 86 | #[derive(Debug, Clone, Copy, PartialEq)] 87 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 88 | pub enum Waveform { 89 | /// The value moves back and forth smoothly. 90 | Sine, 91 | /// The value moves back and forth at a constant speed. 92 | Triangle, 93 | /// The value moves gradually in one direction, then abruptly jumps in the other. 94 | Saw, 95 | /// The value jumps back and forth between two values. 96 | Pulse { 97 | /// The ratio between how much time the oscillator spends on one value vs. the other. 98 | /// 99 | /// This should be a number between `0.0` and `1.0`. A value of `0.5` means the oscillator 100 | /// spends an equal amount of time at both values. 101 | width: f64, 102 | }, 103 | } 104 | 105 | impl Waveform { 106 | #[must_use] 107 | fn value(self, phase: f64) -> f64 { 108 | match self { 109 | Waveform::Sine => (phase * TAU).sin(), 110 | Waveform::Triangle => ((phase + 0.75).fract() - 0.5).abs() * 4.0 - 1.0, 111 | Waveform::Saw => (phase + 0.5).fract() * 2.0 - 1.0, 112 | Waveform::Pulse { width } => { 113 | if phase < width { 114 | 1.0 115 | } else { 116 | -1.0 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | #[derive(Debug)] 124 | struct LfoShared { 125 | removed: AtomicBool, 126 | } 127 | 128 | impl LfoShared { 129 | fn new() -> Self { 130 | Self { 131 | removed: AtomicBool::new(false), 132 | } 133 | } 134 | } 135 | 136 | command_writers_and_readers! { 137 | set_waveform: Waveform, 138 | set_frequency: ValueChangeCommand, 139 | set_amplitude: ValueChangeCommand, 140 | set_offset: ValueChangeCommand, 141 | set_phase: f64, 142 | } 143 | -------------------------------------------------------------------------------- /crates/kira/src/modulator/lfo/builder.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{ 4 | modulator::{Modulator, ModulatorBuilder, ModulatorId}, 5 | Value, 6 | }; 7 | 8 | use super::{command_writers_and_readers, handle::LfoHandle, Lfo, LfoShared, Waveform}; 9 | 10 | /// Configures an LFO modulator. 11 | pub struct LfoBuilder { 12 | /// The oscillation pattern. 13 | pub waveform: Waveform, 14 | /// How quickly the value oscillates. 15 | pub frequency: Value, 16 | /// How much the value oscillates. 17 | /// 18 | /// An amplitude of `2.0` means the modulator will reach a maximum 19 | /// value of `2.0` and a minimum value of `-2.0`. 20 | pub amplitude: Value, 21 | /// The constant value the modulator is offset by. 22 | /// 23 | /// An LFO with an offset of `1.0` and an amplitude of `0.5` will reach 24 | /// a maximum value of `1.5` and a minimum value of `0.5`. 25 | pub offset: Value, 26 | /// The phase the LFO should start at (in radians). 27 | /// 28 | /// This determines when in the oscillation the modulator will start. 29 | pub starting_phase: f64, 30 | } 31 | 32 | impl LfoBuilder { 33 | /// Creates a new [`LfoBuilder`] with the default settings. 34 | #[must_use] 35 | pub fn new() -> Self { 36 | Self::default() 37 | } 38 | 39 | /// Sets the oscillation pattern. 40 | #[must_use = "This method consumes self and returns a modified LfoBuilder, so the return value should be used"] 41 | pub fn waveform(self, waveform: Waveform) -> Self { 42 | Self { waveform, ..self } 43 | } 44 | 45 | /// Sets how quickly the value oscillates. 46 | #[must_use = "This method consumes self and returns a modified LfoBuilder, so the return value should be used"] 47 | pub fn frequency(self, frequency: impl Into>) -> Self { 48 | Self { 49 | frequency: frequency.into(), 50 | ..self 51 | } 52 | } 53 | 54 | /// Sets how much the value oscillates. 55 | /// 56 | /// An amplitude of `2.0` means the modulator will reach a maximum 57 | /// value of `2.0` and a minimum value of `-2.0`. 58 | #[must_use = "This method consumes self and returns a modified LfoBuilder, so the return value should be used"] 59 | pub fn amplitude(self, amplitude: impl Into>) -> Self { 60 | Self { 61 | amplitude: amplitude.into(), 62 | ..self 63 | } 64 | } 65 | 66 | /// Sets a constant value that the modulator is offset by. 67 | /// 68 | /// An LFO with an offset of `1.0` and an amplitude of `0.5` will reach 69 | /// a maximum value of `1.5` and a minimum value of `0.5`. 70 | #[must_use = "This method consumes self and returns a modified LfoBuilder, so the return value should be used"] 71 | pub fn offset(self, offset: impl Into>) -> Self { 72 | Self { 73 | offset: offset.into(), 74 | ..self 75 | } 76 | } 77 | 78 | /// Sets the phase the LFO should start at (in radians). 79 | /// 80 | /// This determines when in the oscillation the modulator will start. 81 | #[must_use = "This method consumes self and returns a modified LfoBuilder, so the return value should be used"] 82 | pub fn starting_phase(self, starting_phase: f64) -> Self { 83 | Self { 84 | starting_phase, 85 | ..self 86 | } 87 | } 88 | } 89 | 90 | impl Default for LfoBuilder { 91 | fn default() -> Self { 92 | Self { 93 | waveform: Waveform::Sine, 94 | frequency: Value::Fixed(2.0), 95 | amplitude: Value::Fixed(1.0), 96 | offset: Value::Fixed(0.0), 97 | starting_phase: 0.0, 98 | } 99 | } 100 | } 101 | 102 | impl ModulatorBuilder for LfoBuilder { 103 | type Handle = LfoHandle; 104 | 105 | fn build(self, id: ModulatorId) -> (Box, Self::Handle) { 106 | let (command_writers, command_readers) = command_writers_and_readers(); 107 | let shared = Arc::new(LfoShared::new()); 108 | ( 109 | Box::new(Lfo::new(&self, command_readers, shared.clone())), 110 | LfoHandle { 111 | id, 112 | command_writers, 113 | shared, 114 | }, 115 | ) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/kira/src/modulator/lfo/handle.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::Ordering, Arc}; 2 | 3 | use crate::{command::handle_param_setters, modulator::ModulatorId}; 4 | 5 | use super::{CommandWriters, LfoShared, Waveform}; 6 | 7 | /// Controls an LFO modulator. 8 | #[derive(Debug)] 9 | pub struct LfoHandle { 10 | pub(super) id: ModulatorId, 11 | pub(super) command_writers: CommandWriters, 12 | pub(super) shared: Arc, 13 | } 14 | 15 | impl LfoHandle { 16 | /// Returns the unique identifier for the modulator. 17 | #[must_use] 18 | pub fn id(&self) -> ModulatorId { 19 | self.id 20 | } 21 | 22 | /// Sets the oscillation pattern. 23 | pub fn set_waveform(&mut self, waveform: Waveform) { 24 | self.command_writers.set_waveform.write(waveform) 25 | } 26 | 27 | handle_param_setters! { 28 | /// Sets how quickly the value oscillates. 29 | frequency: f64, 30 | 31 | /// Sets how much the value oscillates. 32 | /// 33 | /// An amplitude of `2.0` means the modulator will reach a maximum 34 | /// value of `2.0` and a minimum value of `-2.0`. 35 | amplitude: f64, 36 | 37 | /// Sets a constant value that the modulator is offset by. 38 | /// 39 | /// An LFO with an offset of `1.0` and an amplitude of `0.5` will reach 40 | /// a maximum value of `1.5` and a minimum value of `0.5`. 41 | offset: f64, 42 | } 43 | 44 | /// Sets the phase of the LFO (in radians). 45 | pub fn set_phase(&mut self, phase: f64) { 46 | self.command_writers.set_phase.write(phase) 47 | } 48 | } 49 | 50 | impl Drop for LfoHandle { 51 | fn drop(&mut self) { 52 | self.shared.removed.store(true, Ordering::SeqCst); 53 | } 54 | } 55 | 56 | impl From<&LfoHandle> for ModulatorId { 57 | fn from(value: &LfoHandle) -> Self { 58 | value.id 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/kira/src/modulator/lfo/test.rs: -------------------------------------------------------------------------------- 1 | use std::f64::consts::FRAC_1_SQRT_2; 2 | 3 | use approx::assert_relative_eq; 4 | 5 | use crate::modulator::lfo::Waveform; 6 | 7 | #[test] 8 | fn sine() { 9 | test_waveform( 10 | Waveform::Sine, 11 | [ 12 | 0.0, 13 | FRAC_1_SQRT_2, 14 | 1.0, 15 | FRAC_1_SQRT_2, 16 | 0.0, 17 | -FRAC_1_SQRT_2, 18 | -1.0, 19 | -FRAC_1_SQRT_2, 20 | ], 21 | ); 22 | } 23 | 24 | #[test] 25 | fn triangle() { 26 | test_waveform( 27 | Waveform::Triangle, 28 | [0.0, 0.5, 1.0, 0.5, 0.0, -0.5, -1.0, -0.5], 29 | ); 30 | } 31 | 32 | #[test] 33 | fn saw() { 34 | test_waveform( 35 | Waveform::Saw, 36 | [0.0, 0.25, 0.5, 0.75, -1.0, -0.75, -0.5, -0.25], 37 | ); 38 | } 39 | 40 | #[test] 41 | fn pulse() { 42 | test_waveform( 43 | Waveform::Pulse { width: 0.25 }, 44 | [1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], 45 | ); 46 | test_waveform( 47 | Waveform::Pulse { width: 0.75 }, 48 | [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0], 49 | ); 50 | } 51 | 52 | fn test_waveform(waveform: Waveform, values: [f64; 8]) { 53 | for (i, value) in values.iter().enumerate() { 54 | assert_relative_eq!(waveform.value(i as f64 / 8.0), *value); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/kira/src/modulator/tweener.rs: -------------------------------------------------------------------------------- 1 | //! Smoothly transitions values to other values. 2 | 3 | mod builder; 4 | mod handle; 5 | 6 | #[cfg(test)] 7 | mod test; 8 | 9 | use std::{ 10 | sync::{ 11 | atomic::{AtomicBool, Ordering}, 12 | Arc, 13 | }, 14 | time::Duration, 15 | }; 16 | 17 | pub use builder::*; 18 | pub use handle::*; 19 | 20 | use crate::{ 21 | command_writers_and_readers, 22 | info::{Info, WhenToStart}, 23 | Tween, Tweenable, 24 | StartTime, 25 | }; 26 | 27 | use super::Modulator; 28 | 29 | struct Tweener { 30 | state: State, 31 | value: f64, 32 | command_readers: CommandReaders, 33 | shared: Arc, 34 | } 35 | 36 | impl Tweener { 37 | #[must_use] 38 | fn new( 39 | initial_value: f64, 40 | command_readers: CommandReaders, 41 | shared: Arc, 42 | ) -> Self { 43 | Self { 44 | state: State::Idle, 45 | value: initial_value, 46 | command_readers, 47 | shared, 48 | } 49 | } 50 | 51 | fn set(&mut self, target: f64, tween: Tween) { 52 | self.state = State::Tweening { 53 | values: (self.value, target), 54 | time: 0.0, 55 | tween, 56 | } 57 | } 58 | } 59 | 60 | impl Modulator for Tweener { 61 | fn on_start_processing(&mut self) { 62 | if let Some((target, tween)) = self.command_readers.set.read() { 63 | self.set(target, tween); 64 | } 65 | } 66 | 67 | fn update(&mut self, dt: f64, info: &Info) { 68 | if let State::Tweening { 69 | values, 70 | time, 71 | tween, 72 | } = &mut self.state 73 | { 74 | let started = match &mut tween.start_time { 75 | StartTime::Immediate => true, 76 | StartTime::Delayed(time_remaining) => { 77 | if time_remaining.is_zero() { 78 | true 79 | } else { 80 | *time_remaining = 81 | time_remaining.saturating_sub(Duration::from_secs_f64(dt)); 82 | false 83 | } 84 | } 85 | StartTime::ClockTime(clock_time) => { 86 | info.when_to_start(*clock_time) == WhenToStart::Now 87 | } 88 | }; 89 | if !started { 90 | return; 91 | } 92 | *time += dt; 93 | if *time >= tween.duration.as_secs_f64() { 94 | self.value = values.1; 95 | self.state = State::Idle; 96 | } else { 97 | self.value = Tweenable::interpolate(values.0, values.1, tween.value(*time)); 98 | } 99 | } 100 | } 101 | 102 | fn value(&self) -> f64 { 103 | self.value 104 | } 105 | 106 | fn finished(&self) -> bool { 107 | self.shared.removed.load(Ordering::SeqCst) 108 | } 109 | } 110 | 111 | enum State { 112 | Idle, 113 | Tweening { 114 | values: (f64, f64), 115 | time: f64, 116 | tween: Tween, 117 | }, 118 | } 119 | 120 | #[derive(Debug)] 121 | struct TweenerShared { 122 | removed: AtomicBool, 123 | } 124 | 125 | impl TweenerShared { 126 | #[must_use] 127 | fn new() -> Self { 128 | Self { 129 | removed: AtomicBool::new(false), 130 | } 131 | } 132 | } 133 | 134 | command_writers_and_readers! { 135 | set: (f64, Tween), 136 | } 137 | -------------------------------------------------------------------------------- /crates/kira/src/modulator/tweener/builder.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::modulator::{Modulator, ModulatorBuilder, ModulatorId}; 4 | 5 | use super::{command_writers_and_readers, Tweener, TweenerHandle, TweenerShared}; 6 | 7 | /// Configures a tweener. 8 | pub struct TweenerBuilder { 9 | /// The initial value of the tweener. 10 | pub initial_value: f64, 11 | } 12 | 13 | impl ModulatorBuilder for TweenerBuilder { 14 | type Handle = TweenerHandle; 15 | 16 | fn build(self, id: ModulatorId) -> (Box, Self::Handle) { 17 | let (command_writers, command_readers) = command_writers_and_readers(); 18 | let shared = Arc::new(TweenerShared::new()); 19 | ( 20 | Box::new(Tweener::new( 21 | self.initial_value, 22 | command_readers, 23 | shared.clone(), 24 | )), 25 | TweenerHandle { 26 | id, 27 | command_writers, 28 | shared, 29 | }, 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/kira/src/modulator/tweener/handle.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::Ordering, Arc}; 2 | 3 | use crate::{modulator::ModulatorId, tween::Tween}; 4 | 5 | use super::{CommandWriters, TweenerShared}; 6 | 7 | /// Controls a tweener. 8 | #[derive(Debug)] 9 | pub struct TweenerHandle { 10 | pub(super) id: ModulatorId, 11 | pub(super) command_writers: CommandWriters, 12 | pub(super) shared: Arc, 13 | } 14 | 15 | impl TweenerHandle { 16 | /// Returns the unique identifier for the modulator. 17 | pub fn id(&self) -> ModulatorId { 18 | self.id 19 | } 20 | 21 | /// Starts a transition from the current value to a target value with 22 | /// the given tween. 23 | pub fn set(&mut self, target: f64, tween: Tween) { 24 | self.command_writers.set.write((target, tween)) 25 | } 26 | } 27 | 28 | impl From<&TweenerHandle> for ModulatorId { 29 | fn from(handle: &TweenerHandle) -> Self { 30 | handle.id 31 | } 32 | } 33 | 34 | impl Drop for TweenerHandle { 35 | fn drop(&mut self) { 36 | self.shared.removed.store(true, Ordering::SeqCst); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/kira/src/modulator/tweener/test.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use atomic_arena::Arena; 4 | 5 | use crate::info::MockInfoBuilder; 6 | use crate::{ 7 | clock::ClockTime, 8 | modulator::{tweener::TweenerBuilder, ModulatorBuilder, ModulatorId}, 9 | StartTime, Tween, 10 | }; 11 | 12 | /// Tests that the basic tweening behavior of a `Tweener` 13 | /// works properly. 14 | #[test] 15 | #[allow(clippy::float_cmp)] 16 | fn tweening() { 17 | let (mut tweener, mut handle) = 18 | TweenerBuilder { initial_value: 0.0 }.build(generate_fake_modulator_id()); 19 | let info = MockInfoBuilder::new().build(); 20 | 21 | // value should not be changing yet 22 | for _ in 0..3 { 23 | tweener.update(1.0, &info); 24 | assert_eq!(tweener.value(), 0.0); 25 | } 26 | 27 | handle.set( 28 | 1.0, 29 | Tween { 30 | duration: Duration::from_secs(2), 31 | ..Default::default() 32 | }, 33 | ); 34 | tweener.on_start_processing(); 35 | 36 | tweener.update(1.0, &info); 37 | assert_eq!(tweener.value(), 0.5); 38 | tweener.update(1.0, &info); 39 | assert_eq!(tweener.value(), 1.0); 40 | tweener.update(1.0, &info); 41 | assert_eq!(tweener.value(), 1.0); 42 | } 43 | 44 | /// Tests that a `Tweener` with a delayed start time waits for 45 | /// that time before it begins tweening. 46 | #[test] 47 | #[allow(clippy::float_cmp)] 48 | fn waits_for_delay() { 49 | let info = MockInfoBuilder::new().build(); 50 | 51 | let (mut tweener, mut handle) = 52 | TweenerBuilder { initial_value: 0.0 }.build(generate_fake_modulator_id()); 53 | handle.set( 54 | 1.0, 55 | Tween { 56 | start_time: StartTime::Delayed(Duration::from_secs(2)), 57 | duration: Duration::from_secs(1), 58 | ..Default::default() 59 | }, 60 | ); 61 | tweener.on_start_processing(); 62 | 63 | // value should not be changing yet 64 | for _ in 0..2 { 65 | assert_eq!(tweener.value(), 0.0); 66 | tweener.update(1.0, &info); 67 | } 68 | 69 | // the tween should start now 70 | tweener.update(1.0, &info); 71 | assert_eq!(tweener.value(), 1.0); 72 | } 73 | 74 | /// Tests that a `Tweener` with a clock time set as 75 | /// the start time waits for that time before it 76 | /// begins tweening. 77 | #[test] 78 | #[allow(clippy::float_cmp)] 79 | fn waits_for_start_time() { 80 | let (mut tweener, mut handle) = 81 | TweenerBuilder { initial_value: 0.0 }.build(generate_fake_modulator_id()); 82 | let mut info_builder = MockInfoBuilder::new(); 83 | let clock_id_1 = info_builder.add_clock(true, 0, 0.0); 84 | let info = info_builder.build(); 85 | 86 | handle.set( 87 | 1.0, 88 | Tween { 89 | start_time: StartTime::ClockTime(ClockTime { 90 | clock: clock_id_1, 91 | ticks: 2, 92 | fraction: 0.0, 93 | }), 94 | duration: Duration::from_secs(1), 95 | ..Default::default() 96 | }, 97 | ); 98 | tweener.on_start_processing(); 99 | 100 | // value should not be changing yet 101 | for _ in 0..3 { 102 | tweener.update(1.0, &info); 103 | assert_eq!(tweener.value(), 0.0); 104 | } 105 | 106 | let info = { 107 | let mut builder = MockInfoBuilder::new(); 108 | builder.add_clock(true, 1, 0.0); 109 | builder.add_clock(true, 0, 0.0); 110 | builder.build() 111 | }; 112 | 113 | // the tween is set to start at tick 2, so it should not 114 | // start yet 115 | for _ in 0..3 { 116 | tweener.update(1.0, &info); 117 | assert_eq!(tweener.value(), 0.0); 118 | } 119 | 120 | let info = { 121 | let mut builder = MockInfoBuilder::new(); 122 | builder.add_clock(true, 1, 0.0); 123 | builder.add_clock(true, 2, 0.0); 124 | builder.build() 125 | }; 126 | 127 | // a different clock reached tick 2, so the tween should not 128 | // start yet 129 | for _ in 0..3 { 130 | tweener.update(1.0, &info); 131 | assert_eq!(tweener.value(), 0.0); 132 | } 133 | 134 | let info = { 135 | let mut builder = MockInfoBuilder::new(); 136 | builder.add_clock(true, 2, 0.0); 137 | builder.add_clock(true, 2, 0.0); 138 | builder.build() 139 | }; 140 | 141 | // the tween should start now 142 | tweener.update(1.0, &info); 143 | assert_eq!(tweener.value(), 1.0); 144 | } 145 | 146 | fn generate_fake_modulator_id() -> ModulatorId { 147 | let arena = Arena::<()>::new(1); 148 | ModulatorId(arena.controller().try_reserve().unwrap()) 149 | } 150 | -------------------------------------------------------------------------------- /crates/kira/src/panning.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{ 2 | Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign, 3 | }; 4 | 5 | use crate::{tween::Tweenable, Value}; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 8 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 9 | #[cfg_attr(feature = "serde", serde(transparent))] 10 | /// The stereo positioning of a sound. 11 | /// 12 | /// Valid panning values range from `-1.0` to `1.0`. A value of `-1.0` 13 | /// will cause a sound to only be output from the left speaker. A value 14 | /// of `1.0` will cause a sound to only be output from the right speaker. 15 | /// A value of `0.0` will cause a sound to be played at an equal volume 16 | /// from both speakers. 17 | pub struct Panning(pub f32); 18 | 19 | impl Panning { 20 | /// Play the sound from the left speaker only. 21 | pub const LEFT: Self = Self(-1.0); 22 | /// Play the sound from both speakers at the same volume. 23 | pub const CENTER: Self = Self(0.0); 24 | /// Play the sound from the right speaker only. 25 | pub const RIGHT: Self = Self(1.0); 26 | } 27 | 28 | impl Default for Panning { 29 | fn default() -> Self { 30 | Self::CENTER 31 | } 32 | } 33 | 34 | impl Tweenable for Panning { 35 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 36 | Self(Tweenable::interpolate(a.0, b.0, amount)) 37 | } 38 | } 39 | 40 | impl From for Panning { 41 | fn from(value: f32) -> Self { 42 | Self(value) 43 | } 44 | } 45 | 46 | impl From for Value { 47 | fn from(value: f32) -> Self { 48 | Self::Fixed(Panning(value)) 49 | } 50 | } 51 | 52 | impl From for Value { 53 | fn from(value: Panning) -> Self { 54 | Self::Fixed(value) 55 | } 56 | } 57 | 58 | impl Add for Panning { 59 | type Output = Panning; 60 | 61 | fn add(self, rhs: Panning) -> Self::Output { 62 | Self(self.0 + rhs.0) 63 | } 64 | } 65 | 66 | impl AddAssign for Panning { 67 | fn add_assign(&mut self, rhs: Panning) { 68 | self.0 += rhs.0; 69 | } 70 | } 71 | 72 | impl Sub for Panning { 73 | type Output = Panning; 74 | 75 | fn sub(self, rhs: Panning) -> Self::Output { 76 | Self(self.0 - rhs.0) 77 | } 78 | } 79 | 80 | impl SubAssign for Panning { 81 | fn sub_assign(&mut self, rhs: Panning) { 82 | self.0 -= rhs.0; 83 | } 84 | } 85 | 86 | impl Mul for Panning { 87 | type Output = Panning; 88 | 89 | fn mul(self, rhs: f32) -> Self::Output { 90 | Self(self.0 * rhs) 91 | } 92 | } 93 | 94 | impl MulAssign for Panning { 95 | fn mul_assign(&mut self, rhs: f32) { 96 | self.0 *= rhs; 97 | } 98 | } 99 | 100 | impl Div for Panning { 101 | type Output = Panning; 102 | 103 | fn div(self, rhs: f32) -> Self::Output { 104 | Self(self.0 / rhs) 105 | } 106 | } 107 | 108 | impl DivAssign for Panning { 109 | fn div_assign(&mut self, rhs: f32) { 110 | self.0 /= rhs; 111 | } 112 | } 113 | 114 | impl Neg for Panning { 115 | type Output = Panning; 116 | 117 | fn neg(self) -> Self::Output { 118 | Self(-self.0) 119 | } 120 | } 121 | 122 | impl Rem for Panning { 123 | type Output = Panning; 124 | 125 | fn rem(self, rhs: f32) -> Self::Output { 126 | Self(self.0 % rhs) 127 | } 128 | } 129 | 130 | impl RemAssign for Panning { 131 | fn rem_assign(&mut self, rhs: f32) { 132 | self.0 %= rhs; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /crates/kira/src/playback_rate.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{ 2 | Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign, 3 | }; 4 | 5 | use crate::{tween::Tweenable, Value}; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 8 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 9 | #[cfg_attr(feature = "serde", serde(transparent))] 10 | /// How quickly a sound should be played, where `1.0` is the default 11 | /// playback rate. 12 | /// 13 | /// Changing the playback rate of a sound will affect both the speed and pitch. 14 | pub struct PlaybackRate(pub f64); 15 | 16 | impl Default for PlaybackRate { 17 | fn default() -> Self { 18 | Self(1.0) 19 | } 20 | } 21 | 22 | impl Tweenable for PlaybackRate { 23 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 24 | Self(Tweenable::interpolate(a.0, b.0, amount)) 25 | } 26 | } 27 | 28 | impl From for PlaybackRate { 29 | fn from(value: f64) -> Self { 30 | Self(value) 31 | } 32 | } 33 | 34 | impl From for Value { 35 | fn from(value: f64) -> Self { 36 | Self::Fixed(PlaybackRate(value)) 37 | } 38 | } 39 | 40 | impl From for Value { 41 | fn from(value: PlaybackRate) -> Self { 42 | Self::Fixed(value) 43 | } 44 | } 45 | 46 | impl Add for PlaybackRate { 47 | type Output = PlaybackRate; 48 | 49 | fn add(self, rhs: PlaybackRate) -> Self::Output { 50 | Self(self.0 + rhs.0) 51 | } 52 | } 53 | 54 | impl AddAssign for PlaybackRate { 55 | fn add_assign(&mut self, rhs: PlaybackRate) { 56 | self.0 += rhs.0; 57 | } 58 | } 59 | 60 | impl Sub for PlaybackRate { 61 | type Output = PlaybackRate; 62 | 63 | fn sub(self, rhs: PlaybackRate) -> Self::Output { 64 | Self(self.0 - rhs.0) 65 | } 66 | } 67 | 68 | impl SubAssign for PlaybackRate { 69 | fn sub_assign(&mut self, rhs: PlaybackRate) { 70 | self.0 -= rhs.0; 71 | } 72 | } 73 | 74 | impl Mul for PlaybackRate { 75 | type Output = PlaybackRate; 76 | 77 | fn mul(self, rhs: f64) -> Self::Output { 78 | Self(self.0 * rhs) 79 | } 80 | } 81 | 82 | impl MulAssign for PlaybackRate { 83 | fn mul_assign(&mut self, rhs: f64) { 84 | self.0 *= rhs; 85 | } 86 | } 87 | 88 | impl Div for PlaybackRate { 89 | type Output = PlaybackRate; 90 | 91 | fn div(self, rhs: f64) -> Self::Output { 92 | Self(self.0 / rhs) 93 | } 94 | } 95 | 96 | impl DivAssign for PlaybackRate { 97 | fn div_assign(&mut self, rhs: f64) { 98 | self.0 /= rhs; 99 | } 100 | } 101 | 102 | impl Neg for PlaybackRate { 103 | type Output = PlaybackRate; 104 | 105 | fn neg(self) -> Self::Output { 106 | Self(-self.0) 107 | } 108 | } 109 | 110 | impl Rem for PlaybackRate { 111 | type Output = PlaybackRate; 112 | 113 | fn rem(self, rhs: f64) -> Self::Output { 114 | Self(self.0 % rhs) 115 | } 116 | } 117 | 118 | impl RemAssign for PlaybackRate { 119 | fn rem_assign(&mut self, rhs: f64) { 120 | self.0 %= rhs; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /crates/kira/src/playback_state_manager.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | info::Info, parameter::Parameter, sound::PlaybackState, Decibels, StartTime, Tween, Value, 3 | }; 4 | 5 | pub(crate) struct PlaybackStateManager { 6 | state: State, 7 | volume_fade: Parameter, 8 | } 9 | 10 | impl PlaybackStateManager { 11 | pub fn new(fade_in_tween: Option) -> Self { 12 | Self { 13 | state: State::Playing, 14 | volume_fade: fade_in_tween 15 | .map(|tween| { 16 | let mut parameter = 17 | Parameter::new(Value::Fixed(Decibels::SILENCE), Decibels::SILENCE); 18 | parameter.set(Value::Fixed(Decibels::IDENTITY), tween); 19 | parameter 20 | }) 21 | .unwrap_or_else(|| { 22 | Parameter::new(Value::Fixed(Decibels::IDENTITY), Decibels::IDENTITY) 23 | }), 24 | } 25 | } 26 | 27 | pub fn interpolated_fade_volume(&self, amount: f64) -> Decibels { 28 | self.volume_fade.interpolated_value(amount) 29 | } 30 | 31 | pub fn playback_state(&self) -> PlaybackState { 32 | match self.state { 33 | State::Playing => PlaybackState::Playing, 34 | State::Pausing => PlaybackState::Pausing, 35 | State::Paused => PlaybackState::Paused, 36 | State::WaitingToResume { .. } => PlaybackState::WaitingToResume, 37 | State::Resuming => PlaybackState::Resuming, 38 | State::Stopping => PlaybackState::Stopping, 39 | State::Stopped => PlaybackState::Stopped, 40 | } 41 | } 42 | 43 | pub fn pause(&mut self, fade_out_tween: Tween) { 44 | if let State::Stopped = &self.state { 45 | return; 46 | } 47 | self.state = State::Pausing; 48 | self.volume_fade 49 | .set(Value::Fixed(Decibels::SILENCE), fade_out_tween); 50 | } 51 | 52 | pub fn resume(&mut self, start_time: StartTime, fade_in_tween: Tween) { 53 | if let State::Stopped = &self.state { 54 | return; 55 | } 56 | if let StartTime::Immediate = start_time { 57 | self.state = State::Resuming; 58 | self.volume_fade 59 | .set(Value::Fixed(Decibels::IDENTITY), fade_in_tween); 60 | } else { 61 | self.state = State::WaitingToResume { 62 | start_time, 63 | fade_in_tween, 64 | }; 65 | } 66 | } 67 | 68 | pub fn stop(&mut self, fade_out_tween: Tween) { 69 | if let State::Stopped = &self.state { 70 | return; 71 | } 72 | self.state = State::Stopping; 73 | self.volume_fade 74 | .set(Value::Fixed(Decibels::SILENCE), fade_out_tween); 75 | } 76 | 77 | pub fn mark_as_stopped(&mut self) { 78 | self.state = State::Stopped; 79 | } 80 | 81 | pub fn update(&mut self, dt: f64, info: &Info) -> ChangedPlaybackState { 82 | let finished = self.volume_fade.update(dt, info); 83 | match &mut self.state { 84 | State::Playing => {} 85 | State::Pausing => { 86 | if finished { 87 | self.state = State::Paused; 88 | return true; 89 | } 90 | } 91 | State::Paused => {} 92 | State::WaitingToResume { 93 | start_time, 94 | fade_in_tween, 95 | } => { 96 | let will_never_start = start_time.update(dt, info); 97 | if will_never_start { 98 | self.state = State::Stopped; 99 | return true; 100 | } 101 | if *start_time == StartTime::Immediate { 102 | let fade_in_tween = *fade_in_tween; 103 | self.resume(StartTime::Immediate, fade_in_tween); 104 | return true; 105 | } 106 | } 107 | State::Resuming => { 108 | if finished { 109 | self.state = State::Playing; 110 | return true; 111 | } 112 | } 113 | State::Stopping => { 114 | if finished { 115 | self.state = State::Stopped; 116 | return true; 117 | } 118 | } 119 | State::Stopped => {} 120 | } 121 | false 122 | } 123 | } 124 | 125 | pub type ChangedPlaybackState = bool; 126 | 127 | enum State { 128 | Playing, 129 | Pausing, 130 | Paused, 131 | WaitingToResume { 132 | start_time: StartTime, 133 | fade_in_tween: Tween, 134 | }, 135 | Resuming, 136 | Stopping, 137 | Stopped, 138 | } 139 | -------------------------------------------------------------------------------- /crates/kira/src/semitones.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{ 2 | Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign, 3 | }; 4 | 5 | use crate::{tween::Tweenable, PlaybackRate, Value}; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] 8 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 9 | #[cfg_attr(feature = "serde", serde(transparent))] 10 | /// A change in pitch in semitones. 11 | /// 12 | /// This can be used where [`PlaybackRate`](crate::PlaybackRate)s are expected to control 13 | /// the pitch of a sound. 14 | pub struct Semitones(pub f64); 15 | 16 | impl Tweenable for Semitones { 17 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 18 | Self(Tweenable::interpolate(a.0, b.0, amount)) 19 | } 20 | } 21 | 22 | impl From for Semitones { 23 | fn from(value: f64) -> Self { 24 | Self(value) 25 | } 26 | } 27 | 28 | impl From for PlaybackRate { 29 | fn from(value: Semitones) -> Self { 30 | PlaybackRate(2.0f64.powf(value.0 / 12.0)) 31 | } 32 | } 33 | 34 | impl From for Value { 35 | fn from(value: Semitones) -> Self { 36 | Self::Fixed(value.into()) 37 | } 38 | } 39 | 40 | impl Add for Semitones { 41 | type Output = Semitones; 42 | 43 | fn add(self, rhs: Semitones) -> Self::Output { 44 | Self(self.0 + rhs.0) 45 | } 46 | } 47 | 48 | impl AddAssign for Semitones { 49 | fn add_assign(&mut self, rhs: Semitones) { 50 | self.0 += rhs.0; 51 | } 52 | } 53 | 54 | impl Sub for Semitones { 55 | type Output = Semitones; 56 | 57 | fn sub(self, rhs: Semitones) -> Self::Output { 58 | Self(self.0 - rhs.0) 59 | } 60 | } 61 | 62 | impl SubAssign for Semitones { 63 | fn sub_assign(&mut self, rhs: Semitones) { 64 | self.0 -= rhs.0; 65 | } 66 | } 67 | 68 | impl Mul for Semitones { 69 | type Output = Semitones; 70 | 71 | fn mul(self, rhs: f64) -> Self::Output { 72 | Self(self.0 * rhs) 73 | } 74 | } 75 | 76 | impl MulAssign for Semitones { 77 | fn mul_assign(&mut self, rhs: f64) { 78 | self.0 *= rhs; 79 | } 80 | } 81 | 82 | impl Div for Semitones { 83 | type Output = Semitones; 84 | 85 | fn div(self, rhs: f64) -> Self::Output { 86 | Self(self.0 / rhs) 87 | } 88 | } 89 | 90 | impl DivAssign for Semitones { 91 | fn div_assign(&mut self, rhs: f64) { 92 | self.0 /= rhs; 93 | } 94 | } 95 | 96 | impl Neg for Semitones { 97 | type Output = Semitones; 98 | 99 | fn neg(self) -> Self::Output { 100 | Self(-self.0) 101 | } 102 | } 103 | 104 | impl Rem for Semitones { 105 | type Output = Semitones; 106 | 107 | fn rem(self, rhs: f64) -> Self::Output { 108 | Self(self.0 % rhs) 109 | } 110 | } 111 | 112 | impl RemAssign for Semitones { 113 | fn rem_assign(&mut self, rhs: f64) { 114 | self.0 %= rhs; 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | #[test] 120 | #[allow(clippy::float_cmp)] 121 | fn test() { 122 | /// A table of semitone differences to pitch factors. 123 | /// Values calculated from http://www.sengpielaudio.com/calculator-centsratio.htm 124 | const TEST_CALCULATIONS: [(Semitones, PlaybackRate); 5] = [ 125 | (Semitones(0.0), PlaybackRate(1.0)), 126 | (Semitones(1.0), PlaybackRate(1.059463)), 127 | (Semitones(2.0), PlaybackRate(1.122462)), 128 | (Semitones(-1.0), PlaybackRate(0.943874)), 129 | (Semitones(-2.0), PlaybackRate(0.890899)), 130 | ]; 131 | 132 | for (semitones, playback_rate) in TEST_CALCULATIONS { 133 | assert!((PlaybackRate::from(semitones).0 - playback_rate.0).abs() < 0.00001); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crates/kira/src/sound/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | /// Errors that can occur when loading or streaming an audio file. 4 | #[derive(Debug)] 5 | #[cfg_attr(docsrs, doc(cfg(feature = "symphonia")))] 6 | pub enum FromFileError { 7 | /// Could not determine the default audio track in the file. 8 | NoDefaultTrack, 9 | /// Could not determine the sample rate of the audio. 10 | UnknownSampleRate, 11 | /// Could not determine the duration of the audio. 12 | UnknownDuration, 13 | /// The audio uses an unsupported channel configuration. Only 14 | /// mono and stereo audio is supported. 15 | UnsupportedChannelConfiguration, 16 | /// An error occurred while reading the file from the filesystem. 17 | IoError(std::io::Error), 18 | /// An error occurred when parsing the file. 19 | SymphoniaError(symphonia::core::errors::Error), 20 | } 21 | 22 | impl Display for FromFileError { 23 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 24 | match self { 25 | FromFileError::NoDefaultTrack => { 26 | f.write_str("Could not determine the default audio track") 27 | } 28 | FromFileError::UnknownSampleRate => { 29 | f.write_str("Could not detect the sample rate of the audio") 30 | } 31 | FromFileError::UnknownDuration => { 32 | f.write_str("Could not detect the duration of the audio") 33 | } 34 | FromFileError::UnsupportedChannelConfiguration => { 35 | f.write_str("Only mono and stereo audio is supported") 36 | } 37 | FromFileError::IoError(error) => error.fmt(f), 38 | FromFileError::SymphoniaError(error) => error.fmt(f), 39 | } 40 | } 41 | } 42 | 43 | impl std::error::Error for FromFileError { 44 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 45 | match self { 46 | FromFileError::IoError(error) => Some(error), 47 | FromFileError::SymphoniaError(error) => Some(error), 48 | _ => None, 49 | } 50 | } 51 | } 52 | 53 | impl From for FromFileError { 54 | fn from(v: std::io::Error) -> Self { 55 | Self::IoError(v) 56 | } 57 | } 58 | 59 | impl From for FromFileError { 60 | fn from(v: symphonia::core::errors::Error) -> Self { 61 | Self::SymphoniaError(v) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/kira/src/sound/playback_position.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test; 3 | 4 | /// A point in time in a piece of audio. 5 | #[derive(Debug, Clone, Copy, PartialEq)] 6 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 7 | pub enum PlaybackPosition { 8 | /// The time in seconds. 9 | Seconds(f64), 10 | /// The time in samples (individual audio data points). 11 | Samples(usize), 12 | } 13 | 14 | impl PlaybackPosition { 15 | #[must_use] 16 | pub(crate) fn into_samples(self, sample_rate: u32) -> usize { 17 | match self { 18 | PlaybackPosition::Seconds(seconds) => (seconds * sample_rate as f64).round() as usize, 19 | PlaybackPosition::Samples(samples) => samples, 20 | } 21 | } 22 | } 23 | 24 | impl From for PlaybackPosition { 25 | fn from(v: f64) -> Self { 26 | Self::Seconds(v) 27 | } 28 | } 29 | 30 | impl Default for PlaybackPosition { 31 | fn default() -> Self { 32 | Self::Seconds(0.0) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/kira/src/sound/playback_position/test.rs: -------------------------------------------------------------------------------- 1 | use super::PlaybackPosition; 2 | 3 | #[test] 4 | fn into_samples() { 5 | const SAMPLE_RATE: u32 = 44100; 6 | const SECONDS_TO_SAMPLES: &[(f64, usize)] = &[(8.786054, 387465), (0.061519, 2713)]; 7 | for (seconds, samples) in SECONDS_TO_SAMPLES { 8 | assert_eq!( 9 | PlaybackPosition::Seconds(*seconds).into_samples(SAMPLE_RATE), 10 | *samples 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/kira/src/sound/static_sound.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Playable chunks of audio that are loaded into memory all at once. 3 | 4 | To play a static sound, pass a [`StaticSoundData`] to 5 | [`AudioManager::play`](crate::AudioManager::play). 6 | 7 | ```no_run 8 | use kira::{ 9 | AudioManager, AudioManagerSettings, DefaultBackend, 10 | sound::static_sound::{StaticSoundData, StaticSoundSettings}, 11 | }; 12 | 13 | let mut manager = AudioManager::::new(AudioManagerSettings::default())?; 14 | let sound_data = StaticSoundData::from_file("sound.ogg")?; 15 | manager.play(sound_data)?; 16 | # Result::<(), Box>::Ok(()) 17 | ``` 18 | 19 | Compared to streaming sounds, static sounds have lower CPU usage and shorter delays 20 | when starting and seeking, but they use a lot more memory. 21 | */ 22 | 23 | mod data; 24 | mod handle; 25 | mod settings; 26 | mod sound; 27 | 28 | pub use data::*; 29 | pub use handle::*; 30 | pub use settings::*; 31 | 32 | use crate::{ 33 | command::ValueChangeCommand, command_writers_and_readers, tween::Tween, Decibels, Panning, 34 | PlaybackRate, StartTime, 35 | }; 36 | 37 | use super::Region; 38 | 39 | command_writers_and_readers! { 40 | set_volume: ValueChangeCommand, 41 | set_playback_rate: ValueChangeCommand, 42 | set_panning: ValueChangeCommand, 43 | set_loop_region: Option, 44 | pause: Tween, 45 | resume: (StartTime, Tween), 46 | stop: Tween, 47 | seek_by: f64, 48 | seek_to: f64, 49 | } 50 | -------------------------------------------------------------------------------- /crates/kira/src/sound/static_sound/data/from_file.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use symphonia::core::io::{MediaSource, MediaSourceStream}; 4 | 5 | use crate::sound::{ 6 | static_sound::StaticSoundSettings, symphonia::load_frames_from_buffer_ref, FromFileError, 7 | }; 8 | 9 | use super::StaticSoundData; 10 | 11 | impl StaticSoundData { 12 | /// Loads an audio file into a [`StaticSoundData`]. 13 | #[cfg(not(target_arch = "wasm32"))] 14 | #[cfg_attr(docsrs, doc(cfg(all(feature = "symphonia", not(wasm32)))))] 15 | pub fn from_file(path: impl AsRef) -> Result { 16 | Self::from_media_source(std::fs::File::open(path)?) 17 | } 18 | 19 | /// Loads a cursor wrapping audio file data into a [`StaticSoundData`]. 20 | #[cfg_attr(docsrs, doc(cfg(feature = "symphonia")))] 21 | pub fn from_cursor + Send + Sync + 'static>( 22 | cursor: Cursor, 23 | ) -> Result { 24 | Self::from_media_source(cursor) 25 | } 26 | 27 | /// Loads an audio file from a type that implements Symphonia's [`MediaSource`] 28 | /// trait. 29 | #[cfg_attr(docsrs, doc(cfg(feature = "symphonia")))] 30 | pub fn from_media_source( 31 | media_source: impl MediaSource + 'static, 32 | ) -> Result { 33 | Self::from_boxed_media_source(Box::new(media_source)) 34 | } 35 | 36 | fn from_boxed_media_source(media_source: Box) -> Result { 37 | let codecs = symphonia::default::get_codecs(); 38 | let probe = symphonia::default::get_probe(); 39 | let mss = MediaSourceStream::new(media_source, Default::default()); 40 | let mut format_reader = probe 41 | .format( 42 | &Default::default(), 43 | mss, 44 | &Default::default(), 45 | &Default::default(), 46 | )? 47 | .format; 48 | let codec_params = &format_reader 49 | .default_track() 50 | .ok_or(FromFileError::NoDefaultTrack)? 51 | .codec_params; 52 | let sample_rate = codec_params 53 | .sample_rate 54 | .ok_or(FromFileError::UnknownSampleRate)?; 55 | let mut decoder = codecs.make(codec_params, &Default::default())?; 56 | let mut frames = vec![]; 57 | loop { 58 | match format_reader.next_packet() { 59 | Ok(packet) => { 60 | let buffer = decoder.decode(&packet)?; 61 | frames.append(&mut load_frames_from_buffer_ref(&buffer)?); 62 | } 63 | Err(error) => match error { 64 | symphonia::core::errors::Error::IoError(error) => { 65 | if error.kind() == std::io::ErrorKind::UnexpectedEof { 66 | break; 67 | } 68 | return Err(symphonia::core::errors::Error::IoError(error).into()); 69 | } 70 | error => return Err(error.into()), 71 | }, 72 | } 73 | } 74 | Ok(Self { 75 | sample_rate, 76 | frames: frames.into(), 77 | settings: StaticSoundSettings::default(), 78 | slice: None, 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/kira/src/sound/static_sound/data/test.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::Duration}; 2 | 3 | use crate::frame::Frame; 4 | 5 | use super::StaticSoundData; 6 | 7 | #[test] 8 | fn duration() { 9 | let static_sound = StaticSoundData { 10 | sample_rate: 1, 11 | frames: Arc::new([Frame::from_mono(0.0); 4]), 12 | settings: Default::default(), 13 | slice: None, 14 | }; 15 | assert_eq!(static_sound.duration(), Duration::from_secs(4)); 16 | } 17 | 18 | #[test] 19 | fn unsliced_duration() { 20 | let static_sound = StaticSoundData { 21 | sample_rate: 1, 22 | frames: Arc::new([Frame::from_mono(0.0); 4]), 23 | settings: Default::default(), 24 | slice: Some((2, 3)), 25 | }; 26 | assert_eq!(static_sound.unsliced_duration(), Duration::from_secs(4)); 27 | } 28 | 29 | #[test] 30 | fn sliced_duration() { 31 | let static_sound = StaticSoundData { 32 | sample_rate: 1, 33 | frames: Arc::new([Frame::from_mono(0.0); 4]), 34 | settings: Default::default(), 35 | slice: None, 36 | }; 37 | assert_eq!(static_sound.duration(), Duration::from_secs(4)); 38 | 39 | let static_sound = StaticSoundData { 40 | sample_rate: 1, 41 | frames: Arc::new([Frame::from_mono(0.0); 4]), 42 | settings: Default::default(), 43 | slice: Some((2, 3)), 44 | }; 45 | assert_eq!(static_sound.duration(), Duration::from_secs(1)); 46 | } 47 | 48 | #[test] 49 | fn slice() { 50 | let static_sound = StaticSoundData { 51 | sample_rate: 1, 52 | frames: (0..10).map(|i| Frame::from_mono(i as f32)).collect(), 53 | settings: Default::default(), 54 | slice: None, 55 | } 56 | .slice(3.0..6.0); 57 | for i in 0..3 { 58 | assert_eq!( 59 | static_sound.frame_at_index(i), 60 | Some(Frame::from_mono(i as f32 + 3.0)) 61 | ); 62 | } 63 | assert!(static_sound.frame_at_index(3).is_none()); 64 | } 65 | -------------------------------------------------------------------------------- /crates/kira/src/sound/static_sound/settings.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | sound::{IntoOptionalRegion, PlaybackPosition, Region}, 3 | Tween, 4 | Decibels, Panning, PlaybackRate, StartTime, Value, 5 | }; 6 | 7 | /// Settings for a static sound. 8 | #[derive(Debug, Clone, Copy, PartialEq)] 9 | pub struct StaticSoundSettings { 10 | /// When the sound should start playing. 11 | pub start_time: StartTime, 12 | /// Where in the sound playback should start. 13 | pub start_position: PlaybackPosition, 14 | /// The portion of the sound that should be looped. 15 | pub loop_region: Option, 16 | /// Whether the sound should be played in reverse. 17 | pub reverse: bool, 18 | /// The volume of the sound. 19 | pub volume: Value, 20 | /// The playback rate of the sound. 21 | /// 22 | /// Changing the playback rate will change both the speed 23 | /// and the pitch of the sound. 24 | pub playback_rate: Value, 25 | /// The panning of the sound, where 0 is hard left 26 | /// and 1 is hard right. 27 | pub panning: Value, 28 | /// An optional fade-in from silence. 29 | pub fade_in_tween: Option, 30 | } 31 | 32 | impl StaticSoundSettings { 33 | /// Creates a new [`StaticSoundSettings`] with the default settings. 34 | #[must_use] 35 | pub fn new() -> Self { 36 | Self { 37 | start_time: StartTime::default(), 38 | start_position: PlaybackPosition::Seconds(0.0), 39 | reverse: false, 40 | loop_region: None, 41 | volume: Value::Fixed(Decibels::IDENTITY), 42 | playback_rate: Value::Fixed(PlaybackRate(1.0)), 43 | panning: Value::Fixed(Panning::CENTER), 44 | fade_in_tween: None, 45 | } 46 | } 47 | 48 | /** Sets when the sound should start playing. */ 49 | #[must_use = "This method consumes self and returns a modified StaticSoundSettings, so the return value should be used"] 50 | pub fn start_time(self, start_time: impl Into) -> Self { 51 | Self { 52 | start_time: start_time.into(), 53 | ..self 54 | } 55 | } 56 | 57 | /// Sets where in the sound playback should start. 58 | #[must_use = "This method consumes self and returns a modified StaticSoundSettings, so the return value should be used"] 59 | pub fn start_position(self, start_position: impl Into) -> Self { 60 | Self { 61 | start_position: start_position.into(), 62 | ..self 63 | } 64 | } 65 | 66 | /// Sets whether the sound should be played in reverse. 67 | #[must_use = "This method consumes self and returns a modified StaticSoundSettings, so the return value should be used"] 68 | pub fn reverse(self, reverse: bool) -> Self { 69 | Self { reverse, ..self } 70 | } 71 | 72 | /** Sets the portion of the sound that should be looped. */ 73 | #[must_use = "This method consumes self and returns a modified StaticSoundSettings, so the return value should be used"] 74 | pub fn loop_region(self, loop_region: impl IntoOptionalRegion) -> Self { 75 | Self { 76 | loop_region: loop_region.into_optional_region(), 77 | ..self 78 | } 79 | } 80 | 81 | /** Sets the volume of the sound. */ 82 | #[must_use = "This method consumes self and returns a modified StaticSoundSettings, so the return value should be used"] 83 | pub fn volume(self, volume: impl Into>) -> Self { 84 | Self { 85 | volume: volume.into(), 86 | ..self 87 | } 88 | } 89 | 90 | /** 91 | Sets the playback rate of the sound. 92 | 93 | Changing the playback rate will change both the speed 94 | and the pitch of the sound. 95 | */ 96 | #[must_use = "This method consumes self and returns a modified StaticSoundSettings, so the return value should be used"] 97 | pub fn playback_rate(self, playback_rate: impl Into>) -> Self { 98 | Self { 99 | playback_rate: playback_rate.into(), 100 | ..self 101 | } 102 | } 103 | 104 | /** 105 | Sets the panning of the sound, where 0 is hard left 106 | and 1 is hard right. 107 | */ 108 | #[must_use = "This method consumes self and returns a modified StaticSoundSettings, so the return value should be used"] 109 | pub fn panning(self, panning: impl Into>) -> Self { 110 | Self { 111 | panning: panning.into(), 112 | ..self 113 | } 114 | } 115 | 116 | /// Sets the tween used to fade in the sound from silence. 117 | #[must_use = "This method consumes self and returns a modified StaticSoundSettings, so the return value should be used"] 118 | pub fn fade_in_tween(self, fade_in_tween: impl Into>) -> Self { 119 | Self { 120 | fade_in_tween: fade_in_tween.into(), 121 | ..self 122 | } 123 | } 124 | } 125 | 126 | impl Default for StaticSoundSettings { 127 | fn default() -> Self { 128 | Self::new() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /crates/kira/src/sound/static_sound/sound/resampler.rs: -------------------------------------------------------------------------------- 1 | use crate::frame::{interpolate_frame, Frame}; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq)] 4 | struct RecentFrame { 5 | /// A frame of audio. 6 | frame: Frame, 7 | /// The current frame index of the source sound at the 8 | /// time this frame was pushed to the resampler. 9 | frame_index: usize, 10 | } 11 | 12 | pub(super) struct Resampler { 13 | frames: [RecentFrame; 4], 14 | time_until_empty: usize, 15 | } 16 | 17 | impl Resampler { 18 | #[must_use] 19 | pub fn new(starting_frame_index: usize) -> Self { 20 | Self { 21 | frames: [RecentFrame { 22 | frame: Frame::ZERO, 23 | frame_index: starting_frame_index, 24 | }; 4], 25 | time_until_empty: 0, 26 | } 27 | } 28 | 29 | pub fn push_frame(&mut self, frame: Option, sample_index: usize) { 30 | if frame.is_some() { 31 | self.time_until_empty = 4; 32 | } else { 33 | self.time_until_empty = self.time_until_empty.saturating_sub(1); 34 | } 35 | let frame = frame.unwrap_or_default(); 36 | self.frames.copy_within(1.., 0); 37 | self.frames[self.frames.len() - 1] = RecentFrame { 38 | frame, 39 | frame_index: sample_index, 40 | }; 41 | } 42 | 43 | #[must_use] 44 | pub fn get(&self, fractional_position: f32) -> Frame { 45 | interpolate_frame( 46 | self.frames[0].frame, 47 | self.frames[1].frame, 48 | self.frames[2].frame, 49 | self.frames[3].frame, 50 | fractional_position, 51 | ) 52 | } 53 | 54 | /// Returns the index of the frame in the source sound 55 | /// that the user is currently hearing from this resampler. 56 | /// 57 | /// This is not the same as the most recently pushed frame. 58 | /// The user mainly hears a frame between `self.frames[1]` and 59 | /// `self.frames[2]`. `self.frames[0]` and `self.frames[3]` 60 | /// are used to provide additional information to the interpolation 61 | /// algorithm to get a smoother result. 62 | #[must_use] 63 | pub fn current_frame_index(&self) -> usize { 64 | self.frames[1].frame_index 65 | } 66 | 67 | #[must_use] 68 | pub fn empty(&self) -> bool { 69 | self.time_until_empty == 0 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/kira/src/sound/streaming.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Decodes data gradually from an audio file. 3 | 4 | To play a streaming sound, pass a [`StreamingSoundData`] to 5 | [`AudioManager::play`](crate::AudioManager::play). 6 | 7 | ```no_run 8 | use kira::{ 9 | AudioManager, AudioManagerSettings, DefaultBackend, 10 | sound::streaming::{StreamingSoundData, StreamingSoundSettings}, 11 | }; 12 | 13 | let mut manager = AudioManager::::new(AudioManagerSettings::default())?; 14 | let sound_data = StreamingSoundData::from_file("sound.ogg")?; 15 | manager.play(sound_data)?; 16 | # Result::<(), Box>::Ok(()) 17 | ``` 18 | 19 | Streaming sounds use less memory than static sounds, but they use more 20 | CPU, and they can have delays when starting or seeking. 21 | */ 22 | 23 | #![cfg_attr(docsrs, doc(cfg(not(wasm32))))] 24 | 25 | mod data; 26 | mod decoder; 27 | mod handle; 28 | mod settings; 29 | mod sound; 30 | 31 | pub use data::*; 32 | pub use decoder::*; 33 | pub use handle::*; 34 | pub use settings::*; 35 | 36 | use crate::{ 37 | command::{command_writer_and_reader, CommandReader, CommandWriter, ValueChangeCommand}, 38 | Decibels, Panning, PlaybackRate, StartTime, Tween, 39 | }; 40 | 41 | use super::Region; 42 | 43 | #[derive(Debug)] 44 | pub(crate) struct CommandWriters { 45 | set_volume: CommandWriter>, 46 | set_playback_rate: CommandWriter>, 47 | set_panning: CommandWriter>, 48 | set_loop_region: CommandWriter>, 49 | pause: CommandWriter, 50 | resume: CommandWriter<(StartTime, Tween)>, 51 | stop: CommandWriter, 52 | seek_by: CommandWriter, 53 | seek_to: CommandWriter, 54 | } 55 | 56 | pub(crate) struct CommandReaders { 57 | set_volume: CommandReader>, 58 | set_playback_rate: CommandReader>, 59 | set_panning: CommandReader>, 60 | pause: CommandReader, 61 | resume: CommandReader<(StartTime, Tween)>, 62 | stop: CommandReader, 63 | } 64 | 65 | #[derive(Debug)] 66 | pub(crate) struct DecodeSchedulerCommandReaders { 67 | set_loop_region: CommandReader>, 68 | seek_by: CommandReader, 69 | seek_to: CommandReader, 70 | } 71 | 72 | #[must_use] 73 | fn command_writers_and_readers() -> ( 74 | CommandWriters, 75 | CommandReaders, 76 | DecodeSchedulerCommandReaders, 77 | ) { 78 | let (set_volume_writer, set_volume_reader) = command_writer_and_reader(); 79 | let (set_playback_rate_writer, set_playback_rate_reader) = command_writer_and_reader(); 80 | let (set_panning_writer, set_panning_reader) = command_writer_and_reader(); 81 | let (set_loop_region_writer, set_loop_region_reader) = command_writer_and_reader(); 82 | let (pause_writer, pause_reader) = command_writer_and_reader(); 83 | let (resume_writer, resume_reader) = command_writer_and_reader(); 84 | let (stop_writer, stop_reader) = command_writer_and_reader(); 85 | let (seek_by_writer, seek_by_reader) = command_writer_and_reader(); 86 | let (seek_to_writer, seek_to_reader) = command_writer_and_reader(); 87 | ( 88 | CommandWriters { 89 | set_volume: set_volume_writer, 90 | set_playback_rate: set_playback_rate_writer, 91 | set_panning: set_panning_writer, 92 | set_loop_region: set_loop_region_writer, 93 | pause: pause_writer, 94 | resume: resume_writer, 95 | stop: stop_writer, 96 | seek_by: seek_by_writer, 97 | seek_to: seek_to_writer, 98 | }, 99 | CommandReaders { 100 | set_volume: set_volume_reader, 101 | set_playback_rate: set_playback_rate_reader, 102 | set_panning: set_panning_reader, 103 | pause: pause_reader, 104 | resume: resume_reader, 105 | stop: stop_reader, 106 | }, 107 | DecodeSchedulerCommandReaders { 108 | set_loop_region: set_loop_region_reader, 109 | seek_by: seek_by_reader, 110 | seek_to: seek_to_reader, 111 | }, 112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /crates/kira/src/sound/streaming/data/test.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{ 4 | frame::Frame, 5 | sound::streaming::{mock::MockDecoder, StreamingSoundData}, 6 | }; 7 | 8 | #[test] 9 | fn duration() { 10 | let sound = StreamingSoundData { 11 | decoder: Box::new(MockDecoder::new(vec![Frame::from_mono(0.5); 4])), 12 | settings: Default::default(), 13 | slice: None, 14 | }; 15 | assert_eq!(sound.duration(), Duration::from_secs(4)); 16 | } 17 | 18 | #[test] 19 | fn unsliced_duration() { 20 | let sound = StreamingSoundData { 21 | decoder: Box::new(MockDecoder::new(vec![Frame::from_mono(0.5); 4])), 22 | settings: Default::default(), 23 | slice: Some((2, 3)), 24 | }; 25 | assert_eq!(sound.unsliced_duration(), Duration::from_secs(4)); 26 | } 27 | 28 | #[test] 29 | fn sliced_duration() { 30 | let sound = StreamingSoundData { 31 | decoder: Box::new(MockDecoder::new(vec![Frame::from_mono(0.5); 4])), 32 | settings: Default::default(), 33 | slice: Some((2, 3)), 34 | }; 35 | assert_eq!(sound.duration(), Duration::from_secs(1)); 36 | } 37 | -------------------------------------------------------------------------------- /crates/kira/src/sound/streaming/decoder.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | pub(crate) mod mock; 3 | #[cfg(feature = "symphonia")] 4 | pub(crate) mod symphonia; 5 | 6 | use crate::frame::Frame; 7 | 8 | /// Decodes chunks of audio. 9 | pub trait Decoder: Send { 10 | /// Errors that can occur when decoding audio. 11 | type Error; 12 | 13 | /// Returns the sample rate of the audio (in Hz). 14 | #[must_use] 15 | fn sample_rate(&self) -> u32; 16 | 17 | /// Returns the total number of samples of audio. 18 | #[must_use] 19 | fn num_frames(&self) -> usize; 20 | 21 | /// Decodes the next chunk of audio. 22 | fn decode(&mut self) -> Result, Self::Error>; 23 | 24 | /// Seeks to an audio sample. 25 | /// 26 | /// The `index` is the _requested_ sample to seek to. It's OK if the decoder 27 | /// seeks to an earlier sample than the one requested. 28 | /// 29 | /// This should return the sample index that was _actually_ seeked to. 30 | fn seek(&mut self, index: usize) -> Result; 31 | } 32 | 33 | type SeekedToIndex = usize; 34 | -------------------------------------------------------------------------------- /crates/kira/src/sound/streaming/decoder/mock.rs: -------------------------------------------------------------------------------- 1 | use crate::frame::Frame; 2 | 3 | use super::Decoder; 4 | 5 | const MOCK_DECODER_SAMPLE_RATE: u32 = 1; 6 | const MOCK_DECODER_PACKET_SIZE: usize = 3; 7 | 8 | pub(crate) struct MockDecoder { 9 | frames: Vec, 10 | current_frame_index: usize, 11 | } 12 | 13 | impl MockDecoder { 14 | #[must_use] 15 | pub(crate) fn new(frames: Vec) -> Self { 16 | Self { 17 | frames, 18 | current_frame_index: 0, 19 | } 20 | } 21 | } 22 | 23 | impl Decoder for MockDecoder { 24 | type Error = MockDecoderError; 25 | 26 | fn sample_rate(&self) -> u32 { 27 | MOCK_DECODER_SAMPLE_RATE 28 | } 29 | 30 | fn num_frames(&self) -> usize { 31 | self.frames.len() 32 | } 33 | 34 | fn decode(&mut self) -> Result, Self::Error> { 35 | let mut frames = vec![]; 36 | for _ in 0..MOCK_DECODER_PACKET_SIZE { 37 | let frame = self.frames[self.current_frame_index]; 38 | if frame.left.is_nan() || frame.right.is_nan() { 39 | return Err(MockDecoderError); 40 | } 41 | frames.push(frame); 42 | self.current_frame_index += 1; 43 | if self.current_frame_index >= self.frames.len() { 44 | break; 45 | } 46 | } 47 | Ok(frames) 48 | } 49 | 50 | fn seek(&mut self, index: usize) -> Result { 51 | // seek to the beginning of the "packet" to simulate 52 | // seeking behavior with real decoders 53 | let index = 54 | (index as f64 / MOCK_DECODER_PACKET_SIZE as f64) as usize * MOCK_DECODER_PACKET_SIZE; 55 | self.current_frame_index = index; 56 | Ok(index) 57 | } 58 | } 59 | 60 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 61 | pub(crate) struct MockDecoderError; 62 | -------------------------------------------------------------------------------- /crates/kira/src/sound/streaming/decoder/symphonia.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use crate::{ 4 | frame::Frame, 5 | sound::{symphonia::load_frames_from_buffer_ref, FromFileError}, 6 | }; 7 | use symphonia::core::{ 8 | codecs::Decoder, 9 | formats::{FormatReader, SeekMode, SeekTo}, 10 | io::{MediaSource, MediaSourceStream}, 11 | probe::Hint, 12 | }; 13 | 14 | pub(crate) struct SymphoniaDecoder { 15 | format_reader: Box, 16 | decoder: Box, 17 | sample_rate: u32, 18 | num_frames: usize, 19 | track_id: u32, 20 | } 21 | 22 | impl SymphoniaDecoder { 23 | pub(crate) fn new(media_source: Box) -> Result { 24 | let codecs = symphonia::default::get_codecs(); 25 | let probe = symphonia::default::get_probe(); 26 | let mss = MediaSourceStream::new(media_source, Default::default()); 27 | let format_reader = probe 28 | .format( 29 | &Hint::default(), 30 | mss, 31 | &Default::default(), 32 | &Default::default(), 33 | )? 34 | .format; 35 | let default_track = format_reader 36 | .default_track() 37 | .ok_or(FromFileError::NoDefaultTrack)?; 38 | let sample_rate = default_track 39 | .codec_params 40 | .sample_rate 41 | .ok_or(FromFileError::UnknownSampleRate)?; 42 | let num_frames = default_track 43 | .codec_params 44 | .n_frames 45 | .ok_or(FromFileError::UnknownSampleRate)? 46 | .try_into() 47 | .expect("could not convert u64 into usize"); 48 | let decoder = codecs.make(&default_track.codec_params, &Default::default())?; 49 | let track_id = default_track.id; 50 | Ok(Self { 51 | format_reader, 52 | decoder, 53 | sample_rate, 54 | num_frames, 55 | track_id, 56 | }) 57 | } 58 | } 59 | 60 | impl super::Decoder for SymphoniaDecoder { 61 | type Error = FromFileError; 62 | 63 | fn sample_rate(&self) -> u32 { 64 | self.sample_rate 65 | } 66 | 67 | fn num_frames(&self) -> usize { 68 | self.num_frames 69 | } 70 | 71 | fn decode(&mut self) -> Result, Self::Error> { 72 | let packet = self.format_reader.next_packet()?; 73 | let buffer = self.decoder.decode(&packet)?; 74 | load_frames_from_buffer_ref(&buffer) 75 | } 76 | 77 | fn seek(&mut self, index: usize) -> Result { 78 | let seeked_to = self.format_reader.seek( 79 | SeekMode::Accurate, 80 | SeekTo::TimeStamp { 81 | ts: index.try_into().expect("could not convert usize into u64"), 82 | track_id: self.track_id, 83 | }, 84 | )?; 85 | Ok(seeked_to 86 | .actual_ts 87 | .try_into() 88 | .expect("could not convert u64 into usize")) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crates/kira/src/sound/streaming/settings.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | sound::{IntoOptionalRegion, PlaybackPosition, Region}, 3 | Tween, 4 | Decibels, Panning, PlaybackRate, StartTime, Value, 5 | }; 6 | 7 | /// Settings for a streaming sound. 8 | #[derive(Debug, Clone, Copy, PartialEq)] 9 | pub struct StreamingSoundSettings { 10 | /// When the sound should start playing. 11 | pub start_time: StartTime, 12 | /// Where in the sound playback should start. 13 | pub start_position: PlaybackPosition, 14 | /// The portion of the sound that should be looped. 15 | pub loop_region: Option, 16 | /// The volume of the sound. 17 | pub volume: Value, 18 | /// The playback rate of the sound. 19 | /// 20 | /// Changing the playback rate will change both the speed 21 | /// and the pitch of the sound. 22 | pub playback_rate: Value, 23 | /// The panning of the sound, where 0 is hard left 24 | /// and 1 is hard right. 25 | pub panning: Value, 26 | /// An optional fade-in from silence. 27 | pub fade_in_tween: Option, 28 | } 29 | 30 | impl StreamingSoundSettings { 31 | /// Creates a new [`StreamingSoundSettings`] with the default settings. 32 | #[must_use] 33 | pub fn new() -> Self { 34 | Self { 35 | start_time: StartTime::Immediate, 36 | start_position: PlaybackPosition::Seconds(0.0), 37 | loop_region: None, 38 | volume: Value::Fixed(Decibels::IDENTITY), 39 | playback_rate: Value::Fixed(PlaybackRate(1.0)), 40 | panning: Value::Fixed(Panning::CENTER), 41 | fade_in_tween: None, 42 | } 43 | } 44 | 45 | /** Sets when the sound should start playing. */ 46 | #[must_use = "This method consumes self and returns a modified StreamingSoundSettings, so the return value should be used"] 47 | pub fn start_time(self, start_time: impl Into) -> Self { 48 | Self { 49 | start_time: start_time.into(), 50 | ..self 51 | } 52 | } 53 | 54 | /// Sets where in the sound playback should start. 55 | #[must_use = "This method consumes self and returns a modified StreamingSoundSettings, so the return value should be used"] 56 | pub fn start_position(self, start_position: impl Into) -> Self { 57 | Self { 58 | start_position: start_position.into(), 59 | ..self 60 | } 61 | } 62 | 63 | /** Sets the portion of the sound that should be looped. */ 64 | #[must_use = "This method consumes self and returns a modified StreamingSoundSettings, so the return value should be used"] 65 | pub fn loop_region(self, loop_region: impl IntoOptionalRegion) -> Self { 66 | Self { 67 | loop_region: loop_region.into_optional_region(), 68 | ..self 69 | } 70 | } 71 | 72 | /** Sets the volume of the sound. */ 73 | #[must_use = "This method consumes self and returns a modified StreamingSoundSettings, so the return value should be used"] 74 | pub fn volume(self, volume: impl Into>) -> Self { 75 | Self { 76 | volume: volume.into(), 77 | ..self 78 | } 79 | } 80 | 81 | /** 82 | Sets the playback rate of the sound. 83 | 84 | Changing the playback rate will change both the speed 85 | and the pitch of the sound. 86 | */ 87 | #[must_use = "This method consumes self and returns a modified StreamingSoundSettings, so the return value should be used"] 88 | pub fn playback_rate(self, playback_rate: impl Into>) -> Self { 89 | Self { 90 | playback_rate: playback_rate.into(), 91 | ..self 92 | } 93 | } 94 | 95 | /** 96 | Sets the panning of the sound, where -1.0 is hard left 97 | and 1.0 is hard right. 98 | */ 99 | #[must_use = "This method consumes self and returns a modified StreamingSoundSettings, so the return value should be used"] 100 | pub fn panning(self, panning: impl Into>) -> Self { 101 | Self { 102 | panning: panning.into(), 103 | ..self 104 | } 105 | } 106 | 107 | /// Sets the tween used to fade in the instance from silence. 108 | #[must_use = "This method consumes self and returns a modified StreamingSoundSettings, so the return value should be used"] 109 | pub fn fade_in_tween(self, fade_in_tween: impl Into>) -> Self { 110 | Self { 111 | fade_in_tween: fade_in_tween.into(), 112 | ..self 113 | } 114 | } 115 | } 116 | 117 | impl Default for StreamingSoundSettings { 118 | fn default() -> Self { 119 | Self::new() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crates/kira/src/sound/symphonia.rs: -------------------------------------------------------------------------------- 1 | use symphonia::core::{ 2 | audio::{AudioBuffer, AudioBufferRef, Signal}, 3 | conv::{FromSample, IntoSample}, 4 | sample::Sample, 5 | }; 6 | 7 | use crate::frame::Frame; 8 | 9 | use super::FromFileError; 10 | 11 | pub fn load_frames_from_buffer_ref(buffer: &AudioBufferRef) -> Result, FromFileError> { 12 | match buffer { 13 | AudioBufferRef::U8(buffer) => load_frames_from_buffer(buffer), 14 | AudioBufferRef::U16(buffer) => load_frames_from_buffer(buffer), 15 | AudioBufferRef::U24(buffer) => load_frames_from_buffer(buffer), 16 | AudioBufferRef::U32(buffer) => load_frames_from_buffer(buffer), 17 | AudioBufferRef::S8(buffer) => load_frames_from_buffer(buffer), 18 | AudioBufferRef::S16(buffer) => load_frames_from_buffer(buffer), 19 | AudioBufferRef::S24(buffer) => load_frames_from_buffer(buffer), 20 | AudioBufferRef::S32(buffer) => load_frames_from_buffer(buffer), 21 | AudioBufferRef::F32(buffer) => load_frames_from_buffer(buffer), 22 | AudioBufferRef::F64(buffer) => load_frames_from_buffer(buffer), 23 | } 24 | } 25 | 26 | pub fn load_frames_from_buffer( 27 | buffer: &AudioBuffer, 28 | ) -> Result, FromFileError> 29 | where 30 | f32: FromSample, 31 | { 32 | match buffer.spec().channels.count() { 33 | 1 => Ok(buffer 34 | .chan(0) 35 | .iter() 36 | .map(|sample| Frame::from_mono((*sample).into_sample())) 37 | .collect()), 38 | 2 => Ok(buffer 39 | .chan(0) 40 | .iter() 41 | .zip(buffer.chan(1).iter()) 42 | .map(|(left, right)| Frame::new((*left).into_sample(), (*right).into_sample())) 43 | .collect()), 44 | _ => Err(FromFileError::UnsupportedChannelConfiguration), 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/kira/src/sound/transport.rs: -------------------------------------------------------------------------------- 1 | use super::{EndPosition, Region}; 2 | 3 | #[cfg(test)] 4 | mod test; 5 | 6 | pub struct Transport { 7 | pub position: usize, 8 | /// The start and end frames of the sound that should be looped. The upper bound 9 | /// is *exclusive*. 10 | pub loop_region: Option<(usize, usize)>, 11 | pub playing: bool, 12 | } 13 | 14 | impl Transport { 15 | #[must_use] 16 | pub fn new( 17 | start_position: usize, 18 | loop_region: Option, 19 | reverse: bool, 20 | sample_rate: u32, 21 | num_frames: usize, 22 | ) -> Self { 23 | let loop_region = loop_region.map(|loop_region| { 24 | let loop_start = loop_region.start.into_samples(sample_rate); 25 | let loop_end = match loop_region.end { 26 | EndPosition::EndOfAudio => num_frames, 27 | EndPosition::Custom(end_position) => end_position.into_samples(sample_rate), 28 | }; 29 | (loop_start, loop_end) 30 | }); 31 | Self { 32 | position: if reverse { 33 | num_frames - 1 - start_position 34 | } else { 35 | start_position 36 | }, 37 | loop_region, 38 | playing: true, 39 | } 40 | } 41 | 42 | pub fn set_loop_region( 43 | &mut self, 44 | loop_region: Option, 45 | sample_rate: u32, 46 | num_frames: usize, 47 | ) { 48 | self.loop_region = loop_region.map(|loop_region| { 49 | let loop_start = loop_region.start.into_samples(sample_rate); 50 | let loop_end = match loop_region.end { 51 | EndPosition::EndOfAudio => num_frames, 52 | EndPosition::Custom(end_position) => end_position.into_samples(sample_rate), 53 | }; 54 | (loop_start, loop_end) 55 | }); 56 | } 57 | 58 | pub fn increment_position(&mut self, num_frames: usize) { 59 | if !self.playing { 60 | return; 61 | } 62 | self.position += 1; 63 | if let Some((loop_start, loop_end)) = self.loop_region { 64 | while self.position >= loop_end { 65 | self.position -= loop_end - loop_start; 66 | } 67 | } 68 | if self.position >= num_frames { 69 | self.playing = false; 70 | } 71 | } 72 | 73 | pub fn decrement_position(&mut self) { 74 | if !self.playing { 75 | return; 76 | } 77 | if let Some((loop_start, loop_end)) = self.loop_region { 78 | while self.position <= loop_start { 79 | self.position += loop_end - loop_start; 80 | } 81 | } 82 | if self.position == 0 { 83 | self.playing = false; 84 | } else { 85 | self.position -= 1; 86 | } 87 | } 88 | 89 | pub fn seek_to(&mut self, mut position: usize, num_frames: usize) { 90 | if let Some((loop_start, loop_end)) = self.loop_region { 91 | if position > self.position { 92 | while position >= loop_end { 93 | position -= loop_end - loop_start; 94 | } 95 | } else { 96 | while position < loop_start { 97 | position += loop_end - loop_start; 98 | } 99 | } 100 | } 101 | self.position = position; 102 | if self.position >= num_frames { 103 | self.playing = false; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /crates/kira/src/sound/transport/test.rs: -------------------------------------------------------------------------------- 1 | use super::Transport; 2 | 3 | #[test] 4 | fn stops_at_end() { 5 | let mut transport = Transport { 6 | position: 2, 7 | loop_region: None, 8 | playing: true, 9 | }; 10 | for i in 2..4 { 11 | assert_eq!(transport.position, i); 12 | assert!(transport.playing); 13 | transport.increment_position(4); 14 | } 15 | assert_eq!(transport.position, 4); 16 | assert!(!transport.playing); 17 | } 18 | 19 | #[test] 20 | fn stops_at_start_when_playing_backwards() { 21 | let mut transport = Transport { 22 | position: 2, 23 | loop_region: None, 24 | playing: true, 25 | }; 26 | for i in (0..=2).rev() { 27 | assert_eq!(transport.position, i); 28 | assert!(transport.playing); 29 | transport.decrement_position(); 30 | } 31 | assert_eq!(transport.position, 0); 32 | assert!(!transport.playing); 33 | } 34 | 35 | #[test] 36 | fn loops() { 37 | let mut transport = Transport { 38 | position: 0, 39 | loop_region: Some((2, 5)), 40 | playing: true, 41 | }; 42 | for i in 0..5 { 43 | assert_eq!(transport.position, i); 44 | assert!(transport.playing); 45 | transport.increment_position(10); 46 | } 47 | for i in 2..5 { 48 | assert_eq!(transport.position, i); 49 | assert!(transport.playing); 50 | transport.increment_position(10); 51 | } 52 | } 53 | 54 | #[test] 55 | fn loops_when_playing_backward() { 56 | let mut transport = Transport { 57 | position: 0, 58 | loop_region: Some((2, 5)), 59 | playing: true, 60 | }; 61 | transport.position = 10; 62 | for i in (2..=10).rev() { 63 | assert_eq!(transport.position, i); 64 | assert!(transport.playing); 65 | transport.decrement_position(); 66 | } 67 | for i in (2..5).rev() { 68 | assert_eq!(transport.position, i); 69 | assert!(transport.playing); 70 | transport.decrement_position(); 71 | } 72 | } 73 | 74 | #[test] 75 | fn loop_wrapping() { 76 | let mut transport = Transport { 77 | position: 0, 78 | loop_region: Some((2, 5)), 79 | playing: true, 80 | }; 81 | transport.position = 6; 82 | transport.increment_position(10); 83 | assert_eq!(transport.position, 4); 84 | transport.position = 1; 85 | transport.decrement_position(); 86 | assert_eq!(transport.position, 3); 87 | } 88 | 89 | #[test] 90 | fn seek_loop_wrapping() { 91 | let mut transport = Transport { 92 | position: 0, 93 | loop_region: Some((2, 5)), 94 | playing: true, 95 | }; 96 | transport.seek_to(7, 10); 97 | assert_eq!(transport.position, 4); 98 | transport.seek_to(0, 10); 99 | assert_eq!(transport.position, 3); 100 | } 101 | 102 | #[test] 103 | fn seek_out_of_bounds() { 104 | let mut transport = Transport::new(5, None, false, 1, 10); 105 | transport.seek_to(10, 10); 106 | assert!(!transport.playing); 107 | } 108 | -------------------------------------------------------------------------------- /crates/kira/src/start_time.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{ 4 | clock::ClockTime, 5 | info::{Info, WhenToStart}, 6 | }; 7 | 8 | /// Describes when an action should occur. 9 | #[derive(Debug, Clone, Copy, PartialEq, Default)] 10 | pub enum StartTime { 11 | /// The action should occur immediately. 12 | #[default] 13 | Immediate, 14 | /// The action should occur a certain amount of time from now. 15 | Delayed(Duration), 16 | /// The action should occur when a clock reaches a 17 | /// specific time. 18 | ClockTime(ClockTime), 19 | } 20 | 21 | impl StartTime { 22 | pub(crate) fn update(&mut self, dt: f64, info: &Info) -> WillNeverStart { 23 | match self { 24 | StartTime::Immediate => {} 25 | StartTime::Delayed(time_remaining) => { 26 | *time_remaining = time_remaining.saturating_sub(Duration::from_secs_f64(dt)); 27 | if time_remaining.is_zero() { 28 | *self = StartTime::Immediate; 29 | } 30 | } 31 | StartTime::ClockTime(clock_time) => match info.when_to_start(*clock_time) { 32 | WhenToStart::Now => { 33 | *self = StartTime::Immediate; 34 | } 35 | WhenToStart::Later => {} 36 | WhenToStart::Never => return true, 37 | }, 38 | } 39 | false 40 | } 41 | } 42 | 43 | pub(crate) type WillNeverStart = bool; 44 | 45 | impl From for StartTime { 46 | fn from(v: Duration) -> Self { 47 | Self::Delayed(v) 48 | } 49 | } 50 | 51 | impl From for StartTime { 52 | fn from(v: ClockTime) -> Self { 53 | Self::ClockTime(v) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/kira/src/test_helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::{info::MockInfoBuilder, sound::Sound, Frame}; 2 | 3 | pub fn expect_frame_soon(expected_frame: Frame, sound: &mut dyn Sound) { 4 | const NUM_SAMPLES_TO_WAIT: usize = 10; 5 | let mut collected_samples = vec![]; 6 | for _ in 0..NUM_SAMPLES_TO_WAIT { 7 | let frame = sound.process_one(1.0, &MockInfoBuilder::new().build()); 8 | if frame == expected_frame { 9 | return; 10 | } 11 | collected_samples.push(frame); 12 | } 13 | panic!( 14 | "Sound did not output frame with value {:?} within {} samples. Recent samples: {:#?}", 15 | expected_frame, NUM_SAMPLES_TO_WAIT, collected_samples 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /crates/kira/src/track/main.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod handle; 3 | 4 | pub use builder::*; 5 | pub use handle::*; 6 | 7 | use crate::{ 8 | backend::resources::ResourceStorage, 9 | command::{CommandReader, ValueChangeCommand}, 10 | effect::Effect, 11 | info::Info, 12 | sound::Sound, 13 | Decibels, Frame, Parameter, 14 | }; 15 | 16 | pub(crate) struct MainTrack { 17 | volume: Parameter, 18 | set_volume_command_reader: CommandReader>, 19 | sounds: ResourceStorage>, 20 | effects: Vec>, 21 | temp_buffer: Vec, 22 | internal_buffer_size: usize, 23 | } 24 | 25 | impl MainTrack { 26 | pub fn init_effects(&mut self, sample_rate: u32) { 27 | for effect in &mut self.effects { 28 | effect.init(sample_rate, self.internal_buffer_size); 29 | } 30 | } 31 | 32 | pub fn on_change_sample_rate(&mut self, sample_rate: u32) { 33 | for effect in &mut self.effects { 34 | effect.on_change_sample_rate(sample_rate); 35 | } 36 | } 37 | 38 | pub fn on_start_processing(&mut self) { 39 | self.volume 40 | .read_command(&mut self.set_volume_command_reader); 41 | self.sounds.remove_and_add(|sound| sound.finished()); 42 | for (_, sound) in &mut self.sounds { 43 | sound.on_start_processing(); 44 | } 45 | for effect in &mut self.effects { 46 | effect.on_start_processing(); 47 | } 48 | } 49 | 50 | pub fn process(&mut self, out: &mut [Frame], dt: f64, info: &Info) { 51 | self.volume.update(dt * out.len() as f64, info); 52 | for (_, sound) in &mut self.sounds { 53 | sound.process(&mut self.temp_buffer[..out.len()], dt, info); 54 | for (summed_out, sound_out) in out.iter_mut().zip(self.temp_buffer.iter().copied()) { 55 | *summed_out += sound_out; 56 | } 57 | self.temp_buffer.fill(Frame::ZERO); 58 | } 59 | for effect in &mut self.effects { 60 | effect.process(out, dt, info); 61 | } 62 | let num_frames = out.len(); 63 | for (i, frame) in out.iter_mut().enumerate() { 64 | let time_in_chunk = (i + 1) as f64 / num_frames as f64; 65 | let volume = self.volume.interpolated_value(time_in_chunk).as_amplitude(); 66 | *frame *= volume; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/kira/src/track/main/handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | backend::resources::ResourceController, 3 | command::{CommandWriter, ValueChangeCommand}, 4 | sound::{Sound, SoundData}, 5 | Decibels, PlaySoundError, Tween, Value, 6 | }; 7 | 8 | /// Controls the main mixer track. 9 | #[derive(Debug)] 10 | pub struct MainTrackHandle { 11 | pub(crate) set_volume_command_writer: CommandWriter>, 12 | pub(crate) sound_controller: ResourceController>, 13 | } 14 | 15 | impl MainTrackHandle { 16 | /// Plays a sound. 17 | pub fn play( 18 | &mut self, 19 | sound_data: D, 20 | ) -> Result> { 21 | let (sound, handle) = sound_data 22 | .into_sound() 23 | .map_err(PlaySoundError::IntoSoundError)?; 24 | self.sound_controller 25 | .insert(sound) 26 | .map_err(|_| PlaySoundError::SoundLimitReached)?; 27 | Ok(handle) 28 | } 29 | 30 | /// Sets the (post-effects) volume of the mixer track. 31 | pub fn set_volume(&mut self, volume: impl Into>, tween: Tween) { 32 | self.set_volume_command_writer.write(ValueChangeCommand { 33 | target: volume.into(), 34 | tween, 35 | }) 36 | } 37 | 38 | /// Returns the maximum number of sounds that can play simultaneously on this track. 39 | #[must_use] 40 | pub fn sound_capacity(&self) -> usize { 41 | self.sound_controller.capacity() 42 | } 43 | 44 | /// Returns the number of sounds currently playing on this track. 45 | #[must_use] 46 | pub fn num_sounds(&self) -> usize { 47 | self.sound_controller.len() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/kira/src/track/send.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod handle; 3 | 4 | use std::sync::Arc; 5 | 6 | use atomic_arena::Key; 7 | pub use builder::*; 8 | pub use handle::*; 9 | 10 | use crate::{ 11 | command::{CommandReader, ValueChangeCommand}, 12 | effect::Effect, 13 | info::Info, 14 | Decibels, Frame, Parameter, 15 | }; 16 | 17 | use super::TrackShared; 18 | 19 | /// A unique identifier for a mixer send track. 20 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 21 | pub struct SendTrackId(pub(crate) Key); 22 | 23 | impl From<&SendTrackHandle> for SendTrackId { 24 | fn from(handle: &SendTrackHandle) -> Self { 25 | handle.id() 26 | } 27 | } 28 | 29 | pub(crate) struct SendTrack { 30 | shared: Arc, 31 | volume: Parameter, 32 | set_volume_command_reader: CommandReader>, 33 | effects: Vec>, 34 | input: Vec, 35 | internal_buffer_size: usize, 36 | } 37 | 38 | impl SendTrack { 39 | pub fn init_effects(&mut self, sample_rate: u32) { 40 | for effect in &mut self.effects { 41 | effect.init(sample_rate, self.internal_buffer_size); 42 | } 43 | } 44 | 45 | pub fn on_change_sample_rate(&mut self, sample_rate: u32) { 46 | for effect in &mut self.effects { 47 | effect.on_change_sample_rate(sample_rate); 48 | } 49 | } 50 | 51 | #[must_use] 52 | pub fn shared(&self) -> Arc { 53 | self.shared.clone() 54 | } 55 | 56 | pub fn add_input(&mut self, input: &[Frame], volume: Decibels) { 57 | for (input, added) in self.input.iter_mut().zip(input.iter().copied()) { 58 | *input += added * volume.as_amplitude(); 59 | } 60 | } 61 | 62 | pub fn on_start_processing(&mut self) { 63 | self.volume 64 | .read_command(&mut self.set_volume_command_reader); 65 | for effect in &mut self.effects { 66 | effect.on_start_processing(); 67 | } 68 | } 69 | 70 | pub fn process(&mut self, out: &mut [Frame], dt: f64, info: &Info) { 71 | self.volume.update(dt * out.len() as f64, info); 72 | for (out_frame, input_frame) in out.iter_mut().zip(self.input.iter().copied()) { 73 | *out_frame += input_frame; 74 | } 75 | self.input.fill(Frame::ZERO); 76 | for effect in &mut self.effects { 77 | effect.process(out, dt, info); 78 | } 79 | let num_frames = out.len(); 80 | for (i, frame) in out.iter_mut().enumerate() { 81 | let time_in_chunk = (i + 1) as f64 / num_frames as f64; 82 | let volume = self.volume.interpolated_value(time_in_chunk).as_amplitude(); 83 | *frame *= volume; 84 | } 85 | } 86 | } 87 | 88 | pub(crate) struct SendTrackRoute { 89 | pub(crate) volume: Parameter, 90 | pub(crate) set_volume_command_reader: CommandReader>, 91 | } 92 | 93 | impl SendTrackRoute { 94 | pub fn read_commands(&mut self) { 95 | self.volume 96 | .read_command(&mut self.set_volume_command_reader); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/kira/src/track/send/handle.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{ 4 | command::{CommandWriter, ValueChangeCommand}, 5 | Tween, 6 | Decibels, Value, 7 | }; 8 | 9 | use super::{SendTrackId, TrackShared}; 10 | 11 | /// Controls a mixer track. 12 | /// 13 | /// When a [`SendTrackHandle`] is dropped, the corresponding mixer 14 | /// track will be removed. 15 | #[derive(Debug)] 16 | pub struct SendTrackHandle { 17 | pub(crate) id: SendTrackId, 18 | pub(crate) shared: Arc, 19 | pub(crate) set_volume_command_writer: CommandWriter>, 20 | } 21 | 22 | impl SendTrackHandle { 23 | /// Returns a unique identifier for this send track. 24 | pub fn id(&self) -> SendTrackId { 25 | self.id 26 | } 27 | 28 | /// Sets the (post-effects) volume of the send track. 29 | pub fn set_volume(&mut self, volume: impl Into>, tween: Tween) { 30 | self.set_volume_command_writer.write(ValueChangeCommand { 31 | target: volume.into(), 32 | tween, 33 | }) 34 | } 35 | } 36 | 37 | impl Drop for SendTrackHandle { 38 | fn drop(&mut self) { 39 | self.shared.mark_for_removal(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/kira/src/tween.rs: -------------------------------------------------------------------------------- 1 | //! Smooth interpolation between values. 2 | 3 | mod tweenable; 4 | 5 | pub use tweenable::*; 6 | 7 | use std::time::Duration; 8 | 9 | use crate::start_time::StartTime; 10 | 11 | /// Curves the motion of a [`Tween`]. 12 | #[derive(Debug, Clone, Copy, PartialEq)] 13 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 14 | pub enum Easing { 15 | /// Maintains a constant speed for the duration of the [`Tween`]. 16 | Linear, 17 | /// Causes the [`Tween`] to start slow and speed up. A higher 18 | /// value causes the [`Tween`] to speed up more dramatically. 19 | InPowi(i32), 20 | /// Causes the [`Tween`] to start fast and slow down. A higher 21 | /// value causes the [`Tween`] to slow down more dramatically. 22 | OutPowi(i32), 23 | /// Causes the [`Tween`] to start slow, speed up, and then slow 24 | /// back down. A higher values causes the [`Tween`] to have more 25 | /// dramatic speed changes. 26 | InOutPowi(i32), 27 | /// Causes the [`Tween`] to start slow and speed up. A higher 28 | /// value causes the [`Tween`] to speed up more dramatically. 29 | /// 30 | /// This is similar to [`InPowi`](Easing::InPowi), but allows 31 | /// for float intensity values at the cost of being more 32 | /// CPU intensive. 33 | InPowf(f64), 34 | /// Causes the [`Tween`] to start fast and slow down. A higher 35 | /// value causes the [`Tween`] to slow down more dramatically. 36 | /// 37 | /// This is similar to [`OutPowi`](Easing::InPowi), but allows 38 | /// for float intensity values at the cost of being more 39 | /// CPU intensive. 40 | OutPowf(f64), 41 | /// Causes the [`Tween`] to start slow, speed up, and then slow 42 | /// back down. A higher values causes the [`Tween`] to have more 43 | /// dramatic speed changes. 44 | /// 45 | /// This is similar to [`InOutPowi`](Easing::InPowi), but allows 46 | /// for float intensity values at the cost of being more 47 | /// CPU intensive. 48 | InOutPowf(f64), 49 | } 50 | 51 | impl Easing { 52 | pub(crate) fn apply(&self, mut x: f64) -> f64 { 53 | match self { 54 | Easing::Linear => x, 55 | Easing::InPowi(power) => x.powi(*power), 56 | Easing::OutPowi(power) => 1.0 - Self::InPowi(*power).apply(1.0 - x), 57 | Easing::InOutPowi(power) => { 58 | x *= 2.0; 59 | if x < 1.0 { 60 | 0.5 * Self::InPowi(*power).apply(x) 61 | } else { 62 | x = 2.0 - x; 63 | 0.5 * (1.0 - Self::InPowi(*power).apply(x)) + 0.5 64 | } 65 | } 66 | Easing::InPowf(power) => x.powf(*power), 67 | Easing::OutPowf(power) => 1.0 - Self::InPowf(*power).apply(1.0 - x), 68 | Easing::InOutPowf(power) => { 69 | x *= 2.0; 70 | if x < 1.0 { 71 | 0.5 * Self::InPowf(*power).apply(x) 72 | } else { 73 | x = 2.0 - x; 74 | 0.5 * (1.0 - Self::InPowf(*power).apply(x)) + 0.5 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | impl Default for Easing { 82 | fn default() -> Self { 83 | Self::Linear 84 | } 85 | } 86 | 87 | /// Describes a smooth transition between values. 88 | #[derive(Debug, Clone, Copy, PartialEq)] 89 | pub struct Tween { 90 | /// When the motion starts. 91 | pub start_time: StartTime, 92 | /// The duration of the motion. 93 | pub duration: Duration, 94 | /// The curve of the motion. 95 | pub easing: Easing, 96 | } 97 | 98 | impl Tween { 99 | pub(super) fn value(&self, time: f64) -> f64 { 100 | self.easing.apply(time / self.duration.as_secs_f64()) 101 | } 102 | } 103 | 104 | impl Default for Tween { 105 | fn default() -> Self { 106 | Self { 107 | start_time: StartTime::default(), 108 | duration: Duration::from_millis(10), 109 | easing: Easing::Linear, 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /crates/kira/src/tween/tweenable.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use glam::{Quat, Vec3}; 4 | 5 | /// A trait for types that can be smoothly interpolated. 6 | pub trait Tweenable: Copy { 7 | /// Returns an linearly interpolated value between `a` and `b`. 8 | /// 9 | /// An amount of `0.0` should yield `a`, an amount of `1.0` should 10 | /// yield `b`, and an amount of `0.5` should yield a value halfway 11 | /// between `a` and `b`. 12 | #[must_use] 13 | fn interpolate(a: Self, b: Self, amount: f64) -> Self; 14 | } 15 | 16 | impl Tweenable for f32 { 17 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 18 | a + (b - a) * amount as f32 19 | } 20 | } 21 | 22 | impl Tweenable for f64 { 23 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 24 | a + (b - a) * amount 25 | } 26 | } 27 | 28 | impl Tweenable for Vec3 { 29 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 30 | a + (b - a) * amount as f32 31 | } 32 | } 33 | 34 | impl Tweenable for Quat { 35 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 36 | a.slerp(b, amount as f32) 37 | } 38 | } 39 | 40 | impl Tweenable for Duration { 41 | fn interpolate(a: Self, b: Self, amount: f64) -> Self { 42 | let a_secs = a.as_secs_f64(); 43 | let b_secs = b.as_secs_f64(); 44 | Duration::from_secs_f64(a_secs + (b_secs - a_secs) * amount) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/kira/tests/change_sample_rate.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | atomic::{AtomicU32, Ordering}, 3 | Arc, 4 | }; 5 | 6 | use kira::{ 7 | backend::mock::{MockBackend, MockBackendSettings}, 8 | effect::{Effect, EffectBuilder}, 9 | info::Info, 10 | track::TrackBuilder, 11 | AudioManager, AudioManagerSettings, Frame, 12 | }; 13 | use rtrb::{Consumer, Producer, RingBuffer}; 14 | 15 | struct TestEffect { 16 | sample_rate: Arc, 17 | dt_producer: Producer, 18 | } 19 | 20 | impl Effect for TestEffect { 21 | fn init(&mut self, sample_rate: u32, _internal_buffer_size: usize) { 22 | self.sample_rate.store(sample_rate, Ordering::SeqCst); 23 | } 24 | 25 | fn on_change_sample_rate(&mut self, sample_rate: u32) { 26 | self.sample_rate.store(sample_rate, Ordering::SeqCst); 27 | } 28 | 29 | fn process(&mut self, _input: &mut [Frame], dt: f64, _info: &Info) { 30 | self.dt_producer.push(dt).unwrap(); 31 | } 32 | } 33 | 34 | struct TestEffectHandle { 35 | sample_rate: Arc, 36 | dt_consumer: Consumer, 37 | } 38 | 39 | struct TestEffectBuilder; 40 | 41 | impl EffectBuilder for TestEffectBuilder { 42 | type Handle = TestEffectHandle; 43 | 44 | fn build(self) -> (Box, Self::Handle) { 45 | let (dt_producer, dt_consumer) = RingBuffer::new(100); 46 | let sample_rate = Arc::new(AtomicU32::new(0)); 47 | ( 48 | Box::new(TestEffect { 49 | sample_rate: sample_rate.clone(), 50 | dt_producer, 51 | }), 52 | TestEffectHandle { 53 | sample_rate, 54 | dt_consumer, 55 | }, 56 | ) 57 | } 58 | } 59 | 60 | #[test] 61 | fn change_sample_rate() { 62 | let mut manager = AudioManager::::new(AudioManagerSettings { 63 | backend_settings: MockBackendSettings { sample_rate: 100 }, 64 | ..Default::default() 65 | }) 66 | .unwrap(); 67 | let mut effect_handle; 68 | let _track = manager 69 | .add_sub_track({ 70 | let mut builder = TrackBuilder::new(); 71 | effect_handle = builder.add_effect(TestEffectBuilder); 72 | builder 73 | }) 74 | .unwrap(); 75 | let backend = manager.backend_mut(); 76 | backend.on_start_processing(); 77 | assert_eq!(effect_handle.sample_rate.load(Ordering::SeqCst), 100); 78 | backend.process(); 79 | assert_eq!(effect_handle.dt_consumer.pop(), Ok(1.0 / 100.0)); 80 | backend.set_sample_rate(200); 81 | assert_eq!(effect_handle.sample_rate.load(Ordering::SeqCst), 200); 82 | backend.process(); 83 | assert_eq!(effect_handle.dt_consumer.pop(), Ok(1.0 / 200.0)); 84 | } 85 | -------------------------------------------------------------------------------- /crates/kira/tests/streaming_sound_stops_on_error.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use kira::{ 4 | backend::mock::MockBackend, 5 | sound::{ 6 | streaming::{Decoder, StreamingSoundData}, 7 | PlaybackState, 8 | }, 9 | AudioManager, AudioManagerSettings, Frame, 10 | }; 11 | 12 | struct MockDecoder; 13 | 14 | #[derive(Debug, PartialEq, Eq)] 15 | struct MockDecoderError; 16 | 17 | impl Decoder for MockDecoder { 18 | type Error = MockDecoderError; 19 | 20 | fn sample_rate(&self) -> u32 { 21 | 1 22 | } 23 | 24 | fn num_frames(&self) -> usize { 25 | 1 26 | } 27 | 28 | fn decode(&mut self) -> Result, Self::Error> { 29 | Err(MockDecoderError) 30 | } 31 | 32 | fn seek(&mut self, _index: usize) -> Result { 33 | Ok(0) 34 | } 35 | } 36 | 37 | #[test] 38 | fn streaming_sound_stops_on_error() { 39 | let mut manager = AudioManager::::new(AudioManagerSettings::default()).unwrap(); 40 | let data = StreamingSoundData::from_decoder(MockDecoder); 41 | let mut sound = manager.play(data).unwrap(); 42 | manager.backend_mut().on_start_processing(); 43 | std::thread::sleep(Duration::from_secs(1)); 44 | manager.backend_mut().process(); 45 | manager.backend_mut().on_start_processing(); 46 | assert_eq!(sound.state(), PlaybackState::Stopped); 47 | assert_eq!(sound.pop_error(), Some(MockDecoderError)); 48 | assert_eq!(manager.main_track().num_sounds(), 0); 49 | } 50 | -------------------------------------------------------------------------------- /crates/kira/tests/sync_send.rs: -------------------------------------------------------------------------------- 1 | use kira::{ 2 | backend::cpal::CpalBackend, 3 | clock::ClockHandle, 4 | effect::{ 5 | compressor::CompressorHandle, delay::DelayHandle, distortion::DistortionHandle, 6 | eq_filter::EqFilterHandle, filter::FilterHandle, panning_control::PanningControlHandle, 7 | reverb::ReverbHandle, volume_control::VolumeControlHandle, 8 | }, 9 | listener::ListenerHandle, 10 | modulator::{lfo::LfoHandle, tweener::TweenerHandle}, 11 | sound::{static_sound::StaticSoundHandle, streaming::StreamingSoundHandle, FromFileError}, 12 | track::{MainTrackHandle, SendTrackHandle, SpatialTrackHandle, TrackHandle}, 13 | AudioManager, 14 | }; 15 | 16 | fn main() { 17 | sync_send::>(); 18 | sync_send::(); 19 | sync_send::(); 20 | sync_send::(); 21 | sync_send::(); 22 | sync_send::(); 23 | sync_send::(); 24 | sync_send::(); 25 | sync_send::(); 26 | sync_send::(); 27 | sync_send::(); 28 | sync_send::(); 29 | sync_send::(); 30 | sync_send::(); 31 | sync_send::(); 32 | sync_send::(); 33 | sync_send::(); 34 | sync_send::(); 35 | sync_send::>(); 36 | } 37 | 38 | fn sync_send() {} 39 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | v0.8 todo: 2 | 3 | - Consider sealing `IntoOptionalRegion` 4 | - Tweak spatial sound defaults 5 | 6 | Future: 7 | 8 | - Plugin system? 9 | - Spatial audio: 10 | - Doppler effect 11 | - Link emitter distance to effects 12 | --------------------------------------------------------------------------------