[pulseaudio] Fix playing time with pulseaudio sink (#11170) (#11171)

Fixes #11170 by introducing an intelligent thread.sleep (getting the duration of the sound, if possible, then wait the appropriate time for letting the sound play). By the way, the method to get the sound duration is not as easy as I thought.

Also fix a minor issue with the last volume not propertly saved.

And fix some minor warnings by using final local variable.

Signed-off-by: Gwendal ROULLEAU <gwendal.roulleau@gmail.com>
This commit is contained in:
dalgwen 2021-09-08 21:08:05 +02:00 committed by GitHub
parent 56a0449ce1
commit 0a96792a93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 90 additions and 16 deletions

View File

@ -15,15 +15,20 @@ package org.openhab.binding.pulseaudio.internal;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -38,6 +43,7 @@ import org.openhab.core.audio.UnsupportedAudioStreamException;
import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tritonus.share.sampled.file.TAudioFileFormat;
/**
* The audio sink for openhab, implemented by a connection to a pulseaudio sink
@ -87,9 +93,29 @@ public class PulseAudioAudioSink implements AudioSink {
* @param input
* @return
*/
private @Nullable InputStream getPCMStreamFromMp3Stream(InputStream input) {
private @Nullable AudioStreamAndDuration getPCMStreamFromMp3Stream(InputStream input) {
try {
MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
int duration = -1;
if (input instanceof FixedLengthAudioStream) {
final Long audioFileLength = ((FixedLengthAudioStream) input).length();
AudioFileFormat audioFileFormat = mpegAudioFileReader.getAudioFileFormat(input);
if (audioFileFormat instanceof TAudioFileFormat) {
Map<String, Object> taudioFileFormatProperties = ((TAudioFileFormat) audioFileFormat).properties();
if (taudioFileFormatProperties.containsKey("mp3.framesize.bytes")
&& taudioFileFormatProperties.containsKey("mp3.framerate.fps")) {
Integer frameSize = (Integer) taudioFileFormatProperties.get("mp3.framesize.bytes");
Float frameRate = (Float) taudioFileFormatProperties.get("mp3.framerate.fps");
if (frameSize != null && frameRate != null) {
duration = Math.round((audioFileLength / (frameSize * frameRate)) * 1000);
}
}
}
input.reset();
}
AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input);
javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
@ -98,7 +124,8 @@ public class PulseAudioAudioSink implements AudioSink {
javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16,
sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);
return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
AudioInputStream audioInputStreamConverted = mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
return new AudioStreamAndDuration(audioInputStreamConverted, duration);
} catch (IOException | UnsupportedAudioFileException e) {
logger.warn("Cannot convert this mp3 stream to pcm stream: {}", e.getMessage());
@ -126,10 +153,11 @@ public class PulseAudioAudioSink implements AudioSink {
* Disconnect the socket to pulseaudio simple protocol
*/
public void disconnect() {
if (clientSocket != null && isIdle) {
final Socket clientSocketLocal = clientSocket;
if (clientSocketLocal != null && isIdle) {
logger.debug("Disconnecting");
try {
clientSocket.close();
clientSocketLocal.close();
} catch (IOException e) {
}
} else {
@ -137,6 +165,23 @@ public class PulseAudioAudioSink implements AudioSink {
}
}
private AudioStreamAndDuration getWavAudioAndDuration(AudioStream audioStream) {
int duration = -1;
if (audioStream instanceof FixedLengthAudioStream) {
final Long audioFileLength = ((FixedLengthAudioStream) audioStream).length();
try {
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(audioStream);
int frameSize = audioInputStream.getFormat().getFrameSize();
float frameRate = audioInputStream.getFormat().getFrameRate();
float durationInSeconds = (audioFileLength / (frameSize * frameRate));
duration = Math.round(durationInSeconds * 1000);
} catch (UnsupportedAudioFileException | IOException e) {
logger.warn("Error when getting duration information from AudioFile");
}
}
return new AudioStreamAndDuration(audioStream, duration);
}
@Override
public void process(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
@ -145,13 +190,13 @@ public class PulseAudioAudioSink implements AudioSink {
return;
}
InputStream audioInputStream = null;
AudioStreamAndDuration audioInputStreamAndDuration = null;
try {
if (AudioFormat.MP3.isCompatible(audioStream.getFormat())) {
audioInputStream = getPCMStreamFromMp3Stream(audioStream);
audioInputStreamAndDuration = getPCMStreamFromMp3Stream(audioStream);
} else if (AudioFormat.WAV.isCompatible(audioStream.getFormat())) {
audioInputStream = audioStream;
audioInputStreamAndDuration = getWavAudioAndDuration(audioStream);
} else {
throw new UnsupportedAudioFormatException("pulseaudio audio sink can only play pcm or mp3 stream",
audioStream.getFormat());
@ -160,10 +205,23 @@ public class PulseAudioAudioSink implements AudioSink {
for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
try {
connectIfNeeded();
if (audioInputStream != null && clientSocket != null) {
final Socket clientSocketLocal = clientSocket;
if (audioInputStreamAndDuration != null && clientSocketLocal != null) {
// send raw audio to the socket and to pulse audio
isIdle = false;
audioInputStream.transferTo(clientSocket.getOutputStream());
Instant start = Instant.now();
audioInputStreamAndDuration.inputStream.transferTo(clientSocketLocal.getOutputStream());
if (audioInputStreamAndDuration.duration != -1) { // ensure, if the sound has a duration
// that we let at least this time for the system to play
Instant end = Instant.now();
long millisSecondTimedToSendAudioData = Duration.between(start, end).toMillis();
if (millisSecondTimedToSendAudioData < audioInputStreamAndDuration.duration) {
long timeToSleep = audioInputStreamAndDuration.duration
- millisSecondTimedToSendAudioData;
logger.debug("Sleep time to let the system play sound : {}", timeToSleep);
Thread.sleep(timeToSleep);
}
}
break;
}
} catch (IOException e) {
@ -184,8 +242,8 @@ public class PulseAudioAudioSink implements AudioSink {
}
} finally {
try {
if (audioInputStream != null) {
audioInputStream.close();
if (audioInputStreamAndDuration != null) {
audioInputStreamAndDuration.inputStream.close();
}
audioStream.close();
scheduleDisconnect();
@ -219,4 +277,15 @@ public class PulseAudioAudioSink implements AudioSink {
public void setVolume(PercentType volume) {
pulseaudioHandler.setVolume(volume.intValue());
}
private static class AudioStreamAndDuration {
private InputStream inputStream;
private int duration;
public AudioStreamAndDuration(InputStream inputStream, int duration) {
super();
this.inputStream = inputStream;
this.duration = duration + 200; // introduce some delay
}
}
}

View File

@ -231,24 +231,28 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
// refresh to get the current volume level
bridge.getClient().update();
device = bridge.getDevice(name);
savedVolume = device.getVolume();
int oldVolume = device.getVolume();
int newVolume = oldVolume;
if (command.equals(IncreaseDecreaseType.INCREASE)) {
savedVolume = Math.min(100, savedVolume + 5);
newVolume = Math.min(100, oldVolume + 5);
}
if (command.equals(IncreaseDecreaseType.DECREASE)) {
savedVolume = Math.max(0, savedVolume - 5);
newVolume = Math.max(0, oldVolume - 5);
}
bridge.getClient().setVolumePercent(device, savedVolume);
updateState = new PercentType(savedVolume);
bridge.getClient().setVolumePercent(device, newVolume);
updateState = new PercentType(newVolume);
savedVolume = newVolume;
} else if (command instanceof PercentType) {
DecimalType volume = (DecimalType) command;
bridge.getClient().setVolumePercent(device, volume.intValue());
updateState = (PercentType) command;
savedVolume = volume.intValue();
} else if (command instanceof DecimalType) {
// set volume
DecimalType volume = (DecimalType) command;
bridge.getClient().setVolume(device, volume.intValue());
updateState = (DecimalType) command;
savedVolume = volume.intValue();
}
} else if (channelUID.getId().equals(MUTE_CHANNEL)) {
if (command instanceof OnOffType) {
@ -318,6 +322,7 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
AbstractAudioDeviceConfig device = bridge.getDevice(name);
bridge.getClient().setVolumePercent(device, volume);
updateState(VOLUME_CHANNEL, new PercentType(volume));
savedVolume = volume;
}
@Override