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#[cfg(feature = "wasm")]
51#[wasm_bindgen]
52#[derive(Debug, Clone)]
53pub struct AudioInfo {
54 #[wasm_bindgen(skip)]
56 pub version: String,
57 pub sample_rate: u32,
59 pub channels: u8,
61 pub bit_depth: u8,
63 pub total_samples: u64,
67 pub duration_secs: f64,
69 pub file_size: usize,
71 pub compression_ratio: f64,
73 pub crc_valid: bool,
75 pub is_lossy: bool,
77 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#[cfg(feature = "wasm")]
94fn to_js_err(e: impl ::core::fmt::Display) -> JsValue {
95 JsValue::from_str(&e.to_string())
96}
97
98#[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 let metadata_with_waveform = add_analysis_data_if_missing(
128 &metadata.unwrap_or_default(),
129 samples,
130 sample_rate,
131 channels,
132 50, )
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#[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 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 let metadata_with_waveform = add_analysis_data_if_missing(
181 &metadata.unwrap_or_default(),
182 samples,
183 sample_rate,
184 channels,
185 50, )
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#[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 let quality =
221 lossy::QualityPreset::from_bitrate(target_bitrate_kbps, sample_rate, channels).as_f32();
222
223 let metadata_with_waveform = add_analysis_data_if_missing(
225 &metadata.unwrap_or_default(),
226 samples,
227 sample_rate,
228 channels,
229 50, )
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#[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 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 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 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), Some(1024), );
307
308 if let Ok(spectral_bytes) = messagepack_serde::to_vec(&spectral_fingerprint) {
310 flo_metadata.spectrum_fingerprint = Some(spectral_bytes);
311 }
312 }
313
314 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 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 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 to_vec(&flo_metadata).map_err(|e| format!("Failed to serialize metadata: {}", e))
337}
338
339#[cfg(feature = "wasm")]
350#[wasm_bindgen]
351pub fn decode(data: &[u8]) -> Result<Vec<f32>, JsValue> {
352 let reader = Reader::new();
354 let file = reader.read(data).map_err(to_js_err)?;
355
356 let is_transform = file
358 .frames
359 .iter()
360 .any(|f| f.frame_type == (FrameType::Transform as u8));
361
362 if is_transform {
363 decode_transform_file(&file).map_err(to_js_err)
365 } else {
366 let decoder = Decoder::new();
368 decoder.decode_file(&file).map_err(to_js_err)
369 }
370}
371
372#[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 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 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#[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#[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 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 (file.header.total_samples as f64) / (file.header.sample_rate as f64)
459 });
460
461 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 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 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#[cfg(feature = "wasm")]
504#[wasm_bindgen]
505pub fn version() -> String {
506 format!("{}.{}", VERSION_MAJOR, VERSION_MINOR)
507}
508
509#[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#[cfg(feature = "wasm")]
531#[wasm_bindgen]
532pub fn format_time_ms(milliseconds: f64) -> String {
533 format_time(milliseconds / 1000.0)
534}
535
536#[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#[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#[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#[cfg(feature = "wasm")]
611#[wasm_bindgen]
612pub struct WasmStreamingDecoder {
613 inner: StreamingDecoder,
614}
615
616#[cfg(feature = "wasm")]
617#[wasm_bindgen]
618impl WasmStreamingDecoder {
619 #[wasm_bindgen(constructor)]
621 pub fn new() -> Self {
622 Self {
623 inner: StreamingDecoder::new(),
624 }
625 }
626
627 #[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 #[cfg(feature = "wasm")]
636 #[wasm_bindgen]
637 pub fn is_ready(&self) -> bool {
638 self.inner.state() == DecoderState::Ready
639 }
640
641 #[cfg(feature = "wasm")]
643 #[wasm_bindgen]
644 pub fn is_finished(&self) -> bool {
645 self.inner.state() == DecoderState::Finished
646 }
647
648 #[cfg(feature = "wasm")]
650 #[wasm_bindgen]
651 pub fn has_error(&self) -> bool {
652 self.inner.state() == DecoderState::Error
653 }
654
655 #[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 #[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 #[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 #[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 #[cfg(feature = "wasm")]
728 #[wasm_bindgen]
729 pub fn available_frames(&self) -> usize {
730 self.inner.available_frames()
731 }
732
733 #[cfg(feature = "wasm")]
735 #[wasm_bindgen]
736 pub fn current_frame_index(&self) -> usize {
737 self.inner.current_frame_index()
738 }
739
740 #[cfg(feature = "wasm")]
744 #[wasm_bindgen]
745 pub fn reset(&mut self) {
746 self.inner.reset();
747 }
748
749 #[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#[cfg(feature = "wasm")]
767#[wasm_bindgen]
768pub struct WasmStreamingEncoder {
769 inner: StreamingEncoder,
770}
771
772#[cfg(feature = "wasm")]
773#[wasm_bindgen]
774impl WasmStreamingEncoder {
775 #[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 #[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 #[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 #[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 #[cfg(feature = "wasm")]
854 #[wasm_bindgen]
855 pub fn pending_samples(&self) -> usize {
856 self.inner.pending_samples()
857 }
858
859 #[cfg(feature = "wasm")]
864 #[wasm_bindgen]
865 pub fn pending_frames(&self) -> usize {
866 self.inner.pending_frames()
867 }
868
869 #[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 #[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#[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#[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#[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#[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 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#[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 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 let mut obj: serde_json::Value = serde_json::to_value(&meta)
1037 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
1038
1039 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 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 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 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#[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#[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#[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#[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
1162pub fn update_metadata_bytes(flo_data: &[u8], new_metadata: &[u8]) -> FloResult<Vec<u8>> {
1164 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 if flo_data[0..4] != MAGIC {
1173 return Err(crate::core::FloError::from("Invalid flo file: bad magic"));
1174 }
1175
1176 let reader = Reader::new();
1178 let file = reader.read(flo_data)?;
1179
1180 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 let mut result = Vec::with_capacity(meta_offset + new_metadata.len());
1190 result.extend_from_slice(&flo_data[..meta_offset]);
1191
1192 result.extend_from_slice(new_metadata);
1194
1195 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#[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
1222pub fn strip_metadata_bytes(flo_data: &[u8]) -> FloResult<Vec<u8>> {
1230 update_metadata_bytes(flo_data, &[])
1231}
1232
1233#[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#[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
1259pub 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 let reader = Reader::new();
1273 let file = reader.read(flo_data)?;
1274
1275 Ok(file.metadata)
1276}
1277
1278#[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
1285pub fn has_metadata(flo_data: &[u8]) -> bool {
1287 if flo_data.len() < (HEADER_SIZE as usize) {
1288 return false;
1289 }
1290
1291 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
1306pub 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
1326pub 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#[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#[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
1392pub 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#[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 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#[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
1469pub 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#[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#[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#[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#[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 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 let new_meta = b"new metadata that is longer!";
1602 let updated = update_metadata_bytes(&flo_data, new_meta).unwrap();
1603
1604 let reader = Reader::new();
1606 let file = reader.read(&updated).unwrap();
1607
1608 assert_eq!(file.metadata, new_meta);
1610 assert_eq!(file.header.meta_size, new_meta.len() as u64);
1611
1612 let original_file = reader.read(&flo_data).unwrap();
1614 assert_eq!(file.header.data_crc32, original_file.header.data_crc32);
1615
1616 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 let stripped = update_metadata_bytes(&flo_with_meta, &[]).unwrap();
1631
1632 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 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}