mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9933c610a | |||
| 9d81a4d071 | |||
| 86cc9b87c3 | |||
| 0df6f3e56e |
@@ -73,6 +73,8 @@ export function registerControl(names, ...aliases) {
|
|||||||
*/
|
*/
|
||||||
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
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.
|
* Define a custom webaudio node to use as a sound source.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { getAudioContext, registerSound } from './index.mjs';
|
|||||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
export const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
export const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||||
|
|
||||||
export const getCachedBuffer = (url) => bufferCache[url];
|
export const getCachedBuffer = (url) => bufferCache[url];
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import workletsUrl from './worklets.mjs?url';
|
|||||||
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
|
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer, bufferCache, loadCache, onTriggerSample } from './sampler.mjs';
|
||||||
|
|
||||||
export const soundMap = map();
|
export const soundMap = map();
|
||||||
|
|
||||||
@@ -95,6 +95,7 @@ function loadWorklets() {
|
|||||||
return workletsLoading;
|
return workletsLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let stream;
|
||||||
// this function should be called on first user interaction (to avoid console warning)
|
// this function should be called on first user interaction (to avoid console warning)
|
||||||
export async function initAudio(options = {}) {
|
export async function initAudio(options = {}) {
|
||||||
const { disableWorklets = false } = options;
|
const { disableWorklets = false } = options;
|
||||||
@@ -112,6 +113,7 @@ export async function initAudio(options = {}) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('could not load AudioWorklet effects', err);
|
console.warn('could not load AudioWorklet effects', err);
|
||||||
}
|
}
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
|
||||||
logger('[superdough] ready');
|
logger('[superdough] ready');
|
||||||
}
|
}
|
||||||
let audioReady;
|
let audioReady;
|
||||||
@@ -308,6 +310,48 @@ export function resetGlobalEffects() {
|
|||||||
analysersData = {};
|
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) => {
|
export const superdough = async (value, t, hapDuration) => {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
if (typeof value !== 'object') {
|
if (typeof value !== 'object') {
|
||||||
@@ -330,6 +374,7 @@ export const superdough = async (value, t, hapDuration) => {
|
|||||||
// destructure
|
// destructure
|
||||||
let {
|
let {
|
||||||
s = getDefaultValue('s'),
|
s = getDefaultValue('s'),
|
||||||
|
rec,
|
||||||
bank,
|
bank,
|
||||||
source,
|
source,
|
||||||
gain = getDefaultValue('gain'),
|
gain = getDefaultValue('gain'),
|
||||||
@@ -411,11 +456,19 @@ export const superdough = async (value, t, hapDuration) => {
|
|||||||
if (bank && s) {
|
if (bank && s) {
|
||||||
s = `${bank}_${s}`;
|
s = `${bank}_${s}`;
|
||||||
}
|
}
|
||||||
|
if (rec && getSound(rec)) {
|
||||||
|
s = rec;
|
||||||
|
value.s = rec;
|
||||||
|
}
|
||||||
|
|
||||||
// get source AudioNode
|
// get source AudioNode
|
||||||
let sourceNode;
|
let sourceNode;
|
||||||
if (source) {
|
if (source) {
|
||||||
sourceNode = source(t, value, hapDuration);
|
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)) {
|
} else if (getSound(s)) {
|
||||||
const { onTrigger } = getSound(s);
|
const { onTrigger } = getSound(s);
|
||||||
const soundHandle = await onTrigger(t, value, onended);
|
const soundHandle = await onTrigger(t, value, onended);
|
||||||
|
|||||||
@@ -464,3 +464,48 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
|
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);
|
||||||
|
|||||||
@@ -142,29 +142,22 @@ function openDB(config, onOpened) {
|
|||||||
async function processFilesForIDB(files) {
|
async function processFilesForIDB(files) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
Array.from(files)
|
Array.from(files)
|
||||||
.map((s) => {
|
.map((file) => {
|
||||||
const title = s.name;
|
const title = file.name;
|
||||||
|
|
||||||
if (!isAudioFile(title)) {
|
if (!isAudioFile(title)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//create obscured url to file system that can be fetched
|
const path = file.webkitRelativePath;
|
||||||
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;
|
let id = path?.length ? path : title;
|
||||||
if (id == null || title == null || blob == null) {
|
if (id == null || title == null || file == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
blob,
|
blob: file,
|
||||||
id,
|
id,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user