// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.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;

import java.util.*;
import java.io.IOException;
import java.io.PrintStream;
import javax.sound.sampled.*;
import net.avadeaux.klipspringer.alsa.*;
import net.avadeaux.klipspringer.codec.*;
import static java.nio.ByteOrder.*;
import static javax.sound.sampled.AudioFormat.Encoding.*;

/** Utility for listing audio devices. */
public class DeviceList {
    private static class MixerDescription implements Device {
        private static class FormatList extends LinkedList<AudioFormat> { }
        private static class ChanMap extends TreeMap<Integer, FormatList> { }
        private static class RateMap extends TreeMap<Float, ChanMap> { }
        private static class DepthMap extends TreeMap<Integer, RateMap> { }

        public final Mixer.Info mixerInfo;
        private final DepthMap depths = new DepthMap();

        MixerDescription(Mixer.Info mixerInfo) { this.mixerInfo = mixerInfo; }

        public String name() { return mixerInfo.getName(); }

        public String toString() { return "\""+mixerInfo.getName()+"\" ("+mixerInfo.getDescription()+")"; }

        private void add(AudioFormat format) {
            int depth = format.getSampleSizeInBits();
            float rate = format.getSampleRate();
            int chans = format.getChannels();

            RateMap rates = depths.get(depth);
            if (rates == null) { depths.put(depth, (rates = new RateMap())); }

            ChanMap channels = rates.get(rate);
            if (channels == null) { rates.put(rate, (channels = new ChanMap())); }

            FormatList formats = channels.get(chans);
            if (formats == null) { channels.put(chans, (formats = new FormatList())); }

            formats.add(format);
        }

        public PcmFormat selectFormat(AudioFormat format) throws UnsupportedFormatException {
            if (format.getEncoding() != PCM_SIGNED && format.getEncoding() != PCM_UNSIGNED) {
                 throw new UnsupportedFormatException("Non-PCM format not supported: "+format);
            }

            RateMap rates = depths.get(format.getSampleSizeInBits());
            if (rates == null) { throw new UnsupportedFormatException("Format not supported: "+format); }

            ChanMap channels = rates.get(format.getSampleRate());
            if (channels == null) { channels = rates.get((float) AudioSystem.NOT_SPECIFIED); }
            if (channels == null) { throw new UnsupportedFormatException("Format not supported: "+format); }

            FormatList formats = channels.get(format.getChannels());
            if (formats == null) { throw new UnsupportedFormatException("Format not supported: "+format); }

            AudioFormat r = null;
            for (AudioFormat f : formats) {
                if (r == null
                    || r.getFrameSize() != format.getFrameSize() && f.getFrameSize() == format.getFrameSize()
                    || r.isBigEndian() != format.isBigEndian() && f.isBigEndian() == format.isBigEndian()
                    || r.getEncoding() != format.getEncoding() && f.getEncoding() == format.getEncoding()) { r = f; }
            }
            if (r == null) { throw new UnsupportedFormatException("Format not supported: "+format); }
            return PcmFormat.of(r, (int) format.getSampleRate());
        }

        private static String rateFormat(float rate) {
            return rate == AudioSystem.NOT_SPECIFIED ? "unspecified sample rate, " : rate+" Hz, ";
        }

        public boolean anyAvailableFormat() { return depths.size() > 0; }

        private void printChannelRange(PrintStream out, int lo, int hi) {
            if (hi < 1) { return; }
            if (lo == hi) {
                if      (lo == 1) { out.print("mono"); }
                else if (lo == 2) { out.print("stereo"); }
                else              { out.print(lo+" channels"); }
            } else {
                if      (hi == 2) { out.print("mono/stereo"); }
                else              { out.print(lo+"-"+hi+" channels"); }
            }
        }

        public void printAvailableFormats(PrintStream out) {
            for (var depth : depths.entrySet()) {
                for (var rate : depth.getValue().entrySet()) {
                    out.print("    "+depth.getKey()+"-bit, "+rateFormat(rate.getKey()));
                    int lo = 0, hi = -1;
                    for (int c : rate.getValue().keySet()) {
                        if (c == hi+1) { hi = c; }
                        else {
                            printChannelRange(out, lo, hi);
                            lo = hi = c;
                        }
                    }
                    printChannelRange(out, lo, hi);
                    out.println();
                }
            }
        }
    }

    public static Map<String, MixerDescription> inputMixers = collectFormats(true), outputMixers = collectFormats(false);

    private static Map<String, MixerDescription> collectFormats(boolean input) {
        Map<String, MixerDescription> devices = new LinkedHashMap<String, MixerDescription>();
        for (Mixer.Info mxi : AudioSystem.getMixerInfo()) {
            for (Line.Info lni : (input ? AudioSystem.getMixer(mxi).getTargetLineInfo() : AudioSystem.getMixer(mxi).getSourceLineInfo())) {
                if (lni instanceof DataLine.Info) {
                    MixerDescription mxd = null;
                    for (AudioFormat format : ((DataLine.Info) lni).getFormats()) {
                        if (mxd == null) { devices.put(mxi.getName(), (mxd = new MixerDescription(mxi))); }
                        mxd.add(format);
                    }
                }
            }
        }
        return devices;
    }

