summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--assets/asm/explore_harmony1.asm90
-rw-r--r--assets/asm/explore_harmony2.asm14
-rw-r--r--assets/asm/explore_melody.asm32
-rw-r--r--assets/asm/megalovania_base.asm26
-rw-r--r--assets/asm/megalovania_melody.asm26
-rw-r--r--assets/wav/noise_0.wavbin88244 -> 0 bytes
-rw-r--r--assets/wav/noise_1.wavbin88244 -> 0 bytes
-rw-r--r--assets/wav/pulse_12.wavbin244 -> 0 bytes
-rw-r--r--assets/wav/pulse_25.wavbin244 -> 0 bytes
-rw-r--r--assets/wav/pulse_50.wavbin244 -> 0 bytes
-rw-r--r--assets/wav/pulse_75.wavbin244 -> 0 bytes
-rw-r--r--assets/wav/triangle.wavbin244 -> 0 bytes
-rw-r--r--audio/src/channel.rs279
-rw-r--r--audio/src/data.rs12
-rw-r--r--audio/src/lib.rs71
-rw-r--r--audio/src/parse/lex.rs218
-rw-r--r--audio/src/parse/lexer.rs264
-rw-r--r--audio/src/parse/macros.rs214
-rw-r--r--audio/src/parse/mod.rs32
-rw-r--r--audio/src/parse/parser.rs232
-rw-r--r--audio/src/parse/pos.rs83
-rw-r--r--audio/src/parse/util.rs42
-rw-r--r--audio/src/program.rs138
-rw-r--r--game/src/main.rs8
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
deleted file mode 100644
index dc1d2db..0000000
--- a/assets/wav/noise_0.wav
+++ /dev/null
Binary files differ
diff --git a/assets/wav/noise_1.wav b/assets/wav/noise_1.wav
deleted file mode 100644
index c20882f..0000000
--- a/assets/wav/noise_1.wav
+++ /dev/null
Binary files differ
diff --git a/assets/wav/pulse_12.wav b/assets/wav/pulse_12.wav
deleted file mode 100644
index 0127fbb..0000000
--- a/assets/wav/pulse_12.wav
+++ /dev/null
Binary files differ
diff --git a/assets/wav/pulse_25.wav b/assets/wav/pulse_25.wav
deleted file mode 100644
index 0bc51c7..0000000
--- a/assets/wav/pulse_25.wav
+++ /dev/null
Binary files differ
diff --git a/assets/wav/pulse_50.wav b/assets/wav/pulse_50.wav
deleted file mode 100644
index 83e4676..0000000
--- a/assets/wav/pulse_50.wav
+++ /dev/null
Binary files differ
diff --git a/assets/wav/pulse_75.wav b/assets/wav/pulse_75.wav
deleted file mode 100644
index 11f7364..0000000
--- a/assets/wav/pulse_75.wav
+++ /dev/null
Binary files differ
diff --git a/assets/wav/triangle.wav b/assets/wav/triangle.wav
deleted file mode 100644
index 0c9bb9e..0000000
--- a/assets/wav/triangle.wav
+++ /dev/null
Binary files differ
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}");
+ }
+}