diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1a0ae91 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,44 @@ +# Description + +Please include a summary of the change and which issue is fixed. +List any dependencies that are required for this change. + + + + + + +**What kind of change does this PR introduce?** (check at least one) + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Code style update +- [ ] Refactor +- [ ] Build-related changes +- [ ] This change requires a documentation update +- [ ] Other, please describe: + +**Does this PR introduce a breaking change?** (check one) + +- [ ] Yes +- [ ] No + +# Has This Been Tested? + +- [ ] Yes +- [ ] No + +# Checklist: + +- [X] My code follows the style guidelines of this project +- [X] I have performed a self-review of my own code +- [X] I have commented my code, particularly in hard-to-understand areas +- [X] I have made corresponding changes to the documentation +- [X] My changes generate no new warnings +- [X] I have added tests that prove my fix is effective or that my feature works +- [X] New and existing unit tests pass locally with my changes +- [X] Any dependent changes have been merged and published in downstream modules diff --git a/README.md b/README.md index d852172..d89bc8c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,41 @@ +### [![AlexKent](https://user-images.githubusercontent.com/20374208/75432997-f5422100-5957-11ea-87a2-164eb98d83ef.png)](https://www.minepi.com/AlexKent) Support me joining PI Network app with invitation code [AlexKent](https://www.minepi.com/AlexKent) [![AlexKent](https://user-images.githubusercontent.com/20374208/75432997-f5422100-5957-11ea-87a2-164eb98d83ef.png)](https://www.minepi.com/AlexKent) + +--- + +

Java Stream Player ( Library )

+

+🎶 +

+

+ +Java Audio Controller Library with (skip,skipTo,start,stop,pause,play,restart) + This is the next version of JavaZoom BasicPlayer + +

