From c528ff116df4be2ac193a6acd343fd1fdb5b3235 Mon Sep 17 00:00:00 2001 From: sergm Date: Sun, 12 Dec 2021 17:10:18 +0200 Subject: [PATCH] Greatly refined and improved emulation of display facilities (#66) - Introduced display emulation support in mt32emu library itself. The startup banner and the SysEx checksum error are now shown. We now also take care of the control ROM version to emulate the most prominent quirks. - The new API includes a function for retrieving the current display state and a function to reset the display to the Master Volume mode. - The interface ReportHandler has been extended to provide callbacks to get notified about changes in display state. - Additionally, there is a limited support for displaying the emulated LCD on narrow hardware LCDs that have width of 16 characters only. --- mt32emu/CMakeLists.txt | 1 + mt32emu/NEWS.txt | 5 + mt32emu/src/Display.cpp | 354 ++++++++++++++++++++++++ mt32emu/src/Display.h | 91 ++++++ mt32emu/src/MemoryRegion.h | 4 +- mt32emu/src/Part.cpp | 25 +- mt32emu/src/Part.h | 3 + mt32emu/src/Poly.cpp | 17 +- mt32emu/src/Poly.h | 2 + mt32emu/src/Structures.h | 4 + mt32emu/src/Synth.cpp | 205 ++++++++++---- mt32emu/src/Synth.h | 40 ++- mt32emu/src/c_interface/c_interface.cpp | 52 +++- mt32emu/src/c_interface/c_interface.h | 19 ++ mt32emu/src/c_interface/c_types.h | 94 +++++-- mt32emu/src/c_interface/cpp_interface.h | 97 +++++-- 16 files changed, 888 insertions(+), 125 deletions(-) create mode 100644 mt32emu/src/Display.cpp create mode 100644 mt32emu/src/Display.h diff --git a/mt32emu/CMakeLists.txt b/mt32emu/CMakeLists.txt index 74d752d9..8af2fec1 100644 --- a/mt32emu/CMakeLists.txt +++ b/mt32emu/CMakeLists.txt @@ -109,6 +109,7 @@ endif(libmt32emu_SHARED) set(libmt32emu_SOURCES src/Analog.cpp src/BReverbModel.cpp + src/Display.cpp src/File.cpp src/FileStream.cpp src/LA32FloatWaveGenerator.cpp diff --git a/mt32emu/NEWS.txt b/mt32emu/NEWS.txt index 565ec8f7..b5419274 100644 --- a/mt32emu/NEWS.txt +++ b/mt32emu/NEWS.txt @@ -13,6 +13,11 @@ HEAD: * Updated the description of the module FindMT32EMU.cmake. It now creates the IMPORTED target MT32Emu::mt32emu upon success, similarly to the other scripts. Note, this module is not recommended for use with mt32emu version 2.6 and above. (#65) + * Introduced display emulation support that goes beyond the existing low-level API intended + for retrieving the synth state details. The startup banner and the SysEx checksum error + are now shown. We now also take care of the control ROM version to emulate the most + prominent quirks. Additionally, there is a limited support for displaying the emulated LCD + on narrow hardware LCDs that have width of 16 characters only. (#66) 2021-08-07: diff --git a/mt32emu/src/Display.cpp b/mt32emu/src/Display.cpp new file mode 100644 index 00000000..a1130cf5 --- /dev/null +++ b/mt32emu/src/Display.cpp @@ -0,0 +1,354 @@ +/* Copyright (C) 2003, 2004, 2005, 2006, 2008, 2009 Dean Beeler, Jerome Fisher + * Copyright (C) 2011-2021 Dean Beeler, Jerome Fisher, Sergey V. Mikayev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "internals.h" + +#include "Display.h" +#include "Part.h" +#include "Structures.h" +#include "Synth.h" + +namespace MT32Emu { + +/* Details on the emulation model. + * + * There are four display modes emulated: + * - main (Master Volume), set upon startup after showing the welcoming banner; + * - program change notification; + * - custom display message received via a SysEx; + * - error banner (e.g. the MIDI message checksum error). + * Stuff like cursor blinking, patch selection mode, test mode, reaction to the front panel buttons, etc. is out of scope, as more + * convenient UI/UX solutions are likely desired in applications, if at all. + * + * Note, despite the LAPC and CM devices come without the LCD and the front panel buttons, the control ROM does support these, + * if connected to the main board. That's intended for running the test mode in a service centre, as documented. + * + * Within the aforementioned scope, the observable hardware behaviour differs noticeably, depending on the control ROM version. + * At least three milestones can be identified: + * - with MT-32 control ROM V1.06, custom messages are no longer shown unless the display is in the main (Master Volume) mode; + * - with MT-32 control ROM V2.04, new function introduced - Display Reset yet added many other changes (taking the full SysEx + * address into account when processing custom messages and special handling of the ASCII control characters are among them); + * all the second-gen devices, including LAPC-I and CM-32L, behave very similarly; + * - in the third-gen devices, the LCD support was partially cut down in the control ROM (basically, only the status + * of the test mode, the ROM version and the checksum warnings are shown) - it's not fun, so this is NOT emulated. + * + * Features of the old-gen units. + * - Any message with the first address byte 0x20 is processed and has some effect on the LCD. Messages with any other first + * address byte (e.g. starting with 0x21 or 0x1F7F7F with an overlap) are not considered display-relevant. + * - The second and the third address byte are largely irrelevant. Only presence of the second address byte makes an observable + * difference, not the data within. + * - Any string received in the custom message is normalised - all ASCII control characters are replaced with spaces, messages + * shorter than 20 bytes are filled up with spaces to the full supported length. However, should a timbre name contain an ASCII + * control character, it is displayed nevertheless, with zero meaning the end-of-string. + * - Special message 0x20 (of just 1 address byte) shows the contents of the custom message buffer with either the last received + * message or the empty buffer initially filled with spaces. See the note below about the priorities of the display modes. + * - Messages containing two or three bytes with just the address are considered empty and fill the custom message buffer with + * all spaces. The contents of the empty buffer is then shown, depending on the priority of the current display mode. + * - Timing: custom messages are shown until an external event occurs like pressing a front panel button, receiving a new custom + * message, program change, etc., and for indefinitely long otherwise. A program change notification is shown for about 1300 + * milliseconds; when the timer expires, the display returns to the main mode (irrespective to the current display mode). + * When an error occurs, the warning is shown for a limited time only, similarly to the program change notifications. + * - The earlier old-gen devices treat all display modes with equal priority, except the main mode, which has a lower one. This + * makes it possible e.g. to replace the error banner with a custom message or a program change notification, and so on. + * A slightly improved behaviour is observed since the control ROM V1.06, when custom messages were de-prioritised. But still, + * a program change beats an error banner even in the later models. + * + * Features of the second-gen units. + * - All three bytes in SysEx address are now relevant. + * - It is possible to replace individual characters in the custom message buffer which are addressed individually within + * the range 0x200000-0x200013. + * - Writes to higher addresses up to 0x20007F simply make the custom message buffer shown, with either the last received message + * or the empty buffer initially filled with spaces. + * - Writes to address 0x200100 trigger the Display Reset function which resets the display to the main (Master Volume) mode. + * Similarly, showing an error banner is ended. If a program change notification is shown, this function does nothing, however. + * - Writes to other addresses are not considered display-relevant, albeit writing a long string to lower addresses + * (e.g. 0x1F7F7F) that overlaps the display range does result in updating and showing the custom display message. + * - Writing a long string that covers the custom message buffer and address 0x200100 does both things, i.e. updates the buffer + * and triggers the Display Reset function. + * - While the display is not in a user interaction mode, custom messages and error banners have the highest display priority. + * As long as these are shown, program change notifications are suppressed. The display only leaves this mode when the Display + * Reset function is triggered or a front panel button is pressed. Notably, when the user enters the menu, all custom messages + * are ignored, including the Display Reset command, but error banners are shown nevertheless. + * - Sending cut down messages with partially specified address rather leads to undefined behaviour, except for a two-byte message + * 0x20 0x00 which consistently shows the content of the custom message buffer (if priority permits). Otherwise, the behaviour + * depends on the previously submitted address, e.g. the two-byte version of Display Reset may fail depending on the third byte + * of the previous message. One-byte message 0x20 seemingly does Display Reset yet writes a zero character to a position derived + * from the third byte of the preceding message. + * + * Some notes on the behaviour that is common to all hardware models. + * - The display is DM2011 with LSI SED1200D-0A. This unit supports 4 user-programmable characters stored in CGRAM, all 4 get + * loaded at startup. Character #0 is empty (with the cursor underline), #1 is the full block (used to mark active parts), + * #2 is the pipe character (identical to #124 from the CGROM) and #3 is a variation on "down arrow". During normal operation, + * those duplicated characters #2 and #124 are both used in different places and character #3 can only be made visible by adding + * it either to a custom timbre name or a custom message. Character #0 is probably never shown as this code has special meaning + * in the processing routines. For simplicity, we only use characters #124 and #1 in this model. + * - When the main mode is active, the current state of the first 5 parts and the rhythm part is represented by replacing the part + * symbol with the full rectangle character (#1 from the CGRAM). For voice parts, the rectangle is shown as long as at least one + * partial is playing in a non-releasing phase on that part. For the rhythm part, the rectangle blinks briefly when a new NoteOn + * message is received on that part (sometimes even when that actually produces no sound). + */ + +static const char MASTER_VOLUME_WITH_DELIMITER[] = "| 0"; +static const char MASTER_VOLUME_WITH_DELIMITER_AND_PREFIX[] = "|vol: 0"; +static const Bit8u RHYTHM_PART_CODE = 'R'; +static const Bit8u FIELD_DELIMITER = '|'; +static const Bit8u ACTIVE_PART_INDICATOR = 1; + +static const Bit32u DISPLAYED_VOICE_PARTS_COUNT = 5; +static const Bit32u SOUND_GROUP_NAME_WITH_DELIMITER_SIZE = 8; +static const Bit32u MASTER_VOLUME_WITH_DELIMITER_SIZE = sizeof(MASTER_VOLUME_WITH_DELIMITER) - 1; +static const Bit32u MASTER_VOLUME_WITH_DELIMITER_AND_PREFIX_SIZE = sizeof(MASTER_VOLUME_WITH_DELIMITER_AND_PREFIX) - 1; + +// This is the period to show those short blinks of MIDI MESSAGE LED and the rhythm part state. +// Two related countdowns are initialised to 8 and touched each 10 milliseconds by the software timer 0 interrupt handler. +static const Bit32u BLINK_TIME_MILLIS = 80; +static const Bit32u BLINK_TIME_FRAMES = BLINK_TIME_MILLIS * SAMPLE_RATE / 1000; + +// This is based on the (free-running) TIMER1 overflow interrupt. The timer is 16-bit and clocked at 500KHz. +// The message is displayed until 10 overflow interrupts occur. At the standard sample rate, it counts +// precisely as 41943.04 frame times. +static const Bit32u SCHEDULED_DISPLAY_MODE_RESET_FRAMES = 41943; + +/** + * Copies up to lengthLimit characters from possibly null-terminated source to destination. The character of destination located + * at the position of the null terminator (if any) in source and the rest of destination are left untouched. + */ +static void copyNullTerminatedString(Bit8u *destination, const Bit8u *source, Bit32u lengthLimit) { + for (Bit32u i = 0; i < lengthLimit; i++) { + Bit8u c = source[i]; + if (c == 0) break; + destination[i] = c; + } +} + +Display::Display(Synth &useSynth) : + synth(useSynth), + lastLEDState(), + lcdDirty(), + lcdUpdateSignalled(), + lastRhythmPartState(), + mode(Mode_STARTUP_MESSAGE), + midiMessagePlayedSinceLastReset(), + rhythmNotePlayedSinceLastReset() +{ + scheduleDisplayReset(); + const Bit8u *startupMessage = &synth.controlROMData[synth.controlROMMap->startupMessage]; + memcpy(displayBuffer, startupMessage, LCD_TEXT_SIZE); + memset(customMessageBuffer, ' ', LCD_TEXT_SIZE); + memset(voicePartStates, 0, sizeof voicePartStates); +} + +void Display::checkDisplayStateUpdated(bool &midiMessageLEDState, bool &midiMessageLEDUpdated, bool &lcdUpdated) { + midiMessageLEDState = midiMessagePlayedSinceLastReset; + maybeResetTimer(midiMessagePlayedSinceLastReset, midiMessageLEDResetTimestamp); + // Note, the LED represents activity of the voice parts only. + for (Bit32u partIndex = 0; !midiMessageLEDState && partIndex < 8; partIndex++) { + midiMessageLEDState = voicePartStates[partIndex]; + } + midiMessageLEDUpdated = lastLEDState != midiMessageLEDState; + lastLEDState = midiMessageLEDState; + + if (displayResetScheduled && shouldResetTimer(displayResetTimestamp)) setMainDisplayMode(); + + if (lastRhythmPartState != rhythmNotePlayedSinceLastReset && mode == Mode_MAIN) lcdDirty = true; + lastRhythmPartState = rhythmNotePlayedSinceLastReset; + maybeResetTimer(rhythmNotePlayedSinceLastReset, rhythmStateResetTimestamp); + + lcdUpdated = lcdDirty && !lcdUpdateSignalled; + if (lcdUpdated) lcdUpdateSignalled = true; +} + +bool Display::getDisplayState(char *targetBuffer, bool narrowLCD) { + if (lcdUpdateSignalled) { + lcdDirty = false; + lcdUpdateSignalled = false; + + switch (mode) { + case Mode_CUSTOM_MESSAGE: + if (synth.controlROMFeatures->oldMT32DisplayFeatures) { + memcpy(displayBuffer, customMessageBuffer, LCD_TEXT_SIZE); + } else { + copyNullTerminatedString(displayBuffer, customMessageBuffer, LCD_TEXT_SIZE); + } + break; + case Mode_ERROR_MESSAGE: { + const Bit8u *sysexErrorMessage = &synth.controlROMData[synth.controlROMMap->sysexErrorMessage]; + memcpy(displayBuffer, sysexErrorMessage, LCD_TEXT_SIZE); + break; + } + case Mode_PROGRAM_CHANGE: { + Bit8u *writePosition = displayBuffer; + *writePosition++ = '1' + lastProgramChangePartIndex; + *writePosition++ = FIELD_DELIMITER; + if (narrowLCD) { + writePosition[TIMBRE_NAME_SIZE] = 0; + } else { + memcpy(writePosition, lastProgramChangeSoundGroupName, SOUND_GROUP_NAME_WITH_DELIMITER_SIZE); + writePosition += SOUND_GROUP_NAME_WITH_DELIMITER_SIZE; + } + copyNullTerminatedString(writePosition, lastProgramChangeTimbreName, TIMBRE_NAME_SIZE); + break; + } + case Mode_MAIN: { + Bit8u *writePosition = displayBuffer; + for (Bit32u partIndex = 0; partIndex < DISPLAYED_VOICE_PARTS_COUNT; partIndex++) { + *writePosition++ = voicePartStates[partIndex] ? ACTIVE_PART_INDICATOR : '1' + partIndex; + *writePosition++ = ' '; + } + *writePosition++ = lastRhythmPartState ? ACTIVE_PART_INDICATOR : RHYTHM_PART_CODE; + *writePosition++ = ' '; + if (narrowLCD) { + memcpy(writePosition, MASTER_VOLUME_WITH_DELIMITER, MASTER_VOLUME_WITH_DELIMITER_SIZE); + writePosition += MASTER_VOLUME_WITH_DELIMITER_SIZE; + *writePosition = 0; + } else { + memcpy(writePosition, MASTER_VOLUME_WITH_DELIMITER_AND_PREFIX, MASTER_VOLUME_WITH_DELIMITER_AND_PREFIX_SIZE); + writePosition += MASTER_VOLUME_WITH_DELIMITER_AND_PREFIX_SIZE; + } + Bit32u masterVol = synth.mt32ram.system.masterVol; + while (masterVol > 0) { + std::div_t result = std::div(masterVol, 10); + *--writePosition = '0' + result.rem; + masterVol = result.quot; + } + break; + } + default: + break; + } + } + + memcpy(targetBuffer, displayBuffer, LCD_TEXT_SIZE); + targetBuffer[LCD_TEXT_SIZE] = 0; + return lastLEDState; +} + +void Display::setMainDisplayMode() { + displayResetScheduled = false; + mode = Mode_MAIN; + lcdDirty = true; +} + +void Display::midiMessagePlayed() { + midiMessagePlayedSinceLastReset = true; + midiMessageLEDResetTimestamp = synth.renderedSampleCount + BLINK_TIME_FRAMES; +} + +void Display::rhythmNotePlayed() { + rhythmNotePlayedSinceLastReset = true; + rhythmStateResetTimestamp = synth.renderedSampleCount + BLINK_TIME_FRAMES; + midiMessagePlayed(); + if (synth.controlROMFeatures->oldMT32DisplayFeatures && mode == Mode_CUSTOM_MESSAGE) setMainDisplayMode(); +} + +void Display::voicePartStateChanged(Bit8u partIndex, bool activated) { + if (mode == Mode_MAIN) lcdDirty = true; + voicePartStates[partIndex] = activated; + if (synth.controlROMFeatures->oldMT32DisplayFeatures && mode == Mode_CUSTOM_MESSAGE) setMainDisplayMode(); +} + +void Display::masterVolumeChanged() { + if (mode == Mode_MAIN) lcdDirty = true; +} + +void Display::programChanged(Bit8u partIndex) { + if (!synth.controlROMFeatures->oldMT32DisplayFeatures && (mode == Mode_CUSTOM_MESSAGE || mode == Mode_ERROR_MESSAGE)) return; + mode = Mode_PROGRAM_CHANGE; + lcdDirty = true; + scheduleDisplayReset(); + lastProgramChangePartIndex = partIndex; + const Part *part = synth.getPart(partIndex); + lastProgramChangeSoundGroupName = synth.getSoundGroupName(part); + memcpy(lastProgramChangeTimbreName, part->getCurrentInstr(), TIMBRE_NAME_SIZE); +} + +void Display::checksumErrorOccurred() { + if (mode != Mode_ERROR_MESSAGE) { + mode = Mode_ERROR_MESSAGE; + lcdDirty = true; + } + if (synth.controlROMFeatures->oldMT32DisplayFeatures) { + scheduleDisplayReset(); + } else { + displayResetScheduled = false; + } +} + +bool Display::customDisplayMessageReceived(const Bit8u *message, Bit32u startIndex, Bit32u length) { + if (synth.controlROMFeatures->oldMT32DisplayFeatures) { + for (Bit32u i = 0; i < LCD_TEXT_SIZE; i++) { + Bit8u c = i < length ? message[i] : ' '; + if (c < 32 || 127 < c) c = ' '; + customMessageBuffer[i] = c; + } + if (!synth.controlROMFeatures->quirkDisplayCustomMessagePriority + && (mode == Mode_PROGRAM_CHANGE || mode == Mode_ERROR_MESSAGE)) return false; + // Note, real devices keep the display reset timer running. + } else { + if (startIndex > 0x80) return false; + if (startIndex == 0x80) { + if (mode != Mode_PROGRAM_CHANGE) setMainDisplayMode(); + return false; + } + displayResetScheduled = false; + if (startIndex < LCD_TEXT_SIZE) { + if (length > LCD_TEXT_SIZE - startIndex) length = LCD_TEXT_SIZE - startIndex; + memcpy(customMessageBuffer + startIndex, message, length); + } + } + mode = Mode_CUSTOM_MESSAGE; + lcdDirty = true; + return true; +} + +void Display::displayControlMessageReceived(const Bit8u *messageBytes, Bit32u length) { + Bit8u emptyMessage[] = { 0 }; + if (synth.controlROMFeatures->oldMT32DisplayFeatures) { + if (length == 1) { + customDisplayMessageReceived(customMessageBuffer, 0, LCD_TEXT_SIZE); + } else { + customDisplayMessageReceived(emptyMessage, 0, 0); + } + } else { + // Always assume the third byte to be zero for simplicity. + if (length == 2) { + customDisplayMessageReceived(emptyMessage, messageBytes[1] << 7, 0); + } else if (length == 1) { + customMessageBuffer[0] = 0; + customDisplayMessageReceived(emptyMessage, 0x80, 0); + } + } +} + +void Display::scheduleDisplayReset() { + displayResetTimestamp = synth.renderedSampleCount + SCHEDULED_DISPLAY_MODE_RESET_FRAMES; + displayResetScheduled = true; +} + +bool Display::shouldResetTimer(Bit32u scheduledResetTimestamp) { + // Deals with wrapping of renderedSampleCount. + return Bit32s(scheduledResetTimestamp - synth.renderedSampleCount) < 0; +} + +void Display::maybeResetTimer(bool &timerState, Bit32u scheduledResetTimestamp) { + if (timerState && shouldResetTimer(scheduledResetTimestamp)) timerState = false; +} + +} // namespace MT32Emu diff --git a/mt32emu/src/Display.h b/mt32emu/src/Display.h new file mode 100644 index 00000000..e0dc1d2e --- /dev/null +++ b/mt32emu/src/Display.h @@ -0,0 +1,91 @@ +/* Copyright (C) 2003, 2004, 2005, 2006, 2008, 2009 Dean Beeler, Jerome Fisher + * Copyright (C) 2011-2021 Dean Beeler, Jerome Fisher, Sergey V. Mikayev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef MT32EMU_DISPLAY_H +#define MT32EMU_DISPLAY_H + +#include "globals.h" +#include "Types.h" + +namespace MT32Emu { + +class Synth; + +/** Facilitates emulation of internal state of the MIDI MESSAGE LED and the MT-32 LCD. */ +class Display { +public: + static const unsigned int LCD_TEXT_SIZE = 20; + + enum Mode { + Mode_MAIN, // a.k.a. Master Volume + Mode_STARTUP_MESSAGE, + Mode_PROGRAM_CHANGE, + Mode_CUSTOM_MESSAGE, + Mode_ERROR_MESSAGE + }; + + Display(Synth &synth); + void checkDisplayStateUpdated(bool &midiMessageLEDState, bool &midiMessageLEDUpdated, bool &lcdUpdated); + /** Returns whether the MIDI MESSAGE LED is ON and fills the targetBuffer parameter. */ + bool getDisplayState(char *targetBuffer, bool narrowLCD); + void setMainDisplayMode(); + + void midiMessagePlayed(); + void rhythmNotePlayed(); + void voicePartStateChanged(Bit8u partIndex, bool activated); + void masterVolumeChanged(); + void programChanged(Bit8u partIndex); + void checksumErrorOccurred(); + bool customDisplayMessageReceived(const Bit8u *message, Bit32u startIndex, Bit32u length); + void displayControlMessageReceived(const Bit8u *messageBytes, Bit32u length); + +private: + typedef Bit8u DisplayBuffer[LCD_TEXT_SIZE]; + + static const unsigned int TIMBRE_NAME_SIZE = 10; + + Synth &synth; + + bool lastLEDState; + bool lcdDirty; + bool lcdUpdateSignalled; + bool lastRhythmPartState; + bool voicePartStates[8]; + + Bit8u lastProgramChangePartIndex; + const char *lastProgramChangeSoundGroupName; + Bit8u lastProgramChangeTimbreName[TIMBRE_NAME_SIZE]; + + Mode mode; + Bit32u displayResetTimestamp; + bool displayResetScheduled; + Bit32u midiMessageLEDResetTimestamp; + bool midiMessagePlayedSinceLastReset; + Bit32u rhythmStateResetTimestamp; + bool rhythmNotePlayedSinceLastReset; + + DisplayBuffer displayBuffer; + DisplayBuffer customMessageBuffer; + + void scheduleDisplayReset(); + bool shouldResetTimer(Bit32u scheduledResetTimestamp); + void maybeResetTimer(bool &timerState, Bit32u scheduledResetTimestamp); +}; + +} // namespace MT32Emu + +#endif // #ifndef MT32EMU_DISPLAY_H diff --git a/mt32emu/src/MemoryRegion.h b/mt32emu/src/MemoryRegion.h index af47edf4..2cacd577 100644 --- a/mt32emu/src/MemoryRegion.h +++ b/mt32emu/src/MemoryRegion.h @@ -120,7 +120,9 @@ class SystemMemoryRegion : public MemoryRegion { }; class DisplayMemoryRegion : public MemoryRegion { public: - DisplayMemoryRegion(Synth *useSynth) : MemoryRegion(useSynth, NULL, NULL, MR_Display, MT32EMU_MEMADDR(0x200000), SYSEX_BUFFER_SIZE - 1, 1) {} + // Note, we set realMemory to NULL despite the real devices buffer inbound strings. However, it is impossible to retrieve them. + // This entrySize permits emulation of handling a 20-byte display message sent to an old-gen device at address 0x207F7F. + DisplayMemoryRegion(Synth *useSynth) : MemoryRegion(useSynth, NULL, NULL, MR_Display, MT32EMU_MEMADDR(0x200000), 0x4013, 1) {} }; class ResetMemoryRegion : public MemoryRegion { public: diff --git a/mt32emu/src/Part.cpp b/mt32emu/src/Part.cpp index ae55967c..15e8f0df 100644 --- a/mt32emu/src/Part.cpp +++ b/mt32emu/src/Part.cpp @@ -63,6 +63,7 @@ Part::Part(Synth *useSynth, unsigned int usePartNum) { expression = 100; pitchBend = 0; activePartialCount = 0; + activeNonReleasingPolyCount = 0; memset(patchCache, 0, sizeof(patchCache)); } @@ -166,7 +167,7 @@ void Part::refresh() { patchCache[t].reverb = patchTemp->patch.reverbSwitch > 0; } memcpy(currentInstr, timbreTemp->common.name, 10); - synth->newTimbreSet(partNum, patchTemp->patch.timbreGroup, patchTemp->patch.timbreNum, currentInstr); + synth->newTimbreSet(partNum); updatePitchBenderRange(); } @@ -380,6 +381,7 @@ void RhythmPart::noteOn(unsigned int midiKey, unsigned int velocity) { synth->printDebug("%s: Attempted to play invalid key %d (velocity %d)", name, midiKey, velocity); return; } + synth->rhythmNotePlayed(); unsigned int key = midiKey; unsigned int drumNum = key - 24; int drumTimbreNum = rhythmTemp[drumNum].timbre; @@ -609,6 +611,27 @@ void Part::partialDeactivated(Poly *poly) { } } +void RhythmPart::polyStateChanged(PolyState, PolyState) {} + +void Part::polyStateChanged(PolyState oldState, PolyState newState) { + switch (newState) { + case POLY_Playing: + if (activeNonReleasingPolyCount++ == 0) synth->voicePartStateChanged(partNum, true); + break; + case POLY_Releasing: + case POLY_Inactive: + if (oldState == POLY_Playing || oldState == POLY_Held) { + if (--activeNonReleasingPolyCount == 0) synth->voicePartStateChanged(partNum, false); + } + break; + default: + break; + } +#ifdef MT32EMU_TRACE_POLY_STATE_CHANGES + synth->printDebug("Part %d: Changed poly state %d->%d, activeNonReleasingPolyCount=%d", partNum, oldState, newState, activeNonReleasingPolyCount); +#endif +} + PolyList::PolyList() : firstPoly(NULL), lastPoly(NULL) {} bool PolyList::isEmpty() const { diff --git a/mt32emu/src/Part.h b/mt32emu/src/Part.h index f81a89bd..8d08f1f5 100644 --- a/mt32emu/src/Part.h +++ b/mt32emu/src/Part.h @@ -55,6 +55,7 @@ class Part { bool holdpedal; unsigned int activePartialCount; + unsigned int activeNonReleasingPolyCount; PatchCache patchCache[4]; PolyList activePolys; @@ -122,6 +123,7 @@ class Part { // This should only be called by Poly void partialDeactivated(Poly *poly); + virtual void polyStateChanged(PolyState oldState, PolyState newState); // These are rather specialised, and should probably only be used by PartialManager bool abortFirstPoly(PolyState polyState); @@ -146,6 +148,7 @@ class RhythmPart: public Part { unsigned int getAbsTimbreNum() const; void setPan(unsigned int midiPan); void setProgram(unsigned int patchNum); + void polyStateChanged(PolyState oldState, PolyState newState); }; } // namespace MT32Emu diff --git a/mt32emu/src/Poly.cpp b/mt32emu/src/Poly.cpp index cc9aeda7..35b11c9f 100644 --- a/mt32emu/src/Poly.cpp +++ b/mt32emu/src/Poly.cpp @@ -53,7 +53,7 @@ void Poly::reset(unsigned int newKey, unsigned int newVelocity, bool newSustain, activePartialCount--; } } - state = POLY_Inactive; + setState(POLY_Inactive); } key = newKey; @@ -65,7 +65,7 @@ void Poly::reset(unsigned int newKey, unsigned int newVelocity, bool newSustain, partials[i] = newPartials[i]; if (newPartials[i] != NULL) { activePartialCount++; - state = POLY_Playing; + setState(POLY_Playing); } } } @@ -80,7 +80,7 @@ bool Poly::noteOff(bool pedalHeld) { if (state == POLY_Held) { return false; } - state = POLY_Held; + setState(POLY_Held); } else { startDecay(); } @@ -98,7 +98,7 @@ bool Poly::startDecay() { if (state == POLY_Inactive || state == POLY_Releasing) { return false; } - state = POLY_Releasing; + setState(POLY_Releasing); for (int t = 0; t < 4; t++) { Partial *partial = partials[t]; @@ -123,6 +123,13 @@ bool Poly::startAbort() { return true; } +void Poly::setState(PolyState newState) { + if (state == newState) return; + PolyState oldState = state; + state = newState; + part->polyStateChanged(oldState, newState); +} + void Poly::backupCacheToPartials(PatchCache cache[4]) { for (int partialNum = 0; partialNum < 4; partialNum++) { Partial *partial = partials[partialNum]; @@ -171,7 +178,7 @@ void Poly::partialDeactivated(Partial *partial) { } } if (activePartialCount == 0) { - state = POLY_Inactive; + setState(POLY_Inactive); if (part->getSynth()->abortingPoly == this) { part->getSynth()->abortingPoly = NULL; } diff --git a/mt32emu/src/Poly.h b/mt32emu/src/Poly.h index 6085820d..f2ccbc92 100644 --- a/mt32emu/src/Poly.h +++ b/mt32emu/src/Poly.h @@ -41,6 +41,8 @@ class Poly { Poly *next; + void setState(PolyState state); + public: Poly(); void setPart(Part *usePart); diff --git a/mt32emu/src/Structures.h b/mt32emu/src/Structures.h index 3fef4e06..60b0c89d 100644 --- a/mt32emu/src/Structures.h +++ b/mt32emu/src/Structures.h @@ -192,6 +192,8 @@ struct ControlROMFeatureSet { unsigned int quirkKeyShift : 1; unsigned int quirkTVFBaseCutoffLimit : 1; unsigned int quirkFastPitchChanges : 1; + unsigned int quirkDisplayCustomMessagePriority : 1; + unsigned int oldMT32DisplayFeatures : 1; // Features below don't actually depend on control ROM version, which is used to identify hardware model unsigned int defaultReverbMT32Compatible : 1; @@ -222,6 +224,8 @@ struct ControlROMMap { Bit16u timbreMaxTable; // 72 bytes Bit16u soundGroupsTable; // 14 bytes each entry Bit16u soundGroupsCount; + Bit16u startupMessage; // 20 characters + NULL terminator + Bit16u sysexErrorMessage; // 20 characters + NULL terminator }; struct ControlROMPCMStruct { diff --git a/mt32emu/src/Synth.cpp b/mt32emu/src/Synth.cpp index e1df22c3..a15c93d4 100644 --- a/mt32emu/src/Synth.cpp +++ b/mt32emu/src/Synth.cpp @@ -22,6 +22,7 @@ #include "Synth.h" #include "Analog.h" #include "BReverbModel.h" +#include "Display.h" #include "File.h" #include "MemoryRegion.h" #include "MidiEventQueue.h" @@ -41,7 +42,7 @@ namespace MT32Emu { // MIDI interface data transfer rate in samples. Used to simulate the transfer delay. static const double MIDI_DATA_TRANSFER_RATE = double(SAMPLE_RATE) / 31250.0 * 8.0; -static const ControlROMFeatureSet OLD_MT32_COMPATIBLE = { +static const ControlROMFeatureSet OLD_MT32_ELDER = { true, // quirkBasePitchOverflow true, // quirkPitchEnvelopeOverflow true, // quirkRingModulationNoMix @@ -50,10 +51,26 @@ static const ControlROMFeatureSet OLD_MT32_COMPATIBLE = { true, // quirkKeyShift true, // quirkTVFBaseCutoffLimit false, // quirkFastPitchChanges + true, // quirkDisplayCustomMessagePriority + true, // oldMT32DisplayFeatures true, // defaultReverbMT32Compatible true // oldMT32AnalogLPF }; -static const ControlROMFeatureSet CM32L_COMPATIBLE = { +static const ControlROMFeatureSet OLD_MT32_LATER = { + true, // quirkBasePitchOverflow + true, // quirkPitchEnvelopeOverflow + true, // quirkRingModulationNoMix + true, // quirkTVAZeroEnvLevels + true, // quirkPanMult + true, // quirkKeyShift + true, // quirkTVFBaseCutoffLimit + false, // quirkFastPitchChanges + false, // quirkDisplayCustomMessagePriority + true, // oldMT32DisplayFeatures + true, // defaultReverbMT32Compatible + true // oldMT32AnalogLPF +}; +static const ControlROMFeatureSet NEW_MT32_COMPATIBLE = { false, // quirkBasePitchOverflow false, // quirkPitchEnvelopeOverflow false, // quirkRingModulationNoMix @@ -62,6 +79,8 @@ static const ControlROMFeatureSet CM32L_COMPATIBLE = { false, // quirkKeyShift false, // quirkTVFBaseCutoffLimit false, // quirkFastPitchChanges + false, // quirkDisplayCustomMessagePriority + false, // oldMT32DisplayFeatures false, // defaultReverbMT32Compatible false // oldMT32AnalogLPF }; @@ -74,23 +93,25 @@ static const ControlROMFeatureSet CM32LN_COMPATIBLE = { false, // quirkKeyShift false, // quirkTVFBaseCutoffLimit true, // quirkFastPitchChanges + false, // quirkDisplayCustomMessagePriority + false, // oldMT32DisplayFeatures false, // defaultReverbMT32Compatible false // oldMT32AnalogLPF }; static const ControlROMMap ControlROMMaps[] = { - // ID Features PCMmap PCMc tmbrA tmbrAO, tmbrAC tmbrB tmbrBO tmbrBC tmbrR trC rhythm rhyC rsrv panpot prog rhyMax patMax sysMax timMax sndGrp sGC - { "ctrl_mt32_1_04", OLD_MT32_COMPATIBLE, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x73A6, 85, 0x57C7, 0x57E2, 0x57D0, 0x5252, 0x525E, 0x526E, 0x520A, 0x7064, 19 }, - { "ctrl_mt32_1_05", OLD_MT32_COMPATIBLE, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x7414, 85, 0x57C7, 0x57E2, 0x57D0, 0x5252, 0x525E, 0x526E, 0x520A, 0x70CA, 19 }, - { "ctrl_mt32_1_06", OLD_MT32_COMPATIBLE, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x7414, 85, 0x57D9, 0x57F4, 0x57E2, 0x5264, 0x5270, 0x5280, 0x521C, 0x70CA, 19 }, - { "ctrl_mt32_1_07", OLD_MT32_COMPATIBLE, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x73fe, 85, 0x57B1, 0x57CC, 0x57BA, 0x523C, 0x5248, 0x5258, 0x51F4, 0x70B0, 19 }, - {"ctrl_mt32_bluer", OLD_MT32_COMPATIBLE, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x741C, 85, 0x57E5, 0x5800, 0x57EE, 0x5270, 0x527C, 0x528C, 0x5228, 0x70CE, 19 }, - {"ctrl_mt32_2_04", CM32L_COMPATIBLE, 0x8100, 128, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F5D, 0x4F78, 0x4F66, 0x4899, 0x489D, 0x48B6, 0x48CD, 0x5A58, 19 }, - {"ctrl_mt32_2_06", CM32L_COMPATIBLE, 0x8100, 128, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F69, 0x4F84, 0x4F72, 0x48A5, 0x48A9, 0x48C2, 0x48D9, 0x5A64, 19 }, - {"ctrl_mt32_2_07", CM32L_COMPATIBLE, 0x8100, 128, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F81, 0x4F9C, 0x4F8A, 0x48B9, 0x48BD, 0x48D6, 0x48ED, 0x5A78, 19 }, - {"ctrl_cm32l_1_00", CM32L_COMPATIBLE, 0x8100, 256, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F65, 0x4F80, 0x4F6E, 0x48A1, 0x48A5, 0x48BE, 0x48D5, 0x5A6C, 19 }, - {"ctrl_cm32l_1_02", CM32L_COMPATIBLE, 0x8100, 256, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F93, 0x4FAE, 0x4F9C, 0x48CB, 0x48CF, 0x48E8, 0x48FF, 0x5A96, 19 }, - {"ctrl_cm32ln_1_00", CM32LN_COMPATIBLE, 0x8100, 256, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4EC7, 0x4EE2, 0x4ED0, 0x47FF, 0x4803, 0x481C, 0x4833, 0x55A2, 19 } + // ID Features PCMmap PCMc tmbrA tmbrAO, tmbrAC tmbrB tmbrBO tmbrBC tmbrR trC rhythm rhyC rsrv panpot prog rhyMax patMax sysMax timMax sndGrp sGC stMsg sErMsg + {"ctrl_mt32_1_04", OLD_MT32_ELDER, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x73A6, 85, 0x57C7, 0x57E2, 0x57D0, 0x5252, 0x525E, 0x526E, 0x520A, 0x7064, 19, 0x217A, 0x4BB6}, + {"ctrl_mt32_1_05", OLD_MT32_ELDER, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x7414, 85, 0x57C7, 0x57E2, 0x57D0, 0x5252, 0x525E, 0x526E, 0x520A, 0x70CA, 19, 0x217A, 0x4BB6}, + {"ctrl_mt32_1_06", OLD_MT32_LATER, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x7414, 85, 0x57D9, 0x57F4, 0x57E2, 0x5264, 0x5270, 0x5280, 0x521C, 0x70CA, 19, 0x217A, 0x4BBA}, + {"ctrl_mt32_1_07", OLD_MT32_LATER, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x73fe, 85, 0x57B1, 0x57CC, 0x57BA, 0x523C, 0x5248, 0x5258, 0x51F4, 0x70B0, 19, 0x217A, 0x4B92}, + {"ctrl_mt32_bluer", OLD_MT32_LATER, 0x3000, 128, 0x8000, 0x0000, false, 0xC000, 0x4000, false, 0x3200, 30, 0x741C, 85, 0x57E5, 0x5800, 0x57EE, 0x5270, 0x527C, 0x528C, 0x5228, 0x70CE, 19, 0x217A, 0x4BC6}, + {"ctrl_mt32_2_04", NEW_MT32_COMPATIBLE, 0x8100, 128, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F5D, 0x4F78, 0x4F66, 0x4899, 0x489D, 0x48B6, 0x48CD, 0x5A58, 19, 0x1EF0, 0x406D}, + {"ctrl_mt32_2_06", NEW_MT32_COMPATIBLE, 0x8100, 128, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F69, 0x4F84, 0x4F72, 0x48A5, 0x48A9, 0x48C2, 0x48D9, 0x5A64, 19, 0x1EF0, 0x4021}, + {"ctrl_mt32_2_07", NEW_MT32_COMPATIBLE, 0x8100, 128, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F81, 0x4F9C, 0x4F8A, 0x48B9, 0x48BD, 0x48D6, 0x48ED, 0x5A78, 19, 0x1EE7, 0x4035}, + {"ctrl_cm32l_1_00", NEW_MT32_COMPATIBLE, 0x8100, 256, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F65, 0x4F80, 0x4F6E, 0x48A1, 0x48A5, 0x48BE, 0x48D5, 0x5A6C, 19, 0x1EF0, 0x401D}, + {"ctrl_cm32l_1_02", NEW_MT32_COMPATIBLE, 0x8100, 256, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4F93, 0x4FAE, 0x4F9C, 0x48CB, 0x48CF, 0x48E8, 0x48FF, 0x5A96, 19, 0x1EE7, 0x4047}, + {"ctrl_cm32ln_1_00", CM32LN_COMPATIBLE, 0x8100, 256, 0x8000, 0x8000, true, 0x8080, 0x8000, true, 0x8500, 64, 0x8580, 85, 0x4EC7, 0x4EE2, 0x4ED0, 0x47FF, 0x4803, 0x481C, 0x4833, 0x55A2, 19, 0x1F59, 0x3F7C} // (Note that old MT-32 ROMs actually have 86 entries for rhythmTemp) }; @@ -154,6 +175,8 @@ class Renderer { synth.renderedSampleCount += count; } + void updateDisplayState(); + public: Renderer(Synth &useSynth) : synth(useSynth) {} @@ -225,6 +248,11 @@ class Extensions { Bit32u midiEventQueueSize; Bit32u midiEventQueueSysexStorageBufferSize; + + Display *display; + + ReportHandler2 defaultReportHandler; + ReportHandler2 *reportHandler2; }; Bit32u Synth::getLibraryVersionInt() { @@ -260,13 +288,8 @@ Synth::Synth(ReportHandler *useReportHandler) : controlROMMap = NULL; controlROMFeatures = NULL; - if (useReportHandler == NULL) { - reportHandler = new ReportHandler; - isDefaultReportHandler = true; - } else { - reportHandler = useReportHandler; - isDefaultReportHandler = false; - } + reportHandler = useReportHandler != NULL ? useReportHandler : &extensions.defaultReportHandler; + extensions.reportHandler2 = &extensions.defaultReportHandler; extensions.preallocatedReverbMemory = false; for (int i = REVERB_MODE_ROOM; i <= REVERB_MODE_TAP_DELAY; i++) { @@ -305,18 +328,26 @@ Synth::Synth(ReportHandler *useReportHandler) : lastReceivedMIDIEventTimestamp = 0; memset(parts, 0, sizeof(parts)); renderedSampleCount = 0; + extensions.display = NULL; } Synth::~Synth() { close(); // Make sure we're closed and everything is freed - if (isDefaultReportHandler) { - delete reportHandler; - } delete &mt32ram; delete &mt32default; delete &extensions; } +void Synth::setReportHandler2(ReportHandler2 *reportHandler2) { + if (reportHandler2 != NULL) { + reportHandler = reportHandler2; + extensions.reportHandler2 = reportHandler2; + } else { + reportHandler = &extensions.defaultReportHandler; + extensions.reportHandler2 = &extensions.defaultReportHandler; + } +} + void ReportHandler::showLCDMessage(const char *data) { printf("WRITE-LCD: %s\n", data); } @@ -326,26 +357,35 @@ void ReportHandler::printDebug(const char *fmt, va_list list) { printf("\n"); } -void Synth::newTimbreSet(Bit8u partNum, Bit8u timbreGroup, Bit8u timbreNumber, const char patchName[]) { - const char *soundGroupName; - switch (timbreGroup) { +void Synth::rhythmNotePlayed() const { + extensions.display->rhythmNotePlayed(); +} + +void Synth::voicePartStateChanged(Bit8u partNum, bool partActivated) const { + extensions.display->voicePartStateChanged(partNum, partActivated); +} + +void Synth::newTimbreSet(Bit8u partNum) const { + const Part *part = getPart(partNum); + reportHandler->onProgramChanged(partNum, getSoundGroupName(part), part->getCurrentInstr()); +} + +const char *Synth::getSoundGroupName(const Part *part) const { + const PatchParam &patch = part->getPatchTemp()->patch; + Bit8u timbreNumber = patch.timbreNum; + switch (patch.timbreGroup) { case 1: timbreNumber += 64; // Fall-through case 0: - soundGroupName = soundGroupNames[soundGroupIx[timbreNumber]]; - break; + return soundGroupNames[soundGroupIx[timbreNumber]]; case 2: - soundGroupName = soundGroupNames[controlROMMap->soundGroupsCount - 2]; - break; + return soundGroupNames[controlROMMap->soundGroupsCount - 2]; case 3: - soundGroupName = soundGroupNames[controlROMMap->soundGroupsCount - 1]; - break; + return soundGroupNames[controlROMMap->soundGroupsCount - 1]; default: - soundGroupName = NULL; - break; + return NULL; } - reportHandler->onProgramChanged(partNum, soundGroupName, patchName); } #define MT32EMU_PRINT_DEBUG \ @@ -864,6 +904,8 @@ bool Synth::open(const ROMImage &controlROMImage, const ROMImage &pcmROMImage, B return false; } + extensions.display = new Display(*this); + opened = true; activated = false; @@ -876,6 +918,9 @@ bool Synth::open(const ROMImage &controlROMImage, const ROMImage &pcmROMImage, B void Synth::dispose() { opened = false; + delete extensions.display; + extensions.display = NULL; + delete midiQueue; midiQueue = NULL; @@ -1156,16 +1201,21 @@ void Synth::playMsgOnPart(Bit8u part, Bit8u code, Bit8u note, Bit8u velocity) { #endif return; } - + extensions.display->midiMessagePlayed(); break; case 0xC: // Program change //printDebug("Program change %01x", note); parts[part]->setProgram(note); + if (part < 8) { + extensions.display->midiMessagePlayed(); + extensions.display->programChanged(part); + } break; case 0xE: // Pitch bender bend = (velocity << 7) | (note); //printDebug("Pitch bender %02x", bend); parts[part]->setBend(bend); + extensions.display->midiMessagePlayed(); break; default: #if MT32EMU_MONITOR_MIDI > 0 @@ -1223,29 +1273,26 @@ void Synth::playSysexWithoutHeader(Bit8u device, Bit8u command, const Bit8u *sys printDebug("playSysexWithoutHeader: Message is not intended for this device ID (provided: %02x, expected: 0x10 or channel)", int(device)); return; } - // This is checked early in the real devices (before any sysex length checks or further processing) - // FIXME: Response to SYSEX_CMD_DAT reset with partials active (and in general) is untested. - if ((command == SYSEX_CMD_DT1 || command == SYSEX_CMD_DAT) && sysex[0] == 0x7F) { - reset(); - return; - } - if (command == SYSEX_CMD_EOD) { -#if MT32EMU_MONITOR_SYSEX > 0 - printDebug("playSysexWithoutHeader: Ignored unsupported command %02x", command); -#endif - return; - } - if (len < 4) { + // All models process the checksum before anything else and ignore messages lacking the checksum, or containing the checksum only. + if (len < 2) { printDebug("playSysexWithoutHeader: Message is too short (%d bytes)!", len); return; } Bit8u checksum = calcSysexChecksum(sysex, len - 1); if (checksum != sysex[len - 1]) { printDebug("playSysexWithoutHeader: Message checksum is incorrect (provided: %02x, expected: %02x)!", sysex[len - 1], checksum); + if (opened) extensions.display->checksumErrorOccurred(); return; } len -= 1; // Exclude checksum + + if (command == SYSEX_CMD_EOD) { +#if MT32EMU_MONITOR_SYSEX > 0 + printDebug("playSysexWithoutHeader: Ignored unsupported command %02x", command); +#endif + return; + } switch (command) { case SYSEX_CMD_WSD: #if MT32EMU_MONITOR_SYSEX > 0 @@ -1285,12 +1332,34 @@ void Synth::readSysex(Bit8u /*device*/, const Bit8u * /*sysex*/, Bit32u /*len*/) } void Synth::writeSysex(Bit8u device, const Bit8u *sysex, Bit32u len) { - if (!opened) return; + if (!opened || len < 1) return; + + // This is checked early in the real devices (before any sysex length checks or further processing) + if (sysex[0] == 0x7F) { + if (!controlROMFeatures->oldMT32DisplayFeatures) extensions.display->midiMessagePlayed(); + reset(); + return; + } + + extensions.display->midiMessagePlayed(); reportHandler->onMIDIMessagePlayed(); + + if (len < 3) { + // A short message of just 1 or 2 bytes may be written to the display area yet it may cause a user-visible effect, + // similarly to the reset area. + if (sysex[0] == 0x20) { + extensions.display->displayControlMessageReceived(sysex, len); + return; + } + printDebug("writeSysex: Message is too short (%d bytes)!", len); + return; + } + Bit32u addr = (sysex[0] << 16) | (sysex[1] << 8) | (sysex[2]); addr = MT32EMU_MEMADDR(addr); sysex += 3; len -= 3; + //printDebug("Sysex addr: 0x%06x", MT32EMU_SYSEXMEMADDR(addr)); // NOTE: Please keep both lower and upper bounds in each check, for ease of reading @@ -1371,6 +1440,7 @@ void Synth::writeSysexGlobal(Bit32u addr, const Bit8u *sysex, Bit32u len) { if (region == NULL) { printDebug("Sysex write to unrecognised address %06x, len %d", MT32EMU_SYSEXMEMADDR(addr), len); + // FIXME: Real devices may respond differently to a long SysEx that covers adjacent regions. break; } writeMemoryRegion(region, addr, region->getClampedLen(addr, len), sysex); @@ -1689,7 +1759,10 @@ void Synth::writeMemoryRegion(const MemoryRegion *region, Bit32u addr, Bit32u le } break; case MR_Display: - char buf[SYSEX_BUFFER_SIZE]; + if (len > Display::LCD_TEXT_SIZE) len = Display::LCD_TEXT_SIZE; + if (!extensions.display->customDisplayMessageReceived(data, off, len)) break; + // Holds zero-terminated string of the maximum length. + char buf[Display::LCD_TEXT_SIZE + 1]; memcpy(&buf, &data[0], len); buf[len] = 0; #if MT32EMU_MONITOR_SYSEX > 0 @@ -1795,6 +1868,10 @@ void Synth::refreshSystemChanAssign(Bit8u firstPart, Bit8u lastPart) { } void Synth::refreshSystemMasterVol() { + // Note, this should only occur when the user turns the volume knob. When the master volume is set via a SysEx, display + // doesn't actually update on all real devices. However, we anyway update the display, as we don't foresee a dedicated + // API for setting the master volume yet it's rather dubious that one really needs this quirk to be fairly emulated. + if (opened) extensions.display->masterVolumeChanged(); #if MT32EMU_MONITOR_SYSEX > 0 printDebug(" Master volume: %d", mt32ram.system.masterVol); #endif @@ -1844,6 +1921,19 @@ Bit32s Synth::getMasterTunePitchDelta() const { return extensions.masterTunePitchDelta; } +bool Synth::getDisplayState(char *targetBuffer, bool narrowLCD) const { + if (!opened) { + memset(targetBuffer, ' ', Display::LCD_TEXT_SIZE); + targetBuffer[Display::LCD_TEXT_SIZE] = 0; + return false; + } + return extensions.display->getDisplayState(targetBuffer, narrowLCD); +} + +void Synth::setMainDisplayMode() { + if (opened) extensions.display->setMainDisplayMode(); +} + /** Defines an interface of a class that maintains storage of variable-sized data of SysEx messages. */ class MidiEventQueue::SysexDataStorage { public: @@ -2022,6 +2112,15 @@ Bit32u Synth::getStereoOutputSampleRate() const { return (analog == NULL) ? SAMPLE_RATE : analog->getOutputSampleRate(); } +void Renderer::updateDisplayState() { + bool midiMessageLEDState; + bool midiMessageLEDStateUpdated; + bool lcdUpdated; + synth.extensions.display->checkDisplayStateUpdated(midiMessageLEDState, midiMessageLEDStateUpdated, lcdUpdated); + if (midiMessageLEDStateUpdated) synth.extensions.reportHandler2->onMidiMessageLEDStateUpdated(midiMessageLEDState); + if (lcdUpdated) synth.extensions.reportHandler2->onLCDStateUpdated(); +} + template void RendererImpl::doRender(Sample *stereoStream, Bit32u len) { if (!isActivated()) { @@ -2030,6 +2129,7 @@ void RendererImpl::doRender(Sample *stereoStream, Bit32u len) { printDebug("RendererImpl: Invalid call to Analog::process()!\n"); } Synth::muteSampleBuffer(stereoStream, len << 1); + updateDisplayState(); return; } @@ -2386,6 +2486,7 @@ void RendererImpl::produceStreams(const DACOutputStreams &stream getPartialManager().clearAlreadyOutputed(); incRenderedSampleCount(len); + updateDisplayState(); } void Synth::printPartialUsage(Bit32u sampleOffset) { diff --git a/mt32emu/src/Synth.h b/mt32emu/src/Synth.h index 6b9e0c58..51d50ef6 100644 --- a/mt32emu/src/Synth.h +++ b/mt32emu/src/Synth.h @@ -113,8 +113,21 @@ class MT32EMU_EXPORT ReportHandler { virtual void onProgramChanged(Bit8u /* partNum */, const char * /* soundGroupName */, const char * /* patchName */) {} }; +// Extends ReportHandler, so that the client may supply callbacks for reporting signals about updated display state. +class MT32EMU_EXPORT_V(2.6) ReportHandler2 : public ReportHandler { +public: + virtual ~ReportHandler2() {} + + // Invoked to signal about a change of the emulated LCD state. Use method Synth::getDisplayState to retrieve the actual data. + // This callback will not be invoked on further changes, until the client retrieves the LCD state. + virtual void onLCDStateUpdated() {} + // Invoked when the emulated MIDI MESSAGE LED changes state. The ledState parameter represents whether the LED is ON. + virtual void onMidiMessageLEDStateUpdated(bool /* ledState */) {} +}; + class Synth { friend class DefaultMidiStreamParser; +friend class Display; friend class MemoryRegion; friend class Part; friend class Partial; @@ -177,7 +190,7 @@ friend class TVP; bool opened; bool activated; - bool isDefaultReportHandler; + bool isDefaultReportHandler; // No longer used, retained for binary compatibility only. ReportHandler *reportHandler; PartialManager *partialManager; @@ -227,7 +240,10 @@ friend class TVP; void printPartialUsage(Bit32u sampleOffset = 0); - void newTimbreSet(Bit8u partNum, Bit8u timbreGroup, Bit8u timbreNumber, const char patchName[]); + void rhythmNotePlayed() const; + void voicePartStateChanged(Bit8u partNum, bool activated) const; + void newTimbreSet(Bit8u partNum) const; + const char *getSoundGroupName(const Part *part) const; void printDebug(const char *fmt, ...); // partNum should be 0..7 for Part 1..8, or 8 for Rhythm @@ -290,6 +306,10 @@ friend class TVP; MT32EMU_EXPORT explicit Synth(ReportHandler *useReportHandler = NULL); MT32EMU_EXPORT ~Synth(); + // Sets an implementation of ReportHandler2 interface for reporting various errors, information and debug messages. + // If the argument is NULL, the default implementation is installed as a fallback. + MT32EMU_EXPORT_V(2.6) void setReportHandler2(ReportHandler2 *reportHandler2); + // Used to initialise the MT-32. Must be called before any other function. // Returns true if initialization was successful, otherwise returns false. // controlROMImage and pcmROMImage represent full Control and PCM ROM images for use by synth. @@ -533,6 +553,22 @@ friend class TVP; // Stores internal state of emulated synth into an array provided (as it would be acquired from hardware). MT32EMU_EXPORT void readMemory(Bit32u addr, Bit32u len, Bit8u *data); + + // Retrieves the current state of the emulated MT-32 display facilities. + // Typically, the state is updated during the rendering. When that happens, a related callback from ReportHandler2 is invoked. + // However, there might be no need to invoke this method after each update, e.g. when the render buffer is just a few milliseconds + // long. + // The argument targetBuffer must point to an array of at least 21 characters. The result is a null-terminated string. + // The optional argument narrowLCD enables a condensed representation of the displayed information in some cases. This is mainly + // intended to route the result to a hardware LCD that is only 16 characters wide. Automatic scrolling of longer strings + // is not supported. + // Returns whether the MIDI MESSAGE LED is ON and fills the targetBuffer parameter. + MT32EMU_EXPORT_V(2.6) bool getDisplayState(char *targetBuffer, bool narrowLCD = false) const; + + // Resets the emulated LCD to the main mode (Master Volume). This has the same effect as pressing the Master Volume button + // while the display shows some other message. Useful for the new-gen devices as those require a special Display Reset SysEx + // to return to the main mode e.g. from showing a custom display message or a checksum error. + MT32EMU_EXPORT_V(2.6) void setMainDisplayMode(); }; // class Synth } // namespace MT32Emu diff --git a/mt32emu/src/c_interface/c_interface.cpp b/mt32emu/src/c_interface/c_interface.cpp index 509ff5f6..b9263533 100644 --- a/mt32emu/src/c_interface/c_interface.cpp +++ b/mt32emu/src/c_interface/c_interface.cpp @@ -43,7 +43,7 @@ static mt32emu_service_version getSynthVersionID(mt32emu_service_i) { return MT32EMU_SERVICE_VERSION_CURRENT; } -static const mt32emu_service_i_v4 SERVICE_VTABLE = { +static const mt32emu_service_i_v5 SERVICE_VTABLE = { getSynthVersionID, mt32emu_get_supported_report_handler_version, mt32emu_get_supported_midi_receiver_version, @@ -127,13 +127,15 @@ static const mt32emu_service_i_v4 SERVICE_VTABLE = { mt32emu_identify_rom_file, mt32emu_merge_and_add_rom_data, mt32emu_merge_and_add_rom_files, - mt32emu_add_machine_rom_file + mt32emu_add_machine_rom_file, + mt32emu_get_display_state, + mt32emu_set_main_display_mode }; } // namespace MT32Emu struct mt32emu_data { - ReportHandler *reportHandler; + ReportHandler2 *reportHandler; Synth *synth; const ROMImage *controlROMImage; const ROMImage *pcmROMImage; @@ -147,16 +149,19 @@ struct mt32emu_data { namespace MT32Emu { -class DelegatingReportHandlerAdapter : public ReportHandler { +class DelegatingReportHandlerAdapter : public ReportHandler2 { public: DelegatingReportHandlerAdapter(mt32emu_report_handler_i useReportHandler, void *useInstanceData) : delegate(useReportHandler), instanceData(useInstanceData) {} -protected: +private: const mt32emu_report_handler_i delegate; void * const instanceData; -private: + bool isVersionLess(mt32emu_report_handler_version versionID) { + return delegate.v0->getVersionID(delegate) < versionID; + } + void printDebug(const char *fmt, va_list list) { if (delegate.v0->printDebug == NULL) { ReportHandler::printDebug(fmt, list); @@ -267,6 +272,22 @@ class DelegatingReportHandlerAdapter : public ReportHandler { delegate.v0->onProgramChanged(instanceData, partNum, soundGroupName, patchName); } } + + void onLCDStateUpdated() { + if (isVersionLess(MT32EMU_REPORT_HANDLER_VERSION_1) || delegate.v1->onLCDStateUpdated == NULL) { + ReportHandler2::onLCDStateUpdated(); + } else { + delegate.v1->onLCDStateUpdated(instanceData); + } + } + + void onMidiMessageLEDStateUpdated(bool ledState) { + if (isVersionLess(MT32EMU_REPORT_HANDLER_VERSION_1) || delegate.v1->onMidiMessageLEDStateUpdated == NULL) { + ReportHandler2::onMidiMessageLEDStateUpdated(ledState); + } else { + delegate.v1->onMidiMessageLEDStateUpdated(instanceData, ledState ? MT32EMU_BOOL_TRUE : MT32EMU_BOOL_FALSE); + } + } }; class DelegatingMidiStreamParser : public DefaultMidiStreamParser { @@ -438,7 +459,7 @@ extern "C" { mt32emu_service_i mt32emu_get_service_i() { mt32emu_service_i i; - i.v4 = &SERVICE_VTABLE; + i.v5 = &SERVICE_VTABLE; return i; } @@ -515,8 +536,13 @@ mt32emu_return_code mt32emu_identify_rom_file(mt32emu_rom_info *rom_info, const mt32emu_context mt32emu_create_context(mt32emu_report_handler_i report_handler, void *instance_data) { mt32emu_data *data = new mt32emu_data; - data->reportHandler = (report_handler.v0 != NULL) ? new DelegatingReportHandlerAdapter(report_handler, instance_data) : new ReportHandler; - data->synth = new Synth(data->reportHandler); + data->synth = new Synth; + if (report_handler.v0 != NULL) { + data->reportHandler = new DelegatingReportHandlerAdapter(report_handler, instance_data); + data->synth->setReportHandler2(data->reportHandler); + } else { + data->reportHandler = NULL; + } data->midiParser = new DefaultMidiStreamParser(*data->synth); data->controlROMImage = NULL; data->pcmROMImage = NULL; @@ -910,4 +936,12 @@ void mt32emu_read_memory(mt32emu_const_context context, mt32emu_bit32u addr, mt3 context->synth->readMemory(addr, len, data); } +mt32emu_boolean mt32emu_get_display_state(mt32emu_const_context context, char *target_buffer, const mt32emu_boolean narrow_lcd) { + return context->synth->getDisplayState(target_buffer, narrow_lcd != MT32EMU_BOOL_FALSE) ? MT32EMU_BOOL_TRUE : MT32EMU_BOOL_FALSE; +} + +void mt32emu_set_main_display_mode(mt32emu_const_context context) { + context->synth->setMainDisplayMode(); +} + } // extern "C" diff --git a/mt32emu/src/c_interface/c_interface.h b/mt32emu/src/c_interface/c_interface.h index 14e7263b..0bd8ecf7 100644 --- a/mt32emu/src/c_interface/c_interface.h +++ b/mt32emu/src/c_interface/c_interface.h @@ -562,6 +562,25 @@ MT32EMU_EXPORT const char *mt32emu_get_patch_name(mt32emu_const_context context, /** Stores internal state of emulated synth into an array provided (as it would be acquired from hardware). */ MT32EMU_EXPORT void mt32emu_read_memory(mt32emu_const_context context, mt32emu_bit32u addr, mt32emu_bit32u len, mt32emu_bit8u *data); +/** + * Retrieves the current state of the emulated MT-32 display facilities. + * Typically, the state is updated during the rendering. When that happens, a related callback from mt32emu_report_handler_i_v1 + * is invoked. However, there might be no need to invoke this method after each update, e.g. when the render buffer is just + * a few milliseconds long. + * The argument target_buffer must point to an array of at least 21 characters. The result is a null-terminated string. + * The argument narrow_lcd enables a condensed representation of the displayed information in some cases. This is mainly intended + * to route the result to a hardware LCD that is only 16 characters wide. Automatic scrolling of longer strings is not supported. + * Returns whether the MIDI MESSAGE LED is ON and fills the target_buffer parameter. + */ +MT32EMU_EXPORT_V(2.6) mt32emu_boolean mt32emu_get_display_state(mt32emu_const_context context, char *target_buffer, const mt32emu_boolean narrow_lcd); + +/** + * Resets the emulated LCD to the main mode (Master Volume). This has the same effect as pressing the Master Volume button + * while the display shows some other message. Useful for the new-gen devices as those require a special Display Reset SysEx + * to return to the main mode e.g. from showing a custom display message or a checksum error. + */ +MT32EMU_EXPORT_V(2.6) void mt32emu_set_main_display_mode(mt32emu_const_context context); + #ifdef __cplusplus } // extern "C" #endif diff --git a/mt32emu/src/c_interface/c_types.h b/mt32emu/src/c_interface/c_types.h index 6bcf0775..dccddbcd 100644 --- a/mt32emu/src/c_interface/c_types.h +++ b/mt32emu/src/c_interface/c_types.h @@ -111,7 +111,8 @@ typedef struct { /** Report handler interface versions */ typedef enum { MT32EMU_REPORT_HANDLER_VERSION_0 = 0, - MT32EMU_REPORT_HANDLER_VERSION_CURRENT = MT32EMU_REPORT_HANDLER_VERSION_0 + MT32EMU_REPORT_HANDLER_VERSION_1 = 1, + MT32EMU_REPORT_HANDLER_VERSION_CURRENT = MT32EMU_REPORT_HANDLER_VERSION_1 } mt32emu_report_handler_version; /** MIDI receiver interface versions */ @@ -127,7 +128,8 @@ typedef enum { MT32EMU_SERVICE_VERSION_2 = 2, MT32EMU_SERVICE_VERSION_3 = 3, MT32EMU_SERVICE_VERSION_4 = 4, - MT32EMU_SERVICE_VERSION_CURRENT = MT32EMU_SERVICE_VERSION_4 + MT32EMU_SERVICE_VERSION_5 = 5, + MT32EMU_SERVICE_VERSION_CURRENT = MT32EMU_SERVICE_VERSION_5 } mt32emu_service_version; /* === Report Handler Interface === */ @@ -135,42 +137,59 @@ typedef enum { typedef union mt32emu_report_handler_i mt32emu_report_handler_i; /** Interface for handling reported events (initial version) */ -typedef struct { - /** Returns the actual interface version ID */ - mt32emu_report_handler_version (*getVersionID)(mt32emu_report_handler_i i); - - /** Callback for debug messages, in vprintf() format */ - void (*printDebug)(void *instance_data, const char *fmt, va_list list); - /** Callbacks for reporting errors */ - void (*onErrorControlROM)(void *instance_data); - void (*onErrorPCMROM)(void *instance_data); - /** Callback for reporting about displaying a new custom message on LCD */ - void (*showLCDMessage)(void *instance_data, const char *message); - /** Callback for reporting actual processing of a MIDI message */ - void (*onMIDIMessagePlayed)(void *instance_data); +#define MT32EMU_REPORT_HANDLER_I_V0 \ + /** Returns the actual interface version ID */ \ + mt32emu_report_handler_version (*getVersionID)(mt32emu_report_handler_i i); \ +\ + /** Callback for debug messages, in vprintf() format */ \ + void (*printDebug)(void *instance_data, const char *fmt, va_list list); \ + /** Callbacks for reporting errors */ \ + void (*onErrorControlROM)(void *instance_data); \ + void (*onErrorPCMROM)(void *instance_data); \ + /** Callback for reporting about displaying a new custom message on LCD */ \ + void (*showLCDMessage)(void *instance_data, const char *message); \ + /** Callback for reporting actual processing of a MIDI message */ \ + void (*onMIDIMessagePlayed)(void *instance_data); \ /** * Callback for reporting an overflow of the input MIDI queue. * Returns MT32EMU_BOOL_TRUE if a recovery action was taken * and yet another attempt to enqueue the MIDI event is desired. - */ - mt32emu_boolean (*onMIDIQueueOverflow)(void *instance_data); + */ \ + mt32emu_boolean (*onMIDIQueueOverflow)(void *instance_data); \ /** * Callback invoked when a System Realtime MIDI message is detected in functions * mt32emu_parse_stream and mt32emu_play_short_message and the likes. - */ - void (*onMIDISystemRealtime)(void *instance_data, mt32emu_bit8u system_realtime); - /** Callbacks for reporting system events */ - void (*onDeviceReset)(void *instance_data); - void (*onDeviceReconfig)(void *instance_data); - /** Callbacks for reporting changes of reverb settings */ - void (*onNewReverbMode)(void *instance_data, mt32emu_bit8u mode); - void (*onNewReverbTime)(void *instance_data, mt32emu_bit8u time); - void (*onNewReverbLevel)(void *instance_data, mt32emu_bit8u level); - /** Callbacks for reporting various information */ - void (*onPolyStateChanged)(void *instance_data, mt32emu_bit8u part_num); + */ \ + void (*onMIDISystemRealtime)(void *instance_data, mt32emu_bit8u system_realtime); \ + /** Callbacks for reporting system events */ \ + void (*onDeviceReset)(void *instance_data); \ + void (*onDeviceReconfig)(void *instance_data); \ + /** Callbacks for reporting changes of reverb settings */ \ + void (*onNewReverbMode)(void *instance_data, mt32emu_bit8u mode); \ + void (*onNewReverbTime)(void *instance_data, mt32emu_bit8u time); \ + void (*onNewReverbLevel)(void *instance_data, mt32emu_bit8u level); \ + /** Callbacks for reporting various information */ \ + void (*onPolyStateChanged)(void *instance_data, mt32emu_bit8u part_num); \ void (*onProgramChanged)(void *instance_data, mt32emu_bit8u part_num, const char *sound_group_name, const char *patch_name); + +#define MT32EMU_REPORT_HANDLER_I_V1 \ + /** + * Invoked to signal about a change of the emulated LCD state. Use mt32emu_get_display_state to retrieve the actual data. + * This callback will not be invoked on further changes, until the client retrieves the LCD state. + */ \ + void (*onLCDStateUpdated)(void *instance_data); \ + /** Invoked when the emulated MIDI MESSAGE LED changes state. The led_state parameter represents whether the LED is ON. */ \ + void (*onMidiMessageLEDStateUpdated)(void *instance_data, mt32emu_boolean led_state); + +typedef struct { + MT32EMU_REPORT_HANDLER_I_V0 } mt32emu_report_handler_i_v0; +typedef struct { + MT32EMU_REPORT_HANDLER_I_V0 + MT32EMU_REPORT_HANDLER_I_V1 +} mt32emu_report_handler_i_v1; + /** * Extensible interface for handling reported events. * Union intended to view an interface of any subsequent version as any parent interface not requiring a cast. @@ -178,8 +197,12 @@ typedef struct { */ union mt32emu_report_handler_i { const mt32emu_report_handler_i_v0 *v0; + const mt32emu_report_handler_i_v1 *v1; }; +#undef MT32EMU_REPORT_HANDLER_I_V0 +#undef MT32EMU_REPORT_HANDLER_I_V1 + /* === MIDI Receiver Interface === */ typedef union mt32emu_midi_receiver_i mt32emu_midi_receiver_i; @@ -327,6 +350,10 @@ typedef union mt32emu_service_i mt32emu_service_i; mt32emu_return_code (*mergeAndAddROMFiles)(mt32emu_context context, const char *part1_filename, const char *part2_filename); \ mt32emu_return_code (*addMachineROMFile)(mt32emu_context context, const char *machine_id, const char *filename); +#define MT32EMU_SERVICE_I_V5 \ + mt32emu_boolean (*getDisplayState)(mt32emu_const_context context, char *target_buffer, const mt32emu_boolean narrow_lcd); \ + void (*setMainDisplayMode)(mt32emu_const_context context); + typedef struct { MT32EMU_SERVICE_I_V0 } mt32emu_service_i_v0; @@ -357,6 +384,15 @@ typedef struct { MT32EMU_SERVICE_I_V4 } mt32emu_service_i_v4; +typedef struct { + MT32EMU_SERVICE_I_V0 + MT32EMU_SERVICE_I_V1 + MT32EMU_SERVICE_I_V2 + MT32EMU_SERVICE_I_V3 + MT32EMU_SERVICE_I_V4 + MT32EMU_SERVICE_I_V5 +} mt32emu_service_i_v5; + /** * Extensible interface for all the library services. * Union intended to view an interface of any subsequent version as any parent interface not requiring a cast. @@ -368,6 +404,7 @@ union mt32emu_service_i { const mt32emu_service_i_v2 *v2; const mt32emu_service_i_v3 *v3; const mt32emu_service_i_v4 *v4; + const mt32emu_service_i_v5 *v5; }; #undef MT32EMU_SERVICE_I_V0 @@ -375,5 +412,6 @@ union mt32emu_service_i { #undef MT32EMU_SERVICE_I_V2 #undef MT32EMU_SERVICE_I_V3 #undef MT32EMU_SERVICE_I_V4 +#undef MT32EMU_SERVICE_I_V5 #endif /* #ifndef MT32EMU_C_TYPES_H */ diff --git a/mt32emu/src/c_interface/cpp_interface.h b/mt32emu/src/c_interface/cpp_interface.h index aa28327e..c52767bb 100644 --- a/mt32emu/src/c_interface/cpp_interface.h +++ b/mt32emu/src/c_interface/cpp_interface.h @@ -118,6 +118,8 @@ mt32emu_service_i mt32emu_get_service_i(); #define mt32emu_get_playing_notes i.v0->getPlayingNotes #define mt32emu_get_patch_name i.v0->getPatchName #define mt32emu_read_memory i.v0->readMemory +#define mt32emu_get_display_state iV5()->getDisplayState +#define mt32emu_set_main_display_mode iV5()->setMainDisplayMode #else // #if MT32EMU_API_TYPE == 2 @@ -130,7 +132,7 @@ namespace MT32Emu { namespace CppInterfaceImpl { static const mt32emu_report_handler_i NULL_REPORT_HANDLER = { NULL }; -static mt32emu_report_handler_i getReportHandlerThunk(); +static mt32emu_report_handler_i getReportHandlerThunk(mt32emu_report_handler_version); static mt32emu_midi_receiver_i getMidiReceiverThunk(); } @@ -143,8 +145,8 @@ static mt32emu_midi_receiver_i getMidiReceiverThunk(); * See c_types.h and c_interface.h for description of the corresponding interface methods. */ -// Defines the interface for handling reported events. -// Corresponds to the current version of mt32emu_report_handler_i interface. +// Defines the interface for handling reported events (initial version). +// Corresponds to the mt32emu_report_handler_i_v0 interface. class IReportHandler { public: virtual void printDebug(const char *fmt, va_list list) = 0; @@ -166,6 +168,17 @@ class IReportHandler { ~IReportHandler() {} }; +// Extends IReportHandler, so that the client may supply callbacks for reporting signals about updated display state. +// Corresponds to the mt32emu_report_handler_i_v1 interface. +class IReportHandlerV1 : public IReportHandler { +public: + virtual void onLCDStateUpdated() = 0; + virtual void onMidiMessageLEDStateUpdated(bool ledState) = 0; + +protected: + ~IReportHandlerV1() {} +}; + // Defines the interface for receiving MIDI messages generated by MIDI stream parser. // Corresponds to the current version of mt32emu_midi_receiver_i interface. class IMidiReceiver { @@ -212,7 +225,8 @@ class Service { mt32emu_context getContext() { return c; } void createContext(mt32emu_report_handler_i report_handler = CppInterfaceImpl::NULL_REPORT_HANDLER, void *instance_data = NULL) { freeContext(); c = mt32emu_create_context(report_handler, instance_data); } - void createContext(IReportHandler &report_handler) { createContext(CppInterfaceImpl::getReportHandlerThunk(), &report_handler); } + void createContext(IReportHandler &report_handler) { createContext(CppInterfaceImpl::getReportHandlerThunk(MT32EMU_REPORT_HANDLER_VERSION_0), &report_handler); } + void createContext(IReportHandlerV1 &report_handler) { createContext(CppInterfaceImpl::getReportHandlerThunk(MT32EMU_REPORT_HANDLER_VERSION_1), &report_handler); } void freeContext() { if (c != NULL) { mt32emu_free_context(c); c = NULL; } } mt32emu_return_code addROMData(const Bit8u *data, size_t data_size, const mt32emu_sha1_digest *sha1_digest = NULL) { return mt32emu_add_rom_data(c, data, data_size, sha1_digest); } mt32emu_return_code addROMFile(const char *filename) { return mt32emu_add_rom_file(c, filename); } @@ -300,6 +314,9 @@ class Service { const char *getPatchName(Bit8u part_number) { return mt32emu_get_patch_name(c, part_number); } void readMemory(Bit32u addr, Bit32u len, Bit8u *data) { mt32emu_read_memory(c, addr, len, data); } + bool getDisplayState(char *target_buffer, const bool narrow_lcd) { return mt32emu_get_display_state(c, target_buffer, narrow_lcd ? MT32EMU_BOOL_TRUE : MT32EMU_BOOL_FALSE) != MT32EMU_BOOL_FALSE; } + void setMainDisplayMode() { mt32emu_set_main_display_mode(c); } + private: #if MT32EMU_API_TYPE == 2 const mt32emu_service_i i; @@ -311,6 +328,7 @@ class Service { const mt32emu_service_i_v2 *iV2() { return (getVersionID() < MT32EMU_SERVICE_VERSION_2) ? NULL : i.v2; } const mt32emu_service_i_v3 *iV3() { return (getVersionID() < MT32EMU_SERVICE_VERSION_3) ? NULL : i.v3; } const mt32emu_service_i_v4 *iV4() { return (getVersionID() < MT32EMU_SERVICE_VERSION_4) ? NULL : i.v4; } + const mt32emu_service_i_v5 *iV5() { return (getVersionID() < MT32EMU_SERVICE_VERSION_5) ? NULL : i.v5; } #endif Service(const Service &); // prevent copy-construction @@ -319,9 +337,7 @@ class Service { namespace CppInterfaceImpl { -static mt32emu_report_handler_version getReportHandlerVersionID(mt32emu_report_handler_i) { - return MT32EMU_REPORT_HANDLER_VERSION_CURRENT; -} +static mt32emu_report_handler_version getReportHandlerVersionID(mt32emu_report_handler_i); static void printDebug(void *instance_data, const char *fmt, va_list list) { static_cast(instance_data)->printDebug(fmt, list); @@ -379,28 +395,53 @@ static void onProgramChanged(void *instance_data, mt32emu_bit8u part_num, const static_cast(instance_data)->onProgramChanged(part_num, sound_group_name, patch_name); } -static mt32emu_report_handler_i getReportHandlerThunk() { - static const mt32emu_report_handler_i_v0 REPORT_HANDLER_V0_THUNK = { - getReportHandlerVersionID, - printDebug, - onErrorControlROM, - onErrorPCMROM, - showLCDMessage, - onMIDIMessagePlayed, - onMIDIQueueOverflow, - onMIDISystemRealtime, - onDeviceReset, - onDeviceReconfig, - onNewReverbMode, - onNewReverbTime, - onNewReverbLevel, - onPolyStateChanged, - onProgramChanged - }; +static void onLCDStateUpdated(void *instance_data) { + static_cast(instance_data)->onLCDStateUpdated(); +} - static const mt32emu_report_handler_i REPORT_HANDLER_THUNK = { &REPORT_HANDLER_V0_THUNK }; +static void onMidiMessageLEDStateUpdated(void *instance_data, mt32emu_boolean led_state) { + static_cast(instance_data)->onMidiMessageLEDStateUpdated(led_state != MT32EMU_BOOL_FALSE); +} + +#define MT32EMU_REPORT_HANDLER_V0_THUNK \ + getReportHandlerVersionID, \ + printDebug, \ + onErrorControlROM, \ + onErrorPCMROM, \ + showLCDMessage, \ + onMIDIMessagePlayed, \ + onMIDIQueueOverflow, \ + onMIDISystemRealtime, \ + onDeviceReset, \ + onDeviceReconfig, \ + onNewReverbMode, \ + onNewReverbTime, \ + onNewReverbLevel, \ + onPolyStateChanged, \ + onProgramChanged + +static const mt32emu_report_handler_i_v0 REPORT_HANDLER_V0_THUNK = { + MT32EMU_REPORT_HANDLER_V0_THUNK +}; + +static const mt32emu_report_handler_i_v1 REPORT_HANDLER_V1_THUNK = { + MT32EMU_REPORT_HANDLER_V0_THUNK, + onLCDStateUpdated, + onMidiMessageLEDStateUpdated +}; + +#undef MT32EMU_REPORT_HANDLER_THUNK_V0 + +static mt32emu_report_handler_version getReportHandlerVersionID(mt32emu_report_handler_i thunk) { + if (thunk.v0 == &REPORT_HANDLER_V0_THUNK) return MT32EMU_REPORT_HANDLER_VERSION_0; + return MT32EMU_REPORT_HANDLER_VERSION_CURRENT; +} - return REPORT_HANDLER_THUNK; +static mt32emu_report_handler_i getReportHandlerThunk(mt32emu_report_handler_version versionID) { + mt32emu_report_handler_i thunk; + if (versionID == MT32EMU_REPORT_HANDLER_VERSION_0) thunk.v0 = &REPORT_HANDLER_V0_THUNK; + else thunk.v1 = &REPORT_HANDLER_V1_THUNK; + return thunk; } static mt32emu_midi_receiver_version getMidiReceiverVersionID(mt32emu_midi_receiver_i) { @@ -521,6 +562,8 @@ static mt32emu_midi_receiver_i getMidiReceiverThunk() { #undef mt32emu_get_playing_notes #undef mt32emu_get_patch_name #undef mt32emu_read_memory +#undef mt32emu_get_display_state +#undef mt32emu_set_main_display_mode #endif // #if MT32EMU_API_TYPE == 2