diff options
| -rw-r--r-- | assets/asm/explore_harmony1.asm | 90 | ||||
| -rw-r--r-- | assets/asm/explore_harmony2.asm | 14 | ||||
| -rw-r--r-- | assets/asm/explore_melody.asm | 32 | ||||
| -rw-r--r-- | assets/asm/megalovania_base.asm | 26 | ||||
| -rw-r--r-- | assets/asm/megalovania_melody.asm | 26 | ||||
| -rw-r--r-- | assets/wav/noise_0.wav | bin | 88244 -> 0 bytes | |||
| -rw-r--r-- | assets/wav/noise_1.wav | bin | 88244 -> 0 bytes | |||
| -rw-r--r-- | assets/wav/pulse_12.wav | bin | 244 -> 0 bytes | |||
| -rw-r--r-- | assets/wav/pulse_25.wav | bin | 244 -> 0 bytes | |||
| -rw-r--r-- | assets/wav/pulse_50.wav | bin | 244 -> 0 bytes | |||
| -rw-r--r-- | assets/wav/pulse_75.wav | bin | 244 -> 0 bytes | |||
| -rw-r--r-- | assets/wav/triangle.wav | bin | 244 -> 0 bytes | |||
| -rw-r--r-- | audio/src/channel.rs | 279 | ||||
| -rw-r--r-- | audio/src/data.rs | 12 | ||||
| -rw-r--r-- | audio/src/lib.rs | 71 | ||||
| -rw-r--r-- | audio/src/parse/lex.rs | 218 | ||||
| -rw-r--r-- | audio/src/parse/lexer.rs | 264 | ||||
| -rw-r--r-- | audio/src/parse/macros.rs | 214 | ||||
| -rw-r--r-- | audio/src/parse/mod.rs | 32 | ||||
| -rw-r--r-- | audio/src/parse/parser.rs | 232 | ||||
| -rw-r--r-- | audio/src/parse/pos.rs | 83 | ||||
| -rw-r--r-- | audio/src/parse/util.rs | 42 | ||||
| -rw-r--r-- | audio/src/program.rs | 138 | ||||
| -rw-r--r-- | game/src/main.rs | 8 |
24 files changed, 1062 insertions, 719 deletions
diff --git a/assets/asm/explore_harmony1.asm b/assets/asm/explore_harmony1.asm index d313de2..019f07e 100644 --- a/assets/asm/explore_harmony1.asm +++ b/assets/asm/explore_harmony1.asm @@ -3,61 +3,61 @@ ; setup P5 -a d12 +a w12 -%define first +%macro first -- -a v50 -a pe3 -- -a pa4 -- -a pc4 -- -a pe4 -- -a pc4 -- -a pa4 -- -a pe3 -- -a v0 -%end +a v 50 +a p e3 -- +a p a4 -- +a p c4 -- +a p e4 -- +a p c4 -- +a p a4 -- +a p e3 -- +a v 0 +%endmacro -%define second +%macro second -- -a v50 -a pe3 -- -a pg3 -- -a pb4 -- -a pe4 -- -a pb4 -- -a pg3 -- -a pe3 -- -a v0 -%end +a v 50 +a p e3 -- +a p g3 -- +a p b4 -- +a p e4 -- +a p b4 -- +a p g3 -- +a p e3 -- +a v 0 +%endmacro -%define third +%macro third -- -a v50 -a pe3 v50 -- -a pg3 v50 -- -a pc4 v50 -- -a pe4 v50 -- -a pc4 v50 -- -a pg3 v50 -- -a pe3 v50 -- -a v0 -%end +a v 50 +a p e3 v 50 -- +a p g3 v 50 -- +a p c4 v 50 -- +a p e4 v 50 -- +a p c4 v 50 -- +a p g3 v 50 -- +a p e3 v 50 -- +a v 0 +%endmacro -%define fourth +%macro fourth -- a v50 -a pd3 v50 -- -a pg3 v50 -- -a pb4 v50 -- -a pd4 v50 -- -a pb4 v50 -- -a pg3 v50 -- -a pd3 v50 -- +a p d3 v 50 -- +a p g3 v 50 -- +a p b4 v 50 -- +a p d4 v 50 -- +a p b4 v 50 -- +a p g3 v 50 -- +a p d3 v 50 -- a v0 -%end +%endmacro -%define notes +%macro notes first first second @@ -66,7 +66,7 @@ third third fourth fourth -%end +%endmacro notes notes diff --git a/assets/asm/explore_harmony2.asm b/assets/asm/explore_harmony2.asm index 9046fbb..e1560cf 100644 --- a/assets/asm/explore_harmony2.asm +++ b/assets/asm/explore_harmony2.asm @@ -3,13 +3,13 @@ ; setup P5 -t v50 +t v 50 -%define note -t p$1 -32 -%end +%macro note +t p $1 -32 +%endmacro -%define notes +%macro notes note a5 note b5 note c5 @@ -18,9 +18,7 @@ note b5b note a5 note b5b note b5 -%end +%endmacro notes notes -notes -notes diff --git a/assets/asm/explore_melody.asm b/assets/asm/explore_melody.asm index 4642395..23c0ff9 100644 --- a/assets/asm/explore_melody.asm +++ b/assets/asm/explore_melody.asm @@ -5,37 +5,37 @@ -256 ; setup -b v100 d50 +b v100 w50 -%define rest +%macro rest b v0 -1 b v100 -%end +%endmacro -%define erest +%macro erest b v0 -4 b v100 -%end +%endmacro -%define snote +%macro snote b p$1 -2 -%end +%endmacro -%define enote +%macro enote b p$1 -4 -%end +%endmacro -%define qnote +%macro qnote b p$1 -8 -%end +%endmacro -%define hnote +%macro hnote b p$1 -16 -%end +%endmacro -%define cnote +%macro cnote b p$1 -$2 -%end +%endmacro qnote a4 qnote e3 @@ -78,3 +78,5 @@ cnote b4 11 enote c4 qnote b4 qnote g3 + +b v0 diff --git a/assets/asm/megalovania_base.asm b/assets/asm/megalovania_base.asm index 11fdf1f..4f4f2bf 100644 --- a/assets/asm/megalovania_base.asm +++ b/assets/asm/megalovania_base.asm @@ -6,20 +6,20 @@ P4 -128 ; setup -b v100 d50 +b v 100 w 50 -%define notes -b p$1 v100 -- b v0 -- -b p$1 v100 -- b v0 -- -b p$1 v100 - b v0 - -b p$1 v100 - b v0 - -- -b p$2 v100 - b v0 - -- -b p$2 v100 - b v0 - -- -b p$2 v100 - b v0 - -b p$2 v100 - b v0 - -b p$2 v100 - b v0 - -b p$2 v100 -- b v0 -- -%end +%macro notes +b p $1 v 100 -- b v 0 -- +b p $1 v 100 -- b v 0 -- +b p $1 v 100 - b v 0 - +b p $1 v 100 - b v 0 - -- +b p $2 v 100 - b v 0 - -- +b p $2 v 100 - b v 0 - -- +b p $2 v 100 - b v 0 - +b p $2 v 100 - b v 0 - +b p $2 v 100 - b v 0 - +b p $2 v 100 -- b v 0 -- +%endmacro notes d2 d2 notes c2 c2 diff --git a/assets/asm/megalovania_melody.asm b/assets/asm/megalovania_melody.asm index 237f301..9644550 100644 --- a/assets/asm/megalovania_melody.asm +++ b/assets/asm/megalovania_melody.asm @@ -2,21 +2,21 @@ ; track: melody ; setup -a v100 d50 +a v 100 w 50 P4 -%define notes -a p$1 v100 - a v0 - -a p$1 v100 - a v0 - -a pd4 v100 -- a v0 -- -a pa4 v100 -- a v0 -- -- -a pg3# v100 - a v0 - -- -a pg3 v100 - a v0 - -- -a pf3 v100 --- a v0 - -a pd3 v100 - a v0 - -a pf3 v100 - a v0 - -a pg3 v100 - a v0 - -%end +%macro notes +a p $1 v 100 - a v 0 - +a p $1 v 100 - a v 0 - +a p d4 v 100 -- a v 0 -- +a p a4 v 100 -- a v 0 -- -- +a p g3# v 100 - a v 0 - -- +a p g3 v 100 - a v 0 - -- +a p f3 v 100 --- a v 0 - +a p d3 v 100 - a v 0 - +a p f3 v 100 - a v 0 - +a p g3 v 100 - a v 0 - +%endmacro notes d3 notes c3 diff --git a/assets/wav/noise_0.wav b/assets/wav/noise_0.wav Binary files differdeleted file mode 100644 index dc1d2db..0000000 --- a/assets/wav/noise_0.wav +++ /dev/null diff --git a/assets/wav/noise_1.wav b/assets/wav/noise_1.wav Binary files differdeleted file mode 100644 index c20882f..0000000 --- a/assets/wav/noise_1.wav +++ /dev/null diff --git a/assets/wav/pulse_12.wav b/assets/wav/pulse_12.wav Binary files differdeleted file mode 100644 index 0127fbb..0000000 --- a/assets/wav/pulse_12.wav +++ /dev/null diff --git a/assets/wav/pulse_25.wav b/assets/wav/pulse_25.wav Binary files differdeleted file mode 100644 index 0bc51c7..0000000 --- a/assets/wav/pulse_25.wav +++ /dev/null diff --git a/assets/wav/pulse_50.wav b/assets/wav/pulse_50.wav Binary files differdeleted file mode 100644 index 83e4676..0000000 --- a/assets/wav/pulse_50.wav +++ /dev/null diff --git a/assets/wav/pulse_75.wav b/assets/wav/pulse_75.wav Binary files differdeleted file mode 100644 index 11f7364..0000000 --- a/assets/wav/pulse_75.wav +++ /dev/null diff --git a/assets/wav/triangle.wav b/assets/wav/triangle.wav Binary files differdeleted file mode 100644 index 0c9bb9e..0000000 --- a/assets/wav/triangle.wav +++ /dev/null diff --git a/audio/src/channel.rs b/audio/src/channel.rs index 7ed61a6..fa0bbbd 100644 --- a/audio/src/channel.rs +++ b/audio/src/channel.rs @@ -1,199 +1,138 @@ -use raylib::audio::RaylibAudio; +#![expect(clippy::cast_possible_truncation)] -type Music = raylib::audio::Music<'static>; +use std::sync::{Arc, Mutex}; -macro_rules! load_audio { - ($handle:expr, $filepath:expr) => {{ - let mut audio = if cfg!(any(feature = "static", target_arch = "wasm32")) { - let bytes = include_bytes!(concat!("../../", $filepath)); - let vec = Vec::from(bytes); - $handle.new_music_from_memory(".wav", &vec)? - } else { - $handle.new_music($filepath)? - }; - audio.looping = true; - audio - }}; -} +use raylib::{ + audio::{AudioStream, RaylibAudio}, + prelude::audio_stream_callback::set_audio_stream_callback, +}; -pub trait Channel { - fn stop(&self); - fn play(&self); - fn update(&self); - fn is_playing(&self) -> bool; - fn set_volume(&self, volume: f32); - fn set_pitch(&self, pitch: f32); -} +const SAMPLE_RATE: u32 = 44100; +const SAMPLE_SIZE: u32 = i16::BITS; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DutyCycle { - Percent12, - Percent25, - Percent50, - Percent25Neg, +#[derive(Debug, Clone, Copy)] +pub enum ChannelKind { + Pulse { duty_cycle: u8 }, + Triangle, + Noise { mode: bool, lsr: i16 }, } - -pub struct PulseChannel { - pulse_12: Music, - pulse_25: Music, - pulse_50: Music, - pulse_75: Music, - duty: DutyCycle, -} -impl PulseChannel { - pub fn load(handle: &'static RaylibAudio) -> crate::Result<Self> { - Ok(Self { - pulse_12: load_audio!(handle, "assets/wav/pulse_12.wav"), - pulse_25: load_audio!(handle, "assets/wav/pulse_25.wav"), - pulse_50: load_audio!(handle, "assets/wav/pulse_50.wav"), - pulse_75: load_audio!(handle, "assets/wav/pulse_50.wav"), - duty: DutyCycle::Percent50, - }) +impl ChannelKind { + pub const fn pulse() -> Self { + Self::Pulse { duty_cycle: 50 } } - pub fn set_duty(&mut self, duty: DutyCycle) { - self.duty = duty; - if self.is_playing() { - self.stop(); - self.play(); - } - } -} -impl Channel for PulseChannel { - fn stop(&self) { - self.pulse_12.stop_stream(); - self.pulse_25.stop_stream(); - self.pulse_50.stop_stream(); - self.pulse_75.stop_stream(); + pub const fn triangle() -> Self { + Self::Triangle } - fn play(&self) { - use DutyCycle as D; - match self.duty { - D::Percent12 => self.pulse_12.play_stream(), - D::Percent25 => self.pulse_25.play_stream(), - D::Percent50 => self.pulse_50.play_stream(), - D::Percent25Neg => self.pulse_75.play_stream(), + pub const fn noise() -> Self { + Self::Noise { + mode: false, + lsr: 1, } } - fn update(&self) { - self.pulse_12.update_stream(); - self.pulse_25.update_stream(); - self.pulse_50.update_stream(); - self.pulse_75.update_stream(); - } - - fn is_playing(&self) -> bool { - self.pulse_12.is_stream_playing() - || self.pulse_25.is_stream_playing() - || self.pulse_50.is_stream_playing() - || self.pulse_75.is_stream_playing() - } - - fn set_volume(&self, volume: f32) { - self.pulse_12.set_volume(volume); - self.pulse_25.set_volume(volume); - self.pulse_50.set_volume(volume); - self.pulse_75.set_volume(volume); - } - - fn set_pitch(&self, pitch: f32) { - self.pulse_12.set_pitch(pitch); - self.pulse_25.set_pitch(pitch); - self.pulse_50.set_pitch(pitch); - self.pulse_75.set_pitch(pitch); + fn sample(&mut self, phase: f32) -> i16 { + match self { + Self::Pulse { duty_cycle } => { + let duty = *duty_cycle as f32 / 100.0; + if phase < duty { i16::MAX } else { i16::MIN } + } + Self::Triangle => { + let steps = 32; + let step = (phase * steps as f32).floor() as u32; + let value = (((step as f32 / (steps - 1) as f32) * 2.0) - 0.5) * 2.0; + (value * i16::MAX as f32) as i16 + } + Self::Noise { mode, lsr } => { + let feedback = if *mode { + (*lsr & 1) ^ ((*lsr >> 1) & 1) + } else { + (*lsr & 1) ^ ((*lsr >> 6) & 1) + }; + *lsr >>= 1; + *lsr |= feedback << 14; + *lsr + } + } } } -pub struct TriangleChannel { - inner: Music, +#[derive(Debug)] +pub struct Channel { + pub volume: f32, + pub pitch: f32, + pub kind: ChannelKind, + phase: f32, } -impl TriangleChannel { - pub fn load(handle: &'static RaylibAudio) -> crate::Result<Self> { - Ok(Self { - inner: load_audio!(handle, "assets/wav/triangle.wav"), - }) - } -} -impl Channel for TriangleChannel { - fn stop(&self) { - self.inner.stop_stream(); - } - - fn play(&self) { - self.inner.play_stream(); - } - - fn update(&self) { - self.inner.update_stream(); - } - - fn is_playing(&self) -> bool { - self.inner.is_stream_playing() - } - - fn set_volume(&self, volume: f32) { - self.inner.set_volume(volume); +impl Channel { + const fn new(kind: ChannelKind) -> Self { + Self { + volume: 0.0, + pitch: 1.0, + kind, + phase: 0.0, + } } - fn set_pitch(&self, pitch: f32) { - self.inner.set_pitch(pitch); + fn sample(&mut self, buffer: &mut [i16]) { + let freq = self.pitch * 440.0; + let step = freq / SAMPLE_RATE as f32; + for sample in buffer { + self.phase += step; + if self.phase >= 1.0 { + self.phase -= 1.0; + } + let real_note = self.kind.sample(self.phase); + let note = (real_note as f32 * self.volume) as i16; + *sample += note / 4; + } } } -pub struct NoiseChannel { - noise_0: Music, - noise_1: Music, - mode: bool, +pub struct Channels { + pub pulse_a: Channel, + pub pulse_b: Channel, + pub triangle: Channel, + pub noise: Channel, } -impl NoiseChannel { - pub fn load(handle: &'static RaylibAudio) -> crate::Result<Self> { - Ok(Self { - noise_0: load_audio!(handle, "assets/wav/noise_0.wav"), - noise_1: load_audio!(handle, "assets/wav/noise_1.wav"), - mode: false, - }) - } - - pub fn set_mode(&mut self, mode: bool) { - self.mode = mode; - if self.is_playing() { - self.stop(); - self.play(); +impl Channels { + const fn new() -> Self { + let pulse_a = Channel::new(ChannelKind::pulse()); + let pulse_b = Channel::new(ChannelKind::pulse()); + let triangle = Channel::new(ChannelKind::triangle()); + let noise = Channel::new(ChannelKind::noise()); + Self { + pulse_a, + pulse_b, + triangle, + noise, } } } -impl Channel for NoiseChannel { - fn stop(&self) { - self.noise_0.stop_stream(); - self.noise_1.stop_stream(); - } - - fn play(&self) { - if self.mode { - self.noise_1.play_stream(); - } else { - self.noise_0.play_stream(); - } - } - - fn update(&self) { - self.noise_0.update_stream(); - self.noise_0.update_stream(); - } - - fn is_playing(&self) -> bool { - self.noise_0.is_stream_playing() || self.noise_1.is_stream_playing() - } - fn set_volume(&self, volume: f32) { - self.noise_0.set_volume(volume); - self.noise_1.set_volume(volume); - } +pub struct Device<'s> { + pub channels: Arc<Mutex<Channels>>, + #[expect(dead_code)] + stream: AudioStream<'s>, +} - fn set_pitch(&self, pitch: f32) { - self.noise_0.set_pitch(pitch); - self.noise_1.set_pitch(pitch); +#[expect(clippy::unwrap_used)] +impl<'s> Device<'s> { + pub(crate) fn load(handle: &'s RaylibAudio) -> crate::Result<Self> { + let channels = Arc::new(Mutex::new(Channels::new())); + let stream = handle.new_audio_stream(SAMPLE_RATE, SAMPLE_SIZE, 1); + let cb_data = Arc::clone(&channels); + set_audio_stream_callback(&stream, move |buffer| { + let mut state = cb_data.lock().unwrap(); + state.pulse_a.sample(buffer); + state.pulse_b.sample(buffer); + state.triangle.sample(buffer); + state.noise.sample(buffer); + })?; + stream.set_volume(1.0); + stream.set_pitch(1.0); + stream.play(); + Ok(Self { channels, stream }) } } diff --git a/audio/src/data.rs b/audio/src/data.rs index e63254d..9ed8019 100644 --- a/audio/src/data.rs +++ b/audio/src/data.rs @@ -3,13 +3,17 @@ use std::fs; macro_rules! load_asm { ($path:tt) => {{ - if cfg!(any(feature = "static", target_arch = "wasm32")) { + let res = if cfg!(any(feature = "static", target_arch = "wasm32")) { let src = include_str!(concat!("../../", $path)); - Program::parse(src, true)? + Program::parse(src, true) } else { let src = fs::read_to_string($path)?; - Program::parse(&src, true)? - } + Program::parse(&src, true) + }; + res.map_err(|mut err| { + err.file = Some(String::from($path)); + err + })? }}; ($first:tt, $($arg:tt),*) => { load_asm!($first)$(.merge(load_asm!($arg)))* diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 97121f5..02800b9 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -3,7 +3,7 @@ use raylib::audio::RaylibAudio; use std::time::{Duration, Instant}; -use channel::{Channel, NoiseChannel, PulseChannel, TriangleChannel}; +use channel::Device; use data::Data; mod channel; @@ -20,15 +20,16 @@ pub type Result<T> = std::result::Result<T, crate::Error>; const AUDIO_FPS: u32 = 60; const TIME_SLICE: Duration = Duration::from_millis(1000 / AUDIO_FPS as u64); -/// Holds each audio channel -pub struct Channels { - pub pulse_a: PulseChannel, - pub pulse_b: PulseChannel, - pub triangle: TriangleChannel, - pub noise: NoiseChannel, +/// The `Audio` container initalizes the audio subsystem +/// for raylib, leaks it (to gurentee audio is statically loaded), +/// then loads all needed audio samples +pub struct Audio { + device: Device<'static>, + last: Instant, + pub data: Data, } -impl Channels { - pub(crate) fn load() -> crate::Result<Self> { +impl Audio { + pub fn load() -> crate::Result<Self> { // Phantom handle to the raylib audio subsystem // Raylib doesnt use a handle, but the rust bindings // have one to ensure memory safety. @@ -39,60 +40,20 @@ impl Channels { // NOTE: would this cause issues if `Audio::load` was // called multiple times? let handle = Box::leak(Box::new(RaylibAudio::init_audio_device()?)); - - let pulse_a = PulseChannel::load(handle)?; - pulse_a.set_volume(0.0); - pulse_a.play(); - let pulse_b = PulseChannel::load(handle)?; - pulse_b.set_volume(0.0); - pulse_b.play(); - let triangle = TriangleChannel::load(handle)?; - triangle.set_volume(0.0); - triangle.play(); - let noise = NoiseChannel::load(handle)?; - noise.set_volume(0.0); - noise.play(); - - Ok(Self { - pulse_a, - pulse_b, - triangle, - noise, - }) - } -} - -/// The `Audio` container initalizes the audio subsystem -/// for raylib, leaks it (to gurentee audio is statically loaded), -/// then loads all needed audio samples -pub struct Audio { - channels: Channels, - last: Instant, - pub data: Data, -} -impl Audio { - pub fn load() -> crate::Result<Self> { - let channels = Channels::load()?; + let device = Device::load(handle)?; let last = Instant::now(); let data = Data::load()?; - Ok(Self { - channels, - last, - data, - }) + Ok(Self { device, last, data }) } + #[expect(clippy::unwrap_used)] pub fn update(&mut self) { if self.last.elapsed() >= TIME_SLICE { - self.data.explore.exec(&mut self.channels); - self.data.megalovania.exec(&mut self.channels); + let mut channels = self.device.channels.lock().unwrap(); + self.data.explore.exec(&mut channels); + self.data.megalovania.exec(&mut channels); self.last = Instant::now(); } - - self.channels.pulse_a.update(); - self.channels.pulse_b.update(); - self.channels.triangle.update(); - self.channels.noise.update(); } } diff --git a/audio/src/parse/lex.rs b/audio/src/parse/lex.rs deleted file mode 100644 index 0ef8d47..0000000 --- a/audio/src/parse/lex.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::{ - fmt::Display, - iter::Peekable, - str::{Chars, FromStr}, -}; - -use super::Result; -use crate::{channel::DutyCycle, program::ChanSpec}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Token { - Eof, - LineSeparator, - Pause(usize), - PauseLen(u32), - Jump(usize), - ChanSpec(ChanSpec), - SetVolume(u8), - SetPitch(u8), - SetNoiseMode(bool), - SetPulseDuty(DutyCycle), -} -impl Token { - pub const fn is_eol(self) -> bool { - matches!(self, Self::Eof | Self::LineSeparator) - } -} - -pub struct Lexer<'s> { - src: &'s str, - chars: Peekable<Chars<'s>>, - pos: usize, -} -impl<'s> Lexer<'s> { - pub fn new(src: &'s str) -> Self { - let chars = src.chars().peekable(); - Self { src, chars, pos: 0 } - } - - fn filter_char(&mut self, ch: Option<char>, advance: bool) -> Result<char> { - match ch { - Some(c) if c.is_control() && !matches!(c, '\n' | '\r' | '\t') => { - Err(invalid_char(c)) - } - Some(c) => { - if advance { - self.pos += c.len_utf8(); - } - Ok(c) - } - None => Ok('\0'), - } - } - - fn peek(&mut self) -> Result<char> { - let c = self.chars.peek().copied(); - self.filter_char(c, false) - } - - fn next(&mut self) -> Result<char> { - let c = self.chars.next(); - self.filter_char(c, true) - } - - fn next_int<T>(&mut self) -> Result<T> - where - T: FromStr, - <T as FromStr>::Err: Display, - { - let start = self.pos; - while self.peek()?.is_ascii_digit() { - self.next()?; - } - let str = &self.src[start..self.pos]; - str.parse::<T>().map_err(|e| e.to_string()) - } - - fn next_note(&mut self) -> Result<u8> { - if self.peek()?.is_ascii_digit() { - return self.next_int(); - } - - let note = match self.next()? { - 'a' => 80, - 'b' => 82, - 'c' => 83, - 'd' => 85, - 'e' => 87, - 'f' => 88, - 'g' => 90, - c => unexpected(c)?, - }; - - let octave = { - let c = self.next()?; - c.to_digit(10).ok_or_else(|| format!("invalid octave: {c}")) - }?; - - let off = match self.peek()? { - '#' => { - self.next()?; - 1 - } - 'b' => { - self.next()?; - -1 - } - _ => 0, - }; - - let pitch_u32 = (note + octave * 12).wrapping_add_signed(off); - let pitch = u8::try_from(pitch_u32).map_err(|e| format!("{e}"))?; - - Ok(pitch) - } - - fn next_bool(&mut self) -> Result<bool> { - match self.next_int()? { - 0 => Ok(false), - _ => Ok(true), - } - } - - fn next_duty_cycle(&mut self) -> Result<DutyCycle> { - match self.next_int()? { - 12 => Ok(DutyCycle::Percent12), - 25 => Ok(DutyCycle::Percent25), - 50 => Ok(DutyCycle::Percent50), - 75 => Ok(DutyCycle::Percent25Neg), - n => Err(format!("invalid duty cycle: {n}")), - } - } - - fn skip_comment(&mut self) -> Result<()> { - loop { - let next = self.next()?; - if next == '\n' || next == '\0' { - break; - } - } - Ok(()) - } - - fn next_pause(&mut self) -> Result<Token> { - let mut count = 1; - if self.peek()?.is_ascii_digit() { - count = self.next_int()?; - } - Ok(Token::Pause(count)) - } - - pub fn next_token(&mut self) -> Result<Token> { - use Token as T; - loop { - let peek = self.peek()?; - if peek == ';' { - self.skip_comment()?; - } else if matches!(peek, ' ' | '\t' | '\r') { - self.next()?; - } else { - break; - } - } - let token = match self.next()? { - // chan spec - 'a' => Token::ChanSpec(ChanSpec::PulseA), - 'b' => Token::ChanSpec(ChanSpec::PulseB), - 't' => Token::ChanSpec(ChanSpec::Triangle), - 'n' => Token::ChanSpec(ChanSpec::Noise), - // volume - 'v' => T::SetVolume(self.next_int()?), - // pitch - 'p' => T::SetPitch(self.next_note()?), - // duty cycle - 'd' => T::SetPulseDuty(self.next_duty_cycle()?), - // noise mode - 'm' => T::SetNoiseMode(self.next_bool()?), - // pause - '-' => self.next_pause()?, - // jump - 'j' => T::Jump(self.next_int()?), - // pause len - 'P' => T::PauseLen(self.next_int()?), - // eof - '\0' => T::Eof, - // new line - '\n' => T::LineSeparator, - // unexpected - c => unexpected(c)?, - }; - Ok(token) - } -} -impl Iterator for Lexer<'_> { - type Item = Result<Token>; - - fn next(&mut self) -> Option<Self::Item> { - Some(self.next_token()) - } -} - -fn invalid_char(ch: char) -> String { - match ch as u32 { - c @ 0x00..=0x7f => format!("invalid character (codepoint 0x{c:2x})"), - c => format!("invalid character (codepoint U+{c:04x})"), - } -} - -fn unexpected<T>(c: char) -> Result<T> { - let msg = match c { - '\0' => "unexpected end of file".to_owned(), - '\n' => "unexpected newline character".to_owned(), - '\t' => "unexpected tab character".to_owned(), - '\r' => "unexpected return character".to_owned(), - c => format!("unexpected character {c}"), - }; - Err(msg) -} diff --git a/audio/src/parse/lexer.rs b/audio/src/parse/lexer.rs new file mode 100644 index 0000000..59bd264 --- /dev/null +++ b/audio/src/parse/lexer.rs @@ -0,0 +1,264 @@ +use std::{fmt, iter::Peekable, str::Chars}; + +use super::{ + ParserError, Result, + pos::{Pos, Span}, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TokenKind { + Eof, + LineSeparator, + + MacroDefine, + MacroEnd, + Argument, + + PulseA, + PulseB, + Triangle, + Noise, + + Volume, + Pitch, + DutyCycle, + Mode, + PauseLen, + + Identifier, + Integer, + Dash, +} +impl TokenKind { + pub const fn name(self) -> &'static str { + match self { + Self::Eof => "end of file", + Self::LineSeparator => "line seperator", + + Self::MacroDefine => "%define", + Self::MacroEnd => "%end", + Self::Argument => "$", + + Self::PulseA => "pulsea", + Self::PulseB => "pulseb", + Self::Triangle => "triangle", + Self::Noise => "noise", + + Self::Volume => "volume", + Self::Pitch => "pitch", + Self::DutyCycle => "duty cycle", + Self::Mode => "mode", + Self::PauseLen => "pause len", + + Self::Identifier => "identifier", + Self::Integer => "integer", + Self::Dash => "dash", + } + } +} +impl fmt::Display for TokenKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} +use TokenKind as K; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Token<'s> { + pub span: Span, + pub content: &'s str, + pub kind: TokenKind, +} + +pub struct Lexer<'s> { + src: &'s str, + chars: Peekable<Chars<'s>>, + start_pos: Pos, + pos: Pos, +} +impl<'s> Lexer<'s> { + pub fn new(src: &'s str) -> Self { + Self::new_at(src, 1) + } + + pub fn new_at(src: &'s str, line: u32) -> Self { + let pos = Pos { + line, + col: 1, + idx: 0, + }; + Self { + src, + chars: src.chars().peekable(), + start_pos: pos, + pos, + } + } + + fn invalid_char(&self, ch: char) -> ParserError { + let span = Span::new(self.pos, self.pos.advance(ch)); + let msg = match ch as u32 { + c @ 0x00..=0x7f => format!("invalid character (codepoint 0x{c:2x})"), + c => format!("invalid character (codepoint U+{c:04x})"), + }; + ParserError { + span, + msg, + file: None, + } + } + + fn filter_char(&mut self, ch: Option<char>, advance: bool) -> Result<char> { + match ch { + Some(c) if c.is_control() && !matches!(c, '\n' | '\r' | '\t') => { + Err(self.invalid_char(c)) + } + Some(c) => { + if advance { + self.pos = self.pos.advance(c); + } + Ok(c) + } + None => Ok('\0'), + } + } + + fn peek(&mut self) -> Result<char> { + let c = self.chars.peek().copied(); + self.filter_char(c, false) + } + + fn next(&mut self) -> Result<char> { + let c = self.chars.next(); + self.filter_char(c, true) + } + + fn emit(&self, kind: TokenKind) -> Result<Token<'s>> { + let span = Span::new(self.start_pos, self.pos); + Ok(Token { + span, + content: span.of(self.src), + kind, + }) + } + + fn and_emit(&mut self, kind: TokenKind) -> Result<Token<'s>> { + self.next()?; + self.emit(kind) + } + + fn unexpected(&mut self) -> Result<Token<'s>> { + let c = self.peek()?; + let span = Span::new(self.pos, self.pos.advance(c)); + let msg = match c { + '\0' => "unexpected end of file".to_owned(), + '\n' => "unexpected newline character".to_owned(), + '\t' => "unexpected tab character".to_owned(), + '\r' => "unexpected return character".to_owned(), + c => format!("unexpected character {c}"), + }; + Err(ParserError { + span, + msg, + file: None, + }) + } + + fn err<T>(&self, msg: &str) -> Result<T> { + Err(ParserError { + span: Span::new(self.start_pos, self.pos), + msg: msg.to_owned(), + file: None, + }) + } + + fn next_ident(&mut self) -> Result<Token<'s>> { + let first = self.next()?; + loop { + let c = self.peek()?; + let cond = if ('a'..='g').contains(&first) { + c.is_ascii_alphanumeric() || c == '#' + } else { + c.is_ascii_alphabetic() + }; + if !cond { + break; + } + self.next()?; + } + let kind = match Span::new(self.start_pos, self.pos).of(self.src) { + "pulsea" | "a" => K::PulseA, + "pulseb" | "b" => K::PulseB, + "triangle" | "t" => K::Triangle, + "noise" | "n" => K::Noise, + "volume" | "v" => K::Volume, + "pitch" | "p" => K::Pitch, + "dutycycle" | "dc" | "w" => K::DutyCycle, + "mode" | "m" => K::Mode, + "puselen" | "P" => K::PauseLen, + _ => K::Identifier, + }; + self.emit(kind) + } + + fn next_macro_ident(&mut self) -> Result<Token<'s>> { + self.next()?; + let ident = self.next_ident()?; + let kind = match ident.content { + "%macro" => K::MacroDefine, + "%endmacro" => K::MacroEnd, + _ => self.err("expected %macro or %endmacro")?, + }; + self.start_pos = ident.span.start; + self.emit(kind) + } + + fn next_int(&mut self) -> Result<Token<'s>> { + loop { + let c = self.peek()?; + if c.is_ascii_digit() { + self.next()?; + } else { + return self.emit(K::Integer); + } + } + } + + fn next_comment(&mut self) -> Result<Token<'s>> { + while !matches!(self.peek()?, '\0' | '\n') { + self.next()?; + } + self.next_token() + } + + pub fn next_token(&mut self) -> Result<Token<'s>> { + while matches!(self.peek()?, ' ' | '\t' | '\r') { + self.next()?; + } + self.start_pos = self.pos; + match self.peek()? { + // misc + '\0' => self.emit(K::Eof), + '\n' => self.and_emit(K::LineSeparator), + ';' => self.next_comment(), + // macros + '%' => self.next_macro_ident(), + '$' => self.and_emit(K::Argument), + // pause + '-' => self.and_emit(K::Dash), + // integer + c if c.is_ascii_digit() => self.next_int(), + // ident + c if c.is_ascii_alphabetic() => self.next_ident(), + // rest + _ => self.unexpected(), + } + } +} +impl<'s> Iterator for Lexer<'s> { + type Item = Result<Token<'s>>; + + fn next(&mut self) -> Option<Self::Item> { + Some(self.next_token()) + } +} diff --git a/audio/src/parse/macros.rs b/audio/src/parse/macros.rs index d33208a..a42b6d5 100644 --- a/audio/src/parse/macros.rs +++ b/audio/src/parse/macros.rs @@ -1,105 +1,175 @@ -use std::{borrow::Cow, collections::HashMap, str::Lines}; +use std::collections::HashMap; + +use super::{ + Result, + lexer::{Token, TokenKind}, + util::{SpanParserError, TokenError}, +}; + +use TokenKind as K; + +#[derive(Clone, Debug)] +struct Macro<'s> { + contents: Vec<Token<'s>>, + args: Vec<(usize, usize)>, + num_args: usize, +} struct PreProcessor<'s> { - src: &'s str, - pos: usize, - lines: Lines<'s>, - macros: HashMap<&'s str, &'s str>, + idx: usize, + tokens: Vec<Token<'s>>, + macros: HashMap<&'s str, Macro<'s>>, } impl<'s> PreProcessor<'s> { - fn new(src: &'s str) -> Self { - let lines = src.lines(); + fn new(tokens: Vec<Token<'s>>) -> Self { let macros = HashMap::new(); Self { - src, - pos: 0, - lines, + idx: 0, + tokens, macros, } } - fn next(&mut self) -> Option<&'s str> { - self.lines.next().map(|line| { - self.pos += line.len() + 1; - line.trim() - }) + fn next(&mut self) -> Token<'s> { + if self.idx >= self.tokens.len() { + Token { + span: self.tokens[self.idx - 1].span, + content: "", + kind: K::Eof, + } + } else { + self.idx += 1; + self.tokens[self.idx - 1] + } } - fn read_macro(&mut self, full_name: &'s str) { - let name = &full_name[8..]; - let start = self.pos; - let mut end = start; - while let Some(line) = self.next() - && !matches!(line, "%end") - { - end = self.pos; + fn expect(&mut self, kind: TokenKind) -> Result<Token<'s>> { + let token = self.next(); + if token.kind == kind { + Ok(token) + } else { + token.token_err(format!("expected {kind}, got {}", token.kind)) } - let str = &self.src[start..end]; - self.macros.insert(name, str); } - fn read_macros(&mut self) -> String { - let mut buf = String::new(); - while let Some(line) = self.next() { - if line.starts_with("%define ") { - self.read_macro(line); - } else { - buf.push_str(line); - buf.push('\n'); + fn parse_int(&mut self) -> Result<usize> { + let token = self.expect(K::Integer)?; + token.content.parse().span_err(token.span) + } + + fn read_macro(&mut self) -> Result<Macro<'s>> { + let mut nest_counter = 0; + let mut contents = vec![]; + let mut args = vec![]; + let mut num_args = 0; + loop { + let token = self.next(); + match token.kind { + K::Argument => { + let num = self.parse_int()?; + args.push((num, contents.len())); + num_args = num_args.max(num); + } + K::MacroDefine => { + nest_counter += 1; + contents.push(token); + } + K::MacroEnd => { + if nest_counter > 0 { + nest_counter -= 1; + contents.push(token); + } else { + break; + } + } + K::Eof => return token.token_err("macro definition was not closed"), + _ => contents.push(token), } } - buf + + args.sort_by(|(_, l), (_, r)| l.cmp(r).reverse()); + + Ok(Macro { + contents, + args, + num_args, + }) } - fn process(&mut self) -> String { - let rest = self.read_macros(); - let mut lines = rest.lines().map(Cow::Borrowed).collect::<Vec<_>>(); + fn read_macros(&mut self) -> Result<Vec<Token<'s>>> { + let mut buffer = vec![]; + self.idx = 0; loop { - let mut count = 0; - for (name, body) in &self.macros { - count += fill_macro(&mut lines, name, body); - } - if count == 0 { + let token = self.next(); + if token.kind == K::Eof { break; } + if token.kind != K::MacroDefine { + buffer.push(token); + continue; + } + let name = self.expect(K::Identifier)?.content; + let content = self.read_macro()?; + self.macros.insert(name, content); } - lines.join("\n") + Ok(buffer) } -} -fn fill_macro(contents: &mut Vec<Cow<'_, str>>, name: &str, body: &str) -> usize { - let mut count = 0; - let mut idx = 0; - loop { - if idx >= contents.len() { - break; - } - let line = &contents[idx]; - if line.starts_with(name) - && matches!(line.chars().nth(name.len()), None | Some(' ')) - { - fill_macro_once(contents, idx, body); - count += 1; + fn pass(&mut self) -> Result<Vec<Token<'s>>> { + let mut buffer = self.read_macros()?; + let mut idx = 0; + loop { + if idx >= buffer.len() { + break; + } + + let token = buffer[idx]; + + if token.kind != K::Identifier { + idx += 1; + continue; + } + + // TODO: remove clone + let Some(mac) = self.macros.get(token.content).cloned() else { + idx += 1; + continue; + }; + + let mut args = vec![]; + for n in 1..=mac.num_args { + let arg = buffer[idx + n]; + if matches!(arg.kind, K::Eof | K::LineSeparator) { + return arg.token_err("missing macro argument"); + } + args.push(arg); + } + + let mut content = mac.contents; + for (n, idx) in mac.args { + // this works since mac.args is stored by idx descending + content.insert(idx, args[n - 1]); + } + + let len = content.len(); + buffer.splice(idx..=(idx + mac.num_args), content); + idx += len + 1; } - idx += 1; - } - count -} -fn fill_macro_once(contents: &mut Vec<Cow<'_, str>>, idx: usize, body: &str) { - let invoke_line = contents.remove(idx); - let args = invoke_line.split_whitespace().skip(1).collect::<Vec<_>>(); + Ok(buffer) + } - for line in body.lines().rev() { - let mut buf = String::from(line); - for (idx, arg) in args.iter().enumerate() { - let key = format!("${}", idx + 1); - buf = buf.replace(&key, arg); + fn process(&mut self) -> Result<Vec<Token<'s>>> { + loop { + let result = self.pass()?; + if self.tokens == result { + return Ok(result); + } + self.tokens = result; } - contents.insert(idx, Cow::Owned(buf)); } } -pub fn process(src: &str) -> String { - PreProcessor::new(src).process() +pub fn process(tokens: Vec<Token<'_>>) -> Result<Vec<Token<'_>>> { + PreProcessor::new(tokens).process() } diff --git a/audio/src/parse/mod.rs b/audio/src/parse/mod.rs index 726aaa2..895ddbd 100644 --- a/audio/src/parse/mod.rs +++ b/audio/src/parse/mod.rs @@ -1,13 +1,33 @@ use crate::program::Instruction; +use lexer::{Lexer, TokenKind}; use parser::Parser; +use pos::Span; -pub type Result<T> = std::result::Result<T, String>; - -mod lex; +mod lexer; mod macros; mod parser; +mod pos; +mod util; + +pub type Result<T> = std::result::Result<T, ParserError>; + +#[derive(Clone, Debug)] +pub struct ParserError { + pub span: Span, + pub msg: String, + pub file: Option<String>, +} -pub fn parse(raw_src: &str) -> Result<Vec<Instruction>> { - let src = macros::process(raw_src); - Parser::new(&src).parse() +pub fn parse(src: &str) -> Result<Vec<Instruction>> { + let mut tokens = vec![]; + let mut lexer = Lexer::new(src); + loop { + let token = lexer.next_token()?; + tokens.push(token); + if token.kind == TokenKind::Eof { + break; + } + } + tokens = macros::process(tokens)?; + Parser::new(tokens).parse() } diff --git a/audio/src/parse/parser.rs b/audio/src/parse/parser.rs index b46e707..989a6f1 100644 --- a/audio/src/parse/parser.rs +++ b/audio/src/parse/parser.rs @@ -1,104 +1,206 @@ -use std::iter::Peekable; - -use crate::program::{ChanSpec, Instruction}; +use std::{iter::Peekable, str::FromStr, vec::IntoIter}; use super::{ Result, - lex::{Lexer, Token}, + lexer::{Token, TokenKind}, + util::{SpanParserError, TokenError}, }; +use crate::program::{ChanSpec, Instruction}; +use TokenKind as K; pub struct Parser<'s> { - lexer: Peekable<Lexer<'s>>, + tokens: Peekable<IntoIter<Token<'s>>>, + spec: Option<ChanSpec>, + eof: Token<'s>, } impl<'s> Parser<'s> { - pub fn new(src: &'s str) -> Self { + pub fn new(tokens: Vec<Token<'s>>) -> Self { + let eof = Token { + span: tokens[tokens.len() - 1].span, + content: "", + kind: K::Eof, + }; Self { - lexer: Lexer::new(src).peekable(), + tokens: tokens.into_iter().peekable(), + spec: None, + eof, } } - fn next(&mut self) -> Result<Token> { - self.lexer - .next() - .unwrap_or_else(|| Err("should not happen".to_owned())) + fn next(&mut self) -> Token<'s> { + self.tokens.next().unwrap_or(self.eof) } - fn peek(&mut self) -> Result<Token> { - self.lexer - .peek() - .map_or_else(|| Err("should not happen".to_owned()), Result::clone) + fn peek(&mut self) -> Token<'s> { + self.tokens.peek().cloned().unwrap_or(self.eof) } - fn parse_chan_spec(&mut self) -> Result<ChanSpec> { - match self.next()? { - Token::ChanSpec(spec) => Ok(spec), - t => Err(format!("expected channel specifier, got {t:?}")), + fn expect(&mut self, kind: TokenKind) -> Result<Token<'s>> { + let token = self.next(); + if token.kind == kind { + Ok(token) + } else { + token.token_err(format!("expected {kind}, got {}", token.kind)) } } - fn parse_ins(&mut self, spec: ChanSpec) -> Result<Instruction> { - use Token as T; - let t = self.next()?; - let ins = match t { - T::SetPitch(pitch) => Instruction::SetPitch(spec, pitch), - T::SetVolume(volume) => Instruction::SetVolume(spec, volume), - T::SetNoiseMode(mode) if spec == ChanSpec::Noise => { - Instruction::SetNoiseMode(mode) + fn parse_int<T>(&mut self) -> Result<T> + where + T: FromStr, + <T as FromStr>::Err: std::error::Error, + { + let token = self.expect(K::Integer)?; + token.content.parse().span_err(token.span) + } + + fn parse_note(&mut self) -> Result<u8> { + if self.peek().kind == K::Integer { + return self.parse_int(); + } + + let ident = self.expect(K::Identifier)?; + let str = ident.content; + let str_len = str.len(); + let last = str.chars().last().unwrap_or_default(); + + let note_str = &str[..1]; + let note = match note_str { + "a" => 80, + "b" => 82, + "c" => 83, + "d" => 85, + "e" => 87, + "f" => 88, + "g" => 90, + _ => return ident.token_err("invalid note"), + }; + + let octave_str = if last.is_ascii_digit() { + &str[1..str_len] + } else { + &str[1..str_len - 1] + }; + let octave = if !octave_str.is_empty() { + octave_str.parse().span_err(ident.span)? + } else { + 4 + }; + + let off = match last { + 'b' | 'f' => -1, + '#' | 's' => 1, + _ => 0, + }; + + let pitch_u32 = (note + octave * 12) + off; + let pitch = u8::try_from(pitch_u32).span_err(ident.span)?; + + Ok(pitch) + } + + fn parse_bool(&mut self) -> Result<bool> { + let n = self.parse_int::<u8>()?; + Ok(n > 0) + } + + fn parse_chan_ins(&mut self) -> Result<Instruction> { + let token = self.next(); + let Some(spec) = self.spec else { + return token.token_err("missing channel specifier"); + }; + let ins = match token.kind { + K::Volume => { + let volume = self.parse_int()?; + Instruction::SetVolume(spec, volume) + } + K::Pitch => { + let pitch = self.parse_note()?; + Instruction::SetPitch(spec, pitch) + } + K::DutyCycle if spec == ChanSpec::PulseA => { + let cycle = self.parse_int()?; + Instruction::SetPulseDutyA(cycle) } - T::SetPulseDuty(duty_cycle) if spec == ChanSpec::PulseA => { - Instruction::SetPulseDutyA(duty_cycle) + K::DutyCycle if spec == ChanSpec::PulseB => { + let cycle = self.parse_int()?; + Instruction::SetPulseDutyB(cycle) } - T::SetPulseDuty(duty_cycle) if spec == ChanSpec::PulseB => { - Instruction::SetPulseDutyB(duty_cycle) + K::DutyCycle => { + return token.token_err("cannot set duty cycle for this channel"); } - _ => unexpected(t)?, + K::Mode if spec == ChanSpec::Noise => { + let mode = self.parse_bool()?; + Instruction::SetNoiseMode(mode) + } + K::Mode => return token.token_err("cannot set mode for this channel"), + _ => return token.token_err("invalid channel argument"), }; Ok(ins) } - fn parse_line(&mut self, prog: &mut Vec<Instruction>) -> Result<()> { - let spec = self.parse_chan_spec()?; - loop { - prog.push(self.parse_ins(spec)?); - let peek = self.peek()?; - if peek.is_eol() || matches!(peek, Token::Pause(_)) { - break; + fn parse_pause_len(&mut self) -> Result<Instruction> { + self.next(); + let num = self.parse_int()?; + Ok(Instruction::PauseLen(num)) + } + + fn parse_pause(&mut self) -> Result<Instruction> { + self.next(); + let next = self.peek(); + let ins = if next.kind == K::Integer { + let num = self.parse_int()?; + Instruction::Pause(num) + } else { + Instruction::Pause(1) + }; + Ok(ins) + } + + fn parse_ins(&mut self) -> Result<Instruction> { + let token = self.peek(); + let spec = self.spec.take(); + match token.kind { + K::PulseA => { + self.spec = Some(ChanSpec::PulseA); + self.next(); + self.parse_chan_ins() + } + K::PulseB => { + self.spec = Some(ChanSpec::PulseB); + self.next(); + self.parse_chan_ins() + } + K::Triangle => { + self.spec = Some(ChanSpec::Triangle); + self.next(); + self.parse_chan_ins() + } + K::Noise => { + self.spec = Some(ChanSpec::Noise); + self.next(); + self.parse_chan_ins() + } + K::PauseLen => self.parse_pause_len(), + K::Dash => self.parse_pause(), + _ => { + self.spec = spec; + self.parse_chan_ins() } } - Ok(()) } pub fn parse(&mut self) -> Result<Vec<Instruction>> { let mut prog = vec![]; loop { - let t = self.peek()?; - match t { - Token::Eof => break, - Token::LineSeparator => { - self.next()?; + let token = self.peek(); + match token.kind { + K::Eof => break, + K::LineSeparator => { + self.next(); } - Token::Pause(count) => { - self.next()?; - for _ in 0..count { - prog.push(Instruction::Pause); - } - } - Token::Jump(pc) => { - self.next()?; - prog.push(Instruction::Jump(pc)); - } - Token::PauseLen(pause_len) => { - self.next()?; - prog.push(Instruction::PauseLen(pause_len)); - } - _ => self.parse_line(&mut prog)?, + _ => prog.push(self.parse_ins()?), } } Ok(prog) } } - -fn unexpected<T>(t: Token) -> Result<T> { - let msg = format!("unexpected token: {t:?}"); - Err(msg) -} diff --git a/audio/src/parse/pos.rs b/audio/src/parse/pos.rs new file mode 100644 index 0000000..f56a96b --- /dev/null +++ b/audio/src/parse/pos.rs @@ -0,0 +1,83 @@ +use std::fmt; + +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +pub struct Pos { + pub idx: u32, + pub line: u32, + pub col: u32, +} + +impl std::cmp::PartialOrd for Pos { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl std::cmp::Ord for Pos { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.idx.cmp(&other.idx) + } +} + +impl Pos { + pub const fn new() -> Self { + Self { + idx: 0, + line: 1, + col: 1, + } + } + + #[must_use] + pub fn advance(self, c: char) -> Self { + let idx = self.idx + u32::try_from(c.len_utf8()).unwrap_or_default(); + if c == '\n' { + Self { + idx, + line: self.line + 1, + col: 1, + } + } else { + Self { + idx, + line: self.line, + col: self.col + 1, + } + } + } +} + +impl fmt::Display for Pos { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.line, self.col) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Span { + pub start: Pos, + pub end: Pos, +} + +impl Span { + pub const fn new(start: Pos, end: Pos) -> Self { + if end.idx < start.idx { + Self { + start: end, + end: start, + } + } else { + Self { start, end } + } + } + + pub fn of<'a>(&self, s: &'a str) -> &'a str { + &s[(self.start.idx as usize)..(self.end.idx as usize)] + } +} + +impl fmt::Display for Span { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}-{}", self.start, self.end) + } +} diff --git a/audio/src/parse/util.rs b/audio/src/parse/util.rs new file mode 100644 index 0000000..47779c2 --- /dev/null +++ b/audio/src/parse/util.rs @@ -0,0 +1,42 @@ +use super::{ParserError, lexer::Token, pos::Span}; +use std::fmt; + +impl std::error::Error for ParserError {} + +impl fmt::Display for ParserError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(file) = &self.file { + write!(f, "{file} | ")?; + } + write!(f, "{} | {}", self.span, self.msg) + } +} + +pub trait SpanParserError { + type Output; + fn span_err(self, span: Span) -> Self::Output; +} + +impl<T, E: std::error::Error> SpanParserError for Result<T, E> { + type Output = Result<T, ParserError>; + fn span_err(self, span: Span) -> Self::Output { + self.map_err(|e| ParserError { + span, + msg: e.to_string(), + file: None, + }) + } +} + +pub trait TokenError { + fn token_err<T>(self, msg: impl Into<String>) -> Result<T, ParserError>; +} +impl TokenError for Token<'_> { + fn token_err<T>(self, msg: impl Into<String>) -> Result<T, ParserError> { + Err(ParserError { + span: self.span, + msg: msg.into(), + file: None, + }) + } +} diff --git a/audio/src/program.rs b/audio/src/program.rs index d8ece2c..e5d31bb 100644 --- a/audio/src/program.rs +++ b/audio/src/program.rs @@ -1,6 +1,7 @@ +use std::fmt; + use crate::{ - Channels, - channel::{Channel, DutyCycle}, + channel::{ChannelKind, Channels}, parse, }; @@ -11,18 +12,43 @@ pub enum ChanSpec { Triangle, Noise, } +impl fmt::Display for ChanSpec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PulseA => f.write_str("pulsea"), + Self::PulseB => f.write_str("pulseb"), + Self::Triangle => f.write_str("triangle"), + Self::Noise => f.write_str("noise"), + } + } +} #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Instruction { - Pause, - PauseLen(u32), - Jump(usize), + Pause(u16), + PauseLen(u16), + Jump(u16), SetVolume(ChanSpec, u8), SetPitch(ChanSpec, u8), - SetPulseDutyA(DutyCycle), - SetPulseDutyB(DutyCycle), + SetPulseDutyA(u8), + SetPulseDutyB(u8), SetNoiseMode(bool), } +impl fmt::Display for Instruction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Pause(len) => write!(f, "pause {len}"), + Self::PauseLen(len) => write!(f, "pause_len {len}"), + Self::Jump(pc) => write!(f, "jump {pc}"), + Self::SetVolume(chan, v) => write!(f, "{chan} volume {v}"), + Self::SetPitch(chan, p) => write!(f, "{chan} pitch {p}"), + Self::SetPulseDutyA(dc) => write!(f, "{} duty_cycle {dc}", ChanSpec::PulseA), + Self::SetPulseDutyB(dc) => write!(f, "{} duty_cycle {dc}", ChanSpec::PulseB), + Self::SetNoiseMode(mode) => write!(f, "{} mode {mode}", ChanSpec::Noise), + } + } +} +use Instruction as I; fn map_pitch(pitch: u8) -> f32 { const HALF_STEP: f32 = 1.059_463_1; @@ -41,8 +67,8 @@ const fn map_volume(volume: u8) -> f32 { pub struct Program { ins: Vec<Instruction>, pc: usize, - pause_cnt: u32, - pause_len: u32, + pause_cnt: usize, + pause_len: usize, looping: bool, playing: bool, } @@ -68,6 +94,8 @@ impl Program { let mut l = 0; let mut r = 0; + let mut l_pause = 0; + let mut r_pause = 0; let l_ins = self.ins; let r_ins = other.ins; @@ -77,27 +105,52 @@ impl Program { break; } - let mut has_pause = false; while l < l_ins.len() { + if l_pause > 0 { + break; + } let ins = l_ins[l]; l += 1; - if matches!(ins, Instruction::Pause) { - has_pause = true; + if let I::Pause(cnt) = ins { + l_pause = cnt; break; } res.push(ins); } + while r < r_ins.len() { + if r_pause > 0 { + break; + } let ins = r_ins[r]; r += 1; - if matches!(ins, Instruction::Pause) { - has_pause = true; + if let I::Pause(cnt) = ins { + r_pause = cnt; break; } res.push(ins); } - if has_pause { - res.push(Instruction::Pause); + + if l_pause > 0 { + if r_pause > 0 && l_pause >= r_pause { + l_pause -= r_pause; + res.push(I::Pause(r_pause)); + r_pause = 0; + } else if r_pause == 0 { + res.push(I::Pause(l_pause)); + l_pause = 0; + } + } + + if r_pause > 0 { + if l_pause > 0 && r_pause >= l_pause { + r_pause -= l_pause; + res.push(I::Pause(l_pause)); + l_pause = 0; + } else if l_pause == 0 { + res.push(I::Pause(r_pause)); + r_pause = 0; + } } } @@ -107,24 +160,24 @@ impl Program { fn exec_ins(&mut self, channels: &mut Channels, ins: Instruction) { use Instruction as I; match ins { - I::Pause => { - self.pause_cnt = self.pause_len; + I::Pause(cnt) => { + self.pause_cnt = self.pause_len * (cnt as usize); } I::PauseLen(pause_len) => { - self.pause_len = pause_len; + self.pause_len = pause_len as usize; } I::Jump(pc) => { - self.pc = pc; + self.pc = pc as usize; } I::SetVolume(chan_spec, volume) => { // set the volume (amplitude) on a given channel use ChanSpec as C; let v = map_volume(volume); match chan_spec { - C::PulseA => channels.pulse_a.set_volume(v), - C::PulseB => channels.pulse_b.set_volume(v), - C::Triangle => channels.triangle.set_volume(v), - C::Noise => channels.noise.set_volume(v), + C::PulseA => channels.pulse_a.volume = v, + C::PulseB => channels.pulse_b.volume = v, + C::Triangle => channels.triangle.volume = v, + C::Noise => channels.noise.volume = v, } } I::SetPitch(chan_spec, pitch) => { @@ -132,20 +185,26 @@ impl Program { use ChanSpec as C; let p = map_pitch(pitch); match chan_spec { - C::PulseA => channels.pulse_a.set_pitch(p), - C::PulseB => channels.pulse_b.set_pitch(p), - C::Triangle => channels.triangle.set_pitch(p), - C::Noise => channels.noise.set_pitch(p), + C::PulseA => channels.pulse_a.pitch = p, + C::PulseB => channels.pulse_b.pitch = p, + C::Triangle => channels.triangle.pitch = p, + C::Noise => channels.noise.pitch = p, } } - I::SetPulseDutyA(duty_cycle) => { - channels.pulse_a.set_duty(duty_cycle); + I::SetPulseDutyA(dc) => { + if let ChannelKind::Pulse { duty_cycle } = &mut channels.pulse_a.kind { + *duty_cycle = dc; + } } - I::SetPulseDutyB(duty_cycle) => { - channels.pulse_b.set_duty(duty_cycle); + I::SetPulseDutyB(dc) => { + if let ChannelKind::Pulse { duty_cycle } = &mut channels.pulse_b.kind { + *duty_cycle = dc; + } } - I::SetNoiseMode(mode) => { - channels.noise.set_mode(mode); + I::SetNoiseMode(m) => { + if let ChannelKind::Noise { mode, .. } = &mut channels.noise.kind { + *mode = m; + } } } } @@ -192,3 +251,14 @@ impl Program { self.looping = looping; } } +impl fmt::Display for Program { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (idx, ins) in self.ins.iter().enumerate() { + if idx != 0 { + writeln!(f)?; + } + write!(f, "{ins}")?; + } + Ok(()) + } +} diff --git a/game/src/main.rs b/game/src/main.rs index 1ab49fe..cf82239 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -16,7 +16,7 @@ struct Args { seed: Option<u64>, } -pub fn main() -> graphics::Result<()> { +fn run() -> graphics::Result<()> { // Parse arguments let args: Args = argh::from_env(); // Load the window @@ -27,3 +27,9 @@ pub fn main() -> graphics::Result<()> { Game::new(window, args.seed).run(); Ok(()) } + +pub fn main() { + if let Err(err) = run() { + eprintln!("error: {err}"); + } +} |