Merge pull request #1354 from tidalcycles/jade/bytebeats
feat: Byte Beats!
This commit is contained in:
commit
faa60be211
5 changed files with 173 additions and 3 deletions
|
|
@ -445,6 +445,19 @@ export const { coarse } = registerControl('coarse');
|
||||||
*/
|
*/
|
||||||
export const { drive } = registerControl('drive');
|
export const { drive } = registerControl('drive');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create byte beats with custom expressions
|
||||||
|
*
|
||||||
|
* @name byteBeatExpression
|
||||||
|
* @synonyms bbexpr
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} byteBeatExpression pattern the output channels
|
||||||
|
* @example
|
||||||
|
* s("bytebeat").bbexpr('t*(t>>15^t>>66)')
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows you to set the output channels on the interface
|
* Allows you to set the output channels on the interface
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ const defaultDefaultValues = {
|
||||||
shapevol: 1,
|
shapevol: 1,
|
||||||
distortvol: 1,
|
distortvol: 1,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
byteBeatExpression: '0',
|
||||||
delayfeedback: 0.5,
|
delayfeedback: 0.5,
|
||||||
delaytime: 0.25,
|
delaytime: 0.25,
|
||||||
orbit: 1,
|
orbit: 1,
|
||||||
|
|
@ -445,7 +446,7 @@ function mapChannelNumbers(channels) {
|
||||||
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const superdough = async (value, t, hapDuration) => {
|
export const superdough = async (value, t, hapDuration, cps) => {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
||||||
let { stretch } = value;
|
let { stretch } = value;
|
||||||
|
|
@ -475,6 +476,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||||
let {
|
let {
|
||||||
s = getDefaultValue('s'),
|
s = getDefaultValue('s'),
|
||||||
bank,
|
bank,
|
||||||
|
byteBeatExpression,
|
||||||
source,
|
source,
|
||||||
gain = getDefaultValue('gain'),
|
gain = getDefaultValue('gain'),
|
||||||
postgain = getDefaultValue('postgain'),
|
postgain = getDefaultValue('postgain'),
|
||||||
|
|
@ -578,7 +580,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||||
// get source AudioNode
|
// get source AudioNode
|
||||||
let sourceNode;
|
let sourceNode;
|
||||||
if (source) {
|
if (source) {
|
||||||
sourceNode = source(t, value, hapDuration);
|
sourceNode = source(t, value, hapDuration, cps);
|
||||||
} else if (getSound(s)) {
|
} else if (getSound(s)) {
|
||||||
const { onTrigger } = getSound(s);
|
const { onTrigger } = getSound(s);
|
||||||
const onEnded = () => {
|
const onEnded = () => {
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,65 @@ export function registerSynthSounds() {
|
||||||
{ prebake: true, type: 'synth' },
|
{ prebake: true, type: 'synth' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
registerSound(
|
||||||
|
'bytebeat',
|
||||||
|
(begin, value, onended) => {
|
||||||
|
const defaultBeats = ['(t%255 >= t/255%255)*255', '(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400', 't'];
|
||||||
|
const { n } = value;
|
||||||
|
const { byteBeatExpression = defaultBeats[n % defaultBeats.length] } = value;
|
||||||
|
const ac = getAudioContext();
|
||||||
|
|
||||||
|
let { duration } = value;
|
||||||
|
const [attack, decay, sustain, release] = getADSRValues(
|
||||||
|
[value.attack, value.decay, value.sustain, value.release],
|
||||||
|
'linear',
|
||||||
|
[0.001, 0.05, 0.6, 0.01],
|
||||||
|
);
|
||||||
|
const holdend = begin + duration;
|
||||||
|
const end = holdend + release + 0.01;
|
||||||
|
const frequency = getFrequencyFromValue(value);
|
||||||
|
|
||||||
|
let o = getWorklet(
|
||||||
|
ac,
|
||||||
|
'byte-beat-processor',
|
||||||
|
{
|
||||||
|
frequency,
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
outputChannelCount: [2],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
o.port.postMessage(byteBeatExpression);
|
||||||
|
|
||||||
|
let envGain = gainNode(1);
|
||||||
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||||
|
|
||||||
|
let timeoutNode = webAudioTimeout(
|
||||||
|
ac,
|
||||||
|
() => {
|
||||||
|
destroyAudioWorkletNode(o);
|
||||||
|
envGain.disconnect();
|
||||||
|
onended();
|
||||||
|
},
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: envGain,
|
||||||
|
stop: (time) => {
|
||||||
|
timeoutNode.stop(time);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ prebake: true, type: 'synth' },
|
||||||
|
);
|
||||||
|
|
||||||
registerSound(
|
registerSound(
|
||||||
'pulse',
|
'pulse',
|
||||||
(begin, value, onended) => {
|
(begin, value, onended) => {
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,7 @@ class LadderProcessor extends AudioWorkletProcessor {
|
||||||
cutoff = (cutoff * 2 * _PI) / sampleRate;
|
cutoff = (cutoff * 2 * _PI) / sampleRate;
|
||||||
cutoff = cutoff > 1 ? 1 : cutoff;
|
cutoff = cutoff > 1 ? 1 : cutoff;
|
||||||
|
|
||||||
const k = Math.min(8, resonance * 0.4);
|
const k = Math.min(8, resonance * 0.13);
|
||||||
// drive makeup * resonance volume loss makeup
|
// drive makeup * resonance volume loss makeup
|
||||||
let makeupgain = (1 / drive) * Math.min(1.75, 1 + k);
|
let makeupgain = (1 / drive) * Math.min(1.75, 1 + k);
|
||||||
|
|
||||||
|
|
@ -761,3 +761,90 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
|
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
|
||||||
|
|
||||||
|
class ByteBeatProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.codeText = '0';
|
||||||
|
this.port.onmessage = (event) => {
|
||||||
|
this.codeText = event.data
|
||||||
|
.trim()
|
||||||
|
.replace(
|
||||||
|
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
|
||||||
|
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
this.virtualRate = 112600; // target sample rate
|
||||||
|
this.t = null;
|
||||||
|
this.framebuffer = new Float32Array(Math.floor(sampleRate / 60));
|
||||||
|
this.func = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get parameterDescriptors() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'begin',
|
||||||
|
defaultValue: 0,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'frequency',
|
||||||
|
defaultValue: 440,
|
||||||
|
min: Number.EPSILON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'detune',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: Number.NEGATIVE_INFINITY,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'end',
|
||||||
|
defaultValue: 0,
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, params) {
|
||||||
|
if (this.disconnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentTime <= params.begin[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (currentTime >= params.end[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.t == null) {
|
||||||
|
this.t = params.begin[0] * this.virtualRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
let codeText = this.codeText;
|
||||||
|
this.func = Function('t', 'return ' + codeText);
|
||||||
|
const output = outputs[0];
|
||||||
|
const tIncrement = this.virtualRate / sampleRate;
|
||||||
|
|
||||||
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
|
let t = Math.floor(this.t);
|
||||||
|
const detune = getParamValue(i, params.detune);
|
||||||
|
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
|
||||||
|
|
||||||
|
t = (t / (this.virtualRate / 256)) * freq;
|
||||||
|
|
||||||
|
const funcValue = this.func(t);
|
||||||
|
let signal = (funcValue & 255) / 127.5 - 1;
|
||||||
|
const out = signal * 0.2;
|
||||||
|
for (let c = 0; c < output.length; c++) {
|
||||||
|
output[c][i] = out;
|
||||||
|
}
|
||||||
|
this.t = this.t + tIncrement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // keep the audio processing going
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
||||||
|
|
|
||||||
|
|
@ -1516,6 +1516,15 @@ exports[`runs examples > example "brandBy" example index 0 1`] = `
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "byteBeatExpression" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
|
||||||
|
"[ 1/1 → 2/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
|
||||||
|
"[ 2/1 → 3/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
|
||||||
|
"[ 3/1 → 4/1 | s:bytebeat byteBeatExpression:t*(t>>15^t>>66) ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "cat" example index 0 1`] = `
|
exports[`runs examples > example "cat" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/1 | note:e5 ]",
|
"[ 0/1 → 1/1 | note:e5 ]",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue