waa-play 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/adapters.cjs +28 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.cts +42 -0
- package/dist/adapters.d.ts +42 -0
- package/dist/adapters.js +3 -0
- package/dist/adapters.js.map +1 -0
- package/dist/buffer.cjs +24 -0
- package/dist/buffer.cjs.map +1 -0
- package/dist/buffer.d.cts +34 -0
- package/dist/buffer.d.ts +34 -0
- package/dist/buffer.js +3 -0
- package/dist/buffer.js.map +1 -0
- package/dist/chunk-2DL7CAEP.js +69 -0
- package/dist/chunk-2DL7CAEP.js.map +1 -0
- package/dist/chunk-37CPPRLV.js +24 -0
- package/dist/chunk-37CPPRLV.js.map +1 -0
- package/dist/chunk-4LNVRSTM.cjs +72 -0
- package/dist/chunk-4LNVRSTM.cjs.map +1 -0
- package/dist/chunk-5J7S6QV3.cjs +44 -0
- package/dist/chunk-5J7S6QV3.cjs.map +1 -0
- package/dist/chunk-6UTN73HG.cjs +29 -0
- package/dist/chunk-6UTN73HG.cjs.map +1 -0
- package/dist/chunk-AGP2IRC6.js +63 -0
- package/dist/chunk-AGP2IRC6.js.map +1 -0
- package/dist/chunk-C2ASIYN5.js +67 -0
- package/dist/chunk-C2ASIYN5.js.map +1 -0
- package/dist/chunk-CJJC6ASU.js +73 -0
- package/dist/chunk-CJJC6ASU.js.map +1 -0
- package/dist/chunk-CPAT75WD.cjs +60 -0
- package/dist/chunk-CPAT75WD.cjs.map +1 -0
- package/dist/chunk-CRODJ4KS.js +71 -0
- package/dist/chunk-CRODJ4KS.js.map +1 -0
- package/dist/chunk-D5CD5KQZ.cjs +72 -0
- package/dist/chunk-D5CD5KQZ.cjs.map +1 -0
- package/dist/chunk-GYH2JSCY.js +42 -0
- package/dist/chunk-GYH2JSCY.js.map +1 -0
- package/dist/chunk-HTGOHC73.cjs +69 -0
- package/dist/chunk-HTGOHC73.cjs.map +1 -0
- package/dist/chunk-LETS7FKB.js +33 -0
- package/dist/chunk-LETS7FKB.js.map +1 -0
- package/dist/chunk-M5PDY5EZ.cjs +84 -0
- package/dist/chunk-M5PDY5EZ.cjs.map +1 -0
- package/dist/chunk-PZE6HTZR.cjs +358 -0
- package/dist/chunk-PZE6HTZR.cjs.map +1 -0
- package/dist/chunk-QFFQQMU4.cjs +75 -0
- package/dist/chunk-QFFQQMU4.cjs.map +1 -0
- package/dist/chunk-QWNV2BZ5.cjs +37 -0
- package/dist/chunk-QWNV2BZ5.cjs.map +1 -0
- package/dist/chunk-RWJ4EWJT.js +356 -0
- package/dist/chunk-RWJ4EWJT.js.map +1 -0
- package/dist/chunk-T74FBKTY.js +55 -0
- package/dist/chunk-T74FBKTY.js.map +1 -0
- package/dist/chunk-TULV7V5M.cjs +1710 -0
- package/dist/chunk-TULV7V5M.cjs.map +1 -0
- package/dist/chunk-V2QX5K42.js +1708 -0
- package/dist/chunk-V2QX5K42.js.map +1 -0
- package/dist/context.cjs +24 -0
- package/dist/context.cjs.map +1 -0
- package/dist/context.d.cts +27 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +3 -0
- package/dist/context.js.map +1 -0
- package/dist/emitter.cjs +12 -0
- package/dist/emitter.cjs.map +1 -0
- package/dist/emitter.d.cts +24 -0
- package/dist/emitter.d.ts +24 -0
- package/dist/emitter.js +3 -0
- package/dist/emitter.js.map +1 -0
- package/dist/engine-5JK2FCNL.cjs +13 -0
- package/dist/engine-5JK2FCNL.cjs.map +1 -0
- package/dist/engine-M2U4LE3F.js +4 -0
- package/dist/engine-M2U4LE3F.js.map +1 -0
- package/dist/fade.cjs +24 -0
- package/dist/fade.cjs.map +1 -0
- package/dist/fade.d.cts +21 -0
- package/dist/fade.d.ts +21 -0
- package/dist/fade.js +3 -0
- package/dist/fade.js.map +1 -0
- package/dist/index.cjs +165 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes.cjs +48 -0
- package/dist/nodes.cjs.map +1 -0
- package/dist/nodes.d.cts +61 -0
- package/dist/nodes.d.ts +61 -0
- package/dist/nodes.js +3 -0
- package/dist/nodes.js.map +1 -0
- package/dist/play.cjs +13 -0
- package/dist/play.cjs.map +1 -0
- package/dist/play.d.cts +19 -0
- package/dist/play.d.ts +19 -0
- package/dist/play.js +4 -0
- package/dist/play.js.map +1 -0
- package/dist/scheduler.cjs +16 -0
- package/dist/scheduler.cjs.map +1 -0
- package/dist/scheduler.d.cts +39 -0
- package/dist/scheduler.d.ts +39 -0
- package/dist/scheduler.js +3 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/stretcher.cjs +13 -0
- package/dist/stretcher.cjs.map +1 -0
- package/dist/stretcher.d.cts +171 -0
- package/dist/stretcher.d.ts +171 -0
- package/dist/stretcher.js +4 -0
- package/dist/stretcher.js.map +1 -0
- package/dist/synth.cjs +20 -0
- package/dist/synth.cjs.map +1 -0
- package/dist/synth.d.cts +15 -0
- package/dist/synth.d.ts +15 -0
- package/dist/synth.js +3 -0
- package/dist/synth.js.map +1 -0
- package/dist/types-DUrbEbPl.d.cts +177 -0
- package/dist/types-DUrbEbPl.d.ts +177 -0
- package/dist/waveform.cjs +20 -0
- package/dist/waveform.cjs.map +1 -0
- package/dist/waveform.d.cts +22 -0
- package/dist/waveform.d.ts +22 -0
- package/dist/waveform.js +3 -0
- package/dist/waveform.js.map +1 -0
- package/package.json +123 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/waveform.ts
|
|
4
|
+
function extractPeaks(buffer, options) {
|
|
5
|
+
const { resolution = 200, channel = 0 } = options ?? {};
|
|
6
|
+
const data = buffer.getChannelData(channel);
|
|
7
|
+
const blockSize = Math.floor(data.length / resolution);
|
|
8
|
+
const peaks = [];
|
|
9
|
+
for (let i = 0; i < resolution; i++) {
|
|
10
|
+
const start = i * blockSize;
|
|
11
|
+
const end = Math.min(start + blockSize, data.length);
|
|
12
|
+
let max = 0;
|
|
13
|
+
for (let j = start; j < end; j++) {
|
|
14
|
+
const abs = Math.abs(data[j]);
|
|
15
|
+
if (abs > max) max = abs;
|
|
16
|
+
}
|
|
17
|
+
peaks.push(max);
|
|
18
|
+
}
|
|
19
|
+
return peaks;
|
|
20
|
+
}
|
|
21
|
+
function extractPeakPairs(buffer, options) {
|
|
22
|
+
const { resolution = 200, channel = 0 } = options ?? {};
|
|
23
|
+
const data = buffer.getChannelData(channel);
|
|
24
|
+
const blockSize = Math.floor(data.length / resolution);
|
|
25
|
+
const pairs = [];
|
|
26
|
+
for (let i = 0; i < resolution; i++) {
|
|
27
|
+
const start = i * blockSize;
|
|
28
|
+
const end = Math.min(start + blockSize, data.length);
|
|
29
|
+
let min = 0;
|
|
30
|
+
let max = 0;
|
|
31
|
+
for (let j = start; j < end; j++) {
|
|
32
|
+
const sample = data[j];
|
|
33
|
+
if (sample < min) min = sample;
|
|
34
|
+
if (sample > max) max = sample;
|
|
35
|
+
}
|
|
36
|
+
pairs.push({ min, max });
|
|
37
|
+
}
|
|
38
|
+
return pairs;
|
|
39
|
+
}
|
|
40
|
+
function extractRMS(buffer, options) {
|
|
41
|
+
const { resolution = 200, channel = 0 } = options ?? {};
|
|
42
|
+
if (channel === -1) {
|
|
43
|
+
const allChannels = [];
|
|
44
|
+
for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
|
|
45
|
+
allChannels.push(extractRMS(buffer, { resolution, channel: ch }));
|
|
46
|
+
}
|
|
47
|
+
return allChannels[0].map((_, i) => {
|
|
48
|
+
let sum = 0;
|
|
49
|
+
for (const ch of allChannels) {
|
|
50
|
+
sum += ch[i];
|
|
51
|
+
}
|
|
52
|
+
return sum / allChannels.length;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const data = buffer.getChannelData(channel);
|
|
56
|
+
const blockSize = Math.floor(data.length / resolution);
|
|
57
|
+
const rms = [];
|
|
58
|
+
for (let i = 0; i < resolution; i++) {
|
|
59
|
+
const start = i * blockSize;
|
|
60
|
+
const end = Math.min(start + blockSize, data.length);
|
|
61
|
+
let sumSq = 0;
|
|
62
|
+
for (let j = start; j < end; j++) {
|
|
63
|
+
const s = data[j];
|
|
64
|
+
sumSq += s * s;
|
|
65
|
+
}
|
|
66
|
+
rms.push(Math.sqrt(sumSq / (end - start)));
|
|
67
|
+
}
|
|
68
|
+
return rms;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
exports.extractPeakPairs = extractPeakPairs;
|
|
72
|
+
exports.extractPeaks = extractPeaks;
|
|
73
|
+
exports.extractRMS = extractRMS;
|
|
74
|
+
//# sourceMappingURL=chunk-QFFQQMU4.cjs.map
|
|
75
|
+
//# sourceMappingURL=chunk-QFFQQMU4.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/waveform.ts"],"names":[],"mappings":"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","file":"chunk-QFFQQMU4.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// M6: Waveform data extraction\n// ---------------------------------------------------------------------------\n\nimport type { ExtractPeaksOptions, PeakPair } from \"./types.js\";\n\n/**\n * Extract normalised peak amplitude values from an `AudioBuffer`.\n * Returns an array of numbers in the range `[0, 1]`.\n */\nexport function extractPeaks(\n buffer: AudioBuffer,\n options?: ExtractPeaksOptions,\n): number[] {\n const { resolution = 200, channel = 0 } = options ?? {};\n const data = buffer.getChannelData(channel);\n const blockSize = Math.floor(data.length / resolution);\n const peaks: number[] = [];\n\n for (let i = 0; i < resolution; i++) {\n const start = i * blockSize;\n const end = Math.min(start + blockSize, data.length);\n let max = 0;\n for (let j = start; j < end; j++) {\n const abs = Math.abs(data[j]!);\n if (abs > max) max = abs;\n }\n peaks.push(max);\n }\n\n return peaks;\n}\n\n/**\n * Extract min/max peak pairs for detailed waveform rendering.\n */\nexport function extractPeakPairs(\n buffer: AudioBuffer,\n options?: ExtractPeaksOptions,\n): PeakPair[] {\n const { resolution = 200, channel = 0 } = options ?? {};\n const data = buffer.getChannelData(channel);\n const blockSize = Math.floor(data.length / resolution);\n const pairs: PeakPair[] = [];\n\n for (let i = 0; i < resolution; i++) {\n const start = i * blockSize;\n const end = Math.min(start + blockSize, data.length);\n let min = 0;\n let max = 0;\n for (let j = start; j < end; j++) {\n const sample = data[j]!;\n if (sample < min) min = sample;\n if (sample > max) max = sample;\n }\n pairs.push({ min, max });\n }\n\n return pairs;\n}\n\n/**\n * Extract RMS (root mean square) values representing perceived loudness.\n * Returns values in the range `[0, 1]`.\n *\n * When `channel` is set to `-1`, all channels are averaged.\n */\nexport function extractRMS(\n buffer: AudioBuffer,\n options?: ExtractPeaksOptions & { channel?: number },\n): number[] {\n const { resolution = 200, channel = 0 } = options ?? {};\n\n if (channel === -1) {\n // Average across all channels.\n const allChannels: number[][] = [];\n for (let ch = 0; ch < buffer.numberOfChannels; ch++) {\n allChannels.push(extractRMS(buffer, { resolution, channel: ch }));\n }\n return allChannels[0]!.map((_, i) => {\n let sum = 0;\n for (const ch of allChannels) {\n sum += ch[i]!;\n }\n return sum / allChannels.length;\n });\n }\n\n const data = buffer.getChannelData(channel);\n const blockSize = Math.floor(data.length / resolution);\n const rms: number[] = [];\n\n for (let i = 0; i < resolution; i++) {\n const start = i * blockSize;\n const end = Math.min(start + blockSize, data.length);\n let sumSq = 0;\n for (let j = start; j < end; j++) {\n const s = data[j]!;\n sumSq += s * s;\n }\n rms.push(Math.sqrt(sumSq / (end - start)));\n }\n\n return rms;\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/synth.ts
|
|
4
|
+
function createSineBuffer(ctx, frequency, duration) {
|
|
5
|
+
const length = Math.ceil(ctx.sampleRate * duration);
|
|
6
|
+
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
7
|
+
const data = buffer.getChannelData(0);
|
|
8
|
+
for (let i = 0; i < length; i++) {
|
|
9
|
+
data[i] = Math.sin(2 * Math.PI * frequency * i / ctx.sampleRate);
|
|
10
|
+
}
|
|
11
|
+
return buffer;
|
|
12
|
+
}
|
|
13
|
+
function createNoiseBuffer(ctx, duration) {
|
|
14
|
+
const length = Math.ceil(ctx.sampleRate * duration);
|
|
15
|
+
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
16
|
+
const data = buffer.getChannelData(0);
|
|
17
|
+
for (let i = 0; i < length; i++) {
|
|
18
|
+
data[i] = Math.random() * 2 - 1;
|
|
19
|
+
}
|
|
20
|
+
return buffer;
|
|
21
|
+
}
|
|
22
|
+
function createClickBuffer(ctx, frequency, duration) {
|
|
23
|
+
const length = Math.ceil(ctx.sampleRate * duration);
|
|
24
|
+
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
25
|
+
const data = buffer.getChannelData(0);
|
|
26
|
+
for (let i = 0; i < length; i++) {
|
|
27
|
+
const envelope = Math.exp(-5 * i / length);
|
|
28
|
+
data[i] = envelope * Math.sin(2 * Math.PI * frequency * i / ctx.sampleRate);
|
|
29
|
+
}
|
|
30
|
+
return buffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
exports.createClickBuffer = createClickBuffer;
|
|
34
|
+
exports.createNoiseBuffer = createNoiseBuffer;
|
|
35
|
+
exports.createSineBuffer = createSineBuffer;
|
|
36
|
+
//# sourceMappingURL=chunk-QWNV2BZ5.cjs.map
|
|
37
|
+
//# sourceMappingURL=chunk-QWNV2BZ5.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/synth.ts"],"names":[],"mappings":";;;AAQO,SAAS,gBAAA,CACd,GAAA,EACA,SAAA,EACA,QAAA,EACa;AACb,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,aAAa,QAAQ,CAAA;AAClD,EAAA,MAAM,SAAS,GAAA,CAAI,YAAA,CAAa,CAAA,EAAG,MAAA,EAAQ,IAAI,UAAU,CAAA;AACzD,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,cAAA,CAAe,CAAC,CAAA;AAEpC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,EAAQ,CAAA,EAAA,EAAK;AAC/B,IAAA,IAAA,CAAK,CAAC,CAAA,GAAI,IAAA,CAAK,GAAA,CAAK,CAAA,GAAI,KAAK,EAAA,GAAK,SAAA,GAAY,CAAA,GAAK,GAAA,CAAI,UAAU,CAAA;AAAA,EACnE;AAEA,EAAA,OAAO,MAAA;AACT;AAKO,SAAS,iBAAA,CACd,KACA,QAAA,EACa;AACb,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,aAAa,QAAQ,CAAA;AAClD,EAAA,MAAM,SAAS,GAAA,CAAI,YAAA,CAAa,CAAA,EAAG,MAAA,EAAQ,IAAI,UAAU,CAAA;AACzD,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,cAAA,CAAe,CAAC,CAAA;AAEpC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,EAAQ,CAAA,EAAA,EAAK;AAC/B,IAAA,IAAA,CAAK,CAAC,CAAA,GAAI,IAAA,CAAK,MAAA,KAAW,CAAA,GAAI,CAAA;AAAA,EAChC;AAEA,EAAA,OAAO,MAAA;AACT;AAKO,SAAS,iBAAA,CACd,GAAA,EACA,SAAA,EACA,QAAA,EACa;AACb,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,aAAa,QAAQ,CAAA;AAClD,EAAA,MAAM,SAAS,GAAA,CAAI,YAAA,CAAa,CAAA,EAAG,MAAA,EAAQ,IAAI,UAAU,CAAA;AACzD,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,cAAA,CAAe,CAAC,CAAA;AAEpC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,EAAQ,CAAA,EAAA,EAAK;AAE/B,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,CAAK,EAAA,GAAK,IAAK,MAAM,CAAA;AAC3C,IAAA,IAAA,CAAK,CAAC,CAAA,GACJ,QAAA,GAAW,IAAA,CAAK,GAAA,CAAK,CAAA,GAAI,IAAA,CAAK,EAAA,GAAK,SAAA,GAAY,CAAA,GAAK,GAAA,CAAI,UAAU,CAAA;AAAA,EACtE;AAEA,EAAA,OAAO,MAAA;AACT","file":"chunk-QWNV2BZ5.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// M9: Buffer synthesis\n// ---------------------------------------------------------------------------\n\n/**\n * Create an `AudioBuffer` containing a sine wave.\n * Useful for test tones and debugging.\n */\nexport function createSineBuffer(\n ctx: AudioContext,\n frequency: number,\n duration: number,\n): AudioBuffer {\n const length = Math.ceil(ctx.sampleRate * duration);\n const buffer = ctx.createBuffer(1, length, ctx.sampleRate);\n const data = buffer.getChannelData(0);\n\n for (let i = 0; i < length; i++) {\n data[i] = Math.sin((2 * Math.PI * frequency * i) / ctx.sampleRate);\n }\n\n return buffer;\n}\n\n/**\n * Create an `AudioBuffer` containing white noise.\n */\nexport function createNoiseBuffer(\n ctx: AudioContext,\n duration: number,\n): AudioBuffer {\n const length = Math.ceil(ctx.sampleRate * duration);\n const buffer = ctx.createBuffer(1, length, ctx.sampleRate);\n const data = buffer.getChannelData(0);\n\n for (let i = 0; i < length; i++) {\n data[i] = Math.random() * 2 - 1;\n }\n\n return buffer;\n}\n\n/**\n * Create an `AudioBuffer` containing a short click/impulse.\n */\nexport function createClickBuffer(\n ctx: AudioContext,\n frequency: number,\n duration: number,\n): AudioBuffer {\n const length = Math.ceil(ctx.sampleRate * duration);\n const buffer = ctx.createBuffer(1, length, ctx.sampleRate);\n const data = buffer.getChannelData(0);\n\n for (let i = 0; i < length; i++) {\n // Exponential decay envelope.\n const envelope = Math.exp((-5 * i) / length);\n data[i] =\n envelope * Math.sin((2 * Math.PI * frequency * i) / ctx.sampleRate);\n }\n\n return buffer;\n}\n"]}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { createEmitter } from './chunk-GYH2JSCY.js';
|
|
2
|
+
|
|
3
|
+
// src/play.ts
|
|
4
|
+
function play(ctx, buffer, options) {
|
|
5
|
+
const { preservePitch = true } = options ?? {};
|
|
6
|
+
if (preservePitch) {
|
|
7
|
+
return createStretchedPlayback(ctx, buffer, options ?? {});
|
|
8
|
+
}
|
|
9
|
+
const {
|
|
10
|
+
offset: initialOffset = 0,
|
|
11
|
+
loop = false,
|
|
12
|
+
loopStart,
|
|
13
|
+
loopEnd,
|
|
14
|
+
playbackRate: initialRate = 1,
|
|
15
|
+
through = [],
|
|
16
|
+
destination = ctx.destination,
|
|
17
|
+
timeupdateInterval = 50
|
|
18
|
+
} = options ?? {};
|
|
19
|
+
const emitter = createEmitter();
|
|
20
|
+
const duration = buffer.duration;
|
|
21
|
+
let state = "stopped";
|
|
22
|
+
let sourceNode = null;
|
|
23
|
+
let startedAt = 0;
|
|
24
|
+
let pausedAt = initialOffset;
|
|
25
|
+
let currentRate = initialRate;
|
|
26
|
+
let isLooping = loop;
|
|
27
|
+
let timerId = null;
|
|
28
|
+
let disposed = false;
|
|
29
|
+
function createSource() {
|
|
30
|
+
const src = ctx.createBufferSource();
|
|
31
|
+
src.buffer = buffer;
|
|
32
|
+
src.playbackRate.value = currentRate;
|
|
33
|
+
src.loop = isLooping;
|
|
34
|
+
if (loopStart !== void 0) src.loopStart = loopStart;
|
|
35
|
+
if (loopEnd !== void 0) src.loopEnd = loopEnd;
|
|
36
|
+
if (through.length > 0) {
|
|
37
|
+
src.connect(through[0]);
|
|
38
|
+
for (let i = 0; i < through.length - 1; i++) {
|
|
39
|
+
through[i].connect(through[i + 1]);
|
|
40
|
+
}
|
|
41
|
+
through[through.length - 1].connect(destination);
|
|
42
|
+
} else {
|
|
43
|
+
src.connect(destination);
|
|
44
|
+
}
|
|
45
|
+
src.onended = handleEnded;
|
|
46
|
+
return src;
|
|
47
|
+
}
|
|
48
|
+
function startSource(positionInBuffer) {
|
|
49
|
+
sourceNode = createSource();
|
|
50
|
+
sourceNode.start(0, positionInBuffer);
|
|
51
|
+
startedAt = ctx.currentTime - positionInBuffer / currentRate;
|
|
52
|
+
}
|
|
53
|
+
function stopSource() {
|
|
54
|
+
if (sourceNode) {
|
|
55
|
+
sourceNode.onended = null;
|
|
56
|
+
try {
|
|
57
|
+
sourceNode.stop();
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
sourceNode.disconnect();
|
|
61
|
+
sourceNode = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function handleEnded() {
|
|
65
|
+
if (state !== "playing") return;
|
|
66
|
+
if (isLooping) {
|
|
67
|
+
emitter.emit("loop", void 0);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
setState("stopped");
|
|
71
|
+
pausedAt = 0;
|
|
72
|
+
stopTimer();
|
|
73
|
+
emitter.emit("ended", void 0);
|
|
74
|
+
}
|
|
75
|
+
function setState(next) {
|
|
76
|
+
if (state === next) return;
|
|
77
|
+
state = next;
|
|
78
|
+
emitter.emit("statechange", { state: next });
|
|
79
|
+
}
|
|
80
|
+
function startTimer() {
|
|
81
|
+
if (timerId !== null) return;
|
|
82
|
+
timerId = setInterval(() => {
|
|
83
|
+
if (state !== "playing") return;
|
|
84
|
+
emitter.emit("timeupdate", {
|
|
85
|
+
position: getCurrentTime(),
|
|
86
|
+
duration
|
|
87
|
+
});
|
|
88
|
+
}, timeupdateInterval);
|
|
89
|
+
}
|
|
90
|
+
function stopTimer() {
|
|
91
|
+
if (timerId !== null) {
|
|
92
|
+
clearInterval(timerId);
|
|
93
|
+
timerId = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function getCurrentTime() {
|
|
97
|
+
if (state === "playing") {
|
|
98
|
+
const elapsed = (ctx.currentTime - startedAt) * currentRate;
|
|
99
|
+
if (isLooping) {
|
|
100
|
+
const loopDur = (loopEnd ?? duration) - (loopStart ?? 0);
|
|
101
|
+
return (elapsed - (loopStart ?? 0)) % loopDur + (loopStart ?? 0);
|
|
102
|
+
}
|
|
103
|
+
return Math.min(elapsed, duration);
|
|
104
|
+
}
|
|
105
|
+
if (state === "paused") return pausedAt;
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
function pause() {
|
|
109
|
+
if (state !== "playing" || disposed) return;
|
|
110
|
+
pausedAt = getCurrentTime();
|
|
111
|
+
stopSource();
|
|
112
|
+
stopTimer();
|
|
113
|
+
setState("paused");
|
|
114
|
+
emitter.emit("pause", void 0);
|
|
115
|
+
}
|
|
116
|
+
function resume() {
|
|
117
|
+
if (state !== "paused" || disposed) return;
|
|
118
|
+
startSource(pausedAt);
|
|
119
|
+
setState("playing");
|
|
120
|
+
startTimer();
|
|
121
|
+
emitter.emit("resume", void 0);
|
|
122
|
+
}
|
|
123
|
+
function togglePlayPause() {
|
|
124
|
+
if (state === "playing") pause();
|
|
125
|
+
else if (state === "paused") resume();
|
|
126
|
+
}
|
|
127
|
+
function seek(position) {
|
|
128
|
+
if (disposed) return;
|
|
129
|
+
const clamped = Math.max(0, Math.min(position, duration));
|
|
130
|
+
const wasPlaying = state === "playing";
|
|
131
|
+
stopSource();
|
|
132
|
+
stopTimer();
|
|
133
|
+
pausedAt = clamped;
|
|
134
|
+
if (wasPlaying) {
|
|
135
|
+
startSource(clamped);
|
|
136
|
+
startTimer();
|
|
137
|
+
}
|
|
138
|
+
emitter.emit("seek", { position: clamped });
|
|
139
|
+
}
|
|
140
|
+
function stop() {
|
|
141
|
+
if (state === "stopped" || disposed) return;
|
|
142
|
+
stopSource();
|
|
143
|
+
stopTimer();
|
|
144
|
+
pausedAt = 0;
|
|
145
|
+
setState("stopped");
|
|
146
|
+
emitter.emit("stop", void 0);
|
|
147
|
+
}
|
|
148
|
+
function setPlaybackRate(rate) {
|
|
149
|
+
const position = getCurrentTime();
|
|
150
|
+
currentRate = rate;
|
|
151
|
+
if (sourceNode) {
|
|
152
|
+
sourceNode.playbackRate.value = rate;
|
|
153
|
+
startedAt = ctx.currentTime - position / rate;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function setLoop(value) {
|
|
157
|
+
isLooping = value;
|
|
158
|
+
if (sourceNode) {
|
|
159
|
+
sourceNode.loop = value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function dispose() {
|
|
163
|
+
if (disposed) return;
|
|
164
|
+
disposed = true;
|
|
165
|
+
stopSource();
|
|
166
|
+
stopTimer();
|
|
167
|
+
emitter.clear();
|
|
168
|
+
}
|
|
169
|
+
startSource(initialOffset);
|
|
170
|
+
setState("playing");
|
|
171
|
+
startTimer();
|
|
172
|
+
emitter.emit("play", void 0);
|
|
173
|
+
return {
|
|
174
|
+
getState: () => state,
|
|
175
|
+
getCurrentTime,
|
|
176
|
+
getDuration: () => duration,
|
|
177
|
+
getProgress: () => duration > 0 ? getCurrentTime() / duration : 0,
|
|
178
|
+
pause,
|
|
179
|
+
resume,
|
|
180
|
+
togglePlayPause,
|
|
181
|
+
seek,
|
|
182
|
+
stop,
|
|
183
|
+
setPlaybackRate,
|
|
184
|
+
setLoop,
|
|
185
|
+
on: emitter.on.bind(emitter),
|
|
186
|
+
off: emitter.off.bind(emitter),
|
|
187
|
+
dispose
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function createStretchedPlayback(ctx, buffer, options) {
|
|
191
|
+
const {
|
|
192
|
+
offset: initialOffset = 0,
|
|
193
|
+
playbackRate: initialRate = 1,
|
|
194
|
+
through = [],
|
|
195
|
+
destination = ctx.destination,
|
|
196
|
+
timeupdateInterval = 50
|
|
197
|
+
} = options;
|
|
198
|
+
const emitter = createEmitter();
|
|
199
|
+
const duration = buffer.duration;
|
|
200
|
+
let state = "playing";
|
|
201
|
+
let engineInstance = null;
|
|
202
|
+
let timerId = null;
|
|
203
|
+
let disposed = false;
|
|
204
|
+
let currentRate = initialRate;
|
|
205
|
+
let pendingSeek = null;
|
|
206
|
+
emitter.emit("statechange", { state: "playing" });
|
|
207
|
+
emitter.emit("play", void 0);
|
|
208
|
+
import('./engine-M2U4LE3F.js').then(({ createStretcherEngine }) => {
|
|
209
|
+
if (disposed) return;
|
|
210
|
+
engineInstance = createStretcherEngine(ctx, buffer, {
|
|
211
|
+
tempo: currentRate,
|
|
212
|
+
offset: initialOffset,
|
|
213
|
+
through,
|
|
214
|
+
destination,
|
|
215
|
+
timeupdateInterval
|
|
216
|
+
});
|
|
217
|
+
engineInstance.on("buffering", (data) => {
|
|
218
|
+
if (disposed) return;
|
|
219
|
+
emitter.emit("buffering", data);
|
|
220
|
+
});
|
|
221
|
+
engineInstance.on("buffered", (data) => {
|
|
222
|
+
if (disposed) return;
|
|
223
|
+
emitter.emit("buffered", data);
|
|
224
|
+
});
|
|
225
|
+
engineInstance.on("ended", () => {
|
|
226
|
+
if (disposed) return;
|
|
227
|
+
state = "stopped";
|
|
228
|
+
stopTimer();
|
|
229
|
+
emitter.emit("statechange", { state: "stopped" });
|
|
230
|
+
emitter.emit("ended", void 0);
|
|
231
|
+
});
|
|
232
|
+
engineInstance.on("error", (data) => {
|
|
233
|
+
if (disposed) return;
|
|
234
|
+
if (data.fatal) {
|
|
235
|
+
state = "stopped";
|
|
236
|
+
emitter.emit("statechange", { state: "stopped" });
|
|
237
|
+
emitter.emit("ended", void 0);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
engineInstance.start();
|
|
241
|
+
startTimer();
|
|
242
|
+
if (pendingSeek !== null) {
|
|
243
|
+
engineInstance.seek(pendingSeek);
|
|
244
|
+
pendingSeek = null;
|
|
245
|
+
}
|
|
246
|
+
if (state === "paused") {
|
|
247
|
+
engineInstance.pause();
|
|
248
|
+
} else if (state === "stopped") {
|
|
249
|
+
engineInstance.stop();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
function startTimer() {
|
|
253
|
+
if (timerId !== null) return;
|
|
254
|
+
timerId = setInterval(() => {
|
|
255
|
+
if (state !== "playing" || disposed) return;
|
|
256
|
+
emitter.emit("timeupdate", {
|
|
257
|
+
position: getCurrentTime(),
|
|
258
|
+
duration
|
|
259
|
+
});
|
|
260
|
+
}, timeupdateInterval);
|
|
261
|
+
}
|
|
262
|
+
function stopTimer() {
|
|
263
|
+
if (timerId !== null) {
|
|
264
|
+
clearInterval(timerId);
|
|
265
|
+
timerId = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function getCurrentTime() {
|
|
269
|
+
if (pendingSeek !== null) {
|
|
270
|
+
return pendingSeek;
|
|
271
|
+
}
|
|
272
|
+
if (engineInstance) {
|
|
273
|
+
return engineInstance.getCurrentPosition();
|
|
274
|
+
}
|
|
275
|
+
return initialOffset;
|
|
276
|
+
}
|
|
277
|
+
function pause() {
|
|
278
|
+
if (state !== "playing" || disposed) return;
|
|
279
|
+
state = "paused";
|
|
280
|
+
engineInstance?.pause();
|
|
281
|
+
stopTimer();
|
|
282
|
+
emitter.emit("statechange", { state: "paused" });
|
|
283
|
+
emitter.emit("pause", void 0);
|
|
284
|
+
}
|
|
285
|
+
function resume() {
|
|
286
|
+
if (state !== "paused" || disposed) return;
|
|
287
|
+
state = "playing";
|
|
288
|
+
engineInstance?.resume();
|
|
289
|
+
startTimer();
|
|
290
|
+
emitter.emit("statechange", { state: "playing" });
|
|
291
|
+
emitter.emit("resume", void 0);
|
|
292
|
+
}
|
|
293
|
+
function togglePlayPause() {
|
|
294
|
+
if (state === "playing") pause();
|
|
295
|
+
else if (state === "paused") resume();
|
|
296
|
+
}
|
|
297
|
+
function seek(position) {
|
|
298
|
+
if (disposed) return;
|
|
299
|
+
const clamped = Math.max(0, Math.min(position, duration));
|
|
300
|
+
if (engineInstance) {
|
|
301
|
+
engineInstance.seek(clamped);
|
|
302
|
+
} else {
|
|
303
|
+
pendingSeek = clamped;
|
|
304
|
+
}
|
|
305
|
+
emitter.emit("seek", { position: clamped });
|
|
306
|
+
}
|
|
307
|
+
function stop() {
|
|
308
|
+
if (state === "stopped" || disposed) return;
|
|
309
|
+
state = "stopped";
|
|
310
|
+
engineInstance?.stop();
|
|
311
|
+
stopTimer();
|
|
312
|
+
emitter.emit("statechange", { state: "stopped" });
|
|
313
|
+
emitter.emit("stop", void 0);
|
|
314
|
+
}
|
|
315
|
+
function setPlaybackRate(rate) {
|
|
316
|
+
currentRate = rate;
|
|
317
|
+
if (engineInstance) {
|
|
318
|
+
engineInstance.setTempo(rate);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function setLoop(_value) {
|
|
322
|
+
}
|
|
323
|
+
function dispose() {
|
|
324
|
+
if (disposed) return;
|
|
325
|
+
disposed = true;
|
|
326
|
+
stopTimer();
|
|
327
|
+
engineInstance?.dispose();
|
|
328
|
+
emitter.clear();
|
|
329
|
+
}
|
|
330
|
+
function _getStretcherSnapshot() {
|
|
331
|
+
if (!engineInstance) return null;
|
|
332
|
+
return engineInstance.getSnapshot();
|
|
333
|
+
}
|
|
334
|
+
const playback = {
|
|
335
|
+
getState: () => state,
|
|
336
|
+
getCurrentTime,
|
|
337
|
+
getDuration: () => duration,
|
|
338
|
+
getProgress: () => duration > 0 ? getCurrentTime() / duration : 0,
|
|
339
|
+
pause,
|
|
340
|
+
resume,
|
|
341
|
+
togglePlayPause,
|
|
342
|
+
seek,
|
|
343
|
+
stop,
|
|
344
|
+
setPlaybackRate,
|
|
345
|
+
setLoop,
|
|
346
|
+
on: emitter.on.bind(emitter),
|
|
347
|
+
off: emitter.off.bind(emitter),
|
|
348
|
+
dispose,
|
|
349
|
+
_getStretcherSnapshot
|
|
350
|
+
};
|
|
351
|
+
return playback;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export { play };
|
|
355
|
+
//# sourceMappingURL=chunk-RWJ4EWJT.js.map
|
|
356
|
+
//# sourceMappingURL=chunk-RWJ4EWJT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/play.ts"],"names":[],"mappings":"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","file":"chunk-RWJ4EWJT.js","sourcesContent":["// ---------------------------------------------------------------------------\n// M3: Playback engine\n// ---------------------------------------------------------------------------\n\nimport { createEmitter } from \"./emitter.js\";\nimport type {\n Playback,\n PlaybackEventMap,\n PlaybackState,\n PlayOptions,\n StretcherSnapshotExtension,\n} from \"./types.js\";\n\n/**\n * Play an `AudioBuffer` through an `AudioContext` and return a controllable\n * `Playback` handle.\n *\n * ```ts\n * const pb = play(ctx, buffer, { loop: true });\n * pb.on(\"timeupdate\", ({ position }) => console.log(position));\n * pb.pause();\n * pb.resume();\n * pb.seek(10);\n * pb.stop();\n * pb.dispose();\n * ```\n */\nexport function play(\n ctx: AudioContext,\n buffer: AudioBuffer,\n options?: PlayOptions,\n): Playback {\n const { preservePitch = true } = options ?? {};\n\n // ----- Pitch-preserving mode (WSOLA-based time-stretch) -----\n if (preservePitch) {\n return createStretchedPlayback(ctx, buffer, options ?? {});\n }\n\n const {\n offset: initialOffset = 0,\n loop = false,\n loopStart,\n loopEnd,\n playbackRate: initialRate = 1,\n through = [],\n destination = ctx.destination,\n timeupdateInterval = 50,\n } = options ?? {};\n\n const emitter = createEmitter<PlaybackEventMap>();\n const duration = buffer.duration;\n\n // ----- mutable internal state -----\n let state: PlaybackState = \"stopped\";\n let sourceNode: AudioBufferSourceNode | null = null;\n let startedAt = 0; // ctx.currentTime when playback last started/resumed\n let pausedAt = initialOffset; // position in the buffer (seconds)\n let currentRate = initialRate;\n let isLooping = loop;\n let timerId: ReturnType<typeof setInterval> | null = null;\n let disposed = false;\n\n // ----- helpers -----\n\n function createSource(): AudioBufferSourceNode {\n const src = ctx.createBufferSource();\n src.buffer = buffer;\n src.playbackRate.value = currentRate;\n src.loop = isLooping;\n if (loopStart !== undefined) src.loopStart = loopStart;\n if (loopEnd !== undefined) src.loopEnd = loopEnd;\n\n // Connect through the node chain (or directly to destination).\n if (through.length > 0) {\n src.connect(through[0]!);\n for (let i = 0; i < through.length - 1; i++) {\n through[i]!.connect(through[i + 1]!);\n }\n through[through.length - 1]!.connect(destination);\n } else {\n src.connect(destination);\n }\n\n src.onended = handleEnded;\n return src;\n }\n\n function startSource(positionInBuffer: number) {\n sourceNode = createSource();\n sourceNode.start(0, positionInBuffer);\n startedAt = ctx.currentTime - positionInBuffer / currentRate;\n }\n\n function stopSource() {\n if (sourceNode) {\n sourceNode.onended = null;\n try {\n sourceNode.stop();\n } catch {\n // Already stopped — safe to ignore.\n }\n sourceNode.disconnect();\n sourceNode = null;\n }\n }\n\n function handleEnded() {\n // If we manually stopped / paused, the handler was already removed.\n if (state !== \"playing\") return;\n if (isLooping) {\n emitter.emit(\"loop\", undefined as never);\n return;\n }\n setState(\"stopped\");\n pausedAt = 0;\n stopTimer();\n emitter.emit(\"ended\", undefined as never);\n }\n\n function setState(next: PlaybackState) {\n if (state === next) return;\n state = next;\n emitter.emit(\"statechange\", { state: next });\n }\n\n function startTimer() {\n if (timerId !== null) return;\n timerId = setInterval(() => {\n if (state !== \"playing\") return;\n emitter.emit(\"timeupdate\", {\n position: getCurrentTime(),\n duration,\n });\n }, timeupdateInterval);\n }\n\n function stopTimer() {\n if (timerId !== null) {\n clearInterval(timerId);\n timerId = null;\n }\n }\n\n // ----- public API -----\n\n function getCurrentTime(): number {\n if (state === \"playing\") {\n const elapsed = (ctx.currentTime - startedAt) * currentRate;\n if (isLooping) {\n const loopDur =\n (loopEnd ?? duration) - (loopStart ?? 0);\n return ((elapsed - (loopStart ?? 0)) % loopDur) + (loopStart ?? 0);\n }\n return Math.min(elapsed, duration);\n }\n if (state === \"paused\") return pausedAt;\n return 0;\n }\n\n function pause() {\n if (state !== \"playing\" || disposed) return;\n pausedAt = getCurrentTime();\n stopSource();\n stopTimer();\n setState(\"paused\");\n emitter.emit(\"pause\", undefined as never);\n }\n\n function resume() {\n if (state !== \"paused\" || disposed) return;\n startSource(pausedAt);\n setState(\"playing\");\n startTimer();\n emitter.emit(\"resume\", undefined as never);\n }\n\n function togglePlayPause() {\n if (state === \"playing\") pause();\n else if (state === \"paused\") resume();\n }\n\n function seek(position: number) {\n if (disposed) return;\n const clamped = Math.max(0, Math.min(position, duration));\n const wasPlaying = state === \"playing\";\n\n stopSource();\n stopTimer();\n\n pausedAt = clamped;\n\n if (wasPlaying) {\n startSource(clamped);\n startTimer();\n }\n\n emitter.emit(\"seek\", { position: clamped });\n }\n\n function stop() {\n if (state === \"stopped\" || disposed) return;\n stopSource();\n stopTimer();\n pausedAt = 0;\n setState(\"stopped\");\n emitter.emit(\"stop\", undefined as never);\n }\n\n function setPlaybackRate(rate: number) {\n const position = getCurrentTime();\n currentRate = rate;\n if (sourceNode) {\n sourceNode.playbackRate.value = rate;\n startedAt = ctx.currentTime - position / rate;\n }\n }\n\n function setLoop(value: boolean) {\n isLooping = value;\n if (sourceNode) {\n sourceNode.loop = value;\n }\n }\n\n function dispose() {\n if (disposed) return;\n disposed = true;\n stopSource();\n stopTimer();\n emitter.clear();\n }\n\n // ----- kick off initial playback -----\n\n startSource(initialOffset);\n setState(\"playing\");\n startTimer();\n emitter.emit(\"play\", undefined as never);\n\n return {\n getState: () => state,\n getCurrentTime,\n getDuration: () => duration,\n getProgress: () => (duration > 0 ? getCurrentTime() / duration : 0),\n pause,\n resume,\n togglePlayPause,\n seek,\n stop,\n setPlaybackRate,\n setLoop,\n on: emitter.on.bind(emitter) as Playback[\"on\"],\n off: emitter.off.bind(emitter) as Playback[\"off\"],\n dispose,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Stretched playback (preservePitch: true)\n// ---------------------------------------------------------------------------\n\nfunction createStretchedPlayback(\n ctx: AudioContext,\n buffer: AudioBuffer,\n options: PlayOptions,\n): Playback {\n const {\n offset: initialOffset = 0,\n playbackRate: initialRate = 1,\n through = [],\n destination = ctx.destination,\n timeupdateInterval = 50,\n } = options;\n\n const emitter = createEmitter<PlaybackEventMap>();\n const duration = buffer.duration;\n\n let state: PlaybackState = \"playing\";\n let engineInstance: import(\"./stretcher/types.js\").StretcherEngine | null =\n null;\n let timerId: ReturnType<typeof setInterval> | null = null;\n let disposed = false;\n let currentRate = initialRate;\n let pendingSeek: number | null = null;\n\n // Emit initial play event\n emitter.emit(\"statechange\", { state: \"playing\" });\n emitter.emit(\"play\", undefined as never);\n\n // Fire-and-forget dynamic import of the stretcher engine\n import(\"./stretcher/engine.js\").then(({ createStretcherEngine }) => {\n if (disposed) return;\n\n engineInstance = createStretcherEngine(ctx, buffer, {\n tempo: currentRate,\n offset: initialOffset,\n through,\n destination,\n timeupdateInterval,\n });\n\n // Wire stretcher events to playback events\n engineInstance.on(\"buffering\", (data) => {\n if (disposed) return;\n emitter.emit(\"buffering\", data);\n });\n\n engineInstance.on(\"buffered\", (data) => {\n if (disposed) return;\n emitter.emit(\"buffered\", data);\n });\n\n engineInstance.on(\"ended\", () => {\n if (disposed) return;\n state = \"stopped\";\n stopTimer();\n emitter.emit(\"statechange\", { state: \"stopped\" });\n emitter.emit(\"ended\", undefined as never);\n });\n\n engineInstance.on(\"error\", (data) => {\n if (disposed) return;\n if (data.fatal) {\n state = \"stopped\";\n emitter.emit(\"statechange\", { state: \"stopped\" });\n emitter.emit(\"ended\", undefined as never);\n }\n });\n\n // Start engine and timeupdate timer\n engineInstance.start();\n startTimer();\n\n // Apply pending seek if any\n if (pendingSeek !== null) {\n engineInstance.seek(pendingSeek);\n pendingSeek = null;\n }\n\n // If we were paused before the engine loaded, pause it\n if (state === \"paused\") {\n engineInstance.pause();\n } else if (state === \"stopped\") {\n engineInstance.stop();\n }\n });\n\n function startTimer() {\n if (timerId !== null) return;\n timerId = setInterval(() => {\n if (state !== \"playing\" || disposed) return;\n emitter.emit(\"timeupdate\", {\n position: getCurrentTime(),\n duration,\n });\n }, timeupdateInterval);\n }\n\n function stopTimer() {\n if (timerId !== null) {\n clearInterval(timerId);\n timerId = null;\n }\n }\n\n function getCurrentTime(): number {\n if (pendingSeek !== null) {\n return pendingSeek;\n }\n if (engineInstance) {\n return engineInstance.getCurrentPosition();\n }\n return initialOffset;\n }\n\n function pause() {\n if (state !== \"playing\" || disposed) return;\n state = \"paused\";\n engineInstance?.pause();\n stopTimer();\n emitter.emit(\"statechange\", { state: \"paused\" });\n emitter.emit(\"pause\", undefined as never);\n }\n\n function resume() {\n if (state !== \"paused\" || disposed) return;\n state = \"playing\";\n engineInstance?.resume();\n startTimer();\n emitter.emit(\"statechange\", { state: \"playing\" });\n emitter.emit(\"resume\", undefined as never);\n }\n\n function togglePlayPause() {\n if (state === \"playing\") pause();\n else if (state === \"paused\") resume();\n }\n\n function seek(position: number) {\n if (disposed) return;\n const clamped = Math.max(0, Math.min(position, duration));\n if (engineInstance) {\n engineInstance.seek(clamped);\n } else {\n pendingSeek = clamped;\n }\n emitter.emit(\"seek\", { position: clamped });\n }\n\n function stop() {\n if (state === \"stopped\" || disposed) return;\n state = \"stopped\";\n engineInstance?.stop();\n stopTimer();\n emitter.emit(\"statechange\", { state: \"stopped\" });\n emitter.emit(\"stop\", undefined as never);\n }\n\n function setPlaybackRate(rate: number) {\n currentRate = rate;\n if (engineInstance) {\n engineInstance.setTempo(rate);\n }\n }\n\n function setLoop(_value: boolean) {\n // Loop is not supported in stretcher mode\n }\n\n function dispose() {\n if (disposed) return;\n disposed = true;\n stopTimer();\n engineInstance?.dispose();\n emitter.clear();\n }\n\n function _getStretcherSnapshot(): StretcherSnapshotExtension | null {\n if (!engineInstance) return null;\n return engineInstance.getSnapshot();\n }\n\n const playback: Playback & {\n _getStretcherSnapshot: typeof _getStretcherSnapshot;\n } = {\n getState: () => state,\n getCurrentTime,\n getDuration: () => duration,\n getProgress: () => (duration > 0 ? getCurrentTime() / duration : 0),\n pause,\n resume,\n togglePlayPause,\n seek,\n stop,\n setPlaybackRate,\n setLoop,\n on: emitter.on.bind(emitter) as Playback[\"on\"],\n off: emitter.off.bind(emitter) as Playback[\"off\"],\n dispose,\n _getStretcherSnapshot,\n };\n\n return playback;\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/buffer.ts
|
|
2
|
+
async function loadBuffer(ctx, url, options) {
|
|
3
|
+
const response = await fetch(url);
|
|
4
|
+
if (!response.ok) {
|
|
5
|
+
throw new Error(`Failed to fetch audio: ${response.status} ${response.statusText}`);
|
|
6
|
+
}
|
|
7
|
+
if (options?.onProgress && response.body && response.headers.get("content-length")) {
|
|
8
|
+
const total = Number(response.headers.get("content-length"));
|
|
9
|
+
const reader = response.body.getReader();
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let received = 0;
|
|
12
|
+
for (; ; ) {
|
|
13
|
+
const { done, value } = await reader.read();
|
|
14
|
+
if (done) break;
|
|
15
|
+
chunks.push(value);
|
|
16
|
+
received += value.byteLength;
|
|
17
|
+
options.onProgress(total > 0 ? received / total : 0);
|
|
18
|
+
}
|
|
19
|
+
const merged = new Uint8Array(received);
|
|
20
|
+
let offset = 0;
|
|
21
|
+
for (const chunk of chunks) {
|
|
22
|
+
merged.set(chunk, offset);
|
|
23
|
+
offset += chunk.byteLength;
|
|
24
|
+
}
|
|
25
|
+
return ctx.decodeAudioData(merged.buffer);
|
|
26
|
+
}
|
|
27
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
28
|
+
return ctx.decodeAudioData(arrayBuffer);
|
|
29
|
+
}
|
|
30
|
+
async function loadBufferFromBlob(ctx, blob) {
|
|
31
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
32
|
+
return ctx.decodeAudioData(arrayBuffer);
|
|
33
|
+
}
|
|
34
|
+
async function loadBuffers(ctx, map) {
|
|
35
|
+
const entries = Object.entries(map);
|
|
36
|
+
const results = await Promise.all(
|
|
37
|
+
entries.map(async ([key, url]) => {
|
|
38
|
+
const buffer = await loadBuffer(ctx, url);
|
|
39
|
+
return [key, buffer];
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
return new Map(results);
|
|
43
|
+
}
|
|
44
|
+
function getBufferInfo(buffer) {
|
|
45
|
+
return {
|
|
46
|
+
duration: buffer.duration,
|
|
47
|
+
numberOfChannels: buffer.numberOfChannels,
|
|
48
|
+
sampleRate: buffer.sampleRate,
|
|
49
|
+
length: buffer.length
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { getBufferInfo, loadBuffer, loadBufferFromBlob, loadBuffers };
|
|
54
|
+
//# sourceMappingURL=chunk-T74FBKTY.js.map
|
|
55
|
+
//# sourceMappingURL=chunk-T74FBKTY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/buffer.ts"],"names":[],"mappings":";AAeA,eAAsB,UAAA,CACpB,GAAA,EACA,GAAA,EACA,OAAA,EACsB;AACtB,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAChC,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,IAAI,MAAM,CAAA,uBAAA,EAA0B,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,EACpF;AAEA,EAAA,IAAI,OAAA,EAAS,cAAc,QAAA,CAAS,IAAA,IAAQ,SAAS,OAAA,CAAQ,GAAA,CAAI,gBAAgB,CAAA,EAAG;AAClF,IAAA,MAAM,QAAQ,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,gBAAgB,CAAC,CAAA;AAC3D,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,CAAK,SAAA,EAAU;AACvC,IAAA,MAAM,SAAuB,EAAC;AAC9B,IAAA,IAAI,QAAA,GAAW,CAAA;AAEf,IAAA,WAAS;AACP,MAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,MAAA,IAAI,IAAA,EAAM;AACV,MAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AACjB,MAAA,QAAA,IAAY,KAAA,CAAM,UAAA;AAClB,MAAA,OAAA,CAAQ,UAAA,CAAW,KAAA,GAAQ,CAAA,GAAI,QAAA,GAAW,QAAQ,CAAC,CAAA;AAAA,IACrD;AAEA,IAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,QAAQ,CAAA;AACtC,IAAA,IAAI,MAAA,GAAS,CAAA;AACb,IAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,MAAA,MAAA,CAAO,GAAA,CAAI,OAAO,MAAM,CAAA;AACxB,MAAA,MAAA,IAAU,KAAA,CAAM,UAAA;AAAA,IAClB;AAEA,IAAA,OAAO,GAAA,CAAI,eAAA,CAAgB,MAAA,CAAO,MAAM,CAAA;AAAA,EAC1C;AAEA,EAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,EAAY;AAC/C,EAAA,OAAO,GAAA,CAAI,gBAAgB,WAAW,CAAA;AACxC;AAKA,eAAsB,kBAAA,CACpB,KACA,IAAA,EACsB;AACtB,EAAA,MAAM,WAAA,GAAc,MAAM,IAAA,CAAK,WAAA,EAAY;AAC3C,EAAA,OAAO,GAAA,CAAI,gBAAgB,WAAW,CAAA;AACxC;AAaA,eAAsB,WAAA,CACpB,KACA,GAAA,EACmC;AACnC,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA;AAClC,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,GAAA;AAAA,IAC5B,QAAQ,GAAA,CAAI,OAAO,CAAC,GAAA,EAAK,GAAG,CAAA,KAAM;AAChC,MAAA,MAAM,MAAA,GAAS,MAAM,UAAA,CAAW,GAAA,EAAK,GAAG,CAAA;AACxC,MAAA,OAAO,CAAC,KAAK,MAAM,CAAA;AAAA,IACrB,CAAC;AAAA,GACH;AACA,EAAA,OAAO,IAAI,IAAI,OAAO,CAAA;AACxB;AAKO,SAAS,cAAc,MAAA,EAAiC;AAC7D,EAAA,OAAO;AAAA,IACL,UAAU,MAAA,CAAO,QAAA;AAAA,IACjB,kBAAkB,MAAA,CAAO,gBAAA;AAAA,IACzB,YAAY,MAAA,CAAO,UAAA;AAAA,IACnB,QAAQ,MAAA,CAAO;AAAA,GACjB;AACF","file":"chunk-T74FBKTY.js","sourcesContent":["// ---------------------------------------------------------------------------\n// M2: Audio buffer loading utilities\n// ---------------------------------------------------------------------------\n\nimport type { BufferInfo, LoadBufferOptions } from \"./types.js\";\n\n/**\n * Fetch an audio file from a URL and decode it into an `AudioBuffer`.\n *\n * ```ts\n * const buffer = await loadBuffer(ctx, \"/audio/track.mp3\", {\n * onProgress: (p) => console.log(`${(p * 100).toFixed(0)}%`),\n * });\n * ```\n */\nexport async function loadBuffer(\n ctx: AudioContext,\n url: string,\n options?: LoadBufferOptions,\n): Promise<AudioBuffer> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to fetch audio: ${response.status} ${response.statusText}`);\n }\n\n if (options?.onProgress && response.body && response.headers.get(\"content-length\")) {\n const total = Number(response.headers.get(\"content-length\"));\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let received = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n received += value.byteLength;\n options.onProgress(total > 0 ? received / total : 0);\n }\n\n const merged = new Uint8Array(received);\n let offset = 0;\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n return ctx.decodeAudioData(merged.buffer);\n }\n\n const arrayBuffer = await response.arrayBuffer();\n return ctx.decodeAudioData(arrayBuffer);\n}\n\n/**\n * Decode an `AudioBuffer` from a `Blob` or `File`.\n */\nexport async function loadBufferFromBlob(\n ctx: AudioContext,\n blob: Blob,\n): Promise<AudioBuffer> {\n const arrayBuffer = await blob.arrayBuffer();\n return ctx.decodeAudioData(arrayBuffer);\n}\n\n/**\n * Load multiple audio files in parallel.\n *\n * ```ts\n * const buffers = await loadBuffers(ctx, {\n * kick: \"/samples/kick.wav\",\n * snare: \"/samples/snare.wav\",\n * });\n * buffers.get(\"kick\"); // AudioBuffer\n * ```\n */\nexport async function loadBuffers(\n ctx: AudioContext,\n map: Record<string, string>,\n): Promise<Map<string, AudioBuffer>> {\n const entries = Object.entries(map);\n const results = await Promise.all(\n entries.map(async ([key, url]) => {\n const buffer = await loadBuffer(ctx, url);\n return [key, buffer] as const;\n }),\n );\n return new Map(results);\n}\n\n/**\n * Return metadata about an `AudioBuffer`.\n */\nexport function getBufferInfo(buffer: AudioBuffer): BufferInfo {\n return {\n duration: buffer.duration,\n numberOfChannels: buffer.numberOfChannels,\n sampleRate: buffer.sampleRate,\n length: buffer.length,\n };\n}\n"]}
|