From 7c71c031ec6f00961cd5f613953ab32e8716a18a Mon Sep 17 00:00:00 2001 From: Markil3 <75867393+Markil3@users.noreply.github.com> Date: Wed, 11 Aug 2021 10:16:56 -0600 Subject: [PATCH] Some more work with playback. WARNING: UNSTABLE --- .../browserCommands/CommandConfirmation.java | 6 +- .../universeplayer/PlaybackListener.java | 86 +++++ .../regis/universeplayer/PlaybackStatus.java | 28 ++ .../java/edu/regis/universeplayer/Player.java | 65 +++- .../universeplayer/browser/InternetSong.java | 14 + .../{Player.java => LocalPlayer.java} | 230 +++++++++++-- .../universeplayer/player/Interface.java | 4 +- .../universeplayer/player/PlayerControls.java | 189 +++++++++-- player/build.gradle | 4 + player/src/main/c/interface.c | 259 +++++++++++++++ player/src/main/c/interface.h | 88 +++++ player/src/main/c/player.c | 305 +----------------- player/src/main/c/player.h | 65 ++++ player/src/main/c/player_win.c | 109 ++++++- player/src/main/c/windows_mutex.c | 27 ++ 15 files changed, 1128 insertions(+), 351 deletions(-) create mode 100644 interface/src/main/java/edu/regis/universeplayer/PlaybackListener.java create mode 100644 interface/src/main/java/edu/regis/universeplayer/PlaybackStatus.java create mode 100644 interface/src/main/java/edu/regis/universeplayer/browser/InternetSong.java rename interface/src/main/java/edu/regis/universeplayer/localPlayer/{Player.java => LocalPlayer.java} (50%) create mode 100644 player/src/main/c/interface.c create mode 100644 player/src/main/c/interface.h create mode 100644 player/src/main/c/player.h create mode 100644 player/src/main/c/windows_mutex.c diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandConfirmation.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandConfirmation.java index 763a7eb..53544c0 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandConfirmation.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandConfirmation.java @@ -17,7 +17,7 @@ public class CommandConfirmation implements Serializable /** * The error returned by the browser, or null if execution was successful. */ - private BrowserError errorCode; + private Throwable errorCode; private String message; /** @@ -44,7 +44,7 @@ public class CommandConfirmation implements Serializable * * @param error - The error thrown, or null if the command was successful. */ - public CommandConfirmation(BrowserError error) + public CommandConfirmation(Throwable error) { this.errorCode = error; } @@ -64,7 +64,7 @@ public class CommandConfirmation implements Serializable * * @return The error, or null if the command was successful. */ - public BrowserError getError() + public Throwable getError() { return this.errorCode; } diff --git a/interface/src/main/java/edu/regis/universeplayer/PlaybackListener.java b/interface/src/main/java/edu/regis/universeplayer/PlaybackListener.java new file mode 100644 index 0000000..c484faf --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/PlaybackListener.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer; + +import edu.regis.universeplayer.data.Song; + +import java.util.EventListener; +import java.util.EventObject; + +/** + * A playback listener allows a class to listen for events regarding player + * updates. + * + * @author William Hubbard + * @verison 0.1 + */ +public interface PlaybackListener extends EventListener +{ + /** + * Called when playback is changed. + * + * @param status - The playback status. + */ + void onPlaybackChanged(PlaybackInfo status); + + class PlaybackInfo extends EventObject + { + private final Song currentSong; + /** + * The time we are currently at in the song, in seconds. + */ + private final float playTime; + + /** + * The current status of the player. + */ + private final PlaybackStatus status; + + public PlaybackInfo(Player player, Song song, float playTime, PlaybackStatus status) + { + super(player); + this.currentSong = song; + this.playTime = playTime; + this.status = status; + } + + @Override + public Player getSource() + { + return (Player) super.getSource(); + } + + /** + * Obtains the song currently playing. + * + * @return The current song, or null if none is loaded. + */ + public Song getSong() + { + return this.currentSong; + } + + /** + * Obtains the current play time. + * + * @return The play time of the player. This will be zero if the song is + * stopped or not loaded. + */ + public float getPlayTime() + { + return this.playTime; + } + + /** + * Obtains the current status of the player. + * + * @return - The player's playback status. + */ + public PlaybackStatus getStatus() + { + return this.status; + } + } +} diff --git a/interface/src/main/java/edu/regis/universeplayer/PlaybackStatus.java b/interface/src/main/java/edu/regis/universeplayer/PlaybackStatus.java new file mode 100644 index 0000000..086df3a --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/PlaybackStatus.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer; + +/** + * This contains possible states that a player can be in. + */ +public enum PlaybackStatus +{ + /** + * The player is currently playing a song. + */ + PLAYING, + /** + * The player has been paused. + */ + PAUSED, + /** + * The player has a song loaded, but is not playing it. + */ + STOPPED, + /** + * No song is loaded. + */ + EMPTY; +} diff --git a/interface/src/main/java/edu/regis/universeplayer/Player.java b/interface/src/main/java/edu/regis/universeplayer/Player.java index 66f0a7a..60ee44e 100644 --- a/interface/src/main/java/edu/regis/universeplayer/Player.java +++ b/interface/src/main/java/edu/regis/universeplayer/Player.java @@ -7,7 +7,7 @@ package edu.regis.universeplayer; import edu.regis.universeplayer.browserCommands.QueryFuture; import edu.regis.universeplayer.data.Song; -import java.util.concurrent.Future; +import java.util.HashMap; /** * This interface serves as the connection to a music player of some sort, whether it be @@ -19,6 +19,31 @@ import java.util.concurrent.Future; */ public interface Player { + HashMap, Player> REGISTERED_PLAYERS = new HashMap<>(); + + /** + * Obtains a song player that can play the provided song. + * + * @param song - The song to play. + * @return A compatible player, or null if none is found. + */ + static Player getCompatiblePlayer(Song song) + { + Player player = REGISTERED_PLAYERS.get(song.getClass()); + if (player == null) + { + for (Class songClass : REGISTERED_PLAYERS.keySet()) + { + if (songClass.isAssignableFrom(song.getClass())) + { + player = REGISTERED_PLAYERS.get(songClass); + break; + } + } + } + return player; + } + /** * Obtains the song currently playing. * @@ -36,22 +61,30 @@ public interface Player /** * Enables playback of the current song, if one is active. + * * @return A confirmation of whether the command was successful or not. */ QueryFuture play(); /** * Pauses playback of the current song. + * * @return A confirmation of whether the command was successful or not. */ QueryFuture pause(); /** * Toggles between playing and pausing the current song. + * * @return A confirmation of whether the command was successful or not. */ QueryFuture togglePlayback(); + /** + * Stops playback of the current song. + */ + QueryFuture stopSong(); + /** * Sets the current song time to the specified position. * @@ -61,11 +94,10 @@ public interface Player QueryFuture seek(float time); /** - * Checks to see if the song is paused. - * - * @return Whether or not the song is paused. + * Obtains the player's current playback status. + * @return A future for the request. */ - QueryFuture isPaused(); + QueryFuture getStatus(); /** * Obtains the time we are currently at in the current song. @@ -83,7 +115,30 @@ public interface Player /** * Closes the player. + * * @return A confirmation of whether the player was successfully closed. */ QueryFuture close(); + + /** + * Adds a listener for playback status updates. + * + * @param listener - The listener to add. + */ + void addPlaybackListener(PlaybackListener listener); + + /** + * Checks to see if a listener has been added. + * + * @param listener - The listener to look for. + * @return True if the provided listener has been added, false otherwise. + */ + boolean hasPlaybackListener(PlaybackListener listener); + + /** + * Removes a listener for playback status updates. + * + * @param listener - The listener to remove. + */ + void removePlaybackListener(PlaybackListener listener); } diff --git a/interface/src/main/java/edu/regis/universeplayer/browser/InternetSong.java b/interface/src/main/java/edu/regis/universeplayer/browser/InternetSong.java new file mode 100644 index 0000000..2f8d280 --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/browser/InternetSong.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browser; + +import edu.regis.universeplayer.data.Song; + +import java.net.URL; + +public class InternetSong extends Song +{ + public URL location; +} diff --git a/interface/src/main/java/edu/regis/universeplayer/localPlayer/Player.java b/interface/src/main/java/edu/regis/universeplayer/localPlayer/LocalPlayer.java similarity index 50% rename from interface/src/main/java/edu/regis/universeplayer/localPlayer/Player.java rename to interface/src/main/java/edu/regis/universeplayer/localPlayer/LocalPlayer.java index 3f9b151..79e116f 100644 --- a/interface/src/main/java/edu/regis/universeplayer/localPlayer/Player.java +++ b/interface/src/main/java/edu/regis/universeplayer/localPlayer/LocalPlayer.java @@ -4,6 +4,13 @@ package edu.regis.universeplayer.localPlayer; import com.intervigil.wave.WaveReader; +import edu.regis.universeplayer.PlaybackListener; +import edu.regis.universeplayer.PlaybackStatus; +import edu.regis.universeplayer.Player; +import edu.regis.universeplayer.browserCommands.CommandConfirmation; +import edu.regis.universeplayer.browserCommands.QueryFuture; +import edu.regis.universeplayer.data.LocalSong; +import edu.regis.universeplayer.data.Song; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,6 +20,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.LinkedList; +import java.util.concurrent.*; /** * This allows for the control of playback of files on the local file system @@ -20,19 +28,22 @@ import java.util.LinkedList; * @author William Hubbard * @version 0.1 */ -public class Player +public class LocalPlayer implements Player { - private static final Logger logger = LoggerFactory.getLogger(Player.class); - + private static final Logger logger = LoggerFactory.getLogger(LocalPlayer.class); + static { System.loadLibrary("player"); } - + private int currentId; - + + private LocalSong currentSong; private AudioFile currentFile; - + + private final ExecutorService service = Executors.newSingleThreadExecutor(); + /** * Sets the file currently being used. * @@ -46,25 +57,25 @@ public class Player this.currentId = this.setCurrentFile(this.currentFile, (short) header.getChannels(), (short) header .getPcmFormat(), header.getSampleRate()); } - + /** * Updates the file currently being played. * - * @param stream - A reference to the audio data stream. - * @param numChannels - The number of audio channels contained in the file. + * @param stream - A reference to the audio data stream. + * @param numChannels - The number of audio channels contained in the file. * @param bitsPerSample - The number if bits in every sample. - * @param sampleRate - How many samples need to play every second. + * @param sampleRate - How many samples need to play every second. * @return An ID for the current song. */ private native int setCurrentFile(InputStream stream, short numChannels, short bitsPerSample, int sampleRate); - + /** * Saves the current file to * * @param file - The file to save to. */ public native void save(String file); - + /** * Saves the current file as a WAVE file somewhere else. This method is meant for testing only. * @@ -88,7 +99,7 @@ public class Player logger.error("Could not save WAVE file", e); } } - + /** * Obtains the current file. * @@ -98,24 +109,127 @@ public class Player { return this.currentFile; } - + + @Override + public Song getCurrentSong() + { + return this.currentSong; + } + + @Override + public QueryFuture loadSong(LocalSong song) + { + if (this.currentSong == null) + { + this.stopSong(); + } + this.currentSong = song; + Future runnable = service.submit(() -> { + try + { + AudioFile stream = getAudioStream(song.file); + this.setCurrentFile(stream); + } + catch (IOException e) + { + logger.error("Could not load local file " + song.file.getAbsolutePath(), e); + throw new RuntimeException(e); + } + return null; + }); + + return new WrappedFuture<>(runnable); + } + + @Override + public QueryFuture play() + { + return null; + } + + @Override + public QueryFuture pause() + { + return null; + } + + @Override + public QueryFuture togglePlayback() + { + return null; + } + + @Override + public QueryFuture stopSong() + { + return null; + } + + @Override + public void addPlaybackListener(PlaybackListener listener) + { + + } + + @Override + public QueryFuture seek(float time) + { + return null; + } + + @Override + public QueryFuture getStatus() + { + return null; + } + + @Override + public QueryFuture getCurrentTime() + { + return null; + } + + @Override + public QueryFuture getLength() + { + return null; + } + + @Override + public QueryFuture close() + { + return null; + } + /** * Plays the current audio file. */ - public native void play(); - + public native void playSong(); + /** * Pauses playback for the current audio file. */ - public native void pause(); - + public native void pauseSong(); + /** * Checks to see whether the current audio file is paused. * * @return True if there is an audio file present and it is paused, false otherwise. */ - public native boolean isPaused(); - + public native boolean isSongPaused(); + + @Override + public boolean hasPlaybackListener(PlaybackListener listener) + { + return false; + } + + @Override + public void removePlaybackListener(PlaybackListener listener) + { + + } + /** * Obtains an input stream for the requested file. * @@ -128,7 +242,7 @@ public class Player { return new AudioFile(convertFile(file)); } - + /** * Converts any audio file to a stream containing WAV audio file data (courtesy of FFMPEG). * @@ -156,7 +270,7 @@ public class Player args.add("pipe:1"); return Runtime.getRuntime().exec(args.toArray(new String[args.size()])); } - + public static void main(String[] args) { byte[] buffer = new byte[200]; @@ -164,7 +278,7 @@ public class Player File file = new File(args[0]); try (AudioFile stream = getAudioStream(new File(args[0]))) { - Player player = new Player(); + LocalPlayer player = new LocalPlayer(); player.setCurrentFile(stream); player.save(args[0] + ".wav"); } @@ -175,7 +289,7 @@ public class Player // Compare JNI vs Java try (AudioFile stream = getAudioStream(new File(args[0]))) { - Player player = new Player(); + LocalPlayer player = new LocalPlayer(); player.setCurrentFile(stream); player.saveJava(args[0] + ".orig.wav"); } @@ -184,4 +298,72 @@ public class Player logger.error("Could not save audio file " + file, e); } } + + private static class WrappedFuture implements QueryFuture + { + private final Future runnable; + + WrappedFuture(Future runnable) + { + this.runnable = runnable; + } + + @Override + public CommandConfirmation getConfirmation() throws CancellationException, ExecutionException, InterruptedException + { + try + { + this.runnable.get(); + return new CommandConfirmation(); + } + catch (ExecutionException e) + { + return new CommandConfirmation(e); + } + } + + @Override + public CommandConfirmation getConfirmation(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException + { + try + { + this.runnable.get(timeout, unit); + return new CommandConfirmation(); + } + catch (ExecutionException e) + { + return new CommandConfirmation(e); + } + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) + { + return this.runnable.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() + { + return this.runnable.isCancelled(); + } + + @Override + public boolean isDone() + { + return this.runnable.isDone(); + } + + @Override + public T get() throws InterruptedException, ExecutionException + { + return this.runnable.get(); + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException + { + return this.runnable.get(timeout, unit); + } + } } diff --git a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java index 2e036b8..7c43486 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java @@ -345,7 +345,9 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL } case PAUSE -> { } - case NEXT -> Queue.getInstance().skipNext(); + case NEXT -> { + + } case PREVIOUS -> Queue.getInstance().skipPrev(); case SEEK -> { } diff --git a/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java b/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java index c75bc62..02d5cd2 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java @@ -4,10 +4,20 @@ package edu.regis.universeplayer.player; +import edu.regis.universeplayer.PlaybackListener; +import edu.regis.universeplayer.PlaybackStatus; +import edu.regis.universeplayer.Player; +import edu.regis.universeplayer.data.Queue; +import edu.regis.universeplayer.data.Song; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.awt.*; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.util.LinkedList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; import javax.swing.*; @@ -17,19 +27,30 @@ import javax.swing.*; * @author William Hubbard * @version 0.1 */ -public class PlayerControls extends JPanel +public class PlayerControls extends JPanel implements Queue.SongChangeListener, PlaybackListener { + private static final Logger logger = LoggerFactory.getLogger(PlayerControls.class); + private final ImageIcon PLAY_ICON, PAUSE_ICON; + + /** + * A reference to the song currently playing. + */ + private Song currentSong = null; + private Player currentPlayer = null; + private final JButton playButton; private final JButton nextButton; private final JButton prevButton; private final JSlider progress; private final JProgressBar updateProgress; - + + private final ForkJoinPool service = new ForkJoinPool(); + /** * A list of all things interested in knowing when we trigger a command. */ private final LinkedList listeners = new LinkedList<>(); - + public PlayerControls() { final Dimension BUTTON_SIZE = new Dimension(32, 32); @@ -38,54 +59,56 @@ public class PlayerControls extends JPanel JPanel buttonCont, progressCont; FlowLayout buttonLayout; SpringLayout progressLayout; - + SpringLayout layout = new SpringLayout(); this.setLayout(layout); this.setFocusable(true); this.setFocusCycleRoot(false); - + buttonLayout = new FlowLayout(); buttonCont = new JPanel(buttonLayout); this.add(buttonCont); - + this.prevButton = new JButton(); icon = new ImageIcon(this.getClass() - .getResource("/gui/icons/skipPrev.png"), "Previous Button"); + .getResource("/gui/icons/skipPrev.png"), "Previous Button"); icon.setImage(icon.getImage().getScaledInstance(ICON_SIZE.width, ICON_SIZE.height, 0)); this.prevButton.setIcon(icon); this.prevButton.setPreferredSize(BUTTON_SIZE); - this.prevButton.addActionListener(actionEvent -> this.triggerCommandListeners(PlaybackCommand.PREVIOUS, null)); + this.prevButton.addActionListener(actionEvent -> this.previousSong()); buttonCont.add(this.prevButton); - + this.playButton = new JButton(); - icon = new ImageIcon(this.getClass().getResource("/gui/icons/play.png"), "Play Button"); + PAUSE_ICON = icon = new ImageIcon(this.getClass().getResource("/gui/icons/pause.png"), "Pause Button"); + icon.setImage(icon.getImage().getScaledInstance(ICON_SIZE.width, ICON_SIZE.height, 0)); + PLAY_ICON = icon = new ImageIcon(this.getClass().getResource("/gui/icons/play.png"), "Play Button"); icon.setImage(icon.getImage().getScaledInstance(ICON_SIZE.width, ICON_SIZE.height, 0)); this.playButton.setIcon(icon); this.playButton.setPreferredSize(BUTTON_SIZE); - this.playButton.addActionListener(actionEvent -> this.triggerCommandListeners(PlaybackCommand.PLAY, null)); + this.playButton.addActionListener(actionEvent -> this.togglePlayback()); buttonCont.add(this.playButton); - + this.nextButton = new JButton(); icon = new ImageIcon(this.getClass().getResource("/gui/icons/skipNext.png"), "Next Button"); icon.setImage(icon.getImage().getScaledInstance(ICON_SIZE.width, ICON_SIZE.height, 0)); this.nextButton.setIcon(icon); this.nextButton.setPreferredSize(BUTTON_SIZE); - this.nextButton.addActionListener(actionEvent -> this.triggerCommandListeners(PlaybackCommand.NEXT, null)); + this.nextButton.addActionListener(actionEvent -> this.nextSong()); buttonCont.add(this.nextButton); - + progressLayout = new SpringLayout(); progressCont = new JPanel(progressLayout); this.add(progressCont); - + this.progress = new JSlider(); - this.progress.addChangeListener(changeEvent -> this.triggerCommandListeners(PlaybackCommand.SEEK, this.progress.getValue())); + this.progress.addChangeListener(changeEvent -> this.seek(((JSlider) changeEvent.getSource()).getValue())); this.add(this.progress); - + this.updateProgress = new JProgressBar(); this.updateProgress.setStringPainted(true); this.setUpdateProgress(0, 0, null); this.add(this.updateProgress); - + this.addFocusListener(new FocusAdapter() { @Override @@ -110,7 +133,7 @@ public class PlayerControls extends JPanel } } }); - + layout.putConstraint(SpringLayout.NORTH, buttonCont, 0, SpringLayout.NORTH, this); layout.putConstraint(SpringLayout.WEST, buttonCont, 0, SpringLayout.WEST, this); layout.putConstraint(SpringLayout.EAST, buttonCont, 0, SpringLayout.EAST, this); @@ -119,9 +142,82 @@ public class PlayerControls extends JPanel layout.putConstraint(SpringLayout.EAST, this.progress, 5, SpringLayout.EAST, this); layout.putConstraint(SpringLayout.NORTH, this.updateProgress, 5, SpringLayout.SOUTH, this.progress); layout.putConstraint(SpringLayout.WEST, this.updateProgress, 5, SpringLayout.WEST, this); - + layout.putConstraint(SpringLayout.EAST, this, 5, SpringLayout.EAST, this.updateProgress); layout.putConstraint(SpringLayout.SOUTH, this, 5, SpringLayout.SOUTH, this.updateProgress); + + Queue.getInstance().addSongChangeListener(this); + } + + private void seek(int value) + { + this.service.execute(() -> { + if (this.currentPlayer != null) + { + this.currentPlayer.seek(value); + } + }); + this.triggerCommandListeners(PlaybackCommand.SEEK, value); + } + + /** + * Toggles the playback of the current song. + */ + private void togglePlayback() + { + this.service.execute(() -> { + try + { + if (this.currentPlayer != null) + { + switch ((PlaybackStatus) this.currentPlayer.getStatus().get()) + { + case PAUSED -> { + this.currentPlayer.play(); + } + case PLAYING -> { + this.currentPlayer.pause(); + } + case STOPPED, EMPTY -> { + if (Queue.getInstance().size() > 0) + { + if (Queue.getInstance().getCurrentSong() == null) + { + Queue.getInstance().skipToSong(0); + } + else + { + this.currentPlayer.play(); + } + } + } + } + } + } + catch (ExecutionException | InterruptedException e) + { + logger.error("Could not get current playback status", e); + } + }); + this.triggerCommandListeners(PlaybackCommand.PLAY, null); + } + + /** + * Skips to the next song. + */ + private void previousSong() + { + Queue.getInstance().skipPrev(); + this.triggerCommandListeners(PlaybackCommand.PREVIOUS, null); + } + + /** + * Skips to the next song. + */ + private void nextSong() + { + Queue.getInstance().skipNext(); + this.triggerCommandListeners(PlaybackCommand.NEXT, null); } void setUpdateProgress(int updated, int toUpdate, String updating) @@ -148,7 +244,7 @@ public class PlayerControls extends JPanel } } } - + /** * Adds a listener for playback commands. * @@ -158,7 +254,7 @@ public class PlayerControls extends JPanel { this.listeners.add(listener); } - + /** * Removes a playback listener. * @@ -168,7 +264,7 @@ public class PlayerControls extends JPanel { this.listeners.remove(listener); } - + /** * Triggers all the command listeners. * @@ -182,4 +278,51 @@ public class PlayerControls extends JPanel listener.onCommand(command, data); } } + + @Override + public void onSongChange(Queue queue) + { + if (this.currentSong != null) + { + this.currentPlayer.stopSong(); + } + this.currentSong = queue.getCurrentSong(); + this.playButton.setIcon(PLAY_ICON); + if (this.currentSong != null) + { + this.currentPlayer = Player.REGISTERED_PLAYERS.get(this.currentSong.getClass()); + if (!this.currentPlayer.hasPlaybackListener(this)) + { + this.currentPlayer.addPlaybackListener(this); + } + this.progress.setMaximum((int) (this.currentSong.duration / 1000)); + if (this.currentPlayer != null) + { + this.currentPlayer.play(); + } + else + { + logger.error("No logger found for song {}", this.currentSong.getClass()); + } + } + else + { + this.currentPlayer = null; + this.progress.setMaximum(0); + } + } + + @Override + public void onPlaybackChanged(PlaybackInfo status) + { + if (status.getSource() != null && status.getSource() == this.currentPlayer) + { + switch (status.getStatus()) + { + case PLAYING -> this.playButton.setIcon(PAUSE_ICON); + case PAUSED, STOPPED, EMPTY -> this.playButton.setIcon(PLAY_ICON); + } + this.progress.setValue((int) status.getPlayTime()); + } + } } diff --git a/player/build.gradle b/player/build.gradle index ca8fe63..5a7f413 100644 --- a/player/build.gradle +++ b/player/build.gradle @@ -16,6 +16,9 @@ library { def compileTask = binary.compileTask.get() compileTask.includes.from("${Jvm.current().javaHome}/include") + compileTask.source.from fileTree(dir: "src/main/c", include: "interface.h") + compileTask.source.from fileTree(dir: "src/main/c", include: "interface.c") + compileTask.source.from fileTree(dir: "src/main/c", include: "player.h") compileTask.source.from fileTree(dir: "src/main/c", include: "player.c") def osFamily = binary.targetPlatform.targetMachine.operatingSystemFamily @@ -28,6 +31,7 @@ library { } else if (osFamily.windows) { compileTask.includes.from("${Jvm.current().javaHome}/include/win32") compileTask.source.from fileTree(dir: "src/main/c", include: "player_win.c") + compileTask.source.from fileTree(dir: "src/main/c", include: "windows_mutex.c") } def toolChain = binary.toolChain diff --git a/player/src/main/c/interface.c b/player/src/main/c/interface.c new file mode 100644 index 0000000..500df5c --- /dev/null +++ b/player/src/main/c/interface.c @@ -0,0 +1,259 @@ +#include "interface.h" + +/** + * This function is used to initialize the static reference to java.io.InputStream class. + * + * @param env - A reference to the JVM. + * @param audio - An InputStream instance + * @return A reference to the InputStream class. + */ +jclass getInputStreamClass(JNIEnv *env) +{ + if (InputStream == 0 ) + { + jclass tempClass = (*env)->FindClass(env, "java/io/InputStream"); + if (tempClass == 0) + { + fprintf(stderr, "Could not find class java/io/InputStream"); + return 0; + } + InputStream = (*env)->NewGlobalRef(env, tempClass); + } + return InputStream; +} + +/** + * This function is used to initialize the static reference to the AudioStream class. + * + * @param env - A reference to the JVM. + * @param audio - An AudioStream instance + * @return A reference to the AudioStream class. + */ +jclass getAudioStreamClass(JNIEnv *env, jobject audio) +{ + if (AudioStream == 0 ) + { + jclass tempClass = (*env)->GetObjectClass(env, audio); + if (tempClass == 0) + { + fprintf(stderr, "Could not find class edu/regis/universeplayer_localPlayer/AudioFile"); + return 0; + } + AudioStream = (*env)->NewGlobalRef(env, tempClass); + } + return AudioStream; +} + +jclass getHeaderClass(JNIEnv *env) +{ + if (Header == 0) + { + jclass tempClass = (*env)->FindClass(env, "wave/WavHeader"); + if (tempClass == 0) + { + fprintf(stderr, "Could not find wave/WavHeader"); + return 0; + } + Header = (*env)->NewGlobalRef(env, tempClass); + } + return Header; +} + +/** + * This function is used to initialize the static reference to AudioStream's getHeader method. + * + * @param env - A reference to the JVM. + * @param audio - An AudioStream instance + * @return A reference to the getHeader method. + */ +jmethodID getHeaderMethod(JNIEnv *env, jobject audio) +{ + if (AudioStream_getHeader == 0 ) + { + jclass tempClass = getAudioStreamClass(env, audio); + if (tempClass == 0) + { + return 0; + } + AudioStream_getHeader = (*env)->GetMethodID(env, tempClass, "getHeader", "()Lwave/WavHeader;"); + } + return AudioStream_getHeader; +} + +/** + * This function is used to initialize the static reference to AudioStream's getByteStream method. + * + * @param env - A reference to the JVM. + * @param audio - An AudioStream instance + * @return A reference to the getByteStream method. + */ +jmethodID getByteStreamMethod(JNIEnv *env, jobject audio) +{ + if (AudioStream_getByteStream == 0 ) + { + jclass tempClass = getAudioStreamClass(env, audio); + if (tempClass == 0) + { + return 0; + } + AudioStream_getByteStream = (*env)->GetMethodID(env, tempClass, "getByteStream", "()[B"); + } + return AudioStream_getByteStream; +} + +/** + * This function is used to initialize the static references to AudioStream's read methods. + * + * @param env - A reference to the JVM. + * @param audio - An AudioStream instance + * @return A reference to the getByteStream method. + */ +jmethodID getReadMethod(JNIEnv *env, jobject audio) +{ + jclass tempClass; + if (AudioStream_readInt == 0) + { + tempClass = getAudioStreamClass(env, audio); + if (tempClass == 0) + { + fprintf(stderr, "Could not find AudioStream class"); + } + else + { + AudioStream_readInt = (*env)->GetMethodID(env, tempClass, "read", "()I"); + printf("Found class AudioStream"); + } + } + if (InputStream_readBuffer == 0 ) + { + tempClass = getInputStreamClass(env); + if (tempClass == 0) + { + fprintf(stderr, "Could not find InputStream class"); + } + else + { + InputStream_readBuffer = (*env)->GetMethodID(env, tempClass, "read", "([B)I"); + printf("Found class java/io/InputStream"); + } + } + return AudioStream_readInt; +} + +/** + * This function is used to initialize the static reference to WavHeader's various methods. + * + * @param env - A reference to the JVM. + */ +void getHeaderMethods(JNIEnv *env) +{ + jclass tempClass = getHeaderClass(env); + if (tempClass == 0) + { + return; + } + if (Header_getChunkId == 0) + { + Header_getChunkId = (*env)->GetMethodID(env, tempClass, "getChunkID", "()[B"); + if (Header_getChunkId == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getChunkID\n"); + } + } + if (Header_getChunkSize == 0) + { + Header_getChunkSize = (*env)->GetMethodID(env, tempClass, "getChunkSize", "()I"); + if (Header_getChunkSize == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getChunkSize\n"); + } + } + if (Header_getFormat == 0) + { + Header_getFormat = (*env)->GetMethodID(env, tempClass, "getFormat", "()[B"); + if (Header_getFormat == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getFormat\n"); + } + } + if (Header_getSubChunk1ID == 0) + { + Header_getSubChunk1ID = (*env)->GetMethodID(env, tempClass, "getSubChunk1ID", "()[B"); + if (Header_getSubChunk1ID == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getSubChunk1ID\n"); + } + } + if (Header_getSubChunk1Size == 0) + { + Header_getSubChunk1Size = (*env)->GetMethodID(env, tempClass, "getSubChunk1Size", "()I"); + if (Header_getSubChunk1Size == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getSubChunk1Size\n"); + } + } + if (Header_getAudioFormat == 0) + { + Header_getAudioFormat = (*env)->GetMethodID(env, tempClass, "getAudioFormat", "()S"); + if (Header_getAudioFormat == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getAudioFormat\n"); + } + } + if (Header_getNumChannels == 0) + { + Header_getNumChannels = (*env)->GetMethodID(env, tempClass, "getNumChannels", "()S"); + if (Header_getNumChannels == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getNumChannels\n"); + } + } + if (Header_getSampleRate == 0) + { + Header_getSampleRate = (*env)->GetMethodID(env, tempClass, "getSampleRate", "()I"); + if (Header_getSampleRate == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getSampleRate\n"); + } + } + if (Header_getByteRate == 0) + { + Header_getByteRate = (*env)->GetMethodID(env, tempClass, "getByteRate", "()I"); + if (Header_getByteRate == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getByteRate\n"); + } + } + if (Header_getBlockAlign == 0) + { + Header_getBlockAlign = (*env)->GetMethodID(env, tempClass, "getBlockAlign", "()S"); + if (Header_getBlockAlign == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getBlockAlign\n"); + } + } + if (Header_getBitsPerSample == 0) + { + Header_getBitsPerSample = (*env)->GetMethodID(env, tempClass, "getBitsPerSample", "()S"); + if (Header_getBitsPerSample == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getBitsPerSample\n"); + } + } + if (Header_getSubChunk2ID == 0) + { + Header_getSubChunk2ID = (*env)->GetMethodID(env, tempClass, "getSubChunk2ID", "()[B"); + if (Header_getSubChunk2ID == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getSubChunk2ID\n"); + } + } + if (Header_getSubChunk2Size == 0) + { + Header_getSubChunk2Size = (*env)->GetMethodID(env, tempClass, "getSubChunk2Size", "()I"); + if (Header_getSubChunk2Size == 0) + { + fprintf(stderr, "Could not find wave/WavHeader#getSubChunk2Size\n"); + } + } +} \ No newline at end of file diff --git a/player/src/main/c/interface.h b/player/src/main/c/interface.h new file mode 100644 index 0000000..5062915 --- /dev/null +++ b/player/src/main/c/interface.h @@ -0,0 +1,88 @@ +#ifndef INTERFACE_H +#define INTERFACE_H + +#include +#include + +typedef struct { + short numChannels; + short bytesPerChannel; + int sampleRate; +} HeaderData; + +static jclass InputStream; +static jclass AudioStream; +static jclass Header; +static jmethodID AudioStream_getHeader; +static jmethodID AudioStream_getByteStream; +static jmethodID AudioStream_readInt; +static jmethodID InputStream_readBuffer; +static jmethodID Header_getChunkId; +static jmethodID Header_getChunkSize; +static jmethodID Header_getFormat; +static jmethodID Header_getSubChunk1ID; +static jmethodID Header_getSubChunk1Size; +static jmethodID Header_getAudioFormat; +static jmethodID Header_getNumChannels; +static jmethodID Header_getSampleRate; +static jmethodID Header_getByteRate; +static jmethodID Header_getBlockAlign; +static jmethodID Header_getBitsPerSample; +static jmethodID Header_getSubChunk2ID; +static jmethodID Header_getSubChunk2Size; + +/** + * This function is used to initialize the static reference to java.io.InputStream class. + * + * @param env - A reference to the JVM. + * @param audio - An InputStream instance + * @return A reference to the InputStream class. + */ +jclass getInputStreamClass(JNIEnv *env); + +/** + * This function is used to initialize the static reference to the AudioStream class. + * + * @param env - A reference to the JVM. + * @param audio - An AudioStream instance + * @return A reference to the AudioStream class. + */ +jclass getAudioStreamClass(JNIEnv *env, jobject audio); + +jclass getHeaderClass(JNIEnv *env); + +/** + * This function is used to initialize the static reference to AudioStream's getHeader method. + * + * @param env - A reference to the JVM. + * @param audio - An AudioStream instance + * @return A reference to the getHeader method. + */ +jmethodID getHeaderMethod(JNIEnv *env, jobject audio); + +/** + * This function is used to initialize the static reference to AudioStream's getByteStream method. + * + * @param env - A reference to the JVM. + * @param audio - An AudioStream instance + * @return A reference to the getByteStream method. + */ +jmethodID getByteStreamMethod(JNIEnv *env, jobject audio); + +/** + * This function is used to initialize the static references to AudioStream's read methods. + * + * @param env - A reference to the JVM. + * @param audio - An AudioStream instance + * @return A reference to the getByteStream method. + */ +jmethodID getReadMethod(JNIEnv *env, jobject audio); + +/** + * This function is used to initialize the static reference to WavHeader's various methods. + * + * @param env - A reference to the JVM. + */ +void getHeaderMethods(JNIEnv *env); + +#endif /* INTERFACE_H */ \ No newline at end of file diff --git a/player/src/main/c/player.c b/player/src/main/c/player.c index 2539f8e..a3b77a5 100644 --- a/player/src/main/c/player.c +++ b/player/src/main/c/player.c @@ -1,297 +1,20 @@ /* * Copyright (c) 2021 William Hubbard. All Rights Reserved. */ -#include -#include +#include "interface.h" +#include "player.h" -typedef struct { - short numChannels; - short bytesPerChannel; - int sampleRate; -} HeaderData; - -static jclass InputStream; -static jclass AudioStream; -static jclass Header; -static jmethodID AudioStream_getHeader; -static jmethodID AudioStream_getByteStream; -static jmethodID AudioStream_readInt; -static jmethodID InputStream_readBuffer; -static jmethodID Header_getChunkId; -static jmethodID Header_getChunkSize; -static jmethodID Header_getFormat; -static jmethodID Header_getSubChunk1ID; -static jmethodID Header_getSubChunk1Size; -static jmethodID Header_getAudioFormat; -static jmethodID Header_getNumChannels; -static jmethodID Header_getSampleRate; -static jmethodID Header_getByteRate; -static jmethodID Header_getBlockAlign; -static jmethodID Header_getBitsPerSample; -static jmethodID Header_getSubChunk2ID; -static jmethodID Header_getSubChunk2Size; +play_item_t play_list_head = { + 0, /* play_id */ + SA_CLEAR, /* stop_flag */ + NULL, /* prev_item */ + NULL, /* next_item */ + NULL /* mutex */ +}; static jobject currentFile = NULL; static HeaderData currentHeader; -/** - * This function is used to initialize the static reference to java.io.InputStream class. - * - * @param env - A reference to the JVM. - * @param audio - An InputStream instance - * @return A reference to the InputStream class. - */ -jclass getInputStreamClass(JNIEnv *env) -{ - if (InputStream == 0 ) - { - jclass tempClass = (*env)->FindClass(env, "java/io/InputStream"); - if (tempClass == 0) - { - fprintf(stderr, "Could not find class java/io/InputStream"); - return 0; - } - InputStream = (*env)->NewGlobalRef(env, tempClass); - } - return InputStream; -} - -/** - * This function is used to initialize the static reference to the AudioStream class. - * - * @param env - A reference to the JVM. - * @param audio - An AudioStream instance - * @return A reference to the AudioStream class. - */ -jclass getAudioStreamClass(JNIEnv *env, jobject audio) -{ - if (AudioStream == 0 ) - { - jclass tempClass = (*env)->GetObjectClass(env, audio); - if (tempClass == 0) - { - fprintf(stderr, "Could not find class edu/regis/universeplayer_localPlayer/AudioFile"); - return 0; - } - AudioStream = (*env)->NewGlobalRef(env, tempClass); - } - return AudioStream; -} - -jclass getHeaderClass(JNIEnv *env) -{ - if (Header == 0) - { - jclass tempClass = (*env)->FindClass(env, "wave/WavHeader"); - if (tempClass == 0) - { - fprintf(stderr, "Could not find wave/WavHeader"); - return 0; - } - Header = (*env)->NewGlobalRef(env, tempClass); - } - return Header; -} - -/** - * This function is used to initialize the static reference to AudioStream's getHeader method. - * - * @param env - A reference to the JVM. - * @param audio - An AudioStream instance - * @return A reference to the getHeader method. - */ -jmethodID getHeaderMethod(JNIEnv *env, jobject audio) -{ - if (AudioStream_getHeader == 0 ) - { - jclass tempClass = getAudioStreamClass(env, audio); - if (tempClass == 0) - { - return 0; - } - AudioStream_getHeader = (*env)->GetMethodID(env, tempClass, "getHeader", "()Lwave/WavHeader;"); - } - return AudioStream_getHeader; -} - -/** - * This function is used to initialize the static reference to AudioStream's getByteStream method. - * - * @param env - A reference to the JVM. - * @param audio - An AudioStream instance - * @return A reference to the getByteStream method. - */ -jmethodID getByteStreamMethod(JNIEnv *env, jobject audio) -{ - if (AudioStream_getByteStream == 0 ) - { - jclass tempClass = getAudioStreamClass(env, audio); - if (tempClass == 0) - { - return 0; - } - AudioStream_getByteStream = (*env)->GetMethodID(env, tempClass, "getByteStream", "()[B"); - } - return AudioStream_getByteStream; -} - -/** - * This function is used to initialize the static references to AudioStream's read methods. - * - * @param env - A reference to the JVM. - * @param audio - An AudioStream instance - * @return A reference to the getByteStream method. - */ -jmethodID getReadMethod(JNIEnv *env, jobject audio) -{ - jclass tempClass; - if (AudioStream_readInt == 0) - { - tempClass = getAudioStreamClass(env, audio); - if (tempClass == 0) - { - fprintf(stderr, "Could not find AudioStream class"); - } - else - { - AudioStream_readInt = (*env)->GetMethodID(env, tempClass, "read", "()I"); - printf("Found class AudioStream"); - } - } - if (InputStream_readBuffer == 0 ) - { - tempClass = getInputStreamClass(env); - if (tempClass == 0) - { - fprintf(stderr, "Could not find InputStream class"); - } - else - { - InputStream_readBuffer = (*env)->GetMethodID(env, tempClass, "read", "([B)I"); - printf("Found class java/io/InputStream"); - } - } - return AudioStream_readInt; -} - -/** - * This function is used to initialize the static reference to WavHeader's various methods. - * - * @param env - A reference to the JVM. - */ -void getHeaderMethods(JNIEnv *env) -{ - jclass tempClass = getHeaderClass(env); - if (tempClass == 0) - { - return; - } - if (Header_getChunkId == 0) - { - Header_getChunkId = (*env)->GetMethodID(env, tempClass, "getChunkID", "()[B"); - if (Header_getChunkId == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getChunkID\n"); - } - } - if (Header_getChunkSize == 0) - { - Header_getChunkSize = (*env)->GetMethodID(env, tempClass, "getChunkSize", "()I"); - if (Header_getChunkSize == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getChunkSize\n"); - } - } - if (Header_getFormat == 0) - { - Header_getFormat = (*env)->GetMethodID(env, tempClass, "getFormat", "()[B"); - if (Header_getFormat == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getFormat\n"); - } - } - if (Header_getSubChunk1ID == 0) - { - Header_getSubChunk1ID = (*env)->GetMethodID(env, tempClass, "getSubChunk1ID", "()[B"); - if (Header_getSubChunk1ID == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getSubChunk1ID\n"); - } - } - if (Header_getSubChunk1Size == 0) - { - Header_getSubChunk1Size = (*env)->GetMethodID(env, tempClass, "getSubChunk1Size", "()I"); - if (Header_getSubChunk1Size == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getSubChunk1Size\n"); - } - } - if (Header_getAudioFormat == 0) - { - Header_getAudioFormat = (*env)->GetMethodID(env, tempClass, "getAudioFormat", "()S"); - if (Header_getAudioFormat == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getAudioFormat\n"); - } - } - if (Header_getNumChannels == 0) - { - Header_getNumChannels = (*env)->GetMethodID(env, tempClass, "getNumChannels", "()S"); - if (Header_getNumChannels == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getNumChannels\n"); - } - } - if (Header_getSampleRate == 0) - { - Header_getSampleRate = (*env)->GetMethodID(env, tempClass, "getSampleRate", "()I"); - if (Header_getSampleRate == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getSampleRate\n"); - } - } - if (Header_getByteRate == 0) - { - Header_getByteRate = (*env)->GetMethodID(env, tempClass, "getByteRate", "()I"); - if (Header_getByteRate == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getByteRate\n"); - } - } - if (Header_getBlockAlign == 0) - { - Header_getBlockAlign = (*env)->GetMethodID(env, tempClass, "getBlockAlign", "()S"); - if (Header_getBlockAlign == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getBlockAlign\n"); - } - } - if (Header_getBitsPerSample == 0) - { - Header_getBitsPerSample = (*env)->GetMethodID(env, tempClass, "getBitsPerSample", "()S"); - if (Header_getBitsPerSample == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getBitsPerSample\n"); - } - } - if (Header_getSubChunk2ID == 0) - { - Header_getSubChunk2ID = (*env)->GetMethodID(env, tempClass, "getSubChunk2ID", "()[B"); - if (Header_getSubChunk2ID == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getSubChunk2ID\n"); - } - } - if (Header_getSubChunk2Size == 0) - { - Header_getSubChunk2Size = (*env)->GetMethodID(env, tempClass, "getSubChunk2Size", "()I"); - if (Header_getSubChunk2Size == 0) - { - fprintf(stderr, "Could not find wave/WavHeader#getSubChunk2Size\n"); - } - } -} - /** * Obtains an audio stream's header. * @param env - A reference to the JVM. @@ -409,7 +132,7 @@ HeaderData readHeader(JNIEnv *env, jobject header, HeaderData *headerStruct) /** * Sets the file currently being used */ -JNIEXPORT jint JNICALL Java_edu_regis_universeplayer_localPlayer_Player_setCurrentFile(JNIEnv *env, jobject obj, jobject stream, jshort numChannels, jshort bitsPerSample, jint sampleRate) +JNIEXPORT jint JNICALL Java_edu_regis_universeplayer_localPlayer_LocalPlayer_setCurrentFile(JNIEnv *env, jobject obj, jobject stream, jshort numChannels, jshort bitsPerSample, jint sampleRate) { jobject header; if (currentFile != NULL) @@ -423,7 +146,7 @@ JNIEXPORT jint JNICALL Java_edu_regis_universeplayer_localPlayer_Player_setCurre return updatePlayer(env, obj, stream, numChannels, bitsPerSample, sampleRate); } -JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_save(JNIEnv *env, jobject obj, jstring location) +JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_LocalPlayer_save(JNIEnv *env, jobject obj, jstring location) { const int BUFFER_SIZE = 256; /* @@ -471,19 +194,19 @@ JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_save(JNI return; } -JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_play(JNIEnv *env, jobject obj) +JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_LocalPlayer_playSong(JNIEnv *env, jobject obj) { printf("Playing\n"); return; } -JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_pause(JNIEnv *env, jobject obj) +JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_LocalPlayer_pauseSong(JNIEnv *env, jobject obj) { printf("Pausing\n"); return; } -JNIEXPORT jboolean JNICALL Java_edu_regis_universeplayer_localPlayer_Player_isPaused(JNIEnv *env, jobject obj) +JNIEXPORT jboolean JNICALL Java_edu_regis_universeplayer_localPlayer_LocalPlayer_isSongPaused(JNIEnv *env, jobject obj) { printf("Is Paused?\n"); return JNI_FALSE; diff --git a/player/src/main/c/player.h b/player/src/main/c/player.h new file mode 100644 index 0000000..7c27f1e --- /dev/null +++ b/player/src/main/c/player.h @@ -0,0 +1,65 @@ +/* +Simpleaudio Python Extension +Copyright (C) 2015, Joe Hamilton +MIT License (see LICENSE.txt) +*/ + +#ifndef SIMPLEAUDIO_H +#define SIMPLEAUDIO_H + +#include +#include + +#define SA_ERR_STR_LEN (256) + +#define SA_CLEAR (0) +#define SA_STOP (1) + +enum { + NOT_LAST_ITEM = 0, + LAST_ITEM = 1 +}; + +typedef unsigned long long play_id_t; + +/* linked list structure used to track the active playback items/threads */ +typedef struct play_item_s { + /* the play_id of the list head is used to store the next play_id value + used by a new play list item */ + play_id_t play_id; + int stop_flag; + struct play_item_s* prev_item; + struct play_item_s* next_item; + /* the mutex of the list head is used as a 'global' mutex for modifying + and accessing the list itself */ + void* mutex; +} play_item_t; + +typedef struct { + jobject buffer_obj; + void* handle; + int used_bytes; + int len_bytes; + int num_buffers; + int frame_size; + int buffer_size; + play_item_t* play_list_item; + void* list_mutex; +} audio_blob_t; + +jint* play_os(jobject buffer_obj, int len_samples, int num_channels, int bytes_per_chan, int sample_rate, play_item_t* play_list_head, int latency_us); + +void delete_list_item(play_item_t* play_item); +play_item_t* new_list_item(play_item_t* list_head); + +void destroy_audio_blob(audio_blob_t* audio_blob); +audio_blob_t* create_audio_blob(void); + +int get_buffer_size(int latency_us, int sample_rate, int frame_size); + +void* create_mutex(void); +void destroy_mutex(void* mutex); +void grab_mutex(void* mutex); +void release_mutex(void* mutex); + +#endif /* SIMPLEAUDIO_H */ \ No newline at end of file diff --git a/player/src/main/c/player_win.c b/player/src/main/c/player_win.c index 5b5bedb..df0ab3c 100644 --- a/player/src/main/c/player_win.c +++ b/player/src/main/c/player_win.c @@ -1,11 +1,112 @@ /* - * Copyright (c) 2021 William Hubbard. All Rights Reserved. - */ -#include -#include +Simpleaudio Python Extension +Copyright (C) 2015, Joe Hamilton +MIT License (see LICENSE.txt) +*/ +#include "simpleaudio.h" +#include +#include +#include int updatePlayer(JNIEnv *env, jobject obj, jobject stream, jshort numChannels, jshort bitsPerSample, jint sampleRate) { printf("Updating File\n"); return 0; +} + +jint play_os(jobject buffer_obj, int len_samples, int num_channels, int bytes_per_chan, int sample_rate, play_item_t* play_list_head, int latency_us) +{ + char err_msg_buf[SA_ERR_STR_LEN]; + char sys_msg_buf[SA_ERR_STR_LEN / 2]; + audio_blob_t* audio_blob; + + WAVEFORMATEX audio_format; + MMRESULT result; + HANDLE thread_handle = NULL; + DWORD thread_id; + int bytes_per_frame = bytes_per_chan * num_channels; + WAVEHDR* temp_wave_hdr; + int buffer_size; + int i; + + DBG_PLAY_OS_CALL + + buffer_size = get_buffer_size(latency_us / NUM_BUFS, sample_rate, bytes_per_chan * num_channels); + + audio_blob = create_audio_blob(); + audio_blob->buffer_obj = buffer_obj; + audio_blob->list_mutex = play_list_head->mutex; + audio_blob->len_bytes = len_samples * bytes_per_frame; + audio_blob->num_buffers = NUM_BUFS; + + /* setup the linked list item for this playback buffer */ + grab_mutex(play_list_head->mutex); + audio_blob->play_list_item = new_list_item(play_list_head); + release_mutex(play_list_head->mutex); + + /* windows audio device and format headers setup */ + if (bytes_per_chan < 4) { + audio_format.wFormatTag = WAVE_FORMAT_PCM; + } else { + audio_format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; + } + audio_format.nChannels = num_channels; + + audio_format.nSamplesPerSec = sample_rate; + audio_format.nBlockAlign = bytes_per_frame; + /* per MSDN WAVEFORMATEX documentation */ + audio_format.nAvgBytesPerSec = audio_format.nSamplesPerSec * audio_format.nBlockAlign; + audio_format.wBitsPerSample = bytes_per_chan * 8; + audio_format.cbSize = 0; + + /* create the cleanup thread so we can return after calling waveOutWrite + SEE :http://msdn.microsoft.com/en-us/library/windows/desktop/ms682516(v=vs.85).aspx + */ + thread_handle = CreateThread(NULL, 0, bufferThread, audio_blob, 0, &thread_id); + if (thread_handle != NULL) { + /* Close so we don't leak handles - similar to detatched POSIX threads */ + CloseHandle(thread_handle); + } else { + DWORD lastError = GetLastError(); + /* lang code : US En */ + FormatMessage((FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS), NULL, lastError, 0x0409, sys_msg_buf, SYS_STR_LEN, NULL); + WIN_EXCEPTION("Failed to start cleanup thread.", 0, sys_msg_buf, err_msg_buf); + + destroy_audio_blob(audio_blob); + return NULL; + } + + /* open a handle to the default audio device */ + result = waveOutOpen((HWAVEOUT*)&audio_blob->handle, WAVE_MAPPER, &audio_format, thread_id, 0, CALLBACK_THREAD); + if (result != MMSYSERR_NOERROR) { + waveOutGetErrorText(result, sys_msg_buf, SYS_STR_LEN); + WIN_EXCEPTION("Failed to open audio device.", result, sys_msg_buf, err_msg_buf); + + PostThreadMessage(thread_id, WM_QUIT, 0, 0); + destroy_audio_blob(audio_blob); + return NULL; + } + + dbg1("allocating %d buffers of %d bytes\n", NUM_BUFS, buffer_size); + + for (i = 0; i < NUM_BUFS; i++) { + temp_wave_hdr = PyMem_Malloc(sizeof(WAVEHDR)); + memset(temp_wave_hdr, 0, sizeof(WAVEHDR)); + temp_wave_hdr->lpData = PyMem_Malloc(buffer_size); + temp_wave_hdr->dwBufferLength = buffer_size; + + result = fill_buffer(temp_wave_hdr, audio_blob); + if (result != MMSYSERR_NOERROR) { + waveOutGetErrorText(result, sys_msg_buf, SYS_STR_LEN); + WIN_EXCEPTION("Failed to buffer audio.", result, sys_msg_buf, err_msg_buf); + + PostThreadMessage(thread_id, WM_QUIT, 0, 0); + waveOutUnprepareHeader(audio_blob->handle, temp_wave_hdr, sizeof(WAVEHDR)); + waveOutClose(audio_blob->handle); + destroy_audio_blob(audio_blob); + return NULL; + } + } + + return PyLong_FromUnsignedLongLong(audio_blob->play_list_item->play_id); } \ No newline at end of file diff --git a/player/src/main/c/windows_mutex.c b/player/src/main/c/windows_mutex.c new file mode 100644 index 0000000..7822ca3 --- /dev/null +++ b/player/src/main/c/windows_mutex.c @@ -0,0 +1,27 @@ +/* +Simpleaudio Python Extension +Copyright (C) 2015, Joe Hamilton +MIT License (see LICENSE.txt) +*/ + +#include "player.h" +#include +#include + +void* create_mutex() { + void* mutex; + mutex = (void*)CreateMutex(NULL, FALSE, NULL); + return mutex; +} + +void destroy_mutex(void* mutex) { + CloseHandle((HANDLE)mutex); +} + +void grab_mutex(void* mutex) { + WaitForSingleObject((HANDLE)mutex, INFINITE); +} + +void release_mutex(void* mutex) { + ReleaseMutex((HANDLE)mutex); +} \ No newline at end of file