Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qalsaaudiosink.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <QtCore/qcoreapplication.h>
5#include <QtCore/qvarlengtharray.h>
6#include <QtMultimedia/private/qaudiohelpers_p.h>
8#include <QLoggingCategory>
9
11
12Q_STATIC_LOGGING_CATEGORY(lcAlsaOutput, "qt.multimedia.alsa.output");
13//#define DEBUG_AUDIO 1
14
15QAlsaAudioSink::QAlsaAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
16 : QPlatformAudioSink(std::move(device), format, parent)
17{
18 timer = new QTimer(this);
19 connect(timer, &QTimer::timeout, this, &QAlsaAudioSink::userFeed);
20}
21
22QAlsaAudioSink::~QAlsaAudioSink()
23{
24 close();
25 disconnect(timer, &QTimer::timeout, this, &QAlsaAudioSink::userFeed);
26 delete timer;
27}
28
29QAudio::State QAlsaAudioSink::state() const
30{
31 return deviceState;
32}
33
34int QAlsaAudioSink::xrun_recovery(int err)
35{
36 int count = 0;
37 bool reset = false;
38
39 // ESTRPIPE is not available in all OSes where ALSA is available
40 int estrpipe = EIO;
41#ifdef ESTRPIPE
42 estrpipe = ESTRPIPE;
43#endif
44
45 if(err == -EPIPE) {
46 errorState = QAudio::UnderrunError;
47 setError(errorState);
48 err = snd_pcm_prepare(handle);
49 if(err < 0)
50 reset = true;
51
52 } else if ((err == -estrpipe)||(err == -EIO)) {
53 errorState = QAudio::IOError;
54 setError(errorState);
55 while ((err = snd_pcm_resume(handle)) == -EAGAIN) {
56 usleep(100);
57 count++;
58 if(count > 5) {
59 reset = true;
60 break;
61 }
62 }
63 if(err < 0) {
64 err = snd_pcm_prepare(handle);
65 if(err < 0)
66 reset = true;
67 }
68 }
69 if(reset) {
70 close();
71 open();
72 snd_pcm_prepare(handle);
73 return 0;
74 }
75 return err;
76}
77
78int QAlsaAudioSink::setFormat()
79{
80 snd_pcm_format_t pcmformat = SND_PCM_FORMAT_UNKNOWN;
81
82 switch (m_format.sampleFormat()) {
83 case QAudioFormat::UInt8:
84 pcmformat = SND_PCM_FORMAT_U8;
85 break;
86 case QAudioFormat::Int16:
87 if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian)
88 pcmformat = SND_PCM_FORMAT_S16_BE;
89 else
90 pcmformat = SND_PCM_FORMAT_S16_LE;
91 break;
92 case QAudioFormat::Int32:
93 if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian)
94 pcmformat = SND_PCM_FORMAT_S32_BE;
95 else
96 pcmformat = SND_PCM_FORMAT_S32_LE;
97 break;
98 case QAudioFormat::Float:
99 if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian)
100 pcmformat = SND_PCM_FORMAT_FLOAT_BE;
101 else
102 pcmformat = SND_PCM_FORMAT_FLOAT_LE;
103 break;
104 default:
105 break;
106 }
107
108 return pcmformat != SND_PCM_FORMAT_UNKNOWN
109 ? snd_pcm_hw_params_set_format( handle, hwparams, pcmformat)
110 : -1;
111}
112
113void QAlsaAudioSink::start(QIODevice* device)
114{
115 if(deviceState != QAudio::StoppedState)
116 deviceState = QAudio::StoppedState;
117
118 errorState = QAudio::NoError;
119
120 // Handle change of mode
121 if(audioSource && !pullMode) {
122 delete audioSource;
123 audioSource = 0;
124 }
125
126 close();
127
128 pullMode = true;
129 audioSource = device;
130
131 connect(audioSource, &QIODevice::readyRead, timer, [this] {
132 if (!timer->isActive()) {
133 timer->start(period_time / 1000);
134 }
135 });
136 deviceState = QAudio::ActiveState;
137
138 open();
139
140 emit stateChanged(deviceState);
141}
142
143QIODevice* QAlsaAudioSink::start()
144{
145 if(deviceState != QAudio::StoppedState)
146 deviceState = QAudio::StoppedState;
147
148 errorState = QAudio::NoError;
149
150 // Handle change of mode
151 if(audioSource && !pullMode) {
152 delete audioSource;
153 audioSource = 0;
154 }
155
156 close();
157
158 audioSource = new AlsaOutputPrivate(this);
159 audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered);
160 pullMode = false;
161
162 deviceState = QAudio::IdleState;
163
164 open();
165
166 emit stateChanged(deviceState);
167
168 return audioSource;
169}
170
171void QAlsaAudioSink::stop()
172{
173 if(deviceState == QAudio::StoppedState)
174 return;
175 errorState = QAudio::NoError;
176 deviceState = QAudio::StoppedState;
177 close();
178 emit stateChanged(deviceState);
179}
180
181bool QAlsaAudioSink::open()
182{
183 if(opened)
184 return true;
185
186#ifdef DEBUG_AUDIO
187 QTime now(QTime::currentTime());
188 qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()";
189#endif
190 elapsedTimeOffset = 0;
191
192 int dir;
193 int err = -1;
194 int count=0;
195 unsigned int sampleRate = m_format.sampleRate();
196
197 // Step 1: try and open the device
198 while((count < 5) && (err < 0)) {
199 err = snd_pcm_open(&handle, m_audioDevice.id().constData(), SND_PCM_STREAM_PLAYBACK, 0);
200 if(err < 0)
201 count++;
202 }
203 if (( err < 0)||(handle == 0)) {
204 errorState = QAudio::OpenError;
205 setError(errorState);
206 deviceState = QAudio::StoppedState;
207 return false;
208 }
209 snd_pcm_nonblock( handle, 0 );
210
211 // Step 2: Set the desired HW parameters.
212 snd_pcm_hw_params_alloca( &hwparams );
213
214 bool fatal = false;
215 QString errMessage;
216 unsigned int chunks = 8;
217
218 err = snd_pcm_hw_params_any( handle, hwparams );
219 if ( err < 0 ) {
220 fatal = true;
221 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_any: err = %1").arg(err);
222 }
223 if ( !fatal ) {
224 err = snd_pcm_hw_params_set_rate_resample( handle, hwparams, 1 );
225 if ( err < 0 ) {
226 fatal = true;
227 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_rate_resample: err = %1").arg(err);
228 }
229 }
230 if ( !fatal ) {
231 err = snd_pcm_hw_params_set_access( handle, hwparams, access );
232 if ( err < 0 ) {
233 fatal = true;
234 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_access: err = %1").arg(err);
235 }
236 }
237 if ( !fatal ) {
238 err = setFormat();
239 if ( err < 0 ) {
240 fatal = true;
241 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_format: err = %1").arg(err);
242 }
243 }
244 if ( !fatal ) {
245 err = snd_pcm_hw_params_set_channels(handle, hwparams,
246 (unsigned int)m_format.channelCount());
247 if ( err < 0 ) {
248 fatal = true;
249 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_channels: err = %1").arg(err);
250 }
251 }
252 if ( !fatal ) {
253 err = snd_pcm_hw_params_set_rate_near( handle, hwparams, &sampleRate, 0 );
254 if ( err < 0 ) {
255 fatal = true;
256 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_rate_near: err = %1").arg(err);
257 }
258 }
259 if ( !fatal ) {
260 unsigned int maxBufferTime = 0;
261 unsigned int minBufferTime = 0;
262 unsigned int maxPeriodTime = 0;
263 unsigned int minPeriodTime = 0;
264
265 err = snd_pcm_hw_params_get_buffer_time_max(hwparams, &maxBufferTime, &dir);
266 if ( err >= 0)
267 err = snd_pcm_hw_params_get_buffer_time_min(hwparams, &minBufferTime, &dir);
268 if ( err >= 0)
269 err = snd_pcm_hw_params_get_period_time_max(hwparams, &maxPeriodTime, &dir);
270 if ( err >= 0)
271 err = snd_pcm_hw_params_get_period_time_min(hwparams, &minPeriodTime, &dir);
272
273 if ( err < 0 ) {
274 fatal = true;
275 errMessage = QString::fromLatin1("QAudioSink: buffer/period min and max: err = %1").arg(err);
276 } else {
277 static unsigned user_buffer_time = qEnvironmentVariableIntValue("QT_ALSA_OUTPUT_BUFFER_TIME");
278 static unsigned user_period_time = qEnvironmentVariableIntValue("QT_ALSA_OUTPUT_PERIOD_TIME");
279 const bool outOfRange = maxBufferTime < buffer_time || buffer_time < minBufferTime || maxPeriodTime < period_time || minPeriodTime > period_time;
280 if (outOfRange || user_period_time || user_buffer_time) {
281 period_time = user_period_time ? user_period_time : minPeriodTime;
282 if (!user_buffer_time) {
283 chunks = maxBufferTime / period_time;
284 buffer_time = period_time * chunks;
285 } else {
286 buffer_time = user_buffer_time;
287 chunks = buffer_time / period_time;
288 }
289 }
290 qCDebug(lcAlsaOutput) << "buffer time: [" << minBufferTime << "-" << maxBufferTime << "] =" << buffer_time;
291 qCDebug(lcAlsaOutput) << "period time: [" << minPeriodTime << "-" << maxPeriodTime << "] =" << period_time;
292 qCDebug(lcAlsaOutput) << "chunks =" << chunks;
293 }
294 }
295 if ( !fatal ) {
296 err = snd_pcm_hw_params_set_buffer_time_near(handle, hwparams, &buffer_time, &dir);
297 if ( err < 0 ) {
298 fatal = true;
299 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_buffer_time_near: err = %1").arg(err);
300 }
301 }
302 if ( !fatal ) {
303 err = snd_pcm_hw_params_set_period_time_near(handle, hwparams, &period_time, &dir);
304 if ( err < 0 ) {
305 fatal = true;
306 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_period_time_near: err = %1").arg(err);
307 }
308 }
309 if ( !fatal ) {
310 err = snd_pcm_hw_params_set_periods_near(handle, hwparams, &chunks, &dir);
311 if ( err < 0 ) {
312 fatal = true;
313 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_periods_near: err = %1").arg(err);
314 }
315 }
316 if ( !fatal ) {
317 err = snd_pcm_hw_params(handle, hwparams);
318 if ( err < 0 ) {
319 fatal = true;
320 errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params: err = %1").arg(err);
321 }
322 }
323 if( err < 0) {
324 qWarning()<<errMessage;
325 errorState = QAudio::OpenError;
326 setError(errorState);
327 deviceState = QAudio::StoppedState;
328 return false;
329 }
330 snd_pcm_hw_params_get_buffer_size(hwparams,&buffer_frames);
331 buffer_size = snd_pcm_frames_to_bytes(handle,buffer_frames);
332 snd_pcm_hw_params_get_period_size(hwparams,&period_frames, &dir);
333 period_size = snd_pcm_frames_to_bytes(handle,period_frames);
334 snd_pcm_hw_params_get_buffer_time(hwparams,&buffer_time, &dir);
335 snd_pcm_hw_params_get_period_time(hwparams,&period_time, &dir);
336
337 // Step 3: Set the desired SW parameters.
338 snd_pcm_sw_params_t *swparams;
339 snd_pcm_sw_params_alloca(&swparams);
340 snd_pcm_sw_params_current(handle, swparams);
341 snd_pcm_sw_params_set_start_threshold(handle,swparams,period_frames);
342 snd_pcm_sw_params_set_stop_threshold(handle,swparams,buffer_frames);
343 snd_pcm_sw_params_set_avail_min(handle, swparams,period_frames);
344 snd_pcm_sw_params(handle, swparams);
345
346 // Step 4: Prepare audio
347 if(audioBuffer == 0)
348 audioBuffer = new char[snd_pcm_frames_to_bytes(handle,buffer_frames)];
349 snd_pcm_prepare( handle );
350 snd_pcm_start(handle);
351
352 // Step 5: Setup timer
353 bytesAvailable = bytesFree();
354
355 // Step 6: Start audio processing
356 timer->start(period_time/1000);
357
358 elapsedTimeOffset = 0;
359 errorState = QAudio::NoError;
360 totalTimeValue = 0;
361 opened = true;
362
363 return true;
364}
365
366void QAlsaAudioSink::close()
367{
368 timer->stop();
369
370 if ( handle ) {
371 snd_pcm_drain( handle );
372 snd_pcm_close( handle );
373 handle = 0;
374 delete [] audioBuffer;
375 audioBuffer=0;
376 }
377 if(!pullMode && audioSource) {
378 delete audioSource;
379 audioSource = 0;
380 }
381 opened = false;
382}
383
384qsizetype QAlsaAudioSink::bytesFree() const
385{
386 if(resuming)
387 return period_size;
388
389 if(deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState)
390 return 0;
391
392 int frames = snd_pcm_avail_update(handle);
393 if (frames == -EPIPE) {
394 // Try and handle buffer underrun
395 int err = snd_pcm_recover(handle, frames, 0);
396 if (err < 0)
397 return 0;
398 else
399 frames = snd_pcm_avail_update(handle);
400 } else if (frames < 0) {
401 return 0;
402 }
403
404 if ((int)frames > (int)buffer_frames)
405 frames = buffer_frames;
406
407 return snd_pcm_frames_to_bytes(handle, frames);
408}
409
410qint64 QAlsaAudioSink::write( const char *data, qint64 len )
411{
412 // Write out some audio data
413 if ( !handle )
414 return 0;
415#ifdef DEBUG_AUDIO
416 qDebug()<<"frames to write out = "<<
417 snd_pcm_bytes_to_frames( handle, (int)len )<<" ("<<len<<") bytes";
418#endif
419 int frames, err;
420 int space = bytesFree();
421
422 if (!space)
423 return 0;
424
425 if (len < space)
426 space = len;
427
428 frames = snd_pcm_bytes_to_frames(handle, space);
429
430 if (volume() < 1.0f) {
431 QVarLengthArray<char, 4096> out(space);
432 QAudioHelperInternal::qMultiplySamples(volume(), m_format, data, out.data(), space);
433 err = snd_pcm_writei(handle, out.constData(), frames);
434 } else {
435 err = snd_pcm_writei(handle, data, frames);
436 }
437
438 if(err > 0) {
439 totalTimeValue += err;
440 resuming = false;
441 errorState = QAudio::NoError;
442 if (deviceState != QAudio::ActiveState) {
443 deviceState = QAudio::ActiveState;
444 emit stateChanged(deviceState);
445 }
446 return snd_pcm_frames_to_bytes( handle, err );
447 } else
448 err = xrun_recovery(err);
449
450 if(err < 0) {
451 close();
452 errorState = QAudio::FatalError;
453 setError(errorState);
454 deviceState = QAudio::StoppedState;
455 emit stateChanged(deviceState);
456 }
457 return 0;
458}
459
460void QAlsaAudioSink::setBufferSize(qsizetype value)
461{
462 if(deviceState == QAudio::StoppedState)
463 buffer_size = value;
464}
465
466qsizetype QAlsaAudioSink::bufferSize() const
467{
468 return buffer_size;
469}
470
471qint64 QAlsaAudioSink::processedUSecs() const
472{
473 return qint64(1000000) * totalTimeValue / m_format.sampleRate();
474}
475
476void QAlsaAudioSink::resume()
477{
478 if(deviceState == QAudio::SuspendedState) {
479 int err = 0;
480
481 if(handle) {
482 err = snd_pcm_prepare( handle );
483 if(err < 0)
484 xrun_recovery(err);
485
486 err = snd_pcm_start(handle);
487 if(err < 0)
488 xrun_recovery(err);
489
490 bytesAvailable = (int)snd_pcm_frames_to_bytes(handle, buffer_frames);
491 }
492 resuming = true;
493
494 deviceState = suspendedInState;
495 errorState = QAudio::NoError;
496 timer->start(period_time/1000);
497 emit stateChanged(deviceState);
498 }
499}
500
501void QAlsaAudioSink::suspend()
502{
503 if(deviceState == QAudio::ActiveState || deviceState == QAudio::IdleState || resuming) {
504 suspendedInState = deviceState;
505 snd_pcm_drain(handle);
506 timer->stop();
507 deviceState = QAudio::SuspendedState;
508 errorState = QAudio::NoError;
509 emit stateChanged(deviceState);
510 }
511}
512
513void QAlsaAudioSink::userFeed()
514{
515 if(deviceState == QAudio::StoppedState || deviceState == QAudio::SuspendedState)
516 return;
517#ifdef DEBUG_AUDIO
518 QTime now(QTime::currentTime());
519 qDebug()<<now.second()<<"s "<<now.msec()<<"ms :userFeed() OUT";
520#endif
521 if(deviceState == QAudio::IdleState)
522 bytesAvailable = bytesFree();
523
524 deviceReady();
525}
526
527bool QAlsaAudioSink::deviceReady()
528{
529 if(pullMode) {
530 int l = 0;
531 int chunks = bytesAvailable/period_size;
532 if(chunks==0) {
533 bytesAvailable = bytesFree();
534 return false;
535 }
536#ifdef DEBUG_AUDIO
537 qDebug()<<"deviceReady() avail="<<bytesAvailable<<" bytes, period size="<<period_size<<" bytes";
538 qDebug()<<"deviceReady() no. of chunks that can fit ="<<chunks<<", chunks in bytes ="<<period_size*chunks;
539#endif
540 int input = period_frames*chunks;
541 if(input > (int)buffer_frames)
542 input = buffer_frames;
543 l = audioSource->read(audioBuffer,snd_pcm_frames_to_bytes(handle, input));
544
545 // reading can take a while and stream may have been stopped
546 if (!handle)
547 return false;
548
549 if(l > 0) {
550 // Got some data to output
551 if (deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState)
552 return true;
553 qint64 bytesWritten = write(audioBuffer,l);
554 if (bytesWritten != l)
555 audioSource->seek(audioSource->pos()-(l-bytesWritten));
556 bytesAvailable = bytesFree();
557
558 } else if(l == 0) {
559 // Did not get any data to output
560 timer->stop();
561 snd_pcm_drain(handle);
562 bytesAvailable = bytesFree();
563 if(bytesAvailable > snd_pcm_frames_to_bytes(handle, buffer_frames-period_frames)) {
564 // Underrun
565 if (deviceState != QAudio::IdleState) {
566 errorState = audioSource->atEnd() ? QAudio::NoError : QAudio::UnderrunError;
567 setError(errorState);
568 deviceState = QAudio::IdleState;
569 emit stateChanged(deviceState);
570 }
571 }
572
573 } else if(l < 0) {
574 close();
575 deviceState = QAudio::StoppedState;
576 errorState = QAudio::IOError;
577 setError(errorState);
578 emit stateChanged(deviceState);
579 }
580 } else {
581 bytesAvailable = bytesFree();
582 if(bytesAvailable > snd_pcm_frames_to_bytes(handle, buffer_frames-period_frames)) {
583 // Underrun
584 if (deviceState != QAudio::IdleState) {
585 errorState = QAudio::UnderrunError;
586 setError(errorState);
587 deviceState = QAudio::IdleState;
588 emit stateChanged(deviceState);
589 }
590 }
591 }
592
593 if(deviceState != QAudio::ActiveState)
594 return true;
595
596 return true;
597}
598
599void QAlsaAudioSink::reset()
600{
601 if(handle)
602 snd_pcm_reset(handle);
603
604 stop();
605}
606
607AlsaOutputPrivate::AlsaOutputPrivate(QAlsaAudioSink* audio)
608{
609 audioDevice = qobject_cast<QAlsaAudioSink*>(audio);
610}
611
613
614qint64 AlsaOutputPrivate::readData( char* data, qint64 len)
615{
616 Q_UNUSED(data);
617 Q_UNUSED(len);
618
619 return 0;
620}
621
622qint64 AlsaOutputPrivate::writeData(const char* data, qint64 len)
623{
624 int retry = 0;
625 qint64 written = 0;
626 if((audioDevice->deviceState == QAudio::ActiveState)
627 ||(audioDevice->deviceState == QAudio::IdleState)) {
628 while(written < len) {
629 int chunk = audioDevice->write(data+written,(len-written));
630 if(chunk <= 0)
631 retry++;
632 written+=chunk;
633 if(retry > 10)
634 return written;
635 }
636 }
637 return written;
638
639}
640
641QT_END_NAMESPACE
642
643#include "moc_qalsaaudiosink_p.cpp"
qint64 writeData(const char *data, qint64 len) override
Writes up to maxSize bytes from data to the device.
qint64 readData(char *data, qint64 len) override
Reads up to maxSize bytes from the device into data, and returns the number of bytes read or -1 if an...