+++ /dev/null
-/*
- * 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);
- }
- }
-
-}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
+++ /dev/null
-/*
- * 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);
-
-}
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;
}
*/
public class Metadata {
+ /** Marker for unknown metadata. */
+ public static final Metadata UNKNOWN = new Metadata();
+
/** The format metadata. */
private final FormatMetadata formatMetadata;
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);
}
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();
*
* @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);
}
}
});
*
* @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();
}
}
* 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
*/
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();
}
}));
//
@Override
- public Iterator<ControlledComponent> iterator() {
- return components().iterator();
+ public Iterator<Filter> iterator() {
+ return filters().iterator();
}
//
//
/**
- * 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();
}
//
* 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);
}
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.
* @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
* @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;
}
/**
- * 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>
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);
* @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) {
} 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
* 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);
* 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);
+++ /dev/null
-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;
-
-}
+++ /dev/null
-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;
-
-}
+++ /dev/null
-/*
- * 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;
- }
-
-}
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;
*
* @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.
}
//
- // DUMMYFILTER METHODS
+ // FILTER METHODS
//
@Override
+++ /dev/null
-/*
- * 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);
- }
-
-}
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;
*
* @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());
process.destroy();
}
- //
- // DUMMYFILTER METHODS
- //
-
@Override
protected InputStream createInputStream() throws IOException {
return process.getInputStream();
//
@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"));
}
//
+++ /dev/null
-/*
- * 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);
- }
- }
-
-}
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;
/**
*
* @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());
//
@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));
}
//
}
//
- // CONTROLLED METHODS
+ // FILTER METHODS
//
@Override
return Arrays.<Controller<?>>asList(separationKnob);
}
- //
- // AUDIOPROCESSINGFILTER METHODS
- //
-
@Override
protected int[] processSamples(int[] samples) {
if (samples.length == 1) {
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;
*
* @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();
}
//
- // DUMMYFILTER METHODS
+ // FILTER METHODS
//
@Override
}
//
- // CONTROLLED METHODS
+ // FILTER METHODS
//
@Override
return Arrays.<Controller<?>>asList(volumeFader, muteSwitch);
}
- //
- // AUDIOPROCESSINGFILTER METHODS
- //
-
@Override
protected int[] processSamples(int[] samples) {
int[] processedSamples = new int[samples.length];
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;
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());
}
//
- // CONTROLLED METHODS
+ // FILTER METHODS
//
@Override
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);
@Override
public void process(byte[] buffer) throws IOException {
sourceDataLineOutputStream.write(buffer);
+ super.process(buffer);
logger.finest(String.format("AudioSink: Wrote %d Bytes.", buffer.length));
}
//
/**
- * Returns the {@link FloatControl.Type.VOLUME} control.
+ * Returns the {@link FloatControl.Type#VOLUME} control.
*
* @param dataLine
* The data line to search for the control
}
/**
- * Returns the {@link BooleanControl.Type.MUTE} control.
+ * Returns the {@link BooleanControl.Type#MUTE} control.
*
* @param dataLine
* The data line to search for the control
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
*
* @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());
}
//
- // CONTROLLED METHODS
- //
-
- @Override
- public List<Controller<?>> controllers() {
- return Collections.emptyList();
- }
-
- //
- // SINK METHODS
+ // FILTER METHODS
//
@Override
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;
*
* @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());
}
//
- // CONTROLLED METHODS
+ // FILTER METHODS
//
@Override
return Collections.emptyList();
}
- //
- // SINK METHODS
- //
-
@Override
public void open(Metadata metadata) throws IOException {
logger.info(String.format("Connecting to %s:%d...", server, port));
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;
}
//
- // CONTROLLED METHODS
+ // FILTER METHODS
//
@Override
return Collections.emptyList();
}
- //
- // SOURCE METHODS
- //
-
@Override
public byte[] get(int bufferSize) throws IOException {
byte[] buffer = new byte[bufferSize];
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());
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;
* @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;
/**
* 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
*/
}
//
- // CONTROLLED METHODS
+ // FILTER METHODS
//
@Override
return super.metadata();
}
- //
- // SOURCE METHODS
- //
-
@Override
public byte[] get(int bufferSize) throws EOFException, IOException {
while (true) {
// 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) {
import java.util.EventListener;
-import net.pterodactylus.sonitus.data.Source;
-
/**
* Interface for {@link MultiSource} notifications if a source is finished
* playing.
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;
*
* @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());
}
//
- // CONTROLLED METHODS
+ // FILTER METHODS
//
@Override
return Collections.emptyList();
}
- //
- // SOURCE METHODS
- //
-
@Override
public Metadata metadata() {
Optional<ContentMetadata> streamMetadata = metadataStream.getContentMetadata();
+++ /dev/null
-/*
- * 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);
- }
-
-}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
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;
/** 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. */
/** 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.
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);
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() {
@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()));
}
}
});
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>
*/
/** 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.
//
/**
- * 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);
}
//
/* 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++;
}
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);
}
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;
/**
* 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());
--- /dev/null
+/*
+ * 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()));
+ }
+
+}
/* 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()));
/* ignore. */
}
- /* try MP3 now. */
+ /* finally, try MP3. */
try {
rememberingInputStream = new RememberingInputStream(rememberingInputStream.remembered());
InputStream limitedInputStream = ByteStreams.limit(rememberingInputStream, 1048576);
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;
@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
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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));
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
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;
/**
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();
/** 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.
*
* @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;
this.copyrightBit = copyrightBit;
this.originalBit = originalBit;
this.emphasis = emphasis;
+ this.content = content;
}
//
return Emphasis.values()[emphasis];
}
+ /**
+ * Returns the content of this frame.
+ *
+ * @return The content of this frame
+ */
+ public byte[] content() {
+ return content;
+ }
+
//
// STATIC METHODS
//
}
/**
+ * 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
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();
}
*/
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;
}
}
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();
+ }
+ }
}
}
}
--- /dev/null
+/*
+ * 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;
+ }
+
+}