+ +--- + [![Latest Version](https://img.shields.io/github/release/goxr3plus/java-stream-player.svg?style=flat-square)](https://github.com/goxr3plus/java-stream-player/releases) [![HitCount](http://hits.dwyl.io/goxr3plus/java-stream-player.svg)](http://hits.dwyl.io/goxr3plus/java-stream-player) Patreon donate button PayPal donate button - -# Java-stream-player -Java Audio Controller Library with (skip,skipTo,start,stop,pause,play,restart features) -This is the next version of [JavaZoom BasicPlayer](http://www.javazoom.net/jlgui/api.html) - ### What audio formats it supports? -It supports **WAV, AU, AIFF, MP3, OGG VORBIS, FLAC, MONKEY's AUDIO and SPEEX audio formats** , using some external libraries . Although more will be added in future releases. - - -### Step 1. Add the JitPack repository to your build file +- **Fully Supported ✔️** + - WAV + - MP3 +- **Partially not full tested 🚧** + - OGG VORBIS + - FLAC + - MONKEY's AUDIO + - SPEEX +- **Not Supported Yet ❌** + - AAC + - THEORA + - ... all the others + + +### Step 1. Add the JitPack repository to your build file https://jitpack.io/private#goxr3plus/java-stream-player ``` XML diff --git a/pom.xml b/pom.xml index fc849b0..b85c184 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.goxr3plus java-stream-player - 9.0.5 + 10.0.0 diff --git a/src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java b/src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java index f0ee979..3c2c278 100644 --- a/src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java +++ b/src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java @@ -35,13 +35,8 @@ import java.net.URL; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -120,11 +115,6 @@ public class StreamPlayer implements StreamPlayerInterface, Callable { private final ExecutorService streamPlayerExecutorService; private Future future; - /** - * This executor service is used in order the playerState events to be executed - * in an order - */ - private final ExecutorService eventsExecutorService; /** Holds a list of Linteners to be notified about Stream PlayerEvents */ private final ArrayList listeners; @@ -154,20 +144,17 @@ public StreamPlayer() { */ public StreamPlayer(Logger logger) { this(logger, - Executors.newSingleThreadExecutor(new ThreadFactoryWithNamePrefix("StreamPlayer")), - Executors.newSingleThreadExecutor(new ThreadFactoryWithNamePrefix("StreamPlayerEvent"))); + Executors.newSingleThreadExecutor(new ThreadFactoryWithNamePrefix("StreamPlayer"))); } /** * Constructor with settable logger and executor services. * @param logger The logger that will be used by the player * @param streamPlayerExecutorService Executor service for the stream player - * @param eventsExecutorService Executor service for events. */ - public StreamPlayer(Logger logger, ExecutorService streamPlayerExecutorService, ExecutorService eventsExecutorService) { + public StreamPlayer(Logger logger, ExecutorService streamPlayerExecutorService) { this.logger = logger; this.streamPlayerExecutorService = streamPlayerExecutorService; - this.eventsExecutorService = eventsExecutorService; listeners = new ArrayList<>(); outlet = new Outlet(logger); reset(); @@ -210,17 +197,10 @@ public void reset() { * @param encodedStreamPosition in the stream when the event occurs. * @param description the description * - * @return A String Describing if any problem occurred */ - private String generateEvent(final Status status, final int encodedStreamPosition, final Object description) { - try { - return eventsExecutorService - .submit(new StreamPlayerEventLauncher(this, status, encodedStreamPosition, description, listeners)) - .get(); - } catch (InterruptedException | ExecutionException ex) { - logger.log(Level.WARNING, "Problem in StreamPlayer generateEvent() method", ex); - } - return "Problem in StreamPlayer generateEvent() method"; + private void generateEvent(final Status status, final int encodedStreamPosition, final Object description) { + new StreamPlayerEventLauncher(this, status, encodedStreamPosition, description, listeners).call(); + } /** @@ -574,7 +554,7 @@ public void play() throws StreamPlayerException { // Proceed only if we have not problems logger.info("Submitting new StreamPlayer Thread"); - streamPlayerExecutorService.submit(this); + Future submit = streamPlayerExecutorService.submit(this); // Update the status status = Status.PLAYING; @@ -994,17 +974,16 @@ public List getMixers() { // audio mixers that are currently installed on the system. final Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo(); - if (mixerInfos != null) - Arrays.stream(mixerInfos).forEach(mInfo -> { - // line info - final Line.Info lineInfo = new Line.Info(SourceDataLine.class); - final Mixer mixer = AudioSystem.getMixer(mInfo); + Arrays.stream(mixerInfos).forEach(mInfo -> { + // line info + final Line.Info lineInfo = new Line.Info(SourceDataLine.class); + final Mixer mixer = AudioSystem.getMixer(mInfo); - // if line supported - if (mixer.isLineSupported(lineInfo)) - mixers.add(mInfo.getName()); + // if line supported + if (mixer.isLineSupported(lineInfo)) + mixers.add(mInfo.getName()); - }); + }); return mixers; } @@ -1023,7 +1002,7 @@ private Mixer getMixer(final String name) { // audio mixers that are currently installed on the system. final Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo(); - if (name != null && mixerInfos != null) + if (name != null) for (Mixer.Info mixerInfo : mixerInfos) if (mixerInfo.getName().equals(name)) { mixer = AudioSystem.getMixer(mixerInfo); @@ -1067,12 +1046,18 @@ public float getMinimumGain() { /** * Returns Pan precision. + *

+ * Obtains the resolution or granularity of the control, in the units that the control measures. + * The precision is the size of the increment between discrete valid values for this control, + * over the set of supported floating-point values. * - * @return The Precision Value + * @return The Precision Value for the pan control, if it exists, otherwise 0.0. */ @Override public float getPrecision() { - return !outlet.hasControl(FloatControl.Type.PAN, outlet.getPanControl()) ? 0.0F : outlet.getPanControl().getPrecision(); + return !outlet.hasControl(FloatControl.Type.PAN, outlet.getPanControl()) + ? 0 + : outlet.getPanControl().getPrecision(); } diff --git a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java new file mode 100644 index 0000000..4a6c452 --- /dev/null +++ b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java @@ -0,0 +1,73 @@ +package com.goxr3plus.streamplayer.stream; + +import com.goxr3plus.streamplayer.enums.Status; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.SourceDataLine; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +/** + * Tests of all or most of the public methods of StreamPlayer. + * These unit tests are written primarily as documentation of the behavior and as example use case, + * not as a part of test driven development. + */ +public class StreamPlayerFutureImprovementTest { + StreamPlayer player; + private File audioFile; + + @BeforeEach + void setup() { + final Logger logger = mock(Logger.class); + player = new StreamPlayer(logger); + audioFile = new File("Logic - Ballin [Bass Boosted].mp3"); + } + + /** + * This test fails if it's permitted to add a null to the StreamPlayer listener list. + */ + @Test + void addStreamPlayerListener_dontAcceptNull() { + // Currently, we can add a null to the list of stream player listeners. + // Should that really be allowed? + assertThrows(Exception.class, () -> player.addStreamPlayerListener(null)); + + fail("Test not done"); + } + + + @Test + @DisplayName("When play() is called without first calling open(), an exception is thrown") + void playingUnopenedSourceThrowsException() { + + assertThrows(Exception.class, () -> player.play()); + } + + @Test + void seekBytes() throws StreamPlayerException { + player.open(audioFile); + player.play(); + int positionByte1 = player.getPositionByte(); + + player.seekBytes(100); + int positionByte2 = player.getPositionByte(); + + assertTrue( positionByte2 > positionByte1); + + // TODO: It seems that getPositionByte doesn't work. + // It isn't called from within this project, except for in this test. + // It is however called by XR3Player. If XR3Player needs this method, it must be tested + // within this project. The method relies on a map, which doesn't seem to be updated by play() + } + +} diff --git a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java index 4cfbc5b..1c247e4 100644 --- a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java +++ b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java @@ -3,13 +3,18 @@ import com.goxr3plus.streamplayer.enums.Status; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; -import javax.sound.sampled.SourceDataLine; +import javax.sound.sampled.*; import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.*; /** * Tests of all or most of the public methods of StreamPlayer. @@ -85,7 +90,7 @@ void status() throws StreamPlayerException { @Test void gain() throws StreamPlayerException, InterruptedException { // Setup - final double gain1 = 0.99; + final double gain1_dB = 0.5; final double gain2 = 0.2; final double delta = 0.05; final boolean listen = false; @@ -95,8 +100,8 @@ void gain() throws StreamPlayerException, InterruptedException { player.open(audioFile); player.seekTo(30); player.play(); - player.setGain(gain1); - final float actualGain1First = player.getGainValue(); + player.setGain(gain1_dB); + final float actualGain0 = player.getGainValue(); if (listen) Thread.sleep(2000); final float actualGain1 = player.getGainValue(); @@ -104,16 +109,19 @@ void gain() throws StreamPlayerException, InterruptedException { if (listen) Thread.sleep(2000); final float actualGain2 = player.getGainValue(); - player.setGain(gain1); + player.setGain(gain1_dB); if (listen) Thread.sleep(2000); player.stop(); // Verify assertEquals(0, initialGain); - assertEquals(actualGain1First, actualGain1); - assertEquals(gain1, actualGain1, delta); // TODO: Investigate probable bug. - // fail("Test not done"); + assertEquals(actualGain0, actualGain1); + assertEquals(20.0 * Math.log10(gain1_dB), actualGain1, delta); + + // TODO: Consider changing the API. setGain() and getGainValue() have different scales. + // setGain(linear scale), + // whereas getGainValue() returns a logarithmic dB scale value. This is inconsistent. } /** @@ -127,7 +135,7 @@ void gain() throws StreamPlayerException, InterruptedException { @Test void logScaleGain() throws StreamPlayerException, InterruptedException { // Setup - final boolean listen = true; + final boolean listen = false; // Set to true to listen to the test. // Exercise @@ -183,17 +191,26 @@ void maximumGain() throws StreamPlayerException { } @Test - void totalBytes() { - player.getTotalBytes(); + void totalBytes() throws StreamPlayerException, InterruptedException { + int expectedLengthOfExampleAudioFile = 5877062; - fail("Test not done"); + + assertEquals(-1, player.getTotalBytes()); + + player.open(audioFile); + assertEquals(expectedLengthOfExampleAudioFile, player.getTotalBytes()); + + player.play(); + assertEquals(expectedLengthOfExampleAudioFile, player.getTotalBytes()); } @Test void stopped() { - player.isStopped(); - fail("Test not done"); + assertFalse(player.isStopped()); + + player.stop(); + assertTrue(player.isStopped()); } @Test @@ -209,92 +226,195 @@ void sourceDataLine() throws StreamPlayerException { } @Test - void playing() { - final boolean playing = player.isPlaying(); + void playing() throws StreamPlayerException { - assertFalse(playing); + assertFalse(player.isPlaying()); - fail("Test not done"); - } + player.open(audioFile); + assertFalse(player.isPlaying()); - @Test - void pausedOrPlaying() { - player.isPausedOrPlaying(); + player.play(); + assertTrue(player.isPlaying()); - fail("Test not done"); + player.pause(); + assertFalse(player.isPlaying()); } @Test - void paused() { - player.isPaused(); + void pausedOrPlaying() throws StreamPlayerException { - fail("Test not done"); + assertFalse(player.isPausedOrPlaying()); + + player.open(audioFile); + assertFalse(player.isPausedOrPlaying()); + + player.play(); + assertTrue(player.isPausedOrPlaying()); + + player.pause(); + assertTrue(player.isPausedOrPlaying()); + + player.stop(); + assertFalse(player.isPausedOrPlaying()); } @Test - void addStreamPlayerListener_dontAcceptNull() { - assertThrows(Exception.class, () -> player.addStreamPlayerListener(null)); + void paused() throws StreamPlayerException { + assertFalse(player.isPaused()); - fail("Test not done"); + player.open(audioFile); + assertFalse(player.isPaused()); + + player.play(); + assertFalse(player.isPaused()); + + player.pause(); + assertTrue(player.isPaused()); } @Test - void addStreamPlayerListener() { + void addStreamPlayerListener() throws StreamPlayerException, InterruptedException { + // Setup final StreamPlayerListener listener = mock(StreamPlayerListener.class); + + ArgumentCaptor dataSourceCaptor = ArgumentCaptor.forClass(Object.class); + ArgumentCaptor propertiesCaptor1 = ArgumentCaptor.forClass(Map.class); + + // Execute player.addStreamPlayerListener(listener); + player.open(audioFile); + player.play(); + Thread.sleep(30); + + // Verify + verify(listener).opened(dataSourceCaptor.capture(), propertiesCaptor1.capture()); + Object value = dataSourceCaptor.getValue(); + assertTrue(value instanceof File); + + Map value11 = propertiesCaptor1.getValue(); + + assertTrue(value11.containsKey("basicplayer.sourcedataline")); + + verify(listener, times(4)).statusUpdated(any()); + + verify(listener, times(1)).opened(any(), any()); + + verify(listener, atLeast(4)).progress(anyInt(), anyLong(), any(), any()); + verify(listener, atMost(30)).progress(anyInt(), anyLong(), any(), any()); + + // TODO: Make separate tests for the different calls made to the listener + // TODO: Do we need to test the values passed to these methods? - fail("Test not done"); // TODO: CHeck that the listener is actually added } @Test - void mute() { - player.getMute(); + void mute() throws StreamPlayerException { + // TODO: How can mute be tested, without too much assumptions about the actual implementation? + // A manual test would involve listening. + + + assertFalse(player.getMute()); + player.open(audioFile); + player.play(); + player.setMute(true); + assertTrue(player.getMute()); player.setMute(false); + assertFalse(player.getMute()); - fail("Test not done"); } @Test - void speedFactor() { - player.getSpeedFactor(); - player.setSpeedFactor(1000); + void speedFactor() throws StreamPlayerException, InterruptedException { + assertEquals(player.getSpeedFactor(), 1); + + double fast = 1; + player.setSpeedFactor(fast); + assertEquals(fast, player.getSpeedFactor()); + + double slow = 0.5; + player.open(audioFile); + player.play(); + player.setSpeedFactor(slow); + Thread.sleep(50); + assertEquals(slow, player.getSpeedFactor()); + + // TODO: Find a way to verify that the speed factor actually works. That it can be read back is no proof. + // I might be possible to play a short sequence of known length, and measure the time it takes. + // But things that take time are generally not advisable in unit tests. + - fail("Test not done"); } @Test void equalizer() { player.setEqualizer(null, 0); - - fail("Test not done"); + // TODO: Find out what the intention of setEqualizer() is, and make a test for that assumption. } @Test - void play() throws StreamPlayerException { + void play() throws StreamPlayerException, InterruptedException { + // Setup + player.open(audioFile); + + // Pre-validate + assertFalse(player.isPlaying()); + + // Execute player.play(); - fail("Test not done"); + // Verify + assertTrue(player.isPlaying()); + + // TODO: Find way to verify that the player is actually playing, that doesn't need listening. + // The method might look at the playing position, but it must be fairly quick. } @Test - void resume() { - player.resume(); + void resume() throws StreamPlayerException { + assertFalse(player.isPlaying()); - fail("Test not done"); + player.open(audioFile); + assertFalse(player.isPlaying()); + + player.play(); + assertTrue(player.isPlaying()); + + player.pause(); + assertFalse(player.isPlaying()); + + + player.resume(); + assertTrue(player.isPlaying()); } @Test - void pause() { + void pause() throws StreamPlayerException { + + // Setup + player.open(audioFile); + player.play(); + // Pre-validate + assertFalse(player.isPaused()); + + // Execute player.pause(); - fail("Test not done"); + // Verify + assertTrue(player.isPaused()); + } @Test void stop() { + + assertFalse(player.isStopped()); + player.stop(); - fail("Test not done"); + assertTrue(player.isStopped()); + + // TODO: Find a way to verify that playback is stopped by running the stop method. + // The isStopped() method is not enough. } @Test @@ -310,42 +430,49 @@ void pan() throws StreamPlayerException { player.setPan(pan); assertEquals(pan, player.getPan(), delta); + // If we set the pan outside the permitted range, it will not change + // The permitted range is undefined. double outsideRange = 1.1; player.setPan(outsideRange); assertEquals(pan, player.getPan(), delta); + + float precision = player.getPrecision(); + assertNotEquals(0, precision); + double expected = 128.0; // Possibly platform dependent. Tested on a Mac with Intellij. + assertEquals(expected, 1.0/precision, 2.0); } @Test void unknown() { player.isUnknown(); - - fail("Test not done"); + // This is a useless test of a useless method. + // TODO: Remove player.isUnknown(). It's not used, and it's useless. + // There is already getStatus(). } @Test void open() throws StreamPlayerException { - File file = null; + File file = spy(audioFile); player.open(file); + verify(file, atLeast(1)).getPath(); - fail("Test not done"); + // It's unclear what the contract of open() is; what we need it to do. + // It's a pre-requisite for play(), but play() doesn't throw an + // exception if open() is missing. } @Test void mixers() { - player.getMixers(); - - fail("Test not done"); + List mixers = player.getMixers(); + // TODO: Make this method player.getMixers() private, remove it from the interface. + // There is nothing that can be done with the information outside the private scope. } - @Test - void seekBytes() throws StreamPlayerException { - player.seekBytes(0); - fail("Test not done"); - } // The methods tested below aren't used elsewhere in this project, nor in XR3Player + // TODO: Consider each of the tested methods below, to see if they can be removed from StreamPlayer. @Test void lineBufferSize() { @@ -355,10 +482,18 @@ void lineBufferSize() { } @Test - void lineCurrentBufferSize() { - player.getLineCurrentBufferSize(); + void lineCurrentBufferSize() throws StreamPlayerException { + // TODO: Document the purpose of getLineCurrentBufferSize(). What is it good for? + // Can it be removed? The method doesn't really return the current line buffer size, + // but a cached value, which might be the same thing. Hard to say. - fail("Test not done"); + assertEquals(-1, player.getLineCurrentBufferSize(), "Initially, the buffer size is undefined, coded as -1."); + + player.open(audioFile); + assertEquals(-1, player.getLineCurrentBufferSize(), "After the player is opened, the buffer size is undefined"); + + player.play(); + assertEquals(2 * 44100, player.getLineCurrentBufferSize(), "After the play starts, the buffer size 1 second at CD sampling rate"); } @Test @@ -376,17 +511,22 @@ void positionByte() { } @Test - void precision() { - player.getPrecision(); + void precision() throws StreamPlayerException { + assertEquals(0f, player.getPrecision()); - fail("Test not done"); + player.open(audioFile); + player.play(); + + assertNotEquals(0f, player.getPrecision()); + // On one computer the precision = 1/128. There are no guarantees. } @Test - void opened() { - player.isOpened(); + void opened() throws StreamPlayerException { + assertFalse(player.isOpened()); - fail("Test not done"); + player.open(audioFile); + assertTrue(player.isOpened()); } @Test @@ -404,10 +544,31 @@ void removeStreamPlayerListener() { } @Test - void seekTo() throws StreamPlayerException { - player.seekTo(1000); + void seekTo() throws StreamPlayerException, IOException, UnsupportedAudioFileException { - fail("Test not done"); + // Some tests before we do the real tests + AudioFileFormat audioFileFormat = AudioSystem.getAudioFileFormat(audioFile); + + + // Setup + player.open(audioFile); + player.play(); + player.pause(); + int encodedStreamPosition1 = player.getEncodedStreamPosition(); + + // Execute + player.seekTo(10); + + // Verify + int encodedStreamPosition2 = player.getEncodedStreamPosition(); + assertTrue(encodedStreamPosition2 > encodedStreamPosition1); + + // Execute: go backwards + player.seekTo(5); + + // Verify: position goes backwards + int encodedStreamPosition3 = player.getEncodedStreamPosition(); + assertTrue(encodedStreamPosition3 < encodedStreamPosition2); } @Test