diff options
Diffstat (limited to 'desktop/src/main/kotlin')
| -rw-r--r-- | desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt | 43 | ||||
| -rw-r--r-- | desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt | 22 |
2 files changed, 64 insertions, 1 deletions
diff --git a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt index c2f65ca..085ad9a 100644 --- a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt +++ b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Main.kt @@ -9,7 +9,13 @@ import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import coffee.liz.rookery.audio.Chime import coffee.liz.rookery.audio.ChimePlayer +import coffee.liz.rookery.bt.BlueZTransport +import coffee.liz.rookery.bt.CubeBackend +import coffee.liz.rookery.bt.GanCubeConnector +import coffee.liz.rookery.bt.JvmAes128 import coffee.liz.rookery.images.BraveImages +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filter import coffee.liz.rookery.images.ImageHit import coffee.liz.rookery.model.Event import coffee.liz.rookery.scramble.TnoodleScrambler @@ -50,6 +56,9 @@ class LocalConfig { runCatching { java.nio.file.Files.setPosixFilePermissions(file, java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")) } } + fun get(key: String): String? = props.getProperty(key) + fun set(key: String, value: String?) { if (value == null) props.remove(key) else props.setProperty(key, value); save() } + /** Brave Search API key: env ROOKERY_BRAVE_KEY, else `braveApiKey` in this (never synced) file. */ val braveApiKey: String? get() = System.getenv("ROOKERY_BRAVE_KEY")?.takeIf { it.isNotBlank() } ?: props.getProperty("braveApiKey")?.takeIf { it.isNotBlank() } @@ -142,6 +151,40 @@ class DesktopHost(private val config: LocalConfig) : Host { data.bytes to "webp" }.getOrNull() + /** Bluetooth goes through BlueZ, so Linux only for now. */ + override val cubes: CubeBackend? = if (!System.getProperty("os.name").lowercase().contains("linux")) null else object : CubeBackend { + private val transport by lazy { BlueZTransport() } + override fun scan() = transport.scan().filter { GanCubeConnector.isGanCube(it) } + override suspend fun connect(scope: CoroutineScope, address: String, mac: String) = GanCubeConnector(transport, JvmAes128).connect(scope, address, mac) + } + + override val audioSampleRate: Int get() = 44_100 + private val audioFormat = javax.sound.sampled.AudioFormat(44_100f, 16, 1, true, false) + private val lineInfo = javax.sound.sampled.DataLine.Info(javax.sound.sampled.TargetDataLine::class.java, audioFormat) + + override fun audioInputs(): List<String> = javax.sound.sampled.AudioSystem.getMixerInfo() + .filter { runCatching { javax.sound.sampled.AudioSystem.getMixer(it).isLineSupported(lineInfo) }.getOrDefault(false) }.map { it.name }.distinct() + + override fun openAudio(input: String?, onSamples: (FloatArray, Int) -> Unit): AutoCloseable { + val mixer = javax.sound.sampled.AudioSystem.getMixerInfo().firstOrNull { it.name == input }?.let { javax.sound.sampled.AudioSystem.getMixer(it) } + val line = try { + ((mixer?.getLine(lineInfo) ?: javax.sound.sampled.AudioSystem.getLine(lineInfo)) as javax.sound.sampled.TargetDataLine).apply { open(audioFormat, 8192); start() } + } catch (e: Exception) { error("couldn't open ${input ?: "the default input"} (${e.message})") } + val thread = Thread { + val bytes = ByteArray(2048); val samples = FloatArray(1024) + while (line.isOpen) { + val n = line.read(bytes, 0, bytes.size) + if (n <= 0) break + for (i in 0 until n / 2) samples[i] = (((bytes[2 * i + 1].toInt() shl 8) or (bytes[2 * i].toInt() and 0xFF)).toShort()) / 32768f + onSamples(samples, n / 2) + } + }.apply { isDaemon = true; name = "stackmat-audio"; start() } + return AutoCloseable { line.stop(); line.close(); thread.interrupt() } + } + + override fun localPref(key: String): String? = config.get("local.$key") + override fun setLocalPref(key: String, value: String?) = config.set("local.$key", value) + var onFullscreen: (Boolean) -> Unit = {} override fun setFullscreen(on: Boolean) = onFullscreen(on) diff --git a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt index d7621eb..6a00c24 100644 --- a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt +++ b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/Shots.kt @@ -57,6 +57,24 @@ fun shots(args: List<String>) { override suspend fun fetchBytes(url: String) = ByteArray(0) override fun shrinkImage(bytes: ByteArray): Pair<ByteArray, String>? = null override fun setFullscreen(on: Boolean) {} + override val cubes = object : coffee.liz.rookery.bt.CubeBackend { + override fun scan() = kotlinx.coroutines.flow.flowOf(coffee.liz.rookery.bt.BleAdvertisement("AA:BB:CC:00:11:22", "GANicE3_1122", -58)) + override suspend fun connect(scope: CoroutineScope, address: String, mac: String) = object : coffee.liz.rookery.bt.SmartCube { + override val name = "GANicE3_1122"; override val address = address + override val events = kotlinx.coroutines.flow.flow { + emit(coffee.liz.rookery.bt.SmartCubeEvent.Battery(87)) + emit(coffee.liz.rookery.bt.SmartCubeEvent.Facelets("UUUUUUFFFRRRRRRRRRFFDFFDFFDDDDDDDBBBLLLLLLLLLUBBUBBUBB")) + emit(coffee.liz.rookery.bt.SmartCubeEvent.Move("F", null, 0)); kotlinx.coroutines.awaitCancellation() + } + override suspend fun requestFacelets() {}; override suspend fun requestBattery() {}; override suspend fun requestHardware() {} + override suspend fun resetToSolved() {}; override suspend fun disconnect() {} + } + } + override fun audioInputs() = listOf("USB Audio Device") + override fun openAudio(input: String?, onSamples: (FloatArray, Int) -> Unit) = AutoCloseable { } + override val audioSampleRate = 44_100 + override fun localPref(key: String): String? = null + override fun setLocalPref(key: String, value: String?) {} } val model = AppModel(repo, TnoodleScrambler(), host, CoroutineScope(SupervisorJob() + Dispatchers.Default)) model.selectSession("s1") @@ -78,7 +96,9 @@ fun shots(args: List<String>) { shoot("party", settle = 1800) { model.testParty() } model.party = null for (mode in BldMode.entries) shoot("bld-${mode.name.lowercase()}", settle = 4000) { model.tab = Tab.BLD; model.bldMode = mode } - shoot("settings") { model.tab = Tab.SETTINGS } + shoot("settings") { model.tab = Tab.SETTINGS; model.cube.scan(1) } + shoot("settings-cube") { model.cube.connect("AA:BB:CC:00:11:22", "GANicE3_1122"); model.stackmat.change(true) } + model.stackmat.change(false) shoot("import") { model.tab = Tab.IMPORT } model.setTheme(ThemeMode.LIGHT) shoot("timer-paper") { model.tab = Tab.TIMER } |
