mame/src/devices/sound/vgm_visualizer.cpp
hap 014cc81ea6 ui/info: add \n before btanb warning strings,
misc: replace pi constant with M_PI
2023-08-24 21:51:57 +02:00

765 lines
20 KiB
C++

// license:BSD-3-Clause
// copyright-holders:Ryan Holtz
/***************************************************************************
vgm_visualizer.cpp
Virtual VGM visualizer device.
Provides a waterfall view, spectrograph view, and VU view.
***************************************************************************/
#include "emu.h"
#include "sound/vgm_visualizer.h"
#include "wdlfft/fft.h"
#include <cmath>
constexpr int vgmviz_device::SCREEN_HEIGHT;
constexpr float lerp(float a, float b, float f)
{
return (b - a) * f + a;
}
//**************************************************************************
// GLOBAL VARIABLES
//**************************************************************************
// device type definition
DEFINE_DEVICE_TYPE(VGMVIZ, vgmviz_device, "vgmviz", "VGM Visualizer")
/*static*/ const bool vgmviz_device::NEEDS_FFT[VIZ_COUNT] =
{
false, // VIZ_WAVEFORM
true, // VIZ_WATERFALL
true, // VIZ_RAWSPEC
true, // VIZ_BARSPEC4
true, // VIZ_BARSPEC8
true // VIZ_BARSPEC16
};
//**************************************************************************
// LIVE DEVICE
//**************************************************************************
//-------------------------------------------------
// vgmviz_device - constructor
//-------------------------------------------------
vgmviz_device::vgmviz_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock)
: device_t(mconfig, VGMVIZ, tag, owner, clock)
, device_mixer_interface(mconfig, *this, 2)
, m_screen(*this, "screen")
, m_palette(*this, "palette")
{
}
//-------------------------------------------------
// ~vgmviz_device - destructor
//-------------------------------------------------
vgmviz_device::~vgmviz_device()
{
}
//-------------------------------------------------
// device_start - handle device startup
//-------------------------------------------------
void vgmviz_device::device_start()
{
WDL_fft_init();
fill_window();
m_bitmap.resize(SCREEN_WIDTH, SCREEN_HEIGHT);
}
//-------------------------------------------------
// fill_window - fill in the windowing data
//-------------------------------------------------
void vgmviz_device::fill_window()
{
float window_pos_delta = (M_PI * 2) / FFT_LENGTH;
float power = 0;
for (int i = 0; i < (FFT_LENGTH / 2) + 1; i++)
{
float window_pos = i * window_pos_delta;
m_window[i] = 0.53836f - cosf(window_pos) * 0.46164f;
power += m_window[i];
}
power = 0.5f / (power * 2.0f - m_window[FFT_LENGTH / 2]);
for (int i = 0; i < (FFT_LENGTH / 2) + 1; i++)
{
m_window[i] *= power;
}
}
//-------------------------------------------------
// fill_window - apply windowing data to the
// mixed signal
//-------------------------------------------------
void vgmviz_device::apply_window(uint32_t buf_index)
{
float *audio_l = m_audio_buf[buf_index][0];
float *audio_r = m_audio_buf[buf_index][1];
float *buf_l = m_fft_buf[0];
float *buf_r = m_fft_buf[1];
float *window = m_window;
for (int i = 0; i < (FFT_LENGTH / 2) + 1; i++)
{
*buf_l++ = *audio_l++ * *window;
*buf_r++ = *audio_r++ * *window;
window++;
}
for (int i = 0; i < (FFT_LENGTH / 2) - 1; i++)
{
window--;
*buf_l++ = *audio_l++ * *window;
*buf_r++ = *audio_r++ * *window;
}
}
//-------------------------------------------------
// apply_fft - run the FFT on the windowed data
//-------------------------------------------------
void vgmviz_device::apply_fft()
{
WDL_real_fft((WDL_FFT_REAL*)m_fft_buf[0], FFT_LENGTH, 0);
WDL_real_fft((WDL_FFT_REAL*)m_fft_buf[1], FFT_LENGTH, 0);
for (int i = 1; i < FFT_LENGTH/2; i++)
{
for (int chan = 0; chan < 2; chan++)
{
WDL_FFT_COMPLEX* cmpl = (WDL_FFT_COMPLEX*)m_fft_buf[chan] + i;
cmpl->re = sqrtf(cmpl->re * cmpl->re + cmpl->im * cmpl->im);
}
}
}
//-------------------------------------------------
// apply_waterfall - calculate the waterfall-view
// data
//-------------------------------------------------
void vgmviz_device::apply_waterfall()
{
const int total_bars = FFT_LENGTH / 2;
WDL_FFT_COMPLEX* bins[2] = { (WDL_FFT_COMPLEX*)m_fft_buf[0], (WDL_FFT_COMPLEX*)m_fft_buf[1] };
for (int bar = 0; bar < std::min<int>(total_bars, SCREEN_HEIGHT); bar++)
{
if (bar < 2)
{
continue;
}
int permuted = WDL_fft_permute(FFT_LENGTH / 2, bar);
float val = std::max<float>(bins[0][permuted].re, bins[1][permuted].re);
int level = int(log10f(val * 32768.0f) * 95.0f);
m_waterfall_buf[m_waterfall_length % SCREEN_WIDTH][total_bars - bar] = (level < 0) ? 0 : (level > 255 ? 255 : level);
}
m_waterfall_length++;
}
//-------------------------------------------------
// find_levels - find average and peak levels
//-------------------------------------------------
void vgmviz_device::find_levels()
{
if (m_audio_frames_available < 2 || m_current_rate == 0)
{
m_curr_levels[0] = 0.0f;
m_curr_levels[1] = 0.0f;
m_curr_peaks[0] = 0.0f;
m_curr_peaks[1] = 0.0f;
return;
}
m_curr_levels[0] = 0.0f;
m_curr_levels[1] = 0.0f;
int read_index = m_audio_fill_index;
const int samples_needed = m_current_rate / 60;
int samples_remaining = samples_needed;
int samples_found = 0;
do
{
for (int i = std::min<int>(FFT_LENGTH - 1, m_audio_count[read_index]); i >= 0 && samples_remaining > 0; i--, samples_remaining--)
{
for (int chan = 0; chan < 2; chan++)
{
if (m_audio_buf[read_index][chan][i] > m_curr_levels[chan])
{
m_curr_levels[chan] += m_audio_buf[read_index][chan][i];
}
}
samples_found++;
samples_remaining--;
}
read_index = 1 - m_audio_fill_index;
} while (samples_remaining > 0 && read_index != m_audio_fill_index);
if (samples_found > 0)
{
for (int chan = 0; chan < 2; chan++)
{
if (m_curr_levels[chan] > m_curr_peaks[chan])
{
m_curr_peaks[chan] = m_curr_levels[chan];
}
}
}
}
//-------------------------------------------------
// device_reset - handle device reset
//-------------------------------------------------
void vgmviz_device::device_reset()
{
for (int i = 0; i < 2; i++)
{
memset(m_audio_buf[i][0], 0, sizeof(float) * FFT_LENGTH);
memset(m_audio_buf[i][1], 0, sizeof(float) * FFT_LENGTH);
m_audio_count[i] = 0;
}
memset(m_fft_buf[0], 0, sizeof(float) * FFT_LENGTH);
memset(m_fft_buf[1], 0, sizeof(float) * FFT_LENGTH);
m_current_rate = 0;
m_audio_fill_index = 0;
m_audio_frames_available = 0;
memset(m_curr_levels, 0, sizeof(float) * 2);
memset(m_curr_peaks, 0, sizeof(float) * 2);
m_waterfall_length = 0;
for (int i = 0; i < SCREEN_WIDTH; i++)
{
memset(m_waterfall_buf[i], 0, sizeof(int) * 256);
}
m_viz_mode = VIZ_WAVEFORM;
m_clear_pending = true;
m_bitmap.fill(0);
m_history_length = 0;
}
//-------------------------------------------------
// cycle_spectrogram - cycle the visualization
// mode among the valid modes.
//-------------------------------------------------
void vgmviz_device::cycle_viz_mode()
{
m_viz_mode = (viz_mode)((int)m_viz_mode + 1);
if (m_viz_mode == VIZ_COUNT)
{
m_viz_mode = VIZ_WAVEFORM;
}
m_bitmap.fill(0);
m_clear_pending = true;
}
//-------------------------------------------------
// sound_stream_update - update the outgoing
// audio stream and process as necessary
//-------------------------------------------------
void vgmviz_device::sound_stream_update(sound_stream &stream, std::vector<read_stream_view> const &inputs, std::vector<write_stream_view> &outputs)
{
// call the normal interface to actually mix
device_mixer_interface::sound_stream_update(stream, inputs, outputs);
// now consume the outputs
for (int pos = 0; pos < outputs[0].samples(); pos++)
{
for (int i = 0; i < outputs.size(); i++)
{
// Original code took 16-bit sample / 65536.0 instead of 32768.0, so multiply by 0.5 here but is it necessary?
const float sample = outputs[i].get(pos) * 0.5f;
m_audio_buf[m_audio_fill_index][i][m_audio_count[m_audio_fill_index]] = sample + 0.5f;
}
switch (m_viz_mode)
{
default:
update_waveform();
break;
case VIZ_WATERFALL:
case VIZ_RAW_SPEC:
case VIZ_BAR_SPEC4:
case VIZ_BAR_SPEC8:
case VIZ_BAR_SPEC16:
case VIZ_PILLAR_SPEC4:
case VIZ_PILLAR_SPEC8:
case VIZ_PILLAR_SPEC16:
case VIZ_TOP_SPEC:
case VIZ_TOP_SPEC4:
case VIZ_TOP_SPEC8:
case VIZ_TOP_SPEC16:
update_fft();
break;
}
}
}
//-------------------------------------------------
// update_waveform - perform a wave-style update
//-------------------------------------------------
void vgmviz_device::update_waveform()
{
m_history_length++;
m_audio_count[m_audio_fill_index]++;
if (m_audio_count[m_audio_fill_index] >= FFT_LENGTH)
{
m_audio_fill_index = 1 - m_audio_fill_index;
if (m_audio_frames_available < 2)
{
m_audio_frames_available++;
}
m_audio_count[m_audio_fill_index] = 0;
}
}
//-------------------------------------------------
// update_fft - keep the FFT up-to-date
//-------------------------------------------------
void vgmviz_device::update_fft()
{
m_audio_count[m_audio_fill_index]++;
if (m_audio_count[m_audio_fill_index] >= FFT_LENGTH)
{
apply_window(m_audio_fill_index);
apply_fft();
apply_waterfall();
m_audio_fill_index = 1 - m_audio_fill_index;
if (m_audio_frames_available < 2)
{
m_audio_frames_available++;
}
m_audio_count[m_audio_fill_index] = 0;
}
}
//-------------------------------------------------
// init_palette - initialize the palette
//-------------------------------------------------
void vgmviz_device::init_palette(palette_device &palette) const
{
for (int i = 0; i < 256; i++)
{
float percent = (float)i / 255.0f;
if (percent < 0.75f)
{
float r = lerp(0.0f, 1.0f, percent / 0.75f);
float g = 1.0f;
float b = 0.0f;
palette.set_pen_color(i, rgb_t((uint8_t)(r * 255), (uint8_t)(g * 255), (uint8_t)(b * 255)));
}
else
{
float r = lerp(1.0f, 1.0f, (percent - 0.75f) / 0.25f);
float g = lerp(1.0f, 0.0f, (percent - 0.75f) / 0.25f);
float b = 0.0f;
palette.set_pen_color(i, rgb_t((uint8_t)(r * 255), (uint8_t)(g * 255), (uint8_t)(b * 255)));
}
}
for (int i = 0; i < FFT_LENGTH / 2; i++)
{
double h = ((double)i / (FFT_LENGTH / 2)) * 360.0;
double s = 1.0;
double v = 1.0;
double c = s * v;
double x = c * (1 - fabs(fmod(h / 60.0, 2.0) - 1.0));
double m = v - c;
double rs = 0.0;
double gs = 0.0;
double bs = 0.0;
if (h >= 0.0 && h < 60.0)
{
rs = c;
gs = x;
bs = 0.0;
}
else if (h >= 60.0 && h < 120.0)
{
rs = x;
gs = c;
bs = 0.0;
}
else if (h >= 120.0 && h < 180.0)
{
rs = 0.0;
gs = c;
bs = x;
}
else if (h >= 180.0 && h < 240.0)
{
rs = 0.0;
gs = x;
bs = c;
}
else if (h >= 240.0 && h < 300.0)
{
rs = x;
gs = 0.0;
bs = c;
}
else if (h < 360.0)
{
rs = c;
gs = 0.0;
bs = x;
}
palette.set_pen_color(i + 256, rgb_t((uint8_t)((rs + m) * 255), (uint8_t)((gs + m) * 255), (uint8_t)((bs + m) * 255)));
}
for (int y = 0; y < 256; y++)
{
float percent = (float)y / 255.0f;
if (percent < 0.75f)
{
float r = 0.0f;
float g = 0.0f;
float b = lerp(0.0f, 1.0f, percent / 0.5f);
palette.set_pen_color(y + 256 + FFT_LENGTH / 2, rgb_t((uint8_t)(r * 255), (uint8_t)(g * 255), (uint8_t)(b * 255)));
}
else
{
float r = lerp(0.0f, 1.0f, (percent - 0.5f) / 0.5f);
float g = lerp(0.0f, 1.0f, (percent - 0.5f) / 0.5f);
float b = 1.0f;
palette.set_pen_color(y + 256 + FFT_LENGTH / 2, rgb_t((uint8_t)(r * 255), (uint8_t)(g * 255), (uint8_t)(b * 255)));
}
}
palette.set_pen_color(512 + FFT_LENGTH / 2, rgb_t(0, 0, 0));
}
//-------------------------------------------------
// device_add_mconfig - handle device setup
//-------------------------------------------------
void vgmviz_device::device_add_mconfig(machine_config &config)
{
SCREEN(config, m_screen, SCREEN_TYPE_RASTER);
m_screen->set_refresh_hz(60);
m_screen->set_vblank_time(ATTOSECONDS_IN_USEC(2500));
m_screen->set_size(SCREEN_WIDTH, SCREEN_HEIGHT);
m_screen->set_visarea(0, SCREEN_WIDTH-1, 0, SCREEN_HEIGHT-1);
m_screen->set_screen_update(FUNC(vgmviz_device::screen_update));
PALETTE(config, m_palette, FUNC(vgmviz_device::init_palette), 512 + FFT_LENGTH / 2 + 1);
}
//-------------------------------------------------
// screen_update - update vu meters
//-------------------------------------------------
template void vgmviz_device::draw_spectrogram<1, vgmviz_device::SPEC_VIZ_BAR>(bitmap_rgb32 &bitmap);
template void vgmviz_device::draw_spectrogram<4, vgmviz_device::SPEC_VIZ_BAR>(bitmap_rgb32 &bitmap);
template void vgmviz_device::draw_spectrogram<8, vgmviz_device::SPEC_VIZ_BAR>(bitmap_rgb32 &bitmap);
template void vgmviz_device::draw_spectrogram<16, vgmviz_device::SPEC_VIZ_BAR>(bitmap_rgb32 &bitmap);
template void vgmviz_device::draw_spectrogram<4, vgmviz_device::SPEC_VIZ_TOP>(bitmap_rgb32 &bitmap);
template void vgmviz_device::draw_spectrogram<8, vgmviz_device::SPEC_VIZ_TOP>(bitmap_rgb32 &bitmap);
template void vgmviz_device::draw_spectrogram<16, vgmviz_device::SPEC_VIZ_TOP>(bitmap_rgb32 &bitmap);
template void vgmviz_device::draw_spectrogram<3, vgmviz_device::SPEC_VIZ_PILLAR>(bitmap_rgb32 &bitmap);
template void vgmviz_device::draw_spectrogram<6, vgmviz_device::SPEC_VIZ_PILLAR>(bitmap_rgb32 &bitmap);
template void vgmviz_device::draw_spectrogram<12, vgmviz_device::SPEC_VIZ_PILLAR>(bitmap_rgb32 &bitmap);
uint32_t vgmviz_device::screen_update(screen_device &screen, bitmap_rgb32 &bitmap, const rectangle &cliprect)
{
switch (m_viz_mode)
{
default:
bitmap.fill(0, cliprect);
draw_waveform(bitmap);
break;
case VIZ_WATERFALL:
bitmap.fill(0, cliprect);
draw_waterfall(bitmap);
break;
case VIZ_RAW_SPEC:
draw_spectrogram<1, SPEC_VIZ_BAR>(bitmap);
break;
case VIZ_TOP_SPEC:
draw_spectrogram<1, SPEC_VIZ_TOP>(bitmap);
break;
case VIZ_BAR_SPEC4:
draw_spectrogram<4, SPEC_VIZ_BAR>(bitmap);
break;
case VIZ_PILLAR_SPEC4:
draw_spectrogram<3, SPEC_VIZ_PILLAR>(bitmap);
break;
case VIZ_TOP_SPEC4:
draw_spectrogram<4, SPEC_VIZ_TOP>(bitmap);
break;
case VIZ_BAR_SPEC8:
draw_spectrogram<8, SPEC_VIZ_BAR>(bitmap);
break;
case VIZ_PILLAR_SPEC8:
draw_spectrogram<6, SPEC_VIZ_PILLAR>(bitmap);
break;
case VIZ_TOP_SPEC8:
draw_spectrogram<8, SPEC_VIZ_TOP>(bitmap);
break;
case VIZ_BAR_SPEC16:
draw_spectrogram<16, SPEC_VIZ_BAR>(bitmap);
break;
case VIZ_PILLAR_SPEC16:
draw_spectrogram<12, SPEC_VIZ_PILLAR>(bitmap);
break;
case VIZ_TOP_SPEC16:
draw_spectrogram<16, SPEC_VIZ_TOP>(bitmap);
break;
}
return 0;
}
template <int BarSize, int SpecMode> void vgmviz_device::draw_spectrogram(bitmap_rgb32 &bitmap)
{
const int black_index = 512 + FFT_LENGTH / 2;
if (m_clear_pending || SpecMode == SPEC_VIZ_BAR)
{
bitmap.fill(0);
m_clear_pending = false;
}
pen_t const *const pal = m_palette->pens();
int width = SCREEN_WIDTH;
switch (SpecMode)
{
case SPEC_VIZ_PILLAR:
for (int y = 2; y < SCREEN_HEIGHT; y++)
{
uint32_t const *const src = &m_bitmap.pix(y);
uint32_t *const dst = &bitmap.pix(y - 2);
for (int x = SCREEN_WIDTH - 1; x >= 1; x--)
{
dst[x] = src[x - 1];
}
}
width = int(SCREEN_WIDTH * 0.75f);
break;
case SPEC_VIZ_TOP:
for (int y = 1; y < SCREEN_HEIGHT; y++)
{
std::copy_n(&m_bitmap.pix(y), SCREEN_WIDTH, &bitmap.pix(y - 1));
}
break;
default:
break;
}
const int total_bars = FFT_LENGTH / 2;
WDL_FFT_COMPLEX *bins[2] = { (WDL_FFT_COMPLEX *)m_fft_buf[0], (WDL_FFT_COMPLEX *)m_fft_buf[1] };
for (int bar = 2; bar < total_bars && bar < width; bar += BarSize)
{
float max_val = 0.0f;
for (int sub_bar = 0; sub_bar < BarSize && (bar + sub_bar) < total_bars; sub_bar++)
{
int permuted = WDL_fft_permute(FFT_LENGTH/2, bar + sub_bar);
const float max_channel = std::max<float>(bins[0][permuted].re, bins[1][permuted].re);
max_val = std::max<float>(max_channel, max_val);
}
float raw_level = logf(max_val * 32768.0f);
int level = 0;
int pal_index = 0;
switch (SpecMode)
{
case SPEC_VIZ_BAR:
level = int(raw_level * 95.0f);
if (level <= 767)
{
pal_index = 255 - level / 3;
}
for (int y = 0; y < SCREEN_HEIGHT; y++)
{
const int bar_y = SCREEN_HEIGHT - y;
uint32_t *const line = &bitmap.pix(y);
for (int x = 0; x < BarSize; x++)
{
line[(bar - 2) + x] = bar_y <= level ? pal[256 + pal_index] : pal[black_index];
}
}
break;
case SPEC_VIZ_PILLAR:
level = int(raw_level * 59.0f);
if (level < 383)
{
pal_index = 255 - (int)(level * 0.75f);
}
for (int y = 0; y < SCREEN_HEIGHT; y++)
{
const int bar_y = SCREEN_HEIGHT - y;
uint32_t *const line = &bitmap.pix(y);
line[0] = 0;
if (bar_y > level)
{
continue;
}
const uint32_t entry = pal[256 + pal_index];
for (int x = (bar_y < level) ? 0 : 1; x < (BarSize - 1); x++)
{
if (bar_y < (level - 1))
{
const uint8_t r = uint8_t(((entry >> 16) & 0xff) * 0.75f);
const uint8_t g = uint8_t(((entry >> 8) & 0xff) * 0.75f);
const uint8_t b = uint8_t(((entry >> 0) & 0xff) * 0.75f);
line[(bar - 2) + x] = 0xff000000 | (r << 16) | (g << 8) | b;
}
else
{
line[(bar - 2) + x] = pal[256 + pal_index];
}
}
const uint8_t r = uint8_t(((entry >> 16) & 0xff) * 0.5f);
const uint8_t g = uint8_t(((entry >> 8) & 0xff) * 0.5f);
const uint8_t b = uint8_t(((entry >> 0) & 0xff) * 0.5f);
line[(bar - 2) + (BarSize - 1)] = 0xff000000 | (r << 16) | (g << 8) | b;
if (bar_y < level)
{
line[(bar - 2) + (BarSize - 2)] = 0xff000000 | (r << 16) | (g << 8) | b;
}
}
for (int x = 0; x < SCREEN_WIDTH; x++)
{
bitmap.pix(SCREEN_HEIGHT - 1, x) = 0;
bitmap.pix(SCREEN_HEIGHT - 2, x) = 0;
}
for (int y = 0; y < SCREEN_HEIGHT; y++)
std::copy_n(&bitmap.pix(y), SCREEN_WIDTH, &m_bitmap.pix(y));
break;
case SPEC_VIZ_TOP:
level = int(raw_level * 63.0f);
if (level < 255)
{
pal_index = 255 - level;
}
for (int x = 0; x < BarSize; x++)
{
bitmap.pix(SCREEN_HEIGHT - 1, (bar - 2) + x) = level > 0 ? pal[256 + pal_index] : pal[black_index];
}
for (int y = 0; y < SCREEN_HEIGHT; y++)
std::copy_n(&bitmap.pix(y), SCREEN_WIDTH, &m_bitmap.pix(y));
break;
}
}
}
void vgmviz_device::draw_waterfall(bitmap_rgb32 &bitmap)
{
const pen_t *pal = m_palette->pens();
float tex_height = ((float)FFT_LENGTH / 2) - 1.0f;
for (int y = 0; y < SCREEN_HEIGHT; y++)
{
const float v0 = (float)y / SCREEN_HEIGHT;
const float v1 = (float)(y + 1) / SCREEN_HEIGHT;
const float v0h = v0 * tex_height;
const float v1h = v1 * tex_height;
const int v0_index = (int)v0h;
const int v1_index = (int)v1h;
const float interp = v0h - (float)v0_index;
uint32_t* line = &bitmap.pix(y);
for (int x = 0; x < SCREEN_WIDTH; x++)
{
if (m_waterfall_length < SCREEN_WIDTH)
{
const float s0 = m_waterfall_buf[x][v0_index];
const float s1 = m_waterfall_buf[x][v1_index];
const int sample = (int)std::round(lerp(s0, s1, interp));
*line++ = pal[256 + FFT_LENGTH / 2 + sample];
}
else
{
const int x_index = ((m_waterfall_length - SCREEN_WIDTH) + x) % SCREEN_WIDTH;
const float s0 = m_waterfall_buf[x_index][v0_index];
const float s1 = m_waterfall_buf[x_index][v1_index];
const int sample = (int)std::round(lerp(s0, s1, interp));
*line++ = pal[256 + FFT_LENGTH / 2 + sample];
}
}
}
}
void vgmviz_device::draw_waveform(bitmap_rgb32 &bitmap)
{
static const uint32_t MED_GRAY = 0xff7f7f7f;
static const uint32_t WHITE = 0xffffffff;
static const uint32_t LEFT_COLOR = 0xffbf0000;
static const uint32_t RIGHT_COLOR = 0xff00bf00;
static const int CHANNEL_HEIGHT = (SCREEN_HEIGHT / 2) - 1;
static const int CHANNEL_CENTER = CHANNEL_HEIGHT / 2;
if (m_audio_frames_available == 0)
return;
for (int x = 0; x < SCREEN_WIDTH; x++)
{
bitmap.pix(CHANNEL_CENTER, x) = MED_GRAY;
bitmap.pix(CHANNEL_HEIGHT + 1 + CHANNEL_CENTER, x) = MED_GRAY;
const float raw_l = m_audio_buf[1 - m_audio_fill_index][0][((int)m_history_length + 1 + x) % FFT_LENGTH];
const int sample_l = (int)((raw_l - 0.5f) * (CHANNEL_HEIGHT - 1));
const int dy_l = (sample_l == 0) ? 0 : ((sample_l < 0) ? -1 : 1);
const int endy_l = CHANNEL_CENTER;
int y = endy_l - sample_l;
do
{
bitmap.pix(y, x) = LEFT_COLOR;
y += dy_l;
} while(y != endy_l);
const float raw_r = m_audio_buf[1 - m_audio_fill_index][1][((int)m_history_length + 1 + x) % FFT_LENGTH];
const int sample_r = (int)((raw_r - 0.5f) * (CHANNEL_HEIGHT - 1));
const int dy_r = (sample_r == 0) ? 0 : ((sample_r < 0) ? -1 : 1);
const int endy_r = CHANNEL_HEIGHT + 1 + CHANNEL_CENTER;
y = endy_r - sample_r;
do
{
bitmap.pix(y, x) = RIGHT_COLOR;
y += dy_r;
} while(y != endy_r);
bitmap.pix(CHANNEL_HEIGHT, x) = WHITE;
bitmap.pix(CHANNEL_HEIGHT + 1, x) = WHITE;
}
}