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 6f3756eef9990..ba3ba0f1c67ef 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 @@ 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.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 String getId() { * @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 String getId() { 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 void connectIfNeeded() throws IOException, InterruptedException { * 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 void disconnect() { } } + 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 void process(@Nullable AudioStream audioStream) 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 void process(@Nullable AudioStream audioStream) 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 void process(@Nullable AudioStream audioStream) } } finally { try { - if (audioInputStream != null) { - audioInputStream.close(); + if (audioInputStreamAndDuration != null) { + audioInputStreamAndDuration.inputStream.close(); } audioStream.close(); scheduleDisconnect(); @@ -219,4 +277,15 @@ public PercentType getVolume() { 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 41011f5d25497..2aae655d039da 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 void handleCommand(ChannelUID channelUID, Command command) { // 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 void setVolume(int volume) { AbstractAudioDeviceConfig device = bridge.getDevice(name); bridge.getClient().setVolumePercent(device, volume); updateState(VOLUME_CHANNEL, new PercentType(volume)); + savedVolume = volume; } @Override