Merge d20819c93a
into 48e20d660a
commit
3935e2e8b1
|
@ -19,6 +19,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.
|
||||
*
|
||||
|
@ -27,6 +29,7 @@ import org.openhab.core.library.types.PercentType;
|
|||
* @author Christoph Weitkamp - Added parameter to adjust the volume
|
||||
* @author Wouter Born - Added methods for getting all sinks and sources
|
||||
* @author Miguel Álvarez - Add record method
|
||||
* @author Karel Goderis - Add multisink support
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface AudioManager {
|
||||
|
@ -51,6 +54,14 @@ public interface AudioManager {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -60,6 +71,15 @@ public interface AudioManager {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -86,6 +106,15 @@ public interface AudioManager {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -96,6 +125,16 @@ public interface AudioManager {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -113,6 +152,15 @@ public interface AudioManager {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -138,6 +186,19 @@ public interface AudioManager {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -152,6 +213,20 @@ public interface AudioManager {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -256,9 +331,10 @@ public interface AudioManager {
|
|||
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
|
||||
*/
|
||||
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.Reference;
|
||||
|
||||
import io.reactivex.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
for (String sinkId : audioManager.getSinkIds(pattern)) {
|
||||
playOnSink(sinkId, fileName, volume, console);
|
||||
}
|
||||
playOnSinks(audioManager.getSinkIds(pattern), fileName, volume, 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) {
|
||||
switch (args.length) {
|
||||
case 1:
|
||||
|
|
|
@ -24,11 +24,15 @@ import java.nio.file.Path;
|
|||
import java.text.ParseException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.sound.sampled.AudioFileFormat;
|
||||
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.utils.AudioWaveUtils;
|
||||
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.ConfigurableService;
|
||||
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.LoggerFactory;
|
||||
|
||||
import io.reactivex.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* 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 Wouter Born - Sort audio sink and source options
|
||||
* @author Miguel Álvarez - Add record from source
|
||||
* @author Karel Goderis - Add multisink support
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@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_DEFAULT_SINK = "defaultSink";
|
||||
static final String CONFIG_DEFAULT_SOURCE = "defaultSource";
|
||||
static final String AUDIO_THREADPOOL_NAME = "audio";
|
||||
|
||||
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, AudioSink> audioSinks = new ConcurrentHashMap<>();
|
||||
|
||||
private final ExecutorService pool = ThreadPoolManager.getPool(AUDIO_THREADPOOL_NAME);
|
||||
|
||||
/**
|
||||
* default settings filled through the service configuration
|
||||
*/
|
||||
|
@ -114,74 +125,192 @@ public class AudioManagerImpl implements AudioManager, ConfigOptionProvider {
|
|||
|
||||
@Override
|
||||
public void play(@Nullable AudioStream audioStream) {
|
||||
play(audioStream, null);
|
||||
playSingleSink(audioStream, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
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);
|
||||
if (sink != null) {
|
||||
Runnable restoreVolume = handleVolumeCommand(volume, sink);
|
||||
sink.processAndComplete(audioStream).exceptionally(exception -> {
|
||||
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);
|
||||
if (sink == null) {
|
||||
logger.warn("No audio sink provided for playback.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
public void playFile(String fileName) throws AudioException {
|
||||
playFile(fileName, null, null);
|
||||
playFileSingleSink(fileName, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playFile(String fileName, @Nullable PercentType volume) throws AudioException {
|
||||
playFile(fileName, null, volume);
|
||||
playFileSingleSink(fileName, null, volume);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
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.");
|
||||
File file = Path.of(OpenHAB.getConfigFolder(), SOUND_DIR, fileName).toFile();
|
||||
FileAudioStream is = new FileAudioStream(file);
|
||||
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
|
||||
public void stream(@Nullable String url) throws AudioException {
|
||||
stream(url, null);
|
||||
streamSingleSink(url, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
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
|
||||
public void playMelody(String melody) {
|
||||
playMelody(melody, null);
|
||||
playMelodySingleSink(melody, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
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);
|
||||
if (sink == null) {
|
||||
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
|
||||
public void record(int seconds, String filename, @Nullable String sourceId) throws AudioException {
|
||||
var audioSource = sourceId != null ? getSource(sourceId) : getSource();
|
||||
|
@ -359,12 +533,26 @@ public class AudioManagerImpl implements AudioManager, ConfigOptionProvider {
|
|||
|
||||
@Override
|
||||
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<>();
|
||||
|
||||
for (String sinkId : audioSinks.keySet()) {
|
||||
if (sinkId.matches(regex)) {
|
||||
matchedSinkIds.add(sinkId);
|
||||
for (String segment : pattern.split(",")) {
|
||||
segment = segment.trim();
|
||||
|
||||
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.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
@ -80,6 +81,18 @@ public class AudioSinkFake implements AudioSink {
|
|||
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() {
|
||||
return audioFormat;
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ public class Audio {
|
|||
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) {
|
||||
try {
|
||||
AudioActionService.audioManager.playFile(filename, sink);
|
||||
AudioActionService.audioManager.playFile(filename, AudioActionService.audioManager.getSinkIds(sink));
|
||||
} catch (AudioException e) {
|
||||
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 = "volume", text = "the volume to be used") PercentType volume) {
|
||||
try {
|
||||
AudioActionService.audioManager.playFile(filename, sink, volume);
|
||||
AudioActionService.audioManager.playFile(filename, AudioActionService.audioManager.getSinkIds(sink),
|
||||
volume);
|
||||
} catch (AudioException e) {
|
||||
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,
|
||||
@ParamDoc(name = "url", text = "the url of the audio stream") String url) {
|
||||
try {
|
||||
AudioActionService.audioManager.stream(url, sink);
|
||||
AudioActionService.audioManager.stream(url, AudioActionService.audioManager.getSinkIds(sink));
|
||||
} catch (AudioException e) {
|
||||
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.
|
||||
*
|
||||
*
|
||||
* @param volume
|
||||
* @return
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue