@siteed/expo-audio-stream 1.1.2 → 1.1.5
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/build/AudioAnalysis/AudioAnalysis.types.d.ts +74 -0
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
- package/build/AudioAnalysis/AudioAnalysis.types.js +3 -0
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +20 -0
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
- package/build/AudioAnalysis/extractAudioAnalysis.js +88 -0
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
- package/build/AudioAnalysis/extractWaveform.d.ts +8 -0
- package/build/AudioAnalysis/extractWaveform.d.ts.map +1 -0
- package/build/AudioAnalysis/extractWaveform.js +14 -0
- package/build/AudioAnalysis/extractWaveform.js.map +1 -0
- package/build/AudioRecorder.provider.d.ts +11 -0
- package/build/AudioRecorder.provider.d.ts.map +1 -0
- package/build/AudioRecorder.provider.js +36 -0
- package/build/AudioRecorder.provider.js.map +1 -0
- package/build/ExpoAudioStream.native.d.ts +3 -0
- package/build/ExpoAudioStream.native.d.ts.map +1 -0
- package/{src/ExpoAudioStream.native.ts → build/ExpoAudioStream.native.js} +3 -3
- package/build/ExpoAudioStream.native.js.map +1 -0
- package/build/ExpoAudioStream.types.d.ts +76 -0
- package/build/ExpoAudioStream.types.d.ts.map +1 -0
- package/build/ExpoAudioStream.types.js +2 -0
- package/build/ExpoAudioStream.types.js.map +1 -0
- package/build/ExpoAudioStream.web.d.ts +42 -0
- package/build/ExpoAudioStream.web.d.ts.map +1 -0
- package/build/ExpoAudioStream.web.js +203 -0
- package/build/ExpoAudioStream.web.js.map +1 -0
- package/build/ExpoAudioStreamModule.d.ts +3 -0
- package/build/ExpoAudioStreamModule.d.ts.map +1 -0
- package/build/ExpoAudioStreamModule.js +25 -0
- package/build/ExpoAudioStreamModule.js.map +1 -0
- package/build/WebRecorder.web.d.ts +51 -0
- package/build/WebRecorder.web.d.ts.map +1 -0
- package/build/WebRecorder.web.js +298 -0
- package/build/WebRecorder.web.js.map +1 -0
- package/build/constants.d.ts +11 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +14 -0
- package/build/constants.js.map +1 -0
- package/build/events.d.ts +18 -0
- package/build/events.d.ts.map +1 -0
- package/build/events.js +15 -0
- package/build/events.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js.map +1 -0
- package/build/logger.d.ts +9 -0
- package/build/logger.d.ts.map +1 -0
- package/build/logger.js +13 -0
- package/build/logger.js.map +1 -0
- package/build/useAudioRecorder.d.ts +20 -0
- package/build/useAudioRecorder.d.ts.map +1 -0
- package/build/useAudioRecorder.js +271 -0
- package/build/useAudioRecorder.js.map +1 -0
- package/build/utils/BlobFix.d.ts +9 -0
- package/build/utils/BlobFix.d.ts.map +1 -0
- package/{src/utils/BlobFix.ts → build/utils/BlobFix.js} +66 -122
- package/build/utils/BlobFix.js.map +1 -0
- package/build/utils/concatenateBuffers.d.ts +8 -0
- package/build/utils/concatenateBuffers.d.ts.map +1 -0
- package/{src/utils/concatenateBuffers.ts → build/utils/concatenateBuffers.js} +10 -13
- package/build/utils/concatenateBuffers.js.map +1 -0
- package/build/utils/convertPCMToFloat32.d.ts +11 -0
- package/build/utils/convertPCMToFloat32.d.ts.map +1 -0
- package/build/utils/convertPCMToFloat32.js +54 -0
- package/build/utils/convertPCMToFloat32.js.map +1 -0
- package/build/utils/encodingToBitDepth.d.ts +5 -0
- package/build/utils/encodingToBitDepth.d.ts.map +1 -0
- package/build/utils/encodingToBitDepth.js +13 -0
- package/build/utils/encodingToBitDepth.js.map +1 -0
- package/build/utils/getWavFileInfo.d.ts +26 -0
- package/build/utils/getWavFileInfo.d.ts.map +1 -0
- package/build/utils/getWavFileInfo.js +92 -0
- package/build/utils/getWavFileInfo.js.map +1 -0
- package/build/utils/writeWavHeader.d.ts +9 -0
- package/build/utils/writeWavHeader.d.ts.map +1 -0
- package/build/utils/writeWavHeader.js +45 -0
- package/build/utils/writeWavHeader.js.map +1 -0
- package/build/workers/InlineFeaturesExtractor.web.d.ts +2 -0
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
- package/{src/workers/InlineFeaturesExtractor.web.tsx → build/workers/InlineFeaturesExtractor.web.js} +2 -1
- package/build/workers/InlineFeaturesExtractor.web.js.map +1 -0
- package/build/workers/inlineAudioWebWorker.web.d.ts +2 -0
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
- package/{src/workers/inlineAudioWebWorker.web.tsx → build/workers/inlineAudioWebWorker.web.js} +2 -1
- package/build/workers/inlineAudioWebWorker.web.js.map +1 -0
- package/package.json +96 -96
- package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -84
- package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -147
- package/src/AudioAnalysis/extractWaveform.ts +0 -25
- package/src/AudioRecorder.provider.tsx +0 -70
- package/src/ExpoAudioStream.types.ts +0 -80
- package/src/ExpoAudioStream.web.ts +0 -255
- package/src/ExpoAudioStreamModule.ts +0 -31
- package/src/WebRecorder.web.ts +0 -433
- package/src/constants.ts +0 -18
- package/src/events.ts +0 -39
- package/src/index.ts +0 -24
- package/src/logger.ts +0 -22
- package/src/useAudioRecorder.tsx +0 -420
- package/src/utils/convertPCMToFloat32.ts +0 -75
- package/src/utils/encodingToBitDepth.ts +0 -18
- package/src/utils/getWavFileInfo.ts +0 -132
- package/src/utils/writeWavHeader.ts +0 -61
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// packages/expo-audio-stream/src/AudioAnalysis/AudioAnalysis.types.ts
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Represents various audio features extracted from an audio signal.
|
|
5
|
-
*/
|
|
6
|
-
export interface AudioFeatures {
|
|
7
|
-
energy: number // The infinite integral of the squared signal, representing the overall energy of the audio.
|
|
8
|
-
mfcc: number[] // Mel-frequency cepstral coefficients, describing the short-term power spectrum of a sound.
|
|
9
|
-
rms: number // Root mean square value, indicating the amplitude of the audio signal.
|
|
10
|
-
minAmplitude: number // Minimum amplitude value in the audio signal.
|
|
11
|
-
maxAmplitude: number // Maximum amplitude value in the audio signal.
|
|
12
|
-
zcr: number // Zero-crossing rate, indicating the rate at which the signal changes sign.
|
|
13
|
-
spectralCentroid: number // The center of mass of the spectrum, indicating the brightness of the sound.
|
|
14
|
-
spectralFlatness: number // Measure of the flatness of the spectrum, indicating how noise-like the signal is.
|
|
15
|
-
spectralRolloff: number // The frequency below which a specified percentage (usually 85%) of the total spectral energy lies.
|
|
16
|
-
spectralBandwidth: number // The width of the spectrum, indicating the range of frequencies present.
|
|
17
|
-
chromagram: number[] // Chromagram, representing the 12 different pitch classes of the audio.
|
|
18
|
-
tempo: number // Estimated tempo of the audio signal, measured in beats per minute (BPM).
|
|
19
|
-
hnr: number // Harmonics-to-noise ratio, indicating the proportion of harmonics to noise in the audio signal.
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Options to specify which audio features to extract.
|
|
24
|
-
*/
|
|
25
|
-
export interface AudioFeaturesOptions {
|
|
26
|
-
energy?: boolean
|
|
27
|
-
mfcc?: boolean
|
|
28
|
-
rms?: boolean
|
|
29
|
-
zcr?: boolean
|
|
30
|
-
spectralCentroid?: boolean
|
|
31
|
-
spectralFlatness?: boolean
|
|
32
|
-
spectralRolloff?: boolean
|
|
33
|
-
spectralBandwidth?: boolean
|
|
34
|
-
chromagram?: boolean
|
|
35
|
-
tempo?: boolean
|
|
36
|
-
hnr?: boolean
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Represents a single data point in the audio analysis.
|
|
41
|
-
*/
|
|
42
|
-
export interface DataPoint {
|
|
43
|
-
id: number
|
|
44
|
-
amplitude: number
|
|
45
|
-
activeSpeech?: boolean
|
|
46
|
-
dB?: number
|
|
47
|
-
silent?: boolean
|
|
48
|
-
features?: AudioFeatures
|
|
49
|
-
startTime?: number
|
|
50
|
-
endTime?: number
|
|
51
|
-
// start / end position in bytes
|
|
52
|
-
startPosition?: number
|
|
53
|
-
endPosition?: number
|
|
54
|
-
// number of audio samples for this point (samples size depends on bit depth)
|
|
55
|
-
samples?: number
|
|
56
|
-
// TODO: speaker detection
|
|
57
|
-
speaker?: number
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export type AmplitudeAlgorithm = 'peak' | 'rms'
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Represents the complete data from the audio analysis.
|
|
64
|
-
*/
|
|
65
|
-
export interface AudioAnalysis {
|
|
66
|
-
pointsPerSecond: number // How many consolidated value per second
|
|
67
|
-
durationMs: number // Duration of the audio in milliseconds
|
|
68
|
-
bitDepth: number // Bit depth of the audio
|
|
69
|
-
samples: number // Size of the audio in bytes
|
|
70
|
-
numberOfChannels: number // Number of audio channels
|
|
71
|
-
sampleRate: number // Sample rate of the audio
|
|
72
|
-
dataPoints: DataPoint[] // Array of data points from the analysis.
|
|
73
|
-
amplitudeAlgorithm: AmplitudeAlgorithm // Algorithm used to calculate amplitude values.
|
|
74
|
-
amplitudeRange: {
|
|
75
|
-
min: number
|
|
76
|
-
max: number
|
|
77
|
-
}
|
|
78
|
-
// TODO: speaker detection
|
|
79
|
-
speakerChanges?: {
|
|
80
|
-
timestamp: number // Timestamp of the speaker change in milliseconds.
|
|
81
|
-
speaker: number // Speaker identifier.
|
|
82
|
-
}[]
|
|
83
|
-
}
|
|
84
|
-
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// packages/expo-audio-stream/src/AudioAnalysis/extractAudioAnalysis.ts
|
|
2
|
-
import {
|
|
3
|
-
AmplitudeAlgorithm,
|
|
4
|
-
AudioAnalysis,
|
|
5
|
-
AudioFeaturesOptions,
|
|
6
|
-
} from './AudioAnalysis.types'
|
|
7
|
-
import ExpoAudioStreamModule from '../ExpoAudioStreamModule'
|
|
8
|
-
import { isWeb } from '../constants'
|
|
9
|
-
import { getLogger } from '../logger'
|
|
10
|
-
import { convertPCMToFloat32 } from '../utils/convertPCMToFloat32'
|
|
11
|
-
import { getWavFileInfo, WavFileInfo } from '../utils/getWavFileInfo'
|
|
12
|
-
import { InlineFeaturesExtractor } from '../workers/InlineFeaturesExtractor.web'
|
|
13
|
-
|
|
14
|
-
const logger = getLogger('extractAudioAnalysis')
|
|
15
|
-
|
|
16
|
-
export interface ExtractAudioAnalysisProps {
|
|
17
|
-
fileUri?: string // should provide either fileUri or arrayBuffer
|
|
18
|
-
wavMetadata?: WavFileInfo
|
|
19
|
-
arrayBuffer?: ArrayBuffer
|
|
20
|
-
bitDepth?: number
|
|
21
|
-
skipWavHeader?: boolean
|
|
22
|
-
durationMs?: number
|
|
23
|
-
sampleRate?: number
|
|
24
|
-
numberOfChannels?: number
|
|
25
|
-
algorithm?: AmplitudeAlgorithm
|
|
26
|
-
position?: number // Optional number of bytes to skip. Default is 0
|
|
27
|
-
length?: number // Optional number of bytes to read.
|
|
28
|
-
pointsPerSecond?: number // Optional number of points per second. Use to reduce the number of points and compute the number of datapoints to return.
|
|
29
|
-
features?: AudioFeaturesOptions
|
|
30
|
-
featuresExtratorUrl?: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const extractAudioAnalysis = async ({
|
|
34
|
-
fileUri,
|
|
35
|
-
pointsPerSecond = 20,
|
|
36
|
-
arrayBuffer,
|
|
37
|
-
bitDepth,
|
|
38
|
-
skipWavHeader = true,
|
|
39
|
-
durationMs,
|
|
40
|
-
sampleRate,
|
|
41
|
-
numberOfChannels,
|
|
42
|
-
algorithm = 'rms',
|
|
43
|
-
features,
|
|
44
|
-
featuresExtratorUrl,
|
|
45
|
-
}: ExtractAudioAnalysisProps): Promise<AudioAnalysis> => {
|
|
46
|
-
if (isWeb) {
|
|
47
|
-
if (!arrayBuffer && !fileUri) {
|
|
48
|
-
throw new Error('Either arrayBuffer or fileUri must be provided')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!arrayBuffer) {
|
|
52
|
-
logger.log(`fetching fileUri`, fileUri)
|
|
53
|
-
const response = await fetch(fileUri!)
|
|
54
|
-
|
|
55
|
-
if (!response.ok) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
`Failed to fetch fileUri: ${response.statusText}`
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
arrayBuffer = await response.arrayBuffer()
|
|
62
|
-
logger.log(`fetched fileUri`, arrayBuffer.byteLength, arrayBuffer)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Create a new copy of the ArrayBuffer to avoid detachment issues
|
|
66
|
-
const bufferCopy = arrayBuffer.slice(0)
|
|
67
|
-
logger.log(
|
|
68
|
-
`extractAudioAnalysis skipWavHeader=${skipWavHeader} bitDepth=${bitDepth} len=${bufferCopy.byteLength}`,
|
|
69
|
-
bufferCopy.slice(0, 100)
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
let actualBitDepth = bitDepth
|
|
73
|
-
if (!actualBitDepth) {
|
|
74
|
-
logger.log(
|
|
75
|
-
`extractAudioAnalysis bitDepth not provided -- getting wav file info`
|
|
76
|
-
)
|
|
77
|
-
const fileInfo = await getWavFileInfo(bufferCopy)
|
|
78
|
-
actualBitDepth = fileInfo.bitDepth
|
|
79
|
-
}
|
|
80
|
-
logger.log(`extractAudioAnalysis actualBitDepth=${actualBitDepth}`)
|
|
81
|
-
|
|
82
|
-
const {
|
|
83
|
-
pcmValues: channelData,
|
|
84
|
-
min,
|
|
85
|
-
max,
|
|
86
|
-
} = await convertPCMToFloat32({
|
|
87
|
-
buffer: arrayBuffer,
|
|
88
|
-
bitDepth: actualBitDepth,
|
|
89
|
-
skipWavHeader,
|
|
90
|
-
})
|
|
91
|
-
logger.log(
|
|
92
|
-
`extractAudioAnalysis skipWaveHeader=${skipWavHeader} convertPCMToFloat32 length=${channelData.length} range: [ ${min} :: ${max} ]`
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
return new Promise((resolve, reject) => {
|
|
96
|
-
let worker: Worker
|
|
97
|
-
if (featuresExtratorUrl) {
|
|
98
|
-
worker = new Worker(
|
|
99
|
-
new URL(featuresExtratorUrl, window.location.href)
|
|
100
|
-
)
|
|
101
|
-
} else {
|
|
102
|
-
const blob = new Blob([InlineFeaturesExtractor], {
|
|
103
|
-
type: 'application/javascript',
|
|
104
|
-
})
|
|
105
|
-
const url = URL.createObjectURL(blob)
|
|
106
|
-
worker = new Worker(url)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
worker.onmessage = (event) => {
|
|
110
|
-
resolve(event.data.result)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
worker.onerror = (error) => {
|
|
114
|
-
reject(error)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
worker.postMessage({
|
|
118
|
-
command: 'process',
|
|
119
|
-
channelData,
|
|
120
|
-
sampleRate,
|
|
121
|
-
pointsPerSecond,
|
|
122
|
-
algorithm,
|
|
123
|
-
bitDepth,
|
|
124
|
-
fullAudioDurationMs: durationMs,
|
|
125
|
-
numberOfChannels,
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
} else {
|
|
129
|
-
if (!fileUri) {
|
|
130
|
-
throw new Error('fileUri is required')
|
|
131
|
-
}
|
|
132
|
-
logger.log(`extractAudioAnalysis`, {
|
|
133
|
-
fileUri,
|
|
134
|
-
pointsPerSecond,
|
|
135
|
-
algorithm,
|
|
136
|
-
})
|
|
137
|
-
const res = await ExpoAudioStreamModule.extractAudioAnalysis({
|
|
138
|
-
fileUri,
|
|
139
|
-
pointsPerSecond,
|
|
140
|
-
skipWavHeader,
|
|
141
|
-
algorithm,
|
|
142
|
-
features,
|
|
143
|
-
})
|
|
144
|
-
logger.log(`extractAudioAnalysis`, res)
|
|
145
|
-
return res
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import ExpoAudioStreamModule from '../ExpoAudioStreamModule'
|
|
2
|
-
import { getLogger } from '../logger'
|
|
3
|
-
|
|
4
|
-
const logger = getLogger('extractWaveform')
|
|
5
|
-
export interface ExtractWaveformProps {
|
|
6
|
-
fileUri: string
|
|
7
|
-
numberOfSamples: number
|
|
8
|
-
offset?: number
|
|
9
|
-
length?: number
|
|
10
|
-
}
|
|
11
|
-
export const extractWaveform = async ({
|
|
12
|
-
fileUri,
|
|
13
|
-
numberOfSamples,
|
|
14
|
-
offset = 0,
|
|
15
|
-
length,
|
|
16
|
-
}: ExtractWaveformProps): Promise<unknown> => {
|
|
17
|
-
const res = await ExpoAudioStreamModule.extractAudioAnalysis({
|
|
18
|
-
fileUri,
|
|
19
|
-
numberOfSamples,
|
|
20
|
-
offset,
|
|
21
|
-
length,
|
|
22
|
-
})
|
|
23
|
-
logger.log(`extractWaveform`, res)
|
|
24
|
-
return res
|
|
25
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// packages/expo-audio-stream/src/AudioRecorder.provider.tsx
|
|
2
|
-
import React, { createContext, useContext } from 'react'
|
|
3
|
-
|
|
4
|
-
import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
|
|
5
|
-
import {
|
|
6
|
-
AudioRecording,
|
|
7
|
-
RecordingConfig,
|
|
8
|
-
StartRecordingResult,
|
|
9
|
-
} from './ExpoAudioStream.types'
|
|
10
|
-
import { UseAudioRecorderProps, useAudioRecorder } from './useAudioRecorder'
|
|
11
|
-
|
|
12
|
-
export interface UseAudioRecorderState {
|
|
13
|
-
startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>
|
|
14
|
-
stopRecording: () => Promise<AudioRecording | null>
|
|
15
|
-
pauseRecording: () => void
|
|
16
|
-
resumeRecording: () => void
|
|
17
|
-
isRecording: boolean
|
|
18
|
-
isPaused: boolean
|
|
19
|
-
durationMs: number // Duration of the recording
|
|
20
|
-
size: number // Size in bytes of the recorded audio
|
|
21
|
-
analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const initContext: UseAudioRecorderState = {
|
|
25
|
-
isRecording: false,
|
|
26
|
-
isPaused: false,
|
|
27
|
-
durationMs: 0,
|
|
28
|
-
size: 0,
|
|
29
|
-
startRecording: async () => {
|
|
30
|
-
throw new Error('AudioRecorderProvider not found')
|
|
31
|
-
},
|
|
32
|
-
stopRecording: async () => {
|
|
33
|
-
throw new Error('AudioRecorderProvider not found')
|
|
34
|
-
},
|
|
35
|
-
pauseRecording: () => {
|
|
36
|
-
throw new Error('AudioRecorderProvider not found')
|
|
37
|
-
},
|
|
38
|
-
resumeRecording: () => {
|
|
39
|
-
throw new Error('AudioRecorderProvider not found')
|
|
40
|
-
},
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const AudioRecorderContext = createContext<UseAudioRecorderState>(initContext)
|
|
44
|
-
|
|
45
|
-
interface AudioRecorderProviderProps {
|
|
46
|
-
children: React.ReactNode
|
|
47
|
-
config?: UseAudioRecorderProps
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export const AudioRecorderProvider: React.FC<AudioRecorderProviderProps> = ({
|
|
51
|
-
children,
|
|
52
|
-
config = {},
|
|
53
|
-
}) => {
|
|
54
|
-
const audioRecorder = useAudioRecorder(config)
|
|
55
|
-
return (
|
|
56
|
-
<AudioRecorderContext.Provider value={audioRecorder}>
|
|
57
|
-
{children}
|
|
58
|
-
</AudioRecorderContext.Provider>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export const useSharedAudioRecorder = () => {
|
|
63
|
-
const context = useContext(AudioRecorderContext)
|
|
64
|
-
if (!context) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
'useSharedAudioRecorder must be used within an AudioRecorderProvider'
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
return context
|
|
70
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// packages/expo-audio-stream/src/ExpoAudioStream.types.ts
|
|
2
|
-
import {
|
|
3
|
-
AmplitudeAlgorithm,
|
|
4
|
-
AudioAnalysis,
|
|
5
|
-
AudioFeaturesOptions,
|
|
6
|
-
} from './AudioAnalysis/AudioAnalysis.types'
|
|
7
|
-
import { AudioAnalysisEvent } from './events'
|
|
8
|
-
|
|
9
|
-
export interface AudioStreamStatus {
|
|
10
|
-
isRecording: boolean
|
|
11
|
-
isPaused: boolean
|
|
12
|
-
durationMs: number
|
|
13
|
-
size: number
|
|
14
|
-
interval: number
|
|
15
|
-
mimeType: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface AudioDataEvent {
|
|
19
|
-
data: string | Float32Array
|
|
20
|
-
position: number
|
|
21
|
-
fileUri: string
|
|
22
|
-
eventDataSize: number
|
|
23
|
-
totalSize: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type EncodingType = 'pcm_32bit' | 'pcm_16bit' | 'pcm_8bit'
|
|
27
|
-
export type SampleRate = 16000 | 44100 | 48000
|
|
28
|
-
export type BitDepth = 8 | 16 | 32
|
|
29
|
-
|
|
30
|
-
export interface Chunk {
|
|
31
|
-
text: string
|
|
32
|
-
timestamp: [number, number | null]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface TranscriberData {
|
|
36
|
-
id: string
|
|
37
|
-
isBusy: boolean
|
|
38
|
-
text: string
|
|
39
|
-
startTime: number
|
|
40
|
-
endTime: number
|
|
41
|
-
chunks: Chunk[]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface AudioRecording {
|
|
45
|
-
fileUri: string
|
|
46
|
-
filename: string
|
|
47
|
-
durationMs: number
|
|
48
|
-
size: number
|
|
49
|
-
mimeType: string
|
|
50
|
-
channels: number
|
|
51
|
-
bitDepth: BitDepth
|
|
52
|
-
sampleRate: SampleRate
|
|
53
|
-
transcripts?: TranscriberData[]
|
|
54
|
-
wavPCMData?: Float32Array // Full PCM data for the recording in WAV format (only on web, for native use the fileUri)
|
|
55
|
-
analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface StartRecordingResult {
|
|
59
|
-
fileUri: string
|
|
60
|
-
mimeType: string
|
|
61
|
-
channels?: number
|
|
62
|
-
bitDepth?: BitDepth
|
|
63
|
-
sampleRate?: SampleRate
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface RecordingConfig {
|
|
67
|
-
sampleRate?: SampleRate // Sample rate for recording
|
|
68
|
-
channels?: 1 | 2 // 1 or 2 (MONO or STEREO)
|
|
69
|
-
encoding?: EncodingType // Encoding type for the recording
|
|
70
|
-
interval?: number // Interval in milliseconds at which to emit recording data
|
|
71
|
-
|
|
72
|
-
// Optional parameters for audio processing
|
|
73
|
-
enableProcessing?: boolean // Boolean to enable/disable audio processing (default is false)
|
|
74
|
-
pointsPerSecond?: number // Number of data points to extract per second of audio (default is 1000)
|
|
75
|
-
algorithm?: AmplitudeAlgorithm // Algorithm to use for amplitude computation (default is "rms")
|
|
76
|
-
features?: AudioFeaturesOptions // Feature options to extract (default is empty)
|
|
77
|
-
|
|
78
|
-
onAudioStream?: (_: AudioDataEvent) => Promise<void> // Callback function to handle audio stream
|
|
79
|
-
onAudioAnalysis?: (_: AudioAnalysisEvent) => Promise<void> // Callback function to handle audio features extraction results
|
|
80
|
-
}
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
// src/ExpoAudioStreamModule.web.ts
|
|
2
|
-
import { EventEmitter } from 'expo-modules-core'
|
|
3
|
-
|
|
4
|
-
import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
|
|
5
|
-
import {
|
|
6
|
-
AudioRecording,
|
|
7
|
-
AudioStreamStatus,
|
|
8
|
-
BitDepth,
|
|
9
|
-
RecordingConfig,
|
|
10
|
-
StartRecordingResult,
|
|
11
|
-
} from './ExpoAudioStream.types'
|
|
12
|
-
import { WebRecorder } from './WebRecorder.web'
|
|
13
|
-
import { AudioEventPayload } from './events'
|
|
14
|
-
import { getLogger } from './logger'
|
|
15
|
-
import { encodingToBitDepth } from './utils/encodingToBitDepth'
|
|
16
|
-
import { writeWavHeader } from './utils/writeWavHeader'
|
|
17
|
-
|
|
18
|
-
export interface EmitAudioEventProps {
|
|
19
|
-
data: Float32Array
|
|
20
|
-
position: number
|
|
21
|
-
}
|
|
22
|
-
export type EmitAudioEventFunction = (_: EmitAudioEventProps) => void
|
|
23
|
-
export type EmitAudioAnalysisFunction = (_: AudioAnalysis) => void
|
|
24
|
-
|
|
25
|
-
export interface ExpoAudioStreamWebProps {
|
|
26
|
-
audioWorkletUrl: string
|
|
27
|
-
featuresExtratorUrl: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const logger = getLogger('ExpoAudioStreamWeb')
|
|
31
|
-
|
|
32
|
-
export class ExpoAudioStreamWeb extends EventEmitter {
|
|
33
|
-
customRecorder: WebRecorder | null
|
|
34
|
-
audioChunks: ArrayBuffer[]
|
|
35
|
-
isRecording: boolean
|
|
36
|
-
isPaused: boolean
|
|
37
|
-
recordingStartTime: number
|
|
38
|
-
pausedTime: number
|
|
39
|
-
currentDurationMs: number
|
|
40
|
-
currentSize: number
|
|
41
|
-
currentInterval: number
|
|
42
|
-
lastEmittedSize: number
|
|
43
|
-
lastEmittedTime: number
|
|
44
|
-
streamUuid: string | null
|
|
45
|
-
extension: 'webm' | 'wav' = 'wav' // Default extension is 'webm'
|
|
46
|
-
recordingConfig?: RecordingConfig
|
|
47
|
-
bitDepth: BitDepth // Bit depth of the audio
|
|
48
|
-
audioWorkletUrl: string
|
|
49
|
-
featuresExtratorUrl: string
|
|
50
|
-
|
|
51
|
-
constructor({
|
|
52
|
-
audioWorkletUrl,
|
|
53
|
-
featuresExtratorUrl,
|
|
54
|
-
}: ExpoAudioStreamWebProps) {
|
|
55
|
-
const mockNativeModule = {
|
|
56
|
-
addListener: () => {
|
|
57
|
-
// Not used on web
|
|
58
|
-
},
|
|
59
|
-
removeListeners: () => {
|
|
60
|
-
// Not used on web
|
|
61
|
-
},
|
|
62
|
-
}
|
|
63
|
-
super(mockNativeModule) // Pass the mock native module to the parent class
|
|
64
|
-
|
|
65
|
-
this.customRecorder = null
|
|
66
|
-
this.audioChunks = []
|
|
67
|
-
this.isRecording = false
|
|
68
|
-
this.isPaused = false
|
|
69
|
-
this.recordingStartTime = 0
|
|
70
|
-
this.pausedTime = 0
|
|
71
|
-
this.currentDurationMs = 0
|
|
72
|
-
this.currentSize = 0
|
|
73
|
-
this.bitDepth = 32 // Default
|
|
74
|
-
this.currentInterval = 1000 // Default interval in ms
|
|
75
|
-
this.lastEmittedSize = 0
|
|
76
|
-
this.lastEmittedTime = 0
|
|
77
|
-
this.streamUuid = null // Initialize UUID on first recording start
|
|
78
|
-
this.audioWorkletUrl = audioWorkletUrl
|
|
79
|
-
this.featuresExtratorUrl = featuresExtratorUrl
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Utility to handle user media stream
|
|
83
|
-
async getMediaStream() {
|
|
84
|
-
try {
|
|
85
|
-
return await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.error('Failed to get media stream:', error)
|
|
88
|
-
throw error
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Start recording with options
|
|
93
|
-
async startRecording(recordingConfig: RecordingConfig = {}) {
|
|
94
|
-
if (this.isRecording) {
|
|
95
|
-
throw new Error('Recording is already in progress')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
this.bitDepth = encodingToBitDepth({
|
|
99
|
-
encoding: recordingConfig.encoding ?? 'pcm_32bit',
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const audioContext = new (window.AudioContext ||
|
|
103
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
104
|
-
// @ts-ignore - Allow webkitAudioContext for Safari
|
|
105
|
-
window.webkitAudioContext)()
|
|
106
|
-
const stream = await this.getMediaStream()
|
|
107
|
-
|
|
108
|
-
const source = audioContext.createMediaStreamSource(stream)
|
|
109
|
-
|
|
110
|
-
this.customRecorder = new WebRecorder({
|
|
111
|
-
audioContext,
|
|
112
|
-
source,
|
|
113
|
-
recordingConfig,
|
|
114
|
-
audioWorkletUrl: this.audioWorkletUrl,
|
|
115
|
-
emitAudioEventCallback: ({
|
|
116
|
-
data,
|
|
117
|
-
position,
|
|
118
|
-
}: EmitAudioEventProps) => {
|
|
119
|
-
this.audioChunks.push(data)
|
|
120
|
-
this.currentSize += data.byteLength
|
|
121
|
-
this.emitAudioEvent({ data, position })
|
|
122
|
-
this.lastEmittedTime = Date.now()
|
|
123
|
-
this.lastEmittedSize = this.currentSize
|
|
124
|
-
},
|
|
125
|
-
emitAudioAnalysisCallback: (audioAnalysisData: AudioAnalysis) => {
|
|
126
|
-
logger.log(`Emitted AudioAnalysis:`, audioAnalysisData)
|
|
127
|
-
this.emit('AudioAnalysis', audioAnalysisData)
|
|
128
|
-
},
|
|
129
|
-
})
|
|
130
|
-
await this.customRecorder.init()
|
|
131
|
-
this.customRecorder.start()
|
|
132
|
-
|
|
133
|
-
// // Set a timer to stop recording after 5 seconds
|
|
134
|
-
// setTimeout(() => {
|
|
135
|
-
// logger.log("AUTO Stopping recording");
|
|
136
|
-
// this.customRecorder?.stopAndPlay();
|
|
137
|
-
// this.isRecording = false;
|
|
138
|
-
// }, 3000);
|
|
139
|
-
|
|
140
|
-
this.isRecording = true
|
|
141
|
-
this.recordingConfig = recordingConfig
|
|
142
|
-
this.recordingStartTime = Date.now()
|
|
143
|
-
this.pausedTime = 0
|
|
144
|
-
this.lastEmittedSize = 0
|
|
145
|
-
this.lastEmittedTime = 0
|
|
146
|
-
this.streamUuid = Date.now().toString()
|
|
147
|
-
const fileUri = `${this.streamUuid}.${this.extension}`
|
|
148
|
-
const streamConfig: StartRecordingResult = {
|
|
149
|
-
fileUri,
|
|
150
|
-
mimeType: `audio/${this.extension}`,
|
|
151
|
-
bitDepth: this.bitDepth,
|
|
152
|
-
channels: recordingConfig.channels ?? 1,
|
|
153
|
-
sampleRate: recordingConfig.sampleRate ?? 44100,
|
|
154
|
-
}
|
|
155
|
-
return streamConfig
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
emitAudioEvent({ data, position }: EmitAudioEventProps) {
|
|
159
|
-
const fileUri = `${this.streamUuid}.${this.extension}`
|
|
160
|
-
const audioEventPayload: AudioEventPayload = {
|
|
161
|
-
fileUri,
|
|
162
|
-
mimeType: `audio/${this.extension}`,
|
|
163
|
-
lastEmittedSize: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
|
|
164
|
-
deltaSize: data.byteLength,
|
|
165
|
-
position,
|
|
166
|
-
totalSize: this.currentSize,
|
|
167
|
-
buffer: data,
|
|
168
|
-
streamUuid: this.streamUuid ?? '', // Generate or manage UUID for stream identification
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
this.emit('AudioData', audioEventPayload)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Stop recording
|
|
175
|
-
async stopRecording(): Promise<AudioRecording> {
|
|
176
|
-
if (!this.customRecorder) {
|
|
177
|
-
throw new Error('Recorder is not initialized')
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const fullPcmBufferArray = await this.customRecorder.stop()
|
|
181
|
-
|
|
182
|
-
// concat all audio chunks
|
|
183
|
-
logger.debug(`Stopped recording`, fullPcmBufferArray)
|
|
184
|
-
this.isRecording = false
|
|
185
|
-
this.currentDurationMs = Date.now() - this.recordingStartTime
|
|
186
|
-
|
|
187
|
-
const wavConfig = {
|
|
188
|
-
buffer: fullPcmBufferArray.buffer,
|
|
189
|
-
sampleRate: this.recordingConfig?.sampleRate ?? 44100,
|
|
190
|
-
numChannels: this.recordingConfig?.channels ?? 1,
|
|
191
|
-
bitDepth: this.bitDepth,
|
|
192
|
-
}
|
|
193
|
-
logger.debug(`Writing wav header`, wavConfig)
|
|
194
|
-
const wavBuffer = writeWavHeader(wavConfig).slice(0)
|
|
195
|
-
|
|
196
|
-
// Create blob fileUri from audio chunks
|
|
197
|
-
const blob = new Blob([wavBuffer], {
|
|
198
|
-
type: `audio/${this.extension}`,
|
|
199
|
-
})
|
|
200
|
-
const fileUri = URL.createObjectURL(blob)
|
|
201
|
-
|
|
202
|
-
const result: AudioRecording = {
|
|
203
|
-
fileUri,
|
|
204
|
-
filename: `${this.streamUuid}.${this.extension}`,
|
|
205
|
-
wavPCMData: fullPcmBufferArray,
|
|
206
|
-
bitDepth: this.bitDepth,
|
|
207
|
-
channels: this.recordingConfig?.channels ?? 1,
|
|
208
|
-
sampleRate: this.recordingConfig?.sampleRate ?? 44100,
|
|
209
|
-
durationMs: this.currentDurationMs,
|
|
210
|
-
size: this.currentSize,
|
|
211
|
-
mimeType: `audio/${this.extension}`,
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return result
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Pause recording
|
|
218
|
-
async pauseRecording() {
|
|
219
|
-
if (!this.isRecording || this.isPaused) {
|
|
220
|
-
throw new Error('Recording is not active or already paused')
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (this.customRecorder) {
|
|
224
|
-
this.customRecorder.pause()
|
|
225
|
-
}
|
|
226
|
-
this.isPaused = true
|
|
227
|
-
this.pausedTime = Date.now()
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Resume recording
|
|
231
|
-
async resumeRecording() {
|
|
232
|
-
if (!this.isPaused) {
|
|
233
|
-
throw new Error('Recording is not paused')
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (this.customRecorder) {
|
|
237
|
-
this.customRecorder.resume()
|
|
238
|
-
}
|
|
239
|
-
this.isPaused = false
|
|
240
|
-
this.recordingStartTime += Date.now() - this.pausedTime
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Get current status
|
|
244
|
-
status() {
|
|
245
|
-
const status: AudioStreamStatus = {
|
|
246
|
-
isRecording: this.isRecording,
|
|
247
|
-
isPaused: this.isPaused,
|
|
248
|
-
durationMs: Date.now() - this.recordingStartTime,
|
|
249
|
-
size: this.currentSize,
|
|
250
|
-
interval: this.currentInterval,
|
|
251
|
-
mimeType: `audio/${this.extension}`,
|
|
252
|
-
}
|
|
253
|
-
return status
|
|
254
|
-
}
|
|
255
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { requireNativeModule } from 'expo-modules-core'
|
|
2
|
-
import { Platform } from 'react-native'
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
ExpoAudioStreamWeb,
|
|
6
|
-
ExpoAudioStreamWebProps,
|
|
7
|
-
} from './ExpoAudioStream.web'
|
|
8
|
-
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
-
let ExpoAudioStreamModule: any
|
|
11
|
-
|
|
12
|
-
if (Platform.OS === 'web') {
|
|
13
|
-
let instance: ExpoAudioStreamWeb | null = null
|
|
14
|
-
|
|
15
|
-
ExpoAudioStreamModule = (webProps: ExpoAudioStreamWebProps) => {
|
|
16
|
-
if (!instance) {
|
|
17
|
-
instance = new ExpoAudioStreamWeb(webProps)
|
|
18
|
-
}
|
|
19
|
-
return instance
|
|
20
|
-
}
|
|
21
|
-
ExpoAudioStreamModule.requestPermissionsAsync = async () => {
|
|
22
|
-
return { status: 'granted' }
|
|
23
|
-
}
|
|
24
|
-
ExpoAudioStreamModule.getPermissionsAsync = async () => {
|
|
25
|
-
return { status: 'granted' }
|
|
26
|
-
}
|
|
27
|
-
} else {
|
|
28
|
-
ExpoAudioStreamModule = requireNativeModule('ExpoAudioStream')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export default ExpoAudioStreamModule
|