ffmml 0.1.1

Famicon (a.k.a. NES) Flavored Music Macro Language
Documentation
use crate::{
    channel::Channels,
    comment::CommentsOrWhitespaces,
    definitions::{Composer, Definition, Programer, Title},
    macros::Macros,
    oscillators::Oscillator,
    player::MusicPlayer,
};
use std::{
    borrow::Cow,
    error::Error,
    path::{Path, PathBuf},
    sync::Arc,
};
use textparse::{ParseError, Parser, Position, Span};

/// Music object built from an MML script.
#[derive(Debug, Clone)]
pub struct Music {
    title: Option<Title>,
    composer: Option<Composer>,
    programer: Option<Programer>,
    macros: Arc<Macros>,
    channels: Channels,
}

impl Music {
    /// Parses the given MML script and creates a [`Music`] instance.
    ///
    /// This is equivalent with `script.parse::<Music>()`.
    pub fn new(script: &str) -> Result<Self, ParseMusicError> {
        script.parse()
    }

    fn parse(parser: &mut Parser) -> Option<Result<Self, ParseMusicError>> {
        let mut channels = Channels::new();

        let mut title = None;
        let mut composer = None;
        let mut programer = None;
        loop {
            let _: CommentsOrWhitespaces = parser.parse()?;
            if parser.peek_char() != Some('#') {
                break;
            }

            match parser.parse()? {
                Definition::Title(x) => {
                    title = Some(x);
                }
                Definition::Composer(x) => {
                    composer = Some(x);
                }
                Definition::Programer(x) => {
                    programer = Some(x);
                }
                Definition::Channel(x) => {
                    for name in x.channel_names().names() {
                        channels.add_channel(*name, Oscillator::from_kind(x.oscillator_kind()));
                    }
                }
            }
        }

        let _: CommentsOrWhitespaces = parser.parse()?;
        let mut macros = Macros::default();
        macros.parse(parser)?;

        let _: CommentsOrWhitespaces = parser.parse()?;
        if let Err(e) = channels.parse(parser)? {
            return Some(Err(e));
        }

        Some(Ok(Self {
            title,
            composer,
            programer,
            macros: Arc::new(macros),
            channels,
        }))
    }

    /// Music title defined by `#TITLE <VALUE>` in the script.
    pub fn title(&self) -> Option<&str> {
        self.title.as_ref().map(|x| x.get())
    }

    /// Name of the composer of this music defined by `#COMPOSER <VALUE>` in the script.
    pub fn composer(&self) -> Option<&str> {
        self.composer.as_ref().map(|x| x.get())
    }

    /// Name of the programmer who wrote this music script defined by `#PROGRAMER <VALUE>` in the script.
    ///
    /// Note that the typo is intentional.
    pub fn programer(&self) -> Option<&str> {
        self.programer.as_ref().map(|x| x.get())
    }

    pub(crate) fn macros(&self) -> Arc<Macros> {
        self.macros.clone()
    }

    pub(crate) fn channels(&self) -> &Channels {
        &self.channels
    }

    /// Returns a [`MusicPlayer`] instance that generates audio samples.
    pub fn play(&self, sample_rate: u16) -> MusicPlayer {
        MusicPlayer::new(self, sample_rate)
    }
}

impl std::str::FromStr for Music {
    type Err = ParseMusicError;

    fn from_str(text: &str) -> Result<Self, Self::Err> {
        let mut parser = Parser::new(text);
        match Self::parse(&mut parser) {
            None => Err(ParseMusicError::from(parser.into_parse_error())),
            Some(Err(mut e)) => {
                e.text = text.to_owned();
                Err(e)
            }
            Some(Ok(v)) => Ok(v),
        }
    }
}

/// An error returned from [`Music::new()`].
pub struct ParseMusicError {
    textparse_error: Option<Box<ParseError>>,
    text: String,
    position: Position,
    reason: String,
    file_path: Option<PathBuf>,
}

impl ParseMusicError {
    pub(crate) fn new(item: impl Span, reason: &str) -> Self {
        Self {
            textparse_error: None,
            text: String::new(),
            position: item.start_position(),
            reason: reason.to_owned(),
            file_path: None,
        }
    }

    /// Sets the file path of the target MML script.
    ///
    /// The default value is `<UNKNOWN>`.
    pub fn file_path<P: AsRef<Path>>(mut self, file_path: P) -> Self {
        if let Some(e) = self.textparse_error.take() {
            self.textparse_error = Some(Box::new(e.file_path(file_path)));
        } else {
            self.file_path = Some(file_path.as_ref().to_path_buf());
        }
        self
    }
}

impl std::fmt::Debug for ParseMusicError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(e) = &self.textparse_error {
            return e.fmt(f);
        }

        let offset = self.position.get();
        let (line, column) = self.position.line_and_column(&self.text);
        writeln!(f, "{}", self.reason)?;
        writeln!(
            f,
            "  --> {}:{line}:{column}",
            self.file_path
                .as_ref()
                .map(|s| s.to_string_lossy())
                .unwrap_or(Cow::Borrowed("<UNKNOWN>"))
        )?;

        let line_len = format!("{line}").len();
        writeln!(f, "{:line_len$} |", ' ')?;
        writeln!(
            f,
            "{line} | {}",
            self.text[offset + 1 - column..]
                .lines()
                .next()
                .unwrap_or("")
        )?;
        writeln!(f, "{:line_len$} | {:>column$} {}", ' ', '^', self.reason)?;
        Ok(())
    }
}

impl std::fmt::Display for ParseMusicError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{self:?}")
    }
}

impl Error for ParseMusicError {}

impl From<ParseError> for ParseMusicError {
    fn from(value: ParseError) -> Self {
        Self {
            textparse_error: Some(Box::new(value)),
            text: String::new(),
            position: Position::new(0),
            reason: String::new(),
            file_path: None,
        }
    }
}