#c #windows #audio #wasapi
#c #Windows #Аудио #wasapi
Вопрос:
Я пытаюсь визуализировать звуковую волну, захваченную циклом WASAPI, но обнаружил, что записываемые мной пакеты не образуют плавную волну при объединении.
Мое понимание того, как работает клиент захвата WASAPI, заключается в том, что при вызове pCaptureClient->GetBuffer(amp;pData, amp;numFramesAvailable, amp;flags, NULL, NULL)
буфер pData
заполняется спереди numFramesAvailable
точками данных. Каждая точка данных является плавающей, и они чередуются по каналам. Таким образом, чтобы получить все доступные точки данных, я должен привести pData
к указателю с плавающей точкой и принять первые channels * numFramesAvailable
значения. Как только я освобождаю буфер и вызываю GetBuffer
снова, он предоставляет следующий пакет. Я бы предположил, что эти пакеты будут следовать друг за другом, но, похоже, это не так.
Я предполагаю, что либо я делаю неверное предположение о формате аудиоданных, pData
либо в клиенте захвата отсутствуют или перекрываются кадры. Но понятия не имею, как это проверить.
Чтобы сделать приведенный ниже код как можно более кратким, я удалил такие вещи, как проверка состояния ошибки и очистка.
Инициализация клиента захвата:
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
pAudioClient = NULL;
IMMDeviceEnumerator * pDeviceEnumerator = NULL;
IMMDevice * pDeviceEndpoint = NULL;
IAudioClient *pAudioClient = NULL;
IAudioCaptureClient *pCaptureClient = NULL;
int channels;
// Initialize audio device endpoint
CoInitialize(nullptr);
CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)amp;pDeviceEnumerator );
pDeviceEnumerator ->GetDefaultAudioEndpoint(eRender, eConsole, amp;pDeviceEndpoint );
// init audio client
WAVEFORMATEX *pwfx = NULL;
REFERENCE_TIME hnsRequestedDuration = 10000000;
REFERENCE_TIME hnsActualDuration;
audio_device_endpoint->Activate(IID_IAudioClient, CLSCTX_ALL, NULL, (void**)amp;pAudioClient);
pAudioClient->GetMixFormat(amp;pwfx);
pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, hnsRequestedDuration, 0, pwfx, NULL);
channels = pwfx->nChannels;
pAudioClient->GetService(IID_IAudioCaptureClient, (void**)amp;pCaptureClient);
pAudioClient->Start(); // Start recording.
Захват пакетов (обратите внимание, что std::mutex packet_buffer_mutex
и vector<vector<float>> packet_buffer
уже определены и используются другим потоком для безопасного отображения данных):
UINT32 packetLength = 0;
BYTE *pData = NULL;
UINT32 numFramesAvailable;
DWORD flags;
int max_packets = 8;
std::unique_lock<std::mutex>write_guard(packet_buffer_mutex, std::defer_lock);
while (true) {
pCaptureClient->GetNextPacketSize(amp;packetLength);
while (packetLength != 0)
{
// Get the available data in the shared buffer.
pData = NULL;
pCaptureClient->GetBuffer(amp;pData, amp;numFramesAvailable, amp;flags, NULL, NULL);
if (flags amp; AUDCLNT_BUFFERFLAGS_SILENT)
{
pData = NULL; // Tell CopyData to write silence.
}
write_guard.lock();
if (packet_buffer.size() == max_packets) {
packet_buffer.pop_back();
}
if (pData) {
float * pfData = (float*)pData;
packet_buffer.emplace(packet_buffer.begin(), pfData, pfData channels * numFramesAvailable);
} else {
packet_buffer.emplace(packet_buffer.begin());
}
write_guard.unlock();
hpCaptureClient->ReleaseBuffer(numFramesAvailable);
pCaptureClient->GetNextPacketSize(amp;packetLength);
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
Я сохраняю пакеты в vector<vector<float>>
(где каждый vector<float>
является пакетом), удаляя последний и вставляя самый новый в начале, чтобы я мог перебирать их по порядку.
Ниже приведен результат захваченной синусоидальной волны, отображающий переменные значения, поэтому он представляет только один канал. Понятно, где пакеты сшиваются вместе.
Комментарии:
1. Похоже, вы скопировали код отсюда . Вызов SetFormat() отсутствует, что плохо.
2. разве SetFormat не является просто определяемой пользователем функцией, которая сообщает, как копировать данные, с чем я справляюсь сам, когда конвертирую пакет в a
vector<float>
?3. Как часто вы
AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY
возвращаетеflags
Ответ №1:
Что-то воспроизводит синусоидальную волну в Windows; вы записываете синусоидальную волну обратно в аудиообмотке; и синусоидальная волна, которую вы получаете обратно, на самом деле не является синусоидальной волной.
Вы почти наверняка сталкиваетесь с сбоями. Наиболее вероятными причинами сбоев являются:
- Все, что воспроизводит синусоидальную волну для Windows, не получает данные в Windows вовремя, поэтому буфер иссякает.
- Все, что считывает данные обратной связи из Windows, не считывает данные вовремя, поэтому буфер заполняется.
- Что-то идет не так между воспроизведением синусоидальной волны в Windows и чтением ее обратно.
Возможно, что происходит более одного из них.
Вызов IAudioCaptureClient::GetBuffer сообщит вам, если вы прочитали данные слишком поздно. В частности, он будет установлен *pdwFlags
так, чтобы AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY
бит был установлен.
Глядя на ваш код, я вижу, что вы выполняете следующие действия между GetBuffer и WriteBuffer:
- Ожидание на замке
- Иногда выполнение чего-то, называемого «pop_back»
- Выполнение чего-то, что называется «emplace»
Я цитирую из документации, связанной выше:
Клиенты должны избегать чрезмерных задержек между вызовом GetBuffer, который получает пакет, и вызовом ReleaseBuffer, который освобождает пакет. Реализация аудиооборудования предполагает, что вызов GetBuffer и соответствующий вызов ReleaseBuffer происходят в течение одного и того же периода обработки буфера. Клиенты, которые задерживают отправку пакета более чем на один период, рискуют потерять образцы данных.
В частности, вы НИКОГДА не должны ВЫПОЛНЯТЬ КАКИЕ-ЛИБО ИЗ СЛЕДУЮЩИХ ДЕЙСТВИЙ между GetBuffer
и ReleaseBuffer
, потому что в конечном итоге они вызовут сбой:
- Подождите на замке
- Подождите выполнения любой другой операции
- Чтение из файла или запись в файл
- Выделение памяти
Вместо этого предварительно выделите кучу памяти перед вызовом IAudioClient::Start
. По мере поступления каждого буфера записывайте в эту память. На стороне есть регулярно запланированный рабочий элемент, который берет записанную память и записывает ее на диск или что бы вы с ней ни делали.
Комментарии:
1.
std::vector::emplace
может быть выделение памяти, в зависимости отstd::vector::capacity
. Поскольку в примере нет пути для резервирования дополнительной емкости за пределамиGetBuffer
/ReleaseBuffer
span , это означает, что вектор будет иногда расти. Но худшей частью, вероятно, является блокировка; небольшое выделение памяти, подобное этому, происходит довольно быстро.