Compare commits

..

No commits in common. "main" and "standardised-nom-parsing" have entirely different histories.

26 changed files with 871 additions and 1212 deletions

View file

@ -4,26 +4,23 @@ version = "0.1.0"
edition = "2021"
[dependencies]
amplify = "4"
amplify = "4.7.0"
anyhow = "1.0"
cfg-if = "1.0.0"
clap = { version = "4.5", features = ["derive"] }
derived-deref = "2.1.0"
fasteval = "0.2.4"
nom = "7.1.3"
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0.209", features = ["derive"] }
serde_yml = "0.0.12"
splines = "4"
splines = "4.3.1"
strum = { version = "0.26", features = ["derive"] }
strum_macros = "0.26"
tinyaudio = { version = "1", optional = true }
tinyaudio = { version = "0.1", optional = true }
bng_macros = { path = "bng_macros" }
const_format = "0.2.33"
thiserror = "2"
derive-new = "0.7"
naan = "0.1"
lazy_static = "1.5"
tune = "0.35"
thiserror = "1.0.64"
derive-new = "0.7.0"
[features]
default = ["play", "save"]

View file

@ -7,7 +7,6 @@ edition = "2021"
proc-macro = true
[dependencies]
Inflector = "0.11.4"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }

View file

@ -1,9 +1,8 @@
extern crate proc_macro;
use inflector::Inflector;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, FieldsUnnamed, Ident};
use quote::quote;
use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Ident};
#[proc_macro_derive(SlopeModifierParser)]
pub fn slope_modifier_parser(input: TokenStream) -> TokenStream {
@ -17,49 +16,24 @@ pub fn quick_modifier_parser(input: TokenStream) -> TokenStream {
impl_quick_modifier_parser(ast)
}
#[proc_macro_derive(ModifierParser)]
pub fn modifier_parser(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_modifier_parser(ast)
}
trait ToIdent: ToString {
fn to_ident(&self) -> Ident {
Ident::new(&self.to_string(), Span::call_site())
}
}
impl<S: ToString> ToIdent for S {}
fn impl_slope_modifier_parser(ast: DeriveInput) -> TokenStream {
let name = &ast.ident;
let fn_name = name.to_string().to_snake_case().to_ident();
if let Data::Enum(DataEnum { variants, .. }) = ast.data {
let match_arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;
let variant_name_lower = variant_name.to_string().to_lowercase().to_ident();
let const_name = &variant.ident.to_string().to_uppercase().to_ident();
let const_name = Ident::new(&variant.ident.to_string().to_uppercase(),Span::call_site());
quote! {
nom::error::context(
stringify!(#variant_name_lower),
nom::combinator::value(
#name::#variant_name,
nom::character::complete::char(SlopeModifier::#const_name)
)
)
nom::combinator::value(#name::#variant_name, nom::character::complete::char(SlopeModifier::#const_name))
}
});
quote! {
pub fn #fn_name<'a, E: nom::error::ParseError<&'a str> + nom::error::ContextError<&'a str>>(
i: &'a str,
) -> nom::IResult<&'a str, #name, E> {
nom::error::context(
"slope modifier",
impl lex::lexer::Parse for #name {
fn parse(input: &str) -> nom::IResult<&str, #name> {
nom::branch::alt((
#(#match_arms),*
))
)(i)
))(input)
}
}
}
.into()
@ -70,110 +44,37 @@ fn impl_slope_modifier_parser(ast: DeriveInput) -> TokenStream {
fn impl_quick_modifier_parser(ast: DeriveInput) -> TokenStream {
let name = &ast.ident;
let fn_name = name.to_string().to_snake_case().to_ident();
if let Data::Enum(DataEnum { variants, .. }) = ast.data {
let match_arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;
let variant_name_lower = variant_name.to_string().to_lowercase().to_ident();
let const_name = &variant.ident.to_string().to_uppercase().to_ident();
let const_name =
Ident::new(&variant.ident.to_string().to_uppercase(), Span::call_site());
quote! {
nom::error::context(
stringify!(#variant_name_lower),
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)
)
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
)
)
),
#name::#variant_name
)
}
});
quote! {
pub fn #fn_name<'a, E: nom::error::ParseError<&'a str> + nom::error::ContextError<&'a str>>(
i: &'a str,
) -> nom::IResult<&'a str, #name, E> {
nom::error::context(
"quick modifier",
impl lex::lexer::Parse for #name {
fn parse(input: &str) -> nom::IResult<&str, #name> {
nom::branch::alt((
#(#match_arms),*
))
)(i)
}
}
.into()
} else {
panic!("this macro only works on enums")
}
}
fn impl_modifier_parser(ast: DeriveInput) -> TokenStream {
let name = &ast.ident;
let fn_name = name.to_string().to_snake_case().to_ident();
if let Data::Enum(DataEnum { variants, .. }) = ast.data {
let match_arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;
let variant_name_lower = variant_name.to_string().to_lowercase().to_ident();
let const_name = &variant.ident.to_string().to_uppercase().to_ident();
if let Fields::Unnamed(FieldsUnnamed {
paren_token: _,
unnamed,
}) = &variant.fields
{
let type_name = {
let type_name = &unnamed[0].ty.to_token_stream().to_string();
match type_name.strip_prefix("NonZero") {
Some(s) => {
let lower = s.to_lowercase().to_ident();
let type_name = &unnamed[0].ty;
quote! {
nom::combinator::map_opt(
nom::character::complete::#lower,
std::num::#type_name::new
)
}
}
_ => {
let type_name = &unnamed[0].ty;
quote! {
nom::character::complete::#type_name
}
}
}
};
quote! {
nom::error::context(
stringify!(#variant_name_lower),
nom::combinator::map(
nom::sequence::preceded(nom::character::complete::char(#name::#const_name), #type_name),
#name::#variant_name
)
)
))(input)
}
} else {
panic!("this macro only works on unnamed fields")
}
});
quote! {
pub fn #fn_name<'a, E: nom::error::ParseError<&'a str> + nom::error::ContextError<&'a str>>(
i: &'a str,
) -> nom::IResult<&'a str, #name, E> {
nom::error::context(
"modifier",
nom::branch::alt((
#(#match_arms),*
))
)(i)
}
}
.into()

122
check.txt
View file

@ -1,122 +0,0 @@
Checking bng v0.1.0 (/home/p6nj/bng)
error[E0720]: cannot resolve opaque type
--> src/bng/score/lex/lexer.rs:35:39
|
35 | pub fn root<'a, E>(notes: &'a str) -> impl Parser<&'a str, Atoms, E>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive opaque type
...
42 | / terminated(
43 | | many1(preceded(maybe_yml_str_space(), atom(notes))),
44 | | maybe_yml_str_space(),
45 | | )
46 | | .map(Atoms)
| |_______________- returning here with type `nom::Map<impl FnMut(&str) -> std::result::Result<(&str, std::vec::Vec<Atom>), nom::Err<E>>, fn(std::vec::Vec<Atom>) -> Atoms {Atoms}, std::vec::Vec<Atom>>`
|
::: /home/p6nj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/nom-7.1.3/src/sequence/mod.rs:100:6
|
100 | ) -> impl FnMut(I) -> IResult<I, O1, E>
| ---------------------------------- returning this type `nom::Map<impl FnMut(&str) -> std::result::Result<(&str, std::vec::Vec<Atom>), nom::Err<E>>, fn(std::vec::Vec<Atom>) -> Atoms {Atoms}, std::vec::Vec<Atom>>`
error[E0720]: cannot resolve opaque type
--> src/bng/score/lex/lexer.rs:98:37
|
98 | fn r#loop<'a, E>(notes: &'a str) -> impl Parser<&'a str, Atom, E>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive opaque type
...
105 | / context(
106 | | "loop",
107 | | delimited(
108 | | char(Atom::LOOP.0),
... |
121 | | )
122 | | .map(|(n, v)| Atom::Loop(n, v))
| |___________________________________- returning here with type `nom::Map<impl FnMut(&str) -> std::result::Result<(&str, (NonZero<u8>, Atoms)), nom::Err<E>>, {closure@src/bng/score/lex/lexer.rs:122:10: 122:18}, (NonZero<u8>, Atoms)>`
|
::: /home/p6nj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/nom-7.1.3/src/error.rs:236:6
|
236 | ) -> impl FnMut(I) -> IResult<I, O, E>
| --------------------------------- returning this type `nom::Map<impl FnMut(&str) -> std::result::Result<(&str, (NonZero<u8>, Atoms)), nom::Err<E>>, {closure@src/bng/score/lex/lexer.rs:122:10: 122:18}, (NonZero<u8>, Atoms)>`
error[E0720]: cannot resolve opaque type
--> src/bng/score/lex/lexer.rs:185:35
|
185 | fn atom<'a, E>(notes: &'a str) -> impl Parser<&'a str, Atom, E>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive opaque type
...
192 | / context(
193 | | "atom",
194 | | alt((
195 | | note(notes),
... |
204 | | )),
205 | | )
| |_____- returning here with type `impl FnMut(&str) -> std::result::Result<(&str, Atom), nom::Err<E>>`
|
::: /home/p6nj/.cargo/registry/src/index.crates.io-6f17d22bba15001f/nom-7.1.3/src/error.rs:236:6
|
236 | ) -> impl FnMut(I) -> IResult<I, O, E>
| --------------------------------- returning this opaque type `impl FnMut(&str) -> std::result::Result<(&str, Atom), nom::Err<E>>`
error[E0391]: cycle detected when computing type of opaque `bng::score::lex::lexer::root::{opaque#0}`
--> src/bng/score/lex/lexer.rs:35:39
|
35 | pub fn root<'a, E>(notes: &'a str) -> impl Parser<&'a str, Atoms, E>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: ...which requires type-checking `bng::score::lex::lexer::root`...
--> src/bng/score/lex/lexer.rs:46:6
|
46 | .map(Atoms)
| ^^^
= note: ...which requires evaluating trait selection obligation `nom::sequence::terminated::{opaque#0}: core::marker::Sync`...
= note: ...which again requires computing type of opaque `bng::score::lex::lexer::root::{opaque#0}`, completing the cycle
note: cycle used when computing type of `bng::score::lex::lexer::root::{opaque#0}`
--> src/bng/score/lex/lexer.rs:35:39
|
35 | pub fn root<'a, E>(notes: &'a str) -> impl Parser<&'a str, Atoms, E>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
error[E0391]: cycle detected when computing type of opaque `bng::score::lex::lexer::tuple::{opaque#0}`
--> src/bng/score/lex/lexer.rs:125:36
|
125 | fn tuple<'a, E>(notes: &'a str) -> impl Parser<&'a str, Atom, E>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: ...which requires type-checking `bng::score::lex::lexer::tuple`...
--> src/bng/score/lex/lexer.rs:136:6
|
136 | .map(Atom::Tuple)
| ^^^
= note: ...which requires evaluating trait selection obligation `nom::error::context::{opaque#0}: core::marker::Sync`...
= note: ...which again requires computing type of opaque `bng::score::lex::lexer::tuple::{opaque#0}`, completing the cycle
note: cycle used when computing type of `bng::score::lex::lexer::tuple::{opaque#0}`
--> src/bng/score/lex/lexer.rs:125:36
|
125 | fn tuple<'a, E>(notes: &'a str) -> impl Parser<&'a str, Atom, E>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
error[E0391]: cycle detected when computing type of opaque `bng::score::lex::lexer::slope::{opaque#0}`
--> src/bng/score/lex/lexer.rs:139:36
|
139 | fn slope<'a, E>(notes: &'a str) -> impl Parser<&'a str, Atom, E>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: ...which requires type-checking `bng::score::lex::lexer::slope`...
--> src/bng/score/lex/lexer.rs:165:6
|
165 | .map(|((sm, i), v)| Atom::Slope(sm, i, v))
| ^^^
= note: ...which requires evaluating trait selection obligation `nom::error::context::{opaque#0}: core::marker::Sync`...
= note: ...which again requires computing type of opaque `bng::score::lex::lexer::slope::{opaque#0}`, completing the cycle
note: cycle used when computing type of `bng::score::lex::lexer::slope::{opaque#0}`
--> src/bng/score/lex/lexer.rs:139:36
|
139 | fn slope<'a, E>(notes: &'a str) -> impl Parser<&'a str, Atom, E>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
Some errors have detailed explanations: E0391, E0720.
For more information about an error, try `rustc --explain E0391`.
error: could not compile `bng` (bin "bng") due to 6 previous errors

View file

@ -1,61 +0,0 @@
@startuml Class Diagram
package core {
package str {
package traits {
interface FromStr<type Error> {
from_str(s: &str): Result<Self, Self::Error>
}
}
}
}
package cli {
package docstr {}
package schema {
+enum Cli {
+Play { PlayOpts }
+Export { ExportOptions }
+List { ListOptions }
}
+struct PlayOptions {
#input: FileContents
}
+struct ExportOptions {
~input: FileContents
~output: AudioFileName
}
+enum ListOptions {
+Extensions
+struct Math { -kind: Option<MathDocKind> }
+Glyphs
}
+enum MathDocKind {
+Functions
+Operators
+Literals
}
+struct FileContents {
0: String
}
FileContents --|> FromStr: type Error = io::Error
+struct AudioFileName {
0: String
}
+struct UnsupportedFileExtensionError {
}
AudioFileName -|> FromStr: type Error = UnsupportedFileExtensionError
Cli --> PlayOptions
Cli --> ExportOptions
Cli --> ListOptions
PlayOptions --> FileContents
ExportOptions -> FileContents
ExportOptions -> AudioFileName
ListOptions --> MathDocKind
AudioFileName --> UnsupportedFileExtensionError
}
}
package lang {}
package dasp {}
@enduml

View file

@ -1,12 +0,0 @@
! C:\Audio\Tunings\MTS Packs\World Scales Pack\scl\Ancient Greek Didymus Chromatic.scl
!
Didymus' Chromatic
7
!
16/15
10/9
4/3
3/2
8/5
5/3
2/1

View file

@ -1,28 +0,0 @@
! Ancient Greek scales on E (white notes)
! Size of map:
12
! First MIDI note number to retune:
0
! Last MIDI note number to retune:
127
! Middle note where the first entry in the mapping is mapped to:
64
! Reference note for which frequency is given:
64
! Frequency to tune the above note to (floating point e.g. 440.0):
329.627557
! Scale degree to consider as formal octave:
7
! Mapping.
0
1
1
2
2
3
3
4
5
5
6
6

View file

@ -1,57 +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
scl:
Didymus: poc/Ancient Greek Didymus Chromatic.scl
kbm:
Greek: poc/Ancient Greek.kbm
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
; syntax :
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,

View file

@ -1,55 +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
scl:
Greek: poc/Ancient Greek Didymus Chromatic.scl
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
; syntax :
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,

View file

@ -1,55 +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
kbm:
Greek: poc/Ancient Greek.kbm
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
; syntax :
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,

View file

@ -1,49 +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))
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
; syntax :
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,

51
poc/poc.yml Normal file
View file

@ -0,0 +1,51 @@
# fixed
instruments:
# instrument name
sine:
# fixed
expr: sin(2*PI*f*t) # instrument formula (f is the frequency in Hertz, t is the time in seconds)
square:
expr: v*abs(sin(2*PI*f*t))
# fixed
vars:
# name of the variable
v: 1 # initial value of the variable
channels:
melody:
instr: sine
score:
notes: cCdDefFgGaAb
sheet:
aabc.
'ab°A
+d+d+d---
/ff/f\\
ab>c<ba
;this is a comment (or lyrics whatever),
C:CC
(3deff)
(deff)
[ffe]
{l 1-cos((PI*x)/2),acced}
abbc!o5cc!v15feed!l4fedd!t60Gdd
# rest: .
# pizz.: '°
# volume: +-
# length: /\
# octave: ><
# comment?: ;,
# start here: ':'
# slope: {MODIFIER EXPR, score}
# note modifier prefix: n
# volume modifier prefix: v
# octave modifier prefix: o
# length modifier prefix: l
# tempo modifier prefix: t
# loop: ()
# loop with count: (COUNT, score)
# tuple: []
# modifier: !
# volume modifier prefix: v
# octave modifier prefix: o
# length modifier prefix: l
# tempo modifier prefix: t

View file

@ -7,37 +7,75 @@ 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};
use tune::scala::{Kbm, Scl};
mod de;
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(Debug))]
#[cfg_attr(debug_assertions, derive(Serialize, Debug))]
pub(super) struct Channel {
instr: String,
score: Atoms,
}
#[derive(Deserialize, Default)]
#[cfg_attr(debug_assertions, derive(new, Debug))]
#[serde(deny_unknown_fields)]
#[serde(default)]
#[derive(Deserialize)]
#[cfg_attr(debug_assertions, derive(new, Debug, Serialize))]
pub(super) struct BngFile {
instruments: HashMap<String, Instrument>,
channels: HashMap<String, Channel>,
#[serde(rename = "scl")]
scales: HashMap<String, Scale>,
#[serde(rename = "kbm")]
keyboard_mappings: HashMap<String, KeyboardMapping>,
}
#[derive(Debug, Wrapper, From)]
struct Scale(Scl);
#[derive(Debug, Wrapper, From)]
struct KeyboardMapping(Kbm);

View file

@ -1,63 +0,0 @@
use std::{fmt::Display, marker::PhantomData, str::FromStr};
use fasteval::Compiler;
use serde::{de::Visitor, Deserialize};
use super::{Expression, KeyboardMapping, Scale};
mod from_str;
struct FromStrVisitor<'a, V> {
expecting: &'a str,
phantom: PhantomData<V>,
}
impl<'a, V> FromStrVisitor<'a, V> {
fn new(expecting: &'a str) -> Self {
FromStrVisitor {
expecting,
phantom: PhantomData,
}
}
}
impl<'de, 'a, V> Visitor<'de> for FromStrVisitor<'a, V>
where
V: FromStr,
<V as FromStr>::Err: Display,
{
type Value = V;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(self.expecting)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
v.parse::<V>().map_err(serde::de::Error::custom)
}
}
macro_rules! deserialize_fromstr_expect {
($type:ty, $expect:literal) => {
impl<'de> serde::de::Deserialize<'de> for $type {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(FromStrVisitor::new($expect))
}
}
};
}
deserialize_fromstr_expect!(
Expression,
"a math expression following fasteval syntax (https://docs.rs/fasteval)"
);
deserialize_fromstr_expect!(Scale, "a .scl file containing a Scala scale");
deserialize_fromstr_expect!(
KeyboardMapping,
"a .kbm file containing a Scala keyboard mapping"
);

View file

@ -1,53 +0,0 @@
use std::{fs::File, io, str::FromStr};
use amplify::{From, Wrapper};
use fasteval::Compiler;
use thiserror::Error;
use tune::scala::{Kbm, KbmImportError, Scl, SclImportError};
use crate::bng::{KeyboardMapping, Scale};
use super::super::Expression;
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(Debug, Error)]
#[error("{0:?}")]
pub struct ScalaImportError<E>(E);
impl FromStr for Scale {
type Err = ScalaImportError<SclImportError>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Scl::import(
File::open(s)
.map_err(Into::into)
.map_err(ScalaImportError)?,
)
.map(Scale::from)
.map_err(ScalaImportError)
}
}
impl FromStr for KeyboardMapping {
type Err = ScalaImportError<KbmImportError>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Kbm::import(
File::open(s)
.map_err(Into::into)
.map_err(ScalaImportError)?,
)
.map(KeyboardMapping::from)
.map_err(ScalaImportError)
}
}

View file

@ -1,15 +1,17 @@
use std::collections::HashMap;
#[cfg(debug_assertions)]
use super::Expression as Instruction;
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(Debug))]
#[cfg_attr(debug_assertions, derive(Serialize, Debug))]
pub struct Instrument {
#[target]
expr: Instruction,
vars: Option<HashMap<String, f64>>,
vars: Option<HashMap<String, f32>>,
}

View file

@ -5,9 +5,10 @@ use std::{
use amplify::From;
use anyhow::Context;
use bng_macros::{ModifierParser, QuickModifierParser, SlopeModifierParser};
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},
@ -15,21 +16,25 @@ use nom::{
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, Clone)]
#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
#[derive(Deref, From, Default)]
#[cfg_attr(debug_assertions, derive(Serialize, Debug))]
pub struct Atoms(Vec<Atom>);
#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
@ -39,27 +44,54 @@ pub enum Atom {
StartHere,
Modifier(Modifier),
QuickModifier(QuickModifier),
Loop(NonZeroU8, Atoms),
Tuple(Atoms),
Slope(SlopeModifier, Instruction, Atoms),
Loop(NonZeroU8, Vec<Atom>),
Tuple(Vec<Atom>),
Slope(SlopeModifier, Instruction, Vec<Atom>),
Comment,
}
impl Clone for Atom {
#[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::Note(n) => Self::Note(*n),
Self::Rest => Self::Rest,
Self::StartHere => Self::StartHere,
Self::Modifier(m) => Self::Modifier(m.clone()),
Self::QuickModifier(q) => Self::QuickModifier(q.clone()),
Self::Comment => Self::Comment,
_ => unimplemented!("variant can't be cloned right now"),
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, ModifierParser)]
#[derive(Clone)]
#[cfg_attr(debug_assertions, derive(Debug, PartialEq))]
pub enum Modifier {
Volume(u8),

View file

@ -1,18 +1,10 @@
use std::{
fmt::{Debug, Display},
num::TryFromIntError,
ops::Deref,
};
use derive_new::new;
use naan::fun::{F1Once, F2Once};
use nom::{
character::complete::one_of,
combinator::all_consuming,
error::{context, convert_error, ContextError, FromExternalError, ParseError, VerboseError},
multi::{many0, many1},
sequence::{preceded, terminated},
IResult, Needed, Parser,
Parser,
};
use serde::{
de::{self, Deserializer, Visitor},
@ -20,9 +12,37 @@ use serde::{
};
use thiserror::Error;
use crate::bng::score::lex::lexer::{root, set_notes};
use crate::bng::score::lex::lexer::flat_atom_parser;
use super::{Atom, Atoms};
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>
@ -44,23 +64,37 @@ impl<'de> Deserialize<'de> for Atoms {
if sheet.is_empty() {
Ok(Default::default())
} else {
set_notes(notes).unwrap();
all_consuming(root)
.parse(&sheet)
.map_err(|e| pretty_verbose_err.curry().call1(sheet.as_str()).call1(e))
.map_err(de::Error::custom)
.map(|(i, r)| r)
flat_atom_parser_mapper::<D, _>(&sheet, flat_atom_parser(&notes))
}
}
}
fn pretty_verbose_err<I>(input: I, e: nom::Err<VerboseError<I>>) -> String
fn maybe_yml_str_space<'a, E>() -> impl Parser<&'a str, Vec<char>, E>
where
I: Deref<Target = str>,
E: nom::error::ParseError<&'a str>,
{
match e {
nom::Err::Incomplete(Needed::Unknown) => "needed some more bytes".to_string(),
nom::Err::Incomplete(Needed::Size(n)) => format!("needed {} more bytes", n),
nom::Err::Error(e) | nom::Err::Failure(e) => convert_error(input, e),
}
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,4 +1,4 @@
use super::{Atom, Modifier, QuickModifier, SlopeModifier};
use super::{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier};
pub(super) mod lexer;

View file

@ -1,230 +1,87 @@
use std::{
collections::BTreeMap,
num::{NonZeroU16, NonZeroU8, TryFromIntError},
sync::{Mutex, MutexGuard, PoisonError},
num::{NonZeroU16, NonZeroU8},
};
use clap::builder::TypedValueParser;
use const_format::concatcp;
use fasteval::Compiler;
use lazy_static::lazy_static;
use nom::{
branch::alt,
bytes::complete::{is_not, tag, take_till, take_till1},
bytes::complete::{take_till, take_till1},
character::complete::{anychar, char, one_of, space1, u16, u8},
combinator::{all_consuming, cut, eof, map_opt, map_res, not, opt, value, verify},
error::{context, ContextError, ErrorKind, FromExternalError, ParseError},
multi::{many0, many1, many_till},
combinator::{map_opt, map_res, opt, value, verify},
multi::many0,
sequence::{delimited, pair, preceded, separated_pair, terminated},
Err, IResult, Parser,
};
use crate::bng::score::Atoms;
use super::{
super::super::Expression as Instruction, Atom, Modifier, QuickModifier, SlopeModifier,
super::super::Expression as Instruction,
{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier},
};
#[cfg(test)]
mod tests;
lazy_static! {
static ref NOTES: Mutex<Option<String>> = Mutex::new(None);
pub(crate) trait Parse: Sized {
fn parse(input: &str) -> IResult<&str, Self>;
}
pub fn set_notes(
notes: impl ToString,
) -> Result<Option<String>, PoisonError<MutexGuard<'static, Option<String>>>> {
Ok(NOTES.lock()?.replace(notes.to_string()))
}
fn maybe_yml_str_space<'a, E>(i: &'a str) -> IResult<&'a str, Vec<char>, E>
where
E: nom::error::ParseError<&'a str> + ContextError<&'a str>,
{
context("yml white space", many0(one_of(" \t\r\n"))).parse(i)
}
pub fn root<'a, E>(i: &'a str) -> IResult<&'a str, Atoms, E>
where
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, TryFromIntError>
+ FromExternalError<&'a str, E>,
{
many0(delimited(maybe_yml_str_space, atom, maybe_yml_str_space))
.map(Atoms)
.parse(i)
}
fn note<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str> + ContextError<&'a str> + FromExternalError<&'a str, TryFromIntError>,
{
if let Some(notes) = NOTES.lock().unwrap().as_deref() {
context(
"note",
map_res(map_opt(one_of(notes), |c| notes.find(c)), u8::try_from),
)
.map(Atom::Note)
.parse(i)
} else {
panic!("trouble locking the note set!")
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)
}
}
fn rest<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str> + ContextError<&'a str>,
{
value(Atom::Rest, context("rest", char(Atom::REST))).parse(i)
}
fn start_here<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str> + ContextError<&'a str>,
{
value(
Atom::StartHere,
context("start_here", char(Atom::START_HERE)),
)
.parse(i)
}
fn modifier<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str> + ContextError<&'a str>,
{
use super::super::modifier;
context("modifier", preceded(char(Atom::MODIFIER), cut(modifier)))
.map(Atom::Modifier)
.parse(i)
}
fn quick_modifier<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str> + ContextError<&'a str>,
{
use super::super::quick_modifier;
context("quick_modifier", quick_modifier)
.map(Atom::QuickModifier)
.parse(i)
}
fn r#loop<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, TryFromIntError>
+ FromExternalError<&'a str, E>,
{
context(
"loop",
delimited(
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),
cut(pair(
map_opt(opt(u8), |n| {
if let Some(n) = n {
NonZeroU8::new(n)
} else {
unsafe { Some(NonZeroU8::new_unchecked(2)) }
}
}),
root,
)),
cut(char(Atom::LOOP.1)),
),
)
.map(|(n, v)| Atom::Loop(n, v))
.parse(i)
}
fn tuple<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, TryFromIntError>
+ FromExternalError<&'a str, E>,
{
context(
"tuple",
delimited(char(Atom::TUPLE.0), cut(root), cut(char(Atom::TUPLE.1))),
)
.map(Atom::Tuple)
.parse(i)
}
fn slope<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, TryFromIntError>
+ FromExternalError<&'a str, E>,
{
use super::super::slope_modifier;
context(
"slope_starts",
delimited(
char(Atom::SLOPE.0),
cut(separated_pair(
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(
slope_modifier,
SlopeModifier::parse,
char(' '),
map_res(take_till1(|c| c == ','), |s: &str| {
s.parse()
.map_err(|_| E::from_error_kind(s, ErrorKind::Verify))
.map_err(|_| nom::error::Error::new(s, nom::error::ErrorKind::Verify))
}),
),
char(','),
root,
)),
cut(char(Atom::SLOPE.1)),
),
)
.map(|((sm, i), v)| Atom::Slope(sm, i, v))
.parse(i)
}
fn comment<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str> + ContextError<&'a str>,
{
context(
"comment",
),
char(','),
)
.map(|(sm, i)| FlatAtom::SlopeStarts(sm, i)),
value(FlatAtom::SlopeEnds, char(Atom::SLOPE.1)),
value(
Atom::Comment,
FlatAtom::Comment,
delimited(
char(Atom::COMMENT.0),
many0(alt((
value((), comment),
value((), is_not(concatcp!(Atom::COMMENT.0, Atom::COMMENT.1))),
))),
cut(char(Atom::COMMENT.1)),
take_till(|c| c == Atom::COMMENT.1),
char(Atom::COMMENT.1),
),
),
)(i)
}
fn atom<'a, E>(i: &'a str) -> IResult<&'a str, Atom, E>
where
E: ParseError<&'a str>
+ ContextError<&'a str>
+ FromExternalError<&'a str, TryFromIntError>
+ FromExternalError<&'a str, E>,
{
context(
"atom",
alt((
comment,
start_here,
modifier,
quick_modifier,
r#loop,
tuple,
slope,
rest,
note,
)),
)
.parse(i)
))
}

