summaryrefslogtreecommitdiff
path: root/audio
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2025-11-22 13:29:10 -0500
committerFreya Murphy <freya@freyacat.org>2025-11-22 13:29:10 -0500
commit2274d33e469aca544a7aeb899a10769b973ae374 (patch)
tree7ff10964234fd5e8917624508e249265a3d0b19c /audio
parentaudio: move 'asm' files out of data segment (diff)
downloadDungeonCrawl-2274d33e469aca544a7aeb899a10769b973ae374.tar.gz
DungeonCrawl-2274d33e469aca544a7aeb899a10769b973ae374.tar.bz2
DungeonCrawl-2274d33e469aca544a7aeb899a10769b973ae374.zip
audio: refactor into seperate crate
Diffstat (limited to 'audio')
-rw-r--r--audio/Cargo.toml18
-rw-r--r--audio/src/channel.rs199
-rw-r--r--audio/src/data.rs30
-rw-r--r--audio/src/lib.rs97
-rw-r--r--audio/src/parse/lex.rs218
-rw-r--r--audio/src/parse/macros.rs103
-rw-r--r--audio/src/parse/mod.rs13
-rw-r--r--audio/src/parse/parser.rs104
-rw-r--r--audio/src/program.rs184
9 files changed, 966 insertions, 0 deletions
diff --git a/audio/Cargo.toml b/audio/Cargo.toml
new file mode 100644
index 0000000..58f18a2
--- /dev/null
+++ b/audio/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "audio"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+publish.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+raylib.workspace = true
+
+[lints]
+workspace = true
+
+[features]
+default = []
+static = []
diff --git a/audio/src/channel.rs b/audio/src/channel.rs
new file mode 100644
index 0000000..7ed61a6
--- /dev/null
+++ b/audio/src/channel.rs
@@ -0,0 +1,199 @@
+use raylib::audio::RaylibAudio;
+
+type Music = raylib::audio::Music<'static>;
+
+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
+ }};
+}
+
+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);
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DutyCycle {
+ Percent12,
+ Percent25,
+ Percent50,
+ Percent25Neg,
+}
+
+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,
+ })
+ }
+
+ 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();
+ }
+
+ 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(),
+ }
+ }
+
+ 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);
+ }
+}
+
+pub struct TriangleChannel {
+ inner: Music,
+}
+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);
+ }
+
+ fn set_pitch(&self, pitch: f32) {
+ self.inner.set_pitch(pitch);
+ }
+}
+
+pub struct NoiseChannel {
+ noise_0: Music,
+ noise_1: Music,
+ mode: bool,
+}
+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 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);
+ }
+
+ fn set_pitch(&self, pitch: f32) {
+ self.noise_0.set_pitch(pitch);
+ self.noise_1.set_pitch(pitch);
+ }
+}
diff --git a/audio/src/data.rs b/audio/src/data.rs
new file mode 100644
index 0000000..2d1bdc5
--- /dev/null
+++ b/audio/src/data.rs
@@ -0,0 +1,30 @@
+use crate::program::Program;
+use std::fs;
+
+macro_rules! load_asm {
+ ($path:tt) => {{
+ if cfg!(any(feature = "static", target_arch = "wasm32")) {
+ let src = include_str!(concat!("../../", $path));
+ Program::parse(src, true)?
+ } else {
+ let src = fs::read_to_string($path)?;
+ Program::parse(&src, true)?
+ }
+ }};
+ ($first:tt, $($arg:tt)*) => {
+ load_asm!($first)$(.merge(load_asm!($arg)))*
+ };
+}
+
+pub struct Data {
+ pub megalovania: Program,
+}
+impl Data {
+ pub fn load() -> crate::Result<Self> {
+ let megalovania = load_asm!(
+ "assets/asm/megalovania_melody.asm",
+ "assets/asm/megalovania_base.asm"
+ );
+ Ok(Self { megalovania })
+ }
+}
diff --git a/audio/src/lib.rs b/audio/src/lib.rs
new file mode 100644
index 0000000..b2d3e87
--- /dev/null
+++ b/audio/src/lib.rs
@@ -0,0 +1,97 @@
+//! The `audio` crate stores all audio assets that need to be loaded during runtime
+
+use raylib::audio::RaylibAudio;
+use std::time::{Duration, Instant};
+
+use channel::{Channel, NoiseChannel, PulseChannel, TriangleChannel};
+use data::Data;
+
+mod channel;
+mod data;
+mod parse;
+mod program;
+
+/// The `Error` type used within this crate
+pub type Error = Box<dyn std::error::Error>;
+
+/// The `Result` type used witin this crate
+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,
+}
+impl Channels {
+ pub(crate) 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.
+ //
+ // We must leak this handle after allocating it,
+ // if we dont then all audio will be unloaded :(
+ //
+ // 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 last = Instant::now();
+ let data = Data::load()?;
+
+ Ok(Self {
+ channels,
+ last,
+ data,
+ })
+ }
+
+ pub fn update(&mut self) {
+ if self.last.elapsed() >= TIME_SLICE {
+ self.data.megalovania.exec(&mut self.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
new file mode 100644
index 0000000..0ef8d47
--- /dev/null
+++ b/audio/src/parse/lex.rs
@@ -0,0 +1,218 @@
+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/macros.rs b/audio/src/parse/macros.rs
new file mode 100644
index 0000000..1dc33eb
--- /dev/null
+++ b/audio/src/parse/macros.rs
@@ -0,0 +1,103 @@
+use std::{borrow::Cow, collections::HashMap, str::Lines};
+
+struct PreProcessor<'s> {
+ src: &'s str,
+ pos: usize,
+ lines: Lines<'s>,
+ macros: HashMap<&'s str, &'s str>,
+}
+impl<'s> PreProcessor<'s> {
+ fn new(src: &'s str) -> Self {
+ let lines = src.lines();
+ let macros = HashMap::new();
+ Self {
+ src,
+ pos: 0,
+ lines,
+ macros,
+ }
+ }
+
+ fn next(&mut self) -> Option<&'s str> {
+ self.lines.next().map(|line| {
+ self.pos += line.len() + 1;
+ line.trim()
+ })
+ }
+
+ 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;
+ }
+ 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');
+ }
+ }
+ buf
+ }
+
+ fn process(&mut self) -> String {
+ let rest = self.read_macros();
+ let mut lines = rest.lines().map(Cow::Borrowed).collect::<Vec<_>>();
+ loop {
+ let mut count = 0;
+ for (name, body) in &self.macros {
+ count += fill_macro(&mut lines, name, body);
+ }
+ if count == 0 {
+ break;
+ }
+ }
+ lines.join("\n")
+ }
+}
+
+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) {
+ fill_macro_once(contents, idx, body);
+ count += 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<_>>();
+
+ 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);
+ }
+ contents.insert(idx, Cow::Owned(buf));
+ }
+}
+
+pub fn process(src: &str) -> String {
+ PreProcessor::new(src).process()
+}
diff --git a/audio/src/parse/mod.rs b/audio/src/parse/mod.rs
new file mode 100644
index 0000000..726aaa2
--- /dev/null
+++ b/audio/src/parse/mod.rs
@@ -0,0 +1,13 @@
+use crate::program::Instruction;
+use parser::Parser;
+
+pub type Result<T> = std::result::Result<T, String>;
+
+mod lex;
+mod macros;
+mod parser;
+
+pub fn parse(raw_src: &str) -> Result<Vec<Instruction>> {
+ let src = macros::process(raw_src);
+ Parser::new(&src).parse()
+}
diff --git a/audio/src/parse/parser.rs b/audio/src/parse/parser.rs
new file mode 100644
index 0000000..b46e707
--- /dev/null
+++ b/audio/src/parse/parser.rs
@@ -0,0 +1,104 @@
+use std::iter::Peekable;
+
+use crate::program::{ChanSpec, Instruction};
+
+use super::{
+ Result,
+ lex::{Lexer, Token},
+};
+
+pub struct Parser<'s> {
+ lexer: Peekable<Lexer<'s>>,
+}
+impl<'s> Parser<'s> {
+ pub fn new(src: &'s str) -> Self {
+ Self {
+ lexer: Lexer::new(src).peekable(),
+ }
+ }
+
+ fn next(&mut self) -> Result<Token> {
+ self.lexer
+ .next()
+ .unwrap_or_else(|| Err("should not happen".to_owned()))
+ }
+
+ fn peek(&mut self) -> Result<Token> {
+ self.lexer
+ .peek()
+ .map_or_else(|| Err("should not happen".to_owned()), Result::clone)
+ }
+
+ 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 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)
+ }
+ T::SetPulseDuty(duty_cycle) if spec == ChanSpec::PulseA => {
+ Instruction::SetPulseDutyA(duty_cycle)
+ }
+ T::SetPulseDuty(duty_cycle) if spec == ChanSpec::PulseB => {
+ Instruction::SetPulseDutyB(duty_cycle)
+ }
+ _ => unexpected(t)?,
+ };
+ 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;
+ }
+ }
+ 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()?;
+ }
+ 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)?,
+ }
+ }
+ Ok(prog)
+ }
+}
+
+fn unexpected<T>(t: Token) -> Result<T> {
+ let msg = format!("unexpected token: {t:?}");
+ Err(msg)
+}
diff --git a/audio/src/program.rs b/audio/src/program.rs
new file mode 100644
index 0000000..28a5f41
--- /dev/null
+++ b/audio/src/program.rs
@@ -0,0 +1,184 @@
+use crate::{
+ Channels,
+ channel::{Channel, DutyCycle},
+ parse,
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ChanSpec {
+ PulseA,
+ PulseB,
+ Triangle,
+ Noise,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Instruction {
+ Pause,
+ PauseLen(u32),
+ Jump(usize),
+ SetVolume(ChanSpec, u8),
+ SetPitch(ChanSpec, u8),
+ SetPulseDutyA(DutyCycle),
+ SetPulseDutyB(DutyCycle),
+ SetNoiseMode(bool),
+}
+
+fn map_pitch(pitch: u8) -> f32 {
+ const HALF_STEP: f32 = 1.059_463_1;
+ HALF_STEP.powf(pitch as f32 - 128.0)
+}
+
+const fn map_volume(volume: u8) -> f32 {
+ if volume > 100 {
+ 1.0
+ } else {
+ (volume as f32) / 100.0
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Program {
+ ins: Vec<Instruction>,
+ pc: usize,
+ looping: bool,
+ pause_cnt: u32,
+ pause_len: u32,
+}
+impl Program {
+ pub const fn new(ins: Vec<Instruction>, looping: bool) -> Self {
+ let pc = ins.len();
+ Self {
+ ins,
+ pc,
+ looping,
+ pause_cnt: 0,
+ pause_len: 4,
+ }
+ }
+
+ pub fn parse(src: &str, looping: bool) -> parse::Result<Self> {
+ let ins = parse::parse(src)?;
+ Ok(Self::new(ins, looping))
+ }
+
+ pub fn merge(self, other: Self) -> Self {
+ let mut res = vec![];
+
+ let mut l = 0;
+ let mut r = 0;
+ let l_ins = self.ins;
+ let r_ins = other.ins;
+
+ loop {
+ if l >= l_ins.len() && r >= r_ins.len() {
+ // were done here
+ break;
+ }
+
+ let mut has_pause = false;
+ while l < l_ins.len() {
+ let ins = l_ins[l];
+ l += 1;
+ if matches!(ins, Instruction::Pause) {
+ has_pause = true;
+ break;
+ }
+ res.push(ins);
+ }
+ while r < r_ins.len() {
+ let ins = r_ins[r];
+ r += 1;
+ if matches!(ins, Instruction::Pause) {
+ has_pause = true;
+ break;
+ }
+ res.push(ins);
+ }
+ if has_pause {
+ res.push(Instruction::Pause);
+ }
+ }
+
+ Self::new(res, self.looping)
+ }
+
+ 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::PauseLen(pause_len) => {
+ self.pause_len = pause_len;
+ }
+ I::Jump(pc) => {
+ self.pc = pc;
+ }
+ 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),
+ }
+ }
+ I::SetPitch(chan_spec, pitch) => {
+ // set the pitch (freq) on a given channel
+ 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),
+ }
+ }
+ I::SetPulseDutyA(duty_cycle) => {
+ channels.pulse_a.set_duty(duty_cycle);
+ }
+ I::SetPulseDutyB(duty_cycle) => {
+ channels.pulse_b.set_duty(duty_cycle);
+ }
+ I::SetNoiseMode(mode) => {
+ channels.noise.set_mode(mode);
+ }
+ }
+ }
+
+ pub fn exec(&mut self, channels: &mut Channels) {
+ if self.pause_cnt > 0 {
+ self.pause_cnt -= 1;
+ }
+ loop {
+ if self.pause_cnt > 0 {
+ break;
+ }
+ if self.finished() {
+ if self.looping {
+ self.pc = 0;
+ } else {
+ break;
+ }
+ }
+ let ins = self.ins[self.pc];
+ self.exec_ins(channels, ins);
+ self.pc += 1;
+ }
+ }
+
+ pub const fn finished(&self) -> bool {
+ self.pc >= self.ins.len()
+ }
+
+ pub const fn play(&mut self) {
+ self.pc = 0;
+ }
+
+ pub const fn set_looping(&mut self, looping: bool) {
+ self.looping = looping;
+ }
+}