mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-05 08:58:10 -04:00
Compare commits
3 Commits
main
...
trigger-refactor
| Author | SHA1 | Date | |
|---|---|---|---|
| 98e99e8166 | |||
| d7fb9fe992 | |||
| 37d524e639 |
@@ -10,7 +10,7 @@ import { errorLogger, logger } from './logger.mjs';
|
|||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({
|
constructor({
|
||||||
interval,
|
interval,
|
||||||
onTrigger,
|
onSchedule,
|
||||||
onToggle,
|
onToggle,
|
||||||
onError,
|
onError,
|
||||||
getTime,
|
getTime,
|
||||||
@@ -63,12 +63,7 @@ export class Cyclist {
|
|||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const targetTime =
|
const targetTime =
|
||||||
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
||||||
const duration = hap.duration / this.cps;
|
onSchedule?.(hap, targetTime, this.cps);
|
||||||
// the following line is dumb and only here for backwards compatibility
|
|
||||||
// see https://codeberg.org/uzu/strudel/pulls/1004
|
|
||||||
const deadline = targetTime - phase;
|
|
||||||
// this onTrigger has another signature
|
|
||||||
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
|
||||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||||
this.cps = hap.value.cps;
|
this.cps = hap.value.cps;
|
||||||
this.num_ticks_since_cps_change = 0;
|
this.num_ticks_since_cps_change = 0;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { logger } from './logger.mjs';
|
|||||||
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
||||||
|
|
||||||
export class NeoCyclist {
|
export class NeoCyclist {
|
||||||
constructor({ onTrigger, onToggle, getTime }) {
|
constructor({ onSchedule, onToggle, getTime }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 0.5;
|
this.cps = 0.5;
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
@@ -43,8 +43,7 @@ export class NeoCyclist {
|
|||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||||
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
||||||
const duration = cycleToSeconds(hap.duration, this.cps);
|
onSchedule?.(hap, targetTime, this.cps);
|
||||||
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+26
-27
@@ -27,6 +27,7 @@ import {
|
|||||||
import drawLine from './drawLine.mjs';
|
import drawLine from './drawLine.mjs';
|
||||||
import { errorLogger, logger } from './logger.mjs';
|
import { errorLogger, logger } from './logger.mjs';
|
||||||
import { strudelScope } from './evaluate.mjs';
|
import { strudelScope } from './evaluate.mjs';
|
||||||
|
import { getTime } from './schedulerState.mjs';
|
||||||
|
|
||||||
let stringParser;
|
let stringParser;
|
||||||
|
|
||||||
@@ -872,23 +873,41 @@ export class Pattern {
|
|||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Context methods - ones that deal with metadata
|
// Context methods - ones that deal with metadata
|
||||||
|
|
||||||
onTrigger(onTrigger, dominant = true) {
|
/**
|
||||||
|
* Attaches a function that is called for each hap onset.
|
||||||
|
* @name onSchedule
|
||||||
|
* @memberof Pattern
|
||||||
|
* @example
|
||||||
|
* s("bd sd").onSchedule((hap, time, cps) => {
|
||||||
|
* console.log('hap scheduled', time, cps);
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
onSchedule(onSchedule, dominant = true) {
|
||||||
return this.withHap((hap) =>
|
return this.withHap((hap) =>
|
||||||
hap.setContext({
|
hap.setContext({
|
||||||
...hap.context,
|
...hap.context,
|
||||||
onTrigger: (...args) => {
|
onSchedule: (scheduleHap, t, cps) => {
|
||||||
// run previously set trigger, if it exists
|
// run previously set trigger, if it exists
|
||||||
hap.context.onTrigger?.(...args);
|
hap.context.onSchedule?.(scheduleHap, t, cps);
|
||||||
onTrigger(...args);
|
onSchedule(scheduleHap, t, cps);
|
||||||
},
|
},
|
||||||
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
|
// if dominantScheduler is set to true, the default output (webaudio) will be disabled
|
||||||
// when using multiple triggers, you cannot flip this flag to false again!
|
// when using multiple triggers, you cannot flip this flag to false again!
|
||||||
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
||||||
dominantTrigger: hap.context.dominantTrigger || dominant,
|
dominantScheduler: hap.context.dominantScheduler || dominant,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// legacy:
|
||||||
|
onTrigger(onTrigger, dominant = true) {
|
||||||
|
console.warn('onTrigger is deprecated.. use onSchedule(hap, time, cps) instead');
|
||||||
|
return this.onSchedule((hap, t, cps) => {
|
||||||
|
const currentTime = getTime();
|
||||||
|
return onTrigger(hap, currentTime, cps, t);
|
||||||
|
}, dominant);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the content of the current event to the console (visible in the side menu).
|
* Writes the content of the current event to the console (visible in the side menu).
|
||||||
* @tags visualization
|
* @tags visualization
|
||||||
@@ -898,7 +917,7 @@ export class Pattern {
|
|||||||
* s("bd sd").log()
|
* s("bd sd").log()
|
||||||
*/
|
*/
|
||||||
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
||||||
return this.onTrigger((...args) => {
|
return this.onSchedule((...args) => {
|
||||||
logger(func(...args), undefined, getData(...args));
|
logger(func(...args), undefined, getData(...args));
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
@@ -3635,26 +3654,6 @@ export const slice = register(
|
|||||||
false, // turns off auto-patternification
|
false, // turns off auto-patternification
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* make something happen on event time
|
|
||||||
* uses browser timeout which is innacurate for audio tasks
|
|
||||||
* @name onTriggerTime
|
|
||||||
* @tags external_io
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
|
||||||
*/
|
|
||||||
Pattern.prototype.onTriggerTime = function (func) {
|
|
||||||
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
|
||||||
const diff = targetTime - currentTime;
|
|
||||||
window.setTimeout(() => {
|
|
||||||
func(hap);
|
|
||||||
}, diff * 1000);
|
|
||||||
}, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||||
* @name splice
|
* @name splice
|
||||||
|
|||||||
+11
-14
@@ -7,7 +7,7 @@ import {
|
|||||||
setIsStarted,
|
setIsStarted,
|
||||||
setPattern as exposeSchedulerPattern,
|
setPattern as exposeSchedulerPattern,
|
||||||
setTime,
|
setTime,
|
||||||
setTriggerFunc,
|
setSchedulerFunc,
|
||||||
} from './schedulerState.mjs';
|
} from './schedulerState.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
import { evalScope } from './evaluate.mjs';
|
||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
@@ -55,7 +55,7 @@ export function repl({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const schedulerOptions = {
|
const schedulerOptions = {
|
||||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
onSchedule: getScheduler({ defaultOutput }),
|
||||||
getTime,
|
getTime,
|
||||||
onToggle: (started) => {
|
onToggle: (started) => {
|
||||||
updateState({ started });
|
updateState({ started });
|
||||||
@@ -73,7 +73,7 @@ export function repl({
|
|||||||
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||||
const scheduler =
|
const scheduler =
|
||||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||||
setTriggerFunc(schedulerOptions.onTrigger);
|
setSchedulerFunc(schedulerOptions.onSchedule);
|
||||||
setCpsFunc(() => scheduler.cps);
|
setCpsFunc(() => scheduler.cps);
|
||||||
let pPatterns = {};
|
let pPatterns = {};
|
||||||
let anonymousIndex = 0;
|
let anonymousIndex = 0;
|
||||||
@@ -560,20 +560,17 @@ export function repl({
|
|||||||
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTrigger =
|
export const getScheduler =
|
||||||
({ getTime, defaultOutput }) =>
|
({ defaultOutput }) =>
|
||||||
async (hap, deadline, duration, cps, t) => {
|
async (hap, t, cps) => {
|
||||||
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
|
|
||||||
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
|
|
||||||
try {
|
try {
|
||||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
if (!hap.context.onSchedule || !hap.context.dominantScheduler) {
|
||||||
await defaultOutput(hap, deadline, duration, cps, t);
|
await defaultOutput(hap, t, cps);
|
||||||
}
|
}
|
||||||
if (hap.context.onTrigger) {
|
if (hap.context.onSchedule) {
|
||||||
// call signature of output / onTrigger is different...
|
await hap.context.onSchedule(hap, t, cps);
|
||||||
await hap.context.onTrigger(hap, getTime(), cps, t);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorLogger(err, 'getTrigger');
|
errorLogger(err, 'getScheduler');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
let time;
|
let time;
|
||||||
let cpsFunc;
|
let cpsFunc;
|
||||||
let pattern;
|
let pattern;
|
||||||
let triggerFunc;
|
let schedulerFunc;
|
||||||
let isStarted;
|
let isStarted;
|
||||||
export function getTime() {
|
export function getTime() {
|
||||||
if (!time) {
|
if (!time) {
|
||||||
@@ -36,12 +36,12 @@ export function getPattern() {
|
|||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTriggerFunc(func) {
|
export function setSchedulerFunc(func) {
|
||||||
triggerFunc = func;
|
schedulerFunc = func;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTriggerFunc() {
|
export function getSchedulerFunc() {
|
||||||
return triggerFunc;
|
return schedulerFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setIsStarted(val) {
|
export function setIsStarted(val) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const speak = register('speak', function (lang, voice, pat) {
|
export const speak = register('speak', function (lang, voice, pat) {
|
||||||
return pat.onTrigger((hap) => {
|
return pat.onSchedule((hap) => {
|
||||||
triggerSpeech(hap.value, lang, voice);
|
triggerSpeech(hap.value, lang, voice);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1363,7 +1363,7 @@ describe('Pattern', () => {
|
|||||||
|
|
||||||
// Force a trigger
|
// Force a trigger
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
hap.context?.onTrigger?.(hap);
|
hap.context?.onSchedule?.(hap);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||||
@@ -1381,7 +1381,7 @@ describe('Pattern', () => {
|
|||||||
|
|
||||||
// Force a trigger
|
// Force a trigger
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
hap.context?.onTrigger?.(hap);
|
hap.context?.onSchedule?.(hap);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
instrument = instrument || 'triangle';
|
instrument = instrument || 'triangle';
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
return pat.onSchedule((hap, targetTime) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -36,9 +36,10 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
const controls = Object.entries({ ...hap.value, freq })
|
const controls = Object.entries({ ...hap.value, freq })
|
||||||
.flat()
|
.flat()
|
||||||
.join('/');
|
.join('/');
|
||||||
|
|
||||||
// TODO: find out how to send a precise ctx based time
|
// TODO: find out how to send a precise ctx based time
|
||||||
// http://www.csounds.com/manual/html/i.html
|
// http://www.csounds.com/manual/html/i.html
|
||||||
const timeOffset = targetTime - currentTime; // latency ?
|
const timeOffset = targetTime - getAudioContext().currentTime; // latency ?
|
||||||
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
||||||
const params = [
|
const params = [
|
||||||
`"${instrument}"`, // p1: instrument name
|
`"${instrument}"`, // p1: instrument name
|
||||||
@@ -144,7 +145,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
|||||||
p1 = `"${instrument}"`;
|
p1 = `"${instrument}"`;
|
||||||
}
|
}
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
return pat.onSchedule((hap, targetTime) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -152,6 +153,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
|||||||
if (typeof hap.value !== 'object') {
|
if (typeof hap.value !== 'object') {
|
||||||
throw new Error('csound only support objects as hap values');
|
throw new Error('csound only support objects as hap values');
|
||||||
}
|
}
|
||||||
|
const currentTime = getAudioContext().currentTime;
|
||||||
// Time in seconds counting from now.
|
// Time in seconds counting from now.
|
||||||
const p2 = targetTime - currentTime;
|
const p2 = targetTime - currentTime;
|
||||||
const p3 = hap.duration.valueOf() + 0;
|
const p3 = hap.duration.valueOf() + 0;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getAudioContext } from '@strudel/webaudio/index.mjs';
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
||||||
|
|
||||||
@@ -6,7 +7,8 @@ const OFF_MESSAGE = 0x80;
|
|||||||
const CC_MESSAGE = 0xb0;
|
const CC_MESSAGE = 0xb0;
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
Pattern.prototype.midi = function (output) {
|
||||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
return this.onSchedule((hap, targetTime, cps) => {
|
||||||
|
const currentTime = getAudioContext().currentTime;
|
||||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
||||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||||
const latencyMs = 34;
|
const latencyMs = 34;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Pattern, ClockCollator } from '@strudel/core';
|
import { Pattern, ClockCollator } from '@strudel/core';
|
||||||
import { parseControlsFromHap } from 'node_modules/@strudel/osc/osc.mjs';
|
import { parseControlsFromHap } from '@strudel/osc/osc.mjs';
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
|
import { getAudioContext } from '@strudel/webaudio/index.mjs';
|
||||||
|
|
||||||
const collator = new ClockCollator({});
|
const collator = new ClockCollator({});
|
||||||
|
|
||||||
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
export async function oscSchedulerTauri(hap, targetTime, cps = 1) {
|
||||||
const controls = parseControlsFromHap(hap, cps);
|
const controls = parseControlsFromHap(hap, cps);
|
||||||
const params = [];
|
const params = [];
|
||||||
|
const currentTime = getAudioContext().currentTime;
|
||||||
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
||||||
|
|
||||||
Object.keys(controls).forEach((key) => {
|
Object.keys(controls).forEach((key) => {
|
||||||
@@ -32,5 +34,5 @@ export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = function () {
|
||||||
return this.onTrigger(oscTriggerTauri);
|
return this.onSchedule(oscSchedulerTauri);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/webaudio": "workspace:*",
|
||||||
"@tauri-apps/api": "^2.2.0"
|
"@tauri-apps/api": "^2.2.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function initDough() {
|
|||||||
return D.ready;
|
return D.ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) {
|
export async function doughScheduler(hap, targetTime, cps = 1) {
|
||||||
const offset = D.context_offset?.[0];
|
const offset = D.context_offset?.[0];
|
||||||
if (!offset) {
|
if (!offset) {
|
||||||
return; // not ready
|
return; // not ready
|
||||||
@@ -71,5 +71,5 @@ export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) {
|
|||||||
export const dough = register('dough', (pat) => {
|
export const dough = register('dough', (pat) => {
|
||||||
initDough();
|
initDough();
|
||||||
ensureMinimalOutput();
|
ensureMinimalOutput();
|
||||||
return pat.onTrigger(doughTrigger);
|
return pat.onSchedule(doughScheduler);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
getIsStarted,
|
getIsStarted,
|
||||||
getPattern,
|
getPattern,
|
||||||
getTime,
|
getTime,
|
||||||
getTriggerFunc,
|
getSchedulerFunc,
|
||||||
isPattern,
|
isPattern,
|
||||||
logger,
|
logger,
|
||||||
ref,
|
ref,
|
||||||
@@ -342,7 +342,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
|
|
||||||
ensureMinimalOutput();
|
ensureMinimalOutput();
|
||||||
|
|
||||||
return this.sortHapsByPart().onTrigger((hap, _currentTime, cps, targetTime) => {
|
return this.sortHapsByPart().onSchedule((hap, targetTime, cps) => {
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
logger('Midi not enabled');
|
logger('Midi not enabled');
|
||||||
return;
|
return;
|
||||||
@@ -570,8 +570,8 @@ const kListeners = {};
|
|||||||
|
|
||||||
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
||||||
const pattern = getPattern();
|
const pattern = getPattern();
|
||||||
const trigger = getTriggerFunc();
|
const schedule = getSchedulerFunc();
|
||||||
if (!pattern || !trigger) {
|
if (!pattern || !schedule) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const t = now + latencyCycles;
|
const t = now + latencyCycles;
|
||||||
@@ -588,8 +588,7 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const t = ctxNow + (hap.whole.begin - now) / cps;
|
const t = ctxNow + (hap.whole.begin - now) / cps;
|
||||||
const duration = hap.duration / cps;
|
schedule(hap, t, cps);
|
||||||
trigger(hap, t - ctxNow, duration, cps, t);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -624,7 +623,7 @@ export async function midikeys(input) {
|
|||||||
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
||||||
const key = `${input}_${note}`;
|
const key = `${input}_${note}`;
|
||||||
const cps = getCps() ?? 0.5;
|
const cps = getCps() ?? 0.5;
|
||||||
const triggerAvailable = !!(getPattern() && getTriggerFunc());
|
const triggerAvailable = !!(getPattern() && getSchedulerFunc());
|
||||||
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
||||||
const now = getTime();
|
const now = getTime();
|
||||||
const t = now + latencySeconds * cps;
|
const t = now + latencySeconds * cps;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, isPattern, createParams } from '@strudel/core';
|
import { Pattern, getTime, createParams } from '@strudel/core';
|
||||||
import Paho from 'paho-mqtt';
|
import Paho from 'paho-mqtt';
|
||||||
|
|
||||||
const connections = {};
|
const connections = {};
|
||||||
@@ -80,8 +80,7 @@ Pattern.prototype.mqtt = function (
|
|||||||
}
|
}
|
||||||
cx.connect(props);
|
cx.connect(props);
|
||||||
}
|
}
|
||||||
return this.withHap((hap) => {
|
return this.onSchedule((hap, targetTime, cps) => {
|
||||||
const onTrigger = (hap, currentTime, cps, targetTime) => {
|
|
||||||
let msg_topic = topic;
|
let msg_topic = topic;
|
||||||
if (!cx || !cx.isConnected()) {
|
if (!cx || !cx.isConnected()) {
|
||||||
return;
|
return;
|
||||||
@@ -109,13 +108,11 @@ Pattern.prototype.mqtt = function (
|
|||||||
message = new Paho.Message(message);
|
message = new Paho.Message(message);
|
||||||
message.destinationName = msg_topic;
|
message.destinationName = msg_topic;
|
||||||
|
|
||||||
const offset = (targetTime - currentTime + latency) * 1000;
|
const offset = (targetTime - getTime() + latency) * 1000;
|
||||||
|
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
cx.send(message);
|
cx.send(message);
|
||||||
}, offset);
|
}, offset);
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function parseControlsFromHap(hap, cps) {
|
|||||||
return controls;
|
return controls;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
export async function oscScheduler(hap, targetTime, cps = 1) {
|
||||||
const ws = await connect();
|
const ws = await connect();
|
||||||
|
|
||||||
const timeMs = getClockBridge().getPerformanceTime(targetTime);
|
const timeMs = getClockBridge().getPerformanceTime(targetTime);
|
||||||
@@ -90,5 +90,5 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
|||||||
*/
|
*/
|
||||||
export const osc = register('osc', (pat) => {
|
export const osc = register('osc', (pat) => {
|
||||||
ensureMinimalOutput();
|
ensureMinimalOutput();
|
||||||
return pat.onTrigger(oscTrigger);
|
return pat.onSchedule(oscScheduler);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||||
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
||||||
import { oscTrigger } from './osc.mjs';
|
import { oscScheduler } from './osc.mjs';
|
||||||
|
|
||||||
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
|
const scheduler = /* isTauri() ? oscTriggerTauri : */ oscScheduler;
|
||||||
|
|
||||||
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
export const superdirtOutput = (hap, targetTime, cps) => scheduler(hap, targetTime, cps);
|
||||||
const currentTime = performance.now() / 1000;
|
|
||||||
return trigger(hap, currentTime, cps, targetTime);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, isPattern } from '@strudel/core';
|
import { Pattern, getTime } from '@strudel/core';
|
||||||
|
|
||||||
var writeMessagers = {};
|
var writeMessagers = {};
|
||||||
var choosing = false;
|
var choosing = false;
|
||||||
@@ -64,11 +64,10 @@ function crc16(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
|
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
|
||||||
return this.withHap((hap) => {
|
|
||||||
if (!(name in writeMessagers)) {
|
if (!(name in writeMessagers)) {
|
||||||
getWriter(name, br);
|
getWriter(name, br);
|
||||||
}
|
}
|
||||||
const onTrigger = (hap, currentTime, _cps, targetTime) => {
|
return this.onSchedule((hap, targetTime) => {
|
||||||
var message = '';
|
var message = '';
|
||||||
var chk = 0;
|
var chk = 0;
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
@@ -105,12 +104,10 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
|
|||||||
} else {
|
} else {
|
||||||
message = hap.value;
|
message = hap.value;
|
||||||
}
|
}
|
||||||
const offset = (targetTime - currentTime + latency) * 1000;
|
const offset = (targetTime - getTime() + latency) * 1000;
|
||||||
|
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
writeMessagers[name](message, chk);
|
writeMessagers[name](message, chk);
|
||||||
}, offset);
|
}, offset);
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio';
|
|||||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||||
|
|
||||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||||
return this.onTrigger((h, ct, cps, targetTime) => {
|
return this.onSchedule((h, targetTime) => {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const note = getPlayableNoteValue(h);
|
const note = getPlayableNoteValue(h);
|
||||||
const preset = sf.presets[n % sf.presets.length];
|
const preset = sf.presets[n % sf.presets.length];
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const rawdsp = async (code) => {
|
|||||||
worklet.node.connect(ac.destination);
|
worklet.node.connect(ac.destination);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function rawdspTrigger(hap, currentTime, cps, targetTime) {
|
export function rawdspScheduler(hap, time, cps) {
|
||||||
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
|
const currentTime = getAudioContext().currentTime;
|
||||||
|
window.postMessage({ time, dough: hap.value, currentTime, duration: hap.duration, cps });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1048,7 +1048,3 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
|
||||||
superdough(hap, t - ct, hap.duration / cps, cps);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const soundMap = new Map();
|
|||||||
const loadedSounds = new Map();
|
const loadedSounds = new Map();
|
||||||
|
|
||||||
Pattern.prototype.supradough = function () {
|
Pattern.prototype.supradough = function () {
|
||||||
return this.onTrigger((hap, __, cps, begin) => {
|
return this.onSchedule((hap, begin, cps) => {
|
||||||
hap.value._begin = begin;
|
hap.value._begin = begin;
|
||||||
hap.value._duration = hap.duration / cps;
|
hap.value._duration = hap.duration / cps;
|
||||||
!doughWorklet && initDoughWorklet();
|
!doughWorklet && initDoughWorklet();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
superdough,
|
superdough,
|
||||||
getAudioContext,
|
getAudioContext,
|
||||||
setLogger,
|
setLogger,
|
||||||
rawdspTrigger,
|
rawdspScheduler,
|
||||||
registerWorklet,
|
registerWorklet,
|
||||||
setAudioContext,
|
setAudioContext,
|
||||||
initAudio,
|
initAudio,
|
||||||
@@ -33,7 +33,8 @@ const hap2value = (hap) => {
|
|||||||
|
|
||||||
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
||||||
// TODO: refactor output callbacks to eliminate deadline
|
// TODO: refactor output callbacks to eliminate deadline
|
||||||
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
export const webaudioOutput = (hap, t, cps) => {
|
||||||
|
const hapDuration = hap.duration / cps;
|
||||||
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -174,7 +175,7 @@ export function webaudioRepl(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.rawdsp = function () {
|
Pattern.prototype.rawdsp = function () {
|
||||||
return this.onTrigger(rawdspTrigger, 1);
|
return this.onSchedule(rawdspScheduler, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
function audioBufferToWav(buffer, opt) {
|
function audioBufferToWav(buffer, opt) {
|
||||||
@@ -271,3 +272,23 @@ function writeString(view, offset, string) {
|
|||||||
view.setUint8(offset + i, string.charCodeAt(i));
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* make something happen on event time
|
||||||
|
* uses browser timeout which is innacurate for audio tasks
|
||||||
|
* @name onTriggerTime
|
||||||
|
* @tags external_io
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
||||||
|
*/
|
||||||
|
Pattern.prototype.onTriggerTime = function (func) {
|
||||||
|
return this.onSchedule((hap, targetTime) => {
|
||||||
|
const diff = targetTime - getAudioContext().currentTime;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
func(hap);
|
||||||
|
}, diff * 1000);
|
||||||
|
}, false);
|
||||||
|
};
|
||||||
|
|||||||
@@ -8522,6 +8522,19 @@ exports[`runs examples > example "often" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "onSchedule" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/2 | s:bd ]",
|
||||||
|
"[ 1/2 → 1/1 | s:sd ]",
|
||||||
|
"[ 1/1 → 3/2 | s:bd ]",
|
||||||
|
"[ 3/2 → 2/1 | s:sd ]",
|
||||||
|
"[ 2/1 → 5/2 | s:bd ]",
|
||||||
|
"[ 5/2 → 3/1 | s:sd ]",
|
||||||
|
"[ 3/1 → 7/2 | s:bd ]",
|
||||||
|
"[ 7/2 → 4/1 | s:sd ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
|
exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:bd ]",
|
"[ 0/1 → 1/8 | s:bd ]",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function MiniRepl({
|
|||||||
tunes,
|
tunes,
|
||||||
hideHeader = false,
|
hideHeader = false,
|
||||||
canvasHeight = 100,
|
canvasHeight = 100,
|
||||||
onTrigger,
|
onSchedule,
|
||||||
punchcard,
|
punchcard,
|
||||||
punchcardLabels = true,
|
punchcardLabels = true,
|
||||||
claviature,
|
claviature,
|
||||||
@@ -58,8 +58,8 @@ export function MiniRepl({
|
|||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
drawContext,
|
||||||
editPattern: (pat, id) => {
|
editPattern: (pat, id) => {
|
||||||
if (onTrigger) {
|
if (onSchedule) {
|
||||||
pat = pat.onTrigger(onTrigger, false);
|
pat = pat.onSchedule(onSchedule, false);
|
||||||
}
|
}
|
||||||
if (claviature) {
|
if (claviature) {
|
||||||
pat = pat.onPaint((ctx, time, haps, drawTime) => {
|
pat = pat.onPaint((ctx, time, haps, drawTime) => {
|
||||||
|
|||||||
@@ -186,9 +186,9 @@ These functions are more low level, probably not needed by the live coder.
|
|||||||
|
|
||||||
# Other
|
# Other
|
||||||
|
|
||||||
## onTrigger
|
## onSchedule
|
||||||
|
|
||||||
<JsDoc client:idle name="Pattern#onTrigger" h={0} />
|
<JsDoc client:idle name="Pattern#onSchedule" h={0} />
|
||||||
|
|
||||||
## log
|
## log
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Besides a UI for playback control and meta information, the main part of the REP
|
|||||||
|
|
||||||
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
|
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
|
||||||
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
|
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
|
||||||
3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output.
|
3. For each scheduling tick, all generated `Events` are triggered by calling their `onSchedule` method, which is set by the output.
|
||||||
|
|
||||||
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
|
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
|
||||||
|
|
||||||
@@ -134,10 +134,7 @@ let minLatency = 0.1; // min time before a hap should trigger
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const haps = pattern.queryArc(time, time + interval);
|
const haps = pattern.queryArc(time, time + interval);
|
||||||
time += interval; // increment time
|
time += interval; // increment time
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => onSchedule(hap, hap.whole.begin + minLatency));
|
||||||
const deadline = hap.whole.begin - time + minLatency;
|
|
||||||
onTrigger(hap, deadline, duration);
|
|
||||||
});
|
|
||||||
}, interval * 1000); // query each "interval" seconds
|
}, interval * 1000); // query each "interval" seconds
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -181,20 +178,19 @@ This example creates the custom control parameters `x` and `y` which are then us
|
|||||||
|
|
||||||
### Outputs
|
### Outputs
|
||||||
|
|
||||||
Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onTrigger` function which is used to implement the output. A very simple version of the web audio output could look like this:
|
Now that we know how the value of an hap is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onSchedule` function which is used to implement the output. A very simple version of the web audio output could look like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function onTrigger(hap, deadline, duration) {
|
function onSchedule(hap, time, cps) {
|
||||||
const { note } = hap.value;
|
const { note } = hap.value;
|
||||||
const time = getAudioContext().currentTime + deadline;
|
|
||||||
const o = getAudioContext().createOscillator();
|
const o = getAudioContext().createOscillator();
|
||||||
o.frequency.value = getFreq(note);
|
o.frequency.value = getFreq(note);
|
||||||
o.start(time);
|
o.start(time);
|
||||||
o.stop(time + event.duration);
|
o.stop(time + hap.duration / cps);
|
||||||
o.connect(getAudioContext().destination);
|
o.connect(getAudioContext().destination);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
The above example will create an `OscillatorNode` for each hap, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
||||||
|
|
||||||
I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
|
I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ export const debugAudiograph = async (argOptions = {}) => {
|
|||||||
|
|
||||||
/*global all*/
|
/*global all*/
|
||||||
all((pat) =>
|
all((pat) =>
|
||||||
pat.onTrigger(async (hap, duration, cps, t) => {
|
pat.onSchedule(async (hap, t, cps) => {
|
||||||
hap_count++;
|
hap_count++;
|
||||||
const key = Object.entries(hap.value)
|
const key = Object.entries(hap.value)
|
||||||
.map((param) => param.join('/'))
|
.map((param) => param.join('/'))
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
|||||||
|
|
||||||
const patternSetting = (key) =>
|
const patternSetting = (key) =>
|
||||||
register(key, (value, pat) =>
|
register(key, (value, pat) =>
|
||||||
pat.onTrigger(() => {
|
pat.onSchedule(() => {
|
||||||
value = Array.isArray(value) ? value.join(' ') : value;
|
value = Array.isArray(value) ? value.join(' ') : value;
|
||||||
if (value !== settingsMap.get()[key]) {
|
if (value !== settingsMap.get()[key]) {
|
||||||
settingsMap.setKey(key, value);
|
settingsMap.setKey(key, value);
|
||||||
|
|||||||
Reference in New Issue
Block a user