diff --git a/data/config.json b/data/config.json index 40c022d..f10520d 100644 --- a/data/config.json +++ b/data/config.json @@ -1,17 +1,28 @@ { "wled": { - "ctrls": [ - ] + "ctrls": [] }, "midi": { "inputs": [ + { + "name": "Teensy MIDI", + "controlButtons": {}, + "clock": false, + "sustainPedal": null, + "mainTrigger": { + "channel": 0, + "note": 81 + } + }, { "name": "Digital Piano", "keyboard": { "channel": 0 }, + "controlButtons": {}, "clock": false, - "sustainPedal": true + "sustainPedal": true, + "mainTrigger": null }, { "name": "VI49 Out", @@ -21,20 +32,39 @@ }, { "name": "DTX drums", + "alias": "iojijxdwdcwcwcv", "controlButtons": { - "A1": { "channel": 9, "note": 59 } + "A1": { + "channel": 9, + "note": 62 + } }, + "clock": false, + "sustainPedal": null, "mainTrigger": { - "channel": 9, "note": 59 + "channel": 9, + "note": 59 } }, { "name": "IAC Driver Bus 1", "controlButtons": { - "A1": { "channel": 0, "note": 50 }, - "A2": { "channel": 9, "note": 61 }, - "A3": { "channel": 9, "note": 66 }, - "B3": { "channel": 9, "note": 60 } + "A1": { + "channel": 0, + "note": 50 + }, + "A2": { + "channel": 9, + "note": 61 + }, + "A3": { + "channel": 9, + "note": 66 + }, + "B3": { + "channel": 9, + "note": 60 + } } }, { @@ -46,23 +76,59 @@ { "name": "SPD-SX", "controlButtons": { - "A1": { "channel": 9, "note": 65 }, - "A2": { "channel": 9, "note": 61 }, - "A3": { "channel": 9, "note": 66 }, - "B3": { "channel": 9, "note": 60 } + "A1": { + "channel": 9, + "note": 65 + }, + "A2": { + "channel": 9, + "note": 61 + }, + "A3": { + "channel": 9, + "note": 66 + }, + "B3": { + "channel": 9, + "note": 60 + } } }, { "name": "Launchkey Mini MK3 MIDI Port", "controlButtons": { - "A1": { "channel": 9, "note": 40 }, - "A2": { "channel": 9, "note": 41 }, - "A3": { "channel": 9, "note": 42 }, - "A4": { "channel": 9, "note": 43 }, - "B1": { "channel": 9, "note": 36 }, - "B2": { "channel": 9, "note": 37 }, - "B3": { "channel": 9, "note": 38 }, - "B4": { "channel": 9, "note": 39 } + "A1": { + "channel": 9, + "note": 40 + }, + "A2": { + "channel": 9, + "note": 41 + }, + "A3": { + "channel": 9, + "note": 42 + }, + "A4": { + "channel": 9, + "note": 43 + }, + "B1": { + "channel": 9, + "note": 36 + }, + "B2": { + "channel": 9, + "note": 37 + }, + "B3": { + "channel": 9, + "note": 38 + }, + "B4": { + "channel": 9, + "note": 39 + } }, "keyboard": { "channel": 0 @@ -73,39 +139,89 @@ { "name": "nanoKEY Studio Bluetooth", "controlButtons": { - "A1": { "channel": 1, "note": 40 }, - "A2": { "channel": 1, "note": 41 }, - "A3": { "channel": 1, "note": 42 }, - "A4": { "channel": 1, "note": 43 }, - "B1": { "channel": 1, "note": 44 }, - "B2": { "channel": 1, "note": 45 }, - "B3": { "channel": 1, "note": 46 }, - "B4": { "channel": 1, "note": 47 } + "A1": { + "channel": 1, + "note": 40 + }, + "A2": { + "channel": 1, + "note": 41 + }, + "A3": { + "channel": 1, + "note": 42 + }, + "A4": { + "channel": 1, + "note": 43 + }, + "B1": { + "channel": 1, + "note": 44 + }, + "B2": { + "channel": 1, + "note": 45 + }, + "B3": { + "channel": 1, + "note": 46 + }, + "B4": { + "channel": 1, + "note": 47 + } }, "keyboard": { "channel": 0 }, "mainTrigger": { - "channel": 1, "note": 40 + "channel": 1, + "note": 40 } }, { "name": "nanoKEY Studio KEYBOARD/CTRL", "controlButtons": { - "A1": { "channel": 1, "note": 40 }, - "A2": { "channel": 1, "note": 41 }, - "A3": { "channel": 1, "note": 42 }, - "A4": { "channel": 1, "note": 43 }, - "B1": { "channel": 1, "note": 44 }, - "B2": { "channel": 1, "note": 45 }, - "B3": { "channel": 1, "note": 46 }, - "B4": { "channel": 1, "note": 47 } + "A1": { + "channel": 1, + "note": 40 + }, + "A2": { + "channel": 1, + "note": 41 + }, + "A3": { + "channel": 1, + "note": 42 + }, + "A4": { + "channel": 1, + "note": 43 + }, + "B1": { + "channel": 1, + "note": 44 + }, + "B2": { + "channel": 1, + "note": 45 + }, + "B3": { + "channel": 1, + "note": 46 + }, + "B4": { + "channel": 1, + "note": 47 + } }, "keyboard": { "channel": 0 }, "mainTrigger": { - "channel": 1, "note": 40 + "channel": 1, + "note": 40 } }, { @@ -113,6 +229,12 @@ "keyboard": { "channel": 0 } + }, + { + "name": "microKEY2 Air KEYBOARD", + "keyboard": { + "channel": 0 + } } ], "outputs": [ diff --git a/server/src/server.ts b/server/src/server.ts index 754bed7..ec947d9 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,4 +1,5 @@ import http from 'http'; +import fs from 'fs'; import express from 'express'; import cors from 'cors'; @@ -6,7 +7,7 @@ import cors from 'cors'; import {SerializedAction, getActionMap} from '@shared/actions/control_panel_actions'; import type App from '@shared/app'; import {GlobalState} from '@shared/state/global_state'; -import {SubmitControlPanelActionAPIResponse, GetStateAPIResponse} from '@shared/types/api_types'; +import {SubmitControlPanelActionAPIResponse, GetStateAPIResponse, SetConfigAPIRequest, SetConfigAPIResponse} from '@shared/types/api_types'; import {initWebsocketServer} from './websocket_server'; @@ -36,6 +37,18 @@ export default async function initServer(app: App): Promise { }); }); + server.post('/config', async (req, res) => { + const config = req.body.config; + + app.setConfig(config); + + await fs.promises.writeFile('../data/config.json', JSON.stringify(config, null, 2)); + + res.json({ + data: app.getState(), + }); + }); + server.post('/action', async (req, res) => { const action = req.body.action; diff --git a/shared/.eslintrc.json b/shared/.eslintrc.json index 13cda10..927c7be 100644 --- a/shared/.eslintrc.json +++ b/shared/.eslintrc.json @@ -109,7 +109,7 @@ } ], "multiline-ternary": [1, "never"], - "new-cap": 2, + "new-cap": "off", "new-parens": 2, "newline-before-return": 0, "newline-per-chained-call": 0, diff --git a/shared/app.ts b/shared/app.ts index e1aaf48..16f6248 100644 --- a/shared/app.ts +++ b/shared/app.ts @@ -1,6 +1,8 @@ +import {ApplicationModeName} from 'constants/application_mode_constants'; + import {BehaviorSubject, Subject, Subscription} from 'rxjs'; -import ProgressionModeManager from './application_mode_managers/progression_mode_manager'; +import ProgressionModeManager from './application_mode_managers/progression_mode/progression_mode_manager'; import {CHORDS} from './constants/chord_constants'; import {OutputChordSupervisor} from './io/midi/output_chord_supervisor'; @@ -17,11 +19,19 @@ import {AdhocProgressionState, ProgressionState} from './state/progression_state import {ApplicationModeManager} from './application_mode_managers/application_mode_manager'; import AdhocChordCompositionMode from './application_mode_managers/adhoc_chord_mode/achoc_chord_composition_mode'; import AdhocChordPlaybackMode from './application_mode_managers/adhoc_chord_mode/achoc_chord_playback_mode'; +// import {initialState} from './application_mode_managers/adhoc_chord_mode/playback_state'; export default class App { private globalStateSubject: Subject; private controlButtonSubscription: Subscription; + adhocCompositionMode?: AdhocChordCompositionMode; + // adhocCompositionMode?: AdhocChordCompositionMode = new AdhocChordCompositionMode(this.midiService, this); + adhocPlaybackMode?: AdhocChordPlaybackMode; + + private activeMode: ApplicationModeManager; + // private activeMode: ApplicationModeManager; + constructor( private midi: EasyMidi, private stdin: Stdin, @@ -30,6 +40,12 @@ export default class App { ) { this.globalStateSubject = new BehaviorSubject(this.getState()); this.controlButtonSubscription = this.midiService.subscribeToControlButtons(this.handleControlButton); + + // this.adhocPlaybackMode = new AdhocChordPlaybackMode(initialState, this.midiService, this); + // this.activeMode = this.adhocPlaybackMode!; + + this.progressionMode = new ProgressionModeManager(this.midiService, this.wledService, this.config, this); + this.activeMode = this.progressionMode; } handleControlButton = (index: number) => { @@ -50,12 +66,6 @@ export default class App { progressionMode?: ProgressionModeManager; // progressionMode = new ProgressionModeManager(this.midiService, this.wledService, this.config, this); - // adhocCompositionMode?: AdhocChordCompositionMode; - adhocCompositionMode?: AdhocChordCompositionMode = new AdhocChordCompositionMode(this.midiService, this); - adhocPlaybackMode?: AdhocChordPlaybackMode; - - private activeMode: ApplicationModeManager = this.adhocCompositionMode!; - chordSupervisor = new OutputChordSupervisor(this.midiService); deps = { @@ -71,7 +81,13 @@ export default class App { }; getUserData = () => this.userData; + getConfig = () => this.config; + setConfig = async (config: Config) => { + this.config = config; + this.broadcastState(); + }; + getProgressionState: () => ProgressionState | undefined = () => this.progressionMode?.getState(); getAdhocState: () => AdhocProgressionState | undefined = () => { if (this.adhocCompositionMode) { @@ -86,7 +102,10 @@ export default class App { }; getState = (): GlobalState => { + const currentMode = this.activeMode && (this.activeMode as {constructor: {name: string}}).constructor.name as ApplicationModeName; + return { + currentMode, config: this.config, userData: this.userData, // progression: this.getProgressionState(), @@ -162,10 +181,6 @@ export default class App { this.wledService.setRandomPreset(); }, - testMidiNote: () => { - - }, - lockInProgression: () => { if (!this.adhocCompositionMode) { return; diff --git a/shared/application_mode_managers/adhoc_chord_mode/achoc_chord_playback_mode.ts b/shared/application_mode_managers/adhoc_chord_mode/achoc_chord_playback_mode.ts index 34da24a..2ff5376 100644 --- a/shared/application_mode_managers/adhoc_chord_mode/achoc_chord_playback_mode.ts +++ b/shared/application_mode_managers/adhoc_chord_mode/achoc_chord_playback_mode.ts @@ -28,7 +28,7 @@ export default class AdhocChordPlaybackMode implements ApplicationModeManager n.note); - this.app.playSpecificChord(notes); + // this.app.playSpecificChord(notes); } } diff --git a/shared/application_mode_managers/adhoc_chord_mode/playback_state.ts b/shared/application_mode_managers/adhoc_chord_mode/playback_state.ts new file mode 100644 index 0000000..2ccaa12 --- /dev/null +++ b/shared/application_mode_managers/adhoc_chord_mode/playback_state.ts @@ -0,0 +1,18 @@ + +import {Chords} from '../../types/adhoc_chord_mode_types'; + +import {AdhocProgressionState} from '../../state/progression_state'; + +import {michaelSet1} from '../../constants/progression_constants'; + +const chords: Chords = michaelSet1[0].map((chord) => chord.map((note) => ({ + note, + velocity: 100, + channel: 1, +}))); + +export const initialState: AdhocProgressionState = { + chords, + currentIndex: -1, + mode: 'playback', +}; diff --git a/shared/application_mode_managers/progression_mode_manager.ts b/shared/application_mode_managers/progression_mode/progression_mode_manager.ts similarity index 89% rename from shared/application_mode_managers/progression_mode_manager.ts rename to shared/application_mode_managers/progression_mode/progression_mode_manager.ts index cceb87c..d759355 100644 --- a/shared/application_mode_managers/progression_mode_manager.ts +++ b/shared/application_mode_managers/progression_mode/progression_mode_manager.ts @@ -1,19 +1,19 @@ import {Subscription} from 'rxjs'; -import {jimmySet1, jimmySet2, michaelSet1} from '../constants/progression_constants'; +import {jimmySet1, jimmySet2, michaelSet1} from '../../constants/progression_constants'; -import MidiService, {MidiSubjectMessage} from '../io/midi/midi_service'; -import WledService from '../io/wled/wled_service'; -import {Config} from '../types/config_types/config_types'; +import MidiService, {MidiSubjectMessage} from '../../io/midi/midi_service'; +import WledService from '../../io/wled/wled_service'; +import {Config} from '../../types/config_types/config_types'; -import {log} from '../utils'; +import {log} from '../../utils'; -import type App from '../app'; -import {MidiInstrumentName} from '../constants/midi_instrument_constants'; -import {ProgressionState} from '../state/progression_state'; -import {equalControlButton, equalKeyboard} from '../io/midi/midi_utls'; +import type App from '../../app'; +import {MidiInstrumentName} from '../../constants/midi_instrument_constants'; +import {ProgressionState} from '../../state/progression_state'; +import {equalControlButton, equalKeyboard} from '../../io/midi/midi_utls'; -import {ApplicationModeManager} from './application_mode_manager'; +import {ApplicationModeManager} from '../application_mode_manager'; type MidiEventHandler = ((msg: MidiSubjectMessage) => void) | undefined; @@ -36,6 +36,10 @@ export default class ProgressionModeManager implements ApplicationModeManager { + return {buttonData: [{name: 'Toggle Drums M', color: 'green'}]}; + }; + private midiServiceSubject: Subscription; // private qwertyServiceSubject: Subscription; @@ -96,6 +100,13 @@ export default class ProgressionModeManager implements ApplicationModeManager { + // clearInterval(i); + // i = setInterval(this.playChord, 1000); + // this.nextProgression(); + // }, 4999); } getState = (): ProgressionState => this.progressionState; diff --git a/shared/constants/application_mode_constants.ts b/shared/constants/application_mode_constants.ts new file mode 100644 index 0000000..18f4487 --- /dev/null +++ b/shared/constants/application_mode_constants.ts @@ -0,0 +1,3 @@ +export enum ApplicationModeName { + ProgressionModeManager = 'ProgressionModeManager', +} diff --git a/shared/constants/midi_instrument_constants.ts b/shared/constants/midi_instrument_constants.ts index 6880789..52d3a0c 100644 --- a/shared/constants/midi_instrument_constants.ts +++ b/shared/constants/midi_instrument_constants.ts @@ -4,11 +4,15 @@ export enum MidiInstrumentName { SPD_SX = 'SPD-SX', LAUNCHKEY_MINI = 'Launchkey Mini MK3 MIDI Port', KORG_MICRO_KEY = 'microKEY2-37 Air Bluetooth', + KORG_MICRO_KEY_USB = 'microKEY2 Air KEYBOARD', KORG_NANO_KEY_STUDIO_BLUETOOTH = 'nanoKEY Studio Bluetooth', KORG_NANO_KEY_STUDIO_USB = 'nanoKEY Studio KEYBOARD/CTRL', IAC_DRIVER_BUS_1 = 'IAC Driver Bus 1', + IAC_DRIVER_BUS_2 = 'IAC Driver Bus 2', DTX_DRUMS = 'DTX drums', GRAND_PIANO = 'USB MIDI Interface', - VI49 = 'VI49 Out' + VI49 = 'VI49 Out', + + TEENSY_MIDI = 'Teensy MIDI', } diff --git a/shared/io/midi/midi_service.ts b/shared/io/midi/midi_service.ts index 1ab6bc3..16baeab 100644 --- a/shared/io/midi/midi_service.ts +++ b/shared/io/midi/midi_service.ts @@ -17,9 +17,9 @@ export default class MidiService { private inputs: Input[] = []; private outputs: Output[] = []; - constructor(private midi: EasyMidi, private config: Config) { + constructor(private midi: EasyMidi, private config: Pick) { this.midiEventSubject = new Subject(); - this.setupMidi(); + // this.setupMidi(); } setupMidi = async () => { diff --git a/shared/io/midi/output_chord_supervisor.ts b/shared/io/midi/output_chord_supervisor.ts index e4f5d9d..7368c25 100644 --- a/shared/io/midi/output_chord_supervisor.ts +++ b/shared/io/midi/output_chord_supervisor.ts @@ -15,8 +15,8 @@ export class OutputChordSupervisor { // nextChord = nextChord.slice(0, 4); const toRelease = this.heldDownNotes.filter((note) => !nextChord.includes(note)); - const toPress = nextChord; - // const toPress = nextChord.filter(note => !this.heldDownNotes.includes(note)); + // const toPress = nextChord; + const toPress = nextChord.filter((note) => !this.heldDownNotes.includes(note)); this.heldDownNotes = nextChord; toRelease.forEach((note) => { diff --git a/shared/old_index.ts b/shared/old_index.ts index 6281193..b091033 100644 --- a/shared/old_index.ts +++ b/shared/old_index.ts @@ -17,13 +17,18 @@ const wled: WLEDClient | undefined = undefined; export const oldMain = () => { getInputsMain(); - // const input = new easymidi.Input(MidiInstrumentName.GRAND_PIANO); - // const input = new easymidi.Input(MidiInstrumentName.DTX_DRUMS); - // const input = new easymidi.Input(MidiInstrumentName.IAC_DRIVER_BUS_1); - // const input = new easymidi.Input(MidiInstrumentName.YAMAHA_PIANO); - // const input = new easymidi.Input(MidiInstrumentName.KORG_MICRO_KEY); - const input = new easymidi.Input(MidiInstrumentName.LAUNCHKEY_MINI); - listenToAllMidiEvents(input); + try { + const input = new easymidi.Input(MidiInstrumentName.TEENSY_MIDI); + // const input = new easymidi.Input(MidiInstrumentName.DTX_DRUMS); + // const input = new easymidi.Input(MidiInstrumentName.IAC_DRIVER_BUS_2); + // const input = new easymidi.Input(MidiInstrumentName.YAMAHA_PIANO); + // const input = new easymidi.Input(MidiInstrumentName.KORG_MICRO_KEY); + // const input = new easymidi.Input(MidiInstrumentName.LAUNCHKEY_MINI); + // const input = new easymidi.Input('ESP32 BLE MIDI device Bluetooth'); + listenToAllMidiEvents(input); + } catch (e) { + console.log('Error during debug midi message logging. ' + (e as any).toString().split('\n')[0]); + } // wledMain(); diff --git a/shared/state/config_state.ts b/shared/state/config_state.ts index c3a1e7e..5ddd02d 100644 --- a/shared/state/config_state.ts +++ b/shared/state/config_state.ts @@ -1,3 +1,3 @@ -import {Config} from '../types/config_types/config_types'; +import type {Config} from '../types/config_types/config_types'; export type ConfigState = Config; diff --git a/shared/state/global_state.ts b/shared/state/global_state.ts index b2b925e..f023852 100644 --- a/shared/state/global_state.ts +++ b/shared/state/global_state.ts @@ -1,8 +1,11 @@ -import {ConfigState} from './config_state'; -import {AdhocProgressionState, ProgressionState} from './progression_state'; -import {UserDataState} from './user_data_state'; +import {ApplicationModeName} from '../constants/application_mode_constants'; + +import type {ConfigState} from './config_state'; +import type {AdhocProgressionState, ProgressionState} from './progression_state'; +import type {UserDataState} from './user_data_state'; export type GlobalState = { + currentMode: ApplicationModeName | undefined; config: ConfigState; adhocState?: AdhocProgressionState; progression?: ProgressionState; diff --git a/shared/state/progression_state.ts b/shared/state/progression_state.ts index 853937b..3b084e7 100644 --- a/shared/state/progression_state.ts +++ b/shared/state/progression_state.ts @@ -1,4 +1,4 @@ -import {Chords} from '../types/adhoc_chord_mode_types'; +import type {Chords} from '../types/adhoc_chord_mode_types'; export type ProgressionState = { currentProgression: number; diff --git a/shared/types/api_types.ts b/shared/types/api_types.ts index abe0005..50c18ed 100644 --- a/shared/types/api_types.ts +++ b/shared/types/api_types.ts @@ -1,5 +1,7 @@ import {GlobalState} from '../state/global_state'; +import {Config} from './config_types/config_types'; + export type DataResponse = { data: T; } @@ -16,6 +18,9 @@ export type APIResponse = { export type GetStateAPIResponse = APIResponse; +export type SetConfigAPIResponse = APIResponse; +export type SetConfigAPIRequest = {config: Config}; + export type SubmitControlPanelActionAPIResponse = APIResponse; export const isErrorResponse = (response: APIResponse): response is ErrorResponse => { diff --git a/shared/types/trigger_types.ts b/shared/types/trigger_types.ts index 7afb163..a8206a8 100644 --- a/shared/types/trigger_types.ts +++ b/shared/types/trigger_types.ts @@ -14,7 +14,6 @@ export type MidiTriggerMappings = { controlButtons?: {[name: string]: ControlButtonMapping | undefined}; clock?: boolean; - // these should be changed to be ControlButtonMapping to be more configurable - sustainPedal?: boolean; + sustainPedal?: ControlButtonMapping; mainTrigger?: ControlButtonMapping; } diff --git a/webapp/src/actions/action_handler_local.ts b/webapp/src/actions/action_handler_local.ts index 9c2166d..39df22c 100644 --- a/webapp/src/actions/action_handler_local.ts +++ b/webapp/src/actions/action_handler_local.ts @@ -40,7 +40,7 @@ import type {EasyMidi} from '@shared/types/easy_midi_types'; import {MidiMessageType} from '@shared/io/midi/midi_utls'; import {ControlPanelActions, getActionMap} from '@shared/actions/control_panel_actions'; -import {SubmitControlPanelActionAPIResponse, GetStateAPIResponse} from '@shared/types/api_types'; +import {SubmitControlPanelActionAPIResponse, GetStateAPIResponse, SetConfigAPIResponse} from '@shared/types/api_types'; import App from '@shared/app'; @@ -57,7 +57,7 @@ import type {ActionHandler} from './app_actions'; const conf: Config = config; -class EasyMidiWebShim implements EasyMidi { +export class EasyMidiWebShim implements EasyMidi { constructor(private webMidi: typeof WebMidi) { } enable = () => this.webMidi.enable(); @@ -178,14 +178,48 @@ export class LocalActionHandler implements ActionHandler { const midiShim = new EasyMidiWebShim(WebMidi); - // try { - this.app = new App(midiShim, stdin, conf, userData); + const conf = this.initConfig(); + this.app = new App(midiShim, stdin, conf, userData); + // try { // } catch (e) { - // alert(e) + // alert(e); // } } + initConfig = () => { + const storedConfig = localStorage.getItem('jamtools-config'); + if (!storedConfig) { + const config: Config = { + midi: { + inputs: [ + ], + outputs: [], + }, + actions: {}, + wled: { + ctrls: [], + }, + }; + + const toStore = JSON.stringify(config); + localStorage.setItem('jamtools-config', toStore); + + return config; + } + + const config = JSON.parse(storedConfig); + return config; + }; + + setConfig = async (config: Config): Promise => { + const toStore = JSON.stringify(config); + localStorage.setItem('jamtools-config', toStore); + + this.app.setConfig(config); + return {data: this.app.getState()}; + }; + subscribeToMessages = (callback: (msg: WebsocketMessage) => void) => { return this.subscribeToGlobalState((state) => { const msg: WebsocketMessage = { diff --git a/webapp/src/actions/action_handler_remote.ts b/webapp/src/actions/action_handler_remote.ts index 50b5666..0e0cf62 100644 --- a/webapp/src/actions/action_handler_remote.ts +++ b/webapp/src/actions/action_handler_remote.ts @@ -1,5 +1,7 @@ +import {Config} from '@shared/types/config_types/config_types'; + import {ControlPanelActions, SerializedAction} from '../../../shared/actions/control_panel_actions'; -import {SubmitControlPanelActionAPIResponse, GetStateAPIResponse} from '../../../shared/types/api_types'; +import {SubmitControlPanelActionAPIResponse, GetStateAPIResponse, SetConfigAPIResponse, SetConfigAPIRequest} from '../../../shared/types/api_types'; import {subscribeToMessages, WebsocketMessage} from '../websocket/websocket_client'; @@ -19,6 +21,28 @@ export class RemoteActionHandler implements ActionHandler { return subscribeToMessages(callback); }; + setConfig = async (config: Config): Promise => { + const body: SetConfigAPIRequest = { + config, + }; + + let res; + try { + res = await fetch(host + '/config', { + body: JSON.stringify(body), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (e: unknown) { + const e2 = e as Error; + return {error: e2.message}; + } + + return res.json(); + }; + submitControlPanelAction = async (actionName: ControlPanelActions): Promise => { const action: SerializedAction = { action: actionName, diff --git a/webapp/src/actions/app_actions.ts b/webapp/src/actions/app_actions.ts index acb24b3..fe29907 100644 --- a/webapp/src/actions/app_actions.ts +++ b/webapp/src/actions/app_actions.ts @@ -1,12 +1,14 @@ import {ControlPanelActions} from '@shared/actions/control_panel_actions'; -import {GetStateAPIResponse, SubmitControlPanelActionAPIResponse} from '@shared/types/api_types'; +import {GetStateAPIResponse, SetConfigAPIResponse, SubmitControlPanelActionAPIResponse} from '@shared/types/api_types'; import type {subscribeToMessages} from '../websocket/websocket_client'; +import {Config} from '@shared/types/config_types/config_types'; export interface ActionHandler { submitControlPanelAction(actionName: ControlPanelActions): Promise; fetchGlobalState(): Promise; + setConfig(config: Config): Promise; subscribeToMessages: typeof subscribeToMessages; } diff --git a/webapp/src/components/control_panel/control_button.tsx b/webapp/src/components/control_panel/control_button.tsx index d1e3baa..a91932b 100644 --- a/webapp/src/components/control_panel/control_button.tsx +++ b/webapp/src/components/control_panel/control_button.tsx @@ -13,7 +13,7 @@ export type Props = { const isDrumColorAction = (action: string) => action === ControlPanelActions.TOGGLE_DRUM_COLOR_ACTION; const isDrumMusicAction = (action: string) => action === ControlPanelActions.TOGGLE_DRUM_MUSIC_ACTION; -export default function ControlButton(props: Props) { +export default function ControlButton(props: Props): React.ReactNode { const {globalState} = useGlobalState(); const onClick = async () => { diff --git a/webapp/src/components/midi_config_view/control_button_editor.tsx b/webapp/src/components/midi_config_view/control_button_editor.tsx new file mode 100644 index 0000000..e36b903 --- /dev/null +++ b/webapp/src/components/midi_config_view/control_button_editor.tsx @@ -0,0 +1,60 @@ +import React, {useEffect, useState} from 'react'; + +type ControlButtonProps = { + buttonName?: string; + buttonMapping?: ControlButtonMapping; + submit: (mapping: ControlButtonMapping | null) => void; +}; + +export default function ControlButtonEditor({buttonName, buttonMapping, submit}: ControlButtonProps) { + const [name, setName] = useState(buttonName || ''); + const [channel, setChannel] = useState(buttonMapping?.channel || null); + const [note, setNote] = useState(buttonMapping?.note || null); + + useEffect(() => { + setChannel(buttonMapping?.channel ?? null); + setNote(buttonMapping?.note ?? null); + }, [buttonMapping]); + + useEffect(() => { + setName(buttonName || ''); + }, [buttonName]); + + return ( +
+ + setName(e.target.value)} + disabled={true} + /> + + + setChannel(Number(e.target.value))} + /> + + + setNote(Number(e.target.value))} + /> + + +
+ ); +} + +type ControlButtonMapping = { + channel: number; + note: number; +} | null; diff --git a/webapp/src/components/midi_config_view/midi_config_view.tsx b/webapp/src/components/midi_config_view/midi_config_view.tsx new file mode 100644 index 0000000..d4ef93c --- /dev/null +++ b/webapp/src/components/midi_config_view/midi_config_view.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import {useGlobalState} from 'src/hooks/use_global_state'; + +import {MidiInputConfig} from '@shared/types/config_types/config_types'; + +import {ConfigState} from '@shared/state/config_state'; + +import MidiInputForm from './midi_input_form'; + +export default function MidiConfigView() { + const { + globalState, + actionHandler, + } = useGlobalState(); + + if (!globalState) { + return ( +
+ Fetching global state +
+ ); + } + + const submitMidiInputConfigForm = (instrument: MidiInputConfig) => { + const inputs = globalState.config.midi.inputs; + const index = inputs.findIndex((i) => i.name === instrument.name); + const newArr = [...inputs.slice(0, index), instrument, ...inputs.slice(index + 1)]; + const config: ConfigState = { + ...globalState.config, + midi: { + ...globalState.config.midi, + inputs: newArr, + }, + }; + + actionHandler.setConfig(config); + }; + + const inputs = globalState.config.midi.inputs.map((instrument, i) => ( +
+

+ {i + 1}. {instrument.name} +

+ submitMidiInputConfigForm(instrument)} + /> +
+ )); + + return ( +
+ Time to config the midi +
+ {inputs} +
+
+ ); +} diff --git a/webapp/src/components/midi_config_view/midi_input_form.tsx b/webapp/src/components/midi_config_view/midi_input_form.tsx new file mode 100644 index 0000000..6a2e3fc --- /dev/null +++ b/webapp/src/components/midi_config_view/midi_input_form.tsx @@ -0,0 +1,127 @@ +import React, {useState, useEffect} from 'react'; + +import {MidiInputConfig} from '@shared/types/config_types/config_types'; +import {ControlButtonMapping} from '@shared/types/trigger_types'; + +import ControlButtonEditor from './control_button_editor'; + +type Props = { + instrument: MidiInputConfig; + submit: (instrument: MidiInputConfig) => void; +}; + +export default function MidiInputForm({instrument, submit}: Props) { + const [name, setName] = useState(instrument.name); + const [alias, setAlias] = useState(instrument.alias); + const [keyboardChannel, setKeyboardChannel] = useState(instrument.keyboard?.channel); + const [controlButtons, setControlButtons] = useState<{[name: string]: ControlButtonMapping | undefined}>(instrument.controlButtons || {}); + const [clock, setClock] = useState(instrument.clock || false); + const [sustainPedal, setSustainPedal] = useState(instrument.sustainPedal || null); + const [mainTrigger, setMainTrigger] = useState(instrument.mainTrigger || null); + + const handleSubmit = (e?: React.FormEvent) => { + e?.preventDefault(); + + const mappings: MidiInputConfig = { + name, + alias: alias || undefined, + keyboard: keyboardChannel === undefined ? undefined : {channel: keyboardChannel}, + controlButtons, + clock, + sustainPedal, + mainTrigger, + }; + + submit(mappings); + }; + + useEffect(() => { + // Handle prop changes and reinitialize state if necessary + setName(instrument.name); + setAlias(instrument.alias); + setKeyboardChannel(instrument.keyboard?.channel); + setControlButtons(instrument.controlButtons || {}); + setClock(instrument.clock || false); + setSustainPedal(instrument.sustainPedal || null); + setMainTrigger(instrument.mainTrigger || null); + }, [instrument]); + + const updateControlMapping = (name: string, mapping: ControlButtonMapping) => { + const mappings = { + ...controlButtons, + [name]: mapping, + }; + setControlButtons(mappings); + }; + + const controlButtonComponents = Object.keys(controlButtons).map((name) => { + const btn = controlButtons[name]!; + return ( + updateControlMapping(name, mapping)} + /> + ); + }); + + return ( +
+
+ + +
+ +
+ + setAlias(e.target.value)} + /> +
+ +
+ + setKeyboardChannel(Number(e.target.value))} + /> +
+ +
+ {controlButtonComponents} +
+ +
+ + setClock(e.target.checked)} + /> +
+ + {/* The sustainPedal and mainTrigger inputs can be similarly added as controlButtons */} + + +
+ ); +} + +// type ControlButtonMapping = { +// channel: number; +// note: number; +// } | null; + +// type MidiTriggerMappings = { +// name: string; +// alias?: string; +// keyboard?: { channel: number }; +// controlButtons?: { [name: string]: ControlButtonMapping | undefined }; +// clock?: boolean; +// sustainPedal?: ControlButtonMapping; +// mainTrigger?: ControlButtonMapping; +// }; diff --git a/webapp/src/hooks/use_global_state.tsx b/webapp/src/hooks/use_global_state.tsx index 3f2b46f..125e59a 100644 --- a/webapp/src/hooks/use_global_state.tsx +++ b/webapp/src/hooks/use_global_state.tsx @@ -64,7 +64,6 @@ export const GlobalStateProvider = (props: GlobalStateProviderProps) => { messages, setMessages, globalState, - setGlobalState, actionHandler: props.actionHandler, localMode: props.localMode, }), [messages, globalState]); diff --git a/webapp/src/pages/midi_config_page.tsx b/webapp/src/pages/midi_config_page.tsx index e23e419..5bdc604 100644 --- a/webapp/src/pages/midi_config_page.tsx +++ b/webapp/src/pages/midi_config_page.tsx @@ -1,15 +1,9 @@ import React from 'react'; -import {useGlobalState} from '../hooks/use_global_state'; +import MidiConfigView from '../components/midi_config_view/midi_config_view'; export default function MidiConfigPage() { - const { - messages, - } = useGlobalState(); - return ( -
- Time to config the midi -
+ ); } diff --git a/webapp/src/styles.scss b/webapp/src/styles.scss index 53d81d1..af8914b 100644 --- a/webapp/src/styles.scss +++ b/webapp/src/styles.scss @@ -1,6 +1,6 @@ body { background-color: black; - // color: turquoise; + color: turquoise; font-size: 48; font-family: 'Courier New', Courier, monospace }