View file

@ -1,6 +1,6 @@
use const_format::concatcp;
use nom::{
error::{Error, ErrorKind, VerboseError},
error::{Error, ErrorKind},
Err,
};
@ -8,24 +8,15 @@ use flat_atom::{
FASTEVAL_INSTRUCTION as FLATATOM_FASTEVAL_INSTRUCTION, SAMPLE_STR as FLATATOM_SAMPLE_STRING,
};
fn set_notes() {
super::set_notes("abcdefg").unwrap();
}
type NomError<'a> = VerboseError<&'a str>;
mod flat_atom {
use std::num::NonZeroU8;
use fasteval::Compiler;
use lazy_static::lazy_static;
use nom::Parser;
use crate::bng::score::Atoms;
use super::super::{
super::super::super::Expression as Instruction, super::UP, atom, Atom, Modifier,
QuickModifier, SlopeModifier,
super::super::super::Expression as Instruction, super::UP, flat_atom_parser, Atom,
FlatAtom, Modifier, QuickModifier, SlopeModifier,
};
use super::*;
@ -35,43 +26,32 @@ mod flat_atom {
Atom::TUPLE.1,
"ed",
Atom::COMMENT.0,
"hello",
Atom::COMMENT.1
"hello"
);
pub(super) fn sample_str_expr() -> Atoms {
Atoms(vec![
Atom::Tuple(Atoms(vec![Atom::Note(0), Atom::Note(2), Atom::Note(2)])),
Atom::Note(4),
Atom::Note(3),
Atom::Comment,
])
}
pub(super) const FASTEVAL_INSTRUCTION: &str = "1-cos((PI*x)/2)";
#[test]
fn note() {
set_notes();
assert_eq!(
Ok((SAMPLE_STR, Atom::Note(2))),
atom::<NomError>.parse(concatcp!('c', SAMPLE_STR))
Ok((SAMPLE_STR, FlatAtom::Note(2))),
flat_atom_parser("abcdefg").parse(concatcp!('c', SAMPLE_STR))
)
}
#[test]
fn rest() {
assert_eq!(
Ok((SAMPLE_STR, Atom::Rest)),
atom::<NomError>.parse(concatcp!(Atom::REST, SAMPLE_STR))
Ok((SAMPLE_STR, FlatAtom::Rest)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::REST, SAMPLE_STR))
)
}
#[test]
fn start_here() {
assert_eq!(
Ok((SAMPLE_STR, Atom::StartHere)),
atom::<NomError>.parse(concatcp!(Atom::START_HERE, SAMPLE_STR))
Ok((SAMPLE_STR, FlatAtom::StartHere)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::START_HERE, SAMPLE_STR))
)
}
@ -80,102 +60,108 @@ mod flat_atom {
assert_eq!(
Ok((
SAMPLE_STR,
Atom::Modifier(Modifier::Length(unsafe { NonZeroU8::new_unchecked(2) }))
FlatAtom::Modifier(Modifier::Length(unsafe { NonZeroU8::new_unchecked(2) }))
)),
atom::<NomError>.parse(concatcp!(Atom::MODIFIER, Modifier::LENGTH, 2u8, SAMPLE_STR))
flat_atom_parser("abcdefg").parse(concatcp!(
Atom::MODIFIER,
Modifier::LENGTH,
2u8,
SAMPLE_STR
))
)
}
#[test]
fn quick_modifier() {
assert_eq!(
Ok((SAMPLE_STR, Atom::QuickModifier(QuickModifier::Length(UP)))),
atom::<NomError>.parse(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR))
Ok((
SAMPLE_STR,
FlatAtom::QuickModifier(QuickModifier::Length(UP))
)),
flat_atom_parser("abcdefg").parse(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR))
)
}
#[test]
fn r#loop() {
set_notes();
fn loop_starts() {
assert_eq!(
Ok((
SAMPLE_STR,
Atom::Loop(unsafe { NonZeroU8::new_unchecked(3) }, sample_str_expr())
FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(3) })
)),
atom::<NomError>.parse(concatcp!(
Atom::LOOP.0,
3u8,
SAMPLE_STR,
Atom::LOOP.1,
SAMPLE_STR
))
flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.0, 3u8, SAMPLE_STR))
);
assert_eq!(
Ok((
SAMPLE_STR,
Atom::Loop(unsafe { NonZeroU8::new_unchecked(2) }, sample_str_expr())
FlatAtom::LoopStarts(unsafe { NonZeroU8::new_unchecked(2) })
)),
atom::<NomError>.parse(concatcp!(
Atom::LOOP.0,
SAMPLE_STR,
Atom::LOOP.1,
SAMPLE_STR
))
flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.0, SAMPLE_STR))
);
assert_eq!(
Err(nom::Err::Failure(Error::new(
concatcp!(0u8, SAMPLE_STR),
ErrorKind::MapOpt
Err(nom::Err::Error(Error::new(
concatcp!(Atom::LOOP.0, 0u8, SAMPLE_STR),
ErrorKind::Char
))),
atom.parse(concatcp!(Atom::LOOP.0, 0u8, SAMPLE_STR))
flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.0, 0u8, SAMPLE_STR))
)
}
#[test]
fn tuple() {
set_notes();
fn loop_ends() {
assert_eq!(
Ok((SAMPLE_STR, Atom::Tuple(sample_str_expr()))),
atom::<NomError>.parse(concatcp!(
Atom::TUPLE.0,
SAMPLE_STR,
Atom::TUPLE.1,
SAMPLE_STR
))
Ok((SAMPLE_STR, FlatAtom::LoopEnds)),
flat_atom_parser("abcdefg").parse(concatcp!(Atom::LOOP.1, SAMPLE_STR))
)
}
#[test]
fn slope() {
set_notes();
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,
Atom::Slope(
SlopeModifier::Note,
FASTEVAL_INSTRUCTION.parse().unwrap(),
sample_str_expr()
)
FlatAtom::SlopeStarts(SlopeModifier::Note, FASTEVAL_INSTRUCTION.parse().unwrap())
)),
atom::<NomError>.parse(concatcp!(
flat_atom_parser("abcdefg").parse(concatcp!(
Atom::SLOPE.0,
SlopeModifier::NOTE,
' ',
FASTEVAL_INSTRUCTION,
',',
SAMPLE_STR,
Atom::SLOPE.1,
SAMPLE_STR
))
)
}
#[test]
fn comment() {
set_notes();
fn slope_ends() {
assert_eq!(
Ok((SAMPLE_STR, Atom::Comment)),
atom::<NomError>.parse(concatcp!(
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,
@ -184,38 +170,27 @@ mod flat_atom {
))
)
}
#[test]
fn nested_comments() {
set_notes();
assert_eq!(
Ok(("", Atom::Comment)),
atom::<NomError>.parse(";d;;dd,ef,,")
)
}
}
mod modifier {
use std::num::{NonZeroU16, NonZeroU8};
use const_format::concatcp;
use nom::error::{Error, ErrorKind, VerboseError};
use super::{NomError, FLATATOM_SAMPLE_STRING as SAMPLE_STR};
use crate::bng::score::{modifier, Modifier};
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::<NomError>(concatcp!(Modifier::VOLUME, 2u8, SAMPLE_STR))
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(concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR))
Modifier::parse(concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR))
);
}
@ -223,14 +198,14 @@ mod modifier {
fn octave() {
assert_eq!(
Ok((SAMPLE_STR, Modifier::Octave(2))),
modifier::<NomError>(concatcp!(Modifier::OCTAVE, 2u8, SAMPLE_STR))
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(concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR))
Modifier::parse(concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR))
);
}
@ -241,21 +216,21 @@ mod modifier {
SAMPLE_STR,
Modifier::Length(unsafe { NonZeroU8::new_unchecked(2) })
)),
modifier::<NomError>(concatcp!(Modifier::LENGTH, 2u8, SAMPLE_STR))
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(concatcp!(Modifier::LENGTH, 2556u16, SAMPLE_STR))
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(concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR))
Modifier::parse(concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR))
);
}
@ -266,37 +241,35 @@ mod modifier {
SAMPLE_STR,
Modifier::Tempo(unsafe { NonZeroU16::new_unchecked(2) })
)),
modifier::<NomError>(concatcp!(Modifier::TEMPO, 2u8, SAMPLE_STR))
Modifier::parse(concatcp!(Modifier::TEMPO, 2u8, SAMPLE_STR))
);
assert_eq!(
Err(nom::Err::Error(Error::new(
concatcp!(655353u32, SAMPLE_STR),
ErrorKind::Digit
))),
modifier(concatcp!(Modifier::TEMPO, 655353u32, SAMPLE_STR))
Modifier::parse(concatcp!(Modifier::TEMPO, 655353u32, SAMPLE_STR))
);
}
}
mod quick_modifier {
use const_format::concatcp;
use nom::error::VerboseError;
use super::{NomError, FLATATOM_SAMPLE_STRING as SAMPLE_STR};
use super::FLATATOM_SAMPLE_STRING as SAMPLE_STR;
use super::*;
use crate::bng::score::{
lex::{DOWN, OFF, ON, UP},
quick_modifier, QuickModifier,
lex::{lexer::Parse, DOWN, OFF, ON, UP},
QuickModifier,
};
#[test]
fn volume() {
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Volume(UP))),
quick_modifier::<NomError>(concatcp!(QuickModifier::VOLUME.0, SAMPLE_STR))
QuickModifier::parse(concatcp!(QuickModifier::VOLUME.0, SAMPLE_STR))
);
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Volume(DOWN))),
quick_modifier::<NomError>(concatcp!(QuickModifier::VOLUME.1, SAMPLE_STR))
QuickModifier::parse(concatcp!(QuickModifier::VOLUME.1, SAMPLE_STR))
);
}
@ -304,11 +277,11 @@ mod quick_modifier {
fn octave() {
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Octave(UP))),
quick_modifier::<NomError>(concatcp!(QuickModifier::OCTAVE.0, SAMPLE_STR))
QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.0, SAMPLE_STR))
);
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Octave(DOWN))),
quick_modifier::<NomError>(concatcp!(QuickModifier::OCTAVE.1, SAMPLE_STR))
QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.1, SAMPLE_STR))
);
}
@ -316,11 +289,11 @@ mod quick_modifier {
fn length() {
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Length(UP))),
quick_modifier::<NomError>(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR))
QuickModifier::parse(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR))
);
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Length(DOWN))),
quick_modifier::<NomError>(concatcp!(QuickModifier::LENGTH.1, SAMPLE_STR))
QuickModifier::parse(concatcp!(QuickModifier::LENGTH.1, SAMPLE_STR))
);
}
@ -328,22 +301,20 @@ mod quick_modifier {
fn pizz() {
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Pizz(ON))),
quick_modifier::<NomError>(concatcp!(QuickModifier::PIZZ.0, SAMPLE_STR))
QuickModifier::parse(concatcp!(QuickModifier::PIZZ.0, SAMPLE_STR))
);
assert_eq!(
Ok((SAMPLE_STR, QuickModifier::Pizz(OFF))),
quick_modifier::<NomError>(concatcp!(QuickModifier::PIZZ.1, SAMPLE_STR))
QuickModifier::parse(concatcp!(QuickModifier::PIZZ.1, SAMPLE_STR))
);
}
}
#[cfg(test)]
mod slope_modifier {
use const_format::concatcp;
use nom::error::VerboseError;
use super::{NomError, FLATATOM_FASTEVAL_INSTRUCTION as INSTRUCTION};
use crate::bng::score::{slope_modifier, Atom, SlopeModifier};
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);
@ -351,7 +322,7 @@ mod slope_modifier {
fn note() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Note)),
slope_modifier::<NomError>(concatcp!(SlopeModifier::NOTE, SAMPLE_STR))
SlopeModifier::parse(concatcp!(SlopeModifier::NOTE, SAMPLE_STR))
)
}
@ -359,7 +330,7 @@ mod slope_modifier {
fn volume() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Volume)),
slope_modifier::<NomError>(concatcp!(SlopeModifier::VOLUME, SAMPLE_STR))
SlopeModifier::parse(concatcp!(SlopeModifier::VOLUME, SAMPLE_STR))
)
}
@ -367,7 +338,7 @@ mod slope_modifier {
fn octave() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Octave)),
slope_modifier::<NomError>(concatcp!(SlopeModifier::OCTAVE, SAMPLE_STR))
SlopeModifier::parse(concatcp!(SlopeModifier::OCTAVE, SAMPLE_STR))
)
}
@ -375,7 +346,7 @@ mod slope_modifier {
fn length() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Length)),
slope_modifier::<NomError>(concatcp!(SlopeModifier::LENGTH, SAMPLE_STR))
SlopeModifier::parse(concatcp!(SlopeModifier::LENGTH, SAMPLE_STR))
)
}
@ -383,7 +354,7 @@ mod slope_modifier {
fn tempo() {
assert_eq!(
Ok((SAMPLE_STR, SlopeModifier::Tempo)),
slope_modifier::<NomError>(concatcp!(SlopeModifier::TEMPO, SAMPLE_STR))
SlopeModifier::parse(concatcp!(SlopeModifier::TEMPO, SAMPLE_STR))
)
}
}

