aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java
diff options
context:
space:
mode:
authorRubbaBoy <[email protected]>2020-07-06 02:33:28 -0400
committerPhil Burk <[email protected]>2020-10-30 11:19:34 -0700
commit46888fae6eb7b1dd386f7af7d101ead99ae61981 (patch)
tree8969bbfd68d2fb5c0d8b86da49ec2eca230a72ab /src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java
parentc51e92e813dd481603de078f0778e1f75db2ab05 (diff)
Restructured project, added gradle, JUnit, logger, and more
Added Gradle (and removed ant), modernized testing via the JUnit framework, moved standalone examples from the tests directory to a separate module, removed sparsely used Java logger and replaced it with SLF4J. More work could be done, however this is a great start to greatly improving the health of the codebase.
Diffstat (limited to 'src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java')
-rw-r--r--src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java432
1 files changed, 432 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java b/src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java
new file mode 100644
index 0000000..75c4a8a
--- /dev/null
+++ b/src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.devices.javasound;
+
+import java.util.ArrayList;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.Line;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.SourceDataLine;
+import javax.sound.sampled.TargetDataLine;
+
+import com.jsyn.devices.AudioDeviceInputStream;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.devices.AudioDeviceOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Use JavaSound to access the audio hardware.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class JavaSoundAudioDevice implements AudioDeviceManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JavaSoundAudioDevice.class);
+
+ private static final int BYTES_PER_SAMPLE = 2;
+ private static final boolean USE_BIG_ENDIAN = false;
+
+ ArrayList<DeviceInfo> deviceRecords;
+ private double suggestedOutputLatency = 0.040;
+ private double suggestedInputLatency = 0.100;
+ private int defaultInputDeviceID = -1;
+ private int defaultOutputDeviceID = -1;
+
+ public JavaSoundAudioDevice() {
+ String osName = System.getProperty("os.name");
+ if (osName.contains("Windows")) {
+ suggestedOutputLatency = 0.08;
+ LOGGER.info("JSyn: default output latency set to "
+ + ((int) (suggestedOutputLatency * 1000)) + " msec for " + osName);
+ }
+ deviceRecords = new ArrayList<DeviceInfo>();
+ sniffAvailableMixers();
+ dumpAvailableMixers();
+ }
+
+ private void dumpAvailableMixers() {
+ for (DeviceInfo deviceInfo : deviceRecords) {
+ LOGGER.debug("" + deviceInfo);
+ }
+ }
+
+ /**
+ * Build device info and determine default devices.
+ */
+ private void sniffAvailableMixers() {
+ Mixer.Info[] mixers = AudioSystem.getMixerInfo();
+ for (int i = 0; i < mixers.length; i++) {
+ DeviceInfo deviceInfo = new DeviceInfo();
+
+ deviceInfo.name = mixers[i].getName();
+ Mixer mixer = AudioSystem.getMixer(mixers[i]);
+
+ Line.Info[] lines = mixer.getTargetLineInfo();
+ deviceInfo.maxInputs = scanMaxChannels(lines);
+ // Remember first device that supports input.
+ if ((defaultInputDeviceID < 0) && (deviceInfo.maxInputs > 0)) {
+ defaultInputDeviceID = i;
+ }
+
+ lines = mixer.getSourceLineInfo();
+ deviceInfo.maxOutputs = scanMaxChannels(lines);
+ // Remember first device that supports output.
+ if ((defaultOutputDeviceID < 0) && (deviceInfo.maxOutputs > 0)) {
+ defaultOutputDeviceID = i;
+ }
+
+ deviceRecords.add(deviceInfo);
+ }
+ }
+
+ private int scanMaxChannels(Line.Info[] lines) {
+ int maxChannels = 0;
+ for (Line.Info line : lines) {
+ if (line instanceof DataLine.Info) {
+ int numChannels = scanMaxChannels(((DataLine.Info) line));
+ if (numChannels > maxChannels) {
+ maxChannels = numChannels;
+ }
+ }
+ }
+ return maxChannels;
+ }
+
+ private int scanMaxChannels(DataLine.Info info) {
+ int maxChannels = 0;
+ for (AudioFormat format : info.getFormats()) {
+ int numChannels = format.getChannels();
+ if (numChannels > maxChannels) {
+ maxChannels = numChannels;
+ }
+ }
+ return maxChannels;
+ }
+
+ static class DeviceInfo {
+ String name;
+ int maxInputs;
+ int maxOutputs;
+
+ @Override
+ public String toString() {
+ return "AudioDevice: " + name + ", max in = " + maxInputs + ", max out = " + maxOutputs;
+ }
+ }
+
+ private static class JavaSoundStream {
+ AudioFormat format;
+ byte[] bytes;
+ int frameRate;
+ int deviceID;
+ int samplesPerFrame;
+
+ public JavaSoundStream(int deviceID, int frameRate, int samplesPerFrame) {
+ this.deviceID = deviceID;
+ this.frameRate = frameRate;
+ this.samplesPerFrame = samplesPerFrame;
+ format = new AudioFormat(frameRate, 16, samplesPerFrame, true, USE_BIG_ENDIAN);
+ }
+
+ Line getDataLine(DataLine.Info info) throws LineUnavailableException {
+ Line dataLine;
+ if (deviceID >= 0) {
+ Mixer.Info[] mixers = AudioSystem.getMixerInfo();
+ Mixer mixer = AudioSystem.getMixer(mixers[deviceID]);
+ dataLine = mixer.getLine(info);
+ } else {
+ dataLine = AudioSystem.getLine(info);
+ }
+ return dataLine;
+ }
+
+ int calculateBufferSize(double suggestedOutputLatency) {
+ int numFrames = (int) (suggestedOutputLatency * frameRate);
+ return numFrames * samplesPerFrame * BYTES_PER_SAMPLE;
+ }
+
+ }
+
+ private class JavaSoundOutputStream extends JavaSoundStream implements AudioDeviceOutputStream {
+ SourceDataLine line;
+
+ public JavaSoundOutputStream(int deviceID, int frameRate, int samplesPerFrame) {
+ super(deviceID, frameRate, samplesPerFrame);
+ }
+
+ @Override
+ public void start() {
+ DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
+ if (!AudioSystem.isLineSupported(info)) {
+ // Handle the error.
+ LOGGER.error("JavaSoundOutputStream - not supported." + format);
+ } else {
+ try {
+ line = (SourceDataLine) getDataLine(info);
+ int bufferSize = calculateBufferSize(suggestedOutputLatency);
+ line.open(format, bufferSize);
+ LOGGER.debug("Output buffer size = " + bufferSize + " bytes.");
+ line.start();
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ line = null;
+ }
+ }
+ }
+
+ /** Grossly inefficient. Call the array version instead. */
+ @Override
+ public void write(double value) {
+ double[] buffer = new double[1];
+ buffer[0] = value;
+ write(buffer, 0, 1);
+ }
+
+ @Override
+ public void write(double[] buffer) {
+ write(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public void write(double[] buffer, int start, int count) {
+ // Allocate byte buffer if needed.
+ if ((bytes == null) || ((bytes.length * 2) < count)) {
+ bytes = new byte[count * 2];
+ }
+
+ // Convert float samples to LittleEndian bytes.
+ int byteIndex = 0;
+ for (int i = 0; i < count; i++) {
+ // Offset before casting so that we can avoid using floor().
+ // Also round by adding 0.5 so that very small signals go to zero.
+ double temp = (32767.0 * buffer[i + start]) + 32768.5;
+ int sample = ((int) temp) - 32768;
+ if (sample > Short.MAX_VALUE) {
+ sample = Short.MAX_VALUE;
+ } else if (sample < Short.MIN_VALUE) {
+ sample = Short.MIN_VALUE;
+ }
+ bytes[byteIndex++] = (byte) sample; // little end
+ bytes[byteIndex++] = (byte) (sample >> 8); // big end
+ }
+
+ line.write(bytes, 0, byteIndex);
+ }
+
+ @Override
+ public void stop() {
+ if (line != null) {
+ line.stop();
+ line.flush();
+ line.close();
+ line = null;
+ } else {
+ new RuntimeException("AudioOutput stop attempted when no line created.")
+ .printStackTrace();
+ }
+ }
+
+ @Override
+ public double getLatency() {
+ if (line == null) {
+ return 0.0;
+ }
+ int numBytes = line.getBufferSize();
+ int numFrames = numBytes / (BYTES_PER_SAMPLE * samplesPerFrame);
+ return ((double) numFrames) / frameRate;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ }
+
+ private class JavaSoundInputStream extends JavaSoundStream implements AudioDeviceInputStream {
+ TargetDataLine line;
+
+ public JavaSoundInputStream(int deviceID, int frameRate, int samplesPerFrame) {
+ super(deviceID, frameRate, samplesPerFrame);
+ }
+
+ @Override
+ public void start() {
+ DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
+ if (!AudioSystem.isLineSupported(info)) {
+ // Handle the error.
+ LOGGER.error("JavaSoundInputStream - not supported." + format);
+ } else {
+ try {
+ line = (TargetDataLine) getDataLine(info);
+ int bufferSize = calculateBufferSize(suggestedInputLatency);
+ line.open(format, bufferSize);
+ LOGGER.debug("Input buffer size = " + bufferSize + " bytes.");
+ line.start();
+ } catch (Exception e) {
+ e.printStackTrace();
+ line = null;
+ }
+ }
+ }
+
+ @Override
+ public double read() {
+ double[] buffer = new double[1];
+ read(buffer, 0, 1);
+ return buffer[0];
+ }
+
+ @Override
+ public int read(double[] buffer) {
+ return read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read(double[] buffer, int start, int count) {
+ // Allocate byte buffer if needed.
+ if ((bytes == null) || ((bytes.length * 2) < count)) {
+ bytes = new byte[count * 2];
+ }
+ int bytesRead = line.read(bytes, 0, bytes.length);
+
+ // Convert BigEndian bytes to float samples
+ int bi = 0;
+ for (int i = 0; i < count; i++) {
+ int sample = bytes[bi++] & 0x00FF; // little end
+ sample = sample + (bytes[bi++] << 8); // big end
+ buffer[i + start] = sample * (1.0 / 32767.0);
+ }
+ return bytesRead / 4;
+ }
+
+ @Override
+ public void stop() {
+ if (line != null) {
+ line.drain();
+ line.close();
+ } else {
+ new RuntimeException("AudioInput stop attempted when no line created.")
+ .printStackTrace();
+ }
+ }
+
+ @Override
+ public double getLatency() {
+ if (line == null) {
+ return 0.0;
+ }
+ int numBytes = line.getBufferSize();
+ int numFrames = numBytes / (BYTES_PER_SAMPLE * samplesPerFrame);
+ return ((double) numFrames) / frameRate;
+ }
+
+ @Override
+ public int available() {
+ return line.available() / BYTES_PER_SAMPLE;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ }
+
+ @Override
+ public AudioDeviceOutputStream createOutputStream(int deviceID, int frameRate,
+ int samplesPerFrame) {
+ return new JavaSoundOutputStream(deviceID, frameRate, samplesPerFrame);
+ }
+
+ @Override
+ public AudioDeviceInputStream createInputStream(int deviceID, int frameRate, int samplesPerFrame) {
+ return new JavaSoundInputStream(deviceID, frameRate, samplesPerFrame);
+ }
+
+ @Override
+ public double getDefaultHighInputLatency(int deviceID) {
+ return 0.300;
+ }
+
+ @Override
+ public double getDefaultHighOutputLatency(int deviceID) {
+ return 0.300;
+ }
+
+ @Override
+ public int getDefaultInputDeviceID() {
+ return defaultInputDeviceID;
+ }
+
+ @Override
+ public int getDefaultOutputDeviceID() {
+ return defaultOutputDeviceID;
+ }
+
+ @Override
+ public double getDefaultLowInputLatency(int deviceID) {
+ return 0.100;
+ }
+
+ @Override
+ public double getDefaultLowOutputLatency(int deviceID) {
+ return 0.100;
+ }
+
+ @Override
+ public int getDeviceCount() {
+ return deviceRecords.size();
+ }
+
+ @Override
+ public String getDeviceName(int deviceID) {
+ return deviceRecords.get(deviceID).name;
+ }
+
+ @Override
+ public int getMaxInputChannels(int deviceID) {
+ return deviceRecords.get(deviceID).maxInputs;
+ }
+
+ @Override
+ public int getMaxOutputChannels(int deviceID) {
+ return deviceRecords.get(deviceID).maxOutputs;
+ }
+
+ @Override
+ public int setSuggestedOutputLatency(double latency) {
+ suggestedOutputLatency = latency;
+ return 0;
+ }
+
+ @Override
+ public int setSuggestedInputLatency(double latency) {
+ suggestedInputLatency = latency;
+ return 0;
+ }
+
+ @Override
+ public String getName() {
+ return "JavaSound";
+ }
+
+}