/**************************************************************************** ** ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** No Commercial Usage ** This file contains pre-release code and may not be distributed. ** You may use this file in accordance with the terms and conditions ** contained in the Technology Preview License Agreement accompanying ** this package. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** ** ** ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "engine.h" #include "tonegenerator.h" #include "utils.h" #include #include #include #include #include #include #include #include #include //----------------------------------------------------------------------------- // Constants //----------------------------------------------------------------------------- const qint64 BufferDurationUs = 10 * 1000000; const int NotifyIntervalMs = 100; // Size of the level calculation window in microseconds const int LevelWindowUs = 0.1 * 1000000; //----------------------------------------------------------------------------- // Helper functions //----------------------------------------------------------------------------- QDebug& operator<<(QDebug &debug, const QAudioFormat &format) { debug << format.frequency() << "Hz" << format.channels() << "channels"; return debug; } //----------------------------------------------------------------------------- // Constructor and destructor //----------------------------------------------------------------------------- Engine::Engine(QObject *parent) : QObject(parent) , m_mode(QAudio::AudioInput) , m_state(QAudio::StoppedState) , m_generateTone(false) , m_file(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_dataLength(0) , m_rmsLevel(0.0) , m_peakLevel(0.0) , m_spectrumLengthBytes(0) , m_spectrumAnalyser() , m_spectrumPosition(0) , m_count(0) { qRegisterMetaType("FrequencySpectrum"); CHECKED_CONNECT(&m_spectrumAnalyser, SIGNAL(spectrumChanged(FrequencySpectrum)), this, SLOT(spectrumChanged(FrequencySpectrum))); initialize(); #ifdef DUMP_DATA createOutputDir(); #endif #ifdef DUMP_SPECTRUM m_spectrumAnalyser.setOutputPath(outputPath()); #endif } Engine::~Engine() { } //----------------------------------------------------------------------------- // Public functions //----------------------------------------------------------------------------- bool Engine::loadFile(const QString &fileName) { bool result = false; m_generateTone = false; Q_ASSERT(!fileName.isEmpty()); Q_ASSERT(!m_file); m_file = new QFile(fileName, this); m_file->setFileName(fileName); Q_ASSERT(m_file->exists()); if (m_file->open(QFile::ReadOnly)) { m_wavFile.readHeader(*m_file); if (isPCMS16LE(m_wavFile.format())) { result = initialize(); } else { emit errorMessage(tr("Audio format not supported"), formatToString(m_wavFile.format())); } } else { emit errorMessage(tr("Could not open file"), fileName); } delete m_file; m_file = 0; return result; } bool Engine::generateTone(const Tone &tone) { Q_ASSERT(!m_file); m_generateTone = true; m_tone = tone; ENGINE_DEBUG << "Engine::generateTone" << "startFreq" << m_tone.startFreq << "endFreq" << m_tone.endFreq << "amp" << m_tone.amplitude; return initialize(); } bool Engine::generateSweptTone(qreal amplitude) { Q_ASSERT(!m_file); m_generateTone = true; m_tone.startFreq = 1; m_tone.endFreq = 0; m_tone.amplitude = amplitude; ENGINE_DEBUG << "Engine::generateSweptTone" << "startFreq" << m_tone.startFreq << "amp" << m_tone.amplitude; return initialize(); } bool Engine::initializeRecord() { ENGINE_DEBUG << "Engine::initializeRecord"; Q_ASSERT(!m_file); m_generateTone = false; m_tone = SweptTone(); return initialize(); } qint64 Engine::bufferDuration() const { return BufferDurationUs; } qint64 Engine::dataDuration() const { qint64 result = 0; if (QAudioFormat() != m_format) result = audioDuration(m_format, m_dataLength); return result; } qint64 Engine::audioBufferLength() const { qint64 length = 0; if (QAudio::ActiveState == m_state || QAudio::IdleState == m_state) { Q_ASSERT(QAudioFormat() != m_format); switch (m_mode) { case QAudio::AudioInput: length = m_audioInput->bufferSize(); break; case QAudio::AudioOutput: length = m_audioOutput->bufferSize(); break; } } return length; } 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(); spectrumChanged(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 dataDurationChanged(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(); spectrumChanged(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; 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(BufferDurationUs, m_audioInput->processedUSecs()); setRecordPosition(recordPosition); // Calculate level of most recently captured data qint64 levelLength = audioLength(m_format, LevelWindowUs); levelLength = qMin(m_dataLength, levelLength); const qint64 levelPosition = m_dataLength - levelLength; calculateLevel(levelPosition, levelLength); // Calculate spectrum of most recently captured data if (m_dataLength >= m_spectrumLengthBytes) { const qint64 spectrumPosition = m_dataLength - m_spectrumLengthBytes; calculateSpectrum(spectrumPosition); } } break; case QAudio::AudioOutput: { const qint64 playPosition = qMin(dataDuration(), m_audioOutput->processedUSecs()); setPlayPosition(playPosition); qint64 analysisPosition = audioLength(m_format, playPosition); // Calculate level of data starting at current playback position const qint64 levelLength = audioLength(m_format, LevelWindowUs); if (analysisPosition + levelLength < m_dataLength) calculateLevel(analysisPosition, levelLength); if (analysisPosition + m_spectrumLengthBytes < m_dataLength) calculateSpectrum(analysisPosition); if (dataDuration() == playPosition) stopPlayback(); } break; } } void Engine::audioStateChanged(QAudio::State state) { ENGINE_DEBUG << "Engine::audioStateChanged from" << m_state << "to" << state; 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() { 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; const qint64 duration = audioDuration(m_format, m_dataLength); emit dataDurationChanged(duration); } if (m_buffer.size() == m_dataLength) stopRecording(); } void Engine::spectrumChanged(const FrequencySpectrum &spectrum) { ENGINE_DEBUG << "Engine::spectrumChanged" << "pos" << m_spectrumPosition; const qint64 positionUs = audioDuration(m_format, m_spectrumPosition); const qint64 lengthUs = audioDuration(m_format, m_spectrumLengthBytes); emit spectrumChanged(positionUs, lengthUs, spectrum); } //----------------------------------------------------------------------------- // Private functions //----------------------------------------------------------------------------- void Engine::reset() { stopRecording(); stopPlayback(); setState(QAudio::AudioInput, QAudio::StoppedState); setFormat(QAudioFormat()); delete m_audioInput; m_audioInput = 0; m_audioInputIODevice = 0; setRecordPosition(0); delete m_audioOutput; m_audioOutput = 0; setPlayPosition(0); m_buffer.clear(); m_dataLength = 0; m_spectrumPosition = 0; emit dataDurationChanged(0); setLevel(0.0, 0.0, 0); } bool Engine::initialize() { bool result = false; reset(); if (selectFormat()) { const qint64 bufferLength = audioLength(m_format, BufferDurationUs); m_buffer.resize(bufferLength); m_buffer.fill(0); emit bufferDurationChanged(BufferDurationUs); if (m_generateTone) { if (0 == m_tone.endFreq) { const qreal nyquist = nyquistFrequency(m_format); m_tone.endFreq = qMin(qreal(SpectrumHighFreq), nyquist); } // Call function defined in utils.h, at global scope ::generateTone(m_tone, m_format, m_buffer); m_dataLength = m_buffer.size(); emit dataDurationChanged(bufferDuration()); setRecordPosition(bufferDuration()); result = true; } else if (m_file) { const qint64 length = m_wavFile.readData(*m_file, m_buffer, m_format); if (length) { m_dataLength = length; emit dataDurationChanged(dataDuration()); setRecordPosition(dataDuration()); result = true; } } else { m_audioInput = new QAudioInput(m_audioInputDevice, m_format, this); m_audioInput->setNotifyInterval(NotifyIntervalMs); result = true; } m_audioOutput = new QAudioOutput(m_audioOutputDevice, m_format, this); m_audioOutput->setNotifyInterval(NotifyIntervalMs); m_spectrumLengthBytes = SpectrumLengthSamples * (m_format.sampleSize() / 8) * m_format.channels(); } else { if (m_file) emit errorMessage(tr("Audio format not supported"), formatToString(m_format)); else if (m_generateTone) emit errorMessage(tr("No suitable format found"), ""); else emit errorMessage(tr("No common input / output format found"), ""); } ENGINE_DEBUG << "Engine::initialize" << "format" << m_format; return result; } bool Engine::selectFormat() { bool foundSupportedFormat = false; if (m_file) { // Header is read from the WAV file; just need to check whether // it is supported by the audio output device QAudioFormat format = m_wavFile.format(); if (m_audioOutputDevice.isFormatSupported(m_wavFile.format())) { setFormat(m_wavFile.format()); foundSupportedFormat = true; } else { // Try flipping mono <-> stereo const int channels = (format.channels() == 1) ? 2 : 1; format.setChannels(channels); if (m_audioOutputDevice.isFormatSupported(format)) { setFormat(format); foundSupportedFormat = true; } } } else { QList frequenciesList; #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. frequenciesList += 8000; #endif if (!m_generateTone) frequenciesList += m_audioInputDevice.supportedFrequencies(); frequenciesList += m_audioOutputDevice.supportedFrequencies(); frequenciesList = frequenciesList.toSet().toList(); // remove duplicates qSort(frequenciesList); ENGINE_DEBUG << "Engine::initialize frequenciesList" << frequenciesList; QList channelsList; channelsList += m_audioInputDevice.supportedChannels(); channelsList += m_audioOutputDevice.supportedChannels(); 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 frequency, channels; foreach (frequency, frequenciesList) { if (foundSupportedFormat) break; format.setFrequency(frequency); foreach (channels, channelsList) { format.setChannels(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; #ifdef DUMP_AUDIO dumpData(); #endif } 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_dataLength); qreal peakLevel = 0.0; qreal sum = 0.0; const char *ptr = m_buffer.constData() + position; const char *const end = ptr + length; while (ptr < end) { const qint16 value = *reinterpret_cast(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_spectrumLengthBytes <= m_dataLength); Q_ASSERT(0 == m_spectrumLengthBytes % 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_spectrumLengthBytes << "spectrumAnalyser.isReady" << m_spectrumAnalyser.isReady(); if(m_spectrumAnalyser.isReady()) { m_spectrumBuffer = QByteArray::fromRawData(m_buffer.constData() + position, m_spectrumLengthBytes); 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; 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); } #ifdef DUMP_DATA void Engine::createOutputDir() { m_outputDir.setPath("output"); // Ensure output directory exists and is empty if (m_outputDir.exists()) { const QStringList files = m_outputDir.entryList(QDir::Files); QString file; foreach (file, files) m_outputDir.remove(file); } else { QDir::current().mkdir("output"); } } #endif // DUMP_DATA #ifdef DUMP_AUDIO void Engine::dumpData() { const QString txtFileName = m_outputDir.filePath("data.txt"); QFile txtFile(txtFileName); txtFile.open(QFile::WriteOnly | QFile::Text); QTextStream stream(&txtFile); const qint16 *ptr = reinterpret_cast(m_buffer.constData()); const int numSamples = m_dataLength / (2 * m_format.channels()); for (int i=0; i