Merge d20819c93a
into 48e20d660a
commit
3935e2e8b1
|
@ -19,6 +19,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.core.library.types.PercentType;
|
import org.openhab.core.library.types.PercentType;
|
||||||
|
|
||||||
|
import io.reactivex.annotations.NonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service provides functionality around audio services and is the central service to be used directly by others.
|
* This service provides functionality around audio services and is the central service to be used directly by others.
|
||||||
*
|
*
|
||||||
|
@ -27,6 +29,7 @@ import org.openhab.core.library.types.PercentType;
|
||||||
* @author Christoph Weitkamp - Added parameter to adjust the volume
|
* @author Christoph Weitkamp - Added parameter to adjust the volume
|
||||||
* @author Wouter Born - Added methods for getting all sinks and sources
|
* @author Wouter Born - Added methods for getting all sinks and sources
|
||||||
* @author Miguel Álvarez - Add record method
|
* @author Miguel Álvarez - Add record method
|
||||||
|
* @author Karel Goderis - Add multisink support
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public interface AudioManager {
|
public interface AudioManager {
|
||||||
|
@ -51,6 +54,14 @@ public interface AudioManager {
|
||||||
*/
|
*/
|
||||||
void play(@Nullable AudioStream audioStream, @Nullable String sinkId);
|
void play(@Nullable AudioStream audioStream, @Nullable String sinkId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the passed audio stream on the given set of sinks.
|
||||||
|
*
|
||||||
|
* @param audioStream The audio stream to play or null if streaming should be stopped
|
||||||
|
* @param sinkIds The set of the audio sink ids to use
|
||||||
|
*/
|
||||||
|
void play(@Nullable AudioStream audioStream, @NonNull Set<String> sinkIds);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays the passed audio stream on the given sink.
|
* Plays the passed audio stream on the given sink.
|
||||||
*
|
*
|
||||||
|
@ -60,6 +71,15 @@ public interface AudioManager {
|
||||||
*/
|
*/
|
||||||
void play(@Nullable AudioStream audioStream, @Nullable String sinkId, @Nullable PercentType volume);
|
void play(@Nullable AudioStream audioStream, @Nullable String sinkId, @Nullable PercentType volume);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the passed audio stream on the given set of sinks.
|
||||||
|
*
|
||||||
|
* @param audioStream The audio stream to play or null if streaming should be stopped
|
||||||
|
* @param sinkIds The set of the audio sink ids to use
|
||||||
|
* @param volume The volume to be used or null if the default notification volume should be used
|
||||||
|
*/
|
||||||
|
void play(@Nullable AudioStream audioStream, Set<String> sinkIds, @Nullable PercentType volume);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays an audio file from the "sounds" folder using the default audio sink.
|
* Plays an audio file from the "sounds" folder using the default audio sink.
|
||||||
*
|
*
|
||||||
|
@ -86,6 +106,15 @@ public interface AudioManager {
|
||||||
*/
|
*/
|
||||||
void playFile(String fileName, @Nullable String sinkId) throws AudioException;
|
void playFile(String fileName, @Nullable String sinkId) throws AudioException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays an audio file from the "sounds" folder using the given audio sink.
|
||||||
|
*
|
||||||
|
* @param fileName The file from the "sounds" folder
|
||||||
|
* @param sinkIds The set of the audio sink ids to use
|
||||||
|
* @throws AudioException in case the file does not exist or cannot be opened
|
||||||
|
*/
|
||||||
|
void playFile(String fileName, @NonNull Set<String> sinkIds) throws AudioException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays an audio file with the given volume from the "sounds" folder using the given audio sink.
|
* Plays an audio file with the given volume from the "sounds" folder using the given audio sink.
|
||||||
*
|
*
|
||||||
|
@ -96,6 +125,16 @@ public interface AudioManager {
|
||||||
*/
|
*/
|
||||||
void playFile(String fileName, @Nullable String sinkId, @Nullable PercentType volume) throws AudioException;
|
void playFile(String fileName, @Nullable String sinkId, @Nullable PercentType volume) throws AudioException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays an audio file with the given volume from the "sounds" folder using the given audio sink.
|
||||||
|
*
|
||||||
|
* @param fileName The file from the "sounds" folder
|
||||||
|
* @param sinkIds The set of the audio sink ids to use
|
||||||
|
* @param volume The volume to be used or null if the default notification volume should be used
|
||||||
|
* @throws AudioException in case the file does not exist or cannot be opened
|
||||||
|
*/
|
||||||
|
void playFile(String fileName, @NonNull Set<String> sinkIds, @Nullable PercentType volume) throws AudioException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stream audio from the passed url using the default audio sink.
|
* Stream audio from the passed url using the default audio sink.
|
||||||
*
|
*
|
||||||
|
@ -113,6 +152,15 @@ public interface AudioManager {
|
||||||
*/
|
*/
|
||||||
void stream(@Nullable String url, @Nullable String sinkId) throws AudioException;
|
void stream(@Nullable String url, @Nullable String sinkId) throws AudioException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream audio from the passed url to the given set of sinks
|
||||||
|
*
|
||||||
|
* @param url The url to stream from or null if streaming should be stopped
|
||||||
|
* @param sinkIds The set of the audio sink ids to use
|
||||||
|
* @throws AudioException in case the url stream cannot be opened
|
||||||
|
*/
|
||||||
|
void stream(@Nullable String url, @NonNull Set<String> sinkIds) throws AudioException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse and synthesize a melody and play it into the default sink.
|
* Parse and synthesize a melody and play it into the default sink.
|
||||||
*
|
*
|
||||||
|
@ -138,6 +186,19 @@ public interface AudioManager {
|
||||||
*/
|
*/
|
||||||
void playMelody(String melody, @Nullable String sinkId);
|
void playMelody(String melody, @Nullable String sinkId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and synthesize a melody and play it into the given sink.
|
||||||
|
*
|
||||||
|
* The melody should be a spaced separated list of note names or silences (character 0 or O).
|
||||||
|
* You can optionally add the character "'" to increase the note one octave.
|
||||||
|
* You can optionally add ":ms" where ms is an int value to customize the note/silence milliseconds duration
|
||||||
|
* (defaults to 200ms).
|
||||||
|
*
|
||||||
|
* @param melody The url to stream from or null if streaming should be stopped.
|
||||||
|
* @param sinkIds The set of the audio sink ids to use
|
||||||
|
*/
|
||||||
|
void playMelody(String melody, @NonNull Set<String> sinkIds);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse and synthesize a melody and play it into the given sink at the desired volume.
|
* Parse and synthesize a melody and play it into the given sink at the desired volume.
|
||||||
*
|
*
|
||||||
|
@ -152,6 +213,20 @@ public interface AudioManager {
|
||||||
*/
|
*/
|
||||||
void playMelody(String melody, @Nullable String sinkId, @Nullable PercentType volume);
|
void playMelody(String melody, @Nullable String sinkId, @Nullable PercentType volume);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and synthesize a melody and play it into the given sink at the desired volume.
|
||||||
|
*
|
||||||
|
* The melody should be a spaced separated list of note names or silences (character 0 or O).
|
||||||
|
* You can optionally add the character "'" to increase the note one octave.
|
||||||
|
* You can optionally add ":ms" where ms is an int value to customize the note/silence milliseconds duration
|
||||||
|
* (defaults to 200ms).
|
||||||
|
*
|
||||||
|
* @param melody The url to stream from or null if streaming should be stopped.
|
||||||
|
* @param sinkIds The set of the audio sink ids to use
|
||||||
|
* @param volume The volume to be used or null if the default notification volume should be used
|
||||||
|
*/
|
||||||
|
void playMelody(String melody, @NonNull Set<String> sinkIds, @Nullable PercentType volume);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record audio as a WAV file of the specified length to the sounds folder.
|
* Record audio as a WAV file of the specified length to the sounds folder.
|
||||||
*
|
*
|
||||||
|
@ -256,9 +331,10 @@ public interface AudioManager {
|
||||||
AudioSink getSink(@Nullable String sinkId);
|
AudioSink getSink(@Nullable String sinkId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of sink ids that match a given pattern
|
* Get a list of sink ids that match a given set of patterns
|
||||||
*
|
*
|
||||||
* @param pattern pattern to search, can include {@code *} and {@code ?} placeholders
|
* @param pattern patterns to search; patterns is a comma-separated list, whereby each can include {@code *} and
|
||||||
|
* {@code ?} placeholders, or can be literal string to designate a single sink
|
||||||
* @return ids of matching sinks
|
* @return ids of matching sinks
|
||||||
*/
|
*/
|
||||||
Set<String> getSinkIds(String pattern);
|
Set<String> getSinkIds(String pattern);
|
||||||
|
|
|
@ -35,6 +35,8 @@ import org.osgi.service.component.annotations.Activate;
|
||||||
import org.osgi.service.component.annotations.Component;
|
import org.osgi.service.component.annotations.Component;
|
||||||
import org.osgi.service.component.annotations.Reference;
|
import org.osgi.service.component.annotations.Reference;
|
||||||
|
|
||||||
|
import io.reactivex.annotations.NonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Console command extension for all audio features.
|
* Console command extension for all audio features.
|
||||||
*
|
*
|
||||||
|
@ -237,9 +239,7 @@ public class AudioConsoleCommandExtension extends AbstractConsoleCommandExtensio
|
||||||
}
|
}
|
||||||
|
|
||||||
private void playOnSinks(String pattern, String fileName, @Nullable PercentType volume, Console console) {
|
private void playOnSinks(String pattern, String fileName, @Nullable PercentType volume, Console console) {
|
||||||
for (String sinkId : audioManager.getSinkIds(pattern)) {
|
playOnSinks(audioManager.getSinkIds(pattern), fileName, volume, console);
|
||||||
playOnSink(sinkId, fileName, volume, console);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void playOnSink(@Nullable String sinkId, String fileName, @Nullable PercentType volume, Console console) {
|
private void playOnSink(@Nullable String sinkId, String fileName, @Nullable PercentType volume, Console console) {
|
||||||
|
@ -251,6 +251,16 @@ public class AudioConsoleCommandExtension extends AbstractConsoleCommandExtensio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void playOnSinks(@NonNull Set<String> sinkIds, String fileName, @Nullable PercentType volume,
|
||||||
|
Console console) {
|
||||||
|
try {
|
||||||
|
audioManager.playFile(fileName, sinkIds, volume);
|
||||||
|
} catch (AudioException e) {
|
||||||
|
console.println(Objects.requireNonNullElse(e.getMessage(),
|
||||||
|
String.format("An error occurred while playing '%s' on sinks '%s'", fileName, sinkIds)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void stream(String[] args, Console console) {
|
private void stream(String[] args, Console console) {
|
||||||
switch (args.length) {
|
switch (args.length) {
|
||||||
case 1:
|
case 1:
|
||||||
|
|
|
@ -24,11 +24,15 @@ import java.nio.file.Path;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.sound.sampled.AudioFileFormat;
|
import javax.sound.sampled.AudioFileFormat;
|
||||||
import javax.sound.sampled.AudioInputStream;
|
import javax.sound.sampled.AudioInputStream;
|
||||||
|
@ -47,6 +51,7 @@ import org.openhab.core.audio.FileAudioStream;
|
||||||
import org.openhab.core.audio.URLAudioStream;
|
import org.openhab.core.audio.URLAudioStream;
|
||||||
import org.openhab.core.audio.utils.AudioWaveUtils;
|
import org.openhab.core.audio.utils.AudioWaveUtils;
|
||||||
import org.openhab.core.audio.utils.ToneSynthesizer;
|
import org.openhab.core.audio.utils.ToneSynthesizer;
|
||||||
|
import org.openhab.core.common.ThreadPoolManager;
|
||||||
import org.openhab.core.config.core.ConfigOptionProvider;
|
import org.openhab.core.config.core.ConfigOptionProvider;
|
||||||
import org.openhab.core.config.core.ConfigurableService;
|
import org.openhab.core.config.core.ConfigurableService;
|
||||||
import org.openhab.core.config.core.ParameterOption;
|
import org.openhab.core.config.core.ParameterOption;
|
||||||
|
@ -62,6 +67,8 @@ import org.osgi.service.component.annotations.ReferencePolicy;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import io.reactivex.annotations.NonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service provides functionality around audio services and is the central service to be used directly by others.
|
* This service provides functionality around audio services and is the central service to be used directly by others.
|
||||||
*
|
*
|
||||||
|
@ -71,6 +78,7 @@ import org.slf4j.LoggerFactory;
|
||||||
* @author Christoph Weitkamp - Added parameter to adjust the volume
|
* @author Christoph Weitkamp - Added parameter to adjust the volume
|
||||||
* @author Wouter Born - Sort audio sink and source options
|
* @author Wouter Born - Sort audio sink and source options
|
||||||
* @author Miguel Álvarez - Add record from source
|
* @author Miguel Álvarez - Add record from source
|
||||||
|
* @author Karel Goderis - Add multisink support
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@Component(immediate = true, configurationPid = "org.openhab.audio", //
|
@Component(immediate = true, configurationPid = "org.openhab.audio", //
|
||||||
|
@ -82,6 +90,7 @@ public class AudioManagerImpl implements AudioManager, ConfigOptionProvider {
|
||||||
static final String CONFIG_URI = "system:audio";
|
static final String CONFIG_URI = "system:audio";
|
||||||
static final String CONFIG_DEFAULT_SINK = "defaultSink";
|
static final String CONFIG_DEFAULT_SINK = "defaultSink";
|
||||||
static final String CONFIG_DEFAULT_SOURCE = "defaultSource";
|
static final String CONFIG_DEFAULT_SOURCE = "defaultSource";
|
||||||
|
static final String AUDIO_THREADPOOL_NAME = "audio";
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(AudioManagerImpl.class);
|
private final Logger logger = LoggerFactory.getLogger(AudioManagerImpl.class);
|
||||||
|
|
||||||
|
@ -89,6 +98,8 @@ public class AudioManagerImpl implements AudioManager, ConfigOptionProvider {
|
||||||
private final Map<String, AudioSource> audioSources = new ConcurrentHashMap<>();
|
private final Map<String, AudioSource> audioSources = new ConcurrentHashMap<>();
|
||||||
private final Map<String, AudioSink> audioSinks = new ConcurrentHashMap<>();
|
private final Map<String, AudioSink> audioSinks = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final ExecutorService pool = ThreadPoolManager.getPool(AUDIO_THREADPOOL_NAME);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* default settings filled through the service configuration
|
* default settings filled through the service configuration
|
||||||
*/
|
*/
|
||||||
|
@ -114,74 +125,192 @@ public class AudioManagerImpl implements AudioManager, ConfigOptionProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void play(@Nullable AudioStream audioStream) {
|
public void play(@Nullable AudioStream audioStream) {
|
||||||
play(audioStream, null);
|
playSingleSink(audioStream, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void play(@Nullable AudioStream audioStream, @Nullable String sinkId) {
|
public void play(@Nullable AudioStream audioStream, @Nullable String sinkId) {
|
||||||
play(audioStream, sinkId, null);
|
playSingleSink(audioStream, sinkId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void play(@Nullable AudioStream audioStream, @NonNull Set<String> sinkIds) {
|
||||||
|
playMultiSink(audioStream, sinkIds, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void play(@Nullable AudioStream audioStream, @Nullable String sinkId, @Nullable PercentType volume) {
|
public void play(@Nullable AudioStream audioStream, @Nullable String sinkId, @Nullable PercentType volume) {
|
||||||
|
playSingleSink(audioStream, sinkId, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void play(@Nullable AudioStream audioStream, Set<String> sinkIds, @Nullable PercentType volume) {
|
||||||
|
playMultiSink(audioStream, sinkIds, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void playSingleSink(@Nullable AudioStream audioStream, @Nullable String sinkId,
|
||||||
|
@Nullable PercentType volume) {
|
||||||
AudioSink sink = getSink(sinkId);
|
AudioSink sink = getSink(sinkId);
|
||||||
if (sink != null) {
|
if (sink == null) {
|
||||||
Runnable restoreVolume = handleVolumeCommand(volume, sink);
|
logger.warn("No audio sink provided for playback.");
|
||||||
sink.processAndComplete(audioStream).exceptionally(exception -> {
|
return;
|
||||||
logger.warn("Error playing '{}': {}", audioStream, exception.getMessage(), exception);
|
|
||||||
return null;
|
|
||||||
}).thenRun(restoreVolume);
|
|
||||||
} else {
|
|
||||||
logger.warn("Failed playing audio stream '{}' as no audio sink was found.", audioStream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle volume adjustment for the current sink
|
||||||
|
Runnable restoreVolume = handleVolumeCommand(volume, sink);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Process and complete playback asynchronously
|
||||||
|
sink.processAndComplete(audioStream).exceptionally(exception -> {
|
||||||
|
logger.error("Error playing audio stream '{}' on sink '{}': {}", audioStream, sinkId,
|
||||||
|
exception.getMessage(), exception);
|
||||||
|
return null; // Handle the exception gracefully
|
||||||
|
}).thenRun(() -> {
|
||||||
|
restoreVolume.run(); // Ensure volume is restored after playback completes
|
||||||
|
logger.info("Audio stream '{}' has been successfully played on sink '{}'.", audioStream, sinkId);
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error while processing audio stream '{}' on sink '{}': {}", audioStream, sinkId,
|
||||||
|
e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void playMultiSink(@Nullable AudioStream audioStream, Set<String> sinkIds, @Nullable PercentType volume) {
|
||||||
|
if (sinkIds.isEmpty()) {
|
||||||
|
logger.warn("No audio sinks provided for playback.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a list of CompletableFutures for parallel execution
|
||||||
|
List<CompletableFuture<Object>> futures = sinkIds.stream().map(sinkId -> CompletableFuture.supplyAsync(() -> {
|
||||||
|
AudioSink sink = getSink(sinkId);
|
||||||
|
if (sink == null) {
|
||||||
|
logger.warn("Sink '{}' not found. Skipping.", sinkId);
|
||||||
|
return null; // Return null for missing sinks
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle volume adjustment for the current sink
|
||||||
|
Runnable restoreVolume = handleVolumeCommand(volume, sink);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Play the audio stream synchronously on this sink
|
||||||
|
sink.processAndComplete(audioStream);
|
||||||
|
logger.debug("Audio stream '{}' has been played on sink '{}'.", audioStream, sinkId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error playing '{}' on sink '{}': {}", audioStream, sinkId, e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
restoreVolume.run(); // Ensure volume is restored after playback completes
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, pool)).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Wait for all sinks to complete playback
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||||
|
.thenRun(() -> logger.info("Audio stream '{}' has been played on all sinks.", audioStream))
|
||||||
|
.exceptionally(exception -> {
|
||||||
|
logger.error("Error completing playback on all sinks: {}", exception.getMessage(), exception);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playFile(String fileName) throws AudioException {
|
public void playFile(String fileName) throws AudioException {
|
||||||
playFile(fileName, null, null);
|
playFileSingleSink(fileName, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playFile(String fileName, @Nullable PercentType volume) throws AudioException {
|
public void playFile(String fileName, @Nullable PercentType volume) throws AudioException {
|
||||||
playFile(fileName, null, volume);
|
playFileSingleSink(fileName, null, volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playFile(String fileName, @Nullable String sinkId) throws AudioException {
|
public void playFile(String fileName, @Nullable String sinkId) throws AudioException {
|
||||||
playFile(fileName, sinkId, null);
|
playFileSingleSink(fileName, sinkId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playFile(String fileName, @NonNull Set<String> sinkIds) throws AudioException {
|
||||||
|
playFileMultipleSink(fileName, sinkIds, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playFile(String fileName, @Nullable String sinkId, @Nullable PercentType volume) throws AudioException {
|
public void playFile(String fileName, @Nullable String sinkId, @Nullable PercentType volume) throws AudioException {
|
||||||
|
playFileSingleSink(fileName, sinkId, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playFile(String fileName, @NonNull Set<String> sinkIds, @Nullable PercentType volume)
|
||||||
|
throws AudioException {
|
||||||
|
playFileMultipleSink(fileName, sinkIds, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void playFileSingleSink(String fileName, @Nullable String sinkId, @Nullable PercentType volume)
|
||||||
|
throws AudioException {
|
||||||
Objects.requireNonNull(fileName, "File cannot be played as fileName is null.");
|
Objects.requireNonNull(fileName, "File cannot be played as fileName is null.");
|
||||||
File file = Path.of(OpenHAB.getConfigFolder(), SOUND_DIR, fileName).toFile();
|
File file = Path.of(OpenHAB.getConfigFolder(), SOUND_DIR, fileName).toFile();
|
||||||
FileAudioStream is = new FileAudioStream(file);
|
FileAudioStream is = new FileAudioStream(file);
|
||||||
play(is, sinkId, volume);
|
play(is, sinkId, volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void playFileMultipleSink(String fileName, @NonNull Set<String> sinkIds, @Nullable PercentType volume)
|
||||||
|
throws AudioException {
|
||||||
|
Objects.requireNonNull(fileName, "File cannot be played as fileName is null.");
|
||||||
|
File file = Path.of(OpenHAB.getConfigFolder(), SOUND_DIR, fileName).toFile();
|
||||||
|
FileAudioStream is = new FileAudioStream(file);
|
||||||
|
play(is, sinkIds, volume);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stream(@Nullable String url) throws AudioException {
|
public void stream(@Nullable String url) throws AudioException {
|
||||||
stream(url, null);
|
streamSingleSink(url, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stream(@Nullable String url, @Nullable String sinkId) throws AudioException {
|
public void stream(@Nullable String url, @Nullable String sinkId) throws AudioException {
|
||||||
|
streamSingleSink(url, sinkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stream(@Nullable String url, @NonNull Set<String> sinkIds) throws AudioException {
|
||||||
|
streamMultipleSink(url, sinkIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void streamSingleSink(@Nullable String url, @Nullable String sinkId) throws AudioException {
|
||||||
AudioStream audioStream = url != null ? new URLAudioStream(url) : null;
|
AudioStream audioStream = url != null ? new URLAudioStream(url) : null;
|
||||||
play(audioStream, sinkId, null);
|
play(audioStream, sinkId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void streamMultipleSink(@Nullable String url, @NonNull Set<String> sinkIds) throws AudioException {
|
||||||
|
AudioStream audioStream = url != null ? new URLAudioStream(url) : null;
|
||||||
|
play(audioStream, sinkIds, null);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playMelody(String melody) {
|
public void playMelody(String melody) {
|
||||||
playMelody(melody, null);
|
playMelodySingleSink(melody, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playMelody(String melody, @Nullable String sinkId) {
|
public void playMelody(String melody, @Nullable String sinkId) {
|
||||||
playMelody(melody, sinkId, null);
|
playMelodySingleSink(melody, sinkId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playMelody(String melody, @NonNull Set<String> sinkIds) {
|
||||||
|
playMelodyMultiSink(melody, sinkIds, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playMelody(String melody, @Nullable String sinkId, @Nullable PercentType volume) {
|
public void playMelody(String melody, @Nullable String sinkId, @Nullable PercentType volume) {
|
||||||
|
playMelodySingleSink(melody, sinkId, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playMelody(String melody, @NonNull Set<String> sinkIds, @Nullable PercentType volume) {
|
||||||
|
playMelodyMultiSink(melody, sinkIds, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void playMelodySingleSink(String melody, @Nullable String sinkId, @Nullable PercentType volume) {
|
||||||
AudioSink sink = getSink(sinkId);
|
AudioSink sink = getSink(sinkId);
|
||||||
if (sink == null) {
|
if (sink == null) {
|
||||||
logger.warn("Failed playing melody as no audio sink {} was found.", sinkId);
|
logger.warn("Failed playing melody as no audio sink {} was found.", sinkId);
|
||||||
|
@ -201,6 +330,51 @@ public class AudioManagerImpl implements AudioManager, ConfigOptionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void playMelodyMultiSink(String melody, @NonNull Set<String> sinkIds, @Nullable PercentType volume) {
|
||||||
|
|
||||||
|
if (sinkIds.isEmpty()) {
|
||||||
|
logger.warn("Failed playing melody as no audio sinks were provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a list of CompletableFutures for parallel execution
|
||||||
|
List<CompletableFuture<Object>> futures = sinkIds.stream().map(sinkId -> CompletableFuture.supplyAsync(() -> {
|
||||||
|
AudioSink sink = getSink(sinkId);
|
||||||
|
if (sink == null) {
|
||||||
|
logger.warn("Sink '{}' not found. Skipping.", sinkId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var synthesizerFormat = AudioFormat.getBestMatch(ToneSynthesizer.getSupportedFormats(),
|
||||||
|
sink.getSupportedFormats());
|
||||||
|
if (synthesizerFormat == null) {
|
||||||
|
logger.warn("Sink '{}' does not support the required audio format. Skipping.", sinkId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Generate the audio stream for the melody
|
||||||
|
var audioStream = new ToneSynthesizer(synthesizerFormat).getStream(ToneSynthesizer.parseMelody(melody));
|
||||||
|
|
||||||
|
// Play the melody on this sink asynchronously
|
||||||
|
play(audioStream, sinkId, volume);
|
||||||
|
logger.debug("Melody '{}' has been played on sink '{}'.", melody, sinkId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Error playing melody on sink '{}': {}", sinkId, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, pool)).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Wait for all sinks to complete playback
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||||
|
.thenRun(() -> logger.info("Melody '{}' has been played on all sinks.", melody))
|
||||||
|
.exceptionally(exception -> {
|
||||||
|
logger.error("Error completing playback on all sinks: {}", exception.getMessage(), exception);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void record(int seconds, String filename, @Nullable String sourceId) throws AudioException {
|
public void record(int seconds, String filename, @Nullable String sourceId) throws AudioException {
|
||||||
var audioSource = sourceId != null ? getSource(sourceId) : getSource();
|
var audioSource = sourceId != null ? getSource(sourceId) : getSource();
|
||||||
|
@ -359,12 +533,26 @@ public class AudioManagerImpl implements AudioManager, ConfigOptionProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getSinkIds(String pattern) {
|
public Set<String> getSinkIds(String pattern) {
|
||||||
String regex = pattern.replace("?", ".?").replace("*", ".*?");
|
|
||||||
|
if (pattern.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Input cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
Set<String> matchedSinkIds = new HashSet<>();
|
Set<String> matchedSinkIds = new HashSet<>();
|
||||||
|
|
||||||
for (String sinkId : audioSinks.keySet()) {
|
for (String segment : pattern.split(",")) {
|
||||||
if (sinkId.matches(regex)) {
|
segment = segment.trim();
|
||||||
matchedSinkIds.add(sinkId);
|
|
||||||
|
if (segment.contains("*") || segment.contains("?")) {
|
||||||
|
String regex = segment.replace("?", ".?").replace("*", ".*?");
|
||||||
|
|
||||||
|
for (String sinkId : audioSinks.keySet()) {
|
||||||
|
if (sinkId.matches(regex)) {
|
||||||
|
matchedSinkIds.add(sinkId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matchedSinkIds.add(segment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ package org.openhab.core.audio.internal.fake;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
@ -80,6 +81,18 @@ public class AudioSinkFake implements AudioSink {
|
||||||
isStreamProcessed = true;
|
isStreamProcessed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
|
||||||
|
this.audioStream = audioStream;
|
||||||
|
if (audioStream != null) {
|
||||||
|
audioFormat = audioStream.getFormat();
|
||||||
|
} else {
|
||||||
|
isStreamStopped = true;
|
||||||
|
}
|
||||||
|
isStreamProcessed = true;
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
public @Nullable AudioFormat getAudioFormat() {
|
public @Nullable AudioFormat getAudioFormat() {
|
||||||
return audioFormat;
|
return audioFormat;
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ public class Audio {
|
||||||
public static void playSound(@ParamDoc(name = "sink", text = "the id of the sink") String sink,
|
public static void playSound(@ParamDoc(name = "sink", text = "the id of the sink") String sink,
|
||||||
@ParamDoc(name = "filename", text = "the filename with extension") String filename) {
|
@ParamDoc(name = "filename", text = "the filename with extension") String filename) {
|
||||||
try {
|
try {
|
||||||
AudioActionService.audioManager.playFile(filename, sink);
|
AudioActionService.audioManager.playFile(filename, AudioActionService.audioManager.getSinkIds(sink));
|
||||||
} catch (AudioException e) {
|
} catch (AudioException e) {
|
||||||
logger.warn("Failed playing audio file: {}", e.getMessage());
|
logger.warn("Failed playing audio file: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,8 @@ public class Audio {
|
||||||
@ParamDoc(name = "filename", text = "the filename with extension") String filename,
|
@ParamDoc(name = "filename", text = "the filename with extension") String filename,
|
||||||
@ParamDoc(name = "volume", text = "the volume to be used") PercentType volume) {
|
@ParamDoc(name = "volume", text = "the volume to be used") PercentType volume) {
|
||||||
try {
|
try {
|
||||||
AudioActionService.audioManager.playFile(filename, sink, volume);
|
AudioActionService.audioManager.playFile(filename, AudioActionService.audioManager.getSinkIds(sink),
|
||||||
|
volume);
|
||||||
} catch (AudioException e) {
|
} catch (AudioException e) {
|
||||||
logger.warn("Failed playing audio file: {}", e.getMessage());
|
logger.warn("Failed playing audio file: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
@ -103,7 +104,7 @@ public class Audio {
|
||||||
public static synchronized void playStream(@ParamDoc(name = "sink", text = "the id of the sink") String sink,
|
public static synchronized void playStream(@ParamDoc(name = "sink", text = "the id of the sink") String sink,
|
||||||
@ParamDoc(name = "url", text = "the url of the audio stream") String url) {
|
@ParamDoc(name = "url", text = "the url of the audio stream") String url) {
|
||||||
try {
|
try {
|
||||||
AudioActionService.audioManager.stream(url, sink);
|
AudioActionService.audioManager.stream(url, AudioActionService.audioManager.getSinkIds(sink));
|
||||||
} catch (AudioException e) {
|
} catch (AudioException e) {
|
||||||
logger.warn("Failed streaming audio url: {}", e.getMessage());
|
logger.warn("Failed streaming audio url: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
@ -167,7 +168,7 @@ public class Audio {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a float volume to a {@link PercentType} volume and checks if float volume is in the [0;1] range.
|
* Converts a float volume to a {@link PercentType} volume and checks if float volume is in the [0;1] range.
|
||||||
*
|
*
|
||||||
* @param volume
|
* @param volume
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue