From ae76b616b6422bb7f11467a3e7b954b93bd2860d Mon Sep 17 00:00:00 2001
From: Lyubomir Marinov <lyubomir.marinov@jitsi.org>
Date: Fri, 5 Jul 2013 15:21:15 +0300
Subject: [PATCH] Attempts to optimize and improve the acoustic echo
 cancellation (AEC) with Windows Audio Session API (WASAPI).

---
 .../impl/neomedia/device/WASAPISystem.java    |   8 +
 .../protocol/wasapi/AudioCaptureClient.java   | 186 +++++--
 .../media/protocol/wasapi/DataSource.java     |  19 +
 .../media/protocol/wasapi/IMediaBuffer.java   |  36 ++
 .../media/protocol/wasapi/PtrMediaBuffer.java | 110 +++++
 .../media/protocol/wasapi/WASAPIStream.java   | 458 +++++++++++++-----
 6 files changed, 671 insertions(+), 146 deletions(-)
 create mode 100644 src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/IMediaBuffer.java
 create mode 100644 src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/PtrMediaBuffer.java

diff --git a/src/org/jitsi/impl/neomedia/device/WASAPISystem.java b/src/org/jitsi/impl/neomedia/device/WASAPISystem.java
index a3c35604..db257d45 100644
--- a/src/org/jitsi/impl/neomedia/device/WASAPISystem.java
+++ b/src/org/jitsi/impl/neomedia/device/WASAPISystem.java
@@ -893,6 +893,14 @@ protected String getRendererClassName()
         return WASAPIRenderer.class.getName();
     }
 
+    /**
+     * Initializes a new <tt>IMediaObject</tt> instance which represents a Voice
+     * Capture DSP implementing acoustic echo cancellation (AEC).
+     *
+     * @return a new <tt>IMediaObject</tt> instance which represents a Voice
+     * Capture DSP implementing acoustic echo cancellation (AEC)
+     * @throws Exception if initializing the new instance fails
+     */
     public long initializeAEC()
         throws Exception
     {
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 962fb9b2..979b9a40 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
@@ -19,6 +19,18 @@
 import org.jitsi.impl.neomedia.jmfext.media.renderer.audio.*;
 import org.jitsi.util.*;
 
+/**
+ * Abstracts the initialization of an <tt>IAudioCaptureClient</tt> instance from
+ * a <tt>MediaLocator</tt>, the input of data from that
+ * <tt>IAudioCaptureClient</tt>, the buffering of that data, the starting,
+ * stopping and closing of the <tt>IAudioCaptureClient</tt>. Allows
+ * {@link WASAPIStream} to simultaneously utilize multiple
+ * <tt>IAudioCaptureClient</tt> instances (e.g. in the case of acoustic echo
+ * cancellation in which audio is input from both the capture and the render
+ * endpoint devices).
+ *
+ * @author Lyubomir Marinov
+ */
 public class AudioCaptureClient
 {
     /**
@@ -35,18 +47,17 @@ public class AudioCaptureClient
         = Logger.getLogger(AudioCaptureClient.class);
 
     /**
-     * The number of frames to be filled in a <tt>Buffer</tt> in an invocation
-     * of {@link #read(Buffer)}. If this instance implements the
-     * <tt>PushBufferStream</tt> interface,
+     * The number of audio frames to be filled in a <tt>byte[]</tt> in an
+     * invocation of {@link #read(byte[], int, int)}. The method
      * {@link #runInEventHandleCmd(Runnable)} will push via
      * {@link BufferTransferHandler#transferData(PushBufferStream)} when
-     * {@link #iAudioClient} has made at least that many frames available.
+     * {@link #iAudioClient} has made at least that many audio frames available.
      */
     private int bufferFrames;
 
     /**
-     * The size/length in bytes of the <tt>Buffer</tt> to be filled in an
-     * invocation of {@link #read(Buffer)}.
+     * The size/length in bytes of the <tt>byte[]</tt> to be filled in an
+     * invocation of {@link #read(byte[], int, int)}.
      */
     final int bufferSize;
 
@@ -54,7 +65,8 @@ public class AudioCaptureClient
      * The indicator which determines whether the audio stream represented by
      * this instance, {@link #iAudioClient} and {@link #iAudioCaptureClient} is
      * busy and, consequently, its state should not be modified. For example,
-     * the audio stream is busy during the execution of {@link #read(Buffer)}.
+     * the audio stream is busy during the execution of
+     * {@link #read(byte[], int, int)}.
      */
     private boolean busy;
 
@@ -65,21 +77,20 @@ public class AudioCaptureClient
     final long devicePeriod;
 
     /**
-     * The number of channels which which this <tt>SourceStream</tt> has been
-     * connected.
+     * The number of channels of the audio data made available by this instance.
      */
     private int dstChannels;
 
     /**
-     * The frame size in bytes with which this <tt>SourceStream</tt> has been
-     * connected. It is the product of {@link #dstSampleSize} and
+     * The frame size in bytes of the audio data made available by this
+     * instance. It is the product of {@link #dstSampleSize} and
      * {@link #dstChannels}.
      */
     private int dstFrameSize;
 
     /**
-     * The sample size in bytes with which this <tt>SourceStream</tt> has been
-     * connected.
+     * The sample size in bytes of the audio data made available by this
+     * instance.
      */
     private int dstSampleSize;
 
@@ -90,9 +101,9 @@ public class AudioCaptureClient
     private long eventHandle;
 
     /**
-     * The <tt>Runnable</tt> which is scheduled by this <tt>WASAPIStream</tt>
-     * and executed by {@link #eventHandleExecutor} and waits for
-     * {@link #eventHandle} to be signaled.
+     * The <tt>Runnable</tt> which is scheduled by this instance and executed by
+     * {@link #eventHandleExecutor} and waits for {@link #eventHandle} to be
+     * signaled.
      */
     private Runnable eventHandleCmd;
 
@@ -104,27 +115,37 @@ public class AudioCaptureClient
 
     /**
      * The WASAPI <tt>IAudioCaptureClient</tt> obtained from
-     * {@link #iAudioClient} which enables this <tt>SourceStream</tt> to read
-     * input data from the capture endpoint buffer.
+     * {@link #iAudioClient} which enables this instance to read input data from
+     * the capture endpoint buffer.
      */
     private long iAudioCaptureClient;
 
     /**
      * The WASAPI <tt>IAudioClient</tt> instance which enables this
-     * <tt>SourceStream</tt> to create and initialize an audio stream between
-     * this <tt>SourceStream</tt> and the audio engine of the associated audio
+     * <tt>AudioCaptureClient</tt> to create and initialize an audio stream
+     * between this instance and the audio engine of the associated audio
      * endpoint device.
      */
     private long iAudioClient;
 
     /**
-     * The <tt>AudioFormat</tt> of the data output by this
+     * The <tt>AudioFormat</tt> of the data output/made available by this
      * <tt>AudioCaptureClient</tt>.
      */
     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;
 
     /**
@@ -140,14 +161,50 @@ public class AudioCaptureClient
     private int srcSampleSize;
 
     /**
-     * The indicator which determines whether this <tt>SourceStream</tt> is
-     * started i.e. there has been a successful invocation of {@link #start()}
-     * without an intervening invocation of {@link #stop()}.
+     * The indicator which determines whether this <tt>AudioCaptureClient</tt>
+     * is started i.e. there has been a successful invocation of
+     * {@link #start()} without an intervening invocation of {@link #stop()}.
      */
     private boolean started;
 
+    /**
+     * The <tt>BufferTransferHandler</tt> which is to be invoked when this
+     * instance has made audio data available to be read via
+     * {@link #read(byte[], int, int)}.
+     * {@link BufferTransferHandler#transferData(PushBufferStream)} will be
+     * called with a <tt>null</tt> argument because <tt>AudioCaptureClient</tt>
+     * does not implement <tt>PushBufferStream</tt> and has rather been
+     * refactored out of a <tt>PushBufferStream</tt> implementation (i.e.
+     * <tt>WASAPIStream</tt>).
+     */
     private final BufferTransferHandler transferHandler;
 
+    /**
+     * Initializes a new <tt>AudioCaptureClient</tt> instance.
+     *
+     * @param audioSystem the <tt>WASAPISystem</tt> instance which has
+     * contributed <tt>locator</tt> 
+     * @param locator a <tt>MediaLocator</tt> which identifies the audio
+     * endpoint device to be opened and read by the new instance 
+     * @param dataFlow the <tt>AudioSystem.DataFlow</tt> of the audio endpoint
+     * device identified by <tt>locator</tt>. If
+     * <tt>AudioSystem.DataFlow.PLAYBACK</tt> and <tt>streamFlags</tt> includes
+     * {@link WASAPI#AUDCLNT_STREAMFLAGS_LOOPBACK}, allows opening a render
+     * endpoint device in loopback mode and inputing the data that is being
+     * 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 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
+     * initialized with a different <tt>AudioFormat</tt> in which case the new
+     * instance will automatically transcode the data input from the
+     * <tt>IAudioCaptureClient</tt> into the specified <tt>outFormat</tt>.
+     * @param transferHandler the <tt>BufferTransferHandler</tt> to be invoked
+     * when the new instance has made data available to be read via
+     * {@link #read(byte[], int, int)}
+     * @throws Exception if the initialization of the new instance fails
+     */
     public AudioCaptureClient(
             WASAPISystem audioSystem,
             MediaLocator locator,
@@ -294,6 +351,10 @@ public AudioCaptureClient(
         }
     }
 
+    /**
+     * Releases the resources acquired by this instance throughout its lifetime
+     * and prepares it to be garbage collected.
+     */
     public void close()
     {
         if (iAudioCaptureClient != 0)
@@ -325,7 +386,26 @@ public void close()
         started = false;
     }
 
-    private int doRead(byte[] buffer, int offset, int length)
+    /**
+     * Reads audio data from the internal buffer of this instance which has
+     * previously/already been read by this instance from the associated
+     * <tt>IAudioCaptureClient</tt>. Invoked by {@link #read(byte[], int, int)}.
+     *
+     * @param buffer the <tt>byte</tt> array into which the audio data read from
+     * the internal buffer of this instance is to be written
+     * @param offset the offset into <tt>buffer</tt> at which the writing of the
+     * audio data is to begin
+     * @param length the maximum number of bytes in <tt>buffer</tt> starting at
+     * <tt>offset</tt> to be written
+     * @return the number of bytes read from the internal buffer of this
+     * instance and written into the specified <tt>buffer</tt>
+     * @throws IOException if the reading from the internal buffer of this
+     * instance or writing into the specified <tt>buffer</tt> fails
+     */
+    private int doRead(
+            IMediaBuffer iMediaBuffer,
+            byte[] buffer, int offset,
+            int length)
         throws IOException
     {
         int toRead = Math.min(length, remainderLength);
@@ -335,9 +415,14 @@ private int doRead(byte[] buffer, int offset, int length)
             read = 0;
         else
         {
-            System.arraycopy(remainder, 0, buffer, offset, toRead);
-            popFromRemainder(toRead);
-            read = toRead;
+            if (iMediaBuffer == null)
+            {
+                read = toRead;
+                System.arraycopy(remainder, 0, buffer, offset, toRead);
+            }
+            else
+                read = iMediaBuffer.push(remainder, 0, toRead);
+            popFromRemainder(read);
         }
         return read;
     }
@@ -355,8 +440,31 @@ private void popFromRemainder(int length)
             = WASAPIRenderer.pop(remainder, remainderLength, length);
     }
 
+    /**
+     * Reads audio data from this instance into a spcific <tt>byte</tt> array.
+     *
+     * @param buffer the <tt>byte</tt> array into which the audio data read from
+     * this instance is to be written
+     * @param offset the offset in <tt>buffer</tt> at which the writing of the
+     * audio data is to start
+     * @param length the maximum number of bytes in <tt>buffer</tt> starting at
+     * <tt>offset</tt> to be written
+     * @return the number of bytes read from this instance and written into the
+     * specified <tt>buffer</tt>
+     * @throws IOException if the reading from this instance or the writing into
+     * the specified <tt>buffer</tt> fails
+     */
     public int read(byte[] buffer, int offset, int length)
         throws IOException
+    {
+        return read(/* iMediaBuffer */ null, buffer, offset, length);
+    }
+
+    private int read(
+            IMediaBuffer iMediaBuffer,
+            byte[] buffer, int offset,
+            int length)
+        throws IOException
     {
         String message;
 
@@ -380,7 +488,7 @@ else if (!started)
 
         try
         {
-            read = doRead(buffer, offset, length);
+            read = doRead(iMediaBuffer, buffer, offset, length);
             cause = null;
         }
         catch (Throwable t)
@@ -413,6 +521,12 @@ else if (cause instanceof IOException)
         return read;
     }
 
+    public int read(IMediaBuffer iMediaBuffer, int length)
+        throws IOException
+    {
+        return read(iMediaBuffer, /* buffer */ null, /* offset */ 0, length);
+    }
+
     /**
      * Reads from {@link #iAudioCaptureClient} into {@link #remainder} and
      * returns a non-<tt>null</tt> <tt>BufferTransferHandler</tt> if this
@@ -593,6 +707,13 @@ private void runInEventHandleCmd(Runnable eventHandleCmd)
         }
     }
 
+    /**
+     * Starts the transfer of media from the <tt>IAudioCaptureClient</tt>
+     * identified by the <tt>MediaLocator</tt> with which this instance has
+     * been initialized.
+     *
+     * @throws IOException if the starting of the transfer of media fails
+     */
     public synchronized void start()
         throws IOException
     {
@@ -652,6 +773,13 @@ public void run()
         }
     }
 
+    /**
+     * Stops the transfer of media from the <tt>IAudioCaptureClient</tt>
+     * identified by the <tt>MediaLocator</tt> with which this instance has
+     * been initialized.
+     *
+     * @throws IOException if the stopping of the transfer of media fails
+     */
     public synchronized void stop()
         throws IOException
     {
diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/DataSource.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/DataSource.java
index e1ec07fe..098acb83 100644
--- a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/DataSource.java
+++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/DataSource.java
@@ -131,6 +131,13 @@ protected void doDisconnect()
         }
     }
 
+    /**
+     * Gets the <tt>Format</tt>s of media data supported by the audio endpoint
+     * device associated with this instance.
+     *
+     * @return the <tt>Format</tt>s of media data supported by the audio
+     * endpoint device associated with this instance
+     */
     Format[] getIAudioClientSupportedFormats()
     {
         return
@@ -139,6 +146,18 @@ Format[] getIAudioClientSupportedFormats()
                     audioSystem.getAECSupportedFormats());
     }
 
+    /**
+     * Gets the <tt>Format</tt>s of media data supported by the audio endpoint
+     * device associated with this instance.
+     *
+     * @param streamIndex the index of the <tt>SourceStream</tt> within the list
+     * of <tt>SourceStream</tt>s of this <tt>DataSource</tt> on behalf of which
+     * the query is being made
+     * @param aecSupportedFormats the list of <tt>AudioFormat</tt>s supported by
+     * the voice capture DMO implementing acoustic echo cancellation
+     * @return the <tt>Format</tt>s of media data supported by the audio
+     * endpoint device associated with this instance
+     */
     private Format[] getIAudioClientSupportedFormats(
             int streamIndex,
             List<AudioFormat> aecSupportedFormats)
diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/IMediaBuffer.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/IMediaBuffer.java
new file mode 100644
index 00000000..56b0b1f7
--- /dev/null
+++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/IMediaBuffer.java
@@ -0,0 +1,36 @@
+/*
+ * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package org.jitsi.impl.neomedia.jmfext.media.protocol.wasapi;
+
+import java.io.*;
+
+/**
+ * Defines the API of Microsoft's <tt>IMediaBuffer</tt> interface (referred to
+ * as unmanaged) and allows implementing similar abstractions on the Java side
+ * (referred to as managed).
+ *
+ * @author Lyubomir Marinov
+ */
+public interface IMediaBuffer
+{
+    int GetLength()
+        throws IOException;
+
+    int GetMaxLength()
+        throws IOException;
+
+    int pop(byte[] buffer, int offset, int length)
+        throws IOException;
+
+    int push(byte[] buffer, int offset, int length)
+        throws IOException;
+
+    int Release();
+
+    void SetLength(int length)
+        throws IOException;
+}
diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/PtrMediaBuffer.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/PtrMediaBuffer.java
new file mode 100644
index 00000000..a3ea0905
--- /dev/null
+++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/PtrMediaBuffer.java
@@ -0,0 +1,110 @@
+/*
+ * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package org.jitsi.impl.neomedia.jmfext.media.protocol.wasapi;
+
+import static org.jitsi.impl.neomedia.jmfext.media.protocol.wasapi.VoiceCaptureDSP.*;
+
+import java.io.*;
+
+/**
+ * Implements a managed <tt>IMediaBuffer</tt> which wraps around an unmanaged
+ * <tt>IMediaBuffer</tt>.
+ *
+ * @author Lyubomir Marinov
+ */
+public class PtrMediaBuffer
+    implements IMediaBuffer
+{
+    /**
+     * The unmanaged <tt>IMediaBuffer</tt> represented by this instance.
+     */
+    final long ptr;
+
+    /**
+     * Initializes a new managed <tt>IMediaBuffer</tt> which is to represent a
+     * specific unmanaged <tt>IMediaBuffer</tt>.
+     * 
+     * @param ptr the unmanaged <tt>IMediaBuffer</tt> to be represented by the
+     * new instance
+     */
+    public PtrMediaBuffer(long ptr)
+    {
+        if (ptr == 0)
+            throw new IllegalArgumentException("ptr");
+        this.ptr = ptr;
+    }
+
+    public int GetLength()
+        throws IOException
+    {
+        try
+        {
+            return IMediaBuffer_GetLength(ptr);
+        }
+        catch (HResultException hre)
+        {
+            throw new IOException(hre);
+        }
+    }
+
+    public int GetMaxLength()
+        throws IOException
+    {
+        try
+        {
+            return IMediaBuffer_GetMaxLength(ptr);
+        }
+        catch (HResultException hre)
+        {
+            throw new IOException(hre);
+        }
+    }
+
+    public int pop(byte[] buffer, int offset, int length)
+        throws IOException
+    {
+        try
+        {
+            return MediaBuffer_pop(ptr, buffer, offset, length);
+        }
+        catch (HResultException hre)
+        {
+            throw new IOException(hre);
+        }
+    }
+
+    public int push(byte[] buffer, int offset, int length)
+        throws IOException
+    {
+        try
+        {
+            return MediaBuffer_push(ptr, buffer, offset, length);
+        }
+        catch (HResultException hre)
+        {
+            throw new IOException(hre);
+        }
+    }
+
+    public int Release()
+    {
+        return IMediaBuffer_Release(ptr);
+    }
+
+    public void SetLength(int length)
+        throws IOException
+    {
+        try
+        {
+            IMediaBuffer_SetLength(ptr, length);
+        }
+        catch (HResultException hre)
+        {
+            throw new IOException(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 47c7b3f9..57dd2f0f 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
@@ -39,11 +39,23 @@ public class WASAPIStream
      */
     private static Logger logger = Logger.getLogger(WASAPIStream.class);
 
+    /**
+     * 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.
+     *
+     * @param formats the list of <tt>Format</tt>s into which an
+     * <tt>AudioFormat</tt> as similar to the specified <tt>format</tt> as
+     * possible is to be found 
+     * @param format the <tt>AudioFormat</tt> for which a similar
+     * <tt>AudioFormat</tt> is to be found in <tt>formats</tt>
+     * @return an <tt>AudioFormat</tt> which is an element of <tt>formats</tt>
+     * and is as similar to the specified <tt>format</tt> as possible or
+     * <tt>null</tt> if no similarity could be established
+     */
     private static AudioFormat findClosestMatch(
             Format[] formats,
             AudioFormat format)
     {
-
         // Try to find the very specified format.
         AudioFormat match = findFirstMatch(formats, format);
 
@@ -91,6 +103,17 @@ private static AudioFormat findClosestMatch(
         return match;
     }
 
+    /**
+     * Finds the first element of a specific array of <tt>Format</tt>s which
+     * matches in the sense of {@link Format#matches(Format)} a specific
+     * <tt>AudioFormat</tt>.
+     *
+     * @param formats the array of <tt>Format</tt>s which si to be searched
+     * @param format the <tt>AudioFormat</tt> for which a match is to be found
+     * in the specified <tt>formats</tt>
+     * @return the first element of <tt>formats</tt> which matches the specified
+     * <tt>format</tt> or <tt>null</tt> if no match could be found
+     */
     private static AudioFormat findFirstMatch(
             Format[] formats,
             AudioFormat format)
@@ -101,6 +124,29 @@ private static AudioFormat findFirstMatch(
         return null;
     }
 
+    /**
+     * Sets the media type of an input or output stream of a specific
+     * <tt>IMediaObject</tt>.
+     * 
+     * @param iMediaObject the <tt>IMediaObject</tt> to set the media type of
+     * @param inOrOut <tt>true</tt> if the media type of an input stream of the
+     * specified <tt>iMediaObject</tt> is to be set or <tt>false</tt> if the
+     * media type of an output stream of the specified <tt>iMediaObject</tT> is
+     * to be set
+     * @param dwXXXputStreamIndex the zero-based index of the input or output
+     * stream on the specified <tt>iMediaObject</tt> of which the media type is
+     * to be set
+     * @param audioFormat the <tt>AudioFormat</tt> to be set on the specified
+     * stream of the DMO
+     * @param dwFlags bitwise combination of zero or more
+     * <tt>DMO_SET_TYPEF_XXX</tt> flags (defined by the <tt>VoiceCaptureDSP</tt>
+     * class
+     * @return an <tt>HRESULT</tt> value indicating whether the specified
+     * <tt>audioFormat</tt> is acceptable and/or whether it has been set
+     * successfully
+     * @throws HResultException if setting the media type of the specified
+     * stream of the specified <tt>iMediaObject</tt> fails 
+     */
     private static int IMediaObject_SetXXXputType(
             long iMediaObject,
             boolean inOrOut,
@@ -206,6 +252,101 @@ private static int IMediaObject_SetXXXputType(
         return hresult;
     }
 
+    /**
+     * Invokes {@link IMediaBuffer#GetLength()} and logs and swallows any
+     * <tt>IOException</tt>.
+     *
+     * @param iMediaBuffer the <tt>IMediaBuffer</tt> on which the method
+     * <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>.
+     */
+    private static int maybeIMediaBufferGetLength(IMediaBuffer iMediaBuffer)
+    {
+        int length;
+
+        try
+        {
+            length = iMediaBuffer.GetLength();
+        }
+        catch (IOException ioe)
+        {
+            length = 0;
+            logger.error("IMediaBuffer::GetLength", ioe);
+        }
+        return length;
+    }
+
+    /**
+     * Invokes {@link VoiceCaptureDSP#IMediaBuffer_GetLength(long)} and logs and
+     * swallows any <tt>HResultException</tt>.
+     *
+     * @param iMediaBuffer the <tt>IMediaBuffer</tt> on which the function
+     * <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>.
+     */
+    @SuppressWarnings("unused")
+    private static int maybeIMediaBufferGetLength(long iMediaBuffer)
+    {
+        int length;
+
+        try
+        {
+            length = IMediaBuffer_GetLength(iMediaBuffer);
+        }
+        catch (HResultException hre)
+        {
+            length = 0;
+            logger.error("IMediaBuffer_GetLength", hre);
+        }
+        return length;
+    }
+
+    /**
+     * Invokes {@link VoiceCaptureDSP#MediaBuffer_push(long, byte[], int, int)}
+     * on a specific <tt>IMediaBuffer</tt> and logs and swallows any
+     * <tt>HResultException</tT>.
+     *
+     * @param pBuffer the <tt>IMediaBuffer</tt> into which the specified bytes
+     * are to be pushed/written
+     * @param buffer the bytes to be pushed/written into the specified
+     * <tt>pBuffer</tt>
+     * @param offset the offset in <tt>buffer</tt> at which the bytes to be
+     * pushed/written into the specified <tt>pBuffer</tt> start
+     * @param length the number of bytes in <tt>buffer</tt> starting at
+     * <tt>offset</tt> to be pushed/written into the specified <tt>pBuffer</tt>
+     * @return the number of bytes from the specified <tt>buffer</tt>
+     * pushed/written into the specified <tt>pBuffer</tt>  
+     */
+    private static int maybeMediaBufferPush(
+            long pBuffer,
+            byte[] buffer, int offset, int length)
+    {
+        int written;
+        Throwable exception;
+
+        try
+        {
+            written = MediaBuffer_push(pBuffer, buffer, offset, length);
+            exception = null;
+        }
+        catch (HResultException hre)
+        {
+            written = 0;
+            exception = hre;
+        }
+        if ((exception != null) || (written != length))
+        {
+            logger.error(
+                    "Failed to push/write "
+                        + ((written <= 0) ? length : (length - written))
+                        + " bytes into an IMediaBuffer.",
+                    exception);
+        }
+        return written;
+    }
+
     /**
      * Throws a new <tt>IOException</tt> instance initialized with a specific
      * <tt>String</tt> message and a specific <tt>HResultException</tt> cause.
@@ -238,8 +379,14 @@ static void throwNewIOException(String message, HResultException hre)
 
     private int captureBufferMaxLength;
 
-    private long captureIMediaBuffer;
+    private PtrMediaBuffer captureIMediaBuffer;
 
+    /**
+     * The indicator which determines whether {@link #capture} and its
+     * associated resources/states are busy and, consequently, should not be
+     * modified. For example, <tt>capture</tt> is busy during the execution of
+     * {@link #read(Buffer)}.
+     */
     private boolean captureIsBusy;
 
     /**
@@ -257,6 +404,10 @@ static void throwNewIOException(String message, HResultException hre)
 
     private long iMediaBuffer;
 
+    /**
+     * The <tt>IMediaObject</tt> reference to the Voice Capture DSP that
+     * implements the acoustic echo cancellation (AEC) feature.
+     */
     private long iMediaObject;
 
     /**
@@ -277,8 +428,14 @@ static void throwNewIOException(String message, HResultException hre)
 
     private int renderBufferMaxLength;
 
-    private long renderIMediaBuffer;
+    private PtrMediaBuffer renderIMediaBuffer;
 
+    /**
+     * The indicator which determines whether {@link #render} and its associated
+     * resources/states are busy and, consequently, should not be modified. For
+     * example, <tt>render</tt> is busy during the execution of
+     * {@link #read(Buffer)}.
+     */
     private boolean renderIsBusy;
 
     /**
@@ -304,7 +461,7 @@ public WASAPIStream(DataSource dataSource, FormatControl formatControl)
     }
 
     /**
-     * Performs optional configuration of the Voice Capture DSP that implements
+     * Performs optional configuration on the Voice Capture DSP that implements
      * acoustic echo cancellation (AEC).
      *
      * @param iPropertyStore a reference to the <tt>IPropertyStore</tt>
@@ -320,6 +477,35 @@ private void configureAEC(long iPropertyStore)
          * property to true and override the default settings on the
          * MFPKEY_WMAAECMA_FEATR_XXX properties of the Voice Capture DSP. 
          */
+        try
+        {
+            if (MFPKEY_WMAAECMA_FEATURE_MODE != 0)
+            {
+                IPropertyStore_SetValue(
+                        iPropertyStore,
+                        MFPKEY_WMAAECMA_FEATURE_MODE, true);
+                if (MFPKEY_WMAAECMA_FEATR_AES != 0)
+                {
+                    IPropertyStore_SetValue(
+                            iPropertyStore,
+                            MFPKEY_WMAAECMA_FEATR_AES, 2);
+                }
+                if (MFPKEY_WMAAECMA_FEATR_ECHO_LENGTH != 0)
+                {
+                    IPropertyStore_SetValue(
+                            iPropertyStore,
+                            MFPKEY_WMAAECMA_FEATR_ECHO_LENGTH, 256);
+                }
+            }
+        }
+        catch (HResultException hre)
+        {
+            logger.error(
+                    "Failed to perform optional configuration on the Voice"
+                        + " Capture DSP that implements acoustic echo"
+                        + " cancellation (AEC).",
+                    hre);
+        }
     }
 
     /**
@@ -654,7 +840,8 @@ private void initializeAEC(
                                 processed = new byte[bufferMaxLength * 3];
                                 processedLength = 0;
 
-                                this.captureIMediaBuffer = captureIMediaBuffer;
+                                this.captureIMediaBuffer
+                                    = new PtrMediaBuffer(captureIMediaBuffer);
                                 captureIMediaBuffer = 0;
                                 this.dmoOutputDataBuffer = dmoOutputDataBuffer;
                                 dmoOutputDataBuffer = 0;
@@ -662,7 +849,8 @@ private void initializeAEC(
                                 iMediaBuffer = 0;
                                 this.iMediaObject = iMediaObject;
                                 iMediaObject = 0;
-                                this.renderIMediaBuffer = renderIMediaBuffer;
+                                this.renderIMediaBuffer
+                                    = new PtrMediaBuffer(renderIMediaBuffer);
                                 renderIMediaBuffer = 0;
                             }
                             finally
@@ -757,21 +945,29 @@ private void popFromProcessed(int length)
             = WASAPIRenderer.pop(processed, processedLength, length);
     }
 
-    private int processInput(int dwInputStreamIndex)
+    /**
+     * Inputs audio samples from {@link #capture} or {@link #render} and
+     * delivers them to {@link #iMediaObject} which implements the acoustic echo
+     * cancellation (AEC) feature.
+     *
+     * @param dwInputStreamIndex the zero-based index of the input stream on
+     * <tt>iMediaObject</tt> to which audio samples are to be delivered
+     */
+    private void processInput(int dwInputStreamIndex)
     {
-        long pBuffer;
+        PtrMediaBuffer oBuffer;
         int maxLength;
         AudioCaptureClient audioCaptureClient;
 
         switch (dwInputStreamIndex)
         {
         case 0:
-            pBuffer = captureIMediaBuffer;
+            oBuffer = captureIMediaBuffer;
             maxLength = captureBufferMaxLength;
             audioCaptureClient = capture;
             break;
         case 1:
-            pBuffer = renderIMediaBuffer;
+            oBuffer = renderIMediaBuffer;
             maxLength = renderBufferMaxLength;
             audioCaptureClient = render;
             break;
@@ -779,11 +975,15 @@ private int processInput(int dwInputStreamIndex)
             throw new IllegalArgumentException("dwInputStreamIndex");
         }
 
+        long pBuffer = oBuffer.ptr;
         int hresult = S_OK;
-        int processed = 0;
 
         do
         {
+            /*
+             * Attempt to deliver audio samples to the specified input stream
+             * only if it accepts input data at this time.
+             */
             int dwFlags;
 
             try
@@ -802,6 +1002,12 @@ private int processInput(int dwInputStreamIndex)
             if ((dwFlags & DMO_INPUT_STATUSF_ACCEPT_DATA)
                     == DMO_INPUT_STATUSF_ACCEPT_DATA)
             {
+                /*
+                 * The specified input stream reports that it accepts input data
+                 * at this time so read audio samples from the associated
+                 * AudioCaptureClient and then deliver them to the specified
+                 * input stream.
+                 */
                 int toRead;
 
                 try
@@ -816,57 +1022,34 @@ private int processInput(int dwInputStreamIndex)
                 }
                 if (toRead > 0)
                 {
-                    if ((processInputBuffer == null)
-                            || (processInputBuffer.length < toRead))
-                        processInputBuffer = new byte[toRead];
-
-                    int read;
-
+                    /*
+                     * Read audio samples from the associated
+                     * AudioCaptureClient.
+                     */
                     try
                     {
-                        read
-                            = audioCaptureClient.read(
-                                    processInputBuffer,
-                                    0,
-                                    toRead);
+                        audioCaptureClient.read(oBuffer, toRead);
                     }
                     catch (IOException ioe)
                     {
-                        read = 0;
                         logger.error(
                                 "Failed to read from IAudioCaptureClient.",
                                 ioe);
                     }
-                    if (read > 0)
-                    {
-                        int written;
-
-                        try
-                        {
-                            written
-                                = MediaBuffer_push(
-                                        pBuffer,
-                                        processInputBuffer, 0, read);
-                        }
-                        catch (HResultException hre)
-                        {
-                            written = 0;
-                            logger.error("MediaBuffer_push", hre);
-                        }
-                        if (written < read)
-                        {
-                            logger.error(
-                                    "Failed to push/write "
-                                        + ((written <= 0)
-                                                ? read
-                                                : (read - written))
-                                        + " bytes into an IMediaBuffer.");
-                        }
-                        if (written > 0)
-                            processed += written;
-                    }
                 }
 
+                /*
+                 * If the capture endpoint device has delivered audio samples,
+                 * they have to go through the acoustic echo cancellation (AEC)
+                 * regardless of whether the render endpoint device has
+                 * delivered audio samples. Additionally, the duration of the
+                 * audio samples delivered by the render has to be the same as
+                 * the duration of the audio samples delivered by the capture in
+                 * order to have the audio samples delivered by the capture pass
+                 * through the voice capture DSO in entirety. To achieve the
+                 * above, read from the render endpoint device as many audio
+                 * samples as possible and pad with silence if necessary.
+                 */
                 if (dwInputStreamIndex == 1)
                 {
                     int length;
@@ -890,19 +1073,16 @@ private int processInput(int dwInputStreamIndex)
                                 || (processInputBuffer.length < silence))
                             processInputBuffer = new byte[silence];
                         Arrays.fill(processInputBuffer, 0, silence, (byte) 0);
-                        try
-                        {
-                            MediaBuffer_push(
-                                    pBuffer,
-                                    processInputBuffer, 0, silence);
-                        }
-                        catch (HResultException hre)
-                        {
-                            logger.error("MediaBuffer_push", hre);
-                        }
+                        maybeMediaBufferPush(
+                                pBuffer,
+                                processInputBuffer, 0, silence);
                     }
                 }
 
+                /*
+                 * Deliver the audio samples read from the associated
+                 * AudioCaptureClient to the specified input stream.
+                 */
                 try
                 {
                     hresult
@@ -925,8 +1105,6 @@ private int processInput(int dwInputStreamIndex)
                 break; // The input stream cannot accept more input data.
         }
         while (SUCCEEDED(hresult));
-
-        return processed;
     }
 
     /**
@@ -1047,65 +1225,86 @@ else if (cause instanceof IOException)
         while (true);
     }
 
+    /**
+     * Executed by {@link #processThread} and invoked by
+     * {@link #runInProcessThread(Thread)}, inputs audio samples from
+     * {@link #capture} and {@link #render}, delivers them to
+     * {@link #iMediaBuffer} which implements the acoustic echo cancellation
+     * (AEC) features and produces output and caches the output so that it can
+     * be read out of this instance via {@link #read(Buffer)}.
+     *
+     * @return a <tt>BufferTransferHandler</tt> to be invoked if the method has
+     * made available audio samples to be read out of this instance; otherwise,
+     * <tt>null</tt>
+     */
     private BufferTransferHandler runInProcessThread()
     {
         // ProcessInput
         processInput(/* capture */ 0);
-        processInput(/* render */ 1);
+        /*
+         * 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)
+        {
+            processInput(/* render */ 1);
 
-        // ProcessOutput
-        int dwStatus = 0;
+            // ProcessOutput
+            int dwStatus = 0;
 
-        do
-        {
-            try
-            {
-                IMediaObject_ProcessOutput(
-                        iMediaObject,
-                        /* dwFlags */ 0,
-                        1,
-                        dmoOutputDataBuffer);
-            }
-            catch (HResultException hre)
-            {
-                dwStatus = 0;
-                logger.error("IMediaObject_ProcessOutput", hre);
-            }
-            try
+            do
             {
-                int toRead = IMediaBuffer_GetLength(iMediaBuffer);
-
-                if (toRead > 0)
+                try
                 {
-                    /*
-                     * Make sure there is enough room in processed to
-                     * accommodate toRead.
-                     */
-                    int toPop = toRead - (processed.length - processedLength);
+                    IMediaObject_ProcessOutput(
+                            iMediaObject,
+                            /* dwFlags */ 0,
+                            1,
+                            dmoOutputDataBuffer);
+                }
+                catch (HResultException hre)
+                {
+                    dwStatus = 0;
+                    logger.error("IMediaObject_ProcessOutput", hre);
+                }
+                try
+                {
+                    int toRead = IMediaBuffer_GetLength(iMediaBuffer);
 
-                    if (toPop > 0)
-                        popFromProcessed(toPop);
+                    if (toRead > 0)
+                    {
+                        /*
+                         * Make sure there is enough room in processed to
+                         * accommodate toRead.
+                         */
+                        int toPop
+                            = toRead - (processed.length - processedLength);
 
-                    int read
-                        = MediaBuffer_pop(
-                                iMediaBuffer,
-                                processed, processedLength, toRead);
+                        if (toPop > 0)
+                            popFromProcessed(toPop);
 
-                    if (read > 0)
-                        processedLength += read;
+                        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;
                 }
             }
-            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);
         }
-        while ((dwStatus & DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE)
-                == DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE);
 
         /*
          * IMediaObject::ProcessOutput has completed which means that, as far as
@@ -1122,18 +1321,31 @@ private BufferTransferHandler runInProcessThread()
 
             if (SUCCEEDED(hresult))
             {
-                IMediaBuffer_SetLength(captureIMediaBuffer, 0);
-                IMediaBuffer_SetLength(renderIMediaBuffer, 0);
+                captureIMediaBuffer.SetLength(0);
+                renderIMediaBuffer.SetLength(0);
             }
         }
         catch (HResultException hre)
         {
-            logger.error("IMediaBuffer_SetLength", hre);
+            logger.error("IMediaBuffer_Flush", hre);
+        }
+        catch (IOException ioe)
+        {
+            logger.error("IMediaBuffer::SetLength", ioe);
         }
 
         return (processedLength >= bufferMaxLength) ? transferHandler : null;
     }
 
+    /**
+     * Executed by {@link #processThread}, inputs audio samples from
+     * {@link #capture} and {@link #render}, delivers them to
+     * {@link #iMediaBuffer} which implements the acoustic echo cancellation
+     * (AEC) features and produces output and caches the output so that it can
+     * be read out of this instance via {@link #read(Buffer)}.
+     *
+     * @param processThread the <tt>Thread</tt> which is executing the method
+     */
     private void runInProcessThread(Thread processThread)
     {
         try
@@ -1293,6 +1505,10 @@ public synchronized void stop()
         processedLength = 0;
     }
 
+    /**
+     * Notifies this instance that audio data has been made available in
+     * {@link #capture}.
+     */
     private void transferCaptureData()
     {
         if (dataSource.aec)
@@ -1311,6 +1527,10 @@ private void transferCaptureData()
         }
     }
 
+    /**
+     * Notifies this instance that audio data has been made available in
+     * {@link #render}.
+     */
     private void transferRenderData()
     {
         synchronized (this)
@@ -1336,15 +1556,15 @@ private void uninitializeAEC()
             IMediaBuffer_Release(iMediaBuffer);
             iMediaBuffer = 0;
         }
-        if (renderIMediaBuffer != 0)
+        if (renderIMediaBuffer != null)
         {
-            IMediaBuffer_Release(renderIMediaBuffer);
-            renderIMediaBuffer =0;
+            renderIMediaBuffer.Release();
+            renderIMediaBuffer = null;
         }
-        if (captureIMediaBuffer != 0)
+        if (captureIMediaBuffer != null)
         {
-            IMediaBuffer_Release(captureIMediaBuffer);
-            captureIMediaBuffer = 0;
+            captureIMediaBuffer.Release();
+            captureIMediaBuffer = null;
         }
     }
 
@@ -1389,6 +1609,10 @@ private synchronized void waitWhileCaptureIsBusy()
             Thread.currentThread().interrupt();
     }
 
+    /**
+     * Waits on this instance while the value of {@link #precessThread} is not
+     * equal to <tt>null</tt>.
+     */
     private synchronized void waitWhileProcessThread()
     {
         while (processThread != null)
-- 
GitLab