summaryrefslogtreecommitdiff
path: root/audio/src/parse/lex.rs
diff options
context:
space:
mode:
Diffstat (limited to 'audio/src/parse/lex.rs')
-rw-r--r--audio/src/parse/lex.rs218
1 files changed, 218 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)
+}