@edkimmel/expo-audio-stream 0.2.0 → 0.3.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/NATIVE_EVENTS.md +26 -4
- package/README.md +33 -4
- package/android/src/main/java/expo/modules/audiostream/AudioRecorderManager.kt +25 -0
- package/android/src/main/java/expo/modules/audiostream/ExpoPlayAudioStreamModule.kt +2 -1
- package/android/src/main/java/expo/modules/audiostream/FrequencyBandAnalyzer.kt +153 -0
- package/android/src/main/java/expo/modules/audiostream/pipeline/AudioPipeline.kt +55 -0
- package/android/src/main/java/expo/modules/audiostream/pipeline/PipelineIntegration.kt +16 -0
- package/build/events.d.ts +5 -0
- package/build/events.d.ts.map +1 -1
- package/build/events.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/pipeline/types.d.ts +9 -1
- package/build/pipeline/types.d.ts.map +1 -1
- package/build/pipeline/types.js.map +1 -1
- package/build/types.d.ts +17 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/docs/superpowers/plans/2026-03-13-frequency-band-analysis.md +1006 -0
- package/docs/superpowers/specs/2026-03-13-frequency-band-analysis-design.md +276 -0
- package/ios/AudioPipeline.swift +69 -2
- package/ios/ExpoPlayAudioStreamModule.swift +19 -3
- package/ios/FrequencyBandAnalyzer.swift +135 -0
- package/ios/Microphone.swift +29 -4
- package/ios/MicrophoneDataDelegate.swift +1 -1
- package/ios/PipelineIntegration.swift +14 -0
- package/package.json +1 -1
- package/src/events.ts +1 -0
- package/src/index.ts +6 -1
- package/src/pipeline/types.ts +9 -1
- package/src/types.ts +19 -0
package/NATIVE_EVENTS.md
CHANGED
|
@@ -23,6 +23,7 @@ the recording error channel.
|
|
|
23
23
|
| `fileUri` | `string` | Always `""` (file I/O removed) |
|
|
24
24
|
| `lastEmittedSize` | `number` | Previous `totalSize` value |
|
|
25
25
|
| `mimeType` | `string` | e.g. `"audio/wav"` |
|
|
26
|
+
| `frequencyBands` | `{ low, mid, high }?` | dB-scaled RMS energy per band (0–1). Present only when `frequencyBandConfig` is passed to `startMicrophone`. |
|
|
26
27
|
|
|
27
28
|
**Error variant** (same event name, different shape):
|
|
28
29
|
|
|
@@ -96,11 +97,10 @@ Fired after each audio chunk finishes playback.
|
|
|
96
97
|
|
|
97
98
|
---
|
|
98
99
|
|
|
99
|
-
## Pipeline Events
|
|
100
|
+
## Pipeline Events
|
|
100
101
|
|
|
101
|
-
These events are emitted by `AudioPipeline` via `PipelineIntegration
|
|
102
|
-
|
|
103
|
-
path.
|
|
102
|
+
These events are emitted by `AudioPipeline` via `PipelineIntegration` on both
|
|
103
|
+
Android and iOS.
|
|
104
104
|
|
|
105
105
|
### `PipelineStateChanged`
|
|
106
106
|
|
|
@@ -200,6 +200,28 @@ The pipeline transitions from `draining` to `idle`.
|
|
|
200
200
|
|
|
201
201
|
---
|
|
202
202
|
|
|
203
|
+
### `PipelineFrequencyBands`
|
|
204
|
+
|
|
205
|
+
Fired at the interval configured by `frequencyBandIntervalMs` during pipeline
|
|
206
|
+
playback. Uses IIR-based frequency splitting and dB-scaled RMS energy.
|
|
207
|
+
|
|
208
|
+
| Field | Type | Notes |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `low` | `number` | 0–1, energy below `lowCrossoverHz` (default 300 Hz) |
|
|
211
|
+
| `mid` | `number` | 0–1, energy between crossover frequencies |
|
|
212
|
+
| `high` | `number` | 0–1, energy above `highCrossoverHz` (default 2000 Hz) |
|
|
213
|
+
|
|
214
|
+
**Platform:** Android, iOS
|
|
215
|
+
|
|
216
|
+
**JS response:**
|
|
217
|
+
- Drive a visual audio meter or waveform visualization.
|
|
218
|
+
- Values are dB-scaled from raw RMS so they map well to UI bar heights.
|
|
219
|
+
- When no new audio has been pushed, the last known band values are re-emitted
|
|
220
|
+
to maintain a steady cadence. Values drop to zero only when the pipeline is
|
|
221
|
+
idle (disconnected or between turns).
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
203
225
|
### `PipelineAudioFocusLost`
|
|
204
226
|
|
|
205
227
|
Fired when another app takes audio focus (phone call, navigation, music).
|
package/README.md
CHANGED
|
@@ -24,8 +24,13 @@ const { recordingResult, subscription } =
|
|
|
24
24
|
onAudioStream: async (event) => {
|
|
25
25
|
// event.data: base64-encoded PCM chunk
|
|
26
26
|
// event.soundLevel: current mic level (dB)
|
|
27
|
+
// event.frequencyBands: { low, mid, high } (0–1) if configured
|
|
27
28
|
sendToBackend(event.data);
|
|
28
29
|
},
|
|
30
|
+
frequencyBandConfig: {
|
|
31
|
+
lowCrossoverHz: 300,
|
|
32
|
+
highCrossoverHz: 2000,
|
|
33
|
+
},
|
|
29
34
|
});
|
|
30
35
|
|
|
31
36
|
// Later:
|
|
@@ -73,6 +78,7 @@ const result = await Pipeline.connect({
|
|
|
73
78
|
sampleRate: 24000,
|
|
74
79
|
channelCount: 1,
|
|
75
80
|
targetBufferMs: 80,
|
|
81
|
+
frequencyBandIntervalMs: 100, // optional: emit frequency bands every 100ms
|
|
76
82
|
});
|
|
77
83
|
|
|
78
84
|
// Subscribe to events
|
|
@@ -199,6 +205,7 @@ interface RecordingConfig {
|
|
|
199
205
|
encoding?: "pcm_32bit" | "pcm_16bit" | "pcm_8bit";
|
|
200
206
|
interval?: number; // ms between audio data emissions (default 1000)
|
|
201
207
|
onAudioStream?: (event: AudioDataEvent) => Promise<void>;
|
|
208
|
+
frequencyBandConfig?: FrequencyBandConfig; // enable frequency band analysis on mic audio
|
|
202
209
|
}
|
|
203
210
|
```
|
|
204
211
|
|
|
@@ -216,9 +223,11 @@ interface SoundConfig {
|
|
|
216
223
|
|
|
217
224
|
```typescript
|
|
218
225
|
interface ConnectPipelineOptions {
|
|
219
|
-
sampleRate?: number;
|
|
220
|
-
channelCount?: number;
|
|
221
|
-
targetBufferMs?: number;
|
|
226
|
+
sampleRate?: number; // default 24000
|
|
227
|
+
channelCount?: number; // default 1 (mono)
|
|
228
|
+
targetBufferMs?: number; // ms to buffer before priming gate opens (default 80)
|
|
229
|
+
frequencyBandIntervalMs?: number; // emit PipelineFrequencyBands every N ms (omit to disable)
|
|
230
|
+
frequencyBandConfig?: FrequencyBandConfig; // crossover frequencies (optional)
|
|
222
231
|
}
|
|
223
232
|
```
|
|
224
233
|
|
|
@@ -233,13 +242,32 @@ interface PushPipelineAudioOptions {
|
|
|
233
242
|
}
|
|
234
243
|
```
|
|
235
244
|
|
|
245
|
+
### FrequencyBandConfig
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
interface FrequencyBandConfig {
|
|
249
|
+
lowCrossoverHz?: number; // boundary between low and mid bands (default 300)
|
|
250
|
+
highCrossoverHz?: number; // boundary between mid and high bands (default 2000)
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### FrequencyBands
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
interface FrequencyBands {
|
|
258
|
+
low: number; // 0–1, dB-scaled RMS energy below lowCrossoverHz
|
|
259
|
+
mid: number; // 0–1, dB-scaled RMS energy between crossovers
|
|
260
|
+
high: number; // 0–1, dB-scaled RMS energy above highCrossoverHz
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
236
264
|
## Events
|
|
237
265
|
|
|
238
266
|
### Core Events
|
|
239
267
|
|
|
240
268
|
| Event | Payload | Description |
|
|
241
269
|
|-------|---------|-------------|
|
|
242
|
-
| `AudioData` | `{ encoded, position, deltaSize, totalSize, soundLevel, ... }` | Emitted during mic capture at the configured interval. |
|
|
270
|
+
| `AudioData` | `{ encoded, position, deltaSize, totalSize, soundLevel, frequencyBands?, ... }` | Emitted during mic capture at the configured interval. Includes `frequencyBands` when `frequencyBandConfig` is set. |
|
|
243
271
|
| `SoundChunkPlayed` | `{ isFinal: boolean }` | A queued chunk finished playing. `isFinal` when the queue is empty. |
|
|
244
272
|
| `SoundStarted` | (none) | Playback began for a new turn. |
|
|
245
273
|
| `DeviceReconnected` | `{ reason }` | Audio route changed (headphones, Bluetooth, etc). |
|
|
@@ -254,6 +282,7 @@ interface PushPipelineAudioOptions {
|
|
|
254
282
|
| `PipelineZombieDetected` | `{ playbackHead, stalledMs }` | Audio track stalled. |
|
|
255
283
|
| `PipelineUnderrun` | `{ count }` | Jitter buffer underrun (silence inserted). |
|
|
256
284
|
| `PipelineDrained` | `{ turnId }` | All buffered audio for the turn has been played. |
|
|
285
|
+
| `PipelineFrequencyBands` | `{ low, mid, high }` | Frequency band energy (0–1) emitted at `frequencyBandIntervalMs`. |
|
|
257
286
|
| `PipelineAudioFocusLost` | (empty) | Another app took audio focus. |
|
|
258
287
|
| `PipelineAudioFocusResumed` | (empty) | Audio focus regained. |
|
|
259
288
|
|
|
@@ -36,6 +36,7 @@ class AudioRecorderManager(
|
|
|
36
36
|
|
|
37
37
|
// Flag to control whether actual audio data or silence is sent
|
|
38
38
|
private var isSilent = false
|
|
39
|
+
private var frequencyBandAnalyzer: FrequencyBandAnalyzer? = null
|
|
39
40
|
|
|
40
41
|
private lateinit var recordingConfig: RecordingConfig
|
|
41
42
|
private var mimeType = "audio/wav"
|
|
@@ -166,6 +167,14 @@ class AudioRecorderManager(
|
|
|
166
167
|
|
|
167
168
|
recordingThread = Thread { recordingProcess() }.apply { start() }
|
|
168
169
|
|
|
170
|
+
// Create frequency band analyzer
|
|
171
|
+
val bandConfig = options["frequencyBandConfig"] as? Map<*, *>
|
|
172
|
+
frequencyBandAnalyzer = FrequencyBandAnalyzer(
|
|
173
|
+
sampleRate = recordingConfig.sampleRate,
|
|
174
|
+
lowCrossoverHz = (bandConfig?.get("lowCrossoverHz") as? Number)?.toFloat() ?: 300f,
|
|
175
|
+
highCrossoverHz = (bandConfig?.get("highCrossoverHz") as? Number)?.toFloat() ?: 2000f
|
|
176
|
+
)
|
|
177
|
+
|
|
169
178
|
val result = bundleOf(
|
|
170
179
|
"fileUri" to "",
|
|
171
180
|
"channels" to recordingConfig.channels,
|
|
@@ -218,6 +227,7 @@ class AudioRecorderManager(
|
|
|
218
227
|
pausedDuration = 0
|
|
219
228
|
totalDataSize = 0
|
|
220
229
|
streamUuid = null
|
|
230
|
+
frequencyBandAnalyzer = null
|
|
221
231
|
lastEmittedSize = 0
|
|
222
232
|
|
|
223
233
|
Log.d(Constants.TAG, "Audio resources cleaned up")
|
|
@@ -373,6 +383,16 @@ class AudioRecorderManager(
|
|
|
373
383
|
// Calculate power level (using concise expression)
|
|
374
384
|
val soundLevel = if (isSilent) -160.0f else audioDataEncoder.calculatePowerLevel(audioData, length)
|
|
375
385
|
|
|
386
|
+
// Compute frequency bands
|
|
387
|
+
val bands = if (isSilent) {
|
|
388
|
+
FrequencyBands.ZERO
|
|
389
|
+
} else {
|
|
390
|
+
frequencyBandAnalyzer?.let { analyzer ->
|
|
391
|
+
analyzer.processSamplesFromBytes(audioData, length)
|
|
392
|
+
analyzer.harvest()
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
376
396
|
mainHandler.post {
|
|
377
397
|
try {
|
|
378
398
|
eventSender.sendExpoEvent(
|
|
@@ -384,6 +404,11 @@ class AudioRecorderManager(
|
|
|
384
404
|
"position" to positionInMs,
|
|
385
405
|
"mimeType" to mimeType,
|
|
386
406
|
"soundLevel" to soundLevel,
|
|
407
|
+
"frequencyBands" to bundleOf(
|
|
408
|
+
"low" to (bands?.low ?: 0f),
|
|
409
|
+
"mid" to (bands?.mid ?: 0f),
|
|
410
|
+
"high" to (bands?.high ?: 0f)
|
|
411
|
+
),
|
|
387
412
|
"totalSize" to totalDataSize.toLong(),
|
|
388
413
|
"streamUuid" to streamUuid
|
|
389
414
|
)
|
|
@@ -106,7 +106,8 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
|
106
106
|
PipelineIntegration.EVENT_UNDERRUN,
|
|
107
107
|
PipelineIntegration.EVENT_DRAINED,
|
|
108
108
|
PipelineIntegration.EVENT_AUDIO_FOCUS_LOST,
|
|
109
|
-
PipelineIntegration.EVENT_AUDIO_FOCUS_RESUMED
|
|
109
|
+
PipelineIntegration.EVENT_AUDIO_FOCUS_RESUMED,
|
|
110
|
+
PipelineIntegration.EVENT_FREQUENCY_BANDS
|
|
110
111
|
)
|
|
111
112
|
|
|
112
113
|
// Initialize managers for playback and for recording
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
package expo.modules.audiostream
|
|
2
|
+
|
|
3
|
+
import java.util.concurrent.locks.ReentrantLock
|
|
4
|
+
import kotlin.concurrent.withLock
|
|
5
|
+
import kotlin.math.PI
|
|
6
|
+
import kotlin.math.log10
|
|
7
|
+
import kotlin.math.max
|
|
8
|
+
import kotlin.math.min
|
|
9
|
+
import kotlin.math.sqrt
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* RMS energy per frequency band, range [0, 1].
|
|
13
|
+
*/
|
|
14
|
+
data class FrequencyBands(
|
|
15
|
+
val low: Float,
|
|
16
|
+
val mid: Float,
|
|
17
|
+
val high: Float
|
|
18
|
+
) {
|
|
19
|
+
companion object {
|
|
20
|
+
val ZERO = FrequencyBands(0f, 0f, 0f)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Lightweight IIR-based frequency band analyzer.
|
|
26
|
+
*
|
|
27
|
+
* Uses two parallel single-pole low-pass filters to split audio into
|
|
28
|
+
* low / mid / high bands and accumulate RMS energy.
|
|
29
|
+
* Thread-safe: [processSamples] and [harvest] may be called from
|
|
30
|
+
* different threads (guarded by an internal lock).
|
|
31
|
+
*/
|
|
32
|
+
class FrequencyBandAnalyzer(
|
|
33
|
+
sampleRate: Int,
|
|
34
|
+
lowCrossoverHz: Float = 300f,
|
|
35
|
+
highCrossoverHz: Float = 2000f
|
|
36
|
+
) {
|
|
37
|
+
// ── Coefficients (immutable after init) ──────────────────────────
|
|
38
|
+
private val alphaLow: Float = min(1f, (2f * PI.toFloat() * lowCrossoverHz) / sampleRate)
|
|
39
|
+
private val alphaHigh: Float = min(1f, (2f * PI.toFloat() * highCrossoverHz) / sampleRate)
|
|
40
|
+
|
|
41
|
+
// ── Filter state ─────────────────────────────────────────────────
|
|
42
|
+
private var lp1: Float = 0f
|
|
43
|
+
private var lp2: Float = 0f
|
|
44
|
+
|
|
45
|
+
// ── Energy accumulators ──────────────────────────────────────────
|
|
46
|
+
private var lowE: Float = 0f
|
|
47
|
+
private var midE: Float = 0f
|
|
48
|
+
private var highE: Float = 0f
|
|
49
|
+
private var count: Int = 0
|
|
50
|
+
|
|
51
|
+
// ── Synchronization ──────────────────────────────────────────────
|
|
52
|
+
private val lock = ReentrantLock()
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Process a batch of PCM16 samples. Accumulates energy — does NOT
|
|
56
|
+
* produce output. Call [harvest] to read and reset.
|
|
57
|
+
*/
|
|
58
|
+
fun processSamples(samples: ShortArray, length: Int = samples.size) {
|
|
59
|
+
lock.withLock {
|
|
60
|
+
for (i in 0 until length) {
|
|
61
|
+
val s = samples[i].toFloat() / 32768f
|
|
62
|
+
|
|
63
|
+
lp1 += alphaLow * (s - lp1)
|
|
64
|
+
lp2 += alphaHigh * (s - lp2)
|
|
65
|
+
|
|
66
|
+
val low = lp1
|
|
67
|
+
val high = s - lp2
|
|
68
|
+
val mid = s - low - high
|
|
69
|
+
|
|
70
|
+
lowE += low * low
|
|
71
|
+
midE += mid * mid
|
|
72
|
+
highE += high * high
|
|
73
|
+
count++
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Process PCM16 samples from a ByteArray (little-endian Int16).
|
|
80
|
+
*/
|
|
81
|
+
fun processSamplesFromBytes(data: ByteArray, length: Int = data.size) {
|
|
82
|
+
val sampleCount = length / 2
|
|
83
|
+
val samples = ShortArray(sampleCount)
|
|
84
|
+
val buf = java.nio.ByteBuffer.wrap(data, 0, length)
|
|
85
|
+
.order(java.nio.ByteOrder.LITTLE_ENDIAN)
|
|
86
|
+
.asShortBuffer()
|
|
87
|
+
buf.get(samples)
|
|
88
|
+
processSamples(samples, sampleCount)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Whether any samples have been accumulated since the last harvest/reset. */
|
|
92
|
+
fun hasData(): Boolean = lock.withLock { count > 0 }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read accumulated band energy scaled to 0–1 using dB mapping,
|
|
96
|
+
* then reset accumulators.
|
|
97
|
+
*
|
|
98
|
+
* Raw RMS of speech/music PCM typically sits around 0.01–0.15,
|
|
99
|
+
* which is unusable for a visual meter. Converting to dB and
|
|
100
|
+
* mapping the range [–60 dB, 0 dB] → [0, 1] gives perceptually
|
|
101
|
+
* meaningful values.
|
|
102
|
+
*/
|
|
103
|
+
fun harvest(): FrequencyBands {
|
|
104
|
+
lock.withLock {
|
|
105
|
+
if (count == 0) return FrequencyBands.ZERO
|
|
106
|
+
|
|
107
|
+
val n = count.toFloat()
|
|
108
|
+
val result = FrequencyBands(
|
|
109
|
+
low = rmsToScaled(sqrt(lowE / n)),
|
|
110
|
+
mid = rmsToScaled(sqrt(midE / n)),
|
|
111
|
+
high = rmsToScaled(sqrt(highE / n))
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
lowE = 0f
|
|
115
|
+
midE = 0f
|
|
116
|
+
highE = 0f
|
|
117
|
+
count = 0
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
companion object {
|
|
124
|
+
/** Floor in dB — anything below this maps to 0. */
|
|
125
|
+
private const val DB_FLOOR = -60f
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Convert raw RMS (0…1) to a 0–1 meter value via dB scaling.
|
|
129
|
+
* rms 0.09 → –20.9 dB → 0.65
|
|
130
|
+
* rms 0.01 → –40 dB → 0.33
|
|
131
|
+
* rms 0.001 → –60 dB → 0.0
|
|
132
|
+
*/
|
|
133
|
+
private fun rmsToScaled(rms: Float): Float {
|
|
134
|
+
if (rms <= 0f) return 0f
|
|
135
|
+
val db = 20f * log10(rms)
|
|
136
|
+
return max(0f, min(1f, (db - DB_FLOOR) / -DB_FLOOR))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Zero all state (filter accumulators + energy).
|
|
142
|
+
*/
|
|
143
|
+
fun reset() {
|
|
144
|
+
lock.withLock {
|
|
145
|
+
lp1 = 0f
|
|
146
|
+
lp2 = 0f
|
|
147
|
+
lowE = 0f
|
|
148
|
+
midE = 0f
|
|
149
|
+
highE = 0f
|
|
150
|
+
count = 0
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -13,6 +13,8 @@ import android.os.Looper
|
|
|
13
13
|
import android.provider.Settings
|
|
14
14
|
import android.util.Base64
|
|
15
15
|
import android.util.Log
|
|
16
|
+
import expo.modules.audiostream.FrequencyBandAnalyzer
|
|
17
|
+
import expo.modules.audiostream.FrequencyBands
|
|
16
18
|
import java.nio.ByteBuffer
|
|
17
19
|
import java.nio.ByteOrder
|
|
18
20
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
@@ -49,6 +51,7 @@ interface PipelineListener {
|
|
|
49
51
|
fun onDrained(turnId: String)
|
|
50
52
|
fun onAudioFocusLost()
|
|
51
53
|
fun onAudioFocusResumed()
|
|
54
|
+
fun onFrequencyBands(low: Float, mid: Float, high: Float)
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -84,6 +87,9 @@ class AudioPipeline(
|
|
|
84
87
|
private val sampleRate: Int,
|
|
85
88
|
private val channelCount: Int,
|
|
86
89
|
private val targetBufferMs: Int,
|
|
90
|
+
private val frequencyBandIntervalMs: Int = 100,
|
|
91
|
+
private val lowCrossoverHz: Float = 300f,
|
|
92
|
+
private val highCrossoverHz: Float = 2000f,
|
|
87
93
|
private val listener: PipelineListener
|
|
88
94
|
) {
|
|
89
95
|
companion object {
|
|
@@ -184,6 +190,11 @@ class AudioPipeline(
|
|
|
184
190
|
// ── Underrun debounce ───────────────────────────────────────────────
|
|
185
191
|
private var lastReportedUnderrunCount = 0
|
|
186
192
|
|
|
193
|
+
// ── Frequency band analysis ──────────────────────────────────────
|
|
194
|
+
private var frequencyBandAnalyzer: FrequencyBandAnalyzer? = null
|
|
195
|
+
private var frequencyBandExecutor: java.util.concurrent.ScheduledExecutorService? = null
|
|
196
|
+
@Volatile private var lastEmittedBands: FrequencyBands? = null
|
|
197
|
+
|
|
187
198
|
// ── State ───────────────────────────────────────────────────────────
|
|
188
199
|
@Volatile private var state: PipelineState = PipelineState.IDLE
|
|
189
200
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
@@ -260,6 +271,14 @@ class AudioPipeline(
|
|
|
260
271
|
// ── 7. Reset telemetry ──────────────────────────────────────
|
|
261
272
|
resetTelemetry()
|
|
262
273
|
|
|
274
|
+
// ── 8. Frequency band analyzer ──────────────────────────
|
|
275
|
+
frequencyBandAnalyzer = FrequencyBandAnalyzer(
|
|
276
|
+
sampleRate = sampleRate,
|
|
277
|
+
lowCrossoverHz = lowCrossoverHz,
|
|
278
|
+
highCrossoverHz = highCrossoverHz
|
|
279
|
+
)
|
|
280
|
+
startFrequencyBandTimer()
|
|
281
|
+
|
|
263
282
|
setState(PipelineState.IDLE)
|
|
264
283
|
Log.d(TAG, "Connected — sampleRate=$sampleRate ch=$channelCount " +
|
|
265
284
|
"frameSamples=$frameSizeSamples targetBuffer=${targetBufferMs}ms")
|
|
@@ -284,6 +303,12 @@ class AudioPipeline(
|
|
|
284
303
|
zombieThread?.interrupt()
|
|
285
304
|
zombieThread = null
|
|
286
305
|
|
|
306
|
+
// Stop frequency band timer
|
|
307
|
+
frequencyBandExecutor?.shutdownNow()
|
|
308
|
+
frequencyBandExecutor = null
|
|
309
|
+
frequencyBandAnalyzer = null
|
|
310
|
+
lastEmittedBands = null
|
|
311
|
+
|
|
287
312
|
// Remove VolumeGuard
|
|
288
313
|
removeVolumeGuard()
|
|
289
314
|
|
|
@@ -357,6 +382,7 @@ class AudioPipeline(
|
|
|
357
382
|
// so real audio plays immediately without waiting behind queued silence.
|
|
358
383
|
pendingFlush.set(true)
|
|
359
384
|
setState(PipelineState.STREAMING)
|
|
385
|
+
frequencyBandAnalyzer?.reset()
|
|
360
386
|
}
|
|
361
387
|
|
|
362
388
|
// ── Decode base64 → PCM shorts ──────────────────────────────
|
|
@@ -400,6 +426,7 @@ class AudioPipeline(
|
|
|
400
426
|
playbackStartedForTurn = false
|
|
401
427
|
lastReportedUnderrunCount = 0
|
|
402
428
|
setState(PipelineState.IDLE)
|
|
429
|
+
frequencyBandAnalyzer?.reset()
|
|
403
430
|
}
|
|
404
431
|
}
|
|
405
432
|
|
|
@@ -458,6 +485,13 @@ class AudioPipeline(
|
|
|
458
485
|
frame.fill(0)
|
|
459
486
|
}
|
|
460
487
|
|
|
488
|
+
// Analyze frequency bands on the raw Int16 samples.
|
|
489
|
+
// Only feed real audio (streaming/draining) — not silence frames
|
|
490
|
+
// written while idle/priming, which would dilute RMS energy.
|
|
491
|
+
if (!audioFocusLost.get() && (state == PipelineState.STREAMING || state == PipelineState.DRAINING)) {
|
|
492
|
+
frequencyBandAnalyzer?.processSamples(frame, frame.size)
|
|
493
|
+
}
|
|
494
|
+
|
|
461
495
|
// Write to AudioTrack (BLOCKING — will park thread until space available)
|
|
462
496
|
try {
|
|
463
497
|
val written = track.write(frame, 0, frame.size, AudioTrack.WRITE_BLOCKING)
|
|
@@ -577,6 +611,27 @@ class AudioPipeline(
|
|
|
577
611
|
}
|
|
578
612
|
}
|
|
579
613
|
|
|
614
|
+
// ════════════════════════════════════════════════════════════════════
|
|
615
|
+
// Frequency band emission
|
|
616
|
+
// ════════════════════════════════════════════════════════════════════
|
|
617
|
+
|
|
618
|
+
private fun startFrequencyBandTimer() {
|
|
619
|
+
val executor = java.util.concurrent.Executors.newSingleThreadScheduledExecutor { r ->
|
|
620
|
+
Thread(r, "AudioPipeline-FreqBands").apply { isDaemon = true }
|
|
621
|
+
}
|
|
622
|
+
executor.scheduleAtFixedRate({
|
|
623
|
+
if (!running.get()) return@scheduleAtFixedRate
|
|
624
|
+
val analyzer = frequencyBandAnalyzer ?: return@scheduleAtFixedRate
|
|
625
|
+
val bands = if (analyzer.hasData()) {
|
|
626
|
+
analyzer.harvest().also { lastEmittedBands = it }
|
|
627
|
+
} else {
|
|
628
|
+
lastEmittedBands ?: return@scheduleAtFixedRate
|
|
629
|
+
}
|
|
630
|
+
listener.onFrequencyBands(bands.low, bands.mid, bands.high)
|
|
631
|
+
}, frequencyBandIntervalMs.toLong(), frequencyBandIntervalMs.toLong(), java.util.concurrent.TimeUnit.MILLISECONDS)
|
|
632
|
+
frequencyBandExecutor = executor
|
|
633
|
+
}
|
|
634
|
+
|
|
580
635
|
// ════════════════════════════════════════════════════════════════════
|
|
581
636
|
// VolumeGuard
|
|
582
637
|
// ════════════════════════════════════════════════════════════════════
|
|
@@ -92,6 +92,7 @@ class PipelineIntegration(
|
|
|
92
92
|
const val EVENT_DRAINED = "PipelineDrained"
|
|
93
93
|
const val EVENT_AUDIO_FOCUS_LOST = "PipelineAudioFocusLost"
|
|
94
94
|
const val EVENT_AUDIO_FOCUS_RESUMED = "PipelineAudioFocusResumed"
|
|
95
|
+
const val EVENT_FREQUENCY_BANDS = "PipelineFrequencyBands"
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
private var pipeline: AudioPipeline? = null
|
|
@@ -116,12 +117,19 @@ class PipelineIntegration(
|
|
|
116
117
|
val sampleRate = (options["sampleRate"] as? Number)?.toInt() ?: 24000
|
|
117
118
|
val channelCount = (options["channelCount"] as? Number)?.toInt() ?: 1
|
|
118
119
|
val targetBufferMs = (options["targetBufferMs"] as? Number)?.toInt() ?: 80
|
|
120
|
+
val frequencyBandIntervalMs = (options["frequencyBandIntervalMs"] as? Number)?.toInt() ?: 100
|
|
121
|
+
val bandConfig = options["frequencyBandConfig"] as? Map<*, *>
|
|
122
|
+
val lowCrossoverHz = (bandConfig?.get("lowCrossoverHz") as? Number)?.toFloat() ?: 300f
|
|
123
|
+
val highCrossoverHz = (bandConfig?.get("highCrossoverHz") as? Number)?.toFloat() ?: 2000f
|
|
119
124
|
|
|
120
125
|
pipeline = AudioPipeline(
|
|
121
126
|
context = context,
|
|
122
127
|
sampleRate = sampleRate,
|
|
123
128
|
channelCount = channelCount,
|
|
124
129
|
targetBufferMs = targetBufferMs,
|
|
130
|
+
frequencyBandIntervalMs = frequencyBandIntervalMs,
|
|
131
|
+
lowCrossoverHz = lowCrossoverHz,
|
|
132
|
+
highCrossoverHz = highCrossoverHz,
|
|
125
133
|
listener = this
|
|
126
134
|
)
|
|
127
135
|
pipeline!!.connect()
|
|
@@ -303,6 +311,14 @@ class PipelineIntegration(
|
|
|
303
311
|
sendEvent(EVENT_AUDIO_FOCUS_RESUMED, Bundle())
|
|
304
312
|
}
|
|
305
313
|
|
|
314
|
+
override fun onFrequencyBands(low: Float, mid: Float, high: Float) {
|
|
315
|
+
sendEvent(EVENT_FREQUENCY_BANDS, Bundle().apply {
|
|
316
|
+
putFloat("low", low)
|
|
317
|
+
putFloat("mid", mid)
|
|
318
|
+
putFloat("high", high)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
306
322
|
// ── Helper ──────────────────────────────────────────────────────────
|
|
307
323
|
|
|
308
324
|
private fun sendEvent(eventName: String, params: Bundle) {
|
package/build/events.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ export interface AudioEventPayload {
|
|
|
11
11
|
mimeType: string;
|
|
12
12
|
streamUuid: string;
|
|
13
13
|
soundLevel?: number;
|
|
14
|
+
frequencyBands?: {
|
|
15
|
+
low: number;
|
|
16
|
+
mid: number;
|
|
17
|
+
high: number;
|
|
18
|
+
};
|
|
14
19
|
}
|
|
15
20
|
export type SoundChunkPlayedEventPayload = {
|
|
16
21
|
isFinal: boolean;
|
package/build/events.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGzE,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAM7C,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGzE,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAM7C,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7D;AAED,MAAM,MAAM,4BAA4B,GAAG;IACzC,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,wBAAwB;;;;CAI3B,CAAC;AAEX,MAAM,MAAM,uBAAuB,GACjC,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAE3E,MAAM,MAAM,6BAA6B,GAAG;IAC1C,MAAM,EAAE,uBAAuB,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,WAAW;;;;;CAKvB,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,GACpD,iBAAiB,CAEnB;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,CAAC,KAAK,EAAE,4BAA4B,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,iBAAiB,CAEnB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,OAAO,EAChD,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GAChD,iBAAiB,CAEnB"}
|
package/build/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAE3C,OAAO,EAAE,YAAY,EAA0B,MAAM,mBAAmB,CAAC;AAKzE,OAAO,yBAAyB,MAAM,6BAA6B,CAAC;AAEpE,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,yBAAyB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAE3C,OAAO,EAAE,YAAY,EAA0B,MAAM,mBAAmB,CAAC;AAKzE,OAAO,yBAAyB,MAAM,6BAA6B,CAAC;AAEpE,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,yBAAyB,CAAC,CAAC;AAoB5D,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,kBAAkB,EAAE,oBAAoB;IACxC,oBAAoB,EAAE,sBAAsB;IAC5C,OAAO,EAAE,SAAS;CACV,CAAC;AASX,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,SAAS,EAAE,WAAW;IACtB,gBAAgB,EAAE,kBAAkB;IACpC,YAAY,EAAE,cAAc;IAC5B,iBAAiB,EAAE,mBAAmB;CACvC,CAAC;AAEF,MAAM,UAAU,qBAAqB,CACnC,QAAqD;IAErD,OAAQ,OAAe,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,2BAA2B,CACzC,QAAgE;IAEhE,OAAQ,OAAe,CAAC,WAAW,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,SAAiB,EACjB,QAAiD;IAEjD,OAAQ,OAAe,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAC3D,CAAC","sourcesContent":["// packages/expo-audio-stream/src/events.ts\n\nimport { EventEmitter, type EventSubscription } from \"expo-modules-core\";\n\n// Type alias for backwards compatibility\nexport type Subscription = EventSubscription;\n\nimport ExpoPlayAudioStreamModule from \"./ExpoPlayAudioStreamModule\";\n\nconst emitter = new EventEmitter(ExpoPlayAudioStreamModule);\n\nexport interface AudioEventPayload {\n encoded?: string;\n buffer?: Float32Array;\n fileUri: string;\n lastEmittedSize: number;\n position: number;\n deltaSize: number;\n totalSize: number;\n mimeType: string;\n streamUuid: string;\n soundLevel?: number;\n frequencyBands?: { low: number; mid: number; high: number };\n}\n\nexport type SoundChunkPlayedEventPayload = {\n isFinal: boolean;\n};\n\nexport const DeviceReconnectedReasons = {\n newDeviceAvailable: \"newDeviceAvailable\",\n oldDeviceUnavailable: \"oldDeviceUnavailable\",\n unknown: \"unknown\",\n} as const;\n\nexport type DeviceReconnectedReason =\n (typeof DeviceReconnectedReasons)[keyof typeof DeviceReconnectedReasons];\n\nexport type DeviceReconnectedEventPayload = {\n reason: DeviceReconnectedReason;\n};\n\nexport const AudioEvents = {\n AudioData: \"AudioData\",\n SoundChunkPlayed: \"SoundChunkPlayed\",\n SoundStarted: \"SoundStarted\",\n DeviceReconnected: \"DeviceReconnected\",\n};\n\nexport function addAudioEventListener(\n listener: (event: AudioEventPayload) => Promise<void>\n): EventSubscription {\n return (emitter as any).addListener(\"AudioData\", listener);\n}\n\nexport function addSoundChunkPlayedListener(\n listener: (event: SoundChunkPlayedEventPayload) => Promise<void>\n): EventSubscription {\n return (emitter as any).addListener(\"SoundChunkPlayed\", listener);\n}\n\nexport function subscribeToEvent<T extends unknown>(\n eventName: string,\n listener: (event: T | undefined) => Promise<void>\n): EventSubscription {\n return (emitter as any).addListener(eventName, listener);\n}\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EventSubscription } from "expo-modules-core";
|
|
2
2
|
type Subscription = EventSubscription;
|
|
3
|
-
import { AudioDataEvent, AudioRecording, RecordingConfig, StartRecordingResult, SoundConfig, PlaybackMode, Encoding, EncodingTypes, PlaybackModes, IAudioBufferConfig, IAudioPlayPayload, IAudioFrame, BufferHealthState, IBufferHealthMetrics, IAudioBufferManager, IFrameProcessor, IQualityMonitor, BufferedStreamConfig, SmartBufferConfig, SmartBufferMode, NetworkConditions } from "./types";
|
|
3
|
+
import { AudioDataEvent, AudioRecording, RecordingConfig, StartRecordingResult, SoundConfig, PlaybackMode, Encoding, EncodingTypes, FrequencyBands, PlaybackModes, IAudioBufferConfig, IAudioPlayPayload, IAudioFrame, BufferHealthState, IBufferHealthMetrics, IAudioBufferManager, IFrameProcessor, IQualityMonitor, BufferedStreamConfig, SmartBufferConfig, SmartBufferMode, NetworkConditions } from "./types";
|
|
4
4
|
import { SoundChunkPlayedEventPayload, AudioEvents, DeviceReconnectedReason, DeviceReconnectedEventPayload } from "./events";
|
|
5
5
|
declare const SuspendSoundEventTurnId = "suspend-sound-events";
|
|
6
6
|
export declare class ExpoPlayAudioStream {
|
|
@@ -117,7 +117,7 @@ export declare class ExpoPlayAudioStream {
|
|
|
117
117
|
status?: string;
|
|
118
118
|
}>;
|
|
119
119
|
}
|
|
120
|
-
export { AudioDataEvent, SoundChunkPlayedEventPayload, DeviceReconnectedReason, DeviceReconnectedEventPayload, AudioRecording, RecordingConfig, StartRecordingResult, AudioEvents, SuspendSoundEventTurnId, SoundConfig, PlaybackMode, Encoding, EncodingTypes, PlaybackModes, IAudioBufferConfig, IAudioPlayPayload, IAudioFrame, BufferHealthState, IBufferHealthMetrics, IAudioBufferManager, IFrameProcessor, IQualityMonitor, BufferedStreamConfig, SmartBufferConfig, SmartBufferMode, NetworkConditions, };
|
|
120
|
+
export { AudioDataEvent, SoundChunkPlayedEventPayload, DeviceReconnectedReason, DeviceReconnectedEventPayload, AudioRecording, RecordingConfig, StartRecordingResult, AudioEvents, SuspendSoundEventTurnId, SoundConfig, PlaybackMode, Encoding, EncodingTypes, FrequencyBands, PlaybackModes, IAudioBufferConfig, IAudioPlayPayload, IAudioFrame, BufferHealthState, IBufferHealthMetrics, IAudioBufferManager, IFrameProcessor, IQualityMonitor, BufferedStreamConfig, SmartBufferConfig, SmartBufferMode, NetworkConditions, };
|
|
121
121
|
export type { EventSubscription } from "expo-modules-core";
|
|
122
122
|
export type { Subscription } from "./events";
|
|
123
123
|
export { Pipeline } from "./pipeline";
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,KAAK,YAAY,GAAG,iBAAiB,CAAC;AACtC,OAAO,EACL,cAAc,EACd,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,aAAa,EAEb,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAIL,4BAA4B,EAC5B,WAAW,EAEX,uBAAuB,EACvB,6BAA6B,EAC9B,MAAM,UAAU,CAAC;AAElB,QAAA,MAAM,uBAAuB,yBAAyB,CAAC;AAEvD,qBAAa,mBAAmB;IAC9B;;;;OAIG;IACH,MAAM,CAAC,OAAO;IAId;;;;;;;;OAQG;WACU,SAAS,CACpB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,IAAI,CAAC;IAahB;;;;;OAKG;WACU,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IASvC;;;;;;OAMG;WACU,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASnE;;;;;OAKG;WACU,eAAe,CAAC,eAAe,EAAE,eAAe,GAAG,OAAO,CAAC;QACtE,eAAe,EAAE,oBAAoB,CAAC;QACtC,YAAY,CAAC,EAAE,YAAY,CAAC;KAC7B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/build/index.js
CHANGED
|
@@ -72,7 +72,7 @@ export class ExpoPlayAudioStream {
|
|
|
72
72
|
const { onAudioStream, ...options } = recordingConfig;
|
|
73
73
|
if (onAudioStream && typeof onAudioStream == "function") {
|
|
74
74
|
subscription = addAudioEventListener(async (event) => {
|
|
75
|
-
const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, } = event;
|
|
75
|
+
const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands, } = event;
|
|
76
76
|
if (!encoded) {
|
|
77
77
|
console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);
|
|
78
78
|
throw new Error("Encoded audio data is missing");
|
|
@@ -84,6 +84,7 @@ export class ExpoPlayAudioStream {
|
|
|
84
84
|
eventDataSize: deltaSize,
|
|
85
85
|
totalSize,
|
|
86
86
|
soundLevel,
|
|
87
|
+
frequencyBands,
|
|
87
88
|
});
|
|
88
89
|
});
|
|
89
90
|
}
|
|
@@ -125,7 +126,7 @@ export class ExpoPlayAudioStream {
|
|
|
125
126
|
*/
|
|
126
127
|
static subscribeToAudioEvents(onMicrophoneStream) {
|
|
127
128
|
return addAudioEventListener(async (event) => {
|
|
128
|
-
const { fileUri, deltaSize, totalSize, position, encoded, soundLevel } = event;
|
|
129
|
+
const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands } = event;
|
|
129
130
|
if (!encoded) {
|
|
130
131
|
console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);
|
|
131
132
|
throw new Error("Encoded audio data is missing");
|
|
@@ -137,6 +138,7 @@ export class ExpoPlayAudioStream {
|
|
|
137
138
|
eventDataSize: deltaSize,
|
|
138
139
|
totalSize,
|
|
139
140
|
soundLevel,
|
|
141
|
+
frequencyBands,
|
|
140
142
|
});
|
|
141
143
|
});
|
|
142
144
|
}
|