diff --git a/src/schule/ngb/zm/media/Audio.java b/src/schule/ngb/zm/media/Audio.java
new file mode 100644
index 0000000..e2f5137
--- /dev/null
+++ b/src/schule/ngb/zm/media/Audio.java
@@ -0,0 +1,70 @@
+package schule.ngb.zm.media;
+
+/**
+ * Interface für Audio-Medien.
+ */
+public interface Audio {
+
+ /**
+ * Prüft, ob das Medium gerade abgespielt wird.
+ *
+ * @return {@code true}, wenn das Medium abgespielt wird, {@code false}
+ * sonst.
+ */
+ boolean isPlaying();
+
+ /**
+ * Prüft, ob das Medium gerade in einer Schleife abgespielt wird. Wenn
+ * {@code isLooping() == true}, dann muss auch immer
+ * {@code isPlaying() == true} gelten.
+ *
+ * @return @return {@code true}, wenn das Medium in einer Schleife
+ * abgespielt wird, {@code false} sonst.
+ */
+ boolean isLooping();
+
+ /**
+ * Legt die Lautstärke des Mediums beim Abspielen fest.
+ *
+ * Die Lautstärke wird auf einer linearen Skale festgelegt, wobei 0 kein Ton
+ * und 1 volle Lautstärke bedeutet. Werte über 1 verstärken den Ton des
+ * Mediums.
+ *
+ * @param volume Die neue Lautstärke zwischen 0 und 1.
+ * @see https://stackoverflow.com/a/40698149
+ */
+ void setVolume( double volume );
+
+ /**
+ * Startet die Wiedergabe des Mediums und beendet die Methode. Das
+ * Audio-Medium wird einmal abgespielt und stoppt dann.
+ */
+ void playOnce();
+
+ /**
+ * Startet die Wiedergabe des Mediums und blockiert das Programm, bis die
+ * Wiedergabe beendet ist.
+ */
+ void playOnceAndWait();
+
+ /**
+ * Spielt das Medium in einer kontinuierlichen Schleife ab. Die Methode
+ * startet die Wiedergabe und beendet dann direkt die Methode. Um die
+ * Wiedergabe zu stoppen muss {@link #stop()} aufgerufen werden.
+ */
+ void loop();
+
+ /**
+ * Stoppt die Wiedergabe. Wird das Medium gerade nicht abgespielt
+ * {@code isPlaying() == false}, dann passiert nichts.
+ */
+ void stop();
+
+ /**
+ * Stoppt die Wiedergabe und gibt alle Resourcen, die für das Medium
+ * verwendet werden, frei.
+ */
+ void dispose();
+
+}
diff --git a/src/schule/ngb/zm/media/Mixer.java b/src/schule/ngb/zm/media/Mixer.java
new file mode 100644
index 0000000..91b2064
--- /dev/null
+++ b/src/schule/ngb/zm/media/Mixer.java
@@ -0,0 +1,129 @@
+package schule.ngb.zm.media;
+
+import schule.ngb.zm.Constants;
+import schule.ngb.zm.tasks.TaskRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Ein Mixer ist eine Sammlung mehrerer {@link Audio Audio-Medien}, die
+ * gemeinsam kontrolliert werden können.
+ *
+ * Im einfachsten Fall kann die Audio-Gruppe gemeinsam gestartet und gestoppt
+ * werden. Ein Mixer kann die Lautstärke der Medien in Relation zueinander
+ * setzen. Dazu wird jedem Medium ein Faktor mitgegeben. Ein Medium mit dem
+ * Faktor 0.5 ist dann halb so laut wie eines, mit dem Faktor 1.0.
+ *
+ * Darüber hinaus kann ein Mixer Effekte wie einen
+ * {@link #fade(double, int) fadeIn} auf die Medien anwenden.
+ */
+public class Mixer implements Audio {
+
+ private List audios;
+
+ private float volume = 0.8f;
+
+ class AudioWrapper {
+
+ Audio audio;
+
+ float volumeFactor;
+
+ public AudioWrapper( Audio audio, float volumeFactor ) {
+ this.audio = audio;
+ this.volumeFactor = volumeFactor;
+ }
+
+ }
+
+ public Mixer() {
+ this.audios = new ArrayList<>(4);
+ }
+
+ public void add( Audio pAudio ) {
+ add(pAudio, 1f);
+ }
+
+ public void add( Audio pAudio, double pVolumeFactor ) {
+ audios.add(new AudioWrapper(pAudio, (float) pVolumeFactor));
+ pAudio.setVolume(pVolumeFactor * volume);
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return audios.stream().anyMatch(aw -> aw.audio.isPlaying());
+ }
+
+ @Override
+ public boolean isLooping() {
+ return audios.stream().anyMatch(aw -> aw.audio.isLooping());
+ }
+
+ @Override
+ public void setVolume( double pVolume ) {
+ volume = (float) pVolume;
+ audios.stream().forEach(aw -> aw.audio.setVolume(aw.volumeFactor * pVolume));
+ }
+
+ @Override
+ public void playOnce() {
+ audios.stream().forEach(aw -> aw.audio.playOnce());
+ }
+
+ @Override
+ public void playOnceAndWait() {
+ audios.stream().forEach(aw -> aw.audio.playOnce());
+ while( audios.stream().anyMatch(aw -> aw.audio.isPlaying()) ) {
+ try {
+ Thread.sleep(10);
+ } catch( InterruptedException e ) {
+ // Just keep waiting
+ }
+ }
+ }
+
+ @Override
+ public void loop() {
+ audios.stream().forEach(aw -> aw.audio.loop());
+ }
+
+ @Override
+ public void stop() {
+ audios.stream().forEach(aw -> aw.audio.stop());
+ }
+
+ @Override
+ public void dispose() {
+ if( isPlaying() ) {
+ stop();
+ }
+ audios.stream().forEach(aw -> aw.audio.dispose());
+ }
+
+ public void fade( final double to, final int time ) {
+ TaskRunner.run(new Runnable() {
+ @Override
+ public void run() {
+ final long start = System.currentTimeMillis();
+ double t = 0.0;
+ double from = volume;
+ if( !isPlaying() ) {
+ playOnce();
+ }
+ do {
+ setVolume(Constants.interpolate(from, to, t));
+ t = (double) (System.currentTimeMillis() - start) / (double) time;
+
+ try {
+ Thread.sleep(1000 / Constants.framesPerSecond);
+ } catch( InterruptedException e ) {
+ // Keep waiting
+ }
+ } while( t < 1.0 );
+ setVolume(to);
+ }
+ });
+ }
+
+}
diff --git a/src/schule/ngb/zm/media/Music.java b/src/schule/ngb/zm/media/Music.java
index 01ae8f2..1c53ba7 100644
--- a/src/schule/ngb/zm/media/Music.java
+++ b/src/schule/ngb/zm/media/Music.java
@@ -9,7 +9,7 @@ import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
-public class Music {
+public class Music implements Audio {
// size of the byte buffer used to read/write the audio stream
private static final int BUFFER_SIZE = 4096;
@@ -25,16 +25,18 @@ public class Music {
private SourceDataLine audioLine;
- private float volume = 1.0f;
+ private float volume = 0.8f;
public Music( String source ) {
this.audioSource = source;
}
+ @Override
public boolean isPlaying() {
return playing;
}
+ @Override
public boolean isLooping() {
if( !playing ) {
looping = false;
@@ -42,6 +44,7 @@ public class Music {
return looping;
}
+ @Override
public void setVolume( double volume ) {
this.volume = (float) volume;
if( audioLine != null ) {
@@ -58,6 +61,7 @@ public class Music {
gainControl.setValue(vol);
}
+ @Override
public void playOnce() {
if( openLine() ) {
TaskRunner.run(new Runnable() {
@@ -69,23 +73,27 @@ public class Music {
}
}
+ @Override
public void playOnceAndWait() {
if( openLine() ) {
stream();
}
}
+ @Override
public void loop() {
looping = true;
playOnce();
}
+ @Override
public void stop() {
playing = false;
looping = false;
dispose();
}
+ @Override
public void dispose() {
if( audioLine != null ) {
if( audioLine.isRunning() ) {
diff --git a/src/schule/ngb/zm/media/Sound.java b/src/schule/ngb/zm/media/Sound.java
index 004207f..ceda917 100644
--- a/src/schule/ngb/zm/media/Sound.java
+++ b/src/schule/ngb/zm/media/Sound.java
@@ -7,7 +7,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
-public class Sound {
+public class Sound implements Audio {
private boolean playing = false;
@@ -19,7 +19,7 @@ public class Sound {
private boolean disposeAfterPlay = false;
- private float volume = 1.0f;
+ private float volume = 0.8f;
public Sound( String source ) {
this.audioSource = source;