Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.
Draft

looper #1128

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export function registerControl(names, ...aliases) {
*/
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');

export const { rec } = registerControl(['rec', 'n']);

/**
* Define a custom webaudio node to use as a sound source.
*
Expand Down
4 changes: 2 additions & 2 deletions packages/superdough/sampler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { getAudioContext, registerSound } from './index.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { logger } from './logger.mjs';

const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
export const bufferCache = {}; // string: Promise<ArrayBuffer>
export const loadCache = {}; // string: Promise<ArrayBuffer>

export const getCachedBuffer = (url) => bufferCache[url];

Expand Down
55 changes: 54 additions & 1 deletion packages/superdough/superdough.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import workletsUrl from './worklets.mjs?url';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
import { loadBuffer, bufferCache, loadCache, onTriggerSample } from './sampler.mjs';

export const soundMap = map();

Expand Down Expand Up @@ -95,6 +95,7 @@ function loadWorklets() {
return workletsLoading;
}

let stream;
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const { disableWorklets = false } = options;
Expand All @@ -112,6 +113,7 @@ export async function initAudio(options = {}) {
} catch (err) {
console.warn('could not load AudioWorklet effects', err);
}
stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
logger('[superdough] ready');
}
let audioReady;
Expand Down Expand Up @@ -308,6 +310,48 @@ export function resetGlobalEffects() {
analysersData = {};
}

/* async */ function record(name, begin, hapDuration) {
registerSound(
name,
() => {
console.log('trigger recording before its ready...', getAudioContext().currentTime);
},
{},
);
const ac = getAudioContext();
try {
const inputNode = ac.createMediaStreamSource(stream);
const samples = Math.round(hapDuration * ac.sampleRate);
const options = { samples, begin, end: begin + hapDuration };
const recorder = getWorklet(ac, 'recording-processor', {});
recorder.port.postMessage(options);
inputNode.connect(recorder);
/* return */ new Promise((resolve) => {
recorder.port.onmessage = async (e) => {
const audioBuffer = ac.createBuffer(1, samples, ac.sampleRate);
audioBuffer.getChannelData(0).set(e.data.buffer);
const url = `rec:${name}`;
bufferCache[url] = audioBuffer;
loadCache[url] = audioBuffer;
const value = [url];
console.log('register recording', getAudioContext().currentTime);
registerSound(name, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl: undefined,
prebake: false,
tag: undefined,
});
resolve(name);
};
});
return recorder;
} catch (err) {
console.log('err', err);
// reject(err);
}
}

export const superdough = async (value, t, hapDuration) => {
const ac = getAudioContext();
if (typeof value !== 'object') {
Expand All @@ -330,6 +374,7 @@ export const superdough = async (value, t, hapDuration) => {
// destructure
let {
s = getDefaultValue('s'),
rec,
bank,
source,
gain = getDefaultValue('gain'),
Expand Down Expand Up @@ -411,11 +456,19 @@ export const superdough = async (value, t, hapDuration) => {
if (bank && s) {
s = `${bank}_${s}`;
}
if (rec && getSound(rec)) {
s = rec;
value.s = rec;
}

// get source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, value, hapDuration);
} else if (rec && !getSound(rec)) {
console.log('record', rec);
const recorder = record(rec, t, hapDuration);
sourceNode = recorder;
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const soundHandle = await onTrigger(t, value, onended);
Expand Down
45 changes: 45 additions & 0 deletions packages/superdough/worklets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,48 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
}

registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);

class RecordingProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.done = false;
this.head = 0;
this.nudge = 0.1;
this.port.onmessage = (e) => {
this.begin = e.data.begin + this.nudge;

this.samples = e.data.samples;
this.buffer = new Float32Array(this.samples);
};
}
process(inputs, outputs) {
// noop if scheduled recording begin hasn't been reached
// eslint-disable-next-line no-undef
if (currentTime < this.begin) {
return true;
}
if (!this.buffer) {
console.log('buffer not ready..');
return true;
}
// stop when the buffer is full
if (!this.done && this.head >= this.samples) {
this.done = true;
this.port.postMessage({ buffer: this.buffer });
return false;
}

// so far only 1 channel
const input = inputs[0];
// const output = outputs[0];
for (let i = 0; i < input[0].length; i++) {
this.buffer[this.head] = input[0][i] * 0.25;
/* output[0][i] = input[0][i];
output[1][i] = input[0][i]; */
this.head++;
}
return true;
}
}

registerProcessor('recording-processor', RecordingProcessor);
31 changes: 12 additions & 19 deletions website/src/repl/idbutils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -142,29 +142,22 @@ function openDB(config, onOpened) {
async function processFilesForIDB(files) {
return Promise.all(
Array.from(files)
.map((s) => {
const title = s.name;
.map((file) => {
const title = file.name;

if (!isAudioFile(title)) {
return;
}
//create obscured url to file system that can be fetched
const sUrl = URL.createObjectURL(s);
//fetch the sound and turn it into a buffer array
return fetch(sUrl).then((res) => {
return res.blob().then((blob) => {
const path = s.webkitRelativePath;
let id = path?.length ? path : title;
if (id == null || title == null || blob == null) {
return;
}
return {
title,
blob,
id,
};
});
});
const path = file.webkitRelativePath;
let id = path?.length ? path : title;
if (id == null || title == null || file == null) {
return;
}
return {
title,
blob: file,
id,
};
})
.filter(Boolean),
).catch((error) => {
Expand Down