Skip to main content

libflo_audio/
lib.rs

1#![cfg_attr(not(test), no_std)]
2#![allow(clippy::needless_range_loop)]
3
4extern crate alloc;
5
6#[cfg(feature = "wasm")]
7use alloc::format;
8#[cfg(feature = "wasm")]
9use alloc::string::String;
10#[cfg(feature = "wasm")]
11use alloc::string::ToString;
12#[cfg(feature = "wasm")]
13use alloc::vec;
14use alloc::vec::Vec;
15#[cfg(feature = "wasm")]
16use messagepack_serde::{from_slice, to_vec};
17#[cfg(feature = "wasm")]
18use wasm_bindgen::prelude::*;
19
20pub mod core;
21pub mod dsp;
22pub mod lossless;
23pub mod lossy;
24pub mod streaming;
25
26mod reader;
27pub mod seeking;
28mod writer;
29
30pub use core::{
31    compute_crc32, compute_ebu_r128_loudness, extract_spectral_fingerprint, extract_waveform_peaks,
32    metadata::*, rice, ChannelData, FloError, FloErrorExt, FloErrorKind, FloFile, FloResult,
33    FrameType, LoudnessMetrics, ResidualEncoding, HEADER_SIZE, MAGIC, VERSION_MAJOR, VERSION_MINOR,
34};
35pub use lossless::{lpc, Decoder, Encoder};
36pub use lossy::{
37    deserialize_frame, serialize_frame, BlockSize, Mdct, PsychoacousticModel, QualityPreset,
38    TransformDecoder as LossyDecoder, TransformEncoder as LossyEncoder, TransformFrame, WindowType,
39    FRAME_COEFFICIENTS, LONG_BLOCK_SIZE, SHORTS_PER_GROUP, SHORT_BLOCK_SIZE, SHORT_GROUP_OFFSET,
40};
41pub use reader::Reader;
42pub use streaming::{
43    DecoderState, EncodedFrame, StreamingAudioInfo, StreamingDecoder, StreamingEncoder,
44};
45pub use writer::Writer;
46
47// audio info for the info() function
48
49/// info about a flo file
50#[cfg(feature = "wasm")]
51#[wasm_bindgen]
52#[derive(Debug, Clone)]
53pub struct AudioInfo {
54    /// version string like "1.2"
55    #[wasm_bindgen(skip)]
56    pub version: String,
57    /// Sample rate in Hz
58    pub sample_rate: u32,
59    /// Number of channels
60    pub channels: u8,
61    /// Bits per sample
62    pub bit_depth: u8,
63    /// Total sample-frames (samples per channel). This is the number of sample
64    /// instants per channel (not interleaved samples). Use metadata.length_ms
65    /// for a quick duration lookup.
66    pub total_samples: u64,
67    /// Duration in seconds
68    pub duration_secs: f64,
69    /// File size in bytes
70    pub file_size: usize,
71    /// Compression ratio (original / compressed)
72    pub compression_ratio: f64,
73    /// Is CRC valid?
74    pub crc_valid: bool,
75    /// Is lossy compression mode?
76    pub is_lossy: bool,
77    /// Lossy quality 0-4 (only valid if is_lossy)
78    pub lossy_quality: u8,
79}
80
81#[cfg(feature = "wasm")]
82#[wasm_bindgen]
83impl AudioInfo {
84    #[wasm_bindgen(getter)]
85    pub fn version(&self) -> String {
86        self.version.clone()
87    }
88}
89
90// result helpers
91
92/// turn an error into js
93#[cfg(feature = "wasm")]
94fn to_js_err(e: impl ::core::fmt::Display) -> JsValue {
95    JsValue::from_str(&e.to_string())
96}
97
98// api functions
99
100/// encode samples to flo lossless
101///
102/// # Arguments
103/// * `samples` - Interleaved audio samples (f32, -1.0 to 1.0)
104/// * `sample_rate` - Sample rate in Hz (e.g., 44100)
105/// * `channels` - Number of channels (1 or 2)
106/// * `bit_depth` - Bits per sample (16, 24, or 32)
107/// * `metadata` - Optional MessagePack metadata
108///
109/// # Returns
110/// flo file as byte array
111///
112/// # Note
113/// For advanced usage with custom compression levels (0-9),
114/// use the `Encoder` builder pattern directly.
115#[cfg(feature = "wasm")]
116#[wasm_bindgen]
117pub fn encode(
118    samples: &[f32],
119    sample_rate: u32,
120    channels: u8,
121    bit_depth: u8,
122    metadata: Option<Vec<u8>>,
123) -> Result<Vec<u8>, JsValue> {
124    validate_audio_format(samples, sample_rate, channels, bit_depth).map_err(to_js_err)?;
125
126    // Auto-add analysis data if missing
127    let metadata_with_waveform = add_analysis_data_if_missing(
128        &metadata.unwrap_or_default(),
129        samples,
130        sample_rate,
131        channels,
132        50, // 50 peaks per second default
133    )
134    .map_err(to_js_err)?;
135
136    let encoder = Encoder::new(sample_rate, channels, bit_depth);
137    encoder
138        .encode(samples, &metadata_with_waveform)
139        .map_err(to_js_err)
140}
141
142/// encode samples to flo lossy
143///
144/// # Arguments
145/// * `samples` - Interleaved audio samples (f32, -1.0 to 1.0)
146/// * `sample_rate` - Sample rate in Hz (e.g., 44100)
147/// * `channels` - Number of audio channels (1 or 2)
148/// * `bit_depth` - Bits per sample (typically 16)
149/// * `quality` - Quality level 0-4 (0=low/~64kbps, 4=transparent/~320kbps)
150/// * `metadata` - Optional MessagePack metadata
151///
152/// # Returns
153/// flo file as byte array
154///
155/// # Note
156/// For advanced usage with continuous quality control (0.0-1.0) or custom settings,
157/// use the `LossyEncoder` builder pattern directly.
158#[cfg(feature = "wasm")]
159#[wasm_bindgen]
160pub fn encode_lossy(
161    samples: &[f32],
162    sample_rate: u32,
163    channels: u8,
164    _bit_depth: u8,
165    quality: u8,
166    metadata: Option<Vec<u8>>,
167) -> Result<Vec<u8>, JsValue> {
168    validate_audio_format(samples, sample_rate, channels, _bit_depth).map_err(to_js_err)?;
169
170    // quality levels to 0.0-1.0
171    let quality_f32 = match quality {
172        0 => 0.0,
173        1 => 0.35,
174        2 => 0.55,
175        3 => 0.75,
176        _ => 1.0,
177    };
178
179    // Auto-add analysis data if missing
180    let metadata_with_waveform = add_analysis_data_if_missing(
181        &metadata.unwrap_or_default(),
182        samples,
183        sample_rate,
184        channels,
185        50, // 50 peaks per second default
186    )
187    .map_err(to_js_err)?;
188
189    let mut encoder = lossy::TransformEncoder::new(sample_rate, channels, quality_f32);
190    encoder
191        .encode_to_flo(samples, &metadata_with_waveform)
192        .map_err(to_js_err)
193}
194
195/// encode to flo lossy with target bitrate
196///
197/// # Arguments
198/// * `samples` - Interleaved audio samples (f32, -1.0 to 1.0)
199/// * `sample_rate` - Sample rate in Hz (e.g., 44100)
200/// * `channels` - Number of audio channels
201/// * `bit_depth` - Bits per sample (16, 24, or 32)
202/// * `target_bitrate_kbps` - Target bitrate in kbps (e.g., 128, 192, 256, 320)
203/// * `metadata` - Optional MessagePack metadata
204///
205/// # Returns
206/// flo file as byte array
207#[cfg(feature = "wasm")]
208#[wasm_bindgen]
209pub fn encode_with_bitrate(
210    samples: &[f32],
211    sample_rate: u32,
212    channels: u8,
213    _bit_depth: u8,
214    target_bitrate_kbps: u32,
215    metadata: Option<Vec<u8>>,
216) -> Result<Vec<u8>, JsValue> {
217    validate_audio_format(samples, sample_rate, channels, _bit_depth).map_err(to_js_err)?;
218
219    // bitrate to quality
220    let quality =
221        lossy::QualityPreset::from_bitrate(target_bitrate_kbps, sample_rate, channels).as_f32();
222
223    // Auto-add analysis data if missing
224    let metadata_with_waveform = add_analysis_data_if_missing(
225        &metadata.unwrap_or_default(),
226        samples,
227        sample_rate,
228        channels,
229        50, // 50 peaks per second default
230    )
231    .map_err(to_js_err)?;
232
233    let mut encoder = lossy::TransformEncoder::new(sample_rate, channels, quality);
234    encoder
235        .encode_to_flo(samples, &metadata_with_waveform)
236        .map_err(to_js_err)
237}
238
239/// Add analysis data to metadata if not present
240///
241/// # Arguments
242/// * `metadata` - MessagePack metadata bytes
243/// * `samples` - Audio samples (interleaved)
244/// * `sample_rate` - Sample rate in Hz
245/// * `channels` - Number of audio channels
246/// * `peaks_per_second` - Number of peaks per second (default: 50)
247///
248/// # Returns
249/// Updated metadata with analysis data (waveform, spectrum, loudness)
250#[cfg(feature = "wasm")]
251fn validate_audio_format(
252    samples: &[f32],
253    sample_rate: u32,
254    channels: u8,
255    bit_depth: u8,
256) -> Result<(), String> {
257    if sample_rate == 0 {
258        return Err("Sample rate must be greater than zero".to_string());
259    }
260    if channels == 0 {
261        return Err("Channel count must be greater than zero".to_string());
262    }
263    if !matches!(bit_depth, 8 | 16 | 24 | 32) {
264        return Err("Bit depth must be 8, 16, 24, or 32".to_string());
265    }
266    if !samples.len().is_multiple_of(channels as usize) {
267        return Err("Interleaved samples must contain a complete final frame".to_string());
268    }
269    Ok(())
270}
271
272#[cfg(feature = "wasm")]
273fn add_analysis_data_if_missing(
274    metadata: &[u8],
275    samples: &[f32],
276    sample_rate: u32,
277    channels: u8,
278    peaks_per_second: u32,
279) -> Result<Vec<u8>, String> {
280    // Try to parse existing metadata
281    let mut flo_metadata: FloMetadata = if !metadata.is_empty() {
282        from_slice(metadata).map_err(|e| format!("Invalid metadata: {}", e))?
283    } else {
284        FloMetadata::default()
285    };
286
287    // Add waveform data if not present
288    if flo_metadata.waveform_data.is_none() {
289        let waveform = core::analysis::extract_waveform_peaks(
290            samples,
291            channels,
292            sample_rate,
293            peaks_per_second,
294        );
295        flo_metadata.waveform_data = Some(waveform);
296    }
297
298    // Add spectral fingerprint if not present
299    if flo_metadata.spectrum_fingerprint.is_none() {
300        let spectral_fingerprint = core::analysis::extract_spectral_fingerprint(
301            samples,
302            channels,
303            sample_rate,
304            Some(2048), // Default FFT size
305            Some(1024), // Default hop size for 50% overlap
306        );
307
308        // Convert spectral data to bytes for storage
309        if let Ok(spectral_bytes) = messagepack_serde::to_vec(&spectral_fingerprint) {
310            flo_metadata.spectrum_fingerprint = Some(spectral_bytes);
311        }
312    }
313
314    // Add loudness metrics if not present
315    if flo_metadata.loudness_profile.is_empty() {
316        let loudness_metrics =
317            core::ebu_r128::compute_ebu_r128_loudness(samples, channels, sample_rate);
318
319        // Convert loudness metrics to LoudnessPoint format
320        let loudness_point = core::metadata::LoudnessPoint {
321            timestamp_ms: 0,
322            lufs: loudness_metrics.integrated_lufs as f32,
323        };
324
325        flo_metadata.loudness_profile = vec![loudness_point];
326    }
327
328    // Always set length_ms (duration in milliseconds).
329    // `samples` is interleaved (L,R,L,R...)
330    let interleaved_len = samples.len() as u64;
331    let samples_per_channel = interleaved_len / (channels as u64);
332    let length_ms = (((samples_per_channel as f64) / (sample_rate as f64)) * 1000.0) as u64;
333    flo_metadata.length_ms = Some(length_ms);
334
335    // Serialize back to bytes
336    to_vec(&flo_metadata).map_err(|e| format!("Failed to serialize metadata: {}", e))
337}
338
339/// decode flo file to samples
340///
341/// This automatically detects whether the file uses lossless or lossy encoding
342/// and dispatches to the appropriate decoder.
343///
344/// # Arguments
345/// * `data` - flo file bytes
346///
347/// # Returns
348/// Interleaved audio samples (f32, -1.0 to 1.0)
349#[cfg(feature = "wasm")]
350#[wasm_bindgen]
351pub fn decode(data: &[u8]) -> Result<Vec<f32>, JsValue> {
352    // figure out if its transform/lossy
353    let reader = Reader::new();
354    let file = reader.read(data).map_err(to_js_err)?;
355
356    // any transform frames means lossy
357    let is_transform = file
358        .frames
359        .iter()
360        .any(|f| f.frame_type == (FrameType::Transform as u8));
361
362    if is_transform {
363        // lossy
364        decode_transform_file(&file).map_err(to_js_err)
365    } else {
366        // lossless
367        let decoder = Decoder::new();
368        decoder.decode_file(&file).map_err(to_js_err)
369    }
370}
371
372/// Decode a lossy flo file that uses transform-based compression
373///
374/// # Arguments
375/// * `file` - Parsed flo file
376///
377/// # Returns
378/// Interleaved audio samples (f32, -1.0 to 1.0)
379/// Decode a transform-based lossy file
380#[cfg(feature = "wasm")]
381fn decode_transform_file(file: &FloFile) -> FloResult<Vec<f32>> {
382    let mut decoder = lossy::TransformDecoder::new(file.header.sample_rate, file.header.channels);
383    let mut all_samples = Vec::new();
384    let mut frame_count = 0;
385
386    for frame in &file.frames {
387        if frame.channels.is_empty() {
388            continue;
389        }
390
391        // transform data is in first channels residuals
392        let frame_data = &frame.channels[0].residuals;
393
394        if let Some(transform_frame) = lossy::deserialize_frame(frame_data) {
395            let samples = decoder.decode_frame(&transform_frame);
396
397            // skip first frame (pre-roll for overlap-add)
398            if frame_count > 0 {
399                all_samples.extend(samples);
400            }
401            frame_count += 1;
402        } else {
403            return Err(crate::core::FloError::from(
404                "Failed to deserialize transform frame",
405            ));
406        }
407    }
408
409    Ok(all_samples)
410}
411
412/// Validate flo file integrity
413///
414/// # Arguments
415/// * `data` - flo file bytes
416///
417/// # Returns
418/// true if file is valid and CRC matches
419#[cfg(feature = "wasm")]
420#[wasm_bindgen]
421pub fn validate(data: &[u8]) -> Result<bool, JsValue> {
422    let reader = Reader::new();
423    match reader.read(data) {
424        Ok(file) => {
425            let start = (4 + file.header.header_size + file.header.toc_size) as usize;
426            let end = start + (file.header.data_size as usize);
427            if end <= data.len() {
428                let computed = core::crc32::compute(&data[start..end]);
429                Ok(computed == file.header.data_crc32)
430            } else {
431                Ok(false)
432            }
433        }
434        Err(_) => Ok(false),
435    }
436}
437
438/// Get information about a flo file
439///
440/// # Arguments
441/// * `data` - flo file bytes
442///
443/// # Returns
444/// AudioInfo struct with file details
445#[cfg(feature = "wasm")]
446#[wasm_bindgen]
447pub fn info(data: &[u8]) -> Result<AudioInfo, JsValue> {
448    let reader = Reader::new();
449    let file = reader.read(data).map_err(to_js_err)?;
450
451    // Use length_ms from metadata for duration
452    let metadata = FloMetadata::from_msgpack(&file.metadata).unwrap_or_default();
453    let duration_secs = metadata
454        .length_ms
455        .map(|ms| (ms as f64) / 1000.0)
456        .unwrap_or_else(|| {
457            // Fallback: calculate from total_samples / sample_rate
458            (file.header.total_samples as f64) / (file.header.sample_rate as f64)
459        });
460
461    // Calculate original size from total_samples (actual sample count in header)
462    let original_size = ((file.header.total_samples as f64)
463        * (file.header.channels as f64)
464        * ((file.header.bit_depth as f64) / 8.0)) as usize;
465    let compression_ratio = if !data.is_empty() {
466        (original_size as f64) / (data.len() as f64)
467    } else {
468        0.0
469    };
470
471    // check crc
472    let start = (4 + file.header.header_size + file.header.toc_size) as usize;
473    let end = start + (file.header.data_size as usize);
474    let crc_valid = if end <= data.len() {
475        core::crc32::compute(&data[start..end]) == file.header.data_crc32
476    } else {
477        false
478    };
479
480    // lossy mode from flags
481    let is_lossy = (file.header.flags & 0x01) != 0;
482    let lossy_quality = ((file.header.flags >> 8) & 0x0f) as u8;
483
484    Ok(AudioInfo {
485        version: format!(
486            "{}.{}",
487            file.header.version_major, file.header.version_minor
488        ),
489        sample_rate: file.header.sample_rate,
490        channels: file.header.channels,
491        bit_depth: file.header.bit_depth,
492        total_samples: file.header.total_samples,
493        duration_secs,
494        file_size: data.len(),
495        compression_ratio,
496        crc_valid,
497        is_lossy,
498        lossy_quality,
499    })
500}
501
502/// get lib version
503#[cfg(feature = "wasm")]
504#[wasm_bindgen]
505pub fn version() -> String {
506    format!("{}.{}", VERSION_MAJOR, VERSION_MINOR)
507}
508
509/// Format time in seconds to MM:SS or H:MM:SS string
510#[cfg(feature = "wasm")]
511#[wasm_bindgen]
512pub fn format_time(seconds: f64) -> String {
513    if !seconds.is_finite() || seconds < 0.0 {
514        return "0:00".to_string();
515    }
516
517    let total_secs = seconds.floor() as u64;
518    let hours = total_secs / 3600;
519    let mins = (total_secs % 3600) / 60;
520    let secs = total_secs % 60;
521
522    if hours > 0 {
523        format!("{}:{:02}:{:02}", hours, mins, secs)
524    } else {
525        format!("{}:{:02}", mins, secs)
526    }
527}
528
529/// Format time in milliseconds to MM:SS or H:MM:SS string
530#[cfg(feature = "wasm")]
531#[wasm_bindgen]
532pub fn format_time_ms(milliseconds: f64) -> String {
533    format_time(milliseconds / 1000.0)
534}
535
536/// Extract TOC (Table of Contents) entries from a flo file
537///
538/// # Returns
539/// Array of TOC entries with frame indices, byte offsets, and timestamps
540#[cfg(feature = "wasm")]
541#[wasm_bindgen]
542pub fn get_toc(flo_data: &[u8]) -> Result<Vec<JsValue>, JsValue> {
543    let toc = seeking::get_toc(flo_data).map_err(to_js_err)?;
544    let result = toc
545        .into_iter()
546        .map(|entry| {
547            let obj = js_sys::Object::new();
548            js_sys::Reflect::set(&obj, &"frame_index".into(), &entry.frame_index.into()).ok();
549            js_sys::Reflect::set(
550                &obj,
551                &"byte_offset".into(),
552                &(entry.byte_offset as f64).into(),
553            )
554            .ok();
555            js_sys::Reflect::set(&obj, &"frame_size".into(), &entry.frame_size.into()).ok();
556            js_sys::Reflect::set(&obj, &"timestamp_ms".into(), &entry.timestamp_ms.into()).ok();
557            obj.into()
558        })
559        .collect();
560    Ok(result)
561}
562
563/// Decode a single frame by index without decoding the entire file
564///
565/// # Arguments
566/// * `flo_data` - Complete flo file bytes
567/// * `frame_index` - Zero-based frame index
568///
569/// # Returns
570/// Interleaved audio samples for that frame (f32, -1.0 to 1.0)
571#[cfg(feature = "wasm")]
572#[wasm_bindgen]
573pub fn decode_frame_at(flo_data: &[u8], frame_index: u32) -> Result<Vec<f32>, JsValue> {
574    seeking::decode_frame_at(flo_data, frame_index).map_err(to_js_err)
575}
576
577/// Seek to a specific time in milliseconds
578///
579/// # Arguments
580/// * `flo_data` - Complete flo file bytes
581/// * `time_ms` - Target time in milliseconds
582///
583/// # Returns
584/// Seek result object containing frame index, byte offset, timestamp, sample offset, and next timestamp.
585#[cfg(feature = "wasm")]
586#[wasm_bindgen]
587pub fn seek_to_time(flo_data: &[u8], time_ms: u32) -> Result<JsValue, JsValue> {
588    let result = seeking::seek_to_time(flo_data, time_ms).map_err(to_js_err)?;
589
590    let obj = js_sys::Object::new();
591    js_sys::Reflect::set(&obj, &"frame_index".into(), &result.frame_index.into())?;
592    js_sys::Reflect::set(
593        &obj,
594        &"byte_offset".into(),
595        &(result.byte_offset as f64).into(),
596    )?;
597    js_sys::Reflect::set(&obj, &"timestamp_ms".into(), &result.timestamp_ms.into())?;
598    js_sys::Reflect::set(&obj, &"sample_offset".into(), &result.sample_offset.into())?;
599    js_sys::Reflect::set(
600        &obj,
601        &"next_timestamp_ms".into(),
602        &result.next_timestamp_ms.into(),
603    )?;
604
605    Ok(obj.into())
606}
607
608// streaming decoder wasm api
609
610#[cfg(feature = "wasm")]
611#[wasm_bindgen]
612pub struct WasmStreamingDecoder {
613    inner: StreamingDecoder,
614}
615
616#[cfg(feature = "wasm")]
617#[wasm_bindgen]
618impl WasmStreamingDecoder {
619    /// new streaming decoder
620    #[wasm_bindgen(constructor)]
621    pub fn new() -> Self {
622        Self {
623            inner: StreamingDecoder::new(),
624        }
625    }
626
627    /// feed data to the decoder, call as bytes come in from network
628    #[cfg(feature = "wasm")]
629    #[wasm_bindgen]
630    pub fn feed(&mut self, data: &[u8]) -> Result<bool, JsValue> {
631        self.inner.feed(data).map_err(to_js_err)
632    }
633
634    /// Check if the decoder is ready to produce audio
635    #[cfg(feature = "wasm")]
636    #[wasm_bindgen]
637    pub fn is_ready(&self) -> bool {
638        self.inner.state() == DecoderState::Ready
639    }
640
641    /// stream done?
642    #[cfg(feature = "wasm")]
643    #[wasm_bindgen]
644    pub fn is_finished(&self) -> bool {
645        self.inner.state() == DecoderState::Finished
646    }
647
648    /// Check if there was an error
649    #[cfg(feature = "wasm")]
650    #[wasm_bindgen]
651    pub fn has_error(&self) -> bool {
652        self.inner.state() == DecoderState::Error
653    }
654
655    /// Get the current state as a string
656    #[cfg(feature = "wasm")]
657    #[wasm_bindgen]
658    pub fn state(&self) -> String {
659        match self.inner.state() {
660            DecoderState::WaitingForHeader => "waiting_for_header".into(),
661            DecoderState::WaitingForToc => "waiting_for_toc".into(),
662            DecoderState::Ready => "ready".into(),
663            DecoderState::Finished => "finished".into(),
664            DecoderState::Error => "error".into(),
665        }
666    }
667
668    /// Get audio information (available after header is parsed)
669    ///
670    /// Returns null if header hasn't been parsed yet.
671    #[cfg(feature = "wasm")]
672    #[wasm_bindgen]
673    pub fn get_info(&self) -> Result<JsValue, JsValue> {
674        match self.inner.info() {
675            Some(info) => {
676                let obj = js_sys::Object::new();
677                js_sys::Reflect::set(&obj, &"sample_rate".into(), &info.sample_rate.into())?;
678                js_sys::Reflect::set(&obj, &"channels".into(), &info.channels.into())?;
679                js_sys::Reflect::set(&obj, &"bit_depth".into(), &info.bit_depth.into())?;
680                js_sys::Reflect::set(
681                    &obj,
682                    &"total_samples".into(),
683                    &(info.total_samples as f64).into(),
684                )?;
685                js_sys::Reflect::set(&obj, &"is_lossy".into(), &info.is_lossy.into())?;
686                Ok(obj.into())
687            }
688            None => Ok(JsValue::NULL),
689        }
690    }
691
692    /// decode all currently available samples
693    #[cfg(feature = "wasm")]
694    #[wasm_bindgen]
695    pub fn decode_available(&mut self) -> Result<Vec<f32>, JsValue> {
696        self.inner.decode_available().map_err(to_js_err)
697    }
698
699    /// Decode the next available frame
700    ///
701    /// Returns interleaved f32 samples for one frame, or null if no frame is ready.
702    /// This enables true streaming: decode and play frames as they arrive.
703    ///
704    /// Usage pattern:
705    /// ```js
706    /// while (true) {
707    ///     const samples = decoder.next_frame();
708    ///     if (samples === null) break; // No more frames ready
709    ///     playAudio(samples);
710    /// }
711    /// ```
712    #[cfg(feature = "wasm")]
713    #[wasm_bindgen]
714    pub fn next_frame(&mut self) -> Result<JsValue, JsValue> {
715        match self.inner.next_frame() {
716            Ok(Some(samples)) => {
717                let array = js_sys::Float32Array::new_with_length(samples.len() as u32);
718                array.copy_from(&samples);
719                Ok(array.into())
720            }
721            Ok(None) => Ok(JsValue::NULL),
722            Err(e) => Err(to_js_err(e)),
723        }
724    }
725
726    /// how many frames ready to decode
727    #[cfg(feature = "wasm")]
728    #[wasm_bindgen]
729    pub fn available_frames(&self) -> usize {
730        self.inner.available_frames()
731    }
732
733    /// current frame index
734    #[cfg(feature = "wasm")]
735    #[wasm_bindgen]
736    pub fn current_frame_index(&self) -> usize {
737        self.inner.current_frame_index()
738    }
739
740    /// Reset the decoder to initial state
741    ///
742    /// Use this to start decoding a new stream.
743    #[cfg(feature = "wasm")]
744    #[wasm_bindgen]
745    pub fn reset(&mut self) {
746        self.inner.reset();
747    }
748
749    /// bytes currently buffered
750    #[cfg(feature = "wasm")]
751    #[wasm_bindgen]
752    pub fn buffered_bytes(&self) -> usize {
753        self.inner.buffered_bytes()
754    }
755}
756
757#[cfg(feature = "wasm")]
758impl Default for WasmStreamingDecoder {
759    fn default() -> Self {
760        Self::new()
761    }
762}
763
764/// Encodes samples frame-by-frame as they arrive, returning encoded frames ready for transmission.
765/// Use this for low-latency streaming encoding where you push samples and pull frames.
766#[cfg(feature = "wasm")]
767#[wasm_bindgen]
768pub struct WasmStreamingEncoder {
769    inner: StreamingEncoder,
770}
771
772#[cfg(feature = "wasm")]
773#[wasm_bindgen]
774impl WasmStreamingEncoder {
775    /// Create a new streaming encoder
776    ///
777    /// # Arguments
778    /// * `sample_rate` - Sample rate in Hz (e.g., 44100)
779    /// * `channels` - Number of channels (1 or 2)
780    /// * `bit_depth` - Bits per sample (16, 24, or 32)
781    ///
782    /// # Returns
783    /// New encoder instance
784    #[wasm_bindgen(constructor)]
785    pub fn new(sample_rate: u32, channels: u8, bit_depth: u8) -> Self {
786        Self {
787            inner: StreamingEncoder::new(sample_rate, channels, bit_depth),
788        }
789    }
790
791    /// Set compression level (0-9)
792    ///
793    /// # Arguments
794    /// * `level` - Compression level (0=fast/large, 9=slow/small)
795    ///
796    /// # Returns
797    /// Self for method chaining
798    #[cfg(feature = "wasm")]
799    #[wasm_bindgen]
800    pub fn with_compression(mut self, level: u8) -> Self {
801        self.inner = self.inner.with_compression(level);
802        self
803    }
804
805    /// Push audio samples to the encoder
806    ///
807    /// Samples should be interleaved if multi-channel (e.g., [L0, R0, L1, R1, ...] for stereo).
808    /// Frames are encoded automatically when enough samples accumulate.
809    ///
810    /// # Arguments
811    /// * `samples` - Interleaved f32 audio samples (-1.0 to 1.0)
812    ///
813    /// # Returns
814    /// Error if encoding fails
815    #[cfg(feature = "wasm")]
816    #[wasm_bindgen]
817    pub fn push_samples(&mut self, samples: &[f32]) -> Result<(), JsValue> {
818        self.inner.push_samples(samples).map_err(to_js_err)
819    }
820
821    /// Get the next encoded frame if available
822    ///
823    /// Returns an object with: index, timestamp_ms, data (Uint8Array), samples
824    /// Returns null if no frames are ready yet.
825    ///
826    /// # Returns
827    /// Encoded frame object or null
828    #[cfg(feature = "wasm")]
829    #[wasm_bindgen]
830    pub fn next_frame(&mut self) -> JsValue {
831        match self.inner.next_frame() {
832            Some(frame) => {
833                let obj = js_sys::Object::new();
834                let _ = js_sys::Reflect::set(&obj, &"index".into(), &frame.index.into());
835                let _ =
836                    js_sys::Reflect::set(&obj, &"timestamp_ms".into(), &frame.timestamp_ms.into());
837                let _ = js_sys::Reflect::set(
838                    &obj,
839                    &"data".into(),
840                    &js_sys::Uint8Array::from(&frame.data[..]).into(),
841                );
842                let _ = js_sys::Reflect::set(&obj, &"samples".into(), &frame.samples.into());
843                obj.into()
844            }
845            None => JsValue::NULL,
846        }
847    }
848
849    /// Get number of samples currently buffered
850    ///
851    /// # Returns
852    /// Number of sample frames in buffer
853    #[cfg(feature = "wasm")]
854    #[wasm_bindgen]
855    pub fn pending_samples(&self) -> usize {
856        self.inner.pending_samples()
857    }
858
859    /// Get number of encoded frames ready for transmission
860    ///
861    /// # Returns
862    /// Number of ready frames
863    #[cfg(feature = "wasm")]
864    #[wasm_bindgen]
865    pub fn pending_frames(&self) -> usize {
866        self.inner.pending_frames()
867    }
868
869    /// Flush remaining samples and finalize encoding
870    ///
871    /// Call this when done pushing samples. Encodes any remaining partial frame.
872    /// After this, call finalize() to get the complete flo file.
873    ///
874    /// # Returns
875    /// Error if encoding fails
876    #[cfg(feature = "wasm")]
877    #[wasm_bindgen]
878    pub fn flush(&mut self) -> Result<(), JsValue> {
879        self.inner.flush_into_frames().map_err(to_js_err)?;
880        Ok(())
881    }
882
883    /// Build a complete flo file from all accumulated frames
884    ///
885    /// Call flush() first, then this method to generate the complete file.
886    /// The file will contain all encoded frames with proper table-of-contents.
887    ///
888    /// # Arguments
889    /// * `metadata` - Optional metadata bytes (MessagePack encoded, use create_metadata())
890    ///
891    /// # Returns
892    /// Complete flo file as byte array
893    #[cfg(feature = "wasm")]
894    #[wasm_bindgen]
895    pub fn finalize(&mut self, metadata: Option<Vec<u8>>) -> Result<Vec<u8>, JsValue> {
896        let meta = metadata.unwrap_or_default();
897        self.inner.finalize(&meta).map_err(to_js_err)
898    }
899}
900
901#[cfg(feature = "wasm")]
902impl Default for WasmStreamingEncoder {
903    fn default() -> Self {
904        Self {
905            inner: StreamingEncoder::new(44100, 2, 16),
906        }
907    }
908}
909
910/// Create metadata from basic fields and serialize to MessagePack
911///
912/// # Arguments
913/// * `title` - Optional title
914/// * `artist` - Optional artist
915/// * `album` - Optional album
916///
917/// # Returns
918/// MessagePack bytes containing metadata
919#[cfg(feature = "wasm")]
920#[wasm_bindgen]
921pub fn create_metadata(
922    title: Option<String>,
923    artist: Option<String>,
924    album: Option<String>,
925) -> Result<Vec<u8>, JsValue> {
926    let meta = FloMetadata::with_basic(title, artist, album);
927    meta.to_msgpack()
928        .map_err(|e| JsValue::from_str(&e.to_string()))
929}
930
931/// Create metadata from a JavaScript object
932///
933/// Accepts an object with any of the supported metadata fields.
934/// See FloMetadata for available fields.
935///
936/// # Returns
937/// MessagePack bytes containing metadata
938#[cfg(feature = "wasm")]
939#[wasm_bindgen]
940pub fn create_metadata_from_object(obj: JsValue) -> Result<Vec<u8>, JsValue> {
941    let meta: FloMetadata = serde_wasm_bindgen::from_value(obj)
942        .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
943    meta.to_msgpack()
944        .map_err(|e| JsValue::from_str(&e.to_string()))
945}
946
947/// Extract metadata from a flo file
948///
949/// # Arguments
950/// * `data` - flo file bytes
951///
952/// # Returns
953/// JavaScript object with metadata fields (or null if no metadata)
954#[cfg(feature = "wasm")]
955#[wasm_bindgen]
956pub fn get_metadata(data: &[u8]) -> Result<JsValue, JsValue> {
957    let reader = Reader::new();
958    let file = reader.read(data).map_err(to_js_err)?;
959
960    if file.metadata.is_empty() {
961        return Ok(JsValue::NULL);
962    }
963
964    let meta = FloMetadata::from_msgpack(&file.metadata)
965        .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
966
967    serde_wasm_bindgen::to_value(&meta)
968        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
969}
970
971/// Get cover art from a flo file
972///
973/// # Arguments
974/// * `data` - flo file bytes
975///
976/// # Returns
977/// Object with `mime_type` and `data` (Uint8Array) or null if no cover
978#[cfg(feature = "wasm")]
979#[wasm_bindgen]
980pub fn get_cover_art(data: &[u8]) -> Result<JsValue, JsValue> {
981    let reader = Reader::new();
982    let file = reader.read(data).map_err(to_js_err)?;
983
984    if file.metadata.is_empty() {
985        return Ok(JsValue::NULL);
986    }
987
988    let meta = FloMetadata::from_msgpack(&file.metadata)
989        .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
990
991    // try front cover first
992    if let Some(pic) = meta.front_cover().or_else(|| meta.any_picture()) {
993        let obj = js_sys::Object::new();
994        js_sys::Reflect::set(&obj, &"mime_type".into(), &pic.mime_type.clone().into())?;
995        js_sys::Reflect::set(
996            &obj,
997            &"data".into(),
998            &js_sys::Uint8Array::from(&pic.data[..]).into(),
999        )?;
1000        if let Some(ref desc) = pic.description {
1001            js_sys::Reflect::set(&obj, &"description".into(), &desc.clone().into())?;
1002        }
1003        Ok(obj.into())
1004    } else {
1005        Ok(JsValue::NULL)
1006    }
1007}
1008
1009/// Set a single field in existing metadata bytes
1010///
1011/// Uses serde to dynamically set fields - field names match FloMetadata struct.
1012/// For complex fields (pictures, synced_lyrics, etc.) use create_metadata_from_object.
1013///
1014/// # Arguments
1015/// * `metadata` - Existing MessagePack metadata bytes (or empty for new)
1016/// * `field` - Field name (e.g., "title", "artist", "bpm")
1017/// * `value` - Field value (string, number, or null)
1018///
1019/// # Returns
1020/// Updated MessagePack metadata bytes
1021#[cfg(feature = "wasm")]
1022#[wasm_bindgen]
1023pub fn set_metadata_field(
1024    metadata: Option<Vec<u8>>,
1025    field: &str,
1026    value: JsValue,
1027) -> Result<Vec<u8>, JsValue> {
1028    // parse or create new
1029    let meta = match &metadata {
1030        Some(data) if !data.is_empty() => FloMetadata::from_msgpack(data)
1031            .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?,
1032        _ => FloMetadata::new(),
1033    };
1034
1035    // modify via serde
1036    let mut obj: serde_json::Value = serde_json::to_value(&meta)
1037        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
1038
1039    // jsvalue to serde
1040    let json_value = if value.is_null() || value.is_undefined() {
1041        serde_json::Value::Null
1042    } else if let Some(s) = value.as_string() {
1043        serde_json::Value::String(s)
1044    } else if let Some(n) = value.as_f64() {
1045        serde_json::json!(n)
1046    } else if let Some(b) = value.as_bool() {
1047        serde_json::Value::Bool(b)
1048    } else {
1049        // try json for complex stuff
1050        let js_json = js_sys::JSON::stringify(&value)
1051            .map_err(|_| JsValue::from_str("Cannot serialize value"))?;
1052        serde_json::from_str(&js_json.as_string().unwrap_or_default())
1053            .map_err(|e| JsValue::from_str(&format!("Invalid JSON: {}", e)))?
1054    };
1055
1056    // do it
1057    if let serde_json::Value::Object(ref mut map) = obj {
1058        map.insert(field.to_string(), json_value);
1059    } else {
1060        return Err(JsValue::from_str("Internal error: metadata not an object"));
1061    }
1062
1063    // back to struct
1064    let updated: FloMetadata = serde_json::from_value(obj)
1065        .map_err(|e| JsValue::from_str(&format!("Invalid field '{}': {}", field, e)))?;
1066
1067    updated
1068        .to_msgpack()
1069        .map_err(|e| JsValue::from_str(&e.to_string()))
1070}
1071
1072/// Get synced lyrics from a flo file
1073///
1074/// # Returns
1075/// Array of synced lyrics objects or null if none
1076#[cfg(feature = "wasm")]
1077#[wasm_bindgen]
1078pub fn get_synced_lyrics(data: &[u8]) -> Result<JsValue, JsValue> {
1079    let reader = Reader::new();
1080    let file = reader.read(data).map_err(to_js_err)?;
1081
1082    if file.metadata.is_empty() {
1083        return Ok(JsValue::NULL);
1084    }
1085
1086    let meta = FloMetadata::from_msgpack(&file.metadata)
1087        .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
1088
1089    if meta.synced_lyrics.is_empty() {
1090        return Ok(JsValue::NULL);
1091    }
1092
1093    serde_wasm_bindgen::to_value(&meta.synced_lyrics)
1094        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1095}
1096
1097/// Get waveform data from a flo file for instant visualization
1098///
1099/// # Returns
1100/// WaveformData object or null if not present
1101#[cfg(feature = "wasm")]
1102#[wasm_bindgen]
1103pub fn get_waveform_data(data: &[u8]) -> Result<JsValue, JsValue> {
1104    let reader = Reader::new();
1105    let file = reader.read(data).map_err(to_js_err)?;
1106
1107    if file.metadata.is_empty() {
1108        return Ok(JsValue::NULL);
1109    }
1110
1111    let meta = FloMetadata::from_msgpack(&file.metadata)
1112        .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
1113
1114    match meta.waveform_data {
1115        Some(ref waveform) => serde_wasm_bindgen::to_value(waveform)
1116            .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))),
1117        None => Ok(JsValue::NULL),
1118    }
1119}
1120
1121/// Get section markers from a flo file
1122///
1123/// # Returns
1124/// Array of section markers or null if none
1125#[cfg(feature = "wasm")]
1126#[wasm_bindgen]
1127pub fn get_section_markers(data: &[u8]) -> Result<JsValue, JsValue> {
1128    let reader = Reader::new();
1129    let file = reader.read(data).map_err(to_js_err)?;
1130
1131    if file.metadata.is_empty() {
1132        return Ok(JsValue::NULL);
1133    }
1134
1135    let meta = FloMetadata::from_msgpack(&file.metadata)
1136        .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
1137
1138    if meta.section_markers.is_empty() {
1139        return Ok(JsValue::NULL);
1140    }
1141
1142    serde_wasm_bindgen::to_value(&meta.section_markers)
1143        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1144}
1145
1146// zero-copy metadata editing
1147
1148/// update metadata without re-encoding audio
1149///
1150/// # Arguments
1151/// * `flo_data` - Original flo file bytes
1152/// * `new_metadata` - New MessagePack metadata bytes (use create_metadata_*)
1153///
1154/// # Returns
1155/// New flo file with updated metadata
1156#[cfg(feature = "wasm")]
1157#[wasm_bindgen]
1158pub fn update_metadata(flo_data: &[u8], new_metadata: &[u8]) -> Result<Vec<u8>, JsValue> {
1159    update_metadata_bytes(flo_data, new_metadata).map_err(to_js_err)
1160}
1161
1162/// update metadata without re-encoding (native)
1163pub fn update_metadata_bytes(flo_data: &[u8], new_metadata: &[u8]) -> FloResult<Vec<u8>> {
1164    // basic checks
1165    if flo_data.len() < (HEADER_SIZE as usize) {
1166        return Err(crate::core::FloError::from(
1167            "File too small to be valid flo",
1168        ));
1169    }
1170
1171    // check magic
1172    if flo_data[0..4] != MAGIC {
1173        return Err(crate::core::FloError::from("Invalid flo file: bad magic"));
1174    }
1175
1176    // read header for chunk sizes
1177    let reader = Reader::new();
1178    let file = reader.read(flo_data)?;
1179
1180    // find where metadata starts
1181    // layout: magic(4) + header + toc + data + extra + metadata
1182    let meta_offset = 4
1183        + (file.header.header_size as usize)
1184        + (file.header.toc_size as usize)
1185        + (file.header.data_size as usize)
1186        + (file.header.extra_size as usize);
1187
1188    // copy up to metadata
1189    let mut result = Vec::with_capacity(meta_offset + new_metadata.len());
1190    result.extend_from_slice(&flo_data[..meta_offset]);
1191
1192    // add new metadata
1193    result.extend_from_slice(new_metadata);
1194
1195    // fix meta_size in header
1196    // header layout: version(2) + flags(2) + sample_rate(4) + ...
1197    // meta_size is at offset: 4 + 58 = 62
1198    let meta_size_offset = 4 + 2 + 2 + 4 + 1 + 1 + 8 + 1 + 3 + 4 + 8 + 8 + 8 + 8;
1199    let new_meta_size = new_metadata.len() as u64;
1200    result[meta_size_offset..meta_size_offset + 8].copy_from_slice(&new_meta_size.to_le_bytes());
1201
1202    Ok(result)
1203}
1204
1205/// Replace just the metadata in a flo file (convenience function)
1206///
1207/// Takes a metadata object directly instead of MessagePack bytes.
1208///
1209/// # Arguments
1210/// * `flo_data` - Original flo file bytes
1211/// * `metadata` - JavaScript metadata object
1212///
1213/// # Returns
1214/// New flo file with updated metadata
1215#[cfg(feature = "wasm")]
1216#[wasm_bindgen]
1217pub fn set_metadata(flo_data: &[u8], metadata: JsValue) -> Result<Vec<u8>, JsValue> {
1218    let new_meta_bytes = create_metadata_from_object(metadata)?;
1219    update_metadata(flo_data, &new_meta_bytes)
1220}
1221
1222/// Remove all metadata from a flo file
1223///
1224/// # Arguments
1225/// * `flo_data` - Original flo file bytes
1226///
1227/// # Returns
1228/// New flo file with no metadata
1229pub fn strip_metadata_bytes(flo_data: &[u8]) -> FloResult<Vec<u8>> {
1230    update_metadata_bytes(flo_data, &[])
1231}
1232
1233/// Remove all metadata from a flo file
1234///
1235/// # Arguments
1236/// * `flo_data` - Original flo file bytes
1237///
1238/// # Returns
1239/// New flo file with no metadata
1240#[cfg(feature = "wasm")]
1241#[wasm_bindgen]
1242pub fn strip_metadata(flo_data: &[u8]) -> Result<Vec<u8>, JsValue> {
1243    strip_metadata_bytes(flo_data).map_err(to_js_err)
1244}
1245
1246/// Get just the metadata bytes from a flo file
1247///
1248/// # Arguments
1249/// * `flo_data` - flo file bytes
1250///
1251/// # Returns
1252/// Raw MessagePack metadata bytes (or empty array)
1253#[cfg(feature = "wasm")]
1254#[wasm_bindgen]
1255pub fn get_metadata_bytes(flo_data: &[u8]) -> Result<Vec<u8>, JsValue> {
1256    get_metadata_bytes_native(flo_data).map_err(to_js_err)
1257}
1258
1259/// Get just the metadata bytes from a flo file
1260///
1261/// # Arguments
1262/// * `flo_data` - flo file bytes
1263///
1264/// # Returns
1265/// Raw MessagePack metadata bytes (or empty array)
1266pub fn get_metadata_bytes_native(flo_data: &[u8]) -> FloResult<Vec<u8>> {
1267    if flo_data.len() < (HEADER_SIZE as usize) {
1268        return Err(crate::core::FloError::from("File too small"));
1269    }
1270
1271    // just read header for metadata location
1272    let reader = Reader::new();
1273    let file = reader.read(flo_data)?;
1274
1275    Ok(file.metadata)
1276}
1277
1278/// does the file have metadata? (wasm binding)
1279#[cfg(feature = "wasm")]
1280#[wasm_bindgen(js_name = has_metadata)]
1281pub fn has_metadata_wasm(flo_data: &[u8]) -> bool {
1282    has_metadata(flo_data)
1283}
1284
1285/// does the file have metadata? (native)
1286pub fn has_metadata(flo_data: &[u8]) -> bool {
1287    if flo_data.len() < (HEADER_SIZE as usize) {
1288        return false;
1289    }
1290
1291    // fast path, just read meta_size from header
1292    let meta_size_offset = 4 + 2 + 2 + 4 + 1 + 1 + 8 + 1 + 3 + 4 + 8 + 8 + 8 + 8;
1293    if flo_data.len() < meta_size_offset + 8 {
1294        return false;
1295    }
1296
1297    let meta_size = u64::from_le_bytes(
1298        flo_data[meta_size_offset..meta_size_offset + 8]
1299            .try_into()
1300            .unwrap_or([0; 8]),
1301    );
1302
1303    meta_size > 0
1304}
1305
1306/// Extract waveform peaks from audio samples (native version)
1307///
1308/// # Arguments
1309/// * `samples` - Interleaved audio samples (f32, -1.0 to 1.0)
1310/// * `sample_rate` - Sample rate in Hz (e.g., 44100)
1311/// * `channels` - Number of audio channels (1 or 2)
1312/// * `peaks_per_second` - Number of peak values per second (default: 50)
1313///
1314/// # Returns
1315/// WaveformData struct containing extracted peaks
1316pub fn extract_waveform_peaks_to_struct(
1317    samples: &[f32],
1318    sample_rate: u32,
1319    channels: u8,
1320    peaks_per_second: Option<u32>,
1321) -> core::metadata::WaveformData {
1322    let peaks_per_second = peaks_per_second.unwrap_or(50);
1323    core::analysis::extract_waveform_peaks(samples, channels, sample_rate, peaks_per_second)
1324}
1325
1326/// Extract waveform RMS from audio samples
1327///
1328/// # Arguments
1329/// * `samples` - Interleaved audio samples (f32, -1.0 to 1.0)
1330/// * `sample_rate` - Sample rate in Hz (e.g., 44100)
1331/// * `channels` - Number of audio channels (1 or 2)
1332/// * `peaks_per_second` - Number of RMS values per second (default: 50)
1333///
1334/// # Returns
1335/// WaveformData struct containing extracted RMS values
1336pub fn extract_waveform_rms_to_struct(
1337    samples: &[f32],
1338    sample_rate: u32,
1339    channels: u8,
1340    peaks_per_second: Option<u32>,
1341) -> core::metadata::WaveformData {
1342    let peaks_per_second = peaks_per_second.unwrap_or(50);
1343    core::analysis::extract_waveform_rms(samples, channels, sample_rate, peaks_per_second)
1344}
1345
1346/// Extract spectral fingerprint from audio samples
1347///
1348/// # Arguments
1349/// * `samples` - Audio samples (interleaved if multi-channel)
1350/// * `channels` - Number of audio channels
1351/// * `sample_rate` - Sample rate in Hz
1352/// * `fft_size` - FFT window size (must be power of 2)
1353/// * `hop_size` - Hop size between consecutive frames
1354///
1355/// # Returns
1356/// SpectralFingerprint object with frequency analysis
1357#[cfg(target_arch = "wasm32")]
1358#[cfg(feature = "wasm")]
1359#[wasm_bindgen]
1360pub fn extract_spectral_fingerprint_wasm(
1361    samples: &[f32],
1362    channels: u32,
1363    sample_rate: u32,
1364    fft_size: Option<usize>,
1365    hop_size: Option<usize>,
1366) -> Result<JsValue, JsValue> {
1367    use crate::core::extract_spectral_fingerprint;
1368    let fingerprint =
1369        extract_spectral_fingerprint(samples, channels as u8, sample_rate, fft_size, hop_size);
1370    serde_wasm_bindgen::to_value(&fingerprint)
1371        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1372}
1373
1374/// Extract dominant frequencies from spectral fingerprint
1375#[cfg(target_arch = "wasm32")]
1376#[cfg(feature = "wasm")]
1377#[wasm_bindgen]
1378pub fn extract_dominant_frequencies_wasm(
1379    fingerprint_js: JsValue,
1380    num_frequencies: usize,
1381) -> Result<JsValue, JsValue> {
1382    use crate::core::analysis::{extract_dominant_frequencies, SpectralFingerprint};
1383
1384    let fingerprint: SpectralFingerprint = serde_wasm_bindgen::from_value(fingerprint_js)
1385        .map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
1386
1387    let dominant_freqs = extract_dominant_frequencies(&fingerprint, num_frequencies);
1388    serde_wasm_bindgen::to_value(&dominant_freqs)
1389        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1390}
1391
1392/// Extract dominant frequencies from spectral fingerprint
1393///
1394/// # Arguments
1395/// * `fingerprint` - SpectralFingerprint struct
1396/// * `num_frequencies` - Number of dominant frequencies to extract per frame
1397///
1398/// # Returns
1399/// Vector of vectors containing dominant frequencies (Hz) for each frame
1400pub fn extract_dominant_frequencies_to_vec(
1401    fingerprint: &core::analysis::SpectralFingerprint,
1402    num_frequencies: usize,
1403) -> Vec<Vec<f64>> {
1404    core::analysis::extract_dominant_frequencies(fingerprint, num_frequencies)
1405}
1406
1407/// Extract dominant frequencies from audio samples (convenience function)
1408///
1409/// # Arguments
1410/// * `samples` - Audio samples (interleaved if multi-channel)
1411/// * `sample_rate` - Sample rate in Hz
1412/// * `channels` - Number of audio channels
1413/// * `num_frequencies` - Number of dominant frequencies to extract per frame
1414/// * `fft_size` - FFT window size (optional, will auto-select if None)
1415/// * `hop_size` - Hop size between frames (optional, will auto-select if None)
1416///
1417/// # Returns
1418/// JavaScript array of arrays containing dominant frequencies (Hz) for each frame
1419#[cfg(target_arch = "wasm32")]
1420#[cfg(feature = "wasm")]
1421#[wasm_bindgen]
1422pub fn extract_dominant_frequencies_from_samples_wasm(
1423    samples: &[f32],
1424    sample_rate: u32,
1425    channels: u8,
1426    num_frequencies: usize,
1427    fft_size: Option<usize>,
1428    hop_size: Option<usize>,
1429) -> Result<JsValue, JsValue> {
1430    let fingerprint =
1431        extract_spectral_fingerprint(samples, channels, sample_rate, fft_size, hop_size);
1432    let dominant_freqs = extract_dominant_frequencies_to_vec(&fingerprint, num_frequencies);
1433
1434    // Convert to JavaScript arrays
1435    let js_result = js_sys::Array::new();
1436    for frame_freqs in &dominant_freqs {
1437        let frame_array = js_sys::Float64Array::from(&frame_freqs[..]);
1438        js_result.push(&frame_array);
1439    }
1440
1441    Ok(js_result.into())
1442}
1443
1444/// Extract dominant frequencies from spectral fingerprint
1445///
1446/// # Arguments
1447/// * `fingerprint_js` - SpectralFingerprint JavaScript object
1448/// * `num_frequencies` - Number of dominant frequencies to extract per frame
1449///
1450/// # Returns
1451/// JavaScript array of arrays containing dominant frequencies (Hz) for each frame
1452#[cfg(target_arch = "wasm32")]
1453#[cfg(feature = "wasm")]
1454#[wasm_bindgen]
1455pub fn extract_dominant_frequencies_from_fingerprint_wasm(
1456    fingerprint_js: JsValue,
1457    num_frequencies: usize,
1458) -> Result<JsValue, JsValue> {
1459    use crate::core::analysis::{extract_dominant_frequencies, SpectralFingerprint};
1460
1461    let fingerprint: SpectralFingerprint = serde_wasm_bindgen::from_value(fingerprint_js)
1462        .map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
1463
1464    let dominant_freqs = extract_dominant_frequencies(&fingerprint, num_frequencies);
1465    serde_wasm_bindgen::to_value(&dominant_freqs)
1466        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1467}
1468
1469/// Compute spectral similarity between two fingerprints
1470///
1471/// # Arguments
1472/// * `fingerprint1` - First spectral fingerprint
1473/// * `fingerprint2` - Second spectral fingerprint
1474///
1475/// # Returns
1476/// Similarity score between 0.0 (completely different) and 1.0 (identical)
1477pub fn spectral_similarity_score(
1478    fingerprint1: &core::analysis::SpectralFingerprint,
1479    fingerprint2: &core::analysis::SpectralFingerprint,
1480) -> f32 {
1481    core::analysis::spectral_similarity(fingerprint1, fingerprint2)
1482}
1483
1484/// Compute spectral similarity between two fingerprints
1485///
1486/// # Arguments
1487/// * `samples1` - First audio samples
1488/// * `samples2` - Second audio samples
1489/// * `sample_rate` - Sample rate in Hz
1490/// * `channels` - Number of audio channels (1 or 2)
1491/// * `fft_size` - FFT window size (default: 2048)
1492/// * `hop_size` - Hop size between frames (default: fft_size/2)
1493///
1494/// # Returns
1495/// Similarity score between 0.0 (completely different) and 1.0 (identical)
1496#[cfg(target_arch = "wasm32")]
1497#[cfg(feature = "wasm")]
1498#[wasm_bindgen]
1499pub fn spectral_similarity(
1500    samples1: &[f32],
1501    samples2: &[f32],
1502    sample_rate: u32,
1503    channels: u8,
1504    fft_size: Option<usize>,
1505    hop_size: Option<usize>,
1506) -> Result<JsValue, JsValue> {
1507    let fingerprint1 =
1508        extract_spectral_fingerprint(samples1, channels, sample_rate, fft_size, hop_size);
1509    let fingerprint2 =
1510        extract_spectral_fingerprint(samples2, channels, sample_rate, fft_size, hop_size);
1511
1512    let similarity = spectral_similarity_score(&fingerprint1, &fingerprint2);
1513    Ok(JsValue::from_f64(similarity as f64))
1514}
1515
1516/// Compute EBU R128 loudness metrics from audio samples
1517///
1518/// # Arguments
1519/// * `samples` - Audio samples (interleaved if multi-channel)
1520/// * `channels` - Number of audio channels
1521/// * `sample_rate` - Sample rate in Hz
1522///
1523/// # Returns
1524/// LoudnessMetrics object with integrated LUFS, loudness range LU, and true peak dBTP
1525#[cfg(target_arch = "wasm32")]
1526#[cfg(feature = "wasm")]
1527#[wasm_bindgen]
1528pub fn compute_loudness_metrics(
1529    samples: &[f32],
1530    channels: u32,
1531    sample_rate: u32,
1532) -> Result<JsValue, JsValue> {
1533    use crate::core::compute_ebu_r128_loudness;
1534    let metrics = compute_ebu_r128_loudness(samples, channels as u8, sample_rate);
1535    serde_wasm_bindgen::to_value(&metrics)
1536        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1537}
1538
1539/// Extract waveform peaks from audio samples
1540///
1541/// # Arguments
1542/// * `samples` - Audio samples (interleaved if multi-channel)
1543/// * `channels` - Number of audio channels
1544/// * `sample_rate` - Sample rate in Hz
1545/// * `peaks_per_second` - Number of peak values to extract per second
1546///
1547/// # Returns
1548/// WaveformData object with extracted peaks
1549#[cfg(target_arch = "wasm32")]
1550#[cfg(feature = "wasm")]
1551#[wasm_bindgen]
1552pub fn extract_waveform_peaks_wasm(
1553    samples: &[f32],
1554    channels: u32,
1555    sample_rate: u32,
1556    peaks_per_second: Option<u32>,
1557) -> Result<JsValue, JsValue> {
1558    use crate::core::extract_waveform_peaks;
1559    let waveform = extract_waveform_peaks(
1560        samples,
1561        channels as u8,
1562        sample_rate,
1563        peaks_per_second.unwrap_or(50),
1564    );
1565    serde_wasm_bindgen::to_value(&waveform)
1566        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1567}
1568
1569// tests
1570#[cfg(test)]
1571mod tests {
1572    use super::*;
1573    use alloc::vec;
1574    use alloc::vec::Vec;
1575
1576    #[test]
1577    fn test_frame_type_conversion() {
1578        assert_eq!(FrameType::from(0), FrameType::Silence);
1579        assert_eq!(FrameType::from(8), FrameType::Alpc8);
1580        assert_eq!(FrameType::from(254), FrameType::Raw);
1581        assert!(FrameType::Alpc8.is_alpc());
1582        assert!(!FrameType::Silence.is_alpc());
1583        assert_eq!(FrameType::Alpc8.lpc_order(), Some(8));
1584    }
1585
1586    #[test]
1587    #[cfg(feature = "wasm")]
1588    fn test_version() {
1589        assert_eq!(version(), "1.2");
1590    }
1591
1592    #[test]
1593    fn test_update_metadata_preserves_audio() {
1594        // Create a simple flo file with metadata
1595        let samples: Vec<f32> = (0..4410).map(|i| ((i as f32) * 0.01).sin() * 0.5).collect();
1596        let encoder = Encoder::new(44100, 1, 16);
1597        let original_meta = b"original metadata";
1598        let flo_data = encoder.encode(&samples, original_meta).unwrap();
1599
1600        // Update metadata
1601        let new_meta = b"new metadata that is longer!";
1602        let updated = update_metadata_bytes(&flo_data, new_meta).unwrap();
1603
1604        // Verify the new file is valid
1605        let reader = Reader::new();
1606        let file = reader.read(&updated).unwrap();
1607
1608        // Check metadata was updated
1609        assert_eq!(file.metadata, new_meta);
1610        assert_eq!(file.header.meta_size, new_meta.len() as u64);
1611
1612        // Check audio data CRC is unchanged (proves audio wasn't touched)
1613        let original_file = reader.read(&flo_data).unwrap();
1614        assert_eq!(file.header.data_crc32, original_file.header.data_crc32);
1615
1616        // Decode and verify audio is identical
1617        let decoder = Decoder::new();
1618        let original_samples = decoder.decode(&flo_data).unwrap();
1619        let updated_samples = decoder.decode(&updated).unwrap();
1620        assert_eq!(original_samples, updated_samples);
1621    }
1622
1623    #[test]
1624    fn test_strip_metadata() {
1625        let samples: Vec<f32> = vec![0.5; 1000];
1626        let encoder = Encoder::new(44100, 1, 16);
1627        let flo_with_meta = encoder.encode(&samples, b"some metadata here").unwrap();
1628
1629        // Strip metadata using empty bytes (simulates strip)
1630        let stripped = update_metadata_bytes(&flo_with_meta, &[]).unwrap();
1631
1632        // Verify
1633        let reader = Reader::new();
1634        let file = reader.read(&stripped).unwrap();
1635        assert!(file.metadata.is_empty());
1636        assert_eq!(file.header.meta_size, 0);
1637
1638        // File should be smaller
1639        assert!(stripped.len() < flo_with_meta.len());
1640    }
1641
1642    #[test]
1643    fn test_has_metadata() {
1644        let samples: Vec<f32> = vec![0.5; 1000];
1645        let encoder = Encoder::new(44100, 1, 16);
1646
1647        let with_meta = encoder.encode(&samples, b"metadata").unwrap();
1648        let without_meta = encoder.encode(&samples, &[]).unwrap();
1649
1650        assert!(has_metadata(&with_meta));
1651        assert!(!has_metadata(&without_meta));
1652    }
1653}