// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // mpuDrv.c -- the MPU-401 MIDI port driver (mpu401.drv) // // A synth driver (audioDrv.h "Synthesizer drivers") for an external MIDI // synthesizer (a Roland MT-32 or SC-55, a General MIDI module) on an // MPU-401 or compatible port, in UART mode: bytes written to the data // port go out on the MIDI cable as they are. Looked for only when // DVX.INI [audio] midi = mpu401 (at mpuPort, hex, default 330h), because // a port with nothing plugged in would play nothing. // // Status port (data port + 1): bit 6 clear = ready to take a byte, bit 7 // clear = a byte to read. Reset: FFh to the command (status) port, the // port answers FEh; UART mode: 3Fh, answered FEh too (UART-only clones // may not answer that one). Bytes the port sends (active sensing, input) // are read and dropped. put waits a little for the port to be ready and // otherwise returns false, so the caller sends the byte later; it runs at // interrupt time too and is pinned. Close sends All Notes Off on every // channel and resets the port (out of UART mode). #include "audioDrv.h" #include #include #include #include #define PORT_BASE_HEX 16 #define MPU_STATUS 1 // status / command port: data port + 1 #define MPU_NOT_READY 0x40 // status: cannot take a byte #define MPU_NO_DATA 0x80 // status: nothing to read #define MPU_CMD_RESET 0xFF #define MPU_CMD_UART 0x3F #define MPU_ACK 0xFE #define MPU_ACK_READS 50000 // status reads to wait for an answer #define MPU_PUT_READS 64 // status reads put waits for the port #define MPU_DRAIN_MAX 16 // input bytes dropped per put // Sent on close: All Notes Off (controller 123) on each channel #define MIDI_CONTROL 0xB0 #define MIDI_CHANNELS 16 #define MIDI_ALL_NOTES_OFF 123 typedef struct { uint16_t data; uint16_t status; } MpuT; static MpuT sMpu; // ============================================================ // Prototypes // ============================================================ static void mpuClose(void); static bool mpuCommand(uint8_t cmd); static bool mpuLock(bool lock); static bool mpuOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info); static bool mpuPut(uint8_t byte); static void mpuPutWait(uint8_t byte); AUDIO_SYNTH_DRIVER("mpu401", AUDIO_PRIORITY_CARD, .kind = AUDIO_SYNTH_MIDI, .open = mpuOpen, .close = mpuClose, .put = mpuPut ); static void mpuClose(void) { for (uint8_t ch = 0; ch < MIDI_CHANNELS; ch++) { mpuPutWait((uint8_t)(MIDI_CONTROL | ch)); mpuPutWait(MIDI_ALL_NOTES_OFF); mpuPutWait(0); } mpuCommand(MPU_CMD_RESET); mpuLock(false); } // Send a command and wait for its acknowledgement (other bytes read on // the way are dropped). static bool mpuCommand(uint8_t cmd) { int32_t reads = 0; while ((inportb(sMpu.status) & MPU_NOT_READY) && reads < MPU_ACK_READS) { reads++; } outportb(sMpu.status, cmd); for (reads = 0; reads < MPU_ACK_READS; reads++) { if (!(inportb(sMpu.status) & MPU_NO_DATA) && inportb(sMpu.data) == MPU_ACK) { return true; } } return false; } static bool mpuLock(bool lock) { const AudioLockItemT items[] = { { (const void *)mpuPut, 0 }, { &sMpu, sizeof(sMpu) } }; return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock); } // A port left in UART mode ignores the first reset (it takes FFh as // data); the second one is answered. static bool mpuOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info) { if (strcasecmp(prefsGetString(cfg->prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, AUDIO_MIDI_AUTO), AUDIO_MIDI_MPU401) != 0) { return false; } sMpu.data = (uint16_t)strtoul(prefsGetString(cfg->prefs, AUDIO_INI_SECTION, AUDIO_INI_MPU_PORT, AUDIO_MPU_PORT_DEFAULT), NULL, PORT_BASE_HEX); sMpu.status = (uint16_t)(sMpu.data + MPU_STATUS); if (!mpuCommand(MPU_CMD_RESET) && !mpuCommand(MPU_CMD_RESET)) { return false; } // UART-only clones may not acknowledge this. mpuCommand(MPU_CMD_UART); if (!mpuLock(true)) { return false; } snprintf(info->description, sizeof(info->description), "MPU-401 MIDI port at %Xh", (unsigned)sMpu.data); return true; } // Drop what the port has sent, then send the byte if the port gets ready // soon enough. static bool mpuPut(uint8_t byte) { for (int32_t i = 0; i < MPU_DRAIN_MAX && !(inportb(sMpu.status) & MPU_NO_DATA); i++) { (void)inportb(sMpu.data); } for (int32_t i = 0; i < MPU_PUT_READS; i++) { if (!(inportb(sMpu.status) & MPU_NOT_READY)) { outportb(sMpu.data, byte); return true; } } return false; } // put, retried for as long as an acknowledgement may take. static void mpuPutWait(uint8_t byte) { for (int32_t i = 0; i < MPU_ACK_READS / MPU_PUT_READS && !mpuPut(byte); i++) { // the port is busy } }