Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gain Modulation with calculated modulation per event #1095

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
55 changes: 54 additions & 1 deletion packages/core/controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,60 @@ export const { crush } = registerControl('crush');
*/
export const { coarse } = registerControl('coarse');

/**
* modulate the amplitude of a sound with a continuous waveform
*
* @name am
* @synonyms tremelo
* @param {number | Pattern} speed modulation speed in cycles
* @example
* s("triangle").am("2").amshape("<tri saw ramp square>").amdepth(.5)
*
*/
export const { am, tremolo } = registerControl(['am', 'amdepth', 'amskew', 'amphase'], 'tremolo');

/**
* depth of amplitude modulation
*
* @name amdepth
* @param {number | Pattern} depth
* @example
* s("triangle").am(1).amdepth("1")
*
*/
export const { amdepth } = registerControl('amdepth');
/**
* alter the shape of the modulation waveform
*
* @name amskew
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
* @example
* note("{f a c e}%16").am(4).amskew("<.5 0 1>")
*
*/
export const { amskew } = registerControl('amskew');

/**
* alter the phase of the modulation waveform
*
* @name amphase
* @param {number | Pattern} offset the offset in cycles of the modulation
* @example
* note("{f a c e}%16").am(4).amphase("<0 .25 .66>")
*
*/
export const { amphase } = registerControl('amphase');

