summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2025-11-21 22:25:37 -0500
committerFreya Murphy <freya@freyacat.org>2025-11-21 22:25:37 -0500
commit654277d89471010f57794b8022385d2a99a15a14 (patch)
treec10f98cfae20517c23007b941214a4c5f34bfd51
parentdungeon: add chest usage/textures (diff)
downloadDungeonCrawl-654277d89471010f57794b8022385d2a99a15a14.tar.gz
DungeonCrawl-654277d89471010f57794b8022385d2a99a15a14.tar.bz2
DungeonCrawl-654277d89471010f57794b8022385d2a99a15a14.zip
audio: create orchestration system
-rw-r--r--Cargo.toml2
-rw-r--r--assets/noise_0.wavbin0 -> 88244 bytes
-rw-r--r--assets/noise_1.wavbin0 -> 88244 bytes
-rw-r--r--assets/pulse_12.wavbin0 -> 244 bytes
-rw-r--r--assets/pulse_25.wavbin0 -> 244 bytes
-rw-r--r--assets/pulse_50.wavbin0 -> 244 bytes
-rw-r--r--assets/pulse_75.wavbin0 -> 244 bytes
-rw-r--r--assets/triangle.wavbin0 -> 244 bytes
-rw-r--r--game/src/lib.rs5
-rw-r--r--graphics/src/audio.rs46
-rw-r--r--graphics/src/audio/channel.rs199
-rw-r--r--graphics/src/audio/data.rs65
-rw-r--r--graphics/src/audio/mod.rs91
-rw-r--r--graphics/src/audio/parse/lex.rs215
-rw-r--r--graphics/src/audio/parse/macros.rs103
-rw-r--r--graphics/src/audio/parse/mod.rs12
-rw-r--r--graphics/src/audio/parse/parser.rs100
-rw-r--r--graphics/src/audio/program.rs173
-rw-r--r--graphics/src/lib.rs4
19 files changed, 965 insertions, 50 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 847fe53..d0ec1b6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,7 +42,7 @@ features = [
"SUPPORT_MODULE_RTEXTURES",
"SUPPORT_MODULE_RAUDIO",
"SUPPORT_FILEFORMAT_BMP",
- "SUPPORT_FILEFORMAT_OGG",
+ "SUPPORT_FILEFORMAT_WAV",
"SUPPORT_STANDARD_FILEIO",
"SUPPORT_TRACELOG",
]
diff --git a/assets/noise_0.wav b/assets/noise_0.wav
new file mode 100644
index 0000000..dc1d2db
--- /dev/null
+++ b/assets/noise_0.wav
Binary files differ
diff --git a/assets/noise_1.wav b/assets/noise_1.wav
new file mode 100644
index 0000000..c20882f
--- /dev/null
+++ b/assets/noise_1.wav
Binary files differ
diff --git a/assets/pulse_12.wav b/assets/pulse_12.wav
new file mode 100644
index 0000000..0127fbb
--- /dev/null
+++ b/assets/pulse_12.wav
Binary files differ
diff --git a/assets/pulse_25.wav b/assets/pulse_25.wav
new file mode 100644
index 0000000..0bc51c7
--- /dev/null
+++ b/assets/pulse_25.wav
Binary files differ
diff --git a/assets/pulse_50.wav b/assets/pulse_50.wav
new file mode 100644
index 0000000..83e4676
--- /dev/null
+++ b/assets/pulse_50.wav
Binary files differ
diff --git a/assets/pulse_75.wav b/assets/pulse_75.wav
new file mode 100644
index 0000000..11f7364
--- /dev/null
+++ b/assets/pulse_75.wav
Binary files differ
diff --git a/assets/triangle.wav b/assets/triangle.wav
new file mode 100644
index 0000000..0c9bb9e
--- /dev/null
+++ b/assets/triangle.wav
Binary files differ
diff --git a/game/src/lib.rs b/game/src/lib.rs
index 3d9c8a7..4572d8e 100644
--- a/game/src/lib.rs
+++ b/game/src/lib.rs
@@ -104,7 +104,7 @@ impl Game {
}
UpdateResult::MessageUpdated(changed) => {
if changed {
- self.window.audio().speak.play();
+ //self.window.audio().speak.play();
}
}
UpdateResult::GameOver => {
@@ -115,6 +115,9 @@ impl Game {
// Draw a single frame
self.window.draw_frame(&self.dungeon);
+
+ // Update audio
+ self.window.audio().update();
}
}
}
diff --git a/graphics/src/audio.rs b/graphics/src/audio.rs
deleted file mode 100644
index 981defb..0000000
--- a/graphics/src/audio.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-//! The `audio` crate stores all audio assets that need to be loaded during runtime
-
-use raylib::audio::RaylibAudio;
-
-macro_rules! load_audio {
- ($handle:expr, $filepath:expr) => {
- if cfg!(any(feature = "static", target_arch = "wasm32")) {
- let bytes = include_bytes!(concat!("../../", $filepath));
- let wave = $handle.new_wave_from_memory(".ogg", bytes)?;
- $handle.new_sound_from_wave(&wave)?
- } else {
- $handle.new_sound($filepath)?
- }
- };
-}
-
-type Sound = raylib::audio::Sound<'static>;
-
-/// The `Audio` container initalizes the audio subsystem
-/// for raylib, leaks it (to gurentee audio is statically loaded),
-/// then loads all needed audio samples
-#[derive(Debug)]
-pub struct Audio {
- pub speak: Sound,
-}
-impl Audio {
- 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()?));
-
- // yes i know this is sans undertale, it was funny
- // and i cannot think of anything better yet, haha
- // - freya
- let speak = load_audio!(handle, "assets/speak.ogg");
-
- Ok(Self { speak })
- }
-}
diff --git a/graphics/src/audio/channel.rs b/graphics/src/audio/channel.rs
new file mode 100644
index 0000000..be12f75
--- /dev/null
+++ b/graphics/src/audio/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/pulse_12.wav"),
+ pulse_25: load_audio!(handle, "assets/pulse_25.wav"),
+ pulse_50: load_audio!(handle, "assets/pulse_50.wav"),
+ pulse_75: load_audio!(handle, "assets/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/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/noise_0.wav"),
+ noise_1: load_audio!(handle, "assets/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/graphics/src/audio/data.rs b/graphics/src/audio/data.rs
new file mode 100644
index 0000000..8cbdf45
--- /dev/null
+++ b/graphics/src/audio/data.rs
@@ -0,0 +1,65 @@
+use crate::audio::{parse, program::Program};
+
+const MELODY: &str = r#"
+; setup
+a v100 d50
+
+%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 pg3s 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
+
+notes d3
+notes c3
+notes b3
+notes b3f
+
+notes d3
+notes c3
+notes b3
+notes b3f
+"#;
+
+const BASE: &str = r#"
+-128
+
+; setup
+b v100 d50
+
+%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 --
+%end
+
+notes d2 d2
+notes c2 c2
+notes b2 b2
+notes b2 c2
+"#;
+
+pub struct Data {
+ pub test: Program,
+}
+impl Data {
+ pub fn load() -> parse::Result<Self> {
+ let melody = Program::parse(MELODY, true)?;
+ let base = Program::parse(BASE, true)?;
+ let test = melody.merge(base);
+ Ok(Self { test })
+ }
+}
diff --git a/graphics/src/audio/mod.rs b/graphics/src/audio/mod.rs
new file mode 100644
index 0000000..473c05b
--- /dev/null
+++ b/graphics/src/audio/mod.rs
@@ -0,0 +1,91 @@
+//! The `audio` crate stores all audio assets that need to be loaded during runtime
+
+use std::time::{Duration, Instant};
+
+use channel::{Channel, NoiseChannel, PulseChannel, TriangleChannel};
+use raylib::audio::RaylibAudio;
+
+use crate::audio::data::Data;
+
+mod channel;
+mod data;
+mod parse;
+mod program;
+
+pub const TIME_SLICE: Duration = Duration::from_millis(1000 / 60);
+
+/// 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(crate) 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.test.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/graphics/src/audio/parse/lex.rs b/graphics/src/audio/parse/lex.rs
new file mode 100644
index 0000000..f2500da
--- /dev/null
+++ b/graphics/src/audio/parse/lex.rs
@@ -0,0 +1,215 @@
+use std::{
+ fmt::Display,
+ iter::Peekable,
+ str::{Chars, FromStr},
+};
+
+use super::Result;
+use crate::audio::{channel::DutyCycle, program::ChanSpec};
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Token {
+ Eof,
+ LineSeparator,
+ Pause(usize),
+ 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()? {
+ 's' => {
+ self.next()?;
+ 1
+ }
+ 'f' => {
+ 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()?),
+ // 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/graphics/src/audio/parse/macros.rs b/graphics/src/audio/parse/macros.rs
new file mode 100644
index 0000000..1dc33eb
--- /dev/null
+++ b/graphics/src/audio/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/graphics/src/audio/parse/mod.rs b/graphics/src/audio/parse/mod.rs
new file mode 100644
index 0000000..9ff243d
--- /dev/null
+++ b/graphics/src/audio/parse/mod.rs
@@ -0,0 +1,12 @@
+use crate::audio::{parse::parser::Parser, program::Instruction};
+
+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/graphics/src/audio/parse/parser.rs b/graphics/src/audio/parse/parser.rs
new file mode 100644
index 0000000..fbe5f4c
--- /dev/null
+++ b/graphics/src/audio/parse/parser.rs
@@ -0,0 +1,100 @@
+use std::iter::Peekable;
+
+use crate::audio::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));
+ }
+ _ => 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/graphics/src/audio/program.rs b/graphics/src/audio/program.rs
new file mode 100644
index 0000000..f31be06
--- /dev/null
+++ b/graphics/src/audio/program.rs
@@ -0,0 +1,173 @@
+use crate::audio::{
+ 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,
+ 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,
+ tempo: u32,
+ pause: u32,
+}
+impl Program {
+ pub const fn new(ins: Vec<Instruction>, looping: bool) -> Self {
+ let pc = ins.len();
+ Self {
+ ins,
+ pc,
+ looping,
+ tempo: 120,
+ pause: 0,
+ }
+ }
+
+ 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 l_ins = self.ins;
+ let r_ins = other.ins;
+ let mut l = 0;
+ let mut r = 0;
+ loop {
+ if r >= r_ins.len() {
+ break;
+ }
+ let ins = r_ins[r];
+ if ins == Instruction::Pause {
+ // inc l until even
+ loop {
+ if l == l_ins.len() {
+ l_ins.push(Instruction::Pause);
+ break;
+ }
+ if l_ins[l] == Instruction::Pause {
+ l += 1;
+ break;
+ }
+ l += 1;
+ }
+ } else {
+ l_ins.insert(l, ins);
+ }
+ r += 1;
+ }
+
+ Self::new(l_ins, self.looping)
+ }
+
+ fn exec_ins(&mut self, channels: &mut Channels, ins: Instruction) {
+ use Instruction as I;
+ match ins {
+ I::Pause => {
+ // pause execution until next `exec` call
+ self.pause = self.tempo / 60;
+ }
+ 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 > 0 {
+ self.pause -= 1;
+ return;
+ }
+ loop {
+ if self.pause > 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;
+ }
+}
diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs
index fef5f9e..63233f5 100644
--- a/graphics/src/lib.rs
+++ b/graphics/src/lib.rs
@@ -270,7 +270,7 @@ impl Window {
}
/// Get audio data for the window
- pub const fn audio(&self) -> &Audio {
- &self.audio
+ pub const fn audio(&mut self) -> &mut Audio {
+ &mut self.audio
}
}