javaaudiographicsspectrumwasapi

How do i get byte[] pcmData of internal audio (Xt Audio for Java)


How do i get byte[] pcmData (just like in https://github.com/goxr3plus/Java-Spectrum-Analyser-Tutorials) but on xt audio? i wanted to draw the osc (spectrum analyzer) of the internal audio using wasapi (output speakers) of the computer real time. e.g. analyzes youtube audio output real time, internal audio of games, etc.

edit: how do i capture internal audio wasapi pcmdata (internal sound, not mic sound) using xt audio to analyze it on a visualizer? i need byte[]


Solution

  • See below for a complete example. It records 1 second of audio data for each loopback device, converts it to a byte array, then dumps that to a file with the name of the device. I hope it's sufficiently self-explanatory.

    package sample;
    
    import com.sun.jna.Pointer;
    import java.io.FileOutputStream;
    import java.util.EnumSet;
    import xt.audio.Enums.XtDeviceCaps;
    import xt.audio.Enums.XtEnumFlags;
    import xt.audio.Enums.XtSample;
    import xt.audio.Enums.XtSystem;
    import xt.audio.Structs.XtBuffer;
    import xt.audio.Structs.XtBufferSize;
    import xt.audio.Structs.XtChannels;
    import xt.audio.Structs.XtDeviceStreamParams;
    import xt.audio.Structs.XtFormat;
    import xt.audio.Structs.XtMix;
    import xt.audio.Structs.XtStreamParams;
    import xt.audio.XtAudio;
    import xt.audio.XtDevice;
    import xt.audio.XtDeviceList;
    import xt.audio.XtPlatform;
    import xt.audio.XtSafeBuffer;
    import xt.audio.XtService;
    import xt.audio.XtStream;
    
    public class Sample {
    
        // intermediate buffer
        static byte[] BYTES;
        // dump to file (never do this, see below)
        static FileOutputStream fos;
    
        // audio streaming callback
        static int onBuffer(XtStream stream, XtBuffer buffer, Object user) throws Exception {
            XtSafeBuffer safe = XtSafeBuffer.get(stream);
            if(safe == null) return 0;
            // lock buffer from native into java
            safe.lock(buffer);
            // short[] because we specified INT16 below
            // this is the captured audio data
            short[] audio = (short[])safe.getInput();
            // you want a spectrum analyzer, i dump to a file
            // but actually never dump to a file in any serious app
            // see http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing
            processAudio(audio, buffer.frames);
            // unlock buffer from java into native
            safe.unlock(buffer);
            return 0;
        }
    
        static void processAudio(short[] audio, int frames) throws Exception {
            // convert from short[] to byte[]
            for(int frame = 0; frame < frames; frame++) {
                // for 2 channels
                for(int channel = 0; channel < 2; channel++) {
                    // 2 = channels again
                    int sampleIndex = frame * 2 + channel;
                    // 2 = 2 bytes for each short
                    int byteIndex0 = sampleIndex * 2;
                    int byteIndex1 = sampleIndex * 2 + 1;
                    // probably some library method for this, somewhere
                    BYTES[byteIndex0] = (byte)(audio[sampleIndex] & 0x000000FF);
                    BYTES[byteIndex1] = (byte)((audio[sampleIndex] & 0x0000FF00) >> 8);
                }
            }
    
            // by now BYTES contains the data you want,
            // but be sure to account for frame count
            // (i.e. not all off BYTES may contain useful data,
            // might be some unused garbage at the end)
    
            // compute total bytes this round
            // = frame count * 2 channels * 2 bytes per short (INT16)
            int byteCount = frames * 2 * 2;
    
            // write to file - again, never do this in a real app
            fos.write(BYTES, 0, byteCount);
        }
    
        public static void main(String[] args) throws Exception {
            // this initializes platform dependent stuff like COM
            try(XtPlatform platform = XtAudio.init(null, Pointer.NULL, null)) {
                // works on windows only, obviously
                XtService service = platform.getService(XtSystem.WASAPI);
                // list input devices (this includes loopback)
                try(XtDeviceList list = service.openDeviceList(EnumSet.of( XtEnumFlags.INPUT))) {
                    for(int i = 0; i < list.getCount(); i++) {
                        String deviceId = list.getId(i);
                        EnumSet<XtDeviceCaps> caps = list.getCapabilities(deviceId);
                        // filter loopback devices
                        if(caps.contains(XtDeviceCaps.LOOPBACK)) {
                            String deviceName = list.getName(deviceId);
                            // just to check what output we're recording
                            System.out.println(deviceName);
                            // open device
                            try(XtDevice device = service.openDevice(deviceId)) {
                                // 16 bit 48khz
                                XtMix mix = new XtMix(48000, XtSample.INT16);
                                // 2 channels input, no masking
                                XtChannels channels = new XtChannels(2, 0, 0, 0);
                                // final audio format
                                XtFormat format = new XtFormat(mix, channels);
                                // query min/max/default buffer sizes
                                XtBufferSize bufferSize = device.getBufferSize(format);
                                // true->interleaved, onBuffer->audio stream callback
                                XtStreamParams streamParams = new XtStreamParams(true, Sample::onBuffer, null, null);
                                // final initialization params with default buffer size
                                XtDeviceStreamParams deviceParams = new XtDeviceStreamParams(streamParams, format, bufferSize.current);
                                // run stream
                                // safe buffer allows you to get java short[] instead on jna Pointer in the callback
                                try(XtStream stream = device.openStream(deviceParams, null);
                                    var safeBuffer = XtSafeBuffer.register(stream, true)) {
                                    // max frames to enter onBuffer * channels * bytes per sample
                                    BYTES = new byte[stream.getFrames() * 2 * 2];
                                    // make filename valid
                                    String fileName = deviceName.replaceAll("[\\\\/:*?\"<>|]", "");
                                    try(FileOutputStream fos0 = new FileOutputStream(fileName + ".raw")) {
                                        // make filestream accessible to the callback
                                        // could also be done by passsing as userdata to openStream
                                        fos = fos0;
                                        // run for 1 second
                                        stream.start();
                                        Thread.sleep(1000);
                                        stream.stop();
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }