diff --git a/web-editor/.prettierrc.json b/web-editor/.prettierrc.json index 29a2402..334a585 100644 --- a/web-editor/.prettierrc.json +++ b/web-editor/.prettierrc.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/prettierrc", - "semi": false, + "semi": true, "singleQuote": true, "printWidth": 100 } diff --git a/web-editor/README.md b/web-editor/README.md index 0aca07e..5a20658 100644 --- a/web-editor/README.md +++ b/web-editor/README.md @@ -1,4 +1,4 @@ -# Denki Oto Web MIDI Editor +# denki oto Web MIDI Editor This template should help get you started developing with Vue 3 in Vite. diff --git a/web-editor/index.html b/web-editor/index.html index 315ce0b..3c4504f 100644 --- a/web-editor/index.html +++ b/web-editor/index.html @@ -4,7 +4,7 @@ -
( + portMap: P, + ): MIDIInput | MIDIOutput | null { + for (const [, value] of portMap) { + if (validPort(value)) { + return value as P extends MIDIInputMap + ? MIDIInput + : P extends MIDIOutputMap + ? MIDIOutput + : never; + } } - if (!validPort(eventPort)) { - console.log('not valid port') - return - } + return null; + } - const isInput = eventPort.type === 'input' + let midiAccess: MIDIAccess | null = null; - const store = isInput ? inputDeviceState : outputDeviceState - const port = isInput ? input : output + const stateChangeHandler = (e: MIDIConnectionEvent) => { + const eventPort = e.port; + debug('State change fired', extractPortProperties(eventPort)); - store.value = - eventPort.state === 'connected' && eventPort.connection === 'open' - ? 'connected' - : 'disconnected' + // Always rescan all ports + input.value = findPort(midiAccess!.inputs as MIDIInputMap); + output.value = findPort(midiAccess!.outputs as MIDIOutputMap); - debug( - 'State Change Event', - JSON.stringify( - { - ...extractPortProperties(eventPort), - connectionState: store.value, - }, - null, - 2, - ), - ) + if (input.value) { + inputDeviceState.value = + input.value.state === 'connected' && input.value.connection === 'open' + ? 'connected' + : 'disconnected'; + if (input.value.connection !== 'open') { + input.value.open?.(); + } + } else { + inputDeviceState.value = 'disconnected'; + } - if (eventPort.state === 'disconnected') { - port.value = null + if (output.value) { + outputDeviceState.value = + output.value.state === 'connected' && output.value.connection === 'open' + ? 'connected' + : 'disconnected'; + if (output.value.connection !== 'open') { + output.value.open?.(); + } } else { - port.value = isInput ? (eventPort as MIDIInput) : (eventPort as MIDIOutput) + outputDeviceState.value = 'disconnected'; } - } + }; watch(input, (newValue) => { - inputAbortController.value.abort() - inputAbortController.value = new AbortController() + inputAbortController.value.abort(); + inputAbortController.value = new AbortController(); if (newValue) { callbacks.forEach((callback) => newValue.addEventListener('midimessage', (e) => callback(e, output.value), { signal: inputAbortController.value.signal, }), - ) + ); } - }) + }); watch(output, (newValue) => { - outputAbortController.value.abort() - outputAbortController.value = new AbortController() - - newValue?.open() - }) - - const extractPortProperties = (port: MIDIPort | null) => { - return { - connection: port?.connection, - id: port?.id, - manufacturer: port?.manufacturer, - name: port?.name, - state: port?.state, - type: port?.type, - } - } + outputAbortController.value.abort(); + + outputAbortController.value = new AbortController(); + + newValue?.open(); + }); watch( [access, connected, input, inputDeviceState, output, outputDeviceState], @@ -128,115 +129,90 @@ export const useWebMidi = ( null, 2, ), - ) + ); }, { immediate: true }, - ) + ); - function findPort(portMap: MIDIInputMap): MIDIInput - function findPort(portMap: MIDIOutputMap): MIDIOutput - function findPort
( - portMap: P, - ): MIDIInput | MIDIOutput | null { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [_, value] of portMap) { - if (validPort(value)) { - return value as P extends MIDIInputMap - ? MIDIInput - : P extends MIDIOutputMap - ? MIDIOutput - : never - } - } + const requestAccess = () => { + navigator + .requestMIDIAccess({ sysex: true }) + .then( + (value) => { + midiAccess = value; - return null - } + debug('MIDI access request: granted'); - // Why the type assertions? TypeScript has taken the view of only supporting the smaller - // subset covered by all browsers, and not the expanded list with things like Web MIDI - // that Chromium et al. support. My understanding, at least. - const midiPermission = { - name: 'midi' as PermissionName, - sysex: true, - } as PermissionDescriptor - - navigator.permissions - .query(midiPermission) - .then((result) => { - debug(`MIDI + Sysex permissions query result: ${result.state}`) - - if (result.state === 'prompt') { - access.value = 'requesting' - } else if (result.state === 'denied') { - access.value = 'disabled' // WebMIDI API permission was denied by user prompt or permission policy - } - }) - .then(() => { - navigator.requestMIDIAccess({ sysex: true }).then( - (midiAccess) => { - debug('MIDI access request: granted') + midiAccess.addEventListener('statechange', stateChangeHandler); - midiAccess.addEventListener('statechange', stateChangeHandler) + debug( + 'Available MIDI Input ports:', + JSON.stringify([...midiAccess.inputs.values()].map(extractPortProperties), null, 2), + ); - const inputPorts: MIDIInput[] = [] + debug( + 'Available MIDI Output ports:', + JSON.stringify([...midiAccess.outputs.values()].map(extractPortProperties), null, 2), + ); - midiAccess.inputs.forEach((i) => inputPorts.push(i as MIDIInput)) + input.value = findPort(midiAccess.inputs as MIDIInputMap); - debug( - 'Available MIDI Input ports:', - JSON.stringify(inputPorts.map(extractPortProperties), null, 2), - ) - // debug( - // 'Available MIDI Input ports:', - // JSON.stringify( - // [...midiAccess.inputs.values()].map(extractPortProperties), - // null, - // 2, - // ), - // ); + output.value = findPort(midiAccess.outputs as MIDIOutputMap); - const outputPorts: MIDIOutput[] = [] + if (input.value) { + input.value.open?.(); + } - midiAccess.outputs.forEach((o) => outputPorts.push(o as MIDIOutput)) + if (output.value) { + output.value.open?.(); + } - debug( - 'Available MIDI Output ports:', - JSON.stringify(outputPorts.map(extractPortProperties), null, 2), - ) - // debug( - // 'Available MIDI Output ports:', - // JSON.stringify( - // [...midiAccess.outputs.values()].map(extractPortProperties), - // null, - // 2, - // ), - // ); - - input.value = findPort(midiAccess.inputs as MIDIInputMap) - output.value = findPort(midiAccess.outputs as MIDIOutputMap) - - access.value = 'enabled' + access.value = 'enabled'; }, () => { - debug('MIDI access request: denied') + debug('MIDI access request: denied'); - access.value = 'disabled' + access.value = 'disabled'; }, ) - }) - .catch((e) => { - // Likely caused by 'midi' not being in the PermissionName enumeration - // or requestMIDIAccess not being a property of navigator: - // i.e. browser doesn't support web MIDI. - debug(`MIDI (permissions query/access request) error: ${e.message}`) + .catch((e) => { + debug(`MIDI access error: ${e.message}`); + + access.value = 'disabled'; + }); + }; + + // Permissions query fallback + if ('permissions' in navigator && 'query' in navigator.permissions) { + navigator.permissions + .query({ name: 'midi' as PermissionName }) + .then((result) => { + debug(`MIDI permissions query result: ${result.state}`); + + if (result.state === 'prompt') { + access.value = 'requesting'; + } else if (result.state === 'denied') { + access.value = 'disabled'; + } + }) + .finally(requestAccess) + .catch(requestAccess); + } else { + requestAccess(); + } - access.value = 'disabled' - }) + onUnmounted(() => { + inputAbortController.value.abort(); + + outputAbortController.value.abort(); + + midiAccess?.removeEventListener('statechange', stateChangeHandler); + }); return { access: readonly(access), connected, input: readonly(input), output: readonly(output), - } -} + }; +}; diff --git a/web-editor/src/components/8x2/MappingsGroup.vue b/web-editor/src/components/8x2/MappingsGroup.vue index 93c4b7f..8b6b6e5 100644 --- a/web-editor/src/components/8x2/MappingsGroup.vue +++ b/web-editor/src/components/8x2/MappingsGroup.vue @@ -126,7 +126,7 @@ const link = computed({ }); - diff --git a/web-editor/src/components/PotConfiguration.vue b/web-editor/src/components/PotConfiguration.vue index 59fd134..41bf991 100644 --- a/web-editor/src/components/PotConfiguration.vue +++ b/web-editor/src/components/PotConfiguration.vue @@ -186,23 +186,6 @@ onMounted(() => {