diff --git a/src/bindings.rs b/src/bindings.rs index 6ed31c9..3811128 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -42,7 +42,7 @@ pub use callback::{ }; // internal re-exports -pub(crate) use callback::LuaCallback; +pub(crate) use callback::{ContextPlaybackState, LuaCallback}; pub(crate) use timeout::LuaTimeoutHook; pub(crate) use unwrap::{ gate_trigger_from_value, note_events_from_value, pattern_pulse_from_value, diff --git a/src/bindings/callback.rs b/src/bindings/callback.rs index ee28106..aac276e 100644 --- a/src/bindings/callback.rs +++ b/src/bindings/callback.rs @@ -63,6 +63,23 @@ pub fn add_lua_callback_error(name: &str, err: &LuaError) { // ------------------------------------------------------------------------------------------------- +/// Playback state in LuaCallback context. +pub(crate) enum ContextPlaybackState { + Seeking, + Running, +} + +impl ContextPlaybackState { + fn into_bytes_string(self) -> &'static [u8] { + match self { + Self::Seeking => b"seeking", + Self::Running => b"running", + } + } +} + +// ------------------------------------------------------------------------------------------------- + /// Lazily evaluates a lua function the first time it's called, to either use it as a iterator, /// a function which returns a function, or directly as it is. /// @@ -117,6 +134,36 @@ impl LuaCallback { }) } + /// Returns true if the callback is a generator. + /// + /// To test this, the callback must have run at least once, so it returns None if it never has. + pub fn is_stateful(&self) -> Option { + if self.initialized { + Some(self.generator.is_some()) + } else { + None + } + } + + /// Name of the inner function for errors. Usually will be an anonymous function. + pub fn name(&self) -> String { + self.function + .to_ref() + .info() + .name + .unwrap_or("anonymous function".to_string()) + } + + /// Sets the emitters playback state for the callback. + pub fn set_context_playback_state( + &mut self, + playback_state: ContextPlaybackState, + ) -> LuaResult<()> { + let values = &mut self.context.borrow_mut::()?.values; + values.insert(b"playback", playback_state.into_bytes_string().into()); + Ok(()) + } + /// Sets the emitter time base context for the callback. pub fn set_context_time_base(&mut self, time_base: &BeatTimeBase) -> LuaResult<()> { let values = &mut self.context.borrow_mut::()?.values; @@ -208,12 +255,14 @@ impl LuaCallback { /// Sets the emitter context for the callback. pub fn set_emitter_context( &mut self, + playback_state: ContextPlaybackState, time_base: &BeatTimeBase, pulse: PulseIterItem, pulse_step: usize, pulse_time_step: f64, step: usize, ) -> LuaResult<()> { + self.set_context_playback_state(playback_state)?; self.set_gate_context(time_base, pulse, pulse_step, pulse_time_step)?; self.set_context_step(step)?; Ok(()) @@ -222,36 +271,18 @@ impl LuaCallback { /// Sets the cycle context for the callback. pub fn set_cycle_context( &mut self, + playback_state: ContextPlaybackState, time_base: &BeatTimeBase, channel: usize, step: usize, step_length: f64, ) -> LuaResult<()> { + self.set_context_playback_state(playback_state)?; self.set_context_time_base(time_base)?; self.set_context_cycle_step(channel, step, step_length)?; Ok(()) } - /// Name of the inner function for errors. Usually will be an anonymous function. - pub fn name(&self) -> String { - self.function - .to_ref() - .info() - .name - .unwrap_or("anonymous function".to_string()) - } - - /// Returns true if the callback is a generator. - /// - /// To test this, the callback must have run at least once, so it returns None if it never has. - pub fn is_stateful(&self) -> Option { - if self.initialized { - Some(self.generator.is_some()) - } else { - None - } - } - /// Invoke the Lua function or generator and return its result as LuaValue. pub fn call(&mut self) -> LuaResult { self.call_with_arg(LuaValue::Nil) diff --git a/src/bindings/rhythm.rs b/src/bindings/rhythm.rs index 58086af..e0d3a7f 100644 --- a/src/bindings/rhythm.rs +++ b/src/bindings/rhythm.rs @@ -202,6 +202,7 @@ mod test { local pulse_step, pulse_time_step = 1, 0.0 local step = 1 local function validate_context(context) + assert(context.playback == "running") assert(context.beats_per_min == 120) assert(context.beats_per_bar == 4) assert(context.samples_per_sec == 44100) diff --git a/src/event/scripted.rs b/src/event/scripted.rs index 5d61c04..09b7a2c 100644 --- a/src/event/scripted.rs +++ b/src/event/scripted.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use mlua::prelude::*; use crate::{ - bindings::{note_events_from_value, LuaCallback, LuaTimeoutHook}, + bindings::{note_events_from_value, ContextPlaybackState, LuaCallback, LuaTimeoutHook}, event::{fixed::FixedEventIter, NoteEvent}, BeatTimeBase, Event, EventIter, EventIterItem, PulseIterItem, }; @@ -33,11 +33,19 @@ impl ScriptedEventIter { // initialize emitter context for the function let mut callback = callback; let note_event_state = Vec::new(); + let playback_state = ContextPlaybackState::Running; let pulse = PulseIterItem::default(); let pulse_step = 0; let pulse_time_step = 0.0; let step = 0; - callback.set_emitter_context(time_base, pulse, pulse_step, pulse_time_step, step)?; + callback.set_emitter_context( + playback_state, + time_base, + pulse, + pulse_step, + pulse_time_step, + step, + )?; Ok(Self { timeout_hook, callback, @@ -48,10 +56,12 @@ impl ScriptedEventIter { }) } - fn generate(&mut self, pulse: PulseIterItem) -> LuaResult>> { + fn run(&mut self, pulse: PulseIterItem) -> LuaResult>> { // reset timeout self.timeout_hook.reset(); // update function context + let playback_state = ContextPlaybackState::Running; + self.callback.set_context_playback_state(playback_state)?; self.callback.set_context_pulse_value(pulse)?; self.callback .set_context_pulse_step(self.pulse_step, self.pulse_time_step)?; @@ -70,6 +80,8 @@ impl ScriptedEventIter { // reset timeout self.timeout_hook.reset(); // update function context + let playback_state = ContextPlaybackState::Seeking; + self.callback.set_context_playback_state(playback_state)?; self.callback.set_context_pulse_value(pulse)?; self.callback .set_context_pulse_step(self.pulse_step, self.pulse_time_step)?; @@ -116,7 +128,7 @@ impl EventIter for ScriptedEventIter { fn run(&mut self, pulse: PulseIterItem, emit_event: bool) -> Option> { // generate a new event and move or only update pulse counters if emit_event { - let event = match self.generate(pulse) { + let event = match self.run(pulse) { Ok(event) => event, Err(err) => { self.callback.handle_error(&err); diff --git a/src/event/scripted_cycle.rs b/src/event/scripted_cycle.rs index 15ead4f..70c4ac8 100644 --- a/src/event/scripted_cycle.rs +++ b/src/event/scripted_cycle.rs @@ -4,7 +4,10 @@ use fraction::ToPrimitive; use mlua::prelude::*; use crate::{ - bindings::{add_lua_callback_error, note_events_from_value, LuaCallback, LuaTimeoutHook}, + bindings::{ + add_lua_callback_error, note_events_from_value, ContextPlaybackState, LuaCallback, + LuaTimeoutHook, + }, event::{cycle::CycleNoteEvents, EventIter, EventIterItem, NoteEvent}, BeatTimeBase, PulseIterItem, }; @@ -58,10 +61,17 @@ impl ScriptedCycleEventIter { let mappings = HashMap::new(); // initialize emitter context for the function let mut mapping_callback = mapping_callback; + let playback_state = ContextPlaybackState::Running; let channel = 0; let step = 0; let step_length = 0.0; - mapping_callback.set_cycle_context(time_base, channel, step, step_length)?; + mapping_callback.set_cycle_context( + playback_state, + time_base, + channel, + step, + step_length, + )?; let channel_steps = vec![]; Ok(Self { cycle, @@ -138,6 +148,13 @@ impl ScriptedCycleEventIter { if let Some(timeout_hook) = &mut self.timeout_hook { timeout_hook.reset(); } + // set callback playback state + if let Some(callback) = &mut self.mapping_callback { + let playback_state = ContextPlaybackState::Running; + if let Err(err) = callback.set_context_playback_state(playback_state) { + callback.handle_error(&err); + } + } // convert possibly mapped cycle channel items to a list of note events let mut timed_note_events = CycleNoteEvents::new(); for (channel_index, channel_events) in events.into_iter().enumerate() { @@ -187,10 +204,16 @@ impl ScriptedCycleEventIter { } }; if mapping_callback.is_stateful().unwrap_or(true) { - // run statefull callbacks but ignore results + // reset timeout hooks if let Some(timeout_hook) = &mut self.timeout_hook { timeout_hook.reset(); } + // set playback state + let playback_state = ContextPlaybackState::Seeking; + if let Err(err) = mapping_callback.set_context_playback_state(playback_state) { + mapping_callback.handle_error(&err); + } + // run stateful callbacks but ignore results for (channel_index, channel_events) in events.into_iter().enumerate() { if self.channel_steps.len() <= channel_index { self.channel_steps.resize(channel_index + 1, 0); diff --git a/types/nerdo/library/cycle.lua b/types/nerdo/library/cycle.lua index 03b3f2c..4d56313 100644 --- a/types/nerdo/library/cycle.lua +++ b/types/nerdo/library/cycle.lua @@ -9,6 +9,8 @@ ---Context passed to 'cycle:map` functions. ---@class CycleMapContext : TimeContext --- +---Specifies how the cycle currently is running. +---@field playback PlaybackState ---channel/voice index within the cycle. each channel in the cycle gets emitted and thus mapped ---separately, starting with the first channel index 1. ---@field channel integer diff --git a/types/nerdo/library/rhythm.lua b/types/nerdo/library/rhythm.lua index 200bead..34c345a 100644 --- a/types/nerdo/library/rhythm.lua +++ b/types/nerdo/library/rhythm.lua @@ -6,7 +6,9 @@ error("Do not try to execute this file. It's just a type definition file.") ---------------------------------------------------------------------------------------------------- ----RENOISE SPECIFIC: Optional trigger context passed to `pattern` and 'emit' functions. +---Optional trigger context passed to `pattern`, `gate` and 'emit' functions. +---Specifies which keyboard note triggered, if any, started the rhythm. +--- ---@class TriggerContext --- ---Note value that triggered, started the rhythm, if any. @@ -18,7 +20,7 @@ error("Do not try to execute this file. It's just a type definition file.") ---------------------------------------------------------------------------------------------------- ----Context passed to `pattern` functions. +---Transport & playback time context passed to `pattern`, `gate` and `emit` functions. ---@class TimeContext : TriggerContext --- -----Project's tempo in beats per minutes. @@ -30,7 +32,7 @@ error("Do not try to execute this file. It's just a type definition file.") ---------------------------------------------------------------------------------------------------- ----Context passed to `pattern` functions. +---Context passed to `pattern` and `gate` functions. ---@class PatternContext : TimeContext --- ---Continues pulse counter, incrementing with each new **skipped or emitted pulse**. @@ -61,9 +63,16 @@ error("Do not try to execute this file. It's just a type definition file.") ---------------------------------------------------------------------------------------------------- +---- *seeking*: The emitter is auto-seeked to a target time. All results are discarded. Avoid +--- unnecessary computations while seeking, and only maintain your generator's internal state. +---- *running*: The emitter is played back regularly. Results are audible. +---@alias PlaybackState "seeking"|"running" + ---Context passed to 'emit' functions. ---@class EmitterContext : GateContext --- +---Specifies how the emitter currently is running. +---@field playback PlaybackState ---Continues step counter, incrementing with each new *emitted* pulse. ---Unlike `pulse_step` this does not include skipped, zero values pulses so it basically counts ---how often the emit function already got called.