jevstrudel.git / packages / core / cyclist.mjs

cyclist.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 Copyright (C) 2022 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/core/cyclist.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/.

7import createClock from './zyklus.mjs';
8import { errorLogger, logger } from './logger.mjs';
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          }

query the pattern for events

60          const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
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}