first commit
This commit is contained in:
167
ui/app/workspace/logs/views/audioPlayer.tsx
Normal file
167
ui/app/workspace/logs/views/audioPlayer.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Pause, Play, Download } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface AudioPlayerProps {
|
||||
src: string;
|
||||
format?: string; // Optional format: "mp3", "wav", "pcm16", etc.
|
||||
}
|
||||
|
||||
const AudioPlayer = ({ src, format }: AudioPlayerProps) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [audio] = useState<HTMLAudioElement | null>(typeof window !== "undefined" ? new Audio() : null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Convert PCM16 to WAV format
|
||||
const convertPCM16ToWAV = (pcmData: Uint8Array, sampleRate: number = 24000, numChannels: number = 1): Uint8Array => {
|
||||
const bitsPerSample = 16;
|
||||
const byteRate = (sampleRate * numChannels * bitsPerSample) / 8;
|
||||
const blockAlign = (numChannels * bitsPerSample) / 8;
|
||||
const dataSize = pcmData.length;
|
||||
const fileSize = 36 + dataSize;
|
||||
|
||||
const wavBuffer = new ArrayBuffer(44 + dataSize);
|
||||
const view = new DataView(wavBuffer);
|
||||
|
||||
// RIFF header
|
||||
const writeString = (offset: number, string: string) => {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
};
|
||||
|
||||
writeString(0, "RIFF");
|
||||
view.setUint32(4, fileSize, true);
|
||||
writeString(8, "WAVE");
|
||||
|
||||
// fmt subchunk
|
||||
writeString(12, "fmt ");
|
||||
view.setUint32(16, 16, true); // Subchunk1Size
|
||||
view.setUint16(20, 1, true); // AudioFormat (1 = PCM)
|
||||
view.setUint16(22, numChannels, true); // NumChannels
|
||||
view.setUint32(24, sampleRate, true); // SampleRate
|
||||
view.setUint32(28, byteRate, true); // ByteRate
|
||||
view.setUint16(32, blockAlign, true); // BlockAlign
|
||||
view.setUint16(34, bitsPerSample, true); // BitsPerSample
|
||||
|
||||
// data subchunk
|
||||
writeString(36, "data");
|
||||
view.setUint32(40, dataSize, true);
|
||||
|
||||
// Copy PCM data
|
||||
const wavArray = new Uint8Array(wavBuffer);
|
||||
wavArray.set(pcmData, 44);
|
||||
|
||||
return wavArray;
|
||||
};
|
||||
|
||||
const createAudioBlob = (base64Data: string, audioFormat?: string): Blob | null => {
|
||||
try {
|
||||
const binaryString = atob(base64Data);
|
||||
const pcmData = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
|
||||
|
||||
// Handle PCM16 format - convert to WAV
|
||||
if (audioFormat === "pcm16" || audioFormat === "pcm_s16le_16") {
|
||||
const wavData = convertPCM16ToWAV(pcmData);
|
||||
// Create a new ArrayBuffer to ensure proper type
|
||||
const buffer = new ArrayBuffer(wavData.length);
|
||||
new Uint8Array(buffer).set(wavData);
|
||||
return new Blob([buffer], {
|
||||
type: "audio/wav",
|
||||
});
|
||||
}
|
||||
|
||||
// Handle other formats
|
||||
let mimeType = "audio/mpeg"; // Default to MP3
|
||||
if (audioFormat === "wav") {
|
||||
mimeType = "audio/wav";
|
||||
} else if (audioFormat === "ogg") {
|
||||
mimeType = "audio/ogg";
|
||||
} else if (audioFormat === "webm") {
|
||||
mimeType = "audio/webm";
|
||||
}
|
||||
|
||||
return new Blob([pcmData], {
|
||||
type: mimeType,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to decode audio data:", err);
|
||||
setError("Failed to decode audio data. The audio file may be corrupted.");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlayPause = () => {
|
||||
if (!audio || !src) return;
|
||||
|
||||
if (isPlaying) {
|
||||
audio.pause();
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
const audioBlob = createAudioBlob(src, format);
|
||||
if (!audioBlob) return;
|
||||
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
audio.src = audioUrl;
|
||||
audio.play().catch((err) => {
|
||||
console.error("Failed to play audio:", err);
|
||||
setError("Failed to play audio. Please try again.");
|
||||
setIsPlaying(false);
|
||||
});
|
||||
setIsPlaying(true);
|
||||
|
||||
audio.onended = () => {
|
||||
setIsPlaying(false);
|
||||
URL.revokeObjectURL(audioUrl);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!src) return;
|
||||
|
||||
const audioBlob = createAudioBlob(src, format);
|
||||
if (!audioBlob) return;
|
||||
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
|
||||
// Determine file extension based on format
|
||||
let extension = "mp3";
|
||||
if (format === "pcm16" || format === "pcm_s16le_16") {
|
||||
extension = "wav";
|
||||
} else if (format === "wav") {
|
||||
extension = "wav";
|
||||
} else if (format === "ogg") {
|
||||
extension = "ogg";
|
||||
} else if (format === "webm") {
|
||||
extension = "webm";
|
||||
}
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = audioUrl;
|
||||
a.download = `speech-output.${extension}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(audioUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={handlePlayPause} variant="outline" size="sm" className="flex items-center gap-2" disabled={!!error}>
|
||||
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||||
{isPlaying ? "Pause" : "Play"}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleDownload} variant="outline" size="sm" className="flex items-center gap-2" disabled={!!error}>
|
||||
<Download className="h-4 w-4" />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
{error && <div className="text-sm text-red-500">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioPlayer;
|
||||
Reference in New Issue
Block a user