236
src/bng/score/utils.rs Normal file
View file

@ -0,0 +1,236 @@
use super::*;
use strum::Display;
use thiserror::Error;
#[cfg(test)]
mod tests;
#[derive(Debug, EnumDiscriminants)]
#[strum_discriminants(derive(Display))]
pub enum Wrapper {
Loop(NonZeroU8),
Tuple,
Slope(SlopeModifier, Instruction),
}
#[derive(Debug, Error)]
#[cfg_attr(test, derive(PartialEq))]
pub enum InflateError {
#[error("misplaced {0} end symbol")]
MismatchedEnd(WrapperDiscriminants),
}
pub fn inflate(mut flat_atoms: Vec<FlatAtom>) -> Result<Vec<Atom>, InflateError> {
type Error = InflateError;
let mut result = Vec::with_capacity(flat_atoms.len());
let mut loop_stack: Vec<Vec<Atom>> = Vec::new();
let mut tuple_stack: Vec<Vec<Atom>> = Vec::new();
let mut slope_stack: Vec<Vec<Atom>> = Vec::new();
let mut stack_history: Vec<Wrapper> = Vec::new();
for mut atom in flat_atoms.into_iter() {
#[cfg(test)]
{
dbg!(&atom);
dbg!(&loop_stack);
dbg!(&tuple_stack);
dbg!(&slope_stack);
dbg!(&stack_history);
}
match stack_history.last().map(WrapperDiscriminants::from) {
Some(WrapperDiscriminants::Loop) => match atom {
FlatAtom::Note(n) => {
unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::Note(n))
}
FlatAtom::Rest => {
unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::Rest)
}
FlatAtom::StartHere => {
unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::StartHere)
}
FlatAtom::Modifier(m) => {
unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::Modifier(m))
}
FlatAtom::QuickModifier(q) => {
unsafe { loop_stack.last_mut().unwrap_unchecked() }.push(Atom::QuickModifier(q))
}
FlatAtom::LoopStarts(n) => {
loop_stack.push(Vec::new());
stack_history.push(Wrapper::Loop(n));
}
FlatAtom::LoopEnds => {
let popped = unsafe { loop_stack.pop().unwrap_unchecked() };
if stack_history.len() > 1 {
match WrapperDiscriminants::from(
stack_history.get(stack_history.len() - 2).unwrap(),
) {
WrapperDiscriminants::Loop => &mut loop_stack,
WrapperDiscriminants::Tuple => &mut tuple_stack,
WrapperDiscriminants::Slope => &mut slope_stack,
}
.last_mut()
.unwrap()
.push(Atom::Loop(
match stack_history.pop().unwrap() {
Wrapper::Loop(n) => n,
_ => unreachable!("this one is proven to be a loop"),
},
popped,
))
} else {
result.push(Atom::Loop(
match stack_history.pop().unwrap() {
Wrapper::Loop(n) => n,
_ => unreachable!("this one is proven to be a loop"),
},
popped,
))
}
}
FlatAtom::TupleStarts => {
tuple_stack.push(Vec::new());
stack_history.push(Wrapper::Tuple);
}
FlatAtom::TupleEnds => {
return Err(Error::MismatchedEnd(WrapperDiscriminants::Tuple));
}
FlatAtom::SlopeStarts(s, i) => {
slope_stack.push(Vec::new());
stack_history.push(Wrapper::Slope(s, i));
}
FlatAtom::SlopeEnds => {
return Err(Error::MismatchedEnd(WrapperDiscriminants::Slope));
}
FlatAtom::Comment => loop_stack.last_mut().unwrap().push(Atom::Comment),
},
Some(WrapperDiscriminants::Tuple) => match atom {
FlatAtom::Note(n) => tuple_stack.last_mut().unwrap().push(Atom::Note(n)),
FlatAtom::Rest => tuple_stack.last_mut().unwrap().push(Atom::Rest),
FlatAtom::StartHere => tuple_stack.last_mut().unwrap().push(Atom::StartHere),
FlatAtom::Modifier(m) => tuple_stack.last_mut().unwrap().push(Atom::Modifier(m)),
FlatAtom::QuickModifier(q) => {
tuple_stack.last_mut().unwrap().push(Atom::QuickModifier(q))
}
FlatAtom::LoopStarts(n) => {
loop_stack.push(Vec::new());
stack_history.push(Wrapper::Loop(n));
}
FlatAtom::LoopEnds => {
return Err(Error::MismatchedEnd(WrapperDiscriminants::Loop));
}
FlatAtom::TupleStarts => {
tuple_stack.push(Vec::new());
stack_history.push(Wrapper::Tuple);
}
FlatAtom::TupleEnds => {
let popped = tuple_stack.pop().unwrap();
if stack_history.len() > 1 {
match WrapperDiscriminants::from(
stack_history.get(stack_history.len() - 2).unwrap(),
) {
WrapperDiscriminants::Loop => &mut loop_stack,
WrapperDiscriminants::Tuple => &mut tuple_stack,
WrapperDiscriminants::Slope => &mut slope_stack,
}
.last_mut()
.unwrap()
.push({
stack_history.pop();
Atom::Tuple(popped)
})
} else {
result.push(Atom::Tuple(popped))
}
}
FlatAtom::SlopeStarts(s, i) => {
slope_stack.push(Vec::new());
stack_history.push(Wrapper::Slope(s, i));
}
FlatAtom::SlopeEnds => {
return Err(Error::MismatchedEnd(WrapperDiscriminants::Slope));
}
FlatAtom::Comment => tuple_stack.last_mut().unwrap().push(Atom::Comment),
},
Some(WrapperDiscriminants::Slope) => match atom {
FlatAtom::Note(n) => slope_stack.last_mut().unwrap().push(Atom::Note(n)),
FlatAtom::Rest => slope_stack.last_mut().unwrap().push(Atom::Rest),
FlatAtom::StartHere => slope_stack.last_mut().unwrap().push(Atom::StartHere),
FlatAtom::Modifier(m) => slope_stack.last_mut().unwrap().push(Atom::Modifier(m)),
FlatAtom::QuickModifier(q) => {
slope_stack.last_mut().unwrap().push(Atom::QuickModifier(q))
}
FlatAtom::LoopStarts(n) => {
loop_stack.push(Vec::new());
stack_history.push(Wrapper::Loop(n));
}
FlatAtom::LoopEnds => {
return Err(Error::MismatchedEnd(WrapperDiscriminants::Loop));
}
FlatAtom::TupleStarts => {
tuple_stack.push(Vec::new());
stack_history.push(Wrapper::Tuple);
}
FlatAtom::TupleEnds => {
return Err(Error::MismatchedEnd(WrapperDiscriminants::Tuple));
}
FlatAtom::SlopeStarts(s, i) => {
slope_stack.push(Vec::new());
stack_history.push(Wrapper::Slope(s, i));
}
FlatAtom::SlopeEnds => {
let popped = slope_stack.pop().unwrap();
if stack_history.len() > 1 {
match WrapperDiscriminants::from(
stack_history.get(stack_history.len() - 2).unwrap(),
) {
WrapperDiscriminants::Loop => &mut loop_stack,
WrapperDiscriminants::Tuple => &mut tuple_stack,
WrapperDiscriminants::Slope => &mut slope_stack,
}
.last_mut()
.unwrap()
.push(match stack_history.pop().unwrap() {
Wrapper::Slope(m, i) => Atom::Slope(m, i, popped),
_ => unreachable!("this one is proven to be a slope"),
})
} else {
result.push(match stack_history.pop().unwrap() {
Wrapper::Slope(m, i) => Atom::Slope(m, i, popped),
_ => unreachable!("this one is proven to be a slope"),
})
}
}
FlatAtom::Comment => slope_stack.last_mut().unwrap().push(Atom::Comment),
},
None => match atom {
FlatAtom::Note(n) => result.push(Atom::Note(n)),
FlatAtom::Rest => result.push(Atom::Rest),
FlatAtom::StartHere => result.push(Atom::StartHere),
FlatAtom::Modifier(m) => result.push(Atom::Modifier(m)),
FlatAtom::QuickModifier(q) => result.push(Atom::QuickModifier(q)),
FlatAtom::LoopStarts(n) => {
loop_stack.push(Vec::new());
stack_history.push(Wrapper::Loop(n));
}
FlatAtom::LoopEnds => {
return Err(Error::MismatchedEnd(WrapperDiscriminants::Loop));
}
FlatAtom::TupleStarts => {
tuple_stack.push(Vec::new());
stack_history.push(Wrapper::Tuple);
}
FlatAtom::TupleEnds => {
return Err(Error::MismatchedEnd(WrapperDiscriminants::Tuple));
}
FlatAtom::SlopeStarts(s, i) => {
slope_stack.push(Vec::new());
stack_history.push(Wrapper::Slope(s, i));
}
FlatAtom::SlopeEnds => {
return Err(Error::MismatchedEnd(WrapperDiscriminants::Slope));
}
FlatAtom::Comment => result.push(Atom::Comment),
},
}
}
Ok(result)
}

