mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-14 15:18:30 +00:00
added midi support
This commit is contained in:
parent
b0e50db4f6
commit
f6c4bdb8a6
98
repl/package-lock.json
generated
98
repl/package-lock.json
generated
@ -8,16 +8,16 @@
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"codemirror": "^5.65.1",
|
||||
"estraverse": "^5.3.0",
|
||||
"multimap": "^1.1.0",
|
||||
"react": "^17.0.2",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
"shift-parser": "^7.0.3",
|
||||
"shift-reducer": "6.0.0",
|
||||
"shift-regexp-acceptor": "^2.0.3",
|
||||
"shift-spec": "^2018.0.2",
|
||||
"shift-traverser": "^1.0.0",
|
||||
"tone": "^14.7.77"
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@snowpack/plugin-dotenv": "^2.1.0",
|
||||
@ -7291,22 +7291,6 @@
|
||||
"shift-reducer": "6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shift-parser": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/shift-parser/-/shift-parser-7.0.3.tgz",
|
||||
"integrity": "sha512-uYX2ORyZfKZrUc4iKKkO9KOhzUSxCrSBk7QK6ZmShId+BOo1gh1IwecVy97ynyOTpmhPWUttjC8BzsnQl65Zew==",
|
||||
"dependencies": {
|
||||
"multimap": "^1.0.2",
|
||||
"shift-ast": "6.0.0",
|
||||
"shift-reducer": "6.0.0",
|
||||
"shift-regexp-acceptor": "2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/shift-parser/node_modules/shift-ast": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
||||
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
||||
},
|
||||
"node_modules/shift-reducer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shift-reducer/-/shift-reducer-6.0.0.tgz",
|
||||
@ -7335,28 +7319,6 @@
|
||||
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.2.tgz",
|
||||
"integrity": "sha512-5CP/cKDEim4rZ6ViCSipTLY2U7HJr8q/kpDuCBmebFqbx/0DeozWO+9ienHmYjgGLDfHrqj+LBAN67FRK2vE6w=="
|
||||
},
|
||||
"node_modules/shift-traverser": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shift-traverser/-/shift-traverser-1.0.0.tgz",
|
||||
"integrity": "sha512-DMY3512wJbdC+IC+nhLH3/Stgr2BbxbNcg7qyZ6+e5qNnNs8TBQJWdMsRgHlX1JXwF4C0ONKS8VUxsPT0Tf7aw==",
|
||||
"dependencies": {
|
||||
"estraverse": "4.2.0",
|
||||
"shift-spec": "2018.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shift-traverser/node_modules/estraverse": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
||||
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shift-traverser/node_modules/shift-spec": {
|
||||
"version": "2018.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.0.tgz",
|
||||
"integrity": "sha512-/aiPOkj7dbe+CV2VZhIMTHQToZmgniofpRG7Yr7x2/0sO6CSVC++py1Wzf+s+rWSTDHKcLvziVAxjRRV4i4EoQ=="
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
@ -8553,6 +8515,14 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/webmidi": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/webmidi/-/webmidi-2.5.3.tgz",
|
||||
"integrity": "sha512-PyMGvKcDGpvbQUfnmBORQJciyG3VAZ4aHlGy1iRZ3uEs4kG4HCvI7KRthUpM1vuHDPL98lidRIUaoRomkJtWtg==",
|
||||
"engines": {
|
||||
"node": ">0.6.x"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||
@ -14433,24 +14403,6 @@
|
||||
"shift-reducer": "6.0.0"
|
||||
}
|
||||
},
|
||||
"shift-parser": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/shift-parser/-/shift-parser-7.0.3.tgz",
|
||||
"integrity": "sha512-uYX2ORyZfKZrUc4iKKkO9KOhzUSxCrSBk7QK6ZmShId+BOo1gh1IwecVy97ynyOTpmhPWUttjC8BzsnQl65Zew==",
|
||||
"requires": {
|
||||
"multimap": "^1.0.2",
|
||||
"shift-ast": "6.0.0",
|
||||
"shift-reducer": "6.0.0",
|
||||
"shift-regexp-acceptor": "2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"shift-ast": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
||||
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"shift-reducer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shift-reducer/-/shift-reducer-6.0.0.tgz",
|
||||
@ -14481,27 +14433,6 @@
|
||||
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.2.tgz",
|
||||
"integrity": "sha512-5CP/cKDEim4rZ6ViCSipTLY2U7HJr8q/kpDuCBmebFqbx/0DeozWO+9ienHmYjgGLDfHrqj+LBAN67FRK2vE6w=="
|
||||
},
|
||||
"shift-traverser": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shift-traverser/-/shift-traverser-1.0.0.tgz",
|
||||
"integrity": "sha512-DMY3512wJbdC+IC+nhLH3/Stgr2BbxbNcg7qyZ6+e5qNnNs8TBQJWdMsRgHlX1JXwF4C0ONKS8VUxsPT0Tf7aw==",
|
||||
"requires": {
|
||||
"estraverse": "4.2.0",
|
||||
"shift-spec": "2018.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"estraverse": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
||||
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
|
||||
},
|
||||
"shift-spec": {
|
||||
"version": "2018.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.0.tgz",
|
||||
"integrity": "sha512-/aiPOkj7dbe+CV2VZhIMTHQToZmgniofpRG7Yr7x2/0sO6CSVC++py1Wzf+s+rWSTDHKcLvziVAxjRRV4i4EoQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
@ -15420,6 +15351,11 @@
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"dev": true
|
||||
},
|
||||
"webmidi": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/webmidi/-/webmidi-2.5.3.tgz",
|
||||
"integrity": "sha512-PyMGvKcDGpvbQUfnmBORQJciyG3VAZ4aHlGy1iRZ3uEs4kG4HCvI7KRthUpM1vuHDPL98lidRIUaoRomkJtWtg=="
|
||||
},
|
||||
"whatwg-url": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||
|
||||
@ -12,13 +12,16 @@
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"codemirror": "^5.65.1",
|
||||
"estraverse": "^5.3.0",
|
||||
"multimap": "^1.1.0",
|
||||
"react": "^17.0.2",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
"shift-regexp-acceptor": "^2.0.3",
|
||||
"shift-spec": "^2018.0.2",
|
||||
"tone": "^14.7.77"
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@snowpack/plugin-dotenv": "^2.1.0",
|
||||
|
||||
@ -9,6 +9,7 @@ import * as parser from './parse';
|
||||
import CodeMirror from './CodeMirror';
|
||||
import hot from '../public/hot';
|
||||
import { isNote } from 'tone';
|
||||
import { useWebMidi } from './midi';
|
||||
|
||||
const { tetris, tetrisRev, shapeShifted } = tunes;
|
||||
const { parse } = parser;
|
||||
@ -132,6 +133,18 @@ function App() {
|
||||
logBox.current.scrollTop = logBox.current?.scrollHeight;
|
||||
}, [log]);
|
||||
|
||||
useWebMidi({
|
||||
ready: useCallback(({ outputs }) => {
|
||||
pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `"${o.name}"`).join(' | ')}) to the pattern. `);
|
||||
}, []),
|
||||
connected: useCallback(({ outputs }) => {
|
||||
pushLog(`Midi device connected! Available: ${outputs.map((o) => `"${o.name}"`).join(', ')}`);
|
||||
}, []),
|
||||
disconnected: useCallback(({ outputs }) => {
|
||||
pushLog(`Midi device disconnected! Available: ${outputs.map((o) => `"${o.name}"`).join(', ')}`);
|
||||
}, []),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-slate-900 flex flex-col">
|
||||
<header className="flex-none w-full h-16 px-2 flex border-b border-gray-200 bg-white justify-between">
|
||||
|
||||
93
repl/src/midi.ts
Normal file
93
repl/src/midi.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isNote } from 'tone';
|
||||
import _WebMidi from 'webmidi';
|
||||
import { Pattern as _Pattern } from '../../strudel.mjs';
|
||||
import * as Tone from 'tone';
|
||||
|
||||
const WebMidi: any = _WebMidi;
|
||||
const Pattern = _Pattern as any;
|
||||
|
||||
export default function enableWebMidi() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (WebMidi.enabled) {
|
||||
// if already enabled, just resolve WebMidi
|
||||
resolve(WebMidi);
|
||||
return;
|
||||
}
|
||||
WebMidi.enable((err: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(WebMidi);
|
||||
});
|
||||
});
|
||||
}
|
||||
const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
||||
|
||||
Pattern.prototype.midi = function (output: string, channel = 1) {
|
||||
return this.fmap((value: any) => ({
|
||||
...value,
|
||||
onTrigger: (time: number, event: any) => {
|
||||
if (!isNote(value)) {
|
||||
throw new Error('not a note: ' + value);
|
||||
}
|
||||
if (!WebMidi.enabled) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
if (!WebMidi.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
const device = output ? outputByName(output) : WebMidi.outputs[0];
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device ${output ? output : ''} not found. Use one of ${WebMidi.outputs
|
||||
.map((o: any) => `"${o.name}"`)
|
||||
.join(' | ')}`
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
const timingOffset = WebMidi.time - Tone.context.currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
// const inMs = '+' + (time - Tone.context.currentTime) * 1000;
|
||||
// await enableWebMidi()
|
||||
device.playNote(value, channel, {
|
||||
time,
|
||||
duration: event.duration * 1000,
|
||||
// velocity: velocity ?? 0.5,
|
||||
velocity: 0.9,
|
||||
});
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export function useWebMidi(props?: any) {
|
||||
const { ready, connected, disconnected } = props;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [outputs, setOutputs] = useState<any[]>(WebMidi?.outputs || []);
|
||||
useEffect(() => {
|
||||
enableWebMidi()
|
||||
.then(() => {
|
||||
// Reacting when a new device becomes available
|
||||
WebMidi.addListener('connected', (e: any) => {
|
||||
setOutputs([...WebMidi.outputs]);
|
||||
connected?.(WebMidi, e);
|
||||
});
|
||||
// Reacting when a device becomes unavailable
|
||||
WebMidi.addListener('disconnected', (e: any) => {
|
||||
setOutputs([...WebMidi.outputs]);
|
||||
disconnected?.(WebMidi, e);
|
||||
});
|
||||
ready?.(WebMidi);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (err) {
|
||||
//throw new Error("Web Midi could not be enabled...");
|
||||
console.warn('Web Midi could not be enabled..');
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [ready, connected, disconnected, outputs]);
|
||||
const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
||||
return { loading, outputs, outputByName };
|
||||
}
|
||||
@ -2,6 +2,7 @@ import * as krill from '../krill-parser';
|
||||
import * as strudel from '../../strudel.mjs';
|
||||
import { Scale, Note, Interval } from '@tonaljs/tonal';
|
||||
import './tone';
|
||||
import './midi';
|
||||
import * as toneStuff from './tone';
|
||||
import shapeshifter from './shapeshifter';
|
||||
|
||||
|
||||
@ -21,6 +21,8 @@ export const shapeShifted = `stack(
|
||||
).rev()
|
||||
).slow(16).rev()`;
|
||||
|
||||
export const tetrisMidi = `${shapeShifted}.midi('IAC-Treiber Bus 1')`;
|
||||
|
||||
export const tetrisWithFunctions = `stack(sequence(
|
||||
'e5', sequence('b4', 'c5'), 'd5', sequence('c5', 'b4'),
|
||||
'a4', sequence('a4', 'c5'), 'e5', sequence('d5', 'c5'),
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"importsNotUsedAsValues": "error"
|
||||
"importsNotUsedAsValues": "error",
|
||||
"noImplicitAny": false
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user