@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.
- package/README.md +46 -27
- package/build/index.d.ts +11 -12
- package/build/index.js +44 -10
- package/package.json +49 -110
- package/src/index.ts +18 -33
- package/CHANGELOG.md +0 -195
- package/android/build.gradle +0 -105
- package/android/src/main/AndroidManifest.xml +0 -27
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
- package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
- package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
- package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -1936
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -138
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -20
- package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -509
- package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
- package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
- package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
- package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
- package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
- package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
- package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
- package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
- package/android/src/main/res/drawable/ic_microphone.xml +0 -13
- package/android/src/main/res/drawable/ic_pause.xml +0 -10
- package/android/src/main/res/drawable/ic_play.xml +0 -10
- package/android/src/main/res/drawable/ic_stop.xml +0 -10
- package/android/src/main/res/layout/notification_recording.xml +0 -37
- package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
- package/app.plugin.js +0 -1
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -144
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -78
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
- package/build/AudioAnalysis/extractAudioAnalysis.js +0 -229
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
- package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
- package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
- package/build/AudioAnalysis/extractWaveform.js +0 -11
- package/build/AudioAnalysis/extractWaveform.js.map +0 -1
- package/build/AudioRecorder.provider.d.ts +0 -11
- package/build/AudioRecorder.provider.d.ts.map +0 -1
- package/build/AudioRecorder.provider.js +0 -37
- package/build/AudioRecorder.provider.js.map +0 -1
- package/build/ExpoAudioStream.native.d.ts +0 -3
- package/build/ExpoAudioStream.native.d.ts.map +0 -1
- package/build/ExpoAudioStream.native.js +0 -6
- package/build/ExpoAudioStream.native.js.map +0 -1
- package/build/ExpoAudioStream.types.d.ts +0 -206
- package/build/ExpoAudioStream.types.d.ts.map +0 -1
- package/build/ExpoAudioStream.types.js +0 -2
- package/build/ExpoAudioStream.types.js.map +0 -1
- package/build/ExpoAudioStream.web.d.ts +0 -59
- package/build/ExpoAudioStream.web.d.ts.map +0 -1
- package/build/ExpoAudioStream.web.js +0 -285
- package/build/ExpoAudioStream.web.js.map +0 -1
- package/build/ExpoAudioStreamModule.d.ts +0 -3
- package/build/ExpoAudioStreamModule.d.ts.map +0 -1
- package/build/ExpoAudioStreamModule.js +0 -239
- package/build/ExpoAudioStreamModule.js.map +0 -1
- package/build/WebRecorder.web.d.ts +0 -119
- package/build/WebRecorder.web.d.ts.map +0 -1
- package/build/WebRecorder.web.js +0 -436
- package/build/WebRecorder.web.js.map +0 -1
- package/build/constants.d.ts +0 -11
- package/build/constants.d.ts.map +0 -1
- package/build/constants.js +0 -14
- package/build/constants.js.map +0 -1
- package/build/events.d.ts +0 -26
- package/build/events.d.ts.map +0 -1
- package/build/events.js +0 -21
- package/build/events.js.map +0 -1
- package/build/index.d.ts.map +0 -1
- package/build/index.js.map +0 -1
- package/build/useAudioRecorder.d.ts +0 -21
- package/build/useAudioRecorder.d.ts.map +0 -1
- package/build/useAudioRecorder.js +0 -427
- package/build/useAudioRecorder.js.map +0 -1
- package/build/utils/BlobFix.d.ts +0 -9
- package/build/utils/BlobFix.d.ts.map +0 -1
- package/build/utils/BlobFix.js +0 -498
- package/build/utils/BlobFix.js.map +0 -1
- package/build/utils/audioProcessing.d.ts +0 -24
- package/build/utils/audioProcessing.d.ts.map +0 -1
- package/build/utils/audioProcessing.js +0 -133
- package/build/utils/audioProcessing.js.map +0 -1
- package/build/utils/concatenateBuffers.d.ts +0 -8
- package/build/utils/concatenateBuffers.d.ts.map +0 -1
- package/build/utils/concatenateBuffers.js +0 -21
- package/build/utils/concatenateBuffers.js.map +0 -1
- package/build/utils/convertPCMToFloat32.d.ts +0 -13
- package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
- package/build/utils/convertPCMToFloat32.js +0 -120
- package/build/utils/convertPCMToFloat32.js.map +0 -1
- package/build/utils/encodingToBitDepth.d.ts +0 -5
- package/build/utils/encodingToBitDepth.d.ts.map +0 -1
- package/build/utils/encodingToBitDepth.js +0 -13
- package/build/utils/encodingToBitDepth.js.map +0 -1
- package/build/utils/getWavFileInfo.d.ts +0 -26
- package/build/utils/getWavFileInfo.d.ts.map +0 -1
- package/build/utils/getWavFileInfo.js +0 -92
- package/build/utils/getWavFileInfo.js.map +0 -1
- package/build/utils/writeWavHeader.d.ts +0 -49
- package/build/utils/writeWavHeader.d.ts.map +0 -1
- package/build/utils/writeWavHeader.js +0 -91
- package/build/utils/writeWavHeader.js.map +0 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
- package/build/workers/InlineFeaturesExtractor.web.js +0 -828
- package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
- package/build/workers/inlineAudioWebWorker.web.js +0 -157
- package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
- package/expo-module.config.json +0 -9
- package/ios/AudioAnalysisData.swift +0 -74
- package/ios/AudioNotificationManager.swift +0 -135
- package/ios/AudioProcessingHelpers.swift +0 -743
- package/ios/AudioProcessor.swift +0 -858
- package/ios/AudioStreamError.swift +0 -7
- package/ios/AudioStreamManager.swift +0 -1708
- package/ios/AudioStreamManagerDelegate.swift +0 -16
- package/ios/DataPoint.swift +0 -54
- package/ios/DecodingConfig.swift +0 -47
- package/ios/ExpoAudioStream.podspec +0 -27
- package/ios/ExpoAudioStreamModule.swift +0 -698
- package/ios/FFT.swift +0 -62
- package/ios/Features.swift +0 -95
- package/ios/Logger.swift +0 -7
- package/ios/NotificationExtension.swift +0 -15
- package/ios/RecordingResult.swift +0 -22
- package/ios/RecordingSettings.swift +0 -265
- package/ios/WaveformExtractor.swift +0 -105
- package/plugin/build/index.d.ts +0 -21
- package/plugin/build/index.js +0 -191
- package/plugin/src/index.ts +0 -278
- package/plugin/tsconfig.json +0 -10
- package/plugin/tsconfig.tsbuildinfo +0 -1
- package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -165
- package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -370
- package/src/AudioAnalysis/extractWaveform.ts +0 -22
- package/src/AudioRecorder.provider.tsx +0 -54
- package/src/ExpoAudioStream.native.ts +0 -6
- package/src/ExpoAudioStream.types.ts +0 -329
- package/src/ExpoAudioStream.web.ts +0 -359
- package/src/ExpoAudioStreamModule.ts +0 -286
- package/src/WebRecorder.web.ts +0 -580
- package/src/constants.ts +0 -18
- package/src/events.ts +0 -60
- package/src/useAudioRecorder.tsx +0 -620
- package/src/utils/BlobFix.ts +0 -559
- package/src/utils/audioProcessing.ts +0 -205
- package/src/utils/concatenateBuffers.ts +0 -24
- package/src/utils/convertPCMToFloat32.ts +0 -170
- package/src/utils/encodingToBitDepth.ts +0 -18
- package/src/utils/getWavFileInfo.ts +0 -132
- package/src/utils/writeWavHeader.ts +0 -114
- package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
- package/src/workers/inlineAudioWebWorker.web.tsx +0 -156
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// packages/expo-audio-stream/android/src/main/java/net/siteed/audiostream/FFT.kt
|
|
2
|
-
package net.siteed.audiostream
|
|
3
|
-
|
|
4
|
-
import kotlin.math.PI
|
|
5
|
-
import kotlin.math.cos
|
|
6
|
-
import kotlin.math.sin
|
|
7
|
-
import kotlin.math.sqrt
|
|
8
|
-
|
|
9
|
-
class FFT(private val n: Int) {
|
|
10
|
-
private val cosTable = FloatArray(n / 2)
|
|
11
|
-
private val sinTable = FloatArray(n / 2)
|
|
12
|
-
private val hannWindow = FloatArray(n)
|
|
13
|
-
|
|
14
|
-
init {
|
|
15
|
-
// Precompute trig tables
|
|
16
|
-
for (i in 0 until n / 2) {
|
|
17
|
-
cosTable[i] = cos(2.0 * PI * i / n).toFloat()
|
|
18
|
-
sinTable[i] = sin(2.0 * PI * i / n).toFloat()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Precompute normalized Hann window to match vDSP
|
|
22
|
-
val normalizationFactor = sqrt(2.0f / n) // Match vDSP normalization
|
|
23
|
-
for (i in hannWindow.indices) {
|
|
24
|
-
hannWindow[i] = normalizationFactor * 0.5f * (1 - cos(2.0 * PI * i / (n - 1))).toFloat()
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
fun processSegment(segment: FloatArray): FloatArray {
|
|
29
|
-
// Pad or truncate input to match FFT length
|
|
30
|
-
val paddedSegment = if (segment.size < n) {
|
|
31
|
-
segment + FloatArray(n - segment.size)
|
|
32
|
-
} else {
|
|
33
|
-
segment.copyOf(n)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Apply normalized Hann window
|
|
37
|
-
for (i in paddedSegment.indices) {
|
|
38
|
-
paddedSegment[i] *= hannWindow[i]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Perform FFT
|
|
42
|
-
realForward(paddedSegment)
|
|
43
|
-
|
|
44
|
-
return paddedSegment
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
fun realForward(data: FloatArray) {
|
|
48
|
-
realForwardRecursive(data)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private fun realForwardRecursive(data: FloatArray) {
|
|
52
|
-
val n = data.size
|
|
53
|
-
if (n <= 1) return
|
|
54
|
-
|
|
55
|
-
val even = FloatArray(n / 2)
|
|
56
|
-
val odd = FloatArray(n / 2)
|
|
57
|
-
|
|
58
|
-
for (i in 0 until n / 2) {
|
|
59
|
-
even[i] = data[2 * i]
|
|
60
|
-
odd[i] = data[2 * i + 1]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
realForwardRecursive(even)
|
|
64
|
-
realForwardRecursive(odd)
|
|
65
|
-
|
|
66
|
-
for (i in 0 until n / 2) {
|
|
67
|
-
val t = cosTable[i] * odd[i] - sinTable[i] * even[i]
|
|
68
|
-
val u = sinTable[i] * odd[i] + cosTable[i] * even[i]
|
|
69
|
-
data[i] = even[i] + t
|
|
70
|
-
data[i + n / 2] = even[i] - t
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
fun realInverse(powerSpectrum: FloatArray, output: FloatArray) {
|
|
75
|
-
// Copy power spectrum to complex format for inverse FFT
|
|
76
|
-
val complexData = FloatArray(n * 2)
|
|
77
|
-
for (i in 0 until n/2 + 1) {
|
|
78
|
-
complexData[2 * i] = powerSpectrum[i]
|
|
79
|
-
if (2 * i + 1 < complexData.size) {
|
|
80
|
-
complexData[2 * i + 1] = 0f
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Conjugate for inverse FFT
|
|
85
|
-
for (i in 0 until n) {
|
|
86
|
-
if (2 * i + 1 < complexData.size) {
|
|
87
|
-
complexData[2 * i + 1] = -complexData[2 * i + 1]
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Perform forward FFT (which is inverse when input is conjugated)
|
|
92
|
-
realForward(complexData)
|
|
93
|
-
|
|
94
|
-
// Copy real part to output and conjugate again
|
|
95
|
-
for (i in 0 until n) {
|
|
96
|
-
output[i] = complexData[2 * i] / n
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostream
|
|
2
|
-
|
|
3
|
-
import android.os.Bundle
|
|
4
|
-
import androidx.core.os.bundleOf
|
|
5
|
-
|
|
6
|
-
data class Features(
|
|
7
|
-
val energy: Float = 0f,
|
|
8
|
-
val mfcc: List<Float> = emptyList(),
|
|
9
|
-
val rms: Float = 0f,
|
|
10
|
-
val minAmplitude: Float = 0f,
|
|
11
|
-
val maxAmplitude: Float = 0f,
|
|
12
|
-
val zcr: Float = 0f,
|
|
13
|
-
val spectralCentroid: Float = 0f,
|
|
14
|
-
val spectralFlatness: Float = 0f,
|
|
15
|
-
val spectralRollOff: Float = 0f,
|
|
16
|
-
val spectralBandwidth: Float = 0f,
|
|
17
|
-
val tempo: Float = 0f,
|
|
18
|
-
val hnr: Float = 0f,
|
|
19
|
-
val melSpectrogram: List<Float> = emptyList(),
|
|
20
|
-
val chromagram: List<Float> = emptyList(),
|
|
21
|
-
val spectralContrast: List<Float> = emptyList(),
|
|
22
|
-
val tonnetz: List<Float> = emptyList(),
|
|
23
|
-
val pitch: Float = 0f,
|
|
24
|
-
val crc32: Long? = null
|
|
25
|
-
) {
|
|
26
|
-
fun toDictionary(): Map<String, Any> {
|
|
27
|
-
val baseMap = mapOf(
|
|
28
|
-
"energy" to energy,
|
|
29
|
-
"mfcc" to mfcc,
|
|
30
|
-
"rms" to rms,
|
|
31
|
-
"minAmplitude" to minAmplitude,
|
|
32
|
-
"maxAmplitude" to maxAmplitude,
|
|
33
|
-
"zcr" to zcr,
|
|
34
|
-
"spectralCentroid" to spectralCentroid,
|
|
35
|
-
"spectralFlatness" to spectralFlatness,
|
|
36
|
-
"spectralRollOff" to spectralRollOff,
|
|
37
|
-
"spectralBandwidth" to spectralBandwidth,
|
|
38
|
-
"tempo" to tempo,
|
|
39
|
-
"hnr" to hnr,
|
|
40
|
-
"melSpectrogram" to melSpectrogram,
|
|
41
|
-
"chromagram" to chromagram,
|
|
42
|
-
"spectralContrast" to spectralContrast,
|
|
43
|
-
"tonnetz" to tonnetz,
|
|
44
|
-
"pitch" to pitch,
|
|
45
|
-
"crc32" to (crc32 ?: 0)
|
|
46
|
-
)
|
|
47
|
-
return baseMap.filterValues { it != null }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
fun toBundle(): Bundle {
|
|
51
|
-
return bundleOf(
|
|
52
|
-
"energy" to energy,
|
|
53
|
-
"mfcc" to mfcc,
|
|
54
|
-
"rms" to rms,
|
|
55
|
-
"minAmplitude" to minAmplitude,
|
|
56
|
-
"maxAmplitude" to maxAmplitude,
|
|
57
|
-
"zcr" to zcr,
|
|
58
|
-
"spectralCentroid" to spectralCentroid,
|
|
59
|
-
"spectralFlatness" to spectralFlatness,
|
|
60
|
-
"spectralRollOff" to spectralRollOff,
|
|
61
|
-
"spectralBandwidth" to spectralBandwidth,
|
|
62
|
-
"tempo" to tempo,
|
|
63
|
-
"hnr" to hnr,
|
|
64
|
-
"melSpectrogram" to melSpectrogram,
|
|
65
|
-
"chromagram" to chromagram,
|
|
66
|
-
"spectralContrast" to spectralContrast,
|
|
67
|
-
"tonnetz" to tonnetz,
|
|
68
|
-
"pitch" to pitch,
|
|
69
|
-
"crc32" to (crc32 ?: 0)
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
companion object {
|
|
74
|
-
fun parseFeatureOptions(options: Map<*, *>?): Map<String, Boolean> {
|
|
75
|
-
return options?.let { map ->
|
|
76
|
-
mapOf(
|
|
77
|
-
"energy" to (map["energy"] as? Boolean ?: false),
|
|
78
|
-
"mfcc" to (map["mfcc"] as? Boolean ?: false),
|
|
79
|
-
"rms" to (map["rms"] as? Boolean ?: false),
|
|
80
|
-
"zcr" to (map["zcr"] as? Boolean ?: false),
|
|
81
|
-
"dB" to (map["dB"] as? Boolean ?: false),
|
|
82
|
-
"spectralCentroid" to (map["spectralCentroid"] as? Boolean ?: false),
|
|
83
|
-
"spectralFlatness" to (map["spectralFlatness"] as? Boolean ?: false),
|
|
84
|
-
"spectralRollOff" to (map["spectralRollOff"] as? Boolean ?: false),
|
|
85
|
-
"spectralBandwidth" to (map["spectralBandwidth"] as? Boolean ?: false),
|
|
86
|
-
"chromagram" to (map["chromagram"] as? Boolean ?: false),
|
|
87
|
-
"tempo" to (map["tempo"] as? Boolean ?: false),
|
|
88
|
-
"hnr" to (map["hnr"] as? Boolean ?: false),
|
|
89
|
-
"melSpectrogram" to (map["melSpectrogram"] as? Boolean ?: false),
|
|
90
|
-
"spectralContrast" to (map["spectralContrast"] as? Boolean ?: false),
|
|
91
|
-
"tonnetz" to (map["tonnetz"] as? Boolean ?: false),
|
|
92
|
-
"pitch" to (map["pitch"] as? Boolean ?: false),
|
|
93
|
-
"crc32" to (map["crc32"] as? Boolean ?: false)
|
|
94
|
-
)
|
|
95
|
-
} ?: emptyMap()
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostream
|
|
2
|
-
|
|
3
|
-
data class NotificationConfig(
|
|
4
|
-
val title: String = "Recording...",
|
|
5
|
-
val text: String = "",
|
|
6
|
-
val icon: String? = null,
|
|
7
|
-
val channelId: String = "audio_recording_channel",
|
|
8
|
-
val notificationId: Int = 1,
|
|
9
|
-
val actions: List<NotificationAction> = emptyList(),
|
|
10
|
-
val channelName: String = "Audio Recording",
|
|
11
|
-
val channelDescription: String = "Shows audio recording status",
|
|
12
|
-
val waveform: WaveformConfig? = null,
|
|
13
|
-
val lightColor: String = "#FF0000",
|
|
14
|
-
val priority: String = "high",
|
|
15
|
-
val accentColor: String? = null
|
|
16
|
-
) {
|
|
17
|
-
companion object {
|
|
18
|
-
fun fromMap(map: Map<String, Any?>?): NotificationConfig {
|
|
19
|
-
if (map == null) return NotificationConfig()
|
|
20
|
-
|
|
21
|
-
val androidMap = map["android"] as? Map<String, Any?> ?: emptyMap()
|
|
22
|
-
|
|
23
|
-
return NotificationConfig(
|
|
24
|
-
title = map["title"] as? String ?: "Recording...",
|
|
25
|
-
text = map["text"] as? String ?: "",
|
|
26
|
-
icon = map["icon"] as? String,
|
|
27
|
-
channelId = androidMap["channelId"] as? String ?: "audio_recording_channel",
|
|
28
|
-
notificationId = (androidMap["notificationId"] as? Number)?.toInt() ?: 1,
|
|
29
|
-
actions = parseNotificationActions(androidMap["actions"] as? List<Map<String, Any?>>),
|
|
30
|
-
channelName = androidMap["channelName"] as? String ?: "Audio Recording",
|
|
31
|
-
channelDescription = androidMap["channelDescription"] as? String ?: "Shows audio recording status",
|
|
32
|
-
waveform = parseWaveformConfig(androidMap["waveform"] as? Map<String, Any?>),
|
|
33
|
-
lightColor = androidMap["lightColor"] as? String ?: "#FF0000",
|
|
34
|
-
priority = androidMap["priority"] as? String ?: "high",
|
|
35
|
-
accentColor = androidMap["accentColor"] as? String
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private fun parseNotificationActions(actionsList: List<Map<String, Any?>>?): List<NotificationAction> {
|
|
40
|
-
return actionsList?.mapNotNull { actionMap ->
|
|
41
|
-
if (actionMap["title"] != null && actionMap["identifier"] != null) {
|
|
42
|
-
NotificationAction(
|
|
43
|
-
title = actionMap["title"] as String,
|
|
44
|
-
icon = actionMap["icon"] as? String,
|
|
45
|
-
intentAction = actionMap["identifier"] as String
|
|
46
|
-
)
|
|
47
|
-
} else null
|
|
48
|
-
} ?: emptyList()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private fun parseWaveformConfig(waveformMap: Map<String, Any?>?): WaveformConfig? {
|
|
52
|
-
if (waveformMap == null) return null
|
|
53
|
-
|
|
54
|
-
return WaveformConfig(
|
|
55
|
-
color = waveformMap["color"] as? String ?: "#FFFFFF",
|
|
56
|
-
opacity = (waveformMap["opacity"] as? Number)?.toFloat() ?: 1.0f,
|
|
57
|
-
strokeWidth = (waveformMap["strokeWidth"] as? Number)?.toFloat() ?: 1.5f,
|
|
58
|
-
style = waveformMap["style"] as? String ?: "stroke",
|
|
59
|
-
mirror = waveformMap["mirror"] as? Boolean ?: true,
|
|
60
|
-
height = (waveformMap["height"] as? Number)?.toInt() ?: 64
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
data class NotificationAction(
|
|
67
|
-
val title: String,
|
|
68
|
-
val icon: String? = null,
|
|
69
|
-
val intentAction: String
|
|
70
|
-
)
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostream
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.content.pm.PackageManager
|
|
5
|
-
import android.os.Build
|
|
6
|
-
import androidx.core.content.ContextCompat
|
|
7
|
-
import android.Manifest
|
|
8
|
-
import android.util.Log
|
|
9
|
-
|
|
10
|
-
class PermissionUtils(private val context: Context) {
|
|
11
|
-
fun checkRecordingPermission(): Boolean {
|
|
12
|
-
val hasRecordPermission = ContextCompat.checkSelfPermission(
|
|
13
|
-
context,
|
|
14
|
-
Manifest.permission.RECORD_AUDIO
|
|
15
|
-
) == PackageManager.PERMISSION_GRANTED
|
|
16
|
-
|
|
17
|
-
Log.d(Constants.TAG, "RECORD_AUDIO permission: $hasRecordPermission")
|
|
18
|
-
|
|
19
|
-
// Check for foreground service permission on Android 14+
|
|
20
|
-
val hasForegroundService = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
21
|
-
val result = ContextCompat.checkSelfPermission(
|
|
22
|
-
context,
|
|
23
|
-
Manifest.permission.FOREGROUND_SERVICE_MICROPHONE
|
|
24
|
-
) == PackageManager.PERMISSION_GRANTED
|
|
25
|
-
Log.d(Constants.TAG, "FOREGROUND_SERVICE_MICROPHONE permission: $result (Android 14+)")
|
|
26
|
-
result
|
|
27
|
-
} else {
|
|
28
|
-
Log.d(Constants.TAG, "FOREGROUND_SERVICE_MICROPHONE not required (Android < 14)")
|
|
29
|
-
true
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
val result = hasRecordPermission && hasForegroundService
|
|
33
|
-
Log.d(Constants.TAG, "Final recording permission result: $result")
|
|
34
|
-
return result
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
fun checkNotificationPermission(): Boolean {
|
|
38
|
-
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
39
|
-
val hasPermission = ContextCompat.checkSelfPermission(
|
|
40
|
-
context,
|
|
41
|
-
Manifest.permission.POST_NOTIFICATIONS
|
|
42
|
-
) == PackageManager.PERMISSION_GRANTED
|
|
43
|
-
Log.d(Constants.TAG, "POST_NOTIFICATIONS permission: $hasPermission (Android 13+)")
|
|
44
|
-
hasPermission
|
|
45
|
-
} else {
|
|
46
|
-
Log.d(Constants.TAG, "POST_NOTIFICATIONS not required (Android < 13)")
|
|
47
|
-
true
|
|
48
|
-
}
|
|
49
|
-
return result
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
fun checkPhoneStatePermission(): Boolean {
|
|
53
|
-
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
54
|
-
context.checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
|
|
55
|
-
} else {
|
|
56
|
-
true
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostream
|
|
2
|
-
|
|
3
|
-
import android.content.BroadcastReceiver
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.content.Intent
|
|
6
|
-
import android.util.Log
|
|
7
|
-
import expo.modules.kotlin.Promise
|
|
8
|
-
import java.util.concurrent.atomic.AtomicBoolean
|
|
9
|
-
|
|
10
|
-
class RecordingActionReceiver : BroadcastReceiver() {
|
|
11
|
-
companion object {
|
|
12
|
-
const val ACTION_PAUSE_RECORDING = "net.siteed.audiostream.PAUSE_RECORDING"
|
|
13
|
-
const val ACTION_RESUME_RECORDING = "net.siteed.audiostream.RESUME_RECORDING"
|
|
14
|
-
private val isProcessingAction = AtomicBoolean(false)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
override fun onReceive(context: Context, intent: Intent) {
|
|
18
|
-
when (intent.action) {
|
|
19
|
-
ACTION_PAUSE_RECORDING, ACTION_RESUME_RECORDING -> handleRecordingAction(intent.action)
|
|
20
|
-
else -> Log.w("RecordingActionReceiver", "Unknown action: ${intent.action}")
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private fun handleRecordingAction(action: String?) {
|
|
25
|
-
if (!isProcessingAction.compareAndSet(false, true)) {
|
|
26
|
-
Log.d("RecordingActionReceiver", "Action already in progress, skipping")
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
val audioRecorderManager = AudioRecorderManager.getInstance()
|
|
32
|
-
if (audioRecorderManager == null) {
|
|
33
|
-
Log.e("RecordingActionReceiver", "AudioRecorderManager instance is null")
|
|
34
|
-
isProcessingAction.set(false)
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
val notificationPromise = object : Promise {
|
|
39
|
-
override fun resolve(value: Any?) {
|
|
40
|
-
Log.d("RecordingActionReceiver", "$action completed successfully")
|
|
41
|
-
isProcessingAction.set(false)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
45
|
-
Log.e("RecordingActionReceiver", "$action failed: $message", cause)
|
|
46
|
-
isProcessingAction.set(false)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
when (action) {
|
|
51
|
-
ACTION_PAUSE_RECORDING -> audioRecorderManager.pauseRecording(notificationPromise)
|
|
52
|
-
ACTION_RESUME_RECORDING -> audioRecorderManager.resumeRecording(notificationPromise)
|
|
53
|
-
}
|
|
54
|
-
} catch (e: Exception) {
|
|
55
|
-
Log.e("RecordingActionReceiver", "Error processing $action", e)
|
|
56
|
-
isProcessingAction.set(false)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostream
|
|
2
|
-
|
|
3
|
-
import android.media.AudioFormat
|
|
4
|
-
import android.os.Build
|
|
5
|
-
import java.io.File
|
|
6
|
-
|
|
7
|
-
data class RecordingConfig(
|
|
8
|
-
val sampleRate: Int = Constants.DEFAULT_SAMPLE_RATE,
|
|
9
|
-
val channels: Int = 1,
|
|
10
|
-
val encoding: String = "pcm_16bit",
|
|
11
|
-
val keepAwake: Boolean = true,
|
|
12
|
-
val interval: Long = Constants.DEFAULT_INTERVAL,
|
|
13
|
-
val intervalAnalysis: Long = Constants.DEFAULT_INTERVAL_ANALYSIS,
|
|
14
|
-
val enableProcessing: Boolean = false,
|
|
15
|
-
val segmentDurationMs: Int = 100,
|
|
16
|
-
val showNotification: Boolean = false,
|
|
17
|
-
val showWaveformInNotification: Boolean = false,
|
|
18
|
-
val notification: NotificationConfig = NotificationConfig(),
|
|
19
|
-
val features: Map<String, Boolean> = emptyMap(),
|
|
20
|
-
val enableCompressedOutput: Boolean = false,
|
|
21
|
-
val compressedFormat: String = "opus",
|
|
22
|
-
val compressedBitRate: Int = 24000,
|
|
23
|
-
val autoResumeAfterInterruption: Boolean = false,
|
|
24
|
-
val outputDirectory: String? = null,
|
|
25
|
-
val filename: String? = null,
|
|
26
|
-
) {
|
|
27
|
-
companion object {
|
|
28
|
-
fun fromMap(options: Map<String, Any?>?): Result<Pair<RecordingConfig, AudioFormatInfo>> {
|
|
29
|
-
if (options == null) {
|
|
30
|
-
val defaultConfig = RecordingConfig()
|
|
31
|
-
val defaultFormat = AudioFormatInfo(
|
|
32
|
-
format = AudioFormat.ENCODING_PCM_16BIT,
|
|
33
|
-
mimeType = "audio/wav",
|
|
34
|
-
fileExtension = "wav"
|
|
35
|
-
)
|
|
36
|
-
return Result.success(Pair(defaultConfig, defaultFormat))
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Extract features using type-safe helper
|
|
40
|
-
val features = options.getTypedMap<Boolean>("features") { it is Boolean }
|
|
41
|
-
|
|
42
|
-
// Parse notification config using type-safe helper
|
|
43
|
-
val notificationMap = options.getTypedMap<Any?>("notification") { true }
|
|
44
|
-
val notificationConfig = NotificationConfig.fromMap(notificationMap)
|
|
45
|
-
|
|
46
|
-
// Parse compression config
|
|
47
|
-
val compressionMap = options.getTypedMap<Any?>("compression") { true }
|
|
48
|
-
val enableCompressedOutput = compressionMap["enabled"] as? Boolean ?: false
|
|
49
|
-
val compressedFormat = (compressionMap["format"] as? String)?.lowercase() ?: "aac"
|
|
50
|
-
val compressedBitRate = (compressionMap["bitrate"] as? Number)?.toInt() ?: 128000
|
|
51
|
-
|
|
52
|
-
// Validate bitrate if compression is enabled
|
|
53
|
-
if (enableCompressedOutput) {
|
|
54
|
-
when {
|
|
55
|
-
compressedBitRate < 8000 -> return Result.failure(
|
|
56
|
-
IllegalArgumentException("Bitrate must be at least 8000 bps")
|
|
57
|
-
)
|
|
58
|
-
compressedBitRate > 960000 -> return Result.failure(
|
|
59
|
-
IllegalArgumentException("Bitrate cannot exceed 960000 bps")
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Only validate directory if it's provided
|
|
65
|
-
val outputDirectory = options["outputDirectory"] as? String
|
|
66
|
-
if (outputDirectory != null) {
|
|
67
|
-
// Clean up the directory path by removing file:// protocol and normalizing
|
|
68
|
-
val cleanDirectory = outputDirectory
|
|
69
|
-
.replace(Regex("^file://"), "")
|
|
70
|
-
.trim('/')
|
|
71
|
-
.replace("//", "/")
|
|
72
|
-
|
|
73
|
-
val directory = File(cleanDirectory)
|
|
74
|
-
if (!directory.exists()) {
|
|
75
|
-
return Result.failure(IllegalArgumentException("Directory does not exist: $cleanDirectory"))
|
|
76
|
-
}
|
|
77
|
-
if (!directory.isDirectory) {
|
|
78
|
-
return Result.failure(IllegalArgumentException("Path is not a directory: $cleanDirectory"))
|
|
79
|
-
}
|
|
80
|
-
if (!directory.canWrite()) {
|
|
81
|
-
return Result.failure(IllegalArgumentException("Directory is not writable: $cleanDirectory"))
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Initialize the recording configuration with cleaned directory path
|
|
86
|
-
val tempRecordingConfig = RecordingConfig(
|
|
87
|
-
sampleRate = options.getNumberOrDefault("sampleRate", Constants.DEFAULT_SAMPLE_RATE),
|
|
88
|
-
channels = options.getNumberOrDefault("channels", 1),
|
|
89
|
-
encoding = options.getStringOrDefault("encoding", "pcm_16bit"),
|
|
90
|
-
keepAwake = options.getBooleanOrDefault("keepAwake", true),
|
|
91
|
-
interval = options.getNumberOrDefault("interval", Constants.DEFAULT_INTERVAL),
|
|
92
|
-
intervalAnalysis = options.getNumberOrDefault("intervalAnalysis", Constants.DEFAULT_INTERVAL_ANALYSIS),
|
|
93
|
-
enableProcessing = options.getBooleanOrDefault("enableProcessing", false),
|
|
94
|
-
segmentDurationMs = options.getNumberOrDefault("segmentDurationMs", 100),
|
|
95
|
-
showNotification = options.getBooleanOrDefault("showNotification", false),
|
|
96
|
-
showWaveformInNotification = options.getBooleanOrDefault("showWaveformInNotification", false),
|
|
97
|
-
notification = notificationConfig,
|
|
98
|
-
features = features,
|
|
99
|
-
enableCompressedOutput = enableCompressedOutput,
|
|
100
|
-
compressedFormat = compressedFormat,
|
|
101
|
-
compressedBitRate = compressedBitRate,
|
|
102
|
-
autoResumeAfterInterruption = options.getBooleanOrDefault("autoResumeAfterInterruption", false),
|
|
103
|
-
outputDirectory = outputDirectory?.let {
|
|
104
|
-
it.replace(Regex("^file://"), "")
|
|
105
|
-
.trim('/')
|
|
106
|
-
.replace("//", "/")
|
|
107
|
-
},
|
|
108
|
-
filename = options["filename"] as? String
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
// Validate sample rate and channels
|
|
112
|
-
if (tempRecordingConfig.sampleRate !in listOf(16000, 44100, 48000)) {
|
|
113
|
-
return Result.failure(
|
|
114
|
-
IllegalArgumentException("Sample rate must be one of 16000, 44100, or 48000 Hz")
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
if (tempRecordingConfig.channels !in 1..2) {
|
|
118
|
-
return Result.failure(
|
|
119
|
-
IllegalArgumentException("Channels must be either 1 (Mono) or 2 (Stereo)")
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Set encoding and file extension
|
|
124
|
-
val audioFormatInfo = when (tempRecordingConfig.encoding) {
|
|
125
|
-
"pcm_8bit" -> AudioFormatInfo(
|
|
126
|
-
format = AudioFormat.ENCODING_PCM_8BIT,
|
|
127
|
-
mimeType = "audio/wav",
|
|
128
|
-
fileExtension = "wav"
|
|
129
|
-
)
|
|
130
|
-
"pcm_16bit" -> AudioFormatInfo(
|
|
131
|
-
format = AudioFormat.ENCODING_PCM_16BIT,
|
|
132
|
-
mimeType = "audio/wav",
|
|
133
|
-
fileExtension = "wav"
|
|
134
|
-
)
|
|
135
|
-
"pcm_32bit" -> AudioFormatInfo(
|
|
136
|
-
format = AudioFormat.ENCODING_PCM_FLOAT,
|
|
137
|
-
mimeType = "audio/wav",
|
|
138
|
-
fileExtension = "wav"
|
|
139
|
-
)
|
|
140
|
-
"opus" -> {
|
|
141
|
-
if (Build.VERSION.SDK_INT < 29) {
|
|
142
|
-
return Result.failure(
|
|
143
|
-
IllegalArgumentException("Opus encoding not supported on this Android version.")
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
AudioFormatInfo(
|
|
147
|
-
format = if (Build.VERSION.SDK_INT >= 29) 20 else AudioFormat.ENCODING_DEFAULT, // 20 is ENCODING_OPUS
|
|
148
|
-
mimeType = "audio/opus",
|
|
149
|
-
fileExtension = "opus"
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
"aac_lc" -> AudioFormatInfo(
|
|
153
|
-
format = AudioFormat.ENCODING_AAC_LC,
|
|
154
|
-
mimeType = "audio/aac",
|
|
155
|
-
fileExtension = "aac"
|
|
156
|
-
)
|
|
157
|
-
else -> AudioFormatInfo(
|
|
158
|
-
format = AudioFormat.ENCODING_DEFAULT,
|
|
159
|
-
mimeType = "audio/wav",
|
|
160
|
-
fileExtension = "wav"
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return Result.success(Pair(tempRecordingConfig, audioFormatInfo))
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Extension functions for type-safe map access
|
|
170
|
-
private inline fun <reified T> Map<String, Any?>.getTypedMap(
|
|
171
|
-
key: String,
|
|
172
|
-
predicate: (Any?) -> Boolean
|
|
173
|
-
): Map<String, T> {
|
|
174
|
-
return (this[key] as? Map<*, *>)?.mapNotNull { (k, v) ->
|
|
175
|
-
if (k is String && predicate(v)) {
|
|
176
|
-
k to (v as T)
|
|
177
|
-
} else null
|
|
178
|
-
}?.toMap() ?: emptyMap()
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private fun Map<String, Any?>.getStringOrDefault(key: String, default: String): String {
|
|
182
|
-
return this[key] as? String ?: default
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private fun Map<String, Any?>.getBooleanOrDefault(key: String, default: Boolean): Boolean {
|
|
186
|
-
return this[key] as? Boolean ?: default
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private fun Map<String, Any?>.getNumberOrDefault(key: String, default: Int): Int {
|
|
190
|
-
return (this[key] as? Number)?.toInt() ?: default
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private fun Map<String, Any?>.getNumberOrDefault(key: String, default: Long): Long {
|
|
194
|
-
return (this[key] as? Number)?.toLong() ?: default
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private fun Map<String, Any?>.getNumberOrDefault(key: String, default: Double): Double {
|
|
198
|
-
return (this[key] as? Number)?.toDouble() ?: default
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
data class AudioFormatInfo(
|
|
202
|
-
val format: Int,
|
|
203
|
-
val mimeType: String,
|
|
204
|
-
val fileExtension: String
|
|
205
|
-
)
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostream
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Configuration for the notification waveform visualization
|
|
5
|
-
* @property color The color of the waveform (e.g., "#FFFFFF" for white)
|
|
6
|
-
* @property opacity Opacity of the waveform (0.0-1.0)
|
|
7
|
-
* @property strokeWidth Width of the waveform line (default: 1.5f)
|
|
8
|
-
* @property style Drawing style: "stroke" for outline, "fill" for solid
|
|
9
|
-
* @property mirror Whether to mirror the waveform (symmetrical display)
|
|
10
|
-
* @property height Height of the waveform view in dp (default: 64)
|
|
11
|
-
*/
|
|
12
|
-
data class WaveformConfig(
|
|
13
|
-
val color: String = "#FFFFFF",
|
|
14
|
-
val opacity: Float = 1.0f,
|
|
15
|
-
val strokeWidth: Float = 1.5f,
|
|
16
|
-
val style: String = "stroke", // "stroke" or "fill"
|
|
17
|
-
val mirror: Boolean = true,
|
|
18
|
-
val height: Int = 64
|
|
19
|
-
)
|