17#include "imgui_internal.h"
28static void plotLines(std::span<const float> values,
float scale_min,
float scale_max,
gl::vec2 outer_size)
30 const auto& style = ImGui::GetStyle();
32 gl::vec2 pos = ImGui::GetCursorScreenPos();
34 auto outer_br = pos + outer_size;
35 auto inner_tl = outer_tl +
gl::vec2(style.FramePadding);
36 auto inner_br = outer_br -
gl::vec2(style.FramePadding);
38 ImGui::RenderFrame(outer_tl, outer_br, ImGui::GetColorU32(ImGuiCol_FrameBg),
39 true, style.FrameRounding);
41 int num_values = narrow<int>(
values.size());
42 if (num_values < 2)
return;
43 int num_items = num_values - 1;
45 int inner_width = std::max(1,
static_cast<int>(inner_br.x - inner_tl.x));
46 int res_w = std::min(inner_width, num_items);
48 float t_step = 1.0f / (float)res_w;
49 float scale_range = scale_max - scale_min;
50 float inv_scale = (scale_range != 0.0f) ? (1.0f / scale_range) : 0.0f;
52 auto color = ImGui::GetColorU32(ImGuiCol_PlotLines);
54 auto valueToY = [&](
float v) {
55 return 1.0f - std::clamp((v - scale_min) * inv_scale, 0.0f, 1.0f);
58 auto tp0 =
gl::vec2(
t, valueToY(values[0]));
59 auto pos0 = ImLerp(inner_tl, inner_br, tp0);
61 auto* drawList = ImGui::GetWindowDrawList();
62 for (
int n = 0; n < res_w; n++) {
63 auto idx =
static_cast<int>(
t * float(num_items) + 0.5f);
64 assert(0 <= idx && idx < num_values);
65 float v =
values[(idx + 1) % num_values];
69 auto pos1 = ImLerp(inner_tl, inner_br, tp1);
70 drawList->AddLine(pos0, pos1, color);
76static void plotHistogram(std::span<const float> values,
float scale_min,
float scale_max,
gl::vec2 outer_size)
78 const auto& style = ImGui::GetStyle();
80 gl::vec2 pos = ImGui::GetCursorScreenPos();
82 auto outer_br = pos + outer_size;
83 auto inner_tl = outer_tl +
gl::vec2(style.FramePadding);
84 auto inner_br = outer_br -
gl::vec2(style.FramePadding);
86 ImGui::RenderFrame(outer_tl, outer_br, ImGui::GetColorU32(ImGuiCol_FrameBg),
87 true, style.FrameRounding);
88 if (
values.empty())
return;
90 int num_values = narrow<int>(
values.size());
91 int inner_width = std::max(1,
static_cast<int>(inner_br.x - inner_tl.x));
92 int res_w = std::min(inner_width, num_values);
94 float t_step = 1.0f /
static_cast<float>(res_w);
95 float scale_range = scale_max - scale_min;
96 float inv_scale = (scale_range != 0.0f) ? (1.0f / scale_range) : 0.0f;
98 auto valueToY = [&](
float v) {
99 return 1.0f - std::clamp((v - scale_min) * inv_scale, 0.0f, 1.0f);
101 float zero_line = (scale_min * scale_max < 0.0f)
102 ? (1 + scale_min * inv_scale)
103 : (scale_min < 0.0f ? 0.0f : 1.0f);
105 auto color = ImGui::GetColorU32(ImGuiCol_PlotHistogram);
108 auto* drawList = ImGui::GetWindowDrawList();
109 drawList->PrimReserve(6 * res_w, 4 * res_w);
111 for (
int n = 0; n < res_w; n++) {
112 auto idx =
static_cast<int>(t0 * float(num_values) + 0.5f);
113 assert(0 <= idx && idx < num_values);
114 float y0 = valueToY(values[idx]);
115 float t1 = t0 + t_step;
117 auto pos0 = ImLerp(inner_tl, inner_br,
gl::vec2(t0, y0));
118 auto pos1 = ImLerp(inner_tl, inner_br,
gl::vec2(t1, zero_line));
119 drawList->PrimRect(pos0, pos1, color);
136static void paintVUMeter(std::span<const float>& buf,
float factor,
bool muted)
139 gl::vec2 pos = ImGui::GetCursorScreenPos();
140 ImGui::SetNextItemWidth(-FLT_MIN);
141 auto width = ImGui::CalcItemWidth();
142 auto height = ImGui::GetFrameHeight();
143 ImGui::Dummy(
gl::vec2{width, height});
144 if (!ImGui::IsItemVisible())
return;
145 if (buf.size() <= 2)
return;
148 auto len = float(buf.size());
149 auto avg = std::reduce(buf.begin(), buf.end()) / len;
150 auto squaredSum = std::transform_reduce(buf.begin(), buf.end(), 0.0f,
151 std::plus<>{}, [avg](
float x) { auto norm = x - avg; return norm * norm; });
152 if (squaredSum == 0.0f) {
156 auto power =
fast_log2((squaredSum * factor * factor) / len);
159 static constexpr auto convertLog = std::numbers::ln10_v<float> / std::numbers::ln2_v<float>;
160 static constexpr auto range = 6.0f * convertLog;
161 auto clamped = std::clamp(power, -range, 0.0f);
162 auto f = (clamped + range) / range;
166 auto colorL = muted ? ImVec4{0.2f, 0.2f, 0.2f, 1.0f} : ImVec4{0.0f, 1.0f, 0.0f, 1.0f};
167 auto colorR = muted ? ImVec4{0.7f, 0.7f, 0.7f, 1.0f} : ImVec4{1.0f, 0.0f, 0.0f, 1.0f};
168 auto color1 = ImGui::ColorConvertFloat4ToU32(colorL);
169 auto color2 = ImGui::ColorConvertFloat4ToU32(ImLerp(colorL, colorR, f));
170 auto* drawList = ImGui::GetWindowDrawList();
171 drawList->AddRectFilledMultiColor(
173 color1, color2, color2, color1);
176static void paintWave(std::span<const float> buf)
179 auto pos = ImGui::GetCursorPos();
180 ImGui::SetNextItemWidth(-FLT_MIN);
181 auto size =
gl::vec2{ImGui::CalcItemWidth(), ImGui::GetFrameHeight()};
183 if (!ImGui::IsItemVisible())
return;
184 ImGui::SetCursorPos(pos);
186 if (buf.size() < 2) {
188 static constexpr std::array<float, 2> silent = {0.0f, 0.0f};
191 auto peak = std::transform_reduce(buf.begin(), buf.end(), 0.0f,
192 [](
auto x,
auto y) { return std::max(x, y); },
193 [](
auto x) { return std::abs(x); });
194 if (peak == 0.0f) peak = 1.0f;
196 plotLines(buf, -peak, peak, size);
205static ReduceResult reduce(std::span<const float> buf, std::span<float> work,
size_t fftLen,
float sampleRate)
208 auto allocate = [&](
size_t size) {
209 assert(size <= work.size());
210 auto result = work.first(size);
211 work = work.subspan(size);
230 float normalize = 1.0f;
231 std::span<float> extended;
232 if (buf.size() <= fftLen) {
233 extended = allocate(fftLen);
234 auto buf2 = extended.subspan(0, buf.size());
238 assert(buf.size() >= HALF_BAND_EXTRA);
239 extended = allocate(std::max((buf.size() - HALF_BAND_EXTRA) / 2,
size_t(fftLen)));
241 static_assert(HALF_BAND_EXTRA & 1);
242 if ((buf.size() & 1) == 0) {
243 buf = buf.subspan(1);
245 assert(buf.size() >= HALF_BAND_EXTRA);
246 auto buf2 = extended.subspan(0, (buf.size() - HALF_BAND_EXTRA) / 2);
253 }
while (buf.size() > fftLen);
254 extended = extended.subspan(0, fftLen);
257 auto result = extended.subspan(0, buf.size());
258 return {result, extended,
normalize, sampleRate};
261static std::string freq2note(
float freq)
263 static constexpr auto a4_freq = 440.0f;
264 static constexpr std::array<std::string_view, 12> names = {
265 "C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#",
"A",
"A#",
"B"
268 auto n = int(std::lround(12.0f *
fast_log2(freq / a4_freq))) + 9 + 4 * 12;
269 if (n < 0)
return "";
271 auto octave = n / 12;
272 return strCat(names[note], octave);
275static void paintSpectrum(std::span<const float> buf,
float factor,
const SoundDevice& device)
277 static constexpr auto convertLog = std::numbers::ln10_v<float> / std::numbers::ln2_v<float>;
278 static constexpr auto range = 5.0f * convertLog;
281 auto pos = ImGui::GetCursorPos();
282 ImGui::SetNextItemWidth(-FLT_MIN);
283 auto size =
gl::vec2{ImGui::CalcItemWidth(), ImGui::GetFrameHeight()};
285 if (!ImGui::IsItemVisible())
return;
286 ImGui::SetCursorPos(pos);
288 const auto& style = ImGui::GetStyle();
289 auto graphWidth =
size.x - 2.0f * style.FramePadding.x;
290 auto fftLen = std::clamp<size_t>(2 * std::bit_ceil(
size_t(graphWidth)), 256, 2048);
293 buf = buf.last(buf.size() / 8);
296 buf = buf.last(buf.size() / 4);
299 buf = buf.last(buf.size() / 2);
306 float sampleRate = 0.0f;
307 std::array<float, 1023> magnitude_;
308 std::span<float> magnitude;
309 if (buf.size() >= 2) {
312 std::array<float, 32768> workBuf;
313 auto [signal, zeroPadded,
normalize, sampleRate_] = reduce(buf, workBuf, fftLen, device.getNativeSampleRate());
314 sampleRate = sampleRate_;
315 assert(zeroPadded.size() == fftLen);
318 auto window =
hammingWindow(narrow<unsigned>(signal.size()));
319 auto avg = std::reduce(signal.begin(), signal.end()) / float(signal.size());
325 std::array<float, 2048> tmp_;
326 std::array<float, 2048> f_;
341 auto f =
subspan(f_, 0, fftLen);
345 auto offset =
fast_log2(normalize * normalize * (1.0f /
float(fftLen))) + range;
346 auto halfFftLen = fftLen / 2;
347 magnitude =
subspan(magnitude_, 0, halfFftLen - 1);
348 for (
unsigned i = 0; i < halfFftLen - 1; ++i) {
349 float real = f[i + 1];
350 float imag = f[i + 1 + halfFftLen];
351 float mag = real * real + imag * imag;
357 plotHistogram(magnitude, 0.0f, range, size);
360 auto scrnPosX = ImGui::GetCursorScreenPos().x + style.FramePadding.x;
361 auto mouseX = (ImGui::GetIO().MousePos.x - scrnPosX) / graphWidth;
362 if ((mouseX <= 0.0f) || (mouseX >= 1.0f))
return {};
364 if (sampleRate == 0.0f) {
366 sampleRate = device.getNativeSampleRate();
367 auto samples = device.getLastMonoBufferSize();
369 case 256: samples /= 8;
break;
370 case 512: samples /= 4;
break;
371 case 1024: samples /= 2;
break;
374 while (samples > fftLen) {
376 if ((samples & 1) == 0) --samples;
377 samples = (samples - HALF_BAND_EXTRA) / 2;
382 auto freq = std::lround(sampleRate * 0.5f * mouseX);
383 auto note = freq2note(
float(freq));
385 return strCat(freq,
"Hz ", note);
387 auto k = freq / 1000;
388 auto t = (freq % 1000) / 10;
389 char t1 = char(
t / 10) +
'0';
390 char t2 = char(
t % 10) +
'0';
391 return strCat(k,
'.', t1, t2,
"kHz ", note);
396static void stereoToMono(std::span<const float> stereo,
float factorL,
float factorR,
397 std::vector<float>& mono)
399 assert((stereo.size() & 1) == 0);
400 auto size = stereo.size() / 2;
403 mono[i] = factorL * stereo[2 * i + 0]
404 + factorR * stereo[2 * i + 1];
408static void paintDevice(SoundDevice& device, std::span<const MSXMixer::SoundDeviceInfo::ChannelSettings> settings)
410 std::vector<float> tmpBuf;
412 bool stereo = device.hasStereoChannels();
413 auto [factorL_, factorR_] = device.getAmplificationFactor();
414 auto factorL = factorL_;
415 auto factorR = factorR_;
416 auto factor = stereo ? 1.0f : factorL;
420 auto buf = device.getLastBuffer(channel);
421 if (!stereo) return buf;
422 stereoToMono(buf, factorL, factorR, tmpBuf);
423 return std::span<const float>{tmpBuf};
426 if (ImGui::TableNextColumn()) {
429 auto& muteSetting = *settings[channel].mute;
430 bool muted = muteSetting.getBoolean();
431 if (ImGui::TableNextColumn()) {
432 if (ImGui::Checkbox(
"##mute", &muted)) {
433 muteSetting.setBoolean(muted);
436 if (ImGui::TableNextColumn()) {
437 paintVUMeter(monoBuf, factor, muted);
439 if (ImGui::TableNextColumn()) {
442 if (ImGui::TableNextColumn()) {
443 paintSpectrum(monoBuf, factor, device);
450 if (!show || !motherBoard)
return;
452 ImGui::SetNextWindowSize(
gl::vec2{38, 15} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
453 im::Window(
"Audio channel viewer", &show, [&]{
455 auto& device = *info.device;
456 const auto& name = device.getName();
457 if (!ImGui::CollapsingHeader(name.c_str()))
continue;
458 HelpMarker(
"Right-click column header to (un)hide columns.\n"
459 "Drag to reorder or resize columns.");
461 int flags = ImGuiTableFlags_RowBg |
462 ImGuiTableFlags_BordersV |
463 ImGuiTableFlags_BordersOuter |
464 ImGuiTableFlags_Resizable |
465 ImGuiTableFlags_Reorderable |
466 ImGuiTableFlags_Hideable |
467 ImGuiTableFlags_SizingStretchProp;
469 ImGui::TableSetupScrollFreeze(0, 1);
470 ImGui::TableSetupColumn(
"ch.", ImGuiTableColumnFlags_NoReorder | ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed);
472 ImGui::TableSetupColumn(
"mute", ImGuiTableColumnFlags_DefaultHide | ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_WidthFixed);
473 ImGui::TableSetupColumn(
"VU-meter", 0, 1.0f);
474 ImGui::TableSetupColumn(
"Waveform", 0, 2.0f);
475 ImGui::TableSetupColumn(
"Spectrum", 0, 3.0f);
476 ImGui::TableHeadersRow();
478 paintDevice(device, info.channelSettings);
static void execute(std::span< const float, FFT_LEN > input, std::span< float, FFT_LEN > output, std::span< float, FFT_LEN > tmpBuf)
void loadLine(std::string_view name, zstring_view value) override
void save(ImGuiTextBuffer &buf) override
const auto & getDeviceInfos() const
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
void halfBand(std::span< const float > in, std::span< float > out)
std::span< const float > hammingWindow(unsigned n)
vecN< N, T > normalize(const vecN< N, T > &x)
void Table(const char *str_id, int column, ImGuiTableFlags flags, const ImVec2 &outer_size, float inner_width, std::invocable<> auto next)
void Window(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
void ID(const char *str_id, std::invocable<> auto next)
void ID_for_range(std::integral auto count, std::invocable< int > auto next)
This file implemented 3 utility functions:
bool loadOnePersistent(std::string_view name, zstring_view value, C &c, const std::tuple< Elements... > &tup)
void simpleToolTip(std::string_view desc)
void savePersistent(ImGuiTextBuffer &buf, C &c, const std::tuple< Elements... > &tup)
void HelpMarker(std::string_view desc)
constexpr void fill(ForwardRange &&range, const T &value)
constexpr auto copy(InputRange &&range, OutputIter out)
size_t size(std::string_view utf8)
Zip< false, RangesTuple, Is... > zip_equal(RangesTuple &&ranges, std::index_sequence< Is... >)
constexpr auto values(Map &&map)
constexpr auto subspan(Range &&range, size_t offset, size_t count=std::dynamic_extent)
std::span< float > extendedResult
std::span< float > result
constexpr auto xrange(T e)