diff --git a/WavReader/README.md b/WavReader/README.md deleted file mode 100644 index 6de5907..0000000 --- a/WavReader/README.md +++ /dev/null @@ -1 +0,0 @@ -These files were originally posted by Andrii Tkachenko on GitHub in 2016. They have been refactored to function in a Gradle environment. \ No newline at end of file diff --git a/WavReader/build.gradle b/WavReader/build.gradle deleted file mode 100644 index 9679f1a..0000000 --- a/WavReader/build.gradle +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) 2021 William Hubbard. All Rights Reserved. - */ - -apply plugin: "java" -apply plugin: "application" - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -mainClassName = "MetaReader" \ No newline at end of file diff --git a/WavReader/src/main/java/MetaReader.java b/WavReader/src/main/java/MetaReader.java deleted file mode 100644 index 11d107f..0000000 --- a/WavReader/src/main/java/MetaReader.java +++ /dev/null @@ -1,28 +0,0 @@ -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 "); - 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()); - } - } -} diff --git a/WavReader/src/main/java/wave/WavHeader.java b/WavReader/src/main/java/wave/WavHeader.java deleted file mode 100644 index e4657ac..0000000 --- a/WavReader/src/main/java/wave/WavHeader.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) 2016. Andrii Tkachenko. https://github.com/tkaczenko/WavReader - */ - -package wave; - -/** - * This class contains a structure of WAVE file. - * - * @see - * Structure of wave format - * - */ -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; - } -} \ No newline at end of file diff --git a/WavReader/src/main/java/wave/WavHeaderReader.java b/WavReader/src/main/java/wave/WavHeaderReader.java deleted file mode 100644 index 93813c7..0000000 --- a/WavReader/src/main/java/wave/WavHeaderReader.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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 true for big-endian - * false 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 true for big-endian - * false 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; - } - -} diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/interface/build.gradle b/interface/build.gradle index bbdb210..2800a56 100644 --- a/interface/build.gradle +++ b/interface/build.gradle @@ -17,7 +17,9 @@ dependencies { // The production code uses the SLF4J logging API at compile time implementation 'org.slf4j:slf4j-api:1.7.25' implementation 'com.google.code.gson:gson:2.8.7' - implementation project(":WavReader") + implementation 'com.googlecode.soundlibs:jlayer:1.0.1.4' + implementation 'com.mpatric:mp3agic:0.9.1' + implementation project(":libwave") // 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 diff --git a/interface/src/main/java/edu/regis/universeplayer/localPlayer/AudioFile.java b/interface/src/main/java/edu/regis/universeplayer/localPlayer/AudioFile.java index d8ddff7..0fd86f5 100644 --- a/interface/src/main/java/edu/regis/universeplayer/localPlayer/AudioFile.java +++ b/interface/src/main/java/edu/regis/universeplayer/localPlayer/AudioFile.java @@ -7,8 +7,7 @@ package edu.regis.universeplayer.localPlayer; import java.io.IOException; import java.io.InputStream; -import wave.WavHeader; -import wave.WavHeaderReader; +import com.intervigil.wave.WaveReader; /** * Contains information on a WAV audio file, along with a reference to the stream of raw data. @@ -20,7 +19,7 @@ public class AudioFile extends InputStream /** * Contains header information. */ - private final WavHeaderReader header; + private final WaveReader header; /** * Contains a link to the process reading the audio file. */ @@ -40,13 +39,13 @@ public class AudioFile extends InputStream { this.process = stream; this.stream = stream.getInputStream(); - this.header = new WavHeaderReader(this.stream); - this.header.read(); + this.header = new WaveReader(this.stream); + this.header.openWave(); } - public WavHeader getHeader() + public WaveReader getHeader() { - return this.header.getHeader(); + return this.header; } /** @@ -55,7 +54,11 @@ public class AudioFile extends InputStream */ public byte[] getByteStream() { - return this.header.getBuf(); + try { + return this.header.getHeaderBytes(); + } catch (IOException e) { + throw new RuntimeException("Could not recreate header"); + } } @Override @@ -69,4 +72,10 @@ public class AudioFile extends InputStream { return this.process.isAlive() ? Integer.MAX_VALUE : super.available(); } + + @Override + public void close() throws IOException { + super.close(); + this.header.closeWaveFile(); + } } diff --git a/interface/src/main/java/edu/regis/universeplayer/localPlayer/Player.java b/interface/src/main/java/edu/regis/universeplayer/localPlayer/Player.java index be263ae..65b6340 100644 --- a/interface/src/main/java/edu/regis/universeplayer/localPlayer/Player.java +++ b/interface/src/main/java/edu/regis/universeplayer/localPlayer/Player.java @@ -3,6 +3,8 @@ */ package edu.regis.universeplayer.localPlayer; +import com.intervigil.wave.WaveReader; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -10,8 +12,6 @@ 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 * @@ -36,11 +36,11 @@ public class Player */ public void setCurrentFile(AudioFile file) { - WavHeader header = file.getHeader(); + WaveReader 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()); + this.currentId = this.setCurrentFile(this.currentFile, (short) header.getChannels(), (short) header + .getPcmFormat(), header.getSampleRate()); } /** diff --git a/interface/src/main/resources/gui/icons/defaultart.png b/interface/src/main/resources/gui/icons/defaultart.png old mode 100755 new mode 100644 diff --git a/libwave/LICENSE b/libwave/LICENSE new file mode 100644 index 0000000..4da4391 --- /dev/null +++ b/libwave/LICENSE @@ -0,0 +1,17 @@ +libwave is licensed under LGPL + +Copyright (c) 2011 Ethan Chen + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/libwave/build.gradle b/libwave/build.gradle new file mode 100644 index 0000000..a4b05da --- /dev/null +++ b/libwave/build.gradle @@ -0,0 +1,21 @@ +buildscript { + repositories { + jcenter() + google() + } +} + +plugins { + id 'java-library' +} + +dependencies { +} + +sourceSets { + main { + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + } +} + diff --git a/libwave/src/com/intervigil/wave/WaveReader.java b/libwave/src/com/intervigil/wave/WaveReader.java new file mode 100644 index 0000000..9eb5aec --- /dev/null +++ b/libwave/src/com/intervigil/wave/WaveReader.java @@ -0,0 +1,344 @@ +/* WaveReader.java + + Copyright (c) 2011 Ethan Chen + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.intervigil.wave; + +import java.io.*; + +import com.intervigil.wave.exception.InvalidWaveException; + +public class WaveReader { + private static final int WAV_HEADER_CHUNK_ID = 0x52494646; // "RIFF" + private static final int WAV_FORMAT = 0x57415645; // "WAVE" + private static final int WAV_FORMAT_CHUNK_ID = 0x666d7420; // "fmt " + private static final int WAV_DATA_CHUNK_ID = 0x64617461; // "data" + private static final int STREAM_BUFFER_SIZE = 4096; + + private File mInFile; + private BufferedInputStream mInStream; + + private int mSampleRate; + private int mChannels; + private int mSampleBits; + private int mFileSize; + private int mDataSize; + + + /** + * Constructor; initializes WaveReader to read from given file + * + * @param path path to input file + * @param name name of input file + */ + public WaveReader(String path, String name) { + this.mInFile = new File(path + File.separator + name); + } + + /** + * Constructor; initializes WaveReader to read from given file + * + * @param file handle to input file + */ + public WaveReader(File file) { + this.mInFile = file; + } + + /** + * Constructor; initializes WaveReader to read from given file + * + * @param file handle to input file + * @author William Hubbard, 7/24/2021 + */ + public WaveReader(InputStream stream) { + this.mInFile = null; + this.mInStream = new BufferedInputStream(stream, STREAM_BUFFER_SIZE); + } + + /** + * Open WAV file for reading + * + * @throws FileNotFoundException if input file does not exist + * @throws InvalidWaveException if input file is not a valid WAVE file + * @throws IOException if I/O error occurred during file read + */ + public void openWave() throws FileNotFoundException, InvalidWaveException, IOException { + /* + * Only initializes the stream if needed ~ Change made by William Hubbard on 7/24/2021 + */ + if (this.mInStream == null) { + FileInputStream fileStream = new FileInputStream(mInFile); + mInStream = new BufferedInputStream(fileStream, STREAM_BUFFER_SIZE); + } + + int headerId = readUnsignedInt(mInStream); // should be "RIFF" + if (headerId != WAV_HEADER_CHUNK_ID) { + throw new InvalidWaveException(String.format("Invalid WAVE header chunk ID: %d", headerId)); + } + mFileSize = readUnsignedIntLE(mInStream); // length of header + int format = readUnsignedInt(mInStream); // should be "WAVE" + if (format != WAV_FORMAT) { + throw new InvalidWaveException("Invalid WAVE format"); + } + + int formatId = readUnsignedInt(mInStream); // should be "fmt " + if (formatId != WAV_FORMAT_CHUNK_ID) { + throw new InvalidWaveException("Invalid WAVE format chunk ID"); + } + int formatSize = readUnsignedIntLE(mInStream); + if (formatSize != 16) { + + } + int audioFormat = readUnsignedShortLE(mInStream); + if (audioFormat != 1) { + throw new InvalidWaveException("Not PCM WAVE format"); + } + mChannels = readUnsignedShortLE(mInStream); + mSampleRate = readUnsignedIntLE(mInStream); + int byteRate = readUnsignedIntLE(mInStream); + int blockAlign = readUnsignedShortLE(mInStream); + mSampleBits = readUnsignedShortLE(mInStream); + + int dataId = readUnsignedInt(mInStream); + if (dataId != WAV_DATA_CHUNK_ID) { + throw new InvalidWaveException("Invalid WAVE data chunk ID"); + } + mDataSize = readUnsignedIntLE(mInStream); + } + + /** + * Writes a short to a {@link ByteArrayOutputStream}. + * @param stream - The stream to write to. + * @param num - The number to write. + * @throws IOException + * @author William Hubbard, 7/24/2021 + * @author java.io.DataOutputStream#writeShort(int) + */ + private void writeShort(ByteArrayOutputStream stream, short num) throws IOException { + byte[] buf = new byte[2]; + buf[0] = (byte)(num >>> 8); + buf[1] = (byte)(num >>> 0); + stream.write(buf); + } + + /** + * Restructures the original wave header for the file as a byte array. + * @return The original wave header + * @throws IOException + * @author William Hubbard, 7/24/2021 + * @author Ethan Chen, com.intervigil.wave.WaveWriter#writeWaveHeader() + */ + public byte[] getHeaderBytes() throws IOException { + // rewind to beginning of the file + ByteArrayOutputStream stream = new ByteArrayOutputStream(44); + + int bytesPerSec = (mSampleBits + 7) / 8; + + stream.write("RIFF".getBytes()); // WAV chunk header + stream.write(Integer.reverseBytes(36)); // WAV chunk size + stream.write("WAVE".getBytes()); // WAV format + + stream.write("fmt ".getBytes()); // format subchunk header + stream.write(Integer.reverseBytes(16)); // format subchunk size + writeShort(stream, Short.reverseBytes((short) 1)); // audio format + writeShort(stream, Short.reverseBytes((short) mChannels)); // number of channels + stream.write(Integer.reverseBytes(mSampleRate)); // sample rate + stream.write(Integer.reverseBytes(mSampleRate * mChannels * bytesPerSec)); // byte rate + writeShort(stream, Short.reverseBytes((short) (mChannels * bytesPerSec))); // block align + writeShort(stream, Short.reverseBytes((short) mSampleBits)); // bits per sample + + stream.write("data".getBytes()); // data subchunk header + stream.write(Integer.reverseBytes(0)); // data subchunk size + + stream.close(); + + return stream.toByteArray(); + } + + /** + * Get sample rate + * + * @return input file's sample rate + */ + public int getSampleRate() { + return mSampleRate; + } + + /** + * Get number of channels + * + * @return number of channels in input file + */ + public int getChannels() { + return mChannels; + } + + /** + * Get PCM format, S16LE or S8LE + * + * @return number of bits per sample + */ + public int getPcmFormat() { + return mSampleBits; + } + + /** + * Get file size + * + * @return total input file size in bytes + */ + public int getFileSize() { + return mFileSize + 8; + } + + /** + * Get input file's audio data size + * Basically file size without headers included + * + * @return audio data size in bytes + */ + public int getDataSize() { + return mDataSize; + } + + /** + * Get input file length + * + * @return length of file in seconds + */ + public int getLength() { + if (mSampleRate == 0 || mChannels == 0 || (mSampleBits + 7) / 8 == 0) { + return 0; + } else { + return mDataSize / (mSampleRate * mChannels * ((mSampleBits + 7) / 8)); + } + } + + /** + * Read audio data from input file (mono) + * + * @param dst mono audio data output buffer + * @param numSamples number of samples to read + * + * @return number of samples read + * + * @throws IOException if file I/O error occurs + */ + public int read(short[] dst, int numSamples) throws IOException { + if (mChannels != 1) { + return -1; + } + + byte[] buf = new byte[numSamples * 2]; + int index = 0; + int bytesRead = mInStream.read(buf, 0, numSamples * 2); + + for (int i = 0; i < bytesRead; i+=2) { + dst[index] = byteToShortLE(buf[i], buf[i+1]); + index++; + } + + return index; + } + + /** + * Read audio data from input file (stereo) + * + * @param left left channel audio output buffer + * @param right right channel audio output buffer + * @param numSamples number of samples to read + * + * @return number of samples read + * + * @throws IOException if file I/O error occurs + */ + public int read(short[] left, short[] right, int numSamples) throws IOException { + if (mChannels != 2) { + return -1; + } + byte[] buf = new byte[numSamples * 4]; + int index = 0; + int bytesRead = mInStream.read(buf, 0, numSamples * 4); + + for (int i = 0; i < bytesRead; i+=2) { + short val = byteToShortLE(buf[0], buf[i+1]); + if (i % 4 == 0) { + left[index] = val; + } else { + right[index] = val; + index++; + } + } + + return index; + } + + /** + * Close WAV file. WaveReader object cannot be used again following this call. + * + * @throws IOException if I/O error occurred closing filestream + */ + public void closeWaveFile() throws IOException { + if (mInStream != null) { + mInStream.close(); + } + } + + private static short byteToShortLE(byte b1, byte b2) { + return (short) (b1 & 0xFF | ((b2 & 0xFF) << 8)); + } + + private static int readUnsignedInt(BufferedInputStream in) throws IOException { + int ret; + byte[] buf = new byte[4]; + ret = in.read(buf); + if (ret == -1) { + return -1; + } else { + return (((buf[0] & 0xFF) << 24) + | ((buf[1] & 0xFF) << 16) + | ((buf[2] & 0xFF) << 8) + | (buf[3] & 0xFF)); + } + } + + private static int readUnsignedIntLE(BufferedInputStream in) throws IOException { + int ret; + byte[] buf = new byte[4]; + ret = in.read(buf); + if (ret == -1) { + return -1; + } else { + return (buf[0] & 0xFF + | ((buf[1] & 0xFF) << 8) + | ((buf[2] & 0xFF) << 16) + | ((buf[3] & 0xFF) << 24)); + } + } + + private static short readUnsignedShortLE(BufferedInputStream in) throws IOException { + int ret; + byte[] buf = new byte[2]; + ret = in.read(buf, 0, 2); + if (ret == -1) { + return -1; + } else { + return byteToShortLE(buf[0], buf[1]); + } + } +} \ No newline at end of file diff --git a/libwave/src/com/intervigil/wave/WaveWriter.java b/libwave/src/com/intervigil/wave/WaveWriter.java new file mode 100644 index 0000000..2478c6a --- /dev/null +++ b/libwave/src/com/intervigil/wave/WaveWriter.java @@ -0,0 +1,196 @@ +/* WaveWriter.java + + Copyright (c) 2011 Ethan Chen + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.intervigil.wave; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; + +public class WaveWriter { + private static final int OUTPUT_STREAM_BUFFER = 16384; + + private File mOutFile; + private BufferedOutputStream mOutStream; + + private int mSampleRate; + private int mChannels; + private int mSampleBits; + + private int mBytesWritten; + + + /** + * Constructor; initializes WaveWriter with file name and path + * + * @param path output file path + * @param name output file name + * @param sampleRate output sample rate + * @param channels number of channels + * @param sampleBits number of bits per sample (S8LE, S16LE) + */ + public WaveWriter(String path, String name, int sampleRate, int channels, + int sampleBits) { + this.mOutFile = new File(path + File.separator + name); + + this.mSampleRate = sampleRate; + this.mChannels = channels; + this.mSampleBits = sampleBits; + + this.mBytesWritten = 0; + } + + /** + * Constructor; initializes WaveWriter with file name and path + * + * @param file output file handle + * @param sampleRate output sample rate + * @param channels number of channels + * @param sampleBits number of bits per sample (S8LE, S16LE) + */ + public WaveWriter(File file, int sampleRate, int channels, int sampleBits) { + this.mOutFile = file; + + this.mSampleRate = sampleRate; + this.mChannels = channels; + this.mSampleBits = sampleBits; + + this.mBytesWritten = 0; + } + + /** + * Create output WAV file + * + * @return whether file creation succeeded + * + * @throws IOException if file I/O error occurs allocating header + */ + public boolean createWaveFile() throws IOException { + if (mOutFile.exists()) { + mOutFile.delete(); + } + + if (mOutFile.createNewFile()) { + FileOutputStream fileStream = new FileOutputStream(mOutFile); + mOutStream = new BufferedOutputStream(fileStream, OUTPUT_STREAM_BUFFER); + // write 44 bytes of space for the header + mOutStream.write(new byte[44]); + return true; + } + return false; + } + + /** + * Write audio data to output file (mono). Does + * nothing if output file is not mono channel. + * + * @param src mono audio data input buffer + * @param offset offset into src buffer + * @param length buffer size in number of samples + * + * @throws IOException if file I/O error occurs + */ + public void write(short[] src, int offset, int length) throws IOException { + if (mChannels != 1) { + return; + } + if (offset > length) { + throw new IndexOutOfBoundsException(String.format("offset %d is greater than length %d", offset, length)); + } + for (int i = offset; i < length; i++) { + writeUnsignedShortLE(mOutStream, src[i]); + mBytesWritten += 2; + } + } + + /** + * Write audio data to output file (stereo). Does + * nothing if output file is not stereo channel. + * + * @param left left channel audio data buffer + * @param right right channel audio data buffer + * @param offset offset into left/right buffers + * @param length buffer size in number of samples + * + * @throws IOException if file I/O error occurs + */ + public void write(short[] left, short[] right, int offset, int length) throws IOException { + if (mChannels != 2) { + return; + } + if (offset > length) { + throw new IndexOutOfBoundsException(String.format("offset %d is greater than length %d", offset, length)); + } + for (int i = offset; i < length; i++) { + writeUnsignedShortLE(mOutStream, left[i]); + writeUnsignedShortLE(mOutStream, right[i]); + mBytesWritten += 4; + } + } + + /** + * Close output WAV file and write WAV header. WaveWriter + * cannot be used again following this call. + * + * @throws IOException if file I/O error occurs writing WAV header + */ + public void closeWaveFile() throws IOException { + if (mOutStream != null) { + this.mOutStream.flush(); + this.mOutStream.close(); + } + writeWaveHeader(); + } + + private void writeWaveHeader() throws IOException { + // rewind to beginning of the file + RandomAccessFile file = new RandomAccessFile(this.mOutFile, "rw"); + file.seek(0); + + int bytesPerSec = (mSampleBits + 7) / 8; + + file.writeBytes("RIFF"); // WAV chunk header + file.writeInt(Integer.reverseBytes(mBytesWritten + 36)); // WAV chunk size + file.writeBytes("WAVE"); // WAV format + + file.writeBytes("fmt "); // format subchunk header + file.writeInt(Integer.reverseBytes(16)); // format subchunk size + file.writeShort(Short.reverseBytes((short) 1)); // audio format + file.writeShort(Short.reverseBytes((short) mChannels)); // number of channels + file.writeInt(Integer.reverseBytes(mSampleRate)); // sample rate + file.writeInt(Integer.reverseBytes(mSampleRate * mChannels * bytesPerSec)); // byte rate + file.writeShort(Short.reverseBytes((short) (mChannels * bytesPerSec))); // block align + file.writeShort(Short.reverseBytes((short) mSampleBits)); // bits per sample + + file.writeBytes("data"); // data subchunk header + file.writeInt(Integer.reverseBytes(mBytesWritten)); // data subchunk size + + file.close(); + file = null; + } + + private static void writeUnsignedShortLE(BufferedOutputStream stream, short sample) + throws IOException { + // write already writes the lower order byte of this short + stream.write(sample); + stream.write((sample >> 8)); + } +} diff --git a/libwave/src/com/intervigil/wave/exception/InvalidWaveException.java b/libwave/src/com/intervigil/wave/exception/InvalidWaveException.java new file mode 100644 index 0000000..333d70f --- /dev/null +++ b/libwave/src/com/intervigil/wave/exception/InvalidWaveException.java @@ -0,0 +1,38 @@ +/* InvalidWaveException.java + + Copyright (c) 2011 Ethan Chen + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.intervigil.wave.exception; + +import java.io.IOException; + +public class InvalidWaveException extends IOException { + + /** + * Generated serialVersionUID + */ + private static final long serialVersionUID = -8229742633848759378L; + + public InvalidWaveException() { + + } + + public InvalidWaveException(String msg) { + super(msg); + } +} diff --git a/player/build.gradle b/player/build.gradle index d9cb4c3..ca8fe63 100644 --- a/player/build.gradle +++ b/player/build.gradle @@ -16,17 +16,20 @@ library { def compileTask = binary.compileTask.get() compileTask.includes.from("${Jvm.current().javaHome}/include") + compileTask.source.from fileTree(dir: "src/main/c", include: "player.c") + def osFamily = binary.targetPlatform.targetMachine.operatingSystemFamily if (osFamily.macOs) { compileTask.includes.from("${Jvm.current().javaHome}/include/darwin") + compileTask.source.from fileTree(dir: "src/main/c", include: "player_mac.c") } else if (osFamily.linux) { compileTask.includes.from("${Jvm.current().javaHome}/include/linux") + compileTask.source.from fileTree(dir: "src/main/c", include: "player_alsa.c") } 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: "**/*.c") - + def toolChain = binary.toolChain if (toolChain instanceof VisualCpp) { compileTask.compilerArgs.addAll(["/TC"]) diff --git a/player/src/main/c/player.c b/player/src/main/c/player.c index 26ba753..2539f8e 100644 --- a/player/src/main/c/player.c +++ b/player/src/main/c/player.c @@ -4,14 +4,35 @@ #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; static jobject currentFile = NULL; +static HeaderData currentHeader; /** * This function is used to initialize the static reference to java.io.InputStream class. @@ -27,7 +48,7 @@ jclass getInputStreamClass(JNIEnv *env) jclass tempClass = (*env)->FindClass(env, "java/io/InputStream"); if (tempClass == 0) { - printf("Could not find class java/io/InputStream"); + fprintf(stderr, "Could not find class java/io/InputStream"); return 0; } InputStream = (*env)->NewGlobalRef(env, tempClass); @@ -49,7 +70,7 @@ jclass getAudioStreamClass(JNIEnv *env, jobject audio) jclass tempClass = (*env)->GetObjectClass(env, audio); if (tempClass == 0) { - printf("Could not find class edu/regis/universeplayer_localPlayer/AudioFile"); + fprintf(stderr, "Could not find class edu/regis/universeplayer_localPlayer/AudioFile"); return 0; } AudioStream = (*env)->NewGlobalRef(env, tempClass); @@ -57,6 +78,21 @@ jclass getAudioStreamClass(JNIEnv *env, jobject audio) 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. * @@ -114,7 +150,7 @@ jmethodID getReadMethod(JNIEnv *env, jobject audio) tempClass = getAudioStreamClass(env, audio); if (tempClass == 0) { - printf("Could not find AudioStream class"); + fprintf(stderr, "Could not find AudioStream class"); } else { @@ -127,7 +163,7 @@ jmethodID getReadMethod(JNIEnv *env, jobject audio) tempClass = getInputStreamClass(env); if (tempClass == 0) { - printf("Could not find InputStream class"); + fprintf(stderr, "Could not find InputStream class"); } else { @@ -138,6 +174,124 @@ jmethodID getReadMethod(JNIEnv *env, jobject audio) 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. @@ -213,12 +367,12 @@ int readBuffer(JNIEnv *env, jobject stream, char* buffer, int bufferSize) getReadMethod(env, stream); if (InputStream_readBuffer == 0) { - printf("readBuffer method not found\n"); + fprintf(stderr, "readBuffer method not found\n"); return 0; } if (AudioStream_readInt == 0) { - printf("readInt method not found\n"); + fprintf(stderr, "readInt method not found\n"); return 0; } } @@ -234,24 +388,49 @@ int readBuffer(JNIEnv *env, jobject stream, char* buffer, int bufferSize) return returnValue; } +/** + * Converts a WavHeader into something more easily accessible by native code. + * + * @param env - The JVM. + * @param header - The header to read. + * @param headerStruct - The header to write the data to. + */ +HeaderData readHeader(JNIEnv *env, jobject header, HeaderData *headerStruct) +{ + if (Header == 0) + { + getHeaderMethods(env); + } + headerStruct->numChannels = (short) (*env)->CallShortMethod(env, header, Header_getNumChannels); + headerStruct->bytesPerChannel = (short) (*env)->CallShortMethod(env, header, Header_getBitsPerSample); + headerStruct->sampleRate = (int) (*env)->CallIntMethod(env, header, Header_getSampleRate); +} + /** * 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) { + jobject header; if (currentFile != NULL) { (*env)->DeleteLocalRef(env, currentFile); } currentFile = (*env)->NewGlobalRef(env, stream); - printf("Setting File\n"); - return 0; + header = getHeader(env, stream); + readHeader(env, header, ¤tHeader); + + return updatePlayer(env, obj, stream, numChannels, bitsPerSample, sampleRate); } JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_save(JNIEnv *env, jobject obj, jstring location) { - int BUFFER_SIZE = 256; - char buffer[BUFFER_SIZE]; + const int BUFFER_SIZE = 256; + /* + * Windows doesn't seem to like using even a constant for determining array + * size. + */ + char buffer[256]; int read; FILE *output; @@ -262,7 +441,7 @@ JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_save(JNI if (output == NULL) { - printf("File %s can't be opened\n", locationPath); + fprintf(stderr, "File %s can't be opened\n", locationPath); return; } @@ -270,7 +449,7 @@ JNIEXPORT void JNICALL Java_edu_regis_universeplayer_localPlayer_Player_save(JNI jbyteArray headerData = getByteStream(env, currentFile); jsize headerLength = (*env)->GetArrayLength(env, headerData); jbyte* header = (*env)->GetByteArrayElements(env, headerData, 0); - char headerBuff[headerLength]; + char headerBuff[256]; for (int i = 0; i < headerLength; i++) { headerBuff[i] = header[i]; diff --git a/player/src/main/c/player_alsa.c b/player/src/main/c/player_alsa.c new file mode 100644 index 0000000..5b5bedb --- /dev/null +++ b/player/src/main/c/player_alsa.c @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ +#include +#include + +int updatePlayer(JNIEnv *env, jobject obj, jobject stream, jshort numChannels, jshort bitsPerSample, jint sampleRate) +{ + printf("Updating File\n"); + return 0; +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 2b935ce..123166b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,4 +23,4 @@ rootProject.name = 'UniversalMusicPlayer' include ':interface' include ':add-on' include ':player' -include ':WavReader' +include ':libwave'