use crate::composition::{PulseMidiClip, PulsePhrase, PulseSampleClip, PulseSequence, PulseSong};
use crate::drums::{self, PulseDrumGrid, PulseDrumMachine};
use crate::effects::{self, EffectOption, EffectOptions, PulseEffect};
use crate::export;
use crate::export::{ExportOptions, NormalizeOptions};
use crate::files::{self, PulseImportedMidi};
use crate::generators::{self, GeneratorOption, GeneratorOptions, PulseGenerated};
use crate::instruments;
use crate::midi;
use crate::playback::{PulsePlayable, PulsePlaybackEngine, PulsePlaybackHandle};
use crate::rhythm;
use crate::synthesis::{self, PulseSynth, SynthOption, SynthOptions};
use crate::theory;
use mlua::{Function, Lua, Table, UserData, UserDataMethods, Value, Variadic};
const DEFAULT_PCM_CHUNK_FRAMES: usize = 1024;
pub fn register(lua: &Lua) -> mlua::Result<Table> {
let table = lua.create_table()?;
table.set("_VERSION", env!("CARGO_PKG_VERSION"))?;
table.set(
"note",
lua.create_function(|_, name: String| {
theory::parse_note_frequency(&name).map_err(mlua::Error::from)
})?,
)?;
table.set(
"modes",
lua.create_function(|lua, ()| lua.create_sequence_from(theory::mode_names().to_vec()))?,
)?;
table.set(
"chord_kinds",
lua.create_function(|lua, ()| {
lua.create_sequence_from(theory::chord_kind_names().to_vec())
})?,
)?;
table.set(
"scale",
lua.create_function(|lua, (root, mode): (String, String)| {
let notes = theory::scale(&root, &mode).map_err(mlua::Error::from)?;
lua.create_sequence_from(notes)
})?,
)?;
table.set(
"chord",
lua.create_function(|lua, (root, kind): (String, String)| {
let notes = theory::chord(&root, &kind).map_err(mlua::Error::from)?;
lua.create_sequence_from(notes)
})?,
)?;
table.set(
"transpose",
lua.create_function(|lua, (value, semitones): (Value, i32)| {
transpose_lua_value(lua, value, semitones)
})?,
)?;
table.set(
"frequency_to_midi_note",
lua.create_function(|_, frequency: f32| {
midi::frequency_to_midi_note(frequency).map_err(mlua::Error::from)
})?,
)?;
table.set(
"midi_note_to_frequency",
lua.create_function(|_, note: i64| {
midi::midi_note_to_frequency(note).map_err(mlua::Error::from)
})?,
)?;
table.set(
"export_formats",
lua.create_function(|lua, ()| lua.create_sequence_from(files::export_formats().to_vec()))?,
)?;
table.set(
"instruments",
lua.create_function(|lua, ()| {
lua.create_sequence_from(instruments::instrument_names().to_vec())
})?,
)?;
table.set(
"drums",
lua.create_function(|lua, ()| lua.create_sequence_from(drums::drum_names().to_vec()))?,
)?;
table.set(
"effects",
lua.create_function(|lua, ()| lua.create_sequence_from(effects::effect_names()))?,
)?;
table.set(
"effect_info",
lua.create_function(|lua, name: String| {
let info = effects::effect_info(&name).map_err(mlua::Error::from)?;
effect_info_to_lua(lua, info)
})?,
)?;
table.set(
"synths",
lua.create_function(|lua, ()| lua.create_sequence_from(synthesis::synth_names()))?,
)?;
table.set(
"synth_info",
lua.create_function(|lua, name: String| {
let info = synthesis::synth_info(&name).map_err(mlua::Error::from)?;
synth_info_to_lua(lua, info)
})?,
)?;
table.set(
"generators",
lua.create_function(|lua, ()| lua.create_sequence_from(generators::generator_names()))?,
)?;
table.set(
"generator_info",
lua.create_function(|lua, name: String| {
let info = generators::generator_info(&name).map_err(mlua::Error::from)?;
generator_info_to_lua(lua, info)
})?,
)?;
table.set(
"generate",
lua.create_function(|lua, (name, options): (String, Value)| {
let options = generator_options_from_lua(options)?;
let generated = generators::generate(&name, &options).map_err(mlua::Error::from)?;
generated_to_lua(lua, generated)
})?,
)?;
table.set(
"normalize",
lua.create_function(|lua, (values, min, max): (Table, f32, f32)| {
let values = number_sequence_from_lua(values)?;
let normalized =
generators::normalize_values(&values, min, max).map_err(mlua::Error::from)?;
lua.create_sequence_from(normalized)
})?,
)?;
table.set(
"map_to_scale",
lua.create_function(
|lua, (values, scale, root, octaves): (Table, String, String, u32)| {
let values = number_sequence_from_lua(values)?;
let notes = generators::map_values_to_scale(&values, &scale, &root, octaves)
.map_err(mlua::Error::from)?;
lua.create_sequence_from(notes)
},
)?,
)?;
table.set(
"euclidean",
lua.create_function(|lua, (pulses, steps): (usize, usize)| {
let pattern = rhythm::try_euclidean(pulses, steps).map_err(mlua::Error::from)?;
lua.create_sequence_from(pattern)
})?,
)?;
table.set(
"euclidean_pattern",
lua.create_function(|lua, (pulses, steps): (usize, usize)| {
let pattern =
rhythm::try_euclidean_pattern(pulses, steps).map_err(mlua::Error::from)?;
lua.create_sequence_from(pattern)
})?,
)?;
table.set(
"pattern",
lua.create_function(|lua, name: String| {
let pattern = rhythm::named_pattern(&name).map_err(mlua::Error::from)?;
lua.create_sequence_from(pattern)
})?,
)?;
table.set(
"drum_grid",
lua.create_function(|_, ()| Ok(PulseDrumGrid::new()))?,
)?;
table.set(
"drum_machine",
lua.create_function(|_, kit: String| {
PulseDrumMachine::new(&kit).map_err(mlua::Error::from)
})?,
)?;
table.set(
"sequence",
lua.create_function(|_, ()| Ok(PulseSequence::new()))?,
)?;
table.set(
"phrase",
lua.create_function(|_, ()| Ok(PulsePhrase::new()))?,
)?;
table.set(
"sample",
lua.create_function(|_, path: String| Ok(PulseSampleClip::new(path)))?,
)?;
table.set("song", lua.create_function(|_, ()| Ok(PulseSong::new()))?)?;
table.set(
"playback",
lua.create_function(|_, ()| PulsePlaybackEngine::new().map_err(mlua::Error::from))?,
)?;
table.set(
"import_midi",
lua.create_function(|_, path: String| files::import_midi(path).map_err(mlua::Error::from))?,
)?;
table.set(
"stream_pcm",
lua.create_function(|lua, args: Variadic<Value>| stream_pcm_value_to_lua(lua, args))?,
)?;
Ok(table)
}
pub fn register_package_loader(lua: &Lua) -> mlua::Result<()> {
let module = register(lua)?;
lua.register_module("pulse", module)
}
impl UserData for PulseSequence {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method_mut("notes", |_, this, table: Table| {
let notes = table
.sequence_values::<f32>()
.collect::<mlua::Result<Vec<_>>>()?;
*this = this.clone().with_notes(notes);
Ok(this.clone())
});
methods.add_method_mut("chords", |_, this, table: Table| {
let chords = chord_sequence_from_lua(table)?;
*this = this.clone().with_chords(chords);
Ok(this.clone())
});
methods.add_method_mut("durations", |_, this, table: Table| {
let durations = table
.sequence_values::<f32>()
.collect::<mlua::Result<Vec<_>>>()?;
*this = this.clone().with_durations(durations);
Ok(this.clone())
});
methods.add_method_mut("instrument", |_, this, instrument: String| {
*this = this.clone().with_instrument(instrument);
Ok(this.clone())
});
methods.add_method_mut("at", |_, this, start_at: f32| {
*this = this
.clone()
.with_start_at(start_at)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("volume", |_, this, volume: f32| {
*this = this
.clone()
.with_volume(volume)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("pan", |_, this, pan: f32| {
*this = this.clone().with_pan(pan).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("velocity", |_, this, velocity: f32| {
*this = this
.clone()
.with_velocity(velocity)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("effect", |_, this, (name, options): (String, Value)| {
let effect = effect_from_lua_value(&name, options)?;
*this = this.clone().with_effect(effect);
Ok(this.clone())
});
methods.add_method_mut("synth", |_, this, (name, options): (String, Value)| {
let synth = synth_from_lua_value(&name, options)?;
*this = this.clone().with_synth(synth);
Ok(this.clone())
});
methods.add_method("transpose", |_, this, semitones: i32| {
Ok(this.transposed(semitones))
});
}
}
impl UserData for PulseSong {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method_mut("tempo", |_, this, bpm: f32| {
*this = this.clone().with_tempo(bpm).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("add", |_, this, sequence: mlua::AnyUserData| {
let sequence = sequence.borrow::<PulseSequence>()?.clone();
*this = this.clone().add_sequence(sequence);
Ok(this.clone())
});
methods.add_method_mut("add_drum_grid", |_, this, grid: mlua::AnyUserData| {
let grid = grid.borrow::<PulseDrumGrid>()?.clone();
*this = this.clone().add_drum_grid(grid);
Ok(this.clone())
});
methods.add_method_mut("add_phrase", |_, this, phrase: mlua::AnyUserData| {
let phrase = phrase.borrow::<PulsePhrase>()?.clone();
*this = this.clone().add_phrase(phrase);
Ok(this.clone())
});
methods.add_method_mut("add_sample", |_, this, sample_clip: mlua::AnyUserData| {
let sample_clip = sample_clip.borrow::<PulseSampleClip>()?.clone();
*this = this.clone().add_sample_clip(sample_clip);
Ok(this.clone())
});
methods.add_method_mut("add_midi", |_, this, midi: mlua::AnyUserData| {
let midi_clip = midi_clip_from_userdata(midi)?;
*this = this.clone().add_midi_clip(midi_clip);
Ok(this.clone())
});
methods.add_method_mut(
"master_effect",
|_, this, (name, options): (String, Value)| {
let effect = effect_from_lua_value(&name, options)?;
if !effect.allowed_on_master() {
return Err(mlua::Error::from(
crate::error::PulseError::InvalidEffectScope {
effect: effect.name().to_string(),
scope: "master".to_string(),
},
));
}
*this = this.clone().with_master_effect(effect);
Ok(this.clone())
},
);
methods.add_method("export_wav", |_, this, args: Variadic<Value>| {
let (path, options) = export_args_from_lua(args)?;
export::export_wav_with_options(this, path, options).map_err(mlua::Error::from)
});
methods.add_method("export", |_, this, args: Variadic<Value>| {
let (path, options) = export_args_from_lua(args)?;
files::export_with_options(this, path, options).map_err(mlua::Error::from)
});
methods.add_method("export_flac", |_, this, args: Variadic<Value>| {
let (path, options) = export_args_from_lua(args)?;
files::export_flac_with_options(this, path, options).map_err(mlua::Error::from)
});
methods.add_method("export_midi", |_, this, args: Variadic<Value>| {
let (path, options) = export_args_from_lua(args)?;
files::export_midi_with_options(this, path, options).map_err(mlua::Error::from)
});
methods.add_method("stream_pcm", |lua, this, args: Variadic<Value>| {
let (callback, options, chunk_frames) = stream_pcm_args_from_lua(args)?;
let mut mixer = this.to_mixer().map_err(mlua::Error::from)?;
stream_pcm_mixer_to_lua(lua, &mut mixer, callback, options, chunk_frames)
});
methods.add_method("play", |_, this, ()| {
let engine = PulsePlaybackEngine::new().map_err(mlua::Error::from)?;
engine
.play(PulsePlayable::Song(this))
.map_err(mlua::Error::from)
});
methods.add_method("play_at_rate", |_, this, rate: f32| {
let engine = PulsePlaybackEngine::new().map_err(mlua::Error::from)?;
engine
.play_at_rate(PulsePlayable::Song(this), rate)
.map_err(mlua::Error::from)
});
}
}
impl UserData for PulsePhrase {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method_mut("add", |_, this, sequence: mlua::AnyUserData| {
let sequence = sequence.borrow::<PulseSequence>()?.clone();
*this = this.clone().add_sequence(sequence);
Ok(this.clone())
});
methods.add_method_mut("add_drum_grid", |_, this, grid: mlua::AnyUserData| {
let grid = grid.borrow::<PulseDrumGrid>()?.clone();
*this = this.clone().add_drum_grid(grid);
Ok(this.clone())
});
methods.add_method_mut("add_sample", |_, this, sample_clip: mlua::AnyUserData| {
let sample_clip = sample_clip.borrow::<PulseSampleClip>()?.clone();
*this = this.clone().add_sample_clip(sample_clip);
Ok(this.clone())
});
methods.add_method_mut("add_midi", |_, this, midi: mlua::AnyUserData| {
let midi_clip = midi_clip_from_userdata(midi)?;
*this = this.clone().add_midi_clip(midi_clip);
Ok(this.clone())
});
methods.add_method_mut("repeat_times", |_, this, repeat_times: Value| {
let repeat_times = repeat_times_from_lua(repeat_times)?;
*this = this
.clone()
.with_repeat_times(repeat_times)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
}
}
fn midi_clip_from_userdata(userdata: mlua::AnyUserData) -> mlua::Result<PulseMidiClip> {
if let Ok(midi_clip) = userdata.borrow::<PulseMidiClip>() {
return Ok(midi_clip.clone());
}
if let Ok(imported) = userdata.borrow::<PulseImportedMidi>() {
return Ok(imported.as_clip());
}
Err(mlua::Error::FromLuaConversionError {
from: "userdata",
to: "PulseMidiClip or PulseImportedMidi".to_string(),
message: Some("add_midi expects an imported MIDI value or MIDI clip".to_string()),
})
}
impl UserData for PulseSampleClip {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method_mut("rate", |_, this, playback_rate: f32| {
*this = this
.clone()
.with_playback_rate(playback_rate)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("gain", |_, this, gain: f32| {
*this = this.clone().with_gain(gain).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("pitch_shift", |_, this, semitones: f32| {
*this = this
.clone()
.with_pitch_shift(semitones)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("time_stretch", |_, this, factor: f32| {
*this = this
.clone()
.with_time_stretch(factor)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("at", |_, this, start_at: f32| {
*this = this
.clone()
.with_start_at(start_at)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("track", |_, this, track_name: String| {
*this = this.clone().with_track(track_name);
Ok(this.clone())
});
methods.add_method_mut("volume", |_, this, volume: f32| {
*this = this
.clone()
.with_volume(volume)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("pan", |_, this, pan: f32| {
*this = this.clone().with_pan(pan).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("effect", |_, this, (name, options): (String, Value)| {
let effect = effect_from_lua_value(&name, options)?;
*this = this.clone().with_effect(effect);
Ok(this.clone())
});
methods.add_method_mut("slice", |_, this, (start, end): (f32, f32)| {
*this = this
.clone()
.with_slice(start, end)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("reverse", |_, this, ()| {
*this = this.clone().with_reverse();
Ok(this.clone())
});
methods.add_method_mut("loop_for", |_, this, duration: f32| {
*this = this
.clone()
.with_loop_for(duration)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("normalize", |_, this, ()| {
*this = this.clone().with_normalize();
Ok(this.clone())
});
methods.add_method_mut("fade_in", |_, this, duration: f32| {
*this = this
.clone()
.with_fade_in(duration)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("fade_out", |_, this, duration: f32| {
*this = this
.clone()
.with_fade_out(duration)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
}
}
impl UserData for PulseMidiClip {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method_mut("at", |_, this, start_at: f32| {
*this = this
.clone()
.with_start_at(start_at)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("track", |_, this, track_name: String| {
*this = this.clone().with_track(track_name);
Ok(this.clone())
});
methods.add_method_mut("volume", |_, this, volume: f32| {
*this = this
.clone()
.with_volume(volume)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("pan", |_, this, pan: f32| {
*this = this.clone().with_pan(pan).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("repeat_times", |_, this, repeat_times: Value| {
let repeat_times = repeat_times_from_lua(repeat_times)?;
*this = this
.clone()
.with_repeat_times(repeat_times)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("effect", |_, this, (name, options): (String, Value)| {
let effect = effect_from_lua_value(&name, options)?;
*this = this.clone().with_effect(effect);
Ok(this.clone())
});
}
}
impl UserData for PulsePlaybackEngine {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("play", |_, this, userdata: mlua::AnyUserData| {
with_playable_userdata(userdata, |playable| {
this.play(playable).map_err(mlua::Error::from)
})
});
methods.add_method("play_realtime", |_, this, userdata: mlua::AnyUserData| {
with_playable_userdata(userdata, |playable| {
this.play_realtime(playable).map_err(mlua::Error::from)
})
});
methods.add_method("play_looping", |_, this, userdata: mlua::AnyUserData| {
with_playable_userdata(userdata, |playable| {
this.play_looping(playable).map_err(mlua::Error::from)
})
});
methods.add_method(
"play_at_rate",
|_, this, (userdata, rate): (mlua::AnyUserData, f32)| {
with_playable_userdata(userdata, |playable| {
this.play_at_rate(playable, rate).map_err(mlua::Error::from)
})
},
);
methods.add_method(
"play_realtime_at_rate",
|_, this, (userdata, rate): (mlua::AnyUserData, f32)| {
with_playable_userdata(userdata, |playable| {
this.play_realtime_at_rate(playable, rate)
.map_err(mlua::Error::from)
})
},
);
}
}
impl UserData for PulsePlaybackHandle {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("stop", |_, this, ()| this.stop().map_err(mlua::Error::from));
methods.add_method("volume", |_, this, volume: f32| {
this.volume(volume).map_err(mlua::Error::from)
});
methods.add_method("pan", |_, this, pan: f32| {
this.pan(pan).map_err(mlua::Error::from)
});
methods.add_method("rate", |_, this, rate: f32| {
this.rate(rate).map_err(mlua::Error::from)
});
methods.add_method("pause", |_, this, ()| {
this.pause().map_err(mlua::Error::from)
});
methods.add_method("resume", |_, this, ()| {
this.resume().map_err(mlua::Error::from)
});
}
}
impl UserData for PulseImportedMidi {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("at", |_, this, start_at: f32| {
this.as_clip()
.with_start_at(start_at)
.map_err(mlua::Error::from)
});
methods.add_method("track", |_, this, track_name: String| {
Ok(this.as_clip().with_track(track_name))
});
methods.add_method("volume", |_, this, volume: f32| {
this.as_clip()
.with_volume(volume)
.map_err(mlua::Error::from)
});
methods.add_method("pan", |_, this, pan: f32| {
this.as_clip().with_pan(pan).map_err(mlua::Error::from)
});
methods.add_method("repeat_times", |_, this, repeat_times: Value| {
let repeat_times = repeat_times_from_lua(repeat_times)?;
this.as_clip()
.with_repeat_times(repeat_times)
.map_err(mlua::Error::from)
});
methods.add_method("effect", |_, this, (name, options): (String, Value)| {
let effect = effect_from_lua_value(&name, options)?;
Ok(this.as_clip().with_effect(effect))
});
methods.add_method("export_wav", |_, this, args: Variadic<Value>| {
let (path, options) = export_args_from_lua(args)?;
this.export_wav_with_options(path, options)
.map_err(mlua::Error::from)
});
methods.add_method("export", |_, this, args: Variadic<Value>| {
let (path, options) = export_args_from_lua(args)?;
this.export_with_options(path, options)
.map_err(mlua::Error::from)
});
methods.add_method("export_flac", |_, this, args: Variadic<Value>| {
let (path, options) = export_args_from_lua(args)?;
this.export_flac_with_options(path, options)
.map_err(mlua::Error::from)
});
methods.add_method("export_midi", |_, this, args: Variadic<Value>| {
let (path, options) = export_args_from_lua(args)?;
this.export_midi_with_options(path, options)
.map_err(mlua::Error::from)
});
methods.add_method("stream_pcm", |lua, this, args: Variadic<Value>| {
let (callback, options, chunk_frames) = stream_pcm_args_from_lua(args)?;
let mut mixer = this.cloned_mixer();
stream_pcm_mixer_to_lua(lua, &mut mixer, callback, options, chunk_frames)
});
}
}
#[derive(Debug, Clone)]
struct PulsePcmChunk {
samples: Vec<i16>,
sample_rate: u32,
}
impl PulsePcmChunk {
fn new(samples: &[i16], sample_rate: u32) -> Self {
Self {
samples: samples.to_vec(),
sample_rate,
}
}
fn frames(&self) -> usize {
self.samples.len() / export::PCM_CHANNELS
}
fn bytes(&self) -> usize {
self.samples.len() * std::mem::size_of::<i16>()
}
}
impl UserData for PulsePcmChunk {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("frames", |_, this, ()| Ok(this.frames()));
methods.add_method("samples", |_, this, ()| Ok(this.samples.len()));
methods.add_method("bytes", |_, this, ()| Ok(this.bytes()));
methods.add_method("sample_rate", |_, this, ()| Ok(this.sample_rate));
methods.add_method("channels", |_, _, ()| Ok(export::PCM_CHANNELS));
methods.add_method("format", |_, _, ()| Ok("i16le"));
methods.add_method("sample", |_, this, index: usize| {
let Some(index) = index.checked_sub(1) else {
return Ok(None);
};
Ok(this.samples.get(index).copied())
});
}
}
impl UserData for PulseDrumGrid {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method_mut("track", |_, this, track_name: String| {
*this = this.clone().with_track(track_name);
Ok(this.clone())
});
methods.add_method_mut("steps", |_, this, steps: Value| {
let steps = drum_steps_from_lua(steps)?;
*this = this.clone().with_steps(steps).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("step_duration", |_, this, duration: f32| {
*this = this
.clone()
.with_step_duration(duration)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("sound", |_, this, (drum, pattern): (String, Value)| {
let pattern = pattern_from_lua_value(pattern)?;
*this = this
.clone()
.sound(drum, pattern)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("accent", |_, this, pattern: Value| {
let pattern = pattern_from_lua_value(pattern)?;
*this = this.clone().accent(pattern);
Ok(this.clone())
});
methods.add_method_mut("velocity", |_, this, table: Table| {
let velocities = table
.sequence_values::<f32>()
.collect::<mlua::Result<Vec<_>>>()?;
*this = this.clone().velocity(velocities);
Ok(this.clone())
});
methods.add_method_mut("volume", |_, this, volume: f32| {
*this = this
.clone()
.with_volume(volume)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("pan", |_, this, pan: f32| {
*this = this.clone().with_pan(pan).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("repeat_times", |_, this, repeat_times: Value| {
let repeat_times = repeat_times_from_lua(repeat_times)?;
*this = this
.clone()
.with_repeat_times(repeat_times)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("effect", |_, this, (name, options): (String, Value)| {
let effect = effect_from_lua_value(&name, options)?;
*this = this.clone().with_effect(effect);
Ok(this.clone())
});
}
}
impl UserData for PulseDrumMachine {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method_mut("steps", |_, this, steps: Value| {
let steps = drum_steps_from_lua(steps)?;
*this = this.clone().steps(steps).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("step_duration", |_, this, duration: f32| {
*this = this
.clone()
.step_duration(duration)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("kick", |_, this, pattern: Value| {
let pattern = pattern_from_lua_value(pattern)?;
*this = this.clone().kick(pattern).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("snare", |_, this, pattern: Value| {
let pattern = pattern_from_lua_value(pattern)?;
*this = this.clone().snare(pattern).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("hihat", |_, this, pattern: Value| {
let pattern = pattern_from_lua_value(pattern)?;
*this = this.clone().hihat(pattern).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("open_hihat", |_, this, pattern: Value| {
let pattern = pattern_from_lua_value(pattern)?;
*this = this
.clone()
.open_hihat(pattern)
.map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method_mut("clap", |_, this, pattern: Value| {
let pattern = pattern_from_lua_value(pattern)?;
*this = this.clone().clap(pattern).map_err(mlua::Error::from)?;
Ok(this.clone())
});
methods.add_method("grid", |_, this, ()| Ok(this.clone().grid()));
}
}
fn pattern_from_lua_value(value: Value) -> mlua::Result<Vec<usize>> {
match value {
Value::String(pattern) => {
let pattern = pattern.to_str()?;
Ok(rhythm::steps_from_pattern_string(pattern.as_ref()))
}
Value::Table(table) => table
.sequence_values::<usize>()
.collect::<mlua::Result<_>>(),
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "table or pattern string".to_string(),
message: Some(
"pattern expects a table of step indices or a pattern string".to_string(),
),
}),
}
}
fn export_args_from_lua(args: Variadic<Value>) -> mlua::Result<(String, ExportOptions)> {
let mut args = args.into_iter();
let path = match args.next() {
Some(Value::String(path)) => path.to_str()?.as_ref().to_string(),
Some(other) => {
return Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "string".to_string(),
message: Some("export expects a path string".to_string()),
});
}
None => {
return Err(mlua::Error::FromLuaConversionError {
from: "missing",
to: "string".to_string(),
message: Some("export expects a path string".to_string()),
});
}
};
let options = match args.next() {
Some(Value::Nil) | None => ExportOptions::new(),
Some(Value::Table(table)) => export_options_from_lua(table)?,
Some(other) => {
return Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "table or nil".to_string(),
message: Some("export options must be a table or nil".to_string()),
});
}
};
if args.next().is_some() {
return Err(mlua::Error::RuntimeError(
"export expects a path and optional options table".to_string(),
));
}
Ok((path, options))
}
fn stream_pcm_args_from_lua(
args: Variadic<Value>,
) -> mlua::Result<(Function, ExportOptions, usize)> {
stream_pcm_args_from_values(args.into_iter().collect())
}
fn stream_pcm_args_from_values(
values: Vec<Value>,
) -> mlua::Result<(Function, ExportOptions, usize)> {
let mut args = values.into_iter();
let (callback, options, chunk_frames) = match args.next() {
Some(Value::Function(callback)) => {
let (options, chunk_frames) = match args.next() {
Some(Value::Nil) | None => (ExportOptions::new(), DEFAULT_PCM_CHUNK_FRAMES),
Some(Value::Table(table)) => stream_pcm_options_from_lua(table)?,
Some(other) => {
return Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "table or nil".to_string(),
message: Some("stream_pcm options must be a table or nil".to_string()),
});
}
};
(callback, options, chunk_frames)
}
Some(Value::Table(table)) => {
let (options, chunk_frames) = stream_pcm_options_from_lua(table)?;
let callback = match args.next() {
Some(Value::Function(callback)) => callback,
Some(other) => {
return Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "function".to_string(),
message: Some("stream_pcm expects a callback function".to_string()),
});
}
None => {
return Err(mlua::Error::FromLuaConversionError {
from: "missing",
to: "function".to_string(),
message: Some("stream_pcm expects a callback function".to_string()),
});
}
};
(callback, options, chunk_frames)
}
Some(other) => {
return Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "function".to_string(),
message: Some("stream_pcm expects a callback function".to_string()),
});
}
None => {
return Err(mlua::Error::FromLuaConversionError {
from: "missing",
to: "function".to_string(),
message: Some("stream_pcm expects a callback function".to_string()),
});
}
};
if args.next().is_some() {
return Err(mlua::Error::RuntimeError(
"stream_pcm expects a callback and optional options table".to_string(),
));
}
Ok((callback, options, chunk_frames))
}
fn stream_pcm_options_from_lua(table: Table) -> mlua::Result<(ExportOptions, usize)> {
let chunk_frames =
optional_usize_from_lua(&table, "chunk_frames")?.unwrap_or(DEFAULT_PCM_CHUNK_FRAMES);
if chunk_frames == 0 {
return Err(mlua::Error::FromLuaConversionError {
from: "integer",
to: "positive integer".to_string(),
message: Some("stream_pcm option chunk_frames must be positive".to_string()),
});
}
Ok((export_options_from_lua(table)?, chunk_frames))
}
fn stream_pcm_value_to_lua(lua: &Lua, args: Variadic<Value>) -> mlua::Result<Option<usize>> {
let mut args = args.into_iter();
let Some(value) = args.next() else {
return Ok(None);
};
let rest = args.collect::<Vec<_>>();
let Value::UserData(userdata) = value else {
return Ok(None);
};
if let Ok(song) = userdata.borrow::<PulseSong>() {
let (callback, options, chunk_frames) = stream_pcm_args_from_values(rest)?;
let mut mixer = song.to_mixer().map_err(mlua::Error::from)?;
return stream_pcm_mixer_to_lua(lua, &mut mixer, callback, options, chunk_frames);
}
if let Ok(imported) = userdata.borrow::<PulseImportedMidi>() {
let (callback, options, chunk_frames) = stream_pcm_args_from_values(rest)?;
let mut mixer = imported.cloned_mixer();
return stream_pcm_mixer_to_lua(lua, &mut mixer, callback, options, chunk_frames);
}
Ok(None)
}
fn stream_pcm_mixer_to_lua(
lua: &Lua,
mixer: &mut tunes::track::Mixer,
callback: Function,
options: ExportOptions,
chunk_frames: usize,
) -> mlua::Result<Option<usize>> {
let (samples, sample_rate) =
export::render_mixer_pcm_i16(mixer, options).map_err(mlua::Error::from)?;
let samples_per_chunk = chunk_frames.saturating_mul(export::PCM_CHANNELS);
if samples_per_chunk == 0 {
return Ok(None);
}
let mut chunk_count = 0;
for chunk in samples.chunks(samples_per_chunk) {
if chunk.is_empty() {
continue;
}
let userdata = lua.create_userdata(PulsePcmChunk::new(chunk, sample_rate))?;
callback.call::<()>(userdata)?;
chunk_count += 1;
}
Ok(Some(chunk_count))
}
fn export_options_from_lua(table: Table) -> mlua::Result<ExportOptions> {
let gpu = match table.get::<Value>("gpu")? {
Value::Nil => false,
Value::Boolean(value) => value,
other => {
return Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "boolean".to_string(),
message: Some("export option gpu must be a boolean".to_string()),
});
}
};
let sample_rate = match table.get::<Value>("sample_rate")? {
Value::Nil => None,
Value::Integer(0) => {
return Err(mlua::Error::from(
crate::error::PulseError::InvalidExportSampleRate { sample_rate: 0 },
));
}
Value::Integer(value) if value < 0 || value > u32::MAX as i64 => {
return Err(mlua::Error::FromLuaConversionError {
from: "integer",
to: "positive integer".to_string(),
message: Some("export option sample_rate must be a positive integer".to_string()),
});
}
Value::Integer(value) => Some(value as u32),
Value::Number(value)
if !value.is_finite()
|| value <= 0.0
|| value > u32::MAX as f64
|| value.fract() != 0.0 =>
{
return Err(mlua::Error::FromLuaConversionError {
from: "number",
to: "positive integer".to_string(),
message: Some("export option sample_rate must be a positive integer".to_string()),
});
}
Value::Number(value) => Some(value as u32),
other => {
return Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "positive integer".to_string(),
message: Some("export option sample_rate must be a positive integer".to_string()),
});
}
};
let mut options = ExportOptions::new().with_gpu(gpu);
if let Some(sample_rate) = sample_rate {
options = options
.with_sample_rate(sample_rate)
.map_err(mlua::Error::from)?;
}
if let Some(normalize) = normalize_options_from_lua(&table)? {
options = options.with_normalization(normalize);
}
if let Some(bits_per_sample) = optional_u32_from_lua(&table, "flac_bits_per_sample")? {
options = options
.with_flac_bits_per_sample(bits_per_sample)
.map_err(mlua::Error::from)?;
}
if let Some(gain) = optional_f32_from_lua(&table, "midi_velocity_gain")? {
options = options
.with_midi_velocity_gain(gain)
.map_err(mlua::Error::from)?;
}
if let Some(min_velocity) = optional_f32_from_lua(&table, "midi_min_velocity")? {
options = options
.with_midi_min_velocity(min_velocity)
.map_err(mlua::Error::from)?;
}
Ok(options)
}
fn optional_usize_from_lua(table: &Table, key: &str) -> mlua::Result<Option<usize>> {
match table.get::<Value>(key)? {
Value::Nil => Ok(None),
Value::Integer(value) => {
usize::try_from(value)
.map(Some)
.map_err(|_| mlua::Error::FromLuaConversionError {
from: "integer",
to: "non-negative integer".to_string(),
message: Some(format!(
"stream_pcm option {key} must be a non-negative integer"
)),
})
}
Value::Number(value)
if !value.is_finite()
|| value < 0.0
|| value >= usize_upper_bound_as_f64()
|| value.fract() != 0.0 =>
{
Err(mlua::Error::FromLuaConversionError {
from: "number",
to: "non-negative integer".to_string(),
message: Some(format!(
"stream_pcm option {key} must be a non-negative integer"
)),
})
}
Value::Number(value) => Ok(Some(value as usize)),
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "non-negative integer".to_string(),
message: Some(format!(
"stream_pcm option {key} must be a non-negative integer"
)),
}),
}
}
fn repeat_times_from_lua(value: Value) -> mlua::Result<usize> {
let repeat_times = match value {
Value::Integer(value) => usize::try_from(value).ok(),
Value::Number(value)
if value.is_finite()
&& value >= 0.0
&& value < usize_upper_bound_as_f64()
&& value.fract() == 0.0 =>
{
Some(value as usize)
}
_ => None,
};
let Some(repeat_times) = repeat_times else {
return Err(mlua::Error::from(
crate::error::PulseError::InvalidRepeatTimes { repeat_times: 0 },
));
};
if repeat_times == 0 {
return Err(mlua::Error::from(
crate::error::PulseError::InvalidRepeatTimes { repeat_times },
));
}
Ok(repeat_times)
}
fn drum_steps_from_lua(value: Value) -> mlua::Result<usize> {
let steps = match value {
Value::Integer(value) => usize::try_from(value).ok(),
Value::Number(value)
if value.is_finite()
&& value >= 0.0
&& value < usize_upper_bound_as_f64()
&& value.fract() == 0.0 =>
{
Some(value as usize)
}
_ => None,
};
let Some(steps) = steps else {
return Err(mlua::Error::from(
crate::error::PulseError::InvalidDrumGridSteps { steps: 0 },
));
};
if steps == 0 {
return Err(mlua::Error::from(
crate::error::PulseError::InvalidDrumGridSteps { steps },
));
}
Ok(steps)
}
fn usize_upper_bound_as_f64() -> f64 {
let upper = usize::MAX as f64;
if (upper as u128) > usize::MAX as u128 {
upper
} else {
upper + 1.0
}
}
fn optional_u32_from_lua(table: &Table, key: &str) -> mlua::Result<Option<u32>> {
match table.get::<Value>(key)? {
Value::Nil => Ok(None),
Value::Integer(value) if value < 0 || value > u32::MAX as i64 => {
Err(mlua::Error::FromLuaConversionError {
from: "integer",
to: "non-negative integer".to_string(),
message: Some(format!(
"export option {key} must be a non-negative integer"
)),
})
}
Value::Integer(value) => Ok(Some(value as u32)),
Value::Number(value)
if !value.is_finite()
|| value < 0.0
|| value > u32::MAX as f64
|| value.fract() != 0.0 =>
{
Err(mlua::Error::FromLuaConversionError {
from: "number",
to: "non-negative integer".to_string(),
message: Some(format!(
"export option {key} must be a non-negative integer"
)),
})
}
Value::Number(value) => Ok(Some(value as u32)),
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "non-negative integer".to_string(),
message: Some(format!(
"export option {key} must be a non-negative integer"
)),
}),
}
}
fn normalize_options_from_lua(table: &Table) -> mlua::Result<Option<NormalizeOptions>> {
let mode = match table.get::<Value>("normalize")? {
Value::Nil | Value::Boolean(false) => return Ok(None),
Value::Boolean(true) => "peak".to_string(),
Value::String(value) => value.to_str()?.as_ref().to_ascii_lowercase(),
Value::Table(table) => {
let mode = match table.get::<Value>("mode")? {
Value::Nil => "rms".to_string(),
Value::String(value) => value.to_str()?.as_ref().to_ascii_lowercase(),
other => {
return Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "string".to_string(),
message: Some("normalize.mode must be a string".to_string()),
});
}
};
return normalize_options_from_lua_table(mode, &table).map(Some);
}
other => {
return Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "boolean, string, table, or nil".to_string(),
message: Some(
"export option normalize must be true, false, 'peak', 'rms', or a table"
.to_string(),
),
});
}
};
normalize_options_from_lua_table(mode, table).map(Some)
}
fn normalize_options_from_lua_table(mode: String, table: &Table) -> mlua::Result<NormalizeOptions> {
match mode.as_str() {
"peak" => {
let target_db = optional_f32_from_lua(table, "target_db")?.unwrap_or(-1.0);
NormalizeOptions::peak(target_db).map_err(mlua::Error::from)
}
"rms" => {
let target_db = optional_f32_from_lua(table, "target_db")?.unwrap_or(-16.0);
let peak_ceiling_db = optional_f32_from_lua(table, "true_peak_db")?
.or(optional_f32_from_lua(table, "peak_ceiling_db")?)
.unwrap_or(-1.0);
NormalizeOptions::rms(target_db, peak_ceiling_db).map_err(mlua::Error::from)
}
other => Err(mlua::Error::FromLuaConversionError {
from: "string",
to: "peak or rms".to_string(),
message: Some(format!("unsupported normalize mode: {other}")),
}),
}
}
fn optional_f32_from_lua(table: &Table, key: &str) -> mlua::Result<Option<f32>> {
match table.get::<Value>(key)? {
Value::Nil => Ok(None),
Value::Integer(value) => Ok(Some(value as f32)),
Value::Number(value) if value.is_finite() => Ok(Some(value as f32)),
Value::Number(_) => Err(mlua::Error::FromLuaConversionError {
from: "number",
to: "finite number".to_string(),
message: Some(format!("export option {key} must be a finite number")),
}),
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "number".to_string(),
message: Some(format!("export option {key} must be a number")),
}),
}
}
fn with_playable_userdata<T>(
userdata: mlua::AnyUserData,
f: impl FnOnce(PulsePlayable<'_>) -> mlua::Result<T>,
) -> mlua::Result<T> {
if let Ok(song) = userdata.borrow::<PulseSong>() {
return f(PulsePlayable::Song(&song));
}
if let Ok(imported) = userdata.borrow::<PulseImportedMidi>() {
return f(PulsePlayable::ImportedMidi(&imported));
}
Err(mlua::Error::FromLuaConversionError {
from: "userdata",
to: "PulseSong or PulseImportedMidi".to_string(),
message: Some("playback expects a PulseSong or PulseImportedMidi".to_string()),
})
}
fn effect_info_to_lua(lua: &Lua, info: &effects::EffectInfo) -> mlua::Result<Table> {
let table = lua.create_table()?;
table.set("name", info.name)?;
table.set("category", info.category)?;
table.set("scopes", lua.create_sequence_from(info.scopes.to_vec())?)?;
table.set(
"parameters",
lua.create_sequence_from(info.parameters.to_vec())?,
)?;
table.set("presets", lua.create_sequence_from(info.presets.to_vec())?)?;
Ok(table)
}
fn synth_info_to_lua(lua: &Lua, info: &synthesis::SynthInfo) -> mlua::Result<Table> {
let table = lua.create_table()?;
table.set("name", info.name)?;
table.set("category", info.category)?;
table.set(
"parameters",
lua.create_sequence_from(info.parameters.to_vec())?,
)?;
table.set("presets", lua.create_sequence_from(info.presets.to_vec())?)?;
table.set("aliases", lua.create_sequence_from(info.aliases.to_vec())?)?;
Ok(table)
}
fn effect_from_lua_value(name: &str, value: Value) -> mlua::Result<PulseEffect> {
let options = effect_options_from_lua(value)?;
effects::effect_from_options(name, options).map_err(mlua::Error::from)
}
fn effect_options_from_lua(value: Value) -> mlua::Result<EffectOptions> {
match value {
Value::Nil => Ok(EffectOptions::Default),
Value::String(value) => Ok(EffectOptions::Preset(value.to_str()?.as_ref().to_string())),
Value::Table(table) => {
let mut options = std::collections::BTreeMap::new();
for pair in table.pairs::<Value, Value>() {
let (key, value) = pair?;
let Value::String(key) = key else {
return Err(mlua::Error::FromLuaConversionError {
from: key.type_name(),
to: "string".to_string(),
message: Some("effect option names must be strings".to_string()),
});
};
options.insert(
key.to_str()?.as_ref().to_string(),
effect_option_from_lua(value)?,
);
}
Ok(EffectOptions::Params(options))
}
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "nil, preset string, or table".to_string(),
message: Some("effect options must be nil, preset string, or table".to_string()),
}),
}
}
fn effect_option_from_lua(value: Value) -> mlua::Result<EffectOption> {
match value {
Value::Integer(value) => Ok(EffectOption::Integer(value)),
Value::Number(value) => Ok(EffectOption::Number(value as f32)),
Value::String(value) => Ok(EffectOption::Text(value.to_str()?.as_ref().to_string())),
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "number, integer, or string".to_string(),
message: Some("effect table values accept numbers, integers, and strings".to_string()),
}),
}
}
fn synth_from_lua_value(name: &str, value: Value) -> mlua::Result<PulseSynth> {
let options = synth_options_from_lua(value)?;
synthesis::synth_from_options(name, options).map_err(mlua::Error::from)
}
fn synth_options_from_lua(value: Value) -> mlua::Result<SynthOptions> {
match value {
Value::Nil => Ok(SynthOptions::Default),
Value::String(value) => Ok(SynthOptions::Preset(value.to_str()?.as_ref().to_string())),
Value::Table(table) if table.raw_len() > 0 => {
let harmonics = table
.sequence_values::<f32>()
.collect::<mlua::Result<Vec<_>>>()?;
Ok(SynthOptions::Harmonics(harmonics))
}
Value::Table(table) => {
let mut options = std::collections::BTreeMap::new();
for pair in table.pairs::<Value, Value>() {
let (key, value) = pair?;
let Value::String(key) = key else {
return Err(mlua::Error::FromLuaConversionError {
from: key.type_name(),
to: "string".to_string(),
message: Some("synth option names must be strings".to_string()),
});
};
options.insert(
key.to_str()?.as_ref().to_string(),
synth_option_from_lua(value)?,
);
}
Ok(SynthOptions::Params(options))
}
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "nil, preset string, or table".to_string(),
message: Some("synth options must be nil, preset string, or table".to_string()),
}),
}
}
fn synth_option_from_lua(value: Value) -> mlua::Result<SynthOption> {
match value {
Value::Integer(value) => Ok(SynthOption::Integer(value)),
Value::Number(value) => Ok(SynthOption::Number(value as f32)),
Value::String(value) => Ok(SynthOption::Text(value.to_str()?.as_ref().to_string())),
Value::Table(table) if table.raw_len() > 0 => {
let values = table
.sequence_values::<f32>()
.collect::<mlua::Result<Vec<_>>>()?;
Ok(SynthOption::Numbers(values))
}
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "number, integer, string, or number array".to_string(),
message: Some(
"synth table values accept numbers, integers, strings, and number arrays"
.to_string(),
),
}),
}
}
fn generator_info_to_lua(lua: &Lua, info: &generators::GeneratorInfo) -> mlua::Result<Table> {
let table = lua.create_table()?;
table.set("name", info.name)?;
table.set("category", info.category)?;
table.set("output", info.output)?;
table.set(
"parameters",
lua.create_sequence_from(info.parameters.to_vec())?,
)?;
Ok(table)
}
fn generator_options_from_lua(value: Value) -> mlua::Result<GeneratorOptions> {
match value {
Value::Nil => Ok(GeneratorOptions::new()),
Value::Table(table) => {
let mut options = GeneratorOptions::new();
for pair in table.pairs::<Value, Value>() {
let (key, value) = pair?;
let Value::String(key) = key else {
return Err(mlua::Error::FromLuaConversionError {
from: key.type_name(),
to: "string".to_string(),
message: Some("generator option names must be strings".to_string()),
});
};
options.insert(key.to_str()?.as_ref(), generator_option_from_lua(value)?);
}
Ok(options)
}
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "table or nil".to_string(),
message: Some("generate expects an options table or nil".to_string()),
}),
}
}
fn generator_option_from_lua(value: Value) -> mlua::Result<GeneratorOption> {
match value {
Value::Integer(value) => Ok(GeneratorOption::Integer(value)),
Value::Number(value) => Ok(GeneratorOption::Number(value)),
Value::Boolean(value) => Ok(GeneratorOption::Boolean(value)),
Value::String(value) => Ok(GeneratorOption::Text(value.to_str()?.as_ref().to_string())),
Value::Table(table) => generator_table_option_from_lua(table),
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "generator option".to_string(),
message: Some(
"generator options accept numbers, booleans, strings, integer arrays, and string maps"
.to_string(),
),
}),
}
}
fn generator_table_option_from_lua(table: Table) -> mlua::Result<GeneratorOption> {
if table.raw_len() > 0 {
let values = table
.sequence_values::<u32>()
.collect::<mlua::Result<Vec<_>>>()?;
return Ok(GeneratorOption::IntegerList(values));
}
let mut values = std::collections::BTreeMap::new();
for pair in table.pairs::<Value, Value>() {
let (key, value) = pair?;
let Value::String(key) = key else {
return Err(mlua::Error::FromLuaConversionError {
from: key.type_name(),
to: "string".to_string(),
message: Some("generator map option names must be strings".to_string()),
});
};
let Value::String(value) = value else {
return Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "string".to_string(),
message: Some("generator map option values must be strings".to_string()),
});
};
values.insert(
key.to_str()?.as_ref().to_string(),
value.to_str()?.as_ref().to_string(),
);
}
Ok(GeneratorOption::TextMap(values))
}
fn number_sequence_from_lua(table: Table) -> mlua::Result<Vec<f32>> {
table.sequence_values::<f32>().collect::<mlua::Result<_>>()
}
fn chord_sequence_from_lua(table: Table) -> mlua::Result<Vec<Vec<f32>>> {
table
.sequence_values::<Table>()
.map(|entry| number_sequence_from_lua(entry?))
.collect::<mlua::Result<_>>()
}
fn generated_to_lua(lua: &Lua, generated: PulseGenerated) -> mlua::Result<Value> {
match generated {
PulseGenerated::Numbers(values) => Ok(Value::Table(lua.create_sequence_from(values)?)),
PulseGenerated::Integers(values) => Ok(Value::Table(lua.create_sequence_from(values)?)),
PulseGenerated::Steps(values) => Ok(Value::Table(lua.create_sequence_from(values)?)),
PulseGenerated::NumberMatrix(rows) => Ok(Value::Table(number_matrix_to_lua(lua, rows)?)),
PulseGenerated::IntegerMatrix(rows) => Ok(Value::Table(integer_matrix_to_lua(lua, rows)?)),
PulseGenerated::StepMatrix(rows) => Ok(Value::Table(step_matrix_to_lua(lua, rows)?)),
PulseGenerated::Text(value) => Ok(Value::String(lua.create_string(&value)?)),
}
}
fn number_matrix_to_lua(lua: &Lua, rows: Vec<Vec<f32>>) -> mlua::Result<Table> {
let table = lua.create_table()?;
for (index, row) in rows.into_iter().enumerate() {
table.set(index + 1, lua.create_sequence_from(row)?)?;
}
Ok(table)
}
fn integer_matrix_to_lua(lua: &Lua, rows: Vec<Vec<u32>>) -> mlua::Result<Table> {
let table = lua.create_table()?;
for (index, row) in rows.into_iter().enumerate() {
table.set(index + 1, lua.create_sequence_from(row)?)?;
}
Ok(table)
}
fn step_matrix_to_lua(lua: &Lua, rows: Vec<Vec<usize>>) -> mlua::Result<Table> {
let table = lua.create_table()?;
for (index, row) in rows.into_iter().enumerate() {
table.set(index + 1, lua.create_sequence_from(row)?)?;
}
Ok(table)
}
fn transpose_lua_value(lua: &Lua, value: Value, semitones: i32) -> mlua::Result<Value> {
match value {
Value::Number(number) => Ok(Value::Number(theory::transpose_frequency(
number as f32,
semitones,
) as f64)),
Value::Integer(integer) => Ok(Value::Number(theory::transpose_frequency(
integer as f32,
semitones,
) as f64)),
Value::Table(table) => {
let notes: Vec<f32> = table
.sequence_values::<f32>()
.collect::<mlua::Result<_>>()?;
let shifted = theory::transpose_notes(¬es, semitones);
Ok(Value::Table(lua.create_sequence_from(shifted)?))
}
other => Err(mlua::Error::FromLuaConversionError {
from: other.type_name(),
to: "number or table".to_string(),
message: Some("transpose expects a frequency or a sequence of frequencies".to_string()),
}),
}
}