Mercurial > hg > svgui
view layer/SpectrogramLayer.cpp @ 16:02a718909b2d
* Fixes to playback of short looped files, of synthesised content within looped
sections, and a few other fixes
author | Chris Cannam |
---|---|
date | Thu, 26 Jan 2006 11:56:09 +0000 |
parents | 01849cd277e6 |
children | 1deb5f87a18c |
line wrap: on
line source
/* -*- c-basic-offset: 4 -*- vi:set ts=8 sts=4 sw=4: */ /* A waveform viewer and audio annotation editor. Chris Cannam, Queen Mary University of London, 2005-2006 This is experimental software. Not for distribution. */ #include "SpectrogramLayer.h" #include "base/View.h" #include "base/Profiler.h" #include "base/AudioLevel.h" #include "base/Window.h" #include <QPainter> #include <QImage> #include <QPixmap> #include <QRect> #include <QTimer> #include <iostream> #include <cassert> #include <cmath> //#define DEBUG_SPECTROGRAM_REPAINT 1 SpectrogramLayer::SpectrogramLayer(View *w, Configuration config) : Layer(w), m_model(0), m_channel(0), m_windowSize(1024), m_windowType(HanningWindow), m_windowOverlap(50), m_gain(1.0), m_colourRotation(0), m_maxFrequency(8000), m_colourScale(dBColourScale), m_colourScheme(DefaultColours), m_frequencyScale(LinearFrequencyScale), m_cache(0), m_cacheInvalid(true), m_pixmapCache(0), m_pixmapCacheInvalid(true), m_fillThread(0), m_updateTimer(0), m_lastFillExtent(0), m_exiting(false) { if (config == MelodicRange) { setWindowSize(8192); setWindowOverlap(90); setWindowType(ParzenWindow); setMaxFrequency(1000); setColourScale(LinearColourScale); } if (m_view) m_view->setLightBackground(false); m_view->addLayer(this); } SpectrogramLayer::~SpectrogramLayer() { delete m_updateTimer; m_updateTimer = 0; m_exiting = true; m_condition.wakeAll(); if (m_fillThread) m_fillThread->wait(); delete m_fillThread; delete m_cache; } void SpectrogramLayer::setModel(const DenseTimeValueModel *model) { m_mutex.lock(); m_model = model; m_mutex.unlock(); if (!m_model || !m_model->isOK()) return; connect(m_model, SIGNAL(modelChanged()), this, SIGNAL(modelChanged())); connect(m_model, SIGNAL(modelChanged(size_t, size_t)), this, SIGNAL(modelChanged(size_t, size_t))); connect(m_model, SIGNAL(completionChanged()), this, SIGNAL(modelCompletionChanged())); connect(m_model, SIGNAL(modelChanged()), this, SLOT(cacheInvalid())); connect(m_model, SIGNAL(modelChanged(size_t, size_t)), this, SLOT(cacheInvalid(size_t, size_t))); emit modelReplaced(); fillCache(); } Layer::PropertyList SpectrogramLayer::getProperties() const { PropertyList list; list.push_back(tr("Colour")); list.push_back(tr("Colour Scale")); list.push_back(tr("Window Type")); list.push_back(tr("Window Size")); list.push_back(tr("Window Overlap")); list.push_back(tr("Gain")); list.push_back(tr("Colour Rotation")); list.push_back(tr("Max Frequency")); list.push_back(tr("Frequency Scale")); return list; } Layer::PropertyType SpectrogramLayer::getPropertyType(const PropertyName &name) const { if (name == tr("Gain")) return RangeProperty; if (name == tr("Colour Rotation")) return RangeProperty; return ValueProperty; } QString SpectrogramLayer::getPropertyGroupName(const PropertyName &name) const { if (name == tr("Window Size") || name == tr("Window Overlap")) return tr("Window"); if (name == tr("Gain") || name == tr("Colour Rotation") || name == tr("Colour Scale")) return tr("Scale"); if (name == tr("Max Frequency") || name == tr("Frequency Scale")) return tr("Frequency"); return QString(); } int SpectrogramLayer::getPropertyRangeAndValue(const PropertyName &name, int *min, int *max) const { int deft = 0; int throwaway; if (!min) min = &throwaway; if (!max) max = &throwaway; if (name == tr("Gain")) { *min = -50; *max = 50; deft = lrint(log10(m_gain) * 20.0); if (deft < *min) deft = *min; if (deft > *max) deft = *max; } else if (name == tr("Colour Rotation")) { *min = 0; *max = 256; deft = m_colourRotation; } else if (name == tr("Colour Scale")) { *min = 0; *max = 3; deft = (int)m_colourScale; } else if (name == tr("Colour")) { *min = 0; *max = 5; deft = (int)m_colourScheme; } else if (name == tr("Window Type")) { *min = 0; *max = 6; deft = (int)m_windowType; } else if (name == tr("Window Size")) { *min = 0; *max = 10; deft = 0; int ws = m_windowSize; while (ws > 32) { ws >>= 1; deft ++; } } else if (name == tr("Window Overlap")) { *min = 0; *max = 4; deft = m_windowOverlap / 25; if (m_windowOverlap == 90) deft = 4; } else if (name == tr("Max Frequency")) { *min = 0; *max = 9; switch (m_maxFrequency) { case 500: deft = 0; break; case 1000: deft = 1; break; case 1500: deft = 2; break; case 2000: deft = 3; break; case 4000: deft = 4; break; case 6000: deft = 5; break; case 8000: deft = 6; break; case 12000: deft = 7; break; case 16000: deft = 8; break; default: deft = 9; break; } } else if (name == tr("Frequency Scale")) { *min = 0; *max = 1; deft = (int)m_frequencyScale; } else { deft = Layer::getPropertyRangeAndValue(name, min, max); } return deft; } QString SpectrogramLayer::getPropertyValueLabel(const PropertyName &name, int value) const { if (name == tr("Colour")) { switch (value) { default: case 0: return tr("Default"); case 1: return tr("White on Black"); case 2: return tr("Black on White"); case 3: return tr("Red on Blue"); case 4: return tr("Yellow on Black"); case 5: return tr("Red on Black"); } } if (name == tr("Colour Scale")) { switch (value) { default: case 0: return tr("Level Linear"); case 1: return tr("Level Meter"); case 2: return tr("Level dB"); case 3: return tr("Phase"); } } if (name == tr("Window Type")) { switch ((WindowType)value) { default: case RectangularWindow: return tr("Rectangular"); case BartlettWindow: return tr("Bartlett"); case HammingWindow: return tr("Hamming"); case HanningWindow: return tr("Hanning"); case BlackmanWindow: return tr("Blackman"); case GaussianWindow: return tr("Gaussian"); case ParzenWindow: return tr("Parzen"); } } if (name == tr("Window Size")) { return QString("%1").arg(32 << value); } if (name == tr("Window Overlap")) { switch (value) { default: case 0: return tr("None"); case 1: return tr("25 %"); case 2: return tr("50 %"); case 3: return tr("75 %"); case 4: return tr("90 %"); } } if (name == tr("Max Frequency")) { switch (value) { default: case 0: return tr("500 Hz"); case 1: return tr("1 KHz"); case 2: return tr("1.5 KHz"); case 3: return tr("2 KHz"); case 4: return tr("4 KHz"); case 5: return tr("6 KHz"); case 6: return tr("8 KHz"); case 7: return tr("12 KHz"); case 8: return tr("16 KHz"); case 9: return tr("All"); } } if (name == tr("Frequency Scale")) { switch (value) { default: case 0: return tr("Linear"); case 1: return tr("Log"); } } return tr("<unknown>"); } void SpectrogramLayer::setProperty(const PropertyName &name, int value) { if (name == tr("Gain")) { setGain(pow(10, float(value)/20.0)); } else if (name == tr("Colour Rotation")) { setColourRotation(value); } else if (name == tr("Colour")) { if (m_view) m_view->setLightBackground(value == 2); switch (value) { default: case 0: setColourScheme(DefaultColours); break; case 1: setColourScheme(WhiteOnBlack); break; case 2: setColourScheme(BlackOnWhite); break; case 3: setColourScheme(RedOnBlue); break; case 4: setColourScheme(YellowOnBlack); break; case 5: setColourScheme(RedOnBlack); break; } } else if (name == tr("Window Type")) { setWindowType(WindowType(value)); } else if (name == tr("Window Size")) { setWindowSize(32 << value); } else if (name == tr("Window Overlap")) { if (value == 4) setWindowOverlap(90); else setWindowOverlap(25 * value); } else if (name == tr("Max Frequency")) { switch (value) { case 0: setMaxFrequency(500); break; case 1: setMaxFrequency(1000); break; case 2: setMaxFrequency(1500); break; case 3: setMaxFrequency(2000); break; case 4: setMaxFrequency(4000); break; case 5: setMaxFrequency(6000); break; case 6: setMaxFrequency(8000); break; case 7: setMaxFrequency(12000); break; case 8: setMaxFrequency(16000); break; default: case 9: setMaxFrequency(0); break; } } else if (name == tr("Colour Scale")) { switch (value) { default: case 0: setColourScale(LinearColourScale); break; case 1: setColourScale(MeterColourScale); break; case 2: setColourScale(dBColourScale); break; case 3: setColourScale(PhaseColourScale); break; } } else if (name == tr("Frequency Scale")) { switch (value) { default: case 0: setFrequencyScale(LinearFrequencyScale); break; case 1: setFrequencyScale(LogFrequencyScale); break; } } } void SpectrogramLayer::setChannel(int ch) { if (m_channel == ch) return; m_mutex.lock(); m_cacheInvalid = true; m_pixmapCacheInvalid = true; m_channel = ch; m_mutex.unlock(); emit layerParametersChanged(); fillCache(); } int SpectrogramLayer::getChannel() const { return m_channel; } void SpectrogramLayer::setWindowSize(size_t ws) { if (m_windowSize == ws) return; m_mutex.lock(); m_cacheInvalid = true; m_pixmapCacheInvalid = true; m_windowSize = ws; m_mutex.unlock(); emit layerParametersChanged(); fillCache(); } size_t SpectrogramLayer::getWindowSize() const { return m_windowSize; } void SpectrogramLayer::setWindowOverlap(size_t wi) { if (m_windowOverlap == wi) return; m_mutex.lock(); m_cacheInvalid = true; m_pixmapCacheInvalid = true; m_windowOverlap = wi; m_mutex.unlock(); emit layerParametersChanged(); fillCache(); } size_t SpectrogramLayer::getWindowOverlap() const { return m_windowOverlap; } void SpectrogramLayer::setWindowType(WindowType w) { if (m_windowType == w) return; m_mutex.lock(); m_cacheInvalid = true; m_pixmapCacheInvalid = true; m_windowType = w; m_mutex.unlock(); emit layerParametersChanged(); fillCache(); } WindowType SpectrogramLayer::getWindowType() const { return m_windowType; } void SpectrogramLayer::setGain(float gain) { if (m_gain == gain) return; //!!! inadequate for floats! m_mutex.lock(); m_cacheInvalid = true; m_pixmapCacheInvalid = true; m_gain = gain; m_mutex.unlock(); emit layerParametersChanged(); fillCache(); } float SpectrogramLayer::getGain() const { return m_gain; } void SpectrogramLayer::setMaxFrequency(size_t mf) { if (m_maxFrequency == mf) return; m_mutex.lock(); // don't need to invalidate main cache here m_pixmapCacheInvalid = true; m_maxFrequency = mf; m_mutex.unlock(); emit layerParametersChanged(); } size_t SpectrogramLayer::getMaxFrequency() const { return m_maxFrequency; } void SpectrogramLayer::setColourRotation(int r) { m_mutex.lock(); // don't need to invalidate main cache here m_pixmapCacheInvalid = true; if (r < 0) r = 0; if (r > 256) r = 256; int distance = r - m_colourRotation; if (distance != 0) { rotateCacheColourmap(-distance); m_colourRotation = r; } m_mutex.unlock(); emit layerParametersChanged(); } void SpectrogramLayer::setColourScale(ColourScale colourScale) { if (m_colourScale == colourScale) return; m_mutex.lock(); m_cacheInvalid = true; m_pixmapCacheInvalid = true; m_colourScale = colourScale; m_mutex.unlock(); fillCache(); emit layerParametersChanged(); } SpectrogramLayer::ColourScale SpectrogramLayer::getColourScale() const { return m_colourScale; } void SpectrogramLayer::setColourScheme(ColourScheme scheme) { if (m_colourScheme == scheme) return; m_mutex.lock(); // don't need to invalidate main cache here m_pixmapCacheInvalid = true; m_colourScheme = scheme; setCacheColourmap(); m_mutex.unlock(); emit layerParametersChanged(); } SpectrogramLayer::ColourScheme SpectrogramLayer::getColourScheme() const { return m_colourScheme; } void SpectrogramLayer::setFrequencyScale(FrequencyScale frequencyScale) { if (m_frequencyScale == frequencyScale) return; m_mutex.lock(); // don't need to invalidate main cache here m_pixmapCacheInvalid = true; m_frequencyScale = frequencyScale; m_mutex.unlock(); emit layerParametersChanged(); } SpectrogramLayer::FrequencyScale SpectrogramLayer::getFrequencyScale() const { return m_frequencyScale; } void SpectrogramLayer::cacheInvalid() { m_cacheInvalid = true; m_pixmapCacheInvalid = true; m_cachedInitialVisibleArea = false; fillCache(); } void SpectrogramLayer::cacheInvalid(size_t, size_t) { // for now (or forever?) cacheInvalid(); } void SpectrogramLayer::fillCache() { #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer::fillCache" << std::endl; #endif QMutexLocker locker(&m_mutex); m_lastFillExtent = 0; delete m_updateTimer; m_updateTimer = new QTimer(this); connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(fillTimerTimedOut())); m_updateTimer->start(200); if (!m_fillThread) { std::cerr << "SpectrogramLayer::fillCache creating thread" << std::endl; m_fillThread = new CacheFillThread(*this); m_fillThread->start(); } m_condition.wakeAll(); } void SpectrogramLayer::fillTimerTimedOut() { if (m_fillThread && m_model) { size_t fillExtent = m_fillThread->getFillExtent(); #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer::fillTimerTimedOut: extent " << fillExtent << ", last " << m_lastFillExtent << ", total " << m_model->getEndFrame() << std::endl; #endif if (fillExtent >= m_lastFillExtent) { if (fillExtent >= m_model->getEndFrame() && m_lastFillExtent > 0) { #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "complete!" << std::endl; #endif emit modelChanged(); m_pixmapCacheInvalid = true; delete m_updateTimer; m_updateTimer = 0; m_lastFillExtent = 0; } else if (fillExtent > m_lastFillExtent) { #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer: emitting modelChanged(" << m_lastFillExtent << "," << fillExtent << ")" << std::endl; #endif emit modelChanged(m_lastFillExtent, fillExtent); m_pixmapCacheInvalid = true; m_lastFillExtent = fillExtent; } } else { if (m_view) { size_t sf = 0; if (m_view->getStartFrame() > 0) sf = m_view->getStartFrame(); #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer: going backwards, emitting modelChanged(" << sf << "," << m_view->getEndFrame() << ")" << std::endl; #endif emit modelChanged(sf, m_view->getEndFrame()); m_pixmapCacheInvalid = true; } m_lastFillExtent = fillExtent; } } } void SpectrogramLayer::setCacheColourmap() { if (m_cacheInvalid || !m_cache) return; int formerRotation = m_colourRotation; m_cache->setNumColors(256); m_cache->setColor(0, qRgb(255, 255, 255)); for (int pixel = 1; pixel < 256; ++pixel) { QColor colour; int hue, px; switch (m_colourScheme) { default: case DefaultColours: hue = 256 - pixel; colour = QColor::fromHsv(hue, pixel/2 + 128, pixel); break; case WhiteOnBlack: colour = QColor(pixel, pixel, pixel); break; case BlackOnWhite: colour = QColor(256-pixel, 256-pixel, 256-pixel); break; case RedOnBlue: colour = QColor(pixel > 128 ? (pixel - 128) * 2 : 0, 0, pixel < 128 ? pixel : (256 - pixel)); break; case YellowOnBlack: px = 256 - pixel; colour = QColor(px < 64 ? 255 - px/2 : px < 128 ? 224 - (px - 64) : px < 192 ? 160 - (px - 128) * 3 / 2 : 256 - px, pixel, pixel / 4); break; case RedOnBlack: colour = QColor::fromHsv(10, pixel, pixel); break; } m_cache->setColor (pixel, qRgb(colour.red(), colour.green(), colour.blue())); } m_colourRotation = 0; rotateCacheColourmap(m_colourRotation - formerRotation); m_colourRotation = formerRotation; } void SpectrogramLayer::rotateCacheColourmap(int distance) { if (!m_cache) return; QRgb newPixels[256]; newPixels[0] = m_cache->color(0); for (int pixel = 1; pixel < 256; ++pixel) { int target = pixel + distance; while (target < 1) target += 255; while (target > 255) target -= 255; newPixels[target] = m_cache->color(pixel); } for (int pixel = 0; pixel < 256; ++pixel) { m_cache->setColor(pixel, newPixels[pixel]); } } bool SpectrogramLayer::fillCacheColumn(int column, double *input, fftw_complex *output, fftw_plan plan, size_t windowSize, size_t increment, const Window<double> &windower, bool lock) const { int startFrame = increment * column; int endFrame = startFrame + windowSize; startFrame -= int(windowSize - increment) / 2; endFrame -= int(windowSize - increment) / 2; size_t pfx = 0; if (startFrame < 0) { pfx = size_t(-startFrame); for (size_t i = 0; i < pfx; ++i) { input[i] = 0.0; } } size_t got = m_model->getValues(m_channel, startFrame + pfx, endFrame, input + pfx); while (got + pfx < windowSize) { input[got + pfx] = 0.0; ++got; } if (m_gain != 1.0) { for (size_t i = 0; i < windowSize; ++i) { input[i] *= m_gain; } } windower.cut(input); fftw_execute(plan); if (lock) m_mutex.lock(); bool interrupted = false; for (size_t i = 0; i < windowSize / 2; ++i) { int value = 0; if (m_colourScale == PhaseColourScale) { double phase = atan2(-output[i][1], output[i][0]); value = int((phase * 128 / M_PI) + 128); } else { double mag = sqrt(output[i][0] * output[i][0] + output[i][1] * output[i][1]); mag /= windowSize / 2; switch (m_colourScale) { default: case LinearColourScale: value = int(mag * 50 * 256); break; case MeterColourScale: value = AudioLevel::multiplier_to_preview(mag * 50, 256); break; case dBColourScale: mag = 20.0 * log10(mag); mag = (mag + 80.0) / 80.0; if (mag < 0.0) mag = 0.0; if (mag > 1.0) mag = 1.0; value = int(mag * 256); } } if (value > 254) value = 254; if (value < 0) value = 0; if (m_cacheInvalid || m_exiting) { interrupted = true; break; } if (column < m_cache->width() && (int)i < m_cache->height()) { m_cache->setPixel(column, i, value + 1); // 0 is "unset" } } if (lock) m_mutex.unlock(); return !interrupted; } void SpectrogramLayer::CacheFillThread::run() { // std::cerr << "SpectrogramLayer::CacheFillThread::run" << std::endl; m_layer.m_mutex.lock(); while (!m_layer.m_exiting) { bool interrupted = false; // std::cerr << "SpectrogramLayer::CacheFillThread::run in loop" << std::endl; if (m_layer.m_model && m_layer.m_cacheInvalid) { // std::cerr << "SpectrogramLayer::CacheFillThread::run: something to do" << std::endl; while (!m_layer.m_model->isReady()) { m_layer.m_condition.wait(&m_layer.m_mutex, 100); } m_layer.m_cachedInitialVisibleArea = false; m_layer.m_cacheInvalid = false; m_fillExtent = 0; m_fillCompletion = 0; std::cerr << "SpectrogramLayer::CacheFillThread::run: model is ready" << std::endl; size_t start = m_layer.m_model->getStartFrame(); size_t end = m_layer.m_model->getEndFrame(); WindowType windowType = m_layer.m_windowType; size_t windowSize = m_layer.m_windowSize; size_t windowIncrement = m_layer.getWindowIncrement(); size_t visibleStart = start; size_t visibleEnd = end; if (m_layer.m_view) { if (m_layer.m_view->getStartFrame() < 0) { visibleStart = 0; } else { visibleStart = m_layer.m_view->getStartFrame(); visibleStart = (visibleStart / windowIncrement) * windowIncrement; } visibleEnd = m_layer.m_view->getEndFrame(); } delete m_layer.m_cache; size_t width = (end - start) / windowIncrement + 1; size_t height = windowSize / 2; m_layer.m_cache = new QImage(width, height, QImage::Format_Indexed8); // If we're using JACK in mlock mode, this will be locked // and we ought to unlock to avoid memory exhaustion. // Shame it doesn't appear to be possible to allocate // unlocked in the first place. //!!! hm, I don't think this is working. MUNLOCK((void *)m_layer.m_cache, width * height); m_layer.setCacheColourmap(); m_layer.m_cache->fill(0); m_layer.m_mutex.unlock(); double *input = (double *) fftw_malloc(windowSize * sizeof(double)); fftw_complex *output = (fftw_complex *) fftw_malloc(windowSize * sizeof(fftw_complex)); fftw_plan plan = fftw_plan_dft_r2c_1d(windowSize, input, output, FFTW_ESTIMATE); Window<double> windower(windowType, windowSize); if (!plan) { std::cerr << "WARNING: fftw_plan_dft_r2c_1d(" << windowSize << ") failed!" << std::endl; fftw_free(input); fftw_free(output); m_layer.m_mutex.lock(); continue; } int counter = 0; int updateAt = (end / windowIncrement) / 20; if (updateAt < 100) updateAt = 100; bool doVisibleFirst = (visibleStart != start && visibleEnd != end); if (doVisibleFirst) { m_layer.m_mutex.lock(); for (size_t f = visibleStart; f < visibleEnd; f += windowIncrement) { m_layer.fillCacheColumn(int((f - start) / windowIncrement), input, output, plan, windowSize, windowIncrement, windower, false); m_layer.m_mutex.unlock(); m_layer.m_mutex.lock(); if (m_layer.m_cacheInvalid || m_layer.m_exiting) { interrupted = true; m_fillExtent = 0; break; } if (++counter == updateAt) { if (f < end) m_fillExtent = f; m_fillCompletion = size_t(100 * fabsf(float(f - visibleStart) / float(end - start))); counter = 0; } } m_layer.m_mutex.unlock(); } m_layer.m_cachedInitialVisibleArea = true; if (!interrupted && doVisibleFirst) { for (size_t f = visibleEnd; f < end; f += windowIncrement) { if (!m_layer.fillCacheColumn(int((f - start) / windowIncrement), input, output, plan, windowSize, windowIncrement, windower, true)) { interrupted = true; m_fillExtent = 0; break; } if (++counter == updateAt) { if (f < end) m_fillExtent = f; m_fillCompletion = size_t(100 * fabsf(float(f - visibleStart) / float(end - start))); counter = 0; } } } if (!interrupted) { size_t remainingEnd = end; if (doVisibleFirst) { remainingEnd = visibleStart; if (remainingEnd > start) --remainingEnd; else remainingEnd = start; } size_t baseCompletion = m_fillCompletion; for (size_t f = start; f < remainingEnd; f += windowIncrement) { if (!m_layer.fillCacheColumn(int((f - start) / windowIncrement), input, output, plan, windowSize, windowIncrement, windower, true)) { interrupted = true; m_fillExtent = 0; break; } if (++counter == updateAt) { m_fillExtent = f; m_fillCompletion = baseCompletion + size_t(100 * fabsf(float(f - start) / float(end - start))); counter = 0; } } } fftw_destroy_plan(plan); fftw_free(output); fftw_free(input); if (!interrupted) { m_fillExtent = end; m_fillCompletion = 100; } m_layer.m_mutex.lock(); } if (!interrupted) m_layer.m_condition.wait(&m_layer.m_mutex, 2000); } } bool SpectrogramLayer::getYBinRange(int y, float &q0, float &q1) const { int h = m_view->height(); if (y < 0 || y >= h) return false; // Each pixel in a column is drawn from a possibly non- // integral set of frequency bins. if (m_frequencyScale == LinearFrequencyScale) { size_t bins = m_windowSize / 2; if (m_maxFrequency > 0) { int sr = m_model->getSampleRate(); bins = int((double(m_maxFrequency) * m_windowSize) / sr + 0.1); if (bins > m_windowSize / 2) bins = m_windowSize / 2; } q0 = float(h - y - 1) * bins / h; q1 = float(h - y) * bins / h; } else { // This is all most ad-hoc. I'm not at my brightest. int sr = m_model->getSampleRate(); float maxf = m_maxFrequency; if (maxf == 0.0) maxf = float(sr) / 2; float minf = float(sr) / m_windowSize; float maxlogf = log10f(maxf); float minlogf = log10f(minf); float logf0 = minlogf + ((maxlogf - minlogf) * (h - y - 1)) / h; float logf1 = minlogf + ((maxlogf - minlogf) * (h - y)) / h; float f0 = pow(10.f, logf0); float f1 = pow(10.f, logf1); q0 = ((f0 * m_windowSize) / sr) - 1; q1 = ((f1 * m_windowSize) / sr) - 1; // std::cout << "y=" << y << " h=" << h << " maxf=" << maxf << " maxlogf=" // << maxlogf << " logf0=" << logf0 << " f0=" << f0 << " q0=" // << q0 << std::endl; } return true; } bool SpectrogramLayer::getXBinRange(int x, float &s0, float &s1, LayerRange *range) const { long startFrame; int zoomLevel; size_t modelStart; size_t modelEnd; if (range) { startFrame = range->startFrame; zoomLevel = range->zoomLevel; modelStart = range->modelStart; modelEnd = range->modelEnd; } else { startFrame = m_view->getStartFrame(); zoomLevel = m_view->getZoomLevel(); modelStart = m_model->getStartFrame(); modelEnd = m_model->getEndFrame(); } // Each pixel column covers an exact range of sample frames: int f0 = startFrame + x * zoomLevel - modelStart; int f1 = f0 + zoomLevel - 1; if (f1 < int(modelStart) || f0 > int(modelEnd)) return false; // And that range may be drawn from a possibly non-integral // range of spectrogram windows: size_t windowIncrement = getWindowIncrement(); s0 = float(f0) / windowIncrement; s1 = float(f1) / windowIncrement; return true; } bool SpectrogramLayer::getXBinSourceRange(int x, RealTime &min, RealTime &max) const { float s0 = 0, s1 = 0; if (!getXBinRange(x, s0, s1)) return false; int s0i = int(s0 + 0.001); int s1i = int(s1); int windowIncrement = getWindowIncrement(); int w0 = s0i * windowIncrement - (m_windowSize - windowIncrement)/2; int w1 = s1i * windowIncrement + windowIncrement + (m_windowSize - windowIncrement)/2 - 1; min = RealTime::frame2RealTime(w0, m_model->getSampleRate()); max = RealTime::frame2RealTime(w1, m_model->getSampleRate()); return true; } bool SpectrogramLayer::getYBinSourceRange(int y, float &freqMin, float &freqMax) const { float q0 = 0, q1 = 0; if (!getYBinRange(y, q0, q1)) return false; int q0i = int(q0 + 0.001); int q1i = int(q1); int sr = m_model->getSampleRate(); for (int q = q0i; q <= q1i; ++q) { int binfreq = (sr * (q + 1)) / m_windowSize; if (q == q0i) freqMin = binfreq; if (q == q1i) freqMax = binfreq; } return true; } bool SpectrogramLayer::getXYBinSourceRange(int x, int y, float &dbMin, float &dbMax) const { float q0 = 0, q1 = 0; if (!getYBinRange(y, q0, q1)) return false; float s0 = 0, s1 = 0; if (!getXBinRange(x, s0, s1)) return false; int q0i = int(q0 + 0.001); int q1i = int(q1); int s0i = int(s0 + 0.001); int s1i = int(s1); if (m_mutex.tryLock()) { if (m_cache && !m_cacheInvalid) { int cw = m_cache->width(); int ch = m_cache->height(); int min = -1, max = -1; for (int q = q0i; q <= q1i; ++q) { for (int s = s0i; s <= s1i; ++s) { if (s >= 0 && q >= 0 && s < cw && q < ch) { int value = m_cache->scanLine(q)[s]; if (min == -1 || value < min) min = value; if (max == -1 || value > max) max = value; } } } if (min < 0) return false; dbMin = (float(min) / 256.0) * 80.0 - 80.0; dbMax = (float(max + 1) / 256.0) * 80.0 - 80.1; m_mutex.unlock(); return true; } m_mutex.unlock(); } return false; } void SpectrogramLayer::paint(QPainter &paint, QRect rect) const { // Profiler profiler("SpectrogramLayer::paint", true); #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer::paint(): m_model is " << m_model << ", zoom level is " << m_view->getZoomLevel() << ", m_updateTimer " << m_updateTimer << ", pixmap cache invalid " << m_pixmapCacheInvalid << std::endl; #endif if (!m_model || !m_model->isOK() || !m_model->isReady()) { return; } #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer::paint(): About to lock" << std::endl; #endif /* if (m_cachedInitialVisibleArea) { if (!m_mutex.tryLock()) { m_view->update(); return; } } else { */ m_mutex.lock(); // } #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer::paint(): locked" << std::endl; #endif if (m_cacheInvalid) { // lock the mutex before checking this m_mutex.unlock(); #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer::paint(): Cache invalid, returning" << std::endl; #endif return; } bool stillCacheing = (m_updateTimer != 0); #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer::paint(): Still cacheing = " << stillCacheing << std::endl; #endif long startFrame = m_view->getStartFrame(); int zoomLevel = m_view->getZoomLevel(); int x0 = 0; int x1 = m_view->width(); int y0 = 0; int y1 = m_view->height(); bool recreateWholePixmapCache = true; if (!m_pixmapCacheInvalid) { //!!! This cache may have been obsoleted entirely by the //scrolling cache in View. Perhaps experiment with //removing it and see if it makes things even quicker (or else //make it optional) if (int(m_pixmapCacheZoomLevel) == zoomLevel && m_pixmapCache->width() == m_view->width() && m_pixmapCache->height() == m_view->height()) { if (m_pixmapCacheStartFrame / zoomLevel == startFrame / zoomLevel) { #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer: pixmap cache good" << std::endl; #endif m_mutex.unlock(); paint.drawPixmap(rect, *m_pixmapCache, rect); return; } else { #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer: pixmap cache partially OK" << std::endl; #endif recreateWholePixmapCache = false; int dx = (m_pixmapCacheStartFrame - startFrame) / zoomLevel; #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer: dx = " << dx << " (pixmap cache " << m_pixmapCache->width() << "x" << m_pixmapCache->height() << ")" << std::endl; #endif if (dx > -m_pixmapCache->width() && dx < m_pixmapCache->width()) { #if defined(Q_WS_WIN32) || defined(Q_WS_MAC) // Copying a pixmap to itself doesn't work // properly on Windows or Mac (it only works when // moving in one direction). //!!! Need a utility function for this static QPixmap *tmpPixmap = 0; if (!tmpPixmap || tmpPixmap->width() != m_pixmapCache->width() || tmpPixmap->height() != m_pixmapCache->height()) { delete tmpPixmap; tmpPixmap = new QPixmap(m_pixmapCache->width(), m_pixmapCache->height()); } QPainter cachePainter; cachePainter.begin(tmpPixmap); cachePainter.drawPixmap(0, 0, *m_pixmapCache); cachePainter.end(); cachePainter.begin(m_pixmapCache); cachePainter.drawPixmap(dx, 0, *tmpPixmap); cachePainter.end(); #else QPainter cachePainter(m_pixmapCache); cachePainter.drawPixmap(dx, 0, *m_pixmapCache); cachePainter.end(); #endif paint.drawPixmap(rect, *m_pixmapCache, rect); if (dx < 0) { x0 = m_pixmapCache->width() + dx; x1 = m_pixmapCache->width(); } else { x0 = 0; x1 = dx; } } } } else { #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer: pixmap cache useless" << std::endl; #endif } } if (stillCacheing) { x0 = rect.left(); x1 = rect.right() + 1; y0 = rect.top(); y1 = rect.bottom() + 1; } int w = x1 - x0; int h = y1 - y0; // std::cerr << "x0 " << x0 << ", x1 " << x1 << ", w " << w << ", h " << h << std::endl; QImage scaled(w, h, QImage::Format_RGB32); LayerRange range = { m_view->getStartFrame(), m_view->getZoomLevel(), m_model->getStartFrame(), m_model->getEndFrame() }; m_mutex.unlock(); for (int y = 0; y < h; ++y) { m_mutex.lock(); if (m_cacheInvalid) { m_mutex.unlock(); break; } int cw = m_cache->width(); int ch = m_cache->height(); float q0 = 0, q1 = 0; if (!getYBinRange(y0 + y, q0, q1)) { for (int x = 0; x < w; ++x) { assert(x <= scaled.width()); scaled.setPixel(x, y, qRgb(0, 0, 0)); } m_mutex.unlock(); continue; } int q0i = int(q0 + 0.001); int q1i = int(q1); for (int x = 0; x < w; ++x) { float s0 = 0, s1 = 0; if (!getXBinRange(x0 + x, s0, s1, &range)) { assert(x <= scaled.width()); scaled.setPixel(x, y, qRgb(0, 0, 0)); continue; } int s0i = int(s0 + 0.001); int s1i = int(s1); float total = 0, divisor = 0; for (int s = s0i; s <= s1i; ++s) { float sprop = 1.0; if (s == s0i) sprop *= (s + 1) - s0; if (s == s1i) sprop *= s1 - s; for (int q = q0i; q <= q1i; ++q) { float qprop = sprop; if (q == q0i) qprop *= (q + 1) - q0; if (q == q1i) qprop *= q1 - q; if (s >= 0 && q >= 0 && s < cw && q < ch) { total += qprop * m_cache->scanLine(q)[s]; divisor += qprop; } } } if (divisor > 0.0) { /* int pixel = int(total / divisor); if (pixel > 255) pixel = 255; if (pixel < 1) pixel = 1; assert(x <= scaled.width()); scaled.setPixel(x, y, m_cache->color(pixel)); */ float pixel = total / divisor; float lq = pixel - int(pixel); float hq = int(pixel) + 1 - pixel; int pixNum = int(pixel); QRgb low = m_cache->color(pixNum > 255 ? 255 : pixNum); QRgb high = m_cache->color(pixNum > 254 ? 255 : pixNum + 1); QRgb mixed = qRgb (qRed(low) * lq + qRed(high) * hq + 0.01, qGreen(low) * lq + qGreen(high) * hq + 0.01, qBlue(low) * lq + qBlue(high) * hq + 0.01); scaled.setPixel(x, y, mixed); } else { assert(x <= scaled.width()); scaled.setPixel(x, y, qRgb(0, 0, 0)); } } m_mutex.unlock(); } paint.drawImage(x0, y0, scaled); if (recreateWholePixmapCache) { delete m_pixmapCache; m_pixmapCache = new QPixmap(w, h); } QPainter cachePainter(m_pixmapCache); cachePainter.drawImage(x0, y0, scaled); cachePainter.end(); m_pixmapCacheInvalid = false; m_pixmapCacheStartFrame = startFrame; m_pixmapCacheZoomLevel = zoomLevel; #ifdef DEBUG_SPECTROGRAM_REPAINT std::cerr << "SpectrogramLayer::paint() returning" << std::endl; #endif //!!! drawLocalFeatureDescription(paint); } int SpectrogramLayer::getCompletion() const { if (m_updateTimer == 0) return 100; size_t completion = m_fillThread->getFillCompletion(); // std::cerr << "SpectrogramLayer::getCompletion: completion = " << completion << std::endl; return completion; } QRect SpectrogramLayer::getFeatureDescriptionRect(QPainter &paint, QPoint pos) const { if (!m_model || !m_model->isOK()) return QRect(); QString timeLabel = tr("Time: "); QString freqLabel = tr("Hz: "); QString dBLabel = tr("dB: "); // assume time is widest RealTime rtMin, rtMax; if (!getXBinSourceRange(pos.x(), rtMin, rtMax)) return QRect(); QString timeMinText = QString("%1").arg(rtMin.toText(true).c_str()); QString timeMaxText = QString(" - %1").arg(rtMax.toText(true).c_str()); QFontMetrics metrics = paint.fontMetrics(); int labelwidth = std::max(std::max(metrics.width(timeLabel), metrics.width(freqLabel)), metrics.width(dBLabel)); int boxwidth = labelwidth + metrics.width(timeMinText) + metrics.width(timeMaxText); int fontHeight = metrics.height(); int boxheight = fontHeight * 3 + 4; return QRect(0, 0, boxwidth + 20, boxheight + 15); } void SpectrogramLayer::paintLocalFeatureDescription(QPainter &paint, QRect rect, QPoint pos) const { int x = pos.x(); int y = pos.y(); if (!m_model || !m_model->isOK()) return; float dbMin = 0, dbMax = 0; float freqMin = 0, freqMax = 0; RealTime rtMin, rtMax; bool haveDb = false; if (!getXBinSourceRange(x, rtMin, rtMax)) return; if (!getYBinSourceRange(y, freqMin, freqMax)) return; if (getXYBinSourceRange(x, y, dbMin, dbMax)) haveDb = true; QString timeLabel = tr("Time: "); QString freqLabel = tr("Hz: "); QString dBLabel = tr("dB: "); QString timeMinText = QString("%1").arg(rtMin.toText(true).c_str()); QString timeMaxText = QString(" - %1").arg(rtMax.toText(true).c_str()); QString freqMinText = QString("%1").arg(freqMin); QString freqMaxText = ""; if (freqMax != freqMin) { freqMaxText = QString(" - %1").arg(freqMax); } QString dBMinText = ""; QString dBMaxText = ""; if (haveDb) { int dbmxi = int(dbMax - 0.001); int dbmni = int(dbMin - 0.001); dBMinText = QString("%1").arg(dbmni); if (dbmxi != dbmni) dBMaxText = QString(" - %1").arg(dbmxi); } QFontMetrics metrics = paint.fontMetrics(); int labelwidth = std::max(std::max(metrics.width(timeLabel), metrics.width(freqLabel)), metrics.width(dBLabel)); int minwidth = std::max(std::max(metrics.width(timeMinText), metrics.width(freqMinText)), metrics.width(dBMinText)); int maxwidth = std::max(std::max(metrics.width(timeMaxText), metrics.width(freqMaxText)), metrics.width(dBMaxText)); int boxwidth = labelwidth + minwidth + maxwidth; int fontAscent = metrics.ascent(); int fontHeight = metrics.height(); int boxheight = fontHeight * 3 + 4; // paint.setPen(Qt::white); // paint.setBrush(Qt::NoBrush); //!!! int xbase = m_view->width() - boxwidth - 20; int xbase = rect.x() + 5; int ybase = rect.y() + 5; paint.drawRect(xbase, ybase, boxwidth + 10, boxheight + 10 - metrics.descent() + 1); paint.drawText(xbase + 5 + labelwidth - metrics.width(timeLabel), ybase + 5 + fontAscent, timeLabel); paint.drawText(xbase + 5 + labelwidth - metrics.width(freqLabel), ybase + 7 + fontAscent + fontHeight, freqLabel); paint.drawText(xbase + 5 + labelwidth - metrics.width(dBLabel), ybase + 9 + fontAscent + fontHeight * 2, dBLabel); paint.drawText(xbase + 5 + labelwidth + minwidth - metrics.width(timeMinText), ybase + 5 + fontAscent, timeMinText); paint.drawText(xbase + 5 + labelwidth + minwidth - metrics.width(freqMinText), ybase + 7 + fontAscent + fontHeight, freqMinText); paint.drawText(xbase + 5 + labelwidth + minwidth - metrics.width(dBMinText), ybase + 9 + fontAscent + fontHeight * 2, dBMinText); paint.drawText(xbase + 5 + labelwidth + minwidth, ybase + 5 + fontAscent, timeMaxText); paint.drawText(xbase + 5 + labelwidth + minwidth, ybase + 7 + fontAscent + fontHeight, freqMaxText); paint.drawText(xbase + 5 + labelwidth + minwidth, ybase + 9 + fontAscent + fontHeight * 2, dBMaxText); } int SpectrogramLayer::getNearestFeatureFrame(int frame, size_t &resolution, bool snapRight) const { resolution = getWindowIncrement(); int snapFrame = (frame / resolution) * resolution; if (snapRight) snapFrame += resolution; return snapFrame; } /*!!! bool SpectrogramLayer::identifyLocalFeatures(bool on, int x, int y) { return true; //!!! m_identify = on; m_identifyX = x; m_identifyY = y; m_view->update(); */ /* if (!m_model || !m_model->isOK()) return false; std::cerr << "SpectrogramLayer::identifyLocalFeatures(" << on << "," << x << "," << y << ")" << std::endl; float dbMin = 0, dbMax = 0; float freqMin = 0, freqMax = 0; RealTime rtMin, rtMax; if (getXBinSourceRange(x, rtMin, rtMax)) { std::cerr << "Times: " << rtMin << " -> " << rtMax << std::endl; } else return false; if (getYBinSourceRange(y, freqMin, freqMax)) { std::cerr << "Frequencies: " << freqMin << " -> " << freqMax << std::endl; } else return false; if (getXYBinSourceRange(x, y, dbMin, dbMax)) { std::cerr << "dB: " << dbMin << " -> " << dbMax << std::endl; } m_identifyX = x; m_identifyY = y; m_identify = true; */ /*!!! return true; } */ int SpectrogramLayer::getVerticalScaleWidth(QPainter &paint) const { if (!m_model || !m_model->isOK()) return 0; int tw = paint.fontMetrics().width(QString("%1") .arg(m_maxFrequency > 0 ? m_maxFrequency - 1 : m_model->getSampleRate() / 2)); int fw = paint.fontMetrics().width(QString("43Hz")); if (tw < fw) tw = fw; return tw + 13; } void SpectrogramLayer::paintVerticalScale(QPainter &paint, QRect rect) const { if (!m_model || !m_model->isOK()) { return; } int h = rect.height(), w = rect.width(); size_t bins = m_windowSize / 2; int sr = m_model->getSampleRate(); if (m_maxFrequency > 0) { bins = int((double(m_maxFrequency) * m_windowSize) / sr + 0.1); if (bins > m_windowSize / 2) bins = m_windowSize / 2; } int py = -1; int textHeight = paint.fontMetrics().height(); int toff = -textHeight + paint.fontMetrics().ascent() + 2; int bin = -1; for (int y = 0; y < m_view->height(); ++y) { float q0, q1; if (!getYBinRange(m_view->height() - y, q0, q1)) continue; int vy; if (int(q0) > bin) { vy = y; bin = int(q0); } else { continue; } int freq = (sr * (bin + 1)) / m_windowSize; if (py >= 0 && (vy - py) < textHeight - 1) { paint.drawLine(w - 4, h - vy, w, h - vy); continue; } QString text = QString("%1").arg(freq); if (bin == 0) text = QString("%1Hz").arg(freq); paint.drawLine(0, h - vy, w, h - vy); if (h - vy - textHeight >= -2) { int tx = w - 10 - paint.fontMetrics().width(text); paint.drawText(tx, h - vy + toff, text); } py = vy; } } QString SpectrogramLayer::toXmlString(QString indent, QString extraAttributes) const { QString s; s += QString("channel=\"%1\" " "windowSize=\"%2\" " "windowType=\"%3\" " "windowOverlap=\"%4\" " "gain=\"%5\" " "maxFrequency=\"%6\" " "colourScale=\"%7\" " "colourScheme=\"%8\" " "frequencyScale=\"%9\"") .arg(m_channel) .arg(m_windowSize) .arg(m_windowType) .arg(m_windowOverlap) .arg(m_gain) .arg(m_maxFrequency) .arg(m_colourScale) .arg(m_colourScheme) .arg(m_frequencyScale); return Layer::toXmlString(indent, extraAttributes + " " + s); } void SpectrogramLayer::setProperties(const QXmlAttributes &attributes) { bool ok = false; int channel = attributes.value("channel").toInt(&ok); if (ok) setChannel(channel); size_t windowSize = attributes.value("windowSize").toUInt(&ok); if (ok) setWindowSize(windowSize); WindowType windowType = (WindowType) attributes.value("windowType").toInt(&ok); if (ok) setWindowType(windowType); size_t windowOverlap = attributes.value("windowOverlap").toUInt(&ok); if (ok) setWindowOverlap(windowOverlap); float gain = attributes.value("gain").toFloat(&ok); if (ok) setGain(gain); size_t maxFrequency = attributes.value("maxFrequency").toUInt(&ok); if (ok) setMaxFrequency(maxFrequency); ColourScale colourScale = (ColourScale) attributes.value("colourScale").toInt(&ok); if (ok) setColourScale(colourScale); ColourScheme colourScheme = (ColourScheme) attributes.value("colourScheme").toInt(&ok); if (ok) setColourScheme(colourScheme); FrequencyScale frequencyScale = (FrequencyScale) attributes.value("frequencyScale").toInt(&ok); if (ok) setFrequencyScale(frequencyScale); } #ifdef INCLUDE_MOCFILES #include "SpectrogramLayer.moc.cpp" #endif