mp3 and aac export

This commit is contained in:
Skyler Lehmkuhl 2025-12-04 15:58:37 -05:00
parent 727d782190
commit 2cd7682399
7 changed files with 1145 additions and 91 deletions

View File

@ -18,8 +18,7 @@ pathdiff = "0.2"
# Audio export
hound = "3.5"
# TODO: Add MP3 support with a different crate
# mp3lame-encoder API is too complex, need to find a better option
ffmpeg-next = "8.0" # For MP3/AAC encoding
# Node-based audio graph dependencies
dasp_graph = "0.11"

View File

@ -10,7 +10,8 @@ use std::path::Path;
pub enum ExportFormat {
Wav,
Flac,
// TODO: Add MP3 support
Mp3,
Aac,
}
impl ExportFormat {
@ -19,6 +20,8 @@ impl ExportFormat {
match self {
ExportFormat::Wav => "wav",
ExportFormat::Flac => "flac",
ExportFormat::Mp3 => "mp3",
ExportFormat::Aac => "m4a",
}
}
}
@ -72,13 +75,23 @@ pub fn export_audio<P: AsRef<Path>>(
mut event_tx: Option<&mut rtrb::Producer<AudioEvent>>,
) -> Result<(), String>
{
// Render the project to memory
// Route to appropriate export implementation based on format
match settings.format {
ExportFormat::Wav | ExportFormat::Flac => {
// Render to memory then write (existing path)
let samples = render_to_memory(project, pool, midi_pool, settings, event_tx)?;
// Write to file based on format
match settings.format {
ExportFormat::Wav => write_wav(&samples, settings, output_path)?,
ExportFormat::Flac => write_flac(&samples, settings, output_path)?,
_ => unreachable!(),
}
}
ExportFormat::Mp3 => {
export_mp3(project, pool, midi_pool, settings, output_path, event_tx)?;
}
ExportFormat::Aac => {
export_aac(project, pool, midi_pool, settings, output_path, event_tx)?;
}
}
Ok(())
@ -270,7 +283,319 @@ fn write_flac<P: AsRef<Path>>(
Ok(())
}
// TODO: Add MP3 export support with a better library
/// Export audio as MP3 using FFmpeg
fn export_mp3<P: AsRef<Path>>(
project: &mut Project,
pool: &AudioPool,
midi_pool: &MidiClipPool,
settings: &ExportSettings,
output_path: P,
mut event_tx: Option<&mut rtrb::Producer<AudioEvent>>,
) -> Result<(), String> {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
// FFmpeg encoding doesn't support cancellation in this implementation
let cancel_flag = Arc::new(AtomicBool::new(false));
// Initialize FFmpeg
ffmpeg_next::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?;
// Step 1: Render audio to memory
let pcm_samples = render_to_memory(project, pool, midi_pool, settings, event_tx)?;
// Check for cancellation
if cancel_flag.load(Ordering::Relaxed) {
return Err("Export cancelled".to_string());
}
// Step 2: Set up FFmpeg encoder
let encoder_codec = ffmpeg_next::encoder::find(ffmpeg_next::codec::Id::MP3)
.ok_or("MP3 encoder (libmp3lame) not found")?;
// Create output file
let mut output = ffmpeg_next::format::output(&output_path)
.map_err(|e| format!("Failed to create output file: {}", e))?;
// Create encoder
let mut encoder = ffmpeg_next::codec::Context::new_with_codec(encoder_codec)
.encoder()
.audio()
.map_err(|e| format!("Failed to create encoder: {}", e))?;
// Configure encoder
let channel_layout = match settings.channels {
1 => ffmpeg_next::channel_layout::ChannelLayout::MONO,
2 => ffmpeg_next::channel_layout::ChannelLayout::STEREO,
_ => return Err(format!("Unsupported channel count: {}", settings.channels)),
};
encoder.set_rate(settings.sample_rate as i32);
encoder.set_channel_layout(channel_layout);
encoder.set_format(ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Planar));
encoder.set_bit_rate((settings.mp3_bitrate * 1000) as usize);
encoder.set_time_base(ffmpeg_next::Rational(1, settings.sample_rate as i32));
// Open encoder
let mut encoder = encoder.open_as(encoder_codec)
.map_err(|e| format!("Failed to open MP3 encoder: {}", e))?;
// Add stream and set parameters
{
let mut stream = output.add_stream(encoder_codec)
.map_err(|e| format!("Failed to add stream: {}", e))?;
stream.set_parameters(&encoder);
}
// Write header
output.write_header()
.map_err(|e| format!("Failed to write header: {}", e))?;
// Step 3: Encode frames and write to output
let num_frames = pcm_samples.len() / settings.channels as usize;
let planar_samples = convert_to_planar_i16(&pcm_samples, settings.channels);
// Get encoder frame size
let frame_size = encoder.frame_size();
let samples_per_frame = if frame_size > 0 {
frame_size as usize
} else {
1152 // Default MP3 frame size
};
// Encode in chunks
let mut samples_encoded = 0;
while samples_encoded < num_frames {
if cancel_flag.load(Ordering::Relaxed) {
return Err("Export cancelled".to_string());
}
let samples_remaining = num_frames - samples_encoded;
let chunk_size = samples_remaining.min(samples_per_frame);
// Create audio frame
let mut frame = ffmpeg_next::frame::Audio::new(
ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Planar),
chunk_size,
channel_layout,
);
frame.set_rate(settings.sample_rate);
// Copy planar samples to frame
unsafe {
for ch in 0..settings.channels as usize {
let plane = frame.data_mut(ch);
let offset = samples_encoded;
let src = &planar_samples[ch][offset..offset + chunk_size];
std::ptr::copy_nonoverlapping(
src.as_ptr() as *const u8,
plane.as_mut_ptr(),
chunk_size * std::mem::size_of::<i16>(),
);
}
}
// Send frame to encoder
encoder.send_frame(&frame)
.map_err(|e| format!("Failed to send frame: {}", e))?;
// Receive and write packets
receive_and_write_packets(&mut encoder, &mut output)?;
samples_encoded += chunk_size;
}
// Flush encoder
encoder.send_eof()
.map_err(|e| format!("Failed to send EOF: {}", e))?;
receive_and_write_packets(&mut encoder, &mut output)?;
// Write trailer
output.write_trailer()
.map_err(|e| format!("Failed to write trailer: {}", e))?;
Ok(())
}
/// Export audio as AAC using FFmpeg
fn export_aac<P: AsRef<Path>>(
project: &mut Project,
pool: &AudioPool,
midi_pool: &MidiClipPool,
settings: &ExportSettings,
output_path: P,
mut event_tx: Option<&mut rtrb::Producer<AudioEvent>>,
) -> Result<(), String> {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let cancel_flag = Arc::new(AtomicBool::new(false));
// Initialize FFmpeg
ffmpeg_next::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?;
// Step 1: Render audio to memory
let pcm_samples = render_to_memory(project, pool, midi_pool, settings, event_tx)?;
// Check for cancellation
if cancel_flag.load(Ordering::Relaxed) {
return Err("Export cancelled".to_string());
}
// Step 2: Set up FFmpeg encoder
let encoder_codec = ffmpeg_next::encoder::find(ffmpeg_next::codec::Id::AAC)
.ok_or("AAC encoder not found")?;
// Create output file
let mut output = ffmpeg_next::format::output(&output_path)
.map_err(|e| format!("Failed to create output file: {}", e))?;
// Create encoder
let mut encoder = ffmpeg_next::codec::Context::new_with_codec(encoder_codec)
.encoder()
.audio()
.map_err(|e| format!("Failed to create encoder: {}", e))?;
// Configure encoder
let channel_layout = match settings.channels {
1 => ffmpeg_next::channel_layout::ChannelLayout::MONO,
2 => ffmpeg_next::channel_layout::ChannelLayout::STEREO,
_ => return Err(format!("Unsupported channel count: {}", settings.channels)),
};
encoder.set_rate(settings.sample_rate as i32);
encoder.set_channel_layout(channel_layout);
encoder.set_format(ffmpeg_next::format::Sample::F32(ffmpeg_next::format::sample::Type::Planar));
encoder.set_bit_rate((settings.mp3_bitrate * 1000) as usize);
encoder.set_time_base(ffmpeg_next::Rational(1, settings.sample_rate as i32));
// Open encoder
let mut encoder = encoder.open_as(encoder_codec)
.map_err(|e| format!("Failed to open AAC encoder: {}", e))?;
// Add stream and set parameters
{
let mut stream = output.add_stream(encoder_codec)
.map_err(|e| format!("Failed to add stream: {}", e))?;
stream.set_parameters(&encoder);
}
// Write header
output.write_header()
.map_err(|e| format!("Failed to write header: {}", e))?;
// Step 3: Encode frames and write to output
let num_frames = pcm_samples.len() / settings.channels as usize;
let planar_samples = convert_to_planar_f32(&pcm_samples, settings.channels);
// Get encoder frame size
let frame_size = encoder.frame_size();
let samples_per_frame = if frame_size > 0 {
frame_size as usize
} else {
1024 // Default AAC frame size
};
// Encode in chunks
let mut samples_encoded = 0;
while samples_encoded < num_frames {
if cancel_flag.load(Ordering::Relaxed) {
return Err("Export cancelled".to_string());
}
let samples_remaining = num_frames - samples_encoded;
let chunk_size = samples_remaining.min(samples_per_frame);
// Create audio frame
let mut frame = ffmpeg_next::frame::Audio::new(
ffmpeg_next::format::Sample::F32(ffmpeg_next::format::sample::Type::Planar),
chunk_size,
channel_layout,
);
frame.set_rate(settings.sample_rate);
// Copy planar samples to frame
unsafe {
for ch in 0..settings.channels as usize {
let plane = frame.data_mut(ch);
let offset = samples_encoded;
let src = &planar_samples[ch][offset..offset + chunk_size];
std::ptr::copy_nonoverlapping(
src.as_ptr() as *const u8,
plane.as_mut_ptr(),
chunk_size * std::mem::size_of::<f32>(),
);
}
}
// Send frame to encoder
encoder.send_frame(&frame)
.map_err(|e| format!("Failed to send frame: {}", e))?;
// Receive and write packets
receive_and_write_packets(&mut encoder, &mut output)?;
samples_encoded += chunk_size;
}
// Flush encoder
encoder.send_eof()
.map_err(|e| format!("Failed to send EOF: {}", e))?;
receive_and_write_packets(&mut encoder, &mut output)?;
// Write trailer
output.write_trailer()
.map_err(|e| format!("Failed to write trailer: {}", e))?;
Ok(())
}
/// Convert interleaved f32 samples to planar i16 format
fn convert_to_planar_i16(interleaved: &[f32], channels: u32) -> Vec<Vec<i16>> {
let num_frames = interleaved.len() / channels as usize;
let mut planar = vec![vec![0i16; num_frames]; channels as usize];
for (i, chunk) in interleaved.chunks(channels as usize).enumerate() {
for (ch, &sample) in chunk.iter().enumerate() {
let clamped = sample.max(-1.0).min(1.0);
planar[ch][i] = (clamped * 32767.0) as i16;
}
}
planar
}
/// Convert interleaved f32 samples to planar f32 format
fn convert_to_planar_f32(interleaved: &[f32], channels: u32) -> Vec<Vec<f32>> {
let num_frames = interleaved.len() / channels as usize;
let mut planar = vec![vec![0.0f32; num_frames]; channels as usize];
for (i, chunk) in interleaved.chunks(channels as usize).enumerate() {
for (ch, &sample) in chunk.iter().enumerate() {
planar[ch][i] = sample;
}
}
planar
}
/// Receive encoded packets and write to output
fn receive_and_write_packets(
encoder: &mut ffmpeg_next::encoder::Audio,
output: &mut ffmpeg_next::format::context::Output,
) -> Result<(), String> {
let mut encoded = ffmpeg_next::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(output)
.map_err(|e| format!("Failed to write packet: {}", e))?;
}
Ok(())
}
#[cfg(test)]
mod tests {

View File

@ -24,7 +24,7 @@ uuid = { version = "1.0", features = ["v4", "serde"] }
daw-backend = { path = "../../daw-backend" }
# Video decoding
ffmpeg-next = "7.0"
ffmpeg-next = "8.0"
lru = "0.12"
# File I/O

View File

@ -8,7 +8,7 @@ lightningbeam-core = { path = "../lightningbeam-core" }
daw-backend = { path = "../../daw-backend" }
rtrb = "0.3"
cpal = "0.15"
ffmpeg-next = "7.0"
ffmpeg-next = "8.0"
# UI Framework
eframe = { workspace = true }

View File

@ -0,0 +1,413 @@
/// Minimal test program to validate FFmpeg audio encoding workflow
///
/// This program tests encoding raw PCM samples to MP3 using ffmpeg-next.
/// Run with: cargo run --example ffmpeg_test
use std::path::Path;
fn main() -> Result<(), String> {
println!("Testing FFmpeg audio encoding...");
// Initialize FFmpeg
ffmpeg_next::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?;
// Test 1: List available encoders
println!("\nAvailable MP3 encoders:");
if let Some(encoder) = ffmpeg_next::encoder::find(ffmpeg_next::codec::Id::MP3) {
println!(" - Found MP3 encoder: {}", encoder.name());
} else {
println!(" - No MP3 encoder found!");
}
println!("\nAvailable AAC encoders:");
if let Some(encoder) = ffmpeg_next::encoder::find(ffmpeg_next::codec::Id::AAC) {
println!(" - Found AAC encoder: {}", encoder.name());
} else {
println!(" - No AAC encoder found!");
}
// Test 2: Create a simple MP3 encoder and encode silence
test_mp3_encoding()?;
// Test 3: Create a simple AAC encoder and encode silence
test_aac_encoding()?;
println!("\n✅ All tests passed!");
Ok(())
}
fn test_mp3_encoding() -> Result<(), String> {
println!("\nTest: Encoding 1 second of silence to MP3...");
// Output file
let output_path = "/tmp/test_silence.mp3";
// Generate 1 second of stereo silence at 44.1 kHz
let sample_rate = 44100;
let channels = 2;
let duration_secs = 1.0;
let num_samples = (sample_rate as f64 * duration_secs * channels as f64) as usize;
let pcm_samples: Vec<f32> = vec![0.0; num_samples]; // Silence
println!(" Generated {} PCM samples ({}Hz, {} channels, {:.1}s)",
num_samples, sample_rate, channels, duration_secs);
// Encode to MP3
encode_pcm_to_mp3(&pcm_samples, sample_rate, channels, 320, output_path)?;
// Check output file exists
if Path::new(output_path).exists() {
let metadata = std::fs::metadata(output_path).unwrap();
println!(" ✅ Created MP3 file: {} ({} bytes)", output_path, metadata.len());
} else {
return Err("MP3 file was not created!".to_string());
}
Ok(())
}
fn test_aac_encoding() -> Result<(), String> {
println!("\nTest: Encoding 1 second of silence to AAC...");
// Output file
let output_path = "/tmp/test_silence.m4a";
// Generate 1 second of stereo silence at 44.1 kHz
let sample_rate = 44100;
let channels = 2;
let duration_secs = 1.0;
let num_samples = (sample_rate as f64 * duration_secs * channels as f64) as usize;
let pcm_samples: Vec<f32> = vec![0.0; num_samples]; // Silence
println!(" Generated {} PCM samples ({}Hz, {} channels, {:.1}s)",
num_samples, sample_rate, channels, duration_secs);
// Encode to AAC
encode_pcm_to_aac(&pcm_samples, sample_rate, channels, 192, output_path)?;
// Check output file exists
if Path::new(output_path).exists() {
let metadata = std::fs::metadata(output_path).unwrap();
println!(" ✅ Created AAC file: {} ({} bytes)", output_path, metadata.len());
} else {
return Err("AAC file was not created!".to_string());
}
Ok(())
}
/// Encode raw PCM samples to MP3 using ffmpeg-next
fn encode_pcm_to_mp3(
samples: &[f32],
sample_rate: u32,
channels: u32,
bitrate_kbps: u32,
output_path: &str,
) -> Result<(), String> {
use ffmpeg_next as ffmpeg;
// Find MP3 encoder
let encoder_codec = ffmpeg::encoder::find(ffmpeg::codec::Id::MP3)
.ok_or("MP3 encoder not found")?;
println!(" Using encoder: {}", encoder_codec.name());
// Create output format context FIRST (like transcode example)
let mut output = ffmpeg::format::output(&output_path)
.map_err(|e| format!("Failed to create output file: {}", e))?;
// Don't use stream parameters - create encoder directly
// The stream was just added but has no parameters set yet
let mut encoder = ffmpeg::codec::Context::new_with_codec(encoder_codec)
.encoder()
.audio()
.map_err(|e| format!("Failed to create encoder: {}", e))?;
println!(" Created encoder directly from codec");
// Determine channel layout first
let channel_layout = match channels {
1 => ffmpeg::channel_layout::ChannelLayout::MONO,
2 => ffmpeg::channel_layout::ChannelLayout::STEREO,
_ => return Err(format!("Unsupported channel count: {}", channels)),
};
// Configure encoder with explicit format (required in ffmpeg-next 8.0)
encoder.set_rate(sample_rate as i32);
encoder.set_channel_layout(channel_layout);
// Set format to S16 Planar (s16p) which libmp3lame supports
use ffmpeg_next::format::sample::Type;
use ffmpeg_next::format::Sample;
encoder.set_format(Sample::I16(Type::Planar));
encoder.set_bit_rate((bitrate_kbps * 1000) as usize);
encoder.set_time_base(ffmpeg::Rational(1, sample_rate as i32));
println!(" Encoder configured: {}Hz, {} channels, {} kbps",
sample_rate, channels, bitrate_kbps);
println!(" Format before open: {:?}", encoder.format());
// Open encoder (like transcode-audio example)
let mut encoder = encoder.open_as(encoder_codec)
.map_err(|e| format!("Failed to open encoder: {}", e))?;
println!(" ✅ Encoder opened successfully!");
println!(" Opened encoder format: {:?}", encoder.format());
// Now add stream and set its parameters from the opened encoder
let mut stream = output.add_stream(encoder_codec)
.map_err(|e| format!("Failed to add stream: {}", e))?;
stream.set_parameters(&encoder);
// Write header
output.write_header()
.map_err(|e| format!("Failed to write header: {}", e))?;
println!(" Encoding {} samples...", samples.len());
// Convert interleaved f32 to planar i16
let num_frames = samples.len() / channels as usize;
let planar_samples = convert_to_planar_i16(samples, channels);
// Get encoder frame size
let frame_size = encoder.frame_size();
let samples_per_frame = if frame_size > 0 {
frame_size as usize
} else {
1152 // Default MP3 frame size
};
println!(" Frame size: {} samples", samples_per_frame);
// Encode in chunks
let mut samples_encoded = 0;
while samples_encoded < num_frames {
let samples_remaining = num_frames - samples_encoded;
let chunk_size = samples_remaining.min(samples_per_frame);
// Create audio frame
let mut frame = ffmpeg::frame::Audio::new(
ffmpeg::format::Sample::I16(ffmpeg::format::sample::Type::Planar),
chunk_size,
channel_layout,
);
frame.set_rate(sample_rate);
// Copy planar samples to frame
unsafe {
for ch in 0..channels as usize {
let plane = frame.data_mut(ch);
let offset = samples_encoded;
let src = &planar_samples[ch][offset..offset + chunk_size];
std::ptr::copy_nonoverlapping(
src.as_ptr() as *const u8,
plane.as_mut_ptr(),
chunk_size * std::mem::size_of::<i16>(),
);
}
}
// Send frame to encoder
encoder.send_frame(&frame)
.map_err(|e| format!("Failed to send frame: {}", e))?;
// Receive and write packets
receive_and_write_packets(&mut encoder, &mut output)?;
samples_encoded += chunk_size;
}
// Flush encoder
encoder.send_eof()
.map_err(|e| format!("Failed to send EOF: {}", e))?;
receive_and_write_packets(&mut encoder, &mut output)?;
// Write trailer
output.write_trailer()
.map_err(|e| format!("Failed to write trailer: {}", e))?;
println!(" Encoding complete - {} frames encoded", num_frames);
Ok(())
}
/// Convert interleaved f32 samples to planar i16 format
fn convert_to_planar_i16(interleaved: &[f32], channels: u32) -> Vec<Vec<i16>> {
let num_frames = interleaved.len() / channels as usize;
let mut planar = vec![vec![0i16; num_frames]; channels as usize];
for (i, chunk) in interleaved.chunks(channels as usize).enumerate() {
for (ch, &sample) in chunk.iter().enumerate() {
// Clamp and convert f32 (-1.0 to 1.0) to i16
let clamped = sample.max(-1.0).min(1.0);
planar[ch][i] = (clamped * 32767.0) as i16;
}
}
planar
}
/// Receive encoded packets and write to output
fn receive_and_write_packets(
encoder: &mut ffmpeg_next::encoder::Audio,
output: &mut ffmpeg_next::format::context::Output,
) -> Result<(), String> {
let mut encoded = ffmpeg_next::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(output)
.map_err(|e| format!("Failed to write packet: {}", e))?;
}
Ok(())
}
/// Encode raw PCM samples to AAC using ffmpeg-next
fn encode_pcm_to_aac(
samples: &[f32],
sample_rate: u32,
channels: u32,
bitrate_kbps: u32,
output_path: &str,
) -> Result<(), String> {
use ffmpeg_next as ffmpeg;
// Find AAC encoder
let encoder_codec = ffmpeg::encoder::find(ffmpeg::codec::Id::AAC)
.ok_or("AAC encoder not found")?;
println!(" Using encoder: {}", encoder_codec.name());
// Create output format context
let mut output = ffmpeg::format::output(&output_path)
.map_err(|e| format!("Failed to create output file: {}", e))?;
// Create encoder directly from codec
let mut encoder = ffmpeg::codec::Context::new_with_codec(encoder_codec)
.encoder()
.audio()
.map_err(|e| format!("Failed to create encoder: {}", e))?;
println!(" Created encoder directly from codec");
// Determine channel layout
let channel_layout = match channels {
1 => ffmpeg::channel_layout::ChannelLayout::MONO,
2 => ffmpeg::channel_layout::ChannelLayout::STEREO,
_ => return Err(format!("Unsupported channel count: {}", channels)),
};
// Configure encoder - AAC supports F32 Planar (fltp)
encoder.set_rate(sample_rate as i32);
encoder.set_channel_layout(channel_layout);
encoder.set_format(ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar));
encoder.set_bit_rate((bitrate_kbps * 1000) as usize);
encoder.set_time_base(ffmpeg::Rational(1, sample_rate as i32));
println!(" Encoder configured: {}Hz, {} channels, {} kbps",
sample_rate, channels, bitrate_kbps);
println!(" Format before open: {:?}", encoder.format());
// Open encoder
let mut encoder = encoder.open_as(encoder_codec)
.map_err(|e| format!("Failed to open encoder: {}", e))?;
println!(" ✅ Encoder opened successfully!");
println!(" Opened encoder format: {:?}", encoder.format());
// Add stream and set parameters
{
let mut stream = output.add_stream(encoder_codec)
.map_err(|e| format!("Failed to add stream: {}", e))?;
stream.set_parameters(&encoder);
}
// Write header
output.write_header()
.map_err(|e| format!("Failed to write header: {}", e))?;
println!(" Encoding {} samples...", samples.len());
// Convert interleaved f32 to planar f32
let num_frames = samples.len() / channels as usize;
let planar_samples = convert_to_planar_f32(samples, channels);
// Get encoder frame size
let frame_size = encoder.frame_size();
let samples_per_frame = if frame_size > 0 {
frame_size as usize
} else {
1024 // Default AAC frame size
};
println!(" Frame size: {} samples", samples_per_frame);
// Encode in chunks
let mut samples_encoded = 0;
while samples_encoded < num_frames {
let samples_remaining = num_frames - samples_encoded;
let chunk_size = samples_remaining.min(samples_per_frame);
// Create audio frame
let mut frame = ffmpeg::frame::Audio::new(
ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar),
chunk_size,
channel_layout,
);
frame.set_rate(sample_rate);
// Copy planar samples to frame
unsafe {
for ch in 0..channels as usize {
let plane = frame.data_mut(ch);
let offset = samples_encoded;
let src = &planar_samples[ch][offset..offset + chunk_size];
std::ptr::copy_nonoverlapping(
src.as_ptr() as *const u8,
plane.as_mut_ptr(),
chunk_size * std::mem::size_of::<f32>(),
);
}
}
// Send frame to encoder
encoder.send_frame(&frame)
.map_err(|e| format!("Failed to send frame: {}", e))?;
// Receive and write packets
receive_and_write_packets(&mut encoder, &mut output)?;
samples_encoded += chunk_size;
}
// Flush encoder
encoder.send_eof()
.map_err(|e| format!("Failed to send EOF: {}", e))?;
receive_and_write_packets(&mut encoder, &mut output)?;
// Write trailer
output.write_trailer()
.map_err(|e| format!("Failed to write trailer: {}", e))?;
println!(" Encoding complete - {} frames encoded", num_frames);
Ok(())
}
/// Convert interleaved f32 samples to planar f32 format
fn convert_to_planar_f32(interleaved: &[f32], channels: u32) -> Vec<Vec<f32>> {
let num_frames = interleaved.len() / channels as usize;
let mut planar = vec![vec![0.0f32; num_frames]; channels as usize];
for (i, chunk) in interleaved.chunks(channels as usize).enumerate() {
for (ch, &sample) in chunk.iter().enumerate() {
planar[ch][i] = sample;
}
}
planar
}

