update webaudio to main branch
This commit is contained in:
parent
a86e5751eb
commit
aa7ba6ae99
1 changed files with 169 additions and 6 deletions
|
|
@ -7,15 +7,19 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||||
import * as strudel from '@strudel/core';
|
import * as strudel from '@strudel/core';
|
||||||
import {
|
import {
|
||||||
superdough,
|
superdough,
|
||||||
setDefaultAudioContext,
|
|
||||||
getAudioContext,
|
getAudioContext,
|
||||||
setLogger,
|
setLogger,
|
||||||
doughTrigger,
|
doughTrigger,
|
||||||
registerWorklet,
|
registerWorklet,
|
||||||
|
setAudioContext,
|
||||||
|
initAudio,
|
||||||
|
setSuperdoughAudioController,
|
||||||
|
resetGlobalEffects,
|
||||||
|
errorLogger,
|
||||||
} from 'superdough';
|
} from 'superdough';
|
||||||
import './supradough.mjs';
|
import './supradough.mjs';
|
||||||
import { workletUrl } from 'supradough';
|
import { workletUrl } from 'supradough';
|
||||||
|
import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs';
|
||||||
registerWorklet(workletUrl);
|
registerWorklet(workletUrl);
|
||||||
|
|
||||||
const { Pattern, logger, repl } = strudel;
|
const { Pattern, logger, repl } = strudel;
|
||||||
|
|
@ -33,12 +37,76 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
||||||
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
||||||
};
|
};
|
||||||
|
|
||||||
export function webaudioRepl(options = {}) {
|
export async function renderPatternAudio(
|
||||||
if (options.existingAudioCtx) {
|
pattern,
|
||||||
setDefaultAudioContext(options.existingAudioCtx);
|
cps,
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
sampleRate,
|
||||||
|
maxPolyphony,
|
||||||
|
multiChannelOrbits,
|
||||||
|
downloadName = undefined,
|
||||||
|
) {
|
||||||
|
let audioContext = getAudioContext();
|
||||||
|
await audioContext.close();
|
||||||
|
audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate);
|
||||||
|
setAudioContext(audioContext);
|
||||||
|
setSuperdoughAudioController(new SuperdoughAudioController(audioContext));
|
||||||
|
await initAudio({
|
||||||
|
maxPolyphony,
|
||||||
|
multiChannelOrbits,
|
||||||
|
});
|
||||||
|
logger('[webaudio] preloading');
|
||||||
|
|
||||||
|
// Calling superdough(...) in ascending onset time order is important
|
||||||
|
// for controls that depend on the audio graph state like `cut`
|
||||||
|
let haps = pattern
|
||||||
|
.queryArc(begin, end, { _cps: cps })
|
||||||
|
.sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
|
||||||
|
for (const hap of haps) {
|
||||||
|
if (hap.hasOnset()) {
|
||||||
|
try {
|
||||||
|
await superdough(
|
||||||
|
hap2value(hap),
|
||||||
|
(hap.whole.begin.valueOf() - begin) / cps,
|
||||||
|
hap.duration / cps,
|
||||||
|
cps,
|
||||||
|
(hap.whole?.begin.valueOf() - begin) / cps,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errorLogger(err, 'webaudio');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
logger('[webaudio] start rendering');
|
||||||
|
|
||||||
|
return audioContext
|
||||||
|
.startRendering()
|
||||||
|
.then((renderedBuffer) => {
|
||||||
|
const wavBuffer = audioBufferToWav(renderedBuffer);
|
||||||
|
const blob = new Blob([wavBuffer], { type: 'audio/wav' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
downloadName = downloadName ? `${downloadName}.wav` : `${new Date().toISOString()}.wav`;
|
||||||
|
a.download = `${downloadName}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
setAudioContext(null);
|
||||||
|
setSuperdoughAudioController(null);
|
||||||
|
resetGlobalEffects();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function webaudioRepl(options = {}) {
|
||||||
|
const audioContext = options.audioContext ?? getAudioContext();
|
||||||
|
setAudioContext(audioContext);
|
||||||
options = {
|
options = {
|
||||||
getTime: () => getAudioContext().currentTime,
|
getTime: () => audioContext.currentTime,
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
@ -48,3 +116,98 @@ export function webaudioRepl(options = {}) {
|
||||||
Pattern.prototype.dough = function () {
|
Pattern.prototype.dough = function () {
|
||||||
return this.onTrigger(doughTrigger, 1);
|
return this.onTrigger(doughTrigger, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function audioBufferToWav(buffer, opt) {
|
||||||
|
opt = opt || {};
|
||||||
|
|
||||||
|
var numChannels = buffer.numberOfChannels;
|
||||||
|
var sampleRate = buffer.sampleRate;
|
||||||
|
var format = opt.float32 ? 3 : 1;
|
||||||
|
var bitDepth = format === 3 ? 32 : 16;
|
||||||
|
|
||||||
|
var result;
|
||||||
|
if (numChannels === 2) {
|
||||||
|
result = interleave(buffer.getChannelData(0), buffer.getChannelData(1));
|
||||||
|
} else {
|
||||||
|
result = buffer.getChannelData(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodeWAV(result, format, sampleRate, numChannels, bitDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) {
|
||||||
|
var bytesPerSample = bitDepth / 8;
|
||||||
|
var blockAlign = numChannels * bytesPerSample;
|
||||||
|
|
||||||
|
var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample);
|
||||||
|
var view = new DataView(buffer);
|
||||||
|
|
||||||
|
/* RIFF identifier */
|
||||||
|
writeString(view, 0, 'RIFF');
|
||||||
|
/* RIFF chunk length */
|
||||||
|
view.setUint32(4, 36 + samples.length * bytesPerSample, true);
|
||||||
|
/* RIFF type */
|
||||||
|
writeString(view, 8, 'WAVE');
|
||||||
|
/* format chunk identifier */
|
||||||
|
writeString(view, 12, 'fmt ');
|
||||||
|
/* format chunk length */
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
/* sample format (raw) */
|
||||||
|
view.setUint16(20, format, true);
|
||||||
|
/* channel count */
|
||||||
|
view.setUint16(22, numChannels, true);
|
||||||
|
/* sample rate */
|
||||||
|
view.setUint32(24, sampleRate, true);
|
||||||
|
/* byte rate (sample rate * block align) */
|
||||||
|
view.setUint32(28, sampleRate * blockAlign, true);
|
||||||
|
/* block align (channel count * bytes per sample) */
|
||||||
|
view.setUint16(32, blockAlign, true);
|
||||||
|
/* bits per sample */
|
||||||
|
view.setUint16(34, bitDepth, true);
|
||||||
|
/* data chunk identifier */
|
||||||
|
writeString(view, 36, 'data');
|
||||||
|
/* data chunk length */
|
||||||
|
view.setUint32(40, samples.length * bytesPerSample, true);
|
||||||
|
if (format === 1) {
|
||||||
|
// Raw PCM
|
||||||
|
floatTo16BitPCM(view, 44, samples);
|
||||||
|
} else {
|
||||||
|
writeFloat32(view, 44, samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function interleave(inputL, inputR) {
|
||||||
|
var length = inputL.length + inputR.length;
|
||||||
|
var result = new Float32Array(length);
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
var inputIndex = 0;
|
||||||
|
|
||||||
|
while (index < length) {
|
||||||
|
result[index++] = inputL[inputIndex];
|
||||||
|
result[index++] = inputR[inputIndex];
|
||||||
|
inputIndex++;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFloat32(output, offset, input) {
|
||||||
|
for (var i = 0; i < input.length; i++, offset += 4) {
|
||||||
|
output.setFloat32(offset, input[i], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function floatTo16BitPCM(output, offset, input) {
|
||||||
|
for (var i = 0; i < input.length; i++, offset += 2) {
|
||||||
|
var s = Math.max(-1, Math.min(1, input[i]));
|
||||||
|
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeString(view, offset, string) {
|
||||||
|
for (var i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue