diff --git a/.gitignore b/.gitignore index ab951f8..23829d0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,24 @@ Cargo.lock # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +out/ + +# Flamegraph +*.old +*.data + +# Samply +*.json.gz + +# audio files +*.mp3 +*.raw +*.wav +*.flac + +# VSCode +.vscode/settings.json + +# LibreOffice +*\# diff --git a/Cargo.toml b/Cargo.toml index 5c9009e..2dadb3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,34 +1,59 @@ [package] -name = "bng" -version = "0.1.0" -edition = "2021" +name = "bliplib" +version = "0.2.5" +edition = "2024" +authors = ["Breval Ferrari "] +description = "The Bizarre Language for Intermodulation Programming (BLIP)" +license = "MIT" +include = ["LICENSE", "src/*", "doc/*.txt", "doc/fasteval/*.txt"] +repository = "https://gitdab.com/breval/blip" + +[lib] +name = "bliplib" [dependencies] -amplify = "4.7.0" -anyhow = "1.0" -cfg-if = "1.0.0" -clap = { version = "4.5", features = ["derive"] } -derived-deref = "2.1.0" -fasteval = "0.2.4" -nom = "7.1.3" -serde = { version = "1.0.209", features = ["derive"] } -serde_yml = "0.0.12" -splines = "4.3.1" -strum = { version = "0.26", features = ["derive"] } -strum_macros = "0.26" -tinyaudio = { version = "0.1", optional = true } -bng_macros = { path = "bng_macros" } -const_format = "0.2.33" -thiserror = "1.0.64" -derive-new = "0.7.0" +anyhow = { version = "1.0" } +cfg-if = "1" +clap = { version = "4.5.39", features = ["derive"], optional = true } +derive-new = "0.7" +derive_builder = "0.20" +derive_wrapper = "0.1" +fasteval = "0.2" +flacenc = { version = "0.4", optional = true } +getset = "0.1.5" +hound = { version = "3.5", optional = true } +lazy_static = "1.5" +mp3lame-encoder = { version = "0.2", features = ["std"], optional = true } +nom = "8.0" +nom_locate = "5.0" +raw_audio = { version = "0.0", optional = true } +strum = { version = "0.27", features = ["derive"] } +thiserror = "2.0" +rodio = { version = "0.20", default-features = false, optional = true } +dasp_sample = { version = "0", optional = true } +log = "0" +env_logger = { version = "0", optional = true } +fon = "0.5" [features] -default = ["play", "save"] -play = ["dep:tinyaudio"] -save = [] +default = ["all-formats"] +bin = ["clap", "rodio", "dasp_sample", "env_logger"] +all-formats = ["mp3", "wav", "flac", "raw"] +mp3 = ["mp3lame-encoder"] +wav = ["hound"] +flac = ["flacenc"] +raw = ["raw_audio"] -[workspace] -members = ["bng_macros"] +[[bin]] +name = "blip" +path = "src/cli/main.rs" +required-features = ["bin"] +include = ["doc/language-design", "doc/fasteval/*"] + +[[example]] +name = "blip" +path = "src/cli/main.rs" +required-features = ["bin"] [lints.rust] -unused = "allow" +missing_docs = "warn" diff --git a/LICENSE b/LICENSE index f347120..9a714c0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,34 +1,7 @@ -NUCLEAR WASTE SOFTWARE LICENSE V1.0 +Copyright © 2025 Breval Ferrari -Copyright 2024 Breval Ferrari +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -This software license is a message... and part of a system of messages... -pay attention to it! Writing this software and associated documentation -files (the "Software") was important to us. We considered ourselves to be a -powerful culture. This Software is not a place of honor... no highly -esteemed deed is commemorated here... nothing valued is here. What is here was -dangerous and repulsive to us. This message is a warning about danger. The -danger is in a particular location... it increases towards a center... the -center of danger is here... of a particular size and syntax. The danger is -still present, in your time, as it was in ours. The danger is to the mind, and -it can kill. The form of the danger is an emanation of incoherence. The danger -is unleashed only if you substantially disturb this software digitally or -intellectually. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -This software is best shunned and left unexecuted... however, permission is -hereby granted... free of charge... to any person obtaining a copy of this -Software... to deal in the Software without restriction, including without -limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom -the Software is furnished to do so... subject to the following conditions: - -The above copyright notice, warning message, and this permission notice shall -be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, DEATH OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 1d84fe9..0f13d10 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,20 @@ -# bng -[![Crates.io](https://img.shields.io/crates/v/bng.svg)](https://crates.io/crates/bng) -[![Docs.rs](https://docs.rs/bng/badge.svg)](https://docs.rs/bng) -[![License: MIT](https://img.shields.io/badge/License-NWSL-yellow.svg)](https://gist.github.com/DavidBuchanan314/35cb9f8a2f754b9a03a74bed19575661) +bppt logo -Bleeperpreter New Gen - the smarter 'preter! -A better [Bleeperpreter](https://github.com/p6nj/bleeperpreter) from scratch. +[![Crates.io](https://img.shields.io/crates/v/bliplib.svg)](https://crates.io/crates/bliplib) +[![Docs.rs](https://docs.rs/bliplib/badge.svg)](https://docs.rs/bliplib) +[![License: MIT](https://img.shields.io/crates/l/bliplib)](LICENSE) + +# blip + +A better and more flexible [Bleeperpreter](https://github.com/p6nj/bleeperpreter) from scratch. + +| | the | +| :--- | :-------------- | +| B | Bizarre | +| L | Language for | +| I | Intermodulation | +| P | Programming | + +```sh +cargo install bliplib --features bin +``` diff --git a/bng_macros/Cargo.toml b/bng_macros/Cargo.toml deleted file mode 100644 index 1f75c20..0000000 --- a/bng_macros/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "bng_macros" -version = "0.1.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1.0" -quote = "1.0" -syn = { version = "2.0", features = ["full"] } diff --git a/bng_macros/src/lib.rs b/bng_macros/src/lib.rs deleted file mode 100644 index 2ea9f67..0000000 --- a/bng_macros/src/lib.rs +++ /dev/null @@ -1,84 +0,0 @@ -extern crate proc_macro; -use proc_macro::TokenStream; -use proc_macro2::Span; -use quote::quote; -use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Ident}; - -#[proc_macro_derive(SlopeModifierParser)] -pub fn slope_modifier_parser(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - impl_slope_modifier_parser(ast) -} - -#[proc_macro_derive(QuickModifierParser)] -pub fn quick_modifier_parser(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - impl_quick_modifier_parser(ast) -} - -fn impl_slope_modifier_parser(ast: DeriveInput) -> TokenStream { - let name = &ast.ident; - if let Data::Enum(DataEnum { variants, .. }) = ast.data { - let match_arms = variants.iter().map(|variant| { - let variant_name = &variant.ident; - let const_name = Ident::new(&variant.ident.to_string().to_uppercase(),Span::call_site()); - quote! { - nom::combinator::value(#name::#variant_name, nom::character::complete::char(SlopeModifier::#const_name)) - } - }); - - quote! { - impl lex::lexer::Parse for #name { - fn parse(input: &str) -> nom::IResult<&str, #name> { - nom::branch::alt(( - #(#match_arms),* - ))(input) - } - } - } - .into() - } else { - panic!("this macro only works on enums") - } -} - -fn impl_quick_modifier_parser(ast: DeriveInput) -> TokenStream { - let name = &ast.ident; - if let Data::Enum(DataEnum { variants, .. }) = ast.data { - let match_arms = variants.iter().map(|variant| { - let variant_name = &variant.ident; - let const_name = - Ident::new(&variant.ident.to_string().to_uppercase(), Span::call_site()); - quote! { - nom::combinator::map( - nom::branch::alt( - ( - nom::combinator::value( - lex::UP, - nom::character::complete::char(#name::#const_name.0) - ), - nom::combinator::value( - lex::DOWN, - nom::character::complete::char(#name::#const_name.1) - ) - ) - ), - #name::#variant_name - ) - } - }); - - quote! { - impl lex::lexer::Parse for #name { - fn parse(input: &str) -> nom::IResult<&str, #name> { - nom::branch::alt(( - #(#match_arms),* - ))(input) - } - } - } - .into() - } else { - panic!("this macro only works on enums") - } -} diff --git a/doc/BLIP logo.png b/doc/BLIP logo.png new file mode 100644 index 0000000..8d9011f Binary files /dev/null and b/doc/BLIP logo.png differ diff --git a/doc/BLIP presentation.odp b/doc/BLIP presentation.odp new file mode 100644 index 0000000..46166c4 Binary files /dev/null and b/doc/BLIP presentation.odp differ diff --git a/doc/BLIP report.odt b/doc/BLIP report.odt new file mode 100644 index 0000000..e0e128e Binary files /dev/null and b/doc/BLIP report.odt differ diff --git a/doc/BLIP report.pdf b/doc/BLIP report.pdf new file mode 100644 index 0000000..e77fa04 Binary files /dev/null and b/doc/BLIP report.pdf differ diff --git a/doc/Cahier des charges.pdf b/doc/Cahier des charges.pdf new file mode 100644 index 0000000..fd0d58a Binary files /dev/null and b/doc/Cahier des charges.pdf differ diff --git a/doc/cli-design.txt b/doc/cli-design.txt new file mode 100644 index 0000000..7007309 --- /dev/null +++ b/doc/cli-design.txt @@ -0,0 +1,58 @@ +A flexible music programming language powered by math. + +usage: + +blip [play [FILENAME | -c SHEET_MUSIC] [-i EXPR] [-l EXPR] -n NOTE[,...] [-v CHAR=VALUE] | export [FILENAME | -c SHEET_MUSIC] [-i EXPR] [-l EXPR] -n NOTE[,...] [-v CHAR=VALUE] [-f FORMAT] [-o FILENAME] | help [syntax | examples | example | formats]] + + +modes: + play - play a song + FILENAME + name of the file to use as the sheet music [default: standard in] + -c SHEET_MUSIC + sheet music to read instead of reading a file + -n NOTE[,...], --notes NOTE[,...] + available notes: a list of comma-separated alphabetical note names + -i EXPR, --instrument EXPR + signal expression (instrument) to generate music samples (you can use the floating-point number t for the current time in seconds, n for the indice of the currently playing note in the list of available notes starting with 0 and N for the number of available notes) [default: "sin(2*pi()*(442+442*((n+1)/N))*t)"] + -l EXPR, --length EXPR + expression to generate note length in seconds [default: "2^(2-log(2, l))*(60/T)"] + -v VARIABLE=VALUE, --variable VARIABLE=VALUE + add a variable named VARIABLE (an single letter) for the sheet music and all expressions and set its initial value to VALUE (you can override n, t, l and T which is the only way to set the initial tempo) + -m NAME:EXPANSION, --macro NAME:EXPANSION + add a macro named NAME (single character, must not be alphanumeric) which expands to EXPANSION when called in the sheet music + -s NAME:VARIABLE=EXPR, --slope NAME:VARIABLE=EXPR + add a slope expression named NAME which replaces the value of VARIABLE with the result of EXPR each frame (EXPR can use all available variables and instrument variables plus the variables α for the initial value of VARIABLE, β for the final value and Δ for the total duration of the specific slope) + + export - export a song to an audio file or stdout + FILENAME + name of the file to use as the sheet music [default: standard in] + -c SHEET_MUSIC + sheet music to read instead of reading a file + -n NOTE[,...], --notes NOTE[,...] + available notes: a list of comma-separated alphabetical note names + -i EXPR, --instrument EXPR + signal expression (instrument) to generate music samples (you can use the floating-point number t for the current time in seconds, n for the indice of the currently playing note in the list of available notes starting with 0 and N for the number of available notes) [default: "sin(2*pi()*(442+442*((n+1)/N))*t)"] + -l EXPR, --length EXPR + expression to generate note length in seconds [default: "2^(2-log(2, l))*(60/T)"] + -v VARIABLE=VALUE, --variable VARIABLE=VALUE + add a variable named VARIABLE (an single letter) for the sheet music and all expressions and set its initial value to VALUE (you can override n, t, l and T which is the only way to set the initial tempo) + -m NAME:EXPANSION, --macro NAME:EXPANSION + add a macro named NAME (single character, must not be alphanumeric) which expands to EXPANSION when called in the sheet music + -s NAME:VARIABLE=EXPR, --slope NAME:VARIABLE=EXPR + add a slope expression named NAME which replaces the value of VARIABLE with the result of EXPR each frame (EXPR can use all available variables and instrument variables plus the variables α for the initial value of VARIABLE, β for the final value and Δ for the total duration of the specific slope) + -f, --format FORMAT + audio format to use [default: mp3 or raw if the codec is unavailable] + see "help formats" + -o, --output FILENAME + output file [default: standard out] + + help - help menu + syntax + show a help message about expression and sheet music syntax + examples + show a list of expression and sheet music examples + example + show the nth example from the list provided by --examples + formats + show a list of available formats for audio export diff --git a/doc/examples.txt b/doc/examples.txt new file mode 100644 index 0000000..deeeb99 --- /dev/null +++ b/doc/examples.txt @@ -0,0 +1,2 @@ +sine wave: sin(2*pi()*(442*2^((n+1)/N))*t) +classic BeepComp length: 2^(2-log(2, l))*(60/T) \ No newline at end of file diff --git a/doc/fasteval/functions.txt b/doc/fasteval/functions.txt new file mode 100644 index 0000000..26c10c8 --- /dev/null +++ b/doc/fasteval/functions.txt @@ -0,0 +1,30 @@ + * print(...strings and values...) -- Prints to stderr. Very useful to 'probe' an expression. + Evaluates to the last value. + Example: `print("x is", x, "and y is", y)` + Example: `x + print("y:", y) + z == x+y+z` + + * log(base=10, val) -- Logarithm with optional 'base' as first argument. + If not provided, 'base' defaults to '10'. + Example: `log(100) + log(e(), 100)` + + * e() -- Euler's number (2.718281828459045) + * pi() -- π (3.141592653589793) + + * int(val) + * ceil(val) + * floor(val) + * round(modulus=1, val) -- Round with optional 'modulus' as first argument. + Example: `round(1.23456) == 1 && round(0.001, 1.23456) == 1.235` + + * abs(val) + * sign(val) + + * min(val, ...) -- Example: `min(1, -2, 3, -4) == -4` + * max(val, ...) -- Example: `max(1, -2, 3, -4) == 3` + + * sin(radians) * asin(val) + * cos(radians) * acos(val) + * tan(radians) * atan(val) + * sinh(val) * asinh(val) + * cosh(val) * acosh(val) + * tanh(val) * atanh(val) \ No newline at end of file diff --git a/doc/fasteval/literals.txt b/doc/fasteval/literals.txt new file mode 100644 index 0000000..1cd3b27 --- /dev/null +++ b/doc/fasteval/literals.txt @@ -0,0 +1,17 @@ +Several numeric formats are supported: + + Integers: 1, 2, 10, 100, 1001 + + Decimals: 1.0, 1.23456, 0.000001 + + Exponents: 1e3, 1E3, 1e-3, 1E-3, 1.2345e100 + + Suffix: + 1.23p = 0.00000000000123 + 1.23n = 0.00000000123 + 1.23µ, 1.23u = 0.00000123 + 1.23m = 0.00123 + 1.23K, 1.23k = 1230 + 1.23M = 1230000 + 1.23G = 1230000000 + 1.23T = 1230000000000 \ No newline at end of file diff --git a/doc/fasteval/operators.txt b/doc/fasteval/operators.txt new file mode 100644 index 0000000..ec1787a --- /dev/null +++ b/doc/fasteval/operators.txt @@ -0,0 +1,11 @@ +Listed in order of precedence: + + (Highest Precedence) ^ Exponentiation + % Modulo + / Division + * Multiplication + - Subtraction + + Addition + == != < <= >= > Comparisons (all have equal precedence) + && and Logical AND with short-circuit + (Lowest Precedence) || or Logical OR with short-circuit diff --git a/doc/iconx4.png b/doc/iconx4.png new file mode 100644 index 0000000..5e3ef7d Binary files /dev/null and b/doc/iconx4.png differ diff --git a/doc/language-design.txt b/doc/language-design.txt new file mode 100644 index 0000000..bfd0cc0 --- /dev/null +++ b/doc/language-design.txt @@ -0,0 +1,15 @@ +notes: dorémi abc... as they appear in the note list +variables: $v8 assign '8' to the variable named 'v' + $v1+2 + $vv+1 + $v-4.1 as soon as the variable mutation string stops being valid, usual sheet music parsing continues (https://en.wikipedia.org/wiki/Maximal_munch) +loops: (...) + (2...) to loop 2 times + (n...) to loop n times (taking the integer part with the floor function) +tuplet: [...] all inner note lengths are shrinked evenly so that the duration of the whole tuplet is the duration of a single note +slope: {name...} as soon as the name stops matching any known slope name, usual sheet music parsing continues (implementation details will surely mean stopping on the first match to prevent weird errors when two slope names start the same) +silence: . silence is music +start-here: % playhead starts here but the context must be kept (outer loops / tuplets etc) +comment: # the rest of the line is ignored completely + +All of these tokens may be separated by "blank" characters (spaces, tabs, carriage returns etc). diff --git a/doc/uml/cli/class.puml b/doc/uml/cli/class.puml new file mode 100644 index 0000000..e821d7b --- /dev/null +++ b/doc/uml/cli/class.puml @@ -0,0 +1,114 @@ +@startuml "blip class diagram" + +left to right direction + +component clap { + interface Parser +} + +component fasteval { + enum Instruction +} + +component mp3lame-encoder { + enum Quality + enum Bitrate +} + +component hound { + enum SampleFormat +} + +component cli { + struct Cli { + mode: Mode + } + enum Mode { + Play(PlayOpts) + Export(ExportOpts) + Help(HelpOpts) + } + Cli --> Mode + struct PlayOpts { + input: Input + instrument: Instruction [-i, --instrument] + length: Instruction [-l, --length] + variables: HashMap [-v, --variable] + macros: HashMap [-m, --macro] + slopes: HashMap [-s, --slope] + } + Mode --> PlayOpts + enum Input { + File(File) + Stdin(Stdin) [default] + String(String) [-c] + } + PlayOpts --> Input + struct ExportOpts { + playopts: PlayOpts + format: Format + output: Output [-c] + } + Mode --> ExportOpts + enum Format { + Mp3 {bitrate: Bitrate, quality: Quality} + Wav {bps: u16, sample_format: SampleFormat} + Flac {bps: usize} + Raw(RawFormat) + } + enum RawFormat { + ALaw + F32Be + F32Le + F64Be + F64Le + MuLaw + S8 + S16Be + S16Le + S24Be + S24Le + S32Be + S32Le + U8 + U16Be + U16Le + U24Be + U24Le + U32Be + U32Le + } + Format --> RawFormat: "feature = "raw"" + ExportOpts --> Format + enum Output { + File(File) + Stdout(Stdout) [default] + } + ExportOpts --> Output + PlayOpts <-- ExportOpts + enum HelpOpts { + Syntax + Examples + Example(u8) + Format + } + Mode --> HelpOpts +} + +component flacenc-rs { + +} + +component raw_audio { + +} + +cli ..|> Parser: derive +PlayOpts --> Instruction +Format --> Bitrate: "feature = "mp3"" +Format --> Quality: "feature = "mp3"" +Format --> SampleFormat: "feature = "wav"" +Format ..> "flacenc-rs": "feature = "flac"" +Format ..> raw_audio: "feature = "raw"" + +@enduml \ No newline at end of file diff --git a/doc/uml/component.puml b/doc/uml/component.puml new file mode 100644 index 0000000..3d72b9a --- /dev/null +++ b/doc/uml/component.puml @@ -0,0 +1,13 @@ +@startuml "Component diagram" +title "Component Diagram" + +component lib +component cli +component "(ide)" as ide +component "(extension)" as extension + +cli --> lib +ide --> lib +extension --> lib + +@enduml \ No newline at end of file diff --git a/doc/uml/lib/class.puml b/doc/uml/lib/class.puml new file mode 100644 index 0000000..54b4e9e --- /dev/null +++ b/doc/uml/lib/class.puml @@ -0,0 +1,126 @@ +@startuml "bliplib class diagram" + +left to right direction +skinparam linetype ortho + +package std { + interface From { + from(value: T): Self + } +} + +package nom { + interface Parser +} + +package fasteval { + enum Instruction +} + +package parser { + interface TokenParser + struct ParserParametters<'n, 's, 'v, I: Input + Clone + nom::Compare, C: Into> { + +notes: &'n [I] + +slopes: &'s HashMap + +variables: &'v [C] + } + struct Parser<'i, 'n, 's, 'v, I: Input + Clone, C: Into> { + -input: &'i I + -parametters: ParserParametters<'n, 's, 'v, I, C> + +parse_all(): Result>, nom::Error>> + } + struct ParserBuilder<'i, 'n, 's, 'v, I: Input + Clone, C: Into> { + -input: Option<&'i I> + -notes: Option<&'n [I]> + -slopes: Option<&'s HashMap> + -variables: Option<&'v [C]> + +input(self, input: &'i I): Self + +notes(self, notes: &'n [I]): Self + +slopes(self, slopes: &'s HashMap): Self + +variables(self, variables: &'v [C]): Self + } +} + +package compiler { + interface Token { + -apply(&self, context: Context): Context + } + struct Silence { + -parser(): impl TokenParser + } + struct Marker { + -parser(): impl TokenParser + } + struct Note { + +n: u8 + -parser<'a, I: nom::Input + nom::Compare + Clone>(notes: &'a [I]): impl TokenParser + } + struct VariableChange { + +name: char, + +change: Instruction + -parser<'a, I, C: Into + Clone>(variables: &'a [C]): impl TokenParser + } + struct Loop { + +times: usize + +inner: Vec> + -parser<'n, 's, 'v, I: Input + Clone + nom::Compare, C: Into>(parametters: ParserParametters<'n, 's, 'v, I, C>): impl TokenParser + } + struct Tuplet { + +inner: Vec> + -parser<'n, 's, 'v, I: Input + Clone + nom::Compare, C: Into>(parametters: ParserParametters<'n, 's, 'v, I, C>): impl TokenParser + } + struct Slope { + +inner: Vec> + +each_frame: VariableChange + -parser<'n, 's, 'v, I: Input + Clone + nom::Compare, C: Into>(parametters: ParserParametters<'n, 's, 'v, I, C>): impl TokenParser + } + struct Context { + +result: Vec + +variables: HashMap + +instrument: Instruction + +slopes: HashMap + +current_length(&self): f64 + +render(&self, n: Option): Vec + } +} + +compiler.Silence ..|> Token +compiler.Marker ..|> Token +compiler.Note ..|> Token +compiler.VariableChange ..|> Token +compiler.Loop ..|> Token +compiler.Tuplet ..|> Token +compiler.Slope ..|> Token +compiler.Token --> parser.Parser +parser.Parser --> VariableChange +parser.ParserBuilder --> VariableChange + +compiler.Loop --> ParserParametters +compiler.Tuplet --> ParserParametters +compiler.Slope --> ParserParametters + +compiler.Silence --> TokenParser +compiler.Marker --> TokenParser +compiler.Note --> TokenParser +compiler.VariableChange --> TokenParser +compiler.Loop --> TokenParser +compiler.Tuplet --> TokenParser +compiler.Slope --> TokenParser + +interface "From" as from_parserbuilder +interface "nom::Parser>" as nomparser_locatedspan + +from_parserbuilder --> parser.ParserBuilder +from_parserbuilder --> parser.ParserParametters +from_parserbuilder --|> std.From +nomparser_locatedspan --|> nom.Parser +parser.Parser --> nomparser_locatedspan +parser.Parser ..|> from_parserbuilder +parser.TokenParser --|> nomparser_locatedspan: Output = Self, Error = nom::Err +nomparser_locatedspan ..|> parser.TokenParser: for any T +compiler.Slope --> compiler.VariableChange +compiler.Token --> compiler.Context +compiler.VariableChange --> Instruction +Context --> Instruction + +@enduml \ No newline at end of file diff --git a/doc/uml/lib/token activity.puml b/doc/uml/lib/token activity.puml new file mode 100644 index 0000000..526d2e9 --- /dev/null +++ b/doc/uml/lib/token activity.puml @@ -0,0 +1,44 @@ +@startuml "Token.apply activity diagram" +title Token.apply + +left footer +Les méthodes et champs de l'argument "context" et "self" ont été raccourcis pour gagner de la place. +"t" will be incremented by placing the length instruction in "slopes". Context.render will tick every slope variable at every frame. +endfooter + +start + +(C) +switch (T) +case (Silence) + :render(None); +case (Marser) + :result = []; +case (Note) + :render(n); +case (VariableChange) + :variables[name] = change.eval(variables); +case (Loop) + :old_context = context.clone(); + :context.result = []; + :new_context = fold inner into current context with Token.apply; + :return new_context.replace(result, old_context.result + new_context.result * count); +case (Tuplet) + :current_length = current_length(); + :calculate how many samples should fit in the current length; + :old_context = context.clone(); + :context.result = []; + :new_context = fold inner into current context with Token.apply; + :new_context.variables[t] = old_context.variables[t]; + :spray new_context.result with void until it fits the sample count; + :prepend old_context.result; + :return new context; +case (Slope) + :slopes += (each_frame.name, each_frame.change); + :context = fold inner into current context with Token.apply; + :slopes[each_frame.name].drop(); +endswitch + +stop + +@enduml \ No newline at end of file diff --git a/flamegraph.svg b/flamegraph.svg new file mode 100644 index 0000000..1b92faf --- /dev/null +++ b/flamegraph.svg @@ -0,0 +1,491 @@ +Flame Graph Reset ZoomSearch <alloc::collections::btree::map::BTreeMap<alloc::string::String,f64> as fasteval::evalns::EvalNamespace>::lookup (3,948,583 samples, 0.02%)<alloc::vec::Vec<T,A> as core::clone::Clone>::clone (4,345,813 samples, 0.03%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (8,336,504 samples, 0.05%)<bliplib::compiler::Expression as core::str::traits::FromStr>::from_str (8,172,369 samples, 0.05%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::next (8,728,623 samples, 0.05%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (30,221,568 samples, 0.19%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (21,567,570 samples, 0.13%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (12,978,675 samples, 0.08%)[libc.so.6] (4,336,871 samples, 0.03%)_int_free_maybe_consolidate.part.0 (4,336,871 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (21,537,098 samples, 0.13%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (8,666,334 samples, 0.05%)<fasteval::parser::Expression as fasteval::compiler::Compiler>::compile (4,341,872 samples, 0.03%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (4,358,465 samples, 0.03%)<hashbrown::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::fold (4,341,140 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (8,672,883 samples, 0.05%)[unknown] (12,992,164 samples, 0.08%)fasteval::compiler::ExprSlice::from_expr (4,319,281 samples, 0.03%)__memmove_avx_unaligned_erms (4,361,439 samples, 0.03%)_int_free_chunk (8,657,545 samples, 0.05%)_int_malloc (8,652,635 samples, 0.05%)_int_realloc (4,330,325 samples, 0.03%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (8,642,661 samples, 0.05%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (8,579,321 samples, 0.05%)bliplib::compiler::Context::current_length (4,357,069 samples, 0.03%)bliplib::compiler::Context::tick (8,043,680 samples, 0.05%)cfree@GLIBC_2.2.5 (13,500,485 samples, 0.08%)core::ptr::drop_in_place<fasteval::compiler::Instruction> (4,305,860 samples, 0.03%)core::ptr::drop_in_place<fasteval::parser::Value> (4,350,452 samples, 0.03%)core::slice::sort::shared::smallsort::insertion_sort_shift_left (4,335,156 samples, 0.03%)fasteval::compiler::ExprSlice::from_expr (4,359,723 samples, 0.03%)fasteval::compiler::ExprSlice::split (8,684,439 samples, 0.05%)fasteval::compiler::compile_add (4,335,303 samples, 0.03%)fasteval::compiler::compile_mul (4,362,968 samples, 0.03%)fasteval::parser::Parser::read_callable (8,539,960 samples, 0.05%)fasteval::parser::Parser::read_expression (4,348,335 samples, 0.03%)fasteval::parser::Parser::read_value (21,623,788 samples, 0.13%)log::logger::NOP (4,345,815 samples, 0.03%)malloc (4,345,815 samples, 0.03%)malloc (34,286,854 samples, 0.21%)[unknown] (362,222,081 samples, 2.25%)[..unlink_chunk.isra.0 (4,256,602 samples, 0.03%)__memcmp_avx2_movbe (8,633,021 samples, 0.05%)__memmove_avx_unaligned_erms (4,323,310 samples, 0.03%)__rustc::__rdl_dealloc (4,278,079 samples, 0.03%)__sin_fma (4,293,930 samples, 0.03%)_int_free_chunk (17,317,174 samples, 0.11%)_int_free_create_chunk (17,290,659 samples, 0.11%)_int_free_merge_chunk (4,337,021 samples, 0.03%)_int_malloc (8,641,711 samples, 0.05%)<alloc::collections::btree::map::BTreeMap<alloc::string::String,f64> as fasteval::evalns::EvalNamespace>::lookup (8,680,646 samples, 0.05%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (34,648,629 samples, 0.22%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (34,648,629 samples, 0.22%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (30,346,528 samples, 0.19%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (21,665,882 samples, 0.13%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (17,309,730 samples, 0.11%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (17,309,730 samples, 0.11%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (13,069,578 samples, 0.08%)<alloc::collections::btree::map::BTreeMap<alloc::string::String,f64> as fasteval::evalns::EvalNamespace>::lookup (8,702,678 samples, 0.05%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (60,598,609 samples, 0.38%)__sin_fma (21,656,497 samples, 0.13%)__rustc::__rdl_alloc (4,266,046 samples, 0.03%)__memcmp_avx2_movbe (12,943,429 samples, 0.08%)__rustc::__rust_dealloc (4,293,890 samples, 0.03%)<hashbrown::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::fold (77,650,104 samples, 0.48%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (38,843,532 samples, 0.24%)cfree@GLIBC_2.2.5 (17,277,101 samples, 0.11%)__rustc::__rdl_alloc (4,350,771 samples, 0.03%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (82,185,862 samples, 0.51%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::next (60,590,177 samples, 0.38%)malloc (47,591,681 samples, 0.30%)_int_malloc (17,358,746 samples, 0.11%)unlink_chunk.isra.0 (4,326,881 samples, 0.03%)alloc::collections::btree::append::<impl alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Owned,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::bulk_push (17,307,588 samples, 0.11%)__memcmp_avx2_movbe (4,301,192 samples, 0.03%)<alloc::collections::btree::map::BTreeMap<K,V> as core::iter::traits::collect::FromIterator<(K,V)>>::from_iter (108,137,112 samples, 0.67%)core::slice::sort::shared::smallsort::insertion_sort_shift_left (8,643,662 samples, 0.05%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (4,345,855 samples, 0.03%)malloc (4,345,855 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (8,640,919 samples, 0.05%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (8,640,919 samples, 0.05%)<alloc::string::String as core::clone::Clone>::clone (4,337,550 samples, 0.03%)fasteval::compiler::ExprSlice::from_expr (4,345,093 samples, 0.03%)__rustc::__rust_alloc (4,349,442 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (56,185,972 samples, 0.35%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (56,185,972 samples, 0.35%)fasteval::slab::CompileSlab::push_instr (43,199,960 samples, 0.27%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (43,199,960 samples, 0.27%)alloc::raw_vec::finish_grow (43,199,960 samples, 0.27%)malloc (38,850,518 samples, 0.24%)_int_malloc (25,953,288 samples, 0.16%)unlink_chunk.isra.0 (4,334,098 samples, 0.03%)cfree@GLIBC_2.2.5 (4,338,630 samples, 0.03%)fasteval::compiler::compile_add (4,238,384 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (90,314,479 samples, 0.56%)fasteval::slab::CompileSlab::push_instr (4,271,824 samples, 0.03%)__rustc::__rdl_alloc (4,323,863 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (107,223,599 samples, 0.67%)<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (102,961,042 samples, 0.64%)fasteval::compiler::ExprSlice::from_expr (12,646,563 samples, 0.08%)malloc (8,322,700 samples, 0.05%)_int_malloc (8,322,700 samples, 0.05%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (8,676,970 samples, 0.05%)cfree@GLIBC_2.2.5 (8,557,281 samples, 0.05%)fasteval::compiler::ExprSlice::split (4,289,840 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (42,597,615 samples, 0.26%)fasteval::compiler::compile_mul (3,867,330 samples, 0.02%)<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (46,958,385 samples, 0.29%)cfree@GLIBC_2.2.5 (4,360,770 samples, 0.03%)__rustc::__rust_dealloc (4,349,959 samples, 0.03%)cfree@GLIBC_2.2.5 (25,916,524 samples, 0.16%)_int_free_chunk (17,361,498 samples, 0.11%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (218,646,490 samples, 1.36%)malloc (4,359,027 samples, 0.03%)cfree@GLIBC_2.2.5 (26,059,453 samples, 0.16%)_int_free_chunk (21,722,016 samples, 0.14%)fasteval::compiler::ExprSlice::split (3,991,150 samples, 0.02%)cfree@GLIBC_2.2.5 (12,881,816 samples, 0.08%)_int_free_chunk (12,881,816 samples, 0.08%)fasteval::compiler::compile_mul (25,683,614 samples, 0.16%)core::ptr::drop_in_place<fasteval::compiler::Instruction> (4,224,641 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (308,504,168 samples, 1.92%)<..fasteval::compiler::push_mul_leaves (34,123,461 samples, 0.21%)alloc::raw_vec::RawVec<T,A>::grow_one (29,804,454 samples, 0.19%)alloc::raw_vec::finish_grow (29,804,454 samples, 0.19%)realloc (29,804,454 samples, 0.19%)_int_realloc (16,867,156 samples, 0.10%)_int_malloc (8,594,217 samples, 0.05%)__rustc::__rust_dealloc (4,315,265 samples, 0.03%)<fasteval::parser::Expression as fasteval::compiler::Compiler>::compile (321,511,390 samples, 2.00%)<..fasteval::compiler::ExprSlice::from_expr (8,691,957 samples, 0.05%)malloc (8,691,957 samples, 0.05%)core::str::<impl str>::trim_end_matches (3,112,271 samples, 0.02%)core::num::dec2flt::<impl core::str::traits::FromStr for f64>::from_str (8,652,101 samples, 0.05%)__rustc::__rust_dealloc (4,300,831 samples, 0.03%)fasteval::parser::Parser::read_value (30,097,789 samples, 0.19%)fasteval::parser::Parser::read_callable (25,814,224 samples, 0.16%)malloc (25,814,224 samples, 0.16%)_int_malloc (21,528,253 samples, 0.13%)fasteval::parser::Parser::read_value (99,333,681 samples, 0.62%)fasteval::parser::Parser::read_callable (77,676,790 samples, 0.48%)fasteval::parser::Parser::read_expression (64,677,773 samples, 0.40%)malloc (30,244,087 samples, 0.19%)_int_malloc (12,964,704 samples, 0.08%)fasteval::parser::Parser::read_expression (163,843,820 samples, 1.02%)fasteval::parser::Parser::read_value (146,534,529 samples, 0.91%)fasteval::parser::Parser::read_expression (129,229,525 samples, 0.80%)malloc (8,665,205 samples, 0.05%)malloc_consolidate (60,269,799 samples, 0.37%)<bliplib::compiler::Expression as core::str::traits::FromStr>::from_str (615,047,604 samples, 3.82%)<bli..malloc (122,285,279 samples, 0.76%)_int_malloc (109,424,713 samples, 0.68%)unlink_chunk.isra.0 (8,652,895 samples, 0.05%)<alloc::vec::Vec<T,A> as core::clone::Clone>::clone (640,517,293 samples, 3.98%)<all..__memmove_avx_unaligned_erms (21,209,992 samples, 0.13%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (4,361,174 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (8,633,330 samples, 0.05%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (4,303,460 samples, 0.03%)__rustc::__rdl_alloc (4,351,978 samples, 0.03%)__rustc::__rdl_dealloc (4,342,750 samples, 0.03%)cfree@GLIBC_2.2.5 (4,276,146 samples, 0.03%)_int_free_chunk (4,276,146 samples, 0.03%)__rustc::__rust_alloc (4,279,010 samples, 0.03%)fasteval::compiler::ExprSlice::split (30,099,987 samples, 0.19%)malloc (25,820,977 samples, 0.16%)_int_malloc (21,546,434 samples, 0.13%)__rustc::__rdl_dealloc (4,343,389 samples, 0.03%)fasteval::compiler::compile_add (25,829,694 samples, 0.16%)fasteval::slab::CompileSlab::push_instr (17,280,975 samples, 0.11%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (17,280,975 samples, 0.11%)alloc::raw_vec::finish_grow (8,674,456 samples, 0.05%)malloc (8,674,456 samples, 0.05%)_int_malloc (8,674,456 samples, 0.05%)malloc_consolidate (4,342,349 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (94,804,262 samples, 0.59%)<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (90,443,088 samples, 0.56%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (90,443,088 samples, 0.56%)malloc (4,325,639 samples, 0.03%)__rustc::__rust_alloc (4,282,832 samples, 0.03%)fasteval::compiler::ExprSlice::split (12,869,482 samples, 0.08%)__rustc::__rdl_alloc (4,298,327 samples, 0.03%)fasteval::compiler::compile_mul (17,311,375 samples, 0.11%)cfree@GLIBC_2.2.5 (4,288,321 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (133,349,198 samples, 0.83%)fasteval::slab::CompileSlab::push_instr (4,081,247 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (137,652,650 samples, 0.86%)<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (137,652,650 samples, 0.86%)fasteval::compiler::ExprSlice::from_expr (4,303,452 samples, 0.03%)cfree@GLIBC_2.2.5 (4,289,646 samples, 0.03%)fasteval::compiler::ExprSlice::split (4,344,115 samples, 0.03%)malloc (4,344,115 samples, 0.03%)cfree@GLIBC_2.2.5 (4,271,754 samples, 0.03%)core::ptr::drop_in_place<fasteval::compiler::Instruction> (4,297,250 samples, 0.03%)fasteval::compiler::compile_mul (25,786,211 samples, 0.16%)fasteval::slab::CompileSlab::push_instr (4,302,922 samples, 0.03%)fasteval::compiler::push_mul_leaves (6,926,638 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::grow_one (6,926,638 samples, 0.04%)alloc::raw_vec::finish_grow (6,926,638 samples, 0.04%)realloc (6,926,638 samples, 0.04%)_int_realloc (6,926,638 samples, 0.04%)_int_malloc (6,926,638 samples, 0.04%)unlink_chunk.isra.0 (4,351,428 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (191,886,065 samples, 1.19%)malloc (4,229,980 samples, 0.03%)cfree@GLIBC_2.2.5 (4,347,166 samples, 0.03%)_int_free_chunk (4,347,166 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (238,845,684 samples, 1.48%)fasteval::compiler::ExprSlice::split (25,706,831 samples, 0.16%)malloc (21,377,049 samples, 0.13%)_int_malloc (17,280,377 samples, 0.11%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (256,111,974 samples, 1.59%)<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (251,823,239 samples, 1.57%)cfree@GLIBC_2.2.5 (4,350,202 samples, 0.03%)_int_free_chunk (4,350,202 samples, 0.03%)cfree@GLIBC_2.2.5 (17,379,131 samples, 0.11%)_int_free_chunk (8,706,161 samples, 0.05%)fasteval::compiler::compile_mul (25,839,870 samples, 0.16%)core::ptr::drop_in_place<fasteval::compiler::Instruction> (8,460,739 samples, 0.05%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (294,815,622 samples, 1.83%)<..malloc (4,258,584 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (312,093,995 samples, 1.94%)<..<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (303,411,193 samples, 1.89%)<..fasteval::compiler::ExprSlice::from_expr (4,266,002 samples, 0.03%)malloc (4,266,002 samples, 0.03%)cfree@GLIBC_2.2.5 (4,348,815 samples, 0.03%)<fasteval::parser::Expression as fasteval::compiler::Compiler>::compile (325,126,859 samples, 2.02%)<..fasteval::compiler::ExprSlice::from_expr (4,320,911 samples, 0.03%)malloc (4,320,911 samples, 0.03%)__rustc::__rdl_alloc (8,720,215 samples, 0.05%)core::str::<impl str>::trim_start_matches (4,304,797 samples, 0.03%)cfree@GLIBC_2.2.5 (8,523,120 samples, 0.05%)__rustc::__rdl_alloc (4,297,192 samples, 0.03%)fasteval::parser::Parser::read_callable (8,674,101 samples, 0.05%)malloc (4,312,783 samples, 0.03%)core::num::dec2flt::<impl core::str::traits::FromStr for f64>::from_str (4,341,236 samples, 0.03%)fasteval::parser::Parser::read_callable (8,661,670 samples, 0.05%)malloc (4,337,433 samples, 0.03%)core::num::dec2flt::<impl core::str::traits::FromStr for f64>::from_str (12,845,352 samples, 0.08%)core::num::dec2flt::parse::parse_number (8,520,918 samples, 0.05%)fasteval::parser::Parser::read_value (21,511,373 samples, 0.13%)fasteval::parser::Parser::read_callable (4,344,749 samples, 0.03%)fasteval::parser::Parser::read_value (142,427,825 samples, 0.89%)fasteval::parser::Parser::read_expression (116,467,554 samples, 0.72%)fasteval::parser::Parser::read_value (90,587,432 samples, 0.56%)fasteval::parser::Parser::read_expression (64,628,367 samples, 0.40%)fasteval::parser::Parser::read_value (47,383,205 samples, 0.29%)fasteval::parser::Parser::read_expression (25,842,012 samples, 0.16%)malloc (4,330,639 samples, 0.03%)fasteval::parser::Parser::read_expression (215,628,769 samples, 1.34%)fasteval::parser::Parser::read_value (207,022,979 samples, 1.29%)fasteval::parser::Parser::read_callable (207,022,979 samples, 1.29%)fasteval::parser::Parser::read_expression (194,202,575 samples, 1.21%)malloc (4,358,289 samples, 0.03%)<bliplib::compiler::Expression as core::str::traits::FromStr>::from_str (663,451,554 samples, 4.12%)<bli..malloc (88,084,024 samples, 0.55%)_int_malloc (30,328,180 samples, 0.19%)malloc_consolidate (8,713,591 samples, 0.05%)__memmove_avx_unaligned_erms (5,941,772,780 samples, 36.93%)__memmove_avx_unaligned_ermsmalloc_consolidate (21,365,719 samples, 0.13%)<bliplib::compiler::Context as core::clone::Clone>::clone (7,357,316,014 samples, 45.73%)<bliplib::compiler::Context as core::clone::Clone>::clonemalloc (98,626,989 samples, 0.61%)_int_malloc (59,985,575 samples, 0.37%)unlink_chunk.isra.0 (4,273,381 samples, 0.03%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (21,508,365 samples, 0.13%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (17,153,359 samples, 0.11%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (17,153,359 samples, 0.11%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (17,153,359 samples, 0.11%)<alloc::collections::btree::map::BTreeMap<alloc::string::String,f64> as fasteval::evalns::EvalNamespace>::lookup (12,839,466 samples, 0.08%)__memcmp_avx2_movbe (4,301,596 samples, 0.03%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (38,733,470 samples, 0.24%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (34,417,392 samples, 0.21%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (30,096,469 samples, 0.19%)pow@@GLIBC_2.29 (4,299,657 samples, 0.03%)__ieee754_pow_fma (4,299,657 samples, 0.03%)__memmove_avx_unaligned_erms (8,669,124 samples, 0.05%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (4,213,562 samples, 0.03%)cfree@GLIBC_2.2.5 (17,224,809 samples, 0.11%)_int_free_maybe_consolidate.part.0 (21,363,354 samples, 0.13%)malloc_consolidate (21,363,354 samples, 0.13%)cfree@GLIBC_2.2.5 (34,339,310 samples, 0.21%)_int_free_chunk (34,339,310 samples, 0.21%)_int_free_merge_chunk (4,289,890 samples, 0.03%)_int_free_create_chunk (4,289,890 samples, 0.03%)unlink_chunk.isra.0 (4,289,890 samples, 0.03%)core::ptr::drop_in_place<(char,bliplib::compiler::Expression)> (4,347,261 samples, 0.03%)cfree@GLIBC_2.2.5 (4,347,261 samples, 0.03%)_int_free_chunk (4,347,261 samples, 0.03%)<alloc::vec::Vec<T,A> as core::ops::drop::Drop>::drop (13,015,736 samples, 0.08%)cfree@GLIBC_2.2.5 (13,015,736 samples, 0.08%)_int_free_chunk (13,015,736 samples, 0.08%)core::ptr::drop_in_place<fasteval::slab::CompileSlab> (17,337,212 samples, 0.11%)cfree@GLIBC_2.2.5 (4,321,476 samples, 0.03%)<alloc::vec::Vec<T,A> as core::ops::drop::Drop>::drop (4,305,581 samples, 0.03%)cfree@GLIBC_2.2.5 (51,181,522 samples, 0.32%)_int_free_chunk (43,130,930 samples, 0.27%)_int_free_merge_chunk (12,940,146 samples, 0.08%)_int_free_create_chunk (4,367,356 samples, 0.03%)cfree@GLIBC_2.2.5 (12,919,851 samples, 0.08%)core::ptr::drop_in_place<fasteval::parser::Expression> (25,855,339 samples, 0.16%)core::ptr::drop_in_place<fasteval::parser::Value> (8,585,628 samples, 0.05%)bliplib::compiler::Context::current_length (7,719,306,430 samples, 47.98%)bliplib::compiler::Context::current_lengthcore::ptr::drop_in_place<bliplib::compiler::Context> (141,687,744 samples, 0.88%)core::ptr::drop_in_place<fasteval::slab::Slab> (85,663,961 samples, 0.53%)core::ptr::drop_in_place<fasteval::parser::Value> (4,321,519 samples, 0.03%)__memmove_avx_unaligned_erms (4,337,309 samples, 0.03%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (72,425,012 samples, 0.45%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::next (68,098,703 samples, 0.42%)malloc (55,832,070 samples, 0.35%)_int_malloc (29,926,066 samples, 0.19%)unlink_chunk.isra.0 (8,567,792 samples, 0.05%)alloc::collections::btree::append::<impl alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Owned,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::bulk_push (17,037,588 samples, 0.11%)core::slice::sort::shared::smallsort::insertion_sort_shift_left (4,349,742 samples, 0.03%)<alloc::collections::btree::map::BTreeMap<K,V> as core::iter::traits::collect::FromIterator<(K,V)>>::from_iter (98,160,405 samples, 0.61%)malloc (4,348,063 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (8,352,205 samples, 0.05%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (8,352,205 samples, 0.05%)__rustc::__rdl_alloc (4,350,526 samples, 0.03%)fasteval::compiler::ExprSlice::from_expr (8,652,310 samples, 0.05%)malloc (4,301,784 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (25,632,365 samples, 0.16%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (25,632,365 samples, 0.16%)fasteval::slab::CompileSlab::push_instr (8,627,850 samples, 0.05%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (8,627,850 samples, 0.05%)alloc::raw_vec::finish_grow (8,627,850 samples, 0.05%)malloc (8,627,850 samples, 0.05%)_int_malloc (4,324,654 samples, 0.03%)__rustc::__rdl_dealloc (4,307,295 samples, 0.03%)cfree@GLIBC_2.2.5 (4,302,209 samples, 0.03%)fasteval::compiler::ExprSlice::split (4,336,706 samples, 0.03%)fasteval::compiler::compile_add (4,345,474 samples, 0.03%)cfree@GLIBC_2.2.5 (4,345,474 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (77,454,232 samples, 0.48%)malloc (8,628,071 samples, 0.05%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (85,983,179 samples, 0.53%)<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (85,983,179 samples, 0.53%)fasteval::compiler::ExprSlice::from_expr (8,528,947 samples, 0.05%)malloc (8,528,947 samples, 0.05%)_int_malloc (8,528,947 samples, 0.05%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (8,628,401 samples, 0.05%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (8,628,401 samples, 0.05%)<alloc::string::String as core::clone::Clone>::clone (4,345,934 samples, 0.03%)__rustc::__rust_alloc (4,345,934 samples, 0.03%)__rustc::__rdl_alloc (4,336,892 samples, 0.03%)__rustc::__rust_alloc (4,292,587 samples, 0.03%)<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (30,127,867 samples, 0.19%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (30,127,867 samples, 0.19%)fasteval::compiler::ExprSlice::split (12,912,248 samples, 0.08%)malloc (4,282,769 samples, 0.03%)cfree@GLIBC_2.2.5 (21,463,968 samples, 0.13%)_int_free_chunk (21,463,968 samples, 0.13%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (159,204,376 samples, 0.99%)core::ptr::drop_in_place<fasteval::compiler::Instruction> (4,326,046 samples, 0.03%)__rustc::__rdl_alloc (3,918,140 samples, 0.02%)cfree@GLIBC_2.2.5 (8,628,611 samples, 0.05%)_int_free_chunk (4,283,160 samples, 0.03%)fasteval::compiler::ExprSlice::split (4,314,403 samples, 0.03%)__rustc::__rdl_alloc (4,314,403 samples, 0.03%)fasteval::compiler::compile_mul (8,599,444 samples, 0.05%)cfree@GLIBC_2.2.5 (4,322,718 samples, 0.03%)_int_free_chunk (4,322,718 samples, 0.03%)__memmove_avx_unaligned_erms (12,881,283 samples, 0.08%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (214,859,168 samples, 1.34%)fasteval::compiler::push_mul_leaves (25,875,936 samples, 0.16%)alloc::raw_vec::RawVec<T,A>::grow_one (25,875,936 samples, 0.16%)alloc::raw_vec::finish_grow (25,875,936 samples, 0.16%)realloc (25,875,936 samples, 0.16%)_int_realloc (25,875,936 samples, 0.16%)_int_malloc (8,633,982 samples, 0.05%)unlink_chunk.isra.0 (4,324,626 samples, 0.03%)cfree@GLIBC_2.2.5 (12,381,133 samples, 0.08%)_int_free_chunk (12,381,133 samples, 0.08%)<fasteval::parser::Expression as fasteval::compiler::Compiler>::compile (231,494,846 samples, 1.44%)fasteval::compiler::ExprSlice::from_expr (4,254,545 samples, 0.03%)malloc (4,254,545 samples, 0.03%)__memmove_avx_unaligned_erms (4,177,212 samples, 0.03%)__rustc::__rdl_alloc (8,556,593 samples, 0.05%)__rustc::__rust_alloc (4,282,228 samples, 0.03%)core::str::<impl str>::trim_start_matches (4,353,798 samples, 0.03%)core::num::dec2flt::<impl core::str::traits::FromStr for f64>::from_str (4,350,628 samples, 0.03%)core::num::dec2flt::parse::parse_number (4,350,628 samples, 0.03%)core::num::dec2flt::<impl core::str::traits::FromStr for f64>::from_str (17,229,702 samples, 0.11%)core::num::dec2flt::parse::parse_number (8,619,244 samples, 0.05%)__memmove_avx_unaligned_erms (4,267,292 samples, 0.03%)__rustc::__rust_alloc (4,345,389 samples, 0.03%)fasteval::parser::Parser::read_value (3,947,967 samples, 0.02%)fasteval::parser::Parser::read_callable (3,947,967 samples, 0.02%)malloc (3,947,967 samples, 0.02%)_int_malloc (3,947,967 samples, 0.02%)fasteval::parser::Parser::read_value (78,260,904 samples, 0.49%)fasteval::parser::Parser::read_callable (35,531,745 samples, 0.22%)fasteval::parser::Parser::read_expression (22,614,050 samples, 0.14%)malloc (9,961,424 samples, 0.06%)_int_malloc (4,275,246 samples, 0.03%)fasteval::parser::Parser::read_value (133,514,875 samples, 0.83%)fasteval::parser::Parser::read_expression (107,873,899 samples, 0.67%)malloc (8,294,177 samples, 0.05%)_int_malloc (3,933,409 samples, 0.02%)fasteval::parser::Parser::read_expression (176,191,987 samples, 1.10%)malloc (4,347,612 samples, 0.03%)malloc_consolidate (51,642,787 samples, 0.32%)<bliplib::compiler::Expression as core::str::traits::FromStr>::from_str (532,614,617 samples, 3.31%)<bl..malloc (103,557,953 samples, 0.64%)_int_malloc (94,916,542 samples, 0.59%)unlink_chunk.isra.0 (8,679,826 samples, 0.05%)<alloc::vec::Vec<T,A> as core::clone::Clone>::clone (541,274,484 samples, 3.36%)<al..__memmove_avx_unaligned_erms (4,304,269 samples, 0.03%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (4,342,707 samples, 0.03%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (4,278,734 samples, 0.03%)<alloc::string::String as core::clone::Clone>::clone (4,278,734 samples, 0.03%)__rustc::__rust_alloc (4,278,734 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (30,212,359 samples, 0.19%)<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (25,856,367 samples, 0.16%)<alloc::string::String as core::clone::Clone>::clone (12,905,052 samples, 0.08%)__rustc::__rdl_dealloc (4,308,029 samples, 0.03%)cfree@GLIBC_2.2.5 (12,741,161 samples, 0.08%)fasteval::compiler::ExprSlice::split (17,239,004 samples, 0.11%)malloc (17,239,004 samples, 0.11%)_int_malloc (8,684,162 samples, 0.05%)__rustc::__rust_dealloc (4,363,327 samples, 0.03%)cfree@GLIBC_2.2.5 (4,255,517 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (86,063,418 samples, 0.53%)fasteval::compiler::compile_add (17,261,446 samples, 0.11%)fasteval::slab::CompileSlab::push_instr (4,281,555 samples, 0.03%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (4,281,555 samples, 0.03%)alloc::raw_vec::finish_grow (4,281,555 samples, 0.03%)malloc (4,281,555 samples, 0.03%)_int_malloc (4,281,555 samples, 0.03%)malloc_consolidate (4,281,555 samples, 0.03%)cfree@GLIBC_2.2.5 (4,350,714 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (111,118,372 samples, 0.69%)<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (106,839,638 samples, 0.66%)fasteval::compiler::ExprSlice::from_expr (16,425,506 samples, 0.10%)malloc (16,425,506 samples, 0.10%)_int_malloc (4,339,615 samples, 0.03%)cfree@GLIBC_2.2.5 (8,684,035 samples, 0.05%)__rustc::__rust_alloc (8,540,638 samples, 0.05%)fasteval::compiler::ExprSlice::split (21,221,535 samples, 0.13%)malloc (12,680,897 samples, 0.08%)fasteval::compiler::compile_mul (4,288,246 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (158,352,788 samples, 0.98%)malloc (8,695,276 samples, 0.05%)cfree@GLIBC_2.2.5 (4,240,198 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (166,882,829 samples, 1.04%)<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (166,882,829 samples, 1.04%)fasteval::compiler::ExprSlice::from_expr (4,289,843 samples, 0.03%)malloc (4,289,843 samples, 0.03%)__rustc::__rust_alloc (4,287,651 samples, 0.03%)cfree@GLIBC_2.2.5 (4,289,082 samples, 0.03%)fasteval::compiler::ExprSlice::split (4,364,467 samples, 0.03%)malloc (4,364,467 samples, 0.03%)fasteval::compiler::compile_mul (4,357,782 samples, 0.03%)cfree@GLIBC_2.2.5 (4,357,782 samples, 0.03%)_int_free_chunk (4,357,782 samples, 0.03%)__rustc::__rdl_realloc (4,353,411 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (222,679,601 samples, 1.38%)fasteval::compiler::push_mul_leaves (38,497,790 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::grow_one (34,200,655 samples, 0.21%)alloc::raw_vec::finish_grow (29,886,852 samples, 0.19%)realloc (25,533,441 samples, 0.16%)_int_realloc (17,087,989 samples, 0.11%)_int_malloc (12,749,273 samples, 0.08%)cfree@GLIBC_2.2.5 (17,329,018 samples, 0.11%)_int_free_chunk (13,025,969 samples, 0.08%)fasteval::compiler::ExprSlice::split (25,952,605 samples, 0.16%)malloc (21,600,332 samples, 0.13%)_int_malloc (4,353,710 samples, 0.03%)cfree@GLIBC_2.2.5 (4,275,916 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (288,518,790 samples, 1.79%)<..<fasteval::parser::UnaryOp as fasteval::compiler::Compiler>::compile (279,878,185 samples, 1.74%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (279,878,185 samples, 1.74%)fasteval::compiler::compile_add (9,594,681 samples, 0.06%)core::ptr::drop_in_place<fasteval::compiler::Instruction> (4,370,690 samples, 0.03%)cfree@GLIBC_2.2.5 (8,521,735 samples, 0.05%)_int_free_chunk (4,251,109 samples, 0.03%)fasteval::compiler::ExprSlice::split (4,324,552 samples, 0.03%)malloc (4,324,552 samples, 0.03%)cfree@GLIBC_2.2.5 (17,271,006 samples, 0.11%)_int_free_chunk (8,698,535 samples, 0.05%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (348,048,326 samples, 2.16%)<..fasteval::compiler::compile_mul (25,648,050 samples, 0.16%)fasteval::slab::CompileSlab::push_instr (4,312,503 samples, 0.03%)<fasteval::compiler::ExprSlice as fasteval::compiler::Compiler>::compile (365,284,825 samples, 2.27%)<..<fasteval::parser::StdFunc as fasteval::compiler::Compiler>::compile (365,284,825 samples, 2.27%)<..cfree@GLIBC_2.2.5 (17,236,499 samples, 0.11%)_int_free_chunk (4,340,155 samples, 0.03%)cfree@GLIBC_2.2.5 (8,722,783 samples, 0.05%)_int_free_chunk (4,368,360 samples, 0.03%)<fasteval::parser::Expression as fasteval::compiler::Compiler>::compile (382,636,996 samples, 2.38%)<f..fasteval::compiler::ExprSlice::from_expr (4,358,402 samples, 0.03%)malloc (4,358,402 samples, 0.03%)__memmove_avx_unaligned_erms (4,332,699 samples, 0.03%)core::str::<impl str>::trim_start_matches (4,292,970 samples, 0.03%)__rustc::__rdl_alloc (4,367,721 samples, 0.03%)__memmove_avx_unaligned_erms (4,332,098 samples, 0.03%)__rustc::__rust_dealloc (4,333,584 samples, 0.03%)cfree@GLIBC_2.2.5 (4,352,504 samples, 0.03%)__rustc::__rust_alloc (4,322,546 samples, 0.03%)cfree@GLIBC_2.2.5 (4,250,354 samples, 0.03%)fasteval::parser::Parser::read_callable (12,893,738 samples, 0.08%)malloc (4,320,838 samples, 0.03%)core::num::dec2flt::<impl core::str::traits::FromStr for f64>::from_str (12,976,603 samples, 0.08%)core::num::dec2flt::parse::parse_number (4,316,527 samples, 0.03%)__rustc::__rdl_alloc (4,251,415 samples, 0.03%)fasteval::parser::Parser::read_value (12,992,945 samples, 0.08%)core::num::dec2flt::<impl core::str::traits::FromStr for f64>::from_str (8,657,336 samples, 0.05%)core::num::dec2flt::parse::parse_number (4,294,745 samples, 0.03%)fasteval::parser::Parser::read_expression (189,109,929 samples, 1.18%)fasteval::parser::Parser::read_value (176,051,693 samples, 1.09%)fasteval::parser::Parser::read_callable (171,684,038 samples, 1.07%)fasteval::parser::Parser::read_expression (154,297,966 samples, 0.96%)fasteval::parser::Parser::read_value (132,703,897 samples, 0.82%)fasteval::parser::Parser::read_expression (111,187,796 samples, 0.69%)fasteval::parser::Parser::read_value (111,187,796 samples, 0.69%)fasteval::parser::Parser::read_expression (68,991,858 samples, 0.43%)fasteval::parser::Parser::read_value (56,203,624 samples, 0.35%)fasteval::parser::Parser::read_expression (43,219,025 samples, 0.27%)malloc (13,013,072 samples, 0.08%)malloc_consolidate (33,456,146 samples, 0.21%)<bliplib::compiler::Expression as core::str::traits::FromStr>::from_str (723,753,499 samples, 4.50%)<blip..malloc (134,823,384 samples, 0.84%)_int_malloc (57,659,269 samples, 0.36%)unlink_chunk.isra.0 (4,320,680 samples, 0.03%)__memmove_avx_unaligned_erms (5,715,253,719 samples, 35.52%)__memmove_avx_unaligned_erms<bliplib::compiler::Context as core::clone::Clone>::clone (7,087,972,338 samples, 44.05%)<bliplib::compiler::Context as core::clone::Clone>::clonemalloc (99,056,098 samples, 0.62%)_int_malloc (86,142,836 samples, 0.54%)malloc_consolidate (60,236,190 samples, 0.37%)unlink_chunk.isra.0 (8,589,713 samples, 0.05%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (8,716,116 samples, 0.05%)<alloc::collections::btree::map::BTreeMap<alloc::string::String,f64> as fasteval::evalns::EvalNamespace>::lookup (8,716,116 samples, 0.05%)__memcmp_avx2_movbe (4,354,724 samples, 0.03%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (29,982,235 samples, 0.19%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (21,703,962 samples, 0.13%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (17,398,591 samples, 0.11%)__log2_fma (4,362,227 samples, 0.03%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (47,262,684 samples, 0.29%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (42,894,848 samples, 0.27%)<fasteval::compiler::Instruction as fasteval::evaler::Evaler>::eval (38,589,814 samples, 0.24%)pow@@GLIBC_2.29 (4,306,220 samples, 0.03%)__ieee754_pow_fma (4,306,220 samples, 0.03%)__rustc::__rdl_dealloc (4,248,504 samples, 0.03%)cfree@GLIBC_2.2.5 (38,681,720 samples, 0.24%)_int_free_chunk (34,354,579 samples, 0.21%)_int_free_maybe_consolidate.part.0 (30,015,456 samples, 0.19%)malloc_consolidate (30,015,456 samples, 0.19%)core::ptr::drop_in_place<(char,bliplib::compiler::Expression)> (8,635,873 samples, 0.05%)cfree@GLIBC_2.2.5 (8,635,873 samples, 0.05%)_int_free_chunk (8,635,873 samples, 0.05%)<alloc::vec::Vec<T,A> as core::ops::drop::Drop>::drop (4,291,654 samples, 0.03%)cfree@GLIBC_2.2.5 (4,291,654 samples, 0.03%)_int_free_chunk (4,291,654 samples, 0.03%)core::ptr::drop_in_place<fasteval::slab::CompileSlab> (19,781,201 samples, 0.12%)cfree@GLIBC_2.2.5 (15,489,547 samples, 0.10%)_int_free_chunk (15,489,547 samples, 0.10%)_int_free_maybe_consolidate.part.0 (2,417,481 samples, 0.02%)malloc_consolidate (2,417,481 samples, 0.02%)<alloc::vec::Vec<T,A> as core::ops::drop::Drop>::drop (18,638,842 samples, 0.12%)cfree@GLIBC_2.2.5 (4,360,470 samples, 0.03%)_int_free_chunk (4,360,470 samples, 0.03%)cfree@GLIBC_2.2.5 (76,110,078 samples, 0.47%)_int_free_chunk (50,165,094 samples, 0.31%)core::ptr::drop_in_place<fasteval::parser::Expression> (12,930,980 samples, 0.08%)cfree@GLIBC_2.2.5 (8,628,094 samples, 0.05%)bliplib::compiler::Compiler::compile_all (15,318,767,403 samples, 95.21%)bliplib::compiler::Compiler::compile_allbliplib::compiler::Compiler::step (15,318,767,403 samples, 95.21%)bliplib::compiler::Compiler::step<alloc::boxed::Box<dyn bliplib::compiler::Token> as bliplib::compiler::Token>::apply (15,318,767,403 samples, 95.21%)<alloc::boxed::Box<dyn bliplib::compiler::Token> as bliplib::compiler::Token>::apply<bliplib::compiler::Note as bliplib::compiler::Token>::apply (15,318,767,403 samples, 95.21%)<bliplib::compiler::Note as bliplib::compiler::Token>::applybliplib::compiler::Context::render (15,318,767,403 samples, 95.21%)bliplib::compiler::Context::renderbliplib::compiler::Context::tick (7,443,980,163 samples, 46.27%)bliplib::compiler::Context::tickcore::ptr::drop_in_place<bliplib::compiler::Context> (191,883,506 samples, 1.19%)core::ptr::drop_in_place<fasteval::slab::Slab> (120,536,208 samples, 0.75%)core::ptr::drop_in_place<fasteval::parser::Value> (8,546,755 samples, 0.05%)[libasound.so.2.0.0] (1,770,421 samples, 0.01%)[libasound.so.2.0.0] (1,770,421 samples, 0.01%)snd_dlopen (1,770,421 samples, 0.01%)dlopen@GLIBC_2.2.5 (1,770,421 samples, 0.01%)_dlerror_run (1,770,421 samples, 0.01%)_dl_catch_error (1,770,421 samples, 0.01%)_dl_catch_exception (1,770,421 samples, 0.01%)dlopen_doit (1,770,421 samples, 0.01%)_dl_open (1,770,421 samples, 0.01%)_dl_catch_exception (1,770,421 samples, 0.01%)dl_open_worker (1,770,421 samples, 0.01%)_dl_catch_exception (1,770,421 samples, 0.01%)dl_open_worker_begin (1,770,421 samples, 0.01%)_dl_relocate_object (1,770,421 samples, 0.01%)_dl_relocate_object_no_relro (1,770,421 samples, 0.01%)_dl_lookup_symbol_x (1,770,421 samples, 0.01%)do_lookup_x (1,770,421 samples, 0.01%)[libasound.so.2.0.0] (2,979,612 samples, 0.02%)[libasound.so.2.0.0] (2,979,612 samples, 0.02%)__libc_start_main@@GLIBC_2.34 (15,322,349,655 samples, 95.23%)__libc_start_main@@GLIBC_2.34__libc_start_call_main (15,322,349,655 samples, 95.23%)__libc_start_call_mainmain (15,322,349,655 samples, 95.23%)mainstd::rt::lang_start_internal (15,322,349,655 samples, 95.23%)std::rt::lang_start_internalstd::rt::lang_start::{{closure}} (15,322,349,655 samples, 95.23%)std::rt::lang_start::{{closure}}std::sys::backtrace::__rust_begin_short_backtrace (15,322,349,655 samples, 95.23%)std::sys::backtrace::__rust_begin_short_backtraceblip::main (15,322,349,655 samples, 95.23%)blip::mainrodio::stream::OutputStream::try_default (3,582,252 samples, 0.02%)<cpal::platform::platform_impl::Device as cpal::traits::DeviceTrait>::default_output_config (3,582,252 samples, 0.02%)cpal::host::alsa::Device::default_config (3,582,252 samples, 0.02%)cpal::host::alsa::Device::supported_configs (3,582,252 samples, 0.02%)alsa::pcm::PCM::new (3,582,252 samples, 0.02%)snd_pcm_open (3,582,252 samples, 0.02%)_start (15,322,851,159 samples, 95.24%)_startalloc::collections::btree::map::BTreeMap<K,V,A>::insert (12,947,287 samples, 0.08%)alloc::raw_vec::finish_grow (4,296,929 samples, 0.03%)cfree@GLIBC_2.2.5 (43,157,640 samples, 0.27%)core::ptr::drop_in_place<bliplib::compiler::Context> (4,288,360 samples, 0.03%)core::ptr::drop_in_place<fasteval::compiler::Instruction> (17,281,297 samples, 0.11%)core::ptr::drop_in_place<fasteval::parser::Value> (4,361,952 samples, 0.03%)core::ptr::drop_in_place<fasteval::slab::Slab> (4,328,892 samples, 0.03%)core::slice::sort::shared::smallsort::insertion_sort_shift_left (8,503,116 samples, 0.05%)fasteval::compiler::ExprSlice::split (4,166,924 samples, 0.03%)fasteval::compiler::push_mul_leaves (4,358,465 samples, 0.03%)fasteval::parser::Parser::read_callable (12,954,486 samples, 0.08%)fasteval::parser::Parser::read_expression (17,178,760 samples, 0.11%)fasteval::parser::Parser::read_value (17,283,844 samples, 0.11%)fasteval::slab::CompileSlab::push_instr (8,631,881 samples, 0.05%)malloc (51,720,712 samples, 0.32%)malloc_consolidate (4,303,014 samples, 0.03%)blip (16,080,871,551 samples, 99.95%)blippa_context_ref (4,283,271 samples, 0.03%)cpal_alsa_out (5,640,048 samples, 0.04%)__GI___clone3 (5,078,907 samples, 0.03%)start_thread (5,078,907 samples, 0.03%)std::sys::pal::unix::thread::Thread::new::thread_start (5,078,907 samples, 0.03%)core::ops::function::FnOnce::call_once{{vtable.shim}} (5,078,907 samples, 0.03%)std::sys::backtrace::__rust_begin_short_backtrace (5,078,907 samples, 0.03%)cpal::host::alsa::output_stream_worker (5,078,907 samples, 0.03%)cpal::traits::DeviceTrait::build_output_stream::_{{closure}} (3,549,546 samples, 0.02%)rodio::dynamic_mixer::DynamicMixer<S>::sum_current_sources (3,549,546 samples, 0.02%)<rodio::source::uniform::UniformSourceIterator<I,D> as core::iter::traits::iterator::Iterator>::next (3,549,546 samples, 0.02%)<rodio::conversions::sample_rate::SampleRateConverter<I> as core::iter::traits::iterator::Iterator>::next (3,549,546 samples, 0.02%)rodio::conversions::sample_rate::SampleRateConverter<I>::next_input_frame (3,048,042 samples, 0.02%)<rodio::source::done::Done<I> as core::iter::traits::iterator::Iterator>::next (1,676,541 samples, 0.01%)all (16,089,316,105 samples, 100%)threaded-ml (2,804,506 samples, 0.02%)__GI___clone3 (2,804,506 samples, 0.02%)start_thread (2,804,506 samples, 0.02%)[libpulsecommon-17.0.so] (2,804,506 samples, 0.02%)[libpulse.so.0.24.3] (2,804,506 samples, 0.02%)pa_mainloop_run (2,804,506 samples, 0.02%)pa_mainloop_iterate (2,804,506 samples, 0.02%) \ No newline at end of file diff --git a/poc/poc.yml b/poc/poc.yml deleted file mode 100644 index 4e0f25d..0000000 --- a/poc/poc.yml +++ /dev/null @@ -1,51 +0,0 @@ -# fixed -instruments: - # instrument name - sine: - # fixed - expr: sin(2*PI*f*t) # instrument formula (f is the frequency in Hertz, t is the time in seconds) - square: - expr: v*abs(sin(2*PI*f*t)) - # fixed - vars: - # name of the variable - v: 1 # initial value of the variable -channels: - melody: - instr: sine - score: - notes: cCdDefFgGaAb - sheet: - aabc. - 'ab°A - +d+d+d--- - /ff/f\\ - ab>c< - # comment?: ;, - # start here: ':' - # slope: {MODIFIER EXPR, score} - # note modifier prefix: n - # volume modifier prefix: v - # octave modifier prefix: o - # length modifier prefix: l - # tempo modifier prefix: t - # loop: () - # loop with count: (COUNT, score) - # tuple: [] - # modifier: ! - # volume modifier prefix: v - # octave modifier prefix: o - # length modifier prefix: l - # tempo modifier prefix: t diff --git a/src/DOCS.md b/src/DOCS.md new file mode 100644 index 0000000..b812ff9 --- /dev/null +++ b/src/DOCS.md @@ -0,0 +1,108 @@ +# bliplib + +This is the core library for the Bizarre Language for Intermodulation Programming (BLIP). +This crate also contains the binary `blip`. + +BLIP is a music scripting language made by nerds for nerds. +It uses math and physics in a modular grammar to generate sound your way. + +- [Design](#design) + - [History](#history) + - [Features](#features) + - [Your notes](#your-notes) + - [Your variables](#your-variables) + - [Your own instrument](#your-own-instrument) + - [Time is yours](#time-is-yours) + - [Defaults](#defaults) + - [Expressions](#expressions) + - [Grammar](#grammar) +- [Two wolves](#two-wolves) + - [Binary](#binary) + - [Lib](#lib) +- [Thanks](#thanks) + +## Design + +### History + +Computer assisted music production started from the 50's to the 70's and got popular when computers became cheaper and more powerful. A lot of new tools gave us new ways of making music. As any tool, they also shaped the art we made with it in many ways. For example, most Digital Audio Workstations (DAWs) use "patterns" and allow to easily compose a song with drag-and-drop loops. This made our music easily more repetitive and contributed to completely new genres. A lot of electronic dance genres would probably not exist without this technique which was inspired by common artistic repetition techniques used across all modern history. + +Today, the diversity and availability of these tools seems more than ever linked to the diversity of our art styles because we can achieve so much more with a computer that we rely a lot on these tools. So by contributing to the world of computer assisted music production tools, we directly contribute to our artistic diversity. + +My first steps into this rabbit hole started when I found [BeepComp](http://hiromorozumi.com/beepcomp/), an editor for a custom [MML](https://en.wikipedia.org/wiki/Music_Macro_Language) variant. I became fascinated by the software but even more by this new way of thinking about music. Languages all have their own logic and constraints. [MIDI](https://en.wikipedia.org/wiki/MIDI) for example comes with a language that is very precise but almost impossible to write by hand (it's made of bytes) and the standard implies a specific bank of pre-generated sounds and natively only works with a standard set of 12 notes. + + +### Features + +The main idea behind this language is to free constraints imposed by other similar languages, starting with the note set. + +#### Your notes + +As such, you specify how many notes fit in your set and what their names are. +Now, if you're familiar with music, you may have noticed I say "set" and not "octave". This is also intentional : you decide if you want octaves or not, and how they act on the frequencies of your notes or on the generated sound globally. + +#### Your variables + +If you want octaves, you can make a variable store the value of the current octave. You can define any variable and use them to change any parametter of your current song. + +#### Your own instrument + +There is no imposed sound bank for you to choose from. You make your own. On computers, audio is just a bunch of points with amplitudes, so you give the program a mathematical expression. Everytime you want sound to be generated, your expression will be evaluated with the current variables. + +> Don't worry, the binary includes default expressions for everything. You can override them whenever you want to. + +#### Time is yours + +Standard [MML](https://en.wikipedia.org/wiki/Music_Macro_Language) variations impose a value for a length but they don't all agree (4 means a quarter note? or is it 4 seconds? what about the tempo?). BLIP doesn't know how much time any of your notes last. You give another mathematical expression that outputs a number of seconds for a note to last using the current values of your variables. + +#### Defaults + +The BLIP library needs one default variable for the elapsed time in seconds, another one for the note index of the current note played in your set of notes and a last one for the length of notes in seconds, filled by the length expression. The binary program uses `'t'` for time, `'n'` for the current note index and `'L'` for note lengths in seconds. + +It also includes : +- `sin(2*pi()*(442*2^((n+1)/N))*t)` for the default instrument +- `2^(2-log(2, l))*(60/T)` for the default length expression + +As well as some variables : +| var | value | meaning | +| --- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| l | 4 | length of the notes ([BeepComp style](http://www.hiromorozumi.com/beepcomp/documentation/beepcomp_users_guide.html#music_section_note_length)) | +| L | 0 | length of the notes in seconds (filled by the length expression) | +| t | 0 | elapsed time in seconds | +| T | 60 | tempo (beats per minute / BPM) used in the length expression | +| N | number of notes in the set | used to divide the frequencies for all the notes in the octave (instrument expression) | + +#### Expressions + +BLIP uses [`fasteval`](https://crates.io/crates/fasteval). Check out its [syntax](https://docs.rs/fasteval/latest/fasteval/#the-fasteval-expression-mini-language) to know what you can put in your expression. This is also available in the `memo` menu of the program. + +### Grammar + +For the language grammar, see [doc/cli-design.txt](../doc/language-design.txt) or the the `memo` menu of the program. + +## Two wolves + +([meme](https://i.kym-cdn.com/entries/icons/original/000/029/963/inside_you_there_are_two_wolves.png)) + +### Binary + +You can use `cargo install` to install the binary. You will need the `bin` feature. +```sh +cargo install bliplib --features bin +``` + +Then, follow `blip --help` for instructions on how to use the program. + +### Lib + +The library is designed to ease the integration of this language into various tools like GUIs and editor extensions. + +You can (hopefully) add tokens to the language given you specify how to parse and compile it by either creating wrappers for [`parser::Parser`] and [`compiler::Context`] in your own crate or edit this crate by cloning [the GitDab repo](https://gitdab.com/breval/blip) (or forking if you can). + +Everything here is subject to an MIT license which allows you to do almost everything you want with this code as long as you include my [notice](../LICENSE). + +## Thanks + +This project was made for a Bachelor's Degree in Computer Science at the UQAC (Québec University at Chicoutimi / Université du Québec à Chicoutimi) and many thanks goes to Abdenour Bouzouane for his advice and for allowing me to reuse my ideas for my degree. + +This project takes its roots into the first implementations of the [Music Macro Language](https://en.wikipedia.org/wiki/Music_Macro_Language) and would not exist without this great idea. Thank you to all contributors of this niche world of composition tools, especially contributors of [Pure Data](https://github.com/pure-data/pure-data), [SuperCollider](https://supercollider.github.io/) and [Sonic Pi](https://sonic-pi.net/). Special thanks goes to the very kind [Hiro Morozumi](http://hiromorozumi.com/) for the love he put in [BeepComp](http://hiromorozumi.com/beepcomp/). I wish you all the best. diff --git a/src/bng.rs b/src/bng.rs deleted file mode 100644 index d3a2a8d..0000000 --- a/src/bng.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::{collections::HashMap, str::FromStr}; - -use amplify::{From, Wrapper}; -use derive_new::new; -use derived_deref::Deref; -use fasteval::{Compiler, Instruction}; -pub(super) use instrument::Instrument; -pub(super) use score::Atom; -use score::Atoms; -#[cfg(debug_assertions)] -use serde::Serialize; -use serde::{de::Visitor, Deserialize}; - -mod instrument; -mod score; - -#[derive(Debug, PartialEq, Wrapper, From)] -pub struct Expression(Instruction); - -#[cfg(debug_assertions)] -impl Serialize for Expression { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let expr_str = String::new(); - serializer.serialize_str(&format!("{:#?}", self.0)) - } -} - -impl<'de> Deserialize<'de> for Expression { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_str(ExpressionVisitor) - } -} - -pub struct ExpressionVisitor; - -impl<'de> Visitor<'de> for ExpressionVisitor { - type Value = Expression; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter - .write_str("a math expression following fasteval syntax (https://docs.rs/fasteval)") - } - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - v.parse().map_err(serde::de::Error::custom) - } -} - -impl FromStr for Expression { - type Err = fasteval::Error; - fn from_str(s: &str) -> Result { - let parser = fasteval::Parser::new(); - let mut slab = fasteval::Slab::new(); - Ok(parser - .parse(s, &mut slab.ps)? - .from(&slab.ps) - .compile(&slab.ps, &mut slab.cs) - .into()) - } -} - -#[derive(new, Deserialize)] -#[cfg_attr(debug_assertions, derive(Serialize, Debug))] -pub(super) struct Channel { - instr: String, - score: Atoms, -} - -#[derive(Deserialize)] -#[cfg_attr(debug_assertions, derive(new, Debug, Serialize))] -pub(super) struct BngFile { - instruments: HashMap, - channels: HashMap, -} diff --git a/src/bng/instrument.rs b/src/bng/instrument.rs deleted file mode 100644 index af5164f..0000000 --- a/src/bng/instrument.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::collections::HashMap; - -use derive_new::new; -use derived_deref::Deref; -use serde::Deserialize; -#[cfg(debug_assertions)] -use serde::Serialize; - -use super::Expression as Instruction; - -#[derive(Deref, new, Deserialize)] -#[cfg_attr(debug_assertions, derive(Serialize, Debug))] -pub struct Instrument { - #[target] - expr: Instruction, - vars: Option>, -} diff --git a/src/bng/score.rs b/src/bng/score.rs deleted file mode 100644 index 84ba46c..0000000 --- a/src/bng/score.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::{ - error::Error, - num::{NonZeroU16, NonZeroU8}, -}; - -use amplify::From; -use anyhow::Context; -use bng_macros::{QuickModifierParser, SlopeModifierParser}; -use derive_new::new; -use derived_deref::Deref; -use lex::lexer::flat_atom_parser; -use nom::{ - branch::alt, - character::{complete::char, streaming::one_of}, - combinator::{all_consuming, eof}, - multi::many0, - sequence::{preceded, terminated}, -}; -#[cfg(debug_assertions)] -use serde::Serialize; -use serde::{ - de::{self as serde_de, Visitor}, - Deserialize, -}; -use strum::EnumDiscriminants; -use thiserror::Error; -use utils::{inflate, InflateError}; - -mod de; -mod lex; -mod utils; -pub use de::*; - -use super::Expression as Instruction; - -#[derive(Deref, From, Default)] -#[cfg_attr(debug_assertions, derive(Serialize, Debug))] -pub struct Atoms(Vec); - -#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] -pub enum Atom { - Note(u8), - Rest, - StartHere, - Modifier(Modifier), - QuickModifier(QuickModifier), - Loop(NonZeroU8, Vec), - Tuple(Vec), - Slope(SlopeModifier, Instruction, Vec), - Comment, -} - -#[cfg(debug_assertions)] -impl Serialize for Atom { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str("atom") - } -} - -#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] -pub(super) enum FlatAtom { - Note(u8), - Rest, - StartHere, - Modifier(Modifier), - QuickModifier(QuickModifier), - LoopStarts(NonZeroU8), - LoopEnds, - TupleStarts, - TupleEnds, - SlopeStarts(SlopeModifier, Instruction), - SlopeEnds, - Comment, -} - -impl Clone for FlatAtom { - fn clone(&self) -> Self { - match self { - Self::Rest => Self::Rest, - Self::Comment => Self::Comment, - Self::LoopEnds => Self::LoopEnds, - Self::SlopeEnds => Self::SlopeEnds, - Self::StartHere => Self::StartHere, - Self::TupleEnds => Self::TupleEnds, - Self::TupleStarts => Self::TupleStarts, - _ => unimplemented!("variant can't be cloned"), - } - } -} - -#[derive(Clone)] -#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] -pub enum Modifier { - Volume(u8), - Octave(u8), - Length(NonZeroU8), - Tempo(NonZeroU16), -} - -impl Default for Modifier { - fn default() -> Self { - Modifier::Volume(Default::default()) - } -} - -#[derive(QuickModifierParser, Clone)] -#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] -pub enum QuickModifier { - Volume(bool), - Octave(bool), - Length(bool), - Pizz(bool), -} - -#[derive(Clone, Copy, SlopeModifierParser, Debug)] -#[cfg_attr(debug_assertions, derive(PartialEq))] -pub enum SlopeModifier { - Note, - Volume, - Octave, - Length, - Tempo, -} diff --git a/src/bng/score/de.rs b/src/bng/score/de.rs deleted file mode 100644 index aa97ecd..0000000 --- a/src/bng/score/de.rs +++ /dev/null @@ -1,100 +0,0 @@ -use derive_new::new; -use nom::{ - character::complete::one_of, - combinator::all_consuming, - multi::{many0, many1}, - sequence::{preceded, terminated}, - Parser, -}; -use serde::{ - de::{self, Deserializer, Visitor}, - Deserialize, -}; -use thiserror::Error; - -use crate::bng::score::lex::lexer::flat_atom_parser; - -use super::{ - utils::{inflate, InflateError}, - Atoms, FlatAtom, -}; - -#[derive(Debug, Error)] -enum AtomsSerializeError { - #[error("sheet parsing error: {0}")] - Parsing(String), - #[error("sheet semantics: {0}")] - Inflation(#[from] InflateError), -} - -fn nom_err_message(e: nom::Err>) -> String { - match e { - nom::Err::Incomplete(needed) => format!( - "input is incomplete, needed {} byte(s) more", - match needed { - nom::Needed::Unknown => "?".to_string(), - nom::Needed::Size(s) => s.to_string(), - } - ), - nom::Err::Error(e) | nom::Err::Failure(e) => format!( - "got error code {code:#?} at \"{input}\"", - code = e.code, - input = e.input - ), - } -} - -impl<'de> Deserialize<'de> for Atoms { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] - enum Field { - Notes, - Sheet, - } - #[derive(Deserialize, new)] - struct NotesSheet { - notes: String, - sheet: String, - } - let NotesSheet { notes, sheet } = NotesSheet::deserialize(deserializer)?; - if sheet.is_empty() { - Ok(Default::default()) - } else { - flat_atom_parser_mapper::(&sheet, flat_atom_parser(¬es)) - } - } -} - -fn maybe_yml_str_space<'a, E>() -> impl Parser<&'a str, Vec, E> -where - E: nom::error::ParseError<&'a str>, -{ - many0(one_of(" \t\r")) -} - -fn flat_atom_parser_mapper<'a, 'de, D, P>( - input: &'a str, - parser: P, -) -> Result>::Error> -where - D: serde::Deserializer<'de>, - P: Parser<&'a str, FlatAtom, nom::error::Error<&'a str>>, -{ - all_consuming(terminated( - many1(preceded(maybe_yml_str_space(), parser)), - maybe_yml_str_space(), - ))(input) - .map_err(nom_err_message) - .map_err(AtomsSerializeError::Parsing) - .map_err(de::Error::custom) - .and_then(|(_, v)| { - inflate(v) - .map_err(AtomsSerializeError::from) - .map_err(de::Error::custom) - }) - .map(Atoms) -} diff --git a/src/bng/score/lex.rs b/src/bng/score/lex.rs deleted file mode 100644 index bb580a8..0000000 --- a/src/bng/score/lex.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier}; - -pub(super) mod lexer; - -pub(super) const ON: bool = true; -pub(super) const OFF: bool = false; -pub(super) const UP: bool = true; -pub(super) const DOWN: bool = false; - -struct WrappingTokens; -impl WrappingTokens { - const PARENTHESES: (char, char) = ('(', ')'); - const SQUARE_BRACKETS: (char, char) = ('[', ']'); - const BRACKETS: (char, char) = ('{', '}'); - const SEMICOLON_COMMA: (char, char) = (';', ','); - const PLUS_MINUS: (char, char) = ('+', '-'); - const RIGHT_LEFT: (char, char) = ('>', '<'); - const SLASH_BACKSLASH: (char, char) = ('/', '\\'); - const QUOTE_DEG: (char, char) = ('\'', '°'); -} - -impl QuickModifier { - pub(super) const VOLUME: (char, char) = WrappingTokens::PLUS_MINUS; - pub(super) const OCTAVE: (char, char) = WrappingTokens::RIGHT_LEFT; - pub(super) const LENGTH: (char, char) = WrappingTokens::SLASH_BACKSLASH; - pub(super) const PIZZ: (char, char) = WrappingTokens::QUOTE_DEG; -} - -impl Modifier { - pub(super) const VOLUME: char = 'v'; - pub(super) const OCTAVE: char = 'o'; - pub(super) const LENGTH: char = 'l'; - pub(super) const TEMPO: char = 't'; -} - -impl SlopeModifier { - pub(super) const NOTE: char = 'n'; - pub(super) const VOLUME: char = 'v'; - pub(super) const OCTAVE: char = 'o'; - pub(super) const LENGTH: char = 'l'; - pub(super) const TEMPO: char = 't'; -} - -impl Atom { - pub(super) const REST: char = '.'; - pub(super) const START_HERE: char = ':'; - pub(super) const MODIFIER: char = '!'; - pub(super) const LOOP: (char, char) = WrappingTokens::PARENTHESES; - pub(super) const TUPLE: (char, char) = WrappingTokens::SQUARE_BRACKETS; - pub(super) const SLOPE: (char, char) = WrappingTokens::BRACKETS; - pub(super) const COMMENT: (char, char) = WrappingTokens::SEMICOLON_COMMA; -} diff --git a/src/bng/score/lex/lexer.rs b/src/bng/score/lex/lexer.rs deleted file mode 100644 index 762100e..0000000 --- a/src/bng/score/lex/lexer.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::{ - collections::BTreeMap, - num::{NonZeroU16, NonZeroU8}, -}; - -use clap::builder::TypedValueParser; -use fasteval::Compiler; -use nom::{ - branch::alt, - bytes::complete::{take_till, take_till1}, - character::complete::{anychar, char, one_of, space1, u16, u8}, - combinator::{map_opt, map_res, opt, value, verify}, - multi::many0, - sequence::{delimited, pair, preceded, separated_pair, terminated}, - Err, IResult, Parser, -}; - -use super::{ - super::super::Expression as Instruction, - {Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier}, -}; - -#[cfg(test)] -mod tests; - -pub(crate) trait Parse: Sized { - fn parse(input: &str) -> IResult<&str, Self>; -} - -impl Parse for Modifier { - fn parse(input: &str) -> IResult<&str, Self> { - alt(( - preceded(char(Modifier::VOLUME), u8).map(Modifier::Volume), - preceded(char(Modifier::OCTAVE), u8).map(Modifier::Octave), - preceded(char(Modifier::LENGTH), map_opt(u8, NonZeroU8::new)).map(Modifier::Length), - preceded(char(Modifier::TEMPO), map_opt(u16, NonZeroU16::new)).map(Modifier::Tempo), - ))(input) - } -} - -pub fn flat_atom_parser(notes: &str) -> impl Parser<&str, FlatAtom, nom::error::Error<&str>> { - alt(( - map_res(map_opt(one_of(notes), |c| notes.find(c)), u8::try_from).map(FlatAtom::Note), - value(FlatAtom::Rest, char(Atom::REST)), - value(FlatAtom::StartHere, char(Atom::START_HERE)), - preceded(char(Atom::MODIFIER), Modifier::parse).map(FlatAtom::Modifier), - QuickModifier::parse.map(FlatAtom::QuickModifier), - preceded( - char(Atom::LOOP.0), - map_opt(opt(u8), |n| { - if let Some(n) = n { - NonZeroU8::new(n) - } else { - unsafe { Some(NonZeroU8::new_unchecked(2)) } - } - }), - ) - .map(FlatAtom::LoopStarts), - value(FlatAtom::LoopEnds, char(Atom::LOOP.1)), - value(FlatAtom::TupleStarts, char(Atom::TUPLE.0)), - value(FlatAtom::TupleEnds, char(Atom::TUPLE.1)), - terminated( - preceded( - char(Atom::SLOPE.0), - separated_pair( - SlopeModifier::parse, - char(' '), - map_res(take_till1(|c| c == ','), |s: &str| { - s.parse() - .map_err(|_| nom::error::Error::new(s, nom::error::ErrorKind::Verify)) - }), - ), - ), - char(','), - ) - .map(|(sm, i)| FlatAtom::SlopeStarts(sm, i)), - value(FlatAtom::SlopeEnds, char(Atom::SLOPE.1)), - value( - FlatAtom::Comment, - delimited( - char(Atom::COMMENT.0), - take_till(|c| c == Atom::COMMENT.1), - char(Atom::COMMENT.1), - ), - ), - )) -} diff --git a/src/bng/score/lex/lexer/tests.rs b/src/bng/score/lex/lexer/tests.rs deleted file mode 100644 index 96a4af7..0000000 --- a/src/bng/score/lex/lexer/tests.rs +++ /dev/null @@ -1,360 +0,0 @@ -use const_format::concatcp; -use nom::{ - error::{Error, ErrorKind}, - Err, -}; - -use flat_atom::{ - FASTEVAL_INSTRUCTION as FLATATOM_FASTEVAL_INSTRUCTION, SAMPLE_STR as FLATATOM_SAMPLE_STRING, -}; - -mod flat_atom { - use std::num::NonZeroU8; - - use fasteval::Compiler; - use nom::Parser; - - use super::super::{ - super::super::super::Expression as Instruction, super::UP, flat_atom_parser, Atom, - FlatAtom, Modifier, QuickModifier, SlopeModifier, - }; - use super::*; - - pub(super) const SAMPLE_STR: &str = concatcp!( - Atom::TUPLE.0, - "acc", - Atom::TUPLE.1, - "ed", - Atom::COMMENT.0, - "hello" - ); - - pub(super) const FASTEVAL_INSTRUCTION: &str = "1-cos((PI*x)/2)"; - - #[test] - fn note() { - assert_eq!( - Ok((SAMPLE_STR, FlatAtom::Note(2))), - flat_atom_parser("abcdefg").parse(concatcp!('c', SAMPLE_STR)) - ) - } - - #[test] - fn rest() { - assert_eq!( - Ok((SAMPLE_STR, FlatAtom::Rest)), - flat_atom_parser("abcdefg").parse(concatcp!(Atom::REST, SAMPLE_STR)) - ) - } - - #[test] - fn start_here() { - assert_eq!( - Ok((SAMPLE_STR, FlatAtom::StartHere)), - flat_atom_parser("abcdefg").parse(concatcp!(Atom::START_HERE, SAMPLE_STR)) - ) - } - - #[test] - fn modifier() { - assert_eq!( - Ok(( - SAMPLE_STR, - FlatAtom::Modifier(Modifier::Length(unsafe { NonZeroU8::new_unchecked(2) })) - )), - flat_atom_parser("abcdefg").parse(concatcp!( - Atom::MODIFIER, - Modifier::LENGTH, - 2u8, - SAMPLE_STR - )) - ) - } - - #[test] - fn quick_modifier() { - assert_eq!( - Ok(( - SAMPLE_STR, - FlatAtom::QuickModifier(QuickModifier::Length(UP)) - )), - flat_atom_parser("abcdefg").parse(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR)) - ) - } - - #[test] - fn loop_starts() { - assert_eq!( - Ok(( - SAMPLE_STR, - FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(3) }) - )), - flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.0, 3u8, SAMPLE_STR)) - ); - assert_eq!( - Ok(( - SAMPLE_STR, - FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(2) }) - )), - flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.0, SAMPLE_STR)) - ); - assert_eq!( - Err(nom::Err::Error(Error::new( - concatcp!(Atom::LOOP.0, 0u8, SAMPLE_STR), - ErrorKind::Char - ))), - flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.0, 0u8, SAMPLE_STR)) - ) - } - - #[test] - fn loop_ends() { - assert_eq!( - Ok((SAMPLE_STR, FlatAtom::LoopEnds)), - flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.1, SAMPLE_STR)) - ) - } - - #[test] - fn tuple_starts() { - assert_eq!( - Ok((SAMPLE_STR, FlatAtom::TupleStarts)), - flat_atom_parser("abcdefg").parse(concatcp!(Atom::TUPLE.0, SAMPLE_STR)) - ) - } - - #[test] - fn tuple_ends() { - assert_eq!( - Ok((SAMPLE_STR, FlatAtom::TupleEnds)), - flat_atom_parser("abcdefg").parse(concatcp!(Atom::TUPLE.1, SAMPLE_STR)) - ) - } - - #[test] - fn slope_starts() { - assert_eq!( - Ok(( - SAMPLE_STR, - FlatAtom::SlopeStarts(SlopeModifier::Note, FASTEVAL_INSTRUCTION.parse().unwrap()) - )), - flat_atom_parser("abcdefg").parse(concatcp!( - Atom::SLOPE.0, - SlopeModifier::NOTE, - ' ', - FASTEVAL_INSTRUCTION, - ',', - SAMPLE_STR - )) - ) - } - - #[test] - fn slope_ends() { - assert_eq!( - Ok((SAMPLE_STR, FlatAtom::SlopeEnds)), - flat_atom_parser("abcdefg").parse(concatcp!(Atom::SLOPE.1, SAMPLE_STR)) - ) - } - - #[test] - fn comment() { - assert_eq!( - Ok((SAMPLE_STR, FlatAtom::Comment)), - flat_atom_parser("abcdefg").parse(concatcp!( - Atom::COMMENT.0, - "hi I'm a little pony", - SAMPLE_STR, - Atom::COMMENT.1, - SAMPLE_STR - )) - ) - } -} - -mod modifier { - use std::num::{NonZeroU16, NonZeroU8}; - - use super::FLATATOM_SAMPLE_STRING as SAMPLE_STR; - use super::*; - use crate::bng::score::{lex::lexer::Parse, Modifier}; - - #[test] - fn volume() { - assert_eq!( - Ok((SAMPLE_STR, Modifier::Volume(2))), - Modifier::parse(concatcp!(Modifier::VOLUME, 2u8, SAMPLE_STR)) - ); - assert_eq!( - Err(nom::Err::Error(Error::new( - concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR), - ErrorKind::Char - ))), - Modifier::parse(concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR)) - ); - } - - #[test] - fn octave() { - assert_eq!( - Ok((SAMPLE_STR, Modifier::Octave(2))), - Modifier::parse(concatcp!(Modifier::OCTAVE, 2u8, SAMPLE_STR)) - ); - assert_eq!( - Err(nom::Err::Error(Error::new( - concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR), - ErrorKind::Char - ))), - Modifier::parse(concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR)) - ); - } - - #[test] - fn length() { - assert_eq!( - Ok(( - SAMPLE_STR, - Modifier::Length(unsafe { NonZeroU8::new_unchecked(2) }) - )), - Modifier::parse(concatcp!(Modifier::LENGTH, 2u8, SAMPLE_STR)) - ); - assert_eq!( - Err(nom::Err::Error(Error::new( - concatcp!(Modifier::LENGTH, 2556u16, SAMPLE_STR), - ErrorKind::Char - ))), - Modifier::parse(concatcp!(Modifier::LENGTH, 2556u16, SAMPLE_STR)) - ); - assert_eq!( - Err(nom::Err::Error(Error::new( - concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR), - ErrorKind::Char - ))), - Modifier::parse(concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR)) - ); - } - - #[test] - fn tempo() { - assert_eq!( - Ok(( - SAMPLE_STR, - Modifier::Tempo(unsafe { NonZeroU16::new_unchecked(2) }) - )), - Modifier::parse(concatcp!(Modifier::TEMPO, 2u8, SAMPLE_STR)) - ); - assert_eq!( - Err(nom::Err::Error(Error::new( - concatcp!(655353u32, SAMPLE_STR), - ErrorKind::Digit - ))), - Modifier::parse(concatcp!(Modifier::TEMPO, 655353u32, SAMPLE_STR)) - ); - } -} - -mod quick_modifier { - use super::FLATATOM_SAMPLE_STRING as SAMPLE_STR; - use super::*; - use crate::bng::score::{ - lex::{lexer::Parse, DOWN, OFF, ON, UP}, - QuickModifier, - }; - - #[test] - fn volume() { - assert_eq!( - Ok((SAMPLE_STR, QuickModifier::Volume(UP))), - QuickModifier::parse(concatcp!(QuickModifier::VOLUME.0, SAMPLE_STR)) - ); - assert_eq!( - Ok((SAMPLE_STR, QuickModifier::Volume(DOWN))), - QuickModifier::parse(concatcp!(QuickModifier::VOLUME.1, SAMPLE_STR)) - ); - } - - #[test] - fn octave() { - assert_eq!( - Ok((SAMPLE_STR, QuickModifier::Octave(UP))), - QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.0, SAMPLE_STR)) - ); - assert_eq!( - Ok((SAMPLE_STR, QuickModifier::Octave(DOWN))), - QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.1, SAMPLE_STR)) - ); - } - - #[test] - fn length() { - assert_eq!( - Ok((SAMPLE_STR, QuickModifier::Length(UP))), - QuickModifier::parse(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR)) - ); - assert_eq!( - Ok((SAMPLE_STR, QuickModifier::Length(DOWN))), - QuickModifier::parse(concatcp!(QuickModifier::LENGTH.1, SAMPLE_STR)) - ); - } - - #[test] - fn pizz() { - assert_eq!( - Ok((SAMPLE_STR, QuickModifier::Pizz(ON))), - QuickModifier::parse(concatcp!(QuickModifier::PIZZ.0, SAMPLE_STR)) - ); - assert_eq!( - Ok((SAMPLE_STR, QuickModifier::Pizz(OFF))), - QuickModifier::parse(concatcp!(QuickModifier::PIZZ.1, SAMPLE_STR)) - ); - } -} - -#[cfg(test)] -mod slope_modifier { - use super::FLATATOM_FASTEVAL_INSTRUCTION as INSTRUCTION; - use super::*; - use crate::bng::score::{lex::lexer::Parse, Atom, SlopeModifier}; - - const SAMPLE_STR: &str = concatcp!(' ', INSTRUCTION, Atom::SLOPE.1); - - #[test] - fn note() { - assert_eq!( - Ok((SAMPLE_STR, SlopeModifier::Note)), - SlopeModifier::parse(concatcp!(SlopeModifier::NOTE, SAMPLE_STR)) - ) - } - - #[test] - fn volume() { - assert_eq!( - Ok((SAMPLE_STR, SlopeModifier::Volume)), - SlopeModifier::parse(concatcp!(SlopeModifier::VOLUME, SAMPLE_STR)) - ) - } - - #[test] - fn octave() { - assert_eq!( - Ok((SAMPLE_STR, SlopeModifier::Octave)), - SlopeModifier::parse(concatcp!(SlopeModifier::OCTAVE, SAMPLE_STR)) - ) - } - - #[test] - fn length() { - assert_eq!( - Ok((SAMPLE_STR, SlopeModifier::Length)), - SlopeModifier::parse(concatcp!(SlopeModifier::LENGTH, SAMPLE_STR)) - ) - } - - #[test] - fn tempo() { - assert_eq!( - Ok((SAMPLE_STR, SlopeModifier::Tempo)), - SlopeModifier::parse(concatcp!(SlopeModifier::TEMPO, SAMPLE_STR)) - ) - } -} diff --git a/src/bng/score/utils.rs b/src/bng/score/utils.rs deleted file mode 100644 index e7bb56d..0000000 --- a/src/bng/score/utils.rs +++ /dev/null @@ -1,236 +0,0 @@ -use super::*; -use strum::Display; -use thiserror::Error; - -#[cfg(test)] -mod tests; - -#[derive(Debug, EnumDiscriminants)] -#[strum_discriminants(derive(Display))] -pub enum Wrapper { - Loop(NonZeroU8), - Tuple, - Slope(SlopeModifier, Instruction), -} - -#[derive(Debug, Error)] -#[cfg_attr(test, derive(PartialEq))] -pub enum InflateError { - #[error("misplaced {0} end symbol")] - MismatchedEnd(WrapperDiscriminants), -} - -pub fn inflate(mut flat_atoms: Vec) -> Result, InflateError> { - type Error = InflateError; - let mut result = Vec::with_capacity(flat_atoms.len()); - let mut loop_stack: Vec> = Vec::new(); - let mut tuple_stack: Vec> = Vec::new(); - let mut slope_stack: Vec> = Vec::new(); - let mut stack_history: Vec = Vec::new(); - for mut atom in flat_atoms.into_iter() { - #[cfg(test)] - { - dbg!(&atom); - dbg!(&loop_stack); - dbg!(&tuple_stack); - dbg!(&slope_stack); - dbg!(&stack_history); - } - match stack_history.last().map(WrapperDiscriminants::from) { - Some(WrapperDiscriminants::Loop) => match atom { - FlatAtom::Note(n) => { - unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::Note(n)) - } - FlatAtom::Rest => { - unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::Rest) - } - FlatAtom::StartHere => { - unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::StartHere) - } - FlatAtom::Modifier(m) => { - unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::Modifier(m)) - } - FlatAtom::QuickModifier(q) => { - unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::QuickModifier(q)) - } - FlatAtom::LoopStarts(n) => { - loop_stack.push(Vec::new()); - stack_history.push(Wrapper::Loop(n)); - } - FlatAtom::LoopEnds => { - let popped = unsafe { loop_stack.pop().unwrap_unchecked() }; - if stack_history.len() > 1 { - match WrapperDiscriminants::from( - stack_history.get(stack_history.len() - 2).unwrap(), - ) { - WrapperDiscriminants::Loop => &mut loop_stack, - WrapperDiscriminants::Tuple => &mut tuple_stack, - WrapperDiscriminants::Slope => &mut slope_stack, - } - .last_mut() - .unwrap() - .push(Atom::Loop( - match stack_history.pop().unwrap() { - Wrapper::Loop(n) => n, - _ => unreachable!("this one is proven to be a loop"), - }, - popped, - )) - } else { - result.push(Atom::Loop( - match stack_history.pop().unwrap() { - Wrapper::Loop(n) => n, - _ => unreachable!("this one is proven to be a loop"), - }, - popped, - )) - } - } - FlatAtom::TupleStarts => { - tuple_stack.push(Vec::new()); - stack_history.push(Wrapper::Tuple); - } - FlatAtom::TupleEnds => { - return Err(Error::MismatchedEnd(WrapperDiscriminants::Tuple)); - } - FlatAtom::SlopeStarts(s, i) => { - slope_stack.push(Vec::new()); - stack_history.push(Wrapper::Slope(s, i)); - } - FlatAtom::SlopeEnds => { - return Err(Error::MismatchedEnd(WrapperDiscriminants::Slope)); - } - FlatAtom::Comment => loop_stack.last_mut().unwrap().push(Atom::Comment), - }, - Some(WrapperDiscriminants::Tuple) => match atom { - FlatAtom::Note(n) => tuple_stack.last_mut().unwrap().push(Atom::Note(n)), - FlatAtom::Rest => tuple_stack.last_mut().unwrap().push(Atom::Rest), - FlatAtom::StartHere => tuple_stack.last_mut().unwrap().push(Atom::StartHere), - FlatAtom::Modifier(m) => tuple_stack.last_mut().unwrap().push(Atom::Modifier(m)), - FlatAtom::QuickModifier(q) => { - tuple_stack.last_mut().unwrap().push(Atom::QuickModifier(q)) - } - FlatAtom::LoopStarts(n) => { - loop_stack.push(Vec::new()); - stack_history.push(Wrapper::Loop(n)); - } - FlatAtom::LoopEnds => { - return Err(Error::MismatchedEnd(WrapperDiscriminants::Loop)); - } - FlatAtom::TupleStarts => { - tuple_stack.push(Vec::new()); - stack_history.push(Wrapper::Tuple); - } - FlatAtom::TupleEnds => { - let popped = tuple_stack.pop().unwrap(); - if stack_history.len() > 1 { - match WrapperDiscriminants::from( - stack_history.get(stack_history.len() - 2).unwrap(), - ) { - WrapperDiscriminants::Loop => &mut loop_stack, - WrapperDiscriminants::Tuple => &mut tuple_stack, - WrapperDiscriminants::Slope => &mut slope_stack, - } - .last_mut() - .unwrap() - .push({ - stack_history.pop(); - Atom::Tuple(popped) - }) - } else { - result.push(Atom::Tuple(popped)) - } - } - FlatAtom::SlopeStarts(s, i) => { - slope_stack.push(Vec::new()); - stack_history.push(Wrapper::Slope(s, i)); - } - FlatAtom::SlopeEnds => { - return Err(Error::MismatchedEnd(WrapperDiscriminants::Slope)); - } - FlatAtom::Comment => tuple_stack.last_mut().unwrap().push(Atom::Comment), - }, - Some(WrapperDiscriminants::Slope) => match atom { - FlatAtom::Note(n) => slope_stack.last_mut().unwrap().push(Atom::Note(n)), - FlatAtom::Rest => slope_stack.last_mut().unwrap().push(Atom::Rest), - FlatAtom::StartHere => slope_stack.last_mut().unwrap().push(Atom::StartHere), - FlatAtom::Modifier(m) => slope_stack.last_mut().unwrap().push(Atom::Modifier(m)), - FlatAtom::QuickModifier(q) => { - slope_stack.last_mut().unwrap().push(Atom::QuickModifier(q)) - } - FlatAtom::LoopStarts(n) => { - loop_stack.push(Vec::new()); - stack_history.push(Wrapper::Loop(n)); - } - FlatAtom::LoopEnds => { - return Err(Error::MismatchedEnd(WrapperDiscriminants::Loop)); - } - FlatAtom::TupleStarts => { - tuple_stack.push(Vec::new()); - stack_history.push(Wrapper::Tuple); - } - FlatAtom::TupleEnds => { - return Err(Error::MismatchedEnd(WrapperDiscriminants::Tuple)); - } - FlatAtom::SlopeStarts(s, i) => { - slope_stack.push(Vec::new()); - stack_history.push(Wrapper::Slope(s, i)); - } - FlatAtom::SlopeEnds => { - let popped = slope_stack.pop().unwrap(); - if stack_history.len() > 1 { - match WrapperDiscriminants::from( - stack_history.get(stack_history.len() - 2).unwrap(), - ) { - WrapperDiscriminants::Loop => &mut loop_stack, - WrapperDiscriminants::Tuple => &mut tuple_stack, - WrapperDiscriminants::Slope => &mut slope_stack, - } - .last_mut() - .unwrap() - .push(match stack_history.pop().unwrap() { - Wrapper::Slope(m, i) => Atom::Slope(m, i, popped), - _ => unreachable!("this one is proven to be a slope"), - }) - } else { - result.push(match stack_history.pop().unwrap() { - Wrapper::Slope(m, i) => Atom::Slope(m, i, popped), - _ => unreachable!("this one is proven to be a slope"), - }) - } - } - FlatAtom::Comment => slope_stack.last_mut().unwrap().push(Atom::Comment), - }, - None => match atom { - FlatAtom::Note(n) => result.push(Atom::Note(n)), - FlatAtom::Rest => result.push(Atom::Rest), - FlatAtom::StartHere => result.push(Atom::StartHere), - FlatAtom::Modifier(m) => result.push(Atom::Modifier(m)), - FlatAtom::QuickModifier(q) => result.push(Atom::QuickModifier(q)), - FlatAtom::LoopStarts(n) => { - loop_stack.push(Vec::new()); - stack_history.push(Wrapper::Loop(n)); - } - FlatAtom::LoopEnds => { - return Err(Error::MismatchedEnd(WrapperDiscriminants::Loop)); - } - FlatAtom::TupleStarts => { - tuple_stack.push(Vec::new()); - stack_history.push(Wrapper::Tuple); - } - FlatAtom::TupleEnds => { - return Err(Error::MismatchedEnd(WrapperDiscriminants::Tuple)); - } - FlatAtom::SlopeStarts(s, i) => { - slope_stack.push(Vec::new()); - stack_history.push(Wrapper::Slope(s, i)); - } - FlatAtom::SlopeEnds => { - return Err(Error::MismatchedEnd(WrapperDiscriminants::Slope)); - } - FlatAtom::Comment => result.push(Atom::Comment), - }, - } - } - Ok(result) -} diff --git a/src/bng/score/utils/tests.rs b/src/bng/score/utils/tests.rs deleted file mode 100644 index 88d08b9..0000000 --- a/src/bng/score/utils/tests.rs +++ /dev/null @@ -1,206 +0,0 @@ -#[cfg(test)] -mod inflate { - use fasteval::Compiler; - use lex::{ON, UP}; - - use super::{super::*, inflate}; - - const FASTEVAL_INSTRUCTION: &str = "1-cos((PI*x)/2)"; - - fn instruction() -> Instruction { - FASTEVAL_INSTRUCTION.parse().unwrap() - } - - #[test] - fn inflate_flat() { - assert_eq!( - Ok(vec![ - Atom::Note(2), - Atom::Rest, - Atom::StartHere, - Atom::Modifier(Modifier::Volume(2)), - Atom::QuickModifier(QuickModifier::Volume(UP)), - Atom::Comment - ]), - inflate(vec![ - FlatAtom::Note(2), - FlatAtom::Rest, - FlatAtom::StartHere, - FlatAtom::Modifier(Modifier::Volume(2)), - FlatAtom::QuickModifier(QuickModifier::Volume(UP)), - FlatAtom::Comment - ]) - ) - } - - #[test] - fn inflate_loop_l1() { - assert_eq!( - Ok(vec![Atom::Loop( - unsafe { NonZeroU8::new_unchecked(3) }, - vec![Atom::Note(2), Atom::Note(3)] - )]), - inflate(vec![ - FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(3) }), - FlatAtom::Note(2), - FlatAtom::Note(3), - FlatAtom::LoopEnds - ]) - ) - } - - #[test] - fn inflate_tuple_l1() { - assert_eq!( - Ok(vec![Atom::Tuple(vec![Atom::Note(2), Atom::Note(3)])]), - inflate(vec![ - FlatAtom::TupleStarts, - FlatAtom::Note(2), - FlatAtom::Note(3), - FlatAtom::TupleEnds - ]) - ) - } - - #[test] - fn inflate_slope_l1() { - assert_eq!( - Ok(vec![Atom::Slope( - SlopeModifier::Note, - instruction(), - vec![Atom::Note(2), Atom::Note(3)] - )]), - inflate(vec![ - FlatAtom::SlopeStarts(SlopeModifier::Note, instruction()), - FlatAtom::Note(2), - FlatAtom::Note(3), - FlatAtom::SlopeEnds - ]) - ) - } - - #[test] - fn inflate_loop_l2() { - assert_eq!( - Ok(vec![Atom::Loop( - unsafe { NonZeroU8::new_unchecked(2) }, - vec![Atom::Loop( - unsafe { NonZeroU8::new_unchecked(3) }, - vec![Atom::Note(2), Atom::Note(3)] - )] - )]), - inflate(vec![ - FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(2) }), - FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(3) }), - FlatAtom::Note(2), - FlatAtom::Note(3), - FlatAtom::LoopEnds, - FlatAtom::LoopEnds - ]) - ) - } - - #[test] - fn inflate_tuple_l2() { - assert_eq!( - Ok(vec![Atom::Tuple(vec![Atom::Tuple(vec![ - Atom::Note(2), - Atom::Note(3) - ])])]), - inflate(vec![ - FlatAtom::TupleStarts, - FlatAtom::TupleStarts, - FlatAtom::Note(2), - FlatAtom::Note(3), - FlatAtom::TupleEnds, - FlatAtom::TupleEnds - ]) - ) - } - - #[test] - fn inflate_slope_l2() { - assert_eq!( - Ok(vec![Atom::Slope( - SlopeModifier::Note, - instruction(), - vec![Atom::Slope( - SlopeModifier::Length, - instruction(), - vec![Atom::Note(2), Atom::Note(3)] - )] - )]), - inflate(vec![ - FlatAtom::SlopeStarts(SlopeModifier::Note, instruction()), - FlatAtom::SlopeStarts(SlopeModifier::Length, instruction()), - FlatAtom::Note(2), - FlatAtom::Note(3), - FlatAtom::SlopeEnds, - FlatAtom::SlopeEnds - ]) - ) - } - - #[test] - fn mixed() { - assert_eq!( - Ok(vec![Atom::Slope( - SlopeModifier::Note, - instruction(), - vec![Atom::Slope( - SlopeModifier::Length, - instruction(), - vec![ - Atom::Note(2), - Atom::Tuple(vec![Atom::Rest, Atom::Note(6)]), - Atom::Note(3), - Atom::Loop( - unsafe { NonZeroU8::new_unchecked(9) }, - vec![Atom::QuickModifier(QuickModifier::Pizz(ON)), Atom::Note(0)] - ) - ] - )] - )]), - inflate(vec![ - FlatAtom::SlopeStarts(SlopeModifier::Note, instruction()), - FlatAtom::SlopeStarts(SlopeModifier::Length, instruction()), - FlatAtom::Note(2), - FlatAtom::TupleStarts, - FlatAtom::Rest, - FlatAtom::Note(6), - FlatAtom::TupleEnds, - FlatAtom::Note(3), - FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(9) }), - FlatAtom::QuickModifier(QuickModifier::Pizz(ON)), - FlatAtom::Note(0), - FlatAtom::LoopEnds, - FlatAtom::SlopeEnds, - FlatAtom::SlopeEnds - ]) - ) - } - - #[test] - fn mixed_mismatched_end() { - assert_eq!( - Err(InflateError::MismatchedEnd(WrapperDiscriminants::Slope)), - inflate(vec![ - FlatAtom::SlopeStarts(SlopeModifier::Note, instruction()), - FlatAtom::SlopeStarts(SlopeModifier::Length, instruction()), - FlatAtom::Note(2), - FlatAtom::TupleStarts, - FlatAtom::Rest, - FlatAtom::SlopeEnds, // mismatched slope end while in a tuple - FlatAtom::Note(6), - FlatAtom::TupleEnds, - FlatAtom::Note(3), - FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(9) }), - FlatAtom::QuickModifier(QuickModifier::Pizz(ON)), - FlatAtom::Note(0), - FlatAtom::LoopEnds, - FlatAtom::SlopeEnds, - FlatAtom::SlopeEnds, - ]) - ) - } -} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index da8fc4e..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::{fmt::Display, fs::read_to_string, io, str::FromStr}; - -use amplify::{From, Wrapper}; -use clap::Parser; - -/// Cli entry point -#[derive(Clone, Parser)] -#[cfg_attr(debug_assertions, derive(Debug))] -pub enum BngCli { - /// Play the song through default sink - Play(PlayOpts), - /// Export the song to a sound FileContents - Export(ExportOpts), - /// List supported sound FileContents extensions and instrument / song available expressions - #[command(subcommand)] - List(ListOpts), -} - -/// [`BngCli`] "play" command options -#[derive(Clone, Parser)] -#[cfg_attr(debug_assertions, derive(Debug))] -pub struct PlayOpts { - #[arg(value_parser = FileContents::from_str)] - pub(super) input: FileContents, -} - -/// [`BngCli`] "export" command options -#[derive(Clone, Parser)] -#[cfg_attr(debug_assertions, derive(Debug))] -pub struct ExportOpts { - /// Input FileContents (written song FileContents) - #[arg(value_parser = FileContents::from_str)] - input: FileContents, - /// Output FileContents (sound FileContents) - #[arg(value_parser = AudioFileName::from_str)] - output: AudioFileName, -} - -/// [`BngCli`] "list" command sub-commands -#[derive(Clone, Parser)] -#[cfg_attr(debug_assertions, derive(Debug))] -pub enum ListOpts { - /// List supported sound FileContents extensions to export songs - #[command(subcommand)] - Extensions, - /// List available math expressions for instrument definition - #[command(subcommand)] - Math, - /// List available score glyphs and their meaning - #[command(subcommand)] - Glyphs, -} - -#[derive(Clone, Wrapper, From)] -#[wrapper(Deref)] -#[cfg_attr(debug_assertions, derive(Debug))] -pub struct FileContents(String); - -impl FromStr for FileContents { - type Err = io::Error; - fn from_str(s: &str) -> Result { - read_to_string(s).map(Into::into) - } -} - -#[derive(Clone, Wrapper, From)] -#[wrapper(Deref)] -#[cfg_attr(debug_assertions, derive(Debug))] -pub struct AudioFileName(String); - -#[derive(Debug)] -pub struct UnsupportedFileExtensionError; - -impl std::error::Error for UnsupportedFileExtensionError {} - -impl Display for UnsupportedFileExtensionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "The extension of the selected output sound file is not supported." - ) - } -} - -impl FromStr for AudioFileName { - type Err = UnsupportedFileExtensionError; - fn from_str(s: &str) -> Result { - Ok(s.to_owned().into()) - } -} diff --git a/src/cli/cli.rs b/src/cli/cli.rs new file mode 100644 index 0000000..42f7816 --- /dev/null +++ b/src/cli/cli.rs @@ -0,0 +1,470 @@ +#![allow(dead_code)] + +use std::{ + error::Error, + fmt::Debug, + fs::File, + io::{self, Cursor, Read, stdin}, + ops::Not, + str::FromStr, +}; + +use anyhow::anyhow; +use bliplib::compiler::Expression; +use clap::Parser; +use derive_wrapper::AsRef; +use getset::Getters; +use hound::SampleFormat; +use mp3lame_encoder::{Bitrate, Quality}; +use nom::{ + Finish, Parser as P, + branch::alt, + bytes::complete::tag, + character::complete::{char, u16, usize}, + combinator::{rest, success, value}, + error::ErrorKind, + sequence::preceded, +}; +use nom_locate::{LocatedSpan, position}; +use strum::{EnumDiscriminants, EnumIter, EnumString, IntoDiscriminant, IntoStaticStr}; +use thiserror::Error; + +const DEFAULT_INSTRUMENT: &str = "sin(2*pi()*(442*2^((n+1)/N))*t)"; +const DEFAULT_LENGTH: &str = "2^(2-log(2, l))*(60/T)"; + +#[derive(Debug, Parser)] +#[command(version, author, about)] +pub(super) enum Cli { + /// Play a song + Play(PlayOpts), + /// Check for typos + Check(PlayOpts), + /// Export a song to an audio file or stdout + Export(ExportOpts), + /// Memo menu for examples and general help about syntax and supported audio formats + #[command(subcommand)] + Memo(MemoKind), +} + +#[derive(Debug, Parser, Clone, Getters)] +#[getset(get = "pub(super)")] +pub(super) struct PlayOpts { + /// Use this sheet music [default: stdin] + #[command(flatten)] + input: InputGroup, + /// Set available notes ("a,b,c" for example) + #[arg(short, long, value_delimiter = ',')] + notes: Vec, + /// Set the signal expression (instrument) used to generate music samples + #[arg(short, long, default_value = DEFAULT_INSTRUMENT)] + instrument: Expression, + /// Set the expression used to generate note lengths in seconds + #[arg(short, long, default_value = DEFAULT_LENGTH)] + length: Expression, + /// Add a variable named VARIABLE (a single letter) and set its initial value to VALUE + #[arg(short, long = "variable", value_name = "VARIABLE=VALUE", value_parser = parse_key_val::<'=', Letter, f64>)] + #[getset(skip)] + variables: Vec<(Letter, f64)>, + /// Add a macro named NAME (single character, not alphanumeric) which expands to EXPANSION when called in sheet music + #[arg(short, long = "macro", value_name = "NAME:EXPANSION", value_parser = parse_key_val::<':', NotALetter, String>)] + #[getset(skip)] + macros: Vec<(NotALetter, String)>, + /// Add a slope expression named NAME which mutates the VARIABLE with the result of EXPR each frame + #[arg(short, long = "slope", value_name = "NAME:VARIABLE=EXPR", value_parser = parse_key_tuple::)] + #[getset(skip)] + slopes: Vec<(LetterString, (Letter, Expression))>, +} + +impl PlayOpts { + pub(super) fn variables(&self) -> impl Iterator { + self.variables.iter().map(|(c, f)| (c.as_ref(), f)) + } + + pub(super) fn macros(&self) -> impl Iterator { + self.macros.iter().map(|(c, s)| (c.as_ref(), s)) + } + + pub(super) fn slopes(&self) -> impl Iterator { + self.slopes + .iter() + .map(|(name, (v, e))| (name.as_ref(), (v.as_ref(), e))) + } +} + +impl InputGroup +where + Self: Clone, +{ + pub(super) fn get(&self) -> Box { + self.input + .as_ref() + .map(|i| Box::new(i.clone().0) as Box) + .or(self + .sheet_music_string + .as_ref() + .map(|s| Box::new(Cursor::new(s.clone())) as Box)) + .unwrap_or(Box::new(stdin())) + } +} + +#[derive(Debug, Clone, Copy, AsRef)] +struct Letter(char); + +#[derive(Debug, Error)] +enum LetterError { + #[error("the character '{0}' should be a letter")] + Char(char), + #[error("no characters found")] + Empty, +} + +impl FromStr for Letter { + type Err = LetterError; + fn from_str(s: &str) -> Result { + let c = s.chars().next().ok_or(Self::Err::Empty)?; + c.is_alphabetic() + .then_some(c) + .map(Self) + .ok_or(Self::Err::Char(c)) + } +} + +#[derive(Debug, Clone, Copy, AsRef)] +struct NotALetter(char); + +#[derive(Debug, Error)] +enum NotALetterError { + #[error("the character '{0}' should not be a letter")] + Char(char), + #[error("no characters found")] + Empty, +} + +impl FromStr for NotALetter { + type Err = NotALetterError; + fn from_str(s: &str) -> Result { + let c = s.chars().next().ok_or(Self::Err::Empty)?; + c.is_alphabetic() + .not() + .then_some(c) + .map(Self) + .ok_or(Self::Err::Char(c)) + } +} + +#[derive(Debug, Clone, AsRef)] +struct LetterString(String); + +#[derive(Debug, Error)] +enum LetterStringError { + #[error("the string \"{0}\" should be only letters")] + String(String), + #[error("no characters found")] + Empty, +} + +impl FromStr for LetterString { + type Err = LetterStringError; + fn from_str(s: &str) -> Result { + s.is_empty() + .not() + .then_some( + s.chars() + .all(|c| c.is_alphabetic()) + .then_some(s) + .map(str::to_string) + .map(Self) + .ok_or(Self::Err::String(s.to_string())), + ) + .ok_or(Self::Err::Empty)? + } +} + +/// Parse a single key-value pair +/// +/// From https://github.com/clap-rs/clap/blob/6b12a81bafe7b9d013b06981f520ab4c70da5510/examples/typed-derive.rs +fn parse_key_val( + s: &str, +) -> Result<(T, U), Box> +where + T: std::str::FromStr, + T::Err: Error + Send + Sync + 'static, + U: std::str::FromStr, + U::Err: Error + Send + Sync + 'static, +{ + let pos = s + .find(SEP) + .ok_or_else(|| format!("invalid KEY{SEP}value: no `{SEP}` found in `{s}`"))?; + Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) +} + +fn parse_key_tuple( + s: &str, +) -> Result<(T, (U1, U2)), Box> +where + T: std::str::FromStr, + T::Err: Error + Send + Sync + 'static, + U1: std::str::FromStr, + U1::Err: Error + Send + Sync + 'static, + U2: std::str::FromStr, + U2::Err: Error + Send + Sync + 'static, +{ + let pos = s + .find(':') + .ok_or_else(|| format!("invalid KEY:value, no `:` found in `{s}`"))?; + Ok(( + s[..pos].parse()?, + parse_key_val::<'=', _, _>(&s[pos + 1..])?, + )) +} + +#[derive(Debug, Parser, Clone)] +#[group(required = false, multiple = false)] +pub(super) struct InputGroup { + /// Set the path to your sheet music file [default: stdin] + input: Option>, + /// Use this sheet music instead of reading from a file or stdin + #[arg(short = 'c')] + sheet_music_string: Option, +} + +#[derive(Debug)] +pub(super) struct ClonableFile(File); + +impl Clone for ClonableFile { + fn clone(&self) -> Self { + Self(self.0.try_clone().expect("cloning file handle")) + } +} + +impl FromStr for ClonableFile { + type Err = io::Error; + fn from_str(s: &str) -> Result { + match CREATE_IF_NOT_EXISTS { + true => File::create(s), + false => File::open(s), + } + .map(Self) + } +} + +impl From> for File { + fn from(value: ClonableFile) -> Self { + value.0 + } +} + +#[derive(Debug, Parser, Clone)] +pub(super) struct ExportOpts { + #[command(flatten)] + pub(super) playopts: PlayOpts, + /// Audio format to use + #[cfg_attr( + feature = "mp3", + arg(default_value = "mp3 --bitrate 128 --quality best") + )] + #[cfg_attr( + not(feature = "mp3"), + cfg_attr(feature = "raw", arg(default_value = "raw mulaw")) + )] + #[arg(short, long, value_parser = audio_format_parser)] + pub(super) format: AudioFormat, + /// Output file [default: stdout] + #[arg(short, long)] + pub(super) output: Option>, +} + +#[derive(Clone, EnumDiscriminants)] +#[strum_discriminants( + derive(EnumString, IntoStaticStr, EnumIter), + strum(serialize_all = "lowercase") +)] +pub(super) enum AudioFormat { + Mp3 { + bitrate: Bitrate, + quality: Quality, + }, + Wav { + bps: u16, + sample_format: SampleFormat, + }, + Flac { + bps: usize, + }, + Raw(RawAudioFormat), +} + +impl Debug for AudioFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Raw(r) => f.debug_tuple("Raw").field(r).finish(), + _ => self.discriminant().fmt(f), + } + } +} + +impl Default for AudioFormat { + fn default() -> Self { + AudioFormat::Raw(Default::default()) + } +} + +fn audio_format_parser(input: &str) -> Result { + fn mp3<'a>() -> impl P< + LocatedSpan<&'a str>, + Output = AudioFormat, + Error = nom::error::Error>, + > { + preceded( + tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error>>( + AudioFormatDiscriminants::Mp3.into(), + ), + u16.or(success(320)) + .and( + alt(( + value(Quality::Best, tag("b")), + value(Quality::SecondBest, tag("sb")), + value(Quality::NearBest, tag("nb")), + value(Quality::VeryNice, tag("v")), + value(Quality::Nice, tag("n")), + value(Quality::Good, tag("g")), + value(Quality::Decent, tag("d")), + value(Quality::Ok, tag("o")), + value(Quality::SecondWorst, tag("sw")), + value(Quality::Worst, tag("w")), + )) + .or(success(Quality::Good)), + ) + .and(position) + .map_res(|((b, q), p)| { + Ok(AudioFormat::Mp3 { + bitrate: match b { + 8 => Bitrate::Kbps8, + 16 => Bitrate::Kbps16, + 24 => Bitrate::Kbps24, + 32 => Bitrate::Kbps32, + 40 => Bitrate::Kbps40, + 48 => Bitrate::Kbps48, + 64 => Bitrate::Kbps64, + 80 => Bitrate::Kbps80, + 96 => Bitrate::Kbps96, + 112 => Bitrate::Kbps112, + 128 => Bitrate::Kbps128, + 160 => Bitrate::Kbps160, + 192 => Bitrate::Kbps192, + 224 => Bitrate::Kbps224, + 256 => Bitrate::Kbps256, + 320 => Bitrate::Kbps320, + _ => return Err(nom::error::Error::new(p, ErrorKind::Verify)), + }, + quality: q, + }) + }), + ) + } + fn wav<'a>() -> impl P< + LocatedSpan<&'a str>, + Output = AudioFormat, + Error = nom::error::Error>, + > { + preceded( + tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error>>( + AudioFormatDiscriminants::Wav.into(), + ), + u16.or(success(32)) + .and(value(SampleFormat::Int, char('i')).or(success(SampleFormat::Float))) + .map(|(bps, sample_format)| AudioFormat::Wav { bps, sample_format }), + ) + } + fn flac<'a>() -> impl P< + LocatedSpan<&'a str>, + Output = AudioFormat, + Error = nom::error::Error>, + > { + preceded( + tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error>>( + AudioFormatDiscriminants::Flac.into(), + ), + usize.or(success(16)).map(|bps| AudioFormat::Flac { bps }), + ) + } + fn parser<'a>() -> impl P< + LocatedSpan<&'a str>, + Output = AudioFormat, + Error = nom::error::Error>, + > { + alt(( + mp3::<'a>(), + wav::<'a>(), + flac::<'a>(), + rest.map_res(|r: LocatedSpan<&'a str>| { + Ok::>>(AudioFormat::Raw( + (*r == "raw") + .then_some(Default::default()) + .ok_or(nom::error::Error::new(r, ErrorKind::Verify)) + .or(RawAudioFormat::try_from(*r) + .map_err(|_| nom::error::Error::new(r, ErrorKind::Verify)))?, + )) + }), + )) + } + parser() + .parse_complete(LocatedSpan::new(input)) + .finish() + .map(|(_, o)| o) + .map_err(|e| anyhow!("{e:?}")) +} + +#[derive(Debug, Parser, Clone, EnumString, Default, EnumIter, IntoStaticStr)] +#[strum(ascii_case_insensitive)] +pub(super) enum RawAudioFormat { + ALaw, + F32Be, + F32Le, + F64Be, + F64Le, + #[default] + MuLaw, + S8, + S16Be, + S16Le, + S24Be, + S24Le, + S32Be, + S32Le, + U8, + U16Be, + U16Le, + U24Be, + U24Le, + U32Be, + U32Le, +} + +#[derive(Debug, Parser, Clone)] +pub(super) enum MemoKind { + #[command(flatten)] + Syntax(SyntaxTarget), + /// Show a list of examples or a specific example from the list + Examples { n: Option }, + /// Print all available formats + Formats, +} + +#[derive(Debug, Parser, Clone)] +pub(super) enum SyntaxTarget { + /// Print BLIP's grammar + Blip, + #[command(flatten)] + Expressions(FastEvalSyntaxSection), +} + +#[derive(Debug, Parser, Clone)] +pub(super) enum FastEvalSyntaxSection { + /// Print available functions and constants for expressions + Functions, + /// Print available operators for expressions + Ops, + /// Print available literals for expressions + Literals, +} diff --git a/src/cli/main.rs b/src/cli/main.rs new file mode 100644 index 0000000..9127291 --- /dev/null +++ b/src/cli/main.rs @@ -0,0 +1,634 @@ +//! See [the lib docs](https://docs.rs/bliplib) + +#[macro_use] +mod cli; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + fs::File, + io::{Cursor, Write, read_to_string, stdout}, + iter::once, +}; + +use anyhow::{Context as _, anyhow, bail}; +use bliplib::{ + compiler::{Compiler, Context, SAMPLE_RATE, VariableChange}, + parser::{LocatedVerboseError, Parser}, +}; +use clap::Parser as _; +use cli::Cli; +use dasp_sample::{I24, Sample}; +use flacenc::{component::BitRepr, error::Verify}; +use hound::{SampleFormat, WavSpec, WavWriter}; +use log::{debug, error, info, warn}; +use mp3lame_encoder::{Builder as Mp3EncoderBuilder, FlushNoGap, MonoPcm}; +use rodio::{OutputStream, Sink, buffer::SamplesBuffer}; +use strum::IntoEnumIterator; + +use crate::cli::{ + AudioFormatDiscriminants, ExportOpts, FastEvalSyntaxSection, MemoKind, PlayOpts, + RawAudioFormat, SyntaxTarget, +}; + +fn main() -> anyhow::Result<()> { + env_logger::init(); + let cli = Cli::parse(); + debug!("options: {cli:#?}"); + use Cli::*; + match cli { + Check(opts) => { + parse_and_compile(&opts)?; + } + Play(opts) => { + let (_stream, stream_handle) = OutputStream::try_default() + .context("Failed to find (or use) default audio device")?; + info!("output stream acquired"); + let sink = Sink::try_new(&stream_handle).context("Epic audio playback failure")?; + info!("audio sink acquired"); + + let samples: Vec = parse_and_compile(&opts)? + .into_iter() + .map(Sample::to_sample) + .collect(); + info!("result: {} samples", samples.len()); + if samples.is_empty() { + warn!("0 samples generated"); + } + + info!("appending samples to sink"); + sink.append(SamplesBuffer::new(1, SAMPLE_RATE as u32, samples)); + info!("sleeping until end of sink"); + sink.sleep_until_end(); + } + Export(ExportOpts { + playopts, + format, + output, + }) => { + let samples = parse_and_compile(&playopts)?; + info!("result: {} samples", samples.len()); + use cli::AudioFormat::*; + match format { + Wav { bps, sample_format } => { + let mut buff = Cursor::new(Vec::with_capacity(samples.len() * 8)); + { + if sample_format == SampleFormat::Float { + if bps != 32 { + bail!( + "Sorry, only 32 bps is supported for float samples. Use \"wav32\"" + ); + } + let mut writer = WavWriter::new( + &mut buff, + WavSpec { + channels: 1, + sample_rate: SAMPLE_RATE as u32, + bits_per_sample: bps, + sample_format, + }, + ) + .context("Failed to create WAV writer")?; + for sample in samples { + let sample_f32: f32 = sample.to_sample(); + writer.write_sample(sample_f32)?; + } + } else { + let mut writer = WavWriter::new( + &mut buff, + WavSpec { + channels: 1, + sample_rate: SAMPLE_RATE as u32, + bits_per_sample: bps, + sample_format, + }, + ) + .context("Failed to create WAV writer")?; + match bps { + 32 => { + for sample in samples { + let sample_i32: i32 = sample.to_sample(); + writer.write_sample(sample_i32)?; + } + } + 16 => { + for sample in samples { + let sample_i32: i16 = sample.to_sample(); + writer.write_sample(sample_i32)?; + } + } + 8 => { + for sample in samples { + let sample_i32: i8 = sample.to_sample(); + writer.write_sample(sample_i32)?; + } + } + _ => bail!( + "for ints, the only valid bps for the wav backend are 8, 16 or 32" + ), + } + } + } + let mut writer: Box = output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())); + info!("writing samples to output"); + writer.write_all(buff.get_ref())?; + } + Flac { bps } => { + let config = flacenc::config::Encoder::default() + .into_verified() + .expect("Config data error."); + let source = flacenc::source::MemSource::from_samples( + samples + .into_iter() + .map(|sample| match bps { + 8 => sample.to_sample::() as i32, + 16 => sample.to_sample::() as i32, + 24 => sample.to_sample::().inner(), + _=> unimplemented!("sorry, the current implementation for the flac encoder doesn't support any other bitrate than 8, 16 or 24.") + }) + .collect::>() + .as_slice(), + 1, + bps, + SAMPLE_RATE.into(), + ); + let flac_stream = + flacenc::encode_with_fixed_block_size(&config, source, config.block_size) + .expect("Encode failed."); + + // `Stream` imlpements `BitRepr` so you can obtain the encoded stream via + // `ByteSink` struct that implements `BitSink`. + let mut sink = flacenc::bitsink::ByteSink::new(); + flac_stream + .write(&mut sink) + .context("Failed to write samples to FLAC byte sink")?; + + let mut writer: Box = output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())); + writer + .write_all(sink.as_slice()) + .context("Failed to write samples to output")?; + } + Mp3 { bitrate, quality } => { + let buff = { + let mut encoder = Mp3EncoderBuilder::new() + .context("Failed to create MP3 encoder builder")?; + encoder + .set_num_channels(1) + .context("Failed to set MP3 encoder channels")?; + encoder + .set_sample_rate(SAMPLE_RATE.into()) + .context("Failed to set MP3 encoder sample rate")?; + encoder + .set_brate(bitrate) + .context("Failed to set MP3 encoder bitrate")?; + encoder + .set_quality(quality) + .context("Failed to set MP3 encoder quality")?; + + let mut encoder = encoder + .build() + .context("Failed to initialize MP3 encoder")?; + + let input = MonoPcm(samples.as_slice()); + let mut output = Vec::with_capacity( + mp3lame_encoder::max_required_buffer_size(input.0.len()), + ); + let encoded_size = encoder + .encode(input, output.spare_capacity_mut()) + .context("Failed MP3 encoding")?; + unsafe { + output.set_len(output.len().wrapping_add(encoded_size)); + } + let encoded_size = encoder + .flush::(output.spare_capacity_mut()) + .context("Failed MP3 flushing (don't know what that means)")?; + unsafe { + output.set_len(output.len().wrapping_add(encoded_size)); + } + output + }; + let mut writer: Box = output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())); + info!("writing samples to output"); + writer.write_all(&buff)?; + } + // I don't know how to easily solve this copy paste with macros because of the variant name usage + Raw(format) => match format { + RawAudioFormat::ALaw => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::ALaw, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::F32Be => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::F32Be, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::F32Le => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::F32Le, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::F64Be => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::F64Be, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::F64Le => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::F64Le, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::MuLaw => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::MuLaw, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::S8 => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::S8, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::S16Be => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::S16Be, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::S16Le => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::S16Le, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::S24Be => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::S24Be, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::S24Le => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::S24Le, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::S32Be => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::S32Be, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::S32Le => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::S32Le, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::U8 => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::U8, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::U16Be => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::U16Be, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::U16Le => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::U16Le, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::U24Be => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::U24Be, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::U24Le => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::U24Le, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::U32Be => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::U32Be, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + RawAudioFormat::U32Le => raw_audio::Encoder::new( + output + .map(File::from) + .map(Box::new) + .map(|b| b as Box) + .unwrap_or(Box::new(stdout())), + raw_audio::pcm::U32Le, + ) + .encode( + fon::Audio::::with_f64_buffer(SAMPLE_RATE, samples) + .drain(), + ) + .context("Failed to encode to raw audio")?, + }, + } + } + Memo(MemoKind::Syntax(s)) => println!( + "{}", + match s { + SyntaxTarget::Blip => include_str!("../../doc/language-design.txt"), + SyntaxTarget::Expressions(FastEvalSyntaxSection::Functions) => + include_str!("../../doc/fasteval/functions.txt"), + SyntaxTarget::Expressions(FastEvalSyntaxSection::Literals) => + include_str!("../../doc/fasteval/literals.txt"), + SyntaxTarget::Expressions(FastEvalSyntaxSection::Ops) => + include_str!("../../doc/fasteval/operators.txt"), + } + ), + Memo(MemoKind::Examples { n }) => { + let mut examples = include_str!("../../doc/examples.txt") + .lines() + .filter_map(|l| l.split_once(':')); + match n { + None => { + for (id, (name, _)) in examples.enumerate() { + println!("{id}\t{name}") + } + } + Some(id) => println!( + "{}", + examples + .nth(id.into()) + .context("example not found")? + .1 + .trim_start() + ), + } + } + Memo(MemoKind::Formats) => { + for discriminant in AudioFormatDiscriminants::iter() { + use AudioFormatDiscriminants::*; + let ext: Cow<'static, str> = match discriminant { + Mp3 => { + "\t[int, bitrate]\t\t[str, quality from 'w' (\"worst\") to 'b' (\"best\")]\t(default: mp3320g)".into() + } + Wav => { + "\t[int, bytes per sample]\t['i', set sample format to int instead of float]\t(default: wav32)".into() + } + Flac => "\t[int, bits per sample]\t\t\t\t\t\t\t\t(default: flac320000)".into(), + Raw => format!( + "\t[str, subformat]\t\t\t\t\t\t\t\t(default: rawmulaw)\nraw subformats include: {:#?}", + RawAudioFormat::iter() + .map(Into::into) + .collect::>() + ).into(), + }; + println!("* {}{}", Into::<&'static str>::into(discriminant), ext) + } + } + } + Ok(()) +} + +fn parse_and_compile(opts: &PlayOpts) -> anyhow::Result> { + let default_variables = [ + ('l', 4f64), + ('L', 0.0), + ('t', 0.0), + ('T', 60.0), + ('N', opts.notes().len() as f64), + ]; + + info!("building parser"); + let parser = Parser::new( + opts.notes(), + opts.slopes() + .map(|(s, (v, e))| (s, VariableChange(*v, e.clone()))) + .collect::>(), + default_variables + .iter() + .map(|(v, _)| *v) + .chain(opts.variables().map(|(v, _)| *v)) + .collect::>() + .into_iter() + .collect::>(), + ); + info!("reading input"); + let input = read_to_string(opts.input().get()).context("Failed to read input")?; + info!("parsing tokens"); + let tokens = parser + .parse_all(&input) + .map_err(|e| match e { + nom::Err::Incomplete(n) => { + anyhow!("nom parsers said the input was incomplete and needed {n:?} bytes") + } + nom::Err::Error(LocatedVerboseError { location, error }) + | nom::Err::Failure(LocatedVerboseError { location, error }) => error + .unwrap_or(anyhow!("input did not match any known grammar (typo?)")) + .context(format!( + "line {line} column {column} (at \"{at}\")", + line = location.location_line(), + column = location.get_utf8_column(), + at = { + if location.len() > 10 { + location + .chars() + .take(10) + .chain("...".chars()) + .collect::() + } else if location.is_empty() { + String::from("EOF") + } else { + location.to_string() + } + } + )), + }) + .context("Failed to parse input")?; + info!("found {} tokens", tokens.as_ref().len()); + if tokens.as_ref().is_empty() { + warn!("0 tokens parsed"); + } + debug!("tokens: {:#?}", tokens.as_ref()); + + info!("building compiler"); + let compiler = Compiler::from(Context::new( + 'L'.to_string(), + 'n'.to_string(), + default_variables + .into_iter() + .chain(opts.variables().map(|(a, b)| (*a, *b))) + .map(|(c, v)| (c.to_string(), v)) + .collect::>(), + opts.instrument().clone(), + opts.slopes() + .map(|(_, (a, b))| (a.to_string(), b.clone())) + .chain(once(('L'.to_string(), opts.length().clone()))), + )); + info!("compiling to samples"); + compiler + .compile_all(tokens) + .inspect(|v| { + let is_nan = |sample: &f64| sample.abs().is_nan(); + if v.iter().all(is_nan) { + error!("🎉 All your samples are NaN, you got yourself a \"Not a Song\" (NaS)!") + } else if v.iter().any(is_nan) { + error!("Waiter! There's a NaN in my samples!"); + } + }) + .context("Failed to compile tokens to samples") +} diff --git a/src/compiler.rs b/src/compiler.rs new file mode 100644 index 0000000..e1b1fc1 --- /dev/null +++ b/src/compiler.rs @@ -0,0 +1,619 @@ +use std::{ + any::{Any, TypeId, type_name}, + collections::BTreeMap, + f64, + fmt::{Debug, Display}, + str::FromStr, +}; + +use cfg_if::cfg_if; +use derive_new::new; +use derive_wrapper::{AsRef, From}; +use fasteval::{Compiler as _, EvalNamespace, Evaler, Instruction, Slab}; +use log::{debug, trace}; +use thiserror::Error; + +cfg_if! { + if #[cfg(test)] { + /// Static sample rate. + pub const SAMPLE_RATE: u16 = 10; + } else { + /// Static sample rate. + pub const SAMPLE_RATE: u16 = 48000; + } +} + +/// A wrapper for a Vec of tokens. +#[derive(Debug, From, AsRef, Default)] +pub struct TokenVec(pub(crate) Vec>); + +impl IntoIterator for TokenVec { + type Item = Box; + type IntoIter = > as IntoIterator>::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Token for Box { + fn apply(&self, context: Context) -> Result { + self.as_ref().apply(context) + } +} + +impl Type for Box { + fn type_id(&self) -> TypeId { + self.as_ref().type_id() + } +} + +/// Used for getting the `type_id` of a type without it being [`std::any::Any`]. +pub trait Type { + /// Get the type ID of self without self being [`std::any::Any`]. + fn type_id(&self) -> TypeId; +} + +impl Type for T +where + T: Default + 'static, +{ + fn type_id(&self) -> TypeId { + ::type_id(&Default::default()) + } +} + +/// BLIP tokens implement this. +pub trait Token: Debug + Type { + /// Applies itself to a context for compilation into samples. + fn apply(&self, context: Context) -> Result; +} + +#[cfg(test)] +impl PartialEq for TokenVec { + fn eq(&self, other: &Self) -> bool { + format!("{self:?}") == format!("{other:?}") + } +} + +/// Litteral silence. No sound for the duration of a note. +#[derive(Debug, Clone, Copy, Default)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Silence; + +impl Token for Silence { + fn apply(&self, context: Context) -> Result { + debug!("⚡ {}", type_name::()); + let (mut context, mut next) = context.render(None)?; + context.result.append(&mut next); + Ok(context) + } +} + +/// Used to indicate where the playback or export should start from in the sheet music. +#[derive(Debug, Clone, Copy, Default)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Marker; + +impl Token for Marker { + fn apply(&self, mut context: Context) -> Result { + debug!("⚡ {}", type_name::()); + context.result.clear(); + Ok(context) + } +} + +/// A note. The underlying `u8` is for its position in the provided list of notes to be parsed. +#[derive(Debug, Clone, Copy, Default)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Note(pub u8); + +impl Token for Note { + fn apply(&self, context: Context) -> Result { + debug!("⚡ {}", type_name::()); + let (mut context, mut next) = context.render(Some(self.0))?; + context.result.append(&mut next); + Ok(context) + } +} + +/// Describes the instant mutation of a variable by the result of the provided [`Expression`] using the current values of all available variables. +#[derive(Debug, Clone, Default)] +#[cfg_attr(test, derive(PartialEq))] +pub struct VariableChange(pub char, pub Expression); + +impl AsRef for VariableChange { + fn as_ref(&self) -> &VariableChange { + self + } +} + +impl Token for VariableChange { + fn apply(&self, mut context: Context) -> Result { + debug!("⚡ {}", type_name::()); + *context.get_mut(self.0.to_string())? = context.eval(self.1.as_ref())?; + Ok(context) + } +} + +/// A wrapper for other tokens which repeat those tokens N times using the number described by [`LoopCount`]. +#[derive(Debug, Default)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Loop(pub LoopCount, pub TokenVec); + +/// Describes the number of times a loop should repeat its captured sequence of tokens. +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub enum LoopCount { + /// Fixed number of times + Litteral(usize), + /// Variable number of times, using the current value of the underlying variable + Variable(char), +} + +impl Default for LoopCount { + fn default() -> Self { + LoopCount::Litteral(2) + } +} + +impl Token for Loop { + fn apply(&self, mut context: Context) -> Result { + debug!("⚡ {}", type_name::()); + let mut old_result = context.result.clone(); + let count = match self.0 { + LoopCount::Litteral(n) => n, + LoopCount::Variable(v) => *context.get(v.to_string())? as usize, + }; + context.result.clear(); + let new_context = self + .1 + .0 + .iter() + .try_fold(context, |context, t| t.apply(context))?; + old_result.append(&mut new_context.result.repeat(count)); + Ok(Context { + result: old_result, + ..new_context + }) + } +} + +/// The duration of the underlying tokens in a tuplet will fit in the current calculated duration of a single note. The length of all these tokens will be shrinked equally. +#[derive(Debug, Default)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Tuplet(pub TokenVec); + +impl Token for Tuplet { + fn apply(&self, mut context: Context) -> Result { + debug!("⚡ {}", type_name::()); + if self.0.as_ref().is_empty() { + return Err(CompilerError::Nilplet); + } + let mut old_result = context.result.clone(); + context.result.clear(); + let mut new_context = self + .0 + .0 + .iter() + .try_fold(context, |context, t| t.apply(context))?; + let len = new_context.result.len(); + new_context.result = new_context + .result + .into_iter() + .step_by( + len / self + .0 + .0 + .iter() + .filter(|t| { + t.as_ref().type_id() == Type::type_id(&Note(0u8)) + || t.as_ref().type_id() == Type::type_id(&Silence) + }) + .count(), + ) + .collect(); + old_result.append(&mut new_context.result); + Ok(Context { + result: old_result, + ..new_context + }) + } +} + +/// A slope which describes a smooth change of a variable which will affect all captured tokens. Variable mutation occurs every frame / sample. +#[derive(Debug, new, Default)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Slope(pub VariableChange, pub TokenVec); + +impl Token for Slope { + fn apply(&self, mut context: Context) -> Result { + debug!("⚡ {}", type_name::()); + context.slopes.push(( + self.0.as_ref().0.to_string(), + self.0.as_ref().1.as_ref().clone(), + )); + context = self + .1 + .0 + .iter() + .try_fold(context, |context, t| t.apply(context))?; + context.slopes.pop(); + Ok(context) + } +} + +/// A mathematical expression to be evaluated with a set of variables to form a single [`f64`] value. +#[derive(Debug, new, Default)] +pub struct Expression { + /// Expression origin, used for cloning. + from: String, + /// The wrapped fasteval struct. + pub(crate) instruction: Instruction, + /// The slab which contains all the information referenced by the [`Instruction`]. An Instruction without a Slab is usually unusable. + pub(crate) slab: Slab, +} + +impl AsRef for Expression { + fn as_ref(&self) -> &Expression { + self + } +} + +impl Clone for Expression { + fn clone(&self) -> Self { + self.from + .parse() + .unwrap_or_else(|_| panic!("expression {self:?} have an invalid from")) + } +} + +impl PartialEq for Expression { + fn eq(&self, other: &Self) -> bool { + format!("{self:?}") == format!("{other:?}") + } +} + +impl FromStr for Expression { + type Err = fasteval::Error; + fn from_str(s: &str) -> Result { + let mut slab = Slab::new(); + let instruction = fasteval::Parser::new() + .parse(s, &mut slab.ps)? + .from(&slab.ps) + .compile(&slab.ps, &mut slab.cs); + Ok(Expression::new( + s.trim_start().trim_end().to_string(), + instruction, + slab, + )) + } +} + +impl Display for Expression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.from, f) + } +} + +/// Compiler context which persists from one token to the other. +#[derive(Debug, Clone, new)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Context { + note_length_variable: String, + note_index_variable: String, + /// Vec of samples resulting from compilation. + #[new(default)] + pub result: Vec, + /// Set of variables used for every [`Expression`] evaluation. + #[new(into_iter = "(String, f64)")] + pub variables: BTreeMap, + /// Instrument Expression which will be used to directly generate samples from the tokens. + pub instrument: Expression, + /// Set of [`Slope`]s to be applied to their corresponding variable at every frame. The string is the variable name. + #[new(into_iter = "(String, Expression)")] + pub slopes: Vec<(String, Expression)>, +} + +/// A variable which is expected to be present in the [`Context`]'s set of variables could not be found. +#[derive(Debug, Error)] +#[cfg_attr(test, derive(PartialEq))] +#[error("variable not found: {0}")] +pub struct VariableNotFoundError(String); + +/// A slope which is expected to be present in the [`Context`]'s set of slopes could not be found. +#[derive(Debug, Error)] +#[cfg_attr(test, derive(PartialEq))] +#[error("expression not found: {0}")] +pub struct SlopeNotFoundError(String); + +/// An error occurred during the compilation of tokens into samples. +#[derive(Debug, Error)] +#[cfg_attr(test, derive(PartialEq))] +pub enum CompilerError { + /// The error comes from a `fasteval` operation. + #[error("expression evaluation: {0:?}")] + FastEval(#[from] fasteval::Error), + /// See [`VariableNotFoundError`]. + #[error(transparent)] + VariableNotFound(#[from] VariableNotFoundError), + /// See [`SlopeNotFoundError`]. + #[error(transparent)] + SlopeNotFound(#[from] SlopeNotFoundError), + /// A tuplet was found which carried no underlying tokens. + #[error( + "🎉 You successfully made a nilplet / noplet (and I don't know what to do with it)\nTo resume compilation, remove any occurrence of \"[]\" in your sheet music." + )] + Nilplet, +} + +impl Context { + /// Calculates the current note length according to the note length expression and the current values of registered variables. + pub fn current_length(self) -> Result<(Self, f64), CompilerError> { + let Context { + note_length_variable, + note_index_variable, + result, + mut variables, + instrument, + slopes, + } = self; + let mut slopes_iter = slopes + .iter() + .filter_map(|(c, e)| (c == ¬e_length_variable).then_some(e)) + .peekable(); + if slopes_iter.peek().is_none() { + return Err(SlopeNotFoundError(note_length_variable.clone()).into()); + } + for slope in slopes_iter { + *variables + .get_mut(¬e_length_variable) + .ok_or(VariableNotFoundError(note_length_variable.clone()))? = + slope.instruction.eval(&slope.slab, &mut variables)?; + } + let note_length = *variables + .get(¬e_length_variable) + .ok_or(VariableNotFoundError(note_length_variable.clone()))?; + Ok(( + Context { + note_length_variable, + note_index_variable, + result, + variables, + instrument, + slopes, + }, + note_length, + )) + } + + /// Get a variable from the set. + pub fn get(&self, name: impl AsRef + Into) -> Result<&f64, VariableNotFoundError> { + self.variables + .get(name.as_ref()) + .ok_or(VariableNotFoundError(name.into())) + } + + /// Get a variable from the set with a mutable reference. + pub fn get_mut( + &mut self, + name: impl AsRef + Into, + ) -> Result<&mut f64, VariableNotFoundError> { + self.variables + .get_mut(name.as_ref()) + .ok_or(VariableNotFoundError(name.into())) + } + + /// Get a slope from the set using the name of the variable it's supposed to alter the value of. + pub fn get_slopes_for( + &self, + var: impl for<'a> PartialEq<&'a String> + Into, + ) -> Result, SlopeNotFoundError> { + let result: Vec<&Expression> = self + .slopes + .iter() + .filter_map(|(c, e)| (var == c).then_some(e)) + .collect(); + if result.is_empty() { + Err(SlopeNotFoundError(var.into())) + } else { + Ok(result) + } + } + + fn tick(mut self) -> Result { + *self.get_mut("t")? += 1f64 / (SAMPLE_RATE as f64); + let Context { + note_length_variable, + note_index_variable, + result, + mut variables, + instrument, + slopes, + } = self; + for (var, expr) in slopes.iter() { + *variables + .get_mut(var) + .ok_or(VariableNotFoundError(var.clone()))? = + expr.instruction.eval(&expr.slab, &mut variables)?; + } + Ok(Context { + note_length_variable, + note_index_variable, + result, + variables, + instrument, + slopes, + }) + } + + /// Evaluate the result of an Expression using the current values of the variables registered in the set. + pub fn eval(&mut self, expr: &Expression) -> Result { + Self::eval_with(expr, &mut self.variables) + } + + /// Evaluate the result of an Expression using the current values of the variables registered in the provided namespace. + pub fn eval_with( + Expression { + from, + instruction, + slab, + }: &Expression, + ns: &mut impl EvalNamespace, + ) -> Result { + instruction + .eval(slab, ns) + .inspect(|ok| trace!("{from} = {ok}")) + .inspect_err(|e| trace!("{from} = {e}")) + } + + /// Render a note (by providing the index of the note in the note list as `n`) or a silence (with None for `n`) into samples and return them in a Vec. + pub fn render(mut self, n: Option) -> Result<(Self, Vec), CompilerError> { + let curr_t = *self.get("t")?; + if let Some(note) = n { + let mut result = Vec::new(); + self.variables + .insert(self.note_index_variable.clone(), note as f64); + while { + let (new_self, length) = self.current_length()?; + self = new_self; + length + } > *self.get("t")? - curr_t + (1f64 / SAMPLE_RATE as f64) + { + result.push(Self::eval_with(&self.instrument, &mut self.variables)?); + self = self.tick()?; + } + Ok((self, result)) + } else { + while { + let (new_self, length) = self.current_length()?; + self = new_self; + length + } > *self.get("t")? - curr_t + { + self = self.tick()?; + } + let len = (*self.get("t")? - curr_t) * SAMPLE_RATE as f64; + Ok((self, vec![0.0; len as usize])) + } + } + + /// Explodes the context, leaving only the resulting Vec of samples. + pub fn finalize(self) -> Vec { + self.result + } +} + +/// Convenience struct for the whole compilation phase. +#[derive(From)] +#[cfg_attr(test, derive(Debug, PartialEq))] +pub struct Compiler(Context); + +impl Compiler { + /// Applies a single token on the underlying [`Context`]. + pub fn step(self, token: impl Token) -> Result { + token.apply(self.0).map(Into::into) + } + fn apply_all( + self, + tokens: impl IntoIterator, + ) -> Result { + tokens + .into_iter() + .try_fold(self, |acc, token| acc.step(token)) + } + /// Applies every token on the underlying [`Context`] and returns the resulting samples as an iterator. + pub fn compile_all( + self, + tokens: impl IntoIterator, + ) -> Result, CompilerError> { + self.apply_all(tokens).map(|c| c.0).map(Context::finalize) + } +} + +#[cfg(test)] +mod tests { + use crate::compiler::{Compiler, Expression, Note, SAMPLE_RATE, Silence}; + + use super::{CompilerError, Context}; + + #[test] + fn expression_is_clone() { + let expr: Expression = "1 + 5 / x".parse().unwrap(); + assert_eq!(expr, expr.clone()); + } + + fn context_generator() -> Context { + Context::new( + 'L'.to_string(), + 'n'.to_string(), + [ + ('a', 5.0), + ('t', 0.0), + ('n', 0.0), + ('N', 12.0), + ('L', 0.0), + ('l', 4.0), + ('T', 60.0), + ] + .map(|(c, f)| (c.to_string(), f)), + "sin(2*pi()*(442+442*((n+1)/N))*t)".parse().unwrap(), + [('L', "2^(2-log(2, l))*(60/T)")].map(|(c, e)| (c.to_string(), e.parse().unwrap())), + ) + } + + #[test] + fn silence_renders_correct_amount_of_samples() -> Result<(), CompilerError> { + assert_eq!( + SAMPLE_RATE as usize, + Compiler::from(context_generator()) + .apply_all(vec![Silence])? + .0 + .result + .len() + ); + Ok(()) + } + + #[test] + fn silence_renders_zeros() -> Result<(), CompilerError> { + assert!( + Compiler::from(context_generator()) + .apply_all(vec![Silence])? + .0 + .result + .into_iter() + .all(|s| s == 0f64) + ); + Ok(()) + } + + #[test] + fn note_renders_correct_amount_of_samples() -> Result<(), CompilerError> { + assert_eq!( + SAMPLE_RATE as usize, + Compiler::from(context_generator()) + .apply_all(vec![Note(2)])? + .0 + .result + .len() + ); + Ok(()) + } + + #[test] + fn reproducible_note() -> Result<(), CompilerError> { + let note = Note(3); + let mut compiler = Compiler::from(context_generator()); + + compiler = compiler.step(note)?; + let first = compiler.0.result.clone(); + + *compiler.0.get_mut('t'.to_string())? = 0.0; + compiler.0.result.clear(); + compiler = compiler.step(note)?; + let second = compiler.0.result.clone(); + + assert_eq!(first, second); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..96e0497 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +#![doc = include_str!("DOCS.md")] + +/// Compilation stuff (for turning tokens into samples) +pub mod compiler; +/// Parsing stuff (for turning strings into tokens) +pub mod parser; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 5a0fccc..0000000 --- a/src/main.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::collections::HashMap; - -use anyhow::Error; -use bng::{BngFile, Channel, Expression, Instrument}; -/// TODO: remove clap, use only a file or standard in -use clap::Parser; -use cli::{BngCli as Cli, PlayOpts}; -use fasteval::Compiler; - -mod bng; -mod cli; - -fn main() -> Result<(), Error> { - // println!("{}", option_env!("TEST").unwrap_or("ok")); - let args = Cli::parse(); - // #[cfg(debug_assertions)] - // { - // println!("{}", serde_yml::to_string(&bngfile_generator())?); - // println!("{:?}", args); - // } - match args { - Cli::Play(PlayOpts { input }) => { - let bng_file: BngFile = serde_yml::from_str(&input)?; - #[cfg(debug_assertions)] - println!("{:#?}", bng_file); - } - _ => unimplemented!("can't do that yet"), - } - Ok(()) -} - -#[cfg(debug_assertions)] -fn bngfile_generator() -> BngFile { - BngFile::new( - HashMap::from([ - ( - "sine".to_string(), - Instrument::new("sin(2*PI*f*t)".parse().unwrap(), None), - ), - ( - "square".to_string(), - Instrument::new( - "v*abs(sin(2*PI*f*t))".parse().unwrap(), - Some(HashMap::from([("v".to_string(), 1f32)])), - ), - ), - ]), - HashMap::::from([( - "melody".to_string(), - Channel::new("sine".to_string(), vec![].into()), - )]), - ) -} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..c265a7e --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,911 @@ +use std::{ + any::type_name, + borrow::{Borrow, Cow}, + collections::BTreeMap, + marker::PhantomData, +}; + +use anyhow::anyhow; +use derive_new::new; +use fasteval::Evaler; +use log::{debug, trace, warn}; +use nom::{ + AsChar, Compare, Input, Parser as _, + branch::alt, + bytes::complete::{tag, take, take_till}, + character::{ + complete::{char, space1, usize}, + streaming::one_of, + }, + combinator::{all_consuming, cut, opt, value}, + error::{ErrorKind, FromExternalError, ParseError}, + multi::many0, + sequence::{delimited, preceded}, +}; +use nom_locate::LocatedSpan; + +use crate::compiler::{ + Expression, Loop, LoopCount, Marker, Note, Silence, Slope, Token, TokenVec, Tuplet, + VariableChange, +}; + +/// From `nom`'s [`IResult`](https://docs.rs/nom/8.0.0/nom/type.IResult.html) : +/// > Holds the result of parsing functions +/// > +/// > It depends on the input type `I`, the output type `O`, and the error type `E` +/// > (by default `(I, nom::ErrorKind)`) +/// > +/// > The `Ok` side is a pair containing the remainder of the input (the part of the data that +/// > was not parsed) and the produced value. The `Err` side contains an instance of `nom::Err`. +/// > +/// > Outside of the parsing code, you can use the [Finish::finish] method to convert +/// > it to a more common result type +/// +/// The error type in this IResult is [`LocatedVerboseError`]. +pub type IResult> = nom::IResult; + +/// A parser wrapper for `nom` 8 which adds a context to the error. +#[derive(new)] +pub struct VerboseParser>, I> { + parser: P, + context: Cow<'static, str>, + #[new(default)] + phantom: PhantomData, +} + +impl ParseError for LocatedVerboseError { + fn from_error_kind(input: I, _kind: ErrorKind) -> Self { + Self { + location: input, + error: None, + } + } + + fn append(_input: I, _kind: ErrorKind, other: Self) -> Self { + other + } +} + +impl>> nom::Parser for VerboseParser { + type Output = P::Output; + + type Error = LocatedVerboseError; + + fn process( + &mut self, + input: I, + ) -> nom::PResult { + use nom::Err::*; + use nom::Mode; + + let stack_verbose_error = |e: LocatedVerboseError| -> LocatedVerboseError { + LocatedVerboseError { + error: Some(if let Some(cause) = e.error { + cause.context(self.context.clone()) + } else { + anyhow::Error::msg(self.context.clone()) + }), + ..e + } + }; + match self.parser.process::(input) { + Ok(o) => Ok(o), + Err(Error(e)) => Err(Error(OM::Error::map(e, stack_verbose_error))), + Err(Failure(e)) => Err(Failure(stack_verbose_error(e))), + Err(Incomplete(e)) => Err(Incomplete(e)), + } + } +} + +/// An error type for `nom` 8 which adds a context to parser errors using `anyhow::Error`. +#[derive(Debug)] +pub struct LocatedVerboseError { + /// Error location (I is the input type) + pub location: I, + /// Error description / context + pub error: Option, +} + +/// Expect the parser to succeed and if it doesn't, add a context to the error. +pub fn expect( + parser: P, + error_message: impl Into>, +) -> impl nom::Parser> +where + P: nom::Parser>, +{ + VerboseParser::new(parser, error_message.into()) +} + +impl FromExternalError for LocatedVerboseError { + fn from_external_error(input: I, _kind: ErrorKind, e: anyhow::Error) -> Self { + Self { + location: input, + error: Some(e), + } + } +} + +/// Convenience struct for the whole parsing phase. Very generic. +#[derive(new)] +pub struct Parser +where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, +{ + notes: N, + slopes: S, + variables: V, + phantom: PhantomData<(NS, SS)>, +} + +impl<'a, 'p, N, NS, S, SS, SV, V> Parser +where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator + Clone, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, + 'p: 'a, +{ + /// Parse all the input into a Vec of tokens. Returns a descriptive error on failure. + pub fn parse_all( + &'p self, + input: &'a str, + ) -> Result>>> { + debug!("parsing input \"{input}\""); + all_consuming(token_parser(self)) + .parse_complete(LocatedSpan::new(input)) + .map(move |(_, o)| o) + } +} + +fn token_parser<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, +) -> impl nom::Parser< + LocatedSpan<&'a str>, + Output = TokenVec, + Error = LocatedVerboseError>, +> +where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator + Clone, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, + 'p: 'a, +{ + trace!("making the TOKEN parser"); + let space_or_comment = || { + value( + (), + many0(value((), char('#').and(take_till(|c| c == '\n'))).or(value((), space1))), + ) + }; + many0(delimited( + space_or_comment(), + alt(( + expect(Silence::parser(), "expected a silence").map(into_box), + expect(Marker::parser(), "expected a marker").map(into_box), + expect( + VariableChange::parser(&parser.variables).map(into_box), + "variable assignment", + ), + expect(Loop::parser(parser).map(into_box), "in the loop"), + expect(Tuplet::parser(parser).map(into_box), "in the tuplet"), + expect(Slope::parser(parser).map(into_box), "in the slope"), + expect( + Note::parser(parser.notes.as_ref()), + "expected a note as last appeal (input didn't match anything known)", + ) + .map(into_box), + )), + space_or_comment(), + )) + .map(Into::into) +} + +fn into_box<'a>(token: impl Token + 'a) -> Box { + Box::new(token) +} + +impl Silence { + fn parser() -> impl nom::Parser> + where + I: Input, + ::Item: AsChar, + { + trace!("making the {} parser", type_name::()); + value(Self, char('.')) + } +} + +impl Marker { + fn parser() -> impl nom::Parser> + where + I: Input, + ::Item: AsChar, + { + trace!("making the {} parser", type_name::()); + value(Marker, char('%')) + } +} + +impl Note { + fn parser<'a, N, NS, I>( + notes: N, + ) -> impl nom::Parser> + 'a + where + N: IntoIterator, + NS: AsRef, + I: Input + for<'z> Compare<&'z str>, + { + trace!("making the {} parser", type_name::()); + let notes = { + let mut sorted = notes + .into_iter() + .map(|s| s.as_ref().to_string()) + .enumerate() + .collect::>(); + debug!("got notes {sorted:?}"); + sorted.sort_by_key(|(_, n)| n.len()); + sorted.reverse(); + debug!("sorted to {sorted:?}"); + sorted + }; + move |input: I| { + #[allow(clippy::type_complexity)] + let mut parsers: Vec IResult>> = notes + .clone() + .drain(..) + .map(|(i, t)| { + Box::new(move |input: I| { + value(Note(i as u8), tag(t.clone().as_ref())).parse(input) + }) as Box IResult> + }) + .collect(); + alt(parsers.as_mut_slice()).parse(input) + } + } +} + +impl VariableChange { + fn parser(variables: V) -> impl Fn(I) -> IResult + where + I: Input + AsRef + Copy, + ::Item: AsChar, + V: AsRef<[char]>, + { + trace!("making the {} parser", type_name::()); + let variables_string = variables.as_ref().iter().collect::(); + move |i: I| { + preceded( + char('$'), + cut(expect( + one_of(variables_string.as_str()), + format!( + "got unknown variable '{}', expected one of these instead: {:?}", + i.as_ref().chars().nth(1).unwrap_or('?'), + variables_string.chars().collect::>() + ), + ) + .and(cut(expect( + expression_parser(variables.as_ref()), + format!( + "expected a valid expression to assign variable {} to", + i.as_ref().chars().nth(1).unwrap_or('?') + ), + ))) + .map(|(name, change)| VariableChange(name, change))), + ) + .parse(i) + } + } +} + +impl Loop { + fn parser<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Self> + where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator + Clone, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, + 'p: 'a, + { + trace!("making the {} parser", type_name::()); + move |input| { + delimited( + char('('), + opt(alt(( + usize.map(LoopCount::Litteral), + one_of( + parser + .variables + .as_ref() + .iter() + .collect::() + .as_str(), + ) + .map(LoopCount::Variable), + ))) + .and(cut(take_till(|c| c == ')').and_then(cut(expect(all_consuming(token_parser(parser)), "input did not match any known grammar for inner tokens (typo?)"))))), + cut( + expect( + char(')'), + format!( + "the loop started at line {line} column {column} was not closed at this point", + line = input.location_line(), + column = input.get_utf8_column() + ) + ) + ), + ) + .map(|(c, v)| Self(c.unwrap_or_default(), v)) + .parse(input) + } + } +} + +impl Tuplet { + fn parser<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Self> + where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator + Clone, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, + 'p: 'a, + { + trace!("making the {} parser", type_name::()); + |input| { + delimited(char('['), + cut(take_till(|c| c == ']') + .and_then(cut(expect( + all_consuming(token_parser(parser)), + "input did not match any known grammar for inner tokens (typo?)")))), cut( + expect( + char(']'), + format!( + "the tuplet started at line {line} column {column} was not closed at this point", + line = input.location_line(), + column = input.get_utf8_column() + ) + ) + )) + .map(Self) + .parse(input) + } + } +} + +impl Slope { + fn parser<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Self> + where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator + Clone, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, + 'p: 'a, + { + trace!("making the {} parser", type_name::()); + move |input| { + let slopes = { + let mut vec = parser + .slopes + .clone() + .into_iter() + .map(|(s1, s2)| (s1.as_ref().to_string(), s2.borrow().clone())) + .collect::>(); + debug!("got slopes {vec:?}"); + vec.sort_by_key(|(name, _)| name.len()); + vec.reverse(); + debug!("sorted to {vec:?}"); + vec + }; + let iter: std::vec::IntoIter<(String, VariableChange)> = slopes.into_iter(); + delimited( + char('{'), + cut(expect(alt(iter + .map(|(k, v)| { + Box::new(move |input| value(v.clone(), tag(k.as_str())).parse(input)) + as Box< + dyn Fn( + LocatedSpan<&'a str>, + ) + -> IResult, VariableChange>, + > + }) + .collect::, + ) + -> IResult, VariableChange>, + >, + >>() + .as_mut_slice()), + format!( + "expected a slope name from available slope names ({:?})", + parser.slopes.clone().into_iter().map(|(s1, _)| s1.as_ref().to_string()).collect::>() + ))) + .and(cut(take_till(|c| c == '}').and_then(cut( + expect(all_consuming(token_parser(parser)), + "input did not match any known grammar for inner tokens (typo?)"))))), + cut( + expect( + char('}'), + format!( + "the slope started at line {line} column {column} was not closed at this point", + line = input.location_line(), + column = input.get_utf8_column() + ) + ) + ), + ) + .map(|(i, v)| Self::new(i, v)) + .parse(input) + } + } +} + +/// Will return the longest valid fasteval expression +fn expression_parser<'v, I, V, C>(variables: V) -> impl 'v + Fn(I) -> IResult +where + I: Input + AsRef + Copy, + V: IntoIterator, + C: Borrow, +{ + trace!("making the Expression parser"); + let variables: Vec<(String, f64)> = variables + .into_iter() + .map(|v| (v.borrow().to_string(), 0.0)) + .collect(); + debug!("got variables {variables:?}"); + move |input: I| { + take_while_map(|i: I| { + i.as_ref() + .parse::() + .inspect_err(|e| { + trace!( + "failed parsing expression {expr} with {err}", + expr = i.as_ref(), + err = e + ) + }) + .ok() + .and_then(|e| { + e.instruction + .eval( + &e.slab, + &mut BTreeMap::from_iter(variables.clone().drain(..)), + ) + .inspect_err(|e| { + trace!( + "failed expression evaluation {expr:?} with {err}", + expr = e, + err = e + ) + }) + .ok() + .is_some() + .then_some(e) + }) + }) + .parse(input) + } +} + +/// Take the maximum amount of input before `cond` returns [`None`], using the result of the last successful evaluation of `cond` for the output. +pub fn take_while_map(cond: F) -> impl FnMut(I) -> IResult> +where + I: Input + Copy, + F: Fn(I) -> Option, +{ + trace!("making take_while_map parser"); + move |input: I| { + let mut len = input.input_len(); + debug!( + "take_while_map will now match biggest munch from the rest of the input ({len} elements)" + ); + while len > 0 { + let result = take(len).map_opt(&cond).parse(input); + if result.is_ok() { + debug!("found a match using {len} elements"); + return result; + } else { + len -= 1; + } + } + warn!("take_while_map found no match"); + Err(nom::Err::Error(LocatedVerboseError { + location: input, + error: Some(anyhow!("invalid expression")), + })) + } +} + +#[cfg(test)] +mod tests { + use super::{IResult, Parser, expression_parser}; + + use std::{borrow::Borrow, collections::HashMap}; + + use nom::Parser as _; + use nom_locate::LocatedSpan; + + use crate::compiler::{ + Loop, LoopCount, Marker, Note, Silence, Slope, TokenVec, Tuplet, VariableChange, + }; + + fn very_fancy_slope() -> VariableChange { + VariableChange('n', "1+1".parse().unwrap()) + } + + fn normal_slope() -> VariableChange { + VariableChange('n', "1".parse().unwrap()) + } + + type DefaultParser = Parser< + [&'static str; 3], + &'static str, + HashMap, + String, + VariableChange, + [char; 1], + >; + + fn parser_generator() -> DefaultParser { + Parser::new( + ["do", "ré", "mi"], + HashMap::from([ + ("nor".to_string(), very_fancy_slope()), + ("normal".to_string(), normal_slope()), + ]), + ['n'], + ) + } + + #[test] + fn expression_parser_test() { + let parser = expression_parser(&['x']); + let mut working_test_cases = vec![ + ("1", ("", "1")), + ("1*x", ("", "1*x")), + ("56coucou", ("coucou", "56")), // should stop after 56 because c is not a known variable + ( + "8*x + 1 heille salut ça va ou quoi 46 - 5*x", + ("heille salut ça va ou quoi 46 - 5*x", "8*x + 1"), + ), + ]; + let mut not_working_test_cases = vec![ + "", + "(", + "y", + "abcdexx489", + " ", // spaces are not expressions + " h", // because of previous, this fails + // " 1", // the current impl of the parser means that this is valid + ]; + + for (test, expected) in working_test_cases.drain(..) { + let output = parser(test); + if let Ok(result) = output { + assert_eq!( + (expected.0, expected.1.parse().unwrap()), + result, + "case \"{test}\"" + ) + } else { + panic!("result of \"{test}\" was not Ok: {output:?}"); + } + } + + for test in not_working_test_cases.drain(..) { + let output = parser(test); + assert!( + output.is_err(), + "result of \"{test}\" was not Err: {output:?}" + ); + } + } + + #[test] + fn silence() { + let parser = |input| Silence::parser().parse(input); + let mut working_cases = vec![ + (".", ("", Silence)), + (".dd", ("dd", Silence)), + ("..", (".", Silence)), + ]; + let mut not_working_cases = vec!["", ",", "d.", " "]; + for (test, expected) in working_cases.drain(..) { + let output = parser(test); + if let Ok(result) = output { + assert_eq!(expected, result); + } else { + panic!("result of \"{test}\" was not Ok: {output:?}"); + } + } + for test in not_working_cases.drain(..) { + let output = parser(test); + assert!( + output.is_err(), + "result of \"{test}\" was not Err: {output:?}" + ); + } + } + + #[test] + fn marker() { + let parser = |input| Marker::parser().parse(input); + let mut working_cases = vec![ + ("%", ("", Marker)), + ("%dd", ("dd", Marker)), + ("%%", ("%", Marker)), + ]; + let mut not_working_cases = vec!["", ",", "d%", " "]; + for (test, expected) in working_cases.drain(..) { + let output = parser(test); + if let Ok(result) = output { + assert_eq!(expected, result); + } else { + panic!("result of \"{test}\" was not Ok: {output:?}"); + } + } + for test in not_working_cases.drain(..) { + let output = parser(test); + assert!( + output.is_err(), + "result of \"{test}\" was not Err: {output:?}" + ); + } + } + + #[test] + fn note() { + let parser = |input| Note::parser(&["do", "r", "ré", "mi", "m"]).parse(input); + let mut working_cases = vec![ + ("do", ("", Note(0))), + ("ré", ("", Note(2))), + ("rér", ("r", Note(2))), + ("r.", (".", Note(1))), + ("mi", ("", Note(3))), + ("mif", ("f", Note(3))), + ("mi ", (" ", Note(3))), + ("m ", (" ", Note(4))), + ]; + let mut not_working_cases = vec!["", ",", " mi", " ", "dré"]; + for (test, expected) in working_cases.drain(..) { + let output = parser(test); + if let Ok(result) = output { + assert_eq!(expected, result, "{test}"); + } else { + panic!("result of \"{test}\" was not Ok: {output:?}"); + } + } + for test in not_working_cases.drain(..) { + let output = parser(test); + assert!( + output.is_err(), + "result of \"{test}\" was not Err: {output:?}" + ); + } + } + + #[test] + fn variable_change() { + let parser = |input| VariableChange::parser(&['a', 'b', 'ö']).parse(input); + let mut working_cases = vec![("$a5", ("", ('a', "5")))]; + let mut not_working_cases = vec!["", "$", "$a", "$c8", "$d/1"]; + for (test, expected) in working_cases.drain(..) { + let output = parser(test); + if let Ok(result) = output { + assert_eq!( + ( + expected.0, + VariableChange(expected.1.0, expected.1.1.parse().unwrap()) + ), + result, + "case \"{test}\"" + ); + } else { + panic!("result of \"{test}\" was not Ok: {output:?}"); + } + } + for test in not_working_cases.drain(..) { + let output = parser(test); + assert!( + output.is_err(), + "result of \"{test}\" was not Err: {output:?}" + ); + } + } + + #[test] + fn r#loop() { + let parser = Parser::new( + ["do", "ré", "mi"], + HashMap::::default(), + ['n'], + ); + fn parser_builder<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Loop> + where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator + Clone, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, + 'p: 'a, + { + move |input| Loop::parser(parser).parse(input) + } + let parser = parser_builder(&parser); + let mut working_cases = vec![ + ( + "(.%)", + ( + "", + Loop( + LoopCount::Litteral(2), + TokenVec(vec![Box::new(Silence), Box::new(Marker)]), + ), + ), + ), + ("()", ("", Loop(LoopCount::Litteral(2), TokenVec(vec![])))), + ("(4)", ("", Loop(LoopCount::Litteral(4), TokenVec(vec![])))), + ( + "(n)", + ("", Loop(LoopCount::Variable('n'), TokenVec(vec![]))), + ), + ( + "(ndo)", + ( + "", + Loop(LoopCount::Variable('n'), TokenVec(vec![Box::new(Note(0))])), + ), + ), + ]; + let mut not_working_cases = vec!["", "(", ")", "(2", "(p)"]; + for (test, expected) in working_cases.drain(..) { + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); + if let Ok(result) = output { + assert_eq!(expected, result, "case \"{test}\""); + } else { + panic!("result of \"{test}\" was not Ok: {output:?}"); + } + } + for test in not_working_cases.drain(..) { + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); + assert!( + output.is_err(), + "result of \"{test}\" was not Err: {output:?}" + ); + } + } + + #[test] + fn tuplet() { + let parser = Parser::new( + ["do", "ré", "mi"], + HashMap::::default(), + ['n'], + ); + fn parser_builder<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Tuplet> + where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator + Clone, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, + 'p: 'a, + { + move |input| Tuplet::parser(parser).parse(input) + } + let parser = parser_builder(&parser); + let mut working_cases = vec![ + ( + "[.%]", + ( + "", + Tuplet(TokenVec(vec![Box::new(Silence), Box::new(Marker)])), + ), + ), + ("[]", ("", Tuplet(TokenVec(vec![])))), + ("[do]f", ("f", Tuplet(TokenVec(vec![Box::new(Note(0))])))), + ]; + let mut not_working_cases = vec!["", "[", "]", "[2", "[p]"]; + for (test, expected) in working_cases.drain(..) { + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); + if let Ok(result) = output { + assert_eq!(expected, result, "case \"{test}\""); + } else { + panic!("result of \"{test}\" was not Ok: {output:?}"); + } + } + for test in not_working_cases.drain(..) { + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); + assert!( + output.is_err(), + "result of \"{test}\" was not Err: {output:?}" + ); + } + } + + #[test] + fn slope() { + let p = parser_generator(); + let parser_builder = || Slope::parser(&p); + let parser = parser_builder(); + let normal_value = normal_slope(); + let very_fancy_value = very_fancy_slope(); + let mut working_cases = vec![ + ( + "{normal.%}", + ( + "", + Slope( + normal_value.clone(), + TokenVec(vec![Box::new(Silence), Box::new(Marker)]), + ), + ), + ), + ( + "{normal}", + ("", Slope(normal_value.clone(), TokenVec(vec![]))), + ), + ("{nor}", ("", Slope(very_fancy_value, TokenVec(vec![])))), + ( + "{normal do}f", + ("f", Slope(normal_value, TokenVec(vec![Box::new(Note(0))]))), + ), + ]; + let mut not_working_cases = vec![ + "", + "{", + "}", + "{normal", + "{fancy}", + "{norma do}", + "{do}", + "{}", + ]; + for (test, expected) in working_cases.drain(..) { + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); + if let Ok(result) = output { + assert_eq!(expected, result, "case \"{test}\""); + } else { + panic!("result of \"{test}\" was not Ok: {output:?}"); + } + } + for test in not_working_cases.drain(..) { + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); + assert!( + output.is_err(), + "result of \"{test}\" was not Err: {output:?}" + ); + } + } +}