Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Select output line in java for eight channel sound card

edit: I am now using Jack (Jack Audio Connection Kit). See answer below.

I have a soundcard on my Raspberry Pi with 8 output channels (four stereo channels), an Octosound card. What I want to do is select one of the channels to route sound to. With this code I print info of the sound card:

mixers = AudioSystem.getMixerInfo();
    for (Mixer.Info mixerInfo : mixers) {
        logger.debug("\n");
        logger.debug("Found Mixer: " + mixerInfo);

        Mixer m = AudioSystem.getMixer(mixerInfo);

        Line.Info[] sourceLines = m.getSourceLineInfo();
        for (Line.Info li : sourceLines) {
            logger.debug("Found source line: " + li + " " + li.getClass());

            if (li instanceof Port.Info) {
                Port.Info portInfo = (Port.Info) li;
                logger.debug("port found " + portInfo.getName() + " is source " + portInfo.isSource());
                sourceDataLines.add(portInfo);
            }

        }

        Line.Info[] targetLines = m.getTargetLineInfo();

        for (Line.Info li : targetLines) {
            logger.debug("Found target line: " + li + " " + li.getClass());
            outputLines.add(li);

            if (li instanceof Port.Info) {
                Port.Info portInfo = (Port.Info) li;
                logger.debug("port found " + portInfo.getName() + " is source " + portInfo.isSource());
                outputPorts.add(portInfo);
            }
        }
    }


private void lineClose(int soundPort) throws LineUnavailableException {
    Port.Info lineInfo = outputPorts.get(soundPort);
    Line line = (Port) AudioSystem.getLine(lineInfo);
    line.close();
}

private void lineOpen(int l) throws LineUnavailableException {

    for (int i = 0; i < outputPorts.size(); i++) {
        Port.Info lineInfo = outputPorts.get(i);
        Line line = (Port) AudioSystem.getLine(lineInfo);
        if (l == i) {
            line.open();
        } else {
            line.close();
        }
    }
}

This is the output I get:

Found Mixer: audioinjectoroc [default], version 4.9.41-v7+
Found source line: interface SourceDataLine supporting 84 audio formats, and buffers of at least 32 bytes class com.sun.media.sound.DirectAudioDevice$DirectDLI
Found source line: interface Clip supporting 84 audio formats, and buffers of at least 32 bytes class com.sun.media.sound.DirectAudioDevice$DirectDLI
Found target line: interface TargetDataLine supporting 84 audio formats, and buffers of at least 32 bytes class com.sun.media.sound.DirectAudioDevice$DirectDLI

Found Mixer: audioinjectoroc [plughw:0,0], version 4.9.41-v7+
Found source line: interface SourceDataLine supporting 96 audio formats, and buffers of at least 32 bytes class com.sun.media.sound.DirectAudioDevice$DirectDLI
Found source line: interface Clip supporting 96 audio formats, and buffers of at least 32 bytes class com.sun.media.sound.DirectAudioDevice$DirectDLI
Found target line: interface TargetDataLine supporting 96 audio formats, and buffers of at least 32 bytes class com.sun.media.sound.DirectAudioDevice$DirectDLI

Found Mixer: Port audioinjectoroc [hw:0], version 4.9.41-v7+
Found source line: ADC1 source port class com.sun.media.sound.PortMixer$PortInfo
port found ADC1 is source true
Found source line: ADC2 source port class com.sun.media.sound.PortMixer$PortInfo
port found ADC2 is source true
Found source line: ADC3 source port class com.sun.media.sound.PortMixer$PortInfo
port found ADC3 is source true
Found target line: DAC1 target port class com.sun.media.sound.PortMixer$PortInfo
port found DAC1 is source false
Found target line: DAC2 target port class com.sun.media.sound.PortMixer$PortInfo
port found DAC2 is source false
Found target line: DAC3 target port class com.sun.media.sound.PortMixer$PortInfo
port found DAC3 is source false
Found target line: DAC4 target port class com.sun.media.sound.PortMixer$PortInfo
port found DAC4 is source false

Now this is the code I use to output sound from a wav file:

   String path = soundDirectory + soundUrl;
   InputStream is = new FileInputStream(path);
   BufferedInputStream bis = new BufferedInputStream(is);
   AudioInputStream inputStream = AudioSystem.getAudioInputStream(bis);
   AudioFormat format = inputStream.getFormat();

   Mixer.Info mi = mixers[0];

   SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem.getSourceDataLine(format,mi);
   sourceDataLine.open(format);
   sourceDataLine.start();
   byte[] buf = new byte[1024];
   int bytesRead;
   while ((bytesRead = inputStream.read(buf)) != -1){
       sourceDataLine.write(buf, 0, bytesRead);
   }
   inputStream.close();

   sourceDataLine.drain();
   sourceDataLine.stop();
   sourceDataLine.close();

   lineClose(soundPort);

