diff options
Diffstat (limited to 'audio/src/parse')
| -rw-r--r-- | audio/src/parse/lex.rs | 218 | ||||
| -rw-r--r-- | audio/src/parse/macros.rs | 103 | ||||
| -rw-r--r-- | audio/src/parse/mod.rs | 13 | ||||
| -rw-r--r-- | audio/src/parse/parser.rs | 104 |
4 files changed, 438 insertions, 0 deletions
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) +} |