embassy_agb/
sound.rs

1//! Sound mixing support for Game Boy Advance
2//!
3//! This module provides async-friendly wrappers around the agb sound mixer,
4//! allowing you to play up to 8 simultaneous sound channels with various
5//! frequencies and effects.
6//!
7//! # Usage
8//!
9//! 1. Create a mixer with [`InitializedGba::split()`](crate::InitializedGba::split)
10//! 2. Load sound data using [`include_wav!`](agb::include_wav)
11//! 3. Play sounds with [`AsyncMixer::play_sound()`]
12//! 4. Call [`AsyncMixer::frame()`] once per frame before VBlank
13//!
14//! # Example (Convenient API)
15//!
16//! ```rust,no_run
17//! use agb::sound::mixer::{Frequency, SoundChannel};
18//! use agb::include_wav;
19//! use embassy_agb::Spawner;
20//!
21//! static JUMP_SOUND: agb::sound::mixer::SoundData = include_wav!("sfx/jump.wav");
22//!
23//! #[embassy_agb::main]
24//! async fn main(_spawner: Spawner) -> ! {
25//!     let mut gba = embassy_agb::init(Default::default());
26//!     let mut peripherals = gba.peripherals(Frequency::Hz10512);
27//!
28//!     loop {
29//!         if peripherals.input.is_just_pressed_polling(agb::input::Button::A) {
30//!             let channel = SoundChannel::new(JUMP_SOUND);
31//!             peripherals.mixer.play_sound(channel);
32//!         }
33//!
34//!         // Automatically handles input.update(), mixer.frame(), and wait_for_vblank()
35//!         peripherals.wait_frame().await;
36//!     }
37//! }
38//! ```
39//!
40//! # Example (Manual Control)
41//!
42//! For more control over the frame timing, you can use the split API:
43//!
44//! ```rust,no_run
45//! # use agb::sound::mixer::{Frequency, SoundChannel};
46//! # use agb::include_wav;
47//! # use embassy_agb::Spawner;
48//! # static JUMP_SOUND: agb::sound::mixer::SoundData = include_wav!("sfx/jump.wav");
49//! #[embassy_agb::main]
50//! async fn main(_spawner: Spawner) -> ! {
51//!     let mut gba = embassy_agb::init(Default::default());
52//!     let (mut mixer, display, mut input) = gba.split(Frequency::Hz10512);
53//!
54//!     loop {
55//!         input.update();
56//!         
57//!         if input.is_just_pressed_polling(agb::input::Button::A) {
58//!             let channel = SoundChannel::new(JUMP_SOUND);
59//!             mixer.play_sound(channel);
60//!         }
61//!
62//!         mixer.frame(); // Must call once per frame!
63//!         display.wait_for_vblank().await;
64//!     }
65//! }
66//! ```
67
68use agb::sound::mixer::{Frequency, MixerController, SoundChannel};
69
70/// Error type for sound operations
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct SoundError;
73
74impl core::fmt::Display for SoundError {
75    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
76        write!(f, "Sound operation failed")
77    }
78}
79
80/// Async-friendly wrapper for the agb sound mixer
81///
82/// The mixer supports up to 8 simultaneous sound channels and can play
83/// both mono and stereo sounds at various sample rates.
84///
85/// ## Important: Frame Processing
86///
87/// You **must** call [`frame()`](AsyncMixer::frame) exactly once per frame
88/// (60Hz) for proper sound playback. Call it just before waiting for VBlank.
89///
90/// ## Sound Priorities
91///
92/// - **High priority**: Use [`SoundChannel::new_high_priority()`](agb::sound::mixer::SoundChannel::new_high_priority)
93///   for background music or critical sounds that must always play
94/// - **Low priority**: Use [`SoundChannel::new()`](agb::sound::mixer::SoundChannel::new)
95///   for sound effects that can be interrupted
96///
97/// ## Frequencies
98///
99/// Choose a frequency based on quality vs performance:
100/// - [`Frequency::Hz10512`](agb::sound::mixer::Frequency::Hz10512) - Good quality, low CPU usage (recommended)
101/// - [`Frequency::Hz18157`](agb::sound::mixer::Frequency::Hz18157) - Better quality, medium CPU usage
102/// - [`Frequency::Hz32768`](agb::sound::mixer::Frequency::Hz32768) - Best quality, high CPU usage
103///
104/// WAV files must be converted to match the chosen frequency.
105pub struct AsyncMixer<'a> {
106    mixer: agb::sound::mixer::Mixer<'a>,
107}
108
109impl<'a> AsyncMixer<'a> {
110    pub(crate) fn new(mixer_controller: &'a mut MixerController, frequency: Frequency) -> Self {
111        let mixer = mixer_controller.mixer(frequency);
112        Self { mixer }
113    }
114
115    /// Process one frame of audio
116    ///
117    /// **IMPORTANT**: This must be called exactly once per frame (60Hz) for proper sound playback.
118    /// Call this just before waiting for VBlank.
119    ///
120    /// Skipping frames will cause audio glitches and crackling. Calling it more than once
121    /// per frame is harmless but wastes CPU cycles.
122    pub fn frame(&mut self) {
123        self.mixer.frame();
124    }
125
126    /// Play a sound and return its channel ID
127    ///
128    /// Returns `Ok(channel_id)` if the sound starts playing, or `Err(SoundError)`
129    /// if all channels are busy and the sound has low priority.
130    pub fn play_sound(
131        &mut self,
132        channel: SoundChannel,
133    ) -> Result<agb::sound::mixer::ChannelId, SoundError> {
134        self.mixer.play_sound(channel).ok_or(SoundError)
135    }
136
137    /// Get a reference to a playing channel
138    ///
139    /// Returns `Some(&mut channel)` if the channel is still playing, or `None`
140    /// if it has finished or been replaced.
141    pub fn channel(
142        &mut self,
143        id: &agb::sound::mixer::ChannelId,
144    ) -> Option<&mut agb::sound::mixer::SoundChannel> {
145        self.mixer.channel(id)
146    }
147
148    /// Get access to the underlying mixer for synchronous operations
149    pub fn mixer(&mut self) -> &mut agb::sound::mixer::Mixer<'a> {
150        &mut self.mixer
151    }
152}