From 48741e79b255e5b4d5deb908169515834637f780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20M=C3=BCller?= Date: Fri, 14 Jun 2024 13:04:08 +0200 Subject: [PATCH 1/4] allow mapping cycle identifier's to note stacks instead of just single notes --- examples/play.rs | 5 +- src/bindings.rs | 3 +- src/bindings/cycle.rs | 12 ++--- src/event/cycle.rs | 93 +++++++++++++++++++---------------- src/event/scripted_cycle.rs | 42 ++++++++-------- types/nerdo/library/cycle.lua | 20 +++++--- 6 files changed, 97 insertions(+), 78 deletions(-) diff --git a/examples/play.rs b/examples/play.rs index 868a3be..1b39f7b 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -73,7 +73,10 @@ fn main() -> Result<(), Box> { // generate a few phrases let cycle = new_cycle_event("bd [~ bd] ~ ~ bd [~ bd] _ ~ bd [~ bd] ~ ~ bd [~ bd] [_ bd2] [~ bd _ ~]")? - .with_mappings(&[("bd", new_note("c4")), ("bd2", new_note(("c4", None, 0.5)))]); + .with_mappings(&[ + ("bd", vec![new_note("c4")]), + ("bd2", vec![new_note(("c4", None, 0.5))]), + ]); let kick_pattern = beat_time .every_nth_beat(16.0) diff --git a/src/bindings.rs b/src/bindings.rs index 2ae2ed0..658a103 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -45,8 +45,7 @@ pub use callback::{ pub(crate) use callback::LuaCallback; pub(crate) use timeout::LuaTimeoutHook; pub(crate) use unwrap::{ - gate_trigger_from_value, note_event_from_value, note_events_from_value, - pattern_pulse_from_value, + gate_trigger_from_value, note_events_from_value, pattern_pulse_from_value, }; // --------------------------------------------------------------------------------------------- diff --git a/src/bindings/cycle.rs b/src/bindings/cycle.rs index a7ff7a6..b6b9aa8 100644 --- a/src/bindings/cycle.rs +++ b/src/bindings/cycle.rs @@ -2,7 +2,7 @@ use mlua::prelude::*; use crate::{event::NoteEvent, tidal::Cycle}; -use super::unwrap::{bad_argument_error, note_event_from_value}; +use super::unwrap::{bad_argument_error, note_events_from_value}; // --------------------------------------------------------------------------------------------- @@ -10,7 +10,7 @@ use super::unwrap::{bad_argument_error, note_event_from_value}; #[derive(Clone, Debug)] pub struct CycleUserData { pub cycle: Cycle, - pub mappings: Vec<(String, Option)>, + pub mappings: Vec<(String, Vec>)>, pub mapping_function: Option, } @@ -47,7 +47,7 @@ impl LuaUserData for CycleUserData { let cycle = this.cycle.clone(); let mut mappings = Vec::new(); for (k, v) in table.pairs::().flatten() { - mappings.push((k.to_string()?, note_event_from_value(&v, None)?)); + mappings.push((k.to_string()?, note_events_from_value(&v, None)?)); } let mapping_function = None; Ok(CycleUserData { @@ -136,9 +136,9 @@ mod test { .into_iter() .collect::>(), HashMap::from([ - ("a".to_string(), new_note(Note::C0)), - ("b".to_string(), new_note(Note::C4)), - ("c".to_string(), new_note(Note::C6)), + ("a".to_string(), vec![new_note(Note::C0)]), + ("b".to_string(), vec![new_note(Note::C4)]), + ("c".to_string(), vec![new_note(Note::C6)]), ]) ); diff --git a/src/event/cycle.rs b/src/event/cycle.rs index 9d223e5..926a725 100644 --- a/src/event/cycle.rs +++ b/src/event/cycle.rs @@ -45,53 +45,52 @@ impl From<&CycleTarget> for Option { /// Helper struct to convert time tagged events from Cycle into a `Vec` pub(crate) struct CycleNoteEvents { - events: Vec<(Fraction, Fraction, Vec>)>, + events: Vec<(Fraction, Fraction, Vec>)>, } impl CycleNoteEvents { /// Create a new, empty list of events. pub fn new() -> Self { - Self { events: vec![] } + Self { events: Vec::new() } } - /// Add a new cycle channel item. + /// Add note events from a cycle channel event. pub fn add( &mut self, channel: usize, start: Fraction, length: Fraction, - note_event: NoteEvent, + note_events: Vec>, ) { match self .events .binary_search_by(|(time, _, _)| time.cmp(&start)) { Ok(pos) => { - // use max length of all notes in stack - let note_length = &mut self.events[pos].1; - *note_length = (*note_length).max(length); - // add note to existing time stack - let note_events = &mut self.events[pos].2; - note_events.resize(channel + 1, None); - note_events[channel] = Some(note_event); + // use min length of all notes in stack + let event_length = &mut self.events[pos].1; + *event_length = (*event_length).min(length); + // add new notes to existing event stack + let timed_event = &mut self.events[pos].2; + timed_event.resize(channel + 1, None); + timed_event[channel] = Some(Event::NoteEvents(note_events)); } - Err(pos) => self - .events - .insert(pos, (start, length, vec![Some(note_event)])), + Err(pos) => self.events.insert( + pos, + (start, length, vec![Some(Event::NoteEvents(note_events))]), + ), } } - /// Convert to a list of NoteEvents. + /// Convert to a list of EventIterItems. pub fn into_event_iter_items(self) -> Vec { - let mut events: Vec = Vec::with_capacity(self.events.len()); - for (start_time, length, note_events) in self.events.into_iter() { - events.push(EventIterItem::new_with_fraction( - Event::NoteEvents(note_events), - start_time, - length, - )); + let mut event_iter_items: Vec = Vec::with_capacity(self.events.len()); + for (start_time, length, events) in self.events.into_iter() { + for event in events.into_iter().flatten() { + event_iter_items.push(EventIterItem::new_with_fraction(event, start_time, length)); + } } - events + event_iter_items } } @@ -106,7 +105,7 @@ impl CycleNoteEvents { #[derive(Clone, Debug)] pub struct CycleEventIter { cycle: Cycle, - mappings: HashMap>, + mappings: HashMap>>, } impl CycleEventIter { @@ -132,7 +131,10 @@ impl CycleEventIter { } /// Return a new cycle with the given value mappings applied. - pub fn with_mappings + Clone>(self, map: &[(S, Option)]) -> Self { + pub fn with_mappings + Clone>( + self, + map: &[(S, Vec>)], + ) -> Self { let mut mappings = HashMap::new(); for (k, v) in map.iter().cloned() { mappings.insert(k.into(), v); @@ -141,23 +143,25 @@ impl CycleEventIter { } /// Generate a note event from a single cycle event, applying mappings if necessary - fn note_event(&mut self, event: CycleEvent) -> Option { - let mut note_event = { - if let Some(mapped_note_event) = self.mappings.get(event.string()) { - // apply custom note mapping - mapped_note_event.clone() + fn note_events(&mut self, event: CycleEvent) -> Result>, String> { + let mut note_events = { + if let Some(note_events) = self.mappings.get(event.string()) { + // apply custom note mappings + note_events.clone() } else { - // else try to convert value to a note - event.value().into() + // convert the cycle value to a single note + vec![event.value().into()] } }; // inject target instrument, if present if let Some(instrument) = event.target().into() { - if let Some(note_event) = &mut note_event { - note_event.instrument = Some(instrument); + for mut note_event in &mut note_events { + if let Some(note_event) = &mut note_event { + note_event.instrument = Some(instrument); + } } } - note_event + Ok(note_events) } /// Generate next batch of events from the next cycle run. @@ -168,7 +172,7 @@ impl CycleEventIter { match self.cycle.generate() { Ok(events) => events, Err(err) => { - // NB: only expected error here is exceeding the event limit + // NB: only expected error here is exceeding the event limit panic!("Cycle runtime error: {err}"); } } @@ -179,8 +183,16 @@ impl CycleEventIter { for event in channel_events.into_iter() { let start = event.span().start(); let length = event.span().length(); - if let Some(note_event) = self.note_event(event) { - timed_note_events.add(channel_index, start, length, note_event); + match self.note_events(event) { + Ok(note_events) => { + if !note_events.is_empty() { + timed_note_events.add(channel_index, start, length, note_events); + } + } + Err(err) => { + // NB: only expected error here is a chord parser error + panic!("Cycle runtime error: {err}"); + } } } } @@ -221,9 +233,6 @@ pub fn new_cycle_event(input: &str) -> Result { CycleEventIter::from_mini(input) } -pub fn new_cycle_event_with_seed( - input: &str, - seed: [u8; 32], -) -> Result { +pub fn new_cycle_event_with_seed(input: &str, seed: [u8; 32]) -> Result { CycleEventIter::from_mini_with_seed(input, seed) } diff --git a/src/event/scripted_cycle.rs b/src/event/scripted_cycle.rs index 9332468..2a5f2e0 100644 --- a/src/event/scripted_cycle.rs +++ b/src/event/scripted_cycle.rs @@ -4,7 +4,7 @@ use fraction::ToPrimitive; use mlua::prelude::*; use crate::{ - bindings::{add_lua_callback_error, note_event_from_value, LuaCallback, LuaTimeoutHook}, + bindings::{add_lua_callback_error, note_events_from_value, LuaCallback, LuaTimeoutHook}, event::{cycle::CycleNoteEvents, EventIter, EventIterItem, NoteEvent}, BeatTimeBase, PulseIterItem, }; @@ -23,7 +23,7 @@ use crate::tidal::{Cycle, Event as CycleEvent, Value as CycleValue}; #[derive(Clone, Debug)] pub struct ScriptedCycleEventIter { cycle: Cycle, - mappings: HashMap>, + mappings: HashMap>>, mapping_callback: Option, timeout_hook: Option, channel_steps: Vec, @@ -31,7 +31,7 @@ pub struct ScriptedCycleEventIter { impl ScriptedCycleEventIter { /// Return a new cycle with the given value mappings applied. - pub fn with_mappings(cycle: Cycle, mappings: Vec<(String, Option)>) -> Self { + pub fn with_mappings(cycle: Cycle, mappings: Vec<(String, Vec>)>) -> Self { let mappings = mappings.into_iter().collect(); let mapping_callback = None; let timeout_hook = None; @@ -72,15 +72,15 @@ impl ScriptedCycleEventIter { }) } - /// Generate a note event from a single cycle event, applying mappings if necessary - fn note_event( + /// Generate a note event stack from a single cycle event, applying mappings if necessary + fn note_events( &mut self, channel_index: usize, _event_index: usize, event_length: f64, event: CycleEvent, - ) -> LuaResult> { - let mut note_event = { + ) -> LuaResult>> { + let mut note_events = { if let Some(mapping_callback) = self.mapping_callback.as_mut() { // increase step counter if self.channel_steps.len() <= channel_index { @@ -96,17 +96,17 @@ impl ScriptedCycleEventIter { )?; // call mapping function let result = mapping_callback.call_with_arg(event.string())?; - note_event_from_value(&result, None)? - } else if let Some(mapped_note_event) = self.mappings.get(event.string()) { + note_events_from_value(&result, None)? + } else if let Some(note_events) = self.mappings.get(event.string()) { // apply custom note mapping - mapped_note_event.clone() + note_events.clone() } else { - // else try to convert value to a note - event.value().into() + // convert the cycle value to a single note + vec![event.value().into()] } }; // verify that all identifiers are mapped - if note_event.is_none() + if (note_events.is_empty() || note_events.iter().all(|f| f.is_none())) && self.mapping_callback.is_none() && !matches!(event.value(), CycleValue::Rest | CycleValue::Hold) { @@ -117,11 +117,13 @@ impl ScriptedCycleEventIter { } // inject target instrument, if present if let Some(instrument) = event.target().into() { - if let Some(note_event) = &mut note_event { - note_event.instrument = Some(instrument); + for mut note_event in &mut note_events { + if let Some(note_event) = &mut note_event { + note_event.instrument = Some(instrument); + } } } - Ok(note_event) + Ok(note_events) } /// Generate next batch of events from the next cycle run. @@ -149,7 +151,7 @@ impl ScriptedCycleEventIter { let start = event.span().start(); let length = event.span().length(); let event_length = length.to_f64().unwrap_or_default(); - match self.note_event(channel_index, event_index, event_length, event) { + match self.note_events(channel_index, event_index, event_length, event) { Err(err) => { if let Some(callback) = &self.mapping_callback { callback.handle_error(&err) @@ -157,9 +159,9 @@ impl ScriptedCycleEventIter { add_lua_callback_error("map", &err) } } - Ok(note_event) => { - if let Some(note_event) = note_event { - timed_note_events.add(channel_index, start, length, note_event); + Ok(note_events) => { + if !note_events.is_empty() { + timed_note_events.add(channel_index, start, length, note_events); } } } diff --git a/types/nerdo/library/cycle.lua b/types/nerdo/library/cycle.lua index 5c5ed06..e34ab15 100644 --- a/types/nerdo/library/cycle.lua +++ b/types/nerdo/library/cycle.lua @@ -25,22 +25,21 @@ local Cycle = {} ---------------------------------------------------------------------------------------------------- ----@alias MapFunction fun(context: CycleMapContext, value: string):NoteValue ----@alias MapGenerator fun(context: CycleMapContext, value: string):MapFunction +---@alias CycleMapNoteValue NoteValue|(NoteValue[])|Note +---@alias CycleMapFunction fun(context: CycleMapContext, value: string):CycleMapNoteValue +---@alias CycleMapGenerator fun(context: CycleMapContext, value: string):CycleMapFunction ---Map names in in the cycle to custom note events. --- ---By default, strings in cycles are interpreted as notes, and integer values as MIDI note ---values. Custom identifiers such as "bd" are undefined and will result into a rest, when ---they are not mapped explicitely. ---- ----Chords such as "c4'major" are not (yet) supported as values. ----@param map { [string]: NoteValue } +---@param map { [string]: CycleMapNoteValue } ---@return Cycle ---### examples: ---```lua -----Using a fixed mapping table ----cycle("bd [bd sn]"):map({ +---cycle("bd [bd, sn]"):map({ --- bd = "c4", --- sn = "e4 #1 v0.2" ---}) @@ -63,8 +62,15 @@ local Cycle = {} --- return { key = note + octave * 12 } --- end ---end) +-----Using a dynamic map function to map values to chord degrees +---cycle("1 5 1 [6|7]"):map(function(context) +--- local cmin = scale("c", "minor") +--- return function(context, value) +--- return note(cmin:chord(tonumber(value))) +--- end +---end) ---``` ----@overload fun(self, function: MapFunction|MapGenerator) +---@overload fun(self, function: CycleMapFunction|CycleMapGenerator) function Cycle:map(map) end ---------------------------------------------------------------------------------------------------- From b3990c27ecff6f7d2f6cfede7673440cba901c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20M=C3=BCller?= Date: Fri, 14 Jun 2024 15:13:32 +0200 Subject: [PATCH 2/4] add new Value::Chord type to Cycle --- src/tidal/cycle.pest | 12 +++++--- src/tidal/cycle.rs | 72 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/tidal/cycle.pest b/src/tidal/cycle.pest index 1728d93..5d7ea2f 100644 --- a/src/tidal/cycle.pest +++ b/src/tidal/cycle.pest @@ -16,13 +16,17 @@ number = ${ (normal | float | integer) ~ !(ASCII_ALPHA) } octave = { "10" | ASCII_DIGIT } mark = { "#"|"b" } note = ${ (^"a"|^"b"|^"c"|^"d"|^"e"|^"f"|^"g") } -pitch = ${ note ~ mark? ~ octave? ~ !(ASCII_ALPHANUMERIC)} +pitch = ${ note ~ mark? ~ octave? ~ !name} + +/// chord as pitch with mode string, separated via "'" +mode = ${ (ASCII_ALPHANUMERIC | "#" | "-" | "+" | "\u{0394}")+ } +chord = ${ pitch ~ "'" ~ mode } /// type for empty steps -rest = @{ ("~" | "-") ~ !(ASCII_ALPHANUMERIC) } +rest = @{ ("~" | "-") ~ !name } /// type for held steps -hold = @{ "_" ~ !(ASCII_ALPHANUMERIC) } +hold = @{ "_" ~ !name } /// arbitrary string identifier type name = @{ (ASCII_ALPHANUMERIC | "_")+ } @@ -30,7 +34,7 @@ name = @{ (ASCII_ALPHANUMERIC | "_")+ } repeat = { "!" } /// possible literals for single steps -single = { hold | rest | number | pitch | name } +single = { hold | rest | number | chord | pitch | name } /// groups subdivision = { "[" ~ (stack | split | choices | section)? ~ "]" } diff --git a/src/tidal/cycle.rs b/src/tidal/cycle.rs index 7180ee5..02caab0 100644 --- a/src/tidal/cycle.rs +++ b/src/tidal/cycle.rs @@ -189,6 +189,7 @@ pub enum Value { Float(f64), Integer(i32), Pitch(Pitch), + Chord(Pitch, String), Name(String), } @@ -479,20 +480,22 @@ impl Value { match &self { Value::Rest => Target::None, Value::Hold => Target::None, - Value::Name(n) => Target::Name(n.clone()), Value::Integer(i) => Target::Index(*i), Value::Float(f) => Target::Index(*f as i32), Value::Pitch(p) => Target::Name(format!("{:?}", p)), // TODO might not be the best conversion idea + Value::Chord(p, m) => Target::Name(format!("{:?}'{}", p, m)), + Value::Name(n) => Target::Name(n.clone()), } } fn to_integer(&self) -> Option { match &self { Value::Rest => None, Value::Hold => None, - Value::Name(_n) => None, Value::Integer(i) => Some(*i), Value::Float(f) => Some(*f as i32), - Value::Pitch(n) => Some(n.note as i32), + Value::Pitch(n) => Some(n.midi_note() as i32), + Value::Chord(p, _m) => Some(p.midi_note() as i32), + Value::Name(_n) => None, } } @@ -500,10 +503,11 @@ impl Value { match &self { Value::Rest => None, Value::Hold => None, - Value::Name(_n) => None, Value::Integer(i) => Some(*i as f64), Value::Float(f) => Some(*f), - Value::Pitch(n) => Some(n.note as f64), + Value::Pitch(n) => Some(n.midi_note() as f64), + Value::Chord(n, _m) => Some(n.midi_note() as f64), + Value::Name(_n) => None, } } @@ -511,10 +515,11 @@ impl Value { match &self { Value::Rest => None, Value::Hold => None, - Value::Name(_n) => 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(n) => Some((n.note as f64).clamp(0.0, 128.0) / 128.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::Name(_n) => None, } } } @@ -616,6 +621,16 @@ impl Event { } } + #[cfg(test)] + fn with_chord(&self, note: u8, octave: u8, mode: &str) -> Self { + let pitch = Pitch { note, octave }; + Self { + value: Value::Chord(pitch.clone(), mode.to_string()), + string: format!("{}'{}", pitch, mode), + ..self.clone() + } + } + #[cfg(test)] fn with_int(&self, i: i32) -> Self { Self { @@ -1021,6 +1036,22 @@ impl CycleParser { 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, mode.to_string())) + } Rule::name => Ok(Value::Name(pair.as_str().to_string())), _ => Err(format!("unrecognized pair in single\n{:?}", pair)), } @@ -1835,6 +1866,33 @@ mod test { ]] ); + 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("c4'mode").is_ok()); + assert!(Cycle::from("c'm7#\u{0394}").is_ok()); + + assert_cycles( + "", + vec![ + vec![vec![ + Event::at(F::from(0), F::from(1)).with_name("some_name") + ]], + vec![vec![ + Event::at(F::from(0), F::from(1)).with_name("_another_one") + ]], + vec![vec![ + Event::at(F::from(0), F::from(1)).with_chord(0, 4, "chord") + ]], + vec![vec![ + Event::at(F::from(0), F::from(1)).with_chord(0, 4, "-\u{0394}7"), // -Δ7 + ]], + vec![vec![Event::at(F::from(0), F::from(1)).with_name("c6a_name")]], + ], + )?; + assert_cycles( "[1 2] [3 4,[5 6]:42]", vec![vec![ From c726eff962b77715de1aa90ea14cb36cbf779c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20M=C3=BCller?= Date: Fri, 14 Jun 2024 15:14:27 +0200 Subject: [PATCH 3/4] parse & convert chord values in cycle event iters --- src/event/cycle.rs | 58 ++++++++++++++++++++++--------------- src/event/scripted_cycle.rs | 4 +-- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/event/cycle.rs b/src/event/cycle.rs index 926a725..ad288f1 100644 --- a/src/event/cycle.rs +++ b/src/event/cycle.rs @@ -5,31 +5,11 @@ use fraction::Fraction; use crate::{ event::{new_note, Event, EventIter, EventIterItem, InstrumentId, NoteEvent}, tidal::{Cycle, Event as CycleEvent, Target as CycleTarget, Value as CycleValue}, - BeatTimeBase, Note, PulseIterItem, + BeatTimeBase, Chord, Note, PulseIterItem, }; // ------------------------------------------------------------------------------------------------- -/// Default conversion of a cycle event value to an optional NoteEvent, as used by [`EventIter`]. -impl From<&CycleValue> for Option { - fn from(value: &CycleValue) -> Self { - match value { - CycleValue::Hold => None, - CycleValue::Rest => new_note(Note::OFF), - CycleValue::Float(_f) => None, - CycleValue::Integer(i) => new_note(Note::from((*i).clamp(0, 0x7f) as u8)), - CycleValue::Pitch(p) => new_note(Note::from(p.midi_note())), - CycleValue::Name(s) => { - if s.eq_ignore_ascii_case("off") { - new_note(Note::OFF) - } else { - None - } - } - } - } -} - /// Default conversion of a cycle target to an optional instrument id, as used by [`EventIter`]. impl From<&CycleTarget> for Option { fn from(value: &CycleTarget) -> Self { @@ -41,6 +21,38 @@ impl From<&CycleTarget> for Option { } } +/// Default conversion of a CycleValue into a note stack. +/// +/// Returns an error when resolving chord modes failed. +impl TryFrom<&CycleValue> for Vec> { + type Error = String; + + fn try_from(value: &CycleValue) -> Result { + match value { + CycleValue::Hold => Ok(vec![None]), + CycleValue::Rest => Ok(vec![new_note(Note::OFF)]), + CycleValue::Float(_f) => Ok(vec![None]), + CycleValue::Integer(i) => Ok(vec![new_note(Note::from((*i).clamp(0, 0x7f) as u8))]), + CycleValue::Pitch(p) => Ok(vec![new_note(Note::from(p.midi_note()))]), + CycleValue::Chord(p, m) => { + let chord = Chord::try_from((p.midi_note(), m.as_str()))?; + Ok(chord + .intervals() + .iter() + .map(|i| new_note(chord.note().transposed(*i as i32))) + .collect()) + } + CycleValue::Name(s) => { + if s.eq_ignore_ascii_case("off") { + Ok(vec![new_note(Note::OFF)]) + } else { + Ok(vec![None]) + } + } + } + } +} + // ------------------------------------------------------------------------------------------------- /// Helper struct to convert time tagged events from Cycle into a `Vec` @@ -149,8 +161,8 @@ impl CycleEventIter { // apply custom note mappings note_events.clone() } else { - // convert the cycle value to a single note - vec![event.value().into()] + // try converting the cycle value to a single note + event.value().try_into()? } }; // inject target instrument, if present diff --git a/src/event/scripted_cycle.rs b/src/event/scripted_cycle.rs index 2a5f2e0..858cee3 100644 --- a/src/event/scripted_cycle.rs +++ b/src/event/scripted_cycle.rs @@ -101,8 +101,8 @@ impl ScriptedCycleEventIter { // apply custom note mapping note_events.clone() } else { - // convert the cycle value to a single note - vec![event.value().into()] + // try converting the cycle value to a single note + event.value().try_into().map_err(LuaError::RuntimeError)? } }; // verify that all identifiers are mapped From ce5a8d10e41146a1735dd91a64a5c73998f6fa37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20M=C3=BCller?= Date: Sat, 15 Jun 2024 21:58:24 +0200 Subject: [PATCH 4/4] merge events from cycles when consuming them and pad note stacks with note-offs when needed --- src/event/cycle.rs | 57 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/src/event/cycle.rs b/src/event/cycle.rs index ad288f1..ecf8c36 100644 --- a/src/event/cycle.rs +++ b/src/event/cycle.rs @@ -57,16 +57,24 @@ impl TryFrom<&CycleValue> for Vec> { /// Helper struct to convert time tagged events from Cycle into a `Vec` pub(crate) struct CycleNoteEvents { + // collected events for a given time span per channels events: Vec<(Fraction, Fraction, Vec>)>, + // max note event count per channel + event_counts: Vec, } impl CycleNoteEvents { /// Create a new, empty list of events. pub fn new() -> Self { - Self { events: Vec::new() } + let events = Vec::with_capacity(16); + let event_counts = Vec::with_capacity(3); + Self { + events, + event_counts, + } } - /// Add note events from a cycle channel event. + /// Add a single note event stack from a cycle channel event. pub fn add( &mut self, channel: usize, @@ -74,6 +82,12 @@ impl CycleNoteEvents { length: Fraction, note_events: Vec>, ) { + // memorize max event count per channel + if self.event_counts.len() <= channel { + self.event_counts.resize(channel + 1, 0); + } + self.event_counts[channel] = self.event_counts[channel].max(note_events.len()); + // insert events into existing time slot or a new one match self .events .binary_search_by(|(time, _, _)| time.cmp(&start)) @@ -82,25 +96,48 @@ impl CycleNoteEvents { // use min length of all notes in stack let event_length = &mut self.events[pos].1; *event_length = (*event_length).min(length); - // add new notes to existing event stack + // add new notes to existing events let timed_event = &mut self.events[pos].2; timed_event.resize(channel + 1, None); timed_event[channel] = Some(Event::NoteEvents(note_events)); } - Err(pos) => self.events.insert( - pos, - (start, length, vec![Some(Event::NoteEvents(note_events))]), - ), + Err(pos) => { + // insert a new time event + let mut timed_event = Vec::with_capacity(channel + 1); + timed_event.resize(channel + 1, None); + timed_event[channel] = Some(Event::NoteEvents(note_events)); + self.events.insert(pos, (start, length, timed_event)) + } } } /// Convert to a list of EventIterItems. pub fn into_event_iter_items(self) -> Vec { + // max number of note events in a single merged down Event + let max_event_count = self.event_counts.iter().sum::(); + // apply padding per channel, merge down and convert to EventIterItem let mut event_iter_items: Vec = Vec::with_capacity(self.events.len()); - for (start_time, length, events) in self.events.into_iter() { - for event in events.into_iter().flatten() { - event_iter_items.push(EventIterItem::new_with_fraction(event, start_time, length)); + for (start_time, length, mut events) in self.events.into_iter() { + // ensure that each event in the channel, contains the same number of note events + for (channel, mut event) in events.iter_mut().enumerate() { + if let Some(Event::NoteEvents(note_events)) = &mut event { + // pad existing note events with OFFs + note_events.resize_with(self.event_counts[channel], || new_note(Note::OFF)); + } else if self.event_counts[channel] > 0 { + // pad missing note events with 'None' + *event = Some(Event::NoteEvents(vec![None; self.event_counts[channel]])) + } + } + // merge all events that happen at the same time together + let mut merged_note_events = Vec::with_capacity(max_event_count); + for mut event in events.into_iter().flatten() { + if let Event::NoteEvents(note_events) = &mut event { + merged_note_events.append(note_events); + } } + // convert padded, merged note events to a timed 'Event' + let event = Event::NoteEvents(merged_note_events); + event_iter_items.push(EventIterItem::new_with_fraction(event, start_time, length)); } event_iter_items }