X-Git-Url: https://git.pterodactylus.net/?a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsonitus%2Fdata%2Fsink%2FAudioSink.java;h=bbf24d177f9c8cb3656fa7df2a4206dd6672790c;hb=b05346e7daa46f0464a08bab0e179758ea0eb0b4;hp=f29bde8b263e4e602d0932fac13d70af9cac2b11;hpb=a669196536534aa133343982d9c7600253fbf4a8;p=sonitus.git diff --git a/src/main/java/net/pterodactylus/sonitus/data/sink/AudioSink.java b/src/main/java/net/pterodactylus/sonitus/data/sink/AudioSink.java index f29bde8..bbf24d1 100644 --- a/src/main/java/net/pterodactylus/sonitus/data/sink/AudioSink.java +++ b/src/main/java/net/pterodactylus/sonitus/data/sink/AudioSink.java @@ -17,23 +17,38 @@ package net.pterodactylus.sonitus.data.sink; -import static com.google.common.base.Preconditions.*; +import static javax.sound.sampled.BooleanControl.Type.MUTE; +import static javax.sound.sampled.FloatControl.Type.VOLUME; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; import java.util.logging.Logger; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.BooleanControl; +import javax.sound.sampled.Control; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; -import net.pterodactylus.sonitus.data.ConnectException; -import net.pterodactylus.sonitus.data.Connection; +import net.pterodactylus.sonitus.data.Controller; import net.pterodactylus.sonitus.data.Metadata; import net.pterodactylus.sonitus.data.Sink; import net.pterodactylus.sonitus.data.Source; +import net.pterodactylus.sonitus.data.controller.Fader; +import net.pterodactylus.sonitus.data.controller.Switch; +import net.pterodactylus.sonitus.data.event.MetadataUpdated; +import net.pterodactylus.sonitus.io.IntegralWriteOutputStream; + +import com.google.common.base.Preconditions; +import com.google.common.eventbus.EventBus; /** - * {@link Sink} implementation that uses the JDK’s {@link - * javax.sound.sampled.AudioSystem} to play all {@link net.pterodactylus.sonitus.data.Source}s. + * {@link Sink} implementation that uses the JDK’s {@link AudioSystem} to play + * all {@link Source}s. * * @author David ‘Bombe’ Roden */ @@ -42,47 +57,203 @@ public class AudioSink implements Sink { /** The logger. */ private static final Logger logger = Logger.getLogger(AudioSink.class.getName()); - /** The current source. */ - private Source source; + /** The event bus. */ + private final EventBus eventBus; - @Override - public void connect(Source source) throws ConnectException { - this.source = checkNotNull(source, "source must not be null"); - checkState(source.metadata().encoding().equalsIgnoreCase("PCM"), "source must be PCM-encoded"); + /** The volume fader. */ + private final Fader volumeFader; - final Metadata sourceMetadata = source.metadata(); - AudioFormat audioFormat = new AudioFormat(sourceMetadata.frequency(), 16, sourceMetadata.channels(), true, false); - try { - final SourceDataLine sourceDataLine = AudioSystem.getSourceDataLine(audioFormat); - sourceDataLine.open(audioFormat); - sourceDataLine.start(); - new Thread(new Connection(source) { + /** The “mute” switch. */ + private final Switch muteSwitch; + + /** The current metadata. */ + private Metadata metadata; + + /** The audio output. */ + private SourceDataLine sourceDataLine; + + /** A buffered output stream to ensure correct writing to the source data line. */ + private OutputStream sourceDataLineOutputStream = new IntegralWriteOutputStream(new OutputStream() { + + @Override + public void write(int b) throws IOException { + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (sourceDataLine != null) { + sourceDataLine.write(b, off, len); + } + } + }, 1024); + + /** + * Creates a new audio sink. + * + * @param eventBus + * The event bus + */ + public AudioSink(EventBus eventBus) { + this.eventBus = eventBus; + volumeFader = new Fader("Volume") { - @Override - protected int bufferSize() { - return sourceMetadata.channels() * sourceMetadata.frequency() * 2; + @Override + protected void valueSet(Double value) { + /* search for preferred volume control. */ + FloatControl volumeControl = getVolumeControl(sourceDataLine); + if (volumeControl == null) { + /* could not find volume control! */ + return; } - @Override - protected void feed(byte[] buffer) { - sourceDataLine.write(buffer, 0, buffer.length); - logger.finest(String.format("AudioSink: Wrote %d Bytes.", buffer.length)); + volumeControl.setValue((float) (value * volumeControl.getMaximum())); + } + }; + muteSwitch = new Switch("Mute") { + + /** The previous value in case we have to emulate the mute control. */ + private float previousValue; + + @Override + protected void valueSet(Boolean value) { + /* search for mute control. */ + BooleanControl muteControl = getMuteControl(sourceDataLine); + if (muteControl != null) { + muteControl.setValue(value); + return; } - @Override - protected void finish() { - sourceDataLine.stop(); + /* could not find mute control, use volume control! */ + FloatControl volumeControl = getVolumeControl(sourceDataLine); + if (volumeControl == null) { + /* no volume control, either? */ + return; } - }).start(); - metadataUpdated(); - } catch (LineUnavailableException lue1) { - throw new ConnectException(lue1); + + if (value) { + previousValue = volumeControl.getValue(); + volumeControl.setValue(0); + } else { + volumeControl.setValue(previousValue); + } + } + + }; + } + + // + // CONTROLLED METHODS + // + + @Override + public String name() { + return "Audio Output"; + } + + @Override + public Metadata metadata() { + return metadata; + } + + @Override + public List> controllers() { + return Arrays.>asList(volumeFader, muteSwitch); + } + + // + // SINK METHODS + // + + @Override + public void open(Metadata metadata) throws IOException { + Preconditions.checkArgument(metadata.encoding().equalsIgnoreCase("PCM"), "source must be PCM-encoded"); + AudioFormat audioFormat = new AudioFormat(metadata.frequency(), 16, metadata.channels(), true, false); + try { + sourceDataLine = AudioSystem.getSourceDataLine(audioFormat); + sourceDataLine.open(audioFormat); + sourceDataLine.start(); + metadataUpdated(metadata); + } catch (LineUnavailableException e) { + /* TODO */ + throw new IOException(e); } } @Override - public void metadataUpdated() { - /* ignore. */ + public void close() { + sourceDataLine.stop(); + sourceDataLine.close(); + } + + @Override + public void metadataUpdated(Metadata metadata) { + logger.info(String.format("Now playing %s.", metadata)); + this.metadata = metadata; + eventBus.post(new MetadataUpdated(this, metadata)); + } + + @Override + public void process(byte[] buffer) throws IOException { + sourceDataLineOutputStream.write(buffer); + logger.finest(String.format("AudioSink: Wrote %d Bytes.", buffer.length)); + } + + // + // PRIVATE METHODS + // + + /** + * Returns the {@link FloatControl.Type.VOLUME} control. + * + * @param dataLine + * The data line to search for the control + * @return The control, or {@code null} if no volume control could be found + */ + private static FloatControl getVolumeControl(DataLine dataLine) { + return getControl(dataLine, VOLUME, FloatControl.class); + } + + /** + * Returns the {@link BooleanControl.Type.MUTE} control. + * + * @param dataLine + * The data line to search for the control + * @return The control, or {@code null} if no mute control could be found + */ + private static BooleanControl getMuteControl(DataLine dataLine) { + return getControl(dataLine, MUTE, BooleanControl.class); + } + + /** + * Searches the given data line for a control of the given type and returns it. + * If the given data line is {@code null}, {@code null} is returned. + * + * @param dataLine + * The data line to search for a control + * @param controlType + * The type of the control to search + * @param controlClass + * The class of the control + * @param + * The class of the control + * @return The control, or {@code null} if no control could be found + */ + private static T getControl(DataLine dataLine, Control.Type controlType, Class controlClass) { + if (dataLine == null) { + return null; + } + Control[] controls = dataLine.getControls(); + for (Control control : controls) { + if (control.getType().equals(controlType)) { + return (T) control; + } + } + return null; } }