add initial audio playback and wav file support

This commit is contained in:
Gered 2022-05-28 20:29:17 -04:00
parent bc03961a49
commit 342e2a3877
5 changed files with 574 additions and 1 deletions

271
libretrogd/src/audio/mod.rs Normal file
View file

@ -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<u8>,
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<AudioChannel>,
}
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<Item = &AudioChannel> {
self.channels.iter().filter(|channel| channel.playing)
}
#[inline]
pub fn playing_channels_iter_mut(&mut self) -> impl Iterator<Item = &mut AudioChannel> {
self.channels.iter_mut().filter(|channel| channel.playing)
}
#[inline]
pub fn stopped_channels_iter(&mut self) -> impl Iterator<Item = &AudioChannel> {
self.channels.iter().filter(|channel| !channel.playing)
}
#[inline]
pub fn stopped_channels_iter_mut(&mut self) -> impl Iterator<Item = &mut AudioChannel> {
self.channels.iter_mut().filter(|channel| !channel.playing)
}
#[inline]
pub fn channels_iter(&mut self) -> impl Iterator<Item = &AudioChannel> {
self.channels.iter()
}
#[inline]
pub fn channels_iter_mut(&mut self) -> impl Iterator<Item = &mut AudioChannel> {
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<usize> for AudioDevice {
type Output = AudioChannel;
#[inline]
fn index(&self, index: usize) -> &Self::Output {
self.get(index).unwrap()
}
}
impl IndexMut<usize> 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<u8>,
}
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<Self, AudioBufferError> {
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)),
}
}
}
}

270
libretrogd/src/audio/wav.rs Normal file
View file

