Skip to content

Commit 33869ae

Browse files
Add PSR S910 midi transmitter
1 parent eece6dc commit 33869ae

File tree

6 files changed

+801
-123
lines changed

6 files changed

+801
-123
lines changed

.prettierrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"trailingComma": "es5",
33
"tabWidth": 2,
44
"semi": false,
5-
"singleQuote": true
5+
"singleQuote": true,
6+
"printWidth": 160
67
}

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Layout from './components/Layout'
55
import { BrowserRouter, Route, Routes, useLocation } from 'react-router-dom'
66
import Home from './pages/Home/Home'
77
import MidiMonitor from './pages/MidiMonitor'
8-
import MidiTransmitter from './pages/MidiTransmitter'
8+
import MidiTransmitter from './pages/MidiTransmitter/MidiTransmitter'
99
import MidiPlayer from './pages/MidiPlayer'
1010
import MidiSynth from './pages/MidiSynth'
1111

src/components/WebMidi.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const WebMidiContext = createContext<WebMidiContextState>({
1818
export const WebMidiProvider: React.FC<{ children?: React.ReactNode }> = ({
1919
children,
2020
}) => {
21-
const [webMidiEnabled, webMidiError] = useRequestWebMidi()
21+
const [webMidiEnabled, webMidiError] = useRequestWebMidi({ sysex: true })
2222
const { inputs, outputs } = useWebMidiDeviceConnectionListeners()
2323

2424
if (webMidiError !== null) {
@@ -39,7 +39,9 @@ export const WebMidiProvider: React.FC<{ children?: React.ReactNode }> = ({
3939
)
4040
}
4141

42-
function useRequestWebMidi(): [boolean, Error | null] {
42+
export function useRequestWebMidi(options?: {
43+
sysex?: boolean
44+
}): [boolean, Error | null] {
4345
const [webMidiEnabled, setWebMidiEnabled] = useState(false)
4446
const [webMidiError, setWebMidiError] = useState<Error | null>(null)
4547

@@ -50,15 +52,15 @@ function useRequestWebMidi(): [boolean, Error | null] {
5052
}
5153
;(async () => {
5254
try {
53-
await WebMidi.enable()
55+
await WebMidi.enable(options)
5456
console.log('WebMidi enabled')
5557
AppToaster.show({ message: `WebMidi enabled` })
5658
setWebMidiEnabled(true)
5759
} catch (err) {
5860
setWebMidiError(err as Error)
5961
}
6062
})()
61-
}, [setWebMidiEnabled, setWebMidiError])
63+
}, [setWebMidiEnabled, setWebMidiError, options])
6264

6365
return [webMidiEnabled, webMidiError]
6466
}

src/pages/MidiMonitor.tsx

Lines changed: 38 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,27 @@
11
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
22
import useLocalStorageState from '../components/hooks/useLocalStorageState'
3-
import { WebMidi, Event } from 'webmidi'
3+
import { WebMidi, Event, ControlChangeMessageEvent, NoteMessageEvent, MessageEvent } from 'webmidi'
44
import MidiDeviceSelector from '../components/MidiDeviceSelector'
5-
import {
6-
Button,
7-
Checkbox,
8-
ControlGroup,
9-
FormGroup,
10-
NumericInput,
11-
} from '@blueprintjs/core'
5+
import { Button, Checkbox, ControlGroup, FormGroup, NumericInput } from '@blueprintjs/core'
126

137
const eventTypes: { [key: string]: (e: any) => string } = {
148
activesensing: (e: any) => '',
159
clock: (e: any) => '',
16-
controlchange: (e: { controller: { number: any; name: any }; value: any }) =>
17-
[e.controller.number, e.controller.name, e.value].join(' '),
18-
noteon: (e: { note: { name: any; octave: any }; rawVelocity: any }) =>
19-
[e.note.name + e.note.octave, e.rawVelocity].join(' '),
20-
noteoff: (e: { note: { name: any; octave: any }; rawVelocity: any }) =>
21-
[e.note.name + e.note.octave, e.rawVelocity].join(' '),
10+
controlchange: (e: ControlChangeMessageEvent) => [e.controller.number, e.controller.name, e.value].join(' '),
11+
noteon: (e: NoteMessageEvent) => [e.note.name + e.note.octave, e.rawValue].join(' '),
12+
noteoff: (e: NoteMessageEvent) => [e.note.name + e.note.octave, e.rawValue].join(' '),
13+
sysex: (e: MessageEvent) =>
14+
Array.from(e.message.rawData.values())
15+
.map((x) => x.toString(16).padStart(2, '0').toUpperCase())
16+
.join(' '),
2217
}
2318

2419
export default function MidiMonitor() {
2520
const [logs, setLogs] = useState('')
2621
const [tempo, setTempo] = useLocalStorageState('midi:monitor:tempo', 100)
27-
const [deviceId, setDeviceId] = useLocalStorageState(
28-
'midi:monitor:device',
29-
''
30-
)
31-
const [selectedEventTypes, setEventTypes] = useLocalStorageState(
32-
'midi:monitor:eventTypes',
33-
['noteon']
34-
)
35-
const [selectedChannels, setChannels] = useLocalStorageState(
36-
'midi:monitor:channels',
37-
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
38-
)
22+
const [deviceId, setDeviceId] = useLocalStorageState('midi:monitor:device', '')
23+
const [selectedEventTypes, setEventTypes] = useLocalStorageState('midi:monitor:eventTypes', ['noteon'])
24+
const [selectedChannels, setChannels] = useLocalStorageState('midi:monitor:channels', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
3925

4026
const device = WebMidi.getInputById(deviceId)
4127

@@ -46,11 +32,7 @@ export default function MidiMonitor() {
4632
}
4733

4834
useEffect(() => {
49-
const listener = (e: {
50-
type: keyof Event
51-
timestamp: number
52-
channel: { toString: () => string }
53-
}) => {
35+
const listener = (e: { type: keyof Event; timestamp: number; channel: { toString: () => string } }) => {
5436
const getMessage = eventTypes[e.type as any] as any
5537
if (!getMessage) {
5638
console.warn(`No getMessage function for ${e.type}`)
@@ -73,15 +55,16 @@ export default function MidiMonitor() {
7355
.join('\n')
7456
)
7557
}
58+
const options = {
59+
channels: selectedChannels,
60+
}
7661
if (device) {
7762
selectedEventTypes.forEach((eventType: any) => {
78-
device.addListener(eventType, listener, { channels: selectedChannels })
63+
device.addListener(eventType, listener, options)
7964
})
8065
return () => {
8166
selectedEventTypes.forEach((eventType: any) => {
82-
device.removeListener(eventType, listener, {
83-
channels: selectedChannels,
84-
})
67+
device.removeListener(eventType, listener, options)
8568
})
8669
}
8770
}
@@ -97,22 +80,15 @@ export default function MidiMonitor() {
9780
<div>
9881
<FormGroup>
9982
<ControlGroup>
100-
<MidiDeviceSelector
101-
mode="input"
102-
label="Input"
103-
value={deviceId}
104-
onChange={(v: any) => setDeviceId(v)}
105-
/>
83+
<MidiDeviceSelector mode="input" label="Input" value={deviceId} onChange={(v: any) => setDeviceId(v)} />
10684
<NumericInput
10785
leftIcon="time"
10886
// rightElement={<Tag minimal>bpm</Tag>}
10987
placeholder="Tempo"
11088
value={tempo}
11189
min={32}
11290
max={240}
113-
onChange={(e: { target: { value: any } }) =>
114-
setTempo(e.target.value)
115-
}
91+
onChange={(e: { target: { value: any } }) => setTempo(e.target.value)}
11692
/>
11793
<Button onClick={clear}>Clear</Button>
11894
</ControlGroup>
@@ -127,9 +103,7 @@ export default function MidiMonitor() {
127103
if ((e.target as HTMLInputElement).checked) {
128104
setEventTypes((t: any) => [...t, eventType])
129105
} else {
130-
setEventTypes((t: any[]) =>
131-
t.filter((e: string) => e !== eventType)
132-
)
106+
setEventTypes((t: any[]) => t.filter((e: string) => e !== eventType))
133107
}
134108
}}
135109
>
@@ -138,31 +112,24 @@ export default function MidiMonitor() {
138112
))}
139113
</FormGroup>
140114
<FormGroup label="Channels">
141-
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(
142-
(channel) => (
143-
<Checkbox
144-
key={channel}
145-
inline={true}
146-
checked={selectedChannels.includes(channel)}
147-
onChange={(e) => {
148-
if ((e.target as HTMLInputElement).checked) {
149-
setChannels((ch: any) => [...ch, channel])
150-
} else {
151-
setChannels((ch: any[]) =>
152-
ch.filter((e: number) => e !== channel)
153-
)
154-
}
155-
}}
156-
>
157-
{channel}
158-
</Checkbox>
159-
)
160-
)}
115+
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map((channel) => (
116+
<Checkbox
117+
key={channel}
118+
inline={true}
119+
checked={selectedChannels.includes(channel)}
120+
onChange={(e) => {
121+
if ((e.target as HTMLInputElement).checked) {
122+
setChannels((ch: any) => [...ch, channel])
123+
} else {
124+
setChannels((ch: any[]) => ch.filter((e: number) => e !== channel))
125+
}
126+
}}
127+
>
128+
{channel}
129+
</Checkbox>
130+
))}
161131
</FormGroup>
162-
<pre
163-
ref={logRef}
164-
className="bg-black bg-opacity-20 overflow-y-scroll h-[500px]"
165-
>
132+
<pre ref={logRef} className="bg-black bg-opacity-20 overflow-y-scroll h-[500px]">
166133
<code className="py-4 block">{logs}</code>
167134
</pre>
168135
</div>

src/pages/MidiTransmitter.tsx renamed to src/pages/MidiTransmitter/MidiTransmitter.tsx

Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import React, { useState } from 'react'
2-
import useLocalStorageState from '../components/hooks/useLocalStorageState'
2+
import useLocalStorageState from '../../components/hooks/useLocalStorageState'
33
import { WebMidi, Output } from 'webmidi'
4-
import MidiDeviceSelector from '../components/MidiDeviceSelector'
5-
import Select from '../components/Select'
4+
import MidiDeviceSelector from '../../components/MidiDeviceSelector'
5+
import Select from '../../components/Select'
66
import {
77
Button,
88
FormGroup,
99
HTMLTable,
1010
InputGroup,
1111
NumericInput,
1212
} from '@blueprintjs/core'
13+
import PsrS910 from './PsrS910'
1314

1415
const options = [
16+
'psr-s910',
1517
'playNote',
1618
'send',
1719
'sendActiveSensing',
@@ -41,48 +43,6 @@ const options = [
4143
'stopNote',
4244
]
4345

44-
export const handleFormSubmit = (func: any) => (event: any) => {
45-
event.preventDefault()
46-
return func()
47-
}
48-
49-
export default function MidiTransmitter() {
50-
const [deviceId, setDeviceId] = useLocalStorageState(
51-
'midi:transmitter:device',
52-
''
53-
)
54-
const [method, setMethod] = useLocalStorageState(
55-
'midi:transmitter:method',
56-
''
57-
)
58-
const [state, setState] = useState({})
59-
60-
const device = WebMidi.getOutputById(deviceId)
61-
62-
console.log(device)
63-
64-
return (
65-
<div>
66-
<FormGroup label="Device">
67-
<MidiDeviceSelector
68-
mode="output"
69-
label="Output"
70-
value={deviceId}
71-
onChange={(v) => setDeviceId(v)}
72-
/>
73-
</FormGroup>
74-
<FormGroup label="Event">
75-
<Select
76-
options={['', ...options]}
77-
value={method}
78-
onChange={(event) => setMethod(event.currentTarget.value)}
79-
/>
80-
</FormGroup>
81-
{renderMethod(method, device, state, setState)}
82-
</div>
83-
)
84-
}
85-
8646
const methods: {
8747
[key: string]: {
8848
fields: (keyof typeof fieldTypes)[]
@@ -179,12 +139,57 @@ const fieldTypes = {
179139
program: 'string', // check if this is right
180140
}
181141

142+
export const handleFormSubmit = (func: any) => (event: any) => {
143+
event.preventDefault()
144+
return func()
145+
}
146+
147+
export default function MidiTransmitter() {
148+
const [deviceId, setDeviceId] = useLocalStorageState(
149+
'midi:transmitter:device',
150+
''
151+
)
152+
const [method, setMethod] = useLocalStorageState(
153+
'midi:transmitter:method',
154+
''
155+
)
156+
const [state, setState] = useState({})
157+
158+
const device = WebMidi.getOutputById(deviceId)
159+
160+
console.log(device)
161+
162+
return (
163+
<div>
164+
<FormGroup label="Device">
165+
<MidiDeviceSelector
166+
mode="output"
167+
label="Output"
168+
value={deviceId}
169+
onChange={(v) => setDeviceId(v)}
170+
/>
171+
</FormGroup>
172+
<FormGroup label="Event">
173+
<Select
174+
options={['', ...options]}
175+
value={method}
176+
onChange={(event) => setMethod(event.currentTarget.value)}
177+
/>
178+
</FormGroup>
179+
{renderMethod(method, device, state, setState)}
180+
</div>
181+
)
182+
}
183+
182184
const renderMethod = (
183185
method: string,
184-
device: Output | false,
186+
device: Output,
185187
state: any,
186188
setState: any
187189
) => {
190+
if (method === 'psr-s910') {
191+
return <PsrS910 device={device} />
192+
}
188193
const m = methods[method]
189194
if (!m) {
190195
return <div>Not implemented</div>

0 commit comments

Comments
 (0)