[Javasound] Fix microphone data line sharing on Windows OS (#2890)

Fix microphone dataline sharing on Windows OS

Signed-off-by: Miguel Álvarez Díez <miguelwork92@gmail.com>
This commit is contained in:
GiviMAD
2022-04-26 20:06:29 +02:00
committed by GitHub
parent 0ea6feef72
commit 89bc6592da
2 changed files with 115 additions and 45 deletions
@@ -12,9 +12,16 @@
*/
package org.openhab.core.audio.internal.javasound;
import java.util.HashSet;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.TargetDataLine;
@@ -25,7 +32,10 @@ import org.openhab.core.audio.AudioException;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioSource;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.common.ThreadPoolManager;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is an AudioSource from an input channel of the host.
@@ -39,6 +49,8 @@ import org.osgi.service.component.annotations.Component;
@Component(service = AudioSource.class, immediate = true)
public class JavaSoundAudioSource implements AudioSource {
private final Logger logger = LoggerFactory.getLogger(JavaSoundAudioSource.class);
/**
* Java Sound audio format
*/
@@ -61,14 +73,22 @@ public class JavaSoundAudioSource implements AudioSource {
private @Nullable TargetDataLine microphone;
/**
* Set for control microphone close on Windows OS
* Set for control microphone sharing on Windows OS
*/
private final Set<Object> openStreamRefs = new HashSet<>();
private final ConcurrentLinkedQueue<PipedOutputStream> openStreamRefs = new ConcurrentLinkedQueue<>();
/**
* Task for writing microphone data to each of the open sources on Windows OS
*/
private @Nullable Future<?> pipeWriteTask;
private final ScheduledExecutorService executor;
/**
* Constructs a JavaSoundAudioSource
*/
public JavaSoundAudioSource() {
executor = ThreadPoolManager.getScheduledPool("OH-core-javasound-source");
}
private TargetDataLine initMicrophone(javax.sound.sampled.AudioFormat format) throws AudioException {
@@ -82,34 +102,107 @@ public class JavaSoundAudioSource implements AudioSource {
}
@Override
public synchronized AudioStream getInputStream(AudioFormat expectedFormat) throws AudioException {
public AudioStream getInputStream(AudioFormat expectedFormat) throws AudioException {
if (!expectedFormat.isCompatible(audioFormat)) {
throw new AudioException("Cannot produce streams in format " + expectedFormat);
}
// on OSs other than windows we can open multiple lines for the microphone
if (!windowsOS) {
return new JavaSoundInputStream(initMicrophone(format), audioFormat);
TargetDataLine microphone = initMicrophone(format);
var inputStream = new JavaSoundInputStream((InputStream) microphone, audioFormat);
microphone.start();
return inputStream;
}
// on Windows OS we share the microphone line
var ref = new Object();
TargetDataLine microphoneDataLine;
synchronized (openStreamRefs) {
microphoneDataLine = this.microphone;
if (microphoneDataLine == null) {
microphoneDataLine = initMicrophone(format);
this.microphone = microphoneDataLine;
TargetDataLine microphone = this.microphone;
if (microphone == null) {
microphone = initMicrophone(format);
this.microphone = microphone;
}
openStreamRefs.add(ref);
var pipedOutputStream = new PipedOutputStream();
PipedInputStream pipedInputStream;
try {
pipedInputStream = new PipedInputStream(pipedOutputStream, 1024 * 10) {
@Override
public void close() throws IOException {
unregisterPipe(pipedOutputStream);
super.close();
}
};
} catch (IOException ie) {
throw new AudioException("Cannot open stream pipe: " + ie.getMessage());
}
openStreamRefs.add(pipedOutputStream);
var inputStream = new JavaSoundInputStream(pipedInputStream, audioFormat);
microphone.start();
startPipeWrite();
return inputStream;
}
return new JavaSoundInputStream(microphoneDataLine, audioFormat, () -> {
synchronized (openStreamRefs) {
var microphone = this.microphone;
if (openStreamRefs.remove(ref) && openStreamRefs.isEmpty() && microphone != null) {
}
private void startPipeWrite() {
if (this.pipeWriteTask == null) {
this.pipeWriteTask = executor.submit(() -> {
int lengthRead;
byte[] buffer = new byte[1024];
while (!openStreamRefs.isEmpty()) {
TargetDataLine stream = this.microphone;
if (stream != null) {
try {
lengthRead = stream.read(buffer, 0, buffer.length);
for (PipedOutputStream output : openStreamRefs) {
try {
output.write(buffer, 0, lengthRead);
if (openStreamRefs.contains(output)) {
output.flush();
}
} catch (IOException e) {
if (e instanceof InterruptedIOException && openStreamRefs.isEmpty()) {
// task has been ended while writing
return;
}
logger.warn("IOException while writing to source pipe: {}", e.getMessage());
} catch (RuntimeException e) {
logger.warn("RuntimeException while writing to source pipe: {}", e.getMessage());
}
}
} catch (RuntimeException e) {
logger.warn("RuntimeException while reading from JavaSound source: {}", e.getMessage());
}
} else {
logger.warn("Unable access to microphone stream");
}
}
this.pipeWriteTask = null;
});
}
}
private void unregisterPipe(PipedOutputStream pipedOutputStream) {
synchronized (openStreamRefs) {
openStreamRefs.remove(pipedOutputStream);
try {
Thread.sleep(0);
} catch (InterruptedException ignored) {
}
if (openStreamRefs.isEmpty()) {
Future<?> pipeWriteTask = this.pipeWriteTask;
if (pipeWriteTask != null) {
pipeWriteTask.cancel(true);
this.pipeWriteTask = null;
}
TargetDataLine microphone = this.microphone;
if (microphone != null) {
microphone.close();
this.microphone = null;
}
}
});
try {
pipedOutputStream.close();
} catch (IOException ignored) {
}
}
}
@Override
@@ -13,8 +13,7 @@
package org.openhab.core.audio.internal.javasound;
import java.io.IOException;
import javax.sound.sampled.TargetDataLine;
import java.io.InputStream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -31,32 +30,19 @@ import org.openhab.core.audio.AudioStream;
public class JavaSoundInputStream extends AudioStream {
/**
* TargetDataLine for the input
* InputStream for the input
*/
private final TargetDataLine input;
private final InputStream input;
private final AudioFormat format;
private final @Nullable JavaSoundInputStreamCloseHandler closeHandler;
/**
* Constructs a JavaSoundInputStream with the passed input
*
* @param input The mic which data is pulled from
*/
public JavaSoundInputStream(TargetDataLine input, AudioFormat format) {
this(input, format, null);
}
/**
* Constructs a JavaSoundInputStream with the passed input and a close handler.
*
* @param input The mic which data is pulled from
*/
public JavaSoundInputStream(TargetDataLine input, AudioFormat format,
@Nullable JavaSoundInputStreamCloseHandler closeHandler) {
public JavaSoundInputStream(InputStream input, AudioFormat format) {
this.format = format;
this.closeHandler = closeHandler;
this.input = input;
this.input.start();
}
@Override
@@ -86,20 +72,11 @@ public class JavaSoundInputStream extends AudioStream {
@Override
public void close() throws IOException {
var closeHandler = this.closeHandler;
if (closeHandler != null) {
closeHandler.onStreamClosed();
} else {
input.close();
}
input.close();
}
@Override
public AudioFormat getFormat() {
return format;
}
public interface JavaSoundInputStreamCloseHandler {
void onStreamClosed();
}
}