#![cfg_attr(not(test), no_std)]
#![allow(clippy::needless_range_loop)]
extern crate alloc;
#[cfg(feature = "wasm")]
use alloc::format;
#[cfg(feature = "wasm")]
use alloc::string::String;
#[cfg(feature = "wasm")]
use alloc::string::ToString;
#[cfg(feature = "wasm")]
use alloc::vec;
use alloc::vec::Vec;
#[cfg(feature = "wasm")]
use messagepack_serde::{from_slice, to_vec};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
pub mod core;
pub mod dsp;
pub mod lossless;
pub mod lossy;
pub mod streaming;
mod reader;
pub mod seeking;
mod writer;
pub use core::{
compute_crc32, compute_ebu_r128_loudness, extract_spectral_fingerprint, extract_waveform_peaks,
metadata::*, rice, ChannelData, FloError, FloErrorExt, FloErrorKind, FloFile, FloResult,
FrameType, LoudnessMetrics, ResidualEncoding, HEADER_SIZE, MAGIC, VERSION_MAJOR, VERSION_MINOR,
};
pub use lossless::{lpc, Decoder, Encoder};
pub use lossy::{
deserialize_frame, serialize_frame, BlockSize, Mdct, PsychoacousticModel, QualityPreset,
TransformDecoder as LossyDecoder, TransformEncoder as LossyEncoder, TransformFrame, WindowType,
FRAME_COEFFICIENTS, LONG_BLOCK_SIZE, SHORTS_PER_GROUP, SHORT_BLOCK_SIZE, SHORT_GROUP_OFFSET,
};
pub use reader::Reader;
pub use streaming::{
DecoderState, EncodedFrame, StreamingAudioInfo, StreamingDecoder, StreamingEncoder,
};
pub use writer::Writer;
#[cfg(feature = "wasm")]
#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct AudioInfo {
#[wasm_bindgen(skip)]
pub version: String,
pub sample_rate: u32,
pub channels: u8,
pub bit_depth: u8,
pub total_samples: u64,
pub duration_secs: f64,
pub file_size: usize,
pub compression_ratio: f64,
pub crc_valid: bool,
pub is_lossy: bool,
pub lossy_quality: u8,
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
impl AudioInfo {
#[wasm_bindgen(getter)]
pub fn version(&self) -> String {
self.version.clone()
}
}
#[cfg(feature = "wasm")]
fn to_js_err(e: impl ::core::fmt::Display) -> JsValue {
JsValue::from_str(&e.to_string())
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn encode(
samples: &[f32],
sample_rate: u32,
channels: u8,
bit_depth: u8,
metadata: Option<Vec<u8>>,
) -> Result<Vec<u8>, JsValue> {
validate_audio_format(samples, sample_rate, channels, bit_depth).map_err(to_js_err)?;
let metadata_with_waveform = add_analysis_data_if_missing(
&metadata.unwrap_or_default(),
samples,
sample_rate,
channels,
50, )
.map_err(to_js_err)?;
let encoder = Encoder::new(sample_rate, channels, bit_depth);
encoder
.encode(samples, &metadata_with_waveform)
.map_err(to_js_err)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn encode_lossy(
samples: &[f32],
sample_rate: u32,
channels: u8,
_bit_depth: u8,
quality: u8,
metadata: Option<Vec<u8>>,
) -> Result<Vec<u8>, JsValue> {
validate_audio_format(samples, sample_rate, channels, _bit_depth).map_err(to_js_err)?;
let quality_f32 = match quality {
0 => 0.0,
1 => 0.35,
2 => 0.55,
3 => 0.75,
_ => 1.0,
};
let metadata_with_waveform = add_analysis_data_if_missing(
&metadata.unwrap_or_default(),
samples,
sample_rate,
channels,
50, )
.map_err(to_js_err)?;
let mut encoder = lossy::TransformEncoder::new(sample_rate, channels, quality_f32);
encoder
.encode_to_flo(samples, &metadata_with_waveform)
.map_err(to_js_err)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn encode_with_bitrate(
samples: &[f32],
sample_rate: u32,
channels: u8,
_bit_depth: u8,
target_bitrate_kbps: u32,
metadata: Option<Vec<u8>>,
) -> Result<Vec<u8>, JsValue> {
validate_audio_format(samples, sample_rate, channels, _bit_depth).map_err(to_js_err)?;
let quality =
lossy::QualityPreset::from_bitrate(target_bitrate_kbps, sample_rate, channels).as_f32();
let metadata_with_waveform = add_analysis_data_if_missing(
&metadata.unwrap_or_default(),
samples,
sample_rate,
channels,
50, )
.map_err(to_js_err)?;
let mut encoder = lossy::TransformEncoder::new(sample_rate, channels, quality);
encoder
.encode_to_flo(samples, &metadata_with_waveform)
.map_err(to_js_err)
}
#[cfg(feature = "wasm")]
fn validate_audio_format(
samples: &[f32],
sample_rate: u32,
channels: u8,
bit_depth: u8,
) -> Result<(), String> {
if sample_rate == 0 {
return Err("Sample rate must be greater than zero".to_string());
}
if channels == 0 {
return Err("Channel count must be greater than zero".to_string());
}
if !matches!(bit_depth, 8 | 16 | 24 | 32) {
return Err("Bit depth must be 8, 16, 24, or 32".to_string());
}
if !samples.len().is_multiple_of(channels as usize) {
return Err("Interleaved samples must contain a complete final frame".to_string());
}
Ok(())
}
#[cfg(feature = "wasm")]
fn add_analysis_data_if_missing(
metadata: &[u8],
samples: &[f32],
sample_rate: u32,
channels: u8,
peaks_per_second: u32,
) -> Result<Vec<u8>, String> {
let mut flo_metadata: FloMetadata = if !metadata.is_empty() {
from_slice(metadata).map_err(|e| format!("Invalid metadata: {}", e))?
} else {
FloMetadata::default()
};
if flo_metadata.waveform_data.is_none() {
let waveform = core::analysis::extract_waveform_peaks(
samples,
channels,
sample_rate,
peaks_per_second,
);
flo_metadata.waveform_data = Some(waveform);
}
if flo_metadata.spectrum_fingerprint.is_none() {
let spectral_fingerprint = core::analysis::extract_spectral_fingerprint(
samples,
channels,
sample_rate,
Some(2048), Some(1024), );
if let Ok(spectral_bytes) = messagepack_serde::to_vec(&spectral_fingerprint) {
flo_metadata.spectrum_fingerprint = Some(spectral_bytes);
}
}
if flo_metadata.loudness_profile.is_empty() {
let loudness_metrics =
core::ebu_r128::compute_ebu_r128_loudness(samples, channels, sample_rate);
let loudness_point = core::metadata::LoudnessPoint {
timestamp_ms: 0,
lufs: loudness_metrics.integrated_lufs as f32,
};
flo_metadata.loudness_profile = vec![loudness_point];
}
let interleaved_len = samples.len() as u64;
let samples_per_channel = interleaved_len / (channels as u64);
let length_ms = (((samples_per_channel as f64) / (sample_rate as f64)) * 1000.0) as u64;
flo_metadata.length_ms = Some(length_ms);
to_vec(&flo_metadata).map_err(|e| format!("Failed to serialize metadata: {}", e))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn decode(data: &[u8]) -> Result<Vec<f32>, JsValue> {
let reader = Reader::new();
let file = reader.read(data).map_err(to_js_err)?;
let is_transform = file
.frames
.iter()
.any(|f| f.frame_type == (FrameType::Transform as u8));
if is_transform {
decode_transform_file(&file).map_err(to_js_err)
} else {
let decoder = Decoder::new();
decoder.decode_file(&file).map_err(to_js_err)
}
}
#[cfg(feature = "wasm")]
fn decode_transform_file(file: &FloFile) -> FloResult<Vec<f32>> {
let mut decoder = lossy::TransformDecoder::new(file.header.sample_rate, file.header.channels);
let mut all_samples = Vec::new();
let mut frame_count = 0;
for frame in &file.frames {
if frame.channels.is_empty() {
continue;
}
let frame_data = &frame.channels[0].residuals;
if let Some(transform_frame) = lossy::deserialize_frame(frame_data) {
let samples = decoder.decode_frame(&transform_frame);
if frame_count > 0 {
all_samples.extend(samples);
}
frame_count += 1;
} else {
return Err(crate::core::FloError::from(
"Failed to deserialize transform frame",
));
}
}
Ok(all_samples)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn validate(data: &[u8]) -> Result<bool, JsValue> {
let reader = Reader::new();
match reader.read(data) {
Ok(file) => {
let start = (4 + file.header.header_size + file.header.toc_size) as usize;
let end = start + (file.header.data_size as usize);
if end <= data.len() {
let computed = core::crc32::compute(&data[start..end]);
Ok(computed == file.header.data_crc32)
} else {
Ok(false)
}
}
Err(_) => Ok(false),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn info(data: &[u8]) -> Result<AudioInfo, JsValue> {
let reader = Reader::new();
let file = reader.read(data).map_err(to_js_err)?;
let metadata = FloMetadata::from_msgpack(&file.metadata).unwrap_or_default();
let duration_secs = metadata
.length_ms
.map(|ms| (ms as f64) / 1000.0)
.unwrap_or_else(|| {
(file.header.total_samples as f64) / (file.header.sample_rate as f64)
});
let original_size = ((file.header.total_samples as f64)
* (file.header.channels as f64)
* ((file.header.bit_depth as f64) / 8.0)) as usize;
let compression_ratio = if !data.is_empty() {
(original_size as f64) / (data.len() as f64)
} else {
0.0
};
let start = (4 + file.header.header_size + file.header.toc_size) as usize;
let end = start + (file.header.data_size as usize);
let crc_valid = if end <= data.len() {
core::crc32::compute(&data[start..end]) == file.header.data_crc32
} else {
false
};
let is_lossy = (file.header.flags & 0x01) != 0;
let lossy_quality = ((file.header.flags >> 8) & 0x0f) as u8;
Ok(AudioInfo {
version: format!(
"{}.{}",
file.header.version_major, file.header.version_minor
),
sample_rate: file.header.sample_rate,
channels: file.header.channels,
bit_depth: file.header.bit_depth,
total_samples: file.header.total_samples,
duration_secs,
file_size: data.len(),
compression_ratio,
crc_valid,
is_lossy,
lossy_quality,
})
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn version() -> String {
format!("{}.{}", VERSION_MAJOR, VERSION_MINOR)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn format_time(seconds: f64) -> String {
if !seconds.is_finite() || seconds < 0.0 {
return "0:00".to_string();
}
let total_secs = seconds.floor() as u64;
let hours = total_secs / 3600;
let mins = (total_secs % 3600) / 60;
let secs = total_secs % 60;
if hours > 0 {
format!("{}:{:02}:{:02}", hours, mins, secs)
} else {
format!("{}:{:02}", mins, secs)
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn format_time_ms(milliseconds: f64) -> String {
format_time(milliseconds / 1000.0)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn get_toc(flo_data: &[u8]) -> Result<Vec<JsValue>, JsValue> {
let toc = seeking::get_toc(flo_data).map_err(to_js_err)?;
let result = toc
.into_iter()
.map(|entry| {
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &"frame_index".into(), &entry.frame_index.into()).ok();
js_sys::Reflect::set(
&obj,
&"byte_offset".into(),
&(entry.byte_offset as f64).into(),
)
.ok();
js_sys::Reflect::set(&obj, &"frame_size".into(), &entry.frame_size.into()).ok();
js_sys::Reflect::set(&obj, &"timestamp_ms".into(), &entry.timestamp_ms.into()).ok();
obj.into()
})
.collect();
Ok(result)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn decode_frame_at(flo_data: &[u8], frame_index: u32) -> Result<Vec<f32>, JsValue> {
seeking::decode_frame_at(flo_data, frame_index).map_err(to_js_err)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn seek_to_time(flo_data: &[u8], time_ms: u32) -> Result<JsValue, JsValue> {
let result = seeking::seek_to_time(flo_data, time_ms).map_err(to_js_err)?;
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &"frame_index".into(), &result.frame_index.into())?;
js_sys::Reflect::set(
&obj,
&"byte_offset".into(),
&(result.byte_offset as f64).into(),
)?;
js_sys::Reflect::set(&obj, &"timestamp_ms".into(), &result.timestamp_ms.into())?;
js_sys::Reflect::set(&obj, &"sample_offset".into(), &result.sample_offset.into())?;
js_sys::Reflect::set(
&obj,
&"next_timestamp_ms".into(),
&result.next_timestamp_ms.into(),
)?;
Ok(obj.into())
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub struct WasmStreamingDecoder {
inner: StreamingDecoder,
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
impl WasmStreamingDecoder {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
inner: StreamingDecoder::new(),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn feed(&mut self, data: &[u8]) -> Result<bool, JsValue> {
self.inner.feed(data).map_err(to_js_err)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn is_ready(&self) -> bool {
self.inner.state() == DecoderState::Ready
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn is_finished(&self) -> bool {
self.inner.state() == DecoderState::Finished
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn has_error(&self) -> bool {
self.inner.state() == DecoderState::Error
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn state(&self) -> String {
match self.inner.state() {
DecoderState::WaitingForHeader => "waiting_for_header".into(),
DecoderState::WaitingForToc => "waiting_for_toc".into(),
DecoderState::Ready => "ready".into(),
DecoderState::Finished => "finished".into(),
DecoderState::Error => "error".into(),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn get_info(&self) -> Result<JsValue, JsValue> {
match self.inner.info() {
Some(info) => {
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &"sample_rate".into(), &info.sample_rate.into())?;
js_sys::Reflect::set(&obj, &"channels".into(), &info.channels.into())?;
js_sys::Reflect::set(&obj, &"bit_depth".into(), &info.bit_depth.into())?;
js_sys::Reflect::set(
&obj,
&"total_samples".into(),
&(info.total_samples as f64).into(),
)?;
js_sys::Reflect::set(&obj, &"is_lossy".into(), &info.is_lossy.into())?;
Ok(obj.into())
}
None => Ok(JsValue::NULL),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn decode_available(&mut self) -> Result<Vec<f32>, JsValue> {
self.inner.decode_available().map_err(to_js_err)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn next_frame(&mut self) -> Result<JsValue, JsValue> {
match self.inner.next_frame() {
Ok(Some(samples)) => {
let array = js_sys::Float32Array::new_with_length(samples.len() as u32);
array.copy_from(&samples);
Ok(array.into())
}
Ok(None) => Ok(JsValue::NULL),
Err(e) => Err(to_js_err(e)),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn available_frames(&self) -> usize {
self.inner.available_frames()
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn current_frame_index(&self) -> usize {
self.inner.current_frame_index()
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn reset(&mut self) {
self.inner.reset();
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn buffered_bytes(&self) -> usize {
self.inner.buffered_bytes()
}
}
#[cfg(feature = "wasm")]
impl Default for WasmStreamingDecoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub struct WasmStreamingEncoder {
inner: StreamingEncoder,
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
impl WasmStreamingEncoder {
#[wasm_bindgen(constructor)]
pub fn new(sample_rate: u32, channels: u8, bit_depth: u8) -> Self {
Self {
inner: StreamingEncoder::new(sample_rate, channels, bit_depth),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn with_compression(mut self, level: u8) -> Self {
self.inner = self.inner.with_compression(level);
self
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn push_samples(&mut self, samples: &[f32]) -> Result<(), JsValue> {
self.inner.push_samples(samples).map_err(to_js_err)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn next_frame(&mut self) -> JsValue {
match self.inner.next_frame() {
Some(frame) => {
let obj = js_sys::Object::new();
let _ = js_sys::Reflect::set(&obj, &"index".into(), &frame.index.into());
let _ =
js_sys::Reflect::set(&obj, &"timestamp_ms".into(), &frame.timestamp_ms.into());
let _ = js_sys::Reflect::set(
&obj,
&"data".into(),
&js_sys::Uint8Array::from(&frame.data[..]).into(),
);
let _ = js_sys::Reflect::set(&obj, &"samples".into(), &frame.samples.into());
obj.into()
}
None => JsValue::NULL,
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn pending_samples(&self) -> usize {
self.inner.pending_samples()
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn pending_frames(&self) -> usize {
self.inner.pending_frames()
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn flush(&mut self) -> Result<(), JsValue> {
self.inner.flush_into_frames().map_err(to_js_err)?;
Ok(())
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn finalize(&mut self, metadata: Option<Vec<u8>>) -> Result<Vec<u8>, JsValue> {
let meta = metadata.unwrap_or_default();
self.inner.finalize(&meta).map_err(to_js_err)
}
}
#[cfg(feature = "wasm")]
impl Default for WasmStreamingEncoder {
fn default() -> Self {
Self {
inner: StreamingEncoder::new(44100, 2, 16),
}
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn create_metadata(
title: Option<String>,
artist: Option<String>,
album: Option<String>,
) -> Result<Vec<u8>, JsValue> {
let meta = FloMetadata::with_basic(title, artist, album);
meta.to_msgpack()
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn create_metadata_from_object(obj: JsValue) -> Result<Vec<u8>, JsValue> {
let meta: FloMetadata = serde_wasm_bindgen::from_value(obj)
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
meta.to_msgpack()
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn get_metadata(data: &[u8]) -> Result<JsValue, JsValue> {
let reader = Reader::new();
let file = reader.read(data).map_err(to_js_err)?;
if file.metadata.is_empty() {
return Ok(JsValue::NULL);
}
let meta = FloMetadata::from_msgpack(&file.metadata)
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
serde_wasm_bindgen::to_value(&meta)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn get_cover_art(data: &[u8]) -> Result<JsValue, JsValue> {
let reader = Reader::new();
let file = reader.read(data).map_err(to_js_err)?;
if file.metadata.is_empty() {
return Ok(JsValue::NULL);
}
let meta = FloMetadata::from_msgpack(&file.metadata)
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
if let Some(pic) = meta.front_cover().or_else(|| meta.any_picture()) {
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &"mime_type".into(), &pic.mime_type.clone().into())?;
js_sys::Reflect::set(
&obj,
&"data".into(),
&js_sys::Uint8Array::from(&pic.data[..]).into(),
)?;
if let Some(ref desc) = pic.description {
js_sys::Reflect::set(&obj, &"description".into(), &desc.clone().into())?;
}
Ok(obj.into())
} else {
Ok(JsValue::NULL)
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn set_metadata_field(
metadata: Option<Vec<u8>>,
field: &str,
value: JsValue,
) -> Result<Vec<u8>, JsValue> {
let meta = match &metadata {
Some(data) if !data.is_empty() => FloMetadata::from_msgpack(data)
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?,
_ => FloMetadata::new(),
};
let mut obj: serde_json::Value = serde_json::to_value(&meta)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
let json_value = if value.is_null() || value.is_undefined() {
serde_json::Value::Null
} else if let Some(s) = value.as_string() {
serde_json::Value::String(s)
} else if let Some(n) = value.as_f64() {
serde_json::json!(n)
} else if let Some(b) = value.as_bool() {
serde_json::Value::Bool(b)
} else {
let js_json = js_sys::JSON::stringify(&value)
.map_err(|_| JsValue::from_str("Cannot serialize value"))?;
serde_json::from_str(&js_json.as_string().unwrap_or_default())
.map_err(|e| JsValue::from_str(&format!("Invalid JSON: {}", e)))?
};
if let serde_json::Value::Object(ref mut map) = obj {
map.insert(field.to_string(), json_value);
} else {
return Err(JsValue::from_str("Internal error: metadata not an object"));
}
let updated: FloMetadata = serde_json::from_value(obj)
.map_err(|e| JsValue::from_str(&format!("Invalid field '{}': {}", field, e)))?;
updated
.to_msgpack()
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn get_synced_lyrics(data: &[u8]) -> Result<JsValue, JsValue> {
let reader = Reader::new();
let file = reader.read(data).map_err(to_js_err)?;
if file.metadata.is_empty() {
return Ok(JsValue::NULL);
}
let meta = FloMetadata::from_msgpack(&file.metadata)
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
if meta.synced_lyrics.is_empty() {
return Ok(JsValue::NULL);
}
serde_wasm_bindgen::to_value(&meta.synced_lyrics)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn get_waveform_data(data: &[u8]) -> Result<JsValue, JsValue> {
let reader = Reader::new();
let file = reader.read(data).map_err(to_js_err)?;
if file.metadata.is_empty() {
return Ok(JsValue::NULL);
}
let meta = FloMetadata::from_msgpack(&file.metadata)
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
match meta.waveform_data {
Some(ref waveform) => serde_wasm_bindgen::to_value(waveform)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))),
None => Ok(JsValue::NULL),
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn get_section_markers(data: &[u8]) -> Result<JsValue, JsValue> {
let reader = Reader::new();
let file = reader.read(data).map_err(to_js_err)?;
if file.metadata.is_empty() {
return Ok(JsValue::NULL);
}
let meta = FloMetadata::from_msgpack(&file.metadata)
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
if meta.section_markers.is_empty() {
return Ok(JsValue::NULL);
}
serde_wasm_bindgen::to_value(&meta.section_markers)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn update_metadata(flo_data: &[u8], new_metadata: &[u8]) -> Result<Vec<u8>, JsValue> {
update_metadata_bytes(flo_data, new_metadata).map_err(to_js_err)
}
pub fn update_metadata_bytes(flo_data: &[u8], new_metadata: &[u8]) -> FloResult<Vec<u8>> {
if flo_data.len() < (HEADER_SIZE as usize) {
return Err(crate::core::FloError::from(
"File too small to be valid flo",
));
}
if flo_data[0..4] != MAGIC {
return Err(crate::core::FloError::from("Invalid flo file: bad magic"));
}
let reader = Reader::new();
let file = reader.read(flo_data)?;
let meta_offset = 4
+ (file.header.header_size as usize)
+ (file.header.toc_size as usize)
+ (file.header.data_size as usize)
+ (file.header.extra_size as usize);
let mut result = Vec::with_capacity(meta_offset + new_metadata.len());
result.extend_from_slice(&flo_data[..meta_offset]);
result.extend_from_slice(new_metadata);
let meta_size_offset = 4 + 2 + 2 + 4 + 1 + 1 + 8 + 1 + 3 + 4 + 8 + 8 + 8 + 8;
let new_meta_size = new_metadata.len() as u64;
result[meta_size_offset..meta_size_offset + 8].copy_from_slice(&new_meta_size.to_le_bytes());
Ok(result)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn set_metadata(flo_data: &[u8], metadata: JsValue) -> Result<Vec<u8>, JsValue> {
let new_meta_bytes = create_metadata_from_object(metadata)?;
update_metadata(flo_data, &new_meta_bytes)
}
pub fn strip_metadata_bytes(flo_data: &[u8]) -> FloResult<Vec<u8>> {
update_metadata_bytes(flo_data, &[])
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn strip_metadata(flo_data: &[u8]) -> Result<Vec<u8>, JsValue> {
strip_metadata_bytes(flo_data).map_err(to_js_err)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn get_metadata_bytes(flo_data: &[u8]) -> Result<Vec<u8>, JsValue> {
get_metadata_bytes_native(flo_data).map_err(to_js_err)
}
pub fn get_metadata_bytes_native(flo_data: &[u8]) -> FloResult<Vec<u8>> {
if flo_data.len() < (HEADER_SIZE as usize) {
return Err(crate::core::FloError::from("File too small"));
}
let reader = Reader::new();
let file = reader.read(flo_data)?;
Ok(file.metadata)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(js_name = has_metadata)]
pub fn has_metadata_wasm(flo_data: &[u8]) -> bool {
has_metadata(flo_data)
}
pub fn has_metadata(flo_data: &[u8]) -> bool {
if flo_data.len() < (HEADER_SIZE as usize) {
return false;
}
let meta_size_offset = 4 + 2 + 2 + 4 + 1 + 1 + 8 + 1 + 3 + 4 + 8 + 8 + 8 + 8;
if flo_data.len() < meta_size_offset + 8 {
return false;
}
let meta_size = u64::from_le_bytes(
flo_data[meta_size_offset..meta_size_offset + 8]
.try_into()
.unwrap_or([0; 8]),
);
meta_size > 0
}
pub fn extract_waveform_peaks_to_struct(
samples: &[f32],
sample_rate: u32,
channels: u8,
peaks_per_second: Option<u32>,
) -> core::metadata::WaveformData {
let peaks_per_second = peaks_per_second.unwrap_or(50);
core::analysis::extract_waveform_peaks(samples, channels, sample_rate, peaks_per_second)
}
pub fn extract_waveform_rms_to_struct(
samples: &[f32],
sample_rate: u32,
channels: u8,
peaks_per_second: Option<u32>,
) -> core::metadata::WaveformData {
let peaks_per_second = peaks_per_second.unwrap_or(50);
core::analysis::extract_waveform_rms(samples, channels, sample_rate, peaks_per_second)
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn extract_spectral_fingerprint_wasm(
samples: &[f32],
channels: u32,
sample_rate: u32,
fft_size: Option<usize>,
hop_size: Option<usize>,
) -> Result<JsValue, JsValue> {
use crate::core::extract_spectral_fingerprint;
let fingerprint =
extract_spectral_fingerprint(samples, channels as u8, sample_rate, fft_size, hop_size);
serde_wasm_bindgen::to_value(&fingerprint)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn extract_dominant_frequencies_wasm(
fingerprint_js: JsValue,
num_frequencies: usize,
) -> Result<JsValue, JsValue> {
use crate::core::analysis::{extract_dominant_frequencies, SpectralFingerprint};
let fingerprint: SpectralFingerprint = serde_wasm_bindgen::from_value(fingerprint_js)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let dominant_freqs = extract_dominant_frequencies(&fingerprint, num_frequencies);
serde_wasm_bindgen::to_value(&dominant_freqs)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
pub fn extract_dominant_frequencies_to_vec(
fingerprint: &core::analysis::SpectralFingerprint,
num_frequencies: usize,
) -> Vec<Vec<f64>> {
core::analysis::extract_dominant_frequencies(fingerprint, num_frequencies)
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn extract_dominant_frequencies_from_samples_wasm(
samples: &[f32],
sample_rate: u32,
channels: u8,
num_frequencies: usize,
fft_size: Option<usize>,
hop_size: Option<usize>,
) -> Result<JsValue, JsValue> {
let fingerprint =
extract_spectral_fingerprint(samples, channels, sample_rate, fft_size, hop_size);
let dominant_freqs = extract_dominant_frequencies_to_vec(&fingerprint, num_frequencies);
let js_result = js_sys::Array::new();
for frame_freqs in &dominant_freqs {
let frame_array = js_sys::Float64Array::from(&frame_freqs[..]);
js_result.push(&frame_array);
}
Ok(js_result.into())
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn extract_dominant_frequencies_from_fingerprint_wasm(
fingerprint_js: JsValue,
num_frequencies: usize,
) -> Result<JsValue, JsValue> {
use crate::core::analysis::{extract_dominant_frequencies, SpectralFingerprint};
let fingerprint: SpectralFingerprint = serde_wasm_bindgen::from_value(fingerprint_js)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let dominant_freqs = extract_dominant_frequencies(&fingerprint, num_frequencies);
serde_wasm_bindgen::to_value(&dominant_freqs)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
pub fn spectral_similarity_score(
fingerprint1: &core::analysis::SpectralFingerprint,
fingerprint2: &core::analysis::SpectralFingerprint,
) -> f32 {
core::analysis::spectral_similarity(fingerprint1, fingerprint2)
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn spectral_similarity(
samples1: &[f32],
samples2: &[f32],
sample_rate: u32,
channels: u8,
fft_size: Option<usize>,
hop_size: Option<usize>,
) -> Result<JsValue, JsValue> {
let fingerprint1 =
extract_spectral_fingerprint(samples1, channels, sample_rate, fft_size, hop_size);
let fingerprint2 =
extract_spectral_fingerprint(samples2, channels, sample_rate, fft_size, hop_size);
let similarity = spectral_similarity_score(&fingerprint1, &fingerprint2);
Ok(JsValue::from_f64(similarity as f64))
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn compute_loudness_metrics(
samples: &[f32],
channels: u32,
sample_rate: u32,
) -> Result<JsValue, JsValue> {
use crate::core::compute_ebu_r128_loudness;
let metrics = compute_ebu_r128_loudness(samples, channels as u8, sample_rate);
serde_wasm_bindgen::to_value(&metrics)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[cfg(target_arch = "wasm32")]
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn extract_waveform_peaks_wasm(
samples: &[f32],
channels: u32,
sample_rate: u32,
peaks_per_second: Option<u32>,
) -> Result<JsValue, JsValue> {
use crate::core::extract_waveform_peaks;
let waveform = extract_waveform_peaks(
samples,
channels as u8,
sample_rate,
peaks_per_second.unwrap_or(50),
);
serde_wasm_bindgen::to_value(&waveform)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn test_frame_type_conversion() {
assert_eq!(FrameType::from(0), FrameType::Silence);
assert_eq!(FrameType::from(8), FrameType::Alpc8);
assert_eq!(FrameType::from(254), FrameType::Raw);
assert!(FrameType::Alpc8.is_alpc());
assert!(!FrameType::Silence.is_alpc());
assert_eq!(FrameType::Alpc8.lpc_order(), Some(8));
}
#[test]
#[cfg(feature = "wasm")]
fn test_version() {
assert_eq!(version(), "1.2");
}
#[test]
fn test_update_metadata_preserves_audio() {
let samples: Vec<f32> = (0..4410).map(|i| ((i as f32) * 0.01).sin() * 0.5).collect();
let encoder = Encoder::new(44100, 1, 16);
let original_meta = b"original metadata";
let flo_data = encoder.encode(&samples, original_meta).unwrap();
let new_meta = b"new metadata that is longer!";
let updated = update_metadata_bytes(&flo_data, new_meta).unwrap();
let reader = Reader::new();
let file = reader.read(&updated).unwrap();
assert_eq!(file.metadata, new_meta);
assert_eq!(file.header.meta_size, new_meta.len() as u64);
let original_file = reader.read(&flo_data).unwrap();
assert_eq!(file.header.data_crc32, original_file.header.data_crc32);
let decoder = Decoder::new();
let original_samples = decoder.decode(&flo_data).unwrap();
let updated_samples = decoder.decode(&updated).unwrap();
assert_eq!(original_samples, updated_samples);
}
#[test]
fn test_strip_metadata() {
let samples: Vec<f32> = vec![0.5; 1000];
let encoder = Encoder::new(44100, 1, 16);
let flo_with_meta = encoder.encode(&samples, b"some metadata here").unwrap();
let stripped = update_metadata_bytes(&flo_with_meta, &[]).unwrap();
let reader = Reader::new();
let file = reader.read(&stripped).unwrap();
assert!(file.metadata.is_empty());
assert_eq!(file.header.meta_size, 0);
assert!(stripped.len() < flo_with_meta.len());
}
#[test]
fn test_has_metadata() {
let samples: Vec<f32> = vec![0.5; 1000];
let encoder = Encoder::new(44100, 1, 16);
let with_meta = encoder.encode(&samples, b"metadata").unwrap();
let without_meta = encoder.encode(&samples, &[]).unwrap();
assert!(has_metadata(&with_meta));
assert!(!has_metadata(&without_meta));
}
}