mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-20 04:27:05 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9e4b16460 | |||
| 6350dfa31f | |||
| fe691adb28 | |||
| 187307c6bc |
@@ -127,6 +127,12 @@ registerWidget('_scope', (id, options = {}, pat) => {
|
||||
return pat.tag(id).scope({ ...options, ctx, id });
|
||||
});
|
||||
|
||||
registerWidget('_claviature', (id, options = {}, pat) => {
|
||||
options = { height: 75, width: 640, ...options };
|
||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||
return pat.tag(id).claviature({ ...options, ctx, id });
|
||||
});
|
||||
|
||||
registerWidget('_pitchwheel', (id, options = {}, pat) => {
|
||||
let _size = options.size || 200;
|
||||
options = { width: _size, height: _size, ...options, size: _size / 5 };
|
||||
|
||||
@@ -73,8 +73,6 @@ 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.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Pattern, noteToMidi } from '@strudel/core';
|
||||
|
||||
const blackPattern = [0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0];
|
||||
|
||||
export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
return [pc, acc, oct ? Number(oct) : undefined];
|
||||
};
|
||||
const accs = { '#': 1, b: -1, s: 1 };
|
||||
const toMidi = (note) => {
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
}
|
||||
const [pc, acc, oct] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
|
||||
const getMidiKeys = (range, offset) => {
|
||||
const white /* : number[] */ = [];
|
||||
const black /* : number[] */ = [];
|
||||
const to = noteToMidi(range[1]);
|
||||
for (let i = offset; i <= to; i++) {
|
||||
//
|
||||
(blackPattern[i % 12] ? black : white).push(i);
|
||||
}
|
||||
return [white, black];
|
||||
};
|
||||
|
||||
const whiteWidth = (midi, topWidth) => (midi % 12 > 4 ? 7 / 4 : 5 / 3) * topWidth;
|
||||
|
||||
const whiteX = (midi, offset, topWidth) =>
|
||||
Array.from({ length: midi - offset }, (_, i) => i + offset).reduce(
|
||||
(sum, m) => (!blackPattern[m % 12] ? sum + whiteWidth(m, topWidth) : sum),
|
||||
0,
|
||||
); // TODO: calculate mathematically
|
||||
|
||||
/* const blackX = (index, offset, topWidth) => {
|
||||
const cDiff = 12 - (offset % 12);
|
||||
console.log('cDiff', cDiff);
|
||||
const cOffset = whiteX(cDiff + offset);
|
||||
const blackOffset = cOffset + cDiff * topWidth;
|
||||
return (index - offset) * topWidth + blackOffset;
|
||||
}; */
|
||||
|
||||
const parseNote = (note) => (typeof note === 'number' ? note : toMidi(note));
|
||||
|
||||
export function claviature(haps, options) {
|
||||
const {
|
||||
ctx = getDrawContext(),
|
||||
range = ['C1', 'D3'],
|
||||
scaleX = 1,
|
||||
scaleY = 1,
|
||||
palette = [getTheme().foreground, getTheme().background],
|
||||
strokeWidth = 0,
|
||||
stroke = getTheme().foreground,
|
||||
upperWidth = 14,
|
||||
upperHeight = 100,
|
||||
lowerHeight = 45,
|
||||
} = options || {};
|
||||
const offset = parseNote(range[0]);
|
||||
const colorizedMidi = haps.map((hap) => ({
|
||||
keys: [parseNote(hap.value.note)],
|
||||
color: hap.value.color || getTheme().selection,
|
||||
}));
|
||||
/* const to = parseNote(range[1]);
|
||||
const totalKeys = to - offset + 1; */
|
||||
/* const width = totalKeys * topWidth + topWidth + strokeWidth * 2;
|
||||
const height = whiteHeight; */
|
||||
|
||||
const topWidth = upperWidth * scaleX;
|
||||
const [white, black] = getMidiKeys(range, offset);
|
||||
|
||||
const whiteHeight = (upperHeight + lowerHeight) * scaleY;
|
||||
const blackHeight = upperHeight * scaleY;
|
||||
|
||||
const cDiff = 12 - (offset % 12);
|
||||
const cOffset = whiteX(cDiff + offset);
|
||||
const blackOffset = cOffset;
|
||||
|
||||
const blackX = (midi) => (midi - offset) * topWidth + blackOffset;
|
||||
|
||||
const getColor = (midi) => colorizedMidi.find(({ keys }) => keys.includes(midi))?.color;
|
||||
ctx.clearRect(0, 0, ctx.canvas.width * 2, ctx.canvas.height * 2);
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.strokeWidth = strokeWidth;
|
||||
white.forEach((midi) => {
|
||||
ctx.fillStyle = getColor(midi) ?? palette[1];
|
||||
const x = whiteX(midi, offset, topWidth);
|
||||
const width = whiteWidth(midi, topWidth);
|
||||
ctx.fillRect(x, 0, width, whiteHeight);
|
||||
ctx.strokeRect(x, 0, width, whiteHeight);
|
||||
});
|
||||
black.forEach((midi) => {
|
||||
ctx.fillStyle = getColor(midi) ?? palette[0];
|
||||
const x = blackX(midi, offset, topWidth);
|
||||
//ctx.strokeRect(x, 0, topWidth, blackHeight);
|
||||
ctx.fillRect(x, 0, topWidth, blackHeight);
|
||||
});
|
||||
}
|
||||
|
||||
Pattern.prototype.claviature = function (options) {
|
||||
return this.draw((haps) => claviature(haps, options), { id: options.id });
|
||||
};
|
||||
@@ -3,4 +3,5 @@ export * from './color.mjs';
|
||||
export * from './draw.mjs';
|
||||
export * from './pianoroll.mjs';
|
||||
export * from './spiral.mjs';
|
||||
export * from './claviature.mjs';
|
||||
export * from './pitchwheel.mjs';
|
||||
|
||||
@@ -3,8 +3,8 @@ import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
export const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
|
||||
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 { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { loadBuffer, bufferCache, loadCache, onTriggerSample } from './sampler.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
|
||||
export const soundMap = map();
|
||||
|
||||
@@ -95,7 +95,6 @@ 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;
|
||||
@@ -113,7 +112,6 @@ 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;
|
||||
@@ -310,48 +308,6 @@ 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') {
|
||||
@@ -374,7 +330,6 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
// destructure
|
||||
let {
|
||||
s = getDefaultValue('s'),
|
||||
rec,
|
||||
bank,
|
||||
source,
|
||||
gain = getDefaultValue('gain'),
|
||||
@@ -456,19 +411,11 @@ 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);
|
||||
|
||||
@@ -464,48 +464,3 @@ 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);
|
||||
|
||||
@@ -142,22 +142,29 @@ function openDB(config, onOpened) {
|
||||
async function processFilesForIDB(files) {
|
||||
return Promise.all(
|
||||
Array.from(files)
|
||||
.map((file) => {
|
||||
const title = file.name;
|
||||
.map((s) => {
|
||||
const title = s.name;
|
||||
|
||||
if (!isAudioFile(title)) {
|
||||
return;
|
||||
}
|
||||
const path = file.webkitRelativePath;
|
||||
let id = path?.length ? path : title;
|
||||
if (id == null || title == null || file == null) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
title,
|
||||
blob: file,
|
||||
id,
|
||||
};
|
||||
//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,
|
||||
};
|
||||
});
|
||||
});
|
||||
})
|
||||
.filter(Boolean),
|
||||
).catch((error) => {
|
||||
|
||||
Reference in New Issue
Block a user