ALAudioRenderer: code improvements

Hi everyone,

I’ve recently been working on enhancing the sound system within the demo I am currently developing. I’ve found that the current jME API appears to lack support for implementing custom AudioRenderer solutions beyond the existing lwjgl and joal implementations. This presents a significant limitation. Furthermore, extending the functionality of the current AudioRenderer seems to require modifications to the engine’s core codebase, which is challenging given the complexity of the existing source.

JmeDesktopSystem.newAudioRenderer()

// API doesn't really allow for writing custom AudioRenderer 
// implementations using AppSettings.

    @Override
    public AudioRenderer newAudioRenderer(AppSettings settings) {
        initialize(settings);

        AL al;
        ALC alc;
        EFX efx;
        if (settings.getAudioRenderer().startsWith("LWJGL")) {
            al = newObject("com.jme3.audio.lwjgl.LwjglAL");
            alc = newObject("com.jme3.audio.lwjgl.LwjglALC");
            efx = newObject("com.jme3.audio.lwjgl.LwjglEFX");
        } else if (settings.getAudioRenderer().startsWith("JOAL")) {
            al = newObject("com.jme3.audio.joal.JoalAL");
            alc = newObject("com.jme3.audio.joal.JoalALC");
            efx = newObject("com.jme3.audio.joal.JoalEFX");
        } else {
            throw new UnsupportedOperationException(
                    "Unrecognizable audio renderer specified: "
                    + settings.getAudioRenderer());
        }

        if (al == null || alc == null || efx == null) {
            return null;
        }

        return new ALAudioRenderer(al, alc, efx);
    }

LegacyApplication.initAudio()

// why put a Listener object in the LegacyApplication class?
// why does the Listener class have such a generic, non-specialized name?
// why not AudioListener?

    private void initAudio() {
        if (settings.getAudioRenderer() != null && context.getType() != Type.Headless) {
            audioRenderer = JmeSystem.newAudioRenderer(settings);
            audioRenderer.initialize();
            AudioContext.setAudioRenderer(audioRenderer);

            listener = new Listener();
            audioRenderer.setListener(listener);
        }
    }

It also became apparent that many capabilities of the OpenAL library, such as Effects and Sound Filters, have not been fully utilized in the current jme integration.

ALAudioRenderer.setEnvironment()

// ALAudioRenderer only supports ReverbEffect (alias Environment)

    @Override
    public void setEnvironment(Environment env) {
        checkDead();
        synchronized (threadLock) {
            if (audioDisabled || !supportEfx) {
                return;
            }

            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());
            efx.alEffectf(reverbFx, EFX.AL_REVERB_GAINHF, env.getGainHf());
            efx.alEffectf(reverbFx, EFX.AL_REVERB_DECAY_TIME, env.getDecayTime());
            efx.alEffectf(reverbFx, EFX.AL_REVERB_DECAY_HFRATIO, env.getDecayHFRatio());
            efx.alEffectf(reverbFx, EFX.AL_REVERB_REFLECTIONS_GAIN, env.getReflectGain());
            efx.alEffectf(reverbFx, EFX.AL_REVERB_REFLECTIONS_DELAY, env.getReflectDelay());
            efx.alEffectf(reverbFx, EFX.AL_REVERB_LATE_REVERB_GAIN, env.getLateReverbGain());
            efx.alEffectf(reverbFx, EFX.AL_REVERB_LATE_REVERB_DELAY, env.getLateReverbDelay());
            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
            efx.alAuxiliaryEffectSloti(reverbFxSlot, EFX.AL_EFFECTSLOT_EFFECT, reverbFx);
        }
    }

ALAudioRenderer.updateFilter()

// ALAudioRenderer only supports LowPassFilter

    private void updateFilter(Filter f) {
        int id = f.getId();
        if (id == -1) {
            ib.position(0).limit(1);
            efx.alGenFilters(1, ib);
            id = ib.get(0);
            f.setId(id);

            objManager.registerObject(f);
        }

        if (f instanceof LowPassFilter) {
            LowPassFilter lpf = (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());
        }

        f.clearUpdateNeeded();
    }

I temporarily paused development on the demo to thoroughly review the OpenAL documentation and identify untapped potential.

Based on my findings, I discovered a method to bypass the standard AudioRenderer initialization process and integrate my own custom SoundManager, offering a significantly simpler and more modular API. Attached is a screenshot showcasing the functionality I have currently implemented:

    public static void main(String[] args) {
        AppSettings settings = new AppSettings(true);
        settings.setResolution(640, 480);
        settings.setAudioRenderer(null); // disable jME AudioRenderer

        Test_SoundManager app = new Test_SoundManager();
        app.setSettings(settings);
        app.setShowSettings(false);
        app.setPauseOnLostFocus(false);
        app.start();
    }

At this stage, I am still working on integrating multichannel audio management, audio streams, and a dedicated WAV loader. However, I have successfully implemented:

  • SoundManager
  • SoundSource
  • SoundBuffer
  • SoundListener
  • AudioEffects: Chorus, Compressor, Distortion, Echo, Flanger, PitchShift, Reverb
  • AudioFilters: LowPassFilter & HighPassFilter
  • the capability to modify the distance attenuation model equation.
  • OGGLoader

I am eager to explore the extent to which I can improve the audio system and integrate advanced features, drawing inspiration from concepts found in engines like Unity, specifically the AudioMixer and AudioMixerGroup.

Edit:
Maybe something to think about for jme4

In the meantime, here is the new look of my editor for the current jme AudioNodes:

12 Likes

I agree the name is bad.

But this is not a classic Java XxxListener. This is “the listener”. The “ears of the camera”.

2 Likes

