Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Quiet-Wolfe/Rustic-Engine/llms.txt
Use this file to discover all available pages before exploring further.
The rustic-audio crate wraps kira to provide all audio playback for Rustic Engine. It manages up to three simultaneous song tracks (instrumental, player vocals, opponent vocals), miss sound effects, and one-shot sound effects. It exposes a single playback position that the conductor reads each frame.
AudioEngine struct
crates/rustic-audio/src/lib.rs
pub struct AudioEngine {
manager: AudioManager,
inst: Option<StaticSoundHandle>,
vocals_player: Option<StaticSoundHandle>,
vocals_opponent: Option<StaticSoundHandle>,
playing: bool,
miss_sounds: Vec<StaticSoundData>,
miss_index: usize,
loop_music: Option<StaticSoundHandle>,
}
Create one AudioEngine per gameplay session:
let mut audio = AudioEngine::new();
Loading song tracks
Load tracks before starting playback. Each track is paused immediately after loading — you start them together with play():
audio.load_inst(Path::new("assets/songs/bopeebo/Inst.ogg"));
audio.load_vocals(Path::new("assets/songs/bopeebo/Voices.ogg"));
audio.load_opponent_vocals(Path::new("assets/songs/bopeebo/Voices-opponent.ogg"));
load_vocals() and load_opponent_vocals() silently skip loading if the file does not exist, so you do not need to guard these calls behind a needs_voices check — the engine handles it gracefully.
All audio files must be in OGG Vorbis format. kira’s StaticSoundData::from_file handles decoding.
Playback control
audio.play(); // resume all loaded tracks simultaneously
audio.pause(); // pause all tracks simultaneously
All tracks start and pause together, keeping them in sync. If only the instrumental is loaded (when needs_voices = false), the vocal handles are simply absent and the calls are no-ops.
Conductor synchronization
The instrumental track is the authoritative time source:
pub fn position_ms(&self) -> f64 {
self.inst
.as_ref()
.map(|h| h.position() * 1000.0)
.unwrap_or(0.0)
}
Each frame, write this value to conductor.song_position:
conductor.song_position = audio.position_ms();
This ensures all beat/step/section callbacks derive from the actual audio playback position rather than accumulated frame time, which would drift over the course of a song.
Vocals muting
Player vocals are muted on note miss and unmuted on a successful hit, matching Psych Engine’s behavior:
audio.mute_player_vocals(); // set player vocals volume to 0.0
audio.unmute_player_vocals(); // set player vocals volume to 1.0
The opponent vocals track is never muted — the opponent always “sings” regardless of player input.
Miss sounds
Load the three Psych Engine miss sound effects from a directory:
audio.load_miss_sounds(Path::new("assets/sounds"));
// Loads missnote1.ogg, missnote2.ogg, missnote3.ogg
Play a miss sound on each note miss. The engine cycles through the three sounds in order:
One-shot sound effects
Play any OGG file as a one-shot effect at a given volume:
audio.play_sound(Path::new("assets/sounds/countdown3.ogg"), 1.0);
The file is decoded and played immediately. If the path does not exist the call is silently ignored.
Looping background music
For screens that need looping music (such as the game-over screen), use the loop music API:
// Play at full volume
audio.play_loop_music(Path::new("assets/music/gameOver.ogg"));
// Play at a specific volume
audio.play_loop_music_vol(Path::new("assets/music/gameOver.ogg"), 0.8);
// Stop the loop
audio.stop_loop_music();
play_loop_music() stops any currently playing loop before starting the new one, so you do not need to call stop_loop_music() explicitly before switching tracks.
Seeking
Seek all tracks to a position simultaneously:
All three tracks (instrumental, player vocals, opponent vocals) are seeked to the same millisecond position, keeping them in sync after a manual time jump.
Playback state
audio.is_playing() -> bool // true between play() and pause()
audio.is_finished() -> bool // true when the instrumental has reached its end
Use is_finished() in the game loop to detect song end and transition to the results screen.
Integration example
The following shows the typical per-frame update pattern inside the gameplay loop:
// 1. Update conductor from audio position
conductor.song_position = audio.position_ms();
// 2. Check beat transitions
let new_beat = conductor.cur_beat();
if new_beat > last_beat {
last_beat = new_beat;
game_camera.bump_zoom(0.03);
hud_camera.bump_zoom(0.05);
}
// 3. Check for song end
if audio.is_finished() {
transition_to_results();
}