Skip to content

Commit

Permalink
Refactored vorbis-comment logic into separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulWoitaschek committed Jul 19, 2017
1 parent f22f1f1 commit b9f5763
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 150 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.ph1b.audiobook.features.chapterReader.ogg

class OGGPageParseException(message: String) : Exception(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package de.ph1b.audiobook.features.chapterReader.ogg

data class OggPage(
val continuedPacket: Boolean,
val finishedPacket: Boolean,
val firstPageOfStream: Boolean,
val lastPageOfStream: Boolean,
val absoluteGranulePosition: Long,
val streamSerialNumber: Int,
val pageSequenceNumber: Long,
val packets: List<ByteArray>) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is OggPage) return false
return continuedPacket == other.continuedPacket
&& finishedPacket == other.finishedPacket
&& firstPageOfStream == other.firstPageOfStream
&& lastPageOfStream == other.lastPageOfStream
&& absoluteGranulePosition == other.absoluteGranulePosition
&& streamSerialNumber == other.streamSerialNumber
&& pageSequenceNumber == other.pageSequenceNumber
&& packets.size == other.packets.size
&& packets.indices.all { packets[it] contentEquals other.packets[it] }
}

override fun hashCode(): Int {
var hashCode = 17
hashCode = 31 * hashCode + continuedPacket.hashCode()
hashCode = 31 * hashCode + finishedPacket.hashCode()
hashCode = 31 * hashCode + firstPageOfStream.hashCode()
hashCode = 31 * hashCode + lastPageOfStream.hashCode()
hashCode = 31 * hashCode + absoluteGranulePosition.hashCode()
hashCode = 31 * hashCode + streamSerialNumber.hashCode()
hashCode = 31 * hashCode + pageSequenceNumber.hashCode()
packets.forEach {
hashCode = 31 * hashCode + it.contentHashCode()
}
return hashCode
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package de.ph1b.audiobook.features.chapterReader.ogg

import java.util.ArrayDeque

class OggStream(private val pullPage: OggStream.() -> Unit) : Iterator<ByteArray> {
private val packetsQue = ArrayDeque<ByteArray>()
private val packetBuffer = mutableListOf<ByteArray>()
private var isDone = false

fun pushPage(page: OggPage) {
if (isDone) return
val start = if (page.continuedPacket) {
if (page.packets.size > 1 || page.finishedPacket) {
packetBuffer.add(page.packets[0])
packetsQue.add(packetBuffer.concat())
packetBuffer.clear()
}
1
} else 0
val end = page.packets.lastIndex - if (page.finishedPacket) 0 else {
packetBuffer.add(page.packets[page.packets.lastIndex])
1
}
(start..end).mapTo(packetsQue) { page.packets[it] }
if (page.lastPageOfStream) isDone = true
}

override fun hasNext(): Boolean {
while (packetsQue.isEmpty()) {
if (isDone) return false
pullPage()
}
return true
}

override fun next(): ByteArray {
if (!hasNext()) throw NoSuchElementException()
return packetsQue.poll()
}

fun peek(): ByteArray {
if (!hasNext()) throw NoSuchElementException()
return packetsQue.peek()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package de.ph1b.audiobook.features.chapterReader.ogg

import android.util.SparseArray
import de.ph1b.audiobook.features.chapterReader.ogg.vorbisComment.VorbisComment
import de.ph1b.audiobook.features.chapterReader.ogg.vorbisComment.VorbisCommentParseException
import de.ph1b.audiobook.features.chapterReader.ogg.vorbisComment.VorbisCommentReader
import de.ph1b.audiobook.features.chapterReader.readBytes
import de.ph1b.audiobook.features.chapterReader.startsWith
import de.ph1b.audiobook.misc.emptySparseArray
Expand Down Expand Up @@ -52,7 +55,7 @@ private fun readVorbisCommentFromOpusStream(stream: OggStream): VorbisComment {
val capturePattern = packetStream.readBytes(OPUS_TAGS_MAGIC.size)
if (!(capturePattern contentEquals OPUS_TAGS_MAGIC))
throw OpusStreamParseException("Invalid opus tags capture pattern")
return readVorbisComment(packetStream)
return VorbisCommentReader.readComment(packetStream)
}

class VorbisStreamParseException(message: String) : Exception(message)
Expand All @@ -66,5 +69,5 @@ private fun readVorbisCommentFromVorbisStream(stream: OggStream): VorbisComment
val capturePattern = packetStream.readBytes(VORBIS_TAGS_MAGIC.size)
if (!(capturePattern contentEquals VORBIS_TAGS_MAGIC))
throw VorbisStreamParseException("Invalid vorbis comment header capture pattern")
return readVorbisComment(packetStream)
return VorbisCommentReader.readComment(packetStream)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,9 @@ import de.ph1b.audiobook.features.chapterReader.skipBytes
import de.ph1b.audiobook.features.chapterReader.toUInt
import java.io.EOFException
import java.io.InputStream
import java.util.ArrayDeque

private val OGG_PAGE_MAGIC = "OggS".toByteArray()

data class OggPage(
val continuedPacket: Boolean,
val finishedPacket: Boolean,
val firstPageOfStream: Boolean,
val lastPageOfStream: Boolean,
val absoluteGranulePosition: Long,
val streamSerialNumber: Int,
val pageSequenceNumber: Long,
val packets: List<ByteArray>) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is OggPage) return false
return continuedPacket == other.continuedPacket
&& finishedPacket == other.finishedPacket
&& firstPageOfStream == other.firstPageOfStream
&& lastPageOfStream == other.lastPageOfStream
&& absoluteGranulePosition == other.absoluteGranulePosition
&& streamSerialNumber == other.streamSerialNumber
&& pageSequenceNumber == other.pageSequenceNumber
&& packets.size == other.packets.size
&& packets.indices.all { packets[it] contentEquals other.packets[it] }
}

override fun hashCode(): Int {
var hashCode = 17
hashCode = 31 * hashCode + continuedPacket.hashCode()
hashCode = 31 * hashCode + finishedPacket.hashCode()
hashCode = 31 * hashCode + firstPageOfStream.hashCode()
hashCode = 31 * hashCode + lastPageOfStream.hashCode()
hashCode = 31 * hashCode + absoluteGranulePosition.hashCode()
hashCode = 31 * hashCode + streamSerialNumber.hashCode()
hashCode = 31 * hashCode + pageSequenceNumber.hashCode()
packets.forEach {
hashCode = 31 * hashCode + it.contentHashCode()
}
return hashCode
}
}

