OXYGENHUB
◆ main openmsx / src / imgui / ImGuiMsxMusicViewer.cc
20.3 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "ImGuiMsxMusicViewer.hh"
2
3
#include "ImGuiPlot.hh"
4
5
#include "MSXMixer.hh"
6
#include "MSXMotherBoard.hh"
7
#include "YM2413.hh"
8
9
#include "cstd.hh"
10
#include "zstring_view.hh"
11
12
#include <bit>
13
14
namespace openmsx {
15
16
using namespace std::literals;
17
18
ImGuiMsxMusicViewer::ImGuiMsxMusicViewer(ImGuiManager& manager_)
19
	: ImGuiPart(manager_)
20
{
21
}
22
23
bool ImGuiMsxMusicViewer::match(const HoverList& list, Hovers hovers)
24
{
25
	return std::ranges::any_of(list, [&](Hovers h){ return hovers.match(h); });
26
}
27
28
// Returns true if the mouse is hovering the entire current table cell.
29
//
30
// Requirements:
31
// - Must be called *after* ImGui::TableNextColumn()
32
// - Must be called *before* rendering any content in the cell
33
// - Assumes the cell contains only a single line of text.
34
// - Assumes no custom row height or multi-line widgets.
35
static bool IsTextCellHovered(float width = 0.0f)
36
{
37
	gl::vec2 topLeft = ImGui::GetCursorScreenPos();
38
	if (width <= 0.0f) width = ImGui::GetContentRegionAvail().x;
39
	auto height = ImGui::GetTextLineHeightWithSpacing(); // assumes single-line cell
40
	auto bottomRight = topLeft + gl::vec2{width, height};
41
	return ImGui::IsMouseHoveringRect(topLeft, bottomRight);
42
}
43
44
void ImGuiMsxMusicViewer::drawCell(ChipState& cs, HoverList& newHoverList, std::span<const uint8_t> regs, Hovers hovers,
45
                                   std::function<void(uint32_t)> func,
46
                                   float highlightWidth, gl::vec2* getPos)
47
{
48
	if (!ImGui::TableNextColumn()) return;
49
50
	if (getPos) *getPos = ImGui::GetCursorScreenPos();
51
	if (IsTextCellHovered(highlightWidth)) {
52
		newHoverList.push_back(hovers);
53
	}
54
	auto color = [&]{
55
		if (match(cs.hoverList, hovers)) {
56
			const auto& style = ImGui::GetStyle();
57
			if (highlightWidth <= 0.0f) {
58
				highlightWidth = ImGui::GetContentRegionAvail().x + 2.0f * style.CellPadding.x;
59
			}
60
			gl::vec2 size = {highlightWidth, ImGui::GetTextLineHeightWithSpacing()};
61
			gl::vec2 tl = ImGui::GetCursorScreenPos() - gl::vec2(style.CellPadding);
62
			gl::vec2 br = tl + size;
63
			ImDrawList* drawList = ImGui::GetWindowDrawList();
64
			drawList->AddRectFilled(tl, br, getColor(imColor::YELLOW));
65
			return getColor(imColor::BLACK);
66
		}
67
		return getColor(imColor::TEXT);
68
	}();
69
70
	auto extract = [&](const Hover& h) {
71
		return (regs[h.reg] & h.mask) >> std::countr_zero(h.mask);
72
	};
73
	uint32_t value = 0;
74
	if (hovers.h1.reg != 255) {
75
		value = extract(hovers.h1);
76
	}
77
	if (hovers.h2.reg != 255) {
78
		assert(hovers.h1.mask == 255);
79
		value |= extract(hovers.h2) << 8;
80
	}
81
82
	im::StyleColor(ImGuiCol_Text, color, [&]{ func(value); });
83
}
84
85
static constexpr std::array<zstring_view, 16> instrNames = {
86
	"Custom",
87
	"Violin",
88
	"Guitar",
89
	"Piano",
90
	"Flute",
91
	"Clarinet",
92
	"Oboe",
93
	"Trumpet",
94
	"Organ",
95
	"Horn",
96
	"Synthesizer",
97
	"Harpsichord",
98
	"Vibraphone",
99
	"Synthesizer Bass",
100
	"Acoustic Bass",
101
	"Electric Guitar",
102
};
103
104
static void drawVolume(uint32_t v)
105
{
106
	ImGui::RightAlignText(tmpStrCat('-', v * 3), "-15"); // show '0' as '-0'
107
	ImGui::SameLine();
108
	auto size = gl::vec2{19.0f, ImGui::GetFontSize()};
109
	auto outerTL = ImGui::GetCursorScreenPos();
110
	auto outerBR = outerTL + size;
111
	auto innerTL = outerTL + gl::vec2{2.0f, 2.0f};
112
	auto innerBR = gl::vec2{innerTL.x + float(15 - v), outerBR.y - 2.0f};
113
	auto color = ImGui::GetColorU32(ImGuiCol_Text);
114
	ImDrawList* drawList = ImGui::GetWindowDrawList();
115
	drawList->AddRect(outerTL, outerBR, color);
116
	drawList->AddRectFilled(innerTL, innerBR, color);
117
	ImGui::Dummy(size);
118
}
119
120
void ImGuiMsxMusicViewer::paintMelodicChannel(ChipState& cs, HoverList& newHoverList, std::span<const uint8_t, 64> regs, int ch)
121
{
122
	auto r10 = uint8_t(ch + 0x10);
123
	auto r20 = uint8_t(ch + 0x20);
124
	auto r30 = uint8_t(ch + 0x30);
125
	auto v10 = regs[r10];
126
	auto v20 = regs[r20];
127
128
	bool key = (v20 & 0x10) != 0;
129
	auto& kh = cs.keyHistory[ch];
130
	kh.back() = key ? 1.0f : 0.0f;
131
132
	auto fnum = ((v20 & 0x01) << 8) | v10;
133
	auto block = (v20 & 0x0e) >> 1;
134
	auto freq = float(fnum << block) * (3579545.0f / float(72 << 19));
135
136
	if (ImGui::TableNextColumn()) { // channel
137
		ImGui::StrCat(ch + 1);
138
	}
139
	drawCell(cs, newHoverList, regs, {r20, 0x20}, [&](uint32_t v) { // sustain
140
		ImGui::TextUnformatted(v ? "on"sv : "off"sv);
141
	});
142
	Hover keyH{r20, 0x10};
143
	drawCell(cs, newHoverList, regs, keyH, [&](uint32_t) { // key
144
		ImGui::TextUnformatted(key ? "on"sv : "off"sv);
145
	});
146
	drawCell(cs, newHoverList, regs, keyH, [&](uint32_t) { // key plot
147
		gl::vec2 size = {historySize, ImGui::GetFontSize()};
148
		auto topLeft = ImGui::GetCursorScreenPos();
149
		auto bottomRight = topLeft + size;
150
		ImGui::Dummy(size);
151
		plotLinesRaw(kh, 0.0f, 1.0f, topLeft, bottomRight, ImGui::GetColorU32(ImGuiCol_Text));
152
	});
153
	Hovers freqH = {r10, 0xff, r20, 0x0f};
154
	drawCell(cs, newHoverList, regs, freqH, [&](uint32_t) { // frequency
155
		ImGui::StrCat(std::lround(freq));
156
	});
157
	drawCell(cs, newHoverList, regs, freqH, [&](uint32_t) { // note
158
		std::array<char, 3> buf;
159
		ImGui::TextUnformatted(freq2note(freq, buf));
160
	});
161
	drawCell(cs, newHoverList, regs, {r30, 0x0f}, drawVolume);
162
	drawCell(cs, newHoverList, regs, {r30, 0xf0}, [&](uint32_t v) { // instrument
163
		ImGui::StrCat('(', hex_string<1>(v), ") ", instrNames[v]);
164
	});
165
}
166
167
void ImGuiMsxMusicViewer::paintRhythmChannel(ChipState& cs, HoverList& newHoverList, std::span<const uint8_t, 64> regs, int ch)
168
{
169
	auto mask = [&]{
170
		if      (ch == 0) return uint8_t(0x10); // bass
171
		else if (ch == 1) return uint8_t(0x08); // snare
172
		else if (ch == 2) return uint8_t(0x02); // cymbal
173
		else if (ch == 3) return uint8_t(0x01); // hi-hat
174
		else              return uint8_t(0x04); // tom
175
	}();
176
	bool key = (regs[0x0e] & mask) != 0;
177
	auto& kh = cs.keyHistory[ch + 9];
178
	kh.back() = key ? 1.0f : 0.0f;
179
180
	if (ImGui::TableNextColumn()) { // channel
181
		static constexpr std::array<std::string_view, 5> names = {
182
			"Bass drum",
183
			"Snare drum",
184
			"Top Cymbal",
185
			"High hat",
186
			"Tom-tom",
187
		};
188
		ImGui::TextUnformatted(names[ch]);
189
	}
190
	Hovers keyH = {0x0e, mask};
191
	drawCell(cs, newHoverList, regs, keyH, [&](uint32_t) { // key
192
		ImGui::TextUnformatted(key ? "on"sv : "off"sv);
193
	});
194
	drawCell(cs, newHoverList, regs, keyH, [&](uint32_t) { // key plot
195
		gl::vec2 size = {historySize, ImGui::GetFontSize()};
196
		auto topLeft = ImGui::GetCursorScreenPos();
197
		auto bottomRight = topLeft + size;
198
		ImGui::Dummy(size);
199
		plotLinesRaw(kh, 0.0f, 1.0f, topLeft, bottomRight, ImGui::GetColorU32(ImGuiCol_Text));
200
	});
201
	auto volH = [&]{
202
		if      (ch == 0) return Hover{0x36, 0x0f}; // bass
203
		else if (ch == 1) return Hover{0x37, 0x0f}; // snare
204
		else if (ch == 2) return Hover{0x38, 0x0f}; // cymbal
205
		else if (ch == 3) return Hover{0x37, 0xf0}; // hi-hat
206
		else              return Hover{0x38, 0xf0}; // tom
207
	}();
208
	drawCell(cs, newHoverList, regs, volH, drawVolume);
209
}
210
211
void ImGuiMsxMusicViewer::paintChannels(ChipState& cs, HoverList& newHoverList, std::span<const uint8_t, 64> regs)
212
{
213
	bool rhythm = (regs[0x0e] & 0x20) != 0;
214
	auto numChannels = rhythm ? 6 : 9;
215
216
	int flags =
217
		ImGuiTableFlags_Resizable |
218
		ImGuiTableFlags_Reorderable |
219
		ImGuiTableFlags_Hideable |
220
		ImGuiTableFlags_RowBg |
221
		ImGuiTableFlags_BordersInnerV |
222
		ImGuiTableFlags_BordersOuter |
223
		ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX;
224
	im::Table("##melodic", 8, flags, [&]{
225
		ImGui::TableSetupColumn("Ch.");
226
		ImGui::TableSetupColumn("Sustain");
227
		ImGui::TableSetupColumn("Key");
228
		ImGui::TableSetupColumn("Key-plot", ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed, historySize);
229
		ImGui::TableSetupColumn("Freq [Hz]", ImGuiTableColumnFlags_DefaultHide);
230
		ImGui::TableSetupColumn("Note");
231
		ImGui::TableSetupColumn("Vol [dB]");
232
		auto instrNameWidth = ImGui::CalcTextSize("(13) ").x + ImGui::CalcTextSize(instrNames[13]).x;
233
		ImGui::TableSetupColumn("Instrument", ImGuiTableColumnFlags_WidthFixed, instrNameWidth);
234
		ImGui::TableHeadersRow();
235
236
		im::ID_for_range(numChannels, [&](auto ch){
237
			paintMelodicChannel(cs, newHoverList, regs, ch);
238
		});
239
	});
240
	if (!rhythm) return;
241
	im::Table("##rhythm", 4, flags, [&]{
242
		ImGui::TableSetupColumn("Rhythm");
243
		ImGui::TableSetupColumn("Key");
244
		ImGui::TableSetupColumn("Key-plot", ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed, historySize);
245
		ImGui::TableSetupColumn("Vol [dB]");
246
		ImGui::TableHeadersRow();
247
248
		im::ID_for_range(5, [&](auto ch){
249
			paintRhythmChannel(cs, newHoverList, regs, ch);
250
		});
251
	});
252
}
253
254
static constexpr std::array<std::array<uint8_t, 8>, 15> fixedInstruments = {
255
	std::array<uint8_t, 8>{0x71, 0x61, 0x1e, 0x17, 0xd0, 0x78, 0x00, 0x17}, // Violin
256
	std::array<uint8_t, 8>{0x13, 0x41, 0x1a, 0x0d, 0xd8, 0xf7, 0x23, 0x13}, // Guitar
257
	std::array<uint8_t, 8>{0x13, 0x01, 0x89, 0x00, 0xf2, 0xc4, 0x11, 0x23}, // Piano
258
	std::array<uint8_t, 8>{0x31, 0x61, 0x0e, 0x07, 0xa8, 0x64, 0x70, 0x27}, // Flute
259
	std::array<uint8_t, 8>{0x32, 0x21, 0x1e, 0x06, 0xe0, 0x76, 0x00, 0x28}, // Clarinet
260
	std::array<uint8_t, 8>{0x31, 0x22, 0x16, 0x05, 0xe0, 0x71, 0x00, 0x18}, // Oboe
261
	std::array<uint8_t, 8>{0x21, 0x61, 0x1d, 0x07, 0x82, 0x81, 0x10, 0x07}, // Trumpet
262
	std::array<uint8_t, 8>{0x23, 0x21, 0x2d, 0x14, 0xa2, 0x72, 0x00, 0x07}, // Organ
263
	std::array<uint8_t, 8>{0x61, 0x61, 0x1b, 0x06, 0x64, 0x65, 0x10, 0x17}, // Horn
264
	std::array<uint8_t, 8>{0x41, 0x61, 0x0b, 0x18, 0x85, 0xf7, 0x71, 0x07}, // Synthesizer
265
	std::array<uint8_t, 8>{0x13, 0x01, 0x83, 0x11, 0xfa, 0xe4, 0x10, 0x04}, // Harpsichord
266
	std::array<uint8_t, 8>{0x17, 0xc1, 0x24, 0x07, 0xf8, 0xf8, 0x22, 0x12}, // Vibraphone
267
	std::array<uint8_t, 8>{0x61, 0x50, 0x0c, 0x05, 0xc2, 0xf5, 0x20, 0x42}, // Synthesizer Bass
268
	std::array<uint8_t, 8>{0x01, 0x01, 0x55, 0x03, 0xc9, 0x95, 0x03, 0x02}, // Acoustic Bass
269
	std::array<uint8_t, 8>{0x61, 0x41, 0x89, 0x03, 0xf1, 0xe4, 0x40, 0x13}, // Electric Guitar
270
};
271
272
static constexpr int plotResolution = 90;
273
static constexpr auto sine = []{
274
	std::array<float, plotResolution> result = {};
275
	for (auto i : xrange(plotResolution)) {
276
		auto t = double(i) / double(plotResolution);
277
		result[i] = float(cstd::sin<2>(2.0 * Math::pi * 6 * t));
278
	}
279
	return result;
280
}();
281
static constexpr auto half_sine = []{
282
	std::array<float, plotResolution> result = {};
283
	for (auto i : xrange(plotResolution)) {
284
		auto t = double(i) / double(plotResolution);
285
		result[i] = float(std::clamp(cstd::sin<2>(2.0 * Math::pi * 6 * t), 0.0, 1.0));
286
	}
287
	return result;
288
}();
289
static constexpr auto plotAmOff = []{
290
	std::array<float, plotResolution> result = {};
291
	for (auto i : xrange(plotResolution)) {
292
		auto t = double(i) / double(plotResolution);
293
		result[i] = float(cstd::sin<2>(2.0 * Math::pi * 6 * t));
294
	}
295
	return result;
296
}();
297
static constexpr auto plotAmOn = []{
298
	std::array<float, plotResolution> result = {};
299
	for (auto i : xrange(plotResolution)) {
300
		auto t = double(i) / double(plotResolution);
301
		auto mod = cstd::cos<2>(2.0 * Math::pi * t);
302
		auto car = cstd::sin<2>(2.0 * Math::pi * 6 * t);
303
		result[i] = float(car * (1.0 + 0.5 * mod));
304
	}
305
	return result;
306
}();
307
static constexpr auto plotFmOff = []{
308
	std::array<float, plotResolution> result = {};
309
	for (auto i : xrange(plotResolution)) {
310
		auto t = double(i) / double(plotResolution);
311
		auto phase = 2.0 * Math::pi * 6.0 * t;
312
		result[i] = float(cstd::sin<2>(phase));
313
	}
314
	return result;
315
}();
316
static constexpr auto plotFmOn = []{
317
	std::array<float, plotResolution> result = {};
318
	for (auto i : xrange(plotResolution)) {
319
		auto t = double(i) / double(plotResolution);
320
		auto mod = cstd::sin<2>(2.0 * Math::pi * t);
321
		auto phase = 2.0 * Math::pi * (6.0 * t + 0.5 * mod);
322
		result[i] = float(cstd::sin<2>(phase));
323
	}
324
	return result;
325
}();
326
static constexpr auto sineSamples = []{
327
	std::array<float, plotResolution> result = {};
328
	for (auto i : xrange(plotResolution)) {
329
		auto t = double(i) / double(plotResolution);
330
		result[i] = float(cstd::sin<2>(2.0 * Math::pi * t));
331
	}
332
	return result;
333
}();
334
335
void ImGuiMsxMusicViewer::paintInstrument(ChipState& cs, HoverList& newHoverList, std::span<const uint8_t, 64> regs)
336
{
337
	auto customInstr = cs.selectedInstr == 0;
338
	auto instrRegs = customInstr ? subspan<8>(regs) : fixedInstruments[cs.selectedInstr - 1];
339
340
	auto headerColor = ImGui::GetColorU32(ImGuiCol_TableHeaderBg);
341
	auto drawRow = [&](std::string_view desc, Hover modH, Hover carH, auto draw,
342
	                   float highlightWidth = 0.0f, std::span<gl::vec2> getPos = {}) {
343
		if (modH.reg == carH.reg) {
344
			assert(modH.mask != carH.mask);
345
		} else if (carH.reg != 255) {
346
			assert((modH.reg & 1) == 0);
347
			assert((modH.reg + 1) == carH.reg);
348
			assert(modH.mask == carH.mask);
349
		}
350
351
		if (ImGui::TableNextColumn()) {
352
			ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, headerColor);
353
			ImGui::TextUnformatted(desc);
354
		}
355
		drawCell(cs, newHoverList, instrRegs, modH, [&](uint32_t v) {
356
			draw(v);
357
		}, highlightWidth, getPos.empty() ? nullptr : &getPos[0]);
358
		drawCell(cs, newHoverList, instrRegs, carH, [&](uint32_t v) {
359
			if (carH.reg == 255) return;
360
			draw(v);
361
		}, highlightWidth, getPos.empty() ? nullptr : &getPos[1]);
362
	};
363
364
	int flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX;
365
	im::Table("tab", 3, flags, [&] {
366
		if (ImGui::TableNextColumn()) {
367
			auto instrNameWidth = ImGui::CalcTextSize(instrNames[13]).x;
368
			ImGui::SetNextItemWidth(instrNameWidth + ImGui::GetFrameHeight());
369
			im::Combo("##select-instr", instrNames[cs.selectedInstr].c_str(), [&]{
370
				for (auto i : xrange(16)) {
371
					if (ImGui::Selectable(instrNames[i].c_str())) {
372
						cs.selectedInstr = i;
373
					}
374
				}
375
			});
376
		}
377
		if (ImGui::TableNextColumn()) {
378
			ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, headerColor);
379
			ImGui::TextUnformatted("Modulator");
380
		}
381
		if (ImGui::TableNextColumn()) {
382
			ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, headerColor);
383
			ImGui::TextUnformatted("Carrier");
384
		}
385
386
		auto width = ImGui::CalcTextSize("0.5x").x + ImGui::GetStyle().ItemSpacing.x;
387
		auto textAndPlot = [&](const auto& text, std::span<const float> data) {
388
			auto pos = ImGui::GetCursorPos();
389
			ImGui::TextUnformatted(text);
390
			ImGui::SetCursorPos(pos + gl::vec2{width, 0.0f});
391
392
			auto topLeft = ImGui::GetCursorScreenPos();
393
			gl::vec2 plotSize = {plotResolution, ImGui::GetFontSize()};
394
			auto bottomRight = topLeft + plotSize;
395
			ImGui::Dummy(plotSize);
396
			plotLinesRaw(data, -1.0f, 1.0f, topLeft, bottomRight, ImGui::GetColorU32(ImGuiCol_Text));
397
		};
398
399
		int offset = customInstr ? 0 : 64; // don't highlight registers for fixed instruments
400
		drawRow("Waveform"sv, {offset + 3, 0x08}, {offset + 3, 0x10}, [&](uint32_t v) {
401
			textAndPlot(v ? "half"sv : "full"sv, v ? half_sine : sine);
402
		});
403
		drawRow("Tremolo"sv, {offset + 0, 0x80}, {offset + 1, 0x80}, [&](uint32_t v) {
404
			textAndPlot(v ? "on"sv : "off"sv, v ? plotAmOn : plotAmOff);
405
		});
406
		drawRow("Vibrato"sv, {offset + 0, 0x40}, {offset + 1, 0x40}, [&](uint32_t v) {
407
			textAndPlot(v ? "on"sv : "off"sv, v ? plotFmOn : plotFmOff);
408
		});
409
		drawRow("Multiplier"sv, {offset + 0, 0x0f}, {offset + 1, 0x0f}, [&](uint32_t v) {
410
			static constexpr std::array<uint8_t, 16> multipliers = {
411
				0, 1,  2,  3,  4,  5,  6,  7,
412
				8, 9, 10, 10, 12, 12, 15, 15,
413
			};
414
			int m = multipliers[v];
415
			auto text = v ? strCat(m, 'x') : "0.5x";
416
			std::array<float, plotResolution> data;
417
			auto f = m ? 2 * m : 1;
418
			for (auto i : xrange(plotResolution)) {
419
				data[i] = sineSamples[(f * i) % plotResolution];
420
			}
421
			textAndPlot(text, data);
422
		});
423
		drawRow("Key scale rate"sv, {offset + 0, 0x10}, {offset + 1, 0x10}, [](uint32_t v) {
424
			ImGui::TextUnformatted(v ? "stronger"sv : "weaker"sv);
425
		});
426
		drawRow("Key scale level"sv, {offset + 2, 0xc0}, {offset + 3, 0xc0}, [](uint32_t v) {
427
			static constexpr std::array<std::string_view, 4> tab = {
428
				"0", "3", "1.5", "6"
429
			};
430
			ImGui::StrCat('-', tab[v], "dB/octave");
431
		});
432
		drawRow("Attenuation"sv, {offset + 2, 0x3f}, {}, [](uint32_t v) {
433
			auto t = 75 * v;
434
			auto t1 = t / 100;
435
			auto t2 = t % 100;
436
			char t21 = narrow<char>(t2 / 10 + '0');
437
			char t22 = narrow<char>(t2 % 10 + '0');
438
			ImGui::StrCat(t1, '.', t21, t22, "dB");
439
		});
440
		auto printNum = [](uint32_t v) { ImGui::StrCat(v); };
441
		drawRow("Feedback"sv, {offset + 3, 0x07}, {}, printNum);
442
		drawRow("Envelope type"sv, {offset + 0, 0x20}, {offset + 1, 0x20}, [](uint32_t v) {
443
			ImGui::TextUnformatted(v ? "sustained"sv : "percussive"sv);
444
		});
445
		std::array<gl::vec2, 2> posMC;
446
		drawRow("Attack rate"sv,   {offset + 4, 0xf0}, {offset + 5, 0xf0}, printNum, width, posMC);
447
		drawRow("Decay rate"sv,    {offset + 4, 0x0f}, {offset + 5, 0x0f}, printNum, width);
448
		drawRow("Sustain level"sv, {offset + 6, 0xf0}, {offset + 7, 0xf0}, printNum, width);
449
		drawRow("Release rate"sv,  {offset + 6, 0x0f}, {offset + 7, 0x0f}, printNum, width);
450
451
		auto drawADSR = [&](gl::vec2 pos, bool egType, int o) {
452
			auto tl = pos + gl::vec2{width, 0.0f};
453
			auto sz = gl::vec2{plotResolution, 4 * ImGui::GetTextLineHeight() + 6 * ImGui::GetStyle().CellPadding.y};
454
			auto p = [&](float x, float y) { return tl + sz * gl::vec2{x, y}; };
455
			auto pA = p(0.0f, 1.0f);
456
			auto pB = p(0.0f, 0.3f);
457
			auto pC = p(0.3f, 0.0f);
458
			auto pD = p(0.4f, 0.3f);
459
			auto pE = egType ? p(0.8f, 0.3f) : p(0.8f, 0.5f);
460
			auto pF = p(1.0f, 1.0f);
461
462
			auto mouse = ImGui::GetIO().MousePos;
463
			Hovers hoverAttack  = {o + 4, 0xf0};
464
			Hovers hoverDecay   = {o + 4, 0x0f};
465
			Hovers hoverSustain = {o + 6, 0xf0};
466
			Hovers hoverRelease = {o + 6, 0x0f};
467
			if (tl.y <= mouse.y && mouse.y < (tl.y + sz.y) &&
468
			    tl.x <= mouse.x && mouse.x < (tl.x + sz.x)) {
469
				auto h = [&]{
470
					if (mouse.x < pC.x) return hoverAttack;
471
					if (mouse.x < pD.x) return hoverDecay;
472
					if (mouse.x < pE.x) return hoverSustain;
473
					return hoverRelease;
474
				}();
475
				newHoverList.push_back(h);
476
			}
477
478
			ImDrawList* drawList = ImGui::GetWindowDrawList();
479
			auto white = getColor(imColor::TEXT);
480
			auto yellow = getColor(imColor::YELLOW);
481
			bool hlAttack  = match(cs.hoverList, hoverAttack);
482
			drawList->AddBezierQuadratic(pA, pB, pC, hlAttack ? yellow : white, hlAttack ? 2.0f : 1.0f);
483
			auto line = [&](gl::vec2 p1, gl::vec2 p2, Hovers hover) {
484
				bool h = match(cs.hoverList, hover);
485
				drawList->AddLine(p1, p2, h ? yellow : white, h ? 2.0f : 1.0f);
486
			};
487
			line(pC, pD, hoverDecay);
488
			line(pD, pE, hoverSustain);
489
			line(pE, pF, hoverRelease);
490
		};
491
		drawADSR(posMC[0], (instrRegs[0] & 0x20) != 0, offset + 0);
492
		drawADSR(posMC[1], (instrRegs[1] & 0x20) != 0, offset + 1);
493
	});