How about PositionalAudio or SoundEmitter?
I agree that changing the name of an already established class is not a good ideia, but I also agree that Listener is not a good name due to the listener project pattern. Anyway, just giving my two cents.

1 Like

Ear.

It’s not a PostionalAudio. It’s a LISTENER. It’s hearing the sound. PositionalAudio would be MAKING the sound that the “listener” is “hearing”. So it’s not a SoundEmitter, either.

It’s the “ear”… the “listener” (but “listener” is an overloaded word as already discussed).

“AudioReceiver” is probably the most accurate descriptive term… but even that I think could be confusing. AudioReceiverPosition would also work but might confuse people that “position” extends to include orientation and velocity (which in some circles is normal).

It’s always fun when a perfectly good word for a thing doesn’t work because that word is already heavily used for something else… because really, if there were no such things as the “observer pattern”, the word “Listener” would be crystal clear here. :slight_smile:

1 Like

The audiosystem is one of the things i underestimated the complextiy by far. After reading the specs and prototyping my audio pipeline looks quite the same as the graphics pipeline. Besides the naming that is of course, but you have different “rendertargerts” with postprocessing. different per object rendering informations. very very similiar.

Fun fact, i was playing with other engines in my exploration time. And while it is super easy in all of them to setup a ingame cctv camera and an ingame monitor, it is super hard to get audio support for the camera/monitor setup. (in the end it is just allowing multiple listeners, and transforming coordinates taking into account player->monitor and camera->audiosource coordinates)

Hy everyone,
Here are some updates on studies done so far:

Why I Built a wav2ogg Converter Instead of a WAVLoader

Handling audio in applications, especially games, can be surprisingly tricky. When working with Java, LWJGL, and OpenAL, a common requirement is loading sound files. WAV seems like a straightforward choice, right? It’s uncompressed and widely supported. But as I delved deeper into building a robust audio loading system, I hit some roadblocks that led me to a different conclusion and a pivot in my approach.

My initial thought was to build a dedicated WAVLoader. However, after wrestling with the nuances of the WAV format and the tools available in Java, the path became clear: converting WAV files to OGG Vorbis and using LWJGL’s stb_vorbis bindings is often a far more practical and efficient solution.

Here’s why this shift makes sense:

  1. File Size: WAV files are uncompressed PCM data, meaning they are significantly larger. OGG Vorbis, on the other hand, uses lossy compression, resulting in much smaller files. This is crucial for reducing disk space and improving loading times, particularly in projects with many sound assets.
  2. Loading Simplicity: LWJGL provides excellent, relatively simple bindings for the native stb_vorbis library. Loading an OGG file into memory (typically using stb_vorbis_decode_memory) is quite direct. You get the decoded PCM data ready for OpenAL without needing to manually parse complex headers or navigate the quirks of javax.sound.sampled.
  3. Reliability: stb_vorbis is a mature, well-tested decoder for the OGG format. Relying on it lets you bypass all the potential headaches related to the variations, internal codecs, and header issues within the WAV container format that can lead to dreaded UnsupportedAudioFileException. If the OGG file is valid, stb_vorbis will decode it.
  4. Standard Format: OGG Vorbis is an open, well-defined standard, widely supported across platforms and libraries.
  5. OpenAL Compatibility: Regardless of whether you start with WAV or OGG, the data loaded into an OpenAL buffer is uncompressed PCM. From OpenAL’s perspective, the original source format doesn’t matter once the data is in the buffer.

Of course, the OGG approach isn’t without its downsides:

  • Lossy Compression: Being a lossy format, there is a small, theoretically quality loss compared to the original uncompressed WAV. For most game sound effects, this is practically imperceptible, but it might be a consideration for extremely high-fidelity audio needs.
  • Preprocessing Step: It requires converting your WAV files to OGG before running your application. This adds an extra step to your asset pipeline or workflow.

The Conclusion and the Pivot:

After weighing these points, the advantages of using OGG Vorbis with stb_vorbis for loading audio in LWJGL/OpenAL scenarios significantly outweigh the complexities and potential instability of trying to build a universally robust WAV parser in Java.

What are your thoughts on handling audio formats in your projects?

4 Likes

I use ogg for anything that is particular long or I want “absolutely no control over”… and wav for anything I want to be able to index into.

If the seeking behavior of an ogg solution works well then that mitigates 99% of the problem I have with .ogg files. I want to be able to start playing at a specific time and in my experience, .ogg makes that difficult. (Often times you can go forward but not backward or must restart the stream, etc..)

This could partially be related to JME’s current implementation but when I looked into it, I couldn’t find a way to hack in the support I wanted, either.

2 Likes

Hi guys,
I am working on a PR for JME’s ALAudioRenderer to improve its readability and maintainability by adding logs, comments, and error checking for the most relevant OpenAL operations. This will make it easier to understand what is going on under the hood in case of problems.

The current implementation has no logs or comments, making it impossible for a newbie to understand what is going on without reading the OpenAL documentation (which I’m doing).

This PR also fixes a bug where the Environment and Listener would be lost on restart of the AudioRenderer context.

I’ll notify you when it’s ready for review. Initial testing hasn’t revealed any obvious issues, and I plan to explore adding more targeted tests to the jme3-examples module.

I will try to document the API with a UML class diagram to get the big picture. Furthermore, this work opens the possibility of incorporating additional OpenAL filters—HighPassFilter and BandPassFilter—beyond the existing LowPassFilter. This is due to the com.jme3.audio.Filter interface where I recently fixed the cloning bug.

Let me know what you think and if you are open to this kind of PR.

Edit:
** NEW ** UML Class Diagram

9 Likes

Thanks for working on this long-neglected area of JME.

3 Likes