class OGGPageParseException(message: String) : Exception(message)

fun computePacketSizesFromSegmentTable(segmentTable: ByteArray): List<Int>
= segmentTable.map { it.toUInt() }.fold(mutableListOf(0), { acc, e ->
acc[acc.lastIndex] += e
Expand Down Expand Up @@ -114,48 +71,6 @@ fun Iterable<ByteArray>.concat(): ByteArray {
return res
}

class OggStream(private val pullPage: OggStream.() -> Unit) : Iterator<ByteArray> {
private val packetsQue = ArrayDeque<ByteArray>()
private val packetBuffer = mutableListOf<ByteArray>()
private var isDone = false

fun pushPage(page: OggPage) {
if (isDone) return
val start = if (page.continuedPacket) {
if (page.packets.size > 1 || page.finishedPacket) {
packetBuffer.add(page.packets[0])
packetsQue.add(packetBuffer.concat())
packetBuffer.clear()
}
1
} else 0
val end = page.packets.lastIndex - if (page.finishedPacket) 0 else {
packetBuffer.add(page.packets[page.packets.lastIndex])
1
}
(start..end).mapTo(packetsQue) { page.packets[it] }
if (page.lastPageOfStream) isDone = true
}

override fun hasNext(): Boolean {
while (packetsQue.isEmpty()) {
if (isDone) return false
pullPage()
}
return true
}

override fun next(): ByteArray {
if (!hasNext()) throw NoSuchElementException()
return packetsQue.poll()
}

fun peek(): ByteArray {
if (!hasNext()) throw NoSuchElementException()
return packetsQue.peek()
}
}

fun demuxOggStreams(oggPages: Sequence<OggPage>): SparseArray<OggStream> {
val it = oggPages.iterator()
val streamMap = SparseArray<OggStream>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.ph1b.audiobook.features.chapterReader.ogg.vorbisComment

import android.util.SparseArray
import de.ph1b.audiobook.misc.emptySparseArray

data class VorbisComment(val vendor: String, val comments: Map<String, String>) {
/**
* Chapters extracted according to https://wiki.xiph.org/Chapter_Extension
*/
val chapters: SparseArray<String>
get() {
val chapters = SparseArray<String>()
var i = 1
while (true) {
val iStr = i.toString().padStart(3, '0')
val timeStr = comments["CHAPTER$iStr"] ?: break
val name = comments["CHAPTER${iStr}NAME"] ?: return emptySparseArray()
val time = VorbisCommentReader.parseChapterTime(timeStr) ?: return emptySparseArray()
chapters.put(time, name)
++i
}
return chapters
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.ph1b.audiobook.features.chapterReader.ogg.vorbisComment

class VorbisCommentParseException(message: String) : Exception(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package de.ph1b.audiobook.features.chapterReader.ogg.vorbisComment

import de.ph1b.audiobook.features.chapterReader.readBytes
import de.ph1b.audiobook.features.chapterReader.readLeUInt32
import java.io.InputStream

object VorbisCommentReader {

private val VORBIS_COMMENT_CHAPTER_TIME_REGEX = Regex("""(\d+):(\d+):(\d+).(\d+)""")

/**
* Reads vorbis comment according to [specification](https://xiph.org/vorbis/doc/v-comment.html)
*/
fun readComment(stream: InputStream): VorbisComment {
val vendorLength = stream.readLeUInt32()
val vendor = stream.readBytes(vendorLength.toInt()).toString(Charsets.UTF_8)
val numberComments = stream.readLeUInt32()
val comments = (1..numberComments).map {
val length = stream.readLeUInt32()
val comment = stream.readBytes(length.toInt()).toString(Charsets.UTF_8)
val parts = comment.split("=", limit = 2)
if (parts.size != 2) throw VorbisCommentParseException("Expected TAG=value comment format")
Pair(parts[0].toUpperCase(), parts[1])
}.toMap()
return VorbisComment(vendor, comments)
}

fun parseChapterTime(timeStr: String): Int? {
val matchResult = VORBIS_COMMENT_CHAPTER_TIME_REGEX.matchEntire(timeStr)
?: return null
val hours = matchResult.groups[1]!!.value.toInt()
val minutes = hours * 60 + matchResult.groups[2]!!.value.toInt()
val seconds = minutes * 60 + matchResult.groups[3]!!.value.toInt()
val milliseconds = seconds * 1000 + matchResult.groups[4]!!.value.take(3).padEnd(3, '0').toInt()
return milliseconds
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package de.ph1b.audiobook.features.chapterReader.ogg

import de.ph1b.audiobook.features.chapterReader.ogg.vorbisComment.VorbisComment
import de.ph1b.audiobook.features.chapterReader.ogg.vorbisComment.VorbisCommentParseException
import de.ph1b.audiobook.features.chapterReader.ogg.vorbisComment.VorbisCommentReader
import de.ph1b.audiobook.misc.toMap
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown
Expand All @@ -13,10 +16,10 @@ import java.io.ByteArrayInputStream
class VorbisCommentReadingTest {
@Test
fun parseChapterTime() {
assertThat(parseVorbisCommentChapterTime("10:02:10.231")).isEqualTo(36130231)
assertThat(parseVorbisCommentChapterTime("110:2:10.23")).isEqualTo(396130230)
assertThat(parseVorbisCommentChapterTime("0:0:0.11111")).isEqualTo(111)
assertThat(parseVorbisCommentChapterTime("asdasd")).isNull()
assertThat(VorbisCommentReader.parseChapterTime("10:02:10.231")).isEqualTo(36130231)
assertThat(VorbisCommentReader.parseChapterTime("110:2:10.23")).isEqualTo(396130230)
assertThat(VorbisCommentReader.parseChapterTime("0:0:0.11111")).isEqualTo(111)
assertThat(VorbisCommentReader.parseChapterTime("asdasd")).isNull()
}

@Test
Expand Down Expand Up @@ -54,7 +57,7 @@ class VorbisCommentReadingTest {
@Test
fun parseVorbisComment() {
val stream1 = ByteArrayInputStream(Hex.decode("0d00000076656e646f7220737472696e670300000005000000613d6173640a0000005449544c453d7465787407000000757466383dcf80"))
assertThat(readVorbisComment(stream1)).isEqualTo(VorbisComment(
assertThat(VorbisCommentReader.readComment(stream1)).isEqualTo(VorbisComment(
vendor = "vendor string",
comments = mapOf(
"A" to "asd",
Expand All @@ -65,7 +68,7 @@ class VorbisCommentReadingTest {

val stream2 = ByteArrayInputStream(Hex.decode("000000000200000005000000613d61736406000000617364617364"))
try {
readVorbisComment(stream2)
VorbisCommentReader.readComment(stream2)
failBecauseExceptionWasNotThrown(VorbisCommentParseException::class.java)
} catch (_: VorbisCommentParseException) {
}
Expand Down

0 comments on commit b9f5763

Please sign in to comment.