Multi-output audio interface?

Well, I figured out how to output multichannel at least. You can get access to a “SourceDataLine” (output) which allows you to output (bitdepth/8) * channels bytes at a time. Here’s a sketch which outputs a sinewave. I am on a Mac and used the BlackHole virtual audio device for testing.

import javax.sound.sampled.*;

static final String MIXER_NAME = "BlackHole 16ch";
//static final String MIXER_NAME = "MOTU UltraLite mk3 Hybrid";
static final int BIT_DEPTH = 16;
static final int SAMPLE_RATE = 44100;

Mixer.Info[] mixerInfo;
Mixer mixer;
Line line;

AudioFormat af;
SourceDataLine sdl;
void setup()
{
  size(200, 200);
  mixerInfo = AudioSystem.getMixerInfo();

  // find our interface by name
  for (int i=0; i<mixerInfo.length; i++) {
    if (mixerInfo[i].getName().equals(MIXER_NAME)) {
      mixer = AudioSystem.getMixer(mixerInfo[i]);
      print (String.format("Found mixer %s!\n",MIXER_NAME));
      break;
    }
  }
  if (mixer==null) {
    print (String.format ("Can't find mixer %s, quitting\n", MIXER_NAME));
    print ("Available mixers are:\n");
    printArray(mixerInfo);
    return;
  }

  // find the audio format we want
  Line.Info[] lineInfo = mixer.getSourceLineInfo();
  try {
    for (Line.Info li : lineInfo) {
      Line line = mixer.getLine (li);
      if (line instanceof SourceDataLine) {
        SourceDataLine source = (SourceDataLine) line;
        DataLine.Info i = (DataLine.Info) source.getLineInfo();
        for (AudioFormat format : i.getFormats()) {
          if (format.getChannels()>2 && format.getSampleSizeInBits()==BIT_DEPTH && format.isBigEndian() && format.getEncoding()==AudioFormat.Encoding.PCM_SIGNED) {
            print(String.format ("Found this format: %s\n", format));
            af=new AudioFormat (44100, BIT_DEPTH, format.getChannels(), true, true);
          }
        }
      }
    }
 }
 
  catch (LineUnavailableException e) {
    print ("line unavailable, exiting\n");
    return;
  }
  if (af==null) {
    print("Couldn't find the desired audio format\n");
    return;
  }

  try {
    // now we have found a multi-channel audio format in the mixer, open and start a SourceDataLine (output).
    DataLine.Info sdlInfo = new DataLine.Info (SourceDataLine.class, af);
    sdl = (SourceDataLine)mixer.getLine(sdlInfo);
    println (String.format("got the line, %d bytes frame", af.getFrameSize()));
    sdl.open();
    sdl.start();

    // generate some PCM data (a sine wave for simplicity)
    byte[] buffer = new byte[128];
    float step = (TWO_PI*2/buffer.length);
    float angle = 0;
    int i = buffer.length;
    while (i > 0) {
      float sine = sin(angle);
      int sample = (int) Math.round(sine*32767);
      buffer[--i] = (byte) sample;
      buffer[--i] = (byte) (sample >> 8);
      angle += step;
    }

     // write the sinewave 2000x to the buffer
    for (int n=0; n<2000; ++n) {
      writeToOutput(4, buffer);                // write to channel 5 (channels start at 0)
    }

    // shut down audio
    sdl.drain();
    sdl.stop();
    sdl.close();
  }
  catch (LineUnavailableException e) {
    println (e);
  }
}

void writeToOutput (int channel, byte[] buffer) {
 // writes a buffer to aparticular channel of the sound card. (only will work with 16 bit atm)
  
  // for example, an 8-channel soundcard will give you a 16 byte "frame size". To output to channel 5
  // you have to insert your 16-bit audio data into bytes 8+9:
  // 
  // 00 00 00 00 00 00 00 00 LS MS 00 00 00 00 00 00  
  //
  // then write the whole buffer to the SourceDataLine we opened earlier.
  // This writes it one frame at a time which is probably horribly inefficient.
  byte[] b = new byte[af.getFrameSize()];     // make a buffer the same size as the frame
  int lsb = (channel*2);                      // compute position of LSB                           
  for (int i=0; i<buffer.length; i+=2) {      // 2 bytes at a time
    b[lsb] = buffer[i];                       // copy in LSB
    b[lsb+1] = buffer[i+1];                   // copy in MSB
    sdl.write(b, 0, b.length);                // write to soundcard
  }
}

// just for debugging
void dumpBuffer(int w, byte[] buf) {
  for (int i=0; i<buf.length; i++) {
    print (String.format ("%02X ", buf[i]));
    if ((i+1)%w==0) println ();
  }
}

Of course to use this you need the raw sample data. Not a huge deal for me because all I needed was to output a sine wave anyway, but I don’t think raw samples are available in the Sound library.

Taking a look at the jsyn source code I don’t think it would be too difficult to extend, but beyond my capabilities for sure. I’ll leave this here in the hope that someone can manage it!