KorAU

KorAU

KorAU, Kotlin cORoutines AUdio - Audio playing, and sound file decoding

It supports playing sounds, creating dynamic audio streams and decoding audio file formats: wav, mp3 and ogg.

Star

Table of contents:

Using with gradle

Requires Gradle 7.1.1 (JVM 11~17) for building and Kotlin >=1.7.21 for running:

build.gradle.kts

val korauVersion = "4.0.2"

// For multiplatform projects
kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation("com.soywiz.korlibs.korau:korau:$korauVersion") 
            }
        }
    }
}

dependencies {
    // For JVM only
    implementation("com.soywiz.korlibs.korau:korau-jvm:$korauVersion") 
    // For Android only
    implementation("com.soywiz.korlibs.korau:korau-android:$korauVersion") 
    // For JS only
    implementation("com.soywiz.korlibs.korau:korau-js:$korauVersion") 
}

AudioFormat

suspend fun VfsFile.readSoundInfo(formats: AudioFormats = defaultAudioFormats): AudioFormat.Info
// Just Sound Information by default
object MP3 : AudioFormat("mp3")
object OGG : AudioFormat("ogg")

// Decoding and Encoding
object WAV : AudioFormat("wav")

open class AudioFormat(vararg exts: String) {
	val extensions: Set<String>

	data class Info(var duration: TimeSpan = 0.seconds, var channels: Int = 2) : Extra by Extra.Mixin()

	open suspend fun tryReadInfo(data: AsyncStream): Info?
	open suspend fun decodeStream(data: AsyncStream): AudioStream?
	suspend fun decode(data: AsyncStream): AudioData?
	suspend fun decode(data: ByteArray): AudioData?
	open suspend fun encode(data: AudioData, out: AsyncOutputStream, filename: String): Unit

	suspend fun encodeToByteArray(data: AudioData, filename: String = "out.wav", format: AudioFormat = this): ByteArray
}

open class InvalidAudioFormatException(message: String) : RuntimeException(message)
fun invalidAudioFormat(message: String = "invalid audio format"): Nothing

val defaultAudioFormats = AudioFormats().apply { registerStandard() }

class AudioFormats : AudioFormat() {
	val formats = linkedSetOf<AudioFormat>()
	fun register(vararg formats: AudioFormat): AudioFormats = this.apply { this.formats += formats }
	fun register(formats: Iterable<AudioFormat>): AudioFormats = this.apply { this.formats += formats }
}

fun AudioFormats.registerStandard(): AudioFormats = this.apply { register(WAV, OGG, MP3) }

AudioData

class AudioData(val rate: Int, val samples: AudioSamples) {
    companion object {
        val DUMMY: AudioData = AudioData(44100, AudioSamples(2, 0))
    }

    val samplesInterleaved: AudioSamplesInterleaved by lazy { samples.interleaved() }

    val channels: Int
    val totalSamples: Int
    val totalTime: TimeSpan
    fun timeAtSample(sample: Int): TimeSpan

    operator fun get(channel: Int): ShortArray
    operator fun get(channel: Int, sample: Int): Short
    operator fun set(channel: Int, sample: Int, value: Short): Unit
}

enum class AudioConversionQuality { FAST }
fun AudioData.withRate(rate: Int): AudioData
fun AudioData.toStream(): AudioStream = object : AudioStream(rate, channels) {
    var cursor = 0
}

suspend fun AudioData.toNativeSound(): NativeSound
suspend fun AudioData.playAndWait(): Unit
suspend fun VfsFile.readAudioData(formats: AudioFormats = defaultAudioFormats): AudioData

AudioSamples

interface IAudioSamples {
    val channels: Int
    val totalSamples: Int
    val size get() = totalSamples
    fun isEmpty() = size == 0
    fun isNotEmpty() = size != 0
    operator fun get(channel: Int, sample: Int): Short
    operator fun set(channel: Int, sample: Int, value: Short): Unit
    fun getFloat(channel: Int, sample: Int): Float
    fun setFloat(channel: Int, sample: Int, value: Float)
}

class AudioSamples(override val channels: Int, override val totalSamples: Int) : IAudioSamples {
    val data = Array(channels) { ShortArray(totalSamples) }
    operator fun get(channel: Int): ShortArray = data[channel]
}

class AudioSamplesInterleaved(override val channels: Int, override val totalSamples: Int) : IAudioSamples {
    val data = ShortArray(totalSamples * channels)
}

fun AudioSamples.copyOfRange(start: Int, end: Int): AudioSamples
fun IAudioSamples.interleaved(out: AudioSamplesInterleaved = AudioSamplesInterleaved(channels, totalSamples)): AudioSamplesInterleaved
fun IAudioSamples.separated(out: AudioSamples = AudioSamples(channels, totalSamples)): AudioSamples

