Skip to content

Commit

Permalink
OSC decoders/encoders moved from convert to type subpackage
Browse files Browse the repository at this point in the history
  • Loading branch information
morisil committed Sep 4, 2024
1 parent 03528b1 commit 616a990
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 5 deletions.
132 changes: 132 additions & 0 deletions xemantic-osc-api/src/commonMain/kotlin/type/OscDecoders.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* xemantic-osc - Kotlin idiomatic and multiplatform OSC protocol support
* Copyright (C) 2024 Kazimierz Pogoda
*
* This file is part of xemantic-osc.
*
* xemantic-osc is free software: you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* xemantic-osc 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with xemantic-osc.
* If not, see <https://www.gnu.org/licenses/>.
*/

package com.xemantic.osc.type

import com.xemantic.osc.*
import kotlin.reflect.KType
import kotlin.reflect.typeOf

public class OscDecodersBuilder {
@PublishedApi
internal val decoders: MutableMap<KType, OscDecoder<*>> = mutableMapOf()
public inline fun <reified T> decoder(
typeTag: String? = null,
noinline decode: OscReader.() -> T
) {
decoders[typeOf<T>()] = OscDecoder(typeTag, decode)
}
}

public fun oscDecoders(
block: OscDecodersBuilder.() -> Unit
): Map<KType, OscDecoder<*>> =
OscDecodersBuilder().apply(block).decoders

public inline fun <reified T> OscReader.listOf(
elementTypeTag: Char,
crossinline elementDecoder: OscReader.() -> T
): List<T> {
val typeTag = typeTag()
if (typeTag.any { it != elementTypeTag }) {
throw OscInputException(
"Cannot decode List<${typeOf<T>()}>, typeTag must consists " +
"of '$elementTypeTag' characters only, but was: $typeTag"
)
}
return typeTag.map { elementDecoder(this) }.toList()
}

public val DEFAULT_OSC_DECODERS: Map<KType, OscDecoder<*>> = oscDecoders {
decoder<Int>("i") { int() }
decoder<Float>("f") { float() }
decoder<String>("s") { string() }
decoder<ByteArray>("b") { blob() }
decoder<Long>("h") { long() }
decoder<OscTimeTag>("t") { timeTag() }
decoder<Double>("d") { double() }
decoder<Char>("c") { char() }
decoder<Boolean> { typeTagToBoolean(typeTag()[0]) }
decoder<List<Int>> { listOf('i') { int() } }
decoder<List<Float>> { listOf('f') { float() } }
decoder<List<String>> { listOf('s') { string() } }
decoder<List<ByteArray>> { listOf('b') { blob() } }
decoder<List<Long>> { listOf('h') { long() } }
decoder<List<OscTimeTag>> { listOf('t') { timeTag() } }
decoder<List<Double>> { listOf('d') { double() } }
decoder<List<Char>> { listOf('c') { char() } }
decoder<List<Boolean>> { typeTag().map { typeTagToBoolean(it) } }
decoder<List<Any>> { readByTypeTag(typeTag().toList()) }
}

private fun OscReader.readByTypeTag(
typeTag: List<Char>
): List<Any> {
var nextIndex = 0
return typeTag.mapIndexedNotNull { index, char ->
if (index >= nextIndex) {
if (char == '[') {
nextIndex = index + typeTag.drop(index).indexOf(']') + 1
readByTypeTag(
typeTag.slice((index + 1)..<nextIndex - 1)
)
} else {
readByTypeTag(char)
}
} else {
null
}
}
}

public fun OscReader.readByTypeTag(typeTag: Char): Any = when (typeTag) {
'i' -> int()
'f' -> float()
's' -> string()
'b' -> blob()
'h' -> long()
't' -> timeTag()
'd' -> double()
'c' -> char()
'm' -> midiMessage()
'r' -> color()
'T' -> true
'F' -> false
else -> throw OscInputException(
"Unsupported OSC type tag: $typeTag"
)
}

@Suppress("UNCHECKED_CAST")
public inline fun <reified T> defaultOscDecoder(): OscDecoder<T> =
(DEFAULT_OSC_DECODERS[typeOf<T>()]
?: throw IllegalArgumentException(
"No encoder for specified type: ${typeOf<T>()}"
)
) as OscDecoder<T>

