Introduction

Kira is a backend-agnostic library to create expressive audio for games. It provides parameters for smoothly adjusting properties of sounds, a flexible mixer for applying effects to audio, and a clock system for precisely timing audio events.

Examples

Playing a sound multiple times simultaneously

#![allow(unused)]
fn main() {
extern crate kira;

use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
};

// Create an audio manager. This plays sounds and manages resources.
let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let sound_data = StaticSoundData::from_file("sound.ogg", StaticSoundSettings::default())?;
manager.play(sound_data.clone())?;
// After a couple seconds...
manager.play(sound_data.clone())?;
// Cloning the sound data will not use any extra memory.
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Gradually speeding up a sound over time

#![allow(unused)]
fn main() {
extern crate kira;

use std::time::Duration;

use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	tween::Tween,
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let sound_data = StaticSoundData::from_file("sound.ogg", StaticSoundSettings::new())?;
let mut sound = manager.play(sound_data)?;
// Start smoothly adjusting the playback rate parameter.
sound.set_playback_rate(
	2.0,
	Tween {
		duration: Duration::from_secs(3),
		..Default::default()
	},
);
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Playing a sound with a low-pass filter applied

This makes the audio sound muffled.

#![allow(unused)]
fn main() {
extern crate kira;

use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	track::{
		TrackBuilder,
		effect::filter::FilterBuilder,
	},
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
// Create a mixer sub-track with a filter.
let track = manager.add_sub_track({
	let mut builder = TrackBuilder::new();
	builder.add_effect(FilterBuilder::new().cutoff(1000.0));
	builder
})?;
// Play the sound on the track.
let sound_data = StaticSoundData::from_file(
	"sound.ogg",
	StaticSoundSettings::new().track(&track),
)?;
manager.play(sound_data)?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Playing sounds in time with a musical beat

#![allow(unused)]
fn main() {
extern crate kira;

use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	ClockSpeed,
};

const TEMPO: f64 = 120.0;

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
// Create a clock that ticks 120 times per second. In this case,
// each tick is one musical beat. We can use a tick to represent any
// arbitrary amount of time.
let mut clock = manager.add_clock(ClockSpeed::TicksPerMinute(TEMPO))?;
// Play a sound 2 ticks (beats) from now.
let sound_data_1 = StaticSoundData::from_file(
	"sound1.ogg",
	StaticSoundSettings::new().start_time(clock.time() + 2),
)?;
manager.play(sound_data_1)?;
// Play a different sound 4 ticks (beats) from now.
let sound_data_2 = StaticSoundData::from_file(
	"sound2.ogg",
	StaticSoundSettings::new().start_time(clock.time() + 4),
)?;
manager.play(sound_data_2)?;
// Start the clock.
clock.start()?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Installation

To use Kira in your project, add it to the Cargo.toml file for your crate in the dependencies section.

[dependencies]
kira = "0.7.1"

Features

By default, Kira comes with a cpal backend for communicating with the operating system's audio drivers and support for mp3, ogg, flac, and wav files. You can manually pick which of these features to enable by setting default-features to false and listing the specific features you want.

For example, if you're only going to use ogg files, you can disable the other file types to save some compile time and binary size:

[dependencies]
kira = { version = "0.7.1", default-features = false, features = ["cpal", "ogg"] }

Performance

By default, Rust programs run with the dev profile are not optimized. This can lead to poor performance of audio playback and long loading times for audio files. You can alleviate this by building Kira and its audio-related dependencies with a higher optimization level. Add the following to your Cargo.toml:

[profile.dev.package.kira]
opt-level = 3

[profile.dev.package.cpal]
opt-level = 3

[profile.dev.package.symphonia]
opt-level = 3

[profile.dev.package.symphonia-bundle-mp3]
opt-level = 3

[profile.dev.package.symphonia-format-ogg]
opt-level = 3

[profile.dev.package.symphonia-codec-vorbis]
opt-level = 3

[profile.dev.package.symphonia-bundle-flac]
opt-level = 3

[profile.dev.package.symphonia-format-wav]
opt-level = 3

[profile.dev.package.symphonia-codec-pcm]
opt-level = 3

You can also build all of your projects with a higher optimization level by using this snippet instead:

[profile.dev.package."*"]
opt-level = 3

Building dependencies with a higher optimization level does increase compile times, but only when compiling your project from scratch. If you only make changes to your crate, you're not recompiling the dependencies, so you don't suffer from the longer compilation step in that case. Building dependencies optimized and the main crate unoptimized can be a good balance of performance and compile times for games.

Playing Sounds

To start using Kira, create an AudioManager.

#![allow(unused)]
fn main() {
extern crate kira;
use kira::manager::{
	AudioManager, AudioManagerSettings,
	backend::cpal::CpalBackend,
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

The AudioManager allows you to interact with the audio context from gameplay code. AudioManagers can play anything that implements the SoundData trait, such as StaticSoundData or StreamingSoundData.

#![allow(unused)]
fn main() {
extern crate kira;
use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let sound_data = StaticSoundData::from_file("sound.ogg", StaticSoundSettings::new())?;
manager.play(sound_data)?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

If you want to play a sound multiple times, keep a copy of the StaticSoundData around and clone it each time you pass it to AudioManager::play.

#![allow(unused)]
fn main() {
extern crate kira;
use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let sound_data = StaticSoundData::from_file("sound.ogg", StaticSoundSettings::new())?;
manager.play(sound_data.clone())?;
manager.play(sound_data.clone())?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Cloning a StaticSoundData is cheap, so it's perfectly fine to do this.

StreamingSoundData cannot be cloned, so you will have to create a new one each time you want to play a sound.

Modifying playing sounds

AudioManager::play returns a handle to the sound that you can use to query information about the sound or modify it.

#![allow(unused)]
fn main() {
extern crate kira;
use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{PlaybackState, StaticSoundData, StaticSoundSettings},
	tween::Tween,
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let sound_data = StaticSoundData::from_file("sound.ogg", StaticSoundSettings::new())?;
let mut sound = manager.play(sound_data)?;
if sound.state() == PlaybackState::Playing {
	sound.stop(Tween::default())?;
}
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Many parameters of sounds, like volume and playback rate, can be smoothly transitioned to other values.

#![allow(unused)]
fn main() {
extern crate kira;
use std::time::Duration;

use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	tween::Tween,
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let sound_data = StaticSoundData::from_file("sound.ogg", StaticSoundSettings::new())?;
let mut sound = manager.play(sound_data)?;
sound.set_volume(
	0.5,
	Tween {
		duration: Duration::from_secs(2),
		..Default::default()
	},
)?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Some property setters allow you to set the value in different units. For example, volumes can be set in decibels:

#![allow(unused)]
fn main() {
extern crate kira;
use std::time::Duration;
use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	tween::Tween,
	Volume,
};
let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let sound_data = StaticSoundData::from_file("sound.ogg", StaticSoundSettings::new())?;
let mut sound = manager.play(sound_data)?;
sound.set_volume(
	Volume::Decibels(-3.0),
	Tween {
		duration: Duration::from_secs(2),
		..Default::default()
	},
)?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

If you want to change a property instantaneously, use the default Tween. It's fast enough to sound instantaneous, but slow enough to avoid audio artifacts.

The Mixer

Kira has an internal mixer which works like a real-life mixing console. Sounds can be played on "tracks", which are individual streams of audio that can optionally have effects that modify the audio.

Creating and using tracks

The mixer has a "main" track by default, and you can add any number of sub-tracks. To add a sub-track, use AudioManager::add_sub_track.

#![allow(unused)]
fn main() {
extern crate kira;
use std::error::Error;
use kira::{
    manager::{
        AudioManager, AudioManagerSettings,
        backend::cpal::CpalBackend,
    },
    track::TrackBuilder,
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let track = manager.add_sub_track(TrackBuilder::default())?;
Result::<(), Box<dyn Error>>::Ok(())
}

You can configure what track a sound will play on by modifying its settings. This example uses StaticSoundSettings, but StreamingSoundSettings provides the same option.

#![allow(unused)]
fn main() {
extern crate kira;
use std::error::Error;
use kira::{
	manager::{
        AudioManager, AudioManagerSettings,
        backend::cpal::CpalBackend,
    },
    sound::static_sound::{StaticSoundData, StaticSoundSettings},
	track::TrackBuilder,
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let track = manager.add_sub_track(TrackBuilder::default())?;
manager.play(StaticSoundData::from_file(
    "sound.ogg",
    StaticSoundSettings::new().track(&track),
)?)?;
Result::<(), Box<dyn Error>>::Ok(())
}

You can set the volume and panning of a track using TrackHandle::set_volume and TrackHandle::set_panning, respectively. The volume and panning settings will affect all sounds being played on the track.

Effects

You can add effects to the track when creating it using TrackBuilder::add_effect. All sounds that are played on that track will have the effects applied sequentially.

In this example, we'll use the Filter effect, which in the low pass mode will remove high frequencies from sounds, making them sound muffled.

#![allow(unused)]
fn main() {
extern crate kira;
use std::error::Error;
use kira::{
	manager::{
        AudioManager, AudioManagerSettings,
        backend::cpal::CpalBackend,
    },
    sound::static_sound::{StaticSoundData, StaticSoundSettings},
	track::{
        TrackBuilder,
        effect::filter::FilterBuilder,
    },
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let track = manager.add_sub_track({
    let mut builder = TrackBuilder::new();
    builder.add_effect(FilterBuilder::new().cutoff(1000.0));
    builder
})?;
manager.play(StaticSoundData::from_file(
    "sound.ogg",
    StaticSoundSettings::new().track(&track),
)?)?;
Result::<(), Box<dyn Error>>::Ok(())
}

TrackBuilder:add_effect returns a handle that can be used to modify the effect after the track has been created.

#![allow(unused)]
fn main() {
extern crate kira;
use kira::{
	manager::{
    AudioManager, AudioManagerSettings,
    backend::cpal::CpalBackend,
  },
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	track::{effect::filter::FilterBuilder, TrackBuilder},
	tween::Tween,
};
let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let mut filter;
let track = manager.add_sub_track({
	let mut builder = TrackBuilder::new();
	filter = builder.add_effect(FilterBuilder::new().cutoff(1000.0));
	builder
})?;
filter.set_cutoff(4000.0, Tween::default())?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Track routing

By default, the output of all sub-tracks will be fed into the input of the main mixer track without any volume change. It can be useful to customize this behavior.

Let's say we want to be able to control the volume level of gameplay sounds separately from music. We may also want to apply effects to gameplay sounds that come from the player specifically.

We'll end up with a hierarchy like this:

       ┌──────────┐
       │Main track│
       └─▲──────▲─┘
         │      │
         │      │
    ┌────┴─┐   ┌┴────┐
    │Sounds│   │Music│
    └──▲───┘   └─────┘
       │
┌──────┴──────┐
│Player sounds│
└─────────────┘

We can set up the sounds and player_sounds hierarchy using TrackRoutes.

#![allow(unused)]
fn main() {
extern crate kira;
use std::error::Error;
use kira::{
	manager::{
        AudioManager, AudioManagerSettings,
        backend::cpal::CpalBackend,
    },
	track::{TrackRoutes, TrackBuilder},
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let sounds = manager.add_sub_track(TrackBuilder::default())?;
let player_sounds = manager.add_sub_track(
	TrackBuilder::new().routes(TrackRoutes::parent(&sounds)),
)?;
Result::<(), Box<dyn Error>>::Ok(())
}

The default TrackRoutes has a single route to the main mixer track. TrackRoutes::parent will instead create a single route to the track of your choosing.

You can also have one track feed its audio into multiple other tracks. This can be useful for sharing effects between tracks.

For example, let's say we have our sounds split up into player sounds and ambience. This game takes place in a vast cave, so we want all of the sounds to have a reverb effect. We want the ambience to have more reverb than the player sounds so that it feels farther away.

We could put separate reverb effects on both the player and ambience tracks. Since both the player and the ambient sounds are in the same cave, we'll use the same settings for both reverb effects, but we'll increase the mix setting for the ambience, since ambient sounds are supposed to have more reverb. This has some downsides, however:

  • Since most of the settings are supposed to be the same between the two tracks, if we want to change the reverb settings, we have to change them in two different places.
  • We have two separate reverb effects running, which has a higher CPU cost than if we just had one.

A better alternative would be to make a separate reverb track that both the player and ambience tracks are routed to.

        ┌──────────┐
   ┌────►Main track◄───────┐
   │    └─▲────────┘       │
   │      │                │
   │      │            ┌───┴──┐
   │ ┌────┼────────────►Reverb│
   │ │    │            └──▲───┘
   │ │    │               │
   │ │    │               │
┌──┴─┴─┐  │   ┌────────┐  │
│Player│  └───┤Ambience├──┘
└──────┘      └────────┘

Here's what this looks like in practice:

#![allow(unused)]
fn main() {
extern crate kira;
use std::error::Error;
use kira::{
	manager::{
        AudioManager, AudioManagerSettings,
        backend::cpal::CpalBackend,
    },
	track::{
        TrackRoutes, TrackBuilder,
        effect::reverb::ReverbBuilder,
    },
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let reverb = manager.add_sub_track({
    let mut builder = TrackBuilder::new();
    builder.add_effect(ReverbBuilder::new().mix(1.0));
    builder
})?;
let player = manager.add_sub_track(
    TrackBuilder::new().routes(TrackRoutes::new().with_route(&reverb, 0.25)),
);
let ambience = manager.add_sub_track(
    TrackBuilder::new().routes(TrackRoutes::new().with_route(&reverb, 0.5)),
);
Result::<(), Box<dyn Error>>::Ok(())
}

Let's look at this one step at a time:

let reverb = manager.add_sub_track({
    let mut builder = TrackBuilder::new();
    builder.add_effect(ReverbBuilder::new().mix(1.0));
    builder
})?;

We create the reverb track with a Reverb effect. We set the mix to 1.0 so that only the reverb signal is output from this track. We don't need any of the dry signal to come out of this track, since the player and ambience tracks will already be outputting their dry signal to the main track.

let player = manager.add_sub_track(
    TrackBuilder::new().routes(TrackRoutes::new().with_route(&reverb, 0.25)),
);

We create the player track with two routes:

  • The route to the main track with 100% volume. We don't have to set this one explicitly because TrackRoutes::new() adds that route by default.
  • The route to the reverb track with 25% volume.
let ambience = manager.add_sub_track(
    TrackBuilder::new().routes(TrackRoutes::new().with_route(&reverb, 0.5)),
);

The ambience track is set up the same way, except the route to the reverb track has 50% volume, giving us more reverb for these sounds.

Clocks

Creating clocks

Clocks can be used to set the start times of sounds and tweens. To create a clock, use AudioManager::add_clock.

#![allow(unused)]
fn main() {
extern crate kira;
use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	ClockSpeed,
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
clock.start()?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

You can specify the speed of the clock as seconds per tick, ticks per second, or ticks per minute.

Clocks are stopped when you first create them, so be sure to explicitly call ClockHandle::start when you want the clock to start ticking.

Starting sounds on clock ticks

Static sounds (and streaming sounds from the kira-streaming crate) can be set to only start playing when a clock has ticked a certain number of times. You can configure this using StaticSoundSettings::start_time.

#![allow(unused)]
fn main() {
extern crate kira;
use kira::{
	clock::ClockTime,
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	StartTime, ClockSpeed,
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
manager.play(StaticSoundData::from_file(
	"sound.ogg",
	StaticSoundSettings::new().start_time(StartTime::ClockTime(ClockTime {
		clock: clock.id(),
		ticks: 4,
	})),
)?)?;
clock.start()?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

As a shorthand, you can pass the ClockTime directly into StaticSoundSettings::start_time.

#![allow(unused)]
fn main() {
extern crate kira;
use kira::{
	clock::ClockTime,
	manager::{
	 	AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	ClockSpeed, StartTime,
};

let mut manager = AudioManager::<CpalBackend>::new(
	AudioManagerSettings::default(),
)?;
let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
manager.play(StaticSoundData::from_file(
	"sound.ogg",
	StaticSoundSettings::new().start_time(ClockTime {
		clock: clock.id(),
		ticks: 4,
	}),
)?)?;
clock.start()?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

As an even shorter hand, you can use ClockHandle::time to get the clock's current ClockTime, and then add to it to get a time in the future:

#![allow(unused)]
fn main() {
extern crate kira;
use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	ClockSpeed,
};

let mut manager = AudioManager::<CpalBackend>::new(
	AudioManagerSettings::default(),
)?;
let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
manager.play(StaticSoundData::from_file(
	"sound.ogg",
	StaticSoundSettings::new().start_time(clock.time() + 4),
)?)?;
clock.start()?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Starting tweens on clock ticks

You can also use clocks to set the start time of tweens. In this example, we set the playback rate of a sound to start tweening when a clock reaches its third tick.

#![allow(unused)]
fn main() {
extern crate kira;
use std::time::Duration;

use kira::{
	manager::{
		AudioManager, AudioManagerSettings,
		backend::cpal::CpalBackend,
	},
	sound::static_sound::{StaticSoundData, StaticSoundSettings},
	tween::Tween,
	ClockSpeed, StartTime,
};

let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
let mut clock = manager.add_clock(ClockSpeed::SecondsPerTick(1.0))?;
let mut sound = manager.play(StaticSoundData::from_file(
	"sound.ogg",
	StaticSoundSettings::default(),
)?)?;
sound.set_playback_rate(
	0.5,
	Tween {
		start_time: StartTime::ClockTime(clock.time() + 3),
		duration: Duration::from_secs(2),
		..Default::default()
	},
)?;
clock.start()?;
Result::<(), Box<dyn std::error::Error>>::Ok(())
}

Creating Sound Implementations

Sounds in Kira have two phases:

  1. The SoundData phase: the user has created a sound, but it is not yet producing sound on the audio thread. If the sound data has settings, theyshould still be customizable at this point.
  2. The Sound phase: the user has played the sound using AudioManager::play, which transfers ownership to the audio thread.

The SoundData trait has the into_sound function, which "splits" the sound data into the live Sound and a Handle which the user can use to control the sound from gameplay code.

Sounds simply produce a Frame of audio each time process is called. A Sound can be a finite chunk of audio, an infinite stream of audio (e.g. voice chat), or anything else.

Kira does not provide any tools for passing messages from gameplay code to a Sound or vice versa. (Internally, Kira uses the ringbuf crate for this purpose.)