| 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 |
|
| 29 |
|
| 30 |
|
| 31 |
|
| 32 |
|
| 33 |
|
| 34 |
|
| 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(); |
| 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"); |
| 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()) { |
| 137 |
ImGui::StrCat(ch + 1); |
| 138 |
} |
| 139 |
drawCell(cs, newHoverList, regs, {r20, 0x20}, [&](uint32_t v) { |
| 140 |
ImGui::TextUnformatted(v ? "on"sv : "off"sv); |
| 141 |
}); |
| 142 |
Hover keyH{r20, 0x10}; |
| 143 |
drawCell(cs, newHoverList, regs, keyH, [&](uint32_t) { |
| 144 |
ImGui::TextUnformatted(key ? "on"sv : "off"sv); |
| 145 |
}); |
| 146 |
drawCell(cs, newHoverList, regs, keyH, [&](uint32_t) { |
| 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) { |
| 155 |
ImGui::StrCat(std::lround(freq)); |
| 156 |
}); |
| 157 |
drawCell(cs, newHoverList, regs, freqH, [&](uint32_t) { |
| 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) { |
| 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); |
| 171 |
else if (ch == 1) return uint8_t(0x08); |
| 172 |
else if (ch == 2) return uint8_t(0x02); |
| 173 |
else if (ch == 3) return uint8_t(0x01); |
| 174 |
else return uint8_t(0x04); |
| 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()) { |
| 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) { |
| 192 |
ImGui::TextUnformatted(key ? "on"sv : "off"sv); |
| 193 |
}); |
| 194 |
drawCell(cs, newHoverList, regs, keyH, [&](uint32_t) { |
| 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}; |
| 203 |
else if (ch == 1) return Hover{0x37, 0x0f}; |
| 204 |
else if (ch == 2) return Hover{0x38, 0x0f}; |
| 205 |
else if (ch == 3) return Hover{0x37, 0xf0}; |
| 206 |
else return Hover{0x38, 0xf0}; |
| 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}, |
| 256 |
std::array<uint8_t, 8>{0x13, 0x41, 0x1a, 0x0d, 0xd8, 0xf7, 0x23, 0x13}, |
| 257 |
std::array<uint8_t, 8>{0x13, 0x01, 0x89, 0x00, 0xf2, 0xc4, 0x11, 0x23}, |
| 258 |
std::array<uint8_t, 8>{0x31, 0x61, 0x0e, 0x07, 0xa8, 0x64, 0x70, 0x27}, |
| 259 |
std::array<uint8_t, 8>{0x32, 0x21, 0x1e, 0x06, 0xe0, 0x76, 0x00, 0x28}, |
| 260 |
std::array<uint8_t, 8>{0x31, 0x22, 0x16, 0x05, 0xe0, 0x71, 0x00, 0x18}, |
| 261 |
std::array<uint8_t, 8>{0x21, 0x61, 0x1d, 0x07, 0x82, 0x81, 0x10, 0x07}, |
| 262 |
std::array<uint8_t, 8>{0x23, 0x21, 0x2d, 0x14, 0xa2, 0x72, 0x00, 0x07}, |
| 263 |
std::array<uint8_t, 8>{0x61, 0x61, 0x1b, 0x06, 0x64, 0x65, 0x10, 0x17}, |
| 264 |
std::array<uint8_t, 8>{0x41, 0x61, 0x0b, 0x18, 0x85, 0xf7, 0x71, 0x07}, |
| 265 |
std::array<uint8_t, 8>{0x13, 0x01, 0x83, 0x11, 0xfa, 0xe4, 0x10, 0x04}, |
| 266 |
std::array<uint8_t, 8>{0x17, 0xc1, 0x24, 0x07, 0xf8, 0xf8, 0x22, 0x12}, |
| 267 |
std::array<uint8_t, 8>{0x61, 0x50, 0x0c, 0x05, 0xc2, 0xf5, 0x20, 0x42}, |
| 268 |
std::array<uint8_t, 8>{0x01, 0x01, 0x55, 0x03, 0xc9, 0x95, 0x03, 0x02}, |
| 269 |
std::array<uint8_t, 8>{0x61, 0x41, 0x89, 0x03, 0xf1, 0xe4, 0x40, 0x13}, |
| 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; |
| 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); |
| 532 |
kh.back() = {}; |
| 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 |
} |