@ -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<T: Read>(reader: &mut T) -> Result<Self, WavError> {
let mut id = [0u8; 4];
reader.read_exact(&mut id)?;
Ok(ChunkId { id })
}
#[allow(dead_code)]
pub fn write<T: 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<T: ReadBytesExt>(reader: &mut T) -> Result<Self, WavError> {
let chunk_id = ChunkId::read(reader)?;
let size = reader.read_u32::<LittleEndian>()?;
Ok(SubChunkHeader { chunk_id, size })
}
#[allow(dead_code)]
pub fn write<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), WavError> {
self.chunk_id.write(writer)?;
writer.write_u32::<LittleEndian>(self.size)?;
Ok(())
}
}
#[derive(Debug, Copy, Clone)]
struct WavHeader {
file_chunk: SubChunkHeader,
file_container_id: ChunkId,
}
impl WavHeader {
pub fn read<T: ReadBytesExt>(reader: &mut T) -> Result<Self, WavError> {
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<T: WriteBytesExt>(&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<Box<[u8]>>,
}
impl FormatChunk {
pub fn read<T: ReadBytesExt>(
reader: &mut T,
chunk_header: &SubChunkHeader,
) -> Result<Self, WavError> {
let compression_code = reader.read_u16::<LittleEndian>()?;
let channels = reader.read_u16::<LittleEndian>()?;
let frequency = reader.read_u32::<LittleEndian>()?;
let bytes_per_second = reader.read_u32::<LittleEndian>()?;
let block_alignment = reader.read_u16::<LittleEndian>()?;
let bits_per_sample = reader.read_u16::<LittleEndian>()?;
let additional_data_length;
let additional_data;
if chunk_header.size > 16 {
additional_data_length = reader.read_u16::<LittleEndian>()?;
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<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), WavError> {
writer.write_u16::<LittleEndian>(self.compression_code)?;
writer.write_u16::<LittleEndian>(self.channels)?;
writer.write_u32::<LittleEndian>(self.frequency)?;
writer.write_u32::<LittleEndian>(self.bytes_per_second)?;
writer.write_u16::<LittleEndian>(self.block_alignment)?;
writer.write_u16::<LittleEndian>(self.bits_per_sample)?;
if self.additional_data_length > 0 {
writer.write_u16::<LittleEndian>(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<T: ReadBytesExt>(
reader: &mut T,
chunk_header: &SubChunkHeader,
) -> Result<Self, WavError> {
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<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), WavError> {
writer.write_all(self.data.as_ref())?;
Ok(())
}
}
impl AudioBuffer {
pub fn load_wav_bytes<T: ReadBytesExt + Seek>(reader: &mut T) -> Result<AudioBuffer, WavError> {
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<FormatChunk> = None;
let mut data: Option<DataChunk> = 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<AudioBuffer, WavError> {
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(())
}
}

View file

@ -1,6 +1,7 @@
extern crate core; extern crate core;
extern crate sdl2; extern crate sdl2;
pub mod audio;
pub mod entities; pub mod entities;
pub mod events; pub mod events;
pub mod graphics; pub mod graphics;

View file

@ -1,11 +1,13 @@
use byte_slice_cast::AsByteSlice; 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::event::Event;
use sdl2::pixels::PixelFormatEnum; use sdl2::pixels::PixelFormatEnum;
use sdl2::render::{Texture, WindowCanvas}; use sdl2::render::{Texture, WindowCanvas};
use thiserror::Error; use thiserror::Error;
use crate::{DEFAULT_SCALE_FACTOR, SCREEN_HEIGHT, SCREEN_WIDTH}; use crate::{DEFAULT_SCALE_FACTOR, SCREEN_HEIGHT, SCREEN_WIDTH};
use crate::audio::*;
use crate::graphics::*; use crate::graphics::*;
pub use self::input_devices::*; pub use self::input_devices::*;
@ -147,6 +149,11 @@ impl SystemBuilder {
Err(message) => return Err(SystemError::InitError(message)), 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 // create the window
let window_width = screen_width * self.initial_scale_factor; let window_width = screen_width * self.initial_scale_factor;
@ -235,6 +242,25 @@ impl SystemBuilder {
Err(error) => return Err(SystemError::InitError(error.to_string())), 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 // create input device objects, exposed to the application
let keyboard = Keyboard::new(); let keyboard = Keyboard::new();
@ -242,6 +268,7 @@ impl SystemBuilder {
Ok(System { Ok(System {
sdl_context, sdl_context,
sdl_audio_subsystem,
sdl_video_subsystem, sdl_video_subsystem,
sdl_timer_subsystem, sdl_timer_subsystem,
sdl_canvas, sdl_canvas,
@ -249,6 +276,7 @@ impl SystemBuilder {
sdl_texture_pitch, sdl_texture_pitch,
sdl_event_pump, sdl_event_pump,
texture_pixels, texture_pixels,
audio,
video: framebuffer, video: framebuffer,
palette, palette,
font, font,
@ -267,6 +295,7 @@ impl SystemBuilder {
#[allow(dead_code)] #[allow(dead_code)]
pub struct System { pub struct System {
sdl_context: Sdl, sdl_context: Sdl,
sdl_audio_subsystem: AudioSubsystem,
sdl_video_subsystem: VideoSubsystem, sdl_video_subsystem: VideoSubsystem,
sdl_timer_subsystem: TimerSubsystem, sdl_timer_subsystem: TimerSubsystem,
sdl_canvas: WindowCanvas, sdl_canvas: WindowCanvas,
@ -280,6 +309,8 @@ pub struct System {
target_framerate_delta: Option<i64>, target_framerate_delta: Option<i64>,
next_tick: i64, next_tick: i64,
pub audio: sdl2::audio::AudioDevice<AudioDevice>,
/// The primary backbuffer [`Bitmap`] that will be rendered to the screen whenever /// 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 /// [`System::display`] is called. Regardless of the actual window size, this bitmap is always
/// [`SCREEN_WIDTH`]x[`SCREEN_HEIGHT`] pixels in size. /// [`SCREEN_WIDTH`]x[`SCREEN_HEIGHT`] pixels in size.

Binary file not shown.