View File

@ -86,32 +86,347 @@ fn export_audio_daw_backend<P: AsRef<Path>>(
/// Export audio as MP3 using FFmpeg
fn export_audio_ffmpeg_mp3<P: AsRef<Path>>(
_project: &mut Project,
_pool: &AudioPool,
_midi_pool: &MidiClipPool,
_settings: &AudioExportSettings,
_output_path: P,
_cancel_flag: &Arc<AtomicBool>,
project: &mut Project,
pool: &AudioPool,
midi_pool: &MidiClipPool,
settings: &AudioExportSettings,
output_path: P,
cancel_flag: &Arc<AtomicBool>,
) -> Result<(), String> {
// TODO: Implement MP3 export using FFmpeg
// The FFmpeg encoder API is complex and needs more investigation
// For now, users can export as WAV or FLAC (both fully working)
Err("MP3 export is not yet implemented. Please use WAV or FLAC format for now, or export as WAV and convert using an external tool.".to_string())
use ffmpeg_next as ffmpeg;
// Initialize FFmpeg
ffmpeg::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?;
// Convert settings to DAW backend format
let daw_settings = DawExportSettings {
format: ExportFormat::Wav, // Unused, but required
sample_rate: settings.sample_rate,
channels: settings.channels,
bit_depth: 16, // Unused
mp3_bitrate: settings.bitrate_kbps,
start_time: settings.start_time,
end_time: settings.end_time,
};
// Step 1: Render audio to memory
let pcm_samples = render_to_memory(
project,
pool,
midi_pool,
&daw_settings,
None, // No progress events for now
)?;
// Check for cancellation
if cancel_flag.load(Ordering::Relaxed) {
return Err("Export cancelled".to_string());
}
// Step 2: Set up FFmpeg encoder
let encoder_codec = ffmpeg::encoder::find(ffmpeg::codec::Id::MP3)
.ok_or("MP3 encoder (libmp3lame) not found")?;
// Create output file
let mut output = ffmpeg::format::output(&output_path)
.map_err(|e| format!("Failed to create output file: {}", e))?;
// Create encoder
let mut encoder = ffmpeg::codec::Context::new_with_codec(encoder_codec)
.encoder()
.audio()
.map_err(|e| format!("Failed to create encoder: {}", e))?;
// Configure encoder
let channel_layout = match settings.channels {
1 => ffmpeg::channel_layout::ChannelLayout::MONO,
2 => ffmpeg::channel_layout::ChannelLayout::STEREO,
_ => return Err(format!("Unsupported channel count: {}", settings.channels)),
};
encoder.set_rate(settings.sample_rate as i32);
encoder.set_channel_layout(channel_layout);
encoder.set_format(ffmpeg::format::Sample::I16(ffmpeg::format::sample::Type::Planar));
encoder.set_bit_rate((settings.bitrate_kbps * 1000) as usize);
encoder.set_time_base(ffmpeg::Rational(1, settings.sample_rate as i32));
// Open encoder
let mut encoder = encoder.open_as(encoder_codec)
.map_err(|e| format!("Failed to open MP3 encoder: {}", e))?;
// Add stream and set parameters
{
let mut stream = output.add_stream(encoder_codec)
.map_err(|e| format!("Failed to add stream: {}", e))?;
stream.set_parameters(&encoder);
} // Drop stream here to release the borrow
// Write header
output.write_header()
.map_err(|e| format!("Failed to write header: {}", e))?;
// Step 3: Encode frames and write to output
// Convert interleaved f32 samples to planar i16 format
let num_frames = pcm_samples.len() / settings.channels as usize;
let mut planar_samples = convert_to_planar_i16(&pcm_samples, settings.channels);
// Get encoder frame size
let frame_size = encoder.frame_size();
let samples_per_frame = if frame_size > 0 {
frame_size as usize
} else {
1152 // Default MP3 frame size
};
// Encode in chunks
let mut samples_encoded = 0;
while samples_encoded < num_frames {
if cancel_flag.load(Ordering::Relaxed) {
return Err("Export cancelled".to_string());
}
let samples_remaining = num_frames - samples_encoded;
let chunk_size = samples_remaining.min(samples_per_frame);
// Create audio frame
let mut frame = ffmpeg::frame::Audio::new(
ffmpeg::format::Sample::I16(ffmpeg::format::sample::Type::Planar),
chunk_size,
channel_layout,
);
frame.set_rate(settings.sample_rate);
// Copy planar samples to frame
unsafe {
for ch in 0..settings.channels as usize {
let plane = frame.data_mut(ch);
let offset = samples_encoded;
let src = &planar_samples[ch][offset..offset + chunk_size];
std::ptr::copy_nonoverlapping(
src.as_ptr() as *const u8,
plane.as_mut_ptr(),
chunk_size * std::mem::size_of::<i16>(),
);
}
}
// Send frame to encoder
encoder.send_frame(&frame)
.map_err(|e| format!("Failed to send frame: {}", e))?;
// Receive and write packets
receive_and_write_packets(&mut encoder, &mut output)?;
samples_encoded += chunk_size;
}
// Flush encoder
encoder.send_eof()
.map_err(|e| format!("Failed to send EOF: {}", e))?;
receive_and_write_packets(&mut encoder, &mut output)?;
// Write trailer
output.write_trailer()
.map_err(|e| format!("Failed to write trailer: {}", e))?;
Ok(())
}
/// Convert interleaved f32 samples to planar i16 format
fn convert_to_planar_i16(interleaved: &[f32], channels: u32) -> Vec<Vec<i16>> {
let num_frames = interleaved.len() / channels as usize;
let mut planar = vec![vec![0i16; num_frames]; channels as usize];
for (i, chunk) in interleaved.chunks(channels as usize).enumerate() {
for (ch, &sample) in chunk.iter().enumerate() {
// Clamp and convert f32 (-1.0 to 1.0) to i16
let clamped = sample.max(-1.0).min(1.0);
planar[ch][i] = (clamped * 32767.0) as i16;
}
}
planar
}
/// Convert interleaved f32 samples to planar f32 format
fn convert_to_planar_f32(interleaved: &[f32], channels: u32) -> Vec<Vec<f32>> {
let num_frames = interleaved.len() / channels as usize;
let mut planar = vec![vec![0.0f32; num_frames]; channels as usize];
for (i, chunk) in interleaved.chunks(channels as usize).enumerate() {
for (ch, &sample) in chunk.iter().enumerate() {
planar[ch][i] = sample;
}
}
planar
}
/// Receive encoded packets and write to output
fn receive_and_write_packets(
encoder: &mut ffmpeg_next::encoder::Audio,
output: &mut ffmpeg_next::format::context::Output,
) -> Result<(), String> {
let mut encoded = ffmpeg_next::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(output)
.map_err(|e| format!("Failed to write packet: {}", e))?;
}
Ok(())
}
/// Export audio as AAC using FFmpeg
fn export_audio_ffmpeg_aac<P: AsRef<Path>>(
_project: &mut Project,
_pool: &AudioPool,
_midi_pool: &MidiClipPool,
_settings: &AudioExportSettings,
_output_path: P,
_cancel_flag: &Arc<AtomicBool>,
project: &mut Project,
pool: &AudioPool,
midi_pool: &MidiClipPool,
settings: &AudioExportSettings,
output_path: P,
cancel_flag: &Arc<AtomicBool>,
) -> Result<(), String> {
// TODO: Implement AAC export using FFmpeg
// The FFmpeg encoder API is complex and needs more investigation
// For now, users can export as WAV or FLAC (both fully working)
Err("AAC export is not yet implemented. Please use WAV or FLAC format for now, or export as WAV and convert using an external tool.".to_string())
use ffmpeg_next as ffmpeg;
// Initialize FFmpeg
ffmpeg::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?;
// Convert settings to DAW backend format
let daw_settings = DawExportSettings {
format: ExportFormat::Wav, // Unused, but required
sample_rate: settings.sample_rate,
channels: settings.channels,
bit_depth: 16, // Unused
mp3_bitrate: settings.bitrate_kbps,
start_time: settings.start_time,
end_time: settings.end_time,
};
// Step 1: Render audio to memory
let pcm_samples = render_to_memory(
project,
pool,
midi_pool,
&daw_settings,
None, // No progress events for now
)?;
// Check for cancellation
if cancel_flag.load(Ordering::Relaxed) {
return Err("Export cancelled".to_string());
}
// Step 2: Set up FFmpeg encoder
let encoder_codec = ffmpeg::encoder::find(ffmpeg::codec::Id::AAC)
.ok_or("AAC encoder not found")?;
// Create output file
let mut output = ffmpeg::format::output(&output_path)
.map_err(|e| format!("Failed to create output file: {}", e))?;
// Create encoder
let mut encoder = ffmpeg::codec::Context::new_with_codec(encoder_codec)
.encoder()
.audio()
.map_err(|e| format!("Failed to create encoder: {}", e))?;
// Configure encoder
let channel_layout = match settings.channels {
1 => ffmpeg::channel_layout::ChannelLayout::MONO,
2 => ffmpeg::channel_layout::ChannelLayout::STEREO,
_ => return Err(format!("Unsupported channel count: {}", settings.channels)),
};
encoder.set_rate(settings.sample_rate as i32);
encoder.set_channel_layout(channel_layout);
// AAC encoder supports FLTP (F32 Planar) format
encoder.set_format(ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar));
encoder.set_bit_rate((settings.bitrate_kbps * 1000) as usize);
encoder.set_time_base(ffmpeg::Rational(1, settings.sample_rate as i32));
// Open encoder
let mut encoder = encoder.open_as(encoder_codec)
.map_err(|e| format!("Failed to open AAC encoder: {}", e))?;
// Add stream and set parameters
{
let mut stream = output.add_stream(encoder_codec)
.map_err(|e| format!("Failed to add stream: {}", e))?;
stream.set_parameters(&encoder);
} // Drop stream here to release the borrow
// Write header
output.write_header()
.map_err(|e| format!("Failed to write header: {}", e))?;
// Step 3: Encode frames and write to output
// Convert interleaved f32 samples to planar f32 format (no conversion needed, just rearrange)
let num_frames = pcm_samples.len() / settings.channels as usize;
let planar_samples = convert_to_planar_f32(&pcm_samples, settings.channels);
// Get encoder frame size
let frame_size = encoder.frame_size();
let samples_per_frame = if frame_size > 0 {
frame_size as usize
} else {
1024 // Default AAC frame size
};
// Encode in chunks
let mut samples_encoded = 0;
while samples_encoded < num_frames {
if cancel_flag.load(Ordering::Relaxed) {
return Err("Export cancelled".to_string());
}
let samples_remaining = num_frames - samples_encoded;
let chunk_size = samples_remaining.min(samples_per_frame);
// Create audio frame
let mut frame = ffmpeg::frame::Audio::new(
ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar),
chunk_size,
channel_layout,
);
frame.set_rate(settings.sample_rate);
// Copy planar samples to frame
unsafe {
for ch in 0..settings.channels as usize {
let plane = frame.data_mut(ch);
let offset = samples_encoded;
let src = &planar_samples[ch][offset..offset + chunk_size];
std::ptr::copy_nonoverlapping(
src.as_ptr() as *const u8,
plane.as_mut_ptr(),
chunk_size * std::mem::size_of::<f32>(),
);
}
}
// Send frame to encoder
encoder.send_frame(&frame)
.map_err(|e| format!("Failed to send frame: {}", e))?;
// Receive and write packets
receive_and_write_packets(&mut encoder, &mut output)?;
samples_encoded += chunk_size;
}
// Flush encoder
encoder.send_eof()
.map_err(|e| format!("Failed to send EOF: {}", e))?;
receive_and_write_packets(&mut encoder, &mut output)?;
// Write trailer
output.write_trailer()
.map_err(|e| format!("Failed to write trailer: {}", e))?;
Ok(())
}
#[cfg(test)]

