From af1543135d42adc2e97dc5243aeef7418cd3b00d Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 08:39:26 +0100
Subject: [PATCH] feat: dual address auto-switch, custom icon, notifications, image support
---
services/audio.ts | 111 +++++++++++++++++++++++++++++++++++++++++++------------
1 files changed, 87 insertions(+), 24 deletions(-)
diff --git a/services/audio.ts b/services/audio.ts
index 769d299..5fa8bd5 100644
--- a/services/audio.ts
+++ b/services/audio.ts
@@ -3,6 +3,7 @@
requestRecordingPermissionsAsync,
setAudioModeAsync,
} from "expo-audio";
+import * as LegacyFileSystem from "expo-file-system/legacy";
export interface RecordingResult {
uri: string;
@@ -10,41 +11,103 @@
}
let currentPlayer: ReturnType<typeof createAudioPlayer> | 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<boolean> {
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<string> {
+ 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<void> {
- try {
- await stopPlayback();
-
- await setAudioModeAsync({
- playsInSilentMode: true,
- });
-
- const player = createAudioPlayer(uri);
- currentPlayer = player;
-
- player.addListener("playbackStatusUpdate", (status) => {
- if (!status.playing && status.currentTime >= status.duration && status.duration > 0) {
- onFinish?.();
- player.remove();
- if (currentPlayer === player) currentPlayer = null;
- }
- });
-
- player.play();
- } catch (error) {
- console.error("Failed to play audio:", error);
+ audioQueue.push({ uri, onFinish });
+ if (!processingQueue) {
+ processAudioQueue();
}
}
+async function processAudioQueue(): Promise<void> {
+ 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<void> {
+ return new Promise<void>(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<void> {
+ audioQueue.length = 0;
if (currentPlayer) {
try {
currentPlayer.pause();
@@ -53,13 +116,13 @@
// Ignore cleanup errors
}
currentPlayer = null;
+ notifyListeners(false);
}
}
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,
+ const result = await LegacyFileSystem.readAsStringAsync(uri, {
+ encoding: LegacyFileSystem.EncodingType.Base64,
});
return result;
}
--
Gitblit v1.3.1