0

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[]

GDjkhp
  • 11
  • 5
  • Open a capture stream on a wasapi loopback device. You can test XtDevice.getCapabilities() for XtDeviceCaps.LOOPBACK to find loopback devices. There is currently no way to find the loopback device corresponding to the default output device, though. Then the data you want is in XtBuffer.input, in the buffer passed to the XtOnBuffer callback you specified when opening the stream. That's a raw pointer, from there you use JNA to read the data into a byte[]. Alternatively you can use XtSafeBuffer, that gets you a java short[] or float[], which you then manually convert to byte[]. – Sjoerd van Kreel Feb 04 '21 at 13:48
  • omg it's you lol, the creator of xtaudio, it's a huge honor… pls help me, i dunno where to start, i just downloaded the xt audio yesterday and i can't figure out how do i use it lol, i have so many questions, i'm using java and the docs for it is useless lol, pls help me, how do i open a capture stream loopback device? does the XtBuffer.input really returns pcmdata that's a byte array? where do i even start lol? i confused… java code much appreciated, but i will try c++ or other language… – GDjkhp Feb 05 '21 at 00:33
  • See here https://sjoerdvankreel.github.io/xt-audio/ under the tab record->java for an example of opening a regular capture stream. You only need to select a different device for loopback. See here https://sjoerdvankreel.github.io/xt-audio/doc/core/html/index.html for full documentation of the C interface, and here https://sjoerdvankreel.github.io/xt-audio/doc/java/apidocs/xt/audio/package-summary.html for dummy docs of the java interface (which corresponds nearly to the C interface, which is fully documented). – Sjoerd van Kreel Feb 05 '21 at 08:34

1 Answers1

2

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();
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
Sjoerd van Kreel
  • 1,000
  • 6
  • 19
  • ok the code actually works lol, but for only a second or by modifying Thread.sleep, removing Thread.sleep though gives me a null pointer exception at safe.lock(buffer), how do i fix it though? – GDjkhp Feb 07 '21 at 05:01
  • this thing happened [osc bug.png](https://drive.google.com/file/d/1SANM_SPUxMl0iIP6srMcl9TxyS8HYbGM/view?usp=sharing) – GDjkhp Feb 07 '21 at 08:10
  • also how do i get stereo channels? i need left and right channels array – GDjkhp Feb 07 '21 at 08:13
  • The null pointer just might be a bug in xt-audio. OnBuffer gets called on a separate thread so it might be that XtSafeBuffer is already released while the last OnBuffer callback is running. Will look into it. Do you have a stacktrace? The fix for now btw, is just to return from onbuffer immediately if "safe" is null. The short[] audio array passed to processAudio is already stereo. Even samples are left, odd samples are right. – Sjoerd van Kreel Feb 07 '21 at 12:04
  • [another buffer bug](https://drive.google.com/file/d/1nMStjQw-z_3NKclgWkl2RkLgdTVDoyAg/view?usp=sharing) look at the lines, see pic below, how do i smooth it? – GDjkhp Feb 07 '21 at 12:14
  • Can't say without looking at the code for your plotting. Btw do you actually want a spectrum analyzer or just plot the waveform itself? Those are different things. I suggest you download audacity or any other audio editor just so you can check what the recorded data looks like, then compare that to what you are drawing. – Sjoerd van Kreel Feb 07 '21 at 12:17
  • also i think byte[] was a mistake, how do i make it float-y also [weird lines](https://drive.google.com/file/d/1HsvLieLjs84FtfDfQJkiWwXFzwBIYdwD/view?usp=sharing), see pic above, why is there a line in the middle? – GDjkhp Feb 07 '21 at 12:24
  • `float[] pSample1 = bytesToFloats(BYTES); g.setColor(Color.Red); int yLast1 = (int) ((pSample1[0] / 48000) * (float) halfCanvasHeight) + halfCanvasHeight; int samIncrement1 = 1; for (int a = samIncrement1, c = 0; c < canvasWidth; a += samIncrement1, c++) { int yNow = (int) ((pSample1[a] / 48000) * (float) halfCanvasHeight) + halfCanvasHeight; g.drawLine(c, yLast1, c + 1, yNow); yLast1 = yNow; }` – GDjkhp Feb 07 '21 at 12:31
  • `float[] bytesToFloats(byte[] bytes){ float[] floats = new float[bytes.length / 2]; for (int i = 0; i < bytes.length; i += 2){ floats[i/2] = bytes[i] | (bytes[i+1] << 8); } return floats; }` – GDjkhp Feb 07 '21 at 12:33
  • Can you post a complete example? – Sjoerd van Kreel Feb 07 '21 at 12:36
  • also bytes length returns 2112, is this normal? – GDjkhp Feb 07 '21 at 12:36
  • [also here is my video making this shit and drives me insane](https://drive.google.com/file/d/1jKcYH6veonp93qch1vdnzAF5bh29NTGn/view?usp=sharing) – GDjkhp Feb 07 '21 at 12:45
  • [here is the github repo](https://github.com/GDjkhp/weird-visualizer) @Sjoerd van Kreel – GDjkhp Feb 07 '21 at 12:53
  • how do i make the line curves smooth? i think thats my question – GDjkhp Feb 07 '21 at 13:17
  • Don't use BYTES.length. Calculate the number of useful bytes from the framecount instead. If you need advice on the audio I/O part i'm glad to help, but i can't do much for you if the problem is in the visualization code. – Sjoerd van Kreel Feb 07 '21 at 18:01
  • how do i do that? also the thing why i use bytes length is just for testing if it works, what should i code then? – GDjkhp Feb 08 '21 at 00:36
  • [here is my another question, i hope you could help me with this, the BYTES array gives me weird results](https://stackoverflow.com/questions/66095350/weird-line-on-the-middle-of-the-wave-how-do-i-make-it-smooth-real-time-audio-s) @SjoerdvanKreel – GDjkhp Feb 08 '21 at 04:28
  • bytes array gives me weird results, i changed my mind, i want floats array, how do i do that? – GDjkhp Feb 08 '21 at 06:57
  • i need a method that converts BYTES[] (intermediate buffer) to float[], i think stretching the array might fix my problem – GDjkhp Feb 08 '21 at 07:25
  • i need float[] that ranges -1.0 to 1.0, for example, maximum value of bytes returns 1f, separated channels – GDjkhp Feb 08 '21 at 09:38
  • Then convert shorts (short[] audio) to floats directly. Replace BYTES of size frames * channels * 2 by FLOATS of size frames * channels. Then each float = ((float)shorts[i])/Short.MAX_VALUE. – Sjoerd van Kreel Feb 08 '21 at 09:40
  • the buffer only captures the master mixer channel, how do i change it to only the application's mixer channel? how do i refer it as "this"? @Sjoerd – GDjkhp Feb 13 '21 at 02:31
  • Can't be done. Wasapi loopback always captures the final mixdown. Apparently, when supported it uses the hardware's loopback facilities for this, in which case the individual application-level streams aren't even available anymore. See https://learn.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording. – Sjoerd van Kreel Feb 13 '21 at 20:27
  • how do i covert the buffer array to frequency bands array? @Sjoerd van Kreel – GDjkhp Mar 29 '21 at 12:26
  • Using an fft, probably. Better ask on dsp stackexchange. – Sjoerd van Kreel Apr 01 '21 at 14:52