use js_sys::Uint8Array;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use crate::engine::audio::interpreter::driver::AudioInterpreter;
use crate::language::syntax::parser::driver::SimpleParser;
use crate::web::registry::banks;
use crate::web::utils::errors::to_js_error;
#[derive(Serialize, Deserialize)]
pub struct ExportOptions {
#[serde(default = "default_sample_rate")]
pub sample_rate: u32,
#[serde(default = "default_bpm")]
pub bpm: f32,
#[serde(default = "default_bit_depth")]
pub bit_depth: u8,
#[serde(default = "default_format")]
pub format: String,
#[serde(default = "default_mp3_bitrate")]
pub mp3_bitrate: u32,
#[serde(default = "default_quality")]
pub quality: f32, }
fn default_sample_rate() -> u32 {
44100
}
fn default_bpm() -> f32 {
120.0
}
fn default_bit_depth() -> u8 {
16
}
fn default_format() -> String {
"wav".to_string()
}
fn default_mp3_bitrate() -> u32 {
192
}
fn default_quality() -> f32 {
5.0
}
impl Default for ExportOptions {
fn default() -> Self {
Self {
sample_rate: 44100,
bpm: 120.0,
bit_depth: 16,
format: "wav".to_string(),
mp3_bitrate: 192,
quality: 5.0,
}
}
}
#[derive(Serialize)]
pub struct RenderMetadata {
pub duration: f32,
pub sample_count: usize,
pub event_count: usize,
pub sample_rate: u32,
pub bpm: f32,
pub estimated_size_bytes: usize,
}
#[wasm_bindgen]
pub fn get_render_metadata(user_code: &str, options: JsValue) -> Result<JsValue, JsValue> {
use crate::web::registry::playhead;
playhead::clear_events();
let opts: ExportOptions = if options.is_undefined() || options.is_null() {
ExportOptions::default()
} else {
serde_wasm_bindgen::from_value(options)
.map_err(|e| to_js_error(&format!("Invalid options: {}", e)))?
};
let statements = SimpleParser::parse(user_code, std::path::PathBuf::from("wasm_input.deva"))
.map_err(|e| to_js_error(&format!("Parse error: {:?}", e)))?;
let mut interpreter = AudioInterpreter::new(opts.sample_rate);
interpreter.bpm = opts.bpm;
banks::inject_registered_banks(&mut interpreter);
interpreter
.collect_events(&statements)
.map_err(|e| to_js_error(&format!("Event collection error: {}", e)))?;
let duration = interpreter.events().total_duration();
let event_count = interpreter.events().events.len();
let sample_count = (duration * opts.sample_rate as f32) as usize;
let estimated_size_bytes = match opts.format.as_str() {
"wav" => {
let bytes_per_sample = (opts.bit_depth / 8) as usize;
44 + (sample_count * 2 * bytes_per_sample) }
"mp3" | "ogg" | "opus" => {
((opts.mp3_bitrate as f32 * duration) / 8.0) as usize
}
"flac" => {
let bytes_per_sample = (opts.bit_depth / 8) as usize;
let wav_size = 44 + (sample_count * 2 * bytes_per_sample);
(wav_size as f32 * 0.55) as usize
}
_ => sample_count * 4, };
let metadata = RenderMetadata {
duration,
sample_count,
event_count,
sample_rate: opts.sample_rate,
bpm: opts.bpm,
estimated_size_bytes,
};
serde_wasm_bindgen::to_value(&metadata)
.map_err(|e| to_js_error(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen]
pub fn export_audio(
user_code: &str,
options: JsValue,
on_progress: Option<js_sys::Function>,
) -> Result<Uint8Array, JsValue> {
use crate::web::registry::playhead;
playhead::clear_events();
if let Some(callback) = &on_progress {
let _ = callback.call1(&JsValue::NULL, &JsValue::from_f64(0.0));
}
let opts: ExportOptions = if options.is_undefined() || options.is_null() {
ExportOptions::default()
} else {
serde_wasm_bindgen::from_value(options)
.map_err(|e| to_js_error(&format!("Invalid options: {}", e)))?
};
let statements = SimpleParser::parse(user_code, std::path::PathBuf::from("wasm_input.deva"))
.map_err(|e| to_js_error(&format!("Parse error: {:?}", e)))?;
if let Some(callback) = &on_progress {
let _ = callback.call1(&JsValue::NULL, &JsValue::from_f64(0.2));
}
let mut interpreter = AudioInterpreter::new(opts.sample_rate);
interpreter.bpm = opts.bpm;
banks::inject_registered_banks(&mut interpreter);
if let Some(callback) = &on_progress {
let _ = callback.call1(&JsValue::NULL, &JsValue::from_f64(0.4));
}
let buffer = interpreter
.interpret(&statements)
.map_err(|e| to_js_error(&format!("Render error: {}", e)))?;
if let Some(callback) = &on_progress {
let _ = callback.call1(&JsValue::NULL, &JsValue::from_f64(0.7));
}
let bytes = {
use crate::engine::audio::encoders::{AudioFormat, EncoderOptions, encode_audio};
let format = AudioFormat::from_str(&opts.format).ok_or_else(|| {
to_js_error(&format!(
"Unsupported format: '{}'. Supported formats: wav, mp3, ogg, flac, opus",
opts.format
))
})?;
let encoder_opts = match format {
AudioFormat::Wav => EncoderOptions::wav(opts.sample_rate, opts.bit_depth),
AudioFormat::Mp3 => EncoderOptions::mp3(opts.sample_rate, opts.mp3_bitrate),
AudioFormat::Ogg => EncoderOptions::ogg(opts.sample_rate, opts.quality),
AudioFormat::Flac => EncoderOptions::flac(opts.sample_rate, opts.bit_depth),
AudioFormat::Opus => {
let mut opt = EncoderOptions::default();
opt.format = AudioFormat::Opus;
opt.sample_rate = opts.sample_rate;
opt.bitrate_kbps = opts.mp3_bitrate;
opt.quality = opts.quality;
opt
}
};
encode_audio(&buffer, &encoder_opts)
.map_err(|e| to_js_error(&format!("Encoding error: {}", e)))?
};
let array = Uint8Array::new_with_length(bytes.len() as u32);
array.copy_from(&bytes);
if let Some(callback) = &on_progress {
let _ = callback.call1(&JsValue::NULL, &JsValue::from_f64(1.0));
}
Ok(array)
}