Skip to content

Commit

Permalink
Fix infinite loop on mp3 playback with times >= 2.playbackTimes, and …
Browse files Browse the repository at this point in the history
…some more WAV and MP3 fixes (#802)
  • Loading branch information
soywiz authored Jul 12, 2022
1 parent 585865d commit c216d17
Show file tree
Hide file tree
Showing 17 changed files with 244 additions and 43 deletions.
8 changes: 8 additions & 0 deletions kds/src/commonMain/kotlin/com/soywiz/kds/ArrayDeque.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ class ShortArrayDeque(val initialBits: Int = 10) {
val availableWriteWithoutAllocating get() = ring.availableWrite
val availableRead get() = ring.availableRead

fun clone(): ShortArrayDeque {
return ShortArrayDeque(initialBits).also { out ->
out.ring = ring.clone()
out.written = written
out.read = read
}
}

@JvmOverloads
fun writeHead(buffer: ShortArray, offset: Int = 0, size: Int = buffer.size - offset): Int {
val out = ensureWrite(size).ring.writeHead(buffer, offset, size)
Expand Down
10 changes: 10 additions & 0 deletions kds/src/commonMain/kotlin/com/soywiz/kds/RingBuffer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ class ShortRingBuffer(val bits: Int) {
var availableWrite = totalSize; private set
var availableRead = 0; private set

fun clone(): ShortRingBuffer {
return ShortRingBuffer(bits).also { out ->
arraycopy(buffer, 0, out.buffer, 0, buffer.size)
out.readPos = readPos
out.writePos = writePos
out.availableWrite = availableWrite
out.availableRead = availableRead
}
}

@JvmOverloads
fun writeHead(data: ShortArray, offset: Int = 0, size: Int = data.size - offset): Int {
val toWrite = min(availableWrite, size)
Expand Down
3 changes: 3 additions & 0 deletions kds/src/commonMain/kotlin/com/soywiz/kds/internal/internal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ internal fun <T> arraycopy(src: Array<T>, srcPos: Int, dst: Array<T>, dstPos: In
internal fun arraycopy(src: ByteArray, srcPos: Int, dst: ByteArray, dstPos: Int, size: Int) =
src.copyInto(dst, dstPos, srcPos, srcPos + size)

internal fun arraycopy(src: ShortArray, srcPos: Int, dst: ShortArray, dstPos: Int, size: Int) =
src.copyInto(dst, dstPos, srcPos, srcPos + size)

internal fun arraycopy(src: IntArray, srcPos: Int, dst: IntArray, dstPos: Int, size: Int) =
src.copyInto(dst, dstPos, srcPos, srcPos + size)

Expand Down
15 changes: 9 additions & 6 deletions korau/src/commonMain/kotlin/com/soywiz/korau/format/WAV.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,11 @@ open class WAV : AudioFormat("wav") {
}

override suspend fun encode(data: AudioData, out: AsyncOutputStream, filename: String, props: AudioEncodingProps) {
val bytesPerSample = 2

// HEADER
out.writeString("RIFF")
out.write32LE(0x24 + data.samples.totalSamples * 2) // length
out.write32LE(0x24 + data.samples.totalSamples * bytesPerSample * data.channels) // length
out.writeString("WAVE")

// FMT
Expand All @@ -114,14 +116,15 @@ open class WAV : AudioFormat("wav") {
out.write16LE(1) // PCM
out.write16LE(data.channels) // Channels
out.write32LE(data.rate) // SamplesPerSec
out.write32LE(data.rate * data.channels * 2) // AvgBytesPerSec
out.write16LE(2) // BlockAlign
out.write16LE(16) // BitsPerSample
out.write32LE(data.rate * data.channels * bytesPerSample) // AvgBytesPerSec
out.write16LE(bytesPerSample) // BlockAlign
out.write16LE(bytesPerSample * 8) // BitsPerSample

// DATA
out.writeString("data")
out.write32LE(data.samples.totalSamples * 2)
out.writeShortArrayLE(data.samples.interleaved().data)
out.write32LE(data.samples.totalSamples * bytesPerSample * data.channels)
val array = data.samples.interleaved().data
out.writeShortArrayLE(array)
}

data class Fmt(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.soywiz.korau.format.mp3.minimp3

import com.soywiz.kds.ByteArrayDeque
import com.soywiz.klock.min
import com.soywiz.kmem.hasFlags
import com.soywiz.kmem.indexOf
import com.soywiz.korau.format.AudioDecodingProps
import com.soywiz.korau.format.AudioFormat
import com.soywiz.korau.format.MP3
Expand All @@ -10,6 +13,7 @@ import com.soywiz.korau.sound.AudioSamplesDeque
import com.soywiz.korau.sound.AudioStream
import com.soywiz.korio.lang.Closeable
import com.soywiz.korio.stream.AsyncStream
import com.soywiz.korio.stream.FastByteArrayInputStream
import com.soywiz.korio.stream.readBytesUpTo

abstract class BaseMinimp3AudioFormat : AudioFormat("mp3") {
Expand All @@ -22,7 +26,9 @@ abstract class BaseMinimp3AudioFormat : AudioFormat("mp3") {
}

private suspend fun createDecoderStream(data: AsyncStream, props: AudioDecodingProps, table: MP3Base.SeekingTable? = null): AudioStream {
val dataStartPosition = data.position
val decoder = createMp3Decoder()
decoder.reset()
val mp3SeekingTable: MP3Base.SeekingTable? = when (props.exactTimings) {
true -> table ?: (if (data.hasLength()) MP3Base.Parser(data, data.getLength()).getSeekingTable(44100) else null)
else -> null
Expand Down Expand Up @@ -51,19 +57,17 @@ abstract class BaseMinimp3AudioFormat : AudioFormat("mp3") {
override var currentPositionInSamples: Long
get() = _currentPositionInSamples
set(value) {
finished = false
if (mp3SeekingTable != null) {
decoder.pcmDeque!!.clear()
decoder.compressedData!!.clear()
data.position = mp3SeekingTable.locateSample(value)
_currentPositionInSamples = value
} else {
// @TODO: We should try to estimate by using decoder.bitrate_kbps

decoder.pcmDeque!!.clear()
decoder.compressedData!!.clear()
data.position = 0L
_currentPositionInSamples = 0L
}
decoder.reset()
}

override suspend fun read(out: AudioSamples, offset: Int, length: Int): Int {
Expand All @@ -76,22 +80,23 @@ abstract class BaseMinimp3AudioFormat : AudioFormat("mp3") {
}
}
//println("audioSamplesDeque!!.availableRead=${audioSamplesDeque!!.availableRead}")
if (noMoreSamples && decoder.pcmDeque!!.availableRead == 0) {
return if (noMoreSamples && decoder.pcmDeque!!.availableRead == 0) {
finished = true
return 0
}

return decoder.pcmDeque!!.read(out, offset, length).also {
_currentPositionInSamples += length
0
} else {
decoder.pcmDeque!!.read(out, offset, length)
}.also {
_currentPositionInSamples += it
//println(" -> $it")
//println("MP3.read: offset=$offset, length=$length, noMoreSamples=$noMoreSamples, finished=$finished")
}
}

override fun close() {
decoder.close()
}

override suspend fun clone(): AudioStream = createDecoderStream(data, props, table)
override suspend fun clone(): AudioStream = createDecoderStream(data.duplicate().also { it.position = dataStartPosition }, props, table)
}
}

Expand All @@ -105,19 +110,98 @@ abstract class BaseMinimp3AudioFormat : AudioFormat("mp3") {
var pcmDeque: AudioSamplesDeque?
val samples: Int
val frame_bytes: Int
var skipRemaining: Int
var samplesAvailable: Int
var samplesRead: Int
fun decodeFrame(availablePeek: Int): ShortArray?
fun reset() {
pcmDeque?.clear()
compressedData.clear()
skipRemaining = 0
samplesAvailable = -1
samplesRead = 0
}
fun step(): Boolean {
val availablePeek = compressedData.peek(tempBuffer, 0, tempBuffer.size)
val xingIndex = tempBuffer.indexOf(XingTag).takeIf { it >= 0 } ?: tempBuffer.size
val infoIndex = tempBuffer.indexOf(InfoTag).takeIf { it >= 0 } ?: tempBuffer.size
var frames = 0
var delay = 0
var padding = 0

if (xingIndex < tempBuffer.size || infoIndex < tempBuffer.size) {
try {
val index = kotlin.math.min(xingIndex, infoIndex)
val data = FastByteArrayInputStream(tempBuffer, index)
data.skip(7)
if (data.available >= 1) {
val flags = data.readU8()
//println("xing=$xingIndex, infoIndex=$infoIndex, index=$index, flags=$flags")
val FRAMES_FLAG = 1
val BYTES_FLAG = 2
val TOC_FLAG = 4
val VBR_SCALE_FLAG = 8
if (flags.hasFlags(FRAMES_FLAG) && data.available >= 4) {
frames = data.readS32BE()
if (flags.hasFlags(BYTES_FLAG)) data.skip(4)
if (flags.hasFlags(TOC_FLAG)) data.skip(100)
if (flags.hasFlags(VBR_SCALE_FLAG)) data.skip(4)
if (data.available >= 1) {
val info = data.readU8()
if (info != 0) {
data.skip(20)
if (data.available >= 3) {
val t0 = data.readU8()
val t1 = data.readU8()
val t2 = data.readU8()
delay = ((t0 shl 4) or (t1 ushr 4)) + (528 + 1)
padding = (((t1 and 0xF) shl 8) or (t2)) - (528 + 1)
}
//println("frames=$frames, flags=$flags, delay=$delay, padding=$padding, $t0,$t1,$t2")
}
}
}
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
val buf = decodeFrame(availablePeek)

//println("samples=$samples, nchannels=$nchannels, hz=$hz, bitrate_kbps=$bitrate_kbps")
if (frames != 0 || delay != 0 || padding != 0) {
val rpadding = padding * nchannels
val to_skip = delay * nchannels
var detected_samples = samples * frames * nchannels
if (detected_samples >= to_skip) detected_samples -= to_skip
if (rpadding in 1..detected_samples) detected_samples -= rpadding
skipRemaining = to_skip + (samples * nchannels)
samplesAvailable = detected_samples
//println("frames=$frames, delay=$delay, padding=$padding :: rpadding=$rpadding, to_skip=$to_skip, detected_samples=$detected_samples")
}

//println("availablePeek=$availablePeek, frame_bytes=$frame_bytes, samples=$samples, nchannels=$nchannels, hz=$hz, bitrate_kbps=")

if (nchannels != 0 && pcmDeque == null) {
pcmDeque = AudioSamplesDeque(nchannels)
}

if (samples > 0) {
pcmDeque!!.writeInterleaved(buf!!, 0, samples * nchannels)
var offset = 0
var toRead = samples * nchannels

if (skipRemaining > 0) {
val skipNow = kotlin.math.min(skipRemaining, toRead)
offset += skipNow
toRead -= skipNow
skipRemaining -= skipNow
}
if (samplesAvailable >= 0) {
toRead = kotlin.math.min(toRead, samplesAvailable)
samplesAvailable -= toRead
}

//println("writeInterleaved. offset=$offset, toRead=$toRead")
pcmDeque!!.writeInterleaved(buf!!, offset, toRead)
}

//println("mp3decFrameInfo: samples=$samples, channels=${struct.channels}, frame_bytes=${struct.frame_bytes}, frame_offset=${struct.frame_offset}, bitrate_kbps=${struct.bitrate_kbps}, hz=${struct.hz}, layer=${struct.layer}")
Expand All @@ -135,4 +219,9 @@ abstract class BaseMinimp3AudioFormat : AudioFormat("mp3") {
return true
}
}

companion object {
val XingTag = byteArrayOf('X'.code.toByte(), 'i'.code.toByte(), 'n'.code.toByte(), 'g'.code.toByte())
val InfoTag = byteArrayOf('I'.code.toByte(), 'n'.code.toByte(), 'f'.code.toByte(), 'o'.code.toByte())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ internal object Minimp3AudioFormat : BaseMinimp3AudioFormat() {
override var nchannels = 0
override var samples: Int = 0
override var frame_bytes: Int = 0
override var skipRemaining: Int = 0
override var samplesAvailable: Int = 0
override var samplesRead: Int = 0

override fun decodeFrame(availablePeek: Int): ShortArray? {
memWrite(inputData, tempBuffer, 0, availablePeek)
Expand All @@ -55,6 +58,7 @@ internal object Minimp3AudioFormat : BaseMinimp3AudioFormat() {
bitrate_kbps = struct.bitrate_kbps
frame_bytes = struct.frame_bytes
this.samples = samples
//println("samples=$samples, hz=$hz, nchannels=$nchannels, bitrate_kbps=$bitrate_kbps, frameBytes=$frame_bytes")

if (nchannels == 0 || samples <= 0) return null

Expand Down
10 changes: 5 additions & 5 deletions korau/src/commonMain/kotlin/com/soywiz/korau/sound/AudioData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ class AudioData(
val DUMMY by lazy { AudioData(44100, AudioSamples(2, 0)) }
}

val stereo get() = channels > 1
val channels get() = samples.channels
val totalSamples get() = samples.totalSamples
val stereo: Boolean get() = channels > 1
val channels: Int get() = samples.channels
val totalSamples: Int get() = samples.totalSamples
val totalTime: TimeSpan get() = timeAtSample(totalSamples)
fun timeAtSample(sample: Int) = ((sample).toDouble() / rate.toDouble()).seconds
fun timeAtSample(sample: Int): TimeSpan = ((sample).toDouble() / rate.toDouble()).seconds

operator fun get(channel: Int): ShortArray = samples.data[channel]
operator fun get(channel: Int, sample: Int): Short = samples.data[channel][sample]
Expand Down Expand Up @@ -89,7 +89,7 @@ class AudioDataStream(val data: AudioData) : AudioStream(data.rate, data.channel
}


suspend fun AudioData.toSound() = nativeSoundProvider.createSound(this)
suspend fun AudioData.toSound(): Sound = nativeSoundProvider.createSound(this)

suspend fun VfsFile.readAudioData(formats: AudioFormat = defaultAudioFormats, props: AudioDecodingProps = AudioDecodingProps.DEFAULT): AudioData =
this.openUse { formats.decode(this, props) ?: invalidOp("Can't decode audio file ${this@readAudioData}") }
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class AudioSamplesInterleaved(override val channels: Int, override val totalSamp
//val separared by lazy { separated() }
internal val fastShortTransfer = FastShortTransfer()

private fun index(channel: Int, sample: Int) = (sample * channels) + channel
private fun index(channel: Int, sample: Int): Int = (sample * channels) + channel
override operator fun get(channel: Int, sample: Int): Short = data[index(channel, sample)]
override operator fun set(channel: Int, sample: Int, value: Short) { data[index(channel, sample)] = value }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import kotlin.math.min
class AudioSamplesDeque(val channels: Int) {
val buffer = Array(channels) { ShortArrayDeque() }
val availableRead get() = buffer.getOrNull(0)?.availableRead ?: 0
val availableReadMax: Int get() = buffer.map { it.availableRead }.maxOrNull() ?: 0
val availableReadMax: Int get() = buffer.maxOfOrNull { it.availableRead } ?: 0

fun createTempSamples(size: Int = 1024) = AudioSamples(channels, size)

Expand All @@ -28,7 +28,7 @@ class AudioSamplesDeque(val channels: Int) {

// Write samples
fun write(samples: AudioSamples, offset: Int = 0, len: Int = samples.totalSamples - offset) {
for (channel in 0 until samples.channels) write(channel, samples[channel], offset, len)
for (channel in 0 until this.channels) write(channel, samples[channel % samples.channels], offset, len)
}

fun write(samples: AudioSamplesInterleaved, offset: Int = 0, len: Int = samples.totalSamples - offset) {
Expand Down Expand Up @@ -68,15 +68,15 @@ class AudioSamplesDeque(val channels: Int) {
}

fun read(out: AudioSamples, offset: Int = 0, len: Int = out.totalSamples - offset): Int {
val result = min(len, availableRead)
for (channel in 0 until out.channels) this.buffer[channel].read(out[channel], offset, len)
return result
val rlen = min(len, availableRead)
for (channel in 0 until out.channels) this.buffer[channel % this.channels].read(out[channel], offset, rlen)
return rlen
}

fun read(out: AudioSamplesInterleaved, offset: Int = 0, len: Int = out.totalSamples - offset): Int {
val result = min(len, availableRead)
for (channel in 0 until out.channels) for (n in 0 until len) out[channel, offset + n] = this.read(channel)
return result
val rlen = min(len, availableRead)
for (channel in 0 until out.channels) for (n in 0 until rlen) out[channel, offset + n] = this.read(channel)
return rlen
}

fun read(out: IAudioSamples, offset: Int = 0, len: Int = out.totalSamples - offset): Int {
Expand All @@ -93,5 +93,19 @@ class AudioSamplesDeque(val channels: Int) {
for (c in buffer.indices) buffer[c].clear()
}

fun clone(): AudioSamplesDeque {
return AudioSamplesDeque(channels).also {
for (n in buffer.indices) it.buffer[n] = buffer[n].clone()
}
}
override fun toString(): String = "AudioSamplesDeque(channels=$channels, availableRead=$availableRead)"
}

// @TODO: Cloning...
fun AudioSamplesDeque.consumeToData(rate: Int): AudioData {
val samples = AudioSamples(channels, availableRead)
read(samples)
return AudioData(rate, samples)
}

fun AudioSamplesDeque.toData(rate: Int): AudioData = clone().consumeToData(rate)
Loading

0 comments on commit c216d17

Please sign in to comment.