1 /****************************************************************************
3 ** Copyright (C) 2015 The Qt Company Ltd.
4 ** Contact: http://www.qt.io/licensing/
6 ** This file is part of the examples of the Qt Toolkit.
8 ** $QT_BEGIN_LICENSE:BSD$
9 ** You may use this file under the terms of the BSD license as follows:
11 ** "Redistribution and use in source and binary forms, with or without
12 ** modification, are permitted provided that the following conditions are
14 ** * Redistributions of source code must retain the above copyright
15 ** notice, this list of conditions and the following disclaimer.
16 ** * Redistributions in binary form must reproduce the above copyright
17 ** notice, this list of conditions and the following disclaimer in
18 ** the documentation and/or other materials provided with the
20 ** * Neither the name of The Qt Company Ltd nor the names of its
21 ** contributors may be used to endorse or promote products derived
22 ** from this software without specific prior written permission.
25 ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
26 ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
27 ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
28 ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
29 ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
30 ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
31 ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
32 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
33 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35 ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
39 ****************************************************************************/
42 #include "tonegenerator.h"
47 #include <QCoreApplication>
48 #include <QMetaObject>
50 #include <QtMultimedia/QAudioInput>
51 #include <QtMultimedia/QAudioOutput>
56 //-----------------------------------------------------------------------------
58 //-----------------------------------------------------------------------------
60 const qint64 BufferDurationUs = 10 * 1000000;
61 const int NotifyIntervalMs = 100;
63 // Size of the level calculation window in microseconds
64 const int LevelWindowUs = 0.1 * 1000000;
67 //-----------------------------------------------------------------------------
69 //-----------------------------------------------------------------------------
71 QDebug& operator<<(QDebug &debug, const QAudioFormat &format)
73 debug << format.frequency() << "Hz"
74 << format.channels() << "channels";
78 //-----------------------------------------------------------------------------
79 // Constructor and destructor
80 //-----------------------------------------------------------------------------
82 Engine::Engine(QObject *parent)
84 , m_mode(QAudio::AudioInput)
85 , m_state(QAudio::StoppedState)
86 , m_generateTone(false)
89 , m_availableAudioInputDevices
90 (QAudioDeviceInfo::availableDevices(QAudio::AudioInput))
91 , m_audioInputDevice(QAudioDeviceInfo::defaultInputDevice())
93 , m_audioInputIODevice(0)
95 , m_availableAudioOutputDevices
96 (QAudioDeviceInfo::availableDevices(QAudio::AudioOutput))
97 , m_audioOutputDevice(QAudioDeviceInfo::defaultOutputDevice())
100 , m_bufferPosition(0)
103 , m_levelBufferLength(0)
106 , m_spectrumBufferLength(0)
107 , m_spectrumAnalyser()
108 , m_spectrumPosition(0)
111 qRegisterMetaType<FrequencySpectrum>("FrequencySpectrum");
112 qRegisterMetaType<WindowFunction>("WindowFunction");
113 CHECKED_CONNECT(&m_spectrumAnalyser,
114 SIGNAL(spectrumChanged(FrequencySpectrum)),
116 SLOT(spectrumChanged(FrequencySpectrum)));
125 m_spectrumAnalyser.setOutputPath(outputPath());
134 //-----------------------------------------------------------------------------
136 //-----------------------------------------------------------------------------
138 bool Engine::loadFile(const QString &fileName)
142 Q_ASSERT(!m_generateTone);
144 Q_ASSERT(!fileName.isEmpty());
145 m_file = new WavFile(this);
146 if (m_file->open(fileName)) {
147 if (isPCMS16LE(m_file->fileFormat())) {
148 result = initialize();
150 emit errorMessage(tr("Audio format not supported"),
151 formatToString(m_file->fileFormat()));
154 emit errorMessage(tr("Could not open file"), fileName);
157 m_analysisFile = new WavFile(this);
158 m_analysisFile->open(fileName);
163 bool Engine::generateTone(const Tone &tone)
166 Q_ASSERT(!m_generateTone);
168 m_generateTone = true;
170 ENGINE_DEBUG << "Engine::generateTone"
171 << "startFreq" << m_tone.startFreq
172 << "endFreq" << m_tone.endFreq
173 << "amp" << m_tone.amplitude;
177 bool Engine::generateSweptTone(qreal amplitude)
179 Q_ASSERT(!m_generateTone);
181 m_generateTone = true;
182 m_tone.startFreq = 1;
184 m_tone.amplitude = amplitude;
185 ENGINE_DEBUG << "Engine::generateSweptTone"
186 << "startFreq" << m_tone.startFreq
187 << "amp" << m_tone.amplitude;
191 bool Engine::initializeRecord()
194 ENGINE_DEBUG << "Engine::initializeRecord";
195 Q_ASSERT(!m_generateTone);
197 m_generateTone = false;
198 m_tone = SweptTone();
202 qint64 Engine::bufferLength() const
204 return m_file ? m_file->size() : m_bufferLength;
207 void Engine::setWindowFunction(WindowFunction type)
209 m_spectrumAnalyser.setWindowFunction(type);
213 //-----------------------------------------------------------------------------
215 //-----------------------------------------------------------------------------
217 void Engine::startRecording()
220 if (QAudio::AudioInput == m_mode &&
221 QAudio::SuspendedState == m_state) {
222 m_audioInput->resume();
224 m_spectrumAnalyser.cancelCalculation();
225 spectrumChanged(0, 0, FrequencySpectrum());
228 setRecordPosition(0, true);
230 m_mode = QAudio::AudioInput;
231 CHECKED_CONNECT(m_audioInput, SIGNAL(stateChanged(QAudio::State)),
232 this, SLOT(audioStateChanged(QAudio::State)));
233 CHECKED_CONNECT(m_audioInput, SIGNAL(notify()),
234 this, SLOT(audioNotify()));
237 emit dataLengthChanged(0);
238 m_audioInputIODevice = m_audioInput->start();
239 CHECKED_CONNECT(m_audioInputIODevice, SIGNAL(readyRead()),
240 this, SLOT(audioDataReady()));
245 void Engine::startPlayback()
248 if (QAudio::AudioOutput == m_mode &&
249 QAudio::SuspendedState == m_state) {
251 // The Windows backend seems to internally go back into ActiveState
252 // while still returning SuspendedState, so to ensure that it doesn't
253 // ignore the resume() call, we first re-suspend
254 m_audioOutput->suspend();
256 m_audioOutput->resume();
258 m_spectrumAnalyser.cancelCalculation();
259 spectrumChanged(0, 0, FrequencySpectrum());
260 setPlayPosition(0, true);
262 m_mode = QAudio::AudioOutput;
263 CHECKED_CONNECT(m_audioOutput, SIGNAL(stateChanged(QAudio::State)),
264 this, SLOT(audioStateChanged(QAudio::State)));
265 CHECKED_CONNECT(m_audioOutput, SIGNAL(notify()),
266 this, SLOT(audioNotify()));
270 m_bufferPosition = 0;
272 m_audioOutput->start(m_file);
274 m_audioOutputIODevice.close();
275 m_audioOutputIODevice.setBuffer(&m_buffer);
276 m_audioOutputIODevice.open(QIODevice::ReadOnly);
277 m_audioOutput->start(&m_audioOutputIODevice);
283 void Engine::suspend()
285 if (QAudio::ActiveState == m_state ||
286 QAudio::IdleState == m_state) {
288 case QAudio::AudioInput:
289 m_audioInput->suspend();
291 case QAudio::AudioOutput:
292 m_audioOutput->suspend();
298 void Engine::setAudioInputDevice(const QAudioDeviceInfo &device)
300 if (device.deviceName() != m_audioInputDevice.deviceName()) {
301 m_audioInputDevice = device;
306 void Engine::setAudioOutputDevice(const QAudioDeviceInfo &device)
308 if (device.deviceName() != m_audioOutputDevice.deviceName()) {
309 m_audioOutputDevice = device;
315 //-----------------------------------------------------------------------------
317 //-----------------------------------------------------------------------------
319 void Engine::audioNotify()
322 case QAudio::AudioInput: {
323 const qint64 recordPosition = qMin(m_bufferLength, audioLength(m_format, m_audioInput->processedUSecs()));
324 setRecordPosition(recordPosition);
325 const qint64 levelPosition = m_dataLength - m_levelBufferLength;
326 if (levelPosition >= 0)
327 calculateLevel(levelPosition, m_levelBufferLength);
328 if (m_dataLength >= m_spectrumBufferLength) {
329 const qint64 spectrumPosition = m_dataLength - m_spectrumBufferLength;
330 calculateSpectrum(spectrumPosition);
332 emit bufferChanged(0, m_dataLength, m_buffer);
335 case QAudio::AudioOutput: {
336 const qint64 playPosition = audioLength(m_format, m_audioOutput->processedUSecs());
337 setPlayPosition(qMin(bufferLength(), playPosition));
338 const qint64 levelPosition = playPosition - m_levelBufferLength;
339 const qint64 spectrumPosition = playPosition - m_spectrumBufferLength;
341 if (levelPosition > m_bufferPosition ||
342 spectrumPosition > m_bufferPosition ||
343 qMax(m_levelBufferLength, m_spectrumBufferLength) > m_dataLength) {
344 m_bufferPosition = 0;
346 // Data needs to be read into m_buffer in order to be analysed
347 const qint64 readPos = qMax(qint64(0), qMin(levelPosition, spectrumPosition));
348 const qint64 readEnd = qMin(m_analysisFile->size(), qMax(levelPosition + m_levelBufferLength, spectrumPosition + m_spectrumBufferLength));
349 const qint64 readLen = readEnd - readPos + audioLength(m_format, WaveformWindowDuration);
350 qDebug() << "Engine::audioNotify [1]"
351 << "analysisFileSize" << m_analysisFile->size()
352 << "readPos" << readPos
353 << "readLen" << readLen;
354 if (m_analysisFile->seek(readPos + m_analysisFile->headerLength())) {
355 m_buffer.resize(readLen);
356 m_bufferPosition = readPos;
357 m_dataLength = m_analysisFile->read(m_buffer.data(), readLen);
358 qDebug() << "Engine::audioNotify [2]" << "bufferPosition" << m_bufferPosition << "dataLength" << m_dataLength;
360 qDebug() << "Engine::audioNotify [2]" << "file seek error";
362 emit bufferChanged(m_bufferPosition, m_dataLength, m_buffer);
365 if (playPosition >= m_dataLength)
368 if (levelPosition >= 0 && levelPosition + m_levelBufferLength < m_bufferPosition + m_dataLength)
369 calculateLevel(levelPosition, m_levelBufferLength);
370 if (spectrumPosition >= 0 && spectrumPosition + m_spectrumBufferLength < m_bufferPosition + m_dataLength)
371 calculateSpectrum(spectrumPosition);
377 void Engine::audioStateChanged(QAudio::State state)
379 ENGINE_DEBUG << "Engine::audioStateChanged from" << m_state
382 if (QAudio::IdleState == state && m_file && m_file->pos() == m_file->size()) {
385 if (QAudio::StoppedState == state) {
387 QAudio::Error error = QAudio::NoError;
389 case QAudio::AudioInput:
390 error = m_audioInput->error();
392 case QAudio::AudioOutput:
393 error = m_audioOutput->error();
396 if (QAudio::NoError != error) {
405 void Engine::audioDataReady()
407 Q_ASSERT(0 == m_bufferPosition);
408 const qint64 bytesReady = m_audioInput->bytesReady();
409 const qint64 bytesSpace = m_buffer.size() - m_dataLength;
410 const qint64 bytesToRead = qMin(bytesReady, bytesSpace);
412 const qint64 bytesRead = m_audioInputIODevice->read(
413 m_buffer.data() + m_dataLength,
417 m_dataLength += bytesRead;
418 emit dataLengthChanged(dataLength());
421 if (m_buffer.size() == m_dataLength)
425 void Engine::spectrumChanged(const FrequencySpectrum &spectrum)
427 ENGINE_DEBUG << "Engine::spectrumChanged" << "pos" << m_spectrumPosition;
428 emit spectrumChanged(m_spectrumPosition, m_spectrumBufferLength, spectrum);
432 //-----------------------------------------------------------------------------
434 //-----------------------------------------------------------------------------
436 void Engine::resetAudioDevices()
440 m_audioInputIODevice = 0;
441 setRecordPosition(0);
442 delete m_audioOutput;
445 m_spectrumPosition = 0;
446 setLevel(0.0, 0.0, 0);
453 setState(QAudio::AudioInput, QAudio::StoppedState);
454 setFormat(QAudioFormat());
455 m_generateTone = false;
458 delete m_analysisFile;
461 m_bufferPosition = 0;
464 emit dataLengthChanged(0);
468 bool Engine::initialize()
472 QAudioFormat format = m_format;
474 if (selectFormat()) {
475 if (m_format != format) {
478 emit bufferLengthChanged(bufferLength());
479 emit dataLengthChanged(dataLength());
480 emit bufferChanged(0, 0, m_buffer);
481 setRecordPosition(bufferLength());
484 m_bufferLength = audioLength(m_format, BufferDurationUs);
485 m_buffer.resize(m_bufferLength);
487 emit bufferLengthChanged(bufferLength());
488 if (m_generateTone) {
489 if (0 == m_tone.endFreq) {
490 const qreal nyquist = nyquistFrequency(m_format);
491 m_tone.endFreq = qMin(qreal(SpectrumHighFreq), nyquist);
493 // Call function defined in utils.h, at global scope
494 ::generateTone(m_tone, m_format, m_buffer);
495 m_dataLength = m_bufferLength;
496 emit dataLengthChanged(dataLength());
497 emit bufferChanged(0, m_dataLength, m_buffer);
498 setRecordPosition(m_bufferLength);
501 emit bufferChanged(0, 0, m_buffer);
502 m_audioInput = new QAudioInput(m_audioInputDevice, m_format, this);
503 m_audioInput->setNotifyInterval(NotifyIntervalMs);
507 m_audioOutput = new QAudioOutput(m_audioOutputDevice, m_format, this);
508 m_audioOutput->setNotifyInterval(NotifyIntervalMs);
512 emit errorMessage(tr("Audio format not supported"),
513 formatToString(m_format));
514 else if (m_generateTone)
515 emit errorMessage(tr("No suitable format found"), "");
517 emit errorMessage(tr("No common input / output format found"), "");
520 ENGINE_DEBUG << "Engine::initialize" << "m_bufferLength" << m_bufferLength;
521 ENGINE_DEBUG << "Engine::initialize" << "m_dataLength" << m_dataLength;
522 ENGINE_DEBUG << "Engine::initialize" << "format" << m_format;
527 bool Engine::selectFormat()
529 bool foundSupportedFormat = false;
531 if (m_file || QAudioFormat() != m_format) {
532 QAudioFormat format = m_format;
534 // Header is read from the WAV file; just need to check whether
535 // it is supported by the audio output device
536 format = m_file->fileFormat();
537 if (m_audioOutputDevice.isFormatSupported(format)) {
539 foundSupportedFormat = true;
543 QList<int> frequenciesList;
545 // The Windows audio backend does not correctly report format support
546 // (see QTBUG-9100). Furthermore, although the audio subsystem captures
547 // at 11025Hz, the resulting audio is corrupted.
548 frequenciesList += 8000;
552 frequenciesList += m_audioInputDevice.supportedFrequencies();
554 frequenciesList += m_audioOutputDevice.supportedFrequencies();
555 frequenciesList = frequenciesList.toSet().toList(); // remove duplicates
556 qSort(frequenciesList);
557 ENGINE_DEBUG << "Engine::initialize frequenciesList" << frequenciesList;
559 QList<int> channelsList;
560 channelsList += m_audioInputDevice.supportedChannels();
561 channelsList += m_audioOutputDevice.supportedChannels();
562 channelsList = channelsList.toSet().toList();
564 ENGINE_DEBUG << "Engine::initialize channelsList" << channelsList;
567 format.setByteOrder(QAudioFormat::LittleEndian);
568 format.setCodec("audio/pcm");
569 format.setSampleSize(16);
570 format.setSampleType(QAudioFormat::SignedInt);
571 int frequency, channels;
572 foreach (frequency, frequenciesList) {
573 if (foundSupportedFormat)
575 format.setFrequency(frequency);
576 foreach (channels, channelsList) {
577 format.setChannels(channels);
578 const bool inputSupport = m_generateTone ||
579 m_audioInputDevice.isFormatSupported(format);
580 const bool outputSupport = m_audioOutputDevice.isFormatSupported(format);
581 ENGINE_DEBUG << "Engine::initialize checking " << format
582 << "input" << inputSupport
583 << "output" << outputSupport;
584 if (inputSupport && outputSupport) {
585 foundSupportedFormat = true;
591 if (!foundSupportedFormat)
592 format = QAudioFormat();
597 return foundSupportedFormat;
600 void Engine::stopRecording()
603 m_audioInput->stop();
604 QCoreApplication::instance()->processEvents();
605 m_audioInput->disconnect();
607 m_audioInputIODevice = 0;
614 void Engine::stopPlayback()
617 m_audioOutput->stop();
618 QCoreApplication::instance()->processEvents();
619 m_audioOutput->disconnect();
624 void Engine::setState(QAudio::State state)
626 const bool changed = (m_state != state);
629 emit stateChanged(m_mode, m_state);
632 void Engine::setState(QAudio::Mode mode, QAudio::State state)
634 const bool changed = (m_mode != mode || m_state != state);
638 emit stateChanged(m_mode, m_state);
641 void Engine::setRecordPosition(qint64 position, bool forceEmit)
643 const bool changed = (m_recordPosition != position);
644 m_recordPosition = position;
645 if (changed || forceEmit)
646 emit recordPositionChanged(m_recordPosition);
649 void Engine::setPlayPosition(qint64 position, bool forceEmit)
651 const bool changed = (m_playPosition != position);
652 m_playPosition = position;
653 if (changed || forceEmit)
654 emit playPositionChanged(m_playPosition);
657 void Engine::calculateLevel(qint64 position, qint64 length)
663 Q_ASSERT(position + length <= m_bufferPosition + m_dataLength);
665 qreal peakLevel = 0.0;
668 const char *ptr = m_buffer.constData() + position - m_bufferPosition;
669 const char *const end = ptr + length;
671 const qint16 value = *reinterpret_cast<const qint16*>(ptr);
672 const qreal fracValue = pcmToReal(value);
673 peakLevel = qMax(peakLevel, fracValue);
674 sum += fracValue * fracValue;
677 const int numSamples = length / 2;
678 qreal rmsLevel = sqrt(sum / numSamples);
680 rmsLevel = qMax(qreal(0.0), rmsLevel);
681 rmsLevel = qMin(qreal(1.0), rmsLevel);
682 setLevel(rmsLevel, peakLevel, numSamples);
684 ENGINE_DEBUG << "Engine::calculateLevel" << "pos" << position << "len" << length
685 << "rms" << rmsLevel << "peak" << peakLevel;
689 void Engine::calculateSpectrum(qint64 position)
691 #ifdef DISABLE_SPECTRUM
694 Q_ASSERT(position + m_spectrumBufferLength <= m_bufferPosition + m_dataLength);
695 Q_ASSERT(0 == m_spectrumBufferLength % 2); // constraint of FFT algorithm
697 // QThread::currentThread is marked 'for internal use only', but
698 // we're only using it for debug output here, so it's probably OK :)
699 ENGINE_DEBUG << "Engine::calculateSpectrum" << QThread::currentThread()
700 << "count" << m_count << "pos" << position << "len" << m_spectrumBufferLength
701 << "spectrumAnalyser.isReady" << m_spectrumAnalyser.isReady();
703 if(m_spectrumAnalyser.isReady()) {
704 m_spectrumBuffer = QByteArray::fromRawData(m_buffer.constData() + position - m_bufferPosition,
705 m_spectrumBufferLength);
706 m_spectrumPosition = position;
707 m_spectrumAnalyser.calculate(m_spectrumBuffer, m_format);
712 void Engine::setFormat(const QAudioFormat &format)
714 const bool changed = (format != m_format);
716 m_levelBufferLength = audioLength(m_format, LevelWindowUs);
717 m_spectrumBufferLength = SpectrumLengthSamples *
718 (m_format.sampleSize() / 8) * m_format.channels();
720 emit formatChanged(m_format);
723 void Engine::setLevel(qreal rmsLevel, qreal peakLevel, int numSamples)
725 m_rmsLevel = rmsLevel;
726 m_peakLevel = peakLevel;
727 emit levelChanged(m_rmsLevel, m_peakLevel, numSamples);
731 void Engine::createOutputDir()
733 m_outputDir.setPath("output");
735 // Ensure output directory exists and is empty
736 if (m_outputDir.exists()) {
737 const QStringList files = m_outputDir.entryList(QDir::Files);
739 foreach (file, files)
740 m_outputDir.remove(file);
742 QDir::current().mkdir("output");
748 void Engine::dumpData()
750 const QString txtFileName = m_outputDir.filePath("data.txt");
751 QFile txtFile(txtFileName);
752 txtFile.open(QFile::WriteOnly | QFile::Text);
753 QTextStream stream(&txtFile);
754 const qint16 *ptr = reinterpret_cast<const qint16*>(m_buffer.constData());
755 const int numSamples = m_dataLength / (2 * m_format.channels());
756 for (int i=0; i<numSamples; ++i) {
757 stream << i << "\t" << *ptr << "\n";
758 ptr += m_format.channels();
761 const QString pcmFileName = m_outputDir.filePath("data.pcm");
762 QFile pcmFile(pcmFileName);
763 pcmFile.open(QFile::WriteOnly);
764 pcmFile.write(m_buffer.constData(), m_dataLength);