DVX_GUI/src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c
2026-09-29 17:03:51 -05:00

182 lines
6.1 KiB
C

// 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 <pc.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#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
}
}