From f6c4bdb8a6da6e9bd04c89366692fbeaf2e1d19a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 9 Feb 2022 22:10:11 +0100 Subject: [PATCH] added midi support --- repl/package-lock.json | 98 ++++++++---------------------------------- repl/package.json | 5 ++- repl/src/App.tsx | 13 ++++++ repl/src/midi.ts | 93 +++++++++++++++++++++++++++++++++++++++ repl/src/parse.ts | 1 + repl/src/tunes.ts | 2 + repl/tsconfig.json | 3 +- 7 files changed, 132 insertions(+), 83 deletions(-) create mode 100644 repl/src/midi.ts diff --git a/repl/package-lock.json b/repl/package-lock.json index 74909c21..e6ab44ab 100644 --- a/repl/package-lock.json +++ b/repl/package-lock.json @@ -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", diff --git a/repl/package.json b/repl/package.json index b55e1139..27414e0f 100644 --- a/repl/package.json +++ b/repl/package.json @@ -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", diff --git a/repl/src/App.tsx b/repl/src/App.tsx index ad54944b..375e29cd 100644 --- a/repl/src/App.tsx +++ b/repl/src/App.tsx @@ -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 (
diff --git a/repl/src/midi.ts b/repl/src/midi.ts new file mode 100644 index 00000000..d27165b3 --- /dev/null +++ b/repl/src/midi.ts @@ -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(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 }; +} diff --git a/repl/src/parse.ts b/repl/src/parse.ts index 9bee9369..0dc2ad74 100644 --- a/repl/src/parse.ts +++ b/repl/src/parse.ts @@ -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'; diff --git a/repl/src/tunes.ts b/repl/src/tunes.ts index 1f3efd70..dd9708af 100644 --- a/repl/src/tunes.ts +++ b/repl/src/tunes.ts @@ -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'), diff --git a/repl/tsconfig.json b/repl/tsconfig.json index 3018cb7e..d1e7c67a 100644 --- a/repl/tsconfig.json +++ b/repl/tsconfig.json @@ -23,6 +23,7 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, - "importsNotUsedAsValues": "error" + "importsNotUsedAsValues": "error", + "noImplicitAny": false } }