From a0f39302919fbacf7a0d407f01b1a50413ea6f70 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Mon, 02 Mar 2026 23:15:13 +0100
Subject: [PATCH] feat: on-device speech recognition, navigation screen, session picker

---
 services/audio.ts |  105 ++++++++++++++--------------------------------------
 1 files changed, 29 insertions(+), 76 deletions(-)

diff --git a/services/audio.ts b/services/audio.ts
index 31c7dd8..769d299 100644
--- a/services/audio.ts
+++ b/services/audio.ts
@@ -1,112 +1,65 @@
-import { Audio, AVPlaybackStatus } from "expo-av";
+import {
+  createAudioPlayer,
+  requestRecordingPermissionsAsync,
+  setAudioModeAsync,
+} from "expo-audio";
 
 export interface RecordingResult {
   uri: string;
   durationMs: number;
 }
 
-let currentRecording: Audio.Recording | null = null;
-let currentSound: Audio.Sound | null = null;
+let currentPlayer: ReturnType<typeof createAudioPlayer> | null = null;
 
-async function requestPermissions(): Promise<boolean> {
-  const { status } = await Audio.requestPermissionsAsync();
+export async function requestPermissions(): Promise<boolean> {
+  const { status } = await requestRecordingPermissionsAsync();
   return status === "granted";
-}
-
-export async function startRecording(): Promise<Audio.Recording | null> {
-  const granted = await requestPermissions();
-  if (!granted) return null;
-
-  try {
-    await Audio.setAudioModeAsync({
-      allowsRecordingIOS: true,
-      playsInSilentModeIOS: true,
-    });
-
-    const { recording } = await Audio.Recording.createAsync(
-      Audio.RecordingOptionsPresets.HIGH_QUALITY
-    );
-
-    currentRecording = recording;
-    return recording;
-  } catch (error) {
-    console.error("Failed to start recording:", error);
-    return null;
-  }
-}
-
-export async function stopRecording(): Promise<RecordingResult | null> {
-  if (!currentRecording) return null;
-
-  try {
-    await currentRecording.stopAndUnloadAsync();
-    const status = await currentRecording.getStatusAsync();
-    const uri = currentRecording.getURI();
-    currentRecording = null;
-
-    await Audio.setAudioModeAsync({
-      allowsRecordingIOS: false,
-    });
-
-    if (!uri) return null;
-
-    const durationMs = (status as { durationMillis?: number }).durationMillis ?? 0;
-    return { uri, durationMs };
-  } catch (error) {
-    console.error("Failed to stop recording:", error);
-    currentRecording = null;
-    return null;
-  }
 }
 
 export async function playAudio(
   uri: string,
   onFinish?: () => void
-): Promise<Audio.Sound | null> {
+): Promise<void> {
   try {
     await stopPlayback();
 
-    await Audio.setAudioModeAsync({
-      allowsRecordingIOS: false,
-      playsInSilentModeIOS: true,
+    await setAudioModeAsync({
+      playsInSilentMode: true,
     });
 
-    const { sound } = await Audio.Sound.createAsync(
-      { uri },
-      { shouldPlay: true }
-    );
+    const player = createAudioPlayer(uri);
+    currentPlayer = player;
 
-    currentSound = sound;
-
-    sound.setOnPlaybackStatusUpdate((status: AVPlaybackStatus) => {
-      if (status.isLoaded && status.didJustFinish) {
+    player.addListener("playbackStatusUpdate", (status) => {
+      if (!status.playing && status.currentTime >= status.duration && status.duration > 0) {
         onFinish?.();
-        sound.unloadAsync().catch(() => {});
-        currentSound = null;
+        player.remove();
+        if (currentPlayer === player) currentPlayer = null;
       }
     });
 
-    return sound;
+    player.play();
   } catch (error) {
     console.error("Failed to play audio:", error);
-    return null;
   }
 }
 
 export async function stopPlayback(): Promise<void> {
-  if (currentSound) {
+  if (currentPlayer) {
     try {
-      await currentSound.stopAsync();
-      await currentSound.unloadAsync();
+      currentPlayer.pause();
+      currentPlayer.remove();
     } catch {
-      // Ignore errors during cleanup
+      // Ignore cleanup errors
     }
-    currentSound = null;
+    currentPlayer = null;
   }
 }
 
-export function encodeAudioToBase64(uri: string): Promise<string> {
-  // In React Native, we'd use FileSystem from expo-file-system
-  // For now, return the URI as-is since we may not have expo-file-system
-  return Promise.resolve(uri);
+export async function encodeAudioToBase64(uri: string): Promise<string> {
+  const FileSystem = await import("expo-file-system");
+  const result = await FileSystem.readAsStringAsync(uri, {
+    encoding: FileSystem.EncodingType.Base64,
+  });
+  return result;
 }

--
Gitblit v1.3.1