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
18pub 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
232pub 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(¬es, 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}