7 | 🎶 8 |
9 |10 | 11 | Java Audio Controller Library with (skip,skipTo,start,stop,pause,play,restart) 12 | This is the next version of JavaZoom BasicPlayer 13 | 14 |
15 | 16 | --- 17 | 18 | [](https://github.com/goxr3plus/java-stream-player/releases) 19 | [](http://hits.dwyl.io/goxr3plus/java-stream-player) 20 |37 | * The {@code toString} method for class {@code Object} 38 | * returns a string consisting of the name of the class of which the 39 | * object is an instance, the at-sign character `{@code @}', and 40 | * the unsigned hexadecimal representation of the hash code of the 41 | * object. In other words, this method returns a string equal to the 42 | * value of: 43 | *
44 | *47 | * 48 | * @return a string representation of the object. 49 | */ 50 | @Override 51 | String toString(); 52 | 53 | /** 54 | * @return the format of the source data 55 | * @throws UnsupportedAudioFileException if the file type is unsupported 56 | * @throws IOException if there is a runtime problem with IO. 57 | */ 58 | AudioFileFormat getAudioFileFormat() throws UnsupportedAudioFileException, IOException; 59 | 60 | /** 61 | * @return a stream representing the input data, regardless of source. 62 | * @throws UnsupportedAudioFileException if the file type is unsupported 63 | * @throws IOException if there is a runtime problem with IO. 64 | */ 65 | AudioInputStream getAudioInputStream() throws UnsupportedAudioFileException, IOException; 66 | 67 | /** 68 | * @return The duration of the source data in seconds, or -1 if duration is unavailable. 69 | */ 70 | int getDurationInSeconds(); 71 | 72 | /** 73 | * @return The duration of the source data in milliseconds, or -1 if duration is unavailable. 74 | */ 75 | long getDurationInMilliseconds(); 76 | 77 | /** 78 | * @return The duration of the source data in a {@code java.time.Duration} instance, or null if unavailable 79 | */ 80 | Duration getDuration(); 81 | 82 | /** 83 | * @return true if the DataSource is a FileDataSource, 84 | * which happens if the source used to create it is a File 85 | */ 86 | boolean isFile(); 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/FileDataSource.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import com.goxr3plus.streamplayer.enums.AudioType; 4 | import com.goxr3plus.streamplayer.tools.TimeTool; 5 | 6 | import javax.sound.sampled.AudioFileFormat; 7 | import javax.sound.sampled.AudioInputStream; 8 | import javax.sound.sampled.AudioSystem; 9 | import javax.sound.sampled.UnsupportedAudioFileException; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.time.Duration; 13 | 14 | public class FileDataSource implements DataSource { 15 | 16 | private final File source; 17 | 18 | FileDataSource(File source) { 19 | this.source = source; 20 | } 21 | 22 | @Override 23 | public AudioFileFormat getAudioFileFormat() throws UnsupportedAudioFileException, IOException { 24 | return AudioSystem.getAudioFileFormat(this.source); 25 | } 26 | 27 | @Override 28 | public AudioInputStream getAudioInputStream() throws UnsupportedAudioFileException, IOException { 29 | return AudioSystem.getAudioInputStream(source); 30 | } 31 | 32 | @Override 33 | public int getDurationInSeconds() { 34 | return TimeTool.durationInSeconds(source.getAbsolutePath(), AudioType.FILE); 35 | } 36 | 37 | @Override 38 | public long getDurationInMilliseconds() { 39 | return TimeTool.durationInMilliseconds(source.getAbsolutePath(), AudioType.FILE); 40 | } 41 | 42 | @Override 43 | public Duration getDuration() { 44 | return Duration.ofMillis(getDurationInMilliseconds()); 45 | } 46 | 47 | @Override 48 | public Object getSource() { 49 | return source; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "FileDataSource with " + source.toString(); 55 | } 56 | 57 | @Override 58 | public boolean isFile() { 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/Outlet.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import javax.sound.sampled.*; 4 | import java.util.logging.Logger; 5 | 6 | /** 7 | * Owner of the SourceDataLine which is the output line of the player. 8 | * Also owns controls for the SourceDataLine. 9 | * Future goal is to move all handling of the SourceDataLine to here, 10 | * so that the StreamPlayer doesn't have to call {@link #getSourceDataLine()}. 11 | * Another goal is to remove some of the setter and getter methods of this class, 12 | * by moving all code that needs them to this class. 13 | */ 14 | public class Outlet { 15 | 16 | private final Logger logger; 17 | private FloatControl balanceControl; 18 | private FloatControl gainControl; 19 | private BooleanControl muteControl; 20 | private FloatControl panControl; 21 | private SourceDataLine sourceDataLine; 22 | 23 | /** 24 | * @param logger used to log messages 25 | */ 26 | public Outlet(Logger logger) { 27 | this.logger = logger; 28 | } 29 | 30 | 31 | /** 32 | * @return the balance control of the {@link #sourceDataLine} 33 | */ 34 | public FloatControl getBalanceControl() { 35 | return balanceControl; 36 | } 37 | 38 | /** 39 | * @return the gain control of the {@link #sourceDataLine} 40 | */ 41 | public FloatControl getGainControl() { 42 | return gainControl; 43 | } 44 | 45 | /** 46 | * @return the mute control of the {@link #sourceDataLine} 47 | */ 48 | public BooleanControl getMuteControl() { 49 | return muteControl; 50 | } 51 | 52 | /** 53 | * @return the pan control of the {@link #sourceDataLine} 54 | */ 55 | public FloatControl getPanControl() { 56 | return panControl; 57 | } 58 | 59 | /** 60 | * @return the {@link #sourceDataLine}, which is the output audio signal of the player 61 | */ 62 | public SourceDataLine getSourceDataLine() { 63 | return sourceDataLine; 64 | } 65 | 66 | 67 | /** 68 | * @param balanceControl to be set on the {@link #sourceDataLine} 69 | */ 70 | public void setBalanceControl(FloatControl balanceControl) { 71 | this.balanceControl = balanceControl; 72 | } 73 | 74 | /** 75 | * @param gainControl to be set on the {@link #sourceDataLine} 76 | */ 77 | public void setGainControl(FloatControl gainControl) { 78 | this.gainControl = gainControl; 79 | } 80 | 81 | /** 82 | * @param muteControl to be set on the {@link #sourceDataLine} 83 | */ 84 | public void setMuteControl(BooleanControl muteControl) { 85 | this.muteControl = muteControl; 86 | } 87 | 88 | /** 89 | * @param panControl to be set on the {@link #sourceDataLine} 90 | */ 91 | public void setPanControl(FloatControl panControl) { 92 | this.panControl = panControl; 93 | } 94 | 95 | /** 96 | * @param sourceDataLine representing the audio output of the player. 97 | * Usually taken from {@link AudioSystem#getLine(Line.Info)}. 98 | */ 99 | public void setSourceDataLine(SourceDataLine sourceDataLine) { 100 | this.sourceDataLine = sourceDataLine; 101 | } 102 | 103 | 104 | /** 105 | * Check if the Control is Supported by m_line. 106 | * 107 | * @param control the control 108 | * @param component the component 109 | * 110 | * @return true, if successful 111 | */ 112 | public boolean hasControl(final Control.Type control, final Control component) { 113 | return component != null && (sourceDataLine != null) && (sourceDataLine.isControlSupported(control)); 114 | } 115 | 116 | /** 117 | * Returns Gain value. 118 | * 119 | * @return The Gain Value 120 | */ 121 | public float getGainValue() { 122 | 123 | if (hasControl(FloatControl.Type.MASTER_GAIN, getGainControl())) { 124 | return getGainControl().getValue(); 125 | } else { 126 | return 0.0F; 127 | } 128 | } 129 | 130 | /** 131 | * Stop the {@link #sourceDataLine} in a nice way. 132 | * Also nullify it. (Is that necessary?) 133 | */ 134 | void drainStopAndFreeDataLine() { 135 | // Free audio resources. 136 | if (sourceDataLine != null) { 137 | sourceDataLine.drain(); 138 | sourceDataLine.stop(); 139 | sourceDataLine.close(); 140 | this.sourceDataLine = null; // TODO: Is this necessary? Will it not be garbage collected? 141 | } 142 | } 143 | 144 | /** 145 | * Flush and close the {@link #sourceDataLine} in a nice way. 146 | * Also nullify it. (Is that necessary?) 147 | */ 148 | void flushAndFreeDataLine() { 149 | if (sourceDataLine != null) { 150 | sourceDataLine.flush(); 151 | sourceDataLine.close(); 152 | this.sourceDataLine = null; // TODO: Is this necessary? Will it not be garbage collected? 153 | } 154 | } 155 | 156 | /** 157 | * Flush and stop the {@link #sourceDataLine}, if it's running. 158 | */ 159 | void flushAndStop() { 160 | // Flush and stop the source data line 161 | if (sourceDataLine != null && sourceDataLine.isRunning()) { // TODO: Risk for NullPointerException? 162 | sourceDataLine.flush(); 163 | sourceDataLine.stop(); 164 | } 165 | } 166 | 167 | /** 168 | * @return true if the {@link #sourceDataLine} is startable. 169 | */ 170 | boolean isStartable() { 171 | return sourceDataLine != null && !sourceDataLine.isRunning(); 172 | } 173 | 174 | 175 | /** 176 | * Start the {@link #sourceDataLine} 177 | */ 178 | void start() { 179 | sourceDataLine.start(); 180 | } 181 | 182 | /** 183 | * Open the {@link #sourceDataLine}. 184 | * Also create controls for it. 185 | * @param format The wanted audio format. 186 | * @param bufferSize the desired buffer size for the {@link #sourceDataLine} 187 | * @throws LineUnavailableException 188 | */ 189 | void open(AudioFormat format, int bufferSize) throws LineUnavailableException { 190 | logger.info("Entered OpenLine()!:\n"); 191 | 192 | if (sourceDataLine != null) { 193 | sourceDataLine.open(format, bufferSize); 194 | 195 | // opened? 196 | if (sourceDataLine.isOpen()) { 197 | 198 | // Master_Gain Control? 199 | if (sourceDataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)) 200 | setGainControl((FloatControl) sourceDataLine.getControl(FloatControl.Type.MASTER_GAIN)); 201 | else setGainControl(null); 202 | 203 | // PanControl? 204 | if (sourceDataLine.isControlSupported(FloatControl.Type.PAN)) 205 | setPanControl((FloatControl) sourceDataLine.getControl(FloatControl.Type.PAN)); 206 | else setPanControl(null); 207 | 208 | // Mute? 209 | BooleanControl muteControl1 = sourceDataLine.isControlSupported(BooleanControl.Type.MUTE) 210 | ? (BooleanControl) sourceDataLine.getControl(BooleanControl.Type.MUTE) 211 | : null; 212 | setMuteControl(muteControl1); 213 | 214 | // Speakers Balance? 215 | FloatControl balanceControl = sourceDataLine.isControlSupported(FloatControl.Type.BALANCE) 216 | ? (FloatControl) sourceDataLine.getControl(FloatControl.Type.BALANCE) 217 | : null; 218 | setBalanceControl(balanceControl); 219 | } 220 | } 221 | logger.info("Exited OpenLine()!:\n"); 222 | } 223 | 224 | } 225 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/StreamDataSource.java: -------------------------------------------------------------------------------- 1 | package com.goxr3plus.streamplayer.stream; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.time.Duration; 6 | 7 | import javax.sound.sampled.AudioFileFormat; 8 | import javax.sound.sampled.AudioInputStream; 9 | import javax.sound.sampled.AudioSystem; 10 | import javax.sound.sampled.UnsupportedAudioFileException; 11 | 12 | public class StreamDataSource implements DataSource { 13 | 14 | private final InputStream source; 15 | 16 | StreamDataSource(InputStream source) { 17 | this.source = source; 18 | } 19 | 20 | @Override 21 | public AudioFileFormat getAudioFileFormat() throws UnsupportedAudioFileException, IOException { 22 | return AudioSystem.getAudioFileFormat(source); 23 | } 24 | 25 | @Override 26 | public AudioInputStream getAudioInputStream() throws UnsupportedAudioFileException, IOException { 27 | return AudioSystem.getAudioInputStream(source); 28 | } 29 | 30 | @Override 31 | public int getDurationInSeconds() { 32 | return -1; 33 | } 34 | 35 | @Override 36 | public long getDurationInMilliseconds() { 37 | return -1; 38 | } 39 | 40 | @Override 41 | public Duration getDuration() { 42 | return null; 43 | } 44 | 45 | @Override 46 | public Object getSource() { 47 | return source; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "StreamDataSource with " + source.toString(); 53 | } 54 | 55 | @Override 56 | public boolean isFile() { 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * 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 3 | * 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 4 | * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 5 | * Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see 6 | *45 | * getClass().getName() + '@' + Integer.toHexString(hashCode()) 46 | *
470 | * From the AudioInputStream, i.e. from the sound file, we fetch information
471 | * about the format of the audio data. These information include the sampling
472 | * frequency, the number of channels and the size of the samples. There
473 | * information are needed to ask JavaSound for a suitable output line for this
474 | * audio file. Furthermore, we have to give JavaSound a hint about how big the
475 | * internal buffer for the line should be. Here, we say
476 | * AudioSystem.NOT_SPECIFIED, signaling that we don't care about the exact size.
477 | * JavaSound will use some default value for the buffer size.
478 | *
479 | * @throws LineUnavailableException the line unavailable exception
480 | * @throws StreamPlayerException
481 | */
482 | private void createLine() throws LineUnavailableException, StreamPlayerException {
483 |
484 | logger.info("Entered CreateLine()!:\n");
485 |
486 | if (outlet.getSourceDataLine() != null)
487 | logger.warning("Warning Source DataLine is not null!\n");
488 | else {
489 | final AudioFormat sourceFormat = audioInputStream.getFormat();
490 |
491 | logger.info(() -> "Create Line : Source format : " + sourceFormat + "\n");
492 |
493 | // Calculate the Sample Size in bits
494 | int nSampleSizeInBits = sourceFormat.getSampleSizeInBits();
495 | if (sourceFormat.getEncoding() == AudioFormat.Encoding.ULAW || sourceFormat.getEncoding() == AudioFormat.Encoding.ALAW
496 | || nSampleSizeInBits != 8)
497 | nSampleSizeInBits = 16;
498 |
499 | final AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
500 | (float) (sourceFormat.getSampleRate() * speedFactor), nSampleSizeInBits, sourceFormat.getChannels(),
501 | nSampleSizeInBits / 8 * sourceFormat.getChannels(), sourceFormat.getSampleRate(), false);
502 |
503 | // int frameSize = sourceFormat.getChannels() * (nSampleSizeInBits / 8)
504 |
505 | logger.info(() -> "Sample Rate =" + targetFormat.getSampleRate() + ",Frame Rate="
506 | + targetFormat.getFrameRate() + ",Bit Rate=" + targetFormat.getSampleSizeInBits()
507 | + "Target format: " + targetFormat + "\n");
508 |
509 | // Keep a reference on encoded stream to progress notification.
510 | encodedAudioInputStream = audioInputStream;
511 | try {
512 | // Get total length in bytes of the encoded stream.
513 | encodedAudioLength = encodedAudioInputStream.available();
514 | } catch (final IOException e) {
515 | logger.warning("Cannot get m_encodedaudioInputStream.available()\n" + e);
516 | }
517 |
518 | // Create decoded Stream
519 | audioInputStream = AudioSystem.getAudioInputStream(targetFormat, audioInputStream);
520 | final DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, audioInputStream.getFormat(),
521 | AudioSystem.NOT_SPECIFIED);
522 | if (!AudioSystem.isLineSupported(lineInfo))
523 | throw new StreamPlayerException(PlayerException.LINE_NOT_SUPPORTED);
524 |
525 | // ----------About the mixer
526 | if (mixerName == null)
527 | // Primary Sound Driver
528 | mixerName = getMixers().get(0);
529 |
530 | // Continue
531 | mixer = getMixer(mixerName);
532 | if (mixer == null) {
533 | outlet.setSourceDataLine((SourceDataLine) AudioSystem.getLine(lineInfo));
534 | mixerName = null;
535 | } else {
536 | logger.info("Mixer: " + mixer.getMixerInfo());
537 | outlet.setSourceDataLine((SourceDataLine) mixer.getLine(lineInfo));
538 | }
539 |
540 | // --------------------------------------------------------------------------------
541 | logger.info(() -> "Line : " + outlet.getSourceDataLine());
542 | logger.info(() -> "Line Info : " + outlet.getSourceDataLine().getLineInfo());
543 | logger.info(() -> "Line AudioFormat: " + outlet.getSourceDataLine().getFormat() + "\n");
544 | logger.info("Exited CREATELINE()!:\n");
545 | }
546 | }
547 |
548 | /**
549 | * Open the line.
550 | *
551 | * @throws LineUnavailableException the line unavailable exception
552 | * @param audioFormat
553 | * @param currentLineBufferSize
554 | */
555 | private void openLine(AudioFormat audioFormat, int currentLineBufferSize) throws LineUnavailableException {
556 | outlet.open(audioFormat, currentLineBufferSize);
557 | }
558 |
559 | /**
560 | * Starts the play back.
561 | *
562 | * @throws StreamPlayerException the stream player exception
563 | */
564 | @Override
565 | public void play() throws StreamPlayerException {
566 | if (status == Status.STOPPED)
567 | initAudioInputStream();
568 | if (status != Status.OPENED)
569 | return;
570 |
571 | // Shutdown previous Thread Running
572 | awaitTermination();
573 |
574 | // Open SourceDataLine.
575 | try {
576 | initLine();
577 | } catch (final LineUnavailableException ex) {
578 | throw new StreamPlayerException(PlayerException.CAN_NOT_INIT_LINE, ex);
579 | }
580 |
581 | // Open the sourceDataLine
582 | if (outlet.isStartable()) {
583 | outlet.start();
584 |
585 | // Proceed only if we have not problems
586 | logger.info("Submitting new StreamPlayer Thread");
587 | streamPlayerExecutorService.submit(this);
588 |
589 | // Update the status
590 | status = Status.PLAYING;
591 | generateEvent(Status.PLAYING, getEncodedStreamPosition(), null);
592 | }
593 | }
594 |
595 | /**
596 | * Pauses the play back.
597 | *
598 | * Player Status = PAUSED. * @return False if failed(so simple...)
599 | *
600 | * @return true, if successful
601 | */
602 | @Override
603 | public boolean pause() {
604 | if (outlet.getSourceDataLine() == null || status != Status.PLAYING)
605 | return false;
606 | status = Status.PAUSED;
607 | logger.info("pausePlayback() completed");
608 | generateEvent(Status.PAUSED, getEncodedStreamPosition(), null);
609 | return true;
610 | }
611 |
612 | /**
613 | * Stops the play back.
614 | *
615 | * Player Status = STOPPED.
616 | * Thread should free Audio resources.
617 | */
618 | @Override
619 | public void stop() {
620 | if (status == Status.STOPPED)
621 | return;
622 | if (isPlaying())
623 | pause();
624 | status = Status.STOPPED;
625 | // generateEvent(Status.STOPPED, getEncodedStreamPosition(), null);
626 | logger.info("StreamPlayer stopPlayback() completed");
627 | }
628 |
629 | /**
630 | * Resumes the play back.
631 | *
632 | * Player Status = PLAYING* 633 | * 634 | * @return False if failed(so simple...) 635 | */ 636 | @Override 637 | public boolean resume() { 638 | if (outlet.getSourceDataLine() == null || status != Status.PAUSED) 639 | return false; 640 | outlet.start(); 641 | status = Status.PLAYING; 642 | generateEvent(Status.RESUMED, getEncodedStreamPosition(), null); 643 | logger.info("resumePlayback() completed"); 644 | return true; 645 | 646 | } 647 | 648 | /** 649 | * Await for the termination of StreamPlayerExecutorService Thread 650 | */ 651 | private void awaitTermination() { 652 | if (future != null && !future.isDone()) { 653 | try { 654 | // future.get() [Don't use this cause it may hang forever and ever...] 655 | 656 | // Wait ~1 second and then cancel the future 657 | final Thread delay = new Thread(() -> { 658 | try { 659 | for (int i = 0; i < 50; i++) { 660 | if (!future.isDone()) 661 | Thread.sleep(20); 662 | else 663 | break; 664 | logger.log(Level.INFO, "StreamPlayer Future is not yet done..."); 665 | } 666 | 667 | } catch (final InterruptedException ex) { 668 | Thread.currentThread().interrupt(); 669 | logger.log(Level.INFO, ex.getMessage(), ex); 670 | } 671 | }); 672 | 673 | // Start the delay Thread 674 | delay.start(); 675 | // Join until delay Thread is finished 676 | delay.join(); 677 | 678 | } catch (final InterruptedException ex) { 679 | Thread.currentThread().interrupt(); 680 | logger.log(Level.WARNING, ex.getMessage(), ex); 681 | } finally { 682 | // Harmless if task already completed 683 | future.cancel(true); // interrupt if running 684 | } 685 | } 686 | } 687 | 688 | /** 689 | * Skip bytes in the File input stream. It will skip N frames matching to bytes, 690 | * so it will never skip given bytes len 691 | * 692 | * @param bytes the bytes 693 | * 694 | * @return value bigger than 0 for File and value = 0 for URL and InputStream 695 | * 696 | * @throws StreamPlayerException the stream player exception 697 | */ 698 | @Override 699 | public long seekBytes(final long bytes) throws StreamPlayerException { 700 | long totalSkipped = 0; 701 | 702 | // If it is File 703 | if (source.isFile()) { 704 | 705 | // Check if the requested bytes are more than totalBytes of Audio 706 | final long bytesLength = getTotalBytes(); 707 | logger.log(Level.INFO, "Bytes: " + bytes + " BytesLength: " + bytesLength); 708 | if ((bytesLength <= 0) || (bytes >= bytesLength)) { 709 | generateEvent(Status.EOM, getEncodedStreamPosition(), null); 710 | return totalSkipped; 711 | } 712 | 713 | logger.info(() -> "Bytes to skip : " + bytes); 714 | final Status previousStatus = status; 715 | status = Status.SEEKING; 716 | 717 | try { 718 | synchronized (audioLock) { 719 | generateEvent(Status.SEEKING, AudioSystem.NOT_SPECIFIED, null); 720 | initAudioInputStream(); 721 | if (audioInputStream != null) { 722 | 723 | long skipped; 724 | // Loop until bytes are really skipped. 725 | while (totalSkipped < bytes) { // totalSkipped < (bytes-SKIP_INACCURACY_SIZE))) 726 | skipped = audioInputStream.skip(bytes - totalSkipped); 727 | if (skipped == 0) 728 | break; 729 | totalSkipped += skipped; 730 | logger.info("Skipped : " + totalSkipped + "/" + bytes); 731 | if (totalSkipped == -1) 732 | throw new StreamPlayerException( 733 | PlayerException.SKIP_NOT_SUPPORTED); 734 | 735 | logger.info("Skeeping:" + totalSkipped); 736 | } 737 | } 738 | } 739 | generateEvent(Status.SEEKED, getEncodedStreamPosition(), null); 740 | status = Status.OPENED; 741 | if (previousStatus == Status.PLAYING) 742 | play(); 743 | else if (previousStatus == Status.PAUSED) { 744 | play(); 745 | pause(); 746 | } 747 | 748 | } catch (final IOException ex) { 749 | logger.log(Level.WARNING, ex.getMessage(), ex); 750 | } 751 | } 752 | return totalSkipped; 753 | } 754 | 755 | /** 756 | * Skip x seconds of audio 757 | * See {@link #seekBytes(long)} 758 | * 759 | * @param seconds Seconds to Skip 760 | */ 761 | @Override 762 | //todo not finished needs more validations 763 | public long seekSeconds(int seconds) throws StreamPlayerException { 764 | int durationInSeconds = this.getDurationInSeconds(); 765 | 766 | //Validate 767 | validateSeconds(seconds, durationInSeconds); 768 | 769 | //Calculate Bytes 770 | long totalBytes = getTotalBytes(); 771 | double percentage = (seconds * 100) / durationInSeconds; 772 | long bytes = (long) (totalBytes * (percentage / 100)); 773 | 774 | return seekBytes(this.getEncodedStreamPosition() + bytes); 775 | } 776 | 777 | // /** 778 | // * Skip seconds of audio based on the pattern 779 | // * See {@link #seek(long)} 780 | // * 781 | // * @param pattern A string in the format (HH:MM:SS) WHERE h = HOURS , M = minutes , S = seconds 782 | // */ 783 | // public void seek(String pattern) throws StreamPlayerException { 784 | // long bytes = 0; 785 | // 786 | // seek(bytes); 787 | // } 788 | 789 | /** 790 | * Go to X time of the Audio 791 | * See {@link #seekBytes(long)} 792 | * 793 | * @param seconds Seconds to Skip 794 | */ 795 | @Override 796 | public long seekTo(int seconds) throws StreamPlayerException { 797 | int durationInSeconds = this.getDurationInSeconds(); 798 | 799 | //Validate 800 | validateSeconds(seconds, durationInSeconds); 801 | 802 | //Calculate Bytes 803 | long totalBytes = getTotalBytes(); 804 | double percentage = (seconds * 100) / durationInSeconds; 805 | long bytes = (long) (totalBytes * (percentage / 100)); 806 | 807 | return seekBytes(bytes); 808 | } 809 | 810 | 811 | private void validateSeconds(int seconds, int durationInSeconds) { 812 | if (seconds < 0) { 813 | throw new UnsupportedOperationException("Trying to skip negative seconds "); 814 | } else if (seconds >= durationInSeconds) { 815 | throw new UnsupportedOperationException("Trying to skip with seconds {" + seconds + "} > maximum {" + durationInSeconds + "}"); 816 | } 817 | } 818 | 819 | 820 | 821 | /** 822 | * @return The duration of the source data in seconds, or -1 if duration is unavailable. 823 | */ 824 | @Override 825 | public int getDurationInSeconds() { 826 | return source.getDurationInSeconds(); 827 | } 828 | 829 | /** 830 | * @return The duration of the source data in milliseconds, or -1 if duration is unavailable. 831 | */ 832 | @Override 833 | public long getDurationInMilliseconds() { 834 | return source.getDurationInMilliseconds(); 835 | } 836 | 837 | /** 838 | * @return The duration of the source data in a {@code java.time.Duration} instance, or null if unavailable 839 | */ 840 | @Override 841 | public Duration getDuration() { 842 | return source.getDuration(); 843 | } 844 | 845 | /** 846 | * Main loop. 847 | *
848 | * Player Status == STOPPED || SEEKING = End of Thread + Freeing Audio
849 | * Resources.
1127 | * Obtains the resolution or granularity of the control, in the units that the control measures.
1128 | * The precision is the size of the increment between discrete valid values for this control,
1129 | * over the set of supported floating-point values.
1130 | *
1131 | * @return The Precision Value for the pan control, if it exists, otherwise 0.0.
1132 | */
1133 | @Override
1134 | public float getPrecision() {
1135 | return !outlet.hasControl(FloatControl.Type.PAN, outlet.getPanControl())
1136 | ? 0
1137 | : outlet.getPanControl().getPrecision();
1138 |
1139 | }
1140 |
1141 | /**
1142 | * Returns Pan value.
1143 | *
1144 | * @return The Pan Value
1145 | */
1146 | @Override
1147 | public float getPan() {
1148 | return !outlet.hasControl(FloatControl.Type.PAN, outlet.getPanControl()) ? 0.0F : outlet.getPanControl().getValue();
1149 |
1150 | }
1151 |
1152 | /**
1153 | * Return the mute Value(true || false).
1154 | *
1155 | * @return True if muted , False if not
1156 | */
1157 | @Override
1158 | public boolean getMute() {
1159 | return outlet.hasControl(BooleanControl.Type.MUTE, outlet.getMuteControl()) && outlet.getMuteControl().getValue();
1160 | }
1161 |
1162 | /**
1163 | * Return the balance Value.
1164 | *
1165 | * @return The Balance Value
1166 | */
1167 | @Override
1168 | public float getBalance() {
1169 | return !outlet.hasControl(FloatControl.Type.BALANCE, outlet.getBalanceControl()) ? 0f : outlet.getBalanceControl().getValue();
1170 | }
1171 |
1172 | /****
1173 | * Return the total size of this file in bytes.
1174 | *
1175 | * @return encodedAudioLength
1176 | */
1177 | @Override
1178 | public long getTotalBytes() {
1179 | return encodedAudioLength;
1180 | }
1181 |
1182 | /**
1183 | * @return BytePosition
1184 | */
1185 | @Override
1186 | public int getPositionByte() {
1187 | final int positionByte = AudioSystem.NOT_SPECIFIED;
1188 | if (audioProperties != null) {
1189 | if (audioProperties.containsKey("mp3.position.byte"))
1190 | return (Integer) audioProperties.get("mp3.position.byte");
1191 | if (audioProperties.containsKey("ogg.position.byte"))
1192 | return (Integer) audioProperties.get("ogg.position.byte");
1193 | }
1194 | return positionByte;
1195 | }
1196 |
1197 | /** The source data line. */
1198 | public Outlet getOutlet() {
1199 | return outlet;
1200 | }
1201 |
1202 | /**
1203 | * This method will return the status of the player
1204 | *
1205 | * @return The Player Status
1206 | */
1207 | @Override
1208 | public Status getStatus() {
1209 | return status;
1210 | }
1211 |
1212 | /**
1213 | * Deep copy of a Map.
1214 | *
1215 | * @param map The Map to be Copied
1216 | *
1217 | * @return the map that is an exact copy of the given map
1218 | */
1219 | private Map
89 | * Player Status = PAUSED. * @return False if failed(so simple...)
90 | *
91 | * @return true, if successful
92 | */
93 | boolean pause();
94 |
95 | /**
96 | * Stops the play back.
98 | * Player Status = STOPPED.
106 | * Player Status = PLAYING*
107 | *
108 | * @return False if failed(so simple...)
109 | */
110 | boolean resume();
111 |
112 | /**
113 | * Skip bytes in the File input stream. It will skip N frames matching to bytes,
114 | * so it will never skip given bytes len
115 | *
116 | * @param bytes the bytes
117 | *
118 | * @return value bigger than 0 for File and value = 0 for URL and InputStream
119 | *
120 | * @throws StreamPlayerException the stream player exception
121 | */
122 | long seekBytes(long bytes) throws StreamPlayerException;
123 |
124 | /**
125 | * Skip x seconds of audio
126 | * See {@link #seekBytes(long)}
127 | *
128 | * @param seconds Seconds to Skip
129 | */
130 | //todo not finished needs more validations
131 | long seekSeconds(int seconds) throws StreamPlayerException;
132 |
133 | /**
134 | * Go to X time of the Audio
135 | * See {@link #seekBytes(long)}
136 | *
137 | * @param seconds Seconds to Skip
138 | */
139 | long seekTo(int seconds) throws StreamPlayerException;
140 |
141 | int getDurationInSeconds();
142 |
143 | long getDurationInMilliseconds();
144 |
145 | Duration getDuration();
146 |
147 | /**
148 | * Calculates the current position of the encoded audio based on
11 | * The factory creates threads that have names on the form prefix-N-thread-M, where prefix is a string provided in the constructor,
12 | * N is the sequence number of this factory, and M is the sequence number of the thread created by this factory.
13 | */
14 | public class ThreadFactoryWithNamePrefix implements ThreadFactory {
15 |
16 | // Note: The source code for this class was based entirely on
17 | // Executors.DefaultThreadFactory class from the JDK8 source.
18 | // The only change made is the ability to configure the thread
19 | // name prefix.
20 |
21 | private static final AtomicInteger poolNumber = new AtomicInteger(1);
22 | private final ThreadGroup group;
23 | private final AtomicInteger threadNumber = new AtomicInteger(1);
24 | private final String namePrefix;
25 |
26 | /**
27 | * Creates a new ThreadFactory where threads are created with a name prefix of
214 | * The player must be started before maximum and minimum gains can be queried.
215 | *
216 | * // TODO: Is it really acceptable that we cannot check gain before the player is started?
217 | *
218 | * @throws StreamPlayerException
219 | */
220 | @Test
221 | void maximumGain() throws StreamPlayerException {
222 |
223 | player.open(audioFile);
224 | player.play();
225 | final float maximumGain = player.getMaximumGain();
226 | final float minimumGain = player.getMinimumGain();
227 | player.stop();
228 |
229 | assertTrue(minimumGain < maximumGain,
230 | String.format("Maximum gain (%.2f) should be greater than minimum gain (%.2f).",
231 | maximumGain, minimumGain)
232 | );
233 | }
234 |
235 | @Test
236 | void totalBytes() throws StreamPlayerException, InterruptedException {
237 | int expectedLengthOfExampleAudioFile = 5877062;
238 |
239 |
240 | assertEquals(-1, player.getTotalBytes());
241 |
242 | player.open(audioFile);
243 | assertEquals(expectedLengthOfExampleAudioFile, player.getTotalBytes());
244 |
245 | player.play();
246 | assertEquals(expectedLengthOfExampleAudioFile, player.getTotalBytes());
247 | }
248 |
249 | @Test
250 | void stopped() {
251 |
252 | assertFalse(player.isStopped());
253 |
254 | player.stop();
255 | assertTrue(player.isStopped());
256 | }
257 |
258 | @Test
259 | void sourceDataLine() throws StreamPlayerException {
260 | assertNull(player.getSourceDataLine());
261 |
262 | player.open(audioFile);
263 | assertNotNull(player.getSourceDataLine());
264 |
265 | player.play();
266 |
267 | assertNotNull(player.getSourceDataLine());
268 | }
269 |
270 | @Test
271 | void playing() throws StreamPlayerException {
272 |
273 | assertFalse(player.isPlaying());
274 |
275 | player.open(audioFile);
276 | assertFalse(player.isPlaying());
277 |
278 | player.play();
279 | assertTrue(player.isPlaying());
280 |
281 | player.pause();
282 | assertFalse(player.isPlaying());
283 | }
284 |
285 | @Test
286 | void pausedOrPlaying() throws StreamPlayerException {
287 |
288 | assertFalse(player.isPausedOrPlaying());
289 |
290 | player.open(audioFile);
291 | assertFalse(player.isPausedOrPlaying());
292 |
293 | player.play();
294 | assertTrue(player.isPausedOrPlaying());
295 |
296 | player.pause();
297 | assertTrue(player.isPausedOrPlaying());
298 |
299 | player.stop();
300 | assertFalse(player.isPausedOrPlaying());
301 | }
302 |
303 | @Test
304 | void paused() throws StreamPlayerException {
305 | assertFalse(player.isPaused());
306 |
307 | player.open(audioFile);
308 | assertFalse(player.isPaused());
309 |
310 | player.play();
311 | assertFalse(player.isPaused());
312 |
313 | player.pause();
314 | assertTrue(player.isPaused());
315 | }
316 |
317 | @Test
318 | void addStreamPlayerListener() throws StreamPlayerException, InterruptedException {
319 | // Setup
320 | final StreamPlayerListener listener = mock(StreamPlayerListener.class);
321 |
322 | ArgumentCaptor
850 | * Player Status == PLAYING = Audio stream data sent to Audio line.
851 | * Player Status == PAUSED = Waiting for another status.
852 | */
853 | @Override
854 | public Void call() {
855 | int nBytesRead = 0;
856 | final int audioDataLength = EXTERNAL_BUFFER_SIZE;
857 | final ByteBuffer audioDataBuffer = ByteBuffer.allocate(audioDataLength);
858 | audioDataBuffer.order(ByteOrder.LITTLE_ENDIAN);
859 |
860 | // Lock stream while playing.
861 | synchronized (audioLock) {
862 | // Main play/pause loop.
863 | while ((nBytesRead != -1) && status != Status.STOPPED && status != Status.NOT_SPECIFIED
864 | && status != Status.SEEKING) {
865 |
866 | try {
867 | // Playing?
868 | if (status == Status.PLAYING) {
869 |
870 | // System.out.println("Inside Stream Player Run method")
871 | int toRead = audioDataLength;
872 | int totalRead = 0;
873 |
874 | // Reads up a specified maximum number of bytes from audio stream
875 | // wtf i have written here omg //to fix! cause it is complicated
876 | for (; toRead > 0 && (nBytesRead = audioInputStream.read(audioDataBuffer.array(), totalRead,
877 | toRead)) != -1; toRead -= nBytesRead, totalRead += nBytesRead)
878 |
879 | // Check for under run
880 | if (outlet.getSourceDataLine().available() >= outlet.getSourceDataLine().getBufferSize())
881 | logger.info(() -> "Underrun> Available=" + outlet.getSourceDataLine().available()
882 | + " , SourceDataLineBuffer=" + outlet.getSourceDataLine().getBufferSize());
883 |
884 | // Check if anything has been read
885 | if (totalRead > 0) {
886 | trimBuffer = audioDataBuffer.array();
887 | if (totalRead < trimBuffer.length) {
888 | trimBuffer = new byte[totalRead];
889 | // Copies an array from the specified source array, beginning at the specified
890 | // position, to the specified position of the destination array
891 | // The number of components copied is equal to the length argument.
892 | System.arraycopy(audioDataBuffer.array(), 0, trimBuffer, 0, totalRead);
893 | }
894 |
895 | // Writes audio data to the mixer via this source data line
896 | outlet.getSourceDataLine().write(trimBuffer, 0, totalRead);
897 |
898 | // Compute position in bytes in encoded stream.
899 | final int nEncodedBytes = getEncodedStreamPosition();
900 |
901 | // Notify all registered Listeners
902 | listeners.forEach(listener -> {
903 | if (audioInputStream instanceof PropertiesContainer) {
904 | // Pass audio parameters such as instant
905 | // bit rate, ...
906 | listener.progress(nEncodedBytes, outlet.getSourceDataLine().getMicrosecondPosition(),
907 | trimBuffer, ((PropertiesContainer) audioInputStream).properties());
908 | } else
909 | // Pass audio parameters
910 | listener.progress(nEncodedBytes, outlet.getSourceDataLine().getMicrosecondPosition(),
911 | trimBuffer, emptyMap);
912 | });
913 |
914 | }
915 |
916 | } else if (status == Status.PAUSED) {
917 | // Flush and stop the source data line
918 | outlet.flushAndStop();
919 | goOutOfPause();
920 |
921 | }
922 | } catch (final IOException ex) {
923 | logger.log(Level.WARNING, "\"Decoder Exception: \" ", ex);
924 | status = Status.STOPPED;
925 | generateEvent(Status.STOPPED, getEncodedStreamPosition(), null);
926 | }
927 | }
928 | // Free audio resources.
929 | outlet.drainStopAndFreeDataLine();
930 |
931 | // Close stream.
932 | closeStream();
933 |
934 | // Notification of "End Of Media"
935 | if (nBytesRead == -1)
936 | generateEvent(Status.EOM, AudioSystem.NOT_SPECIFIED, null);
937 |
938 | }
939 | // Generate Event
940 | status = Status.STOPPED;
941 | generateEvent(Status.STOPPED, AudioSystem.NOT_SPECIFIED, null);
942 |
943 | // Log
944 | logger.info("Decoding thread completed");
945 |
946 | return null;
947 | }
948 |
949 | private void goOutOfPause() {
950 | try {
951 | while (status == Status.PAUSED) {
952 | Thread.sleep(50);
953 | }
954 | } catch (final InterruptedException ex) {
955 | Thread.currentThread().interrupt();
956 | logger.warning("Thread cannot sleep.\n" + ex);
957 | }
958 | }
959 |
960 | /**
961 | * Calculates the current position of the encoded audio based on
962 | * nEncodedBytes = encodedAudioLength -
963 | * encodedAudioInputStream.available();
964 | *
965 | * @return The Position of the encoded stream in term of bytes
966 | */
967 | @Override
968 | public int getEncodedStreamPosition() {
969 | int position = -1;
970 | if (source.isFile() && encodedAudioInputStream != null)
971 | try {
972 | position = encodedAudioLength - encodedAudioInputStream.available();
973 | } catch (final IOException ex) {
974 | logger.log(Level.WARNING, "Cannot get m_encodedaudioInputStream.available()", ex);
975 | stop();
976 | }
977 | return position;
978 | }
979 |
980 | /**
981 | * Close stream.
982 | */
983 | private void closeStream() {
984 | try {
985 | if (audioInputStream != null) {
986 | audioInputStream.close();
987 | logger.info("Stream closed");
988 | }
989 | } catch (final IOException ex) {
990 | logger.warning("Cannot close stream\n" + ex);
991 | }
992 | }
993 |
994 | /**
995 | * Return SourceDataLine buffer size.
996 | *
997 | * @return -1 maximum buffer size.
998 | */
999 | @Override
1000 | public int getLineBufferSize() {
1001 | return lineBufferSize;
1002 | }
1003 |
1004 | /**
1005 | * Return SourceDataLine current buffer size.
1006 | *
1007 | * @return The current line buffer size
1008 | */
1009 | @Override
1010 | public int getLineCurrentBufferSize() {
1011 | return currentLineBufferSize;
1012 | }
1013 |
1014 | /**
1015 | * Returns all available mixers.
1016 | *
1017 | * @return A List of available Mixers
1018 | */
1019 | @Override
1020 | public List
88 | *
97 | *
99 | * Thread should free Audio resources.
100 | */
101 | void stop();
102 |
103 | /**
104 | * Resumes the play back.
105 | *
149 | * nEncodedBytes = encodedAudioLength -
150 | * encodedAudioInputStream.available();
151 | *
152 | * @return The Position of the encoded stream in term of bytes
153 | */
154 | int getEncodedStreamPosition();
155 |
156 | /**
157 | * Return SourceDataLine buffer size.
158 | *
159 | * @return -1 maximum buffer size.
160 | */
161 | int getLineBufferSize();
162 |
163 | /**
164 | * Return SourceDataLine current buffer size.
165 | *
166 | * @return The current line buffer size
167 | */
168 | int getLineCurrentBufferSize();
169 |
170 | /**
171 | * Returns all available mixers.
172 | *
173 | * @return A List of available Mixers
174 | */
175 | Listprefix
.
28 | *
29 | * @param prefix
30 | * Thread name prefix. Never use a value of "pool" as in that case you might as well have used
31 | * {@link java.util.concurrent.Executors#defaultThreadFactory()}.
32 | */
33 | public ThreadFactoryWithNamePrefix(String prefix) {
34 | SecurityManager s = System.getSecurityManager();
35 | group = ( s != null ) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
36 | namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-";
37 | }
38 |
39 | @Override
40 | public Thread newThread(Runnable r) {
41 | Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
42 | if (t.isDaemon()) {
43 | t.setDaemon(false);
44 | }
45 | if (t.getPriority() != Thread.NORM_PRIORITY) {
46 | t.setPriority(Thread.NORM_PRIORITY);
47 | }
48 | return t;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/goxr3plus/streamplayer/stream/UrlDataSource.java:
--------------------------------------------------------------------------------
1 | package com.goxr3plus.streamplayer.stream;
2 |
3 | import javax.sound.sampled.AudioFileFormat;
4 | import javax.sound.sampled.AudioInputStream;
5 | import javax.sound.sampled.AudioSystem;
6 | import javax.sound.sampled.UnsupportedAudioFileException;
7 | import java.io.IOException;
8 | import java.net.URL;
9 | import java.time.Duration;
10 |
11 | public class UrlDataSource implements DataSource {
12 |
13 | private final URL source;
14 |
15 | UrlDataSource(URL source) {
16 | this.source = source;
17 | }
18 |
19 | @Override
20 | public AudioFileFormat getAudioFileFormat() throws UnsupportedAudioFileException, IOException {
21 | return AudioSystem.getAudioFileFormat(source);
22 | }
23 |
24 | @Override
25 | public AudioInputStream getAudioInputStream() throws UnsupportedAudioFileException, IOException {
26 | return AudioSystem.getAudioInputStream(source);
27 | }
28 |
29 | @Override
30 | public int getDurationInSeconds() {
31 | return -1;
32 | }
33 |
34 | @Override
35 | public long getDurationInMilliseconds() {
36 | return -1;
37 | }
38 |
39 | @Override
40 | public Duration getDuration() {
41 | return null;
42 | }
43 |
44 | @Override
45 | public Object getSource() {
46 | return source;
47 | }
48 |
49 | @Override
50 | public String toString() {
51 | return "UrlDataSource with " + source.toString();
52 | }
53 |
54 | @Override
55 | public boolean isFile() {
56 | return false;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/com/goxr3plus/streamplayer/tools/IOInfo.java:
--------------------------------------------------------------------------------
1 | package com.goxr3plus.streamplayer.tools;
2 |
3 | import org.apache.commons.io.FilenameUtils;
4 |
5 | public class IOInfo {
6 |
7 | /**
8 | * Returns the extension of file(without (.)) for example (ai.mp3)->(mp3)
9 | * and to lowercase (Mp3 -> mp3)
10 | *
11 | * @param absolutePath The File absolute path
12 | *
13 | * @return the File extension
14 | */
15 | public static String getFileExtension(final String absolutePath) {
16 | return FilenameUtils.getExtension(absolutePath).toLowerCase();
17 |
18 | // int i = path.lastIndexOf('.'); // characters contained before (.)
19 | //
20 | // if the name is not empty
21 | // if (i > 0 && i < path.length() - 1)
22 | // return path.substring(i + 1).toLowerCase()
23 | //
24 | // return null
25 | }
26 |
27 | /**
28 | * Returns the name of the file for example if file path is (C:/Give me
29 | * more/no no/media.ogg) it returns (media.ogg)
30 | *
31 | * @param absolutePath the path
32 | *
33 | * @return the File title+extension
34 | */
35 | public static String getFileName(final String absolutePath) {
36 | return FilenameUtils.getName(absolutePath);
37 |
38 | }
39 |
40 | /**
41 | * Returns the title of the file for example if file name is (club.mp3)
42 | * it returns (club)
43 | *
44 | * @param absolutePath The File absolute path
45 | *
46 | * @return the File title
47 | */
48 | public static String getFileTitle(final String absolutePath) {
49 | return FilenameUtils.getBaseName(absolutePath);
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/com/goxr3plus/streamplayer/tools/TimeTool.java:
--------------------------------------------------------------------------------
1 | package com.goxr3plus.streamplayer.tools;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 |
6 | import javax.sound.sampled.AudioFormat;
7 | import javax.sound.sampled.AudioInputStream;
8 | import javax.sound.sampled.AudioSystem;
9 | import javax.sound.sampled.UnsupportedAudioFileException;
10 |
11 | import org.jaudiotagger.audio.mp3.MP3AudioHeader;
12 | import org.jaudiotagger.audio.mp3.MP3File;
13 |
14 | import com.goxr3plus.streamplayer.enums.AudioType;
15 |
16 | public final class TimeTool {
17 |
18 | private TimeTool() {
19 | }
20 |
21 | /**
22 | * Returns the time in format %02d:%02d.
23 | *
24 | * @param seconds the seconds
25 | * @return the time edited on hours
26 | */
27 | public static String getTimeEditedOnHours(final int seconds) {
28 |
29 | return String.format("%02d:%02d", seconds / 60, seconds % 60);
30 |
31 | }
32 |
33 | /**
34 | * Returns the time in format %02d:%02d:%02d if( minutes >60 ) or
35 | * %02dsec if (seconds<60) %02d:%02d.
36 | *
37 | * @param seconds the seconds
38 | * @return the time edited in format %02d:%02d:%02d if( minutes >60 ) or
39 | * %02d:%02d. [[SuppressWarningsSpartan]]
40 | */
41 | public static String getTimeEdited(final int seconds) {
42 | if (seconds < 60) // duration < 1 minute
43 | return String.format("%02ds", seconds % 60);
44 | else if ((seconds / 60) / 60 <= 0) // duration < 1 hour
45 | return String.format("%02dm:%02d", (seconds / 60) % 60, seconds % 60);
46 | else
47 | return String.format("%02dh:%02dm:%02d", (seconds / 60) / 60, (seconds / 60) % 60, seconds % 60);
48 | }
49 |
50 | /**
51 | * /** Returns the time in format %02d:%02d:%02d if( minutes >60 ) or
52 | * %02d:%02d.
53 | *
54 | * @param ms The milliseconds
55 | * @return The Time edited in format %02d:%02d:%02d if( minutes >60 ) or
56 | * %02d:%02d.
57 | *
58 | */
59 | public static String millisecondsToTime(final long ms) {
60 | final int millis = (int) ((ms % 1000) / 100);
61 | // int seconds = (int) ((ms / 1000) % 60);
62 | // int minutes = (int) ((ms / (1000 * 60)) % 60);
63 | // int hours = (int) ((ms / (1000 * 60 * 60)) % 24);
64 |
65 | // if (minutes > 60)
66 | // return String.format("%02d:%02d:%02d.%d", hours, minutes, seconds, millis);
67 | // else
68 | // return String.format("%02d:%02d.%d", minutes, seconds, millis);
69 |
70 | return String.format(".%d", millis);
71 |
72 | }
73 |
74 | /**
75 | * Returns the time of Audio to seconds
76 | *
77 | * @param name the name
78 | * @param type
79 | * 1->URL
80 | * 2->FILE
81 | * 3->INPUTSTREAM
82 | * @return time in milliseconds
83 | */
84 | public static int durationInSeconds(final String name, final AudioType type) {
85 |
86 | final long time = TimeTool.durationInMilliseconds(name, type);
87 |
88 | return (int) ((time == 0 || time == -1) ? time : time / 1000);
89 |
90 | // Long microseconds = (Long)AudioSystem.getAudioFileFormat(new
91 | // File(audio)).properties().get("duration") int mili = (int)(microseconds /
92 | // 1000L);
93 | // int sec = milli / 1000 % 60;
94 | // int min = milli / 1000 / 60;
95 |
96 | }
97 |
98 | /**
99 | * This method determines the duration of given data.
100 | *
101 | * @param input The name of the input
102 | * @param audioType URL, FILE, INPUTSTREAM, UNKOWN;
103 | * @return Returns the duration of URL/FILE/INPUTSTREAM in milliseconds
104 | */
105 | public static long durationInMilliseconds(final String input, final AudioType audioType) {
106 | return audioType == AudioType.FILE ? durationInMilliseconds_Part2(new File(input))
107 | : (audioType == AudioType.URL || audioType == AudioType.INPUTSTREAM || audioType == AudioType.UNKNOWN)
108 | ? -1
109 | : -1;
110 | }
111 |
112 | /**
113 | * Used by method durationInMilliseconds() to get file duration.
114 | *
115 | * @param file the file
116 | * @return the int
117 | */
118 | private static long durationInMilliseconds_Part2(final File file) {
119 | long milliseconds = -1;
120 |
121 | // exists?
122 | if (file.exists() && file.length() != 0) {
123 |
124 | // extension?
125 | final String extension = IOInfo.getFileExtension(file.getName());
126 |
127 | // MP3?
128 | if ("mp3".equals(extension)) {
129 | try {
130 | milliseconds = new MP3File(file).getMP3AudioHeader().getTrackLength() * 1000;
131 | if (milliseconds == 0) {
132 | MP3AudioHeader header = new MP3File(file).getMP3AudioHeader();
133 | int samplesPerFrame;
134 | switch(header.getMpegLayer()) {
135 | case("Layer 1"):
136 | samplesPerFrame = 384;
137 | break;
138 | case("Layer 2"):
139 | samplesPerFrame = 576;
140 | break;
141 | case("Layer 3"):
142 | samplesPerFrame = 1152;
143 | break;
144 | default:
145 | samplesPerFrame = 1152;
146 | break;
147 | }
148 |
149 | double frameLengthInMilliseconds = (((double) samplesPerFrame / header.getSampleRateAsNumber()) * 1000);
150 | milliseconds = (long) (header.getNumberOfFrames() * frameLengthInMilliseconds);
151 | }
152 |
153 | // milliseconds = (int) ( (Long)
154 | // AudioSystem.getAudioFileFormat(file).properties().get("duration") / 1000 );
155 |
156 | // Get the result of mp3agic if the duration is bigger than 6 minutes
157 | // if (milliseconds / 1000 > 60 * 9) {
158 | // System.out.println("Entered..");
159 | // milliseconds = tryWithMp3Agic(file);
160 | // }
161 |
162 | } catch (final Exception ex) {
163 | System.err.println("Problem getting the time of-> " + file.getAbsolutePath());
164 | }
165 | // }
166 | }
167 | // WAVE || OGG?
168 | else if ("ogg".equals(extension) || "wav".equals(extension)) {
169 | try (AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file)) {
170 | final AudioFormat format = audioInputStream.getFormat();
171 | milliseconds = (long) (((double) file.length() / ( format.getFrameSize() * (double) format.getFrameRate())) * 1000);
172 | } catch (IOException | UnsupportedAudioFileException ex) {
173 | System.err.println("Problem getting the time of-> " + file.getAbsolutePath());
174 | }
175 | }
176 | }
177 |
178 | // System.out.println("Passed with error")
179 | return milliseconds < 0 ? -1 : milliseconds;
180 | }
181 |
182 | }
183 |
--------------------------------------------------------------------------------
/src/test/java/com/goxr3plus/streamplayer/stream/SourceDataLineTest.java:
--------------------------------------------------------------------------------
1 | package com.goxr3plus.streamplayer.stream;
2 |
3 | import org.junit.jupiter.api.AfterEach;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.Test;
6 | import org.mockito.BDDMockito;
7 |
8 | import javax.sound.sampled.SourceDataLine;
9 | import java.io.File;
10 | import java.util.logging.Logger;
11 |
12 | import static java.lang.Math.log10;
13 | import static org.junit.jupiter.api.Assertions.*;
14 | import static org.mockito.ArgumentMatchers.booleanThat;
15 | import static org.mockito.Mockito.mock;
16 |
17 | public class SourceDataLineTest {
18 |
19 | StreamPlayer player;
20 | private File audioFile;
21 |
22 | @BeforeEach
23 | void setup() {
24 | final Logger logger = mock(Logger.class);
25 | player = new StreamPlayer(logger);
26 | audioFile = new File("Logic - Ballin [Bass Boosted].mp3");
27 | }
28 |
29 | @AfterEach
30 | void tearDown() {
31 | player.stop();
32 | }
33 |
34 | @Test
35 | void gain() throws StreamPlayerException, InterruptedException {
36 | // Setup
37 | final double gain1 = 0.83;
38 | final double gain2 = 0.2;
39 | final double delta = 0.05;
40 | final boolean listen = false;
41 |
42 | // Exercise
43 | final float initialGain = player.getGainValue();
44 | player.open(audioFile);
45 | player.seekTo(30);
46 | player.play();
47 | player.setGain(gain1);
48 | final float actualGain1First = player.getGainValue();
49 | if (listen) Thread.sleep(2000);
50 | final float actualGain1 = player.getGainValue();
51 |
52 | player.setGain(gain2);
53 | if (listen) Thread.sleep(2000);
54 | final float actualGain2 = player.getGainValue();
55 |
56 | player.setGain(gain1);
57 | if (listen) Thread.sleep(2000);
58 |
59 | player.stop();
60 |
61 | // Verify
62 | assertEquals(0, initialGain);
63 | assertEquals(actualGain1First, actualGain1);
64 | assertEquals(20*log10(gain1), actualGain1, delta); // TODO: Investigate probable bug.
65 | // fail("Test not done");
66 | }
67 |
68 | /**
69 | * Plays music if "listen" is true.
70 | * Varies the gain, and checks that it can be read back.
71 | * If listen is true, it plays for 2 seconds per gain level.
72 | *
73 | * @throws StreamPlayerException
74 | * @throws InterruptedException
75 | */
76 | @Test
77 | void logScaleGain() throws StreamPlayerException, InterruptedException {
78 | // Setup
79 | final boolean listen = false;
80 |
81 | // Exercise
82 |
83 | player.open(audioFile);
84 | player.seekTo(30);
85 | player.play();
86 |
87 | assertGainCanBeSetTo(-10, listen);
88 | assertGainCanBeSetTo(-75, listen);
89 | assertGainCanBeSetTo(0, listen);
90 | assertGainCanBeSetTo(6, listen);
91 |
92 | player.stop();
93 | }
94 |
95 | private void assertGainCanBeSetTo(double gain, boolean listen) throws InterruptedException {
96 | final float atGain = playAtGain(listen, gain);
97 | assertEquals(gain, atGain, 0.01);
98 | }
99 |
100 | private float playAtGain(boolean listen, double gain) throws InterruptedException {
101 | player.setLogScaleGain(gain);
102 | if (listen) {
103 | Thread.sleep(2000);
104 | }
105 | return player.getGainValue();
106 | }
107 |
108 | @Test
109 | void balance() throws StreamPlayerException {
110 | // Setup
111 | final float wantedBalance = 0.5f;
112 |
113 | //Exercise
114 | player.open(audioFile);
115 | player.play(); // Necessary to be able to set the balance
116 |
117 | final float initialBalance = player.getBalance();
118 | player.setBalance(wantedBalance);
119 | player.stop(); // Probably not needed, but cleanup is good.
120 | final float actualBalance = player.getBalance(); // Can be made before or after stop()
121 |
122 | // Verify
123 | assertEquals(0, initialBalance);
124 | assertEquals(wantedBalance, actualBalance);
125 | }
126 |
127 | @Test
128 | void pan() throws StreamPlayerException {
129 | double delta = 1e-6;
130 | final float initialPan = player.getPan();
131 | assertEquals(0, initialPan);
132 |
133 | player.open(audioFile);
134 | player.play();
135 |
136 | double pan = -0.9;
137 | player.setPan(pan);
138 | assertEquals(pan, player.getPan(), delta);
139 |
140 | double outsideRange = 1.1;
141 | player.setPan(outsideRange);
142 | assertEquals(pan, player.getPan(), delta);
143 | }
144 |
145 | @Test
146 | void mute() throws StreamPlayerException {
147 | assertFalse(player.getMute());
148 | player.setMute(true);
149 | assertFalse(player.getMute());
150 | player.open(audioFile);
151 | player.setMute(true);
152 | assertFalse(player.getMute());
153 |
154 | player.play();
155 | player.setMute(true);
156 | assertTrue(player.getMute()); // setMute works only after play() has been called.
157 |
158 |
159 | player.setMute(false);
160 | assertFalse(player.getMute());
161 | }
162 |
163 | @Test
164 | void sourceDataLine() throws StreamPlayerException {
165 | assertNull(player.getSourceDataLine());
166 |
167 | player.open(audioFile);
168 | assertNotNull(player.getSourceDataLine());
169 |
170 | player.play();
171 |
172 | assertNotNull(player.getSourceDataLine());
173 | }
174 |
175 | @Test
176 | void playAndPause() throws StreamPlayerException, InterruptedException {
177 | boolean listen = true;
178 | player.open(audioFile);
179 | player.play();
180 | player.seekTo(30);
181 | if (listen) Thread.sleep(200);
182 |
183 | player.pause();
184 | if (listen) Thread.sleep(100);
185 |
186 | player.resume(); // TODO: Examine what happens if play() is called instead.
187 | if (listen) Thread.sleep(200);
188 | //player.stop();
189 |
190 | // TODO: asserts and listen=false
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerEventTest.java:
--------------------------------------------------------------------------------
1 | package com.goxr3plus.streamplayer.stream;
2 |
3 | import com.goxr3plus.streamplayer.enums.Status;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.Test;
6 | import org.mockito.configuration.IMockitoConfiguration;
7 |
8 | import static org.junit.jupiter.api.Assertions.*;
9 | import static org.mockito.Mockito.mock;
10 |
11 | class StreamPlayerEventTest {
12 |
13 | private StreamPlayer source;
14 | private Object description;
15 | private Status status;
16 | private int encodededStreamPosition;
17 | private StreamPlayerEvent event;
18 |
19 | @BeforeEach
20 | void setUp() {
21 | description = new Object();
22 | source = mock(StreamPlayer.class);
23 | status = Status.RESUMED;
24 | encodededStreamPosition = 12345;
25 | event = new StreamPlayerEvent(source, status, encodededStreamPosition, description);
26 | }
27 |
28 | @Test
29 | void itReturnsTheStatus() {
30 | assertEquals(status, event.getPlayerStatus());
31 | }
32 |
33 | @Test
34 | void itReturnsTheEncodedStreamPosition() {
35 | assertEquals(encodededStreamPosition, event.getEncodedStreamPosition());
36 | }
37 |
38 | @Test
39 | void itReturnsTheSource() {
40 | assertSame(source, event.getSource());
41 | }
42 |
43 | @Test
44 | void itReturnsTheDescription() {
45 | assertSame(description, event.getDescription());
46 | }
47 |
48 | @Test
49 | void itReturnsAString() {
50 | final String actual = event.toString();
51 | final String expected = "Source :="
52 | + source.toString()
53 | + " , Player Status := RESUMED , EncodedStreamPosition :=12345 , Description :="
54 | + description.toString();
55 | assertEquals(expected, actual);
56 | }
57 |
58 |
59 | }
--------------------------------------------------------------------------------
/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java:
--------------------------------------------------------------------------------
1 | package com.goxr3plus.streamplayer.stream;
2 |
3 | import com.goxr3plus.streamplayer.enums.Status;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.Disabled;
6 | import org.junit.jupiter.api.DisplayName;
7 | import org.junit.jupiter.api.Test;
8 |
9 | import javax.sound.sampled.AudioFileFormat;
10 | import javax.sound.sampled.AudioSystem;
11 | import javax.sound.sampled.SourceDataLine;
12 | import javax.sound.sampled.UnsupportedAudioFileException;
13 | import java.io.File;
14 | import java.io.IOException;
15 | import java.util.List;
16 | import java.util.logging.Logger;
17 |
18 | import static org.junit.jupiter.api.Assertions.*;
19 | import static org.mockito.Mockito.mock;
20 |
21 | /**
22 | * Tests of all or most of the public methods of StreamPlayer.
23 | * These unit tests are written primarily as documentation of the behavior and as example use case,
24 | * not as a part of test driven development.
25 | */
26 | public class StreamPlayerFutureImprovementTest {
27 | StreamPlayer player;
28 | private File audioFile;
29 |
30 | @BeforeEach
31 | void setup() {
32 | final Logger logger = mock(Logger.class);
33 | player = new StreamPlayer(logger);
34 | audioFile = new File("Logic - Ballin [Bass Boosted].mp3");
35 | }
36 |
37 | /**
38 | * This test fails if it's permitted to add a null to the StreamPlayer listener list.
39 | */
40 | @Test
41 | @Disabled("This test fails with the current implementation. The test exists to illustrate a future improvement.")
42 | void addStreamPlayerListener_dontAcceptNull() {
43 | // We can't allow nulls in the list of listeners, because they will cause NullPointerExceptions.
44 | // One way to handle it is to require that an exception is thrown immediately when we
45 | // try to add the null.
46 | assertThrows(NullPointerException.class, () -> player.addStreamPlayerListener(null));
47 |
48 | // An alternative way would be to use some kind of null annotation, to disallow
49 | // nulls being passed at compile time.
50 | }
51 |
52 |
53 | @Test
54 | @DisplayName("When play() is called without first calling open(), an exception is thrown")
55 | @Disabled("This test fails with the current implementation. The test exists to illustrate a future improvement.")
56 | void playingUnopenedSourceThrowsException() {
57 |
58 | assertThrows(Exception.class, () -> player.play());
59 | }
60 |
61 | @Test
62 | @Disabled("This test fails with the current implementation. The test exists to illustrate a future improvement.")
63 | void seekBytes() throws StreamPlayerException {
64 | player.open(audioFile);
65 | player.play();
66 | int positionByte1 = player.getPositionByte();
67 |
68 | player.seekBytes(100);
69 | int positionByte2 = player.getPositionByte();
70 |
71 | assertTrue( positionByte2 > positionByte1);
72 |
73 | // TODO: It seems that getPositionByte doesn't work.
74 | // It isn't called from within this project, except for in this test.
75 | // It is however called by XR3Player. If XR3Player needs this method, it must be tested
76 | // within this project. The method relies on a map, which doesn't seem to be updated by play()
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java:
--------------------------------------------------------------------------------
1 | package com.goxr3plus.streamplayer.stream;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.junit.jupiter.api.Assertions.assertFalse;
5 | import static org.junit.jupiter.api.Assertions.assertNotEquals;
6 | import static org.junit.jupiter.api.Assertions.assertNotNull;
7 | import static org.junit.jupiter.api.Assertions.assertNull;
8 | import static org.junit.jupiter.api.Assertions.assertTrue;
9 | import static org.junit.jupiter.api.Assertions.fail;
10 | import static org.mockito.ArgumentMatchers.any;
11 | import static org.mockito.ArgumentMatchers.anyInt;
12 | import static org.mockito.ArgumentMatchers.anyLong;
13 | import static org.mockito.Mockito.atLeast;
14 | import static org.mockito.Mockito.atMost;
15 | import static org.mockito.Mockito.mock;
16 | import static org.mockito.Mockito.spy;
17 | import static org.mockito.Mockito.times;
18 | import static org.mockito.Mockito.verify;
19 |
20 | import java.io.File;
21 | import java.io.IOException;
22 | import java.util.List;
23 | import java.util.Map;
24 | import java.util.logging.Logger;
25 |
26 | import javax.sound.sampled.*;
27 |
28 | import org.junit.jupiter.api.BeforeEach;
29 | import org.junit.jupiter.api.Disabled;
30 | import org.junit.jupiter.api.Test;
31 | import org.mockito.ArgumentCaptor;
32 |
33 | import com.goxr3plus.streamplayer.enums.Status;
34 |
35 | /**
36 | * Tests of all or most of the public methods of StreamPlayer.
37 | * These unit tests are written primarily as documentation of the behavior and as example use case,
38 | * not as a part of test driven development.
39 | */
40 | public class StreamPlayerMethodsTest {
41 | StreamPlayer player;
42 | private File audioFile;
43 |
44 | @BeforeEach
45 | void setup() {
46 | final Logger logger = mock(Logger.class);
47 | player = new StreamPlayer(logger);
48 | audioFile = new File("Logic - Ballin [Bass Boosted].mp3");
49 | }
50 |
51 | @Test
52 | void duration() throws StreamPlayerException {
53 | audioFile = new File("Logic - Ballin [Bass Boosted].mp3");
54 | player.open(audioFile);
55 | assertEquals(245, player.getDurationInSeconds());
56 | assertEquals(245000, player.getDurationInMilliseconds());
57 | assertNotNull(player.getDuration());
58 | assertEquals(245, player.getDuration().getSeconds());
59 | assertEquals(player.getDuration().toMillis(), player.getDurationInMilliseconds());
60 |
61 | audioFile = new File("kick.wav");
62 | player.open(audioFile);
63 | assertEquals(0, player.getDurationInSeconds());
64 | assertEquals(111, player.getDurationInMilliseconds());
65 |
66 | audioFile = new File("kick.mp3");
67 | player.open(audioFile);
68 | assertEquals(0, player.getDurationInSeconds());
69 | // Note: the result of calculating a .mp3's duration is different than that of a .wav file
70 | assertEquals(156, player.getDurationInMilliseconds());
71 | }
72 |
73 | @Test
74 | void balance() throws StreamPlayerException {
75 | // Setup
76 | final float wantedBalance = 0.5f;
77 |
78 | //Exercise
79 | player.open(audioFile);
80 | player.play(); // Necessary to be able to set the balance
81 |
82 | final float initialBalance = player.getBalance();
83 | player.setBalance(wantedBalance);
84 | player.stop(); // Probably not needed, but cleanup is good.
85 | final float actualBalance = player.getBalance(); // Can be made before or after stop()
86 |
87 | // Verify
88 | assertEquals(0, initialBalance);
89 | assertEquals(wantedBalance, actualBalance);
90 | }
91 |
92 | @Test
93 | void status() throws StreamPlayerException {
94 | // Setup
95 | final File audioFile = new File("Logic - Ballin [Bass Boosted].mp3");
96 |
97 | // Exercise
98 | final Status initialStatus = player.getStatus();
99 |
100 | player.open(audioFile);
101 | final Status statusAfterOpen = player.getStatus();
102 |
103 | player.stop();
104 | final Status statusAfterFirstStop = player.getStatus();
105 |
106 | player.play();
107 | final Status statusAfterPlay = player.getStatus();
108 |
109 | player.pause();
110 | final Status statusAfterPause = player.getStatus();
111 |
112 | player.seekTo(40);
113 | final Status statusAfterSeeking = player.getStatus();
114 |
115 | player.stop();
116 | final Status statusAfterSecondStop = player.getStatus();
117 |
118 | // Verify
119 | assertEquals(Status.NOT_SPECIFIED, initialStatus);
120 | assertEquals(Status.OPENED, statusAfterOpen);
121 | assertEquals(Status.STOPPED, statusAfterFirstStop);
122 | assertEquals(Status.PLAYING, statusAfterPlay);
123 | assertEquals(Status.PAUSED, statusAfterPause);
124 | assertEquals(Status.PAUSED, statusAfterSeeking); // Still paused (or paused again)
125 | assertEquals(Status.STOPPED, statusAfterSecondStop);
126 | }
127 |
128 | @Test
129 | void gain() throws StreamPlayerException, InterruptedException {
130 | // Setup
131 | final double gain1_dB = 0.5;
132 | final double gain2 = 0.2;
133 | final double delta = 0.05;
134 |
135 | // By setting listen to true, you an listen to the musig being played,
136 | // and hear that the gain changes.
137 | // This is totally against the rules for unit testing, but can be useful.
138 | final boolean listen = false;
139 |
140 | // Exercise
141 | final float initialGain = player.getGainValue();
142 | player.open(audioFile);
143 | player.seekTo(30);
144 | player.play();
145 | player.setGain(gain1_dB);
146 | final float actualGain0 = player.getGainValue();
147 | if (listen) Thread.sleep(2000);
148 | final float actualGain1 = player.getGainValue();
149 |
150 | player.setGain(gain2);
151 | if (listen) Thread.sleep(2000);
152 | final float actualGain2 = player.getGainValue();
153 |
154 | player.setGain(gain1_dB);
155 | if (listen) Thread.sleep(2000);
156 |
157 | player.stop();
158 |
159 | // Verify
160 | assertEquals(0, initialGain);
161 | assertEquals(actualGain0, actualGain1);
162 | assertEquals(20.0 * Math.log10(gain1_dB), actualGain1, delta);
163 |
164 | // TODO: Consider changing the API. setGain() and getGainValue() have different scales.
165 | // setGain(linear scale),
166 | // whereas getGainValue() returns a logarithmic dB scale value. This is inconsistent.
167 | }
168 |
169 | /**
170 | * Plays music if "listen" is true.
171 | * Varies the gain, and checks that it can be read back.
172 | * If listen is true, it plays for 2 seconds per gain level.
173 | *
174 | * @throws StreamPlayerException
175 | * @throws InterruptedException
176 | */
177 | @Test
178 | void logScaleGain() throws StreamPlayerException, InterruptedException {
179 | // Setup
180 | final boolean listen = false; // Set to true to listen to the test.
181 |
182 | // Exercise
183 |
184 | player.open(audioFile);
185 | player.seekTo(30);
186 | player.play();
187 |
188 | assertGainCanBeSetTo(-10, listen);
189 | assertGainCanBeSetTo(-75, listen);
190 | assertGainCanBeSetTo(0, listen);
191 | assertGainCanBeSetTo(6, listen);
192 |
193 | player.stop();
194 | }
195 |
196 | private void assertGainCanBeSetTo(double gain, boolean listen) throws InterruptedException {
197 | final float atGain = playAtGain(listen, gain);
198 | assertEquals(gain, atGain, 0.01);
199 | }
200 |
201 | private float playAtGain(boolean listen, double gain) throws InterruptedException {
202 | player.setLogScaleGain(gain);
203 | if (listen) {
204 | Thread.sleep(2000);
205 | }
206 | return player.getGainValue();
207 | }
208 |
209 | /**
210 | * Test that the maximum gain is greater than the minimum gain. That is about all we can expect.
211 | * The actual values depend on the available {@link SourceDataLine}.
212 | * We don't know anything about its scale beforehand.
213 | *