Format
This commit is contained in:
parent
428a4b3dd7
commit
4bdaf577c0
9 changed files with 360 additions and 318 deletions
|
|
@ -42,9 +42,9 @@ const extensions = {
|
|||
isMultiCursorEnabled: (on) =>
|
||||
on
|
||||
? [
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
||||
]
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
||||
]
|
||||
: [],
|
||||
};
|
||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||
|
|
@ -269,10 +269,9 @@ export class StrudelMirror {
|
|||
await this.repl.evaluate(this.code);
|
||||
}
|
||||
async exportAudio(begin, end, sampleRate, downloadName = undefined) {
|
||||
await this.repl.evaluate(this.code, false)
|
||||
await this.repl.evaluate(this.code, false);
|
||||
await this.repl.exportAudio(begin, end, sampleRate, downloadName);
|
||||
this.repl.scheduler.stop();
|
||||
|
||||
}
|
||||
async stop() {
|
||||
this.repl.scheduler.stop();
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export class Cyclist {
|
|||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||
}
|
||||
logger('[cyclist] exporting');
|
||||
await renderPatternAudio(this.pattern, this.cps, begin, end, sampleRate, downloadName)
|
||||
await renderPatternAudio(this.pattern, this.cps, begin, end, sampleRate, downloadName);
|
||||
}
|
||||
|
||||
pause() {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ export function repl({
|
|||
|
||||
const stop = () => scheduler.stop();
|
||||
const start = () => scheduler.start();
|
||||
const exportAudio = async (begin, end, sampleRate, downloadName = undefined) => await scheduler.exportAudio(begin, end, sampleRate, downloadName);
|
||||
const exportAudio = async (begin, end, sampleRate, downloadName = undefined) =>
|
||||
await scheduler.exportAudio(begin, end, sampleRate, downloadName);
|
||||
const pause = () => scheduler.pause();
|
||||
const toggle = () => scheduler.toggle();
|
||||
const setCps = (cps) => {
|
||||
|
|
@ -263,18 +264,18 @@ export function repl({
|
|||
|
||||
export const getTrigger =
|
||||
({ getTime, defaultOutput }) =>
|
||||
async (hap, deadline, duration, cps, t) => {
|
||||
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
|
||||
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
await defaultOutput(hap, deadline, duration, cps, t);
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(hap, getTime(), cps, t);
|
||||
}
|
||||
} catch (err) {
|
||||
errorLogger(err, 'getTrigger');
|
||||
async (hap, deadline, duration, cps, t) => {
|
||||
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
|
||||
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
await defaultOutput(hap, deadline, duration, cps, t);
|
||||
}
|
||||
};
|
||||
if (hap.context.onTrigger) {
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(hap, getTime(), cps, t);
|
||||
}
|
||||
} catch (err) {
|
||||
errorLogger(err, 'getTrigger');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { analysers, resetGlobalEffects } from "./superdough.mjs";
|
||||
import { analysers, resetGlobalEffects } from './superdough.mjs';
|
||||
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
audioContext = new AudioContext();
|
||||
resetGlobalEffects()
|
||||
resetGlobalEffects();
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export const setAudioContext = (context) => {
|
||||
audioContext = context
|
||||
audioContext = context;
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ export function applyFM(param, value, begin) {
|
|||
duration,
|
||||
} = value;
|
||||
let modulator;
|
||||
let stop = () => { };
|
||||
let stop = () => {};
|
||||
|
||||
if (fmModulationIndex) {
|
||||
const ac = getAudioContext();
|
||||
|
|
|
|||
|
|
@ -212,7 +212,9 @@ export function loadWorklets() {
|
|||
if (!workletsLoading) {
|
||||
const audioCtx = getAudioContext();
|
||||
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
|
||||
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))).then(() => workletsLoading = undefined);
|
||||
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))).then(
|
||||
() => (workletsLoading = undefined),
|
||||
);
|
||||
}
|
||||
|
||||
return workletsLoading;
|
||||
|
|
@ -250,7 +252,7 @@ export async function initAudio(options = {}) {
|
|||
logger('[superdough] failed to set audio interface', 'warning');
|
||||
}
|
||||
}
|
||||
if (!audioCtx instanceof OfflineAudioContext) {
|
||||
if ((!audioCtx) instanceof OfflineAudioContext) {
|
||||
await audioCtx.resume();
|
||||
}
|
||||
if (disableWorklets) {
|
||||
|
|
@ -288,7 +290,7 @@ function getSuperdoughAudioController() {
|
|||
}
|
||||
|
||||
export function setSuperdoughAudioController(newController) {
|
||||
controller = newController
|
||||
controller = newController;
|
||||
return controller;
|
||||
}
|
||||
|
||||
|
|
@ -401,7 +403,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
);
|
||||
return;
|
||||
} // FIXME: fix
|
||||
// destructure
|
||||
// destructure
|
||||
let {
|
||||
tremolo,
|
||||
tremolosync,
|
||||
|
|
@ -743,4 +745,4 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||
|
||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
||||
superdough(hap, t - ct, hap.duration / cps, cps);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,23 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||
*/
|
||||
|
||||
import * as strudel from '@strudel/core';
|
||||
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet, setAudioContext, getSampleBufferSource, loadBuffer, getSampleInfo, getSound, initAudio, setSuperdoughAudioController, loadWorklets, setMaxPolyphony, setMultiChannelOrbits } from 'superdough';
|
||||
import {
|
||||
superdough,
|
||||
getAudioContext,
|
||||
setLogger,
|
||||
doughTrigger,
|
||||
registerWorklet,
|
||||
setAudioContext,
|
||||
getSampleBufferSource,
|
||||
loadBuffer,
|
||||
getSampleInfo,
|
||||
getSound,
|
||||
initAudio,
|
||||
setSuperdoughAudioController,
|
||||
loadWorklets,
|
||||
setMaxPolyphony,
|
||||
setMultiChannelOrbits,
|
||||
} from 'superdough';
|
||||
import './supradough.mjs';
|
||||
import { workletUrl } from 'supradough';
|
||||
import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs';
|
||||
|
|
@ -28,59 +44,63 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
|||
};
|
||||
|
||||
export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, downloadName = undefined) {
|
||||
const oldAudioCtx = getAudioContext()
|
||||
await oldAudioCtx.close()
|
||||
let audioContext = new OfflineAudioContext(2, (end - begin) / cps * sampleRate, sampleRate);
|
||||
setAudioContext(audioContext)
|
||||
let context = getAudioContext()
|
||||
setSuperdoughAudioController(new SuperdoughAudioController(context))
|
||||
setMaxPolyphony(1024)
|
||||
setMultiChannelOrbits(true)
|
||||
await loadWorklets()
|
||||
const oldAudioCtx = getAudioContext();
|
||||
await oldAudioCtx.close();
|
||||
let audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate);
|
||||
setAudioContext(audioContext);
|
||||
let context = getAudioContext();
|
||||
setSuperdoughAudioController(new SuperdoughAudioController(context));
|
||||
setMaxPolyphony(1024);
|
||||
setMultiChannelOrbits(true);
|
||||
await loadWorklets();
|
||||
logger('[webaudio] start rendering');
|
||||
|
||||
let haps = pattern.queryArc(begin, end, { _cps: cps })
|
||||
Promise.all(haps.map(async h => {
|
||||
let s;
|
||||
if (h.value.s) {
|
||||
if (h.value.bank) {
|
||||
s = `${h.value.bank}_${h.value.s}`;
|
||||
let haps = pattern.queryArc(begin, end, { _cps: cps });
|
||||
Promise.all(
|
||||
haps.map(async (h) => {
|
||||
let s;
|
||||
if (h.value.s) {
|
||||
if (h.value.bank) {
|
||||
s = `${h.value.bank}_${h.value.s}`;
|
||||
} else {
|
||||
s = h.value.s;
|
||||
}
|
||||
let bank = getSound(s).data.samples;
|
||||
if (bank) {
|
||||
let { url: sampleUrl, label } = getSampleInfo(h.value, bank);
|
||||
await loadBuffer(sampleUrl, context, label);
|
||||
}
|
||||
}
|
||||
else {
|
||||
s = h.value.s
|
||||
}
|
||||
let bank = getSound(s).data.samples
|
||||
if (bank) {
|
||||
let { url: sampleUrl, label } = getSampleInfo(h.value, bank);
|
||||
await loadBuffer(sampleUrl, context, label)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}),
|
||||
);
|
||||
haps.forEach(async (hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
hap.ensureObjectValue();
|
||||
await superdough(hap.value, hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf());
|
||||
}
|
||||
})
|
||||
|
||||
return context.startRendering().then((renderedBuffer) => {
|
||||
console.log(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)
|
||||
await initAudio()
|
||||
});
|
||||
|
||||
return context
|
||||
.startRendering()
|
||||
.then((renderedBuffer) => {
|
||||
console.log(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);
|
||||
await initAudio();
|
||||
});
|
||||
}
|
||||
|
||||
export function webaudioRepl(options = {}) {
|
||||
|
|
@ -96,97 +116,97 @@ Pattern.prototype.dough = function () {
|
|||
return this.onTrigger(doughTrigger, 1);
|
||||
};
|
||||
|
||||
|
||||
function audioBufferToWav(buffer, opt) {
|
||||
opt = opt || {}
|
||||
opt = opt || {};
|
||||
|
||||
var numChannels = buffer.numberOfChannels
|
||||
var sampleRate = buffer.sampleRate
|
||||
var format = opt.float32 ? 3 : 1
|
||||
var bitDepth = format === 3 ? 32 : 16
|
||||
var numChannels = buffer.numberOfChannels;
|
||||
var sampleRate = buffer.sampleRate;
|
||||
var format = opt.float32 ? 3 : 1;
|
||||
var bitDepth = format === 3 ? 32 : 16;
|
||||
|
||||
var result
|
||||
var result;
|
||||
if (numChannels === 2) {
|
||||
result = interleave(buffer.getChannelData(0), buffer.getChannelData(1))
|
||||
result = interleave(buffer.getChannelData(0), buffer.getChannelData(1));
|
||||
} else {
|
||||
result = buffer.getChannelData(0)
|
||||
result = buffer.getChannelData(0);
|
||||
}
|
||||
|
||||
return encodeWAV(result, format, sampleRate, numChannels, bitDepth)
|
||||
return encodeWAV(result, format, sampleRate, numChannels, bitDepth);
|
||||
}
|
||||
|
||||
function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) {
|
||||
var bytesPerSample = bitDepth / 8
|
||||
var blockAlign = numChannels * bytesPerSample
|
||||
var bytesPerSample = bitDepth / 8;
|
||||
var blockAlign = numChannels * bytesPerSample;
|
||||
|
||||
var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample)
|
||||
var view = new DataView(buffer)
|
||||
var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample);
|
||||
var view = new DataView(buffer);
|
||||
|
||||
/* RIFF identifier */
|
||||
writeString(view, 0, 'RIFF')
|
||||
writeString(view, 0, 'RIFF');
|
||||
/* RIFF chunk length */
|
||||
view.setUint32(4, 36 + samples.length * bytesPerSample, true)
|
||||
view.setUint32(4, 36 + samples.length * bytesPerSample, true);
|
||||
/* RIFF type */
|
||||
writeString(view, 8, 'WAVE')
|
||||
writeString(view, 8, 'WAVE');
|
||||
/* format chunk identifier */
|
||||
writeString(view, 12, 'fmt ')
|
||||
writeString(view, 12, 'fmt ');
|
||||
/* format chunk length */
|
||||
view.setUint32(16, 16, true)
|
||||
view.setUint32(16, 16, true);
|
||||
/* sample format (raw) */
|
||||
view.setUint16(20, format, true)
|
||||
view.setUint16(20, format, true);
|
||||
/* channel count */
|
||||
view.setUint16(22, numChannels, true)
|
||||
view.setUint16(22, numChannels, true);
|
||||
/* sample rate */
|
||||
view.setUint32(24, sampleRate, true)
|
||||
view.setUint32(24, sampleRate, true);
|
||||
/* byte rate (sample rate * block align) */
|
||||
view.setUint32(28, sampleRate * blockAlign, true)
|
||||
view.setUint32(28, sampleRate * blockAlign, true);
|
||||
/* block align (channel count * bytes per sample) */
|
||||
view.setUint16(32, blockAlign, true)
|
||||
view.setUint16(32, blockAlign, true);
|
||||
/* bits per sample */
|
||||
view.setUint16(34, bitDepth, true)
|
||||
view.setUint16(34, bitDepth, true);
|
||||
/* data chunk identifier */
|
||||
writeString(view, 36, 'data')
|
||||
writeString(view, 36, 'data');
|
||||
/* data chunk length */
|
||||
view.setUint32(40, samples.length * bytesPerSample, true)
|
||||
if (format === 1) { // Raw PCM
|
||||
floatTo16BitPCM(view, 44, samples)
|
||||
view.setUint32(40, samples.length * bytesPerSample, true);
|
||||
if (format === 1) {
|
||||
// Raw PCM
|
||||
floatTo16BitPCM(view, 44, samples);
|
||||
} else {
|
||||
writeFloat32(view, 44, samples)
|
||||
writeFloat32(view, 44, samples);
|
||||
}
|
||||
|
||||
return buffer
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function interleave(inputL, inputR) {
|
||||
var length = inputL.length + inputR.length
|
||||
var result = new Float32Array(length)
|
||||
var length = inputL.length + inputR.length;
|
||||
var result = new Float32Array(length);
|
||||
|
||||
var index = 0
|
||||
var inputIndex = 0
|
||||
var index = 0;
|
||||
var inputIndex = 0;
|
||||
|
||||
while (index < length) {
|
||||
result[index++] = inputL[inputIndex]
|
||||
result[index++] = inputR[inputIndex]
|
||||
inputIndex++
|
||||
result[index++] = inputL[inputIndex];
|
||||
result[index++] = inputR[inputIndex];
|
||||
inputIndex++;
|
||||
}
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
function writeFloat32(output, offset, input) {
|
||||
for (var i = 0; i < input.length; i++, offset += 4) {
|
||||
output.setFloat32(offset, input[i], true)
|
||||
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)
|
||||
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))
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue