Compare commits
No commits in common. "main" and "standardised-nom-parsing" have entirely different histories.
main
...
standardis
41 changed files with 1618 additions and 3764 deletions
21
.gitignore
vendored
21
.gitignore
vendored
|
@ -20,24 +20,3 @@ 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
|
||||
*\#
|
||||
|
|
77
Cargo.toml
77
Cargo.toml
|
@ -1,59 +1,34 @@
|
|||
[package]
|
||||
name = "bliplib"
|
||||
version = "0.2.5"
|
||||
edition = "2024"
|
||||
authors = ["Breval Ferrari <breval.ferrari@fish.golf>"]
|
||||
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"
|
||||
name = "bng"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
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"
|
||||
|
||||
[features]
|
||||
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"]
|
||||
default = ["play", "save"]
|
||||
play = ["dep:tinyaudio"]
|
||||
save = []
|
||||
|
||||
[[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"]
|
||||
[workspace]
|
||||
members = ["bng_macros"]
|
||||
|
||||
[lints.rust]
|
||||
missing_docs = "warn"
|
||||
unused = "allow"
|
||||
|
|
35
LICENSE
35
LICENSE
|
@ -1,7 +1,34 @@
|
|||
Copyright © 2025 Breval Ferrari
|
||||
NUCLEAR WASTE SOFTWARE LICENSE V1.0
|
||||
|
||||
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:
|
||||
Copyright 2024 Breval Ferrari
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
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 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.
|
||||
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.
|
||||
|
|
25
README.md
25
README.md
|
@ -1,20 +1,7 @@
|
|||
<img src="https://gitdab.com/breval/blip/raw/branch/main/doc/iconx4.png" alt="bppt logo" align="right">
|
||||
# bng
|
||||
[](https://crates.io/crates/bng)
|
||||
[](https://docs.rs/bng)
|
||||
[](https://gist.github.com/DavidBuchanan314/35cb9f8a2f754b9a03a74bed19575661)
|
||||
|
||||
[](https://crates.io/crates/bliplib)
|
||||
[](https://docs.rs/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
|
||||
```
|
||||
Bleeperpreter New Gen - the smarter 'preter!
|
||||
A better [Bleeperpreter](https://github.com/p6nj/bleeperpreter) from scratch.
|
||||
|
|
12
bng_macros/Cargo.toml
Normal file
12
bng_macros/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[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"] }
|
84
bng_macros/src/lib.rs
Normal file
84
bng_macros/src/lib.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
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")
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,58 +0,0 @@
|
|||
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 <n> | 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 <n>
|
||||
show the nth example from the list provided by --examples
|
||||
formats
|
||||
show a list of available formats for audio export
|
|
@ -1,2 +0,0 @@
|
|||
sine wave: sin(2*pi()*(442*2^((n+1)/N))*t)
|
||||
classic BeepComp length: 2^(2-log(2, l))*(60/T)
|
|
@ -1,30 +0,0 @@
|
|||
* 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)
|
|
@ -1,17 +0,0 @@
|
|||
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
|
|
@ -1,11 +0,0 @@
|
|||
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
|
BIN
doc/iconx4.png
BIN
doc/iconx4.png
Binary file not shown.
Before Width: | Height: | Size: 4.8 KiB |
|
@ -1,15 +0,0 @@
|
|||
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).
|
|
@ -1,114 +0,0 @@
|
|||
@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<char, f64> [-v, --variable]
|
||||
macros: HashMap<char, String> [-m, --macro]
|
||||
slopes: HashMap<String, (char, Instruction)> [-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
|
|
@ -1,13 +0,0 @@
|
|||
@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
|
|
@ -1,126 +0,0 @@
|
|||
@startuml "bliplib class diagram"
|
||||
|
||||
left to right direction
|
||||
skinparam linetype ortho
|
||||
|
||||
package std {
|
||||
interface From<T> {
|
||||
from(value: T): Self
|
||||
}
|
||||
}
|
||||
|
||||
package nom {
|
||||
interface Parser<I>
|
||||
}
|
||||
|
||||
package fasteval {
|
||||
enum Instruction
|
||||
}
|
||||
|
||||
package parser {
|
||||
interface TokenParser<I>
|
||||
struct ParserParametters<'n, 's, 'v, I: Input + Clone + nom::Compare<I>, C: Into<char>> {
|
||||
+notes: &'n [I]
|
||||
+slopes: &'s HashMap<I, VariableChange>
|
||||
+variables: &'v [C]
|
||||
}
|
||||
struct Parser<'i, 'n, 's, 'v, I: Input + Clone, C: Into<char>> {
|
||||
-input: &'i I
|
||||
-parametters: ParserParametters<'n, 's, 'v, I, C>
|
||||
+parse_all(): Result<Vec<Box<dyn Token>>, nom::Error<nom_locate::LocatedSpan<&'i I>>>
|
||||
}
|
||||
struct ParserBuilder<'i, 'n, 's, 'v, I: Input + Clone, C: Into<char>> {
|
||||
-input: Option<&'i I>
|
||||
-notes: Option<&'n [I]>
|
||||
-slopes: Option<&'s HashMap<I, VariableChange>>
|
||||
-variables: Option<&'v [C]>
|
||||
+input(self, input: &'i I): Self
|
||||
+notes(self, notes: &'n [I]): Self
|
||||
+slopes(self, slopes: &'s HashMap<I, VariableChange>): Self
|
||||
+variables(self, variables: &'v [C]): Self
|
||||
}
|
||||
}
|
||||
|
||||
package compiler {
|
||||
interface Token {
|
||||
-apply(&self, context: Context): Context
|
||||
}
|
||||
struct Silence {
|
||||
-parser<I>(): impl TokenParser<I>
|
||||
}
|
||||
struct Marker {
|
||||
-parser<I>(): impl TokenParser<I>
|
||||
}
|
||||
struct Note {
|
||||
+n: u8
|
||||
-parser<'a, I: nom::Input + nom::Compare<I> + Clone>(notes: &'a [I]): impl TokenParser<I>
|
||||
}
|
||||
struct VariableChange {
|
||||
+name: char,
|
||||
+change: Instruction
|
||||
-parser<'a, I, C: Into<char> + Clone>(variables: &'a [C]): impl TokenParser<I>
|
||||
}
|
||||
struct Loop {
|
||||
+times: usize
|
||||
+inner: Vec<Box<dyn Token>>
|
||||
-parser<'n, 's, 'v, I: Input + Clone + nom::Compare<I>, C: Into<char>>(parametters: ParserParametters<'n, 's, 'v, I, C>): impl TokenParser<I>
|
||||
}
|
||||
struct Tuplet {
|
||||
+inner: Vec<Box<dyn Token>>
|
||||
-parser<'n, 's, 'v, I: Input + Clone + nom::Compare<I>, C: Into<char>>(parametters: ParserParametters<'n, 's, 'v, I, C>): impl TokenParser<I>
|
||||
}
|
||||
struct Slope {
|
||||
+inner: Vec<Box<dyn Token>>
|
||||
+each_frame: VariableChange
|
||||
-parser<'n, 's, 'v, I: Input + Clone + nom::Compare<I>, C: Into<char>>(parametters: ParserParametters<'n, 's, 'v, I, C>): impl TokenParser<I>
|
||||
}
|
||||
struct Context {
|
||||
+result: Vec<f64>
|
||||
+variables: HashMap<char, f64>
|
||||
+instrument: Instruction
|
||||
+slopes: HashMap<char, Instruction>
|
||||
+current_length(&self): f64
|
||||
+render(&self, n: Option<u8>): Vec<f64>
|
||||
}
|
||||
}
|
||||
|
||||
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<ParserBuilder>" as from_parserbuilder
|
||||
interface "nom::Parser<nom_locate::LocatedSpan<&'i I>>" 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<nom::Error<nom_locate::LocatedSpan<&'i I>
|
||||
nomparser_locatedspan ..|> parser.TokenParser: for any T
|
||||
compiler.Slope --> compiler.VariableChange
|
||||
compiler.Token --> compiler.Context
|
||||
compiler.VariableChange --> Instruction
|
||||
Context --> Instruction
|
||||
|
||||
@enduml
|
|
@ -1,44 +0,0 @@
|
|||
@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
|
491
flamegraph.svg
491
flamegraph.svg
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 167 KiB |
51
poc/poc.yml
Normal file
51
poc/poc.yml
Normal file
|
@ -0,0 +1,51 @@
|
|||
# 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<ba
|
||||
;this is a comment (or lyrics whatever),
|
||||
C:CC
|
||||
(3deff)
|
||||
(deff)
|
||||
[ffe]
|
||||
{l 1-cos((PI*x)/2),acced}
|
||||
abbc!o5cc!v15feed!l4fedd!t60Gdd
|
||||
# rest: .
|
||||
# pizz.: '°
|
||||
# volume: +-
|
||||
# length: /\
|
||||
# octave: ><
|
||||
# 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
|
108
src/DOCS.md
108
src/DOCS.md
|
@ -1,108 +0,0 @@
|
|||
# bliplib <!-- omit from toc -->
|
||||
|
||||
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.
|
81
src/bng.rs
Normal file
81
src/bng.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let expr_str = String::new();
|
||||
serializer.serialize_str(&format!("{:#?}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Expression {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
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<Self, Self::Err> {
|
||||
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<String, Instrument>,
|
||||
channels: HashMap<String, Channel>,
|
||||
}
|
17
src/bng/instrument.rs
Normal file
17
src/bng/instrument.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
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<HashMap<String, f32>>,
|
||||
}
|
126
src/bng/score.rs
Normal file
126
src/bng/score.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
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<Atom>);
|
||||
|
||||
#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
|
||||
pub enum Atom {
|
||||
Note(u8),
|
||||
Rest,
|
||||
StartHere,
|
||||
Modifier(Modifier),
|
||||
QuickModifier(QuickModifier),
|
||||
Loop(NonZeroU8, Vec<Atom>),
|
||||
Tuple(Vec<Atom>),
|
||||
Slope(SlopeModifier, Instruction, Vec<Atom>),
|
||||
Comment,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
impl Serialize for Atom {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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,
|
||||
}
|
100
src/bng/score/de.rs
Normal file
100
src/bng/score/de.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
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<nom::error::Error<&str>>) -> 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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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::<D, _>(&sheet, flat_atom_parser(¬es))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_yml_str_space<'a, E>() -> impl Parser<&'a str, Vec<char>, 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<Atoms, <D as Deserializer<'de>>::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)
|
||||
}
|
52
src/bng/score/lex.rs
Normal file
52
src/bng/score/lex.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
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;
|
||||
}
|
87
src/bng/score/lex/lexer.rs
Normal file
87
src/bng/score/lex/lexer.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
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),
|
||||
),
|
||||
),
|
||||
))
|
||||
}
|
360
src/bng/score/lex/lexer/tests.rs
Normal file
360
src/bng/score/lex/lexer/tests.rs
Normal file
|
@ -0,0 +1,360 @@
|
|||
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))
|
||||
)
|
||||
}
|
||||
}
|
236
src/bng/score/utils.rs
Normal file
236
src/bng/score/utils.rs
Normal file
|
@ -0,0 +1,236 @@
|
|||
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<FlatAtom>) -> Result<Vec<Atom>, InflateError> {
|
||||
type Error = InflateError;
|
||||
let mut result = Vec::with_capacity(flat_atoms.len());
|
||||
let mut loop_stack: Vec<Vec<Atom>> = Vec::new();
|
||||
let mut tuple_stack: Vec<Vec<Atom>> = Vec::new();
|
||||
let mut slope_stack: Vec<Vec<Atom>> = Vec::new();
|
||||
let mut stack_history: Vec<Wrapper> = 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)
|
||||
}
|
206
src/bng/score/utils/tests.rs
Normal file
206
src/bng/score/utils/tests.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
#[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,
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
90
src/cli.rs
Normal file
90
src/cli.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
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<Self, Self::Err> {
|
||||
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<Self, Self::Err> {
|
||||
Ok(s.to_owned().into())
|
||||
}
|
||||
}
|
470
src/cli/cli.rs
470
src/cli/cli.rs
|
@ -1,470 +0,0 @@
|
|||
#![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<false>,
|
||||
/// Set available notes ("a,b,c" for example)
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
notes: Vec<String>,
|
||||
/// 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::<LetterString, Letter, Expression>)]
|
||||
#[getset(skip)]
|
||||
slopes: Vec<(LetterString, (Letter, Expression))>,
|
||||
}
|
||||
|
||||
impl PlayOpts {
|
||||
pub(super) fn variables(&self) -> impl Iterator<Item = (&char, &f64)> {
|
||||
self.variables.iter().map(|(c, f)| (c.as_ref(), f))
|
||||
}
|
||||
|
||||
pub(super) fn macros(&self) -> impl Iterator<Item = (&char, &String)> {
|
||||
self.macros.iter().map(|(c, s)| (c.as_ref(), s))
|
||||
}
|
||||
|
||||
pub(super) fn slopes(&self) -> impl Iterator<Item = (&String, (&char, &Expression))> {
|
||||
self.slopes
|
||||
.iter()
|
||||
.map(|(name, (v, e))| (name.as_ref(), (v.as_ref(), e)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<const CREATE_IF_NOT_EXISTS: bool> InputGroup<CREATE_IF_NOT_EXISTS>
|
||||
where
|
||||
Self: Clone,
|
||||
{
|
||||
pub(super) fn get(&self) -> Box<dyn Read> {
|
||||
self.input
|
||||
.as_ref()
|
||||
.map(|i| Box::new(i.clone().0) as Box<dyn Read>)
|
||||
.or(self
|
||||
.sheet_music_string
|
||||
.as_ref()
|
||||
.map(|s| Box::new(Cursor::new(s.clone())) as Box<dyn Read>))
|
||||
.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<Self, Self::Err> {
|
||||
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<Self, Self::Err> {
|
||||
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<Self, Self::Err> {
|
||||
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<const SEP: char, T, U>(
|
||||
s: &str,
|
||||
) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
|
||||
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<T, U1, U2>(
|
||||
s: &str,
|
||||
) -> Result<(T, (U1, U2)), Box<dyn Error + Send + Sync + 'static>>
|
||||
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<const CREATE_IF_NOT_EXISTS: bool> {
|
||||
/// Set the path to your sheet music file [default: stdin]
|
||||
input: Option<ClonableFile<CREATE_IF_NOT_EXISTS>>,
|
||||
/// Use this sheet music instead of reading from a file or stdin
|
||||
#[arg(short = 'c')]
|
||||
sheet_music_string: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ClonableFile<const CREATE_IF_NOT_EXISTS: bool>(File);
|
||||
|
||||
impl<const CREATE_IF_NOT_EXISTS: bool> Clone for ClonableFile<CREATE_IF_NOT_EXISTS> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.try_clone().expect("cloning file handle"))
|
||||
}
|
||||
}
|
||||
|
||||
impl<const CREATE_IF_NOT_EXISTS: bool> FromStr for ClonableFile<CREATE_IF_NOT_EXISTS> {
|
||||
type Err = io::Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match CREATE_IF_NOT_EXISTS {
|
||||
true => File::create(s),
|
||||
false => File::open(s),
|
||||
}
|
||||
.map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const CREATE_IF_NOT_EXISTS: bool> From<ClonableFile<CREATE_IF_NOT_EXISTS>> for File {
|
||||
fn from(value: ClonableFile<CREATE_IF_NOT_EXISTS>) -> 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<ClonableFile<true>>,
|
||||
}
|
||||
|
||||
#[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<AudioFormat, anyhow::Error> {
|
||||
fn mp3<'a>() -> impl P<
|
||||
LocatedSpan<&'a str>,
|
||||
Output = AudioFormat,
|
||||
Error = nom::error::Error<LocatedSpan<&'a str>>,
|
||||
> {
|
||||
preceded(
|
||||
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
|
||||
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<LocatedSpan<&'a str>>,
|
||||
> {
|
||||
preceded(
|
||||
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
|
||||
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<LocatedSpan<&'a str>>,
|
||||
> {
|
||||
preceded(
|
||||
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
|
||||
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<LocatedSpan<&'a str>>,
|
||||
> {
|
||||
alt((
|
||||
mp3::<'a>(),
|
||||
wav::<'a>(),
|
||||
flac::<'a>(),
|
||||
rest.map_res(|r: LocatedSpan<&'a str>| {
|
||||
Ok::<AudioFormat, nom::error::Error<LocatedSpan<&'a str>>>(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<u8> },
|
||||
/// 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,
|
||||
}
|
634
src/cli/main.rs
634
src/cli/main.rs
|
@ -1,634 +0,0 @@
|
|||
//! 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<f32> = 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<dyn Write> = output
|
||||
.map(File::from)
|
||||
.map(Box::new)
|
||||
.map(|b| b as Box<dyn Write>)
|
||||
.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::<i8>() as i32,
|
||||
16 => sample.to_sample::<i16>() as i32,
|
||||
24 => sample.to_sample::<I24>().inner(),
|
||||
_=> unimplemented!("sorry, the current implementation for the flac encoder doesn't support any other bitrate than 8, 16 or 24.")
|
||||
})
|
||||
.collect::<Vec<i32>>()
|
||||
.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<dyn Write> = output
|
||||
.map(File::from)
|
||||
.map(Box::new)
|
||||
.map(|b| b as Box<dyn Write>)
|
||||
.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::<FlushNoGap>(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<dyn Write> = output
|
||||
.map(File::from)
|
||||
.map(Box::new)
|
||||
.map(|b| b as Box<dyn Write>)
|
||||
.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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::ALaw,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::F32Be,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::F32Le,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::F64Be,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::F64Le,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::MuLaw,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::S8,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::S16Be,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::S16Le,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::S24Be,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::S24Le,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::S32Be,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::S32Le,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::U8,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::U16Be,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::U16Le,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::U24Be,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::U24Le,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::U32Be,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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<dyn Write>)
|
||||
.unwrap_or(Box::new(stdout())),
|
||||
raw_audio::pcm::U32Le,
|
||||
)
|
||||
.encode(
|
||||
fon::Audio::<fon::mono::Mono64>::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::<Vec<&'static str>>()
|
||||
).into(),
|
||||
};
|
||||
println!("* {}{}", Into::<&'static str>::into(discriminant), ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_and_compile(opts: &PlayOpts) -> anyhow::Result<Vec<f64>> {
|
||||
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::<Vec<_>>(),
|
||||
default_variables
|
||||
.iter()
|
||||
.map(|(v, _)| *v)
|
||||
.chain(opts.variables().map(|(v, _)| *v))
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
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::<String>()
|
||||
} 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::<HashMap<_, _>>(),
|
||||
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")
|
||||
}
|
619
src/compiler.rs
619
src/compiler.rs
|
@ -1,619 +0,0 @@
|
|||
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<Box<dyn Token>>);
|
||||
|
||||
impl IntoIterator for TokenVec {
|
||||
type Item = Box<dyn Token>;
|
||||
type IntoIter = <Vec<Box<(dyn Token + 'static)>> as IntoIterator>::IntoIter;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Token for Box<dyn Token> {
|
||||
fn apply(&self, context: Context) -> Result<Context, CompilerError> {
|
||||
self.as_ref().apply(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type for Box<dyn Token> {
|
||||
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<T> Type for T
|
||||
where
|
||||
T: Default + 'static,
|
||||
{
|
||||
fn type_id(&self) -> TypeId {
|
||||
<Self as Any>::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<Context, CompilerError>;
|
||||
}
|
||||
|
||||
#[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<Context, CompilerError> {
|
||||
debug!("⚡ {}", type_name::<Self>());
|
||||
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<Context, CompilerError> {
|
||||
debug!("⚡ {}", type_name::<Self>());
|
||||
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<Context, CompilerError> {
|
||||
debug!("⚡ {}", type_name::<Self>());
|
||||
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<VariableChange> for VariableChange {
|
||||
fn as_ref(&self) -> &VariableChange {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Token for VariableChange {
|
||||
fn apply(&self, mut context: Context) -> Result<Context, CompilerError> {
|
||||
debug!("⚡ {}", type_name::<Self>());
|
||||
*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<Context, CompilerError> {
|
||||
debug!("⚡ {}", type_name::<Self>());
|
||||
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<Context, CompilerError> {
|
||||
debug!("⚡ {}", type_name::<Self>());
|
||||
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<Context, CompilerError> {
|
||||
debug!("⚡ {}", type_name::<Self>());
|
||||
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<Expression> 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<Self, Self::Err> {
|
||||
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<f64>,
|
||||
/// Set of variables used for every [`Expression`] evaluation.
|
||||
#[new(into_iter = "(String, f64)")]
|
||||
pub variables: BTreeMap<String, f64>,
|
||||
/// 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<str> + Into<String>) -> 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<str> + Into<String>,
|
||||
) -> 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<String>,
|
||||
) -> Result<Vec<&Expression>, 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, CompilerError> {
|
||||
*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<f64, fasteval::Error> {
|
||||
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<f64, fasteval::Error> {
|
||||
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<u8>) -> Result<(Self, Vec<f64>), 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<f64> {
|
||||
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<Self, CompilerError> {
|
||||
token.apply(self.0).map(Into::into)
|
||||
}
|
||||
fn apply_all(
|
||||
self,
|
||||
tokens: impl IntoIterator<Item = impl Token>,
|
||||
) -> Result<Self, CompilerError> {
|
||||
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<Item = impl Token>,
|
||||
) -> Result<Vec<f64>, 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(())
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
#![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;
|
53
src/main.rs
Normal file
53
src/main.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
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::<String, Channel>::from([(
|
||||
"melody".to_string(),
|
||||
Channel::new("sine".to_string(), vec![].into()),
|
||||
)]),
|
||||
)
|
||||
}
|
911
src/parser.rs
911
src/parser.rs
|
@ -1,911 +0,0 @@
|
|||
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<I, O, E = LocatedVerboseError<I>> = nom::IResult<I, O, E>;
|
||||
|
||||
/// A parser wrapper for `nom` 8 which adds a context to the error.
|
||||
#[derive(new)]
|
||||
pub struct VerboseParser<P: nom::Parser<I, Error = LocatedVerboseError<I>>, I> {
|
||||
parser: P,
|
||||
context: Cow<'static, str>,
|
||||
#[new(default)]
|
||||
phantom: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<I> ParseError<I> for LocatedVerboseError<I> {
|
||||
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<I, P: nom::Parser<I, Error = LocatedVerboseError<I>>> nom::Parser<I> for VerboseParser<P, I> {
|
||||
type Output = P::Output;
|
||||
|
||||
type Error = LocatedVerboseError<I>;
|
||||
|
||||
fn process<OM: nom::OutputMode>(
|
||||
&mut self,
|
||||
input: I,
|
||||
) -> nom::PResult<OM, I, Self::Output, Self::Error> {
|
||||
use nom::Err::*;
|
||||
use nom::Mode;
|
||||
|
||||
let stack_verbose_error = |e: LocatedVerboseError<I>| -> LocatedVerboseError<I> {
|
||||
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::<OM>(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<I> {
|
||||
/// Error location (I is the input type)
|
||||
pub location: I,
|
||||
/// Error description / context
|
||||
pub error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
/// Expect the parser to succeed and if it doesn't, add a context to the error.
|
||||
pub fn expect<P, I>(
|
||||
parser: P,
|
||||
error_message: impl Into<Cow<'static, str>>,
|
||||
) -> impl nom::Parser<I, Output = P::Output, Error = LocatedVerboseError<I>>
|
||||
where
|
||||
P: nom::Parser<I, Error = LocatedVerboseError<I>>,
|
||||
{
|
||||
VerboseParser::new(parser, error_message.into())
|
||||
}
|
||||
|
||||
impl<I> FromExternalError<I, anyhow::Error> for LocatedVerboseError<I> {
|
||||
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<N, NS, S, SS, SV, V>
|
||||
where
|
||||
N: AsRef<[NS]>,
|
||||
NS: AsRef<str>,
|
||||
S: IntoIterator<Item = (SS, SV)>,
|
||||
SS: AsRef<str>,
|
||||
SV: Borrow<VariableChange>,
|
||||
V: AsRef<[char]>,
|
||||
{
|
||||
notes: N,
|
||||
slopes: S,
|
||||
variables: V,
|
||||
phantom: PhantomData<(NS, SS)>,
|
||||
}
|
||||
|
||||
impl<'a, 'p, N, NS, S, SS, SV, V> Parser<N, NS, S, SS, SV, V>
|
||||
where
|
||||
N: AsRef<[NS]>,
|
||||
NS: AsRef<str>,
|
||||
S: IntoIterator<Item = (SS, SV)> + Clone,
|
||||
SS: AsRef<str>,
|
||||
SV: Borrow<VariableChange>,
|
||||
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<TokenVec, nom::Err<LocatedVerboseError<nom_locate::LocatedSpan<&'a str>>>> {
|
||||
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<N, NS, S, SS, SV, V>,
|
||||
) -> impl nom::Parser<
|
||||
LocatedSpan<&'a str>,
|
||||
Output = TokenVec,
|
||||
Error = LocatedVerboseError<LocatedSpan<&'a str>>,
|
||||
>
|
||||
where
|
||||
N: AsRef<[NS]>,
|
||||
NS: AsRef<str>,
|
||||
S: IntoIterator<Item = (SS, SV)> + Clone,
|
||||
SS: AsRef<str>,
|
||||
SV: Borrow<VariableChange>,
|
||||
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<dyn Token + 'a> {
|
||||
Box::new(token)
|
||||
}
|
||||
|
||||
impl Silence {
|
||||
fn parser<I>() -> impl nom::Parser<I, Output = Self, Error = LocatedVerboseError<I>>
|
||||
where
|
||||
I: Input,
|
||||
<I as Input>::Item: AsChar,
|
||||
{
|
||||
trace!("making the {} parser", type_name::<Self>());
|
||||
value(Self, char('.'))
|
||||
}
|
||||
}
|
||||
|
||||
impl Marker {
|
||||
fn parser<I>() -> impl nom::Parser<I, Output = Self, Error = LocatedVerboseError<I>>
|
||||
where
|
||||
I: Input,
|
||||
<I as Input>::Item: AsChar,
|
||||
{
|
||||
trace!("making the {} parser", type_name::<Self>());
|
||||
value(Marker, char('%'))
|
||||
}
|
||||
}
|
||||
|
||||
impl Note {
|
||||
fn parser<'a, N, NS, I>(
|
||||
notes: N,
|
||||
) -> impl nom::Parser<I, Output = Self, Error = LocatedVerboseError<I>> + 'a
|
||||
where
|
||||
N: IntoIterator<Item = NS>,
|
||||
NS: AsRef<str>,
|
||||
I: Input + for<'z> Compare<&'z str>,
|
||||
{
|
||||
trace!("making the {} parser", type_name::<Self>());
|
||||
let notes = {
|
||||
let mut sorted = notes
|
||||
.into_iter()
|
||||
.map(|s| s.as_ref().to_string())
|
||||
.enumerate()
|
||||
.collect::<Vec<_>>();
|
||||
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<Box<dyn Fn(I) -> IResult<I, Self>>> = 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<dyn Fn(I) -> IResult<I, Self>>
|
||||
})
|
||||
.collect();
|
||||
alt(parsers.as_mut_slice()).parse(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VariableChange {
|
||||
fn parser<I, V>(variables: V) -> impl Fn(I) -> IResult<I, Self>
|
||||
where
|
||||
I: Input + AsRef<str> + Copy,
|
||||
<I as Input>::Item: AsChar,
|
||||
V: AsRef<[char]>,
|
||||
{
|
||||
trace!("making the {} parser", type_name::<Self>());
|
||||
let variables_string = variables.as_ref().iter().collect::<String>();
|
||||
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::<Vec<_>>()
|
||||
),
|
||||
)
|
||||
.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<N, NS, S, SS, SV, V>,
|
||||
) -> impl Fn(LocatedSpan<&'a str>) -> IResult<LocatedSpan<&'a str>, Self>
|
||||
where
|
||||
N: AsRef<[NS]>,
|
||||
NS: AsRef<str>,
|
||||
S: IntoIterator<Item = (SS, SV)> + Clone,
|
||||
SS: AsRef<str>,
|
||||
SV: Borrow<VariableChange>,
|
||||
V: AsRef<[char]>,
|
||||
'p: 'a,
|
||||
{
|
||||
trace!("making the {} parser", type_name::<Self>());
|
||||
move |input| {
|
||||
delimited(
|
||||
char('('),
|
||||
opt(alt((
|
||||
usize.map(LoopCount::Litteral),
|
||||
one_of(
|
||||
parser
|
||||
.variables
|
||||
.as_ref()
|
||||
.iter()
|
||||
.collect::<String>()
|
||||
.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<N, NS, S, SS, SV, V>,
|
||||
) -> impl Fn(LocatedSpan<&'a str>) -> IResult<LocatedSpan<&'a str>, Self>
|
||||
where
|
||||
N: AsRef<[NS]>,
|
||||
NS: AsRef<str>,
|
||||
S: IntoIterator<Item = (SS, SV)> + Clone,
|
||||
SS: AsRef<str>,
|
||||
SV: Borrow<VariableChange>,
|
||||
V: AsRef<[char]>,
|
||||
'p: 'a,
|
||||
{
|
||||
trace!("making the {} parser", type_name::<Self>());
|
||||
|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<N, NS, S, SS, SV, V>,
|
||||
) -> impl Fn(LocatedSpan<&'a str>) -> IResult<LocatedSpan<&'a str>, Self>
|
||||
where
|
||||
N: AsRef<[NS]>,
|
||||
NS: AsRef<str>,
|
||||
S: IntoIterator<Item = (SS, SV)> + Clone,
|
||||
SS: AsRef<str>,
|
||||
SV: Borrow<VariableChange>,
|
||||
V: AsRef<[char]>,
|
||||
'p: 'a,
|
||||
{
|
||||
trace!("making the {} parser", type_name::<Self>());
|
||||
move |input| {
|
||||
let slopes = {
|
||||
let mut vec = parser
|
||||
.slopes
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(s1, s2)| (s1.as_ref().to_string(), s2.borrow().clone()))
|
||||
.collect::<Vec<(String, VariableChange)>>();
|
||||
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<LocatedSpan<&'a str>, VariableChange>,
|
||||
>
|
||||
})
|
||||
.collect::<Vec<
|
||||
Box<
|
||||
dyn Fn(
|
||||
LocatedSpan<&'a str>,
|
||||
)
|
||||
-> IResult<LocatedSpan<&'a str>, 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::<Vec<_>>()
|
||||
)))
|
||||
.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<I, Expression>
|
||||
where
|
||||
I: Input + AsRef<str> + Copy,
|
||||
V: IntoIterator<Item = C>,
|
||||
C: Borrow<char>,
|
||||
{
|
||||
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::<Expression>()
|
||||
.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<F, I, O>(cond: F) -> impl FnMut(I) -> IResult<I, O, LocatedVerboseError<I>>
|
||||
where
|
||||
I: Input + Copy,
|
||||
F: Fn(I) -> Option<O>,
|
||||
{
|
||||
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>,
|
||||
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::<String, VariableChange>::default(),
|
||||
['n'],
|
||||
);
|
||||
fn parser_builder<'a, 'p, N, NS, S, SS, SV, V>(
|
||||
parser: &'p Parser<N, NS, S, SS, SV, V>,
|
||||
) -> impl Fn(LocatedSpan<&'a str>) -> IResult<LocatedSpan<&'a str>, Loop>
|
||||
where
|
||||
N: AsRef<[NS]>,
|
||||
NS: AsRef<str>,
|
||||
S: IntoIterator<Item = (SS, SV)> + Clone,
|
||||
SS: AsRef<str>,
|
||||
SV: Borrow<VariableChange>,
|
||||
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::<String, VariableChange>::default(),
|
||||
['n'],
|
||||
);
|
||||
fn parser_builder<'a, 'p, N, NS, S, SS, SV, V>(
|
||||
parser: &'p Parser<N, NS, S, SS, SV, V>,
|
||||
) -> impl Fn(LocatedSpan<&'a str>) -> IResult<LocatedSpan<&'a str>, Tuplet>
|
||||
where
|
||||
N: AsRef<[NS]>,
|
||||
NS: AsRef<str>,
|
||||
S: IntoIterator<Item = (SS, SV)> + Clone,
|
||||
SS: AsRef<str>,
|
||||
SV: Borrow<VariableChange>,
|
||||
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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue