Determinación de la latencia en el procesamiento de audio
Frecuentes
Visto 608 equipos
2
He estado trabajando en la implementación de un sistema para la captura y el análisis de audio en tiempo real dentro de un proyecto de software de música existente. El objetivo de este sistema es comenzar a capturar audio cuando el usuario presiona el botón de grabación (o después de un período de conteo especificado), determinar las notas que el usuario canta o toca y anotar estas notas en un pentagrama musical. La esencia de mi método es utilizar un subproceso para capturar fragmentos de datos de audio y ponerlos en una cola, y otro subproceso para eliminar los datos de la cola y realizar el análisis.
Este esquema funciona bien, pero tengo problemas para cuantificar la latencia entre el inicio de la captura de audio y la reproducción de los instrumentos de acompañamiento MIDI. La captura de audio comienza antes de que los instrumentos MIDI comiencen a reproducirse y, presumiblemente, el usuario sincronizará su interpretación con los instrumentos MIDI. Por lo tanto, necesito ignorar los datos de audio capturados antes de que los instrumentos MIDI de respaldo comiencen a reproducirse y solo analizar los datos de audio recopilados después de ese punto.
La reproducción de las pistas de acompañamiento está a cargo de un cuerpo de código que ha estado funcionando durante bastante tiempo y mantenido por otra persona, por lo que me gustaría evitar refactorizar todo el programa si es posible. La captura de audio se controla con un objeto Timer y una clase que amplía TimerTask, cuyas instancias se crean en una clase pesada (~25k líneas) llamada Notate. Notate también controla los objetos que manejan la reproducción de las pistas de acompañamiento, por cierto. El método .scheduleAtFixedRate() de Timer se usa para controlar los períodos de captura de audio, y TimerTask notifica al subproceso de captura que comience llamando a .notify() en la cola (ArrayBlockingQueue).
Mi estrategia para calcular el intervalo de tiempo entre la inicialización de estos dos procesos ha sido restar la marca de tiempo tomada justo antes de que comience la captura (en milisegundos) de la marca de tiempo tomada en el momento en que comienza la reproducción, que defino como cuando el .start () se llama al método Java Sequencer que está a cargo de las pistas de acompañamiento MIDI. Luego uso el resultado para determinar la cantidad de muestras de audio que espero que se hayan capturado durante este intervalo (n) e ignoro los primeros n * 2 bytes en la matriz de datos de audio capturados (n * 2 porque estoy capturando 16- muestras de bits, mientras que los datos se almacenan como una matriz de bytes... 2 bytes por muestra).
Sin embargo, este método no me está dando resultados precisos. El desplazamiento calculado siempre es menor de lo que espero que sea, de modo que queda una cantidad no trivial (y desafortunadamente variada) de espacio "vacío" en los datos de audio después de comenzar el análisis en la posición designada. Esto hace que el programa intente analizar los datos de audio recopilados cuando el usuario aún no había comenzado a tocar junto con los instrumentos MIDI de acompañamiento, agregando efectivamente silencios (la ausencia de notas musicales) al comienzo del pasaje musical del usuario y arruinando los valores de ritmo. calculado para todas las notas posteriores.
A continuación se muestra el código de mi subproceso de captura de audio, que también determina la latencia y el desplazamiento de posición correspondiente para la matriz de datos de audio capturados. ¿Alguien puede ofrecer una idea de por qué mi método para determinar la latencia no funciona correctamente?
public class CaptureThread extends Thread
{
public void run()
{
//number of bytes to capture before putting data in the queue.
//determined via the sample rate, tempo, and # of "beats" in 1 "measure"
int bytesToCapture = (int) ((SAMPLE_RATE * 2.) / (score.getTempo()
/ score.getMetre()[0] / 60.));
//temporary buffer - will be added to ByteArrayOutputStream upon filling.
byte tempBuffer[] = new byte[target.getBufferSize() / 5];
int limit = (int) (bytesToCapture / tempBuffer.length);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(bytesToCapture);
int bytesRead;
try
{ //Loop until stopCapture is set.
while (!stopCapture)
{ //first, wait for notification from TimerTask
synchronized (thisCapture)
{
thisCapture.wait();
}
if (!processingStarted)
{ //the time at which audio capture begins
startTime = System.currentTimeMillis();
}
//start the TargetDataLine, from which audio data is read
target.start();
//collect 1 captureInterval's worth of data
for (int n = 0; n < limit; n++)
{
bytesRead = target.read(tempBuffer, 0, tempBuffer.length);
if (bytesRead > 0)
{ //Append data to output stream.
outputStream.write(tempBuffer, 0, bytesRead);
}
}
if (!processingStarted)
{
long difference = (midiSynth.getPlaybackStartTime()
+ score.getCountInTime() * 1000 - startTime);
positionOffset = (int) ((difference / 1000.)
* SAMPLE_RATE * 2.);
if (positionOffset % 2 != 0)
{ //1 sample = 2 bytes, so positionOffset must be even
positionOffset += 1;
}
}
if (outputStream.size() > 0)
{ //package data collected in the output stream into a byte array
byte[] capturedAudioData = outputStream.toByteArray();
//add captured data to the queue for processing
processingQueue.add(capturedAudioData);
synchronized (processingQueue)
{
try
{ //notify the analysis thread that data is in the queue
processingQueue.notify();
} catch (Exception e)
{
//handle the error
}
}
outputStream.reset(); //reset the output stream
}
}
} catch (Exception e)
{
//handle error
}
}
}
Estoy investigando el uso de un Mezclador objeto de sincronizar el TargetDataLine que acepta datos del micrófono y la línea que maneja la reproducción de los instrumentos MIDI. Ahora para encontrar la línea que maneja la reproducción... ¿Alguna idea?
1 Respuestas
1
Google tiene una buena aplicación de código abierto llamada AudioBufferSize con la que probablemente estés familiarizado. Modifiqué esta aplicación para probar la latencia unidireccional, es decir, el tiempo entre el momento en que un usuario presiona un botón y la API de audio reproduce el sonido. Aquí está el código que agregué a AudioBufferSize para lograr esto. ¿Podría utilizar este enfoque para proporcionar el delta de tiempo entre el evento y el momento en que el usuario lo percibe?
final Button latencyButton = (Button) findViewById(R.id.latencyButton);
latencyButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mLatencyStartTime = getCurrentTime();
latencyButton.setEnabled(false);
// Do the latency calculation, play a 440 hz sound for 250 msec
AudioTrack sound = generateTone(440, 250);
sound.setNotificationMarkerPosition(count /2); // Listen for the end of the sample
sound.setPlaybackPositionUpdateListener(new OnPlaybackPositionUpdateListener() {
public void onPeriodicNotification(AudioTrack sound) { }
public void onMarkerReached(AudioTrack sound) {
// The sound has finished playing, so record the time
mLatencyStopTime = getCurrentTime();
diff = mLatencyStopTime - mLatencyStartTime;
// Update the latency result
TextView lat = (TextView)findViewById(R.id.latency);
lat.setText(diff + " ms");
latencyButton.setEnabled(true);
logUI("Latency test result= " + diff + " ms");
}
});
sound.play();
}
});
Hay una referencia a generateTone que se parece a esto:
private AudioTrack generateTone(double freqHz, int durationMs) {
int count = (int)(44100.0 * 2.0 * (durationMs / 1000.0)) & ~1;
short[] samples = new short[count];
for(int i = 0; i < count; i += 2){
short sample = (short)(Math.sin(2 * Math.PI * i / (44100.0 / freqHz)) * 0x7FFF);
samples[i + 0] = sample;
samples[i + 1] = sample;
}
AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC, 44100,
AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT,
count * (Short.SIZE / 8), AudioTrack.MODE_STATIC);
track.write(samples, 0, count);
return track;
}
Me acabo de dar cuenta, esta pregunta tiene varios años. Lo siento, tal vez alguien lo encuentre útil.
Respondido 21 Feb 17, 05:02
No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas java multithreading audio midi or haz tu propia pregunta.
Usted menciona que el temporizador comienza cuando comienza el hilo de reproducción. Cualquier posibilidad de que esto sea no cuando empieza la salida de audio? ¿Hay algún tipo de retraso antes de que la reproducción de audio emita sonido? ¿Puedes escupir un mensaje de la consola cuando el temporizador de reproducción comience a asegurarse de que es cuando realmente se produce el sonido? - Gray
@Gray Me preocupa que este sea el caso. Los tiempos que estoy recibiendo parecer para ser precisos, pero no estoy seguro de que correspondan al momento exacto (dentro de 1 ms, por supuesto) en que se emite el sonido. ¿Hay una forma práctica de probar esto? - WellsHuxley
Nuevamente, ¿puedes escupir un mensaje de consola y mirar y escuchar para asegurarte de que el mensaje y el sonido aparezcan al mismo tiempo? - Gray
Sí, el mensaje parece mostrarse simultáneamente con el sonido. - WellsHuxley
La latencia de audio de Java no es pequeña y varía de una plataforma a otra. Debe utilizar el reloj de su dispositivo como reloj maestro, no como reloj del sistema. Debe hacer preguntas como "¿Cuántas muestras de audio se han reproducido?" no "¿cuándo comencé a enviar audio al dispositivo?". Sin embargo, no recuerdo si el tiempo del dispositivo está disponible en Java. - Bjorn Roche