// 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