Merge remote-tracking branch 'github/master'
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 4 Jun 2013 19:16:48 +0000 (21:16 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 4 Jun 2013 19:16:48 +0000 (21:16 +0200)
45 files changed:
src/main/java/net/pterodactylus/sonitus/data/AbstractControlledComponent.java [deleted file]
src/main/java/net/pterodactylus/sonitus/data/AbstractFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sonitus/data/ControlledComponent.java [deleted file]
src/main/java/net/pterodactylus/sonitus/data/Filter.java
src/main/java/net/pterodactylus/sonitus/data/Metadata.java
src/main/java/net/pterodactylus/sonitus/data/MetadataListener.java
src/main/java/net/pterodactylus/sonitus/data/Pipeline.java
src/main/java/net/pterodactylus/sonitus/data/Sink.java [deleted file]
src/main/java/net/pterodactylus/sonitus/data/Source.java [deleted file]
src/main/java/net/pterodactylus/sonitus/data/event/SourceFinishedEvent.java [deleted file]
src/main/java/net/pterodactylus/sonitus/data/filter/AudioProcessingFilter.java
src/main/java/net/pterodactylus/sonitus/data/filter/DummyFilter.java [deleted file]
src/main/java/net/pterodactylus/sonitus/data/filter/ExternalFilter.java
src/main/java/net/pterodactylus/sonitus/data/filter/OggVorbisDecoder.java
src/main/java/net/pterodactylus/sonitus/data/filter/PredicateFilter.java [deleted file]
src/main/java/net/pterodactylus/sonitus/data/filter/RateLimitingFilter.java
src/main/java/net/pterodactylus/sonitus/data/filter/SoxResampleFilter.java
src/main/java/net/pterodactylus/sonitus/data/filter/StereoSeparationFilter.java
src/main/java/net/pterodactylus/sonitus/data/filter/TimeCounterFilter.java
src/main/java/net/pterodactylus/sonitus/data/filter/VolumeFilter.java
src/main/java/net/pterodactylus/sonitus/data/sink/AudioSink.java
src/main/java/net/pterodactylus/sonitus/data/sink/FileSink.java
src/main/java/net/pterodactylus/sonitus/data/sink/Icecast2Sink.java
src/main/java/net/pterodactylus/sonitus/data/source/FileSource.java
src/main/java/net/pterodactylus/sonitus/data/source/MultiSource.java
src/main/java/net/pterodactylus/sonitus/data/source/SourceFinishedListener.java
src/main/java/net/pterodactylus/sonitus/data/source/StreamSource.java
src/main/java/net/pterodactylus/sonitus/gui/ComponentInfoPanel.java [deleted file]
src/main/java/net/pterodactylus/sonitus/gui/FilterInfoPanel.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sonitus/gui/MainWindow.java
src/main/java/net/pterodactylus/sonitus/gui/PipelinePanel.java
src/main/java/net/pterodactylus/sonitus/gui/SwitchPanel.java
src/main/java/net/pterodactylus/sonitus/io/FlacIdentifier.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sonitus/io/IdentifyingInputStream.java
src/main/java/net/pterodactylus/sonitus/io/ProcessingOutputStream.java
src/main/java/net/pterodactylus/sonitus/io/RememberingInputStream.java
src/main/java/net/pterodactylus/sonitus/io/flac/BlockType.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sonitus/io/flac/Data.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sonitus/io/flac/Header.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sonitus/io/flac/MetadataBlock.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sonitus/io/flac/Stream.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sonitus/io/flac/StreamInfo.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sonitus/io/mp3/Frame.java
src/main/java/net/pterodactylus/sonitus/io/mp3/Parser.java
src/test/java/net/pterodactylus/sonitus/io/RememberingInputStreamTest.java [new file with mode: 0644]

diff --git a/src/main/java/net/pterodactylus/sonitus/data/AbstractControlledComponent.java b/src/main/java/net/pterodactylus/sonitus/data/AbstractControlledComponent.java
deleted file mode 100644 (file)
index fe338c1..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Sonitus - AbstractControlledComponent.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sonitus.data;
-
-import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
-
-import com.google.common.collect.Lists;
-
-/**
- * Abstract {@link ControlledComponent} implementation that takes care of
- * managing {@link MetadataListener}s.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public abstract class AbstractControlledComponent implements ControlledComponent {
-
-       /** The name of this filter. */
-       private final String name;
-
-       /** The list of metadata listeners. */
-       private final List<MetadataListener> metadataListeners = Lists.newCopyOnWriteArrayList();
-
-       /** The current metadata. */
-       private final AtomicReference<Metadata> metadata = new AtomicReference<Metadata>();
-
-       /**
-        * Creates a new abstract controlled component.
-        *
-        * @param name
-        *              The name of the component
-        */
-       protected AbstractControlledComponent(String name) {
-               this.name = name;
-       }
-
-       //
-       // LISTENER MANAGEMENT
-       //
-
-       @Override
-       public void addMetadataListener(MetadataListener metadataListener) {
-               metadataListeners.add(metadataListener);
-       }
-
-       @Override
-       public void removeMetadataListener(MetadataListener metadataListener) {
-               metadataListeners.remove(metadataListener);
-       }
-
-       //
-       // CONTROLLEDCOMPONENT METHODS
-       //
-
-       @Override
-       public String name() {
-               return name;
-       }
-
-       @Override
-       public Metadata metadata() {
-               return metadata.get();
-       }
-
-       @Override
-       public void metadataUpdated(Metadata metadata) {
-               if (metadata.equals(this.metadata.get())) {
-                       return;
-               }
-               this.metadata.set(metadata);
-               fireMetadataUpdated(metadata);
-       }
-
-       //
-       // EVENT METHODS
-       //
-
-       /**
-        * Notifies all registered metadata listeners that the metadata has changed.
-        *
-        * @param metadata
-        *              The new metadata
-        */
-       protected void fireMetadataUpdated(Metadata metadata) {
-               for (MetadataListener metadataListener : metadataListeners) {
-                       metadataListener.metadataUpdated(this, metadata);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sonitus/data/AbstractFilter.java b/src/main/java/net/pterodactylus/sonitus/data/AbstractFilter.java
new file mode 100644 (file)
index 0000000..6fa6511
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Sonitus - AbstractFilter.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.data;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.google.common.collect.Lists;
+import com.google.common.io.Closeables;
+
+/**
+ * Abstract {@link Filter} implementation that takes care of managing {@link
+ * MetadataListener}s and pipes its input to its output.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractFilter implements Filter {
+
+       /** The name of this filter. */
+       private final String name;
+
+       /** The list of metadata listeners. */
+       private final List<MetadataListener> metadataListeners = Lists.newCopyOnWriteArrayList();
+
+       /** The current metadata. */
+       private final AtomicReference<Metadata> metadata = new AtomicReference<Metadata>();
+
+       /** The input stream from which to read. */
+       private InputStream inputStream;
+
+       /** The output stream to which to write. */
+       private OutputStream outputStream;
+
+       /**
+        * Creates a new abstract filter.
+        *
+        * @param name
+        *              The name of the filter
+        */
+       protected AbstractFilter(String name) {
+               this.name = name;
+       }
+
+       //
+       // LISTENER MANAGEMENT
+       //
+
+       @Override
+       public void addMetadataListener(MetadataListener metadataListener) {
+               metadataListeners.add(metadataListener);
+       }
+
+       @Override
+       public void removeMetadataListener(MetadataListener metadataListener) {
+               metadataListeners.remove(metadataListener);
+       }
+
+       //
+       // FILTER METHODS
+       //
+
+       @Override
+       public String name() {
+               return name;
+       }
+
+       @Override
+       public List<Controller<?>> controllers() {
+               return Collections.emptyList();
+       }
+
+       @Override
+       public Metadata metadata() {
+               return metadata.get();
+       }
+
+       @Override
+       public void metadataUpdated(Metadata metadata) {
+               if (metadata.equals(this.metadata.get())) {
+                       return;
+               }
+               this.metadata.set(metadata);
+               fireMetadataUpdated(metadata);
+       }
+
+       @Override
+       public void open(Metadata metadata) throws IOException {
+               metadataUpdated(metadata);
+               inputStream = createInputStream();
+               outputStream = createOutputStream();
+       }
+
+       @Override
+       public void close() {
+               try {
+                       Closeables.close(outputStream, true);
+                       Closeables.close(inputStream, true);
+               } catch (IOException e) {
+                       /* won’t throw. */
+               }
+       }
+
+       @Override
+       public void process(byte[] buffer) throws IOException {
+               outputStream.write(buffer);
+               outputStream.flush();
+       }
+
+       @Override
+       public byte[] get(int bufferSize) throws IOException {
+               byte[] buffer = new byte[bufferSize];
+               int read = inputStream.read(buffer);
+               if (read == -1) {
+                       throw new EOFException();
+               }
+               return Arrays.copyOf(buffer, read);
+       }
+
+       //
+       // EVENT METHODS
+       //
+
+       /**
+        * Notifies all registered metadata listeners that the metadata has changed.
+        *
+        * @param metadata
+        *              The new metadata
+        */
+       protected void fireMetadataUpdated(Metadata metadata) {
+               for (MetadataListener metadataListener : metadataListeners) {
+                       metadataListener.metadataUpdated(this, metadata);
+               }
+       }
+
+       //
+       // SUBCLASS METHODS
+       //
+
+       /**
+        * Creates the input stream from which {@link net.pterodactylus.sonitus.data.Pipeline}
+        * will read the audio data. If you override this, you have to override {@link
+        * #createOutputStream()}, too!
+        *
+        * @return The input stream to read from
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       protected InputStream createInputStream() throws IOException {
+               return new PipedInputStream();
+       }
+
+       /**
+        * Creates the output stream to which {@link net.pterodactylus.sonitus.data.Pipeline}
+        * will write the audio data. If you override this, you have to override {@link
+        * #createInputStream()}, too!
+        *
+        * @return The output stream to write to
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       protected OutputStream createOutputStream() throws IOException {
+               return new PipedOutputStream((PipedInputStream) inputStream);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sonitus/data/ControlledComponent.java b/src/main/java/net/pterodactylus/sonitus/data/ControlledComponent.java
deleted file mode 100644 (file)
index d3fde8e..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Sonitus - Controlled.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sonitus.data;
-
-import java.util.List;
-
-/**
- * Interface for components that can be controlled externally in some way.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface ControlledComponent {
-
-       /**
-        * Adds the given listener to the list of registered listeners.
-        *
-        * @param metadataListener
-        *              The metadata listener to add
-        */
-       void addMetadataListener(MetadataListener metadataListener);
-
-       /**
-        * Removes the given listener from the list of registered listeners.
-        *
-        * @param metadataListener
-        *              The metadata listener to remove
-        */
-       void removeMetadataListener(MetadataListener metadataListener);
-
-       /**
-        * Returns the name of this controlled component.
-        *
-        * @return The name of this controlled component
-        */
-       public String name();
-
-       /**
-        * Returns the current metadata of this component.
-        *
-        * @return The current metadata of this component
-        */
-       public Metadata metadata();
-
-       /**
-        * Returns the controllers offered by this component.
-        *
-        * @return The controllers of this component
-        */
-       public List<Controller<?>> controllers();
-
-       /**
-        * Notifies the sink that the metadata of the audio stream has changed. This
-        * method should return as fast as possible, i.e. every heavy lifting should be
-        * done from another thread.
-        *
-        * @param metadata
-        *              The new metadata
-        */
-       void metadataUpdated(Metadata metadata);
-
-}
index c251cfe..ec8fb94 100644 (file)
 
 package net.pterodactylus.sonitus.data;
 
+import java.io.IOException;
+import java.util.List;
+
 /**
- * A filter is both a {@link Source} and a {@link Sink}. It is used to process
+ * A filter is both a source and a sink for audio data. It is used to process
  * the audio date in whatever way seems appropriate.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public interface Filter extends ControlledComponent, Source, Sink {
+public interface Filter {
+
+       /**
+        * Adds the given listener to the list of registered listeners.
+        *
+        * @param metadataListener
+        *              The metadata listener to add
+        */
+       void addMetadataListener(MetadataListener metadataListener);
+
+       /**
+        * Removes the given listener from the list of registered listeners.
+        *
+        * @param metadataListener
+        *              The metadata listener to remove
+        */
+       void removeMetadataListener(MetadataListener metadataListener);
+
+       /**
+        * Returns the name of this filter.
+        *
+        * @return The name of this filter
+        */
+       String name();
+
+       /**
+        * Returns the controllers offered by this filter.
+        *
+        * @return The controllers of this filter
+        */
+       List<Controller<?>> controllers();
+
+       /**
+        * Returns the metadata of the audio stream.
+        *
+        * @return The metadata of the audio stream
+        */
+       Metadata metadata();
+
+       /**
+        * Notifies the sink that the metadata of the audio stream has changed. This
+        * method should return as fast as possible, i.e. every heavy lifting should be
+        * done from another thread.
+        *
+        * @param metadata
+        *              The new metadata
+        */
+       void metadataUpdated(Metadata metadata);
+
+       /**
+        * Retrieves data from the audio stream.
+        *
+        * @param bufferSize
+        *              The maximum amount of bytes to retrieve from the audio stream
+        * @return A buffer filled with up to {@code bufferSize} bytes of data; the
+        *         returned buffer may contain less data than requested but will not
+        *         contain excess elements
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       byte[] get(int bufferSize) throws IOException;
+
+       /**
+        * Opens this sink using the format parameters of the given metadata.
+        *
+        * @param metadata
+        *              The metadata of the stream
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       void open(Metadata metadata) throws IOException;
+
+       /** Closes this sink. */
+       void close();
+
+       /**
+        * Processes the given buffer of data.
+        *
+        * @param buffer
+        *              The data to process
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       void process(byte[] buffer) throws IOException;
 
 }
index 91acad4..61c0100 100644 (file)
@@ -30,6 +30,9 @@ import com.google.common.base.Optional;
  */
 public class Metadata {
 
+       /** Marker for unknown metadata. */
+       public static final Metadata UNKNOWN = new Metadata();
+
        /** The format metadata. */
        private final FormatMetadata formatMetadata;
 
index 01efe78..6b88ddf 100644 (file)
@@ -25,13 +25,13 @@ package net.pterodactylus.sonitus.data;
 public interface MetadataListener {
 
        /**
-        * Notifies a listener when the metadata of the given component was updated.
+        * Notifies a listener when the metadata of the given filter was updated.
         *
-        * @param component
-        *              The component whose metadata was updated
+        * @param filter
+        *              The filter whose metadata was updated
         * @param metadata
         *              The new metadata
         */
-       void metadataUpdated(ControlledComponent component, Metadata metadata);
+       void metadataUpdated(Filter filter, Metadata metadata);
 
 }
index ab6500e..5bbcd63 100644 (file)
@@ -36,26 +36,27 @@ import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.util.concurrent.MoreExecutors;
 
 /**
- * A pipeline is responsible for streaming audio data from a {@link Source} to
- * an arbitrary number of connected {@link Filter}s and {@link Sink}s.
+ * A pipeline is responsible for streaming audio data from a {@link Filter} to
+ * an arbitrary number of connected {@link Filter}s.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Pipeline implements Iterable<ControlledComponent> {
+public class Pipeline implements Iterable<Filter> {
 
        /** The logger. */
        private static final Logger logger = Logger.getLogger(Pipeline.class.getName());
 
        /** The source of the audio stream. */
-       private final Source source;
+       private final Filter source;
 
-       /** The sinks for each source. */
-       private final Multimap<Source, Sink> sinks;
+       /** The filters for each source. */
+       private final ListMultimap<Filter, Filter> filters;
 
        /** All started connections. */
        private final List<Connection> connections = Lists.newArrayList();
@@ -65,23 +66,21 @@ public class Pipeline implements Iterable<ControlledComponent> {
         *
         * @param source
         *              The source of the audio stream
-        * @param sinks
-        *              The sinks for each source
+        * @param filters
+        *              The filters for each source
         */
-       private Pipeline(Source source, Multimap<Source, Sink> sinks) {
+       private Pipeline(Filter source, Multimap<Filter, Filter> filters) {
                this.source = Preconditions.checkNotNull(source, "source must not be null");
-               this.sinks = Preconditions.checkNotNull(sinks, "sinks must not be null");
-               for (ControlledComponent component : Lists.reverse(components())) {
-                       logger.finest(String.format("Adding Listener to %s.", component.name()));
-                       component.addMetadataListener(new MetadataListener() {
+               this.filters = ArrayListMultimap.create(Preconditions.checkNotNull(filters, "filters must not be null"));
+               for (Filter filter : Lists.reverse(filters())) {
+                       logger.finest(String.format("Adding Listener to %s.", filter.name()));
+                       filter.addMetadataListener(new MetadataListener() {
+
                                @Override
-                               public void metadataUpdated(ControlledComponent component, Metadata metadata) {
-                                       if (!(component instanceof Source)) {
-                                               return;
-                                       }
-                                       for (ControlledComponent controlledComponent : sinks((Source) component)) {
-                                               logger.fine(String.format("Updating Metadata from %s to %s as %s.", component.name(), controlledComponent.name(), metadata));
-                                               controlledComponent.metadataUpdated(metadata);
+                               public void metadataUpdated(Filter filter, Metadata metadata) {
+                                       for (Filter sinks : filters(filter)) {
+                                               logger.fine(String.format("Updating Metadata from %s to %s as %s.", filter.name(), sinks.name(), metadata));
+                                               sinks.metadataUpdated(metadata);
                                        }
                                }
                        });
@@ -97,38 +96,37 @@ public class Pipeline implements Iterable<ControlledComponent> {
         *
         * @return This pipeline’s source
         */
-       public Source source() {
+       public Filter source() {
                return source;
        }
 
        /**
-        * Returns all {@link Sink}s (or {@link Filter}s, really) that are connected to
-        * the given source.
+        * Returns all {@link Filter}s that are connected to the given filter.
         *
-        * @param source
-        *              The source to get the sinks for
-        * @return The sinks connected to the given source, or an empty list if the
-        *         source does not exist in this pipeline
+        * @param filter
+        *              The filter to get the connected filters for
+        * @return The filters connected to the given filter, or an empty list if the
+        *         filter does not exist in this pipeline, or is not connected to any filters
         */
-       public Collection<Sink> sinks(Source source) {
-               return sinks.get(source);
+       public List<Filter> filters(Filter filter) {
+               return filters.get(filter);
        }
 
        /**
-        * Returns the traffic counters of the given controlled component.
+        * Returns the traffic counters of the given filter.
         *
-        * @param controlledComponent
-        *              The controlled component to get the traffic counters for
-        * @return The traffic counters for the given controlled component
+        * @param filter
+        *              The filter to get the traffic counters for
+        * @return The traffic counters for the given filter
         */
-       public TrafficCounter trafficCounter(ControlledComponent controlledComponent) {
+       public TrafficCounter trafficCounter(Filter filter) {
                long input = -1;
                long output = -1;
                for (Connection connection : connections) {
                        /* the connection where the source matches knows the output. */
-                       if (connection.source.equals(controlledComponent)) {
+                       if (connection.source.equals(filter)) {
                                output = connection.counter();
-                       } else if (connection.sinks.contains(controlledComponent)) {
+                       } else if (connection.sinks.contains(filter)) {
                                input = connection.counter();
                        }
                }
@@ -143,7 +141,7 @@ public class Pipeline implements Iterable<ControlledComponent> {
         * Starts the pipeline.
         *
         * @throws IOException
-        *              if any of the sinks can not be opened
+        *              if any of the filters can not be opened
         * @throws IllegalStateException
         *              if the pipeline is already running
         */
@@ -151,26 +149,24 @@ public class Pipeline implements Iterable<ControlledComponent> {
                if (!connections.isEmpty()) {
                        throw new IllegalStateException("Pipeline is already running!");
                }
-               List<Source> sources = Lists.newArrayList();
-               sources.add(source);
+               List<Filter> filters = Lists.newArrayList();
+               filters.add(source);
                /* collect all source->sink pairs. */
-               while (!sources.isEmpty()) {
-                       Source source = sources.remove(0);
-                       Collection<Sink> sinks = this.sinks.get(source);
-                       connections.add(new Connection(source, sinks));
-                       for (Sink sink : sinks) {
+               while (!filters.isEmpty()) {
+                       Filter filter = filters.remove(0);
+                       Collection<Filter> sinks = this.filters.get(filter);
+                       connections.add(new Connection(filter, sinks));
+                       for (Filter sink : sinks) {
                                logger.info(String.format("Opening %s with %s...", sink.name(), source.metadata()));
-                               sink.open(source.metadata());
-                               if (sink instanceof Filter) {
-                                       sources.add((Source) sink);
-                               }
+                               sink.open(filter.metadata());
+                               filters.add(sink);
                        }
                }
                for (Connection connection : connections) {
-                       String threadName = String.format("%s → %s.", connection.source.name(), FluentIterable.from(connection.sinks).transform(new Function<Sink, String>() {
+                       String threadName = String.format("%s → %s.", connection.source.name(), FluentIterable.from(connection.sinks).transform(new Function<Filter, String>() {
 
                                @Override
-                               public String apply(Sink sink) {
+                               public String apply(Filter sink) {
                                        return sink.name();
                                }
                        }));
@@ -194,8 +190,8 @@ public class Pipeline implements Iterable<ControlledComponent> {
        //
 
        @Override
-       public Iterator<ControlledComponent> iterator() {
-               return components().iterator();
+       public Iterator<Filter> iterator() {
+               return filters().iterator();
        }
 
        //
@@ -203,26 +199,24 @@ public class Pipeline implements Iterable<ControlledComponent> {
        //
 
        /**
-        * Returns all components of this pipeline, listed breadth-first, starting with
+        * Returns all filters of this pipeline, listed breadth-first, starting with
         * the source.
         *
-        * @return All components of this pipeline
+        * @return All filters of this pipeline
         */
-       public List<ControlledComponent> components() {
-               ImmutableList.Builder<ControlledComponent> components = ImmutableList.builder();
-               List<ControlledComponent> currentComponents = Lists.newArrayList();
-               components.add(source);
-               currentComponents.add(source);
-               while (!currentComponents.isEmpty()) {
-                       Collection<Sink> sinks = this.sinks((Source) currentComponents.remove(0));
-                       for (Sink sink : sinks) {
-                               components.add(sink);
-                               if (sink instanceof Source) {
-                                       currentComponents.add(sink);
-                               }
+       public List<Filter> filters() {
+               ImmutableList.Builder<Filter> filters = ImmutableList.builder();
+               List<Filter> remainingFilters = Lists.newArrayList();
+               filters.add(source);
+               remainingFilters.add(source);
+               while (!remainingFilters.isEmpty()) {
+                       Collection<Filter> sinks = this.filters(remainingFilters.remove(0));
+                       for (Filter sink : sinks) {
+                               filters.add(sink);
+                               remainingFilters.add(sink);
                        }
                }
-               return components.build();
+               return filters.build();
        }
 
        //
@@ -236,7 +230,7 @@ public class Pipeline implements Iterable<ControlledComponent> {
         *              The source at which to start
         * @return A builder for a new pipeline
         */
-       public static Builder builder(Source source) {
+       public static Builder builder(Filter source) {
                return new Builder(source);
        }
 
@@ -248,13 +242,13 @@ public class Pipeline implements Iterable<ControlledComponent> {
        public static class Builder {
 
                /** The source of the pipeline. */
-               private final Source source;
+               private final Filter source;
 
-               /** The sinks to which each source streams. */
-               private Multimap<Source, Sink> nextSinks = ArrayListMultimap.create();
+               /** The filters to which each source streams. */
+               private Multimap<Filter, Filter> nextSinks = ArrayListMultimap.create();
 
                /** The last added source. */
-               private Source lastSource;
+               private Filter lastSource;
 
                /**
                 * Creates a new builder.
@@ -262,31 +256,27 @@ public class Pipeline implements Iterable<ControlledComponent> {
                 * @param source
                 *              The source that starts the pipeline
                 */
-               private Builder(Source source) {
+               private Builder(Filter source) {
                        this.source = source;
                        lastSource = source;
                }
 
                /**
-                * Adds a {@link Sink} (or {@link Filter} as a recipient for the last added
-                * {@link Source}.
+                * Adds a {@link Filter} as a recipient for the last added source.
                 *
                 * @param sink
                 *              The sink to add
                 * @return This builder
-                * @throws IllegalStateException
-                *              if the last added {@link Sink} was not also a {@link Source}
                 */
-               public Builder to(Sink sink) {
-                       Preconditions.checkState(lastSource != null, "last added Sink was not a Source");
+               public Builder to(Filter sink) {
                        nextSinks.put(lastSource, sink);
-                       lastSource = (sink instanceof Filter) ? (Source) sink : null;
+                       lastSource = sink;
                        return this;
                }
 
                /**
                 * Locates the given source and sets it as the last added node so that the
-                * next invocation of {@link #to(Sink)} can “fork” the pipeline.
+                * next invocation of {@link #to(Filter)} can “fork” the pipeline.
                 *
                 * @param source
                 *              The source to locate
@@ -294,7 +284,7 @@ public class Pipeline implements Iterable<ControlledComponent> {
                 * @throws IllegalStateException
                 *              if the given source was not previously added as a sink
                 */
-               public Builder find(Source source) {
+               public Builder find(Filter source) {
                        Preconditions.checkState(nextSinks.containsValue(source));
                        lastSource = source;
                        return this;
@@ -312,8 +302,8 @@ public class Pipeline implements Iterable<ControlledComponent> {
        }
 
        /**
-        * A connection is responsible for streaming audio from one {@link Source} to
-        * an arbitrary number of {@link Sink}s it is connected to. A connection is
+        * A connection is responsible for streaming audio from one {@link Filter} to
+        * an arbitrary number of {@link Filter}s it is connected to. A connection is
         * started by creating a {@link Thread} wrapping it and starting said thread.
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
@@ -321,10 +311,10 @@ public class Pipeline implements Iterable<ControlledComponent> {
        public class Connection implements Runnable {
 
                /** The source. */
-               private final Source source;
+               private final Filter source;
 
-               /** The sinks. */
-               private final Collection<Sink> sinks;
+               /** The filters. */
+               private final Collection<Filter> sinks;
 
                /** Whether the feeder was stopped. */
                private final AtomicBoolean stopped = new AtomicBoolean(false);
@@ -344,9 +334,9 @@ public class Pipeline implements Iterable<ControlledComponent> {
                 * @param source
                 *              The source of the stream
                 * @param sinks
-                *              The sinks to which to stream
+                *              The filters to which to stream
                 */
-               public Connection(Source source, Collection<Sink> sinks) {
+               public Connection(Filter source, Collection<Filter> sinks) {
                        this.source = source;
                        this.sinks = sinks;
                        if (sinks.size() < 2) {
@@ -406,10 +396,10 @@ public class Pipeline implements Iterable<ControlledComponent> {
                                        } catch (IOException ioe1) {
                                                throw new IOException(String.format("I/O error while reading from %s.", source.name()), ioe1);
                                        }
-                                       List<Future<Void>> futures = executorService.invokeAll(FluentIterable.from(sinks).transform(new Function<Sink, Callable<Void>>() {
+                                       List<Future<Void>> futures = executorService.invokeAll(FluentIterable.from(sinks).transform(new Function<Filter, Callable<Void>>() {
 
                                                @Override
-                                               public Callable<Void> apply(final Sink sink) {
+                                               public Callable<Void> apply(final Filter sink) {
                                                        return new Callable<Void>() {
 
                                                                @Override
@@ -485,7 +475,7 @@ public class Pipeline implements Iterable<ControlledComponent> {
                 * Returns the number of input bytes.
                 *
                 * @return The number of input bytes, or {@link Optional#absent()} if the
-                *         component can not receive input
+                *         filter did not receive input
                 */
                public Optional<Long> input() {
                        return (input == -1) ? Optional.<Long>absent() : Optional.of(input);
@@ -495,7 +485,7 @@ public class Pipeline implements Iterable<ControlledComponent> {
                 * Returns the number of output bytes.
                 *
                 * @return The number of output bytes, or {@link Optional#absent()} if the
-                *         component can not send output
+                *         filter did not send output
                 */
                public Optional<Long> output() {
                        return (output == -1) ? Optional.<Long>absent() : Optional.of(output);
diff --git a/src/main/java/net/pterodactylus/sonitus/data/Sink.java b/src/main/java/net/pterodactylus/sonitus/data/Sink.java
deleted file mode 100644 (file)
index 937a4e5..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-package net.pterodactylus.sonitus.data;
-
-import java.io.IOException;
-
-/**
- * A sink is a destination for audio data. It can be played on speakers, it can
- * be written to a file, or it can be sent to a remote streaming server.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Sink extends ControlledComponent {
-
-       /**
-        * Opens this sink using the format parameters of the given metadata.
-        *
-        * @param metadata
-        *              The metadata of the stream
-        * @throws IOException
-        *              if an I/O error occurs
-        */
-       void open(Metadata metadata) throws IOException;
-
-       /** Closes this sink. */
-       void close();
-
-       /**
-        * Processes the given buffer of data.
-        *
-        * @param buffer
-        *              The data to process
-        * @throws IOException
-        *              if an I/O error occurs
-        */
-       void process(byte[] buffer) throws IOException;
-
-}
diff --git a/src/main/java/net/pterodactylus/sonitus/data/Source.java b/src/main/java/net/pterodactylus/sonitus/data/Source.java
deleted file mode 100644 (file)
index b12ec20..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-package net.pterodactylus.sonitus.data;
-
-import java.io.IOException;
-
-/**
- * A source produces an audio stream and accompanying metadata.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Source extends ControlledComponent {
-
-       /**
-        * Returns the metadata of the audio stream.
-        *
-        * @return The metadata of the audio stream
-        */
-       Metadata metadata();
-
-       /**
-        * Retrieves data from the audio stream.
-        *
-        * @param bufferSize
-        *              The maximum amount of bytes to retrieve from the audio stream
-        * @return A buffer filled with up to {@code bufferSize} bytes of data; the
-        *         returned buffer may contain less data than requested but will not
-        *         contain excess elements
-        * @throws IOException
-        *              if an I/O error occurs
-        */
-       byte[] get(int bufferSize) throws IOException;
-
-}
diff --git a/src/main/java/net/pterodactylus/sonitus/data/event/SourceFinishedEvent.java b/src/main/java/net/pterodactylus/sonitus/data/event/SourceFinishedEvent.java
deleted file mode 100644 (file)
index 6161a16..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Sonitus - SourceFinishedEvent.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sonitus.data.event;
-
-import net.pterodactylus.sonitus.data.Source;
-
-/**
- * Event that is sent to an {@link com.google.common.eventbus.EventBus} when
- * a {@link Source} is no longer in use.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class SourceFinishedEvent {
-
-       /** The source that is no longer in use. */
-       private final Source source;
-
-       /**
-        * Creates a new source finished event.
-        *
-        * @param source
-        *              The source that is no longer in use
-        */
-       public SourceFinishedEvent(Source source) {
-               this.source = source;
-       }
-
-       /**
-        * Returns the source that is no longer in use
-        *
-        * @return The source that is no longer in use
-        */
-       public Source source() {
-               return source;
-       }
-
-}
index e87935b..933109e 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sonitus.data.filter;
 import java.io.IOException;
 import java.io.OutputStream;
 
+import net.pterodactylus.sonitus.data.AbstractFilter;
 import net.pterodactylus.sonitus.data.Filter;
 import net.pterodactylus.sonitus.io.ProcessingOutputStream;
 
@@ -28,7 +29,7 @@ import net.pterodactylus.sonitus.io.ProcessingOutputStream;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public abstract class AudioProcessingFilter extends DummyFilter {
+public abstract class AudioProcessingFilter extends AbstractFilter implements Filter {
 
        /**
         * Creates a new audio processing filter with the given name.
@@ -41,7 +42,7 @@ public abstract class AudioProcessingFilter extends DummyFilter {
        }
 
        //
-       // DUMMYFILTER METHODS
+       // FILTER METHODS
        //
 
        @Override
diff --git a/src/main/java/net/pterodactylus/sonitus/data/filter/DummyFilter.java b/src/main/java/net/pterodactylus/sonitus/data/filter/DummyFilter.java
deleted file mode 100644 (file)
index ae6899d..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Sonitus - AbstractFilter.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sonitus.data.filter;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import net.pterodactylus.sonitus.data.AbstractControlledComponent;
-import net.pterodactylus.sonitus.data.Controller;
-import net.pterodactylus.sonitus.data.Filter;
-import net.pterodactylus.sonitus.data.Metadata;
-
-import com.google.common.io.Closeables;
-
-/**
- * Dummy {@link Filter} implementation that pipes its input to its output.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class DummyFilter extends AbstractControlledComponent implements Filter {
-
-       /** The input stream from which to read. */
-       private InputStream inputStream;
-
-       /** The output stream to which to write. */
-       private OutputStream outputStream;
-
-       /**
-        * Creates a new dummy filter with the given name.
-        *
-        * @param name
-        *              The name of the filter
-        */
-       public DummyFilter(String name) {
-               super(name);
-       }
-
-       //
-       // CONTROLLED METHODS
-       //
-
-       @Override
-       public List<Controller<?>> controllers() {
-               return Collections.emptyList();
-       }
-
-       //
-       // FILTER METHODS
-       //
-
-       @Override
-       public void open(Metadata metadata) throws IOException {
-               metadataUpdated(metadata);
-               inputStream = createInputStream();
-               outputStream = createOutputStream();
-       }
-
-       @Override
-       public void close() {
-               try {
-                       Closeables.close(outputStream, true);
-                       Closeables.close(inputStream, true);
-               } catch (IOException e) {
-                       /* won’t throw. */
-               }
-       }
-
-       @Override
-       public void process(byte[] buffer) throws IOException {
-               outputStream.write(buffer);
-               outputStream.flush();
-       }
-
-       @Override
-       public byte[] get(int bufferSize) throws IOException {
-               byte[] buffer = new byte[bufferSize];
-               int read = inputStream.read(buffer);
-               if (read == -1) {
-                       throw new EOFException();
-               }
-               return Arrays.copyOf(buffer, read);
-       }
-
-       //
-       // SUBCLASS METHODS
-       //
-
-       /**
-        * Creates the input stream from which {@link net.pterodactylus.sonitus.data.Pipeline}
-        * will read the audio data. If you override this, you have to override {@link
-        * #createOutputStream()}, too!
-        *
-        * @return The input stream to read from
-        * @throws IOException
-        *              if an I/O error occurs
-        */
-       protected InputStream createInputStream() throws IOException {
-               return new PipedInputStream();
-       }
-
-       /**
-        * Creates the output stream to which {@link net.pterodactylus.sonitus.data.Pipeline}
-        * will write the audio data. If you override this, you have to override {@link
-        * #createInputStream()}, too!
-        *
-        * @return The output stream to write to
-        * @throws IOException
-        *              if an I/O error occurs
-        */
-       protected OutputStream createOutputStream() throws IOException {
-               return new PipedOutputStream((PipedInputStream) inputStream);
-       }
-
-}
index 990f976..a482cf6 100644 (file)
@@ -22,6 +22,8 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sonitus.data.AbstractFilter;
+import net.pterodactylus.sonitus.data.Filter;
 import net.pterodactylus.sonitus.data.Metadata;
 import net.pterodactylus.sonitus.io.InputStreamDrainer;
 
@@ -34,7 +36,7 @@ import com.google.common.collect.Iterables;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public abstract class ExternalFilter extends DummyFilter {
+public abstract class ExternalFilter extends AbstractFilter implements Filter {
 
        /** The logger. */
        private final Logger logger = Logger.getLogger(getClass().getName());
@@ -69,10 +71,6 @@ public abstract class ExternalFilter extends DummyFilter {
                process.destroy();
        }
 
-       //
-       // DUMMYFILTER METHODS
-       //
-
        @Override
        protected InputStream createInputStream() throws IOException {
                return process.getInputStream();
index b142fdd..93ff3aa 100644 (file)
@@ -68,16 +68,11 @@ public class OggVorbisDecoder extends ExternalFilter {
        //
 
        @Override
-       public Metadata metadata() {
-               return super.metadata().encoding("PCM");
-       }
-
-       @Override
        public void open(Metadata metadata) throws IOException {
                checkNotNull(metadata, "metadata must not be null");
                checkArgument(metadata.encoding().equalsIgnoreCase("Vorbis"), "source must be Vorbis-encoded");
 
-               super.open(metadata);
+               super.open(metadata.encoding("PCM"));
        }
 
        //
diff --git a/src/main/java/net/pterodactylus/sonitus/data/filter/PredicateFilter.java b/src/main/java/net/pterodactylus/sonitus/data/filter/PredicateFilter.java
deleted file mode 100644 (file)
index b400dc7..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Sonitus - PredicateFilter.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sonitus.data.filter;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import java.io.IOException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import net.pterodactylus.sonitus.data.Filter;
-import net.pterodactylus.sonitus.data.Metadata;
-
-import com.google.common.base.Predicate;
-
-/**
- * {@link Filter} implementation that uses a {@link Predicate} to determine
- * whether a filter will be used or the data will only be passed through.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class PredicateFilter extends DummyFilter {
-
-       /** The predicate. */
-       private final Predicate<Metadata> metadataPredicate;
-
-       /** The filter to use if the predicate matches. */
-       private final Filter filter;
-
-       /** Whether the predicate currently matches. */
-       private final AtomicBoolean metadataMatches = new AtomicBoolean(false);
-
-       /**
-        * Creates a new predicate filter.
-        *
-        * @param metadataPredicate
-        *              The predicate to evaluate every time the metadata changes
-        * @param filter
-        */
-       public PredicateFilter(Predicate<Metadata> metadataPredicate, Filter filter) {
-               super(String.format("%s (maybe)", filter.name()));
-               this.metadataPredicate = metadataPredicate;
-               this.filter = filter;
-       }
-
-       //
-       // FILTER METHODS
-       //
-
-       @Override
-       public void open(Metadata metadata) throws IOException {
-               checkNotNull(metadata, "metadata must not be null");
-
-               metadataMatches.set(metadataPredicate.apply(metadata));
-               if (metadataMatches.get()) {
-                       filter.open(metadata);
-               } else {
-                       super.open(metadata);
-               }
-       }
-
-       @Override
-       public void close() {
-               if (metadataMatches.get()) {
-                       filter.close();
-               } else {
-                       super.close();
-               }
-       }
-
-       @Override
-       public Metadata metadata() {
-               if (metadataMatches.get()) {
-                       return filter.metadata();
-               }
-               return super.metadata();
-       }
-
-       @Override
-       public void metadataUpdated(Metadata metadata) {
-               metadataMatches.set(metadataPredicate.apply(metadata));
-               if (metadataMatches.get()) {
-                       filter.metadataUpdated(metadata);
-               } else {
-                       super.metadataUpdated(metadata);
-               }
-       }
-
-       @Override
-       public void process(byte[] buffer) throws IOException {
-               if (metadataMatches.get()) {
-                       filter.process(buffer);
-               } else {
-                       super.process(buffer);
-               }
-       }
-
-       @Override
-       public byte[] get(int bufferSize) throws IOException {
-               if (metadataMatches.get()) {
-                       return filter.get(bufferSize);
-               } else {
-                       return super.get(bufferSize);
-               }
-       }
-
-}
index b2890ca..04ed2d1 100644 (file)
@@ -20,6 +20,8 @@ package net.pterodactylus.sonitus.data.filter;
 import java.io.IOException;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sonitus.data.AbstractFilter;
+import net.pterodactylus.sonitus.data.Filter;
 import net.pterodactylus.sonitus.data.Metadata;
 
 /**
@@ -29,7 +31,7 @@ import net.pterodactylus.sonitus.data.Metadata;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class RateLimitingFilter extends DummyFilter {
+public class RateLimitingFilter extends AbstractFilter implements Filter {
 
        /** The logger. */
        private static final Logger logger = Logger.getLogger(RateLimitingFilter.class.getName());
index 2415bbb..d412259 100644 (file)
@@ -58,16 +58,11 @@ public class SoxResampleFilter extends ExternalFilter {
        //
 
        @Override
-       public Metadata metadata() {
-               return super.metadata().frequency(rate);
-       }
-
-       @Override
        public void open(Metadata metadata) throws IOException {
                checkNotNull(metadata, "metadata must not be null");
                checkArgument(metadata.encoding().equalsIgnoreCase("PCM"), "source must be PCM-encoded");
 
-               super.open(metadata);
+               super.open(metadata.frequency(rate));
        }
 
        //
index 3993e11..6eef02c 100644 (file)
@@ -42,7 +42,7 @@ public class StereoSeparationFilter extends AudioProcessingFilter {
        }
 
        //
-       // CONTROLLED METHODS
+       // FILTER METHODS
        //
 
        @Override
@@ -50,10 +50,6 @@ public class StereoSeparationFilter extends AudioProcessingFilter {
                return Arrays.<Controller<?>>asList(separationKnob);
        }
 
-       //
-       // AUDIOPROCESSINGFILTER METHODS
-       //
-
        @Override
        protected int[] processSamples(int[] samples) {
                if (samples.length == 1) {
index b584a48..0eb8281 100644 (file)
@@ -21,6 +21,7 @@ import java.io.IOException;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 
+import net.pterodactylus.sonitus.data.AbstractFilter;
 import net.pterodactylus.sonitus.data.Filter;
 import net.pterodactylus.sonitus.data.Metadata;
 
@@ -31,7 +32,7 @@ import net.pterodactylus.sonitus.data.Metadata;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class TimeCounterFilter extends DummyFilter {
+public class TimeCounterFilter extends AbstractFilter implements Filter {
 
        /** The byte counter. */
        private final AtomicLong counter = new AtomicLong();
@@ -95,7 +96,7 @@ public class TimeCounterFilter extends DummyFilter {
        }
 
        //
-       // DUMMYFILTER METHODS
+       // FILTER METHODS
        //
 
        @Override
index 02570db..01d325f 100644 (file)
@@ -47,7 +47,7 @@ public class VolumeFilter extends AudioProcessingFilter {
        }
 
        //
-       // CONTROLLED METHODS
+       // FILTER METHODS
        //
 
        @Override
@@ -55,10 +55,6 @@ public class VolumeFilter extends AudioProcessingFilter {
                return Arrays.<Controller<?>>asList(volumeFader, muteSwitch);
        }
 
-       //
-       // AUDIOPROCESSINGFILTER METHODS
-       //
-
        @Override
        protected int[] processSamples(int[] samples) {
                int[] processedSamples = new int[samples.length];
index 3409280..6cace62 100644 (file)
@@ -34,11 +34,10 @@ import javax.sound.sampled.FloatControl;
 import javax.sound.sampled.LineUnavailableException;
 import javax.sound.sampled.SourceDataLine;
 
-import net.pterodactylus.sonitus.data.AbstractControlledComponent;
+import net.pterodactylus.sonitus.data.AbstractFilter;
 import net.pterodactylus.sonitus.data.Controller;
+import net.pterodactylus.sonitus.data.Filter;
 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.io.IntegralWriteOutputStream;
@@ -46,12 +45,12 @@ import net.pterodactylus.sonitus.io.IntegralWriteOutputStream;
 import com.google.common.base.Preconditions;
 
 /**
- * {@link Sink} implementation that uses the JDK’s {@link AudioSystem} to play
- * all {@link Source}s.
+ * {@link Filter} implementation that uses the JDK’s {@link AudioSystem} to play
+ * all the audio signal.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class AudioSink extends AbstractControlledComponent implements Sink {
+public class AudioSink extends AbstractFilter {
 
        /** The logger. */
        private static final Logger logger = Logger.getLogger(AudioSink.class.getName());
@@ -135,7 +134,7 @@ public class AudioSink extends AbstractControlledComponent implements Sink {
        }
 
        //
-       // CONTROLLED METHODS
+       // FILTER METHODS
        //
 
        @Override
@@ -143,13 +142,10 @@ public class AudioSink extends AbstractControlledComponent implements Sink {
                return Arrays.<Controller<?>>asList(volumeFader, muteSwitch);
        }
 
-       //
-       // SINK METHODS
-       //
-
        @Override
        public void open(Metadata metadata) throws IOException {
                Preconditions.checkArgument(metadata.encoding().equalsIgnoreCase("PCM"), "source must be PCM-encoded");
+               super.open(metadata);
                AudioFormat audioFormat = new AudioFormat(metadata.frequency(), 16, metadata.channels(), true, false);
                try {
                        sourceDataLine = AudioSystem.getSourceDataLine(audioFormat);
@@ -178,6 +174,7 @@ public class AudioSink extends AbstractControlledComponent implements Sink {
        @Override
        public void process(byte[] buffer) throws IOException {
                sourceDataLineOutputStream.write(buffer);
+               super.process(buffer);
                logger.finest(String.format("AudioSink: Wrote %d Bytes.", buffer.length));
        }
 
@@ -186,7 +183,7 @@ public class AudioSink extends AbstractControlledComponent implements Sink {
        //
 
        /**
-        * Returns the {@link FloatControl.Type.VOLUME} control.
+        * Returns the {@link FloatControl.Type#VOLUME} control.
         *
         * @param dataLine
         *              The data line to search for the control
@@ -197,7 +194,7 @@ public class AudioSink extends AbstractControlledComponent implements Sink {
        }
 
        /**
-        * Returns the {@link BooleanControl.Type.MUTE} control.
+        * Returns the {@link BooleanControl.Type#MUTE} control.
         *
         * @param dataLine
         *              The data line to search for the control
index be934f3..5011321 100644 (file)
@@ -19,14 +19,10 @@ package net.pterodactylus.sonitus.data.sink;
 
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
 import java.util.logging.Logger;
 
-import net.pterodactylus.sonitus.data.AbstractControlledComponent;
-import net.pterodactylus.sonitus.data.Controller;
+import net.pterodactylus.sonitus.data.AbstractFilter;
 import net.pterodactylus.sonitus.data.Metadata;
-import net.pterodactylus.sonitus.data.Sink;
 
 /**
  * {@link net.pterodactylus.sonitus.data.Sink} that writes all received data
@@ -34,7 +30,7 @@ import net.pterodactylus.sonitus.data.Sink;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class FileSink extends AbstractControlledComponent implements Sink {
+public class FileSink extends AbstractFilter {
 
        /** The logger. */
        private static final Logger logger = Logger.getLogger(FileSink.class.getName());
@@ -57,16 +53,7 @@ public class FileSink extends AbstractControlledComponent implements Sink {
        }
 
        //
-       // CONTROLLED METHODS
-       //
-
-       @Override
-       public List<Controller<?>> controllers() {
-               return Collections.emptyList();
-       }
-
-       //
-       // SINK METHODS
+       // FILTER METHODS
        //
 
        @Override
index 8e1c154..f98fba5 100644 (file)
@@ -28,10 +28,9 @@ import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import net.pterodactylus.sonitus.data.AbstractControlledComponent;
+import net.pterodactylus.sonitus.data.AbstractFilter;
 import net.pterodactylus.sonitus.data.Controller;
 import net.pterodactylus.sonitus.data.Metadata;
-import net.pterodactylus.sonitus.data.Sink;
 import net.pterodactylus.sonitus.io.InputStreamDrainer;
 
 import com.google.common.io.BaseEncoding;
@@ -43,7 +42,7 @@ import com.google.common.io.Closeables;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Icecast2Sink extends AbstractControlledComponent implements Sink {
+public class Icecast2Sink extends AbstractFilter {
 
        /** The logger. */
        private static final Logger logger = Logger.getLogger(Icecast2Sink.class.getName());
@@ -111,7 +110,7 @@ public class Icecast2Sink extends AbstractControlledComponent implements Sink {
        }
 
        //
-       // CONTROLLED METHODS
+       // FILTER METHODS
        //
 
        @Override
@@ -119,10 +118,6 @@ public class Icecast2Sink extends AbstractControlledComponent implements Sink {
                return Collections.emptyList();
        }
 
-       //
-       // SINK METHODS
-       //
-
        @Override
        public void open(Metadata metadata) throws IOException {
                logger.info(String.format("Connecting to %s:%d...", server, port));
index 9aff8d4..7251f65 100644 (file)
@@ -27,21 +27,21 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
-import net.pterodactylus.sonitus.data.AbstractControlledComponent;
+import net.pterodactylus.sonitus.data.AbstractFilter;
 import net.pterodactylus.sonitus.data.Controller;
+import net.pterodactylus.sonitus.data.Filter;
 import net.pterodactylus.sonitus.data.Metadata;
-import net.pterodactylus.sonitus.data.Source;
 import net.pterodactylus.sonitus.io.IdentifyingInputStream;
 
 import com.google.common.base.Optional;
 
 /**
- * A {@link net.pterodactylus.sonitus.data.Source} that is read from the local
- * file system.
+ * A {@link Filter} that reads a file from the local file system and does not
+ * expect any input.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class FileSource extends AbstractControlledComponent implements Source {
+public class FileSource extends AbstractFilter {
 
        /** The path of the file. */
        private final String path;
@@ -73,7 +73,7 @@ public class FileSource extends AbstractControlledComponent implements Source {
        }
 
        //
-       // CONTROLLED METHODS
+       // FILTER METHODS
        //
 
        @Override
@@ -81,10 +81,6 @@ public class FileSource extends AbstractControlledComponent implements Source {
                return Collections.emptyList();
        }
 
-       //
-       // SOURCE METHODS
-       //
-
        @Override
        public byte[] get(int bufferSize) throws IOException {
                byte[] buffer = new byte[bufferSize];
index 80c3f56..b8afd82 100644 (file)
@@ -27,21 +27,20 @@ import java.util.concurrent.atomic.AtomicReference;
 import java.util.logging.Logger;
 import javax.swing.event.EventListenerList;
 
-import net.pterodactylus.sonitus.data.AbstractControlledComponent;
+import net.pterodactylus.sonitus.data.AbstractFilter;
 import net.pterodactylus.sonitus.data.Controller;
+import net.pterodactylus.sonitus.data.Filter;
 import net.pterodactylus.sonitus.data.Metadata;
-import net.pterodactylus.sonitus.data.Source;
 
 import com.google.inject.Inject;
 
 /**
- * {@link Source} implementation that simply forwards another source and
- * supports changing the source without letting the {@link
- * net.pterodactylus.sonitus.data.Sink} know.
+ * {@link Filter} implementation that simply forwards data from another filter
+ * and supports changing the source without letting downstream filters know.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class MultiSource extends AbstractControlledComponent implements Source {
+public class MultiSource extends AbstractFilter {
 
        /** The logger. */
        private static final Logger logger = Logger.getLogger(MultiSource.class.getName());
@@ -50,7 +49,7 @@ public class MultiSource extends AbstractControlledComponent implements Source {
        private final EventListenerList sourceFinishedListeners = new EventListenerList();
 
        /** The current source. */
-       private final AtomicReference<Source> source = new AtomicReference<Source>();
+       private final AtomicReference<Filter> source = new AtomicReference<Filter>();
 
        /** Whether the source was changed. */
        private boolean sourceChanged;
@@ -95,10 +94,10 @@ public class MultiSource extends AbstractControlledComponent implements Source {
         * @param source
         *              The new source to use
         */
-       public void setSource(Source source) {
+       public void setSource(Filter source) {
                checkNotNull(source, "source must not be null");
 
-               Source oldSource = this.source.getAndSet(source);
+               Filter oldSource = this.source.getAndSet(source);
                if (!source.equals(oldSource)) {
                        synchronized (this.source) {
                                sourceChanged = true;
@@ -115,7 +114,7 @@ public class MultiSource extends AbstractControlledComponent implements Source {
 
        /**
         * Notifies all registered listeners that the current source finished playing
-        * and that a new source should be {@link #setSource(Source) set}.
+        * and that a new source should be {@link #setSource(Filter) set}.
         *
         * @see SourceFinishedListener
         */
@@ -126,7 +125,7 @@ public class MultiSource extends AbstractControlledComponent implements Source {
        }
 
        //
-       // CONTROLLED METHODS
+       // FILTER METHODS
        //
 
        @Override
@@ -144,10 +143,6 @@ public class MultiSource extends AbstractControlledComponent implements Source {
                return super.metadata();
        }
 
-       //
-       // SOURCE METHODS
-       //
-
        @Override
        public byte[] get(int bufferSize) throws EOFException, IOException {
                while (true) {
@@ -167,7 +162,7 @@ public class MultiSource extends AbstractControlledComponent implements Source {
        // PRIVATE METHODS
        //
 
-       /** Waits for a new source to be {@link #setSource(Source) set}. */
+       /** Waits for a new source to be {@link #setSource(Filter) set}. */
        private void waitForNewSource() {
                fireSourceFinished();
                synchronized (source) {
index 35ad426..63c1eee 100644 (file)
@@ -19,8 +19,6 @@ package net.pterodactylus.sonitus.data.source;
 
 import java.util.EventListener;
 
-import net.pterodactylus.sonitus.data.Source;
-
 /**
  * Interface for {@link MultiSource} notifications if a source is finished
  * playing.
index dc6e4d0..a66fb7a 100644 (file)
@@ -27,12 +27,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.logging.Logger;
 
-import net.pterodactylus.sonitus.data.AbstractControlledComponent;
+import net.pterodactylus.sonitus.data.AbstractFilter;
 import net.pterodactylus.sonitus.data.ContentMetadata;
 import net.pterodactylus.sonitus.data.Controller;
 import net.pterodactylus.sonitus.data.FormatMetadata;
 import net.pterodactylus.sonitus.data.Metadata;
-import net.pterodactylus.sonitus.data.Source;
 import net.pterodactylus.sonitus.io.MetadataStream;
 
 import com.google.common.base.Optional;
@@ -47,7 +46,7 @@ import com.google.common.primitives.Ints;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class StreamSource extends AbstractControlledComponent implements Source {
+public class StreamSource extends AbstractFilter {
 
        /** The logger. */
        private static final Logger logger = Logger.getLogger(StreamSource.class.getName());
@@ -127,7 +126,7 @@ public class StreamSource extends AbstractControlledComponent implements Source
        }
 
        //
-       // CONTROLLED METHODS
+       // FILTER METHODS
        //
 
        @Override
@@ -140,10 +139,6 @@ public class StreamSource extends AbstractControlledComponent implements Source
                return Collections.emptyList();
        }
 
-       //
-       // SOURCE METHODS
-       //
-
        @Override
        public Metadata metadata() {
                Optional<ContentMetadata> streamMetadata = metadataStream.getContentMetadata();
diff --git a/src/main/java/net/pterodactylus/sonitus/gui/ComponentInfoPanel.java b/src/main/java/net/pterodactylus/sonitus/gui/ComponentInfoPanel.java
deleted file mode 100644 (file)
index 752ceef..0000000
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Sonitus - ComponentInfoPanel.java - Copyright © 2013 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sonitus.gui;
-
-import java.awt.Dimension;
-import java.awt.Font;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import javax.swing.BorderFactory;
-import javax.swing.Box;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-
-import net.pterodactylus.sonitus.data.ControlledComponent;
-import net.pterodactylus.sonitus.data.Controller;
-import net.pterodactylus.sonitus.data.FormatMetadata;
-import net.pterodactylus.sonitus.data.controller.Fader;
-import net.pterodactylus.sonitus.data.controller.Knob;
-import net.pterodactylus.sonitus.data.controller.Switch;
-
-import com.google.common.base.Optional;
-
-/**
- * Panel that shows information about a {@link ControlledComponent}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class ComponentInfoPanel extends JPanel {
-
-       /** The name of the component. */
-       private final JLabel headerLabel = new JLabel();
-
-       /** The number of received input bytes. */
-       private final JLabel inputLabel = new JLabel();
-
-       /** The number of sent output bytes. */
-       private final JLabel outputLabel = new JLabel();
-
-       /** The current format metadata. */
-       private final JLabel formatLabel = new JLabel();
-
-       /**
-        * Creates a new component info panel.
-        *
-        * @param controlledComponent
-        *              The component to display
-        */
-       public ComponentInfoPanel(ControlledComponent controlledComponent) {
-               super(new GridBagLayout());
-
-               setPreferredSize(new Dimension(300, 0));
-               createPanel(controlledComponent);
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Sets the number of received input bytes.
-        *
-        * @param input
-        *              The number of received input bytes
-        * @return This panel
-        */
-       public ComponentInfoPanel input(Optional<Long> input) {
-               if (input.isPresent()) {
-                       inputLabel.setText(format(input.get()));
-               } else {
-                       inputLabel.setText("");
-               }
-               return this;
-       }
-
-       /**
-        * Sets the number of sent output bytes.
-        *
-        * @param output
-        *              The number of sent output input bytes
-        * @return This panel
-        */
-       public ComponentInfoPanel output(Optional<Long> output) {
-               if (output.isPresent()) {
-                       outputLabel.setText(format(output.get()));
-               } else {
-                       outputLabel.setText("");
-               }
-               return this;
-       }
-
-       /**
-        * Sets the current format metadata.
-        *
-        * @param metadata
-        *              The format metadata
-        * @return This panel
-        */
-       public ComponentInfoPanel format(Optional<FormatMetadata> metadata) {
-               if (metadata.isPresent()) {
-                       formatLabel.setText(metadata.get().toString());
-               } else {
-                       formatLabel.setText("");
-               }
-               return this;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Creates the panel for the given controlled component.
-        *
-        * @param controlledComponent
-        *              The controlled component
-        */
-       private void createPanel(ControlledComponent controlledComponent) {
-               setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
-
-               headerLabel.setText(controlledComponent.name());
-               headerLabel.setFont(headerLabel.getFont().deriveFont(Font.BOLD));
-
-               int line = 0;
-               add(headerLabel, new GridBagConstraints(0, line++, 2, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
-               add(new JLabel("Input"), new GridBagConstraints(0, line, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(18, 0, 0, 0), 0, 0));
-               add(inputLabel, new GridBagConstraints(1, line++, 1, 1, 1.0, 0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(18, 6, 0, 0), 0, 0));
-               add(new JLabel("Output"), new GridBagConstraints(0, line, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(6, 0, 0, 0), 0, 0));
-               add(outputLabel, new GridBagConstraints(1, line++, 1, 1, 1.0, 0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
-               add(new JLabel("Format"), new GridBagConstraints(0, line, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(6, 0, 0, 0), 0, 0));
-               add(formatLabel, new GridBagConstraints(1, line++, 1, 1, 1.0, 0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
-
-               /* add the controllers. */
-               for (Controller<?> controller : controlledComponent.controllers()) {
-                       add(new JLabel(controller.name()), new GridBagConstraints(0, line, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(6, 0, 0, 6), 0, 0));
-                       if (controller instanceof Fader) {
-                               add(new FaderPanel((Fader) controller), new GridBagConstraints(1, line++, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 0, 0, 0), 0, 0));
-                       } else if (controller instanceof Switch) {
-                               add(new SwitchPanel((Switch) controller), new GridBagConstraints(1, line++, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 0, 0, 0), 0, 0));
-                       } else if (controller instanceof Knob) {
-                               add(new KnobPanel((Knob) controller), new GridBagConstraints(1, line++, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 0, 0, 0), 0, 0));
-                       }
-               }
-
-               add(Box.createVerticalGlue(), new GridBagConstraints(1, line++, 2, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(6, 6, 0, 0), 0, 0));
-       }
-
-       /**
-        * Formats the number using SI prefixes so that a maximum of 3 digits are
-        * shown.
-        *
-        * @param number
-        *              The number to format
-        * @return The formatted number
-        */
-       private static String format(long number) {
-               String[] prefixes = { "", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei" };
-               double shortenedNumber = number;
-               for (String prefix : prefixes) {
-                       if (shortenedNumber < 1000) {
-                               return String.format("%.1f %sB", shortenedNumber, prefix);
-                       }
-                       shortenedNumber /= 1024;
-               }
-               return String.format("%.1e B", (double) number);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sonitus/gui/FilterInfoPanel.java b/src/main/java/net/pterodactylus/sonitus/gui/FilterInfoPanel.java
new file mode 100644 (file)
index 0000000..b26afe9
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * Sonitus - FilterInfoPanel.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.gui;
+
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import net.pterodactylus.sonitus.data.Controller;
+import net.pterodactylus.sonitus.data.Filter;
+import net.pterodactylus.sonitus.data.FormatMetadata;
+import net.pterodactylus.sonitus.data.controller.Fader;
+import net.pterodactylus.sonitus.data.controller.Knob;
+import net.pterodactylus.sonitus.data.controller.Switch;
+
+import com.google.common.base.Optional;
+
+/**
+ * Panel that shows information about a {@link Filter}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FilterInfoPanel extends JPanel {
+
+       /** The name of the filter. */
+       private final JLabel headerLabel = new JLabel();
+
+       /** The number of received input bytes. */
+       private final JLabel inputLabel = new JLabel();
+
+       /** The number of sent output bytes. */
+       private final JLabel outputLabel = new JLabel();
+
+       /** The current format metadata. */
+       private final JLabel formatLabel = new JLabel();
+
+       /**
+        * Creates a new filter info panel.
+        *
+        * @param filter
+        *              The filter to display
+        */
+       public FilterInfoPanel(Filter filter) {
+               super(new GridBagLayout());
+
+               setPreferredSize(new Dimension(300, 0));
+               createPanel(filter);
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Sets the number of received input bytes.
+        *
+        * @param input
+        *              The number of received input bytes
+        * @return This panel
+        */
+       public FilterInfoPanel input(Optional<Long> input) {
+               if (input.isPresent()) {
+                       inputLabel.setText(format(input.get()));
+               } else {
+                       inputLabel.setText("");
+               }
+               return this;
+       }
+
+       /**
+        * Sets the number of sent output bytes.
+        *
+        * @param output
+        *              The number of sent output input bytes
+        * @return This panel
+        */
+       public FilterInfoPanel output(Optional<Long> output) {
+               if (output.isPresent()) {
+                       outputLabel.setText(format(output.get()));
+               } else {
+                       outputLabel.setText("");
+               }
+               return this;
+       }
+
+       /**
+        * Sets the current format metadata.
+        *
+        * @param metadata
+        *              The format metadata
+        * @return This panel
+        */
+       public FilterInfoPanel format(Optional<FormatMetadata> metadata) {
+               if (metadata.isPresent()) {
+                       formatLabel.setText(metadata.get().toString());
+               } else {
+                       formatLabel.setText("");
+               }
+               return this;
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Creates the panel for the given filter.
+        *
+        * @param filter
+        *              The filter to create a panel for
+        */
+       private void createPanel(Filter filter) {
+               setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
+
+               headerLabel.setText(filter.name());
+               headerLabel.setFont(headerLabel.getFont().deriveFont(Font.BOLD));
+
+               int line = 0;
+               add(headerLabel, new GridBagConstraints(0, line++, 2, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+               add(new JLabel("Input"), new GridBagConstraints(0, line, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(18, 0, 0, 0), 0, 0));
+               add(inputLabel, new GridBagConstraints(1, line++, 1, 1, 1.0, 0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(18, 6, 0, 0), 0, 0));
+               add(new JLabel("Output"), new GridBagConstraints(0, line, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(6, 0, 0, 0), 0, 0));
+               add(outputLabel, new GridBagConstraints(1, line++, 1, 1, 1.0, 0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               add(new JLabel("Format"), new GridBagConstraints(0, line, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(6, 0, 0, 0), 0, 0));
+               add(formatLabel, new GridBagConstraints(1, line++, 1, 1, 1.0, 0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+
+               /* add the controllers. */
+               for (Controller<?> controller : filter.controllers()) {
+                       add(new JLabel(controller.name()), new GridBagConstraints(0, line, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(6, 0, 0, 6), 0, 0));
+                       if (controller instanceof Fader) {
+                               add(new FaderPanel((Fader) controller), new GridBagConstraints(1, line++, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 0, 0, 0), 0, 0));
+                       } else if (controller instanceof Switch) {
+                               add(new SwitchPanel((Switch) controller), new GridBagConstraints(1, line++, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 0, 0, 0), 0, 0));
+                       } else if (controller instanceof Knob) {
+                               add(new KnobPanel((Knob) controller), new GridBagConstraints(1, line++, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(6, 0, 0, 0), 0, 0));
+                       }
+               }
+
+               add(Box.createVerticalGlue(), new GridBagConstraints(1, line++, 2, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(6, 6, 0, 0), 0, 0));
+       }
+
+       /**
+        * Formats the number using SI prefixes so that a maximum of 3 digits are
+        * shown.
+        *
+        * @param number
+        *              The number to format
+        * @return The formatted number
+        */
+       private static String format(long number) {
+               String[] prefixes = { "", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei" };
+               double shortenedNumber = number;
+               for (String prefix : prefixes) {
+                       if (shortenedNumber < 1000) {
+                               return String.format("%.1f %sB", shortenedNumber, prefix);
+                       }
+                       shortenedNumber /= 1024;
+               }
+               return String.format("%.1e B", (double) number);
+       }
+
+}
index bb69578..85a49fb 100644 (file)
@@ -30,9 +30,9 @@ import javax.swing.JTabbedPane;
 import javax.swing.Timer;
 import javax.swing.WindowConstants;
 
-import net.pterodactylus.sonitus.data.ControlledComponent;
+import net.pterodactylus.sonitus.data.Filter;
 import net.pterodactylus.sonitus.data.Pipeline;
-import net.pterodactylus.sonitus.gui.PipelinePanel.ComponentSelectionListener;
+import net.pterodactylus.sonitus.gui.PipelinePanel.FilterSelectionListener;
 import net.pterodactylus.sonitus.main.Version;
 
 import com.google.common.base.Optional;
@@ -48,7 +48,7 @@ public class MainWindow extends JFrame {
        /** The pipeline to display. */
        private final Pipeline pipeline;
 
-       /** The tabbed pane displaying all controlled components. */
+       /** The tabbed pane displaying all pipelines. */
        private final JTabbedPane tabbedPane = new JTabbedPane();
 
        /** The info panel card layout. */
@@ -57,8 +57,8 @@ public class MainWindow extends JFrame {
        /** The info panel. */
        private final JPanel infoPanel = new JPanel(infoPanelCardLayout);
 
-       /** The mapping from controlled components to info panels. */
-       private final Map<ControlledComponent, ComponentInfoPanel> controlledInfoPanels = Maps.newHashMap();
+       /** The mapping from filters to info panels. */
+       private final Map<Filter, FilterInfoPanel> filterInfoPanels = Maps.newHashMap();
 
        /**
         * Creates a new main window.
@@ -72,11 +72,11 @@ public class MainWindow extends JFrame {
                tabbedPane.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
                final JPanel pipelineInfoPanel = new JPanel(new BorderLayout(12, 12));
                PipelinePanel pipelinePanel = new PipelinePanel(pipeline);
-               pipelinePanel.addComponentHoverListener(new ComponentSelectionListener() {
+               pipelinePanel.addFilterSelectionListener(new FilterSelectionListener() {
 
                        @Override
-                       public void componentSelected(ControlledComponent controlledComponent) {
-                               infoPanelCardLayout.show(infoPanel, controlledComponent.name());
+                       public void filterSelected(Filter filter) {
+                               infoPanelCardLayout.show(infoPanel, filter.name());
                        }
                });
                pipelineInfoPanel.add(pipelinePanel, BorderLayout.CENTER);
@@ -85,11 +85,11 @@ public class MainWindow extends JFrame {
                getContentPane().add(tabbedPane, BorderLayout.CENTER);
                setSize(new Dimension(800, 450));
 
-               /* create info panels for all components. */
-               for (ControlledComponent controlledComponent : pipeline) {
-                       ComponentInfoPanel componentInfoPanel = new ComponentInfoPanel(controlledComponent);
-                       infoPanel.add(componentInfoPanel, controlledComponent.name());
-                       controlledInfoPanels.put(controlledComponent, componentInfoPanel);
+               /* create info panels for all filters. */
+               for (Filter fliter : pipeline) {
+                       FilterInfoPanel filterInfoPanel = new FilterInfoPanel(fliter);
+                       infoPanel.add(filterInfoPanel, fliter.name());
+                       filterInfoPanels.put(fliter, filterInfoPanel);
                }
 
                Timer timer = new Timer(250, new ActionListener() {
@@ -97,11 +97,11 @@ public class MainWindow extends JFrame {
                        @Override
                        public void actionPerformed(ActionEvent actionEvent) {
                                /* update all info panels. */
-                               for (ControlledComponent controlled : MainWindow.this.pipeline) {
-                                       ComponentInfoPanel componentInfoPanel = controlledInfoPanels.get(controlled);
-                                       componentInfoPanel.input(MainWindow.this.pipeline.trafficCounter(controlled).input());
-                                       componentInfoPanel.output(MainWindow.this.pipeline.trafficCounter(controlled).output());
-                                       componentInfoPanel.format(Optional.of(controlled.metadata().format()));
+                               for (Filter filter : MainWindow.this.pipeline) {
+                                       FilterInfoPanel filterInfoPanel = filterInfoPanels.get(filter);
+                                       filterInfoPanel.input(MainWindow.this.pipeline.trafficCounter(filter).input());
+                                       filterInfoPanel.output(MainWindow.this.pipeline.trafficCounter(filter).output());
+                                       filterInfoPanel.format(Optional.of(filter.metadata().format()));
                                }
                        }
                });
index d47003d..93189ef 100644 (file)
@@ -38,15 +38,13 @@ import javax.swing.JPanel;
 import javax.swing.UIManager;
 import javax.swing.event.EventListenerList;
 
-import net.pterodactylus.sonitus.data.ControlledComponent;
+import net.pterodactylus.sonitus.data.Filter;
 import net.pterodactylus.sonitus.data.Metadata;
 import net.pterodactylus.sonitus.data.MetadataListener;
 import net.pterodactylus.sonitus.data.Pipeline;
-import net.pterodactylus.sonitus.data.Sink;
-import net.pterodactylus.sonitus.data.Source;
 
 /**
- * {@link JPanel} that displays all components of a {@link Pipeline}.
+ * {@link JPanel} that displays all filters of a {@link Pipeline}.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
@@ -58,11 +56,11 @@ public class PipelinePanel extends JPanel {
        /** The pipeline being displayed. */
        private final Pipeline pipeline;
 
-       /** The component hover listeners. */
-       private final EventListenerList componentSelectionListeners = new EventListenerList();
+       /** The filter selection listeners. */
+       private final EventListenerList filterSelectionListeners = new EventListenerList();
 
-       /** The currently selected component. */
-       private JComponent selectedComponent;
+       /** The currently selected filter. */
+       private JComponent selectedFilter;
 
        /**
         * Creates a new pipeline panel displaying the given pipeline.
@@ -81,13 +79,13 @@ public class PipelinePanel extends JPanel {
        //
 
        /**
-        * Adds the given component selection listener to this panel.
+        * Adds the given filter selection listener to this panel.
         *
-        * @param componentSelectionListener
-        *              The component selection listener to add
+        * @param filterSelectionListener
+        *              The filter selection listener to add
         */
-       public void addComponentHoverListener(ComponentSelectionListener componentSelectionListener) {
-               componentSelectionListeners.add(ComponentSelectionListener.class, componentSelectionListener);
+       public void addFilterSelectionListener(FilterSelectionListener filterSelectionListener) {
+               filterSelectionListeners.add(FilterSelectionListener.class, filterSelectionListener);
        }
 
        //
@@ -99,16 +97,11 @@ public class PipelinePanel extends JPanel {
                /* clear everything. */
                removeAll();
 
-               /* count all sinks. */
+               /* count all filters. */
                int sinkCount = 0;
-               for (ControlledComponent component : pipeline.components()) {
-                       if (!(component instanceof Source)) {
-                               logger.finest(String.format("%s is not a Source, skipping.", component.name()));
-                               sinkCount++;
-                               continue;
-                       }
-                       Collection<Sink> sinks = pipeline.sinks((Source) component);
-                       logger.finest(String.format("%s has %d sinks: %s", component.name(), sinks.size(), sinks));
+               for (Filter filter : pipeline.filters()) {
+                       Collection<Filter> sinks = pipeline.filters(filter);
+                       logger.finest(String.format("%s has %d filters: %s", filter.name(), sinks.size(), sinks));
                        if (sinks.isEmpty()) {
                                sinkCount++;
                        }
@@ -120,120 +113,119 @@ public class PipelinePanel extends JPanel {
                        gridCellCount *= n;
                }
 
-               /* paint all components recursively. */
-               addControlled(pipeline.source(), 0, 0, gridCellCount, null);
+               /* paint all filters recursively. */
+               addFilter(pipeline.source(), 0, 0, gridCellCount, null);
        }
 
        /**
-        * Displays the given component.
+        * Displays the given filter.
         *
-        * @param controlledComponent
-        *              The component to add this panel.
+        * @param filter
+        *              The filter to add this panel.
         * @param level
-        *              The level at which to show the component (the source is level {@code 0})
+        *              The level at which to show the filter (the source is level {@code 0})
         * @param position
-        *              The position at which to display the component
+        *              The position at which to display the filter
         * @param width
-        *              The width of the component in grid cells
+        *              The width of the filter in grid cells
         */
-       private void addControlled(final ControlledComponent controlledComponent, int level, int position, int width, ControlledComponent parentComponent) {
-               /* create a GUI component that displays the component. */
-               final JPanel componentPanel = createComponentPanel(controlledComponent, parentComponent);
-               componentPanel.addMouseListener(new MouseAdapter() {
+       private void addFilter(final Filter filter, int level, int position, int width, Filter parentFilter) {
+               /* create a GUI component that displays the filter. */
+               final JPanel filterPanel = createFilterPanel(filter, parentFilter);
+               filterPanel.addMouseListener(new MouseAdapter() {
 
                        @Override
                        public void mouseClicked(MouseEvent e) {
                                for (Component component : getComponents()) {
                                        component.setBackground(UIManager.getColor("Panel.background"));
                                }
-                               for (ComponentSelectionListener componentSelectionListener : componentSelectionListeners.getListeners(ComponentSelectionListener.class)) {
-                                       componentPanel.setBackground(Color.LIGHT_GRAY);
-                                       componentSelectionListener.componentSelected(controlledComponent);
+                               for (FilterSelectionListener filterSelectionListener : filterSelectionListeners.getListeners(FilterSelectionListener.class)) {
+                                       filterPanel.setBackground(Color.LIGHT_GRAY);
+                                       filterSelectionListener.filterSelected(filter);
                                }
-                               selectedComponent = componentPanel;
+                               selectedFilter = filterPanel;
                        }
 
                        @Override
                        public void mouseEntered(MouseEvent mouseEvent) {
-                               if (componentPanel != selectedComponent) {
-                                       componentPanel.setBackground(Color.white);
+                               if (filterPanel != selectedFilter) {
+                                       filterPanel.setBackground(Color.white);
                                }
                        }
 
                        @Override
                        public void mouseExited(MouseEvent mouseEvent) {
-                               if (componentPanel != selectedComponent) {
-                                       componentPanel.setBackground(UIManager.getColor("Panel.background"));
+                               if (filterPanel != selectedFilter) {
+                                       filterPanel.setBackground(UIManager.getColor("Panel.background"));
                                }
                        }
                });
 
-               /* show component. */
-               add(componentPanel, new GridBagConstraints(position, level, width, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
+               /* show filter. */
+               add(filterPanel, new GridBagConstraints(position, level, width, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
 
-               /* if the component does not have connected sinks, exit here. */
-               if (!(controlledComponent instanceof Source)) {
+               /* if the filter does not have connected filters, exit here. */
+               Collection<Filter> sinks = pipeline.filters(filter);
+               if (sinks.isEmpty()) {
                        add(new JPanel(), new GridBagConstraints(position, 999, width, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
                        return;
                }
 
-               /* iterate over the component’s sinks. */
-               Collection<Sink> sinks = pipeline.sinks((Source) controlledComponent);
+               /* iterate over the filter’s connected filters. */
                if (!sinks.isEmpty()) {
                        int sinkWidth = width / sinks.size();
                        int sinkIndex = 0;
-                       for (Sink connectedSink : sinks) {
-                               /* distribute all sinks evenly below this source. */
-                               addControlled(connectedSink, level + 1, position + sinkIndex * sinkWidth, sinkWidth, controlledComponent);
+                       for (Filter connectedSink : sinks) {
+                               /* distribute all filters evenly below this source. */
+                               addFilter(connectedSink, level + 1, position + sinkIndex * sinkWidth, sinkWidth, filter);
                                sinkIndex++;
                        }
                }
        }
 
        /**
-        * Creates a panel displaying a single component.
+        * Creates a panel displaying a single filter.
         *
-        * @param controlledComponent
-        *              The component to display
+        * @param filter
+        *              The filter to display
         * @return The created panel
         */
-       private static JPanel createComponentPanel(final ControlledComponent controlledComponent, final ControlledComponent parentComponent) {
-               JPanel componentPanel = new JPanel(new BorderLayout(12, 12));
-               componentPanel.setBorder(createCompoundBorder(createEtchedBorder(), createEmptyBorder(0, 4, 0, 3)));
-               componentPanel.add(new JLabel(controlledComponent.name()), BorderLayout.WEST);
-               final JLabel titleLabel = new JLabel(controlledComponent.metadata().fullTitle());
+       private static JPanel createFilterPanel(final Filter filter, final Filter parentFilter) {
+               JPanel filterPanel = new JPanel(new BorderLayout(12, 12));
+               filterPanel.setBorder(createCompoundBorder(createEtchedBorder(), createEmptyBorder(0, 4, 0, 3)));
+               filterPanel.add(new JLabel(filter.name()), BorderLayout.WEST);
+               final JLabel titleLabel = new JLabel(filter.metadata().fullTitle());
                titleLabel.setFont(titleLabel.getFont().deriveFont(titleLabel.getFont().getSize2D() * 0.8f));
-               componentPanel.add(titleLabel, BorderLayout.EAST);
-               if (parentComponent != null) {
-                       titleLabel.setVisible(!parentComponent.metadata().fullTitle().equals(controlledComponent.metadata().fullTitle()));
+               filterPanel.add(titleLabel, BorderLayout.EAST);
+               if (parentFilter != null) {
+                       titleLabel.setVisible(!parentFilter.metadata().fullTitle().equals(filter.metadata().fullTitle()));
                }
-               controlledComponent.addMetadataListener(new MetadataListener() {
+               filter.addMetadataListener(new MetadataListener() {
 
                        @Override
-                       public void metadataUpdated(ControlledComponent component, Metadata metadata) {
+                       public void metadataUpdated(Filter filter, Metadata metadata) {
                                titleLabel.setText(metadata.fullTitle());
-                               titleLabel.setVisible((parentComponent == null) || !parentComponent.metadata().fullTitle().equals(metadata.fullTitle()));
+                               titleLabel.setVisible((parentFilter == null) || !parentFilter.metadata().fullTitle().equals(metadata.fullTitle()));
                        }
                });
-               return componentPanel;
+               return filterPanel;
        }
 
        /**
         * Interface for objects that want to be notified if the user moves the mouse
-        * cursor over a controlled component.
+        * cursor over a filter.
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
         */
-       public static interface ComponentSelectionListener extends EventListener {
+       public static interface FilterSelectionListener extends EventListener {
 
                /**
-                * Notifies the listener that the mouse is now over the given controlled
-                * component.
+                * Notifies the listener that the mouse is now over the given filter.
                 *
-                * @param controlledComponent
-                *              The controlled component now under the mouse
+                * @param filter
+                *              The filter now under the mouse
                 */
-               void componentSelected(ControlledComponent controlledComponent);
+               void filterSelected(Filter filter);
 
        }
 
index e3350a7..dba2f3c 100644 (file)
@@ -22,9 +22,7 @@ import java.awt.GridBagLayout;
 import java.awt.Insets;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import javax.swing.BorderFactory;
 import javax.swing.JCheckBox;
-import javax.swing.JLabel;
 import javax.swing.JPanel;
 
 import net.pterodactylus.sonitus.data.controller.Switch;
@@ -39,8 +37,8 @@ public class SwitchPanel extends JPanel {
        /**
         * Creates a new fader panel.
         *
-        * @param fader
-        *              The fader being controlled
+        * @param switchController
+        *              The switch being controlled
         */
        public SwitchPanel(final Switch switchController) {
                super(new GridBagLayout());
diff --git a/src/main/java/net/pterodactylus/sonitus/io/FlacIdentifier.java b/src/main/java/net/pterodactylus/sonitus/io/FlacIdentifier.java
new file mode 100644 (file)
index 0000000..5e01555
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Sonitus - FlacIdentifier.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.io;
+
+import static net.pterodactylus.sonitus.io.flac.BlockType.STREAMINFO;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import net.pterodactylus.sonitus.data.ContentMetadata;
+import net.pterodactylus.sonitus.data.FormatMetadata;
+import net.pterodactylus.sonitus.data.Metadata;
+import net.pterodactylus.sonitus.io.flac.MetadataBlock;
+import net.pterodactylus.sonitus.io.flac.Stream;
+import net.pterodactylus.sonitus.io.flac.StreamInfo;
+
+import com.google.common.base.Optional;
+
+/**
+ * An identifier for FLAC input streams.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FlacIdentifier {
+
+       /**
+        * Tries to identify the FLAC file contained in the given stream.
+        *
+        * @param inputStream
+        *              The input stream
+        * @return The identified metadata, or {@link Optional#absent()} if the
+        *         metadata can not be identified
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       public static Optional<Metadata> identify(InputStream inputStream) throws IOException {
+               Optional<Stream> stream = Stream.parse(inputStream);
+               if (!stream.isPresent()) {
+                       return Optional.absent();
+               }
+
+               List<MetadataBlock> streamInfos = stream.get().metadataBlocks(STREAMINFO);
+               if (streamInfos.isEmpty()) {
+                       /* FLAC file without STREAMINFO is invalid. */
+                       return Optional.absent();
+               }
+
+               MetadataBlock streamInfoBlock = streamInfos.get(0);
+               StreamInfo streamInfo = (StreamInfo) streamInfoBlock.data();
+
+               return Optional.of(new Metadata(new FormatMetadata(streamInfo.numberOfChannels(), streamInfo.sampleRate(), "FLAC"), new ContentMetadata()));
+       }
+
+}
index 6035cbf..3a4a9fb 100644 (file)
@@ -84,8 +84,19 @@ public class IdentifyingInputStream extends FilterInputStream {
                /* remember everything we read here. */
                RememberingInputStream rememberingInputStream = new RememberingInputStream(inputStream);
 
-               /* try Ogg Vorbis first. */
+               /* first, try formats with unambiguous layouts. */
                try {
+                       Optional<Metadata> metadata = FlacIdentifier.identify(rememberingInputStream);
+                       if (metadata.isPresent()) {
+                               return Optional.of(new IdentifyingInputStream(rememberingInputStream.remembered(), metadata.get()));
+                       }
+               } catch (EOFException eofe1) {
+                       /* ignore. */
+               }
+
+               /* try Ogg Vorbis next. */
+               try {
+                       rememberingInputStream = new RememberingInputStream(rememberingInputStream.remembered());
                        Optional<Metadata> metadata = OggVorbisIdentifier.identify(rememberingInputStream);
                        if (metadata.isPresent()) {
                                return Optional.of(new IdentifyingInputStream(rememberingInputStream.remembered(), metadata.get()));
@@ -94,7 +105,7 @@ public class IdentifyingInputStream extends FilterInputStream {
                        /* ignore. */
                }
 
-               /* try MP3 now. */
+               /* finally, try MP3. */
                try {
                        rememberingInputStream = new RememberingInputStream(rememberingInputStream.remembered());
                        InputStream limitedInputStream = ByteStreams.limit(rememberingInputStream, 1048576);
index ded9df1..9afc1f5 100644 (file)
@@ -33,7 +33,7 @@ public abstract class ProcessingOutputStream extends FilterOutputStream {
        private final int channels;
 
        /** The current sample’s channel values. */
-       private int[] currentSamples;
+       private final int[] currentSamples;
 
        /** The index of the current channel. */
        private int currentSampleIndex;
index 5fae4b2..8c2e3e6 100644 (file)
@@ -81,11 +81,7 @@ public class RememberingInputStream extends FilterInputStream {
 
        @Override
        public int read(byte[] bytes) throws IOException {
-               int read = super.read(bytes);
-               if (read != -1) {
-                       rememberBuffer.write(bytes, 0, read);
-               }
-               return read;
+               return read(bytes, 0, bytes.length);
        }
 
        @Override
diff --git a/src/main/java/net/pterodactylus/sonitus/io/flac/BlockType.java b/src/main/java/net/pterodactylus/sonitus/io/flac/BlockType.java
new file mode 100644 (file)
index 0000000..8cecc02
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Sonitus - BlockType.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.io.flac;
+
+/**
+ * The type of a metadata block.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public enum BlockType {
+
+       /** A STREAMINFO block. */
+       STREAMINFO {
+               @Override
+               public Data createData(byte[] content) {
+                       return new StreamInfo(content);
+               }
+       },
+
+       /** A PADDING block. */
+       PADDING,
+
+       /** An APPLICATION block. */
+       APPLICATION,
+
+       /** A SEEKTABLE block. */
+       SEEKTABLE,
+
+       /** A VORBIS_COMMENT block. */
+       VORBIS_COMMENT,
+
+       /** A CUESHEET block. */
+       CUESHEET,
+
+       /** A PICTURE block. */
+       PICTURE,
+
+       /** A RESERVED block. */
+       RESERVED,
+
+       /** An INVALID block. */
+       INVALID;
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Creates a {@link Data} object from the given byte array. Block type
+        * enumeration values can override this to return specialized parser objects.
+        *
+        * @param content
+        *              The content of the metadata block
+        * @return The metadata block as a data object
+        */
+       public Data createData(byte[] content) {
+               return new Data(content);
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Creates a block type from the given block type number.
+        *
+        * @param blockType
+        *              The block type number
+        * @return The parsed block type
+        */
+       public static BlockType valueOf(int blockType) {
+               if ((blockType >= 0) && (blockType <= 6)) {
+                       return values()[blockType];
+               }
+               if ((blockType > 6) && (blockType < 127)) {
+                       return RESERVED;
+               }
+               return INVALID;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sonitus/io/flac/Data.java b/src/main/java/net/pterodactylus/sonitus/io/flac/Data.java
new file mode 100644 (file)
index 0000000..d054494
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Sonitus - Data.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.io.flac;
+
+import static com.google.common.io.ByteStreams.readFully;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Accessor type that can parse the contents of a {@link MetadataBlock}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Data {
+
+       /** The content of the metadata block. */
+       private final byte[] content;
+
+       /**
+        * Creates a new data accessor.
+        *
+        * @param content
+        *              The content of the metadata block
+        */
+       public Data(byte[] content) {
+               this.content = content;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the content of this metadata block.
+        *
+        * @return The content of the metadata block
+        */
+       public byte[] content() {
+               return content;
+       }
+
+       //
+       // SUBCLASS METHODS
+       //
+
+       /**
+        * Parses the given number of bits from the content of this metadata block,
+        * starting at the given byte offset and bit offset (bit 0 being the most
+        * significant bit).
+        *
+        * @param byteOffset
+        *              The byte offset at which to start reading
+        * @param bitOffset
+        *              The bit offset at which to start reading
+        * @param numberOfBits
+        *              The number of bits to parse (should be <= 64)
+        * @return The parsed bits
+        */
+       protected long parseBits(int byteOffset, int bitOffset, int numberOfBits) {
+               long value = 0;
+               int currentByteOffset = byteOffset;
+               int currentBitOffset = bitOffset;
+               int bitsRemaining = numberOfBits;
+
+               while (bitsRemaining > 0) {
+                       value = (value << Math.min(8, bitsRemaining)) | ((content[currentByteOffset] & (0xff >>> currentBitOffset)) >> (8 - currentBitOffset - Math.min(bitsRemaining, 8 - currentBitOffset)));
+                       bitsRemaining -= Math.min(bitsRemaining, 8 - currentBitOffset);
+                       currentBitOffset = 0;
+                       currentByteOffset++;
+               }
+
+               return value;
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Creates a new data accessor from the given input stream.
+        *
+        * @param inputStream
+        *              The input stream to read the contents of the metadata block from
+        * @param blockType
+        *              The type of the metadata block
+        * @param length
+        *              The length of the metadata block
+        * @return The parsed metadata block
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       public static Data parse(InputStream inputStream, BlockType blockType, int length) throws IOException {
+               byte[] buffer = new byte[length];
+               readFully(inputStream, buffer);
+               return blockType.createData(buffer);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sonitus/io/flac/Header.java b/src/main/java/net/pterodactylus/sonitus/io/flac/Header.java
new file mode 100644 (file)
index 0000000..bd6770e
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Sonitus - Header.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.io.flac;
+
+import static com.google.common.io.ByteStreams.readFully;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Header for a {@link MetadataBlock}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Header {
+
+       /** Whether this metadata block is the last metadata block. */
+       private final boolean lastMetadataBlock;
+
+       /** The type of the metadata block. */
+       private final BlockType blockType;
+
+       /** The length of the metadata block. */
+       private final int length;
+
+       /**
+        * Creates a new metadata block header.
+        *
+        * @param lastMetadataBlock
+        *              {@code true} if this metadata block is the last metadata block in the FLAC
+        *              stream, {@code false} otherwise
+        * @param blockType
+        *              The type of the metadata block
+        * @param length
+        *              The length of the metadata block
+        */
+       private Header(boolean lastMetadataBlock, BlockType blockType, int length) {
+               this.lastMetadataBlock = lastMetadataBlock;
+               this.blockType = blockType;
+               this.length = length;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns whether this metadata block is the last metadata block in the FLAC
+        * {@link Stream}.
+        *
+        * @return {@code true} if this metadata block is last metadata block in the
+        *         FLAC stream, {@code false} otherwise
+        */
+       public boolean isLastMetadataBlock() {
+               return lastMetadataBlock;
+       }
+
+       /**
+        * Returns the type of the metadata block.
+        *
+        * @return The type of the metadata block
+        */
+       public BlockType blockType() {
+               return blockType;
+       }
+
+       /**
+        * Returns the length of the metadata block.
+        *
+        * @return The length of the metadata block
+        */
+       public int length() {
+               return length;
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Parses a metadata block header from the current position of the given input
+        * stream.
+        *
+        * @param inputStream
+        *              The input stream to parse the header from
+        * @return The parsed header
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       public static Header parse(InputStream inputStream) throws IOException {
+               byte[] buffer = new byte[4];
+               readFully(inputStream, buffer);
+               boolean lastMetadataBlock = ((buffer[0] >> 7) & 0x01) != 0;
+               BlockType blockType = BlockType.valueOf(buffer[0] & 0x7f);
+               int length = ((buffer[1] & 0xff) << 16) | ((buffer[2] & 0xff) << 8) | (buffer[3] & 0xff);
+               return new Header(lastMetadataBlock, blockType, length);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sonitus/io/flac/MetadataBlock.java b/src/main/java/net/pterodactylus/sonitus/io/flac/MetadataBlock.java
new file mode 100644 (file)
index 0000000..9966714
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Sonitus - MetadataBlock.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.io.flac;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A metadata block.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MetadataBlock {
+
+       /** The header of the metadata block. */
+       private final Header header;
+
+       /** The data of the metadata block. */
+       private final Data data;
+
+       /**
+        * Creates a new metadata block.
+        *
+        * @param header
+        *              The header of the metadata block
+        * @param data
+        *              The data of the metadata block
+        */
+       MetadataBlock(Header header, Data data) {
+               this.header = header;
+               this.data = data;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the header of this metadata block.
+        *
+        * @return The header of this metadata block
+        */
+       public Header header() {
+               return header;
+       }
+
+       /**
+        * Returns the data of this metadata block.
+        *
+        * @return The data of this metadata block
+        */
+       public Data data() {
+               return data;
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Parses the metadata block from the current position of the given input
+        * stream.
+        *
+        * @param inputStream
+        *              The input stream to parse the metadata block from
+        * @return The parsed metadata block
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       public static MetadataBlock parse(InputStream inputStream) throws IOException {
+               Header header = Header.parse(inputStream);
+               Data data = Data.parse(inputStream, header.blockType(), header.length());
+               return new MetadataBlock(header, data);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sonitus/io/flac/Stream.java b/src/main/java/net/pterodactylus/sonitus/io/flac/Stream.java
new file mode 100644 (file)
index 0000000..30a653e
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Sonitus - MetadataBlock.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.io.flac;
+
+import static com.google.common.io.ByteStreams.readFully;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Lists;
+
+/**
+ * Parser and container for information about a FLAC stream.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Stream {
+
+       /** The metadata blocks of the stream. */
+       private final List<MetadataBlock> metadataBlocks = Lists.newArrayList();
+
+       /**
+        * Creates a new FLAC stream containing the given metadata blocks.
+        *
+        * @param metadataBlocks
+        *              The metadata blocks in order of appearance
+        */
+       private Stream(List<MetadataBlock> metadataBlocks) {
+               this.metadataBlocks.addAll(metadataBlocks);
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns all metadata blocks of the given block type, in the order they
+        * appear in this stream.
+        *
+        * @param blockType
+        *              The block type to get all metadata blocks for
+        * @return The metadata blocks of the given block type
+        */
+       public List<MetadataBlock> metadataBlocks(final BlockType blockType) {
+               return FluentIterable.from(metadataBlocks).filter(new Predicate<MetadataBlock>() {
+
+                       @Override
+                       public boolean apply(MetadataBlock metadataBlock) {
+                               return metadataBlock.header().blockType() == blockType;
+                       }
+               }).toList();
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Parses the given input stream and returns information about the stream if it
+        * can be successfully parsed as a FLAC stream.
+        *
+        * @param inputStream
+        *              The input stream containing the FLAC stream
+        * @return The parsed FLAC stream, or {@link Optional#absent()} if no FLAC
+        *         stream could be found at the stream’s current position
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       public static Optional<Stream> parse(InputStream inputStream) throws IOException {
+               byte[] streamTag = new byte[4];
+               readFully(inputStream, streamTag);
+               if (!Arrays.equals(streamTag, new byte[] { 'f', 'L', 'a', 'C' })) {
+                       return Optional.absent();
+               }
+               List<MetadataBlock> metadataBlocks = Lists.newArrayList();
+               while (true) {
+                       MetadataBlock metadataBlock = MetadataBlock.parse(inputStream);
+                       metadataBlocks.add(metadataBlock);
+                       if (metadataBlock.header().isLastMetadataBlock()) {
+                               break;
+                       }
+               }
+               return Optional.of(new Stream(metadataBlocks));
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sonitus/io/flac/StreamInfo.java b/src/main/java/net/pterodactylus/sonitus/io/flac/StreamInfo.java
new file mode 100644 (file)
index 0000000..5dadd65
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Sonitus - StreamInfo.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.io.flac;
+
+/**
+ * Parser for a {@link BlockType#STREAMINFO} metadata block.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class StreamInfo extends Data {
+
+       /**
+        * Creates a new STREAMINFO block from the given buffer.
+        *
+        * @param content
+        *              The contents of the metadata block
+        */
+       public StreamInfo(byte[] content) {
+               super(content);
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the minimum block size.
+        *
+        * @return The minimum block size (in samples)
+        */
+       public int minimumBlockSize() {
+               return (int) parseBits(0, 0, 16);
+       }
+
+       /**
+        * Returns the maximum block size.
+        *
+        * @return The maximum block size (in samples)
+        */
+       public int maximumBlockSize() {
+               return (int) parseBits(2, 0, 16);
+       }
+
+       /**
+        * Returns the minimum frame size.
+        *
+        * @return The minimum frame size (in bytes)
+        */
+       public int minimumFrameSize() {
+               return (int) parseBits(4, 0, 24);
+       }
+
+       /**
+        * Returns the maximum frame size.
+        *
+        * @return The maximum frame size (in bytes)
+        */
+       public int maximumFrameSize() {
+               return (int) parseBits(7, 0, 24);
+       }
+
+       /**
+        * Returns the sample rate.
+        *
+        * @return The sample rate (in Hertz)
+        */
+       public int sampleRate() {
+               return (int) parseBits(10, 0, 20);
+       }
+
+       /**
+        * Returns the number of channels.
+        *
+        * @return The number of channels
+        */
+       public int numberOfChannels() {
+               return (int) (parseBits(12, 4, 3) + 1);
+       }
+
+       /**
+        * Returns the number of bits per sample.
+        *
+        * @return The number of bits per sample
+        */
+       public int bitsPerSample() {
+               return (int) (parseBits(12, 7, 5) + 1);
+       }
+
+       /**
+        * Returns the total number of samples.
+        *
+        * @return The total number of samples
+        */
+       public long totalSamples() {
+               return parseBits(13, 4, 36);
+       }
+
+}
index e6b38e0..b157e2c 100644 (file)
 
 package net.pterodactylus.sonitus.io.mp3;
 
+import java.util.Arrays;
 import java.util.Map;
 
 import com.google.common.base.Optional;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
-import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableMap;
 
 /**
@@ -132,7 +132,7 @@ public class Frame {
                        mpegAudioVersionMapBuilder.put(MpegAudioVersion.VERSION_1, mpeg1Builder.build());
 
                        /* MPEG 2 & 2.5. */
-                       ImmutableBiMap.Builder<LayerDescription, Map<Integer, Integer>> mpeg2Builder = ImmutableBiMap.builder();
+                       ImmutableMap.Builder<LayerDescription, Map<Integer, Integer>> mpeg2Builder = ImmutableMap.builder();
 
                        /* Layer 1. */
                        bitrates = ImmutableMap.builder();
@@ -216,6 +216,9 @@ public class Frame {
        /** The decoded emphasis mode. */
        private final int emphasis;
 
+       /** The content of the frame. */
+       private final byte[] content;
+
        /**
         * Creates a new frame from the given values.
         *
@@ -244,7 +247,7 @@ public class Frame {
         * @param emphasis
         *              The emphasis
         */
-       private Frame(int mpegAudioVersionId, int layerDescription, int protectionBit, int bitrateIndex, int samplingRateFrequencyIndex, int paddingBit, int privateBit, int channelMode, int modeExtension, int copyrightBit, int originalBit, int emphasis) {
+       private Frame(int mpegAudioVersionId, int layerDescription, int protectionBit, int bitrateIndex, int samplingRateFrequencyIndex, int paddingBit, int privateBit, int channelMode, int modeExtension, int copyrightBit, int originalBit, int emphasis, byte[] content) {
                this.mpegAudioVersionId = mpegAudioVersionId;
                this.layerDescription = layerDescription;
                this.protectionBit = protectionBit;
@@ -257,6 +260,7 @@ public class Frame {
                this.copyrightBit = copyrightBit;
                this.originalBit = originalBit;
                this.emphasis = emphasis;
+               this.content = content;
        }
 
        //
@@ -364,6 +368,15 @@ public class Frame {
                return Emphasis.values()[emphasis];
        }
 
+       /**
+        * Returns the content of this frame.
+        *
+        * @return The content of this frame
+        */
+       public byte[] content() {
+               return content;
+       }
+
        //
        // STATIC METHODS
        //
@@ -385,6 +398,33 @@ public class Frame {
        }
 
        /**
+        * Calculates the frame length in bytes for the frame starting at the given
+        * offset in the given buffer. This method should only be called for a buffer
+        * and an offset for which {@link #isFrame(byte[], int, int)} returns {@code
+        * true}.
+        *
+        * @param buffer
+        *              The buffer storing the frame
+        * @param offset
+        *              The offset of the frame
+        * @return The length of the frame in bytes, or {@code -1} if the frame length
+        *         can not be calculated
+        */
+       public static int getFrameLength(byte[] buffer, int offset) {
+               MpegAudioVersion mpegAudioVersion = MpegAudioVersion.values()[(buffer[offset + 1] & 0x18) >>> 3];
+               LayerDescription layerDescription = LayerDescription.values()[(buffer[offset + 1] & 0x06) >>> 1];
+               int bitrate = bitrateSupplier.get().get(mpegAudioVersion).get(layerDescription).get((buffer[offset + 2] & 0xf0) >>> 4) * 1000;
+               int samplingRate = samplingRateSupplier.get().get(mpegAudioVersion).get((buffer[offset + 2] & 0x0c) >>> 2);
+               int paddingBit = (buffer[offset + 2] & 0x02) >>> 1;
+               if (layerDescription == LayerDescription.LAYER_1) {
+                       return (12 * bitrate / samplingRate + paddingBit) * 4;
+               } else if ((layerDescription == LayerDescription.LAYER_2) || (layerDescription == LayerDescription.LAYER_3)) {
+                       return 144 * bitrate / samplingRate + paddingBit;
+               }
+               return -1;
+       }
+
+       /**
         * Tries to create an MPEG audio from the given data.
         *
         * @param buffer
@@ -410,7 +450,8 @@ public class Frame {
                        int copyright = (buffer[offset + 3] & 0x08) >> 3;
                        int original = (buffer[offset + 3] & 0x04) >> 2;
                        int emphasis = buffer[offset + 3] & 0x03;
-                       return Optional.of(new Frame(mpegAudioVersionId, layerDescription, protectionBit, bitrateIndex, samplingRateFrequencyIndex, paddingBit, privateBit, channelMode, modeExtension, copyright, original, emphasis));
+                       int frameLength = getFrameLength(buffer, offset);
+                       return Optional.of(new Frame(mpegAudioVersionId, layerDescription, protectionBit, bitrateIndex, samplingRateFrequencyIndex, paddingBit, privateBit, channelMode, modeExtension, copyright, original, emphasis, Arrays.copyOfRange(buffer, 4, frameLength)));
                }
                return Optional.absent();
        }
index 8a7e771..bbdb5a3 100644 (file)
@@ -53,16 +53,16 @@ public class Parser {
         */
        public Parser(InputStream inputStream) throws IOException {
                this.inputStream = inputStream;
-               readFully(inputStream, buffer, 0, 3);
-               if ((buffer[0] == 'I') && (buffer[1] == 'D') && (buffer[2] == '3')) {
-                       readFully(inputStream, buffer, 0, 3);
+               readFully(inputStream, buffer, 1, 3);
+               if ((buffer[1] == 'I') && (buffer[2] == 'D') && (buffer[3] == '3')) {
+                       readFully(inputStream, buffer, 1, 3);
                        byte[] lengthBuffer = new byte[4];
                        readFully(inputStream, lengthBuffer, 0, 4);
                        int headerLength = (lengthBuffer[0] << 21) | (lengthBuffer[1] << 14) | (lengthBuffer[2] << 7) | lengthBuffer[3];
                        id3Tag = new byte[headerLength + 10];
-                       System.arraycopy(new byte[] { 'I', 'D', '3', buffer[0], buffer[1], buffer[2], lengthBuffer[0], lengthBuffer[1], lengthBuffer[2], lengthBuffer[3] }, 0, id3Tag, 0, 10);
+                       System.arraycopy(new byte[] { 'I', 'D', '3', buffer[1], buffer[2], buffer[3], lengthBuffer[0], lengthBuffer[1], lengthBuffer[2], lengthBuffer[3] }, 0, id3Tag, 0, 10);
                        readFully(inputStream, id3Tag, 10, headerLength);
-                       readFully(inputStream, buffer, 0, 3);
+                       readFully(inputStream, buffer, 1, 3);
                } else {
                        id3Tag = null;
                }
@@ -93,9 +93,17 @@ public class Parser {
                        }
                        System.arraycopy(buffer, 1, buffer, 0, 3);
                        buffer[3] = (byte) r;
-                       Optional<Frame> frame = Frame.create(buffer, 0, 4);
-                       if (frame.isPresent()) {
-                               return frame.get();
+                       if (Frame.isFrame(buffer, 0, 4)) {
+                               int frameLength = Frame.getFrameLength(buffer, 0);
+                               if (frameLength != -1) {
+                                       byte[] content = new byte[frameLength + 4];
+                                       readFully(inputStream, content, 4, frameLength);
+                                       System.arraycopy(buffer, 0, content, 0, 4);
+                                       Optional<Frame> frame = Frame.create(content, 0, frameLength + 4);
+                                       if (frame.isPresent()) {
+                                               return frame.get();
+                                       }
+                               }
                        }
                }
        }
diff --git a/src/test/java/net/pterodactylus/sonitus/io/RememberingInputStreamTest.java b/src/test/java/net/pterodactylus/sonitus/io/RememberingInputStreamTest.java
new file mode 100644 (file)
index 0000000..05104bb
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Sonitus - RememberingInputStreamTest.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sonitus.io;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Random;
+
+import com.google.common.io.ByteStreams;
+import org.testng.annotations.Test;
+
+/**
+ * Test case for {@link RememberingInputStream}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RememberingInputStreamTest {
+
+       /**
+        * Tests {@link RememberingInputStream#remembered()}.
+        *
+        * @throws IOException
+        *              if an I/O error occurs
+        */
+       @Test
+       public void test() throws IOException {
+               RememberingInputStream rememberingInputStream;
+               byte[] randomData = generateData(System.currentTimeMillis(), 1048576);
+               InputStream inputStream = new ByteArrayInputStream(randomData);
+               byte[] readBytes;
+
+               rememberingInputStream = new RememberingInputStream(inputStream);
+               readBytes = new byte[524288];
+               ByteStreams.readFully(rememberingInputStream, readBytes);
+               assertThat(readBytes, is(Arrays.copyOfRange(randomData, 0, 524288)));
+
+               rememberingInputStream = new RememberingInputStream(rememberingInputStream.remembered());
+               readBytes = new byte[131072];
+               ByteStreams.readFully(rememberingInputStream, readBytes);
+               assertThat(readBytes, is(Arrays.copyOfRange(randomData, 0, 131072)));
+
+               rememberingInputStream = new RememberingInputStream(rememberingInputStream.remembered());
+               readBytes = new byte[1048576];
+               ByteStreams.readFully(rememberingInputStream, readBytes);
+               assertThat(readBytes, is(Arrays.copyOfRange(randomData, 0, 1048576)));
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Generates random data.
+        *
+        * @param seed
+        *              The seed for the random number generator
+        * @param length
+        *              The length of the data to generate
+        * @return The generated random data
+        */
+       private byte[] generateData(long seed, int length) {
+               Random random = new Random(seed);
+               byte[] buffer = new byte[length];
+               random.nextBytes(buffer);
+               return buffer;
+       }
+
+}