From 224d417f502f5f93f617c5b387620fdabdc91f2d Mon Sep 17 00:00:00 2001 From: Sven Gothel Date: Sun, 9 Jul 2023 16:37:04 +0200 Subject: ALAudioSink: Utilize AL_SOFT_events if available, i.e. use callback for released buffer count instead of polling With wait == true, we simply wait until enough buffers have arrived, otherwise take what we got - both w/o polling and querying the alSource. --- make/scripts/make.joal.all.linux-x86_64.sh | 3 + make/scripts/tests.sh | 5 +- src/java/com/jogamp/openal/util/ALAudioSink.java | 139 ++++++++++++++------- .../com/jogamp/openal/test/manual/Synth02bAL.java | 121 ++++++++++++++++++ 4 files changed, 225 insertions(+), 43 deletions(-) create mode 100644 src/test/com/jogamp/openal/test/manual/Synth02bAL.java diff --git a/make/scripts/make.joal.all.linux-x86_64.sh b/make/scripts/make.joal.all.linux-x86_64.sh index d2346a4..f348300 100755 --- a/make/scripts/make.joal.all.linux-x86_64.sh +++ b/make/scripts/make.joal.all.linux-x86_64.sh @@ -18,6 +18,9 @@ if [ -z "$ANT_PATH" ] ; then exit fi +# -Dc.compiler.debug=true \ +# -Djavacdebuglevel="source,lines,vars" \ + export SOURCE_LEVEL=1.8 export TARGET_LEVEL=1.8 export TARGET_RT_JAR=/opt-share/jre1.8.0_212/lib/rt.jar diff --git a/make/scripts/tests.sh b/make/scripts/tests.sh index f7a1084..5342a22 100755 --- a/make/scripts/tests.sh +++ b/make/scripts/tests.sh @@ -72,6 +72,8 @@ function jrun() { #D_ARGS="-Djoal.debug=all" #D_ARGS="-Djogamp.debug.JNILibLoader" #X_ARGS="-verbose:jni" + #X_ARGS="-Xcheck:jni" + #X_ARGS="-verbose:jni -Xcheck:jni -Xrs -Xnoclassgc" #X_ARGS="-Xrs" # StartFlightRecording: delay=10s, @@ -105,7 +107,8 @@ function testnormal() { #testnormal com.jogamp.openal.test.manual.OpenALTest $* #testnormal com.jogamp.openal.test.manual.Sound3DTest $* #testnormal com.jogamp.openal.test.manual.Synth01AL $* -testnormal com.jogamp.openal.test.manual.Synth02AL $* +#testnormal com.jogamp.openal.test.manual.Synth02AL $* +testnormal com.jogamp.openal.test.manual.Synth02bAL $* #testnormal com.jogamp.openal.test.junit.ALVersionTest $* #testnormal com.jogamp.openal.test.junit.ALutWAVLoaderTest $* #testnormal com.jogamp.openal.test.junit.ALExtLoopbackDeviceSOFTTest $* diff --git a/src/java/com/jogamp/openal/util/ALAudioSink.java b/src/java/com/jogamp/openal/util/ALAudioSink.java index 8342a4d..cc74be7 100644 --- a/src/java/com/jogamp/openal/util/ALAudioSink.java +++ b/src/java/com/jogamp/openal/util/ALAudioSink.java @@ -50,6 +50,8 @@ import com.jogamp.openal.ALCdevice; import com.jogamp.openal.ALConstants; import com.jogamp.openal.ALException; import com.jogamp.openal.ALExt; +import com.jogamp.openal.ALExt.ALEVENTPROCSOFT; +import com.jogamp.openal.ALExtConstants; import com.jogamp.openal.sound3d.AudioSystem3D; import com.jogamp.openal.sound3d.Context; import com.jogamp.openal.sound3d.Device; @@ -79,6 +81,7 @@ public final class ALAudioSink implements AudioSink { private boolean hasEXTFloat32; private boolean hasEXTDouble; private boolean hasALC_thread_local_context; + private boolean hasAL_SOFT_events; private int sourceCount; private float defaultLatency; private float latency; @@ -213,8 +216,9 @@ public final class ALAudioSink implements AudioSink { hasEXTMcFormats = al.alIsExtensionPresent(ALHelpers.AL_EXT_MCFORMATS); hasEXTFloat32 = al.alIsExtensionPresent(ALHelpers.AL_EXT_FLOAT32); hasEXTDouble = al.alIsExtensionPresent(ALHelpers.AL_EXT_DOUBLE); - hasALC_thread_local_context = alc.alcIsExtensionPresent(null, ALHelpers.ALC_EXT_thread_local_context) || - alc.alcIsExtensionPresent(device.getALDevice(), ALHelpers.ALC_EXT_thread_local_context) ; + hasALC_thread_local_context = context.hasALC_thread_local_context; + hasAL_SOFT_events = al.alIsExtensionPresent(ALHelpers.AL_SOFT_events); + int checkErrIter = 1; AudioSystem3D.checkError(device, "init."+checkErrIter++, DEBUG, false); int defaultSampleRate = DefaultFormat.sampleRate; @@ -279,6 +283,7 @@ public final class ALAudioSink implements AudioSink { System.out.println("ALAudioSink: hasEXTFloat32 "+hasEXTFloat32); System.out.println("ALAudioSink: hasEXTDouble "+hasEXTDouble); System.out.println("ALAudioSink: hasALC_thread_local_context "+hasALC_thread_local_context); + System.out.println("ALAudioSink: hasAL_SOFT_events "+hasAL_SOFT_events); System.out.println("ALAudioSink: maxSupportedChannels "+getMaxSupportedChannels(false)); System.out.println("ALAudioSink: nativeAudioFormat "+nativeFormat); System.out.println("ALAudioSink: defaultMixerRefreshRate "+(1000f*defaultLatency)+" ms, "+(1f/defaultLatency)+" Hz"); @@ -632,6 +637,14 @@ public final class ALAudioSink implements AudioSink { alFramesPlaying.dump(System.err, "Playi-init"); } } + if( hasAL_SOFT_events ) { + alExt.alEventCallbackSOFT(alEventCallback, context.getALContext()); + alExt.alEventControlSOFT(1, new int[] { ALExtConstants.AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT + // , ALExtConstants.AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT + // , ALExtConstants.AL_EVENT_TYPE_DISCONNECTED_SOFT + }, 0, true); + System.err.println("CALLBACK registered"); + } } finally { if( releaseContext ) { release(false /* throw */); @@ -662,8 +675,8 @@ public final class ALAudioSink implements AudioSink { } */ private boolean growBuffers(final int addByteCount) { - if( !alFramesFree.isEmpty() || !alFramesPlaying.isFull() ) { - throw new InternalError("Buffers: Avail is !empty "+alFramesFree+" or Playing is !full "+alFramesPlaying); + if( !hasAL_SOFT_events && !alFramesFree.isEmpty() || !alFramesPlaying.isFull() ) { + throw new InternalError("Buffers: Avail is !empty "+alFramesFree+" or Playing is !full "+alFramesPlaying+", while !hasAL_SOFT_events"); } final float addDuration = chosenFormat.getBytesDuration(addByteCount); // [s] final float queuedDuration = chosenFormat.getBytesDuration(alBufferBytesQueued); // [s] @@ -748,6 +761,7 @@ public final class ALAudioSink implements AudioSink { available = false; if( null != context ) { makeCurrent(true /* throw */); + alExt.alEventCallbackSOFT(null, context.getALContext()); } try { stopImpl(true); @@ -765,6 +779,29 @@ public final class ALAudioSink implements AudioSink { return available; } + final ALEVENTPROCSOFT alEventCallback = new ALEVENTPROCSOFT() { + @SuppressWarnings("unused") + @Override + public void callback(final int eventType, final int object, final int param, + final int length, final String message, final Object userParam) { + if( false ) { + final com.jogamp.openal.ALContextKey k = new com.jogamp.openal.ALContextKey(userParam); + System.err.println("ALAudioSink.Event: type "+toHexString(eventType)+", obj "+toHexString(object)+", param "+param+ + ", msg[len "+length+", val '"+message+"'], userParam "+k); + } + if( ALExtConstants.AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT == eventType && + alSource.getID() == object ) + { + synchronized( eventReleasedBuffersLock ) { + eventReleasedBuffers += param; + eventReleasedBuffersLock.notifyAll(); + } + } + } + }; + private final Object eventReleasedBuffersLock = new Object(); + private volatile int eventReleasedBuffers = 0; + /** * Dequeuing playing audio frames. * @param wait if true, waiting for completion of audio buffers @@ -775,57 +812,74 @@ public final class ALAudioSink implements AudioSink { final int releaseBufferCount; if( alBufferBytesQueued > 0 ) { final int releaseBufferLimes = Math.max(1, alFramesPlaying.size() / 4 ); - final int sleepLimes = Math.round( releaseBufferLimes * 1000f*avgFrameDuration ); - int i=0; - int slept = 0; + final long sleepLimes = Math.round( releaseBufferLimes * 1000.0*avgFrameDuration ); + int wait_cycles=0; + long slept = 0; int releasedBuffers = 0; boolean onceBusyDebug = true; final long t0 = DEBUG ? Clock.currentNanos() : 0; do { - releasedBuffers = alSource.getBuffersProcessed(); - if( wait && releasedBuffers < releaseBufferLimes ) { - i++; - // clip wait at [avgFrameDuration .. 300] ms - final int sleep = Math.max(2, Math.min(300, Math.round( (releaseBufferLimes-releasedBuffers) * 1000f*avgFrameDuration) ) ) - 1; // 1 ms off for busy-loop - if( slept + sleep + 1 <= sleepLimes ) { - if( DEBUG ) { - System.err.println(getThreadName()+": ALAudioSink: Dequeue.wait-sleep["+i+"].1:"+ - "releaseBuffers "+releasedBuffers+"/"+releaseBufferLimes+", sleep "+sleep+"/"+slept+"/"+sleepLimes+ - " ms, playImpl "+(ALConstants.AL_PLAYING == getSourceState(false))+", "+shortString()); - } - release(true /* throw */); - try { - Thread.sleep( sleep ); - slept += sleep; - } catch (final InterruptedException e) { - } finally { - makeCurrent(true /* throw */); + if( hasAL_SOFT_events ) { + synchronized( eventReleasedBuffersLock ) { + while( wait && alBufferBytesQueued > 0 && eventReleasedBuffers < releaseBufferLimes ) { + wait_cycles++; + try { + eventReleasedBuffersLock.wait(); + } catch (final InterruptedException e) { } } - } else { - // Empirical best behavior w/ openal-soft (sort of needs min ~21ms to complete processing a buffer even if period < 20ms?) + releasedBuffers = eventReleasedBuffers; + eventReleasedBuffers = 0; if( DEBUG ) { - if( onceBusyDebug ) { - System.err.println(getThreadName()+": ALAudioSink: Dequeue.wait-sleep["+i+"].2:"+ - "releaseBuffers "+releasedBuffers+"/"+releaseBufferLimes+", sleep "+sleep+"->1/"+slept+"/"+sleepLimes+ + slept += TimeUnit.NANOSECONDS.toMillis(Clock.currentNanos()-t0); + System.err.println(getThreadName()+": ALAudioSink.Event.wait["+wait_cycles+"]: released buffer count "+releasedBuffers+", limes "+releaseBufferLimes+", slept "+slept+" ms, free total "+alFramesFree.size()); + } + } + } else { + releasedBuffers = alSource.getBuffersProcessed(); + if( wait && releasedBuffers < releaseBufferLimes ) { + wait_cycles++; + // clip wait at [avgFrameDuration .. 300] ms + final int sleep = Math.max(2, Math.min(300, Math.round( (releaseBufferLimes-releasedBuffers) * 1000f*avgFrameDuration) ) ) - 1; // 1 ms off for busy-loop + if( slept + sleep + 1 <= sleepLimes ) { + if( DEBUG ) { + System.err.println(getThreadName()+": ALAudioSink: Dequeue.wait-sleep["+wait_cycles+"].1:"+ + "releaseBuffers "+releasedBuffers+"/"+releaseBufferLimes+", sleep "+sleep+"/"+slept+"/"+sleepLimes+ " ms, playImpl "+(ALConstants.AL_PLAYING == getSourceState(false))+", "+shortString()); - onceBusyDebug = false; } - } - release(true /* throw */); - try { - Thread.sleep( 1 ); - slept += 1; - } catch (final InterruptedException e) { - } finally { - makeCurrent(true /* throw */); + release(true /* throw */); + try { + Thread.sleep( sleep ); + slept += sleep; + } catch (final InterruptedException e) { + } finally { + makeCurrent(true /* throw */); + } + } else { + // Empirical best behavior w/ openal-soft (sort of needs min ~21ms to complete processing a buffer even if period < 20ms?) + if( DEBUG ) { + if( onceBusyDebug ) { + System.err.println(getThreadName()+": ALAudioSink: Dequeue.wait-sleep["+wait_cycles+"].2:"+ + "releaseBuffers "+releasedBuffers+"/"+releaseBufferLimes+", sleep "+sleep+"->1/"+slept+"/"+sleepLimes+ + " ms, playImpl "+(ALConstants.AL_PLAYING == getSourceState(false))+", "+shortString()); + onceBusyDebug = false; + } + } + release(true /* throw */); + try { + Thread.sleep( 1 ); + slept += 1; + } catch (final InterruptedException e) { + } finally { + makeCurrent(true /* throw */); + } } } } - } while ( wait && releasedBuffers < releaseBufferLimes && alBufferBytesQueued > 0 ); + } while ( wait && alBufferBytesQueued > 0 && releasedBuffers < releaseBufferLimes ); releaseBufferCount = releasedBuffers; if( DEBUG ) { final long t1 = Clock.currentNanos(); - System.err.println(getThreadName()+": ALAudioSink: Dequeue.wait-done["+i+"]: "+TimeUnit.NANOSECONDS.toMillis(t1-t0)+ + System.err.println(getThreadName()+": ALAudioSink: Dequeue.wait-done["+wait_cycles+"]: "+TimeUnit.NANOSECONDS.toMillis(t1-t0)+ "ms , releaseBuffers "+releaseBufferCount+"/"+releaseBufferLimes+", slept "+slept+" ms, playImpl "+(ALConstants.AL_PLAYING == getSourceState(false))+ ", "+shortString()); } @@ -935,7 +989,7 @@ public final class ALAudioSink implements AudioSink { avgFrameDuration = chosenFormat.getBytesDuration( alBufferBytesQueued ) / alFramesPlaying.size(); // try to dequeue w/o waiting first - dequeueBuffer(false, pts, duration); + dequeueBuffer(false /* wait */, pts, duration); if( alFramesFree.isEmpty() ) { // try to grow growBuffers(byteCount); @@ -1229,5 +1283,6 @@ public final class ALAudioSink implements AudioSink { public final int getPTS() { return playingPTS; } private static final String toHexString(final int v) { return "0x"+Integer.toHexString(v); } + private static final String toHexString(final long v) { return "0x"+Long.toHexString(v); } private static final String getThreadName() { return Thread.currentThread().getName(); } } diff --git a/src/test/com/jogamp/openal/test/manual/Synth02bAL.java b/src/test/com/jogamp/openal/test/manual/Synth02bAL.java new file mode 100644 index 0000000..9c9005e --- /dev/null +++ b/src/test/com/jogamp/openal/test/manual/Synth02bAL.java @@ -0,0 +1,121 @@ +/** + * Copyright 2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.openal.test.manual; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.concurrent.TimeUnit; + +import com.jogamp.common.os.Clock; +import com.jogamp.openal.ALFactory; +import com.jogamp.openal.JoalVersion; +import com.jogamp.openal.util.SimpleSineSynth; + +/** + * Using two continuous simple off-thread mutable sine wave synthesizer. + *

+ * Implementation utilizes an off-thread worker thread, + * allowing to change frequency and amplitude without disturbance. + *

+ *

+ * Latency is hardcoded as 1 - 3 times frameDuration, having a frameDuration of 12 ms. + * Averages around 24 ms. + *

+ *

+ * Latency needs improvement to have a highly responsive life-music synthesizer. + *

+ */ +public final class Synth02bAL { + public static float atof(final String str, final float def) { + try { + return Float.parseFloat(str); + } catch (final Exception ex) { + ex.printStackTrace(); + } + return def; + } + + public static String enterValue(final String message) { + final BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); + System.err.println(message); + try { + final String s = stdin.readLine(); + System.err.println(s); + return s; + } catch (final IOException e) { e.printStackTrace(); } + return ""; + } + + public static void main(final String[] args) { + float freq = SimpleSineSynth.MIDDLE_C; + for(int i=0; i