/****************************************************************************
**
** 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 <math.h>
#include <QCoreApplication>
#include <QMetaObject>
#include <QSet>
#include <QtMultimedia/QAudioInput>
#include <QtMultimedia/QAudioOutput>
#include <QDebug>
#include <QThread>
#include <QFile>
//-----------------------------------------------------------------------------
// 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>("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<int> 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<int> 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<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_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<const qint16*>(m_buffer.constData());
const int numSamples = m_dataLength / (2 * m_format.channels());
for (int i=0; i<numSamples; ++i) {
stream << i << "\t" << *ptr << "\n";
ptr += m_format.channels();
}
const QString pcmFileName = m_outputDir.filePath("data.pcm");
QFile pcmFile(pcmFileName);
pcmFile.open(QFile::WriteOnly);
pcmFile.write(m_buffer.constData(), m_dataLength);
}
#endif // DUMP_AUDIO