c# - ¿Cómo producir un tono y silencio precisos en el tiempo?
.net audio (4)
Tengo un proyecto de C # que reproduce el código Morse para los canales RSS. Lo escribo usando Managed DirectX, solo para descubrir que Managed DirectX es viejo y obsoleto. La tarea que tengo es reproducir ráfagas de onda sinusoidal pura intercaladas con períodos de silencio (el código) que están cronometrados con precisión en cuanto a su duración. Necesito poder llamar a una función que reproduce un tono puro durante tantos milisegundos, luego Thread.Sleep () y luego reproducir otro, etc. En su forma más rápida, los tonos y espacios pueden ser tan cortos como 40 ms.
Funciona bastante bien en Managed DirectX. Para obtener el tono preciso y preciso, creo 1 seg. de onda sinusoidal en una memoria intermedia secundaria, luego, para reproducir un tono de cierta duración, busco dentro de x milisegundos del final de la memoria intermedia y luego juego.
He probado System.Media.SoundPlayer. Es un perdedor [editar - vea mi respuesta a continuación] porque tiene que Reproducir (), Dormir (), luego Detener () para longitudes de tono arbitrarias. El resultado es un tono que es demasiado largo, variable según la carga de la CPU. Se necesita una cantidad de tiempo indeterminada para detener el tono.
Entonces me embarqué en un largo intento de usar NAudio 1.3 . Terminé con una secuencia residente en la memoria que proporciona los datos de tono, y de nuevo buscando dejar la longitud deseada de tono restante en la transmisión, y luego reproducir. Esto funcionó bien en la clase DirectSoundOut por un tiempo (ver a continuación) pero la clase WaveOut muere rápidamente con una afirmación interna que dice que los búferes todavía están en la cola a pesar de PlayerStopped = true. Esto es extraño ya que toco hasta el final y pongo una espera de la misma duración entre el final del tono y el inicio del siguiente. Pensarías que 80 ms después de iniciar Play de un tono de 40 ms no tendría buffers en la cola.
DirectSoundOut funciona bien por un tiempo, pero su problema es que por cada reproducción de tono Play () produce un hilo separado. Eventualmente (5 minutos más o menos) simplemente deja de funcionar. Puede ver el hilo después del hilo después de salir del hilo en la ventana de Salida mientras ejecuta el proyecto en VS2008 IDE. No creo objetos nuevos durante la reproducción, simplemente busco () la secuencia de tonos y luego llamo a Play () una y otra vez, así que no creo que sea un problema con los buffers huérfanos / lo que sea que se acumulen hasta que se ahogue.
No tengo paciencia con esto, así que le pregunto con la esperanza de que alguien aquí se haya enfrentado a un requisito similar y pueda orientarme en una dirección con una solución probable.
Debería ser bastante fácil convertir de ManagedDX a SlimDX ...
Editar: ¿Qué te detiene, por cierto, solo pre-generar ''n'' muestras de onda sinusoidal? (Donde n es el más cercano a la cantidad de milisegundos que desea). Realmente no lleva tanto tiempo generar los datos. Además de eso, si tienes un buffer de 22Khz y quieres las 100 muestras finales, ¿por qué no envías ''buffer + 21950'' y configuras la longitud del buffer en 100 muestras?
Intenta usar XNA.
Deberá proporcionar un archivo, o una secuencia a un tono estático, que pueda repetir. Luego puede cambiar el tono y el volumen de ese tono.
Como XNA está hecho para juegos, no tendrá ningún problema con retrasos de 40 ms.
Lo mejor es generar las ondas sinusoidales y silenciarlas juntas en un buffer que toques. Es decir, siempre juegue algo, pero escriba lo que necesite al lado de ese búfer.
Usted conoce la frecuencia de muestreo y, dada la frecuencia de muestreo, puede calcular la cantidad de muestras que necesita escribir.
uint numSamples = timeWantedInSeconds * sampleRate;
Esa es la cantidad de muestras que necesita para generar una onda sinusoidal o silencio, cualquiera que sea. Luego simplemente llena el buffer según sea necesario. De esta forma, obtienes el momento más preciso posible.
No lo puedo creer ... Volví a System.Media.SoundPlayer y lo hice para hacer lo que quisiera ... ninguna biblioteca gigante de dependencias con 95% de código no utilizado y / o peculiaridades esperando a ser descubierto :-) . ¡Además, se ejecuta en MacOSX bajo Mono (2.6)! [incorrecto - sin sonido, hará una pregunta por separado]
Usé un MemoryStream y BinaryWriter para guardar un archivo WAV, completar con el encabezado RIFF y fragmentar. No se necesita ningún fragmento de "hecho", esto es muestras de 16 bits a 44100Hz. Así que ahora tengo un MemoryStream con 1000 ms de muestras y envuelto por un BinaryReader.
En un archivo RIFF hay dos longitudes de 4 bytes / 32 bits, la longitud "general" que es de 4 bytes en la secuencia (justo después de "RIFF" en ASCII), y una longitud de "datos" justo antes de los bytes de datos de muestra . Mi estrategia era buscar en la transmisión y usar BinaryWriter para alterar las dos longitudes para engañar al SoundPlayer y hacerle pensar que la transmisión de audio es de la duración / duración que quiero, y luego reproducirla (). La próxima vez, la duración es diferente, así que una vez más, sobrescriba las longitudes en el MemoryStream con BinaryWriter, Flush () y, una vez más, llame a Play ().
Cuando probé esto, no pude hacer que SoundPlayer viera los cambios en la transmisión, incluso si configuré su propiedad Stream. Me vi obligado a crear un nuevo SoundPlayer ... ¿cada 40 milisegundos? No.
Bueno, quiero volver a ese código hoy y comencé a buscar a los miembros de SoundPlayer. Vi "SoundLocation" y lo leí. Allí dijo que un efecto secundario de establecer SoundLocation sería anular la propiedad Stream, y viceversa para Stream. Así que agregué una línea de código para establecer la propiedad SOUNDLocation en algo falso, "x", luego establecí la propiedad Stream en mi (recién modificado) MemoryStream. Maldita sea si no entendía y tocaba un tono con precisión todo el tiempo que yo le pedía. No parece haber ningún efecto secundario loco como el tiempo muerto después o el aumento de la memoria, o ??? Toma 1-2 milisegundos para hacer ese ajuste de la corriente WAV y luego cargar / iniciar el reproductor, pero es muy pequeño y el precio es correcto.
También implementé una propiedad Frequency que vuelve a generar las muestras y usa el truco Seek / BinaryWriter para superponer los datos antiguos en RIFF / WAV MemoryStream con el mismo número de muestras pero para una frecuencia diferente, y nuevamente hice lo mismo para una Propiedad de amplitud
Este proyecto está en SourceForge . Puede obtener el código C # para este truco en SPTones.CS desde esta página en el navegador SVN . Gracias a todos los que proporcionaron información sobre esto, incluido @arke, cuyo pensamiento era muy cercano al mío. Lo aprecio