add initial audio playback and wav file support
This commit is contained in:
parent
bc03961a49
commit
342e2a3877
271
libretrogd/src/audio/mod.rs
Normal file
271
libretrogd/src/audio/mod.rs
Normal 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
270
libretrogd/src/audio/wav.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
BIN
libretrogd/test-assets/22khz_8bit_1ch.wav
Normal file
BIN
libretrogd/test-assets/22khz_8bit_1ch.wav
Normal file
Binary file not shown.
Loading…
Reference in a new issue