Skip to content

Commit

Permalink
add playback context state to the emitter and cycle mapping context
Browse files Browse the repository at this point in the history
... so generators can skip processing values and only manage their internal state
  • Loading branch information
emuell committed Jul 12, 2024
1 parent 124c7af commit c016c98
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
71 changes: 51 additions & 20 deletions src/bindings/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<bool> {
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::<CallbackContext>()?.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::<CallbackContext>()?.values;
Expand Down Expand Up @@ -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(())
Expand All @@ -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<bool> {
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<LuaValue> {
self.call_with_arg(LuaValue::Nil)
Expand Down
1 change: 1 addition & 0 deletions src/bindings/rhythm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 16 additions & 4 deletions src/event/scripted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
Expand All @@ -48,10 +56,12 @@ impl ScriptedEventIter {
})
}

fn generate(&mut self, pulse: PulseIterItem) -> LuaResult<Option<Vec<EventIterItem>>> {
fn run(&mut self, pulse: PulseIterItem) -> LuaResult<Option<Vec<EventIterItem>>> {
// 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)?;
Expand All @@ -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)?;
Expand Down Expand Up @@ -116,7 +128,7 @@ impl EventIter for ScriptedEventIter {
fn run(&mut self, pulse: PulseIterItem, emit_event: bool) -> Option<Vec<EventIterItem>> {
// 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);
Expand Down
29 changes: 26 additions & 3 deletions src/event/scripted_cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions types/nerdo/library/cycle.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions types/nerdo/library/rhythm.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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**.
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit c016c98

Please sign in to comment.