diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 7e50b559..46d92a0f 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -2,7 +2,7 @@ name: Benchmark on: pull_request: - branches: [ "master" ] + branches: [ "master", "dev" ] env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c63570f9..70ef7e07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build & Test on: pull_request: - branches: [ "master" ] + branches: [ "master", "dev" ] env: CARGO_TERM_COLOR: always diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 00000000..d4160cb3 --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,38 @@ +// Project-local debug tasks +// +// For more documentation on how to configure debug tasks, +// see: https://zed.dev/docs/debugger +[ + { + "label": "Build & Debug play", + "build": { + "command": "cargo", + "args": [ + "build", + "--example", + "play", + "--features=player,fundsp,cpal-output" + ] + }, + "program": "$ZED_WORKTREE_ROOT/target/debug/examples/play", + "sourceLanguages": ["rust"], + "request": "launch", + "adapter": "CodeLLDB" + }, + { + "label": "Build & Debug play-script", + "build": { + "command": "cargo", + "args": [ + "build", + "--example", + "play-script", + "--features=scripting,player,cpal-output" + ] + }, + "program": "$ZED_WORKTREE_ROOT/target/debug/examples/play-script", + "sourceLanguages": ["rust"], + "request": "launch", + "adapter": "CodeLLDB" + } +] diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..b8c92b4c --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,19 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{ + "lsp": { + "rust-analyzer": { + "initialization_options": { + "cargo": { + "autoreload": true, + "features": ["scripting", "player", "cpal-output"] + }, + "check": { + "command": "clippy" + } + } + } + } +} diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 00000000..11a17129 --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,23 @@ +[ + { + "label": "cargo build", + "command": "cargo build --features=scripting,player,cpal-output --example=*", + "show_command": true, + "reveal": "never", + "tags": ["build"] + }, + { + "label": "cargo test", + "command": "cargo test --features=scripting,player,cpal-output", + "show_command": true, + "reveal": "never", + "tags": ["test"] + }, + { + "label": "cargo clippy", + "command": "cargo clippy --features=scripting,player,cpal-output --example=*", + "show_command": true, + "reveal": "never", + "tags": ["check"] + } +] diff --git a/Cargo.toml b/Cargo.toml index 1792db7a..037762e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,15 +36,14 @@ mlua = { version = "^0.11", features = [ ], optional = true } # optional -> player -crossbeam-channel = { version = "^0.5", default-features = false, optional = true } dashmap = { version = "^6.1", optional = true } -phonic = { version = "^0.11", default-features = false, optional = true } +phonic = { version = "^0.12", default-features = false, optional = true } # dev dependencies [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] pretty_assertions = { version = "^1.4" } notify = { version = "^8.0", default-features = false, features = ["macos_fsevent"] } -ctrlc = { version = "^3.4" } +ctrlc = { version = "^3.5" } criterion = { version = "^0.7", default-features = false } simplelog = { version = "^0.12", default-features = false, features = [ "termcolor", @@ -63,8 +62,8 @@ debug = "full" dhat-profiler = ["dhat"] # example player implementation -player = ["crossbeam-channel", "dashmap", "phonic"] -# example player's audio output driver +player = ["dashmap", "phonic"] +# example player's audio output driver web-output = ["phonic/web-output"] cpal-output = ["phonic/cpal-output"] diff --git a/benches/benchmarks/pattern.rs b/benches/benchmarks/pattern.rs index d4141d75..2610a3b4 100644 --- a/benches/benchmarks/pattern.rs +++ b/benches/benchmarks/pattern.rs @@ -19,7 +19,7 @@ fn create_phrase() -> Phrase { .unwrap() .with_mappings(&[ ("bd", vec![new_note("c4")]), - ("bd2", vec![new_note(("c4", None, 0.5))]), + ("bd2", vec![new_note(("c4", None, None, 0.5))]), ]); let kick_pattern = beat_time.every_nth_beat(16.0).emit(kick_cycle); @@ -80,46 +80,46 @@ fn create_phrase() -> Phrase { .every_nth_eighth(1.0) .with_rhythm([1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1].to_rhythm()) .emit(new_note_sequence_emitter(vec![ - new_note((bass_notes[0], None, 0.5)), - new_note((bass_notes[2], None, 0.5)), - new_note((bass_notes[3], None, 0.5)), - new_note((bass_notes[0], None, 0.5)), - new_note((bass_notes[2], None, 0.5)), - new_note((bass_notes[3], None, 0.5)), - new_note((bass_notes[6].transposed(-12), None, 0.5)), + new_note((bass_notes[0], None, None, 0.5)), + new_note((bass_notes[2], None, None, 0.5)), + new_note((bass_notes[3], None, None, 0.5)), + new_note((bass_notes[0], None, None, 0.5)), + new_note((bass_notes[2], None, None, 0.5)), + new_note((bass_notes[3], None, None, 0.5)), + new_note((bass_notes[6].transposed(-12), None, None, 0.5)), ])); let synth_pattern = beat_time .every_nth_bar(4.0) .emit(new_polyphonic_note_sequence_emitter(vec![ vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("G 4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("G 4", None, None, 0.3)), ], vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("F 4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("F 4", None, None, 0.3)), ], vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("G 4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("G 4", None, None, 0.3)), ], vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("A#4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("A#4", None, None, 0.3)), ], ])); let fx_pattern = beat_time .every_nth_seconds(8.0) .emit(new_polyphonic_note_sequence_emitter(vec![ - vec![new_note(("C 4", None, 0.2)), None, None], - vec![None, new_note(("C 4", None, 0.2)), None], - vec![None, None, new_note(("F 4", None, 0.2))], + vec![new_note(("C 4", None, None, 0.2)), None, None], + vec![None, new_note(("C 4", None, None, 0.2)), None], + vec![None, None, new_note(("F 4", None, None, 0.2))], ])); Phrase::new( diff --git a/bindings/includes/pattrns.h b/bindings/includes/pattrns.h index 93ad1e34..7ba2bb91 100644 --- a/bindings/includes/pattrns.h +++ b/bindings/includes/pattrns.h @@ -2,8 +2,8 @@ /* This file is autogenerated by cbindgen. Do not modify manually. */ -#include #include +#include #include namespace pattrns { @@ -14,6 +14,9 @@ constexpr static const uint32_t NO_INSTRUMENT_ID = UINT32_MAX; /// Parameter change value which refers to an empty, undefined parameter. constexpr static const uint32_t NO_PARAMETER_ID = UINT32_MAX; +/// Glide value which refers to an unset, undefined glide value. +constexpr static const float NO_GLIDE_VALUE = -1.0; + /// Note value which refers to an empty, undefined note. constexpr static const uint8_t EMPTY_NOTE = 254; @@ -57,9 +60,9 @@ struct ParameterSet { uint32_t parameters_len; }; -/// C lang compatible Result representation for new_pattern_from_string/file. -/// Error Strings must be deleted with `drop_error_string`. -/// Pattern values must be deleted with `drop_pattern`, +/// C lang compatible Result representation for +/// new_pattern_from_string/file. Error Strings must be deleted with +/// `drop_error_string`. Pattern values must be deleted with `drop_pattern`, struct PatternResult { enum class Tag { Error, @@ -82,32 +85,28 @@ struct PatternResult { static PatternResult Error(const char *const &_0) { PatternResult result; - ::new (&result.error._0) (const char*)(_0); + ::new (&result.error._0)(const char *)(_0); result.tag = Tag::Error; return result; } - bool IsError() const { - return tag == Tag::Error; - } + bool IsError() const { return tag == Tag::Error; } - const char*const & AsError() const { + const char *const &AsError() const { assert(IsError()); return error._0; } static PatternResult Value(Pattern *const &_0) { PatternResult result; - ::new (&result.value._0) (Pattern*)(_0); + ::new (&result.value._0)(Pattern *)(_0); result.tag = Tag::Value; return result; } - bool IsValue() const { - return tag == Tag::Value; - } + bool IsValue() const { return tag == Tag::Value; } - Pattern*const & AsValue() const { + Pattern *const &AsValue() const { assert(IsValue()); return value._0; } @@ -145,32 +144,28 @@ struct ParameterSetResult { static ParameterSetResult Error(const char *const &_0) { ParameterSetResult result; - ::new (&result.error._0) (const char*)(_0); + ::new (&result.error._0)(const char *)(_0); result.tag = Tag::Error; return result; } - bool IsError() const { - return tag == Tag::Error; - } + bool IsError() const { return tag == Tag::Error; } - const char*const & AsError() const { + const char *const &AsError() const { assert(IsError()); return error._0; } static ParameterSetResult Value(ParameterSet *const &_0) { ParameterSetResult result; - ::new (&result.value._0) (ParameterSet*)(_0); + ::new (&result.value._0)(ParameterSet *)(_0); result.tag = Tag::Value; return result; } - bool IsValue() const { - return tag == Tag::Value; - } + bool IsValue() const { return tag == Tag::Value; } - ParameterSet*const & AsValue() const { + ParameterSet *const &AsValue() const { assert(IsValue()); return value._0; } @@ -188,9 +183,7 @@ struct VoidResult { const char *_0; }; - struct Ok_Body { - - }; + struct Ok_Body {}; Tag tag; union { @@ -200,16 +193,14 @@ struct VoidResult { static VoidResult Error(const char *const &_0) { VoidResult result; - ::new (&result.error._0) (const char*)(_0); + ::new (&result.error._0)(const char *)(_0); result.tag = Tag::Error; return result; } - bool IsError() const { - return tag == Tag::Error; - } + bool IsError() const { return tag == Tag::Error; } - const char*const & AsError() const { + const char *const &AsError() const { assert(IsError()); return error._0; } @@ -220,9 +211,7 @@ struct VoidResult { return result; } - bool IsOk() const { - return tag == Tag::Ok; - } + bool IsOk() const { return tag == Tag::Ok; } }; /// C lang compatible representation of a rust `Result`. @@ -249,32 +238,28 @@ struct F64Result { static F64Result Error(const char *const &_0) { F64Result result; - ::new (&result.error._0) (const char*)(_0); + ::new (&result.error._0)(const char *)(_0); result.tag = Tag::Error; return result; } - bool IsError() const { - return tag == Tag::Error; - } + bool IsError() const { return tag == Tag::Error; } - const char*const & AsError() const { + const char *const &AsError() const { assert(IsError()); return error._0; } static F64Result Value(const double &_0) { F64Result result; - ::new (&result.value._0) (double)(_0); + ::new (&result.value._0)(double)(_0); result.tag = Tag::Value; return result; } - bool IsValue() const { - return tag == Tag::Value; - } + bool IsValue() const { return tag == Tag::Value; } - const double& AsValue() const { + const double &AsValue() const { assert(IsValue()); return value._0; } @@ -304,32 +289,28 @@ struct UInt32Result { static UInt32Result Error(const char *const &_0) { UInt32Result result; - ::new (&result.error._0) (const char*)(_0); + ::new (&result.error._0)(const char *)(_0); result.tag = Tag::Error; return result; } - bool IsError() const { - return tag == Tag::Error; - } + bool IsError() const { return tag == Tag::Error; } - const char*const & AsError() const { + const char *const &AsError() const { assert(IsError()); return error._0; } static UInt32Result Value(const uint32_t &_0) { UInt32Result result; - ::new (&result.value._0) (uint32_t)(_0); + ::new (&result.value._0)(uint32_t)(_0); result.tag = Tag::Value; return result; } - bool IsValue() const { - return tag == Tag::Value; - } + bool IsValue() const { return tag == Tag::Value; } - const uint32_t& AsValue() const { + const uint32_t &AsValue() const { assert(IsValue()); return value._0; } @@ -339,6 +320,7 @@ struct UInt32Result { struct NoteEvent { uint8_t note; uint32_t instrument; + float glide; float volume; float panning; float delay; @@ -356,7 +338,8 @@ struct ParameterChangeEvent { float value; }; -/// C lang compatible representation of a rust `Vec`. +/// C lang compatible representation of a rust +/// `Vec`. struct ParameterChangeEvents { const ParameterChangeEvent *events_ptr; uint32_t events_len; @@ -371,9 +354,9 @@ struct PatternPlaybackEvent { ParameterChangeEvents parameter_change_events; }; -using AllocFn = void*(*)(uint32_t, uint32_t); +using AllocFn = void *(*)(uint32_t, uint32_t); -using DeallocFn = void(*)(void*, uint32_t, uint32_t); +using DeallocFn = void (*)(void *, uint32_t, uint32_t); extern "C" { @@ -383,29 +366,34 @@ void drop_error_string(const char *error); /// Drop array of input parameters, created via `pattern_parameters` void drop_parameter_set(ParameterSet *parameters); -/// Create a new pattern from the given script file path, using the given beat time and instrument. -/// The returned pattern result must be deleted via `drop_pattern` or `drop_error_string`. +/// Create a new pattern from the given script file path, using the given beat +/// time and instrument. The returned pattern result must be deleted via +/// `drop_pattern` or `drop_error_string`. PatternResult new_pattern_from_file(Timebase time_base, const uint32_t *instrument_id, const char *file_name); -/// Create a new pattern from the given script contents, using the given beat time and instrument. -/// The returned pattern result must be deleted via `drop_pattern` or `drop_error_string`. +/// Create a new pattern from the given script contents, using the given beat +/// time and instrument. The returned pattern result must be deleted via +/// `drop_pattern` or `drop_error_string`. PatternResult new_pattern_from_string(Timebase time_base, const uint32_t *instrument_id, const char *content, const char *content_name); -/// Create a new resetted clone from an existing pattern with the given timebase and instrument id. -/// The returned pattern result must be deleted via `drop_pattern` or `drop_error_string`. +/// Create a new resetted clone from an existing pattern with the given timebase +/// and instrument id. The returned pattern result must be deleted via +/// `drop_pattern` or `drop_error_string`. PatternResult new_pattern_instance(Pattern *this_, Timebase time_base); /// Get parameters of a pattern. -/// The returned result must be deleted via `drop_parameter_set` or `drop_error_string`. +/// The returned result must be deleted via `drop_parameter_set` or +/// `drop_error_string`. ParameterSetResult pattern_parameters(Pattern *this_); /// Set a single parameter value of a pattern. -VoidResult set_pattern_parameter_value(Pattern *this_, const char *id, double value); +VoidResult set_pattern_parameter_value(Pattern *this_, const char *id, + double value); /// Get length in samples of a pattern's step. F64Result pattern_samples_per_step(Pattern *this_); @@ -424,17 +412,15 @@ VoidResult set_pattern_trigger_event(Pattern *this_, /// Run pattern, consuming the single next due event only. /// NB: Events are only valid within the callback, so they must be consumed /// or copied when used outside of the callback. -VoidResult run_pattern(Pattern *this_, - void *callback_context, - void (*callback)(void*, const PatternPlaybackEvent*)); +VoidResult run_pattern(Pattern *this_, void *callback_context, + void (*callback)(void *, const PatternPlaybackEvent *)); -/// Run pattern, consuming all events which the pattern generated up to given sample time. -/// NB: Events are only valid within the callback, so they must be consumed -/// or copied when used outside of the callback. -VoidResult run_pattern_until_time(Pattern *this_, - uint64_t time, - void *callback_context, - void (*callback)(void*, const PatternPlaybackEvent*)); +/// Run pattern, consuming all events which the pattern generated up to given +/// sample time. NB: Events are only valid within the callback, so they must be +/// consumed or copied when used outside of the callback. +VoidResult +run_pattern_until_time(Pattern *this_, uint64_t time, void *callback_context, + void (*callback)(void *, const PatternPlaybackEvent *)); /// Run/seek pattern, discarding all events up to the given time. VoidResult advance_pattern_until_time(Pattern *this_, uint64_t time); @@ -442,13 +428,14 @@ VoidResult advance_pattern_until_time(Pattern *this_, uint64_t time); /// Delete a pattern which got allocated via `new_pattern_from_string/file`. void drop_pattern(Pattern *pattern); -/// Initialize lib and set external allocator, which should be used instead of the system -/// allocator as global allocator (unless the "dhat-profiler" feature is enabled). +/// Initialize lib and set external allocator, which should be used instead of +/// the system allocator as global allocator (unless the "dhat-profiler" feature +/// is enabled). VoidResult initialize(AllocFn alloc, DeallocFn dealloc); /// Finalize lib: no more calls into the library are allowed after this VoidResult finalize(); -} // extern "C" +} // extern "C" -} // namespace pattrns +} // namespace pattrns diff --git a/bindings/src/lib.rs b/bindings/src/lib.rs index 607b4266..ef5bc1e9 100644 --- a/bindings/src/lib.rs +++ b/bindings/src/lib.rs @@ -58,8 +58,8 @@ macro_rules! try_catch { // evaluate block let result = $block; // when the block caused a callback error, return the error - if let Some(lua_error) = pattrns::has_lua_callback_errors() { - $result_type::Error(new_raw_cstring(&lua_error.to_string())) + if let Some(callback_error) = pattrns::has_lua_callback_errors() { + $result_type::Error(new_raw_cstring(&callback_error.error.to_string())) } else { // else return the block's return value result @@ -81,6 +81,9 @@ pub const NO_INSTRUMENT_ID: u32 = u32::MAX; /// Parameter change value which refers to an empty, undefined parameter. pub const NO_PARAMETER_ID: u32 = u32::MAX; +/// Glide value which refers to an unset, undefined glide value. +pub const NO_GLIDE_VALUE: f32 = -1.0; + /// Note value which refers to an empty, undefined note. pub const EMPTY_NOTE: u8 = 0xFE; const_assert_eq!(EMPTY_NOTE, pattrns::Note::EMPTY as u8); @@ -127,6 +130,7 @@ pub unsafe extern "C" fn drop_error_string(error: *const c_char) { pub struct NoteEvent { pub note: u8, pub instrument: u32, + pub glide: f32, pub volume: f32, pub panning: f32, pub delay: f32, @@ -138,6 +142,7 @@ impl Default for NoteEvent { Self { note: EMPTY_NOTE, instrument: NO_INSTRUMENT_ID, + glide: NO_GLIDE_VALUE, volume: 1.0, panning: 0.0, delay: 0.0, @@ -151,12 +156,14 @@ impl From<&pattrns::NoteEvent> for NoteEvent { let instrument = value .instrument .map_or(NO_INSTRUMENT_ID, |id| usize::from(id) as u32); + let glide = value.glide.map_or(NO_GLIDE_VALUE, |value| value); let volume = value.volume; let panning = value.panning; let delay = value.delay; Self { - instrument, note, + instrument, + glide, volume, panning, delay, @@ -172,6 +179,10 @@ impl From<&NoteEvent> for pattrns::NoteEvent { NO_INSTRUMENT_ID => None, _ => Some(pattrns::InstrumentId::from(value.instrument as usize)), }, + glide: match value.glide { + NO_GLIDE_VALUE => None, + _ => Some(value.glide), + }, volume: value.volume, panning: value.panning, delay: value.delay, diff --git a/docs/src/API/note.md b/docs/src/API/note.md index ba904939..7d65c240 100644 --- a/docs/src/API/note.md +++ b/docs/src/API/note.md @@ -14,9 +14,10 @@ > attributes: > ```md > -'#' -> instrument (integer > 0) -> -'v' -> volume (number in range [0-1]) -> -'p' -> panning (number in range [-1-1]) -> -'d' -> delay (number in range [0-1]) +> -'g' -> glide (number in range [0-INF] - glide step duration) +> -'v' -> volume (number in range [0-1] - zero to full volume) +> -'p' -> panning (number in range [-1-1] - full left to full right) +> -'d' -> delay (number in range [0-1] - delay in units) > ``` > > #### examples: @@ -110,7 +111,11 @@ ### delay([*self*](../API/builtins/self.md), delay : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) `->`[`Note`](../API/note.md#Note) -> Set the note's delay attribute to the specified value or values. +> Set the note's delay attribute to the specified value or values. +### glide([*self*](../API/builtins/self.md), glide : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) +`->`[`Note`](../API/note.md#Note) + +> Set the note's glide attribute to the specified value or values. diff --git a/docs/src/API/sequence.md b/docs/src/API/sequence.md index 1bbe12e6..28ee596d 100644 --- a/docs/src/API/sequence.md +++ b/docs/src/API/sequence.md @@ -18,6 +18,10 @@ > sequence(48, "c5", {}) > -- sequence of a +5 transposed C4 and G4 major chord > sequence("c4'maj", "g4'maj"):transpose(5) +> -- glide from c4 to e4 +> sequence{"c4", "e4 g1.0"} +> -- glide from c4 to e4 in half of the step time +> sequence{"c4", "e4 g0.5"} > ``` @@ -86,7 +90,11 @@ ### delay([*self*](../API/builtins/self.md), delay : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) `->`[`Sequence`](../API/sequence.md#Sequence) -> Set the delay attribute of all notes to the specified value or values. +> Set the delay attribute of all notes to the specified value or values. +### glide([*self*](../API/builtins/self.md), glide : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) +`->`[`Sequence`](../API/sequence.md#Sequence) + +> Set the glide attribute of all notes to the specified value or values. diff --git a/docs/src/guide/notes&scales.md b/docs/src/guide/notes&scales.md index 5fdd5ab5..bff7d68a 100644 --- a/docs/src/guide/notes&scales.md +++ b/docs/src/guide/notes&scales.md @@ -14,11 +14,13 @@ Raw integer values like `48`, are interpreted as MIDI note numbers in the `note` Instead of using a string, you can also specify notes via a Lua table with the following properties. -- `"key"` - REQUIRED - MIDI Note number such as `48` or a string, such as `"c4"` -- `"instrument"` OPTIONAL - Instrument/Sample/Patch number >= 0 -- `"volume"` - OPTIONAL - Volume number in range [0.0 - 1.0] -- `"panning"` - OPTIONAL - Panning factor in range [-1.0 - 1.0] where 0 is center -- `"delay"` - OPTIONAL - Delay factor in range [0.0 - 1.0] +- `"key"` - *required* - MIDI Note number such as `48` or a string, such as `"c4"` +- `"instrument"` *optional* - Instrument/Sample/Patch number >= 0 +- `'glide'` - *optional* - Glide factor in range [0 - INF] +- `"volume"` - *optional* - Volume number in range [0.0 - 1.0] +- `"panning"` - *optional* - Panning factor in range [-1.0 - 1.0] where 0 is center +- `"delay"` - *optional* - Delay factor in range [0.0 - 1.0] + » `event = { key = 48, volume = 0.1 }` *a c4 with volume 0.1* @@ -31,13 +33,25 @@ Valid keys are `c,d,e,f,g,a,b`. Valid modifiers are `#` and `b`. Valid octaves a Other note properties can be specified in the string notation as well. -- `'#'` instrument -- `'v'` volume -- `'p'` panning -- `'d'` delay +- `'#'` instrument (integer >= 0) +- `'g'` glide (number in range [0-INF]) +- `'v'` volume (number in range [0-1]) +- `'p'` panning (number in range [-1-1]) +- `'d'` delay (number in range [0-1]) » `event = { "f#4 #1 v0.2" }` *emit a f sharp for instrument 1 with volume 0.2* +### Note Glides + +Note glides (portamento) can be archived by setting a `g` note property value: + +With a glide value of `1`, the target note is reached within a full time step's duration, regardless of the note range. A glide value of `0.5` reaches the target note in half the time, meaning it glides twice as fast. A value of `0` instantly glides to the specified note. + +» `event = sequence{"c4", "e4 g0.5"}` glide from c4 to e4 in half of the step time + +» `event = sequence{"c4", "e4 g0.0"}` instantly glide from c4 to e4 + + ### Note Chord Strings To create a chords from a note string, append a `'` character to the key and specify a chord mode. diff --git a/examples/assets/bass.lua b/examples/assets/bass.lua index 9fd60926..1a4ac3be 100644 --- a/examples/assets/bass.lua +++ b/examples/assets/bass.lua @@ -1,18 +1 @@ --- local scale = scale("c5", {0,1,3,5,7,9,11}) -local scale = scale("c5", "natural minor") - -return pattern { - unit = "1/8", - pulse = pulse.from({ 1, 0.5, 1, 0 }, { 0, 1, 0, 0 }, { 1, 0, 1, 0 }, { 0, 1, 0, 1 }), - gate = function(context) - return context.pulse_value == 1.0 - end, - event = pulse.from(1, 3, 4, 1, 3, 4, -7):map(function(index, value) - if value < 0 then - return { key = scale.notes[-value] - 12, volume = 0.7 } - else - return { key = scale.notes[value], volume = 0.7 } - end - end - ) -} +return cycle "[C5 ~ D#5:v0.5 _ _ F5:g0.1:v0.4 _ <~ A#4:>:v0.4]:v0.6" diff --git a/examples/assets/bass.wav b/examples/assets/bass.wav index d0bf386c..732c575c 100644 Binary files a/examples/assets/bass.wav and b/examples/assets/bass.wav differ diff --git a/examples/play.rs b/examples/play.rs index 7d65f154..707e70b3 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -69,7 +69,7 @@ fn main() -> Result<(), Box> { )? .with_mappings(&[ ("bd", vec![new_note("c4")]), - ("bd2", vec![new_note(("c4", None, 0.5))]), + ("bd2", vec![new_note(("c4", None, None, 0.5))]), ]); let kick_pattern = beat_time @@ -140,45 +140,45 @@ fn main() -> Result<(), Box> { .with_instrument(BASS) .with_rhythm([1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1].to_rhythm()) .emit(new_note_sequence_emitter(vec![ - new_note((bass_notes[0], None, 0.5)), - new_note((bass_notes[2], None, 0.5)), - new_note((bass_notes[3], None, 0.5)), - new_note((bass_notes[0], None, 0.5)), - new_note((bass_notes[2], None, 0.5)), - new_note((bass_notes[3], None, 0.5)), - new_note((bass_notes[6].transposed(-12), None, 0.5)), + new_note((bass_notes[0], None, None, 0.33)), + new_note((bass_notes[2], None, Some(0.5), 0.25)), + new_note((bass_notes[3], None, None, 0.33)), + new_note((bass_notes[0], None, None, 0.33)), + new_note((bass_notes[2], None, Some(0.0), 0.25)), + new_note((bass_notes[3], None, None, 0.33)), + new_note((bass_notes[6].transposed(-12), None, Some(1.0), 0.33)), ])); let synth_pattern = beat_time.every_nth_bar(4.0).with_instrument(SYNTH).emit( new_polyphonic_note_sequence_emitter(vec![ vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("G 4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("G 4", None, None, 0.3)), ], vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("F 4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("F 4", None, None, 0.3)), ], vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("G 4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("G 4", None, None, 0.3)), ], vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("A#4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("A#4", None, None, 0.3)), ], ]), ); let fx_pattern = beat_time.every_nth_seconds(8.0).with_instrument(FX).emit( new_polyphonic_note_sequence_emitter(vec![ - vec![new_note(("C 4", None, 0.2)), None, None], - vec![None, new_note(("C 4", None, 0.2)), None], - vec![None, None, new_note(("F 4", None, 0.2))], + vec![new_note(("C 4", None, None, 0.2)), None, None], + vec![None, new_note(("C 4", None, None, 0.2)), None], + vec![None, None, new_note(("F 4", None, None, 0.2))], ]), ); diff --git a/examples/playground/Cargo.lock b/examples/playground/Cargo.lock index 4a57adbb..00b1b85e 100644 --- a/examples/playground/Cargo.lock +++ b/examples/playground/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -38,15 +38,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "basedrop" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47611e530cf21bb89fbfe322aa8b1feae1ace33ff65f8ebf40fe3b4b99b7c9a2" +checksum = "b0cab808e4f26ab216811b4b759e85823f9630a4da86e90e207bdc99cf565d32" [[package]] name = "bindgen" @@ -54,7 +54,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools", @@ -76,9 +76,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "serde", @@ -101,9 +101,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -113,10 +113,11 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.25" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -131,9 +132,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clang-sys" @@ -147,21 +148,21 @@ dependencies = [ ] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "libc", + "unicode-segmentation", ] [[package]] -name = "crossbeam-channel" -version = "0.5.15" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "crossbeam-utils", + "libc", ] [[package]] @@ -181,9 +182,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -205,21 +206,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", + "rustc_version", "syn", "unicode-xid", ] @@ -259,7 +262,7 @@ checksum = "9090516cd8e9eafb95f30dbb3a121bfd9420800ec380a9b077564e923c9dcf2d" dependencies = [ "bindgen", "emscripten-rs-macros", - "which 8.0.0", + "which", ] [[package]] @@ -279,12 +282,12 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -293,6 +296,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "four-cc" version = "0.4.0" @@ -311,14 +320,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "wasip2", ] [[package]] @@ -350,9 +359,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "lazy_static" @@ -362,9 +371,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libloading" @@ -378,48 +387,47 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lua-src" -version = "548.1.1" -source = "git+https://github.com/emuell/lua-src-rs?tag=mlua_v0.11.3#676430dd412b9c67c002f9ea84ebde0062ba1e40" +version = "550.0.0" +source = "git+https://github.com/emuell/lua-src-rs?tag=mlua_v0.11.6#f6bc67601f081aa04e194acd85a49e693da79f18" dependencies = [ "cc", ] [[package]] name = "luajit-src" -version = "210.6.1+f9140a6" +version = "210.6.6+707c12b" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "813bd31f2759443affa687c0d9c5eb5cf6cb0e898810ab197408431d746054bf" +checksum = "a86cc925d4053d0526ae7f5bc765dbd0d7a5d1a63d43974f4966cb349ca63295" dependencies = [ "cc", - "which 7.0.3", + "which", ] [[package]] @@ -433,9 +441,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "minimal-lexical" @@ -445,12 +453,13 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mlua" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3dd94c3c4dea0049b22296397040840a8f6b5b5229f438434ba82df402b42d" +checksum = "ccd36acfa49ce6ee56d1307a061dd302c564eee757e6e4cd67eb4f7204846fab" dependencies = [ "bstr", "either", + "libc", "mlua-sys", "num-traits", "parking_lot", @@ -460,12 +469,13 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.8.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4dc9cfc5a7698899802e97480617d9726f7da78c910db989d4d0fd4991d900" +checksum = "0f1c3a7fc7580227ece249fd90aa2fa3b39eb2b49d3aec5e103b3e85f2c3dfc8" dependencies = [ "cc", "cfg-if", + "libc", "lua-src", "luajit-src", "pkg-config", @@ -527,9 +537,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -537,15 +547,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", ] [[package]] @@ -558,7 +568,6 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" name = "pattrns" version = "0.9.3" dependencies = [ - "crossbeam-channel", "dashmap", "derive_more", "lazy_static", @@ -576,20 +585,19 @@ dependencies = [ [[package]] name = "pest" -version = "2.8.0" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", - "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -597,9 +605,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -610,26 +618,24 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ - "once_cell", "pest", "sha2", ] [[package]] name = "phonic" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1fd25dfb07bfc402b468201176aec0c510d9e09d9632133cf8646ec8889ea8" +checksum = "cd2f0d0889a4d8b7e7dc1070534cf07a50954886e1684993a1d7c1d1efd79b53" dependencies = [ "assume", "audio_thread_priority", "basedrop", "byteorder", - "crossbeam-channel", "crossbeam-queue", "dashmap", "emscripten-rs-macros", @@ -673,9 +679,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -683,9 +689,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -718,24 +724,24 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", @@ -753,9 +759,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom", ] @@ -783,18 +789,18 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.11.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -804,9 +810,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -815,9 +821,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "riff" @@ -841,30 +847,33 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "scopeguard" @@ -872,20 +881,36 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -894,14 +919,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -923,9 +949,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "strum" @@ -950,9 +976,9 @@ dependencies = [ [[package]] name = "symphonia" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" dependencies = [ "lazy_static", "symphonia-bundle-flac", @@ -968,9 +994,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-flac" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" dependencies = [ "log", "symphonia-core", @@ -980,9 +1006,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" dependencies = [ "lazy_static", "log", @@ -992,9 +1018,9 @@ dependencies = [ [[package]] name = "symphonia-codec-adpcm" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" dependencies = [ "log", "symphonia-core", @@ -1002,9 +1028,9 @@ dependencies = [ [[package]] name = "symphonia-codec-alac" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" +checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab" dependencies = [ "log", "symphonia-core", @@ -1012,9 +1038,9 @@ dependencies = [ [[package]] name = "symphonia-codec-pcm" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" dependencies = [ "log", "symphonia-core", @@ -1022,9 +1048,9 @@ dependencies = [ [[package]] name = "symphonia-core" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" dependencies = [ "arrayvec", "bitflags 1.3.2", @@ -1035,9 +1061,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" dependencies = [ "log", "symphonia-core", @@ -1047,9 +1073,9 @@ dependencies = [ [[package]] name = "symphonia-format-riff" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" dependencies = [ "extended", "log", @@ -1059,9 +1085,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" dependencies = [ "encoding_rs", "lazy_static", @@ -1071,9 +1097,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" dependencies = [ "symphonia-core", "symphonia-metadata", @@ -1081,40 +1107,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -1124,9 +1130,15 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-xid" @@ -1141,24 +1153,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "which" -version = "7.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" -dependencies = [ - "either", - "env_home", - "rustix", - "winsafe", + "wit-bindgen", ] [[package]] @@ -1174,9 +1174,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -1189,11 +1189,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] @@ -1267,30 +1267,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/playground/Cargo.toml b/examples/playground/Cargo.toml index 260edfa1..55ce48be 100644 --- a/examples/playground/Cargo.toml +++ b/examples/playground/Cargo.toml @@ -19,4 +19,4 @@ serde_json = { version = "^1.0" } [patch.crates-io] # custom patched version with pthread support -lua-src = { git = 'https://github.com/emuell/lua-src-rs', tag = "mlua_v0.11.3" } +lua-src = { git = 'https://github.com/emuell/lua-src-rs', tag = "mlua_v0.11.6" } diff --git a/examples/playground/build.rs b/examples/playground/build.rs index 777cf14f..5f58d516 100644 --- a/examples/playground/build.rs +++ b/examples/playground/build.rs @@ -9,8 +9,7 @@ fn main() { println!("cargo::rustc-link-arg=-sASSERTIONS=2"); } // compile options - println!("cargo::rustc-link-arg=-fexceptions"); - println!("cargo::rustc-link-arg=-sNO_DISABLE_EXCEPTION_CATCHING"); + println!("cargo::rustc-link-arg=-fwasm-exceptions"); println!("cargo::rustc-link-arg=-sUSE_PTHREADS=1"); println!("cargo::rustc-link-arg=-sPTHREAD_POOL_SIZE=4"); // memory options diff --git a/examples/playground/src/main.rs b/examples/playground/src/main.rs index 8b9b45c0..997fcfbf 100644 --- a/examples/playground/src/main.rs +++ b/examples/playground/src/main.rs @@ -171,6 +171,7 @@ impl Playground { let mut player = SamplePlayer::new(Arc::clone(&sample_pool), None)?; player.set_sample_root_note(Note::C4); player.set_new_note_action(NewNoteAction::Off(Some(Duration::from_millis(350)))); + player.set_playback_preload_time(Duration::from_secs_f64(Self::PLAYBACK_PRELOAD_SECONDS)); // sequence & pattern let sequence = None; @@ -294,11 +295,7 @@ impl Playground { pub fn start_playing(&mut self) { if !self.playing { // reset play head - let preload_offset = self - .time_base - .seconds_to_samples(Self::PLAYBACK_PRELOAD_SECONDS); - self.output_start_sample_time = - self.player.inner().output_sample_frame_position() + preload_offset; + self.output_start_sample_time = self.player.inner().output_sample_frame_position(); self.emitted_sample_time = 0; // reset sequence if let Some(sequence) = self.sequence.as_mut() { @@ -494,25 +491,23 @@ impl Playground { // run the player, when playing and audio output is not suspended if !suspended && (self.playing || !self.playing_notes.is_empty()) { - // calculate emitted and playback time differences - let time_base = self.time_base; - let output_sample_time = self.player.inner().output_sample_frame_position(); - let samples_played = // can be be negative, because we start with a preload offset - (output_sample_time as i64 - self.output_start_sample_time as i64).max(0) as u64; - let seconds_played = time_base.samples_to_seconds(samples_played); - let seconds_emitted = time_base.samples_to_seconds(self.emitted_sample_time); - // run sequence ahead of player up to PLAYBACK_PRELOAD_SECONDS seconds - let seconds_to_emit = - (seconds_played - seconds_emitted + Self::PLAYBACK_PRELOAD_SECONDS).max(0.0); - let samples_to_emit = time_base.seconds_to_samples(seconds_to_emit); - if seconds_to_emit > 4.0 * Self::PLAYBACK_PRELOAD_SECONDS { + // calculate samples to emit + let samples_to_emit = self.player.calculate_samples_to_emit( + &self.time_base, + self.output_start_sample_time, + self.emitted_sample_time, + ); + let playback_preload = self + .time_base + .seconds_to_samples(self.player.playback_preload_time().as_secs_f64()); + if samples_to_emit > 4 * playback_preload { // we lost too much time: maybe because the browser suspended the run loop self.player.advance_until_time( self.sequence.as_mut().unwrap(), self.emitted_sample_time + samples_to_emit, ); } else if samples_to_emit > 0 { - // continue running player to generate events in real-time + // continue generating events in real-time self.player.run_until_time( self.sequence.as_mut().unwrap(), self.output_start_sample_time, diff --git a/src/bindings.rs b/src/bindings.rs index cc21c240..6510a384 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -44,19 +44,24 @@ mod unwrap; // public re-exports pub use callback::{ - add_lua_callback_error, clear_lua_callback_errors, has_lua_callback_errors, lua_callback_errors, + add_lua_callback_error, clear_lua_callback_errors, has_lua_callback_errors, + lua_callback_errors, CallbackError, }; // internal re-exports pub(crate) use callback::{ContextPlaybackState, LuaCallback}; pub(crate) use timeout::LuaTimeoutHook; -pub(crate) use unwrap::{gate_trigger_from_value, note_events_from_value, pulse_from_value}; +pub(crate) use unwrap::{ + assign_cycle_vars_from_table, gate_trigger_from_value, note_events_from_value, pulse_from_value, +}; // --------------------------------------------------------------------------------------------- /// Global shared Lua data, unique in every new Lua instance. #[derive(Debug, Clone)] pub(crate) struct LuaAppData { + /// File name of the source that this app data is created for. + pub(crate) source: String, /// Global random seed, set by math.randomseed() for each Lua instance and passed to /// newly created pattern impls. pub(crate) rand_seed: Option, @@ -68,10 +73,12 @@ pub(crate) struct LuaAppData { impl LuaAppData { fn new() -> Self { + let source = String::new(); let rand_seed = None; let rand_rgn = Xoshiro256PlusPlus::from_seed(rand::rng().random()); let declared_globals = HashSet::new(); Self { + source, rand_seed, rand_rgn, declared_globals, @@ -118,6 +125,13 @@ pub fn new_pattern_from_file>( register_bindings(&mut lua, &timeout_hook, &time_base)?; // restart the timeout hook timeout_hook.reset(); + // memorize source origin in app_data + { + let mut app_data = lua + .app_data_mut::() + .expect("Failed to access Lua app data"); + app_data.source = file_path.as_ref().to_string_lossy().to_string(); + } // compile and evaluate script let chunk = lua.load(file_path.as_ref()); let result = chunk.eval::()?; @@ -141,6 +155,13 @@ pub fn new_pattern_from_string( register_bindings(&mut lua, &timeout_hook, &time_base)?; // restart the timeout hook timeout_hook.reset(); + // memorize source origin in app_data + { + let mut app_data = lua + .app_data_mut::() + .expect("Failed to access Lua app data"); + app_data.source = format!("[string \"{}\"]", script_name); + } // compile and evaluate script let chunk = lua.load(script).set_name(script_name); let result = chunk.eval::()?; @@ -277,14 +298,15 @@ fn register_global_bindings( // function cycle(input) globals.raw_set( "cycle", - lua.create_function(|lua, arg: LuaString| -> LuaResult { + lua.create_function(|lua, cycle: LuaString| -> LuaResult { // NB: don't keep borrowing app_data_ref here - let rand_seed = { - lua.app_data_ref::() - .expect("Failed to access Lua app data") - .rand_seed + let (source, rand_seed) = { + let app_data = lua + .app_data_ref::() + .expect("Failed to access Lua app data"); + (app_data.source.clone(), app_data.rand_seed) }; - CycleUserData::from(arg, rand_seed) + CycleUserData::from(cycle, &source, rand_seed) })?, )?; @@ -795,6 +817,8 @@ fn compile_chunk(chunk: &'static str, _name: &'static str) -> LuaResult> mod test { use super::*; + // use pretty_assertions::assert_eq; + fn new_test_engine( beats_per_min: f32, beats_per_bar: u32, @@ -849,7 +873,7 @@ mod test { .load( r#" local i = 0 - while true do + while true do i = i + 1 end "#, @@ -862,7 +886,7 @@ mod test { .load( r#" local i = 0 - while i < 100 do + while i < 100 do i = i + 1 end "#, diff --git a/src/bindings/callback.rs b/src/bindings/callback.rs index e4b4f3a2..3cee1ee4 100644 --- a/src/bindings/callback.rs +++ b/src/bindings/callback.rs @@ -1,4 +1,9 @@ -use std::{cell::RefCell, collections::HashMap, fmt::Debug, rc::Rc}; +use std::{ + cell::RefCell, + collections::HashMap, + fmt::{Debug, Display}, + rc::Rc, +}; use mlua::prelude::*; @@ -9,8 +14,41 @@ use crate::{BeatTimeBase, Event, Parameter, ParameterSet, RhythmEvent}; // ------------------------------------------------------------------------------------------------- +/// Wraps a Lua runtime error and info about where it happens. +#[derive(Debug, Clone)] +pub struct CallbackError { + /// Source file or `[string = "XYZ"]` where the error happened. + pub source: Option, + /// Source file's line where the error happened, or -1 when unknown. + pub source_line: Option, + /// Name of the function which was called when the error happened. + pub function: String, + /// The actual error. + pub error: LuaError, +} + +/// Format Callback error to a Lua compiler alike error string. +impl Display for CallbackError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let source = self.source.clone().unwrap_or("unknown src".to_string()); + let source_line = if let Some(line) = self.source_line { + line.to_string() + } else { + "".to_string() + }; + let err = self.error.to_string(); + if let Some(stripped_err) = err.strip_prefix("runtime error: ") { + f.write_fmt(format_args!( + "runtime error: {source}:{source_line}: {stripped_err}" + )) + } else { + f.write_fmt(format_args!("{source}:{source_line}: {err}")) + } + } +} + lazy_static! { - static ref LUA_CALLBACK_ERRORS: RwLock> = Vec::new().into(); + static ref LUA_CALLBACK_ERRORS: RwLock> = Vec::new().into(); } /// Returns some error if there are any Lua callback errors, with the !first! error that happened. @@ -18,7 +56,7 @@ lazy_static! { /// /// ### Panics /// Panics if accessing the global lua callback error vector fails. -pub fn has_lua_callback_errors() -> Option { +pub fn has_lua_callback_errors() -> Option { LUA_CALLBACK_ERRORS .read() .expect("Failed to lock Lua callback error vector") @@ -31,7 +69,7 @@ pub fn has_lua_callback_errors() -> Option { /// /// ### Panics /// Panics if accessing the global lua callback error vector failed. -pub fn lua_callback_errors() -> Vec { +pub fn lua_callback_errors() -> Vec { LUA_CALLBACK_ERRORS .read() .expect("Failed to lock Lua callback error vector") @@ -53,12 +91,22 @@ pub fn clear_lua_callback_errors() { /// /// ### Panics /// Panics if accessing the global lua callback error vector failed. -pub fn add_lua_callback_error(name: &str, err: &LuaError) { - log::warn!("Lua callback '{}' failed to evaluate:\n{}", name, err); +pub fn add_lua_callback_error( + source: Option, + source_line: Option, + function: String, + error: LuaError, +) { + // log::warn!("{source}:{source_line}: Lua callback '{name}' failed to evaluate:\n{err}"); LUA_CALLBACK_ERRORS .write() .expect("Failed to lock Lua callback error vector") - .push(err.clone()); + .push(CallbackError { + source, + source_line, + function, + error, + }); } // ------------------------------------------------------------------------------------------------- @@ -163,6 +211,17 @@ impl LuaCallback { } } + /// Name of the source file for errors. + #[allow(clippy::misnamed_getters)] + pub fn source(&self) -> Option { + self.function.info().short_src + } + + /// Line number in source where the function is defined for errors. + pub fn source_line(&self) -> Option { + self.function.info().line_defined + } + /// Name of the inner function for errors. Usually will be an anonymous function. pub fn name(&self) -> String { self.function @@ -343,7 +402,7 @@ impl LuaCallback { /// Report a Lua callback errors. The error will be logged and usually cleared after /// the next callback call. pub fn handle_error(&self, err: &LuaError) { - add_lua_callback_error(&self.name(), err) + add_lua_callback_error(self.source(), self.source_line(), self.name(), err.clone()) } /// Reset the callback function or iterator to its initial state. @@ -554,6 +613,8 @@ mod test { }, }; + use pretty_assertions::assert_eq; + fn new_test_engine( beats_per_min: f32, beats_per_bar: u32, @@ -668,6 +729,7 @@ mod test { let trigger_event = Event::NoteEvents(vec![Some(NoteEvent { note: Note::A4, instrument: None, + glide: Some(5.0), volume: 0.5, panning: 0.0, delay: 0.25, @@ -677,6 +739,7 @@ mod test { let trigger_event2 = Event::NoteEvents(vec![Some(NoteEvent { note: Note::C4, instrument: None, + glide: None, volume: 1.0, panning: -1.0, delay: 0.5, diff --git a/src/bindings/cycle.rs b/src/bindings/cycle.rs index c3cb7700..5ba8fd2d 100644 --- a/src/bindings/cycle.rs +++ b/src/bindings/cycle.rs @@ -1,6 +1,6 @@ use mlua::prelude::*; -use crate::{event::NoteEvent, tidal::Cycle}; +use crate::{bindings::unwrap::assign_cycle_vars_from_table, event::NoteEvent, tidal::Cycle}; use super::unwrap::{bad_argument_error, note_events_from_value}; @@ -12,20 +12,27 @@ pub struct CycleUserData { pub cycle: Cycle, pub mappings: Vec<(String, Vec>)>, pub mapping_function: Option, + pub variables_function: Option, } impl CycleUserData { - pub fn from(arg: LuaString, seed: Option) -> LuaResult { + pub fn from(arg: LuaString, source: &str, seed: Option) -> LuaResult { let mut cycle = Cycle::from(&arg.to_string_lossy()).map_err(LuaError::runtime)?; + if !source.is_empty() { + cycle = cycle.with_source(source); + } if let Some(seed) = seed { cycle = cycle.with_seed(seed); } + let mappings = Vec::new(); let mapping_function = None; + let variables_function = None; Ok(CycleUserData { cycle, mappings, mapping_function, + variables_function, }) } } @@ -33,27 +40,45 @@ impl CycleUserData { impl LuaUserData for CycleUserData { fn add_methods>(methods: &mut M) { methods.add_method_mut("map", |_lua, this, value: LuaValue| match value { - LuaValue::Function(func) => { - let cycle = this.cycle.clone(); - let mappings = Vec::new(); - let mapping_function = Some(func); - Ok(CycleUserData { - cycle, - mappings, - mapping_function, - }) - } + LuaValue::Function(func) => Ok(Self { + mapping_function: Some(func), + mappings: Vec::new(), + ..this.clone() + }), LuaValue::Table(table) => { - let cycle = this.cycle.clone(); let mut mappings = Vec::new(); for (k, v) in table.pairs::().flatten() { mappings.push((k.to_string()?, note_events_from_value(&v, None)?)); } - let mapping_function = None; - Ok(CycleUserData { - cycle, + Ok(Self { + mapping_function: None, mappings, - mapping_function, + ..this.clone() + }) + } + _ => Err(bad_argument_error( + None, + "map", + 1, + format!( + "map argument must be a table but is a '{}'", + value.type_name() + ) + .as_str(), + )), + }); + + methods.add_method_mut("var", |_lua, this, value: LuaValue| match value { + LuaValue::Table(table) => { + let mut cloned = this.clone(); + assign_cycle_vars_from_table(&mut cloned.cycle, table)?; + Ok(cloned) + } + LuaValue::Function(func) => { + this.cycle.clear_vars(); + Ok(Self { + variables_function: Some(func), + ..this.clone() }) } _ => Err(bad_argument_error( @@ -82,9 +107,11 @@ mod test { bindings::*, emitter::{cycle::CycleEmitter, scripted_cycle::ScriptedCycleEmitter}, event::new_note, - Emitter, Event, Note, RhythmEvent, + CycleSubCycle, Emitter, Event, Note, RhythmEvent, }; + use pretty_assertions::assert_eq; + fn new_test_engine() -> LuaResult<(Lua, LuaTimeoutHook)> { new_test_engine_with_timebase(&BeatTimeBase { beats_per_min: 120.0, @@ -119,6 +146,31 @@ mod test { Ok(()) } + #[test] + fn variables() -> LuaResult<()> { + let (lua, _) = new_test_engine()?; + + let mapped_cycle = evaluate_cycle_userdata( + &lua, + r#"cycle("a b c"):var({x = "c0", y = "b4:v0.5", z = "[a b c]"})"#, + )?; + + assert_eq!( + mapped_cycle.cycle.get_var("x").expect("x should exist"), + CycleSubCycle::from("c0").expect("valid subcycle") + ); + + assert_eq!( + mapped_cycle.cycle.get_var("y").expect("y should exist"), + CycleSubCycle::from("b4:v0.5").expect("valid subcycle") + ); + + assert!(mapped_cycle.cycle.get_var("a").is_none()); + assert!(mapped_cycle.cycle.get_var("z").is_some()); + + Ok(()) + } + #[test] fn mappings() -> LuaResult<()> { let (lua, _) = new_test_engine()?; @@ -155,7 +207,7 @@ mod test { ); // check note properties - let mapped_cycle = evaluate_cycle_userdata(&lua, r#"cycle("a:1:v0.1:p-1.0:d0.3")"#)?; + let mapped_cycle = evaluate_cycle_userdata(&lua, r#"cycle("a:1:g0.1:v0.1:p-1.0:d0.3")"#)?; let mut event_iter = CycleEmitter::new(mapped_cycle.cycle).with_mappings(&mapped_cycle.mappings); assert_eq!( @@ -165,9 +217,10 @@ mod test { Some(vec![Event::NoteEvents(vec![new_note(( Note::A4, InstrumentId::from(1), + Some(0.1), 0.1, -1.0, - 0.3 + 0.3, ))]),]) ); @@ -201,8 +254,13 @@ mod test { .run(RhythmEvent::default(), true) .map(|events| events.into_iter().map(|e| e.event).collect::>()), Some(vec![ - Event::NoteEvents(vec![new_note((Note::C4, InstrumentId::from(1), 0.1))]), - Event::NoteEvents(vec![new_note((Note::C4, InstrumentId::from(66), 1.0))]) + Event::NoteEvents(vec![new_note((Note::C4, InstrumentId::from(1), None, 0.1))]), + Event::NoteEvents(vec![new_note(( + Note::C4, + InstrumentId::from(66), + None, + 1.0 + ))]) ]) ); @@ -222,7 +280,7 @@ mod test { let mapped_cycle = evaluate_cycle_userdata( &lua, r#" - cycle("wurst a b c"):map(function(context, value) + cycle("wurst a b c"):map(function(context, value) assert(context.beats_per_min, 120) assert(context.beats_per_bar, 4) assert(context.samples_per_sec, 44100) @@ -237,6 +295,7 @@ mod test { LuaCallback::new(&lua, mapped_cycle.mapping_function.unwrap().clone())?; let mut event_iter = ScriptedCycleEmitter::with_mapping_callback( mapped_cycle.cycle, + None, &timeout_hook, mapping_callback, &time_base, diff --git a/src/bindings/note.rs b/src/bindings/note.rs index 4e1f2276..ced52d52 100644 --- a/src/bindings/note.rs +++ b/src/bindings/note.rs @@ -2,9 +2,9 @@ use mlua::prelude::*; use super::unwrap::{ amplify_array_from_value, bad_argument_error, chord_events_from_intervals, - chord_events_from_mode, delay_array_from_value, instrument_array_from_value, - note_events_from_value, panning_array_from_value, sequence_from_value, - transpose_steps_array_from_value, volume_array_from_value, + chord_events_from_mode, delay_array_from_value, glide_array_from_value, + instrument_array_from_value, note_events_from_value, panning_array_from_value, + sequence_from_value, transpose_steps_array_from_value, volume_array_from_value, }; use crate::{ @@ -211,6 +211,28 @@ impl LuaUserData for NoteUserData { drop(this); Ok(ud) }); + + methods.add_function("glide", |lua, (ud, value): (LuaAnyUserData, LuaValue)| { + let mut this = ud.borrow_mut::()?; + let glides = glide_array_from_value(lua, value, this.notes.len())?; + for (note, glide) in this.notes.iter_mut().zip(glides.into_iter()) { + if let Some(glide) = glide { + if !(0.0..).contains(&glide) { + return Err(bad_argument_error( + "glide", + "value", + 1, + "glide must be in range [0.0..]", + )); + } + } + if let Some(note) = note { + note.glide = glide; + } + } + drop(this); + Ok(ud) + }); } } @@ -221,6 +243,8 @@ mod test { use super::*; use crate::{bindings::*, event::new_note}; + use pretty_assertions::assert_eq; + fn new_test_engine() -> LuaResult<(Lua, LuaTimeoutHook)> { let (mut lua, mut timeout_hook) = new_engine()?; register_bindings( @@ -278,15 +302,23 @@ mod test { assert!(evaluate_note_userdata(&lua, r#"note("C#1 v-2.0")"#).is_err()); assert!(evaluate_note_userdata(&lua, r#"note("C#1 p-1.0")"#).is_ok()); assert!(evaluate_note_userdata(&lua, r#"note("C#1 d-1.0")"#).is_err()); - let note_event = evaluate_note_userdata(&lua, r#"note("C#1 #2 v0.5 p0.1 d0.2")"#)?; + assert!(evaluate_note_userdata(&lua, r#"note("C#1 g-1.0")"#).is_err()); + let note_event = evaluate_note_userdata(&lua, r#"note("C#1 #2 v0.5 p0.1 d0.2 g5")"#)?; assert_eq!( note_event.notes, - vec![new_note((Note::Cs1, InstrumentId::from(2), 0.5, 0.1, 0.2))] + vec![new_note(( + Note::Cs1, + InstrumentId::from(2), + Some(5.0), + 0.5, + 0.1, + 0.2, + ))] ); let note_event = evaluate_note_userdata(&lua, r#"note("C#1 d0.2")"#)?; assert_eq!( note_event.notes, - vec![new_note((Note::Cs1, None, 1.0, 0.0, 0.2))] + vec![new_note((Note::Cs1, None, None, 1.0, 0.0, 0.2))] ); // Note string array @@ -299,7 +331,10 @@ mod test { let note_event = evaluate_note_userdata(&lua, r#"note({"C#1 v0.5", "C5"})"#)?; assert_eq!( note_event.notes, - vec![new_note(("c#1", None, 0.5)), new_note(("c5", None, 1.0))] + vec![ + new_note(("c#1", None, None, 0.5)), + new_note(("c5", None, None, 1.0)) + ] ); // Note int @@ -328,7 +363,7 @@ mod test { let note_event = evaluate_note_userdata(&lua, r#"note({key = "c8"})"#)?; assert_eq!(note_event.notes, vec![new_note("c8")]); let note_event = evaluate_note_userdata(&lua, r#"note({key = "G8", volume = 0.5})"#)?; - assert_eq!(note_event.notes, vec![new_note(("g8", None, 0.5))]); + assert_eq!(note_event.notes, vec![new_note(("g8", None, None, 0.5))]); let note_event = evaluate_note_userdata(&lua, r#"note({key = 60})"#)?; assert_eq!(note_event.notes, vec![new_note("c5")]); let note_event = evaluate_note_userdata(&lua, r#"note({key = "60"})"#)?; @@ -342,8 +377,8 @@ mod test { assert_eq!( poly_note_event.notes, vec![ - new_note(("c#1", None, 0.5)), - new_note(("g2", None, 0.75)), + new_note(("c#1", None, None, 0.5)), + new_note(("g2", None, None, 0.75)), None ] ); @@ -352,11 +387,11 @@ mod test { assert_eq!( evaluate_note_userdata( &lua, - r#"note(note("c4 #100 v0.2 p0.3 d0.4", "", "e4").notes)"# + r#"note(note("c4 #100 g8 v0.2 p0.3 d0.4", "", "e4").notes)"# )? .notes, vec![ - new_note(("c4", InstrumentId::from(100), 0.2, 0.3, 0.4)), + new_note(("c4", InstrumentId::from(100), Some(8.0), 0.2, 0.3, 0.4,)), None, new_note("e4"), ] @@ -397,17 +432,17 @@ mod test { assert_eq!( evaluate_note_userdata(&lua, r#"note("c7'maj v0.2")"#)?.notes, vec![ - new_note(("c7", None, 0.2)), - new_note(("e7", None, 0.2)), - new_note(("g7", None, 0.2)), + new_note(("c7", None, None, 0.2)), + new_note(("e7", None, None, 0.2)), + new_note(("g7", None, None, 0.2)), ] ); assert_eq!( evaluate_note_userdata(&lua, r#"note("c4'm v0.2", "c7")"#)?.notes, vec![ - new_note(("c4", None, 0.2)), - new_note(("d#4", None, 0.2)), - new_note(("g4", None, 0.2)), + new_note(("c4", None, None, 0.2)), + new_note(("d#4", None, None, 0.2)), + new_note(("g4", None, None, 0.2)), new_note("c7"), ] ); @@ -451,9 +486,9 @@ mod test { evaluate_note_userdata(&lua, r#"note("c4 v0.5", "d4 v0.5", "e4 v0.5"):volume(0.2)"#)? .notes, vec![ - new_note(("c4", None, 0.2)), - new_note(("d4", None, 0.2)), - new_note(("e4", None, 0.2)), + new_note(("c4", None, None, 0.2)), + new_note(("d4", None, None, 0.2)), + new_note(("e4", None, None, 0.2)), ] ); assert_eq!( @@ -463,9 +498,9 @@ mod test { )? .notes, vec![ - new_note(("c4", None, 0.0)), - new_note(("d4", None, 0.0)), - new_note(("e4", None, 0.5)), + new_note(("c4", None, None, 0.0)), + new_note(("d4", None, None, 0.0)), + new_note(("e4", None, None, 0.5)), ] ); assert_eq!( @@ -475,9 +510,9 @@ mod test { )? .notes, vec![ - new_note(("c4", None, 0.1)), - new_note(("d4", None, 0.2)), - new_note(("e4", None, 0.3)), + new_note(("c4", None, None, 0.1)), + new_note(("d4", None, None, 0.2)), + new_note(("e4", None, None, 0.3)), ] ); @@ -489,9 +524,9 @@ mod test { )? .notes, vec![ - new_note(("c4", None, 1.0)), - new_note(("d4", None, 1.0)), - new_note(("e4", None, 1.0)), + new_note(("c4", None, None, 1.0)), + new_note(("d4", None, None, 1.0)), + new_note(("e4", None, None, 1.0)), ] ); assert_eq!( @@ -501,9 +536,9 @@ mod test { )? .notes, vec![ - new_note(("c4", None, 0.5)), - new_note(("d4", None, 1.0)), - new_note(("e4", None, 0.5)), + new_note(("c4", None, None, 0.5)), + new_note(("d4", None, None, 1.0)), + new_note(("e4", None, None, 0.5)), ] ); Ok(()) @@ -523,17 +558,17 @@ mod test { assert_eq!( evaluate_note_userdata(&lua, r#"note("c4", "d4", "e4"):panning(-1.0)"#)?.notes, vec![ - new_note(("c4", None, 1.0, -1.0)), - new_note(("d4", None, 1.0, -1.0)), - new_note(("e4", None, 1.0, -1.0)), + new_note(("c4", None, None, 1.0, -1.0)), + new_note(("d4", None, None, 1.0, -1.0)), + new_note(("e4", None, None, 1.0, -1.0)), ] ); assert_eq!( evaluate_note_userdata(&lua, r#"note("c4", "d4", "e4"):panning({-1.0, 1.0})"#)?.notes, vec![ - new_note(("c4", None, 1.0, -1.0)), - new_note(("d4", None, 1.0, 1.0)), - new_note(("e4", None, 1.0, 0.0)), + new_note(("c4", None, None, 1.0, -1.0)), + new_note(("d4", None, None, 1.0, 1.0)), + new_note(("e4", None, None, 1.0, 0.0)), ] ); Ok(()) @@ -553,17 +588,49 @@ mod test { assert_eq!( evaluate_note_userdata(&lua, r#"note("c4", "d4", "e4"):delay(0.75)"#)?.notes, vec![ - new_note(("c4", None, 1.0, 0.0, 0.75)), - new_note(("d4", None, 1.0, 0.0, 0.75)), - new_note(("e4", None, 1.0, 0.0, 0.75)), + new_note(("c4", None, None, 1.0, 0.0, 0.75)), + new_note(("d4", None, None, 1.0, 0.0, 0.75)), + new_note(("e4", None, None, 1.0, 0.0, 0.75)), ] ); assert_eq!( evaluate_note_userdata(&lua, r#"note("c4", "d4", "e4"):delay({0.25, 0.5})"#)?.notes, vec![ - new_note(("c4", None, 1.0, 0.0, 0.25)), - new_note(("d4", None, 1.0, 0.0, 0.5)), - new_note(("e4", None, 1.0, 0.0)), + new_note(("c4", None, None, 1.0, 0.0, 0.25)), + new_note(("d4", None, None, 1.0, 0.0, 0.5)), + new_note(("e4", None, None, 1.0, 0.0)), + ] + ); + + Ok(()) + } + + #[test] + fn note_glide() -> LuaResult<()> { + let (lua, _) = new_test_engine()?; + + // delay + assert!(evaluate_note_userdata(&lua, r#"note("c4"):glide(1.0)"#).is_ok()); + assert!(evaluate_note_userdata(&lua, r#"note("c4"):glide()"#).is_err()); + assert!(evaluate_note_userdata(&lua, r#"note("c4"):glide(-1)"#).is_err()); + assert!(evaluate_note_userdata(&lua, r#"note("c4"):glide({})"#).is_ok()); + assert!(evaluate_note_userdata(&lua, r#"note("c4"):glide({"wurst"})"#).is_err()); + assert!(evaluate_note_userdata(&lua, r#"note("c4"):glide({nil, 1})"#).is_ok()); + assert_eq!( + evaluate_note_userdata(&lua, r#"note("c4", "d4", "e4"):glide(0.5)"#)?.notes, + vec![ + new_note(("c4", None, Some(0.5), 1.0, 0.0, 0.0,)), + new_note(("d4", None, Some(0.5), 1.0, 0.0, 0.0,)), + new_note(("e4", None, Some(0.5), 1.0, 0.0, 0.0,)), + ] + ); + assert_eq!( + evaluate_note_userdata(&lua, r#"note("c4", "d4", "e4"):glide({0.25, nil, 0.5})"#)? + .notes, + vec![ + new_note(("c4", None, Some(0.25), 1.0, 0.0, 0.0,)), + new_note(("d4", None, None, 1.0, 0.0, 0.0)), + new_note(("e4", None, Some(0.5), 1.0, 0.0, 0.0)), ] ); diff --git a/src/bindings/parameter.rs b/src/bindings/parameter.rs index 559ff6f1..d110b24b 100644 --- a/src/bindings/parameter.rs +++ b/src/bindings/parameter.rs @@ -19,6 +19,8 @@ mod test { use super::*; use crate::bindings::*; + // use pretty_assertions::assert_eq; + fn new_test_engine() -> LuaResult { // create a new engine and register bindings let (mut lua, mut timeout_hook) = new_engine()?; diff --git a/src/bindings/pattern.rs b/src/bindings/pattern.rs index 9cbfee07..56be1d0c 100644 --- a/src/bindings/pattern.rs +++ b/src/bindings/pattern.rs @@ -79,6 +79,8 @@ mod test { RhythmEvent, }; + use pretty_assertions::assert_eq; + fn new_test_engine( beats_per_min: f32, beats_per_bar: u32, @@ -154,11 +156,12 @@ mod test { Some(PatternEvent { time: 22050, event: Some(Event::NoteEvents(vec![Some(NoteEvent { - instrument: None, note: Note::C6, + instrument: None, + glide: None, volume: 1.0, panning: 0.0, - delay: 0.0 + delay: 0.0, })])), duration: 11025 }) @@ -173,6 +176,7 @@ mod test { let trigger_event = Event::NoteEvents(vec![Some(NoteEvent { note: Note::A4, instrument: None, + glide: Some(0.5), volume: 0.5, panning: 0.0, delay: 0.25, @@ -196,6 +200,7 @@ mod test { assert(trigger_notes[1].volume == 0.5) assert(trigger_notes[1].panning == 0.0) assert(trigger_notes[1].delay == 0.25) + assert(trigger_notes[1].glide == 0.5) assert(context.pulse_step == pulse_step) assert(context.pulse_time_step == pulse_time_step) end @@ -273,11 +278,12 @@ mod test { Some(PatternEvent { time: 0, event: Some(Event::NoteEvents(vec![Some(NoteEvent { - instrument: None, note: Note::C4, + instrument: None, + glide: None, volume: 1.0, panning: 0.0, - delay: 0.0 + delay: 0.0, })])), duration: 11025, }) @@ -304,7 +310,7 @@ mod test { resolution = 2, offset = 3, pulse = {1,0,1,0}, - event = {"c5", "c5 v0.4", {"c7", "c7 v1.0"}} + event = {"c5", "c5 v0.4", {"c7", "c7 v1.0 g5"}} } "#, ) @@ -351,6 +357,7 @@ mod test { let trigger_event = Event::NoteEvents(vec![Some(NoteEvent { note: Note::C4, instrument: None, + glide: None, volume: 0.25, panning: 0.5, delay: 0.75, @@ -391,11 +398,12 @@ mod test { Some(PatternEvent { time: 0, event: Some(Event::NoteEvents(vec![Some(NoteEvent { - instrument: None, note: Note::C4, + instrument: None, + glide: None, volume: 1.0, panning: 0.0, - delay: 0.0 + delay: 0.0, })],),), duration: 48 }) diff --git a/src/bindings/scale.rs b/src/bindings/scale.rs index 48df54c9..62ccb3b8 100644 --- a/src/bindings/scale.rs +++ b/src/bindings/scale.rs @@ -134,6 +134,8 @@ mod test { use super::*; use crate::bindings::*; + use pretty_assertions::assert_eq; + fn new_test_engine() -> LuaResult { // create a new engine and register bindings let (mut lua, mut timeout_hook) = new_engine()?; diff --git a/src/bindings/sequence.rs b/src/bindings/sequence.rs index d2b28e31..90f7a159 100644 --- a/src/bindings/sequence.rs +++ b/src/bindings/sequence.rs @@ -7,6 +7,7 @@ use super::unwrap::{ }; use crate::{ + bindings::unwrap::glide_array_from_value, event::{InstrumentId, NoteEvent}, note::Note, }; @@ -161,7 +162,7 @@ impl LuaUserData for SequenceUserData { if !(-1.0..=1.0).contains(&panning) { return Err(bad_argument_error( "panning", - "volume", + "value", 1, "panning must be in range [-1.0..=1.0]", )); @@ -181,7 +182,7 @@ impl LuaUserData for SequenceUserData { if !(0.0..=1.0).contains(&delay) { return Err(bad_argument_error( "delay", - "panning", + "value", 1, "delay must be in range [-1.0..=1.0]", )); @@ -193,6 +194,28 @@ impl LuaUserData for SequenceUserData { drop(this); Ok(ud) }); + + methods.add_function("glide", |lua, (ud, value): (LuaAnyUserData, LuaValue)| { + let mut this = ud.borrow_mut::()?; + let glides = glide_array_from_value(lua, value, this.notes.len())?; + for (notes, glide) in this.notes.iter_mut().zip(glides) { + if let Some(glide) = glide { + if !(0.0..).contains(&glide) { + return Err(bad_argument_error( + "glide", + "value", + 1, + "glide must be in range [0.0..]", + )); + } + } + for note in notes.iter_mut().flatten() { + note.glide = glide; + } + } + drop(this); + Ok(ud) + }); } } @@ -203,6 +226,8 @@ mod test { use super::*; use crate::{bindings::*, event::new_note}; + use pretty_assertions::assert_eq; + fn evaluate_sequence_userdata(lua: &Lua, expression: &str) -> LuaResult { Ok(lua .load(expression) @@ -236,9 +261,9 @@ mod test { assert_eq!( note_sequence_event.notes, vec![ - vec![new_note(("c#1", None, 0.5))], + vec![new_note(("c#1", None, None, 0.5))], vec![None], - vec![new_note(("g2", None, 1.0))] + vec![new_note(("g2", None, None, 1.0))] ] ); let poly_note_sequence_event = evaluate_sequence_userdata( @@ -252,14 +277,14 @@ mod test { poly_note_sequence_event.notes, vec![ vec![ - new_note(("c#1", None, 1.0)), + new_note(("c#1", None, None, 1.0)), None, - new_note(("g2", None, 0.75)), + new_note(("g2", None, None, 0.75)), ], vec![ - new_note(("a#5", None, 0.2)), + new_note(("a#5", None, None, 0.2)), None, - new_note(("b1", None, 0.1)) + new_note(("b1", None, None, 0.1)) ] ] ); @@ -282,9 +307,9 @@ mod test { vec![ vec![new_note("c4"), new_note("e4"), new_note("g4"),], vec![ - new_note(("a#5", None, 0.2)), + new_note(("a#5", None, None, 0.2)), None, - new_note(("b1", None, 0.1)) + new_note(("b1", None, None, 0.1)) ] ] ); @@ -324,12 +349,12 @@ mod test { assert_eq!( evaluate_sequence_userdata( &lua, - r#"sequence(sequence{{"c4 #1 v0.2 p0.3 d0.4", "d4"}, {}, {"e4"}}.notes)"# + r#"sequence(sequence{{"c4 #1 g0.5 v0.2 p0.3 d0.4", "d4"}, {}, {"e4"}}.notes)"# )? .notes, vec![ vec![ - new_note(("c4", InstrumentId::from(1), 0.2, 0.3, 0.4)), + new_note(("c4", InstrumentId::from(1), Some(0.5), 0.2, 0.3, 0.4)), new_note("d4"), ], vec![None], @@ -357,6 +382,10 @@ mod test { evaluate_sequence_userdata(&lua, r#"sequence({key = "c"}, "d", "f"):delay(0.0)"#) .is_ok() ); + assert!( + evaluate_sequence_userdata(&lua, r#"sequence({key = "c"}, "d", "f"):glide(2.0)"#) + .is_ok() + ); assert!(evaluate_sequence_userdata( &lua, // r#"sequence("c", "d", "f"):transpose({1, 2})"# @@ -374,12 +403,29 @@ mod test { evaluate_sequence_userdata(&lua, r#"sequence("c", "d", "f"):delay({0.0, 0.25})"#) .is_ok() ); + assert!( + evaluate_sequence_userdata(&lua, r#"sequence("c", "d", "f"):glide({0.0, 0.5})"#) + .is_ok() + ); assert!(evaluate_sequence_userdata( &lua, r#"sequence("c", "d", "f"):volume(1.0):delay({0.0, 0.25})"# ) .is_ok()); + assert_eq!( + evaluate_sequence_userdata( + &lua, + r#"sequence{"c4", "d4", "e4"}:glide({1.0, nil, 2.0})"# + )? + .notes, + vec![ + vec![new_note(("c4", None, Some(1.0), 1.0, 0.0, 0.0,)),], + vec![new_note(("d4", None, None, 1.0, 0.0, 0.0)),], + vec![new_note(("e4", None, Some(2.0), 1.0, 0.0, 0.0,)),], + ] + ); + Ok(()) } } diff --git a/src/bindings/unwrap.rs b/src/bindings/unwrap.rs index d25ba280..37716aa4 100644 --- a/src/bindings/unwrap.rs +++ b/src/bindings/unwrap.rs @@ -10,6 +10,7 @@ use crate::{ parameter::ParameterUserData, sequence::SequenceUserData, LuaTimeoutHook, }, prelude::*, + CycleSubCycle, }; // --------------------------------------------------------------------------------------------- @@ -94,6 +95,9 @@ impl IntoLua for NoteEvent { LuaInteger::try_from(usize::from(instrument)).unwrap_or(LuaInteger::MAX), )?; } + if let Some(glide) = self.glide { + table.set("glide", glide as f64)?; + } table.set("volume", self.volume as f64)?; table.set("panning", self.panning as f64)?; table.set("delay", self.delay as f64)?; @@ -195,6 +199,47 @@ pub(crate) fn sequence_from_table(table: &LuaTable) -> Option> { // --------------------------------------------------------------------------------------------- +fn optional_float_array_from_value( + lua: &Lua, + value: LuaValue, + array_len: usize, + name: &str, + range: Range, +) -> LuaResult>> +where + Range: RangeBounds + std::fmt::Debug, +{ + // NB: use pairs instead of sequence values to not stop at nil values in the array + let mut values: Vec> = vec![]; + if let Some(value_table) = value.as_table() { + for value in value_table.pairs::() { + let (index, value) = value?; + if index >= 1 { + values.resize(index as usize, None); + if value != LuaValue::Nil { + values[index as usize - 1] = Some(f32::from_lua(value, lua)?); + } + } + } + } else { + let value = f32::from_lua(value, lua)?; + values = (0..array_len) + .map(|_| Some(value)) + .collect::>>(); + } + for value in values.iter().flatten() { + if !range.contains(value) { + return Err(bad_argument_error( + None, + name, + 1, + format!("{} must be in range [{:?}] but is '{}'", name, range, value).as_str(), + )); + } + } + Ok(values) +} + fn float_array_from_value( lua: &Lua, value: LuaValue, @@ -309,26 +354,33 @@ pub(crate) fn delay_array_from_value( float_array_from_value(lua, value, array_len, "delay", 0.0..=1.0) } +pub(crate) fn glide_array_from_value( + lua: &Lua, + value: LuaValue, + array_len: usize, +) -> LuaResult>> { + optional_float_array_from_value(lua, value, array_len, "glide", 0.0..) +} + // --------------------------------------------------------------------------------------------- -fn float_value_from_table( +fn optional_float_value_from_table( table: &LuaTable, name: &'static str, range: Range, - default: f32, -) -> LuaResult +) -> LuaResult> where Range: RangeBounds + std::fmt::Debug, { let value = table.get::(name)?; if value.is_nil() { - Ok(default) + Ok(None) } else if let Some(value) = value .as_number() .or(value.as_integer().map(|i| i as LuaNumber)) { if range.contains(&(value as f32)) { - Ok(value as f32) + Ok(Some(value as f32)) } else { Err(LuaError::RuntimeError(format!( "{} property must be in range [{:?}] but is '{}'", @@ -344,6 +396,18 @@ where } } +fn float_value_from_table( + table: &LuaTable, + name: &'static str, + range: Range, + default: f32, +) -> LuaResult +where + Range: RangeBounds + std::fmt::Debug, +{ + Ok(optional_float_value_from_table(table, name, range)?.unwrap_or(default)) +} + pub(crate) fn instrument_value_from_table(table: &LuaTable) -> LuaResult> { let value = table.get::("instrument")?; if value.is_nil() { @@ -378,6 +442,10 @@ pub(crate) fn delay_value_from_table(table: &LuaTable) -> LuaResult { float_value_from_table(table, "delay", 0.0..1.0, 0.0) } +pub(crate) fn glide_value_from_table(table: &LuaTable) -> LuaResult> { + optional_float_value_from_table(table, "glide", 0.0..) +} + fn float_value_from_string( str: &str, name: &'static str, @@ -445,6 +513,10 @@ pub(crate) fn delay_value_from_string(str: &str) -> LuaResult { float_value_from_string(str, "delay", 0.0..1.0, 0.0) } +pub(crate) fn glide_value_from_string(str: &str) -> LuaResult { + float_value_from_string(str, "glide", 0.0.., 0.0) +} + // ------------------------------------------------------------------------------------------------- pub(crate) fn is_empty_note_string(s: &str) -> bool { @@ -514,6 +586,7 @@ pub(crate) fn note_event_from_string(str: &str) -> LuaResult> let mut volume = 1.0; let mut panning = 0.0; let mut delay = 0.0; + let mut glide = None; for split in white_space_splits { if let Some(instrument_str) = split.strip_prefix('#') { instrument = instrument_value_from_string(instrument_str)?; @@ -523,14 +596,25 @@ pub(crate) fn note_event_from_string(str: &str) -> LuaResult> panning = panning_value_from_string(panning_str)?; } else if let Some(delay_str) = split.strip_prefix('d') { delay = delay_value_from_string(delay_str)?; + } else if let Some(glide_str) = split.strip_prefix('g') { + glide = Some(glide_value_from_string(glide_str)?); } else { return Err(LuaError::RuntimeError( - format!("invalid note string segment: '{}'. ", split) + - "expecting only number values with '#' (instrument),'v' (volume), 'p' (panning) or 'd' (delay) prefixes here."), - ); + format!("invalid note property: '{split}'. ") + + "expecting only number values with " + + "'#' (instrument), 'v' (volume), 'p' (panning), 'd' (delay) or 'g' (glide) " + + "prefixes here.", + )); } } - Ok(new_note((note, instrument, volume, panning, delay))) + Ok(Some(NoteEvent { + note, + instrument, + glide, + volume, + panning, + delay, + })) } } @@ -548,21 +632,30 @@ pub(crate) fn note_event_from_table_map(table: &LuaTable) -> LuaResult LuaResult LuaResult>()) } @@ -933,6 +1032,45 @@ pub(crate) fn gate_from_value( // ------------------------------------------------------------------------------------------------- +fn cycle_to_lua_error(arg: &LuaValue, string: String) -> LuaError { + LuaError::FromLuaConversionError { + from: arg.type_name(), + to: "cycle variable".to_string(), + message: Some(string), + } +} + +fn subcycle_from_value(arg: &LuaValue) -> LuaResult { + let subcycle_result = match arg { + LuaValue::Integer(x) => { + Ok(CycleSubCycle::integer((*x).try_into().map_err(|err| { + cycle_to_lua_error(arg, format!("{err}")) + })?)) + } + LuaValue::Number(x) => Ok(CycleSubCycle::float(*x)), + LuaValue::String(x) => CycleSubCycle::from(&x.to_str()?), + LuaValue::Boolean(x) => Ok(CycleSubCycle::integer(if *x { 1 } else { 0 })), + LuaValue::Nil => Ok(CycleSubCycle::rest()), + // TODO convert from note table presentation to cycle note? + _ => { + return Err(LuaError::FromLuaConversionError { + from: arg.type_name(), + to: "cycle variable".to_string(), + message: Some("type couldn't be converted to a sub cycle".to_string()), + }) + } + }; + + subcycle_result.map_err(|err| cycle_to_lua_error(arg, err)) +} + +pub(crate) fn assign_cycle_vars_from_table(cycle: &mut Cycle, table: LuaTable) -> LuaResult<()> { + for (k, v) in table.pairs::().flatten() { + cycle.set_var(&k.to_string()?, subcycle_from_value(&v)?) + } + Ok(()) +} + pub(crate) fn emitter_from_value( lua: &Lua, timeout_hook: &LuaTimeoutHook, @@ -951,10 +1089,16 @@ pub(crate) fn emitter_from_value( // NB: take instead of cloning: cycle userdata has no other usage than being defined let userdata = userdata.take::()?; let cycle = userdata.cycle; + let variables_callback = if let Some(func) = userdata.variables_function { + Some(LuaCallback::new(lua, func)?) + } else { + None + }; if let Some(mapping_function) = userdata.mapping_function { let mapping_callback = LuaCallback::new(lua, mapping_function)?; let emitter = ScriptedCycleEmitter::with_mapping_callback( cycle, + variables_callback, timeout_hook, mapping_callback, time_base, @@ -962,7 +1106,8 @@ pub(crate) fn emitter_from_value( Ok(Box::new(emitter)) } else { let mappings = userdata.mappings; - let emitter = ScriptedCycleEmitter::with_mappings(cycle, mappings); + let emitter = + ScriptedCycleEmitter::with_mappings(cycle, variables_callback, mappings); Ok(Box::new(emitter)) } } else { diff --git a/src/emitter/cycle.rs b/src/emitter/cycle.rs index 78de28ec..c8a8a431 100644 --- a/src/emitter/cycle.rs +++ b/src/emitter/cycle.rs @@ -1,15 +1,16 @@ -use std::{collections::HashMap, ops::RangeBounds}; +use std::{cell::RefCell, collections::HashMap, ops::RangeBounds, rc::Rc}; type Fraction = num_rational::Rational32; use crate::{ - event::new_note, BeatTimeBase, Chord, Cycle, CycleEvent, CycleTarget, CycleValue, Emitter, - EmitterEvent, Event, InstrumentId, Note, NoteEvent, ParameterSet, RhythmEvent, + event::new_note, BeatTimeBase, Chord, Cycle, CycleEvent, CycleSubCycle, CycleTarget, + CycleValue, Emitter, EmitterEvent, Event, InstrumentId, Note, NoteEvent, Parameter, + ParameterSet, ParameterType, RhythmEvent, }; // ------------------------------------------------------------------------------------------------- -/// Default conversion of a CycleValue into a note stack. +/// Try converting a [`CycleValue`] into a note event stack. /// /// Returns an error when resolving chord modes failed. impl TryFrom<&CycleValue> for Vec> { @@ -17,6 +18,7 @@ impl TryFrom<&CycleValue> for Vec> { fn try_from(value: &CycleValue) -> Result { match value { + CycleValue::Null => Ok(vec![None]), CycleValue::Hold => Ok(vec![None]), CycleValue::Rest => Ok(vec![new_note(Note::OFF)]), CycleValue::Float(_f) => Ok(vec![None]), @@ -44,28 +46,52 @@ impl TryFrom<&CycleValue> for Vec> { // ------------------------------------------------------------------------------------------------- +impl Parameter { + /// Convert a [`Parameter`] value to a [`CycleValue`]. + pub fn into_var(&self, enum_sub_cycles: &[CycleSubCycle]) -> CycleSubCycle { + match self.parameter_type() { + ParameterType::Boolean => CycleSubCycle::integer((self.value() >= 0.5) as i32), + ParameterType::Float => CycleSubCycle::float(self.value()), + ParameterType::Integer => CycleSubCycle::integer(self.value().round() as i32), + ParameterType::Enum => enum_sub_cycles[self.value().round() as usize].clone(), + } + } + + /// Parse enum parameter values into sub-cycle results. + pub fn parse_subcycles(&self) -> Vec> { + match self.parameter_type() { + ParameterType::Enum => self + .value_strings() + .iter() + .map(|string| { + CycleSubCycle::from(string).map_err(|err| { + format!( + "Failed to convert enum parameter value '{string}' to sub-cycle: {err}" + ) + }) + }) + .collect(), + _ => vec![], + } + } +} + +// ------------------------------------------------------------------------------------------------- + // Conversion helpers for cycle targets -fn float_value_in_range( - maybe_float: &Option, - name: &'static str, - range: Range, -) -> Result +fn float_value_in_range(float: &f64, name: &'static str, range: Range) -> Result where Range: RangeBounds + std::fmt::Debug, { - maybe_float - .map(|v| v as f32) - .ok_or_else(|| format!("{} property must be a number value", name)) - .and_then(|v| { - if range.contains(&v) { - Ok(v) - } else { - Err(format!( - "{} property must be in range [{:?}] but is '{}'", - name, range, v - )) - } - }) + let v = *float as f32; + if range.contains(&v) { + Ok(v) + } else { + Err(format!( + "{} property must be in range [{:?}] but is '{}'", + name, range, v + )) + } } fn integer_value_in_range( @@ -99,41 +125,47 @@ pub(crate) fn apply_cycle_note_properties( for target in targets { match target { CycleTarget::Index(index) => { - let index = integer_value_in_range( - *index, - "instrument", - 0.., - )?; + let index = integer_value_in_range(*index, "instrument", 0..)?; let instrument = InstrumentId::from(index as usize); for note_event in note_events.iter_mut().flatten() { note_event.instrument = Some(instrument); } } - CycleTarget::Named(name, value) => { - match name.as_bytes() { - b"v" => { - let volume = float_value_in_range(value, "volume", 0.0..=1.0)?; - for note_event in note_events.iter_mut().flatten() { - note_event.volume = volume; - } + CycleTarget::NamedFloat(name, value) => match name.as_bytes() { + b"v" => { + let volume = float_value_in_range(value, "volume", 0.0..=1.0)?; + for note_event in note_events.iter_mut().flatten() { + note_event.volume = volume; } - b"p" => { - let panning = float_value_in_range(value, "panning", -1.0..=1.0)?; - for note_event in note_events.iter_mut().flatten() { - note_event.panning = panning; - } + } + b"p" => { + let panning = float_value_in_range(value, "panning", -1.0..=1.0)?; + for note_event in note_events.iter_mut().flatten() { + note_event.panning = panning; } - b"d" => { - let delay = float_value_in_range(value, "delay", 0.0..1.0)?; - for note_event in note_events.iter_mut().flatten() { - note_event.delay = delay; - } + } + b"d" => { + let delay = float_value_in_range(value, "delay", 0.0..1.0)?; + for note_event in note_events.iter_mut().flatten() { + note_event.delay = delay; } - _ => { - return Err(format!("invalid note property: '{}'. ", name) + - "expecting number values with '#' (instrument),'v' (volume), 'p' (panning) or 'd' (delay) prefixes here.") + } + b"g" => { + let glide = float_value_in_range(value, "glide", 0.0..)?; + for note_event in note_events.iter_mut().flatten() { + note_event.glide = Some(glide); } } + _ => { + return Err( + format!("invalid note property: '{name}'. ") + + "expecting only number values with " + + "'#' (instrument), 'v' (volume), 'p' (panning), 'd' (delay) or 'g' (glide) " + + "prefixes here."); + } + }, + CycleTarget::Named(name) => { + return Err(format!("{} property must be a number value", name)) } } } @@ -241,14 +273,20 @@ impl CycleNoteEvents { #[derive(Clone, Debug)] pub struct CycleEmitter { cycle: Cycle, + parameters: Vec<(Rc>, Vec)>, mappings: HashMap>>, } impl CycleEmitter { /// Create a new cycle emitter from the given precompiled cycle. pub(crate) fn new(cycle: Cycle) -> Self { + let parameters = vec![]; let mappings = HashMap::new(); - Self { cycle, mappings } + Self { + cycle, + parameters, + mappings, + } } /// Try creating a new cycle emitter from the given mini notation string. @@ -281,7 +319,7 @@ impl CycleEmitter { /// Generate a note event from a single cycle event, applying mappings if necessary fn map_note_event(&mut self, event: CycleEvent) -> Result>, String> { let mut note_events = { - if let Some(note_events) = self.mappings.get(event.string()) { + if let Some(note_events) = self.mappings.get(event.as_str().as_ref()) { // apply custom note mappings note_events.clone() } else { @@ -297,6 +335,12 @@ impl CycleEmitter { /// Generate next batch of events from the next cycle run. /// Converts cycle events to note events and flattens channels into note columns. fn generate(&mut self) -> Vec { + // inject parameter values into the cycle as variables + for (parameter_ref, enum_values) in &self.parameters { + let parameter = parameter_ref.borrow(); + self.cycle + .set_var(parameter.id(), parameter.into_var(enum_values)); + } // run the cycle event generator let events = { match self.cycle.generate() { @@ -340,8 +384,25 @@ impl Emitter for CycleEmitter { // nothing to do } - fn set_parameters(&mut self, _parameters: ParameterSet) { - // nothing to do + fn set_parameters(&mut self, parameters: ParameterSet) { + // parse and unwrap cycle values + self.parameters = parameters + .iter() + .map(|parameter| { + ( + Rc::clone(parameter), + parameter + .borrow() + .parse_subcycles() + .into_iter() + .map(|result| { + // we got no way indicate runtime errors here, so just panic + result.unwrap_or_else(|err| panic!("{err}")) + }) + .collect(), + ) + }) + .collect(); } fn run(&mut self, _pulse: RhythmEvent, emit_event: bool) -> Option> { @@ -376,3 +437,233 @@ pub fn new_cycle_emitter(input: &str) -> Result { pub fn new_cycle_emitter_with_seed(input: &str, seed: u64) -> Result { CycleEmitter::from_mini_with_seed(input, seed) } + +// ------------------------------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::Parameter; + use std::{cell::RefCell, rc::Rc}; + + use pretty_assertions::assert_eq; + + fn run_emitter( + emitter: &mut CycleEmitter, + ) -> Result, Box> { + emitter + .run( + RhythmEvent { + value: 1.0, + step_time: 1.0, + }, + true, + ) + .ok_or("No events emitted".into()) + } + + #[test] + fn parameter_conversion() -> Result<(), Box> { + // floats + let param = Rc::new(RefCell::new(Parameter::with_float( + "velocity", + "", + "", + 0.0..=1.0, + 0.8, + ))); + let mut variable = new_cycle_emitter("a:v$velocity")?; + variable.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = new_cycle_emitter("a:v0.8")?; + assert_eq!(run_emitter(&mut variable)?, run_emitter(&mut expected)?); + + // integers + let param = Rc::new(RefCell::new(Parameter::with_integer( + "steps", + "", + "", + 1..=8, + 3, + ))); + let mut variable = new_cycle_emitter("a*$steps")?; + variable.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = new_cycle_emitter("a*3")?; + assert_eq!(run_emitter(&mut variable)?, run_emitter(&mut expected)?); + + // bool + let param = Rc::new(RefCell::new(Parameter::with_boolean( + "enabled", "", "", true, + ))); + let mut variable = new_cycle_emitter("a*$enabled")?; + variable.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = new_cycle_emitter("a*1")?; + assert_eq!(run_emitter(&mut variable)?, run_emitter(&mut expected)?); + + // enum + let param = Rc::new(RefCell::new(Parameter::with_enum( + "sample", + "", + "", + vec!["kick".to_string(), "snare".to_string()], + "snare".to_string(), + ))); + let mut variable = new_cycle_emitter("$sample")?; + variable.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = new_cycle_emitter("snare")?; + // NB: both will be `None` here as there are no mappings... + assert_eq!(run_emitter(&mut variable)?, run_emitter(&mut expected)?); + + Ok(()) + } + + #[test] + fn parameter_enum_values() -> Result<(), Box> { + let param = Rc::new(RefCell::new(Parameter::with_enum( + "enum", + "", + "", + vec!["c4".to_string(), "[c4 c5]*4".to_string()], + "[c4 c5]*4".to_string(), + ))); + let mut emitter = new_cycle_emitter("$enum")?; + emitter.set_parameters(vec![Rc::clone(¶m)]); + + let mut expected = new_cycle_emitter("[c4 c5]*4")?; + assert_eq!(run_emitter(&mut emitter)?, run_emitter(&mut expected)?); + + Ok(()) + } + + #[test] + #[should_panic] + fn parameter_enum_values_error() { + let param = Rc::new(RefCell::new(Parameter::with_enum( + "enum", + "", + "", + vec!["c4".to_string(), "broken]".to_string()], + "c4".to_string(), + ))); + let mut variable = new_cycle_emitter("$enum").unwrap(); + variable.set_parameters(vec![Rc::clone(¶m)]); // this should throw + } + + #[test] + fn parameter_value_changes() -> Result<(), Box> { + let param = Rc::new(RefCell::new(Parameter::with_float( + "velocity", + "", + "", + 0.0..=1.0, + 0.8, + ))); + let mut emitter = new_cycle_emitter("a:v$velocity")?; + emitter.set_parameters(vec![Rc::clone(¶m)]); + + // initial value + let mut expected = new_cycle_emitter("a:v0.8")?; + assert_eq!(run_emitter(&mut emitter)?, run_emitter(&mut expected)?); + + // changed parameter value + param.borrow_mut().set_value(0.5); + let mut expected = new_cycle_emitter("a:v0.5")?; + assert_eq!(run_emitter(&mut emitter)?, run_emitter(&mut expected)?); + + Ok(()) + } + + #[test] + fn note_mappings() -> Result<(), Box> { + // single note mapping + let mut emitter = new_cycle_emitter("bd sd")?.with_mappings(&[ + ("bd", vec![new_note(Note::C4)]), + ("sd", vec![new_note(Note::D4)]), + ]); + let events = run_emitter(&mut emitter)?; + assert_eq!(events.len(), 2); + assert_eq!(events[0].event, Event::NoteEvents(vec![new_note(Note::C4)])); + assert_eq!(events[1].event, Event::NoteEvents(vec![new_note(Note::D4)])); + + // multi note mapping + let mut emitter = new_cycle_emitter("x")? + .with_mappings(&[("x", vec![new_note(Note::C4), new_note(Note::E4)])]); + let events = run_emitter(&mut emitter)?; + assert_eq!(events.len(), 1); + assert_eq!( + events[0].event, + Event::NoteEvents(vec![new_note(Note::C4), new_note(Note::E4)]) + ); + + // mapping with rest (None = empty note) + let mut emitter = new_cycle_emitter("a b")? + .with_mappings(&[("a", vec![new_note(Note::C4)]), ("b", vec![None])]); + let events = run_emitter(&mut emitter)?; + assert_eq!(events.len(), 2); + assert_eq!(events[0].event, Event::NoteEvents(vec![new_note(Note::C4)])); + assert_eq!(events[1].event, Event::NoteEvents(vec![None])); + + // mapping combined with targets + let mut emitter = + new_cycle_emitter("bd:v0.5")?.with_mappings(&[("bd", vec![new_note(Note::C4)])]); + let events = run_emitter(&mut emitter)?; + assert_eq!(events.len(), 1); + if let Event::NoteEvents(notes) = &events[0].event { + let event = notes[0].clone(); + assert_eq!(event, new_note((Note::C4, None, None, 0.5))); + } else { + panic!("expected NoteEvents"); + } + + Ok(()) + } + + #[test] + fn note_mappings_with_parameters() -> Result<(), Box> { + let param = Rc::new(RefCell::new(Parameter::with_float( + "vel", + "", + "", + 0.0..=1.0, + 0.7, + ))); + let mut emitter = + new_cycle_emitter("bd:v$vel")?.with_mappings(&[("bd", vec![new_note(Note::C4)])]); + emitter.set_parameters(vec![Rc::clone(¶m)]); + let events = run_emitter(&mut emitter)?; + if let Event::NoteEvents(notes) = &events[0].event { + let event = notes[0].clone(); + assert_eq!(event, new_note((Note::C4, None, None, 0.7))); + } else { + panic!("expected NoteEvents"); + } + + // change parameter value + param.borrow_mut().set_value(0.3); + let events = run_emitter(&mut emitter)?; + if let Event::NoteEvents(notes) = &events[0].event { + let event = notes[0].clone(); + assert_eq!(event, new_note((Note::C4, None, None, 0.3))); + } else { + panic!("expected NoteEvents"); + } + + // mapped enum strings + let param = Rc::new(RefCell::new(Parameter::with_enum( + "sample", + "", + "", + vec!["kick".to_string(), "snare".to_string()], + "snare".to_string(), + ))); + let mut emitter = + new_cycle_emitter("$sample")?.with_mappings(&[("snare", vec![new_note(Note::C4)])]); + emitter.set_parameters(vec![Rc::clone(¶m)]); + let mut expected = + new_cycle_emitter("snare")?.with_mappings(&[("snare", vec![new_note(Note::C4)])]); + let emitter_result = run_emitter(&mut emitter)?; + let expected_result = run_emitter(&mut expected)?; + assert_eq!(emitter_result, expected_result); + + Ok(()) + } +} diff --git a/src/emitter/scripted_cycle.rs b/src/emitter/scripted_cycle.rs index 346addcb..ba06c525 100644 --- a/src/emitter/scripted_cycle.rs +++ b/src/emitter/scripted_cycle.rs @@ -1,17 +1,20 @@ -use std::collections::HashMap; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; use num_traits::ToPrimitive; -use mlua::prelude::{LuaError, LuaResult}; +use mlua::{ + prelude::{LuaError, LuaResult}, + Value, +}; use crate::{ bindings::{ - add_lua_callback_error, note_events_from_value, ContextPlaybackState, LuaCallback, - LuaTimeoutHook, + add_lua_callback_error, assign_cycle_vars_from_table, note_events_from_value, + ContextPlaybackState, LuaCallback, LuaTimeoutHook, }, emitter::cycle::{apply_cycle_note_properties, CycleNoteEvents}, - BeatTimeBase, Cycle, CycleEvent, CycleValue, Emitter, EmitterEvent, Event, NoteEvent, - ParameterSet, RhythmEvent, + BeatTimeBase, Cycle, CycleEvent, CycleSubCycle, CycleValue, Emitter, EmitterEvent, Event, + NoteEvent, Parameter, ParameterSet, RhythmEvent, }; // ------------------------------------------------------------------------------------------------- @@ -20,29 +23,38 @@ use crate::{ /// /// Channels from cycle are merged down into note events on different voices. /// Values in cycles can be mapped to notes with an optional mapping table or -/// callbacks from from scripts. +/// callbacks from scripts. /// /// See also [`CycleEmitter`](`super::cycle::CycleEmitter`) #[derive(Clone, Debug)] pub struct ScriptedCycleEmitter { cycle: Cycle, + parameters: Vec<(Rc>, Vec)>, mappings: HashMap>>, mapping_callback: Option, + variables_callback: Option, timeout_hook: Option, channel_steps: Vec, } impl ScriptedCycleEmitter { /// Return a new cycle with the given value mappings applied. - pub fn with_mappings(cycle: Cycle, mappings: Vec<(String, Vec>)>) -> Self { + pub(crate) fn with_mappings( + cycle: Cycle, + variables_callback: Option, + mappings: Vec<(String, Vec>)>, + ) -> Self { + let parameters = vec![]; let mappings = mappings.into_iter().collect(); let mapping_callback = None; let timeout_hook = None; let channel_steps = vec![]; Self { cycle, + parameters, mappings, mapping_callback, + variables_callback, timeout_hook, channel_steps, } @@ -51,6 +63,7 @@ impl ScriptedCycleEmitter { /// Return a new cycle with the given mapping callback applied. pub(crate) fn with_mapping_callback( cycle: Cycle, + variables_callback: Option, timeout_hook: &LuaTimeoutHook, mapping_callback: LuaCallback, time_base: &BeatTimeBase, @@ -58,6 +71,7 @@ impl ScriptedCycleEmitter { // create a new timeout_hook instance and reset it before calling the function let mut timeout_hook = timeout_hook.clone(); timeout_hook.reset(); + let parameters = vec![]; let mappings = HashMap::new(); // initialize emitter context for the function let mut mapping_callback = mapping_callback; @@ -75,8 +89,10 @@ impl ScriptedCycleEmitter { let channel_steps = vec![]; Ok(Self { cycle, + parameters, mappings, mapping_callback: Some(mapping_callback), + variables_callback, timeout_hook: Some(timeout_hook), channel_steps, }) @@ -99,9 +115,9 @@ impl ScriptedCycleEmitter { step_length, )?; // call mapping function - let result = mapping_callback.call_with_arg(event.string())?; + let result = mapping_callback.call_with_arg(event.as_str().as_ref())?; note_events_from_value(&result, None)? - } else if let Some(note_events) = self.mappings.get(event.string()) { + } else if let Some(note_events) = self.mappings.get(event.as_str().as_ref()) { // apply custom note mapping note_events.clone() } else { @@ -116,7 +132,7 @@ impl ScriptedCycleEmitter { { return Err(LuaError::runtime(format!( "invalid/unknown identifier in cycle: '{}'. please check for typos or add a custom mapping for it.", - event.string() + event.as_str() ))); } // apply note properties from targets @@ -129,12 +145,61 @@ impl ScriptedCycleEmitter { /// Generate next batch of events from the next cycle run. /// Converts cycle events to note events and flattens channels into note columns. fn generate(&mut self) -> Vec { + // inject parameter values into cycle as variables + for (parameter_ref, enum_values) in &self.parameters { + let parameter = parameter_ref.borrow(); + self.cycle + .set_var(parameter.id(), parameter.into_var(enum_values)); + } + + if let Some(variables_callback) = &mut self.variables_callback { + let playback_state = ContextPlaybackState::Running; + if let Err(err) = variables_callback.set_context_playback_state(playback_state) { + variables_callback.handle_error(&err); + } + if let Err(err) = variables_callback + .set_context_parameters(self.parameters.iter().map(|(p, _)| Rc::clone(p)).collect()) + { + variables_callback.handle_error(&err); + } + + match variables_callback.call() { + Err(err) => { + variables_callback.handle_error(&err); + } + Ok(value) => match value { + Value::Table(table) => { + if let Err(err) = assign_cycle_vars_from_table(&mut self.cycle, table) { + add_lua_callback_error(None, None, "vars".to_string(), err); + } + } + _ => { + add_lua_callback_error( + None, + None, + "vars".to_string(), + LuaError::RuntimeError( + "vars should return a table of variables".to_string(), + ), + ); + } + }, + } + } + // run the cycle event generator let events = { match self.cycle.generate() { Ok(events) => events, Err(err) => { - add_lua_callback_error("cycle", &LuaError::RuntimeError(err)); + let source = self.cycle.source().clone(); + let source_line = None; + add_lua_callback_error( + source, + source_line, + "generate".to_string(), + LuaError::RuntimeError(err), + ); // skip processing events return vec![]; } @@ -170,7 +235,9 @@ impl ScriptedCycleEmitter { if let Some(callback) = &self.mapping_callback { callback.handle_error(&err) } else { - add_lua_callback_error("map", &err) + let source = self.cycle.source().clone(); + let source_line = None; + add_lua_callback_error(source, source_line, "map".to_string(), err); } } Ok(note_events) => { @@ -194,7 +261,7 @@ impl ScriptedCycleEmitter { match self.cycle.generate() { Ok(events) => events, Err(err) => { - add_lua_callback_error("cycle", &LuaError::RuntimeError(err)); + mapping_callback.handle_error(&LuaError::RuntimeError(err)); return; } } @@ -225,12 +292,12 @@ impl ScriptedCycleEmitter { channel_step, step_length, ) { - add_lua_callback_error("cycle", &err); + mapping_callback.handle_error(&err); return; } // call mapping function - if let Err(err) = mapping_callback.call_with_arg(event.string()) { - add_lua_callback_error("cycle", &err); + if let Err(err) = mapping_callback.call_with_arg(event.as_str().as_ref()) { + mapping_callback.handle_error(&err); return; } } @@ -276,6 +343,36 @@ impl Emitter for ScriptedCycleEmitter { } fn set_parameters(&mut self, parameters: ParameterSet) { + // parse and unwrap cycle subcycle values from enum parameters + let unwrap_sub_cycle_result = |sub_cycle: Result| -> CycleSubCycle { + sub_cycle.unwrap_or_else(|err| { + // forwarding parse error as runtime errors + add_lua_callback_error( + None, + None, + "cycle".to_string(), + LuaError::RuntimeError(err), + ); + // return rest value to prevent further runtime errors, which would mask the original error + CycleSubCycle::rest() + }) + }; + self.parameters = parameters + .iter() + .map(|parameter| { + ( + Rc::clone(parameter), + parameter + .borrow() + .parse_subcycles() + .into_iter() + .map(unwrap_sub_cycle_result) + .collect(), + ) + }) + .collect(); + + // pass parameters to the mapping callback context if let Some(timeout_hook) = &mut self.timeout_hook { timeout_hook.reset(); } diff --git a/src/event.rs b/src/event.rs index fddfcd18..080f61e9 100644 --- a/src/event.rs +++ b/src/event.rs @@ -43,22 +43,28 @@ pub struct ParameterId(usize); #[derive(Clone, PartialEq, Debug)] pub struct NoteEvent { pub note: Note, - pub instrument: Option, - pub volume: f32, // [0 - INF] - pub panning: f32, // [-1 - 1] - pub delay: f32, // [0 - 1] + pub instrument: Option, // [0 - INF] or None + pub glide: Option, // [0 - INF] or None + pub volume: f32, // [0 - 1] + pub panning: f32, // [-1 - 1] + pub delay: f32, // [0 - 1] } impl NoteEvent { pub fn to_string(&self, show_instruments: bool) -> String { if show_instruments { format!( - "{} {} {:.2} {:.2} {:.2}", + "{} #{} g{} v{:.2} p{:.2} d{:.2}", self.note, if let Some(instrument) = self.instrument { - format!("#{:02}", instrument) + format!("{:02}", instrument) } else { - "NA".to_string() + "-".to_string() + }, + if let Some(glide) = self.glide { + format!("{:.2}", glide) + } else { + "-".to_string() }, self.volume, self.panning, @@ -66,8 +72,16 @@ impl NoteEvent { ) } else { format!( - "{} {:.2} {:.2} {:.2}", - self.note, self.volume, self.panning, self.delay + "{} g{} v{:.2} p{:.2} d{:.2}", + self.note, + if let Some(glide) = self.glide { + format!("{:.2}", glide) + } else { + "-".to_string() + }, + self.volume, + self.panning, + self.delay ) } } @@ -83,6 +97,7 @@ where Self { note, instrument: None, + glide: None, volume: 1.0, panning: 0.0, delay: 0.0, @@ -101,6 +116,27 @@ where Self { note, instrument, + glide: None, + volume: 1.0, + panning: 0.0, + delay: 0.0, + } + } +} + +impl, I: Into>> From<(N, I, Option)> for NoteEvent +where + >::Error: std::fmt::Debug, +{ + // Initialize from a (Instrument, Note, Glide) tuple + fn from((note, instrument, glide): (N, I, Option)) -> Self { + let note = note.try_into().expect("Failed to convert note"); + let instrument = instrument.into(); + let glide = glide.map(|g| g.max(0.0)); + Self { + note, + instrument, + glide, volume: 1.0, panning: 0.0, delay: 0.0, @@ -108,18 +144,20 @@ where } } -impl, I: Into>> From<(N, I, f32)> for NoteEvent +impl, I: Into>> From<(N, I, Option, f32)> for NoteEvent where >::Error: std::fmt::Debug, { - // Initialize from a (Instrument, Note, Volume) tuple - fn from((note, instrument, volume): (N, I, f32)) -> Self { + // Initialize from a (Instrument, Note, Glide, Volume) tuple + fn from((note, instrument, glide, volume): (N, I, Option, f32)) -> Self { let note = note.try_into().expect("Failed to convert note"); let instrument = instrument.into(); + let glide = glide.map(|g| g.max(0.0)); let volume = volume.clamp(0.0, 1.0); Self { note, instrument, + glide, volume, panning: 0.0, delay: 0.0, @@ -127,19 +165,22 @@ where } } -impl, I: Into>> From<(N, I, f32, f32)> for NoteEvent +impl, I: Into>> From<(N, I, Option, f32, f32)> + for NoteEvent where >::Error: std::fmt::Debug, { - // Initialize from a (Instrument, Note, Volume, Panning) tuple - fn from((note, instrument, volume, panning): (N, I, f32, f32)) -> Self { + // Initialize from a (Instrument, Note, Glide, Volume, Panning) tuple + fn from((note, instrument, glide, volume, panning): (N, I, Option, f32, f32)) -> Self { let note = note.try_into().expect("Failed to convert note"); let instrument = instrument.into(); + let glide = glide.map(|g| g.max(0.0)); let volume = volume.clamp(0.0, 1.0); let panning = panning.clamp(-1.0, 1.0); Self { note, instrument, + glide, volume, panning, delay: 0.0, @@ -147,20 +188,25 @@ where } } -impl, I: Into>> From<(N, I, f32, f32, f32)> for NoteEvent +impl, I: Into>> From<(N, I, Option, f32, f32, f32)> + for NoteEvent where >::Error: std::fmt::Debug, { - // Initialize from a (Instrument, Note, Volume, Panning, Delay) tuple - fn from((note, instrument, volume, panning, delay): (N, I, f32, f32, f32)) -> Self { + // Initialize from a (Instrument, Note, Glide, Volume, Panning, Delay) tuple + fn from( + (note, instrument, glide, volume, panning, delay): (N, I, Option, f32, f32, f32), + ) -> Self { let note = note.try_into().expect("Failed to convert note"); let instrument = instrument.into(); + let glide = glide.map(|g| g.max(0.0)); let volume = volume.clamp(0.0, 1.0); let panning = panning.clamp(-1.0, 1.0); let delay = delay.clamp(0.0, 1.0); Self { note, instrument, + glide, volume, panning, delay, diff --git a/src/lib.rs b/src/lib.rs index 37d60270..9bae68c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,8 @@ pub use crate::{ rhythm::{Rhythm, RhythmEvent}, sequence::Sequence, tidal::{ - Cycle, Event as CycleEvent, Span as CycleSpan, Target as CycleTarget, Value as CycleValue, + Constant as CycleValue, Cycle, Event as CycleEvent, Span as CycleSpan, + SubCycle as CycleSubCycle, Target as CycleTarget, }, time::{ BeatTimeBase, BeatTimeStep, ExactSampleTime, SampleTime, SampleTimeBase, SampleTimeDisplay, @@ -67,6 +68,11 @@ pub use crate::{ }, }; +/// Default [`Pattern`] impls. +pub mod patterns { + pub use super::pattern::{beat_time::BeatTimePattern, second_time::SecondTimePattern}; +} + /// Default [`Rhythm`] impls. pub mod rhythms { pub use super::rhythm::{empty::EmptyRhythm, fixed::FixedRhythm}; diff --git a/src/note.rs b/src/note.rs index a74276f1..9de99b61 100644 --- a/src/note.rs +++ b/src/note.rs @@ -375,7 +375,7 @@ impl Display for Note { ]; match self { Self::EMPTY => write!(f, "---"), - Self::OFF => write!(f, "off"), + Self::OFF => write!(f, "OFF"), _ => { let octave = (*self as u8 / 12) as i32; let note = (*self as u8 % 12) as usize; @@ -411,7 +411,7 @@ mod test { assert_eq!(Note::Cs0.to_string(), "C#0"); assert_eq!(Note::G9.to_string(), "G9"); assert_eq!(Note::Fs10.to_string(), "F#10"); - assert_eq!(Note::OFF.to_string(), "off"); + assert_eq!(Note::OFF.to_string(), "OFF"); } #[test] diff --git a/src/phrase.rs b/src/phrase.rs index 87ca4a3c..6dc1f9a8 100644 --- a/src/phrase.rs +++ b/src/phrase.rs @@ -446,13 +446,13 @@ mod test { .every_nth_eighth(1.0) .with_rhythm([1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1].to_rhythm()) .emit(new_note_sequence_emitter(vec![ - new_note((bass_notes[0], None, 0.5)), - new_note((bass_notes[2], None, 0.5)), - new_note((bass_notes[3], None, 0.5)), - new_note((bass_notes[0], None, 0.5)), - new_note((bass_notes[2], None, 0.5)), - new_note((bass_notes[3], None, 0.5)), - new_note((bass_notes[6].transposed(-12), None, 0.5)), + new_note((bass_notes[0], None, None, 0.5)), + new_note((bass_notes[2], None, None, 0.5)), + new_note((bass_notes[3], None, None, 0.5)), + new_note((bass_notes[0], None, None, 0.5)), + new_note((bass_notes[2], None, None, 0.5)), + new_note((bass_notes[3], None, None, 0.5)), + new_note((bass_notes[6].transposed(-12), None, None, 0.5)), ])); let synth_pattern = @@ -460,24 +460,24 @@ mod test { .every_nth_bar(4.0) .emit(new_polyphonic_note_sequence_emitter(vec![ vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("G 4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("G 4", None, None, 0.3)), ], vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("F 4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("F 4", None, None, 0.3)), ], vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("G 4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("G 4", None, None, 0.3)), ], vec![ - new_note(("C 4", None, 0.3)), - new_note(("D#4", None, 0.3)), - new_note(("A#4", None, 0.3)), + new_note(("C 4", None, None, 0.3)), + new_note(("D#4", None, None, 0.3)), + new_note(("A#4", None, None, 0.3)), ], ])); @@ -485,9 +485,9 @@ mod test { beat_time .every_nth_seconds(8.0) .emit(new_polyphonic_note_sequence_emitter(vec![ - vec![new_note(("C 4", None, 0.2)), None, None], - vec![None, new_note(("C 4", None, 0.2)), None], - vec![None, None, new_note(("F 4", None, 0.2))], + vec![new_note(("C 4", None, None, 0.2)), None, None], + vec![None, new_note(("C 4", None, None, 0.2)), None], + vec![None, None, new_note(("F 4", None, None, 0.2))], ])); let tone_pattern = beat_time diff --git a/src/player.rs b/src/player.rs index c4d6805d..6cf675fa 100644 --- a/src/player.rs +++ b/src/player.rs @@ -6,6 +6,7 @@ use std::{ path::Path, sync::{ atomic::{AtomicUsize, Ordering}, + mpsc::SyncSender, Arc, }, time::Duration, @@ -13,8 +14,6 @@ use std::{ use dashmap::DashMap; -use crossbeam_channel::Sender; - use phonic::{ sources::PreloadedFileSource, utils::speed_from_note, DefaultOutputDevice, Error, FilePlaybackOptions, PlaybackId, PlaybackStatusContext, PlaybackStatusEvent, @@ -23,7 +22,7 @@ use phonic::{ use crate::{ time::{SampleTimeBase, SampleTimeDisplay}, - BeatTimeBase, Event, ExactSampleTime, InstrumentId, Note, PatternEvent, PatternSlot, + BeatTimeBase, Event, ExactSampleTime, InstrumentId, Note, NoteEvent, PatternEvent, PatternSlot, SampleTime, Sequence, }; @@ -204,13 +203,13 @@ impl SamplePlaybackContext { pub struct SamplePlayer { inner: PhonicPlayer, sample_pool: Arc, - playing_notes: Vec>, + playing_notes: Vec>, new_note_action: NewNoteAction, sample_root_note: Note, - playback_preload_time: Duration, - playback_pos_emit_rate: Duration, show_events: bool, - playback_sample_time: SampleTime, + playback_pos_emit_rate: Duration, + playback_preload_time: Duration, + playback_start_sample_time: SampleTime, emitted_sample_time: SampleTime, } @@ -223,31 +222,35 @@ impl SamplePlayer { /// /// # Errors /// returns an error if the player could not be created. - pub fn new>>>( + pub fn new>>>( sample_pool: Arc, playback_status_sender: S, ) -> Result> { // create player let audio_output = DefaultOutputDevice::open()?; let inner = PhonicPlayer::new(audio_output, playback_status_sender); + let playing_notes = Vec::new(); + let new_note_action = NewNoteAction::default(); let sample_root_note = Note::C5; - let playback_preload = Duration::from_millis(Self::DEFAULT_PLAYBACK_PRELOAD_MS); - let playback_pos_emit_rate = Duration::from_secs(1); let show_events = false; - let playback_sample_time = inner.output_sample_frame_position(); + + let playback_pos_emit_rate = Duration::from_secs(1); + let playback_preload_time = Duration::from_millis(Self::DEFAULT_PLAYBACK_PRELOAD_MS); + let playback_start_sample_time = inner.output_sample_frame_position(); let emitted_sample_time = 0; + Ok(Self { inner, sample_pool, playing_notes, new_note_action, sample_root_note, - playback_preload_time: playback_preload, - playback_pos_emit_rate, show_events, - playback_sample_time, + playback_preload_time, + playback_pos_emit_rate, + playback_start_sample_time, emitted_sample_time, }) } @@ -326,9 +329,9 @@ impl SamplePlayer { /// Stop all currently playing sources in the given pattern slot index. pub fn stop_sources_in_pattern_slot(&mut self, pattern_index: usize) { - for (playback_id, _) in self.playing_notes[pattern_index].values() { + for playing_note in self.playing_notes[pattern_index].values() { // ignore result: source maybe already is stopped - let _ = self.inner.stop_source(*playback_id, None); + let _ = self.inner.stop_source(playing_note.playback_id, None); } self.playing_notes[pattern_index].clear(); } @@ -352,24 +355,24 @@ impl SamplePlayer { } /// Run the given sequence until it stops or the passed stop condition function returns true. - pub fn run_until bool>( + pub fn run_until bool>( &mut self, previous_sequence: Option<&mut Sequence>, sequence: &mut Sequence, time_base: &dyn SampleTimeBase, reset_playback_pos: bool, - stop_fn: StopFn, + mut stop_fn: StopFn, ) { - // reset time counters when starting the first time or when explicitly requested, else continue - // playing from our previous time to avoid interrupting playback streams if reset_playback_pos || self.emitted_sample_time == 0 { + // reset time counters and the sequence when starting the first time or when requested self.reset_playback_position(sequence); log::debug!(target: "Player", "Resetting playback pos"); } else { + // else continue playing from our previous time, to avoid interrupting playback self.prepare_run_until_time( previous_sequence, sequence, - self.playback_sample_time, + self.playback_start_sample_time, self.emitted_sample_time, ); log::debug!(target: "Player", @@ -377,32 +380,33 @@ impl SamplePlayer { time_base.samples_to_seconds(self.emitted_sample_time) ); } + while !stop_fn() { - let playback_preload_secs = self.playback_preload_time.as_secs_f64(); - // calculate emitted and playback time differences - let seconds_emitted = time_base.samples_to_seconds(self.emitted_sample_time); - let seconds_played = time_base.samples_to_seconds( - self.inner.output_sample_frame_position() - self.playback_sample_time, - ); - let seconds_to_emit = seconds_played - seconds_emitted + playback_preload_secs * 2.0; // run sequence ahead of player by the self.playback_preload time - if seconds_to_emit >= playback_preload_secs || self.emitted_sample_time == 0 { - let samples_to_emit = time_base.seconds_to_samples(seconds_to_emit); + let samples_to_emit = self.calculate_samples_to_emit( + time_base, + self.playback_start_sample_time, + self.emitted_sample_time, + ); + let playback_preload = + time_base.seconds_to_samples(self.playback_preload_time.as_secs_f64()); + if samples_to_emit >= playback_preload || self.emitted_sample_time == 0 { + // consume events in the preload range self.run_until_time( sequence, - self.playback_sample_time, + self.playback_start_sample_time, self.emitted_sample_time + samples_to_emit, ); self.emitted_sample_time += samples_to_emit; } else { - // wait until next events are due, but check stop_fn at least every... - const MAX_SLEEP_TIME: f64 = 0.1; - let time_until_next_emit_batch = (playback_preload_secs - seconds_to_emit).max(0.0); + // wait until next events are due and call stop_fn + let max_sleep_time = self.playback_preload_time.as_secs_f64() / 4.0; + let seconds_until_next_emit_batch = + time_base.samples_to_seconds(playback_preload.saturating_sub(samples_to_emit)); let mut time_slept = 0.0; - while time_slept < time_until_next_emit_batch && !stop_fn() { - let sleep_amount = time_until_next_emit_batch.min(MAX_SLEEP_TIME); - std::thread::sleep(std::time::Duration::from_secs_f64(sleep_amount)); - // log::debug!(target: "Player", "Slept {} seconds", sleep_amount); + while time_slept < seconds_until_next_emit_batch && !stop_fn() { + let sleep_amount = seconds_until_next_emit_batch.min(max_sleep_time); + std::thread::sleep(Duration::from_secs_f64(sleep_amount)); time_slept += sleep_amount; } } @@ -428,41 +432,51 @@ impl SamplePlayer { if self.playing_notes.iter().any(|notes| !notes.is_empty()) { // Process note stop events from the previous sequence let stop_time = if let Some(previous_sequence) = previous_sequence { - // Get maximum step length in samples of all currently playing back patterns + // Get maximum pattern step length in samples of all currently playing back patterns + let pattern_slots = { + match previous_sequence.current_phrase() { + Some(phrase) => phrase.pattern_slots(), + None => &[], + } + }; let mut max_step_length: ExactSampleTime = 0.0; - for pattern_slot in previous_sequence.current_phrase().pattern_slots() { + for pattern_slot in pattern_slots { if let PatternSlot::Pattern(pattern) = pattern_slot { - max_step_length = max_step_length.max(pattern.borrow().step_length()); + let pattern = pattern.borrow(); + // We can't assume that every step produces a note-on, so run entire patterns + // or at least 4 steps with dynamic pattern generators. + max_step_length = max_step_length + .max(pattern.step_length() * pattern.step_count().max(4) as f64); } } // Run sequence and handle note-offs only to stop playing notes - let note_stope_lookup_time = + let note_stop_lookup_time = time_offset + time + max_step_length.ceil() as SampleTime; previous_sequence.consume_events_until_time( - note_stope_lookup_time, + note_stop_lookup_time, &mut |pattern_index, pattern_event| { - if !self.playing_notes[pattern_index].is_empty() { - self.handle_pattern_event_note_offs( - time_offset, - pattern_index, - pattern_event, - ); - } + self.handle_pattern_event_note_offs( + time_offset, + pattern_index, + pattern_event, + ); }, ); // stop remaining notes at the lookup time range's end - note_stope_lookup_time + note_stop_lookup_time } else { // stop remaining notes at the time the new sequence starts time_offset + time }; - // stop remaining playing notes at the time we're applying the new sequence + // stop remaining playing notes at the lookup time or time we're applying the new sequence for playing_notes in &mut self.playing_notes { - for (playback_id, _) in playing_notes.values() { - // ignore result: source maybe already is stopped - let _ = self.inner.stop_source(*playback_id, stop_time); + for playing_note in playing_notes.values_mut() { + if playing_note.stop_time.is_none() { + // ignore stop result: source maybe already is stopped + let _ = self.inner.stop_source(playing_note.playback_id, stop_time); + playing_note.stop_time = Some(stop_time); + } } - playing_notes.clear(); } } // update playing notes state to fit the new sequence @@ -472,6 +486,27 @@ impl SamplePlayer { sequence.advance_until_time(time); } + /// Calculate how many samples need to be emitted based on current playback position + /// and playback preload time. + pub fn calculate_samples_to_emit( + &self, + time_base: &dyn SampleTimeBase, + playback_start_sample_time: SampleTime, + emitted_sample_time: SampleTime, + ) -> SampleTime { + // current, relative playback time + let playback_head = self + .inner + .output_sample_frame_position() + .saturating_sub(playback_start_sample_time); + // current, relative emit time + let emitter_head = playback_head as i64 - emitted_sample_time as i64; + // apply 2 * preload: first half as buffer, second as fill area + let preload_samples = + time_base.seconds_to_samples(self.playback_preload_time.as_secs_f64()); + (emitter_head + 2 * preload_samples as i64).max(0) as SampleTime + } + /// Manually seek the given sequence to the given time offset and actual position. pub fn advance_until_time(&mut self, sequence: &mut Sequence, time: SampleTime) { self.stop_all_sources(); @@ -499,8 +534,7 @@ impl SamplePlayer { pattern_index: usize, pattern_event: PatternEvent, ) { - let playing_notes_in_pattern = &mut self.playing_notes[pattern_index]; - if let Some(Event::NoteEvents(notes)) = pattern_event.event { + if let Some(Event::NoteEvents(notes)) = &pattern_event.event { for (voice_index, note_event) in notes.iter().enumerate() { let note_event = match note_event { None => continue, @@ -509,14 +543,19 @@ impl SamplePlayer { // Handle note off or stop action only if note_event.note.is_note_off() || (note_event.note.is_note_on() + && note_event.glide.is_none() && self.new_note_action != NewNoteAction::Continue) { - if let Some((playback_id, _)) = playing_notes_in_pattern.get(&voice_index) { - // ignore result: source maybe already is stopped - let _ = self - .inner - .stop_source(*playback_id, time_offset + pattern_event.time); - playing_notes_in_pattern.remove(&voice_index); + let stop_time = self.note_event_time(&pattern_event, note_event, time_offset); + + if let Some(playing_note) = + self.playing_notes[pattern_index].get_mut(&voice_index) + { + if playing_note.stop_time.is_none_or(|time| time > stop_time) { + // ignore stop result: source maybe already is stopped + let _ = self.inner.stop_source(playing_note.playback_id, stop_time); + playing_note.stop_time = Some(stop_time) + } } } } @@ -544,79 +583,194 @@ impl SamplePlayer { ); } + // Remove expired pending note stops + self.playing_notes[pattern_index].retain(|_, playing_note| { + playing_note + .stop_time + .is_none_or(|stop_time| stop_time >= pattern_event.time + time_offset) + }); + // Process note events - let playing_notes_in_pattern = &mut self.playing_notes[pattern_index]; - if let Some(Event::NoteEvents(notes)) = pattern_event.event { + if let Some(Event::NoteEvents(notes)) = &pattern_event.event { for (voice_index, note_event) in notes.iter().enumerate() { let note_event = match note_event { - None => continue, Some(note_event) => note_event, + None => continue, }; // Handle note off or stop action if note_event.note.is_note_off() || (note_event.note.is_note_on() + && note_event.glide.is_none() && self.new_note_action != NewNoteAction::Continue) { - if let Some((playback_id, _)) = playing_notes_in_pattern.get(&voice_index) { - // ignore result: source maybe already is stopped - let _ = self - .inner - .stop_source(*playback_id, time_offset + pattern_event.time); - playing_notes_in_pattern.remove(&voice_index); + let stop_time = self.note_event_time(&pattern_event, note_event, time_offset); + + if let Some(playing_note) = + self.playing_notes[pattern_index].get_mut(&voice_index) + { + if playing_note.stop_time.is_none_or(|time| time > stop_time) { + // ignore stop result: source maybe already is stopped + let _ = self.inner.stop_source(playing_note.playback_id, stop_time); + playing_note.stop_time = Some(stop_time); + } } } // Play new note - if !note_event.note.is_note_on() { - continue; - } - if let Some(instrument) = note_event.instrument { - let midi_note = (note_event.note as i32 + 60 - self.sample_root_note as i32) - .clamp(0, 127) as u8; - let volume = note_event.volume.max(0.0); - let panning = note_event.panning.clamp(-1.0, 1.0); - let mut playback_options = FilePlaybackOptions::default() - .speed(speed_from_note(midi_note)) - .volume(volume) - .panning(panning) - .playback_pos_emit_rate(self.playback_pos_emit_rate); - playback_options.fade_out_duration = match self.new_note_action { - NewNoteAction::Continue | NewNoteAction::Stop => { - Some(Duration::from_millis(100)) + if note_event.note.is_note_on() { + if let Some(instrument) = note_event.instrument { + let start_time = + self.note_event_time(&pattern_event, note_event, time_offset); + if note_event.glide.is_none() + || !self.play_glided_note( + pattern_index, + voice_index, + &pattern_event, + note_event, + start_time, + ) + { + self.play_new_note( + pattern_index, + voice_index, + note_event, + instrument, + start_time, + ); } - NewNoteAction::Off(duration) => duration, - }; - playback_options.target_mixer = self.sample_pool.target_mixer(instrument); - - let playback_sample_rate = self.inner.output_sample_rate(); - if let Ok(sample) = - self.sample_pool - .sample(instrument, playback_options, playback_sample_rate) - { - let sample_delay = - (note_event.delay * pattern_event.duration as f32) as SampleTime; - let start_time = Some(time_offset + pattern_event.time + sample_delay); - - let context: Option = - Some(Arc::new(SamplePlaybackContext { - pattern_index: Some(pattern_index), - voice_index: Some(voice_index), - })); - - let playback_id = self - .inner - .play_file_source_with_context(sample, start_time, context) - .expect("Failed to play file source"); - - playing_notes_in_pattern - .insert(voice_index, (playback_id, note_event.note)); - } else { - log::error!(target: "Player", "Failed to get sample with id {}", instrument); } } } } } + // calculate absolute sample time from the given time_offset, applying note event delay. + fn note_event_time( + &self, + pattern_event: &PatternEvent, + note_event: &NoteEvent, + time_offset: SampleTime, + ) -> SampleTime { + let delay = note_event.delay.clamp(0.0, 1.0); + time_offset + pattern_event.time + (delay * pattern_event.duration as f32) as SampleTime + } + + // convert given normalized glide value into a semitones per second based glide value. + fn note_glide_value( + glide: f32, + source_note: Note, + target_note: Note, + samples_per_sec: u32, + event_duration_in_samples: u64, + ) -> f32 { + let semitones = (target_note as u8 as f32 - source_note as u8 as f32).abs(); + if glide <= 0.0 || semitones == 0.0 || event_duration_in_samples == 0 { + return f32::MAX; + } + let event_duration_in_seconds = + (event_duration_in_samples as f64 / samples_per_sec as f64) as f32; + semitones / event_duration_in_seconds / glide + } + + fn play_glided_note( + &mut self, + pattern_index: usize, + voice_index: usize, + pattern_event: &PatternEvent, + note_event: &crate::NoteEvent, + start_time: SampleTime, + ) -> bool { + if let Some(playing_note) = self.playing_notes[pattern_index].get(&voice_index) { + if playing_note.stop_time.is_none_or(|t| t > start_time) { + let midi_note = (note_event.note as i32 + 60 - self.sample_root_note as i32) + .clamp(0, 127) as u8; + let speed = speed_from_note(midi_note); + let volume = note_event.volume.max(0.0); + let panning = note_event.panning.clamp(-1.0, 1.0); + let glide = note_event.glide.unwrap_or(0.0).max(0.0); + let semitones_per_sec_glide = Self::note_glide_value( + glide, + playing_note.note, + note_event.note, + self.inner.output_sample_rate(), + pattern_event.duration, + ); + let playback_id = playing_note.playback_id; + return self + .inner + .set_source_speed( + playback_id, + speed, + Some(semitones_per_sec_glide), + start_time, + ) + .and(self.inner.set_source_volume( + playback_id, + volume, + start_time, // + )) + .and(self.inner.set_source_panning( + playback_id, + panning, + start_time, // + )) + .is_ok(); + } + } + // no note playing which can be glided + false + } + + fn play_new_note( + &mut self, + pattern_index: usize, + voice_index: usize, + note_event: &crate::NoteEvent, + instrument: InstrumentId, + start_time: SampleTime, + ) { + let midi_note = + (note_event.note as i32 + 60 - self.sample_root_note as i32).clamp(0, 127) as u8; + let volume = note_event.volume.max(0.0); + let panning = note_event.panning.clamp(-1.0, 1.0); + + let mut playback_options = FilePlaybackOptions::default() + .speed(speed_from_note(midi_note)) + .volume(volume) + .panning(panning) + .playback_pos_emit_rate(self.playback_pos_emit_rate); + playback_options.fade_out_duration = match self.new_note_action { + NewNoteAction::Continue | NewNoteAction::Stop => Some(Duration::from_millis(100)), + NewNoteAction::Off(duration) => duration, + }; + + let playback_sample_rate = self.inner.output_sample_rate(); + if let Ok(sample) = + self.sample_pool + .sample(instrument, playback_options, playback_sample_rate) + { + let context: Option = Some(Arc::new(SamplePlaybackContext { + pattern_index: Some(pattern_index), + voice_index: Some(voice_index), + })); + + let playback_id = self + .inner + .play_file_source_with_context(sample, Some(start_time), context) + .expect("Failed to play file source"); + + self.playing_notes[pattern_index].insert( + voice_index, + PlayingNote { + playback_id, + note: note_event.note, + stop_time: None, + }, + ); + } else { + log::error!(target: "Player", "Failed to get sample with id {}", instrument); + } + } + fn reset_playback_position(&mut self, sequence: &Sequence) { // stop whatever is playing in case we're restarting self.stop_all_sources(); @@ -624,7 +778,20 @@ impl SamplePlayer { self.playing_notes .resize_with(sequence.phrase_pattern_slot_count(), HashMap::new); // fetch player's actual position and use it as start offset - self.playback_sample_time = self.inner.output_sample_frame_position(); + self.playback_start_sample_time = self.inner.output_sample_frame_position(); self.emitted_sample_time = 0; } } + +// ------------------------------------------------------------------------------------------------- + +/// Single playing note in a player pattern's channel. +#[derive(Debug, Clone, Copy)] +struct PlayingNote { + /// The playback ID of the playing note. + playback_id: PlaybackId, + /// The MIDI note value of the playing note. + note: Note, + /// Some, when a stop note is scheduled for the note. + stop_time: Option, +} diff --git a/src/prelude.rs b/src/prelude.rs index 289b85b2..967951ce 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -55,7 +55,7 @@ pub use super::{ pub use super::{ bindings::{ clear_lua_callback_errors, has_lua_callback_errors, lua_callback_errors, - new_pattern_from_file, new_pattern_from_string, + new_pattern_from_file, new_pattern_from_string, CallbackError, }, emitter::{scripted::ScriptedEmitter, scripted_cycle::ScriptedCycleEmitter}, gate::scripted::ScriptedGate, diff --git a/src/sequence.rs b/src/sequence.rs index 2e5cf08e..145707e9 100644 --- a/src/sequence.rs +++ b/src/sequence.rs @@ -39,6 +39,14 @@ impl Sequence { /// Update the sequence's internal time bases with a new time base. pub fn set_time_base(&mut self, time_base: &BeatTimeBase) { + // update samples to next phrase for the new time base + if let Some(phrase) = self.current_phrase() { + self.sample_position_in_phrase = (self.sample_position_in_phrase as f64 + / phrase.length().to_samples(&self.time_base) + * phrase.length().to_samples(time_base)) + as u64; + } + // apply new time base self.time_base = *time_base; for phrase in &mut self.phrases { phrase.set_time_base(time_base); @@ -46,13 +54,13 @@ impl Sequence { } /// Read-only access to the currently played back phrase. - pub fn current_phrase(&self) -> &Phrase { - &self.phrases[self.phrase_index] + pub fn current_phrase(&self) -> Option<&Phrase> { + self.phrases.get(self.phrase_index) } /// Read-only access to the currently played back phrase. - pub fn current_phrase_mut(&mut self) -> &mut Phrase { - &mut self.phrases[self.phrase_index] + pub fn current_phrase_mut(&mut self) -> Option<&mut Phrase> { + self.phrases.get_mut(self.phrase_index) } /// Read-only access to all phrases. @@ -86,25 +94,29 @@ impl Sequence { if next_phrase_start <= samples_to_run { // run current phrase until it ends let sample_position = self.sample_position; - self.current_phrase_mut() + self.phrases[self.phrase_index] .consume_events_until_time(sample_position + next_phrase_start, consumer); // select next phrase in the sequence - let previous_phrase = self.current_phrase_mut().clone(); + let previous_phrase = self.phrases[self.phrase_index].clone(); self.phrase_index = (self.phrase_index + 1) % self.phrases().len(); self.sample_position_in_phrase = 0; self.sample_position += next_phrase_start; // reset the new phrase or apply continues modes if self.phrases().len() > 1 { let sample_offset = self.sample_position; - self.current_phrase_mut() + self.phrases[self.phrase_index] .reset_with_offset(sample_offset, &previous_phrase); } } else { - // keep running the current phrase - let sample_position = self.sample_position; - self.current_phrase_mut() - .consume_events_until_time(sample_position + samples_to_run, consumer); - self.sample_position_in_phrase += samples_to_run; + // keep running the current phrase, if there is one... + if !self.phrases.is_empty() { + let sample_time = self.sample_position + samples_to_run; + self.phrases[self.phrase_index] + .consume_events_until_time(sample_time, consumer); + self.sample_position_in_phrase += samples_to_run; + } else { + self.sample_position_in_phrase = 0; + } self.sample_position += samples_to_run; } } @@ -121,26 +133,28 @@ impl Sequence { if next_phrase_start <= samples_to_run { // run current phrase until it ends let sample_position = self.sample_position; - self.current_phrase_mut() + self.phrases[self.phrase_index] .advance_until_time(sample_position + next_phrase_start); // select next phrase in the sequence - let previous_phrase = self.current_phrase_mut().clone(); + let previous_phrase = self.phrases[self.phrase_index].clone(); self.phrase_index = (self.phrase_index + 1) % self.phrases().len(); self.sample_position_in_phrase = 0; self.sample_position += next_phrase_start; // reset the new phrase or apply continues modes if self.phrases().len() > 1 { let sample_offset = self.sample_position; - self.current_phrase_mut() + self.phrases[self.phrase_index] .reset_with_offset(sample_offset, &previous_phrase); } } else { - // keep running the current phrase - let sample_position = self.sample_position; - self.current_phrase_mut() - .advance_until_time(sample_position + samples_to_run); - self.sample_position_in_phrase += samples_to_run; - self.sample_position += samples_to_run; + // keep running the current phrase, if there is one... + if !self.phrases.is_empty() { + let sample_time = self.sample_position + samples_to_run; + self.phrases[self.phrase_index].advance_until_time(sample_time); + self.sample_position_in_phrase += samples_to_run; + } else { + self.sample_position_in_phrase = 0; + } } } } @@ -159,10 +173,16 @@ impl Sequence { } fn samples_until_next_phrase(&self, time: u64) -> (u64, u64) { - let phrase_length_in_samples = - self.current_phrase().length().to_samples(&self.time_base) as SampleTime; - let next_phrase_start = phrase_length_in_samples - self.sample_position_in_phrase; - let samples_to_run = time - self.sample_position; - (next_phrase_start, samples_to_run) + if let Some(phrase) = self.current_phrase() { + let phrase_length_in_samples = + phrase.length().to_samples(&self.time_base) as SampleTime; + let next_phrase_start = + phrase_length_in_samples.saturating_sub(self.sample_position_in_phrase); + let samples_to_run = time - self.sample_position; + (next_phrase_start, samples_to_run) + } else { + let samples_to_run = time - self.sample_position; + (u64::MAX, samples_to_run) + } } } diff --git a/src/tidal.rs b/src/tidal.rs index 2b73d0b8..83c24c36 100644 --- a/src/tidal.rs +++ b/src/tidal.rs @@ -1,4 +1,4 @@ //! Tidal mini parser and event generator, used as `Emitter`. mod cycle; -pub use cycle::{Cycle, Event, Span, Target, Value}; +pub use cycle::{Constant, Cycle, Event, Span, SubCycle, Target}; diff --git a/src/tidal/cycle.pest b/src/tidal/cycle.pest index 56691b2d..d4b8d835 100644 --- a/src/tidal/cycle.pest +++ b/src/tidal/cycle.pest @@ -4,9 +4,9 @@ // define whitespaces as space, tab, non-breaking space and newlines WHITESPACE = _{ " " | "\t" | "\u{A0}" | NEWLINE } -/// numbers types allowing [ "1" "1.0" "1." ".1" "0x1a" "0xFF" ] +/// numbers types allowing [ "10" "10.0" "10." ".10" "0x1a" "0xFF" ] integer = @{ "-"? ~ ((("0x" | "0X") ~ ASCII_HEX_DIGIT+) | ASCII_DIGIT+) } -float = @{ "-"? ~ (ASCII_DIGIT ~ "." ~ ASCII_DIGIT*) | ("." ~ ASCII_DIGIT+) } +float = @{ "-"? ~ (ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT*) | ("." ~ ASCII_DIGIT+) } number = ${ (float | integer) ~ !ASCII_ALPHA } /// case-insensitive pitch type with note, optional octave and sharp or flat mark @@ -16,11 +16,13 @@ note = ${ (^"a"|^"b"|^"c"|^"d"|^"e"|^"f"|^"g") } pitch = ${ note ~ mark? ~ octave? ~ !name} /// target properties such as volume "v0.1" (pattrns extension) -target = ${ ("#" ~ integer) | (ASCII_ALPHA ~ float) } +target = ${ ("#" ~ (integer | variable)) | (ASCII_ALPHA ~ (float | variable)) } /// patterns for assigning target keys with pattern on the right side target_name = ${ "#" | name } target_assign = { target_name ~ "=" ~ parameter } +variable = ${ "$" ~ name} + /// chord as pitch with mode string, separated via "'" mode = ${ (ASCII_ALPHANUMERIC | "#" | "-" | "+" | "^")+ } chord = ${ pitch ~ "'" ~ mode } @@ -37,7 +39,8 @@ name = @{ ASCII_ALPHANUMERIC ~ (ASCII_ALPHANUMERIC | "_")* } repeat = { "!" } /// possible literals for single steps -single = { hold | rest | chord | target | pitch | number | name } +constant = _{ hold | rest | chord | target | pitch | number | name } +single = { constant } choice_op = {"|"} stack_op = {","} @@ -55,13 +58,13 @@ polymeter = { "{" ~ sections? ~ "}" ~ polymeter_tail? } group = _{ subdivision | alternating | polymeter } /// parameter for expressions with operators -parameter = _{ single | group } -single_parameter = _{ single } +parameter = _{ single | group | variable } +single_parameter = _{ single | variable } /// static operators -op_replicate = ${ "!" ~ number } -op_weight = ${ "@" ~ number? } -op_degrade = ${ "?" ~ number? } +op_replicate = ${ "!" ~ single_parameter } +op_weight = ${ "@" ~ single_parameter? } +op_degrade = ${ "?" ~ single_parameter? } /// dynamic operators op_fast = { "*" ~ parameter } @@ -73,11 +76,14 @@ op_bjorklund = { "(" ~ (single_parameter ~ ",")+ ~ single_parameter ~ ")" } /// all operators op = _{ op_target | op_degrade | op_replicate | op_weight | op_fast | op_slow | op_bjorklund } -expression = { (single | group) ~ op+ } +expression = { (single | group | variable) ~ op+ } -range = ${ integer ~ ".." ~ integer } +range = ${ (integer | variable) ~ ".." ~ (integer | variable) } /// helper container that splits steps into sections -section = _{ ( expression | range | single | repeat | group)+ } +section = _{ ( expression | range | single | repeat | group | variable )+ } /// the root of the cycle mini = { SOI ~ sections? ~ EOI } + +/// parser for a single constant +constant_literal = { SOI ~ constant ~ EOI } diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index d9d37a2e..edbb4a71 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -1,4 +1,7 @@ -use std::rc::Rc; +use std::{ + collections::{HashMap, HashSet}, + rc::Rc, +}; #[cfg(test)] use std::fmt::Display; @@ -20,14 +23,52 @@ const OVERFLOW_ERROR: &str = "Internal error: integer overflow in cycle"; // ------------------------------------------------------------------------------------------------- +type Vars = HashMap, Step>; + +/// public wrapper over a constant Step containing no variables +/// to be used as a variable for the main cycle +#[derive(Debug, Clone, PartialEq, Default)] +pub struct SubCycle { + step: Step, +} + +impl SubCycle { + pub fn from(input: &str) -> Result { + let cycle = Cycle::from(input)?; + let vars = cycle.root.collect_vars(); + if vars.is_empty() { + Ok(SubCycle { step: cycle.root }) + } else { + Err(format!( + "sub-cycles may not contain variables, found '{}'", + vars.into_iter().collect::>().join(",") + )) + } + } + pub fn rest() -> Self { + Self::new(Step::constant(Constant::Rest, None)) + } + pub fn float(f: f64) -> Self { + Self::new(Step::constant(Constant::Float(f), None)) + } + pub fn integer(i: i32) -> Self { + Self::new(Step::constant(Constant::Integer(i), None)) + } + fn new(step: Step) -> Self { + Self { step } + } +} + /// Tidal cycle mini notation parser and event generator. #[derive(Debug, Clone, PartialEq)] pub struct Cycle { root: Step, event_limit: usize, input: String, + source: Option, seed: Option, state: CycleState, + vars: Option, } impl Cycle { @@ -39,42 +80,45 @@ impl Cycle { /// /// Returns a parse error, when the given string is not a valid mini notation expression. pub fn from(input: &str) -> Result { - match CycleParser::parse(Rule::mini, input) { - Ok(mut tree) => { - if let Some(mini) = tree.next() { - #[cfg(test)] - { - println!("\nTREE"); - Self::print_pairs(&mini, 0); - } - let input = input.to_string(); - let root = CycleParser::step(mini)?; - let state = CycleState { - events: 0, - iteration: 0, - rng: Xoshiro256PlusPlus::from_seed(rng().random()), - }; - let seed = None; - let event_limit = Self::EVENT_LIMIT_DEFAULT; - let cycle = Self { - input, - seed, - root, - state, - event_limit, - }; - #[cfg(test)] - { - println!("\nCYCLE"); - cycle.print(); - } - Ok(cycle) - } else { - Err("couldn't parse input".to_string()) - } + CycleParser::parse_from_rule(Rule::mini, input).and_then(|root_pair| { + let root = CycleParser::step(root_pair)?; + let input = input.to_string(); + let state = CycleState { + events: 0, + iteration: 0, + rng: Xoshiro256PlusPlus::from_seed(rng().random()), + }; + let seed = None; + let source = None; + let event_limit = Self::EVENT_LIMIT_DEFAULT; + let vars = None; + let cycle = Self { + input, + seed, + source, + root, + state, + event_limit, + vars, + }; + #[cfg(test)] + { + println!("\nCYCLE"); + cycle.print(); } - Err(err) => Err(format!("{}", err)), - } + Ok(cycle) + }) + } + + #[cfg(test)] + fn constant_from(input: &str) -> Result { + CycleParser::parse_from_rule(Rule::constant_literal, input).and_then(|root| { + let string = root.as_str(); + match CycleParser::single(root)? { + Step::Single(single) => Ok(single.value), + _ => Err(format!("single constant expected, found {}", string)), + } + }) } /// Rebuild/configure a newly created cycle to use the given custom seed. @@ -89,6 +133,18 @@ impl Cycle { } } + /// Rebuild/configure a newly created cycle with the given source hint. + pub fn with_source(self, source: &str) -> Self { + debug_assert!( + self.state.iteration == 0, + "Should not reconfigure seed of running cycle" + ); + Self { + source: Some(source.to_string()), + ..self + } + } + /// Rebuild/configure cycle to use the given custom event count limit. pub fn with_event_limit(self, event_limit: usize) -> Self { Self { @@ -97,10 +153,36 @@ impl Cycle { } } + pub fn set_var(&mut self, name: &str, subcycle: SubCycle) { + if let Some(vars) = self.vars.as_mut() { + vars.insert(name.into(), subcycle.step); + } else { + let mut vars = HashMap::new(); + vars.insert(name.into(), subcycle.step); + self.vars = Some(vars); + } + } + + pub fn get_var(&self, name: &str) -> Option { + self.vars + .as_ref() + .and_then(|vars| vars.get(name).cloned().map(SubCycle::new)) + } + + pub fn clear_vars(&mut self) { + self.vars = None + } + /// Check if a cycle may give different outputs between cycles. pub fn is_stateful(&self) -> bool { // TODO improve: * and / can change the output, <1> does not etc.. - self.input.contains(['<', '{', '|', '?', '/', '*']) + self.input.contains(['<', '{', '|', '?', '/', '*', '$']) + } + + /// When the cycle got created from a script source, + /// return source string indentifier (maybe a path), else None. + pub fn source(&self) -> &Option { + &self.source } /// Query for the next iteration of output. @@ -112,7 +194,14 @@ impl Cycle { if let Some(seed) = self.seed { self.state.rng = Xoshiro256PlusPlus::seed_from_u64(seed.wrapping_add(cycle as u64)); } - let mut events = Self::output(&self.root, &mut self.state, cycle, self.event_limit, false)?; + let mut events = Self::output( + &self.root, + &mut self.state, + cycle, + self.event_limit, + false, + self.vars.as_ref(), + )?; self.state.iteration += 1; events.transform_spans(&Span::default()); Ok(events.export()) @@ -135,7 +224,7 @@ impl Cycle { pub struct Event { length: Fraction, span: Span, - value: Value, + value: Constant, string: Rc, targets: Vec, } @@ -145,7 +234,7 @@ impl Default for Event { Self { length: Fraction::default(), span: Span::default(), - value: Value::default(), + value: Constant::default(), string: Rc::from("~"), targets: vec![], } @@ -153,14 +242,19 @@ impl Default for Event { } impl Event { - /// The step's original parsed value. - pub fn value(&self) -> &Value { - &self.value + /// The step's value as string representation: Either the value name's original string value + /// or the original value string, unparsed as fallback. + pub fn as_str(&self) -> Rc { + match &self.value { + // prefer `Name` over self.string, as the string may contain unresolved variables + Constant::Name(name) => Rc::clone(name), + _ => Rc::clone(&self.string), + } } - /// The step's original value string, unparsed. - pub fn string(&self) -> &str { - &self.string + /// The step's original parsed value. + pub fn value(&self) -> &Constant { + &self.value } /// The step's time span. @@ -180,7 +274,7 @@ impl Event { } /// Time span for musical events within Cycles. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Span { start: Fraction, end: Fraction, @@ -209,7 +303,8 @@ impl Span { /// Possible types of values that are emitted by a [`Cycle`]. #[derive(Clone, Debug, Default, PartialEq)] -pub enum Value { +pub enum Constant { + Null, #[default] Rest, Hold, @@ -225,7 +320,15 @@ pub enum Value { #[derive(Clone, Debug, PartialEq)] pub enum Target { Index(i32), - Named(Rc, Option), + NamedFloat(Rc, f64), + Named(Rc), +} + +/// The kind of target used for target assignments +#[derive(Clone, Debug, PartialEq)] +pub enum TargetKind { + Index, + Named(Rc), } impl Target { @@ -234,10 +337,21 @@ impl Target { // both are indices: compare index values (Self::Index(_), Self::Index(_)) => true, // both are names: compare names only - (Self::Named(a, _), Self::Named(b, _)) => a == b, + (Self::NamedFloat(a, _), Self::NamedFloat(b, _)) => a == b, _ => false, } } + + pub fn to_integer(&self) -> Option { + match self { + Target::Index(i) => Some(*i), + _ => None, + } + } + + pub fn named_float(name: &str, float: f64) -> Self { + Self::NamedFloat(Rc::from(name), float) + } } /// Pitch with note and octave information for cycle events. @@ -257,32 +371,50 @@ impl Pitch { #[derive(Clone, Debug, PartialEq)] enum Step { + Var(Rc), Single(Single), - Alternating(Alternating), Subdivision(Subdivision), Polymeter(Polymeter), Stack(Stack), Choices(Choices), SpeedExpression(SpeedExpression), - TargetExpression(TargetExpression), - Degrade(Degrade), + Replicated(Replicated), + Weighted(Weighted), + Targeted(Targeted), + Degrade(Degraded), Bjorklund(Bjorklund), - Static(Static), + Ranged(Ranged), + Repeat, +} + +impl Default for Step { + fn default() -> Self { + Self::Single(Single::default()) + } } impl Step { + pub fn constant(constant: Constant, string: Option<&str>) -> Self { + Self::Single(Single { + value: constant, + string: Rc::from(string.unwrap_or_default()), + }) + } + #[cfg(test)] fn inner_steps(&self) -> Vec<&Step> { match self { + Step::Var(_) => vec![], Step::Single(_s) => vec![], - Step::Alternating(a) => a.steps.iter().collect(), - Step::Polymeter(pm) => pm.steps.as_ref().inner_steps(), + Step::Polymeter(p) => p.stack.iter().collect(), Step::Subdivision(sd) => sd.steps.iter().collect(), Step::Choices(cs) => cs.choices.iter().collect(), Step::Stack(st) => st.stack.iter().collect(), - Step::SpeedExpression(e) => vec![&e.left, &e.right], - Step::Degrade(e) => vec![&e.step], - Step::TargetExpression(e) => vec![&e.left, &e.right], + Step::SpeedExpression(e) => vec![&e.step, &e.mult], + Step::Weighted(e) => vec![&e.step, &e.weight], + Step::Replicated(e) => vec![&e.step, &e.repeats], + Step::Degrade(e) => vec![&e.step, &e.chance], + Step::Targeted(e) => vec![&e.step, &e.target], Step::Bjorklund(b) => { if let Some(rotation) = &b.rotation { vec![&b.left, &b.steps, &b.pulses, &**rotation] @@ -290,91 +422,107 @@ impl Step { vec![&b.left, &b.steps, &b.pulses] } } - Step::Static(s) => match s { - Static::Repeat => vec![], - Static::Expression(e) => vec![&e.left], - Static::Range(_) => vec![], - }, + Step::Ranged(r) => vec![&r.start, &r.end], + Step::Repeat => vec![], } } - fn inner_steps_mut(&mut self) -> Vec<&mut Step> { + fn get_vars(&self, vars: &mut HashSet>) { match self { - Step::Single(_s) => vec![], - Step::Alternating(a) => a.steps.iter_mut().collect(), - Step::Subdivision(sd) => sd.steps.iter_mut().collect(), - Step::SpeedExpression(e) => vec![&mut e.left], - Step::Choices(cs) => cs.choices.iter_mut().collect(), - Step::Polymeter(pm) => pm.steps.as_mut().inner_steps_mut(), - Step::Stack(st) => st.stack.iter_mut().collect(), - Step::Degrade(e) => vec![&mut e.step], - Step::TargetExpression(e) => vec![&mut e.left], - Step::Bjorklund(b) => vec![&mut b.left], - Step::Static(s) => match s { - Static::Repeat => vec![], - Static::Range(_) => vec![], - Static::Expression(_) => vec![], - }, + Step::Var(name) => { + vars.insert(Rc::clone(name)); + } + Step::Single(_s) => (), + Step::Polymeter(pm) => { + pm.stack.iter().for_each(|s| s.get_vars(vars)); + if let Some(c) = pm.count.as_ref() { + c.get_vars(vars) + } + } + Step::Subdivision(sd) => sd.steps.iter().for_each(|s| s.get_vars(vars)), + Step::Choices(cs) => cs.choices.iter().for_each(|s| s.get_vars(vars)), + Step::Stack(st) => st.stack.iter().for_each(|s| s.get_vars(vars)), + Step::SpeedExpression(e) => { + e.step.get_vars(vars); + e.mult.get_vars(vars); + } + Step::Weighted(e) => { + e.step.get_vars(vars); + e.weight.get_vars(vars); + } + Step::Replicated(e) => { + e.step.get_vars(vars); + e.repeats.get_vars(vars); + } + Step::Degrade(e) => { + e.step.get_vars(vars); + e.chance.get_vars(vars); + } + Step::Targeted(e) => { + e.step.get_vars(vars); + e.target.get_vars(vars); + } + Step::Bjorklund(b) => { + b.steps.get_vars(vars); + b.pulses.get_vars(vars); + if let Some(rotation) = &b.rotation { + rotation.get_vars(vars); + } + } + Step::Ranged(r) => { + r.start.get_vars(vars); + r.end.get_vars(vars); + } + Step::Repeat => (), } } - fn mutate_singles(&mut self, fun: &mut F) - where - F: FnMut(&mut Single), - { - match self { - Self::Single(s) => fun(s), - _ => self - .inner_steps_mut() - .iter_mut() - .for_each(|s| s.mutate_singles(fun)), - } + fn collect_vars(&self) -> HashSet> { + let mut vars = HashSet::new(); + self.get_vars(&mut vars); + vars } fn rest() -> Self { - Self::Single(Single::default()) + Self::Single(Single { + value: Constant::Rest, + string: Rc::from("~"), + }) } - fn subdivision(steps: Vec) -> Self { + + fn subdivision(steps: Vec) -> Self { Self::Subdivision(Subdivision { steps }) } - fn alternating(steps: Vec) -> Self { - Self::Alternating(Alternating { steps }) + + fn alternating(steps: Vec) -> Self { + Self::polymeter( + vec![steps], + Some(Self::constant(Constant::Integer(1), None)), + ) } - fn polymeter(steps: Vec, count: Step) -> Self { - Step::Polymeter(Polymeter { - steps: Box::new(Step::subdivision(steps)), - count: Box::new(count), + fn polymeter(stack: Vec>, count: Option) -> Self { + Self::Polymeter(Polymeter { + stack: stack.into_iter().map(Self::subdivision).collect(), + count: count.map(Box::new), }) } } -#[derive(Clone, Debug, PartialEq)] -enum Static { - Expression(StaticExpression), - Range(Range), - Repeat, -} - #[derive(Clone, Debug, PartialEq)] struct Single { - value: Value, + value: Constant, string: Rc, } impl Default for Single { fn default() -> Self { Single { - value: Value::Rest, - string: Rc::from("~"), + value: Constant::Null, + string: Rc::from(""), } } } -#[derive(Clone, Debug, PartialEq)] -struct Alternating { - steps: Vec, -} - #[derive(Clone, Debug, PartialEq)] struct Subdivision { steps: Vec, @@ -382,18 +530,9 @@ struct Subdivision { #[derive(Clone, Debug, PartialEq)] struct Polymeter { - count: Box, - steps: Box, -} - -impl Polymeter { - fn length(&self) -> usize { - if let Step::Subdivision(s) = self.steps.as_ref() { - s.steps.len() - } else { - 1 - } // unreachable - } + // a list of exclusively Subdivision steps + stack: Vec, + count: Option>, } #[derive(Clone, Debug, PartialEq)] @@ -410,18 +549,14 @@ struct Stack { enum SpeedOp { Fast(), // * Slow(), // / -} - -#[derive(Clone, Debug, PartialEq)] -enum StaticOp { - Replicate(), // ! - Weight(), // @ + Fit(Fraction), } #[derive(Clone, Debug, PartialEq)] enum Operator { - Static(StaticOp), Speed(SpeedOp), + Replicate(), // ! + Weight(), // @ Target(), // : Bjorklund(), // (p,s,r) Degrade(), // ? @@ -431,8 +566,8 @@ impl Operator { fn parse(pair: Pair) -> Result { match pair.as_rule() { Rule::op_degrade => Ok(Self::Degrade()), - Rule::op_replicate => Ok(Self::Static(StaticOp::Replicate())), - Rule::op_weight => Ok(Self::Static(StaticOp::Weight())), + Rule::op_replicate => Ok(Self::Replicate()), + Rule::op_weight => Ok(Self::Weight()), Rule::op_fast => Ok(Self::Speed(SpeedOp::Fast())), Rule::op_slow => Ok(Self::Speed(SpeedOp::Slow())), Rule::op_target => Ok(Self::Target()), @@ -445,27 +580,33 @@ impl Operator { #[derive(Clone, Debug, PartialEq)] struct SpeedExpression { op: SpeedOp, - left: Box, - right: Box, + step: Box, + mult: Box, } #[derive(Clone, Debug, PartialEq)] -struct StaticExpression { - op: StaticOp, - left: Box, - right: Value, +struct Weighted { + step: Box, + weight: Box, } #[derive(Clone, Debug, PartialEq)] -struct Degrade { +struct Replicated { step: Box, - chance: Value, + repeats: Box, } #[derive(Clone, Debug, PartialEq)] -struct TargetExpression { - left: Box, - right: Box, +struct Degraded { + step: Box, + chance: Box, +} + +#[derive(Clone, Debug, PartialEq)] +struct Targeted { + step: Box, + kind: Option, + target: Box, } #[derive(Clone, Debug, PartialEq)] @@ -477,33 +618,20 @@ struct Bjorklund { } #[derive(Clone, Debug, PartialEq)] -struct Range { - start: i32, - end: i32, +struct Ranged { + start: Box, + end: Box, } // ------------------------------------------------------------------------------------------------- impl Target { - fn parse(value: &Value, value_string: &Rc) -> Option { - match value { - Value::Rest | Value::Hold => None, - Value::Integer(i) => Some(Self::from_index(*i)), - Value::Name(name) => Some(Self::from_name(Rc::clone(name))), - Value::Target(t) => Some(t.clone()), - Value::Float(_) | Value::Pitch(_) | Value::Chord(_, _) => { - // pass unexpected values as raw string and let clients deal with conversions or errors - Some(Self::from_name(Rc::clone(value_string))) - } - } - } - fn from_index(index: i32) -> Self { Self::Index(index) } fn from_name(str: Rc) -> Self { - Self::Named(str, None) + Self::Named(str) } } @@ -585,7 +713,7 @@ impl Display for Pitch { } } -impl Value { +impl Constant { fn parse_integer(str: &str) -> Result { if let Some(hex) = str.strip_prefix("0x").or(str.strip_prefix("0X")) { i32::from_str_radix(hex, 16).map_err(|err| err.to_string()) @@ -602,61 +730,101 @@ impl Value { str.parse::().map_err(|err| err.to_string()) } - fn from_float(str: &str) -> Result { + fn from_float(str: &str) -> Result { Self::parse_float(str).map(Self::Float) } - fn from_integer(str: &str) -> Result { + fn from_integer(str: &str) -> Result { Self::parse_integer(str).map(Self::Integer) } fn to_integer(&self) -> Option { match &self { - Value::Rest => None, - Value::Hold => None, - Value::Integer(i) => Some(*i), - Value::Float(f) => Some(*f as i32), - Value::Pitch(n) => Some(n.midi_note() as i32), - Value::Chord(p, _m) => Some(p.midi_note() as i32), - Value::Target(t) => match t { - Target::Index(i) => Some(*i), - Target::Named(_, v) => v.map(|f| f as i32), - }, - Value::Name(_n) => None, + Self::Null => None, + Self::Rest => None, + Self::Hold => None, + Self::Integer(i) => Some(*i), + Self::Float(f) => Some(*f as i32), + Self::Pitch(n) => Some(n.midi_note() as i32), + Self::Chord(p, _m) => Some(p.midi_note() as i32), + Self::Target(t) => t.to_integer(), + Self::Name(_n) => None, } } fn to_float(&self) -> Option { match &self { - Value::Rest => None, - Value::Hold => None, - Value::Integer(i) => Some(*i as f64), - Value::Float(f) => Some(*f), - Value::Pitch(n) => Some(n.midi_note() as f64), - Value::Chord(n, _m) => Some(n.midi_note() as f64), - Value::Target(t) => match t { + Self::Null => None, + Self::Float(f) => Some(*f), + Self::Integer(i) => Some(*i as f64), + Self::Pitch(n) => Some(n.midi_note() as f64), + Self::Chord(n, _m) => Some(n.midi_note() as f64), + Self::Target(t) => match t { Target::Index(i) => Some(*i as f64), - Target::Named(_, v) => *v, + Target::NamedFloat(_, f) => Some(*f), + Target::Named(_) => None, }, - Value::Name(_n) => None, + + Self::Rest => None, + Self::Hold => None, + Self::Name(_n) => None, } } fn to_chance(&self) -> Option { match &self { - Value::Rest => None, - Value::Hold => None, - Value::Integer(i) => Some((*i as f64).clamp(0.0, 100.0) / 100.0), - Value::Float(f) => Some(f.clamp(0.0, 1.0)), - Value::Pitch(p) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), - Value::Chord(p, _m) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), - Value::Target(t) => match t { + Self::Null => None, + Self::Rest => None, + Self::Hold => None, + Self::Integer(i) => Some((*i as f64).clamp(0.0, 100.0) / 100.0), + Self::Float(f) => Some(f.clamp(0.0, 1.0)), + Self::Pitch(p) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), + Self::Chord(p, _m) => Some((p.midi_note() as f64).clamp(0.0, 128.0) / 128.0), + Self::Target(t) => match t { Target::Index(i) => Some(*i as f64), - Target::Named(_, v) => v.map(|f| f.clamp(0.0, 1.0)), + Target::NamedFloat(_, f) => Some(f.clamp(0.0, 1.0)), + Target::Named(_) => None, + }, + Self::Name(_n) => None, + } + } + + fn to_fraction(&self) -> Option { + self.to_float().and_then(Fraction::from_f64) + } + + fn to_target_without_kind(&self, string: &Rc) -> Option { + match self { + Self::Null => None, + Self::Target(t) => Some(t.clone()), + Self::Integer(i) => Some(Target::from_index(*i)), + Self::Name(name) => Some(Target::from_name(Rc::clone(name))), + Self::Float(_) | Self::Pitch(_) | Self::Chord(_, _) => { + // pass unexpected values as raw string and let clients deal with conversions or errors + Some(Target::from_name(Rc::clone(string))) + } + Self::Rest | Self::Hold => None, + } + } + + fn to_target_with_kind(&self, kind: &TargetKind) -> Option { + match self { + // inner targets override outer target + Self::Target(t) => Some(t.clone()), + // // TODO allow string values for target outputs as per #94 + // Self::Name(_name) => None, + _ => match kind { + TargetKind::Index => self.to_integer().map(Target::from_index), + TargetKind::Named(name) => self + .to_float() + .map(|f| Target::NamedFloat(Rc::clone(name), f)), }, - Value::Name(_n) => None, } } + fn to_target(&self, string: &Rc, kind: Option<&TargetKind>) -> Option { + kind.and_then(|kind| self.to_target_with_kind(kind)) + .or(self.to_target_without_kind(string)) + } } impl Span { @@ -730,7 +898,7 @@ impl Event { end: start + length, }, string: Rc::from("~"), - value: Value::Rest, + value: Constant::Rest, targets: vec![], } } @@ -739,7 +907,7 @@ impl Event { fn with_note(&self, note: u8, octave: u8) -> Self { let pitch = Pitch { note, octave }; Self { - value: Value::Pitch(pitch.clone()), + value: Constant::Pitch(pitch.clone()), string: Rc::from(pitch.to_string()), ..self.clone() } @@ -749,7 +917,7 @@ impl Event { fn with_chord(&self, note: u8, octave: u8, mode: &str) -> Self { let pitch = Pitch { note, octave }; Self { - value: Value::Chord(pitch.clone(), Rc::from(mode)), + value: Constant::Chord(pitch.clone(), Rc::from(mode)), string: Rc::from(format!("{}'{}", pitch, mode)), ..self.clone() } @@ -758,7 +926,7 @@ impl Event { #[cfg(test)] fn with_int(&self, i: i32) -> Self { Self { - value: Value::Integer(i), + value: Constant::Integer(i), string: Rc::from(i.to_string()), ..self.clone() } @@ -767,7 +935,7 @@ impl Event { #[cfg(test)] fn with_name(&self, n: &'static str) -> Self { Self { - value: Value::Name(Rc::from(n)), + value: Constant::Name(Rc::from(n)), string: Rc::from(n.to_string()), ..self.clone() } @@ -776,7 +944,7 @@ impl Event { #[cfg(test)] fn with_float(&self, f: f64) -> Self { Self { - value: Value::Float(f), + value: Constant::Float(f), string: Rc::from(f.to_string()), ..self.clone() } @@ -851,16 +1019,42 @@ impl Events { length: Fraction::ONE, span: Span::default(), string: Rc::from("~"), - value: Value::Rest, + value: Constant::Rest, targets: vec![], }) } - fn maybe_poly(poly: PolyEvents) -> Self { - if poly.channels.len() == 1 { - poly.channels.into_iter().next().expect("len is 1") - } else { - Self::Poly(poly) + fn named(name: &str) -> Self { + Events::Single(Event { + length: Fraction::ONE, + span: Span::default(), + string: Rc::from(name), + value: Constant::Name(Rc::from(name)), + targets: vec![], + }) + } + + fn poly(channels: Vec, length: Fraction, span: Span) -> Self { + match channels.len() { + 0 => Self::empty(), + 1 => channels.first().expect("len is 1").to_owned(), + _ => Self::Poly(PolyEvents { + length, + span, + channels, + }), + } + } + + fn multi(events: Vec, length: Fraction, span: Span) -> Self { + match events.len() { + 0 => Self::empty(), + 1 => events.first().expect("len is 1").to_owned(), + _ => Self::Multi(MultiEvents { + span, + length, + events, + }), } } @@ -872,23 +1066,36 @@ impl Events { } } + fn set_length(&mut self, length: Fraction) { + match self { + Events::Single(s) => s.length = length, + Events::Multi(m) => m.length = length, + Events::Poly(p) => p.length = length, + } + } + + fn first(&self) -> Option<&Event> { + match self { + Events::Single(s) => Some(s), + Events::Multi(m) => m.events.first().and_then(Self::first), + Events::Poly(p) => p.channels.first().and_then(Self::first), + } + } + fn get_span(&self) -> Span { match self { - Events::Single(s) => s.span.clone(), - Events::Multi(m) => m.span.clone(), - Events::Poly(p) => p.span.clone(), + Events::Single(s) => s.span, + Events::Multi(m) => m.span, + Events::Poly(p) => p.span, } } /// Fits a list of events into a Span of 0..1 - fn subdivide_lengths(events: &mut Vec) { + fn subdivide_lengths(events: &mut [Events]) { let mut length = Fraction::ZERO; - for e in &mut *events { - match e { - Events::Single(s) => length += s.length, - Events::Multi(m) => length += m.length, - Events::Poly(p) => length += p.length, - } + + for e in events.iter_mut() { + length += e.get_length(); } let step_size = if length != Fraction::ZERO { Fraction::ONE / length @@ -896,22 +1103,25 @@ impl Events { Fraction::ZERO }; let mut start = Fraction::ZERO; - for e in &mut *events { + for e in events.iter_mut() { match e { Events::Single(s) => { s.length *= step_size; - s.span = Span::new(start, start + s.length); - start += s.length + s.span.start = start; + s.span.end = start + s.length; + start = s.span.end } Events::Multi(m) => { m.length *= step_size; - m.span = Span::new(start, start + m.length); - start += m.length + m.span.start = start; + m.span.end = start + m.length; + start = m.span.end } Events::Poly(p) => { p.length *= step_size; - p.span = Span::new(start, start + p.length); - start += p.length + p.span.start = start; + p.span.end = start + p.length; + start = p.span.end } } } @@ -923,35 +1133,14 @@ impl Events { { match self { Events::Multi(m) => { - let mut filtered = Vec::with_capacity(m.events.len()); - for e in &mut m.events { - match e { - Events::Single(s) => { - if predicate(s) { - filtered.push(e.clone()) - } - } - _ => { - if e.filter_mut(predicate) { - filtered.push(e.clone()) - } - } - } - } - m.events = filtered; + m.events.retain_mut(|e| e.filter_mut(predicate)); !m.events.is_empty() } Events::Poly(p) => { - let mut filtered = Vec::with_capacity(p.channels.len()); - for e in &mut p.channels { - if e.filter_mut(predicate) { - filtered.push(e.clone()) - } - } - p.channels = filtered; + p.channels.retain_mut(|e| e.filter_mut(predicate)); !p.channels.is_empty() } - Events::Single(_) => true, + Events::Single(e) => predicate(e), } } @@ -1041,20 +1230,20 @@ impl Events { } } - /// Recursively collapses Multi and Poly Events into vectors of Single Events - fn flatten(&self, channels: &mut Vec>, channel: &mut usize) { + /// recursively collapses Multi and Poly Events into vectors of Single Events + fn flatten(self, channels: &mut Vec>, channel: &mut usize) { if channels.len() <= *channel { channels.push(vec![]) } match self { - Events::Single(s) => channels[*channel].push(s.clone()), + Events::Single(s) => channels[*channel].push(s), Events::Multi(m) => { - for e in &m.events { + for e in m.events { e.flatten(channels, channel); } } Events::Poly(p) => { - for e in &p.channels { + for e in p.channels { e.flatten(channels, channel); *channel += 1 } @@ -1064,11 +1253,11 @@ impl Events { // filter out holds while extending preceding events fn merge_holds(events: &mut Vec) { - if events.iter().any(|e| e.value == Value::Hold) { + if events.iter().any(|e| e.value == Constant::Hold) { let mut result: Vec = Vec::with_capacity(events.len()); for e in events.iter() { match e.value { - Value::Hold => { + Constant::Hold => { if let Some(last) = result.last_mut() { last.extend(e) } @@ -1084,14 +1273,14 @@ impl Events { // so any remaining rest can be converted to a note-off later // rests at the beginning of a pattern also get dropped fn merge_rests(events: &mut Vec) { - if events.iter().any(|e| e.value == Value::Rest) { + if events.iter().any(|e| e.value == Constant::Rest) { let mut result: Vec = Vec::with_capacity(events.len()); for e in events.iter() { match e.value { - Value::Rest => { + Constant::Rest => { if let Some(last) = result.last_mut() { match last.value { - Value::Rest => last.extend(e), + Constant::Rest => last.extend(e), _ => result.push(e.clone()), } } @@ -1105,15 +1294,15 @@ impl Events { /// Removes Holds by extending preceding events and filters out Rests fn merge(channels: &mut [Vec]) { - for events in &mut *channels { + for events in channels.iter_mut() { Self::merge_holds(events); } - for events in channels { + for events in channels.iter_mut() { Self::merge_rests(events); } } - fn export(&self) -> Vec> { + fn export(self) -> Vec> { let mut channels = vec![]; self.flatten(&mut channels, &mut 0); Self::merge(&mut channels); @@ -1121,7 +1310,6 @@ impl Events { #[cfg(test)] { - self.print(0); println!("\nOUTPUT"); let channel_count = channels.len(); for (ci, channel) in channels.iter().enumerate() { @@ -1136,32 +1324,6 @@ impl Events { channels } - - #[cfg(test)] - fn print(&self, depth: usize) { - let indent = " ".repeat(depth * 2); - match self { - Events::Single(s) => println!("{}'{}' {}", indent, s.string, s), - Events::Multi(m) => { - println!( - "{}multi {} -> {} [{}]", - indent, m.span.start, m.span.end, m.length - ); - for e in &m.events { - e.print(depth + 1) - } - } - Events::Poly(p) => { - println!( - "{}poly {} -> {} [{}]", - indent, p.span.start, p.span.end, p.length - ); - for e in &p.channels { - e.print(depth + 1) - } - } - } - } } // ------------------------------------------------------------------------------------------------- @@ -1170,18 +1332,49 @@ impl Events { #[grammar = "tidal/cycle.pest"] struct CycleParser {} -/// the errors here should be unreachable unless there is a bug in the pest grammar impl CycleParser { + fn parse_from_rule(rule: Rule, input: &'_ str) -> Result, String> { + match Self::parse(rule, input) { + Ok(mut tree) => { + if let Some(step_pair) = tree.next() { + #[cfg(test)] + { + println!("\nTREE"); + Self::print_pairs(&step_pair, 0); + } + Ok(step_pair) + } else { + Err("couldn't parse input".to_string()) + } + } + Err(err) => Err(format!("{}", err)), + } + } + + #[cfg(test)] + fn print_pairs(pair: &Pair, level: usize) { + println!( + "{} {:?} {:?}", + indent_lines(level), + pair.as_rule(), + pair.as_str() + ); + for p in pair.clone().into_inner() { + Self::print_pairs(&p, level + 1) + } + } + + /// the errors here should be unreachable unless there is a bug in the pest grammar /// recursively parse a pair as a Step fn step(pair: Pair) -> Result { match pair.as_rule() { - Rule::single => Self::single(pair), - Rule::repeat => Ok(Step::Static(Static::Repeat)), + Rule::variable => Self::variable(pair), + Rule::single => Ok(Self::single(pair)?), + Rule::repeat => Ok(Step::Repeat), Rule::subdivision | Rule::mini => Self::group(pair, Step::subdivision), Rule::alternating => Self::group(pair, Step::alternating), Rule::polymeter => Self::polymeter(pair), Rule::range => Self::range(pair), - Rule::target_assign => Self::target_assign(pair), Rule::expression => Self::expression(pair), _ => Err(format!( "unexpected rule, this is a bug in the parser\n{:?}", @@ -1190,128 +1383,131 @@ impl CycleParser { } } - /// parse a pair inside a single as a value - fn value(pair: Pair) -> Result { - match pair.as_rule() { - Rule::integer => Value::from_integer(pair.as_str()), - Rule::float => Value::from_float(pair.as_str()), - Rule::number => { - if let Some(n) = pair.into_inner().next() { - match n.as_rule() { - Rule::integer => Value::from_integer(n.as_str()), - Rule::float => Value::from_float(n.as_str()), - _ => Err(format!("unrecognized number\n{:?}", n)), - } - } else { - Err("empty single".to_string()) - } - } - Rule::hold => Ok(Value::Hold), - Rule::rest => Ok(Value::Rest), - Rule::pitch => Ok(Value::Pitch(Pitch::parse(pair))), - Rule::chord => { - let mut pitch = Pitch { note: 0, octave: 4 }; - let mut mode = ""; - for p in pair.into_inner() { - match p.as_rule() { - Rule::pitch => { - pitch = Pitch::parse(p); - } - Rule::mode => { - mode = p.as_str(); - } - _ => (), - } - } - Ok(Value::Chord(pitch, Rc::from(mode))) - } - Rule::target => { - let name = pair.as_str().get(0..1).ok_or(format!( - "error in grammar, missing target key in pair\n{:?}", - pair - ))?; - let value = pair.clone().into_inner().next().ok_or(format!( - "error in grammar, missing target value in pair\n{:?}", - pair - ))?; - - match name.as_bytes() { - b"#" => Ok(Value::Target(Target::Index(Value::parse_integer( - value.as_str(), - )?))), - _ => Ok(Value::Target(Target::Named( - Rc::from(name), - Some(Value::parse_float(value.as_str())?), - ))), - } - } - Rule::name => Ok(Value::Name(Rc::from(pair.as_str()))), - _ => Err(format!("unrecognized target value\n{:?}", pair)), - } + fn variable(pair: Pair) -> Result { + pair.into_inner() + .next() + .ok_or_else(|| "error in grammar, missing variable name".to_string()) + .map(|name_pair| Rc::from(name_pair.as_str())) + .map(Step::Var) } - fn single(pair: Pair) -> Result { + fn variable_target( + pair: Pair, + target_name: &str, + kind: TargetKind, + ) -> Result { + let name = Rc::from(target_name); pair.clone() .into_inner() .next() - .ok_or_else(|| format!("empty single {}", pair)) - .and_then(|value_pair| { - Ok(Step::Single(Single { - string: Rc::from(value_pair.as_str()), - value: Self::value(value_pair)?, + .map(|variable_pair| { + Ok(Step::Targeted(Targeted { + step: Box::new(Step::Single(Single { + value: Constant::Null, + string: Rc::clone(&name), + })), + kind: Some(kind), + target: Box::new(Self::variable(variable_pair)?), })) }) + .ok_or_else(|| format!("error in grammar, unexpected rule for variable\n{pair:?}"))? } - /// transform static steps into their final form and push them onto a list - fn push_applied(steps: &mut Vec, step: Step) { - match &step { - Step::Static(s) => match s { - Static::Repeat => { - let repeat = steps.last().cloned().unwrap_or(Step::rest()); - steps.push(repeat) + /// parse a pair inside a single as a value + fn single(pair: Pair) -> Result { + let pair = pair + .clone() + .into_inner() + .next() + .ok_or_else(|| format!("empty single {}", pair))?; + + let string = Rc::from(pair.as_str()); + + let constant = + match pair.as_rule() { + // Rule::variable => Self::variable(pair), + Rule::target => { + let name = pair.as_str().get(0..1).ok_or_else(|| { + format!("error in grammar, missing target key in pair\n{:?}", pair) + })?; + let value = pair.clone().into_inner().next().ok_or_else(|| { + format!("error in grammar, missing target value in pair\n{:?}", pair) + })?; + + match name.as_bytes() { + b"#" => match value.as_rule() { + Rule::integer => Constant::Target(Target::Index( + Constant::parse_integer(value.as_str())?, + )), + Rule::variable => { + return Self::variable_target(pair, name, TargetKind::Index) + } + _ => { + return Err("error in grammar, unexpected rule for target index" + .to_string()) + } + }, + _ => match value.as_rule() { + Rule::float => Constant::Target(Target::NamedFloat( + Rc::from(name), + Constant::parse_float(value.as_str())?, + )), + Rule::variable => { + return Self::variable_target( + pair, + name, + TargetKind::Named(Rc::from(name)), + ) + } + _ => { + return Err("error in grammar, unexpected rule for target float" + .to_string()) + } + }, + } } - Static::Expression(e) => match e.op { - StaticOp::Replicate() => { - steps.push(e.left.as_ref().clone()); - if let Some(repeats) = e.right.to_integer() { - if repeats > 0 { - for _i in 1..repeats { - steps.push(e.left.as_ref().clone()) - } + _ => match pair.as_rule() { + Rule::integer => Constant::from_integer(pair.as_str())?, + Rule::float => Constant::from_float(pair.as_str())?, + Rule::number => { + if let Some(n) = pair.into_inner().next() { + match n.as_rule() { + Rule::integer => Constant::from_integer(n.as_str())?, + Rule::float => Constant::from_float(n.as_str())?, + _ => return Err(format!("unrecognized number\n{:?}", n)), } + } else { + return Err("empty single".to_string()); } } - StaticOp::Weight() => { - steps.push(e.left.as_ref().clone()); - if let Some(repeats) = e.right.to_integer() { - if repeats > 0 { - for _i in 1..repeats { - steps.push(Step::Single(Single { - value: Value::Hold, - string: Rc::from("_"), - })) + Rule::hold => Constant::Hold, + Rule::rest => Constant::Rest, + Rule::pitch => Constant::Pitch(Pitch::parse(pair)), + Rule::chord => { + let mut pitch = Pitch { note: 0, octave: 4 }; + let mut mode = ""; + for p in pair.into_inner() { + match p.as_rule() { + Rule::pitch => { + pitch = Pitch::parse(p); } + Rule::mode => { + mode = p.as_str(); + } + _ => (), } } + Constant::Chord(pitch, Rc::from(mode)) } + Rule::name => Constant::Name(Rc::from(pair.as_str())), + _ => return Err(format!("unrecognized target value\n{:?}", pair)), }, - Static::Range(r) => { - let range = if r.start <= r.end { - Box::new(r.start..=r.end) as Box> - } else { - Box::new((r.end..=r.start).rev()) as Box> - }; - for i in range { - steps.push(Step::Single(Single { - value: Value::Integer(i), - string: Rc::from(i.to_string()), - })) - } - } - }, - _ => steps.push(step), - } + }; + + Ok(Step::Single(Single { + value: constant, + string, + })) } /// helper to split a list of pairs over a rule, used for stacks and split shorthand @@ -1336,9 +1532,9 @@ impl CycleParser { if p.as_rule() == Rule::choice_op { is_choice = true; } else if is_choice { - let last = choiced_pairs.last_mut().ok_or_else(|| { - "this can never happen as '|' can never start a section".to_string() - })?; + let last = choiced_pairs + .last_mut() + .ok_or("this can never happen as '|' can never start a section")?; last.push(p); is_choice = false } else { @@ -1352,7 +1548,7 @@ impl CycleParser { if let Some(first) = vs.first() { if vs.len() > 1 { Ok(Step::Choices(Choices { - choices: Self::section_vec(vs)?, + choices: Self::with_choices(vs)?, })) } else { Self::step(first.clone()) @@ -1364,19 +1560,10 @@ impl CycleParser { .collect() } - fn section_vec(pairs: Vec>) -> Result, String> { - let choiced_steps = Self::with_choices(pairs)?; - let mut steps = Vec::with_capacity(choiced_steps.len()); - for step in choiced_steps.into_iter() { - Self::push_applied(&mut steps, step) - } - Ok(steps) - } - fn section(pairs: Vec>) -> Result, String> { let split_pairs = Self::split_over(pairs, Rule::split_op) .into_iter() - .map(Self::section_vec) + .map(Self::with_choices) .collect::>, String>>()?; Ok(if split_pairs.len() > 1 { @@ -1418,7 +1605,10 @@ impl CycleParser { if let Some(count) = pair.clone().into_inner().next() { Self::step(count) } else { - Err(format!("missing polymeter count '{}'", pair.as_str())) + Err(format!( + "error in grammar, missing polymeter count '{}'", + pair.as_str() + )) } } @@ -1447,71 +1637,49 @@ impl CycleParser { }; match (count, stack, steps) { - (Some(count), None, Some(steps)) => { - // a regular polymeter with explicit count - Ok(Step::polymeter(steps, count)) - } - (Some(count), Some(stack), _) => { - // sections in a stack with explicit count will all have that - Ok(Step::Stack(Stack { - stack: stack - .into_iter() - .map(|steps| Step::polymeter(steps, count.clone())) - .collect(), - })) - } - (None, Some(stack), _) => { - let count = stack - .first() - .map(Vec::len) - .ok_or_else(|| format!("empty stack {:?}", stack))?; - - if stack.len() > 1 && count > 0 { - let count = Step::Single(Single { - value: Value::Integer(count as i32), - string: Rc::from(count.to_string()), - }); - // if there is a stack but no count, the first section will determine the count of the rest - Ok(Step::Stack(Stack { - stack: stack - .into_iter() - .map(|steps| Step::polymeter(steps, count.clone())) - .collect(), - })) - } else { - // unreachable, a stack will always have more than one sections with each having at least one item - Err(format!("invalid stack {:?}", stack)) - } - } + // empty polymeters become a single rest + // {}, {}%2 ... + (_, None, None) => Ok(Step::rest()), // if there is only one section and no count, it is treated as a subdivision (None, None, Some(steps)) => Ok(Step::subdivision(steps)), - // empty polymeter like {} and {}%2 will become a single rest - _ => Ok(Step::rest()), + // a mono polymeter with optional count + (count, None, Some(steps)) => Ok(Step::polymeter(vec![steps], count)), + // a stacked polymeter with optional count + (count, Some(stack), _) => Ok(Step::polymeter(stack, count)), + } + } + + fn integer_or_variable(pair: Pair) -> Result { + match pair.as_rule() { + Rule::integer => Ok(Step::constant( + Constant::Integer(pair.as_str().parse::().map_err(|_| { + format!( + "error in grammar, integer cannot be parsed '{}'", + pair.as_str() + ) + })?), + Some(pair.as_str()), + )), + Rule::variable => Self::variable(pair), + _ => Err("error in grammar".to_string()), } } fn range(pair: Pair) -> Result { let mut inner = pair.clone().into_inner(); + let start_pair = inner .next() - .ok_or_else(|| format!("empty expression\n{:?}", pair))?; - let start = start_pair.as_str().parse::().map_err(|_| { - format!( - "range expected integer on the left side, got '{}'", - start_pair.as_str() - ) - })?; + .ok_or_else(|| format!("error in grammar, empty range expression\n{:?}", pair))?; let end_pair = inner .next() - .ok_or_else(|| "range expression has no right side".to_string())?; - let end = end_pair.as_str().parse::().map_err(|_| { - format!( - "range expected integer on the right side, got '{}'", - end_pair.as_str() - ) - })?; - Ok(Step::Static(Static::Range(Range { start, end }))) + .ok_or("error in grammar, incomplete range expression")?; + + Ok(Step::Ranged(Ranged { + start: Box::new(Self::integer_or_variable(start_pair)?), + end: Box::new(Self::integer_or_variable(end_pair)?), + })) } fn bjorklund(left: Step, op_pair: Pair) -> Result { @@ -1541,38 +1709,54 @@ impl CycleParser { String::from("unreachable: missing right hand side from op_pair, error in grammar!") } - fn static_expression(left: Step, op: StaticOp, op_pair: Pair) -> Result { - let right = if let Some(right_pair) = op_pair.into_inner().next() { - right_pair - .into_inner() - .next() - .ok_or_else(Self::invalid_right_hand) - .and_then(Self::value)? + fn optional_single_right_hand( + op_pair: Pair, + default: fn() -> Step, + ) -> Result { + if let Some(pair) = op_pair.into_inner().next() { + match pair.as_rule() { + Rule::single => Self::single(pair), + Rule::variable => Self::variable(pair), + _ => Err("error in grammar".to_string()), + } } else { - Value::Integer(2) - }; + Ok(default()) + } + } - Ok(Step::Static(Static::Expression(StaticExpression { - left: Box::new(left), - right, - op, - }))) + // TODO allow for a pattern on the right for weight and replicate + // with the current pest setup, this seems impossible if we want to support optional parameter here + // at least it is impossible without major rearrangement of the grammar and parsing + fn weight_expression(left: Step, op_pair: Pair) -> Result { + let weight = Self::optional_single_right_hand(op_pair, || { + Step::constant(Constant::Float(2.0), Some("2.0")) + })?; + + Ok(Step::Weighted(Weighted { + step: Box::new(left), + weight: Box::new(weight), + })) + } + + fn replicate_expression(left: Step, op_pair: Pair) -> Result { + let count = Self::optional_single_right_hand(op_pair, || { + Step::constant(Constant::Float(2.0), Some("2.0")) + })?; + + Ok(Step::Replicated(Replicated { + step: Box::new(left), + repeats: Box::new(count), + })) } fn degrade_expression(step: Step, op_pair: Pair) -> Result { - let chance = if let Some(right_pair) = op_pair.into_inner().next() { - right_pair - .into_inner() - .next() - .ok_or_else(Self::invalid_right_hand) - .and_then(Self::value)? - } else { - Value::Float(0.5) - }; + let chance = Self::optional_single_right_hand(op_pair, || { + Step::constant(Constant::Float(0.5), Some("0.5")) + })?; - Ok(Step::Degrade(Degrade { + Ok(Step::Degrade(Degraded { step: Box::new(step), - chance, + chance: Box::new(chance), })) } @@ -1583,8 +1767,8 @@ impl CycleParser { .ok_or_else(Self::invalid_right_hand) .and_then(Self::step)?; Ok(Step::SpeedExpression(SpeedExpression { - left: Box::new(left), - right: Box::new(right), + step: Box::new(left), + mult: Box::new(right), op, })) } @@ -1593,11 +1777,35 @@ impl CycleParser { let right = op_pair .into_inner() .next() - .ok_or_else(Self::invalid_right_hand) - .and_then(Self::step)?; - Ok(Step::TargetExpression(TargetExpression { - left: Box::new(left), - right: Box::new(right), + .ok_or_else(Self::invalid_right_hand)?; + + let (kind, target) = match right.as_rule() { + Rule::target_assign => { + let mut right = right.into_inner(); + + let target_name_pair = + right.next().ok_or("error in grammar, missing target key")?; + if target_name_pair.as_rule() != Rule::target_name { + return Err("error in grammar, expected target_name".to_string()); + } + + let p = right.next().ok_or("missing step pattern")?; + let mut target_name = target_name_pair.into_inner(); + if let Some(target_name) = target_name.next() { + // target name was specified + (Some(TargetKind::Named(Rc::from(target_name.as_str()))), p) + } else { + // # was used as indexed target + (Some(TargetKind::Index), p) + } + } + _ => (None, right), + }; + + Ok(Step::Targeted(Targeted { + step: Box::new(left), + kind, + target: Box::new(Self::step(target)?), })) } @@ -1612,7 +1820,8 @@ impl CycleParser { // Loop over operators and parameters, creating a nested expression if multiple pairs are present for op_pair in inner { left = match Operator::parse(op_pair.clone())? { - Operator::Static(op) => Self::static_expression(left, op, op_pair)?, + Operator::Replicate() => Self::replicate_expression(left, op_pair)?, + Operator::Weight() => Self::weight_expression(left, op_pair)?, Operator::Speed(op) => Self::speed_expression(left, op, op_pair)?, Operator::Target() => Self::target_expression(left, op_pair)?, Operator::Degrade() => Self::degrade_expression(left, op_pair)?, @@ -1621,38 +1830,6 @@ impl CycleParser { } Ok(left) } - fn target_assign(pair: Pair) -> Result { - let mut inner = pair.into_inner(); - - let k = inner.next().ok_or("error in grammar, missing target key")?; - if k.as_rule() != Rule::target_name { - return Err("error in grammar, expected target_name".to_string()); - } - - let p = inner.next().ok_or("missing step pattern")?; - let mut pattern = Self::step(p)?; - let mut key = k.into_inner(); - if let Some(name) = key.next() { - pattern.mutate_singles(&mut |single: &mut Single| { - if let Some(f) = single.value.to_float() { - if !matches!(single.value, Value::Target(_)) { - single.value = - Value::Target(Target::Named(Rc::from(name.as_str()), Some(f))); - } - } - }); - } else { - pattern.mutate_singles(&mut |single: &mut Single| { - if let Some(i) = single.value.to_integer() { - if !matches!(single.value, Value::Target(_)) { - single.value = Value::Target(Target::Index(i)); - } - } - }); - } - - Ok(pattern) - } } // ------------------------------------------------------------------------------------------------- @@ -1671,6 +1848,7 @@ impl Cycle { span: &Span, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { let range = span.whole_range(); let mut cycles = Vec::with_capacity(range.clone().count()); @@ -1679,12 +1857,12 @@ impl Cycle { Fraction::from_u32(cycle).ok_or(OVERFLOW_ERROR)?, Fraction::from_u32(cycle + 1).ok_or(OVERFLOW_ERROR)?, ); - let mut events = Self::output(step, state, cycle, limit, overlap)?; + let mut events = Self::output(step, state, cycle, limit, overlap, vars)?; events.transform_spans(&span); cycles.push(events) } let mut events = Events::Multi(MultiEvents { - span: span.clone(), + span: *span, length: span.length(), events: cycles, }); @@ -1694,58 +1872,102 @@ impl Cycle { fn output_multiplied( step: &Step, + mult: Fraction, state: &mut CycleState, cycle: u32, - mult: Fraction, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { let span = Span::new( Fraction::from_u32(cycle).ok_or(OVERFLOW_ERROR)? * mult, Fraction::from_u32(cycle + 1).ok_or(OVERFLOW_ERROR)? * mult, ); - let mut events = Self::output_span(step, state, &span, limit, overlap)?; + let mut events = Self::output_span(step, state, &span, limit, overlap, vars)?; events.normalize_spans(&span); Ok(events) } - // helper to calculate the right multiplier for polymeter and speed expressions - fn step_multiplier(step: &Step, value: &Value) -> Fraction { - match step { - Step::Polymeter(pm) => { - let length = pm.length() as f64; - let count = value.to_float().unwrap_or(0.0); - Fraction::from_f64(count).unwrap_or(Fraction::ZERO) - / Fraction::from_f64(length).unwrap_or(Fraction::ONE) + fn step_length( + step: &Step, + state: &mut CycleState, + cycle: u32, + limit: usize, + overlap: bool, + vars: Option<&Vars>, + ) -> Result { + let length_mod = match step { + Step::Replicated(replicated) => Some(replicated.repeats.as_ref()), + Step::Weighted(weighted) => Some(weighted.weight.as_ref()), + _ => None, + }; + + if let Some(step) = length_mod { + let right_events = Self::output(step, state, cycle, limit, overlap, vars)?; + Ok(right_events + .first() + .and_then(|e| e.value.to_fraction()) + .unwrap_or(Fraction::ONE)) + } else { + Ok(Fraction::ONE) + } + } + + fn sub_length( + step: &Step, + state: &mut CycleState, + cycle: u32, + limit: usize, + overlap: bool, + vars: Option<&Vars>, + ) -> Result { + Ok(match step { + Step::Subdivision(sub) => { + let mut length = Fraction::ZERO; + let mut last_length = Fraction::ZERO; + for s in sub.steps.iter() { + if !matches!(s, Step::Repeat) { + last_length = Self::step_length(s, state, cycle, limit, overlap, vars)?; + } + length += last_length; + } + length } - Step::SpeedExpression(e) => match e.op { - SpeedOp::Fast() => { - if let Some(right) = value.to_float() { - Fraction::from_f64(right).unwrap_or(Fraction::ZERO) + _ => Fraction::ONE, + }) + } + + // helper to calculate the right multiplier for polymeter and speed expressions + fn step_multiplier(op: &SpeedOp, value: &Constant) -> Fraction { + match op { + SpeedOp::Fast() => value.to_fraction().unwrap_or(Fraction::ZERO), + SpeedOp::Slow() => value + .to_float() + .and_then(|div| { + if div != 0.0 { + Fraction::from_f64(1.0 / div) } else { - Fraction::ZERO + None } - } - SpeedOp::Slow() => { - if let Some(right) = value.to_float() { - if right != 0.0 { - Fraction::from_f64(1.0 / right).unwrap_or(Fraction::ZERO) - } else { - Fraction::ZERO - } + }) + .unwrap_or(Fraction::ZERO), + SpeedOp::Fit(outer) => value + .to_fraction() + .and_then(|inner| { + if *outer != Fraction::ZERO { + Some(inner / outer) } else { - Fraction::from(0) + None } - } - }, - _ => Fraction::from(1), + }) + .unwrap_or(Fraction::ZERO), } } // overlay two lists of events and apply the targets from the second to the first - fn apply_targets(events: &mut [Event], target_events: &[Event]) { + fn apply_targets(events: &mut [Event], target_events: &[Event], kind: Option<&TargetKind>) { for target_event in target_events.iter() { - if let Some(target) = Target::parse(&target_event.value, &target_event.string) { + if let Some(target) = target_event.value.to_target(&target_event.string, kind) { for event in events.iter_mut() { if event.span.overlaps(&target_event.span) && !{ @@ -1782,29 +2004,34 @@ impl Cycle { state: &mut CycleState, cycle: u32, limit: usize, + vars: Option<&Vars>, ) -> Result<(Vec>, Span), String> { - let mut events = Self::output(step, state, cycle, limit, true)?; + let mut events = Self::output(step, state, cycle, limit, true, vars)?; events.transform_spans(&events.get_span()); let mut channels = vec![]; + let span = events.get_span(); events.flatten(&mut channels, &mut 0); Events::merge(&mut channels); - Ok((channels, events.get_span())) + Ok((channels, span)) } // generate events from Target expressions fn output_with_target( - left: &Step, - right: &Step, + exp: &Targeted, state: &mut CycleState, cycle: u32, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { - match right { - // multiply with single values to avoid generating events + let (step, target_kind, target_step) = + (exp.step.as_ref(), exp.kind.as_ref(), exp.target.as_ref()); + + match target_step { + // assign single value to avoid generating events Step::Single(single) => { - let mut events = Self::output(left, state, cycle, limit, overlap)?; - if let Some(target) = Target::parse(&single.value, &single.string) { + let mut events = Self::output(step, state, cycle, limit, overlap, vars)?; + if let Some(target) = single.value.to_target(&single.string, target_kind) { events.mutate_events(&mut |event: &mut Event| { if !{ let this = &event; @@ -1819,58 +2046,57 @@ impl Cycle { } _ => { // generate all the events as flat vecs from both the left and right side of the expression - let (left_channels, left_span) = Self::output_flat(left, state, cycle, limit)?; - let (target_channels, _) = Self::output_flat(right, state, cycle, limit)?; + let (left_channels, left_span) = + Self::output_flat(step, state, cycle, limit, vars)?; + let (target_channels, _) = + Self::output_flat(target_step, state, cycle, limit, vars)?; // iterate over channels from both sides to create necessary new stacks if the right side is polyphonic let mut channel_events: Vec = Vec::with_capacity(target_channels.len()); for channel in target_channels.into_iter() { for left_channel in left_channels.iter() { let mut cloned_left = left_channel.clone(); - Self::apply_targets(&mut cloned_left, &channel); - channel_events.push(Events::Multi(MultiEvents { - length: left_span.length(), - span: left_span.clone(), - events: cloned_left.into_iter().map(Events::Single).collect(), - })); + Self::apply_targets(&mut cloned_left, &channel, target_kind); + channel_events.push(Events::multi( + cloned_left.into_iter().map(Events::Single).collect(), + left_span.length(), + left_span, + )); } } // put all the resulting events back together - Ok(Events::maybe_poly(PolyEvents { - length: left_span.length(), - span: left_span, - channels: channel_events, - })) + Ok(Events::poly(channel_events, left_span.length(), left_span)) } } } // output a multiplied pattern expression with support for patterns on the right side + #[allow(clippy::too_many_arguments)] fn output_with_speed( - right: &Step, step: &Step, + op: &SpeedOp, + mult: &Step, state: &mut CycleState, cycle: u32, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { - let left = match step { - Step::Polymeter(pm) => pm.steps.as_ref(), - Step::SpeedExpression(exp) => exp.left.as_ref(), - _ => step, - }; - match right { + match mult { // multiply with single values to avoid generating events Step::Single(single) => { // apply multiplier - let multiplier = Self::step_multiplier(step, &single.value); + let multiplier = Self::step_multiplier(op, &single.value); Ok(Self::output_multiplied( - left, state, cycle, multiplier, limit, overlap, + step, multiplier, state, cycle, limit, overlap, vars, )?) } _ => { // generate and flatten the events for the right side of the expression - let events = Self::output(right, state, cycle, limit, overlap)?; + let mut events = Self::output(mult, state, cycle, limit, overlap, vars)?; + events.transform_spans(&Span::default()); + let length = events.get_length(); + let span = events.get_span(); let channels = events.export(); // extract a float to use as mult from each event and output the step with it @@ -1878,28 +2104,19 @@ impl Cycle { for channel in channels.into_iter() { let mut multi_events: Vec = Vec::with_capacity(channel.len()); for event in channel { - // apply multiplier - let multiplier = Self::step_multiplier(step, &event.value); + let multiplier = Self::step_multiplier(op, &event.value); let mut partial_events = Self::output_multiplied( - left, state, cycle, multiplier, limit, overlap, + step, multiplier, state, cycle, limit, overlap, vars, )?; - // crop and push to multi events + // crop to each span of the resulting multipliers and concat partial_events.crop(&event.span, overlap); multi_events.push(partial_events); } - channel_events.push(Events::Multi(MultiEvents { - length: events.get_length(), - span: events.get_span(), - events: multi_events, - })); + channel_events.push(Events::multi(multi_events, length, span)); } // put all the resulting events back together - Ok(Events::maybe_poly(PolyEvents { - length: events.get_length(), - span: events.get_span(), - channels: channel_events, - })) + Ok(Events::poly(channel_events, length, span)) } } } @@ -1911,8 +2128,20 @@ impl Cycle { cycle: u32, limit: usize, overlap: bool, + vars: Option<&Vars>, ) -> Result { let events = match step { + Step::Var(name) => { + if let Some(vars) = vars { + if let Some(step) = vars.get(name) { + Self::output(step, state, cycle, limit, overlap, Some(vars))? + } else { + Events::named(name) + } + } else { + Events::named(name) + } + } Step::Single(s) => { state.events += 1; if state.events > limit { @@ -1933,40 +2162,115 @@ impl Cycle { if sd.steps.is_empty() { Events::empty() } else { - let mut events = Vec::with_capacity(sd.steps.len()); + let mut events: Vec = Vec::with_capacity(sd.steps.len()); for s in &sd.steps { - let e = Self::output(s, state, cycle, limit, overlap)?; - events.push(e) + events.push(if matches!(s, Step::Repeat) { + if let Some(last) = events.last() { + last.clone() + } else { + Events::empty() + } + } else { + Self::output(s, state, cycle, limit, overlap, vars)? + }) } Events::subdivide_lengths(&mut events); - Events::Multi(MultiEvents { - span: Span::default(), - length: Fraction::ONE, - events, - }) + Events::multi(events, Fraction::ONE, Span::default()) } } - Step::Alternating(a) => { - if a.steps.is_empty() { - Events::empty() - } else { - let length = a.steps.len() as u32; - let current = cycle % length; - a.steps - .get(current as usize) - .map(|step| Self::output(step, state, cycle / length, limit, overlap)) - .unwrap_or( - Ok(Events::empty()), // unreachable - )? - } + Step::Weighted(we) => { + let weight = Self::output(we.weight.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_fraction()) + .unwrap_or(Fraction::ONE); + + let mut events = + Self::output(we.step.as_ref(), state, cycle, limit, overlap, vars)?; + events.set_length(weight); + events + } + Step::Replicated(we) => { + let repeats = + Self::output(we.repeats.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_float()) + .unwrap_or(2.0); + + let ceil = repeats.ceil(); + let len = if ceil == 0.0 { 1 } else { ceil as usize }; + let mult = repeats / ceil; + + let steps = vec![we.step.as_ref().clone(); len]; + let sub = Step::subdivision(steps); + + // PERF cache this if the right side is static + let step = Step::SpeedExpression(SpeedExpression { + op: SpeedOp::Fast(), + step: Box::from(sub), + mult: Box::from(Step::Single(Single { + value: Constant::Float(mult), + string: Rc::from(""), + })), + }); + + let mut events = Self::output(&step, state, cycle, limit, overlap, vars)?; + events.set_length(Fraction::from_f64(repeats).unwrap_or(Fraction::ONE)); + events } Step::Choices(cs) => { let choice = state.rng.random_range(0..cs.choices.len()); - Self::output(&cs.choices[choice], state, cycle, limit, overlap)? + Self::output(&cs.choices[choice], state, cycle, limit, overlap, vars)? } Step::Polymeter(pm) => { - Self::output_with_speed(pm.count.as_ref(), step, state, cycle, limit, overlap)? + if let Some(count) = &pm.count { + let mut channels = vec![]; + for sub in pm.stack.iter() { + let length = Self::sub_length(sub, state, cycle, limit, overlap, vars)?; + let events = Self::output_with_speed( + sub, + &SpeedOp::Fit(length), + count, + state, + cycle, + limit, + overlap, + vars, + )?; + channels.push(events) + } + Events::poly(channels, Fraction::ONE, Span::default()) + } else { + let Some(first) = pm.stack.first() else { + return Ok(Events::empty()); + }; + + let first_length = Self::sub_length(first, state, cycle, limit, overlap, vars)?; + let mult = Step::constant( + Constant::Float((first_length).to_f64().unwrap_or_default()), + None, + ); + let mut channels = Vec::with_capacity(pm.stack.len()); + for (i, sub) in pm.stack.iter().enumerate() { + let length = if i == 0 { + first_length + } else { + Self::sub_length(sub, state, cycle, limit, overlap, vars)? + }; + let events = Self::output_with_speed( + sub, + &SpeedOp::Fit(length), + &mult, + state, + cycle, + limit, + overlap, + vars, + )?; + channels.push(events) + } + Events::poly(channels, Fraction::ONE, Span::default()) + } } Step::Stack(st) => { if st.stack.is_empty() { @@ -1974,152 +2278,142 @@ impl Cycle { } else { let mut channels = Vec::with_capacity(st.stack.len()); for s in &st.stack { - channels.push(Self::output(s, state, cycle, limit, overlap)?) + channels.push(Self::output(s, state, cycle, limit, overlap, vars)?) } - Events::maybe_poly(PolyEvents { - span: Span::default(), - length: Fraction::ONE, - channels, - }) + Events::poly(channels, Fraction::ONE, Span::default()) } } Step::Degrade(d) => { - let mut out = Self::output(d.step.as_ref(), state, cycle, limit, overlap)?; + let chance = Self::output(d.chance.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_chance()); + + let mut out = Self::output(d.step.as_ref(), state, cycle, limit, overlap, vars)?; out.mutate_events(&mut |event: &mut Event| { - if let Some(chance) = d.chance.to_chance() { + if let Some(chance) = chance { if chance < state.rng.random_range(0.0..1.0) { - event.value = Value::Rest + event.value = Constant::Rest } } }); out } - Step::TargetExpression(e) => Self::output_with_target( - e.left.as_ref(), - e.right.as_ref(), + Step::Targeted(e) => Self::output_with_target(e, state, cycle, limit, overlap, vars)?, + Step::SpeedExpression(e) => Self::output_with_speed( + e.step.as_ref(), + &e.op, + e.mult.as_ref(), state, cycle, limit, overlap, + vars, )?, - Step::SpeedExpression(e) => { - Self::output_with_speed(e.right.as_ref(), step, state, cycle, limit, overlap)? - } Step::Bjorklund(b) => { let mut events = vec![]; - #[allow(clippy::single_match)] + + let steps = Self::output(b.steps.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0); + let pulses = Self::output(b.pulses.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0); + let rotation = { + if let Some(r) = &b.rotation { + Self::output(r.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0) + } else { + 0 + } + }; + // TODO support something other than Step::Single as the right hand side - match b.pulses.as_ref() { - Step::Single(pulses_single) => { - match b.steps.as_ref() { - Step::Single(steps_single) => { - let rotation = match &b.rotation { - None => None, - Some(r) => match r.as_ref() { - Step::Single(rotation_single) => { - rotation_single.value.to_integer() - } - _ => None, // TODO support something other than Step::Single as rotation - }, - }; - if let Some(steps) = steps_single.value.to_integer() { - if let Some(pulses) = pulses_single.value.to_integer() { - events.reserve(pulses as usize); - let out = Self::output( - b.left.as_ref(), - state, - cycle, - limit, - overlap, - )?; - for pulse in euclidean( - steps.max(0) as u32, - pulses.max(0) as u32, - rotation.unwrap_or(0), - ) { - if pulse { - events.push(out.clone()) - } else { - events.push(Events::empty()) - } - } - } - } - } - _ => (), // TODO support something other than Step::Single as steps - } + events.reserve(pulses as usize); + let out = Self::output(b.left.as_ref(), state, cycle, limit, overlap, vars)?; + for pulse in euclidean(steps.max(0) as u32, pulses.max(0) as u32, rotation) { + if pulse { + events.push(out.clone()) + } else { + events.push(Events::empty()) } - _ => (), // TODO support something other than Step::Single as pulses } + Events::subdivide_lengths(&mut events); - Events::Multi(MultiEvents { - span: Span::default(), - length: Fraction::ONE, - events, - }) + Events::multi(events, Fraction::ONE, Span::default()) } - Step::Static(_) => { - // Repeat only makes it here if it had no preceding value - // Range and Expression should be applied in Self::push_applied - Events::empty() + Step::Ranged(r) => { + let start = Self::output(r.start.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0); + let end = Self::output(r.end.as_ref(), state, cycle, limit, overlap, vars)? + .first() + .and_then(|e| e.value.to_integer()) + .unwrap_or(0); + + let length = (start - end).abs(); + let range = if start <= end { + Box::new(start..=end) as Box> + } else { + Box::new((end..=start).rev()) as Box> + }; + + // PERF cache this if the range is static + let step = Step::Weighted(Weighted { + weight: Box::new(Step::constant(Constant::Integer(length), None)), + step: Box::new(Step::Subdivision(Subdivision { + steps: { + let mut steps = vec![]; + for i in range { + steps.push(Step::Single(Single { + value: Constant::Integer(i), + string: Rc::from(i.to_string()), + })) + } + steps + }, + })), + }); + + Self::output(&step, state, cycle, limit, overlap, vars)? } + Step::Repeat => Events::empty(), }; Ok(events) } - #[cfg(test)] - fn indent_lines(level: usize) -> String { - let mut lines = String::new(); - for i in 0..level { - lines += [" │", " |"][i % 2]; - } - lines - } - #[cfg(test)] fn print_steps(step: &Step, level: usize) { let name = match step { + Step::Var(name) => format!("Var {name:?}"), Step::Single(s) => match &s.value { - Value::Pitch(_p) => format!("{:?} {}", s.value, s.string), + Constant::Pitch(_p) => format!("{:?} {}", s.value, s.string), _ => format!("{:?} {:?}", s.value, s.string), }, Step::Subdivision(sd) => format!("Subdivision [{}]", sd.steps.len()), - Step::Alternating(a) => format!("Alternating <{}>", a.steps.len()), - Step::Polymeter(pm) => format!("Polymeter {{{}}}", pm.length()), //, pm.count), + Step::Polymeter(pm) => format!("Polymeter {{{:?}}}", pm.count), Step::Choices(cs) => format!("Choices |{}|", cs.choices.len()), Step::Stack(st) => format!("Stack ({})", st.stack.len()), Step::SpeedExpression(e) => format!("Speed Expression {:?}", e.op), - Step::TargetExpression(_e) => String::from("Target Expression"), - Step::Static(s) => match s { - Static::Repeat => "Repeat".to_string(), - Static::Range(r) => format!("Range {}..{}", r.start, r.end), - Static::Expression(e) => { - format!("Static Expression {:?} : {:?}", e.op, e.right) - } - }, + Step::Weighted(we) => format!("Weight Expression {:?}", we.weight), + Step::Replicated(we) => format!("Replicate Expression {:?}", we.repeats), + Step::Targeted(e) => format!("Target Expression {:?}", e.kind), Step::Degrade(d) => format!("Degrade ? {:?}", d.chance), Step::Bjorklund(_b) => format!("Bjorklund {}", ""), + Step::Ranged(_) => "Ranged".to_string(), + Step::Repeat => "Repeat".to_string(), }; - println!("{} {}", Self::indent_lines(level), name); + println!("{} {}", indent_lines(level), name); for step in step.inner_steps() { Self::print_steps(step, level + 1) } } - #[cfg(test)] - fn print_pairs(pair: &Pair, level: usize) { - println!( - "{} {:?} {:?}", - Self::indent_lines(level), - pair.as_rule(), - pair.as_str() - ); - for p in pair.clone().into_inner() { - Self::print_pairs(&p, level + 1) - } - } - #[cfg(test)] fn print(&self) { Self::print_steps(&self.root, 0); @@ -2129,1022 +2423,13 @@ impl Cycle { // ------------------------------------------------------------------------------------------------- #[cfg(test)] -mod test { - use super::*; - - use pretty_assertions::assert_eq; - - fn assert_cycles(input: &str, outputs: Vec>>) -> Result<(), String> { - let mut cycle = Cycle::from(input)?; - for out in outputs { - assert_eq!(cycle.generate()?, out, "with input: '{}'", input); - } - Ok(()) - } - - fn assert_cycle_equality(a: &str, b: &str) -> Result<(), String> { - let seed = rand::rng().random(); - assert_eq!( - Cycle::from(a)?.with_seed(seed).generate()?, - Cycle::from(b)?.with_seed(seed).generate()?, - ); - Ok(()) - } - - fn assert_cycle_advancing(input: &str) -> Result<(), String> { - let seed = rand::rng().random(); - for number_of_runs in 1..9 { - let mut cycle1 = Cycle::from(input)?.with_seed(seed); - let mut cycle2 = Cycle::from(input)?.with_seed(seed); - for _ in 0..number_of_runs { - let _ = cycle1.generate()?; - cycle2.advance(); - } - assert_eq!(cycle1.generate()?, cycle2.generate()?); - } - Ok(()) - } - - #[test] - fn span() -> Result<(), String> { - assert!(Span::new(Fraction::new(0, 1), Fraction::new(1, 1)) - .includes(&Span::new(Fraction::new(1, 2), Fraction::new(2, 1)))); - Ok(()) - } - - #[test] - fn parse() -> Result<(), String> { - assert!(Cycle::from("a b c [d").is_err()); - assert!(Cycle::from("a b/ c [d").is_err()); - assert!(Cycle::from("a b--- c [d").is_err()); - assert!(Cycle::from("*a b c [d").is_err()); - assert!(Cycle::from("a {{{}}").is_err()); - assert!(Cycle::from("] a z [").is_err()); - assert!(Cycle::from("->err").is_err()); - assert!(Cycle::from("(a, b)").is_err()); - assert!(Cycle::from("#(12, 32)").is_err()); - assert!(Cycle::from("#c $").is_err()); - - assert!(Cycle::from("c44'mode").is_err()); - assert!(Cycle::from("c4'!mode").is_err()); - assert!(Cycle::from("y'mode").is_err()); - assert!(Cycle::from("c4'mo'de").is_err()); - assert!(Cycle::from("_names_cannot_start_with_underscore").is_err()); - - assert!(Cycle::from("c4'mode").is_ok()); - assert!(Cycle::from("c'm7#^-").is_ok()); - assert!(Cycle::from("[[[[[[[[]]]]]][[[[[]][[[]]]]]][[[][[[]]]]][[[[]]]]]]").is_ok()); - - Ok(()) - } - - #[test] - fn generate() -> Result<(), String> { - assert_eq!( - Cycle::from("[0x0] [0x1A] [0XA] [-0X5] [-0XA0] [-0Xaa]")?.generate()?, - [[ - Event::at(Fraction::new(0, 6), Fraction::new(1, 6)).with_int(0x0), - Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_int(0x1a), - Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_int(0xa), - Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_int(-0x5), - Event::at(Fraction::new(4, 6), Fraction::new(1, 6)).with_int(-0xa0), - Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_int(-0xaa), - ]] - ); - - assert_eq!( - Cycle::from("[0] [1] [1.01] [0.01] [0.] [.01]")?.generate()?, - [[ - Event::at(Fraction::new(0, 6), Fraction::new(1, 6)).with_int(0), - Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_int(1), - Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(1.01), - Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(0.01), - Event::at(Fraction::new(4, 6), Fraction::new(1, 6)).with_float(0.0), - Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_float(0.01), - ]] - ); - - let empty_events: Vec> = vec![]; - assert_eq!(Cycle::from("a*[]")?.generate()?, empty_events); - assert_eq!(Cycle::from("[c d]/0")?.generate()?, empty_events); - assert_eq!(Cycle::from("[c d]*0")?.generate()?, empty_events); - - assert!(Cycle::from("[c d]/1000000000000").is_err()); // too large for fraction - assert_eq!( - Cycle::from("[c d]/1000000")?.generate()?, - [[Event::at(Fraction::from(0), Fraction::new(1, 1)).with_note(0, 4)]] - ); - - assert_eq!( - Cycle::from("a b c d")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_note(9, 4), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_note(11, 4), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(0, 4), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(2, 4), - ]] - ); - assert_eq!( - Cycle::from("\ta\r\n\tb\nc\n d\n\n")?.generate()?, - Cycle::from("a b c d")?.generate()? - ); - assert_eq!( - Cycle::from("a b [ c d ]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 3)).with_note(9, 4), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_note(11, 4), - Event::at(Fraction::new(2, 3), Fraction::new(1, 6)).with_note(0, 4), - Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_note(2, 4), - ]] - ); - assert_eq!( - Cycle::from("[a a] [b4 b5 b6] [c0 d1 c2 d3]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 6)).with_note(9, 4), - Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_note(9, 4), - Event::at(Fraction::new(3, 9), Fraction::new(1, 9)).with_note(11, 4), - Event::at(Fraction::new(4, 9), Fraction::new(1, 9)).with_note(11, 5), - Event::at(Fraction::new(5, 9), Fraction::new(1, 9)).with_note(11, 6), - Event::at(Fraction::new(8, 12), Fraction::new(1, 12)).with_note(0, 0), - Event::at(Fraction::new(9, 12), Fraction::new(1, 12)).with_note(2, 1), - Event::at(Fraction::new(10, 12), Fraction::new(1, 12)).with_note(0, 2), - Event::at(Fraction::new(11, 12), Fraction::new(1, 12)).with_note(2, 3), - ]] - ); - assert_eq!( - Cycle::from("[a0 [bb1 [b2 c3]]] c#4 [[[d5 D#6] E7 ] F8]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 6)).with_note(9, 0), - Event::at(Fraction::new(1, 6), Fraction::new(1, 12)).with_note(10, 1), - Event::at(Fraction::new(3, 12), Fraction::new(1, 24)).with_note(11, 2), - Event::at(Fraction::new(7, 24), Fraction::new(1, 24)).with_note(0, 3), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_note(1, 4), - Event::at(Fraction::new(2, 3), Fraction::new(1, 24)).with_note(2, 5), - Event::at(Fraction::new(17, 24), Fraction::new(1, 24)).with_note(3, 6), - Event::at(Fraction::new(9, 12), Fraction::new(1, 12)).with_note(4, 7), - Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_note(5, 8), - ]] - ); - assert_eq!( - Cycle::from("[R [e [n o]]] , [[[i s] e ] _]")?.generate()?, - vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_name("R"), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)).with_note(4, 4), - Event::at(Fraction::new(3, 4), Fraction::new(1, 8)).with_name("n"), - Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_name("o"), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 8)).with_name("i"), - Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_name("s"), - Event::at(Fraction::new(1, 4), Fraction::new(3, 4)).with_note(4, 4), - ], - ] - ); - - assert_cycles( - "", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 4) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_note(11, 4) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_note(2, 4) - ]], - ], - )?; - - assert_cycles( - " >", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(9, 4), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(11, 4), - ]], - vec![vec![ - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(0, 4) - ]], - vec![vec![ - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(11, 4) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(9, 0), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(2, 4), - ]], - ], - )?; - - assert_cycles( - "< b, >", - vec![ - vec![ - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 4)], - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4)], - ], - vec![ - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(11, 4)], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(2, 4), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(4, 4), - ], - ], - vec![ - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 8)], - vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4)], - ], - ], - )?; - - assert_cycles( - "{-3 -2 -1 0 1 2 3}%4", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-3), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-2), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(-1), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(0), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(-3), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-2), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-1), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(0), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(1), - ]], - ], - )?; - - assert_cycles( - "{<0 0 d#8:test> 1 :0xB [<.5 0.95> 1.]}%3", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(0), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(1), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) - .with_note(0, 4) - .with_target(Target::from_index(0xB)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 6)).with_float(0.5), - Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_float(1.0), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 3)) - .with_note(2, 4) - .with_target(Target::from_index(0xB)), - Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(0.95), - Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(1.0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) - .with_note(3, 8) - .with_target(Target::from_name("test".into())), - ]], - ], - )?; - - assert_eq!( - Cycle::from("[1 middle _] {}%42 [] <>")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 12)).with_int(1), - Event::at(Fraction::new(1, 12), Fraction::new(1, 6)).with_name("middle"), - Event::at(Fraction::new(1, 4), Fraction::new(3, 4)), - ]] - ); - - assert_eq!( - Cycle::from("[1 __ 2] 3")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(3, 8)).with_int(1), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(2), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(3), - ]] - ); - - assert_cycles( - "", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_name("some_name") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_name("another_one") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "chord") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "-^7") - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_name("c6a_name") - ]], - ], - )?; - - assert_cycles( - "[1 2] [3 4,[5 6]:42]", - vec![vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(4), - ], - vec![ - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_int(5) - .with_target(Target::from_index(42)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(6) - .with_target(Target::from_index(42)), - ], - ]], - )?; - - assert_eq!( - Cycle::from("1 second*2 eb3*3 [32 32]*4")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_name("second"), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_name("second"), - Event::at(Fraction::new(6, 12), Fraction::new(1, 12)).with_note(3, 3), - Event::at(Fraction::new(7, 12), Fraction::new(1, 12)).with_note(3, 3), - Event::at(Fraction::new(8, 12), Fraction::new(1, 12)).with_note(3, 3), - Event::at(Fraction::new(24, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(25, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(26, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(27, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(28, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(29, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(30, 32), Fraction::new(1, 32)).with_int(32), - Event::at(Fraction::new(31, 32), Fraction::new(1, 32)).with_int(32), - ]] - ); - - assert_cycles( - "tresillo(6,8), outside(4,11)", - vec![vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(1, 8), Fraction::new(1, 8)), - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(4, 8), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(5, 8), Fraction::new(1, 8)), - Event::at(Fraction::new(6, 8), Fraction::new(1, 8)).with_name("tresillo"), - Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_name("tresillo"), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 11)).with_name("outside"), - Event::at(Fraction::new(1, 11), Fraction::new(2, 11)), - Event::at(Fraction::new(3, 11), Fraction::new(1, 11)).with_name("outside"), - Event::at(Fraction::new(4, 11), Fraction::new(2, 11)), - Event::at(Fraction::new(6, 11), Fraction::new(1, 11)).with_name("outside"), - Event::at(Fraction::new(7, 11), Fraction::new(2, 11)), - Event::at(Fraction::new(9, 11), Fraction::new(1, 11)).with_name("outside"), - Event::at(Fraction::new(10, 11), Fraction::new(1, 11)), - ], - ]], - )?; - - assert_cycles( - "[<1 10> <2 20>:a](2,5)", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(1), - Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) - .with_int(2) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), - Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(1), - Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) - .with_int(2) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(10), - Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) - .with_int(20) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), - Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(10), - Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) - .with_int(20) - .with_target(Target::from_name("a".into())), - Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), - ]], - ], - )?; - - assert_eq!( - Cycle::from("1!2 3 [4!3 5]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(1), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(12, 16), Fraction::new(1, 16)).with_int(4), - Event::at(Fraction::new(13, 16), Fraction::new(1, 16)).with_int(4), - Event::at(Fraction::new(14, 16), Fraction::new(1, 16)).with_int(4), - Event::at(Fraction::new(15, 16), Fraction::new(1, 16)).with_int(5), - ]] - ); - - assert_cycles( - "[0 1]!2 !2", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 8)).with_int(0), - Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_int(1), - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_int(0), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(1), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(9, 4), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(9, 4), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 8)).with_int(0), - Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_int(1), - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_int(0), - Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(1), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(11, 4), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(11, 4), - ]], - ], - )?; - - assert_cycles( - "[0 1]/2", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 1)).with_int(0) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 1)).with_int(1) - ]], - ], - )?; - - assert_cycles( - "[0 1]*2.5", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 5)).with_int(0), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)).with_int(1), - Event::at(Fraction::new(2, 5), Fraction::new(1, 5)).with_int(0), - Event::at(Fraction::new(3, 5), Fraction::new(1, 5)).with_int(1), - Event::at(Fraction::new(4, 5), Fraction::new(1, 5)).with_int(0), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 5)).with_int(1), - Event::at(Fraction::new(1, 5), Fraction::new(1, 5)).with_int(0), - Event::at(Fraction::new(2, 5), Fraction::new(1, 5)).with_int(1), - Event::at(Fraction::new(3, 5), Fraction::new(1, 5)).with_int(0), - Event::at(Fraction::new(4, 5), Fraction::new(1, 5)).with_int(1), - ]], - ], - )?; - - assert_eq!( - Cycle::from("a:1 b:target")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_name("target".into())) - ]] - ); - - assert_cycles( - "a:<1 2>", - vec![ - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(1))]], - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(2))]], - ], - )?; - - assert_cycles( - "a:1:2:Target", - vec![vec![vec![Event::at( - Fraction::from(0), - Fraction::new(1, 1), - ) - .with_note(9, 4) - .with_targets(vec![ - Target::from_index(1), - Target::from_name("Target".into()), - ])]]], - )?; - - assert_cycles( - "[a:1:2]:<3 4>", - vec![ - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(1))]], - vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) - .with_note(9, 4) - .with_target(Target::from_index(1))]], - ], - )?; - - // target expression preserves the structure from the left side - assert_eq!( - Cycle::from("[a b c d]:[1 2 3]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) - .with_note(0, 4) - .with_target(Target::from_index(2)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(2, 4) - .with_target(Target::from_index(3)), - ]] - ); - - // when using ~ as a target, it's possible selectively skip overriding the outer target from within - assert_cycles( - "[a [b:<~ 7> b:<8 9>]]:[1 [2 3], 4]", - vec![ - vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - // this iteration lets the outer context set the target - .with_target(Target::from_index(2)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(8)), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(4)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(4)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(8)), - ], - ], - vec![ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(7)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(9)), - ], - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(4)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(7)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_note(11, 4) - .with_target(Target::from_index(9)), - ], - ], - ], - )?; - - assert_cycles( - "[a b]:<1 target>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(1)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_name("target".into())), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_name("target".into())), - ]], - ], - )?; - - assert_cycles( - "[a:1 b]:<3 4>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(3)), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(4)), - ]], - ], - )?; - - assert_eq!( - Cycle::from("a:1 b:v0.1:v1.0:p1.0")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_targets(vec![ - Target::Named("v".into(), Some(0.1)), - // second v should not be applied - Target::Named("p".into(), Some(1.0)), - ]) - ]] - ); - - // outer instrument values shouldn't override inner ones - assert_eq!( - Cycle::from("[a:#2 b]:#3")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(2)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::from_index(3)), - ]] - ); - - assert_eq!( - Cycle::from("a:1:#1 b:#1:1")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_note(9, 4) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_note(11, 4) - .with_target(Target::Index(1)), - ]] - ); - - assert_eq!( - Cycle::from("c(3,8,9)")?.generate()?, - [[ - Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_note(0, 4), - Event::at(Fraction::new(3, 8), Fraction::new(1, 4)), - Event::at(Fraction::new(5, 8), Fraction::new(1, 8)).with_note(0, 4), - Event::at(Fraction::new(6, 8), Fraction::new(1, 8)), - Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_note(0, 4), - ]] - ); - - assert_cycle_equality("a? b?", "a?0.5 b?0.5")?; - assert_cycle_equality("[a b c](3,8,9)", "[a b c](3,8,1)")?; - assert_cycle_equality("[a b c](3,8,7)", "[a b c](3,8,-1)")?; - assert_cycle_equality("[a a a a]", "[a ! ! !]")?; - assert_cycle_equality("[! ! a !]", "[~ ~ a a]")?; - assert_cycle_equality("a ~ ~ ~", "a - - -")?; - assert_cycle_equality("[a b] ! ! !", "[a b] [a b] [a b] ")?; - assert_cycle_equality("{a b!2 c}%3", "{a b b c}%3")?; - assert_cycle_equality("a b, {c d e}%2", "{a b, c d e}")?; - assert_cycle_equality("0..3", "0 1 2 3")?; - assert_cycle_equality("-5..-8", "-5 -6 -7 -8")?; - assert_cycle_equality("a b . c d", "[a b] [c d]")?; - assert_cycle_equality( - "a b . c d e . f g h i [j k . l m]", - "[a b] [c d e] [f g h i [[j k] [l m]]]", - )?; - assert_cycle_equality( - "a b . c d e , f g h i . j k, l m", - "[a b] [c d e], [[f g h i] [j k]], [l m]", - )?; - assert_cycle_equality("{a b . c d . f g h}%2", "{[a b] [c d] [f g h]}%2")?; - assert_cycle_equality("", "<[a b] [c d] [f g h]>")?; - - assert_cycles( - "[0 1 2]/2", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(2, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - vec![vec![ - Event::at(Fraction::new(1, 3), Fraction::new(2, 3)).with_int(2) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(2, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - ], - )?; - - assert_cycles( - "0*<1 2>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::from(1)).with_int(0) - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(0), - ]], - ], - )?; - - assert_eq!( - Cycle::from("0*[4 3]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(0), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(0), - ]] - ); - - assert_cycles( - "{0 1 2 3}%<2 3>", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(1), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(3), - Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), - Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), - ]], - ], - )?; - - // TODO test random outputs // parse_with_debug("[a b c d]?0.5"); - - Ok(()) - } - - #[test] - fn expression_chains() -> Result<(), String> { - assert_cycle_equality("a*3/2", "a*1.5")?; - assert_cycle_equality("[a b c d]*2*4", "[a b c d]*8")?; - assert_cycle_equality("a/2/3/4/5", "a/120")?; - assert_cycle_equality( - "[a b c d e f]:[[v.2 v.5]*3]", - "[a b c d e f]:[v.2 v.5 v.2 v.5 v.2 v.5]", - )?; - assert_cycle_equality( - "[a:0 b:0 c:1 d:1 e:2 f:2 g:3 h:3]/2*4", - "[a b c d e f g h]:[0 1 2 3]/2*4", - )?; - assert_cycle_equality("[a:0 b:0 c:1 d:1]/2", "[a b c d]/2:<0 1>")?; - - assert_eq!( - Cycle::from("[0 1]*2:[1 2 3 4]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_int(0) - .with_target(Target::from_index(1)), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::from_index(2)), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) - .with_int(0) - .with_target(Target::from_index(3)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::from_index(4)), - ]] - ); - - assert_cycles( - "[a b c:v.2 d]:p.5:[v.1 v.3]:v.8/4", - vec![ - vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) - .with_note(9, 4) - .with_targets(vec![ - Target::Named("p".into(), Some(0.5)), - Target::Named("v".into(), Some(0.1)), - ])]], - vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) - .with_note(11, 4) - .with_targets(vec![ - Target::Named("p".into(), Some(0.5)), - Target::Named("v".into(), Some(0.1)), - ])]], - vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) - .with_note(0, 4) - .with_targets(vec![ - Target::Named("v".into(), Some(0.2)), - Target::Named("p".into(), Some(0.5)), - ])]], - vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) - .with_note(2, 4) - .with_targets(vec![ - Target::Named("p".into(), Some(0.5)), - Target::Named("v".into(), Some(0.3)), - ])]], - ], - )?; - Ok(()) - } - - #[test] - fn event_limit() -> Result<(), String> { - assert!(Cycle::from("[[a b c d]*100]*100")?.generate().is_err()); - assert!(Cycle::from("[[a b c d]*100]*100")? - .with_event_limit(0x10000) - .generate() - .is_ok()); - Ok(()) - } - - #[test] - fn stacks() -> Result<(), String> { - assert_eq!( - Cycle::from("bd [bd, cp], ~ hh")?.generate()?, - [ - vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)).with_name("bd"), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("bd") - ], - vec![Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("cp")], - vec![Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("hh")] - ] - ); - Ok(()) - } - - #[test] - fn advancing() -> Result<(), String> { - assert_cycle_advancing("[a b c d]")?; // stateless - assert_cycle_advancing("[a b], [c d]")?; - assert_cycle_advancing("{a b}%2 {a b}*5")?; // stateful - assert_cycle_advancing("[a b]*5 [a b]/5")?; - assert_cycle_advancing("[a b c d]")?; - assert_cycle_advancing("a ")?; - assert_cycle_advancing("[a b? c d]|[c? d?]")?; - assert_cycle_advancing("[{a b}/2 c d], e? {a b}*2")?; - Ok(()) - } - - #[test] - fn target_assign() -> Result<(), String> { - assert_cycle_equality( - "[a b c [d e f g]]:[v.5 v.3 v.2 v.1]:[p.5 p.25 p.1 p.9]", - "[a b c [d e f g]]:v=[.5 .3 .2 .1]:p=[.5 .25 .1 .9]", - )?; - assert_eq!( - Cycle::from("[1 2 3 4]:p=[.1 .2 .3 .4]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::Named("p".into(), Some(0.1))), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_int(2) - .with_target(Target::Named("p".into(), Some(0.2))), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) - .with_int(3) - .with_target(Target::Named("p".into(), Some(0.3))), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(4) - .with_target(Target::Named("p".into(), Some(0.4))), - ]] - ); - assert_eq!( - Cycle::from("[1 2 3 4]:#=[1 c4 3 0.2]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::Index(1)), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_int(2) - .with_target(Target::Index(48)), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) - .with_int(3) - .with_target(Target::Index(3)), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(4) - .with_target(Target::Index(0)), - ]] - ); - - assert_eq!( - Cycle::from("[1 2 3 4]:long=[1 _ ~ 0.2]")?.generate()?, - [[ - Event::at(Fraction::from(0), Fraction::new(1, 4)) - .with_int(1) - .with_target(Target::Named("long".into(), Some(1.0))), - Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) - .with_int(2) - .with_target(Target::Named("long".into(), Some(1.0))), - Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), - Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) - .with_int(4) - .with_target(Target::Named("long".into(), Some(0.2))), - ]] - ); - - assert_cycles( - "[1 2 3 4 5 6 7 8]/4:d=<.1 .2 .3 .4>:v=[<.3 .2 .1>*2/3]", - vec![ - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_int(1) - .with_targets(vec![ - Target::Named("d".into(), Some(0.1)), - Target::Named("v".into(), Some(0.3)), - ]), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_int(2) - .with_targets(vec![ - Target::Named("d".into(), Some(0.1)), - Target::Named("v".into(), Some(0.3)), - ]), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_int(3) - .with_targets(vec![ - Target::Named("d".into(), Some(0.2)), - Target::Named("v".into(), Some(0.3)), - ]), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_int(4) - .with_targets(vec![ - Target::Named("d".into(), Some(0.2)), - Target::Named("v".into(), Some(0.2)), - ]), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_int(5) - .with_targets(vec![ - Target::Named("d".into(), Some(0.3)), - Target::Named("v".into(), Some(0.2)), - ]), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_int(6) - .with_targets(vec![ - Target::Named("d".into(), Some(0.3)), - Target::Named("v".into(), Some(0.2)), - ]), - ]], - vec![vec![ - Event::at(Fraction::from(0), Fraction::new(1, 2)) - .with_int(7) - .with_targets(vec![ - Target::Named("d".into(), Some(0.4)), - Target::Named("v".into(), Some(0.1)), - ]), - Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) - .with_int(8) - .with_targets(vec![ - Target::Named("d".into(), Some(0.4)), - Target::Named("v".into(), Some(0.1)), - ]), - ]], - ], - )?; - - assert_cycle_equality( - "[1 2 3 4]:v=[0.2 0.3 0.4 p.8]", - "[1 2 3 4]:[v0.2 v0.3 v0.4 p.8]", - )?; - Ok(()) +fn indent_lines(level: usize) -> String { + let mut lines = String::new(); + for i in 0..level { + lines += [" │", " |"][i % 2]; } + lines } + +#[cfg(test)] +mod tests; diff --git a/src/tidal/cycle/tests.rs b/src/tidal/cycle/tests.rs new file mode 100644 index 00000000..ca6bbf10 --- /dev/null +++ b/src/tidal/cycle/tests.rs @@ -0,0 +1,1204 @@ +use rand::Rng; + +type Fraction = num_rational::Rational32; + +use super::*; + +use pretty_assertions::assert_eq; + +fn assert_cycles(input: &str, outputs: Vec>>) -> Result<(), String> { + let mut cycle = Cycle::from(input)?; + for out in outputs { + assert_eq!(cycle.generate()?, out, "with input: '{}'", input); + } + Ok(()) +} + +fn assert_cycle_equality(a: &str, b: &str) -> Result<(), String> { + let seed = rand::rng().random(); + assert_eq!( + Cycle::from(a)?.with_seed(seed).generate()?, + Cycle::from(b)?.with_seed(seed).generate()?, + ); + Ok(()) +} + +fn assert_cycle_advancing(input: &str) -> Result<(), String> { + let seed = rand::rng().random(); + for number_of_runs in 1..9 { + let mut cycle1 = Cycle::from(input)?.with_seed(seed); + let mut cycle2 = Cycle::from(input)?.with_seed(seed); + for _ in 0..number_of_runs { + let _ = cycle1.generate()?; + cycle2.advance(); + } + assert_eq!(cycle1.generate()?, cycle2.generate()?); + } + Ok(()) +} + +fn cycle_with_vars(input: &str, subcycles: Vec<(&str, &str)>) -> Result { + let mut cycle = Cycle::from(input)?; + for (name, input) in subcycles { + let subcycle = SubCycle::from(input)?; + cycle.set_var(name, subcycle); + } + Ok(cycle) +} + +#[test] +fn span() -> Result<(), String> { + assert!(Span::new(Fraction::new(0, 1), Fraction::new(1, 1)) + .includes(&Span::new(Fraction::new(1, 2), Fraction::new(2, 1)))); + Ok(()) +} + +#[test] +fn weight_and_replicate() -> Result<(), String> { + let mut cycle = Cycle::from("a!1.5 b")?; + let _events = cycle.generate()?; + Ok(()) +} + +#[test] +fn variables() -> Result<(), String> { + assert!(SubCycle::from("a b c d").is_ok()); + // subcycles cannot contain variables + assert!(SubCycle::from("[a b c d]*$mult").is_err()); + assert!(SubCycle::from("$note $note $note").is_err()); + assert!(SubCycle::from("a:p=<0.5 0.2 $right>").is_err()); + + assert_eq!( + cycle_with_vars("a b $note d", vec![("note", "e4")])?.generate(), + Cycle::from("a b e4 d")?.generate() + ); + + // unset variables convert into named + assert_eq!( + Cycle::from("a $note")?.generate(), + Cycle::from("a note")?.generate() + ); + + assert_eq!( + cycle_with_vars("a:$index", vec![("index", "12")])?.generate(), + Cycle::from("a:12")?.generate() + ); + + assert_eq!( + cycle_with_vars("a*$mult", vec![("mult", "1 2")])?.generate(), + Cycle::from("a*[1 2]")?.generate() + ); + + assert_eq!( + cycle_with_vars("[a b c d]:p=[$f1 $f2]", vec![("f1", "0.5"), ("f2", "0.9")])?.generate(), + Cycle::from("[a b c d]:p=[0.5 0.9]")?.generate() + ); + + assert_eq!( + cycle_with_vars("a*$mult", vec![("mult", "2.0")])?.generate(), + Cycle::from("a*2")?.generate() + ); + + assert_eq!( + cycle_with_vars("a@$length b", vec![("length", "3.0")])?.generate(), + Cycle::from("a@3 b")?.generate() + ); + + assert_eq!( + cycle_with_vars("a:p$float", vec![("float", "0.9")])?.generate(), + Cycle::from("a:p0.9")?.generate() + ); + + assert_eq!( + cycle_with_vars("[a b c d]:p=[$f1 $f2]", vec![("f1", "0.5"), ("f2", "0.9")])?.generate(), + Cycle::from("[a b c d]:p=[0.5 0.9]")?.generate() + ); + + assert_eq!( + cycle_with_vars("a@$length b", vec![("length", "3.0")])?.generate(), + Cycle::from("a _ _ b")?.generate() + ); + + assert_eq!( + cycle_with_vars("a!$repeat b", vec![("repeat", "3.0")])?.generate(), + Cycle::from("a a a b")?.generate() + ); + + assert_eq!( + cycle_with_vars( + "a*[[$int1 $int2!$repeat]*$mult]", + vec![ + ("int1", "1"), + ("int2", "2"), + ("repeat", "2.0"), + ("mult", "2.5") + ] + )? + .generate(), + Cycle::from("a*[[1 2 2]*2.5]")?.generate() + ); + + let mut cycle = cycle_with_vars("a!$repeat", vec![])?; + let outputs = ["a", "a a", "a a a", "a a a a"]; + for (i, o) in outputs.iter().enumerate() { + cycle.set_var("repeat", SubCycle::from(&(i + 1).to_string())?); + assert_eq!(cycle.generate(), Cycle::from(o)?.generate()); + } + + let mut cycle = cycle_with_vars("$note", vec![])?; + let mut static_cycle = Cycle::from("")?; + for note in ["a", "b", "c", "d"] { + cycle.set_var("note", SubCycle::from(note)?); + assert_eq!(cycle.generate(), static_cycle.generate()); + } + + Ok(()) +} + +#[test] +fn polymeter() -> Result<(), String> { + assert_eq!( + Cycle::from("{0 1 2, 0 1 2 3}")?.generate(), + Cycle::from("{0 1 2, 0 1 2 3}%3")?.generate(), + ); + + assert_eq!( + Cycle::from("{0 ! ! 1}%2")?.generate(), + Cycle::from("{0 0 0 1}%2")?.generate(), + ); + + assert_eq!( + Cycle::from("{0 1!2, 0 1 2 3}")?.generate(), + Cycle::from("{0 1 1, 0 1 2 3}%3")?.generate(), + ); + + assert_eq!( + Cycle::from("{a b c d}%3")?.generate(), + Cycle::from("[a b c d]*0.75")?.generate(), + ); + + assert_eq!( + Cycle::from("{a@2 ! c, d e f}")?.generate(), + Cycle::from("{a@2 a@2 c, d e f}")?.generate(), + ); + + assert_cycles( + "{-3 -2 -1 0 1 2 3}%4", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-3), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-2), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(-1), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(0), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(-3), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(-2), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(-1), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(0), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(1), + ]], + ], + )?; + + assert_cycle_equality("{a b!2 c}%3", "{a b b c}%3")?; + assert_cycle_equality("a b, {c d e}%2", "{a b, c d e}")?; + assert_cycle_equality("{a b . c d . f g h}%2", "{[a b] [c d] [f g h]}%2")?; + + assert_cycles( + "{0 1 2 3}%<2 3>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(1), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(3), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + ], + )?; + + Ok(()) +} + +#[test] +fn constant_literals() -> Result<(), String> { + assert_eq!( + Cycle::constant_from("c3")?, + Constant::Pitch(Pitch { note: 0, octave: 3 }) + ); + assert_eq!( + Cycle::constant_from("v0.5")?, + Constant::Target(Target::named_float("v", 0.5)) + ); + assert_eq!(Cycle::constant_from("1.0")?, Constant::Float(1.0)); + assert_eq!(Cycle::constant_from("3.75")?, Constant::Float(3.75)); + assert_eq!(Cycle::constant_from("8")?, Constant::Integer(8)); + assert_eq!(Cycle::constant_from("~")?, Constant::Rest); + assert_eq!(Cycle::constant_from("_")?, Constant::Hold); + assert_eq!(Cycle::constant_from("name")?, Constant::Name("name".into())); + + assert!(Cycle::constant_from("$param").is_err()); + assert!(Cycle::constant_from("[1 2 3]").is_err()); + assert!(Cycle::constant_from("<1 2 3>").is_err()); + assert!(Cycle::constant_from("{{a b c}}%2").is_err()); + assert!(Cycle::constant_from("2 _ 3").is_err()); + Ok(()) +} + +#[test] +fn parse() -> Result<(), String> { + assert!(Cycle::from("a b c [d").is_err()); + assert!(Cycle::from("a b/ c [d").is_err()); + assert!(Cycle::from("a b--- c [d").is_err()); + assert!(Cycle::from("*a b c [d").is_err()); + assert!(Cycle::from("a {{{}}").is_err()); + assert!(Cycle::from("] a z [").is_err()); + assert!(Cycle::from("->err").is_err()); + assert!(Cycle::from("(a, b)").is_err()); + assert!(Cycle::from("#(12, 32)").is_err()); + assert!(Cycle::from("#c $").is_err()); + + assert!(Cycle::from("c44'mode").is_err()); + assert!(Cycle::from("c4'!mode").is_err()); + assert!(Cycle::from("y'mode").is_err()); + assert!(Cycle::from("c4'mo'de").is_err()); + assert!(Cycle::from("_names_cannot_start_with_underscore").is_err()); + + assert!(Cycle::from("c4'mode").is_ok()); + assert!(Cycle::from("c'm7#^-").is_ok()); + assert!(Cycle::from("[[[[[[[[]]]]]][[[[[]][[[]]]]]][[[][[[]]]]][[[[]]]]]]").is_ok()); + + Ok(()) +} + +#[test] +fn targets() -> Result<(), String> { + assert_eq!( + Cycle::from("a:v=0.5")?.generate()?, + [[Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::named_float("v", 0.5))]] + ); + + assert_eq!( + Cycle::from("a:1 b:target")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_name("target".into())) + ]] + ); + + assert_cycles( + "a:<1 2>", + vec![ + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(1))]], + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(2))]], + ], + )?; + + assert_cycles( + "a:1:2:Target", + vec![vec![vec![Event::at( + Fraction::from(0), + Fraction::new(1, 1), + ) + .with_note(9, 4) + .with_targets(vec![ + Target::from_index(1), + Target::from_name("Target".into()), + ])]]], + )?; + + assert_cycles( + "[a:1:2]:<3 4>", + vec![ + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(1))]], + vec![vec![Event::at(Fraction::from(0), Fraction::new(1, 1)) + .with_note(9, 4) + .with_target(Target::from_index(1))]], + ], + )?; + + // target expression preserves the structure from the left side + assert_eq!( + Cycle::from("[a b c d]:[1 2 3]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) + .with_note(0, 4) + .with_target(Target::from_index(2)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(2, 4) + .with_target(Target::from_index(3)), + ]] + ); + + assert_cycles( + "{<0 0 d#8:test> 1 :0xB [<.5 0.95> 1.]}%3", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 3)).with_int(0), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(1), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) + .with_note(0, 4) + .with_target(Target::from_index(0xB)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 6)).with_float(0.5), + Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_float(1.0), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 3)) + .with_note(2, 4) + .with_target(Target::from_index(0xB)), + Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(0.95), + Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(1.0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)) + .with_note(3, 8) + .with_target(Target::from_name("test".into())), + ]], + ], + )?; + + assert_cycles( + "[1 2] [3 4,[5 6]:42]", + vec![vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(2), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_int(4), + ], + vec![ + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_int(5) + .with_target(Target::from_index(42)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(6) + .with_target(Target::from_index(42)), + ], + ]], + )?; + + assert_cycles( + "[<1 10> <2 20>:a](2,5)", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(1), + Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) + .with_int(2) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), + Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(1), + Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) + .with_int(2) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 10)).with_int(10), + Event::at(Fraction::new(1, 10), Fraction::new(1, 10)) + .with_int(20) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)), + Event::at(Fraction::new(2, 5), Fraction::new(1, 10)).with_int(10), + Event::at(Fraction::new(5, 10), Fraction::new(1, 10)) + .with_int(20) + .with_target(Target::from_name("a".into())), + Event::at(Fraction::new(3, 5), Fraction::new(2, 5)), + ]], + ], + )?; + + // when using ~ as a target, it's possible selectively skip overriding the outer target from within + assert_cycles( + "[a [b:<~ 7> b:<8 9>]]:[1 [2 3], 4]", + vec![ + vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + // this iteration lets the outer context set the target + .with_target(Target::from_index(2)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(8)), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(4)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(4)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(8)), + ], + ], + vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(7)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(9)), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(4)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(7)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_note(11, 4) + .with_target(Target::from_index(9)), + ], + ], + ], + )?; + + assert_cycles( + "[a b]:<1 target>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(1)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_name("target".into())), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_name("target".into())), + ]], + ], + )?; + + assert_cycle_equality( + "[1 2 3 4]:v=[0.2 0.3 0.4 p.8]", + "[1 2 3 4]:[v0.2 v0.3 v0.4 p.8]", + )?; + + assert_cycle_equality("[1 2 3 4]:g=[0.1 10.]", "[1 2 3 4]:[g0.1 g10.]")?; + + assert_cycles( + "[a:1 b]:<3 4>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(3)), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(4)), + ]], + ], + )?; + + assert_eq!( + Cycle::from("a:1 b:v0.1:v1.0:p1.0:g100.0")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_targets(vec![ + Target::named_float("v", 0.1), + // second v should not be applied + Target::named_float("p", 1.0), + Target::named_float("g", 100.0), + ]) + ]] + ); + + // outer instrument values shouldn't override inner ones + assert_eq!( + Cycle::from("[a:#2 b]:#3")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(2)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::from_index(3)), + ]] + ); + + assert_eq!( + Cycle::from("a:1:#1 b:#1:1")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_note(9, 4) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_note(11, 4) + .with_target(Target::Index(1)), + ]] + ); + + Ok(()) +} + +#[test] +fn generate() -> Result<(), String> { + assert_eq!( + Cycle::from("[0x0] [0x1A] [0XA] [-0X5] [-0XA0] [-0Xaa]")?.generate()?, + [[ + Event::at(Fraction::new(0, 6), Fraction::new(1, 6)).with_int(0x0), + Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_int(0x1a), + Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_int(0xa), + Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_int(-0x5), + Event::at(Fraction::new(4, 6), Fraction::new(1, 6)).with_int(-0xa0), + Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_int(-0xaa), + ]] + ); + + assert_eq!( + Cycle::from("[0] [1] [1.01] [0.01] [0.] [.01]")?.generate()?, + [[ + Event::at(Fraction::new(0, 6), Fraction::new(1, 6)).with_int(0), + Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_int(1), + Event::at(Fraction::new(2, 6), Fraction::new(1, 6)).with_float(1.01), + Event::at(Fraction::new(3, 6), Fraction::new(1, 6)).with_float(0.01), + Event::at(Fraction::new(4, 6), Fraction::new(1, 6)).with_float(0.0), + Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_float(0.01), + ]] + ); + + let empty_events: Vec> = vec![]; + assert_eq!(Cycle::from("a*[]")?.generate()?, empty_events); + assert_eq!(Cycle::from("[c d]/0")?.generate()?, empty_events); + assert_eq!(Cycle::from("[c d]*0")?.generate()?, empty_events); + + assert!(Cycle::from("[c d]/1000000000000").is_err()); // too large for fraction + assert_eq!( + Cycle::from("[c d]/1000000")?.generate()?, + [[Event::at(Fraction::from(0), Fraction::new(1, 1)).with_note(0, 4)]] + ); + + assert_eq!( + Cycle::from("a b c d")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_note(9, 4), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_note(11, 4), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(0, 4), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(2, 4), + ]] + ); + assert_eq!( + Cycle::from("\ta\r\n\tb\nc\n d\n\n")?.generate()?, + Cycle::from("a b c d")?.generate()? + ); + assert_eq!( + Cycle::from("a b [ c d ]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 3)).with_note(9, 4), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_note(11, 4), + Event::at(Fraction::new(2, 3), Fraction::new(1, 6)).with_note(0, 4), + Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_note(2, 4), + ]] + ); + assert_eq!( + Cycle::from("[a a] [b4 b5 b6] [c0 d1 c2 d3]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 6)).with_note(9, 4), + Event::at(Fraction::new(1, 6), Fraction::new(1, 6)).with_note(9, 4), + Event::at(Fraction::new(3, 9), Fraction::new(1, 9)).with_note(11, 4), + Event::at(Fraction::new(4, 9), Fraction::new(1, 9)).with_note(11, 5), + Event::at(Fraction::new(5, 9), Fraction::new(1, 9)).with_note(11, 6), + Event::at(Fraction::new(8, 12), Fraction::new(1, 12)).with_note(0, 0), + Event::at(Fraction::new(9, 12), Fraction::new(1, 12)).with_note(2, 1), + Event::at(Fraction::new(10, 12), Fraction::new(1, 12)).with_note(0, 2), + Event::at(Fraction::new(11, 12), Fraction::new(1, 12)).with_note(2, 3), + ]] + ); + assert_eq!( + Cycle::from("[a0 [bb1 [b2 c3]]] c#4 [[[d5 D#6] E7 ] F8]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 6)).with_note(9, 0), + Event::at(Fraction::new(1, 6), Fraction::new(1, 12)).with_note(10, 1), + Event::at(Fraction::new(3, 12), Fraction::new(1, 24)).with_note(11, 2), + Event::at(Fraction::new(7, 24), Fraction::new(1, 24)).with_note(0, 3), + Event::at(Fraction::new(1, 3), Fraction::new(1, 3)).with_note(1, 4), + Event::at(Fraction::new(2, 3), Fraction::new(1, 24)).with_note(2, 5), + Event::at(Fraction::new(17, 24), Fraction::new(1, 24)).with_note(3, 6), + Event::at(Fraction::new(9, 12), Fraction::new(1, 12)).with_note(4, 7), + Event::at(Fraction::new(5, 6), Fraction::new(1, 6)).with_note(5, 8), + ]] + ); + assert_eq!( + Cycle::from("[R [e [n o]]] , [[[i s] e ] _]")?.generate()?, + vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_name("R"), + Event::at(Fraction::new(1, 2), Fraction::new(1, 4)).with_note(4, 4), + Event::at(Fraction::new(3, 4), Fraction::new(1, 8)).with_name("n"), + Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_name("o"), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 8)).with_name("i"), + Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_name("s"), + Event::at(Fraction::new(1, 4), Fraction::new(3, 4)).with_note(4, 4), + ], + ] + ); + + assert_cycles( + "", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 4) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_note(11, 4) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_note(2, 4) + ]], + ], + )?; + + assert_cycles( + " >", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(9, 4), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(11, 4), + ]], + vec![vec![ + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(0, 4) + ]], + vec![vec![ + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(11, 4) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(9, 0), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(2, 4), + ]], + ], + )?; + + assert_cycles( + "< b, >", + vec![ + vec![ + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 4)], + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4)], + ], + vec![ + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(11, 4)], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_note(2, 4), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_note(4, 4), + ], + ], + vec![ + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(9, 8)], + vec![Event::at(Fraction::from(0), Fraction::from(1)).with_note(0, 4)], + ], + ], + )?; + + assert_eq!( + Cycle::from("[1 middle _] {}%42 [] <>")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 12)).with_int(1), + Event::at(Fraction::new(1, 12), Fraction::new(1, 6)).with_name("middle"), + Event::at(Fraction::new(1, 4), Fraction::new(3, 4)), + ]] + ); + + assert_eq!( + Cycle::from("[1 __ 2] 3")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(3, 8)).with_int(1), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(2), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(3), + ]] + ); + + assert_cycles( + "", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_name("some_name") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_name("another_one") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "chord") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_chord(0, 4, "-^7") + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_name("c6a_name") + ]], + ], + )?; + + assert_eq!( + Cycle::from("1 second*2 eb3*3 [32 32]*4")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_name("second"), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_name("second"), + Event::at(Fraction::new(6, 12), Fraction::new(1, 12)).with_note(3, 3), + Event::at(Fraction::new(7, 12), Fraction::new(1, 12)).with_note(3, 3), + Event::at(Fraction::new(8, 12), Fraction::new(1, 12)).with_note(3, 3), + Event::at(Fraction::new(24, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(25, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(26, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(27, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(28, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(29, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(30, 32), Fraction::new(1, 32)).with_int(32), + Event::at(Fraction::new(31, 32), Fraction::new(1, 32)).with_int(32), + ]] + ); + + assert_cycles( + "tresillo(6,8), outside(4,11)", + vec![vec![ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(1, 8), Fraction::new(1, 8)), + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(4, 8), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(5, 8), Fraction::new(1, 8)), + Event::at(Fraction::new(6, 8), Fraction::new(1, 8)).with_name("tresillo"), + Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_name("tresillo"), + ], + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 11)).with_name("outside"), + Event::at(Fraction::new(1, 11), Fraction::new(2, 11)), + Event::at(Fraction::new(3, 11), Fraction::new(1, 11)).with_name("outside"), + Event::at(Fraction::new(4, 11), Fraction::new(2, 11)), + Event::at(Fraction::new(6, 11), Fraction::new(1, 11)).with_name("outside"), + Event::at(Fraction::new(7, 11), Fraction::new(2, 11)), + Event::at(Fraction::new(9, 11), Fraction::new(1, 11)).with_name("outside"), + Event::at(Fraction::new(10, 11), Fraction::new(1, 11)), + ], + ]], + )?; + + assert_eq!( + Cycle::from("1!2 3 [4!3 5]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(1), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(12, 16), Fraction::new(1, 16)).with_int(4), + Event::at(Fraction::new(13, 16), Fraction::new(1, 16)).with_int(4), + Event::at(Fraction::new(14, 16), Fraction::new(1, 16)).with_int(4), + Event::at(Fraction::new(15, 16), Fraction::new(1, 16)).with_int(5), + ]] + ); + + assert_cycles( + "[0 1]!2 !2", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 8)).with_int(0), + Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_int(1), + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_int(0), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(1), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(9, 4), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(9, 4), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 8)).with_int(0), + Event::at(Fraction::new(1, 8), Fraction::new(1, 8)).with_int(1), + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_int(0), + Event::at(Fraction::new(3, 8), Fraction::new(1, 8)).with_int(1), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_note(11, 4), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)).with_note(11, 4), + ]], + ], + )?; + + assert_cycles( + "[0 1]/2", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 1)).with_int(0) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 1)).with_int(1) + ]], + ], + )?; + + assert_cycles( + "[0 1]*2.5", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 5)).with_int(0), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)).with_int(1), + Event::at(Fraction::new(2, 5), Fraction::new(1, 5)).with_int(0), + Event::at(Fraction::new(3, 5), Fraction::new(1, 5)).with_int(1), + Event::at(Fraction::new(4, 5), Fraction::new(1, 5)).with_int(0), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 5)).with_int(1), + Event::at(Fraction::new(1, 5), Fraction::new(1, 5)).with_int(0), + Event::at(Fraction::new(2, 5), Fraction::new(1, 5)).with_int(1), + Event::at(Fraction::new(3, 5), Fraction::new(1, 5)).with_int(0), + Event::at(Fraction::new(4, 5), Fraction::new(1, 5)).with_int(1), + ]], + ], + )?; + + assert_eq!( + Cycle::from("c(3,8,9)")?.generate()?, + [[ + Event::at(Fraction::new(2, 8), Fraction::new(1, 8)).with_note(0, 4), + Event::at(Fraction::new(3, 8), Fraction::new(1, 4)), + Event::at(Fraction::new(5, 8), Fraction::new(1, 8)).with_note(0, 4), + Event::at(Fraction::new(6, 8), Fraction::new(1, 8)), + Event::at(Fraction::new(7, 8), Fraction::new(1, 8)).with_note(0, 4), + ]] + ); + + assert_cycle_equality("a? b?", "a?0.5 b?0.5")?; + assert_cycle_equality("[a b c](3,8,9)", "[a b c](3,8,1)")?; + assert_cycle_equality("[a b c](3,8,7)", "[a b c](3,8,-1)")?; + assert_cycle_equality("[a a a a]", "[a ! ! !]")?; + assert_cycle_equality("[! ! a !]", "[~ ~ a a]")?; + assert_cycle_equality("a ~ ~ ~", "a - - -")?; + assert_cycle_equality("[a b] ! ! !", "[a b] [a b] [a b] ")?; + assert_cycle_equality("0..3", "0 1 2 3")?; + assert_cycle_equality("-5..-8", "-5 -6 -7 -8")?; + assert_cycle_equality("a b . c d", "[a b] [c d]")?; + assert_cycle_equality( + "a b . c d e . f g h i [j k . l m]", + "[a b] [c d e] [f g h i [[j k] [l m]]]", + )?; + assert_cycle_equality( + "a b . c d e , f g h i . j k, l m", + "[a b] [c d e], [[f g h i] [j k]], [l m]", + )?; + assert_cycle_equality("", "<[a b] [c d] [f g h]>")?; + + assert_cycles( + "[0 1 2]/2", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(2, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + vec![vec![ + Event::at(Fraction::new(1, 3), Fraction::new(2, 3)).with_int(2) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(2, 3)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(1), + ]], + ], + )?; + + assert_cycles( + "0*<1 2>", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::from(1)).with_int(0) + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_int(0), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_int(0), + ]], + ], + )?; + + assert_eq!( + Cycle::from("0*[4 3]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)).with_int(0), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)).with_int(0), + Event::at(Fraction::new(2, 3), Fraction::new(1, 3)).with_int(0), + ]] + ); + + // TODO test random outputs // parse_with_debug("[a b c d]?0.5"); + + Ok(()) +} + +#[test] +fn expression_chains() -> Result<(), String> { + assert_cycle_equality("a*3/2", "a*1.5")?; + assert_cycle_equality("[a b c d]*2*4", "[a b c d]*8")?; + assert_cycle_equality("a/2/3/4/5", "a/120")?; + assert_cycle_equality( + "[a b c d e f]:[[v.2 v.5]*3]", + "[a b c d e f]:[v.2 v.5 v.2 v.5 v.2 v.5]", + )?; + assert_cycle_equality( + "[a:0 b:0 c:1 d:1 e:2 f:2 g:3 h:3]/2*4", + "[a b c d e f g h]:[0 1 2 3]/2*4", + )?; + assert_cycle_equality("[a:0 b:0 c:1 d:1]/2", "[a b c d]/2:<0 1>")?; + + assert_eq!( + Cycle::from("[0 1]*2:[1 2 3 4]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_int(0) + .with_target(Target::from_index(1)), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::from_index(2)), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) + .with_int(0) + .with_target(Target::from_index(3)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::from_index(4)), + ]] + ); + + assert_cycles( + "[a b c:v.2 d]:p.5:[v.1 v.3]:v.8/4", + vec![ + vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) + .with_note(9, 4) + .with_targets(vec![ + Target::named_float("p", 0.5), + Target::named_float("v", 0.1), + ])]], + vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) + .with_note(11, 4) + .with_targets(vec![ + Target::named_float("p", 0.5), + Target::named_float("v", 0.1), + ])]], + vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) + .with_note(0, 4) + .with_targets(vec![ + Target::named_float("v", 0.2), + Target::named_float("p", 0.5), + ])]], + vec![vec![Event::at(Fraction::from(0), Fraction::from(1)) + .with_note(2, 4) + .with_targets(vec![ + Target::named_float("p", 0.5), + Target::named_float("v", 0.3), + ])]], + ], + )?; + Ok(()) +} + +#[test] +fn event_limit() -> Result<(), String> { + assert!(Cycle::from("[[a b c d]*100]*100")?.generate().is_err()); + assert!(Cycle::from("[[a b c d]*100]*100")? + .with_event_limit(0x10000) + .generate() + .is_ok()); + Ok(()) +} + +#[test] +fn stacks() -> Result<(), String> { + assert_eq!( + Cycle::from("bd [bd, cp], ~ hh")?.generate()?, + [ + vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)).with_name("bd"), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("bd") + ], + vec![Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("cp")], + vec![Event::at(Fraction::new(1, 2), Fraction::new(1, 2)).with_name("hh")] + ] + ); + Ok(()) +} + +#[test] +fn advancing() -> Result<(), String> { + assert_cycle_advancing("[a b c d]")?; // stateless + assert_cycle_advancing("[a b], [c d]")?; + assert_cycle_advancing("{a b}%2 {a b}*5")?; // stateful + assert_cycle_advancing("[a b]*5 [a b]/5")?; + assert_cycle_advancing("[a b c d]")?; + assert_cycle_advancing("a ")?; + assert_cycle_advancing("[a b? c d]|[c? d?]")?; + assert_cycle_advancing("[{a b}/2 c d], e? {a b}*2")?; + Ok(()) +} + +#[test] +fn target_assign() -> Result<(), String> { + assert_cycle_equality( + "[a b c [d e f g]]:[v.5 v.3 v.2 v.1]:[p.5 p.25 p.1 p.9]", + "[a b c [d e f g]]:v=[.5 .3 .2 .1]:p=[.5 .25 .1 .9]", + )?; + assert_eq!( + Cycle::from("[1 2 3 4]:p=[.1 .2 .3 .4]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::named_float("p", 0.1)), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_int(2) + .with_target(Target::named_float("p", 0.2)), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) + .with_int(3) + .with_target(Target::named_float("p", 0.3)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(4) + .with_target(Target::named_float("p", 0.4)), + ]] + ); + assert_eq!( + Cycle::from("[1 2 3 4]:#=[1 c4 3 0.2]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::Index(1)), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_int(2) + .with_target(Target::Index(48)), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)) + .with_int(3) + .with_target(Target::Index(3)), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(4) + .with_target(Target::Index(0)), + ]] + ); + + assert_eq!( + Cycle::from("[1 2 3 4]:long=[1 _ ~ 0.2]")?.generate()?, + [[ + Event::at(Fraction::from(0), Fraction::new(1, 4)) + .with_int(1) + .with_target(Target::named_float("long", 1.0)), + Event::at(Fraction::new(1, 4), Fraction::new(1, 4)) + .with_int(2) + .with_target(Target::named_float("long", 1.0)), + Event::at(Fraction::new(2, 4), Fraction::new(1, 4)).with_int(3), + Event::at(Fraction::new(3, 4), Fraction::new(1, 4)) + .with_int(4) + .with_target(Target::named_float("long", 0.2)), + ]] + ); + + assert_cycles( + "[1 2 3 4 5 6 7 8]/4:d=<.1 .2 .3 .4>:v=[<.3 .2 .1>*2/3]", + vec![ + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_int(1) + .with_targets(vec![ + Target::named_float("d", 0.1), + Target::named_float("v", 0.3), + ]), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_int(2) + .with_targets(vec![ + Target::named_float("d", 0.1), + Target::named_float("v", 0.3), + ]), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_int(3) + .with_targets(vec![ + Target::named_float("d", 0.2), + Target::named_float("v", 0.3), + ]), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_int(4) + .with_targets(vec![ + Target::named_float("d", 0.2), + Target::named_float("v", 0.2), + ]), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_int(5) + .with_targets(vec![ + Target::named_float("d", 0.3), + Target::named_float("v", 0.2), + ]), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_int(6) + .with_targets(vec![ + Target::named_float("d", 0.3), + Target::named_float("v", 0.2), + ]), + ]], + vec![vec![ + Event::at(Fraction::from(0), Fraction::new(1, 2)) + .with_int(7) + .with_targets(vec![ + Target::named_float("d", 0.4), + Target::named_float("v", 0.1), + ]), + Event::at(Fraction::new(1, 2), Fraction::new(1, 2)) + .with_int(8) + .with_targets(vec![ + Target::named_float("d", 0.4), + Target::named_float("v", 0.1), + ]), + ]], + ], + )?; + + Ok(()) +} diff --git a/types/pattrns/library/note.lua b/types/pattrns/library/note.lua index db5bdcfc..8b501f9a 100644 --- a/types/pattrns/library/note.lua +++ b/types/pattrns/library/note.lua @@ -80,6 +80,12 @@ function Note:panning(panning) end ---@nodiscard function Note:delay(delay) end +---Set the note's glide attribute to the specified value or values. +---@param glide number|number[] +---@return Note +---@nodiscard +function Note:glide(glide) end + ---------------------------------------------------------------------------------------------------- ---@alias NoteValue Note|NoteTable|string|number|nil @@ -91,9 +97,10 @@ function Note:delay(delay) end --- attributes: ---```md --- -'#' -> instrument (integer > 0) ---- -'v' -> volume (number in range [0-1]) ---- -'p' -> panning (number in range [-1-1]) ---- -'d' -> delay (number in range [0-1]) +--- -'g' -> glide (number in range [0-INF] - glide step duration) +--- -'v' -> volume (number in range [0-1] - zero to full volume) +--- -'p' -> panning (number in range [-1-1] - full left to full right) +--- -'d' -> delay (number in range [0-1] - delay in units) ---``` --- ---### examples: diff --git a/types/pattrns/library/sequence.lua b/types/pattrns/library/sequence.lua index 599dcd56..097a093a 100644 --- a/types/pattrns/library/sequence.lua +++ b/types/pattrns/library/sequence.lua @@ -69,6 +69,12 @@ function Sequence:panning(panning) end ---@nodiscard function Sequence:delay(delay) end +---Set the glide attribute of all notes to the specified value or values. +---@param glide number|number[] +---@return Sequence +---@nodiscard +function Sequence:glide(glide) end + ---------------------------------------------------------------------------------------------------- ---Create a sequence from an array of note values or note value varargs. @@ -82,6 +88,10 @@ function Sequence:delay(delay) end ---sequence(48, "c5", {}) ----- sequence of a +5 transposed C4 and G4 major chord ---sequence("c4'maj", "g4'maj"):transpose(5) +----- glide from c4 to e4 +---sequence{"c4", "e4 g1.0"} +----- glide from c4 to e4 in half of the step time +---sequence{"c4", "e4 g0.5"} --- ``` ---@param ... NoteValue|Note ---@return Sequence