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