2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.pulseaudio.internal;
15 import java.io.IOException;
16 import java.net.Socket;
17 import java.time.Duration;
18 import java.time.Instant;
20 import java.util.concurrent.CompletableFuture;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.TimeUnit;
24 import javax.sound.sampled.UnsupportedAudioFileException;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
29 import org.openhab.core.audio.AudioFormat;
30 import org.openhab.core.audio.AudioSink;
31 import org.openhab.core.audio.AudioStream;
32 import org.openhab.core.audio.FileAudioStream;
33 import org.openhab.core.audio.UnsupportedAudioFormatException;
34 import org.openhab.core.audio.UnsupportedAudioStreamException;
35 import org.openhab.core.audio.utils.AudioSinkUtils;
36 import org.openhab.core.common.Disposable;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * The audio sink for openhab, implemented by a connection to a pulseaudio sink
43 * @author Gwendal Roulleau - Initial contribution
44 * @author Miguel Álvarez - move some code to the PulseaudioSimpleProtocolStream class so sink and source can extend
49 public class PulseAudioAudioSink extends PulseaudioSimpleProtocolStream implements AudioSink {
51 private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
53 private AudioSinkUtils audioSinkUtils;
55 private static final Set<AudioFormat> SUPPORTED_FORMATS = Set.of(AudioFormat.WAV, AudioFormat.MP3);
56 private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Set.of(AudioStream.class);
57 private static final AudioFormat TARGET_FORMAT = new AudioFormat(AudioFormat.CONTAINER_WAVE,
58 AudioFormat.CODEC_PCM_SIGNED, false, 16, 4 * 44100, 44100L, 2);
60 public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler,
61 AudioSinkUtils audioSinkUtils) {
62 super(pulseaudioHandler, scheduler);
63 this.audioSinkUtils = audioSinkUtils;
67 public void process(@Nullable AudioStream audioStream)
68 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
69 processAndComplete(audioStream);
73 public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
74 if (audioStream == null) {
75 return CompletableFuture.completedFuture(null);
78 try (ConvertedInputStream normalizedPCMStream = new ConvertedInputStream(audioStream)) {
79 for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
82 final Socket clientSocketLocal = clientSocket;
83 if (clientSocketLocal != null) {
84 // send raw audio to the socket and to pulse audio
85 Instant start = Instant.now();
86 if (normalizedPCMStream.getDuration() != -1) {
87 // ensure, if the sound has a duration
88 // that we let at least this time for the system to play
89 normalizedPCMStream.transferTo(clientSocketLocal.getOutputStream());
90 Instant end = Instant.now();
91 long millisSecondTimedToSendAudioData = Duration.between(start, end).toMillis();
92 if (millisSecondTimedToSendAudioData < normalizedPCMStream.getDuration()) {
93 CompletableFuture<@Nullable Void> soundPlayed = new CompletableFuture<>();
94 long timeToWait = normalizedPCMStream.getDuration() - millisSecondTimedToSendAudioData;
95 logger.debug("Some time to let the system play sound : {}", timeToWait);
96 scheduler.schedule(() -> soundPlayed.complete(null), timeToWait, TimeUnit.MILLISECONDS);
99 return CompletableFuture.completedFuture(null);
102 // We have a second method available to guess the duration, and it is during transfer
103 Long timeStampEnd = audioSinkUtils.transferAndAnalyzeLength(normalizedPCMStream,
104 clientSocketLocal.getOutputStream(), TARGET_FORMAT);
105 CompletableFuture<@Nullable Void> soundPlayed = new CompletableFuture<>();
106 if (timeStampEnd != null) {
107 long now = System.nanoTime();
108 long timeToWait = timeStampEnd - now;
109 if (timeToWait > 0) {
110 scheduler.schedule(() -> soundPlayed.complete(null), timeToWait,
111 TimeUnit.NANOSECONDS);
115 return CompletableFuture.completedFuture(null);
119 } catch (IOException e) {
120 disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
121 if (countAttempt == 2) { // we won't retry : log and quit
122 final Socket clientSocketLocal = clientSocket;
123 String port = clientSocketLocal != null ? Integer.toString(clientSocketLocal.getPort())
126 "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
127 pulseaudioHandler.getHost(), port, e.getMessage());
128 return CompletableFuture.completedFuture(null);
130 } catch (InterruptedException ie) {
131 logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
132 return CompletableFuture.completedFuture(null);
135 } catch (UnsupportedAudioFileException | UnsupportedAudioFormatException | IOException e) {
136 return CompletableFuture.failedFuture(new UnsupportedAudioFormatException(
137 "Cannot send sound to the pulseaudio sink", audioStream.getFormat(), e));
140 // if the stream is not needed anymore, then we should call back the AudioStream to let it a chance
142 if (audioStream instanceof Disposable disposableAudioStream) {
144 disposableAudioStream.dispose();
145 } catch (IOException e) {
146 String fileName = audioStream instanceof FileAudioStream file ? file.toString() : "unknown";
147 if (logger.isDebugEnabled()) {
148 logger.debug("Cannot dispose of stream {}", fileName, e);
150 logger.warn("Cannot dispose of stream {}, reason {}", fileName, e.getMessage());
155 return CompletableFuture.completedFuture(null);
159 public Set<AudioFormat> getSupportedFormats() {
160 return SUPPORTED_FORMATS;
164 public Set<Class<? extends AudioStream>> getSupportedStreams() {
165 return SUPPORTED_STREAMS;