From 774c0c4cf5bbdf3488e4844262765cdbd8055980 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov <lyubomir.marinov@jitsi.org> Date: Tue, 9 Jul 2013 15:01:42 +0300 Subject: [PATCH] Works on improving the acoustic echo cancellation (AEC) of Windows Audio Session API (WASAPI). --- .../impl/neomedia/device/WASAPISystem.java | 28 +- .../protocol/AbstractBufferCaptureDevice.java | 1 - .../protocol/wasapi/AudioCaptureClient.java | 110 +++-- .../media/protocol/wasapi/WASAPIStream.java | 432 ++++++++++++------ .../media/renderer/audio/WASAPIRenderer.java | 9 +- 5 files changed, 391 insertions(+), 189 deletions(-) diff --git a/src/org/jitsi/impl/neomedia/device/WASAPISystem.java b/src/org/jitsi/impl/neomedia/device/WASAPISystem.java index db257d45..e72023ca 100644 --- a/src/org/jitsi/impl/neomedia/device/WASAPISystem.java +++ b/src/org/jitsi/impl/neomedia/device/WASAPISystem.java @@ -33,6 +33,15 @@ public class WASAPISystem */ private static String audioSessionGuid; + /** + * The default duration of audio data in milliseconds to be read from + * <tt>WASAPIStream</tt> in an invocation of + * {@link WASAPIStream#read(Buffer)} or to be processed by + * <tt>WASAPIRenderer</tt> in an invocation of + * {@link WASAPIRenderer#process(Buffer)}. + */ + public static final long DEFAULT_BUFFER_DURATION = 20; + /** * The default interval in milliseconds between periodic processing passes * by the audio engine. @@ -971,7 +980,10 @@ public long initializeAEC() * endpoint device identified by the specified <tt>locator</tt> * @param streamFlags * @param eventHandle - * @param hnsBufferDuration + * @param hnsBufferDuration the base of the duration in milliseconds of the + * buffer that the audio application will share with the audio engine. If + * {@link Format#NOT_SPECIFIED}, the method uses the default interval + * between periodic passes by the audio engine. * @param formats an array of alternative <tt>AudioFormat</tt>s with which * initialization of a new <tt>IAudioClient</tt> instance is to be * attempted. The first element of the <tt>formats</tt> array which is @@ -1107,12 +1119,24 @@ public long initializeIAudioClient( if (eventHandle != 0) streamFlags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK; + if (hnsBufferDuration == Format.NOT_SPECIFIED) + { + hnsBufferDuration + = IAudioClient_GetDefaultDevicePeriod(iAudioClient) + / 10000; + if (hnsBufferDuration <= 1) + { + hnsBufferDuration + = WASAPISystem.DEFAULT_DEVICE_PERIOD; + } + } + int hresult = IAudioClient_Initialize( iAudioClient, shareMode, streamFlags, - hnsBufferDuration, + 3 * hnsBufferDuration * 10000, /* hnsPeriodicity */ 0, waveformatex, audioSessionGuid); diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/AbstractBufferCaptureDevice.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/AbstractBufferCaptureDevice.java index 3663dbb4..2151934d 100644 --- a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/AbstractBufferCaptureDevice.java +++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/AbstractBufferCaptureDevice.java @@ -19,7 +19,6 @@ import org.jitsi.impl.neomedia.control.*; import org.jitsi.util.*; -// disambiguation /** * Facilitates the implementations of the <tt>CaptureDevice</tt> and diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/AudioCaptureClient.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/AudioCaptureClient.java index 979b9a40..06722ef2 100644 --- a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/AudioCaptureClient.java +++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/AudioCaptureClient.java @@ -33,12 +33,6 @@ */ public class AudioCaptureClient { - /** - * The default duration of the audio data in milliseconds to be read from - * <tt>WASAPIStream</tt> in an invocation of {@link #read(Buffer)}. - */ - static final long DEFAULT_BUFFER_DURATION = 20; - /** * The <tt>Logger</tt> used by the <tt>AudioCaptureClient</tt> class and its * instances to log debug information. @@ -46,6 +40,20 @@ public class AudioCaptureClient private static final Logger logger = Logger.getLogger(AudioCaptureClient.class); + /** + * The internal buffer of this instance in which audio data is read from the + * associated <tt>IAudioCaptureClient</tt> by the instance and awaits to be + * read out of this instance via {@link #read(byte[], int, int)}. + */ + private byte[] available; + + /** + * The number of bytes in {@link #available} which represent valid audio + * data read from the associated <tt>IAudioCaptureClient</tt> by this + * instance. + */ + private int availableLength; + /** * The number of audio frames to be filled in a <tt>byte[]</tt> in an * invocation of {@link #read(byte[], int, int)}. The method @@ -134,20 +142,6 @@ public class AudioCaptureClient */ final AudioFormat outFormat; - /** - * The internal buffer of this instance in which audio data is read from the - * associated <tt>IAudioCaptureClient</tt> by the instance and awaits to be - * read out of this instance via {@link #read(byte[], int, int)}. - */ - private byte[] remainder; - - /** - * The number of bytes in {@link #remainder} which represent valid audio - * data read from the associated <tt>IAudioCaptureClient</tt> by this - * instance. - */ - private int remainderLength; - /** * The number of channels with which {@link #iAudioClient} has been * initialized. @@ -194,6 +188,10 @@ public class AudioCaptureClient * written on that render endpoint device * @param streamFlags zero or more of the <tt>AUDCLNT_STREAMFLAGS_XXX</tt> * flags defined by the <tt>WASAPI</tt> class + * @param hnsBufferDuration the base of the duration in milliseconds of the + * buffer that the audio application will share with the audio engine. If + * {@link Format#NOT_SPECIFIED}, the method uses the default interval + * between periodic passes by the audio engine. * @param outFormat the <tt>AudioFormat</tt> of the data to be made * available by the new instance. Eventually, the * <tt>IAudioCaptureClient</tt> to be represented by the new instance may be @@ -210,6 +208,7 @@ public AudioCaptureClient( MediaLocator locator, AudioSystem.DataFlow dataFlow, int streamFlags, + long hnsBufferDuration, AudioFormat outFormat, BufferTransferHandler transferHandler) throws Exception @@ -227,7 +226,6 @@ public AudioCaptureClient( * WASAPIRenderer and WASAPIStream. There is no particular * reason/requirement to do so. */ - long hnsBufferDuration = 3 * DEFAULT_BUFFER_DURATION * 10000; long iAudioClient = audioSystem.initializeIAudioClient( locator, @@ -304,6 +302,8 @@ public AudioCaptureClient( = WASAPISystem.DEFAULT_DEVICE_PERIOD; } this.devicePeriod = devicePeriod; + if (hnsBufferDuration == Format.NOT_SPECIFIED) + hnsBufferDuration = devicePeriod; srcChannels = inFormat.getChannels(); srcSampleSize @@ -315,12 +315,11 @@ public AudioCaptureClient( dstFrameSize = dstSampleSize * dstChannels; bufferFrames - = (int) - (DEFAULT_BUFFER_DURATION * sampleRate / 1000); + = (int) (hnsBufferDuration * sampleRate / 1000); bufferSize = dstFrameSize * bufferFrames; - remainder = new byte[numBufferFrames * dstFrameSize]; - remainderLength = 0; + available = new byte[numBufferFrames * dstFrameSize]; + availableLength = 0; this.eventHandle = eventHandle; eventHandle = 0; @@ -381,8 +380,8 @@ public void close() eventHandle = 0; } - remainder = null; - remainderLength = 0; + available = null; + availableLength = 0; started = false; } @@ -408,7 +407,7 @@ private int doRead( int length) throws IOException { - int toRead = Math.min(length, remainderLength); + int toRead = Math.min(length, availableLength); int read; if (toRead == 0) @@ -418,30 +417,46 @@ private int doRead( if (iMediaBuffer == null) { read = toRead; - System.arraycopy(remainder, 0, buffer, offset, toRead); + System.arraycopy(available, 0, buffer, offset, toRead); } else - read = iMediaBuffer.push(remainder, 0, toRead); - popFromRemainder(read); + read = iMediaBuffer.push(available, 0, toRead); + popFromAvailable(read); } return read; } /** - * Pops a specific number of bytes from {@link #remainder}. For example, - * because such a number of bytes have been read from <tt>remainder</tt> and + * Gets the number of bytes of audio samples which have been read from the + * associated <tt>IAudioCaptureClient</tt> by this instance and are + * available to be read out of this instance via + * {@link #read(byte[], int, int)}. + * + * @return the number of bytes of audio samples which have been read from + * the associated <tt>IAudioCaptureClient</tt> by this instance and are + * available to be read out of this instance via + * <tt>read(byte[], int, int)</tt> + */ + int getAvailableLength() + { + return availableLength; + } + + /** + * Pops a specific number of bytes from {@link #available}. For example, + * because such a number of bytes have been read from <tt>available</tt> and * written into a <tt>Buffer</tt>. * - * @param length the number of bytes to pop from <tt>remainder</tt> + * @param length the number of bytes to pop from <tt>available</tt> */ - private void popFromRemainder(int length) + private void popFromAvailable(int length) { - remainderLength - = WASAPIRenderer.pop(remainder, remainderLength, length); + availableLength + = WASAPIRenderer.pop(available, availableLength, length); } /** - * Reads audio data from this instance into a spcific <tt>byte</tt> array. + * Reads audio data from this instance into a specific <tt>byte</tt> array. * * @param buffer the <tt>byte</tt> array into which the audio data read from * this instance is to be written @@ -528,7 +543,7 @@ public int read(IMediaBuffer iMediaBuffer, int length) } /** - * Reads from {@link #iAudioCaptureClient} into {@link #remainder} and + * Reads from {@link #iAudioCaptureClient} into {@link #available} and * returns a non-<tt>null</tt> <tt>BufferTransferHandler</tt> if this * instance is to push audio data. * @@ -553,30 +568,29 @@ private BufferTransferHandler readInEventHandleCmd() numFramesInNextPacket = 0; // Silence the compiler. logger.error("IAudioCaptureClient_GetNextPacketSize", hre); } - if (numFramesInNextPacket != 0) { int toRead = numFramesInNextPacket * dstFrameSize; /* - * Make sure there is enough room in remainder to accommodate + * Make sure there is enough room in available to accommodate * toRead. */ - int toPop = toRead - (remainder.length - remainderLength); + int toPop = toRead - (available.length - availableLength); if (toPop > 0) - popFromRemainder(toPop); + popFromAvailable(toPop); try { int read = IAudioCaptureClient_Read( iAudioCaptureClient, - remainder, remainderLength, toRead, + available, availableLength, toRead, srcSampleSize, srcChannels, dstSampleSize, dstChannels); - remainderLength += read; + availableLength += read; } catch (HResultException hre) { @@ -584,7 +598,7 @@ private BufferTransferHandler readInEventHandleCmd() } } - return (remainderLength >= bufferSize) ? transferHandler : null; + return (availableLength >= bufferSize) ? transferHandler : null; } /** @@ -727,7 +741,7 @@ public synchronized void start() IAudioClient_Start(iAudioClient); started = true; - remainderLength = 0; + availableLength = 0; if ((eventHandle != 0) && (this.eventHandleCmd == null)) { Runnable eventHandleCmd @@ -798,7 +812,7 @@ public synchronized void stop() started = false; waitWhileEventHandleCmd(); - remainderLength = 0; + availableLength = 0; } catch (HResultException hre) { diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPIStream.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPIStream.java index 57dd2f0f..51b50143 100644 --- a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPIStream.java +++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPIStream.java @@ -33,12 +33,28 @@ public class WASAPIStream extends AbstractPushBufferStream<DataSource> { + /** + * The zero-based index of the input stream of the <tt>IMediaObject</tt> + * that represents the Voice Capture DSP implementing the acoustic echo + * cancellation (AEC) feature that delivers the audio samples from the + * microphone. + */ + private static final int CAPTURE_INPUT_STREAM_INDEX = 0; + /** * The <tt>Logger</tt> used by the <tt>WASAPIStream</tt> class and its * instances to log debug information. */ private static Logger logger = Logger.getLogger(WASAPIStream.class); + /** + * The zero-based index of the input stream of the <tt>IMediaObject</tt> + * that represents the Voice Capture DSP implementing the acoustic echo + * cancellation (AEC) feature that delivers the audio samples from the + * speaker (line). + */ + private static final int RENDER_INPUT_STREAM_INDEX = 1; + /** * Finds an <tt>AudioFormat</tt> in a specific list of <tt>Format</tt>s * which is as similar to a specific <tt>AudioFormat</tt> as possible. @@ -257,9 +273,9 @@ private static int IMediaObject_SetXXXputType( * <tt>IOException</tt>. * * @param iMediaBuffer the <tt>IMediaBuffer</tt> on which the method - * <tt>GetLength<tt> is to be invoked + * <tt>GetLength</tt> is to be invoked * @return the length of the specified <tt>iMediaBuffer</tt>. If the method - * <tt>GetLength<tt> fails, returns <tt>0</tt>. + * <tt>GetLength</tt> fails, returns <tt>0</tt>. */ private static int maybeIMediaBufferGetLength(IMediaBuffer iMediaBuffer) { @@ -272,7 +288,7 @@ private static int maybeIMediaBufferGetLength(IMediaBuffer iMediaBuffer) catch (IOException ioe) { length = 0; - logger.error("IMediaBuffer::GetLength", ioe); + logger.error("IMediaBuffer.GetLength", ioe); } return length; } @@ -282,9 +298,9 @@ private static int maybeIMediaBufferGetLength(IMediaBuffer iMediaBuffer) * swallows any <tt>HResultException</tt>. * * @param iMediaBuffer the <tt>IMediaBuffer</tt> on which the function - * <tt>IMediaBuffer_GetLength<tt> is to be invoked + * <tt>IMediaBuffer_GetLength</tt> is to be invoked * @return the length of the specified <tt>iMediaBuffer</tt>. If the - * function <tt>IMediaBuffer_GetLength<tt> fails, returns <tt>0</tt>. + * function <tt>IMediaBuffer_GetLength</tt> fails, returns <tt>0</tt>. */ @SuppressWarnings("unused") private static int maybeIMediaBufferGetLength(long iMediaBuffer) @@ -389,6 +405,8 @@ static void throwNewIOException(String message, HResultException hre) */ private boolean captureIsBusy; + private double captureNanosPerByte; + /** * The length in milliseconds of the interval between successive, periodic * processing passes by the audio engine on the data in the endpoint buffer. @@ -428,6 +446,8 @@ static void throwNewIOException(String message, HResultException hre) private int renderBufferMaxLength; + private double renderBytesPerNano; + private PtrMediaBuffer renderIMediaBuffer; /** @@ -438,6 +458,12 @@ static void throwNewIOException(String message, HResultException hre) */ private boolean renderIsBusy; + /** + * The indicator which determines whether no reading from {@link #render} is + * to be performed until it reaches a certain threshold of availability. + */ + private boolean replenishRender; + /** * The indicator which determines whether this <tt>SourceStream</tt> is * started i.e. there has been a successful invocation of {@link #start()} @@ -460,6 +486,27 @@ public WASAPIStream(DataSource dataSource, FormatControl formatControl) super(dataSource, formatControl); } + /** + * Computes/determines the duration in nanoseconds of audio samples which + * are represented by a specific number of bytes and which are in encoded in + * the <tt>outFormat</tt> of {@link #capture}. + * + * @param length the number of bytes comprising the audio samples of which + * the duration in nanoseconds is to be computed/determined + * @return the duration in nanoseconds of audio samples which are + * represented by the specified number of bytes and which are encoded in the + * <tt>outFormat</tt> of <tt>capture</tt> + */ + private long computeCaptureDuration(int length) + { + return (long) (length * captureNanosPerByte); + } + + private int computeRenderLength(long duration) + { + return (int) (duration * renderBytesPerNano); + } + /** * Performs optional configuration on the Voice Capture DSP that implements * acoustic echo cancellation (AEC). @@ -716,11 +763,12 @@ private void initializeAEC( } try { + int dwInputStreamIndex = CAPTURE_INPUT_STREAM_INDEX; int hresult = IMediaObject_SetXXXputType( iMediaObject, /* IMediaObject_SetInputType */ true, - /* dwInputStreamIndex */ 0, + dwInputStreamIndex, inFormat0, /* dwFlags */ 0); @@ -728,22 +776,23 @@ private void initializeAEC( { throw new HResultException( hresult, - "IMediaObject_SetInputType, dwOutputStreamIndex 0, " - + inFormat0); + "IMediaObject_SetInputType, dwInputStreamIndex " + + dwInputStreamIndex + ", " + inFormat0); } + dwInputStreamIndex = RENDER_INPUT_STREAM_INDEX; hresult = IMediaObject_SetXXXputType( iMediaObject, /* IMediaObject_SetInputType */ true, - /* dwInputStreamIndex */ 1, + dwInputStreamIndex, inFormat1, /* dwFlags */ 0); if (FAILED(hresult)) { throw new HResultException( hresult, - "IMediaObject_SetInputType, dwOutputStreamIndex 1, " - + inFormat1); + "IMediaObject_SetInputType, dwInputStreamIndex " + + dwInputStreamIndex + ", " + inFormat1); } hresult = IMediaObject_SetXXXputType( @@ -804,9 +853,8 @@ private void initializeAEC( * outFormat.getChannels(); int outFrames = (int) - (AudioCaptureClient.DEFAULT_BUFFER_DURATION - * ((int) outFormat.getSampleRate()) - / 1000); + (WASAPISystem.DEFAULT_BUFFER_DURATION + * ((int) outFormat.getSampleRate()) / 1000); long iMediaBuffer = MediaBuffer_alloc(outFrameSize * outFrames); @@ -852,6 +900,45 @@ private void initializeAEC( this.renderIMediaBuffer = new PtrMediaBuffer(renderIMediaBuffer); renderIMediaBuffer = 0; + + /* + * Prepare to be ready to compute/determine the + * duration in nanoseconds of a specific number + * of bytes representing audio samples encoded + * in the outFormat of capture. + */ + { + AudioFormat af = capture.outFormat; + double sampleRate = af.getSampleRate(); + int sampleSizeInBits + = af.getSampleSizeInBits(); + int channels = af.getChannels(); + + captureNanosPerByte + = (8d * 1000d * 1000d * 1000d) + / (sampleRate + * sampleSizeInBits + * channels); + } + /* + * Prepare to be ready to compute/determine the + * number of bytes representing a specific + * duration in nanoseconds of audio samples + * encoded in the outFormat of render. + */ + { + AudioFormat af = render.outFormat; + double sampleRate = af.getSampleRate(); + int sampleSizeInBits + = af.getSampleSizeInBits(); + int channels = af.getChannels(); + + renderBytesPerNano + = (sampleRate + * sampleSizeInBits + * channels) + / (8d * 1000d * 1000d * 1000d); + } } finally { @@ -893,21 +980,28 @@ private void initializeAEC( private void initializeCapture(MediaLocator locator, AudioFormat format) throws Exception { + long hnsBufferDuration + = dataSource.aec + ? Format.NOT_SPECIFIED + : WASAPISystem.DEFAULT_BUFFER_DURATION; + BufferTransferHandler transferHandler + = new BufferTransferHandler() + { + public void transferData(PushBufferStream stream) + { + transferCaptureData(); + } + }; + capture = new AudioCaptureClient( dataSource.audioSystem, locator, AudioSystem.DataFlow.CAPTURE, /* streamFlags */ 0, + hnsBufferDuration, format, - new BufferTransferHandler() - { - public void transferData( - PushBufferStream stream) - { - transferCaptureData(); - } - }); + transferHandler); bufferSize = capture.bufferSize; devicePeriod = capture.devicePeriod; } @@ -915,21 +1009,23 @@ public void transferData( private void initializeRender(final MediaLocator locator, AudioFormat format) throws Exception { + /* + * XXX The method transferRenderData does not read any data from render + * at this time. If the transferHandler (which will normally invoke + * transferRenderData) was non-null, it would cause excessive CPU use. + */ + BufferTransferHandler transferHandler = null; + render = new AudioCaptureClient( dataSource.audioSystem, locator, AudioSystem.DataFlow.PLAYBACK, WASAPI.AUDCLNT_STREAMFLAGS_LOOPBACK, + /* hnsBufferDuration */ Format.NOT_SPECIFIED, format, - new BufferTransferHandler() - { - public void transferData( - PushBufferStream stream) - { - transferRenderData(); - } - }); + transferHandler); + replenishRender = true; } /** @@ -952,28 +1048,34 @@ private void popFromProcessed(int length) * * @param dwInputStreamIndex the zero-based index of the input stream on * <tt>iMediaObject</tt> to which audio samples are to be delivered + * @param maxLength the maximum number of bytes to the delivered through the + * specified input stream. Ignored if negative or greater than the actual + * capacity/maximum length of the <tt>IMediaBuffer</tt> associated with the specified + * <tt>dwInputStreamIndex</tt>. */ - private void processInput(int dwInputStreamIndex) + private void processInput(int dwInputStreamIndex, int maxLength) { PtrMediaBuffer oBuffer; - int maxLength; + int bufferMaxLength; AudioCaptureClient audioCaptureClient; switch (dwInputStreamIndex) { - case 0: + case CAPTURE_INPUT_STREAM_INDEX: oBuffer = captureIMediaBuffer; - maxLength = captureBufferMaxLength; + bufferMaxLength = captureBufferMaxLength; audioCaptureClient = capture; break; - case 1: + case RENDER_INPUT_STREAM_INDEX: oBuffer = renderIMediaBuffer; - maxLength = renderBufferMaxLength; + bufferMaxLength = renderBufferMaxLength; audioCaptureClient = render; break; default: throw new IllegalArgumentException("dwInputStreamIndex"); } + if ((maxLength < 0) || (maxLength > bufferMaxLength)) + maxLength = bufferMaxLength; long pBuffer = oBuffer.ptr; int hresult = S_OK; @@ -1008,17 +1110,31 @@ private void processInput(int dwInputStreamIndex) * AudioCaptureClient and then deliver them to the specified * input stream. */ - int toRead; + int toRead = Format.NOT_SPECIFIED; - try + if ((dwInputStreamIndex == RENDER_INPUT_STREAM_INDEX) + && replenishRender) { - toRead = maxLength - IMediaBuffer_GetLength(pBuffer); + int replenishThreshold = renderBufferMaxLength; + + if (audioCaptureClient.getAvailableLength() + < replenishThreshold) + toRead = 0; + else + replenishRender = false; } - catch (HResultException hre) + if (toRead == Format.NOT_SPECIFIED) { - hresult = hre.getHResult(); - toRead = 0; - logger.error("IMediaBuffer_GetLength", hre); + try + { + toRead = maxLength - IMediaBuffer_GetLength(pBuffer); + } + catch (HResultException hre) + { + hresult = hre.getHResult(); + toRead = 0; + logger.error("IMediaBuffer_GetLength", hre); + } } if (toRead > 0) { @@ -1028,7 +1144,11 @@ private void processInput(int dwInputStreamIndex) */ try { - audioCaptureClient.read(oBuffer, toRead); + int read = audioCaptureClient.read(oBuffer, toRead); + + if ((dwInputStreamIndex == RENDER_INPUT_STREAM_INDEX) + && (read == 0)) + replenishRender = true; } catch (IOException ioe) { @@ -1050,7 +1170,7 @@ private void processInput(int dwInputStreamIndex) * above, read from the render endpoint device as many audio * samples as possible and pad with silence if necessary. */ - if (dwInputStreamIndex == 1) + if (dwInputStreamIndex == RENDER_INPUT_STREAM_INDEX) { int length; @@ -1100,6 +1220,7 @@ private void processInput(int dwInputStreamIndex) if (hresult != DMO_E_NOTACCEPTING) logger.error("IMediaObject_ProcessInput", hre); } + break; // XXX We risk a busy wait unless we break here. } else break; // The input stream cannot accept more input data. @@ -1107,6 +1228,67 @@ private void processInput(int dwInputStreamIndex) while (SUCCEEDED(hresult)); } + /** + * Invokes <tt>IMediaObject::ProcessOutput</tt> on {@link #iMediaObject} + * that represents the Voice Capture DSP implementing the acoustic echo + * cancellation (AEC) feature. + */ + private void processOutput() + { + int dwStatus = 0; + + do + { + try + { + IMediaObject_ProcessOutput( + iMediaObject, + /* dwFlags */ 0, + 1, + dmoOutputDataBuffer); + } + catch (HResultException hre) + { + dwStatus = 0; + logger.error("IMediaObject_ProcessOutput", hre); + } + try + { + int toRead = IMediaBuffer_GetLength(iMediaBuffer); + + if (toRead > 0) + { + /* + * Make sure there is enough room in processed to + * accommodate toRead. + */ + int toPop = toRead - (processed.length - processedLength); + + if (toPop > 0) + popFromProcessed(toPop); + + int read + = MediaBuffer_pop( + iMediaBuffer, + processed, processedLength, toRead); + + if (read > 0) + processedLength += read; + } + } + catch (HResultException hre) + { + logger.error( + "Failed to read from acoustic echo cancellation (AEC)" + + " output IMediaBuffer.", + hre); + break; + } + } + while ((dwStatus & DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE) + == DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE); + } + /** * {@inheritDoc} */ @@ -1206,6 +1388,15 @@ public void read(Buffer buffer) buffer.setLength(length); break; } + else + { + /* + * TODO The implementation of PushBufferStream.read(Buffer) + * should not block, it should return with whatever is + * available. + */ + yield(); + } } else { @@ -1239,102 +1430,77 @@ else if (cause instanceof IOException) */ private BufferTransferHandler runInProcessThread() { - // ProcessInput - processInput(/* capture */ 0); - /* - * If the capture endpoint device has not made any audio samples - * available, there is no input to be processed. Moreover, it is - * incorrect to input from the render endpoint device in such a case. - */ - if (maybeIMediaBufferGetLength(captureIMediaBuffer) != 0) + int captureMaxLength = this.captureBufferMaxLength; + + do { - processInput(/* render */ 1); + processInput(CAPTURE_INPUT_STREAM_INDEX, captureMaxLength); - // ProcessOutput - int dwStatus = 0; + /* + * If the capture endpoint device has not made any audio samples + * available, there is no input to be processed. Moreover, inputting + * from the render endpoint device in such a case will be + * inappropriate because it will (repeatedly) introduce random skew + * in the audio delivered by the render endpoint device. + */ + int captureLength = maybeIMediaBufferGetLength(captureIMediaBuffer); + boolean flush; - do + if (captureLength < captureMaxLength) + flush = false; + else { - try - { - IMediaObject_ProcessOutput( - iMediaObject, - /* dwFlags */ 0, - 1, - dmoOutputDataBuffer); - } - catch (HResultException hre) - { - dwStatus = 0; - logger.error("IMediaObject_ProcessOutput", hre); - } - try - { - int toRead = IMediaBuffer_GetLength(iMediaBuffer); + int renderMaxLength + = computeRenderLength( + computeCaptureDuration(captureLength)); - if (toRead > 0) - { - /* - * Make sure there is enough room in processed to - * accommodate toRead. - */ - int toPop - = toRead - (processed.length - processedLength); - - if (toPop > 0) - popFromProcessed(toPop); + processInput(RENDER_INPUT_STREAM_INDEX, renderMaxLength); - int read - = MediaBuffer_pop( - iMediaBuffer, - processed, processedLength, toRead); - - if (read > 0) - processedLength += read; - } - } - catch (HResultException hre) - { - logger.error( - "Failed to read from acoustic echo cancellation" - + " (AEC) output IMediaBuffer.", - hre); - break; - } + processOutput(); + flush = true; } - while ((dwStatus & DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE) - == DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE); - } - /* - * IMediaObject::ProcessOutput has completed which means that, as far as - * it is concerned, it does not have any input data to process. Make - * sure that the states of the IMediaBuffer instances are in accord. - */ - try - { /* - * XXX Make sure that the IMediaObject releases any IMediaBuffer - * references it holds. + * IMediaObject::ProcessOutput has completed which means that, as + * far as it is concerned, it does not have any input data to + * process. Make sure that the states of the IMediaBuffer instances + * are in accord. */ - int hresult = IMediaObject_Flush(iMediaObject); + try + { + /* + * XXX Make sure that the IMediaObject releases any IMediaBuffer + * references it holds. + */ + if (SUCCEEDED(IMediaObject_Flush(iMediaObject)) && flush) + { + captureIMediaBuffer.SetLength(0); + renderIMediaBuffer.SetLength(0); + } + } + catch (HResultException hre) + { + logger.error("IMediaBuffer_Flush", hre); + } + catch (IOException ioe) + { + logger.error("IMediaBuffer.SetLength", ioe); + } - if (SUCCEEDED(hresult)) + if (!flush) { - captureIMediaBuffer.SetLength(0); - renderIMediaBuffer.SetLength(0); + BufferTransferHandler transferHandler = this.transferHandler; + + if ((transferHandler != null) + && (processedLength >= bufferMaxLength)) + return transferHandler; + else + break; } } - catch (HResultException hre) - { - logger.error("IMediaBuffer_Flush", hre); - } - catch (IOException ioe) - { - logger.error("IMediaBuffer::SetLength", ioe); - } + while (true); - return (processedLength >= bufferMaxLength) ? transferHandler : null; + return null; } /** @@ -1498,6 +1664,7 @@ public synchronized void stop() { waitWhileRenderIsBusy(); render.stop(); + replenishRender = true; } started = false; @@ -1531,12 +1698,17 @@ private void transferCaptureData() * Notifies this instance that audio data has been made available in * {@link #render}. */ + @SuppressWarnings("unused") private void transferRenderData() { - synchronized (this) - { - notifyAll(); - } + /* + * This is a CaptureDevice and its goal is to push the audio samples + * delivered by the capture endpoint device out in their entirety. When + * the render endpoint device pushes and whether it pushes frequently + * and sufficiently enough to stay in sync with the capture endpoint + * device for the purposes of the acoustic echo cancellation (AEC) is a + * separate question. + */ } private void uninitializeAEC() diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/WASAPIRenderer.java b/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/WASAPIRenderer.java index b795421f..64543bcd 100644 --- a/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/WASAPIRenderer.java +++ b/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/WASAPIRenderer.java @@ -32,12 +32,6 @@ public class WASAPIRenderer extends AbstractAudioRenderer<WASAPISystem> { - /** - * The default base of the endpoint buffer capacity in milliseconds to - * initialize new <tt>IAudioClient</tt> instances with. - */ - private static final long DEFAULT_BUFFER_DURATION = 60; - /** * The <tt>Logger</tt> used by the <tt>WASAPIRenderer</tt> class and its * instances to log debug information. @@ -316,14 +310,13 @@ public synchronized void open() try { - long hnsBufferDuration = DEFAULT_BUFFER_DURATION * 10000; long iAudioClient = audioSystem.initializeIAudioClient( locator, dataFlow, /* streamFlags */ 0, eventHandle, - hnsBufferDuration, + WASAPISystem.DEFAULT_BUFFER_DURATION, formats); if (iAudioClient == 0) -- GitLab