494
}
495
496
void ImGuiMsxMusicViewer::paintRegisters(ChipState& cs, HoverList& newHoverList, std::span<const uint8_t, 64> regs)
497
{
498
	int flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX;
499
	im::Table("##reg-tab", 17, flags, [&] {
500
		ImGui::TableSetupColumn("");
501
		std::array<char, 2> name = {};
502
		for (int i : xrange(16)) {
503
			name[0] = char(i + (i < 10 ? '0' : ('A' - 10)));
504
			ImGui::TableSetupColumn(name.data());
505
		}
506
		ImGui::TableHeadersRow();
507
508
		auto headerColor = ImGui::GetColorU32(ImGuiCol_TableHeaderBg);
509
		for (auto row : xrange(4)) {
510
			if (ImGui::TableNextColumn()) {
511
				ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, headerColor);
512
				ImGui::StrCat(row, "0-", row, 'F');
513
			}
514
			for (auto column : xrange(16)) {
515
				auto reg = uint8_t(16 * row + column);
516
				drawCell(cs, newHoverList, regs, {reg, 0xff}, [&](uint32_t v){
517
					bool disabled = (row == 0) ? (8 <= column && column <= 13) : (9 <= column);
518
					im::Disabled(disabled, [&]{
519
						ImGui::StrCat(hex_string<2>(v));
520
					});
521
				});
522
			}
523
		}
524
	});
