// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.eavadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.
package net.avadeaux.klipspringer.alsa;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import javax.sound.sampled.*;
import net.avadeaux.klipspringer.codec.*;
/** Base class that has static members for accessing ALSA devices, as well as being a common base
* class for {@link Alsa.Player} and {@link Alsa.Tuner} classes used for sending to or receiving
* from specific devices. {@link Alsa.Player#devices} and {@link Alsa.Tuner#devices} can be used
* for finding out what devices are available.
*/
public abstract class Alsa implements Closeable {
static {
Library.init();
if (Library.alsaSupported()) { alsaInit(); }
}
private enum Status { ACTIVE, PAUSED, CLOSED }
final long pcm;
final int hwBufferFrames;
final int recoveryAttempts;
private final PcmFormat format;
private final Device.BitLayout lout;
private Status status = Status.ACTIVE;
private boolean pausable;
Alsa(Device device, boolean input, PcmFormat format, double bufferSecs, int recoveryAttempts) throws IOException {
if (!Library.alsaSupported()) { throw new UnsatisfiedLinkError("ALSA native library not available"); }
AlsaDevice adev = (AlsaDevice) device;
byte[] bdname = adev.name.getBytes(StandardCharsets.UTF_8);
bdname = Arrays.copyOf(bdname, bdname.length+1); // zero-terminate
pcm = open(bdname, input, (int) (bufferSecs*1000000), format.signed(), format.rate(), adev.scaledBips(format.bips()), format.channels(), format.fs(), format.bigend());
hwBufferFrames = bufferFrames(pcm);
this.recoveryAttempts = recoveryAttempts;
this.format = format;
lout = device.layout(format);
pausable = adev.pausable();
}
/** Gets the audio format of {@link Player} or {@link Tuner}. */
public final PcmFormat format() { return format; }
/** Gets the bit layout of {@link Player}. */
public final Device.BitLayout layout() { return lout; }
boolean isOpen() { return status != Status.CLOSED; }
/** Waits for {@link Player} to send on any data it has buffered up and closes it. */
public synchronized void drain() throws IOException {
drain(pcm);
close();
}
/** Checks if {@link Player} supports pausing. */
public boolean canPause() { return pausable; }
/** Pauses {@link Player} (if it {@link #canPause()}). */
public synchronized void pause() throws IOException {
if (status == Status.ACTIVE) {
pause(pcm, 0);
status = Status.PAUSED;
}
}
/** Takes a paused {@link Player} into playing mode. */
public synchronized void unpause() throws IOException {
if (status == Status.PAUSED) {
pause(pcm, 0);
status = Status.ACTIVE;
}
}
/** See {@link Closeable#close()}. */
public synchronized void close() {
if (status != Status.CLOSED) {
status = Status.CLOSED;
close(pcm);
}
}
/** Class for sending PCM data to an ALSA device. */
public static class Player extends Alsa implements Device.Player {
/** Map from available output device names to the device objects. */
public final static Map<String, Device> devices = AlsaDevice.collectDevices(false);
/** Creates a player to output PCM to the given device in the given format, specifying the
* size of the external buffer, and the number of recovery attempts in case of buffer
* underrun. Available devices are obtained through {@link #devices}. A reasonable number
* of recovery attempts is 2, unless you want to set it to 0 and get an exception as soon
* as an underrun happens.
*/
public Player(Device device, PcmFormat format, double bufferSecs, int recoveryAttempts) throws IOException {
super(device, false, format, bufferSecs, recoveryAttempts);
}
public synchronized boolean write(ByteBuffer data) throws IOException {
if (!isOpen()) { return false; }
if (!data.isDirect()) { return ByteTransfer.writeDirect(data, this, format().fs()); }
int p = data.position();
int nf = data.remaining()/format().fs();
int wf = write(pcm, data, p, nf, recoveryAttempts);
if (wf == nf) {
data.position(data.limit());
return true;
} else {
data.position(p + wf*format().fs());
return false;
}
}
public int bufferedFrames() throws IOException { return hwBufferFrames - availableFrames(pcm, recoveryAttempts); }
}
/** Class for receiving PCM data from an ALSA device. */
public static class Tuner extends Alsa implements Device.Tuner {
/** Map from available input device names to the device objects. */
public final static Map<String, Device> devices = AlsaDevice.collectDevices(true);
/** Creates a tuner to input PCM from the given device in the given format, specifying the
* size of the external buffer, and the number of recovery attempts in case of buffer
* overrun. Available devices are obtained through {@link #devices}. A reasonable number of
* recovery attempts is 2 if you want to continue receiving after a stutter in the input,
* or 0 if you want to get an exception as soon as an overrun happens.
*/
public Tuner(Device device, PcmFormat format, double bufferSecs, int recoveryAttempts) throws IOException {
super(device, true, format, bufferSecs, recoveryAttempts);
}
// Waits for a reasonable amount of frames to be available, given the number of frames that
// would fill the buffer. Returns the recommended number of frames to read, or zero if
// closed before the awaited number of frames was reached.
private int awaitAvailable(int bufFrames) throws IOException {
if (!isOpen()) { return 0; }
int min = Math.min(bufFrames, hwBufferFrames/10);
while (true) {
int avail = availableFrames(pcm, recoveryAttempts);
if (avail >= min) { return Math.min(avail, bufFrames); }
try { wait(10 + 1000L*(min - avail)/format().rate()); } catch (InterruptedException e) { }
if (!isOpen()) { return 0; }
}
}
public synchronized boolean read(ByteBuffer dest) throws IOException {
if (!isOpen()) { return false; }
if (!dest.isDirect()) { return ByteTransfer.readDirect(dest, this, format().fs()); }
while (dest.hasRemaining()) {
int nf = awaitAvailable(dest.remaining()/format().fs());
if (nf == 0) { return false; }
int p = dest.position();
int rf = read(pcm, dest, p, nf, recoveryAttempts);
dest.position(p + rf*format().fs());
if (rf < nf) { return false; }
}
return true;
}
}
// Native methods.
private native static void alsaInit();
private native long open(byte[] fnam, boolean input, int bufferMicros, boolean signed, int rate, int bips, int channels, int fs, boolean bigend) throws IOException;
private native void close(long pcm);
native int write(long pcm, ByteBuffer direct, int off, int frames, int recoveryAttempts) throws IOException;
native int read(long pcm, ByteBuffer direct, int off, int frames, int recoveryAttempts) throws IOException;
private native void pause(long pcm, int enable) throws IOException;
private native void drain(long pcm) throws IOException;
native int availableFrames(long pcm, int recoveryAttempts) throws IOException;
private native int bufferFrames(long pcm) throws IOException;
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home