diff --git a/libretrogd/src/audio/buffer/mod.rs b/libretrogd/src/audio/buffer/mod.rs index b4498a8..b8adf9d 100644 --- a/libretrogd/src/audio/buffer/mod.rs +++ b/libretrogd/src/audio/buffer/mod.rs @@ -10,6 +10,7 @@ pub enum AudioBufferError { ConversionError(String), } +/// Holds audio sample data that can be played via [`AudioDevice`]. #[derive(Debug, Clone)] pub struct AudioBuffer { spec: AudioSpec, @@ -17,6 +18,8 @@ pub struct AudioBuffer { } impl AudioBuffer { + /// Creates and returns a new, empty, [`AudioBuffer`] that will hold audio sample data in the + /// spec/format given. pub fn new(spec: AudioSpec) -> Self { AudioBuffer { spec, @@ -24,11 +27,14 @@ impl AudioBuffer { } } + /// Returns the spec of the audio sample data that this buffer contains. #[inline] pub fn spec(&self) -> &AudioSpec { &self.spec } + /// Converts the audio sample data in this buffer to the spec given, returning the newly + /// converted buffer. pub fn convert(self, to_spec: &AudioSpec) -> Result { if self.spec == *to_spec { Ok(self) diff --git a/libretrogd/src/audio/buffer/wav.rs b/libretrogd/src/audio/buffer/wav.rs index c2066cb..9af9718 100644 --- a/libretrogd/src/audio/buffer/wav.rs +++ b/libretrogd/src/audio/buffer/wav.rs @@ -192,6 +192,8 @@ impl DataChunk { } impl AudioBuffer { + /// Loads the bytes of a WAV file into an [`AudioBuffer`]. The returned buffer will be in its + /// original format and may need to be converted before it can be played. pub fn load_wav_bytes(reader: &mut T) -> Result { let file_size = reader.stream_size()?; @@ -285,6 +287,8 @@ impl AudioBuffer { Ok(audio_buffer) } + /// Loads a WAV file into an [`AudioBuffer`]. The returned buffer will be in its original + /// format and may need to be converted before it can be played. pub fn load_wav_file(path: &Path) -> Result { let f = File::open(path)?; let mut reader = BufReader::new(f); diff --git a/libretrogd/src/audio/device.rs b/libretrogd/src/audio/device.rs index a1f531e..6abd306 100644 --- a/libretrogd/src/audio/device.rs +++ b/libretrogd/src/audio/device.rs @@ -5,12 +5,27 @@ use thiserror::Error; use crate::audio::*; +/// Represents a "channel" of audio playback that will be mixed together with all of the other +/// actively playing audio channels to get the final audio playback. pub struct AudioChannel { + /// Whether the channel is currently playing or not. pub playing: bool, + /// Whether this channel is playing on a loop or not. If not, once the end of the [`data`] + /// buffer is reached, or the [`AudioGenerator::gen_sample`] method returns `None`, playback + /// on this channel will automatically stop and [`playing`] will be changed to `false`. pub loops: bool, + /// The audio data buffer (samples) that this channel will play from, **only** if [`generator`] + /// is `None`. pub data: Vec, + /// An [`AudioGenerator`] instance that will be used to dynamically generate audio data to play + /// on this channel _instead of_ playing from [`data`]. Set this to `None` to play from audio + /// data in [`data`] instead. pub generator: Option>, + /// The volume level to play this channel at. 1.0 is "normal", 0.0 is completely silent. pub volume: f32, + /// The current playback position (index). 0 is the start of playback. The end position is + /// either the (current) size of the [`data`] buffer or dependant on the implementation of this + /// channel's current [`generator`] if not `None`. pub position: usize, } @@ -26,6 +41,7 @@ impl AudioChannel { } } + /// Returns the audio sample for the given position, or `None` if that position is invalid. #[inline] fn data_at(&mut self, position: usize) -> Option { if let Some(generator) = &mut self.generator { @@ -79,6 +95,8 @@ impl AudioChannel { } } + /// Resets the audio channel to a "blank slate", clearing the audio buffer, setting no current + /// audio generator, and turning playback off. #[inline] pub fn reset(&mut self) { self.data.clear(); @@ -87,6 +105,9 @@ impl AudioChannel { self.playing = false; } + /// Copies the data from the given audio buffer into this channel's buffer (clearing it first, + /// and extending the size of the buffer if necessary) and then begins playback from position 0. + /// This also sets the associated [`generator`] to `None`. #[inline] pub fn play_buffer(&mut self, buffer: &AudioBuffer, loops: bool) { self.data.clear(); @@ -97,6 +118,8 @@ impl AudioChannel { self.loops = loops; } + /// Begins playback on this channel from the given [`AudioGenerator`] instance from position 0. + /// This also clears the existing audio buffer contents. #[inline] pub fn play_generator(&mut self, generator: Box, loops: bool) { self.data.clear(); @@ -106,11 +129,15 @@ impl AudioChannel { self.loops = loops; } + /// Returns true if this channel has something that can be played back currently. #[inline] pub fn is_playable(&self) -> bool { !self.data.is_empty() || self.generator.is_some() } + /// Begins playback on this channel, only if playback is currently possible with its current + /// state (if it has some sample data in the buffer or if an [`AudioGenerator`] is set). + /// Resets the position to 0 if playback is started and returns true, otherwise returns false. #[inline] pub fn play(&mut self, loops: bool) -> bool { if self.is_playable() { @@ -123,6 +150,7 @@ impl AudioChannel { } } + /// Stops playback on this channel. #[inline] pub fn stop(&mut self) { self.playing = false; @@ -140,12 +168,17 @@ pub enum AudioDeviceError { ChannelIndexOutOfRange(usize), } +/// Represents the audio device and performs mixing of all of the [`AudioChannel`]s that are +/// currently playing. You should not be creating this manually, but obtaining it as needed via +/// [`Audio::lock`]. pub struct AudioDevice { spec: AudioSpec, channels: Vec, pub volume: f32, } +/// SDL audio callback implementation which performs audio mixing, generating the final sample data +/// that will be played by the system's audio device. impl AudioCallback for AudioDevice { type Channel = u8; @@ -164,6 +197,7 @@ impl AudioCallback for AudioDevice { } impl AudioDevice { + /// Creates a new [`AudioDevice`] instance, using the given spec as its playback format. pub fn new(spec: AudioSpec) -> Self { let mut channels = Vec::new(); for _ in 0..NUM_CHANNELS { @@ -176,16 +210,21 @@ impl AudioDevice { } } + /// Returns the spec that this device is currently set to play. All audio to be played via + /// this device must be pre-converted to match this spec! #[inline] pub fn spec(&self) -> &AudioSpec { &self.spec } + /// Returns true if any of the audio channels are currently playing, false otherwise. #[inline] pub fn is_playing(&self) -> bool { self.channels.iter().any(|channel| channel.playing) } + /// Stops the specified channel's playback, or does nothing if that channel was not currently + /// playing. This does not affect the channel's other state (data buffer, etc). pub fn stop_channel(&mut self, channel_index: usize) -> Result<(), AudioDeviceError> { if channel_index >= NUM_CHANNELS { Err(AudioDeviceError::ChannelIndexOutOfRange(channel_index)) @@ -195,12 +234,17 @@ impl AudioDevice { } } + /// Stops playback of all channels. pub fn stop_all(&mut self) { for channel in self.channels.iter_mut() { channel.stop(); } } + /// Tries to play the given [`AudioBuffer`] on the first channel found that is not already + /// playing. If a free channel is found, playback will be started by copying the buffer's + /// contents to the channel. The index of the channel is returned. If playback was not started + /// because no channel is free currently, then `None` is returned. pub fn play_buffer( &mut self, buffer: &AudioBuffer, @@ -218,6 +262,8 @@ impl AudioDevice { } } + /// Plays the given [`AudioBuffer`] on the specified channel. Whatever that channel was playing + /// will be interrupted and replaced with a copy of the given buffer's data. pub fn play_buffer_on_channel( &mut self, channel_index: usize, @@ -234,6 +280,10 @@ impl AudioDevice { } } + /// Tries to play the given [`AudioGenerator`] on the first channel found that is not already + /// playing. If a free channel is found, playback will be started and the index of the channel + /// will be returned. If playback was not started because no channel is free currently, then + /// `None` is returned. pub fn play_generator( &mut self, generator: Box, @@ -247,6 +297,8 @@ impl AudioDevice { } } + /// Plays the given [`AudioGenerator`] on the specified channel. Whatever that channel was + /// playing will be interrupted and replaced. pub fn play_generator_on_channel( &mut self, channel_index: usize, @@ -261,41 +313,51 @@ impl AudioDevice { } } + /// Returns an iterator of any [`AudioChannel`]s that are currently playing. #[inline] pub fn playing_channels_iter(&mut self) -> impl Iterator { self.channels.iter().filter(|channel| channel.playing) } + /// Returns an iterator of mutable [`AudioChannel`]s that are currently playing. #[inline] pub fn playing_channels_iter_mut(&mut self) -> impl Iterator { self.channels.iter_mut().filter(|channel| channel.playing) } + /// Returns an iterator of [`AudioChannel`]s that are not currently playing. #[inline] pub fn stopped_channels_iter(&mut self) -> impl Iterator { self.channels.iter().filter(|channel| !channel.playing) } + /// Returns an iterator of mutable [`AudioChannel`]s that are not currently playing. #[inline] pub fn stopped_channels_iter_mut(&mut self) -> impl Iterator { self.channels.iter_mut().filter(|channel| !channel.playing) } + /// Returns an iterator of all [`AudioChannel`]s. #[inline] pub fn channels_iter(&mut self) -> impl Iterator { self.channels.iter() } + /// Returns an iterator of all [`AudioChannel`]s as mutable references. #[inline] pub fn channels_iter_mut(&mut self) -> impl Iterator { self.channels.iter_mut() } + /// Returns a reference to the specified [`AudioChannel`] or `None` if the index specified + /// is not valid. #[inline] pub fn get(&self, index: usize) -> Option<&AudioChannel> { self.channels.get(index) } + /// Returns a mutable reference to the specified [`AudioChannel`] or `None` if the index + /// specified is not valid. #[inline] pub fn get_mut(&mut self, index: usize) -> Option<&mut AudioChannel> { self.channels.get_mut(index) @@ -305,6 +367,8 @@ impl AudioDevice { impl Index for AudioDevice { type Output = AudioChannel; + /// Returns a reference to the specified [`AudioChannel`] or panics if the index specified is + /// not valid. #[inline] fn index(&self, index: usize) -> &Self::Output { self.get(index).unwrap() @@ -312,6 +376,8 @@ impl Index for AudioDevice { } impl IndexMut for AudioDevice { + /// Returns a mutable reference to the specified [`AudioChannel`] or panics if the index + /// specified is not valid. #[inline] fn index_mut(&mut self, index: usize) -> &mut Self::Output { self.get_mut(index).unwrap() diff --git a/libretrogd/src/audio/mod.rs b/libretrogd/src/audio/mod.rs index 8d22507..273cf0c 100644 --- a/libretrogd/src/audio/mod.rs +++ b/libretrogd/src/audio/mod.rs @@ -10,6 +10,7 @@ pub mod buffer; pub mod device; pub mod queue; +/// The number of simultaneously playing audio channels supported by this library currently. pub const NUM_CHANNELS: usize = 8; pub const AUDIO_FREQUENCY_44KHZ: u32 = 44100; @@ -18,11 +19,15 @@ pub const AUDIO_FREQUENCY_11KHZ: u32 = 11025; pub const SILENCE: u8 = AudioFormatNum::SILENCE; +/// The target audio frequency supported by this library currently. pub const TARGET_AUDIO_FREQUENCY: u32 = AUDIO_FREQUENCY_22KHZ; +/// The number of channels per audio buffer supported by this library currently. pub const TARGET_AUDIO_CHANNELS: u8 = 1; ////////////////////////////////////////////////////////////////////////////////////////////////// +/// Represents an "audio specification" for an audio buffer or the audio device itself. Useful +/// to know what format an audio buffer is in and to specify conversion formats, etc. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct AudioSpec { frequency: u32, @@ -31,6 +36,13 @@ pub struct AudioSpec { } impl AudioSpec { + /// Creates a new `AudioSpec` with the properties specified. + /// + /// # Arguments + /// + /// * `frequency`: the frequency of the audio + /// * `channels`: the number of channels of the audio (e.g. 1 = mono, 2 = stereo, etc) + /// * `format`: indicates the format of the bytes making up the audio buffer. pub fn new(frequency: u32, channels: u8, format: AudioFormat) -> Self { AudioSpec { frequency, @@ -49,6 +61,8 @@ impl AudioSpec { self.channels } + /// An SDL2 [`sdl2::audio::AudioFormat`] value indicating the audio format of the bytes making + /// up an audio buffer. #[inline] pub fn format(&self) -> AudioFormat { self.format @@ -57,7 +71,12 @@ impl AudioSpec { ////////////////////////////////////////////////////////////////////////////////////////////////// +// NOTE: this is currently hardcoded such that 8-bit samples must always be used! + +/// Used to implement custom/dynamic audio generation. pub trait AudioGenerator: Send { + /// Generates and returns the sample for the given playback position. `None` is returned if + /// there is no sample for that position (e.g. it might be past the "end"). fn gen_sample(&mut self, position: usize) -> Option; } @@ -69,12 +88,20 @@ pub enum AudioError { OpenDeviceFailed(String), } +/// Top-level abstraction over the system's audio output device. To play audio or change other +/// playback properties, you will need to lock the audio device via [`Audio::lock`] to obtain an +/// [`AudioDevice`]. pub struct Audio { spec: AudioSpec, sdl_audio_device: sdl2::audio::AudioDevice, } impl Audio { + /// Creates a new [`Audio`] instance, wrapping the given SDL [`sdl2::audio::AudioSubsystem`]. + /// The `desired_spec` given specifies the target audio playback format. + /// + /// Ideally, you should not be creating an instance of this yourself and should just use the + /// one provided by [`crate::system::System`]. pub fn new( desired_spec: AudioSpecDesired, sdl_audio_subsystem: &AudioSubsystem, @@ -106,26 +133,36 @@ impl Audio { } } + /// Returns current audio device's audio specification/format for playback. All [`AudioBuffer`]s + /// that are to be used for playback must be converted to match this before they can be played. #[inline] pub fn spec(&self) -> &AudioSpec { &self.spec } + /// Returns the current status of the audio device (e.g. whether it is paused, stopped, etc). #[inline] pub fn status(&self) -> sdl2::audio::AudioStatus { self.sdl_audio_device.status() } + /// Pauses all audio playback. #[inline] pub fn pause(&mut self) { self.sdl_audio_device.pause() } + /// Resumes all audio playback. #[inline] pub fn resume(&mut self) { self.sdl_audio_device.resume() } + /// Locks the audio device so that new audio data can be provided or playback altered. A + /// [`AudioDevice`] instance is returned on successful lock which can be used to interact with + /// the actual system's audio playback. The audio device is unlocked once this instance is + /// dropped. Ideally, you will want to keep the audio device for **as _short_ a time as + /// possible!** #[inline] pub fn lock(&mut self) -> sdl2::audio::AudioDeviceLockGuard { self.sdl_audio_device.lock() diff --git a/libretrogd/src/audio/queue.rs b/libretrogd/src/audio/queue.rs index 5560383..8867ff0 100644 --- a/libretrogd/src/audio/queue.rs +++ b/libretrogd/src/audio/queue.rs @@ -35,12 +35,18 @@ pub enum AudioCommand { }, } +/// A convenience abstraction that can be used to queue up commands to be issued to an +/// [`AudioDevice`]. This can be more useful to utilize in applications versus needing to directly +/// lock the [`AudioDevice`] and then determine what your application needs to do and issue those +/// commands that time. [`AudioQueue`] lets you play/stop audio in more of a "fire-and-forget" +/// manner. pub struct AudioQueue { spec: AudioSpec, commands: VecDeque, } impl AudioQueue { + /// Creates and returns a new [`AudioQueue`] instance. pub fn new(audio: &Audio) -> Self { AudioQueue { spec: audio.spec, @@ -48,11 +54,15 @@ impl AudioQueue { } } + /// Returns the spec that this queue is currently set to play. All audio to be played via + /// this queue must be pre-converted to match this spec! This spec is a copy of the one that + /// was obtained from the [`Audio`] instance used to create this [`AudioQueue`]. #[inline] pub fn spec(&self) -> &AudioSpec { &self.spec } + /// Queues a stop command for the given channel. pub fn stop_channel(&mut self, channel_index: usize) -> Result<(), AudioDeviceError> { if channel_index >= NUM_CHANNELS { Err(AudioDeviceError::ChannelIndexOutOfRange(channel_index)) @@ -62,10 +72,14 @@ impl AudioQueue { } } + /// Queues a command that will stop playback on all channels. pub fn stop_all(&mut self) { self.commands.push_back(AudioCommand::StopAllChannels); } + /// Queues a command to play a copy of the given [`AudioBuffer`]'s data. The buffer will be + /// played on the first channel found that is not already playing. If all channels are already + /// playing, then nothing will be done. pub fn play_buffer( &mut self, buffer: &AudioBuffer, @@ -82,6 +96,10 @@ impl AudioQueue { } } + /// Queues a command to play the given [`AudioBuffer`]'s data. The buffer will be played on + /// the first channel found that is not already playing. If all channels are already playing, + /// then nothing will be done. This method is more performant than [`AudioQueue::play_buffer`], + /// as that method will always immediately copy the given buffer to create the queued command. pub fn play_buffer_rc( &mut self, buffer: Rc, @@ -98,6 +116,9 @@ impl AudioQueue { } } + /// Queues a command to play a copy of the given [`AudioBuffer`]'s data on the channel + /// specified. Whatever that channel was playing will be interrupted to begin playing this + /// buffer. pub fn play_buffer_on_channel( &mut self, channel_index: usize, @@ -118,6 +139,10 @@ impl AudioQueue { } } + /// Queues a command to play the given [`AudioBuffer`]'s data on the channel specified. Whatever + /// that channel was playing will be interrupted to begin playing this buffer. This method is + /// more performant than [`AudioQueue::play_buffer_on_channel`], as that method will always + /// immediately copy the given buffer to create the queued command. pub fn play_buffer_rc_on_channel( &mut self, channel_index: usize, @@ -138,6 +163,8 @@ impl AudioQueue { } } + /// Queues a command to play the given [`AudioGenerator`] on the first channel found that is + /// not already playing. If all channels are already playing, then nothing will be done. pub fn play_generator( &mut self, generator: Box, @@ -147,6 +174,8 @@ impl AudioQueue { Ok(()) } + /// Queues a command to play the given [`AudioGenerator`] on the channel specified. Whatever + /// that channel was playing will be interrupted to begin playing this generator. pub fn play_generator_on_channel( &mut self, channel_index: usize, @@ -161,6 +190,8 @@ impl AudioQueue { Ok(()) } + /// Flushes the queued commands, issuing them in the same order they were created, to the + /// given [`AudioDevice`]. pub fn apply_to_device(&mut self, device: &mut AudioDevice) -> Result<(), AudioDeviceError> { loop { if let Some(command) = self.commands.pop_front() { @@ -197,6 +228,10 @@ impl AudioQueue { } } + /// Flushes the queued commands, issuing them in the same order they were created, to the + /// given [`Audio`] instance. This method automatically handles obtaining a locked + /// [`AudioDevice`], and so is a bit more convenient to use if you don't actually need to + /// interact with the [`AudioDevice`] itself in your code. pub fn apply(&mut self, audio: &mut Audio) -> Result<(), AudioDeviceError> { let mut device = audio.lock(); self.apply_to_device(&mut device) diff --git a/libretrogd/src/system/mod.rs b/libretrogd/src/system/mod.rs index 2c107f8..93dcf8f 100644 --- a/libretrogd/src/system/mod.rs +++ b/libretrogd/src/system/mod.rs @@ -303,7 +303,13 @@ pub struct System { target_framerate_delta: Option, next_tick: i64, + /// An [`Audio`] instance that allows interacting with the system's audio output device. pub audio: Audio, + + /// An [`AudioQueue`] instance that can queue up playback/stop commands to be issued to the + /// system's [`Audio`] instance a bit more flexibly. If you use this, your application must + /// manually call [`AudioQueue::apply`] or [`AudioQueue::apply_to_device`] in your loop to + /// flush the queued commands, otherwise this queue will not do anything. pub audio_queue: AudioQueue, /// The primary backbuffer [`Bitmap`] that will be rendered to the screen whenever