Adds basic support for native APIs for a local song player.
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,3 +4,8 @@
|
|||||||
/*/build/
|
/*/build/
|
||||||
/local.properties
|
/local.properties
|
||||||
/browser/profile/extensions/universal_music@regis.edu.xpi
|
/browser/profile/extensions/universal_music@regis.edu.xpi
|
||||||
|
/browser/profile/cache2/*
|
||||||
|
/browser/profile/datareporting/*
|
||||||
|
/browser/profile/startupCache/*
|
||||||
|
/browser/profile/sessionstore-backups/*
|
||||||
|
/browser/profile/lock
|
||||||
1
WavReader/README.md
Normal file
1
WavReader/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
These files were originally posted by Andrii Tkachenko on GitHub in 2016. They have been refactored to function in a Gradle environment.
|
||||||
11
WavReader/build.gradle
Normal file
11
WavReader/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
apply plugin: "java"
|
||||||
|
apply plugin: "application"
|
||||||
|
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
targetCompatibility = 1.8
|
||||||
|
|
||||||
|
mainClassName = "MetaReader"
|
||||||
28
WavReader/src/main/java/MetaReader.java
Normal file
28
WavReader/src/main/java/MetaReader.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import wave.WavHeader;
|
||||||
|
import wave.WavHeaderReader;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Console utility for reading meta data of the wave files
|
||||||
|
*/
|
||||||
|
public class MetaReader {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
System.out.println("Illegal command line arguments.\n" +
|
||||||
|
"Usage MetaReader\n\t MetaReader <source file path>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WavHeader wavHeader;
|
||||||
|
try {
|
||||||
|
WavHeaderReader wavHeaderReader = new WavHeaderReader(args[0]);
|
||||||
|
wavHeader = wavHeaderReader.read();
|
||||||
|
System.out.println(wavHeader.toString());
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
System.out.println("Error: File " + args[0] + " not found!");
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println("Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
WavReader/src/main/java/wave/WavHeader.java
Normal file
150
WavReader/src/main/java/wave/WavHeader.java
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2016. Andrii Tkachenko. https://github.com/tkaczenko/WavReader
|
||||||
|
*/
|
||||||
|
|
||||||
|
package wave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains a structure of WAVE file.
|
||||||
|
*
|
||||||
|
* @see <a href =
|
||||||
|
* "https://github.com/tkacz-/WavReader/blob/master/wav-sound-format.gif">
|
||||||
|
* Structure of wave format
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public class WavHeader {
|
||||||
|
private byte[] chunkID = new byte[4];
|
||||||
|
private int chunkSize;
|
||||||
|
private byte[] format = new byte[4];
|
||||||
|
private byte[] subChunk1ID = new byte[4];
|
||||||
|
private int subChunk1Size;
|
||||||
|
private short audioFormat;
|
||||||
|
private short numChannels;
|
||||||
|
private int sampleRate;
|
||||||
|
private int byteRate;
|
||||||
|
private short blockAlign;
|
||||||
|
private short bitsPerSample;
|
||||||
|
private byte[] subChunk2ID = new byte[4];
|
||||||
|
private int subChunk2Size;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "The RIFF chunk desriptor: " + new String(this.getChunkID()) + "\n" +
|
||||||
|
"Size of this chunk: " + this.getChunkSize() + "\n" +
|
||||||
|
"Format: " + new String(this.getFormat()) + "\n" + "\n" +
|
||||||
|
"fmt subchunk: " + new String(this.getSubChunk1ID()) + "\n" +
|
||||||
|
"Size of this chunk: " + this.getSubChunk1Size() + "\n" +
|
||||||
|
"Audio format: " + this.getAudioFormat() + "\n" +
|
||||||
|
"Number of channels: " + this.getNumChannels() + "\n" +
|
||||||
|
"Sample rate: " + this.getSampleRate() + "\n" +
|
||||||
|
"Byte rate: " + this.getByteRate() + "\n" +
|
||||||
|
"Block align: " + this.getBlockAlign() + "\n" +
|
||||||
|
"Bits per sample: " + this.getBitsPerSample() + "\n" + "\n" +
|
||||||
|
"data subchunk: " + new String(this.getSubChunk2ID()) + "\n" +
|
||||||
|
"Size of this chunk: " + this.getSubChunk2Size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getChunkID() {
|
||||||
|
return chunkID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChunkID(byte[] chunkID) {
|
||||||
|
this.chunkID = chunkID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getChunkSize() {
|
||||||
|
return chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChunkSize(int chunkSize) {
|
||||||
|
this.chunkSize = chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getFormat() {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFormat(byte[] format) {
|
||||||
|
this.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSubChunk1ID() {
|
||||||
|
return subChunk1ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubChunk1ID(byte[] subChunk1ID) {
|
||||||
|
this.subChunk1ID = subChunk1ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSubChunk1Size() {
|
||||||
|
return subChunk1Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubChunk1Size(int subChunk1Size) {
|
||||||
|
this.subChunk1Size = subChunk1Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getAudioFormat() {
|
||||||
|
return audioFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioFormat(short audioFormat) {
|
||||||
|
this.audioFormat = audioFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getNumChannels() {
|
||||||
|
return numChannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNumChannels(short numChannels) {
|
||||||
|
this.numChannels = numChannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSampleRate() {
|
||||||
|
return sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSampleRate(int sampleRate) {
|
||||||
|
this.sampleRate = sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getByteRate() {
|
||||||
|
return byteRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setByteRate(int byteRate) {
|
||||||
|
this.byteRate = byteRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getBlockAlign() {
|
||||||
|
return blockAlign;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlockAlign(short blockAlign) {
|
||||||
|
this.blockAlign = blockAlign;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short getBitsPerSample() {
|
||||||
|
return bitsPerSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBitsPerSample(short bitsPerSample) {
|
||||||
|
this.bitsPerSample = bitsPerSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSubChunk2ID() {
|
||||||
|
return subChunk2ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubChunk2ID(byte[] subChunk2ID) {
|
||||||
|
this.subChunk2ID = subChunk2ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSubChunk2Size() {
|
||||||
|
return subChunk2Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubChunk2Size(int subChunk2Size) {
|
||||||
|
this.subChunk2Size = subChunk2Size;
|
||||||
|
}
|
||||||
|
}
|
||||||
159
WavReader/src/main/java/wave/WavHeaderReader.java
Normal file
159
WavReader/src/main/java/wave/WavHeaderReader.java
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2016. Andrii Tkachenko. https://github.com/tkaczenko/WavReader
|
||||||
|
*/
|
||||||
|
|
||||||
|
package wave;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom wave header reader which use class
|
||||||
|
*
|
||||||
|
* @see WavHeader
|
||||||
|
*/
|
||||||
|
public class WavHeaderReader {
|
||||||
|
/**
|
||||||
|
* Size for the wave header
|
||||||
|
*/
|
||||||
|
private static final int HEADER_SIZE = 44;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffer which contain bytes of the wave header
|
||||||
|
*/
|
||||||
|
private byte[] buf = new byte[HEADER_SIZE];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wave header
|
||||||
|
*/
|
||||||
|
private WavHeader header = new WavHeader();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InputStream to the wave file
|
||||||
|
*/
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty constructor
|
||||||
|
*/
|
||||||
|
public WavHeaderReader() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor which create own FileInputStream
|
||||||
|
*
|
||||||
|
* @param source absolute path to the file
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public WavHeaderReader(String source) throws IOException {
|
||||||
|
inputStream = new FileInputStream(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor which use created InputStream
|
||||||
|
*
|
||||||
|
* @param inputStream InputStream to the wave file
|
||||||
|
*/
|
||||||
|
public WavHeaderReader(InputStream inputStream) {
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read wave header of the file
|
||||||
|
*
|
||||||
|
* @return wave.WavHeader object
|
||||||
|
* @throws IOException
|
||||||
|
* @see WavHeader
|
||||||
|
*/
|
||||||
|
public WavHeader read() throws IOException {
|
||||||
|
int res = inputStream.read(buf);
|
||||||
|
if (res != HEADER_SIZE) {
|
||||||
|
throw new IOException("Could not read header.");
|
||||||
|
}
|
||||||
|
header.setChunkID(Arrays.copyOfRange(buf, 0, 4));
|
||||||
|
if (new String(header.getChunkID()).compareTo("RIFF") != 0) {
|
||||||
|
throw new IOException("Illegal format.");
|
||||||
|
}
|
||||||
|
header.setChunkSize(toInt(4, false));
|
||||||
|
header.setFormat(Arrays.copyOfRange(buf, 8, 12));
|
||||||
|
header.setSubChunk1ID(Arrays.copyOfRange(buf, 12, 16));
|
||||||
|
header.setSubChunk1Size(toInt(16, false));
|
||||||
|
header.setAudioFormat(toShort(20, false));
|
||||||
|
header.setNumChannels(toShort(22, false));
|
||||||
|
header.setSampleRate(toInt(24, false));
|
||||||
|
header.setByteRate(toInt(28, false));
|
||||||
|
header.setBlockAlign(toShort(32, false));
|
||||||
|
header.setBitsPerSample(toShort(34, false));
|
||||||
|
header.setSubChunk2ID(Arrays.copyOfRange(buf, 36, 40));
|
||||||
|
header.setSubChunk2Size(toInt(40, false));
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert byte[] array to int number
|
||||||
|
*
|
||||||
|
* @param start start position of the buffer
|
||||||
|
* @param endian <code>true</code> for big-endian
|
||||||
|
* <code>false</code> for little-endian
|
||||||
|
* @return converted number
|
||||||
|
*/
|
||||||
|
private int toInt(int start, boolean endian) {
|
||||||
|
int k = (endian) ? 1 : -1;
|
||||||
|
if (!endian) {
|
||||||
|
start += 3;
|
||||||
|
}
|
||||||
|
return (buf[start] << 24) + (buf[start + k * 1] << 16) +
|
||||||
|
(buf[start + k * 2] << 8) + buf[start + k * 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert byte[] array to short number
|
||||||
|
*
|
||||||
|
* @param start start position of the buffer
|
||||||
|
* @param endian <code>true</code> for big-endian
|
||||||
|
* <code>false</code> for little-endian
|
||||||
|
* @return converted number
|
||||||
|
*/
|
||||||
|
private short toShort(int start, boolean endian) {
|
||||||
|
short k = (endian) ? (short) 1 : -1;
|
||||||
|
if (!endian) {
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
return (short) ((buf[start] << 8) + (buf[start + k * 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the buffer which contain wave hader
|
||||||
|
*
|
||||||
|
* @return buffer
|
||||||
|
*/
|
||||||
|
public byte[] getBuf() {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return wave.WavHeader object which contain wave header
|
||||||
|
*
|
||||||
|
* @return wave.WavHeader object
|
||||||
|
*/
|
||||||
|
public WavHeader getHeader() {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return input stream to the wave file
|
||||||
|
*
|
||||||
|
* @return Input steam to the wave file
|
||||||
|
*/
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInputStream(InputStream inputStream) {
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ dependencies {
|
|||||||
// The production code uses the SLF4J logging API at compile time
|
// The production code uses the SLF4J logging API at compile time
|
||||||
implementation 'org.slf4j:slf4j-api:1.7.25'
|
implementation 'org.slf4j:slf4j-api:1.7.25'
|
||||||
implementation 'com.google.code.gson:gson:2.8.7'
|
implementation 'com.google.code.gson:gson:2.8.7'
|
||||||
|
implementation project(":WavReader")
|
||||||
|
|
||||||
// Declare the dependency for your favourite test framework you want to use in your tests.
|
// Declare the dependency for your favourite test framework you want to use in your tests.
|
||||||
// TestNG is also supported by the Gradle Test task. Just change the
|
// TestNG is also supported by the Gradle Test task. Just change the
|
||||||
@@ -27,5 +28,12 @@ dependencies {
|
|||||||
|
|
||||||
// Define the main class for the application
|
// Define the main class for the application
|
||||||
mainClassName = defaultPackage + '.player.Interface'
|
mainClassName = defaultPackage + '.player.Interface'
|
||||||
|
//mainClassName = defaultPackage + '.localPlayer.Player'
|
||||||
|
|
||||||
compileJava.dependsOn rootProject.bundleAddOn
|
compileJava.dependsOn rootProject.bundleAddOn
|
||||||
|
|
||||||
|
run {
|
||||||
|
systemProperty "java.library.path", file("${project(":player").buildDir}/lib/main/debug").absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
compileJava.dependsOn ':player:linkDebug'
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package edu.regis.universeplayer.localPlayer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import wave.WavHeader;
|
||||||
|
import wave.WavHeaderReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains information on a WAV audio file, along with a reference to the stream of raw data.
|
||||||
|
*
|
||||||
|
* @author William Hubbard
|
||||||
|
*/
|
||||||
|
public class AudioFile extends InputStream
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Contains header information.
|
||||||
|
*/
|
||||||
|
private final WavHeaderReader header;
|
||||||
|
/**
|
||||||
|
* Contains a link to the process reading the audio file.
|
||||||
|
*/
|
||||||
|
private final Process process;
|
||||||
|
/**
|
||||||
|
* The stream for the actual audio data.
|
||||||
|
*/
|
||||||
|
private InputStream stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an audio file from a stream
|
||||||
|
*
|
||||||
|
* @param stream - The stream to create it from.
|
||||||
|
* @throws IOException - Thrown when an error occurs creating the input stream.
|
||||||
|
*/
|
||||||
|
AudioFile(Process stream) throws IOException
|
||||||
|
{
|
||||||
|
this.process = stream;
|
||||||
|
this.stream = stream.getInputStream();
|
||||||
|
this.header = new WavHeaderReader(this.stream);
|
||||||
|
this.header.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WavHeader getHeader()
|
||||||
|
{
|
||||||
|
return this.header.getHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the byte string representation of this header.
|
||||||
|
* @return A byte string containing this file's header.
|
||||||
|
*/
|
||||||
|
public byte[] getByteStream()
|
||||||
|
{
|
||||||
|
return this.header.getBuf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException
|
||||||
|
{
|
||||||
|
return this.stream.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException
|
||||||
|
{
|
||||||
|
return this.process.isAlive() ? Integer.MAX_VALUE : super.available();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
package edu.regis.universeplayer.localPlayer;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
import wave.WavHeader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This allows for the control of playback of files on the local file system
|
||||||
|
*
|
||||||
|
* @author William Hubbard
|
||||||
|
* @version 0.1
|
||||||
|
*/
|
||||||
|
public class Player
|
||||||
|
{
|
||||||
|
static
|
||||||
|
{
|
||||||
|
System.loadLibrary("player");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int currentId;
|
||||||
|
|
||||||
|
private AudioFile currentFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the file currently being used.
|
||||||
|
*
|
||||||
|
* @param file - The current audio file.
|
||||||
|
*/
|
||||||
|
public void setCurrentFile(AudioFile file)
|
||||||
|
{
|
||||||
|
WavHeader header = file.getHeader();
|
||||||
|
this.currentFile = file;
|
||||||
|
// TODO - Ensure that bytes per sample and bits per sample don't bother things
|
||||||
|
this.currentId = this.setCurrentFile(this.currentFile, header.getNumChannels(), header
|
||||||
|
.getBitsPerSample(), 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 bitsPerSample - The number if bits in every sample.
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* @param file - The file to save to.
|
||||||
|
*/
|
||||||
|
public void saveJava(String file)
|
||||||
|
{
|
||||||
|
try (FileOutputStream out = new FileOutputStream(file))
|
||||||
|
{
|
||||||
|
out.write(this.getAudioFile().getByteStream());
|
||||||
|
byte[] buffer = new byte[256];
|
||||||
|
int read = this.getAudioFile().read(buffer);
|
||||||
|
while (read > 0)
|
||||||
|
{
|
||||||
|
out.write(buffer, 0, read);
|
||||||
|
read = this.getAudioFile().read(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the current file.
|
||||||
|
*
|
||||||
|
* @return The current audio file we are working with.
|
||||||
|
*/
|
||||||
|
public AudioFile getAudioFile()
|
||||||
|
{
|
||||||
|
return this.currentFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the current audio file.
|
||||||
|
*/
|
||||||
|
public native void play();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses playback for the current audio file.
|
||||||
|
*/
|
||||||
|
public native void pause();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains an input stream for the requested file.
|
||||||
|
*
|
||||||
|
* @param file - The file to read
|
||||||
|
* @return An raw stream for the file
|
||||||
|
* @throws FileNotFoundException - Thrown should the file not exist.
|
||||||
|
* @throws IOException - Thrown should an error occur when reading the file
|
||||||
|
*/
|
||||||
|
public static AudioFile getAudioStream(File file) throws IOException
|
||||||
|
{
|
||||||
|
return new AudioFile(convertFile(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts any audio file to a stream containing WAV audio file data (courtesy of FFMPEG).
|
||||||
|
*
|
||||||
|
* @param file - The file to convert
|
||||||
|
* @return An input stream containing the file data
|
||||||
|
* @throws FileNotFoundException - Thrown should the file not exist.
|
||||||
|
* @throws IOException - Thrown should an error occur when reading the file
|
||||||
|
*/
|
||||||
|
protected static Process convertFile(File file) throws FileNotFoundException, IOException
|
||||||
|
{
|
||||||
|
LinkedList<String> args = new LinkedList<>();
|
||||||
|
if (!file.isFile())
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Must provide a file");
|
||||||
|
}
|
||||||
|
args.add("ffmpeg");
|
||||||
|
args.add("-hide_banner");
|
||||||
|
args.add("-loglevel");
|
||||||
|
args.add("error");
|
||||||
|
args.add("-y");
|
||||||
|
args.add("-i");
|
||||||
|
args.add(file.getAbsolutePath());
|
||||||
|
args.add("-f");
|
||||||
|
args.add("wav");
|
||||||
|
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];
|
||||||
|
int len;
|
||||||
|
File file = new File(args[0]);
|
||||||
|
try (AudioFile stream = getAudioStream(new File(args[0])))
|
||||||
|
{
|
||||||
|
Player player = new Player();
|
||||||
|
player.setCurrentFile(stream);
|
||||||
|
player.save(args[0] + ".wav");
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
// Compare JNI vs Java
|
||||||
|
try (AudioFile stream = getAudioStream(new File(args[0])))
|
||||||
|
{
|
||||||
|
Player player = new Player();
|
||||||
|
player.setCurrentFile(stream);
|
||||||
|
player.saveJava(args[0] + ".orig.wav");
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
player/build.gradle
Normal file
37
player/build.gradle
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import org.gradle.internal.jvm.Jvm
|
||||||
|
|
||||||
|
/*
|
||||||
|
* There is currently no "C application" plugin, so this build uses the "C++ application" plugin and then reconfigures it
|
||||||
|
* to build C instead.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id 'cpp-library'
|
||||||
|
}
|
||||||
|
|
||||||
|
library {
|
||||||
|
binaries.configureEach { CppBinary binary ->
|
||||||
|
def compileTask = binary.compileTask.get()
|
||||||
|
compileTask.includes.from("${Jvm.current().javaHome}/include")
|
||||||
|
|
||||||
|
def osFamily = binary.targetPlatform.targetMachine.operatingSystemFamily
|
||||||
|
if (osFamily.macOs) {
|
||||||
|
compileTask.includes.from("${Jvm.current().javaHome}/include/darwin")
|
||||||
|
} else if (osFamily.linux) {
|
||||||
|
compileTask.includes.from("${Jvm.current().javaHome}/include/linux")
|
||||||
|
} else if (osFamily.windows) {
|
||||||
|
compileTask.includes.from("${Jvm.current().javaHome}/include/win32")
|
||||||
|
}
|
||||||
|
|
||||||
|
compileTask.source.from fileTree(dir: "src/main/c", include: "**/*.c")
|
||||||
|
|
||||||
|
def toolChain = binary.toolChain
|
||||||
|
if (toolChain instanceof VisualCpp) {
|
||||||
|
compileTask.compilerArgs.addAll(["/TC"])
|
||||||
|
} else if (toolChain instanceof GccCompatibleToolChain) {
|
||||||
|
compileTask.compilerArgs.addAll(["-x", "c", "-std=c11"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
311
player/src/main/c/player.c
Normal file
311
player/src/main/c/player.c
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
#include <jni.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static jclass InputStream;
|
||||||
|
static jclass AudioStream;
|
||||||
|
static jmethodID AudioStream_getHeader;
|
||||||
|
static jmethodID AudioStream_getByteStream;
|
||||||
|
static jmethodID AudioStream_readInt;
|
||||||
|
static jmethodID InputStream_readBuffer;
|
||||||
|
|
||||||
|
static jobject currentFile = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
printf("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)
|
||||||
|
{
|
||||||
|
printf("Could not find class edu/regis/universeplayer_localPlayer/AudioFile");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
AudioStream = (*env)->NewGlobalRef(env, tempClass);
|
||||||
|
}
|
||||||
|
return AudioStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
printf("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)
|
||||||
|
{
|
||||||
|
printf("Could not find InputStream class");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InputStream_readBuffer = (*env)->GetMethodID(env, tempClass, "read", "([B)I");
|
||||||
|
printf("Found class java/io/InputStream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AudioStream_readInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains an audio stream's header.
|
||||||
|
* @param env - A reference to the JVM.
|
||||||
|
* @param stream - The audio stream to obtain the header from
|
||||||
|
* @return The header information.
|
||||||
|
*/
|
||||||
|
jobject getHeader(JNIEnv *env, jobject stream)
|
||||||
|
{
|
||||||
|
if (AudioStream_getHeader == 0)
|
||||||
|
{
|
||||||
|
getHeaderMethod(env, stream);
|
||||||
|
if (AudioStream_getHeader == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobject returnValue = (*env)->CallObjectMethod(env, stream, AudioStream_getHeader);
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the original raw byte data for an audio stream's header.
|
||||||
|
* @param env - A reference to the JVM.
|
||||||
|
* @param stream - The audio stream to obtain the header from.
|
||||||
|
* @return The header information.
|
||||||
|
*/
|
||||||
|
jbyteArray getByteStream(JNIEnv *env, jobject stream)
|
||||||
|
{
|
||||||
|
if (AudioStream_getByteStream == 0)
|
||||||
|
{
|
||||||
|
getByteStreamMethod(env, stream);
|
||||||
|
if (AudioStream_getByteStream == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jbyteArray returnValue = (jbyteArray) (*env)->CallObjectMethod(env, stream, AudioStream_getByteStream);
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a single integer from the audio stream.
|
||||||
|
* @param env - A reference to the JVM.
|
||||||
|
* @param stream - The audio stream to obtain the header from.
|
||||||
|
* @return The integer read.
|
||||||
|
*/
|
||||||
|
int readInt(JNIEnv *env, jobject stream)
|
||||||
|
{
|
||||||
|
if (AudioStream_readInt == 0)
|
||||||
|
{
|
||||||
|
getReadMethod(env, stream);
|
||||||
|
if (AudioStream_readInt == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jint returnValue = (*env)->CallIntMethod(env, stream, AudioStream_readInt);
|
||||||
|
return (int) returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills a provided buffer with input from the audio stream.
|
||||||
|
* @param env - A reference to the JVM.
|
||||||
|
* @param stream - The audio stream to obtain the header from.
|
||||||
|
* @param buffer - The buffer to read information to
|
||||||
|
* @param bufferSize - The size of the buffer
|
||||||
|
* @return How much information was actually read.
|
||||||
|
*/
|
||||||
|
int readBuffer(JNIEnv *env, jobject stream, char* buffer, int bufferSize)
|
||||||
|
{
|
||||||
|
if (InputStream_readBuffer == 0)
|
||||||
|
{
|
||||||
|
getReadMethod(env, stream);
|
||||||
|
if (InputStream_readBuffer == 0)
|
||||||
|
{
|
||||||
|
printf("readBuffer method not found\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (AudioStream_readInt == 0)
|
||||||
|
{
|
||||||
|
printf("readInt method not found\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jbyteArray jbuffer = (*env)->NewByteArray(env, bufferSize);
|
||||||
|
jint returnValue = (*env)->CallIntMethod(env, stream, InputStream_readBuffer, jbuffer);
|
||||||
|
jbyte *jbufferEl = (*env)->GetByteArrayElements(env, jbuffer, 0);
|
||||||
|
for (int i = 0; i < returnValue; i++)
|
||||||
|
{
|
||||||
|
buffer[i] = jbufferEl[i];
|
||||||
|
}
|
||||||
|
(*env)->ReleaseByteArrayElements(env, jbuffer, jbufferEl, 0);
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
{
|
||||||
|
if (currentFile != NULL)
|
||||||
|
{
|
||||||
|
(*env)->DeleteLocalRef(env, currentFile);
|
||||||
|
}
|
||||||
|
currentFile = (*env)->NewGlobalRef(env, stream);
|
||||||
|
printf("Setting File\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_save(JNIEnv *env, jobject obj, jstring location)
|
||||||
|
{
|
||||||
|
int BUFFER_SIZE = 256;
|
||||||
|
char buffer[BUFFER_SIZE];
|
||||||
|
int read;
|
||||||
|
|
||||||
|
FILE *output;
|
||||||
|
|
||||||
|
char buf[128];
|
||||||
|
const char *locationPath = (*env)->GetStringUTFChars(env, location, 0);
|
||||||
|
output = fopen(locationPath, "w");
|
||||||
|
|
||||||
|
if (output == NULL)
|
||||||
|
{
|
||||||
|
printf("File %s can't be opened\n", locationPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtains header data
|
||||||
|
jbyteArray headerData = getByteStream(env, currentFile);
|
||||||
|
jsize headerLength = (*env)->GetArrayLength(env, headerData);
|
||||||
|
jbyte* header = (*env)->GetByteArrayElements(env, headerData, 0);
|
||||||
|
char headerBuff[headerLength];
|
||||||
|
for (int i = 0; i < headerLength; i++)
|
||||||
|
{
|
||||||
|
headerBuff[i] = header[i];
|
||||||
|
}
|
||||||
|
fwrite(headerBuff, headerLength, 1, output);
|
||||||
|
(*env)->ReleaseByteArrayElements(env, headerData, header, 0);
|
||||||
|
|
||||||
|
// Writes the actual file
|
||||||
|
read = readBuffer(env, currentFile, headerBuff, BUFFER_SIZE);
|
||||||
|
while (read > 0)
|
||||||
|
{
|
||||||
|
fwrite(headerBuff, read, 1, output);
|
||||||
|
read = readBuffer(env, currentFile, headerBuff, BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(output);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, location, locationPath);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_play(JNIEnv *env, jobject obj)
|
||||||
|
{
|
||||||
|
printf("Playing\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_pause(JNIEnv *env, jobject obj)
|
||||||
|
{
|
||||||
|
printf("Pausing\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_edu_regis_universeplayer_localPlayer_Player_isPaused(JNIEnv *env, jobject obj)
|
||||||
|
{
|
||||||
|
printf("Is Paused?\n");
|
||||||
|
return JNI_FALSE;
|
||||||
|
}
|
||||||
@@ -22,3 +22,5 @@ include 'services:webservice'
|
|||||||
rootProject.name = 'UniversalMusicPlayer'
|
rootProject.name = 'UniversalMusicPlayer'
|
||||||
include ':interface'
|
include ':interface'
|
||||||
include ':add-on'
|
include ':add-on'
|
||||||
|
include ':player'
|
||||||
|
include ':WavReader'
|
||||||
|
|||||||
Reference in New Issue
Block a user