展示实时音频数据的简单应用程序。
The audiolevels example shows how feed real-time dynamic data to a graph using Q3DBars .
This example reads the audio levels from a microphone and displays those levels in a bar graph. To increase the load for demonstration purposes, and to make the graph little fancier, slightly modified data is used to fill multiple rows.
The interesting stuff happens in
AudioLevels
and
AudioLevelsIODevice
classes, so we concentrate on those and skip explaining the basic
Q3DBars
functionality - for that see
条形范例
.
要运行范例从 Qt Creator ,打开 欢迎 模式,然后选择范例从 范例 。更多信息,拜访 构建和运行范例 .
AudioLevelsIODevice
subclasses
QIODevice
and is given as input device for
QAudioInput
class, so it receives microphone data.
In the header file for
AudioLevels
class we declare necessary members:
Q3DBars *m_graph; AudioLevelsIODevice *m_device; QAudioInput *m_audioInput;
And initialize the microphone listening in the source:
QAudioDeviceInfo inputDevice = QAudioDeviceInfo::defaultInputDevice(); if (inputDevice.supportedSampleRates().size() > 0 && inputDevice.supportedChannelCounts().size() > 0 && inputDevice.supportedSampleSizes().size() > 0 && inputDevice.supportedCodecs().size() > 0) { QAudioFormat formatAudio; formatAudio.setSampleRate(inputDevice.supportedSampleRates().at(0)); formatAudio.setChannelCount(inputDevice.supportedChannelCounts().at(0)); formatAudio.setSampleSize(inputDevice.supportedSampleSizes().at(0)); formatAudio.setCodec(inputDevice.supportedCodecs().at(0)); formatAudio.setByteOrder(QAudioFormat::LittleEndian); formatAudio.setSampleType(QAudioFormat::UnSignedInt); m_audioInput = new QAudioInput(inputDevice, formatAudio, this); #ifdef Q_OS_MAC // OS X seems to wait for entire buffer to fill before calling writeData, so use smaller buffer m_audioInput->setBufferSize(256); #else m_audioInput->setBufferSize(1024); #endif m_device = new AudioLevelsIODevice(m_graph->seriesList().at(0)->dataProxy(), this); m_device->open(QIODevice::WriteOnly); m_audioInput->start(m_device); } else { // No graph content can be shown, so add a custom warning label QCustom3DLabel *titleLabel = new QCustom3DLabel("No valid audio input device found", QFont(), QVector3D(0.2f, 0.2f, 0.0f), QVector3D(1.0f, 1.0f, 0.0f), QQuaternion()); titleLabel->setPositionAbsolute(true); titleLabel->setFacingCamera(true); m_graph->addCustomItem(titleLabel); }
In the header file for
AudioLevelsIODevice
class we store pointers to the data proxy and also the data array we give to the proxy, because we reuse the same array to keep memory reallocations to the minimum:
QBarDataProxy *m_proxy; QBarDataArray *m_array;
In the source file we define some static constants to define size of the data array and the middle row index, as well as the resolution of the visualization. You may need to adjust these values to get decent performance in low-end devices:
static const int resolution = 8; static const int rowSize = 800; static const int rowCount = 7; // Must be odd number static const int middleRow = rowCount / 2;
The
resolution
constant indicates the sample rate, for example, value 8 means every eighth byte from audio input is visualized. This is necessary to make the data readable, as it would otherwise make the graph scroll too fast.
在
AudioLevelsIODevice
class constructor we initialize the data array:
m_array->reserve(rowCount); for (int i = 0; i < rowCount; i++) m_array->append(new QBarDataRow(rowSize));
The
AudioLevelsIODevice::writeData
function is called whenever there is new audio data available to be visualized. There we move the old data along the rows and insert new data in the beginning of the rows:
qint64 AudioLevelsIODevice::writeData(const char *data, qint64 maxSize) { // The amount of new data available. int newDataSize = maxSize / resolution; // If we get more data than array size, we need to adjust the start index for new data. int newDataStartIndex = qMax(0, (newDataSize - rowSize)); // Move the old data ahead in the rows (only do first half of rows + middle one now). // If the amount of new data was larger than row size, skip copying. if (!newDataStartIndex) { for (int i = 0; i <= middleRow; i++) { QBarDataItem *srcPos = m_array->at(i)->data(); QBarDataItem *dstPos = srcPos + newDataSize; memmove((void *)dstPos, (void *)srcPos, (rowSize - newDataSize) * sizeof(QBarDataItem)); } } // Insert data in reverse order, so that newest data is always at the front of the row. int index = 0; for (int i = newDataSize - 1; i >= newDataStartIndex; i--) { // Add 0.01 to the value to avoid gaps in the graph (i.e. zero height bars). // Also, scale to 0...100 float value = float(quint8(data[resolution * i]) - 128) / 1.28f + 0.01f; (*m_array->at(middleRow))[index].setValue(value); // Insert a fractional value into front half of the rows. for (int j = 1; j <= middleRow; j++) { float fractionalValue = value / float(j + 1); (*m_array->at(middleRow - j))[index].setValue(fractionalValue); } index++; } // Copy the front half of rows to the back half for symmetry. index = 0; for (int i = rowCount - 1; i > middleRow; i--) { QBarDataItem *srcPos = m_array->at(index++)->data(); QBarDataItem *dstPos = m_array->at(i)->data(); memcpy((void *)dstPos, (void *)srcPos, rowSize * sizeof(QBarDataItem)); } // Reset the proxy array now that data has been updated to trigger a redraw. m_proxy->resetArray(m_array); return maxSize; }
We use a couple of techniques here to improve performance. First, we reuse the existing data array, as this allows us to avoid any extra memory allocations in our application code. This also means the data array dimensions do not change, which further improves efficiency in the bar graph renderer. Secondly, since each row is a
QVector
of bar data items, which do not allocate any data that needs deletion, we can utilize
memmove
and
memcpy
functions to quickly move and copy data around.
注意:
In the future versions of Qt Data Visualization,
QBarDataItem
might get extended so that it does allocate some memory to store other optional bar properties besides the value. In use cases where those optional properties are used, using
memmove
and
memcpy
could lead to memory leaks, so use them with care.