@siteed/expo-audio-stream 2.0.1 → 2.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.
Files changed (166) hide show
  1. package/README.md +46 -27
  2. package/build/index.d.ts +11 -12
  3. package/build/index.js +44 -10
  4. package/package.json +49 -110
  5. package/src/index.ts +18 -33
  6. package/CHANGELOG.md +0 -195
  7. package/android/build.gradle +0 -105
  8. package/android/src/main/AndroidManifest.xml +0 -27
  9. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
  10. package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
  11. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
  12. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
  13. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
  14. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -1936
  15. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
  16. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -138
  17. package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -20
  18. package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
  19. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -509
  20. package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
  21. package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
  22. package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
  23. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
  24. package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
  25. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
  26. package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
  27. package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
  28. package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
  29. package/android/src/main/res/drawable/ic_microphone.xml +0 -13
  30. package/android/src/main/res/drawable/ic_pause.xml +0 -10
  31. package/android/src/main/res/drawable/ic_play.xml +0 -10
  32. package/android/src/main/res/drawable/ic_stop.xml +0 -10
  33. package/android/src/main/res/layout/notification_recording.xml +0 -37
  34. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
  35. package/app.plugin.js +0 -1
  36. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -144
  37. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
  38. package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
  39. package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  40. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -78
  41. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
  42. package/build/AudioAnalysis/extractAudioAnalysis.js +0 -229
  43. package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  44. package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
  45. package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
  46. package/build/AudioAnalysis/extractWaveform.js +0 -11
  47. package/build/AudioAnalysis/extractWaveform.js.map +0 -1
  48. package/build/AudioRecorder.provider.d.ts +0 -11
  49. package/build/AudioRecorder.provider.d.ts.map +0 -1
  50. package/build/AudioRecorder.provider.js +0 -37
  51. package/build/AudioRecorder.provider.js.map +0 -1
  52. package/build/ExpoAudioStream.native.d.ts +0 -3
  53. package/build/ExpoAudioStream.native.d.ts.map +0 -1
  54. package/build/ExpoAudioStream.native.js +0 -6
  55. package/build/ExpoAudioStream.native.js.map +0 -1
  56. package/build/ExpoAudioStream.types.d.ts +0 -206
  57. package/build/ExpoAudioStream.types.d.ts.map +0 -1
  58. package/build/ExpoAudioStream.types.js +0 -2
  59. package/build/ExpoAudioStream.types.js.map +0 -1
  60. package/build/ExpoAudioStream.web.d.ts +0 -59
  61. package/build/ExpoAudioStream.web.d.ts.map +0 -1
  62. package/build/ExpoAudioStream.web.js +0 -285
  63. package/build/ExpoAudioStream.web.js.map +0 -1
  64. package/build/ExpoAudioStreamModule.d.ts +0 -3
  65. package/build/ExpoAudioStreamModule.d.ts.map +0 -1
  66. package/build/ExpoAudioStreamModule.js +0 -239
  67. package/build/ExpoAudioStreamModule.js.map +0 -1
  68. package/build/WebRecorder.web.d.ts +0 -119
  69. package/build/WebRecorder.web.d.ts.map +0 -1
  70. package/build/WebRecorder.web.js +0 -436
  71. package/build/WebRecorder.web.js.map +0 -1
  72. package/build/constants.d.ts +0 -11
  73. package/build/constants.d.ts.map +0 -1
  74. package/build/constants.js +0 -14
  75. package/build/constants.js.map +0 -1
  76. package/build/events.d.ts +0 -26
  77. package/build/events.d.ts.map +0 -1
  78. package/build/events.js +0 -21
  79. package/build/events.js.map +0 -1
  80. package/build/index.d.ts.map +0 -1
  81. package/build/index.js.map +0 -1
  82. package/build/useAudioRecorder.d.ts +0 -21
  83. package/build/useAudioRecorder.d.ts.map +0 -1
  84. package/build/useAudioRecorder.js +0 -427
  85. package/build/useAudioRecorder.js.map +0 -1
  86. package/build/utils/BlobFix.d.ts +0 -9
  87. package/build/utils/BlobFix.d.ts.map +0 -1
  88. package/build/utils/BlobFix.js +0 -498
  89. package/build/utils/BlobFix.js.map +0 -1
  90. package/build/utils/audioProcessing.d.ts +0 -24
  91. package/build/utils/audioProcessing.d.ts.map +0 -1
  92. package/build/utils/audioProcessing.js +0 -133
  93. package/build/utils/audioProcessing.js.map +0 -1
  94. package/build/utils/concatenateBuffers.d.ts +0 -8
  95. package/build/utils/concatenateBuffers.d.ts.map +0 -1
  96. package/build/utils/concatenateBuffers.js +0 -21
  97. package/build/utils/concatenateBuffers.js.map +0 -1
  98. package/build/utils/convertPCMToFloat32.d.ts +0 -13
  99. package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
  100. package/build/utils/convertPCMToFloat32.js +0 -120
  101. package/build/utils/convertPCMToFloat32.js.map +0 -1
  102. package/build/utils/encodingToBitDepth.d.ts +0 -5
  103. package/build/utils/encodingToBitDepth.d.ts.map +0 -1
  104. package/build/utils/encodingToBitDepth.js +0 -13
  105. package/build/utils/encodingToBitDepth.js.map +0 -1
  106. package/build/utils/getWavFileInfo.d.ts +0 -26
  107. package/build/utils/getWavFileInfo.d.ts.map +0 -1
  108. package/build/utils/getWavFileInfo.js +0 -92
  109. package/build/utils/getWavFileInfo.js.map +0 -1
  110. package/build/utils/writeWavHeader.d.ts +0 -49
  111. package/build/utils/writeWavHeader.d.ts.map +0 -1
  112. package/build/utils/writeWavHeader.js +0 -91
  113. package/build/utils/writeWavHeader.js.map +0 -1
  114. package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
  115. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
  116. package/build/workers/InlineFeaturesExtractor.web.js +0 -828
  117. package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
  118. package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
  119. package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
  120. package/build/workers/inlineAudioWebWorker.web.js +0 -157
  121. package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
  122. package/expo-module.config.json +0 -9
  123. package/ios/AudioAnalysisData.swift +0 -74
  124. package/ios/AudioNotificationManager.swift +0 -135
  125. package/ios/AudioProcessingHelpers.swift +0 -743
  126. package/ios/AudioProcessor.swift +0 -858
  127. package/ios/AudioStreamError.swift +0 -7
  128. package/ios/AudioStreamManager.swift +0 -1708
  129. package/ios/AudioStreamManagerDelegate.swift +0 -16
  130. package/ios/DataPoint.swift +0 -54
  131. package/ios/DecodingConfig.swift +0 -47
  132. package/ios/ExpoAudioStream.podspec +0 -27
  133. package/ios/ExpoAudioStreamModule.swift +0 -698
  134. package/ios/FFT.swift +0 -62
  135. package/ios/Features.swift +0 -95
  136. package/ios/Logger.swift +0 -7
  137. package/ios/NotificationExtension.swift +0 -15
  138. package/ios/RecordingResult.swift +0 -22
  139. package/ios/RecordingSettings.swift +0 -265
  140. package/ios/WaveformExtractor.swift +0 -105
  141. package/plugin/build/index.d.ts +0 -21
  142. package/plugin/build/index.js +0 -191
  143. package/plugin/src/index.ts +0 -278
  144. package/plugin/tsconfig.json +0 -10
  145. package/plugin/tsconfig.tsbuildinfo +0 -1
  146. package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -165
  147. package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -370
  148. package/src/AudioAnalysis/extractWaveform.ts +0 -22
  149. package/src/AudioRecorder.provider.tsx +0 -54
  150. package/src/ExpoAudioStream.native.ts +0 -6
  151. package/src/ExpoAudioStream.types.ts +0 -329
  152. package/src/ExpoAudioStream.web.ts +0 -359
  153. package/src/ExpoAudioStreamModule.ts +0 -286
  154. package/src/WebRecorder.web.ts +0 -580
  155. package/src/constants.ts +0 -18
  156. package/src/events.ts +0 -60
  157. package/src/useAudioRecorder.tsx +0 -620
  158. package/src/utils/BlobFix.ts +0 -559
  159. package/src/utils/audioProcessing.ts +0 -205
  160. package/src/utils/concatenateBuffers.ts +0 -24
  161. package/src/utils/convertPCMToFloat32.ts +0 -170
  162. package/src/utils/encodingToBitDepth.ts +0 -18
  163. package/src/utils/getWavFileInfo.ts +0 -132
  164. package/src/utils/writeWavHeader.ts +0 -114
  165. package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
  166. package/src/workers/inlineAudioWebWorker.web.tsx +0 -156
@@ -1,138 +0,0 @@
1
- package net.siteed.audiostream
2
-
3
- import android.app.Service
4
- import android.content.Context
5
- import android.content.Intent
6
- import android.os.Build
7
- import android.os.IBinder
8
- import android.util.Log
9
- import android.os.Handler
10
- import android.os.Looper
11
- import expo.modules.kotlin.Promise
12
- import android.app.NotificationChannel
13
- import android.app.NotificationManager
14
- import android.os.Build.VERSION_CODES
15
- import android.app.Notification
16
- import androidx.core.app.NotificationCompat
17
-
18
- class AudioRecordingService : Service() {
19
- private val notificationManager by lazy {
20
- AudioNotificationManager.getInstance(applicationContext)
21
- }
22
- private val mainHandler = Handler(Looper.getMainLooper())
23
- private var isRunning = false
24
-
25
- override fun onBind(intent: Intent?): IBinder? = null
26
-
27
- override fun onCreate() {
28
- super.onCreate()
29
- Log.d(Constants.TAG, "AudioRecordingService onCreate")
30
- }
31
-
32
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
33
- Log.d(Constants.TAG, "AudioRecordingService onStartCommand")
34
-
35
- // Check if service is being started from BOOT_COMPLETED
36
- val isFromBoot = intent?.action == Intent.ACTION_BOOT_COMPLETED
37
-
38
- if (!isRunning) {
39
- isRunning = true
40
-
41
- // Don't start foreground service if coming from BOOT_COMPLETED on Android 15+
42
- if (!isFromBoot || Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
43
- // Start as foreground service if keepAwake is true, regardless of notification settings
44
- val keepAwake = AudioRecorderManager.getInstance()?.getKeepAwakeStatus() ?: true
45
- if (keepAwake) {
46
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
47
- // Create a minimal notification channel if needed
48
- val channel = NotificationChannel(
49
- "recording_service",
50
- "Recording Service",
51
- NotificationManager.IMPORTANCE_LOW
52
- ).apply {
53
- setSound(null, null)
54
- enableLights(false)
55
- enableVibration(false)
56
- }
57
- val notificationManager = getSystemService(NotificationManager::class.java)
58
- notificationManager.createNotificationChannel(channel)
59
-
60
- // Create minimal silent notification
61
- val notification = NotificationCompat.Builder(this, "recording_service")
62
- .setContentTitle("")
63
- .setContentText("")
64
- .setSmallIcon(R.drawable.ic_microphone)
65
- .setOngoing(true)
66
- .setSound(null)
67
- .setVibrate(null)
68
- .setDefaults(0)
69
- .setPriority(NotificationCompat.PRIORITY_LOW)
70
- .build()
71
-
72
- startForeground(1, notification)
73
- }
74
- }
75
- }
76
- }
77
-
78
- return START_STICKY
79
- }
80
-
81
- override fun onDestroy() {
82
- Log.d(Constants.TAG, "AudioRecordingService onDestroy")
83
-
84
- stopForeground(STOP_FOREGROUND_REMOVE)
85
-
86
- isRunning = false
87
- super.onDestroy()
88
- }
89
-
90
- override fun onTaskRemoved(rootIntent: Intent?) {
91
- super.onTaskRemoved(rootIntent)
92
- Log.d(Constants.TAG, "AudioRecordingService onTaskRemoved")
93
-
94
- // Stop recording when app is killed
95
- AudioRecorderManager.getInstance()?.let { manager ->
96
- mainHandler.post {
97
- // Create a simple promise object for internal use
98
- val promise = object : Promise {
99
- override fun resolve(value: Any?) {
100
- Log.d(Constants.TAG, "Successfully stopped recording on task removed")
101
- cleanup()
102
- }
103
- override fun reject(code: String, message: String?, cause: Throwable?) {
104
- Log.e(Constants.TAG, "Failed to stop recording on task removed: $message")
105
- cleanup()
106
- }
107
- }
108
-
109
- try {
110
- manager.stopRecording(promise)
111
- } catch (e: Exception) {
112
- promise.reject("ERROR", e.message, e)
113
- }
114
- }
115
- }
116
- }
117
-
118
-
119
- private fun cleanup() {
120
- stopForeground(STOP_FOREGROUND_REMOVE)
121
- stopSelf()
122
- }
123
-
124
- companion object {
125
- fun startService(context: Context) {
126
- val serviceIntent = Intent(context, AudioRecordingService::class.java)
127
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
128
- context.startForegroundService(serviceIntent)
129
- } else {
130
- context.startService(serviceIntent)
131
- }
132
- }
133
-
134
- fun stopService(context: Context) {
135
- context.stopService(Intent(context, AudioRecordingService::class.java))
136
- }
137
- }
138
- }
@@ -1,20 +0,0 @@
1
- package net.siteed.audiostream
2
-
3
- object Constants {
4
- const val AUDIO_EVENT_NAME = "AudioData"
5
- const val AUDIO_ANALYSIS_EVENT_NAME = "AudioAnalysis"
6
- const val RECORDING_INTERRUPTED_EVENT_NAME = "onRecordingInterrupted"
7
- const val DEFAULT_SAMPLE_RATE = 16000 // Default sample rate for audio recording
8
- const val DEFAULT_CHANNEL_CONFIG = 1 // Mono
9
- const val DEFAULT_AUDIO_FORMAT = 16 // 16-bit PCM
10
- const val DEFAULT_INTERVAL = 1000L
11
- const val DEFAULT_INTERVAL_ANALYSIS = 500L
12
- const val MIN_INTERVAL = 10L // Minimum interval in ms for emitting audio data
13
- const val WAV_HEADER_SIZE = 44
14
- const val RIFF_HEADER = 0x52494646 // "RIFF"
15
- const val WAVE_HEADER = 0x57415645 // "WAVE"
16
- const val FMT_CHUNK_ID = 0x666d7420 // "fmt "
17
- const val DATA_CHUNK_ID = 0x64617461 // "data"
18
- const val INFO_CHUNK_ID = 0x494E464F // "info"
19
- const val TAG = "AudioRecorderModule"
20
- }
@@ -1,7 +0,0 @@
1
- package net.siteed.audiostream
2
-
3
- import android.os.Bundle
4
-
5
- interface EventSender {
6
- fun sendExpoEvent(eventName: String, params: Bundle)
7
- }
@@ -1,509 +0,0 @@
1
- // packages/expo-audio-stream/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt
2
- package net.siteed.audiostream
3
-
4
- import android.Manifest
5
- import android.app.ActivityManager
6
- import android.content.Context
7
- import android.os.Build
8
- import android.os.Bundle
9
- import android.util.Log
10
- import androidx.annotation.RequiresApi
11
- import androidx.core.os.bundleOf
12
- import expo.modules.kotlin.Promise
13
- import expo.modules.kotlin.modules.Module
14
- import expo.modules.kotlin.modules.ModuleDefinition
15
- import expo.modules.interfaces.permissions.Permissions
16
- import java.util.zip.CRC32
17
-
18
- class ExpoAudioStreamModule : Module(), EventSender {
19
- private lateinit var audioRecorderManager: AudioRecorderManager
20
- private lateinit var audioProcessor: AudioProcessor
21
-
22
- @RequiresApi(Build.VERSION_CODES.R)
23
- override fun definition() = ModuleDefinition {
24
- // The module will be accessible from `requireNativeModule('ExpoAudioStream')` in JavaScript.
25
- Name("ExpoAudioStream")
26
-
27
- Events(
28
- Constants.AUDIO_EVENT_NAME,
29
- Constants.AUDIO_ANALYSIS_EVENT_NAME,
30
- Constants.RECORDING_INTERRUPTED_EVENT_NAME
31
- )
32
-
33
- // Initialize AudioRecorderManager
34
- initializeManager()
35
-
36
- AsyncFunction("startRecording") { options: Map<String, Any?>, promise: Promise ->
37
- audioRecorderManager.startRecording(options, promise)
38
- }
39
-
40
- Function("clearAudioFiles") {
41
- audioRecorderManager.clearAudioStorage()
42
- }
43
-
44
- Function("status") {
45
- return@Function audioRecorderManager.getStatus()
46
- }
47
-
48
- AsyncFunction("listAudioFiles") { promise: Promise ->
49
- audioRecorderManager.listAudioFiles(promise)
50
- }
51
-
52
- AsyncFunction("pauseRecording") { promise: Promise ->
53
- audioRecorderManager.pauseRecording(promise)
54
- }
55
-
56
- AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
57
- try {
58
- val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
59
-
60
- // Get time or byte range options
61
- val startTimeMs = options["startTimeMs"] as? Number
62
- val endTimeMs = options["endTimeMs"] as? Number
63
- val position = options["position"] as? Number
64
- val length = options["length"] as? Number
65
- val segmentDurationMs = (options["segmentDurationMs"] as? Number)?.toInt() ?: 100
66
-
67
- // Validate ranges - can have time range OR byte range OR no range
68
- val hasTimeRange = startTimeMs != null && endTimeMs != null
69
- val hasByteRange = position != null && length != null
70
-
71
- // Only throw if both ranges are provided
72
- if (hasTimeRange && hasByteRange) {
73
- throw IllegalArgumentException("Cannot specify both time range and byte range")
74
- }
75
-
76
- // Get decoding options with default configuration
77
- val defaultConfig = DecodingConfig(
78
- targetSampleRate = null,
79
- targetChannels = 1, // Default to mono
80
- targetBitDepth = 16,
81
- normalizeAudio = false
82
- )
83
-
84
- val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
85
- DecodingConfig(
86
- targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
87
- targetChannels = decodingOptionsMap["targetChannels"] as? Int,
88
- targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
89
- normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
90
- )
91
- } ?: defaultConfig
92
-
93
- // Load audio data based on range type (or full file if no range specified)
94
- val audioData = when {
95
- hasByteRange -> {
96
- val format = audioProcessor.getAudioFormat(fileUri)
97
- ?: throw IllegalArgumentException("Could not determine audio format")
98
-
99
- // Calculate time range from byte position
100
- val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
101
- val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
102
- val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
103
-
104
- Log.d(Constants.TAG, "Loading audio with byte range: position=$position, length=$length")
105
-
106
- audioProcessor.loadAudioRange(
107
- fileUri = fileUri,
108
- startTimeMs = effectiveStartTimeMs,
109
- endTimeMs = effectiveEndTimeMs,
110
- config = config
111
- )
112
- }
113
- hasTimeRange -> {
114
- Log.d(Constants.TAG, "Loading audio with time range: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs")
115
-
116
- audioProcessor.loadAudioRange(
117
- fileUri = fileUri,
118
- startTimeMs = startTimeMs!!.toLong(),
119
- endTimeMs = endTimeMs!!.toLong(),
120
- config = config
121
- )
122
- }
123
- else -> {
124
- Log.d(Constants.TAG, "Loading entire audio file")
125
- audioProcessor.loadAudioFromAnyFormat(fileUri, config)
126
- }
127
- } ?: throw IllegalStateException("Failed to load audio data")
128
-
129
- val featuresMap = options["features"] as? Map<*, *>
130
- val features = Features.parseFeatureOptions(featuresMap)
131
-
132
- val recordingConfig = RecordingConfig(
133
- sampleRate = audioData.sampleRate,
134
- channels = audioData.channels,
135
- encoding = when (audioData.bitDepth) {
136
- 8 -> "pcm_8bit"
137
- 16 -> "pcm_16bit"
138
- 32 -> "pcm_32bit"
139
- else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
140
- },
141
- segmentDurationMs = segmentDurationMs,
142
- features = features
143
- )
144
-
145
- Log.d(Constants.TAG, "extractAudioAnalysis: $recordingConfig")
146
- audioProcessor.resetCumulativeAmplitudeRange()
147
-
148
- val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
149
- promise.resolve(analysisData.toDictionary())
150
- } catch (e: Exception) {
151
- Log.e(Constants.TAG, "Failed to extract audio analysis: ${e.message}", e)
152
- promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
153
- }
154
- }
155
-
156
- AsyncFunction("resumeRecording") { promise: Promise ->
157
- audioRecorderManager.resumeRecording(promise)
158
- }
159
-
160
- AsyncFunction("stopRecording") { promise: Promise ->
161
- audioRecorderManager.stopRecording(promise)
162
- }
163
-
164
- AsyncFunction("requestPermissionsAsync") { promise: Promise ->
165
- try {
166
- val permissions = mutableListOf(
167
- Manifest.permission.RECORD_AUDIO,
168
- Manifest.permission.READ_PHONE_STATE
169
- )
170
-
171
- // Add foreground service permission for Android 14+
172
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
173
- Log.d(Constants.TAG, "Adding FOREGROUND_SERVICE_MICROPHONE permission request")
174
- permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
175
- }
176
-
177
- Log.d(Constants.TAG, "Requesting permissions: $permissions")
178
- Permissions.askForPermissionsWithPermissionsManager(
179
- appContext.permissions,
180
- promise,
181
- *permissions.toTypedArray()
182
- )
183
- } catch (e: Exception) {
184
- Log.e(Constants.TAG, "Error requesting permissions", e)
185
- promise.reject("PERMISSION_ERROR", "Failed to request permissions: ${e.message}", e)
186
- }
187
- }
188
-
189
- AsyncFunction("getPermissionsAsync") { promise: Promise ->
190
- val permissions = mutableListOf(
191
- Manifest.permission.RECORD_AUDIO,
192
- Manifest.permission.READ_PHONE_STATE
193
- )
194
-
195
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
196
- permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
197
- }
198
-
199
- Permissions.getPermissionsWithPermissionsManager(
200
- appContext.permissions,
201
- promise,
202
- *permissions.toTypedArray()
203
- )
204
- }
205
-
206
- AsyncFunction("requestNotificationPermissionsAsync") { promise: Promise ->
207
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
208
- Permissions.askForPermissionsWithPermissionsManager(
209
- appContext.permissions,
210
- promise,
211
- Manifest.permission.POST_NOTIFICATIONS
212
- )
213
- } else {
214
- promise.resolve(
215
- bundleOf(
216
- "status" to "granted",
217
- "expires" to "never",
218
- "granted" to true
219
- )
220
- )
221
- }
222
- }
223
-
224
- AsyncFunction("getNotificationPermissionsAsync") { promise: Promise ->
225
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
226
- Permissions.getPermissionsWithPermissionsManager(
227
- appContext.permissions,
228
- promise,
229
- Manifest.permission.POST_NOTIFICATIONS
230
- )
231
- } else {
232
- promise.resolve(
233
- bundleOf(
234
- "status" to "granted",
235
- "expires" to "never",
236
- "granted" to true
237
- )
238
- )
239
- }
240
- }
241
-
242
- AsyncFunction("trimAudio") { options: Map<String, Any>, promise: Promise ->
243
- try {
244
- val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
245
- val startTimeMs = requireNotNull(options["startTimeMs"] as? Number)?.toLong()
246
- ?: throw IllegalArgumentException("startTimeMs is required")
247
- val endTimeMs = requireNotNull(options["endTimeMs"] as? Number)?.toLong()
248
- ?: throw IllegalArgumentException("endTimeMs is required")
249
- val outputFileName = options["outputFileName"] as? String
250
-
251
- // Get decoding options
252
- val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
253
- val decodingConfig = if (decodingOptionsMap != null) {
254
- DecodingConfig(
255
- targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
256
- targetChannels = decodingOptionsMap["targetChannels"] as? Int,
257
- targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
258
- normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
259
- )
260
- } else null
261
-
262
- Log.d(Constants.TAG, """
263
- Trimming audio with params:
264
- - fileUri: $fileUri
265
- - startTimeMs: $startTimeMs
266
- - endTimeMs: $endTimeMs
267
- - outputFileName: ${outputFileName ?: "auto-generated"}
268
- """.trimIndent())
269
-
270
- val trimmedAudio = audioProcessor.trimAudio(
271
- fileUri = fileUri,
272
- startTimeMs = startTimeMs,
273
- endTimeMs = endTimeMs,
274
- config = decodingConfig,
275
- outputFileName = outputFileName
276
- ) ?: throw IllegalStateException("Failed to trim audio")
277
-
278
- // Create a map with the available data
279
- val resultMap = mapOf<String, Any>(
280
- "sampleRate" to trimmedAudio.sampleRate,
281
- "channels" to trimmedAudio.channels,
282
- "bitDepth" to trimmedAudio.bitDepth,
283
- "dataSize" to trimmedAudio.data.size
284
- )
285
-
286
- promise.resolve(resultMap)
287
- } catch (e: Exception) {
288
- Log.e(Constants.TAG, "Failed to trim audio: ${e.message}", e)
289
- promise.reject("TRIM_ERROR", e.message ?: "Unknown error", e)
290
- }
291
- }
292
-
293
- OnDestroy {
294
- AudioRecorderManager.destroy()
295
- }
296
-
297
- // Add a new function to check if recording is actually running
298
- AsyncFunction("checkRecordingStatus") { promise: Promise ->
299
- val isServiceRunning = AudioRecordingService::class.java.name.let { className ->
300
- val manager = appContext.reactContext?.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
301
- manager?.getRunningServices(Integer.MAX_VALUE)
302
- ?.any { it.service.className == className }
303
- } ?: false
304
-
305
- val status = audioRecorderManager.getStatus()
306
-
307
- // If service is running but isRecording is false, we need to cleanup
308
- if (isServiceRunning && !status.getBoolean("isRecording")) {
309
- audioRecorderManager.cleanup()
310
- AudioRecordingService.stopService(appContext.reactContext!!)
311
- }
312
-
313
- promise.resolve(status)
314
- }
315
-
316
- AsyncFunction("extractAudioData") { options: Map<String, Any>, promise: Promise ->
317
- try {
318
- val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
319
- val startTimeMs = options["startTimeMs"] as? Number
320
- val endTimeMs = options["endTimeMs"] as? Number
321
- val position = options["position"] as? Number
322
- val length = options["length"] as? Number
323
-
324
- // Validate that we have either time range or byte range, but not both and not neither
325
- val hasTimeRange = startTimeMs != null && endTimeMs != null
326
- val hasByteRange = position != null && length != null
327
-
328
- if (!hasTimeRange && !hasByteRange) {
329
- throw IllegalArgumentException("Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
330
- }
331
- if (hasTimeRange && hasByteRange) {
332
- throw IllegalArgumentException("Cannot specify both time range and byte range")
333
- }
334
-
335
- // Get decoding options
336
- val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
337
- val decodingConfig = if (decodingOptionsMap != null) {
338
- DecodingConfig(
339
- targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
340
- targetChannels = decodingOptionsMap["targetChannels"] as? Int,
341
- targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
342
- normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
343
- ).also {
344
- Log.d(Constants.TAG, """
345
- Using decoding config:
346
- - targetSampleRate: ${it.targetSampleRate ?: "original"}
347
- - targetChannels: ${it.targetChannels ?: "original"}
348
- - targetBitDepth: ${it.targetBitDepth}
349
- - normalizeAudio: ${it.normalizeAudio}
350
- """.trimIndent())
351
- }
352
- } else null
353
-
354
- val audioData = if (hasByteRange) {
355
- val format = audioProcessor.getAudioFormat(fileUri)
356
- ?: throw IllegalArgumentException("Could not determine audio format")
357
-
358
- // Calculate time range from byte position
359
- val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
360
- val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
361
- val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
362
-
363
- Log.d(Constants.TAG, """
364
- Converting byte range to time range:
365
- - position: $position bytes
366
- - length: $length bytes
367
- - bytesPerSecond: $bytesPerSecond
368
- - effectiveStartTimeMs: $effectiveStartTimeMs
369
- - effectiveEndTimeMs: $effectiveEndTimeMs
370
- """.trimIndent())
371
-
372
- audioProcessor.loadAudioRange(
373
- fileUri = fileUri,
374
- startTimeMs = effectiveStartTimeMs,
375
- endTimeMs = effectiveEndTimeMs,
376
- config = decodingConfig
377
- )
378
- } else {
379
- // Must be time range due to earlier validation
380
- Log.d(Constants.TAG, """
381
- Using time range:
382
- - startTimeMs: $startTimeMs
383
- - endTimeMs: $endTimeMs
384
- """.trimIndent())
385
-
386
- audioProcessor.loadAudioRange(
387
- fileUri = fileUri,
388
- startTimeMs = startTimeMs!!.toLong(),
389
- endTimeMs = endTimeMs!!.toLong(),
390
- config = decodingConfig
391
- )
392
- } ?: throw IllegalStateException("Failed to load audio data")
393
-
394
- Log.d(Constants.TAG, """
395
- Audio data loaded successfully:
396
- - data size: ${audioData.data.size} bytes
397
- - sampleRate: ${audioData.sampleRate}
398
- - channels: ${audioData.channels}
399
- - bitDepth: ${audioData.bitDepth}
400
- - durationMs: ${audioData.durationMs}
401
- """.trimIndent())
402
-
403
- val includeNormalizedData = options["includeNormalizedData"] as? Boolean ?: false
404
- val includeBase64Data = options["includeBase64Data"] as? Boolean ?: false
405
- val includeWavHeader = options["includeWavHeader"] as? Boolean ?: false
406
- val bytesPerSample = audioData.bitDepth / 8
407
- val samples = audioData.data.size / (bytesPerSample * audioData.channels)
408
-
409
- // Create the result map
410
- val resultMap = mutableMapOf<String, Any>()
411
-
412
- // Add WAV header if requested
413
- if (includeWavHeader) {
414
- // Use ByteArrayOutputStream to write the WAV header and data
415
- val outputStream = java.io.ByteArrayOutputStream()
416
- val audioFileHandler = AudioFileHandler(appContext.reactContext!!.filesDir)
417
-
418
- // Write the WAV header
419
- audioFileHandler.writeWavHeader(
420
- outputStream,
421
- audioData.sampleRate,
422
- audioData.channels,
423
- audioData.bitDepth
424
- )
425
-
426
- // Write the PCM data
427
- outputStream.write(audioData.data)
428
-
429
- // Get the complete WAV data
430
- val wavData = outputStream.toByteArray()
431
-
432
- resultMap["pcmData"] = wavData
433
- resultMap["hasWavHeader"] = true
434
-
435
- Log.d(Constants.TAG, "Added WAV header to PCM data, total size: ${wavData.size} bytes")
436
- } else {
437
- resultMap["pcmData"] = audioData.data
438
- resultMap["hasWavHeader"] = false
439
- }
440
-
441
- // Add the rest of the data
442
- resultMap.putAll(mapOf(
443
- "sampleRate" to audioData.sampleRate,
444
- "channels" to audioData.channels,
445
- "bitDepth" to audioData.bitDepth,
446
- "durationMs" to audioData.durationMs,
447
- "format" to "pcm_${audioData.bitDepth}bit",
448
- "samples" to samples
449
- ))
450
-
451
- // Add checksum if requested
452
- if (options["computeChecksum"] == true) {
453
- val crc32 = CRC32()
454
- crc32.update(audioData.data)
455
- resultMap["checksum"] = crc32.value.toInt()
456
-
457
- Log.d(Constants.TAG, "Computed CRC32 checksum: ${crc32.value}")
458
- }
459
-
460
- if (includeNormalizedData) {
461
- val float32Data = AudioFormatUtils.convertByteArrayToFloatArray(
462
- audioData.data,
463
- "pcm_${audioData.bitDepth}bit"
464
- )
465
- resultMap["normalizedData"] = float32Data
466
- }
467
-
468
- if (includeBase64Data) {
469
- // Convert the PCM data to a base64 string
470
- val base64Data = android.util.Base64.encodeToString(
471
- audioData.data,
472
- android.util.Base64.NO_WRAP
473
- )
474
- resultMap["base64Data"] = base64Data
475
- }
476
-
477
- promise.resolve(resultMap)
478
- } catch (e: Exception) {
479
- Log.e(Constants.TAG, "Failed to extract audio data: ${e.message}")
480
- Log.e(Constants.TAG, "Stack trace: ${e.stackTraceToString()}")
481
- promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
482
- }
483
- }
484
- }
485
-
486
- private fun initializeManager() {
487
- val androidContext =
488
- appContext.reactContext ?: throw IllegalStateException("Android context not available")
489
- val permissionUtils = PermissionUtils(androidContext)
490
- val audioEncoder = AudioDataEncoder()
491
- audioRecorderManager =
492
- AudioRecorderManager(androidContext, androidContext.filesDir, permissionUtils, audioEncoder, this)
493
- audioRecorderManager = AudioRecorderManager.initialize(
494
- androidContext,
495
- androidContext.filesDir,
496
- permissionUtils,
497
- audioEncoder,
498
- this
499
- )
500
- audioProcessor = AudioProcessor(androidContext.filesDir)
501
- }
502
-
503
-
504
- override fun sendExpoEvent(eventName: String, params: Bundle) {
505
- Log.d(Constants.TAG, "Sending event: $eventName")
506
- this@ExpoAudioStreamModule.sendEvent(eventName, params)
507
- }
508
-
509
- }