File: Display.cpp

package info (click to toggle)
scummvm 2.9.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 450,580 kB
  • sloc: cpp: 4,299,825; asm: 28,322; python: 12,901; sh: 11,302; java: 9,289; xml: 7,895; perl: 2,639; ansic: 2,465; yacc: 1,670; javascript: 1,020; makefile: 933; lex: 578; awk: 275; objc: 82; sed: 11; php: 1
file content (354 lines) | stat: -rw-r--r-- 17,498 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
/* 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 <http://www.gnu.org/licenses/>.
 */

#include <cstdlib>
#include <cstring>

#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