Class Sounds
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 Summary
Modifier and TypeMethodDescriptionstatic voidDrops every shared load, so the nextloadShared(java.nio.file.Path)orfromResourceShared(java.lang.String)of a source re-reads it (e.g.static AudioClipdecode(byte[] fileBytes) Decodes an encoded audio file from memory, on the calling thread, in whatever formats the installedAudioDecoderaccepts.decodeAsync(byte[] fileBytes) Decodes in-memory bytes on theUiworker pool and hands the clip toonSuccesson the UI thread; failures arrive atonFailurerather than being thrown here.static AudioClipfromResource(String resource) Reads a classpath resource whole and decodes it, on the calling thread; seeload(Path)for the cost, andfromResourceShared(java.lang.String)for the same work on the worker pool.static CompletableFuture<AudioClip> fromResourceShared(String resource) Reads and decodes a classpath resource on theUiworker pool; the returned future is already running and completes on the UI thread.static voidinstallDecoder(AudioDecoder newDecoder) Installs the backend audio decoder (called once at backend startup).static voidinstallEngine(AudioEngine newEngine) Installs the backend audio engine (called once at backend startup).static booleanWhether an engine is installed and an audio device is available.static AudioClipReadsfilewhole 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.static CompletableFuture<AudioClip> loadShared(Path file) Reads and decodesfileon theUiworker 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.static PlaybackPlayscliponce 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 whatplay(AudioClip, float, boolean)warns about.static PlaybackPlayscliponce atgainin [0..1] (no-op if audio is unavailable).static PlaybackPlaysclipatgainin [0..1], optionally looping.static Playbackplay(AudioClip clip, PlayOptions options) Playsclipwith fullPlayOptions: pitch, pan or 3D position, mixer bus and steal priority.static voidsetBusGain(AudioBus bus, float gain) Setsbus's volume in [0..1], applied live to its playbacks; seesetMasterGain(float).static voidsetListener(Vec3 position, Vec3 forward, Vec3 up) Positions the 3D listener; seeAudioEngine.setListener(limn.math.Vec3, limn.math.Vec3, limn.math.Vec3).static voidsetMasterGain(float gain) Sets the global volume in [0..1], applied live to everything sounding.static Playbackstream(Path file, PlayOptions options) Streams the audio file atfile, in whatever formats the installedAudioDecodercan 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.static Playbackstream(AudioStreamSource source, PlayOptions options) Streams PCM frames pulled fromsource, 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.streamAsync(Path file, PlayOptions options) Opensfileand starts streaming it on theUiworker pool, handing thePlaybackhandle toonSuccesson the UI thread: the asynchronous form ofstream(Path, PlayOptions), and what an application should use to start music while a scene is appearing.static voiduninstallDecoder(AudioDecoder candidate) Uninstallscandidateif it is the installed decoder (backend shutdown).static voiduninstallEngine(AudioEngine candidate) Uninstallscandidateif it is the installed engine (backend shutdown).Opens the audio device on theUiworker pool so that the firstplay(limn.sound.AudioClip)does not pay for it, and hands whateverisAvailable()answers from then on toonSuccesson the UI thread.
-
Method Details
-
installEngine
Installs the backend audio engine (called once at backend startup). -
uninstallEngine
Uninstallscandidateif it is the installed engine (backend shutdown). -
installDecoder
Installs the backend audio decoder (called once at backend startup). -
uninstallDecoder
Uninstallscandidateif 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
Opens the audio device on theUiworker pool so that the firstplay(limn.sound.AudioClip)does not pay for it, and hands whateverisAvailable()answers from then on toonSuccesson 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
Asyncis an unstarted description, and one rule that holds across all of them is worth more than thestart()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)andfromResourceShared(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
falserather than failing, matching the best-effort silenceplay(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(), andonSuccessthen receives whether something played would be heard - Throws:
IllegalStateException- if no backend is running (there is no worker pool to use)
-
decode
Decodes an encoded audio file from memory, on the calling thread, in whatever formats the installedAudioDecoderaccepts. 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
Readsfilewhole 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
Reads a classpath resource whole and decodes it, on the calling thread; seeload(Path)for the cost, andfromResourceShared(java.lang.String)for the same work on the worker pool. -
decodeAsync
Decodes in-memory bytes on theUiworker pool and hands the clip toonSuccesson the UI thread; failures arrive atonFailurerather 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 whereloadShared(java.nio.file.Path)cannot be.- Throws:
IllegalStateException- if no backend is running
-
play
Playscliponce 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 whatplay(AudioClip, float, boolean)warns about. -
play
Playscliponce atgainin [0..1] (no-op if audio is unavailable). -
play
Playsclipatgainin [0..1], optionally looping. Returns aPlaybackhandle (orPlayback.NONEwhen 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
Playsclipwith fullPlayOptions: 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 onplay(AudioClip, float, boolean). -
stream
Streams the audio file atfile, in whatever formats the installedAudioDecodercan 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 isDEFAULTS.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
Opensfileand starts streaming it on theUiworker pool, handing thePlaybackhandle toonSuccesson the UI thread: the asynchronous form ofstream(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/deliverIfand callstart(). 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.NONEor fails; neither leaves a file open.- Parameters:
file- the track, opened on the worker pooloptions- gain, bus, priority and looping, read once at admission- Returns:
- the unstarted work; nothing is opened until
start() - Throws:
NullPointerException- if either argument is nullIllegalStateException- if no backend is running (there is no worker pool to use)
-
stream
Streams PCM frames pulled fromsource, 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 noAudioDecoder: the caller has already done the decoding this facade would otherwise arrange.This call takes ownership of
sourceand 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 returnsPlayback.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, whoseAudioStreamSource.channels()andAudioStreamSource.sampleRate()are read once at admissionoptions- gain, bus, priority and whether the engine rewinds at the end of data viaAudioStreamSource.reset()- Returns:
- a handle to the started stream, or
Playback.NONEwhen 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
Setsbus's volume in [0..1], applied live to its playbacks; seesetMasterGain(float). -
setListener
Positions the 3D listener; seeAudioEngine.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; seeisAvailable()for what that costs.
-