@lumavox/react-native 0.14.1 → 0.16.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 +12 -0
- package/dist/audio.native.d.ts +77 -8
- package/dist/audio.native.js +118 -24
- package/dist/audio.web.d.ts +1 -0
- package/dist/audio.web.js +32 -41
- package/dist/device-audio.d.ts +23 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -58,6 +58,18 @@ function Talk({ agentId }: { agentId: string }) {
|
|
|
58
58
|
echo; on a phone it is refused until `react-native-audio-api` 1.0 does —
|
|
59
59
|
`listen({ assumeNoEcho: true })` with headphones on. Background audio
|
|
60
60
|
comes with 1.0 too.
|
|
61
|
+
- **A clock for a lip-synced face or subtitles**, on a phone and the web:
|
|
62
|
+
`audio.onScheduled(({ pcm, sample_rate, chunk, startsAt }) => …)` tells
|
|
63
|
+
every frame as it is queued, before it plays; draw against
|
|
64
|
+
`audio.heardTime()`; `audio.onStopped` when a stop drops what was
|
|
65
|
+
queued; `audio.onCapture` for the microphone's frames.
|
|
66
|
+
`createAudio({ leadInMs: 350 })` gives a model that long to read a
|
|
67
|
+
reply's first frame (and adds it to the first word); `outputLatencyMs`
|
|
68
|
+
is what the phone's sound takes to the ear, which the library cannot
|
|
69
|
+
say — Bluetooth earbuds add about 150–250 ms.
|
|
70
|
+
- On the web the app may choose the microphone, its processing and the
|
|
71
|
+
speaker (`inputs()`, `setInput`, `setProcessing`, `outputs()`,
|
|
72
|
+
`setOutputDevice`); on a phone the system does — `[]` and `false`.
|
|
61
73
|
|
|
62
74
|
The guide, with the bare React Native setup: the docs' **SDKs** tab,
|
|
63
75
|
*React Native*.
|
package/dist/audio.native.d.ts
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A phone's player and recorder, on `react-native-audio-api` 0.13.6: what
|
|
3
|
+
* Metro picks for `./audio` on iOS and Android (`index.ts`). With
|
|
4
|
+
* `session.ts`, the only file that touches the library.
|
|
5
|
+
*
|
|
6
|
+
* - **The reply plays through one `AudioBufferQueueSourceNode`**, started
|
|
7
|
+
* once: each frame's PCM is a buffer enqueued behind the last, gapless,
|
|
8
|
+
* and a queue that runs dry plays silence until the next. `clearBuffers`
|
|
9
|
+
* stops it at once.
|
|
10
|
+
* - **The context runs at the reply's rate**, rebuilt — what is queued
|
|
11
|
+
* dropped — when an `audio.start` brings another: 0.13.6's queue plays
|
|
12
|
+
* every buffer at its context's rate whatever the buffer says (its
|
|
13
|
+
* `AudioBufferBaseSourceNode`), so a 24 kHz reply in a 48 kHz context
|
|
14
|
+
* would play an octave up. The device's own resampling does the rest.
|
|
15
|
+
* - **The agent stopped speaking when the last buffer enqueued has ended**,
|
|
16
|
+
* counted by the ids `enqueueBuffer` answers: the queue's own
|
|
17
|
+
* `isLastBufferInQueue` can say so just as the next frame is enqueued, and
|
|
18
|
+
* a buffer `clearBuffers` dropped never ends.
|
|
19
|
+
* - **The clock is the context's, and the playhead ours**: the queue says
|
|
20
|
+
* nothing of when a buffer starts, so each frame starts where the last
|
|
21
|
+
* ends — `max(playhead, currentTime)`, as the web's player keeps it — and
|
|
22
|
+
* is told so (`onScheduled`). A lead-in (`leadInMs`) is a buffer of
|
|
23
|
+
* silence enqueued ahead of a frame that finds the queue dry. A new rate's
|
|
24
|
+
* context starts its clock at 0 again, what the old one queued stopped.
|
|
25
|
+
* - **What is heard is the clock less the app's `outputLatencyMs`**:
|
|
26
|
+
* 0.13.6 says nothing of the device's latency — its source nodes'
|
|
27
|
+
* `getLatency()` is their time-stretcher's — so the app, which can know
|
|
28
|
+
* its earbuds are on, says it.
|
|
29
|
+
* - **The microphone records at 16 kHz**, mono, 100 ms a buffer — less to
|
|
30
|
+
* send than 24 kHz, and a rate the socket takes. `onAudioReady` hands the
|
|
31
|
+
* samples over JSI as floats, at the rate the device chose, which need
|
|
32
|
+
* not be the one asked: read from the buffer and resampled (`resample`).
|
|
33
|
+
* - **An interruption** — a call, Siri, another app, the app leaving the
|
|
34
|
+
* screen — suspends the context, so the reply's frames keep queueing and
|
|
35
|
+
* play from where they stopped; and closes the recorder, whose audio is
|
|
36
|
+
* thrown away (the hook discards the recording on the socket). The audio
|
|
37
|
+
* comes back on its own when the system says it may, else on `resume()`
|
|
38
|
+
* or the next press.
|
|
39
|
+
* - **No hands-free**: 0.13.6 cancels no echo, so a microphone left open
|
|
40
|
+
* hears the agent (D12, D16). `handsFree` refuses until 1.0; with
|
|
41
|
+
* headphones on, the app passes `assumeNoEcho`.
|
|
42
|
+
*/
|
|
43
|
+
import { type AgentAudio, type AudioDevice, type ScheduledAudio } from "@lumavox/client";
|
|
1
44
|
import type { AudioOptions, CreateAudio, DeviceAudio, Output } from "./device-audio.js";
|
|
2
45
|
/** Hz the microphone records at unless a rate the socket takes is asked. */
|
|
3
46
|
export declare const RECORD_RATE = 16000;
|
|
@@ -5,23 +48,42 @@ export declare const RECORD_RATE = 16000;
|
|
|
5
48
|
export declare const PHONE_ECHO = "Hands-free needs echo cancellation, which comes with react-native-audio-api 1.0. Use push-to-talk, or assumeNoEcho with headphones on.";
|
|
6
49
|
export declare class PhoneAudio implements DeviceAudio {
|
|
7
50
|
interrupted: boolean;
|
|
51
|
+
/** On a phone the system chooses the speaker. */
|
|
52
|
+
readonly canChooseOutput = false;
|
|
8
53
|
private context;
|
|
9
54
|
private queue;
|
|
10
55
|
/** The ids of the buffers enqueued and not yet played. */
|
|
11
56
|
private readonly playing;
|
|
57
|
+
/** Where the last buffer enqueued ends, on the context's clock. */
|
|
58
|
+
private playhead;
|
|
12
59
|
/** A byte of a sample split across two frames. */
|
|
13
60
|
private carry;
|
|
14
61
|
private capture;
|
|
15
62
|
private readonly drained;
|
|
63
|
+
private readonly stopped;
|
|
64
|
+
private readonly scheduled;
|
|
65
|
+
private readonly captured;
|
|
16
66
|
private readonly interruptions;
|
|
17
67
|
private readonly unwatch;
|
|
68
|
+
/** Seconds. */
|
|
69
|
+
private readonly leadIn;
|
|
70
|
+
private readonly latency;
|
|
18
71
|
constructor(options?: AudioOptions);
|
|
19
72
|
prime(): Promise<void>;
|
|
20
|
-
play(pcm: ArrayBuffer, frame:
|
|
21
|
-
sample_rate: number;
|
|
22
|
-
}): void;
|
|
73
|
+
play(pcm: ArrayBuffer, frame: Parameters<AgentAudio["play"]>[1]): void;
|
|
23
74
|
stop(): void;
|
|
24
75
|
onDrained(fn: () => void): () => void;
|
|
76
|
+
now(): number;
|
|
77
|
+
heardTime(): number;
|
|
78
|
+
onScheduled(fn: (audio: ScheduledAudio) => void): () => void;
|
|
79
|
+
onStopped(fn: () => void): () => void;
|
|
80
|
+
onCapture(fn: (pcm: ArrayBuffer, rate: number) => void): () => void;
|
|
81
|
+
/** The system's microphone and speaker: none listed, none set. */
|
|
82
|
+
inputs(): Promise<AudioDevice[]>;
|
|
83
|
+
outputs(): Promise<AudioDevice[]>;
|
|
84
|
+
setInput(): void;
|
|
85
|
+
setProcessing(): void;
|
|
86
|
+
setOutputDevice(): Promise<boolean>;
|
|
25
87
|
/** At 16 kHz, or at the rate asked when the socket takes it. */
|
|
26
88
|
startCapture(onPcm: (pcm: ArrayBuffer) => void, options?: {
|
|
27
89
|
rate?: number;
|
|
@@ -34,18 +96,25 @@ export declare class PhoneAudio implements DeviceAudio {
|
|
|
34
96
|
handsFree(): ReturnType<DeviceAudio["handsFree"]>;
|
|
35
97
|
onInterruption(fn: (interrupted: boolean) => void): () => void;
|
|
36
98
|
dispose(): void;
|
|
99
|
+
/** Behind what is queued; the playhead moves to its end. */
|
|
100
|
+
private enqueue;
|
|
37
101
|
/** The queue for a reply at `rate`: this one, or a new context's. */
|
|
38
102
|
private player;
|
|
39
103
|
/**
|
|
40
|
-
* The wire's PCM
|
|
41
|
-
* the edge sends whole samples (`apps/edge/src/voice/tts.ts`),
|
|
42
|
-
* byte dropped shifts every byte after it and turns the rest
|
|
43
|
-
* the bug that once made the console's playback white noise.
|
|
104
|
+
* The wire's PCM in whole samples. **A split sample is carried, never
|
|
105
|
+
* dropped**: the edge sends whole samples (`apps/edge/src/voice/tts.ts`),
|
|
106
|
+
* but an odd byte dropped shifts every byte after it and turns the rest
|
|
107
|
+
* into noise — the bug that once made the console's playback white noise.
|
|
44
108
|
*/
|
|
45
|
-
private
|
|
109
|
+
private whole;
|
|
46
110
|
/** The audio was taken: the reply waits, the recording is thrown away. */
|
|
47
111
|
private interrupt;
|
|
48
112
|
private quiet;
|
|
113
|
+
/**
|
|
114
|
+
* Call each of the app's handlers. One that throws is rethrown on its own
|
|
115
|
+
* turn, where the app's error reporting sees it — not here, where it
|
|
116
|
+
* would stop the player or the recorder halfway.
|
|
117
|
+
*/
|
|
49
118
|
private tell;
|
|
50
119
|
}
|
|
51
120
|
export declare const createAudio: CreateAudio;
|
package/dist/audio.native.js
CHANGED
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
* counted by the ids `enqueueBuffer` answers: the queue's own
|
|
17
17
|
* `isLastBufferInQueue` can say so just as the next frame is enqueued, and
|
|
18
18
|
* a buffer `clearBuffers` dropped never ends.
|
|
19
|
+
* - **The clock is the context's, and the playhead ours**: the queue says
|
|
20
|
+
* nothing of when a buffer starts, so each frame starts where the last
|
|
21
|
+
* ends — `max(playhead, currentTime)`, as the web's player keeps it — and
|
|
22
|
+
* is told so (`onScheduled`). A lead-in (`leadInMs`) is a buffer of
|
|
23
|
+
* silence enqueued ahead of a frame that finds the queue dry. A new rate's
|
|
24
|
+
* context starts its clock at 0 again, what the old one queued stopped.
|
|
25
|
+
* - **What is heard is the clock less the app's `outputLatencyMs`**:
|
|
26
|
+
* 0.13.6 says nothing of the device's latency — its source nodes'
|
|
27
|
+
* `getLatency()` is their time-stretcher's — so the app, which can know
|
|
28
|
+
* its earbuds are on, says it.
|
|
19
29
|
* - **The microphone records at 16 kHz**, mono, 100 ms a buffer — less to
|
|
20
30
|
* send than 24 kHz, and a rate the socket takes. `onAudioReady` hands the
|
|
21
31
|
* samples over JSI as floats, at the rate the device chose, which need
|
|
@@ -42,15 +52,24 @@ const RECORD_SECONDS = 0.1;
|
|
|
42
52
|
export class PhoneAudio {
|
|
43
53
|
constructor(options = {}) {
|
|
44
54
|
this.interrupted = false;
|
|
55
|
+
/** On a phone the system chooses the speaker. */
|
|
56
|
+
this.canChooseOutput = false;
|
|
45
57
|
this.context = null;
|
|
46
58
|
this.queue = null;
|
|
47
59
|
/** The ids of the buffers enqueued and not yet played. */
|
|
48
60
|
this.playing = new Set();
|
|
61
|
+
/** Where the last buffer enqueued ends, on the context's clock. */
|
|
62
|
+
this.playhead = 0;
|
|
49
63
|
/** A byte of a sample split across two frames. */
|
|
50
64
|
this.carry = null;
|
|
51
65
|
this.capture = null;
|
|
52
66
|
this.drained = new Set();
|
|
67
|
+
this.stopped = new Set();
|
|
68
|
+
this.scheduled = new Set();
|
|
69
|
+
this.captured = new Set();
|
|
53
70
|
this.interruptions = new Set();
|
|
71
|
+
this.leadIn = (options.leadInMs ?? 0) / 1000;
|
|
72
|
+
this.latency = (options.outputLatencyMs ?? 0) / 1000;
|
|
54
73
|
session.configure(options.output ?? "speaker");
|
|
55
74
|
this.unwatch = session.watch(({ began, shouldResume }) => {
|
|
56
75
|
if (began)
|
|
@@ -67,28 +86,80 @@ export class PhoneAudio {
|
|
|
67
86
|
this.player(this.context?.sampleRate ?? DEFAULT_SAMPLE_RATE);
|
|
68
87
|
}
|
|
69
88
|
play(pcm, frame) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
const rate = frame.sample_rate;
|
|
90
|
+
this.player(rate);
|
|
91
|
+
const whole = this.whole(pcm);
|
|
92
|
+
if (whole.byteLength === 0)
|
|
73
93
|
return;
|
|
74
|
-
const
|
|
94
|
+
const context = this.context;
|
|
95
|
+
const samples = pcm16ToFloat(whole);
|
|
96
|
+
const buffer = context.createBuffer(1, samples.length, rate);
|
|
75
97
|
buffer.copyToChannel(samples, 0);
|
|
76
|
-
|
|
98
|
+
// Read once the buffer is made, as near its enqueue as can be: copying
|
|
99
|
+
// a frame over JSI took 11 ms on Android's emulator, and a frame that
|
|
100
|
+
// starts at once was told as starting that long before it was queued.
|
|
101
|
+
const now = context.currentTime;
|
|
102
|
+
if (this.playhead <= now) {
|
|
103
|
+
// The queue ran dry: this frame starts a run, after the lead-in.
|
|
104
|
+
this.playhead = now;
|
|
105
|
+
// Never an empty buffer: the library throws on a length of 0.
|
|
106
|
+
const silence = Math.round(this.leadIn * rate);
|
|
107
|
+
if (silence > 0)
|
|
108
|
+
this.enqueue(context.createBuffer(1, silence, rate));
|
|
109
|
+
}
|
|
110
|
+
const startsAt = this.playhead;
|
|
111
|
+
this.enqueue(buffer);
|
|
112
|
+
this.tell(this.scheduled, {
|
|
113
|
+
pcm: whole,
|
|
114
|
+
sample_rate: rate,
|
|
115
|
+
channels: frame.channels,
|
|
116
|
+
chunk: frame.chunk,
|
|
117
|
+
turn: frame.turn,
|
|
118
|
+
startsAt,
|
|
119
|
+
duration: buffer.duration,
|
|
120
|
+
});
|
|
77
121
|
}
|
|
78
122
|
stop() {
|
|
79
123
|
const was = this.playing.size > 0;
|
|
80
124
|
this.queue?.clearBuffers();
|
|
81
125
|
this.playing.clear();
|
|
126
|
+
this.playhead = 0;
|
|
82
127
|
// Half a sample of audio that will never play now.
|
|
83
128
|
this.carry = null;
|
|
84
|
-
if (was)
|
|
85
|
-
|
|
129
|
+
if (!was)
|
|
130
|
+
return;
|
|
131
|
+
this.tell(this.drained);
|
|
132
|
+
this.tell(this.stopped);
|
|
86
133
|
}
|
|
87
134
|
onDrained(fn) {
|
|
88
|
-
this.drained
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
135
|
+
return add(this.drained, fn);
|
|
136
|
+
}
|
|
137
|
+
now() {
|
|
138
|
+
return this.context?.currentTime ?? 0;
|
|
139
|
+
}
|
|
140
|
+
heardTime() {
|
|
141
|
+
return Math.max(0, this.now() - this.latency);
|
|
142
|
+
}
|
|
143
|
+
onScheduled(fn) {
|
|
144
|
+
return add(this.scheduled, fn);
|
|
145
|
+
}
|
|
146
|
+
onStopped(fn) {
|
|
147
|
+
return add(this.stopped, fn);
|
|
148
|
+
}
|
|
149
|
+
onCapture(fn) {
|
|
150
|
+
return add(this.captured, fn);
|
|
151
|
+
}
|
|
152
|
+
/** The system's microphone and speaker: none listed, none set. */
|
|
153
|
+
async inputs() {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
async outputs() {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
setInput() { }
|
|
160
|
+
setProcessing() { }
|
|
161
|
+
async setOutputDevice() {
|
|
162
|
+
return false;
|
|
92
163
|
}
|
|
93
164
|
/** At 16 kHz, or at the rate asked when the socket takes it. */
|
|
94
165
|
async startCapture(onPcm, options) {
|
|
@@ -118,7 +189,9 @@ export class PhoneAudio {
|
|
|
118
189
|
const at = buffer.sampleRate === rate
|
|
119
190
|
? samples
|
|
120
191
|
: resample(samples, buffer.sampleRate, rate);
|
|
121
|
-
|
|
192
|
+
const pcm = floatToPcm16(at).buffer;
|
|
193
|
+
onPcm(pcm);
|
|
194
|
+
this.tell(this.captured, pcm, rate);
|
|
122
195
|
});
|
|
123
196
|
if (ready.status === "error")
|
|
124
197
|
throw new Error(ready.message);
|
|
@@ -165,10 +238,7 @@ export class PhoneAudio {
|
|
|
165
238
|
return { ok: false, reason: PHONE_ECHO };
|
|
166
239
|
}
|
|
167
240
|
onInterruption(fn) {
|
|
168
|
-
this.interruptions
|
|
169
|
-
return () => {
|
|
170
|
-
this.interruptions.delete(fn);
|
|
171
|
-
};
|
|
241
|
+
return add(this.interruptions, fn);
|
|
172
242
|
}
|
|
173
243
|
dispose() {
|
|
174
244
|
this.stop();
|
|
@@ -178,6 +248,11 @@ export class PhoneAudio {
|
|
|
178
248
|
this.context = null;
|
|
179
249
|
this.queue = null;
|
|
180
250
|
}
|
|
251
|
+
/** Behind what is queued; the playhead moves to its end. */
|
|
252
|
+
enqueue(buffer) {
|
|
253
|
+
this.playing.add(this.queue.enqueueBuffer(buffer));
|
|
254
|
+
this.playhead += buffer.duration;
|
|
255
|
+
}
|
|
181
256
|
/** The queue for a reply at `rate`: this one, or a new context's. */
|
|
182
257
|
player(rate) {
|
|
183
258
|
if (this.queue && this.context?.sampleRate === rate)
|
|
@@ -207,12 +282,12 @@ export class PhoneAudio {
|
|
|
207
282
|
return queue;
|
|
208
283
|
}
|
|
209
284
|
/**
|
|
210
|
-
* The wire's PCM
|
|
211
|
-
* the edge sends whole samples (`apps/edge/src/voice/tts.ts`),
|
|
212
|
-
* byte dropped shifts every byte after it and turns the rest
|
|
213
|
-
* the bug that once made the console's playback white noise.
|
|
285
|
+
* The wire's PCM in whole samples. **A split sample is carried, never
|
|
286
|
+
* dropped**: the edge sends whole samples (`apps/edge/src/voice/tts.ts`),
|
|
287
|
+
* but an odd byte dropped shifts every byte after it and turns the rest
|
|
288
|
+
* into noise — the bug that once made the console's playback white noise.
|
|
214
289
|
*/
|
|
215
|
-
|
|
290
|
+
whole(pcm) {
|
|
216
291
|
let bytes = new Uint8Array(pcm);
|
|
217
292
|
if (this.carry !== null) {
|
|
218
293
|
const joined = new Uint8Array(bytes.length + 1);
|
|
@@ -225,7 +300,7 @@ export class PhoneAudio {
|
|
|
225
300
|
this.carry = bytes[bytes.length - 1];
|
|
226
301
|
bytes = bytes.slice(0, -1);
|
|
227
302
|
}
|
|
228
|
-
return
|
|
303
|
+
return bytes.buffer;
|
|
229
304
|
}
|
|
230
305
|
/** The audio was taken: the reply waits, the recording is thrown away. */
|
|
231
306
|
interrupt() {
|
|
@@ -252,11 +327,30 @@ export class PhoneAudio {
|
|
|
252
327
|
// swallowed: an interruption may have stopped the engine already.
|
|
253
328
|
});
|
|
254
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Call each of the app's handlers. One that throws is rethrown on its own
|
|
332
|
+
* turn, where the app's error reporting sees it — not here, where it
|
|
333
|
+
* would stop the player or the recorder halfway.
|
|
334
|
+
*/
|
|
255
335
|
tell(listeners, ...args) {
|
|
256
|
-
for (const fn of listeners)
|
|
257
|
-
|
|
336
|
+
for (const fn of listeners) {
|
|
337
|
+
try {
|
|
338
|
+
fn(...args);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
throw error;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
258
346
|
}
|
|
259
347
|
}
|
|
348
|
+
function add(listeners, fn) {
|
|
349
|
+
listeners.add(fn);
|
|
350
|
+
return () => {
|
|
351
|
+
listeners.delete(fn);
|
|
352
|
+
};
|
|
353
|
+
}
|
|
260
354
|
export const createAudio = (options) => new PhoneAudio(options);
|
|
261
355
|
/** Ask for the microphone, if it was not granted: whether it is now. */
|
|
262
356
|
export const requestMicrophone = () => session.microphone();
|
package/dist/audio.web.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CreateAudio } from "./device-audio.js";
|
|
2
|
+
/** The lead-in, the microphone and its processing are the browser's; `output` and `outputLatencyMs` the phone's. */
|
|
2
3
|
export declare const createAudio: CreateAudio;
|
|
3
4
|
/** Ask for the microphone, if it was not granted: whether it is now. */
|
|
4
5
|
export declare function requestMicrophone(): Promise<boolean>;
|
package/dist/audio.web.js
CHANGED
|
@@ -1,45 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A browser's player and recorder: what Metro picks for `./audio` on the
|
|
3
|
+
* web (`index.ts`), and what anything picking no platform file gets
|
|
4
|
+
* (`audio.ts`). `react-native-audio-api` has no recorder, no queue and no
|
|
5
|
+
* real permission request on the web, so this is the client's own
|
|
6
|
+
* `@lumavox/client/web-audio` — the console's — with the phone's extras
|
|
7
|
+
* answered as a browser can.
|
|
8
|
+
*
|
|
9
|
+
* - **`prime()` must run in the press that starts the conversation**:
|
|
10
|
+
* browsers start sound only from a gesture.
|
|
11
|
+
* - **No interruptions are told**: a browser pauses a tab's audio itself.
|
|
12
|
+
* - **No earpiece**: `setOutput` does nothing; the speaker is chosen by its
|
|
13
|
+
* id (`setOutputDevice`), where the browser lets a page.
|
|
14
|
+
* - **Hands-free where the browser cancels echo**, as the client's says
|
|
15
|
+
* (`handsFree`), its reply lowered while the person may be talking.
|
|
16
|
+
* - **The client's adapter itself, with the phone's extras added**: every
|
|
17
|
+
* method it has — the clock, the frames, the devices — is passed through
|
|
18
|
+
* by being it, so none can be left out.
|
|
19
|
+
* - **The import needs Metro's package `exports`** — on by default from
|
|
20
|
+
* React Native 0.79 and Expo SDK 53; before, an app sets
|
|
21
|
+
* `unstable_enablePackageExports` (the docs' React Native page).
|
|
22
|
+
*/
|
|
1
23
|
import { createWebAudio } from "@lumavox/client/web-audio";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
stop() {
|
|
14
|
-
this.web.stop();
|
|
15
|
-
}
|
|
16
|
-
onDrained(fn) {
|
|
17
|
-
return this.web.onDrained(fn);
|
|
18
|
-
}
|
|
19
|
-
startCapture(onPcm, options) {
|
|
20
|
-
return this.web.startCapture(onPcm, options);
|
|
21
|
-
}
|
|
22
|
-
stopCapture() {
|
|
23
|
-
return this.web.stopCapture();
|
|
24
|
-
}
|
|
25
|
-
dispose() {
|
|
26
|
-
this.web.dispose();
|
|
27
|
-
}
|
|
28
|
-
resume() {
|
|
29
|
-
return this.web.prime();
|
|
30
|
-
}
|
|
31
|
-
setOutput(_output) { }
|
|
32
|
-
handsFree() {
|
|
33
|
-
return this.web.handsFree();
|
|
34
|
-
}
|
|
35
|
-
duck(on) {
|
|
36
|
-
this.web.duck(on);
|
|
37
|
-
}
|
|
38
|
-
onInterruption(_fn) {
|
|
39
|
-
return () => { };
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
export const createAudio = () => new BrowserAudio();
|
|
24
|
+
/** The lead-in, the microphone and its processing are the browser's; `output` and `outputLatencyMs` the phone's. */
|
|
25
|
+
export const createAudio = (options) => {
|
|
26
|
+
const web = createWebAudio(options);
|
|
27
|
+
return Object.assign(web, {
|
|
28
|
+
interrupted: false,
|
|
29
|
+
resume: () => web.prime(),
|
|
30
|
+
setOutput: () => { },
|
|
31
|
+
onInterruption: () => () => { },
|
|
32
|
+
});
|
|
33
|
+
};
|
|
43
34
|
/** Ask for the microphone, if it was not granted: whether it is now. */
|
|
44
35
|
export async function requestMicrophone() {
|
|
45
36
|
try {
|
package/dist/device-audio.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What `createAudio()` answers on every platform — a phone's
|
|
3
3
|
* (`audio.native.ts`) and a browser's (`audio.web.ts`) — so an app written
|
|
4
|
-
* once runs on both: the `AgentAudio` that `useAgent` drives,
|
|
4
|
+
* once runs on both: the `AgentAudio` that `useAgent` drives, the clock
|
|
5
|
+
* and the frames an app draws against (`PlaybackClock`), the devices a
|
|
6
|
+
* browser lets it choose (`DeviceChoices`, none on a phone), and what an
|
|
5
7
|
* app does with the device itself.
|
|
6
8
|
*/
|
|
7
|
-
import type { AgentAudio } from "@lumavox/client";
|
|
9
|
+
import type { AgentAudio, DeviceChoices, PlaybackClock, Processing } from "@lumavox/client";
|
|
8
10
|
/** Where the agent is heard with no headphones on. */
|
|
9
11
|
export type Output = "speaker" | "earpiece";
|
|
10
12
|
export interface AudioOptions {
|
|
@@ -15,8 +17,26 @@ export interface AudioOptions {
|
|
|
15
17
|
* 1.0 lets the earpiece be chosen.
|
|
16
18
|
*/
|
|
17
19
|
output?: Output;
|
|
20
|
+
/**
|
|
21
|
+
* Milliseconds a reply's first frame — or one after a gap — waits before
|
|
22
|
+
* it plays, for an app that reads each frame first (`onScheduled`): as
|
|
23
|
+
* much is added to the first word heard. None on a phone unless set, 60
|
|
24
|
+
* in a browser.
|
|
25
|
+
*/
|
|
26
|
+
leadInMs?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Milliseconds from the phone's player to the ear, which `heardTime()`
|
|
29
|
+
* takes off the clock: `react-native-audio-api` 0.13 cannot say it, so
|
|
30
|
+
* `0` unless set — Bluetooth earbuds add about 150 to 250. A browser says
|
|
31
|
+
* its own.
|
|
32
|
+
*/
|
|
33
|
+
outputLatencyMs?: number;
|
|
34
|
+
/** The microphone, by its `inputs()` id, in a browser; a phone's system chooses. */
|
|
35
|
+
input?: string;
|
|
36
|
+
/** The browser's cleanup of the microphone; a phone's is the system's. */
|
|
37
|
+
processing?: Processing;
|
|
18
38
|
}
|
|
19
|
-
export interface DeviceAudio extends AgentAudio {
|
|
39
|
+
export interface DeviceAudio extends AgentAudio, PlaybackClock, DeviceChoices {
|
|
20
40
|
/**
|
|
21
41
|
* Get ready to play. **On the web, call it from the press that starts
|
|
22
42
|
* the conversation**: browsers start sound only from a gesture. On a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumavox/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Voice conversations with Lumavox agents in a React Native app, on iOS, Android and the web: the client, the React hooks, and push-to-talk audio on react-native-audio-api.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit && tsc --noEmit -p test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@lumavox/client": "0.
|
|
34
|
-
"@lumavox/react": "0.
|
|
33
|
+
"@lumavox/client": "0.16.0",
|
|
34
|
+
"@lumavox/react": "0.16.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": ">=18",
|