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 test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
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 General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include <QtTest/QtTest> |
30 | #include <QtCore/qlocale.h> |
31 | #include <QtCore/QTemporaryDir> |
32 | #include <QtCore/QSharedPointer> |
33 | #include <QtCore/QScopedPointer> |
34 | |
35 | #include <qaudioinput.h> |
36 | #include <qaudiodeviceinfo.h> |
37 | #include <qaudioformat.h> |
38 | #include <qaudio.h> |
39 | |
40 | #include "wavheader.h" |
41 | |
42 | //TESTED_COMPONENT=src/multimedia |
43 | |
44 | #define AUDIO_BUFFER 192000 |
45 | #define RANGE_ERR 0.5 |
46 | |
47 | template<typename T> inline bool qTolerantCompare(T value, T expected) |
48 | { |
49 | return qAbs(value - expected) < (RANGE_ERR * expected); |
50 | } |
51 | |
52 | #ifndef QTRY_VERIFY2 |
53 | #define QTRY_VERIFY2(__expr,__msg) \ |
54 | do { \ |
55 | const int __step = 50; \ |
56 | const int __timeout = 5000; \ |
57 | if (!(__expr)) { \ |
58 | QTest::qWait(0); \ |
59 | } \ |
60 | for (int __i = 0; __i < __timeout && !(__expr); __i+=__step) { \ |
61 | QTest::qWait(__step); \ |
62 | } \ |
63 | QVERIFY2(__expr,__msg); \ |
64 | } while(0) |
65 | #endif |
66 | |
67 | class tst_QAudioInput : public QObject |
68 | { |
69 | Q_OBJECT |
70 | public: |
71 | tst_QAudioInput(QObject* parent=0) : QObject(parent) {} |
72 | |
73 | private slots: |
74 | void initTestCase(); |
75 | |
76 | void format(); |
77 | void invalidFormat_data(); |
78 | void invalidFormat(); |
79 | |
80 | void bufferSize(); |
81 | void notifyInterval(); |
82 | void disableNotifyInterval(); |
83 | |
84 | void stopWhileStopped(); |
85 | void suspendWhileStopped(); |
86 | void resumeWhileStopped(); |
87 | |
88 | void pull_data(){generate_audiofile_testrows();} |
89 | void pull(); |
90 | |
91 | void pullSuspendResume_data(){generate_audiofile_testrows();} |
92 | void pullSuspendResume(); |
93 | |
94 | void push_data(){generate_audiofile_testrows();} |
95 | void push(); |
96 | |
97 | void pushSuspendResume_data(){generate_audiofile_testrows();} |
98 | void pushSuspendResume(); |
99 | |
100 | void reset_data(){generate_audiofile_testrows();} |
101 | void reset(); |
102 | |
103 | void volume_data(){generate_audiofile_testrows();} |
104 | void volume(); |
105 | |
106 | private: |
107 | typedef QSharedPointer<QFile> FilePtr; |
108 | |
109 | QString formatToFileName(const QAudioFormat &format); |
110 | |
111 | void generate_audiofile_testrows(); |
112 | |
113 | QAudioDeviceInfo audioDevice; |
114 | QList<QAudioFormat> testFormats; |
115 | QList<FilePtr> audioFiles; |
116 | QScopedPointer<QTemporaryDir> m_temporaryDir; |
117 | |
118 | QScopedPointer<QByteArray> m_byteArray; |
119 | QScopedPointer<QBuffer> m_buffer; |
120 | |
121 | bool m_inCISystem; |
122 | }; |
123 | |
124 | void tst_QAudioInput::generate_audiofile_testrows() |
125 | { |
126 | QTest::addColumn<FilePtr>(name: "audioFile" ); |
127 | QTest::addColumn<QAudioFormat>(name: "audioFormat" ); |
128 | |
129 | for (int i=0; i<audioFiles.count(); i++) { |
130 | QTest::newRow(dataTag: QString("Audio File %1" ).arg(a: i).toLocal8Bit().constData()) |
131 | << audioFiles.at(i) << testFormats.at(i); |
132 | |
133 | // Only run first format in CI system to reduce test times |
134 | if (m_inCISystem) |
135 | break; |
136 | } |
137 | } |
138 | |
139 | QString tst_QAudioInput::formatToFileName(const QAudioFormat &format) |
140 | { |
141 | const QString formatEndian = (format.byteOrder() == QAudioFormat::LittleEndian) |
142 | ? QString("LE" ) : QString("BE" ); |
143 | |
144 | const QString formatSigned = (format.sampleType() == QAudioFormat::SignedInt) |
145 | ? QString("signed" ) : QString("unsigned" ); |
146 | |
147 | return QString("%1_%2_%3_%4_%5" ) |
148 | .arg(a: format.sampleRate()) |
149 | .arg(a: format.sampleSize()) |
150 | .arg(a: formatSigned) |
151 | .arg(a: formatEndian) |
152 | .arg(a: format.channelCount()); |
153 | } |
154 | |
155 | void tst_QAudioInput::initTestCase() |
156 | { |
157 | qRegisterMetaType<QAudioFormat>(); |
158 | |
159 | // Only perform tests if audio output device exists |
160 | const QList<QAudioDeviceInfo> devices = |
161 | QAudioDeviceInfo::availableDevices(mode: QAudio::AudioInput); |
162 | |
163 | if (devices.size() <= 0) |
164 | QSKIP("No audio backend" ); |
165 | |
166 | audioDevice = QAudioDeviceInfo::defaultInputDevice(); |
167 | |
168 | |
169 | QAudioFormat format; |
170 | |
171 | format.setCodec("audio/pcm" ); |
172 | |
173 | if (audioDevice.isFormatSupported(format: audioDevice.preferredFormat())) |
174 | testFormats.append(t: audioDevice.preferredFormat()); |
175 | |
176 | // PCM 8000 mono S8 |
177 | format.setSampleRate(8000); |
178 | format.setSampleSize(8); |
179 | format.setSampleType(QAudioFormat::SignedInt); |
180 | format.setByteOrder(QAudioFormat::LittleEndian); |
181 | format.setChannelCount(1); |
182 | if (audioDevice.isFormatSupported(format)) |
183 | testFormats.append(t: format); |
184 | |
185 | // PCM 11025 mono S16LE |
186 | format.setSampleRate(11025); |
187 | format.setSampleSize(16); |
188 | if (audioDevice.isFormatSupported(format)) |
189 | testFormats.append(t: format); |
190 | |
191 | // PCM 22050 mono S16LE |
192 | format.setSampleRate(22050); |
193 | if (audioDevice.isFormatSupported(format)) |
194 | testFormats.append(t: format); |
195 | |
196 | // PCM 22050 stereo S16LE |
197 | format.setChannelCount(2); |
198 | if (audioDevice.isFormatSupported(format)) |
199 | testFormats.append(t: format); |
200 | |
201 | // PCM 44100 stereo S16LE |
202 | format.setSampleRate(44100); |
203 | if (audioDevice.isFormatSupported(format)) |
204 | testFormats.append(t: format); |
205 | |
206 | // PCM 48000 stereo S16LE |
207 | format.setSampleRate(48000); |
208 | if (audioDevice.isFormatSupported(format)) |
209 | testFormats.append(t: format); |
210 | |
211 | QVERIFY(testFormats.size()); |
212 | |
213 | const QChar slash = QLatin1Char('/'); |
214 | QString temporaryPattern = QDir::tempPath(); |
215 | if (!temporaryPattern.endsWith(c: slash)) |
216 | temporaryPattern += slash; |
217 | temporaryPattern += "tst_qaudioinputXXXXXX" ; |
218 | m_temporaryDir.reset(other: new QTemporaryDir(temporaryPattern)); |
219 | m_temporaryDir->setAutoRemove(true); |
220 | QVERIFY(m_temporaryDir->isValid()); |
221 | |
222 | const QString temporaryAudioPath = m_temporaryDir->path() + slash; |
223 | for (const QAudioFormat &format : qAsConst(t&: testFormats)) { |
224 | const QString fileName = temporaryAudioPath + formatToFileName(format) + QStringLiteral(".wav" ); |
225 | audioFiles.append(t: FilePtr::create(arguments: fileName)); |
226 | } |
227 | qgetenv(varName: "QT_TEST_CI" ).toInt(ok: &m_inCISystem,base: 10); |
228 | } |
229 | |
230 | void tst_QAudioInput::format() |
231 | { |
232 | QAudioInput audioInput(audioDevice.preferredFormat(), this); |
233 | |
234 | QAudioFormat requested = audioDevice.preferredFormat(); |
235 | QAudioFormat actual = audioInput.format(); |
236 | |
237 | QVERIFY2((requested.channelCount() == actual.channelCount()), |
238 | QString("channels: requested=%1, actual=%2" ).arg(requested.channelCount()).arg(actual.channelCount()).toLocal8Bit().constData()); |
239 | QVERIFY2((requested.sampleRate() == actual.sampleRate()), |
240 | QString("sampleRate: requested=%1, actual=%2" ).arg(requested.sampleRate()).arg(actual.sampleRate()).toLocal8Bit().constData()); |
241 | QVERIFY2((requested.sampleSize() == actual.sampleSize()), |
242 | QString("sampleSize: requested=%1, actual=%2" ).arg(requested.sampleSize()).arg(actual.sampleSize()).toLocal8Bit().constData()); |
243 | QVERIFY2((requested.codec() == actual.codec()), |
244 | QString("codec: requested=%1, actual=%2" ).arg(requested.codec()).arg(actual.codec()).toLocal8Bit().constData()); |
245 | QVERIFY2((requested.byteOrder() == actual.byteOrder()), |
246 | QString("byteOrder: requested=%1, actual=%2" ).arg(requested.byteOrder()).arg(actual.byteOrder()).toLocal8Bit().constData()); |
247 | QVERIFY2((requested.sampleType() == actual.sampleType()), |
248 | QString("sampleType: requested=%1, actual=%2" ).arg(requested.sampleType()).arg(actual.sampleType()).toLocal8Bit().constData()); |
249 | } |
250 | |
251 | void tst_QAudioInput::invalidFormat_data() |
252 | { |
253 | QTest::addColumn<QAudioFormat>(name: "invalidFormat" ); |
254 | |
255 | QAudioFormat format; |
256 | |
257 | QTest::newRow(dataTag: "Null Format" ) |
258 | << format; |
259 | |
260 | format = audioDevice.preferredFormat(); |
261 | format.setChannelCount(0); |
262 | QTest::newRow(dataTag: "Channel count 0" ) |
263 | << format; |
264 | |
265 | format = audioDevice.preferredFormat(); |
266 | format.setSampleRate(0); |
267 | QTest::newRow(dataTag: "Sample rate 0" ) |
268 | << format; |
269 | |
270 | format = audioDevice.preferredFormat(); |
271 | format.setSampleSize(0); |
272 | QTest::newRow(dataTag: "Sample size 0" ) |
273 | << format; |
274 | } |
275 | |
276 | void tst_QAudioInput::invalidFormat() |
277 | { |
278 | QFETCH(QAudioFormat, invalidFormat); |
279 | |
280 | QVERIFY2(!audioDevice.isFormatSupported(invalidFormat), |
281 | "isFormatSupported() is returning true on an invalid format" ); |
282 | |
283 | QAudioInput audioInput(invalidFormat, this); |
284 | |
285 | // Check that we are in the default state before calling start |
286 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
287 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
288 | |
289 | audioInput.start(); |
290 | |
291 | // Check that error is raised |
292 | QTRY_VERIFY2((audioInput.error() == QAudio::OpenError),"error() was not set to QAudio::OpenError after start()" ); |
293 | } |
294 | |
295 | void tst_QAudioInput::bufferSize() |
296 | { |
297 | QAudioInput audioInput(audioDevice.preferredFormat(), this); |
298 | |
299 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation" ); |
300 | |
301 | audioInput.setBufferSize(512); |
302 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize(512)" ); |
303 | QVERIFY2((audioInput.bufferSize() == 512), |
304 | QString("bufferSize: requested=512, actual=%2" ).arg(audioInput.bufferSize()).toLocal8Bit().constData()); |
305 | |
306 | audioInput.setBufferSize(4096); |
307 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize(4096)" ); |
308 | QVERIFY2((audioInput.bufferSize() == 4096), |
309 | QString("bufferSize: requested=4096, actual=%2" ).arg(audioInput.bufferSize()).toLocal8Bit().constData()); |
310 | |
311 | audioInput.setBufferSize(8192); |
312 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize(8192)" ); |
313 | QVERIFY2((audioInput.bufferSize() == 8192), |
314 | QString("bufferSize: requested=8192, actual=%2" ).arg(audioInput.bufferSize()).toLocal8Bit().constData()); |
315 | } |
316 | |
317 | void tst_QAudioInput::notifyInterval() |
318 | { |
319 | QAudioInput audioInput(audioDevice.preferredFormat(), this); |
320 | |
321 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation" ); |
322 | |
323 | audioInput.setNotifyInterval(50); |
324 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(50)" ); |
325 | QVERIFY2((audioInput.notifyInterval() == 50), |
326 | QString("notifyInterval: requested=50, actual=%2" ).arg(audioInput.notifyInterval()).toLocal8Bit().constData()); |
327 | |
328 | audioInput.setNotifyInterval(100); |
329 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(100)" ); |
330 | QVERIFY2((audioInput.notifyInterval() == 100), |
331 | QString("notifyInterval: requested=100, actual=%2" ).arg(audioInput.notifyInterval()).toLocal8Bit().constData()); |
332 | |
333 | audioInput.setNotifyInterval(250); |
334 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(250)" ); |
335 | QVERIFY2((audioInput.notifyInterval() == 250), |
336 | QString("notifyInterval: requested=250, actual=%2" ).arg(audioInput.notifyInterval()).toLocal8Bit().constData()); |
337 | |
338 | audioInput.setNotifyInterval(1000); |
339 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(1000)" ); |
340 | QVERIFY2((audioInput.notifyInterval() == 1000), |
341 | QString("notifyInterval: requested=1000, actual=%2" ).arg(audioInput.notifyInterval()).toLocal8Bit().constData()); |
342 | } |
343 | |
344 | void tst_QAudioInput::disableNotifyInterval() |
345 | { |
346 | // Sets an invalid notification interval (QAudioInput::setNotifyInterval(0)) |
347 | // Checks that |
348 | // - No error is raised (QAudioInput::error() returns QAudio::NoError) |
349 | // - if <= 0, set to zero and disable notify signal |
350 | |
351 | QAudioInput audioInput(audioDevice.preferredFormat(), this); |
352 | |
353 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation" ); |
354 | |
355 | audioInput.setNotifyInterval(0); |
356 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(0)" ); |
357 | QVERIFY2((audioInput.notifyInterval() == 0), |
358 | "notifyInterval() is not zero after setNotifyInterval(0)" ); |
359 | |
360 | audioInput.setNotifyInterval(-1); |
361 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(-1)" ); |
362 | QVERIFY2((audioInput.notifyInterval() == 0), |
363 | "notifyInterval() is not zero after setNotifyInterval(-1)" ); |
364 | |
365 | //start and run to check if notify() is emitted |
366 | if (audioFiles.size() > 0) { |
367 | QAudioInput audioInputCheck(testFormats.at(i: 0), this); |
368 | audioInputCheck.setNotifyInterval(0); |
369 | QSignalSpy notifySignal(&audioInputCheck, SIGNAL(notify())); |
370 | QFile *audioFile = audioFiles.at(i: 0).data(); |
371 | audioFile->open(flags: QIODevice::WriteOnly); |
372 | audioInputCheck.start(device: audioFile); |
373 | QTest::qWait(ms: 3000); // 3 seconds should be plenty |
374 | audioInputCheck.stop(); |
375 | QVERIFY2((notifySignal.count() == 0), |
376 | QString("didn't disable notify interval: shouldn't have got any but got %1" ).arg(notifySignal.count()).toLocal8Bit().constData()); |
377 | audioFile->close(); |
378 | } |
379 | } |
380 | |
381 | void tst_QAudioInput::stopWhileStopped() |
382 | { |
383 | // Calls QAudioInput::stop() when object is already in StoppedState |
384 | // Checks that |
385 | // - No state change occurs |
386 | // - No error is raised (QAudioInput::error() returns QAudio::NoError) |
387 | |
388 | QAudioInput audioInput(audioDevice.preferredFormat(), this); |
389 | |
390 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
391 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
392 | |
393 | QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); |
394 | audioInput.stop(); |
395 | |
396 | // Check that no state transition occurred |
397 | QVERIFY2((stateSignal.count() == 0), "stop() while stopped is emitting a signal and it shouldn't" ); |
398 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()" ); |
399 | } |
400 | |
401 | void tst_QAudioInput::suspendWhileStopped() |
402 | { |
403 | // Calls QAudioInput::suspend() when object is already in StoppedState |
404 | // Checks that |
405 | // - No state change occurs |
406 | // - No error is raised (QAudioInput::error() returns QAudio::NoError) |
407 | |
408 | QAudioInput audioInput(audioDevice.preferredFormat(), this); |
409 | |
410 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
411 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
412 | |
413 | QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); |
414 | audioInput.suspend(); |
415 | |
416 | // Check that no state transition occurred |
417 | QVERIFY2((stateSignal.count() == 0), "stop() while suspended is emitting a signal and it shouldn't" ); |
418 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()" ); |
419 | } |
420 | |
421 | void tst_QAudioInput::resumeWhileStopped() |
422 | { |
423 | // Calls QAudioInput::resume() when object is already in StoppedState |
424 | // Checks that |
425 | // - No state change occurs |
426 | // - No error is raised (QAudioInput::error() returns QAudio::NoError) |
427 | |
428 | QAudioInput audioInput(audioDevice.preferredFormat(), this); |
429 | |
430 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
431 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
432 | |
433 | QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); |
434 | audioInput.resume(); |
435 | |
436 | // Check that no state transition occurred |
437 | QVERIFY2((stateSignal.count() == 0), "resume() while stopped is emitting a signal and it shouldn't" ); |
438 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after resume()" ); |
439 | } |
440 | |
441 | void tst_QAudioInput::pull() |
442 | { |
443 | QFETCH(FilePtr, audioFile); |
444 | QFETCH(QAudioFormat, audioFormat); |
445 | |
446 | QAudioInput audioInput(audioFormat, this); |
447 | |
448 | audioInput.setNotifyInterval(100); |
449 | |
450 | QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); |
451 | QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); |
452 | |
453 | // Check that we are in the default state before calling start |
454 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
455 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
456 | QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation" ); |
457 | |
458 | audioFile->close(); |
459 | audioFile->open(flags: QIODevice::WriteOnly); |
460 | WavHeader (audioFormat); |
461 | QVERIFY(wavHeader.write(*audioFile)); |
462 | |
463 | audioInput.start(device: audioFile.data()); |
464 | |
465 | // Check that QAudioInput immediately transitions to ActiveState or IdleState |
466 | QTRY_VERIFY2((stateSignal.count() > 0),"didn't emit signals on start()" ); |
467 | QVERIFY2((audioInput.state() == QAudio::ActiveState || audioInput.state() == QAudio::IdleState), |
468 | "didn't transition to ActiveState or IdleState after start()" ); |
469 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()" ); |
470 | QVERIFY(audioInput.periodSize() > 0); |
471 | stateSignal.clear(); |
472 | |
473 | // Check that 'elapsed' increases |
474 | QTest::qWait(ms: 40); |
475 | QVERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()" ); |
476 | |
477 | // Allow some recording to happen |
478 | QTest::qWait(ms: 3000); // 3 seconds should be plenty |
479 | |
480 | stateSignal.clear(); |
481 | |
482 | qint64 processedUs = audioInput.processedUSecs(); |
483 | |
484 | audioInput.stop(); |
485 | QTest::qWait(ms: 40); |
486 | QTRY_VERIFY2((stateSignal.count() == 1), |
487 | QString("didn't emit StoppedState signal after stop(), got %1 signals instead" ).arg(stateSignal.count()).toLocal8Bit().constData()); |
488 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()" ); |
489 | |
490 | QVERIFY2(qTolerantCompare(processedUs, 3040000LL), |
491 | QString("processedUSecs() doesn't fall in acceptable range, should be 3040000 (%1)" ).arg(processedUs).toLocal8Bit().constData()); |
492 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()" ); |
493 | QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState" ); |
494 | QVERIFY2(notifySignal.count() > 0, "not emitting notify() signal" ); |
495 | |
496 | WavHeader::writeDataLength(device&: *audioFile, dataLength: audioFile->pos() - WavHeader::headerLength()); |
497 | audioFile->close(); |
498 | |
499 | } |
500 | |
501 | void tst_QAudioInput::pullSuspendResume() |
502 | { |
503 | #ifdef Q_OS_LINUX |
504 | if (m_inCISystem) |
505 | QSKIP("QTBUG-26504 Fails 20% of time with pulseaudio backend" ); |
506 | #endif |
507 | QFETCH(FilePtr, audioFile); |
508 | QFETCH(QAudioFormat, audioFormat); |
509 | |
510 | QAudioInput audioInput(audioFormat, this); |
511 | |
512 | audioInput.setNotifyInterval(100); |
513 | |
514 | QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); |
515 | QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); |
516 | |
517 | // Check that we are in the default state before calling start |
518 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
519 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
520 | QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation" ); |
521 | |
522 | audioFile->close(); |
523 | audioFile->open(flags: QIODevice::WriteOnly); |
524 | WavHeader (audioFormat); |
525 | QVERIFY(wavHeader.write(*audioFile)); |
526 | |
527 | audioInput.start(device: audioFile.data()); |
528 | |
529 | // Check that QAudioInput immediately transitions to ActiveState or IdleState |
530 | QTRY_VERIFY2((stateSignal.count() > 0),"didn't emit signals on start()" ); |
531 | QVERIFY2((audioInput.state() == QAudio::ActiveState || audioInput.state() == QAudio::IdleState), |
532 | "didn't transition to ActiveState or IdleState after start()" ); |
533 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()" ); |
534 | QVERIFY(audioInput.periodSize() > 0); |
535 | stateSignal.clear(); |
536 | |
537 | // Check that 'elapsed' increases |
538 | QTest::qWait(ms: 40); |
539 | QVERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()" ); |
540 | |
541 | // Allow some recording to happen |
542 | QTest::qWait(ms: 3000); // 3 seconds should be plenty |
543 | |
544 | QVERIFY2((audioInput.state() == QAudio::ActiveState), |
545 | "didn't transition to ActiveState after some recording" ); |
546 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after some recording" ); |
547 | |
548 | stateSignal.clear(); |
549 | |
550 | audioInput.suspend(); |
551 | |
552 | // Give backends running in separate threads a chance to suspend. |
553 | QTest::qWait(ms: 100); |
554 | |
555 | QVERIFY2((stateSignal.count() == 1), |
556 | QString("didn't emit SuspendedState signal after suspend(), got %1 signals instead" ).arg(stateSignal.count()).toLocal8Bit().constData()); |
557 | QVERIFY2((audioInput.state() == QAudio::SuspendedState), "didn't transitions to SuspendedState after stop()" ); |
558 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()" ); |
559 | stateSignal.clear(); |
560 | |
561 | // Check that only 'elapsed', and not 'processed' increases while suspended |
562 | qint64 elapsedUs = audioInput.elapsedUSecs(); |
563 | qint64 processedUs = audioInput.processedUSecs(); |
564 | QTest::qWait(ms: 1000); |
565 | QVERIFY(audioInput.elapsedUSecs() > elapsedUs); |
566 | QVERIFY(audioInput.processedUSecs() == processedUs); |
567 | |
568 | audioInput.resume(); |
569 | |
570 | // Give backends running in separate threads a chance to resume. |
571 | QTest::qWait(ms: 100); |
572 | |
573 | // Check that QAudioInput immediately transitions to ActiveState |
574 | QVERIFY2((stateSignal.count() == 1), |
575 | QString("didn't emit signal after resume(), got %1 signals instead" ).arg(stateSignal.count()).toLocal8Bit().constData()); |
576 | QVERIFY2((audioInput.state() == QAudio::ActiveState), "didn't transition to ActiveState after resume()" ); |
577 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()" ); |
578 | stateSignal.clear(); |
579 | |
580 | processedUs = audioInput.processedUSecs(); |
581 | |
582 | audioInput.stop(); |
583 | QTest::qWait(ms: 40); |
584 | QTRY_VERIFY2((stateSignal.count() == 1), |
585 | QString("didn't emit StoppedState signal after stop(), got %1 signals instead" ).arg(stateSignal.count()).toLocal8Bit().constData()); |
586 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()" ); |
587 | |
588 | QVERIFY2(qTolerantCompare(processedUs, 3040000LL), |
589 | QString("processedUSecs() doesn't fall in acceptable range, should be 3040000 (%1)" ).arg(processedUs).toLocal8Bit().constData()); |
590 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()" ); |
591 | QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState" ); |
592 | QVERIFY2(notifySignal.count() > 0, "not emitting notify() signal" ); |
593 | |
594 | WavHeader::writeDataLength(device&: *audioFile,dataLength: audioFile->pos()-WavHeader::headerLength()); |
595 | audioFile->close(); |
596 | } |
597 | |
598 | void tst_QAudioInput::push() |
599 | { |
600 | QFETCH(FilePtr, audioFile); |
601 | QFETCH(QAudioFormat, audioFormat); |
602 | |
603 | QAudioInput audioInput(audioFormat, this); |
604 | |
605 | audioInput.setNotifyInterval(100); |
606 | |
607 | QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); |
608 | QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); |
609 | |
610 | // Check that we are in the default state before calling start |
611 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
612 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
613 | QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation" ); |
614 | |
615 | audioFile->close(); |
616 | audioFile->open(flags: QIODevice::WriteOnly); |
617 | WavHeader (audioFormat); |
618 | QVERIFY(wavHeader.write(*audioFile)); |
619 | |
620 | // Set a large buffer to avoid underruns during QTest::qWaits |
621 | audioInput.setBufferSize(audioFormat.bytesForDuration(duration: 1000000)); |
622 | |
623 | QIODevice* feed = audioInput.start(); |
624 | |
625 | // Check that QAudioInput immediately transitions to IdleState |
626 | QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit IdleState signal on start()" ); |
627 | QVERIFY2((audioInput.state() == QAudio::IdleState), |
628 | "didn't transition to IdleState after start()" ); |
629 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()" ); |
630 | QVERIFY(audioInput.periodSize() > 0); |
631 | stateSignal.clear(); |
632 | |
633 | // Check that 'elapsed' increases |
634 | QTest::qWait(ms: 40); |
635 | QVERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()" ); |
636 | |
637 | qint64 totalBytesRead = 0; |
638 | bool firstBuffer = true; |
639 | QByteArray buffer(AUDIO_BUFFER, 0); |
640 | qint64 len = (audioFormat.sampleRate()*audioFormat.channelCount()*(audioFormat.sampleSize()/8)*2); // 2 seconds |
641 | while (totalBytesRead < len) { |
642 | QTRY_VERIFY_WITH_TIMEOUT(audioInput.bytesReady() >= audioInput.periodSize(), 10000); |
643 | qint64 bytesRead = feed->read(data: buffer.data(), maxlen: audioInput.periodSize()); |
644 | audioFile->write(data: buffer.constData(),len: bytesRead); |
645 | totalBytesRead+=bytesRead; |
646 | if (firstBuffer && bytesRead) { |
647 | // Check for transition to ActiveState when data is provided |
648 | QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit ActiveState signal on data" ); |
649 | QVERIFY2((audioInput.state() == QAudio::ActiveState), |
650 | "didn't transition to ActiveState after data" ); |
651 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()" ); |
652 | firstBuffer = false; |
653 | } |
654 | } |
655 | |
656 | QTest::qWait(ms: 1000); |
657 | |
658 | stateSignal.clear(); |
659 | |
660 | qint64 processedUs = audioInput.processedUSecs(); |
661 | |
662 | audioInput.stop(); |
663 | QTest::qWait(ms: 40); |
664 | QTRY_VERIFY2((stateSignal.count() == 1), |
665 | QString("didn't emit StoppedState signal after stop(), got %1 signals instead" ).arg(stateSignal.count()).toLocal8Bit().constData()); |
666 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()" ); |
667 | |
668 | QVERIFY2(qTolerantCompare(processedUs, 2040000LL), |
669 | QString("processedUSecs() doesn't fall in acceptable range, should be 2040000 (%1)" ).arg(processedUs).toLocal8Bit().constData()); |
670 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()" ); |
671 | QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState" ); |
672 | QVERIFY2(notifySignal.count() > 0, "not emitting notify() signal" ); |
673 | |
674 | WavHeader::writeDataLength(device&: *audioFile,dataLength: audioFile->pos()-WavHeader::headerLength()); |
675 | audioFile->close(); |
676 | } |
677 | |
678 | void tst_QAudioInput::pushSuspendResume() |
679 | { |
680 | #ifdef Q_OS_LINUX |
681 | if (m_inCISystem) |
682 | QSKIP("QTBUG-26504 Fails 20% of time with pulseaudio backend" ); |
683 | #endif |
684 | QFETCH(FilePtr, audioFile); |
685 | QFETCH(QAudioFormat, audioFormat); |
686 | QAudioInput audioInput(audioFormat, this); |
687 | |
688 | audioInput.setNotifyInterval(100); |
689 | audioInput.setBufferSize(audioFormat.bytesForDuration(duration: 1000000)); |
690 | |
691 | QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); |
692 | QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); |
693 | |
694 | // Check that we are in the default state before calling start |
695 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
696 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
697 | QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation" ); |
698 | |
699 | audioFile->close(); |
700 | audioFile->open(flags: QIODevice::WriteOnly); |
701 | WavHeader (audioFormat); |
702 | QVERIFY(wavHeader.write(*audioFile)); |
703 | |
704 | QIODevice* feed = audioInput.start(); |
705 | |
706 | // Check that QAudioInput immediately transitions to IdleState |
707 | QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit IdleState signal on start()" ); |
708 | QVERIFY2((audioInput.state() == QAudio::IdleState), |
709 | "didn't transition to IdleState after start()" ); |
710 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()" ); |
711 | QVERIFY(audioInput.periodSize() > 0); |
712 | stateSignal.clear(); |
713 | |
714 | // Check that 'elapsed' increases |
715 | QTest::qWait(ms: 40); |
716 | QTRY_VERIFY2((audioInput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()" ); |
717 | |
718 | qint64 totalBytesRead = 0; |
719 | bool firstBuffer = true; |
720 | QByteArray buffer(AUDIO_BUFFER, 0); |
721 | qint64 len = (audioFormat.sampleRate()*audioFormat.channelCount()*(audioFormat.sampleSize()/8)); // 1 seconds |
722 | while (totalBytesRead < len) { |
723 | QTRY_VERIFY_WITH_TIMEOUT(audioInput.bytesReady() >= audioInput.periodSize(), 10000); |
724 | qint64 bytesRead = feed->read(data: buffer.data(), maxlen: audioInput.periodSize()); |
725 | audioFile->write(data: buffer.constData(),len: bytesRead); |
726 | totalBytesRead+=bytesRead; |
727 | if (firstBuffer && bytesRead) { |
728 | // Check for transition to ActiveState when data is provided |
729 | QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit ActiveState signal on data" ); |
730 | QVERIFY2((audioInput.state() == QAudio::ActiveState), |
731 | "didn't transition to ActiveState after data" ); |
732 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()" ); |
733 | firstBuffer = false; |
734 | } |
735 | } |
736 | stateSignal.clear(); |
737 | |
738 | audioInput.suspend(); |
739 | |
740 | // Give backends running in separate threads a chance to suspend |
741 | QTest::qWait(ms: 100); |
742 | |
743 | QVERIFY2((stateSignal.count() == 1), |
744 | QString("didn't emit SuspendedState signal after suspend(), got %1 signals instead" ).arg(stateSignal.count()).toLocal8Bit().constData()); |
745 | QVERIFY2((audioInput.state() == QAudio::SuspendedState), "didn't transitions to SuspendedState after stop()" ); |
746 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()" ); |
747 | stateSignal.clear(); |
748 | |
749 | // Check that only 'elapsed', and not 'processed' increases while suspended |
750 | qint64 elapsedUs = audioInput.elapsedUSecs(); |
751 | qint64 processedUs = audioInput.processedUSecs(); |
752 | QTest::qWait(ms: 1000); |
753 | QVERIFY(audioInput.elapsedUSecs() > elapsedUs); |
754 | QVERIFY(audioInput.processedUSecs() == processedUs); |
755 | |
756 | // Drain any data, in case we run out of space when resuming |
757 | const int reads = audioInput.bytesReady() / audioInput.periodSize(); |
758 | for (int r = 0; r < reads; ++r) |
759 | feed->read(data: buffer.data(), maxlen: audioInput.periodSize()); |
760 | |
761 | audioInput.resume(); |
762 | |
763 | // Check that QAudioInput immediately transitions to Active or IdleState |
764 | QTRY_VERIFY2((stateSignal.count() > 0),"didn't emit signals on resume()" ); |
765 | QVERIFY2((audioInput.state() == QAudio::ActiveState || audioInput.state() == QAudio::IdleState), |
766 | "didn't transition to ActiveState or IdleState after resume()" ); |
767 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()" ); |
768 | QVERIFY(audioInput.periodSize() > 0); |
769 | |
770 | // Let it play out what is in buffer and go to Idle before continue |
771 | QTest::qWait(ms: 1000); |
772 | stateSignal.clear(); |
773 | |
774 | // Read another seconds worth |
775 | totalBytesRead = 0; |
776 | firstBuffer = true; |
777 | while (totalBytesRead < len && audioInput.state() != QAudio::StoppedState) { |
778 | QTRY_VERIFY_WITH_TIMEOUT(audioInput.bytesReady() >= audioInput.periodSize(), 10000); |
779 | qint64 bytesRead = feed->read(data: buffer.data(), maxlen: audioInput.periodSize()); |
780 | audioFile->write(data: buffer.constData(),len: bytesRead); |
781 | totalBytesRead+=bytesRead; |
782 | } |
783 | stateSignal.clear(); |
784 | |
785 | processedUs = audioInput.processedUSecs(); |
786 | |
787 | audioInput.stop(); |
788 | QTest::qWait(ms: 40); |
789 | QVERIFY2((stateSignal.count() == 1), |
790 | QString("didn't emit StoppedState signal after stop(), got %1 signals instead" ).arg(stateSignal.count()).toLocal8Bit().constData()); |
791 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()" ); |
792 | |
793 | QVERIFY2(qTolerantCompare(processedUs, 2040000LL), |
794 | QString("processedUSecs() doesn't fall in acceptable range, should be 2040000 (%1)" ).arg(processedUs).toLocal8Bit().constData()); |
795 | QVERIFY2((audioInput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState" ); |
796 | |
797 | WavHeader::writeDataLength(device&: *audioFile,dataLength: audioFile->pos()-WavHeader::headerLength()); |
798 | audioFile->close(); |
799 | } |
800 | |
801 | void tst_QAudioInput::reset() |
802 | { |
803 | QFETCH(QAudioFormat, audioFormat); |
804 | |
805 | // Try both push/pull.. the vagaries of Active vs Idle are tested elsewhere |
806 | { |
807 | QAudioInput audioInput(audioFormat, this); |
808 | |
809 | audioInput.setNotifyInterval(100); |
810 | |
811 | QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); |
812 | QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); |
813 | |
814 | // Check that we are in the default state before calling start |
815 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
816 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
817 | QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation" ); |
818 | |
819 | QIODevice* device = audioInput.start(); |
820 | // Check that QAudioInput immediately transitions to IdleState |
821 | QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit IdleState signal on start()" ); |
822 | QVERIFY2((audioInput.state() == QAudio::IdleState), "didn't transition to IdleState after start()" ); |
823 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()" ); |
824 | QVERIFY(audioInput.periodSize() > 0); |
825 | QTRY_VERIFY2_WITH_TIMEOUT((audioInput.bytesReady() > audioInput.periodSize()), "no bytes available after starting" , 10000); |
826 | |
827 | // Trigger a read |
828 | QByteArray data = device->read(maxlen: audioInput.periodSize()); |
829 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()" ); |
830 | stateSignal.clear(); |
831 | |
832 | audioInput.reset(); |
833 | QTRY_VERIFY2((stateSignal.count() == 1),"didn't emit StoppedState signal after reset()" ); |
834 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after reset()" ); |
835 | QVERIFY2((audioInput.bytesReady() == 0), "buffer not cleared after reset()" ); |
836 | } |
837 | |
838 | { |
839 | QAudioInput audioInput(audioFormat, this); |
840 | QBuffer buffer; |
841 | |
842 | audioInput.setNotifyInterval(100); |
843 | |
844 | QSignalSpy notifySignal(&audioInput, SIGNAL(notify())); |
845 | QSignalSpy stateSignal(&audioInput, SIGNAL(stateChanged(QAudio::State))); |
846 | |
847 | // Check that we are in the default state before calling start |
848 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()" ); |
849 | QVERIFY2((audioInput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()" ); |
850 | QVERIFY2((audioInput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation" ); |
851 | |
852 | audioInput.start(device: &buffer); |
853 | |
854 | // Check that QAudioInput immediately transitions to ActiveState |
855 | QTRY_VERIFY2((stateSignal.count() >= 1),"didn't emit state changed signal on start()" ); |
856 | QTRY_VERIFY2((audioInput.state() == QAudio::ActiveState), "didn't transition to ActiveState after start()" ); |
857 | QVERIFY2((audioInput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()" ); |
858 | QVERIFY(audioInput.periodSize() > 0); |
859 | stateSignal.clear(); |
860 | |
861 | audioInput.reset(); |
862 | QTRY_VERIFY2((stateSignal.count() >= 1),"didn't emit StoppedState signal after reset()" ); |
863 | QVERIFY2((audioInput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after reset()" ); |
864 | QVERIFY2((audioInput.bytesReady() == 0), "buffer not cleared after reset()" ); |
865 | } |
866 | } |
867 | |
868 | void tst_QAudioInput::volume() |
869 | { |
870 | QFETCH(QAudioFormat, audioFormat); |
871 | |
872 | const qreal half(0.5f); |
873 | const qreal one(1.0f); |
874 | |
875 | QAudioInput audioInput(audioFormat, this); |
876 | |
877 | qreal volume = audioInput.volume(); |
878 | audioInput.setVolume(half); |
879 | QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 5); |
880 | // Wait a while to see if this changes |
881 | QTest::qWait(ms: 500); |
882 | QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 5); |
883 | |
884 | audioInput.setVolume(one); |
885 | QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 10); |
886 | // Wait a while to see if this changes |
887 | QTest::qWait(ms: 500); |
888 | QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 10); |
889 | |
890 | audioInput.setVolume(half); |
891 | audioInput.start(); |
892 | QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 5); |
893 | audioInput.setVolume(one); |
894 | QTRY_VERIFY(qRound(audioInput.volume()*10.0f) == 10); |
895 | |
896 | audioInput.setVolume(volume); |
897 | } |
898 | |
899 | QTEST_MAIN(tst_QAudioInput) |
900 | |
901 | #include "tst_qaudioinput.moc" |
902 | |