c++ windows audio waveoutwrite

c++ - ¿Por qué waveOutWrite() causaría una excepción en el montón de depuración?



windows audio (9)

Mientras investigaba este tema, encontré varias menciones de la siguiente situación en línea, invariablemente como preguntas sin respuesta en los foros de programación. Espero que publicar esto aquí sirva al menos para documentar mis hallazgos.

Primero, el síntoma: mientras ejecuto un código bastante estándar que utiliza waveOutWrite () para producir audio PCM, a veces aparece esto cuando se ejecuta bajo el depurador:

ntdll.dll!_DbgBreakPoint@0() ntdll.dll!_RtlpBreakPointHeap@4() + 0x28 bytes ntdll.dll!_RtlpValidateHeapEntry@12() + 0x113 bytes ntdll.dll!_RtlDebugGetUserInfoHeap@20() + 0x96 bytes ntdll.dll!_RtlGetUserInfoHeap@20() + 0x32743 bytes kernel32.dll!_GlobalHandle@4() + 0x3a bytes wdmaud.drv!_waveCompleteHeader@4() + 0x40 bytes wdmaud.drv!_waveThread@4() + 0x9c bytes kernel32.dll!_BaseThreadStart@8() + 0x37 bytes

Si bien el sospechoso obvio sería un montón de corrupción en otro lugar del código, descubrí que ese no es el caso. Además, pude reproducir este problema usando el siguiente código (esto es parte de una aplicación de MFC basada en un diálogo :)

void CwaveoutDlg::OnBnClickedButton1() { WAVEFORMATEX wfx; wfx.nSamplesPerSec = 44100; /* sample rate */ wfx.wBitsPerSample = 16; /* sample size */ wfx.nChannels = 2; wfx.cbSize = 0; /* size of _extra_ info */ wfx.wFormatTag = WAVE_FORMAT_PCM; wfx.nBlockAlign = (wfx.wBitsPerSample >> 3) * wfx.nChannels; wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec; waveOutOpen(&hWaveOut, WAVE_MAPPER, &wfx, (DWORD_PTR)m_hWnd, 0, CALLBACK_WINDOW ); ZeroMemory(&header, sizeof(header)); header.dwBufferLength = 4608; header.lpData = (LPSTR)GlobalLock(GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, 4608)); waveOutPrepareHeader(hWaveOut, &header, sizeof(header)); waveOutWrite(hWaveOut, &header, sizeof(header)); } afx_msg LRESULT CwaveoutDlg::OnWOMDone(WPARAM wParam, LPARAM lParam) { HWAVEOUT dev = (HWAVEOUT)wParam; WAVEHDR *hdr = (WAVEHDR*)lParam; waveOutUnprepareHeader(dev, hdr, sizeof(WAVEHDR)); GlobalFree(GlobalHandle(hdr->lpData)); ZeroMemory(hdr, sizeof(*hdr)); hdr->dwBufferLength = 4608; hdr->lpData = (LPSTR)GlobalLock(GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, 4608)); waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR)); waveOutWrite(hWaveOut, hdr, sizeof(WAVEHDR)); return 0; }

Antes de que alguien comente sobre esto, sí, el código de muestra reproduce la memoria no inicializada. No intente esto con sus parlantes girados hacia arriba.

Algunas depuraciones revelaron la siguiente información: waveOutPrepareHeader () rellena header.reserved con un puntero a lo que parece ser una estructura que contiene al menos dos punteros como sus dos primeros miembros. El primer puntero se establece en NULL. Después de llamar a waveOutWrite (), este puntero se establece en un puntero asignado en el montón global. En pseudo código, se vería algo como esto:

struct Undocumented { void *p1, *p2; } /* This might have more members */ MMRESULT waveOutPrepareHeader( handle, LPWAVEHDR hdr, ...) { hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented)); /* Do more stuff... */ } MMRESULT waveOutWrite( handle, LPWAVEHDR hdr, ...) { /* The following assignment fails rarely, causing the problem: */ hdr->reserved->p1 = malloc( /* chunk of private data */ ); /* Probably more code to initiate playback */ }

