Compare commits

...
Sign in to create a new pull request.

133 commits

Author SHA1 Message Date
brevalferrari
bbf182f762
make Expression Display using original string 2025-06-19 14:12:14 +02:00
brevalferrari
b7933c966e blip report (finished) 2025-06-18 17:56:52 +02:00
brevalferrari
6aa665dd85 fix blip install (missing files) 2025-06-18 17:40:43 +02:00
brevalferrari
742cd8b2aa blip report (unfinished) 2025-06-17 12:50:49 +02:00
brevalferrari
8873051d91 change subtitle cause it's not the end of my studies 2025-06-14 14:46:27 +02:00
Breval Ferrari
6c73620c7b
finish presentation 2025-06-14 11:54:41 +02:00
Breval Ferrari
76436e102f
icon was too big 2025-06-14 11:52:30 +02:00
Breval Ferrari
a6df18cd68
icon in README 2025-06-14 11:49:45 +02:00
Breval Ferrari
c7e522bd4c
icon for README 2025-06-14 11:49:27 +02:00
Breval Ferrari
726d0ee63e
really smaller logo 2025-06-14 11:48:56 +02:00
Breval Ferrari
e9a2ac7041
remove logo text 2025-06-14 11:11:24 +02:00
Breval Ferrari
a6f6009f21
smaller logo 2025-06-14 09:40:03 +02:00
brevalferrari
9b23a80894 project french presentation ... 2025-06-14 08:52:04 +02:00
brevalferrari
20bd95c364 project french presentation 2025-06-13 20:19:33 +02:00
brevalferrari
c33ceef4db logo 2025-06-13 20:16:21 +02:00
brevalferrari
b7cd83b2b5 cahier des charges 2025-06-12 15:29:38 +02:00
brevalferrari
bd9e239c77 flac & raw export 2025-06-10 16:12:19 +02:00
brevalferrari
b0444e13d4 complete doc 2025-06-09 19:08:30 +02:00
brevalferrari
4f39e98e8b new version for docs 2025-06-09 11:24:04 +02:00
brevalferrari
2eb06b32d0 proper wav + mp3 export 2025-06-09 01:11:50 +02:00
brevalferrari
7e5120586f rust-analyzer settings 2025-06-07 20:19:44 +02:00
brevalferrari
9a5e237733 complete cli memo menu 2025-06-07 20:04:22 +02:00
brevalferrari
2f5d9aed32 docs.rs index 2025-06-07 18:18:40 +02:00
brevalferrari
45d384cb85 add cargo install command 2025-06-07 18:16:37 +02:00
brevalferrari
df342893ee fix rustfmt 2025-06-07 17:38:00 +02:00
brevalferrari
5e38b15f93 more detail for language design 2025-06-07 17:37:33 +02:00
brevalferrari
e45053eebe blip as example for docs.rs example scraping 2025-06-07 17:37:15 +02:00
brevalferrari
674f7e49d2 log err on NaN instead of panicking 2025-06-07 15:05:07 +02:00
brevalferrari
49f02fd617 meant 0.2.0 since it breaks a lil 2025-06-07 13:04:04 +02:00
brevalferrari
e464ccacfe v0.1.1 2025-06-07 13:02:21 +02:00
brevalferrari
09e07a1c8c better compiler errors 2025-06-07 13:01:38 +02:00
brevalferrari
bbccf46d47 deny empty tuplets 2025-06-07 12:58:36 +02:00
brevalferrari
7bf1f0c6b1 v0.1 yay!! 2025-06-07 02:24:06 +02:00
brevalferrari
b918cbeaae change wrapper higher expects to context-like 2025-06-07 02:21:40 +02:00
brevalferrari
47af63c73c fix cli slope name downcasting panic 2025-06-07 02:19:19 +02:00
brevalferrari
8ce0676e7d way better parser errors 2025-06-07 02:18:46 +02:00
brevalferrari
f5448e0e78 new command: check (like cargo) 2025-06-06 01:34:28 +02:00
brevalferrari
b03b0ef5ee parser cuts 2025-06-06 01:34:05 +02:00
brevalferrari
7460a14bc1 better parsing error reporting 2025-06-06 01:21:03 +02:00
brevalferrari
f55a8c7e47 more log 2025-06-06 01:04:44 +02:00
brevalferrari
41aeef83d4 fix cli variable override 2025-06-06 00:57:25 +02:00
brevalferrari
f7d5b57b66 fix default instrument 2025-06-06 00:56:55 +02:00
brevalferrari
5651af2ac5 fix audio 2025-06-05 19:46:23 +02:00
brevalferrari
6a6d6c1270 better NAN prevention 2025-06-05 19:37:19 +02:00
brevalferrari
52cbb88a6d better logs 2025-06-05 18:45:13 +02:00
brevalferrari
03ded1fb74 assert not to have any NaN sample 2025-06-05 18:33:23 +02:00
brevalferrari
c6cde8ffbf export mode 2025-06-05 17:47:28 +02:00
brevalferrari
da38edbfd1 create export file if not exists 2025-06-05 17:39:37 +02:00
brevalferrari
e9fcec8b32 faster 2025-06-05 01:39:05 +02:00
brevalferrari
8024b18850 samply 2025-06-05 00:22:30 +02:00
brevalferrari
fb9b57f753 fixed missing variable & length slope, flamegraph 2025-06-04 22:07:18 +02:00
brevalferrari
17525717e8 clippy 2025-06-04 12:33:58 +02:00
brevalferrari
788eebaea5 set expr failure to trace level 2025-06-04 12:32:06 +02:00
brevalferrari
3fd3dafc0d fix notes cli parsing 2025-06-04 12:31:50 +02:00
brevalferrari
1dc5db9bd2 pretty debug cli 2025-06-04 12:25:36 +02:00
brevalferrari
9ff44f5182 bin log 2025-06-04 12:24:39 +02:00
brevalferrari
0ccc81551a log 2025-06-04 12:10:13 +02:00
brevalferrari
20c7db953e fix tests 2025-06-02 23:30:07 +02:00
brevalferrari
c395b2ae38 remove warnings 2025-06-02 23:28:47 +02:00
brevalferrari
25f4eb1b13 generalize default variables 2025-06-02 23:27:51 +02:00
brevalferrari
bd40b124b0 don't allow dead code anymore 2025-06-02 23:26:16 +02:00
brevalferrari
bb8e150a30 simple playback feature 2025-06-02 21:00:54 +02:00
Breval Ferrari
3168370a37
simplify bliplib API 2025-06-01 23:32:18 +02:00
Breval Ferrari
c0e3478ae0
split cli, cli utilities 2025-05-31 11:36:18 +02:00
Breval Ferrari
1074adb9e7
remove test version of Context.eval_with() 2025-05-30 10:45:39 +02:00
Breval Ferrari
f39306e3d6
simplify instrument making by restricting it to range -1..1 2025-05-30 10:44:28 +02:00
brevalferrari
11788900b9 fix 2025-05-29 00:33:19 +02:00
brevalferrari
9d02a2faaf fix precise note length, add test 2025-05-28 20:56:48 +02:00
brevalferrari
de97e43b63 really replace π 2025-05-28 16:59:31 +02:00
brevalferrari
c96bce8b37 replace π, more tests 2025-05-28 16:58:58 +02:00
brevalferrari
478ce49d8d replace total note length variable from l to L, swap fasteval log_2 with log(2,...), fix silence rendering, first compiler test 2025-05-28 16:44:32 +02:00
brevalferrari
ce5a8760c1 clippy 2025-05-28 13:29:06 +02:00
brevalferrari
73fb1d52b5 turn slopes map into a vec to have multiple slopes for same variable, struct Compiler 2025-05-28 12:54:01 +02:00
Breval Ferrari
7852f2d5c5 add the 2025-05-23 20:33:04 +02:00
Breval Ferrari
b6c2681b07 prepare Cargo.toml for crates.io upload 2025-05-23 20:30:47 +02:00
Breval Ferrari
4102b93686 rest of Token.apply impls, Default for everyone 2025-05-23 20:06:28 +02:00
Breval Ferrari
4bc2e2b8ee implement Clone for Expression (really works) 2025-05-23 15:25:45 +02:00
Breval Ferrari
7e831c40ac first Token.apply() impls, context methods 2025-05-23 15:12:12 +02:00
Breval Ferrari
69c2869388 slope parser test, fix for longest string matching first 2025-05-21 18:31:31 +02:00
Breval Ferrari
fb39301b31 tuplet parser 2025-05-21 18:15:48 +02:00
Breval Ferrari
28bd727090 move input to parsing time, loop parser test 2025-05-21 18:11:39 +02:00
Breval Ferrari
1044ad9b1d clippy 2025-05-21 17:17:00 +02:00
Breval Ferrari
d9eab53858 fix expression parser, make tests more verbose 2025-05-21 17:16:00 +02:00
Breval Ferrari
92a4dd02eb expression parser rewrite with helper (failing tests) 2025-05-21 16:07:04 +02:00
Breval Ferrari
22771168d2 note test, make Note parser generic over notes and match longer strings first 2025-05-21 12:22:58 +02:00
Breval Ferrari
fa49625f7f test setup, silence & expression tests 2025-05-21 12:00:35 +02:00
Breval Ferrari
16f79c302a make parsers generic over I again for tests and other things 2025-05-21 11:20:39 +02:00
Breval Ferrari
8563ee3de2 fix lifetime issues 2025-05-20 23:18:26 +02:00
Breval Ferrari
8387aa61bd tuplet and slope parser (lifetime issue) 2025-05-20 22:42:00 +02:00
Breval Ferrari
cfdcc50973 loop, clippy :D 2025-05-20 18:58:39 +02:00
Breval Ferrari
6cb3633cd1 parser: add possible space or comment 2025-05-20 18:28:15 +02:00
Breval Ferrari
4fa49e2181 first four parsers 2025-05-20 16:45:30 +02:00
Breval Ferrari
0240602c19 CLI export opts 2025-05-19 17:52:29 +02:00
Breval Ferrari
42e52155e1 cli: play 2025-05-16 15:25:18 +02:00
Breval Ferrari
283c6a7dd6 shorten cli entry command derive 2025-05-15 15:59:49 +02:00
Breval Ferrari
69b6bf9d0f complete lib structure 2025-05-15 15:55:03 +02:00
Breval Ferrari
4b2291b7a1 update license badge 2025-05-14 13:03:54 +02:00
Breval Ferrari
0269e8831c basic lib & cli structure, uploaded to crates.io 2025-05-14 13:00:57 +02:00
Breval Ferrari
9b7a85da99 move tokens to compiler module 2025-05-13 23:21:56 +02:00
Breval Ferrari
faac093116 activity diagram for token application 2025-05-13 23:05:23 +02:00
Breval Ferrari
f4e6bd6e3d switch variablechange string to fasteval instruction, add context structure for compiling 2025-05-13 23:05:00 +02:00
Breval Ferrari
227d6bcec3 CLI UML 2025-05-12 20:23:55 +02:00
Breval Ferrari
7db0d90a1d typo 2025-05-12 16:29:55 +02:00
Breval Ferrari
9268f46212 typo 2025-05-12 16:20:40 +02:00
Breval Ferrari
1ef7e89ae0 switch signal expression option to -i / --instrument 2025-05-12 16:15:59 +02:00
Breval Ferrari
56b0e234c7 switch to simpler cargo-like modes 2025-05-12 16:01:17 +02:00
Breval Ferrari
9369d19ef1 start cli uml 2025-05-08 13:05:51 -04:00
Breval Ferrari
b432f5adce add acronym meaning 2025-05-07 16:07:52 -04:00
Breval Ferrari
6c15d52d3b lib UML complete (I think) 2025-05-07 15:49:47 -04:00
Breval Ferrari
f09e00cdba wrong way: proxy struct for opaque type alias 2025-05-07 14:28:27 -04:00
Breval Ferrari
8caeacc679 gitignore for plantuml exports 2025-05-06 19:42:56 -04:00
Breval Ferrari
52922efbe5 lib class diagram part 1 2025-05-06 19:42:16 -04:00
Breval Ferrari
cc1038ab89 component diagram 2025-05-06 19:41:52 -04:00
Breval Ferrari
40f3f23525
cleanup 2025-05-06 09:27:19 -04:00
Breval Ferrari
3fdb495a3b
language design 2025-05-01 20:21:03 -04:00
Breval Ferrari
574460a098
add l default variable and note length expression with classic MML
default
2025-05-01 19:57:17 -04:00
Breval Ferrari
f865d0a4aa
fix typo in default instrument 2025-05-01 19:38:48 -04:00
Breval Ferrari
322422bf19
new CLI design 2025-05-01 16:03:52 -04:00
Breval Ferrari
d4c8b13376
change name and license, update README 2025-05-01 12:31:32 -04:00
d27e7825de
cli UML 2024-11-28 20:50:12 -05:00
e40f6f48a1
list subcommand support 2024-11-17 01:51:36 -05:00
efceea0705
update deps 2024-11-17 01:51:06 -05:00
36b0fe394c
add Scl / Kbm parsing 2024-11-13 16:18:56 -05:00
219d3e88fb
replace f32 w f64 (supported by fasteval 2024-11-11 15:14:23 -05:00
6dffde0cb7
fix everything!!!! :D 2024-11-10 00:50:04 -05:00
4bb38e0f3e
fix tests, weird nested comment bug 2024-11-09 23:41:46 -05:00
f0be539c69
no more type dep cycle (resolved with lazy_static) 2024-11-09 22:37:01 -05:00
1c91b113fb
no more FlatAtoms 2024-11-09 19:37:12 -05:00
9c910b8076
divide atom parser into sub-parsers with context 2024-11-09 18:58:28 -05:00
900b8b198e
move things around, add curry 2024-11-09 18:39:28 -05:00
315238f14d
root parser 2024-11-09 17:59:49 -05:00
bdb42ac846
verbose error support 2024-11-09 17:18:25 -05:00
a2566f1be3
context & function names without another Parse trait 2024-11-09 12:53:28 -05:00
41 changed files with 3764 additions and 1618 deletions

21
.gitignore vendored
View file

@ -20,3 +20,24 @@ Cargo.lock
# and can be added to the global gitignore or merged into this file. For a more nuclear # 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. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
out/
# Flamegraph
*.old
*.data
# Samply
*.json.gz
# audio files
*.mp3
*.raw
*.wav
*.flac
# VSCode
.vscode/settings.json
# LibreOffice
*\#

View file

@ -1,34 +1,59 @@
[package] [package]
name = "bng" name = "bliplib"
version = "0.1.0" version = "0.2.5"
edition = "2021" 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"
[dependencies] [dependencies]
amplify = "4.7.0" anyhow = { version = "1.0" }
anyhow = "1.0" cfg-if = "1"
cfg-if = "1.0.0" clap = { version = "4.5.39", features = ["derive"], optional = true }
clap = { version = "4.5", features = ["derive"] } derive-new = "0.7"
derived-deref = "2.1.0" derive_builder = "0.20"
fasteval = "0.2.4" derive_wrapper = "0.1"
nom = "7.1.3" fasteval = "0.2"
serde = { version = "1.0.209", features = ["derive"] } flacenc = { version = "0.4", optional = true }
serde_yml = "0.0.12" getset = "0.1.5"
splines = "4.3.1" hound = { version = "3.5", optional = true }
strum = { version = "0.26", features = ["derive"] } lazy_static = "1.5"
strum_macros = "0.26" mp3lame-encoder = { version = "0.2", features = ["std"], optional = true }
tinyaudio = { version = "0.1", optional = true } nom = "8.0"
bng_macros = { path = "bng_macros" } nom_locate = "5.0"
const_format = "0.2.33" raw_audio = { version = "0.0", optional = true }
thiserror = "1.0.64" strum = { version = "0.27", features = ["derive"] }
derive-new = "0.7.0" thiserror = "2.0"
rodio = { version = "0.20", default-features = false, optional = true }
dasp_sample = { version = "0", optional = true }
log = "0"
env_logger = { version = "0", optional = true }
fon = "0.5"
[features] [features]
default = ["play", "save"] default = ["all-formats"]
play = ["dep:tinyaudio"] bin = ["clap", "rodio", "dasp_sample", "env_logger"]
save = [] all-formats = ["mp3", "wav", "flac", "raw"]
mp3 = ["mp3lame-encoder"]
wav = ["hound"]
flac = ["flacenc"]
raw = ["raw_audio"]
[workspace] [[bin]]
members = ["bng_macros"] name = "blip"
path = "src/cli/main.rs"
required-features = ["bin"]
include = ["doc/language-design", "doc/fasteval/*"]
[[example]]
name = "blip"
path = "src/cli/main.rs"
required-features = ["bin"]
[lints.rust] [lints.rust]
unused = "allow" missing_docs = "warn"

35
LICENSE
View file

@ -1,34 +1,7 @@
NUCLEAR WASTE SOFTWARE LICENSE V1.0 Copyright © 2025 Breval Ferrari
Copyright 2024 Breval Ferrari Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
This software license is a message... and part of a system of messages... The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
This software is best shunned and left unexecuted... however, permission is 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.
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.

View file

@ -1,7 +1,20 @@
# bng <img src="https://gitdab.com/breval/blip/raw/branch/main/doc/iconx4.png" alt="bppt logo" align="right">
[![Crates.io](https://img.shields.io/crates/v/bng.svg)](https://crates.io/crates/bng)
[![Docs.rs](https://docs.rs/bng/badge.svg)](https://docs.rs/bng)
[![License: MIT](https://img.shields.io/badge/License-NWSL-yellow.svg)](https://gist.github.com/DavidBuchanan314/35cb9f8a2f754b9a03a74bed19575661)
Bleeperpreter New Gen - the smarter 'preter! [![Crates.io](https://img.shields.io/crates/v/bliplib.svg)](https://crates.io/crates/bliplib)
A better [Bleeperpreter](https://github.com/p6nj/bleeperpreter) from scratch. [![Docs.rs](https://docs.rs/bliplib/badge.svg)](https://docs.rs/bliplib)
[![License: MIT](https://img.shields.io/crates/l/bliplib)](LICENSE)
# blip
A better and more flexible [Bleeperpreter](https://github.com/p6nj/bleeperpreter) from scratch.
| | the |
| :--- | :-------------- |
| B | Bizarre |
| L | Language for |
| I | Intermodulation |
| P | Programming |
```sh
cargo install bliplib --features bin
```

View file

@ -1,12 +0,0 @@
[package]
name = "bng_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }

View file

@ -1,84 +0,0 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Ident};
#[proc_macro_derive(SlopeModifierParser)]
pub fn slope_modifier_parser(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_slope_modifier_parser(ast)
}
#[proc_macro_derive(QuickModifierParser)]
pub fn quick_modifier_parser(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_quick_modifier_parser(ast)
}
fn impl_slope_modifier_parser(ast: DeriveInput) -> TokenStream {
let name = &ast.ident;
if let Data::Enum(DataEnum { variants, .. }) = ast.data {
let match_arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;
let const_name = Ident::new(&variant.ident.to_string().to_uppercase(),Span::call_site());
quote! {
nom::combinator::value(#name::#variant_name, nom::character::complete::char(SlopeModifier::#const_name))
}
});
quote! {
impl lex::lexer::Parse for #name {
fn parse(input: &str) -> nom::IResult<&str, #name> {
nom::branch::alt((
#(#match_arms),*
))(input)
}
}
}
.into()
} else {
panic!("this macro only works on enums")
}
}
fn impl_quick_modifier_parser(ast: DeriveInput) -> TokenStream {
let name = &ast.ident;
if let Data::Enum(DataEnum { variants, .. }) = ast.data {
let match_arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;
let const_name =
Ident::new(&variant.ident.to_string().to_uppercase(), Span::call_site());
quote! {
nom::combinator::map(
nom::branch::alt(
(
nom::combinator::value(
lex::UP,
nom::character::complete::char(#name::#const_name.0)
),
nom::combinator::value(
lex::DOWN,
nom::character::complete::char(#name::#const_name.1)
)
)
),
#name::#variant_name
)
}
});
quote! {
impl lex::lexer::Parse for #name {
fn parse(input: &str) -> nom::IResult<&str, #name> {
nom::branch::alt((
#(#match_arms),*
))(input)
}
}
}
.into()
} else {
panic!("this macro only works on enums")
}
}

BIN
doc/BLIP logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
doc/BLIP presentation.odp Normal file

Binary file not shown.

BIN
doc/BLIP report.odt Normal file

Binary file not shown.

BIN
doc/BLIP report.pdf Normal file

Binary file not shown.

BIN
doc/Cahier des charges.pdf Normal file

Binary file not shown.

58
doc/cli-design.txt Normal file
View file

@ -0,0 +1,58 @@
A flexible music programming language powered by math.
usage:
blip [play [FILENAME | -c SHEET_MUSIC] [-i EXPR] [-l EXPR] -n NOTE[,...] [-v CHAR=VALUE] | export [FILENAME | -c SHEET_MUSIC] [-i EXPR] [-l EXPR] -n NOTE[,...] [-v CHAR=VALUE] [-f FORMAT] [-o FILENAME] | help [syntax | examples | example <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

2
doc/examples.txt Normal file
View file

@ -0,0 +1,2 @@
sine wave: sin(2*pi()*(442*2^((n+1)/N))*t)
classic BeepComp length: 2^(2-log(2, l))*(60/T)

View file

@ -0,0 +1,30 @@
* print(...strings and values...) -- Prints to stderr. Very useful to 'probe' an expression.
Evaluates to the last value.
Example: `print("x is", x, "and y is", y)`
Example: `x + print("y:", y) + z == x+y+z`
* log(base=10, val) -- Logarithm with optional 'base' as first argument.
If not provided, 'base' defaults to '10'.
Example: `log(100) + log(e(), 100)`
* e() -- Euler's number (2.718281828459045)
* pi() -- π (3.141592653589793)
* int(val)
* ceil(val)
* floor(val)
* round(modulus=1, val) -- Round with optional 'modulus' as first argument.
Example: `round(1.23456) == 1 && round(0.001, 1.23456) == 1.235`
* abs(val)
* sign(val)
* min(val, ...) -- Example: `min(1, -2, 3, -4) == -4`
* max(val, ...) -- Example: `max(1, -2, 3, -4) == 3`
* sin(radians) * asin(val)
* cos(radians) * acos(val)
* tan(radians) * atan(val)
* sinh(val) * asinh(val)
* cosh(val) * acosh(val)
* tanh(val) * atanh(val)

17
doc/fasteval/literals.txt Normal file
View file

@ -0,0 +1,17 @@
Several numeric formats are supported:
Integers: 1, 2, 10, 100, 1001
Decimals: 1.0, 1.23456, 0.000001
Exponents: 1e3, 1E3, 1e-3, 1E-3, 1.2345e100
Suffix:
1.23p = 0.00000000000123
1.23n = 0.00000000123
1.23µ, 1.23u = 0.00000123
1.23m = 0.00123
1.23K, 1.23k = 1230
1.23M = 1230000
1.23G = 1230000000
1.23T = 1230000000000

View file

@ -0,0 +1,11 @@
Listed in order of precedence:
(Highest Precedence) ^ Exponentiation
% Modulo
/ Division
* Multiplication
- Subtraction
+ Addition
== != < <= >= > Comparisons (all have equal precedence)
&& and Logical AND with short-circuit
(Lowest Precedence) || or Logical OR with short-circuit

BIN
doc/iconx4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

15
doc/language-design.txt Normal file
View file

@ -0,0 +1,15 @@
notes: dorémi abc... as they appear in the note list
variables: $v8 assign '8' to the variable named 'v'
$v1+2
$vv+1
$v-4.1 as soon as the variable mutation string stops being valid, usual sheet music parsing continues (https://en.wikipedia.org/wiki/Maximal_munch)
loops: (...)
(2...) to loop 2 times
(n...) to loop n times (taking the integer part with the floor function)
tuplet: [...] all inner note lengths are shrinked evenly so that the duration of the whole tuplet is the duration of a single note
slope: {name...} as soon as the name stops matching any known slope name, usual sheet music parsing continues (implementation details will surely mean stopping on the first match to prevent weird errors when two slope names start the same)
silence: . silence is music
start-here: % playhead starts here but the context must be kept (outer loops / tuplets etc)
comment: # the rest of the line is ignored completely
All of these tokens may be separated by "blank" characters (spaces, tabs, carriage returns etc).

114
doc/uml/cli/class.puml Normal file
View file

@ -0,0 +1,114 @@
@startuml "blip class diagram"
left to right direction
component clap {
interface Parser
}
component fasteval {
enum Instruction
}
component mp3lame-encoder {
enum Quality
enum Bitrate
}
component hound {
enum SampleFormat
}
component cli {
struct Cli {
mode: Mode
}
enum Mode {
Play(PlayOpts)
Export(ExportOpts)
Help(HelpOpts)
}
Cli --> Mode
struct PlayOpts {
input: Input
instrument: Instruction [-i, --instrument]
length: Instruction [-l, --length]
variables: HashMap<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

13
doc/uml/component.puml Normal file
View file

@ -0,0 +1,13 @@
@startuml "Component diagram"
title "Component Diagram"
component lib
component cli
component "(ide)" as ide
component "(extension)" as extension
cli --> lib
ide --> lib
extension --> lib
@enduml

126
doc/uml/lib/class.puml Normal file
View file

@ -0,0 +1,126 @@
@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

View file

@ -0,0 +1,44 @@
@startuml "Token.apply activity diagram"
title Token.apply
left footer
Les méthodes et champs de l'argument "context" et "self" ont été raccourcis pour gagner de la place.
"t" will be incremented by placing the length instruction in "slopes". Context.render will tick every slope variable at every frame.
endfooter
start
(C)
switch (T)
case (Silence)
:render(None);
case (Marser)
:result = [];
case (Note)
:render(n);
case (VariableChange)
:variables[name] = change.eval(variables);
case (Loop)
:old_context = context.clone();
:context.result = [];
:new_context = fold inner into current context with Token.apply;
:return new_context.replace(result, old_context.result + new_context.result * count);
case (Tuplet)
:current_length = current_length();
:calculate how many samples should fit in the current length;
:old_context = context.clone();
:context.result = [];
:new_context = fold inner into current context with Token.apply;
:new_context.variables[t] = old_context.variables[t];
:spray new_context.result with void until it fits the sample count;
:prepend old_context.result;
:return new context;
case (Slope)
:slopes += (each_frame.name, each_frame.change);
:context = fold inner into current context with Token.apply;
:slopes[each_frame.name].drop();
endswitch
stop
@enduml

491
flamegraph.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 167 KiB

View file

@ -1,51 +0,0 @@
# fixed
instruments:
# instrument name
sine:
# fixed
expr: sin(2*PI*f*t) # instrument formula (f is the frequency in Hertz, t is the time in seconds)
square:
expr: v*abs(sin(2*PI*f*t))
# fixed
vars:
# name of the variable
v: 1 # initial value of the variable
channels:
melody:
instr: sine
score:
notes: cCdDefFgGaAb
sheet:
aabc.
'ab°A
+d+d+d---
/ff/f\\
ab>c<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 Normal file
View file

@ -0,0 +1,108 @@
# 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.

View file

@ -1,81 +0,0 @@
use std::{collections::HashMap, str::FromStr};
use amplify::{From, Wrapper};
use derive_new::new;
use derived_deref::Deref;
use fasteval::{Compiler, Instruction};
pub(super) use instrument::Instrument;
pub(super) use score::Atom;
use score::Atoms;
#[cfg(debug_assertions)]
use serde::Serialize;
use serde::{de::Visitor, Deserialize};
mod instrument;
mod score;
#[derive(Debug, PartialEq, Wrapper, From)]
pub struct Expression(Instruction);
#[cfg(debug_assertions)]
impl Serialize for Expression {
fn serialize<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>,
}

View file

@ -1,17 +0,0 @@
use std::collections::HashMap;
use derive_new::new;
use derived_deref::Deref;
use serde::Deserialize;
#[cfg(debug_assertions)]
use serde::Serialize;
use super::Expression as Instruction;
#[derive(Deref, new, Deserialize)]
#[cfg_attr(debug_assertions, derive(Serialize, Debug))]
pub struct Instrument {
#[target]
expr: Instruction,
vars: Option<HashMap<String, f32>>,
}

View file

@ -1,126 +0,0 @@
use std::{
error::Error,
num::{NonZeroU16, NonZeroU8},
};
use amplify::From;
use anyhow::Context;
use bng_macros::{QuickModifierParser, SlopeModifierParser};
use derive_new::new;
use derived_deref::Deref;
use lex::lexer::flat_atom_parser;
use nom::{
branch::alt,
character::{complete::char, streaming::one_of},
combinator::{all_consuming, eof},
multi::many0,
sequence::{preceded, terminated},
};
#[cfg(debug_assertions)]
use serde::Serialize;
use serde::{
de::{self as serde_de, Visitor},
Deserialize,
};
use strum::EnumDiscriminants;
use thiserror::Error;
use utils::{inflate, InflateError};
mod de;
mod lex;
mod utils;
pub use de::*;
use super::Expression as Instruction;
#[derive(Deref, From, Default)]
#[cfg_attr(debug_assertions, derive(Serialize, Debug))]
pub struct Atoms(Vec<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,
}

View file

@ -1,100 +0,0 @@
use derive_new::new;
use nom::{
character::complete::one_of,
combinator::all_consuming,
multi::{many0, many1},
sequence::{preceded, terminated},
Parser,
};
use serde::{
de::{self, Deserializer, Visitor},
Deserialize,
};
use thiserror::Error;
use crate::bng::score::lex::lexer::flat_atom_parser;
use super::{
utils::{inflate, InflateError},
Atoms, FlatAtom,
};
#[derive(Debug, Error)]
enum AtomsSerializeError {
#[error("sheet parsing error: {0}")]
Parsing(String),
#[error("sheet semantics: {0}")]
Inflation(#[from] InflateError),
}
fn nom_err_message(e: nom::Err<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(&notes))
}
}
}
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)
}

View file

@ -1,52 +0,0 @@
use super::{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier};
pub(super) mod lexer;
pub(super) const ON: bool = true;
pub(super) const OFF: bool = false;
pub(super) const UP: bool = true;
pub(super) const DOWN: bool = false;
struct WrappingTokens;
impl WrappingTokens {
const PARENTHESES: (char, char) = ('(', ')');
const SQUARE_BRACKETS: (char, char) = ('[', ']');
const BRACKETS: (char, char) = ('{', '}');
const SEMICOLON_COMMA: (char, char) = (';', ',');
const PLUS_MINUS: (char, char) = ('+', '-');
const RIGHT_LEFT: (char, char) = ('>', '<');
const SLASH_BACKSLASH: (char, char) = ('/', '\\');
const QUOTE_DEG: (char, char) = ('\'', '°');
}
impl QuickModifier {
pub(super) const VOLUME: (char, char) = WrappingTokens::PLUS_MINUS;
pub(super) const OCTAVE: (char, char) = WrappingTokens::RIGHT_LEFT;
pub(super) const LENGTH: (char, char) = WrappingTokens::SLASH_BACKSLASH;
pub(super) const PIZZ: (char, char) = WrappingTokens::QUOTE_DEG;
}
impl Modifier {
pub(super) const VOLUME: char = 'v';
pub(super) const OCTAVE: char = 'o';
pub(super) const LENGTH: char = 'l';
pub(super) const TEMPO: char = 't';
}
impl SlopeModifier {
pub(super) const NOTE: char = 'n';
pub(super) const VOLUME: char = 'v';
pub(super) const OCTAVE: char = 'o';
pub(super) const LENGTH: char = 'l';
pub(super) const TEMPO: char = 't';
}
impl Atom {
pub(super) const REST: char = '.';
pub(super) const START_HERE: char = ':';
pub(super) const MODIFIER: char = '!';
pub(super) const LOOP: (char, char) = WrappingTokens::PARENTHESES;
pub(super) const TUPLE: (char, char) = WrappingTokens::SQUARE_BRACKETS;
pub(super) const SLOPE: (char, char) = WrappingTokens::BRACKETS;
pub(super) const COMMENT: (char, char) = WrappingTokens::SEMICOLON_COMMA;
}

View file

@ -1,87 +0,0 @@
use std::{
collections::BTreeMap,
num::{NonZeroU16, NonZeroU8},
};
use clap::builder::TypedValueParser;
use fasteval::Compiler;
use nom::{
branch::alt,
bytes::complete::{take_till, take_till1},
character::complete::{anychar, char, one_of, space1, u16, u8},
combinator::{map_opt, map_res, opt, value, verify},
multi::many0,
sequence::{delimited, pair, preceded, separated_pair, terminated},
Err, IResult, Parser,
};
use super::{
super::super::Expression as Instruction,
{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier},
};
#[cfg(test)]
mod tests;
pub(crate) trait Parse: Sized {
fn parse(input: &str) -> IResult<&str, Self>;
}
impl Parse for Modifier {
fn parse(input: &str) -> IResult<&str, Self> {
alt((
preceded(char(Modifier::VOLUME), u8).map(Modifier::Volume),
preceded(char(Modifier::OCTAVE), u8).map(Modifier::Octave),
preceded(char(Modifier::LENGTH), map_opt(u8, NonZeroU8::new)).map(Modifier::Length),
preceded(char(Modifier::TEMPO), map_opt(u16, NonZeroU16::new)).map(Modifier::Tempo),
))(input)
}
}
pub fn flat_atom_parser(notes: &str) -> impl Parser<&str, FlatAtom, nom::error::Error<&str>> {
alt((
map_res(map_opt(one_of(notes), |c| notes.find(c)), u8::try_from).map(FlatAtom::Note),
value(FlatAtom::Rest, char(Atom::REST)),
value(FlatAtom::StartHere, char(Atom::START_HERE)),
preceded(char(Atom::MODIFIER), Modifier::parse).map(FlatAtom::Modifier),
QuickModifier::parse.map(FlatAtom::QuickModifier),
preceded(
char(Atom::LOOP.0),
map_opt(opt(u8), |n| {
if let Some(n) = n {
NonZeroU8::new(n)
} else {
unsafe { Some(NonZeroU8::new_unchecked(2)) }
}
}),
)
.map(FlatAtom::LoopStarts),
value(FlatAtom::LoopEnds, char(Atom::LOOP.1)),
value(FlatAtom::TupleStarts, char(Atom::TUPLE.0)),
value(FlatAtom::TupleEnds, char(Atom::TUPLE.1)),
terminated(
preceded(
char(Atom::SLOPE.0),
separated_pair(
SlopeModifier::parse,
char(' '),
map_res(take_till1(|c| c == ','), |s: &str| {
s.parse()
.map_err(|_| nom::error::Error::new(s, nom::error::ErrorKind::Verify))
}),
),
),
char(','),
)
.map(|(sm, i)| FlatAtom::SlopeStarts(sm, i)),
value(FlatAtom::SlopeEnds, char(Atom::SLOPE.1)),
value(
FlatAtom::Comment,
delimited(
char(Atom::COMMENT.0),
take_till(|c| c == Atom::COMMENT.1),
char(Atom::COMMENT.1),
),
),
))
}

View file

@ -1,360 +0,0 @@
use const_format::concatcp;
use nom::{
error::{Error, ErrorKind},
Err,
};
use flat_atom::{
FASTEVAL_INSTRUCTION as FLATATOM_FASTEVAL_INSTRUCTION, SAMPLE_STR as FLATATOM_SAMPLE_STRING,
};
mod flat_atom {
use std::num::NonZeroU8;
use fasteval::Compiler;
use nom::Parser;
use super::super::{
super::super::super::Expression as Instruction, super::UP, flat_atom_parser, Atom,
FlatAtom, Modifier, QuickModifier, SlopeModifier,
};
use super::*;
pub(super) const SAMPLE_STR: &str = concatcp!(
Atom::TUPLE.0,
"acc",
Atom::TUPLE.1,
"ed",
Atom::COMMENT.0,
"hello"
);
pub(super) const FASTEVAL_INSTRUCTION: &str = "1-cos((PI*x)/2)";
#[test]
fn note() {
assert_eq!(
Ok((SAMPLE_STR, FlatAtom::Note(2))),
flat_atom_parser("abcdefg").parse(concatcp!('c', SAMPLE_STR))
)
}
#[test]
fn rest() {
assert_eq!(
Ok((SAMPLE_STR, FlatAtom::Rest)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::REST, SAMPLE_STR))
)
}
#[test]
fn start_here() {
assert_eq!(
Ok((SAMPLE_STR, FlatAtom::StartHere)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::START_HERE, SAMPLE_STR))
)
}
#[test]
fn modifier() {
assert_eq!(
Ok((
SAMPLE_STR,
FlatAtom::Modifier(Modifier::Length(unsafe { NonZeroU8::new_unchecked(2) }))
)),
flat_atom_parser("abcdefg").parse(concatcp!(
Atom::MODIFIER,
Modifier::LENGTH,
2u8,
SAMPLE_STR
))
)
}
#[test]
fn quick_modifier() {
assert_eq!(
Ok((
SAMPLE_STR,
FlatAtom::QuickModifier(QuickModifier::Length(UP))
)),
flat_atom_parser("abcdefg").parse(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR))
)
}
#[test]
fn loop_starts() {
assert_eq!(
Ok((
SAMPLE_STR,
FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(3) })
)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.0, 3u8, SAMPLE_STR))
);
assert_eq!(
Ok((
SAMPLE_STR,
FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(2) })
)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.0, SAMPLE_STR))
);
assert_eq!(
Err(nom::Err::Error(Error::new(
concatcp!(Atom::LOOP.0, 0u8, SAMPLE_STR),
ErrorKind::Char
))),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.0, 0u8, SAMPLE_STR))
)
}
#[test]
fn loop_ends() {
assert_eq!(
Ok((SAMPLE_STR, FlatAtom::LoopEnds)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.1, SAMPLE_STR))
)
}
#[test]
fn tuple_starts() {
assert_eq!(
Ok((SAMPLE_STR, FlatAtom::TupleStarts)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::TUPLE.0, SAMPLE_STR))
)
}
#[test]
fn tuple_ends() {
assert_eq!(
Ok((SAMPLE_STR, FlatAtom::TupleEnds)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::TUPLE.1, SAMPLE_STR))
)
}
#[test]
fn slope_starts() {
assert_eq!(
Ok((
SAMPLE_STR,
FlatAtom::SlopeStarts(SlopeModifier::Note, FASTEVAL_INSTRUCTION.parse().unwrap())
)),
flat_atom_parser("abcdefg").parse(concatcp!(
Atom::SLOPE.0,
SlopeModifier::NOTE,
' ',
FASTEVAL_INSTRUCTION,
',',
SAMPLE_STR
))
)
}
#[test]
fn slope_ends() {
assert_eq!(
Ok((SAMPLE_STR, FlatAtom::SlopeEnds)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::SLOPE.1, SAMPLE_STR))
)
}
#[test]
fn comment() {
assert_eq!(
Ok((SAMPLE_STR, FlatAtom::Comment)),
flat_atom_parser("abcdefg").parse(concatcp!(
Atom::COMMENT.0,
"hi I'm a little pony",
SAMPLE_STR,
Atom::COMMENT.1,
SAMPLE_STR
))
)
}
}
mod modifier {
use std::num::{NonZeroU16, NonZeroU8};
use super::FLATATOM_SAMPLE_STRING as SAMPLE_STR;
use super::*;
use crate::bng::score::{lex::lexer::Parse, Modifier};
#[test]
fn volume() {
assert_eq!(
Ok((SAMPLE_STR, Modifier::Volume(2))),
Modifier::parse(concatcp!(Modifier::VOLUME, 2u8, SAMPLE_STR))
);
assert_eq!(
Err(nom::Err::Error(Error::new(
concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR),
ErrorKind::Char
))),
Modifier::parse(concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR))
);
}
#[test]
fn octave() {
assert_eq!(
Ok((SAMPLE_STR, Modifier::Octave(2))),
Modifier::parse(concatcp!(Modifier::OCTAVE, 2u8, SAMPLE_STR))
);
assert_eq!(
Err(nom::Err::Error(Error::new(
concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR),
ErrorKind::Char
))),
Modifier::parse(concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR))
);
}
#[test]
fn length() {
assert_eq!(
Ok((
SAMPLE_STR,
Modifier::Length(unsafe { NonZeroU8::new_unchecked(2) })
)),
Modifier::parse(concatcp!(Modifier::LENGTH, 2u8, SAMPLE_STR))
);
assert_eq!(
Err(nom::Err::Error(Error::new(
concatcp!(Modifier::LENGTH, 2556u16, SAMPLE_STR),
ErrorKind::Char
))),
Modifier::parse(concatcp!(Modifier::LENGTH, 2556u16, SAMPLE_STR))
);
assert_eq!(
Err(nom::Err::Error(Error::new(
concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR),
ErrorKind::Char
))),
Modifier::parse(concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR))
);
}
#[test]
fn tempo() {
assert_eq!(
Ok((
SAMPLE_STR,
Modifier::Tempo(unsafe { NonZeroU16::new_unchecked(2) })
)),
Modifier::parse(concatcp!(Modifier::TEMPO, 2u8, SAMPLE_STR))
);
assert_eq!(
Err(nom::Err::Error(Error::new(
concatcp!(655353u32, SAMPLE_STR),
ErrorKind::Digit
))),
Modifier::parse(concatcp!(Modifier::TEMPO, 655353u32, SAMPLE_STR))
);
}
}
mod quick_modifier {
use super::FLATATOM_SAMPLE_STRING as SAMPLE_STR;
use super::*;
use crate::bng::score::{
lex::{lexer::Parse, DOWN, OFF, ON, UP},
QuickModifier,
};
#[test]
fn volume() {
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Volume(UP))),
QuickModifier::parse(concatcp!(QuickModifier::VOLUME.0, SAMPLE_STR))
);
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Volume(DOWN))),
QuickModifier::parse(concatcp!(QuickModifier::VOLUME.1, SAMPLE_STR))
);
}
#[test]
fn octave() {
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Octave(UP))),
QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.0, SAMPLE_STR))
);
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Octave(DOWN))),
QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.1, SAMPLE_STR))
);
}
#[test]
fn length() {
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Length(UP))),
QuickModifier::parse(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR))
);
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Length(DOWN))),
QuickModifier::parse(concatcp!(QuickModifier::LENGTH.1, SAMPLE_STR))
);
}
#[test]
fn pizz() {
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Pizz(ON))),
QuickModifier::parse(concatcp!(QuickModifier::PIZZ.0, SAMPLE_STR))
);
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Pizz(OFF))),
QuickModifier::parse(concatcp!(QuickModifier::PIZZ.1, SAMPLE_STR))
);
}
}
#[cfg(test)]
mod slope_modifier {
use super::FLATATOM_FASTEVAL_INSTRUCTION as INSTRUCTION;
use super::*;
use crate::bng::score::{lex::lexer::Parse, Atom, SlopeModifier};
const SAMPLE_STR: &str = concatcp!(' ', INSTRUCTION, Atom::SLOPE.1);
#[test]
fn note() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Note)),
SlopeModifier::parse(concatcp!(SlopeModifier::NOTE, SAMPLE_STR))
)
}
#[test]
fn volume() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Volume)),
SlopeModifier::parse(concatcp!(SlopeModifier::VOLUME, SAMPLE_STR))
)
}
#[test]
fn octave() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Octave)),
SlopeModifier::parse(concatcp!(SlopeModifier::OCTAVE, SAMPLE_STR))
)
}
#[test]
fn length() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Length)),
SlopeModifier::parse(concatcp!(SlopeModifier::LENGTH, SAMPLE_STR))
)
}
#[test]
fn tempo() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Tempo)),
SlopeModifier::parse(concatcp!(SlopeModifier::TEMPO, SAMPLE_STR))
)
}
}

