Skip to content

Commit

Permalink
[pulseaudio] Fix playing time with pulseaudio sink (#11170) (#11171)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
dalgwen authored Sep 8, 2021
1 parent 56a0449 commit 0a96792
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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<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();

Expand All @@ -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());
Expand Down Expand Up @@ -126,17 +153,35 @@ 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 {
logger.debug("Stream still running or socket not open");
}
}

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 {
Expand All @@ -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());
Expand All @@ -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) {
Expand All @@ -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();
Expand Down Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 0a96792

Please sign in to comment.