mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-26 13:08:28 +00:00
fix: move desktopbridge dependency to website
This commit is contained in:
parent
282f36c47c
commit
328af5f560
@ -1,10 +1,10 @@
|
|||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
import { noteToMidi } from '@strudel.cycles/core';
|
import { Pattern, noteToMidi } from '@strudel.cycles/core';
|
||||||
|
|
||||||
const ON_MESSAGE = 0x90;
|
const ON_MESSAGE = 0x90;
|
||||||
const OFF_MESSAGE = 0x80;
|
const OFF_MESSAGE = 0x80;
|
||||||
|
|
||||||
export function processMidi(output) {
|
Pattern.prototype.midi = function (output) {
|
||||||
return this.onTrigger((time, hap, currentTime) => {
|
return this.onTrigger((time, hap, currentTime) => {
|
||||||
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
|
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
|
||||||
const offset = (time - currentTime) * 1000;
|
const offset = (time - currentTime) * 1000;
|
||||||
@ -53,4 +53,4 @@ export function processMidi(output) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@ -5,8 +5,136 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern } from '@strudel.cycles/core';
|
import { Pattern } from '@strudel.cycles/core';
|
||||||
import * as webmidihandler from './webmidihandler.mjs';
|
import * as _WebMidi from 'webmidi';
|
||||||
import * as midibridge from '../desktopbridge/midibridge.mjs';
|
import { isPattern, logger } from '@strudel.cycles/core';
|
||||||
import { isTauri } from '../desktopbridge/utils.mjs';
|
import { noteToMidi } from '@strudel.cycles/core';
|
||||||
|
import { Note } from 'webmidi';
|
||||||
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
|
export const { WebMidi } = _WebMidi;
|
||||||
|
|
||||||
Pattern.prototype.midi = isTauri() ? midibridge.processMidi : webmidihandler.processMidi;
|
function supportsMidi() {
|
||||||
|
return typeof navigator.requestMIDIAccess === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMidiDeviceNamesString(outputs) {
|
||||||
|
return outputs.map((o) => `'${o.name}'`).join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableWebMidi(options = {}) {
|
||||||
|
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
||||||
|
if (WebMidi.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!supportsMidi()) {
|
||||||
|
throw new Error('Your Browser does not support WebMIDI.');
|
||||||
|
}
|
||||||
|
WebMidi.addListener('connected', () => {
|
||||||
|
onConnected?.(WebMidi);
|
||||||
|
});
|
||||||
|
WebMidi.addListener('enabled', () => {
|
||||||
|
onEnabled?.(WebMidi);
|
||||||
|
});
|
||||||
|
// Reacting when a device becomes unavailable
|
||||||
|
WebMidi.addListener('disconnected', (e) => {
|
||||||
|
onDisconnected?.(WebMidi, e);
|
||||||
|
});
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (WebMidi.enabled) {
|
||||||
|
// if already enabled, just resolve WebMidi
|
||||||
|
resolve(WebMidi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WebMidi.enable((err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
onReady?.(WebMidi);
|
||||||
|
resolve(WebMidi);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
||||||
|
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||||
|
|
||||||
|
// output?: string | number, outputs: typeof WebMidi.outputs
|
||||||
|
function getDevice(output, outputs) {
|
||||||
|
if (!outputs.length) {
|
||||||
|
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||||
|
}
|
||||||
|
if (typeof output === 'number') {
|
||||||
|
return outputs[output];
|
||||||
|
}
|
||||||
|
if (typeof output === 'string') {
|
||||||
|
return outputByName(output);
|
||||||
|
}
|
||||||
|
// attempt to default to first IAC device if none is specified
|
||||||
|
const IACOutput = outputs.find((output) => output.name.includes('IAC'));
|
||||||
|
const device = IACOutput ?? outputs[0];
|
||||||
|
if (!device) {
|
||||||
|
throw new Error(
|
||||||
|
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${getMidiDeviceNamesString(WebMidi.outputs)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IACOutput ?? outputs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.midi = function (output) {
|
||||||
|
if (isPattern(output)) {
|
||||||
|
throw new Error(
|
||||||
|
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||||
|
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||||
|
}')`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enableWebMidi({
|
||||||
|
onEnabled: ({ outputs }) => {
|
||||||
|
const device = getDevice(output, outputs);
|
||||||
|
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
||||||
|
logger(
|
||||||
|
`Midi enabled! Using "${device.name}". ${
|
||||||
|
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onDisconnected: ({ outputs }) =>
|
||||||
|
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.onTrigger((time, hap, currentTime, cps) => {
|
||||||
|
if (!WebMidi.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const device = getDevice(output, WebMidi.outputs);
|
||||||
|
hap.ensureObjectValue();
|
||||||
|
|
||||||
|
const offset = (time - currentTime) * 1000;
|
||||||
|
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
||||||
|
const timeOffsetString = `+${offset}`;
|
||||||
|
|
||||||
|
// destructure value
|
||||||
|
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
||||||
|
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
||||||
|
|
||||||
|
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
||||||
|
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
||||||
|
if (note != null) {
|
||||||
|
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||||
|
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
||||||
|
device.playNote(midiNote, midichan, {
|
||||||
|
time: timeOffsetString,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (ccv && ccn) {
|
||||||
|
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||||
|
throw new Error('expected ccv to be a number between 0 and 1');
|
||||||
|
}
|
||||||
|
if (!['string', 'number'].includes(typeof ccn)) {
|
||||||
|
throw new Error('expected ccn to be a number or a string');
|
||||||
|
}
|
||||||
|
const scaled = Math.round(ccv * 127);
|
||||||
|
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
import * as _WebMidi from 'webmidi';
|
|
||||||
import { isPattern, logger } from '@strudel.cycles/core';
|
|
||||||
import { noteToMidi } from '@strudel.cycles/core';
|
|
||||||
import { Note } from 'webmidi';
|
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
|
||||||
export const { WebMidi } = _WebMidi;
|
|
||||||
|
|
||||||
function supportsMidi() {
|
|
||||||
return typeof navigator.requestMIDIAccess === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMidiDeviceNamesString(outputs) {
|
|
||||||
return outputs.map((o) => `'${o.name}'`).join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableWebMidi(options = {}) {
|
|
||||||
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
|
||||||
if (WebMidi.enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!supportsMidi()) {
|
|
||||||
throw new Error('Your Browser does not support WebMIDI.');
|
|
||||||
}
|
|
||||||
WebMidi.addListener('connected', () => {
|
|
||||||
onConnected?.(WebMidi);
|
|
||||||
});
|
|
||||||
WebMidi.addListener('enabled', () => {
|
|
||||||
onEnabled?.(WebMidi);
|
|
||||||
});
|
|
||||||
// Reacting when a device becomes unavailable
|
|
||||||
WebMidi.addListener('disconnected', (e) => {
|
|
||||||
onDisconnected?.(WebMidi, e);
|
|
||||||
});
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (WebMidi.enabled) {
|
|
||||||
// if already enabled, just resolve WebMidi
|
|
||||||
resolve(WebMidi);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
WebMidi.enable((err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
onReady?.(WebMidi);
|
|
||||||
resolve(WebMidi);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
|
||||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
|
||||||
|
|
||||||
// output?: string | number, outputs: typeof WebMidi.outputs
|
|
||||||
function getDevice(output, outputs) {
|
|
||||||
if (!outputs.length) {
|
|
||||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
|
||||||
}
|
|
||||||
if (typeof output === 'number') {
|
|
||||||
return outputs[output];
|
|
||||||
}
|
|
||||||
if (typeof output === 'string') {
|
|
||||||
return outputByName(output);
|
|
||||||
}
|
|
||||||
// attempt to default to first IAC device if none is specified
|
|
||||||
const IACOutput = outputs.find((output) => output.name.includes('IAC'));
|
|
||||||
const device = IACOutput ?? outputs[0];
|
|
||||||
if (!device) {
|
|
||||||
throw new Error(
|
|
||||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${getMidiDeviceNamesString(WebMidi.outputs)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IACOutput ?? outputs[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processMidi(output) {
|
|
||||||
if (isPattern(output)) {
|
|
||||||
throw new Error(
|
|
||||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
|
||||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
|
||||||
}')`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
enableWebMidi({
|
|
||||||
onEnabled: ({ outputs }) => {
|
|
||||||
const device = getDevice(output, outputs);
|
|
||||||
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
|
||||||
logger(
|
|
||||||
`Midi enabled! Using "${device.name}". ${
|
|
||||||
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onDisconnected: ({ outputs }) =>
|
|
||||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.onTrigger((time, hap, currentTime, cps) => {
|
|
||||||
if (!WebMidi.enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const device = getDevice(output, WebMidi.outputs);
|
|
||||||
hap.ensureObjectValue();
|
|
||||||
|
|
||||||
const offset = (time - currentTime) * 1000;
|
|
||||||
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
|
||||||
const timeOffsetString = `+${offset}`;
|
|
||||||
|
|
||||||
// destructure value
|
|
||||||
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
|
||||||
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
|
||||||
|
|
||||||
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
|
||||||
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
|
||||||
if (note != null) {
|
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
|
||||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
|
||||||
device.playNote(midiNote, midichan, {
|
|
||||||
time: timeOffsetString,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (ccv && ccn) {
|
|
||||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
|
||||||
throw new Error('expected ccv to be a number between 0 and 1');
|
|
||||||
}
|
|
||||||
if (!['string', 'number'].includes(typeof ccn)) {
|
|
||||||
throw new Error('expected ccn to be a number or a string');
|
|
||||||
}
|
|
||||||
const scaled = Math.round(ccv * 127);
|
|
||||||
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -588,6 +588,9 @@ importers:
|
|||||||
'@strudel.cycles/xen':
|
'@strudel.cycles/xen':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/xen
|
version: link:../packages/xen
|
||||||
|
'@strudel/desktopbridge':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/desktopbridge
|
||||||
'@supabase/supabase-js':
|
'@supabase/supabase-js':
|
||||||
specifier: ^2.21.0
|
specifier: ^2.21.0
|
||||||
version: 2.21.0
|
version: 2.21.0
|
||||||
@ -597,6 +600,9 @@ importers:
|
|||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.8
|
specifier: ^0.5.8
|
||||||
version: 0.5.9(tailwindcss@3.3.2)
|
version: 0.5.9(tailwindcss@3.3.2)
|
||||||
|
'@tauri-apps/api':
|
||||||
|
specifier: ^1.4.0
|
||||||
|
version: 1.4.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^18.16.3
|
specifier: ^18.16.3
|
||||||
version: 18.16.3
|
version: 18.16.3
|
||||||
|
|||||||
@ -34,9 +34,11 @@
|
|||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"@strudel.cycles/xen": "workspace:*",
|
"@strudel.cycles/xen": "workspace:*",
|
||||||
|
"@strudel/desktopbridge": "workspace:*",
|
||||||
"@supabase/supabase-js": "^2.21.0",
|
"@supabase/supabase-js": "^2.21.0",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tailwindcss/typography": "^0.5.8",
|
"@tailwindcss/typography": "^0.5.8",
|
||||||
|
"@tauri-apps/api": "^1.4.0",
|
||||||
"@types/node": "^18.16.3",
|
"@types/node": "^18.16.3",
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.2.1",
|
"@types/react-dom": "^18.2.1",
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { settingsMap, useSettings, setLatestCode } from '../settings.mjs';
|
|||||||
import Loader from './Loader';
|
import Loader from './Loader';
|
||||||
import { settingPatterns } from '../settings.mjs';
|
import { settingPatterns } from '../settings.mjs';
|
||||||
import { code2hash, hash2code } from './helpers.mjs';
|
import { code2hash, hash2code } from './helpers.mjs';
|
||||||
|
import { isTauri } from '../tauri.mjs';
|
||||||
|
|
||||||
const { latestCode } = settingsMap.get();
|
const { latestCode } = settingsMap.get();
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ const modules = [
|
|||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel.cycles/mini'),
|
||||||
import('@strudel.cycles/midi'),
|
isTauri() ? import('@strudel/desktopbridge') : import('@strudel.cycles/midi'),
|
||||||
import('@strudel.cycles/xen'),
|
import('@strudel.cycles/xen'),
|
||||||
import('@strudel.cycles/webaudio'),
|
import('@strudel.cycles/webaudio'),
|
||||||
import('@strudel.cycles/osc'),
|
import('@strudel.cycles/osc'),
|
||||||
|
|||||||
4
website/src/tauri.mjs
Normal file
4
website/src/tauri.mjs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
|
||||||
|
export const Invoke = invoke;
|
||||||
|
export const isTauri = () => window.__TAURI_IPC__ != null;
|
||||||
Loading…
x
Reference in New Issue
Block a user