View file

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

View file

@ -1,10 +1,7 @@
use std::{convert::Infallible, fmt::Display, fs::read_to_string, io, str::FromStr};
use std::{fmt::Display, fs::read_to_string, io, str::FromStr};
use amplify::{From, Wrapper};
use clap::Parser;
use strum::EnumString;
pub mod doc;
/// Cli entry point
#[derive(Clone, Parser)]
@ -12,9 +9,9 @@ pub mod doc;
pub enum BngCli {
/// Play the song through default sink
Play(PlayOpts),
/// Export the song to a sound file
/// Export the song to a sound FileContents
Export(ExportOpts),
/// List supported sound file extensions and instrument / song available expressions
/// List supported sound FileContents extensions and instrument / song available expressions
#[command(subcommand)]
List(ListOpts),
}
@ -31,10 +28,10 @@ pub struct PlayOpts {
#[derive(Clone, Parser)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct ExportOpts {
/// Input file (written song file)
/// Input FileContents (written song FileContents)
#[arg(value_parser = FileContents::from_str)]
input: FileContents,
/// Output file (sound file)
/// Output FileContents (sound FileContents)
#[arg(value_parser = AudioFileName::from_str)]
output: AudioFileName,
}
@ -43,29 +40,17 @@ pub struct ExportOpts {
#[derive(Clone, Parser)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub enum ListOpts {
/// List supported sound file extensions to export songs
/// List supported sound FileContents extensions to export songs
#[command(subcommand)]
Extensions,
/// Show the math syntax used for instrument definition and slopes
Math {
/// Kind of syntax you want to list (functions, operators or literals)
#[arg(value_parser = MathDocKind::from_str)]
kind: Option<MathDocKind>,
},
/// List available math expressions for instrument definition
#[command(subcommand)]
Math,
/// List available score glyphs and their meaning
#[command(subcommand)]
Glyphs,
}
#[derive(Clone, Copy, EnumString)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub enum MathDocKind {
#[strum(ascii_case_insensitive)]
Functions,
#[strum(ascii_case_insensitive)]
Operators,
#[strum(ascii_case_insensitive)]
Literals,
}
#[derive(Clone, Wrapper, From)]
#[wrapper(Deref)]
#[cfg_attr(debug_assertions, derive(Debug))]

