@siteed/expo-audio-stream 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import { RecordingOptions } from "./ExpoAudioStream.types";
1
+ import { AudioStreamResult, RecordingOptions } from "./ExpoAudioStream.types";
2
2
  interface UseAudioRecorderState {
3
- startRecording: (_: RecordingOptions) => Promise<void>;
4
- stopRecording: () => Promise<number>;
3
+ startRecording: (_: RecordingOptions) => Promise<string | null>;
4
+ stopRecording: () => Promise<AudioStreamResult | null>;
5
5
  pauseRecording: () => void;
6
6
  isRecording: boolean;
7
7
  isPaused: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"useAudioRecording.d.ts","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"AAIA,OAAO,EAAwC,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAOjG,UAAU,qBAAqB;IAC3B,cAAc,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,aAAa,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,gBAAgB,CAAC,EAAC,aAAa,EAAC,EAAE;IAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAA;CAAC,GAAG,qBAAqB,CAuHjH"}
1
+ {"version":3,"file":"useAudioRecording.d.ts","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,iBAAiB,EAAqB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAUpH,UAAU,qBAAqB;IAC3B,cAAc,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChE,aAAa,EAAE,MAAM,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACvD,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CAChB;AAGD,wBAAgB,gBAAgB,CAAC,EAAC,aAAa,EAAC,EAAE;IAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAA;CAAC,GAAG,qBAAqB,CA4GjH"}
@@ -1,10 +1,11 @@
1
1
  import { NativeModulesProxy, EventEmitter, Platform } from 'expo-modules-core';
2
2
  import { useCallback, useEffect, useState } from "react";
3
3
  import ExpoAudioStreamModule from './ExpoAudioStreamModule';
4
- import { addChangeListener } from '.';
5
- import * as FileSystem from 'expo-file-system';
4
+ import { addAudioEventListener } from '.';
6
5
  import { decode as atob } from 'base-64';
6
+ import debug from 'debug';
7
7
  const emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);
8
+ const log = debug("expo-audio-stream:useAudioRecording");
8
9
  export function useAudioRecorder({ onAudioStream }) {
9
10
  const [isRecording, setIsRecording] = useState(false);
10
11
  const [isPaused, setIsPaused] = useState(false);
@@ -22,27 +23,35 @@ export function useAudioRecorder({ onAudioStream }) {
22
23
  return () => null;
23
24
  }, [isRecording, isPaused]);
