mp3 and aac export
This commit is contained in:
parent
727d782190
commit
2cd7682399
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
let samples = render_to_memory(project, pool, midi_pool, settings, event_tx)?;
|
||||
|
||||
// Write to file based on format
|
||||
// Route to appropriate export implementation based on format
|
||||
match settings.format {
|
||||
ExportFormat::Wav => write_wav(&samples, settings, output_path)?,
|
||||
ExportFormat::Flac => write_flac(&samples, settings, output_path)?,
|
||||
ExportFormat::Wav | ExportFormat::Flac => {
|
||||
// Render to memory then write (existing path)
|
||||
let samples = render_to_memory(project, pool, midi_pool, settings, event_tx)?;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue