ARTICLE AD BOX
I am not sure why this is happening so I am coming here to look for any sort of help I can get. Basically when recording any jack device such as firefox in my app it comes out as distorted. But it works perfectly fine when recording it to a wav file in the demo code attached, or when recording my microphone. The full source of my application is at https://github.com/1vers1on/bolt
This image shows the impulses when recording firefox.
This is the waveform when recording firefox when nothing is playing.
This waveform is what is shown when recording my microphone. Completely normal as expected.
The relevant files in my source code are "Bolt.java", "PortAudioInputStream.java", "InputThread.java", and all of the jni folders and c source code.
The following demo code works completely as expected with all input devices.
package net.ellie.bolt; import java.io.ByteArrayInputStream; import java.io.File; import java.util.List; import javax.sound.sampled.AudioFileFormat; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import net.ellie.bolt.jni.portaudio.AudioInputStream; import net.ellie.bolt.jni.portaudio.PortAudioJNI; public class Main { public static void main(String[] args) { // try { // Bolt.run(); // } catch (Exception e) { // e.printStackTrace(); // } final int durationSeconds = 5; final int channelsPreferred = 2; final double sampleRatePreferred = 48000.0; PortAudioJNI pa = new PortAudioJNI(); int init = pa.initialize(); if (init != 0) { System.err.println("Failed to initialize PortAudio (code=" + init + ")"); return; } try { List<PortAudioJNI.DeviceInfo> devices = pa.enumerateDevices(); if (devices == null || devices.isEmpty()) { System.err.println("No audio devices found"); return; } for (PortAudioJNI.DeviceInfo d : devices) { System.out.printf("Device %d: %s (in: %d ch, out: %d ch, default SR: %.1f)\n", d.index(), d.name(), d.maxInputChannels(), d.maxOutputChannels(), d.defaultSampleRate()); } PortAudioJNI.DeviceInfo device = devices.get(27); int channels = Math.min(device.maxInputChannels(), channelsPreferred); double sampleRate = sampleRatePreferred; if (!pa.isFormatSupported(device.index(), channels, sampleRate)) { channels = Math.max(1, Math.min(device.maxInputChannels(), 1)); sampleRate = device.defaultSampleRate() > 0 ? device.defaultSampleRate() : sampleRatePreferred; } long framesPerBuffer = 256; AudioInputStream inputStream = pa.openInputStream(device.index(), channels, sampleRate, framesPerBuffer); try (inputStream) { inputStream.start(); int totalFrames = (int) (sampleRate * durationSeconds); int bytesPerSample = 2; int bytesPerFrame = bytesPerSample * channels; byte[] data = new byte[totalFrames * bytesPerFrame]; int bytesCaptured = 0; while (bytesCaptured < data.length) { int remaining = data.length - bytesCaptured; int toRead = Math.max(bytesPerFrame, Math.min(remaining, bytesPerFrame * 1024)); int read = inputStream.read(data, bytesCaptured, toRead); if (read <= 0) break; bytesCaptured += read; } int framesCaptured = bytesCaptured / bytesPerFrame; float[] floatInterleaved = pcm16LeBytesToFloats(data, framesCaptured, channels); float gain = 0.8f; for (int i = 0; i < floatInterleaved.length; i++) { floatInterleaved[i] *= gain; } byte[] outBytes = floatsToPcm16LeBytes(floatInterleaved, framesCaptured, channels); AudioFormat format = new AudioFormat( (float) sampleRate, 16, channels, true, false ); ByteArrayInputStream bais = new ByteArrayInputStream(outBytes); javax.sound.sampled.AudioInputStream ais = new javax.sound.sampled.AudioInputStream(bais, format, framesCaptured); File out = new File("output.wav"); AudioSystem.write(ais, AudioFileFormat.Type.WAVE, out); System.out.printf("Saved %d frames to %s at %.0f Hz, %d ch (processed)\n", framesCaptured, out.getAbsolutePath(), sampleRate, channels); } } catch (Exception e) { e.printStackTrace(); } finally { pa.terminate(); } } private static float[] pcm16LeBytesToFloats(byte[] bytes, int frames, int channels) { int samples = frames * channels; float[] out = new float[samples]; int bi = 0; for (int i = 0; i < samples; i++) { int lo = bytes[bi++] & 0xFF; int hi = bytes[bi++]; int sample = (hi << 8) | lo; float f = sample >= 0 ? (sample / 32767.0f) : (sample / 32768.0f); out[i] = f; } return out; } private static byte[] floatsToPcm16LeBytes(float[] floats, int frames, int channels) { int samples = frames * channels; byte[] out = new byte[samples * 2]; int bi = 0; for (int i = 0; i < samples; i++) { float f = floats[i]; if (f > 1.0f) f = 1.0f; if (f < -1.0f) f = -1.0f; int s; if (f >= 0) { s = (int) Math.round(f * 32767.0); } else { s = (int) Math.round(f * 32768.0); } out[bi++] = (byte) (s & 0xFF); out[bi++] = (byte) ((s >>> 8) & 0xFF); } return out; } }Here is the PortAudioInputSource.java
package net.ellie.bolt.input.sources.real; import java.io.IOException; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.ellie.bolt.contexts.PortAudioContext; import net.ellie.bolt.input.CloseableInputSource; import net.ellie.bolt.jni.portaudio.AudioInputStream; import net.ellie.bolt.jni.portaudio.PortAudioJNI; public class PortAudioInputSource implements CloseableInputSource { private final Logger logger = LoggerFactory.getLogger(PortAudioInputSource.class); private volatile boolean running = true; private AudioInputStream audioInputStream; private final int sampleRate; private final int channelCount; private final int framesPerBuffer; private byte[] byteBuffer; // re-used between reads to avoid allocations public PortAudioInputSource(int deviceIndex, int channels, double sampleRate, long framesPerBuffer) { PortAudioJNI pa = PortAudioContext.getInstance().getPortAudioJNI(); PortAudioJNI.DeviceInfo device = null; List<PortAudioJNI.DeviceInfo> devices = pa.enumerateDevices(); for (PortAudioJNI.DeviceInfo d : devices) { if (d.index() == deviceIndex) { device = d; break; } } if (device == null) { throw new RuntimeException("Device not found: " + deviceIndex); } int maxInputChannels = device.maxInputChannels(); int actualChannels = channels; if (channels == 1 && maxInputChannels < 1) { actualChannels = Math.max(1, maxInputChannels); logger.warn("Device {} doesn't support {} channels, using {} channels instead", device.name(), channels, actualChannels); } this.sampleRate = (int) sampleRate; this.channelCount = Math.max(1, actualChannels); this.framesPerBuffer = (int) Math.max(1, framesPerBuffer); logger.info("Opening PortAudio input stream with deviceIndex={}, channels={} (requested: {}), sampleRate={}, framesPerBuffer={}", deviceIndex, this.channelCount, channels, sampleRate, framesPerBuffer); audioInputStream = PortAudioContext.getInstance().getPortAudioJNI() .openInputStream(deviceIndex, this.channelCount, sampleRate, framesPerBuffer); this.byteBuffer = new byte[this.framesPerBuffer * this.channelCount * 2]; audioInputStream.start(); } @Override public int read(float[] buffer, int offset, int length) throws InterruptedException, IOException { if (!running || audioInputStream == null) { return 0; } int bytesToRead = Math.min(byteBuffer.length, length * 2 * channelCount); int bytesRead = audioInputStream.read(byteBuffer, 0, bytesToRead); if (bytesRead <= 0) { return 0; } int framesCaptured = bytesRead / (2 * channelCount); int framesToCopy = Math.min(framesCaptured, length); int bi = 0; for (int i = 0; i < framesToCopy; i++) { float sum = 0; int channelsRead = 0; for (int j = 0; j < channelCount; j++) { if (bi + 1 >= byteBuffer.length) break; int lo = byteBuffer[bi++] & 0xFF; int hi = byteBuffer[bi++]; int sample = (hi << 8) | lo; float f = sample >= 0 ? (sample / 32767.0f) : (sample / 32768.0f); sum += f; channelsRead++; } if (channelsRead > 0) { buffer[offset + i] = sum / channelsRead; } else { buffer[offset + i] = 0; } } return framesToCopy; } @Override public int getSampleRate() { return sampleRate; } @Override public void stop() { running = false; if (audioInputStream != null) { audioInputStream.stop(); audioInputStream.close(); audioInputStream = null; } } @Override public String getName() { return "PortAudio Input Source"; } @Override public boolean isRunning() { return running; } @Override public boolean isComplex() { return false; } @Override public void close() { stop(); } }This is the InputThread.java file
package net.ellie.bolt.input; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; import net.ellie.bolt.config.Configuration; import net.ellie.bolt.dsp.buffers.CircularFloatBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class InputThread implements Runnable { private static final Logger logger = LoggerFactory.getLogger(InputThread.class); private final CloseableInputSource inputSource; private final AtomicBoolean running = new AtomicBoolean(false); private final CircularFloatBuffer buffer; // Throttling state private final int complexFactor; private final int sampleRate; // samples per second for one channel; effective rate accounts for complexFactor private long nextReadDeadlineNanos = 0L; private final float[] readBuffer; private Thread localInputThread = null; public InputThread(CloseableInputSource inputSource, int sampleRate) { this.inputSource = inputSource; this.complexFactor = inputSource.isComplex() ? 2 : 1; this.sampleRate = sampleRate; int bufferSize = Configuration.getFftSize() * 32 * complexFactor; // TODO: figure out the correct size this.buffer = new CircularFloatBuffer(bufferSize); this.readBuffer = new float[4 * 16384 * complexFactor]; // TODO: maybe increase the size } public void start() { running.set(true); localInputThread = new Thread(this, "InputThread-" + inputSource.getName()); localInputThread.start(); } @Override public void run() { logger.info("InputThread for {} started", inputSource.getName()); try { nextReadDeadlineNanos = System.nanoTime(); while (running.get()) { long now = System.nanoTime(); long waitNanos = nextReadDeadlineNanos - now; if (waitNanos > 0) { long waitMillis = waitNanos / 1_000_000L; int waitExtraNanos = (int)(waitNanos % 1_000_000L); if (waitMillis > 0 || waitExtraNanos > 0) { try { Thread.sleep(waitMillis, waitExtraNanos); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } } int samplesRead = inputSource.read(readBuffer, 0, readBuffer.length); if (samplesRead > 0) { buffer.write(readBuffer, 0, samplesRead); double secondsForChunk = (double) samplesRead / (double) (sampleRate * complexFactor); long nanosForChunk = (long) (secondsForChunk * 1_000_000_000L); nextReadDeadlineNanos = System.nanoTime() + nanosForChunk; } else { try { Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } nextReadDeadlineNanos = System.nanoTime() + 1_000_000L; } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (IOException e) { e.printStackTrace(); } finally { try { inputSource.close(); } catch (Exception ignored) {} } } public void stop() { running.set(false); if (localInputThread != null) { try { localInputThread.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } inputSource.stop(); logger.info("InputThread for {} stopped", inputSource.getName()); } public CircularFloatBuffer getBuffer() { return buffer; } }