Normalmente, waveCompleteHeader (), una función interna de wdmaud.dll, devuelve el encabezado a la aplicación. waveCompleteHeader () intenta desasignar el puntero asignado por waveOutWrite () llamando a GlobalHandle () / GlobalUnlock () y amigos. A veces, las bombas GlobalHandle (), como se muestra arriba.

Ahora, la razón por la que las bombas GlobalHandle () no se deben a un daño en el montón, como sospeché al principio, es porque waveOutWrite () regresó sin establecer el primer puntero en la estructura interna a un puntero válido. Sospecho que libera la memoria apuntada por ese puntero antes de regresar, pero aún no lo he desmontado.

Esto solo parece ocurrir cuando el sistema de reproducción de onda tiene pocos búfers, por lo que estoy usando un solo encabezado para reproducir esto.

En este punto, tengo un buen caso en contra de que esto sea un error en mi aplicación, después de todo, mi aplicación ni siquiera se está ejecutando. ¿Alguien ha visto esto antes?

Estoy viendo esto en Windows XP SP2. La tarjeta de audio es de SigmaTel y la versión del controlador es 5.10.0.4995.

Notas:

Para evitar confusiones en el futuro, me gustaría señalar que la respuesta que sugiere que el problema radica en el uso de malloc () / free () para gestionar los búferes que se están reproduciendo es simplemente incorrecta. Notarás que cambié el código anterior para reflejar la sugerencia, para evitar que más personas cometan el mismo error: no hace la diferencia. El buffer liberado por waveCompleteHeader () no es el que contiene los datos PCM, la responsabilidad de liberar el búfer PCM recae en la aplicación, y no es necesario que se asigne de manera específica.

Además, me aseguro de que ninguna de las llamadas a la API de WaveOut que uso falle.

Actualmente estoy asumiendo que esto es un error en Windows o en el controlador de audio. Las opiniones disidentes son siempre bienvenidas.


No estoy seguro acerca de este problema en particular, pero ¿ha considerado usar una biblioteca de audio multiplataforma de alto nivel? Hay muchas peculiaridades con la programación de audio de Windows, y estas bibliotecas pueden ahorrarte muchos dolores de cabeza.

Los ejemplos incluyen PortAudio , RtAudio y SDL .


Lo primero que haría sería verificar los valores de retorno de las funciones de waveOutX. Si alguno de ellos falla, lo que no es irrazonable dado el escenario que describes, y sigues adelante independientemente, entonces no es sorprendente que las cosas comiencen a ir mal. Supongo que waveOutWrite devolverá MMSYSERR_NOMEM en algún momento.


Estoy viendo el mismo problema y he hecho algunos análisis yo mismo:

waveOutWrite () asigna (es decir, GlobalAlloc) un puntero a un área de almacenamiento dinámico de 354 bytes y lo almacena correctamente en el área de datos apuntada por header.reserved.

Pero cuando este área del montón se libere nuevamente (en waveCompleteHeader (), de acuerdo con su análisis, no tengo los símbolos para wdmaud.drv), el byte menos significativo del puntero se ha establecido en cero, lo que invalida el puntero (mientras que el montón aún no está dañado). En otras palabras, lo que sucede es algo así como:

  • (BYTE *) (header.reserved) = 0

Por lo tanto, no estoy de acuerdo con sus afirmaciones en un punto: waveOutWrite () almacena primero un puntero válido; el puntero solo se corrompe después de otro subproceso. Probablemente ese es el mismo hilo (mxdmessage) que luego trata de liberar esta área de montón, pero aún no encontré el punto donde se almacena el byte cero.