24
25
  useEffect(() => {
25
- const subscribe = addChangeListener(async ({ fileUri, deltaSize, totalSize, from, streamUuid, encoded, buffer }) => {
26
- console.debug(`Received audio event:`, { fileUri, deltaSize, totalSize, from, streamUuid, encodedLength: encoded?.length });
26
+ const subscribe = addAudioEventListener(async ({ fileUri, deltaSize, totalSize, from, streamUuid, encoded, mimeType, buffer }) => {
27
+ log(`Received audio event:`, { fileUri, deltaSize, totalSize, mimeType, from, streamUuid, encodedLength: encoded?.length });
27
28
  if (deltaSize > 0) {
28
- // Fetch the audio data from the fileUri
29
- const options = {
30
- encoding: FileSystem.EncodingType.Base64,
31
- position: from,
32
- length: deltaSize,
33
- };
29
+ // Coming from native ( ios / android ) otherwise buffer is set
34
30
  if (Platform.OS !== 'web') {
35
31
  // Read the audio file as a base64 string for comparison
36
32
  try {
37
- const base64Content = await FileSystem.readAsStringAsync(fileUri, options);
38
- const binaryData = atob(base64Content);
33
+ // convert encoded string to binary data
34
+ const binaryData = atob(encoded);
39
35
  const content = new Uint8Array(binaryData.length);
40
36
  for (let i = 0; i < binaryData.length; i++) {
41
37
  content[i] = binaryData.charCodeAt(i);
42
38
  }
43
- // TODO: get the filetype based on audio setting and encoding
44
- const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array
45
- console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`);
39
+ const audioBlob = new Blob([content], { type: mimeType });
40
+ // Below code is optional, used to compare encoded data to audio on file system
41
+ // Fetch the audio data from the fileUri
42
+ // const options = {
43
+ // encoding: FileSystem.EncodingType.Base64,
44
+ // position: from,
45
+ // length: deltaSize,
46
+ // };
47
+ // const base64Content = await FileSystem.readAsStringAsync(fileUri, options);
48
+ // const binaryData = atob(base64Content);
49
+ // const content = new Uint8Array(binaryData.length);
50
+ // for (let i = 0; i < binaryData.length; i++) {
51
+ // content[i] = binaryData.charCodeAt(i);
52
+ // }
53
+ // const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array
54
+ // console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`)
46
55
  onAudioStream?.(audioBlob);
47
56
  }
48
57
  catch (error) {
@@ -50,6 +59,7 @@ export function useAudioRecorder({ onAudioStream }) {
50
59
  }
51
60
  }
52
61
  else if (buffer) {
62
+ // Coming from web
53
63
  onAudioStream?.(buffer);
54
64
  }
55
65
  }
@@ -57,54 +67,36 @@ export function useAudioRecorder({ onAudioStream }) {
57
67
  return () => subscribe.remove();
58
68
  }, [isRecording, onAudioStream]);
59
69
  const startRecording = useCallback(async (recordingOptions) => {
60
- if (!isRecording) {
61
- setIsRecording(true);
62
- setIsPaused(false);
63
- setSize(0);
64
- setDuration(0);
65
- const startTime = Date.now();
66
- console.log(`module shims`, ExpoAudioStreamModule);
67
- try {
68
- console.log(`start recoding`, recordingOptions);
69
- await ExpoAudioStreamModule.startRecording(recordingOptions);
70
- }
71
- catch (error) {
72
- console.error('Error starting recording:', error);
73
- setIsRecording(false);
74
- }
70
+ setIsRecording(true);
71
+ setIsPaused(false);
72
+ setSize(0);
73
+ setDuration(0);
74
+ try {
75
+ log(`start recoding`, recordingOptions);
76
+ const fileUrl = await ExpoAudioStreamModule.startRecording(recordingOptions);
77
+ return fileUrl;
75
78
  }
76
- }, [isRecording]);
77
- const stopRecording = useCallback(async () => {
78
- if (isRecording) {
79
+ catch (error) {
80
+ console.error('Error starting recording:', error);
79
81
  setIsRecording(false);
80
- setIsPaused(false);
81
- try {
82
- const recordedDuration = await ExpoAudioStreamModule.stopRecording();
83
- setDuration(recordedDuration);
84
- return recordedDuration;
85
- }
86
- catch (error) {
87
- console.error('Error stopping recording:', error);
88
- return 0;
89
- }
90
82
  }
91
- return 0;
92
- }, [isRecording]);
93
- const pauseRecording = useCallback(() => {
94
- if (isRecording) {
95
- ExpoAudioStreamModule.stopRecording().catch(console.error);
83
+ }, []);
84
+ const stopRecording = useCallback(async () => {
85
+ setIsRecording(false);
86
+ setIsPaused(false);
87
+ const result = await ExpoAudioStreamModule.stopRecording();
88
+ return result;
89
+ }, []);
90
+ const pauseRecording = useCallback(async () => {
91
+ try {
92
+ await ExpoAudioStreamModule.stopRecording();
96
93
  setIsPaused(true);
97
94
  setIsRecording(false);
98
95
  }
99
- }, [isRecording]);
100
- // Cleanup listener on unmount to prevent memory leaks
101
- useEffect(() => {
102
- return () => {
103
- if (isRecording) {
104
- ExpoAudioStreamModule.stopRecording().catch(console.error);
105
- }
106
- };
107
- }, [isRecording]);
96
+ catch (error) {
97
+ console.error('Error pausing recording:', error);
98
+ }
99
+ }, []);
108
100
  return {
109
101
  startRecording,
110
102
  stopRecording,
@@ -1 +1 @@
1
- {"version":3,"file":"useAudioRecording.js","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"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","sourcesContent":["import { NativeModulesProxy, EventEmitter, type Subscription, Platform } from 'expo-modules-core';\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport ExpoAudioStreamModule from './ExpoAudioStreamModule';\nimport { AudioEventPayload, AudioStreamStatus, RecordingOptions } from \"./ExpoAudioStream.types\";\nimport { addChangeListener } from '.';\nimport * as FileSystem from 'expo-file-system';\nimport { decode as atob } from 'base-64';\n\nconst emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);\n\ninterface UseAudioRecorderState {\n startRecording: (_: RecordingOptions) => Promise<void>;\n stopRecording: () => Promise<number>;\n pauseRecording: () => void;\n isRecording: boolean;\n isPaused: boolean;\n duration: number; // Duration of the recording\n size: number; // Size in bytes of the recorded audio\n}\n\nexport function useAudioRecorder({onAudioStream}: {onAudioStream?: (buffer: Blob) => void}): UseAudioRecorderState {\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [size, setSize] = useState(0);\n\n useEffect( () => {\n if(isRecording || isPaused) {\n const interval = setInterval(() => {\n const status: AudioStreamStatus = ExpoAudioStreamModule.status()\n setDuration(status.duration);\n setSize(status.size);\n }, 1000);\n return () => clearInterval(interval);\n }\n\n return () => null;\n }, [isRecording, isPaused])\n\n\n useEffect(() => {\n const subscribe = addChangeListener(async ({fileUri, deltaSize, totalSize, from, streamUuid, encoded, buffer}) => {\n console.debug(`Received audio event:`, {fileUri, deltaSize, totalSize, from, streamUuid, encodedLength: encoded?.length})\n if(deltaSize > 0) {\n // Fetch the audio data from the fileUri\n const options = {\n encoding: FileSystem.EncodingType.Base64,\n position: from,\n length: deltaSize,\n };\n\n if(Platform.OS !== 'web') {\n // Read the audio file as a base64 string for comparison\n try {\n const base64Content = await FileSystem.readAsStringAsync(fileUri, options);\n const binaryData = atob(base64Content);\n const content = new Uint8Array(binaryData.length);\n for (let i = 0; i < binaryData.length; i++) {\n content[i] = binaryData.charCodeAt(i);\n }\n\n // TODO: get the filetype based on audio setting and encoding\n const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array\n console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`)\n onAudioStream?.(audioBlob);\n } catch (error) {\n console.error('Error reading audio file:', error);\n }\n } else if(buffer) {\n onAudioStream?.(buffer);\n }\n }\n });\n return () => subscribe.remove();\n }, [isRecording, onAudioStream]);\n\n\n const startRecording = useCallback(async (recordingOptions: RecordingOptions) => {\n if (!isRecording) {\n setIsRecording(true);\n setIsPaused(false);\n setSize(0);\n setDuration(0);\n const startTime = Date.now();\n\n console.log(`module shims`, ExpoAudioStreamModule)\n try {\n console.log(`start recoding`, recordingOptions)\n await ExpoAudioStreamModule.startRecording(recordingOptions);\n\n } catch (error) {\n console.error('Error starting recording:', error);\n setIsRecording(false);\n }\n }\n }, [isRecording]);\n\n const stopRecording = useCallback(async (): Promise<number> => {\n if (isRecording) {\n setIsRecording(false);\n setIsPaused(false);\n try {\n const recordedDuration = await ExpoAudioStreamModule.stopRecording();\n setDuration(recordedDuration);\n return recordedDuration;\n } catch (error) {\n console.error('Error stopping recording:', error);\n return 0;\n }\n }\n return 0;\n }, [isRecording]);\n\n const pauseRecording = useCallback(() => {\n if (isRecording) {\n ExpoAudioStreamModule.stopRecording().catch(console.error);\n setIsPaused(true);\n setIsRecording(false);\n }\n }, [isRecording]);\n\n // Cleanup listener on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n if (isRecording) {\n ExpoAudioStreamModule.stopRecording().catch(console.error);\n }\n };\n }, [isRecording]);\n\n return {\n startRecording,\n stopRecording,\n pauseRecording,\n isPaused,\n isRecording,\n duration,\n size\n };\n}"]}
1
+ {"version":3,"file":"useAudioRecording.js","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"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","sourcesContent":["import { NativeModulesProxy, EventEmitter, type Subscription, Platform } from 'expo-modules-core';\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport ExpoAudioStreamModule from './ExpoAudioStreamModule';\nimport { AudioEventPayload, AudioStreamResult, AudioStreamStatus, RecordingOptions } from \"./ExpoAudioStream.types\";\nimport { addAudioEventListener } from '.';\nimport * as FileSystem from 'expo-file-system';\nimport { decode as atob } from 'base-64';\nimport debug from 'debug';\n\nconst emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);\n\nconst log = debug(\"expo-audio-stream:useAudioRecording\");\n\ninterface UseAudioRecorderState {\n startRecording: (_: RecordingOptions) => Promise<string | null>;\n stopRecording: () => Promise<AudioStreamResult | null>;\n pauseRecording: () => void;\n isRecording: boolean;\n isPaused: boolean;\n duration: number; // Duration of the recording\n size: number; // Size in bytes of the recorded audio\n}\n\n\nexport function useAudioRecorder({onAudioStream}: {onAudioStream?: (buffer: Blob) => void}): UseAudioRecorderState {\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [size, setSize] = useState(0);\n\n useEffect( () => {\n if(isRecording || isPaused) {\n const interval = setInterval(() => {\n const status: AudioStreamStatus = ExpoAudioStreamModule.status()\n setDuration(status.duration);\n setSize(status.size);\n }, 1000);\n return () => clearInterval(interval);\n }\n\n return () => null;\n }, [isRecording, isPaused])\n\n\n useEffect(() => {\n const subscribe = addAudioEventListener(async ({fileUri, deltaSize, totalSize, from, streamUuid, encoded, mimeType, buffer}) => {\n log(`Received audio event:`, {fileUri, deltaSize, totalSize, mimeType, from, streamUuid, encodedLength: encoded?.length})\n if(deltaSize > 0) {\n // Coming from native ( ios / android ) otherwise buffer is set\n if(Platform.OS !== 'web') {\n // Read the audio file as a base64 string for comparison\n try {\n // convert encoded string to binary data\n const binaryData = atob(encoded);\n const content = new Uint8Array(binaryData.length);\n for (let i = 0; i < binaryData.length; i++) {\n content[i] = binaryData.charCodeAt(i);\n }\n const audioBlob = new Blob([content], { type: mimeType });\n\n // Below code is optional, used to compare encoded data to audio on file system\n // Fetch the audio data from the fileUri\n // const options = {\n // encoding: FileSystem.EncodingType.Base64,\n // position: from,\n // length: deltaSize,\n // };\n // const base64Content = await FileSystem.readAsStringAsync(fileUri, options);\n // const binaryData = atob(base64Content);\n // const content = new Uint8Array(binaryData.length);\n // for (let i = 0; i < binaryData.length; i++) {\n // content[i] = binaryData.charCodeAt(i);\n // }\n // const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array\n // console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`)\n\n onAudioStream?.(audioBlob);\n } catch (error) {\n console.error('Error reading audio file:', error);\n }\n } else if(buffer) {\n // Coming from web\n onAudioStream?.(buffer);\n }\n }\n });\n return () => subscribe.remove();\n }, [isRecording, onAudioStream]);\n\n\n const startRecording = useCallback(async (recordingOptions: RecordingOptions) => {\n setIsRecording(true);\n setIsPaused(false);\n setSize(0);\n setDuration(0);\n try {\n log(`start recoding`, recordingOptions)\n const fileUrl = await ExpoAudioStreamModule.startRecording(recordingOptions);\n\n return fileUrl;\n } catch (error) {\n console.error('Error starting recording:', error);\n setIsRecording(false);\n }\n }, []);\n\n const stopRecording = useCallback(async () => {\n setIsRecording(false);\n setIsPaused(false);\n const result: AudioStreamResult = await ExpoAudioStreamModule.stopRecording();\n return result;\n }, []);\n\n const pauseRecording = useCallback(async () => {\n try {\n await ExpoAudioStreamModule.stopRecording()\n setIsPaused(true);\n setIsRecording(false);\n } catch (error) {\n console.error('Error pausing recording:', error);\n }\n }, []);\n\n return {\n startRecording,\n stopRecording,\n pauseRecording,\n isPaused,\n isRecording,\n duration,\n size\n };\n}"]}
@@ -9,15 +9,44 @@ import Foundation
9
9
  import AVFoundation
10
10
 
11
11
  struct RecordingSettings {
12
- var sampleRate: Double = 48000.0
12
+ var sampleRate: Double
13
13
  var numberOfChannels: Int = 1
14
14
  var bitDepth: Int = 16
15
15
  }
16
16
 
17
+ // Helper to convert to little-endian byte array
18
+ extension UInt32 {
19
+ var littleEndianBytes: [UInt8] {
20
+ let value = self.littleEndian
21
+ return [UInt8(value & 0xff), UInt8((value >> 8) & 0xff), UInt8((value >> 16) & 0xff), UInt8((value >> 24) & 0xff)]
22
+ }
23
+ }
24
+
25
+ extension UInt16 {
26
+ var littleEndianBytes: [UInt8] {
27
+ let value = self.littleEndian
28
+ return [UInt8(value & 0xff), UInt8((value >> 8) & 0xff)]
29
+ }
30
+ }
31
+
32
+
33
+ struct RecordingResult {
34
+ var fileUri: String
35
+ var mimeType: String
36
+ var duration: Int64
37
+ var size: Int64
38
+ }
39
+
17
40
  protocol AudioStreamManagerDelegate: AnyObject {
18
41
  func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64)
19
42
  }
20
43
 
44
+ enum AudioStreamError: Error {
45
+ case audioSessionSetupFailed(String)
46
+ case fileCreationFailed(URL)
47
+ case audioProcessingError(String)
48
+ }
49
+
21
50
  class AudioStreamManager: NSObject {
22
51
  private let audioEngine = AVAudioEngine()
23
52
  private var inputNode: AVAudioInputNode {
@@ -33,7 +62,9 @@ class AudioStreamManager: NSObject {
33
62
  private var isPaused = false
34
63
  private var pausedDuration = 0
35
64
  private var fileManager = FileManager.default
65
+ private var recordingSettings: RecordingSettings?
36
66
  internal var recordingUUID: UUID?
67
+ internal var mimeType: String = "audio/wav"
37
68
  weak var delegate: AudioStreamManagerDelegate? // Define the delegate here
38
69
 
39
70
  override init() {
@@ -52,85 +83,205 @@ class AudioStreamManager: NSObject {
52
83
  }
53
84
  }
54
85
 
86
+ @objc func handleAudioSessionInterruption(notification: Notification) {
87
+ guard let info = notification.userInfo,
88
+ let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
89
+ let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
90
+ return
91
+ }
92
+
93
+ if type == .began {
94
+ // Pause your audio recording
95
+ } else if type == .ended {
96
+ if let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt {
97
+ let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
98
+ if options.contains(.shouldResume) {
99
+ // Resume your audio recording
100
+ try? AVAudioSession.sharedInstance().setActive(true)
101
+ }
102
+ }
103
+ }
104
+ }
105
+
55
106
  private func createRecordingFile() -> URL? {
56
107
  let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
57
108
  recordingUUID = UUID()
58
- let fileName = "\(recordingUUID!.uuidString).pcm"
109
+ let fileName = "\(recordingUUID!.uuidString).wav"
59
110
  let fileURL = documentsDirectory.appendingPathComponent(fileName)
60
- fileManager.createFile(atPath: fileURL.path, contents: nil, attributes: nil)
61
- print("Recording file created at:", fileURL.path)
62
-
111
+
112
+ if fileManager.createFile(atPath: fileURL.path, contents: nil, attributes: nil) {
113
+ do {
114
+ let fileHandle = try FileHandle(forWritingTo: fileURL)
115
+ let wavHeader = createWavHeader(dataSize: 0) // Initially set data size to 0
116
+ fileHandle.write(wavHeader)
117
+ fileHandle.closeFile()
118
+ print("Recording file with header created at:", fileURL.path)
119
+ } catch {
120
+ print("Failed to write WAV header: \(error.localizedDescription)")
121
+ return nil
122
+ }
123
+ }
63
124
  return fileURL
64
125
  }
65
126
 
127
+ private func createWavHeader(dataSize: Int) -> Data {
128
+ var header = Data()
129
+
130
+ let sampleRate = UInt32(recordingSettings!.sampleRate)
131
+ let channels = UInt32(recordingSettings!.numberOfChannels)
132
+ let bitDepth = UInt32(recordingSettings!.bitDepth)
133
+
134
+ // Calculate byteRate
135
+ let byteRate = sampleRate * channels * (bitDepth / 8)
136
+
137
+ // "RIFF" chunk descriptor
138
+ header.append(contentsOf: "RIFF".utf8)
139
+ header.append(contentsOf: UInt32(36 + dataSize).littleEndianBytes)
140
+ header.append(contentsOf: "WAVE".utf8)
141
+
142
+ // "fmt " sub-chunk
143
+ header.append(contentsOf: "fmt ".utf8)
144
+ header.append(contentsOf: UInt32(16).littleEndianBytes) // PCM format requires 16 bytes for the fmt sub-chunk
145
+ header.append(contentsOf: UInt16(1).littleEndianBytes) // Audio format 1 for PCM
146
+ header.append(contentsOf: UInt16(channels).littleEndianBytes)
147
+ header.append(contentsOf: sampleRate.littleEndianBytes)
148
+ header.append(contentsOf: byteRate.littleEndianBytes) // byteRate
149
+ header.append(contentsOf: UInt16(channels * (bitDepth / 8)).littleEndianBytes) // blockAlign
150
+ header.append(contentsOf: UInt16(bitDepth).littleEndianBytes) // bits per sample
151
+
152
+ // "data" sub-chunk
153
+ header.append(contentsOf: "data".utf8)
154
+ header.append(contentsOf: UInt32(dataSize).littleEndianBytes) // Sub-chunk data size
155
+
156
+ return header
157
+ }
158
+
159
+
66
160
  func getStatus() -> [String: Any] {
67
161
  let currentTime = Date()
68
162
  let totalRecordedTime = startTime != nil ? Int(currentTime.timeIntervalSince(startTime!)) - pausedDuration : 0
69
163
  return [
70
- "duration": totalRecordedTime,
164
+ "duration": totalRecordedTime * 1000,
71
165
  "isRecording": isRecording,
72
166
  "isPaused": isPaused,
167
+ "mimeType": mimeType,
73
168
  "size": totalDataSize,
74
169
  "interval": emissionInterval
75
170
  ]
76
171
  }
77
172
 
78
- func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) {
79
- guard !isRecording else { return }
80
-
81
- emissionInterval = max(100.0, Double(intervalMilliseconds)) / 1000.0 // Convert ms to seconds, ensure minimum of 100 ms
82
- lastEmissionTime = Date() // Reset last emission time
173
+ func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> String? {
174
+ guard !isRecording else {
175
+ print("Debug: Recording is already in progress.")
176
+ return nil
177
+ }
178
+
179
+ emissionInterval = max(100.0, Double(intervalMilliseconds)) / 1000.0
180
+ lastEmissionTime = Date()
181
+ recordingSettings = settings
83
182
 
84
- // Configure audio session for the desired sample rate and channel count
85
183
  let session = AVAudioSession.sharedInstance()
86
184
  do {
185
+ print("Debug: Configuring audio session with sample rate: \(settings.sampleRate) Hz")
87
186
  try session.setPreferredSampleRate(settings.sampleRate)
88
187
  try session.setPreferredIOBufferDuration(1024 / settings.sampleRate)
89
188
  try session.setCategory(.playAndRecord)
90
189
  try session.setActive(true)
190
+ print("Debug: Audio session activated successfully.")
91
191
  } catch {
92
- print("Failed to set up audio session: \(error)")
93
- return
192
+ print("Error: Failed to set up audio session with preferred settings: \(error.localizedDescription)")
193
+ return nil
194
+ }
195
+
196
+ NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption), name: AVAudioSession.interruptionNotification, object: nil)
197
+
198
+ guard let channelLayout = AVAudioChannelLayout(layoutTag: settings.numberOfChannels == 1 ? kAudioChannelLayoutTag_Mono : kAudioChannelLayoutTag_Stereo) else {
199
+ print("Error: Failed to create channel layout.")
200
+ return nil
94
201
  }
95
-
96
- // Create an audio format with specified or default settings
97
- let channelLayout = AVAudioChannelLayout(layoutTag: settings.numberOfChannels == 1 ? kAudioChannelLayoutTag_Mono : kAudioChannelLayoutTag_Stereo) ?? AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_Stereo)!
98
202
  let errorFormat = AVAudioFormat(standardFormatWithSampleRate: settings.sampleRate, channelLayout: channelLayout)
99
-
100
- // Create an audio format with default settings
101
- let format = audioEngine.inputNode.inputFormat(forBus: 0)
102
-
103
- // Debugging statements
104
- print("Desired Sample Rate:", settings.sampleRate)
105
- print("Channel Layout:", channelLayout.description)
106
- print("Created Audio Format Sample Rate: \(format.sampleRate) channelLayout: \(format.channelLayout) channelCount: \(format.channelCount)")
107
- print("Error Audio Format Sample Rate: \(errorFormat.sampleRate) channel Layout: \(errorFormat.channelLayout) channelCount: \(errorFormat.channelCount)")
108
- print("Hardware Format Sample Rate:", audioEngine.inputNode.inputFormat(forBus: 0).sampleRate)
109
-
110
- // Install tap on the input node and handle audio buffer
203
+
111
204
  audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: errorFormat) { [weak self] (buffer, time) in
112
- guard let self = self, let fileURL = self.recordingFileURL else { return }
205
+ guard let self = self, let fileURL = self.recordingFileURL else {
206
+ print("Error: File URL or self is nil during buffer processing.")
207
+ return
208
+ }
113
209
  self.processAudioBuffer(buffer, fileURL: fileURL)
114
210
  }
115
211
 
116
212
  recordingFileURL = createRecordingFile()
213
+ if recordingFileURL == nil {
214
+ print("Error: Failed to create recording file.")
215
+ return nil
216
+ }
217
+
117
218
  do {
118
219
  startTime = Date()
119
220
  try audioEngine.start()
120
221
  isRecording = true
222
+ print("Debug: Recording started successfully.")
223
+ return recordingFileURL?.absoluteString
121
224
  } catch {
122
- print("Could not start the audio engine: \(error)")
225
+ print("Error: Could not start the audio engine: \(error.localizedDescription)")
123
226
  isRecording = false
227
+ return nil
124
228
  }
125
229
  }
230
+
126
231
 
127
- func stopRecording() {
232
+ func stopRecording() -> RecordingResult? {
128
233
  audioEngine.stop()
129
234
  audioEngine.inputNode.removeTap(onBus: 0)
130
235
  isRecording = false
131
- recordingFileURL = nil // Optionally reset or handle the finalization of the file
132
- print("Recording stopped.")
133
-
236
+
237
+ guard let fileURL = recordingFileURL, let startTime = startTime else {
238
+ print("Recording or file URL is nil.")
239
+ return nil
240
+ }
241
+
242
+ let endTime = Date()
243
+ let duration = Int64(endTime.timeIntervalSince(startTime) * 1000) - Int64(pausedDuration * 1000)
244
+
245
+ // Calculate the total size of audio data written to the file
246
+ let filePath = fileURL.path
247
+ do {
248
+ let fileAttributes = try FileManager.default.attributesOfItem(atPath: filePath)
249
+ let fileSize = fileAttributes[FileAttributeKey.size] as? Int64 ?? 0
250
+
251
+ // Update the WAV header with the correct file size
252
+ updateWavHeader(fileURL: fileURL, totalDataSize: fileSize - 44) // Subtract the header size to get audio data size
253
+
254
+ let result = RecordingResult(fileUri: fileURL.absoluteString, mimeType: mimeType, duration: duration, size: fileSize)
255
+ recordingFileURL = nil // Reset for next recording
256
+ return result
257
+ } catch {
258
+ print("Failed to fetch file attributes: \(error)")
259
+ return nil
260
+ }
261
+ }
262
+
263
+ private func updateWavHeader(fileURL: URL, totalDataSize: Int64) {
264
+ do {
265
+ let fileHandle = try FileHandle(forUpdating: fileURL)
266
+ defer { fileHandle.closeFile() }
267
+
268
+ // Calculate sizes
269
+ let fileSize = totalDataSize + 44 - 8 // Total file size minus 8 bytes for 'RIFF' and size field itself
270
+ let dataSize = totalDataSize // Size of the 'data' sub-chunk
271
+
272
+ // Update RIFF chunk size at offset 4
273
+ fileHandle.seek(toFileOffset: 4)
274
+ let fileSizeBytes = UInt32(fileSize).littleEndianBytes
275
+ fileHandle.write(Data(fileSizeBytes))
276
+
277
+ // Update data chunk size at offset 40
278
+ fileHandle.seek(toFileOffset: 40)
279
+ let dataSizeBytes = UInt32(dataSize).littleEndianBytes
280
+ fileHandle.write(Data(dataSizeBytes))
281
+
282
+ } catch let error {
283
+ print("Error updating WAV header: \(error)")
284
+ }
134
285
  }
135
286
 
136
287
  private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, fileURL: URL) {
@@ -146,18 +297,19 @@ class AudioStreamManager: NSObject {
146
297
  }
147
298
  let data = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
148
299
 
300
+ print("Writing data size: \(data.count) bytes") // Debug: Check the size of data being written
149
301
  fileHandle.seekToEndOfFile()
150
302
  fileHandle.write(data)
151
303
  fileHandle.closeFile()
152
304
 
153
305
  totalDataSize += Int64(data.count)
154
-
306
+ print("Total data size written: \(totalDataSize) bytes") // Debug: Check total data written
307
+
155
308
  let currentTime = Date()
156
309
  if let lastEmissionTime = lastEmissionTime, currentTime.timeIntervalSince(lastEmissionTime) >= emissionInterval {
157
310
  if let startTime = startTime {
158
311
  let recordingTime = currentTime.timeIntervalSince(startTime)
159
312
  print("Emitting data: Recording time \(recordingTime) seconds, Data size \(totalDataSize) bytes")
160
- print("delegate", self.delegate)
161
313
  self.delegate?.audioStreamManager(self, didReceiveAudioData: data, recordingTime: recordingTime, totalDataSize: totalDataSize)
162
314
  self.lastEmissionTime = currentTime // Update last emission time
163
315
  self.lastEmittedSize = totalDataSize
@@ -1,10 +1,11 @@
1
1
  import ExpoModulesCore
2
+ import AVFoundation
2
3
 
3
4
  let audioDataEvent: String = "AudioData"
4
5
 
5
6
  public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
6
7
  private var streamManager = AudioStreamManager()
7
-
8
+
8
9
  public func definition() -> ModuleDefinition {
9
10
  Name("ExpoAudioStream")
10
11
 
@@ -15,7 +16,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
15
16
  print("Setting streamManager delegate")
16
17
  streamManager.delegate = self
17
18
  }
18
-
19
+
19
20
  AsyncFunction("startRecording") { (options: [String: Any], promise: Promise) in
20
21
  self.checkMicrophonePermission { granted in
21
22
  guard granted else {
@@ -24,15 +25,15 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
24
25
  }
25
26
 
26
27
  // Extract settings from provided options, using default values if necessary
27
- let sampleRate = options["sampleRate"] as? Double ?? 48000.0
28
- let numberOfChannels = options["channelConfig"] as? Int ?? 1
29
- let bitDepth = options["audioFormat"] as? Int ?? 16
28
+ let sampleRate = options["sampleRate"] as? Double ?? 16000.0 // it fails if not 48000, why?
29
+ let numberOfChannels = options["channelConfig"] as? Int ?? 1 // Mono channel configuration
30
+ let bitDepth = options["audioFormat"] as? Int ?? 16 // 16bits
30
31
  let interval = options["interval"] as? Int ?? 1000
31
32
 
32
33
  let settings = RecordingSettings(sampleRate: sampleRate, numberOfChannels: numberOfChannels, bitDepth: bitDepth)
33
- self.streamManager.startRecording(settings: settings, intervalMilliseconds: interval)
34
+ let url = self.streamManager.startRecording(settings: settings, intervalMilliseconds: interval)
34
35
 
35
- promise.resolve(nil)
36
+ promise.resolve(url)
36
37
  }
37
38
  }
38
39
 
@@ -41,8 +42,17 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
41
42
  }
42
43
 
43
44
  AsyncFunction("stopRecording") { (promise: Promise) in
44
- self.streamManager.stopRecording()
45
- promise.resolve(nil)
45
+ if let recordingResult = self.streamManager.stopRecording() {
46
+ // Convert RecordingResult to a dictionary
47
+ let resultDict: [String: Any] = [
48
+ "fileUri": recordingResult.fileUri,
49
+ "duration": recordingResult.duration,
50
+ "size": recordingResult.size
51
+ ]
52
+ promise.resolve(resultDict)
53
+ } else {
54
+ promise.reject("ERROR", "Failed to stop recording or no recording in progress.")
55
+ }
46
56
  }
47
57
 
48
58
  AsyncFunction("listAudioFiles") { (promise: Promise) in
@@ -56,7 +66,6 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
56
66
  }
57
67
 
58
68
  func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64) {
59
- print("audioStreamManager debug sending data")
60
69
  guard let fileURL = manager.recordingFileURL else { return }
61
70
  let encodedData = data.base64EncodedString()
62
71
 
@@ -71,13 +80,13 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
71
80
  "encoded": encodedData,
72
81
  "deltaSize": deltaSize,
73
82
  "totalSize": fileSize,
83
+ "mimeType": manager.mimeType,
74
84
  "streamUuid": manager.recordingUUID?.uuidString ?? UUID().uuidString
75
85
  ]
76
86
 
77
87
  // Update the last emitted size for the next calculation
78
88
  manager.lastEmittedSize += Int64(deltaSize)
79
89
 
80
- print("Sending audio data", eventBody)
81
90
  // Emit the event to JavaScript
82
91
  sendEvent(audioDataEvent, eventBody)
83
92
  }
@@ -122,7 +131,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
122
131
 
123
132
  do {
124
133
  let files = try FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil)
125
- let audioFiles = files.filter { $0.pathExtension == "pcm" }.map { $0.lastPathComponent }
134
+ let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.lastPathComponent }
126
135
  return audioFiles
127
136
  } catch {
128
137
  print("Error listing audio files:", error.localizedDescription)