Compare commits

..

1 commit
main ... dirty

Author SHA1 Message Date
Breval Ferrari
1f2d23e051
g 2025-06-01 23:33:58 +02:00
21 changed files with 302 additions and 1997 deletions

19
.gitignore vendored
View file

@ -22,22 +22,3 @@ Cargo.lock
#.idea/
out/
# Flamegraph
*.old
*.data
# Samply
*.json.gz
# audio files
*.mp3
*.raw
*.wav
*.flac
# VSCode
.vscode/settings.json
# LibreOffice
*\#

View file

@ -1,20 +1,20 @@
[package]
name = "bliplib"
version = "0.2.5"
version = "0.0.2"
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"]
include = ["LICENSE", "src/*"]
repository = "https://gitdab.com/breval/blip"
[lib]
name = "bliplib"
[dependencies]
anyhow = { version = "1.0" }
anyhow = { version = "1.0", optional = true }
cfg-if = "1"
clap = { version = "4.5.39", features = ["derive"], optional = true }
clap = { version = "4.5.38", features = ["derive"], optional = true }
derive-new = "0.7"
derive_builder = "0.20"
derive_wrapper = "0.1"
@ -29,15 +29,10 @@ nom_locate = "5.0"
raw_audio = { version = "0.0", optional = true }
strum = { version = "0.27", features = ["derive"] }
thiserror = "2.0"
rodio = { version = "0.20", default-features = false, optional = true }
dasp_sample = { version = "0", optional = true }
log = "0"
env_logger = { version = "0", optional = true }
fon = "0.5"
[features]
default = ["all-formats"]
bin = ["clap", "rodio", "dasp_sample", "env_logger"]
default = ["bin", "all-formats"]
bin = ["anyhow", "clap"]
all-formats = ["mp3", "wav", "flac", "raw"]
mp3 = ["mp3lame-encoder"]
wav = ["hound"]
@ -48,12 +43,3 @@ raw = ["raw_audio"]
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]
missing_docs = "warn"

View file

