mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-14 04:14:38 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70c932a760 | |||
| f5631a01ae | |||
| 989b5dcc3a | |||
| 5de891f31d | |||
| 325e348bbf | |||
| fe117e23fd | |||
| 69a82ddc5b | |||
| 4209506fd0 | |||
| ff790c803d | |||
| f0efaabe20 | |||
| 5097ae8fc0 | |||
| 6b1e6cc7af | |||
| 9c6eca9a59 | |||
| 02bb88bde4 | |||
| c16511dfbd | |||
| 5054756ca2 |
@@ -140,3 +140,9 @@ registerWidget('_spectrum', (id, options = {}, pat) => {
|
|||||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
return pat.spectrum({ ...options, ctx, id });
|
return pat.spectrum({ ...options, ctx, id });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerWidget('_textbox', (id, options = {}, pat) => {
|
||||||
|
options = { width: 500, height: 30, ...options };
|
||||||
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
|
return pat.textbox({ ...options, ctx, id });
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ function getTime() {
|
|||||||
let num_cycles_at_cps_change = 0;
|
let num_cycles_at_cps_change = 0;
|
||||||
let num_ticks_since_cps_change = 0;
|
let num_ticks_since_cps_change = 0;
|
||||||
let num_seconds_at_cps_change = 0;
|
let num_seconds_at_cps_change = 0;
|
||||||
|
let max_cycle = null;
|
||||||
|
|
||||||
let cps = 0.5;
|
let cps = 0.5;
|
||||||
// {id: {started: boolean}}
|
// {id: {started: boolean}}
|
||||||
const clients = new Map();
|
const clients = new Map();
|
||||||
@@ -39,6 +41,7 @@ const sendTick = (phase, duration, tick, time) => {
|
|||||||
cps,
|
cps,
|
||||||
time,
|
time,
|
||||||
cycle,
|
cycle,
|
||||||
|
max_cycle,
|
||||||
});
|
});
|
||||||
num_ticks_since_cps_change++;
|
num_ticks_since_cps_change++;
|
||||||
};
|
};
|
||||||
@@ -60,9 +63,10 @@ const stopClock = async (id) => {
|
|||||||
|
|
||||||
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
|
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
|
||||||
//dont stop the clock if other instances are running...
|
//dont stop the clock if other instances are running...
|
||||||
if (!started || otherClientStarted) {
|
// actually do stop it
|
||||||
return;
|
// if (!started || otherClientStarted) {
|
||||||
}
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
clock.stop();
|
clock.stop();
|
||||||
setCycle(0);
|
setCycle(0);
|
||||||
@@ -74,6 +78,10 @@ const setCycle = (cycle) => {
|
|||||||
num_cycles_at_cps_change = cycle;
|
num_cycles_at_cps_change = cycle;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setMaxCycle = (cycle) => {
|
||||||
|
max_cycle = cycle;
|
||||||
|
};
|
||||||
|
|
||||||
const processMessage = (message) => {
|
const processMessage = (message) => {
|
||||||
const { type, payload } = message;
|
const { type, payload } = message;
|
||||||
|
|
||||||
@@ -92,6 +100,10 @@ const processMessage = (message) => {
|
|||||||
setCycle(payload.cycle);
|
setCycle(payload.cycle);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'setmaxcycle': {
|
||||||
|
setMaxCycle(payload.maxcycle);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'toggle': {
|
case 'toggle': {
|
||||||
if (payload.started) {
|
if (payload.started) {
|
||||||
startClock(message.id);
|
startClock(message.id);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class Cyclist {
|
|||||||
onToggle,
|
onToggle,
|
||||||
onError,
|
onError,
|
||||||
getTime,
|
getTime,
|
||||||
latency = 0.1,
|
latency = 0.03,
|
||||||
setInterval,
|
setInterval,
|
||||||
clearInterval,
|
clearInterval,
|
||||||
beforeStart,
|
beforeStart,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function logger(message, type, data = {}) {
|
|||||||
}
|
}
|
||||||
lastMessage = message;
|
lastMessage = message;
|
||||||
lastTime = t;
|
lastTime = t;
|
||||||
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
console.log(`${t} %c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||||
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new CustomEvent(logKey, {
|
new CustomEvent(logKey, {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances.
|
neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances.
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/neocyclist.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ export class NeoCyclist {
|
|||||||
constructor({ onTrigger, onToggle, getTime }) {
|
constructor({ onTrigger, onToggle, getTime }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 0.5;
|
this.cps = 0.5;
|
||||||
|
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
this.time_at_last_tick_message = 0;
|
this.time_at_last_tick_message = 0;
|
||||||
// the clock of the worker and the audio context clock can drift apart over time
|
// the clock of the worker and the audio context clock can drift apart over time
|
||||||
@@ -19,16 +20,18 @@ export class NeoCyclist {
|
|||||||
// in order to schedule events consistently.
|
// in order to schedule events consistently.
|
||||||
this.collator = new ClockCollator({ getTargetClockTime: getTime });
|
this.collator = new ClockCollator({ getTargetClockTime: getTime });
|
||||||
this.onToggle = onToggle;
|
this.onToggle = onToggle;
|
||||||
this.latency = 0.1; // fixed trigger time offset
|
this.latency = -0.1; // fixed trigger time offset
|
||||||
this.cycle = 0;
|
this.cycle = 0;
|
||||||
|
this.maxcycle = null;
|
||||||
this.id = Math.round(Date.now() * Math.random());
|
this.id = Math.round(Date.now() * Math.random());
|
||||||
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
|
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
|
||||||
this.worker.port.start();
|
this.worker.port.start();
|
||||||
this.channel = new BroadcastChannel('strudeltick');
|
this.channel = new BroadcastChannel('strudeltick');
|
||||||
const tickCallback = (payload) => {
|
const tickCallback = (payload) => {
|
||||||
const { cps, begin, end, cycle, time } = payload;
|
const { cps, begin, end, cycle, maxcycle, time } = payload;
|
||||||
this.cps = cps;
|
this.cps = cps;
|
||||||
this.cycle = cycle;
|
this.cycle = cycle;
|
||||||
|
this.maxcycle = maxcycle;
|
||||||
const currentTime = this.collator.calculateOffset(time) + time;
|
const currentTime = this.collator.calculateOffset(time) + time;
|
||||||
processHaps(begin, end, currentTime);
|
processHaps(begin, end, currentTime);
|
||||||
this.time_at_last_tick_message = currentTime;
|
this.time_at_last_tick_message = currentTime;
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from './draw.mjs';
|
|||||||
export * from './pianoroll.mjs';
|
export * from './pianoroll.mjs';
|
||||||
export * from './spiral.mjs';
|
export * from './spiral.mjs';
|
||||||
export * from './pitchwheel.mjs';
|
export * from './pitchwheel.mjs';
|
||||||
|
export * from './textbox.mjs';
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { getTheme, getDrawContext } from './draw.mjs';
|
||||||
|
import { Pattern } from '@strudel/core';
|
||||||
|
|
||||||
|
export function textbox({ haps, ctx, id, margin = 10, fontsize = 24 } = {}) {
|
||||||
|
const w = ctx.canvas.width;
|
||||||
|
const h = ctx.canvas.height;
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
const color = getTheme().foreground;
|
||||||
|
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
if (hap.hasOnset()) {
|
||||||
|
const hapColor = hap.value.color || color;
|
||||||
|
ctx.strokeStyle = hapColor;
|
||||||
|
ctx.fillStyle = hapColor;
|
||||||
|
const { velocity = 1, gain = 1 } = hap.value || {};
|
||||||
|
const alpha = velocity * gain;
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.font = `${fontsize}px sans-serif`;
|
||||||
|
ctx.fillText(hap.value, 0, fontsize);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.textbox = function (options = {}) {
|
||||||
|
let { ctx = getDrawContext(), id = 1 } = options;
|
||||||
|
this.draw(
|
||||||
|
(haps, time) => {
|
||||||
|
textbox({
|
||||||
|
...options,
|
||||||
|
time,
|
||||||
|
ctx,
|
||||||
|
haps: haps.filter((hap) => hap.isActive(time)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lookbehind: 0,
|
||||||
|
lookahead: 0,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
@@ -153,7 +153,6 @@ samples('github:tidalcycles/dirt-samples')
|
|||||||
|
|
||||||
The format is `github:<user>/<repo>/<branch>`.
|
The format is `github:<user>/<repo>/<branch>`.
|
||||||
|
|
||||||
If `<repo>` and `<branch>` are not specified, they will default to `samples` and `main` respectively.
|
|
||||||
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
||||||
|
|
||||||
The format is also expected to be the same as explained above.
|
The format is also expected to be the same as explained above.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getCommonSampleInfoFromBank } from './util.mjs';
|
import { getCommonSampleInfo } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||||
@@ -23,34 +23,35 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSampleInfo(hapValue, bank) {
|
||||||
|
const { speed = 1.0 } = hapValue;
|
||||||
|
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||||
|
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
|
return { transpose, url, index, midi, label, playbackRate };
|
||||||
|
}
|
||||||
|
|
||||||
export const getSampleBuffer = async (label, sampleUrl, resolveUrl) => {
|
// takes hapValue and returns buffer + playbackRate.
|
||||||
|
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
||||||
|
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
||||||
if (resolveUrl) {
|
if (resolveUrl) {
|
||||||
sampleUrl = await resolveUrl(sampleUrl);
|
sampleUrl = await resolveUrl(sampleUrl);
|
||||||
}
|
}
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const buffer = await loadBuffer(sampleUrl, ac, label);
|
const buffer = await loadBuffer(sampleUrl, ac, label);
|
||||||
return buffer
|
|
||||||
};
|
|
||||||
|
|
||||||
function getBufferPlaybackRate(hapValue, buffer, transpose) {
|
|
||||||
const { speed = 1.0 } = hapValue;
|
|
||||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
|
||||||
if (hapValue.unit === 'c') {
|
if (hapValue.unit === 'c') {
|
||||||
playbackRate = playbackRate * buffer.duration;
|
playbackRate = playbackRate * buffer.duration;
|
||||||
}
|
}
|
||||||
return playbackRate;
|
return { buffer, playbackRate };
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// creates playback ready AudioBufferSourceNode from hapValue
|
// creates playback ready AudioBufferSourceNode from hapValue
|
||||||
export function getSampleBufferSource(hapValue, buffer,transpose) {
|
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
||||||
|
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
|
||||||
if (hapValue.speed < 0) {
|
if (hapValue.speed < 0) {
|
||||||
// should this be cached?
|
// should this be cached?
|
||||||
buffer = reverseBuffer(buffer);
|
buffer = reverseBuffer(buffer);
|
||||||
}
|
}
|
||||||
const playbackRate = getBufferPlaybackRate(hapValue, buffer, transpose)
|
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const bufferSource = ac.createBufferSource();
|
const bufferSource = ac.createBufferSource();
|
||||||
bufferSource.buffer = buffer;
|
bufferSource.buffer = buffer;
|
||||||
@@ -120,20 +121,13 @@ function githubPath(base, subpath = '') {
|
|||||||
if (!base.startsWith('github:')) {
|
if (!base.startsWith('github:')) {
|
||||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||||
}
|
}
|
||||||
let path = base.slice('github:'.length);
|
let [_, path] = base.split('github:');
|
||||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
if (path.split('/').length === 2) {
|
||||||
let components = path.split('/');
|
// assume main as default branch if none set
|
||||||
let user = components[0];
|
path += '/main';
|
||||||
let repo = components.length >= 2 ? components[1] : 'samples';
|
|
||||||
let branch = components.length >= 3 ? components[2] : 'main';
|
|
||||||
let other = components.slice(3);
|
|
||||||
if (subpath) {
|
|
||||||
other.push(subpath);
|
|
||||||
}
|
}
|
||||||
other = other.join('/');
|
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||||
|
|
||||||
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
||||||
@@ -263,7 +257,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
|||||||
|
|
||||||
const cutGroups = [];
|
const cutGroups = [];
|
||||||
|
|
||||||
export async function onTriggerSample(t, value, onended, bufferSrc) {
|
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||||
let {
|
let {
|
||||||
s,
|
s,
|
||||||
nudge = 0, // TODO: is this in seconds?
|
nudge = 0, // TODO: is this in seconds?
|
||||||
@@ -285,7 +279,7 @@ export async function onTriggerSample(t, value, onended, bufferSrc) {
|
|||||||
// destructure adsr here, because the default should be different for synths and samples
|
// destructure adsr here, because the default should be different for synths and samples
|
||||||
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||||
|
|
||||||
const { bufferSource, sliceDuration, offset } = bufferSrc
|
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
|
||||||
|
|
||||||
// asny stuff above took too long?
|
// asny stuff above took too long?
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
@@ -347,21 +341,14 @@ export async function onTriggerSample(t, value, onended, bufferSrc) {
|
|||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBufferSrcFromBank(hapValue, bank, resolveUrl = undefined) {
|
|
||||||
const { transpose, url, label } = getCommonSampleInfoFromBank(hapValue, bank)
|
|
||||||
let buffer = await getSampleBuffer(label,url, resolveUrl);
|
|
||||||
return getSampleBufferSource(hapValue, buffer, transpose)
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerSample(key, bank, params) {
|
function registerSample(key, bank, params) {
|
||||||
registerSound(key, async (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined)), {
|
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||||
type: 'sample',
|
type: 'sample',
|
||||||
samples: bank,
|
samples: bank,
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function registerSampleSource(key, bank, params) {
|
export function registerSampleSource(key, bank, params) {
|
||||||
const isWavetable = key.startsWith('wt_');
|
const isWavetable = key.startsWith('wt_');
|
||||||
if (isWavetable) {
|
if (isWavetable) {
|
||||||
|
|||||||
@@ -80,13 +80,12 @@ export function secondsToCycle(t, cps) {
|
|||||||
// deduces relevant info for sample loading from hap.value and sample definition
|
// deduces relevant info for sample loading from hap.value and sample definition
|
||||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||||
export function getCommonSampleInfoFromBank(hapValue, bank) {
|
export function getCommonSampleInfo(hapValue, bank) {
|
||||||
const { s, n = 0 } = hapValue;
|
const { s, n = 0 } = hapValue;
|
||||||
let midi = valueToMidi(hapValue, 36);
|
let midi = valueToMidi(hapValue, 36);
|
||||||
|
let transpose = midi - 36; // C3 is middle C;
|
||||||
let url;
|
let url;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let {transpose, label} = getCommonSampleInfo(hapValue)
|
|
||||||
|
|
||||||
if (Array.isArray(bank)) {
|
if (Array.isArray(bank)) {
|
||||||
index = getSoundIndex(n, bank.length);
|
index = getSoundIndex(n, bank.length);
|
||||||
url = bank[index];
|
url = bank[index];
|
||||||
@@ -103,20 +102,6 @@ export function getCommonSampleInfoFromBank(hapValue, bank) {
|
|||||||
index = getSoundIndex(n, bank[closest].length);
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
url = bank[closest][index];
|
url = bank[closest][index];
|
||||||
}
|
}
|
||||||
label = `${s}:${index}`;
|
const label = `${s}:${index}`;
|
||||||
return { transpose, url, label };
|
return { transpose, url, index, midi, label };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getCommonSampleInfo(hapValue) {
|
|
||||||
const { s, n = 0 } = hapValue;
|
|
||||||
let midi = valueToMidi(hapValue, 36);
|
|
||||||
let transpose = midi - 36; // C3 is middle C;
|
|
||||||
const label = `${s}:${n}`;
|
|
||||||
return { transpose, label };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getAudioContext, registerSound } from './index.mjs';
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
import { getCommonSampleInfoFromBank } from './util.mjs';
|
import { getCommonSampleInfo } from './util.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
applyParameterModulators,
|
applyParameterModulators,
|
||||||
@@ -216,7 +216,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
|
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
|
||||||
}
|
}
|
||||||
const frequency = getFrequencyFromValue(value);
|
const frequency = getFrequencyFromValue(value);
|
||||||
const { url, label } = getCommonSampleInfoFromBank(value, tables);
|
const { url, label } = getCommonSampleInfo(value, tables);
|
||||||
const payload = await getPayload(url, label, frameLen);
|
const payload = await getPayload(url, label, frameLen);
|
||||||
let holdEnd = t + duration;
|
let holdEnd = t + duration;
|
||||||
if (clip !== undefined) {
|
if (clip !== undefined) {
|
||||||
@@ -231,12 +231,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
begin: t,
|
begin: t,
|
||||||
end: envEnd,
|
end: envEnd,
|
||||||
frequency,
|
frequency,
|
||||||
freqspread: value.detune,
|
detune: value.detune,
|
||||||
position: value.wt,
|
position: value.wt,
|
||||||
warp: value.warp,
|
warp: value.warp,
|
||||||
warpMode: warpmode,
|
warpMode: warpmode,
|
||||||
voices: Math.max(value.unison ?? 1, 1),
|
voices: Math.max(value.unison ?? 1, 1),
|
||||||
panspread: value.spread,
|
spread: value.spread,
|
||||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||||
},
|
},
|
||||||
{ outputChannelCount: [2] },
|
{ outputChannelCount: [2] },
|
||||||
|
|||||||
@@ -1050,15 +1050,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
return [
|
return [
|
||||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
|
||||||
{ name: 'detune', defaultValue: 0 },
|
{ name: 'detune', defaultValue: 0.18 },
|
||||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
|
||||||
{ name: 'warpMode', defaultValue: 0 },
|
{ name: 'warpMode', defaultValue: 0 },
|
||||||
{ name: 'voices', defaultValue: 1, min: 1 },
|
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
|
||||||
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
|
||||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1236,12 +1235,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
_sampleFrame(frame, phase) {
|
_sampleFrame(frame, phase) {
|
||||||
const len = frame.length;
|
const len = frame.length;
|
||||||
const pos = phase * len;
|
const pos = phase * len;
|
||||||
let i = pos | 0;
|
const i = pos | 0;
|
||||||
if (i >= len) i = 0; // fast wrap
|
|
||||||
const frac = pos - i;
|
const frac = pos - i;
|
||||||
const a = frame[i];
|
const a = frame[i];
|
||||||
let i1 = i + 1;
|
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
|
||||||
if (i1 >= len) i1 = 0;
|
|
||||||
const b = frame[i1];
|
const b = frame[i1];
|
||||||
return a + (b - a) * frac;
|
return a + (b - a) * frac;
|
||||||
}
|
}
|
||||||
@@ -1271,7 +1268,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
for (let i = 0; i < outL.length; i++) {
|
for (let i = 0; i < outL.length; i++) {
|
||||||
const detune = pv(parameters.detune, i);
|
const detune = pv(parameters.detune, i);
|
||||||
const freqspread = pv(parameters.freqspread, i);
|
|
||||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||||
const idx = tablePos * (this.numFrames - 1);
|
const idx = tablePos * (this.numFrames - 1);
|
||||||
const fIdx = idx | 0;
|
const fIdx = idx | 0;
|
||||||
@@ -1280,9 +1276,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
const warpMode = pv(parameters.warpMode, i);
|
const warpMode = pv(parameters.warpMode, i);
|
||||||
const voices = pv(parameters.voices, i);
|
const voices = pv(parameters.voices, i);
|
||||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0;
|
||||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
const gain1 = Math.sqrt(0.5 - 0.5 * spread);
|
||||||
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
const gain2 = Math.sqrt(0.5 + 0.5 * spread);
|
||||||
let f = pv(parameters.frequency, i);
|
let f = pv(parameters.frequency, i);
|
||||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||||
const normalizer = 1 / Math.sqrt(voices);
|
const normalizer = 1 / Math.sqrt(voices);
|
||||||
@@ -1295,7 +1291,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
gainL = gain2;
|
gainL = gain2;
|
||||||
gainR = gain1;
|
gainR = gain1;
|
||||||
}
|
}
|
||||||
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
|
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
|
||||||
const dPhase = fVoice * this.invSR;
|
const dPhase = fVoice * this.invSR;
|
||||||
const level = this._chooseMip(dPhase);
|
const level = this._chooseMip(dPhase);
|
||||||
const table = this.tables[level];
|
const table = this.tables[level];
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
version="1.0"
|
||||||
|
id="Layer_1"
|
||||||
|
width="284.46"
|
||||||
|
height="284.46"
|
||||||
|
viewBox="0 0 284.46 284.46"
|
||||||
|
overflow="visible"
|
||||||
|
enable-background="new 0 0 284.46 284.46"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:version="0.32"
|
||||||
|
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
|
||||||
|
sodipodi:docname="encoder_disc.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
|
||||||
|
id="metadata2068"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs2066">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</defs><sodipodi:namedview
|
||||||
|
inkscape:window-height="1131"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
borderopacity="1.0"
|
||||||
|
bordercolor="#666666"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
id="base"
|
||||||
|
inkscape:zoom="1.0549412"
|
||||||
|
inkscape:cx="195.27154"
|
||||||
|
inkscape:cy="-101.90141"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:current-layer="Layer_1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<path
|
||||||
|
id="path1"
|
||||||
|
style="fill:#f9f9f9;stroke:none;stroke-width:1.23072"
|
||||||
|
d="M 142.22902,0.61574073 A 141.61462,141.61462 0 0 0 0.61574173,142.22901 141.61462,141.61462 0 0 0 142.22902,283.84425 141.61462,141.61462 0 0 0 283.84425,142.22901 141.61462,141.61462 0 0 0 142.22902,0.61574073 Z m 0,106.22069927 a 35.393924,35.393924 0 0 1 35.39453,35.39257 35.393924,35.393924 0 0 1 -35.39453,35.39455 35.393924,35.393924 0 0 1 -35.39258,-35.39455 35.393924,35.393924 0 0 1 35.39258,-35.39257 z" /><g
|
||||||
|
id="g2"><path
|
||||||
|
d="M 242.47,41.99 242.441,42.02 217.4,67.06 C 198.17,47.83 171.589,35.93 142.23,35.93 V 0.5 c 39.17,0 74.6,15.85 100.24,41.49 z"
|
||||||
|
id="path2015" /><path
|
||||||
|
d="m 142.23,0.5 v 35.43 c -29.37,0 -55.93,11.89 -75.17,31.13 L 42.02,42.02 41.99,41.99 C 67.63,16.35 103.06,0.5 142.23,0.5 Z"
|
||||||
|
id="path2017" /><path
|
||||||
|
d="m 142.23,35.93 v 35.43 c -19.59,0 -37.3,7.92 -50.12,20.75 L 67.06,67.06 C 86.3,47.82 112.86,35.93 142.23,35.93 Z"
|
||||||
|
id="path2019" /><path
|
||||||
|
d="M 67.06,67.06 92.11,92.11 C 79.28,104.93 71.36,122.64 71.36,142.23 H 35.93 c 0,-29.36 11.89,-55.94 31.13,-75.17 z"
|
||||||
|
id="path2021" /><path
|
||||||
|
d="M 92.11,192.35 67.06,217.4 C 47.82,198.171 35.93,171.59 35.93,142.23 h 35.43 c 0,19.589 7.92,37.3 20.75,50.12 z"
|
||||||
|
id="path2023" /><path
|
||||||
|
d="m 142.23,213.09 v 35.44 c -29.37,0 -55.93,-11.891 -75.17,-31.131 l 25.05,-25.05 c 12.82,12.821 30.53,20.741 50.12,20.741 z"
|
||||||
|
id="path2025" /><path
|
||||||
|
d="m 142.23,248.53 v 35.43 c -39.17,0 -74.6,-15.851 -100.24,-41.49 l 0.03,-0.03 25.04,-25.04 c 19.24,19.24 45.8,31.13 75.17,31.13 z"
|
||||||
|
id="path2027" /><path
|
||||||
|
d="m 142.23,283.96 v -35.43 c 29.36,0 55.94,-11.9 75.17,-31.131 l 25.04,25.04 0.029,0.03 C 216.83,268.109 181.4,283.96 142.23,283.96 Z"
|
||||||
|
id="path2029" /><path
|
||||||
|
d="m 177.66,142.229 h 35.43 c 0,19.59 -7.92,37.301 -20.74,50.12 L 167.3,167.3 c 6.4,-6.401 10.36,-15.25 10.36,-25.071 z"
|
||||||
|
id="path2031" /><path
|
||||||
|
d="m 167.3,167.3 25.05,25.05 c -12.819,12.82 -30.529,20.74 -50.12,20.74 v -35.43 c 9.8,0 18.66,-3.95 25.07,-10.36 z"
|
||||||
|
id="path2033" /><path
|
||||||
|
d="m 142.23,177.66 v 35.43 c -19.59,0 -37.3,-7.92 -50.12,-20.74 l 25.05,-25.04 c 6.42,6.41 15.28,10.35 25.07,10.35 z"
|
||||||
|
id="path2035" /><path
|
||||||
|
d="m 117.16,167.3 v 0.01 L 92.11,192.35 C 79.28,179.531 71.36,161.82 71.36,142.23 h 35.43 c 0,9.82 3.96,18.669 10.37,25.07 z"
|
||||||
|
id="path2037" /><path
|
||||||
|
fill="none"
|
||||||
|
stroke="#000000"
|
||||||
|
d="m 142.23,283.96 c -39.17,0 -74.6,-15.851 -100.24,-41.49 C 16.35,216.83 0.5,181.399 0.5,142.229 0.5,103.059 16.35,67.629 41.99,41.989 67.63,16.35 103.06,0.5 142.23,0.5 c 39.17,0 74.6,15.85 100.24,41.49 25.641,25.64 41.49,61.07 41.49,100.24 0,39.17 -15.85,74.601 -41.49,100.24 -25.64,25.639 -61.07,41.49 -100.24,41.49 z"
|
||||||
|
id="path2041" /><path
|
||||||
|
fill="none"
|
||||||
|
stroke="#000000"
|
||||||
|
d="m 248.53,142.229 c 0,-29.35 -11.891,-55.93 -31.13,-75.17 -19.23,-19.23 -45.811,-31.13 -75.17,-31.13 -29.37,0 -55.93,11.89 -75.17,31.13 -19.24,19.23 -31.13,45.81 -31.13,75.17 0,29.36 11.89,55.94 31.13,75.17 19.24,19.24 45.8,31.131 75.17,31.131 29.36,0 55.94,-11.9 75.17,-31.131 19.24,-19.239 31.13,-45.819 31.13,-75.17 z"
|
||||||
|
id="path2043" /><path
|
||||||
|
fill="none"
|
||||||
|
stroke="#000000"
|
||||||
|
d="m 142.23,213.09 c -19.59,0 -37.3,-7.92 -50.12,-20.74 -12.83,-12.819 -20.75,-30.53 -20.75,-50.12 0,-19.59 7.92,-37.3 20.75,-50.12 12.82,-12.83 30.53,-20.75 50.12,-20.75 19.59,0 37.3,7.92 50.12,20.75 12.82,12.82 20.74,30.53 20.74,50.12 0,19.59 -7.92,37.301 -20.74,50.12 -12.82,12.82 -30.53,20.74 -50.12,20.74 z"
|
||||||
|
id="path2045" /><path
|
||||||
|
fill="none"
|
||||||
|
stroke="#000000"
|
||||||
|
d="m 117.16,167.31 c 6.42,6.41 15.28,10.351 25.07,10.351 9.8,0 18.66,-3.95 25.07,-10.36 6.4,-6.4 10.36,-15.25 10.36,-25.07 0,-9.819 -3.95,-18.67 -10.36,-25.07 -6.399,-6.41 -15.27,-10.36 -25.07,-10.36 -9.8,0 -18.66,3.95 -25.08,10.35 -6.4,6.4 -10.36,15.26 -10.36,25.08 0,9.82 3.96,18.67 10.37,25.07"
|
||||||
|
id="path2047" /><path
|
||||||
|
id="polyline2049"
|
||||||
|
style="fill:none;stroke:#000000"
|
||||||
|
d="m 142.23,177.66 v 35.43 35.44 35.43"
|
||||||
|
sodipodi:nodetypes="cccc" /><path
|
||||||
|
id="polyline2051"
|
||||||
|
style="fill:none;stroke:#000000"
|
||||||
|
d="m 167.3,167.3 25.05,25.05 25.05,25.049 25.04,25.04"
|
||||||
|
sodipodi:nodetypes="cccc" /><path
|
||||||
|
id="polyline2053"
|
||||||
|
style="fill:none;stroke:#000000"
|
||||||
|
d="m 177.66,142.229 h 35.43 35.44 35.43"
|
||||||
|
sodipodi:nodetypes="cccc" /><path
|
||||||
|
id="polyline2055"
|
||||||
|
style="fill:none;stroke:#000000"
|
||||||
|
d="M 167.3,117.16 192.35,92.11 217.4,67.06 242.44,42.02"
|
||||||
|
sodipodi:nodetypes="cccc" /><path
|
||||||
|
id="polyline2057"
|
||||||
|
style="fill:none;stroke:#000000"
|
||||||
|
d="M 142.23,106.8 V 71.36 35.93 0.5"
|
||||||
|
sodipodi:nodetypes="cccc" /><path
|
||||||
|
id="polyline2059"
|
||||||
|
style="fill:none;stroke:#000000"
|
||||||
|
d="M 117.15,117.15 92.11,92.11 67.06,67.06 42.02,42.02"
|
||||||
|
sodipodi:nodetypes="cccc" /><path
|
||||||
|
id="polyline2061"
|
||||||
|
style="fill:none;stroke:#000000"
|
||||||
|
d="M 106.79,142.229 H 71.36 35.93 0.5"
|
||||||
|
sodipodi:nodetypes="cccc" /><path
|
||||||
|
id="polyline2063"
|
||||||
|
style="fill:none;stroke:#000000"
|
||||||
|
d="m 117.16,167.3 v 0.01 l -25.05,25.04 -25.05,25.049 -25.04,25.04"
|
||||||
|
sodipodi:nodetypes="ccccc" /></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,68 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function Tap({ initialCps = 0.4, maxSamples = 3 }) {
|
||||||
|
const [timestamps, setTimestamps] = useState([]);
|
||||||
|
const [cps, setCps] = useState(initialCps);
|
||||||
|
|
||||||
|
function addTap(ts = Date.now()) {
|
||||||
|
setTimestamps((prev) => {
|
||||||
|
const next = [...prev, ts].slice(-(maxSamples + 1));
|
||||||
|
calcCps(next);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcCps(times) {
|
||||||
|
if (!times || times.length < 2) return;
|
||||||
|
const intervals = [];
|
||||||
|
for (let i = 1; i < times.length; i++) {
|
||||||
|
intervals.push(times[i] - times[i - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const avgMs = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
||||||
|
const newCps = 1000 / avgMs;
|
||||||
|
if (Number.isFinite(newCps) && newCps > 0 && newCps < 1000) {
|
||||||
|
setCps(newCps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTap(e) {
|
||||||
|
e && e.preventDefault();
|
||||||
|
addTap();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset(e) {
|
||||||
|
e && e.preventDefault();
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKey(e) {
|
||||||
|
if (e.code === 'Space') {
|
||||||
|
e.preventDefault();
|
||||||
|
addTap();
|
||||||
|
} else if (e.code === 'Backspace') {
|
||||||
|
e.preventDefault();
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
setTimestamps([]);
|
||||||
|
// setCps(initialCps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-col items-center p-7 font-medium text-white">
|
||||||
|
<div>{cps.toFixed(2)} cps</div>
|
||||||
|
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={handleTap}>
|
||||||
|
Tap
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
Vinyl.jsx - <short description TODO>
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/App.js>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { setInterval, clearInterval } from 'worker-timers';
|
||||||
|
import { NeoCyclist } from '@strudel/core/neocyclist.mjs';
|
||||||
|
import { getAudioContext } from '@strudel/webaudio';
|
||||||
|
import { saw } from '@strudel/core';
|
||||||
|
import encoder from '../../public/encoder_disc.svg';
|
||||||
|
|
||||||
|
console.log(encoder);
|
||||||
|
|
||||||
|
const schedulerOptions = {
|
||||||
|
onTrigger: (x) => {},
|
||||||
|
getTime: () => getAudioContext().currentTime,
|
||||||
|
onToggle: (started) => console.log('started: ', started),
|
||||||
|
setInterval,
|
||||||
|
clearInterval,
|
||||||
|
// beforeStart,
|
||||||
|
};
|
||||||
|
const cyclist = new NeoCyclist(schedulerOptions);
|
||||||
|
|
||||||
|
export function Vinyl() {
|
||||||
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
const [cyclepos, setCyclepos] = useState(0);
|
||||||
|
const activate = () => setIsActive(!isActive);
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
if (!cyclist.started) {
|
||||||
|
cyclist.start();
|
||||||
|
// cyclist.setPattern(saw.segment(8));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cyclist.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
setCyclepos(cyclist.cycle);
|
||||||
|
}, 10);
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deg = (cyclepos % 1) * 360;
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
fontSize: '20em',
|
||||||
|
transform: 'rotate(' + deg + 'deg)',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div onClick={activate} style={style}>
|
||||||
|
<center>
|
||||||
|
<img src={encoder.src} />
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import HeadCommon from '../components/HeadCommon.astro';
|
||||||
|
import { Vinyl } from '../components/Vinyl.jsx';
|
||||||
|
import Tap from '../components/Tap.jsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en" class="m-0 dark">
|
||||||
|
<head>
|
||||||
|
<HeadCommon />
|
||||||
|
<title>Strudel Cycler</title>
|
||||||
|
</head>
|
||||||
|
<body class="h-app-height bg-background m-0">
|
||||||
|
<div>Hello there.</div>
|
||||||
|
<Vinyl client:only="react" />
|
||||||
|
<Tap client:only="react" />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -60,23 +60,6 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp
|
|||||||
.room(0.5)`}
|
.room(0.5)`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
## Write your own chained function
|
|
||||||
|
|
||||||
You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function.
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
client:idle
|
|
||||||
tune={`const effectChain = register('effectChain', (pat) => pat
|
|
||||||
.s("sawtooth")
|
|
||||||
.cutoff(500)
|
|
||||||
//.delay(0.5)
|
|
||||||
.room(0.5)
|
|
||||||
)
|
|
||||||
note("a3 c#4 e4 a4").effectChain()`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
Try adding `.rev()` after `effectChain()` to hear further effects added.
|
|
||||||
|
|
||||||
# Comments
|
# Comments
|
||||||
|
|
||||||
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
|||||||
|
|
||||||
### scrub
|
### scrub
|
||||||
|
|
||||||
<JsDoc client:idle name="Pattern.scrub" h={0} />
|
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
|
||||||
|
|
||||||
### speed
|
### speed
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { getSampleBufferSource, onTriggerSample, registerSampleSource } from '@strudel/webaudio';
|
import { registerSampleSource } from '@strudel/webaudio';
|
||||||
import { isAudioFile } from './files.mjs';
|
import { isAudioFile } from './files.mjs';
|
||||||
import { getSoundIndex, logger } from '@strudel/core';
|
import { logger } from '@strudel/core';
|
||||||
import { registerSound } from '@strudel/webaudio';
|
|
||||||
import { getCommonSampleInfo } from '../../../packages/superdough/util.mjs';
|
|
||||||
|
|
||||||
//utilites for writing and reading to the indexdb
|
//utilites for writing and reading to the indexdb
|
||||||
|
|
||||||
@@ -27,15 +25,6 @@ function clearAllIDB() {
|
|||||||
|
|
||||||
export function clearIDB(dbName) {
|
export function clearIDB(dbName) {
|
||||||
return window.indexedDB.deleteDatabase(dbName);
|
return window.indexedDB.deleteDatabase(dbName);
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function registerSampleFromIdb(key, bank, params) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// queries the DB, and registers the sounds so they can be played
|
// queries the DB, and registers the sounds so they can be played
|
||||||
@@ -63,76 +52,31 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
if (!isAudioFile(title)) {
|
if (!isAudioFile(title)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitRelativePath = soundFile.id.split('/');
|
const splitRelativePath = soundFile.id.split('/');
|
||||||
let parentDirectory =
|
let parentDirectory =
|
||||||
//fallback to file name before period and seperator if no parent directory
|
//fallback to file name before period and seperator if no parent directory
|
||||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||||
|
const blob = soundFile.blob;
|
||||||
|
|
||||||
// const blob = soundFile.blob;
|
return blobToDataUrl(blob).then((soundPath) => {
|
||||||
|
|
||||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
||||||
|
|
||||||
titlePathMap.set(title, soundFile.id);
|
titlePathMap.set(title, soundPath);
|
||||||
|
|
||||||
sounds.set(parentDirectory, titlePathMap);
|
sounds.set(parentDirectory, titlePathMap);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// return blobToDataUrl(blob).then((soundPath) => {
|
|
||||||
// const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
|
||||||
|
|
||||||
// titlePathMap.set(title, soundPath);
|
|
||||||
|
|
||||||
// sounds.set(parentDirectory, titlePathMap);
|
|
||||||
// return;
|
|
||||||
// });
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
sounds.forEach((titlePathMap, key) => {
|
sounds.forEach((titlePathMap, key) => {
|
||||||
const bank = Array.from(titlePathMap.keys())
|
const value = Array.from(titlePathMap.keys())
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
})
|
})
|
||||||
.map((title) => titlePathMap.get(title));
|
.map((title) => titlePathMap.get(title));
|
||||||
|
|
||||||
|
registerSampleSource(key, value, { prebake: false });
|
||||||
|
|
||||||
|
|
||||||
registerSound(key, async (t, hapValue, onended) => {
|
|
||||||
const { s, n = 0 } = hapValue;
|
|
||||||
const index = getSoundIndex(n, bank.length);
|
|
||||||
let {transpose, label} = getCommonSampleInfo(hapValue)
|
|
||||||
const storeKey = bank[index];
|
|
||||||
|
|
||||||
openDB(config, (objectStore) => {
|
|
||||||
const getRequest = objectStore.get(storeKey);
|
|
||||||
getRequest.onsuccess = async (event) => {
|
|
||||||
const result = event.target.result;
|
|
||||||
let buffer = result?.blob.arrayBuffer ? await result.blob.arrayBuffer() : null;
|
|
||||||
console.info(buffer)
|
|
||||||
if (buffer) {
|
|
||||||
const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
|
|
||||||
onTriggerSample(t, hapValue, onended, bufferSource)
|
|
||||||
} else {
|
|
||||||
logger(`Could not load sample for ${storeKey}`, 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
|
|
||||||
// const buffer = objectStore.getKey(key)
|
|
||||||
// const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
|
|
||||||
|
|
||||||
// onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined))
|
|
||||||
|
|
||||||
}, {
|
|
||||||
type: 'sample',
|
|
||||||
samples: bank,
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// registerSampleSource(key, value, { prebake: false });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger('imported sounds registered!', 'success');
|
logger('imported sounds registered!', 'success');
|
||||||
|
|||||||
Reference in New Issue
Block a user