Skip to main content

mlua_pulse/
lua.rs

1use crate::composition::{PulseMidiClip, PulsePhrase, PulseSampleClip, PulseSequence, PulseSong};
2use crate::drums::{self, PulseDrumGrid, PulseDrumMachine};
3use crate::effects::{self, EffectOption, EffectOptions, PulseEffect};
4use crate::export;
5use crate::export::{ExportOptions, NormalizeOptions};
6use crate::files::{self, PulseImportedMidi};
7use crate::generators::{self, GeneratorOption, GeneratorOptions, PulseGenerated};
8use crate::instruments;
9use crate::midi;
10use crate::playback::{PulsePlayable, PulsePlaybackEngine, PulsePlaybackHandle};
11use crate::rhythm;
12use crate::synthesis::{self, PulseSynth, SynthOption, SynthOptions};
13use crate::theory;
14use mlua::{Function, Lua, Table, UserData, UserDataMethods, Value, Variadic};
15
16const DEFAULT_PCM_CHUNK_FRAMES: usize = 1024;
17
18/// Builds the Lua-facing `pulse` module table.
19pub fn register(lua: &Lua) -> mlua::Result<Table> {
20    let table = lua.create_table()?;
21    table.set("_VERSION", env!("CARGO_PKG_VERSION"))?;
22
23    table.set(
24        "note",
25        lua.create_function(|_, name: String| {
26            theory::parse_note_frequency(&name).map_err(mlua::Error::from)
27        })?,
28    )?;
29
30    table.set(
31        "modes",
32        lua.create_function(|lua, ()| lua.create_sequence_from(theory::mode_names().to_vec()))?,
33    )?;
34
35    table.set(
36        "chord_kinds",
37        lua.create_function(|lua, ()| {
38            lua.create_sequence_from(theory::chord_kind_names().to_vec())
39        })?,
40    )?;
41
42    table.set(
43        "scale",
44        lua.create_function(|lua, (root, mode): (String, String)| {
45            let notes = theory::scale(&root, &mode).map_err(mlua::Error::from)?;
46            lua.create_sequence_from(notes)
47        })?,
48    )?;
49
50    table.set(
51        "chord",
52        lua.create_function(|lua, (root, kind): (String, String)| {
53            let notes = theory::chord(&root, &kind).map_err(mlua::Error::from)?;
54            lua.create_sequence_from(notes)
55        })?,
56    )?;
57
58    table.set(
59        "transpose",
60        lua.create_function(|lua, (value, semitones): (Value, i32)| {
61            transpose_lua_value(lua, value, semitones)
62        })?,
63    )?;
64
65    table.set(
66        "frequency_to_midi_note",
67        lua.create_function(|_, frequency: f32| {
68            midi::frequency_to_midi_note(frequency).map_err(mlua::Error::from)
69        })?,
70    )?;
71
72    table.set(
73        "midi_note_to_frequency",
74        lua.create_function(|_, note: i64| {
75            midi::midi_note_to_frequency(note).map_err(mlua::Error::from)
76        })?,
77    )?;
78
79    table.set(
80        "export_formats",
81        lua.create_function(|lua, ()| lua.create_sequence_from(files::export_formats().to_vec()))?,
82    )?;
83
84    table.set(
85        "instruments",
86        lua.create_function(|lua, ()| {
87            lua.create_sequence_from(instruments::instrument_names().to_vec())
88        })?,
89    )?;
90
91    table.set(
92        "drums",
93        lua.create_function(|lua, ()| lua.create_sequence_from(drums::drum_names().to_vec()))?,
94    )?;
95
96    table.set(
97        "effects",
98        lua.create_function(|lua, ()| lua.create_sequence_from(effects::effect_names()))?,
99    )?;
100
101    table.set(
102        "effect_info",
103        lua.create_function(|lua, name: String| {
104            let info = effects::effect_info(&name).map_err(mlua::Error::from)?;
105            effect_info_to_lua(lua, info)
106        })?,
107    )?;
108
109    table.set(
110        "synths",
111        lua.create_function(|lua, ()| lua.create_sequence_from(synthesis::synth_names()))?,
112    )?;
113
114    table.set(
115        "synth_info",
116        lua.create_function(|lua, name: String| {
117            let info = synthesis::synth_info(&name).map_err(mlua::Error::from)?;
118            synth_info_to_lua(lua, info)
119        })?,
120    )?;
121
122    table.set(
123        "generators",
124        lua.create_function(|lua, ()| lua.create_sequence_from(generators::generator_names()))?,
125    )?;
126
127    table.set(
128        "generator_info",
129        lua.create_function(|lua, name: String| {
130            let info = generators::generator_info(&name).map_err(mlua::Error::from)?;
131            generator_info_to_lua(lua, info)
132        })?,
133    )?;
134
135    table.set(
136        "generate",
137        lua.create_function(|lua, (name, options): (String, Value)| {
138            let options = generator_options_from_lua(options)?;
139            let generated = generators::generate(&name, &options).map_err(mlua::Error::from)?;
140            generated_to_lua(lua, generated)
141        })?,
142    )?;
143
144    table.set(
145        "normalize",
146        lua.create_function(|lua, (values, min, max): (Table, f32, f32)| {
147            let values = number_sequence_from_lua(values)?;
148            let normalized =
149                generators::normalize_values(&values, min, max).map_err(mlua::Error::from)?;
150            lua.create_sequence_from(normalized)
151        })?,
152    )?;
153
154    table.set(
155        "map_to_scale",
156        lua.create_function(
157            |lua, (values, scale, root, octaves): (Table, String, String, u32)| {
158                let values = number_sequence_from_lua(values)?;
159                let notes = generators::map_values_to_scale(&values, &scale, &root, octaves)
160                    .map_err(mlua::Error::from)?;
161                lua.create_sequence_from(notes)
162            },
163        )?,
164    )?;
165
166    table.set(
167        "euclidean",
168        lua.create_function(|lua, (pulses, steps): (usize, usize)| {
169            let pattern = rhythm::try_euclidean(pulses, steps).map_err(mlua::Error::from)?;
170            lua.create_sequence_from(pattern)
171        })?,
172    )?;
173
174    table.set(
175        "euclidean_pattern",
176        lua.create_function(|lua, (pulses, steps): (usize, usize)| {
177            let pattern =
178                rhythm::try_euclidean_pattern(pulses, steps).map_err(mlua::Error::from)?;
179            lua.create_sequence_from(pattern)
180        })?,
181    )?;
182
183    table.set(
184        "pattern",
185        lua.create_function(|lua, name: String| {
186            let pattern = rhythm::named_pattern(&name).map_err(mlua::Error::from)?;
187            lua.create_sequence_from(pattern)
188        })?,
189    )?;
190
191    table.set(
192        "drum_grid",
193        lua.create_function(|_, ()| Ok(PulseDrumGrid::new()))?,
194    )?;
195
196    table.set(
197        "drum_machine",
198        lua.create_function(|_, kit: String| {
199            PulseDrumMachine::new(&kit).map_err(mlua::Error::from)
200        })?,
201    )?;
202
203    table.set(
204        "sequence",
205        lua.create_function(|_, ()| Ok(PulseSequence::new()))?,
206    )?;
207    table.set(
208        "phrase",
209        lua.create_function(|_, ()| Ok(PulsePhrase::new()))?,
210    )?;
211    table.set(
212        "sample",
213        lua.create_function(|_, path: String| Ok(PulseSampleClip::new(path)))?,
214    )?;
215    table.set("song", lua.create_function(|_, ()| Ok(PulseSong::new()))?)?;
216    table.set(
217        "playback",
218        lua.create_function(|_, ()| PulsePlaybackEngine::new().map_err(mlua::Error::from))?,
219    )?;
220    table.set(
221        "import_midi",
222        lua.create_function(|_, path: String| files::import_midi(path).map_err(mlua::Error::from))?,
223    )?;
224    table.set(
225        "stream_pcm",
226        lua.create_function(|lua, args: Variadic<Value>| stream_pcm_value_to_lua(lua, args))?,
227    )?;
228
229    Ok(table)
230}
231
232/// Registers `pulse` so Lua code can call `require("pulse")`.
233pub fn register_package_loader(lua: &Lua) -> mlua::Result<()> {
234    let module = register(lua)?;
235    lua.register_module("pulse", module)
236}
237
238impl UserData for PulseSequence {
239    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
240        methods.add_method_mut("notes", |_, this, table: Table| {
241            let notes = table
242                .sequence_values::<f32>()
243                .collect::<mlua::Result<Vec<_>>>()?;
244            *this = this.clone().with_notes(notes);
245            Ok(this.clone())
246        });
247
248        methods.add_method_mut("chords", |_, this, table: Table| {
249            let chords = chord_sequence_from_lua(table)?;
250            *this = this.clone().with_chords(chords);
251            Ok(this.clone())
252        });
253
254        methods.add_method_mut("durations", |_, this, table: Table| {
255            let durations = table
256                .sequence_values::<f32>()
257                .collect::<mlua::Result<Vec<_>>>()?;
258            *this = this.clone().with_durations(durations);
259            Ok(this.clone())
260        });
261
262        methods.add_method_mut("instrument", |_, this, instrument: String| {
263            *this = this.clone().with_instrument(instrument);
264            Ok(this.clone())
265        });
266
267        methods.add_method_mut("at", |_, this, start_at: f32| {
268            *this = this
269                .clone()
270                .with_start_at(start_at)
271                .map_err(mlua::Error::from)?;
272            Ok(this.clone())
273        });
274
275        methods.add_method_mut("volume", |_, this, volume: f32| {
276            *this = this
277                .clone()
278                .with_volume(volume)
279                .map_err(mlua::Error::from)?;
280            Ok(this.clone())
281        });
282
283        methods.add_method_mut("pan", |_, this, pan: f32| {
284            *this = this.clone().with_pan(pan).map_err(mlua::Error::from)?;
285            Ok(this.clone())
286        });
287
288        methods.add_method_mut("velocity", |_, this, velocity: f32| {
289            *this = this
290                .clone()
291                .with_velocity(velocity)
292                .map_err(mlua::Error::from)?;
293            Ok(this.clone())
294        });
295
296        methods.add_method_mut("effect", |_, this, (name, options): (String, Value)| {
297            let effect = effect_from_lua_value(&name, options)?;
298            *this = this.clone().with_effect(effect);
299            Ok(this.clone())
300        });
301
302        methods.add_method_mut("synth", |_, this, (name, options): (String, Value)| {
303            let synth = synth_from_lua_value(&name, options)?;
304            *this = this.clone().with_synth(synth);
305            Ok(this.clone())
306        });
307
308        methods.add_method("transpose", |_, this, semitones: i32| {
309            Ok(this.transposed(semitones))
310        });
311    }
312}
313
314impl UserData for PulseSong {
315    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
316        methods.add_method_mut("tempo", |_, this, bpm: f32| {
317            *this = this.clone().with_tempo(bpm).map_err(mlua::Error::from)?;
318            Ok(this.clone())
319        });
320
321        methods.add_method_mut("add", |_, this, sequence: mlua::AnyUserData| {
322            let sequence = sequence.borrow::<PulseSequence>()?.clone();
323            *this = this.clone().add_sequence(sequence);
324            Ok(this.clone())
325        });
326
327        methods.add_method_mut("add_drum_grid", |_, this, grid: mlua::AnyUserData| {
328            let grid = grid.borrow::<PulseDrumGrid>()?.clone();
329            *this = this.clone().add_drum_grid(grid);
330            Ok(this.clone())
331        });
332
333        methods.add_method_mut("add_phrase", |_, this, phrase: mlua::AnyUserData| {
334            let phrase = phrase.borrow::<PulsePhrase>()?.clone();
335            *this = this.clone().add_phrase(phrase);
336            Ok(this.clone())
337        });
338
339        methods.add_method_mut("add_sample", |_, this, sample_clip: mlua::AnyUserData| {
340            let sample_clip = sample_clip.borrow::<PulseSampleClip>()?.clone();
341            *this = this.clone().add_sample_clip(sample_clip);
342            Ok(this.clone())
343        });
344
345        methods.add_method_mut("add_midi", |_, this, midi: mlua::AnyUserData| {
346            let midi_clip = midi_clip_from_userdata(midi)?;
347            *this = this.clone().add_midi_clip(midi_clip);
348            Ok(this.clone())
349        });
350
351        methods.add_method_mut(
352            "master_effect",
353            |_, this, (name, options): (String, Value)| {
354                let effect = effect_from_lua_value(&name, options)?;
355                if !effect.allowed_on_master() {
356                    return Err(mlua::Error::from(
357                        crate::error::PulseError::InvalidEffectScope {
358                            effect: effect.name().to_string(),
359                            scope: "master".to_string(),
360                        },
361                    ));
362                }
363                *this = this.clone().with_master_effect(effect);
364                Ok(this.clone())
365            },
366        );
367
368        methods.add_method("export_wav", |_, this, args: Variadic<Value>| {
369            let (path, options) = export_args_from_lua(args)?;
370            export::export_wav_with_options(this, path, options).map_err(mlua::Error::from)
371        });
372
373        methods.add_method("export", |_, this, args: Variadic<Value>| {
374            let (path, options) = export_args_from_lua(args)?;
375            files::export_with_options(this, path, options).map_err(mlua::Error::from)
376        });
377
378        methods.add_method("export_flac", |_, this, args: Variadic<Value>| {
379            let (path, options) = export_args_from_lua(args)?;
380            files::export_flac_with_options(this, path, options).map_err(mlua::Error::from)
381        });
382
383        methods.add_method("export_midi", |_, this, args: Variadic<Value>| {
384            let (path, options) = export_args_from_lua(args)?;
385            files::export_midi_with_options(this, path, options).map_err(mlua::Error::from)
386        });
387
388        methods.add_method("stream_pcm", |lua, this, args: Variadic<Value>| {
389            let (callback, options, chunk_frames) = stream_pcm_args_from_lua(args)?;
390            let mut mixer = this.to_mixer().map_err(mlua::Error::from)?;
391            stream_pcm_mixer_to_lua(lua, &mut mixer, callback, options, chunk_frames)
392        });
393
394        methods.add_method("play", |_, this, ()| {
395            let engine = PulsePlaybackEngine::new().map_err(mlua::Error::from)?;
396            engine
397                .play(PulsePlayable::Song(this))
398                .map_err(mlua::Error::from)
399        });
400
401        methods.add_method("play_at_rate", |_, this, rate: f32| {
402            let engine = PulsePlaybackEngine::new().map_err(mlua::Error::from)?;
403            engine
404                .play_at_rate(PulsePlayable::Song(this), rate)
405                .map_err(mlua::Error::from)
406        });
407    }
408}
409
410impl UserData for PulsePhrase {
411    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
412        methods.add_method_mut("add", |_, this, sequence: mlua::AnyUserData| {
413            let sequence = sequence.borrow::<PulseSequence>()?.clone();
414            *this = this.clone().add_sequence(sequence);
415            Ok(this.clone())
416        });
417
418        methods.add_method_mut("add_drum_grid", |_, this, grid: mlua::AnyUserData| {
419            let grid = grid.borrow::<PulseDrumGrid>()?.clone();
420            *this = this.clone().add_drum_grid(grid);
421            Ok(this.clone())
422        });
423
424        methods.add_method_mut("add_sample", |_, this, sample_clip: mlua::AnyUserData| {
425            let sample_clip = sample_clip.borrow::<PulseSampleClip>()?.clone();
426            *this = this.clone().add_sample_clip(sample_clip);
427            Ok(this.clone())
428        });
429
430        methods.add_method_mut("add_midi", |_, this, midi: mlua::AnyUserData| {
431            let midi_clip = midi_clip_from_userdata(midi)?;
432            *this = this.clone().add_midi_clip(midi_clip);
433            Ok(this.clone())
434        });
435
436        methods.add_method_mut("repeat_times", |_, this, repeat_times: Value| {
437            let repeat_times = repeat_times_from_lua(repeat_times)?;
438            *this = this
439                .clone()
440                .with_repeat_times(repeat_times)
441                .map_err(mlua::Error::from)?;
442            Ok(this.clone())
443        });
444    }
445}
446
447fn midi_clip_from_userdata(userdata: mlua::AnyUserData) -> mlua::Result<PulseMidiClip> {
448    if let Ok(midi_clip) = userdata.borrow::<PulseMidiClip>() {
449        return Ok(midi_clip.clone());
450    }
451
452    if let Ok(imported) = userdata.borrow::<PulseImportedMidi>() {
453        return Ok(imported.as_clip());
454    }
455
456    Err(mlua::Error::FromLuaConversionError {
457        from: "userdata",
458        to: "PulseMidiClip or PulseImportedMidi".to_string(),
459        message: Some("add_midi expects an imported MIDI value or MIDI clip".to_string()),
460    })
461}
462
463impl UserData for PulseSampleClip {
464    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
465        methods.add_method_mut("rate", |_, this, playback_rate: f32| {
466            *this = this
467                .clone()
468                .with_playback_rate(playback_rate)
469                .map_err(mlua::Error::from)?;
470            Ok(this.clone())
471        });
472
473        methods.add_method_mut("gain", |_, this, gain: f32| {
474            *this = this.clone().with_gain(gain).map_err(mlua::Error::from)?;
475            Ok(this.clone())
476        });
477
478        methods.add_method_mut("pitch_shift", |_, this, semitones: f32| {
479            *this = this
480                .clone()
481                .with_pitch_shift(semitones)
482                .map_err(mlua::Error::from)?;
483            Ok(this.clone())
484        });
485
486        methods.add_method_mut("time_stretch", |_, this, factor: f32| {
487            *this = this
488                .clone()
489                .with_time_stretch(factor)
490                .map_err(mlua::Error::from)?;
491            Ok(this.clone())
492        });
493
494        methods.add_method_mut("at", |_, this, start_at: f32| {
495            *this = this
496                .clone()
497                .with_start_at(start_at)
498                .map_err(mlua::Error::from)?;
499            Ok(this.clone())
500        });
501
502        methods.add_method_mut("track", |_, this, track_name: String| {
503            *this = this.clone().with_track(track_name);
504            Ok(this.clone())
505        });
506
507        methods.add_method_mut("volume", |_, this, volume: f32| {
508            *this = this
509                .clone()
510                .with_volume(volume)
511                .map_err(mlua::Error::from)?;
512            Ok(this.clone())
513        });
514
515        methods.add_method_mut("pan", |_, this, pan: f32| {
516            *this = this.clone().with_pan(pan).map_err(mlua::Error::from)?;
517            Ok(this.clone())
518        });
519
520        methods.add_method_mut("effect", |_, this, (name, options): (String, Value)| {
521            let effect = effect_from_lua_value(&name, options)?;
522            *this = this.clone().with_effect(effect);
523            Ok(this.clone())
524        });
525
526        methods.add_method_mut("slice", |_, this, (start, end): (f32, f32)| {
527            *this = this
528                .clone()
529                .with_slice(start, end)
530                .map_err(mlua::Error::from)?;
531            Ok(this.clone())
532        });
533
534        methods.add_method_mut("reverse", |_, this, ()| {
535            *this = this.clone().with_reverse();
536            Ok(this.clone())
537        });
538
539        methods.add_method_mut("loop_for", |_, this, duration: f32| {
540            *this = this
541                .clone()
542                .with_loop_for(duration)
543                .map_err(mlua::Error::from)?;
544            Ok(this.clone())
545        });
546
547        methods.add_method_mut("normalize", |_, this, ()| {
548            *this = this.clone().with_normalize();
549            Ok(this.clone())
550        });
551
552        methods.add_method_mut("fade_in", |_, this, duration: f32| {
553            *this = this
554                .clone()
555                .with_fade_in(duration)
556                .map_err(mlua::Error::from)?;
557            Ok(this.clone())
558        });
559
560        methods.add_method_mut("fade_out", |_, this, duration: f32| {
561            *this = this
562                .clone()
563                .with_fade_out(duration)
564                .map_err(mlua::Error::from)?;
565            Ok(this.clone())
566        });
567    }
568}
569
570impl UserData for PulseMidiClip {
571    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
572        methods.add_method_mut("at", |_, this, start_at: f32| {
573            *this = this
574                .clone()
575                .with_start_at(start_at)
576                .map_err(mlua::Error::from)?;
577            Ok(this.clone())
578        });
579
580        methods.add_method_mut("track", |_, this, track_name: String| {
581            *this = this.clone().with_track(track_name);
582            Ok(this.clone())
583        });
584
585        methods.add_method_mut("volume", |_, this, volume: f32| {
586            *this = this
587                .clone()
588                .with_volume(volume)
589                .map_err(mlua::Error::from)?;
590            Ok(this.clone())
591        });
592
593        methods.add_method_mut("pan", |_, this, pan: f32| {
594            *this = this.clone().with_pan(pan).map_err(mlua::Error::from)?;
595            Ok(this.clone())
596        });
597
598        methods.add_method_mut("repeat_times", |_, this, repeat_times: Value| {
599            let repeat_times = repeat_times_from_lua(repeat_times)?;
600            *this = this
601                .clone()
602                .with_repeat_times(repeat_times)
603                .map_err(mlua::Error::from)?;
604            Ok(this.clone())
605        });
606
607        methods.add_method_mut("effect", |_, this, (name, options): (String, Value)| {
608            let effect = effect_from_lua_value(&name, options)?;
609            *this = this.clone().with_effect(effect);
610            Ok(this.clone())
611        });
612    }
613}
614
615impl UserData for PulsePlaybackEngine {
616    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
617        methods.add_method("play", |_, this, userdata: mlua::AnyUserData| {
618            with_playable_userdata(userdata, |playable| {
619                this.play(playable).map_err(mlua::Error::from)
620            })
621        });
622
623        methods.add_method("play_realtime", |_, this, userdata: mlua::AnyUserData| {
624            with_playable_userdata(userdata, |playable| {
625                this.play_realtime(playable).map_err(mlua::Error::from)
626            })
627        });
628
629        methods.add_method("play_looping", |_, this, userdata: mlua::AnyUserData| {
630            with_playable_userdata(userdata, |playable| {
631                this.play_looping(playable).map_err(mlua::Error::from)
632            })
633        });
634
635        methods.add_method(
636            "play_at_rate",
637            |_, this, (userdata, rate): (mlua::AnyUserData, f32)| {
638                with_playable_userdata(userdata, |playable| {
639                    this.play_at_rate(playable, rate).map_err(mlua::Error::from)
640                })
641            },
642        );
643
644        methods.add_method(
645            "play_realtime_at_rate",
646            |_, this, (userdata, rate): (mlua::AnyUserData, f32)| {
647                with_playable_userdata(userdata, |playable| {
648                    this.play_realtime_at_rate(playable, rate)
649                        .map_err(mlua::Error::from)
650                })
651            },
652        );
653    }
654}
655
656impl UserData for PulsePlaybackHandle {
657    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
658        methods.add_method("stop", |_, this, ()| this.stop().map_err(mlua::Error::from));
659
660        methods.add_method("volume", |_, this, volume: f32| {
661            this.volume(volume).map_err(mlua::Error::from)
662        });
663
664        methods.add_method("pan", |_, this, pan: f32| {
665            this.pan(pan).map_err(mlua::Error::from)
666        });
667
668        methods.add_method("rate", |_, this, rate: f32| {
669            this.rate(rate).map_err(mlua::Error::from)
670        });
671
672        methods.add_method("pause", |_, this, ()| {
673            this.pause().map_err(mlua::Error::from)
674        });
675
676        methods.add_method("resume", |_, this, ()| {
677            this.resume().map_err(mlua::Error::from)
678        });
679    }
680}
681
682impl UserData for PulseImportedMidi {
683    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
684        methods.add_method("at", |_, this, start_at: f32| {
685            this.as_clip()
686                .with_start_at(start_at)
687                .map_err(mlua::Error::from)
688        });
689
690        methods.add_method("track", |_, this, track_name: String| {
691            Ok(this.as_clip().with_track(track_name))
692        });
693
694        methods.add_method("volume", |_, this, volume: f32| {
695            this.as_clip()
696                .with_volume(volume)
697                .map_err(mlua::Error::from)
698        });
699
700        methods.add_method("pan", |_, this, pan: f32| {
701            this.as_clip().with_pan(pan).map_err(mlua::Error::from)
702        });
703
704        methods.add_method("repeat_times", |_, this, repeat_times: Value| {
705            let repeat_times = repeat_times_from_lua(repeat_times)?;
706            this.as_clip()
707                .with_repeat_times(repeat_times)
708                .map_err(mlua::Error::from)
709        });
710
711        methods.add_method("effect", |_, this, (name, options): (String, Value)| {
712            let effect = effect_from_lua_value(&name, options)?;
713            Ok(this.as_clip().with_effect(effect))
714        });
715
716        methods.add_method("export_wav", |_, this, args: Variadic<Value>| {
717            let (path, options) = export_args_from_lua(args)?;
718            this.export_wav_with_options(path, options)
719                .map_err(mlua::Error::from)
720        });
721
722        methods.add_method("export", |_, this, args: Variadic<Value>| {
723            let (path, options) = export_args_from_lua(args)?;
724            this.export_with_options(path, options)
725                .map_err(mlua::Error::from)
726        });
727
728        methods.add_method("export_flac", |_, this, args: Variadic<Value>| {
729            let (path, options) = export_args_from_lua(args)?;
730            this.export_flac_with_options(path, options)
731                .map_err(mlua::Error::from)
732        });
733
734        methods.add_method("export_midi", |_, this, args: Variadic<Value>| {
735            let (path, options) = export_args_from_lua(args)?;
736            this.export_midi_with_options(path, options)
737                .map_err(mlua::Error::from)
738        });
739
740        methods.add_method("stream_pcm", |lua, this, args: Variadic<Value>| {
741            let (callback, options, chunk_frames) = stream_pcm_args_from_lua(args)?;
742            let mut mixer = this.cloned_mixer();
743            stream_pcm_mixer_to_lua(lua, &mut mixer, callback, options, chunk_frames)
744        });
745    }
746}
747
748#[derive(Debug, Clone)]
749struct PulsePcmChunk {
750    samples: Vec<i16>,
751    sample_rate: u32,
752}
753
754impl PulsePcmChunk {
755    fn new(samples: &[i16], sample_rate: u32) -> Self {
756        Self {
757            samples: samples.to_vec(),
758            sample_rate,
759        }
760    }
761
762    fn frames(&self) -> usize {
763        self.samples.len() / export::PCM_CHANNELS
764    }
765
766    fn bytes(&self) -> usize {
767        self.samples.len() * std::mem::size_of::<i16>()
768    }
769}
770
771impl UserData for PulsePcmChunk {
772    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
773        methods.add_method("frames", |_, this, ()| Ok(this.frames()));
774
775        methods.add_method("samples", |_, this, ()| Ok(this.samples.len()));
776
777        methods.add_method("bytes", |_, this, ()| Ok(this.bytes()));
778
779        methods.add_method("sample_rate", |_, this, ()| Ok(this.sample_rate));
780
781        methods.add_method("channels", |_, _, ()| Ok(export::PCM_CHANNELS));
782
783        methods.add_method("format", |_, _, ()| Ok("i16le"));
784
785        methods.add_method("sample", |_, this, index: usize| {
786            let Some(index) = index.checked_sub(1) else {
787                return Ok(None);
788            };
789            Ok(this.samples.get(index).copied())
790        });
791    }
792}
793
794impl UserData for PulseDrumGrid {
795    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
796        methods.add_method_mut("track", |_, this, track_name: String| {
797            *this = this.clone().with_track(track_name);
798            Ok(this.clone())
799        });
800
801        methods.add_method_mut("steps", |_, this, steps: Value| {
802            let steps = drum_steps_from_lua(steps)?;
803            *this = this.clone().with_steps(steps).map_err(mlua::Error::from)?;
804            Ok(this.clone())
805        });
806
807        methods.add_method_mut("step_duration", |_, this, duration: f32| {
808            *this = this
809                .clone()
810                .with_step_duration(duration)
811                .map_err(mlua::Error::from)?;
812            Ok(this.clone())
813        });
814
815        methods.add_method_mut("sound", |_, this, (drum, pattern): (String, Value)| {
816            let pattern = pattern_from_lua_value(pattern)?;
817            *this = this
818                .clone()
819                .sound(drum, pattern)
820                .map_err(mlua::Error::from)?;
821            Ok(this.clone())
822        });
823
824        methods.add_method_mut("accent", |_, this, pattern: Value| {
825            let pattern = pattern_from_lua_value(pattern)?;
826            *this = this.clone().accent(pattern);
827            Ok(this.clone())
828        });
829
830        methods.add_method_mut("velocity", |_, this, table: Table| {
831            let velocities = table
832                .sequence_values::<f32>()
833                .collect::<mlua::Result<Vec<_>>>()?;
834            *this = this.clone().velocity(velocities);
835            Ok(this.clone())
836        });
837
838        methods.add_method_mut("volume", |_, this, volume: f32| {
839            *this = this
840                .clone()
841                .with_volume(volume)
842                .map_err(mlua::Error::from)?;
843            Ok(this.clone())
844        });
845
846        methods.add_method_mut("pan", |_, this, pan: f32| {
847            *this = this.clone().with_pan(pan).map_err(mlua::Error::from)?;
848            Ok(this.clone())
849        });
850
851        methods.add_method_mut("repeat_times", |_, this, repeat_times: Value| {
852            let repeat_times = repeat_times_from_lua(repeat_times)?;
853            *this = this
854                .clone()
855                .with_repeat_times(repeat_times)
856                .map_err(mlua::Error::from)?;
857            Ok(this.clone())
858        });
859
860        methods.add_method_mut("effect", |_, this, (name, options): (String, Value)| {
861            let effect = effect_from_lua_value(&name, options)?;
862            *this = this.clone().with_effect(effect);
863            Ok(this.clone())
864        });
865    }
866}
867
868impl UserData for PulseDrumMachine {
869    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
870        methods.add_method_mut("steps", |_, this, steps: Value| {
871            let steps = drum_steps_from_lua(steps)?;
872            *this = this.clone().steps(steps).map_err(mlua::Error::from)?;
873            Ok(this.clone())
874        });
875
876        methods.add_method_mut("step_duration", |_, this, duration: f32| {
877            *this = this
878                .clone()
879                .step_duration(duration)
880                .map_err(mlua::Error::from)?;
881            Ok(this.clone())
882        });
883
884        methods.add_method_mut("kick", |_, this, pattern: Value| {
885            let pattern = pattern_from_lua_value(pattern)?;
886            *this = this.clone().kick(pattern).map_err(mlua::Error::from)?;
887            Ok(this.clone())
888        });
889
890        methods.add_method_mut("snare", |_, this, pattern: Value| {
891            let pattern = pattern_from_lua_value(pattern)?;
892            *this = this.clone().snare(pattern).map_err(mlua::Error::from)?;
893            Ok(this.clone())
894        });
895
896        methods.add_method_mut("hihat", |_, this, pattern: Value| {
897            let pattern = pattern_from_lua_value(pattern)?;
898            *this = this.clone().hihat(pattern).map_err(mlua::Error::from)?;
899            Ok(this.clone())
900        });
901
902        methods.add_method_mut("open_hihat", |_, this, pattern: Value| {
903            let pattern = pattern_from_lua_value(pattern)?;
904            *this = this
905                .clone()
906                .open_hihat(pattern)
907                .map_err(mlua::Error::from)?;
908            Ok(this.clone())
909        });
910
911        methods.add_method_mut("clap", |_, this, pattern: Value| {
912            let pattern = pattern_from_lua_value(pattern)?;
913            *this = this.clone().clap(pattern).map_err(mlua::Error::from)?;
914            Ok(this.clone())
915        });
916
917        methods.add_method("grid", |_, this, ()| Ok(this.clone().grid()));
918    }
919}
920
921fn pattern_from_lua_value(value: Value) -> mlua::Result<Vec<usize>> {
922    match value {
923        Value::String(pattern) => {
924            let pattern = pattern.to_str()?;
925            Ok(rhythm::steps_from_pattern_string(pattern.as_ref()))
926        }
927        Value::Table(table) => table
928            .sequence_values::<usize>()
929            .collect::<mlua::Result<_>>(),
930        other => Err(mlua::Error::FromLuaConversionError {
931            from: other.type_name(),
932            to: "table or pattern string".to_string(),
933            message: Some(
934                "pattern expects a table of step indices or a pattern string".to_string(),
935            ),
936        }),
937    }
938}
939
940fn export_args_from_lua(args: Variadic<Value>) -> mlua::Result<(String, ExportOptions)> {
941    let mut args = args.into_iter();
942    let path = match args.next() {
943        Some(Value::String(path)) => path.to_str()?.as_ref().to_string(),
944        Some(other) => {
945            return Err(mlua::Error::FromLuaConversionError {
946                from: other.type_name(),
947                to: "string".to_string(),
948                message: Some("export expects a path string".to_string()),
949            });
950        }
951        None => {
952            return Err(mlua::Error::FromLuaConversionError {
953                from: "missing",
954                to: "string".to_string(),
955                message: Some("export expects a path string".to_string()),
956            });
957        }
958    };
959
960    let options = match args.next() {
961        Some(Value::Nil) | None => ExportOptions::new(),
962        Some(Value::Table(table)) => export_options_from_lua(table)?,
963        Some(other) => {
964            return Err(mlua::Error::FromLuaConversionError {
965                from: other.type_name(),
966                to: "table or nil".to_string(),
967                message: Some("export options must be a table or nil".to_string()),
968            });
969        }
970    };
971
972    if args.next().is_some() {
973        return Err(mlua::Error::RuntimeError(
974            "export expects a path and optional options table".to_string(),
975        ));
976    }
977
978    Ok((path, options))
979}
980
981fn stream_pcm_args_from_lua(
982    args: Variadic<Value>,
983) -> mlua::Result<(Function, ExportOptions, usize)> {
984    stream_pcm_args_from_values(args.into_iter().collect())
985}
986
987fn stream_pcm_args_from_values(
988    values: Vec<Value>,
989) -> mlua::Result<(Function, ExportOptions, usize)> {
990    let mut args = values.into_iter();
991    let (callback, options, chunk_frames) = match args.next() {
992        Some(Value::Function(callback)) => {
993            let (options, chunk_frames) = match args.next() {
994                Some(Value::Nil) | None => (ExportOptions::new(), DEFAULT_PCM_CHUNK_FRAMES),
995                Some(Value::Table(table)) => stream_pcm_options_from_lua(table)?,
996                Some(other) => {
997                    return Err(mlua::Error::FromLuaConversionError {
998                        from: other.type_name(),
999                        to: "table or nil".to_string(),
1000                        message: Some("stream_pcm options must be a table or nil".to_string()),
1001                    });
1002                }
1003            };
1004            (callback, options, chunk_frames)
1005        }
1006        Some(Value::Table(table)) => {
1007            let (options, chunk_frames) = stream_pcm_options_from_lua(table)?;
1008            let callback = match args.next() {
1009                Some(Value::Function(callback)) => callback,
1010                Some(other) => {
1011                    return Err(mlua::Error::FromLuaConversionError {
1012                        from: other.type_name(),
1013                        to: "function".to_string(),
1014                        message: Some("stream_pcm expects a callback function".to_string()),
1015                    });
1016                }
1017                None => {
1018                    return Err(mlua::Error::FromLuaConversionError {
1019                        from: "missing",
1020                        to: "function".to_string(),
1021                        message: Some("stream_pcm expects a callback function".to_string()),
1022                    });
1023                }
1024            };
1025            (callback, options, chunk_frames)
1026        }
1027        Some(other) => {
1028            return Err(mlua::Error::FromLuaConversionError {
1029                from: other.type_name(),
1030                to: "function".to_string(),
1031                message: Some("stream_pcm expects a callback function".to_string()),
1032            });
1033        }
1034        None => {
1035            return Err(mlua::Error::FromLuaConversionError {
1036                from: "missing",
1037                to: "function".to_string(),
1038                message: Some("stream_pcm expects a callback function".to_string()),
1039            });
1040        }
1041    };
1042
1043    if args.next().is_some() {
1044        return Err(mlua::Error::RuntimeError(
1045            "stream_pcm expects a callback and optional options table".to_string(),
1046        ));
1047    }
1048
1049    Ok((callback, options, chunk_frames))
1050}
1051
1052fn stream_pcm_options_from_lua(table: Table) -> mlua::Result<(ExportOptions, usize)> {
1053    let chunk_frames =
1054        optional_usize_from_lua(&table, "chunk_frames")?.unwrap_or(DEFAULT_PCM_CHUNK_FRAMES);
1055    if chunk_frames == 0 {
1056        return Err(mlua::Error::FromLuaConversionError {
1057            from: "integer",
1058            to: "positive integer".to_string(),
1059            message: Some("stream_pcm option chunk_frames must be positive".to_string()),
1060        });
1061    }
1062    Ok((export_options_from_lua(table)?, chunk_frames))
1063}
1064
1065fn stream_pcm_value_to_lua(lua: &Lua, args: Variadic<Value>) -> mlua::Result<Option<usize>> {
1066    let mut args = args.into_iter();
1067    let Some(value) = args.next() else {
1068        return Ok(None);
1069    };
1070    let rest = args.collect::<Vec<_>>();
1071    let Value::UserData(userdata) = value else {
1072        return Ok(None);
1073    };
1074
1075    if let Ok(song) = userdata.borrow::<PulseSong>() {
1076        let (callback, options, chunk_frames) = stream_pcm_args_from_values(rest)?;
1077        let mut mixer = song.to_mixer().map_err(mlua::Error::from)?;
1078        return stream_pcm_mixer_to_lua(lua, &mut mixer, callback, options, chunk_frames);
1079    }
1080
1081    if let Ok(imported) = userdata.borrow::<PulseImportedMidi>() {
1082        let (callback, options, chunk_frames) = stream_pcm_args_from_values(rest)?;
1083        let mut mixer = imported.cloned_mixer();
1084        return stream_pcm_mixer_to_lua(lua, &mut mixer, callback, options, chunk_frames);
1085    }
1086
1087    Ok(None)
1088}
1089
1090fn stream_pcm_mixer_to_lua(
1091    lua: &Lua,
1092    mixer: &mut tunes::track::Mixer,
1093    callback: Function,
1094    options: ExportOptions,
1095    chunk_frames: usize,
1096) -> mlua::Result<Option<usize>> {
1097    let (samples, sample_rate) =
1098        export::render_mixer_pcm_i16(mixer, options).map_err(mlua::Error::from)?;
1099    let samples_per_chunk = chunk_frames.saturating_mul(export::PCM_CHANNELS);
1100    if samples_per_chunk == 0 {
1101        return Ok(None);
1102    }
1103
1104    let mut chunk_count = 0;
1105    for chunk in samples.chunks(samples_per_chunk) {
1106        if chunk.is_empty() {
1107            continue;
1108        }
1109        let userdata = lua.create_userdata(PulsePcmChunk::new(chunk, sample_rate))?;
1110        callback.call::<()>(userdata)?;
1111        chunk_count += 1;
1112    }
1113
1114    Ok(Some(chunk_count))
1115}
1116
1117fn export_options_from_lua(table: Table) -> mlua::Result<ExportOptions> {
1118    let gpu = match table.get::<Value>("gpu")? {
1119        Value::Nil => false,
1120        Value::Boolean(value) => value,
1121        other => {
1122            return Err(mlua::Error::FromLuaConversionError {
1123                from: other.type_name(),
1124                to: "boolean".to_string(),
1125                message: Some("export option gpu must be a boolean".to_string()),
1126            });
1127        }
1128    };
1129
1130    let sample_rate = match table.get::<Value>("sample_rate")? {
1131        Value::Nil => None,
1132        Value::Integer(0) => {
1133            return Err(mlua::Error::from(
1134                crate::error::PulseError::InvalidExportSampleRate { sample_rate: 0 },
1135            ));
1136        }
1137        Value::Integer(value) if value < 0 || value > u32::MAX as i64 => {
1138            return Err(mlua::Error::FromLuaConversionError {
1139                from: "integer",
1140                to: "positive integer".to_string(),
1141                message: Some("export option sample_rate must be a positive integer".to_string()),
1142            });
1143        }
1144        Value::Integer(value) => Some(value as u32),
1145        Value::Number(value)
1146            if !value.is_finite()
1147                || value <= 0.0
1148                || value > u32::MAX as f64
1149                || value.fract() != 0.0 =>
1150        {
1151            return Err(mlua::Error::FromLuaConversionError {
1152                from: "number",
1153                to: "positive integer".to_string(),
1154                message: Some("export option sample_rate must be a positive integer".to_string()),
1155            });
1156        }
1157        Value::Number(value) => Some(value as u32),
1158        other => {
1159            return Err(mlua::Error::FromLuaConversionError {
1160                from: other.type_name(),
1161                to: "positive integer".to_string(),
1162                message: Some("export option sample_rate must be a positive integer".to_string()),
1163            });
1164        }
1165    };
1166
1167    let mut options = ExportOptions::new().with_gpu(gpu);
1168    if let Some(sample_rate) = sample_rate {
1169        options = options
1170            .with_sample_rate(sample_rate)
1171            .map_err(mlua::Error::from)?;
1172    }
1173    if let Some(normalize) = normalize_options_from_lua(&table)? {
1174        options = options.with_normalization(normalize);
1175    }
1176    if let Some(bits_per_sample) = optional_u32_from_lua(&table, "flac_bits_per_sample")? {
1177        options = options
1178            .with_flac_bits_per_sample(bits_per_sample)
1179            .map_err(mlua::Error::from)?;
1180    }
1181    if let Some(gain) = optional_f32_from_lua(&table, "midi_velocity_gain")? {
1182        options = options
1183            .with_midi_velocity_gain(gain)
1184            .map_err(mlua::Error::from)?;
1185    }
1186    if let Some(min_velocity) = optional_f32_from_lua(&table, "midi_min_velocity")? {
1187        options = options
1188            .with_midi_min_velocity(min_velocity)
1189            .map_err(mlua::Error::from)?;
1190    }
1191
1192    Ok(options)
1193}
1194
1195fn optional_usize_from_lua(table: &Table, key: &str) -> mlua::Result<Option<usize>> {
1196    match table.get::<Value>(key)? {
1197        Value::Nil => Ok(None),
1198        Value::Integer(value) => {
1199            usize::try_from(value)
1200                .map(Some)
1201                .map_err(|_| mlua::Error::FromLuaConversionError {
1202                    from: "integer",
1203                    to: "non-negative integer".to_string(),
1204                    message: Some(format!(
1205                        "stream_pcm option {key} must be a non-negative integer"
1206                    )),
1207                })
1208        }
1209        Value::Number(value)
1210            if !value.is_finite()
1211                || value < 0.0
1212                || value >= usize_upper_bound_as_f64()
1213                || value.fract() != 0.0 =>
1214        {
1215            Err(mlua::Error::FromLuaConversionError {
1216                from: "number",
1217                to: "non-negative integer".to_string(),
1218                message: Some(format!(
1219                    "stream_pcm option {key} must be a non-negative integer"
1220                )),
1221            })
1222        }
1223        Value::Number(value) => Ok(Some(value as usize)),
1224        other => Err(mlua::Error::FromLuaConversionError {
1225            from: other.type_name(),
1226            to: "non-negative integer".to_string(),
1227            message: Some(format!(
1228                "stream_pcm option {key} must be a non-negative integer"
1229            )),
1230        }),
1231    }
1232}
1233
1234fn repeat_times_from_lua(value: Value) -> mlua::Result<usize> {
1235    let repeat_times = match value {
1236        Value::Integer(value) => usize::try_from(value).ok(),
1237        Value::Number(value)
1238            if value.is_finite()
1239                && value >= 0.0
1240                && value < usize_upper_bound_as_f64()
1241                && value.fract() == 0.0 =>
1242        {
1243            Some(value as usize)
1244        }
1245        _ => None,
1246    };
1247
1248    let Some(repeat_times) = repeat_times else {
1249        return Err(mlua::Error::from(
1250            crate::error::PulseError::InvalidRepeatTimes { repeat_times: 0 },
1251        ));
1252    };
1253
1254    if repeat_times == 0 {
1255        return Err(mlua::Error::from(
1256            crate::error::PulseError::InvalidRepeatTimes { repeat_times },
1257        ));
1258    }
1259
1260    Ok(repeat_times)
1261}
1262
1263fn drum_steps_from_lua(value: Value) -> mlua::Result<usize> {
1264    let steps = match value {
1265        Value::Integer(value) => usize::try_from(value).ok(),
1266        Value::Number(value)
1267            if value.is_finite()
1268                && value >= 0.0
1269                && value < usize_upper_bound_as_f64()
1270                && value.fract() == 0.0 =>
1271        {
1272            Some(value as usize)
1273        }
1274        _ => None,
1275    };
1276
1277    let Some(steps) = steps else {
1278        return Err(mlua::Error::from(
1279            crate::error::PulseError::InvalidDrumGridSteps { steps: 0 },
1280        ));
1281    };
1282
1283    if steps == 0 {
1284        return Err(mlua::Error::from(
1285            crate::error::PulseError::InvalidDrumGridSteps { steps },
1286        ));
1287    }
1288
1289    Ok(steps)
1290}
1291
1292fn usize_upper_bound_as_f64() -> f64 {
1293    let upper = usize::MAX as f64;
1294    if (upper as u128) > usize::MAX as u128 {
1295        upper
1296    } else {
1297        upper + 1.0
1298    }
1299}
1300
1301fn optional_u32_from_lua(table: &Table, key: &str) -> mlua::Result<Option<u32>> {
1302    match table.get::<Value>(key)? {
1303        Value::Nil => Ok(None),
1304        Value::Integer(value) if value < 0 || value > u32::MAX as i64 => {
1305            Err(mlua::Error::FromLuaConversionError {
1306                from: "integer",
1307                to: "non-negative integer".to_string(),
1308                message: Some(format!(
1309                    "export option {key} must be a non-negative integer"
1310                )),
1311            })
1312        }
1313        Value::Integer(value) => Ok(Some(value as u32)),
1314        Value::Number(value)
1315            if !value.is_finite()
1316                || value < 0.0
1317                || value > u32::MAX as f64
1318                || value.fract() != 0.0 =>
1319        {
1320            Err(mlua::Error::FromLuaConversionError {
1321                from: "number",
1322                to: "non-negative integer".to_string(),
1323                message: Some(format!(
1324                    "export option {key} must be a non-negative integer"
1325                )),
1326            })
1327        }
1328        Value::Number(value) => Ok(Some(value as u32)),
1329        other => Err(mlua::Error::FromLuaConversionError {
1330            from: other.type_name(),
1331            to: "non-negative integer".to_string(),
1332            message: Some(format!(
1333                "export option {key} must be a non-negative integer"
1334            )),
1335        }),
1336    }
1337}
1338
1339fn normalize_options_from_lua(table: &Table) -> mlua::Result<Option<NormalizeOptions>> {
1340    let mode = match table.get::<Value>("normalize")? {
1341        Value::Nil | Value::Boolean(false) => return Ok(None),
1342        Value::Boolean(true) => "peak".to_string(),
1343        Value::String(value) => value.to_str()?.as_ref().to_ascii_lowercase(),
1344        Value::Table(table) => {
1345            let mode = match table.get::<Value>("mode")? {
1346                Value::Nil => "rms".to_string(),
1347                Value::String(value) => value.to_str()?.as_ref().to_ascii_lowercase(),
1348                other => {
1349                    return Err(mlua::Error::FromLuaConversionError {
1350                        from: other.type_name(),
1351                        to: "string".to_string(),
1352                        message: Some("normalize.mode must be a string".to_string()),
1353                    });
1354                }
1355            };
1356            return normalize_options_from_lua_table(mode, &table).map(Some);
1357        }
1358        other => {
1359            return Err(mlua::Error::FromLuaConversionError {
1360                from: other.type_name(),
1361                to: "boolean, string, table, or nil".to_string(),
1362                message: Some(
1363                    "export option normalize must be true, false, 'peak', 'rms', or a table"
1364                        .to_string(),
1365                ),
1366            });
1367        }
1368    };
1369
1370    normalize_options_from_lua_table(mode, table).map(Some)
1371}
1372
1373fn normalize_options_from_lua_table(mode: String, table: &Table) -> mlua::Result<NormalizeOptions> {
1374    match mode.as_str() {
1375        "peak" => {
1376            let target_db = optional_f32_from_lua(table, "target_db")?.unwrap_or(-1.0);
1377            NormalizeOptions::peak(target_db).map_err(mlua::Error::from)
1378        }
1379        "rms" => {
1380            let target_db = optional_f32_from_lua(table, "target_db")?.unwrap_or(-16.0);
1381            let peak_ceiling_db = optional_f32_from_lua(table, "true_peak_db")?
1382                .or(optional_f32_from_lua(table, "peak_ceiling_db")?)
1383                .unwrap_or(-1.0);
1384            NormalizeOptions::rms(target_db, peak_ceiling_db).map_err(mlua::Error::from)
1385        }
1386        other => Err(mlua::Error::FromLuaConversionError {
1387            from: "string",
1388            to: "peak or rms".to_string(),
1389            message: Some(format!("unsupported normalize mode: {other}")),
1390        }),
1391    }
1392}
1393
1394fn optional_f32_from_lua(table: &Table, key: &str) -> mlua::Result<Option<f32>> {
1395    match table.get::<Value>(key)? {
1396        Value::Nil => Ok(None),
1397        Value::Integer(value) => Ok(Some(value as f32)),
1398        Value::Number(value) if value.is_finite() => Ok(Some(value as f32)),
1399        Value::Number(_) => Err(mlua::Error::FromLuaConversionError {
1400            from: "number",
1401            to: "finite number".to_string(),
1402            message: Some(format!("export option {key} must be a finite number")),
1403        }),
1404        other => Err(mlua::Error::FromLuaConversionError {
1405            from: other.type_name(),
1406            to: "number".to_string(),
1407            message: Some(format!("export option {key} must be a number")),
1408        }),
1409    }
1410}
1411
1412fn with_playable_userdata<T>(
1413    userdata: mlua::AnyUserData,
1414    f: impl FnOnce(PulsePlayable<'_>) -> mlua::Result<T>,
1415) -> mlua::Result<T> {
1416    if let Ok(song) = userdata.borrow::<PulseSong>() {
1417        return f(PulsePlayable::Song(&song));
1418    }
1419
1420    if let Ok(imported) = userdata.borrow::<PulseImportedMidi>() {
1421        return f(PulsePlayable::ImportedMidi(&imported));
1422    }
1423
1424    Err(mlua::Error::FromLuaConversionError {
1425        from: "userdata",
1426        to: "PulseSong or PulseImportedMidi".to_string(),
1427        message: Some("playback expects a PulseSong or PulseImportedMidi".to_string()),
1428    })
1429}
1430
1431fn effect_info_to_lua(lua: &Lua, info: &effects::EffectInfo) -> mlua::Result<Table> {
1432    let table = lua.create_table()?;
1433    table.set("name", info.name)?;
1434    table.set("category", info.category)?;
1435    table.set("scopes", lua.create_sequence_from(info.scopes.to_vec())?)?;
1436    table.set(
1437        "parameters",
1438        lua.create_sequence_from(info.parameters.to_vec())?,
1439    )?;
1440    table.set("presets", lua.create_sequence_from(info.presets.to_vec())?)?;
1441    Ok(table)
1442}
1443
1444fn synth_info_to_lua(lua: &Lua, info: &synthesis::SynthInfo) -> mlua::Result<Table> {
1445    let table = lua.create_table()?;
1446    table.set("name", info.name)?;
1447    table.set("category", info.category)?;
1448    table.set(
1449        "parameters",
1450        lua.create_sequence_from(info.parameters.to_vec())?,
1451    )?;
1452    table.set("presets", lua.create_sequence_from(info.presets.to_vec())?)?;
1453    table.set("aliases", lua.create_sequence_from(info.aliases.to_vec())?)?;
1454    Ok(table)
1455}
1456
1457fn effect_from_lua_value(name: &str, value: Value) -> mlua::Result<PulseEffect> {
1458    let options = effect_options_from_lua(value)?;
1459    effects::effect_from_options(name, options).map_err(mlua::Error::from)
1460}
1461
1462fn effect_options_from_lua(value: Value) -> mlua::Result<EffectOptions> {
1463    match value {
1464        Value::Nil => Ok(EffectOptions::Default),
1465        Value::String(value) => Ok(EffectOptions::Preset(value.to_str()?.as_ref().to_string())),
1466        Value::Table(table) => {
1467            let mut options = std::collections::BTreeMap::new();
1468            for pair in table.pairs::<Value, Value>() {
1469                let (key, value) = pair?;
1470                let Value::String(key) = key else {
1471                    return Err(mlua::Error::FromLuaConversionError {
1472                        from: key.type_name(),
1473                        to: "string".to_string(),
1474                        message: Some("effect option names must be strings".to_string()),
1475                    });
1476                };
1477                options.insert(
1478                    key.to_str()?.as_ref().to_string(),
1479                    effect_option_from_lua(value)?,
1480                );
1481            }
1482            Ok(EffectOptions::Params(options))
1483        }
1484        other => Err(mlua::Error::FromLuaConversionError {
1485            from: other.type_name(),
1486            to: "nil, preset string, or table".to_string(),
1487            message: Some("effect options must be nil, preset string, or table".to_string()),
1488        }),
1489    }
1490}
1491
1492fn effect_option_from_lua(value: Value) -> mlua::Result<EffectOption> {
1493    match value {
1494        Value::Integer(value) => Ok(EffectOption::Integer(value)),
1495        Value::Number(value) => Ok(EffectOption::Number(value as f32)),
1496        Value::String(value) => Ok(EffectOption::Text(value.to_str()?.as_ref().to_string())),
1497        other => Err(mlua::Error::FromLuaConversionError {
1498            from: other.type_name(),
1499            to: "number, integer, or string".to_string(),
1500            message: Some("effect table values accept numbers, integers, and strings".to_string()),
1501        }),
1502    }
1503}
1504
1505fn synth_from_lua_value(name: &str, value: Value) -> mlua::Result<PulseSynth> {
1506    let options = synth_options_from_lua(value)?;
1507    synthesis::synth_from_options(name, options).map_err(mlua::Error::from)
1508}
1509
1510fn synth_options_from_lua(value: Value) -> mlua::Result<SynthOptions> {
1511    match value {
1512        Value::Nil => Ok(SynthOptions::Default),
1513        Value::String(value) => Ok(SynthOptions::Preset(value.to_str()?.as_ref().to_string())),
1514        Value::Table(table) if table.raw_len() > 0 => {
1515            let harmonics = table
1516                .sequence_values::<f32>()
1517                .collect::<mlua::Result<Vec<_>>>()?;
1518            Ok(SynthOptions::Harmonics(harmonics))
1519        }
1520        Value::Table(table) => {
1521            let mut options = std::collections::BTreeMap::new();
1522            for pair in table.pairs::<Value, Value>() {
1523                let (key, value) = pair?;
1524                let Value::String(key) = key else {
1525                    return Err(mlua::Error::FromLuaConversionError {
1526                        from: key.type_name(),
1527                        to: "string".to_string(),
1528                        message: Some("synth option names must be strings".to_string()),
1529                    });
1530                };
1531                options.insert(
1532                    key.to_str()?.as_ref().to_string(),
1533                    synth_option_from_lua(value)?,
1534                );
1535            }
1536            Ok(SynthOptions::Params(options))
1537        }
1538        other => Err(mlua::Error::FromLuaConversionError {
1539            from: other.type_name(),
1540            to: "nil, preset string, or table".to_string(),
1541            message: Some("synth options must be nil, preset string, or table".to_string()),
1542        }),
1543    }
1544}
1545
1546fn synth_option_from_lua(value: Value) -> mlua::Result<SynthOption> {
1547    match value {
1548        Value::Integer(value) => Ok(SynthOption::Integer(value)),
1549        Value::Number(value) => Ok(SynthOption::Number(value as f32)),
1550        Value::String(value) => Ok(SynthOption::Text(value.to_str()?.as_ref().to_string())),
1551        Value::Table(table) if table.raw_len() > 0 => {
1552            let values = table
1553                .sequence_values::<f32>()
1554                .collect::<mlua::Result<Vec<_>>>()?;
1555            Ok(SynthOption::Numbers(values))
1556        }
1557        other => Err(mlua::Error::FromLuaConversionError {
1558            from: other.type_name(),
1559            to: "number, integer, string, or number array".to_string(),
1560            message: Some(
1561                "synth table values accept numbers, integers, strings, and number arrays"
1562                    .to_string(),
1563            ),
1564        }),
1565    }
1566}
1567
1568fn generator_info_to_lua(lua: &Lua, info: &generators::GeneratorInfo) -> mlua::Result<Table> {
1569    let table = lua.create_table()?;
1570    table.set("name", info.name)?;
1571    table.set("category", info.category)?;
1572    table.set("output", info.output)?;
1573    table.set(
1574        "parameters",
1575        lua.create_sequence_from(info.parameters.to_vec())?,
1576    )?;
1577    Ok(table)
1578}
1579
1580fn generator_options_from_lua(value: Value) -> mlua::Result<GeneratorOptions> {
1581    match value {
1582        Value::Nil => Ok(GeneratorOptions::new()),
1583        Value::Table(table) => {
1584            let mut options = GeneratorOptions::new();
1585            for pair in table.pairs::<Value, Value>() {
1586                let (key, value) = pair?;
1587                let Value::String(key) = key else {
1588                    return Err(mlua::Error::FromLuaConversionError {
1589                        from: key.type_name(),
1590                        to: "string".to_string(),
1591                        message: Some("generator option names must be strings".to_string()),
1592                    });
1593                };
1594                options.insert(key.to_str()?.as_ref(), generator_option_from_lua(value)?);
1595            }
1596            Ok(options)
1597        }
1598        other => Err(mlua::Error::FromLuaConversionError {
1599            from: other.type_name(),
1600            to: "table or nil".to_string(),
1601            message: Some("generate expects an options table or nil".to_string()),
1602        }),
1603    }
1604}
1605
1606fn generator_option_from_lua(value: Value) -> mlua::Result<GeneratorOption> {
1607    match value {
1608        Value::Integer(value) => Ok(GeneratorOption::Integer(value)),
1609        Value::Number(value) => Ok(GeneratorOption::Number(value)),
1610        Value::Boolean(value) => Ok(GeneratorOption::Boolean(value)),
1611        Value::String(value) => Ok(GeneratorOption::Text(value.to_str()?.as_ref().to_string())),
1612        Value::Table(table) => generator_table_option_from_lua(table),
1613        other => Err(mlua::Error::FromLuaConversionError {
1614            from: other.type_name(),
1615            to: "generator option".to_string(),
1616            message: Some(
1617                "generator options accept numbers, booleans, strings, integer arrays, and string maps"
1618                    .to_string(),
1619            ),
1620        }),
1621    }
1622}
1623
1624fn generator_table_option_from_lua(table: Table) -> mlua::Result<GeneratorOption> {
1625    if table.raw_len() > 0 {
1626        let values = table
1627            .sequence_values::<u32>()
1628            .collect::<mlua::Result<Vec<_>>>()?;
1629        return Ok(GeneratorOption::IntegerList(values));
1630    }
1631
1632    let mut values = std::collections::BTreeMap::new();
1633    for pair in table.pairs::<Value, Value>() {
1634        let (key, value) = pair?;
1635        let Value::String(key) = key else {
1636            return Err(mlua::Error::FromLuaConversionError {
1637                from: key.type_name(),
1638                to: "string".to_string(),
1639                message: Some("generator map option names must be strings".to_string()),
1640            });
1641        };
1642        let Value::String(value) = value else {
1643            return Err(mlua::Error::FromLuaConversionError {
1644                from: value.type_name(),
1645                to: "string".to_string(),
1646                message: Some("generator map option values must be strings".to_string()),
1647            });
1648        };
1649        values.insert(
1650            key.to_str()?.as_ref().to_string(),
1651            value.to_str()?.as_ref().to_string(),
1652        );
1653    }
1654    Ok(GeneratorOption::TextMap(values))
1655}
1656
1657fn number_sequence_from_lua(table: Table) -> mlua::Result<Vec<f32>> {
1658    table.sequence_values::<f32>().collect::<mlua::Result<_>>()
1659}
1660
1661fn chord_sequence_from_lua(table: Table) -> mlua::Result<Vec<Vec<f32>>> {
1662    table
1663        .sequence_values::<Table>()
1664        .map(|entry| number_sequence_from_lua(entry?))
1665        .collect::<mlua::Result<_>>()
1666}
1667
1668fn generated_to_lua(lua: &Lua, generated: PulseGenerated) -> mlua::Result<Value> {
1669    match generated {
1670        PulseGenerated::Numbers(values) => Ok(Value::Table(lua.create_sequence_from(values)?)),
1671        PulseGenerated::Integers(values) => Ok(Value::Table(lua.create_sequence_from(values)?)),
1672        PulseGenerated::Steps(values) => Ok(Value::Table(lua.create_sequence_from(values)?)),
1673        PulseGenerated::NumberMatrix(rows) => Ok(Value::Table(number_matrix_to_lua(lua, rows)?)),
1674        PulseGenerated::IntegerMatrix(rows) => Ok(Value::Table(integer_matrix_to_lua(lua, rows)?)),
1675        PulseGenerated::StepMatrix(rows) => Ok(Value::Table(step_matrix_to_lua(lua, rows)?)),
1676        PulseGenerated::Text(value) => Ok(Value::String(lua.create_string(&value)?)),
1677    }
1678}
1679
1680fn number_matrix_to_lua(lua: &Lua, rows: Vec<Vec<f32>>) -> mlua::Result<Table> {
1681    let table = lua.create_table()?;
1682    for (index, row) in rows.into_iter().enumerate() {
1683        table.set(index + 1, lua.create_sequence_from(row)?)?;
1684    }
1685    Ok(table)
1686}
1687
1688fn integer_matrix_to_lua(lua: &Lua, rows: Vec<Vec<u32>>) -> mlua::Result<Table> {
1689    let table = lua.create_table()?;
1690    for (index, row) in rows.into_iter().enumerate() {
1691        table.set(index + 1, lua.create_sequence_from(row)?)?;
1692    }
1693    Ok(table)
1694}
1695
1696fn step_matrix_to_lua(lua: &Lua, rows: Vec<Vec<usize>>) -> mlua::Result<Table> {
1697    let table = lua.create_table()?;
1698    for (index, row) in rows.into_iter().enumerate() {
1699        table.set(index + 1, lua.create_sequence_from(row)?)?;
1700    }
1701    Ok(table)
1702}
1703
1704fn transpose_lua_value(lua: &Lua, value: Value, semitones: i32) -> mlua::Result<Value> {
1705    match value {
1706        Value::Number(number) => Ok(Value::Number(theory::transpose_frequency(
1707            number as f32,
1708            semitones,
1709        ) as f64)),
1710        Value::Integer(integer) => Ok(Value::Number(theory::transpose_frequency(
1711            integer as f32,
1712            semitones,
1713        ) as f64)),
1714        Value::Table(table) => {
1715            let notes: Vec<f32> = table
1716                .sequence_values::<f32>()
1717                .collect::<mlua::Result<_>>()?;
1718            let shifted = theory::transpose_notes(&notes, semitones);
1719            Ok(Value::Table(lua.create_sequence_from(shifted)?))
1720        }
1721        other => Err(mlua::Error::FromLuaConversionError {
1722            from: other.type_name(),
1723            to: "number or table".to_string(),
1724            message: Some("transpose expects a frequency or a sequence of frequencies".to_string()),
1725        }),
1726    }
1727}