All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.korlibs.audio.sound.SoundAudioStream.kt Maven / Gradle / Ivy

The newest version!
package korlibs.audio.sound

import korlibs.concurrent.lock.*
import korlibs.datastructure.*
import korlibs.math.*
import korlibs.time.*
import kotlinx.coroutines.*
import kotlin.coroutines.*
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.*

class SoundAudioStream(
    coroutineContext: CoroutineContext,
    val stream: AudioStream,
    var soundProvider: NativeSoundProvider,
    val closeStream: Boolean = false,
    override val name: String = stream.name ?: "Unknown",
    val onComplete: (suspend () -> Unit)? = null
) : Sound(coroutineContext) {
    val nativeSound = this
    override val length: Duration get() = stream.totalLength
    override suspend fun decode(maxSamples: Int): AudioData = stream.toData(maxSamples)
    override suspend fun toStream(): AudioStream = stream.clone()
    override val nchannels: Int get() = stream.channels

    companion object {
        private val ID_POOL = ConcurrentPool { it }
    }

    @OptIn(ExperimentalStdlibApi::class)
    override fun play(coroutineContext: CoroutineContext, params: PlaybackParameters): SoundChannel {
        val dequeLock = Lock()
        val deque = AudioSamplesDeque(nchannels)
        var playing = true
        var flushing = false
        var newStream: AudioStream? = null
        val channelId = ID_POOL.alloc()
        val dispatcherName = "SoundChannel-SoundAudioStream-$channelId"
        var times = params.times

        // In the scenario of 44100 hz and 2048 samples is the chunk size
        // that's typically ~46 milliseconds, so buffering will be multiple of that
        val BUFFER_SAMPLES = (stream.rate * params.bufferTime.seconds).toInt().nextMultipleOf(AudioPlatformOutput.DEFAULT_BLOCK_SIZE)

        val nas = soundProvider.createNewPlatformAudioOutput(nchannels, stream.rate) {
            it.data.fill(AudioSample.ZERO)
            dequeLock {
                if (flushing || deque.availableRead >= BUFFER_SAMPLES) {
                    deque.read(it)
                }
            }
        }
        nas.copySoundPropsFromCombined(params, this)

        var currentPositionRequest: Duration? = null
        //println("dispatcher[a]=$dispatcher, thread=${currentThreadName}:${currentThreadId}")
        val job = CoroutineScope(coroutineContext).launch(coroutineContext) {
            //println("dispatcher[b]=$dispatcher, thread=${currentThreadName}:${currentThreadId}")
            val stream = stream.clone()
            newStream = stream
            stream.seek(params.startTime)
            playing = true
            //println("!!! STARTED: $this")
            try {
                val temp = AudioSamples(stream.channels, 2048)
                var started = false
                while (times.hasMore) {
                    stream.seek(0.seconds)
                    while (true) {
                        //println("STREAM")
                        while (nas.paused) {
                            delay(2.milliseconds)
                            //println("PAUSED")
                        }
                        if (currentPositionRequest != null) {
                            dequeLock { deque.clear() }
                            stream.seek(currentPositionRequest!!)
                            currentPositionRequest = null
                        }
                        val read = stream.read(temp, 0, temp.totalSamples)
                        dequeLock { deque.write(temp, 0, read) }
                        if (stream.finished || deque.availableRead >= BUFFER_SAMPLES) {
                            if (!started) {
                                started = true
                                nas.start()
                            } else if (deque.availableRead >= BUFFER_SAMPLES * 2) {
                                while (deque.availableRead >= BUFFER_SAMPLES) delay(1.milliseconds)
                            }
                        }
                        yield()
                        if (stream.finished) break
                    }
                    yield()
                    times = times.oneLess
                }
                flushing = true
                var n = 0
                while (nas.running && deque.availableRead > 0 && n++ < 8) delay(10L)
            } catch (e: CancellationException) {
                // Do nothing
                params.onCancel?.invoke()
            } finally {
                nas.stop()
                if (closeStream) stream.close()
                playing = false
                ID_POOL.free(channelId)
                params.onFinish?.invoke()
                onComplete?.invoke()
            }
        }

        fun close() {
            job.cancel()
        }
        return object : SoundChannel(nativeSound) {
            override var volume: Double by nas::volume
            override var pitch: Double by nas::pitch
            override var panning: Double by nas::panning
            override var current: Duration
                get() = currentPositionRequest ?: newStream?.currentTime ?: 0.milliseconds
                set(value) {
                    currentPositionRequest = value
                }
            override val total: Duration get() = newStream?.totalLength ?: stream.totalLength
            override val state: SoundChannelState
                get() = when {
                    nas.paused -> SoundChannelState.PAUSED
                    playing -> SoundChannelState.PLAYING
                    else -> SoundChannelState.STOPPED
                }

            override fun pause() {
                nas.paused = true
            }

            override fun resume() {
                nas.paused = false
            }

            override fun stop() = close()
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy