From 342e2a3877fe54d3de7056930a354e2413a0cf13 Mon Sep 17 00:00:00 2001 From: gered Date: Sat, 28 May 2022 20:29:17 -0400 Subject: [PATCH] add initial audio playback and wav file support --- libretrogd/src/audio/mod.rs | 271 ++++++++++++++++++++++ libretrogd/src/audio/wav.rs | 270 +++++++++++++++++++++ libretrogd/src/lib.rs | 1 + libretrogd/src/system/mod.rs | 33 ++- libretrogd/test-assets/22khz_8bit_1ch.wav | Bin 0 -> 11970 bytes 5 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 libretrogd/src/audio/mod.rs create mode 100644 libretrogd/src/audio/wav.rs create mode 100644 libretrogd/test-assets/22khz_8bit_1ch.wav 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 0000000000000000000000000000000000000000..153187e7cec919a76121400be34ef67ef3964187 GIT binary patch literal 11970 zcmeI&30Mk&v(IB9rKq-h;q1eg?+EOia(Sm0{`z?zXZP&K;`?CM{W+s`01QIUWE;p`*I(Cu_5u6Q#f&wasgw2e z?%XjZEL{&t?>abSWv%sHGT&w6MlW7?w2#vpgsmWz$2)sAImUj1mDgf3GlDRhYGQ2e z;6MA&Mxg3$m-NbkmG zuU-xCUR_X-ClIXj_kZQ3@v~+vNl3`Z%{6DSzW;A$c6Me;ilDM`kGHq5wUy7~ow#*t z*$XfD^7-C&cFykZOE{c&UVPCZGgEl@@JXrkp^lEu=+UF|ySkdo%a5#DwW|B+)1Yb7 z40Lsa)6>s1HO0HQtcj2JwYPWj^V=bn{`TO(0~U*AK67TdRC>zR*2=)Zz=_K>CWxg{ zsr2^k+f5fQid|e>7P8sNEiIP}4GmqRqBf+br{|ZI7QjbMcXoDuo*-=6+uIv!YipaE zn@+9>kYHDh- zGB%z%W5$df5{V?%+uK`TU*94%HFcjzB=WVkwmt|)9e{0-SS;QPpX&*yg1X7AsjI8I zcJt=VJMe2)8yg$#^z`&LZQ7I#SAAvo?%nxiWo5#QjEv~W$jGp;u#K&)t!?nJZBRA( z+rgRY>gx5u!NF#5NBcWEI*omMW&L(&X905tbzP>Bq3er!) zT{b(Jo143KgUk6~&N#5#-`{^%US8e~sP!OEkj&@vSHf{-V*xiNCT0ul;IH%Z^9wlx zzJW?>_k_xg{`tw1CqE+*9v&Wkk3r)*zziD0VA?-`%}iiA9xTiTF|PpgDx~{Y5Dyh) zy$C)aCWh+@!l1%ZlN}JxAS59vDQOp+_EAR~Nb&;Qw}B=4gozc%e;$J0;D)82CxSgE zkQqi`$4{UI4RIp622}nPal|LChbFjr1!`4C$2<;MGzN)lK~@?g2hnY|@5bf%0%Q-m zMpxH@^9cVvthgKzr(l6T$WjX=78+P0Ne@BF&mqxB4D=6?Na%!(N7X+r1+@ev1iF#F z{;SZ+Kmd22loU;V(Cg6RhK3WOu<$h?BxJrB#(|ar8@6B(p_u&??A5U_OjxX!@ikdk zWZ^40S95{11-cS~uiM8b5>*pOhk<~+Z%g9fDuN^)+m?(S{Q>(o>Q+rnADdvN=*mFr zk60OMGVlxOM`$Y;xetV>In&bk2T7kkB3=KS6g!PH2el|jrj9#Uiu+DgRTht8g1RVR zO{H)a6P5FaTKWUoT08`ZN>7AUL1oSZTgswn5kZjI9h z`pzJ4K+%d84V_r#&i(scGSzekLU|lSOS#21OxzVKTnSA0$cJdw zR;dP-6|LW?)OKC1w&bNs+!(mC>8eqkTm8+dcXn>Di0DHk9cmSFMxQ2&m)d$jcdSAc zk3&xCnx*~?3YDhVGkb1br8Iq`#+ANZzI?f{@|ebsYDR*HQf|Go~DM#g9)EiQ$P+OYrzR%oyvzVJxCY-Ckv^WR8G7 z%SzMxU~ZBtg~3WAWlt9Jf~t`{IQujo|5_g@UOk^-LI0>)+G(&4UgY%0LiId~2a10B z9eA#OA+w8d1LW%w?NfyZ0j)L`_1oh9qaWmPp4*SvNz2(ttHTcsI{r#^0*4EJ`8VJM zzmd^C$$WOi@_?*mX>ek->?~?R@jmjsCcLBn-Jst!4MX1BqmGtqtiBJ|Y6%8kO*{)) z1uFXBfy67}{sDsTY&54;6R12`)730r@nxFUTMDmid)r7v-!Kg}#_Kp)ajoptFkN80 zavhW+^$y{mnzyIYk^8i_cIbVm0DtNB43fBV2Dbj=yBQ@~