// 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 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(); } //------------------------------------------------- // fill_window - fill in the windowing data //------------------------------------------------- void vgmviz_device::fill_window() { float window_pos_delta = (3.14159265358979f * 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() { 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(total_bars, SCREEN_HEIGHT); bar++) { if (bar < 2) { continue; } int permuted = WDL_fft_permute(FFT_LENGTH / 2, bar); float val = bins[0][permuted].re + bins[1][permuted].re; int level = int(logf(val * 32768.0f) * 31.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(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_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; } } //------------------------------------------------- // sound_stream_update - update the outgoing // audio stream and process as necessary //------------------------------------------------- void vgmviz_device::sound_stream_update(sound_stream &stream, stream_sample_t **inputs, stream_sample_t **outputs, int samples) { // clear output buffers for (int output = 0; output < m_outputs; output++) std::fill_n(outputs[output], samples, 0); m_current_rate = stream.sample_rate(); // loop over samples const u8 *outmap = &m_outputmap[0]; // for each input, add it to the appropriate output for (int pos = 0; pos < samples; pos++) { for (int inp = 0; inp < m_auto_allocated_inputs; inp++) { outputs[outmap[inp]][pos] += inputs[inp][pos]; } for (int i = 0; i < m_outputs; i++) { const float sample = (float)(int16_t)outputs[i][pos] / 65336.0f; 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(outputs); break; case VIZ_WATERFALL: case VIZ_RAWSPEC: case VIZ_BARSPEC4: case VIZ_BARSPEC8: case VIZ_BARSPEC16: update_fft(outputs); break; } } } //------------------------------------------------- // update_waveform - perform a wave-style update //------------------------------------------------- void vgmviz_device::update_waveform(stream_sample_t **outputs) { 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(stream_sample_t **outputs) { 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 //------------------------------------------------- uint32_t vgmviz_device::screen_update(screen_device &screen, bitmap_rgb32 &bitmap, const rectangle &cliprect) { bitmap.fill(0, cliprect); switch (m_viz_mode) { default: draw_waveform(bitmap); break; case VIZ_WATERFALL: draw_waterfall(bitmap); break; case VIZ_RAWSPEC: case VIZ_BARSPEC4: case VIZ_BARSPEC8: case VIZ_BARSPEC16: draw_spectrogram(bitmap); break; } return 0; } void vgmviz_device::draw_spectrogram(bitmap_rgb32 &bitmap) { const pen_t *pal = m_palette->pens(); const int black_idx = (512 + FFT_LENGTH / 2); /* find_levels(); int chan_x = 0; for (int chan = 0; chan < 2; chan++) { int level = int(m_curr_levels[chan] * 255.0f); int peak = int(m_curr_peaks[chan] * 255.0f); for (int y = 0; y < 512; y++) { int bar_y = 255 - (y >> 1); for (int x = 0; x < 7; x++) { uint32_t *line = &bitmap.pix32(y + 256); bool lit = bar_y <= level || bar_y == peak; line[chan_x + x] = pal[lit ? bar_y : black_idx]; } } chan_x += 8; m_curr_peaks[chan] *= 0.99f; } */ int bar_size = 1; switch (m_viz_mode) { default: bar_size = 1; break; case VIZ_BARSPEC4: bar_size = 4; break; case VIZ_BARSPEC8: bar_size = 8; break; case VIZ_BARSPEC16: bar_size = 16; break; } 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 < total_bars && bar < SCREEN_WIDTH; bar += bar_size) { if (bar < 2) { continue; } float max_val = 0.0f; for (int sub_bar = 0; sub_bar < bar_size && (bar + sub_bar) < total_bars; sub_bar++) { int permuted = WDL_fft_permute(FFT_LENGTH/2, bar + sub_bar); max_val = std::max((bins[0][permuted].re + bins[1][permuted].re) * 0.5f, max_val); } int level = int(logf(max_val * 32768.0f) * 96.0f); for (int y = 0; y < SCREEN_HEIGHT; y++) { int bar_y = SCREEN_HEIGHT - y; uint32_t *line = &bitmap.pix32(y); bool lit = bar_y <= level; const int x_limit = bar_size == 1 ? 1 : bar_size - 1; for (int x = 0; x < x_limit; x++) { line[(bar - 2) + x] = pal[lit ? (256 + bar) : black_idx]; } } } } 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.pix32(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.pix32(CHANNEL_CENTER, x) = MED_GRAY; bitmap.pix32(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.pix32(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.pix32(y, x) = RIGHT_COLOR; y += dy_r; } while(y != endy_r); bitmap.pix32(CHANNEL_HEIGHT, x) = WHITE; bitmap.pix32(CHANNEL_HEIGHT + 1, x) = WHITE; } }