mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
configuring package, wiring everything together
This commit is contained in:
parent
284f5615cc
commit
c29c4c0a8b
@ -2,6 +2,7 @@
|
|||||||
export * from './packages/core/index.mjs';
|
export * from './packages/core/index.mjs';
|
||||||
export * from './packages/csound/index.mjs';
|
export * from './packages/csound/index.mjs';
|
||||||
export * from './packages/embed/index.mjs';
|
export * from './packages/embed/index.mjs';
|
||||||
|
export * from './packages/desktopbridge/index.mjs';
|
||||||
export * from './packages/midi/index.mjs';
|
export * from './packages/midi/index.mjs';
|
||||||
export * from './packages/mini/index.mjs';
|
export * from './packages/mini/index.mjs';
|
||||||
export * from './packages/osc/index.mjs';
|
export * from './packages/osc/index.mjs';
|
||||||
|
|||||||
3
packages/desktopbridge/README.md
Normal file
3
packages/desktopbridge/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# @strudel/desktopbridge
|
||||||
|
|
||||||
|
This package contains utilities used to communicate with the Tauri backend
|
||||||
8
packages/desktopbridge/index.mjs
Normal file
8
packages/desktopbridge/index.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
index.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/desktopbridge/index.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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './midibridge.mjs';
|
||||||
|
export * from './utils.mjs';
|
||||||
57
packages/desktopbridge/midibridge.mjs
Normal file
57
packages/desktopbridge/midibridge.mjs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Invoke } from './utils.mjs';
|
||||||
|
import { noteToMidi } from '@strudel.cycles/core';
|
||||||
|
|
||||||
|
const ON_MESSAGE = 0x90;
|
||||||
|
const OFF_MESSAGE = 0x80;
|
||||||
|
|
||||||
|
export function processMidi(output) {
|
||||||
|
return this.onTrigger((time, hap, currentTime) => {
|
||||||
|
console.log('here');
|
||||||
|
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
|
||||||
|
const offset = (time - currentTime) * 1000;
|
||||||
|
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
|
||||||
|
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
||||||
|
const roundedOffset = Math.round(offset);
|
||||||
|
const midichan = (hap.value.midichan ?? 1) - 1;
|
||||||
|
const requestedport = output ?? 'IAC';
|
||||||
|
const messagesfromjs = [];
|
||||||
|
if (note != null) {
|
||||||
|
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||||
|
messagesfromjs.push({
|
||||||
|
requestedport,
|
||||||
|
message: [ON_MESSAGE + midichan, midiNumber, velocity],
|
||||||
|
offset: roundedOffset,
|
||||||
|
});
|
||||||
|
messagesfromjs.push({
|
||||||
|
requestedport,
|
||||||
|
message: [OFF_MESSAGE + midichan, midiNumber, velocity],
|
||||||
|
offset: roundedOffset + duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
messagesfromjs.push({
|
||||||
|
requestedport,
|
||||||
|
message: [ON_MESSAGE + midichan, ccn, scaled],
|
||||||
|
offset: roundedOffset,
|
||||||
|
});
|
||||||
|
messagesfromjs.push({
|
||||||
|
requestedport,
|
||||||
|
message: [OFF_MESSAGE + midichan, ccn, scaled],
|
||||||
|
offset: roundedOffset + duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// invoke is temporarily blocking, run in an async process
|
||||||
|
if (messagesfromjs.length) {
|
||||||
|
setTimeout(() => {
|
||||||
|
Invoke('sendmidi', { messagesfromjs });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
28
packages/desktopbridge/package.json
Normal file
28
packages/desktopbridge/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel/desktopbridge",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "send midi messages between the JS and Tauri (Rust) sides of the Studel desktop app",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Jade Rowland <jaderowlanddev@gmail.com>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"@tauri-apps/api": "^1.4.0"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/tidalcycles/strudel#readme"
|
||||||
|
}
|
||||||
4
packages/desktopbridge/utils.mjs
Normal file
4
packages/desktopbridge/utils.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;
|
||||||
@ -4,137 +4,9 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
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/>.
|
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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _WebMidi from 'webmidi';
|
import { Pattern } from '@strudel.cycles/core';
|
||||||
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
|
import * as webmidihandler from './webmidihandler.mjs';
|
||||||
import { noteToMidi } from '@strudel.cycles/core';
|
import * as midibridge from '../desktopbridge/midibridge.mjs';
|
||||||
import { Note } from 'webmidi';
|
import { isTauri } from '../desktopbridge/utils.mjs';
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
|
||||||
export const { WebMidi } = _WebMidi;
|
|
||||||
|
|
||||||
function supportsMidi() {
|
Pattern.prototype.midi = isTauri() ? midibridge.processMidi : webmidihandler.processMidi;
|
||||||
return typeof navigator.requestMIDIAccess === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMidiDeviceNamesString(outputs) {
|
|
||||||
return outputs.map((o) => `'${o.name}'`).join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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: string | number, channel = 1) {
|
|
||||||
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,58 +0,0 @@
|
|||||||
import { invoke } from '@tauri-apps/api/tauri';
|
|
||||||
import { noteToMidi } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
const ON_MESSAGE = 0x90;
|
|
||||||
const OFF_MESSAGE = 0x80;
|
|
||||||
|
|
||||||
export function playNote(hap, offset, output) {
|
|
||||||
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
|
|
||||||
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
|
|
||||||
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
|
||||||
const roundedOffset = Math.round(offset);
|
|
||||||
const midichan = (hap.value.midichan ?? 1) - 1;
|
|
||||||
const requestedport = output ?? 'IAC';
|
|
||||||
const messagesfromjs = [];
|
|
||||||
if (note != null) {
|
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
|
||||||
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [ON_MESSAGE + midichan, midiNumber, velocity],
|
|
||||||
offset: roundedOffset,
|
|
||||||
});
|
|
||||||
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [OFF_MESSAGE + midichan, midiNumber, velocity],
|
|
||||||
offset: roundedOffset + duration,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [ON_MESSAGE + midichan, ccn, scaled],
|
|
||||||
offset: roundedOffset,
|
|
||||||
});
|
|
||||||
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [OFF_MESSAGE + midichan, ccn, scaled],
|
|
||||||
offset: roundedOffset + duration,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// invoke is temporarily blocking, run in an async process
|
|
||||||
if (messagesfromjs.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
invoke('sendmidi', { messagesfromjs });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
133
packages/midi/webmidihandler.mjs
Normal file
133
packages/midi/webmidihandler.mjs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@ -1,5 +1,9 @@
|
|||||||
lockfileVersion: '6.0'
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@ -176,6 +180,15 @@ importers:
|
|||||||
specifier: ^4.3.3
|
specifier: ^4.3.3
|
||||||
version: 4.3.3(@types/node@18.16.3)
|
version: 4.3.3(@types/node@18.16.3)
|
||||||
|
|
||||||
|
packages/desktopbridge:
|
||||||
|
dependencies:
|
||||||
|
'@strudel.cycles/core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core
|
||||||
|
'@tauri-apps/api':
|
||||||
|
specifier: ^1.4.0
|
||||||
|
version: 1.4.0
|
||||||
|
|
||||||
packages/embed: {}
|
packages/embed: {}
|
||||||
|
|
||||||
packages/midi:
|
packages/midi:
|
||||||
@ -3944,6 +3957,11 @@ packages:
|
|||||||
tailwindcss: 3.3.2
|
tailwindcss: 3.3.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tauri-apps/api@1.4.0:
|
||||||
|
resolution: {integrity: sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw==}
|
||||||
|
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tauri-apps/cli-darwin-arm64@1.4.0:
|
/@tauri-apps/cli-darwin-arm64@1.4.0:
|
||||||
resolution: {integrity: sha512-nA/ml0SfUt6/CYLVbHmT500Y+ijqsuv5+s9EBnVXYSLVg9kbPUZJJHluEYK+xKuOj6xzyuT/+rZFMRapmJD3jQ==}
|
resolution: {integrity: sha512-nA/ml0SfUt6/CYLVbHmT500Y+ijqsuv5+s9EBnVXYSLVg9kbPUZJJHluEYK+xKuOj6xzyuT/+rZFMRapmJD3jQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@ -8025,6 +8043,7 @@ packages:
|
|||||||
/iconv-lite@0.6.3:
|
/iconv-lite@0.6.3:
|
||||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
requiresBuild: true
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user