Package limn.sound

Class Sounds

java.lang.Object
limn.sound.Sounds

public final class Sounds extends Object
Audio facade, backed by the running backend's AudioEngine and AudioDecoder (both installed at startup, mirroring Images). Load or synthesize an AudioClip, then play(limn.sound.AudioClip) it.

Playback is best-effort: with no backend or no audio device, play(limn.sound.AudioClip) is a silent no-op returning Playback.NONE; feedback sounds never become a hard dependency. Decoding, by contrast, requires a backend (there is no fallback CPU decoder) and throws if none is installed.


 AudioClip chime = Sounds.fromResource("/app/sounds/chime.ogg"); // or a .wav
 Sounds.play(chime);
 // ...or synthesize without shipping an asset:
 Sounds.play(AudioClip.tone(880, 0.12f, 0.5f));
 
  • Method Details

    • installEngine

      public static void installEngine(AudioEngine newEngine)
      Installs the backend audio engine (called once at backend startup).
    • uninstallEngine

      public static void uninstallEngine(AudioEngine candidate)
      Uninstalls candidate if it is the installed engine (backend shutdown).
    • installDecoder

      public static void installDecoder(AudioDecoder newDecoder)
      Installs the backend audio decoder (called once at backend startup).
    • uninstallDecoder

      public static void uninstallDecoder(AudioDecoder candidate)
      Uninstalls candidate if it is the installed decoder (backend shutdown).
    • isAvailable

      public static boolean isAvailable()
      Whether an engine is installed and an audio device is available.

      The first call may open the audio device: an engine is allowed to defer loading the platform audio library and waking the default output until something asks, and that costs tens to hundreds of milliseconds, longer when the output is asleep or on a Bluetooth link. It blocks the calling thread for that time, so on the UI thread it is a visible freeze. warmUpAsync() asks the same question on the worker pool; every call once the device is open is effectively a field read.

      Returns:
      whether something played now would be heard
    • warmUpAsync

      public static Work<Boolean> warmUpAsync()
      Opens the audio device on the Ui worker pool so that the first play(limn.sound.AudioClip) does not pay for it, and hands whatever isAvailable() answers from then on to onSuccess on the UI thread. Started once during startup, well before the first feedback sound:
      
       Sounds.warmUpAsync().onSuccess(audible -> soundToggle.setEnabled(audible)).start();
       

      Returned unstarted, and dropping it warms nothing at all. It is a description rather than a job already running even though nothing here waits for its answer, because every form in this toolkit whose name ends in Async is an unstarted description, and one rule that holds across all of them is worth more than the start() it would save: a facade that started itself would be the exception a reader has to remember, at the one call site where forgetting is silent. The de-duplicated loaders are the other family and say so in their names: loadShared(java.nio.file.Path) and fromResourceShared(java.lang.String) are running before the caller sees them.

      Idempotent and cheap after the first time (the engine opens the device once), and it never fails: a machine with no audio device, and a process with no engine installed, both deliver false rather than failing, matching the best-effort silence play(limn.sound.AudioClip) gives. It reports no progress: waking a device has no fraction anyone can compute. Nothing it produces holds a resource, so it carries no disposer and needs none.

      Returns:
      the unstarted work; the device is not opened until start(), and onSuccess then receives whether something played would be heard
      Throws:
      IllegalStateException - if no backend is running (there is no worker pool to use)
    • decode

      public static AudioClip decode(byte[] fileBytes)
      Decodes an encoded audio file from memory, on the calling thread, in whatever formats the installed AudioDecoder accepts. Decoding a whole clip walks every sample, so a long track is long enough to drop frames on the UI thread; decodeAsync(byte[]) is the same work on the worker pool.
    • load

      public static AudioClip load(Path file)
      Reads file whole and decodes it, on the calling thread: one file read plus a full decode, which on the UI thread is a freeze for as long as both take. loadShared(java.nio.file.Path) is the same work on the worker pool.
    • fromResource

      public static AudioClip fromResource(String resource)
      Reads a classpath resource whole and decodes it, on the calling thread; see load(Path) for the cost, and fromResourceShared(java.lang.String) for the same work on the worker pool.
    • loadShared

      public static CompletableFuture<AudioClip> loadShared(Path file)
      Reads and decodes file on the Ui worker pool, a bounded pool shared with every other background task, so a stage chained onto the result may block without starving the UI, but a long one delays other loads. The returned future is already running and completes on the UI thread, where thenAccept may touch widgets directly: loadShared(file).thenAccept(Sounds::play).

      Shared, which is why this is not a Work and not called loadAsync. Deduplicated by absolute path: concurrent and later calls for the same file return the same future and decode once, and every caller shares one immutable AudioClip; a result two callers are waiting for is not one either of them may cancel, so there is nothing to start and nothing to withdraw. A load that fails is dropped from that cache, so a later call retries it. clearSharedCache() drops the successful ones too.

      Failures do not throw here; they arrive at exceptionally/whenComplete on the UI thread. Requires a running backend, like the synchronous form.

    • fromResourceShared

      public static CompletableFuture<AudioClip> fromResourceShared(String resource)
      Reads and decodes a classpath resource on the Ui worker pool; the returned future is already running and completes on the UI thread. Deduplicated by resource name, retried after a failure and reporting failures through the future exactly as loadShared(Path) does.
    • decodeAsync

      public static Work<AudioClip> decodeAsync(byte[] fileBytes)
      Decodes in-memory bytes on the Ui worker pool and hands the clip to onSuccess on the UI thread; failures arrive at onFailure rather than being thrown here.

      Returned unstarted: attach the handlers, then start(). Uncached (the caller owns the bytes, and two arrays with equal contents are two decodes), and being unshared is what lets this one be a cancellable job where loadShared(java.nio.file.Path) cannot be.

      Throws:
      IllegalStateException - if no backend is running
    • clearSharedCache

      public static void clearSharedCache()
      Drops every shared load, so the next loadShared(java.nio.file.Path) or fromResourceShared(java.lang.String) of a source re-reads it (e.g. after files changed on disk). Already-delivered clips are unaffected.
    • play

      public static Playback play(AudioClip clip)
      Plays clip once at full volume (no-op if audio is unavailable): the feedback-sound call, and the one most likely to be the process's first, which is what play(AudioClip, float, boolean) warns about.
    • play

      public static Playback play(AudioClip clip, float gain)
      Plays clip once at gain in [0..1] (no-op if audio is unavailable).
    • play

      public static Playback play(AudioClip clip, float gain, boolean loop)
      Plays clip at gain in [0..1], optionally looping. Returns a Playback handle (or Playback.NONE when no audio device is available). Safe to call from any thread.

      No asynchronous form, because the work here is a handful of device calls on an already decoded clip and it has to return the handle the caller stops. The first call in the process is the exception: it may be the one that opens the audio device, which costs tens to hundreds of milliseconds and blocks whichever thread makes it, so the first feedback click an application plays is a freeze unless warmUpAsync() has already paid for it during startup.

    • play

      public static Playback play(AudioClip clip, PlayOptions options)
      Plays clip with full PlayOptions: pitch, pan or 3D position, mixer bus and steal priority. Safe to call from any thread, and the first call in the process carries the device-open cost described on play(AudioClip, float, boolean).
    • stream

      public static Playback stream(Path file, PlayOptions options)
      Streams the audio file at file, in whatever formats the installed AudioDecoder can stream: decoded incrementally on the engine's streaming thread, so a long music track costs ring buffers instead of a whole decoded clip on the heap. The conventional music setup is DEFAULTS.withBus(AudioBus.MUSIC).withPriority(HIGH).withLoop(true).

      Getting a stream started is not incremental, even though playing it is. This call opens the file, and a decoder is allowed to read it whole to be able to seek in it and to decode a first frame to learn the format; the engine then decodes the first buffers before the sound starts. On a several-megabyte track that is a read of every byte plus a decode, all on the calling thread, which for the "start the music as the scene appears" call is the UI thread. Use streamAsync(java.nio.file.Path, limn.sound.PlayOptions) there; this form suits a caller already on a worker thread.

    • streamAsync

      public static Work<Playback> streamAsync(Path file, PlayOptions options)
      Opens file and starts streaming it on the Ui worker pool, handing the Playback handle to onSuccess on the UI thread: the asynchronous form of stream(Path, PlayOptions), and what an application should use to start music while a scene is appearing.
      
       music = Sounds.streamAsync(track, PlayOptions.DEFAULTS.withBus(AudioBus.MUSIC).withLoop(true))
                     .onSuccess(playback -> this.playback = playback)
                     .deliverIf(view::isShowing)
                     .start();
       

      Returned unstarted and already carrying a disposer, so a caller cannot leak by forgetting one: register onSuccess/onFailure/deliverIf and call start(). Cancelling the job, or refusing the delivery, stops the stream and closes the file; a cancel that arrives before the engine has admitted the track means nothing ever sounds, and one that arrives after it means a fraction of a second does. Replacing the disposer with one of your own removes that guarantee.

      Reports no progress: neither opening the file nor priming the device has a fraction anyone can compute, so a registered progress handler would never be called.

      Everything expensive happens in the body (waking the audio device, the file read, the priming decode), and the handle is produced there too, so it is already playing by the time it is delivered. With no engine, no audio device, or a file the decoder will not stream, the body completes with Playback.NONE or fails; neither leaves a file open.

      Parameters:
      file - the track, opened on the worker pool
      options - gain, bus, priority and looping, read once at admission
      Returns:
      the unstarted work; nothing is opened until start()
      Throws:
      NullPointerException - if either argument is null
      IllegalStateException - if no backend is running (there is no worker pool to use)
    • stream

      public static Playback stream(AudioStreamSource source, PlayOptions options)
      Streams PCM frames pulled from source, for audio that is already open and has no file of its own, such as the audio track of a container something else is demultiplexing. Requires no AudioDecoder: the caller has already done the decoding this facade would otherwise arrange.

      This call takes ownership of source and closes it, on every path without exception. The caller must not close it afterwards and must not hand it to anything else: a source closed twice is a decoder torn down under a streaming thread still reading it. That holds when playback ends, when it is stopped, and equally when nothing ever sounds: no engine installed, no audio device, a channel count that is neither mono nor stereo, a full admission queue, or a source that yields no frames at all. Every one of those returns Playback.NONE, and in every one of them the source has been closed before this returns.

      Which thread does the closing is not the caller's to assume: it is this thread when the stream never starts, and the engine's streaming thread once it has. Implementations of AudioStreamSource.close() are documented idempotent and must tolerate either.

      The engine's streaming thread pulls frames from here on, so the source must not be touched by the caller after this call. Safe to call from any thread.

      No asynchronous form of its own, deliberately: whoever holds an open source opened it somewhere, and that somewhere is where the background work belongs (streamAsync(java.nio.file.Path, limn.sound.PlayOptions) for a file, or the caller's own worker for a source demultiplexed out of something else). It is not free, though: the engine primes several device buffers before returning, which is a decode of the first fraction of a second on this thread. Calling it on the UI thread with a source that was opened elsewhere is the one shape that still stalls a frame.

      Parameters:
      source - the open source, whose AudioStreamSource.channels() and AudioStreamSource.sampleRate() are read once at admission
      options - gain, bus, priority and whether the engine rewinds at the end of data via AudioStreamSource.reset()
      Returns:
      a handle to the started stream, or Playback.NONE when nothing sounds
      Throws:
      NullPointerException - if either argument is null, in which case nothing is closed because nothing was accepted
    • setMasterGain

      public static void setMasterGain(float gain)
      Sets the global volume in [0..1], applied live to everything sounding. No asynchronous form: it re-applies a gain to the voices currently playing, which is bounded by the voice count and reads nothing, and a volume slider that took effect a frame later would feel broken.
    • setBusGain

      public static void setBusGain(AudioBus bus, float gain)
      Sets bus's volume in [0..1], applied live to its playbacks; see setMasterGain(float).
    • setListener

      public static void setListener(Vec3 position, Vec3 forward, Vec3 up)
      Positions the 3D listener; see AudioEngine.setListener(limn.math.Vec3, limn.math.Vec3, limn.math.Vec3). No asynchronous form: it is a couple of device calls, and it is typically driven per frame from a camera, where a deferred one would arrive behind the picture it belongs to. It can nonetheless be the first call that opens the audio device; see isAvailable() for what that costs.