@intellectif/lk-react 13.0.0 → 14.1.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/CHANGELOG.md +61 -0
- package/README.md +11 -3
- package/dist/{WrittenResponse-B5p_-RnJ.d.ts → WrittenResponse-DrzI89Wg.d.ts} +1 -1
- package/dist/{WrittenResponse-DWtS4tOL.d.cts → WrittenResponse-rRpOr9W1.d.cts} +1 -1
- package/dist/{chunk-UU2NV6SU.cjs → chunk-4Y5TELVU.cjs} +14 -13
- package/dist/chunk-4Y5TELVU.cjs.map +1 -0
- package/dist/{chunk-XAGWBVK3.js → chunk-5CP2Z6IS.js} +6 -5
- package/dist/chunk-5CP2Z6IS.js.map +1 -0
- package/dist/{chunk-NVUFEMPZ.cjs → chunk-5JCTJFQU.cjs} +30 -16
- package/dist/chunk-5JCTJFQU.cjs.map +1 -0
- package/dist/chunk-5JFMXBH7.js +888 -0
- package/dist/chunk-5JFMXBH7.js.map +1 -0
- package/dist/{chunk-NVP6UEQQ.js → chunk-AS2QGND5.js} +4 -4
- package/dist/{chunk-NVP6UEQQ.js.map → chunk-AS2QGND5.js.map} +1 -1
- package/dist/{chunk-3Q4I4KT6.js → chunk-AWQ53DON.js} +78 -2
- package/dist/chunk-AWQ53DON.js.map +1 -0
- package/dist/{chunk-DQERNAWI.js → chunk-B4BCEQNO.js} +26 -31
- package/dist/chunk-B4BCEQNO.js.map +1 -0
- package/dist/chunk-BRPEB43C.cjs +13 -0
- package/dist/chunk-BRPEB43C.cjs.map +1 -0
- package/dist/{chunk-74QXBWCI.cjs → chunk-CTW3G3EH.cjs} +6 -6
- package/dist/{chunk-74QXBWCI.cjs.map → chunk-CTW3G3EH.cjs.map} +1 -1
- package/dist/{chunk-AMGK7DDM.cjs → chunk-D4DQFTA7.cjs} +2 -2
- package/dist/chunk-D4DQFTA7.cjs.map +1 -0
- package/dist/{chunk-R7DI5UMU.js → chunk-DB4DLHN6.js} +22 -4
- package/dist/chunk-DB4DLHN6.js.map +1 -0
- package/dist/{chunk-K3EA547J.cjs → chunk-DPEY7DSE.cjs} +27 -33
- package/dist/chunk-DPEY7DSE.cjs.map +1 -0
- package/dist/{chunk-W4MXRZAB.js → chunk-IDYZ2FA2.js} +6 -5
- package/dist/chunk-IDYZ2FA2.js.map +1 -0
- package/dist/{chunk-K7NMNOPI.cjs → chunk-JG4DWAUJ.cjs} +15 -14
- package/dist/chunk-JG4DWAUJ.cjs.map +1 -0
- package/dist/chunk-KIO6QXGT.cjs +894 -0
- package/dist/chunk-KIO6QXGT.cjs.map +1 -0
- package/dist/chunk-MCO52PV5.js +520 -0
- package/dist/chunk-MCO52PV5.js.map +1 -0
- package/dist/{chunk-HT4FFZZQ.cjs → chunk-MZITNWZ2.cjs} +233 -38
- package/dist/chunk-MZITNWZ2.cjs.map +1 -0
- package/dist/{chunk-Z63JRG4M.cjs → chunk-N536RUCZ.cjs} +15 -14
- package/dist/chunk-N536RUCZ.cjs.map +1 -0
- package/dist/{chunk-PSHNTDKG.js → chunk-NJAF34GN.js} +13 -161
- package/dist/chunk-NJAF34GN.js.map +1 -0
- package/dist/chunk-OD65OZIW.cjs +159 -0
- package/dist/chunk-OD65OZIW.cjs.map +1 -0
- package/dist/{chunk-2GXSA322.cjs → chunk-PSUKWVKU.cjs} +15 -14
- package/dist/chunk-PSUKWVKU.cjs.map +1 -0
- package/dist/chunk-RCMYEWBE.js +11 -0
- package/dist/chunk-RCMYEWBE.js.map +1 -0
- package/dist/{chunk-PY53ABWC.js → chunk-RCPW6HVY.js} +23 -9
- package/dist/chunk-RCPW6HVY.js.map +1 -0
- package/dist/{chunk-DQNVAXG6.js → chunk-RXS455MR.js} +2 -2
- package/dist/chunk-RXS455MR.js.map +1 -0
- package/dist/chunk-TUVEJLOT.js +537 -0
- package/dist/chunk-TUVEJLOT.js.map +1 -0
- package/dist/{chunk-YZWXVZB3.js → chunk-U432HTUW.js} +6 -5
- package/dist/chunk-U432HTUW.js.map +1 -0
- package/dist/chunk-UDCQTOVU.cjs +523 -0
- package/dist/chunk-UDCQTOVU.cjs.map +1 -0
- package/dist/{chunk-5N7OLFHB.js → chunk-UDQS5INQ.js} +6 -5
- package/dist/chunk-UDQS5INQ.js.map +1 -0
- package/dist/{chunk-YILH4ETT.js → chunk-UDZWYHDV.js} +226 -31
- package/dist/chunk-UDZWYHDV.js.map +1 -0
- package/dist/{chunk-SBIJJ374.cjs → chunk-UMDWVMB5.cjs} +24 -6
- package/dist/chunk-UMDWVMB5.cjs.map +1 -0
- package/dist/{chunk-MA7GXUWC.cjs → chunk-VSFHJ7FB.cjs} +78 -2
- package/dist/chunk-VSFHJ7FB.cjs.map +1 -0
- package/dist/{chunk-J73TDY7J.cjs → chunk-WC4OD3EE.cjs} +25 -173
- package/dist/chunk-WC4OD3EE.cjs.map +1 -0
- package/dist/chunk-YM64MS4G.cjs +547 -0
- package/dist/chunk-YM64MS4G.cjs.map +1 -0
- package/dist/chunk-YORQQXGO.js +155 -0
- package/dist/chunk-YORQQXGO.js.map +1 -0
- package/dist/components/ActivityPreview.cjs +15 -10
- package/dist/components/ActivityPreview.d.cts +9 -3
- package/dist/components/ActivityPreview.d.ts +9 -3
- package/dist/components/ActivityPreview.js +14 -9
- package/dist/components/ActivitySequence.cjs +16 -11
- package/dist/components/ActivitySequence.d.cts +115 -4
- package/dist/components/ActivitySequence.d.ts +115 -4
- package/dist/components/ActivitySequence.js +15 -10
- package/dist/components/Dictation.cjs +7 -5
- package/dist/components/Dictation.d.cts +2 -1
- package/dist/components/Dictation.d.ts +2 -1
- package/dist/components/Dictation.js +6 -4
- package/dist/components/FillInTheBlanks.cjs +6 -5
- package/dist/components/FillInTheBlanks.d.cts +2 -1
- package/dist/components/FillInTheBlanks.d.ts +2 -1
- package/dist/components/FillInTheBlanks.js +5 -4
- package/dist/components/GapSelect.cjs +6 -5
- package/dist/components/GapSelect.d.cts +2 -1
- package/dist/components/GapSelect.d.ts +2 -1
- package/dist/components/GapSelect.js +5 -4
- package/dist/components/MultipleChoice.cjs +6 -5
- package/dist/components/MultipleChoice.d.cts +2 -1
- package/dist/components/MultipleChoice.d.ts +2 -1
- package/dist/components/MultipleChoice.js +5 -4
- package/dist/components/PronunciationFeedback.cjs +16 -0
- package/dist/components/PronunciationFeedback.cjs.map +1 -0
- package/dist/components/PronunciationFeedback.d.cts +67 -0
- package/dist/components/PronunciationFeedback.d.ts +67 -0
- package/dist/components/PronunciationFeedback.js +7 -0
- package/dist/components/PronunciationFeedback.js.map +1 -0
- package/dist/components/ReadAloud.cjs +21 -0
- package/dist/components/ReadAloud.cjs.map +1 -0
- package/dist/components/ReadAloud.d.cts +15 -0
- package/dist/components/ReadAloud.d.ts +15 -0
- package/dist/components/ReadAloud.js +12 -0
- package/dist/components/ReadAloud.js.map +1 -0
- package/dist/components/StimulusPanel.cjs +4 -4
- package/dist/components/StimulusPanel.d.cts +2 -1
- package/dist/components/StimulusPanel.d.ts +2 -1
- package/dist/components/StimulusPanel.js +3 -3
- package/dist/components/WrittenResponse.cjs +6 -5
- package/dist/components/WrittenResponse.d.cts +4 -3
- package/dist/components/WrittenResponse.d.ts +4 -3
- package/dist/components/WrittenResponse.js +5 -4
- package/dist/hooks/useSpeechRecorder.cjs +17 -0
- package/dist/hooks/useSpeechRecorder.cjs.map +1 -0
- package/dist/hooks/useSpeechRecorder.d.cts +127 -0
- package/dist/hooks/useSpeechRecorder.d.ts +127 -0
- package/dist/hooks/useSpeechRecorder.js +4 -0
- package/dist/hooks/useSpeechRecorder.js.map +1 -0
- package/dist/i18n/LkIntlProvider.cjs +7 -7
- package/dist/i18n/LkIntlProvider.d.cts +3 -2
- package/dist/i18n/LkIntlProvider.d.ts +3 -2
- package/dist/i18n/LkIntlProvider.js +1 -1
- package/dist/index.cjs +53 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +17 -12
- package/dist/index.js.map +1 -1
- package/dist/machine-CuwKqWXC.d.cts +26 -0
- package/dist/machine-CuwKqWXC.d.ts +26 -0
- package/dist/{strings-DPTyLpHt.d.cts → strings-l8Wv1Xax.d.ts} +230 -4
- package/dist/{strings-DPTyLpHt.d.ts → strings-wjAF2ONm.d.cts} +230 -4
- package/dist/theme/ThemeProvider.cjs +6 -6
- package/dist/theme/ThemeProvider.d.cts +4 -4
- package/dist/theme/ThemeProvider.d.ts +4 -4
- package/dist/theme/ThemeProvider.js +1 -1
- package/dist/theme/skin.css +612 -3
- package/package.json +33 -3
- package/dist/chunk-2GXSA322.cjs.map +0 -1
- package/dist/chunk-3Q4I4KT6.js.map +0 -1
- package/dist/chunk-5N7OLFHB.js.map +0 -1
- package/dist/chunk-AMGK7DDM.cjs.map +0 -1
- package/dist/chunk-DQERNAWI.js.map +0 -1
- package/dist/chunk-DQNVAXG6.js.map +0 -1
- package/dist/chunk-HT4FFZZQ.cjs.map +0 -1
- package/dist/chunk-J73TDY7J.cjs.map +0 -1
- package/dist/chunk-K3EA547J.cjs.map +0 -1
- package/dist/chunk-K7NMNOPI.cjs.map +0 -1
- package/dist/chunk-MA7GXUWC.cjs.map +0 -1
- package/dist/chunk-NVUFEMPZ.cjs.map +0 -1
- package/dist/chunk-PSHNTDKG.js.map +0 -1
- package/dist/chunk-PY53ABWC.js.map +0 -1
- package/dist/chunk-R7DI5UMU.js.map +0 -1
- package/dist/chunk-SBIJJ374.cjs.map +0 -1
- package/dist/chunk-UU2NV6SU.cjs.map +0 -1
- package/dist/chunk-W4MXRZAB.js.map +0 -1
- package/dist/chunk-XAGWBVK3.js.map +0 -1
- package/dist/chunk-YILH4ETT.js.map +0 -1
- package/dist/chunk-YZWXVZB3.js.map +0 -1
- package/dist/chunk-Z63JRG4M.cjs.map +0 -1
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
|
|
6
|
+
// src/hooks/useSpeechRecorder.ts
|
|
7
|
+
|
|
8
|
+
// src/hooks/speech-recorder/machine.ts
|
|
9
|
+
var IDLE_RECORDER = {
|
|
10
|
+
status: "idle",
|
|
11
|
+
error: null,
|
|
12
|
+
level: 0,
|
|
13
|
+
elapsedMs: 0,
|
|
14
|
+
take: null,
|
|
15
|
+
takesUsed: 0
|
|
16
|
+
};
|
|
17
|
+
function reduceRecorder(state, event) {
|
|
18
|
+
switch (event.kind) {
|
|
19
|
+
case "requested":
|
|
20
|
+
return {
|
|
21
|
+
...state,
|
|
22
|
+
status: "requesting-permission",
|
|
23
|
+
error: null,
|
|
24
|
+
level: 0,
|
|
25
|
+
elapsedMs: 0,
|
|
26
|
+
take: null
|
|
27
|
+
};
|
|
28
|
+
case "started":
|
|
29
|
+
return state.status === "requesting-permission" ? { ...state, status: "recording" } : state;
|
|
30
|
+
case "progress":
|
|
31
|
+
return state.status === "recording" ? { ...state, level: event.level, elapsedMs: event.elapsedMs } : state;
|
|
32
|
+
case "captured":
|
|
33
|
+
return {
|
|
34
|
+
...state,
|
|
35
|
+
status: "recorded",
|
|
36
|
+
error: null,
|
|
37
|
+
level: 0,
|
|
38
|
+
elapsedMs: event.take.durationMs,
|
|
39
|
+
take: event.take,
|
|
40
|
+
takesUsed: state.takesUsed + 1
|
|
41
|
+
};
|
|
42
|
+
case "failed":
|
|
43
|
+
return { ...state, status: "error", error: event.error, level: 0, take: null };
|
|
44
|
+
case "discarded":
|
|
45
|
+
return { ...state, status: "idle", error: null, level: 0, elapsedMs: 0, take: null };
|
|
46
|
+
case "reset":
|
|
47
|
+
return state === IDLE_RECORDER ? state : IDLE_RECORDER;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/hooks/speech-recorder/resample.ts
|
|
52
|
+
var TARGET_SAMPLE_RATE = 16e3;
|
|
53
|
+
var CUTOFF_FRACTION = 0.45;
|
|
54
|
+
var KERNEL_HALF_PERIODS = 8;
|
|
55
|
+
function downmix(channels) {
|
|
56
|
+
if (channels.length === 0) {
|
|
57
|
+
return new Float32Array(0);
|
|
58
|
+
}
|
|
59
|
+
let frames = channels[0].length;
|
|
60
|
+
for (const channel of channels) {
|
|
61
|
+
if (channel.length < frames) {
|
|
62
|
+
frames = channel.length;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const mono = new Float32Array(frames);
|
|
66
|
+
for (const channel of channels) {
|
|
67
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
68
|
+
mono[frame] += channel[frame];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
72
|
+
mono[frame] /= channels.length;
|
|
73
|
+
}
|
|
74
|
+
return mono;
|
|
75
|
+
}
|
|
76
|
+
function joinBlocks(blocks, frames) {
|
|
77
|
+
const joined = new Float32Array(frames);
|
|
78
|
+
let at = 0;
|
|
79
|
+
for (const block of blocks) {
|
|
80
|
+
joined.set(block, at);
|
|
81
|
+
at += block.length;
|
|
82
|
+
}
|
|
83
|
+
return joined;
|
|
84
|
+
}
|
|
85
|
+
function lowPass(samples, inputRate) {
|
|
86
|
+
const cutoff = TARGET_SAMPLE_RATE * CUTOFF_FRACTION / inputRate;
|
|
87
|
+
const half = Math.max(1, Math.round(KERNEL_HALF_PERIODS * inputRate / TARGET_SAMPLE_RATE));
|
|
88
|
+
const taps = new Float32Array(half * 2 + 1);
|
|
89
|
+
let gain = 0;
|
|
90
|
+
for (let tap = 0; tap < taps.length; tap += 1) {
|
|
91
|
+
const at = tap - half;
|
|
92
|
+
const angle = 2 * Math.PI * cutoff * at;
|
|
93
|
+
const sinc = at === 0 ? 2 * cutoff : Math.sin(angle) / (Math.PI * at);
|
|
94
|
+
const window = 0.5 - 0.5 * Math.cos(2 * Math.PI * tap / (taps.length - 1));
|
|
95
|
+
taps[tap] = sinc * window;
|
|
96
|
+
gain += taps[tap];
|
|
97
|
+
}
|
|
98
|
+
for (let tap = 0; tap < taps.length; tap += 1) {
|
|
99
|
+
taps[tap] /= gain;
|
|
100
|
+
}
|
|
101
|
+
const last = samples.length - 1;
|
|
102
|
+
const filtered = new Float32Array(samples.length);
|
|
103
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
104
|
+
let total = 0;
|
|
105
|
+
for (let tap = 0; tap < taps.length; tap += 1) {
|
|
106
|
+
const at = index + tap - half;
|
|
107
|
+
total += taps[tap] * samples[at < 0 ? 0 : at > last ? last : at];
|
|
108
|
+
}
|
|
109
|
+
filtered[index] = total;
|
|
110
|
+
}
|
|
111
|
+
return filtered;
|
|
112
|
+
}
|
|
113
|
+
function resampleToTarget(samples, inputRate) {
|
|
114
|
+
if (samples.length === 0) {
|
|
115
|
+
return new Float32Array(0);
|
|
116
|
+
}
|
|
117
|
+
if (inputRate === TARGET_SAMPLE_RATE) {
|
|
118
|
+
return samples.slice();
|
|
119
|
+
}
|
|
120
|
+
const source = inputRate > TARGET_SAMPLE_RATE ? lowPass(samples, inputRate) : samples;
|
|
121
|
+
const length = Math.round(samples.length * TARGET_SAMPLE_RATE / inputRate);
|
|
122
|
+
const resampled = new Float32Array(length);
|
|
123
|
+
const step = inputRate / TARGET_SAMPLE_RATE;
|
|
124
|
+
const last = source.length - 1;
|
|
125
|
+
for (let index = 0; index < length; index += 1) {
|
|
126
|
+
const at = index * step;
|
|
127
|
+
const left = Math.min(Math.floor(at), last);
|
|
128
|
+
const right = Math.min(left + 1, last);
|
|
129
|
+
const fraction = at - left;
|
|
130
|
+
resampled[index] = source[left] * (1 - fraction) + source[right] * fraction;
|
|
131
|
+
}
|
|
132
|
+
return resampled;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/hooks/speech-recorder/wav.ts
|
|
136
|
+
var WAV_MIME_TYPE = "audio/wav";
|
|
137
|
+
var HEADER_BYTES = 44;
|
|
138
|
+
function writeTag(view, offset, tag) {
|
|
139
|
+
for (let index = 0; index < tag.length; index += 1) {
|
|
140
|
+
view.setUint8(offset + index, tag.charCodeAt(index));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function encodeWav16(samples, sampleRate) {
|
|
144
|
+
const dataBytes = samples.length * 2;
|
|
145
|
+
const bytes = new Uint8Array(HEADER_BYTES + dataBytes);
|
|
146
|
+
const view = new DataView(bytes.buffer);
|
|
147
|
+
writeTag(view, 0, "RIFF");
|
|
148
|
+
view.setUint32(4, HEADER_BYTES - 8 + dataBytes, true);
|
|
149
|
+
writeTag(view, 8, "WAVE");
|
|
150
|
+
writeTag(view, 12, "fmt ");
|
|
151
|
+
view.setUint32(16, 16, true);
|
|
152
|
+
view.setUint16(20, 1, true);
|
|
153
|
+
view.setUint16(22, 1, true);
|
|
154
|
+
view.setUint32(24, sampleRate, true);
|
|
155
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
156
|
+
view.setUint16(32, 2, true);
|
|
157
|
+
view.setUint16(34, 16, true);
|
|
158
|
+
writeTag(view, 36, "data");
|
|
159
|
+
view.setUint32(40, dataBytes, true);
|
|
160
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
161
|
+
const sample = samples[index];
|
|
162
|
+
const clamped = sample < -1 ? -1 : sample > 1 ? 1 : sample;
|
|
163
|
+
view.setInt16(
|
|
164
|
+
HEADER_BYTES + index * 2,
|
|
165
|
+
Math.round(clamped * (clamped < 0 ? 32768 : 32767)),
|
|
166
|
+
true
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return bytes;
|
|
170
|
+
}
|
|
171
|
+
function peakOf(samples) {
|
|
172
|
+
let peak = 0;
|
|
173
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
174
|
+
const magnitude = Math.abs(samples[index]);
|
|
175
|
+
if (magnitude > peak) {
|
|
176
|
+
peak = magnitude;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return peak > 1 ? 1 : peak;
|
|
180
|
+
}
|
|
181
|
+
function rmsOf(samples) {
|
|
182
|
+
if (samples.length === 0) {
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
let total = 0;
|
|
186
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
187
|
+
total += samples[index] * samples[index];
|
|
188
|
+
}
|
|
189
|
+
const rms = Math.sqrt(total / samples.length);
|
|
190
|
+
return rms > 1 ? 1 : rms;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/hooks/speech-recorder/worklet.ts
|
|
194
|
+
var CAPTURE_PROCESSOR = "lk-speech-capture";
|
|
195
|
+
var CAPTURE_PROCESSOR_SOURCE = `class LkSpeechCapture extends AudioWorkletProcessor {
|
|
196
|
+
process(inputs) {
|
|
197
|
+
const input = inputs[0];
|
|
198
|
+
if (input === undefined || input.length === 0) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
const block = [];
|
|
202
|
+
for (let channel = 0; channel < input.length; channel += 1) {
|
|
203
|
+
block.push(input[channel].slice());
|
|
204
|
+
}
|
|
205
|
+
this.port.postMessage(block);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
registerProcessor(${JSON.stringify(CAPTURE_PROCESSOR)}, LkSpeechCapture);
|
|
210
|
+
`;
|
|
211
|
+
function createProcessorUrl() {
|
|
212
|
+
const urls = globalThis.URL;
|
|
213
|
+
if (urls === void 0) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const create = urls.createObjectURL;
|
|
217
|
+
const revoke = urls.revokeObjectURL;
|
|
218
|
+
if (create === void 0 || revoke === void 0) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const url = create.call(urls, new Blob([CAPTURE_PROCESSOR_SOURCE], { type: "text/javascript" }));
|
|
222
|
+
return {
|
|
223
|
+
url,
|
|
224
|
+
release: () => {
|
|
225
|
+
revoke.call(urls, url);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/hooks/useSpeechRecorder.ts
|
|
231
|
+
var PROCESSOR_BUFFER_FRAMES = 4096;
|
|
232
|
+
var LEVEL_UPDATES_PER_SECOND = 10;
|
|
233
|
+
function stopTracks(stream) {
|
|
234
|
+
for (const track of stream.getTracks()) {
|
|
235
|
+
track.stop();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function closeQuietly(context) {
|
|
239
|
+
void context.close().catch(() => {
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function microphoneError(error) {
|
|
243
|
+
switch (error?.name) {
|
|
244
|
+
case "NotAllowedError":
|
|
245
|
+
case "SecurityError":
|
|
246
|
+
return "permission-denied";
|
|
247
|
+
case "NotFoundError":
|
|
248
|
+
return "no-device";
|
|
249
|
+
case "NotSupportedError":
|
|
250
|
+
return "unsupported";
|
|
251
|
+
default:
|
|
252
|
+
return "failed";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function createWorkletNode(context, workletUrl, onBlock) {
|
|
256
|
+
const AudioWorkletNodeConstructor = globalThis.AudioWorkletNode;
|
|
257
|
+
const worklet = context.audioWorklet;
|
|
258
|
+
if (AudioWorkletNodeConstructor === void 0 || worklet === void 0) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const minted = workletUrl === void 0 ? createProcessorUrl() : null;
|
|
262
|
+
const url = workletUrl ?? minted?.url;
|
|
263
|
+
if (url === void 0) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
await worklet.addModule(url);
|
|
268
|
+
const node = new AudioWorkletNodeConstructor(context, CAPTURE_PROCESSOR);
|
|
269
|
+
node.port.onmessage = (event) => {
|
|
270
|
+
onBlock(event.data);
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
node,
|
|
274
|
+
detach: () => {
|
|
275
|
+
node.port.onmessage = null;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
} finally {
|
|
281
|
+
minted?.release();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function createProcessorNode(context, onBlock) {
|
|
285
|
+
const create = context.createScriptProcessor;
|
|
286
|
+
if (create === void 0) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const node = create.call(context, PROCESSOR_BUFFER_FRAMES, 1, 1);
|
|
290
|
+
node.onaudioprocess = (event) => {
|
|
291
|
+
const buffer = event.inputBuffer;
|
|
292
|
+
const channels = [];
|
|
293
|
+
for (let channel = 0; channel < buffer.numberOfChannels; channel += 1) {
|
|
294
|
+
channels.push(buffer.getChannelData(channel));
|
|
295
|
+
}
|
|
296
|
+
onBlock(channels);
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
node,
|
|
300
|
+
detach: () => {
|
|
301
|
+
node.onaudioprocess = null;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function useSpeechRecorder(options) {
|
|
306
|
+
const optionsRef = react.useRef(options);
|
|
307
|
+
optionsRef.current = options;
|
|
308
|
+
const [state, setState] = react.useState(IDLE_RECORDER);
|
|
309
|
+
const stateRef = react.useRef(IDLE_RECORDER);
|
|
310
|
+
const sessionRef = react.useRef(null);
|
|
311
|
+
const startingRef = react.useRef(false);
|
|
312
|
+
const attemptRef = react.useRef(0);
|
|
313
|
+
const dispatch = react.useCallback((event) => {
|
|
314
|
+
const next = reduceRecorder(stateRef.current, event);
|
|
315
|
+
if (next === stateRef.current) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
stateRef.current = next;
|
|
319
|
+
setState(next);
|
|
320
|
+
}, []);
|
|
321
|
+
const endSession = react.useCallback(() => {
|
|
322
|
+
const session = sessionRef.current;
|
|
323
|
+
sessionRef.current = null;
|
|
324
|
+
session?.release();
|
|
325
|
+
}, []);
|
|
326
|
+
const finish = react.useCallback(() => {
|
|
327
|
+
const session = sessionRef.current;
|
|
328
|
+
if (session === null) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
sessionRef.current = null;
|
|
332
|
+
session.release();
|
|
333
|
+
const { minDurationMs } = optionsRef.current;
|
|
334
|
+
const samples = resampleToTarget(joinBlocks(session.blocks, session.frames), session.rate);
|
|
335
|
+
const durationMs = samples.length / TARGET_SAMPLE_RATE * 1e3;
|
|
336
|
+
if (minDurationMs !== void 0 && durationMs < minDurationMs) {
|
|
337
|
+
dispatch({ kind: "failed", error: "too-short" });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
dispatch({
|
|
341
|
+
kind: "captured",
|
|
342
|
+
take: {
|
|
343
|
+
blob: new Blob([encodeWav16(samples, TARGET_SAMPLE_RATE)], { type: WAV_MIME_TYPE }),
|
|
344
|
+
mimeType: WAV_MIME_TYPE,
|
|
345
|
+
durationMs,
|
|
346
|
+
peakLevel: peakOf(samples)
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}, [dispatch]);
|
|
350
|
+
const handleBlock = react.useCallback(
|
|
351
|
+
(channels) => {
|
|
352
|
+
const session = sessionRef.current;
|
|
353
|
+
if (session === null) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
let block = downmix(channels);
|
|
357
|
+
const room = session.maxFrames - session.frames;
|
|
358
|
+
if (block.length > room) {
|
|
359
|
+
block = block.subarray(0, room);
|
|
360
|
+
}
|
|
361
|
+
session.blocks.push(block);
|
|
362
|
+
session.frames += block.length;
|
|
363
|
+
const level = rmsOf(block);
|
|
364
|
+
if (level > session.peak) {
|
|
365
|
+
session.peak = level;
|
|
366
|
+
}
|
|
367
|
+
if (session.frames >= session.maxFrames) {
|
|
368
|
+
finish();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (session.frames - session.reportedAt < session.rate / LEVEL_UPDATES_PER_SECOND) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
session.reportedAt = session.frames;
|
|
375
|
+
const peak = session.peak;
|
|
376
|
+
session.peak = 0;
|
|
377
|
+
dispatch({
|
|
378
|
+
kind: "progress",
|
|
379
|
+
level: peak,
|
|
380
|
+
elapsedMs: session.frames / session.rate * 1e3
|
|
381
|
+
});
|
|
382
|
+
},
|
|
383
|
+
[dispatch, finish]
|
|
384
|
+
);
|
|
385
|
+
const start = react.useCallback(async () => {
|
|
386
|
+
const { maxDurationMs, maxTakes: maxTakes2, workletUrl } = optionsRef.current;
|
|
387
|
+
if (startingRef.current || sessionRef.current !== null) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (maxTakes2 !== void 0 && stateRef.current.takesUsed >= maxTakes2) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const attempt = attemptRef.current;
|
|
394
|
+
startingRef.current = true;
|
|
395
|
+
dispatch({ kind: "requested" });
|
|
396
|
+
let release = () => {
|
|
397
|
+
};
|
|
398
|
+
try {
|
|
399
|
+
const view = globalThis;
|
|
400
|
+
const devices = view.navigator?.mediaDevices;
|
|
401
|
+
const getUserMedia = devices?.getUserMedia;
|
|
402
|
+
const AudioContextConstructor = view.AudioContext;
|
|
403
|
+
if (devices === void 0 || getUserMedia === void 0 || AudioContextConstructor === void 0) {
|
|
404
|
+
dispatch({ kind: "failed", error: "unsupported" });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
let stream;
|
|
408
|
+
try {
|
|
409
|
+
stream = await getUserMedia.call(devices, { audio: true });
|
|
410
|
+
} catch (error) {
|
|
411
|
+
dispatch({ kind: "failed", error: microphoneError(error) });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
release = () => {
|
|
415
|
+
stopTracks(stream);
|
|
416
|
+
};
|
|
417
|
+
if (attempt !== attemptRef.current) {
|
|
418
|
+
release();
|
|
419
|
+
dispatch({ kind: "discarded" });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
let context;
|
|
423
|
+
try {
|
|
424
|
+
context = new AudioContextConstructor();
|
|
425
|
+
} catch {
|
|
426
|
+
release();
|
|
427
|
+
dispatch({ kind: "failed", error: "unsupported" });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const open = context;
|
|
431
|
+
release = () => {
|
|
432
|
+
stopTracks(stream);
|
|
433
|
+
closeQuietly(open);
|
|
434
|
+
};
|
|
435
|
+
const attached = await createWorkletNode(open, workletUrl, handleBlock) ?? createProcessorNode(open, handleBlock);
|
|
436
|
+
if (attached === null) {
|
|
437
|
+
release();
|
|
438
|
+
dispatch({ kind: "failed", error: "unsupported" });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const source = open.createMediaStreamSource(stream);
|
|
442
|
+
const sink = open.createGain();
|
|
443
|
+
sink.gain.value = 0;
|
|
444
|
+
source.connect(attached.node);
|
|
445
|
+
attached.node.connect(sink);
|
|
446
|
+
sink.connect(open.destination);
|
|
447
|
+
release = () => {
|
|
448
|
+
attached.detach();
|
|
449
|
+
attached.node.disconnect();
|
|
450
|
+
source.disconnect();
|
|
451
|
+
sink.disconnect();
|
|
452
|
+
stopTracks(stream);
|
|
453
|
+
closeQuietly(open);
|
|
454
|
+
};
|
|
455
|
+
if (attempt !== attemptRef.current) {
|
|
456
|
+
release();
|
|
457
|
+
dispatch({ kind: "discarded" });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
sessionRef.current = {
|
|
461
|
+
blocks: [],
|
|
462
|
+
frames: 0,
|
|
463
|
+
rate: open.sampleRate,
|
|
464
|
+
maxFrames: Math.max(1, Math.floor(maxDurationMs / 1e3 * open.sampleRate)),
|
|
465
|
+
peak: 0,
|
|
466
|
+
reportedAt: 0,
|
|
467
|
+
release
|
|
468
|
+
};
|
|
469
|
+
dispatch({ kind: "started" });
|
|
470
|
+
} catch {
|
|
471
|
+
release();
|
|
472
|
+
sessionRef.current = null;
|
|
473
|
+
dispatch({ kind: "failed", error: "failed" });
|
|
474
|
+
} finally {
|
|
475
|
+
startingRef.current = false;
|
|
476
|
+
}
|
|
477
|
+
}, [dispatch, handleBlock]);
|
|
478
|
+
const stop = react.useCallback(() => {
|
|
479
|
+
if (sessionRef.current === null) {
|
|
480
|
+
attemptRef.current += 1;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
finish();
|
|
484
|
+
}, [finish]);
|
|
485
|
+
const discard = react.useCallback(() => {
|
|
486
|
+
attemptRef.current += 1;
|
|
487
|
+
endSession();
|
|
488
|
+
dispatch({ kind: "discarded" });
|
|
489
|
+
}, [dispatch, endSession]);
|
|
490
|
+
const reset = react.useCallback(() => {
|
|
491
|
+
attemptRef.current += 1;
|
|
492
|
+
endSession();
|
|
493
|
+
dispatch({ kind: "reset" });
|
|
494
|
+
}, [dispatch, endSession]);
|
|
495
|
+
react.useEffect(() => {
|
|
496
|
+
return () => {
|
|
497
|
+
attemptRef.current += 1;
|
|
498
|
+
endSession();
|
|
499
|
+
};
|
|
500
|
+
}, [endSession]);
|
|
501
|
+
const { maxTakes } = options;
|
|
502
|
+
return react.useMemo(
|
|
503
|
+
() => ({
|
|
504
|
+
status: state.status,
|
|
505
|
+
error: state.error,
|
|
506
|
+
level: state.level,
|
|
507
|
+
elapsedMs: state.elapsedMs,
|
|
508
|
+
take: state.take,
|
|
509
|
+
takesUsed: state.takesUsed,
|
|
510
|
+
canRecord: maxTakes === void 0 || state.takesUsed < maxTakes,
|
|
511
|
+
start,
|
|
512
|
+
stop,
|
|
513
|
+
discard,
|
|
514
|
+
reset
|
|
515
|
+
}),
|
|
516
|
+
[state, maxTakes, start, stop, discard, reset]
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
exports.CAPTURE_PROCESSOR_SOURCE = CAPTURE_PROCESSOR_SOURCE;
|
|
521
|
+
exports.useSpeechRecorder = useSpeechRecorder;
|
|
522
|
+
//# sourceMappingURL=chunk-UDCQTOVU.cjs.map
|
|
523
|
+
//# sourceMappingURL=chunk-UDCQTOVU.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/speech-recorder/machine.ts","../src/hooks/speech-recorder/resample.ts","../src/hooks/speech-recorder/wav.ts","../src/hooks/speech-recorder/worklet.ts","../src/hooks/useSpeechRecorder.ts"],"names":["useRef","useState","useCallback","maxTakes","useEffect","useMemo"],"mappings":"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","file":"chunk-UDCQTOVU.cjs","sourcesContent":["'use client';\n\n/**\n * The recorder's state, as one pure reduction.\n *\n * Kept out of the hook because the hook's own work — opening a microphone,\n * building a graph, tearing both down — is asynchronous, and a state machine\n * spread across five callbacks is exactly where an auto-stop that races a\n * learner's `stop()` hides. Every transition is here, synchronous and\n * testable without a microphone; the hook only says which event happened.\n *\n * No transition reads a browser global, which is what lets `status`, `error`\n * and `canRecord` be rendered on a server.\n */\n\n/** A recording the learner has made and not yet discarded. */\nexport interface RecordedTake {\n /** Mono 16-bit PCM WAV at 16 kHz — the format `inspectWav` measures. */\n blob: Blob;\n mimeType: string;\n /** The encoded recording's own length, not a wall-clock timing of the attempt. */\n durationMs: number;\n /** The loudest sample of the take, 0..1. */\n peakLevel: number;\n}\n\nexport type SpeechRecorderStatus =\n | 'idle'\n | 'requesting-permission'\n | 'recording'\n | 'recorded'\n | 'error';\n\nexport type SpeechRecorderError =\n | 'permission-denied'\n | 'no-device'\n | 'unsupported'\n | 'too-short'\n | 'failed';\n\nexport interface RecorderState {\n status: SpeechRecorderStatus;\n error: SpeechRecorderError | null;\n level: number;\n elapsedMs: number;\n take: RecordedTake | null;\n takesUsed: number;\n}\n\nexport type RecorderEvent =\n | { kind: 'requested' }\n | { kind: 'started' }\n | { kind: 'progress'; level: number; elapsedMs: number }\n | { kind: 'captured'; take: RecordedTake }\n | { kind: 'failed'; error: SpeechRecorderError }\n | { kind: 'discarded' }\n | { kind: 'reset' };\n\n/** The state a recorder mounts in, on a server as on a client. */\nexport const IDLE_RECORDER: RecorderState = {\n status: 'idle',\n error: null,\n level: 0,\n elapsedMs: 0,\n take: null,\n takesUsed: 0,\n};\n\n/**\n * Applies one event. Returns the state it was given, unchanged and identical,\n * when the event moves nothing — so a block arriving after teardown costs no\n * render.\n */\nexport function reduceRecorder(state: RecorderState, event: RecorderEvent): RecorderState {\n switch (event.kind) {\n case 'requested':\n // A new attempt clears the previous take. The learner has replaced it,\n // and a take left on screen through the next recording would offer a\n // submit button for audio that is no longer the one just made.\n return {\n ...state,\n status: 'requesting-permission',\n error: null,\n level: 0,\n elapsedMs: 0,\n take: null,\n };\n case 'started':\n // Only from a request this machine made. A microphone that opens after\n // the learner discarded the attempt must not put the meter back up.\n return state.status === 'requesting-permission' ? { ...state, status: 'recording' } : state;\n case 'progress':\n return state.status === 'recording'\n ? { ...state, level: event.level, elapsedMs: event.elapsedMs }\n : state;\n case 'captured':\n // The take count rises here and nowhere else, and `discard` never gives\n // one back: a budget a re-record refunded would bound nothing.\n return {\n ...state,\n status: 'recorded',\n error: null,\n level: 0,\n elapsedMs: event.take.durationMs,\n take: event.take,\n takesUsed: state.takesUsed + 1,\n };\n case 'failed':\n // No take was produced, so none was spent — a cough must not cost a\n // learner their only attempt. The elapsed time survives, because a\n // \"too short\" message reads better beside the length that was too short.\n return { ...state, status: 'error', error: event.error, level: 0, take: null };\n case 'discarded':\n return { ...state, status: 'idle', error: null, level: 0, elapsedMs: 0, take: null };\n case 'reset':\n // The budget goes back, which is the whole difference from `discarded`.\n // A discard is the same learner replacing their own take and must pay\n // for it; a reset is a different reading altogether — a new activity in\n // the same mounted component — and the takes it may have are its own.\n return state === IDLE_RECORDER ? state : IDLE_RECORDER;\n }\n}\n","'use client';\n\n/**\n * Turning the blocks a capture node delivers into one mono 16 kHz signal.\n *\n * 16 kHz mono is the rate every mainstream assessor accepts and the rate\n * `inspectWav` measures exactly, while a microphone is opened at whatever rate\n * the device runs at — 44.1 or 48 kHz on most, already 16 kHz on some. The\n * three steps are separated and pure so the arithmetic that decides what a\n * server will measure can be tested without a browser.\n */\n\n/** The capture rate every mainstream assessor accepts. */\nexport const TARGET_SAMPLE_RATE = 16_000;\n\n/**\n * Where the anti-alias filter stops passing, as a fraction of the target rate.\n *\n * Below 0.5 on purpose: a filter cutting at exactly the target's Nyquist is\n * only 6 dB down there and its transition band folds straight back into the\n * signal. Cutting at 7.2 kHz leaves the transition band room to finish before\n * 8 kHz, and speech carries next to nothing in the 800 Hz that costs.\n */\nconst CUTOFF_FRACTION = 0.45;\n\n/**\n * Half the kernel width, counted in target-rate sample periods. Sixteen\n * periods of taps is enough Hann-windowed sinc to put a tone an octave above\n * the cutoff far below anything a meter or an assessor would notice, and short\n * enough that filtering a whole take costs a fraction of a second.\n */\nconst KERNEL_HALF_PERIODS = 8;\n\n/**\n * Averages the channels of one capture block into mono.\n *\n * Always allocates, never returns what it was given: a `ScriptProcessorNode`\n * hands out the same buffer on every callback and overwrites it in place, so a\n * mono capture that passed the input straight through would find every block\n * it had kept replaced by the last one.\n *\n * Channels of unequal length are read to the shortest. Nothing should produce\n * them, and the alternative to a bound is reading past the end of one and\n * poisoning the whole take with NaN.\n */\nexport function downmix(channels: readonly Float32Array[]): Float32Array {\n if (channels.length === 0) {\n return new Float32Array(0);\n }\n let frames = channels[0].length;\n for (const channel of channels) {\n if (channel.length < frames) {\n frames = channel.length;\n }\n }\n const mono = new Float32Array(frames);\n for (const channel of channels) {\n for (let frame = 0; frame < frames; frame += 1) {\n mono[frame] += channel[frame];\n }\n }\n for (let frame = 0; frame < frames; frame += 1) {\n mono[frame] /= channels.length;\n }\n return mono;\n}\n\n/** Concatenates captured blocks into the single signal a take is encoded from. */\nexport function joinBlocks(blocks: readonly Float32Array[], frames: number): Float32Array {\n const joined = new Float32Array(frames);\n let at = 0;\n for (const block of blocks) {\n joined.set(block, at);\n at += block.length;\n }\n return joined;\n}\n\n/**\n * A windowed-sinc low-pass, applied at the input rate before any samples are\n * dropped.\n *\n * Without it, resampling is aliasing: every component above the target's\n * Nyquist folds back down into the speech band as a tone that was never\n * spoken, and an assessor scores what it hears. The window is Hann and the\n * kernel is normalised to unit gain, so a signal already inside the band comes\n * through at its own amplitude.\n *\n * Edges are extended by repeating the first and last sample rather than by\n * zero-padding, which would put a step — and so a click — at each end of every\n * take.\n */\nfunction lowPass(samples: Float32Array, inputRate: number): Float32Array {\n const cutoff = (TARGET_SAMPLE_RATE * CUTOFF_FRACTION) / inputRate;\n const half = Math.max(1, Math.round((KERNEL_HALF_PERIODS * inputRate) / TARGET_SAMPLE_RATE));\n const taps = new Float32Array(half * 2 + 1);\n let gain = 0;\n for (let tap = 0; tap < taps.length; tap += 1) {\n const at = tap - half;\n const angle = 2 * Math.PI * cutoff * at;\n const sinc = at === 0 ? 2 * cutoff : Math.sin(angle) / (Math.PI * at);\n const window = 0.5 - 0.5 * Math.cos((2 * Math.PI * tap) / (taps.length - 1));\n taps[tap] = sinc * window;\n gain += taps[tap];\n }\n for (let tap = 0; tap < taps.length; tap += 1) {\n taps[tap] /= gain;\n }\n\n const last = samples.length - 1;\n const filtered = new Float32Array(samples.length);\n for (let index = 0; index < samples.length; index += 1) {\n let total = 0;\n for (let tap = 0; tap < taps.length; tap += 1) {\n const at = index + tap - half;\n total += taps[tap] * samples[at < 0 ? 0 : at > last ? last : at];\n }\n filtered[index] = total;\n }\n return filtered;\n}\n\n/**\n * Resamples mono samples from `inputRate` to {@link TARGET_SAMPLE_RATE}.\n *\n * Every path returns an array the caller owns, the equal-rate one included, so\n * no caller has to know whether it was handed its own buffer back.\n *\n * The filter runs only when the rate is coming down. Interpolating upwards\n * invents no frequencies above the input's own Nyquist, so there is nothing\n * for an anti-alias filter to remove and applying one would only dull a\n * recording a 16 kHz device already captured cleanly.\n */\nexport function resampleToTarget(samples: Float32Array, inputRate: number): Float32Array {\n if (samples.length === 0) {\n return new Float32Array(0);\n }\n if (inputRate === TARGET_SAMPLE_RATE) {\n return samples.slice();\n }\n const source = inputRate > TARGET_SAMPLE_RATE ? lowPass(samples, inputRate) : samples;\n const length = Math.round((samples.length * TARGET_SAMPLE_RATE) / inputRate);\n const resampled = new Float32Array(length);\n const step = inputRate / TARGET_SAMPLE_RATE;\n const last = source.length - 1;\n for (let index = 0; index < length; index += 1) {\n const at = index * step;\n const left = Math.min(Math.floor(at), last);\n const right = Math.min(left + 1, last);\n const fraction = at - left;\n resampled[index] = source[left] * (1 - fraction) + source[right] * fraction;\n }\n return resampled;\n}\n","'use client';\n\n/**\n * 16-bit PCM WAV, and the two level measurements taken over the same samples.\n *\n * This is the one format the SDK's `inspectWav` reads, so these bytes are the\n * bytes a server measures a take with — anything else would need a decoder on\n * the application's side before a grade could exist. Nothing here touches Web\n * Audio, which is why the encoder can be unit tested against `inspectWav`\n * itself rather than against a browser.\n */\n\n/** The media type of what {@link encodeWav16} returns. */\nexport const WAV_MIME_TYPE = 'audio/wav';\n\n/** RIFF + a 16-byte `fmt ` + `data`, every field fixed-size. */\nconst HEADER_BYTES = 44;\n\n/** Writes four ASCII characters, which is all a RIFF tag ever is. */\nfunction writeTag(view: DataView, offset: number, tag: string): void {\n for (let index = 0; index < tag.length; index += 1) {\n view.setUint8(offset + index, tag.charCodeAt(index));\n }\n}\n\n/**\n * Encodes mono samples as a 16-bit PCM WAV.\n *\n * The two scale factors are not a typo. Signed 16-bit runs from -32768 to\n * 32767, so a single factor cannot reach full scale in both directions: 0x7fff\n * throughout leaves a full-scale negative peak reading -0.99997, and 0x8000\n * throughout overflows into the positive at +1. Each sign gets the factor that\n * lands exactly on its own limit.\n *\n * The buffer is named in the return type because a `Blob` refuses a view that\n * might be backed by a `SharedArrayBuffer`, and these bytes go straight into\n * one.\n */\nexport function encodeWav16(samples: Float32Array, sampleRate: number): Uint8Array<ArrayBuffer> {\n const dataBytes = samples.length * 2;\n const bytes = new Uint8Array(HEADER_BYTES + dataBytes);\n const view = new DataView(bytes.buffer);\n\n writeTag(view, 0, 'RIFF');\n view.setUint32(4, HEADER_BYTES - 8 + dataBytes, true);\n writeTag(view, 8, 'WAVE');\n writeTag(view, 12, 'fmt ');\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true); // PCM, uncompressed\n view.setUint16(22, 1, true); // mono\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * 2, true); // byte rate: one channel of two bytes\n view.setUint16(32, 2, true); // block align\n view.setUint16(34, 16, true); // bits per sample\n writeTag(view, 36, 'data');\n view.setUint32(40, dataBytes, true);\n\n for (let index = 0; index < samples.length; index += 1) {\n const sample = samples[index];\n const clamped = sample < -1 ? -1 : sample > 1 ? 1 : sample;\n view.setInt16(\n HEADER_BYTES + index * 2,\n Math.round(clamped * (clamped < 0 ? 0x8000 : 0x7fff)),\n true,\n );\n }\n return bytes;\n}\n\n/** The loudest sample, 0..1 — what a take reports as its `peakLevel`. */\nexport function peakOf(samples: Float32Array): number {\n let peak = 0;\n for (let index = 0; index < samples.length; index += 1) {\n const magnitude = Math.abs(samples[index]);\n if (magnitude > peak) {\n peak = magnitude;\n }\n }\n return peak > 1 ? 1 : peak;\n}\n\n/**\n * Root mean square, 0..1 — what the recorder reports as its `level`.\n *\n * RMS rather than the peak, because a meter driven by peaks sits near full\n * scale on any speech at all and tells a learner nothing about whether they\n * are being heard. It is also the measure `inspectWav` decides voiced time\n * with, so what the meter shows and what the server later counts as speech are\n * the same quantity.\n */\nexport function rmsOf(samples: Float32Array): number {\n if (samples.length === 0) {\n return 0;\n }\n let total = 0;\n for (let index = 0; index < samples.length; index += 1) {\n total += samples[index] * samples[index];\n }\n const rms = Math.sqrt(total / samples.length);\n return rms > 1 ? 1 : rms;\n}\n","'use client';\n\n/**\n * The audio worklet processor, and the module URL it is loaded through.\n *\n * The processor must run inside `AudioWorkletGlobalScope`, which can only be\n * reached by URL. Source text turned into a Blob URL is the only form of that\n * which needs nothing of the consuming application: `import.meta` is not\n * available in every target this package is bundled for — the CommonJS build\n * among them — and a separate asset file would have to be copied into the\n * public directory of every application that installs the SDK.\n *\n * An application whose Content-Security-Policy refuses that URL self-hosts the\n * same source and passes its URL as `workletUrl` instead. The source is public\n * for exactly that reason — see {@link CAPTURE_PROCESSOR_SOURCE}.\n */\n\n/** The name the processor registers itself under, and is constructed by. */\nexport const CAPTURE_PROCESSOR = 'lk-speech-capture';\n\n/**\n * The audio worklet module `useSpeechRecorder` captures through, as source\n * text — exported so an application with a strict Content-Security-Policy can\n * serve it itself.\n *\n * By default the hook loads this text from a `blob:` URL. A worklet module is\n * loaded as a script, so it is `script-src` that must allow `blob:` —\n * `worker-src` does not govern it. Where that is refused, write this string to\n * a file your policy's `script-src` does allow, at build time and from the\n * installed package so it moves with the version you run, and pass that file's\n * URL as `workletUrl`:\n *\n * ```ts\n * import { writeFileSync } from 'node:fs';\n * import { CAPTURE_PROCESSOR_SOURCE } from '@intellectif/lk-react/hooks/useSpeechRecorder';\n *\n * writeFileSync('public/lk-speech-capture.js', CAPTURE_PROCESSOR_SOURCE);\n * ```\n *\n * **The contract** a module at `workletUrl` must meet, which this source does:\n * register an `AudioWorkletProcessor` under the name `lk-speech-capture`, and\n * from its `process(inputs)` post the first input's channels to its `port` as\n * an array with one `Float32Array` per channel, **copied** — the render\n * quantum's buffers belong to the audio thread and are overwritten on the next\n * quantum, so a block posted without copying arrives as whatever the\n * microphone heard some milliseconds later. Return `true`, which keeps the\n * processor alive while its source is silent. Anything else changes what is\n * recorded, so serve this text unchanged rather than a copy of your own.\n */\nexport const CAPTURE_PROCESSOR_SOURCE = `class LkSpeechCapture extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input === undefined || input.length === 0) {\n return true;\n }\n const block = [];\n for (let channel = 0; channel < input.length; channel += 1) {\n block.push(input[channel].slice());\n }\n this.port.postMessage(block);\n return true;\n }\n}\nregisterProcessor(${JSON.stringify(CAPTURE_PROCESSOR)}, LkSpeechCapture);\n`;\n\n/** A module URL and the release that frees it. */\nexport interface ProcessorModule {\n url: string;\n release(): void;\n}\n\n/** The object-URL pair, as much of it as an environment happens to have. */\ninterface ObjectUrls {\n createObjectURL?(blob: Blob): string;\n revokeObjectURL?(url: string): void;\n}\n\n/**\n * Wraps {@link CAPTURE_PROCESSOR_SOURCE} in an object URL.\n *\n * Returns `null` where object URLs are unavailable — a server render, a test\n * environment, a hardened embedder — rather than throwing, because the caller\n * has a working fallback for exactly that case and a missing URL factory is\n * not a failed recording.\n *\n * Called from `start()` and never at module scope: `URL.createObjectURL` is a\n * browser global, and reading one while a module is evaluated is what breaks a\n * server render.\n */\nexport function createProcessorUrl(): ProcessorModule | null {\n const urls = (globalThis as { URL?: ObjectUrls }).URL;\n if (urls === undefined) {\n return null;\n }\n const create = urls.createObjectURL;\n const revoke = urls.revokeObjectURL;\n if (create === undefined || revoke === undefined) {\n return null;\n }\n const url = create.call(urls, new Blob([CAPTURE_PROCESSOR_SOURCE], { type: 'text/javascript' }));\n return {\n url,\n release: () => {\n revoke.call(urls, url);\n },\n };\n}\n","'use client';\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n IDLE_RECORDER,\n type RecordedTake,\n type RecorderEvent,\n type RecorderState,\n reduceRecorder,\n type SpeechRecorderError,\n type SpeechRecorderStatus,\n} from './speech-recorder/machine.js';\nimport {\n downmix,\n joinBlocks,\n resampleToTarget,\n TARGET_SAMPLE_RATE,\n} from './speech-recorder/resample.js';\nimport { encodeWav16, peakOf, rmsOf, WAV_MIME_TYPE } from './speech-recorder/wav.js';\nimport { CAPTURE_PROCESSOR, createProcessorUrl } from './speech-recorder/worklet.js';\n\n// Public because `workletUrl` is: an option that names a module is only usable\n// if the module's text can be reached without reading a minified chunk.\nexport { CAPTURE_PROCESSOR_SOURCE } from './speech-recorder/worklet.js';\nexport type { RecordedTake, SpeechRecorderError, SpeechRecorderStatus };\n\nexport interface SpeechRecorderOptions {\n /** The longest a take may be. Reached, the recording stops itself. */\n maxDurationMs: number;\n /** Shorter than this and the take is refused as `too-short`. */\n minDurationMs?: number;\n /** How many takes the learner may make. Unbounded when absent. */\n maxTakes?: number;\n /**\n * The URL of a self-hosted copy of {@link CAPTURE_PROCESSOR_SOURCE}, for an\n * application whose Content-Security-Policy does not allow `blob:` in\n * `script-src`. Serve that string unchanged; the contract a module here must\n * meet is written on it.\n *\n * **A URL that does not work is not reported.** If the module fails to load,\n * or loads without registering `lk-speech-capture`, the hook records through\n * the deprecated `ScriptProcessorNode` on the main thread instead — the\n * fallback it keeps for engines without audio worklets — and a recorder with\n * a broken `workletUrl` looks like a working one. Only an engine with neither\n * path reports `error: 'unsupported'`. Check that the module is actually\n * fetched.\n */\n workletUrl?: string;\n}\n\nexport interface SpeechRecorder {\n status: SpeechRecorderStatus;\n error: SpeechRecorderError | null;\n /** The loudest block since the last update, 0..1. */\n level: number;\n /** How much audio has been captured, from the sample count. */\n elapsedMs: number;\n take: RecordedTake | null;\n takesUsed: number;\n /** Whether a take remains. Not whether a `start()` would be accepted now. */\n canRecord: boolean;\n /** Opens the microphone and begins capturing. **Never rejects.** */\n start(): Promise<void>;\n /** Ends the capture and encodes the take. */\n stop(): void;\n /** Throws the take away and releases the microphone. */\n discard(): void;\n /**\n * Starts over: releases the microphone, throws the take away **and gives the\n * budget back**.\n *\n * That last part is the whole difference from {@link SpeechRecorder.discard},\n * which deliberately refunds nothing — a budget a re-record gave back would\n * bound nothing. A reset is for a recorder that has been handed a different\n * reading while it stayed mounted: the takes belong to the activity, so the\n * next activity's are its own. Calling it for the same reading hands a\n * learner takes they have already spent.\n */\n reset(): void;\n}\n\n/**\n * The capture graph, typed by the surface this hook uses rather than by the\n * DOM lib.\n *\n * Two reasons, both load-bearing. The lib declares `audioWorklet` and\n * `createScriptProcessor` non-optional, and the engines this hook falls back\n * for are exactly the ones missing one or the other — comparing a type that\n * cannot be `undefined` against `undefined` is a compile error, so the lib's\n * types would force the fallback to be written blind. And a test harness can\n * implement these six members; it cannot implement `BaseAudioContext`.\n */\ninterface CaptureNode {\n connect(destination: CaptureNode): unknown;\n disconnect(): void;\n}\n\ninterface CaptureGain extends CaptureNode {\n readonly gain: { value: number };\n}\n\ninterface WorkletCaptureNode extends CaptureNode {\n readonly port: { onmessage: ((event: { data: unknown }) => void) | null };\n}\n\ninterface ScriptProcessorCaptureNode extends CaptureNode {\n onaudioprocess:\n | ((event: {\n inputBuffer: { numberOfChannels: number; getChannelData(channel: number): Float32Array };\n }) => void)\n | null;\n}\n\ninterface CaptureStream {\n getTracks(): readonly { stop(): void }[];\n}\n\ninterface CaptureContext {\n readonly sampleRate: number;\n readonly destination: CaptureNode;\n readonly audioWorklet?: { addModule(url: string): Promise<void> };\n createMediaStreamSource(stream: CaptureStream): CaptureNode;\n createGain(): CaptureGain;\n createScriptProcessor?(\n bufferFrames: number,\n inputChannels: number,\n outputChannels: number,\n ): ScriptProcessorCaptureNode;\n close(): Promise<void>;\n}\n\ninterface CaptureGlobals {\n navigator?: {\n mediaDevices?: { getUserMedia?(constraints: { audio: boolean }): Promise<CaptureStream> };\n };\n AudioContext?: new () => CaptureContext;\n AudioWorkletNode?: new (context: CaptureContext, name: string) => WorkletCaptureNode;\n}\n\n/** A capture node and the removal of the handler it was given. */\ninterface AttachedNode {\n node: CaptureNode;\n /** Detaches the handler, so a block delivered after teardown reaches nothing. */\n detach(): void;\n}\n\ntype BlockHandler = (channels: readonly Float32Array[]) => void;\n\n/** One recording in progress. Everything it holds must be released exactly once. */\ninterface CaptureSession {\n blocks: Float32Array[];\n frames: number;\n rate: number;\n /** The auto-stop deadline, as a sample count, beside the samples it bounds. */\n maxFrames: number;\n /** The loudest block since the meter last reported. */\n peak: number;\n /** The frame count at the last meter update. */\n reportedAt: number;\n release(): void;\n}\n\n/**\n * A `ScriptProcessorNode`'s buffer, in frames.\n *\n * This fallback runs its callback on the main thread, so the buffer has to\n * hold whatever a render blocks it for; 4096 frames is about 85 ms at 48 kHz,\n * and a smaller one drops samples out of the middle of a take whenever the\n * page is busy. The worklet path, which runs on the audio thread, needs none\n * of this.\n */\nconst PROCESSOR_BUFFER_FRAMES = 4096;\n\n/** How often the meter and the elapsed time may move. */\nconst LEVEL_UPDATES_PER_SECOND = 10;\n\nfunction stopTracks(stream: CaptureStream): void {\n for (const track of stream.getTracks()) {\n track.stop();\n }\n}\n\nfunction closeQuietly(context: CaptureContext): void {\n void context.close().catch(() => {\n /* a context the page has already closed; nothing depends on the result */\n });\n}\n\n/**\n * Which refusal `getUserMedia` made.\n *\n * Read from `name`, not from the message: messages are localised and are\n * rewritten between browser versions, while the names are specified. Anything\n * unrecognised is `failed` rather than a guess — telling a learner their\n * permission was denied when their device was busy sends them to the wrong\n * setting.\n */\nfunction microphoneError(error: unknown): SpeechRecorderError {\n switch ((error as { name?: string } | null)?.name) {\n case 'NotAllowedError':\n case 'SecurityError':\n return 'permission-denied';\n case 'NotFoundError':\n return 'no-device';\n case 'NotSupportedError':\n return 'unsupported';\n default:\n return 'failed';\n }\n}\n\n/**\n * The worklet capture node, or `null` when this engine cannot build one.\n *\n * `null` rather than a throw for every one of its reasons — no\n * `AudioWorkletNode`, no worklet thread, an object URL the page's\n * Content-Security-Policy refuses, a module that fails to load — because none\n * of them is a failed recording: the caller has a working fallback for all of\n * them, and only the absence of both is an unsupported browser.\n */\nasync function createWorkletNode(\n context: CaptureContext,\n workletUrl: string | undefined,\n onBlock: BlockHandler,\n): Promise<AttachedNode | null> {\n const AudioWorkletNodeConstructor = (globalThis as unknown as CaptureGlobals).AudioWorkletNode;\n const worklet = context.audioWorklet;\n if (AudioWorkletNodeConstructor === undefined || worklet === undefined) {\n return null;\n }\n const minted = workletUrl === undefined ? createProcessorUrl() : null;\n const url = workletUrl ?? minted?.url;\n if (url === undefined) {\n return null;\n }\n try {\n await worklet.addModule(url);\n const node = new AudioWorkletNodeConstructor(context, CAPTURE_PROCESSOR);\n node.port.onmessage = (event) => {\n onBlock(event.data as readonly Float32Array[]);\n };\n return {\n node,\n detach: () => {\n node.port.onmessage = null;\n },\n };\n } catch {\n return null;\n } finally {\n // Revoked as soon as the module has been fetched, whether it loaded or\n // not: an object URL lives until the document is discarded otherwise, and\n // this one is minted once per take.\n minted?.release();\n }\n}\n\n/** The `ScriptProcessorNode` fallback, or `null` on an engine without one. */\nfunction createProcessorNode(context: CaptureContext, onBlock: BlockHandler): AttachedNode | null {\n const create = context.createScriptProcessor;\n if (create === undefined) {\n return null;\n }\n const node = create.call(context, PROCESSOR_BUFFER_FRAMES, 1, 1);\n node.onaudioprocess = (event) => {\n const buffer = event.inputBuffer;\n const channels: Float32Array[] = [];\n for (let channel = 0; channel < buffer.numberOfChannels; channel += 1) {\n channels.push(buffer.getChannelData(channel));\n }\n onBlock(channels);\n };\n return {\n node,\n detach: () => {\n node.onaudioprocess = null;\n },\n };\n}\n\n/**\n * Records a take of speech as mono 16-bit PCM WAV at 16 kHz.\n *\n * That is the one format the SDK measures directly: `inspectWav` reads it\n * without a decoding library, and it is what every mainstream pronunciation\n * assessor accepts. A take is captured at whatever rate the device runs at,\n * downmixed to mono, low-pass filtered and resampled, then encoded — so what a\n * server measures is what this hook produced, not a claim about it.\n *\n * **Server rendering.** `status`, `error` and `canRecord` are derived from the\n * take count alone and never from a browser global, so the markup a server\n * produces is the markup the browser's first render produces. Support is\n * discovered inside {@link SpeechRecorder.start} and reported as\n * `error: 'unsupported'`; the obvious `!!navigator.mediaDevices` guard at\n * render time would make a server render and its hydration disagree.\n *\n * **Identity.** `start`, `stop`, `discard` and `reset` keep one identity for the\n * life of the hook, so they are safe in a dependency array. The returned object keeps\n * one identity for as long as none of its values changes, so a render caused\n * by something else does not invalidate an effect that depends on it.\n *\n * `start()` does nothing when no take remains, so `canRecord` is presentation:\n * mark the control `aria-disabled` rather than `disabled`, which would take it\n * out of the tab order and leave a learner nothing to read.\n *\n * ```tsx\n * const recorder = useSpeechRecorder({ maxDurationMs: 20_000, maxTakes: 3 });\n * <button\n * type=\"button\"\n * aria-disabled={!recorder.canRecord}\n * onClick={() => {\n * void recorder.start();\n * }}\n * >\n * Record\n * </button>\n * ```\n */\nexport function useSpeechRecorder(options: SpeechRecorderOptions): SpeechRecorder {\n // The latest options without changing `start`/`stop`/`discard`'s identity\n // when the caller passes an inline object, exactly as `useXAPI` keeps its\n // config.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const [state, setState] = useState<RecorderState>(IDLE_RECORDER);\n // The state as of this instant, written synchronously. Two events in one\n // tick — an auto-stop reached inside a capture callback and the learner's\n // own `stop()` — would otherwise both read the value React has not committed\n // yet, which is the bug `AudioTransport` and `ActivitySequence` each carry a\n // report of.\n const stateRef = useRef<RecorderState>(IDLE_RECORDER);\n const sessionRef = useRef<CaptureSession | null>(null);\n const startingRef = useRef(false);\n // Bumped by anything that abandons an attempt. `start()` re-reads it after\n // every await: a learner who discards, or a component that unmounts, while\n // the permission prompt is still open must not have a microphone opened\n // behind them and left open for ever.\n const attemptRef = useRef(0);\n\n const dispatch = useCallback((event: RecorderEvent) => {\n const next = reduceRecorder(stateRef.current, event);\n if (next === stateRef.current) {\n return;\n }\n stateRef.current = next;\n setState(next);\n }, []);\n\n const endSession = useCallback(() => {\n const session = sessionRef.current;\n sessionRef.current = null;\n session?.release();\n }, []);\n\n const finish = useCallback(() => {\n const session = sessionRef.current;\n if (session === null) {\n return;\n }\n sessionRef.current = null;\n session.release();\n\n const { minDurationMs } = optionsRef.current;\n const samples = resampleToTarget(joinBlocks(session.blocks, session.frames), session.rate);\n // Taken from the encoded samples, so a take reports the duration a server\n // will measure in its bytes rather than a wall-clock timing of the attempt.\n //\n // No second bound is applied here. The capture was bounded at the device's\n // own rate, and resampling cannot push a whole-millisecond bound over:\n // checked across every rate from 8 to 192 kHz and every bound up to five\n // minutes, the encoded length never exceeds what the bound allows. (A\n // fractional `maxDurationMs` can overshoot by up to a fortieth of a\n // millisecond, which is less than one sample at this rate.)\n const durationMs = (samples.length / TARGET_SAMPLE_RATE) * 1000;\n if (minDurationMs !== undefined && durationMs < minDurationMs) {\n dispatch({ kind: 'failed', error: 'too-short' });\n return;\n }\n dispatch({\n kind: 'captured',\n take: {\n blob: new Blob([encodeWav16(samples, TARGET_SAMPLE_RATE)], { type: WAV_MIME_TYPE }),\n mimeType: WAV_MIME_TYPE,\n durationMs,\n peakLevel: peakOf(samples),\n },\n });\n }, [dispatch]);\n\n const handleBlock = useCallback(\n (channels: readonly Float32Array[]) => {\n const session = sessionRef.current;\n if (session === null) {\n return;\n }\n let block = downmix(channels);\n const room = session.maxFrames - session.frames;\n if (block.length > room) {\n block = block.subarray(0, room);\n }\n session.blocks.push(block);\n session.frames += block.length;\n const level = rmsOf(block);\n if (level > session.peak) {\n session.peak = level;\n }\n\n // Auto-stop is measured from the samples themselves, never from a timer:\n // the samples ARE the recording, so a take cannot run past its bound\n // however the page's timers were scheduled, and there is no deadline\n // left for a learner's own `stop()` to race.\n if (session.frames >= session.maxFrames) {\n finish();\n return;\n }\n if (session.frames - session.reportedAt < session.rate / LEVEL_UPDATES_PER_SECOND) {\n return;\n }\n session.reportedAt = session.frames;\n // The meter reports the loudest block since it last did, not the latest\n // one: at ten updates a second the latest block is one in forty, and a\n // meter that samples speech that thinly flickers instead of reading.\n const peak = session.peak;\n session.peak = 0;\n dispatch({\n kind: 'progress',\n level: peak,\n elapsedMs: (session.frames / session.rate) * 1000,\n });\n },\n [dispatch, finish],\n );\n\n const start = useCallback(async (): Promise<void> => {\n const { maxDurationMs, maxTakes, workletUrl } = optionsRef.current;\n // A second start while one is in flight, or while a capture is running, is\n // a learner pressing twice; a start with no takes left is a control the\n // caller should not have offered. Neither is an error worth a message.\n if (startingRef.current || sessionRef.current !== null) {\n return;\n }\n if (maxTakes !== undefined && stateRef.current.takesUsed >= maxTakes) {\n return;\n }\n const attempt = attemptRef.current;\n startingRef.current = true;\n dispatch({ kind: 'requested' });\n // Everything acquired so far, in the order it must be given back. Each\n // step replaces it, so every early return and the catch release exactly\n // what exists at that point.\n let release = (): void => {};\n\n try {\n // Support is discovered HERE and nowhere else — see the hook's note on\n // server rendering.\n const view = globalThis as unknown as CaptureGlobals;\n const devices = view.navigator?.mediaDevices;\n const getUserMedia = devices?.getUserMedia;\n const AudioContextConstructor = view.AudioContext;\n if (\n devices === undefined ||\n getUserMedia === undefined ||\n AudioContextConstructor === undefined\n ) {\n dispatch({ kind: 'failed', error: 'unsupported' });\n return;\n }\n\n let stream: CaptureStream;\n try {\n stream = await getUserMedia.call(devices, { audio: true });\n } catch (error) {\n dispatch({ kind: 'failed', error: microphoneError(error) });\n return;\n }\n release = () => {\n stopTracks(stream);\n };\n if (attempt !== attemptRef.current) {\n release();\n dispatch({ kind: 'discarded' });\n return;\n }\n\n let context: CaptureContext;\n try {\n context = new AudioContextConstructor();\n } catch {\n release();\n dispatch({ kind: 'failed', error: 'unsupported' });\n return;\n }\n const open = context;\n release = () => {\n stopTracks(stream);\n closeQuietly(open);\n };\n\n const attached =\n (await createWorkletNode(open, workletUrl, handleBlock)) ??\n createProcessorNode(open, handleBlock);\n if (attached === null) {\n release();\n dispatch({ kind: 'failed', error: 'unsupported' });\n return;\n }\n\n const source = open.createMediaStreamSource(stream);\n // A capture node is only pulled while it is reachable from the\n // destination, so the graph ends in a gain of zero: without the\n // connection some engines never call the processor at all, and with a\n // plain one the learner hears themselves through their own speakers.\n const sink = open.createGain();\n sink.gain.value = 0;\n source.connect(attached.node);\n attached.node.connect(sink);\n sink.connect(open.destination);\n release = () => {\n attached.detach();\n attached.node.disconnect();\n source.disconnect();\n sink.disconnect();\n stopTracks(stream);\n closeQuietly(open);\n };\n if (attempt !== attemptRef.current) {\n release();\n dispatch({ kind: 'discarded' });\n return;\n }\n\n sessionRef.current = {\n blocks: [],\n frames: 0,\n rate: open.sampleRate,\n maxFrames: Math.max(1, Math.floor((maxDurationMs / 1000) * open.sampleRate)),\n peak: 0,\n reportedAt: 0,\n release,\n };\n dispatch({ kind: 'started' });\n } catch {\n // `start()` never rejects. A recorder that throws into a click handler\n // leaves the learner with a dead control and no message, so anything\n // unforeseen becomes `failed` — which the caller already has a message\n // for — and gives back whatever had been acquired.\n release();\n sessionRef.current = null;\n dispatch({ kind: 'failed', error: 'failed' });\n } finally {\n startingRef.current = false;\n }\n }, [dispatch, handleBlock]);\n\n const stop = useCallback(() => {\n if (sessionRef.current === null) {\n // Before the microphone opens there is nothing to encode, so a stop is a\n // cancelled attempt: the bump is what makes `start()` give back the\n // stream it is about to be handed.\n attemptRef.current += 1;\n return;\n }\n finish();\n }, [finish]);\n\n const discard = useCallback(() => {\n attemptRef.current += 1;\n endSession();\n dispatch({ kind: 'discarded' });\n }, [dispatch, endSession]);\n\n const reset = useCallback(() => {\n attemptRef.current += 1;\n endSession();\n dispatch({ kind: 'reset' });\n }, [dispatch, endSession]);\n\n useEffect(() => {\n // Cleanup only. Every acquisition happens inside `start()` and is released\n // by the session it created, so the mount run has nothing to do and\n // StrictMode's extra mount/cleanup pair leaks nothing.\n return () => {\n attemptRef.current += 1;\n endSession();\n };\n }, [endSession]);\n\n const { maxTakes } = options;\n return useMemo(\n () => ({\n status: state.status,\n error: state.error,\n level: state.level,\n elapsedMs: state.elapsedMs,\n take: state.take,\n takesUsed: state.takesUsed,\n canRecord: maxTakes === undefined || state.takesUsed < maxTakes,\n start,\n stop,\n discard,\n reset,\n }),\n [state, maxTakes, start, stop, discard, reset],\n );\n}\n"]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { FeedbackRegion } from './chunk-RCMYEWBE.js';
|
|
3
|
+
import { ActivityErrorBoundary, isDevelopment, randomSessionId, ANONYMOUS_ACTOR, objectIdFor } from './chunk-B4BCEQNO.js';
|
|
4
|
+
import { ActivityMedia } from './chunk-DB4DLHN6.js';
|
|
4
5
|
import { useActivityState } from './chunk-6N43WDVG.js';
|
|
5
|
-
import { useLkStrings } from './chunk-
|
|
6
|
+
import { useLkStrings } from './chunk-AWQ53DON.js';
|
|
6
7
|
import { validateActivity, ActivitySchemaError, seededShuffle, score, xAPIBuilder, xapiDefinitionFor } from '@intellectif/lk-core';
|
|
7
8
|
import { useMemo, useRef, useState, useEffect, Fragment } from 'react';
|
|
8
9
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
@@ -290,5 +291,5 @@ function MultipleChoice2(props) {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
export { MultipleChoice2 as MultipleChoice };
|
|
293
|
-
//# sourceMappingURL=chunk-
|
|
294
|
-
//# sourceMappingURL=chunk-
|
|
294
|
+
//# sourceMappingURL=chunk-UDQS5INQ.js.map
|
|
295
|
+
//# sourceMappingURL=chunk-UDQS5INQ.js.map
|