From 2274d33e469aca544a7aeb899a10769b973ae374 Mon Sep 17 00:00:00 2001 From: Freya Murphy Date: Sat, 22 Nov 2025 13:29:10 -0500 Subject: audio: refactor into seperate crate --- Cargo.lock | 8 ++ Cargo.toml | 3 +- audio/Cargo.toml | 18 +++ audio/src/channel.rs | 199 +++++++++++++++++++++++++++++++++ audio/src/data.rs | 30 +++++ audio/src/lib.rs | 97 +++++++++++++++++ audio/src/parse/lex.rs | 218 +++++++++++++++++++++++++++++++++++++ audio/src/parse/macros.rs | 103 ++++++++++++++++++ audio/src/parse/mod.rs | 13 +++ audio/src/parse/parser.rs | 104 ++++++++++++++++++ audio/src/program.rs | 184 +++++++++++++++++++++++++++++++ graphics/Cargo.toml | 3 +- graphics/src/audio/channel.rs | 199 --------------------------------- graphics/src/audio/data.rs | 30 ----- graphics/src/audio/mod.rs | 92 ---------------- graphics/src/audio/parse/lex.rs | 218 ------------------------------------- graphics/src/audio/parse/macros.rs | 103 ------------------ graphics/src/audio/parse/mod.rs | 12 -- graphics/src/audio/parse/parser.rs | 104 ------------------ graphics/src/audio/program.rs | 184 ------------------------------- graphics/src/lib.rs | 8 +- 21 files changed, 983 insertions(+), 947 deletions(-) create mode 100644 audio/Cargo.toml create mode 100644 audio/src/channel.rs create mode 100644 audio/src/data.rs create mode 100644 audio/src/lib.rs create mode 100644 audio/src/parse/lex.rs create mode 100644 audio/src/parse/macros.rs create mode 100644 audio/src/parse/mod.rs create mode 100644 audio/src/parse/parser.rs create mode 100644 audio/src/program.rs delete mode 100644 graphics/src/audio/channel.rs delete mode 100644 graphics/src/audio/data.rs delete mode 100644 graphics/src/audio/mod.rs delete mode 100644 graphics/src/audio/parse/lex.rs delete mode 100644 graphics/src/audio/parse/macros.rs delete mode 100644 graphics/src/audio/parse/mod.rs delete mode 100644 graphics/src/audio/parse/parser.rs delete mode 100644 graphics/src/audio/program.rs diff --git a/Cargo.lock b/Cargo.lock index dfa2443..24f74cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,13 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6" +[[package]] +name = "audio" +version = "0.1.0" +dependencies = [ + "raylib", +] + [[package]] name = "bindgen" version = "0.71.1" @@ -184,6 +191,7 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" name = "graphics" version = "0.1.0" dependencies = [ + "audio", "dungeon", "raylib", ] diff --git a/Cargo.toml b/Cargo.toml index d0ec1b6..8af51a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["dungeon", "game", "graphics"] +members = ["audio","dungeon", "game", "graphics"] [workspace.package] version = "0.1.0" @@ -16,6 +16,7 @@ publish = false rust-version = "1.88" [workspace.dependencies] +audio = { path = "audio" } dungeon = { path = "dungeon" } game = { path = "game" } getrandom = "0.3" 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 { + 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 { + 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 { + 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 { + 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; + +/// The `Result` type used witin this crate +pub type Result = std::result::Result; + +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 { + // 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 { + 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>, + 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, advance: bool) -> Result { + 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 { + let c = self.chars.peek().copied(); + self.filter_char(c, false) + } + + fn next(&mut self) -> Result { + let c = self.chars.next(); + self.filter_char(c, true) + } + + fn next_int(&mut self) -> Result + where + T: FromStr, + ::Err: Display, + { + let start = self.pos; + while self.peek()?.is_ascii_digit() { + self.next()?; + } + let str = &self.src[start..self.pos]; + str.parse::().map_err(|e| e.to_string()) + } + + fn next_note(&mut self) -> Result { + 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 { + match self.next_int()? { + 0 => Ok(false), + _ => Ok(true), + } + } + + fn next_duty_cycle(&mut self) -> Result { + 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 { + 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 { + 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; + + fn next(&mut self) -> Option { + 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(c: char) -> Result { + 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::>(); + 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>, 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>, idx: usize, body: &str) { + let invoke_line = contents.remove(idx); + let args = invoke_line.split_whitespace().skip(1).collect::>(); + + 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 = std::result::Result; + +mod lex; +mod macros; +mod parser; + +pub fn parse(raw_src: &str) -> Result> { + 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>, +} +impl<'s> Parser<'s> { + pub fn new(src: &'s str) -> Self { + Self { + lexer: Lexer::new(src).peekable(), + } + } + + fn next(&mut self) -> Result { + self.lexer + .next() + .unwrap_or_else(|| Err("should not happen".to_owned())) + } + + fn peek(&mut self) -> Result { + self.lexer + .peek() + .map_or_else(|| Err("should not happen".to_owned()), Result::clone) + } + + fn parse_chan_spec(&mut self) -> Result { + match self.next()? { + Token::ChanSpec(spec) => Ok(spec), + t => Err(format!("expected channel specifier, got {t:?}")), + } + } + + fn parse_ins(&mut self, spec: ChanSpec) -> Result { + 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) -> 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> { + 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: Token) -> Result { + 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, + pc: usize, + looping: bool, + pause_cnt: u32, + pause_len: u32, +} +impl Program { + pub const fn new(ins: Vec, 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 { + 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; + } +} diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 0240745..0687272 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -8,6 +8,7 @@ publish.workspace = true rust-version.workspace = true [dependencies] +audio.workspace = true dungeon.workspace = true raylib.workspace = true @@ -19,7 +20,7 @@ default = [] x11 = ["raylib/GLFW_BUILD_X11"] wayland = ["raylib/wayland", "raylib/GLFW_BUILD_WAYLAND"] sdl = ["raylib/sdl", "raylib/SUPPORT_MODULE_RTEXT"] -static = [] +static = ["audio/static"] # Individual features seem to currently be # broken on windows diff --git a/graphics/src/audio/channel.rs b/graphics/src/audio/channel.rs deleted file mode 100644 index a1ad144..0000000 --- a/graphics/src/audio/channel.rs +++ /dev/null @@ -1,199 +0,0 @@ -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 { - 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 { - 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 { - 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/graphics/src/audio/data.rs b/graphics/src/audio/data.rs deleted file mode 100644 index 9c68523..0000000 --- a/graphics/src/audio/data.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::audio::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 { - let megalovania = load_asm!( - "assets/asm/megalovania_melody.asm", - "assets/asm/megalovania_base.asm" - ); - Ok(Self { megalovania }) - } -} diff --git a/graphics/src/audio/mod.rs b/graphics/src/audio/mod.rs deleted file mode 100644 index 2b85a0f..0000000 --- a/graphics/src/audio/mod.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! 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; - -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 { - // 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 { - 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/graphics/src/audio/parse/lex.rs b/graphics/src/audio/parse/lex.rs deleted file mode 100644 index 9969c9e..0000000 --- a/graphics/src/audio/parse/lex.rs +++ /dev/null @@ -1,218 +0,0 @@ -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), - 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>, - 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, advance: bool) -> Result { - 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 { - let c = self.chars.peek().copied(); - self.filter_char(c, false) - } - - fn next(&mut self) -> Result { - let c = self.chars.next(); - self.filter_char(c, true) - } - - fn next_int(&mut self) -> Result - where - T: FromStr, - ::Err: Display, - { - let start = self.pos; - while self.peek()?.is_ascii_digit() { - self.next()?; - } - let str = &self.src[start..self.pos]; - str.parse::().map_err(|e| e.to_string()) - } - - fn next_note(&mut self) -> Result { - 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 { - match self.next_int()? { - 0 => Ok(false), - _ => Ok(true), - } - } - - fn next_duty_cycle(&mut self) -> Result { - 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 { - 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 { - 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; - - fn next(&mut self) -> Option { - 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(c: char) -> Result { - 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 deleted file mode 100644 index 1dc33eb..0000000 --- a/graphics/src/audio/parse/macros.rs +++ /dev/null @@ -1,103 +0,0 @@ -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::>(); - 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>, 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>, idx: usize, body: &str) { - let invoke_line = contents.remove(idx); - let args = invoke_line.split_whitespace().skip(1).collect::>(); - - 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 deleted file mode 100644 index 9ff243d..0000000 --- a/graphics/src/audio/parse/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::audio::{parse::parser::Parser, program::Instruction}; - -pub type Result = std::result::Result; - -mod lex; -mod macros; -mod parser; - -pub fn parse(raw_src: &str) -> Result> { - 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 deleted file mode 100644 index 5157fa1..0000000 --- a/graphics/src/audio/parse/parser.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::iter::Peekable; - -use crate::audio::program::{ChanSpec, Instruction}; - -use super::{ - Result, - lex::{Lexer, Token}, -}; - -pub struct Parser<'s> { - lexer: Peekable>, -} -impl<'s> Parser<'s> { - pub fn new(src: &'s str) -> Self { - Self { - lexer: Lexer::new(src).peekable(), - } - } - - fn next(&mut self) -> Result { - self.lexer - .next() - .unwrap_or_else(|| Err("should not happen".to_owned())) - } - - fn peek(&mut self) -> Result { - self.lexer - .peek() - .map_or_else(|| Err("should not happen".to_owned()), Result::clone) - } - - fn parse_chan_spec(&mut self) -> Result { - match self.next()? { - Token::ChanSpec(spec) => Ok(spec), - t => Err(format!("expected channel specifier, got {t:?}")), - } - } - - fn parse_ins(&mut self, spec: ChanSpec) -> Result { - 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) -> 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> { - 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: Token) -> Result { - let msg = format!("unexpected token: {t:?}"); - Err(msg) -} diff --git a/graphics/src/audio/program.rs b/graphics/src/audio/program.rs deleted file mode 100644 index f5decd6..0000000 --- a/graphics/src/audio/program.rs +++ /dev/null @@ -1,184 +0,0 @@ -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, - 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, - pc: usize, - looping: bool, - pause_cnt: u32, - pause_len: u32, -} -impl Program { - pub const fn new(ins: Vec, 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 { - 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; - } -} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 63233f5..fa83b98 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -3,13 +3,15 @@ use std::time::Duration; +use audio::Audio; use dungeon::Dungeon; -use raylib::prelude::*; +use raylib::{ + RaylibHandle, RaylibThread, + ffi::{KeyboardKey, TraceLogLevel}, +}; -use crate::audio::Audio; use crate::render::Renderer; -mod audio; mod render; mod timer; -- cgit v1.2.3-freya