summaryrefslogtreecommitdiff
path: root/audio/src/parse
diff options
context:
space:
mode:
Diffstat (limited to 'audio/src/parse')
-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
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)
+}