View file

@ -1,236 +0,0 @@
use super::*;
use strum::Display;
use thiserror::Error;
#[cfg(test)]
mod tests;
#[derive(Debug, EnumDiscriminants)]
#[strum_discriminants(derive(Display))]
pub enum Wrapper {
Loop(NonZeroU8),
Tuple,
Slope(SlopeModifier, Instruction),
}
#[derive(Debug, Error)]
#[cfg_attr(test, derive(PartialEq))]
pub enum InflateError {
#[error("misplaced {0} end symbol")]
MismatchedEnd(WrapperDiscriminants),
}
pub fn inflate(mut flat_atoms: Vec<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)
}

View file

@ -1,206 +0,0 @@
#[cfg(test)]
mod inflate {
use fasteval::Compiler;
use lex::{ON, UP};
use super::{super::*, inflate};
const FASTEVAL_INSTRUCTION: &str = "1-cos((PI*x)/2)";
fn instruction() -> Instruction {
FASTEVAL_INSTRUCTION.parse().unwrap()
}
#[test]
fn inflate_flat() {
assert_eq!(
Ok(vec![
Atom::Note(2),
Atom::Rest,
Atom::StartHere,
Atom::Modifier(Modifier::Volume(2)),
Atom::QuickModifier(QuickModifier::Volume(UP)),
Atom::Comment
]),
inflate(vec![
FlatAtom::Note(2),
FlatAtom::Rest,
FlatAtom::StartHere,
FlatAtom::Modifier(Modifier::Volume(2)),
FlatAtom::QuickModifier(QuickModifier::Volume(UP)),
FlatAtom::Comment
])
)
}
#[test]
fn inflate_loop_l1() {
assert_eq!(
Ok(vec![Atom::Loop(
unsafe { NonZeroU8::new_unchecked(3) },
vec![Atom::Note(2), Atom::Note(3)]
)]),
inflate(vec![
FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(3) }),
FlatAtom::Note(2),
FlatAtom::Note(3),
FlatAtom::LoopEnds
])
)
}
#[test]
fn inflate_tuple_l1() {
assert_eq!(
Ok(vec![Atom::Tuple(vec![Atom::Note(2), Atom::Note(3)])]),
inflate(vec![
FlatAtom::TupleStarts,
FlatAtom::Note(2),
FlatAtom::Note(3),
FlatAtom::TupleEnds
])
)
}
#[test]
fn inflate_slope_l1() {
assert_eq!(
Ok(vec![Atom::Slope(
SlopeModifier::Note,
instruction(),
vec![Atom::Note(2), Atom::Note(3)]
)]),
inflate(vec![
FlatAtom::SlopeStarts(SlopeModifier::Note, instruction()),
FlatAtom::Note(2),
FlatAtom::Note(3),
FlatAtom::SlopeEnds
])
)
}
#[test]
fn inflate_loop_l2() {
assert_eq!(
Ok(vec![Atom::Loop(
unsafe { NonZeroU8::new_unchecked(2) },
vec![Atom::Loop(
unsafe { NonZeroU8::new_unchecked(3) },
vec![Atom::Note(2), Atom::Note(3)]
)]
)]),
inflate(vec![
FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(2) }),
FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(3) }),
FlatAtom::Note(2),
FlatAtom::Note(3),
FlatAtom::LoopEnds,
FlatAtom::LoopEnds
])
)
}
#[test]
fn inflate_tuple_l2() {
assert_eq!(
Ok(vec![Atom::Tuple(vec![Atom::Tuple(vec![
Atom::Note(2),
Atom::Note(3)
])])]),
inflate(vec![
FlatAtom::TupleStarts,
FlatAtom::TupleStarts,
FlatAtom::Note(2),
FlatAtom::Note(3),
FlatAtom::TupleEnds,
FlatAtom::TupleEnds
])
)
}
#[test]
fn inflate_slope_l2() {
assert_eq!(
Ok(vec![Atom::Slope(
SlopeModifier::Note,
instruction(),
vec![Atom::Slope(
SlopeModifier::Length,
instruction(),
vec![Atom::Note(2), Atom::Note(3)]
)]
)]),
inflate(vec![
FlatAtom::SlopeStarts(SlopeModifier::Note, instruction()),
FlatAtom::SlopeStarts(SlopeModifier::Length, instruction()),
FlatAtom::Note(2),
FlatAtom::Note(3),
FlatAtom::SlopeEnds,
FlatAtom::SlopeEnds
])
)
}
#[test]
fn mixed() {
assert_eq!(
Ok(vec![Atom::Slope(
SlopeModifier::Note,
instruction(),
vec![Atom::Slope(
SlopeModifier::Length,
instruction(),
vec![
Atom::Note(2),
Atom::Tuple(vec![Atom::Rest, Atom::Note(6)]),
Atom::Note(3),
Atom::Loop(
unsafe { NonZeroU8::new_unchecked(9) },
vec![Atom::QuickModifier(QuickModifier::Pizz(ON)), Atom::Note(0)]
)
]
)]
)]),
inflate(vec![
FlatAtom::SlopeStarts(SlopeModifier::Note, instruction()),
FlatAtom::SlopeStarts(SlopeModifier::Length, instruction()),
FlatAtom::Note(2),
FlatAtom::TupleStarts,
FlatAtom::Rest,
FlatAtom::Note(6),
FlatAtom::TupleEnds,
FlatAtom::Note(3),
FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(9) }),
FlatAtom::QuickModifier(QuickModifier::Pizz(ON)),
FlatAtom::Note(0),
FlatAtom::LoopEnds,
FlatAtom::SlopeEnds,
FlatAtom::SlopeEnds
])
)
}
#[test]
fn mixed_mismatched_end() {
assert_eq!(
Err(InflateError::MismatchedEnd(WrapperDiscriminants::Slope)),
inflate(vec![
FlatAtom::SlopeStarts(SlopeModifier::Note, instruction()),
FlatAtom::SlopeStarts(SlopeModifier::Length, instruction()),
FlatAtom::Note(2),
FlatAtom::TupleStarts,
FlatAtom::Rest,
FlatAtom::SlopeEnds, // mismatched slope end while in a tuple
FlatAtom::Note(6),
FlatAtom::TupleEnds,
FlatAtom::Note(3),
FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(9) }),
FlatAtom::QuickModifier(QuickModifier::Pizz(ON)),
FlatAtom::Note(0),
FlatAtom::LoopEnds,
FlatAtom::SlopeEnds,
FlatAtom::SlopeEnds,
])
)
}
}