Esto no sucede muy a menudo, y la misma área de montón (misma dirección) se ha asignado y desasignado con éxito antes. Estoy bastante convencido de que este es un error en algún lugar del código del sistema.


Use Application Verifier para descubrir qué está pasando, si hace algo sospechoso, lo detectará mucho antes.



Puede ser útil mirar el código fuente de Wine , aunque es posible que Wine haya corregido cualquier error que haya, y también es posible que Wine tenga otros errores. Los archivos relevantes son dlls / winmm / winmm.c, dlls / winmm / lolvldrv.c, y posiblemente otros. ¡Buena suerte!


Ahora, la razón por la que las bombas GlobalHandle () no se deben a un daño en el montón, como sospeché al principio, es porque waveOutWrite () regresó sin establecer el primer puntero en la estructura interna a un puntero válido. Sospecho que libera la memoria apuntada por ese puntero antes de regresar, pero aún no lo he desmontado.

Puedo reproducir esto con tu código en mi sistema. Veo algo similar a lo que informó Johannes. Después de la llamada a WaveOutWrite, hdr-> reserved normalmente contiene un puntero a la memoria asignada (que parece contener el nombre del dispositivo de salida de onda en Unicode, entre otras cosas).

Pero ocasionalmente, después de regresar de WaveOutWrite (), el byte señalado por hdr->reserved se establece en 0. Este es normalmente el byte menos significativo de ese puntero. El resto de los bytes en hdr->reserved están bien, y el bloque de memoria al que normalmente apunta sigue asignado y sin corromper.

Probablemente esté siendo golpeado por otro hilo: puedo detectar el cambio con un punto de corte condicional inmediatamente después de la llamada a WaveOutWrite (). Y el punto de corte de depuración del sistema está ocurriendo en otro hilo, no en el controlador de mensajes.

Sin embargo, no puedo provocar que se produzca el punto de interrupción de depuración del sistema si utilizo una función de devolución de llamada en lugar de la bomba de mensajes de Windows. ( fdwOpen = CALLBACK_FUNCTION en WaveOutOpen ()) Cuando lo hago de esta manera, mi manejador OnWOMDone es llamado por un hilo diferente - posiblemente el responsable de la corrupción.

Así que creo que hay un error, ya sea en Windows o en el controlador, pero creo que puedes evitar manipular WOM_DONE con una función de devolución de llamada en lugar de la bomba de mensajes de Windows.


¿Qué pasa con el hecho de que no puedes llamar a las funciones de winmm desde la devolución de llamada? MSDN no menciona tales restricciones sobre los mensajes de ventana, pero el uso de los mensajes de ventana es similar a la función de devolución de llamada. Posiblemente, internamente se implementa como una función de devolución de llamada desde el controlador y esa devolución de llamada hace SendMessage. Internamente, Waveout debe mantener una lista enlazada de encabezados que se escribieron usando waveOutWrite; Entonces, supongo que:

hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented));

establece los punteros anteriores / siguientes de la lista enlazada o algo como esto. Si escribe más búferes, entonces si comprueba los punteros y si alguno de ellos apuntan el uno al otro, entonces mi conjetura es probablemente correcta.

Múltiples fuentes en la web mencionan que no necesita preparar / preparar los mismos encabezados repetidamente. Si comenta el encabezado Prepare / unprepare en el ejemplo original, entonces parece funcionar bien sin ningún problema.


Resolví el problema al sondear la reproducción de sonido y los retrasos:

WAVEHDR header = { buffer, sizeof(buffer), 0, 0, 0, 0, 0, 0 }; waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR)); waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR)); /* * wait a while for the block to play then start trying * to unprepare the header. this will fail until the block has * played. */ while (waveOutUnprepareHeader(hWaveOut,&header,sizeof(WAVEHDR)) == WAVERR_STILLPLAYING) Sleep(100); waveOutClose(hWaveOut);

Reproducción de audio en Windows utilizando la interfaz waveOut