private fun typeTagToBoolean(
typeTag: Char
): Boolean = when (typeTag) {
'T' -> true
'F' -> false
else -> throw OscInputException(
"Invalid typeTag for Boolean: $typeTag"
)
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* xemantic-osc - Kotlin idiomatic and multiplatform OSC protocol support
* Copyright (C) 2023 Kazimierz Pogoda
* Copyright (C) 2024 Kazimierz Pogoda
*
* This file is part of xemantic-osc.
*
Expand All @@ -16,11 +16,11 @@
* If not, see <https://www.gnu.org/licenses/>.
*/

package com.xemantic.osc.convert
package com.xemantic.osc.type

import com.xemantic.osc.OscEncoder
import com.xemantic.osc.protocol.OscTimeTag
import com.xemantic.osc.protocol.OscWriter
import com.xemantic.osc.OscInputException
import com.xemantic.osc.typeTag
import kotlin.reflect.KType
import kotlin.reflect.typeOf

Expand All @@ -44,9 +44,18 @@ private inline fun <reified T> listEncoder(
crossinline elementEncoder: OscEncoder<T>
): OscEncoder<List<T>> = { list ->
typeTag(CharArray(list.size) { elementTypeTag }.concatToString())
list.forEach { elementEncoder(OscWriter(output), it) }
list.forEach { elementEncoder(this, it) }
}

//https://www.cnmat.berkeley.edu/sites/default/files/attachments/2015_Dynamic_Message_Oriented_Middleware.pdf
// TODO externalize all the default encoders
public val intEncoder: OscEncoder<Int> = { typeTag("i"); int(it) }
public val Int.oscEncoder: OscEncoder<Int> get() = intEncoder

public val longEncoder: OscEncoder<Long> = { typeTag("h"); long(it) }

public val midiMessageEncoder: OscEncoder<OscMidiMessage> = { }

public val DEFAULT_OSC_ENCODERS: Map<KType, OscEncoder<*>> = oscEncoders {
encoder<Int> { typeTag("i"); int(it) }
encoder<Float> { typeTag("f"); float(it) }
Expand All @@ -57,6 +66,8 @@ public val DEFAULT_OSC_ENCODERS: Map<KType, OscEncoder<*>> = oscEncoders {
encoder<Double> { typeTag("d"); double(it) }
encoder<Char> { typeTag("c"); char(it) }
encoder<Boolean> { typeTag(it.typeTag) }
encoder<OscMidiMessage> { typeTag("m"); midiMessage(it) }
encoder<OscColor> { typeTag("c"); color(it) }
encoder<List<Int>>(listEncoder('i') { int(it) })
encoder<List<Float>>(listEncoder('f') { float(it) })
encoder<List<String>>(listEncoder('s') { string(it) })
Expand All @@ -70,4 +81,48 @@ public val DEFAULT_OSC_ENCODERS: Map<KType, OscEncoder<*>> = oscEncoders {
}
}

@Suppress("UNCHECKED_CAST")
public inline fun <reified T> defaultOscEncoder(): OscEncoder<T> =
(DEFAULT_OSC_ENCODERS[typeOf<T>()]
?: throw IllegalArgumentException(
"No encoder for specified type: ${typeOf<T>()}"
)
) as OscEncoder<T>

internal val Boolean.typeTag: String get() = if (this) "T" else "F"


public val genericEncoder: OscEncoder<List<Any>> = { anys ->
val typeTag = anys.map { any ->
when (any) {
is Int -> 'i'
is Float -> 'f'
is String -> 's'
is ByteArray -> 'b'
is Long -> 'h'
is OscTimeTag -> 't'
is Double -> 'd'
is Char -> 'c'
is Boolean -> any.typeTag[0]
else -> throw OscInputException(
"Unsupported type: ${any::class} in input list"
)
}
}.toCharArray().concatToString()
typeTag(typeTag)
anys.forEach { any ->
when (any) {
is Int -> int(any)
is Float -> float(any)
is String -> string(any)
is ByteArray -> blob(any)
is Long -> long(any)
is Double -> double(any)
is Char -> char(any)
is Boolean -> any.typeTag // This is arbitrary
else -> throw OscInputException(
"Unsupported type: ${any::class} in input list"
)
}
}
}

0 comments on commit 616a990

Please sign in to comment.