1/* 2cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/neocyclist.mjs> 3Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/cyclist.mjs> 4This 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/>. 5*/ 6 7import createClock from './zyklus.mjs'; 8import { errorLogger, logger } from './logger.mjs'; 9 10export class Cyclist { 11 constructor({ 12 interval, 13 onTrigger, 14 onToggle, 15 onError, 16 getTime, 17 latency = 0.1, 18 setInterval, 19 clearInterval, 20 beforeStart, 21 }) { 22 this.started = false; 23 this.beforeStart = beforeStart; 24 this.cps = 0.5; 25 this.num_ticks_since_cps_change = 0; 26 this.lastTick = 0; // absolute time when last tick (clock callback) happened 27 this.lastBegin = 0; // query begin of last tick 28 this.lastEnd = 0; // query end of last tick 29 this.getTime = getTime; // get absolute time 30 this.num_cycles_at_cps_change = 0; 31 this.seconds_at_cps_change; // clock phase when cps was changed 32 this.onToggle = onToggle; 33 this.latency = latency; // fixed trigger time offset 34 this.clock = createClock( 35 getTime, 36 // called slightly before each cycle 37 (phase, duration, _, t) => { 38 if (this.num_ticks_since_cps_change === 0) { 39 this.num_cycles_at_cps_change = this.lastEnd; 40 this.seconds_at_cps_change = phase; 41 } 42 this.num_ticks_since_cps_change++; 43 const seconds_since_cps_change = this.num_ticks_since_cps_change * duration; 44 const num_cycles_since_cps_change = seconds_since_cps_change * this.cps; 45 46 try { 47 const begin = this.lastEnd; 48 this.lastBegin = begin; 49 const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change; 50 this.lastEnd = end; 51 this.lastTick = phase; 52 53 if (phase < t) { 54 // avoid querying haps that are in the past anyway 55 console.log(`skip query: too late`); 56 return; 57 } 58 59 // query the pattern for events 60 const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' }); 61 62 haps.forEach((hap) => { 63 if (hap.hasOnset()) { 64 const targetTime = 65 (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; 66 const duration = hap.duration / this.cps; 67 // the following line is dumb and only here for backwards compatibility 68 // see https://codeberg.org/uzu/strudel/pulls/1004 69 const deadline = targetTime - phase; 70 // this onTrigger has another signature 71 onTrigger?.(hap, deadline, duration, this.cps, targetTime); 72 if (hap.value.cps !== undefined && this.cps != hap.value.cps) { 73 this.cps = hap.value.cps; 74 this.num_ticks_since_cps_change = 0; 75 } 76 } 77 }); 78 } catch (e) { 79 errorLogger(e); 80 onError?.(e); 81 } 82 }, 83 interval, // duration of each cycle 84 0.1, 85 0.1, 86 setInterval, 87 clearInterval, 88 ); 89 } 90 now() { 91 if (!this.started) { 92 return 0; 93 } 94 const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; 95 return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; 96 } 97 setStarted(v) { 98 this.started = v; 99 this.onToggle?.(v); 100 } 101 async start() { 102 // jevstrudel: a stop, or another start, while beforeStart is awaited 103 // (samples loading, Jev's opening, a party guest waiting for the host) 104 // cancels this start: the song stopped or replaced must not begin once 105 // it is ready. 106 const run = (this.startRun = (this.startRun ?? 0) + 1); 107 await this.beforeStart?.(); 108 if (run !== this.startRun) return; 109 this.num_ticks_since_cps_change = 0; 110 this.num_cycles_at_cps_change = 0; 111 if (!this.pattern) { 112 throw new Error('Scheduler: no pattern set! call .setPattern first.'); 113 } 114 logger('[cyclist] start'); 115 this.clock.start(); 116 this.setStarted(true); 117 } 118 pause() { 119 this.startRun = (this.startRun ?? 0) + 1; 120 logger('[cyclist] pause'); 121 this.clock.pause(); 122 this.setStarted(false); 123 } 124 stop() { 125 this.startRun = (this.startRun ?? 0) + 1; 126 logger('[cyclist] stop'); 127 this.clock.stop(); 128 this.lastEnd = 0; 129 this.setStarted(false); 130 } 131 async setPattern(pat, autostart = false) { 132 this.pattern = pat; 133 if (autostart && !this.started) { 134 await this.start(); 135 } 136 } 137 setCps(cps = 0.5) { 138 if (this.cps === cps) { 139 return; 140 } 141 this.cps = cps; 142 this.num_ticks_since_cps_change = 0; 143 } 144 log(begin, end, haps) { 145 const onsets = haps.filter((h) => h.hasOnset()); 146 console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); 147 } 148}