MythTV master
audiooutputgraph.cpp
Go to the documentation of this file.
1// Std
2#include <climits>
3#include <cmath>
4#include <cstdint>
5#include <thread>
6
7// Qt
8#include <QtGlobal>
9#include <QImage>
10#include <QByteArray>
11#include <QPair>
12#include <QMutexLocker>
13
14// MythTV
15#include "libmythbase/compat.h"
17#include "libmythui/mythimage.h"
19
20#include "audiooutputgraph.h"
21
22using namespace std::chrono_literals;
23
24#define LOC QString("AOG: ")
25
26const int kBufferMilliSecs = 500;
27
28class AudioOutputGraph::AOBuffer : public QByteArray
29{
30 public:
31 AOBuffer() = default;
32
35 static int BitsPerChannel() { return sizeof(short) * CHAR_BIT; }
36 int Channels() const { return m_channels; }
37 std::chrono::milliseconds Next() const { return m_tcNext; }
38 std::chrono::milliseconds First() const { return m_tcFirst; }
39
40 using Range = std::pair<std::chrono::milliseconds, std::chrono::milliseconds>;
41
42 // Determine the range of sound samples to use, from first to second, centered
43 // around the requested timecode. The range is clipped against the first and
44 // last timecode of all the sound samples that are available.
45 // It is possible that requested Timecode is not in the resulting range.
46 Range Avail(std::chrono::milliseconds Timecode) const
47 {
48 if (Timecode == 0ms || Timecode == -1ms)
49 Timecode = m_tcNext;
50
51 std::chrono::milliseconds first = Timecode - Samples2MS(m_maxSamples / 2);
52 if (first < m_tcFirst)
53 first = m_tcFirst;
54
55 std::chrono::milliseconds second = first + Samples2MS(m_maxSamples);
56 if (second > m_tcNext)
57 {
58 second = m_tcNext;
59 if (second < first + Samples2MS(m_maxSamples))
60 {
61 first = second - Samples2MS(m_maxSamples);
62 if (first < m_tcFirst)
63 first = m_tcFirst;
64 }
65 }
66 return {first, second};
67 }
68
69 // All samples to be shown in the graph are present
70 bool EnoughData(std::chrono::milliseconds Timecode)
71 {
72 // return m_tcNext >= Timecode + Samples2MS(m_maxSamples / 2);
73 return (m_tcFirst <= Timecode) && (m_tcNext >= Timecode + Samples2MS(m_maxSamples / 2));
74 }
75
76 // Number of samples to be shown in the graph
77 int Samples(Range Available) const
78 {
79 return MS2Samples(Available.second - Available.first);
80 }
81
82 // Reset all values and release sample buffer
83 void Empty()
84 {
85 m_tcFirst = 0ms;
86 m_tcNext = 0ms;
87 m_bits = 0;
88 m_channels = 0;
89 resize(0);
90 }
91
92 void Append(const void * Buffer, unsigned long Length, std::chrono::milliseconds Timecode,
93 int Channels, int Bits)
94 {
95 if (m_bits != Bits || m_channels != Channels)
96 {
97 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("Length:%1 Timecode:%2 Channels:%3 Bits:%4")
98 .arg(Length).arg(Timecode.count()).arg(Channels).arg(Bits));
99 Resize(Channels, Bits);
100 m_tcFirst = Timecode;
101 m_tcNext = Timecode;
102 }
103
104 auto samples = Bytes2Samples(static_cast<unsigned>(Length));
105 std::chrono::milliseconds tcNext = Timecode + Samples2MS(samples);
106
107 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
108 QString("m_tcFirst:%1 m_tcNext:%2 Timecode:%3 samples:%4 ms:%5")
109 .arg(m_tcFirst.count()).arg(m_tcNext.count()).arg(Timecode.count())
110 .arg(samples).arg(Samples2MS(samples).count()));
111
112 if (qAbs((Timecode - m_tcNext).count()) <= 1)
113 {
114 Append(Buffer, Length, Bits);
115 m_tcNext = tcNext;
116 }
117 else if (Timecode >= m_tcFirst && tcNext <= m_tcNext)
118 {
119 // Duplicate
120 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("Duplicate"));
121 return;
122 }
123 else
124 {
125 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("Discontinuity %1 -> %2")
126 .arg(m_tcNext.count()).arg(Timecode.count()));
127
128 Resize(Channels, Bits);
129 Append(Buffer, Length, Bits);
130 m_tcFirst = Timecode;
131 m_tcNext = tcNext;
132 }
133
134 auto overflow = size() - m_sizeMax;
135
136 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("Overflow:%1 size:%2 m_sizeMax:%3")
137 .arg(overflow).arg(size()).arg(m_sizeMax));
138
139 if (overflow > 0)
140 {
141 remove(0, overflow);
142
143 auto old_m_tcFirst = m_tcFirst;
145 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
146 QString("Overflow:%1 m_tcFirst old, new, delta:%2 %3 %4")
147 .arg(overflow).arg(old_m_tcFirst.count()).arg(m_tcFirst.count())
148 .arg((m_tcFirst - old_m_tcFirst).count()));
149 }
150 }
151
152 // Pointer to first sample to be shown in the graph
153 const int16_t* Data16(Range Available) const
154 {
155 auto start = MS2Samples(Available.first - m_tcFirst);
156 return reinterpret_cast<const int16_t*>(constData() + (static_cast<ptrdiff_t>(start) * BytesPerSampleNormalized()));
157 }
158
159 protected:
160
161 // Bytes per sample as received; one value for one channel can be 8, 16 or 32 bits.
163 {
164 return static_cast<uint>(m_channels * ((m_bits + 7) / 8));
165 }
166
167 unsigned Bytes2Samples(unsigned Bytes) const
168 {
169 return (m_channels && m_bits) ? Bytes / BytesPerSample() : 0;
170 }
171
172 // Bytes per sample as stored; one value for one channel is always 16 bits
174 {
175 return static_cast<uint>(m_channels * sizeof(int16_t));
176 }
177
178 unsigned Bytes2SamplesNormalized(unsigned Bytes) const
179 {
180 return (m_channels && m_bits) ? Bytes / BytesPerSampleNormalized() : 0;
181 }
182
183 std::chrono::milliseconds Samples2MS(unsigned Samples) const
184 {
185 return m_sampleRate ? std::chrono::milliseconds((Samples * 1000UL + m_sampleRate - 1) / m_sampleRate) : 0ms; // round up
186 }
187
188 int MS2Samples(std::chrono::milliseconds Msecs) const
189 {
190 return Msecs > 0ms ? static_cast<int>((Msecs.count() * m_sampleRate) / 1000) : 0; // NB round down
191 }
192
193 // Store all received samples but convert the sample from 8, 16 or 32 bits to 16 bits.
194
195 // All arithmetic that refers to samples stored inside (in the QByteArray that we inherit from)
196 // must be done with the ..Normalized functions which use the fixed size of 16 bits.
197 // Arithmetic on the incoming data must use the number of bits per sample (8, 16 or 32).
198 void Append(const void * Buffer, unsigned long Length, int Bits)
199 {
200 switch (Bits)
201 {
202 case 8:
203 // Convert 8bit unsigned to 16bit signed
204 {
205 auto count = Length;
206 auto n = size();
207 resize(n + static_cast<int>(sizeof(int16_t) * count));
208 const auto * src = reinterpret_cast<const uchar*>(Buffer);
209 auto * dst = reinterpret_cast<int16_t*>(data() + n);
210 while (count--)
211 *dst++ = static_cast<int16_t>((static_cast<int16_t>(*src++) - CHAR_MAX) << (16 - CHAR_BIT));
212 }
213 break;
214 case 16:
215 // Copy 16bit samples without conversion
216 append(reinterpret_cast<const char*>(Buffer), static_cast<int>(Length));
217 break;
218 case 32:
219 // Convert 32bit float to 16bit signed
220 {
221 unsigned long count = Length / sizeof(float);
222 auto n = size();
223 resize(n + static_cast<int>(sizeof(int16_t) * count));
224 const float f((1 << 15) - 1);
225 const auto * src = reinterpret_cast<const float*>(Buffer);
226 auto * dst = reinterpret_cast<int16_t*>(data() + n);
227 while (count--)
228 *dst++ = static_cast<int16_t>(f * (*src++));
229 }
230 break;
231 default:
232 append(reinterpret_cast<const char*>(Buffer), static_cast<int>(Length));
233 break;
234 }
235 }
236
237 private:
238 void Resize(int Channels, int Bits)
239 {
240 m_bits = Bits;
242 m_sizeMax = static_cast<int>(((m_sampleRate * kBufferMilliSecs) / 1000) * BytesPerSampleNormalized());
243 resize(0);
244 }
245
246 private:
247 std::chrono::milliseconds m_tcFirst { 0ms };
248 std::chrono::milliseconds m_tcNext { 0ms };
251 int m_bits { 0 };
252 int m_channels { 0 };
253 int m_sizeMax { 0 };
254};
255
257 m_buffer(new AudioOutputGraph::AOBuffer())
258{
259}
260
262{
263 delete m_buffer;
264 if (m_image)
265 m_image->DecrRef();
266}
267
269{
270 QMutexLocker lock(&m_mutex);
271 m_painter = Painter;
273}
274
276{
277 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("Set sample rate %1").arg(SampleRate));
278 QMutexLocker lock(&m_mutex);
280}
281
283{
284 LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("Set sample count %1").arg(SampleCount));
285 QMutexLocker lock(&m_mutex);
286 m_buffer->SetMaxSamples(SampleCount);
287}
288
290{
291}
292
293void AudioOutputGraph::add(const void * Buffer, unsigned long Length,
294 std::chrono::milliseconds Timecode, int Channnels, int Bits)
295{
296 QMutexLocker lock(&m_mutex);
297 m_buffer->Append(Buffer, Length, Timecode, Channnels, Bits);
298}
299
301{
302 LOG(VB_PLAYBACK, LOG_INFO, LOC + "Reset");
303 QMutexLocker lock(&m_mutex);
304 m_buffer->Empty();
305}
306
307MythImage* AudioOutputGraph::GetImage(std::chrono::milliseconds Timecode) const
308{
309 // Needed to erase a previous image if there is no audio data available.
310 auto EmptyImage = [this]() {
311 QImage image(8, 8, QImage::Format_ARGB32);
312 image.fill(0);
313 if (m_image)
314 m_image->Assign(image);
315 return m_image;
316 };
317
318 QMutexLocker lock(&m_mutex);
319 AOBuffer::Range avail = m_buffer->Avail(Timecode);
320
321 // Wait until all audio data has arrived for our requested timecode.
322 bool data_present = false;
323 for (int i=0; i<10 && !data_present; i++)
324 {
325 if (m_buffer->EnoughData(Timecode))
326 {
327 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
328 QString("Audio data for timecode %1 arrived after %2 wait")
329 .arg(Timecode.count()).arg(i));
330 data_present = true;
331 }
332 else
333 {
334 lock.unlock();
335 std::this_thread::sleep_for(20ms);
336 avail = m_buffer->Avail(Timecode);
337 lock.relock();
338 }
339 }
340
341 if (!data_present)
342 {
343 LOG(VB_PLAYBACK, LOG_DEBUG, LOC +
344 QString("No audio data for timecode %1")
345 .arg(Timecode.count()));
346 return EmptyImage();
347 }
348
349 LOG(VB_PLAYBACK, LOG_INFO, LOC +
350 QString("GetImage for timecode %1 using [%2..%3] available [%4..%5]")
351 .arg(Timecode.count()).arg(avail.first.count()).arg(avail.second.count())
352 .arg(m_buffer->First().count()).arg(m_buffer->Next().count()) );
353
354 auto width = m_buffer->Samples(avail);
355 if (width <= 0)
356 {
357 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
358 QString("GetImage for timecode %1 SKIPPED because width <= 0").arg(Timecode.count()));
359 return EmptyImage();
360 }
361
362 const unsigned range = 1U << AudioOutputGraph::AOBuffer::BitsPerChannel();
363 const auto threshold = 20 * log10(1.0 / range); // 16bit=-96.3296dB => ~6dB/bit
364 auto height = static_cast<int>(-ceil(threshold)); // 96
365 if (height <= 0)
366 {
367 LOG(VB_PLAYBACK, LOG_WARNING, LOC +
368 QString("GetImage for timecode %1 SKIPPED because height <= 0").arg(Timecode.count()));
369 return EmptyImage();
370 }
371
372 const int channels = m_buffer->Channels();
373
374 // Start position of audio data has to be before max (the end of the buffer)
375 const auto * data = m_buffer->Data16(avail);
376 const auto * max = reinterpret_cast<const int16_t*>(m_buffer->constData() + m_buffer->size());
377 if (data >= max)
378 {
379 LOG(VB_PLAYBACK, LOG_ERR, LOC +
380 QString("GetImage for timecode %1 SKIPPED because data starts beyond end of buffer")
381 .arg(Timecode.count()));
382 return EmptyImage();
383 }
384
385 // End position of audio data cannot extend beyond max (the end of the buffer)
386 if ((data + (channels * static_cast<ptrdiff_t>(width))) > max)
387 {
388 LOG(VB_PLAYBACK, LOG_WARNING, LOC + "Buffer overflow. Clipping samples.");
389 width = static_cast<int>(max - data) / channels;
390 }
391
392 QImage image(width, height, QImage::Format_ARGB32);
393 image.fill(0);
394
395 for (int x = 0; x < width; ++x)
396 {
397 auto left = data[0];
398 auto right = channels > 1 ? data[1] : left;
399 data += channels;
400
401 auto avg = qAbs(left) + qAbs(right);
402 double db = 20 * log10(static_cast<double>(avg ? avg : 1) / range);
403 auto idb = static_cast<int>(ceil(db));
404 auto rgb { qRgb(255, 0, 0) };
405 if (idb <= m_dBsilence)
406 rgb = qRgb(255, 255, 255);
407 else if (idb <= m_dBquiet)
408 rgb = qRgb( 0, 255, 255);
409 else if (idb <= m_dBLoud)
410 rgb = qRgb( 0, 255, 0);
411 else if (idb <= m_dbMax)
412 rgb = qRgb(255, 255, 0);
413
414 int v = height - static_cast<int>(height * (db / threshold));
415 if (v >= height)
416 v = height - 1;
417 for (int y = 0; y <= v; ++y)
418 image.setPixel(x, height - 1 - y, rgb);
419 }
420
421 if (m_image)
422 m_image->Assign(image);
423 return m_image;
424}
#define LOC
const int kBufferMilliSecs
void Append(const void *Buffer, unsigned long Length, std::chrono::milliseconds Timecode, int Channels, int Bits)
bool EnoughData(std::chrono::milliseconds Timecode)
std::chrono::milliseconds m_tcNext
std::chrono::milliseconds Samples2MS(unsigned Samples) const
const int16_t * Data16(Range Available) const
void Resize(int Channels, int Bits)
Range Avail(std::chrono::milliseconds Timecode) const
void Append(const void *Buffer, unsigned long Length, int Bits)
std::chrono::milliseconds First() const
int Samples(Range Available) const
std::pair< std::chrono::milliseconds, std::chrono::milliseconds > Range
unsigned Bytes2SamplesNormalized(unsigned Bytes) const
void SetMaxSamples(uint16_t Samples)
std::chrono::milliseconds Next() const
int MS2Samples(std::chrono::milliseconds Msecs) const
std::chrono::milliseconds m_tcFirst
unsigned Bytes2Samples(unsigned Bytes) const
void SetSampleRate(uint16_t SampleRate)
MythImage * m_image
~AudioOutputGraph() override
MythImage * GetImage(std::chrono::milliseconds Timecode) const
void prepare() override
void SetSampleRate(uint16_t SampleRate)
void SetSampleCount(uint16_t SampleCount)
void SetPainter(MythPainter *Painter)
AOBuffer *const m_buffer
void add(const void *Buffer, unsigned long Length, std::chrono::milliseconds Timecode, int Channnels, int Bits) override
MythPainter * m_painter
void Assign(const QImage &img)
Definition: mythimage.cpp:77
int DecrRef(void) override
Decrements reference count and deletes on 0.
Definition: mythimage.cpp:52
static const std::array< const uint64_t, 4 > samples
Definition: element.cpp:46
unsigned int uint
Definition: freesurround.h:24
unsigned short uint16_t
Definition: iso6937tables.h:3
static uint32_t avg(uint32_t A, uint32_t B)
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39