diff --git a/Cargo.toml b/Cargo.toml index 065ad59..49f4123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ approx = "0.3" bitflags = "1" bitvec = "0.17.4" byteorder = "1.3" +cpal = "0.12.1" directories = "2" gfx = "0.18" gfx_core = "0.9" diff --git a/src/builtin/pixtone.pcm b/src/builtin/pixtone.pcm new file mode 100644 index 0000000..6bc97f5 Binary files /dev/null and b/src/builtin/pixtone.pcm differ diff --git a/src/builtin_fs.rs b/src/builtin_fs.rs index 1007353..d253431 100644 --- a/src/builtin_fs.rs +++ b/src/builtin_fs.rs @@ -110,6 +110,7 @@ impl BuiltinFS { FSNode::File("builtin_font.fnt", include_bytes!("builtin/builtin_font.fnt")), FSNode::File("builtin_font_0.png", include_bytes!("builtin/builtin_font_0.png")), FSNode::File("builtin_font_1.png", include_bytes!("builtin/builtin_font_1.png")), + FSNode::File("pixtone.pcm", include_bytes!("builtin/pixtone.pcm")), ]) ], } diff --git a/src/ggez/error.rs b/src/ggez/error.rs index 9feb314..d8a2de9 100644 --- a/src/ggez/error.rs +++ b/src/ggez/error.rs @@ -4,7 +4,7 @@ use std::error::Error; use std::fmt; use std::string::FromUtf8Error; -use std::sync::Arc; +use std::sync::{Arc, PoisonError}; /// An enum containing all kinds of game framework errors. #[derive(Debug, Clone)] @@ -232,3 +232,31 @@ impl From for GameError { GameError::ParseError(errstr) } } + +impl From for GameError { + fn from(s: cpal::DefaultStreamConfigError) -> GameError { + let errstr = format!("Default stream config error: {}", s); + GameError::AudioError(errstr) + } +} + +impl From for GameError { + fn from(s: cpal::PlayStreamError) -> GameError { + let errstr = format!("Play stream error: {}", s); + GameError::AudioError(errstr) + } +} + +impl From for GameError { + fn from(s: cpal::BuildStreamError) -> GameError { + let errstr = format!("Build stream error: {}", s); + GameError::AudioError(errstr) + } +} + +impl From> for GameError { + fn from(s: PoisonError) -> GameError { + let errstr = format!("Poison error: {}", s); + GameError::EventLoopError(errstr) + } +} diff --git a/src/main.rs b/src/main.rs index 1af8948..363f406 100644 --- a/src/main.rs +++ b/src/main.rs @@ -187,7 +187,7 @@ impl Game { texture_set: TextureSet::new(base_path), base_path: str!(base_path), stages: Vec::with_capacity(96), - sound_manager: SoundManager::new(ctx), + sound_manager: SoundManager::new(ctx)?, constants, scale, screen_size, diff --git a/src/sound/mod.rs b/src/sound/mod.rs index d17ed92..4247040 100644 --- a/src/sound/mod.rs +++ b/src/sound/mod.rs @@ -1,33 +1,204 @@ -use crate::ggez::{Context, GameResult}; +use std::sync::{mpsc, RwLock}; +use std::sync::mpsc::{Receiver, Sender}; + +use bitflags::_core::time::Duration; +use cpal::Sample; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + +use crate::engine_constants::EngineConstants; +use crate::ggez::{Context, filesystem, GameResult}; +use crate::ggez::GameError::AudioError; +use crate::sound::organya::Song; +use crate::sound::playback::PlaybackEngine; +use crate::sound::wave_bank::SoundBank; +use crate::str; pub mod pixtone; +mod wave_bank; +mod organya; +mod playback; +mod stuff; +mod wav; pub struct SoundManager { - intro: Vec, - sloop: Vec, + tx: Sender, + current_song_id: usize, } -//unsafe impl Send for SoundManager {} +static SONGS: [&'static str; 42] = [ + "XXXX", + "WANPAKU", + "ANZEN", + "GAMEOVER", + "GRAVITY", + "WEED", + "MDOWN2", + "FIREEYE", + "VIVI", + "MURA", + "FANFALE1", + "GINSUKE", + "CEMETERY", + "PLANT", + "KODOU", + "FANFALE3", + "FANFALE2", + "DR", + "ESCAPE", + "JENKA", + "MAZE", + "ACCESS", + "IRONH", + "GRAND", + "Curly", + "OSIDE", + "REQUIEM", + "WANPAK2", + "QUIET", + "LASTCAVE", + "BALCONY", + "LASTBTL", + "LASTBT3", + "ENDING", + "ZONBIE", + "BDOWN", + "HELL", + "JENKA2", + "MARINE", + "BALLOS", + "TOROKO", + "WHITE" +]; impl SoundManager { - pub fn new(ctx: &mut Context) -> SoundManager { - SoundManager { - intro: Vec::new(), - sloop: Vec::new(), - } + pub fn new(ctx: &mut Context) -> GameResult { + let (tx, rx): (Sender, Receiver) = mpsc::channel(); + + let host = cpal::default_host(); + let device = host.default_output_device().ok_or_else(|| AudioError(str!("Error initializing audio device.")))?; + let config = device.default_output_config()?; + + let bnk = wave_bank::SoundBank::load_from(filesystem::open(ctx, "/builtin/pixtone.pcm")?)?; + + std::thread::spawn(move || { + if let Err(err) = match config.sample_format() { + cpal::SampleFormat::F32 => run::(rx, bnk, &device, &config.into()), + cpal::SampleFormat::I16 => run::(rx, bnk, &device, &config.into()), + cpal::SampleFormat::U16 => run::(rx, bnk, &device, &config.into()), + } { + log::error!("Something went wrong in audio thread: {}", err); + } + }); + + Ok(SoundManager { + tx: tx.clone(), + current_song_id: 0, + }) } - pub fn play_song(&mut self, ctx: &mut Context) -> GameResult { - /*self.intro.clear(); - self.sloop.clear(); - ggez::filesystem::open(ctx, "/base/Ogg11/curly_intro.ogg")?.read_to_end(&mut self.intro)?; - ggez::filesystem::open(ctx, "/base/Ogg11/curly_loop.ogg")?.read_to_end(&mut self.sloop)?; + pub fn play_song(&mut self, song_id: usize, constants: &EngineConstants, ctx: &mut Context) -> GameResult { + if self.current_song_id == song_id { + return Ok(()); + } - let sink = Sink::new(ctx.audio_context.device()); - sink.append(rodio::Decoder::new(Cursor::new(self.intro.clone()))?); - sink.append(rodio::Decoder::new(Cursor::new(self.sloop.clone()))?); - sink.detach();*/ + if let Some(song_name) = SONGS.get(song_id) { + let org = organya::Song::load_from(filesystem::open(ctx, ["/base/Org/", &song_name.to_lowercase(), ".org"].join(""))?)?; + log::info!("Playing song: {}", song_name); + self.current_song_id = song_id; + self.tx.send(PlaybackMessage::PlaySong(org)); + } Ok(()) } } + +enum PlaybackMessage { + Stop, + PlaySong(Song), +} + +#[derive(PartialEq, Eq)] +enum PlaybackState { + Stopped, + Playing, +} + +fn run(rx: Receiver, bank: SoundBank, + device: &cpal::Device, config: &cpal::StreamConfig) -> GameResult where + T: cpal::Sample, +{ + let sample_rate = config.sample_rate.0 as f32; + let channels = config.channels as usize; + let mut state = PlaybackState::Stopped; + let mut new_song: Option = None; + let mut engine = PlaybackEngine::new(Song::empty(), &bank); + + log::info!("Audio format: {} {}", sample_rate, channels); + engine.set_sample_rate(sample_rate as usize); + engine.loops = usize::MAX; + + let mut buf = vec![0x8080; 441]; + let mut index = 0; + let mut frames = engine.render_to(&mut buf); + + let err_fn = |err| eprintln!("an error occurred on stream: {}", err); + + let stream = device.build_output_stream( + config, + move |data: &mut [T], _: &cpal::OutputCallbackInfo| { + match rx.try_recv() { + Ok(PlaybackMessage::PlaySong(song)) => { + engine.start_song(song, &bank); + + for i in &mut buf[0..frames] { *i = 0x8080 }; + frames = engine.render_to(&mut buf); + index = 0; + + state = PlaybackState::Playing; + } + _ => {} + } + + for frame in data.chunks_mut(channels) { + let sample: u16 = { + if state == PlaybackState::Stopped { + 0x8000 + } else if index < frames { + let sample = buf[index]; + index += 1; + if index & 1 == 0 { (sample & 0xff) << 8 } else { sample & 0xff00 } + } else { + for i in &mut buf[0..frames] { *i = 0x8080 }; + frames = engine.render_to(&mut buf); + index = 0; + let sample = buf[0]; + (sample & 0xff) << 8 + } + }; + + let value: T = cpal::Sample::from::(&sample); + for sample in frame.iter_mut() { + *sample = value; + } + } + }, + err_fn, + )?; + stream.play()?; + + loop { + std::thread::sleep(Duration::from_millis(10)); + } +} + +fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> u16) + where + T: cpal::Sample, +{ + for frame in output.chunks_mut(channels) { + let value: T = cpal::Sample::from::(&next_sample()); + for sample in frame.iter_mut() { + *sample = value; + } + } +} diff --git a/src/sound/organya.rs b/src/sound/organya.rs new file mode 100644 index 0000000..bb90c6b --- /dev/null +++ b/src/sound/organya.rs @@ -0,0 +1,225 @@ +#[repr(u8)] +#[derive(Debug, Copy, Clone)] +pub enum Version { + // Can't find any files with this signature, + // But apparently these files had no Pi flag. + Beta = b'1', + Main = b'2', + // OrgMaker 2.05 Extended Drums + Extended = b'3' +} + +#[derive(Debug, Copy, Clone)] +pub struct LoopRange { + // inclusive + pub start: i32, + // exclusive + pub end: i32 +} + +#[derive(Debug, Copy, Clone)] +pub struct Display { + pub beats: u8, + pub steps: u8 +} + +#[derive(Debug, Copy, Clone)] +pub struct Timing { + pub wait: u16, + pub loop_range: LoopRange +} + +#[derive(Copy, Clone)] +#[derive(Debug)] +pub struct Instrument { + pub freq: u16, + pub inst: u8, + pub pipi: u8, + pub notes: u16 +} + +pub struct Track { + pub inst: Instrument, + pub notes: Vec +} + +impl Clone for Track { + fn clone(&self) -> Track { + Track { + inst: self.inst, + notes: self.notes.clone(), + } + } +} + +impl std::fmt::Debug for Track { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.inst.fmt(f) + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct Note { + pub pos: i32, + pub key: u8, + pub len: u8, + pub vol: u8, + pub pan: u8 +} + +#[derive(Debug)] +pub struct Song { + pub version: Version, + pub time: Timing, + pub tracks: [Track; 16] +} + +impl Clone for Song { + fn clone(&self) -> Song { + Song { + version: self.version, + time: self.time, + tracks: self.tracks.clone(), + } + } +} + +use byteorder::{LE, ReadBytesExt}; +use std::io; + +impl Song { + pub fn empty() -> Song { + Song { + version: Version::Main, + time: Timing { wait: 8, loop_range: LoopRange { start: 0, end: 1 } }, + tracks: [ + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + ] + } + } + + pub fn load_from(mut f: R) -> io::Result { + let mut magic = [0; 6]; + + f.read_exact(&mut magic)?; + + let version = + match &magic { + b"Org-01" => Version::Beta, + b"Org-02" => Version::Main, + b"Org-03" => Version::Extended, + _ => return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid magic number")) + }; + + let wait = f.read_u16::()?; + let _bpm = f.read_u8()?; + let _spb = f.read_u8()?; + let start = f.read_i32::()?; + let end = f.read_i32::()?; + + use std::mem::MaybeUninit as Mu; + + let mut insts: [Mu; 16] = unsafe { + Mu::uninit().assume_init() + }; + + for i in insts.iter_mut() { + let freq = f.read_u16::()?; + let inst = f.read_u8()?; + let pipi = f.read_u8()?; + let notes = f.read_u16::()?; + + *i = Mu::new(Instrument { + freq, + inst, + pipi, + notes + }); + } + + let insts: [Instrument; 16] = unsafe { + std::mem::transmute(insts) + }; + + let mut tracks: [Mu; 16] = unsafe { + Mu::uninit().assume_init() + }; + + for (i, t) in tracks.iter_mut().enumerate() { + let count = insts[i].notes as usize; + + #[repr(C)] + #[derive(Copy, Clone)] + struct UninitNote { + pos: Mu, + key: Mu, + len: Mu, + vol: Mu, + pan: Mu + } + + let mut notes: Vec = unsafe { + vec![Mu::uninit().assume_init(); count] + }; + + for note in notes.iter_mut() { + note.pos = Mu::new(f.read_i32::()?); + } + + for note in notes.iter_mut() { + note.key = Mu::new(f.read_u8()?); + } + + for note in notes.iter_mut() { + note.len = Mu::new(f.read_u8()?); + } + + for note in notes.iter_mut() { + note.vol = Mu::new(f.read_u8()?); + } + + for note in notes.iter_mut() { + note.pan = Mu::new(f.read_u8()?); + } + + *t = Mu::new(Track { + inst: insts[i], + notes: unsafe { std::mem::transmute(notes) } + }); + } + + let tracks = unsafe { + std::mem::transmute(tracks) + }; + + let song = Song { + version, + time: Timing { + wait, + loop_range: LoopRange { + start, + end + } + }, + tracks + }; + + Ok(song) + } +} diff --git a/src/sound/playback.rs b/src/sound/playback.rs new file mode 100644 index 0000000..4c17f4b --- /dev/null +++ b/src/sound/playback.rs @@ -0,0 +1,512 @@ +use std::mem::MaybeUninit; + +use crate::sound::organya::Song as Organya; +use crate::sound::stuff::*; +use crate::sound::wav::*; +use crate::sound::wave_bank::SoundBank; + +pub struct PlaybackEngine { + song: Organya, + lengths: [u8; 8], + swaps: [usize; 8], + keys: [u8; 8], + track_buffers: [RenderBuffer; 136], + output_format: WavFormat, + play_pos: i32, + frames_this_tick: usize, + frames_per_tick: usize, + pub loops: usize, +} + +pub struct PlaybackState { + song: Organya, + play_pos: i32, +} + +impl PlaybackEngine { + pub fn new(song: Organya, samples: &SoundBank) -> Self { + + // Octave 0 Track 0 Swap 0 + // Octave 0 Track 1 Swap 0 + // ... + // Octave 1 Track 0 Swap 0 + // ... + // Octave 0 Track 0 Swap 1 + // octave * 8 + track + swap + // 128..136: Drum Tracks + let mut buffers: [MaybeUninit; 136] = unsafe { + MaybeUninit::uninit().assume_init() + }; + + // track + for i in 0..8 { + let sound_index = song.tracks[i].inst.inst as usize; + + // WAVE100 uses 8-bit signed audio, but wav audio wants 8-bit unsigned. + // On 2s complement system, we can simply flip the top bit + // No need to cast to u8 here because the sound bank data is one big &[u8]. + let sound = samples.get_wave(sound_index) + .iter() + .map(|&x| x ^ 128) + .collect(); + + let format = WavFormat { channels: 1, sample_rate: 22050, bit_depth: 8 }; + + let rbuf = RenderBuffer::new_organya(WavSample { format, data: sound }); + + // octave + for j in 0..8 { + // swap + for &k in &[0, 64] { + buffers[i + (j * 8) + k] = MaybeUninit::new(rbuf.clone()); + } + } + } + + for (inst, buf) in song.tracks[8..].iter().zip(buffers[128..].iter_mut()) { + *buf = + MaybeUninit::new( + RenderBuffer::new( + // FIXME: *frustrated screaming* + samples.samples[inst.inst.inst as usize].clone() + ) + ); + } + + let frames_per_tick = (44100 / 1000) * song.time.wait as usize; + + PlaybackEngine { + song, + lengths: [0; 8], + swaps: [0; 8], + keys: [255; 8], + track_buffers: unsafe { std::mem::transmute(buffers) }, + play_pos: 0, + output_format: WavFormat { + channels: 2, + sample_rate: 44100, + bit_depth: 16, + }, + frames_this_tick: 0, + frames_per_tick, + loops: 1, + } + } + + pub fn set_sample_rate(&mut self, sample_rate: usize) { + self.output_format.sample_rate = sample_rate as u32; + self.frames_per_tick = (sample_rate / 1000) * self.song.time.wait as usize; + } + + pub fn get_state(&self) -> PlaybackState { + PlaybackState { + song: self.song.clone(), + play_pos: self.play_pos, + } + } + + pub fn set_state(&mut self, state: PlaybackState, samples: &SoundBank) { + self.start_song(state.song, samples); + self.play_pos = state.play_pos; + } + + pub fn start_song(&mut self, song: Organya, samples: &SoundBank) { + for i in 0..8 { + let sound_index = song.tracks[i].inst.inst as usize; + let sound = samples.get_wave(sound_index) + .iter() + .map(|&x| x ^ 128) + .collect(); + + let format = WavFormat { channels: 1, sample_rate: 22050, bit_depth: 8 }; + + let rbuf = RenderBuffer::new_organya(WavSample { format, data: sound }); + + for j in 0..8 { + for &k in &[0, 64] { + self.track_buffers[i + (j * 8) + k] = rbuf.clone(); + } + } + } + + for (inst, buf) in song.tracks[8..].iter().zip(self.track_buffers[128..].iter_mut()) { + *buf = RenderBuffer::new(samples.samples[inst.inst.inst as usize].clone()); + } + + self.song = song; + self.play_pos = 0; + self.frames_per_tick = (self.output_format.sample_rate as usize / 1000) * self.song.time.wait as usize; + self.frames_this_tick = 0; + for i in self.lengths.iter_mut() { *i = 0 }; + for i in self.swaps.iter_mut() { *i = 0 }; + for i in self.keys.iter_mut() { *i = 255 }; + } + + #[allow(unused)] + pub fn set_position(&mut self, position: i32) { + self.play_pos = position; + } + + pub fn get_total_samples(&self) -> u32 { + let ticks_intro = self.song.time.loop_range.start; + let ticks_loop = self.song.time.loop_range.end - self.song.time.loop_range.start; + let ticks_total = ticks_intro + ticks_loop + (ticks_loop * self.loops as i32); + + self.frames_per_tick as u32 * ticks_total as u32 + } + + fn update_play_state(&mut self) { + for track in 0..8 { + if let Some(note) = + self.song.tracks[track].notes.iter().find(|x| x.pos == self.play_pos) { + // New note + //eprintln!("{:?}", &self.keys); + if note.key != 255 { + if self.keys[track] == 255 { // New + let octave = (note.key / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + for k in 0..16 { + let swap = if k >= 8 { 64 } else { 0 }; + let key = note.key % 12; + let p_oct = k % 8; + + let freq = org_key_to_freq(key + p_oct * 12, self.song.tracks[track].inst.freq as i16); + + let l = p_oct as usize * 8 + track + swap; + self.track_buffers[l].set_frequency(freq as u32); + self.track_buffers[l].organya_select_octave(p_oct as usize, self.song.tracks[track].inst.pipi != 0); + } + self.track_buffers[j].looping = true; + self.track_buffers[j].playing = true; + // last playing key + self.keys[track] = note.key; + } else if self.keys[track] == note.key { // Same + //assert!(self.lengths[track] == 0); + let octave = (self.keys[track] / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + if self.song.tracks[track].inst.pipi == 0 { + self.track_buffers[j].looping = false; + } + self.swaps[track] += 64; + self.swaps[track] %= 128; + let j = octave as usize + track + self.swaps[track]; + self.track_buffers[j].organya_select_octave(note.key as usize / 12, self.song.tracks[track].inst.pipi != 0); + self.track_buffers[j].looping = true; + self.track_buffers[j].playing = true; + } else { // change + let octave = (self.keys[track] / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + if self.song.tracks[track].inst.pipi == 0 { + self.track_buffers[j].looping = false; + } + self.swaps[track] += 64; + self.swaps[track] %= 128; + let octave = (note.key / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + for k in 0..16 { + let swap = if k >= 8 { 64 } else { 0 }; + let key = note.key % 12; + let p_oct = k % 8; + + let freq = org_key_to_freq(key + p_oct * 12, self.song.tracks[track].inst.freq as i16); + let l = p_oct as usize * 8 + track + swap; + self.track_buffers[l].set_frequency(freq as u32); + self.track_buffers[l].organya_select_octave(p_oct as usize, self.song.tracks[track].inst.pipi != 0); + } + self.track_buffers[j].looping = true; + self.track_buffers[j].playing = true; + self.keys[track] = note.key; + } + + self.lengths[track] = note.len; + } + + if self.keys[track] != 255 { + let octave = (self.keys[track] / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + + if note.vol != 255 { + let vol = org_vol_to_vol(note.vol); + self.track_buffers[j].set_volume(vol); + } + + if note.pan != 255 { + let pan = org_pan_to_pan(note.pan); + self.track_buffers[j].set_pan(pan); + } + } + } + + if self.lengths[track] == 0 { + if self.keys[track] != 255 { + let octave = (self.keys[track] / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + if self.song.tracks[track].inst.pipi == 0 { + self.track_buffers[j].looping = false; + } + self.keys[track] = 255; + } + } + + self.lengths[track] = self.lengths[track].saturating_sub(1); + } + + for i in 8..16 { + let j = i + 120; + + let notes = &self.song.tracks[i].notes; + + // start a new note + // note (hah) that drums are unaffected by length and pi values. This is the only case we have to handle. + if let Some(note) = + notes.iter().find(|x| x.pos == self.play_pos) { + + // FIXME: Add constants for dummy values + if note.key != 255 { + let freq = org_key_to_drum_freq(note.key); + self.track_buffers[j].set_frequency(freq as u32); + self.track_buffers[j].set_position(0); + self.track_buffers[j].playing = true; + } + + if note.vol != 255 { + let vol = org_vol_to_vol(note.vol); + self.track_buffers[j].set_volume(vol); + } + + if note.pan != 255 { + let pan = org_pan_to_pan(note.pan); + self.track_buffers[j].set_pan(pan); + } + } + } + } + + pub fn render_to(&mut self, buf: &mut [u16]) -> usize { + for (i, frame) in buf.iter_mut().enumerate() { + if self.frames_this_tick == 0 { + self.update_play_state() + } + + mix(std::slice::from_mut(frame), self.output_format, &mut self.track_buffers); + + self.frames_this_tick += 1; + + if self.frames_this_tick == self.frames_per_tick { + self.play_pos += 1; + + if self.play_pos == self.song.time.loop_range.end { + self.play_pos = self.song.time.loop_range.start; + + if self.loops == 0 { + return i + 1; + } + + self.loops -= 1; + } + + self.frames_this_tick = 0; + } + } + + buf.len() + } +} + +// TODO: Create a MixingBuffer or something... +fn mix(dst: &mut [u16], dst_fmt: WavFormat, srcs: &mut [RenderBuffer]) { + let freq = dst_fmt.sample_rate as f64; + + for buf in srcs { + if buf.playing { + // index into sound samples + let advance = buf.frequency as f64 / freq; + + let vol = centibel_to_scale(buf.volume); + + let (pan_l, pan_r) = + match buf.pan.signum() { + 0 => (1.0, 1.0), + 1 => (centibel_to_scale(-buf.pan), 1.0), + -1 => (1.0, centibel_to_scale(buf.pan)), + _ => unsafe { std::hint::unreachable_unchecked() } + }; + + fn clamp(v: T, limit: T) -> T { + if v > limit { + limit + } else { + v + } + } + + // s1: sample 1 + // s2: sample 2 + // sp: previous sample (before s1) + // sn: next sample (after s2) + // mu: position to interpolate for + fn cubic_interp(s1: f32, s2: f32, sp: f32, sn: f32, mu: f32) -> f32 { + let mu2 = mu * mu; + let a0 = sn - s2 - sp + s1; + let a1 = sp - s1 - a0; + let a2 = s2 - sp; + let a3 = s1; + + a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3 + } + + #[allow(unused_variables)] + + for frame in dst.iter_mut() { + let pos = buf.position as usize + buf.base_pos; + // -1..1 + let s1 = (buf.sample.data[pos] as f32 - 128.0) / 128.0; + let s2 = (buf.sample.data[clamp(pos + 1, buf.base_pos + buf.len - 1)] as f32 - 128.0) / 128.0; + let s3 = (buf.sample.data[clamp(pos + 2, buf.base_pos + buf.len - 1)] as f32 - 128.0) / 128.0; + let s4 = (buf.sample.data[pos.saturating_sub(1)] as f32 - 128.0) / 128.0; + + use std::f32::consts::PI; + + let r1 = buf.position.fract() as f32; + let r2 = (1.0 - f32::cos(r1 * PI)) / 2.0; + + //let s = s1; // No interp + //let s = s1 + (s2 - s1) * r1; // Linear interp + //let s = s1 * (1.0 - r2) + s2 * r2; // Cosine interp + let s = cubic_interp(s1, s2, s4, s3, r1); // Cubic interp + // Ideally we want sinc/lanczos interpolation, since that's what DirectSound appears to use. + + // -128..128 + let sl = s * pan_l * vol * 128.0; + let sr = s * pan_r * vol * 128.0; + + buf.position += advance; + + if buf.position as usize >= buf.len { + if buf.looping && buf.nloops != 1 { + buf.position %= buf.len as f64; + if buf.nloops != -1 { + buf.nloops -= 1; + } + } else { + buf.position = 0.0; + buf.playing = false; + break; + } + } + + let [mut l, mut r] = frame.to_le_bytes(); + // -128..127 + let xl = (l ^ 128) as i8; + let xr = (r ^ 128) as i8; + + // 0..255 + l = xl.saturating_add(sl as i8) as u8 ^ 128; + r = xr.saturating_add(sr as i8) as u8 ^ 128; + + *frame = u16::from_le_bytes([l, r]); + } + } + } +} + +pub fn centibel_to_scale(a: i32) -> f32 { + f32::powf(10.0, a as f32 / 2000.0) +} + +#[derive(Clone)] +pub struct RenderBuffer { + pub position: f64, + pub frequency: u32, + pub volume: i32, + pub pan: i32, + pub sample: WavSample, + pub playing: bool, + pub looping: bool, + pub base_pos: usize, + pub len: usize, + // -1 = infinite + pub nloops: i32, +} + +impl RenderBuffer { + pub fn new(sample: WavSample) -> RenderBuffer { + RenderBuffer { + position: 0.0, + frequency: sample.format.sample_rate, + volume: 0, + pan: 0, + len: sample.data.len(), + sample, + playing: false, + looping: false, + base_pos: 0, + nloops: -1, + } + } + + pub fn new_organya(mut sample: WavSample) -> RenderBuffer { + let wave = sample.data.clone(); + sample.data.clear(); + + for size in &[256_usize, 256, 128, 128, 64, 32, 16, 8] { + let step = 256 / size; + let mut acc = 0; + + for _ in 0..*size { + sample.data.push(wave[acc]); + acc += step; + + if acc >= 256 { + acc = 0; + } + } + } + + RenderBuffer::new(sample) + } + + #[inline] + pub fn organya_select_octave(&mut self, octave: usize, pipi: bool) { + const OFFS: &[usize] = &[0x000, 0x100, + 0x200, 0x280, + 0x300, 0x340, + 0x360, 0x370]; + const LENS: &[usize] = &[256_usize, 256, 128, 128, 64, 32, 16, 8]; + self.base_pos = OFFS[octave]; + self.len = LENS[octave]; + self.position %= self.len as f64; + if pipi && !self.playing { + self.nloops = ((octave + 1) * 4) as i32; + } + } + + #[inline] + pub fn set_frequency(&mut self, frequency: u32) { + //assert!(frequency >= 100 && frequency <= 100000); + //dbg!(frequency); + self.frequency = frequency; + } + + #[inline] + pub fn set_volume(&mut self, volume: i32) { + assert!(volume >= -10000 && volume <= 0); + + self.volume = volume; + } + + #[inline] + pub fn set_pan(&mut self, pan: i32) { + assert!(pan >= -10000 && pan <= 10000); + + self.pan = pan; + } + + #[inline] + #[allow(unused)] + pub fn set_position(&mut self, position: u32) { + assert!(position < self.sample.data.len() as u32 / self.sample.format.bit_depth as u32); + + self.position = position as f64; + } +} diff --git a/src/sound/stuff.rs b/src/sound/stuff.rs new file mode 100644 index 0000000..6285f58 --- /dev/null +++ b/src/sound/stuff.rs @@ -0,0 +1,36 @@ +pub const FRQ_TBL: [i16; 12] = [ + 261,278,294,311,329,349,371,391,414,440,466,494 +]; + +pub const PAN_TBL: [i16; 13] = [ + 0,43,86,129,172,215,256,297,340,383,426,469,512 +]; + +pub const OCT_TBL: [i16; 8] = [ + 32,64,64,128,128,128,128,128 +]; + +pub fn org_key_to_freq(key: u8, a: i16) -> i32 { + let (oct, pitch) = org_key_to_oct_pitch(key); + + let freq = FRQ_TBL[pitch as usize] as f32; + let oct = OCT_TBL[oct as usize] as f32; + + (freq * oct) as i32 + (a as i32 - 1000) +} + +pub fn org_key_to_drum_freq(key: u8) -> i32 { + key as i32 * 800 + 100 +} + +pub fn org_pan_to_pan(pan: u8) -> i32 { + (PAN_TBL[pan as usize] as i32 - 256) * 10 +} + +pub fn org_vol_to_vol(vol: u8) -> i32 { + (vol as i32 - 255) * 8 +} + +pub fn org_key_to_oct_pitch(key: u8) -> (u8, u8) { + (key/12, key%12) +} diff --git a/src/sound/wav.rs b/src/sound/wav.rs new file mode 100644 index 0000000..2fe9333 --- /dev/null +++ b/src/sound/wav.rs @@ -0,0 +1,120 @@ +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct RiffChunk { + id: [u8; 4], + length: u32 +} + +use std::fmt; + +impl fmt::Display for RiffChunk { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use std::ascii::escape_default as esc; + + write!(f, "chunk \"{}{}{}{}\", length: {}", + esc(self.id[0]), + esc(self.id[1]), + esc(self.id[2]), + esc(self.id[3]), + self.length + ) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct WavFormat { + pub channels: u16, + pub sample_rate: u32, + pub bit_depth: u16 +} + +impl fmt::Display for WavFormat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} channels, {} Hz, {}-bit", + self.channels, + self.sample_rate, + self.bit_depth + ) + } +} + +#[derive(Clone)] +pub struct WavSample { + pub format: WavFormat, + pub data: Vec +} + +impl fmt::Display for WavSample { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}, {} samples", + self.format, + // num_bytes / bytes_per_sample + self.data.len() / ((self.format.bit_depth / 8) * self.format.channels) as usize + ) + } +} + +use byteorder::{LE, ReadBytesExt}; +use std::io; + +impl RiffChunk { + pub fn read_from(mut f: R) -> io::Result { + let mut id = [0; 4]; + + f.read_exact(&mut id)?; + let length = f.read_u32::()?; + + Ok(RiffChunk { id, length }) + } +} + +impl WavSample { + pub fn read_from(mut f: R) -> io::Result { + let riff = RiffChunk::read_from(&mut f)?; + + match &riff.id { + b"RIFF" => {}, + b"RIFX" => panic!("Cannot handle RIFX data!"), + _ => panic!("Expected RIFF signature, found {}", riff) + } + + let mut rfmt = [0; 4]; + + f.read_exact(&mut rfmt)?; + + assert_eq!(rfmt, *b"WAVE"); + + let fmt = RiffChunk::read_from(&mut f)?; + + assert_eq!(fmt.id, *b"fmt "); + //assert_eq!(fmt.length, 16); + + let afmt = f.read_u16::()?; + + debug_assert!(afmt == 1); + + let channels = f.read_u16::()?; + let samples = f.read_u32::()?; + let _brate = f.read_u32::()?; + let _balgn = f.read_u16::()?; + let bits = f.read_u16::()?; + + let data = RiffChunk::read_from(&mut f)?; + + assert_eq!(data.id, *b"data"); + + let mut buf = vec![0; data.length as usize]; + + f.read_exact(&mut buf)?; + + Ok( + WavSample { + format: WavFormat { + channels, + sample_rate: samples, + bit_depth: bits + }, + data: buf + } + ) + } +} diff --git a/src/sound/wave_bank.rs b/src/sound/wave_bank.rs new file mode 100644 index 0000000..c32f142 --- /dev/null +++ b/src/sound/wave_bank.rs @@ -0,0 +1,45 @@ +use crate::sound::wav; + +use std::io; +use std::fmt; + +pub struct SoundBank { + // FIXME: would prefer Box<[u8; 25600]> + pub wave100: Box<[u8]>, + + pub samples: Vec +} + +impl fmt::Display for SoundBank { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "WAVE100: {:2X?}...", &self.wave100[..8])?; + + for sample in self.samples.iter() { + writeln!(f, "{}", sample)?; + } + + Ok(()) + } +} + +impl SoundBank { + pub fn load_from(mut f: R) -> io::Result { + // no box [0; 25600] yet + let mut wave100 = vec![0; 25600].into_boxed_slice(); + + f.read_exact(&mut *wave100)?; + + let mut samples = Vec::with_capacity(16); + + loop { + match wav::WavSample::read_from(&mut f) { + Ok(sample) => samples.push(sample), + Err(_) => return Ok(SoundBank { wave100, samples }) + } + } + } + + pub fn get_wave(&self, index: usize) -> &[u8] { + &self.wave100[index*256..(index+1)*256] + } +}