From 6c1ff6a09a9842773f01d68e1312bce1aeb56e90 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sun, 4 May 2025 13:16:08 +0200 Subject: [PATCH 01/13] ALAudioRenderer: code improvements --- .../jme3/audio/openal/ALAudioRenderer.java | 312 +++++++++--------- 1 file changed, 160 insertions(+), 152 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index aa3c77bbf2..e46a1e0cd2 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2022 jMonkeyEngine + * Copyright (c) 2009-2025 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -99,25 +99,26 @@ private void initOpenAL() { return; } - // Find maximum # of sources supported by this implementation - ArrayList channelList = new ArrayList<>(); - for (int i = 0; i < MAX_NUM_CHANNELS; i++) { - int chan = al.alGenSources(); - if (al.alGetError() != 0) { - break; - } else { - channelList.add(chan); - } - } + enumerateAvailableChannels(); - channels = new int[channelList.size()]; - for (int i = 0; i < channels.length; i++) { - channels[i] = channelList.get(i); + printAudioRendererInfo(); + + // Pause device is a feature used specifically on Android + // where the application could be closed but still running, + // thus the audio context remains open but no audio should be playing. + supportPauseDevice = alc.alcIsExtensionPresent("ALC_SOFT_pause_device"); + if (!supportPauseDevice) { + logger.log(Level.WARNING, "Pausing audio device not supported."); } - ib = BufferUtils.createIntBuffer(channels.length); - channelSources = new AudioSource[channels.length]; + // Disconnected audio devices (such as USB sound cards, headphones...) + // never reconnect, the whole context must be re-created + supportDisconnect = alc.alcIsExtensionPresent("ALC_EXT_disconnect"); + initEfx(); + } + + private void printAudioRendererInfo() { final String deviceName = alc.alcGetString(ALC.ALC_DEVICE_SPECIFIER); logger.log(Level.INFO, "Audio Renderer Information\n" @@ -137,19 +138,30 @@ private void initOpenAL() { alc.alcGetString(ALC.ALC_EXTENSIONS), al.alGetString(AL_EXTENSIONS) }); + } - // Pause device is a feature used specifically on Android - // where the application could be closed but still running, - // thus the audio context remains open but no audio should be playing. - supportPauseDevice = alc.alcIsExtensionPresent("ALC_SOFT_pause_device"); - if (!supportPauseDevice) { - logger.log(Level.WARNING, "Pausing audio device not supported."); + private void enumerateAvailableChannels() { + // Find maximum # of sources supported by this implementation + ArrayList channelList = new ArrayList<>(); + for (int i = 0; i < MAX_NUM_CHANNELS; i++) { + int chan = al.alGenSources(); + if (al.alGetError() != 0) { + break; + } else { + channelList.add(chan); + } } - // Disconnected audio devices (such as USB sound cards, headphones...) - // never reconnect, the whole context must be re-created - supportDisconnect = alc.alcIsExtensionPresent("ALC_EXT_disconnect"); + channels = new int[channelList.size()]; + for (int i = 0; i < channels.length; i++) { + channels[i] = channelList.get(i); + } + ib = BufferUtils.createIntBuffer(channels.length); + channelSources = new AudioSource[channels.length]; + } + + private void initEfx() { supportEfx = alc.alcIsExtensionPresent("ALC_EXT_EFX"); if (supportEfx) { ib.position(0).limit(1); @@ -327,7 +339,7 @@ public float getSourcePlaybackTime(AudioSource src) { return 0; } - int id = channels[src.getChannel()]; + int sourceId = channels[src.getChannel()]; AudioData data = src.getAudioData(); int playbackOffsetBytes = 0; @@ -342,18 +354,11 @@ public float getSourcePlaybackTime(AudioSource src) { // The number of unenqueued bytes that the decoder thread // keeps track of. - int unqueuedBytes = stream.getUnqueuedBufferBytes(); - - // Additional processed buffers that the decoder thread - // did not unenqueue yet (it only updates 20 times per second). - int unqueuedBytesExtra = al.alGetSourcei(id, AL_BUFFERS_PROCESSED) * BUFFER_SIZE; - - // Total additional bytes that need to be considered. - playbackOffsetBytes = unqueuedBytes; // + unqueuedBytesExtra; + playbackOffsetBytes = stream.getUnqueuedBufferBytes(); } // Add byte offset from source (for both streams and buffers) - playbackOffsetBytes += al.alGetSourcei(id, AL_BYTE_OFFSET); + playbackOffsetBytes += al.alGetSourcei(sourceId, AL_BYTE_OFFSET); // Compute time value from bytes // E.g. for 44100 source with 2 channels and 16 bits per sample: @@ -389,75 +394,76 @@ public void updateSourceParam(AudioSource src, AudioParam param) { assert src.getChannel() >= 0; - int id = channels[src.getChannel()]; + int sourceId = channels[src.getChannel()]; + int filterId = EFX.AL_FILTER_NULL; + switch (param) { case Position: if (!src.isPositional()) { return; } - Vector3f pos = src.getPosition(); - al.alSource3f(id, AL_POSITION, pos.x, pos.y, pos.z); + al.alSource3f(sourceId, AL_POSITION, pos.x, pos.y, pos.z); break; + case Velocity: if (!src.isPositional()) { return; } - Vector3f vel = src.getVelocity(); - al.alSource3f(id, AL_VELOCITY, vel.x, vel.y, vel.z); + al.alSource3f(sourceId, AL_VELOCITY, vel.x, vel.y, vel.z); break; + case MaxDistance: if (!src.isPositional()) { return; } - - al.alSourcef(id, AL_MAX_DISTANCE, src.getMaxDistance()); + al.alSourcef(sourceId, AL_MAX_DISTANCE, src.getMaxDistance()); break; + case RefDistance: if (!src.isPositional()) { return; } - - al.alSourcef(id, AL_REFERENCE_DISTANCE, src.getRefDistance()); + al.alSourcef(sourceId, AL_REFERENCE_DISTANCE, src.getRefDistance()); break; + case ReverbFilter: if (!supportEfx || !src.isPositional() || !src.isReverbEnabled()) { return; } - - int filter = EFX.AL_FILTER_NULL; - if (src.getReverbFilter() != null) { - Filter f = src.getReverbFilter(); - if (f.isUpdateNeeded()) { - updateFilter(f); + Filter reverbFilter = src.getReverbFilter(); + if (reverbFilter != null) { + if (reverbFilter.isUpdateNeeded()) { + updateFilter(reverbFilter); } - filter = f.getId(); + filterId = reverbFilter.getId(); } - al.alSource3i(id, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filter); + al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filterId); break; + case ReverbEnabled: if (!supportEfx || !src.isPositional()) { return; } - if (src.isReverbEnabled()) { updateSourceParam(src, AudioParam.ReverbFilter); } else { - al.alSource3i(id, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); + al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); } break; + case IsPositional: if (!src.isPositional()) { // Play in headspace - al.alSourcei(id, AL_SOURCE_RELATIVE, AL_TRUE); - al.alSource3f(id, AL_POSITION, 0, 0, 0); - al.alSource3f(id, AL_VELOCITY, 0, 0, 0); + al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE); + al.alSource3f(sourceId, AL_POSITION, 0, 0, 0); + al.alSource3f(sourceId, AL_VELOCITY, 0, 0, 0); // Disable reverb - al.alSource3i(id, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); + al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); } else { - al.alSourcei(id, AL_SOURCE_RELATIVE, AL_FALSE); + al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_FALSE); updateSourceParam(src, AudioParam.Position); updateSourceParam(src, AudioParam.Velocity); updateSourceParam(src, AudioParam.MaxDistance); @@ -465,131 +471,133 @@ public void updateSourceParam(AudioSource src, AudioParam param) { updateSourceParam(src, AudioParam.ReverbEnabled); } break; + case Direction: if (!src.isDirectional()) { return; } - Vector3f dir = src.getDirection(); - al.alSource3f(id, AL_DIRECTION, dir.x, dir.y, dir.z); + al.alSource3f(sourceId, AL_DIRECTION, dir.x, dir.y, dir.z); break; + case InnerAngle: if (!src.isDirectional()) { return; } - - al.alSourcef(id, AL_CONE_INNER_ANGLE, src.getInnerAngle()); + al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, src.getInnerAngle()); break; + case OuterAngle: if (!src.isDirectional()) { return; } - - al.alSourcef(id, AL_CONE_OUTER_ANGLE, src.getOuterAngle()); + al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, src.getOuterAngle()); break; + case IsDirectional: if (src.isDirectional()) { updateSourceParam(src, AudioParam.Direction); updateSourceParam(src, AudioParam.InnerAngle); updateSourceParam(src, AudioParam.OuterAngle); - al.alSourcef(id, AL_CONE_OUTER_GAIN, 0); + al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 0); } else { - al.alSourcef(id, AL_CONE_INNER_ANGLE, 360); - al.alSourcef(id, AL_CONE_OUTER_ANGLE, 360); - al.alSourcef(id, AL_CONE_OUTER_GAIN, 1f); + al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, 360); + al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, 360); + al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 1f); } break; + case DryFilter: if (!supportEfx) { return; } Filter dryFilter = src.getDryFilter(); - int filterId; - if (dryFilter == null) { - filterId = EFX.AL_FILTER_NULL; - } else { + if (dryFilter != null) { if (dryFilter.isUpdateNeeded()) { updateFilter(dryFilter); } filterId = dryFilter.getId(); } // NOTE: must re-attach filter for changes to apply. - al.alSourcei(id, EFX.AL_DIRECT_FILTER, filterId); + al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, filterId); break; + case Looping: - if (src.isLooping() && !(src.getAudioData() instanceof AudioStream)) { - al.alSourcei(id, AL_LOOPING, AL_TRUE); + if (src.getAudioData() instanceof AudioStream) { + al.alSourcei(sourceId, AL_LOOPING, AL_FALSE); } else { - al.alSourcei(id, AL_LOOPING, AL_FALSE); + // AudioData instanceof AudioBuffer + al.alSourcei(sourceId, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE); } break; + case Volume: - al.alSourcef(id, AL_GAIN, src.getVolume()); + al.alSourcef(sourceId, AL_GAIN, src.getVolume()); break; + case Pitch: - al.alSourcef(id, AL_PITCH, src.getPitch()); + al.alSourcef(sourceId, AL_PITCH, src.getPitch()); break; } } } - private void setSourceParams(int id, AudioSource src, boolean forceNonLoop) { + private void setSourceParams(int sourceId, AudioSource src, boolean forceNonLoop) { if (src.isPositional()) { Vector3f pos = src.getPosition(); Vector3f vel = src.getVelocity(); - al.alSource3f(id, AL_POSITION, pos.x, pos.y, pos.z); - al.alSource3f(id, AL_VELOCITY, vel.x, vel.y, vel.z); - al.alSourcef(id, AL_MAX_DISTANCE, src.getMaxDistance()); - al.alSourcef(id, AL_REFERENCE_DISTANCE, src.getRefDistance()); - al.alSourcei(id, AL_SOURCE_RELATIVE, AL_FALSE); + al.alSource3f(sourceId, AL_POSITION, pos.x, pos.y, pos.z); + al.alSource3f(sourceId, AL_VELOCITY, vel.x, vel.y, vel.z); + al.alSourcef(sourceId, AL_MAX_DISTANCE, src.getMaxDistance()); + al.alSourcef(sourceId, AL_REFERENCE_DISTANCE, src.getRefDistance()); + al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_FALSE); if (src.isReverbEnabled() && supportEfx) { - int filter = EFX.AL_FILTER_NULL; + int filterId = EFX.AL_FILTER_NULL; if (src.getReverbFilter() != null) { Filter f = src.getReverbFilter(); if (f.isUpdateNeeded()) { updateFilter(f); } - filter = f.getId(); + filterId = f.getId(); } - al.alSource3i(id, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filter); + al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filterId); } } else { // play in headspace - al.alSourcei(id, AL_SOURCE_RELATIVE, AL_TRUE); - al.alSource3f(id, AL_POSITION, 0, 0, 0); - al.alSource3f(id, AL_VELOCITY, 0, 0, 0); + al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE); + al.alSource3f(sourceId, AL_POSITION, 0, 0, 0); + al.alSource3f(sourceId, AL_VELOCITY, 0, 0, 0); } if (src.getDryFilter() != null && supportEfx) { Filter f = src.getDryFilter(); if (f.isUpdateNeeded()) { updateFilter(f); - // NOTE: must re-attach filter for changes to apply. - al.alSourcei(id, EFX.AL_DIRECT_FILTER, f.getId()); + al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, f.getId()); } } if (forceNonLoop || src.getAudioData() instanceof AudioStream) { - al.alSourcei(id, AL_LOOPING, AL_FALSE); + al.alSourcei(sourceId, AL_LOOPING, AL_FALSE); } else { - al.alSourcei(id, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE); + al.alSourcei(sourceId, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE); } - al.alSourcef(id, AL_GAIN, src.getVolume()); - al.alSourcef(id, AL_PITCH, src.getPitch()); - al.alSourcef(id, AL_SEC_OFFSET, src.getTimeOffset()); + al.alSourcef(sourceId, AL_GAIN, src.getVolume()); + al.alSourcef(sourceId, AL_PITCH, src.getPitch()); + al.alSourcef(sourceId, AL_SEC_OFFSET, src.getTimeOffset()); if (src.isDirectional()) { Vector3f dir = src.getDirection(); - al.alSource3f(id, AL_DIRECTION, dir.x, dir.y, dir.z); - al.alSourcef(id, AL_CONE_INNER_ANGLE, src.getInnerAngle()); - al.alSourcef(id, AL_CONE_OUTER_ANGLE, src.getOuterAngle()); - al.alSourcef(id, AL_CONE_OUTER_GAIN, 0); + al.alSource3f(sourceId, AL_DIRECTION, dir.x, dir.y, dir.z); + al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, src.getInnerAngle()); + al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, src.getOuterAngle()); + al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 0); } else { - al.alSourcef(id, AL_CONE_INNER_ANGLE, 360); - al.alSourcef(id, AL_CONE_OUTER_ANGLE, 360); - al.alSourcef(id, AL_CONE_OUTER_GAIN, 1f); + al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, 360); + al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, 360); + al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 1f); } } @@ -606,6 +614,7 @@ public void updateListenerParam(Listener listener, ListenerParam param) { Vector3f pos = listener.getLocation(); al.alListener3f(AL_POSITION, pos.x, pos.y, pos.z); break; + case Rotation: Vector3f dir = listener.getDirection(); Vector3f up = listener.getUp(); @@ -615,10 +624,12 @@ public void updateListenerParam(Listener listener, ListenerParam param) { fb.flip(); al.alListener(AL_ORIENTATION, fb); break; + case Velocity: Vector3f vel = listener.getVelocity(); al.alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z); break; + case Volume: al.alListenerf(AL_GAIN, listener.getVolume()); break; @@ -643,7 +654,7 @@ private void setListenerParams(Listener listener) { } private int newChannel() { - if (freeChannels.size() > 0) { + if (!freeChannels.isEmpty()) { return freeChannels.remove(0); } else if (nextChan < channels.length) { return nextChan++; @@ -708,7 +719,7 @@ private boolean fillBuffer(AudioStream stream, int id) { nativeBuf.put(arrayBuf, 0, size); nativeBuf.flip(); - al.alBufferData(id, convertFormat(stream), nativeBuf, size, stream.getSampleRate()); + al.alBufferData(id, getOpenALFormat(stream), nativeBuf, size, stream.getSampleRate()); return true; } @@ -765,10 +776,8 @@ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean lo private void attachStreamToSource(int sourceId, AudioStream stream, boolean looping) { boolean success = false; - // Reset the stream. Typically happens if it finished playing on - // its own and got reclaimed. - // Note that AudioNode.stop() already resets the stream - // since it might not be at the EOF when stopped. + // Reset the stream. Typically, happens if it finished playing on its own and got reclaimed. + // Note that AudioNode.stop() already resets the stream since it might not be at the EOF when stopped. if (stream.isEOF()) { stream.setTime(0); } @@ -800,9 +809,8 @@ private void attachStreamToSource(int sourceId, AudioStream stream, boolean loop } } - private boolean attachBufferToSource(int sourceId, AudioBuffer buffer) { + private void attachBufferToSource(int sourceId, AudioBuffer buffer) { al.alSourcei(sourceId, AL_BUFFER, buffer.getId()); - return true; } private void attachAudioToSource(int sourceId, AudioData data, boolean looping) { @@ -831,8 +839,7 @@ private void clearChannel(int index) { al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, EFX.AL_FILTER_NULL); } if (src.isPositional()) { - AudioSource pas = src; - if (pas.isReverbEnabled() && supportEfx) { + if (src.isReverbEnabled() && supportEfx) { al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); } } @@ -863,7 +870,6 @@ public void update(float tpf) { } private void checkDevice() { - // If the device is disconnected, pick a new one if (isDisconnected()) { logger.log(Level.INFO, "Current audio device disconnected."); @@ -1010,8 +1016,7 @@ public void setListener(Listener listener) { } if (this.listener != null) { - // previous listener no longer associated with current - // renderer + // previous listener no longer associated with current renderer this.listener.setRenderer(null); } @@ -1047,14 +1052,14 @@ public void playSourceInstance(AudioSource src) { return; } - if (src.getAudioData() instanceof AudioStream) { + AudioData audioData = src.getAudioData(); + if (audioData instanceof AudioStream) { throw new UnsupportedOperationException( - "Cannot play instances " - + "of audio streams. Use play() instead."); + "Cannot play instances of audio streams. Use play() instead."); } - if (src.getAudioData().isUpdateNeeded()) { - updateAudioData(src.getAudioData()); + if (audioData.isUpdateNeeded()) { + updateAudioData(audioData); } // create a new index for an audio-channel @@ -1064,12 +1069,11 @@ public void playSourceInstance(AudioSource src) { } int sourceId = channels[index]; - clearChannel(index); // set parameters, like position and max distance - setSourceParams(sourceId, src, true); - attachAudioToSource(sourceId, src.getAudioData(), false); + setSourceParams(sourceId, src, true); // forceNonLoop + attachAudioToSource(sourceId, audioData, false); // no looping channelSources[index] = src; // play the channel @@ -1101,14 +1105,14 @@ public void playSource(AudioSource src) { clearChannel(index); src.setChannel(index); - AudioData data = src.getAudioData(); - if (data.isUpdateNeeded()) { - updateAudioData(data); + AudioData audioData = src.getAudioData(); + if (audioData.isUpdateNeeded()) { + updateAudioData(audioData); } channelSources[index] = src; setSourceParams(channels[index], src, false); - attachAudioToSource(channels[index], data, src.isLooping()); + attachAudioToSource(channels[index], audioData, src.isLooping()); } al.alSourcePlay(channels[src.getChannel()]); @@ -1141,13 +1145,13 @@ public void stopSource(AudioSource src) { } if (src.getStatus() != Status.Stopped) { - int chan = src.getChannel(); - assert chan != -1; // if it's not stopped, must have id + int channel = src.getChannel(); + assert channel != -1; // if it's not stopped, must have id src.setStatus(Status.Stopped); src.setChannel(-1); - clearChannel(chan); - freeChannel(chan); + clearChannel(channel); + freeChannel(channel); if (src.getAudioData() instanceof AudioStream) { // If the stream is seekable, then rewind it. @@ -1163,25 +1167,29 @@ public void stopSource(AudioSource src) { } } - private int convertFormat(AudioData ad) { - switch (ad.getBitsPerSample()) { - case 8: - if (ad.getChannels() == 1) { - return AL_FORMAT_MONO8; - } else if (ad.getChannels() == 2) { - return AL_FORMAT_STEREO8; - } + private int getOpenALFormat(AudioData audioData) { + int channels = audioData.getChannels(); + int bitsPerSample = audioData.getBitsPerSample(); - break; - case 16: - if (ad.getChannels() == 1) { - return AL_FORMAT_MONO16; - } else { - return AL_FORMAT_STEREO16; - } + if (channels == 1) { + if (bitsPerSample == 8) { + return AL_FORMAT_MONO8; + } else if (bitsPerSample == 16) { + return AL_FORMAT_MONO16; + } else { + throw new UnsupportedOperationException("Illegal sample size: " + bitsPerSample); + } + } else if (channels == 2) { + if (bitsPerSample == 8) { + return AL_FORMAT_STEREO8; + } else if (bitsPerSample == 16) { + return AL_FORMAT_STEREO16; + } else { + throw new UnsupportedOperationException("Illegal sample size: " + bitsPerSample); + } + } else { + throw new UnsupportedOperationException("Only mono or stereo is supported. Channels: " + channels); } - throw new UnsupportedOperationException("Unsupported channels/bits combination: " - + "bits=" + ad.getBitsPerSample() + ", channels=" + ad.getChannels()); } private void updateAudioBuffer(AudioBuffer ab) { @@ -1196,7 +1204,7 @@ private void updateAudioBuffer(AudioBuffer ab) { } ab.getData().clear(); - al.alBufferData(id, convertFormat(ab), ab.getData(), ab.getData().capacity(), ab.getSampleRate()); + al.alBufferData(id, getOpenALFormat(ab), ab.getData(), ab.getData().capacity(), ab.getSampleRate()); ab.clearUpdateNeeded(); } From eb40ab697c6f6eff231e62b91178a76f1f5cd2bf Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sun, 4 May 2025 13:19:25 +0200 Subject: [PATCH 02/13] Update ALAudioRenderer.java --- .../jme3/audio/openal/ALAudioRenderer.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index e46a1e0cd2..7501c8a5ae 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -122,13 +122,13 @@ private void printAudioRendererInfo() { final String deviceName = alc.alcGetString(ALC.ALC_DEVICE_SPECIFIER); logger.log(Level.INFO, "Audio Renderer Information\n" - + " * Device: {0}\n" - + " * Vendor: {1}\n" - + " * Renderer: {2}\n" - + " * Version: {3}\n" - + " * Supported channels: {4}\n" - + " * ALC extensions: {5}\n" - + " * AL extensions: {6}", + + " * Device: {0}\n" + + " * Vendor: {1}\n" + + " * Renderer: {2}\n" + + " * Version: {3}\n" + + " * Supported channels: {4}\n" + + " * ALC extensions: {5}\n" + + " * AL extensions: {6}", new Object[] { deviceName, al.alGetString(AL_VENDOR), @@ -255,7 +255,7 @@ private void checkDead() { @Override public void run() { - long updateRateNanos = (long) (UPDATE_RATE * 1000000000); + long updateRateNanos = (long) (UPDATE_RATE * 1_000_000_000); mainloop: while (true) { long startTime = System.nanoTime(); @@ -962,8 +962,7 @@ public void updateInRenderThread(float tpf) { // jME3 state does not match OAL state. // This is only relevant for bound sources. throw new AssertionError("Unexpected sound status. " - + "OAL: " + oalStatus - + ", JME: " + jmeStatus); + + "OAL: " + oalStatus + ", JME: " + jmeStatus); } } else { // Stopped channel was not cleared correctly. @@ -995,7 +994,9 @@ public void updateInDecoderThread(float tpf) { // Keep filling data (even if we are stopped / paused) boolean buffersWereFilled = fillStreamingSource(sourceId, stream, src.isLooping()); - if (buffersWereFilled && oalStatus == Status.Stopped && jmeStatus == Status.Playing) { + if (buffersWereFilled + && oalStatus == Status.Stopped + && jmeStatus == Status.Playing) { // The source got stopped due to buffer starvation. // Start it again. logger.log(Level.WARNING, "Buffer starvation occurred while playing stream"); From 2ed2008d6b2748ff9e6b83e1de1b08b66430b28f Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sun, 4 May 2025 16:44:24 +0200 Subject: [PATCH 03/13] ALAudioRenderer: add checkAlError() method --- .../jme3/audio/openal/ALAudioRenderer.java | 114 ++++++++++++++---- 1 file changed, 92 insertions(+), 22 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index 7501c8a5ae..4f841644bb 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -56,32 +56,48 @@ public class ALAudioRenderer implements AudioRenderer, Runnable { private static final int BUFFER_SIZE = 35280; private static final int STREAMING_BUFFER_COUNT = 5; private static final int MAX_NUM_CHANNELS = 64; - private IntBuffer ib = BufferUtils.createIntBuffer(1); - private final FloatBuffer fb = BufferUtils.createVector3Buffer(2); - private final ByteBuffer nativeBuf = BufferUtils.createByteBuffer(BUFFER_SIZE); - private final byte[] arrayBuf = new byte[BUFFER_SIZE]; - private int[] channels; - private AudioSource[] channelSources; - private int nextChan = 0; - private final ArrayList freeChannels = new ArrayList<>(); + + // Buffers for OpenAL calls + private IntBuffer ib = BufferUtils.createIntBuffer(1); // Reused for single int operations + private final FloatBuffer fb = BufferUtils.createVector3Buffer(2); // For listener orientation + private final ByteBuffer nativeBuf = BufferUtils.createByteBuffer(BUFFER_SIZE); // For streaming data + private final byte[] arrayBuf = new byte[BUFFER_SIZE]; // Intermediate array buffer for streaming + + // Channel management + private int[] channels; // OpenAL source IDs + private AudioSource[] channelSources; // jME source associated with each channel + private int nextChan = 0; // Next available channel index + private final ArrayList freeChannels = new ArrayList<>(); // Pool of freed channels + + // Listener and environment private Listener listener; + private int reverbFx = -1; // EFX reverb effect ID + private int reverbFxSlot = -1; // EFX effect slot ID + + // State and capabilities private boolean audioDisabled = false; private boolean supportEfx = false; private boolean supportPauseDevice = false; private boolean supportDisconnect = false; private int auxSends = 0; - private int reverbFx = -1; - private int reverbFxSlot = -1; - // Fill streaming sources every 50 ms - private static final float UPDATE_RATE = 0.05f; + // Update thread + private static final float UPDATE_RATE = 0.05f; // Update streaming sources every 50ms private final Thread decoderThread = new Thread(this, THREAD_NAME); - private final Object threadLock = new Object(); + private final Object threadLock = new Object(); // Lock for thread safety + // OpenAL API interfaces private final AL al; private final ALC alc; private final EFX efx; + /** + * Creates a new ALAudioRenderer instance. + * + * @param al The OpenAL interface. + * @param alc The OpenAL Context interface. + * @param efx The OpenAL Effects Extension interface. + */ public ALAudioRenderer(AL al, ALC alc, EFX efx) { this.al = al; this.alc = alc; @@ -261,6 +277,7 @@ public void run() { long startTime = System.nanoTime(); if (Thread.interrupted()) { + logger.info("Audio decoder thread interrupted, exiting."); break; } @@ -272,17 +289,20 @@ public void run() { long endTime = System.nanoTime(); long diffTime = endTime - startTime; + // Sleep to maintain the desired update rate if (diffTime < updateRateNanos) { long desiredEndTime = startTime + updateRateNanos; while (System.nanoTime() < desiredEndTime) { try { Thread.sleep(1); } catch (InterruptedException ex) { + logger.info("Audio decoder thread interrupted during sleep, exiting."); break mainloop; } } } } + logger.info("Audio decoder thread finished."); } @Override @@ -671,6 +691,10 @@ private void freeChannel(int index) { } } + /** + * Configures the global reverb effect based on the Environment settings. + * @param env The Environment object. + */ @Override public void setEnvironment(Environment env) { checkDead(); @@ -692,8 +716,13 @@ public void setEnvironment(Environment env) { efx.alEffectf(reverbFx, EFX.AL_REVERB_AIR_ABSORPTION_GAINHF, env.getAirAbsorbGainHf()); efx.alEffectf(reverbFx, EFX.AL_REVERB_ROOM_ROLLOFF_FACTOR, env.getRoomRolloffFactor()); - // attach effect to slot + if (checkAlError("setting reverb effect parameters")) { + return; + } + + // (Re)attach the configured reverb effect to the slot efx.alAuxiliaryEffectSloti(reverbFxSlot, EFX.AL_EFFECTSLOT_EFFECT, reverbFx); + checkAlError("attaching reverb effect to slot"); } } @@ -994,8 +1023,8 @@ public void updateInDecoderThread(float tpf) { // Keep filling data (even if we are stopped / paused) boolean buffersWereFilled = fillStreamingSource(sourceId, stream, src.isLooping()); - if (buffersWereFilled - && oalStatus == Status.Stopped + if (buffersWereFilled + && oalStatus == Status.Stopped && jmeStatus == Status.Playing) { // The source got stopped due to buffer starvation. // Start it again. @@ -1168,7 +1197,14 @@ public void stopSource(AudioSource src) { } } + /** + * Gets the corresponding OpenAL format enum for the audio data properties. + * @param audioData The AudioData. + * @return The OpenAL format enum (e.g., AL_FORMAT_STEREO16). + * @throws UnsupportedOperationException if the format is not supported. + */ private int getOpenALFormat(AudioData audioData) { + int channels = audioData.getChannels(); int bitsPerSample = audioData.getBitsPerSample(); @@ -1177,20 +1213,19 @@ private int getOpenALFormat(AudioData audioData) { return AL_FORMAT_MONO8; } else if (bitsPerSample == 16) { return AL_FORMAT_MONO16; - } else { - throw new UnsupportedOperationException("Illegal sample size: " + bitsPerSample); } } else if (channels == 2) { if (bitsPerSample == 8) { return AL_FORMAT_STEREO8; } else if (bitsPerSample == 16) { return AL_FORMAT_STEREO16; - } else { - throw new UnsupportedOperationException("Illegal sample size: " + bitsPerSample); } - } else { - throw new UnsupportedOperationException("Only mono or stereo is supported. Channels: " + channels); } + // Add support for AL_EXT_MCFORMATS if needed later + + // Format not supported + throw new UnsupportedOperationException("Unsupported audio format: " + + channels + " channels, " + bitsPerSample + " bits per sample."); } private void updateAudioBuffer(AudioBuffer ab) { @@ -1275,4 +1310,39 @@ public void deleteAudioData(AudioData ad) { } } } + + /** + * Checks for OpenAL errors and logs a warning if an error occurred. + * @param location A string describing where the check is occurring (for logging). + * @return True if an error occurred, false otherwise. + */ + private boolean checkAlError(String location) { + int error = al.alGetError(); + if (error != AL_NO_ERROR) { + String errorString; + switch (error) { + case AL_INVALID_NAME: + errorString = "AL_INVALID_NAME"; + break; + case AL_INVALID_ENUM: + errorString = "AL_INVALID_ENUM"; + break; + case AL_INVALID_VALUE: + errorString = "AL_INVALID_VALUE"; + break; + case AL_INVALID_OPERATION: + errorString = "AL_INVALID_OPERATION"; + break; + case AL_OUT_OF_MEMORY: + errorString = "AL_OUT_OF_MEMORY"; + break; + default: + errorString = "Unknown AL error code: " + error; + break; + } + logger.log(Level.WARNING, "OpenAL Error ({0}) at {1}", new Object[]{errorString, location}); + return true; + } + return false; + } } From cf3de683e52db50071172adc9e8f3fa4175bb323 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sun, 4 May 2025 18:55:57 +0200 Subject: [PATCH 04/13] add Environment field --- .../jme3/audio/openal/ALAudioRenderer.java | 284 +++++++++++++----- 1 file changed, 209 insertions(+), 75 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index 4f841644bb..503f02f890 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -44,6 +44,9 @@ import java.util.logging.Level; import java.util.logging.Logger; +/** + * ALAudioRenderer is the backend implementation for OpenAL audio rendering. + */ public class ALAudioRenderer implements AudioRenderer, Runnable { private static final Logger logger = Logger.getLogger(ALAudioRenderer.class.getName()); @@ -66,11 +69,12 @@ public class ALAudioRenderer implements AudioRenderer, Runnable { // Channel management private int[] channels; // OpenAL source IDs private AudioSource[] channelSources; // jME source associated with each channel - private int nextChan = 0; // Next available channel index + private int nextChannelIndex = 0; // Next available channel index private final ArrayList freeChannels = new ArrayList<>(); // Pool of freed channels // Listener and environment private Listener listener; + private Environment environment; private int reverbFx = -1; // EFX reverb effect ID private int reverbFxSlot = -1; // EFX effect slot ID @@ -110,7 +114,7 @@ private void initOpenAL() { alc.createALC(); } } catch (UnsatisfiedLinkError ex) { - logger.log(Level.SEVERE, "Failed to load audio library", ex); + logger.log(Level.SEVERE, "Failed to load audio library (OpenAL). Audio will be disabled.", ex); audioDisabled = true; return; } @@ -156,6 +160,9 @@ private void printAudioRendererInfo() { }); } + /** + * Generates OpenAL sources to determine the maximum number supported. + */ private void enumerateAvailableChannels() { // Find maximum # of sources supported by this implementation ArrayList channelList = new ArrayList<>(); @@ -177,39 +184,52 @@ private void enumerateAvailableChannels() { channelSources = new AudioSource[channels.length]; } + /** + * Initializes the EFX extension if supported. + */ private void initEfx() { supportEfx = alc.alcIsExtensionPresent("ALC_EXT_EFX"); if (supportEfx) { - ib.position(0).limit(1); + ib.clear().limit(1); alc.alcGetInteger(EFX.ALC_EFX_MAJOR_VERSION, ib, 1); int major = ib.get(0); - ib.position(0).limit(1); + + ib.clear().limit(1); alc.alcGetInteger(EFX.ALC_EFX_MINOR_VERSION, ib, 1); int minor = ib.get(0); logger.log(Level.INFO, "Audio effect extension version: {0}.{1}", new Object[]{major, minor}); + ib.clear().limit(1); alc.alcGetInteger(EFX.ALC_MAX_AUXILIARY_SENDS, ib, 1); auxSends = ib.get(0); logger.log(Level.INFO, "Audio max auxiliary sends: {0}", auxSends); - // create slot - ib.position(0).limit(1); + // Create reverb effect slot + ib.clear().limit(1); efx.alGenAuxiliaryEffectSlots(1, ib); reverbFxSlot = ib.get(0); - // create effect - ib.position(0).limit(1); + // Create reverb effect + ib.clear().limit(1); efx.alGenEffects(1, ib); reverbFx = ib.get(0); + + // Configure effect type efx.alEffecti(reverbFx, EFX.AL_EFFECT_TYPE, EFX.AL_EFFECT_REVERB); + checkAlError("setting reverb effect type"); // attach reverb effect to effect slot efx.alAuxiliaryEffectSloti(reverbFxSlot, EFX.AL_EFFECTSLOT_EFFECT, reverbFx); + checkAlError("attaching reverb effect to slot"); + } else { logger.log(Level.WARNING, "OpenAL EFX not available! Audio effects won't work."); } } + /** + * Destroys the OpenAL context, deleting sources, buffers, filters, and effects. + */ private void destroyOpenAL() { if (audioDisabled) { alc.destroyALC(); @@ -228,20 +248,28 @@ private void destroyOpenAL() { ib.put(channels); ib.flip(); al.alDeleteSources(channels.length, ib); + checkAlError("deleting sources"); - // delete audio buffers and filters + // Delete audio buffers and filters managed by NativeObjectManager objManager.deleteAllObjects(this); + // Delete EFX objects if they were created if (supportEfx) { - ib.position(0).limit(1); - ib.put(0, reverbFx); - efx.alDeleteEffects(1, ib); + if (reverbFx != -1) { + ib.position(0).limit(1); + ib.put(0, reverbFx); + efx.alDeleteEffects(1, ib); + checkAlError("deleting reverbFx effect " + reverbFx); + reverbFx = -1; + } - // If this is not allocated, why is it deleted? - // Commented out to fix native crash in OpenAL. - ib.position(0).limit(1); - ib.put(0, reverbFxSlot); - efx.alDeleteAuxiliaryEffectSlots(1, ib); + if (reverbFxSlot != -1) { + ib.position(0).limit(1); + ib.put(0, reverbFxSlot); + efx.alDeleteAuxiliaryEffectSlots(1, ib); + checkAlError("deleting effect reverbFxSlot " + reverbFxSlot); + reverbFxSlot = -1; + } } alc.destroyALC(); @@ -263,12 +291,19 @@ public void initialize() { decoderThread.start(); } + /** + * Checks if the audio thread has terminated unexpectedly. + * @throws IllegalStateException if the decoding thread is terminated. + */ private void checkDead() { if (decoderThread.getState() == Thread.State.TERMINATED) { throw new IllegalStateException("Decoding thread is terminated"); } } + /** + * Main loop for the audio decoder thread. Updates streaming sources. + */ @Override public void run() { long updateRateNanos = (long) (UPDATE_RATE * 1_000_000_000); @@ -305,27 +340,38 @@ public void run() { logger.info("Audio decoder thread finished."); } + /** + * Shuts down the audio decoder thread and destroys the OpenAL context. + */ @Override public void cleanup() { // kill audio thread - if (!decoderThread.isAlive()) { - return; + if (decoderThread.isAlive()) { + decoderThread.interrupt(); + try { + decoderThread.join(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); // Re-interrupt thread + logger.log(Level.WARNING, "Interrupted while waiting for audio thread to finish.", ex); + } } - decoderThread.interrupt(); - try { - decoderThread.join(); - } catch (InterruptedException ex) { + // Destroy OpenAL context (only if initialized and not disabled) + if (!audioDisabled && alc.isCreated()) { + destroyOpenAL(); } - - // destroy OpenAL context - destroyOpenAL(); } + /** + * Updates an OpenAL filter object based on the jME Filter properties. + * Generates the AL filter ID if necessary. + * @param f The Filter object. + */ private void updateFilter(Filter f) { int id = f.getId(); if (id == -1) { - ib.position(0).limit(1); + // Generate OpenAL filter ID + ib.clear().limit(1); efx.alGenFilters(1, ib); id = ib.get(0); f.setId(id); @@ -334,16 +380,22 @@ private void updateFilter(Filter f) { } if (f instanceof LowPassFilter) { - LowPassFilter lpf = (LowPassFilter) f; + LowPassFilter lowPass = (LowPassFilter) f; efx.alFilteri(id, EFX.AL_FILTER_TYPE, EFX.AL_FILTER_LOWPASS); - efx.alFilterf(id, EFX.AL_LOWPASS_GAIN, lpf.getVolume()); - efx.alFilterf(id, EFX.AL_LOWPASS_GAINHF, lpf.getHighFreqVolume()); - } else { - throw new UnsupportedOperationException("Filter type unsupported: " - + f.getClass().getName()); + efx.alFilterf(id, EFX.AL_LOWPASS_GAIN, lowPass.getVolume()); + efx.alFilterf(id, EFX.AL_LOWPASS_GAINHF, lowPass.getHighFreqVolume()); + } + // ** Add other filter types (HighPass, BandPass) here if implemented ** + else { + logger.log(Level.WARNING, "Unsupported filter type: {0}", f.getClass().getName()); } - f.clearUpdateNeeded(); + if (checkAlError("updating filter " + id)) { + deleteFilter(f); // Try to clean up + f.resetObject(); + } else { + f.clearUpdateNeeded(); // Mark as updated in AL + } } @Override @@ -621,9 +673,21 @@ private void setSourceParams(int sourceId, AudioSource src, boolean forceNonLoop } } + /** + * Updates a specific parameter for the listener. + * + * @param listener The listener object. + * @param param The parameter to update. + */ @Override public void updateListenerParam(Listener listener, ListenerParam param) { checkDead(); + // Check if this listener is the active one + if (this.listener != listener) { + logger.warning("updateListenerParam called on inactive listener."); + return; + } + synchronized (threadLock) { if (audioDisabled) { return; @@ -631,61 +695,79 @@ public void updateListenerParam(Listener listener, ListenerParam param) { switch (param) { case Position: - Vector3f pos = listener.getLocation(); - al.alListener3f(AL_POSITION, pos.x, pos.y, pos.z); + applyListenerPosition(listener); break; - case Rotation: - Vector3f dir = listener.getDirection(); - Vector3f up = listener.getUp(); - fb.rewind(); - fb.put(dir.x).put(dir.y).put(dir.z); - fb.put(up.x).put(up.y).put(up.z); - fb.flip(); - al.alListener(AL_ORIENTATION, fb); + applyListenerRotation(listener); break; - case Velocity: - Vector3f vel = listener.getVelocity(); - al.alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z); + applyListenerVelocity(listener); break; - case Volume: - al.alListenerf(AL_GAIN, listener.getVolume()); + applyListenerVolume(listener); + break; + default: + logger.log(Level.WARNING, "Unhandled listener parameter: {0}", param); break; } } } + /** + * Applies all parameters from the listener object to OpenAL. + * @param listener The listener object. + */ private void setListenerParams(Listener listener) { + applyListenerPosition(listener); + applyListenerRotation(listener); + applyListenerVelocity(listener); + applyListenerVolume(listener); + } + + // --- Listener Parameter Helper Methods --- + + private void applyListenerPosition(Listener listener) { Vector3f pos = listener.getLocation(); - Vector3f vel = listener.getVelocity(); + al.alListener3f(AL_POSITION, pos.x, pos.y, pos.z); + checkAlError("setting listener position"); + } + + private void applyListenerRotation(Listener listener) { Vector3f dir = listener.getDirection(); Vector3f up = listener.getUp(); - - al.alListener3f(AL_POSITION, pos.x, pos.y, pos.z); - al.alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z); + // Use the shared FloatBuffer fb fb.rewind(); fb.put(dir.x).put(dir.y).put(dir.z); fb.put(up.x).put(up.y).put(up.z); fb.flip(); al.alListener(AL_ORIENTATION, fb); + checkAlError("setting listener orientation"); + } + + private void applyListenerVelocity(Listener listener) { + Vector3f vel = listener.getVelocity(); + al.alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z); + checkAlError("setting listener velocity"); + } + + private void applyListenerVolume(Listener listener) { al.alListenerf(AL_GAIN, listener.getVolume()); + checkAlError("setting listener volume"); } private int newChannel() { if (!freeChannels.isEmpty()) { return freeChannels.remove(0); - } else if (nextChan < channels.length) { - return nextChan++; + } else if (nextChannelIndex < channels.length) { + return nextChannelIndex++; } else { return -1; } } private void freeChannel(int index) { - if (index == nextChan - 1) { - nextChan--; + if (index == nextChannelIndex - 1) { + nextChannelIndex--; } else { freeChannels.add(index); } @@ -703,6 +785,7 @@ public void setEnvironment(Environment env) { return; } + // Apply reverb properties from the Environment object efx.alEffectf(reverbFx, EFX.AL_REVERB_DENSITY, env.getDensity()); efx.alEffectf(reverbFx, EFX.AL_REVERB_DIFFUSION, env.getDiffusion()); efx.alEffectf(reverbFx, EFX.AL_REVERB_GAIN, env.getGain()); @@ -723,6 +806,8 @@ public void setEnvironment(Environment env) { // (Re)attach the configured reverb effect to the slot efx.alAuxiliaryEffectSloti(reverbFxSlot, EFX.AL_EFFECTSLOT_EFFECT, reverbFx); checkAlError("attaching reverb effect to slot"); + + this.environment = env; } } @@ -859,17 +944,23 @@ private void clearChannel(int index) { int sourceId = channels[index]; al.alSourceStop(sourceId); + checkAlError("stopping source " + sourceId + " on clearChannel"); // For streaming sources, this will clear all queued buffers. al.alSourcei(sourceId, AL_BUFFER, 0); + checkAlError("detaching buffer from source " + sourceId); - if (src.getDryFilter() != null && supportEfx) { - // detach filter - al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, EFX.AL_FILTER_NULL); - } - if (src.isPositional()) { - if (src.isReverbEnabled() && supportEfx) { + if (supportEfx) { + if (src.getDryFilter() != null) { + // detach direct filter + al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, EFX.AL_FILTER_NULL); + checkAlError("detaching direct filter from source " + sourceId); + } + + if (src.isPositional() && src.isReverbEnabled()) { + // Detach auxiliary send filter (reverb) al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); + checkAlError("detaching aux filter from source " + sourceId); } } @@ -916,8 +1007,35 @@ private boolean isDisconnected() { } private void restartAudioRenderer() { + // Preserve internal state variables + Listener currentListener = this.listener; + Environment currentEnvironment = this.environment; + + // Destroy existing OpenAL resources destroyOpenAL(); + + // Re-initialize OpenAL + // Creates new context, enumerates channels, checks caps, inits EFX initOpenAL(); + + // Restore Listener and Environment (if possible and successful init) + if (!audioDisabled) { + if (currentListener != null) { + setListener(currentListener); // Re-apply listener params + } + if (currentEnvironment != null) { + setEnvironment(currentEnvironment); // Re-apply environment + } + // TODO: What about existing AudioSource objects? + // Their state (Playing/Paused/Stopped) is lost. + // Their AudioData (buffers/streams) needs re-uploading/re-preparing. + // This requires iterating through all known AudioNodes, which the renderer doesn't track. + // The application layer would need to handle re-playing sounds after a device reset. + logger.warning("Audio renderer restarted. Application may need to re-play active sounds."); + + } else { + logger.severe("Audio remained disabled after attempting restart."); + } } public void updateInRenderThread(float tpf) { @@ -1051,8 +1169,13 @@ public void setListener(Listener listener) { } this.listener = listener; - this.listener.setRenderer(this); - setListenerParams(listener); + + if (this.listener != null) { + this.listener.setRenderer(this); + setListenerParams(listener); + } else { + logger.info("Listener set to null."); + } } } @@ -1263,14 +1386,18 @@ private void updateAudioStream(AudioStream as) { as.clearUpdateNeeded(); } - private void updateAudioData(AudioData ad) { - if (ad instanceof AudioBuffer) { - updateAudioBuffer((AudioBuffer) ad); - } else if (ad instanceof AudioStream) { - updateAudioStream((AudioStream) ad); + private void updateAudioData(AudioData audioData) { + if (audioData instanceof AudioBuffer) { + updateAudioBuffer((AudioBuffer) audioData); + } else if (audioData instanceof AudioStream) { + updateAudioStream((AudioStream) audioData); } } + /** + * Deletes the OpenAL filter object associated with the Filter. + * @param filter The Filter object. + */ @Override public void deleteFilter(Filter filter) { int id = filter.getId(); @@ -1278,33 +1405,40 @@ public void deleteFilter(Filter filter) { ib.position(0).limit(1); ib.put(id).flip(); efx.alDeleteFilters(1, ib); + checkAlError("deleting filter " + id); filter.resetObject(); } } + /** + * Deletes the OpenAL objects associated with the AudioData. + * @param audioData The AudioData to delete. + */ @Override - public void deleteAudioData(AudioData ad) { + public void deleteAudioData(AudioData audioData) { synchronized (threadLock) { if (audioDisabled) { return; } - if (ad instanceof AudioBuffer) { - AudioBuffer ab = (AudioBuffer) ad; + if (audioData instanceof AudioBuffer) { + AudioBuffer ab = (AudioBuffer) audioData; int id = ab.getId(); if (id != -1) { ib.put(0, id); ib.position(0).limit(1); al.alDeleteBuffers(1, ib); + checkAlError("deleting buffer " + id); ab.resetObject(); } - } else if (ad instanceof AudioStream) { - AudioStream as = (AudioStream) ad; + } else if (audioData instanceof AudioStream) { + AudioStream as = (AudioStream) audioData; int[] ids = as.getIds(); if (ids != null) { ib.clear(); ib.put(ids).flip(); al.alDeleteBuffers(ids.length, ib); + checkAlError("deleting " + ids.length + " buffers"); as.resetObject(); } } From 72fc3e9d305273ae5597ee4dfc992788a0a25c68 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sun, 4 May 2025 19:42:20 +0200 Subject: [PATCH 05/13] ALAudioRenderer: comments and logging --- .../jme3/audio/openal/ALAudioRenderer.java | 110 ++++++++++++------ 1 file changed, 76 insertions(+), 34 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index 503f02f890..d6671c04f7 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -192,33 +192,33 @@ private void initEfx() { if (supportEfx) { ib.clear().limit(1); alc.alcGetInteger(EFX.ALC_EFX_MAJOR_VERSION, ib, 1); - int major = ib.get(0); + int majorVersion = ib.get(0); ib.clear().limit(1); alc.alcGetInteger(EFX.ALC_EFX_MINOR_VERSION, ib, 1); - int minor = ib.get(0); - logger.log(Level.INFO, "Audio effect extension version: {0}.{1}", new Object[]{major, minor}); + int minorVersion = ib.get(0); + logger.log(Level.INFO, "Audio effect extension version: {0}.{1}", new Object[]{majorVersion, minorVersion}); ib.clear().limit(1); alc.alcGetInteger(EFX.ALC_MAX_AUXILIARY_SENDS, ib, 1); auxSends = ib.get(0); logger.log(Level.INFO, "Audio max auxiliary sends: {0}", auxSends); - // Create reverb effect slot + // 1. Create reverb effect slot ib.clear().limit(1); efx.alGenAuxiliaryEffectSlots(1, ib); reverbFxSlot = ib.get(0); - // Create reverb effect + // 2. Create reverb effect ib.clear().limit(1); efx.alGenEffects(1, ib); reverbFx = ib.get(0); - // Configure effect type + // 3. Configure effect type efx.alEffecti(reverbFx, EFX.AL_EFFECT_TYPE, EFX.AL_EFFECT_REVERB); checkAlError("setting reverb effect type"); - // attach reverb effect to effect slot + // 4. attach reverb effect to effect slot efx.alAuxiliaryEffectSloti(reverbFxSlot, EFX.AL_EFFECTSLOT_EFFECT, reverbFx); checkAlError("attaching reverb effect to slot"); @@ -937,6 +937,12 @@ private void attachAudioToSource(int sourceId, AudioData data, boolean looping) } } + /** + * Stops the AL source on the channel, detaches buffers and filters, + * and clears the jME source association. Does NOT free the channel index itself. + * + * @param index The channel index to clear. + */ private void clearChannel(int index) { // make room at this channel if (channelSources[index] != null) { @@ -978,7 +984,7 @@ private AudioSource.Status convertStatus(int oalStatus) { case AL_PLAYING: return Status.Playing; default: - throw new UnsupportedOperationException("Unrecognized OAL state: " + oalStatus); + throw new UnsupportedOperationException("Unrecognized OpenAL state: " + oalStatus); } } @@ -1038,6 +1044,12 @@ private void restartAudioRenderer() { } } + /** + * Internal update logic called from the render thread within the lock. + * Checks source statuses and reclaims finished channels. + * + * @param tpf Time per frame (currently unused). + */ public void updateInRenderThread(float tpf) { if (audioDisabled) { return; @@ -1047,69 +1059,88 @@ public void updateInRenderThread(float tpf) { AudioSource src = channelSources[i]; if (src == null) { - continue; + continue; // No source on this channel } int sourceId = channels[i]; boolean boundSource = i == src.getChannel(); boolean reclaimChannel = false; - Status oalStatus = convertStatus(al.alGetSourcei(sourceId, AL_SOURCE_STATE)); + // Get OpenAL status for the source + int oalState = al.alGetSourcei(sourceId, AL_SOURCE_STATE); + Status oalStatus = convertStatus(oalState); + // --- Handle Instanced Playback (Not bound to a specific channel) --- if (!boundSource) { - // Rules for instanced playback vary significantly. - // Handle it here. if (oalStatus == Status.Stopped) { - // Instanced audio stopped playing. Reclaim channel. - clearChannel(i); - freeChannel(i); + // Instanced audio (non-looping buffer) finished playing. Reclaim channel. + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Reclaiming channel {0} from finished instance.", i); + } + clearChannel(i); // Stop source, detach buffer/filter + freeChannel(i); // Add channel back to the free pool } else if (oalStatus == Status.Paused) { - throw new AssertionError("Instanced audio cannot be paused"); + throw new AssertionError("Instanced audio source on channel " + i + " cannot be paused."); } + // If Playing, do nothing, let it finish. continue; } + // --- Handle Bound Playback (Normal play/pause/stop) --- Status jmeStatus = src.getStatus(); // Check if we need to sync JME status with OAL status. if (oalStatus != jmeStatus) { if (oalStatus == Status.Stopped && jmeStatus == Status.Playing) { - // Maybe we need to reclaim the channel. + + // Source stopped playing unexpectedly (finished or starved) if (src.getAudioData() instanceof AudioStream) { AudioStream stream = (AudioStream) src.getAudioData(); if (stream.isEOF() && !src.isLooping()) { - // Stream finished playing + // Stream reached EOF and is not looping. + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Stream source on channel {0} finished.", i); + } reclaimChannel = true; } else { - // Stream still has data. - // Buffer starvation occurred. - // Audio decoder thread will fill the data - // and start the channel again. + // Stream still has data or is looping, but stopped. + // This indicates buffer starvation. The decoder thread will handle restarting it. + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Stream source on channel {0} likely starved.", i); + } + // Don't reclaim channel here, let decoder thread refill and restart. } } else { // Buffer finished playing. if (src.isLooping()) { - // When a device is disconnected, all sources - // will enter the "stopped" state. - logger.warning("A looping sound has stopped playing"); + // This is unexpected for looping buffers unless the device was disconnected/reset. + logger.log(Level.WARNING, "Looping buffer source on channel {0} stopped unexpectedly.", i); + } else { + // Non-looping buffer finished normally. + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Buffer source on channel {0} finished.", i); + } } reclaimChannel = true; } if (reclaimChannel) { + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Reclaiming channel {0} from finished source.", i); + } src.setStatus(Status.Stopped); src.setChannel(-1); - clearChannel(i); - freeChannel(i); + clearChannel(i); // Stop AL source, detach buffers/filters + freeChannel(i); // Add channel back to the free pool } } else { // jME3 state does not match OAL state. // This is only relevant for bound sources. throw new AssertionError("Unexpected sound status. " - + "OAL: " + oalStatus + ", JME: " + jmeStatus); + + "OpenAL: " + oalStatus + ", JME: " + jmeStatus); } } else { // Stopped channel was not cleared correctly. @@ -1120,6 +1151,12 @@ public void updateInRenderThread(float tpf) { } } + /** + * Internal update logic called from the decoder thread within the lock. + * Fills streaming buffers and restarts starved sources. Deletes unused objects. + * + * @param tpf Time per frame (currently unused). + */ public void updateInDecoderThread(float tpf) { if (audioDisabled) { return; @@ -1128,6 +1165,7 @@ public void updateInDecoderThread(float tpf) { for (int i = 0; i < channels.length; i++) { AudioSource src = channelSources[i]; + // Only process streaming sources associated with this channel if (src == null || !(src.getAudioData() instanceof AudioStream)) { continue; } @@ -1135,23 +1173,27 @@ public void updateInDecoderThread(float tpf) { int sourceId = channels[i]; AudioStream stream = (AudioStream) src.getAudioData(); - Status oalStatus = convertStatus(al.alGetSourcei(sourceId, AL_SOURCE_STATE)); + // Get current AL state, primarily to check if we need to restart playback + int oalState = al.alGetSourcei(sourceId, AL_SOURCE_STATE); + Status oalStatus = convertStatus(oalState); Status jmeStatus = src.getStatus(); // Keep filling data (even if we are stopped / paused) boolean buffersWereFilled = fillStreamingSource(sourceId, stream, src.isLooping()); + // Check if the source stopped due to buffer starvation while it was supposed to be playing if (buffersWereFilled && oalStatus == Status.Stopped && jmeStatus == Status.Playing) { // The source got stopped due to buffer starvation. // Start it again. - logger.log(Level.WARNING, "Buffer starvation occurred while playing stream"); + logger.log(Level.WARNING, "Buffer starvation detected for stream on channel {0}. Restarting playback.", i); al.alSourcePlay(sourceId); + checkAlError("restarting starved source " + sourceId); } } - // Delete any unused objects. + // Delete any unused objects (buffers, filters) that are no longer referenced. objManager.deleteUnused(this); } @@ -1323,7 +1365,7 @@ public void stopSource(AudioSource src) { /** * Gets the corresponding OpenAL format enum for the audio data properties. * @param audioData The AudioData. - * @return The OpenAL format enum (e.g., AL_FORMAT_STEREO16). + * @return The OpenAL format enum. * @throws UnsupportedOperationException if the format is not supported. */ private int getOpenALFormat(AudioData audioData) { @@ -1429,7 +1471,7 @@ public void deleteAudioData(AudioData audioData) { ib.position(0).limit(1); al.alDeleteBuffers(1, ib); checkAlError("deleting buffer " + id); - ab.resetObject(); + ab.resetObject(); // Mark as deleted on JME side } } else if (audioData instanceof AudioStream) { AudioStream as = (AudioStream) audioData; @@ -1439,7 +1481,7 @@ public void deleteAudioData(AudioData audioData) { ib.put(ids).flip(); al.alDeleteBuffers(ids.length, ib); checkAlError("deleting " + ids.length + " buffers"); - as.resetObject(); + as.resetObject(); // Mark as deleted on JME side } } } From 81f5614a199801bb5612386229a3ebc1ac7f4227 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Mon, 5 May 2025 15:07:14 +0200 Subject: [PATCH 06/13] Update ALAudioRenderer.java - remove recursive calls to updateSourceParam() method - more checkAlError - added helper methods - added comments - javadoc - fix Destroys the OpenAL context, deleting sources, buffers, filters, and effects. --- .../jme3/audio/openal/ALAudioRenderer.java | 640 +++++++++++------- 1 file changed, 383 insertions(+), 257 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index d6671c04f7..cf4cfc1676 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -83,7 +83,6 @@ public class ALAudioRenderer implements AudioRenderer, Runnable { private boolean supportEfx = false; private boolean supportPauseDevice = false; private boolean supportDisconnect = false; - private int auxSends = 0; // Update thread private static final float UPDATE_RATE = 0.05f; // Update streaming sources every 50ms @@ -108,6 +107,9 @@ public ALAudioRenderer(AL al, ALC alc, EFX efx) { this.efx = efx; } + /** + * Initializes the OpenAL and ALC context. + */ private void initOpenAL() { try { if (!alc.isCreated()) { @@ -123,17 +125,15 @@ private void initOpenAL() { printAudioRendererInfo(); - // Pause device is a feature used specifically on Android - // where the application could be closed but still running, - // thus the audio context remains open but no audio should be playing. + // Check for specific ALC extensions supportPauseDevice = alc.alcIsExtensionPresent("ALC_SOFT_pause_device"); if (!supportPauseDevice) { - logger.log(Level.WARNING, "Pausing audio device not supported."); + logger.log(Level.WARNING, "Pausing audio device not supported (ALC_SOFT_pause_device)."); } - - // Disconnected audio devices (such as USB sound cards, headphones...) - // never reconnect, the whole context must be re-created supportDisconnect = alc.alcIsExtensionPresent("ALC_EXT_disconnect"); + if (!supportDisconnect) { + logger.log(Level.INFO, "Device disconnect detection not supported (ALC_EXT_disconnect)."); + } initEfx(); } @@ -167,11 +167,11 @@ private void enumerateAvailableChannels() { // Find maximum # of sources supported by this implementation ArrayList channelList = new ArrayList<>(); for (int i = 0; i < MAX_NUM_CHANNELS; i++) { - int chan = al.alGenSources(); + int sourceId = al.alGenSources(); if (al.alGetError() != 0) { break; } else { - channelList.add(chan); + channelList.add(sourceId); } } @@ -201,8 +201,8 @@ private void initEfx() { ib.clear().limit(1); alc.alcGetInteger(EFX.ALC_MAX_AUXILIARY_SENDS, ib, 1); - auxSends = ib.get(0); - logger.log(Level.INFO, "Audio max auxiliary sends: {0}", auxSends); + int maxAuxSends = ib.get(0); + logger.log(Level.INFO, "Audio max auxiliary sends: {0}", maxAuxSends); // 1. Create reverb effect slot ib.clear().limit(1); @@ -232,11 +232,10 @@ private void initEfx() { */ private void destroyOpenAL() { if (audioDisabled) { - alc.destroyALC(); - return; + return; // Nothing to destroy if context wasn't created } - // stop any playing channels + // Stops channels and detaches buffers/filters for (int i = 0; i < channelSources.length; i++) { if (channelSources[i] != null) { clearChannel(i); @@ -256,7 +255,7 @@ private void destroyOpenAL() { // Delete EFX objects if they were created if (supportEfx) { if (reverbFx != -1) { - ib.position(0).limit(1); + ib.clear().limit(1); ib.put(0, reverbFx); efx.alDeleteEffects(1, ib); checkAlError("deleting reverbFx effect " + reverbFx); @@ -264,7 +263,7 @@ private void destroyOpenAL() { } if (reverbFxSlot != -1) { - ib.position(0).limit(1); + ib.clear().limit(1); ib.put(0, reverbFxSlot); efx.alDeleteAuxiliaryEffectSlots(1, ib); checkAlError("deleting effect reverbFxSlot " + reverbFxSlot); @@ -272,9 +271,19 @@ private void destroyOpenAL() { } } + channels = null; // Force re-enumeration + channelSources = null; + freeChannels.clear(); + nextChannelIndex = 0; + alc.destroyALC(); + logger.info("OpenAL context destroyed."); } + /** + * Initializes the OpenAL context, enumerates channels, checks capabilities, + * and starts the audio decoder thread. + */ @Override public void initialize() { if (decoderThread.isAlive()) { @@ -284,6 +293,11 @@ public void initialize() { // Initialize OpenAL context. initOpenAL(); + if (audioDisabled) { + logger.warning("Audio Disabled. Cannot start decoder thread."); + return; + } + // Initialize decoder thread. // Set high priority to avoid buffer starvation. decoderThread.setDaemon(true); @@ -356,10 +370,8 @@ public void cleanup() { } } - // Destroy OpenAL context (only if initialized and not disabled) - if (!audioDisabled && alc.isCreated()) { - destroyOpenAL(); - } + // Destroy OpenAL context + destroyOpenAL(); } /** @@ -384,20 +396,25 @@ private void updateFilter(Filter f) { efx.alFilteri(id, EFX.AL_FILTER_TYPE, EFX.AL_FILTER_LOWPASS); efx.alFilterf(id, EFX.AL_LOWPASS_GAIN, lowPass.getVolume()); efx.alFilterf(id, EFX.AL_LOWPASS_GAINHF, lowPass.getHighFreqVolume()); + + if (checkAlError("updating filter " + id)) { + deleteFilter(f); // Try to clean up + } else { + f.clearUpdateNeeded(); // Mark as updated in AL + } } // ** Add other filter types (HighPass, BandPass) here if implemented ** else { - logger.log(Level.WARNING, "Unsupported filter type: {0}", f.getClass().getName()); - } - - if (checkAlError("updating filter " + id)) { - deleteFilter(f); // Try to clean up - f.resetObject(); - } else { - f.clearUpdateNeeded(); // Mark as updated in AL + throw new UnsupportedOperationException("Unsupported filter type: " + f.getClass().getName()); } } + /** + * Gets the current playback time (in seconds) for a source. + * For streams, includes the time represented by already processed buffers. + * @param src The audio source. + * @return The playback time in seconds, or 0 if not playing or invalid. + */ @Override public float getSourcePlaybackTime(AudioSource src) { checkDead(); @@ -406,31 +423,30 @@ public float getSourcePlaybackTime(AudioSource src) { return 0; } - // See comment in updateSourceParam(). if (src.getChannel() < 0) { - return 0; + return 0; // Not playing or invalid state } int sourceId = channels[src.getChannel()]; AudioData data = src.getAudioData(); + if (data == null) { + return 0; // No audio data + } int playbackOffsetBytes = 0; + // For streams, add the bytes from buffers that have already been fully processed and unqueued. if (data instanceof AudioStream) { - // Because audio streams are processed in buffer chunks, - // we have to compute the amount of time the stream was already - // been playing based on the number of buffers that were processed. AudioStream stream = (AudioStream) data; - - // NOTE: the assumption is that all enqueued buffers are the same size. - // this is currently enforced by fillBuffer(). - - // The number of unenqueued bytes that the decoder thread - // keeps track of. + // This value is updated by the decoder thread when buffers are unqueued. playbackOffsetBytes = stream.getUnqueuedBufferBytes(); } // Add byte offset from source (for both streams and buffers) - playbackOffsetBytes += al.alGetSourcei(sourceId, AL_BYTE_OFFSET); + int byteOffset = al.alGetSourcei(sourceId, AL_BYTE_OFFSET); + if (checkAlError("getting source byte offset for " + sourceId)) { + return 0; // Error getting offset + } + playbackOffsetBytes += byteOffset; // Compute time value from bytes // E.g. for 44100 source with 2 channels and 16 bits per sample: @@ -439,10 +455,20 @@ public float getSourcePlaybackTime(AudioSource src) { * data.getChannels() * data.getBitsPerSample() / 8); + if (bytesPerSecond <= 0) { + logger.warning("Invalid bytesPerSecond calculated for source. Cannot get playback time."); + return 0; // Avoid division by zero + } + return (float) playbackOffsetBytes / bytesPerSecond; } } + /** + * Updates a specific parameter for an audio source on its assigned channel. + * @param src The audio source. + * @param param The parameter to update. + */ @Override public void updateSourceParam(AudioSource src, AudioParam param) { checkDead(); @@ -451,156 +477,88 @@ public void updateSourceParam(AudioSource src, AudioParam param) { return; } - // There is a race condition in AudioSource that can - // cause this to be called for a node that has been - // detached from its channel. For example, setVolume() - // called from the render thread may see that the AudioSource - // still has a channel value but the audio thread may - // clear that channel before setVolume() gets to call - // updateSourceParam() (because the audio stopped playing - // on its own right as the volume was set). In this case, - // it should be safe to just ignore the update. - if (src.getChannel() < 0) { + int channel = src.getChannel(); + // Parameter updates only make sense if the source is associated with a channel + // and hasn't been stopped (which would set channel to -1). + if (channel < 0) { + // This can happen due to race conditions if a source stops playing + // right as a parameter update is requested from another thread. + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Ignoring parameter update for source {0} as it's not validly associated with channel {1}.", + new Object[]{src, channel}); + } return; } - assert src.getChannel() >= 0; - - int sourceId = channels[src.getChannel()]; - int filterId = EFX.AL_FILTER_NULL; + int sourceId = channels[channel]; switch (param) { case Position: - if (!src.isPositional()) { - return; + if (src.isPositional()) { + Vector3f pos = src.getPosition(); + al.alSource3f(sourceId, AL_POSITION, pos.x, pos.y, pos.z); } - Vector3f pos = src.getPosition(); - al.alSource3f(sourceId, AL_POSITION, pos.x, pos.y, pos.z); break; case Velocity: - if (!src.isPositional()) { - return; + if (src.isPositional()) { + Vector3f vel = src.getVelocity(); + al.alSource3f(sourceId, AL_VELOCITY, vel.x, vel.y, vel.z); } - Vector3f vel = src.getVelocity(); - al.alSource3f(sourceId, AL_VELOCITY, vel.x, vel.y, vel.z); break; case MaxDistance: - if (!src.isPositional()) { - return; + if (src.isPositional()) { + al.alSourcef(sourceId, AL_MAX_DISTANCE, src.getMaxDistance()); } - al.alSourcef(sourceId, AL_MAX_DISTANCE, src.getMaxDistance()); break; case RefDistance: - if (!src.isPositional()) { - return; - } - al.alSourcef(sourceId, AL_REFERENCE_DISTANCE, src.getRefDistance()); - break; - - case ReverbFilter: - if (!supportEfx || !src.isPositional() || !src.isReverbEnabled()) { - return; - } - Filter reverbFilter = src.getReverbFilter(); - if (reverbFilter != null) { - if (reverbFilter.isUpdateNeeded()) { - updateFilter(reverbFilter); - } - filterId = reverbFilter.getId(); - } - al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filterId); - break; - - case ReverbEnabled: - if (!supportEfx || !src.isPositional()) { - return; - } - if (src.isReverbEnabled()) { - updateSourceParam(src, AudioParam.ReverbFilter); - } else { - al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); + if (src.isPositional()) { + al.alSourcef(sourceId, AL_REFERENCE_DISTANCE, src.getRefDistance()); } break; case IsPositional: - if (!src.isPositional()) { - // Play in headspace - al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE); - al.alSource3f(sourceId, AL_POSITION, 0, 0, 0); - al.alSource3f(sourceId, AL_VELOCITY, 0, 0, 0); - - // Disable reverb - al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); - } else { - al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_FALSE); - updateSourceParam(src, AudioParam.Position); - updateSourceParam(src, AudioParam.Velocity); - updateSourceParam(src, AudioParam.MaxDistance); - updateSourceParam(src, AudioParam.RefDistance); - updateSourceParam(src, AudioParam.ReverbEnabled); - } + applySourcePositionalState(sourceId, src); break; case Direction: - if (!src.isDirectional()) { - return; + if (src.isDirectional()) { + Vector3f dir = src.getDirection(); + al.alSource3f(sourceId, AL_DIRECTION, dir.x, dir.y, dir.z); } - Vector3f dir = src.getDirection(); - al.alSource3f(sourceId, AL_DIRECTION, dir.x, dir.y, dir.z); break; case InnerAngle: - if (!src.isDirectional()) { - return; + if (src.isDirectional()) { + al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, src.getInnerAngle()); } - al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, src.getInnerAngle()); break; case OuterAngle: - if (!src.isDirectional()) { - return; + if (src.isDirectional()) { + al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, src.getOuterAngle()); } - al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, src.getOuterAngle()); break; case IsDirectional: - if (src.isDirectional()) { - updateSourceParam(src, AudioParam.Direction); - updateSourceParam(src, AudioParam.InnerAngle); - updateSourceParam(src, AudioParam.OuterAngle); - al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 0); - } else { - al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, 360); - al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, 360); - al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 1f); - } + applySourceDirectionalState(sourceId, src); break; case DryFilter: - if (!supportEfx) { - return; - } - Filter dryFilter = src.getDryFilter(); - if (dryFilter != null) { - if (dryFilter.isUpdateNeeded()) { - updateFilter(dryFilter); - } - filterId = dryFilter.getId(); + applySourceDryFilter(sourceId, src); + break; + + case ReverbEnabled: + case ReverbFilter: + if (src.isPositional()) { + applySourceReverbFilter(sourceId, src); } - // NOTE: must re-attach filter for changes to apply. - al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, filterId); break; case Looping: - if (src.getAudioData() instanceof AudioStream) { - al.alSourcei(sourceId, AL_LOOPING, AL_FALSE); - } else { - // AudioData instanceof AudioBuffer - al.alSourcei(sourceId, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE); - } + applySourceLooping(sourceId, src, false); break; case Volume: @@ -610,56 +568,107 @@ public void updateSourceParam(AudioSource src, AudioParam param) { case Pitch: al.alSourcef(sourceId, AL_PITCH, src.getPitch()); break; + + default: + logger.log(Level.WARNING, "Unhandled source parameter update: {0}", param); + break; } } } + /** + * Applies all parameters from the AudioSource to the specified OpenAL source ID. + * Used when initially playing a source or instance. + * + * @param sourceId The OpenAL source ID. + * @param src The jME AudioSource. + * @param forceNonLoop If true, looping will be disabled regardless of source setting (used for instances). + */ private void setSourceParams(int sourceId, AudioSource src, boolean forceNonLoop) { + + al.alSourcef(sourceId, AL_GAIN, src.getVolume()); + al.alSourcef(sourceId, AL_PITCH, src.getPitch()); + al.alSourcef(sourceId, AL_SEC_OFFSET, src.getTimeOffset()); + + applySourceLooping(sourceId, src, forceNonLoop); + applySourcePositionalState(sourceId, src); + applySourceDirectionalState(sourceId, src); + applySourceDryFilter(sourceId, src); + } + + // --- Source Parameter Helper Methods --- + + private void applySourceDryFilter(int sourceId, AudioSource src) { + if (supportEfx && src.getDryFilter() != null) { + Filter f = src.getDryFilter(); + if (f.isUpdateNeeded()) { + updateFilter(f); + // NOTE: must re-attach filter for changes to apply. + al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, f.getId()); + checkAlError("setting source direct filter for " + sourceId); + } + } + } + + private void applySourceReverbFilter(int sourceId, AudioSource src) { + if (supportEfx && src.isReverbEnabled()) { + int filterId = EFX.AL_FILTER_NULL; + if (src.getReverbFilter() != null) { + Filter f = src.getReverbFilter(); + if (f.isUpdateNeeded()) { + updateFilter(f); + } + filterId = f.getId(); + } + al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filterId); + checkAlError("setting source reverb send for " + sourceId); + } + } + + private void applySourceLooping(int sourceId, AudioSource src, boolean forceNonLoop) { + boolean looping = !forceNonLoop && src.isLooping(); + // Streams handle looping internally by rewinding, not via AL_LOOPING. + if (src.getAudioData() instanceof AudioStream) { + looping = false; + } + al.alSourcei(sourceId, AL_LOOPING, looping ? AL_TRUE : AL_FALSE); + checkAlError("setting source looping for " + sourceId); + } + + /** Sets AL_SOURCE_RELATIVE and applies position/velocity/distance accordingly */ + private void applySourcePositionalState(int sourceId, AudioSource src) { if (src.isPositional()) { + // Play in world space: absolute position/velocity Vector3f pos = src.getPosition(); Vector3f vel = src.getVelocity(); al.alSource3f(sourceId, AL_POSITION, pos.x, pos.y, pos.z); al.alSource3f(sourceId, AL_VELOCITY, vel.x, vel.y, vel.z); - al.alSourcef(sourceId, AL_MAX_DISTANCE, src.getMaxDistance()); al.alSourcef(sourceId, AL_REFERENCE_DISTANCE, src.getRefDistance()); + al.alSourcef(sourceId, AL_MAX_DISTANCE, src.getMaxDistance()); al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_FALSE); - if (src.isReverbEnabled() && supportEfx) { - int filterId = EFX.AL_FILTER_NULL; - if (src.getReverbFilter() != null) { - Filter f = src.getReverbFilter(); - if (f.isUpdateNeeded()) { - updateFilter(f); - } - filterId = f.getId(); - } - al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filterId); - } + applySourceReverbFilter(sourceId, src); + } else { - // play in headspace - al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE); + // Play in headspace: relative to listener, fixed position/velocity al.alSource3f(sourceId, AL_POSITION, 0, 0, 0); al.alSource3f(sourceId, AL_VELOCITY, 0, 0, 0); - } + al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE); - if (src.getDryFilter() != null && supportEfx) { - Filter f = src.getDryFilter(); - if (f.isUpdateNeeded()) { - updateFilter(f); - // NOTE: must re-attach filter for changes to apply. - al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, f.getId()); - } - } + // Disable distance attenuation for non-positional sounds + al.alSourcef(sourceId, AL_REFERENCE_DISTANCE, 1e10f); + al.alSourcef(sourceId, AL_MAX_DISTANCE, 2e10f); - if (forceNonLoop || src.getAudioData() instanceof AudioStream) { - al.alSourcei(sourceId, AL_LOOPING, AL_FALSE); - } else { - al.alSourcei(sourceId, AL_LOOPING, src.isLooping() ? AL_TRUE : AL_FALSE); + // Disable reverb send for non-positional sounds + if (supportEfx) { + al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); + } } - al.alSourcef(sourceId, AL_GAIN, src.getVolume()); - al.alSourcef(sourceId, AL_PITCH, src.getPitch()); - al.alSourcef(sourceId, AL_SEC_OFFSET, src.getTimeOffset()); + checkAlError("setting source positional state for " + sourceId); + } + /** Sets cone angles/gain based on whether the source is directional */ + private void applySourceDirectionalState(int sourceId, AudioSource src) { if (src.isDirectional()) { Vector3f dir = src.getDirection(); al.alSource3f(sourceId, AL_DIRECTION, dir.x, dir.y, dir.z); @@ -667,10 +676,12 @@ private void setSourceParams(int sourceId, AudioSource src, boolean forceNonLoop al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, src.getOuterAngle()); al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 0); } else { - al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, 360); - al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, 360); + // Omnidirectional: 360 degree cone, full gain + al.alSourcef(sourceId, AL_CONE_INNER_ANGLE, 360f); + al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, 360f); al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 1f); } + checkAlError("setting source directional state for " + sourceId); } /** @@ -811,33 +822,58 @@ public void setEnvironment(Environment env) { } } - private boolean fillBuffer(AudioStream stream, int id) { - int size = 0; - int result; + /** + * Fills a single OpenAL buffer with data from the audio stream. + * Uses the shared nativeBuf and arrayBuf. + * + * @param stream The AudioStream to read from. + * @param bufferId The OpenAL buffer ID to fill. + * @return True if the buffer was filled with data, false if stream EOF was reached before filling. + */ + private boolean fillBuffer(AudioStream stream, int bufferId) { + int totalBytesRead = 0; + int bytesRead; - while (size < arrayBuf.length) { - result = stream.readSamples(arrayBuf, size, arrayBuf.length - size); + while (totalBytesRead < arrayBuf.length) { + bytesRead = stream.readSamples(arrayBuf, totalBytesRead, arrayBuf.length - totalBytesRead); - if (result > 0) { - size += result; + if (bytesRead > 0) { + totalBytesRead += bytesRead; } else { break; } } - if (size == 0) { + if (totalBytesRead == 0) { return false; } + // Copy data from arrayBuf to nativeBuf nativeBuf.clear(); - nativeBuf.put(arrayBuf, 0, size); + nativeBuf.put(arrayBuf, 0, totalBytesRead); nativeBuf.flip(); - al.alBufferData(id, getOpenALFormat(stream), nativeBuf, size, stream.getSampleRate()); + // Upload data to the OpenAL buffer + int format = getOpenALFormat(stream); + int sampleRate = stream.getSampleRate(); + al.alBufferData(bufferId, format, nativeBuf, totalBytesRead, sampleRate); + + if (checkAlError("filling buffer " + bufferId + " for stream")) { + return false; + } return true; } + /** + * Unqueues processed buffers from a streaming source and refills/requeues them. + * Updates the stream's internal count of processed bytes. + * + * @param sourceId The OpenAL source ID. + * @param stream The AudioStream. + * @param looping Whether the stream should loop internally. + * @return True if at least one buffer was successfully refilled and requeued. + */ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean looping) { boolean success = false; int processed = al.alGetSourcei(sourceId, AL_BUFFERS_PROCESSED); @@ -855,22 +891,22 @@ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean lo // be the case... unqueuedBufferBytes += BUFFER_SIZE; - boolean active = fillBuffer(stream, buffer); - - if (!active && !stream.isEOF()) { + // Try to refill the buffer + boolean filled = fillBuffer(stream, buffer); + if (!filled && !stream.isEOF()) { throw new AssertionError(); } - if (!active && looping) { + if (!filled && looping) { stream.setTime(0); - active = fillBuffer(stream, buffer); - if (!active) { + filled = fillBuffer(stream, buffer); // Try filling again + if (!filled) { throw new IllegalStateException("Looping streaming source " + "was rewound but could not be filled"); } } - if (active) { + if (filled) { ib.position(0).limit(1); ib.put(0, buffer); al.alSourceQueueBuffers(sourceId, 1, ib); @@ -882,6 +918,7 @@ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean lo } } + // Update the stream's internal counter for processed bytes stream.setUnqueuedBufferBytes(stream.getUnqueuedBufferBytes() + unqueuedBufferBytes); return success; @@ -897,19 +934,22 @@ private void attachStreamToSource(int sourceId, AudioStream stream, boolean loop } for (int id : stream.getIds()) { - boolean active = fillBuffer(stream, id); - if (!active && !stream.isEOF()) { + // Try to refill the buffer + boolean filled = fillBuffer(stream, id); + if (!filled && !stream.isEOF()) { throw new AssertionError(); } - if (!active && looping) { + + if (!filled && looping) { stream.setTime(0); - active = fillBuffer(stream, id); - if (!active) { + filled = fillBuffer(stream, id); + if (!filled) { throw new IllegalStateException("Looping streaming source " + "was rewound but could not be filled"); } } - if (active) { + + if (filled) { ib.position(0).limit(1); ib.put(id).flip(); al.alSourceQueueBuffers(sourceId, 1, ib); @@ -974,8 +1014,8 @@ private void clearChannel(int index) { } } - private AudioSource.Status convertStatus(int oalStatus) { - switch (oalStatus) { + private AudioSource.Status convertStatus(int openALState) { + switch (openALState) { case AL_INITIAL: case AL_STOPPED: return Status.Stopped; @@ -984,7 +1024,7 @@ private AudioSource.Status convertStatus(int oalStatus) { case AL_PLAYING: return Status.Playing; default: - throw new UnsupportedOperationException("Unrecognized OpenAL state: " + oalStatus); + throw new UnsupportedOperationException("Unrecognized OpenAL state: " + openALState); } } @@ -995,20 +1035,30 @@ public void update(float tpf) { } } + /** + * Checks the device connection status and attempts to restart the renderer if disconnected. + * Called periodically from the decoder thread. + */ private void checkDevice() { - // If the device is disconnected, pick a new one - if (isDisconnected()) { - logger.log(Level.INFO, "Current audio device disconnected."); + if (isDeviceDisconnected()) { + logger.log(Level.WARNING, "Audio device disconnected! Attempting to restart audio renderer..."); restartAudioRenderer(); } } - private boolean isDisconnected() { - if (!supportDisconnect) { + /** + * Checks if the audio device has been disconnected. + * Requires ALC_EXT_disconnect extension. + * @return True if disconnected, false otherwise or if not supported. + */ + private boolean isDeviceDisconnected() { + if (audioDisabled || !supportDisconnect) { return false; } + ib.clear().limit(1); alc.alcGetInteger(ALC.ALC_CONNECTED, ib, 1); + // Returns 1 if connected, 0 if disconnected. return ib.get(0) == 0; } @@ -1067,19 +1117,19 @@ public void updateInRenderThread(float tpf) { boolean reclaimChannel = false; // Get OpenAL status for the source - int oalState = al.alGetSourcei(sourceId, AL_SOURCE_STATE); - Status oalStatus = convertStatus(oalState); + int openALState = al.alGetSourcei(sourceId, AL_SOURCE_STATE); + Status openALStatus = convertStatus(openALState); // --- Handle Instanced Playback (Not bound to a specific channel) --- if (!boundSource) { - if (oalStatus == Status.Stopped) { + if (openALStatus == Status.Stopped) { // Instanced audio (non-looping buffer) finished playing. Reclaim channel. if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "Reclaiming channel {0} from finished instance.", i); } clearChannel(i); // Stop source, detach buffer/filter freeChannel(i); // Add channel back to the free pool - } else if (oalStatus == Status.Paused) { + } else if (openALStatus == Status.Paused) { throw new AssertionError("Instanced audio source on channel " + i + " cannot be paused."); } @@ -1090,9 +1140,9 @@ public void updateInRenderThread(float tpf) { // --- Handle Bound Playback (Normal play/pause/stop) --- Status jmeStatus = src.getStatus(); - // Check if we need to sync JME status with OAL status. - if (oalStatus != jmeStatus) { - if (oalStatus == Status.Stopped && jmeStatus == Status.Playing) { + // Check if we need to sync JME status with OpenAL status. + if (openALStatus != jmeStatus) { + if (openALStatus == Status.Stopped && jmeStatus == Status.Playing) { // Source stopped playing unexpectedly (finished or starved) if (src.getAudioData() instanceof AudioStream) { @@ -1137,14 +1187,14 @@ public void updateInRenderThread(float tpf) { freeChannel(i); // Add channel back to the free pool } } else { - // jME3 state does not match OAL state. + // jME3 state does not match OpenAL state. // This is only relevant for bound sources. throw new AssertionError("Unexpected sound status. " - + "OpenAL: " + oalStatus + ", JME: " + jmeStatus); + + "OpenAL: " + openALStatus + ", JME: " + jmeStatus); } } else { // Stopped channel was not cleared correctly. - if (oalStatus == Status.Stopped) { + if (openALStatus == Status.Stopped) { throw new AssertionError("Channel " + i + " was not reclaimed"); } } @@ -1174,8 +1224,8 @@ public void updateInDecoderThread(float tpf) { AudioStream stream = (AudioStream) src.getAudioData(); // Get current AL state, primarily to check if we need to restart playback - int oalState = al.alGetSourcei(sourceId, AL_SOURCE_STATE); - Status oalStatus = convertStatus(oalState); + int openALState = al.alGetSourcei(sourceId, AL_SOURCE_STATE); + Status openALStatus = convertStatus(openALState); Status jmeStatus = src.getStatus(); // Keep filling data (even if we are stopped / paused) @@ -1183,7 +1233,7 @@ public void updateInDecoderThread(float tpf) { // Check if the source stopped due to buffer starvation while it was supposed to be playing if (buffersWereFilled - && oalStatus == Status.Stopped + && openALStatus == Status.Stopped && jmeStatus == Status.Playing) { // The source got stopped due to buffer starvation. // Start it again. @@ -1221,24 +1271,45 @@ public void setListener(Listener listener) { } } + /** + * Pauses all audio playback by pausing the OpenAL device context. + * Requires ALC_SOFT_pause_device extension. + * @throws UnsupportedOperationException if the extension is not supported. + */ @Override public void pauseAll() { if (!supportPauseDevice) { - throw new UnsupportedOperationException("Pause device is NOT supported!"); + throw new UnsupportedOperationException( + "Pausing the audio device is not supported by the current OpenAL driver" + + " (requires ALC_SOFT_pause_device)."); } alc.alcDevicePauseSOFT(); + logger.info("Audio device paused."); } + /** + * Resumes all audio playback by resuming the OpenAL device context. + * Requires ALC_SOFT_pause_device extension. + * @throws UnsupportedOperationException if the extension is not supported. + */ @Override public void resumeAll() { if (!supportPauseDevice) { - throw new UnsupportedOperationException("Pause device is NOT supported!"); + throw new UnsupportedOperationException( + "Resuming the audio device is not supported by the current OpenAL driver" + + " (requires ALC_SOFT_pause_device)."); } alc.alcDeviceResumeSOFT(); + logger.info("Audio device resumed."); } + /** + * Plays an audio source as a one-shot instance (non-looping buffer). + * A free channel is allocated temporarily. + * @param src The audio source to play. + */ @Override public void playSourceInstance(AudioSource src) { checkDead(); @@ -1248,6 +1319,10 @@ public void playSourceInstance(AudioSource src) { } AudioData audioData = src.getAudioData(); + if (audioData == null) { + logger.warning("playSourceInstance called with null AudioData."); + return; + } if (audioData instanceof AudioStream) { throw new UnsupportedOperationException( "Cannot play instances of audio streams. Use play() instead."); @@ -1257,25 +1332,33 @@ public void playSourceInstance(AudioSource src) { updateAudioData(audioData); } - // create a new index for an audio-channel + // Allocate a temporary channel int index = newChannel(); if (index == -1) { + logger.log(Level.WARNING, "No channel available to play instance of {0}", src); return; } + // Ensure channel is clean before use int sourceId = channels[index]; clearChannel(index); - // set parameters, like position and max distance - setSourceParams(sourceId, src, true); // forceNonLoop - attachAudioToSource(sourceId, audioData, false); // no looping + // Set parameters for this specific instance (force non-looping) + setSourceParams(sourceId, src, true); + attachAudioToSource(sourceId, audioData, false); channelSources[index] = src; // play the channel al.alSourcePlay(sourceId); + checkAlError("playing source instance " + sourceId); } } + /** + * Plays an audio source, allocating a persistent channel for it. + * Handles both buffers and streams. Can be paused and stopped. + * @param src The audio source to play. + */ @Override public void playSource(AudioSource src) { checkDead(); @@ -1291,30 +1374,46 @@ public void playSource(AudioSource src) { // something different from -1 when first playing an AudioNode. // assert src.getChannel() != -1; - // allocate channel to this source + AudioData audioData = src.getAudioData(); + if (audioData == null) { + logger.log(Level.WARNING, "playSource called on source with null AudioData: {0}", src); + return; + } + if (audioData.isUpdateNeeded()) { + updateAudioData(audioData); + } + + // Allocate a temporary channel int index = newChannel(); if (index == -1) { - logger.log(Level.WARNING, "No channel available to play {0}", src); + logger.log(Level.WARNING, "No channel available to play instance of {0}", src); return; } + + // Ensure channel is clean before use + int sourceId = channels[index]; clearChannel(index); src.setChannel(index); - AudioData audioData = src.getAudioData(); - if (audioData.isUpdateNeeded()) { - updateAudioData(audioData); - } - + // Set all source parameters and attach the audio data + setSourceParams(sourceId, src, false); + attachAudioToSource(sourceId, audioData, src.isLooping()); channelSources[index] = src; - setSourceParams(channels[index], src, false); - attachAudioToSource(channels[index], audioData, src.isLooping()); } - al.alSourcePlay(channels[src.getChannel()]); - src.setStatus(Status.Playing); + // play the channel + int sourceId = channels[src.getChannel()]; + al.alSourcePlay(sourceId); + if (!checkAlError("playing source " + sourceId)) { + src.setStatus(Status.Playing); // Update JME status on success + } } } + /** + * Pauses a playing audio source. + * @param src The audio source to pause. + */ @Override public void pauseSource(AudioSource src) { checkDead(); @@ -1326,12 +1425,20 @@ public void pauseSource(AudioSource src) { if (src.getStatus() == Status.Playing) { assert src.getChannel() != -1; - al.alSourcePause(channels[src.getChannel()]); - src.setStatus(Status.Paused); + int sourceId = channels[src.getChannel()]; + al.alSourcePause(sourceId); + if (!checkAlError("pausing source " + sourceId)) { + src.setStatus(Status.Paused); // Update JME status on success + } } } } + /** + * Stops a playing or paused audio source, releasing its channel. + * For streams, resets or closes the stream. + * @param src The audio source to stop. + */ @Override public void stopSource(AudioSource src) { synchronized (threadLock) { @@ -1349,8 +1456,8 @@ public void stopSource(AudioSource src) { freeChannel(channel); if (src.getAudioData() instanceof AudioStream) { - // If the stream is seekable, then rewind it. - // Otherwise, close it, as it is no longer valid. + // If the stream is seekable, rewind it to the beginning. + // Otherwise (non-seekable), close it, as it might be invalid now. AudioStream stream = (AudioStream) src.getAudioData(); if (stream.isSeekable()) { stream.setTime(0); @@ -1386,44 +1493,63 @@ private int getOpenALFormat(AudioData audioData) { return AL_FORMAT_STEREO16; } } - // Add support for AL_EXT_MCFORMATS if needed later // Format not supported throw new UnsupportedOperationException("Unsupported audio format: " + channels + " channels, " + bitsPerSample + " bits per sample."); } + /** + * Uploads buffer data to OpenAL. Generates buffer ID if needed. + * @param ab The AudioBuffer. + */ private void updateAudioBuffer(AudioBuffer ab) { int id = ab.getId(); if (ab.getId() == -1) { - ib.position(0).limit(1); + ib.clear().limit(1); al.alGenBuffers(1, ib); + checkAlError("generating bufferId"); id = ib.get(0); ab.setId(id); + // Register for automatic cleanup if unused objManager.registerObject(ab); } - ab.getData().clear(); - al.alBufferData(id, getOpenALFormat(ab), ab.getData(), ab.getData().capacity(), ab.getSampleRate()); - ab.clearUpdateNeeded(); + ByteBuffer data = ab.getData(); + + data.clear(); // Ensure buffer is ready for reading + int format = getOpenALFormat(ab); + int sampleRate = ab.getSampleRate(); + + al.alBufferData(id, format, data, data.capacity(), sampleRate); + if (!checkAlError("uploading buffer data for ID " + id)) { + ab.clearUpdateNeeded(); + } } + /** + * Prepares OpenAL buffers for an AudioStream. Generates buffer IDs. + * Does not fill buffers with data yet. + * @param as The AudioStream. + */ private void updateAudioStream(AudioStream as) { + // Delete old buffers if they exist (e.g., re-initializing stream) if (as.getIds() != null) { deleteAudioData(as); } int[] ids = new int[STREAMING_BUFFER_COUNT]; - ib.position(0).limit(STREAMING_BUFFER_COUNT); + ib.clear().limit(STREAMING_BUFFER_COUNT); + al.alGenBuffers(STREAMING_BUFFER_COUNT, ib); - ib.position(0).limit(STREAMING_BUFFER_COUNT); - ib.get(ids); + checkAlError("generating stream buffers ids"); - // Not registered with object manager. - // AudioStreams can be handled without object manager - // since their lifecycle is known to the audio renderer. + ib.rewind(); + ib.get(ids); + // Streams are managed directly, not via NativeObjectManager, + // because their lifecycle is tied to active playback. as.setIds(ids); as.clearUpdateNeeded(); } @@ -1444,7 +1570,7 @@ private void updateAudioData(AudioData audioData) { public void deleteFilter(Filter filter) { int id = filter.getId(); if (id != -1) { - ib.position(0).limit(1); + ib.clear().limit(1); ib.put(id).flip(); efx.alDeleteFilters(1, ib); checkAlError("deleting filter " + id); @@ -1468,7 +1594,7 @@ public void deleteAudioData(AudioData audioData) { int id = ab.getId(); if (id != -1) { ib.put(0, id); - ib.position(0).limit(1); + ib.clear().limit(1); al.alDeleteBuffers(1, ib); checkAlError("deleting buffer " + id); ab.resetObject(); // Mark as deleted on JME side From 9f426078dc314af9045e12a321d30ecdfd159308 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Mon, 5 May 2025 22:16:32 +0200 Subject: [PATCH 07/13] fix updateSourceParams --- .../jme3/audio/openal/ALAudioRenderer.java | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index cf4cfc1676..88a75d4a10 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -551,6 +551,16 @@ public void updateSourceParam(AudioSource src, AudioParam param) { break; case ReverbEnabled: + if (!supportEfx || !src.isPositional()) { + return; + } + if (!src.isReverbEnabled()) { + al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); + } else { + applySourceReverbFilter(sourceId, src); + } + break; + case ReverbFilter: if (src.isPositional()) { applySourceReverbFilter(sourceId, src); @@ -599,21 +609,25 @@ private void setSourceParams(int sourceId, AudioSource src, boolean forceNonLoop // --- Source Parameter Helper Methods --- private void applySourceDryFilter(int sourceId, AudioSource src) { - if (supportEfx && src.getDryFilter() != null) { - Filter f = src.getDryFilter(); - if (f.isUpdateNeeded()) { - updateFilter(f); - // NOTE: must re-attach filter for changes to apply. - al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, f.getId()); - checkAlError("setting source direct filter for " + sourceId); + if (supportEfx) { + int filterId = EFX.AL_FILTER_NULL; + if (src.getDryFilter() != null) { + Filter f = src.getDryFilter(); + if (f.isUpdateNeeded()) { + updateFilter(f); + } + filterId = f.getId(); } + // NOTE: must re-attach filter for changes to apply. + al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, filterId); + checkAlError("setting source direct filter for " + sourceId); } } private void applySourceReverbFilter(int sourceId, AudioSource src) { - if (supportEfx && src.isReverbEnabled()) { + if (supportEfx) { int filterId = EFX.AL_FILTER_NULL; - if (src.getReverbFilter() != null) { + if (src.isReverbEnabled() && src.getReverbFilter() != null) { Filter f = src.getReverbFilter(); if (f.isUpdateNeeded()) { updateFilter(f); @@ -647,18 +661,19 @@ private void applySourcePositionalState(int sourceId, AudioSource src) { al.alSourcef(sourceId, AL_MAX_DISTANCE, src.getMaxDistance()); al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_FALSE); - applySourceReverbFilter(sourceId, src); - + if (supportEfx) { + if (!src.isReverbEnabled()) { + al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); + } else { + applySourceReverbFilter(sourceId, src); + } + } } else { // Play in headspace: relative to listener, fixed position/velocity al.alSource3f(sourceId, AL_POSITION, 0, 0, 0); al.alSource3f(sourceId, AL_VELOCITY, 0, 0, 0); al.alSourcei(sourceId, AL_SOURCE_RELATIVE, AL_TRUE); - // Disable distance attenuation for non-positional sounds - al.alSourcef(sourceId, AL_REFERENCE_DISTANCE, 1e10f); - al.alSourcef(sourceId, AL_MAX_DISTANCE, 2e10f); - // Disable reverb send for non-positional sounds if (supportEfx) { al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); @@ -948,7 +963,7 @@ private void attachStreamToSource(int sourceId, AudioStream stream, boolean loop + "was rewound but could not be filled"); } } - + if (filled) { ib.position(0).limit(1); ib.put(id).flip(); @@ -1368,20 +1383,17 @@ public void playSource(AudioSource src) { } if (src.getStatus() == Status.Playing) { + // Already playing, do nothing. return; - } else if (src.getStatus() == Status.Stopped) { - // Assertion removed because it seems it's not possible to have - // something different from -1 when first playing an AudioNode. - // assert src.getChannel() != -1; + } + + if (src.getStatus() == Status.Stopped) { AudioData audioData = src.getAudioData(); if (audioData == null) { logger.log(Level.WARNING, "playSource called on source with null AudioData: {0}", src); return; } - if (audioData.isUpdateNeeded()) { - updateAudioData(audioData); - } // Allocate a temporary channel int index = newChannel(); @@ -1395,10 +1407,14 @@ public void playSource(AudioSource src) { clearChannel(index); src.setChannel(index); + if (audioData.isUpdateNeeded()) { + updateAudioData(audioData); + } + // Set all source parameters and attach the audio data + channelSources[index] = src; setSourceParams(sourceId, src, false); attachAudioToSource(sourceId, audioData, src.isLooping()); - channelSources[index] = src; } // play the channel From 4badf3e4331fda9cbb87a618e7fe091ddfb69d73 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Tue, 6 May 2025 18:01:12 +0200 Subject: [PATCH 08/13] reorder update source params --- .../jme3/audio/openal/ALAudioRenderer.java | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index 88a75d4a10..d21aa743cb 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -31,9 +31,20 @@ */ package com.jme3.audio.openal; -import com.jme3.audio.*; +import com.jme3.audio.AudioBuffer; +import com.jme3.audio.AudioData; +import com.jme3.audio.AudioParam; +import com.jme3.audio.AudioRenderer; +import com.jme3.audio.AudioSource; import com.jme3.audio.AudioSource.Status; import static com.jme3.audio.openal.AL.*; + +import com.jme3.audio.AudioStream; +import com.jme3.audio.Environment; +import com.jme3.audio.Filter; +import com.jme3.audio.Listener; +import com.jme3.audio.ListenerParam; +import com.jme3.audio.LowPassFilter; import com.jme3.math.Vector3f; import com.jme3.util.BufferUtils; import com.jme3.util.NativeObjectManager; @@ -550,20 +561,19 @@ public void updateSourceParam(AudioSource src, AudioParam param) { applySourceDryFilter(sourceId, src); break; - case ReverbEnabled: - if (!supportEfx || !src.isPositional()) { - return; - } - if (!src.isReverbEnabled()) { - al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); - } else { + case ReverbFilter: + if (src.isPositional()) { applySourceReverbFilter(sourceId, src); } break; - case ReverbFilter: - if (src.isPositional()) { - applySourceReverbFilter(sourceId, src); + case ReverbEnabled: + if (supportEfx && src.isPositional()) { + if (!src.isReverbEnabled()) { + al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); + } else { + applySourceReverbFilter(sourceId, src); + } } break; @@ -876,7 +886,6 @@ private boolean fillBuffer(AudioStream stream, int bufferId) { if (checkAlError("filling buffer " + bufferId + " for stream")) { return false; } - return true; } From 00605517d6db05f81175b98bb11195f54c846420 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Wed, 7 May 2025 15:54:47 +0200 Subject: [PATCH 09/13] enhanced log message --- .../src/main/java/com/jme3/audio/openal/ALAudioRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index d21aa743cb..e89e0091dd 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -1344,7 +1344,7 @@ public void playSourceInstance(AudioSource src) { AudioData audioData = src.getAudioData(); if (audioData == null) { - logger.warning("playSourceInstance called with null AudioData."); + logger.log(Level.WARNING, "playSourceInstance called on source with null AudioData: {0}", src); return; } if (audioData instanceof AudioStream) { From db60f781b6144713203c76bc61dbadd758c0533e Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Thu, 8 May 2025 18:53:59 +0200 Subject: [PATCH 10/13] Update ALAudioRenderer: enhanced log message --- .../java/com/jme3/audio/openal/ALAudioRenderer.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index e89e0091dd..ed10494999 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -1447,6 +1447,12 @@ public void pauseSource(AudioSource src) { return; } + AudioData audioData = src.getAudioData(); + if (audioData == null) { + logger.log(Level.WARNING, "pauseSource called on source with null AudioData: {0}", src); + return; + } + if (src.getStatus() == Status.Playing) { assert src.getChannel() != -1; @@ -1471,6 +1477,12 @@ public void stopSource(AudioSource src) { return; } + AudioData audioData = src.getAudioData(); + if (audioData == null) { + logger.log(Level.WARNING, "stopSource called on source with null AudioData: {0}", src); + return; + } + if (src.getStatus() != Status.Stopped) { int channel = src.getChannel(); assert channel != -1; // if it's not stopped, must have id From 05e496a87f33f7e927bb966b10f97a144ac5845f Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sat, 10 May 2025 01:22:07 +0200 Subject: [PATCH 11/13] Prefer ArrayDeque over ArrayList You should consider using ArrayDeque when: You need a collection that efficiently supports adding and removing elements from both ends. This makes it ideal for implementing data structures like queues and stacks. You frequently perform removals from the beginning of the collection. see ALAudioRenderer.newChannel() and ALAudioRenderer.freeChannel() --- .../java/com/jme3/audio/openal/ALAudioRenderer.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index ed10494999..8bd323cb10 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -51,6 +51,7 @@ import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; @@ -81,7 +82,7 @@ public class ALAudioRenderer implements AudioRenderer, Runnable { private int[] channels; // OpenAL source IDs private AudioSource[] channelSources; // jME source associated with each channel private int nextChannelIndex = 0; // Next available channel index - private final ArrayList freeChannels = new ArrayList<>(); // Pool of freed channels + private final ArrayDeque freeChannels = new ArrayDeque<>(); // Pool of freed channels // Listener and environment private Listener listener; @@ -793,7 +794,7 @@ private void applyListenerVolume(Listener listener) { private int newChannel() { if (!freeChannels.isEmpty()) { - return freeChannels.remove(0); + return freeChannels.removeFirst(); } else if (nextChannelIndex < channels.length) { return nextChannelIndex++; } else { @@ -906,7 +907,7 @@ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean lo for (int i = 0; i < processed; i++) { int buffer; - ib.position(0).limit(1); + ib.clear().limit(1); al.alSourceUnqueueBuffers(sourceId, 1, ib); buffer = ib.get(0); @@ -931,7 +932,7 @@ private boolean fillStreamingSource(int sourceId, AudioStream stream, boolean lo } if (filled) { - ib.position(0).limit(1); + ib.clear().limit(1); ib.put(0, buffer); al.alSourceQueueBuffers(sourceId, 1, ib); // At least one buffer enqueued = success. @@ -974,7 +975,7 @@ private void attachStreamToSource(int sourceId, AudioStream stream, boolean loop } if (filled) { - ib.position(0).limit(1); + ib.clear().limit(1); ib.put(id).flip(); al.alSourceQueueBuffers(sourceId, 1, ib); success = true; From a0d7b4916cd8804e05e06d9f87fe02b2f78eb7dd Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Wed, 14 May 2025 14:29:35 +0200 Subject: [PATCH 12/13] ALAudioRenderer.run(): set log Level to FINE --- .../main/java/com/jme3/audio/openal/ALAudioRenderer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index 8bd323cb10..f8fd5fb562 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -338,7 +338,7 @@ public void run() { long startTime = System.nanoTime(); if (Thread.interrupted()) { - logger.info("Audio decoder thread interrupted, exiting."); + logger.fine("Audio decoder thread interrupted, exiting."); break; } @@ -357,13 +357,13 @@ public void run() { try { Thread.sleep(1); } catch (InterruptedException ex) { - logger.info("Audio decoder thread interrupted during sleep, exiting."); + logger.fine("Audio decoder thread interrupted during sleep, exiting."); break mainloop; } } } } - logger.info("Audio decoder thread finished."); + logger.fine("Audio decoder thread finished."); } /** From d080f5411d5fdcb3ecca80c025a2ed33ea06ef69 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Thu, 15 May 2025 15:45:09 +0200 Subject: [PATCH 13/13] Refactor: Remove redundant checkAlError call for performance removes the repeated call to the `checkAlError` method. The repeated string creation and the associated overhead of the error checking can negatively impact performance, especially in a frequently executed game loop. By removing this redundant check, we aim to improve overall performance and reduce garbage collection pressure. --- .../jme3/audio/openal/ALAudioRenderer.java | 95 +------------------ 1 file changed, 5 insertions(+), 90 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java index f8fd5fb562..d86b07885c 100644 --- a/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java +++ b/jme3-core/src/main/java/com/jme3/audio/openal/ALAudioRenderer.java @@ -228,11 +228,9 @@ private void initEfx() { // 3. Configure effect type efx.alEffecti(reverbFx, EFX.AL_EFFECT_TYPE, EFX.AL_EFFECT_REVERB); - checkAlError("setting reverb effect type"); // 4. attach reverb effect to effect slot efx.alAuxiliaryEffectSloti(reverbFxSlot, EFX.AL_EFFECTSLOT_EFFECT, reverbFx); - checkAlError("attaching reverb effect to slot"); } else { logger.log(Level.WARNING, "OpenAL EFX not available! Audio effects won't work."); @@ -259,7 +257,6 @@ private void destroyOpenAL() { ib.put(channels); ib.flip(); al.alDeleteSources(channels.length, ib); - checkAlError("deleting sources"); // Delete audio buffers and filters managed by NativeObjectManager objManager.deleteAllObjects(this); @@ -270,7 +267,6 @@ private void destroyOpenAL() { ib.clear().limit(1); ib.put(0, reverbFx); efx.alDeleteEffects(1, ib); - checkAlError("deleting reverbFx effect " + reverbFx); reverbFx = -1; } @@ -278,7 +274,6 @@ private void destroyOpenAL() { ib.clear().limit(1); ib.put(0, reverbFxSlot); efx.alDeleteAuxiliaryEffectSlots(1, ib); - checkAlError("deleting effect reverbFxSlot " + reverbFxSlot); reverbFxSlot = -1; } } @@ -408,15 +403,8 @@ private void updateFilter(Filter f) { efx.alFilteri(id, EFX.AL_FILTER_TYPE, EFX.AL_FILTER_LOWPASS); efx.alFilterf(id, EFX.AL_LOWPASS_GAIN, lowPass.getVolume()); efx.alFilterf(id, EFX.AL_LOWPASS_GAINHF, lowPass.getHighFreqVolume()); - - if (checkAlError("updating filter " + id)) { - deleteFilter(f); // Try to clean up - } else { - f.clearUpdateNeeded(); // Mark as updated in AL - } - } - // ** Add other filter types (HighPass, BandPass) here if implemented ** - else { + f.clearUpdateNeeded(); + } else { throw new UnsupportedOperationException("Unsupported filter type: " + f.getClass().getName()); } } @@ -455,9 +443,6 @@ public float getSourcePlaybackTime(AudioSource src) { // Add byte offset from source (for both streams and buffers) int byteOffset = al.alGetSourcei(sourceId, AL_BYTE_OFFSET); - if (checkAlError("getting source byte offset for " + sourceId)) { - return 0; // Error getting offset - } playbackOffsetBytes += byteOffset; // Compute time value from bytes @@ -631,7 +616,6 @@ private void applySourceDryFilter(int sourceId, AudioSource src) { } // NOTE: must re-attach filter for changes to apply. al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, filterId); - checkAlError("setting source direct filter for " + sourceId); } } @@ -646,7 +630,6 @@ private void applySourceReverbFilter(int sourceId, AudioSource src) { filterId = f.getId(); } al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, reverbFxSlot, 0, filterId); - checkAlError("setting source reverb send for " + sourceId); } } @@ -657,7 +640,6 @@ private void applySourceLooping(int sourceId, AudioSource src, boolean forceNonL looping = false; } al.alSourcei(sourceId, AL_LOOPING, looping ? AL_TRUE : AL_FALSE); - checkAlError("setting source looping for " + sourceId); } /** Sets AL_SOURCE_RELATIVE and applies position/velocity/distance accordingly */ @@ -690,7 +672,6 @@ private void applySourcePositionalState(int sourceId, AudioSource src) { al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); } } - checkAlError("setting source positional state for " + sourceId); } /** Sets cone angles/gain based on whether the source is directional */ @@ -707,7 +688,6 @@ private void applySourceDirectionalState(int sourceId, AudioSource src) { al.alSourcef(sourceId, AL_CONE_OUTER_ANGLE, 360f); al.alSourcef(sourceId, AL_CONE_OUTER_GAIN, 1f); } - checkAlError("setting source directional state for " + sourceId); } /** @@ -766,7 +746,6 @@ private void setListenerParams(Listener listener) { private void applyListenerPosition(Listener listener) { Vector3f pos = listener.getLocation(); al.alListener3f(AL_POSITION, pos.x, pos.y, pos.z); - checkAlError("setting listener position"); } private void applyListenerRotation(Listener listener) { @@ -778,18 +757,15 @@ private void applyListenerRotation(Listener listener) { fb.put(up.x).put(up.y).put(up.z); fb.flip(); al.alListener(AL_ORIENTATION, fb); - checkAlError("setting listener orientation"); } private void applyListenerVelocity(Listener listener) { Vector3f vel = listener.getVelocity(); al.alListener3f(AL_VELOCITY, vel.x, vel.y, vel.z); - checkAlError("setting listener velocity"); } private void applyListenerVolume(Listener listener) { al.alListenerf(AL_GAIN, listener.getVolume()); - checkAlError("setting listener volume"); } private int newChannel() { @@ -836,14 +812,8 @@ public void setEnvironment(Environment env) { efx.alEffectf(reverbFx, EFX.AL_REVERB_AIR_ABSORPTION_GAINHF, env.getAirAbsorbGainHf()); efx.alEffectf(reverbFx, EFX.AL_REVERB_ROOM_ROLLOFF_FACTOR, env.getRoomRolloffFactor()); - if (checkAlError("setting reverb effect parameters")) { - return; - } - // (Re)attach the configured reverb effect to the slot efx.alAuxiliaryEffectSloti(reverbFxSlot, EFX.AL_EFFECTSLOT_EFFECT, reverbFx); - checkAlError("attaching reverb effect to slot"); - this.environment = env; } } @@ -884,9 +854,6 @@ private boolean fillBuffer(AudioStream stream, int bufferId) { int sampleRate = stream.getSampleRate(); al.alBufferData(bufferId, format, nativeBuf, totalBytesRead, sampleRate); - if (checkAlError("filling buffer " + bufferId + " for stream")) { - return false; - } return true; } @@ -1015,23 +982,19 @@ private void clearChannel(int index) { int sourceId = channels[index]; al.alSourceStop(sourceId); - checkAlError("stopping source " + sourceId + " on clearChannel"); // For streaming sources, this will clear all queued buffers. al.alSourcei(sourceId, AL_BUFFER, 0); - checkAlError("detaching buffer from source " + sourceId); if (supportEfx) { if (src.getDryFilter() != null) { // detach direct filter al.alSourcei(sourceId, EFX.AL_DIRECT_FILTER, EFX.AL_FILTER_NULL); - checkAlError("detaching direct filter from source " + sourceId); } if (src.isPositional() && src.isReverbEnabled()) { // Detach auxiliary send filter (reverb) al.alSource3i(sourceId, EFX.AL_AUXILIARY_SEND_FILTER, 0, 0, EFX.AL_FILTER_NULL); - checkAlError("detaching aux filter from source " + sourceId); } } @@ -1264,7 +1227,6 @@ public void updateInDecoderThread(float tpf) { // Start it again. logger.log(Level.WARNING, "Buffer starvation detected for stream on channel {0}. Restarting playback.", i); al.alSourcePlay(sourceId); - checkAlError("restarting starved source " + sourceId); } } @@ -1375,7 +1337,6 @@ public void playSourceInstance(AudioSource src) { // play the channel al.alSourcePlay(sourceId); - checkAlError("playing source instance " + sourceId); } } @@ -1430,9 +1391,7 @@ public void playSource(AudioSource src) { // play the channel int sourceId = channels[src.getChannel()]; al.alSourcePlay(sourceId); - if (!checkAlError("playing source " + sourceId)) { - src.setStatus(Status.Playing); // Update JME status on success - } + src.setStatus(Status.Playing); // Update JME status } } @@ -1459,9 +1418,7 @@ public void pauseSource(AudioSource src) { int sourceId = channels[src.getChannel()]; al.alSourcePause(sourceId); - if (!checkAlError("pausing source " + sourceId)) { - src.setStatus(Status.Paused); // Update JME status on success - } + src.setStatus(Status.Paused); // Update JME status } } } @@ -1532,7 +1489,6 @@ private int getOpenALFormat(AudioData audioData) { } } - // Format not supported throw new UnsupportedOperationException("Unsupported audio format: " + channels + " channels, " + bitsPerSample + " bits per sample."); } @@ -1546,7 +1502,6 @@ private void updateAudioBuffer(AudioBuffer ab) { if (ab.getId() == -1) { ib.clear().limit(1); al.alGenBuffers(1, ib); - checkAlError("generating bufferId"); id = ib.get(0); ab.setId(id); @@ -1561,9 +1516,7 @@ private void updateAudioBuffer(AudioBuffer ab) { int sampleRate = ab.getSampleRate(); al.alBufferData(id, format, data, data.capacity(), sampleRate); - if (!checkAlError("uploading buffer data for ID " + id)) { - ab.clearUpdateNeeded(); - } + ab.clearUpdateNeeded(); } /** @@ -1581,7 +1534,6 @@ private void updateAudioStream(AudioStream as) { ib.clear().limit(STREAMING_BUFFER_COUNT); al.alGenBuffers(STREAMING_BUFFER_COUNT, ib); - checkAlError("generating stream buffers ids"); ib.rewind(); ib.get(ids); @@ -1611,7 +1563,6 @@ public void deleteFilter(Filter filter) { ib.clear().limit(1); ib.put(id).flip(); efx.alDeleteFilters(1, ib); - checkAlError("deleting filter " + id); filter.resetObject(); } } @@ -1634,7 +1585,6 @@ public void deleteAudioData(AudioData audioData) { ib.put(0, id); ib.clear().limit(1); al.alDeleteBuffers(1, ib); - checkAlError("deleting buffer " + id); ab.resetObject(); // Mark as deleted on JME side } } else if (audioData instanceof AudioStream) { @@ -1644,45 +1594,10 @@ public void deleteAudioData(AudioData audioData) { ib.clear(); ib.put(ids).flip(); al.alDeleteBuffers(ids.length, ib); - checkAlError("deleting " + ids.length + " buffers"); as.resetObject(); // Mark as deleted on JME side } } } } - /** - * Checks for OpenAL errors and logs a warning if an error occurred. - * @param location A string describing where the check is occurring (for logging). - * @return True if an error occurred, false otherwise. - */ - private boolean checkAlError(String location) { - int error = al.alGetError(); - if (error != AL_NO_ERROR) { - String errorString; - switch (error) { - case AL_INVALID_NAME: - errorString = "AL_INVALID_NAME"; - break; - case AL_INVALID_ENUM: - errorString = "AL_INVALID_ENUM"; - break; - case AL_INVALID_VALUE: - errorString = "AL_INVALID_VALUE"; - break; - case AL_INVALID_OPERATION: - errorString = "AL_INVALID_OPERATION"; - break; - case AL_OUT_OF_MEMORY: - errorString = "AL_OUT_OF_MEMORY"; - break; - default: - errorString = "Unknown AL error code: " + error; - break; - } - logger.log(Level.WARNING, "OpenAL Error ({0}) at {1}", new Object[]{errorString, location}); - return true; - } - return false; - } }