@ -1,11 +1,9 @@
<img src="https://gitdab.com/breval/blip/raw/branch/main/doc/iconx4.png" alt="bppt logo" align="right">
# blip
[![Crates.io](https://img.shields.io/crates/v/bliplib.svg)](https://crates.io/crates/bliplib)
[![Docs.rs](https://docs.rs/bliplib/badge.svg)](https://docs.rs/bliplib)
[![License: MIT](https://img.shields.io/crates/l/bliplib)](LICENSE)
# blip
A better and more flexible [Bleeperpreter](https://github.com/p6nj/bleeperpreter) from scratch.
| | the |
@ -14,7 +12,3 @@ A better and more flexible [Bleeperpreter](https://github.com/p6nj/bleeperpreter
| L | Language for |
| I | Intermodulation |
| P | Programming |
```sh
cargo install bliplib --features bin
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -1,8 +1,8 @@
notes: dorémi abc... as they appear in the note list
variables: $v8 assign '8' to the variable named 'v'
variables: $v8
$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)
$v-4.1 as soon as the variable mutation string stops being valid, usual sheet music parsing continues
loops: (...)
(2...) to loop 2 times
(n...) to loop n times (taking the integer part with the floor function)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 167 KiB

View file

@ -1,108 +0,0 @@
# bliplib <!-- omit from toc -->
This is the core library for the Bizarre Language for Intermodulation Programming (BLIP).
This crate also contains the binary `blip`.
BLIP is a music scripting language made by nerds for nerds.
It uses math and physics in a modular grammar to generate sound your way.
- [Design](#design)
- [History](#history)
- [Features](#features)
- [Your notes](#your-notes)
- [Your variables](#your-variables)
- [Your own instrument](#your-own-instrument)
- [Time is yours](#time-is-yours)
- [Defaults](#defaults)
- [Expressions](#expressions)
- [Grammar](#grammar)
- [Two wolves](#two-wolves)
- [Binary](#binary)
- [Lib](#lib)
- [Thanks](#thanks)
## Design
### History
Computer assisted music production started from the 50's to the 70's and got popular when computers became cheaper and more powerful. A lot of new tools gave us new ways of making music. As any tool, they also shaped the art we made with it in many ways. For example, most Digital Audio Workstations (DAWs) use "patterns" and allow to easily compose a song with drag-and-drop loops. This made our music easily more repetitive and contributed to completely new genres. A lot of electronic dance genres would probably not exist without this technique which was inspired by common artistic repetition techniques used across all modern history.
Today, the diversity and availability of these tools seems more than ever linked to the diversity of our art styles because we can achieve so much more with a computer that we rely a lot on these tools. So by contributing to the world of computer assisted music production tools, we directly contribute to our artistic diversity.
My first steps into this rabbit hole started when I found [BeepComp](http://hiromorozumi.com/beepcomp/), an editor for a custom [MML](https://en.wikipedia.org/wiki/Music_Macro_Language) variant. I became fascinated by the software but even more by this new way of thinking about music. Languages all have their own logic and constraints. [MIDI](https://en.wikipedia.org/wiki/MIDI) for example comes with a language that is very precise but almost impossible to write by hand (it's made of bytes) and the standard implies a specific bank of pre-generated sounds and natively only works with a standard set of 12 notes.
### Features
The main idea behind this language is to free constraints imposed by other similar languages, starting with the note set.
#### Your notes
As such, you specify how many notes fit in your set and what their names are.
Now, if you're familiar with music, you may have noticed I say "set" and not "octave". This is also intentional : you decide if you want octaves or not, and how they act on the frequencies of your notes or on the generated sound globally.
#### Your variables
If you want octaves, you can make a variable store the value of the current octave. You can define any variable and use them to change any parametter of your current song.
#### Your own instrument
There is no imposed sound bank for you to choose from. You make your own. On computers, audio is just a bunch of points with amplitudes, so you give the program a mathematical expression. Everytime you want sound to be generated, your expression will be evaluated with the current variables.
> Don't worry, the binary includes default expressions for everything. You can override them whenever you want to.
#### Time is yours
Standard [MML](https://en.wikipedia.org/wiki/Music_Macro_Language) variations impose a value for a length but they don't all agree (4 means a quarter note? or is it 4 seconds? what about the tempo?). BLIP doesn't know how much time any of your notes last. You give another mathematical expression that outputs a number of seconds for a note to last using the current values of your variables.
#### Defaults
The BLIP library needs one default variable for the elapsed time in seconds, another one for the note index of the current note played in your set of notes and a last one for the length of notes in seconds, filled by the length expression. The binary program uses `'t'` for time, `'n'` for the current note index and `'L'` for note lengths in seconds.
It also includes :
- `sin(2*pi()*(442*2^((n+1)/N))*t)` for the default instrument
- `2^(2-log(2, l))*(60/T)` for the default length expression
As well as some variables :
| var | value | meaning |
| --- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| l | 4 | length of the notes ([BeepComp style](http://www.hiromorozumi.com/beepcomp/documentation/beepcomp_users_guide.html#music_section_note_length)) |
| L | 0 | length of the notes in seconds (filled by the length expression) |
| t | 0 | elapsed time in seconds |
| T | 60 | tempo (beats per minute / BPM) used in the length expression |
| N | number of notes in the set | used to divide the frequencies for all the notes in the octave (instrument expression) |
#### Expressions
BLIP uses [`fasteval`](https://crates.io/crates/fasteval). Check out its [syntax](https://docs.rs/fasteval/latest/fasteval/#the-fasteval-expression-mini-language) to know what you can put in your expression. This is also available in the `memo` menu of the program.
### Grammar
For the language grammar, see [doc/cli-design.txt](../doc/language-design.txt) or the the `memo` menu of the program.
## Two wolves
([meme](https://i.kym-cdn.com/entries/icons/original/000/029/963/inside_you_there_are_two_wolves.png))
### Binary
You can use `cargo install` to install the binary. You will need the `bin` feature.
```sh
cargo install bliplib --features bin
```
Then, follow `blip --help` for instructions on how to use the program.
### Lib
The library is designed to ease the integration of this language into various tools like GUIs and editor extensions.
You can (hopefully) add tokens to the language given you specify how to parse and compile it by either creating wrappers for [`parser::Parser`] and [`compiler::Context`] in your own crate or edit this crate by cloning [the GitDab repo](https://gitdab.com/breval/blip) (or forking if you can).
Everything here is subject to an MIT license which allows you to do almost everything you want with this code as long as you include my [notice](../LICENSE).
## Thanks
This project was made for a Bachelor's Degree in Computer Science at the UQAC (Québec University at Chicoutimi / Université du Québec à Chicoutimi) and many thanks goes to Abdenour Bouzouane for his advice and for allowing me to reuse my ideas for my degree.
This project takes its roots into the first implementations of the [Music Macro Language](https://en.wikipedia.org/wiki/Music_Macro_Language) and would not exist without this great idea. Thank you to all contributors of this niche world of composition tools, especially contributors of [Pure Data](https://github.com/pure-data/pure-data), [SuperCollider](https://supercollider.github.io/) and [Sonic Pi](https://sonic-pi.net/). Special thanks goes to the very kind [Hiro Morozumi](http://hiromorozumi.com/) for the love he put in [BeepComp](http://hiromorozumi.com/beepcomp/). I wish you all the best.

View file

@ -26,34 +26,34 @@ use nom::{
sequence::preceded,
};
use nom_locate::{LocatedSpan, position};
use strum::{EnumDiscriminants, EnumIter, EnumString, IntoDiscriminant, IntoStaticStr};
use strum::{EnumDiscriminants, EnumString, IntoDiscriminant, IntoStaticStr};
use thiserror::Error;
const DEFAULT_INSTRUMENT: &str = "sin(2*pi()*(442*2^((n+1)/N))*t)";
const DEFAULT_INSTRUMENT: &str = "sin(2*pi()*(442+442*((n+1)/N))*t)";
const DEFAULT_LENGTH: &str = "2^(2-log(2, l))*(60/T)";
#[derive(Debug, Parser)]
#[derive(Parser)]
#[cfg_attr(debug_assertions, derive(Debug))]
#[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),
Memo(Memo),
}
#[derive(Debug, Parser, Clone, Getters)]
#[derive(Parser, Clone, Getters)]
#[cfg_attr(debug_assertions, derive(Debug))]
#[getset(get = "pub(super)")]
pub(super) struct PlayOpts {
/// Use this sheet music [default: stdin]
#[command(flatten)]
input: InputGroup<false>,
input: InputGroup,
/// Set available notes ("a,b,c" for example)
#[arg(short, long, value_delimiter = ',')]
#[arg(short, long)]
notes: Vec<String>,
/// Set the signal expression (instrument) used to generate music samples
#[arg(short, long, default_value = DEFAULT_INSTRUMENT)]
@ -72,7 +72,7 @@ pub(super) struct PlayOpts {
/// 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))>,
slopes: Vec<(LetterString, (char, Expression))>,
}
impl PlayOpts {
@ -87,14 +87,11 @@ impl PlayOpts {
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)))
.map(|(name, (v, e))| (name.as_ref(), (v, e)))
}
}
impl<const CREATE_IF_NOT_EXISTS: bool> InputGroup<CREATE_IF_NOT_EXISTS>
where
Self: Clone,
{
impl InputGroup {
pub(super) fn get(&self) -> Box<dyn Read> {
self.input
.as_ref()
@ -107,7 +104,8 @@ where
}
}
#[derive(Debug, Clone, Copy, AsRef)]
#[derive(Clone, Copy, AsRef)]
#[cfg_attr(debug_assertions, derive(Debug))]
struct Letter(char);
#[derive(Debug, Error)]
@ -129,7 +127,8 @@ impl FromStr for Letter {
}
}
#[derive(Debug, Clone, Copy, AsRef)]
#[derive(Clone, Copy, AsRef)]
#[cfg_attr(debug_assertions, derive(Debug))]
struct NotALetter(char);
#[derive(Debug, Error)]
@ -152,7 +151,8 @@ impl FromStr for NotALetter {
}
}
#[derive(Debug, Clone, AsRef)]
#[derive(Clone, AsRef)]
#[cfg_attr(debug_assertions, derive(Debug))]
struct LetterString(String);
#[derive(Debug, Error)]
@ -218,46 +218,38 @@ where
))
}
#[derive(Debug, Parser, Clone)]
#[derive(Parser, Clone)]
#[cfg_attr(debug_assertions, derive(Debug))]
#[group(required = false, multiple = false)]
pub(super) struct InputGroup<const CREATE_IF_NOT_EXISTS: bool> {
pub(super) struct InputGroup {
/// Set the path to your sheet music file [default: stdin]
input: Option<ClonableFile<CREATE_IF_NOT_EXISTS>>,
input: Option<ClonableFile>,
/// 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);
struct ClonableFile(File);
impl<const CREATE_IF_NOT_EXISTS: bool> Clone for ClonableFile<CREATE_IF_NOT_EXISTS> {
impl Clone for ClonableFile {
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> {
impl FromStr for ClonableFile {
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)
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)]
#[derive(Parser, Clone)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub(super) struct ExportOpts {
#[command(flatten)]
pub(super) playopts: PlayOpts,
playopts: PlayOpts,
/// Audio format to use
#[cfg_attr(
feature = "mp3",
@ -268,18 +260,14 @@ pub(super) struct ExportOpts {
cfg_attr(feature = "raw", arg(default_value = "raw mulaw"))
)]
#[arg(short, long, value_parser = audio_format_parser)]
pub(super) format: AudioFormat,
format: AudioFormat,
/// Output file [default: stdout]
#[arg(short, long)]
pub(super) output: Option<ClonableFile<true>>,
output: Option<ClonableFile>,
}
#[derive(Clone, EnumDiscriminants)]
#[strum_discriminants(
derive(EnumString, IntoStaticStr, EnumIter),
strum(serialize_all = "lowercase")
)]
pub(super) enum AudioFormat {
#[strum_discriminants(derive(EnumString, IntoStaticStr), strum(serialize_all = "lowercase"))]
enum AudioFormat {
Mp3 {
bitrate: Bitrate,
quality: Quality,
@ -294,6 +282,7 @@ pub(super) enum AudioFormat {
Raw(RawAudioFormat),
}
#[cfg(debug_assertions)]
impl Debug for AudioFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@ -371,8 +360,8 @@ fn audio_format_parser(input: &str) -> Result<AudioFormat, anyhow::Error> {
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)))
u16.or(success(320))
.and(value(SampleFormat::Float, char('f')).or(success(SampleFormat::Int)))
.map(|(bps, sample_format)| AudioFormat::Wav { bps, sample_format }),
)
}
@ -385,7 +374,9 @@ fn audio_format_parser(input: &str) -> Result<AudioFormat, anyhow::Error> {
tag::<&'a str, LocatedSpan<&'a str>, nom::error::Error<LocatedSpan<&'a str>>>(
AudioFormatDiscriminants::Flac.into(),
),
usize.or(success(16)).map(|bps| AudioFormat::Flac { bps }),
usize
.or(success(320000))
.map(|bps| AudioFormat::Flac { bps }),
)
}
fn parser<'a>() -> impl P<
@ -399,11 +390,8 @@ fn audio_format_parser(input: &str) -> Result<AudioFormat, anyhow::Error> {
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)))?,
RawAudioFormat::try_from(*r)
.map_err(|_| nom::error::Error::new(r, ErrorKind::Verify))?,
))
}),
))
@ -415,9 +403,10 @@ fn audio_format_parser(input: &str) -> Result<AudioFormat, anyhow::Error> {
.map_err(|e| anyhow!("{e:?}"))
}
#[derive(Debug, Parser, Clone, EnumString, Default, EnumIter, IntoStaticStr)]
#[derive(Parser, Clone, EnumString, Default)]
#[cfg_attr(debug_assertions, derive(Debug))]
#[strum(ascii_case_insensitive)]
pub(super) enum RawAudioFormat {
enum RawAudioFormat {
ALaw,
F32Be,
F32Le,
@ -441,30 +430,18 @@ pub(super) enum RawAudioFormat {
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
#[derive(Parser, Clone)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub(super) enum Memo {
Syntax,
#[command(subcommand)]
Example(Example),
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,
#[derive(Parser, Clone)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub(super) enum Example {
List,
N { id: u8 },
}

View file

@ -1,634 +1,23 @@
//! 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 bliplib::{compiler::VariableChange, parser::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();
fn main() {
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");
Play(play_opts) => {
let parser = Parser::new(
opts.notes(),
opts.slopes()
play_opts.notes(),
play_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<_>>(),
play_opts.variables().map(|(v, _)| *v).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()
Export(export_opts) => todo!(),
Memo(memo) => todo!(),
}
}
)),
})
.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")
}

View file

@ -1,8 +1,8 @@
use std::{
any::{Any, TypeId, type_name},
collections::BTreeMap,
any::{Any, TypeId},
collections::{BTreeMap, HashMap},
f64,
fmt::{Debug, Display},
fmt::Debug,
str::FromStr,
};
@ -10,46 +10,21 @@ 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;
const SAMPLE_RATE: u16 = 10;
} else {
/// Static sample rate.
pub const SAMPLE_RATE: u16 = 48000;
const SAMPLE_RATE: u16 = 48000;
}
}
/// A wrapper for a Vec of tokens.
#[derive(Debug, From, AsRef, Default)]
#[derive(From, AsRef, Default)]
#[cfg_attr(test, derive(Debug))]
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;
}
@ -62,9 +37,13 @@ where
}
}
/// BLIP tokens implement this.
#[cfg(not(test))]
pub trait Token: Type {
fn apply(&self, context: Context) -> Result<Context, CompilerError>;
}
#[cfg(test)]
pub trait Token: Debug + Type {
/// Applies itself to a context for compilation into samples.
fn apply(&self, context: Context) -> Result<Context, CompilerError>;
}
@ -75,50 +54,43 @@ impl PartialEq for TokenVec {
}
}
/// Litteral silence. No sound for the duration of a note.
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(test, derive(PartialEq))]
#[derive(Clone, Copy, Default)]
#[cfg_attr(test, derive(Debug, 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)?;
fn apply(&self, mut context: Context) -> Result<Context, CompilerError> {
let 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))]
#[derive(Clone, Copy, Default)]
#[cfg_attr(test, derive(Debug, 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))]
#[derive(Clone, Copy, Default)]
#[cfg_attr(test, derive(Debug, 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))?;
fn apply(&self, mut context: Context) -> Result<Context, CompilerError> {
let 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))]
#[derive(Clone, Default)]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct VariableChange(pub char, pub Expression);
impl AsRef<VariableChange> for VariableChange {
@ -129,24 +101,18 @@ impl AsRef<VariableChange> for VariableChange {
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())?;
*context.get_mut(self.0)? = 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))]
#[derive(Default)]
#[cfg_attr(test, derive(Debug, 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))]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub enum LoopCount {
/// Fixed number of times
Litteral(usize),
/// Variable number of times, using the current value of the underlying variable
Variable(char),
}
@ -158,11 +124,10 @@ impl Default for LoopCount {
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,
LoopCount::Variable(v) => *context.get(v)? as usize,
};
context.result.clear();
let new_context = self
@ -178,17 +143,12 @@ impl Token for Loop {
}
}
/// 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))]
#[derive(Default)]
#[cfg_attr(test, derive(Debug, 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
@ -220,18 +180,15 @@ impl Token for Tuplet {
}
}
/// 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))]
#[derive(new, Default)]
#[cfg_attr(test, derive(Debug, 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
.slopes
.push((self.0.as_ref().0, self.0.as_ref().1.as_ref().clone()));
context = self
.1
.0
@ -242,14 +199,11 @@ impl Token for Slope {
}
}
/// A mathematical expression to be evaluated with a set of variables to form a single [`f64`] value.
#[derive(Debug, new, Default)]
#[derive(new, Default)]
#[cfg_attr(any(debug_assertions, test), derive(Debug))]
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,
}
@ -289,227 +243,152 @@ impl FromStr for Expression {
}
}
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))]
#[derive(Clone, new)]
#[cfg_attr(test, derive(Debug, 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.
#[new(into_iter = "(char, f64)")]
pub variables: HashMap<char, f64>,
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)>,
#[new(into_iter = "(char, Expression)")]
pub slopes: Vec<(char, 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);
pub struct VariableNotFoundError(char);
/// 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);
pub struct SlopeNotFoundError(char);
/// 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)]
#[error("{0}")]
VariableNotFound(#[from] VariableNotFoundError),
/// See [`SlopeNotFoundError`].
#[error(transparent)]
#[error("{0}")]
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
pub fn current_length(&self) -> Result<f64, CompilerError> {
self.get_slopes_for('L')
.map_err(CompilerError::from)?
.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,
.try_fold(
self.clone(),
|mut acc, slope| -> Result<Context, CompilerError> {
// just in case next slopes use L too
*acc.get_mut('L')? = self.eval(slope).map_err(Into::<CompilerError>::into)?;
Ok(acc)
},
note_length,
))
)?
.get('L')
.map_err(Into::into)
.cloned()
}
/// Get a variable from the set.
pub fn get(&self, name: impl AsRef<str> + Into<String>) -> Result<&f64, VariableNotFoundError> {
pub fn get(&self, name: char) -> Result<&f64, VariableNotFoundError> {
self.variables.get(&name).ok_or(VariableNotFoundError(name))
}
pub fn get_mut(&mut self, name: char) -> Result<&mut f64, VariableNotFoundError> {
self.variables
.get(name.as_ref())
.ok_or(VariableNotFoundError(name.into()))
.get_mut(&name)
.ok_or(VariableNotFoundError(name))
}
/// 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> {
pub fn get_slopes_for(&self, var: char) -> Result<Vec<&Expression>, SlopeNotFoundError> {
let result: Vec<&Expression> = self
.slopes
.iter()
.filter_map(|(c, e)| (var == c).then_some(e))
.filter_map(|(c, e)| (c == &var).then_some(e))
.collect();
if result.is_empty() {
Err(SlopeNotFoundError(var.into()))
Err(SlopeNotFoundError(var))
} 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,
})
fn tick(&mut self) -> Result<(), CompilerError> {
*self.get_mut('t')? += 1f64 / (SAMPLE_RATE as f64);
*self = self.slopes.iter().try_fold(
self.clone(),
|mut acc, (v, e)| -> Result<Self, CompilerError> {
*acc.get_mut(*v)? = acc.eval(e)?;
Ok(acc)
},
)?;
Ok(())
}
/// 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)
fn namespace_generator(&self) -> impl Iterator<Item = (String, f64)> {
self.variables.iter().map(|(c, f)| (c.to_string(), *f))
}
pub fn eval(&self, expr: &Expression) -> Result<f64, fasteval::Error> {
self.eval_with(
expr,
&mut self.namespace_generator().collect::<BTreeMap<_, _>>(),
)
}
/// Evaluate the result of an Expression using the current values of the variables registered in the provided namespace.
pub fn eval_with(
&self,
Expression {
from,
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}"))
instruction.eval(slab, ns)
}
/// 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")?;
pub fn render(&mut self, n: Option<u8>) -> Result<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()?;
let mut map = self.namespace_generator().collect::<BTreeMap<_, _>>();
map.insert('n'.to_string(), note as f64);
while self.current_length()? > *self.get('t')? - curr_t + (1f64 / SAMPLE_RATE as f64) {
result.push(self.eval_with(&self.instrument, &mut map)? * f64::MAX);
self.tick()?;
map = self.namespace_generator().fold(map, |mut acc, (k, v)| {
acc.insert(k, v);
acc
})
}
Ok((self, result))
Ok(result)
} else {
while {
let (new_self, length) = self.current_length()?;
self = new_self;
length
} > *self.get("t")? - curr_t
{
self = self.tick()?;
while self.current_length()? > *self.get('t')? - curr_t {
self.tick()?;
}
let len = (*self.get("t")? - curr_t) * SAMPLE_RATE as f64;
Ok((self, vec![0.0; len as usize]))
Ok(vec![
0.0;
((*self.get('t')? - curr_t) * SAMPLE_RATE as f64)
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);
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)
}
@ -521,7 +400,6 @@ impl Compiler {
.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>,
@ -544,20 +422,16 @@ mod tests {
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())),
[('L', "2^(2-log(2, l))*(60/T)")].map(|(c, e)| (c, e.parse().unwrap())),
)
}
@ -608,7 +482,7 @@ mod tests {
compiler = compiler.step(note)?;
let first = compiler.0.result.clone();
*compiler.0.get_mut('t'.to_string())? = 0.0;
*compiler.0.get_mut('t')? = 0.0;
compiler.0.result.clear();
compiler = compiler.step(note)?;
let second = compiler.0.result.clone();

View file

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

View file

@ -1,24 +1,17 @@
use std::{
any::type_name,
borrow::{Borrow, Cow},
collections::BTreeMap,
marker::PhantomData,
};
use std::{borrow::Borrow, 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 _,
AsChar, Compare, Finish, IResult, Input, Parser as NomParser,
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},
combinator::{opt, value},
error::{Error, ParseError},
multi::many0,
sequence::{delimited, preceded},
};
@ -29,104 +22,6 @@ use crate::compiler::{
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
@ -143,7 +38,7 @@ where
phantom: PhantomData<(NS, SS)>,
}
impl<'a, 'p, N, NS, S, SS, SV, V> Parser<N, NS, S, SS, SV, V>
impl<'a, N, NS, S, SS, SV, V> Parser<N, NS, S, SS, SV, V>
where
N: AsRef<[NS]>,
NS: AsRef<str>,
@ -151,37 +46,32 @@ where
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,
&self,
input: &'a str,
) -> Result<TokenVec, nom::Err<LocatedVerboseError<nom_locate::LocatedSpan<&'a str>>>> {
debug!("parsing input \"{input}\"");
all_consuming(token_parser(self))
) -> Result<TokenVec, Error<nom_locate::LocatedSpan<&'a str>>> {
token_parser(self)
.parse_complete(LocatedSpan::new(input))
.map(move |(_, o)| o)
.finish()
.map(|(_, 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>>,
>
fn token_parser<'a, I, N, NS, S, SS, SV, V>(
parser: &Parser<N, NS, S, SS, SV, V>,
) -> impl NomParser<I, Output = TokenVec, Error = nom::error::Error<I>>
where
I: Input + AsRef<str> + for<'z> nom::Compare<&'z str> + Copy,
<I as Input>::Item: AsChar,
<I as Input>::Item: PartialEq<char>,
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(
(),
@ -191,20 +81,13 @@ where
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(
Silence::parser().map(into_box),
Marker::parser().map(into_box),
Note::parser(parser.notes.as_ref()).map(into_box),
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),
Loop::parser(parser).map(into_box),
Tuplet::parser(parser).map(into_box),
Slope::parser(parser).map(into_box),
)),
space_or_comment(),
))
@ -216,23 +99,21 @@ fn into_box<'a>(token: impl Token + 'a) -> Box<dyn Token + 'a> {
}
impl Silence {
fn parser<I>() -> impl nom::Parser<I, Output = Self, Error = LocatedVerboseError<I>>
fn parser<I>() -> impl NomParser<I, Output = Self, Error = nom::error::Error<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>>
fn parser<I>() -> impl NomParser<I, Output = Self, Error = nom::error::Error<I>>
where
I: Input,
<I as Input>::Item: AsChar,
{
trace!("making the {} parser", type_name::<Self>());
value(Marker, char('%'))
}
}
@ -240,30 +121,26 @@ impl Marker {
impl Note {
fn parser<'a, N, NS, I>(
notes: N,
) -> impl nom::Parser<I, Output = Self, Error = LocatedVerboseError<I>> + 'a
) -> impl NomParser<I, Output = Self, Error = nom::error::Error<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(..)
.rev()
.map(|(i, t)| {
Box::new(move |input: I| {
value(Note(i as u8), tag(t.clone().as_ref())).parse(input)
@ -282,47 +159,33 @@ impl VariableChange {
<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))),
one_of(variables.as_ref().iter().collect::<String>().as_str()),
)
.and(expression_parser(variables.as_ref()))
.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>
fn parser<I, N, NS, S, SS, SV, V>(
parser: &Parser<N, NS, S, SS, SV, V>,
) -> impl Fn(I) -> IResult<I, Self>
where
I: Input + AsRef<str> + for<'z> nom::Compare<&'z str> + Copy,
<I as Input>::Item: AsChar,
<I as Input>::Item: PartialEq<char>,
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('('),
@ -338,17 +201,8 @@ impl Loop {
)
.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(
.and(token_parser(parser)),
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)
@ -357,34 +211,22 @@ impl Loop {
}
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>
fn parser<I, N, NS, S, SS, SV, V>(
parser: &Parser<N, NS, S, SS, SV, V>,
) -> impl Fn(I) -> IResult<I, Self>
where
I: Input + for<'z> Compare<&'z str> + AsRef<str> + Copy,
<I as Input>::Item: AsChar,
<I as Input>::Item: PartialEq<char>,
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()
)
)
))
delimited(char('['), token_parser(parser), char(']'))
.map(Self)
.parse(input)
}
@ -392,19 +234,20 @@ impl Tuplet {
}
impl Slope {
fn parser<'a, 'p, N, NS, S, SS, SV, V>(
fn parser<'p, I, 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>
) -> impl Fn(I) -> IResult<I, Self>
where
I: Input + for<'z> Compare<&'z str> + AsRef<str> + Copy,
<I as Input>::Item: AsChar,
<I as Input>::Item: PartialEq<char>,
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
@ -413,51 +256,22 @@ impl Slope {
.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();
let iter: std::iter::Rev<std::vec::IntoIter<(String, VariableChange)>> =
slopes.into_iter().rev();
delimited(
char('{'),
cut(expect(alt(iter
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>,
>
Box::new(move |input: I| value(v.clone(), tag(k.as_str())).parse(input))
as Box<dyn Fn(I) -> IResult<I, 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(
.collect::<Vec<Box<dyn Fn(I) -> IResult<I, VariableChange>>>>()
.as_mut_slice())
.and(token_parser(parser)),
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)
@ -472,37 +286,18 @@ where
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| {
i.as_ref().parse::<Expression>().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)
@ -512,46 +307,40 @@ where
}
}
/// 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>>
pub fn take_while_map<F, I, O, Error: ParseError<I>>(
cond: F,
) -> impl FnMut(I) -> IResult<I, O, Error>
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")),
}))
Err(nom::Err::Incomplete(nom::Needed::Unknown))
}
}
#[cfg(test)]
mod tests {
use super::{IResult, Parser, expression_parser};
use super::Parser;
use std::{borrow::Borrow, collections::HashMap};
use std::collections::HashMap;
use nom::Parser as _;
use nom_locate::LocatedSpan;
use nom::{IResult, Parser as _};
use crate::compiler::{
use crate::{
compiler::{
Loop, LoopCount, Marker, Note, Silence, Slope, TokenVec, Tuplet, VariableChange,
},
parser::expression_parser,
};
fn very_fancy_slope() -> VariableChange {
@ -562,16 +351,14 @@ mod tests {
VariableChange('n', "1".parse().unwrap())
}
type DefaultParser = Parser<
fn parser_generator() -> Parser<
[&'static str; 3],
&'static str,
HashMap<String, VariableChange>,
String,
VariableChange,
[char; 1],
>;
fn parser_generator() -> DefaultParser {
> {
Parser::new(
["do", "", "mi"],
HashMap::from([
@ -740,26 +527,15 @@ mod tests {
#[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 slopes = Default::default();
fn parser_builder<'s>(
slopes: &'s HashMap<String, VariableChange>,
) -> impl Fn(&str) -> IResult<&str, Loop> {
move |input: &str| {
Loop::parser(&Parser::new(["do", "", "mi"], slopes, ['n'])).parse(input)
}
let parser = parser_builder(&parser);
}
let parser = parser_builder(&slopes);
let mut working_cases = vec![
(
"(.%)",
@ -787,7 +563,7 @@ mod tests {
];
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));
let output = parser(test);
if let Ok(result) = output {
assert_eq!(expected, result, "case \"{test}\"");
} else {
@ -795,7 +571,7 @@ mod tests {
}
}
for test in not_working_cases.drain(..) {
let output = parser(test.into()).map(|(ls, o)| (*ls, o));
let output = parser(test);
assert!(
output.is_err(),
"result of \"{test}\" was not Err: {output:?}"
@ -805,26 +581,15 @@ mod tests {
#[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 slopes = Default::default();
fn parser_builder<'s>(
slopes: &'s HashMap<String, VariableChange>,
) -> impl Fn(&str) -> IResult<&str, Tuplet> {
move |input: &str| {
Tuplet::parser(&Parser::new(["do", "", "mi"], slopes, ['n'])).parse(input)
}
let parser = parser_builder(&parser);
}
let parser = parser_builder(&slopes);
let mut working_cases = vec![
(
"[.%]",
@ -838,7 +603,7 @@ mod tests {
];
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));
let output = parser(test);
if let Ok(result) = output {
assert_eq!(expected, result, "case \"{test}\"");
} else {
@ -846,7 +611,7 @@ mod tests {
}
}
for test in not_working_cases.drain(..) {
let output = parser(test.into()).map(|(ls, o)| (*ls, o));
let output = parser(test);
assert!(
output.is_err(),
"result of \"{test}\" was not Err: {output:?}"
@ -893,7 +658,7 @@ mod tests {
"{}",
];
for (test, expected) in working_cases.drain(..) {
let output = parser(test.into()).map(|(ls, o)| (*ls, o));
let output = parser(test);
if let Ok(result) = output {
assert_eq!(expected, result, "case \"{test}\"");
} else {
@ -901,7 +666,7 @@ mod tests {
}
}
for test in not_working_cases.drain(..) {
let output = parser(test.into()).map(|(ls, o)| (*ls, o));
let output = parser(test);
assert!(
output.is_err(),
"result of \"{test}\" was not Err: {output:?}"