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;
19 import java.util.HashSet;
21 import java.util.concurrent.ScheduledExecutorService;
23 import javax.sound.sampled.UnsupportedAudioFileException;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
28 import org.openhab.core.audio.AudioFormat;
29 import org.openhab.core.audio.AudioSink;
30 import org.openhab.core.audio.AudioStream;
31 import org.openhab.core.audio.FixedLengthAudioStream;
32 import org.openhab.core.audio.UnsupportedAudioFormatException;
33 import org.openhab.core.audio.UnsupportedAudioStreamException;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * The audio sink for openhab, implemented by a connection to a pulseaudio sink
40 * @author Gwendal Roulleau - Initial contribution
41 * @author Miguel Álvarez - move some code to the PulseaudioSimpleProtocolStream class so sink and source can extend
46 public class PulseAudioAudioSink extends PulseaudioSimpleProtocolStream implements AudioSink {
48 private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
50 private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
51 private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
54 SUPPORTED_FORMATS.add(AudioFormat.WAV);
55 SUPPORTED_FORMATS.add(AudioFormat.MP3);
56 SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
59 public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
60 super(pulseaudioHandler, scheduler);
64 public void process(@Nullable AudioStream audioStream)
65 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
66 if (audioStream == null) {
70 try (ConvertedInputStream normalizedPCMStream = new ConvertedInputStream(audioStream)) {
71 for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
74 final Socket clientSocketLocal = clientSocket;
75 if (clientSocketLocal != null) {
76 // send raw audio to the socket and to pulse audio
77 Instant start = Instant.now();
78 normalizedPCMStream.transferTo(clientSocketLocal.getOutputStream());
79 if (normalizedPCMStream.getDuration() != -1) { // ensure, if the sound has a duration
80 // that we let at least this time for the system to play
81 Instant end = Instant.now();
82 long millisSecondTimedToSendAudioData = Duration.between(start, end).toMillis();
83 if (millisSecondTimedToSendAudioData < normalizedPCMStream.getDuration()) {
84 long timeToSleep = normalizedPCMStream.getDuration() - millisSecondTimedToSendAudioData;
85 logger.debug("Sleep time to let the system play sound : {}", timeToSleep);
86 Thread.sleep(timeToSleep);
91 } catch (IOException e) {
92 disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
93 if (countAttempt == 2) { // we won't retry : log and quit
94 final Socket clientSocketLocal = clientSocket;
95 String port = clientSocketLocal != null ? Integer.toString(clientSocketLocal.getPort())
98 "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
99 pulseaudioHandler.getHost(), port, e.getMessage());
102 } catch (InterruptedException ie) {
103 logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
107 } catch (UnsupportedAudioFileException | IOException e) {
108 throw new UnsupportedAudioFormatException("Cannot send sound to the pulseaudio sink",
109 audioStream.getFormat(), e);
116 public Set<AudioFormat> getSupportedFormats() {
117 return SUPPORTED_FORMATS;
121 public Set<Class<? extends AudioStream>> getSupportedStreams() {
122 return SUPPORTED_STREAMS;