View file

@ -1,90 +0,0 @@
use std::{fmt::Display, fs::read_to_string, io, str::FromStr};
use amplify::{From, Wrapper};
use clap::Parser;
/// Cli entry point
#[derive(Clone, Parser)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub enum BngCli {
/// Play the song through default sink
Play(PlayOpts),
/// Export the song to a sound FileContents
Export(ExportOpts),
/// List supported sound FileContents extensions and instrument / song available expressions
#[command(subcommand)]
List(ListOpts),
}
/// [`BngCli`] "play" command options
#[derive(Clone, Parser)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct PlayOpts {
#[arg(value_parser = FileContents::from_str)]
pub(super) input: FileContents,
}
/// [`BngCli`] "export" command options
#[derive(Clone, Parser)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct ExportOpts {
/// Input FileContents (written song FileContents)
#[arg(value_parser = FileContents::from_str)]
input: FileContents,
/// Output FileContents (sound FileContents)
#[arg(value_parser = AudioFileName::from_str)]
output: AudioFileName,
}
/// [`BngCli`] "list" command sub-commands
#[derive(Clone, Parser)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub enum ListOpts {
/// List supported sound FileContents extensions to export songs
#[command(subcommand)]
Extensions,
/// List available math expressions for instrument definition
#[command(subcommand)]
Math,
/// List available score glyphs and their meaning
#[command(subcommand)]
Glyphs,
}
#[derive(Clone, Wrapper, From)]
#[wrapper(Deref)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct FileContents(String);
impl FromStr for FileContents {
type Err = io::Error;
fn from_str(s: &str) -> Result<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 Normal file
View file

@ -0,0 +1,470 @@
#![allow(dead_code)]
use std::{
error::Error,
fmt::Debug,
fs::File,
io::{self, Cursor, Read, stdin},
ops::Not,
str::FromStr,
};
use anyhow::anyhow;
use bliplib::compiler::Expression;
use clap::Parser;
use derive_wrapper::AsRef;
use getset::Getters;
use hound::SampleFormat;
use mp3lame_encoder::{Bitrate, Quality};
use nom::{
Finish, Parser as P,
branch::alt,
bytes::complete::tag,
character::complete::{char, u16, usize},
combinator::{rest, success, value},
error::ErrorKind,
sequence::preceded,
};
use nom_locate::{LocatedSpan, position};
use strum::{EnumDiscriminants, EnumIter, EnumString, IntoDiscriminant, IntoStaticStr};
use thiserror::Error;
const DEFAULT_INSTRUMENT: &str = "sin(2*pi()*(442*2^((n+1)/N))*t)";
const DEFAULT_LENGTH: &str = "2^(2-log(2, l))*(60/T)";
#[derive(Debug, Parser)]
#[command(version, author, about)]
pub(super) enum Cli {
/// Play a song
Play(PlayOpts),
/// Check for typos
Check(PlayOpts),
/// Export a song to an audio file or stdout
Export(ExportOpts),
/// Memo menu for examples and general help about syntax and supported audio formats
#[command(subcommand)]
Memo(MemoKind),
}
#[derive(Debug, Parser, Clone, Getters)]
#[getset(get = "pub(super)")]
pub(super) struct PlayOpts {
/// Use this sheet music [default: stdin]
#[command(flatten)]
input: InputGroup<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 Normal file
View file

@ -0,0 +1,634 @@
//! See [the lib docs](https://docs.rs/bliplib)
#[macro_use]
mod cli;
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
fs::File,
io::{Cursor, Write, read_to_string, stdout},
iter::once,
};
use anyhow::{Context as _, anyhow, bail};
use bliplib::{
compiler::{Compiler, Context, SAMPLE_RATE, VariableChange},
parser::{LocatedVerboseError, Parser},
};
use clap::Parser as _;
use cli::Cli;
use dasp_sample::{I24, Sample};
use flacenc::{component::BitRepr, error::Verify};
use hound::{SampleFormat, WavSpec, WavWriter};
use log::{debug, error, info, warn};
use mp3lame_encoder::{Builder as Mp3EncoderBuilder, FlushNoGap, MonoPcm};
use rodio::{OutputStream, Sink, buffer::SamplesBuffer};
use strum::IntoEnumIterator;
use crate::cli::{
AudioFormatDiscriminants, ExportOpts, FastEvalSyntaxSection, MemoKind, PlayOpts,
RawAudioFormat, SyntaxTarget,
};
fn main() -> anyhow::Result<()> {
env_logger::init();
let cli = Cli::parse();
debug!("options: {cli:#?}");
use Cli::*;
match cli {
Check(opts) => {
parse_and_compile(&opts)?;
}
Play(opts) => {
let (_stream, stream_handle) = OutputStream::try_default()
.context("Failed to find (or use) default audio device")?;
info!("output stream acquired");
let sink = Sink::try_new(&stream_handle).context("Epic audio playback failure")?;
info!("audio sink acquired");
let samples: Vec<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 Normal file
View file

@ -0,0 +1,619 @@
use std::{
any::{Any, TypeId, type_name},
collections::BTreeMap,
f64,
fmt::{Debug, Display},
str::FromStr,
};
use cfg_if::cfg_if;
use derive_new::new;
use derive_wrapper::{AsRef, From};
use fasteval::{Compiler as _, EvalNamespace, Evaler, Instruction, Slab};
use log::{debug, trace};
use thiserror::Error;
cfg_if! {
if #[cfg(test)] {
/// Static sample rate.
pub const SAMPLE_RATE: u16 = 10;
} else {
/// Static sample rate.
pub const SAMPLE_RATE: u16 = 48000;
}
}
/// A wrapper for a Vec of tokens.
#[derive(Debug, From, AsRef, Default)]
pub struct TokenVec(pub(crate) Vec<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 == &note_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(&note_length_variable)
.ok_or(VariableNotFoundError(note_length_variable.clone()))? =
slope.instruction.eval(&slope.slab, &mut variables)?;
}
let note_length = *variables
.get(&note_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(())
}
}

6
src/lib.rs Normal file
View file

@ -0,0 +1,6 @@
#![doc = include_str!("DOCS.md")]
/// Compilation stuff (for turning tokens into samples)
pub mod compiler;
/// Parsing stuff (for turning strings into tokens)
pub mod parser;

View file

@ -1,53 +0,0 @@
use std::collections::HashMap;
use anyhow::Error;
use bng::{BngFile, Channel, Expression, Instrument};
/// TODO: remove clap, use only a file or standard in
use clap::Parser;
use cli::{BngCli as Cli, PlayOpts};
use fasteval::Compiler;
mod bng;
mod cli;
fn main() -> Result<(), Error> {
// println!("{}", option_env!("TEST").unwrap_or("ok"));
let args = Cli::parse();
// #[cfg(debug_assertions)]
// {
// println!("{}", serde_yml::to_string(&bngfile_generator())?);
// println!("{:?}", args);
// }
match args {
Cli::Play(PlayOpts { input }) => {
let bng_file: BngFile = serde_yml::from_str(&input)?;
#[cfg(debug_assertions)]
println!("{:#?}", bng_file);
}
_ => unimplemented!("can't do that yet"),
}
Ok(())
}
#[cfg(debug_assertions)]
fn bngfile_generator() -> BngFile {
BngFile::new(
HashMap::from([
(
"sine".to_string(),
Instrument::new("sin(2*PI*f*t)".parse().unwrap(), None),
),
(
"square".to_string(),
Instrument::new(
"v*abs(sin(2*PI*f*t))".parse().unwrap(),
Some(HashMap::from([("v".to_string(), 1f32)])),
),
),
]),
HashMap::<String, Channel>::from([(
"melody".to_string(),
Channel::new("sine".to_string(), vec![].into()),
)]),
)
}

911
src/parser.rs Normal file
View file

@ -0,0 +1,911 @@
use std::{
any::type_name,
borrow::{Borrow, Cow},
collections::BTreeMap,
marker::PhantomData,
};
use anyhow::anyhow;
use derive_new::new;
use fasteval::Evaler;
use log::{debug, trace, warn};
use nom::{
AsChar, Compare, Input, Parser as _,
branch::alt,
bytes::complete::{tag, take, take_till},
character::{
complete::{char, space1, usize},
streaming::one_of,
},
combinator::{all_consuming, cut, opt, value},
error::{ErrorKind, FromExternalError, ParseError},
multi::many0,
sequence::{delimited, preceded},
};
use nom_locate::LocatedSpan;
use crate::compiler::{
Expression, Loop, LoopCount, Marker, Note, Silence, Slope, Token, TokenVec, Tuplet,
VariableChange,
};
/// From `nom`'s [`IResult`](https://docs.rs/nom/8.0.0/nom/type.IResult.html) :
/// > Holds the result of parsing functions
/// >
/// > It depends on the input type `I`, the output type `O`, and the error type `E`
/// > (by default `(I, nom::ErrorKind)`)
/// >
/// > The `Ok` side is a pair containing the remainder of the input (the part of the data that
/// > was not parsed) and the produced value. The `Err` side contains an instance of `nom::Err`.
/// >
/// > Outside of the parsing code, you can use the [Finish::finish] method to convert
/// > it to a more common result type
///
/// The error type in this IResult is [`LocatedVerboseError`].
pub type IResult<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", "", "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", "", "mi", "m"]).parse(input);
let mut working_cases = vec![
("do", ("", Note(0))),
("", ("", 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", "", "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", "", "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:?}"
);
}
}
}