@waveform-playlist/worklets 10.0.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/LICENSE.md +21 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +11 -0
- package/dist/index.mjs.map +1 -0
- package/dist/worklet/meter-processor.worklet.js +62 -0
- package/dist/worklet/meter-processor.worklet.js.map +1 -0
- package/dist/worklet/meter-processor.worklet.mjs +60 -0
- package/dist/worklet/meter-processor.worklet.mjs.map +1 -0
- package/dist/worklet/recording-processor.worklet.js +76 -0
- package/dist/worklet/recording-processor.worklet.js.map +1 -0
- package/dist/worklet/recording-processor.worklet.mjs +74 -0
- package/dist/worklet/recording-processor.worklet.mjs.map +1 -0
- package/package.json +50 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 Naomi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const meterProcessorUrl: string;
|
|
2
|
+
declare const recordingProcessorUrl: string;
|
|
3
|
+
/** Message shape posted by the meter-processor worklet */
|
|
4
|
+
interface MeterMessage {
|
|
5
|
+
peak: number[];
|
|
6
|
+
rms: number[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { type MeterMessage, meterProcessorUrl, recordingProcessorUrl };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const meterProcessorUrl: string;
|
|
2
|
+
declare const recordingProcessorUrl: string;
|
|
3
|
+
/** Message shape posted by the meter-processor worklet */
|
|
4
|
+
interface MeterMessage {
|
|
5
|
+
peak: number[];
|
|
6
|
+
rms: number[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { type MeterMessage, meterProcessorUrl, recordingProcessorUrl };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
meterProcessorUrl: () => meterProcessorUrl,
|
|
24
|
+
recordingProcessorUrl: () => recordingProcessorUrl
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(src_exports);
|
|
27
|
+
var import_meta = {};
|
|
28
|
+
var meterProcessorUrl = new URL("./worklet/meter-processor.worklet.js", import_meta.url).href;
|
|
29
|
+
var recordingProcessorUrl = new URL(
|
|
30
|
+
"./worklet/recording-processor.worklet.js",
|
|
31
|
+
import_meta.url
|
|
32
|
+
).href;
|
|
33
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
34
|
+
0 && (module.exports = {
|
|
35
|
+
meterProcessorUrl,
|
|
36
|
+
recordingProcessorUrl
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const meterProcessorUrl = new URL('./worklet/meter-processor.worklet.js', import.meta.url)\n .href;\n\nexport const recordingProcessorUrl = new URL(\n './worklet/recording-processor.worklet.js',\n import.meta.url\n).href;\n\n/** Message shape posted by the meter-processor worklet */\nexport interface MeterMessage {\n peak: number[];\n rms: number[];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,oBAAoB,IAAI,IAAI,wCAAwC,YAAY,GAAG,EAC7F;AAEI,IAAM,wBAAwB,IAAI;AAAA,EACvC;AAAA,EACA,YAAY;AACd,EAAE;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var meterProcessorUrl = new URL("./worklet/meter-processor.worklet.js", import.meta.url).href;
|
|
3
|
+
var recordingProcessorUrl = new URL(
|
|
4
|
+
"./worklet/recording-processor.worklet.js",
|
|
5
|
+
import.meta.url
|
|
6
|
+
).href;
|
|
7
|
+
export {
|
|
8
|
+
meterProcessorUrl,
|
|
9
|
+
recordingProcessorUrl
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const meterProcessorUrl = new URL('./worklet/meter-processor.worklet.js', import.meta.url)\n .href;\n\nexport const recordingProcessorUrl = new URL(\n './worklet/recording-processor.worklet.js',\n import.meta.url\n).href;\n\n/** Message shape posted by the meter-processor worklet */\nexport interface MeterMessage {\n peak: number[];\n rms: number[];\n}\n"],"mappings":";AAAO,IAAM,oBAAoB,IAAI,IAAI,wCAAwC,YAAY,GAAG,EAC7F;AAEI,IAAM,wBAAwB,IAAI;AAAA,EACvC;AAAA,EACA,YAAY;AACd,EAAE;","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// src/worklet/meter-processor.worklet.ts
|
|
4
|
+
var MeterProcessor = class extends AudioWorkletProcessor {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
super();
|
|
7
|
+
const { numberOfChannels, updateRate } = options.processorOptions;
|
|
8
|
+
this.numberOfChannels = numberOfChannels;
|
|
9
|
+
this.blocksPerUpdate = Math.max(1, Math.floor(sampleRate / (128 * updateRate)));
|
|
10
|
+
this.blocksProcessed = 0;
|
|
11
|
+
this.maxPeak = new Array(numberOfChannels).fill(0);
|
|
12
|
+
this.sumSquares = new Array(numberOfChannels).fill(0);
|
|
13
|
+
this.sampleCount = new Array(numberOfChannels).fill(0);
|
|
14
|
+
}
|
|
15
|
+
process(inputs, outputs, _parameters) {
|
|
16
|
+
const input = inputs[0];
|
|
17
|
+
const output = outputs[0];
|
|
18
|
+
if (!input || input.length === 0) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
for (let ch = 0; ch < output.length; ch++) {
|
|
22
|
+
const inputChannel = input[ch];
|
|
23
|
+
const outputChannel = output[ch];
|
|
24
|
+
if (inputChannel && outputChannel) {
|
|
25
|
+
outputChannel.set(inputChannel);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (let ch = 0; ch < this.numberOfChannels; ch++) {
|
|
29
|
+
const inputChannel = input[ch];
|
|
30
|
+
if (!inputChannel) continue;
|
|
31
|
+
let peak = this.maxPeak[ch];
|
|
32
|
+
let sum = this.sumSquares[ch];
|
|
33
|
+
for (let i = 0; i < inputChannel.length; i++) {
|
|
34
|
+
const sample = inputChannel[i];
|
|
35
|
+
const abs = Math.abs(sample);
|
|
36
|
+
if (abs > peak) peak = abs;
|
|
37
|
+
sum += sample * sample;
|
|
38
|
+
}
|
|
39
|
+
this.maxPeak[ch] = peak;
|
|
40
|
+
this.sumSquares[ch] = sum;
|
|
41
|
+
this.sampleCount[ch] += inputChannel.length;
|
|
42
|
+
}
|
|
43
|
+
this.blocksProcessed++;
|
|
44
|
+
if (this.blocksProcessed >= this.blocksPerUpdate) {
|
|
45
|
+
const peak = [];
|
|
46
|
+
const rms = [];
|
|
47
|
+
for (let ch = 0; ch < this.numberOfChannels; ch++) {
|
|
48
|
+
peak.push(this.maxPeak[ch]);
|
|
49
|
+
const count = this.sampleCount[ch];
|
|
50
|
+
rms.push(count > 0 ? Math.sqrt(this.sumSquares[ch] / count) : 0);
|
|
51
|
+
}
|
|
52
|
+
this.port.postMessage({ peak, rms });
|
|
53
|
+
this.maxPeak.fill(0);
|
|
54
|
+
this.sumSquares.fill(0);
|
|
55
|
+
this.sampleCount.fill(0);
|
|
56
|
+
this.blocksProcessed = 0;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
registerProcessor("meter-processor", MeterProcessor);
|
|
62
|
+
//# sourceMappingURL=meter-processor.worklet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/worklet/meter-processor.worklet.ts"],"sourcesContent":["/**\n * MeterProcessor — AudioWorklet processor for sample-accurate peak/RMS metering\n *\n * Pass-through node: audio flows through unchanged while levels are computed.\n * Accumulates peak (max absolute sample) and RMS (root mean square) across all\n * 128-sample quantums, posting results at ~updateRate Hz via postMessage.\n *\n * RMS Strategy: Simple interval average (not sliding window).\n * Trade-off: A sliding window (like openDAW's 100ms circular buffer) provides\n * smoother loudness display. Our interval-based approach may appear jumpier\n * since each update only reflects ~16ms of audio. For visual metering at 60fps\n * the difference is subtle. A circular buffer can be added later without\n * changing the message format or hook API.\n */\n\ndeclare const sampleRate: number;\n\ninterface AudioParamDescriptor {\n name: string;\n defaultValue?: number;\n minValue?: number;\n maxValue?: number;\n automationRate?: 'a-rate' | 'k-rate';\n}\n\ndeclare class AudioWorkletProcessor {\n readonly port: MessagePort;\n process(\n inputs: Float32Array[][],\n outputs: Float32Array[][],\n parameters: Record<string, Float32Array>\n ): boolean;\n}\ndeclare function registerProcessor(\n name: string,\n processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & {\n parameterDescriptors?: AudioParamDescriptor[];\n }\n): void;\n\ninterface MeterProcessorOptions {\n numberOfChannels: number;\n updateRate: number;\n}\n\nclass MeterProcessor extends AudioWorkletProcessor {\n private numberOfChannels: number;\n private blocksPerUpdate: number;\n private blocksProcessed: number;\n private maxPeak: number[];\n private sumSquares: number[];\n private sampleCount: number[];\n\n constructor(options: { processorOptions: MeterProcessorOptions } & AudioWorkletNodeOptions) {\n super();\n const { numberOfChannels, updateRate } = options.processorOptions;\n this.numberOfChannels = numberOfChannels;\n this.blocksPerUpdate = Math.max(1, Math.floor(sampleRate / (128 * updateRate)));\n this.blocksProcessed = 0;\n this.maxPeak = new Array(numberOfChannels).fill(0);\n this.sumSquares = new Array(numberOfChannels).fill(0);\n this.sampleCount = new Array(numberOfChannels).fill(0);\n }\n\n process(\n inputs: Float32Array[][],\n outputs: Float32Array[][],\n _parameters: Record<string, Float32Array>\n ): boolean {\n const input = inputs[0];\n const output = outputs[0];\n\n if (!input || input.length === 0) {\n return true;\n }\n\n for (let ch = 0; ch < output.length; ch++) {\n const inputChannel = input[ch];\n const outputChannel = output[ch];\n if (inputChannel && outputChannel) {\n outputChannel.set(inputChannel);\n }\n }\n\n for (let ch = 0; ch < this.numberOfChannels; ch++) {\n const inputChannel = input[ch];\n if (!inputChannel) continue;\n\n let peak = this.maxPeak[ch];\n let sum = this.sumSquares[ch];\n\n for (let i = 0; i < inputChannel.length; i++) {\n const sample = inputChannel[i];\n const abs = Math.abs(sample);\n if (abs > peak) peak = abs;\n sum += sample * sample;\n }\n\n this.maxPeak[ch] = peak;\n this.sumSquares[ch] = sum;\n this.sampleCount[ch] += inputChannel.length;\n }\n\n this.blocksProcessed++;\n\n if (this.blocksProcessed >= this.blocksPerUpdate) {\n const peak: number[] = [];\n const rms: number[] = [];\n\n for (let ch = 0; ch < this.numberOfChannels; ch++) {\n peak.push(this.maxPeak[ch]);\n const count = this.sampleCount[ch];\n rms.push(count > 0 ? Math.sqrt(this.sumSquares[ch] / count) : 0);\n }\n\n this.port.postMessage({ peak, rms });\n\n this.maxPeak.fill(0);\n this.sumSquares.fill(0);\n this.sampleCount.fill(0);\n this.blocksProcessed = 0;\n }\n\n return true;\n }\n}\n\nregisterProcessor('meter-processor', MeterProcessor);\n"],"mappings":";;;AA6CA,IAAM,iBAAN,cAA6B,sBAAsB;AAAA,EAQjD,YAAY,SAAgF;AAC1F,UAAM;AACN,UAAM,EAAE,kBAAkB,WAAW,IAAI,QAAQ;AACjD,SAAK,mBAAmB;AACxB,SAAK,kBAAkB,KAAK,IAAI,GAAG,KAAK,MAAM,cAAc,MAAM,WAAW,CAAC;AAC9E,SAAK,kBAAkB;AACvB,SAAK,UAAU,IAAI,MAAM,gBAAgB,EAAE,KAAK,CAAC;AACjD,SAAK,aAAa,IAAI,MAAM,gBAAgB,EAAE,KAAK,CAAC;AACpD,SAAK,cAAc,IAAI,MAAM,gBAAgB,EAAE,KAAK,CAAC;AAAA,EACvD;AAAA,EAEA,QACE,QACA,SACA,aACS;AACT,UAAM,QAAQ,OAAO,CAAC;AACtB,UAAM,SAAS,QAAQ,CAAC;AAExB,QAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,aAAO;AAAA,IACT;AAEA,aAAS,KAAK,GAAG,KAAK,OAAO,QAAQ,MAAM;AACzC,YAAM,eAAe,MAAM,EAAE;AAC7B,YAAM,gBAAgB,OAAO,EAAE;AAC/B,UAAI,gBAAgB,eAAe;AACjC,sBAAc,IAAI,YAAY;AAAA,MAChC;AAAA,IACF;AAEA,aAAS,KAAK,GAAG,KAAK,KAAK,kBAAkB,MAAM;AACjD,YAAM,eAAe,MAAM,EAAE;AAC7B,UAAI,CAAC,aAAc;AAEnB,UAAI,OAAO,KAAK,QAAQ,EAAE;AAC1B,UAAI,MAAM,KAAK,WAAW,EAAE;AAE5B,eAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,cAAM,SAAS,aAAa,CAAC;AAC7B,cAAM,MAAM,KAAK,IAAI,MAAM;AAC3B,YAAI,MAAM,KAAM,QAAO;AACvB,eAAO,SAAS;AAAA,MAClB;AAEA,WAAK,QAAQ,EAAE,IAAI;AACnB,WAAK,WAAW,EAAE,IAAI;AACtB,WAAK,YAAY,EAAE,KAAK,aAAa;AAAA,IACvC;AAEA,SAAK;AAEL,QAAI,KAAK,mBAAmB,KAAK,iBAAiB;AAChD,YAAM,OAAiB,CAAC;AACxB,YAAM,MAAgB,CAAC;AAEvB,eAAS,KAAK,GAAG,KAAK,KAAK,kBAAkB,MAAM;AACjD,aAAK,KAAK,KAAK,QAAQ,EAAE,CAAC;AAC1B,cAAM,QAAQ,KAAK,YAAY,EAAE;AACjC,YAAI,KAAK,QAAQ,IAAI,KAAK,KAAK,KAAK,WAAW,EAAE,IAAI,KAAK,IAAI,CAAC;AAAA,MACjE;AAEA,WAAK,KAAK,YAAY,EAAE,MAAM,IAAI,CAAC;AAEnC,WAAK,QAAQ,KAAK,CAAC;AACnB,WAAK,WAAW,KAAK,CAAC;AACtB,WAAK,YAAY,KAAK,CAAC;AACvB,WAAK,kBAAkB;AAAA,IACzB;AAEA,WAAO;AAAA,EACT;AACF;AAEA,kBAAkB,mBAAmB,cAAc;","names":[]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/worklet/meter-processor.worklet.ts
|
|
2
|
+
var MeterProcessor = class extends AudioWorkletProcessor {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
super();
|
|
5
|
+
const { numberOfChannels, updateRate } = options.processorOptions;
|
|
6
|
+
this.numberOfChannels = numberOfChannels;
|
|
7
|
+
this.blocksPerUpdate = Math.max(1, Math.floor(sampleRate / (128 * updateRate)));
|
|
8
|
+
this.blocksProcessed = 0;
|
|
9
|
+
this.maxPeak = new Array(numberOfChannels).fill(0);
|
|
10
|
+
this.sumSquares = new Array(numberOfChannels).fill(0);
|
|
11
|
+
this.sampleCount = new Array(numberOfChannels).fill(0);
|
|
12
|
+
}
|
|
13
|
+
process(inputs, outputs, _parameters) {
|
|
14
|
+
const input = inputs[0];
|
|
15
|
+
const output = outputs[0];
|
|
16
|
+
if (!input || input.length === 0) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
for (let ch = 0; ch < output.length; ch++) {
|
|
20
|
+
const inputChannel = input[ch];
|
|
21
|
+
const outputChannel = output[ch];
|
|
22
|
+
if (inputChannel && outputChannel) {
|
|
23
|
+
outputChannel.set(inputChannel);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (let ch = 0; ch < this.numberOfChannels; ch++) {
|
|
27
|
+
const inputChannel = input[ch];
|
|
28
|
+
if (!inputChannel) continue;
|
|
29
|
+
let peak = this.maxPeak[ch];
|
|
30
|
+
let sum = this.sumSquares[ch];
|
|
31
|
+
for (let i = 0; i < inputChannel.length; i++) {
|
|
32
|
+
const sample = inputChannel[i];
|
|
33
|
+
const abs = Math.abs(sample);
|
|
34
|
+
if (abs > peak) peak = abs;
|
|
35
|
+
sum += sample * sample;
|
|
36
|
+
}
|
|
37
|
+
this.maxPeak[ch] = peak;
|
|
38
|
+
this.sumSquares[ch] = sum;
|
|
39
|
+
this.sampleCount[ch] += inputChannel.length;
|
|
40
|
+
}
|
|
41
|
+
this.blocksProcessed++;
|
|
42
|
+
if (this.blocksProcessed >= this.blocksPerUpdate) {
|
|
43
|
+
const peak = [];
|
|
44
|
+
const rms = [];
|
|
45
|
+
for (let ch = 0; ch < this.numberOfChannels; ch++) {
|
|
46
|
+
peak.push(this.maxPeak[ch]);
|
|
47
|
+
const count = this.sampleCount[ch];
|
|
48
|
+
rms.push(count > 0 ? Math.sqrt(this.sumSquares[ch] / count) : 0);
|
|
49
|
+
}
|
|
50
|
+
this.port.postMessage({ peak, rms });
|
|
51
|
+
this.maxPeak.fill(0);
|
|
52
|
+
this.sumSquares.fill(0);
|
|
53
|
+
this.sampleCount.fill(0);
|
|
54
|
+
this.blocksProcessed = 0;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
registerProcessor("meter-processor", MeterProcessor);
|
|
60
|
+
//# sourceMappingURL=meter-processor.worklet.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/worklet/meter-processor.worklet.ts"],"sourcesContent":["/**\n * MeterProcessor — AudioWorklet processor for sample-accurate peak/RMS metering\n *\n * Pass-through node: audio flows through unchanged while levels are computed.\n * Accumulates peak (max absolute sample) and RMS (root mean square) across all\n * 128-sample quantums, posting results at ~updateRate Hz via postMessage.\n *\n * RMS Strategy: Simple interval average (not sliding window).\n * Trade-off: A sliding window (like openDAW's 100ms circular buffer) provides\n * smoother loudness display. Our interval-based approach may appear jumpier\n * since each update only reflects ~16ms of audio. For visual metering at 60fps\n * the difference is subtle. A circular buffer can be added later without\n * changing the message format or hook API.\n */\n\ndeclare const sampleRate: number;\n\ninterface AudioParamDescriptor {\n name: string;\n defaultValue?: number;\n minValue?: number;\n maxValue?: number;\n automationRate?: 'a-rate' | 'k-rate';\n}\n\ndeclare class AudioWorkletProcessor {\n readonly port: MessagePort;\n process(\n inputs: Float32Array[][],\n outputs: Float32Array[][],\n parameters: Record<string, Float32Array>\n ): boolean;\n}\ndeclare function registerProcessor(\n name: string,\n processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & {\n parameterDescriptors?: AudioParamDescriptor[];\n }\n): void;\n\ninterface MeterProcessorOptions {\n numberOfChannels: number;\n updateRate: number;\n}\n\nclass MeterProcessor extends AudioWorkletProcessor {\n private numberOfChannels: number;\n private blocksPerUpdate: number;\n private blocksProcessed: number;\n private maxPeak: number[];\n private sumSquares: number[];\n private sampleCount: number[];\n\n constructor(options: { processorOptions: MeterProcessorOptions } & AudioWorkletNodeOptions) {\n super();\n const { numberOfChannels, updateRate } = options.processorOptions;\n this.numberOfChannels = numberOfChannels;\n this.blocksPerUpdate = Math.max(1, Math.floor(sampleRate / (128 * updateRate)));\n this.blocksProcessed = 0;\n this.maxPeak = new Array(numberOfChannels).fill(0);\n this.sumSquares = new Array(numberOfChannels).fill(0);\n this.sampleCount = new Array(numberOfChannels).fill(0);\n }\n\n process(\n inputs: Float32Array[][],\n outputs: Float32Array[][],\n _parameters: Record<string, Float32Array>\n ): boolean {\n const input = inputs[0];\n const output = outputs[0];\n\n if (!input || input.length === 0) {\n return true;\n }\n\n for (let ch = 0; ch < output.length; ch++) {\n const inputChannel = input[ch];\n const outputChannel = output[ch];\n if (inputChannel && outputChannel) {\n outputChannel.set(inputChannel);\n }\n }\n\n for (let ch = 0; ch < this.numberOfChannels; ch++) {\n const inputChannel = input[ch];\n if (!inputChannel) continue;\n\n let peak = this.maxPeak[ch];\n let sum = this.sumSquares[ch];\n\n for (let i = 0; i < inputChannel.length; i++) {\n const sample = inputChannel[i];\n const abs = Math.abs(sample);\n if (abs > peak) peak = abs;\n sum += sample * sample;\n }\n\n this.maxPeak[ch] = peak;\n this.sumSquares[ch] = sum;\n this.sampleCount[ch] += inputChannel.length;\n }\n\n this.blocksProcessed++;\n\n if (this.blocksProcessed >= this.blocksPerUpdate) {\n const peak: number[] = [];\n const rms: number[] = [];\n\n for (let ch = 0; ch < this.numberOfChannels; ch++) {\n peak.push(this.maxPeak[ch]);\n const count = this.sampleCount[ch];\n rms.push(count > 0 ? Math.sqrt(this.sumSquares[ch] / count) : 0);\n }\n\n this.port.postMessage({ peak, rms });\n\n this.maxPeak.fill(0);\n this.sumSquares.fill(0);\n this.sampleCount.fill(0);\n this.blocksProcessed = 0;\n }\n\n return true;\n }\n}\n\nregisterProcessor('meter-processor', MeterProcessor);\n"],"mappings":";AA6CA,IAAM,iBAAN,cAA6B,sBAAsB;AAAA,EAQjD,YAAY,SAAgF;AAC1F,UAAM;AACN,UAAM,EAAE,kBAAkB,WAAW,IAAI,QAAQ;AACjD,SAAK,mBAAmB;AACxB,SAAK,kBAAkB,KAAK,IAAI,GAAG,KAAK,MAAM,cAAc,MAAM,WAAW,CAAC;AAC9E,SAAK,kBAAkB;AACvB,SAAK,UAAU,IAAI,MAAM,gBAAgB,EAAE,KAAK,CAAC;AACjD,SAAK,aAAa,IAAI,MAAM,gBAAgB,EAAE,KAAK,CAAC;AACpD,SAAK,cAAc,IAAI,MAAM,gBAAgB,EAAE,KAAK,CAAC;AAAA,EACvD;AAAA,EAEA,QACE,QACA,SACA,aACS;AACT,UAAM,QAAQ,OAAO,CAAC;AACtB,UAAM,SAAS,QAAQ,CAAC;AAExB,QAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,aAAO;AAAA,IACT;AAEA,aAAS,KAAK,GAAG,KAAK,OAAO,QAAQ,MAAM;AACzC,YAAM,eAAe,MAAM,EAAE;AAC7B,YAAM,gBAAgB,OAAO,EAAE;AAC/B,UAAI,gBAAgB,eAAe;AACjC,sBAAc,IAAI,YAAY;AAAA,MAChC;AAAA,IACF;AAEA,aAAS,KAAK,GAAG,KAAK,KAAK,kBAAkB,MAAM;AACjD,YAAM,eAAe,MAAM,EAAE;AAC7B,UAAI,CAAC,aAAc;AAEnB,UAAI,OAAO,KAAK,QAAQ,EAAE;AAC1B,UAAI,MAAM,KAAK,WAAW,EAAE;AAE5B,eAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,cAAM,SAAS,aAAa,CAAC;AAC7B,cAAM,MAAM,KAAK,IAAI,MAAM;AAC3B,YAAI,MAAM,KAAM,QAAO;AACvB,eAAO,SAAS;AAAA,MAClB;AAEA,WAAK,QAAQ,EAAE,IAAI;AACnB,WAAK,WAAW,EAAE,IAAI;AACtB,WAAK,YAAY,EAAE,KAAK,aAAa;AAAA,IACvC;AAEA,SAAK;AAEL,QAAI,KAAK,mBAAmB,KAAK,iBAAiB;AAChD,YAAM,OAAiB,CAAC;AACxB,YAAM,MAAgB,CAAC;AAEvB,eAAS,KAAK,GAAG,KAAK,KAAK,kBAAkB,MAAM;AACjD,aAAK,KAAK,KAAK,QAAQ,EAAE,CAAC;AAC1B,cAAM,QAAQ,KAAK,YAAY,EAAE;AACjC,YAAI,KAAK,QAAQ,IAAI,KAAK,KAAK,KAAK,WAAW,EAAE,IAAI,KAAK,IAAI,CAAC;AAAA,MACjE;AAEA,WAAK,KAAK,YAAY,EAAE,MAAM,IAAI,CAAC;AAEnC,WAAK,QAAQ,KAAK,CAAC;AACnB,WAAK,WAAW,KAAK,CAAC;AACtB,WAAK,YAAY,KAAK,CAAC;AACvB,WAAK,kBAAkB;AAAA,IACzB;AAEA,WAAO;AAAA,EACT;AACF;AAEA,kBAAkB,mBAAmB,cAAc;","names":[]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// src/worklet/recording-processor.worklet.ts
|
|
4
|
+
var RecordingProcessor = class extends AudioWorkletProcessor {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
this.bufferSize = 0;
|
|
8
|
+
this.buffers = [];
|
|
9
|
+
this.samplesCollected = 0;
|
|
10
|
+
this.isRecording = false;
|
|
11
|
+
this.channelCount = 1;
|
|
12
|
+
this.port.onmessage = (event) => {
|
|
13
|
+
const { command, sampleRate: sampleRate2, channelCount } = event.data;
|
|
14
|
+
if (command === "start") {
|
|
15
|
+
this.isRecording = true;
|
|
16
|
+
this.channelCount = channelCount || 1;
|
|
17
|
+
this.bufferSize = Math.floor((sampleRate2 || 48e3) * 0.016);
|
|
18
|
+
this.buffers = [];
|
|
19
|
+
for (let i = 0; i < this.channelCount; i++) {
|
|
20
|
+
this.buffers[i] = new Float32Array(this.bufferSize);
|
|
21
|
+
}
|
|
22
|
+
this.samplesCollected = 0;
|
|
23
|
+
} else if (command === "stop") {
|
|
24
|
+
this.isRecording = false;
|
|
25
|
+
if (this.samplesCollected > 0) {
|
|
26
|
+
this.flushBuffers();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
process(inputs, _outputs, _parameters) {
|
|
32
|
+
if (!this.isRecording) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const input = inputs[0];
|
|
36
|
+
if (!input || input.length === 0) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const frameCount = input[0].length;
|
|
40
|
+
if (this.bufferSize <= 0) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
let offset = 0;
|
|
44
|
+
while (offset < frameCount) {
|
|
45
|
+
const remaining = this.bufferSize - this.samplesCollected;
|
|
46
|
+
const toCopy = Math.min(remaining, frameCount - offset);
|
|
47
|
+
for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {
|
|
48
|
+
const inputChannel = input[channel];
|
|
49
|
+
const buffer = this.buffers[channel];
|
|
50
|
+
for (let i = 0; i < toCopy; i++) {
|
|
51
|
+
buffer[this.samplesCollected + i] = inputChannel[offset + i];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this.samplesCollected += toCopy;
|
|
55
|
+
offset += toCopy;
|
|
56
|
+
if (this.samplesCollected >= this.bufferSize) {
|
|
57
|
+
this.flushBuffers();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
flushBuffers() {
|
|
63
|
+
const channels = [];
|
|
64
|
+
for (let i = 0; i < this.channelCount; i++) {
|
|
65
|
+
channels.push(this.buffers[i].slice(0, this.samplesCollected));
|
|
66
|
+
}
|
|
67
|
+
this.port.postMessage({
|
|
68
|
+
channels,
|
|
69
|
+
sampleRate,
|
|
70
|
+
channelCount: this.channelCount
|
|
71
|
+
});
|
|
72
|
+
this.samplesCollected = 0;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
registerProcessor("recording-processor", RecordingProcessor);
|
|
76
|
+
//# sourceMappingURL=recording-processor.worklet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/worklet/recording-processor.worklet.ts"],"sourcesContent":["/**\n * RecordingProcessor - AudioWorklet processor for capturing raw audio data\n *\n * This processor runs in the AudioWorklet thread and captures audio samples\n * at the browser's native sample rate. It buffers samples and sends them to\n * the main thread at regular intervals (~16ms) for peak generation and\n * waveform visualization.\n *\n * Message Format (to main thread):\n * {\n * channels: Float32Array[], // Per-channel audio samples for this chunk\n * sampleRate: number, // Sample rate of the audio\n * channelCount: number // Number of channels\n * }\n *\n * Note: VU meter levels are handled by the meter-processor worklet in\n * useMicrophoneLevel hook, not by this worklet.\n */\n\n// Type declarations for AudioWorklet context\ndeclare const sampleRate: number;\n\ninterface AudioParamDescriptor {\n name: string;\n defaultValue?: number;\n minValue?: number;\n maxValue?: number;\n automationRate?: 'a-rate' | 'k-rate';\n}\n\ndeclare class AudioWorkletProcessor {\n readonly port: MessagePort;\n process(\n inputs: Float32Array[][],\n outputs: Float32Array[][],\n parameters: Record<string, Float32Array>\n ): boolean;\n}\ndeclare function registerProcessor(\n name: string,\n processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & {\n parameterDescriptors?: AudioParamDescriptor[];\n }\n): void;\n\ninterface RecordingProcessorMessage {\n channels: Float32Array[];\n sampleRate: number;\n channelCount: number;\n}\n\nclass RecordingProcessor extends AudioWorkletProcessor {\n private buffers: Float32Array[];\n private bufferSize: number;\n private samplesCollected: number;\n private isRecording: boolean;\n private channelCount: number;\n\n constructor() {\n super();\n\n // Buffer size for ~16ms at 48kHz (approximately one animation frame)\n // This will be adjusted based on actual sample rate\n this.bufferSize = 0;\n this.buffers = [];\n this.samplesCollected = 0;\n this.isRecording = false;\n this.channelCount = 1;\n\n // Listen for control messages from main thread\n this.port.onmessage = (event) => {\n const { command, sampleRate, channelCount } = event.data;\n\n if (command === 'start') {\n this.isRecording = true;\n this.channelCount = channelCount || 1;\n\n // Calculate buffer size for ~16ms chunks (60 fps)\n // At 48kHz: 48000 * 0.016 = 768 samples\n this.bufferSize = Math.floor((sampleRate || 48000) * 0.016);\n\n // Initialize buffers for each channel\n this.buffers = [];\n for (let i = 0; i < this.channelCount; i++) {\n this.buffers[i] = new Float32Array(this.bufferSize);\n }\n this.samplesCollected = 0;\n } else if (command === 'stop') {\n this.isRecording = false;\n\n // Send any remaining buffered samples\n if (this.samplesCollected > 0) {\n this.flushBuffers();\n }\n }\n };\n }\n\n process(\n inputs: Float32Array[][],\n _outputs: Float32Array[][],\n _parameters: Record<string, Float32Array>\n ): boolean {\n if (!this.isRecording) {\n return true; // Keep processor alive\n }\n\n const input = inputs[0];\n if (!input || input.length === 0) {\n return true; // No input yet, keep alive\n }\n\n const frameCount = input[0].length;\n\n if (this.bufferSize <= 0) {\n return true; // Not yet configured via 'start' command\n }\n\n let offset = 0;\n\n // Process samples in chunks that fit within the buffer.\n // The AudioWorklet quantum (128 samples) may not divide evenly into\n // bufferSize (e.g., 705 at 44100Hz), so a single frame can cross\n // the buffer boundary. Without this loop, samples beyond bufferSize\n // are silently dropped by the typed array, causing audio gaps.\n while (offset < frameCount) {\n const remaining = this.bufferSize - this.samplesCollected;\n const toCopy = Math.min(remaining, frameCount - offset);\n\n for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {\n const inputChannel = input[channel];\n const buffer = this.buffers[channel];\n\n for (let i = 0; i < toCopy; i++) {\n buffer[this.samplesCollected + i] = inputChannel[offset + i];\n }\n }\n\n this.samplesCollected += toCopy;\n offset += toCopy;\n\n // When buffer is full, send to main thread\n if (this.samplesCollected >= this.bufferSize) {\n this.flushBuffers();\n }\n }\n\n return true; // Keep processor alive\n }\n\n private flushBuffers(): void {\n // Send all channel buffers to main thread\n const channels: Float32Array[] = [];\n for (let i = 0; i < this.channelCount; i++) {\n channels.push(this.buffers[i].slice(0, this.samplesCollected));\n }\n\n this.port.postMessage({\n channels,\n sampleRate: sampleRate,\n channelCount: this.channelCount,\n } as RecordingProcessorMessage);\n\n // Reset buffer\n this.samplesCollected = 0;\n }\n}\n\n// Register the processor\nregisterProcessor('recording-processor', RecordingProcessor);\n"],"mappings":";;;AAmDA,IAAM,qBAAN,cAAiC,sBAAsB;AAAA,EAOrD,cAAc;AACZ,UAAM;AAIN,SAAK,aAAa;AAClB,SAAK,UAAU,CAAC;AAChB,SAAK,mBAAmB;AACxB,SAAK,cAAc;AACnB,SAAK,eAAe;AAGpB,SAAK,KAAK,YAAY,CAAC,UAAU;AAC/B,YAAM,EAAE,SAAS,YAAAA,aAAY,aAAa,IAAI,MAAM;AAEpD,UAAI,YAAY,SAAS;AACvB,aAAK,cAAc;AACnB,aAAK,eAAe,gBAAgB;AAIpC,aAAK,aAAa,KAAK,OAAOA,eAAc,QAAS,KAAK;AAG1D,aAAK,UAAU,CAAC;AAChB,iBAAS,IAAI,GAAG,IAAI,KAAK,cAAc,KAAK;AAC1C,eAAK,QAAQ,CAAC,IAAI,IAAI,aAAa,KAAK,UAAU;AAAA,QACpD;AACA,aAAK,mBAAmB;AAAA,MAC1B,WAAW,YAAY,QAAQ;AAC7B,aAAK,cAAc;AAGnB,YAAI,KAAK,mBAAmB,GAAG;AAC7B,eAAK,aAAa;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,QACE,QACA,UACA,aACS;AACT,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,OAAO,CAAC;AACtB,QAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,MAAM,CAAC,EAAE;AAE5B,QAAI,KAAK,cAAc,GAAG;AACxB,aAAO;AAAA,IACT;AAEA,QAAI,SAAS;AAOb,WAAO,SAAS,YAAY;AAC1B,YAAM,YAAY,KAAK,aAAa,KAAK;AACzC,YAAM,SAAS,KAAK,IAAI,WAAW,aAAa,MAAM;AAEtD,eAAS,UAAU,GAAG,UAAU,KAAK,IAAI,MAAM,QAAQ,KAAK,YAAY,GAAG,WAAW;AACpF,cAAM,eAAe,MAAM,OAAO;AAClC,cAAM,SAAS,KAAK,QAAQ,OAAO;AAEnC,iBAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,iBAAO,KAAK,mBAAmB,CAAC,IAAI,aAAa,SAAS,CAAC;AAAA,QAC7D;AAAA,MACF;AAEA,WAAK,oBAAoB;AACzB,gBAAU;AAGV,UAAI,KAAK,oBAAoB,KAAK,YAAY;AAC5C,aAAK,aAAa;AAAA,MACpB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAqB;AAE3B,UAAM,WAA2B,CAAC;AAClC,aAAS,IAAI,GAAG,IAAI,KAAK,cAAc,KAAK;AAC1C,eAAS,KAAK,KAAK,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,gBAAgB,CAAC;AAAA,IAC/D;AAEA,SAAK,KAAK,YAAY;AAAA,MACpB;AAAA,MACA;AAAA,MACA,cAAc,KAAK;AAAA,IACrB,CAA8B;AAG9B,SAAK,mBAAmB;AAAA,EAC1B;AACF;AAGA,kBAAkB,uBAAuB,kBAAkB;","names":["sampleRate"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/worklet/recording-processor.worklet.ts
|
|
2
|
+
var RecordingProcessor = class extends AudioWorkletProcessor {
|
|
3
|
+
constructor() {
|
|
4
|
+
super();
|
|
5
|
+
this.bufferSize = 0;
|
|
6
|
+
this.buffers = [];
|
|
7
|
+
this.samplesCollected = 0;
|
|
8
|
+
this.isRecording = false;
|
|
9
|
+
this.channelCount = 1;
|
|
10
|
+
this.port.onmessage = (event) => {
|
|
11
|
+
const { command, sampleRate: sampleRate2, channelCount } = event.data;
|
|
12
|
+
if (command === "start") {
|
|
13
|
+
this.isRecording = true;
|
|
14
|
+
this.channelCount = channelCount || 1;
|
|
15
|
+
this.bufferSize = Math.floor((sampleRate2 || 48e3) * 0.016);
|
|
16
|
+
this.buffers = [];
|
|
17
|
+
for (let i = 0; i < this.channelCount; i++) {
|
|
18
|
+
this.buffers[i] = new Float32Array(this.bufferSize);
|
|
19
|
+
}
|
|
20
|
+
this.samplesCollected = 0;
|
|
21
|
+
} else if (command === "stop") {
|
|
22
|
+
this.isRecording = false;
|
|
23
|
+
if (this.samplesCollected > 0) {
|
|
24
|
+
this.flushBuffers();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
process(inputs, _outputs, _parameters) {
|
|
30
|
+
if (!this.isRecording) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const input = inputs[0];
|
|
34
|
+
if (!input || input.length === 0) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const frameCount = input[0].length;
|
|
38
|
+
if (this.bufferSize <= 0) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
let offset = 0;
|
|
42
|
+
while (offset < frameCount) {
|
|
43
|
+
const remaining = this.bufferSize - this.samplesCollected;
|
|
44
|
+
const toCopy = Math.min(remaining, frameCount - offset);
|
|
45
|
+
for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {
|
|
46
|
+
const inputChannel = input[channel];
|
|
47
|
+
const buffer = this.buffers[channel];
|
|
48
|
+
for (let i = 0; i < toCopy; i++) {
|
|
49
|
+
buffer[this.samplesCollected + i] = inputChannel[offset + i];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.samplesCollected += toCopy;
|
|
53
|
+
offset += toCopy;
|
|
54
|
+
if (this.samplesCollected >= this.bufferSize) {
|
|
55
|
+
this.flushBuffers();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
flushBuffers() {
|
|
61
|
+
const channels = [];
|
|
62
|
+
for (let i = 0; i < this.channelCount; i++) {
|
|
63
|
+
channels.push(this.buffers[i].slice(0, this.samplesCollected));
|
|
64
|
+
}
|
|
65
|
+
this.port.postMessage({
|
|
66
|
+
channels,
|
|
67
|
+
sampleRate,
|
|
68
|
+
channelCount: this.channelCount
|
|
69
|
+
});
|
|
70
|
+
this.samplesCollected = 0;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
registerProcessor("recording-processor", RecordingProcessor);
|
|
74
|
+
//# sourceMappingURL=recording-processor.worklet.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/worklet/recording-processor.worklet.ts"],"sourcesContent":["/**\n * RecordingProcessor - AudioWorklet processor for capturing raw audio data\n *\n * This processor runs in the AudioWorklet thread and captures audio samples\n * at the browser's native sample rate. It buffers samples and sends them to\n * the main thread at regular intervals (~16ms) for peak generation and\n * waveform visualization.\n *\n * Message Format (to main thread):\n * {\n * channels: Float32Array[], // Per-channel audio samples for this chunk\n * sampleRate: number, // Sample rate of the audio\n * channelCount: number // Number of channels\n * }\n *\n * Note: VU meter levels are handled by the meter-processor worklet in\n * useMicrophoneLevel hook, not by this worklet.\n */\n\n// Type declarations for AudioWorklet context\ndeclare const sampleRate: number;\n\ninterface AudioParamDescriptor {\n name: string;\n defaultValue?: number;\n minValue?: number;\n maxValue?: number;\n automationRate?: 'a-rate' | 'k-rate';\n}\n\ndeclare class AudioWorkletProcessor {\n readonly port: MessagePort;\n process(\n inputs: Float32Array[][],\n outputs: Float32Array[][],\n parameters: Record<string, Float32Array>\n ): boolean;\n}\ndeclare function registerProcessor(\n name: string,\n processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & {\n parameterDescriptors?: AudioParamDescriptor[];\n }\n): void;\n\ninterface RecordingProcessorMessage {\n channels: Float32Array[];\n sampleRate: number;\n channelCount: number;\n}\n\nclass RecordingProcessor extends AudioWorkletProcessor {\n private buffers: Float32Array[];\n private bufferSize: number;\n private samplesCollected: number;\n private isRecording: boolean;\n private channelCount: number;\n\n constructor() {\n super();\n\n // Buffer size for ~16ms at 48kHz (approximately one animation frame)\n // This will be adjusted based on actual sample rate\n this.bufferSize = 0;\n this.buffers = [];\n this.samplesCollected = 0;\n this.isRecording = false;\n this.channelCount = 1;\n\n // Listen for control messages from main thread\n this.port.onmessage = (event) => {\n const { command, sampleRate, channelCount } = event.data;\n\n if (command === 'start') {\n this.isRecording = true;\n this.channelCount = channelCount || 1;\n\n // Calculate buffer size for ~16ms chunks (60 fps)\n // At 48kHz: 48000 * 0.016 = 768 samples\n this.bufferSize = Math.floor((sampleRate || 48000) * 0.016);\n\n // Initialize buffers for each channel\n this.buffers = [];\n for (let i = 0; i < this.channelCount; i++) {\n this.buffers[i] = new Float32Array(this.bufferSize);\n }\n this.samplesCollected = 0;\n } else if (command === 'stop') {\n this.isRecording = false;\n\n // Send any remaining buffered samples\n if (this.samplesCollected > 0) {\n this.flushBuffers();\n }\n }\n };\n }\n\n process(\n inputs: Float32Array[][],\n _outputs: Float32Array[][],\n _parameters: Record<string, Float32Array>\n ): boolean {\n if (!this.isRecording) {\n return true; // Keep processor alive\n }\n\n const input = inputs[0];\n if (!input || input.length === 0) {\n return true; // No input yet, keep alive\n }\n\n const frameCount = input[0].length;\n\n if (this.bufferSize <= 0) {\n return true; // Not yet configured via 'start' command\n }\n\n let offset = 0;\n\n // Process samples in chunks that fit within the buffer.\n // The AudioWorklet quantum (128 samples) may not divide evenly into\n // bufferSize (e.g., 705 at 44100Hz), so a single frame can cross\n // the buffer boundary. Without this loop, samples beyond bufferSize\n // are silently dropped by the typed array, causing audio gaps.\n while (offset < frameCount) {\n const remaining = this.bufferSize - this.samplesCollected;\n const toCopy = Math.min(remaining, frameCount - offset);\n\n for (let channel = 0; channel < Math.min(input.length, this.channelCount); channel++) {\n const inputChannel = input[channel];\n const buffer = this.buffers[channel];\n\n for (let i = 0; i < toCopy; i++) {\n buffer[this.samplesCollected + i] = inputChannel[offset + i];\n }\n }\n\n this.samplesCollected += toCopy;\n offset += toCopy;\n\n // When buffer is full, send to main thread\n if (this.samplesCollected >= this.bufferSize) {\n this.flushBuffers();\n }\n }\n\n return true; // Keep processor alive\n }\n\n private flushBuffers(): void {\n // Send all channel buffers to main thread\n const channels: Float32Array[] = [];\n for (let i = 0; i < this.channelCount; i++) {\n channels.push(this.buffers[i].slice(0, this.samplesCollected));\n }\n\n this.port.postMessage({\n channels,\n sampleRate: sampleRate,\n channelCount: this.channelCount,\n } as RecordingProcessorMessage);\n\n // Reset buffer\n this.samplesCollected = 0;\n }\n}\n\n// Register the processor\nregisterProcessor('recording-processor', RecordingProcessor);\n"],"mappings":";AAmDA,IAAM,qBAAN,cAAiC,sBAAsB;AAAA,EAOrD,cAAc;AACZ,UAAM;AAIN,SAAK,aAAa;AAClB,SAAK,UAAU,CAAC;AAChB,SAAK,mBAAmB;AACxB,SAAK,cAAc;AACnB,SAAK,eAAe;AAGpB,SAAK,KAAK,YAAY,CAAC,UAAU;AAC/B,YAAM,EAAE,SAAS,YAAAA,aAAY,aAAa,IAAI,MAAM;AAEpD,UAAI,YAAY,SAAS;AACvB,aAAK,cAAc;AACnB,aAAK,eAAe,gBAAgB;AAIpC,aAAK,aAAa,KAAK,OAAOA,eAAc,QAAS,KAAK;AAG1D,aAAK,UAAU,CAAC;AAChB,iBAAS,IAAI,GAAG,IAAI,KAAK,cAAc,KAAK;AAC1C,eAAK,QAAQ,CAAC,IAAI,IAAI,aAAa,KAAK,UAAU;AAAA,QACpD;AACA,aAAK,mBAAmB;AAAA,MAC1B,WAAW,YAAY,QAAQ;AAC7B,aAAK,cAAc;AAGnB,YAAI,KAAK,mBAAmB,GAAG;AAC7B,eAAK,aAAa;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,QACE,QACA,UACA,aACS;AACT,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,OAAO,CAAC;AACtB,QAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,MAAM,CAAC,EAAE;AAE5B,QAAI,KAAK,cAAc,GAAG;AACxB,aAAO;AAAA,IACT;AAEA,QAAI,SAAS;AAOb,WAAO,SAAS,YAAY;AAC1B,YAAM,YAAY,KAAK,aAAa,KAAK;AACzC,YAAM,SAAS,KAAK,IAAI,WAAW,aAAa,MAAM;AAEtD,eAAS,UAAU,GAAG,UAAU,KAAK,IAAI,MAAM,QAAQ,KAAK,YAAY,GAAG,WAAW;AACpF,cAAM,eAAe,MAAM,OAAO;AAClC,cAAM,SAAS,KAAK,QAAQ,OAAO;AAEnC,iBAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,iBAAO,KAAK,mBAAmB,CAAC,IAAI,aAAa,SAAS,CAAC;AAAA,QAC7D;AAAA,MACF;AAEA,WAAK,oBAAoB;AACzB,gBAAU;AAGV,UAAI,KAAK,oBAAoB,KAAK,YAAY;AAC5C,aAAK,aAAa;AAAA,MACpB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAqB;AAE3B,UAAM,WAA2B,CAAC;AAClC,aAAS,IAAI,GAAG,IAAI,KAAK,cAAc,KAAK;AAC1C,eAAS,KAAK,KAAK,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,gBAAgB,CAAC;AAAA,IAC/D;AAEA,SAAK,KAAK,YAAY;AAAA,MACpB;AAAA,MACA;AAAA,MACA,cAAc,KAAK;AAAA,IACrB,CAA8B;AAG9B,SAAK,mBAAmB;AAAA,EAC1B;AACF;AAGA,kBAAkB,uBAAuB,kBAAkB;","names":["sampleRate"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@waveform-playlist/worklets",
|
|
3
|
+
"version": "10.0.0",
|
|
4
|
+
"description": "AudioWorklet processors for waveform-playlist (metering, recording)",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"keywords": [
|
|
17
|
+
"waveform",
|
|
18
|
+
"audio",
|
|
19
|
+
"audioworklet",
|
|
20
|
+
"metering",
|
|
21
|
+
"waveform-playlist"
|
|
22
|
+
],
|
|
23
|
+
"author": "Naomi Aro",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/naomiaro/waveform-playlist.git",
|
|
28
|
+
"directory": "packages/worklets"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://naomiaro.github.io/waveform-playlist",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/naomiaro/waveform-playlist/issues"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"tsup": "^8.0.1",
|
|
40
|
+
"typescript": "^5.3.3",
|
|
41
|
+
"vitest": "^3.0.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"dev": "tsup --watch",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest"
|
|
49
|
+
}
|
|
50
|
+
}
|