@prosopo/audio-assets 0.0.0-stage → 0.2.1

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 (50) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +23 -0
  2. package/.turbo/turbo-build$colon$tsc.log +14 -0
  3. package/.turbo/turbo-build.log +25 -0
  4. package/CHANGELOG.md +25 -0
  5. package/LICENSE +201 -0
  6. package/dist/_virtual/_rolldown/runtime.js +3 -0
  7. package/dist/cjs/distort.cjs +91 -0
  8. package/dist/cjs/encode.cjs +40 -0
  9. package/dist/cjs/index.cjs +109 -0
  10. package/dist/cjs/levels.cjs +18 -0
  11. package/dist/cjs/phonemes.cjs +217 -0
  12. package/dist/cjs/synth.cjs +222 -0
  13. package/dist/distort.d.ts +7 -0
  14. package/dist/distort.d.ts.map +1 -0
  15. package/dist/distort.js +88 -0
  16. package/dist/distort.js.map +1 -0
  17. package/dist/encode.d.ts +4 -0
  18. package/dist/encode.d.ts.map +1 -0
  19. package/dist/encode.js +39 -0
  20. package/dist/encode.js.map +1 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +84 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/levels.d.ts +3 -0
  26. package/dist/levels.d.ts.map +1 -0
  27. package/dist/levels.js +17 -0
  28. package/dist/levels.js.map +1 -0
  29. package/dist/phonemes.d.ts +4 -0
  30. package/dist/phonemes.d.ts.map +1 -0
  31. package/dist/phonemes.js +216 -0
  32. package/dist/phonemes.js.map +1 -0
  33. package/dist/synth.d.ts +13 -0
  34. package/dist/synth.d.ts.map +1 -0
  35. package/dist/synth.js +220 -0
  36. package/dist/synth.js.map +1 -0
  37. package/dist/tests/audioAssets.unit.test.d.ts +2 -0
  38. package/dist/tests/audioAssets.unit.test.d.ts.map +1 -0
  39. package/dist/tests/audioAssets.unit.test.js +220 -0
  40. package/dist/tests/audioAssets.unit.test.js.map +1 -0
  41. package/dist/types.d.ts +35 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +2 -0
  44. package/dist/types.js.map +1 -0
  45. package/package.json +52 -3
  46. package/scripts/preview.ts +141 -0
  47. package/vite.cjs.config.ts +23 -0
  48. package/vite.esm.config.ts +20 -0
  49. package/vite.test.config.ts +32 -0
  50. package/README.md +0 -3
