Matthias Nott
2026-03-07 af1543135d42adc2e97dc5243aeef7418cd3b00d
services/audio.ts
....@@ -3,6 +3,7 @@
33 requestRecordingPermissionsAsync,
44 setAudioModeAsync,
55 } from "expo-audio";
6
+import * as LegacyFileSystem from "expo-file-system/legacy";
67
78 export interface RecordingResult {
89 uri: string;
....@@ -10,41 +11,103 @@
1011 }
1112
1213 let currentPlayer: ReturnType<typeof createAudioPlayer> | null = null;
14
+const playingListeners = new Set<(playing: boolean) => void>();
15
+
16
+// Audio queue for chaining sequential voice notes
17
+const audioQueue: Array<{ uri: string; onFinish?: () => void }> = [];
18
+let processingQueue = false;
19
+
20
+function notifyListeners(playing: boolean): void {
21
+ for (const cb of playingListeners) cb(playing);
22
+}
23
+
24
+export function onPlayingChange(cb: (playing: boolean) => void): () => void {
25
+ playingListeners.add(cb);
26
+ return () => { playingListeners.delete(cb); };
27
+}
1328
1429 export async function requestPermissions(): Promise<boolean> {
1530 const { status } = await requestRecordingPermissionsAsync();
1631 return status === "granted";
1732 }
1833
34
+let audioCounter = 0;
35
+
36
+/**
37
+ * Convert a base64 audio string to a file URI.
38
+ */
39
+export async function saveBase64Audio(base64: string, ext = "m4a"): Promise<string> {
40
+ const tmpPath = `${LegacyFileSystem.cacheDirectory}pailot-voice-${++audioCounter}.${ext}`;
41
+ await LegacyFileSystem.writeAsStringAsync(tmpPath, base64, {
42
+ encoding: LegacyFileSystem.EncodingType.Base64,
43
+ });
44
+ return tmpPath;
45
+}
46
+
47
+/**
48
+ * Queue audio for playback. Multiple calls chain sequentially —
49
+ * the next voice note plays only after the current one finishes.
50
+ */
1951 export async function playAudio(
2052 uri: string,
2153 onFinish?: () => void
2254 ): Promise<void> {
23
- try {
24
- await stopPlayback();
25
-
26
- await setAudioModeAsync({
27
- playsInSilentMode: true,
28
- });
29
-
30
- const player = createAudioPlayer(uri);
31
- currentPlayer = player;
32
-
33
- player.addListener("playbackStatusUpdate", (status) => {
34
- if (!status.playing && status.currentTime >= status.duration && status.duration > 0) {
35
- onFinish?.();
36
- player.remove();
37
- if (currentPlayer === player) currentPlayer = null;
38
- }
39
- });
40
-
41
- player.play();
42
- } catch (error) {
43
- console.error("Failed to play audio:", error);
55
+ audioQueue.push({ uri, onFinish });
56
+ if (!processingQueue) {
57
+ processAudioQueue();
4458 }
4559 }
4660
61
+async function processAudioQueue(): Promise<void> {
62
+ if (processingQueue) return;
63
+ processingQueue = true;
64
+
65
+ while (audioQueue.length > 0) {
66
+ const item = audioQueue.shift()!;
67
+ await playOneAudio(item.uri, item.onFinish);
68
+ }
69
+
70
+ processingQueue = false;
71
+}
72
+
73
+function playOneAudio(uri: string, onFinish?: () => void): Promise<void> {
74
+ return new Promise<void>(async (resolve) => {
75
+ try {
76
+ await setAudioModeAsync({ playsInSilentMode: true });
77
+
78
+ const player = createAudioPlayer(uri);
79
+ currentPlayer = player;
80
+ notifyListeners(true);
81
+
82
+ player.addListener("playbackStatusUpdate", (status) => {
83
+ if (!status.playing && status.currentTime >= status.duration && status.duration > 0) {
84
+ onFinish?.();
85
+ player.remove();
86
+ if (currentPlayer === player) {
87
+ currentPlayer = null;
88
+ if (audioQueue.length === 0) notifyListeners(false);
89
+ }
90
+ resolve();
91
+ }
92
+ });
93
+
94
+ player.play();
95
+ } catch (error) {
96
+ console.error("Failed to play audio:", error);
97
+ resolve();
98
+ }
99
+ });
100
+}
101
+
102
+export function isPlaying(): boolean {
103
+ return currentPlayer !== null;
104
+}
105
+
106
+/**
107
+ * Stop current playback and clear the queue.
108
+ */
47109 export async function stopPlayback(): Promise<void> {
110
+ audioQueue.length = 0;
48111 if (currentPlayer) {
49112 try {
50113 currentPlayer.pause();
....@@ -53,13 +116,13 @@
53116 // Ignore cleanup errors
54117 }
55118 currentPlayer = null;
119
+ notifyListeners(false);
56120 }
57121 }
58122
59123 export async function encodeAudioToBase64(uri: string): Promise<string> {
60
- const FileSystem = await import("expo-file-system");
61
- const result = await FileSystem.readAsStringAsync(uri, {
62
- encoding: FileSystem.EncodingType.Base64,
124
+ const result = await LegacyFileSystem.readAsStringAsync(uri, {
125
+ encoding: LegacyFileSystem.EncodingType.Base64,
63126 });
64127 return result;
65128 }