import { createAudioPlayer, requestRecordingPermissionsAsync, setAudioModeAsync, } from "expo-audio"; import * as LegacyFileSystem from "expo-file-system/legacy"; export interface RecordingResult { uri: string; durationMs: number; } let currentPlayer: ReturnType | null = null; const playingListeners = new Set<(playing: boolean) => void>(); // Audio queue for chaining sequential voice notes const audioQueue: Array<{ uri: string; onFinish?: () => void }> = []; let processingQueue = false; function notifyListeners(playing: boolean): void { for (const cb of playingListeners) cb(playing); } export function onPlayingChange(cb: (playing: boolean) => void): () => void { playingListeners.add(cb); return () => { playingListeners.delete(cb); }; } export async function requestPermissions(): Promise { const { status } = await requestRecordingPermissionsAsync(); return status === "granted"; } let audioCounter = 0; /** * Convert a base64 audio string to a file URI. */ export async function saveBase64Audio(base64: string, ext = "m4a"): Promise { const tmpPath = `${LegacyFileSystem.cacheDirectory}pailot-voice-${++audioCounter}.${ext}`; await LegacyFileSystem.writeAsStringAsync(tmpPath, base64, { encoding: LegacyFileSystem.EncodingType.Base64, }); return tmpPath; } /** * Queue audio for playback. Multiple calls chain sequentially — * the next voice note plays only after the current one finishes. */ export async function playAudio( uri: string, onFinish?: () => void ): Promise { audioQueue.push({ uri, onFinish }); if (!processingQueue) { processAudioQueue(); } } async function processAudioQueue(): Promise { if (processingQueue) return; processingQueue = true; while (audioQueue.length > 0) { const item = audioQueue.shift()!; await playOneAudio(item.uri, item.onFinish); } processingQueue = false; } function playOneAudio(uri: string, onFinish?: () => void): Promise { return new Promise(async (resolve) => { try { await setAudioModeAsync({ playsInSilentMode: true }); const player = createAudioPlayer(uri); currentPlayer = player; notifyListeners(true); player.addListener("playbackStatusUpdate", (status) => { if (!status.playing && status.currentTime >= status.duration && status.duration > 0) { onFinish?.(); player.remove(); if (currentPlayer === player) { currentPlayer = null; if (audioQueue.length === 0) notifyListeners(false); } resolve(); } }); player.play(); } catch (error) { console.error("Failed to play audio:", error); resolve(); } }); } export function isPlaying(): boolean { return currentPlayer !== null; } /** * Stop current playback and clear the queue. */ export async function stopPlayback(): Promise { audioQueue.length = 0; if (currentPlayer) { try { currentPlayer.pause(); currentPlayer.remove(); } catch { // Ignore cleanup errors } currentPlayer = null; notifyListeners(false); } } export async function encodeAudioToBase64(uri: string): Promise { const result = await LegacyFileSystem.readAsStringAsync(uri, { encoding: LegacyFileSystem.EncodingType.Base64, }); return result; }