osc.mjsannotatedosc.mjssource94 lines · 3.8 KB · raw
1/*
2osc.mjs - <short description TODO>
3Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/osc.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 { logger, parseNumeral, register, isNote, noteToMidi } from '@strudel/core';
8import { ensureMinimalOutput, getClockBridge } from '@strudel/webaudio';
9
10let connection; // Promise<OSC>
11function connect() {
12  if (!connection) {
13    // make sure this runs only once
14    connection = new Promise((resolve, reject) => {
15      const ws = new WebSocket('ws://localhost:8080');
16      ws.addEventListener('open', (event) => {
17        logger(`[osc] websocket connected`);
18        resolve(ws);
19      });
20      ws.addEventListener('close', (event) => {
21        logger(`[osc] websocket closed`);
22        connection = undefined; // allows new connection afterwards
23        console.log('[osc] disconnected');
24        reject('OSC connection closed');
25      });
26      ws.addEventListener('error', (err) => reject(err));
27    }).catch((err) => {
28      connection = undefined;
29      throw new Error('Could not connect to OSC server. Is it running?');
30    });
31  }
32  return connection;
33}
34
35export function parseControlsFromHap(hap, cps) {
36  hap.ensureObjectValue();
37  const cycle = hap.wholeOrPart().begin.valueOf();
38  const delta = hap.duration.valueOf() / cps;
39  const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
40  // make sure n and note are numbers
41  controls.n && (controls.n = parseNumeral(controls.n));
42  if (typeof controls.note !== 'undefined') {
43    if (isNote(controls.note)) {
44      controls.midinote = noteToMidi(controls.note, controls.octave || 3);
45    } else {
46      controls.midinote = parseNumeral(controls.note);
47    }
48  }
49  controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
50  // speed adjustment for CPS is handled on the DSP side in superdirt and pattern side in Strudel,
51  // so we need to undo the adjustment before sending the message to superdirt.
52  controls.unit === 'c' && controls.speed != null && (controls.speed = controls.speed / cps);
53  const channels = controls.channels;
54  channels != undefined && (controls.channels = JSON.stringify(channels));
55  return controls;
56}
57
58export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
59  const ws = await connect();
60
61  const timeMs = getClockBridge().getPerformanceTime(targetTime);
62  if (!timeMs) {
63    logger('[osc] clockbridge not ready');
64    return;
65  }
66  const ts = performance.timeOrigin + timeMs;
67
68  const controls = parseControlsFromHap(hap, cps);
69  const keyvals = Object.entries(controls).flat();
70  const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
71
72  if ('oschost' in hap.value) {
73    msg['host'] = hap.value['oschost'];
74  }
75  if ('oscport' in hap.value) {
76    msg['port'] = hap.value['oscport'];
77  }
78  ws.send(JSON.stringify(msg));
79}
80
81/**
82 *
83 * Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
84 * For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output/)
85 *
86 * @name osc
87 * @tags external_io
88 * @memberof Pattern
89 * @returns Pattern
90 */
91export const osc = register('osc', (pat) => {
92  ensureMinimalOutput();
93  return pat.onTrigger(oscTrigger);
94});