@@ -0,0 +1,217 @@
1
+ //#region src/phonemes.ts
2
+ var STOP_BURST_MS = 18;
3
+ var VOICED_FRICATIVE_VOICE_GAIN = .45;
4
+ var NASAL_VOICE_GAIN = .5;
5
+ var APPROXIMANT_VOICE_GAIN = .85;
6
+ var segmentDefaults = {
7
+ bandwidths: [
8
+ 80,
9
+ 110,
10
+ 160
11
+ ],
12
+ voiceGain: 1,
13
+ noiseGain: 0,
14
+ noiseCentreHz: 0,
15
+ noiseBandwidthHz: 0
16
+ };
17
+ var vowel = (id, f1, f2, f3, durationMs) => ({
18
+ ...segmentDefaults,
19
+ id,
20
+ excitation: "voiced",
21
+ formants: [
22
+ f1,
23
+ f2,
24
+ f3
25
+ ],
26
+ durationMs
27
+ });
28
+ var fricative = (id, centreHz, bandwidthHz, gain, durationMs) => ({
29
+ ...segmentDefaults,
30
+ id,
31
+ excitation: "unvoiced",
32
+ formants: [
33
+ 0,
34
+ 0,
35
+ 0
36
+ ],
37
+ durationMs,
38
+ voiceGain: 0,
39
+ noiseGain: gain,
40
+ noiseCentreHz: centreHz,
41
+ noiseBandwidthHz: bandwidthHz
42
+ });
43
+ var voicedFricative = (id, f1, f2, f3, centreHz, bandwidthHz, gain, durationMs) => ({
44
+ ...segmentDefaults,
45
+ id,
46
+ excitation: "mixed",
47
+ formants: [
48
+ f1,
49
+ f2,
50
+ f3
51
+ ],
52
+ durationMs,
53
+ voiceGain: VOICED_FRICATIVE_VOICE_GAIN,
54
+ noiseGain: gain,
55
+ noiseCentreHz: centreHz,
56
+ noiseBandwidthHz: bandwidthHz
57
+ });
58
+ var nasal = (id, f1, f2, f3, durationMs) => ({
59
+ ...segmentDefaults,
60
+ id,
61
+ excitation: "voiced",
62
+ formants: [
63
+ f1,
64
+ f2,
65
+ f3
66
+ ],
67
+ bandwidths: [
68
+ 180,
69
+ 250,
70
+ 320
71
+ ],
72
+ durationMs,
73
+ voiceGain: NASAL_VOICE_GAIN
74
+ });
75
+ var closure = (id, durationMs) => ({
76
+ ...segmentDefaults,
77
+ id,
78
+ excitation: "silence",
79
+ formants: [
80
+ 0,
81
+ 0,
82
+ 0
83
+ ],
84
+ durationMs,
85
+ voiceGain: 0
86
+ });
87
+ var burst = (id, centreHz, bandwidthHz, gain) => ({
88
+ ...segmentDefaults,
89
+ id,
90
+ excitation: "unvoiced",
91
+ formants: [
92
+ 0,
93
+ 0,
94
+ 0
95
+ ],
96
+ durationMs: STOP_BURST_MS,
97
+ voiceGain: 0,
98
+ noiseGain: gain,
99
+ noiseCentreHz: centreHz,
100
+ noiseBandwidthHz: bandwidthHz,
101
+ abrupt: true
102
+ });
103
+ var IY = vowel("IY", 270, 2290, 3010, 150);
104
+ var IH = vowel("IH", 390, 1990, 2550, 95);
105
+ var EH = vowel("EH", 530, 1840, 2480, 110);
106
+ var AH = vowel("AH", 640, 1190, 2390, 105);
107
+ var AA = vowel("AA", 730, 1090, 2440, 140);
108
+ var AO = vowel("AO", 570, 840, 2410, 150);
109
+ var UW = vowel("UW", 300, 870, 2240, 140);
110
+ var OW_OFF = vowel("OW^", 330, 900, 2300, 90);
111
+ var R = {
112
+ ...vowel("R", 490, 1350, 1690, 85),
113
+ voiceGain: APPROXIMANT_VOICE_GAIN
114
+ };
115
+ var W = {
116
+ ...vowel("W", 300, 610, 2200, 70),
117
+ voiceGain: APPROXIMANT_VOICE_GAIN
118
+ };
119
+ var N = nasal("N", 250, 1750, 2600, 90);
120
+ var S = fricative("S", 5800, 3200, .5, 130);
121
+ var F = fricative("F", 4200, 5e3, .24, 120);
122
+ var TH = fricative("TH", 5200, 5600, .2, 110);
123
+ var Z = voicedFricative("Z", 300, 1600, 2500, 5200, 3e3, .3, 110);
124
+ var V = voicedFricative("V", 320, 1100, 2400, 4e3, 4600, .16, 95);
125
+ var T = [closure("T-", 45), burst("T+", 3800, 3400, .55)];
126
+ var K = [closure("K-", 45), burst("K+", 2100, 2200, .5)];
127
+ var AY = [AA, {
128
+ ...IY,
129
+ durationMs: 110
130
+ }];
131
+ var EY = [EH, {
132
+ ...IY,
133
+ durationMs: 105
134
+ }];
135
+ /** "zero", not "oh": a lone vowel with no consonant anchor is the first thing lost to noise. */
136
+ var DIGITS = [
137
+ {
138
+ answer: "0",
139
+ phonemes: [
140
+ Z,
141
+ IH,
142
+ R,
143
+ ...[AO, OW_OFF]
144
+ ]
145
+ },
146
+ {
147
+ answer: "1",
148
+ phonemes: [
149
+ W,
150
+ AH,
151
+ N
152
+ ]
153
+ },
154
+ {
155
+ answer: "2",
156
+ phonemes: [...T, UW]
157
+ },
158
+ {
159
+ answer: "3",
160
+ phonemes: [
161
+ TH,
162
+ R,
163
+ IY
164
+ ]
165
+ },
166
+ {
167
+ answer: "4",
168
+ phonemes: [
169
+ F,
170
+ AO,
171
+ R
172
+ ]
173
+ },
174
+ {
175
+ answer: "5",
176
+ phonemes: [
177
+ F,
178
+ ...AY,
179
+ V
180
+ ]
181
+ },
182
+ {
183
+ answer: "6",
184
+ phonemes: [
185
+ S,
186
+ IH,
187
+ ...K,
188
+ S
189
+ ]
190
+ },
191
+ {
192
+ answer: "7",
193
+ phonemes: [
194
+ S,
195
+ EH,
196
+ V,
197
+ AH,
198
+ N
199
+ ]
200
+ },
201
+ {
202
+ answer: "8",
203
+ phonemes: [...EY, ...T]
204
+ },
205
+ {
206
+ answer: "9",
207
+ phonemes: [
208
+ N,
209
+ ...AY,
210
+ N
211
+ ]
212
+ }
213
+ ];
214
+ var ANSWER_ALPHABET = DIGITS.map((d) => d.answer).join("");
215
+ //#endregion
216
+ exports.ANSWER_ALPHABET = ANSWER_ALPHABET;
217
+ exports.DIGITS = DIGITS;
@@ -0,0 +1,222 @@
1
+ const require_levels = require("./levels.cjs");
2
+ //#region src/synth.ts
3
+ /** Not 8 kHz: /s/, /f/ and /θ/ live at 4-8 kHz and are what separate six, five and three. */
4
+ var SAMPLE_RATE = 16e3;
5
+ var NYQUIST_GUARD_HZ = 100;
6
+ var MIN_FORMANT_BANDWIDTH_HZ = 10;
7
+ var MIN_NOISE_BANDWIDTH_HZ = 100;
8
+ var LEVEL_RAMP_SECONDS = .004;
9
+ var EDGE_FADE_SECONDS = .008;
10
+ var WORD_PEAK = .9;
11
+ var SILENT_RMS = 1e-9;
12
+ var GLOTTAL_OPEN_QUOTIENT = .6;
13
+ var GLOTTAL_SPEED_QUOTIENT = .35;
14
+ var FORMANT_INDICES = [
15
+ 0,
16
+ 1,
17
+ 2
18
+ ];
19
+ var randomVoice = (prng) => ({
20
+ f0Hz: prng.range(85, 210),
21
+ f0DriftRatio: prng.range(.04, .16),
22
+ formantScale: prng.range(.86, 1.22),
23
+ rate: prng.range(.82, 1.24),
24
+ tiltCornerHz: prng.range(4500, 8e3)
25
+ });
26
+ /** Two-pole formant resonator, normalised to unity DC gain so a cascade stays bounded. */
27
+ var createResonator = (sampleRate) => {
28
+ let y1 = 0;
29
+ let y2 = 0;
30
+ let a = 1;
31
+ let b = 0;
32
+ let c = 0;
33
+ return {
34
+ setTarget(freqHz, bandwidthHz) {
35
+ const f = Math.min(Math.max(freqHz, 1), sampleRate / 2 - NYQUIST_GUARD_HZ);
36
+ const bw = Math.max(bandwidthHz, MIN_FORMANT_BANDWIDTH_HZ);
37
+ const r = Math.exp(-Math.PI * bw / sampleRate);
38
+ const theta = 2 * Math.PI * f / sampleRate;
39
+ b = 2 * r * Math.cos(theta);
40
+ c = -(r * r);
41
+ a = 1 - b - c;
42
+ },
43
+ process(x) {
44
+ const y = a * x + b * y1 + c * y2;
45
+ y2 = y1;
46
+ y1 = y;
47
+ return y;
48
+ }
49
+ };
50
+ };
51
+ var createOnePoleLowPass = (sampleRate, cutoffHz) => {
52
+ const dt = 1 / sampleRate;
53
+ const alpha = dt / (1 / (2 * Math.PI * cutoffHz) + dt);
54
+ let y = 0;
55
+ return (x) => {
56
+ y += alpha * (x - y);
57
+ return y;
58
+ };
59
+ };
60
+ /**
61
+ * Lip radiation, a +6 dB/octave differentiator. Without it the glottal
62
+ * pulse's DC and steep rolloff bury F2 and F3, which are what tell vowels
63
+ * apart.
64
+ */
65
+ var createRadiation = () => {
66
+ let previous = 0;
67
+ return (x) => {
68
+ const y = x - previous;
69
+ previous = x;
70
+ return y;
71
+ };
72
+ };
73
+ /** Rosenberg glottal pulse; a bare impulse train through the formants sounds like a buzzer. */
74
+ var createGlottalSource = (sampleRate) => {
75
+ const rise = GLOTTAL_OPEN_QUOTIENT * (1 - GLOTTAL_SPEED_QUOTIENT);
76
+ const fall = GLOTTAL_OPEN_QUOTIENT * GLOTTAL_SPEED_QUOTIENT;
77
+ let phase = 0;
78
+ return (f0Hz) => {
79
+ phase += f0Hz / sampleRate;
80
+ if (phase >= 1) phase -= 1;
81
+ if (phase < rise) return .5 * (1 - Math.cos(Math.PI * phase / rise));
82
+ if (phase < GLOTTAL_OPEN_QUOTIENT) return Math.cos(Math.PI / 2 * ((phase - rise) / fall));
83
+ return 0;
84
+ };
85
+ };
86
+ var lerp = (from, to, t) => from + (to - from) * t;
87
+ var isBlendable = (previous, current) => !current.abrupt && !previous.abrupt && previous.excitation !== "silence" && current.excitation !== "silence";
88
+ var buildFrames = (phonemes, voice, sampleRate) => {
89
+ const frames = [];
90
+ const segments = [];
91
+ const scaled = (p) => [
92
+ p.formants[0] * voice.formantScale,
93
+ p.formants[1] * voice.formantScale,
94
+ p.formants[2] * voice.formantScale
95
+ ];
96
+ let previous;
97
+ for (const current of phonemes) {
98
+ const lengthSamples = Math.max(1, Math.round(current.durationMs * voice.rate * sampleRate / 1e3));
99
+ const from = previous && isBlendable(previous, current) ? previous : current;
100
+ const blendSamples = from === current ? 0 : Math.floor(lengthSamples / 3);
101
+ const source = scaled(from);
102
+ const target = scaled(current);
103
+ segments.push({
104
+ start: frames.length,
105
+ end: frames.length + lengthSamples,
106
+ targetLevel: current.excitation === "silence" ? 0 : Math.max(current.voiceGain, current.noiseGain)
107
+ });
108
+ for (let n = 0; n < lengthSamples; n++) {
109
+ const t = blendSamples > 0 ? Math.min(1, n / blendSamples) : 1;
110
+ frames.push({
111
+ formants: [
112
+ lerp(source[0], target[0], t),
113
+ lerp(source[1], target[1], t),
114
+ lerp(source[2], target[2], t)
115
+ ],
116
+ bandwidths: [
117
+ lerp(from.bandwidths[0], current.bandwidths[0], t),
118
+ lerp(from.bandwidths[1], current.bandwidths[1], t),
119
+ lerp(from.bandwidths[2], current.bandwidths[2], t)
120
+ ],
121
+ voiceGain: lerp(from.voiceGain, current.voiceGain, t),
122
+ noiseGain: lerp(from.noiseGain, current.noiseGain, t),
123
+ noiseCentreHz: current.noiseCentreHz,
124
+ noiseBandwidthHz: current.noiseBandwidthHz,
125
+ silent: current.excitation === "silence"
126
+ });
127
+ }
128
+ previous = current;
129
+ }
130
+ return {
131
+ frames,
132
+ segments
133
+ };
134
+ };
135
+ /**
136
+ * Rescale each segment to its target level, ramping the gain across
137
+ * boundaries: a gain step clicks, and a click at every phoneme boundary is a
138
+ * free segmentation cue for an attacker.
139
+ */
140
+ var applySegmentLevels = (samples, segments, sampleRate) => {
141
+ const rampLength = Math.max(1, Math.round(sampleRate * LEVEL_RAMP_SECONDS));
142
+ const gains = segments.map((segment) => {
143
+ if (segment.targetLevel <= 0) return 0;
144
+ const level = require_levels.rms(samples.subarray(segment.start, segment.end));
145
+ return level < SILENT_RMS ? 0 : segment.targetLevel / level;
146
+ });
147
+ const track = new Float32Array(samples.length);
148
+ segments.forEach((segment, i) => {
149
+ track.fill(gains[i] ?? 0, segment.start, segment.end);
150
+ });
151
+ for (let i = 1; i < segments.length; i++) {
152
+ const segment = segments[i];
153
+ const gain = gains[i];
154
+ const previousGain = gains[i - 1];
155
+ if (!segment || gain === void 0 || previousGain === void 0) continue;
156
+ const length = Math.min(rampLength, segment.end - segment.start);
157
+ for (let n = 0; n < length; n++) {
158
+ const index = segment.start + n;
159
+ if (index >= track.length) break;
160
+ const t = .5 * (1 - Math.cos(Math.PI * n / length));
161
+ track[index] = previousGain + (gain - previousGain) * t;
162
+ }
163
+ }
164
+ for (let n = 0; n < samples.length; n++) samples[n] = (samples[n] ?? 0) * (track[n] ?? 0);
165
+ };
166
+ /** Raised-cosine fade in and out, in place. */
167
+ var applyEdgeFade = (samples, fadeLength) => {
168
+ const length = Math.min(fadeLength, Math.floor(samples.length / 2));
169
+ for (let n = 0; n < length; n++) {
170
+ const gain = .5 * (1 - Math.cos(Math.PI * n / length));
171
+ const tail = samples.length - 1 - n;
172
+ samples[n] = (samples[n] ?? 0) * gain;
173
+ samples[tail] = (samples[tail] ?? 0) * gain;
174
+ }
175
+ };
176
+ var synthesiseUtterance = (utterance, voice, prng, sampleRate = SAMPLE_RATE) => {
177
+ const { frames, segments } = buildFrames(utterance.phonemes, voice, sampleRate);
178
+ const out = new Float32Array(frames.length);
179
+ const formantFilters = [
180
+ createResonator(sampleRate),
181
+ createResonator(sampleRate),
182
+ createResonator(sampleRate)
183
+ ];
184
+ const noiseFilter = createResonator(sampleRate);
185
+ const glottis = createGlottalSource(sampleRate);
186
+ const radiation = createRadiation();
187
+ const tilt = createOnePoleLowPass(sampleRate, voice.tiltCornerHz);
188
+ const f0Start = voice.f0Hz * (1 + voice.f0DriftRatio / 2);
189
+ const f0End = voice.f0Hz * (1 - voice.f0DriftRatio / 2);
190
+ const jitterDepth = prng.range(.004, .02);
191
+ frames.forEach((frame, n) => {
192
+ if (frame.silent) return;
193
+ const progress = frames.length > 1 ? n / (frames.length - 1) : 0;
194
+ const f0 = lerp(f0Start, f0End, progress) * (1 + jitterDepth * (prng.next() - .5));
195
+ let sample = 0;
196
+ if (frame.voiceGain > 0) {
197
+ let voiced = tilt(glottis(f0));
198
+ for (const k of FORMANT_INDICES) {
199
+ formantFilters[k].setTarget(frame.formants[k], frame.bandwidths[k]);
200
+ voiced = formantFilters[k].process(voiced);
201
+ }
202
+ sample += voiced * frame.voiceGain;
203
+ }
204
+ if (frame.noiseGain > 0) {
205
+ noiseFilter.setTarget(frame.noiseCentreHz, Math.max(frame.noiseBandwidthHz, MIN_NOISE_BANDWIDTH_HZ));
206
+ const noise = prng.next() * 2 - 1;
207
+ sample += noiseFilter.process(noise) * frame.noiseGain;
208
+ }
209
+ out[n] = radiation(sample);
210
+ });
211
+ applySegmentLevels(out, segments, sampleRate);
212
+ applyEdgeFade(out, Math.round(sampleRate * EDGE_FADE_SECONDS));
213
+ require_levels.normalise(out, WORD_PEAK);
214
+ return {
215
+ samples: out,
216
+ sampleRate
217
+ };
218
+ };
219
+ //#endregion
220
+ exports.SAMPLE_RATE = SAMPLE_RATE;
221
+ exports.randomVoice = randomVoice;
222
+ exports.synthesiseUtterance = synthesiseUtterance;
@@ -0,0 +1,7 @@
1
+ import type { Prng } from "@prosopo/puzzle-assets";
2
+ import type { AudioBuffer } from "./types.js";
3
+ export declare const mixInto: (target: Float32Array, source: Float32Array, offset: number, gain: number) => void;
4
+ export declare const addNoiseBed: (buffer: AudioBuffer, prng: Prng, snrDb: number) => void;
5
+ export declare const addReverb: (buffer: AudioBuffer, mix: number) => void;
6
+ export declare const finalise: (buffer: AudioBuffer) => void;
7
+ //# sourceMappingURL=distort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distort.d.ts","sourceRoot":"","sources":["../src/distort.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAU9C,eAAO,MAAM,OAAO,WACX,YAAY,UACZ,YAAY,UACZ,MAAM,QACR,MAAM,KACV,IAMF,CAAC;AAMF,eAAO,MAAM,WAAW,WACf,WAAW,QACb,IAAI,SACH,MAAM,KACX,IAsBF,CAAC;AAUF,eAAO,MAAM,SAAS,WAAY,WAAW,OAAO,MAAM,KAAG,IAkC5D,CAAC;AAMF,eAAO,MAAM,QAAQ,WAAY,WAAW,KAAG,IAM9C,CAAC"}
@@ -0,0 +1,88 @@
1
+ import { normalise, rms } from "./levels.js";
2
+ //#region src/distort.ts
3
+ var REVERB_COMB_DELAYS_MS = [
4
+ 29.7,
5
+ 37.1,
6
+ 41.1,
7
+ 43.7
8
+ ];
9
+ var REVERB_COMB_FEEDBACK = .72;
10
+ var REVERB_ALLPASS_DELAYS_MS = [5, 1.7];
11
+ var REVERB_ALLPASS_FEEDBACK = .5;
12
+ var SOFT_CLIP_DRIVE = 1.2;
13
+ var OUTPUT_PEAK = .92;
14
+ /** Adds `source` into `target` from `offset`; anything past the end is dropped. */
15
+ var mixInto = (target, source, offset, gain) => {
16
+ const start = Math.max(0, Math.floor(offset));
17
+ const count = Math.min(source.length, target.length - start);
18
+ for (let n = 0; n < count; n++) target[start + n] = (target[start + n] ?? 0) + (source[n] ?? 0) * gain;
19
+ };
20
+ /**
21
+ * Pink-ish noise at `snrDb` below the signal's RMS. Pink rather than white
22
+ * because white noise spends its energy above the formants and masks little.
23
+ */
24
+ var addNoiseBed = (buffer, prng, snrDb) => {
25
+ const signalRms = rms(buffer.samples);
26
+ if (signalRms === 0) return;
27
+ const targetRms = signalRms / 10 ** (snrDb / 20);
28
+ let b0 = 0;
29
+ let b1 = 0;
30
+ let b2 = 0;
31
+ const noise = new Float32Array(buffer.samples.length);
32
+ for (let n = 0; n < noise.length; n++) {
33
+ const white = prng.next() * 2 - 1;
34
+ b0 = .99765 * b0 + white * .099;
35
+ b1 = .963 * b1 + white * .2965;
36
+ b2 = .57 * b2 + white * 1.0526;
37
+ noise[n] = (b0 + b1 + b2 + white * .1848) * .2;
38
+ }
39
+ const noiseRms = rms(noise);
40
+ if (noiseRms === 0) return;
41
+ mixInto(buffer.samples, noise, 0, targetRms / noiseRms);
42
+ };
43
+ var delaySamples = (delayMs, sampleRate) => Math.max(1, Math.round(delayMs / 1e3 * sampleRate));
44
+ /**
45
+ * Schroeder reverb: parallel combs into series allpasses. Its purpose is to
46
+ * smear onsets, which otherwise make it easy to chop the clip into
47
+ * one-digit segments and classify each in isolation.
48
+ */
49
+ var addReverb = (buffer, mix) => {
50
+ if (mix <= 0) return;
51
+ const { samples, sampleRate } = buffer;
52
+ const wet = new Float32Array(samples.length);
53
+ for (const delayMs of REVERB_COMB_DELAYS_MS) {
54
+ const delay = delaySamples(delayMs, sampleRate);
55
+ const line = new Float32Array(delay);
56
+ let index = 0;
57
+ for (let n = 0; n < samples.length; n++) {
58
+ const delayed = line[index] ?? 0;
59
+ wet[n] = (wet[n] ?? 0) + delayed / REVERB_COMB_DELAYS_MS.length;
60
+ line[index] = (samples[n] ?? 0) + delayed * REVERB_COMB_FEEDBACK;
61
+ index = (index + 1) % delay;
62
+ }
63
+ }
64
+ for (const delayMs of REVERB_ALLPASS_DELAYS_MS) {
65
+ const delay = delaySamples(delayMs, sampleRate);
66
+ const line = new Float32Array(delay);
67
+ let index = 0;
68
+ for (let n = 0; n < wet.length; n++) {
69
+ const input = wet[n] ?? 0;
70
+ const delayed = line[index] ?? 0;
71
+ line[index] = input + delayed * REVERB_ALLPASS_FEEDBACK;
72
+ wet[n] = -input * REVERB_ALLPASS_FEEDBACK + delayed;
73
+ index = (index + 1) % delay;
74
+ }
75
+ }
76
+ for (let n = 0; n < samples.length; n++) samples[n] = (samples[n] ?? 0) * (1 - mix) + (wet[n] ?? 0) * mix;
77
+ };
78
+ /**
79
+ * tanh soft clip, then normalise: hard clipping at 16-bit conversion is
80
+ * audibly nasty and a distinctive artefact.
81
+ */
82
+ var finalise = (buffer) => {
83
+ const { samples } = buffer;
84
+ for (let n = 0; n < samples.length; n++) samples[n] = Math.tanh((samples[n] ?? 0) * SOFT_CLIP_DRIVE);
85
+ normalise(samples, OUTPUT_PEAK);
86
+ };
87
+ //#endregion
88
+ export { addNoiseBed, addReverb, finalise, mixInto };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distort.js","sourceRoot":"","sources":["../src/distort.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { AudioBuffer } from "./types.js";
2
+ export declare const encodeWav: (buffer: AudioBuffer) => Buffer;
3
+ export declare const toDataUri: (wav: Buffer) => string;
4
+ //# sourceMappingURL=encode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../src/encode.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAgB9C,eAAO,MAAM,SAAS,WAAY,WAAW,KAAG,MA+B/C,CAAC;AAEF,eAAO,MAAM,SAAS,QAAS,MAAM,KAAG,MACU,CAAC"}
package/dist/encode.js ADDED
@@ -0,0 +1,39 @@
1
+ //#region src/encode.ts
2
+ var RIFF_HEADER_BYTES = 44;
3
+ var RIFF_PREAMBLE_BYTES = 8;
4
+ var FMT_CHUNK_BYTES = 16;
5
+ var BITS_PER_SAMPLE = 16;
6
+ var BYTES_PER_SAMPLE = BITS_PER_SAMPLE / 8;
7
+ var CHANNELS = 1;
8
+ var PCM_FORMAT_TAG = 1;
9
+ var INT16_SCALE = 32767;
10
+ /**
11
+ * Uncompressed WAV because every browser plays 16-bit PCM with no codec
12
+ * dependency or artefacts that could be mistaken for the challenge.
13
+ */
14
+ var encodeWav = (buffer) => {
15
+ const { samples, sampleRate } = buffer;
16
+ const dataBytes = samples.length * BYTES_PER_SAMPLE;
17
+ const out = Buffer.alloc(RIFF_HEADER_BYTES + dataBytes);
18
+ out.write("RIFF", 0, "ascii");
19
+ out.writeUInt32LE(RIFF_HEADER_BYTES - RIFF_PREAMBLE_BYTES + dataBytes, 4);
20
+ out.write("WAVE", 8, "ascii");
21
+ out.write("fmt ", 12, "ascii");
22
+ out.writeUInt32LE(FMT_CHUNK_BYTES, 16);
23
+ out.writeUInt16LE(PCM_FORMAT_TAG, 20);
24
+ out.writeUInt16LE(CHANNELS, 22);
25
+ out.writeUInt32LE(sampleRate, 24);
26
+ out.writeUInt32LE(sampleRate * CHANNELS * BYTES_PER_SAMPLE, 28);
27
+ out.writeUInt16LE(CHANNELS * BYTES_PER_SAMPLE, 32);
28
+ out.writeUInt16LE(BITS_PER_SAMPLE, 34);
29
+ out.write("data", 36, "ascii");
30
+ out.writeUInt32LE(dataBytes, 40);
31
+ for (let n = 0; n < samples.length; n++) {
32
+ const clamped = Math.max(-1, Math.min(1, samples[n] ?? 0));
33
+ out.writeInt16LE(Math.round(clamped * INT16_SCALE), RIFF_HEADER_BYTES + n * BYTES_PER_SAMPLE);
34
+ }
35
+ return out;
36
+ };
37
+ var toDataUri = (wav) => `data:audio/wav;base64,${wav.toString("base64")}`;
38
+ //#endregion
39
+ export { encodeWav, toDataUri };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encode.js","sourceRoot":"","sources":["../src/encode.ts"],"names":[],"mappings":"AAgBA,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAC9B,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,gBAAgB,GAAG,eAAe,GAAG,CAAC,CAAC;AAC7C,MAAM,QAAQ,GAAG,CAAC,CAAC;AACnB,MAAM,cAAc,GAAG,CAAC,CAAC;AAEzB,MAAM,WAAW,GAAG,MAAM,CAAC;AAM3B,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAmB,EAAU,EAAE;IACxD,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC;IACvC,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,gBAAgB,CAAC;IACpD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IAExD,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IAC9B,GAAG,CAAC,aAAa,CAAC,iBAAiB,GAAG,mBAAmB,GAAG,SAAS,EAAE,CAAC,CAAC,CAAC;IAC1E,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IAE9B,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;IAC/B,GAAG,CAAC,aAAa,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IACvC,GAAG,CAAC,aAAa,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IACtC,GAAG,CAAC,aAAa,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChC,GAAG,CAAC,aAAa,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAClC,GAAG,CAAC,aAAa,CAAC,UAAU,GAAG,QAAQ,GAAG,gBAAgB,EAAE,EAAE,CAAC,CAAC;IAChE,GAAG,CAAC,aAAa,CAAC,QAAQ,GAAG,gBAAgB,EAAE,EAAE,CAAC,CAAC;IACnD,GAAG,CAAC,aAAa,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAEvC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;IAC/B,GAAG,CAAC,aAAa,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAEjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAEzC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3D,GAAG,CAAC,YAAY,CACf,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,WAAW,CAAC,EACjC,iBAAiB,GAAG,CAAC,GAAG,gBAAgB,CACxC,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACZ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAW,EAAU,EAAE,CAChD,yBAAyB,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { AudioRenderSettings, RenderedAudioChallenge } from "./types.js";
2
+ export { createPrng, createSeed } from "@prosopo/puzzle-assets";
3
+ export type { Prng } from "@prosopo/puzzle-assets";
4
+ export { ANSWER_ALPHABET, DIGITS } from "./phonemes.js";
5
+ export { SAMPLE_RATE, randomVoice, synthesiseUtterance } from "./synth.js";
6
+ export type { Voice } from "./synth.js";
7
+ export { encodeWav, toDataUri } from "./encode.js";
8
+ export { addNoiseBed } from "./distort.js";
9
+ export { rms } from "./levels.js";
10
+ export type { AudioBuffer, AudioRenderSettings, Excitation, Phoneme, RenderedAudioChallenge, Utterance, } from "./types.js";
11
+ export declare const DEFAULT_RENDER_SETTINGS: AudioRenderSettings;
12
+ export declare const renderAudioChallenge: (settings?: AudioRenderSettings, sampleRate?: number) => RenderedAudioChallenge;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAEX,mBAAmB,EACnB,sBAAsB,EAEtB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAChE,YAAY,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAC3E,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,YAAY,EACX,WAAW,EACX,mBAAmB,EACnB,UAAU,EACV,OAAO,EACP,sBAAsB,EACtB,SAAS,GACT,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,uBAAuB,EAAE,mBAOrC,CAAC;AAuDF,eAAO,MAAM,oBAAoB,cACtB,mBAAmB,eACjB,MAAM,KAChB,sBA6DF,CAAC"}