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});