From 0a96792a93e382a3f4daddb4d020916328dc6444 Mon Sep 17 00:00:00 2001 From: dalgwen Date: Wed, 8 Sep 2021 21:08:05 +0200 Subject: [PATCH] [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 --- .../internal/PulseAudioAudioSink.java | 91 ++++++++++++++++--- .../internal/handler/PulseaudioHandler.java | 15 ++- 2 files changed, 90 insertions(+), 16 deletions(-) diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java index 6f3756eef..ba3ba0f1c 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java @@ -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 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 + } + } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java index 41011f5d2..2aae655d0 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java @@ -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