/**
* shape of amplitude modulation
*
* @name amshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
* note("{f g c d}%16").am(4).amshape("ramp").s("sawtooth")
*
*/
export const { amshape } = registerControl('amshape');
/**
* Allows you to set the output channels on the interface
*
Expand Down Expand Up @@ -1529,7 +1583,6 @@ export const { zmod } = registerControl('zmod');
// like crush but scaled differently
export const { zcrush } = registerControl('zcrush');
export const { zdelay } = registerControl('zdelay');
export const { tremolo } = registerControl('tremolo');
export const { zzfx } = registerControl('zzfx');

/**
Expand Down
86 changes: 43 additions & 43 deletions packages/core/cyclist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,61 +8,51 @@ import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';

export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) {
constructor({ interval = 0.05, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) {
this.started = false;
this.cps = 0.5;
this.num_ticks_since_cps_change = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
this.time_at_last_tick_message = 0;
this.cycle = 0;
this.getTime = getTime; // get absolute time
this.num_cycles_at_cps_change = 0;
this.seconds_at_cps_change; // clock phase when cps was changed
this.num_ticks_since_cps_change = 0;
this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset

this.interval = interval;

this.clock = createClock(
getTime,
// called slightly before each cycle
(phase, duration, _, t) => {
if (this.num_ticks_since_cps_change === 0) {
this.num_cycles_at_cps_change = this.lastEnd;
this.seconds_at_cps_change = phase;
(phase, duration, _, time) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the idea behind these changes? I can't quite figure out the intention by just looking at the code o_O

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cycle calculation has to be adjusted slightly to compensate for when the event is actually played so that the phase of the modulation can be lined up. I already had made the cycle calculation on the neocyclist, so I just brought that logic over as well as the setCycle function. I could not figure out how to get the cycle offset adjustment to calculate properly with current cyclist implementation. Maybe you might know a better way to get this value?

if (this.started === false) {
return;
}
this.num_ticks_since_cps_change++;
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
const num_seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
const tickdeadline = phase - time;
const lastTick = time + tickdeadline;
const num_cycles_since_cps_change = num_seconds_since_cps_change * this.cps;
const begin = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
const secondsSinceLastTick = time - lastTick - duration;
const eventLength = duration * this.cps;
const end = begin + eventLength;
this.cycle = begin + secondsSinceLastTick * this.cps;

try {
const begin = this.lastEnd;
this.lastBegin = begin;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
this.lastEnd = end;
this.lastTick = phase;
//account for latency and tick duration when using cycle calculations for audio downstream
const cycle_gap = (this.latency - duration) * this.cps;

if (phase < t) {
// avoid querying haps that are in the past anyway
console.log(`skip query: too late`);
return;
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
haps.forEach((hap) => {
if (hap.hasOnset()) {
let targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps;
targetTime = targetTime + this.latency + tickdeadline + time - num_seconds_since_cps_change;
const duration = hap.duration / this.cps;
onTrigger?.(hap, tickdeadline, duration, this.cps, targetTime, this.cycle - cycle_gap);
}

// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });

haps.forEach((hap) => {
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
const duration = hap.duration / this.cps;
// the following line is dumb and only here for backwards compatibility
// see https://github.com/tidalcycles/strudel/pull/1004
const deadline = targetTime - phase;
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
}
});
} catch (e) {
logger(`[cyclist] error: ${e.message}`);
onError?.(e);
}
});
this.time_at_last_tick_message = time;
this.num_ticks_since_cps_change++;
},
interval, // duration of each cycle
0.1,
Expand All @@ -75,16 +65,24 @@ export class Cyclist {
if (!this.started) {
return 0;
}
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps;
return this.cycle + gap;
}

setCycle = (cycle) => {
this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = cycle;
};
setStarted(v) {
this.started = v;

this.setCycle(0);
this.onToggle?.(v);
}
start() {
this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = 0;

if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
Expand Down Expand Up @@ -113,6 +111,8 @@ export class Cyclist {
if (this.cps === cps) {
return;
}
const num_seconds_since_cps_change = this.num_ticks_since_cps_change * this.interval;
this.num_cycles_at_cps_change = this.num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
this.cps = cps;
this.num_ticks_since_cps_change = 0;
}
Expand Down
5 changes: 1 addition & 4 deletions packages/core/neocyclist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
this.started = false;
this.cps = 0.5;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;

this.num_cycles_at_cps_change = 0;
this.onToggle = onToggle;
this.latency = 0.1; // fixed trigger time offset
this.cycle = 0;
Expand Down Expand Up @@ -86,7 +83,7 @@ export class NeoCyclist {
this.latency +
this.worker_time_dif;
const duration = hap.duration / this.cps;
onTrigger?.(hap, 0, duration, this.cps, targetTime);
onTrigger?.(hap, 0, duration, this.cps, targetTime, this.cycle);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could cycle potentially be deduced from the hap itself? something like hap.whole.begin.sam() ? not sure

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was the original plan, but the events don't line up perfectly unless you get the adjusted cycle that accounts for latency etc. Honestly could use some help here, maybe there is a better way to do this.

}
});
};
Expand Down
6 changes: 3 additions & 3 deletions packages/core/repl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,15 @@ export function repl({

export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps, t) => {
async (hap, deadline, duration, cps, t, cycle = 0) => {
// TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps, t);
await defaultOutput(hap, deadline, duration, cps, t, cycle);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t, cycle);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
Expand Down
21 changes: 20 additions & 1 deletion packages/superdough/superdough.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export function resetGlobalEffects() {
analysersData = {};
}

export const superdough = async (value, t, hapDuration) => {
export const superdough = async (value, t, hapDuration, cps, cycle) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could there be a default value for cycle to make am (and potential future friends) be relative to seconds? so am(4) would be 4Hz. thinking about using superdough without strudel, where cycle is a non existing concept

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah you could just pass in the time in seconds since clock start and it should work that way. I agree having both options would be nice for different effects

const ac = getAudioContext();
if (typeof value !== 'object') {
throw new Error(
Expand All @@ -281,6 +281,10 @@ export const superdough = async (value, t, hapDuration) => {
}
// destructure
let {
am,
amdepth = 1,
amskew = 0.5,
amphase = 0,
s = 'triangle',
bank,
source,
Expand Down Expand Up @@ -322,8 +326,10 @@ export const superdough = async (value, t, hapDuration) => {
phasercenter,
//
coarse,

crush,
shape,

shapevol = 1,
distort,
distortvol = 1,
Expand Down Expand Up @@ -470,6 +476,19 @@ export const superdough = async (value, t, hapDuration) => {
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
am !== undefined &&
chain.push(
getWorklet(ac, 'am-processor', {
speed: am,
depth: amdepth,
skew: amskew,
phaseoffset: amphase,
// shape: amshape,

cps,
cycle,
}),
);

compressorThreshold !== undefined &&
chain.push(
Expand Down
Loading
Loading