From af365d7a0378a56a073d581b0d37bcab2bc327bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophia=20K=C3=B6hler?= Date: Sun, 30 Mar 2025 23:07:44 +0200 Subject: [PATCH 1/3] Add Wave Extensible Chunks --- .../cc/suffro/bpmanalyzer/wav/Constants.kt | 1 + .../cc/suffro/bpmanalyzer/wav/WAVReader.kt | 64 +++++++++++++++++-- .../cc/suffro/bpmanalyzer/wav/data/Error.kt | 6 ++ .../suffro/bpmanalyzer/wav/data/FmtChunks.kt | 49 ++++++++++++++ .../cc/suffro/bpmanalyzer/wav/data/Wav.kt | 11 ---- .../speedadjustment/SpeedAdjusterTest.kt | 5 +- .../suffro/bpmanalyzer/wav/WAVReaderTest.kt | 7 +- 7 files changed, 120 insertions(+), 23 deletions(-) create mode 100644 src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/Constants.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/Constants.kt index c8c33e5..58eb365 100644 --- a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/Constants.kt +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/Constants.kt @@ -3,6 +3,7 @@ package cc.suffro.bpmanalyzer.wav const val RIFF_HEADER_CHUNK_SIZE = 4 const val RIFF_SIGNATURE = "RIFF" const val WAVE_SIGNATURE = "WAVE" +const val FACT_SIGNATURE = "fact" const val FMT_SIGNATURE = "fmt " const val DATA_SIGNATURE = "data" diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt index d688923..831e6c2 100644 --- a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt @@ -1,13 +1,18 @@ package cc.suffro.bpmanalyzer.wav +import cc.suffro.bpmanalyzer.wav.WAVReader.readAsInt +import cc.suffro.bpmanalyzer.wav.WAVReader.readAsShort import cc.suffro.bpmanalyzer.wav.WAVReader.readBuffer import cc.suffro.bpmanalyzer.wav.data.AudioFormat import cc.suffro.bpmanalyzer.wav.data.DataChunk import cc.suffro.bpmanalyzer.wav.data.Error import cc.suffro.bpmanalyzer.wav.data.ErrorType +import cc.suffro.bpmanalyzer.wav.data.ExtensibleChunk import cc.suffro.bpmanalyzer.wav.data.FileReader import cc.suffro.bpmanalyzer.wav.data.FmtChunk +import cc.suffro.bpmanalyzer.wav.data.PcmFmtChunk import cc.suffro.bpmanalyzer.wav.data.Wav +import cc.suffro.bpmanalyzer.wav.data.WaveExtensibleFmtChunk import java.io.BufferedInputStream import java.io.InputStream import java.nio.ByteBuffer @@ -38,9 +43,10 @@ object WAVReader : FileReader { val bitsPerSample = input.readAsShort() // TODO: implement extensible wave format - if (audioFormat == AudioFormat.WAVE_FORMAT_EXTENSIBLE) { - val throwAway = input.readBuffer(36) - } + val extensibleChunk = if (audioFormat == AudioFormat.WAVE_FORMAT_EXTENSIBLE) { + input.readWaveExtensibleChunk(bitsPerSample, blockAlign, numChannels, riffChunkSize) + } else null + // data val dataSignature = String(input.readNBytes(4), Charsets.US_ASCII) check(dataSignature == DATA_SIGNATURE, ErrorType.UNEXPECTED_DATA_SIGNATURE) @@ -48,8 +54,8 @@ object WAVReader : FileReader { val data = input.readNBytes(dataChunkSize) check(data.size == dataChunkSize, ErrorType.WRONG_DATA_SIZE) - val fmtChunk = - FmtChunk( + val pcmFmtChunk = + PcmFmtChunk( riffChunkSize = riffChunkSize, fmtChunkSize = fmtChunkSize, audioFormat = audioFormat, @@ -60,10 +66,14 @@ object WAVReader : FileReader { bitsPerSample = bitsPerSample, ) + val fmtChunk = extensibleChunk?.let { + WaveExtensibleFmtChunk(standardChunk = pcmFmtChunk, extensibleChunk = it) + } ?: pcmFmtChunk + Wav( filePath = path, fmtChunk = fmtChunk, - dataChunk = DataChunk(dataChunkSize, data.readSamples(fmtChunk)), + dataChunk = DataChunk(dataChunkSize, data.readSamples(pcmFmtChunk)), ) } @@ -132,7 +142,7 @@ object WAVReader : FileReader { private fun InputStream.readBuffer(size: Int): ByteBuffer = readNBytes(size) - ?.takeIf { it.size == size } + .takeIf { it.size == size } ?.let(ByteBuffer::wrap) ?.apply { order(ByteOrder.LITTLE_ENDIAN) } ?: throw ParsingException(Error(ErrorType.UNEXPECTED_EOF)) @@ -167,4 +177,44 @@ object WAVReader : FileReader { throw ParsingException(Error(error, lazyMessage())) } } + + private fun InputStream.readWaveExtensibleChunk( + bitsPerSample: Short, + blockAlign: Short, + numChannels: Short, + riffChunkSize: Int + ): ExtensibleChunk { + check(bitsPerSample.toInt() == 8 * blockAlign / numChannels, ErrorType.INVALID_W_BITS_PER_SAMPLE) + + val cbSize = readAsShort() + check(cbSize.toInt() == 22, ErrorType.INVALID_WAVE_FORMAT_EXTENSIBLE) + + val validBitsPerSample = readAsShort() + check(validBitsPerSample.toInt() == bitsPerSample.toInt(), ErrorType.INVALID_WAVE_FORMAT_EXTENSIBLE) + + val channelMask = readAsInt() + val subFormat = readBuffer(16) + check( + subFormat[0].toInt() == 0xFF && subFormat[1].toInt() == 0xFE, + ErrorType.INVALID_WAVE_FORMAT_EXTENSIBLE + ) + + val ckId = String(readNBytes(4), Charsets.US_ASCII) + check(ckId == FACT_SIGNATURE, ErrorType.UNEXPECTED_FMT_SIGNATURE) + + val factChunkSize = readAsInt() + check(factChunkSize == 4, ErrorType.UNEXPECTED_FMT_SIGNATURE) + + val factSampleLength = readAsInt() + check(factSampleLength == riffChunkSize / blockAlign * numChannels, ErrorType.UNEXPECTED_FMT_SIGNATURE) + + return ExtensibleChunk( + cbSize = cbSize, + validBitsPerSample = validBitsPerSample, + channelMask = channelMask, + subFormat = subFormat, + factChunkSize = factChunkSize, + factSampleLength = factSampleLength, + ) + } } diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Error.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Error.kt index 1198fb5..3311d61 100644 --- a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Error.kt +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Error.kt @@ -1,20 +1,26 @@ package cc.suffro.bpmanalyzer.wav.data internal enum class ErrorType { + INVALID_WAVE_FORMAT_EXTENSIBLE, + INVALID_W_BITS_PER_SAMPLE, NOT_A_RIFF, NOT_A_WAV, UNEXPECTED_DATA_SIGNATURE, UNEXPECTED_EOF, + UNEXPECTED_FACT_SIGNATURE, UNEXPECTED_FMT_SIGNATURE, WRONG_DATA_SIZE, ; override fun toString() = when (this) { + INVALID_WAVE_FORMAT_EXTENSIBLE -> "Invalid WAVE format extensible. Expected 22 bytes." + INVALID_W_BITS_PER_SAMPLE -> "Invalid bits per sample. Expected 8, 16, or 24." NOT_A_RIFF -> "No RIFF header detected." NOT_A_WAV -> "RIFF is not of type WAV." UNEXPECTED_DATA_SIGNATURE -> "File contains invalid data signature." UNEXPECTED_EOF -> "RIFF file ended unexpectedly." + UNEXPECTED_FACT_SIGNATURE -> "File contains invalid fact signature." UNEXPECTED_FMT_SIGNATURE -> "File contains invalid fmt signature." WRONG_DATA_SIZE -> "Data Chunk doesn't have the required size." } diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt new file mode 100644 index 0000000..b60faf1 --- /dev/null +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt @@ -0,0 +1,49 @@ +package cc.suffro.bpmanalyzer.wav.data + +import java.nio.ByteBuffer + +sealed class FmtChunk { + abstract val riffChunkSize: Int + abstract val fmtChunkSize: Int + abstract val audioFormat: AudioFormat + abstract val numChannels: Short + abstract val sampleRate: Int + abstract val byteRate: Int + abstract val blockAlign: Short + abstract val bitsPerSample: Short +} + +data class PcmFmtChunk( + override val riffChunkSize: Int, + override val fmtChunkSize: Int, + override val audioFormat: AudioFormat, + override val numChannels: Short, + override val sampleRate: Int, + override val byteRate: Int, + override val blockAlign: Short, + override val bitsPerSample: Short, +) : FmtChunk() + +data class WaveExtensibleFmtChunk( + private val standardChunk: PcmFmtChunk, + private val extensibleChunk: ExtensibleChunk, +) : FmtChunk() { + + override val riffChunkSize: Int get() = standardChunk.riffChunkSize + override val fmtChunkSize: Int get() = standardChunk.fmtChunkSize + override val audioFormat: AudioFormat get() = standardChunk.audioFormat + override val numChannels: Short get() = standardChunk.numChannels + override val sampleRate: Int get() = standardChunk.sampleRate + override val byteRate: Int get() = standardChunk.byteRate + override val blockAlign: Short get() = standardChunk.blockAlign + override val bitsPerSample: Short get() = standardChunk.bitsPerSample +} + +data class ExtensibleChunk( + private val cbSize: Short, + private val validBitsPerSample: Short, + private val channelMask: Int, + private val subFormat: ByteBuffer, + private val factChunkSize: Int, + private val factSampleLength: Int +) \ No newline at end of file diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Wav.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Wav.kt index dfe8606..00eb2ee 100644 --- a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Wav.kt +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Wav.kt @@ -28,17 +28,6 @@ data class DataChunk( } } -data class FmtChunk( - val riffChunkSize: Int, - val fmtChunkSize: Int, - val audioFormat: AudioFormat, - val numChannels: Short, - val sampleRate: Int, - val byteRate: Int, - val blockAlign: Short, - val bitsPerSample: Short, -) - data class Wav( val filePath: Path, val fmtChunk: FmtChunk, diff --git a/src/test/kotlin/cc/suffro/bpmanalyzer/speedadjustment/SpeedAdjusterTest.kt b/src/test/kotlin/cc/suffro/bpmanalyzer/speedadjustment/SpeedAdjusterTest.kt index b65fddf..fe263c6 100644 --- a/src/test/kotlin/cc/suffro/bpmanalyzer/speedadjustment/SpeedAdjusterTest.kt +++ b/src/test/kotlin/cc/suffro/bpmanalyzer/speedadjustment/SpeedAdjusterTest.kt @@ -7,6 +7,7 @@ import cc.suffro.bpmanalyzer.wav.data.DataChunk import cc.suffro.bpmanalyzer.wav.data.FileReader import cc.suffro.bpmanalyzer.wav.data.FileWriter import cc.suffro.bpmanalyzer.wav.data.FmtChunk +import cc.suffro.bpmanalyzer.wav.data.PcmFmtChunk import cc.suffro.bpmanalyzer.wav.data.Wav import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test @@ -33,7 +34,7 @@ class SpeedAdjusterTest : BaseTest() { val wav = Wav( filePath = Path.of("dummy.wav"), - fmtChunk = FmtChunk(1, 1, AudioFormat.PCM, 1, 1, 1, 1, 1), + fmtChunk = PcmFmtChunk(1, 1, AudioFormat.PCM, 1, 1, 1, 1, 1), dataChunk = DataChunk(0, arrayOf(data)), ) @@ -49,7 +50,7 @@ class SpeedAdjusterTest : BaseTest() { val wav = Wav( filePath = Path.of("dummy.wav"), - fmtChunk = FmtChunk(1, 1, AudioFormat.PCM, 2, 1, 1, 1, 1), + fmtChunk = PcmFmtChunk(1, 1, AudioFormat.PCM, 2, 1, 1, 1, 1), dataChunk = DataChunk(0, data), ) diff --git a/src/test/kotlin/cc/suffro/bpmanalyzer/wav/WAVReaderTest.kt b/src/test/kotlin/cc/suffro/bpmanalyzer/wav/WAVReaderTest.kt index 610982c..f540e32 100644 --- a/src/test/kotlin/cc/suffro/bpmanalyzer/wav/WAVReaderTest.kt +++ b/src/test/kotlin/cc/suffro/bpmanalyzer/wav/WAVReaderTest.kt @@ -9,6 +9,7 @@ import cc.suffro.bpmanalyzer.wav.data.AudioFormat import cc.suffro.bpmanalyzer.wav.data.DataChunk import cc.suffro.bpmanalyzer.wav.data.FileReader import cc.suffro.bpmanalyzer.wav.data.FmtChunk +import cc.suffro.bpmanalyzer.wav.data.PcmFmtChunk import cc.suffro.bpmanalyzer.wav.data.Wav import cc.suffro.bpmanalyzer.wav.data.WindowProcessingParams import org.junit.jupiter.api.Disabled @@ -139,7 +140,7 @@ class WAVReaderTest : BaseTest() { Stream.of( Arguments.of( "src/test/resources/samples/220.wav", - FmtChunk( + PcmFmtChunk( riffChunkSize = 654006, fmtChunkSize = 16, audioFormat = AudioFormat.PCM, @@ -153,7 +154,7 @@ class WAVReaderTest : BaseTest() { ), Arguments.of( "src/test/resources/samples/440.wav", - FmtChunk( + PcmFmtChunk( riffChunkSize = 880110, fmtChunkSize = 16, audioFormat = AudioFormat.PCM, @@ -179,7 +180,7 @@ class WAVReaderTest : BaseTest() { Stream.of( Arguments.of( "src/test/resources/samples/kick_140_24PCM.wav", - FmtChunk( + PcmFmtChunk( riffChunkSize = 1542852, fmtChunkSize = 16, audioFormat = AudioFormat.PCM, From f229ff48468f6e074d0e6833d4fbd6f2502ac837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophia=20K=C3=B6hler?= Date: Sun, 30 Mar 2025 23:10:42 +0200 Subject: [PATCH 2/3] :art: fix linting --- .../cc/suffro/bpmanalyzer/wav/WAVReader.kt | 20 +++++++++++-------- .../suffro/bpmanalyzer/wav/data/FmtChunks.kt | 5 ++--- .../speedadjustment/SpeedAdjusterTest.kt | 1 - 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt index 831e6c2..6c329e7 100644 --- a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt @@ -43,9 +43,12 @@ object WAVReader : FileReader { val bitsPerSample = input.readAsShort() // TODO: implement extensible wave format - val extensibleChunk = if (audioFormat == AudioFormat.WAVE_FORMAT_EXTENSIBLE) { - input.readWaveExtensibleChunk(bitsPerSample, blockAlign, numChannels, riffChunkSize) - } else null + val extensibleChunk = + if (audioFormat == AudioFormat.WAVE_FORMAT_EXTENSIBLE) { + input.readWaveExtensibleChunk(bitsPerSample, blockAlign, numChannels, riffChunkSize) + } else { + null + } // data val dataSignature = String(input.readNBytes(4), Charsets.US_ASCII) @@ -66,9 +69,10 @@ object WAVReader : FileReader { bitsPerSample = bitsPerSample, ) - val fmtChunk = extensibleChunk?.let { - WaveExtensibleFmtChunk(standardChunk = pcmFmtChunk, extensibleChunk = it) - } ?: pcmFmtChunk + val fmtChunk = + extensibleChunk?.let { + WaveExtensibleFmtChunk(standardChunk = pcmFmtChunk, extensibleChunk = it) + } ?: pcmFmtChunk Wav( filePath = path, @@ -182,7 +186,7 @@ object WAVReader : FileReader { bitsPerSample: Short, blockAlign: Short, numChannels: Short, - riffChunkSize: Int + riffChunkSize: Int, ): ExtensibleChunk { check(bitsPerSample.toInt() == 8 * blockAlign / numChannels, ErrorType.INVALID_W_BITS_PER_SAMPLE) @@ -196,7 +200,7 @@ object WAVReader : FileReader { val subFormat = readBuffer(16) check( subFormat[0].toInt() == 0xFF && subFormat[1].toInt() == 0xFE, - ErrorType.INVALID_WAVE_FORMAT_EXTENSIBLE + ErrorType.INVALID_WAVE_FORMAT_EXTENSIBLE, ) val ckId = String(readNBytes(4), Charsets.US_ASCII) diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt index b60faf1..3df7d99 100644 --- a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt @@ -28,7 +28,6 @@ data class WaveExtensibleFmtChunk( private val standardChunk: PcmFmtChunk, private val extensibleChunk: ExtensibleChunk, ) : FmtChunk() { - override val riffChunkSize: Int get() = standardChunk.riffChunkSize override val fmtChunkSize: Int get() = standardChunk.fmtChunkSize override val audioFormat: AudioFormat get() = standardChunk.audioFormat @@ -45,5 +44,5 @@ data class ExtensibleChunk( private val channelMask: Int, private val subFormat: ByteBuffer, private val factChunkSize: Int, - private val factSampleLength: Int -) \ No newline at end of file + private val factSampleLength: Int, +) diff --git a/src/test/kotlin/cc/suffro/bpmanalyzer/speedadjustment/SpeedAdjusterTest.kt b/src/test/kotlin/cc/suffro/bpmanalyzer/speedadjustment/SpeedAdjusterTest.kt index fe263c6..394a2b2 100644 --- a/src/test/kotlin/cc/suffro/bpmanalyzer/speedadjustment/SpeedAdjusterTest.kt +++ b/src/test/kotlin/cc/suffro/bpmanalyzer/speedadjustment/SpeedAdjusterTest.kt @@ -6,7 +6,6 @@ import cc.suffro.bpmanalyzer.wav.data.AudioFormat import cc.suffro.bpmanalyzer.wav.data.DataChunk import cc.suffro.bpmanalyzer.wav.data.FileReader import cc.suffro.bpmanalyzer.wav.data.FileWriter -import cc.suffro.bpmanalyzer.wav.data.FmtChunk import cc.suffro.bpmanalyzer.wav.data.PcmFmtChunk import cc.suffro.bpmanalyzer.wav.data.Wav import org.junit.jupiter.api.Disabled From ac1af849c24eeec3ae891fc2913e97f5789623ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophia=20K=C3=B6hler?= Date: Sun, 6 Apr 2025 00:34:17 +0200 Subject: [PATCH 3/3] :cxonstruction: add limits for data types to avoid errors when casted --- .../cc/suffro/bpmanalyzer/wav/WAVReader.kt | 4 -- .../cc/suffro/bpmanalyzer/wav/WavWriter.kt | 65 ++++++++++++++++++- .../suffro/bpmanalyzer/wav/data/FmtChunks.kt | 14 ++-- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt index 6c329e7..cdff26f 100644 --- a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt @@ -1,8 +1,5 @@ package cc.suffro.bpmanalyzer.wav -import cc.suffro.bpmanalyzer.wav.WAVReader.readAsInt -import cc.suffro.bpmanalyzer.wav.WAVReader.readAsShort -import cc.suffro.bpmanalyzer.wav.WAVReader.readBuffer import cc.suffro.bpmanalyzer.wav.data.AudioFormat import cc.suffro.bpmanalyzer.wav.data.DataChunk import cc.suffro.bpmanalyzer.wav.data.Error @@ -42,7 +39,6 @@ object WAVReader : FileReader { val blockAlign = input.readAsShort() val bitsPerSample = input.readAsShort() - // TODO: implement extensible wave format val extensibleChunk = if (audioFormat == AudioFormat.WAVE_FORMAT_EXTENSIBLE) { input.readWaveExtensibleChunk(bitsPerSample, blockAlign, numChannels, riffChunkSize) diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WavWriter.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WavWriter.kt index 96c8e1f..d9f66dc 100644 --- a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WavWriter.kt +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/WavWriter.kt @@ -1,15 +1,20 @@ package cc.suffro.bpmanalyzer.wav +import cc.suffro.bpmanalyzer.wav.data.AudioFormat import cc.suffro.bpmanalyzer.wav.data.DataChunk import cc.suffro.bpmanalyzer.wav.data.FileWriter import cc.suffro.bpmanalyzer.wav.data.FmtChunk import cc.suffro.bpmanalyzer.wav.data.Wav +import cc.suffro.bpmanalyzer.wav.data.WaveExtensibleFmtChunk import java.io.BufferedOutputStream import java.io.OutputStream +import java.math.BigDecimal +import java.math.BigInteger import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.file.Files import java.nio.file.Path +import kotlin.math.min object WavWriter : FileWriter { override fun write( @@ -53,6 +58,17 @@ object WavWriter : FileWriter { output.write(intToByteArray(fmtChunk.byteRate)) output.write(shortToByteArray(fmtChunk.blockAlign)) output.write(shortToByteArray(fmtChunk.bitsPerSample)) + + if (fmtChunk.audioFormat == AudioFormat.WAVE_FORMAT_EXTENSIBLE) { + val extensibleFmtChunk = (fmtChunk as WaveExtensibleFmtChunk).extensibleChunk + output.write(shortToByteArray(extensibleFmtChunk.cbSize)) + output.write(shortToByteArray(extensibleFmtChunk.validBitsPerSample)) + output.write(intToByteArray(extensibleFmtChunk.channelMask)) + output.write(bufferToByteArray(extensibleFmtChunk.subFormat, 16)) + output.write(FACT_SIGNATURE.toByteArray(Charsets.US_ASCII)) + output.write(intToByteArray(extensibleFmtChunk.factChunkSize)) + output.write(intToByteArray(extensibleFmtChunk.factSampleLength)) + } } private fun writeDataChunk( @@ -64,12 +80,13 @@ object WavWriter : FileWriter { output.write(intToByteArray(dataChunk.dataChunkSize)) val sampleCount = dataChunk.dataChunkSize / fmtChunk.blockAlign - // TODO: add 24 and 32 bits per sample when (fmtChunk.bitsPerSample.toInt()) { 16 -> { for (sampleIndex in 0 until sampleCount) { for (channel in 0 until fmtChunk.numChannels) { - val shortValue = (dataChunk.data[channel][sampleIndex] * Short.MAX_VALUE).toInt().toShort() + val intValue = + min((dataChunk.data[channel][sampleIndex] * Short.MAX_VALUE).toInt(), Int.MAX_VALUE) + val shortValue = intValue.toShort() val sampleBytes = ByteBuffer.allocate(Short.SIZE_BYTES).apply { order(ByteOrder.LITTLE_ENDIAN) @@ -80,6 +97,41 @@ object WavWriter : FileWriter { } } + 24 -> { + for (sampleIndex in 0 until sampleCount) { + for (channel in 0 until fmtChunk.numChannels) { + val intValue = + min((dataChunk.data[channel][sampleIndex] * MAX_VALUE_24BIT).toInt(), MAX_VALUE_24BIT) + val sampleBytes = + byteArrayOf( + intValue.toByte(), + (intValue shr 8).toByte(), + (intValue shr 16).toByte(), + ) + output.write(sampleBytes) + } + } + } + + 32 -> { + for (sampleIndex in 0 until sampleCount) { + for (channel in 0 until fmtChunk.numChannels) { + val intAsBigInteger = + BigInteger.valueOf((dataChunk.data[channel][sampleIndex] * Int.MAX_VALUE).toLong()) + val bigDecimal = BigDecimal.valueOf(dataChunk.data[channel][sampleIndex]) + val multiplied = intAsBigInteger.toBigDecimal().multiply(bigDecimal) + val intValue = multiplied.toInt() + + val sampleBytes = + ByteBuffer.allocate(Int.SIZE_BYTES).apply { + order(ByteOrder.LITTLE_ENDIAN) + putInt(intValue) + }.array() + output.write(sampleBytes) + } + } + } + else -> throw IllegalArgumentException("Unsupported bits per sample: ${fmtChunk.bitsPerSample}") } } @@ -96,6 +148,15 @@ object WavWriter : FileWriter { putShort(value) }.array() + private fun bufferToByteArray( + buffer: ByteBuffer, + size: Int, + ): ByteArray = + ByteBuffer.allocate(size).apply { + order(ByteOrder.LITTLE_ENDIAN) + put(buffer) + }.array() + private fun sampleToBytes( sample: Double, bitsPerSample: Int, diff --git a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt index 3df7d99..47df153 100644 --- a/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt +++ b/src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt @@ -26,7 +26,7 @@ data class PcmFmtChunk( data class WaveExtensibleFmtChunk( private val standardChunk: PcmFmtChunk, - private val extensibleChunk: ExtensibleChunk, + val extensibleChunk: ExtensibleChunk, ) : FmtChunk() { override val riffChunkSize: Int get() = standardChunk.riffChunkSize override val fmtChunkSize: Int get() = standardChunk.fmtChunkSize @@ -39,10 +39,10 @@ data class WaveExtensibleFmtChunk( } data class ExtensibleChunk( - private val cbSize: Short, - private val validBitsPerSample: Short, - private val channelMask: Int, - private val subFormat: ByteBuffer, - private val factChunkSize: Int, - private val factSampleLength: Int, + val cbSize: Short, + val validBitsPerSample: Short, + val channelMask: Int, + val subFormat: ByteBuffer, + val factChunkSize: Int, + val factSampleLength: Int, )