From 65e740f4a8e0509c5b9d7844c291ee73083de9c1 Mon Sep 17 00:00:00 2001 From: AShiningRay Date: Wed, 7 Jun 2023 17:17:05 -0300 Subject: [PATCH 1/6] PlatformPlayer: Implement initial IMA ADPCM decoding support In order to decode IMA ADPCM wav files, we first read the incoming wav's header and check if the reported Audio Format is '17', indicating that it is an IMA ADPCM stream. If it is, we begin to decode its samples into signed PCM16LE while maintaining the same amount of channels, and sampling rate. Once that is done, we build a new header for the decoded stream to reflect its new format and then send the completed stream back to PlatformPlayer's wavPlayer so it can handle it like a standard wav stream. Additionally, a simple 'low-pass filter' was added to combat crackling on resulting samples, as it was really overbearing, it vastly improves quality at almost no cost. --- src/org/recompile/mobile/PlatformPlayer.java | 37 +- .../recompile/mobile/WavImaAdpcmDecoder.java | 370 ++++++++++++++++++ 2 files changed, 401 insertions(+), 6 deletions(-) create mode 100644 src/org/recompile/mobile/WavImaAdpcmDecoder.java diff --git a/src/org/recompile/mobile/PlatformPlayer.java b/src/org/recompile/mobile/PlatformPlayer.java index b0cf3699..b357101b 100644 --- a/src/org/recompile/mobile/PlatformPlayer.java +++ b/src/org/recompile/mobile/PlatformPlayer.java @@ -274,10 +274,15 @@ public boolean isRunning() private class wavPlayer extends audioplayer { - + /* PCM WAV variables */ private AudioInputStream wavStream; private Clip wavClip; - + /* IMA ADPCM WAV variables */ + private WavImaAdpcmDecoder adpcmDec = new WavImaAdpcmDecoder(); + InputStream decodedStream; + private int[] wavHeaderData = new int[4]; + + /* Player control variables */ private int loops = 0; private long time = 0L; @@ -286,10 +291,30 @@ public wavPlayer(InputStream stream) { try { - wavStream = AudioSystem.getAudioInputStream(stream); - wavClip = AudioSystem.getClip(); - wavClip.open(wavStream); - state = Player.PREFETCHED; + /* + * A wav header is generally 44-bytes long, so mark it on the stream as we'll need to reset to + * the actual stream data after checking the header. + */ + stream.mark(44); + wavHeaderData = adpcmDec.readHeader(stream); + stream.reset(); + + /* We only check for IMA ADPCM at the moment. */ + if(wavHeaderData[0] != 17) /* If it's not IMA ADPCM we don't need to do anything to the stream. */ + { + wavStream = AudioSystem.getAudioInputStream(stream); + wavClip = AudioSystem.getClip(); + wavClip.open(wavStream); + state = Player.PREFETCHED; + } + else /* But if it is IMA ADPCM, we have to decode it manually. */ + { + decodedStream = adpcmDec.decodeImaAdpcm(stream, wavHeaderData); + wavStream = AudioSystem.getAudioInputStream(decodedStream); + wavClip = AudioSystem.getClip(); + wavClip.open(wavStream); + state = Player.PREFETCHED; + } } catch (Exception e) { diff --git a/src/org/recompile/mobile/WavImaAdpcmDecoder.java b/src/org/recompile/mobile/WavImaAdpcmDecoder.java new file mode 100644 index 00000000..7ceabca2 --- /dev/null +++ b/src/org/recompile/mobile/WavImaAdpcmDecoder.java @@ -0,0 +1,370 @@ +/* + This file is part of FreeJ2ME. + + FreeJ2ME is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FreeJ2ME 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FreeJ2ME. If not, see http://www.gnu.org/licenses/ +*/ +package org.recompile.mobile; + +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; + +public class WavImaAdpcmDecoder +{ + /* Information about this audio format: https://wiki.multimedia.cx/index.php/IMA_ADPCM */ + + /* Variables to hold the previously decoded sample and step used, per channel (if needed) */ + private static int[] prevSample; + private static int[] prevStep; + + /* + * This one acts as the simplest low-pass filter ever, dividing each decoded sample by + * the given amount. Anything from '2' to '4' vastly improves the resulting audio quality + * by cutting off more of the high range which crackles a lot. '3' is a good middle-ground + * between 2's clearer but still prone to crackling and 4's more consistent but muffled sound. + */ + private static final int smoothing = 3; + + private static final int[] ima_step_index_table = + { + -1, -1, -1, -1, 2, 4, 6, 8, + -1, -1, -1, -1, 2, 4, 6, 8 + }; + + private static final int[] ima_step_size_table = + { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, + 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, + 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, + 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, + 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, + 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 + }; + + /* + * This method will decode IMA WAV ADPCM into linear PCM_S16LE. + * Note: Largely based on ffmpeg's implementation. + */ + public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, int block_size) + { + byte[] output; + byte adpcmSample; + int inputIndex = 0; + int outputIndex = 44; /* Give some space for the header */ + int outputSize, decodedSample; + short curChannel; + + prevSample = new int[2]; + prevStep = new int[2]; + + outputSize = 44 + (inputSize * 4); + output = new byte[outputSize]; + + while (inputSize > 0) + { + if (inputSize % block_size == 0) + { + prevSample[0] = (int) (((input[inputIndex]& 0xFF)) | ((input[inputIndex+1]) << 8) & 0xFF00); + prevStep[0] = (int) (input[inputIndex+2]); + inputIndex += 4; + inputSize -= 4; + outputSize -= 16; + + if (numChannels > 1) + { + prevSample[1] = (int) (((input[inputIndex]& 0xFF)) | ((input[inputIndex+1]) << 8) & 0xFF00); + prevStep[1] = (int) (input[inputIndex + 2]); + inputIndex += 4; + inputSize -= 4; + outputSize -= 16; + } + } + + /* In the very rare (pretty much non-existent) cases where some j2me app + * might use stereo ima adpcm, we should decode each audio channel. + * Again largely based on ffmpeg. */ + if (numChannels > 1) + { + for (short i = 0; i < 8; i++) + { + if(i < 4) { curChannel = 0; } + else { curChannel = 1; } + + adpcmSample = (byte)(input[inputIndex] & 0x0f); + decodedSample = decodeSample(curChannel, adpcmSample); + output[outputIndex + ((i & 3) << 3) + (curChannel << 1)] = (byte)(decodedSample & 0xff); + output[outputIndex + ((i & 3) << 3) + (curChannel << 1) + 1] = (byte)(decodedSample >> 8); + + adpcmSample = (byte)((input[inputIndex] >> 4) & 0x0f); + decodedSample = decodeSample(curChannel, adpcmSample); + output[outputIndex + ((i & 3) << 3) + (curChannel << 1) + 4] = (byte)(decodedSample & 0xff); + output[outputIndex + ((i & 3) << 3) + (curChannel << 1) + 5] = (byte)(decodedSample >> 8); + inputIndex++; + } + outputIndex += 32; + inputSize -= 8; + } + else + { + adpcmSample = (byte)(input[inputIndex] & 0x0f); + decodedSample = decodeSample(0, adpcmSample); + output[outputIndex++] = (byte)(decodedSample & 0xff); + output[outputIndex++] = (byte)((decodedSample >> 8) & 0xff); + + adpcmSample = (byte)((input[inputIndex] >> 4) & 0x0f); + decodedSample = decodeSample(0, adpcmSample); + output[outputIndex++] = (byte)(decodedSample & 0xff); + output[outputIndex++] = (byte)((decodedSample >> 8) & 0xff); + + inputIndex++; + inputSize--; + } + } + + return output; + } + + /* This method will decode a single IMA ADPCM sample to linear PCM_S16LE sample. */ + static short decodeSample(int channel, int adpcmSample) + { + int stepSize; + int decoded; + stepSize = ima_step_size_table[prevStep[channel]] & 0x0000FFFF; + decoded = (stepSize >> 3) & 0x1fff; + + if ((adpcmSample & 1) != 0) { decoded += (stepSize >> 2) & 0x3fff; } + if ((adpcmSample & 2) != 0) { decoded += (stepSize >> 1) & 0x7fff; } + if ((adpcmSample & 4) != 0) { decoded += stepSize; } + if ((adpcmSample & 8) != 0) { decoded = -(short) decoded; } + + decoded += (short) prevSample[channel]; + decoded = decoded / smoothing; + + if ((short) decoded < -32768) { decoded = -32768; } + else if ((short) decoded > 32767) { decoded = 32767; } + + prevSample[channel] = (short) decoded; + prevStep[channel] += ima_step_index_table[(int)(adpcmSample & 0x0FF)]; + + if (prevStep[channel] < 0) { prevStep[channel] = 0; } + else if (prevStep[channel] > 88) { prevStep[channel] = 88; } + + return (short) (decoded & 0xFFFF); + } + + /* + * Since the InputStream is always expected to be positioned right at the start + * of the byte data, read WAV file's header to determine its type. + * + * Optionally it also returns some information about the audio format to help build a + * new header for the decoded stream. + */ + public static int[] readHeader(InputStream input) throws IOException + { + /* + The header of a WAV (RIFF) file is 44 bytes long and has the following format: + + CHAR[4] "RIFF" header + UINT32 Size of the file (chunkSize). + CHAR[4] "WAVE" format + CHAR[4] "fmt " header + UINT32 SubChunkSize (examples: 12 for PCM unsigned 8-bit ) + UINT16 AudioFormat (ex: 1 [PCM], 17 [IMA ADPCM] ) + UINT16 NumChannels + UINT32 SampleRate + UINT32 BytesPerSec (samplerate*frame size) + UINT16 frame Size (256 on some gameloft games) + UINT16 BitsPerSample (gameloft games appear to use 4) + CHAR[4] "data" header + UINT32 Length of sample data. + + */ + + String riff = readInputStreamASCII(input, 4); + int dataSize = readInputStreamInt32(input); + String format = readInputStreamASCII(input, 4); + String fmt = readInputStreamASCII(input, 4); + int chunkSize = readInputStreamInt32(input); + short audioFormat = (short) readInputStreamInt16(input); + short audioChannels = (short) readInputStreamInt16(input); + int sampleRate = readInputStreamInt32(input); + int bytesPerSec = readInputStreamInt32(input); + short frameSize = (short) readInputStreamInt16(input); + short bitsPerSample = (short) readInputStreamInt16(input); + String data = readInputStreamASCII(input, 4); + int sampleDataLength = readInputStreamInt32(input); + + /* Those are only meant for debugging. */ + /* + System.out.println("WAV HEADER_START"); + + System.out.println(riff); + System.out.println("FileSize:" + dataSize); + System.out.println("Format: " + format); + + System.out.println("---'fmt' header---\n"); + System.out.println("Header ChunkSize:" + Integer.toString(chunkSize)); + System.out.println("AudioFormat: " + Integer.toString(audioFormat)); + System.out.println("AudioChannels:" + Integer.toString(audioChannels)); + System.out.println("SampleRate:" + Integer.toString(sampleRate)); + System.out.println("BytesPerSec:" + Integer.toString(bytesPerSec)); + System.out.println("FrameSize:" + Integer.toString(frameSize)); + System.out.println("BitsPerSample:" + Integer.toHexString(bitsPerSample)); + + System.out.println("\n---'data' header---\n"); + System.out.println("SampleData Length:" + Integer.toString(sampleDataLength)); + + System.out.println("WAV HEADER_END\n\n\n"); + */ + + /* + * We need the audio format to check if it's ADPCM or PCM, and the file's + * dataSize + SampleRate + audioChannels to decode ADPCM and build the new header. + */ + return new int[] {audioFormat, dataSize, sampleRate, audioChannels}; + } + + /* Read a 16-bit little-endian unsigned integer from input.*/ + public static int readInputStreamInt16(InputStream input) throws IOException + { return ( input.read() & 0xFF ) | ( ( input.read() & 0xFF ) << 8 ); } + + /* Read a 32-bit little-endian signed integer from input.*/ + public static int readInputStreamInt32(InputStream input) throws IOException + { + return ( input.read() & 0xFF ) | ( ( input.read() & 0xFF ) << 8 ) + | ( ( input.read() & 0xFF ) << 16 ) | ( ( input.read() & 0xFF ) << 24 ); + } + + /* Return a String containing 'n' Characters of ASCII/ISO-8859-1 text from input. */ + public static String readInputStreamASCII(InputStream input, int nChars) throws IOException + { + byte[] chars = new byte[nChars]; + readInputStreamData(input, chars, 0, nChars); + return new String(chars, "ISO-8859-1"); + } + + /* Read 'n' Bytes from the InputStream starting from the specified offset into the output array. */ + public static void readInputStreamData(InputStream input, byte[] output, int offset, int nBytes) throws IOException + { + int end = offset + nBytes; + while(offset < end) + { + int read = input.read(output, offset, end - offset); + if(read < 0) throw new java.io.EOFException(); + offset += read; + } + } + + /* + * Builds a WAV header that describes the decoded ADPCM file on the first 44 bytes. + * Data: little-endian, 16-bit, signed, same sample rate and channels as source IMA ADPCM. + */ + private void buildHeader(byte[] buffer, int numChannels, int sampleRate) + { + final short bitsPerSample = 16; /* 16-bit PCM */ + final short audioFormat = 1; /* WAV linear PCM */ + final int subChunkSize = 16; /* Fixed size for Wav Linear PCM*/ + final int chunk = 0x46464952; /* 'RIFF' */ + final int format = 0x45564157; /* 'WAVE' */ + final int subChunk1 = 0x20746d66; /* 'fmt ' */ + final int subChunk2 = 0x61746164; /* 'data' */ + + /* + * We'll have 16 bits per sample, so each sample has 2 bytes, with that we just divide + * the size of the byte buffer (minus the header) by 2, then multiply by the amount + * of channels... assuming i didn't mess anything up, which is likely with this much code. + */ + final int samplesPerChannel = ((buffer.length-44) / 2) * numChannels; + + final short frameSize = (short) (numChannels * bitsPerSample / 8); + final int sampleDataLength = samplesPerChannel * frameSize; + final int bytesPerSec = sampleRate * frameSize; + final int dataSize = 36 + sampleDataLength; + + /* ChunkID */ + buffer[0] = (byte) (chunk & 0xff); + buffer[1] = (byte) ((chunk >>> 8) & 0xff); + buffer[2] = (byte) ((chunk >>> 16) & 0xff); + buffer[3] = (byte) ((chunk >>> 24) & 0xff); + /* ChunkSize (or File size) */ + buffer[4] = (byte) (dataSize & 0xff); + buffer[5] = (byte) ((dataSize >> 8) & 0xff); + buffer[6] = (byte) ((dataSize >> 16) & 0xff); + buffer[7] = (byte) ((dataSize >> 24) & 0xff); + /* Format (WAVE) */ + buffer[8] = (byte) (format & 0xff); + buffer[9] = (byte) ((format >>> 8 ) & 0xff); + buffer[10] = (byte) ((format >>> 16) & 0xff); + buffer[11] = (byte) ((format >>> 24) & 0xff); + /* SubchunkID (fmt) */ + buffer[12] = (byte) (subChunk1 & 0xff); + buffer[13] = (byte) ((subChunk1 >>> 8) & 0xff); + buffer[14] = (byte) ((subChunk1 >>> 16) & 0xff); + buffer[15] = (byte) ((subChunk1 >>> 24) & 0xff); + /* SubchunkSize (or format chunk size) */ + buffer[16] = (byte) (subChunkSize & 0xff); + buffer[17] = (byte) ((subChunkSize >> 8) & 0xff); + buffer[18] = (byte) ((subChunkSize >> 16) & 0xff); + buffer[19] = (byte) ((subChunkSize >> 24) & 0xff); + /* Audioformat */ + buffer[20] = (byte) (audioFormat & 0xff); + buffer[21] = (byte) ((audioFormat >> 8) & 0xff); + /* NumChannels (will be the same as source ADPCM) */ + buffer[22] = (byte) (numChannels & 0xff); + buffer[23] = (byte) ((numChannels >> 8) & 0xff); + /* SampleRate (will be the same as source ADPCM) */ + buffer[24] = (byte) (sampleRate & 0xff); + buffer[25] = (byte) ((sampleRate >> 8) & 0xff); + buffer[26] = (byte) ((sampleRate >> 16) & 0xff); + buffer[27] = (byte) ((sampleRate >> 24) & 0xff); + /* ByteRate (BytesPerSec) */ + buffer[28] = (byte) (bytesPerSec & 0xff); + buffer[29] = (byte) ((bytesPerSec >> 8) & 0xff); + buffer[30] = (byte) ((bytesPerSec >> 16) & 0xff); + buffer[31] = (byte) ((bytesPerSec >> 24) & 0xff); + /* BlockAlign (Frame Size) */ + buffer[32] = (byte) (frameSize & 0xff); + buffer[33] = (byte) ((frameSize >> 8) & 0xff); + /* BitsPerSample */ + buffer[34] = (byte) (bitsPerSample & 0xff); + buffer[35] = (byte) ((bitsPerSample >> 8) & 0xff); + /* Subchunk2ID (data) */ + buffer[36] = (byte) (subChunk2 & 0xff); + buffer[37] = (byte) ((subChunk2 >>> 8) & 0xff); + buffer[38] = (byte) ((subChunk2 >>> 16) & 0xff); + buffer[39] = (byte) ((subChunk2 >>> 24) & 0xff); + /* Subchunk2 Size (sampledata length) */ + buffer[40] = (byte) (sampleDataLength & 0xff); + buffer[41] = (byte) ((sampleDataLength >> 8) & 0xff); + buffer[42] = (byte) ((sampleDataLength >> 16) & 0xff); + buffer[43] = (byte) ((sampleDataLength >> 24) & 0xff); + } + + /* Decode the received IMA WAV ADPCM stream into a signed PCM16LE byte array, then return it to PlatformPlayer. */ + public ByteArrayInputStream decodeImaAdpcm(InputStream stream, int[] wavHeaderData) throws IOException + { + byte[] input = new byte[stream.available()]; + readInputStreamData(stream, input, 0, stream.available()); + + byte[] output = decodeADPCM(input, input.length, wavHeaderData[3], wavHeaderData[1]); + buildHeader(output, wavHeaderData[3], wavHeaderData[2]); /* Builds a new header for the decoded stream. */ + + return new ByteArrayInputStream(output); + } + +} From 3b9eebfd97eda1c0533c57e4238083fc69ce743b Mon Sep 17 00:00:00 2001 From: AShiningRay Date: Thu, 8 Jun 2023 23:18:43 -0300 Subject: [PATCH 2/6] ImaAdpcmDecoder: readHeader doesn't need to be static. --- src/org/recompile/mobile/WavImaAdpcmDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/recompile/mobile/WavImaAdpcmDecoder.java b/src/org/recompile/mobile/WavImaAdpcmDecoder.java index 7ceabca2..fb7816bb 100644 --- a/src/org/recompile/mobile/WavImaAdpcmDecoder.java +++ b/src/org/recompile/mobile/WavImaAdpcmDecoder.java @@ -174,7 +174,7 @@ static short decodeSample(int channel, int adpcmSample) * Optionally it also returns some information about the audio format to help build a * new header for the decoded stream. */ - public static int[] readHeader(InputStream input) throws IOException + public int[] readHeader(InputStream input) throws IOException { /* The header of a WAV (RIFF) file is 44 bytes long and has the following format: From 28b1177a14a0ecb6fe337044dfdead0d9fc6dfc0 Mon Sep 17 00:00:00 2001 From: AShiningRay Date: Fri, 9 Jun 2023 13:33:23 -0300 Subject: [PATCH 3/6] ImaAdpcmDecoder: Make some improvements to the decoder. The initial implementation had tons of issues (incorrect reading of chunks and their preambles, lack of clamping on some ima step indices just to name a few). This commit fixes some of them while also adding comments to help understand what is being done inside the decoder's class. --- .../recompile/mobile/WavImaAdpcmDecoder.java | 146 ++++++++++++------ 1 file changed, 98 insertions(+), 48 deletions(-) diff --git a/src/org/recompile/mobile/WavImaAdpcmDecoder.java b/src/org/recompile/mobile/WavImaAdpcmDecoder.java index fb7816bb..15aa6e2a 100644 --- a/src/org/recompile/mobile/WavImaAdpcmDecoder.java +++ b/src/org/recompile/mobile/WavImaAdpcmDecoder.java @@ -28,14 +28,6 @@ public class WavImaAdpcmDecoder /* Variables to hold the previously decoded sample and step used, per channel (if needed) */ private static int[] prevSample; private static int[] prevStep; - - /* - * This one acts as the simplest low-pass filter ever, dividing each decoded sample by - * the given amount. Anything from '2' to '4' vastly improves the resulting audio quality - * by cutting off more of the high range which crackles a lot. '3' is a good middle-ground - * between 2's clearer but still prone to crackling and 4's more consistent but muffled sound. - */ - private static final int smoothing = 3; private static final int[] ima_step_index_table = { @@ -58,59 +50,103 @@ public class WavImaAdpcmDecoder /* * This method will decode IMA WAV ADPCM into linear PCM_S16LE. - * Note: Largely based on ffmpeg's implementation. + * Note: Partially based on ffmpeg's implementation. */ - public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, int block_size) + public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, int frameSize) { byte[] output; byte adpcmSample; - int inputIndex = 0; - int outputIndex = 44; /* Give some space for the header */ + int inputIndex = 0, outputIndex = 0; int outputSize, decodedSample; - short curChannel; + byte curChannel; prevSample = new int[2]; prevStep = new int[2]; - outputSize = 44 + (inputSize * 4); + outputSize = (inputSize * 4); output = new byte[outputSize]; + /* Decode until the whole input (ADPCM) stream is depleted */ while (inputSize > 0) { - if (inputSize % block_size == 0) + /* Check if the decoder reached the beginning of a new chunk to see if the preamble needs to be read. */ + if (inputSize % frameSize == 0) { - prevSample[0] = (int) (((input[inputIndex]& 0xFF)) | ((input[inputIndex+1]) << 8) & 0xFF00); - prevStep[0] = (int) (input[inputIndex+2]); + /* Bytes 0 and 1 describe the chunk's initial predictor value (little-endian) */ + prevSample[0] = (int) ((input[inputIndex])) | ((input[inputIndex+1]) << 8); + /* Byte 1 is the chunk's initial index on the step_size_table */ + prevStep[0] = (int) (input[inputIndex+2]); + + /* Make sure to clamp the step into the valid interval just in case */ + if (prevStep[0] < 0) { prevStep[0] = 0; } + else if (prevStep[0] > 88) { prevStep[0] = 88; } + + /* + * Byte 3 is usually reserved and set as '0' (null) in the beginning of each + * IMA ADPCM chunk (https://wiki.multimedia.cx/index.php/Microsoft_IMA_ADPCM), + * so if it is not that, we reached the end of the stream. + */ + if(input[inputIndex+3] != 0) { return output; } + + /* + * For each 4 bits used in IMA ADPCM, 16 must be used for PCM so adjust + * indices and sizes accordingly. + */ inputIndex += 4; inputSize -= 4; outputSize -= 16; - if (numChannels > 1) + if (numChannels == 2) /* If we're dealing with stereo IMA ADPCM: */ { - prevSample[1] = (int) (((input[inputIndex]& 0xFF)) | ((input[inputIndex+1]) << 8) & 0xFF00); - prevStep[1] = (int) (input[inputIndex + 2]); + /* Bytes 4 and 5 describe the chunk's initial predictor value (little-endian) */ + prevSample[1] = (int) ((input[inputIndex])) | ((input[inputIndex+1]) << 8); + prevStep[1] = (int) (input[inputIndex + 2]); + + if (prevStep[1] < 0) { prevStep[1] = 0; } + else if (prevStep[1] > 88) { prevStep[1] = 88; } + + /* + * Byte 7 is usually reserved and set as '0' (null) in the beginning of each + * IMA ADPCM chunk, so if it is not that, we reached the end of the stream. + */ + if(input[inputIndex+3] != 0) { return output; } + inputIndex += 4; inputSize -= 4; outputSize -= 16; } } - + + /* If the decoder isn't at the beginning of a chunk, or the preamble has already been read, + * decode ADPCM samples inside that same chunk. + */ + /* In the very rare (pretty much non-existent) cases where some j2me app - * might use stereo ima adpcm, we should decode each audio channel. - * Again largely based on ffmpeg. */ - if (numChannels > 1) + * might use stereo IMA ADPCM, we should decode each audio channel. + * + * If the format is stereo, it is assumed to be interleaved, which means that + * the stream will have a left channel sample followed by a right channel sample, + * followed by a left... and so on. In ADPCM those samples appear to be setup in + * such a way that 4 bytes (or 8 nibbles) for the left channel are followed by 4 bytes + * for the right, at least according to https://wiki.multimedia.cx/index.php/Microsoft_IMA_ADPCM. + */ + if (numChannels == 2) { + /* + * So in the case it's a stereo stream, decode 8 nibbles from both left and right channels, interleaving + * them in the resulting PCM stream. + */ for (short i = 0; i < 8; i++) { if(i < 4) { curChannel = 0; } else { curChannel = 1; } - adpcmSample = (byte)(input[inputIndex] & 0x0f); + adpcmSample = (byte) (input[inputIndex] & 0x0f); decodedSample = decodeSample(curChannel, adpcmSample); output[outputIndex + ((i & 3) << 3) + (curChannel << 1)] = (byte)(decodedSample & 0xff); output[outputIndex + ((i & 3) << 3) + (curChannel << 1) + 1] = (byte)(decodedSample >> 8); - adpcmSample = (byte)((input[inputIndex] >> 4) & 0x0f); + adpcmSample = (byte) ((input[inputIndex] >> 4) & 0x0f); decodedSample = decodeSample(curChannel, adpcmSample); output[outputIndex + ((i & 3) << 3) + (curChannel << 1) + 4] = (byte)(decodedSample & 0xff); output[outputIndex + ((i & 3) << 3) + (curChannel << 1) + 5] = (byte)(decodedSample >> 8); @@ -119,15 +155,21 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i outputIndex += 32; inputSize -= 8; } - else + else { + /* + * If it's mono, just decode nibbles from ADPCM into PCM data sequentially, there's no sample + * interleaving to worry about . + */ + curChannel = 0; + adpcmSample = (byte)(input[inputIndex] & 0x0f); - decodedSample = decodeSample(0, adpcmSample); + decodedSample = decodeSample(curChannel, adpcmSample); output[outputIndex++] = (byte)(decodedSample & 0xff); output[outputIndex++] = (byte)((decodedSample >> 8) & 0xff); adpcmSample = (byte)((input[inputIndex] >> 4) & 0x0f); - decodedSample = decodeSample(0, adpcmSample); + decodedSample = decodeSample(curChannel, adpcmSample); output[outputIndex++] = (byte)(decodedSample & 0xff); output[outputIndex++] = (byte)((decodedSample >> 8) & 0xff); @@ -140,31 +182,39 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i } /* This method will decode a single IMA ADPCM sample to linear PCM_S16LE sample. */ - static short decodeSample(int channel, int adpcmSample) + static short decodeSample(int channel, byte adpcmSample) { - int stepSize; - int decoded; - stepSize = ima_step_size_table[prevStep[channel]] & 0x0000FFFF; - decoded = (stepSize >> 3) & 0x1fff; + /* + * This decode procedure is based on the following document: + * https://www.cs.columbia.edu/~hgs/audio/dvi/IMA_ADPCM.pdf + */ + + /* Get the step size to be used when decoding the given sample. */ + int stepSize = ima_step_size_table[prevStep[channel]] & 0x0000FFFF; + + /* This variable acts as 'difference' and then 'newSample' */ + int decodedSample = (stepSize >> 3) & 0x1fff; - if ((adpcmSample & 1) != 0) { decoded += (stepSize >> 2) & 0x3fff; } - if ((adpcmSample & 2) != 0) { decoded += (stepSize >> 1) & 0x7fff; } - if ((adpcmSample & 4) != 0) { decoded += stepSize; } - if ((adpcmSample & 8) != 0) { decoded = -(short) decoded; } + /* Similar to cs.columbia doc's first code block on Page 32 */ + if ((adpcmSample & 4) != 0) { decodedSample += stepSize; } + if ((adpcmSample & 2) != 0) { decodedSample += (stepSize >> 1) & 0x7fff; } + if ((adpcmSample & 1) != 0) { decodedSample += (stepSize >> 2) & 0x3fff; } - decoded += (short) prevSample[channel]; - decoded = decoded / smoothing; + if ((adpcmSample & 8) != 0) { decodedSample = -(short) decodedSample; } - if ((short) decoded < -32768) { decoded = -32768; } - else if ((short) decoded > 32767) { decoded = 32767; } + decodedSample += (short) prevSample[channel]; - prevSample[channel] = (short) decoded; + if ((short) decodedSample < -32768) { decodedSample = -32768; } + else if ((short) decodedSample > 32767) { decodedSample = 32767; } + + prevSample[channel] = (short) decodedSample; prevStep[channel] += ima_step_index_table[(int)(adpcmSample & 0x0FF)]; if (prevStep[channel] < 0) { prevStep[channel] = 0; } else if (prevStep[channel] > 88) { prevStep[channel] = 88; } - return (short) (decoded & 0xFFFF); + /* Return the decoded sample */ + return (short) (decodedSample & 0xFFFF); } /* @@ -188,7 +238,7 @@ UINT16 AudioFormat (ex: 1 [PCM], 17 [IMA ADPCM] ) UINT16 NumChannels UINT32 SampleRate UINT32 BytesPerSec (samplerate*frame size) - UINT16 frame Size (256 on some gameloft games) + UINT16 frameSize or blockAlign (256 on some gameloft games) UINT16 BitsPerSample (gameloft games appear to use 4) CHAR[4] "data" header UINT32 Length of sample data. @@ -236,7 +286,7 @@ UINT16 BitsPerSample (gameloft games appear to use 4) * We need the audio format to check if it's ADPCM or PCM, and the file's * dataSize + SampleRate + audioChannels to decode ADPCM and build the new header. */ - return new int[] {audioFormat, dataSize, sampleRate, audioChannels}; + return new int[] {audioFormat, sampleRate, audioChannels, frameSize}; } /* Read a 16-bit little-endian unsigned integer from input.*/ @@ -361,8 +411,8 @@ public ByteArrayInputStream decodeImaAdpcm(InputStream stream, int[] wavHeaderDa byte[] input = new byte[stream.available()]; readInputStreamData(stream, input, 0, stream.available()); - byte[] output = decodeADPCM(input, input.length, wavHeaderData[3], wavHeaderData[1]); - buildHeader(output, wavHeaderData[3], wavHeaderData[2]); /* Builds a new header for the decoded stream. */ + byte[] output = decodeADPCM(input, input.length, wavHeaderData[2], wavHeaderData[3]); + buildHeader(output, wavHeaderData[2], wavHeaderData[1]); /* Builds a new header for the decoded stream. */ return new ByteArrayInputStream(output); } From 939c791a004689c62752b681d638209725bd182e Mon Sep 17 00:00:00 2001 From: AShiningRay Date: Sun, 11 Jun 2023 18:46:57 -0300 Subject: [PATCH 4/6] ImaAdpcmDecoder: More adjustments to IMA ADPCM decoder This is an additional pass to further cleanup and improve the code. Checking if the fourth preamble byte is '!= 0' has no use here and does more harm than good, and in cases the preamble is read one or more times, the decoder accounts for it on the resulting stream's size for better time and space efficiency by eliminating the need to copy the data into a new correctly sized byte array. IMA ADPCM's header is also fully stripped from the decoding process as well, it is only used to get information regarding format, number of channels, bit rate and so on. --- src/org/recompile/mobile/PlatformPlayer.java | 7 ++- .../recompile/mobile/WavImaAdpcmDecoder.java | 51 +++++++++---------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/org/recompile/mobile/PlatformPlayer.java b/src/org/recompile/mobile/PlatformPlayer.java index b357101b..0f0b3ec8 100644 --- a/src/org/recompile/mobile/PlatformPlayer.java +++ b/src/org/recompile/mobile/PlatformPlayer.java @@ -292,12 +292,11 @@ public wavPlayer(InputStream stream) try { /* - * A wav header is generally 44-bytes long, so mark it on the stream as we'll need to reset to - * the actual stream data after checking the header. + * A wav header is generally 44-bytes long, and it is what we need to read in order to get + * the stream's format, frame size, bit rate, number of channels, etc. which gives us information + * on the kind of codec needed to play or decode the incoming stream. */ - stream.mark(44); wavHeaderData = adpcmDec.readHeader(stream); - stream.reset(); /* We only check for IMA ADPCM at the moment. */ if(wavHeaderData[0] != 17) /* If it's not IMA ADPCM we don't need to do anything to the stream. */ diff --git a/src/org/recompile/mobile/WavImaAdpcmDecoder.java b/src/org/recompile/mobile/WavImaAdpcmDecoder.java index 15aa6e2a..7a255f23 100644 --- a/src/org/recompile/mobile/WavImaAdpcmDecoder.java +++ b/src/org/recompile/mobile/WavImaAdpcmDecoder.java @@ -19,6 +19,7 @@ import java.io.InputStream; import java.io.ByteArrayInputStream; import java.io.IOException; + import java.util.Arrays; public class WavImaAdpcmDecoder @@ -56,14 +57,25 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i { byte[] output; byte adpcmSample; - int inputIndex = 0, outputIndex = 0; - int outputSize, decodedSample; byte curChannel; + int inputIndex = 0, outputIndex = 44; /* Give some space for the header */ + int decodedSample; + /* + * Make sure that the output size is 4 times as big as input's due to IMA ADPCM being able to pack 4 bytes of standard PCM + * data into a single one, with 44 additional bytes in place to accomodate for the new header that will be created afterwards. + * + * Also, calculate the final expected output size right here instead of decreasing the output size on each preamble read, + * checking if the final size is doesn't match what was initially expected (if there's at least one preamble, it won't) + * and then copying everything into a new byte array, which is too costly and can be cut entirely by just passing the + * correct output size right at the start. This is done because preamble data should not be added into the decoded stream, + * and each preamble is 4 bytes long on IMA ADPCM, which means it would take 16 bytes on the decoded stream for each preamble added. + */ + final int outputSize = (inputSize * 4) + 44 - (16 * (int) java.lang.Math.floor(inputSize / frameSize)); + prevSample = new int[2]; prevStep = new int[2]; - outputSize = (inputSize * 4); output = new byte[outputSize]; /* Decode until the whole input (ADPCM) stream is depleted */ @@ -81,20 +93,12 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i if (prevStep[0] < 0) { prevStep[0] = 0; } else if (prevStep[0] > 88) { prevStep[0] = 88; } - /* - * Byte 3 is usually reserved and set as '0' (null) in the beginning of each - * IMA ADPCM chunk (https://wiki.multimedia.cx/index.php/Microsoft_IMA_ADPCM), - * so if it is not that, we reached the end of the stream. - */ - if(input[inputIndex+3] != 0) { return output; } - /* * For each 4 bits used in IMA ADPCM, 16 must be used for PCM so adjust * indices and sizes accordingly. */ inputIndex += 4; inputSize -= 4; - outputSize -= 16; if (numChannels == 2) /* If we're dealing with stereo IMA ADPCM: */ { @@ -105,23 +109,18 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i if (prevStep[1] < 0) { prevStep[1] = 0; } else if (prevStep[1] > 88) { prevStep[1] = 88; } - /* - * Byte 7 is usually reserved and set as '0' (null) in the beginning of each - * IMA ADPCM chunk, so if it is not that, we reached the end of the stream. - */ - if(input[inputIndex+3] != 0) { return output; } - inputIndex += 4; inputSize -= 4; - outputSize -= 16; } } - /* If the decoder isn't at the beginning of a chunk, or the preamble has already been read, + /* + * If the decoder isn't at the beginning of a chunk, or the preamble has already been read, * decode ADPCM samples inside that same chunk. */ - /* In the very rare (pretty much non-existent) cases where some j2me app + /* + * In the very rare (pretty much non-existent) cases where some j2me app * might use stereo IMA ADPCM, we should decode each audio channel. * * If the format is stereo, it is assumed to be interleaved, which means that @@ -159,7 +158,7 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i { /* * If it's mono, just decode nibbles from ADPCM into PCM data sequentially, there's no sample - * interleaving to worry about . + * interleaving to worry about. */ curChannel = 0; @@ -177,7 +176,7 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i inputSize--; } } - + return output; } @@ -218,8 +217,8 @@ static short decodeSample(int channel, byte adpcmSample) } /* - * Since the InputStream is always expected to be positioned right at the start - * of the byte data, read WAV file's header to determine its type. + * Since the header is always expected to be positioned right at the start + * of a byte array, read it to determine the WAV type. * * Optionally it also returns some information about the audio format to help build a * new header for the decoded stream. @@ -284,7 +283,7 @@ UINT16 BitsPerSample (gameloft games appear to use 4) /* * We need the audio format to check if it's ADPCM or PCM, and the file's - * dataSize + SampleRate + audioChannels to decode ADPCM and build the new header. + * dataSize, SampleRate and audioChannels to decode ADPCM and build a new header. */ return new int[] {audioFormat, sampleRate, audioChannels, frameSize}; } @@ -337,7 +336,7 @@ private void buildHeader(byte[] buffer, int numChannels, int sampleRate) /* * We'll have 16 bits per sample, so each sample has 2 bytes, with that we just divide * the size of the byte buffer (minus the header) by 2, then multiply by the amount - * of channels... assuming i didn't mess anything up, which is likely with this much code. + * of channels... assuming i didn't mess anything up on the calculus. */ final int samplesPerChannel = ((buffer.length-44) / 2) * numChannels; From 1fb6cf3f7eed1ac32adea4e51d6b12ab5f563dc6 Mon Sep 17 00:00:00 2001 From: AShiningRay Date: Tue, 27 Jun 2023 23:36:00 -0300 Subject: [PATCH 5/6] ImaAdpcmDecoder: Even more refinements to the decoder. Time for some final touches on this one. Header creation was reworked, decoding algorithm is now much better with little to no crackling even on small streams, everything that isn't going to change (and arrays that aren't going to be reassigned) are now final for better performance, and the code is a bit more readable as well. --- .../recompile/mobile/WavImaAdpcmDecoder.java | 219 +++++++++--------- 1 file changed, 114 insertions(+), 105 deletions(-) diff --git a/src/org/recompile/mobile/WavImaAdpcmDecoder.java b/src/org/recompile/mobile/WavImaAdpcmDecoder.java index 7a255f23..6e3e7b79 100644 --- a/src/org/recompile/mobile/WavImaAdpcmDecoder.java +++ b/src/org/recompile/mobile/WavImaAdpcmDecoder.java @@ -20,15 +20,30 @@ import java.io.ByteArrayInputStream; import java.io.IOException; -import java.util.Arrays; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; public class WavImaAdpcmDecoder { + /* Information about this audio format: https://wiki.multimedia.cx/index.php/IMA_ADPCM */ - /* Variables to hold the previously decoded sample and step used, per channel (if needed) */ - private static int[] prevSample; - private static int[] prevStep; + /* + * Variables to hold the previously decoded sample and step used, per channel (if needed) + * "NOTE: Arrays that won't be reassigned (but its values can still change) and variables + * that won't be changed are marked as final throughout the code to try and optimize the + * execution as much as possible since FreeJ2ME has a habit of freezing when adpcm samples + * are being decoded. So far only seems to happen in Java 8 and on my more limited devices." + * - @AShiningRay + */ + private static final int LEFTCHANNEL = 0; + private static final int RIGHTCHANNEL = 1; + + private static final int HEADERSIZE = 44; + private static final int PCMPREAMBLESIZE = 16; + + private static final int[] prevSample = {0, 0}; + private static final int[] prevStep = {0, 0}; private static final int[] ima_step_index_table = { @@ -50,15 +65,13 @@ public class WavImaAdpcmDecoder }; /* - * This method will decode IMA WAV ADPCM into linear PCM_S16LE. - * Note: Partially based on ffmpeg's implementation. + * This method will decode IMA WAV ADPCM into linear PCM_S16LE. */ - public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, int frameSize) + public static byte[] decodeADPCM(final byte[] input, int inputSize, final int numChannels, final int frameSize) { - byte[] output; byte adpcmSample; byte curChannel; - int inputIndex = 0, outputIndex = 44; /* Give some space for the header */ + int inputIndex = 0, outputIndex = HEADERSIZE; /* Give some space for the header by starting from byte 44. */ int decodedSample; /* @@ -71,43 +84,48 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i * correct output size right at the start. This is done because preamble data should not be added into the decoded stream, * and each preamble is 4 bytes long on IMA ADPCM, which means it would take 16 bytes on the decoded stream for each preamble added. */ - final int outputSize = (inputSize * 4) + 44 - (16 * (int) java.lang.Math.floor(inputSize / frameSize)); - - prevSample = new int[2]; - prevStep = new int[2]; - - output = new byte[outputSize]; + final int outputSize = (inputSize * 4) + HEADERSIZE - (PCMPREAMBLESIZE * (int) java.lang.Math.floor(inputSize / frameSize)); + final byte[] output = new byte[outputSize]; + + /* Initialize the predictor's sample and step values. */ + prevSample[LEFTCHANNEL] = 0; + prevStep[LEFTCHANNEL] = ima_step_size_table[2]; + + if(numChannels == 2) + { + prevSample[RIGHTCHANNEL] = 0; + prevStep[RIGHTCHANNEL] = ima_step_size_table[2]; + } - /* Decode until the whole input (ADPCM) stream is depleted */ while (inputSize > 0) { /* Check if the decoder reached the beginning of a new chunk to see if the preamble needs to be read. */ if (inputSize % frameSize == 0) { /* Bytes 0 and 1 describe the chunk's initial predictor value (little-endian) */ - prevSample[0] = (int) ((input[inputIndex])) | ((input[inputIndex+1]) << 8); - /* Byte 1 is the chunk's initial index on the step_size_table */ - prevStep[0] = (int) (input[inputIndex+2]); + prevSample[LEFTCHANNEL] = ((input[inputIndex])) | ((input[inputIndex+1]) << 8); + /* Byte 2 is the chunk's initial index on the step_size_table */ + prevStep[LEFTCHANNEL] = (input[inputIndex+2]); - /* Make sure to clamp the step into the valid interval just in case */ - if (prevStep[0] < 0) { prevStep[0] = 0; } - else if (prevStep[0] > 88) { prevStep[0] = 88; } + if (prevStep[LEFTCHANNEL] < 0) { prevStep[LEFTCHANNEL] = 0; } + else if (prevStep[LEFTCHANNEL] > 88) { prevStep[LEFTCHANNEL] = 88; } /* * For each 4 bits used in IMA ADPCM, 16 must be used for PCM so adjust - * indices and sizes accordingly. + * indices and sizes accordingly. Byte 3 is reserved and has no practical + * use for us. */ inputIndex += 4; inputSize -= 4; if (numChannels == 2) /* If we're dealing with stereo IMA ADPCM: */ { - /* Bytes 4 and 5 describe the chunk's initial predictor value (little-endian) */ - prevSample[1] = (int) ((input[inputIndex])) | ((input[inputIndex+1]) << 8); - prevStep[1] = (int) (input[inputIndex + 2]); + /* Bytes 4 and 5 describe the chunk's initial predictor value (little-endian) for the second channel */ + prevSample[RIGHTCHANNEL] = ((input[inputIndex])) | ((input[inputIndex+1]) << 8); + prevStep[RIGHTCHANNEL] = (input[inputIndex + 2]); - if (prevStep[1] < 0) { prevStep[1] = 0; } - else if (prevStep[1] > 88) { prevStep[1] = 88; } + if (prevStep[RIGHTCHANNEL] < 0) { prevStep[RIGHTCHANNEL] = 0; } + else if (prevStep[RIGHTCHANNEL] > 88) { prevStep[RIGHTCHANNEL] = 88; } inputIndex += 4; inputSize -= 4; @@ -137,8 +155,8 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i */ for (short i = 0; i < 8; i++) { - if(i < 4) { curChannel = 0; } - else { curChannel = 1; } + if(i < 4) { curChannel = LEFTCHANNEL; } + else { curChannel = RIGHTCHANNEL; } adpcmSample = (byte) (input[inputIndex] & 0x0f); decodedSample = decodeSample(curChannel, adpcmSample); @@ -158,17 +176,15 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i { /* * If it's mono, just decode nibbles from ADPCM into PCM data sequentially, there's no sample - * interleaving to worry about. + * interleaving to worry about, much less multiple channels, so we only use channel 0. */ - curChannel = 0; - adpcmSample = (byte)(input[inputIndex] & 0x0f); - decodedSample = decodeSample(curChannel, adpcmSample); + decodedSample = decodeSample(LEFTCHANNEL, adpcmSample); output[outputIndex++] = (byte)(decodedSample & 0xff); output[outputIndex++] = (byte)((decodedSample >> 8) & 0xff); adpcmSample = (byte)((input[inputIndex] >> 4) & 0x0f); - decodedSample = decodeSample(curChannel, adpcmSample); + decodedSample = decodeSample(LEFTCHANNEL, adpcmSample); output[outputIndex++] = (byte)(decodedSample & 0xff); output[outputIndex++] = (byte)((decodedSample >> 8) & 0xff); @@ -181,7 +197,7 @@ public static byte[] decodeADPCM(byte[] input, int inputSize, int numChannels, i } /* This method will decode a single IMA ADPCM sample to linear PCM_S16LE sample. */ - static short decodeSample(int channel, byte adpcmSample) + static short decodeSample(final int channel, byte adpcmSample) { /* * This decode procedure is based on the following document: @@ -191,28 +207,36 @@ static short decodeSample(int channel, byte adpcmSample) /* Get the step size to be used when decoding the given sample. */ int stepSize = ima_step_size_table[prevStep[channel]] & 0x0000FFFF; - /* This variable acts as 'difference' and then 'newSample' */ + /* This variable acts as 'difference' and then 'newSample' on columbia's doc */ int decodedSample = (stepSize >> 3) & 0x1fff; - /* Similar to cs.columbia doc's first code block on Page 32 */ + /* + * Similar to cs.columbia doc's first code block on Page 32, multiplication through + * repetitive addition. Nowadays using multiplication is probably faster on most archs, + * but let's follow the reference implementation for the sake of improving documentation. + */ if ((adpcmSample & 4) != 0) { decodedSample += stepSize; } if ((adpcmSample & 2) != 0) { decodedSample += (stepSize >> 1) & 0x7fff; } if ((adpcmSample & 1) != 0) { decodedSample += (stepSize >> 2) & 0x3fff; } + if ((adpcmSample & 8) != 0) { decodedSample = -decodedSample; } - if ((adpcmSample & 8) != 0) { decodedSample = -(short) decodedSample; } - - decodedSample += (short) prevSample[channel]; - - if ((short) decodedSample < -32768) { decodedSample = -32768; } - else if ((short) decodedSample > 32767) { decodedSample = 32767; } + decodedSample += prevSample[channel]; - prevSample[channel] = (short) decodedSample; - prevStep[channel] += ima_step_index_table[(int)(adpcmSample & 0x0FF)]; + /* + * Clamps the value of decodedSample to that of a short data type. At this point, the decoded + * sample already should already fit nicely into a short type value range as per columbia's doc. + */ + if (decodedSample < -32768) { decodedSample = -32768; } + else if (decodedSample > 32767) { decodedSample = 32767; } + + prevSample[channel] = decodedSample; + /* Basically columbia doc's "calculate stepsize" snippet */ + prevStep[channel] += ima_step_index_table[(int)(adpcmSample & 0x0FF)]; if (prevStep[channel] < 0) { prevStep[channel] = 0; } else if (prevStep[channel] > 88) { prevStep[channel] = 88; } - /* Return the decoded sample */ + /* Return the decoded sample casted to short (16-bit) */ return (short) (decodedSample & 0xFFFF); } @@ -323,95 +347,80 @@ public static void readInputStreamData(InputStream input, byte[] output, int off * Builds a WAV header that describes the decoded ADPCM file on the first 44 bytes. * Data: little-endian, 16-bit, signed, same sample rate and channels as source IMA ADPCM. */ - private void buildHeader(byte[] buffer, int numChannels, int sampleRate) + private void buildHeader(final byte[] buffer, final short numChannels, final int sampleRate) { final short bitsPerSample = 16; /* 16-bit PCM */ final short audioFormat = 1; /* WAV linear PCM */ - final int subChunkSize = 16; /* Fixed size for Wav Linear PCM*/ - final int chunk = 0x46464952; /* 'RIFF' */ - final int format = 0x45564157; /* 'WAVE' */ - final int subChunk1 = 0x20746d66; /* 'fmt ' */ - final int subChunk2 = 0x61746164; /* 'data' */ + final int subChunkSize = 16; /* Fixed size for Wav Linear PCM */ + final int chunk = 0x52494646; /* 'RIFF' */ + final int format = 0x57415645; /* 'WAVE' */ + final int subChunk1 = 0x666d7420; /* 'fmt ' */ + final int subChunk2 = 0x64617461; /* 'data' */ /* * We'll have 16 bits per sample, so each sample has 2 bytes, with that we just divide - * the size of the byte buffer (minus the header) by 2, then multiply by the amount + * the size of the byte buffer (minus the header) by (bitsPerSample/8), then multiply by the amount * of channels... assuming i didn't mess anything up on the calculus. */ - final int samplesPerChannel = ((buffer.length-44) / 2) * numChannels; + final int samplesPerChannel = buffer.length-44 / ((bitsPerSample/8) * numChannels); + + /* + * Frame size is fairly standard, and PCM's fixed sample size makes it so the frameSize is either 2 bytes + * for mono, or 4 bytes for stereo. + */ + final short frameSize = (short) (numChannels * (bitsPerSample / 8)); + + /* + * Previously only took into account mono streams. And since we know the framesize and + * the amount of samples per channel, in a format that has a fixed amount of bits per sample, + * we can account for multiple audio channels on sampleDataLength with a simpler calculus: + */ + final int sampleDataLength = (samplesPerChannel * numChannels) * frameSize; - final short frameSize = (short) (numChannels * bitsPerSample / 8); - final int sampleDataLength = samplesPerChannel * frameSize; - final int bytesPerSec = sampleRate * frameSize; - final int dataSize = 36 + sampleDataLength; + /* + * Represents how many bytes are streamed per second. With all of the data above, it's trivial to + * calculate by getting the sample rate, the amount of channels and bytes per sample (bitsPerSample / 8) + */ + final int bytesPerSec = sampleRate * numChannels * (bitsPerSample / 8); + + /* NOTE: ChunkSize includes the header, so sampleDataLength + 44, which is the byte size of our header */ /* ChunkID */ - buffer[0] = (byte) (chunk & 0xff); - buffer[1] = (byte) ((chunk >>> 8) & 0xff); - buffer[2] = (byte) ((chunk >>> 16) & 0xff); - buffer[3] = (byte) ((chunk >>> 24) & 0xff); + ByteBuffer.wrap(buffer, 0, 4).order(ByteOrder.BIG_ENDIAN).putInt(chunk); /* ChunkSize (or File size) */ - buffer[4] = (byte) (dataSize & 0xff); - buffer[5] = (byte) ((dataSize >> 8) & 0xff); - buffer[6] = (byte) ((dataSize >> 16) & 0xff); - buffer[7] = (byte) ((dataSize >> 24) & 0xff); + ByteBuffer.wrap(buffer, 4, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(sampleDataLength + 44); /* Format (WAVE) */ - buffer[8] = (byte) (format & 0xff); - buffer[9] = (byte) ((format >>> 8 ) & 0xff); - buffer[10] = (byte) ((format >>> 16) & 0xff); - buffer[11] = (byte) ((format >>> 24) & 0xff); + ByteBuffer.wrap(buffer, 8, 4).order(ByteOrder.BIG_ENDIAN).putInt(format); /* SubchunkID (fmt) */ - buffer[12] = (byte) (subChunk1 & 0xff); - buffer[13] = (byte) ((subChunk1 >>> 8) & 0xff); - buffer[14] = (byte) ((subChunk1 >>> 16) & 0xff); - buffer[15] = (byte) ((subChunk1 >>> 24) & 0xff); + ByteBuffer.wrap(buffer, 12, 4).order(ByteOrder.BIG_ENDIAN).putInt(subChunk1); /* SubchunkSize (or format chunk size) */ - buffer[16] = (byte) (subChunkSize & 0xff); - buffer[17] = (byte) ((subChunkSize >> 8) & 0xff); - buffer[18] = (byte) ((subChunkSize >> 16) & 0xff); - buffer[19] = (byte) ((subChunkSize >> 24) & 0xff); + ByteBuffer.wrap(buffer, 16, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(subChunkSize); /* Audioformat */ - buffer[20] = (byte) (audioFormat & 0xff); - buffer[21] = (byte) ((audioFormat >> 8) & 0xff); + ByteBuffer.wrap(buffer, 20, 2).order(ByteOrder.LITTLE_ENDIAN).putShort(audioFormat); /* NumChannels (will be the same as source ADPCM) */ - buffer[22] = (byte) (numChannels & 0xff); - buffer[23] = (byte) ((numChannels >> 8) & 0xff); + ByteBuffer.wrap(buffer, 22, 2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) numChannels); /* SampleRate (will be the same as source ADPCM) */ - buffer[24] = (byte) (sampleRate & 0xff); - buffer[25] = (byte) ((sampleRate >> 8) & 0xff); - buffer[26] = (byte) ((sampleRate >> 16) & 0xff); - buffer[27] = (byte) ((sampleRate >> 24) & 0xff); + ByteBuffer.wrap(buffer, 24, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(sampleRate); /* ByteRate (BytesPerSec) */ - buffer[28] = (byte) (bytesPerSec & 0xff); - buffer[29] = (byte) ((bytesPerSec >> 8) & 0xff); - buffer[30] = (byte) ((bytesPerSec >> 16) & 0xff); - buffer[31] = (byte) ((bytesPerSec >> 24) & 0xff); + ByteBuffer.wrap(buffer, 28, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(bytesPerSec); /* BlockAlign (Frame Size) */ - buffer[32] = (byte) (frameSize & 0xff); - buffer[33] = (byte) ((frameSize >> 8) & 0xff); + ByteBuffer.wrap(buffer, 32, 2).order(ByteOrder.LITTLE_ENDIAN).putShort(frameSize); /* BitsPerSample */ - buffer[34] = (byte) (bitsPerSample & 0xff); - buffer[35] = (byte) ((bitsPerSample >> 8) & 0xff); + ByteBuffer.wrap(buffer, 34, 2).order(ByteOrder.LITTLE_ENDIAN).putShort(bitsPerSample); /* Subchunk2ID (data) */ - buffer[36] = (byte) (subChunk2 & 0xff); - buffer[37] = (byte) ((subChunk2 >>> 8) & 0xff); - buffer[38] = (byte) ((subChunk2 >>> 16) & 0xff); - buffer[39] = (byte) ((subChunk2 >>> 24) & 0xff); + ByteBuffer.wrap(buffer, 36, 4).order(ByteOrder.BIG_ENDIAN).putInt(subChunk2); /* Subchunk2 Size (sampledata length) */ - buffer[40] = (byte) (sampleDataLength & 0xff); - buffer[41] = (byte) ((sampleDataLength >> 8) & 0xff); - buffer[42] = (byte) ((sampleDataLength >> 16) & 0xff); - buffer[43] = (byte) ((sampleDataLength >> 24) & 0xff); + ByteBuffer.wrap(buffer, 40, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(sampleDataLength); } /* Decode the received IMA WAV ADPCM stream into a signed PCM16LE byte array, then return it to PlatformPlayer. */ public ByteArrayInputStream decodeImaAdpcm(InputStream stream, int[] wavHeaderData) throws IOException { - byte[] input = new byte[stream.available()]; + final byte[] input = new byte[stream.available()]; readInputStreamData(stream, input, 0, stream.available()); byte[] output = decodeADPCM(input, input.length, wavHeaderData[2], wavHeaderData[3]); - buildHeader(output, wavHeaderData[2], wavHeaderData[1]); /* Builds a new header for the decoded stream. */ + buildHeader(output, (short) wavHeaderData[2], wavHeaderData[1]); /* Builds a new header for the decoded stream. */ return new ByteArrayInputStream(output); } From 5f688473cd15538a0c80c9021fdf3578db2a7334 Mon Sep 17 00:00:00 2001 From: AShiningRay Date: Wed, 9 Aug 2023 00:41:37 -0300 Subject: [PATCH 6/6] ImaAdpcmDecoder: Reset the stream after reading its header. Failing to reset the stream will make FreeJ2ME load PCM files without their header, which can cause issues with media playback. The header only needs to be removed for IMA ADPCM files, as those have to be decoded into PCM. --- src/org/recompile/mobile/PlatformPlayer.java | 5 ++++- src/org/recompile/mobile/WavImaAdpcmDecoder.java | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/org/recompile/mobile/PlatformPlayer.java b/src/org/recompile/mobile/PlatformPlayer.java index 0f0b3ec8..960abb56 100644 --- a/src/org/recompile/mobile/PlatformPlayer.java +++ b/src/org/recompile/mobile/PlatformPlayer.java @@ -294,9 +294,12 @@ public wavPlayer(InputStream stream) /* * A wav header is generally 44-bytes long, and it is what we need to read in order to get * the stream's format, frame size, bit rate, number of channels, etc. which gives us information - * on the kind of codec needed to play or decode the incoming stream. + * on the kind of codec needed to play or decode the incoming stream. The stream needs to be reset + * or else PCM files will be loaded without a header and it might cause issues with playback. */ + stream.mark(48); wavHeaderData = adpcmDec.readHeader(stream); + stream.reset(); /* We only check for IMA ADPCM at the moment. */ if(wavHeaderData[0] != 17) /* If it's not IMA ADPCM we don't need to do anything to the stream. */ diff --git a/src/org/recompile/mobile/WavImaAdpcmDecoder.java b/src/org/recompile/mobile/WavImaAdpcmDecoder.java index 6e3e7b79..426b9855 100644 --- a/src/org/recompile/mobile/WavImaAdpcmDecoder.java +++ b/src/org/recompile/mobile/WavImaAdpcmDecoder.java @@ -416,6 +416,9 @@ private void buildHeader(final byte[] buffer, final short numChannels, final int /* Decode the received IMA WAV ADPCM stream into a signed PCM16LE byte array, then return it to PlatformPlayer. */ public ByteArrayInputStream decodeImaAdpcm(InputStream stream, int[] wavHeaderData) throws IOException { + /* Remove the header from the stream, we shouldn't "decode" it as if it was a sample */ + readHeader(stream); + final byte[] input = new byte[stream.available()]; readInputStreamData(stream, input, 0, stream.available());