AudioSamplesDeque

class AudioSamplesDeque(val channels: Int) {
    val buffer: Array<ShortArrayDeque>
    val availableRead: Int
    val availableReadMax: Int

    fun read(channel: Int): Short
    fun write(channel: Int, sample: Short)   
    fun write(samples: AudioSamples, offset: Int = 0, len: Int = samples.size - offset)
    fun write(samples: AudioSamplesInterleaved, offset: Int = 0, len: Int = samples.size - offset)
    fun write(samples: IAudioSamples, offset: Int = 0, len: Int = samples.size - offset)
    fun write(channel: Int, data: ShortArray, offset: Int = 0, len: Int = data.size - offset)
    fun write(channel: Int, data: FloatArray, offset: Int = 0, len: Int = data.size - offset)
    fun writeInterleaved(data: ShortArray, offset: Int, len: Int = data.size - offset, channels: Int
    fun read(out: AudioSamples, offset: Int = 0, len: Int = out.totalSamples - offset): Int
    fun read(out: AudioSamplesInterleaved, offset: Int = 0, len: Int = out.totalSamples - offset): Int
    fun read(out: IAudioSamples, offset: Int = 0, len: Int = out.totalSamples - offset): Int
}

AudioStream

open class AudioStream(val rate: Int, val channels: Int) : Closeable {
    open val finished: Boolean
    val totalLengthInSamples: Long?
    val totalLength: TimeSpan
    open suspend fun read(out: AudioSamples, offset: Int, length: Int): Int = 0

    companion object {
        fun generator(rate: Int, channels: Int, generateChunk: suspend AudioSamplesDeque.(step: Int) -> Boolean): AudioStream
    }
}

suspend fun AudioStream.toData(maxSamples: Int = Int.MAX_VALUE): AudioData
suspend fun AudioStream.playAndWait(bufferSeconds: Double = 0.1)
suspend fun VfsFile.readAudioStream(formats: AudioFormats = defaultAudioFormats)
suspend fun VfsFile.writeAudio(data: AudioData, formats: AudioFormats = defaultAudioFormats)

AudioTone

object AudioTone {
    fun generate(length: TimeSpan, freq: Double, rate: Int = 44100): AudioData
}

NativeSound

expect val nativeSoundProvider: NativeSoundProvider

open class NativeSoundProvider {
	open val target: String = "unknown"
	open fun initOnce()
    open fun createAudioStream(freq: Int = 44100): PlatformAudioOutput
	protected open fun init(): Unit
	open suspend fun createSound(data: ByteArray, streaming: Boolean = false): NativeSound
	open suspend fun createSound(vfs: Vfs, path: String, streaming: Boolean = false): NativeSound
	suspend fun createSound(file: FinalVfsFile, streaming: Boolean = false): NativeSound
	suspend fun createSound(file: VfsFile, streaming: Boolean = false): NativeSound
	open suspend fun createSound(data: AudioData, formats: AudioFormats = defaultAudioFormats, streaming: Boolean = false): NativeSound
	suspend fun playAndWait(stream: AudioStream, bufferSeconds: Double = 0.1)
}

abstract class NativeSoundChannel(val sound: NativeSound) {
	open var volume: Double
	open var pitch: Double
	open val current: TimeSpan
	open val total: TimeSpan
	open val playing: Boolean
	abstract fun stop()
}

suspend fun NativeSoundChannel.await(progress: NativeSoundChannel.(current: TimeSpan, total: TimeSpan) -> Unit = { current, total -> })

abstract class NativeSound {
	open val length: TimeSpan = 0.seconds
	abstract suspend fun decode(): AudioData
	abstract fun play(): NativeSoundChannel
}

suspend fun NativeSound.toData(): AudioData
suspend fun NativeSound.toStream(): AudioStream

suspend fun NativeSound.playAndWait(progress: NativeSoundChannel.(current: TimeSpan, total: TimeSpan) -> Unit = { current, total -> }): Unit

suspend fun VfsFile.readNativeSound(streaming: Boolean = false): NativeSound
suspend fun VfsFile.readNativeSoundOptimized(streaming: Boolean = false): NativeSound

SoundUtils

object SoundUtils {
	fun convertS16ToF32(channels: Int, input: ShortArray, leftVolume: Int, rightVolume: Int): FloatArray

PlatformAudioOutput

open class PlatformAudioOutput(freq: Int) {
	open val availableSamples: Int = 0
	open suspend fun add(samples: AudioSamples, offset: Int = 0, size: Int = samples.totalSamples) = Unit
	suspend fun add(data: AudioData) = add(data.samples, 0, data.totalSamples)
	open fun start() = Unit
	open fun stop() = Unit
}