    private static DataLine getLine(Mixer.Info mxi, Class lineClass, PcmFormat format, int lineBufBytes)
        throws LineUnavailableException, UnsupportedFormatException
    {
        try {
            return (DataLine) AudioSystem.getMixer(mxi).getLine(new DataLine.Info(lineClass, format, lineBufBytes));
        } catch (IllegalArgumentException ex) {
            throw new UnsupportedFormatException(ex);
        }
    }

    public static Device.Tuner.Factory inputFactory(String devName, double bufferSecs, int recoveryAttempts) throws IOException {
        if (devName != null && devName.startsWith("klipalsa:")) {
            Device device = Alsa.Tuner.devices.get(devName);
            if (device == null) { throw new IOException("No such input device: "+devName); }
            return new Device.Tuner.Factory() {
                public Device.Tuner open(PcmFormat format) throws IOException {
                    return new Alsa.Tuner(device, format, bufferSecs, recoveryAttempts);
                }
                public PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
                    return device.selectFormat(fmt);
                }
                public Device.BitLayout layout(PcmFormat fmt) {
                    return device.layout(fmt);
                }
            };
        } else {
            if (devName == null) { devName = AudioSystem.getMixer(null).getMixerInfo().getName(); }
            MixerDescription mxd = inputMixers.get(devName);
            if (mxd == null) { throw new IOException("No such input mixer: "+devName); }
            return new Device.Tuner.Factory() {
                public Device.Tuner open(PcmFormat format) throws IOException {
                    try {
                        TargetDataLine line = (TargetDataLine) getLine(mxd.mixerInfo, TargetDataLine.class, format, format.fs() * (int) (bufferSecs * format.getSampleRate()));
                        line.open();
                        line.start();
                        return new LineTuner(line);
                    } catch (LineUnavailableException ex) {
                        throw new IOException(ex);
                    }
                }
                public PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
                    return mxd.selectFormat(fmt);
                }
            };
        }
    }

    public static Device.Player.Factory outputFactory(String devName, double bufferSecs, int recoveryAttempts) throws IOException {
        if (devName != null && devName.startsWith("klipalsa:")) {
            Device device = Alsa.Player.devices.get(devName);
            if (device == null) { throw new IOException("No such output device: "+devName); }
            return new Device.Player.Factory() {
                public Device.Player open(PcmFormat format) throws IOException {
                    return new Alsa.Player(device, format, bufferSecs, recoveryAttempts);
                }
                public PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
                    return device.selectFormat(fmt);
                }
                public Device.BitLayout layout(PcmFormat fmt) {
                    return device.layout(fmt);
                }
            };
        } else {
            if (devName == null) { devName = AudioSystem.getMixer(null).getMixerInfo().getName(); }
            MixerDescription mxd = outputMixers.get(devName);
            if (mxd == null) { throw new IOException("No such output mixer: "+devName); }
            return new Device.Player.Factory() {
                public Device.Player open(PcmFormat format) throws IOException {
                    try {
                        SourceDataLine line = (SourceDataLine) getLine(mxd.mixerInfo, SourceDataLine.class, format, format.fs() * (int) (bufferSecs * format.getSampleRate()));
                        return new LinePlayer(line, false);
                    } catch (LineUnavailableException ex) {
                        throw new IOException(ex);
                    }
                }
                public PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
                    return mxd.selectFormat(fmt);
                }
            };
        }
    }

    private static void printDevices(int argPos, String[] args, Collection<Map<String, ? extends Device>> devsets, boolean listFormats) {
        if (argPos == args.length) {
            for (var devset : devsets) {
                for (Device dev : devset.values()) {
                    System.out.println(dev);
                    if (listFormats) { dev.printAvailableFormats(System.out); }
                }
            }
        } else {
            for (int i = argPos; i < args.length; i++) {
                for (var devset : devsets) {
                    Device dev = devset.get(args[i]);
                    if (dev != null) {
                        System.out.println(args[i]);
                        if (listFormats) { dev.printAvailableFormats(System.out); }
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        boolean listInputs = true, listOutputs = true, listFormats = false;

        String helpText = "Arguments: [options] [names]\n\n"
            +"Options:\n"
            +"-i                 List only inputs\n"
            +"-o                 List only outputs\n"
            +"-f                 List available formats\n"
            +"-h or --h          Print this message\n"
            +"--                 Remaining arguments are names\n";

        int argPos = 0;
        while (argPos < args.length) {
            String o = args[argPos];
            if      (o.charAt(0) != '-')   { break; }
            argPos++;
            if      ("--"      .equals(o)) { break; }
            else if ("-i"      .equals(o)) { listInputs = true; listOutputs = false; }
            else if ("-o"      .equals(o)) { listOutputs = true; listInputs = false; }
            else if ("-f"      .equals(o)) { listFormats = true; }
            else if (o.startsWith("-h") || o.startsWith("--h")) { System.out.println(helpText); return; }
            else {
                System.err.println("Unregognized option "+o+" (use -h for usage)");
                System.exit(2);
            }
        }

        try {
            if (listInputs) {
                if (listOutputs) { System.out.println("-------- Inputs: --------"); }
                printDevices(argPos, args, List.of(inputMixers, Alsa.Tuner.devices), listFormats);
            }
            if (listOutputs) {
                if (listInputs) { System.out.println("-------- Outputs: --------"); }
                printDevices(argPos, args, List.of(outputMixers, Alsa.Player.devices), listFormats);
            }
        } catch (Throwable th) {
            System.err.println(th);
            if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
            System.exit(1);
        }
    }
}

Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home