Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/kotlin/cc/suffro/bpmanalyzer/wav/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
68 changes: 59 additions & 9 deletions src/main/kotlin/cc/suffro/bpmanalyzer/wav/WAVReader.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package cc.suffro.bpmanalyzer.wav

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
Expand Down Expand Up @@ -37,19 +39,22 @@ object WAVReader : FileReader<Wav> {
val blockAlign = input.readAsShort()
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)
val dataChunkSize = input.readAsInt()
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,
Expand All @@ -60,10 +65,15 @@ object WAVReader : FileReader<Wav> {
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)),
)
}

Expand Down Expand Up @@ -132,7 +142,7 @@ object WAVReader : FileReader<Wav> {

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))
Expand Down Expand Up @@ -167,4 +177,44 @@ object WAVReader : FileReader<Wav> {
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,
)
}
}
65 changes: 63 additions & 2 deletions src/main/kotlin/cc/suffro/bpmanalyzer/wav/WavWriter.kt
Original file line number Diff line number Diff line change
@@ -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<Wav> {
override fun write(
Expand Down Expand Up @@ -53,6 +58,17 @@ object WavWriter : FileWriter<Wav> {
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(
Expand All @@ -64,12 +80,13 @@ object WavWriter : FileWriter<Wav> {
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)
Expand All @@ -80,6 +97,41 @@ object WavWriter : FileWriter<Wav> {
}
}

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}")
}
}
Expand All @@ -96,6 +148,15 @@ object WavWriter : FileWriter<Wav> {
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,
Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Error.kt
Original file line number Diff line number Diff line change
@@ -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."
}
Expand Down
48 changes: 48 additions & 0 deletions src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/FmtChunks.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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,
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(
val cbSize: Short,
val validBitsPerSample: Short,
val channelMask: Int,
val subFormat: ByteBuffer,
val factChunkSize: Int,
val factSampleLength: Int,
)
11 changes: 0 additions & 11 deletions src/main/kotlin/cc/suffro/bpmanalyzer/wav/data/Wav.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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.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
Expand All @@ -33,7 +33,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)),
)

Expand All @@ -49,7 +49,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),
)

Expand Down
7 changes: 4 additions & 3 deletions src/test/kotlin/cc/suffro/bpmanalyzer/wav/WAVReaderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -153,7 +154,7 @@ class WAVReaderTest : BaseTest() {
),
Arguments.of(
"src/test/resources/samples/440.wav",
FmtChunk(
PcmFmtChunk(
riffChunkSize = 880110,
fmtChunkSize = 16,
audioFormat = AudioFormat.PCM,
Expand All @@ -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,
Expand Down