1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include <QtCore/qdebug.h>
41 #include <QtCore/qurl.h>
42 #include <QtCore/qdir.h>
43 #include <qaudiodeviceinfo.h>
44 
45 #include "qmediarecorder.h"
46 
47 #include "audiocapturesession.h"
48 #include "audiocaptureprobecontrol.h"
49 
50 QT_BEGIN_NAMESPACE
51 
startProbes(const QAudioFormat & format)52 void FileProbeProxy::startProbes(const QAudioFormat &format)
53 {
54     m_format = format;
55 }
56 
stopProbes()57 void FileProbeProxy::stopProbes()
58 {
59     m_format = QAudioFormat();
60 }
61 
addProbe(AudioCaptureProbeControl * probe)62 void FileProbeProxy::addProbe(AudioCaptureProbeControl *probe)
63 {
64     QMutexLocker locker(&m_probeMutex);
65 
66     if (m_probes.contains(probe))
67         return;
68 
69     m_probes.append(probe);
70 }
71 
removeProbe(AudioCaptureProbeControl * probe)72 void FileProbeProxy::removeProbe(AudioCaptureProbeControl *probe)
73 {
74     QMutexLocker locker(&m_probeMutex);
75     m_probes.removeOne(probe);
76 }
77 
writeData(const char * data,qint64 len)78 qint64 FileProbeProxy::writeData(const char *data, qint64 len)
79 {
80     if (m_format.isValid()) {
81         QMutexLocker locker(&m_probeMutex);
82 
83         for (AudioCaptureProbeControl* probe : qAsConst(m_probes))
84             probe->bufferProbed(data, len, m_format);
85     }
86 
87     return QFile::writeData(data, len);
88 }
89 
AudioCaptureSession(QObject * parent)90 AudioCaptureSession::AudioCaptureSession(QObject *parent)
91     : QObject(parent)
92     , m_state(QMediaRecorder::StoppedState)
93     , m_status(QMediaRecorder::UnloadedStatus)
94     , m_audioInput(0)
95     , m_deviceInfo(QAudioDeviceInfo::defaultInputDevice())
96     , m_wavFile(true)
97     , m_volume(1.0)
98     , m_muted(false)
99 {
100     m_format = m_deviceInfo.preferredFormat();
101 }
102 
~AudioCaptureSession()103 AudioCaptureSession::~AudioCaptureSession()
104 {
105     setState(QMediaRecorder::StoppedState);
106 }
107 
format() const108 QAudioFormat AudioCaptureSession::format() const
109 {
110     return m_format;
111 }
112 
setFormat(const QAudioFormat & format)113 void AudioCaptureSession::setFormat(const QAudioFormat &format)
114 {
115     m_format = format;
116 }
117 
setContainerFormat(const QString & formatMimeType)118 void AudioCaptureSession::setContainerFormat(const QString &formatMimeType)
119 {
120     m_wavFile = (formatMimeType.isEmpty()
121                  || QString::compare(formatMimeType, QLatin1String("audio/x-wav")) == 0);
122 }
123 
containerFormat() const124 QString AudioCaptureSession::containerFormat() const
125 {
126     if (m_wavFile)
127         return QStringLiteral("audio/x-wav");
128 
129     return QStringLiteral("audio/x-raw");
130 }
131 
outputLocation() const132 QUrl AudioCaptureSession::outputLocation() const
133 {
134     return m_actualOutputLocation;
135 }
136 
setOutputLocation(const QUrl & location)137 bool AudioCaptureSession::setOutputLocation(const QUrl& location)
138 {
139     if (m_requestedOutputLocation == location)
140         return false;
141 
142     m_actualOutputLocation = QUrl();
143     m_requestedOutputLocation = location;
144 
145     if (m_requestedOutputLocation.isEmpty())
146         return true;
147 
148     if (m_requestedOutputLocation.isValid() && (m_requestedOutputLocation.isLocalFile()
149                                        || m_requestedOutputLocation.isRelative())) {
150         emit actualLocationChanged(m_requestedOutputLocation);
151         return true;
152     }
153 
154     m_requestedOutputLocation = QUrl();
155     return false;
156 }
157 
position() const158 qint64 AudioCaptureSession::position() const
159 {
160     if (m_audioInput)
161         return m_audioInput->processedUSecs() / 1000;
162     return 0;
163 }
164 
setState(QMediaRecorder::State state)165 void AudioCaptureSession::setState(QMediaRecorder::State state)
166 {
167     if (m_state == state)
168         return;
169 
170     m_state = state;
171     emit stateChanged(m_state);
172 
173     switch (m_state) {
174     case QMediaRecorder::StoppedState:
175         stop();
176         break;
177     case QMediaRecorder::PausedState:
178         pause();
179         break;
180     case QMediaRecorder::RecordingState:
181         record();
182         break;
183     }
184 }
185 
state() const186 QMediaRecorder::State AudioCaptureSession::state() const
187 {
188     return m_state;
189 }
190 
setStatus(QMediaRecorder::Status status)191 void AudioCaptureSession::setStatus(QMediaRecorder::Status status)
192 {
193     if (m_status == status)
194         return;
195 
196     m_status = status;
197     emit statusChanged(m_status);
198 }
199 
status() const200 QMediaRecorder::Status AudioCaptureSession::status() const
201 {
202     return m_status;
203 }
204 
defaultDir() const205 QDir AudioCaptureSession::defaultDir() const
206 {
207     QStringList dirCandidates;
208 
209     dirCandidates << QDir::home().filePath("Documents");
210     dirCandidates << QDir::home().filePath("My Documents");
211     dirCandidates << QDir::homePath();
212     dirCandidates << QDir::currentPath();
213     dirCandidates << QDir::tempPath();
214 
215     for (const QString &path : qAsConst(dirCandidates)) {
216         QDir dir(path);
217         if (dir.exists() && QFileInfo(path).isWritable())
218             return dir;
219     }
220 
221     return QDir();
222 }
223 
generateFileName(const QString & requestedName,const QString & extension) const224 QString AudioCaptureSession::generateFileName(const QString &requestedName,
225                                               const QString &extension) const
226 {
227     if (requestedName.isEmpty())
228         return generateFileName(defaultDir(), extension);
229 
230     QString path = requestedName;
231 
232     if (QFileInfo(path).isRelative())
233         path = defaultDir().absoluteFilePath(path);
234 
235     if (QFileInfo(path).isDir())
236         return generateFileName(QDir(path), extension);
237 
238     if (!path.endsWith(extension))
239         path.append(QString(".%1").arg(extension));
240 
241     return path;
242 }
243 
generateFileName(const QDir & dir,const QString & ext) const244 QString AudioCaptureSession::generateFileName(const QDir &dir,
245                                               const QString &ext) const
246 {
247     int lastClip = 0;
248     const auto list = dir.entryList(QStringList() << QString("clip_*.%1").arg(ext));
249     for (const QString &fileName : list) {
250         int imgNumber = fileName.midRef(5, fileName.size()-6-ext.length()).toInt();
251         lastClip = qMax(lastClip, imgNumber);
252     }
253 
254     QString name = QString("clip_%1.%2").arg(lastClip+1,
255                                      4, //fieldWidth
256                                      10,
257                                      QLatin1Char('0')).arg(ext);
258 
259     return dir.absoluteFilePath(name);
260 }
261 
record()262 void AudioCaptureSession::record()
263 {
264     if (m_status == QMediaRecorder::PausedStatus) {
265         m_audioInput->resume();
266     } else {
267         if (m_deviceInfo.isNull()) {
268             emit error(QMediaRecorder::ResourceError,
269                        QStringLiteral("No input device available."));
270             m_state = QMediaRecorder::StoppedState;
271             emit stateChanged(m_state);
272             setStatus(QMediaRecorder::UnavailableStatus);
273             return;
274         }
275 
276         setStatus(QMediaRecorder::LoadingStatus);
277 
278         m_format = m_deviceInfo.nearestFormat(m_format);
279         m_audioInput = new QAudioInput(m_deviceInfo, m_format);
280         connect(m_audioInput, SIGNAL(stateChanged(QAudio::State)),
281                 this, SLOT(audioInputStateChanged(QAudio::State)));
282         connect(m_audioInput, SIGNAL(notify()),
283                 this, SLOT(notify()));
284 
285 
286         QString filePath = generateFileName(
287                     m_requestedOutputLocation.isLocalFile() ? m_requestedOutputLocation.toLocalFile()
288                                                    : m_requestedOutputLocation.toString(),
289                     m_wavFile ? QLatin1String("wav")
290                               : QLatin1String("raw"));
291 
292         m_actualOutputLocation = QUrl::fromLocalFile(filePath);
293         if (m_actualOutputLocation != m_requestedOutputLocation)
294             emit actualLocationChanged(m_actualOutputLocation);
295 
296         file.setFileName(filePath);
297 
298         setStatus(QMediaRecorder::LoadedStatus);
299         setStatus(QMediaRecorder::StartingStatus);
300 
301         if (file.open(QIODevice::WriteOnly)) {
302             if (m_wavFile) {
303                 memset(&header,0,sizeof(CombinedHeader));
304                 memcpy(header.riff.descriptor.id,"RIFF",4);
305                 header.riff.descriptor.size = 0xFFFFFFFF; // This should be updated on stop(), filesize-8
306                 memcpy(header.riff.type,"WAVE",4);
307                 memcpy(header.wave.descriptor.id,"fmt ",4);
308                 header.wave.descriptor.size = 16;
309                 header.wave.audioFormat = 1; // for PCM data
310                 header.wave.numChannels = m_format.channelCount();
311                 header.wave.sampleRate = m_format.sampleRate();
312                 header.wave.byteRate = m_format.sampleRate()*m_format.channelCount()*m_format.sampleSize()/8;
313                 header.wave.blockAlign = m_format.channelCount()*m_format.sampleSize()/8;
314                 header.wave.bitsPerSample = m_format.sampleSize();
315                 memcpy(header.data.descriptor.id,"data",4);
316                 header.data.descriptor.size = 0xFFFFFFFF; // This should be updated on stop(),samples*channels*sampleSize/8
317                 file.write((char*)&header,sizeof(CombinedHeader));
318             }
319 
320             setVolumeHelper(m_muted ? 0 : m_volume);
321 
322             file.startProbes(m_format);
323             m_audioInput->start(qobject_cast<QIODevice*>(&file));
324         } else {
325             delete m_audioInput;
326             m_audioInput = 0;
327             emit error(QMediaRecorder::ResourceError,
328                        QStringLiteral("Can't open output location"));
329             m_state = QMediaRecorder::StoppedState;
330             emit stateChanged(m_state);
331             setStatus(QMediaRecorder::UnloadedStatus);
332         }
333     }
334 }
335 
pause()336 void AudioCaptureSession::pause()
337 {
338     m_audioInput->suspend();
339 }
340 
stop()341 void AudioCaptureSession::stop()
342 {
343     if(m_audioInput) {
344         m_audioInput->stop();
345         file.stopProbes();
346         file.close();
347         if (m_wavFile) {
348             qint32 fileSize = file.size();
349             file.open(QIODevice::ReadWrite | QIODevice::Unbuffered);
350             file.read((char*)&header,sizeof(CombinedHeader));
351             header.riff.descriptor.size = fileSize - 8; // The RIFF chunk size is the file size minus
352                                                         // the first two RIFF fields (8 bytes)
353             header.data.descriptor.size = fileSize - 44; // dataSize = fileSize - headerSize (44 bytes)
354             file.seek(0);
355             file.write((char*)&header,sizeof(CombinedHeader));
356             file.close();
357         }
358         delete m_audioInput;
359         m_audioInput = 0;
360         setStatus(QMediaRecorder::UnloadedStatus);
361     }
362 }
363 
addProbe(AudioCaptureProbeControl * probe)364 void AudioCaptureSession::addProbe(AudioCaptureProbeControl *probe)
365 {
366     file.addProbe(probe);
367 }
368 
removeProbe(AudioCaptureProbeControl * probe)369 void AudioCaptureSession::removeProbe(AudioCaptureProbeControl *probe)
370 {
371     file.removeProbe(probe);
372 }
373 
audioInputStateChanged(QAudio::State state)374 void AudioCaptureSession::audioInputStateChanged(QAudio::State state)
375 {
376     switch(state) {
377     case QAudio::ActiveState:
378         setStatus(QMediaRecorder::RecordingStatus);
379         break;
380     case QAudio::SuspendedState:
381         setStatus(QMediaRecorder::PausedStatus);
382         break;
383     case QAudio::StoppedState:
384         setStatus(QMediaRecorder::FinalizingStatus);
385         break;
386     default:
387         break;
388     }
389 }
390 
notify()391 void AudioCaptureSession::notify()
392 {
393     emit positionChanged(position());
394 }
395 
setCaptureDevice(const QString & deviceName)396 void AudioCaptureSession::setCaptureDevice(const QString &deviceName)
397 {
398     m_captureDevice = deviceName;
399 
400     QList<QAudioDeviceInfo> devices = QAudioDeviceInfo::availableDevices(QAudio::AudioInput);
401     for (int i = 0; i < devices.size(); ++i) {
402         QAudioDeviceInfo info = devices.at(i);
403         if (m_captureDevice == info.deviceName()){
404             m_deviceInfo = info;
405             return;
406         }
407     }
408     m_deviceInfo = QAudioDeviceInfo::defaultInputDevice();
409 }
410 
volume() const411 qreal AudioCaptureSession::volume() const
412 {
413     return m_volume;
414 }
415 
isMuted() const416 bool AudioCaptureSession::isMuted() const
417 {
418     return m_muted;
419 }
420 
setVolume(qreal v)421 void AudioCaptureSession::setVolume(qreal v)
422 {
423     qreal boundedVolume = qBound(qreal(0), v, qreal(1));
424 
425     if (m_volume == boundedVolume)
426         return;
427 
428     m_volume = boundedVolume;
429 
430     if (!m_muted)
431         setVolumeHelper(m_volume);
432 
433     emit volumeChanged(m_volume);
434 }
435 
setMuted(bool muted)436 void AudioCaptureSession::setMuted(bool muted)
437 {
438     if (m_muted == muted)
439         return;
440 
441     m_muted = muted;
442 
443     setVolumeHelper(m_muted ? 0 : m_volume);
444 
445     emit mutedChanged(m_muted);
446 }
447 
setVolumeHelper(qreal volume)448 void AudioCaptureSession::setVolumeHelper(qreal volume)
449 {
450     if (!m_audioInput)
451         return;
452 
453     m_audioInput->setVolume(volume);
454 }
455 
456 
457 
458 QT_END_NAMESPACE
459