diff --git a/libretrogd/src/audio/mod.rs b/libretrogd/src/audio/mod.rs new file mode 100644 index 0000000..ee1aa8a --- /dev/null +++ b/libretrogd/src/audio/mod.rs @@ -0,0 +1,271 @@ +use std::ops::{Index, IndexMut}; + +use sdl2::audio::AudioCallback; +use thiserror::Error; + +pub use self::wav::*; + +pub mod wav; + +pub const NUM_CHANNELS: usize = 8; + +pub const AUDIO_FREQUENCY_44KHZ: u32 = 44100; +pub const AUDIO_FREQUENCY_22KHZ: u32 = 22050; +pub const AUDIO_FREQUENCY_11KHZ: u32 = 11025; + +pub const SILENCE: u8 = sdl2::audio::AudioFormatNum::SILENCE; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct AudioSpec { + pub frequency: u32, + pub channels: u8, +} + +impl AudioSpec { + #[inline] + pub fn frequency(&self) -> u32 { + self.frequency + } + + #[inline] + pub fn channels(&self) -> u8 { + self.channels + } +} + +#[derive(Debug, Clone)] +pub struct AudioChannel { + pub playing: bool, + pub loops: bool, + pub data: Vec, + pub volume: f32, + pub position: usize, +} + +impl AudioChannel { + pub fn new() -> Self { + AudioChannel { + playing: false, + loops: false, + volume: 1.0, + position: 0, + data: Vec::new(), + } + } + + #[inline] + fn next_sample(&mut self) -> i16 { + if let Some(sample) = self.data.get(self.position) { + self.position += 1; + (*sample as i16) - 128 + } else { + 0 + } + } + + #[inline] + pub fn get_audio_frame(&mut self) -> i16 { + if !self.playing { + return 0; + } else if self.position >= self.data.len() { + if self.loops { + self.position = 0; + } else { + self.stop(); + return 0; + } + } + + let raw_sample = self.next_sample(); + (raw_sample as f32 * self.volume) as i16 + } + + pub fn reset(&mut self) { + self.data.clear(); + self.position = 0; + self.playing = false; + } + + pub fn play_buffer(&mut self, buffer: &AudioBuffer, loops: bool) { + self.data.clear(); + self.data.extend(&buffer.data); + self.position = 0; + self.playing = true; + self.loops = loops; + } + + pub fn play(&mut self, loops: bool) { + if !self.data.is_empty() { + self.position = 0; + self.playing = true; + self.loops = loops; + } + } + + pub fn stop(&mut self) { + self.playing = false; + } +} + +pub struct AudioDevice { + spec: AudioSpec, + channels: Vec, +} + +impl AudioCallback for AudioDevice { + type Channel = u8; + + fn callback(&mut self, out: &mut [u8]) { + for dest in out.iter_mut() { + let mut sample: i16 = 0; + for channel in self.channels.iter_mut() { + sample += channel.get_audio_frame(); + } + *dest = (sample.clamp(-128, 127) + 128) as u8; + } + } +} + +impl AudioDevice { + pub fn new(spec: AudioSpec) -> Self { + let mut channels = Vec::new(); + for _ in 0..NUM_CHANNELS { + channels.push(AudioChannel::new()); + } + AudioDevice { spec, channels } + } + + #[inline] + pub fn spec(&self) -> &AudioSpec { + &self.spec + } + + #[inline] + pub fn is_playing(&self) -> bool { + self.channels.iter().any(|channel| channel.playing) + } + + pub fn stop_all(&mut self) { + for channel in self.channels.iter_mut() { + channel.stop(); + } + } + + pub fn play_buffer(&mut self, buffer: &AudioBuffer, loops: bool) -> Option<&mut AudioChannel> { + if let Some(channel) = self.stopped_channels_iter_mut().next() { + channel.play_buffer(buffer, loops); + Some(channel) + } else { + None + } + } + + #[inline] + pub fn playing_channels_iter(&mut self) -> impl Iterator { + self.channels.iter().filter(|channel| channel.playing) + } + + #[inline] + pub fn playing_channels_iter_mut(&mut self) -> impl Iterator { + self.channels.iter_mut().filter(|channel| channel.playing) + } + + #[inline] + pub fn stopped_channels_iter(&mut self) -> impl Iterator { + self.channels.iter().filter(|channel| !channel.playing) + } + + #[inline] + pub fn stopped_channels_iter_mut(&mut self) -> impl Iterator { + self.channels.iter_mut().filter(|channel| !channel.playing) + } + + #[inline] + pub fn channels_iter(&mut self) -> impl Iterator { + self.channels.iter() + } + + #[inline] + pub fn channels_iter_mut(&mut self) -> impl Iterator { + self.channels.iter_mut() + } + + #[inline] + pub fn get(&self, index: usize) -> Option<&AudioChannel> { + self.channels.get(index) + } + + #[inline] + pub fn get_mut(&mut self, index: usize) -> Option<&mut AudioChannel> { + self.channels.get_mut(index) + } +} + +impl Index for AudioDevice { + type Output = AudioChannel; + + #[inline] + fn index(&self, index: usize) -> &Self::Output { + self.get(index).unwrap() + } +} + +impl IndexMut for AudioDevice { + #[inline] + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + self.get_mut(index).unwrap() + } +} + +#[derive(Error, Debug)] +pub enum AudioBufferError { + #[error("Error during format conversion: {0}")] + ConversionError(String), +} + +#[derive(Debug, Clone)] +pub struct AudioBuffer { + spec: AudioSpec, + pub data: Vec, +} + +impl AudioBuffer { + pub fn new(frequency: u32, channels: u8) -> Self { + AudioBuffer { + spec: AudioSpec { + frequency, + channels, + }, + data: Vec::new(), + } + } + + #[inline] + pub fn spec(&self) -> &AudioSpec { + &self.spec + } + + pub fn convert(self, frequency: u32, channels: u8) -> Result { + if self.spec.frequency == frequency && self.spec.channels == channels { + Ok(self) + } else { + use sdl2::audio::AudioFormat; + let converter = sdl2::audio::AudioCVT::new( + AudioFormat::U8, + self.spec.channels, + self.spec.frequency as i32, + AudioFormat::U8, + channels, + frequency as i32, + ); + match converter { + Ok(converter) => { + let mut result = AudioBuffer::new(frequency, channels); + result.data = converter.convert(self.data); + Ok(result) + } + Err(string) => Err(AudioBufferError::ConversionError(string)), + } + } + } +} diff --git a/libretrogd/src/audio/wav.rs b/libretrogd/src/audio/wav.rs new file mode 100644 index 0000000..031d637 --- /dev/null +++ b/libretrogd/src/audio/wav.rs @@ -0,0 +1,270 @@ +use std::fs::File; +use std::io; +use std::io::{BufReader, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::audio::AudioBuffer; + +#[derive(Error, Debug)] +pub enum WavError { + #[error("Bad or unsupported WAV file: {0}")] + BadFile(String), + + #[error("WAV I/O error")] + IOError(#[from] std::io::Error), +} + +#[derive(Debug, Copy, Clone)] +struct ChunkId { + id: [u8; 4], +} + +impl ChunkId { + pub fn read(reader: &mut T) -> Result { + let mut id = [0u8; 4]; + reader.read_exact(&mut id)?; + Ok(ChunkId { id }) + } + + #[allow(dead_code)] + pub fn write(&self, writer: &mut T) -> Result<(), WavError> { + writer.write_all(&self.id)?; + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +struct SubChunkHeader { + chunk_id: ChunkId, + size: u32, +} + +impl SubChunkHeader { + pub fn read(reader: &mut T) -> Result { + let chunk_id = ChunkId::read(reader)?; + let size = reader.read_u32::()?; + Ok(SubChunkHeader { chunk_id, size }) + } + + #[allow(dead_code)] + pub fn write(&self, writer: &mut T) -> Result<(), WavError> { + self.chunk_id.write(writer)?; + writer.write_u32::(self.size)?; + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +struct WavHeader { + file_chunk: SubChunkHeader, + file_container_id: ChunkId, +} + +impl WavHeader { + pub fn read(reader: &mut T) -> Result { + let file_chunk = SubChunkHeader::read(reader)?; + let file_container_id = ChunkId::read(reader)?; + Ok(WavHeader { + file_chunk, + file_container_id, + }) + } + + #[allow(dead_code)] + pub fn write(&self, writer: &mut T) -> Result<(), WavError> { + self.file_chunk.write(writer)?; + self.file_container_id.write(writer)?; + Ok(()) + } +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +struct FormatChunk { + compression_code: u16, + channels: u16, + frequency: u32, + bytes_per_second: u32, + block_alignment: u16, + bits_per_sample: u16, + additional_data_length: u16, + additional_data: Option>, +} + +impl FormatChunk { + pub fn read( + reader: &mut T, + chunk_header: &SubChunkHeader, + ) -> Result { + let compression_code = reader.read_u16::()?; + let channels = reader.read_u16::()?; + let frequency = reader.read_u32::()?; + let bytes_per_second = reader.read_u32::()?; + let block_alignment = reader.read_u16::()?; + let bits_per_sample = reader.read_u16::()?; + let additional_data_length; + let additional_data; + if chunk_header.size > 16 { + additional_data_length = reader.read_u16::()?; + let mut buffer = vec![0u8; additional_data_length as usize]; + reader.read(&mut buffer)?; + additional_data = Some(buffer.into_boxed_slice()); + } else { + additional_data_length = 0; + additional_data = None; + } + + Ok(FormatChunk { + compression_code, + channels, + frequency, + bytes_per_second, + block_alignment, + bits_per_sample, + additional_data_length, + additional_data, + }) + } + + #[allow(dead_code)] + pub fn write(&self, writer: &mut T) -> Result<(), WavError> { + writer.write_u16::(self.compression_code)?; + writer.write_u16::(self.channels)?; + writer.write_u32::(self.frequency)?; + writer.write_u32::(self.bytes_per_second)?; + writer.write_u16::(self.block_alignment)?; + writer.write_u16::(self.bits_per_sample)?; + if self.additional_data_length > 0 { + writer.write_u16::(self.additional_data_length)?; + writer.write_all(&self.additional_data.as_ref().unwrap())?; + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct DataChunk { + data: Box<[u8]>, +} + +impl DataChunk { + pub fn read( + reader: &mut T, + chunk_header: &SubChunkHeader, + ) -> Result { + let mut buffer = vec![0u8; chunk_header.size as usize]; + let bytes_read = reader.read(&mut buffer)?; + // bunch of tools (like sfxr, jsfxr) sometimes generating "data" chunks that are too large. + // probably these tools are just incorrectly hard-coded to always assume 16-bit, because + // every time so far i have seen this, the data chunk size is exactly twice what the actual + // data size is for an 8-bit wav file. + // so, lets chop off the excess, so we don't have a very large amount of zero's at the end + // which would probably result in audio clicking if played as-is! + buffer.truncate(bytes_read); + Ok(DataChunk { + data: buffer.into_boxed_slice(), + }) + } + + #[allow(dead_code)] + pub fn write(&self, writer: &mut T) -> Result<(), WavError> { + writer.write_all(self.data.as_ref())?; + Ok(()) + } +} + +impl AudioBuffer { + pub fn load_wav_bytes(reader: &mut T) -> Result { + let header = WavHeader::read(reader)?; + if header.file_chunk.chunk_id.id != *b"RIFF" { + return Err(WavError::BadFile(String::from( + "Unexpected RIFF chunk id, probably not a WAV file", + ))); + } + if header.file_container_id.id != *b"WAVE" { + return Err(WavError::BadFile(String::from( + "Unexpected RIFF container id, probably not a WAV file", + ))); + } + + let mut format: Option = None; + let mut data: Option = None; + + loop { + let chunk_header = match SubChunkHeader::read(reader) { + Ok(header) => header, + Err(WavError::IOError(io_error)) + if io_error.kind() == io::ErrorKind::UnexpectedEof => + { + break; + } + Err(err) => return Err(err), + }; + let chunk_data_position = reader.stream_position()?; + + // read only the chunks we recognize / care about + if chunk_header.chunk_id.id == *b"fmt " { + format = Some(FormatChunk::read(reader, &chunk_header)?); + if format.as_ref().unwrap().compression_code != 1 { + return Err(WavError::BadFile(String::from( + "Only PCM format WAV files are supported", + ))); + } + if format.as_ref().unwrap().bits_per_sample != 8 { + return Err(WavError::BadFile(String::from( + "Only 8-bit sample WAV files are supported", + ))); + } + } else if chunk_header.chunk_id.id == *b"data" { + data = Some(DataChunk::read(reader, &chunk_header)?); + } + + // move to the start of the next chunk (possibly skipping over the current chunk if we + // didn't recognize it above ...) + reader.seek(SeekFrom::Start( + chunk_data_position + chunk_header.size as u64, + ))?; + } + + // all done reading the file, now convert the read data into an AudioBuffer ... + + let mut audio_buffer; + + if let Some(format) = format { + audio_buffer = AudioBuffer::new(format.frequency, format.channels as u8); + } else { + return Err(WavError::BadFile(String::from("No 'fmt ' chunk was found"))); + } + + if let Some(data) = data { + audio_buffer.data = data.data.into_vec(); + } else { + return Err(WavError::BadFile(String::from("No 'data' chunk was found"))); + } + + Ok(audio_buffer) + } + + pub fn load_wav_file(path: &Path) -> Result { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + Self::load_wav_bytes(&mut reader) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn load_wav_file() -> Result<(), WavError> { + let wav_buffer = AudioBuffer::load_wav_file(Path::new("./test-assets/22khz_8bit_1ch.wav"))?; + assert_eq!(22050, wav_buffer.spec().frequency()); + assert_eq!(1, wav_buffer.spec().channels()); + assert_eq!(8184, wav_buffer.data.len()); + Ok(()) + } +} diff --git a/libretrogd/src/lib.rs b/libretrogd/src/lib.rs index fe17665..4e15a29 100644 --- a/libretrogd/src/lib.rs +++ b/libretrogd/src/lib.rs @@ -1,6 +1,7 @@ extern crate core; extern crate sdl2; +pub mod audio; pub mod entities; pub mod events; pub mod graphics; diff --git a/libretrogd/src/system/mod.rs b/libretrogd/src/system/mod.rs index 5a0087e..28d5390 100644 --- a/libretrogd/src/system/mod.rs +++ b/libretrogd/src/system/mod.rs @@ -1,11 +1,13 @@ use byte_slice_cast::AsByteSlice; -use sdl2::{EventPump, Sdl, TimerSubsystem, VideoSubsystem}; +use sdl2::{AudioSubsystem, EventPump, Sdl, TimerSubsystem, VideoSubsystem}; +use sdl2::audio::AudioSpecDesired; use sdl2::event::Event; use sdl2::pixels::PixelFormatEnum; use sdl2::render::{Texture, WindowCanvas}; use thiserror::Error; use crate::{DEFAULT_SCALE_FACTOR, SCREEN_HEIGHT, SCREEN_WIDTH}; +use crate::audio::*; use crate::graphics::*; pub use self::input_devices::*; @@ -147,6 +149,11 @@ impl SystemBuilder { Err(message) => return Err(SystemError::InitError(message)), }; + let sdl_audio_subsystem = match sdl_context.audio() { + Ok(audio_subsystem) => audio_subsystem, + Err(message) => return Err(SystemError::InitError(message)), + }; + // create the window let window_width = screen_width * self.initial_scale_factor; @@ -235,6 +242,25 @@ impl SystemBuilder { Err(error) => return Err(SystemError::InitError(error.to_string())), }; + let audio_spec = AudioSpecDesired { + freq: Some(AUDIO_FREQUENCY_22KHZ as i32), + channels: Some(1), + samples: None, + }; + + let audio = match sdl_audio_subsystem.open_playback(None, &audio_spec, |spec| { + let our_spec = AudioSpec { + frequency: spec.freq as u32, + channels: spec.channels, + }; + + AudioDevice::new(our_spec) + }) { + Ok(audio_device) => audio_device, + Err(error) => return Err(SystemError::InitError(error)), + }; + audio.resume(); + // create input device objects, exposed to the application let keyboard = Keyboard::new(); @@ -242,6 +268,7 @@ impl SystemBuilder { Ok(System { sdl_context, + sdl_audio_subsystem, sdl_video_subsystem, sdl_timer_subsystem, sdl_canvas, @@ -249,6 +276,7 @@ impl SystemBuilder { sdl_texture_pitch, sdl_event_pump, texture_pixels, + audio, video: framebuffer, palette, font, @@ -267,6 +295,7 @@ impl SystemBuilder { #[allow(dead_code)] pub struct System { sdl_context: Sdl, + sdl_audio_subsystem: AudioSubsystem, sdl_video_subsystem: VideoSubsystem, sdl_timer_subsystem: TimerSubsystem, sdl_canvas: WindowCanvas, @@ -280,6 +309,8 @@ pub struct System { target_framerate_delta: Option, next_tick: i64, + pub audio: sdl2::audio::AudioDevice, + /// The primary backbuffer [`Bitmap`] that will be rendered to the screen whenever /// [`System::display`] is called. Regardless of the actual window size, this bitmap is always /// [`SCREEN_WIDTH`]x[`SCREEN_HEIGHT`] pixels in size. diff --git a/libretrogd/test-assets/22khz_8bit_1ch.wav b/libretrogd/test-assets/22khz_8bit_1ch.wav new file mode 100644 index 0000000..153187e Binary files /dev/null and b/libretrogd/test-assets/22khz_8bit_1ch.wav differ