View File

@ -142,79 +142,33 @@ impl ExportOrchestrator {
return;
}
println!("🧵 [EXPORT THREAD] Starting export for format: {:?}", settings.format);
// Convert settings to DAW backend format
let daw_settings = daw_backend::audio::ExportSettings {
format: match settings.format {
lightningbeam_core::export::AudioFormat::Wav => daw_backend::audio::ExportFormat::Wav,
lightningbeam_core::export::AudioFormat::Flac => daw_backend::audio::ExportFormat::Flac,
lightningbeam_core::export::AudioFormat::Mp3 |
lightningbeam_core::export::AudioFormat::Aac => {
// MP3/AAC not supported yet
progress_tx
.send(ExportProgress::Error {
message: format!("{} export not yet implemented. Please use WAV or FLAC format.", settings.format.name()),
})
.ok();
return;
}
lightningbeam_core::export::AudioFormat::Mp3 => daw_backend::audio::ExportFormat::Mp3,
lightningbeam_core::export::AudioFormat::Aac => daw_backend::audio::ExportFormat::Aac,
},
sample_rate: settings.sample_rate,
channels: settings.channels,
bit_depth: settings.bit_depth,
mp3_bitrate: 320, // Not used for WAV/FLAC
mp3_bitrate: settings.bitrate_kbps,
start_time: settings.start_time,
end_time: settings.end_time,
};
println!("🧵 [EXPORT THREAD] Starting non-blocking export...");
// Use DAW backend export for all formats
let result = Self::run_daw_backend_export(
&daw_settings,
&output_path,
&audio_controller,
&cancel_flag,
);
// Start the export (non-blocking - just sends the query)
{
let mut controller = audio_controller.lock().unwrap();
println!("🧵 [EXPORT THREAD] Sending export query...");
if let Err(e) = controller.start_export_audio(&daw_settings, &output_path) {
println!("🧵 [EXPORT THREAD] Failed to start export: {}", e);
progress_tx.send(ExportProgress::Error { message: e }).ok();
return;
}
println!("🧵 [EXPORT THREAD] Export query sent, lock released");
}
// Poll for completion without holding the lock for extended periods
let duration = settings.end_time - settings.start_time;
let start_time = std::time::Instant::now();
let result = loop {
if cancel_flag.load(Ordering::Relaxed) {
break Err("Export cancelled by user".to_string());
}
// Sleep before polling to avoid spinning
std::thread::sleep(std::time::Duration::from_millis(100));
// Brief lock to poll for completion
let poll_result = {
let mut controller = audio_controller.lock().unwrap();
controller.poll_export_completion()
};
match poll_result {
Ok(Some(result)) => {
// Export completed
println!("🧵 [EXPORT THREAD] Export completed: {:?}", result.is_ok());
break result;
}
Ok(None) => {
// Still in progress - actual progress comes via AudioEvent::ExportProgress
// No need to send progress here
}
Err(e) => {
// Polling error (shouldn't happen)
println!("🧵 [EXPORT THREAD] Poll error: {}", e);
break Err(e);
}
}
};
println!("🧵 [EXPORT THREAD] Export loop finished");
println!("🧵 [EXPORT THREAD] Export finished");
// Send completion or error
match result {
@ -232,6 +186,54 @@ impl ExportOrchestrator {
}
}
}
/// Run export using DAW backend (for all formats)
fn run_daw_backend_export(
settings: &daw_backend::audio::ExportSettings,
output_path: &PathBuf,
audio_controller: &Arc<std::sync::Mutex<daw_backend::EngineController>>,
cancel_flag: &Arc<AtomicBool>,
) -> Result<(), String> {
println!("🧵 [EXPORT THREAD] Starting DAW backend export...");
// Start the export (non-blocking - just sends the query)
{
let mut controller = audio_controller.lock().unwrap();
println!("🧵 [EXPORT THREAD] Sending export query...");
controller.start_export_audio(settings, output_path)?;
println!("🧵 [EXPORT THREAD] Export query sent, lock released");
}
// Poll for completion without holding the lock for extended periods
loop {
if cancel_flag.load(Ordering::Relaxed) {
return Err("Export cancelled by user".to_string());
}
// Sleep before polling to avoid spinning
std::thread::sleep(std::time::Duration::from_millis(100));
// Brief lock to poll for completion
let poll_result = {
let mut controller = audio_controller.lock().unwrap();
controller.poll_export_completion()
};
match poll_result {
Ok(Some(result)) => {
println!("🧵 [EXPORT THREAD] DAW backend export completed: {:?}", result.is_ok());
return result;
}
Ok(None) => {
// Still in progress
}
Err(e) => {
println!("🧵 [EXPORT THREAD] Poll error: {}", e);
return Err(e);
}
}
}
}
}
impl Default for ExportOrchestrator {