use mlua_pulse::composition::{PulsePhrase, PulseSampleClip, PulseSequence, PulseSong};
use mlua_pulse::effects::{effect_from_options, EffectOptions};
use mlua_pulse::export::{export_wav, ExportOptions};
use mlua_pulse::files::{
export, export_flac, export_midi, export_midi_with_options, export_with_options, import_midi,
};
use mlua_pulse::theory::parse_note_frequency;
use std::path::Path;
fn write_test_source_wav(path: &Path) {
let sample_rate = 44_100;
let frames = sample_rate / 4;
let samples = (0..frames)
.map(|frame| {
let t = frame as f32 / sample_rate as f32;
(2.0 * std::f32::consts::PI * 220.0 * t).sin() * 0.4
})
.collect();
tunes::synthesis::sample::Sample::from_mono(samples, sample_rate)
.export_wav(path)
.expect("test source wav should be written");
}
fn write_constant_source_wav(path: &Path, amplitude: f32) {
let sample_rate = 44_100;
let frames = sample_rate / 10;
let samples = vec![amplitude; frames as usize];
tunes::synthesis::sample::Sample::from_mono(samples, sample_rate)
.export_wav(path)
.expect("constant test source wav should be written");
}
fn write_impulse_bed_source_wav(path: &Path) {
let sample_rate = 44_100;
let mut samples = vec![0.02; (sample_rate / 2) as usize];
samples[0] = 1.0;
tunes::synthesis::sample::Sample::from_mono(samples, sample_rate)
.export_wav(path)
.expect("impulse-bed test source wav should be written");
}
fn sample_clip_song(source: &Path) -> PulseSong {
PulseSong::new().add_sample_clip(PulseSampleClip::new(source.to_string_lossy().as_ref()))
}
fn wav_samples(path: &Path) -> Vec<f32> {
let mut reader = hound::WavReader::open(path).expect("wav should open");
reader
.samples::<i16>()
.map(|sample| sample.expect("wav sample should decode") as f32 / 32767.0)
.collect()
}
fn linear_to_db(value: f32) -> f32 {
20.0 * value.max(f32::MIN_POSITIVE).log10()
}
fn wav_peak_db(path: &Path) -> f32 {
let peak = wav_samples(path)
.into_iter()
.fold(0.0_f32, |current, sample| current.max(sample.abs()));
linear_to_db(peak)
}
fn wav_rms_db(path: &Path) -> f32 {
let samples = wav_samples(path);
let energy = samples.iter().map(|sample| sample * sample).sum::<f32>() / samples.len() as f32;
linear_to_db(energy.sqrt())
}
fn midi_note_on_velocities(path: &Path) -> Vec<u8> {
let bytes = std::fs::read(path).expect("midi should be readable");
let smf = midly::Smf::parse(&bytes).expect("midi should parse");
smf.tracks
.iter()
.flat_map(|track| track.iter())
.filter_map(|event| match event.kind {
midly::TrackEventKind::Midi {
message: midly::MidiMessage::NoteOn { vel, .. },
..
} if vel.as_int() > 0 => Some(vel.as_int()),
_ => None,
})
.collect()
}
fn wav_sample_rate(path: &Path) -> u32 {
let bytes = std::fs::read(path).expect("wav should be readable");
u32::from_le_bytes(
bytes[24..28]
.try_into()
.expect("wav sample-rate header should have 4 bytes"),
)
}
fn flac_bits_per_sample(path: &Path) -> u32 {
let bytes = std::fs::read(path).expect("flac should be readable");
assert_eq!(&bytes[0..4], b"fLaC");
assert_eq!(
bytes[4] & 0x7f,
0,
"first metadata block should be STREAMINFO"
);
let length = ((bytes[5] as usize) << 16) | ((bytes[6] as usize) << 8) | bytes[7] as usize;
assert!(
length >= 18,
"STREAMINFO should include packed sample metadata"
);
let packed = u64::from_be_bytes(
bytes[18..26]
.try_into()
.expect("STREAMINFO packed field should have 8 bytes"),
);
((packed >> 36) & 0x1f) as u32 + 1
}
fn simple_song() -> PulseSong {
let c4 = parse_note_frequency("C4").unwrap();
PulseSong::new().add_sequence(
PulseSequence::new()
.with_notes(vec![c4])
.with_durations(vec![0.25])
.with_instrument("electric_piano"),
)
}
#[test]
fn imported_midi_clip_merges_into_song_mixer_with_offset_repeat_and_mix_controls() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let midi_input = temp_dir.path().join("source.mid");
export_midi(&simple_song(), &midi_input).expect("midi input should export");
let clip = import_midi(&midi_input)
.expect("midi input should import")
.as_clip()
.with_start_at(0.5)
.expect("start offset should be valid")
.with_track("imported_theme")
.with_volume(0.7)
.expect("volume should be valid")
.with_pan(0.35)
.expect("pan should be valid")
.with_repeat_times(2)
.expect("repeat count should be valid")
.with_effect(
effect_from_options("chorus", EffectOptions::Default)
.expect("default chorus should parse"),
);
let mixer = PulseSong::new().add_midi_clip(clip).to_mixer().unwrap();
let tracks = mixer.all_tracks();
assert!(!tracks.is_empty());
assert!(tracks.iter().all(|track| track
.name
.as_deref()
.unwrap_or("")
.starts_with("imported_theme")));
assert!(tracks
.iter()
.all(|track| (track.volume - 0.7).abs() < f32::EPSILON));
assert!(tracks
.iter()
.all(|track| (track.pan - 0.35).abs() < f32::EPSILON));
assert!(tracks.iter().all(|track| track.effects.chorus.is_some()));
let mut event_times = tracks
.iter()
.flat_map(|track| {
track
.events
.iter()
.map(tunes::track::AudioEvent::start_time)
})
.collect::<Vec<_>>();
event_times.sort_by(|left, right| left.partial_cmp(right).unwrap());
assert!(event_times.iter().any(|time| (*time - 0.5).abs() < 0.01));
assert!(event_times.iter().any(|time| *time > 0.6));
}
#[test]
fn midi_clip_inside_repeated_phrase_offsets_events_on_song_timeline() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let midi_input = temp_dir.path().join("phrase-source.mid");
export_midi(&simple_song(), &midi_input).expect("midi input should export");
let clip = import_midi(&midi_input)
.expect("midi input should import")
.as_clip()
.with_start_at(0.25)
.expect("start offset should be valid")
.with_track("phrase_theme")
.with_repeat_times(2)
.expect("repeat count should be valid");
let phrase_duration = clip.duration().expect("clip duration should be known");
let source_duration = (phrase_duration - clip.start_at()) / clip.repeat_times() as f32;
let phrase = PulsePhrase::new()
.add_midi_clip(clip)
.with_repeat_times(2)
.expect("phrase repeat count should be valid");
assert!((phrase.duration().unwrap() - phrase_duration).abs() < 0.01);
let mixer = PulseSong::new().add_phrase(phrase).to_mixer().unwrap();
let tracks = mixer.all_tracks();
assert!(!tracks.is_empty());
assert!(tracks.iter().all(|track| track
.name
.as_deref()
.unwrap_or("")
.starts_with("phrase_theme")));
let event_times = tracks
.iter()
.flat_map(|track| {
track
.events
.iter()
.map(tunes::track::AudioEvent::start_time)
})
.collect::<Vec<_>>();
for expected in [
0.25,
0.25 + source_duration,
phrase_duration + 0.25,
phrase_duration + 0.25 + source_duration,
] {
assert!(
event_times
.iter()
.any(|time| (*time - expected).abs() < 0.01),
"expected MIDI phrase event near {expected}, got {event_times:?}"
);
}
}
fn export_effect_base_song() -> PulseSong {
let c4 = parse_note_frequency("C4").unwrap();
PulseSong::new().add_sequence(
PulseSequence::new()
.with_notes(vec![c4])
.with_durations(vec![0.5])
.with_instrument("electric_piano"),
)
}
fn effected_song() -> PulseSong {
let effect = mlua_pulse::effects::effect_from_options("eq", {
let mut options = std::collections::BTreeMap::new();
options.insert(
"low_gain".to_string(),
mlua_pulse::effects::EffectOption::Number(0.0),
);
options.insert(
"mid_gain".to_string(),
mlua_pulse::effects::EffectOption::Number(0.0),
);
options.insert(
"high_gain".to_string(),
mlua_pulse::effects::EffectOption::Number(0.0),
);
mlua_pulse::effects::EffectOptions::Params(options)
})
.expect("eq effect should parse");
export_effect_base_song().with_master_effect(effect)
}
#[test]
fn exports_non_empty_riff_wave_file() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("foundation-loop.wav");
let c4 = parse_note_frequency("C4").unwrap();
let song = PulseSong::new().add_sequence(
PulseSequence::new()
.with_notes(vec![c4])
.with_durations(vec![0.25])
.with_instrument("electric_piano"),
);
export_wav(&song, &output).expect("export should succeed");
let bytes = std::fs::read(&output).expect("wav should be readable");
assert!(bytes.len() > 44);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
}
#[test]
fn lua_dsl_exports_wav_file() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("lua-foundation-loop.wav");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local notes = pulse.scale("C4", "ionian")
pulse.song()
:tempo(120)
:add(
pulse.sequence()
:notes(notes)
:durations({{0.25, 0.25, 0.25, 0.25, 0.5, 0.5, 0.5, 1.0}})
:instrument("electric_piano")
)
:export_wav("{}")
"#,
output_text
))
.exec()
.expect("lua export should run");
let bytes = std::fs::read(&output).expect("wav should be readable");
assert!(bytes.len() > 44);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
}
#[test]
fn exports_non_empty_flac_file() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("song.flac");
export_flac(&simple_song(), &output).expect("flac export should succeed");
let bytes = std::fs::read(&output).expect("flac should be readable");
assert!(bytes.len() > 4);
assert_eq!(&bytes[0..4], b"fLaC");
}
#[test]
fn master_effect_changes_wav_export_bytes() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let dry_output = temp_dir.path().join("dry.wav");
let wet_output = temp_dir.path().join("wet.wav");
export_wav(&export_effect_base_song(), &dry_output).expect("dry wav export should succeed");
export_wav(&effected_song(), &wet_output).expect("effected wav export should succeed");
let dry = std::fs::read(&dry_output).expect("dry wav should be readable");
let wet = std::fs::read(&wet_output).expect("effected wav should be readable");
assert_eq!(&dry[0..4], b"RIFF");
assert_eq!(&wet[0..4], b"RIFF");
assert_eq!(&dry[8..12], b"WAVE");
assert_eq!(&wet[8..12], b"WAVE");
assert!(dry != wet, "master effect should change WAV bytes");
}
#[test]
fn master_effect_changes_flac_export_bytes() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let dry_output = temp_dir.path().join("dry.flac");
let wet_output = temp_dir.path().join("wet.flac");
export_flac(&export_effect_base_song(), &dry_output).expect("dry flac export should succeed");
export_flac(&effected_song(), &wet_output).expect("effected flac export should succeed");
let dry = std::fs::read(&dry_output).expect("dry flac should be readable");
let wet = std::fs::read(&wet_output).expect("effected flac should be readable");
assert_eq!(&dry[0..4], b"fLaC");
assert_eq!(&wet[0..4], b"fLaC");
assert!(dry != wet, "master effect should change FLAC bytes");
}
#[test]
fn exports_non_empty_midi_file() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("song.mid");
export_midi(&simple_song(), &output).expect("midi export should succeed");
let bytes = std::fs::read(&output).expect("midi should be readable");
assert!(bytes.len() > 14);
assert_eq!(&bytes[0..4], b"MThd");
}
#[test]
fn unified_export_dispatches_by_file_extension() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let wav_output = temp_dir.path().join("song.wav");
let flac_output = temp_dir.path().join("song.flac");
let midi_output = temp_dir.path().join("song.midi");
let song = simple_song();
export(&song, &wav_output).expect("wav export should succeed");
export(&song, &flac_output).expect("flac export should succeed");
export(&song, &midi_output).expect("midi export should succeed");
assert_eq!(&std::fs::read(&wav_output).unwrap()[0..4], b"RIFF");
assert_eq!(&std::fs::read(&flac_output).unwrap()[0..4], b"fLaC");
assert_eq!(&std::fs::read(&midi_output).unwrap()[0..4], b"MThd");
let invalid =
export(&song, temp_dir.path().join("song.ogg")).expect_err("unknown extension should fail");
assert_eq!(invalid.to_string(), "unsupported export format: ogg");
}
#[test]
fn rust_export_options_can_select_audio_sample_rate() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let wav_output = temp_dir.path().join("song-48k.wav");
let flac_output = temp_dir.path().join("song-48k.flac");
let options = ExportOptions::new().with_sample_rate(48_000).unwrap();
export_with_options(&simple_song(), &wav_output, options).expect("wav export should succeed");
export_with_options(&simple_song(), &flac_output, options).expect("flac export should succeed");
assert_eq!(wav_sample_rate(&wav_output), 48_000);
assert_eq!(&std::fs::read(&flac_output).unwrap()[0..4], b"fLaC");
let error = ExportOptions::new()
.with_sample_rate(0)
.expect_err("zero sample rate should fail");
assert_eq!(error.to_string(), "invalid export sample_rate: 0");
}
#[test]
fn rust_export_options_can_peak_normalize_wav() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let source = temp_dir.path().join("source.wav");
let output = temp_dir.path().join("normalized.wav");
write_constant_source_wav(&source, 0.2);
let options = ExportOptions::new()
.with_sample_rate(44_100)
.unwrap()
.with_peak_normalization(-6.0)
.unwrap();
export_with_options(&sample_clip_song(&source), &output, options)
.expect("peak-normalized wav export should succeed");
assert!((wav_peak_db(&output) - -6.0).abs() < 0.1);
}
#[test]
fn rust_export_options_can_rms_normalize_wav_with_peak_ceiling() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let source = temp_dir.path().join("source.wav");
let output = temp_dir.path().join("rms-normalized.wav");
write_constant_source_wav(&source, 0.1);
let options = ExportOptions::new()
.with_sample_rate(44_100)
.unwrap()
.with_rms_normalization(-18.0, -1.0)
.unwrap();
export_with_options(&sample_clip_song(&source), &output, options)
.expect("rms-normalized wav export should succeed");
assert!((wav_rms_db(&output) - -18.0).abs() < 0.1);
assert!(wav_peak_db(&output) <= -1.0 + 0.1);
}
#[test]
fn rust_normalized_flac_defaults_to_16_bit_and_allows_24_bit() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let source = temp_dir.path().join("source.wav");
let default_output = temp_dir.path().join("normalized-default.flac");
let high_res_output = temp_dir.path().join("normalized-24.flac");
write_constant_source_wav(&source, 0.1);
let default_options = ExportOptions::new()
.with_sample_rate(44_100)
.unwrap()
.with_peak_normalization(-3.0)
.unwrap();
export_with_options(&sample_clip_song(&source), &default_output, default_options)
.expect("default normalized flac export should succeed");
assert_eq!(flac_bits_per_sample(&default_output), 16);
let high_res_options = default_options.with_flac_bits_per_sample(24).unwrap();
export_with_options(
&sample_clip_song(&source),
&high_res_output,
high_res_options,
)
.expect("24-bit normalized flac export should succeed");
assert_eq!(flac_bits_per_sample(&high_res_output), 24);
let error = ExportOptions::new()
.with_flac_bits_per_sample(12)
.expect_err("unsupported FLAC bit depth should fail");
assert_eq!(error.to_string(), "invalid export flac_bits_per_sample: 12");
}
#[test]
fn rust_rms_normalization_limits_transient_peaks_without_missing_target() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let source = temp_dir.path().join("impulse-bed.wav");
let output = temp_dir.path().join("limited-rms.wav");
write_impulse_bed_source_wav(&source);
let options = ExportOptions::new()
.with_sample_rate(44_100)
.unwrap()
.with_rms_normalization(-18.0, -1.0)
.unwrap();
export_with_options(&sample_clip_song(&source), &output, options)
.expect("rms-normalized transient wav export should succeed");
assert!((wav_rms_db(&output) - -18.0).abs() < 0.75);
assert!(wav_peak_db(&output) <= -1.0 + 0.1);
}
#[test]
fn rust_export_options_can_raise_midi_velocity_for_listening() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("loud-midi.mid");
let c4 = parse_note_frequency("C4").unwrap();
let song = PulseSong::new().add_sequence(
PulseSequence::new()
.with_notes(vec![c4])
.with_durations(vec![0.25])
.with_instrument("electric_piano")
.with_volume(0.3)
.unwrap()
.with_velocity(0.25)
.unwrap(),
);
let options = ExportOptions::new()
.with_midi_velocity_gain(2.0)
.unwrap()
.with_midi_min_velocity(0.55)
.unwrap();
export_midi_with_options(&song, &output, options)
.expect("midi export with loudness options should succeed");
let velocities = midi_note_on_velocities(&output);
assert!(!velocities.is_empty());
assert!(
velocities.iter().all(|velocity| *velocity >= 70),
"expected all note-on velocities to be lifted, got {velocities:?}"
);
}
#[test]
fn lua_song_stream_pcm_emits_userdata_chunks() {
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(
r#"
local pulse = require("pulse")
local chunks = 0
local bytes = 0
local first_format = nil
local first_sample_rate = nil
local first_channels = nil
local song = pulse.song()
:add(
pulse.sequence()
:notes({440.0})
:durations({0.25})
:instrument("electric_piano")
)
local returned = song:stream_pcm(function(chunk)
assert(type(chunk) == "userdata")
local frames = chunk:frames()
local sample_rate = chunk:sample_rate()
local channels = chunk:channels()
local format = chunk:format()
local chunk_bytes = chunk:bytes()
assert(type(chunk:sample(1)) == "number")
assert(frames > 0)
assert(sample_rate == 22050)
assert(channels == 2)
assert(format == "i16le")
assert(chunk_bytes == frames * channels * 2)
chunks = chunks + 1
bytes = bytes + chunk_bytes
first_format = first_format or format
first_sample_rate = first_sample_rate or sample_rate
first_channels = first_channels or channels
end, {
sample_rate = 22050,
chunk_frames = 128,
normalize = true,
})
assert(type(returned) == "number")
assert(returned == chunks)
assert(chunks > 1)
assert(bytes > 0)
assert(first_format == "i16le")
assert(first_sample_rate == 22050)
assert(first_channels == 2)
local reversed_chunks = 0
local reversed_returned = song:stream_pcm({
sample_rate = 16000,
chunk_frames = 256,
}, function(chunk)
assert(chunk:sample_rate() == 16000)
reversed_chunks = reversed_chunks + 1
end)
assert(reversed_returned == reversed_chunks)
assert(reversed_chunks > 0)
local top_level_chunks = 0
local top_level_returned = pulse.stream_pcm(song, {
sample_rate = 12000,
chunk_frames = 512,
}, function(chunk)
assert(chunk:sample_rate() == 12000)
top_level_chunks = top_level_chunks + 1
end)
assert(top_level_returned == top_level_chunks)
assert(top_level_chunks > 0)
"#,
)
.exec()
.expect("Lua PCM stream should emit userdata chunks");
}
#[test]
fn lua_stream_pcm_rejects_oversized_chunk_frames_before_rendering() {
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(
r#"
local pulse = require("pulse")
local song = pulse.song()
:add(
pulse.sequence()
:notes({440.0})
:durations({0.25})
:instrument("electric_piano")
)
local called = false
local ok, err = pcall(function()
song:stream_pcm(function(_chunk)
called = true
end, {
chunk_frames = 18446744073709551616.0,
})
end)
assert(ok == false)
assert(called == false)
assert(tostring(err):find("stream_pcm option chunk_frames must be a non-negative integer", 1, true), tostring(err))
"#,
)
.exec()
.expect("Lua oversized chunk_frames should be rejected during argument parsing");
}
#[test]
fn lua_imported_midi_stream_pcm_emits_userdata_chunks() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let midi_input = temp_dir.path().join("input.mid");
export_midi(&simple_song(), &midi_input).expect("midi export should create input");
let midi_text = midi_input.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local imported = pulse.import_midi("{}")
local chunks = 0
local bytes = 0
local returned = imported:stream_pcm(function(chunk)
assert(type(chunk) == "userdata")
assert(chunk:bytes() == chunk:frames() * chunk:channels() * 2)
assert(chunk:sample_rate() == 16000)
assert(chunk:channels() == 2)
assert(chunk:format() == "i16le")
chunks = chunks + 1
bytes = bytes + chunk:bytes()
end, {{ sample_rate = 16000, chunk_frames = 64 }})
assert(returned == chunks)
assert(chunks > 0)
assert(bytes > 0)
"#,
midi_text
))
.exec()
.expect("Lua imported MIDI PCM stream should emit userdata chunks");
}
#[test]
fn lua_imported_midi_participates_in_song_and_phrase_exports() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let midi_input = temp_dir.path().join("source.mid");
let wav_output = temp_dir.path().join("arranged-midi.wav");
let midi_output = temp_dir.path().join("arranged-midi.mid");
export_midi(&simple_song(), &midi_input).expect("midi export should create input");
let input_text = midi_input.to_string_lossy().replace('\\', "\\\\");
let wav_text = wav_output.to_string_lossy().replace('\\', "\\\\");
let midi_text = midi_output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local midi = pulse.import_midi("{}")
:at(0.25)
:track("borrowed_theme")
:volume(0.8)
:pan(-0.2)
:repeat_times(2)
:effect("reverb", "room")
local phrase = pulse.phrase()
:add_midi(midi)
:repeat_times(2)
pulse.song()
:add_midi(midi)
:add_phrase(phrase)
:export_wav("{}")
pulse.song()
:add_midi(midi)
:export_midi("{}")
"#,
input_text, wav_text, midi_text
))
.exec()
.expect("Lua imported MIDI arrangement should run");
assert!(std::fs::metadata(wav_output).unwrap().len() > 44);
assert!(std::fs::metadata(midi_output).unwrap().len() > 0);
}
#[test]
fn lua_midi_and_sample_share_phrase_arrangement_exports() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let midi_input = temp_dir.path().join("combined-source.mid");
let sample_input = temp_dir.path().join("texture.wav");
let wav_output = temp_dir.path().join("combined.wav");
let flac_output = temp_dir.path().join("combined.flac");
let midi_output = temp_dir.path().join("combined.mid");
export_midi(&simple_song(), &midi_input).expect("midi export should create input");
write_test_source_wav(&sample_input);
let midi_text = midi_input.to_string_lossy().replace('\\', "\\\\");
let sample_text = sample_input.to_string_lossy().replace('\\', "\\\\");
let wav_text = wav_output.to_string_lossy().replace('\\', "\\\\");
let flac_text = flac_output.to_string_lossy().replace('\\', "\\\\");
let midi_output_text = midi_output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local texture = pulse.sample("{}")
:slice(0.0, 0.1)
:loop_for(0.5)
:at(0.125)
:track("texture_loop")
:volume(0.35)
:pan(-0.25)
:effect("delay", "slapback")
local midi = pulse.import_midi("{}")
:track("phrase_theme")
:at(0.25)
:volume(0.75)
:pan(0.2)
:repeat_times(2)
:effect("chorus", "subtle")
local phrase = pulse.phrase()
:add_sample(texture)
:add_midi(midi)
:repeat_times(2)
local song = pulse.song()
:add_phrase(phrase)
:master_effect("limiter", "standard")
song:export_wav("{}", {{ normalize = "peak", target_db = -1.0 }})
song:export_flac("{}", {{ normalize = "rms", target_db = -18.0, true_peak_db = -1.0 }})
song:export_midi("{}", {{ midi_velocity_gain = 2.0, midi_min_velocity = 0.5 }})
"#,
sample_text, midi_text, wav_text, flac_text, midi_output_text
))
.exec()
.expect("Lua combined sample and MIDI phrase should export");
assert_eq!(&std::fs::read(&wav_output).unwrap()[0..4], b"RIFF");
assert_eq!(&std::fs::read(&flac_output).unwrap()[0..4], b"fLaC");
assert_eq!(&std::fs::read(&midi_output).unwrap()[0..4], b"MThd");
assert!(
!midi_note_on_velocities(&midi_output).is_empty(),
"combined MIDI export should keep imported MIDI notes"
);
}
#[test]
fn lua_top_level_stream_pcm_returns_nil_for_unsupported_values() {
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(
r#"
local pulse = require("pulse")
local called = false
local result = pulse.stream_pcm(pulse.sequence(), function(_chunk)
called = true
end)
assert(result == nil)
assert(called == false)
"#,
)
.exec()
.expect("top-level PCM stream should return nil for unsupported values");
}
#[cfg(not(feature = "gpu"))]
#[test]
fn rust_gpu_export_option_reports_missing_feature_when_disabled() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("gpu-request.wav");
let error = export_with_options(&simple_song(), &output, ExportOptions::new().with_gpu(true))
.expect_err("requesting gpu without the gpu feature should fail");
assert_eq!(error.to_string(), "gpu feature is not enabled");
assert!(
!output.exists(),
"failed export should not create a WAV file"
);
}
#[cfg(feature = "gpu")]
#[test]
fn gpu_export_option_is_available_when_feature_enabled() {
assert!(ExportOptions::new().with_gpu(true).gpu());
}
#[test]
fn imports_midi_and_reexports_audio_and_midi() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let midi_input = temp_dir.path().join("input.mid");
let wav_output = temp_dir.path().join("from-midi.wav");
let flac_output = temp_dir.path().join("from-midi.flac");
let midi_output = temp_dir.path().join("roundtrip.mid");
export_midi(&simple_song(), &midi_input).expect("midi export should create input");
let imported = import_midi(&midi_input).expect("midi import should succeed");
imported
.export_wav(&wav_output)
.expect("imported midi should export wav");
imported
.export_flac(&flac_output)
.expect("imported midi should export flac");
imported
.export_midi(&midi_output)
.expect("imported midi should export midi");
assert_eq!(&std::fs::read(&wav_output).unwrap()[0..4], b"RIFF");
assert_eq!(&std::fs::read(&flac_output).unwrap()[0..4], b"fLaC");
assert_eq!(&std::fs::read(&midi_output).unwrap()[0..4], b"MThd");
}
#[test]
fn lua_unified_export_methods_dispatch_by_extension() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let song_wav = temp_dir.path().join("lua-song.wav");
let song_flac = temp_dir.path().join("lua-song.flac");
let song_midi = temp_dir.path().join("lua-song.mid");
let imported_wav = temp_dir.path().join("imported.wav");
let imported_midi = temp_dir.path().join("imported.mid");
let song_wav_text = song_wav.to_string_lossy().replace('\\', "\\\\");
let song_flac_text = song_flac.to_string_lossy().replace('\\', "\\\\");
let song_midi_text = song_midi.to_string_lossy().replace('\\', "\\\\");
let imported_wav_text = imported_wav.to_string_lossy().replace('\\', "\\\\");
let imported_midi_text = imported_midi.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local song = pulse.song()
:add(
pulse.sequence()
:notes({{440.0}})
:durations({{0.25}})
:instrument("electric_piano")
)
song:export("{}")
song:export("{}")
song:export("{}")
local imported = pulse.import_midi("{}")
imported:export("{}")
imported:export("{}")
local ok_invalid, err_invalid = pcall(function()
song:export("song.aiff")
end)
assert(ok_invalid == false)
assert(tostring(err_invalid):find("unsupported export format: aiff", 1, true))
"#,
song_wav_text,
song_flac_text,
song_midi_text,
song_midi_text,
imported_wav_text,
imported_midi_text
))
.exec()
.expect("Lua unified export should run");
assert_eq!(&std::fs::read(&song_wav).unwrap()[0..4], b"RIFF");
assert_eq!(&std::fs::read(&song_flac).unwrap()[0..4], b"fLaC");
assert_eq!(&std::fs::read(&song_midi).unwrap()[0..4], b"MThd");
assert_eq!(&std::fs::read(&imported_wav).unwrap()[0..4], b"RIFF");
assert_eq!(&std::fs::read(&imported_midi).unwrap()[0..4], b"MThd");
}
#[test]
fn lua_export_options_can_select_audio_sample_rate() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let wav_output = temp_dir.path().join("lua-48k.wav");
let flac_output = temp_dir.path().join("lua-48k.flac");
let wav_text = wav_output.to_string_lossy().replace('\\', "\\\\");
let flac_text = flac_output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local song = pulse.song()
:add(
pulse.sequence()
:notes({{440.0}})
:durations({{0.25}})
:instrument("electric_piano")
)
song:export("{}", {{ sample_rate = 48000 }})
song:export_flac("{}", {{ sample_rate = 48000 }})
local ok, err = pcall(function()
song:export("bad.wav", {{ sample_rate = 0 }})
end)
assert(ok == false)
assert(tostring(err):find("invalid export sample_rate: 0", 1, true))
"#,
wav_text, flac_text
))
.exec()
.expect("Lua sample-rate export options should run");
assert_eq!(wav_sample_rate(&wav_output), 48_000);
assert_eq!(&std::fs::read(&flac_output).unwrap()[0..4], b"fLaC");
}
#[test]
fn lua_export_options_can_normalize_audio_and_lift_midi_velocity() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let source = temp_dir.path().join("source.wav");
let wav_output = temp_dir.path().join("lua-normalized.wav");
let midi_output = temp_dir.path().join("lua-loud.mid");
write_constant_source_wav(&source, 0.1);
let source_text = source.to_string_lossy().replace('\\', "\\\\");
let wav_text = wav_output.to_string_lossy().replace('\\', "\\\\");
let midi_text = midi_output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local options = {{
sample_rate = 44100,
normalize = "rms",
target_db = -18,
true_peak_db = -1,
midi_velocity_gain = 2.0,
midi_min_velocity = 0.55,
}}
local song = pulse.song()
:add_sample(pulse.sample("{}"))
:add(
pulse.sequence()
:notes({{440.0}})
:durations({{0.25}})
:instrument("electric_piano")
:volume(0.3)
:velocity(0.25)
)
song:export_wav("{}", options)
song:export_midi("{}", options)
local ok_target, err_target = pcall(function()
song:export_wav("bad.wav", {{ normalize = "rms", target_db = 3 }})
end)
assert(ok_target == false)
assert(tostring(err_target):find("invalid export normalize option target_db: 3", 1, true))
"#,
source_text, wav_text, midi_text
))
.exec()
.expect("Lua loudness export options should run");
assert!((wav_rms_db(&wav_output) - -18.0).abs() < 0.1);
assert!(midi_note_on_velocities(&midi_output)
.iter()
.all(|velocity| *velocity >= 70));
}
#[test]
fn lua_export_options_can_select_normalized_flac_bit_depth() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let source = temp_dir.path().join("source.wav");
let output = temp_dir.path().join("lua-normalized-24.flac");
write_constant_source_wav(&source, 0.1);
let source_text = source.to_string_lossy().replace('\\', "\\\\");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local song = pulse.song():add_sample(pulse.sample("{}"))
song:export_flac("{}", {{
sample_rate = 44100,
normalize = true,
flac_bits_per_sample = 24,
}})
local ok_bits, err_bits = pcall(function()
song:export_flac("bad.flac", {{
normalize = true,
flac_bits_per_sample = 12,
}})
end)
assert(ok_bits == false)
assert(tostring(err_bits):find("invalid export flac_bits_per_sample: 12", 1, true))
"#,
source_text, output_text
))
.exec()
.expect("Lua FLAC bit-depth options should run");
assert_eq!(flac_bits_per_sample(&output), 24);
}
#[test]
fn lua_export_options_reject_invalid_sample_rates_before_rendering() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("invalid-sample-rate.wav");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local song = pulse.song()
:add(
pulse.sequence()
:notes({{440.0}})
:durations({{0.25}})
:instrument("electric_piano")
)
local invalid_rates = {{
{{ -1, "export option sample_rate must be a positive integer" }},
{{ 44100.5, "export option sample_rate must be a positive integer" }},
{{ 4294967296, "export option sample_rate must be a positive integer" }},
{{ "44100", "export option sample_rate must be a positive integer" }},
}}
for _, case in ipairs(invalid_rates) do
local ok, err = pcall(function()
song:export("{}", {{ sample_rate = case[1] }})
end)
assert(ok == false)
assert(tostring(err):find(case[2], 1, true), tostring(err))
end
"#,
output_text
))
.exec()
.expect("Lua invalid sample-rate export options should be rejected");
assert!(
!output.exists(),
"invalid export options should fail before rendering"
);
}
#[cfg(not(feature = "gpu"))]
#[test]
fn lua_gpu_export_option_reports_missing_feature_when_disabled() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("lua-gpu-request.wav");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local song = pulse.song()
:add(
pulse.sequence()
:notes({{440.0}})
:durations({{0.25}})
:instrument("electric_piano")
)
local ok, err = pcall(function()
song:export("{}", {{ gpu = true }})
end)
assert(ok == false)
assert(tostring(err):find("gpu feature is not enabled", 1, true))
"#,
output_text
))
.exec()
.expect("Lua GPU option should report a stable error");
assert!(
!output.exists(),
"failed export should not create a WAV file"
);
}
#[test]
fn lua_drum_machine_exports_wav_and_midi() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let wav_output = temp_dir.path().join("drums.wav");
let midi_output = temp_dir.path().join("drums.mid");
let wav_text = wav_output.to_string_lossy().replace('\\', "\\\\");
let midi_text = midi_output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local beat = pulse.drum_machine("808")
:steps(16)
:kick("x---x---x---x---")
:snare("----x-------x---")
:hihat("x-x-x-x-x-x-x-x-")
:grid()
local song = pulse.song()
:tempo(120)
:add_drum_grid(beat)
song:export_wav("{}")
song:export_midi("{}")
"#,
wav_text, midi_text
))
.exec()
.expect("lua drum machine export should run");
assert_eq!(&std::fs::read(&wav_output).unwrap()[0..4], b"RIFF");
assert_eq!(&std::fs::read(&midi_output).unwrap()[0..4], b"MThd");
}
#[test]
fn lua_layered_drum_aliases_export_wav() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let wav_output = temp_dir.path().join("layered-drums.wav");
let wav_text = wav_output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local beat = pulse.drum_grid()
:steps(16)
:sound("kick_808_deep_layer", {{0, 8}})
:sound("snare_808_body_layer", {{4, 12}})
:sound("hihat_909_sizzle_layer", "x-x-x-x-x-x-x-x-")
pulse.song()
:tempo(120)
:add_drum_grid(beat)
:export_wav("{}")
"#,
wav_text
))
.exec()
.expect("Lua layered drum export should run");
let bytes = std::fs::read(&wav_output).expect("wav should be readable");
assert!(bytes.len() > 44);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
}
#[test]
fn lua_effect_chain_exports_wav() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("effects-chain.wav");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local phrase = pulse.sequence()
:notes(pulse.scale("C4", "ionian"))
:durations({{0.25, 0.25, 0.25, 0.25, 0.5, 0.5, 0.5, 1.0}})
:instrument("supersaw")
:effect("filter", {{ type = "low_pass", cutoff = 1800.0, resonance = 0.4 }})
:effect("delay", {{ time = 0.25, feedback = 0.35, mix = 0.3 }})
local beat = pulse.drum_machine("808")
:steps(16)
:kick("x---x---x---x---")
:snare("----x-------x---")
:hihat("x-x-x-x-x-x-x-x-")
:grid()
:effect("compressor", {{ threshold = 0.6, ratio = 4.0, attack = 0.01, release = 0.08, makeup_gain = 1.0 }})
pulse.song()
:tempo(120)
:add(phrase)
:add_drum_grid(beat)
:master_effect("reverb", "hall")
:master_effect("limiter", "standard")
:export_wav("{}")
"#,
output_text
))
.exec()
.expect("Lua effect chain export should run");
let bytes = std::fs::read(&output).expect("wav should be readable");
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
}
#[test]
fn lua_algorithm_generators_export_wav_and_midi() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let wav_output = temp_dir.path().join("algorithmic.wav");
let midi_output = temp_dir.path().join("algorithmic.mid");
let wav_text = wav_output.to_string_lossy().replace('\\', "\\\\");
let midi_text = midi_output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local fib = pulse.generate("fibonacci", {{ length = 8 }})
local notes = pulse.map_to_scale(fib, "minor_pentatonic", "C4", 2)
local durations = pulse.normalize(
pulse.generate("van_der_corput", {{ length = 8, base = 2 }}),
0.125,
0.5
)
local phrase = pulse.sequence()
:notes(notes)
:durations(durations)
:instrument("electric_piano")
local beat = pulse.drum_grid()
:steps(16)
:sound("kick_808", pulse.generate("cantor_rhythm_16"))
:sound("snare_808", {{4, 12}})
:sound("hihat_808_closed", "x-x-x-x-x-x-x-x-")
local song = pulse.song()
:tempo(118)
:add(phrase)
:add_drum_grid(beat)
song:export_wav("{}")
song:export_midi("{}")
"#,
wav_text, midi_text
))
.exec()
.expect("Lua algorithmic export should run");
assert_eq!(&std::fs::read(&wav_output).unwrap()[0..4], b"RIFF");
assert_eq!(&std::fs::read(&midi_output).unwrap()[0..4], b"MThd");
}
#[test]
fn lua_phrase_arrangement_exports_wav_and_midi() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let wav_output = temp_dir.path().join("phrase-arrangement.wav");
let midi_output = temp_dir.path().join("phrase-arrangement.mid");
let wav_text = wav_output.to_string_lossy().replace('\\', "\\\\");
let midi_text = midi_output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local verse = pulse.phrase()
:add(
pulse.sequence()
:notes(pulse.scale("C4", "minor"))
:durations({{0.25, 0.25, 0.25, 0.25, 0.5, 0.5, 0.5, 1.0}})
:instrument("electric_piano")
)
:add_drum_grid(
pulse.drum_machine("808")
:steps(16)
:kick("x---x---x---x---")
:snare("----x-------x---")
:hihat("x-x-x-x-x-x-x-x-")
:grid()
)
:repeat_times(2)
local chorus = pulse.phrase()
:add(
pulse.sequence()
:notes(pulse.chord("C4", "maj7"))
:durations({{0.5, 0.5, 0.5, 0.5}})
:instrument("supersaw")
)
local song = pulse.song()
:tempo(118)
:add_phrase(verse)
:add_phrase(chorus)
:master_effect("limiter", "standard")
song:export_wav("{}")
song:export_midi("{}")
"#,
wav_text, midi_text
))
.exec()
.expect("Lua phrase arrangement export should run");
assert_eq!(&std::fs::read(&wav_output).unwrap()[0..4], b"RIFF");
assert_eq!(&std::fs::read(&wav_output).unwrap()[8..12], b"WAVE");
assert_eq!(&std::fs::read(&midi_output).unwrap()[0..4], b"MThd");
}
#[test]
fn lua_synthesis_algorithms_export_wav() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("synthesis-algorithms.wav");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local fm = pulse.sequence()
:notes({{440.0, 550.0, 660.0}})
:durations({{0.25, 0.25, 0.5}})
:instrument("electric_piano")
:synth("fm", "bell")
local additive = pulse.sequence()
:notes({{220.0, 330.0, 440.0}})
:durations({{0.5, 0.25, 0.25}})
:instrument("electric_piano")
:synth("additive", {{1.0, 0.5, 0.25, 0.125}})
local wavetable = pulse.sequence()
:notes({{330.0, 392.0, 494.0}})
:durations({{0.25, 0.25, 0.5}})
:instrument("electric_piano")
:synth("wavetable")
pulse.song()
:tempo(112)
:add(fm)
:add(additive)
:add(wavetable)
:export_wav("{}")
"#,
output_text
))
.exec()
.expect("Lua synthesis export should run");
let bytes = std::fs::read(&output).expect("wav should be readable");
assert!(bytes.len() > 44);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
}
#[test]
fn lua_karplus_strong_synthesis_exports_wav() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let output = temp_dir.path().join("karplus-strong.wav");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local pluck = pulse.sequence()
:notes({{261.63, 329.63, 392.00, 523.25}})
:durations({{0.25, 0.25, 0.25, 0.5}})
:instrument("electric_piano")
:synth("karplus_strong", {{
decay = 0.996,
brightness = 0.7,
seed = 1234,
}})
pulse.song()
:tempo(120)
:add(pluck)
:export_wav("{}")
"#,
output_text
))
.exec()
.expect("Lua Karplus-Strong export should run");
let bytes = std::fs::read(&output).expect("wav should be readable");
assert!(bytes.len() > 44);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
}
#[test]
fn lua_granular_synthesis_exports_wav() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let source = temp_dir.path().join("source.wav");
let output = temp_dir.path().join("granular.wav");
write_test_source_wav(&source);
let source_text = source.to_string_lossy().replace('\\', "\\\\");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local texture = pulse.sequence()
:synth("granular", {{
source = "{}",
duration = 0.75,
preset = "texture",
grain_size_ms = 25.0,
density = 0.75,
position = 0.4,
position_spread = 0.08,
pitch_variation = 0.02,
}})
pulse.song()
:tempo(100)
:add(texture)
:export_wav("{}")
"#,
source_text, output_text
))
.exec()
.expect("Lua granular export should run");
let bytes = std::fs::read(&output).expect("wav should be readable");
assert!(bytes.len() > 44);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
}
#[test]
fn lua_sample_clip_exports_wav_with_pitch_and_time_adjustments() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let source = temp_dir.path().join("source.wav");
let output = temp_dir.path().join("sample-clip.wav");
write_test_source_wav(&source);
let source_text = source.to_string_lossy().replace('\\', "\\\\");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local clip = pulse.sample("{}")
:slice(0.0, 0.05)
:reverse()
:loop_for(0.1)
:normalize()
:fade_in(0.005)
:fade_out(0.005)
:pitch_shift(7.0)
:time_stretch(1.25)
:rate(0.75)
:gain(0.8)
pulse.song()
:tempo(100)
:add_sample(clip)
:export_wav("{}")
"#,
source_text, output_text
))
.exec()
.expect("Lua sample clip export should run");
let bytes = std::fs::read(&output).expect("wav should be readable");
assert!(bytes.len() > 44);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
}
#[test]
fn lua_phrase_sample_clip_repeats_into_wav_export() {
let temp_dir = tempfile::tempdir().expect("temp dir should be created");
let source = temp_dir.path().join("source.wav");
let output = temp_dir.path().join("phrase-sample-clip.wav");
write_test_source_wav(&source);
let source_text = source.to_string_lossy().replace('\\', "\\\\");
let output_text = output.to_string_lossy().replace('\\', "\\\\");
let lua = mlua::Lua::new();
mlua_pulse::register_package_loader(&lua).expect("pulse module should register");
lua.load(format!(
r#"
local pulse = require("pulse")
local clip = pulse.sample("{}")
:at(0.125)
:slice(0.0, 0.05)
:gain(0.7)
:time_stretch(1.1)
local phrase = pulse.phrase()
:add_sample(clip)
:repeat_times(2)
pulse.song()
:add_phrase(phrase)
:export_wav("{}")
"#,
source_text, output_text
))
.exec()
.expect("Lua phrase sample clip export should run");
let bytes = std::fs::read(&output).expect("wav should be readable");
assert!(bytes.len() > 44);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WAVE");
}