View file

@ -1,98 +0,0 @@
pub mod math {
pub const FUNCTIONS: &str = r#"* 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)"#;
pub const OPERATORS: &str = r#"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"#;
pub const LITERALS: &str = r#"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"#;
}
pub mod glyphs {
pub const ALL: &str = r#"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
EXAMPLE
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"#;
}

View file

@ -4,7 +4,7 @@ use anyhow::Error;
use bng::{BngFile, Channel, Expression, Instrument};
/// TODO: remove clap, use only a file or standard in
use clap::Parser;
use cli::{doc, BngCli as Cli, ListOpts, MathDocKind, PlayOpts};
use cli::{BngCli as Cli, PlayOpts};
use fasteval::Compiler;
mod bng;
@ -24,27 +24,30 @@ fn main() -> Result<(), Error> {
#[cfg(debug_assertions)]
println!("{:#?}", bng_file);
}
Cli::List(l) => match l {
ListOpts::Extensions => println!("you can't export songs yet"),
ListOpts::Math { kind } => match kind {
None => println!(
"Functions :\n{}\n\nOperators :\n{}\n\nLiterals :\n{}",
doc::math::FUNCTIONS,
doc::math::OPERATORS,
doc::math::LITERALS
),
Some(m) => println!(
"{}",
match m {
MathDocKind::Functions => doc::math::FUNCTIONS,
MathDocKind::Operators => doc::math::OPERATORS,
MathDocKind::Literals => doc::math::LITERALS,
}
),
},
ListOpts::Glyphs => println!("{}", doc::glyphs::ALL),
},
Cli::Export(_) => unimplemented!("can't do that yet"),
_ => 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()),
)]),
)
}