/* Copyright (C) 2003, 2004, 2005, 2006, 2008, 2009 Dean Beeler, Jerome Fisher * Copyright (C) 2011-2022 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.isDisplayOldMT32Compatible()) { 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.isDisplayOldMT32Compatible() && mode == Mode_CUSTOM_MESSAGE) setMainDisplayMode(); } void Display::voicePartStateChanged(Bit8u partIndex, bool activated) { if (mode == Mode_MAIN) lcdDirty = true; voicePartStates[partIndex] = activated; if (synth.isDisplayOldMT32Compatible() && mode == Mode_CUSTOM_MESSAGE) setMainDisplayMode(); } void Display::masterVolumeChanged() { if (mode == Mode_MAIN) lcdDirty = true; } void Display::programChanged(Bit8u partIndex) { if (!synth.isDisplayOldMT32Compatible() && (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.isDisplayOldMT32Compatible()) { scheduleDisplayReset(); } else { displayResetScheduled = false; } } bool Display::customDisplayMessageReceived(const Bit8u *message, Bit32u startIndex, Bit32u length) { if (synth.isDisplayOldMT32Compatible()) { 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.isDisplayOldMT32Compatible()) { 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