diff options
Diffstat (limited to 'tests/spectrum/spectrumapp')
17 files changed, 2147 insertions, 0 deletions
diff --git a/tests/spectrum/spectrumapp/engine.cpp b/tests/spectrum/spectrumapp/engine.cpp new file mode 100644 index 00000000..60605223 --- /dev/null +++ b/tests/spectrum/spectrumapp/engine.cpp @@ -0,0 +1,619 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#include "engine.h" +#include "utils.h" + +#include <math.h> + +#include <QAudioInput> +#include <QAudioOutput> +#include <QCoreApplication> +#include <QDebug> +#include <QFile> +#include <QMetaObject> +#include <QSet> +#include <QThread> + +//----------------------------------------------------------------------------- +// Constants +//----------------------------------------------------------------------------- + +const qint64 BufferDurationUs = 10 * 1000000; +const int NotifyIntervalMs = 100; + +// Size of the level calculation window in microseconds +const int LevelWindowUs = 0.1 * 1000000; + +//----------------------------------------------------------------------------- +// Constructor and destructor +//----------------------------------------------------------------------------- + +Engine::Engine(QObject *parent) + : QObject(parent), + m_mode(QAudio::AudioInput), + m_state(QAudio::StoppedState), + m_generateTone(false), + m_file(0), + m_analysisFile(0), + m_availableAudioInputDevices + (QAudioDeviceInfo::availableDevices(QAudio::AudioInput)), + m_audioInputDevice(QAudioDeviceInfo::defaultInputDevice()), + m_audioInput(0), + m_audioInputIODevice(0), + m_recordPosition(0), + m_availableAudioOutputDevices + (QAudioDeviceInfo::availableDevices(QAudio::AudioOutput)), + m_audioOutputDevice(QAudioDeviceInfo::defaultOutputDevice()), + m_audioOutput(0), + m_playPosition(0), + m_bufferPosition(0), + m_bufferLength(0), + m_dataLength(0), + m_levelBufferLength(0), + m_rmsLevel(0.0), + m_peakLevel(0.0), + m_spectrumBufferLength(0), + m_spectrumAnalyser(), + m_spectrumPosition(0), + m_count(0) +{ + qRegisterMetaType<FrequencySpectrum>("FrequencySpectrum"); + qRegisterMetaType<WindowFunction>("WindowFunction"); + CHECKED_CONNECT(&m_spectrumAnalyser, + SIGNAL(spectrumChanged(FrequencySpectrum)), + this, + SLOT(spectrumChanged(FrequencySpectrum))); + + initialize(); + qDebug() << "output devices:"; + foreach (QAudioDeviceInfo device, m_availableAudioOutputDevices) + qDebug() << device.deviceName(); + qDebug() << "input devices:"; + foreach (QAudioDeviceInfo device, m_availableAudioInputDevices) + qDebug() << device.deviceName(); +} + +Engine::~Engine() +{ + +} + +//----------------------------------------------------------------------------- +// Public functions +//----------------------------------------------------------------------------- + +bool Engine::loadFile(const QString &fileName) +{ + reset(); + bool result = false; + Q_ASSERT(!m_generateTone); + Q_ASSERT(!m_file); + Q_ASSERT(!fileName.isEmpty()); + m_file = new WavFile(this); + if (m_file->open(fileName)) { + if (isPCMS16LE(m_file->fileFormat())) { + result = initialize(); + } else { + ENGINE_DEBUG << "Audio format not supported" << formatToString(m_file->fileFormat()); + } + } else { + ENGINE_DEBUG << "Could not open file" << fileName; + } + if (result) { + m_analysisFile = new WavFile(this); + m_analysisFile->open(fileName); + } + return result; +} + +qint64 Engine::bufferLength() const +{ + return m_file ? m_file->size() : m_bufferLength; +} + +void Engine::setWindowFunction(WindowFunction type) +{ + m_spectrumAnalyser.setWindowFunction(type); +} + + +//----------------------------------------------------------------------------- +// Public slots +//----------------------------------------------------------------------------- + +void Engine::startRecording() +{ + if (m_audioInput) { + if (QAudio::AudioInput == m_mode && + QAudio::SuspendedState == m_state) { + m_audioInput->resume(); + } else { + m_spectrumAnalyser.cancelCalculation(); + changedSpectrum(0, 0, FrequencySpectrum()); + + m_buffer.fill(0); + setRecordPosition(0, true); + stopPlayback(); + m_mode = QAudio::AudioInput; + CHECKED_CONNECT(m_audioInput, SIGNAL(stateChanged(QAudio::State)), + this, SLOT(audioStateChanged(QAudio::State))); + CHECKED_CONNECT(m_audioInput, SIGNAL(notify()), + this, SLOT(audioNotify())); + m_count = 0; + m_dataLength = 0; + emit dataLengthChanged(0); + m_audioInputIODevice = m_audioInput->start(); + CHECKED_CONNECT(m_audioInputIODevice, SIGNAL(readyRead()), + this, SLOT(audioDataReady())); + } + } +} + +void Engine::startPlayback() +{ + if (m_audioOutput) { + if (QAudio::AudioOutput == m_mode && + QAudio::SuspendedState == m_state) { +#ifdef Q_OS_WIN + // The Windows backend seems to internally go back into ActiveState + // while still returning SuspendedState, so to ensure that it doesn't + // ignore the resume() call, we first re-suspend + m_audioOutput->suspend(); +#endif + m_audioOutput->resume(); + } else { + m_spectrumAnalyser.cancelCalculation(); + changedSpectrum(0, 0, FrequencySpectrum()); + setPlayPosition(0, true); + stopRecording(); + m_mode = QAudio::AudioOutput; + CHECKED_CONNECT(m_audioOutput, SIGNAL(stateChanged(QAudio::State)), + this, SLOT(audioStateChanged(QAudio::State))); + CHECKED_CONNECT(m_audioOutput, SIGNAL(notify()), + this, SLOT(audioNotify())); + m_count = 0; + if (m_file) { + m_file->seek(0); + m_bufferPosition = 0; + m_dataLength = 0; + m_audioOutput->start(m_file); + } else { + m_audioOutputIODevice.close(); + m_audioOutputIODevice.setBuffer(&m_buffer); + m_audioOutputIODevice.open(QIODevice::ReadOnly); + m_audioOutput->start(&m_audioOutputIODevice); + } + } + } +} + +void Engine::suspend() +{ + if (QAudio::ActiveState == m_state || + QAudio::IdleState == m_state) { + switch (m_mode) { + case QAudio::AudioInput: + m_audioInput->suspend(); + break; + case QAudio::AudioOutput: + m_audioOutput->suspend(); + break; + } + } +} + +void Engine::setAudioInputDevice(const QAudioDeviceInfo &device) +{ + if (device.deviceName() != m_audioInputDevice.deviceName()) { + m_audioInputDevice = device; + initialize(); + } +} + +void Engine::setAudioOutputDevice(const QAudioDeviceInfo &device) +{ + if (device.deviceName() != m_audioOutputDevice.deviceName()) { + m_audioOutputDevice = device; + initialize(); + } +} + + +//----------------------------------------------------------------------------- +// Private slots +//----------------------------------------------------------------------------- + +void Engine::audioNotify() +{ + switch (m_mode) { + case QAudio::AudioInput: { + const qint64 recordPosition = qMin(m_bufferLength, audioLength(m_format, m_audioInput->processedUSecs())); + setRecordPosition(recordPosition); + const qint64 levelPosition = m_dataLength - m_levelBufferLength; + if (levelPosition >= 0) + calculateLevel(levelPosition, m_levelBufferLength); + if (m_dataLength >= m_spectrumBufferLength) { + const qint64 spectrumPosition = m_dataLength - m_spectrumBufferLength; + calculateSpectrum(spectrumPosition); + } + emit bufferChanged(0, m_dataLength, m_buffer); + } + break; + case QAudio::AudioOutput: { + const qint64 playPosition = audioLength(m_format, m_audioOutput->processedUSecs()); + setPlayPosition(qMin(bufferLength(), playPosition)); + const qint64 levelPosition = playPosition - m_levelBufferLength; + const qint64 spectrumPosition = playPosition - m_spectrumBufferLength; + if (m_file) { + if (levelPosition > m_bufferPosition || + spectrumPosition > m_bufferPosition || + qMax(m_levelBufferLength, m_spectrumBufferLength) > m_dataLength) { + m_bufferPosition = 0; + m_dataLength = 0; + // Data needs to be read into m_buffer in order to be analysed + const qint64 readPos = qMax(qint64(0), qMin(levelPosition, spectrumPosition)); + const qint64 readEnd = qMin(m_analysisFile->size(), qMax(levelPosition + m_levelBufferLength, spectrumPosition + m_spectrumBufferLength)); + const qint64 readLen = readEnd - readPos + audioLength(m_format, WaveformWindowDuration); + ENGINE_DEBUG << "Engine::audioNotify [1]" + << "analysisFileSize" << m_analysisFile->size() + << "readPos" << readPos + << "readLen" << readLen; + if (m_analysisFile->seek(readPos + m_analysisFile->headerLength())) { + m_buffer.resize(readLen); + m_bufferPosition = readPos; + m_dataLength = m_analysisFile->read(m_buffer.data(), readLen); + ENGINE_DEBUG << "Engine::audioNotify [2]" << "bufferPosition" << m_bufferPosition << "dataLength" << m_dataLength; + } else { + ENGINE_DEBUG << "Engine::audioNotify [2]" << "file seek error"; + } + emit bufferChanged(m_bufferPosition, m_dataLength, m_buffer); + } + } else { + if (playPosition >= m_dataLength) + stopPlayback(); + } + if (levelPosition >= 0 && levelPosition + m_levelBufferLength < m_bufferPosition + m_dataLength) + calculateLevel(levelPosition, m_levelBufferLength); + if (spectrumPosition >= 0 && spectrumPosition + m_spectrumBufferLength < m_bufferPosition + m_dataLength) + calculateSpectrum(spectrumPosition); + } + break; + } +} + +void Engine::audioStateChanged(QAudio::State state) +{ + ENGINE_DEBUG << "Engine::audioStateChanged from" << m_state + << "to" << state; + + if (QAudio::IdleState == state && m_file && m_file->pos() == m_file->size()) { + stopPlayback(); + } else { + if (QAudio::StoppedState == state) { + // Check error + QAudio::Error error = QAudio::NoError; + switch (m_mode) { + case QAudio::AudioInput: + error = m_audioInput->error(); + break; + case QAudio::AudioOutput: + error = m_audioOutput->error(); + break; + } + if (QAudio::NoError != error) { + reset(); + return; + } + } + setState(state); + } +} + +void Engine::audioDataReady() +{ + Q_ASSERT(0 == m_bufferPosition); + const qint64 bytesReady = m_audioInput->bytesReady(); + const qint64 bytesSpace = m_buffer.size() - m_dataLength; + const qint64 bytesToRead = qMin(bytesReady, bytesSpace); + + const qint64 bytesRead = m_audioInputIODevice->read( + m_buffer.data() + m_dataLength, + bytesToRead); + + if (bytesRead) { + m_dataLength += bytesRead; + emit dataLengthChanged(dataLength()); + } + + if (m_buffer.size() == m_dataLength) + stopRecording(); +} + +void Engine::spectrumChanged(const FrequencySpectrum &spectrum) +{ + ENGINE_DEBUG << "Engine::spectrumChanged" << "pos" << m_spectrumPosition; + emit changedSpectrum(m_spectrumPosition, m_spectrumBufferLength, spectrum); +} + + +//----------------------------------------------------------------------------- +// Private functions +//----------------------------------------------------------------------------- + +void Engine::resetAudioDevices() +{ + delete m_audioInput; + m_audioInput = 0; + m_audioInputIODevice = 0; + setRecordPosition(0); + delete m_audioOutput; + m_audioOutput = 0; + setPlayPosition(0); + m_spectrumPosition = 0; + setLevel(0.0, 0.0, 0); +} + +void Engine::reset() +{ + stopRecording(); + stopPlayback(); + setState(QAudio::AudioInput, QAudio::StoppedState); + setFormat(QAudioFormat()); + m_generateTone = false; + delete m_file; + m_file = 0; + delete m_analysisFile; + m_analysisFile = 0; + m_buffer.clear(); + m_bufferPosition = 0; + m_bufferLength = 0; + m_dataLength = 0; + emit dataLengthChanged(0); + resetAudioDevices(); +} + +bool Engine::initialize() +{ + bool result = false; + + QAudioFormat format = m_format; + + if (selectFormat()) { + resetAudioDevices(); + if (m_file) { + emit bufferLengthChanged(bufferLength()); + emit dataLengthChanged(dataLength()); + emit bufferChanged(0, 0, m_buffer); + setRecordPosition(bufferLength()); + result = true; + } + m_audioOutput = new QAudioOutput(m_audioOutputDevice, m_format, this); + m_audioOutput->setNotifyInterval(NotifyIntervalMs); + } else { + if (m_file) + ENGINE_DEBUG << "Audio format not supported" << formatToString(m_format); + else + ENGINE_DEBUG << "No common input / output format found"; + } + + ENGINE_DEBUG << "Engine::initialize" << "m_bufferLength" << m_bufferLength; + ENGINE_DEBUG << "Engine::initialize" << "m_dataLength" << m_dataLength; + ENGINE_DEBUG << "Engine::initialize" << "format" << m_format; + + return result; +} + +bool Engine::selectFormat() +{ + bool foundSupportedFormat = false; + + if (m_file || QAudioFormat() != m_format) { + QAudioFormat format = m_format; + if (m_file) + // Header is read from the WAV file; just need to check whether + // it is supported by the audio output device + format = m_file->fileFormat(); + if (m_audioOutputDevice.isFormatSupported(format)) { + setFormat(format); + foundSupportedFormat = true; + } + } else { + + QList<int> sampleRatesList; +#ifdef Q_OS_WIN + // The Windows audio backend does not correctly report format support + // (see QTBUG-9100). Furthermore, although the audio subsystem captures + // at 11025Hz, the resulting audio is corrupted. + sampleRatesList += 8000; +#endif + + if (!m_generateTone) + sampleRatesList += m_audioInputDevice.supportedSampleRates(); + + sampleRatesList += m_audioOutputDevice.supportedSampleRates(); + sampleRatesList = sampleRatesList.toSet().toList(); // remove duplicates + qSort(sampleRatesList); + ENGINE_DEBUG << "Engine::initialize frequenciesList" << sampleRatesList; + + QList<int> channelsList; + channelsList += m_audioInputDevice.supportedChannelCounts(); + channelsList += m_audioOutputDevice.supportedChannelCounts(); + channelsList = channelsList.toSet().toList(); + qSort(channelsList); + ENGINE_DEBUG << "Engine::initialize channelsList" << channelsList; + + QAudioFormat format; + format.setByteOrder(QAudioFormat::LittleEndian); + format.setCodec("audio/pcm"); + format.setSampleSize(16); + format.setSampleType(QAudioFormat::SignedInt); + int sampleRate, channels; + foreach (sampleRate, sampleRatesList) { + if (foundSupportedFormat) + break; + format.setSampleRate(sampleRate); + foreach (channels, channelsList) { + format.setChannelCount(channels); + const bool inputSupport = m_generateTone || + m_audioInputDevice.isFormatSupported(format); + const bool outputSupport = m_audioOutputDevice.isFormatSupported(format); + ENGINE_DEBUG << "Engine::initialize checking " << format + << "input" << inputSupport + << "output" << outputSupport; + if (inputSupport && outputSupport) { + foundSupportedFormat = true; + break; + } + } + } + + if (!foundSupportedFormat) + format = QAudioFormat(); + + setFormat(format); + } + + return foundSupportedFormat; +} + +void Engine::stopRecording() +{ + if (m_audioInput) { + m_audioInput->stop(); + QCoreApplication::instance()->processEvents(); + m_audioInput->disconnect(); + } + m_audioInputIODevice = 0; +} + +void Engine::stopPlayback() +{ + if (m_audioOutput) { + m_audioOutput->stop(); + QCoreApplication::instance()->processEvents(); + m_audioOutput->disconnect(); + setPlayPosition(0); + } +} + +void Engine::setState(QAudio::State state) +{ + const bool changed = (m_state != state); + m_state = state; + if (changed) + emit stateChanged(m_mode, m_state); +} + +void Engine::setState(QAudio::Mode mode, QAudio::State state) +{ + const bool changed = (m_mode != mode || m_state != state); + m_mode = mode; + m_state = state; + if (changed) + emit stateChanged(m_mode, m_state); +} + +void Engine::setRecordPosition(qint64 position, bool forceEmit) +{ + const bool changed = (m_recordPosition != position); + m_recordPosition = position; + if (changed || forceEmit) + emit recordPositionChanged(m_recordPosition); +} + +void Engine::setPlayPosition(qint64 position, bool forceEmit) +{ + const bool changed = (m_playPosition != position); + m_playPosition = position; + if (changed || forceEmit) + emit playPositionChanged(m_playPosition); +} + +void Engine::calculateLevel(qint64 position, qint64 length) +{ +#ifdef DISABLE_LEVEL + Q_UNUSED(position) + Q_UNUSED(length) +#else + Q_ASSERT(position + length <= m_bufferPosition + m_dataLength); + + qreal peakLevel = 0.0; + + qreal sum = 0.0; + const char *ptr = m_buffer.constData() + position - m_bufferPosition; + const char *const end = ptr + length; + while (ptr < end) { + const qint16 value = *reinterpret_cast<const qint16*>(ptr); + const qreal fracValue = pcmToReal(value); + peakLevel = qMax(peakLevel, fracValue); + sum += fracValue * fracValue; + ptr += 2; + } + const int numSamples = length / 2; + qreal rmsLevel = sqrt(sum / numSamples); + + rmsLevel = qMax(qreal(0.0), rmsLevel); + rmsLevel = qMin(qreal(1.0), rmsLevel); + setLevel(rmsLevel, peakLevel, numSamples); + + ENGINE_DEBUG << "Engine::calculateLevel" << "pos" << position << "len" << length + << "rms" << rmsLevel << "peak" << peakLevel; +#endif +} + +void Engine::calculateSpectrum(qint64 position) +{ +#ifdef DISABLE_SPECTRUM + Q_UNUSED(position) +#else + Q_ASSERT(position + m_spectrumBufferLength <= m_bufferPosition + m_dataLength); + Q_ASSERT(0 == m_spectrumBufferLength % 2); // constraint of FFT algorithm + + // QThread::currentThread is marked 'for internal use only', but + // we're only using it for debug output here, so it's probably OK :) + ENGINE_DEBUG << "Engine::calculateSpectrum" << QThread::currentThread() + << "count" << m_count << "pos" << position << "len" << m_spectrumBufferLength + << "spectrumAnalyser.isReady" << m_spectrumAnalyser.isReady(); + + if (m_spectrumAnalyser.isReady()) { + m_spectrumBuffer = QByteArray::fromRawData(m_buffer.constData() + position - m_bufferPosition, + m_spectrumBufferLength); + m_spectrumPosition = position; + m_spectrumAnalyser.calculate(m_spectrumBuffer, m_format); + } +#endif +} + +void Engine::setFormat(const QAudioFormat &format) +{ + const bool changed = (format != m_format); + m_format = format; + m_levelBufferLength = audioLength(m_format, LevelWindowUs); + m_spectrumBufferLength = SpectrumLengthSamples * + (m_format.sampleSize() / 8) * m_format.channelCount(); + if (changed) + emit formatChanged(m_format); +} + +void Engine::setLevel(qreal rmsLevel, qreal peakLevel, int numSamples) +{ + m_rmsLevel = rmsLevel; + m_peakLevel = peakLevel; + emit levelChanged(m_rmsLevel, m_peakLevel, numSamples); +} diff --git a/tests/spectrum/spectrumapp/engine.h b/tests/spectrum/spectrumapp/engine.h new file mode 100644 index 00000000..cdd8373f --- /dev/null +++ b/tests/spectrum/spectrumapp/engine.h @@ -0,0 +1,247 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#ifndef ENGINE_H +#define ENGINE_H + +#include "spectrum.h" +#include "spectrumanalyser.h" +#include "wavfile.h" + +#include <QAudioDeviceInfo> +#include <QAudioFormat> +#include <QBuffer> +#include <QByteArray> +#include <QDir> +#include <QObject> +#include <QVector> + +class FrequencySpectrum; +QT_BEGIN_NAMESPACE +class QAudioInput; +class QAudioOutput; +QT_END_NAMESPACE + +/** + * This class interfaces with the QtMultimedia audio classes, and also with + * the SpectrumAnalyser class. Its role is to manage the capture and playback + * of audio data, meanwhile performing real-time analysis of the audio level + * and frequency spectrum. + */ +class Engine : public QObject +{ + Q_OBJECT + +public: + explicit Engine(QObject *parent = 0); + ~Engine(); + + const QList<QAudioDeviceInfo> &availableAudioInputDevices() const + { return m_availableAudioInputDevices; } + + const QList<QAudioDeviceInfo> &availableAudioOutputDevices() const + { return m_availableAudioOutputDevices; } + + QAudio::Mode mode() const { return m_mode; } + QAudio::State state() const { return m_state; } + + /** + * \return Current audio format + * \note May be QAudioFormat() if engine is not initialized + */ + const QAudioFormat& format() const { return m_format; } + + /** + * Stop any ongoing recording or playback, and reset to ground state. + */ + void reset(); + + /** + * Load data from WAV file + */ + bool loadFile(const QString &fileName); + + /** + * Position of the audio input device. + * \return Position in bytes. + */ + qint64 recordPosition() const { return m_recordPosition; } + + /** + * RMS level of the most recently processed set of audio samples. + * \return Level in range (0.0, 1.0) + */ + qreal rmsLevel() const { return m_rmsLevel; } + + /** + * Peak level of the most recently processed set of audio samples. + * \return Level in range (0.0, 1.0) + */ + qreal peakLevel() const { return m_peakLevel; } + + /** + * Position of the audio output device. + * \return Position in bytes. + */ + qint64 playPosition() const { return m_playPosition; } + + /** + * Length of the internal engine buffer. + * \return Buffer length in bytes. + */ + qint64 bufferLength() const; + + /** + * Amount of data held in the buffer. + * \return Data length in bytes. + */ + qint64 dataLength() const { return m_dataLength; } + + /** + * Set window function applied to audio data before spectral analysis. + */ + void setWindowFunction(WindowFunction type); + +public slots: + void startRecording(); + void startPlayback(); + void suspend(); + void setAudioInputDevice(const QAudioDeviceInfo &device); + void setAudioOutputDevice(const QAudioDeviceInfo &device); + +signals: + void stateChanged(QAudio::Mode mode, QAudio::State state); + + /** + * Format of audio data has changed + */ + void formatChanged(const QAudioFormat &format); + + /** + * Length of buffer has changed. + * \param duration Duration in microseconds + */ + void bufferLengthChanged(qint64 duration); + + /** + * Amount of data in buffer has changed. + * \param Length of data in bytes + */ + void dataLengthChanged(qint64 duration); + + /** + * Position of the audio input device has changed. + * \param position Position in bytes + */ + void recordPositionChanged(qint64 position); + + /** + * Position of the audio output device has changed. + * \param position Position in bytes + */ + void playPositionChanged(qint64 position); + + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + + /** + * Spectrum has changed. + * \param position Position of start of window in bytes + * \param length Length of window in bytes + * \param spectrum Resulting frequency spectrum + */ + void changedSpectrum(qint64 position, qint64 length, const FrequencySpectrum &spectrum); + + /** + * Buffer containing audio data has changed. + * \param position Position of start of buffer in bytes + * \param buffer Buffer + */ + void bufferChanged(qint64 position, qint64 length, const QByteArray &buffer); + +private slots: + void audioNotify(); + void audioStateChanged(QAudio::State state); + void audioDataReady(); + void spectrumChanged(const FrequencySpectrum &spectrum); + +private: + void resetAudioDevices(); + bool initialize(); + bool selectFormat(); + void stopRecording(); + void stopPlayback(); + void setState(QAudio::State state); + void setState(QAudio::Mode mode, QAudio::State state); + void setFormat(const QAudioFormat &format); + void setRecordPosition(qint64 position, bool forceEmit = false); + void setPlayPosition(qint64 position, bool forceEmit = false); + void calculateLevel(qint64 position, qint64 length); + void calculateSpectrum(qint64 position); + void setLevel(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + QAudio::Mode m_mode; + QAudio::State m_state; + + bool m_generateTone; + SweptTone m_tone; + + WavFile* m_file; + // We need a second file handle via which to read data into m_buffer + // for analysis + WavFile* m_analysisFile; + + QAudioFormat m_format; + + const QList<QAudioDeviceInfo> m_availableAudioInputDevices; + QAudioDeviceInfo m_audioInputDevice; + QAudioInput* m_audioInput; + QIODevice* m_audioInputIODevice; + qint64 m_recordPosition; + + const QList<QAudioDeviceInfo> m_availableAudioOutputDevices; + QAudioDeviceInfo m_audioOutputDevice; + QAudioOutput* m_audioOutput; + qint64 m_playPosition; + QBuffer m_audioOutputIODevice; + + QByteArray m_buffer; + qint64 m_bufferPosition; + qint64 m_bufferLength; + qint64 m_dataLength; + + int m_levelBufferLength; + qreal m_rmsLevel; + qreal m_peakLevel; + + int m_spectrumBufferLength; + QByteArray m_spectrumBuffer; + SpectrumAnalyser m_spectrumAnalyser; + qint64 m_spectrumPosition; + + int m_count; + +}; + +#endif // ENGINE_H diff --git a/tests/spectrum/spectrumapp/frequencyspectrum.cpp b/tests/spectrum/spectrumapp/frequencyspectrum.cpp new file mode 100644 index 00000000..013d0454 --- /dev/null +++ b/tests/spectrum/spectrumapp/frequencyspectrum.cpp @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#include "frequencyspectrum.h" + +FrequencySpectrum::FrequencySpectrum(int numPoints) + : m_elements(numPoints) +{ + +} + +void FrequencySpectrum::reset() +{ + iterator i = begin(); + for ( ; i != end(); ++i) + *i = Element(); +} + +int FrequencySpectrum::count() const +{ + return m_elements.count(); +} + +FrequencySpectrum::Element &FrequencySpectrum::operator[](int index) +{ + return m_elements[index]; +} + +const FrequencySpectrum::Element &FrequencySpectrum::operator[](int index) const +{ + return m_elements[index]; +} + +FrequencySpectrum::iterator FrequencySpectrum::begin() +{ + return m_elements.begin(); +} + +FrequencySpectrum::iterator FrequencySpectrum::end() +{ + return m_elements.end(); +} + +FrequencySpectrum::const_iterator FrequencySpectrum::begin() const +{ + return m_elements.begin(); +} + +FrequencySpectrum::const_iterator FrequencySpectrum::end() const +{ + return m_elements.end(); +} diff --git a/tests/spectrum/spectrumapp/frequencyspectrum.h b/tests/spectrum/spectrumapp/frequencyspectrum.h new file mode 100644 index 00000000..fac9a1b7 --- /dev/null +++ b/tests/spectrum/spectrumapp/frequencyspectrum.h @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#ifndef FREQUENCYSPECTRUM_H +#define FREQUENCYSPECTRUM_H + +#include <QtCore/QVector> + +/** + * Represents a frequency spectrum as a series of elements, each of which + * consists of a frequency, an amplitude and a phase. + */ +class FrequencySpectrum { +public: + FrequencySpectrum(int numPoints = 0); + + struct Element { + Element() + : frequency(0.0), amplitude(0.0), phase(0.0), clipped(false) + { } + + /** + * Frequency in Hertz + */ + qreal frequency; + + /** + * Amplitude in range [0.0, 1.0] + */ + qreal amplitude; + + /** + * Phase in range [0.0, 2*PI] + */ + qreal phase; + + /** + * Indicates whether value has been clipped during spectrum analysis + */ + bool clipped; + }; + + typedef QVector<Element>::iterator iterator; + typedef QVector<Element>::const_iterator const_iterator; + + void reset(); + + int count() const; + Element& operator[](int index); + const Element& operator[](int index) const; + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + +private: + QVector<Element> m_elements; + +}; + +#endif // FREQUENCYSPECTRUM_H diff --git a/tests/spectrum/spectrumapp/main.cpp b/tests/spectrum/spectrumapp/main.cpp new file mode 100644 index 00000000..32076b88 --- /dev/null +++ b/tests/spectrum/spectrumapp/main.cpp @@ -0,0 +1,201 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#include "engine.h" +#include "utils.h" + +#include <QtDataVis3D/q3dbars.h> +#include <QtDataVis3D/qbardataproxy.h> +#include <QtDataVis3D/qvalueaxis.h> + +#include <QGuiApplication> +#include <QAudio> +#include <QTimer> + +//#define USE_CONES + +using namespace QtDataVis3D; + +class MainApp : public QObject +{ +public: + MainApp(Q3DBars *window); + ~MainApp(); + + void start(QString fileName); + +public slots: + void spectrumChanged(qint64 position, qint64 length, const FrequencySpectrum &spectrum); + void stateChanged(QAudio::Mode mode, QAudio::State state); + +private slots: + void restart(); + +private: + int barIndex(qreal frequency) const; + +private: + Q3DBars *m_chart; + Engine *m_engine; + QTimer *m_restartTimer; + // Lower bound of first band in the spectrum in Hz + qreal m_lowFreq; + // Upper band of last band in the spectrum in Hz + qreal m_highFreq; +}; + +MainApp::MainApp(Q3DBars *window) + : m_chart(window), + m_engine(new Engine(this)), + m_restartTimer(new QTimer(this)), + m_lowFreq(SpectrumLowFreq), + m_highFreq(SpectrumHighFreq) +{ + m_chart->setDataWindow(SpectrumNumBands * 2, SpectrumNumBands); + // Disable grid + m_chart->setGridVisible(false); + // Disable auto-scaling of height by defining explicit range + m_chart->valueAxis()->setRange(0.0, 1.0); + // Disable shadows + m_chart->setShadowQuality(QDataVis::ShadowNone); +#if USE_CONES + // Set bar specifications; make them a bit wider than deep and make them be drawn 75% + // inside each other + m_chart->setBarSpecs(1.25), QSizeF(0.2, -0.75)); + // Set bar type, smooth cones + m_chart->setBarType(QDataVis::Cones, true); + // Adjust zoom manually; automatic zoom level calculation does not work well with negative + // spacings (in setBarSpecs) + m_chart->setCameraPosition(10.0f, 5.0f, 70); +#else + // Set bar specifications; make them twice as wide as they're deep + m_chart->setBarSpecs(2.0, QSizeF(0.0, 0.0)); + // Set bar type, flat bars + m_chart->setBarType(QDataVis::Bars, false); + // Adjust camera position + m_chart->setCameraPosition(10.0f, 7.5f, 75); +#endif + // Set color scheme + m_chart->setBarColor(QColor(Qt::black), QColor(Qt::red), QColor(Qt::darkYellow)); + // Disable selection + m_chart->setSelectionMode(QDataVis::ModeNone); + QObject::connect(m_engine, &Engine::changedSpectrum, this, &MainApp::spectrumChanged); + QObject::connect(m_engine, &Engine::stateChanged, this, &MainApp::stateChanged); + m_restartTimer->setSingleShot(true); + QObject::connect(m_restartTimer, &QTimer::timeout, this, &MainApp::restart); + + QBarDataProxy *proxy = new QBarDataProxy; + m_chart->setActiveDataProxy(proxy); +} + +MainApp::~MainApp() +{ + delete m_engine; + delete m_restartTimer; +} + +void MainApp::start(QString fileName) +{ + m_engine->loadFile(fileName); + m_engine->startPlayback(); +} + +//----------------------------------------------------------------------------- +// Public slots +//----------------------------------------------------------------------------- + +void MainApp::spectrumChanged(qint64 position, qint64 length, const FrequencySpectrum &spectrum) +{ + Q_UNUSED(position); + Q_UNUSED(length); + //qDebug() << "updating bar values" << position << length; + QBarDataRow *data = new QBarDataRow(SpectrumNumBands); + for (int bar = 0; bar < SpectrumNumBands; bar++) { + // init data set + (*data)[bar].setValue(qreal(0.0)); + } + FrequencySpectrum::const_iterator i = spectrum.begin(); + const FrequencySpectrum::const_iterator end = spectrum.end(); + for ( ; i != end; ++i) { + const FrequencySpectrum::Element e = *i; + if (e.frequency >= m_lowFreq && e.frequency < m_highFreq) { + (*data)[barIndex(e.frequency)].setValue(qMax(data->at(barIndex(e.frequency)).value(), qreal(e.amplitude))); + } + } + static_cast<QBarDataProxy *>(m_chart->activeDataProxy())->insertRow(0, data); +} + +void MainApp::stateChanged(QAudio::Mode mode, QAudio::State state) +{ + //qDebug() << "mode:" << mode << " state: " << state; + // Restart once playback is finished + if (QAudio::AudioOutput == mode && QAudio::StoppedState == state) + m_restartTimer->start(500); +} + +//----------------------------------------------------------------------------- +// Private slots +//----------------------------------------------------------------------------- + +void MainApp::restart() +{ + // Change file each time + QString fileToLoad = QStringLiteral(":/file"); + static int fileNo = 3; + QString nrStr; + nrStr.setNum(fileNo); + fileToLoad.append(nrStr); + //qDebug() << fileToLoad; + start(fileToLoad); + fileNo++; + if (fileNo > 3) + fileNo = 1; +} + +//----------------------------------------------------------------------------- +// Private functions +//----------------------------------------------------------------------------- + +int MainApp::barIndex(qreal frequency) const +{ + Q_ASSERT(frequency >= m_lowFreq && frequency < m_highFreq); + const qreal bandWidth = (m_highFreq - m_lowFreq) / SpectrumNumBands; + const int index = (frequency - m_lowFreq) / bandWidth; + if (index < 0 || index >= SpectrumNumBands) + Q_ASSERT(false); + //qDebug() << "insert to" << index; + return index; +} + +//----------------------------------------------------------------------------- +// main +//----------------------------------------------------------------------------- +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + app.setApplicationName("QtDataVis3D spectrum analyzer"); + + Q3DBars window; + window.resize(1024, 768); + window.show(); + + MainApp *mainApp = new MainApp(&window); + mainApp->start(QStringLiteral(":/file2")); + + return app.exec(); +} diff --git a/tests/spectrum/spectrumapp/soundFiles/Rockhop.wav b/tests/spectrum/spectrumapp/soundFiles/Rockhop.wav Binary files differnew file mode 100644 index 00000000..e56e1c0f --- /dev/null +++ b/tests/spectrum/spectrumapp/soundFiles/Rockhop.wav diff --git a/tests/spectrum/spectrumapp/soundFiles/futurebells_beat.wav b/tests/spectrum/spectrumapp/soundFiles/futurebells_beat.wav Binary files differnew file mode 100644 index 00000000..c45cbc71 --- /dev/null +++ b/tests/spectrum/spectrumapp/soundFiles/futurebells_beat.wav diff --git a/tests/spectrum/spectrumapp/soundFiles/onclassical_demo_fiati-di-parma_thuille_terzo-tempo_sestetto_small-version.wav b/tests/spectrum/spectrumapp/soundFiles/onclassical_demo_fiati-di-parma_thuille_terzo-tempo_sestetto_small-version.wav Binary files differnew file mode 100644 index 00000000..78b8dbda --- /dev/null +++ b/tests/spectrum/spectrumapp/soundFiles/onclassical_demo_fiati-di-parma_thuille_terzo-tempo_sestetto_small-version.wav diff --git a/tests/spectrum/spectrumapp/spectrum.h b/tests/spectrum/spectrumapp/spectrum.h new file mode 100644 index 00000000..015989d5 --- /dev/null +++ b/tests/spectrum/spectrumapp/spectrum.h @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#ifndef SPECTRUM_H +#define SPECTRUM_H + +#include "utils.h" +#include "fftreal_wrapper.h" // For FFTLengthPowerOfTwo +#include <qglobal.h> + +//----------------------------------------------------------------------------- +// Constants +//----------------------------------------------------------------------------- + +// Number of audio samples used to calculate the frequency spectrum +const int SpectrumLengthSamples = PowerOfTwo<FFTLengthPowerOfTwo>::Result; + +// Number of bands in the frequency spectrum +const int SpectrumNumBands = 30; + +// Lower bound of first band in the spectrum +const qreal SpectrumLowFreq = 0.0; // Hz + +// Upper band of last band in the spectrum +const qreal SpectrumHighFreq = 1000.0; // Hz + +// Waveform window size in microseconds +const qint64 WaveformWindowDuration = 500 * 1000; + +// Fudge factor used to calculate the spectrum bar heights +const qreal SpectrumAnalyserMultiplier = 0.15; + +// Disable message timeout +const int NullMessageTimeout = -1; + + +//----------------------------------------------------------------------------- +// Types and data structures +//----------------------------------------------------------------------------- + +enum WindowFunction { + NoWindow, + HannWindow +}; + +const WindowFunction DefaultWindowFunction = HannWindow; + +struct Tone +{ + Tone(qreal freq = 0.0, qreal amp = 0.0) + : frequency(freq), amplitude(amp) + { } + + // Start and end frequencies for swept tone generation + qreal frequency; + + // Amplitude in range [0.0, 1.0] + qreal amplitude; +}; + +struct SweptTone +{ + SweptTone(qreal start = 0.0, qreal end = 0.0, qreal amp = 0.0) + : startFreq(start), endFreq(end), amplitude(amp) + { Q_ASSERT(end >= start); } + + SweptTone(const Tone &tone) + : startFreq(tone.frequency), endFreq(tone.frequency), amplitude(tone.amplitude) + { } + + // Start and end frequencies for swept tone generation + qreal startFreq; + qreal endFreq; + + // Amplitude in range [0.0, 1.0] + qreal amplitude; +}; + + +//----------------------------------------------------------------------------- +// Macros +//----------------------------------------------------------------------------- + +// Macro which connects a signal to a slot, and which causes application to +// abort if the connection fails. This is intended to catch programming errors +// such as mis-typing a signal or slot name. It is necessary to write our own +// macro to do this - the following idiom +// Q_ASSERT(connect(source, signal, receiver, slot)); +// will not work because Q_ASSERT compiles to a no-op in release builds. + +#define CHECKED_CONNECT(source, signal, receiver, slot) \ + if (!connect(source, signal, receiver, slot)) \ + qt_assert_x(Q_FUNC_INFO, "CHECKED_CONNECT failed", __FILE__, __LINE__); + +#endif // SPECTRUM_H + diff --git a/tests/spectrum/spectrumapp/spectrum.qrc b/tests/spectrum/spectrumapp/spectrum.qrc new file mode 100644 index 00000000..9368abc7 --- /dev/null +++ b/tests/spectrum/spectrumapp/spectrum.qrc @@ -0,0 +1,7 @@ +<RCC> + <qresource prefix="/"> + <file alias="file1">soundFiles/onclassical_demo_fiati-di-parma_thuille_terzo-tempo_sestetto_small-version.wav</file> + <file alias="file2">soundFiles/Rockhop.wav</file> + <file alias="file3">soundFiles/futurebells_beat.wav</file> + </qresource> +</RCC> diff --git a/tests/spectrum/spectrumapp/spectrumanalyser.cpp b/tests/spectrum/spectrumapp/spectrumanalyser.cpp new file mode 100644 index 00000000..4cebfde9 --- /dev/null +++ b/tests/spectrum/spectrumapp/spectrumanalyser.cpp @@ -0,0 +1,209 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#include "spectrumanalyser.h" +#include "utils.h" +#include "fftreal_wrapper.h" + +#include <qmath.h> +#include <qmetatype.h> +#include <QAudioFormat> +#include <QThread> + +SpectrumAnalyserThread::SpectrumAnalyserThread(QObject *parent) + : QObject(parent), + m_fft(new FFTRealWrapper), + m_numSamples(SpectrumLengthSamples), + m_windowFunction(DefaultWindowFunction), + m_window(SpectrumLengthSamples, 0.0), + m_input(SpectrumLengthSamples, 0.0), + m_output(SpectrumLengthSamples, 0.0), + m_spectrum(SpectrumLengthSamples) + #ifdef SPECTRUM_ANALYSER_SEPARATE_THREAD + , m_thread(new QThread(this)) + #endif +{ +#ifdef SPECTRUM_ANALYSER_SEPARATE_THREAD + // moveToThread() cannot be called on a QObject with a parent + setParent(0); + moveToThread(m_thread); + m_thread->start(); +#endif + calculateWindow(); +} + +SpectrumAnalyserThread::~SpectrumAnalyserThread() +{ + delete m_fft; +} + +void SpectrumAnalyserThread::setWindowFunction(WindowFunction type) +{ + m_windowFunction = type; + calculateWindow(); +} + +void SpectrumAnalyserThread::calculateWindow() +{ + for (int i=0; i<m_numSamples; ++i) { + DataType x = 0.0; + + switch (m_windowFunction) { + case NoWindow: + x = 1.0; + break; + case HannWindow: + x = 0.5 * (1 - qCos((2 * M_PI * i) / (m_numSamples - 1))); + break; + default: + Q_ASSERT(false); + } + + m_window[i] = x; + } +} + +void SpectrumAnalyserThread::calculateSpectrum(const QByteArray &buffer, + int inputFrequency, + int bytesPerSample) +{ + Q_ASSERT(buffer.size() == m_numSamples * bytesPerSample); + + // Initialize data array + const char *ptr = buffer.constData(); + for (int i=0; i<m_numSamples; ++i) { + const qint16 pcmSample = *reinterpret_cast<const qint16*>(ptr); + // Scale down to range [-1.0, 1.0] + const DataType realSample = pcmToReal(pcmSample); + const DataType windowedSample = realSample * m_window[i]; + m_input[i] = windowedSample; + ptr += bytesPerSample; + } + + // Calculate the FFT + m_fft->calculateFFT(m_output.data(), m_input.data()); + + // Analyze output to obtain amplitude and phase for each frequency + for (int i=2; i<=m_numSamples/2; ++i) { + // Calculate frequency of this complex sample + m_spectrum[i].frequency = qreal(i * inputFrequency) / (m_numSamples); + + const qreal real = m_output[i]; + qreal imag = 0.0; + if (i>0 && i<m_numSamples/2) + imag = m_output[m_numSamples/2 + i]; + + const qreal magnitude = sqrt(real*real + imag*imag); + qreal amplitude = SpectrumAnalyserMultiplier * log(magnitude); + + // Bound amplitude to [0.0, 1.0] + m_spectrum[i].clipped = (amplitude > 1.0); + amplitude = qMax(qreal(0.0), amplitude); + amplitude = qMin(qreal(1.0), amplitude); + m_spectrum[i].amplitude = amplitude; + } + + emit calculationComplete(m_spectrum); +} + + +//============================================================================= +// SpectrumAnalyser +//============================================================================= + +SpectrumAnalyser::SpectrumAnalyser(QObject *parent) + : QObject(parent), + m_thread(new SpectrumAnalyserThread(this)), + m_state(Idle) +{ + CHECKED_CONNECT(m_thread, SIGNAL(calculationComplete(FrequencySpectrum)), + this, SLOT(calculationComplete(FrequencySpectrum))); +} + +SpectrumAnalyser::~SpectrumAnalyser() +{ + +} + +//----------------------------------------------------------------------------- +// Public functions +//----------------------------------------------------------------------------- + +void SpectrumAnalyser::setWindowFunction(WindowFunction type) +{ + const bool b = QMetaObject::invokeMethod(m_thread, "setWindowFunction", + Qt::AutoConnection, + Q_ARG(WindowFunction, type)); + Q_ASSERT(b); + Q_UNUSED(b) // suppress warnings in release builds +} + +void SpectrumAnalyser::calculate(const QByteArray &buffer, + const QAudioFormat &format) +{ + // QThread::currentThread is marked 'for internal use only', but + // we're only using it for debug output here, so it's probably OK :) + SPECTRUMANALYSER_DEBUG << "SpectrumAnalyser::calculate" + << QThread::currentThread() + << "state" << m_state; + + if (isReady()) { + Q_ASSERT(isPCMS16LE(format)); + + const int bytesPerSample = format.sampleSize() * format.channelCount() / 8; + + m_state = Busy; + + // Invoke SpectrumAnalyserThread::calculateSpectrum using QMetaObject. If + // m_thread is in a different thread from the current thread, the + // calculation will be done in the child thread. + // Once the calculation is finished, a calculationChanged signal will be + // emitted by m_thread. + const bool b = QMetaObject::invokeMethod(m_thread, "calculateSpectrum", + Qt::AutoConnection, + Q_ARG(QByteArray, buffer), + Q_ARG(int, format.sampleRate()), + Q_ARG(int, bytesPerSample)); + Q_ASSERT(b); + Q_UNUSED(b) // suppress warnings in release builds + } +} + +bool SpectrumAnalyser::isReady() const +{ + return (Idle == m_state); +} + +void SpectrumAnalyser::cancelCalculation() +{ + if (Busy == m_state) + m_state = Cancelled; +} + + +//----------------------------------------------------------------------------- +// Private slots +//----------------------------------------------------------------------------- + +void SpectrumAnalyser::calculationComplete(const FrequencySpectrum &spectrum) +{ + Q_ASSERT(Idle != m_state); + if (Busy == m_state) + emit spectrumChanged(spectrum); + m_state = Idle; +} diff --git a/tests/spectrum/spectrumapp/spectrumanalyser.h b/tests/spectrum/spectrumapp/spectrumanalyser.h new file mode 100644 index 00000000..6d9291ef --- /dev/null +++ b/tests/spectrum/spectrumapp/spectrumanalyser.h @@ -0,0 +1,150 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#ifndef SPECTRUMANALYSER_H +#define SPECTRUMANALYSER_H + +#include <QByteArray> +#include <QObject> +#include <QVector> + +#include "frequencyspectrum.h" +#include "spectrum.h" + +#include "FFTRealFixLenParam.h" + +QT_FORWARD_DECLARE_CLASS(QAudioFormat) +QT_FORWARD_DECLARE_CLASS(QThread) + +class FFTRealWrapper; + +class SpectrumAnalyserThreadPrivate; + +/** + * Implementation of the spectrum analysis which can be run in a + * separate thread. + */ +class SpectrumAnalyserThread : public QObject +{ + Q_OBJECT + +public: + SpectrumAnalyserThread(QObject *parent); + ~SpectrumAnalyserThread(); + +public slots: + void setWindowFunction(WindowFunction type); + void calculateSpectrum(const QByteArray &buffer, + int inputFrequency, + int bytesPerSample); + +signals: + void calculationComplete(const FrequencySpectrum &spectrum); + +private: + void calculateWindow(); + +private: + FFTRealWrapper* m_fft; + + const int m_numSamples; + + WindowFunction m_windowFunction; + + typedef FFTRealFixLenParam::DataType DataType; + + QVector<DataType> m_window; + + QVector<DataType> m_input; + QVector<DataType> m_output; + + FrequencySpectrum m_spectrum; + +#ifdef SPECTRUM_ANALYSER_SEPARATE_THREAD + QThread* m_thread; +#endif +}; + +/** + * Class which performs frequency spectrum analysis on a window of + * audio samples, provided to it by the Engine. + */ +class SpectrumAnalyser : public QObject +{ + Q_OBJECT + +public: + SpectrumAnalyser(QObject *parent = 0); + ~SpectrumAnalyser(); + +public: + /* + * Set the windowing function which is applied before calculating the FFT + */ + void setWindowFunction(WindowFunction type); + + /* + * Calculate a frequency spectrum + * + * \param buffer Audio data + * \param format Format of audio data + * + * Frequency spectrum is calculated asynchronously. The result is returned + * via the spectrumChanged signal. + * + * An ongoing calculation can be cancelled by calling cancelCalculation(). + * + */ + void calculate(const QByteArray &buffer, const QAudioFormat &format); + + /* + * Check whether the object is ready to perform another calculation + */ + bool isReady() const; + + /* + * Cancel an ongoing calculation + * + * Note that cancelling is asynchronous. + */ + void cancelCalculation(); + +signals: + void spectrumChanged(const FrequencySpectrum &spectrum); + +private slots: + void calculationComplete(const FrequencySpectrum &spectrum); + +private: + void calculateWindow(); + +private: + + SpectrumAnalyserThread* m_thread; + + enum State { + Idle, + Busy, + Cancelled + }; + + State m_state; +}; + +#endif // SPECTRUMANALYSER_H + diff --git a/tests/spectrum/spectrumapp/spectrumapp.pro b/tests/spectrum/spectrumapp/spectrumapp.pro new file mode 100644 index 00000000..0fc0584c --- /dev/null +++ b/tests/spectrum/spectrumapp/spectrumapp.pro @@ -0,0 +1,80 @@ +!include( ../../tests.pri ) { + error( "Couldn't find the tests.pri file!" ) +} + +!include( ../spectrum.pri ) { + error( "Couldn't find the spectrum.pri file!" ) +} + +static: error(This application cannot be statically linked to the fftreal library) + +TEMPLATE = app + +TARGET = spectrum + +QT += multimedia + +SOURCES += main.cpp \ + engine.cpp \ + frequencyspectrum.cpp \ + spectrumanalyser.cpp \ + utils.cpp \ + wavfile.cpp + +HEADERS += engine.h \ + frequencyspectrum.h \ + spectrum.h \ + spectrumanalyser.h \ + utils.h \ + wavfile.h + +fftreal_dir = ../3rdparty/fftreal + +INCLUDEPATH += $${fftreal_dir} + +RESOURCES = spectrum.qrc + +# Dynamic linkage against FFTReal DLL +!contains(DEFINES, DISABLE_FFT) { + macx { + # Link to fftreal framework + LIBS += -F$${fftreal_dir} + LIBS += -framework fftreal + } else { + LIBS += -L..$${spectrum_build_dir} + LIBS += -lfftreal + } +} + + +android { + target.path = /libs/$$ANDROID_TARGET_ARCH +} else { + target.path = $$[QT_INSTALL_EXAMPLES]/datavis3d/spectrum +} +INSTALLS += target + +# Deployment + +DESTDIR = ..$${spectrum_build_dir} +macx { + !contains(DEFINES, DISABLE_FFT) { + # Relocate fftreal.framework into spectrum.app bundle + framework_dir = ../spectrum.app/Contents/Frameworks + framework_name = fftreal.framework/Versions/1/fftreal + QMAKE_POST_LINK = \ + mkdir -p $${framework_dir} &&\ + rm -rf $${framework_dir}/fftreal.framework &&\ + cp -R $${fftreal_dir}/fftreal.framework $${framework_dir} &&\ + install_name_tool -id @executable_path/../Frameworks/$${framework_name} \ + $${framework_dir}/$${framework_name} &&\ + install_name_tool -change $${framework_name} \ + @executable_path/../Frameworks/$${framework_name} \ + ../spectrum.app/Contents/MacOS/spectrum + } +} else { + linux-g++*: { + # Provide relative path from application to fftreal library + QMAKE_LFLAGS += -Wl,--rpath=\\\$\$ORIGIN + } +} diff --git a/tests/spectrum/spectrumapp/utils.cpp b/tests/spectrum/spectrumapp/utils.cpp new file mode 100644 index 00000000..bad6cc48 --- /dev/null +++ b/tests/spectrum/spectrumapp/utils.cpp @@ -0,0 +1,117 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#include <QAudioFormat> +#include "utils.h" + +qint64 audioDuration(const QAudioFormat &format, qint64 bytes) +{ + return (bytes * 1000000) / + (format.sampleRate() * format.channelCount() * (format.sampleSize() / 8)); +} + +qint64 audioLength(const QAudioFormat &format, qint64 microSeconds) +{ + qint64 result = (format.sampleRate() * format.channelCount() * (format.sampleSize() / 8)) + * microSeconds / 1000000; + result -= result % (format.channelCount() * format.sampleSize()); + return result; +} + +qreal nyquistFrequency(const QAudioFormat &format) +{ + return format.sampleRate() / 2; +} + +QString formatToString(const QAudioFormat &format) +{ + QString result; + + if (QAudioFormat() != format) { + if (format.codec() == "audio/pcm") { + Q_ASSERT(format.sampleType() == QAudioFormat::SignedInt); + + const QString formatEndian = (format.byteOrder() == QAudioFormat::LittleEndian) + ? QString("LE") : QString("BE"); + + QString formatType; + switch (format.sampleType()) { + case QAudioFormat::SignedInt: + formatType = "signed"; + break; + case QAudioFormat::UnSignedInt: + formatType = "unsigned"; + break; + case QAudioFormat::Float: + formatType = "float"; + break; + case QAudioFormat::Unknown: + formatType = "unknown"; + break; + } + + QString formatChannels = QString("%1 channels").arg(format.channelCount()); + switch (format.channelCount()) { + case 1: + formatChannels = "mono"; + break; + case 2: + formatChannels = "stereo"; + break; + } + + result = QString("%1 Hz %2 bit %3 %4 %5") + .arg(format.sampleRate()) + .arg(format.sampleSize()) + .arg(formatType) + .arg(formatEndian) + .arg(formatChannels); + } else { + result = format.codec(); + } + } + + return result; +} + +bool isPCM(const QAudioFormat &format) +{ + return (format.codec() == "audio/pcm"); +} + + +bool isPCMS16LE(const QAudioFormat &format) +{ + return isPCM(format) && + format.sampleType() == QAudioFormat::SignedInt && + format.sampleSize() == 16 && + format.byteOrder() == QAudioFormat::LittleEndian; +} + +const qint16 PCMS16MaxValue = 32767; +const quint16 PCMS16MaxAmplitude = 32768; // because minimum is -32768 + +qreal pcmToReal(qint16 pcm) +{ + return qreal(pcm) / PCMS16MaxAmplitude; +} + +qint16 realToPcm(qreal real) +{ + return real * PCMS16MaxValue; +} diff --git a/tests/spectrum/spectrumapp/utils.h b/tests/spectrum/spectrumapp/utils.h new file mode 100644 index 00000000..f0ae5633 --- /dev/null +++ b/tests/spectrum/spectrumapp/utils.h @@ -0,0 +1,90 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#ifndef UTILS_H +#define UTILS_H + +#include <QtCore/qglobal.h> +#include <QDebug> + +QT_FORWARD_DECLARE_CLASS(QAudioFormat) + +//----------------------------------------------------------------------------- +// Miscellaneous utility functions +//----------------------------------------------------------------------------- + +qint64 audioDuration(const QAudioFormat &format, qint64 bytes); +qint64 audioLength(const QAudioFormat &format, qint64 microSeconds); + +QString formatToString(const QAudioFormat &format); + +qreal nyquistFrequency(const QAudioFormat &format); + +// Scale PCM value to [-1.0, 1.0] +qreal pcmToReal(qint16 pcm); + +// Scale real value in [-1.0, 1.0] to PCM +qint16 realToPcm(qreal real); + +// Check whether the audio format is PCM +bool isPCM(const QAudioFormat &format); + +// Check whether the audio format is signed, little-endian, 16-bit PCM +bool isPCMS16LE(const QAudioFormat &format); + +// Compile-time calculation of powers of two + +template<int N> class PowerOfTwo +{ public: static const int Result = PowerOfTwo<N-1>::Result * 2; }; + +template<> class PowerOfTwo<0> +{ public: static const int Result = 1; }; + + +//----------------------------------------------------------------------------- +// Debug output +//----------------------------------------------------------------------------- + +class NullDebug +{ +public: + template <typename T> + NullDebug& operator<<(const T&) { return *this; } +}; + +inline NullDebug nullDebug() { return NullDebug(); } + +#ifdef LOG_ENGINE +# define ENGINE_DEBUG qDebug() +#else +# define ENGINE_DEBUG nullDebug() +#endif + +#ifdef LOG_SPECTRUMANALYSER +# define SPECTRUMANALYSER_DEBUG qDebug() +#else +# define SPECTRUMANALYSER_DEBUG nullDebug() +#endif + +#ifdef LOG_WAVEFORM +# define WAVEFORM_DEBUG qDebug() +#else +# define WAVEFORM_DEBUG nullDebug() +#endif + +#endif // UTILS_H diff --git a/tests/spectrum/spectrumapp/wavfile.cpp b/tests/spectrum/spectrumapp/wavfile.cpp new file mode 100644 index 00000000..24482507 --- /dev/null +++ b/tests/spectrum/spectrumapp/wavfile.cpp @@ -0,0 +1,129 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#include <qendian.h> +#include <QVector> +#include <QDebug> +#include "utils.h" +#include "wavfile.h" + +struct chunk +{ + char id[4]; + quint32 size; +}; + +struct RIFFHeader +{ + chunk descriptor; // "RIFF" + char type[4]; // "WAVE" +}; + +struct WAVEHeader +{ + chunk descriptor; + quint16 audioFormat; + quint16 numChannels; + quint32 sampleRate; + quint32 byteRate; + quint16 blockAlign; + quint16 bitsPerSample; +}; + +struct DATAHeader +{ + chunk descriptor; +}; + +struct CombinedHeader +{ + RIFFHeader riff; + WAVEHeader wave; +}; + +WavFile::WavFile(QObject *parent) + : QFile(parent), + m_headerLength(0) +{ + +} + +bool WavFile::open(const QString &fileName) +{ + close(); + setFileName(fileName); + return QFile::open(QIODevice::ReadOnly) && readHeader(); +} + +const QAudioFormat &WavFile::fileFormat() const +{ + return m_fileFormat; +} + +qint64 WavFile::headerLength() const +{ + return m_headerLength; +} + +bool WavFile::readHeader() +{ + seek(0); + CombinedHeader header; + bool result = read(reinterpret_cast<char *>(&header), sizeof(CombinedHeader)) == sizeof(CombinedHeader); + if (result) { + if ((memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0 + || memcmp(&header.riff.descriptor.id, "RIFX", 4) == 0) + && memcmp(&header.riff.type, "WAVE", 4) == 0 + && memcmp(&header.wave.descriptor.id, "fmt ", 4) == 0 + && (header.wave.audioFormat == 1 || header.wave.audioFormat == 0)) { + + // Read off remaining header information + DATAHeader dataHeader; + + if (qFromLittleEndian<quint32>(header.wave.descriptor.size) > sizeof(WAVEHeader)) { + // Extended data available + quint16 extraFormatBytes; + if (peek((char*)&extraFormatBytes, sizeof(quint16)) != sizeof(quint16)) + return false; + const qint64 throwAwayBytes = sizeof(quint16) + qFromLittleEndian<quint16>(extraFormatBytes); + if (read(throwAwayBytes).size() != throwAwayBytes) + return false; + } + + if (read((char*)&dataHeader, sizeof(DATAHeader)) != sizeof(DATAHeader)) + return false; + + // Establish format + if (memcmp(&header.riff.descriptor.id, "RIFF", 4) == 0) + m_fileFormat.setByteOrder(QAudioFormat::LittleEndian); + else + m_fileFormat.setByteOrder(QAudioFormat::BigEndian); + + int bps = qFromLittleEndian<quint16>(header.wave.bitsPerSample); + m_fileFormat.setChannelCount(qFromLittleEndian<quint16>(header.wave.numChannels)); + m_fileFormat.setCodec("audio/pcm"); + m_fileFormat.setSampleRate(qFromLittleEndian<quint32>(header.wave.sampleRate)); + m_fileFormat.setSampleSize(qFromLittleEndian<quint16>(header.wave.bitsPerSample)); + m_fileFormat.setSampleType(bps == 8 ? QAudioFormat::UnSignedInt : QAudioFormat::SignedInt); + } else { + result = false; + } + } + m_headerLength = pos(); + return result; +} diff --git a/tests/spectrum/spectrumapp/wavfile.h b/tests/spectrum/spectrumapp/wavfile.h new file mode 100644 index 00000000..e408911b --- /dev/null +++ b/tests/spectrum/spectrumapp/wavfile.h @@ -0,0 +1,44 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the QtDataVis3D module. +** +** Licensees holding valid Qt Enterprise licenses may use this file in +** accordance with the Qt Enterprise License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** +****************************************************************************/ + +#ifndef WAVFILE_H +#define WAVFILE_H + +#include <QObject> +#include <QFile> +#include <QAudioFormat> + +class WavFile : public QFile +{ +public: + WavFile(QObject *parent = 0); + + using QFile::open; + bool open(const QString &fileName); + const QAudioFormat &fileFormat() const; + qint64 headerLength() const; + +private: + bool readHeader(); + +private: + QAudioFormat m_fileFormat; + qint64 m_headerLength; +}; + +#endif // WAVFILE_H |