525
}
526
527
void ImGuiMsxMusicViewer::paintMsxMusic(const YM2413& ym2413)
528
{
529
	auto& cs = chips[ym2413.getName()];
530
	for (auto& kh : cs.keyHistory) {
531
		std::shift_left(kh.begin(), kh.end(), 1); // TODO c++23 range version
532
		kh.back() = {}; // may or may not get overwritten, so initialize now
533
	}
534
	HoverList newHoverList;
535
	auto regs = ym2413.peekRegs();
536
537
	im::TreeNode("Channels", ImGuiTreeNodeFlags_DefaultOpen, [&]{
538
		paintChannels(cs, newHoverList, regs);
539
	});
540
	im::TreeNode("Instrument settings", ImGuiTreeNodeFlags_DefaultOpen, [&]{
541
		paintInstrument(cs, newHoverList, regs);
542
	});
543
	im::TreeNode("Registers", ImGuiTreeNodeFlags_DefaultOpen, [&]{
544
		paintRegisters(cs, newHoverList, regs);
545
	});
546
547
	cs.hoverList = std::move(newHoverList);
548
}
549
550
void ImGuiMsxMusicViewer::paint(MSXMotherBoard* motherBoard)
551
{
552
	if (!show || !motherBoard) return;
553
554
	ImGui::SetNextWindowSize(gl::vec2{33, 50} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
555
	im::Window("MSX Music Viewer", &show, [&]{
556
		std::vector<const YM2413*> devices;
557
		for (const auto& info: motherBoard->getMSXMixer().getDeviceInfos()) {
558
			if (const auto* device = dynamic_cast<const YM2413*>(info.device)) {
559
				devices.push_back(device);
560
			}
561
		}
562
		if (auto num = devices.size(); num == 0) {
563
			ImGui::TextUnformatted("No MSX-Music devices currently present in the system.");
564
		} else if (num == 1) {
565
			paintMsxMusic(*devices.front());
566
		} else {
567
			for (const auto* device : devices) {
568
				im::TreeNode(device->getName().c_str(), [&] {
569
					paintMsxMusic(*device);
570
				});
571
			}
572
		}
573
	});
574
}
575
576
} // namespace openmsx