From e24f0a64838b0a9bd445f714349d95c07990346a Mon Sep 17 00:00:00 2001 From: soywiz Date: Sun, 5 Feb 2023 22:49:05 +0100 Subject: [PATCH 1/4] [WIP] Linux ALSA support to replace OpenAL on linux --- .../com/soywiz/korau/sound/impl/alsa/Alsa.kt | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 korau/src/jvmMain/kotlin/com/soywiz/korau/sound/impl/alsa/Alsa.kt diff --git a/korau/src/jvmMain/kotlin/com/soywiz/korau/sound/impl/alsa/Alsa.kt b/korau/src/jvmMain/kotlin/com/soywiz/korau/sound/impl/alsa/Alsa.kt new file mode 100644 index 0000000000..af4d837fec --- /dev/null +++ b/korau/src/jvmMain/kotlin/com/soywiz/korau/sound/impl/alsa/Alsa.kt @@ -0,0 +1,276 @@ +package com.soywiz.korau.sound.impl.alsa + +import com.soywiz.klock.* +import com.soywiz.korau.sound.* +import com.sun.jna.Library +import com.sun.jna.Memory +import com.sun.jna.Native +import com.sun.jna.Pointer +import java.util.Random + +object AlsaTest { + @JvmStatic fun main(args: Array) { + val cmpPtr = Memory(1024L).also { it.clear() } + val params = Memory(1024L).also { it.clear() } + val temp = Memory(1024L).also { it.clear() } + + val channels = 2 + val rate = 44100 + + //cmpPtr.clear() + //cmpPtr.setLong(0L, 0L) + println("test") + ASound2.snd_pcm_open(cmpPtr, "default", ASound2.SND_PCM_STREAM_PLAYBACK, 0).also { + if (it != 0) error("Can't initialize ALSA") + } + val pcm = cmpPtr.getPointer(0L) + println("pcm=$pcm") + println(ASound2.snd_pcm_hw_params_any(pcm, params)) + ASound2.snd_pcm_hw_params_set_access(pcm, params, ASound2.SND_PCM_ACCESS_RW_INTERLEAVED).also { + if (it != 0) error("Error calling snd_pcm_hw_params_set_access=$it") + } + ASound2.snd_pcm_hw_params_set_format(pcm, params, ASound2.SND_PCM_FORMAT_S16_LE).also { + if (it != 0) error("Error calling snd_pcm_hw_params_set_format=$it") + } + ASound2.snd_pcm_hw_params_set_channels(pcm, params, channels).also { + if (it != 0) error("Error calling snd_pcm_hw_params_set_channels=$it") + } + ASound2.snd_pcm_hw_params_set_rate(pcm, params, rate, +1).also { + if (it != 0) error("Error calling snd_pcm_hw_params_set_rate=$it") + } + ASound2.snd_pcm_hw_params(pcm, params).also { + if (it != 0) error("Error calling snd_pcm_hw_params=$it") + } + + println(ASound2.snd_pcm_name(pcm)) + println(ASound2.snd_pcm_state_name(ASound2.snd_pcm_state(pcm))) + ASound2.snd_pcm_hw_params_get_channels(params, temp).also { + if (it != 0) error("Error calling snd_pcm_hw_params_get_channels=$it") + } + val cchannels = temp.getInt(0L) + ASound2.snd_pcm_hw_params_get_rate(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_rate=$it") } + val crate = temp.getInt(0L) + ASound2.snd_pcm_hw_params_get_period_size(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_period_size=$it") } + val frames = temp.getInt(0L) + println("cchannels: $cchannels, rate=$crate, frames=$frames") + val buff = Memory((frames * channels * 2).toLong()).also { it.clear() } + ASound2.snd_pcm_hw_params_get_period_time(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_period_size=$it") } + //val random = Random(0L) + + val data = AudioTone.generate(1.seconds, 400.0) + + var nn = 0 + while (true) { + for (n in 0 until frames * channels) { + val value = data[0, nn] + buff.setShort((n * 2).toLong(), value) + nn++ + if (nn >= data.totalSamples) nn = 0 + } + val result = ASound2.snd_pcm_writei(pcm, buff, frames) + println("result=$result") + if (result == -ASound2.EPIPE) { + ASound2.snd_pcm_prepare(pcm) + } + } + + ASound2.snd_pcm_drain(pcm) + ASound2.snd_pcm_close(pcm) + } +} + +object ASound2 { + @JvmStatic external fun snd_pcm_open(pcmp: Pointer, name: String, stream: Int, mode: Int): Int + @JvmStatic external fun snd_pcm_hw_params_any(pcmp: Pointer, params: Pointer): Int + @JvmStatic external fun snd_pcm_hw_params_set_access(pcmp: Pointer, params: Pointer, access: Int): Int + @JvmStatic external fun snd_pcm_hw_params_set_format(pcmp: Pointer, params: Pointer, format: Int): Int + @JvmStatic external fun snd_pcm_hw_params_set_channels(pcmp: Pointer, params: Pointer, channels: Int): Int + @JvmStatic external fun snd_pcm_hw_params_set_rate(pcmp: Pointer, params: Pointer, rate: Int, dir: Int): Int + @JvmStatic external fun snd_pcm_hw_params(pcmp: Pointer, params: Pointer): Int + @JvmStatic external fun snd_pcm_name(pcmp: Pointer): String + @JvmStatic external fun snd_pcm_state(pcm: Pointer): Int + @JvmStatic external fun snd_pcm_state_name(state: Int): String + @JvmStatic external fun snd_pcm_hw_params_get_channels(params: Pointer, out: Pointer): Int + @JvmStatic external fun snd_pcm_hw_params_get_rate(params: Pointer?, value: Pointer?, dir: Pointer?): Int + @JvmStatic external fun snd_pcm_hw_params_get_period_size(params: Pointer?, value: Pointer?, dir: Pointer?): Int + @JvmStatic external fun snd_pcm_hw_params_get_period_time(params: Pointer?, value: Pointer?, dir: Pointer?): Int + @JvmStatic external fun snd_pcm_writei(pcm: Pointer, buffer: Pointer, size: Int): Int + @JvmStatic external fun snd_pcm_prepare(pcm: Pointer): Int + @JvmStatic external fun snd_pcm_drain(pcm: Pointer): Int + @JvmStatic external fun snd_pcm_close(pcm: Pointer): Int + + const val EPIPE = 32 /* Broken pipe */ + const val EBADFD = 77 /* File descriptor in bad state */ + const val ESTRPIPE = 86 /* Streams pipe error */ + + + const val SND_PCM_STREAM_PLAYBACK = 0 + const val SND_PCM_STREAM_CAPTURE = 1 + + + /** mmap access with simple interleaved channels */ + const val SND_PCM_ACCESS_MMAP_INTERLEAVED = 0 + /** mmap access with simple non interleaved channels */ + const val SND_PCM_ACCESS_MMAP_NONINTERLEAVED = 1 + /** mmap access with complex placement */ + const val SND_PCM_ACCESS_MMAP_COMPLEX = 2 + /** snd_pcm_readi/snd_pcm_writei access */ + const val SND_PCM_ACCESS_RW_INTERLEAVED = 3 + /** snd_pcm_readn/snd_pcm_writen access */ + const val SND_PCM_ACCESS_RW_NONINTERLEAVED = 4 + + const val SND_PCM_FORMAT_S16_LE = 2 + + /** Open */ + const val SND_PCM_STATE_OPEN = 0 + /** Setup installed */ + const val SND_PCM_STATE_SETUP = 1 + /** Ready to start */ + const val SND_PCM_STATE_PREPARED = 2 + /** Running */ + const val SND_PCM_STATE_RUNNING = 3 + /** Stopped: underrun (playback) or overrun (capture) detected */ + const val SND_PCM_STATE_XRUN = 4 + /** Draining: running (playback) or stopped (capture) */ + const val SND_PCM_STATE_DRAINING = 5 + /** Paused */ + const val SND_PCM_STATE_PAUSED = 6 + /** Hardware is suspended */ + const val SND_PCM_STATE_SUSPENDED = 7 + /** Hardware is disconnected */ + const val SND_PCM_STATE_DISCONNECTED = 8 + + + init { + Native.register("libasound.so.2") + } +} + +/* +➜ korge git:(main) ✗ cat ~/alsatest.c +/* + * Simple sound playback using ALSA API and libasound. + * + * Compile: + * $ cc -o play sound_playback.c -lasound + * + * Usage: + * $ ./play < + * + * Examples: + * $ ./play 44100 2 5 < /dev/urandom + * $ ./play 22050 1 8 < /path/to/file.wav + * + * Copyright (C) 2009 Alessandro Ghedini + * -------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * Alessandro Ghedini wrote this file. As long as you retain this + * notice you can do whatever you want with this stuff. If we + * meet some day, and you think this stuff is worth it, you can + * buy me a beer in return. + * -------------------------------------------------------------- + */ + +#include +#include + +#define PCM_DEVICE "default" + +int main(int argc, char **argv) { + unsigned int pcm, tmp, dir; + int rate, channels, seconds; + snd_pcm_t *pcm_handle; + snd_pcm_hw_params_t *params; + snd_pcm_uframes_t frames; + char *buff; + int buff_size, loops; + + if (argc < 4) { + printf("Usage: %s \n", + argv[0]); + return -1; + } + + rate = atoi(argv[1]); + channels = atoi(argv[2]); + seconds = atoi(argv[3]); + + /* Open the PCM device in playback mode */ + if (pcm = snd_pcm_open(&pcm_handle, PCM_DEVICE, + SND_PCM_STREAM_PLAYBACK, 0) < 0) + printf("ERROR: Can't open \"%s\" PCM device. %s\n", + PCM_DEVICE, snd_strerror(pcm)); + + /* Allocate parameters object and fill it with default values*/ + snd_pcm_hw_params_alloca(¶ms); + + snd_pcm_hw_params_any(pcm_handle, params); + + /* Set parameters */ + if (pcm = snd_pcm_hw_params_set_access(pcm_handle, params, + SND_PCM_ACCESS_RW_INTERLEAVED) < 0) + printf("ERROR: Can't set interleaved mode. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params_set_format(pcm_handle, params, + SND_PCM_FORMAT_S16_LE) < 0) + printf("ERROR: Can't set format. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params_set_channels(pcm_handle, params, channels) < 0) + printf("ERROR: Can't set channels number. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params_set_rate_near(pcm_handle, params, &rate, 0) < 0) + printf("ERROR: Can't set rate. %s\n", snd_strerror(pcm)); + + /* Write parameters */ + if (pcm = snd_pcm_hw_params(pcm_handle, params) < 0) + printf("ERROR: Can't set harware parameters. %s\n", snd_strerror(pcm)); + + /* Resume information */ + printf("PCM name: '%s'\n", snd_pcm_name(pcm_handle)); + + printf("PCM state: %s\n", snd_pcm_state_name(snd_pcm_state(pcm_handle))); + + snd_pcm_hw_params_get_channels(params, &tmp); + printf("channels: %i ", tmp); + + if (tmp == 1) + printf("(mono)\n"); + else if (tmp == 2) + printf("(stereo)\n"); + + snd_pcm_hw_params_get_rate(params, &tmp, 0); + printf("rate: %d bps\n", tmp); + + printf("seconds: %d\n", seconds); + + /* Allocate buffer to hold single period */ + snd_pcm_hw_params_get_period_size(params, &frames, 0); + + buff_size = frames * channels * 2 /* 2 -> sample size */; + buff = (char *) malloc(buff_size); + + snd_pcm_hw_params_get_period_time(params, &tmp, NULL); + + for (loops = (seconds * 1000000) / tmp; loops > 0; loops--) { + + if (pcm = read(0, buff, buff_size) == 0) { + printf("Early end of file.\n"); + return 0; + } + + if (pcm = snd_pcm_writei(pcm_handle, buff, frames) == -EPIPE) { + printf("XRUN.\n"); + snd_pcm_prepare(pcm_handle); + } else if (pcm < 0) { + printf("ERROR. Can't write to PCM device. %s\n", snd_strerror(pcm)); + } + + } + + snd_pcm_drain(pcm_handle); + snd_pcm_close(pcm_handle); + free(buff); + + return 0; +}% + */ From b29f068e06e68e1b9d2d8caf05c7e06f18c79431 Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 6 Feb 2023 09:54:43 +0100 Subject: [PATCH 2/4] Linux ALSA support on linux --- .../com/soywiz/kds/thread/NativeThread.kt | 22 ++ .../com/soywiz/korau/sound/impl/alsa/Alsa.kt | 233 +++++++++---- .../korau/sound/ALSANativeSoundProvider.kt | 189 +++++++++++ .../korau/sound/OpenALNativeSoundProvider.kt | 306 ++++++++++++++++++ 4 files changed, 683 insertions(+), 67 deletions(-) create mode 100644 kds/src/nativeMain/kotlin/com/soywiz/kds/thread/NativeThread.kt create mode 100644 korau/src/linuxMain/kotlin/com/soywiz/korau/sound/ALSANativeSoundProvider.kt create mode 100644 korau/src/linuxMain/kotlin/com/soywiz/korau/sound/OpenALNativeSoundProvider.kt diff --git a/kds/src/nativeMain/kotlin/com/soywiz/kds/thread/NativeThread.kt b/kds/src/nativeMain/kotlin/com/soywiz/kds/thread/NativeThread.kt new file mode 100644 index 0000000000..fc07610b17 --- /dev/null +++ b/kds/src/nativeMain/kotlin/com/soywiz/kds/thread/NativeThread.kt @@ -0,0 +1,22 @@ +package com.soywiz.kds.thread + +import kotlin.native.concurrent.* + +class NativeThread(val code: () -> Unit) { + var isDaemon: Boolean = false + + fun start() { + val worker = Worker.start() + worker.executeAfter { + try { + code() + } finally { + worker.requestTermination() + } + } + } + + fun interrupt() { + // No operation + } +} diff --git a/korau/src/jvmMain/kotlin/com/soywiz/korau/sound/impl/alsa/Alsa.kt b/korau/src/jvmMain/kotlin/com/soywiz/korau/sound/impl/alsa/Alsa.kt index af4d837fec..f76503eb87 100644 --- a/korau/src/jvmMain/kotlin/com/soywiz/korau/sound/impl/alsa/Alsa.kt +++ b/korau/src/jvmMain/kotlin/com/soywiz/korau/sound/impl/alsa/Alsa.kt @@ -1,31 +1,87 @@ package com.soywiz.korau.sound.impl.alsa +import com.soywiz.kds.lock.* import com.soywiz.klock.* +import com.soywiz.kmem.* import com.soywiz.korau.sound.* -import com.sun.jna.Library +import com.soywiz.korio.async.* +import com.soywiz.korio.file.std.* import com.sun.jna.Memory import com.sun.jna.Native import com.sun.jna.Pointer -import java.util.Random +import kotlinx.coroutines.* +import kotlin.coroutines.* + +object ALSAExample { + @JvmStatic + fun main(args: Array) { + runBlocking { + val sp = ALSANativeSoundProvider() + //val sp = JnaOpenALNativeSoundProvider() + val job1 = launch(coroutineContext) { + //sp.playAndWait(AudioTone.generate(10.seconds, 400.0).toStream()) + sp.playAndWait(resourcesVfs["Snowland.mp3"].readMusic().toStream()) + } + val job2 = launch(coroutineContext) { + //sp.playAndWait(AudioTone.generate(10.seconds, 200.0).toStream()) + } + println("Waiting...") + job1.join() + job2.join() + println("Done") + } + } +} -object AlsaTest { - @JvmStatic fun main(args: Array) { - val cmpPtr = Memory(1024L).also { it.clear() } - val params = Memory(1024L).also { it.clear() } - val temp = Memory(1024L).also { it.clear() } +class ALSANativeSoundProvider : NativeSoundProvider() { + override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { + return ALSAPlatformAudioOutput(this, coroutineContext, freq) + } +} + +class ALSAPlatformAudioOutput( + val soundProvider: ALSANativeSoundProvider, + coroutineContext: CoroutineContext, + frequency: Int, +) : PlatformAudioOutput(coroutineContext, frequency) { + val channels = 2 + val cmpPtr = Memory(1024L).also { it.clear() } + val params = Memory(1024L).also { it.clear() } + val temp = Memory(1024L).also { it.clear() } + var pcm: Pointer? = Pointer.NULL + private val lock = Lock() + val sdeque = AudioSamplesDeque(channels) + var running = true + var thread: Thread? = null + + init { + start() + } + + override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { + if (!ASound2.initialized) return super.add(samples, offset, size) + + while (running && lock { sdeque.availableRead > 4 * 1024 }) { + delay(10.milliseconds) + } + lock { sdeque.write(samples, offset, size) } + } + + override fun start() { + sdeque.clear() + running = true - val channels = 2 - val rate = 44100 + if (!ASound2.initialized) return //cmpPtr.clear() //cmpPtr.setLong(0L, 0L) - println("test") + //println("test") ASound2.snd_pcm_open(cmpPtr, "default", ASound2.SND_PCM_STREAM_PLAYBACK, 0).also { if (it != 0) error("Can't initialize ALSA") } - val pcm = cmpPtr.getPointer(0L) - println("pcm=$pcm") - println(ASound2.snd_pcm_hw_params_any(pcm, params)) + pcm = cmpPtr.getPointer(0L) + //println("pcm=$pcm") + ASound2.snd_pcm_hw_params_any(pcm, params) ASound2.snd_pcm_hw_params_set_access(pcm, params, ASound2.SND_PCM_ACCESS_RW_INTERLEAVED).also { if (it != 0) error("Error calling snd_pcm_hw_params_set_access=$it") } @@ -35,15 +91,15 @@ object AlsaTest { ASound2.snd_pcm_hw_params_set_channels(pcm, params, channels).also { if (it != 0) error("Error calling snd_pcm_hw_params_set_channels=$it") } - ASound2.snd_pcm_hw_params_set_rate(pcm, params, rate, +1).also { + ASound2.snd_pcm_hw_params_set_rate(pcm, params, frequency, +1).also { if (it != 0) error("Error calling snd_pcm_hw_params_set_rate=$it") } ASound2.snd_pcm_hw_params(pcm, params).also { if (it != 0) error("Error calling snd_pcm_hw_params=$it") } - println(ASound2.snd_pcm_name(pcm)) - println(ASound2.snd_pcm_state_name(ASound2.snd_pcm_state(pcm))) + //println(ASound2.snd_pcm_name(pcm)) + //println(ASound2.snd_pcm_state_name(ASound2.snd_pcm_state(pcm))) ASound2.snd_pcm_hw_params_get_channels(params, temp).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_channels=$it") } @@ -52,11 +108,65 @@ object AlsaTest { val crate = temp.getInt(0L) ASound2.snd_pcm_hw_params_get_period_size(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_period_size=$it") } val frames = temp.getInt(0L) - println("cchannels: $cchannels, rate=$crate, frames=$frames") + //println("cchannels: $cchannels, rate=$crate, frames=$frames") val buff = Memory((frames * channels * 2).toLong()).also { it.clear() } ASound2.snd_pcm_hw_params_get_period_time(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_period_size=$it") } //val random = Random(0L) + thread = Thread { + val samples = AudioSamplesInterleaved(channels, frames) + try { + mainLoop@ while (running) { + while (lock { sdeque.availableRead < frames }) { + if (!running) break@mainLoop + Thread.sleep(1L) + } + val readCount = lock { sdeque.read(samples, 0, frames) } + //println("readCount=$readCount") + val panning = this.panning.toFloat() + //val panning = -1f + //val panning = +0f + //val panning = +1f + val volume = this.volume.toFloat().clamp01() + for (ch in 0 until channels) { + val pan = (if (ch == 0) -panning else +panning) + 1f + val npan = pan.clamp01() + val rscale: Float = npan * volume + //println("panning=$panning, volume=$volume, pan=$pan, npan=$npan, rscale=$rscale") + for (n in 0 until readCount) { + buff.setShort( + ((n * channels + ch) * 2).toLong(), + (samples[ch, n] * rscale).toInt().toShort() + ) + } + } + val result = ASound2.snd_pcm_writei(pcm, buff, frames) + //println("result=$result") + if (result == -ASound2.EPIPE) { + ASound2.snd_pcm_prepare(pcm) + } + } + } catch (e: InterruptedException) { + // Done + } + }.also { + it.isDaemon = true + it.start() + } + } + + override fun stop() { + running = false + thread?.interrupt() + if (!ASound2.initialized) return + + ASound2.snd_pcm_drain(pcm) + ASound2.snd_pcm_close(pcm) + } +} +object AlsaTest { + @JvmStatic fun main(args: Array) { + /* val data = AudioTone.generate(1.seconds, 400.0) var nn = 0 @@ -73,76 +183,65 @@ object AlsaTest { ASound2.snd_pcm_prepare(pcm) } } + */ - ASound2.snd_pcm_drain(pcm) - ASound2.snd_pcm_close(pcm) } } object ASound2 { - @JvmStatic external fun snd_pcm_open(pcmp: Pointer, name: String, stream: Int, mode: Int): Int - @JvmStatic external fun snd_pcm_hw_params_any(pcmp: Pointer, params: Pointer): Int - @JvmStatic external fun snd_pcm_hw_params_set_access(pcmp: Pointer, params: Pointer, access: Int): Int - @JvmStatic external fun snd_pcm_hw_params_set_format(pcmp: Pointer, params: Pointer, format: Int): Int - @JvmStatic external fun snd_pcm_hw_params_set_channels(pcmp: Pointer, params: Pointer, channels: Int): Int - @JvmStatic external fun snd_pcm_hw_params_set_rate(pcmp: Pointer, params: Pointer, rate: Int, dir: Int): Int - @JvmStatic external fun snd_pcm_hw_params(pcmp: Pointer, params: Pointer): Int - @JvmStatic external fun snd_pcm_name(pcmp: Pointer): String - @JvmStatic external fun snd_pcm_state(pcm: Pointer): Int + var initialized = false + + @JvmStatic external fun snd_pcm_open(pcmPtr: Pointer?, name: String, stream: Int, mode: Int): Int + @JvmStatic external fun snd_pcm_hw_params_any(pcm: Pointer?, params: Pointer): Int + @JvmStatic external fun snd_pcm_hw_params_set_access(pcm: Pointer?, params: Pointer, access: Int): Int + @JvmStatic external fun snd_pcm_hw_params_set_format(pcm: Pointer?, params: Pointer, format: Int): Int + @JvmStatic external fun snd_pcm_hw_params_set_channels(pcm: Pointer?, params: Pointer, channels: Int): Int + @JvmStatic external fun snd_pcm_hw_params_set_rate(pcm: Pointer?, params: Pointer, rate: Int, dir: Int): Int + @JvmStatic external fun snd_pcm_hw_params(pcm: Pointer?, params: Pointer): Int + @JvmStatic external fun snd_pcm_name(pcm: Pointer?): String + @JvmStatic external fun snd_pcm_state(pcm: Pointer?): Int @JvmStatic external fun snd_pcm_state_name(state: Int): String @JvmStatic external fun snd_pcm_hw_params_get_channels(params: Pointer, out: Pointer): Int @JvmStatic external fun snd_pcm_hw_params_get_rate(params: Pointer?, value: Pointer?, dir: Pointer?): Int @JvmStatic external fun snd_pcm_hw_params_get_period_size(params: Pointer?, value: Pointer?, dir: Pointer?): Int @JvmStatic external fun snd_pcm_hw_params_get_period_time(params: Pointer?, value: Pointer?, dir: Pointer?): Int - @JvmStatic external fun snd_pcm_writei(pcm: Pointer, buffer: Pointer, size: Int): Int - @JvmStatic external fun snd_pcm_prepare(pcm: Pointer): Int - @JvmStatic external fun snd_pcm_drain(pcm: Pointer): Int - @JvmStatic external fun snd_pcm_close(pcm: Pointer): Int - - const val EPIPE = 32 /* Broken pipe */ - const val EBADFD = 77 /* File descriptor in bad state */ - const val ESTRPIPE = 86 /* Streams pipe error */ + @JvmStatic external fun snd_pcm_writei(pcm: Pointer?, buffer: Pointer, size: Int): Int + @JvmStatic external fun snd_pcm_prepare(pcm: Pointer?): Int + @JvmStatic external fun snd_pcm_drain(pcm: Pointer?): Int + @JvmStatic external fun snd_pcm_close(pcm: Pointer?): Int + const val EPIPE = 32 // Broken pipe + const val EBADFD = 77 // File descriptor in bad state + const val ESTRPIPE = 86 // Streams pipe error const val SND_PCM_STREAM_PLAYBACK = 0 const val SND_PCM_STREAM_CAPTURE = 1 - - /** mmap access with simple interleaved channels */ - const val SND_PCM_ACCESS_MMAP_INTERLEAVED = 0 - /** mmap access with simple non interleaved channels */ - const val SND_PCM_ACCESS_MMAP_NONINTERLEAVED = 1 - /** mmap access with complex placement */ - const val SND_PCM_ACCESS_MMAP_COMPLEX = 2 - /** snd_pcm_readi/snd_pcm_writei access */ - const val SND_PCM_ACCESS_RW_INTERLEAVED = 3 - /** snd_pcm_readn/snd_pcm_writen access */ - const val SND_PCM_ACCESS_RW_NONINTERLEAVED = 4 + const val SND_PCM_ACCESS_MMAP_INTERLEAVED = 0 // mmap access with simple interleaved channels + const val SND_PCM_ACCESS_MMAP_NONINTERLEAVED = 1 // mmap access with simple non interleaved channels + const val SND_PCM_ACCESS_MMAP_COMPLEX = 2 // mmap access with complex placement + const val SND_PCM_ACCESS_RW_INTERLEAVED = 3 // snd_pcm_readi/snd_pcm_writei access + const val SND_PCM_ACCESS_RW_NONINTERLEAVED = 4 // /snd_pcm_writen access const val SND_PCM_FORMAT_S16_LE = 2 - /** Open */ - const val SND_PCM_STATE_OPEN = 0 - /** Setup installed */ - const val SND_PCM_STATE_SETUP = 1 - /** Ready to start */ - const val SND_PCM_STATE_PREPARED = 2 - /** Running */ - const val SND_PCM_STATE_RUNNING = 3 - /** Stopped: underrun (playback) or overrun (capture) detected */ - const val SND_PCM_STATE_XRUN = 4 - /** Draining: running (playback) or stopped (capture) */ - const val SND_PCM_STATE_DRAINING = 5 - /** Paused */ - const val SND_PCM_STATE_PAUSED = 6 - /** Hardware is suspended */ - const val SND_PCM_STATE_SUSPENDED = 7 - /** Hardware is disconnected */ - const val SND_PCM_STATE_DISCONNECTED = 8 - + const val SND_PCM_STATE_OPEN = 0 // Open + const val SND_PCM_STATE_SETUP = 1 // Setup installed + const val SND_PCM_STATE_PREPARED = 2 // Ready to start + const val SND_PCM_STATE_RUNNING = 3 // Running + const val SND_PCM_STATE_XRUN = 4 // Stopped: underrun (playback) or overrun (capture) detected + const val SND_PCM_STATE_DRAINING = 5 // Draining: running (playback) or stopped (capture) + const val SND_PCM_STATE_PAUSED = 6 // Paused + const val SND_PCM_STATE_SUSPENDED = 7 // Hardware is suspended + const val SND_PCM_STATE_DISCONNECTED = 8 // Hardware is disconnected init { - Native.register("libasound.so.2") + try { + Native.register("libasound.so.2") + initialized = true + } catch (e: Throwable) { + e.printStackTrace() + } } } diff --git a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/ALSANativeSoundProvider.kt b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/ALSANativeSoundProvider.kt new file mode 100644 index 0000000000..552694ed79 --- /dev/null +++ b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/ALSANativeSoundProvider.kt @@ -0,0 +1,189 @@ +package com.soywiz.korau.sound + +import com.soywiz.kds.lock.* +import com.soywiz.kds.thread.* +import com.soywiz.klock.* +import com.soywiz.kmem.* +import com.soywiz.kmem.dyn.* +import com.soywiz.korio.async.* +import kotlinx.cinterop.* +import kotlinx.coroutines.* +import kotlin.coroutines.* + +val alsaNativeSoundProvider: ALSANativeSoundProvider? by lazy { + try { + ALSANativeSoundProvider() + } catch (e: Throwable) { + e.printStackTrace() + null + } +} + +class ALSANativeSoundProvider : NativeSoundProvider() { + init { + //println("ALSANativeSoundProvider.init") + } + override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { + //println("ALSANativeSoundProvider.createPlatformAudioOutput(freq=$freq)") + return ALSAPlatformAudioOutput(this, coroutineContext, freq) + } +} + +class ALSAPlatformAudioOutput( + val soundProvider: ALSANativeSoundProvider, + coroutineContext: CoroutineContext, + frequency: Int, +) : PlatformAudioOutput(coroutineContext, frequency) { + val arena = Arena() + val channels = 2 + var pcm: COpaquePointer? = null + private val lock = Lock() + val sdeque = AudioSamplesDeque(channels) + var running = true + var thread: NativeThread? = null + + init { + start() + } + + override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { + if (!ASound2.initialized) return super.add(samples, offset, size) + + while (running && lock { sdeque.availableRead > 4 * 1024 }) { + delay(10.milliseconds) + } + lock { sdeque.write(samples, offset, size) } + } + + override fun start() { + sdeque.clear() + running = true + + if (!ASound2.initialized) return + + arena.clear() + val cmpPtr: CPointer>> = arena.allocArray(16) + val params: CPointer = arena.allocArray(16).reinterpret() + val temp: CPointer> = arena.allocArray(16) + + //cmpPtr.clear() + //cmpPtr.setLong(0L, 0L) + //println("ALSANativeSoundProvider.snd_pcm_open") + ASound2.snd_pcm_open(cmpPtr, "default".cstr.placeTo(arena), ASound2.SND_PCM_STREAM_PLAYBACK, 0).also { if (it != 0) error("Can't initialize ALSA") } + pcm = cmpPtr[0] + //println("ALSANativeSoundProvider.snd_pcm_open: pcm=$pcm") + ASound2.snd_pcm_hw_params_any(pcm, params) + ASound2.snd_pcm_hw_params_set_access(pcm, params, ASound2.SND_PCM_ACCESS_RW_INTERLEAVED).also { if (it != 0) error("Error calling snd_pcm_hw_params_set_access=$it") } + ASound2.snd_pcm_hw_params_set_format(pcm, params, ASound2.SND_PCM_FORMAT_S16_LE).also { if (it != 0) error("Error calling snd_pcm_hw_params_set_format=$it") } + ASound2.snd_pcm_hw_params_set_channels(pcm, params, channels).also { if (it != 0) error("Error calling snd_pcm_hw_params_set_channels=$it") } + ASound2.snd_pcm_hw_params_set_rate(pcm, params, frequency, +1).also { if (it != 0) error("Error calling snd_pcm_hw_params_set_rate=$it") } + ASound2.snd_pcm_hw_params(pcm, params).also { if (it != 0) error("Error calling snd_pcm_hw_params=$it") } + + //println(ASound2.snd_pcm_name(pcm)) + //println(ASound2.snd_pcm_state_name(ASound2.snd_pcm_state(pcm))) + ASound2.snd_pcm_hw_params_get_channels(params, temp).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_channels=$it") } + val cchannels = temp[0] + ASound2.snd_pcm_hw_params_get_rate(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_rate=$it") } + val crate = temp[0] + ASound2.snd_pcm_hw_params_get_period_size(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_period_size=$it") } + val frames = temp[0] + //println("cchannels: $cchannels, rate=$crate, frames=$frames") + ASound2.snd_pcm_hw_params_get_period_time(params, temp, null).also { if (it != 0) error("Error calling snd_pcm_hw_params_get_period_size=$it") } + //val random = Random(0L) + //println("ALSANativeSoundProvider: Before starting Sound thread!") + thread = NativeThread { + //println("ALSANativeSoundProvider: Started Sound thread!") + memScoped { + val buff = allocArray(frames * channels) + val samples = AudioSamplesInterleaved(channels, frames) + mainLoop@ while (running) { + while (lock { sdeque.availableRead < frames }) { + if (!running) break@mainLoop + blockingSleep(1.milliseconds) + } + val readCount = lock { sdeque.read(samples, 0, frames) } + //println("ALSANativeSoundProvider: readCount=$readCount") + val panning = this@ALSAPlatformAudioOutput.panning.toFloat() + //val panning = -1f + //val panning = +0f + //val panning = +1f + val volume = this@ALSAPlatformAudioOutput.volume.toFloat().clamp01() + for (ch in 0 until channels) { + val pan = (if (ch == 0) -panning else +panning) + 1f + val npan = pan.clamp01() + val rscale: Float = npan * volume + //println("panning=$panning, volume=$volume, pan=$pan, npan=$npan, rscale=$rscale") + for (n in 0 until readCount) { + buff[n * channels + ch] = (samples[ch, n] * rscale).toInt().toShort() + } + } + val result = ASound2.snd_pcm_writei(pcm, buff, frames) + //println("result=$result") + if (result == -ASound2.EPIPE) { + ASound2.snd_pcm_prepare(pcm) + } + } + } + }.also { + it.isDaemon = true + it.start() + } + } + + override fun stop() { + running = false + thread?.interrupt() + if (!ASound2.initialized) return + + ASound2.snd_pcm_drain(pcm) + ASound2.snd_pcm_close(pcm) + arena.clear() + } +} + +internal object ASound2 : DynamicLibrary("libasound.so.2") { + inline val initialized: Boolean get() = isAvailable + val snd_pcm_open by func<(pcmPtr: COpaquePointer?, name: CPointer, stream: Int, mode: Int) -> Int>() + val snd_pcm_hw_params_any by func<(pcm: COpaquePointer?, params: COpaquePointer) -> Int>() + val snd_pcm_hw_params_set_access by func<(pcm: COpaquePointer?, params: COpaquePointer, access: Int) -> Int>() + val snd_pcm_hw_params_set_format by func<(pcm: COpaquePointer?, params: COpaquePointer, format: Int) -> Int>() + val snd_pcm_hw_params_set_channels by func<(pcm: COpaquePointer?, params: COpaquePointer, channels: Int) -> Int>() + val snd_pcm_hw_params_set_rate by func<(pcm: COpaquePointer?, params: COpaquePointer, rate: Int, dir: Int) -> Int>() + val snd_pcm_hw_params by func<(pcm: COpaquePointer?, params: COpaquePointer) -> Int>() + val snd_pcm_name by func<(pcm: COpaquePointer?) -> CPointer>() + val snd_pcm_state by func<(pcm: COpaquePointer?) -> Int>() + val snd_pcm_state_name by func<(state: Int) -> CPointer>() + val snd_pcm_hw_params_get_channels by func<(params: COpaquePointer, out: COpaquePointer) -> Int>() + val snd_pcm_hw_params_get_rate by func<(params: COpaquePointer?, value: COpaquePointer?, dir: COpaquePointer?) -> Int>() + val snd_pcm_hw_params_get_period_size by func<(params: COpaquePointer?, value: COpaquePointer?, dir: COpaquePointer?) -> Int>() + val snd_pcm_hw_params_get_period_time by func<(params: COpaquePointer?, value: COpaquePointer?, dir: COpaquePointer?) -> Int>() + val snd_pcm_writei by func<(pcm: COpaquePointer?, buffer: COpaquePointer, size: Int) -> Int>() + val snd_pcm_prepare by func<(pcm: COpaquePointer?) -> Int>() + val snd_pcm_drain by func<(pcm: COpaquePointer?) -> Int>() + val snd_pcm_close by func<(pcm: COpaquePointer?) -> Int>() + + const val EPIPE = 32 // Broken pipe + const val EBADFD = 77 // File descriptor in bad state + const val ESTRPIPE = 86 // Streams pipe error + + const val SND_PCM_STREAM_PLAYBACK = 0 + const val SND_PCM_STREAM_CAPTURE = 1 + + const val SND_PCM_ACCESS_MMAP_INTERLEAVED = 0 // mmap access with simple interleaved channels + const val SND_PCM_ACCESS_MMAP_NONINTERLEAVED = 1 // mmap access with simple non interleaved channels + const val SND_PCM_ACCESS_MMAP_COMPLEX = 2 // mmap access with complex placement + const val SND_PCM_ACCESS_RW_INTERLEAVED = 3 // snd_pcm_readi/snd_pcm_writei access + const val SND_PCM_ACCESS_RW_NONINTERLEAVED = 4 // /snd_pcm_writen access + + const val SND_PCM_FORMAT_S16_LE = 2 + + const val SND_PCM_STATE_OPEN = 0 // Open + const val SND_PCM_STATE_SETUP = 1 // Setup installed + const val SND_PCM_STATE_PREPARED = 2 // Ready to start + const val SND_PCM_STATE_RUNNING = 3 // Running + const val SND_PCM_STATE_XRUN = 4 // Stopped: underrun (playback) or overrun (capture) detected + const val SND_PCM_STATE_DRAINING = 5 // Draining: running (playback) or stopped (capture) + const val SND_PCM_STATE_PAUSED = 6 // Paused + const val SND_PCM_STATE_SUSPENDED = 7 // Hardware is suspended + const val SND_PCM_STATE_DISCONNECTED = 8 // Hardware is disconnected +} diff --git a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/OpenALNativeSoundProvider.kt b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/OpenALNativeSoundProvider.kt new file mode 100644 index 0000000000..c821315ec7 --- /dev/null +++ b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/OpenALNativeSoundProvider.kt @@ -0,0 +1,306 @@ +package com.soywiz.korau.sound + +import com.soywiz.klock.TimeSpan +import com.soywiz.klock.milliseconds +import com.soywiz.klock.seconds +import com.soywiz.korau.format.AudioDecodingProps +import com.soywiz.korio.async.delay +import com.soywiz.korio.async.launchImmediately +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert +import kotlinx.cinterop.invoke +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.usePinned +import kotlinx.cinterop.value +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext +import kotlin.math.sqrt + +val openalNativeSoundProvider: OpenALNativeSoundProvider? by lazy { + try { + OpenALNativeSoundProvider() + } catch (e: Throwable) { + e.printStackTrace() + null + } +} + +class OpenALNativeSoundProvider : NativeSoundProvider() { + val device = AL.alcOpenDevice(null) + //val device: CPointer? = null + val context = device?.let { AL.alcCreateContext(it, null).also { + AL.alcMakeContextCurrent(it) + memScoped { + AL.alListener3f(AL.AL_POSITION, 0f, 0f, 1.0f) + AL.alListener3f(AL.AL_VELOCITY, 0f, 0f, 0f) + val listenerOri = floatArrayOf(0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f) + listenerOri.usePinned { + AL.alListenerfv(AL.AL_ORIENTATION, it.addressOf(0)) + } + } + } } + + internal fun makeCurrent() { + AL.alcMakeContextCurrent(context) + } + + override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound { + return if (streaming) { + super.createSound(data, streaming, props, name) + } else { + OpenALNativeSoundNoStream(this, coroutineContext, audioFormats.decode(data, props), name = name) + } + } + + override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { + return OpenALPlatformAudioOutput(this, coroutineContext, freq) + } +} + +class OpenALPlatformAudioOutput( + val provider: OpenALNativeSoundProvider, + coroutineContext: CoroutineContext, + freq: Int, + val sourceProvider: SourceProvider = SourceProvider(0.convert()) +) : PlatformAudioOutput(coroutineContext, freq) { + val sourceProv = JnaSoundPropsProvider(sourceProvider) + override var availableSamples: Int = 0 + + override var pitch: Double by sourceProv::pitch + override var volume: Double by sourceProv::volume + override var panning: Double by sourceProv::panning + + var source: ALuint + get() = sourceProvider.source + set(value) { sourceProvider.source = value } + + //val source + + //alSourceQueueBuffers + + //val buffersPool = Pool(6) { all.alGenBuffer() } + //val buffers = IntArray(32) + //val nbuffers = 6 + //val buffers = IntArray(nbuffers) + + init { + start() + } + + override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { + availableSamples += samples.totalSamples + try { + memScoped { + provider.makeCurrent() + val tempBuffers = alloc() + ensureSource() + while (true) { + //val buffer = al.alGetSourcei(source, AL.AL_BUFFER) + //val sampleOffset = al.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) + val processed = AL.alGetSourcei(source, AL.AL_BUFFERS_PROCESSED) + val queued = AL.alGetSourcei(source, AL.AL_BUFFERS_QUEUED) + val total = processed + queued + val state = AL.alGetSourceState(source) + val playing = state == AL.AL_PLAYING + + //println("buffer=$buffer, processed=$processed, queued=$queued, state=$state, playing=$playing, sampleOffset=$sampleOffset") + //println("Samples.add") + + if (processed <= 0 && total >= 6) { + delay(10.milliseconds) + continue + } + + if (total < 6) { + tempBuffers.value = AL.alGenBuffer() + AL.checkAlErrors("alGenBuffers") + //println("alGenBuffers: ${tempBuffers[0]}") + } else { + AL.alSourceUnqueueBuffers(source, 1, tempBuffers.ptr) + AL.checkAlErrors("alSourceUnqueueBuffers") + //println("alSourceUnqueueBuffers: ${tempBuffers[0]}") + } + //println("samples: $samples - $offset, $size") + //al.alBufferData(tempBuffers[0], samples.copyOfRange(offset, offset + size), frequency, panning, volume) + AL.alBufferData(tempBuffers.value, samples.copyOfRange(offset, offset + size), frequency, panning) + AL.alSourceQueueBuffers(source, 1, tempBuffers.ptr) + AL.checkAlErrors("alSourceQueueBuffers") + + //val gain = al.alGetSourcef(source, AL.AL_GAIN) + //val pitch = al.alGetSourcef(source, AL.AL_PITCH) + //println("gain=$gain, pitch=$pitch") + if (!playing) { + AL.alSourcePlay(source) + } + break + } + } + } finally { + availableSamples -= samples.totalSamples + } + } + + fun ensureSource() { + if (source.toInt() != 0) return + provider.makeCurrent() + + source = AL.alGenSource() + //for (n in buffers.indices) buffers[n] = alGenBuffer() .toInt() + } + + override fun start() { + ensureSource() + AL.alSourcePlay(source) + AL.checkAlErrors("alSourcePlay") + //checkAlErrors() + } + + override fun stop() { + provider.makeCurrent() + + AL.alSourceStop(source) + if (source.toInt() != 0) { + AL.alDeleteSource(source) + source = 0.convert() + } + //for (n in buffers.indices) { + // if (buffers[n] != 0) { + // alDeleteBuffer(buffers[n]) + // buffers[n] = 0 + // } + //} + } +} + +// https://ffainelli.github.io/openal-example/ +class OpenALNativeSoundNoStream( + val provider: OpenALNativeSoundProvider, + coroutineContext: CoroutineContext, + val data: AudioData?, + val sourceProvider: SourceProvider = SourceProvider(0.convert()), + override val name: String = "Unknown" +) : Sound(coroutineContext), SoundProps by JnaSoundPropsProvider(sourceProvider) { + override suspend fun decode(maxSamples: Int): AudioData = data ?: AudioData.DUMMY + + var source: ALuint + get() = sourceProvider.source + set(value) { sourceProvider.source = value } + + override val length: TimeSpan get() = data?.totalTime ?: 0.seconds + + override fun play(coroutineContext: CoroutineContext, params: PlaybackParameters): SoundChannel { + val data = data ?: return DummySoundChannel(this) + provider.makeCurrent() + val buffer = AL.alGenBuffer() + AL.alBufferData(buffer, data, panning, volume) + + source = AL.alGenSource() + AL.alSourcei(source, AL.AL_BUFFER, buffer.convert()) + AL.checkAlErrors("alSourcei") + + var stopped = false + + val channel = object : SoundChannel(this), SoundProps by JnaSoundPropsProvider(sourceProvider) { + val totalSamples get() = data.totalSamples + var currentSampleOffset: Int + get() = AL.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) + set(value) { + AL.alSourcei(source, AL.AL_SAMPLE_OFFSET, value) + } + + override var current: TimeSpan + get() = data.timeAtSample(currentSampleOffset) + set(value) { AL.alSourcef(source, AL.AL_SEC_OFFSET, value.seconds.toFloat()) } + override val total: TimeSpan get() = data.totalTime + + override val state: SoundChannelState get() { + val result = AL.alGetSourceState(source) + AL.checkAlErrors("alGetSourceState") + return when (result) { + AL.AL_INITIAL -> SoundChannelState.INITIAL + AL.AL_PLAYING -> SoundChannelState.PLAYING + AL.AL_PAUSED -> SoundChannelState.PAUSED + AL.AL_STOPPED -> SoundChannelState.STOPPED + else -> SoundChannelState.STOPPED + } + } + + override fun pause() { + AL.alSourcePause(source) + } + + override fun resume() { + AL.alSourcePlay(source) + } + + override fun stop() { + if (!stopped) { + stopped = true + AL.alDeleteSource(source) + AL.alDeleteBuffer(buffer) + } + } + }.also { + it.copySoundPropsFrom(params) + } + launchImmediately(coroutineContext[ContinuationInterceptor] ?: coroutineContext) { + var times = params.times + var startTime = params.startTime + try { + while (times.hasMore) { + times = times.oneLess + channel.reset() + AL.alSourcef(source, AL.AL_SEC_OFFSET, startTime.seconds.toFloat()) + AL.alSourcePlay(source) + //checkAlErrors("alSourcePlay") + startTime = 0.seconds + while (channel.playing) delay(1L) + } + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + e.printStackTrace() + } finally { + channel.stop() + } + } + return channel + } +} + +data class SourceProvider(var source: ALuint) + +class JnaSoundPropsProvider(val sourceProvider: SourceProvider) : SoundProps { + val source get() = sourceProvider.source + + private val temp1 = FloatArray(3) + private val temp2 = FloatArray(3) + private val temp3 = FloatArray(3) + + override var pitch: Double + get() = AL.alGetSourcef(source, AL.AL_PITCH).toDouble() + set(value) = AL.alSourcef(source, AL.AL_PITCH, value.toFloat()) + override var volume: Double + get() = AL.alGetSourcef(source, AL.AL_GAIN).toDouble() + set(value) = AL.alSourcef(source, AL.AL_GAIN, value.toFloat()) + override var panning: Double + get() = memScoped { + val temp1 = alloc() + val temp2 = alloc() + val temp3 = alloc() + AL.alGetSource3f(source, AL.AL_POSITION, temp1.ptr, temp2.ptr, temp3.ptr) + temp1.value.toDouble() + } + set(value) { + val pan = value.toFloat() + AL.alSourcef(source, AL.AL_ROLLOFF_FACTOR, 0.0f); + AL.alSourcei(source, AL.AL_SOURCE_RELATIVE, 1); + AL.alSource3f(source, AL.AL_POSITION, pan, 0f, -sqrt(1.0f - pan * pan)); + //println("SET PANNING: source=$source, pan=$pan") + } +} From 68201a278ddf95820479376521b0059a30cceacc Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 6 Feb 2023 10:08:43 +0100 Subject: [PATCH 3/4] Fix K/N linux nativeSoundProvider --- .../korau/sound/NativeNativeSoundProvider.kt | 310 +----------------- .../korau/sound/OpenALNativeSoundProvider.kt | 306 ----------------- .../ALSA.kt} | 4 +- .../korau/sound/{AL.kt => backends/OpenAL.kt} | 306 ++++++++++++++++- 4 files changed, 308 insertions(+), 618 deletions(-) delete mode 100644 korau/src/linuxMain/kotlin/com/soywiz/korau/sound/OpenALNativeSoundProvider.kt rename korau/src/linuxMain/kotlin/com/soywiz/korau/sound/{ALSANativeSoundProvider.kt => backends/ALSA.kt} (99%) rename korau/src/linuxMain/kotlin/com/soywiz/korau/sound/{AL.kt => backends/OpenAL.kt} (57%) diff --git a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/NativeNativeSoundProvider.kt b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/NativeNativeSoundProvider.kt index ef0756fb7b..bd476632bb 100644 --- a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/NativeNativeSoundProvider.kt +++ b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/NativeNativeSoundProvider.kt @@ -1,308 +1,10 @@ package com.soywiz.korau.sound -import com.soywiz.klock.TimeSpan -import com.soywiz.klock.milliseconds -import com.soywiz.klock.seconds -import com.soywiz.korau.format.AudioDecodingProps -import com.soywiz.korio.async.delay -import com.soywiz.korio.async.launchImmediately -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.alloc -import kotlinx.cinterop.convert -import kotlinx.cinterop.invoke -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.usePinned -import kotlinx.cinterop.value -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.delay -import kotlin.coroutines.ContinuationInterceptor -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext -import kotlin.math.sqrt +import com.soywiz.korau.sound.backends.* -val openalNativeSoundProvider: OpenALNativeSoundProvider? by lazy { - try { - OpenALNativeSoundProvider() - } catch (e: Throwable) { - e.printStackTrace() - null - } +actual val nativeSoundProvider: NativeSoundProvider by lazy { + (null as? NativeSoundProvider?) + ?: alsaNativeSoundProvider + ?: openalNativeSoundProvider + ?: DummyNativeSoundProvider } -actual val nativeSoundProvider: NativeSoundProvider get() = openalNativeSoundProvider ?: DummyNativeSoundProvider - -class OpenALNativeSoundProvider : NativeSoundProvider() { - val device = AL.alcOpenDevice(null) - //val device: CPointer? = null - val context = device?.let { AL.alcCreateContext(it, null).also { - AL.alcMakeContextCurrent(it) - memScoped { - AL.alListener3f(AL.AL_POSITION, 0f, 0f, 1.0f) - AL.alListener3f(AL.AL_VELOCITY, 0f, 0f, 0f) - val listenerOri = floatArrayOf(0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f) - listenerOri.usePinned { - AL.alListenerfv(AL.AL_ORIENTATION, it.addressOf(0)) - } - } - } } - - internal fun makeCurrent() { - AL.alcMakeContextCurrent(context) - } - - override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound { - return if (streaming) { - super.createSound(data, streaming, props, name) - } else { - OpenALNativeSoundNoStream(this, coroutineContext, audioFormats.decode(data, props), name = name) - } - } - - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { - return OpenALPlatformAudioOutput(this, coroutineContext, freq) - } -} - -class OpenALPlatformAudioOutput( - val provider: OpenALNativeSoundProvider, - coroutineContext: CoroutineContext, - freq: Int, - val sourceProvider: SourceProvider = SourceProvider(0.convert()) -) : PlatformAudioOutput(coroutineContext, freq) { - val sourceProv = JnaSoundPropsProvider(sourceProvider) - override var availableSamples: Int = 0 - - override var pitch: Double by sourceProv::pitch - override var volume: Double by sourceProv::volume - override var panning: Double by sourceProv::panning - - var source: ALuint - get() = sourceProvider.source - set(value) { sourceProvider.source = value } - - //val source - - //alSourceQueueBuffers - - //val buffersPool = Pool(6) { all.alGenBuffer() } - //val buffers = IntArray(32) - //val nbuffers = 6 - //val buffers = IntArray(nbuffers) - - init { - start() - } - - override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { - availableSamples += samples.totalSamples - try { - memScoped { - provider.makeCurrent() - val tempBuffers = alloc() - ensureSource() - while (true) { - //val buffer = al.alGetSourcei(source, AL.AL_BUFFER) - //val sampleOffset = al.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) - val processed = AL.alGetSourcei(source, AL.AL_BUFFERS_PROCESSED) - val queued = AL.alGetSourcei(source, AL.AL_BUFFERS_QUEUED) - val total = processed + queued - val state = AL.alGetSourceState(source) - val playing = state == AL.AL_PLAYING - - //println("buffer=$buffer, processed=$processed, queued=$queued, state=$state, playing=$playing, sampleOffset=$sampleOffset") - //println("Samples.add") - - if (processed <= 0 && total >= 6) { - delay(10.milliseconds) - continue - } - - if (total < 6) { - tempBuffers.value = AL.alGenBuffer() - AL.checkAlErrors("alGenBuffers") - //println("alGenBuffers: ${tempBuffers[0]}") - } else { - AL.alSourceUnqueueBuffers(source, 1, tempBuffers.ptr) - AL.checkAlErrors("alSourceUnqueueBuffers") - //println("alSourceUnqueueBuffers: ${tempBuffers[0]}") - } - //println("samples: $samples - $offset, $size") - //al.alBufferData(tempBuffers[0], samples.copyOfRange(offset, offset + size), frequency, panning, volume) - AL.alBufferData(tempBuffers.value, samples.copyOfRange(offset, offset + size), frequency, panning) - AL.alSourceQueueBuffers(source, 1, tempBuffers.ptr) - AL.checkAlErrors("alSourceQueueBuffers") - - //val gain = al.alGetSourcef(source, AL.AL_GAIN) - //val pitch = al.alGetSourcef(source, AL.AL_PITCH) - //println("gain=$gain, pitch=$pitch") - if (!playing) { - AL.alSourcePlay(source) - } - break - } - } - } finally { - availableSamples -= samples.totalSamples - } - } - - fun ensureSource() { - if (source.toInt() != 0) return - provider.makeCurrent() - - source = AL.alGenSource() - //for (n in buffers.indices) buffers[n] = alGenBuffer() .toInt() - } - - override fun start() { - ensureSource() - AL.alSourcePlay(source) - AL.checkAlErrors("alSourcePlay") - //checkAlErrors() - } - - override fun stop() { - provider.makeCurrent() - - AL.alSourceStop(source) - if (source.toInt() != 0) { - AL.alDeleteSource(source) - source = 0.convert() - } - //for (n in buffers.indices) { - // if (buffers[n] != 0) { - // alDeleteBuffer(buffers[n]) - // buffers[n] = 0 - // } - //} - } -} - -// https://ffainelli.github.io/openal-example/ -class OpenALNativeSoundNoStream( - val provider: OpenALNativeSoundProvider, - coroutineContext: CoroutineContext, - val data: AudioData?, - val sourceProvider: SourceProvider = SourceProvider(0.convert()), - override val name: String = "Unknown" -) : Sound(coroutineContext), SoundProps by JnaSoundPropsProvider(sourceProvider) { - override suspend fun decode(maxSamples: Int): AudioData = data ?: AudioData.DUMMY - - var source: ALuint - get() = sourceProvider.source - set(value) { sourceProvider.source = value } - - override val length: TimeSpan get() = data?.totalTime ?: 0.seconds - - override fun play(coroutineContext: CoroutineContext, params: PlaybackParameters): SoundChannel { - val data = data ?: return DummySoundChannel(this) - provider.makeCurrent() - val buffer = AL.alGenBuffer() - AL.alBufferData(buffer, data, panning, volume) - - source = AL.alGenSource() - AL.alSourcei(source, AL.AL_BUFFER, buffer.convert()) - AL.checkAlErrors("alSourcei") - - var stopped = false - - val channel = object : SoundChannel(this), SoundProps by JnaSoundPropsProvider(sourceProvider) { - val totalSamples get() = data.totalSamples - var currentSampleOffset: Int - get() = AL.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) - set(value) { - AL.alSourcei(source, AL.AL_SAMPLE_OFFSET, value) - } - - override var current: TimeSpan - get() = data.timeAtSample(currentSampleOffset) - set(value) { AL.alSourcef(source, AL.AL_SEC_OFFSET, value.seconds.toFloat()) } - override val total: TimeSpan get() = data.totalTime - - override val state: SoundChannelState get() { - val result = AL.alGetSourceState(source) - AL.checkAlErrors("alGetSourceState") - return when (result) { - AL.AL_INITIAL -> SoundChannelState.INITIAL - AL.AL_PLAYING -> SoundChannelState.PLAYING - AL.AL_PAUSED -> SoundChannelState.PAUSED - AL.AL_STOPPED -> SoundChannelState.STOPPED - else -> SoundChannelState.STOPPED - } - } - - override fun pause() { - AL.alSourcePause(source) - } - - override fun resume() { - AL.alSourcePlay(source) - } - - override fun stop() { - if (!stopped) { - stopped = true - AL.alDeleteSource(source) - AL.alDeleteBuffer(buffer) - } - } - }.also { - it.copySoundPropsFrom(params) - } - launchImmediately(coroutineContext[ContinuationInterceptor] ?: coroutineContext) { - var times = params.times - var startTime = params.startTime - try { - while (times.hasMore) { - times = times.oneLess - channel.reset() - AL.alSourcef(source, AL.AL_SEC_OFFSET, startTime.seconds.toFloat()) - AL.alSourcePlay(source) - //checkAlErrors("alSourcePlay") - startTime = 0.seconds - while (channel.playing) delay(1L) - } - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - e.printStackTrace() - } finally { - channel.stop() - } - } - return channel - } -} - -data class SourceProvider(var source: ALuint) - -class JnaSoundPropsProvider(val sourceProvider: SourceProvider) : SoundProps { - val source get() = sourceProvider.source - - private val temp1 = FloatArray(3) - private val temp2 = FloatArray(3) - private val temp3 = FloatArray(3) - - override var pitch: Double - get() = AL.alGetSourcef(source, AL.AL_PITCH).toDouble() - set(value) = AL.alSourcef(source, AL.AL_PITCH, value.toFloat()) - override var volume: Double - get() = AL.alGetSourcef(source, AL.AL_GAIN).toDouble() - set(value) = AL.alSourcef(source, AL.AL_GAIN, value.toFloat()) - override var panning: Double - get() = memScoped { - val temp1 = alloc() - val temp2 = alloc() - val temp3 = alloc() - AL.alGetSource3f(source, AL.AL_POSITION, temp1.ptr, temp2.ptr, temp3.ptr) - temp1.value.toDouble() - } - set(value) { - val pan = value.toFloat() - AL.alSourcef(source, AL.AL_ROLLOFF_FACTOR, 0.0f); - AL.alSourcei(source, AL.AL_SOURCE_RELATIVE, 1); - AL.alSource3f(source, AL.AL_POSITION, pan, 0f, -sqrt(1.0f - pan * pan)); - //println("SET PANNING: source=$source, pan=$pan") - } -} - diff --git a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/OpenALNativeSoundProvider.kt b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/OpenALNativeSoundProvider.kt deleted file mode 100644 index c821315ec7..0000000000 --- a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/OpenALNativeSoundProvider.kt +++ /dev/null @@ -1,306 +0,0 @@ -package com.soywiz.korau.sound - -import com.soywiz.klock.TimeSpan -import com.soywiz.klock.milliseconds -import com.soywiz.klock.seconds -import com.soywiz.korau.format.AudioDecodingProps -import com.soywiz.korio.async.delay -import com.soywiz.korio.async.launchImmediately -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.alloc -import kotlinx.cinterop.convert -import kotlinx.cinterop.invoke -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.usePinned -import kotlinx.cinterop.value -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.delay -import kotlin.coroutines.ContinuationInterceptor -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext -import kotlin.math.sqrt - -val openalNativeSoundProvider: OpenALNativeSoundProvider? by lazy { - try { - OpenALNativeSoundProvider() - } catch (e: Throwable) { - e.printStackTrace() - null - } -} - -class OpenALNativeSoundProvider : NativeSoundProvider() { - val device = AL.alcOpenDevice(null) - //val device: CPointer? = null - val context = device?.let { AL.alcCreateContext(it, null).also { - AL.alcMakeContextCurrent(it) - memScoped { - AL.alListener3f(AL.AL_POSITION, 0f, 0f, 1.0f) - AL.alListener3f(AL.AL_VELOCITY, 0f, 0f, 0f) - val listenerOri = floatArrayOf(0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f) - listenerOri.usePinned { - AL.alListenerfv(AL.AL_ORIENTATION, it.addressOf(0)) - } - } - } } - - internal fun makeCurrent() { - AL.alcMakeContextCurrent(context) - } - - override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound { - return if (streaming) { - super.createSound(data, streaming, props, name) - } else { - OpenALNativeSoundNoStream(this, coroutineContext, audioFormats.decode(data, props), name = name) - } - } - - override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { - return OpenALPlatformAudioOutput(this, coroutineContext, freq) - } -} - -class OpenALPlatformAudioOutput( - val provider: OpenALNativeSoundProvider, - coroutineContext: CoroutineContext, - freq: Int, - val sourceProvider: SourceProvider = SourceProvider(0.convert()) -) : PlatformAudioOutput(coroutineContext, freq) { - val sourceProv = JnaSoundPropsProvider(sourceProvider) - override var availableSamples: Int = 0 - - override var pitch: Double by sourceProv::pitch - override var volume: Double by sourceProv::volume - override var panning: Double by sourceProv::panning - - var source: ALuint - get() = sourceProvider.source - set(value) { sourceProvider.source = value } - - //val source - - //alSourceQueueBuffers - - //val buffersPool = Pool(6) { all.alGenBuffer() } - //val buffers = IntArray(32) - //val nbuffers = 6 - //val buffers = IntArray(nbuffers) - - init { - start() - } - - override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { - availableSamples += samples.totalSamples - try { - memScoped { - provider.makeCurrent() - val tempBuffers = alloc() - ensureSource() - while (true) { - //val buffer = al.alGetSourcei(source, AL.AL_BUFFER) - //val sampleOffset = al.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) - val processed = AL.alGetSourcei(source, AL.AL_BUFFERS_PROCESSED) - val queued = AL.alGetSourcei(source, AL.AL_BUFFERS_QUEUED) - val total = processed + queued - val state = AL.alGetSourceState(source) - val playing = state == AL.AL_PLAYING - - //println("buffer=$buffer, processed=$processed, queued=$queued, state=$state, playing=$playing, sampleOffset=$sampleOffset") - //println("Samples.add") - - if (processed <= 0 && total >= 6) { - delay(10.milliseconds) - continue - } - - if (total < 6) { - tempBuffers.value = AL.alGenBuffer() - AL.checkAlErrors("alGenBuffers") - //println("alGenBuffers: ${tempBuffers[0]}") - } else { - AL.alSourceUnqueueBuffers(source, 1, tempBuffers.ptr) - AL.checkAlErrors("alSourceUnqueueBuffers") - //println("alSourceUnqueueBuffers: ${tempBuffers[0]}") - } - //println("samples: $samples - $offset, $size") - //al.alBufferData(tempBuffers[0], samples.copyOfRange(offset, offset + size), frequency, panning, volume) - AL.alBufferData(tempBuffers.value, samples.copyOfRange(offset, offset + size), frequency, panning) - AL.alSourceQueueBuffers(source, 1, tempBuffers.ptr) - AL.checkAlErrors("alSourceQueueBuffers") - - //val gain = al.alGetSourcef(source, AL.AL_GAIN) - //val pitch = al.alGetSourcef(source, AL.AL_PITCH) - //println("gain=$gain, pitch=$pitch") - if (!playing) { - AL.alSourcePlay(source) - } - break - } - } - } finally { - availableSamples -= samples.totalSamples - } - } - - fun ensureSource() { - if (source.toInt() != 0) return - provider.makeCurrent() - - source = AL.alGenSource() - //for (n in buffers.indices) buffers[n] = alGenBuffer() .toInt() - } - - override fun start() { - ensureSource() - AL.alSourcePlay(source) - AL.checkAlErrors("alSourcePlay") - //checkAlErrors() - } - - override fun stop() { - provider.makeCurrent() - - AL.alSourceStop(source) - if (source.toInt() != 0) { - AL.alDeleteSource(source) - source = 0.convert() - } - //for (n in buffers.indices) { - // if (buffers[n] != 0) { - // alDeleteBuffer(buffers[n]) - // buffers[n] = 0 - // } - //} - } -} - -// https://ffainelli.github.io/openal-example/ -class OpenALNativeSoundNoStream( - val provider: OpenALNativeSoundProvider, - coroutineContext: CoroutineContext, - val data: AudioData?, - val sourceProvider: SourceProvider = SourceProvider(0.convert()), - override val name: String = "Unknown" -) : Sound(coroutineContext), SoundProps by JnaSoundPropsProvider(sourceProvider) { - override suspend fun decode(maxSamples: Int): AudioData = data ?: AudioData.DUMMY - - var source: ALuint - get() = sourceProvider.source - set(value) { sourceProvider.source = value } - - override val length: TimeSpan get() = data?.totalTime ?: 0.seconds - - override fun play(coroutineContext: CoroutineContext, params: PlaybackParameters): SoundChannel { - val data = data ?: return DummySoundChannel(this) - provider.makeCurrent() - val buffer = AL.alGenBuffer() - AL.alBufferData(buffer, data, panning, volume) - - source = AL.alGenSource() - AL.alSourcei(source, AL.AL_BUFFER, buffer.convert()) - AL.checkAlErrors("alSourcei") - - var stopped = false - - val channel = object : SoundChannel(this), SoundProps by JnaSoundPropsProvider(sourceProvider) { - val totalSamples get() = data.totalSamples - var currentSampleOffset: Int - get() = AL.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) - set(value) { - AL.alSourcei(source, AL.AL_SAMPLE_OFFSET, value) - } - - override var current: TimeSpan - get() = data.timeAtSample(currentSampleOffset) - set(value) { AL.alSourcef(source, AL.AL_SEC_OFFSET, value.seconds.toFloat()) } - override val total: TimeSpan get() = data.totalTime - - override val state: SoundChannelState get() { - val result = AL.alGetSourceState(source) - AL.checkAlErrors("alGetSourceState") - return when (result) { - AL.AL_INITIAL -> SoundChannelState.INITIAL - AL.AL_PLAYING -> SoundChannelState.PLAYING - AL.AL_PAUSED -> SoundChannelState.PAUSED - AL.AL_STOPPED -> SoundChannelState.STOPPED - else -> SoundChannelState.STOPPED - } - } - - override fun pause() { - AL.alSourcePause(source) - } - - override fun resume() { - AL.alSourcePlay(source) - } - - override fun stop() { - if (!stopped) { - stopped = true - AL.alDeleteSource(source) - AL.alDeleteBuffer(buffer) - } - } - }.also { - it.copySoundPropsFrom(params) - } - launchImmediately(coroutineContext[ContinuationInterceptor] ?: coroutineContext) { - var times = params.times - var startTime = params.startTime - try { - while (times.hasMore) { - times = times.oneLess - channel.reset() - AL.alSourcef(source, AL.AL_SEC_OFFSET, startTime.seconds.toFloat()) - AL.alSourcePlay(source) - //checkAlErrors("alSourcePlay") - startTime = 0.seconds - while (channel.playing) delay(1L) - } - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - e.printStackTrace() - } finally { - channel.stop() - } - } - return channel - } -} - -data class SourceProvider(var source: ALuint) - -class JnaSoundPropsProvider(val sourceProvider: SourceProvider) : SoundProps { - val source get() = sourceProvider.source - - private val temp1 = FloatArray(3) - private val temp2 = FloatArray(3) - private val temp3 = FloatArray(3) - - override var pitch: Double - get() = AL.alGetSourcef(source, AL.AL_PITCH).toDouble() - set(value) = AL.alSourcef(source, AL.AL_PITCH, value.toFloat()) - override var volume: Double - get() = AL.alGetSourcef(source, AL.AL_GAIN).toDouble() - set(value) = AL.alSourcef(source, AL.AL_GAIN, value.toFloat()) - override var panning: Double - get() = memScoped { - val temp1 = alloc() - val temp2 = alloc() - val temp3 = alloc() - AL.alGetSource3f(source, AL.AL_POSITION, temp1.ptr, temp2.ptr, temp3.ptr) - temp1.value.toDouble() - } - set(value) { - val pan = value.toFloat() - AL.alSourcef(source, AL.AL_ROLLOFF_FACTOR, 0.0f); - AL.alSourcei(source, AL.AL_SOURCE_RELATIVE, 1); - AL.alSource3f(source, AL.AL_POSITION, pan, 0f, -sqrt(1.0f - pan * pan)); - //println("SET PANNING: source=$source, pan=$pan") - } -} diff --git a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/ALSANativeSoundProvider.kt b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/backends/ALSA.kt similarity index 99% rename from korau/src/linuxMain/kotlin/com/soywiz/korau/sound/ALSANativeSoundProvider.kt rename to korau/src/linuxMain/kotlin/com/soywiz/korau/sound/backends/ALSA.kt index 552694ed79..b36c8c2b09 100644 --- a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/ALSANativeSoundProvider.kt +++ b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/backends/ALSA.kt @@ -1,13 +1,13 @@ -package com.soywiz.korau.sound +package com.soywiz.korau.sound.backends import com.soywiz.kds.lock.* import com.soywiz.kds.thread.* import com.soywiz.klock.* import com.soywiz.kmem.* import com.soywiz.kmem.dyn.* +import com.soywiz.korau.sound.* import com.soywiz.korio.async.* import kotlinx.cinterop.* -import kotlinx.coroutines.* import kotlin.coroutines.* val alsaNativeSoundProvider: ALSANativeSoundProvider? by lazy { diff --git a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/AL.kt b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/backends/OpenAL.kt similarity index 57% rename from korau/src/linuxMain/kotlin/com/soywiz/korau/sound/AL.kt rename to korau/src/linuxMain/kotlin/com/soywiz/korau/sound/backends/OpenAL.kt index b0868da8e2..d8d981cf28 100644 --- a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/AL.kt +++ b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/backends/OpenAL.kt @@ -1,11 +1,305 @@ +package com.soywiz.korau.sound.backends -package com.soywiz.korau.sound - -import com.soywiz.kmem.clamp01 -import com.soywiz.kmem.dyn.DynamicLibrary -import com.soywiz.kmem.dyn.func -import com.soywiz.kmem.startAddressOf +import com.soywiz.klock.TimeSpan +import com.soywiz.klock.milliseconds +import com.soywiz.klock.seconds +import com.soywiz.kmem.* +import com.soywiz.kmem.dyn.* +import com.soywiz.korau.format.AudioDecodingProps +import com.soywiz.korau.sound.* +import com.soywiz.korio.async.delay +import com.soywiz.korio.async.launchImmediately import kotlinx.cinterop.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext +import kotlin.math.sqrt + +val openalNativeSoundProvider: OpenALNativeSoundProvider? by lazy { + try { + OpenALNativeSoundProvider() + } catch (e: Throwable) { + e.printStackTrace() + null + } +} + +class OpenALNativeSoundProvider : NativeSoundProvider() { + val device = AL.alcOpenDevice(null) + //val device: CPointer? = null + val context = device?.let { AL.alcCreateContext(it, null).also { + AL.alcMakeContextCurrent(it) + memScoped { + AL.alListener3f(AL.AL_POSITION, 0f, 0f, 1.0f) + AL.alListener3f(AL.AL_VELOCITY, 0f, 0f, 0f) + val listenerOri = floatArrayOf(0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f) + listenerOri.usePinned { + AL.alListenerfv(AL.AL_ORIENTATION, it.addressOf(0)) + } + } + } } + + internal fun makeCurrent() { + AL.alcMakeContextCurrent(context) + } + + override suspend fun createSound(data: ByteArray, streaming: Boolean, props: AudioDecodingProps, name: String): Sound { + return if (streaming) { + super.createSound(data, streaming, props, name) + } else { + OpenALNativeSoundNoStream(this, coroutineContext, audioFormats.decode(data, props), name = name) + } + } + + override fun createPlatformAudioOutput(coroutineContext: CoroutineContext, freq: Int): PlatformAudioOutput { + return OpenALPlatformAudioOutput(this, coroutineContext, freq) + } +} + +class OpenALPlatformAudioOutput( + val provider: OpenALNativeSoundProvider, + coroutineContext: CoroutineContext, + freq: Int, + val sourceProvider: SourceProvider = SourceProvider(0.convert()) +) : PlatformAudioOutput(coroutineContext, freq) { + val sourceProv = JnaSoundPropsProvider(sourceProvider) + override var availableSamples: Int = 0 + + override var pitch: Double by sourceProv::pitch + override var volume: Double by sourceProv::volume + override var panning: Double by sourceProv::panning + + var source: ALuint + get() = sourceProvider.source + set(value) { sourceProvider.source = value } + + //val source + + //alSourceQueueBuffers + + //val buffersPool = Pool(6) { all.alGenBuffer() } + //val buffers = IntArray(32) + //val nbuffers = 6 + //val buffers = IntArray(nbuffers) + + init { + start() + } + + override suspend fun add(samples: AudioSamples, offset: Int, size: Int) { + availableSamples += samples.totalSamples + try { + memScoped { + provider.makeCurrent() + val tempBuffers = alloc() + ensureSource() + while (true) { + //val buffer = al.alGetSourcei(source, AL.AL_BUFFER) + //val sampleOffset = al.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) + val processed = AL.alGetSourcei(source, AL.AL_BUFFERS_PROCESSED) + val queued = AL.alGetSourcei(source, AL.AL_BUFFERS_QUEUED) + val total = processed + queued + val state = AL.alGetSourceState(source) + val playing = state == AL.AL_PLAYING + + //println("buffer=$buffer, processed=$processed, queued=$queued, state=$state, playing=$playing, sampleOffset=$sampleOffset") + //println("Samples.add") + + if (processed <= 0 && total >= 6) { + delay(10.milliseconds) + continue + } + + if (total < 6) { + tempBuffers.value = AL.alGenBuffer() + AL.checkAlErrors("alGenBuffers") + //println("alGenBuffers: ${tempBuffers[0]}") + } else { + AL.alSourceUnqueueBuffers(source, 1, tempBuffers.ptr) + AL.checkAlErrors("alSourceUnqueueBuffers") + //println("alSourceUnqueueBuffers: ${tempBuffers[0]}") + } + //println("samples: $samples - $offset, $size") + //al.alBufferData(tempBuffers[0], samples.copyOfRange(offset, offset + size), frequency, panning, volume) + AL.alBufferData(tempBuffers.value, samples.copyOfRange(offset, offset + size), frequency, panning) + AL.alSourceQueueBuffers(source, 1, tempBuffers.ptr) + AL.checkAlErrors("alSourceQueueBuffers") + + //val gain = al.alGetSourcef(source, AL.AL_GAIN) + //val pitch = al.alGetSourcef(source, AL.AL_PITCH) + //println("gain=$gain, pitch=$pitch") + if (!playing) { + AL.alSourcePlay(source) + } + break + } + } + } finally { + availableSamples -= samples.totalSamples + } + } + + fun ensureSource() { + if (source.toInt() != 0) return + provider.makeCurrent() + + source = AL.alGenSource() + //for (n in buffers.indices) buffers[n] = alGenBuffer() .toInt() + } + + override fun start() { + ensureSource() + AL.alSourcePlay(source) + AL.checkAlErrors("alSourcePlay") + //checkAlErrors() + } + + override fun stop() { + provider.makeCurrent() + + AL.alSourceStop(source) + if (source.toInt() != 0) { + AL.alDeleteSource(source) + source = 0.convert() + } + //for (n in buffers.indices) { + // if (buffers[n] != 0) { + // alDeleteBuffer(buffers[n]) + // buffers[n] = 0 + // } + //} + } +} + +// https://ffainelli.github.io/openal-example/ +class OpenALNativeSoundNoStream( + val provider: OpenALNativeSoundProvider, + coroutineContext: CoroutineContext, + val data: AudioData?, + val sourceProvider: SourceProvider = SourceProvider(0.convert()), + override val name: String = "Unknown" +) : Sound(coroutineContext), SoundProps by JnaSoundPropsProvider(sourceProvider) { + override suspend fun decode(maxSamples: Int): AudioData = data ?: AudioData.DUMMY + + var source: ALuint + get() = sourceProvider.source + set(value) { sourceProvider.source = value } + + override val length: TimeSpan get() = data?.totalTime ?: 0.seconds + + override fun play(coroutineContext: CoroutineContext, params: PlaybackParameters): SoundChannel { + val data = data ?: return DummySoundChannel(this) + provider.makeCurrent() + val buffer = AL.alGenBuffer() + AL.alBufferData(buffer, data, panning, volume) + + source = AL.alGenSource() + AL.alSourcei(source, AL.AL_BUFFER, buffer.convert()) + AL.checkAlErrors("alSourcei") + + var stopped = false + + val channel = object : SoundChannel(this), SoundProps by JnaSoundPropsProvider(sourceProvider) { + val totalSamples get() = data.totalSamples + var currentSampleOffset: Int + get() = AL.alGetSourcei(source, AL.AL_SAMPLE_OFFSET) + set(value) { + AL.alSourcei(source, AL.AL_SAMPLE_OFFSET, value) + } + + override var current: TimeSpan + get() = data.timeAtSample(currentSampleOffset) + set(value) { AL.alSourcef(source, AL.AL_SEC_OFFSET, value.seconds.toFloat()) } + override val total: TimeSpan get() = data.totalTime + + override val state: SoundChannelState get() { + val result = AL.alGetSourceState(source) + AL.checkAlErrors("alGetSourceState") + return when (result) { + AL.AL_INITIAL -> SoundChannelState.INITIAL + AL.AL_PLAYING -> SoundChannelState.PLAYING + AL.AL_PAUSED -> SoundChannelState.PAUSED + AL.AL_STOPPED -> SoundChannelState.STOPPED + else -> SoundChannelState.STOPPED + } + } + + override fun pause() { + AL.alSourcePause(source) + } + + override fun resume() { + AL.alSourcePlay(source) + } + + override fun stop() { + if (!stopped) { + stopped = true + AL.alDeleteSource(source) + AL.alDeleteBuffer(buffer) + } + } + }.also { + it.copySoundPropsFrom(params) + } + launchImmediately(coroutineContext[ContinuationInterceptor] ?: coroutineContext) { + var times = params.times + var startTime = params.startTime + try { + while (times.hasMore) { + times = times.oneLess + channel.reset() + AL.alSourcef(source, AL.AL_SEC_OFFSET, startTime.seconds.toFloat()) + AL.alSourcePlay(source) + //checkAlErrors("alSourcePlay") + startTime = 0.seconds + while (channel.playing) delay(1L) + } + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + e.printStackTrace() + } finally { + channel.stop() + } + } + return channel + } +} + +data class SourceProvider(var source: ALuint) + +class JnaSoundPropsProvider(val sourceProvider: SourceProvider) : SoundProps { + val source get() = sourceProvider.source + + private val temp1 = FloatArray(3) + private val temp2 = FloatArray(3) + private val temp3 = FloatArray(3) + + override var pitch: Double + get() = AL.alGetSourcef(source, AL.AL_PITCH).toDouble() + set(value) = AL.alSourcef(source, AL.AL_PITCH, value.toFloat()) + override var volume: Double + get() = AL.alGetSourcef(source, AL.AL_GAIN).toDouble() + set(value) = AL.alSourcef(source, AL.AL_GAIN, value.toFloat()) + override var panning: Double + get() = memScoped { + val temp1 = alloc() + val temp2 = alloc() + val temp3 = alloc() + AL.alGetSource3f(source, AL.AL_POSITION, temp1.ptr, temp2.ptr, temp3.ptr) + temp1.value.toDouble() + } + set(value) { + val pan = value.toFloat() + AL.alSourcef(source, AL.AL_ROLLOFF_FACTOR, 0.0f); + AL.alSourcei(source, AL.AL_SOURCE_RELATIVE, 1); + AL.alSource3f(source, AL.AL_POSITION, pan, 0f, -sqrt(1.0f - pan * pan)); + //println("SET PANNING: source=$source, pan=$pan") + } +} //typealias ALuintVar = UIntVar typealias ALuint = Int From bbd74c5b35b47792289fbdc0c36b1dab707f024a Mon Sep 17 00:00:00 2001 From: soywiz Date: Mon, 6 Feb 2023 10:09:26 +0100 Subject: [PATCH 4/4] Fix K/N linux nativeSoundProvider --- .../soywiz/korau/sound/NativeNativeSoundProvider.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/NativeNativeSoundProvider.kt b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/NativeNativeSoundProvider.kt index bd476632bb..1a800932be 100644 --- a/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/NativeNativeSoundProvider.kt +++ b/korau/src/linuxMain/kotlin/com/soywiz/korau/sound/NativeNativeSoundProvider.kt @@ -3,8 +3,13 @@ package com.soywiz.korau.sound import com.soywiz.korau.sound.backends.* actual val nativeSoundProvider: NativeSoundProvider by lazy { - (null as? NativeSoundProvider?) - ?: alsaNativeSoundProvider - ?: openalNativeSoundProvider - ?: DummyNativeSoundProvider + try { + (null as? NativeSoundProvider?) + ?: alsaNativeSoundProvider + ?: openalNativeSoundProvider + ?: DummyNativeSoundProvider + } catch (e: Throwable) { + e.printStackTrace() + DummyNativeSoundProvider + } }