I have tried a number of things, but in all cases, sound comes out of all ouputs.

like image 892
Christine Avatar asked Aug 22 '17 15:08

Christine


1 Answers

I found the solution myself. I now use Jack (Jack Audio Connection Kit, see here. It's a bit of a hassle to get Jack running on a Raspberry Pi. There's good info here.

I use JnaJack with provides an interface between Java and Jack.

You can't run Jack on Raspbian out of the box. Debian Wheezy had a patch, but Raspbian Jessie doesn't seem to have that. So you need to create a version of Jackd2 that doesn't use DBus. Here it's explained how to build Jackd2 without DBus. There's a snag: all you have to do is remove the two lines that refer to DBus. Everything else that they tell you to patch has now been patched in Raspbian by default, or so it seems. These lines you need to replace: after you downloaded the source, in debian/rules change

waf-configure-options += $(if $(filter linux,$(DEB_HOST_ARCH_OS)),--alsa --dbus) 
became
waf-configure-options += $(if $(filter linux,$(DEB_HOST_ARCH_OS)),--alsa)

dh_install -pjackd2 debian/tmp/usr/share/dbus-1/*
became
#dh_install -pjackd2 debian/tmp/usr/share/dbus-1/*

I modified the SimpleAudioClient example found in the JnaJack source:

public class SimpleAudioClient {

    private boolean autoconnect = true;
    private JackClient client;
    private Processor processor;
    private Callback callback;
    private ShutDownHook shutDownHook;
    private JackPort[] inputPorts;
    private JackPort[] outputPorts;
    private FloatBuffer[] inputBuffers;
    private FloatBuffer[] outputBuffers;
    private float samplerate;
    private int buffersize;
    private volatile boolean active;

    private Logger logger = Logger.getLogger(getClass().getSimpleName());
    private int channelNumber = 0;

    public SimpleAudioClient() throws JackException {

        Jack jack = Jack.getInstance();
        logger.debug("Jack instance " + jack.toString());
        EnumSet<JackOptions> options = EnumSet.of(JackOptions.JackNoStartServer);
        EnumSet<JackStatus> status = EnumSet.noneOf(JackStatus.class);
        try {
            client = jack.openClient("jna_jack", options, status);
        } catch (JackException ex) {
            System.out.println("ERROR : Status : " + status);
            throw ex;
        }

        String[] inputs = new String[0];
        inputPorts = new JackPort[inputs.length];
        EnumSet<JackPortFlags> flags = EnumSet.of(JackPortFlags.JackPortIsInput);
        for (int i = 0; i < inputs.length; i++) {
            //inputPorts[i] = client.registerPort(inputs[i], JackPortType.AUDIO, flags);
        }

        String[] outputs = new String[]{"playback_1", "playback_2", "playback_3", "playback_4", "playback_5", "playback_6", "playback_7", "playback_8"};
        outputPorts = new JackPort[outputs.length];
        flags = EnumSet.of(JackPortFlags.JackPortIsOutput);
        for (int i = 0; i < outputs.length; i++) {
            outputPorts[i] = client.registerPort(outputs[i], JackPortType.AUDIO, flags);
        }

        processor = new SineAudioSource();

        this.inputBuffers = new FloatBuffer[inputPorts.length];
        this.outputBuffers = new FloatBuffer[outputPorts.length];
        this.callback = new Callback();
        this.shutDownHook = new ShutDownHook();
        client.onShutdown(shutDownHook);

        for (JackPort port : inputPorts) {
            logger.debug("input port " + port.getType() + " " + port.getName());
        }

        for (JackPort port : outputPorts) {
            logger.debug("output port " + port.getType() + " " + port.getName());
        }
    }

    public void activate(int channelNr) throws JackException {

        this.channelNumber = channelNr;

        try {
            samplerate = client.getSampleRate();
            System.out.println("Sample rate = " + samplerate);
            buffersize = client.getBufferSize();
            System.out.println("Buffersize = " + buffersize);
            processor.setup(samplerate, buffersize);
            active = true;
            client.setProcessCallback(callback);
            client.activate();
            if (autoconnect) {
                doAutoconnect();
            }
        } catch (Exception ex) {
            active = false;
            throw new JackException("Could not activate Jack client");
        }
    }

    private void doAutoconnect() throws JackException {
        Jack jack = Jack.getInstance();
        String[] physical = jack.getPorts(client, null, JackPortType.AUDIO,
                EnumSet.of(JackPortFlags.JackPortIsInput, JackPortFlags.JackPortIsPhysical));
        int count = Math.min(outputPorts.length, physical.length);
        for (int i = 0; i < count; i++) {
            logger.debug("output port " + outputPorts[i].getName());
            jack.connect(client, outputPorts[i].getName(), physical[i]);
        }
        physical = jack.getPorts(client, null, JackPortType.AUDIO,
                EnumSet.of(JackPortFlags.JackPortIsOutput, JackPortFlags.JackPortIsPhysical));
        count = Math.min(inputPorts.length, physical.length);
        for (int i = 0; i < count; i++) {
            logger.debug("input port " + inputPorts[i].getName());
            //jack.connect(client, physical[i], inputPorts[i].getName());
        }
    }

    public void shutdown() {
        active = false;
        client.deactivate();
        client.close();
    }

    private void processBuffers(int nframes) {
        for (int i = 0; i < inputPorts.length; i++) {
            inputBuffers[i] = inputPorts[i].getFloatBuffer();
        }
        for (int i = 0; i < outputPorts.length; i++) {
            outputBuffers[i] = outputPorts[i].getFloatBuffer();
        }
        processor.process(channelNumber, inputBuffers, outputBuffers);
    }

    private class Callback implements JackProcessCallback {

        public boolean process(JackClient client,final int nframes) {

            if (!active) {
                return false;
            } else {
                try {
                    processBuffers(nframes);
                    return true;
                } catch (Exception ex) {
                    System.out.println("ERROR : " + ex);
                    active = false;
                    return false;
                }

            }
        }
    }

    private class ShutDownHook implements JackShutdownCallback {

        public void clientShutdown(JackClient client) {
            active = false;
            processor.shutdown();
        }
    }

    public static interface Processor {

        public void setup(float samplerate, int buffersize);

        public void process(int channelNumber, FloatBuffer[] inputs, FloatBuffer[] outputs);

        public void shutdown();
    }

    /**
     * Create a SimpleAudioClient.
     *
     * @return client
     * @throws org.jaudiolibs.jnajack.JackException
     */
    public static SimpleAudioClient create(
    ) throws JackException {

        return new SimpleAudioClient();
    }
}

