Skip to content

Commit e72f652

Browse files
committed
better flo processing
1 parent 55378d0 commit e72f652

6 files changed

Lines changed: 323 additions & 134 deletions

File tree

Build/src/helpers/audioProcessor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import initFlo from "@flo-audio/libflo-audio";
66
export const createAudioProcessor = () => {
77
let floInitialized = false;
88
const processAudioBatch = async (songs: Song[]): Promise<Song[]> => {
9-
// Ensure FLO WASM is initialized before any decode
9+
// Ensure flo WASM is initialized before any decode
1010
if (!floInitialized) {
1111
await initFlo();
1212
floInitialized = true;
@@ -29,7 +29,7 @@ export const createAudioProcessor = () => {
2929
let mimeType = res.headers.get("content-type") || "audio/mpeg";
3030
const ext = song.url.split(".").pop()?.toLowerCase() || "";
3131

32-
// If FLO file, decode using @flo-audio/libflo-audio (placeholder)
32+
// If flo file, decode using @flo-audio/libflo-audio (placeholder)
3333
if (ext === "flo") {
3434
// TODO: Integrate @flo-audio/libflo-audio decoder here
3535
// Example:

Build/src/helpers/filePickerHelper.tsx

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -481,49 +481,57 @@ function processFiles(files: File[]): AudioFile[] {
481481
// ---------------------
482482
export async function extractAudioMetadata(file: File): Promise<AudioMetadata> {
483483
setProcessingState(true);
484+
484485
const ext = file.name.split(".").pop()?.toLowerCase();
485-
// Use FLO-specific extraction for .flo files
486+
487+
// Use flo-specific extraction for .flo files
486488
if (ext === "flo") {
487489
try {
488490
const arrayBuffer = await file.arrayBuffer();
489-
const { getFloMetadata, getFloCoverArt, getFloSyncedLyrics, getFloInfo } =
490-
await import("./floProcessor");
491+
const {
492+
getFloMetadata,
493+
getFloCoverArt,
494+
getFloSyncedLyrics,
495+
getFloInfo
496+
} = await import("./floProcessor");
497+
498+
// Get all flo data in parallel
491499
const [meta, cover, lyrics, info] = await Promise.all([
492500
getFloMetadata(arrayBuffer),
493501
getFloCoverArt(arrayBuffer),
494502
getFloSyncedLyrics(arrayBuffer),
495503
getFloInfo(arrayBuffer),
496504
]);
505+
506+
// Process album art if available
497507
let albumArt: string | undefined = undefined;
498508
if (cover && cover.data && cover.data.length > 0) {
499-
// Animated cover art is supported (webp/gif) and not compressed
500-
const blob = new Blob([new Uint8Array(cover.data)], {
501-
type: cover.mime_type,
502-
});
509+
const blob = new Blob([cover.data], { type: cover.mime_type });
503510
albumArt = await new Promise<string>((resolve, reject) => {
504511
const reader = new FileReader();
505512
reader.onloadend = () => resolve(reader.result as string);
506513
reader.onerror = () => reject(new Error("Failed to read album art"));
507514
reader.readAsDataURL(blob);
508515
});
509516
}
510-
// Lyrics: convert to EmbeddedLyrics[]
511-
let embeddedLyrics = undefined;
512-
if (Array.isArray(lyrics)) {
517+
518+
// Convert synced lyrics to EmbeddedLyrics format
519+
let embeddedLyrics: EmbeddedLyrics[] | undefined = undefined;
520+
if (Array.isArray(lyrics) && lyrics.length > 0) {
513521
embeddedLyrics = [
514522
{
515523
synced: true,
516524
lines: lyrics.map((l) => ({
517525
text: l.text,
518-
timestamp: l.timestamp_ms,
526+
timestamp: l.timestamp_ms / 1000, // Convert ms to seconds
519527
})),
520528
},
521529
];
522530
}
523-
// Info: duration, encoding, etc
524-
const duration = info?.duration_secs ?? 0;
531+
532+
// Build encoding details from info
525533
const encoding: EncodingDetails = {
526-
bitrate: undefined,
534+
bitrate: undefined, // flo doesn't provide bitrate directly
527535
codec: "flo",
528536
sampleRate: info?.sample_rate,
529537
channels: info?.channels,
@@ -532,17 +540,34 @@ export async function extractAudioMetadata(file: File): Promise<AudioMetadata> {
532540
lossless: !info?.is_lossy,
533541
profile: info?.is_lossy ? "lossy" : "lossless",
534542
};
543+
535544
return {
536545
title: meta?.title || file.name.replace(/\.[^/.]+$/, ""),
537546
artist: meta?.artist || i18n.t("common.unknownArtist"),
538547
album: meta?.album || i18n.t("common.unknownAlbum"),
539-
duration,
548+
duration: info?.duration_secs || 0,
540549
albumArt,
541550
embeddedLyrics,
542551
encoding,
543-
gapless: undefined, // TODO: add if FLO supports gapless info
552+
gapless: undefined, // flo doesn't expose gapless info currently
544553
metadataWarnings: undefined,
545554
};
555+
} catch (error) {
556+
console.error("Failed to extract flo metadata:", error);
557+
// Fallback to basic file info
558+
return {
559+
title: file.name.replace(/\.[^/.]+$/, ""),
560+
artist: i18n.t("common.unknownArtist"),
561+
album: i18n.t("common.unknownAlbum"),
562+
duration: 0,
563+
albumArt: undefined,
564+
embeddedLyrics: undefined,
565+
encoding: {
566+
codec: "flo",
567+
container: "flo",
568+
},
569+
gapless: undefined,
570+
};
546571
} finally {
547572
setProcessingState(false);
548573
}

Build/src/helpers/floProcessor.ts

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,88 @@
1-
import * as flo from "@flo-audio/libflo-audio";
2-
import initFlo from "@flo-audio/libflo-audio";
1+
// floProcessor.ts - Fixed flo audio processing
2+
import initFlo, * as flo from "@flo-audio/libflo-audio";
33

44
let floInitialized = false;
5+
56
async function ensureFloInitialized() {
67
if (!floInitialized) {
78
await initFlo();
89
floInitialized = true;
910
}
1011
}
1112

12-
// Decode a .flo file buffer to AudioBuffer using @flo-audio/libflo
13+
// Decode a .flo file buffer to AudioBuffer using @flo-audio/libflo-audio
1314
export async function decodeFloToAudioBuffer(
1415
floData: ArrayBuffer,
1516
audioContext: AudioContext,
1617
): Promise<AudioBuffer> {
1718
await ensureFloInitialized();
19+
1820
const uint8Flo = new Uint8Array(floData);
21+
22+
// Decode flo to interleaved Float32Array samples
1923
const samples = flo.decode(uint8Flo);
24+
25+
// Get file info for audio properties
2026
const fileInfo = flo.info(uint8Flo);
2127
const { channels, sample_rate } = fileInfo;
22-
const length = samples.length / channels;
23-
const audioBuffer = audioContext.createBuffer(channels, length, sample_rate);
24-
// Deinterleave
28+
29+
// Calculate number of frames
30+
const frameCount = samples.length / channels;
31+
32+
// Create AudioBuffer
33+
const audioBuffer = audioContext.createBuffer(
34+
channels,
35+
frameCount,
36+
sample_rate
37+
);
38+
39+
// Deinterleave samples into separate channels
2540
for (let ch = 0; ch < channels; ch++) {
2641
const channelData = audioBuffer.getChannelData(ch);
27-
for (let i = 0; i < length; i++) {
42+
for (let i = 0; i < frameCount; i++) {
2843
channelData[i] = samples[i * channels + ch];
2944
}
3045
}
46+
3147
return audioBuffer;
3248
}
3349

34-
// Get FLO file info (sample rate, channels, bit depth, duration, etc)
35-
export async function getFloInfo(floData: ArrayBuffer): Promise<any> {
50+
// Get flo file info (returns object with sample_rate, channels, bit_depth, etc.)
51+
export async function getFloInfo(floData: ArrayBuffer): Promise<{
52+
sample_rate: number;
53+
channels: number;
54+
bit_depth: number;
55+
duration_secs: number;
56+
is_lossy: boolean;
57+
compression_ratio?: number;
58+
}> {
3659
await ensureFloInitialized();
3760
const uint8Flo = new Uint8Array(floData);
3861
return flo.info(uint8Flo);
3962
}
4063

41-
// Validate FLO file integrity (CRC32)
64+
// Validate flo file integrity (CRC32)
4265
export async function validateFlo(floData: ArrayBuffer): Promise<boolean> {
4366
await ensureFloInitialized();
4467
const uint8Flo = new Uint8Array(floData);
4568
return flo.validate(uint8Flo);
4669
}
4770

48-
// Extract metadata as JS object (title, artist, album, etc)
71+
// Extract metadata as JS object (returns null if no metadata)
4972
export async function getFloMetadata(
5073
floData: ArrayBuffer,
51-
): Promise<any | null> {
74+
): Promise<{
75+
title?: string;
76+
artist?: string;
77+
album?: string;
78+
[key: string]: any;
79+
} | null> {
5280
await ensureFloInitialized();
5381
const uint8Flo = new Uint8Array(floData);
5482
return flo.get_metadata(uint8Flo);
5583
}
5684

57-
// Extract cover art (returns { mime_type, data })
85+
// Extract cover art (returns { mime_type, data } or null)
5886
export async function getFloCoverArt(
5987
floData: ArrayBuffer,
6088
): Promise<{ mime_type: string; data: Uint8Array } | null> {
@@ -63,7 +91,7 @@ export async function getFloCoverArt(
6391
return flo.get_cover_art(uint8Flo);
6492
}
6593

66-
// Extract synchronized lyrics (returns array or null)
94+
// Extract synchronized lyrics (returns array of { timestamp_ms, text } or null)
6795
export async function getFloSyncedLyrics(
6896
floData: ArrayBuffer,
6997
): Promise<Array<{ timestamp_ms: number; text: string }> | null> {
@@ -72,10 +100,13 @@ export async function getFloSyncedLyrics(
72100
return flo.get_synced_lyrics(uint8Flo);
73101
}
74102

75-
// Create FLO metadata from JS object (returns Uint8Array)
103+
// Create flo metadata from JS object (returns Uint8Array)
76104
export async function createFloMetadataFromObject(
77105
obj: any,
78106
): Promise<Uint8Array> {
79107
await ensureFloInitialized();
80108
return flo.create_metadata_from_object(obj);
81109
}
110+
111+
// Export initialization function
112+
export { initFlo, ensureFloInitialized };

0 commit comments

Comments
 (0)