@memberjunction/livekit-room-server 6.2.0-edge.2 → 6.2.0-edge.4

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.
Files changed (82) hide show
  1. package/README.md +85 -0
  2. package/dist/index.d.ts +18 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +18 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/livekit-agent-room-coordinator.d.ts +220 -13
  7. package/dist/livekit-agent-room-coordinator.d.ts.map +1 -1
  8. package/dist/livekit-agent-room-coordinator.js +409 -65
  9. package/dist/livekit-agent-room-coordinator.js.map +1 -1
  10. package/dist/livekit-egress-service.d.ts +7 -0
  11. package/dist/livekit-egress-service.d.ts.map +1 -1
  12. package/dist/livekit-egress-service.js +11 -0
  13. package/dist/livekit-egress-service.js.map +1 -1
  14. package/dist/livekit-native-module.d.ts +18 -0
  15. package/dist/livekit-native-module.d.ts.map +1 -0
  16. package/dist/livekit-native-module.js +27 -0
  17. package/dist/livekit-native-module.js.map +1 -0
  18. package/dist/livekit-participant-service.d.ts +47 -0
  19. package/dist/livekit-participant-service.d.ts.map +1 -0
  20. package/dist/livekit-participant-service.js +64 -0
  21. package/dist/livekit-participant-service.js.map +1 -0
  22. package/dist/livekit-sip-service.d.ts +141 -0
  23. package/dist/livekit-sip-service.d.ts.map +1 -0
  24. package/dist/livekit-sip-service.js +169 -0
  25. package/dist/livekit-sip-service.js.map +1 -0
  26. package/dist/livekit-token-service.d.ts +26 -5
  27. package/dist/livekit-token-service.d.ts.map +1 -1
  28. package/dist/livekit-token-service.js +22 -7
  29. package/dist/livekit-token-service.js.map +1 -1
  30. package/dist/livekit-webhook.d.ts +65 -0
  31. package/dist/livekit-webhook.d.ts.map +1 -0
  32. package/dist/livekit-webhook.js +65 -0
  33. package/dist/livekit-webhook.js.map +1 -0
  34. package/dist/meeting-dial-in-service.d.ts +83 -0
  35. package/dist/meeting-dial-in-service.d.ts.map +1 -0
  36. package/dist/meeting-dial-in-service.js +202 -0
  37. package/dist/meeting-dial-in-service.js.map +1 -0
  38. package/dist/room-audio/audio-decoder.d.ts +40 -0
  39. package/dist/room-audio/audio-decoder.d.ts.map +1 -0
  40. package/dist/room-audio/audio-decoder.js +205 -0
  41. package/dist/room-audio/audio-decoder.js.map +1 -0
  42. package/dist/room-audio/comfort-tone.d.ts +23 -0
  43. package/dist/room-audio/comfort-tone.d.ts.map +1 -0
  44. package/dist/room-audio/comfort-tone.js +70 -0
  45. package/dist/room-audio/comfort-tone.js.map +1 -0
  46. package/dist/room-audio/pcm-frame-pump.d.ts +134 -0
  47. package/dist/room-audio/pcm-frame-pump.d.ts.map +1 -0
  48. package/dist/room-audio/pcm-frame-pump.js +253 -0
  49. package/dist/room-audio/pcm-frame-pump.js.map +1 -0
  50. package/dist/room-audio/room-audio-decode-cache.d.ts +37 -0
  51. package/dist/room-audio/room-audio-decode-cache.d.ts.map +1 -0
  52. package/dist/room-audio/room-audio-decode-cache.js +75 -0
  53. package/dist/room-audio/room-audio-decode-cache.js.map +1 -0
  54. package/dist/room-audio/room-audio-player.d.ts +178 -0
  55. package/dist/room-audio/room-audio-player.d.ts.map +1 -0
  56. package/dist/room-audio/room-audio-player.js +404 -0
  57. package/dist/room-audio/room-audio-player.js.map +1 -0
  58. package/dist/room-audio/room-audio-sources.d.ts +47 -0
  59. package/dist/room-audio/room-audio-sources.d.ts.map +1 -0
  60. package/dist/room-audio/room-audio-sources.js +116 -0
  61. package/dist/room-audio/room-audio-sources.js.map +1 -0
  62. package/dist/room-audio/room-speech-synthesizer.d.ts +55 -0
  63. package/dist/room-audio/room-speech-synthesizer.d.ts.map +1 -0
  64. package/dist/room-audio/room-speech-synthesizer.js +56 -0
  65. package/dist/room-audio/room-speech-synthesizer.js.map +1 -0
  66. package/dist/room-authorization.d.ts +58 -0
  67. package/dist/room-authorization.d.ts.map +1 -0
  68. package/dist/room-authorization.js +212 -0
  69. package/dist/room-authorization.js.map +1 -0
  70. package/dist/room-handoff/handoff-offer-registry.d.ts +147 -0
  71. package/dist/room-handoff/handoff-offer-registry.d.ts.map +1 -0
  72. package/dist/room-handoff/handoff-offer-registry.js +548 -0
  73. package/dist/room-handoff/handoff-offer-registry.js.map +1 -0
  74. package/dist/room-handoff/handoff-types.d.ts +172 -0
  75. package/dist/room-handoff/handoff-types.d.ts.map +1 -0
  76. package/dist/room-handoff/handoff-types.js +16 -0
  77. package/dist/room-handoff/handoff-types.js.map +1 -0
  78. package/dist/room-handoff/room-handoff-engine.d.ts +149 -0
  79. package/dist/room-handoff/room-handoff-engine.d.ts.map +1 -0
  80. package/dist/room-handoff/room-handoff-engine.js +563 -0
  81. package/dist/room-handoff/room-handoff-engine.js.map +1 -0
  82. package/package.json +15 -9
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @fileoverview {@link DecodeAudio} — turns an audio file's bytes (WAV or MP3) into mono PCM16, the one shape the
3
+ * room audio player plays. WAV is parsed here (RIFF chunk walk, integer PCM 8/16/24/32-bit and IEEE float 32/64-bit,
4
+ * any channel count downmixed); MP3 is decoded by `mpg123-decoder`, a WebAssembly build of libmpg123 with no native
5
+ * build step. Anything else is refused with an error that names the format it saw.
6
+ *
7
+ * @module @memberjunction/livekit-room-server
8
+ */
9
+ import { MPEGDecoder } from 'mpg123-decoder';
10
+ /** The longest audio {@link DecodeAudio} accepts, in seconds (15 minutes). Hold music loops; a longer file is a mistake. */
11
+ export const MAX_DECODED_AUDIO_SECONDS = 15 * 60;
12
+ const WAV_FORMAT_PCM = 1;
13
+ const WAV_FORMAT_FLOAT = 3;
14
+ const WAV_FORMAT_EXTENSIBLE = 0xfffe;
15
+ /**
16
+ * Decodes WAV or MP3 bytes into mono PCM16. The format is read from the bytes' own signature first and from the hint
17
+ * only when the bytes carry none (a bare MP3 frame stream).
18
+ *
19
+ * @param bytes The file's bytes.
20
+ * @param hint The MIME type and/or file name, when known.
21
+ * @returns Mono PCM16 at the file's own sample rate.
22
+ * @throws {Error} when the format is not WAV or MP3, the file is malformed or empty, or it is longer than
23
+ * {@link MAX_DECODED_AUDIO_SECONDS}.
24
+ */
25
+ export async function DecodeAudio(bytes, hint) {
26
+ if (bytes.byteLength === 0) {
27
+ throw new Error('Cannot decode audio: the file is empty.');
28
+ }
29
+ const format = detectFormat(bytes, hint);
30
+ const decoded = format === 'wav' ? decodeWav(bytes) : await decodeMp3(bytes);
31
+ AssertAudioDurationWithinCap(decoded.Pcm.length, decoded.SampleRate);
32
+ return decoded;
33
+ }
34
+ /** Throws when `sampleCount` samples at `sampleRate` run longer than {@link MAX_DECODED_AUDIO_SECONDS}. */
35
+ export function AssertAudioDurationWithinCap(sampleCount, sampleRate) {
36
+ const seconds = sampleCount / sampleRate;
37
+ if (seconds > MAX_DECODED_AUDIO_SECONDS) {
38
+ throw new Error(`Audio is ${Math.round(seconds)} s long; the room audio player accepts at most ${MAX_DECODED_AUDIO_SECONDS} s ` +
39
+ '(15 minutes). Trim the file — hold music loops, so a few minutes is plenty.');
40
+ }
41
+ }
42
+ // ── Format detection ─────────────────────────────────────────────────────────────────────────────────────────────
43
+ function ascii(bytes, start, length) {
44
+ return String.fromCharCode(...bytes.subarray(start, start + length));
45
+ }
46
+ function isMp3Signature(bytes) {
47
+ if (bytes.byteLength >= 3 && ascii(bytes, 0, 3) === 'ID3') {
48
+ return true;
49
+ }
50
+ // An MPEG audio frame header starts with 11 set sync bits.
51
+ return bytes.byteLength >= 2 && bytes[0] === 0xff && (bytes[1] & 0xe0) === 0xe0;
52
+ }
53
+ /** Names a recognisable but unsupported container from its signature, for the error message. */
54
+ function describeUnsupportedSignature(bytes) {
55
+ if (bytes.byteLength >= 4 && ascii(bytes, 0, 4) === 'OggS')
56
+ return 'Ogg (Vorbis/Opus)';
57
+ if (bytes.byteLength >= 4 && ascii(bytes, 0, 4) === 'fLaC')
58
+ return 'FLAC';
59
+ if (bytes.byteLength >= 8 && ascii(bytes, 4, 4) === 'ftyp')
60
+ return 'MP4/M4A (AAC)';
61
+ if (bytes.byteLength >= 4 && ascii(bytes, 0, 4) === 'FORM')
62
+ return 'AIFF';
63
+ if (bytes.byteLength >= 4 && bytes[0] === 0x1a && bytes[1] === 0x45 && bytes[2] === 0xdf && bytes[3] === 0xa3)
64
+ return 'WebM/Matroska';
65
+ return undefined;
66
+ }
67
+ function formatFromHint(hint) {
68
+ const mime = hint?.MimeType?.toLowerCase().split(';')[0].trim() ?? '';
69
+ const name = hint?.FileName?.toLowerCase() ?? '';
70
+ if (['audio/wav', 'audio/x-wav', 'audio/wave', 'audio/vnd.wave'].includes(mime) || name.endsWith('.wav'))
71
+ return 'wav';
72
+ if (['audio/mpeg', 'audio/mp3', 'audio/mpeg3', 'audio/x-mpeg-3'].includes(mime) || name.endsWith('.mp3'))
73
+ return 'mp3';
74
+ return undefined;
75
+ }
76
+ function detectFormat(bytes, hint) {
77
+ if (bytes.byteLength >= 12 && ascii(bytes, 0, 4) === 'RIFF' && ascii(bytes, 8, 4) === 'WAVE') {
78
+ return 'wav';
79
+ }
80
+ if (isMp3Signature(bytes)) {
81
+ return 'mp3';
82
+ }
83
+ const unsupported = describeUnsupportedSignature(bytes);
84
+ const hinted = unsupported ? undefined : formatFromHint(hint);
85
+ if (hinted === 'mp3') {
86
+ return 'mp3'; // an MP3 stream may begin mid-frame; let the decoder resync
87
+ }
88
+ const seen = unsupported ?? (hint?.MimeType || hint?.FileName || 'unrecognised bytes');
89
+ throw new Error(`Unsupported audio format: ${seen}. The room audio player plays WAV (PCM or float) and MP3.`);
90
+ }
91
+ // ── WAV ──────────────────────────────────────────────────────────────────────────────────────────────────────────
92
+ /** Walks the RIFF chunks and returns the `fmt ` description and the `data` byte range. */
93
+ function readWavChunks(bytes) {
94
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
95
+ let format;
96
+ let offset = 12;
97
+ while (offset + 8 <= bytes.byteLength) {
98
+ const id = ascii(bytes, offset, 4);
99
+ const size = view.getUint32(offset + 4, true);
100
+ const body = offset + 8;
101
+ if (id === 'fmt ') {
102
+ format = readWavFormat(view, body, size);
103
+ }
104
+ else if (id === 'data') {
105
+ if (!format) {
106
+ throw new Error('Malformed WAV: the data chunk comes before the fmt chunk.');
107
+ }
108
+ // A streaming writer may leave the size as 0 or 0xFFFFFFFF; clamp to what is actually there.
109
+ return { Format: format, DataOffset: body, DataLength: Math.min(size, bytes.byteLength - body) };
110
+ }
111
+ offset = body + size + (size & 1); // chunks are word-aligned: an odd-sized chunk carries one pad byte
112
+ }
113
+ throw new Error(format ? 'Malformed WAV: no data chunk.' : 'Malformed WAV: no fmt chunk.');
114
+ }
115
+ function readWavFormat(view, body, size) {
116
+ if (size < 16 || body + 16 > view.byteLength) {
117
+ throw new Error('Malformed WAV: the fmt chunk is truncated.');
118
+ }
119
+ let formatCode = view.getUint16(body, true);
120
+ if (formatCode === WAV_FORMAT_EXTENSIBLE && size >= 40 && body + 26 <= view.byteLength) {
121
+ formatCode = view.getUint16(body + 24, true); // the first two bytes of the SubFormat GUID are the real code
122
+ }
123
+ return {
124
+ FormatCode: formatCode,
125
+ Channels: view.getUint16(body + 2, true),
126
+ SampleRate: view.getUint32(body + 4, true),
127
+ BitsPerSample: view.getUint16(body + 14, true),
128
+ };
129
+ }
130
+ function sampleReaderFor(format) {
131
+ const { FormatCode: code, BitsPerSample: bits } = format;
132
+ if (code === WAV_FORMAT_PCM && bits === 8)
133
+ return (v, o) => (v.getUint8(o) - 128) / 128;
134
+ if (code === WAV_FORMAT_PCM && bits === 16)
135
+ return (v, o) => v.getInt16(o, true) / 32768;
136
+ if (code === WAV_FORMAT_PCM && bits === 24) {
137
+ return (v, o) => ((v.getUint8(o) | (v.getUint8(o + 1) << 8) | (v.getInt8(o + 2) << 16)) / 8388608);
138
+ }
139
+ if (code === WAV_FORMAT_PCM && bits === 32)
140
+ return (v, o) => v.getInt32(o, true) / 2147483648;
141
+ if (code === WAV_FORMAT_FLOAT && bits === 32)
142
+ return (v, o) => v.getFloat32(o, true);
143
+ if (code === WAV_FORMAT_FLOAT && bits === 64)
144
+ return (v, o) => v.getFloat64(o, true);
145
+ throw new Error(`Unsupported WAV encoding: format code ${code}, ${bits}-bit. The room audio player plays integer PCM ` +
146
+ '(8/16/24/32-bit) and IEEE float (32/64-bit) WAV.');
147
+ }
148
+ function decodeWav(bytes) {
149
+ const { Format: format, DataOffset: dataOffset, DataLength: dataLength } = readWavChunks(bytes);
150
+ if (format.Channels < 1 || format.SampleRate < 1) {
151
+ throw new Error(`Malformed WAV: ${format.Channels} channel(s) at ${format.SampleRate} Hz.`);
152
+ }
153
+ const read = sampleReaderFor(format);
154
+ const bytesPerSample = format.BitsPerSample / 8;
155
+ const frameBytes = bytesPerSample * format.Channels;
156
+ const frames = Math.floor(dataLength / frameBytes);
157
+ if (frames === 0) {
158
+ throw new Error('Cannot decode audio: the WAV data chunk holds no samples.');
159
+ }
160
+ AssertAudioDurationWithinCap(frames, format.SampleRate); // before allocating the output for an over-long file
161
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
162
+ const pcm = new Int16Array(frames);
163
+ for (let frame = 0; frame < frames; frame++) {
164
+ const base = dataOffset + frame * frameBytes;
165
+ let sum = 0;
166
+ for (let channel = 0; channel < format.Channels; channel++) {
167
+ sum += read(view, base + channel * bytesPerSample);
168
+ }
169
+ pcm[frame] = FloatToPcm16(sum / format.Channels);
170
+ }
171
+ return { Pcm: pcm, SampleRate: format.SampleRate };
172
+ }
173
+ /** Converts one [-1, 1] float sample to PCM16, clamping anything outside the range. */
174
+ export function FloatToPcm16(sample) {
175
+ const clamped = sample > 1 ? 1 : sample < -1 ? -1 : sample;
176
+ return clamped < 0 ? Math.round(clamped * 32768) : Math.round(clamped * 32767);
177
+ }
178
+ // ── MP3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────
179
+ async function decodeMp3(bytes) {
180
+ const decoder = new MPEGDecoder();
181
+ try {
182
+ await decoder.ready;
183
+ const result = decoder.decode(bytes);
184
+ if (result.samplesDecoded === 0 || result.channelData.length === 0) {
185
+ const reason = result.errors.length > 0 ? result.errors[0].message : 'no audio frames found';
186
+ throw new Error(`Cannot decode MP3: ${reason}.`);
187
+ }
188
+ return { Pcm: downmixFloatChannels(result.channelData, result.samplesDecoded), SampleRate: result.sampleRate };
189
+ }
190
+ finally {
191
+ decoder.free();
192
+ }
193
+ }
194
+ function downmixFloatChannels(channels, sampleCount) {
195
+ const pcm = new Int16Array(sampleCount);
196
+ for (let i = 0; i < sampleCount; i++) {
197
+ let sum = 0;
198
+ for (const channel of channels) {
199
+ sum += channel[i];
200
+ }
201
+ pcm[i] = FloatToPcm16(sum / channels.length);
202
+ }
203
+ return pcm;
204
+ }
205
+ //# sourceMappingURL=audio-decoder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-decoder.js","sourceRoot":"","sources":["../../src/room-audio/audio-decoder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview {@link GenerateComfortTone} — a procedurally generated hold loop for when a queue has no music
3
+ * configured. Four soft major/minor pads (C – Am – F – G, the progression everyone has heard a thousand times and
4
+ * nobody minds) with slow raised-cosine swells, a quiet bass an octave below, and a touch of second harmonic for
5
+ * warmth. Each chord fades fully out before the next fades in, so the loop seam is silent and never clicks.
6
+ *
7
+ * Synthesized at runtime: no audio asset ships, so there is nothing to license.
8
+ *
9
+ * @module @memberjunction/livekit-room-server
10
+ */
11
+ /** Length of one pass of the comfort tone, in seconds. */
12
+ export declare const COMFORT_TONE_LOOP_SECONDS = 10;
13
+ /** Peak level of the comfort tone as a fraction of full scale (about −15 dBFS — quieter than speech). */
14
+ export declare const COMFORT_TONE_PEAK = 0.18;
15
+ /**
16
+ * Generates one {@link COMFORT_TONE_LOOP_SECONDS}-second pass of the comfort tone as mono PCM16. Loop it end to end.
17
+ *
18
+ * @param sampleRate The rate to synthesize at, in Hz (generate at the playback rate to avoid resampling).
19
+ * @returns Mono PCM16 samples; every sample's magnitude is at most {@link COMFORT_TONE_PEAK} of full scale.
20
+ * @throws {Error} when `sampleRate` is not a positive integer.
21
+ */
22
+ export declare function GenerateComfortTone(sampleRate: number): Int16Array;
23
+ //# sourceMappingURL=comfort-tone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comfort-tone.d.ts","sourceRoot":"","sources":["../../src/room-audio/comfort-tone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,0DAA0D;AAC1D,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,yGAAyG;AACzG,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAiBtC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,CAQlE"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @fileoverview {@link GenerateComfortTone} — a procedurally generated hold loop for when a queue has no music
3
+ * configured. Four soft major/minor pads (C – Am – F – G, the progression everyone has heard a thousand times and
4
+ * nobody minds) with slow raised-cosine swells, a quiet bass an octave below, and a touch of second harmonic for
5
+ * warmth. Each chord fades fully out before the next fades in, so the loop seam is silent and never clicks.
6
+ *
7
+ * Synthesized at runtime: no audio asset ships, so there is nothing to license.
8
+ *
9
+ * @module @memberjunction/livekit-room-server
10
+ */
11
+ /** Length of one pass of the comfort tone, in seconds. */
12
+ export const COMFORT_TONE_LOOP_SECONDS = 10;
13
+ /** Peak level of the comfort tone as a fraction of full scale (about −15 dBFS — quieter than speech). */
14
+ export const COMFORT_TONE_PEAK = 0.18;
15
+ /** One chord of the progression: the frequencies of its voices, root first. */
16
+ const CHORDS = [
17
+ [261.63, 329.63, 392.0], // C major (C4 E4 G4)
18
+ [220.0, 261.63, 329.63], // A minor (A3 C4 E4)
19
+ [174.61, 220.0, 261.63], // F major (F3 A3 C4)
20
+ [196.0, 246.94, 293.66], // G major (G3 B3 D4)
21
+ ];
22
+ /** Fraction of each chord spent swelling in (and, symmetrically, fading out). */
23
+ const SWELL_FRACTION = 0.3;
24
+ /** Relative weight of the bass voice (the chord root an octave down) and of each voice's second harmonic. */
25
+ const BASS_WEIGHT = 0.5;
26
+ const HARMONIC_WEIGHT = 0.15;
27
+ /**
28
+ * Generates one {@link COMFORT_TONE_LOOP_SECONDS}-second pass of the comfort tone as mono PCM16. Loop it end to end.
29
+ *
30
+ * @param sampleRate The rate to synthesize at, in Hz (generate at the playback rate to avoid resampling).
31
+ * @returns Mono PCM16 samples; every sample's magnitude is at most {@link COMFORT_TONE_PEAK} of full scale.
32
+ * @throws {Error} when `sampleRate` is not a positive integer.
33
+ */
34
+ export function GenerateComfortTone(sampleRate) {
35
+ if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
36
+ throw new Error(`GenerateComfortTone needs a positive integer sample rate; got ${sampleRate}.`);
37
+ }
38
+ const chordSamples = Math.round((COMFORT_TONE_LOOP_SECONDS / CHORDS.length) * sampleRate);
39
+ const out = new Int16Array(chordSamples * CHORDS.length);
40
+ CHORDS.forEach((chord, index) => renderChord(out, index * chordSamples, chordSamples, chord, sampleRate));
41
+ return out;
42
+ }
43
+ /** Writes one enveloped chord into `out` starting at `start`. */
44
+ function renderChord(out, start, length, chord, sampleRate) {
45
+ const voices = [...chord, chord[0] / 2];
46
+ const weights = [...chord.map(() => 1), BASS_WEIGHT];
47
+ // Normalise so the worst case (every voice and harmonic in phase at the envelope peak) stays at COMFORT_TONE_PEAK.
48
+ const worstCase = weights.reduce((total, weight) => total + weight * (1 + HARMONIC_WEIGHT), 0);
49
+ const scale = (COMFORT_TONE_PEAK * 32767) / worstCase;
50
+ for (let i = 0; i < length; i++) {
51
+ const t = i / sampleRate;
52
+ let sample = 0;
53
+ for (let v = 0; v < voices.length; v++) {
54
+ const phase = 2 * Math.PI * voices[v] * t;
55
+ sample += weights[v] * (Math.sin(phase) + HARMONIC_WEIGHT * Math.sin(2 * phase));
56
+ }
57
+ out[start + i] = Math.round(sample * swellEnvelope(i / length) * scale);
58
+ }
59
+ }
60
+ /** A raised-cosine swell: 0 at both ends of the chord, 1 across the middle. */
61
+ function swellEnvelope(position) {
62
+ if (position < SWELL_FRACTION) {
63
+ return 0.5 - 0.5 * Math.cos((Math.PI * position) / SWELL_FRACTION);
64
+ }
65
+ if (position > 1 - SWELL_FRACTION) {
66
+ return 0.5 - 0.5 * Math.cos((Math.PI * (1 - position)) / SWELL_FRACTION);
67
+ }
68
+ return 1;
69
+ }
70
+ //# sourceMappingURL=comfort-tone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comfort-tone.js","sourceRoot":"","sources":["../../src/room-audio/comfort-tone.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @fileoverview {@link PcmFramePump} — plays a PCM16 buffer out to a sink in real time, one 20 ms frame at a time.
3
+ *
4
+ * The native LiveKit wrapper's `publishAudio` queues whatever it is given and drains it as fast as the WebRTC audio
5
+ * source accepts it; it does not pace. Handing it a whole song would buffer minutes of audio in memory and make pause,
6
+ * stop and announcements lag by that much. So the pump paces itself: a fast ticker compares monotonic elapsed time
7
+ * with frames sent and emits only what is due, plus a small lead (~150 ms) that absorbs timer jitter. The lead is all
8
+ * that is ever outstanding downstream.
9
+ *
10
+ * - **Drift correction** — frames due are computed from the elapsed time since the timeline started, never by
11
+ * counting ticks, so a late tick sends two frames and the average rate stays exact.
12
+ * - **Bounded catch-up** — after a long stall (a blocked event loop) the pump does not burst the backlog; it drops it
13
+ * and restarts the timeline, so the downstream queue can never grow past the lead plus a small margin.
14
+ * - **Loop**, **Pause/Resume** (resume restarts the timeline — no burst), and **InsertClip**: the music fades down to a
15
+ * duck level, the clip plays over it, and the music fades back up.
16
+ *
17
+ * The clock and ticker are injectable so tests drive time by hand.
18
+ *
19
+ * @module @memberjunction/livekit-room-server
20
+ */
21
+ /** A running ticker the pump can cancel. */
22
+ export interface PumpTicker {
23
+ /** Stops the ticker. Idempotent. */
24
+ Cancel(): void;
25
+ }
26
+ /** The pump's source of time and ticks — `performance.now()` + `setInterval` in production, a fake in tests. */
27
+ export interface PumpClock {
28
+ /** Monotonic milliseconds. */
29
+ Now(): number;
30
+ /** Calls `callback` every `intervalMs` until cancelled. */
31
+ StartTicker(callback: () => void, intervalMs: number): PumpTicker;
32
+ }
33
+ /** Why a pump stopped: its (non-looping) audio ran out, it was stopped, or the sink threw. */
34
+ export type PumpEndReason = 'Completed' | 'Stopped' | 'SinkError';
35
+ /** Construction options for {@link PcmFramePump}. */
36
+ export interface PcmFramePumpOptions {
37
+ /** Mono PCM16 to play, at {@link SampleRate}. Must not be empty. */
38
+ Music: Int16Array;
39
+ /** The rate of {@link Music}, of inserted clips, and of the frames handed to {@link Sink}. */
40
+ SampleRate: number;
41
+ /** Receives each frame. Every frame is a freshly allocated array the sink may keep. */
42
+ Sink: (frame: Int16Array<ArrayBuffer>) => void;
43
+ /** Loop {@link Music} forever. Default `true`. */
44
+ Loop?: boolean;
45
+ /** Frame duration in ms. Default 20. */
46
+ FrameMs?: number;
47
+ /** How far ahead of real time the pump keeps the sink, in ms. Default 150. */
48
+ LeadMs?: number;
49
+ /** How long the music takes to fade down for a clip (and back up after), in ms. Default 150. */
50
+ DuckMs?: number;
51
+ /** The music's level while a clip plays, 0–1. Default 0.15. */
52
+ DuckLevel?: number;
53
+ /** Time source. Default: {@link SystemPumpClock}. */
54
+ Clock?: PumpClock;
55
+ /** Called once when the pump stops, with the reason. */
56
+ OnEnded?: (reason: PumpEndReason) => void;
57
+ }
58
+ /** The production clock: `performance.now()` and an unref'd `setInterval` (a playing pump never holds the process open). */
59
+ export declare const SystemPumpClock: PumpClock;
60
+ /** Paces mono PCM16 out to a sink in real time. See the file overview. */
61
+ export declare class PcmFramePump {
62
+ private readonly music;
63
+ private readonly sampleRate;
64
+ private readonly sink;
65
+ private readonly loop;
66
+ private readonly frameMs;
67
+ private readonly samplesPerFrame;
68
+ private readonly leadFrames;
69
+ private readonly maxBurstFrames;
70
+ private readonly duckLevel;
71
+ private readonly gainStep;
72
+ private readonly clock;
73
+ private readonly onEnded?;
74
+ private ticker;
75
+ private running;
76
+ private paused;
77
+ private ended;
78
+ private timelineStart;
79
+ private framesOnTimeline;
80
+ private framesSent;
81
+ private musicPosition;
82
+ private musicDone;
83
+ private gain;
84
+ private readonly clipQueue;
85
+ private currentClip;
86
+ private clipPosition;
87
+ /** @throws {Error} when the music is empty or a timing option is not positive. */
88
+ constructor(options: PcmFramePumpOptions);
89
+ /** True between {@link Start} and the pump ending (including while paused). */
90
+ get IsRunning(): boolean;
91
+ /** True while paused. */
92
+ get IsPaused(): boolean;
93
+ /** Frames handed to the sink so far. */
94
+ get FramesSent(): number;
95
+ /** Samples per frame at the pump's rate. */
96
+ get SamplesPerFrame(): number;
97
+ /** Starts playback: sends the lead immediately, then paces in real time. A second call is a no-op. */
98
+ Start(): void;
99
+ /** Stops sending frames until {@link Resume}. Playback position, ducking and queued clips are kept. */
100
+ Pause(): void;
101
+ /** Continues after {@link Pause} from where it left off, on a fresh timeline (so nothing bursts). */
102
+ Resume(): void;
103
+ /**
104
+ * How much already-sent audio is still ahead of real time, in ms: the pump keeps the sink up to `LeadMs` ahead, so
105
+ * after the last frame is sent the room is still playing this much. Zero once real time has caught up.
106
+ */
107
+ get QueuedAheadMs(): number;
108
+ /**
109
+ * Plays `pcm` over the music: the music fades down to the duck level, the clip plays, the music fades back up.
110
+ * Clips queue behind one another.
111
+ *
112
+ * @param pcm Mono PCM16 at the pump's sample rate.
113
+ * @returns Resolves `true` once the clip has been sent in full, `false` if the pump stopped first or the clip is empty.
114
+ */
115
+ InsertClip(pcm: Int16Array): Promise<boolean>;
116
+ /** Stops the pump for good. Idempotent; pending clips resolve `false`. */
117
+ Stop(): void;
118
+ private restartTimeline;
119
+ private tick;
120
+ /** Frames owed to the sink now, given elapsed time and the lead. Drops the backlog after a long stall. */
121
+ private framesDue;
122
+ private emitFrame;
123
+ /** The next output sample in [-1, 1]: ducked music plus whatever clip is playing. */
124
+ private nextSample;
125
+ /** Moves the music gain one step toward its target: the duck level while a clip is queued or playing, else full. */
126
+ private advanceGain;
127
+ private startQueuedClipIfDucked;
128
+ private nextMusicSample;
129
+ private nextClipSample;
130
+ /** Ends the pump once: cancels the ticker, fails any pending clips, reports the reason. */
131
+ private finish;
132
+ private failPendingClips;
133
+ }
134
+ //# sourceMappingURL=pcm-frame-pump.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pcm-frame-pump.d.ts","sourceRoot":"","sources":["../../src/room-audio/pcm-frame-pump.ts"],"names":[],"mappings":"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"}