jevstrudel.git / website / src / components / Dough / dough-repl.mjs

import { Dough, doughsamples } from 'dough-synth';

2import { Dough, doughsamples } from 'https://unpkg.com/dough-synth@0.1.9/dough.js';
3import { Pattern, noteToMidi, evaluate, stack } from '@strudel/core';

import doughUrl from 'dough-synth?url';

5import { transpiler } from '@strudel/transpiler';

const doughBaseUrl = doughUrl.split('/').slice(0, -1).join('/') + '/';

7const doughBaseUrl = 'https://unpkg.com/dough-synth@0.1.9/';
9Object.assign(globalThis, { doughsamples });
10
11export class DoughRepl {
12  pattern;
13  latency = 0.1;
14  cps = 0.5;
15  origin;
16  t0;
17  lasttime;
18  strudel;
19  q = [];
20  constructor() {
21    this.ready = this.init();
22  }
23  async init() {
24    // init dough immediately, so that it can attach the document click event to initAudio immediately
25    this.dough = new Dough({
26      base: doughBaseUrl,
27      //base: "../", // local dev
28      onTick: ({ t0, t1 }) => {
29        if (!this.pattern) {
30          return;
31        }
32        this.origin ??= t0;
33        this.t0 = t0;
34        this.lasttime = performance.now();
35        const a = (t0 - this.origin) * this.cps;
36        const b = (t1 - this.origin) * this.cps;
37
38        const haps = this.pattern.queryArc(a, b).filter((hap) => hap.hasOnset());
39        if (!haps.length) {
40          return;
41        }
42        haps.forEach((hap) => {
43          const time = hap.whole.begin.valueOf() / this.cps + this.origin + this.latency;
44          const duration = hap.duration.valueOf() / this.cps;
45          const event = {
46            dough: 'play',
47            ...hap.value,
48            time,
49            duration,
50          };
51          if (event.note && typeof event.note === 'string') {
52            event.note = noteToMidi(event.note);
53          }
54          //console.log("event", JSON.stringify(Object.entries(event)));
55          this.dough.evaluate(event);
56          this.q.push({ event, hap });
57        });
58      },
59    });
60    // miniAllStrings();
61    const setcps = (cps) => (this.cps = cps);
62    const setcpm = (cpm) => setcps(cpm / 60);
63    const replScope = { setcps, setcpm };
64    Object.assign(globalThis, replScope);
65  }
66  async evaluate(code) {
67    await this.ready;
68    let patterns = [];
69    Pattern.prototype.p = function (id) {
70      if (!id.startsWith('_')) {
71        patterns.push(this);
72      }
73    };
74
75    let { meta } = await evaluate(code, transpiler, { addReturn: false, wrapAsync: true, emitWidgets: false });
76    const { miniLocations } = meta;
77
78    this.pattern = stack(...patterns);
79    return { miniLocations, pattern: this.pattern };
80  }
81  stop() {
82    this.pattern = undefined;
83    this.origin = undefined;
84  }
85  prebake() {
86    return Promise.all([
87      // doughsamples('github:eddyflux/crate')
88    ]);
89  }
90  // tbd: move this to dough-synth
91  get time() {
92    return this.t0 + (performance.now() - this.lasttime) / 1000 + this.latency;
93  }
94  processHaps() {
95    const currentHaps = [];
96    const time = this.time;
97    this.q = this.q.filter(({ event, hap }) => {
98      const end = event.time + event.duration;
99      const isActive = time >= event.time && time <= end;
100      if (isActive) {
101        currentHaps.push(hap);
102      }
103      return end > time; // delete old events
104      // we do NOT return !isActive, because a frame might miss an event, which would cause a leak
105    });
106    // console.log(this.q.length); // to check for leaks
107    return currentHaps;
108  }
109}