I modified the SineAudioClient from the sample code into this:

public class SineAudioSource implements SimpleAudioClient.Processor {

    private final static int TABLE_SIZE = 200;
    private int left_phase = 0;
    private int right_phase = 0;
    private float[] data;

    public void setup(float samplerate, int buffersize) {
        data = new float[TABLE_SIZE];
        for (int i = 0; i < TABLE_SIZE; i++) {
            data[i] = (float) (0.2 * Math.sin(((double) i / (double) TABLE_SIZE) * Math.PI * 2.0));
        }
    }

    public void process(int channelNumber, FloatBuffer[] inputs, FloatBuffer[] outputs) {

        FloatBuffer left = outputs[channelNumber];
        int size = left.capacity();
        for (int i = 0; i < size; i++) {
            left.put(i, data[left_phase]);
            left_phase += 2;
            right_phase += 3;
            if (left_phase >= TABLE_SIZE) {
                left_phase -= TABLE_SIZE;
            }
        }
    }

    public void shutdown() {
        System.out.println("Sine Audio Source shutdown");
    }
}

so it plays a sine wave for two seconds in each of the eight channels of the sound card. I haven't tried input channels (yet), I read that it's hard to get Jack to work on Raspbian when both input and output is activated.

I start Jack before I run my app, the start command is

/usr/bin/jackd -dalsa -dhw:audioinjectoroc -r48000 -p1024 -n2 -P &

The log when you start jack should show

creating alsa driver ... hw:audioinjectoroc|-|1024|2|48000|0|0|nomon|swmeter|-|32bit

where "audioinjector" is the name of the sound card. If it shows

...hw:audioinjectoroc|hw:audioinjectoroc|1024 ...

then you'll have issues connecting to it.

You can view Jack settings with QJackCtl, which you can run on your Raspberry Pi and access with an X server from another computer. I didn't manage to run X Windows on the Pi.

If you want to play wav files via Jack, this is a good example of how to read a wav file and feed it to jack.

Edit: the example is a good starting point, but you need to make a few changes. It's best to open all ports that you plan to use, call client.activate(), and in the JackCallback route the channels from your audio file to the appropriate channels in the sound card. You can use qjackctl to see what's happening in Jack.

like image 197
Christine Avatar answered Oct 21 '22 22:10

Christine