From 144f29a69ed4e3975b06b316480e6a12faef0746 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 22 Jan 2024 00:35:00 +0100 Subject: [PATCH 01/48] PoC of generating sound from Tiny. --- .../github/minigdx/tiny/engine/GameEngine.kt | 17 ++++- .../minigdx/tiny/engine/GameResourceAccess.kt | 6 ++ .../com/github/minigdx/tiny/lua/NotesLib.kt | 26 ++++++++ .../com/github/minigdx/tiny/lua/SfxLib.kt | 44 +++++++++++++ .../minigdx/tiny/resources/GameScript.kt | 2 + .../github/minigdx/tiny/sound/SoundManager.kt | 8 +++ .../minigdx/tiny/sound/WaveGenerator.kt | 58 +++++++++++++++++ .../tiny/platform/test/HeadlessPlatform.kt | 6 ++ .../platform/webgl/PicoAudioSoundMananger.kt | 6 ++ .../platform/glfw/JavaMidiSoundManager.kt | 63 ++++++++++++++++++- 10 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt create mode 100644 tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt index 79ef1a28..a31759e6 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt @@ -24,6 +24,8 @@ import com.github.minigdx.tiny.resources.ResourceType.GAME_SOUND import com.github.minigdx.tiny.resources.ResourceType.GAME_SPRITESHEET import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.resources.SpriteSheet +import com.github.minigdx.tiny.sound.SoundManager +import com.github.minigdx.tiny.sound.WaveGenerator import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -91,6 +93,9 @@ class GameEngine( private var debugEnabled: Boolean = true private val debugActions = mutableListOf() + private val notes = mutableListOf() + private var longuestDuration: Seconds = 0f + private lateinit var scripts: Array private lateinit var spriteSheets: Array private lateinit var levels: Array @@ -115,6 +120,7 @@ class GameEngine( private lateinit var renderContext: RenderContext private lateinit var inputHandler: InputHandler private lateinit var inputManager: InputManager + private lateinit var soundManager: SoundManager private lateinit var resourceFactory: ResourceFactory @@ -123,7 +129,7 @@ class GameEngine( inputHandler = platform.initInputHandler() inputManager = platform.initInputManager() - platform.initSoundManager(inputHandler) + soundManager = platform.initSoundManager(inputHandler) resourceFactory = ResourceFactory(vfs, platform, logger, gameOptions.colors()) @@ -316,6 +322,10 @@ class GameEngine( } } + soundManager.playNotes(notes, longuestDuration) + notes.clear() + longuestDuration = 0f + // Fixed step simulation accumulator += delta if (accumulator >= REFRESH_LIMIT) { @@ -465,6 +475,11 @@ class GameEngine( return sounds[protected] } + override fun note(wave: WaveGenerator) { + longuestDuration = max(longuestDuration, wave.duration) + notes.add(wave) + } + override fun script(name: String): GameScript? { return scripts .drop(1) // drop the _boot.lua diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt index e59604ff..1464fe67 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt @@ -5,6 +5,7 @@ import com.github.minigdx.tiny.resources.GameLevel import com.github.minigdx.tiny.resources.GameScript import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.resources.SpriteSheet +import com.github.minigdx.tiny.sound.WaveGenerator sealed interface DebugAction data class DebugMessage(val mesage: String, val color: String) : DebugAction @@ -41,8 +42,13 @@ interface GameResourceAccess { */ fun level(index: Int): GameLevel? + /** + * Access sound by its index + */ fun sound(index: Int): Sound? + fun note(wave: WaveGenerator) + /** * Find a script by its name. */ diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt new file mode 100644 index 00000000..bf5361c9 --- /dev/null +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt @@ -0,0 +1,26 @@ +package com.github.minigdx.tiny.lua + +import org.luaj.vm2.LuaTable +import org.luaj.vm2.LuaValue +import org.luaj.vm2.lib.TwoArgFunction + +enum class Note(val frequency: Float) { + C1(65.4064f), + D1(73.4162f), + E1(82.4069f), +} + +class NotesLib : TwoArgFunction() { + + override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + val keys = LuaTable() + + Note.values().forEach { note -> + keys[note.name.lowercase()] = valueOf(note.ordinal) + } + + arg2["notes"] = keys + arg2["package"]["loaded"]["notes"] = keys + return keys + } +} diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index 54c2f250..85ea3c2c 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -4,8 +4,14 @@ import com.github.mingdx.tiny.doc.TinyArg import com.github.mingdx.tiny.doc.TinyCall import com.github.mingdx.tiny.doc.TinyFunction import com.github.mingdx.tiny.doc.TinyLib +import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.engine.GameResourceAccess import com.github.minigdx.tiny.resources.Sound +import com.github.minigdx.tiny.sound.SawTooth +import com.github.minigdx.tiny.sound.SineWave +import com.github.minigdx.tiny.sound.SquareWave +import com.github.minigdx.tiny.sound.TriangleWave +import com.github.minigdx.tiny.sound.WaveGenerator import org.luaj.vm2.LuaTable import org.luaj.vm2.LuaValue import org.luaj.vm2.lib.OneArgFunction @@ -22,11 +28,49 @@ class SfxLib( ctrl.set("play", play()) ctrl.set("loop", loop()) ctrl.set("stop", stop()) + ctrl.set("sine", sine()) + ctrl.set("square", square()) + ctrl.set("triangle", triangle()) + ctrl.set("saw", sawtooth()) arg2.set("sfx", ctrl) arg2.get("package").get("loaded").set("sfx", ctrl) return ctrl } + abstract inner class WaveFunction : TwoArgFunction() { + private val notes = Note.values() + + override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + val note = if (arg1.isint()) { + notes[arg1.checkint()] + } else { + return NIL + } + + val duration = arg2.optdouble(0.1) + resourceAccess.note(wave(note, duration.toFloat())) + return NIL + } + + abstract fun wave(note: Note, duration: Seconds): WaveGenerator + } + + inner class sine : WaveFunction() { + override fun wave(note: Note, duration: Seconds) = SineWave(note, duration) + } + + inner class sawtooth : WaveFunction() { + override fun wave(note: Note, duration: Seconds) = SawTooth(note, duration) + } + + inner class square : WaveFunction() { + override fun wave(note: Note, duration: Seconds) = SquareWave(note, duration) + } + + inner class triangle : WaveFunction() { + override fun wave(note: Note, duration: Seconds) = TriangleWave(note, duration) + } + @TinyFunction( "Play a sound by it's index. " + "The index of a sound is given by it's position in the sounds field from the `_tiny.json` file." + diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt index 65f4a70c..138da17f 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt @@ -11,6 +11,7 @@ import com.github.minigdx.tiny.lua.JuiceLib import com.github.minigdx.tiny.lua.KeysLib import com.github.minigdx.tiny.lua.MapLib import com.github.minigdx.tiny.lua.MathLib +import com.github.minigdx.tiny.lua.NotesLib import com.github.minigdx.tiny.lua.SfxLib import com.github.minigdx.tiny.lua.ShapeLib import com.github.minigdx.tiny.lua.SprLib @@ -88,6 +89,7 @@ class GameScript( load(tinyLib) load(sprLib) load(JuiceLib()) + load(NotesLib()) LoadState.install(this) LuaC.install(this) } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index baeb76f8..9b7baeb8 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -1,5 +1,6 @@ package com.github.minigdx.tiny.sound +import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler interface MidiSound { @@ -15,4 +16,11 @@ interface SoundManager { fun initSoundManager(inputHandler: InputHandler) suspend fun createSound(data: ByteArray): MidiSound + + fun playNotes(notes: List, longuestDuration: Seconds) + + companion object { + + const val SAMPLE_RATE = 44100 + } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt new file mode 100644 index 00000000..eec5fe93 --- /dev/null +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -0,0 +1,58 @@ +package com.github.minigdx.tiny.sound + +import com.github.minigdx.tiny.Seconds +import com.github.minigdx.tiny.lua.Note +import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE +import kotlin.math.sin + +sealed class WaveGenerator(val note: Note, val duration: Seconds) { + + val period = SAMPLE_RATE.toFloat() / note.frequency + + /** + * Return a value between -1.0 and 1.0 + */ + abstract fun generate(sample: Int): Float + + internal fun angle(sample: Int): Float { + return (TWO_PI * sample) / period + } + + companion object { + internal const val PI = kotlin.math.PI.toFloat() + internal const val TWO_PI = 2.0f * PI + } +} + +class SawTooth(note: Note, duration: Seconds) : WaveGenerator(note, duration) { + override fun generate(sample: Int): Float { + return (2 * (angle(sample) / TWO_PI)) + } +} +class SineWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { + override fun generate(sample: Int): Float { + return sin(angle(sample)) + } +} + +class SquareWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { + override fun generate(sample: Int): Float { + val value = sin(angle(sample)) + return if (value > 0f) { + 1f + } else { + -1f + } + } +} + +class TriangleWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { + override fun generate(sample: Int): Float { + val angle = angle(sample) + return if (angle < PI) { + 2 * angle / PI + } else { + 2 * (PI - angle) / PI + 1 + } + } +} diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt index 6755254c..2a7d846d 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt @@ -1,11 +1,13 @@ package com.github.minigdx.tiny.platform.test +import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.engine.GameLoop import com.github.minigdx.tiny.engine.GameOptions import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.graphic.FrameBuffer import com.github.minigdx.tiny.input.InputHandler import com.github.minigdx.tiny.input.InputManager +import com.github.minigdx.tiny.lua.Note import com.github.minigdx.tiny.platform.ImageData import com.github.minigdx.tiny.platform.Platform import com.github.minigdx.tiny.platform.RenderContext @@ -85,6 +87,10 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map override fun stop() = Unit } } + + override fun playNotes(notes: List>, longuestDuration: Seconds) { + TODO("Not yet implemented") + } } } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index 994178b5..915ed1cc 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -1,6 +1,8 @@ package com.github.minigdx.tiny.platform.webgl +import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler +import com.github.minigdx.tiny.lua.Note import com.github.minigdx.tiny.sound.MidiSound import com.github.minigdx.tiny.sound.SoundManager @@ -31,4 +33,8 @@ class PicoAudioSoundMananger : SoundManager { val smf = audio.parseSMF(data) return PicoAudioSound(audio, smf) } + + override fun playNotes(notes: List>) { + TODO("Not yet implemented") + } } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index 9f3c183c..cfb86df8 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -1,12 +1,20 @@ package com.github.minigdx.tiny.platform.glfw +import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler import com.github.minigdx.tiny.sound.MidiSound import com.github.minigdx.tiny.sound.SoundManager +import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE +import com.github.minigdx.tiny.sound.WaveGenerator import java.io.ByteArrayInputStream import javax.sound.midi.MidiSystem import javax.sound.midi.Sequencer import javax.sound.midi.Sequencer.LOOP_CONTINUOUSLY +import javax.sound.sampled.AudioFormat +import javax.sound.sampled.AudioSystem +import javax.sound.sampled.SourceDataLine +import kotlin.experimental.and +import kotlin.experimental.or class JavaMidiSound(private val data: ByteArray) : MidiSound { @@ -47,9 +55,62 @@ class JavaMidiSound(private val data: ByteArray) : MidiSound { } class JavaMidiSoundManager : SoundManager { - override fun initSoundManager(inputHandler: InputHandler) = Unit + + private lateinit var notesLine: SourceDataLine + + private var buffer = ByteArray(0) + + override fun initSoundManager(inputHandler: InputHandler) { + notesLine = AudioSystem.getSourceDataLine( + AudioFormat( + AudioFormat.Encoding.PCM_SIGNED, + SoundManager.SAMPLE_RATE.toFloat(), + 16, + 1, // TODO: set 2 to get Stereo + 2, + SoundManager.SAMPLE_RATE.toFloat(), + false, + ), + ) + + notesLine.open() + notesLine.start() + } override suspend fun createSound(data: ByteArray): MidiSound { return JavaMidiSound(data) } + + override fun playNotes(notes: List, longuestDuration: Seconds) { + if (notes.isEmpty()) return + + val sampleRate = SAMPLE_RATE + + // TODO: boucle sur les notes. + // regarder dans le buffer et faire un + entre note et buffer + // write le tout et let's go + // mettre a jour la List : LiveNote(note, duration, type) + buffer = ByteArray((longuestDuration * sampleRate).toInt() * 2) + notes.forEach { wave -> + val numSamples: Int = (sampleRate * wave.duration).toInt() + + for (i in 0 until numSamples) { + val byteA = buffer[2 * i] and 0xFF.toByte() + val byteB = (buffer[2 * i + 1]).toInt().shl(8).toByte() + + // TODO: le mix de note n'est pas bon. + // TODO: la fin du son n'est pas ouf. + val actualBuffer = byteA.or(byteB).toShort() + + val sample = wave.generate(i) + val sampleValue = (Short.MAX_VALUE * sample).toInt().toShort() + actualBuffer + + buffer[2 * i] = (sampleValue and 0xFF).toByte() + buffer[2 * i + 1] = ((sampleValue shr 8) and 0xFF).toByte() + } + } + + // generate the byte array + notesLine.write(buffer, 0, buffer.size) + } } From 66ada823e3e34f74ef35748ffe096823852e6992 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 22 Jan 2024 01:00:23 +0100 Subject: [PATCH 02/48] Add notes and documentation --- .../com/github/minigdx/tiny/lua/NotesLib.kt | 58 +++++++++++++++++-- .../com/github/minigdx/tiny/lua/SfxLib.kt | 9 ++- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt index bf5361c9..a91989c6 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt @@ -5,9 +5,59 @@ import org.luaj.vm2.LuaValue import org.luaj.vm2.lib.TwoArgFunction enum class Note(val frequency: Float) { - C1(65.4064f), - D1(73.4162f), - E1(82.4069f), + C0(16.35f), + Cs0(17.32f), + Db0(17.32f), + D0(18.35f), + Ds0(19.45f), + Eb0(19.45f), + E0(20.60f), + F0(21.83f), + Fs0(23.12f), + Gb0(23.12f), + G0(24.50f), + Gs0(25.96f), + Ab0(25.96f), + A0(27.50f), + As0(29.14f), + Bb0(29.14f), + B0(30.87f), + + C1(32.70f), + Cs1(34.65f), + Db1(34.65f), + D1(36.71f), + Ds1(38.89f), + Eb1(38.89f), + E1(41.20f), + F1(43.65f), + Fs1(46.25f), + Gb1(46.25f), + G1(49.00f), + Gs1(51.91f), + Ab1(51.91f), + A1(55.00f), + As1(58.27f), + Bb1(58.27f), + B1(61.74f), + + C2(65.41f), + Cs2(69.30f), + Db2(69.30f), + D2(73.42f), + Ds2(77.78f), + Eb2(77.78f), + E2(82.41f), + F2(87.31f), + Fs2(92.50f), + Gb2(92.50f), + G2(98.00f), + Gs2(103.83f), + Ab2(103.83f), + A2(110.00f), + As2(116.54f), + Bb2(116.54f), + B2(123.47f), } class NotesLib : TwoArgFunction() { @@ -16,7 +66,7 @@ class NotesLib : TwoArgFunction() { val keys = LuaTable() Note.values().forEach { note -> - keys[note.name.lowercase()] = valueOf(note.ordinal) + keys[note.name] = valueOf(note.ordinal) } arg2["notes"] = keys diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index 85ea3c2c..cbd54e56 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -40,7 +40,10 @@ class SfxLib( abstract inner class WaveFunction : TwoArgFunction() { private val notes = Note.values() - override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + override fun call( + @TinyArg("note", description = "Note from c0 to b8. Please check `note` for more information.") arg1: LuaValue, + @TinyArg("duration", description = "Duration in the note in seconds (default 0.1 second)") arg2: LuaValue, + ): LuaValue { val note = if (arg1.isint()) { notes[arg1.checkint()] } else { @@ -55,18 +58,22 @@ class SfxLib( abstract fun wave(note: Note, duration: Seconds): WaveGenerator } + @TinyFunction("Generate and play a sine wave sound.") inner class sine : WaveFunction() { override fun wave(note: Note, duration: Seconds) = SineWave(note, duration) } + @TinyFunction("Generate and play a sawtooth wave sound.") inner class sawtooth : WaveFunction() { override fun wave(note: Note, duration: Seconds) = SawTooth(note, duration) } + @TinyFunction("Generate and play a square wave sound.") inner class square : WaveFunction() { override fun wave(note: Note, duration: Seconds) = SquareWave(note, duration) } + @TinyFunction("Generate and play a triangle wave sound.") inner class triangle : WaveFunction() { override fun wave(note: Note, duration: Seconds) = TriangleWave(note, duration) } From 5097391f84c75310f42b05acb42a0ab7e4546ef3 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 22 Jan 2024 11:30:53 +0100 Subject: [PATCH 03/48] PoC of generating sound on the Javascript platform --- .../com/github/minigdx/tiny/lua/SfxLib.kt | 14 +++++ .../minigdx/tiny/sound/WaveGenerator.kt | 26 +++++++++ .../tiny/platform/test/HeadlessPlatform.kt | 6 +-- .../tiny/platform/webgl/AudioContext.kt | 54 +++++++++++++++++++ .../platform/webgl/PicoAudioSoundMananger.kt | 34 ++++++++++-- .../platform/glfw/JavaMidiSoundManager.kt | 6 +-- 6 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/AudioContext.kt diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index cbd54e56..ed639fd2 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -7,6 +7,8 @@ import com.github.mingdx.tiny.doc.TinyLib import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.engine.GameResourceAccess import com.github.minigdx.tiny.resources.Sound +import com.github.minigdx.tiny.sound.NoiseWave +import com.github.minigdx.tiny.sound.PulseWave import com.github.minigdx.tiny.sound.SawTooth import com.github.minigdx.tiny.sound.SineWave import com.github.minigdx.tiny.sound.SquareWave @@ -31,6 +33,8 @@ class SfxLib( ctrl.set("sine", sine()) ctrl.set("square", square()) ctrl.set("triangle", triangle()) + ctrl.set("noise", noise()) + ctrl.set("pulse", pulse()) ctrl.set("saw", sawtooth()) arg2.set("sfx", ctrl) arg2.get("package").get("loaded").set("sfx", ctrl) @@ -78,6 +82,16 @@ class SfxLib( override fun wave(note: Note, duration: Seconds) = TriangleWave(note, duration) } + @TinyFunction("Generate and play a noise wave sound.") + inner class noise : WaveFunction() { + override fun wave(note: Note, duration: Seconds) = NoiseWave(note, duration) + } + + @TinyFunction("Generate and play a pulse wave sound.") + inner class pulse : WaveFunction() { + override fun wave(note: Note, duration: Seconds) = PulseWave(note, duration) + } + @TinyFunction( "Play a sound by it's index. " + "The index of a sound is given by it's position in the sounds field from the `_tiny.json` file." + diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index eec5fe93..211ab0c9 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -3,7 +3,9 @@ package com.github.minigdx.tiny.sound import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.lua.Note import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE +import kotlin.math.abs import kotlin.math.sin +import kotlin.random.Random sealed class WaveGenerator(val note: Note, val duration: Seconds) { @@ -29,6 +31,7 @@ class SawTooth(note: Note, duration: Seconds) : WaveGenerator(note, duration) { return (2 * (angle(sample) / TWO_PI)) } } + class SineWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { override fun generate(sample: Int): Float { return sin(angle(sample)) @@ -56,3 +59,26 @@ class TriangleWave(note: Note, duration: Seconds) : WaveGenerator(note, duration } } } + +class NoiseWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { + + private var lastNoise = 0.0f + override fun generate(sample: Int): Float { + val white = Random.nextFloat() * 2 - 1 + val brown = (lastNoise + (0.02f * white)) / 1.02f + lastNoise = brown + return brown * 3.5f + } +} + +class PulseWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { + override fun generate(sample: Int): Float { + val angle = angle(sample) + + val t = angle % 1 + val k = abs(2.0 * ((angle / 128.0) % 1.0) - 1.0) + val u = (t + 0.5 * k) % 1.0 + val ret = abs(4.0 * u - 2.0) - abs(8.0 * t - 4.0) + return (ret / 6.0).toFloat() + } +} diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt index 2a7d846d..15b7012f 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt @@ -7,7 +7,6 @@ import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.graphic.FrameBuffer import com.github.minigdx.tiny.input.InputHandler import com.github.minigdx.tiny.input.InputManager -import com.github.minigdx.tiny.lua.Note import com.github.minigdx.tiny.platform.ImageData import com.github.minigdx.tiny.platform.Platform import com.github.minigdx.tiny.platform.RenderContext @@ -15,6 +14,7 @@ import com.github.minigdx.tiny.platform.SoundData import com.github.minigdx.tiny.platform.WindowManager import com.github.minigdx.tiny.sound.MidiSound import com.github.minigdx.tiny.sound.SoundManager +import com.github.minigdx.tiny.sound.WaveGenerator import com.github.minigdx.tiny.util.MutableFixedSizeList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -88,9 +88,7 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map } } - override fun playNotes(notes: List>, longuestDuration: Seconds) { - TODO("Not yet implemented") - } + override fun playNotes(notes: List, longuestDuration: Seconds) = Unit } } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/AudioContext.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/AudioContext.kt new file mode 100644 index 00000000..7bdb87ac --- /dev/null +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/AudioContext.kt @@ -0,0 +1,54 @@ +package com.github.minigdx.tiny.platform.webgl + +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Float32Array +import org.w3c.dom.events.Event +import kotlin.js.Promise + +// https://github.com/seisuke/pikot8/blob/main/shared/src/jsMain/kotlin/io/github/seisuke/pikot8/AudioContext.kt +external class AudioContext { + + val sampleRate: Float + val destination: AudioNode + val baseLatency: Double // seconds, experimental + val outputLatency: Double // seconds, experimental + fun close() + fun createOscillator(): OscillatorNode + + fun createBuffer(numOfChannels: Int, length: Int, sampleRate: Int): AudioBuffer + fun createBufferSource(): AudioBufferSourceNode + fun decodeAudioData(data: ArrayBuffer): Promise + + fun createGain(): GainNode +} + +open external class AudioNode { + var onended: ((Event) -> Unit)? + fun connect(destination: AudioNode, output: Int = definedExternally, input: Int = definedExternally): AudioNode +} + +external class OscillatorNode : AudioNode { + fun start(time: Double = definedExternally) +} + +external class AudioBuffer { + fun getChannelData(channel: Int): Float32Array +} + +external class AudioBufferSourceNode : AudioNode { + fun start(time: Double = definedExternally) + fun stop(time: Double = definedExternally) + var buffer: AudioBuffer + var loop: Boolean +} + +external class AudioParam { + val defaultValue: Float + val maxValue: Float + val minValue: Float + var value: Float +} + +external class GainNode : AudioNode { + val gain: AudioParam +} diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index 915ed1cc..d3e00356 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -2,9 +2,10 @@ package com.github.minigdx.tiny.platform.webgl import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler -import com.github.minigdx.tiny.lua.Note import com.github.minigdx.tiny.sound.MidiSound import com.github.minigdx.tiny.sound.SoundManager +import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE +import com.github.minigdx.tiny.sound.WaveGenerator class PicoAudioSound(val audio: dynamic, val smf: dynamic) : MidiSound { override fun play() { @@ -26,7 +27,11 @@ class PicoAudioSound(val audio: dynamic, val smf: dynamic) : MidiSound { class PicoAudioSoundMananger : SoundManager { - override fun initSoundManager(inputHandler: InputHandler) = Unit + lateinit var audioContext: AudioContext + + override fun initSoundManager(inputHandler: InputHandler) { + audioContext = AudioContext() + } override suspend fun createSound(data: ByteArray): MidiSound { val audio = js("var PicoAudio = require('picoaudio'); new PicoAudio.default()") @@ -34,7 +39,28 @@ class PicoAudioSoundMananger : SoundManager { return PicoAudioSound(audio, smf) } - override fun playNotes(notes: List>) { - TODO("Not yet implemented") + private fun toAudioBuffer(wave: WaveGenerator): AudioBuffer { + val audioBuffer = audioContext.createBuffer( + 1, + (wave.duration * SAMPLE_RATE).toInt(), + SAMPLE_RATE, + ) + val channel = audioBuffer.getChannelData(0) + + val numSamples: Int = (SAMPLE_RATE * wave.duration).toInt() + + val values = (0 until numSamples).map { index -> + wave.generate(index) + } + channel.set(values.toTypedArray()) + return audioBuffer + } + override fun playNotes(notes: List, longuestDuration: Seconds) { + // FIXME: how to merge waves?? + val firstWave = notes.firstOrNull() ?: return + val source = audioContext.createBufferSource() + source.buffer = toAudioBuffer(firstWave) + source.connect(audioContext.destination) + source.start() } } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index cfb86df8..e958bf87 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -84,15 +84,13 @@ class JavaMidiSoundManager : SoundManager { override fun playNotes(notes: List, longuestDuration: Seconds) { if (notes.isEmpty()) return - val sampleRate = SAMPLE_RATE - // TODO: boucle sur les notes. // regarder dans le buffer et faire un + entre note et buffer // write le tout et let's go // mettre a jour la List : LiveNote(note, duration, type) - buffer = ByteArray((longuestDuration * sampleRate).toInt() * 2) + buffer = ByteArray((longuestDuration * SAMPLE_RATE).toInt() * 2) notes.forEach { wave -> - val numSamples: Int = (sampleRate * wave.duration).toInt() + val numSamples: Int = (SAMPLE_RATE * wave.duration).toInt() for (i in 0 until numSamples) { val byteA = buffer[2 * i] and 0xFF.toByte() From e930867bd523b2a8da7511d0f1317931af1f7ce3 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 22 Jan 2024 13:55:36 +0100 Subject: [PATCH 04/48] Merge multiple notes into the same sound --- .../github/minigdx/tiny/sound/SoundManager.kt | 10 +++++++ .../minigdx/tiny/sound/WaveGenerator.kt | 11 +++++++- .../platform/webgl/PicoAudioSoundMananger.kt | 25 ++++++++++------- .../platform/glfw/JavaMidiSoundManager.kt | 28 ++++++------------- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index 9b7baeb8..b183d4b6 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -19,6 +19,16 @@ interface SoundManager { fun playNotes(notes: List, longuestDuration: Seconds) + fun mix(sample: Int, notes: List): Float { + var result = 0f + notes.forEach { + if (it.accept(sample)) { + result = result.toRawBits().or(it.generate(sample).toRawBits()).toFloat() + } + } + return result + } + companion object { const val SAMPLE_RATE = 44100 diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index 211ab0c9..10f47b6a 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -7,15 +7,24 @@ import kotlin.math.abs import kotlin.math.sin import kotlin.random.Random -sealed class WaveGenerator(val note: Note, val duration: Seconds) { +sealed class WaveGenerator(note: Note, val duration: Seconds) { val period = SAMPLE_RATE.toFloat() / note.frequency + val numberOfSample = SAMPLE_RATE * duration + /** * Return a value between -1.0 and 1.0 */ abstract fun generate(sample: Int): Float + /** + * Is a wave can be generated for this sample? + */ + internal fun accept(sample: Int): Boolean { + return sample < numberOfSample + } + internal fun angle(sample: Int): Float { return (TWO_PI * sample) / period } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index d3e00356..090fbaad 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -6,6 +6,8 @@ import com.github.minigdx.tiny.sound.MidiSound import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE import com.github.minigdx.tiny.sound.WaveGenerator +import org.khronos.webgl.Float32Array +import org.khronos.webgl.set class PicoAudioSound(val audio: dynamic, val smf: dynamic) : MidiSound { override fun play() { @@ -39,27 +41,30 @@ class PicoAudioSoundMananger : SoundManager { return PicoAudioSound(audio, smf) } - private fun toAudioBuffer(wave: WaveGenerator): AudioBuffer { + private fun toAudioBuffer(notes: List, longuestDuration: Seconds): AudioBuffer { + val numSamples = (longuestDuration * SAMPLE_RATE).toInt() + val audioBuffer = audioContext.createBuffer( 1, - (wave.duration * SAMPLE_RATE).toInt(), + numSamples, SAMPLE_RATE, ) val channel = audioBuffer.getChannelData(0) - val numSamples: Int = (SAMPLE_RATE * wave.duration).toInt() - - val values = (0 until numSamples).map { index -> - wave.generate(index) + val result = Float32Array((SAMPLE_RATE * longuestDuration).toInt()) + (0 until numSamples).forEach { index -> + val signal = mix(index, notes) + result[index] = signal } - channel.set(values.toTypedArray()) + channel.set(result) return audioBuffer } + override fun playNotes(notes: List, longuestDuration: Seconds) { - // FIXME: how to merge waves?? - val firstWave = notes.firstOrNull() ?: return + if (notes.isEmpty()) return + val source = audioContext.createBufferSource() - source.buffer = toAudioBuffer(firstWave) + source.buffer = toAudioBuffer(notes, longuestDuration) source.connect(audioContext.destination) source.start() } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index e958bf87..fa9d8377 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -14,7 +14,6 @@ import javax.sound.sampled.AudioFormat import javax.sound.sampled.AudioSystem import javax.sound.sampled.SourceDataLine import kotlin.experimental.and -import kotlin.experimental.or class JavaMidiSound(private val data: ByteArray) : MidiSound { @@ -84,28 +83,17 @@ class JavaMidiSoundManager : SoundManager { override fun playNotes(notes: List, longuestDuration: Seconds) { if (notes.isEmpty()) return - // TODO: boucle sur les notes. - // regarder dans le buffer et faire un + entre note et buffer - // write le tout et let's go - // mettre a jour la List : LiveNote(note, duration, type) buffer = ByteArray((longuestDuration * SAMPLE_RATE).toInt() * 2) - notes.forEach { wave -> - val numSamples: Int = (SAMPLE_RATE * wave.duration).toInt() + val numSamples: Int = (SAMPLE_RATE * longuestDuration).toInt() + for (i in 0 until numSamples) { + val sample = mix(i, notes) - for (i in 0 until numSamples) { - val byteA = buffer[2 * i] and 0xFF.toByte() - val byteB = (buffer[2 * i + 1]).toInt().shl(8).toByte() + val sampleValue: Float = (sample * Short.MAX_VALUE) + val clippedValue = sampleValue.coerceIn(Short.MIN_VALUE.toFloat(), Short.MAX_VALUE.toFloat()) + val result = clippedValue.toInt().toShort() - // TODO: le mix de note n'est pas bon. - // TODO: la fin du son n'est pas ouf. - val actualBuffer = byteA.or(byteB).toShort() - - val sample = wave.generate(i) - val sampleValue = (Short.MAX_VALUE * sample).toInt().toShort() + actualBuffer - - buffer[2 * i] = (sampleValue and 0xFF).toByte() - buffer[2 * i + 1] = ((sampleValue shr 8) and 0xFF).toByte() - } + buffer[2 * i] = (result and 0xFF).toByte() + buffer[2 * i + 1] = (result.toInt().shr(8) and 0xFF).toByte() } // generate the byte array From effedc6febd6e6987977c72ab708e924248ee966 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 22 Jan 2024 13:58:13 +0100 Subject: [PATCH 05/48] Fix test compilation --- .../com/github/minigdx/tiny/sound/SoundManager.kt | 2 +- .../kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt | 2 ++ .../kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt | 2 ++ .../minigdx/tiny/platform/test/HeadlessPlatform.kt | 2 +- .../tiny/platform/webgl/PicoAudioSoundMananger.kt | 10 +++++----- .../minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt | 6 +++--- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index b183d4b6..add73c05 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -17,7 +17,7 @@ interface SoundManager { suspend fun createSound(data: ByteArray): MidiSound - fun playNotes(notes: List, longuestDuration: Seconds) + fun playNotes(notes: List, longestDuration: Seconds) fun mix(sample: Int, notes: List): Float { var result = 0f diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt index e62ffb55..1f9e4400 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt @@ -7,6 +7,7 @@ import com.github.minigdx.tiny.resources.GameLevel import com.github.minigdx.tiny.resources.GameScript import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.resources.SpriteSheet +import com.github.minigdx.tiny.sound.WaveGenerator import org.luaj.vm2.LuaValue.Companion.valueOf import kotlin.test.Test import kotlin.test.assertEquals @@ -21,6 +22,7 @@ class GfxLibTest { override fun level(index: Int): GameLevel? = null override fun sound(index: Int): Sound? = null override fun script(name: String): GameScript? = null + override fun note(wave: WaveGenerator) = Unit } @Test diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt index d75101d2..541be145 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt @@ -11,6 +11,7 @@ import com.github.minigdx.tiny.resources.GameScript import com.github.minigdx.tiny.resources.ResourceType import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.resources.SpriteSheet +import com.github.minigdx.tiny.sound.WaveGenerator import org.luaj.vm2.LuaValue.Companion.valueOf import org.luaj.vm2.LuaValue.Companion.varargsOf import kotlin.test.Test @@ -36,6 +37,7 @@ class StdLibTest { override fun level(index: Int): GameLevel? = null override fun sound(index: Int): Sound? = null override fun script(name: String): GameScript? = null + override fun note(wave: WaveGenerator) = Unit } private val gameOptions = GameOptions( diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt index 15b7012f..bb3f32c0 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt @@ -88,7 +88,7 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map } } - override fun playNotes(notes: List, longuestDuration: Seconds) = Unit + override fun playNotes(notes: List, longestDuration: Seconds) = Unit } } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index 090fbaad..4c3f213c 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -41,8 +41,8 @@ class PicoAudioSoundMananger : SoundManager { return PicoAudioSound(audio, smf) } - private fun toAudioBuffer(notes: List, longuestDuration: Seconds): AudioBuffer { - val numSamples = (longuestDuration * SAMPLE_RATE).toInt() + private fun toAudioBuffer(notes: List, longestDuration: Seconds): AudioBuffer { + val numSamples = (longestDuration * SAMPLE_RATE).toInt() val audioBuffer = audioContext.createBuffer( 1, @@ -51,7 +51,7 @@ class PicoAudioSoundMananger : SoundManager { ) val channel = audioBuffer.getChannelData(0) - val result = Float32Array((SAMPLE_RATE * longuestDuration).toInt()) + val result = Float32Array((SAMPLE_RATE * longestDuration).toInt()) (0 until numSamples).forEach { index -> val signal = mix(index, notes) result[index] = signal @@ -60,11 +60,11 @@ class PicoAudioSoundMananger : SoundManager { return audioBuffer } - override fun playNotes(notes: List, longuestDuration: Seconds) { + override fun playNotes(notes: List, longestDuration: Seconds) { if (notes.isEmpty()) return val source = audioContext.createBufferSource() - source.buffer = toAudioBuffer(notes, longuestDuration) + source.buffer = toAudioBuffer(notes, longestDuration) source.connect(audioContext.destination) source.start() } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index fa9d8377..9f290bb1 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -80,11 +80,11 @@ class JavaMidiSoundManager : SoundManager { return JavaMidiSound(data) } - override fun playNotes(notes: List, longuestDuration: Seconds) { + override fun playNotes(notes: List, longestDuration: Seconds) { if (notes.isEmpty()) return - buffer = ByteArray((longuestDuration * SAMPLE_RATE).toInt() * 2) - val numSamples: Int = (SAMPLE_RATE * longuestDuration).toInt() + buffer = ByteArray((longestDuration * SAMPLE_RATE).toInt() * 2) + val numSamples: Int = (SAMPLE_RATE * longestDuration).toInt() for (i in 0 until numSamples) { val sample = mix(i, notes) From 49c99bac9837efdf0fa6b10cc77a15b6d28e9c76 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 22 Jan 2024 19:00:48 +0100 Subject: [PATCH 06/48] Support custom volume for each notes --- .../com/github/minigdx/tiny/lua/SfxLib.kt | 29 ++++++++++++------- .../github/minigdx/tiny/sound/SoundManager.kt | 3 +- .../minigdx/tiny/sound/WaveGenerator.kt | 15 +++++----- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index ed639fd2..d434380e 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -4,6 +4,7 @@ import com.github.mingdx.tiny.doc.TinyArg import com.github.mingdx.tiny.doc.TinyCall import com.github.mingdx.tiny.doc.TinyFunction import com.github.mingdx.tiny.doc.TinyLib +import com.github.minigdx.tiny.Percent import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.engine.GameResourceAccess import com.github.minigdx.tiny.resources.Sound @@ -17,7 +18,10 @@ import com.github.minigdx.tiny.sound.WaveGenerator import org.luaj.vm2.LuaTable import org.luaj.vm2.LuaValue import org.luaj.vm2.lib.OneArgFunction +import org.luaj.vm2.lib.ThreeArgFunction import org.luaj.vm2.lib.TwoArgFunction +import kotlin.math.max +import kotlin.math.min @TinyLib("sfx", "Sound API to play/loop/stop a sound.") class SfxLib( @@ -41,12 +45,16 @@ class SfxLib( return ctrl } - abstract inner class WaveFunction : TwoArgFunction() { + abstract inner class WaveFunction : ThreeArgFunction() { private val notes = Note.values() override fun call( - @TinyArg("note", description = "Note from c0 to b8. Please check `note` for more information.") arg1: LuaValue, + @TinyArg( + "note", + description = "Note from c0 to b8. Please check `note` for more information.", + ) arg1: LuaValue, @TinyArg("duration", description = "Duration in the note in seconds (default 0.1 second)") arg2: LuaValue, + @TinyArg("volume", description = "Volume express in percentage (between 0.0 and 1.0)") arg3: LuaValue, ): LuaValue { val note = if (arg1.isint()) { notes[arg1.checkint()] @@ -55,41 +63,42 @@ class SfxLib( } val duration = arg2.optdouble(0.1) - resourceAccess.note(wave(note, duration.toFloat())) + val volume = max(min(arg3.optdouble(1.0), 1.0), 0.0) + resourceAccess.note(wave(note, duration.toFloat(), volume.toFloat())) return NIL } - abstract fun wave(note: Note, duration: Seconds): WaveGenerator + abstract fun wave(note: Note, duration: Seconds, volume: Percent): WaveGenerator } @TinyFunction("Generate and play a sine wave sound.") inner class sine : WaveFunction() { - override fun wave(note: Note, duration: Seconds) = SineWave(note, duration) + override fun wave(note: Note, duration: Seconds, volume: Percent) = SineWave(note, duration, volume) } @TinyFunction("Generate and play a sawtooth wave sound.") inner class sawtooth : WaveFunction() { - override fun wave(note: Note, duration: Seconds) = SawTooth(note, duration) + override fun wave(note: Note, duration: Seconds, volume: Percent) = SawTooth(note, duration, volume) } @TinyFunction("Generate and play a square wave sound.") inner class square : WaveFunction() { - override fun wave(note: Note, duration: Seconds) = SquareWave(note, duration) + override fun wave(note: Note, duration: Seconds, volume: Percent) = SquareWave(note, duration, volume) } @TinyFunction("Generate and play a triangle wave sound.") inner class triangle : WaveFunction() { - override fun wave(note: Note, duration: Seconds) = TriangleWave(note, duration) + override fun wave(note: Note, duration: Seconds, volume: Percent) = TriangleWave(note, duration, volume) } @TinyFunction("Generate and play a noise wave sound.") inner class noise : WaveFunction() { - override fun wave(note: Note, duration: Seconds) = NoiseWave(note, duration) + override fun wave(note: Note, duration: Seconds, volume: Percent) = NoiseWave(note, duration, volume) } @TinyFunction("Generate and play a pulse wave sound.") inner class pulse : WaveFunction() { - override fun wave(note: Note, duration: Seconds) = PulseWave(note, duration) + override fun wave(note: Note, duration: Seconds, volume: Percent) = PulseWave(note, duration, volume) } @TinyFunction( diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index add73c05..b18cdf76 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -23,7 +23,8 @@ interface SoundManager { var result = 0f notes.forEach { if (it.accept(sample)) { - result = result.toRawBits().or(it.generate(sample).toRawBits()).toFloat() + val sampleValue = it.generate(sample) * it.volume + result += sampleValue } } return result diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index 10f47b6a..4bcbfa9d 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -1,5 +1,6 @@ package com.github.minigdx.tiny.sound +import com.github.minigdx.tiny.Percent import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.lua.Note import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE @@ -7,7 +8,7 @@ import kotlin.math.abs import kotlin.math.sin import kotlin.random.Random -sealed class WaveGenerator(note: Note, val duration: Seconds) { +sealed class WaveGenerator(note: Note, val duration: Seconds, val volume: Percent) { val period = SAMPLE_RATE.toFloat() / note.frequency @@ -35,19 +36,19 @@ sealed class WaveGenerator(note: Note, val duration: Seconds) { } } -class SawTooth(note: Note, duration: Seconds) : WaveGenerator(note, duration) { +class SawTooth(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { return (2 * (angle(sample) / TWO_PI)) } } -class SineWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { +class SineWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { return sin(angle(sample)) } } -class SquareWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { +class SquareWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { val value = sin(angle(sample)) return if (value > 0f) { @@ -58,7 +59,7 @@ class SquareWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) } } -class TriangleWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { +class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { val angle = angle(sample) return if (angle < PI) { @@ -69,7 +70,7 @@ class TriangleWave(note: Note, duration: Seconds) : WaveGenerator(note, duration } } -class NoiseWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { +class NoiseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { private var lastNoise = 0.0f override fun generate(sample: Int): Float { @@ -80,7 +81,7 @@ class NoiseWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { } } -class PulseWave(note: Note, duration: Seconds) : WaveGenerator(note, duration) { +class PulseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { val angle = angle(sample) From 19c994d1288bd0c0f058b45ff3119f0ed84a307c Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Tue, 23 Jan 2024 08:36:47 +0100 Subject: [PATCH 07/48] Add fade out on the sound and run the sound in background on the JVM --- tiny-cli/src/jvmMain/resources/sfx/_tiny.json | 36 ++++++++ tiny-cli/src/jvmMain/resources/sfx/game.lua | 80 ++++++++++++++++++ tiny-cli/src/jvmMain/resources/sfx/mouse.lua | 26 ++++++ .../src/jvmMain/resources/sfx/tiny-export.zip | Bin 0 -> 214135 bytes .../github/minigdx/tiny/sound/SoundManager.kt | 13 +++ .../platform/webgl/PicoAudioSoundMananger.kt | 5 +- .../platform/glfw/JavaMidiSoundManager.kt | 59 ++++++++----- 7 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 tiny-cli/src/jvmMain/resources/sfx/_tiny.json create mode 100644 tiny-cli/src/jvmMain/resources/sfx/game.lua create mode 100644 tiny-cli/src/jvmMain/resources/sfx/mouse.lua create mode 100644 tiny-cli/src/jvmMain/resources/sfx/tiny-export.zip diff --git a/tiny-cli/src/jvmMain/resources/sfx/_tiny.json b/tiny-cli/src/jvmMain/resources/sfx/_tiny.json new file mode 100644 index 00000000..12f05a68 --- /dev/null +++ b/tiny-cli/src/jvmMain/resources/sfx/_tiny.json @@ -0,0 +1,36 @@ +{ + "version": "V1", + "name": "Tiny SFX Sequencer", + "resolution": { + "width": 512, + "height": 288 + }, + "sprites": { + "width": 8, + "height": 8 + }, + "hideMouseCursor": true, + "zoom": 2, + "colors": [ + "#000000", + "#1D2B53", + "#7E2553", + "#008751", + "#AB5236", + "#5F574F", + "#C2C3C7", + "#FFF1E8", + "#FF004D", + "#FFA300", + "#FFEC27", + "#00E436", + "#29ADFF", + "#83769C", + "#FF77A8", + "#FFCCAA" + ], + "scripts": [ + "game.lua", + "mouse.lua" + ] +} \ No newline at end of file diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua new file mode 100644 index 00000000..6cd93b08 --- /dev/null +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -0,0 +1,80 @@ +local mouse = require("mouse") + +local triggers = {} + +local Trigger = { + tick = 0, + note = 0, + width = 8, + height = 4, + active = false, + player = false, +} + +local metronome = { + tick = 0 +} + +function _init() + for x = 1, 10 do + for y = 1, 40 do + table.insert(triggers, new(Trigger, { + tick = x, + note = y, + x = x * 10, + y = y * 6 + })) + end + end +end + +function on_click(x, y) + for i = 1, #triggers do + local trigger = triggers[i] + if x > trigger.x and x < trigger.x + trigger.width and y > trigger.y and y < trigger.y + trigger.height then + trigger.active = not trigger.active + end + end +end + +function tick_updated() + for i = 1, #triggers do + local trigger = triggers[i] + if metronome.tick == trigger.tick and trigger.active then + sfx.noise(trigger.note, 0.1666) + end + end +end + +function _update() + mouse._update(on_click) + + if tiny.frame % 10 == 0 then + metronome.tick = metronome.tick + 1 + if (metronome.tick > 10) then + metronome.tick = 1 + end + tick_updated() + end +end + +function _draw() + gfx.cls() + + for i = 1, #triggers do + local trigger = triggers[i] + local color = 7 + if trigger.tick == metronome.tick then + color = 9 + end + if(trigger.active) then + -- gfx.dither(0xA5A5) + shape.rectf(trigger.x, trigger.y, trigger.width, trigger.height, 8) + else + shape.rect(trigger.x, trigger.y, trigger.width, trigger.height, color) + -- gfx.dither() + end + end + + mouse._draw() +end diff --git a/tiny-cli/src/jvmMain/resources/sfx/mouse.lua b/tiny-cli/src/jvmMain/resources/sfx/mouse.lua new file mode 100644 index 00000000..e2441846 --- /dev/null +++ b/tiny-cli/src/jvmMain/resources/sfx/mouse.lua @@ -0,0 +1,26 @@ + + + +local Mouse = { + x = 0, + y = 0, +} + +local mouse = new(Mouse) + +mouse._update = function(on_click) + local pos = ctrl.touch() + mouse.x = pos.x + mouse.y = pos.y + + local clicked = ctrl.touched(0) + if clicked then + on_click(clicked.x, clicked.y) + end +end + +mouse._draw = function() + shape.circle(mouse.x, mouse.y, 2, 9) +end + +return mouse \ No newline at end of file diff --git a/tiny-cli/src/jvmMain/resources/sfx/tiny-export.zip b/tiny-cli/src/jvmMain/resources/sfx/tiny-export.zip new file mode 100644 index 0000000000000000000000000000000000000000..12f570cbde2c0d8f31bb8659b351c743fb8618b9 GIT binary patch literal 214135 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

!2S?ZJJLR zo95%owTq9wLY!$SY?%QFC|)Y7NyNoAvn0Rlg(Q()rk{%RGPP8scl7!fq?awOi1e@` z{d1|z^&$C3wahgHOo=eDi*O22mG#Qdlq%=~q6ne~kW?%*W08DcxThN6i}HT{ePp1p3-2MD*%JmHzL6~6g(E%@N891CWEvvk?Keqq zJ3}yR-;)SelKicPEmniP$f%3GeeEfUj&;Y60+86T?6n1tr2K)_Xd8YvBC4HD@nr=T zowDfkBED2-X$K1n1IoB{RQ$b#g)}k-yfi_;fZgMAbV4$#QqjVK!3%>^f}u%5QNvgt zlz9z_CmSGgB3kUn?IglSEvJUL0eiw21(j@zN>aXTco1CbxWl1#-ho7to82;Qii=em zQ=v)S$oTKT)+{M6WvrKZw|9oIRCtk#^$mEk&}SUwY}kNfRn>g&X8GQ&Nq|g?_p(EpNDOvCWZ?5}w z3J4cRtRu&#)LM(QD{ig8jR1HnJl>+zn$iDu3?tgyTrn4oXlu)ci?WOC`S}YYSd7K> zEtV^O>#1eHNR3CcvZ^&7()|Di1f3t6aQ2xFQS=wsLWdC+=kvG-j)}=iJ>*Va2+WMx z0hQ*6imunv`o*RZFT#VAkB`2LNioJ0$^*MTY7Cr#I$+M9?uN5!p?qce%1Hrr>Rhqg z5*Oj+8^`f4t}Z36Cw&qFk_Tl1L_89c6w78WX$FfvGLfh{5X{Zf*GTL?EVqI!(ZmvGU4ZDls zrMhr9q3zhG-FU*5nVr7KVy$2n%YMXbc^8vQg`j`=bam0WbiBlH<>|>2cnw_*qU*CW zA;E)~;-UetC7@)|KY@}4pH?=wB;VjP-1LqLNOCz#*)d568XQXZOj`BkjBpa4N+Jv{o;Xa@GZ-`wX7Urd0Di^6 zuP&W9F{r3KA{@L5*5F6VT?BMFpvwU5Ch!N;*I4yg@S~0ow0A!Ct z_9RkS`f5WwI}U(D4md0*osXNqF2U;fZN$ztxlyH3=|*KrC9d;Ir4ryORPAK}Q>|FMNMt;xF?)z zl34SrpxWuyJlcToZ+EO4gWHvNa0|@bU}t{Y!e*Bl>N6GqVRhImU=xazHD$=JJ z(w|>8onhhgi>5PK@=K<#A$_Dsf5?#j{JQE49r;z&*U%9tI$#Y}(eeD!>kJ*G3$LMg zUv*0x1D}O;>T2A1#o8f%O&r$_h3_4KD8C&0(}?nGwLgt0zmogYh)UOaB||qAL-UKg zKV52m1^A~Cl`fzR^kos&>q-_-W*|!zQ24^$%(@8^kOK8Azd*re#YHtiYAV zZ3}E8Hj@%aFPv@z0i#d61m|cl#{fy78(3_{%5q;ASAjz{7pl?)n+n&$g1^a2Xel=z zNXCUC1MNb%!y!_5v=8={&cDKpZ;P_Q1D{mEM{UaS(Fn*|VNLMOBN(YBpQ?jax8Pb^ zoy=nM$nzQ03_EzS9<#8Z7J{WOutd!oMWw2#dbaHRqGsh_y|BP1i!Q8`Nwk4BYn43> zts}z&UD~|<@;nO*c=17OTC<7a<;>fzZ^(QVXF}S*Apuu$CM19YGOb>Hh`;^+Q}?Fr zjpImy@DF*v*xGvCDd5-Bn#jch}L`m6Pj7mS~B#MAiZ$Wk{Fl@?RH+oF^${4N~Y9!(N+1K&rI z(LOg432Ie4*E_`_De43^rm$$R@N+VF4O=+yWynUvL#c z6P?iyHoK7-=e*`F;pSOvxh-xUmwZ7jgm}wUcBAHHttN}b*g6Ytcgd22#L|!3NH`nY zjjHgjv|`^-3S%s+76RsR}%`W=T2tJyr=SAK6Uoo-C$JpvPmC>JMf6MHG&JEDjKOEGXB7{>6gIE2&00-QaytZk ztH%majfX!0qU<3V@!DO!Ym$!`rvrO<)a@ER;O_x$SvCKW12q0kz2DY!9eW#nOb zlI`r+_`f`(ABUl8Z=*zdYoV!E)nubD?hU7LrpdX0*W2HZzlZM!$Mp^%$S>Nt0dml@ zQD3+M1}S}dI<7aQXzJ$1j1Kfs?{dp zXk7J0$FJF+pKXYXL26HQ9(AA(>cIX{1ClwSM^aCBb~{(3C}H0e7U@U05K65`@z%;& zq^P}ePrQ{#Pvd+PAVyiNa0&;RS>Pq^;t{@edBn}_!J}1}AXoR`)2-@|qVkAml7}@% zCT^iBvHX6-Go0_t^geLgWacT!Bkp!D<8>$TT6i1S>bhRK3H~MGkCs&PWa$xgu#Fq* zCRgovje8FkujMA7mk#L4X(B=pl*}e0(KvP?-S;qt2p*OO6J`#sf_Nc#glhUL{3!Zc z7l^89d+Beh4`U;=z;Uosr`{oDFh0!AK*y zpbI#VnCE2r0yVbfVCcfK|da6*ms&pr=w7} zU$q{1th{-P0?KRl?wJr8IGoZqoGUq;cKM)d_*bf4{%_l52cPDeBIE7aMM`q~fpycj z?F&q>X;hlZEi0{IzMh*V zdL*B>j@Y%oPaVkhvFoRqGce;C;Y_I@kl%me)q?#3*9)j|MmA=s4dRrwo>ceSCU)0%O9~EMe|)`09=9Xn!jqcJph>(I z!EL(tX&L+g8!VLNtK@aiIhiKoX(t@MFvkJa9&Eu)82p*Zw=3{%c7!M6en{JkB$H`A zG|k6l;Ea`ExKC-mM4y4jmc4Q(({*lI_S2x>J9vF?ynl4?dZ!Hk|BP-Lzr@=DFIz?I z9f@Yb#EJ%b=@5foauIKL2k1r-6)~ilsnj2%#C}cUWBQtma{D!p`}8#*rS@wYzoxG# ziFS~ye?$lQjOSYo-+a-?XMSmhx4%kLH7}vV#HV1K@D^98VrQs|E zaX3&M9hgTH$;mxWO{KAffoILkG7wl*qx^Kg|6J!Nb?mVGki|U&5^Wpa&i$N9*GLug z5z=FdAJxi*dsF}u4vnv*I^Hmz%yH0Zj+@-)=pfG*rUSk00i#J0e1|)8>t$cwOH83z{o|5%_SNG_d0AfTm2DREE|TjVf3?Sco$_jJ)q zayX6cPZIh$nVq}(XzQXh%$uQl>0Fyru-z4$sD@&kIo;3TRfmRJIZ)M!S_A;D9H<51 zHv{~JmSy5XzxstAW={QHwPO3JpuXOv@duy)!IiYW^BrDHcxE-OgE2AL8O{RWMCpt5 zmQoVf0wUi`33!n1kjs3d4wa-$H>r%hhEGrDe01Z7Q+mj)-h-FDi=eD4#ZIt#qA6Pk z9tpZV>3}HFoy@&-MIcwdshd&&zpMc#E>=gTlen=G<&?$}ysh~G!~B07xKzAWXo&?& zsQ{#M1ZER)yOcS8P75_id&6y)?3D*~7Ut$`f}$KPPcW#gbx^4Yc4QGxMRk46B=!Vn zkUBs&RvxBtj-&nz+}_!c6_G;mO!tMUJN`nBzj$sCpftl?c!q5jwGtK9mCY{v51$#+ z!^a_Oqr=+Bl@gMwA!T^B0Xl2F2kgnIHY0PWZ3c}M959WgQR_-Zl^aRIINR*=M8<+7%@HomTO(B+{uO(%aO0#Kl-*Gfaa9jb0#dd5}}3I>i*RRlDN!mVF)=o=K$t|0O)7%sz1F{OTi zlAWfZoM1Hh4ec8=Gk+Oyem>kzh(3y|5>dEovh5X$<1*<_0CQQu^DymQ#g+rx-5|HR zkbieZ!!u$9BS>*)wj-m?w{wG;usQ>F_>xlbe-o=5l!)Zn>Q-*|;vFG418}PO{6s08 zfJ>tAJ=O=atALzP;CweUuewy#VMz7Vq_;QF{tMt97lc+v+i1gP>yR2d@yWoF#;S8| z9*u>4W$mYz582m#-$ zptq|7_1r;2$^@z}^0Lz)r5V?g6X4^x5FGV}25cT`3ssz}(@Ne@(BA=*KbB)7lC(Ulhi*%}1-UE1n|1L}J%in=~imeSxqJb{=7D}8zrSf9igYB$keJ9zT_k#rILe@cZHUcC@qC7un_0~wng0t zG&1vp41W-dUvEI>;!dA{_+L!q2$`TPpFf*rX7oKa3^gViPG_laVLEGa(-8^1%W18Z zO*^`ZM+cPqHIR!GvZV#YbRnySihm@|uIysLg#)<=&RyjqjGEep#_(4yF?5?f$X+aH z@od-Ynxo_eYb^sjwB~Ge9=hWfF!Z-kgC+&iq)?})Vkl0!_D}PYy0VAgIvdMN2aSw9 zd_I!iZo; zDKQIlE+n(vs(BWI;e9F29(7OhHm~oxfPQ+~25(A_Uv>%BhkBN`j5ouD*0MdOhe%G# zWn&U)#j+SjCw{iqrgdYgP;cF|QLK%Xp*E~~ui~%t-2(iDz1cia=(*Zmt4B91RYR}< z+sErU@V)_`igPpX!;gVX;}bsH)A6jKvl=g!5(8@V24$qfaah$2<8qi!mwIic@TVJV zLmWwck%Gi|aBXqc>UpkftXdv0=<@5fH4G2`q0+awK6>qMKGI$<_@wkp0C6#I=@lTV z?z9TCv-4L7#``OHF|Z}Tz<6xczcG6zCMq7PHJLyBUFP3{1cZi>Nm>Asha!`wCy}R{ z$MZm@{fq*ml0Pbu^uQ}Gga=ILWned@2Gl zDW=R`a{Y5|WP?V6Xrwyy{->k!?9JADAg6LD4kb`UTdWa-;a}liEm4~>AabBjOcuix z+O^qO{q&@0zYKm2_-s|lnz-_v#Lv~!Q>E)ib(lg7i1wTlu0`zYeD8FAu8(5wjAGR` ziq+H8${EF3a}^BXgnPqs~zEO=|kQif>7=QYQFZE;UBklKe#~1LYQro9G zpWX|?V7+X$J-rtEd=&Y+A`E8mS=N}?^W3~%M$_GOJlkHcJvo^!aCrGF_X9R00h^zjqm$9=pd$$}*MgZjT8SaM?rq~VJgOohbp zx@Cnuixq~m(+KdV)RC#ob~qPii&Z?Qajke<`_F>cY22DFXL{&-psBOMs=lXV4c^li zSi82TAGGf2`({r^AfK?O?+-+O?w)?osLT@Z(jd1j2b9>`Q8>`?RT1n$Cd}~rpk6=J zZqTMlYBw&bH>E4V#^SmmL0m`jaKJLbpgL^ue0_%$9krAtHL=vP#*n3k^fpNF*ah?0 zgm`RH=dlaHV~hBLcx<5=NZTjJT2edPZfGr75}fL@WKJy);HIG zF#%_ifOky584+++C*YkR;4;1=0xk^!X~?(Q9m`9BH-mr2D5r9ipYbTCG|Fjxl%I)F zUdNx&D6i{m(%c}=X@flWY>?+KY>?-;LH_a&-yje4?rwxNZZ`7k)NW6=hF)20YtZ;J z9)E8rwxG(2Sl$Fu2z%-18yjuCLd~nhT&{FRTqSpNh*J8OJg}A6O=@FRx@P3Ve$LUF zd6#Jij6@Ec=FZhJd3irO%4T!B26w)KcbNtSp2e2ZZe$R!ah-T94xV#eMND|$VZ!PK zCajPNe@3S}kLM$v>b|gd2F8QOm;o|j2FQ#V007?idu_MT3h1A#kHM$pT>cq7fRok} z=r6)`VPm7k@AJ0!)(=qp%|CpJ?rR;kZ#Bq1C>JP{#A3u6H8#smd&Yw~W-nx+5ft>T zY62j&F+M=o<_>y-4?5W@+bgQ#I?1NVw4;qAebgaAaY&*9^^4AAoMo$WD^H4QnQV1- z)7?(MkJUscyPa@rvdUML;_svrwiz?Qtfi%Ub?h-WCmY%x^6-m}66S4NYF_I)0i| zMUqd)<+!7(tqGO%M{nPc?U@VyhBR!ClLc^Wz1Gkvw#3j%WehDszcu4o zOysc~1VMLrK3%euf+p8V^Ubo?Q3NXbpCNa3vHuxy6HH5Tn^i_n3ZP2;r7_akIFy}s zN3OQOC=~np+~E2UUi`kTqHmN ze9;R2>+iq#^8MNlie?8ljSeoQvgvg2{g;1zxes)cb^FL0eUz<(`26j+->!vUHpfsj z;8(5izx&~fFW17an()g8{M&Z;U;gy=r?>CEUEj%Vvy-aPN$L$M2A#b9;)}K5F)+6a z{?;?kZQy_W`P~as+yH;q0KaSn|Haqqm%*|L{=NbJsulcqKd)z?t0wq|2KYrQ_&@#d z*R|jmP4JHm@L4=nyQ(pdeb;;nM$4W@pNmP z&(zsIE4Tg!s@RYfJQLFDv8K$J3?ul|M#q~59y3uz6a5+mQX}Q{TI#dfx7A=G${A?K zJ(bfZs5Y|q7<|OnIv(vd-W=cJ-HMei-kLTyte9bMO_Q_P)@;e3gf%ZrOi-wqIHPlX z-fTEt{A^&?TA2FF-1vP$y_uhGcUTKebUGFc)SkCN{i6$Y-guU;%}*t~D*?EW*@xa8 z|AjC4?=a~#4M3vhi^K^(ImRC zN4CWFGFH8e=?$03mR7GbQFSKB{5e=R8s!(NJdZC-RwW&&wAbyruS*g4#cFk}u-=*_ zCB2e1a#5^~w6TEA`}Nu^nwg8vA~npRoK^h$O1yjV&m=u$+jJ&#e3XVcmTNNW?I zD+P3==QF3La{NfnJ2N!@>t}{$ub`PBjGEIk!ReWN;`9`DdbZ89%xZRb`=kUs-o;vi zBfk-#6^U7V%5aGp4z%Hn!U`+w&dMN-FuLXI5S(+H(?wQe3j@_(g-y)C+QoxgY&9rbHQGNRo04Cw%dI|%rc6_TF35B3Gb#p?D7V7 zu<5qpm)FqB!FU1>CPo&Uj}Y|TgRiY?5Xvb|gl_lp2y%<~bq#TAxB;IQ>Ps<@hA%do z!ZRVFZOUy$%Yj1RgZkrR)Q$SQX+9+X1Rv7AyxlZ+7B~HU^h8dB+Qs!o7ZpYy#K#TR z326gA&i-%qREPdH6nyjGrOv!|wdpsq4fq~AxxY>(WipMnW~)+% ze@3}rFiP71ZrJ{^6`|iD4!_%itMznwy+{DFWQv#TUQiGHgH{t_x>zK$@nUOro#bPv zu%*EhxJ)q0ojVsAgfPM=Agb9?(JL+I%cM*vTMD8VsjJrUd2$p=2#S1|tFW_cIkd&= zq3+~h=ce7MLV*g5gLY~0ZF)mAeIKlxYlPRqOLaHclxVPOZy<0H4X!t9AX2FafwXzk zOSftM2~8i|ruBeYY#UP3osaKkNh!skw-a5rkaa76-LjQ#_2__nMbMNy3%{#FiRM3q zjdu+qhJCw#ga~`y+>iaP5@K6_NwBp?*t4Q%9g0DPFArIY7q0q*~Mgi~AJ?`ci_9auIXM z08a$)!~uTaAcd=iraEWwqs!wDYulZh^nJ9oud7>u5myKf|B|rmku!S;QJb&rfSMJi znHWXUHfLsn;t!U#A0q*8s{WgEBT(+?`P{dk+gfi3WLB;({*y&0Pcj7ei(can& zA|W29YdSNnXr@h@q3^fEi=4i>fDVM}R5bxn3n_RKlC+F7N;A84XhtY>o%yJa8ddj@$<3 zop1nt6?x5h;leHzQ!i2V(pdX2g#!pO8Kt_(g{fD7pN3or;Xv>L<#v%h8Kf*hMXzH8 zZQO#Lx4}d;IE%L@8qG66a~_|8&&75xnl}+%sTNhdLXE*r3K5 z=;>&D>9$SWbPVEJtF>OjPgdDvTqc?NfB)2#n%tT1*5-B;%>_t}lX_xcvk#{QXn5w4LieAcD7nG;H zQ6`?czx=9%!-YT}ZuL%CDj^~9X}sUx*-1{~UjJxk zXa8V3j+338H+}dwl$|9I7C)rW09$-%o$zJ#{=uMpHSHe;zx%KM>ijO0S!v$%xv2AB z>VF-l?6oZ#yCuiA39Q) zh@&F@vJ86t!^52<>UGsBug>Xw8^HBDf>gqf69GN@W>nArMPvT8XPZcEgZXzT^rPYz zs3x7$#`!3tIFmdY_KGuo%xZmrMTjI20aNlOqgbuF#oWV{1omlm|R^k7VO#}vFbl{n7u@ptke|EMHT_!Aubh6J(XWDjjpF>@>*`{c(B{(2cE&(zx0IvWdFVq|5Owm z?Dd|5u7Z$5QZi(Qx2oa5IqcVc?DfF6{BfdKoI1Pw_vaT`u}pv!0)eHL9u=h0R45Zy%g(Pi`) zy^Vf~D?$pkyq`vandqTe8#7vI(HPWatb2%cmzW_7KP=-n@JapQg)rx}x|*MKLvo2lqOryW@J9WG5POuZy5BE$GX*w@;uiNB8#gecU^s&-bGn`}rpB9n$BU(S!Z`5ciJg^TX)M ze!hx($MpGXbZ$SN$9;xQUXE7WgbJ{2M|Mc#6)!6;O z2B6&mVU1!|a1 z2v7<6A02WwuvZHOoCqgJ`8Wv-ofmC-k*C8qFi#xC2_6?{Uw#b#PI+dMkYAI~+jis@ z9fB)vH&#YL%?0^4@+Q^%13uo1GZ@Ji*)9b=Q$n_Brw+3QY!!wP8qYzVJ49D?Au+N1vebD<@x)6d;CXAan+9a-R zHAqbk@L3Bu%x0Wi%EtivVc0;Wv-Jd5g7hE;Szze4C;$BKlb3xLGmr&pWNffl(a$~p zc?~}`s8vIsw#P$AzSJG&l=@^Llu{lUeo{h9O?w8Jw=>S`a^jYbNi|ZU&qTg=GGLgB z5AcWPcKJF+BYW<8KM7CijE)j82ihtzgAm!B|JhNy68#x>Aghr)UJa<=|5F8>^E4ah z5Bh46k`sT6=lHVC=rZe-!6$fDe2ZqYveiOz(bJh2pyt2GDSX^NLZ|Sn%>!=RqJN2x z_qS-l^pXt&wkLrd2wUEM_y^I?5>JNGZwZqv;__S~QF9c+BC6efYaA~b2%8#TV`Fd) z37qdY`2Eu(V&i+aJ2lPry_-Duzf1VsKdj>rr}S@BM?w|gzd|00aqEcN5|Rx;DZ$|N z!#e7k7=I2Sh`O7F`ODkG{N*q3U{2xN@$S`QpC#YbcWA)&8D~-Ef4URaJG{2_*@M3- zAiSese0~NWWVUkVn9oai#~!}=4DJFSl)jO%?om>>opFUpqt0bXYs9wEu#7)svgOCf7AhsdDHDqv zKTs0lFE$uO!y3pL0O0eeL&9ouwWYM0EqF7KOqI5@`%S6Sb!6-{_RA(Npi1pI(t!}J zQJf$HQ~Fj0(&7`kD;gnVJ#}@9b~jP#PuKH@%hJ0L?>@Z&&4HWxo#zF+_n4T|VR_mg zrJ@pXD-P`Ci;_4sS{LB@RXZumlb*a@(#eExuuHaF+Alh3Fs~v@??xl0|5vT_Z?xDI zK*fA~wSGP%1hAM|R9!TUAo{9x-V8yW$(M0C@{oV~} zK6@s`sjR-oldsDVHb+|fD)C2J6Y+&5D0Nc(Zd}e^EmH_CoQA_c77~FjI)UmFGL1n& z2cWwNWF_27cy@pOWWHJgv=JZ0VV3k`Pxbe12?@X~e`R(4!p5TOHH@_+fEJA4sGrw! z6gJhYL`Yg39n=g@{rpekDM7ES&P*->@G6j%zjk^BkZcSH#=&99(l+_@*G}rZum+R-U{=>`lF>ZyfpZ3;e zjp7#b-kif3{T^??H)wL@G|4t;vUHkUw>FXa=Q*H(g+i(8e~%s!-e-lZjd7p%n8Okt ztXh&(pU`kB8{U0F!>o5_8;;kw<#V$~+0o?X`sZ3q{2mup`(r7tKL*>IuAjUNe=6ZW zz95FgRIv)elVzk+8Fetsk9k(7<9QN!nsTA!Ahpm~M{FY0TOX5Z)Xm%6-zSYn!gSKd zl-;Ew^P8X(bYxo2o=%U8qZwwHs2g)`a`gx=9wzK|gk5IbC1pMayP`Q?VUrM>xL9ra zy!iKP#U|T>Q*91TtT`CNiYO8I(auMLwnt+^Oa;3w8Z^>6KGA`-&&=9Bgo};i=P+P9 z3cd4pv{O2m{=ya+tPmOG`k~J$BDD!JGab?`k3TDeG#sVEA_0;=kGgp85OZ9bJhCX7 zXgYY%`W1!TL+hB6{%efr&gYmbPc&fqBOOt1Kjp}y1096z$T4R;h~F`XrPFAn5)IdW zNJ*hO1b5Q+fxk*fY4U!>ZPz;z(TgzH;fIk6 zvp@n4DE!;D7@rxCVIJ1K8q~b>O`B%B2Ccv^CusL#w$<3TO|X z2<^|P*n25~zIr!6Jv2oK6Z4-@scdI4njae${?APUaP^{q(EdOxLkiQwg=}Ga_{LZi zp<-FA$A6AFv+Z<-qTS0wK=g|KQNUS{bxXgysymnmGZ<*mGMGO)gUMbPjG2sS|FxNn z>^F}DT7KHJWS>0ttbOe7ow27yBdY>~ItV)D!*!x`lRLG6Qar_uY4fnIO*US(wQJW= zu=+{InAfg=*p&EZw{zOj0NU4}ZSV;T5kS^o-ie*g4->b$65k5E=R5K2=_#vq!cL9Y zO};7S_EIO@Y!E|7z%MhtYuxX!RB(AB|8ZJ0`K=N#J6eX9Y3Nebj? z-!=pO3ToG-dw=nK0Po?GQI5VZ_#uzMyF?we^HFNQrtu+tg>4C)Lkf`RRG7uD5ej_I z@q5V|3t%*>vAe#7(wIx?{jfs6{EXPkbw^E$QARt?;DxD$o~xq8xm(*{U(?nM^}Jr* zRPS2WTjTD1BkMKYz4?}{_Od38%~lVJUP_~*4Rv7A+}YU6;oX?)fN8+f&Nm`s|SCTKQ;#O2iZzEucwCQZO-eahmjDyqz^!|rcXP&9YBml zXN9L)&rswuQZ^+IGgX;YZdCo|1q6jScJQf%b?(4(8FfO+x5h=0{I)S&?Uv3Qlc>nnLsQbFnL4V)vVUH>v zi%zaKAH?zg>wdp~*yyEq;PyiG4n!x5jXO~^INpD;2cYM%>|ojGU}_G-s&h1A_V#<- z*V-@*jj{%JKp+dzbE&zD83K6A{;up44$0}O;3=%^#}#l9Ps5PhtLAnZR`&Zi&T*W) z5E1<|L&~wndFA0~K|UAb1Tc13+QqoCJS~TjHV~;a#pfE2>y{a9SQq53uGgM+LIg{6 z)Kz5fM#4l55hA#AX_wGF-~8Kp&i`#a7yq`N%YR$XRZGvT6OIf9{ZMbQ+}}w?8}=BZ~twhz4P~M9}uHP_;pt9_cbOB999+wb|zE`=^SFEN0q$;T7hZj zU8WRafSgD9ZUr)taWf12{zZiH@}G*t81>pWE&De{uPg#Lcv04WgOQ6?>faLnSoYtL z%6Ney%Y8!2o5LIQjC#Yyy9-?Bo{A|{90j6LsT#pEveN7``fGW5dc32Gd%e;n^4hAIKbltIcO&7P`E^g5AvOx#z=GElT zq|>Dj(?js_rti*Z?x5Iob>A`?dLWisUv&dIHD287%=8A456Q>6aUS}8@bwFaNP0`B zST@x?!!?G1hbP1CFcS|qQwTR+#7WKByG!|X*?-)OpB@BOn&OTFcF#7MH=4|`iOzqn z_&7)?q`{~+r~}%!fL7W&@(m?rU8$l;fzSPq_4BN=MrUW(*}l${g`MGTpY6y18bdQu z`0@7x09NOl1OoY*cg$C1(1Ey(V>n8d$t=mi2RzYwPaT1=gFuc5Of3S?+iIH;I7b9T zHxz6Gf^m#X3>H~b*Vn7OOr`_vnT7Pg$+ThyiSDv!LJ&1YgI zcKB6I_R(C!=@sZ7H8$)oL=p zij%I%nG5IzkV$e1Z{*=_ z{HG`L8wUW5@9;q7Z4CPCve5TNC5$6vO$ZXwCh%wk{u^pIkA65F03RFxA2on+zxAQK zwVj;zFR0A5p4_3;&vlWpji%UW&*GYRxp*Ylc;`j6o2eKTe z*_MW}H6IsS%T*39K*kw|If97Q15ZoSNa;d6Tu+}0KpW5+MW$+SY19?-I`ilO?*ntE z&h4&|yy`Ru??68A&1ga{j~~^X&Fua0s=z42$NE*<1u}i)qP<|nMB}k+!J~l#?YwHY z!#5!G?;cf~K}UNfI$I9ym(y}6WAzME`W%nd*_GR9@tFX1=vzozt-hz9!V4M3^{TdR z_36VMA#SUzCxBwco3sK=i{gC%M-Sd~ae0D!S(5>1|mH5)Y3_`c6I_!Y0SMI`wAlq{-6fg!0=BNvLfe9=ALgd^ICV-<<1JzA?A3$0D%gO47Yw0l&+aVM*SPpd#XFsgJx~u z6lP;Qk$YB@+IV60W+%R~ODxlRD@cVjg!9BOSM@FcC&CRQ7tH7{K7Y)Vx>`Y$+G<_uT1;mE zJFz|C>_qm@xQvYCV||8L)Z!J|iEn#qNpo&2a^hl;xEK z1bt>&?d-V2te~+o0_sY8uPdh3(Co;V*v_ybF>` zkkR4fynG^^HGO9Nx;MUkY$cz$_LcM;6k6fHEq{b?@c{?l@k>k@zEdnwql6(>DEKig@at0_4EKM8y?W0|_} zNyqm(SXn~Ld5&9S;~TNUJxUjIA4GnfjX)T?RZlk^+t5oDn1;35_;`X7DCMkp?HOBe`TN0zD z=u47-7f8^59o0doC2j%^YN%#-U2DzRoXos9K{{CcwU@Z2AH%J@>s~`o6!gS5iG_uw z!`VQGi4v*&$O{dgkj?nC4VWb6Hbe`4}+QTM}$z zcIwz;nvS{kxq?OM?hCH=hv$%#l~_@$~3GFTh36h;U=qqk6z!!Cy-yQ!lbm zyPYdJ4)!qfR?2PRxVkC^gXTFl&$)Sp&3ATmn16-Y!!`OxYpA0zG`AEHgKVVc za~B96sYf_q%HDx z-cwDelIER|0`0nIgIk(Pw{$0KeplG2j!+iW7#FL-O}(1-CeXVY5V$SvArR*>q0#QX ztsU8#QZv<8>hCc0K$uz%&SSBdhDH{kI;6dgj5hlq4Uxhw+q71~>lf$i1iJ(~44!P9 zG3r3vhLqhp6%_rzlq?xFi<)cI+C99|Y1`E^0wLJ6)~bjnWFJMpw8oG+hiU5@wadc# z{7+~FSdCb5a#9z9v;xyWXdJ?jlt`7;M2NJoNYf2@Sy92YC@?2G4`$LPAx!$Y*4-d$ zPFJmmD3M=ChUpU(COfEW|5Y6gu(TNqQ&q(}SEePrEfbNVT5W@cghC=8kC}x|&zP2r*h$lxwg0R)>kiJT|M3o`p0;!#WNg}g*bJtxf<;l_c#so#QN=RLtceUR#Y3F(@@)kZLfGY6c%v~dB)-X zPIE97ADH56U-6+SE_}sDrue~E{MrKO;iy^+g)L4txq*SN8lxV$@4F-qDb9il);2y^gE>x zpjTQ5xpcSnV}oR5*gOpf-I}^roff0O(U$bzX{dvP(J!e}g|4Tm6(#UwxPtcvSwnhg z_u|VS7q)5N`1z6&k_$!iSYRKm?5|v?o4JEFzQ3p>RSpDIqD0L|5ziImm4XMofctB6 zI13yOCOw97F`S89t+Te(N)Z5yVQybz;Kl$pICK52Ma2qtM_$z<$uQ3HBhJ+GLPrOo z=qPpydT+qQ0X#D>(68;;@d4Y|q8mCPw8QK73_Qv6*06o)KR;RBXkmN5qux!?(iYv< ziV|BiL8pR&J@`DpZZC9;$J+04P4_sj{hri*Pf;X8{6m(iVue`Sa4V~l_)DDehW2{j z+47vLKy)>cQt8EczI=&+h6J` z@`2&Xw6R+H%o%(rOB|LR1+}%huWQ={`RJ{9(5yHXXM0=muvzikTk)t_apJA`x><4R zt$5t5IICB@02&=cJ=)c5#fz4T-Fn5jx8h+FV(P7U)U0^f0EJ)#&%C-tX1ge(SXQ7-vIaMr*SwP&9?yZdYqOQOyKS>E$ap zpv3H^cS~+=Zk0!@qCNQ@pvuz|Xdh|4CZwkI*3_ztnro7F8O3=bS0~qL=Mw$+t^Y~$ zIpVqz+)<~Vl6&9S2;*2{%*Q=#^wO_QPvb7HiabtF;{H*;cd#E0a#XeuU}KOgJ;?P# zoHk}l>sGzpAV8n#@IUQ)0`l_}aijKNt4K%PK`Om}X@d0C^GSTzb5=Fpp&mtLl%Op0 zmvqI5a_<-#{!hF5wq^;2rmW2;gPdhqz8v#Q*Ci^N$4#q7qc%y8&+wY*vRVJ0^jlr$ z%Ig~<+?cTb$at!-3W>X*d8wK3j6LApji=MBe~E?w{5N7%^AcaeT(X70{F22>rx<@h zKR@C9j`t0q-!7zKwwKZVU+SQmcIVG@hnbI3+d%_z9M=&EHHdRbn|d0MM5=1%;t82w zljGtxMNfBm>6;GMIwNmKB`xURCh2PS97`}}E%{3pS59$NFTQn(Z|lW(PVrs6_`xYQ zw~61PN!KNT8;euXirnmU_4`_N;R=nhBeK)DcLZ+`IWp1dY0m`2(97rKc$5tS{1%>^ z>_26D$K+xE4vmwoT(AJaq3b=m^~C$Q8+-%Y3^S{alUdLyR#iUXaJ}!)n$#AvHeU-T z$!;?XC3mrb{D&+OFYJRic$jvBNk&W_J+M^;PT1Q&taLz~ayT!Ixsm`O2j|Gay5}1& zy)gZ?ZTR$rb|?Emw?21A&x-q1U~8Jt9hZR_?WFJKwUCaT8TF@E{zdJba7J(KtfFJT z>dp38eADU<0&?9Qjf*IOjEsB3h1UDbi1y>`O^nJMxZMa{1 zyq!80@2vKFfkT;@7pzRLZe)qXh)(-g4uSk`^r%owdxcg+F9V&(V<-|v+Cir_XKsr8 zfXRUsW8-BNOKL&E;vp-?_ghJxuP}rZgel09ajqjfDY~GE?c1~!v>rm{b0vZJ2BlU{ zpjX^?O`D4rzOY!%95T(F`c0=e+lHn5 z{LIwEX4{xbz*ZB%j)oc0E9QS`iO}Dt+C9u}qx7n4US(GpLjoM&m*eaKqfHc62wpZQ z0fz^KP+)HdN0B9oRwovA4)i(FJoB2-3f3DhVr=q*&DAf*+KYtKZku zBH7TD!ZnNJv@6NHe~3?ANep6U+wIbj+0fX^WXOb0auO%X(B;a@QQN^8(%zJ?jXlS%V$9+lZOeWNx1VG12WUcp zZL%G^UcZazt*LU{0$5YTCb;iL^BNT&YgFW@dk~9og#)UT%JU!9E;H#>fOS{!fP8ZT z9|3;yY0`@roFN8h?HHV?x@s`jk>8}|i{`vS*3j)QN4an$REyIAewV+b#<4?v%sD9{SKr@b*!VRvk^ zpL2qvWsv+w^&el)anHO8`qA7jwpC-XDGK6(MG`3rsAK)plm7Dh7(av=pV|1qMyQHUrqW6#qK-fP>&(zl~QWxDqh)l^^*}06T-2aL0pkMz6N;n zXhkw1jmIU}GZG$4fpq=?o_4tJD&OI0*r(xizDzn%D)&DagxLd;DWu4Zgt$C_JIs`~ zEhTq@&infg-Z93VKV5fBDhSXZWUbx3P8dDbAU{1F964GuzwX4Y^t+m?psTO>*Q;1s zC%p>t$Z8KazOBC-Wv#=111H(+>FEt9^qCy|@Tk{{ZKU)GC&ilG_P}AN90Y2il*l>u zNK;<{I`p9gTh}~N#DwT!8F1SMQx*8hE0dhw3Tpj~k6fcpm0hG+TFjFvJ|KrTatn5Tqhp;y@NVFtmY!!wvYMyeV)gda4q2QJjpl)oHRKTpGRGw0yu%*sw zA3nT#_3Gott+d#hCf9kQj&;ej6TRsPXMUWg#9msP?(HW+pkM1aP#96v1Hu68h@n}N zkY|MAjM7;z;Uiu$3JhcRy2i)`9hmG=;z$J`2b$Ox8J(;bbQvzi=sim-pGd7ORP8g! zUXk?fYs}w=ck^Na?0usoEPezTZ6)op?Hm>#z8;k*%OPX!>+T3_gk6yDM~CX0`Ss?t zW!lnnp0&!aqf~npV=iMHNFao~0cZN+6WL&%+>fWpBwdadThnxwmL^5x*6;2+yS&ZO zX;1uugNr?-g`668i6@GalY`Li;aO;hRW;V46hySfT*cpg?{C&y>L{$C`qG0_n39;i zefSr?yy@d#q6f1D<#npn*Yg|9U!xSw*LDFM^QU9;sn_lEw_e|VBtC|ZUGecif7}-z zkND#o@lAKr>lx1!wbgFx#pkXPVd`*K$q<59q*;}0mCsH2WDAD~o`&&v5kMLU^R~8+ z47K37)E@R}ZIRV_ZESJuA*;l<`hSkATd5!4DD2f4YgZg0aKVVg)R(9#1O+MI;mJO$ zRRTN*)UA~Ab;_=O_Uwnuo)nYub+T0?H&v2NIG)EipCuiKnGH>IzTOo)Bt~f7GC}@d z(MDRDY;#0FNPFG^WXHn|YtydqEQD_MNg}N$#aUmT;BRX}OiEb2K!?ujY_nu~YzDyv zn85-(INa9z1MT*;a2i+j%O$4U73L3;gcJK z#{ZBQ=?GpaT!yZ&QUOg2b-|5Gupx|J?CZX9j(BtEXy!UDu9D=I=q!bAUTjRZ z6#j4}jpfdHBVzxxDP7^@CYG-FUcq-aR6^njh>&okdGn-zrDh&k1N5_qOyej~f^&;! zd(CXO#D?5AiZQi9aVW;ma1RcLZoM}>50NV=GzQqU z^)rvx66o8?aN9RGH+LRg_0rxKrL<<@Phle6j)jqw7a3WxufZGE)D9ahMLSkKg!P-c zyw&VPB`e1+e($2|+N5Pa5HSBBHqZ6cJ;;RTo~~meEWB%gX7rC ziR*$derPkjLhZgIva^TwezN@<;mV}B_78p~qsScHZD!MT$-_cc_|%ylAm zWv}xV_0t8PbTSjJiY|ma`R1Y8BX<7ugdV7tc}D`X!EdDKR#2|CCW}=8VXYf=GpuXl zUZ;5L779*kW@vZh641oTFf^0Y_@6|bpc*A1r<5{Ryx*qgA{5zdi)IntCM zs)=h&kzX;|w~cOHQgrLZYNF2R3R*foUto}Hab(C~emGK7iMwQBL*a1d_7_1w_!Gw?2!NE zOM*ED`cZTAV<8sxWjn<$a5d@(QVY@O%HPQ5=+$ORWWJg9@#47YXagE%r`=(g%6RH~ z@SPHx#3_3?>bThE4y+YhTq-UGidSZ&bB}{GdXLXHqFya$g!>vn)Mo*KOFrtgKY)7l z2GXFH%#m5p5UC?Vay;UjzT7GTOOIR?uA58ds1oSo#Mw>tQ>!3}6FS~^@s_R&UF8_5 z2$HX1yw07S9N!8j6p+=tmi9)-#n$Y>o#?={5d10+Ij$H3BSwYS(1{&T<`KK%h&_a` zyQt^Tzmyt?{w4!NRX0F*E}z*=V3FLap>(3IxVl-}J3c(Uckpmn?7AV<#o?RgLN0X* z8yzJW_&g6WvMA)P(#-N6@=uYZq6`LJ2R_26Bk{a_L1x)r)c9t!Qe``VT!6o{ z!0eYQ|TOP|0fjlh7EdTCaq(`0L`q{QmFGw5Wit=qInmHM=0lI|EVzr$p9D2_Z3 zg-o$JMBx$BQC3^nrOb5@lXiPax~yAq3fz1eXHM=rtD0q2BjYNlh() zi;&BwdV<4@29dAo=JVP7&3e_jT;> z-^D~3`md?^Yi|C!Fn`U=Ux|HvDz)d5b5o%GhWMR=xZoVUuw?xKL*4QNBZ&AObSWS}j@dGhNo%i$GLz+t!x(4v_dWbU)~H1QJf^a?jikC3Q5o*6 z`FRHN5)dSPITeN|&zV^?d>}iAyKZhoC+Didn|)!a@t_;k6h0Rkh2Ked7$8(=JRa+g zwJwb4G=%3>)hTeU^|XvSX_+iRNmCMcvdSoS{wnVXY9u<(?mhk(vV9YBI}p4f|4R-! zcCbenfE;^=N4tq2RKf3jQVJV{TazyVh9U^IE$ES(KGEgER<&%-3QmR2vEezkb5@N_ z$S|~L%#5>s@W1@JfKN=g6`ylf5Xyy;NIl}bhNp))h51#;R=)4fiBMo{$5l^r|79^O zbmo<);91}=QNeif_oyHOqxdxTB9i55Y6?zVVoC~r6>ns>o!DM~AMXFBtG~-5hlbz_ z_sBS@BVE+%n~MeM2dzC!;7|f_12vn~>J6(Jb2DdNITvBV@H=81V5wjZSSSPkg#$F) zS0nLlAnNkHh%q!C9B4lb_;vUuWbI;#^f*+*+K*6fT)?r^Y|%PJL*_vZ{M<);u+a#sp>}D4 zr-sIIZfoE11vsJ56BgReNj6?4r(2kw_?&I7oma13bzECF&kWw&5f~E`9H+^6+p!?O zq5aq-;CjH=H8$-P*_f9twh*)~7A$lZ5I3TPOMY6c)n48Zpn8+y=6xO$4}fyS%pQhPb<#S_|_;i@Iz4Q->HRgL@)t4(!U*Bdxq>#z1UI`hfG1ws4OMuWjAuQGG7R6H!tR}1~9NFc9d zT;MqH)x~m%w@e7z-dnViB{jxy{HX{CYu5G8tCQ|1HXe9)w8kAz)_MpXmqJpX22C-q zlh7ETHBj_KNwt4~)Mab>cv1&dBB;bvS)$-DxA+I_T$nm5ac4)ZTJ$j#VVl~;mVx?g z?<_WDLiZ6|DD*8IYsf??o9O#YzuS0QNPU ztJbPn9qO&D`gcyKWOV>dc@m%5NIN1g&7v`&uSMua!46ZLmF=E*i{&i0)WRAHPN$0* z-dGnLZGduy>V1@Rzjr3%4D3)9Ui~)IkX2Q(eM&)B0^Hl+{?U(QFJ{bPuk zO##cT2sigzUIuGCy}Y#%>@bGZ-~io|M*N-=zU+%H7))xwZX)OQz`R)B-NC*EfpK!2 zZXW4N-+mbrDzELAb3#RY!B{qfHwX3$2CW&0IEqs}w8Sk*ZOMgOa$!p_vdw@z_eSi9 z=hU6sx+&EaBe- z({tCmt~T$wVz_wdRlj)q*Qrp4zV1dk>Zb|&UB!o*y{jP^dgf8$20H_lA?LIMA_#+5 zl4w=RMz&uF+MJ->)XWmRDHcCZ#GFOWn|OUn)u(Ks3w{U3;)>ktBC`$W1Q+lY9c;T? zfwix3m6`hZ?FJxl5~63DW6=ys8yJH`-$5l321|{3uz9F!ukhji6iMSGjNPS%?nlMna?#F0PrTQ zYTgUa;{vZ*>87B1dityks&G`%g{i21Y42MTY72L(^KdlBszZ8V?c9UXJ-$wDMwpD_ zGrnKuf8#M8Wz9>8MX-v-7@xB={m%kBkEJ`0=-+$z#$&Evi=)T1kB<059g}()h-)OT zOsH*)N&n*In`z*RP}rbs#|~A0`-OMzz3@(8QgWRz&v9l`vtRIqH~$+~boKZm%0c7Z#$-%71O?}#?P1DVji?FK!yH|TfkmyX_DdFyr-2=2I)LMPQ> zf?yCiZte+q?>6SiI)bgZE_l@Bl*Y#da`zbuClm?;n$X>1j`7~*U6^+y^}DN7w7{f8sjCP* zcw{W=QywV`RMaX=!|QgA#rD3hZen(VDo1loqgBn9kVU{E*l(HRTt{YjAFSBiRw)q$ z*~IrD51P0MC*8innw-xmiK}RZGcdO^z{c|xY8NX`@Si&b zX<49r4ol*LsF2h#Mng01g^w6lzK@p;3f1ljY>80@Kt7P)lFQx&v9~m6>a*+u68vek zFAr%QLxS6VL6rjXhVAV`?3bRnGux0dczR0d^pslxt0l5UU#cqd5QxtZ6Mg@fK^CHL zaBx8Hfz1owg0gkcz_TC|7h=Zv6qD)6xfe^b+O=4_IA3WYSrc{B7E&-@7&2%XRI{3= zb#e44wj7Vjaw8HvdWusDN!@U?zxV0wJSn=j0+jz+f))Qx?>`%mM8oWZaa|Fg>9$*;qZD}|#5knfqY)5b*V!D>bd140ZtZqe@j&X^P8$8ZbRy<6R zUt7IT5V>L4sA|pyI5?XquDQ_*9?mrndxjtadeZ%6)^4mB-I4Tr%sMLC1GnjkASkJ}olz-|z=Ocaia>50Wvt~F zooq{U4SeOiu$X(9U|Z}7bi*~YA!?+^bMB6KrQt14y(Wgn+3rT7zVHc7HxW!f)l_8s zq3}0yY04X8gK6M!E#o+wGts5+P`vbUk=P_MZTaA-TPjvy`^Ay(=K1LfMSuPCD9G4y zVk69f=>rnY+K~1O4ctA^CtfR!aUEgjkHlgod`GUlZ0T1Kgkf3<(Qf$k#ViCl zT82{X^-=0SBTm}3$61s(E|EUFKn|0EMskkBFfC12V5JRMVZ-MKVMv<86$;`^=zNE7 zga>S!30IgFKbB6>rT@z91<8ELl34&+ z+3qK`?(c2UeXaXzTa?yXm43|3PI65aCg!(H2RWI+pUwd5en|M|t-jNsw_MIp9{fFD z)gY%e1OAO23+N`k&3M3ya~24;ts5Go+wiH+8?^qmBPEA)@0$ys4bxLm&$@64N<0Ru zZ7C6kHmsCK6F?%?LPRVBVQwW0mc5s3(?qmcq{7Q@eA(uu7dw{?U30j##Xjm=c(?VU z&)Y0Lze^pJUubY#$DD(#X~{TJ{RJ=j{HIdD=N-kdMI#IQmH zDjh0?fTz?LUUd{Qit{)h6=*dhlMVQ(nfC%!Y@NoUxdW1CrefFic?~34N8#aZA6#$i zuGu-cVK=^~WC-Kb3(|F-!keYiD(ik)XxCkhuqNqj&GYt5VP_1mhwjM!~Nn@3c~#FNQlV8i+jWfsGGZS^3Ve@~=3FZ<-+p?1kX!tarkP_my@Z;Q7G9q5V@& zLEJw))PA-m=gSysz{Bc~>@}b?si90;kq-)gMFxH2ayMxjE6vS_g|1w=umqa6 z8PxqVT$ee3rBiR0V-Ras=LCbh=8d;=Abv4KPf%bOL>?K&bQ|sU2FbAeknJXr9;hg- z(>Opjp|UhKYpC#N4;8GBnyyj#0LTirFIBknFhygeC61Irs{eImV}2DniakKO_jYAC z~L2 z^=a!(fQa)<%1PAV&->%{Y!$tG%&%DL*MGWE+4s(<>9jO8i@s+CXuQ;foSya%Yzj4} zpm)?#aNP2}tK-K($9KMt3&8Ew;~NOh`)YD%j3VZHmp^ML7t?mOgYva}nZoSV(8wy% zZ+gzzX9jCqA5br>l)^t{z6HanX$xm)_)5Y1Yk?m{2NtF(mp#}&A&kC;Kku(RgZqNB z*w#TBw93_w7y-}I_D`91*t#du|566i zOuJdaxH6qjuOapUi-n|2ece;AbGH4}Q8H0`VX4X&%_I@37F_8nxcpFjj9ok{Ri|y-M4Wqffla}%Q)3%pn-S(2aw8#ooVLbJyMEtmN1pf}kr7+v^ zc6!Lld^YO9ADuyF06tFIbTMew`7h2>Q+XD+n`CK)&(5?!tXdYWrwvNpjFr*q1(8c_ zN~fQ?;BD3~oceWn*{px()K`Rfp}yK#f?qOyT&dcmsc3OA4NP7Cq>IY8dLy0#|q&H{AT3{>ek&QKKM<2m6?72< z2sM}c7y!6bnl-+Jdj%&nREZph+&;&&3;AdIIa; zO<)^4+~wgB-_+sX>=`jfBr#_Pt3vwstF&hlzuypJ2j&X#M#@kc-K`p3Kirgi`vc~b zA#1>fR)~>WaJPX)D9YsuI}}JAH6ZVA#IAoaFUm}DRK$tu_YNJSkRojVyzJ3C#X{m* zA&PSj)wdd9%{kN@tTsXRSz}qptSYsmmc1w`ZVUQ$8;>Q56GZVM zT98mcL~loD=2W3X<88PRuHJQfAXy%{dL~f5Yvb@GbNC%`_#JaNZ;M#8e0o~Gsub>F zG^4%_>r#L|A0Zb_V>{^Rv$lS(x!=#I-_N*TCvEEW)6+F~Jgv!@bCNTM9yxRVf}A-= zIWqGhl6FtEWzJ!T#ED(@rs^D>!S~bN=so@o+g)c|6iJRRgD=KP&!290Lbb(t zzLGt3;^|h)Fa)W#-^psRNG59cBVlHKYr*{U3LkF4UM6w}an&kQ74_Xa);x!ruda>) z{564JaDRa;n&3Y}aD2H`D(Zjsy8Qz>?WjeUU_PEqFFa#YQk5sK(Gq=8LVkt=co`Sv z$lg^n$5sWx3u)Kr6lHSViarXq;WW7zSBsLEc&_N&nN7A<*B!cHy6}*#)H;n?PqP4` zUzduB;4jc&E)>}zG=WEx$Xc(hX9V@|453dBw=z?kQmEuWogE8qmn_V21eh;TqF$DQDYURA4<#oiiBN9B`qk z{Xhwr(GQBQFno+Dg#L(wlJ&G7;PC=#Xrl04%146R3P$SW?M0k!`h8&8+wr2}M5(uU z6ij`G7Inn92*T#3dZj)iv|C8JhnH)#!BT}(jdU)qyx4v(WjrP&pH*PDtP{Xq->uk! zgsHOhQQV%IQgLcZ&8cbDE+@V!f`G0#=N2-sO%$ZwGlD*1g|9vv=V|!>{zN)MDZOdC z`)BdFpTQ~400}Zww{ys1jOw>>i_e8xO)16nO$knps0ECjpog?OQ^iPB63gg%%`yTg zg91phd|_74e#@*bJ#^NDV!e=a8PP$&>{~lK^!O)Seb~*&{q=a*%9lgz+4=qPXW@=qqO3) zqE60aCu90HiRU(o9rSe;jfK)P8C6p0nc%Z(lv2+ZX*OOg9{&E%AwH@SxSvct^KXM# zGKf<0XR2gWr(AK47!YC@M3hv5VR{ln+OqW2`gS<2ZvcT;jf3LE+x8|Afk-vUfSO9aiTZfs~*R}|7>Hy{+UOxo2var)#g?t zxqaH-R|B5+;6VU3x!xF$>&@}FUWdnf+=bPK6ygR%HqvtsaEUiK^p}ctW^CoZ+8miH zFEXa`exq_xt1MzY6XHWNk{o#90z$T<-6w_9y@K+vM$94Of(tBOGO#j{+*IR55g0#{ z&j0Ata-m+5_lXub6>cVs2nJ|?Q}GF;2+Sa3r<}tynSi2~xON!=bJL$@_K&gO>Ba)M^eTHE=_>iT0UNOfWZNQ$+2nah>iT$$5aC z9?ojDX?TxZox-^9d-wcm>~f7NSEyM-pkuX?L%I>EA;)6PGRq3YmEEI>LWb8^`wR!D z2|d2B5UIsOFJZdAZDn;9hpT>J#bJrs2B$!H=uI&~ApXk=K%X{fu@8y%wSCBeT;GS9 zg`c1V=w)eDf&GnCf!wG9qyuPgG}@&rxY^PHsiDV$Re?&X0#&;zFjuO;DxMovfOG)f z{kaok7g_1P&akg>G!d%6#b_>7fs6I3z&~&bSQRjx@Z_hc9sJa;^Wz8$Z4Ju-GOwKr zeBEUUh+O&OrjRN>q=kX>c{?Q0>eZw-V@0A*>*DLp+D+^2zWit0Rvc-oW~r|S%%yl~ z{aUFQm7q{?hp>hny4Enf7ia(|Ko*AjLIR5aV4jvQ@ppGaYi-TmihU^#N&~A(eVU<_ zZ=$rk{JZVtpfNK1yzjBA+JU`FUPG)(L#bL6rBdIx)2LJ7M&lp(fwl&l9)@C;@|WjC zTM69@kJ8!+H@ZYP`puUJ-?x}TfOYXA2)3qk`>cy)Lt(rzYXy}p0JDoa37q}D6X{p5 z5YQ4n{qp5se*F4}?>kZF+iHBd)qT~4+%Z(241yg@X+EyZXEyE4#`*aeGGs#W#1>Ci zaJ;9_7j(`IO(8uU!Lo&IGD}LUjbP!eErfeF`Z%jYgCg76UA_2vqU0o%EgNNu7kFq> zeHeEtFTM4kT$1VaZEtsOwcgxxZv?zP=ek622T!#NuIj4-Nd zBo=W55RJq*^{gK%jPqhuWz&HgsXAZ{TvC>bh9xC0dQT5s^KfI7*~w|hW}`M7Aj=6W zexQqLMG{8(?V8ICvPj$r&463}rNXyB=*r@{bzsSjDCs*TxhUCpN(xbO;FOf238{M`vfjQkjCpmepjW0iV_o>ZD_JDokfiL2N zpO=7-t@T`Tj{8+hJ(sxWt@T_|PdxSfRCrh6X#7^1?A4ylXF)6;BFo@m>GZmIh5 zw^WVx)d4s=iTC#VBdbmi9FV6O=8UgH9shTro}4H|6=;H~BjQ%b=AHxUUaxLVya>Rc zXgv|rVXA#)ci8BXXve~28wBOhA(!d!rtkGu!dm}ZbFQhKd(rED0(MWIgkAAZCx@{$ zQt(hQ!R3E@tpDqi#`@S`aNcCiu6okd5hW_{U47b1Lt-l6&U7cR3uw8>S4t^Q;4l3tH=nqc1GR$G zW~>z}dhGW*;C__Rh0OJMas>w`zMp|Z41`sw*G6ObSN}S%)~sKFKPCQmsn~+eP}Og6 zCzJ;Wper~tYewAxI{}$=+0RIJ@i0m@> zIBw0=F5FY6_G)1ZALk=2wQC#6*1x`F_nq}`P48s)bv6f3b5UV7TSfM2ffoP>{+l%> z@F{K)b)z(zl&=s10QiGj`fD``E~=fy{T{14!Q*+l$J$1xTj}9M^8~1tUrl?dEwaR(~CW-hvN!U;I^e*8Pff zfXA{t(TTJ+;;-iV)g`{FOI(1J`jwd*gq03Rv?|h^oY%Yr{Q?O)%?DI*&VP3o?uF6w zw$bUf(O(6M8%?LRF&MOPc#3`S@V=#3+jKczK!PGp+uXp9bNwEan-@@koi-K)sz()_Ls8|#0>v|0T*p-q<1^hh@eFd4<8H`{IH4Z0ot}$d z7vMfBEEb@XsANpE8{wH%z798qF+Uj1k*I2nHULq1HwM8n9Gv0x8r=NeC41^X=1h4} zYO0tK%G~Qq=#nhuDSivd$W*E*PRn4XEZ+P-!+)aX5&ZaSf!8?z7Ul}x1v`nI{VD{9 z#BGErVnOknNTRnGkcw6+kP@t7B%?}L=}K6!;R#khsdfR-fxl+W^m#P^^)<{MT;b+X z>zFKU0kK4Q3AtKp|D{JPijWbvz_g>2tI>nZxi~c#>6Ykut8!S;er{?#_!;WJJo?y^ zo~PWrIEOdnspvtpwnM%7#7e6G7b&=ai}n(uy}aWe;K0BKzh4OLDxB)NUN7lN;~;R} zgA7>=5T2#d%moCO%HU-mbeB~K7<9`s+pC~MD=IYV`@F`8;JN@5dja3ZDo2$4Kppb$ zHrd#NCUrQ0&CQTvTS%E6n%EX{R^Rf#(LT?2D18cT2>Y!*-SAWtkVP2YafZ_u87erQ zlugxjsk&;U{{6A<^5wYVRexZz5Y~vB4al_<4~({6NFAy8Q8m_-FGu9~K5{m9q?|O3 zi#9jeu|VK=Ain4wI0n>Q99+a2yMuYlXe0Of&HD0hzO&}5mi_z8(xJ7WUs_pSOZTe&G&2<7##axF5x7B&LH0`^YI@r987fdLgL zDLnh3Dl%yomjGjx>Tv5Hm_-wX4QMzFLz{ zK@_+vw%cgY0s;C0ngxs&u1X5*&;r*b1$JqHSxJFCS|BVbuult2O9~v&0=LC!HXG(3 ziwkAU;=ZH>V-^ppz+b9BbDcs9gl!UqFTo7$(FCJV_%l>M5zPMYMY{-3cMzt;WcnbP9)A4EZIGs7=~B z+k6)mo0|)^h!d`$j8WLquS}-VCEJ4z7uQzMehnwNqt|m{tf1<4vTCrC;$TZgRmtYk z4pp5NTxnOYwCgnaO1pd{x8UK6>j9A0_bb@p50xfaJ$nEoiKyEV0ajdq-%~Byve;t52A{+^KP_W*>Ap6|ZRZifp zWCDz*<^ZX@cN|F3{HkB?0Pw<-B&=`Lq}e1fy&||!v-f(fY+XWj`eRNPP;!;oI`lpV zS-32?(&d7SAsW)D5XzQ0IT#W?C@sX{13bxPn@npYyoL~T4g4n^Zo%R`XI-J^lMgeZ zM$dIIc{2(L$r%b~ zMSZFA7(TUwX5C5iUe?8&-Kvc-h!=A_j8s*+sb6z169EI$@01*R!BqO7ey?aG8=b5! z+1RiDlK%QKQHF9v z$;&6k$>r`3U^3i%us(&BjA(l8gUQ}~xhn2gWk-;1hrd_>7`c$;0aF6CRRITp`_lHN zsnY!1L6pwgFN;)oqui7>DaxIIcw)-tByq)E$+?%RvzR+A zF>GC@spm|yx2ZOJ<5u`$;5RUE#!m*dbYBjByJ0u#?fsp-ohqGEUA)~z*I8CUe_%6( zRctiayqm3hv%TBgY3%Ioc-yUJXS?(@L+UYl)&Jr#io|Nx+k2ZqZG_9kWBYAGEuF31 zoDX{CEAPJ-5EN$ao#oj{!8K9!&Nu1d z|4rxYeEmS@N8g1I@KirEKs}_Oj$jaO2dmdRAnP6T_5N7Aa!XO`EuPZbwxz4B(gahd zueGq($a~$TUd(RtwIEsZX7lvEA_j%IiMB{*knKYl6@4td1Egl28}-FGf3r1=4M?ql zp6og>yB6dpwQM?jV$KWNvAD-@!Qfx#OQXYt3aeGHvYzm$Aa9OZrKNgp{A7 z#%(#`ISwRHX~LDYg#thQql)+z>Pfc^{;Mk64~=-&)ZXy#G%H(Wzst7}Hjl?UtgH0i zzp5qzdpqVSWB4Hh@9}$#8I?IfH#w|#tMzrqYHTLRo%{k^ZE|?X%4NTBcF2miouqnL zugXSzXkd3|r?vYFYZ_|9kDX1w+LfvXFs;+K9%E#nRI9nSyU$ATe%N{Pdp)ON@Dj&Q z!0IGsxVBY?0PNfi<5E}vFakwt`Qa=JHwyAH5(O9f-N+NR1rzBty)MpBu5el)?*idP@j%{1qp|Q< zz?XBhTnH*QkT0s#Jh~N56TVFQh)xR1v0vI;xZx<8Rd_Bh@+R>-G0!td!9l)?*E2n$ zM2>Pq-rY!VnGOAt(Ky0a#8-wGGX^m~Y=fZ+2NH0+!UBkW20;FV#|Bs}58*!l$fAL4H$_IzajL$0}qxfz~BoxqS|`4q}i z^G=2X=HP!akG(|@WeLysvj$KJW}oaVW>atNzbaedRt0SexO0kDmz9qz;lI(VoZ)2j zYxKGDFLD|=^9)Ea3AuxW@@BxD4(TY(VXT=>CR+@-TfQ?uCuBB%26_-kMj3SYBC^cp zW(M{PzKAaalCg+VUjZZE##U(Sg<|c3HxS;gTv!&c@4jT3g$I7$lFJ{W-6UOTE2gpW zc#d;hcI1I8xp^c!f^9t z+Q0azu?<^|lCjH$tem?hL#lLh?qTH}#bOZv8chSvKy)7q zfK;bbTE_D51H$`Dz#I>?!b@5zwK4D7=UvC>n5LqwXlPYS4nq(PGN|i3?ZU-Y-(#x} zXDc>sb`enCJ+T?C+zkHMtO~9O!==_LRn%LBA~q_nR>oJ0;gjtKs(yp25Pk}LfmB}T z{2uZ(9Km09SnA@@gU4z&>1B#cOE4m~l{f>&nA>2Wb->O##QJCFQ43f3LJyq8cGLHLaF?;*s~#$Kk*5PM-CNY%PMtTM=@ucp;Uu7XlfGp1Wd zTnooUiMQZV;l=k|`d&rKKkhLP&SPoa0+MbYkubx;+NN!5Z2CK`cBu2IAsYvS&p!eN zxW!kkhQJ0mCp<&w?s?5V1OO8;oza8Wk9ocuPpZHl!>OAja4!USJqhDIPg(Z! zLje0uyZ9W)ghsVd<5q4=KsH98NgjKm?E8}TvsMMyJTY>2YPO#lp3DJdF92!A-y+k?-Wo)555((eu zHgXxqQm=gm&%R;RKja0?PjUh}K zShtf?ug?+Q;lGuV$_PUUJ$dJTfeVEF_)QkX!hrm(V%Y=$LS?i_S22Usd*C2LgDkWc zG3S>;<9;r_#;cJYcYHhJfNqqGuq0+0^NkO&`tVMUS%My#^iVT;9P<@l2uKzsD%k3B zyW+H0#x5VoS}kMwUaKlEWED?Vxu#W`?2Z#rHB^<)vWg;Hr?g57rEz*bNWa=>grB3e zrCU5_;tWrD{mR_f1qsd|;iFWysM z2M2|EyFXO1(P$PIXZOcy?i6aaesGq0v_|!VAFACf?nu*KZEflW+e4G>nKfozl!icvQw}FF{dOS8vDq0ee*3GAoq7~+KsBvK^yN|fH$}; zmDh9XXdci2!C{PqBdg&ZCI%^jvfHlZZLyo8v1KKgR7&_hItk@qMbOh^8VtR)G>RAj zfGmX+KdK1gb;g#4?5>=p)=?b0_aJ9P1LUsXi^L&!F@U9tW(8TwHgBU`RVK5J6$hqFldQu_fJ6Sj5AkpQ-8HCmb=b2V&Fk-(rSUKO-8{;s-+msHLr`E99ij>TtfrK$U)P$ zKh<=j@nqADJVEOgQSHjl`}=fU;F5=f`u=pE9MkT>(~Hr3`g7~4&wC9%Vo!`X{=pG9 z&{5~BK;9lkrGsPeV_UGJIFq3x>vgx$G`QvhT!GigP#mR0hwXRYV~e@Z-gO4G5snoK z2!k_?<0X2F=G+DAAC)c9FM-)A2r$!V?Z4P5p1J9Nf%ze}qmD>Og4S@65ZdxHM8JSK z&s1QgP?Z=LN+3PQW4VdgsZ_z-|sXa#KqT;%S8A0=+*n| zDulaHq|LnrYE?K>+`%*O%>rKmNt?0zoUF{D-`793ebx+ujD9D48yC=&cz%HA!>q4= zY-g+@&<6tfK3Pm;rRL-Oo%3W3wyWU5*94K4T{6i8To5e|7wsK1F(ABZLuLra+4n`U zmb6F}W5>791vX~(+};OY13SeFLgup0c*U|YO33s|U>BM&!UD>|qjE52gb|3^7wo2i z>2}eMJr+O_;akw;rFdF7#gLTTp9Rq|e{32K=~+YrRdJ*I6?ZY;rx1XkCs@lCrW?c` zGx)*%WV@ng{W@aUMAwEn!AVtOMiC(qKN{>azL6eKys0^BYQ4@Klmtyk=w!Qw>4 zIr4vGu`ql9Co^KTawympW`5{@i89_mo+XU`%<(A$r~|Mdjv2wmHVoB55L6ttzJU&|4Fj?1-uiOZoX zDxqn$mzIoq>^6jJ?O+uN%(e!aRxpfi`46ZCG}(7M5J|t#3iHQ^QLY0XeurTo2TDX5 zi*wAw?-l`{fG2DazY@0BNiUYB&sA{XKY$Z$C%~5hndoWLF-4_w{po|GIZ?54IGj9xx(yYHITP0~Hh?h= zkX?ni>OIJOfe;v@3`i<`hUkCcMxd^^WVVxuhuKZJKFws`v8Y0 z!Un@<-VvuY&`e)iz(WK>UJONknA}b-3OYzt8Tj#-+h_{TF+aL+aV4KZYglDx9jfFR z)XJqhT65z`HuJ)?Bb1!K$_4!u!;-FfcXJFNU*Us*Zb2UUic3>%(dlC%qW&Z@X=9Nn z76F-{GtU37S?(Hy8NdkXTa{`Bya8}NdLeQ4saQw3CJN~01&C6qawTPkFvuvHzVMI2 z-=J}lwzXb~JO|id`pKMaSHl-14UQRnC|OM=Wi><}$u6>_8#%I(0pC*pd~o4JwqSlA zIcw|ZqzDh}j*ye}TkSG*g36zen~`?OwLix-j4@I^7YG}CITg9-;&@sNy9;*;Y>Ede z`ZV=e4a*Bh$=Yp@`fLAy1euu089F$CC1VHrpl!AZLjXr9NJNC>Kz9`V^*8#R1xW%D zw2Nv*WhG$~_jIgi-JfSlAvQO|^|Jodkc8IfptW8ybd3%v;S|b!35#)%_UXE#5!{+FD&iV%B3xAlH8*FzT!E@p%ffl;V84Ux0qe#RT-CK09{@24(f}*lX3J9u zcvo+BBgQT;m@5*PvT?*6snZ;Puc(inVO zE}??XA)_T+d@Upx2(r0qRaJ=2&ZRSxPF$SdDP2J5m(x6BvP@(hr?`&nBN=*2t?VO0 z>#8kQ&@aD5`ddGMcoPzLwBTfWh?)LwTU~CsLl=GF1igG&A-uU-v7&IhbI6?*4qrdF zg3~JFOF01UwmRIs8*j_S<|cQ-pE!EO5Q$GY)XK?B*pA_=Vh#k;tuNxx48!A}l{4R> zuM2SBcUB0NgO@XtAk1|$5;lN4UT6rC;bqylXex+DjSq`hTgPdtcdZn|jQ+&3^9r2! zstUx1*J?LDWDOdLc~PJO<%RG;2?acI&p5~!J<2I*dh)4}r`=*t@e{qEP$B+_X~tC% zg~32Y$QXzLhN~__+au&I^vnh05DD6d%(3TL{b z&+7GxWdf=zkCnTj5EFA+ZyhN_J;^M*bf=)v9p&z4uW*fE6v0Dv%EgtEl+dFRUg+h) zI!<&=t@$sdNEw;d=vo5VR(qai256H!2nOe6Eur7g7a<}K9RM>|F+rpV;Q~G8@_A9dxAfYGYNhXem5~z&mrn{c@flTa7hxPD!bsgePmPV1Er(HI|9uO#s z5Oo~fom{5DMmR=Eb0#5c!4=KY4iVeEZQOAaVo#&(sNBOAq_h#=4e~qYL&Ip@I);sw zUhBjbwhOEsBka1!0OHtUlzO9tdcDrR3Af#`<8_5aX+jn6pmQ|d5Jny{yNsLCo{0v z;I9+%I>|2iOJBcKd3+-B4us|W?LfaJjk@}6?1`}s>AQs&Co(OPey{qU>USAPRfQ*C zr0sRN9M(D4k-%(*SjC~LO`0u>Br%rpgLz({A9OXN#nH-IAms#h9iLqWs)=w#n@3Lu z;xMI!HaiF@+q^XrpX2grHHS!jdDjprE>T!*LxMq0+sh_?Bw6YmcRvxTlafZB@J(CY zOd!09^Nl?j@8kQ;#WM2<#5gEX8>|QgA_9LZWp8-BgfvBRgvdXc0^V zIMPLtn)GraUds8HA73q_J>Ce(G9yBTP(No3y|7X{f(6HrdDo^n=5GLA*)f%iz|ClH zR;#WG16$z0xbdIJvT*kk)h8tRAa!Npi73D7Z#;B--n08^<%CIw!*1zLtJp4(gnnY> zBf9Xna>)xpSweONOsqoQA8!Bq@IAzBI?^&;Zzyr?rN&=DsPo2M>(_vKgalcz{UDChrOi9*sEExd6P%bHq@|Bl+0Iizhj^r)r|UoM zR4Zczl~jDnXZGpXJs3dY=_QC)ErF{1)V$-|yj7*238wV%J98`zWQ1WEqVIX)qUwwX zA&CBMIX9Bl4rD+0TUj}Eh$o@o`=RU^;p*%e;dioUgoq+e{(Y-++1eGWgFlDx^FMrb zj{kkDGgYgjI20jcm&;TO;j#K3zGSEWz9qXgmrNxxzSB!~uOA=&zp#35p2peA;}}tk zFa?nHo(x1GPGplmlcNS2Eelqm!<-!JW~o7tZa5|l z$40~DQ7wcFLHj(zs_d{-f6}lzn)z>^-3bUYEdwI5%v--$JW$6hd~@ z*hRx@;kX~n=2NscfpC^VxQMR3vn-i=;Ycw_V2FL`twHojPefjR3riH19I;odn=ft* z34A(hUl?cg!5e$A7Y;ohkAf}6J?#_peK3=?)QusBun@J68uy$A{Q!2Z&7Cr!eI+C1 zL7AK8L0Osip)#X}2z#E?d2d+UO@cf zsXRY={raqe%y;GEhok4m6>$+%>J_{LDv-~&mUwfQ$AOohe-)pnL0Ijy81vUunn zC1+{uxigL$J|*nLwwmeJ!G?qXE|-5j^4~Src^!zyOJ^F3$06g^#pB4C$Vbn4mx#x)vyjy$&L{ck zJJ+6g44iMW?3HtxiN|Z_vwWO7-{j-W8TjHcbdKd?o11u}#l#cAR|tmVCS zyHlzu17Gy+A__)pU}=FCd(W~lL*lg{`5bt+o10egn zANq-Prwom2_ztqQ>+cLPnd2gb7)F_HZoa*|VmDTeAqj>--o*gH>AjUJ3wb~x+xuTA z3hdC4t5_OH;p!9dkySf}kWW?xFDp0@tQ>@df`h^P8xcp<(BVT-x!glxv3+=W2u1gT)-R8iF+bKB`9!$t(akhj#2@|Ae>1^ttLb>SBsJKgH8g?_)cavlNrrqKm~a{ zFdOG;Y;S9;t1)TSK3D5+v!})`fBY2aXZE_)k!$|gZR3xN8e4{A7p#^H3?+w~ z{@}i)EBCBy_aE8CMF51y_nba}Zr{>B982iQrqP70C_cVEZQ3QM6t+sP4hER*x0;<+ zW83nqu=;C)4$MZE+(Cy73v92x1gXDjg82$|Mb;>RPNH2zhDZ4WbuBf5aJdW*Rp=hH z-BiEqa9gMR65$bQsH%QDwg}-Z-G#sm4}oI!{1QgAr9U}CU1!A{^`*U(#*|YxNl(Z~ z3nRmA3})F>?lnA@lFe4TdC)$nH}-bxn`!kI5xz34Ziy#&S6_y>wYq&Pc2o+u3Ikv^ zlL#{9QK+e8jJXAM(J-xag`)?@oNm;<`@5Y&nG!k*Z-_RJpH8}{73w6E=Z`^LVr zuj~i=$o^=b+0X40``A9U-`KD1xArspgZ;vOXMeU|+Mn#->@W83_AC1j`%n9;{mp)F zr)+|$9i(>5j&y1#shy>^5AJ35Ahq4p9;WsDQoYpn>y#Wa`)yisS9Ik9K}s+} zXoBb<5Qbo#l#vLW>KX;rM^)nkt#LmPu|gUc>bigL@*1;1G;%Vqc(Om;z>CxbC}Gt~ zx_I|g7cf9H)YIiZr-rUc9IRa2`FCL<;{2DyO3xwDM~-?exikk^8D$Q4(3eoS7y%yx z|2*n}y-c-h2f$m7^gT=a6??6Mv~M7OS2|!76RTh7#L9ndH#*>Kl8A4$TCLIn&(Vy( zKr%YBO=4R^I=e+xC(!B2ZrCr<4eONj!8^?$E(7V*x5JtTDqU`jC>k{F@-eZJ%oT4 z(P6^tfg=O^10K3;Log(wu?sY2lG>|J0xT7+q40Vg%|-Y;wuQq$A#MHGQkm;HrNVA< zZ9~qD5U`TAy5KGt`IeKOlgXAvC>r5sVY8zU+UdD}p@e(BXc;SMlR!18-+ts|xW!xy z?3!f>vpNqi?EB187gpi8`~fTXF4OIE7f!a~dLUm6JVU0ei~OFdOTN2m#GLfI z8Zt%*keZ5oU8|@W@SzL!u^cI{m#KQbo2<>E7)*;?#!-ddn#WnFn$8y*>Y=^*|IhLN zi*w8w5b#0sEFTQCJMc6%e5(HkPxv3=giB_C+uPMKcYv5(45n)0a6)_+X~fEU8(B#i zX#8p3W@U8Y&*Bdm>HtwlDGt#FZ`co88$F<}|MLTKoQ49(B+=}(*C zQkKMA3GN_9T4L?D5u{SPw!je!CI&O8<}RnuNl;|2kc)VD8CqG0RHYi47Zy+$E>}R- z3FA3~-7rI0aT&@=a)fP4I>_iZIsZP^=wRo5X3+up##*u2%Hx1^vJ4qf6V}J;A&j%C zZ}I2Rbm|SIFjHOSdU~Ol3QkY{R95F|7)ngU4zSU|>mW&urWFZt4f%PD#T~4gOF;-x z4hc{6s%aC1g_YC^R{SyIB)Sg} zwv3f}4OMLM)M`TPCU70l0eze)P{4H_c!9=+$%S|fp!c}CrTqfr1AAY>#}%bap!w8( znLABP)))4i_AbLQ?Snn_`{1BIb*(q4)v|@{`>!o@$!xdkZB?q(-euKV?F0R0r_oP4 zUqCzaDE?1CahMaj^qgl92JX$t>EQFx>rco1PnaWd(CAD6R?#zrR3GQ8OEJMEqIK|m zRK=!BBZC_QnV=5q6cF4H#Hho_i-(@@dru>_%N&%}VmF+4owb>J7tFHRT4n2cz?#^q z0I*HHB;imq3qt)lWS@;=2oa-t^?uk%MgRSvlj~_S2u1bxN$&e$?)xp>W`#=3*+!_? z*L5C)M{gus_sR!oFagajSgpH5aO)gKS(suHg4~upcyYu!R6UwY^gr7LU+f8l%u?*0 z1`isjIDiGV0U{)o7A`SxlwO7T|LPE_nCucXiTdX?Di$46Zgrna%!%?{G(`>VSQop| za5QO{fOMA^&N7;`UqOM{+8cLXt#tl~Akc}baAgEmOFPs>uog7HfEg}W0`w5fUtAOn zO?zAp7gm8N^tv*D&|`p_zsKU0@Bnl4auYu)Ox)uWe+X?)7yGCu9^%C99XihZ{hCPW-r|e|E z0O$i416Rh&U{d1=tr>PtYi?6zN0wyAX<~^S#PE@A?S~M33kD6P6m-%qyc7;!3xLo@ zUEj>kw@AeH@TBoT%o_`AX#H`$j1f|mLKzU`&qNG@u?2PXAprX>W6U-!V|?1?<>9D` zs#WpVsf&_M04E!#H*Q)+Z2&dBv~P0oerT>-KioEcFBy5RE59xt77$Beg=(J_-7@4+ zZ_VOd)ubzQLQ{P8OxXi=xl^2-I_JU_5cW)Zr;gyc*=X;z_glNI1OC#3mkrX|hXO2S z92TF2c1C3BQ0#*Oe7UG-uGGZ%MOvs{Xwl~_{ubIkk)_kCk!uCT+QJprVeEnydK;+s zrRnfdlKFLo1Rg2kVn$XXM*n15!?32!rT-14=8Hu~&$*@uoc?TJZL_7`Z}0BywAzgN z*=yGKntQu-N-cd+mKI>SMYiezn;{ZP=qJiyjaP+rjrh9$9@-HRKG9BwEdmcMvy(qf zuv0u3&YObsSB)LLmU%6F)1Stqt4YfQiTCCml?KY9Rfxa`d?sOF4-`_t0lkC)p>l@^ z4xsvg>_X5YyRg5%f6%D!w)XADL9<@p-#chhQZxvjkk3$da0N{Un)+JU!cZS^Eoy-X zp*F|pxz)!rRCjVZ8SmyY-A zT%?B)uC>%8Fb8#$66ofZ|4NctD#N!WWmE z#d~rV8wUri{hfpT=Dxjm&}!^9_Sy%Oc?|Lh&E;rd7}zdh9?eNE;O8)h8z+UPhP3NS2RZPd0VfhZ?Px+*smz6%%xg;iZ4 zW5okm+(WS?>cT}~MglVIE1vnEjMWG95|H= z7Zs3KEmVn5?A`Mde9!3MiF*Ix`0Udgc-y3WjhOVSC~pI+Ihp*B7lfh7f1404CNS*s zd2EO}WD7cUK!y@-p90WBM#*KJ1ey(cl&7worQRGk8o?_=a-kltzjexcs4usn2^~L0 z^GRw5(h@qb-tZ)r+iwC4pSVP>Uz7h76*%cXLLI{tv!(}~U@N4Q;cG?GbZ7kRJqrlW zCgQ~a6j&Kh5ieK=+0;Vt`NX*+=g~UDwREW&T)?e8GZ%E?N2j)e(7JbS|49-b2$Vu`4>ma3(tYGBIjDje(f>?p?gnxpy z-5_f_7fbtFU^R@T{ZxRr4Zfx?1ABvTMFJ?Gxcx0KF^Elx?=R5coOFF7xApgY-@l7S zdB1bs?-#Z+@e}R8QU^UNZuuSA+IzC)4?EJE3`!i<`+!(FfSvevcUOLglKr%!S|Ci9 z>ew%Kd6#3KZ9LXx8^V(vhlK57s; zsy%UDpfw`l`V&_EGDkIiE$r_lU*|sq`;stu0U_?8j*TJs^2iyX_fmta5ITWE*VHMj zsyK-~7v!WU6Fp&Yu4NCAykLu>(uK;C0n!j$% z+xb0A{yMiJX-pLrxFpwnN!|zlv9m__RUuJ?LWzRe0!mC>i!F#}#~4tYugH$MBw=h& zl7U(bZa)L`j%NiB?6o5-*Oto%#&1^f10%bP#6hnk_Ik3*$hR>#jdq|63W>>qjp=$b zf^Z!)_M4l5U8blDRY;%pI!{n5e>Mc$hgvSS2JEjgXI@{|ZN;iB4{~tm57U8#io9G1 zc(|sF5?)COsx^-qne)@tiARTy+ZCp6Jopu(|Nj*5F$8u9 z%1_w}=hk&lORY$*{CXsjuVB~l(vIQJrh<_!jky4LiXHdJ6MQ{`g#!C#GF^1=4 z^)Gke+@Mey_@@Db?SyDtmpH=u!{k63Vxjhin>OTTtMerJzfLAZVg^C!m_KP0@ zs(uYgd_9CCu%#L}Z&GUjdX}E$ZH;q9093Zw%N0PvW;Y;`t5`cXe;5KC%_A^|+3b?t zr99wS2l54{nDLm1vM5w3v`hqAan9RYpl5DNR__Fm>^>Iv6u8yN2x05OD`YLLdXC8$ zL0kD&){SO1Hv#OV$;#%YY}C<>%D|Kiz}ZFgdrD)jd&-Xof=xbh5MPM2qrQW4=9~iC zF|qj(rnt*=QaJ`7xc5<&s%wXg3?4fmdKk)MHSSG?@Mqx+&&OzzI~EyW*oD5ZUC1x= z*8oCls_TpWY5)`rl--utXbwh(9b-JO&cll;_p2vX?5Ul>+(DS;Bx=gw)TT0l5$Ph` z=-bd*h;H=HG$K_ZDGKr za^hO@d~vfHI0UOsEyKXTn=mZw17pGtnh)FvVq?hk6!o0Ja_RQiBUKp;*}>yV19w@c zXL2g^H&dd&SvXT^J&{H14AvQQJhm57xeW($=q&Vk96Ay&bK-s|&n25XM9&nbbBwwP z?MgBcQ@IsWndzy_U@DWIbGuy5U@EsgXCkLEvNxPbcj8R8X697pVk&bol}l$Xr*a)z zmpGLT`;ty&JDcfU1)FAyj!waeEkY$L$%g zzhZj^usuElujO*k<3Q(YR0V|i(B|58!z{OB>JQf{K zb;ncac-V8s%jFb09`_iqiH_L?0!4<-aBIpszK%u5i$cc>8DEukJTCJ<8pXoYZQV+X z_E@ijS&$mcI_IMop1pRNrE9ltvc`x7!_ZAOnASqZz=DBIZMrLrNuph0>Z9+ckekrl zKs==sML0@++zo1T6awKNVb#xnUgA9XsIgSX}tdi@jey?L-7z}c0>%*!)57) zQQ{3F$c%$Wyh3g=CGBaL!%B#JhmI(tBl6fVGCrcL1{vE|k7!0mGz+HFAU7mas?=!I z_xE@9+I#yrthUSqiUZQ$U}Ru_qQ>O2VGQ_)gn-AXKQ*9{7XtG<4IVJmS5a#CU`P{d zqaBNCV2-AA|f-X*MToz)E9GmM45ZD zjJdT`$sOl4BKKVUe-_*KMBhzk5c0)FAr=E(IT;%UTXSJ~gpdqWoH8&3(T~T6l!9=~E~V648)ZCvQvwh+3dR_REnQ23 z343)PcvA*zE_h;z@HK0(mt<3P{^5fOwSYzo*w8Ib?HKM}$e$&z#g~K4`2m~g1`a|+ zpJC`k3scLEeynF&a`-tEd-Blt-^c8s!*=y+jH%l?f5zZCD+Fc;W&P8btr-r;=A-Gz zH(`~(FLN(CRd`NnRah4ofhUA{d2s4iolEqiC-cZ3&OCMymu3KB(r+;+Cc3f1ruGdc z1HJh&g?bDSJcWD^+7i$CBrenbB8cWU&2A777MgK zHb%@8V#CIQgE~sv3Ikd$IVuSRpczg*;yTnYxvfD`n{D8f`}O5^+#w6`fiA>rUZhhZ zJoyY`eTanXO>MX*2iR=G2(dF_CGj(@UX`@ETQ6z#-Osdo_oJ8N(%A}Hjmm^vIrw>g&)zkq+sfJpZPUXL1Quy^kRdIX{EZ8?Xh^0G(Y$xG#|S1^rbZoPA3VJZ zYiqfL@6+XSA?}r16d>IqC3|~V7mM41#5&N3FB)m%<6UbKU7~tu^t|S$ZCw1IIr{V3&Z|<+`2W!Xu2miZk zS7x+eixzB&g7CLxS29r$N9tB<-kwzn6QIsEHz(kk&en0f$F75R9jDQ5RMG2(9Tvuf z6W@Dzn-dcsgAKGZA@E@R1bmjjV`7`Tf*@r;$9SWdG|Os&K2|8a2f(>RO4(iSIRhIQ6=kO=g+-*kRKFo2fkQ5gcJ@q*%`X)D0#Bn!J9QhI#sI$?_uF;!1Z86b zinD3xW7HZ$oEoGSQpw!_&2x>$Kv<=Un=DmcCBL11POgBM62t!DsqRoavlZ3w;`Ua z_OxBtqpAcKw(uKOI%2NCfcb1FKR_-Am}B&hd5^1JsU5S-c$+c!0X?-iJe94Ds^R!^ z$<7O3$2q$Vm27t1nQm@QYjNAUB%C}n{3%r7x}$jH#mMx@6Ka6K)VahiuAO0QZP zFXe@fbmQjc^At1cU*jW1yK6nth=ac+uU3Y zq3q_Y((mXvufI0%tUiFD2Z3LZ_wiJMPaHU&ffbGj%=l_^^MOO{krPYcK2iW(7~+v0 zL<=$(Rgat_gpt@edb|P+JGAwY9hNiaWA3m#N9&f{VL9Q4<*eF$j)&!BbMu5B7TxoS zp7^e0KA+2z^IV;rGv`d5oMY$AJ~d9xNBfwboYTU|d4nhCRGl3Aweu!7gSXJY>)Z^U z@fp0XcHcS>ALH5P=ChvDXg2vk-^zi$RRewPyjBDK;Jmh97z2G{f8a;&g+9bk@|_cS z)(dj-?lw2?;N-pg+b8eNd52AZhLd-SCy%xM`H)*4>7(+^w@!`o_L+gfyZm{hMPva6 zXk=tBA@Tqlg*(;bP)ab6eG9?on%Olr_4p{s#Ubz~9y`j!oLIA6s|6uN`N9feNGK_t zK9_r>impljE-~}flyGn>H%58WGT+QHS7l}1my>;@MCuU^ltlIdQ!VrwgNvG9L={_@ zodGftQu!)FD5zICZ0yR+^@5VlK>g+kRdJX5c4pw)3F0G&K9m?qYje6^6#f!xf`Q|A zMIO^GC&qCw10lHvT);4-3%H;PAq&_k%8}j!)cJOy*T=`uNr<*mG8X+j5kM?xf#Ug| zIN)gk0~l?1a2HgzDvXn5WFJ5X+l|gQ-w-XatCIwuz>7vn@=}uo^3?eDnrO=hQXa+q zmzLYZa#ylk&d5&}+;15dgS1gosNv-7?OGblJfsCWpb?T5&w!8MCVmW@__3r9Z48<&l0_40t6nAn2t)0xa>_05IMj^W^vSAv<)Wwy&uIVc5X2BFNN6?MF0aXT^bzp;2MR4?Tf`8;lNLJWBm_d_Qz0e~So~-R zsn0~L$nRu1sa1q;Z`Hoy(ZTI&j||}6Bfi2@;y^#kIndvD1*1+;mnhdgsdVDbW(2I< zeGUDW`=-;*)e2a6Sn>V{w(X2)R)_mKmdj|L*wA?Um?@6?IKuNj(cu<0|BP?`k!PO~ zHLf}D(ZFi%>};~ry#q1#iQW|l`%yu{WZ#5@lNFOWUh)3acrPe2qW{t`8SLw2zS=hp zZC)Frx9(iLf1iV^6L;6gN5ni;0)WrG%}o&omT1BZl0;B=u_MEE^h0b$P3w;vx?tow@<@81Eq zJSRXG5ulq>pu2$|ZQ;WT&#fj=Ru!P`xp4CJ?B_%&WJ29TlG}?D=j`=4Y5>LQ*aLlpodU?T%18=lzg0X+=4(m*I)pqy+%^;9i^4P zkqTcea0iQ719|0!&3cDmCR^;UhaL$zrz{Vr1#2X(jRSKhlcapdvK8^1pSm)NuH+{`kon>Q%bV?Zn$0A%$VjmN-Gs#mAB1excv-S{b6h}N! zkQe7>I$ANKjTP_bPeb0NUelC7UY3Kr>r%*@X~>(ELf(vR^|w6a-T$vao*E8c)*pww z8@)=mx&63XjR1*9XNk&=x(ouMQI78>CQouMo=MKdGgvN(KqYy~GqWMhuVhFz(NlYG zui4n&+26Mtje}Ob4Kgaoz-C7d;?RPFZ_NEKga4lYCA&)owqF%OW0-_*X9eG0Uds8H zFhm_yp)_@T;a)@o?%az;8UxU_xz@N^OZSN>Wt~eNp*3d#_rauUPxDT~(qmI!ErY_% zaFzV*<^p}DHrL!t@5#;dequ94ZZ1ddAltMEGEmw>hStV>%f@=krp~x4d>y$rwA!+% z+_FV+%NPYZvQvTjexH+W1om;_aOC}8RIU#2g#9p+w8gJDry`O!;Lz)}IR?4FB> zfl69kmRcNB)QQPhP;M3%3=P$+`CBo#UcF!MGz9qNtrH1t7Qm9nlG+M1WflvnjA13# zB$|49aPmL_8AA);?xM*uZZ4b{_RxtTX?$+}JTZSB!_U*4xaPEQ=brLA_c*an32O|+ z-jEw!Z5q4Un}RA2 zZeil>dULR`OJNbC>`FL=0`ts!TogV*oWPaM#-`rD&{r~4C z^`bDTXM9p068jlp>!H|((n;-jY(LutaM?T9+dXJCTJ8PaW~;HY=QR(S|4*j3pPwFO z;yxFq_d&=J68i&@BS5hixpVXhUcSq{{0*rY9U}4k!p_C@4{P%*@vVU8Whtr|zEFDT zb7-BX)EE1A`ZW%$z?o;3#rk2Tbbe2U5{v!?uM9#K9WW)eB_k-jMa|L3yVHS&ARmYC z&gWhTQc%X|F|wM7DrlShl3IrV&S`}4y!-0Os&;T`h-4z6?%MYlXuM&8^`VYj4L{(E!tZK^zrGOpTpv2*C68`cspwZrpZD_Yhsob)`#2XP-lac!_PaFSIvkILW?T5@Geu5Jk{5D&+%0AnkH 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 diff --git a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua new file mode 100644 index 00000000..86c0e9c5 --- /dev/null +++ b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua @@ -0,0 +1,78 @@ +local Fader = { + x = 0, + y = 0, + width = 11, + height = 80, + min_value = 0, + max_value = 10, + value = nil, + tip_color = 9, + disabled_color = 7, + label = "", + type = "fader", + data = nil, + on_value_update = function(fader, value) + end +} + +local Button = { + +} + +local faders = {} +local widgets = {} + +local factory = {} + + +factory.createFader = function(value) + + local result = new(Fader, value) + table.insert(widgets, result) + table.insert(faders, result) + return result +end + +function inside_widget(w, x, y) + return w.x <= x and w.x + w.width >= x and w.y <= y and w.y + w.height + 12 >= y +end + +factory.on_click = function(x, y) + for f in all(faders) do + if inside_widget(f, x, y) then + local percent = math.max(0.0, 1.0 - ((y - f.y) / f.height)) + local value = percent * (f.max_value - f.min_value) + f.min_value + f.on_value_update(f, value) + end + end +end + +factory._update = function(mouse) + +end + +factory._draw = function() + for f in all(faders) do + + local y = f.height - 4 + + if f.value then + y = f.height - ((f.value - f.min_value) / (f.max_value - f.min_value) * f.height) + end + local tipy = f.y + y + + if f.value > f.min_value then + local linex = f.x + f.width * 0.5 + gfx.dither(0xA5A5) + shape.line(linex, tipy, linex, f.y + f.height, 7) + gfx.dither() + shape.rectf(f.x, tipy, f.width, 4, f.tip_color) + else + shape.rectf(f.x, tipy, f.width, 4, f.disabled_color) + end + + print(f.label, f.x, f.y + f.height + 5) + end +end + +return factory diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt index a31759e6..6ee434ba 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt @@ -28,11 +28,11 @@ import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.WaveGenerator import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.launch +import org.luaj.vm2.Globals import org.luaj.vm2.LuaError import org.luaj.vm2.LuaValue.Companion.valueOf import kotlin.math.max @@ -77,12 +77,13 @@ class ScriptsCollector(private val events: MutableList) : FlowColl } } -@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class) class GameEngine( val gameOptions: GameOptions, val platform: Platform, val vfs: VirtualFileSystem, val logger: Logger, + val customizeLuaGlobal: GameResourceAccess.(Globals) -> Unit = {}, ) : GameLoop, GameResourceAccess { private val events: MutableList = mutableListOf() @@ -117,10 +118,10 @@ class GameEngine( private var accumulator: Seconds = 0f - private lateinit var renderContext: RenderContext - private lateinit var inputHandler: InputHandler - private lateinit var inputManager: InputManager - private lateinit var soundManager: SoundManager + lateinit var renderContext: RenderContext + lateinit var inputHandler: InputHandler + lateinit var inputManager: InputManager + lateinit var soundManager: SoundManager private lateinit var resourceFactory: ResourceFactory @@ -188,7 +189,7 @@ class GameEngine( // Always put the boot script at the top of the stack val bootScript = resource as GameScript bootScript.resourceAccess = this - bootScript.evaluate() + bootScript.evaluate(customizeLuaGlobal) scripts[0] = bootScript } @@ -203,7 +204,7 @@ class GameEngine( // Don't put the engine script in the stack engineGameScript = resource as GameScript engineGameScript?.resourceAccess = this - engineGameScript?.evaluate() + engineGameScript?.evaluate(customizeLuaGlobal) } BOOT_SPRITESHEET -> { @@ -237,7 +238,7 @@ class GameEngine( // Always put the boot script at the top of the stack val bootScript = resource as GameScript bootScript.resourceAccess = this - bootScript.evaluate() + bootScript.evaluate(customizeLuaGlobal) scripts[0] = bootScript } @@ -245,7 +246,7 @@ class GameEngine( resource as GameScript resource.resourceAccess = this val isValid = try { - resource.isValid() + resource.isValid(customizeLuaGlobal) true } catch (ex: LuaError) { popupError(ex) @@ -264,7 +265,7 @@ class GameEngine( // Don't put the engine script in the stack engineGameScript = resource as GameScript engineGameScript?.resourceAccess = this - engineGameScript?.evaluate() + engineGameScript?.evaluate(customizeLuaGlobal) } BOOT_SPRITESHEET -> { @@ -303,7 +304,7 @@ class GameEngine( "Stop $name to switch the next game script ${scripts[current]?.name}" } // Reevaluate the game to flush the previous state. - scripts[current]?.evaluate() + scripts[current]?.evaluate(customizeLuaGlobal) scripts[current]?.setState(state) } catch (ex: LuaError) { popupError(ex) @@ -314,7 +315,7 @@ class GameEngine( sounds.forEach { s -> s?.stop() } try { val state = getState() - evaluate() + evaluate(customizeLuaGlobal) setState(state) inError = false } catch (ex: LuaError) { @@ -379,6 +380,7 @@ class GameEngine( valueOf(color), ) } + is DebugEnabled -> Unit // NOP is DebugLine -> { val (x1, y1, x2, y2, color) = debugAction @@ -391,6 +393,7 @@ class GameEngine( valueOf(color), ) } + is DebugPoint -> { val (x, y, color) = debugAction engineGameScript?.invoke( @@ -414,6 +417,7 @@ class GameEngine( is DebugEnabled -> { debugEnabled = action.enabled } + else -> debugActions.add(action) } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt index 1464fe67..2539fb60 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt @@ -47,6 +47,12 @@ interface GameResourceAccess { */ fun sound(index: Int): Sound? + /** + * Play a note represented by a wave. + * + * All notes added in the same update loop will be played at the same time + * at the end of the update loop. + */ fun note(wave: WaveGenerator) /** diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt index e100d0b3..6190ef4e 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt @@ -5,168 +5,178 @@ import org.luaj.vm2.LuaTable import org.luaj.vm2.LuaValue import org.luaj.vm2.lib.TwoArgFunction -enum class Note(val frequency: Float) { - C0(16.35f), - Cs0(17.32f), - Db0(17.32f), - D0(18.35f), - Ds0(19.45f), - Eb0(19.45f), - E0(20.60f), - F0(21.83f), - Fs0(23.12f), - Gb0(23.12f), - G0(24.50f), - Gs0(25.96f), - Ab0(25.96f), - A0(27.50f), - As0(29.14f), - Bb0(29.14f), - B0(30.87f), +private const val OCTAVE_0 = 0 +private const val OCTAVE_1 = OCTAVE_0 + 12 +private const val OCTAVE_2 = OCTAVE_1 + 12 +private const val OCTAVE_3 = OCTAVE_2 + 12 +private const val OCTAVE_4 = OCTAVE_3 + 12 +private const val OCTAVE_5 = OCTAVE_4 + 12 +private const val OCTAVE_6 = OCTAVE_5 + 12 +private const val OCTAVE_7 = OCTAVE_6 + 12 +private const val OCTAVE_8 = OCTAVE_7 + 12 - C1(32.70f), - Cs1(34.65f), - Db1(34.65f), - D1(36.71f), - Ds1(38.89f), - Eb1(38.89f), - E1(41.20f), - F1(43.65f), - Fs1(46.25f), - Gb1(46.25f), - G1(49.00f), - Gs1(51.91f), - Ab1(51.91f), - A1(55.00f), - As1(58.27f), - Bb1(58.27f), - B1(61.74f), +enum class Note(val frequency: Float, val index: Int) { + C0(16.35f, OCTAVE_0 + 1), + Cs0(17.32f, OCTAVE_0 + 2), + Db0(17.32f, OCTAVE_0 + 2), + D0(18.35f, OCTAVE_0 + 3), + Ds0(19.45f, OCTAVE_0 + 4), + Eb0(19.45f, OCTAVE_0 + 4), + E0(20.60f, OCTAVE_0 + 5), + F0(21.83f, OCTAVE_0 + 6), + Fs0(23.12f, OCTAVE_0 + 7), + Gb0(23.12f, OCTAVE_0 + 7), + G0(24.50f, OCTAVE_0 + 8), + Gs0(25.96f, OCTAVE_0 + 9), + Ab0(25.96f, OCTAVE_0 + 9), + A0(27.50f, OCTAVE_0 + 10), + As0(29.14f, OCTAVE_0 + 11), + Bb0(29.14f, OCTAVE_0 + 11), + B0(30.87f, OCTAVE_0 + 12), - C2(65.41f), - Cs2(69.30f), - Db2(69.30f), - D2(73.42f), - Ds2(77.78f), - Eb2(77.78f), - E2(82.41f), - F2(87.31f), - Fs2(92.50f), - Gb2(92.50f), - G2(98.00f), - Gs2(103.83f), - Ab2(103.83f), - A2(110.00f), - As2(116.54f), - Bb2(116.54f), - B2(123.47f), + C1(32.70f, OCTAVE_1 + 1), + Cs1(34.65f, OCTAVE_1 + 2), + Db1(34.65f, OCTAVE_1 + 2), + D1(36.71f, OCTAVE_1 + 3), + Ds1(38.89f, OCTAVE_1 + 4), + Eb1(38.89f, OCTAVE_1 + 4), + E1(41.20f, OCTAVE_1 + 5), + F1(43.65f, OCTAVE_1 + 6), + Fs1(46.25f, OCTAVE_1 + 7), + Gb1(46.25f, OCTAVE_1 + 7), + G1(49.00f, OCTAVE_1 + 8), + Gs1(51.91f, OCTAVE_1 + 9), + Ab1(51.91f, OCTAVE_1 + 9), + A1(55.00f, OCTAVE_1 + 10), + As1(58.27f, OCTAVE_1 + 11), + Bb1(58.27f, OCTAVE_1 + 11), + B1(61.74f, OCTAVE_1 + 12), - C3(130.81f), - Cs3(138.59f), - Db3(138.59f), - D3(146.83f), - Ds3(155.56f), - Eb3(155.56f), - E3(164.81f), - F3(174.61f), - Fs3(185.00f), - Gb3(185.00f), - G3(196.00f), - Gs3(207.65f), - Ab3(207.65f), - A3(220.00f), - As3(233.08f), - Bb3(233.08f), - B3(246.94f), + C2(65.41f, OCTAVE_2 + 1), + Cs2(69.30f, OCTAVE_2 + 2), + Db2(69.30f, OCTAVE_2 + 2), + D2(73.42f, OCTAVE_2 + 3), + Ds2(77.78f, OCTAVE_2 + 4), + Eb2(77.78f, OCTAVE_2 + 4), + E2(82.41f, OCTAVE_2 + 5), + F2(87.31f, OCTAVE_2 + 6), + Fs2(92.50f, OCTAVE_2 + 7), + Gb2(92.50f, OCTAVE_2 + 7), + G2(98.00f, OCTAVE_2 + 8), + Gs2(103.83f, OCTAVE_2 + 9), + Ab2(103.83f, OCTAVE_2 + 9), + A2(110.00f, OCTAVE_2 + 10), + As2(116.54f, OCTAVE_2 + 11), + Bb2(116.54f, OCTAVE_2 + 11), + B2(123.47f, OCTAVE_2 + 12), - C4(261.63f), - Cs4(277.18f), - Db4(277.18f), - D4(293.66f), - Ds4(311.13f), - Eb4(311.13f), - E4(329.63f), - F4(349.23f), - Fs4(369.99f), - Gb4(369.99f), - G4(392.00f), - Gs4(415.30f), - Ab4(415.30f), - A4(440.00f), - As4(466.16f), - Bb4(466.16f), - B4(493.88f), + C3(130.81f, OCTAVE_3 + 1), + Cs3(138.59f, OCTAVE_3 + 2), + Db3(138.59f, OCTAVE_3 + 2), + D3(146.83f, OCTAVE_3 + 3), + Ds3(155.56f, OCTAVE_3 + 4), + Eb3(155.56f, OCTAVE_3 + 4), + E3(164.81f, OCTAVE_3 + 5), + F3(174.61f, OCTAVE_3 + 6), + Fs3(185.00f, OCTAVE_3 + 7), + Gb3(185.00f, OCTAVE_3 + 7), + G3(196.00f, OCTAVE_3 + 8), + Gs3(207.65f, OCTAVE_3 + 9), + Ab3(207.65f, OCTAVE_3 + 9), + A3(220.00f, OCTAVE_3 + 10), + As3(233.08f, OCTAVE_3 + 11), + Bb3(233.08f, OCTAVE_3 + 11), + B3(246.94f, OCTAVE_3 + 12), - C5(523.25f), - Cs5(554.37f), - Db5(554.37f), - D5(587.33f), - Ds5(622.25f), - Eb5(622.25f), - E5(659.26f), - F5(698.46f), - Fs5(739.99f), - Gb5(739.99f), - G5(783.99f), - Gs5(830.61f), - Ab5(830.61f), - A5(880.00f), - As5(932.33f), - Bb5(932.33f), - B5(987.77f), + C4(261.63f, OCTAVE_4 + 1), + Cs4(277.18f, OCTAVE_4 + 2), + Db4(277.18f, OCTAVE_4 + 2), + D4(293.66f, OCTAVE_4 + 3), + Ds4(311.13f, OCTAVE_4 + 4), + Eb4(311.13f, OCTAVE_4 + 4), + E4(329.63f, OCTAVE_4 + 5), + F4(349.23f, OCTAVE_4 + 6), + Fs4(369.99f, OCTAVE_4 + 7), + Gb4(369.99f, OCTAVE_4 + 7), + G4(392.00f, OCTAVE_4 + 8), + Gs4(415.30f, OCTAVE_4 + 9), + Ab4(415.30f, OCTAVE_4 + 9), + A4(440.00f, OCTAVE_4 + 10), + As4(466.16f, OCTAVE_4 + 11), + Bb4(466.16f, OCTAVE_4 + 11), + B4(493.88f, OCTAVE_4 + 12), - C6(1046.50f), - Cs6(1108.73f), - Db6(1108.73f), - D6(1174.66f), - Ds6(1244.51f), - Eb6(1244.51f), - E6(1318.51f), - F6(1396.91f), - Fs6(1479.98f), - Gb6(1479.98f), - G6(1567.98f), - Gs6(1661.22f), - Ab6(1661.22f), - A6(1760.00f), - As6(1864.66f), - Bb6(1864.66f), - B6(1975.53f), + C5(523.25f, OCTAVE_5 + 1), + Cs5(554.37f, OCTAVE_5 + 2), + Db5(554.37f, OCTAVE_5 + 2), + D5(587.33f, OCTAVE_5 + 3), + Ds5(622.25f, OCTAVE_5 + 4), + Eb5(622.25f, OCTAVE_5 + 4), + E5(659.26f, OCTAVE_5 + 5), + F5(698.46f, OCTAVE_5 + 6), + Fs5(739.99f, OCTAVE_5 + 7), + Gb5(739.99f, OCTAVE_5 + 7), + G5(783.99f, OCTAVE_5 + 8), + Gs5(830.61f, OCTAVE_5 + 9), + Ab5(830.61f, OCTAVE_5 + 9), + A5(880.00f, OCTAVE_5 + 10), + As5(932.33f, OCTAVE_5 + 11), + Bb5(932.33f, OCTAVE_5 + 11), + B5(987.77f, OCTAVE_5 + 12), - C7(2093.00f), - Cs7(2217.46f), - Db7(2217.46f), - D7(2349.32f), - Ds7(2489.02f), - Eb7(2489.02f), - E7(2637.02f), - F7(2793.83f), - Fs7(2959.96f), - Gb7(2959.96f), - G7(3135.96f), - Gs7(3322.44f), - Ab7(3322.44f), - A7(3520.00f), - As7(3729.31f), - Bb7(3729.31f), - B7(3951.07f), + C6(1046.50f, OCTAVE_6 + 1), + Cs6(1108.73f, OCTAVE_6 + 2), + Db6(1108.73f, OCTAVE_6 + 2), + D6(1174.66f, OCTAVE_6 + 3), + Ds6(1244.51f, OCTAVE_6 + 4), + Eb6(1244.51f, OCTAVE_6 + 4), + E6(1318.51f, OCTAVE_6 + 5), + F6(1396.91f, OCTAVE_6 + 6), + Fs6(1479.98f, OCTAVE_6 + 7), + Gb6(1479.98f, OCTAVE_6 + 7), + G6(1567.98f, OCTAVE_6 + 8), + Gs6(1661.22f, OCTAVE_6 + 9), + Ab6(1661.22f, OCTAVE_6 + 9), + A6(1760.00f, OCTAVE_6 + 10), + As6(1864.66f, OCTAVE_6 + 11), + Bb6(1864.66f, OCTAVE_6 + 11), + B6(1975.53f, OCTAVE_6 + 12), - C8(4186.01f), - Cs8(4434.92f), - Db8(4434.92f), - D8(4698.63f), - Ds8(4978.03f), - Eb8(4978.03f), - E8(5274.04f), - F8(5587.65f), - Fs8(5919.91f), - Gb8(5919.91f), - G8(6271.93f), - Gs8(6644.88f), - Ab8(6644.88f), - A8(7040.00f), - As8(7458.62f), - Bb8(7458.62f), - B8(7902.13f), + C7(2093.00f, OCTAVE_7 + 1), + Cs7(2217.46f, OCTAVE_7 + 2), + Db7(2217.46f, OCTAVE_7 + 2), + D7(2349.32f, OCTAVE_7 + 3), + Ds7(2489.02f, OCTAVE_7 + 4), + Eb7(2489.02f, OCTAVE_7 + 4), + E7(2637.02f, OCTAVE_7 + 5), + F7(2793.83f, OCTAVE_7 + 6), + Fs7(2959.96f, OCTAVE_7 + 7), + Gb7(2959.96f, OCTAVE_7 + 7), + G7(3135.96f, OCTAVE_7 + 8), + Gs7(3322.44f, OCTAVE_7 + 9), + Ab7(3322.44f, OCTAVE_7 + 9), + A7(3520.00f, OCTAVE_7 + 10), + As7(3729.31f, OCTAVE_7 + 11), + Bb7(3729.31f, OCTAVE_7 + 11), + B7(3951.07f, OCTAVE_7 + 12), + + C8(4186.01f, OCTAVE_8 + 1), + Cs8(4434.92f, OCTAVE_8 + 2), + Db8(4434.92f, OCTAVE_8 + 2), + D8(4698.63f, OCTAVE_8 + 3), + Ds8(4978.03f, OCTAVE_8 + 4), + Eb8(4978.03f, OCTAVE_8 + 4), + E8(5274.04f, OCTAVE_8 + 5), + F8(5587.65f, OCTAVE_8 + 6), + Fs8(5919.91f, OCTAVE_8 + 7), + Gb8(5919.91f, OCTAVE_8 + 7), + G8(6271.93f, OCTAVE_8 + 8), + Gs8(6644.88f, OCTAVE_8 + 9), + Ab8(6644.88f, OCTAVE_8 + 9), + A8(7040.00f, OCTAVE_8 + 10), + As8(7458.62f, OCTAVE_8 + 11), + Bb8(7458.62f, OCTAVE_8 + 11), + B8(7902.13f, OCTAVE_8 + 12), } @TinyLib( @@ -180,7 +190,7 @@ class NotesLib : TwoArgFunction() { val keys = LuaTable() Note.values().forEach { note -> - keys[note.name] = valueOf(note.ordinal) + keys[note.name] = valueOf(note.index) } arg2["notes"] = keys diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index d434380e..e76ae0db 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -46,7 +46,7 @@ class SfxLib( } abstract inner class WaveFunction : ThreeArgFunction() { - private val notes = Note.values() + private val notes = Note.values().associateBy { it.index } override fun call( @TinyArg( @@ -57,7 +57,7 @@ class SfxLib( @TinyArg("volume", description = "Volume express in percentage (between 0.0 and 1.0)") arg3: LuaValue, ): LuaValue { val note = if (arg1.isint()) { - notes[arg1.checkint()] + notes[arg1.checkint()] ?: return NIL } else { return NIL } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt index 04664c97..0138fa8d 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt @@ -83,12 +83,12 @@ interface Platform { * Create a SourceStream from the name of the resource. * Regarding the platform, the name can be adjusted. */ - fun createByteArrayStream(name: String): SourceStream + fun createByteArrayStream(name: String, canUseJarPrefix: Boolean = true): SourceStream /** * Create a SourceStream from an image from uncompressed data. */ - fun createImageStream(name: String): SourceStream + fun createImageStream(name: String, canUseJarPrefix: Boolean = true): SourceStream /** * Create a SourceStream from a midi file. diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt index 138da17f..65a7eb88 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt @@ -67,7 +67,7 @@ class GameScript( class State(val args: LuaValue) - private fun createLuaGlobals(forValidation: Boolean = false): Globals = Globals().apply { + private fun createLuaGlobals(customizeLuaGlobal: GameResourceAccess.(Globals) -> Unit, forValidation: Boolean = false): Globals = Globals().apply { val sprLib = SprLib(this@GameScript.gameOptions, this@GameScript.resourceAccess) load(TinyBaseLib(this@GameScript.resourceAccess)) @@ -90,20 +90,23 @@ class GameScript( load(sprLib) load(JuiceLib()) load(NotesLib()) + + this@GameScript.resourceAccess.customizeLuaGlobal(this) + LoadState.install(this) LuaC.install(this) } - suspend fun isValid(): Boolean { - with(createLuaGlobals(forValidation = true)) { + suspend fun isValid(customizeLuaGlobal: GameResourceAccess.(Globals) -> Unit): Boolean { + with(createLuaGlobals(customizeLuaGlobal, forValidation = true)) { load(content.decodeToString()).call() get("_init").nullIfNil()?.callSuspend(valueOf(gameOptions.width), valueOf(gameOptions.height)) } return true } - suspend fun evaluate() { - globals = createLuaGlobals() + suspend fun evaluate(customizeLuaGlobal: GameResourceAccess.(Globals) -> Unit) { + globals = createLuaGlobals(customizeLuaGlobal) evaluated = true exited = -1 diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/ResourceFactory.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/ResourceFactory.kt index 2fd5b04b..e58206f3 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/ResourceFactory.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/ResourceFactory.kt @@ -151,6 +151,8 @@ class ResourceFactory( fun bootscript(name: String, inputHandler: InputHandler, gameOptions: GameOptions) = script(0, name, inputHandler, gameOptions, BOOT_GAMESCRIPT) + private val protectedResources = setOf(BOOT_GAMESCRIPT, ENGINE_GAMESCRIPT, BOOT_SPRITESHEET) + private fun script( index: Int, name: String, @@ -159,7 +161,12 @@ class ResourceFactory( resourceType: ResourceType, ): Flow { var version = 0 - return vfs.watch(platform.createByteArrayStream(name)).map { content -> + return vfs.watch( + platform.createByteArrayStream( + name = name, + canUseJarPrefix = !protectedResources.contains(resourceType), + ), + ).map { content -> GameScript(version++, index, name, gameOptions, inputHandler, resourceType).apply { this.content = content } @@ -180,7 +187,12 @@ class ResourceFactory( private fun spritesheet(index: Int, name: String, resourceType: ResourceType): Flow { var version = 0 - return vfs.watch(platform.createImageStream(name)).map { imageData -> + return vfs.watch( + platform.createImageStream( + name = name, + canUseJarPrefix = !protectedResources.contains(resourceType), + ), + ).map { imageData -> val sheet = convertToColorIndex(imageData.data, imageData.width, imageData.height) SpriteSheet(version++, index, name, resourceType, sheet, imageData.width, imageData.height) }.onEach { diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index 1fe70b26..d3cf92c3 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -19,6 +19,8 @@ interface SoundManager { fun playNotes(notes: List, longestDuration: Seconds) + fun playSfx(notes: List) + fun mix(sample: Int, notes: List): Float { var result = 0f notes.forEach { diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index 4bcbfa9d..30cc538f 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -92,3 +92,9 @@ class PulseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGen return (ret / 6.0).toFloat() } } + +class SilenceWave(duration: Seconds) : WaveGenerator(Note.C0, duration, 1.0f) { + override fun generate(sample: Int): Float { + return 0f + } +} diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/MusicLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/MusicLibTest.kt new file mode 100644 index 00000000..ee315135 --- /dev/null +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/MusicLibTest.kt @@ -0,0 +1,20 @@ +package com.github.minigdx.tiny.lua + +import kotlin.test.Test +import kotlin.test.assertEquals + +class MusicLibTest { + + fun trim(str: String): String { + val lastIndex = str.lastIndexOf(')') + if (lastIndex < 0) return str + return str.substring(0, lastIndex + 2) + } + + @Test + fun trimMusic() { + val str = "*-*-sine(Eb)-*-*-" + + assertEquals("*-*-sine(Eb)-", trim(str)) + } +} diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt index bb3f32c0..01e90807 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt @@ -89,6 +89,7 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map } override fun playNotes(notes: List, longestDuration: Seconds) = Unit + override fun playSfx(notes: List) = Unit } } @@ -96,13 +97,13 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map return Dispatchers.Unconfined } - override fun createByteArrayStream(name: String): SourceStream { + override fun createByteArrayStream(name: String, canUseJarPrefix: Boolean): SourceStream { val data = (resources[name] as? String?)?.encodeToByteArray() ?: resources[name] as? ByteArray ?: throw IllegalStateException("$name is not a valid ByteArray.") return ObjectStream(data) } - override fun createImageStream(name: String): SourceStream { + override fun createImageStream(name: String, canUseJarPrefix: Boolean): SourceStream { val data = resources[name] as? ImageData ?: throw IllegalStateException("$name is not a valid ImageData.") return ObjectStream(data) } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index 0dba3258..60155bd7 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -61,6 +61,10 @@ class PicoAudioSoundMananger : SoundManager { return audioBuffer } + override fun playSfx(notes: List) { + TODO("Not yet implemented") + } + override fun playNotes(notes: List, longestDuration: Seconds) { if (notes.isEmpty()) return diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt index 80f66496..2672b6b3 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt @@ -91,11 +91,11 @@ class WebGlPlatform( return Dispatchers.Default } - override fun createByteArrayStream(name: String): SourceStream { + override fun createByteArrayStream(name: String, canUseJarPrefix: Boolean): SourceStream { return AjaxStream("$rootUrl/$name") } - override fun createImageStream(name: String): SourceStream { + override fun createImageStream(name: String, canUseJarPrefix: Boolean): SourceStream { return ImageDataStream("$rootUrl/$name") } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/InputStreamStream.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/InputStreamStream.kt index 6960cc11..40af9669 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/InputStreamStream.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/InputStreamStream.kt @@ -1,9 +1,13 @@ package com.github.minigdx.tiny.file +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.InputStream class InputStreamStream(private val source: InputStream) : SourceStream { override suspend fun read(): ByteArray { - return source.readAllBytes() + return withContext(Dispatchers.IO) { + source.readAllBytes() + } } } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt index 80bae2d8..b3701017 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt @@ -49,6 +49,7 @@ class GlfwPlatform( private val vfs: VirtualFileSystem, private val workdirectory: File, private val render: Render = GLRender(KglLwjgl, logger, gameOptions), + private val jarResourcePrefix: String = "", ) : Platform { private var window: Long = 0 @@ -308,8 +309,14 @@ class GlfwPlatform( return ImageData(result, width, height) } - override fun createByteArrayStream(name: String): SourceStream { - val fromJar = GlfwPlatform::class.java.getResourceAsStream("/$name") + override fun createByteArrayStream(name: String, canUseJarPrefix: Boolean): SourceStream { + val resourceName = if (canUseJarPrefix) { + "$jarResourcePrefix/$name" + } else { + "/$name" + } + + val fromJar = GlfwPlatform::class.java.getResourceAsStream(resourceName) return if (fromJar != null) { InputStreamStream(fromJar) } else { @@ -317,10 +324,10 @@ class GlfwPlatform( } } - override fun createImageStream(name: String): SourceStream { + override fun createImageStream(name: String, canUseJarPrefix: Boolean): SourceStream { return object : SourceStream { - private val delegate = createByteArrayStream(name) + private val delegate = createByteArrayStream(name, canUseJarPrefix) override suspend fun exists(): Boolean = delegate.exists() diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index dd6ac880..fa101693 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -97,6 +97,15 @@ class JavaMidiSoundManager : SoundManager { override fun playNotes(notes: List, longestDuration: Seconds) { if (notes.isEmpty()) return + val buffer = generateAudioBuffer(longestDuration, notes) + + bufferQueue.offer(buffer) + } + + private fun generateAudioBuffer( + longestDuration: Seconds, + notes: List, + ): ByteArray { val numSamples: Int = (SAMPLE_RATE * longestDuration).toInt() val buffer = ByteArray(numSamples * 2) val fadeOutIndex = getFadeOutIndex(longestDuration) @@ -111,7 +120,21 @@ class JavaMidiSoundManager : SoundManager { buffer[2 * i] = (result and 0xFF).toByte() buffer[2 * i + 1] = (result.toInt().shr(8) and 0xFF).toByte() } + return buffer + } + + override fun playSfx(notes: List) { + if (notes.isEmpty()) return + + val numSamples: Int = (SAMPLE_RATE * notes.first().duration * notes.size).toInt() + val sfxBuffer = ByteArray(numSamples * 2) + var currentIndex = 0 + notes.forEach { + val buffer = generateAudioBuffer(it.duration, listOf(it)) + buffer.copyInto(sfxBuffer, destinationOffset = currentIndex) + currentIndex += buffer.size + } - bufferQueue.offer(buffer) + bufferQueue.offer(sfxBuffer) } } diff --git a/tiny-web-editor/src/jsMain/kotlin/Main.kt b/tiny-web-editor/src/jsMain/kotlin/Main.kt index e8b5e649..1e39765f 100644 --- a/tiny-web-editor/src/jsMain/kotlin/Main.kt +++ b/tiny-web-editor/src/jsMain/kotlin/Main.kt @@ -174,7 +174,7 @@ class EditorWebGlPlatform(val delegate: Platform) : Platform { override fun io(): CoroutineDispatcher = delegate.io() - override fun createByteArrayStream(name: String): SourceStream { + override fun createByteArrayStream(name: String, canUseJarPrefix: Boolean): SourceStream { return if (name.startsWith("#")) { EditorStream(name) } else { @@ -182,6 +182,8 @@ class EditorWebGlPlatform(val delegate: Platform) : Platform { } } - override fun createImageStream(name: String): SourceStream = delegate.createImageStream(name) + override fun createImageStream(name: String, canUseJarPrefix: Boolean): SourceStream = delegate.createImageStream( + name, + ) override fun createSoundStream(name: String): SourceStream = delegate.createSoundStream(name) } From 0ea9b4c287da10d9333c0aacafc6c231d3a692e2 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Wed, 24 Jan 2024 19:44:18 +0100 Subject: [PATCH 10/48] Moving the custom "MusicLib" back into SfxLib --- .../minigdx/tiny/cli/command/RunCommand.kt | 4 +- .../minigdx/tiny/cli/command/SfxCommand.kt | 77 ------------------- tiny-cli/src/jvmMain/resources/sfx/game.lua | 2 +- .../github/minigdx/tiny/engine/GameEngine.kt | 8 ++ .../minigdx/tiny/engine/GameResourceAccess.kt | 5 ++ .../com/github/minigdx/tiny/lua/SfxLib.kt | 48 ++++++++++++ 6 files changed, 64 insertions(+), 80 deletions(-) diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt index ce80f11b..261cb31c 100644 --- a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt @@ -18,8 +18,8 @@ import java.io.File class RunCommand : CliktCommand(name = "run", help = "Run your game.") { val gameDirectory by argument(help = "The directory containing all game information") - .file(mustExist = true, canBeDir = true, canBeFile = false) - .default(File(".")) + .file(mustExist = true, canBeDir = true, canBeFile = false) + .default(File(".")) fun isOracleOrOpenJDK(): Boolean { val vendor = System.getProperty("java.vendor")?.lowercase() diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt index 280d3861..cf26ca02 100644 --- a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt @@ -4,91 +4,15 @@ import com.github.ajalt.clikt.core.Abort import com.github.ajalt.clikt.core.CliktCommand import com.github.minigdx.tiny.cli.config.GameParameters import com.github.minigdx.tiny.engine.GameEngine -import com.github.minigdx.tiny.engine.GameResourceAccess import com.github.minigdx.tiny.file.CommonVirtualFileSystem import com.github.minigdx.tiny.log.StdOutLogger -import com.github.minigdx.tiny.lua.Note import com.github.minigdx.tiny.lua.errorLine import com.github.minigdx.tiny.platform.glfw.GlfwPlatform import com.github.minigdx.tiny.render.LwjglGLRender -import com.github.minigdx.tiny.sound.NoiseWave -import com.github.minigdx.tiny.sound.PulseWave -import com.github.minigdx.tiny.sound.SilenceWave -import com.github.minigdx.tiny.sound.SineWave -import com.github.minigdx.tiny.sound.SoundManager -import com.github.minigdx.tiny.sound.TriangleWave import kotlinx.serialization.json.decodeFromStream -import org.luaj.vm2.Globals import org.luaj.vm2.LuaError -import org.luaj.vm2.LuaTable -import org.luaj.vm2.LuaValue -import org.luaj.vm2.lib.TwoArgFunction import java.io.File -class MusicLib(private val soundManager: SoundManager) : TwoArgFunction() { - override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { - val ctrl = LuaTable() - ctrl.set("play", play()) - arg2.set("music", ctrl) - arg2.get("package").get("loaded").set("music", ctrl) - return ctrl - } - - private fun extractNote(str: String): Note { - val note = str.substringAfter("(").substringBefore(")") - return Note.valueOf(note) - } - - private val acceptedTypes = setOf("sine", "noise", "pulse", "triangle") - - private fun extractWaveType(str: String): String? { - if (str == "*") return str - - val type = str.substringBefore("(") - return if (acceptedTypes.contains(type)) { - type - } else { - null - } - } - - inner class play : TwoArgFunction() { - - fun trim(str: String): String { - val lastIndex = str.lastIndexOf(')') - if (lastIndex < 0) return str - return str.substring(0, lastIndex + 2) - } - - override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { - val bpm = arg2.optint(120) - val duration = 60 / bpm.toFloat() / 4 - val score = arg1.optjstring("")!! - val parts = trim(score).split("-") - val waves = parts.mapNotNull { - val wave = extractWaveType(it) - when (wave) { - "*" -> SilenceWave(duration) - "sine" -> SineWave(extractNote(it), duration) - "triangle" -> TriangleWave(extractNote(it), duration) - "noise" -> NoiseWave(extractNote(it), duration) - "pulse" -> PulseWave(extractNote(it), duration) - else -> null - } - } - soundManager.playSfx(waves) - return NIL - } - } -} - -private val customizeGlobal: GameResourceAccess.(Globals) -> Unit = { global -> - val engine = (this as? GameEngine) - if (engine != null) { - global.load(MusicLib(engine.soundManager)) - } -} - class SfxCommand : CliktCommand(name = "sfx", help = "Start the SFX Editor") { fun isOracleOrOpenJDK(): Boolean { val vendor = System.getProperty("java.vendor")?.lowercase() @@ -132,7 +56,6 @@ class SfxCommand : CliktCommand(name = "sfx", help = "Start the SFX Editor") { ), vfs = vfs, logger = logger, - customizeGlobal, ).main() } catch (ex: Exception) { echo( diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index eb8b8776..e39e015f 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -74,7 +74,7 @@ function _update() score = score .. "*-" end end - music.play(score, 220) + sfx.sfx(score, 220) end local new_wave = current_wave diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt index 6ee434ba..204e9b11 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt @@ -95,6 +95,7 @@ class GameEngine( private val debugActions = mutableListOf() private val notes = mutableListOf() + private var sfx: List? = null private var longuestDuration: Seconds = 0f private lateinit var scripts: Array @@ -327,6 +328,9 @@ class GameEngine( notes.clear() longuestDuration = 0f + sfx?.run { soundManager.playSfx(this) } + sfx = null + // Fixed step simulation accumulator += delta if (accumulator >= REFRESH_LIMIT) { @@ -484,6 +488,10 @@ class GameEngine( notes.add(wave) } + override fun sfx(waves: List) { + sfx = waves + } + override fun script(name: String): GameScript? { return scripts .drop(1) // drop the _boot.lua diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt index 2539fb60..7de355a9 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt @@ -55,6 +55,11 @@ interface GameResourceAccess { */ fun note(wave: WaveGenerator) + /** + * Play the sfx represented by this list of waves. + */ + fun sfx(waves: List) + /** * Find a script by its name. */ diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index e76ae0db..679c72e9 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -11,6 +11,7 @@ import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.sound.NoiseWave import com.github.minigdx.tiny.sound.PulseWave import com.github.minigdx.tiny.sound.SawTooth +import com.github.minigdx.tiny.sound.SilenceWave import com.github.minigdx.tiny.sound.SineWave import com.github.minigdx.tiny.sound.SquareWave import com.github.minigdx.tiny.sound.TriangleWave @@ -40,6 +41,7 @@ class SfxLib( ctrl.set("noise", noise()) ctrl.set("pulse", pulse()) ctrl.set("saw", sawtooth()) + ctrl.set("sfx", sfx()) arg2.set("sfx", ctrl) arg2.get("package").get("loaded").set("sfx", ctrl) return ctrl @@ -159,6 +161,52 @@ class SfxLib( } } + private fun extractNote(str: String): Note { + val note = str.substringAfter("(").substringBefore(")") + return Note.valueOf(note) + } + + inner class sfx : TwoArgFunction() { + + private val acceptedTypes = setOf("sine", "noise", "pulse", "triangle") + private fun extractWaveType(str: String): String? { + if (str == "*") return str + + val type = str.substringBefore("(") + return if (acceptedTypes.contains(type)) { + type + } else { + null + } + } + + private fun trim(str: String): String { + val lastIndex = str.lastIndexOf(')') + if (lastIndex < 0) return str + return str.substring(0, lastIndex + 2) + } + + override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + val bpm = arg2.optint(120) + val duration = 60 / bpm.toFloat() / 4 + val score = arg1.optjstring("")!! + val parts = trim(score).split("-") + val waves = parts.mapNotNull { + val wave = extractWaveType(it) + when (wave) { + "*" -> SilenceWave(duration) + "sine" -> SineWave(extractNote(it), duration) + "triangle" -> TriangleWave(extractNote(it), duration) + "noise" -> NoiseWave(extractNote(it), duration) + "pulse" -> PulseWave(extractNote(it), duration) + else -> null + } + } + resourceAccess.sfx(waves) + return NIL + } + } + private fun canPlay(sound: Sound?): Sound? { return if (playSound) { sound From 53702c9312f4d95d15e27249877f605a04f05e59 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Wed, 24 Jan 2024 19:53:19 +0100 Subject: [PATCH 11/48] Support sfx on the web platform --- .../platform/webgl/PicoAudioSoundMananger.kt | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index 60155bd7..e7b16515 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -62,7 +62,31 @@ class PicoAudioSoundMananger : SoundManager { } override fun playSfx(notes: List) { - TODO("Not yet implemented") + if (notes.isEmpty()) return + + val numSamples: Int = (SAMPLE_RATE * notes.first().duration * notes.size).toInt() + val sfxBuffer = audioContext.createBuffer( + 1, + numSamples, + SAMPLE_RATE, + ) + + var currentIndex = 0 + val result = Float32Array(numSamples) + val channel = sfxBuffer.getChannelData(0) + + notes.forEach { + val buffer = toAudioBuffer(listOf(it), it.duration) + result.set(buffer.getChannelData(0), currentIndex) + currentIndex += buffer.getChannelData(0).length + } + + channel.set(result) + + val source = audioContext.createBufferSource() + source.buffer = sfxBuffer + source.connect(audioContext.destination) + source.start() } override fun playNotes(notes: List, longestDuration: Seconds) { From b926d658f48308a8ed1e50d995898c497d3f9d0b Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Thu, 25 Jan 2024 16:11:49 +0100 Subject: [PATCH 12/48] PoC of SFX editor --- tiny-cli/sfx.aseprite | Bin 0 -> 1193 bytes tiny-cli/src/jvmMain/resources/sfx/_tiny.json | 41 ++++++- tiny-cli/src/jvmMain/resources/sfx/game.lua | 97 ++++++++++------- tiny-cli/src/jvmMain/resources/sfx/mouse.lua | 8 +- tiny-cli/src/jvmMain/resources/sfx/sfx.png | Bin 0 -> 741 bytes .../src/jvmMain/resources/sfx/widgets.lua | 101 ++++++++++++++---- 6 files changed, 187 insertions(+), 60 deletions(-) create mode 100644 tiny-cli/sfx.aseprite create mode 100644 tiny-cli/src/jvmMain/resources/sfx/sfx.png diff --git a/tiny-cli/sfx.aseprite b/tiny-cli/sfx.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..5816f24d4c1d0ac8c2ae3d0a4be6794c6af9c9e4 GIT binary patch literal 1193 zcmZ3P6?5JmcI0JX;9yvIBW%;tpQbE1Cef=#GHk%K<^SS;@?Zb2`0xJL{?-4$|3IN9Kjfdu2N`&rFSsBt|IR<`KIgLX6|dR9 zGA2Ho(3SZ2JkVH%b&tzrCBRV!i8F*d>=|T$?s$9sAnzdq9@YT;Lkt~}0!}g(Mtsc$ zX`B`txWd&8j1H}c@RYLF^laV##Jg$fo@q;T84e_Eb@o1Jz4Y%|r|cau`5pbcSFLF0 zRsFm6?H)1TK>piX^YpgwzWX+>eE03U@5;9C-aBos?yN=gcJ4nf|4sU`xrFG~Y2UVQ z*?d1}>o41RH=pe|8vZx>hR)lKA3Z*>Sm>M%nwI|O?bqu!gs&&Q*m~n;?nmV{6TNO< zZ;q`p{kPEaDgU{pZ;JAms?W@P%wJh_eA0Il6ZLaPZpcl4yzzeT&o%%5&)K+s$-9l8 r?yQzPU3cOx`}~ru trigger.x and x < trigger.x + trigger.width and y > trigger.y and y < trigger.y + trigger.height then - trigger.active = not trigger.active + for i = 0, #waves - 1 do + local w = widgets.createButton({ + x = 10, + y = 10 + i * 16, + overlay = 16 + i, + data = { + wave = waves[i + 1] + }, + on_active_button = on_active_button + }) + + if i == 0 then + w.status = 2 end end end function _update() - mouse._update(widgets.on_click) + mouse._update(widgets.on_update, widgets.on_click) widgets._update() if ctrl.pressed(keys.space) then @@ -69,8 +92,8 @@ function _update() for f in all(faders) do if f.data ~= nil and f.data.note ~= nil then - score = score .. f.data.wave.."(" .. f.data.note .. ")-" - else + score = score .. f.data.wave .. "(" .. f.data.note .. ")-" + else score = score .. "*-" end end @@ -79,7 +102,7 @@ function _update() local new_wave = current_wave if ctrl.pressed(keys.up) then - for i=1,#waves do + for i = 1, #waves do if waves[i].type == current_wave.type then local next_index = (i % #waves) + 1 new_wave = waves[next_index] @@ -92,9 +115,9 @@ end function _draw() gfx.cls() + shape.gradient(0, 0, window.width, window.height, 2, 3) widgets._draw() + mouse._draw(current_wave.color) - mouse._draw() - - print(current_wave.type) + print(current_wave.type, 10, 2) end diff --git a/tiny-cli/src/jvmMain/resources/sfx/mouse.lua b/tiny-cli/src/jvmMain/resources/sfx/mouse.lua index e3d5fbef..4cadc01c 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/mouse.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/mouse.lua @@ -8,19 +8,21 @@ local Mouse = { local mouse = new(Mouse) -mouse._update = function(on_click) +mouse._update = function(on_update, on_click) local pos = ctrl.touch() mouse.x = pos.x mouse.y = pos.y + on_update(pos.x, pos.y) + local clicked = ctrl.touching(0) if clicked then on_click(pos.x, pos.y) end end -mouse._draw = function() - shape.circle(mouse.x, mouse.y, 2, 9) +mouse._draw = function(color) + shape.circle(mouse.x, mouse.y, 2, color) end return mouse \ No newline at end of file diff --git a/tiny-cli/src/jvmMain/resources/sfx/sfx.png b/tiny-cli/src/jvmMain/resources/sfx/sfx.png new file mode 100644 index 0000000000000000000000000000000000000000..c965e1e40d10c832dfa4ad036ac2d7167ffb39c7 GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G!U;i$lZxy-8q?;3=B;3JzX3_ zD(1Ysy*EoFP=f8j_Y?Qrx~3>jvi3;c-;&xbB6O*W(Jam}?_k)GLrNV>wx5(`Icn6p zT~UTxQug}I*mSePh-Z}viREebGoDqS=TG;VBqO7r&GzHl(_J<$wg!DS4}RRVulldi za`pDTT>0$3mu(sAWDP*%_CaCXnNGv7B= z&U)rAq{wjluONfz=cuA!ue?*s1YXDrgm=#hmp_&Kj(^MS{faCJo8#3hWD*L5t=B}$=JIgowukZI+sZQaa&1VTjl*>mf&R%lg>eu?bUt8V(&XaE9&n#6- zNMndLV+^0pG= x and w.y <= y and w.y + w.height + 12 >= y end +factory.on_update = function(x, y) + for f in all(buttons) do + if f.status == 1 then + f.status = 0 + end + + if f.status == 0 and inside_widget(f, x, y) then + f.status = 1 + end + end +end factory.on_click = function(x, y) for f in all(faders) do if inside_widget(f, x, y) then @@ -45,33 +70,71 @@ factory.on_click = function(x, y) f.on_value_update(f, value) end end + + local prec = nil + local current = nil + for f in all(buttons) do + if f.status == 2 then + prec = f + elseif f.status == 1 and inside_widget(f, x, y) then + current = f + end + end + -- active the current button and deactive the previous activated + if current ~= nil then + if prec ~= nil then + prec.status = 0 + end + current.status = 2 + current.on_active_button(current, prec) + end end factory._update = function(mouse) end -factory._draw = function() - for f in all(faders) do +function draw_fader(f) + local y = f.height - 4 - local y = f.height - 4 + if f.value then + y = f.height - ((f.value - f.min_value) / (f.max_value - f.min_value) * f.height) + end + local tipy = f.y + y + + if f.value > f.min_value then + local linex = f.x + f.width * 0.5 + gfx.dither(0xA5A5) + shape.line(linex, tipy, linex, f.y + f.height, 7) + gfx.dither() + shape.rectf(f.x, tipy, f.width, 4, f.tip_color) + else + shape.rectf(f.x, tipy, f.width, 4, f.disabled_color) + end - if f.value then - y = f.height - ((f.value - f.min_value) / (f.max_value - f.min_value) * f.height) - end - local tipy = f.y + y - - if f.value > f.min_value then - local linex = f.x + f.width * 0.5 - gfx.dither(0xA5A5) - shape.line(linex, tipy, linex, f.y + f.height, 7) - gfx.dither() - shape.rectf(f.x, tipy, f.width, 4, f.tip_color) - else - shape.rectf(f.x, tipy, f.width, 4, f.disabled_color) - end + print(f.label, f.x, f.y + f.height + 5) +end + +function draw_button(button) + local background = 0 + if button.status > 0 then + background = 1 + end + + spr.draw(background, button.x, button.y) + + if button.overlay ~= nil then + + spr.draw(button.overlay, button.x, button.y) + end +end +factory._draw = function() + for f in all(faders) do + draw_fader(f) + end - print(f.label, f.x, f.y + f.height + 5) + for b in all(buttons) do + draw_button(b) end end From 0b5d0899cb6455119a14b52e5ecbc57ef7e4ac97 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Fri, 26 Jan 2024 12:23:51 +0100 Subject: [PATCH 13/48] Play SFX sound from a file, on the JVM platform --- .../minigdx/tiny/cli/command/AddCommand.kt | 2 +- tiny-cli/src/jvmMain/resources/sfx/game.lua | 1 + .../tiny/file/SoundDataSourceStream.kt | 6 +- .../com/github/minigdx/tiny/lua/SfxLib.kt | 44 +++++++++------ .../github/minigdx/tiny/platform/Platform.kt | 4 +- .../github/minigdx/tiny/sound/SoundManager.kt | 6 +- .../tiny/platform/test/HeadlessPlatform.kt | 6 +- .../platform/webgl/PicoAudioSoundMananger.kt | 6 +- .../platform/glfw/JavaMidiSoundManager.kt | 56 ++++++++++++++++--- 9 files changed, 95 insertions(+), 36 deletions(-) diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/AddCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/AddCommand.kt index 5e738702..4ae8fc0b 100644 --- a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/AddCommand.kt +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/AddCommand.kt @@ -40,7 +40,7 @@ class AddCommand : CliktCommand(name = "add", help = "Add a resource to your gam // Add script gameParameters = gameParameters.addScript(r) "script" - } else if (r.endsWith("mid") || r.endsWith("midi")) { + } else if (r.endsWith("mid") || r.endsWith("midi") || r.endsWith("sfx")) { // Add midi gameParameters = gameParameters.addSound(r) "sound" diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 21f6fb01..1e04c1d8 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -97,6 +97,7 @@ function _update() score = score .. "*-" end end + debug.console(score) sfx.sfx(score, 220) end diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/SoundDataSourceStream.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/SoundDataSourceStream.kt index 81e11bd2..0f70b903 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/SoundDataSourceStream.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/SoundDataSourceStream.kt @@ -14,7 +14,11 @@ class SoundDataSourceStream( override suspend fun read(): SoundData { val bytes = delegate.read() - val sound = soundManager.createSound(bytes) + val sound = if (name.endsWith(".sfx")) { + soundManager.createSfxSound(bytes) + } else { + soundManager.createMidiSound(bytes) + } return SoundData(name, sound) } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index 679c72e9..742c3913 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -161,14 +161,30 @@ class SfxLib( } } - private fun extractNote(str: String): Note { - val note = str.substringAfter("(").substringBefore(")") - return Note.valueOf(note) + inner class sfx : TwoArgFunction() { + + override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + val bpm = arg2.optint(120) + val duration = 60 / bpm.toFloat() / 4 + val score = arg1.optjstring("")!! + val waves = convertScoreToWaves(score, duration) + resourceAccess.sfx(waves) + return NIL + } } - inner class sfx : TwoArgFunction() { + private fun canPlay(sound: Sound?): Sound? { + return if (playSound) { + sound + } else { + null + } + } + + companion object { private val acceptedTypes = setOf("sine", "noise", "pulse", "triangle") + private fun extractWaveType(str: String): String? { if (str == "*") return str @@ -180,16 +196,18 @@ class SfxLib( } } + private fun extractNote(str: String): Note { + val note = str.substringAfter("(").substringBefore(")") + return Note.valueOf(note) + } + private fun trim(str: String): String { val lastIndex = str.lastIndexOf(')') if (lastIndex < 0) return str return str.substring(0, lastIndex + 2) } - override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { - val bpm = arg2.optint(120) - val duration = 60 / bpm.toFloat() / 4 - val score = arg1.optjstring("")!! + fun convertScoreToWaves(score: String, duration: Seconds): List { val parts = trim(score).split("-") val waves = parts.mapNotNull { val wave = extractWaveType(it) @@ -202,16 +220,8 @@ class SfxLib( else -> null } } - resourceAccess.sfx(waves) - return NIL - } - } - private fun canPlay(sound: Sound?): Sound? { - return if (playSound) { - sound - } else { - null + return waves } } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt index 0138fa8d..7c51c337 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt @@ -7,7 +7,7 @@ import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.graphic.FrameBuffer import com.github.minigdx.tiny.input.InputHandler import com.github.minigdx.tiny.input.InputManager -import com.github.minigdx.tiny.sound.MidiSound +import com.github.minigdx.tiny.sound.Sound import com.github.minigdx.tiny.sound.SoundManager import kotlinx.coroutines.CoroutineDispatcher @@ -19,7 +19,7 @@ class ImageData( // Height of the Image val height: Pixel, ) -class SoundData(val name: String, val sound: MidiSound) +class SoundData(val name: String, val sound: Sound) interface Platform { /** diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index d3cf92c3..b73d861c 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -3,7 +3,7 @@ package com.github.minigdx.tiny.sound import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler -interface MidiSound { +interface Sound { fun play() fun loop() @@ -15,7 +15,9 @@ interface SoundManager { fun initSoundManager(inputHandler: InputHandler) - suspend fun createSound(data: ByteArray): MidiSound + suspend fun createSfxSound(bytes: ByteArray): Sound + + suspend fun createMidiSound(data: ByteArray): Sound fun playNotes(notes: List, longestDuration: Seconds) diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt index 01e90807..45a145c2 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt @@ -12,7 +12,7 @@ import com.github.minigdx.tiny.platform.Platform import com.github.minigdx.tiny.platform.RenderContext import com.github.minigdx.tiny.platform.SoundData import com.github.minigdx.tiny.platform.WindowManager -import com.github.minigdx.tiny.sound.MidiSound +import com.github.minigdx.tiny.sound.Sound import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.WaveGenerator import com.github.minigdx.tiny.util.MutableFixedSizeList @@ -78,8 +78,8 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map return object : SoundManager { override fun initSoundManager(inputHandler: InputHandler) = Unit - override suspend fun createSound(data: ByteArray): MidiSound { - return object : MidiSound { + override suspend fun createMidiSound(data: ByteArray): Sound { + return object : Sound { override fun play() = Unit override fun loop() = Unit diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index e7b16515..44404ebf 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -2,14 +2,14 @@ package com.github.minigdx.tiny.platform.webgl import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler -import com.github.minigdx.tiny.sound.MidiSound +import com.github.minigdx.tiny.sound.Sound import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE import com.github.minigdx.tiny.sound.WaveGenerator import org.khronos.webgl.Float32Array import org.khronos.webgl.set -class PicoAudioSound(val audio: dynamic, val smf: dynamic) : MidiSound { +class PicoAudioSound(val audio: dynamic, val smf: dynamic) : Sound { override fun play() { audio.init() audio.setData(smf) @@ -35,7 +35,7 @@ class PicoAudioSoundMananger : SoundManager { audioContext = AudioContext() } - override suspend fun createSound(data: ByteArray): MidiSound { + override suspend fun createMidiSound(data: ByteArray): Sound { val audio = js("var PicoAudio = require('picoaudio'); new PicoAudio.default()") val smf = audio.parseSMF(data) return PicoAudioSound(audio, smf) diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index fa101693..a9adcf2e 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -2,7 +2,8 @@ package com.github.minigdx.tiny.platform.glfw import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler -import com.github.minigdx.tiny.sound.MidiSound +import com.github.minigdx.tiny.lua.SfxLib +import com.github.minigdx.tiny.sound.Sound import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE import com.github.minigdx.tiny.sound.WaveGenerator @@ -13,10 +14,37 @@ import javax.sound.midi.MidiSystem import javax.sound.midi.Sequencer import javax.sound.midi.Sequencer.LOOP_CONTINUOUSLY import javax.sound.sampled.AudioFormat +import javax.sound.sampled.AudioInputStream import javax.sound.sampled.AudioSystem +import javax.sound.sampled.Clip import kotlin.experimental.and -class JavaMidiSound(private val data: ByteArray) : MidiSound { +class SfxSound(byteArray: ByteArray) : Sound { + + private val clip: Clip + + init { + val audioFormat = AudioFormat(SAMPLE_RATE.toFloat(), 16, 1, true, false) + val audioStream = AudioInputStream(ByteArrayInputStream(byteArray), audioFormat, byteArray.size.toLong()) + clip = AudioSystem.getClip() + clip.open(audioStream) + // audioStream.close() + } + override fun play() { + stop() + clip.start() + } + + override fun loop() { + clip.loop(LOOP_CONTINUOUSLY) + } + + override fun stop() { + clip.stop() + clip.framePosition = 0 + } +} +class JavaMidiSound(private val data: ByteArray) : Sound { private var sequencer: Sequencer? = null @@ -66,11 +94,11 @@ class JavaMidiSoundManager : SoundManager { val notesLine = AudioSystem.getSourceDataLine( AudioFormat( AudioFormat.Encoding.PCM_SIGNED, - SoundManager.SAMPLE_RATE.toFloat(), + SAMPLE_RATE.toFloat(), 16, 1, // TODO: set 2 to get Stereo 2, - SoundManager.SAMPLE_RATE.toFloat(), + SAMPLE_RATE.toFloat(), false, ), ) @@ -90,10 +118,19 @@ class JavaMidiSoundManager : SoundManager { backgroundAudio.start() } - override suspend fun createSound(data: ByteArray): MidiSound { + override suspend fun createMidiSound(data: ByteArray): Sound { return JavaMidiSound(data) } + override suspend fun createSfxSound(bytes: ByteArray): Sound { + val score = bytes.decodeToString() + val duration = 60f / 120f / 4.0f + val waves = SfxLib.convertScoreToWaves(score, duration) + + val buffer = generateScoreBuffer(waves) + return SfxSound(buffer) + } + override fun playNotes(notes: List, longestDuration: Seconds) { if (notes.isEmpty()) return @@ -126,6 +163,12 @@ class JavaMidiSoundManager : SoundManager { override fun playSfx(notes: List) { if (notes.isEmpty()) return + val sfxBuffer = generateScoreBuffer(notes) + + bufferQueue.offer(sfxBuffer) + } + + private fun generateScoreBuffer(notes: List): ByteArray { val numSamples: Int = (SAMPLE_RATE * notes.first().duration * notes.size).toInt() val sfxBuffer = ByteArray(numSamples * 2) var currentIndex = 0 @@ -134,7 +177,6 @@ class JavaMidiSoundManager : SoundManager { buffer.copyInto(sfxBuffer, destinationOffset = currentIndex) currentIndex += buffer.size } - - bufferQueue.offer(sfxBuffer) + return sfxBuffer } } From 61e5d425d466db70aa3bb8d8cd067da20ed6d17e Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Fri, 26 Jan 2024 13:49:48 +0100 Subject: [PATCH 14/48] play sfx also on the web platform --- .../platform/webgl/PicoAudioSoundMananger.kt | 63 ++++++++++++++++--- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index 44404ebf..a12b47d0 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -2,6 +2,7 @@ package com.github.minigdx.tiny.platform.webgl import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler +import com.github.minigdx.tiny.lua.SfxLib import com.github.minigdx.tiny.sound.Sound import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE @@ -27,6 +28,27 @@ class PicoAudioSound(val audio: dynamic, val smf: dynamic) : Sound { } } +class SfxSound( + private val buffer: Float32Array, + private val picoAudioSoundMananger: PicoAudioSoundMananger, +) : Sound { + + private var currentSource: AudioBufferSourceNode? = null + + override fun play() { + stop() + currentSource = picoAudioSoundMananger.playSfxBuffer(buffer) + } + + override fun loop() { + currentSource = picoAudioSoundMananger.playSfxBuffer(buffer, loop = true) + } + + override fun stop() { + currentSource?.stop() + } +} + class PicoAudioSoundMananger : SoundManager { lateinit var audioContext: AudioContext @@ -35,6 +57,14 @@ class PicoAudioSoundMananger : SoundManager { audioContext = AudioContext() } + override suspend fun createSfxSound(bytes: ByteArray): Sound { + val score = bytes.decodeToString() + val duration = 60f / 120f / 4.0f + val waves = SfxLib.convertScoreToWaves(score, duration) + val buffer = generateSfxBuffer((waves.size * duration * SAMPLE_RATE).toInt(), waves) + return SfxSound(buffer, this) + } + override suspend fun createMidiSound(data: ByteArray): Sound { val audio = js("var PicoAudio = require('picoaudio'); new PicoAudio.default()") val smf = audio.parseSMF(data) @@ -65,28 +95,43 @@ class PicoAudioSoundMananger : SoundManager { if (notes.isEmpty()) return val numSamples: Int = (SAMPLE_RATE * notes.first().duration * notes.size).toInt() + + val result = generateSfxBuffer(numSamples, notes) + + playSfxBuffer(result) + } + + internal fun playSfxBuffer(result: Float32Array, loop: Boolean = false): AudioBufferSourceNode { val sfxBuffer = audioContext.createBuffer( 1, - numSamples, + result.length, SAMPLE_RATE, ) + val channel = sfxBuffer.getChannelData(0) + channel.set(result) + + val source = audioContext.createBufferSource() + source.buffer = sfxBuffer + source.connect(audioContext.destination) + source.loop = loop + source.start() + return source + } + + internal fun generateSfxBuffer( + numSamples: Int, + notes: List, + ): Float32Array { var currentIndex = 0 val result = Float32Array(numSamples) - val channel = sfxBuffer.getChannelData(0) notes.forEach { val buffer = toAudioBuffer(listOf(it), it.duration) result.set(buffer.getChannelData(0), currentIndex) currentIndex += buffer.getChannelData(0).length } - - channel.set(result) - - val source = audioContext.createBufferSource() - source.buffer = sfxBuffer - source.connect(audioContext.destination) - source.start() + return result } override fun playNotes(notes: List, longestDuration: Seconds) { From 0df399706c976e29a0b7fb9d4c2dfcbcc184b9e7 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Fri, 26 Jan 2024 14:38:22 +0100 Subject: [PATCH 15/48] Add Shutdown hook to close the application correctly --- .../com/github/minigdx/tiny/cli/command/RunCommand.kt | 11 +++++++++-- .../github/minigdx/tiny/cli/command/ServeCommand.kt | 7 +++++++ .../com/github/minigdx/tiny/cli/command/SfxCommand.kt | 11 +++++++++-- .../com/github/minigdx/tiny/engine/GameEngine.kt | 1 + .../com/github/minigdx/tiny/sound/SoundManager.kt | 2 ++ .../tiny/platform/glfw/JavaMidiSoundManager.kt | 9 ++++++++- 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt index 261cb31c..21efc590 100644 --- a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt @@ -48,12 +48,19 @@ class RunCommand : CliktCommand(name = "run", help = "Run your game.") { val logger = StdOutLogger("tiny-cli") val vfs = CommonVirtualFileSystem() val gameOption = gameParameters.toGameOptions() - GameEngine( + val gameEngine = GameEngine( gameOptions = gameOption, platform = GlfwPlatform(gameOption, logger, vfs, gameDirectory, LwjglGLRender(logger, gameOption)), vfs = vfs, logger = logger, - ).main() + ) + Runtime.getRuntime().addShutdownHook( + Thread { + gameEngine.end() + echo("\uD83D\uDC4B See you soon!") + }, + ) + gameEngine.main() } catch (ex: Exception) { echo("\uD83E\uDDE8 An unexpected exception occurred. The application will stop. It might be a bug in Tiny. If so, please report it.") when (ex) { diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/ServeCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/ServeCommand.kt index ebd0440c..4296a3f4 100644 --- a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/ServeCommand.kt +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/ServeCommand.kt @@ -106,6 +106,13 @@ class ServeCommand : CliktCommand(name = "serve", help = "Run your game as a web val server = embeddedServer(Netty, port = port, module = method) echo("\uD83D\uDE80 Try your game on http://localhost:$port with your browser.") + + Runtime.getRuntime().addShutdownHook( + Thread { + server.stop() + echo("\uD83D\uDC4B See you soon!") + }, + ) // Starts the server and waits for the engine to stop and exits. server.start(wait = true) // start a browser to the address diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt index cf26ca02..5a9d426c 100644 --- a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt @@ -44,7 +44,7 @@ class SfxCommand : CliktCommand(name = "sfx", help = "Start the SFX Editor") { val logger = StdOutLogger("tiny-cli") val vfs = CommonVirtualFileSystem() val gameOption = gameParameters.toGameOptions() - GameEngine( + val gameEngine = GameEngine( gameOptions = gameOption, platform = GlfwPlatform( gameOption, @@ -56,7 +56,14 @@ class SfxCommand : CliktCommand(name = "sfx", help = "Start the SFX Editor") { ), vfs = vfs, logger = logger, - ).main() + ) + Runtime.getRuntime().addShutdownHook( + Thread { + gameEngine.end() + echo("\uD83D\uDC4B See you soon!") + }, + ) + gameEngine.main() } catch (ex: Exception) { echo( "\uD83E\uDDE8 An unexpected exception occurred. " + diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt index 204e9b11..4c77d44f 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt @@ -504,6 +504,7 @@ class GameEngine( override fun end() { sounds.forEach { it?.stop() } + soundManager.destroy() } companion object { diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index b73d861c..cf5a90a4 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -15,6 +15,8 @@ interface SoundManager { fun initSoundManager(inputHandler: InputHandler) + fun destroy() = Unit + suspend fun createSfxSound(bytes: ByteArray): Sound suspend fun createMidiSound(data: ByteArray): Sound diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index a9adcf2e..431689ae 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -108,7 +108,9 @@ class JavaMidiSoundManager : SoundManager { while (isActive) { val nextBuffer = bufferQueue.take() - notesLine.write(nextBuffer, 0, nextBuffer.size) + if (isActive) { + notesLine.write(nextBuffer, 0, nextBuffer.size) + } } notesLine.close() } @@ -179,4 +181,9 @@ class JavaMidiSoundManager : SoundManager { } return sfxBuffer } + + override fun destroy() { + isActive = false + bufferQueue.offer(ByteArray(0)) // unlock the thread to quit + } } From 2d395515751fb87fe0a125d463d67f504a87b1ce Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Fri, 26 Jan 2024 14:53:54 +0100 Subject: [PATCH 16/48] Fix tests compilation after update on the sound part --- .../kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt | 1 + .../kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt | 1 + .../minigdx/tiny/platform/test/HeadlessPlatform.kt | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt index 1f9e4400..736c85a0 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt @@ -23,6 +23,7 @@ class GfxLibTest { override fun sound(index: Int): Sound? = null override fun script(name: String): GameScript? = null override fun note(wave: WaveGenerator) = Unit + override fun sfx(waves: List) = Unit } @Test diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt index 541be145..1debf387 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt @@ -38,6 +38,7 @@ class StdLibTest { override fun sound(index: Int): Sound? = null override fun script(name: String): GameScript? = null override fun note(wave: WaveGenerator) = Unit + override fun sfx(waves: List) = Unit } private val gameOptions = GameOptions( diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt index 45a145c2..2053b10a 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt @@ -77,6 +77,15 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map override fun initSoundManager(inputHandler: InputHandler): SoundManager { return object : SoundManager { override fun initSoundManager(inputHandler: InputHandler) = Unit + override suspend fun createSfxSound(bytes: ByteArray): Sound { + return object : Sound { + override fun play() = Unit + + override fun loop() = Unit + + override fun stop() = Unit + } + } override suspend fun createMidiSound(data: ByteArray): Sound { return object : Sound { From a3c96167f499ec8323b5b14086e32db8d02c7e83 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Sun, 28 Jan 2024 11:50:50 +0100 Subject: [PATCH 17/48] Support of tabs in the SFX Editor Add support in it for saw/square pulse. --- tiny-cli/sfx.aseprite | Bin 1193 -> 1484 bytes tiny-cli/src/jvmMain/resources/sfx/game.lua | 109 ++++++++++++++---- tiny-cli/src/jvmMain/resources/sfx/mouse.lua | 11 +- tiny-cli/src/jvmMain/resources/sfx/sfx.png | Bin 741 -> 1091 bytes .../src/jvmMain/resources/sfx/widgets.lua | 104 ++++++++++++++++- .../com/github/minigdx/tiny/lua/SfxLib.kt | 8 +- .../minigdx/tiny/sound/WaveGenerator.kt | 2 +- 7 files changed, 205 insertions(+), 29 deletions(-) diff --git a/tiny-cli/sfx.aseprite b/tiny-cli/sfx.aseprite index 5816f24d4c1d0ac8c2ae3d0a4be6794c6af9c9e4..a89622413c277fd0d92795649abe614ac50c1e98 100644 GIT binary patch delta 601 zcmV-f0;c_`3Cs%u%mtAGegaGdk%Ik`U;!AjmjU_#e>(yI00kfd000000RI9200000 z0002q0385$ob8%1PQ)+}0P_(_gb;5*2vJb-3_4mKLPNtRpx`SMG`zsb2R`|X?d&=x zo8)GsZESCnOER*(xnUTFlZzYI62mW3^Nne`_FiHAH*%hgEt@{=RnE3)Em4zggFbOXdS@2JggT%e*@l4$HF<>TQ?d9{MhhcExh}|(mC8) zFR@1&@6vsrBtq`geCvA_u=Wl2)=lg~E&Mn?{_ggw`Tg*@Q}eCwm&e*O+}j3{bz(d| z_@j+p`IcDwg`WTb0006WuiY*HU><^RsduIJ0`6&WakN~@`HSo3zWY2AAIsf8yIl0T ze-?m0XuPX97`xWuU2v|4d)~Q{ce(bQV|WJ=+;Hi9s1q09UFzBt_kvehze+CR9W)U8 zR&y`gSMx6I`v>nzj4{_wT3b_UKk6$7`OJaPfOPG-XQAGCYBiTy{iM~oT|VAH nBdS_UZatmd66#0n88I(jmt;4F>sitF$Pb delta 310 zcmV-60m=T%3#kbLsRWS%egY{3k%IjJ1CwC^7qgcE`T>6!0RR96AOZjY000300ssI2 z0000006zd60C=43*TD_KFboAyFTwzbSr9@TI5G}{aApZOunJcuFfziZSVyFfsD}+D{jKf17EMtP+qG^kx4Yf#Zg;!e z-R^d`yS;ywoh_Q8p1HrzANCv5Hy9Q5miD)_v)@v+`aPc6=D5XI{ae^9?XmGl@B}z4 z%TktS{O$GE*b~=b=(X6{ZSf8p65N} I32+SQbMBRvjQ{`u diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 1e04c1d8..50ef448b 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -11,7 +11,7 @@ local labels = {"C0", "Db0", "D0", "Eb0", "E0", "F0", "Gb0", "G0", "Ab0", "A0", local waves = {{ type = "sine", - color = 9, + color = 9 }, { type = "noise", color = 4 @@ -21,15 +21,24 @@ local waves = {{ }, { type = "triangle", color = 13 +}, { + type = "saw", + color = 11 +}, { + type = "square", + color = 15 }} +local faders = {} local current_wave = waves[1] function on_fader_update(fader, value) fader.value = math.ceil(value) fader.data = { note = labels[fader.value], - wave = current_wave.type + wave = current_wave.type, + value = fader.value, + color = current_wave.color } fader.label = labels[fader.value] fader.tip_color = current_wave.color @@ -39,7 +48,44 @@ function on_active_button(current, prec) current_wave = current.data.wave end -local faders = {} +function on_active_tab(current, prec) + local data = {} + -- save the current score + for f in all(faders) do + table.insert(data, { + wave = f.data.wave, + note = f.data.note, + value = f.value, + color = f.tip_color + }) + end + prec.data = data + + -- restore the previous score + if current.data ~= nil then + local data = current.data + for k,f in ipairs(faders) do + f.data = data[k] + f.value = data[k].value + f.label = labels[f.value] + f.tip_color = data[k].color + end + else + -- no data, reset to 0 + for k,f in ipairs(faders) do + f.value = 0 + f.label = "" + f.data = { + wave = "", + note = 0, + value = 0, + color = 0 + } + -- f.tip_color = data[k].color + end + end +end + local window = { width = 0, @@ -50,10 +96,11 @@ function _init(w, h) window.width = w window.height = h + -- faders for i = 1, 32 do local f = widgets.createFader({ x = 10 + 16 + i * 12, - y = 16 + 10 + 2, + y = 16 + 16 + 2, height = 256 - 18, label = labels[0], value = 0, @@ -66,10 +113,11 @@ function _init(w, h) table.insert(faders, f) end + -- buttons for i = 0, #waves - 1 do local w = widgets.createButton({ x = 10, - y = 10 + i * 16, + y = 16 + i * 16, overlay = 16 + i, data = { wave = waves[i + 1] @@ -81,22 +129,45 @@ function _init(w, h) w.status = 2 end end + + -- tabs + local tab = widgets.createTab({ + x = 0, + width = 2 * 16 + 8, + status = 1, + label = "hello", + on_active_tab = on_active_tab + }) + + widgets.createTab({ + x = tab.width, + width = 24, + status = 0, + on_active_tab = on_active_tab, + new_tab = true + }) +end + +function generate_score() + local score = "" + + for f in all(faders) do + if f.data ~= nil and f.data.note ~= nil then + score = score .. f.data.wave .. "(" .. f.data.note .. ")-" + else + score = score .. "*-" + end + end + return score end function _update() - mouse._update(widgets.on_update, widgets.on_click) + mouse._update(widgets.on_update, widgets.on_click, widgets.on_clicked) widgets._update() if ctrl.pressed(keys.space) then - local score = "" - for f in all(faders) do - if f.data ~= nil and f.data.note ~= nil then - score = score .. f.data.wave .. "(" .. f.data.note .. ")-" - else - score = score .. "*-" - end - end + local score = generate_score() debug.console(score) sfx.sfx(score, 220) end @@ -112,13 +183,11 @@ function _update() current_wave = new_wave end end - +-- function _draw() - gfx.cls() - - shape.gradient(0, 0, window.width, window.height, 2, 3) + gfx.cls(2) + -- background for tabs + shape.rectf(0, 0, window.width, 8, 1) widgets._draw() mouse._draw(current_wave.color) - - print(current_wave.type, 10, 2) end diff --git a/tiny-cli/src/jvmMain/resources/sfx/mouse.lua b/tiny-cli/src/jvmMain/resources/sfx/mouse.lua index 4cadc01c..30944ba7 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/mouse.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/mouse.lua @@ -8,17 +8,22 @@ local Mouse = { local mouse = new(Mouse) -mouse._update = function(on_update, on_click) +mouse._update = function(on_update, on_click, on_clicked) local pos = ctrl.touch() mouse.x = pos.x mouse.y = pos.y on_update(pos.x, pos.y) - local clicked = ctrl.touching(0) - if clicked then + local clicking = ctrl.touching(0) + if clicking then on_click(pos.x, pos.y) end + + local clicked = ctrl.touched(0) + if clicked then + on_clicked(pos.x, pos.y) + end end mouse._draw = function(color) diff --git a/tiny-cli/src/jvmMain/resources/sfx/sfx.png b/tiny-cli/src/jvmMain/resources/sfx/sfx.png index c965e1e40d10c832dfa4ad036ac2d7167ffb39c7..f04dff881870dfaa487ebefa21b0b27d00c6f3cb 100644 GIT binary patch delta 839 zcmV-N1GxO<1;YrCFnKKC?6$PqOX2kaNI_)UmKB*MD_yG@hjMvy(WycDYx> zi?{FM=U*d5L@d+3loNI5W1js!8c&Kj2P|hao}~A$A4WvP{y{nSc9*?~h{J1_jtDeS-h}>xV@R0}KIXSXUXY+}}>iJ|V68i^tb05Yadl8@3f+7Z z`>3SgkBF!uB5vQhIVb??;9a(TP9+9^9})!s06Co=r#L;{=#m7y7Pr?{RDLs~-GhoKBJQ z{1#wNGN^-oIvFgY4*pdnf%M_Z%>B8T54$82VjZ zJ%69707~{%0!VR`s(4XV@uKQV0=*N&tl_~5wSV>aLz95uXIFe40@T4@|6eD8=C-!a zMa0~azV*FEo32v7>%DrY&{gjD9S#f${=Q{?{dsc(6s@hNpWFM_z5MjwS=-iRx)Nxs z4BE#0^|r40fG%yx!Mp&a==?Fz^iZI+_umlB;XqqOpzMbkdQA*A*8;Tl^qJQQEZXxg z@*v(KQ~;E0p~rx|d;Pxi0c+GY7tqza>-pJcZtnk?#X*zt0o;-T1sFgE{{ci)uzL!i R><9n=002ovPDHLkV1gerq_Y43 delta 485 zcmVZ<0h@`j49R$TPg|{Ib3mF~P*{2fTU_7%VIe$Gl%_Td0RR&w0ARud08E$wfC&=-Fk!Rg=ehhLXZhRT zcc}-)Q7+HV>Zi}MLB-FWeovC@Qvc@tM=t}xNJ^4io}UeRowsS;UF!h=fc-2*E!X=N!P6Fen!(C7{kkkg2*7rK z1whTb*39>^d79==5+MNF{Sg2)^ICFKN^Z)g5a=BcSBsigAIf9y544-VAp~HvUnh8) z=5KyCA<(|o^}i&)8+@Ivd###HYyD04nq@*)^Ea9hSUq= x and w.y <= y and w.y + w.height + 12 >= y + return w.x <= x and x <= w.x + w.width and w.y <= y and y <= w.y + w.height end factory.on_update = function(x, y) @@ -62,7 +80,9 @@ factory.on_update = function(x, y) end end end + factory.on_click = function(x, y) + -- on click faders for f in all(faders) do if inside_widget(f, x, y) then local percent = math.max(0.0, 1.0 - ((y - f.y) / f.height)) @@ -70,7 +90,11 @@ factory.on_click = function(x, y) f.on_value_update(f, value) end end +end + +factory.on_clicked = function(x, y) + -- on click buttons local prec = nil local current = nil for f in all(buttons) do @@ -88,9 +112,84 @@ factory.on_click = function(x, y) current.status = 2 current.on_active_button(current, prec) end + + -- on click tab + local new_active = nil + local current_active = nil + for t in all(tabs) do + if t.status == 1 then + current_active = t + elseif inside_widget(t, x, y) and t.status == 0 then + new_active = t + end + + end + + if new_active ~= nil then + if new_active.new_tab then + new_active.width = 2 * 16 + 8 + new_active.label = "new_sfx" + new_active.new_tab = false + + if #tabs < 12 then + factory.createTab({ + width = 24, + new_tab = true, + x = new_active.x + new_active.width, + on_active_tab = new_active.on_active_tab + }) + table.insert(tabs, t) + end + + end + current_active.status = 0 + new_active.status = 1 + new_active.on_active_tab(new_active, current_active) + end end factory._update = function(mouse) + +end + +function draw_tabs() + local active_tab = tabs[1] + for index = #tabs, 1, -1 do + local v = tabs[index] + if v.status == 0 then + draw_tab(v) + else + active_tab = v + end + end + + draw_tab(active_tab) +end + +function draw_tab(tab) + local offset = tab.status * 8 + + -- body + local time = math.floor(tab.width / 16) + local rest = tab.width % 16 + for i = 0, time - 1 do + spr.sdraw(tab.x + (i) * 16, 0, 80, offset, 16, 8) + + end + + spr.sdraw(tab.x + (time) * 16, 0, 80, offset, rest, 8) + + -- right + spr.sdraw(tab.x + tab.width, 0, 96, offset, 8, 8) + + local center = tab.width * 0.5 - #tab.label * 0.5 * 4 + + print(tab.label, tab.x + center, tab.y + 2) + + -- left + if tab.status == 1 then + spr.sdraw(tab.x - 8, 0, 64, 8, 8, 8) + end end @@ -124,7 +223,6 @@ function draw_button(button) spr.draw(background, button.x, button.y) if button.overlay ~= nil then - spr.draw(button.overlay, button.x, button.y) end end @@ -136,6 +234,8 @@ factory._draw = function() for b in all(buttons) do draw_button(b) end + + draw_tabs() end return factory diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index 742c3913..5cf45d0b 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -10,7 +10,7 @@ import com.github.minigdx.tiny.engine.GameResourceAccess import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.sound.NoiseWave import com.github.minigdx.tiny.sound.PulseWave -import com.github.minigdx.tiny.sound.SawTooth +import com.github.minigdx.tiny.sound.SawToothWave import com.github.minigdx.tiny.sound.SilenceWave import com.github.minigdx.tiny.sound.SineWave import com.github.minigdx.tiny.sound.SquareWave @@ -80,7 +80,7 @@ class SfxLib( @TinyFunction("Generate and play a sawtooth wave sound.") inner class sawtooth : WaveFunction() { - override fun wave(note: Note, duration: Seconds, volume: Percent) = SawTooth(note, duration, volume) + override fun wave(note: Note, duration: Seconds, volume: Percent) = SawToothWave(note, duration, volume) } @TinyFunction("Generate and play a square wave sound.") @@ -183,7 +183,7 @@ class SfxLib( companion object { - private val acceptedTypes = setOf("sine", "noise", "pulse", "triangle") + private val acceptedTypes = setOf("sine", "noise", "pulse", "triangle", "saw", "square") private fun extractWaveType(str: String): String? { if (str == "*") return str @@ -215,6 +215,8 @@ class SfxLib( "*" -> SilenceWave(duration) "sine" -> SineWave(extractNote(it), duration) "triangle" -> TriangleWave(extractNote(it), duration) + "square" -> SquareWave(extractNote(it), duration) + "saw" -> SawToothWave(extractNote(it), duration) "noise" -> NoiseWave(extractNote(it), duration) "pulse" -> PulseWave(extractNote(it), duration) else -> null diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index 30cc538f..b66bbe39 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -36,7 +36,7 @@ sealed class WaveGenerator(note: Note, val duration: Seconds, val volume: Percen } } -class SawTooth(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { +class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { return (2 * (angle(sample) / TWO_PI)) } From fedd8b890284dbbd2d9ff5daa4c84bc41d058d68 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Sun, 28 Jan 2024 12:12:15 +0100 Subject: [PATCH 18/48] Fix issue when generating screenshot: The previous code was using a buffer that was not updated anymore. Instead, it should use the color buffer which is now using only color index --- .../com/github/minigdx/tiny/graphic/FrameBuffer.kt | 2 -- .../minigdx/tiny/platform/glfw/GlfwPlatform.kt | 13 ++++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt index a1b52b77..7f459fdc 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt @@ -78,8 +78,6 @@ class FrameBuffer( internal val colorIndexBuffer: PixelArray = PixelArray(width, height, PixelFormat.INDEX) - internal var buffer: ByteArray = ByteArray(height * width * PixelFormat.RGBA) - internal val clipper: Clipper = Clipper(width, height) internal val blender = Blender(gamePalette) diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt index b3701017..f7a4a71c 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt @@ -266,17 +266,16 @@ class GlfwPlatform( val width = buffer.width val height = buffer.height val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) - val colorData = buffer.buffer for (y in 0 until height) { for (x in 0 until width) { - val i = y * width + x - val r = colorData[i * 4 + 0].toInt() and 0xff - val g = colorData[i * 4 + 1].toInt() and 0xff - val b = colorData[i * 4 + 2].toInt() and 0xff - val a = colorData[i * 4 + 3].toInt() and 0xff + val colorData = buffer.gamePalette.getRGBA(buffer.pixel(x, y)) + val r = colorData[0].toInt() and 0xff + val g = colorData[1].toInt() and 0xff + val b = colorData[2].toInt() and 0xff + val a = colorData[3].toInt() and 0xff val color = (a shl 24) or (r shl 16) or (g shl 8) or b - image.setRGB(y, x, color) + image.setRGB(x, y, color) } } From d5e1ee1ea12b766cbcc669c5eff5fa751acd0ee3 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 29 Jan 2024 11:12:52 +0100 Subject: [PATCH 19/48] Make the noise waveform volume change regarding the note used. So the note can have an effect on this waveform. --- .../kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index b66bbe39..ca5bd6d8 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -70,14 +70,14 @@ class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : Wave } } -class NoiseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { +class NoiseWave(private val note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { private var lastNoise = 0.0f override fun generate(sample: Int): Float { val white = Random.nextFloat() * 2 - 1 val brown = (lastNoise + (0.02f * white)) / 1.02f lastNoise = brown - return brown * 3.5f + return brown * 3.5f * note.index / Note.B8.index } } From 328919155ca2f997ee1c5201c544e6518cdb227c Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 29 Jan 2024 14:53:41 +0100 Subject: [PATCH 20/48] Refactor the sound manager by putting most of the generation in the common code instead of platform code. --- .../github/minigdx/tiny/sound/SoundManager.kt | 81 ++++++++++++++++--- .../minigdx/tiny/sound/WaveGenerator.kt | 12 ++- .../tiny/platform/test/HeadlessPlatform.kt | 7 +- .../platform/webgl/PicoAudioSoundMananger.kt | 65 +++------------ .../platform/glfw/JavaMidiSoundManager.kt | 50 ++---------- 5 files changed, 103 insertions(+), 112 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index cf5a90a4..0ee81a00 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -11,25 +11,88 @@ interface Sound { fun stop() } -interface SoundManager { +abstract class SoundManager { - fun initSoundManager(inputHandler: InputHandler) + abstract fun initSoundManager(inputHandler: InputHandler) - fun destroy() = Unit + open fun destroy() = Unit - suspend fun createSfxSound(bytes: ByteArray): Sound + abstract suspend fun createSfxSound(bytes: ByteArray): Sound - suspend fun createMidiSound(data: ByteArray): Sound + abstract suspend fun createMidiSound(data: ByteArray): Sound - fun playNotes(notes: List, longestDuration: Seconds) + fun playNotes(notes: List, longestDuration: Seconds) { + if (notes.isEmpty()) return - fun playSfx(notes: List) + val result = createNotesBuffer(longestDuration, notes) + playBuffer(result) + } + + fun playSfx(notes: List) { + if (notes.isEmpty()) return + + val result = createScoreBuffer(notes) + + playBuffer(result) + } + + protected fun createNotesBuffer( + longestDuration: Seconds, + notes: List, + ): FloatArray { + val numSamples: Int = (SAMPLE_RATE * longestDuration).toInt() + val fadeOutIndex = getFadeOutIndex(longestDuration) + val offsetSample = 0 + val result = FloatArray(numSamples) + for (i in 0 until numSamples) { + val sampleMixed = mix(i, notes, offsetSample) + val sample = fadeOut(sampleMixed, i, fadeOutIndex, numSamples) + result[i] = sample + } + return result + } + + protected fun createScoreBuffer(notes: List): FloatArray { + val duration = notes.first().duration + val noteSamples = (SAMPLE_RATE * duration).toInt() + val noteFadeOutIndex = getFadeOutIndex(duration) // fade out index within the note. + val numSamples: Int = (noteSamples * notes.size) + var offsetSample = 0 + var currentIndex = 0 + val result = FloatArray(numSamples) + + notes.forEachIndexed { index, note -> + val mixedNotes = listOf(note) + val nextNoteIsDifferent = index == notes.size - 1 || notes[index + 1].isSame(note) + + for (i in 0 until noteSamples) { + val sampleMixed = mix(i, mixedNotes, offsetSample) + // Last note or different kind of note after + val sample = if (nextNoteIsDifferent || notes[index + 1].isSilence) { + fadeOut(sampleMixed, i, noteFadeOutIndex, numSamples) + } else { + sampleMixed + } + result[currentIndex++] = sample + offsetSample++ + } + if (nextNoteIsDifferent) { + offsetSample = 0 + } + } + return result + } + + /** + * @param buffer byte array representing the sound. Each sample is represented with a float from -1.0f to 1.0f + */ + abstract fun playBuffer(buffer: FloatArray) - fun mix(sample: Int, notes: List): Float { + private fun mix(sample: Int, notes: List, offsetSample: Int = 0): Float { var result = 0f notes.forEach { if (it.accept(sample)) { - val sampleValue = it.generate(sample) * it.volume + val sampleValue = it.generate(sample + offsetSample) * it.volume result += sampleValue } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index ca5bd6d8..e08e4f8a 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -8,7 +8,7 @@ import kotlin.math.abs import kotlin.math.sin import kotlin.random.Random -sealed class WaveGenerator(note: Note, val duration: Seconds, val volume: Percent) { +sealed class WaveGenerator(val note: Note, val duration: Seconds, val volume: Percent) { val period = SAMPLE_RATE.toFloat() / note.frequency @@ -30,6 +30,12 @@ sealed class WaveGenerator(note: Note, val duration: Seconds, val volume: Percen return (TWO_PI * sample) / period } + fun isSame(other: WaveGenerator): Boolean { + return (other.note == this.note && this::class == other::class) + } + + open val isSilence: Boolean = false + companion object { internal const val PI = kotlin.math.PI.toFloat() internal const val TWO_PI = 2.0f * PI @@ -70,7 +76,7 @@ class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : Wave } } -class NoiseWave(private val note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { +class NoiseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { private var lastNoise = 0.0f override fun generate(sample: Int): Float { @@ -97,4 +103,6 @@ class SilenceWave(duration: Seconds) : WaveGenerator(Note.C0, duration, 1.0f) { override fun generate(sample: Int): Float { return 0f } + + override val isSilence: Boolean = true } diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt index 2053b10a..30ada99f 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt @@ -1,6 +1,5 @@ package com.github.minigdx.tiny.platform.test -import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.engine.GameLoop import com.github.minigdx.tiny.engine.GameOptions import com.github.minigdx.tiny.file.SourceStream @@ -14,7 +13,6 @@ import com.github.minigdx.tiny.platform.SoundData import com.github.minigdx.tiny.platform.WindowManager import com.github.minigdx.tiny.sound.Sound import com.github.minigdx.tiny.sound.SoundManager -import com.github.minigdx.tiny.sound.WaveGenerator import com.github.minigdx.tiny.util.MutableFixedSizeList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -75,7 +73,7 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map override fun initInputManager(): InputManager = input override fun initSoundManager(inputHandler: InputHandler): SoundManager { - return object : SoundManager { + return object : SoundManager() { override fun initSoundManager(inputHandler: InputHandler) = Unit override suspend fun createSfxSound(bytes: ByteArray): Sound { return object : Sound { @@ -97,8 +95,7 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map } } - override fun playNotes(notes: List, longestDuration: Seconds) = Unit - override fun playSfx(notes: List) = Unit + override fun playBuffer(buffer: FloatArray) = Unit } } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index a12b47d0..1ce0e6e8 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -1,12 +1,10 @@ package com.github.minigdx.tiny.platform.webgl -import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler import com.github.minigdx.tiny.lua.SfxLib import com.github.minigdx.tiny.sound.Sound import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE -import com.github.minigdx.tiny.sound.WaveGenerator import org.khronos.webgl.Float32Array import org.khronos.webgl.set @@ -49,7 +47,7 @@ class SfxSound( } } -class PicoAudioSoundMananger : SoundManager { +class PicoAudioSoundMananger : SoundManager() { lateinit var audioContext: AudioContext @@ -61,7 +59,7 @@ class PicoAudioSoundMananger : SoundManager { val score = bytes.decodeToString() val duration = 60f / 120f / 4.0f val waves = SfxLib.convertScoreToWaves(score, duration) - val buffer = generateSfxBuffer((waves.size * duration * SAMPLE_RATE).toInt(), waves) + val buffer = convertBuffer(createScoreBuffer(waves)) return SfxSound(buffer, this) } @@ -71,34 +69,17 @@ class PicoAudioSoundMananger : SoundManager { return PicoAudioSound(audio, smf) } - private fun toAudioBuffer(notes: List, longestDuration: Seconds): AudioBuffer { - val numSamples = (longestDuration * SAMPLE_RATE).toInt() - val fadeOutIndex = getFadeOutIndex(longestDuration) - - val audioBuffer = audioContext.createBuffer( - 1, - numSamples, - SAMPLE_RATE, - ) - val channel = audioBuffer.getChannelData(0) - - val result = Float32Array(numSamples) - (0 until numSamples).forEach { index -> - val signal = fadeOut(mix(index, notes), index, fadeOutIndex, numSamples) - result[index] = signal - } - channel.set(result) - return audioBuffer + override fun playBuffer(buffer: FloatArray) { + val result = convertBuffer(buffer) + playSfxBuffer(result) } - override fun playSfx(notes: List) { - if (notes.isEmpty()) return - - val numSamples: Int = (SAMPLE_RATE * notes.first().duration * notes.size).toInt() - - val result = generateSfxBuffer(numSamples, notes) - - playSfxBuffer(result) + private fun convertBuffer(buffer: FloatArray): Float32Array { + val result = Float32Array(buffer.size) + buffer.forEachIndexed { index, byte -> + result[index] = byte + } + return result } internal fun playSfxBuffer(result: Float32Array, loop: Boolean = false): AudioBufferSourceNode { @@ -118,28 +99,4 @@ class PicoAudioSoundMananger : SoundManager { source.start() return source } - - internal fun generateSfxBuffer( - numSamples: Int, - notes: List, - ): Float32Array { - var currentIndex = 0 - val result = Float32Array(numSamples) - - notes.forEach { - val buffer = toAudioBuffer(listOf(it), it.duration) - result.set(buffer.getChannelData(0), currentIndex) - currentIndex += buffer.getChannelData(0).length - } - return result - } - - override fun playNotes(notes: List, longestDuration: Seconds) { - if (notes.isEmpty()) return - - val source = audioContext.createBufferSource() - source.buffer = toAudioBuffer(notes, longestDuration) - source.connect(audioContext.destination) - source.start() - } } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index 431689ae..f4a532f1 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -1,12 +1,10 @@ package com.github.minigdx.tiny.platform.glfw -import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler import com.github.minigdx.tiny.lua.SfxLib import com.github.minigdx.tiny.sound.Sound import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE -import com.github.minigdx.tiny.sound.WaveGenerator import java.io.ByteArrayInputStream import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.BlockingQueue @@ -28,7 +26,6 @@ class SfxSound(byteArray: ByteArray) : Sound { val audioStream = AudioInputStream(ByteArrayInputStream(byteArray), audioFormat, byteArray.size.toLong()) clip = AudioSystem.getClip() clip.open(audioStream) - // audioStream.close() } override fun play() { stop() @@ -82,7 +79,7 @@ class JavaMidiSound(private val data: ByteArray) : Sound { } } -class JavaMidiSoundManager : SoundManager { +class JavaMidiSoundManager : SoundManager() { // When closing the application, switch isActive to false to stop the background thread. private var isActive = true @@ -129,59 +126,28 @@ class JavaMidiSoundManager : SoundManager { val duration = 60f / 120f / 4.0f val waves = SfxLib.convertScoreToWaves(score, duration) - val buffer = generateScoreBuffer(waves) + val buffer = convertBuffer(createScoreBuffer(waves)) return SfxSound(buffer) } - override fun playNotes(notes: List, longestDuration: Seconds) { - if (notes.isEmpty()) return - - val buffer = generateAudioBuffer(longestDuration, notes) - - bufferQueue.offer(buffer) + override fun playBuffer(buffer: FloatArray) { + bufferQueue.offer(convertBuffer(buffer)) } - private fun generateAudioBuffer( - longestDuration: Seconds, - notes: List, + private fun convertBuffer( + audioBuffer: FloatArray, ): ByteArray { - val numSamples: Int = (SAMPLE_RATE * longestDuration).toInt() - val buffer = ByteArray(numSamples * 2) - val fadeOutIndex = getFadeOutIndex(longestDuration) - - for (i in 0 until numSamples) { - val sample = fadeOut(mix(i, notes), i, fadeOutIndex, numSamples) - + val buffer = ByteArray(audioBuffer.size * 2) + audioBuffer.forEachIndexed { i, sample -> val sampleValue: Float = (sample * Short.MAX_VALUE) val clippedValue = sampleValue.coerceIn(Short.MIN_VALUE.toFloat(), Short.MAX_VALUE.toFloat()) val result = clippedValue.toInt().toShort() - buffer[2 * i] = (result and 0xFF).toByte() buffer[2 * i + 1] = (result.toInt().shr(8) and 0xFF).toByte() } return buffer } - override fun playSfx(notes: List) { - if (notes.isEmpty()) return - - val sfxBuffer = generateScoreBuffer(notes) - - bufferQueue.offer(sfxBuffer) - } - - private fun generateScoreBuffer(notes: List): ByteArray { - val numSamples: Int = (SAMPLE_RATE * notes.first().duration * notes.size).toInt() - val sfxBuffer = ByteArray(numSamples * 2) - var currentIndex = 0 - notes.forEach { - val buffer = generateAudioBuffer(it.duration, listOf(it)) - buffer.copyInto(sfxBuffer, destinationOffset = currentIndex) - currentIndex += buffer.size - } - return sfxBuffer - } - override fun destroy() { isActive = false bufferQueue.offer(ByteArray(0)) // unlock the thread to quit From 77da23fc65e875f5955966c5104dbd4afd55d998 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 29 Jan 2024 14:54:02 +0100 Subject: [PATCH 21/48] Better UX by setting at 0 the fader value when clicking under it. --- tiny-cli/src/jvmMain/resources/sfx/widgets.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua index 951d47e1..a6c1c976 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua @@ -84,7 +84,13 @@ end factory.on_click = function(x, y) -- on click faders for f in all(faders) do - if inside_widget(f, x, y) then + local box = { + x = f.x, + y = f.y, + width = f.width, + height = f.height + 12 + } + if inside_widget(box, x, y) then local percent = math.max(0.0, 1.0 - ((y - f.y) / f.height)) local value = percent * (f.max_value - f.min_value) + f.min_value f.on_value_update(f, value) From 4bcecbf678d74d65e256e1df59c1b991be6f20b7 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 29 Jan 2024 15:22:44 +0100 Subject: [PATCH 22/48] Adjust the triangle and sawtooth sound generation --- .../com/github/minigdx/tiny/sound/WaveGenerator.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index e08e4f8a..2bd8b603 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -44,7 +44,9 @@ sealed class WaveGenerator(val note: Note, val duration: Seconds, val volume: Pe class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { - return (2 * (angle(sample) / TWO_PI)) + val angle: Float = (angle(sample) / TWO_PI) % period + val phase = (angle * 2f) - 1f + return phase } } @@ -67,11 +69,12 @@ class SquareWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGe class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { - val angle = angle(sample) - return if (angle < PI) { - 2 * angle / PI + val angle: Float = (angle(sample) / TWO_PI) % period + val phase = (angle * 2f) - 1f + return if (phase < 0) { + phase + 1f } else { - 2 * (PI - angle) / PI + 1 + 1f - phase } } } From 6637d0d94ef2034b1a0c68c5fabf9eaab8dc13e7 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 29 Jan 2024 23:06:18 +0100 Subject: [PATCH 23/48] Add crossover between notes --- .../github/minigdx/tiny/sound/SoundManager.kt | 54 +++++++++++-------- .../minigdx/tiny/sound/WaveGenerator.kt | 20 ++++++- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index 0ee81a00..5a65d939 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -45,7 +45,7 @@ abstract class SoundManager { val offsetSample = 0 val result = FloatArray(numSamples) for (i in 0 until numSamples) { - val sampleMixed = mix(i, notes, offsetSample) + val sampleMixed = mix(i, notes) val sample = fadeOut(sampleMixed, i, fadeOutIndex, numSamples) result[i] = sample } @@ -54,31 +54,43 @@ abstract class SoundManager { protected fun createScoreBuffer(notes: List): FloatArray { val duration = notes.first().duration - val noteSamples = (SAMPLE_RATE * duration).toInt() - val noteFadeOutIndex = getFadeOutIndex(duration) // fade out index within the note. - val numSamples: Int = (noteSamples * notes.size) var offsetSample = 0 var currentIndex = 0 - val result = FloatArray(numSamples) - notes.forEachIndexed { index, note -> - val mixedNotes = listOf(note) - val nextNoteIsDifferent = index == notes.size - 1 || notes[index + 1].isSame(note) + fun merge(head: WaveGenerator, tail: List): List { + if (tail.isEmpty()) { + return listOf(head) + } + + val next = tail.first() + return if (next.isSame(head)) { + merge(head.copy(head.duration + next.duration, head.volume), tail.drop(1)) + } else { + listOf(head) + merge(next, tail.drop(1)) + } + } + val mergedNotes = merge(notes.first(), notes.drop(1)) + SilenceWave(0.1f) + + var prec: WaveGenerator? = null + var lastSample = 0 + val result = FloatArray((mergedNotes.sumOf { it.duration.toDouble() } * SAMPLE_RATE).toInt()) + mergedNotes.forEachIndexed { index, note -> + val crossover = (0.05f * SAMPLE_RATE).toInt() + val mixedNotes = listOf(note) + val noteSamples = (SAMPLE_RATE * note.duration).toInt() for (i in 0 until noteSamples) { - val sampleMixed = mix(i, mixedNotes, offsetSample) - // Last note or different kind of note after - val sample = if (nextNoteIsDifferent || notes[index + 1].isSilence) { - fadeOut(sampleMixed, i, noteFadeOutIndex, numSamples) - } else { - sampleMixed + var sampleMixed = mix(i, mixedNotes) + + // crossover + if (prec != null && i <= crossover) { + sampleMixed = (sampleMixed + fadeOut(prec!!.generate(lastSample + i), lastSample + i, lastSample, lastSample + crossover)) } - result[currentIndex++] = sample - offsetSample++ - } - if (nextNoteIsDifferent) { - offsetSample = 0 + result[currentIndex++] = sampleMixed } + + prec = note + lastSample = noteSamples } return result } @@ -88,11 +100,11 @@ abstract class SoundManager { */ abstract fun playBuffer(buffer: FloatArray) - private fun mix(sample: Int, notes: List, offsetSample: Int = 0): Float { + private fun mix(sample: Int, notes: List): Float { var result = 0f notes.forEach { if (it.accept(sample)) { - val sampleValue = it.generate(sample + offsetSample) * it.volume + val sampleValue = it.generate(sample) * it.volume result += sampleValue } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index 2bd8b603..e69f479f 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -14,6 +14,8 @@ sealed class WaveGenerator(val note: Note, val duration: Seconds, val volume: Pe val numberOfSample = SAMPLE_RATE * duration + open val isSilence: Boolean = false + /** * Return a value between -1.0 and 1.0 */ @@ -34,7 +36,7 @@ sealed class WaveGenerator(val note: Note, val duration: Seconds, val volume: Pe return (other.note == this.note && this::class == other::class) } - open val isSilence: Boolean = false + abstract fun copy(duration: Seconds, volume: Percent): WaveGenerator companion object { internal const val PI = kotlin.math.PI.toFloat() @@ -48,12 +50,16 @@ class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : Wave val phase = (angle * 2f) - 1f return phase } + + override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SawToothWave(note, duration, volume) } class SineWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { return sin(angle(sample)) } + + override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SineWave(note, duration, volume) } class SquareWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -65,6 +71,8 @@ class SquareWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGe -1f } } + + override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SquareWave(note, duration, volume) } class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -77,6 +85,8 @@ class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : Wave 1f - phase } } + + override fun copy(duration: Seconds, volume: Percent): WaveGenerator = TriangleWave(note, duration, volume) } class NoiseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -88,6 +98,8 @@ class NoiseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGen lastNoise = brown return brown * 3.5f * note.index / Note.B8.index } + + override fun copy(duration: Seconds, volume: Percent): WaveGenerator = NoiseWave(note, duration, volume) } class PulseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -100,12 +112,16 @@ class PulseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGen val ret = abs(4.0 * u - 2.0) - abs(8.0 * t - 4.0) return (ret / 6.0).toFloat() } + + override fun copy(duration: Seconds, volume: Percent): WaveGenerator = PulseWave(note, duration, volume) } class SilenceWave(duration: Seconds) : WaveGenerator(Note.C0, duration, 1.0f) { + + override val isSilence: Boolean = true override fun generate(sample: Int): Float { return 0f } - override val isSilence: Boolean = true + override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SilenceWave(duration) } From cce30e30f0bb1d0fcbd79f74ed8c63232e7c17f7 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 29 Jan 2024 23:35:17 +0100 Subject: [PATCH 24/48] rework the triangle and sawtooth wave form --- .../com/github/minigdx/tiny/sound/WaveGenerator.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index e69f479f..616d5da5 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -46,7 +46,7 @@ sealed class WaveGenerator(val note: Note, val duration: Seconds, val volume: Pe class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { - val angle: Float = (angle(sample) / TWO_PI) % period + val angle: Float = sin(angle(sample)) val phase = (angle * 2f) - 1f return phase } @@ -77,13 +77,9 @@ class SquareWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGe class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { - val angle: Float = (angle(sample) / TWO_PI) % period - val phase = (angle * 2f) - 1f - return if (phase < 0) { - phase + 1f - } else { - 1f - phase - } + val angle: Float = sin(angle(sample)) + val phase = (angle + 1.0) % 1.0 // Normalize sinValue to the range [0, 1] + return (if (phase < 0.5) 4.0 * phase - 1.0 else 3.0 - 4.0 * phase).toFloat() } override fun copy(duration: Seconds, volume: Percent): WaveGenerator = TriangleWave(note, duration, volume) From 0c3f9a3e7a987d6da39d08a04e5af470cc7f1a9b Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 29 Jan 2024 23:41:43 +0100 Subject: [PATCH 25/48] Remove useless variables --- .../kotlin/com/github/minigdx/tiny/sound/SoundManager.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index 5a65d939..38474eab 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -42,7 +42,6 @@ abstract class SoundManager { ): FloatArray { val numSamples: Int = (SAMPLE_RATE * longestDuration).toInt() val fadeOutIndex = getFadeOutIndex(longestDuration) - val offsetSample = 0 val result = FloatArray(numSamples) for (i in 0 until numSamples) { val sampleMixed = mix(i, notes) @@ -53,8 +52,6 @@ abstract class SoundManager { } protected fun createScoreBuffer(notes: List): FloatArray { - val duration = notes.first().duration - var offsetSample = 0 var currentIndex = 0 fun merge(head: WaveGenerator, tail: List): List { @@ -75,7 +72,7 @@ abstract class SoundManager { var prec: WaveGenerator? = null var lastSample = 0 val result = FloatArray((mergedNotes.sumOf { it.duration.toDouble() } * SAMPLE_RATE).toInt()) - mergedNotes.forEachIndexed { index, note -> + mergedNotes.forEach { note -> val crossover = (0.05f * SAMPLE_RATE).toInt() val mixedNotes = listOf(note) val noteSamples = (SAMPLE_RATE * note.duration).toInt() From d93e64376db0fa04eb384f5edaeac62e5423bcb6 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Tue, 30 Jan 2024 21:56:05 +0100 Subject: [PATCH 26/48] Introduce workspace. A workspace can contains local files that can be loaded/saved. --- tiny-cli/sfx.aseprite | Bin 1484 -> 1573 bytes .../minigdx/tiny/cli/command/SfxCommand.kt | 30 ++++- tiny-cli/src/jvmMain/resources/sfx/game.lua | 115 ++++++++++++++---- tiny-cli/src/jvmMain/resources/sfx/sfx.png | Bin 1091 -> 1234 bytes .../src/jvmMain/resources/sfx/widgets.lua | 20 +-- .../com/github/minigdx/tiny/file/LocalFile.kt | 11 ++ .../github/minigdx/tiny/lua/WorkspaceLib.kt | 108 ++++++++++++++++ .../github/minigdx/tiny/platform/Platform.kt | 3 + .../minigdx/tiny/resources/GameScript.kt | 4 + .../minigdx/tiny/resources/ResourceFactory.kt | 2 +- .../tiny/platform/test/HeadlessPlatform.kt | 10 ++ .../github/minigdx/tiny/file/JsLocalFile.kt | 20 +++ .../tiny/platform/webgl/WebGlPlatform.kt | 14 +++ .../github/minigdx/tiny/file/JvmLocalFile.kt | 22 ++++ .../tiny/platform/glfw/GlfwPlatform.kt | 4 + tiny-web-editor/src/jsMain/kotlin/Main.kt | 2 + 16 files changed, 327 insertions(+), 38 deletions(-) create mode 100644 tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/LocalFile.kt create mode 100644 tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/WorkspaceLib.kt create mode 100644 tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt create mode 100644 tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/JvmLocalFile.kt diff --git a/tiny-cli/sfx.aseprite b/tiny-cli/sfx.aseprite index a89622413c277fd0d92795649abe614ac50c1e98..374b8054f1df0e615b24ca3c5e3c49362a1fe3c3 100644 GIT binary patch delta 682 zcmV;b0#*IY3#AMKB?gfKegUPife-_KlmY+%1t0yrn4;a=BJ=jHXy`>zjCgq*mKmi#S$+G2s- zaoiu<#2{?|IuNd9?B7e*HP#ItNFshV-Ip{5>2s#u!M(!)-ia{x4(=TW@J_@K-od^3 zNxh@@dbu?bcxUe%+?yZj0$uaSjf3!REbrjnyp+649i(;tt9O>q5bzx0a7aCXcY5Fr z;E;2mZ&>YI?v+w4<|2$!TmcM&{^0`p$`2D>3 z9JK8RCPLy}#X;IvH2o$z*TcQwT*R4^wxtezZ_tJTK>zo~|OU@;3S#>PS zZA;c)O8ukOhiJVewbESK^QrV)9sdtU;adUw^Z0{(wu}!$f(i6@cv^J-d ztgr0lYsbLUIYyh&->g&TXu0Mb!1i$Go_`GLU7)t+Qd>XKI=9R3;hoSq>Rzr~c`9II zL~AX%`?++xArj}7wsC0oj@e$4|8G&81{fXYy5)bN$vZ*&h4)<qJEaH Q_|BT;U3B2wH}^=zy0@NUbpQYW delta 592 zcmV-W0I$9n=L&GPa;42g~yuio@KKYF8>^df!c_mky*u~yp$eZ9&M_r_j(ahG!tOdA zPHkc#_W(NJpJjNz$DV5#>pjp!ICgw4Z7k%SGxZMlwgcX&F!v7kwgKL$ScG@Dw|-LZ z;=Nw3CW3eN&f(triCu_m9=UlC@0R5q?yZ-Scd3K%*kkq1@*4u*A+|&60p7KPHy|PB zK*_t*8@xM&ISZs{9l*OhtQSFl1Kv%?!a3YqHyQ{0*zjL1y!*k@Iow+>u}2#3(tV#K zLhjUj>w6Zk_6_&eP3%K0{5U`U?)Iwr{qVU{^R4ff$J#U8+Xj+#VmvPaOr%g6BpoJ>e>|df>&9;N-p9ZG!Xk%b1&Oh^DgcC z2k%ObwYV31l{!{icj6sr$+;wLtIlP)zNG$A+S8@&rEz0!Y22%MR-5Nad%02@K0*nx zcQLiOO<7~MIBN%z@B=bnUrkq276FHJ4ibq}91yKHfnis#;5KKbQ6! eqH%7lPXgY-EC}nC#{%8~%n#qvA;&ij2KjUq$Rv0G diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt index 5a9d426c..ed68f6f6 100644 --- a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt @@ -2,10 +2,15 @@ package com.github.minigdx.tiny.cli.command import com.github.ajalt.clikt.core.Abort import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.default +import com.github.ajalt.clikt.parameters.types.file import com.github.minigdx.tiny.cli.config.GameParameters import com.github.minigdx.tiny.engine.GameEngine import com.github.minigdx.tiny.file.CommonVirtualFileSystem +import com.github.minigdx.tiny.file.JvmLocalFile import com.github.minigdx.tiny.log.StdOutLogger +import com.github.minigdx.tiny.lua.WorkspaceLib import com.github.minigdx.tiny.lua.errorLine import com.github.minigdx.tiny.platform.glfw.GlfwPlatform import com.github.minigdx.tiny.render.LwjglGLRender @@ -14,6 +19,11 @@ import org.luaj.vm2.LuaError import java.io.File class SfxCommand : CliktCommand(name = "sfx", help = "Start the SFX Editor") { + + val gameDirectory by argument(help = "The directory containing all game information") + .file(mustExist = true, canBeDir = true, canBeFile = false) + .default(File(".")) + fun isOracleOrOpenJDK(): Boolean { val vendor = System.getProperty("java.vendor")?.lowercase() return vendor?.contains("oracle") == true || vendor?.contains("eclipse") == true || vendor?.contains("openjdk") == true @@ -39,19 +49,29 @@ class SfxCommand : CliktCommand(name = "sfx", help = "Start the SFX Editor") { ) throw Abort() } - val gameParameters = GameParameters.JSON.decodeFromStream(configFile) + val commandParameters = GameParameters.JSON.decodeFromStream(configFile) + + val gameConfig = gameDirectory.resolve("_tiny.json") + if (gameConfig.exists()) { + val parameters = GameParameters.read(gameConfig) + WorkspaceLib.DEFAULT = parameters.toGameOptions().sounds.map { + JvmLocalFile(it, gameDirectory) + } + } else { + WorkspaceLib.DEFAULT = listOf(JvmLocalFile("sfx1.sfx", workingDirectory = gameDirectory)) + } val logger = StdOutLogger("tiny-cli") val vfs = CommonVirtualFileSystem() - val gameOption = gameParameters.toGameOptions() + val commandOptions = commandParameters.toGameOptions() val gameEngine = GameEngine( - gameOptions = gameOption, + gameOptions = commandOptions, platform = GlfwPlatform( - gameOption, + commandOptions, logger, vfs, File("."), - LwjglGLRender(logger, gameOption), + LwjglGLRender(logger, commandOptions), jarResourcePrefix = "/sfx", ), vfs = vfs, diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 50ef448b..66a2a621 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -48,6 +48,8 @@ function on_active_button(current, prec) current_wave = current.data.wave end +local active_tab = nil + function on_active_tab(current, prec) local data = {} -- save the current score @@ -56,7 +58,7 @@ function on_active_tab(current, prec) wave = f.data.wave, note = f.data.note, value = f.value, - color = f.tip_color + color = f.tip_color }) end prec.data = data @@ -64,7 +66,7 @@ function on_active_tab(current, prec) -- restore the previous score if current.data ~= nil then local data = current.data - for k,f in ipairs(faders) do + for k, f in ipairs(faders) do f.data = data[k] f.value = data[k].value f.label = labels[f.value] @@ -72,7 +74,7 @@ function on_active_tab(current, prec) end else -- no data, reset to 0 - for k,f in ipairs(faders) do + for k, f in ipairs(faders) do f.value = 0 f.label = "" f.data = { @@ -81,21 +83,58 @@ function on_active_tab(current, prec) value = 0, color = 0 } - -- f.tip_color = data[k].color end end -end + active_tab = current + debug.console(active_tab.label) +end local window = { width = 0, height = 0 } + +function on_new_tab(tab) + local filename = ws.create("sfx", "sfx") + tab.label = filename +end + +function on_play_button() + local score = generate_score() + sfx.sfx(score, 220) +end + +function on_save_button() + local score = generate_score() + ws.save(active_tab.label, score) +end + function _init(w, h) + widgets.on_new_tab = on_new_tab window.width = w window.height = h + widgets.createButton({ + x = 10, + y = 16, + overlay = 22, + grouped = false, + on_active_button = on_play_button + }) + + widgets.createButton({ + x = 10, + y = 16 + 2 + 16, + overlay = 23, + data = { + save = true + }, + grouped = false, + on_active_button = on_save_button + }) + -- faders for i = 1, 32 do local f = widgets.createFader({ @@ -114,10 +153,10 @@ function _init(w, h) end -- buttons - for i = 0, #waves - 1 do + for i = #waves - 1, 0, -1 do local w = widgets.createButton({ x = 10, - y = 16 + i * 16, + y = 250 - i * 16, overlay = 16 + i, data = { wave = waves[i + 1] @@ -131,16 +170,42 @@ function _init(w, h) end -- tabs - local tab = widgets.createTab({ - x = 0, - width = 2 * 16 + 8, - status = 1, - label = "hello", - on_active_tab = on_active_tab - }) + + local files = ws.list() + + local tabs = {} + local new_tab_x = 0 + if #files > 0 then + for w in all(files) do + local tab = widgets.createTab({ + x = 0, + width = 2 * 16 + 8, + status = 0, + label = w, + on_active_tab = on_active_tab + }) + table.insert(tabs, tab) + new_tab_x = new_tab_x + tab.width + end + else + local file = ws.create("sfx", "sfx") + local tab = widgets.createTab({ + x = 0, + width = 2 * 16 + 8, + status = 0, + label = file, + on_active_tab = on_active_tab + }) + + table.insert(tabs, tab) + new_tab_x = new_tab_x + tab.width + end + + tabs[1].status = 1 + active_tab = tabs[1] widgets.createTab({ - x = tab.width, + x = new_tab_x, width = 24, status = 0, on_active_tab = on_active_tab, @@ -173,21 +238,23 @@ function _update() end local new_wave = current_wave - if ctrl.pressed(keys.up) then - for i = 1, #waves do - if waves[i].type == current_wave.type then - local next_index = (i % #waves) + 1 - new_wave = waves[next_index] - end - end - current_wave = new_wave - end end -- function _draw() gfx.cls(2) -- background for tabs shape.rectf(0, 0, window.width, 8, 1) + -- octave limits + local per_octave = math.floor((256 - 18) / 9) -- height / nb octaves + for octave = 9, 0, -1 do + local y = 34 + (256 - 18) - octave * per_octave + gfx.dither(0x1010) + shape.line(36, y - 2, 36 + 32 * 12, y - 2, 3) + gfx.dither() + print(";6bOpLBP> z9~{0vdL>z>=h6GTuP%#-=&kfq)bQ^ghuI|Bw=DHm`mJreBY($$3CUw&R=(Gz-bz0m z>}|%{^~;qS9=(1O-+mVLvAo)yb9$aIk4?ai&wCA(7cYxz0P`8YNCM1B4kObyWZ>68|@3i>5 zOP1Q+0X|U<%zuyKxY+V)bare&51?c81m@4d-e%joFwnyH0P-oor=PdHBWH}81iExJ zS!Bf-fLXHe31CW1s?&!?u-V+ z0yv|sV3Y(F!}99GP7Ml500nIaVcXlMo`b#3SYCbD_J3~FvJ`QAUw)l?`HRn+wC{qh z1Ygg605j*-+Rg06(W^905;)k~j9Y6rJM;Z!_wU|Lzy4ltGT@u9K1M`DllDLU{7dTx zqbtE*Bnix(7e}u~o=05@{)mVyBI5qt+qD8958kHj+tkJ2uR@{#0H95~cT()$ndvSy zERxCH3V*zNC&i;@*|`KzeE-q2k@afYk5lclji_AXUOPeE=hR3E4(ubx+(p_^%zN#yyer{_4PW z4Eij4d@n0L8tuTl8+ub*sB;M*#d=DyK9eV2@qfzSGzkp5@>l5&m%5C;9ja+2w%JEH2+dC0IY#pAwzrE9s ztt-b#Vdrl*4)Mv*LBO-6K|ZKRvITkY=eBqHr~VFFj>R@|8nBoI z#t!~LQ-2kv124j$a$r<|cRlzAIUD?N)-YJpfce1S*lVpi9xz2^<-$A!|36=U9BY;1 zF2`b+$?p6tseN-$jh@mtKg#OD53rL_1KN{d0|yv@3-=#Z)#ac+NKKC?6$PqOX2kaNI_)UmKB*MD_yG@hjMvy(WycDYx> zi?{FM=U*d5L@d+3loNI5W1js!8c&Kj2P|hao}~A$A4WvP{y{nSc9*?~h{J1_jtDeS-h}>xV@R0}KIXSXUXY+}}>iJ|V68i^tb05Yadl8@3f+7Z z`>3SgkBF!uB5vQhIVb??;9a(TP9+9^9})!s06Co=r#L;{=#m7y7Pr?{RDLs~-GhoKBJQ z{1#wNGN^-oIvFgY4*pdnf%M_Z%>B8T54$82VjZ zJ%69707~{%0!VR`s(4XV@uKQV0=*N&tl_~5wSV>aLz95uXIFe40@T4@|6eD8=C-!a zMa0~azV*FEo32v7>%DrY&{gjD9S#f${=Q{?{dsc(6s@hNpWFM_z5MjwS=-iRx)Nxs z4BE#0^|r40fG%yx!Mp&a==?Fz^iZI+_umlB;XqqOpzMbkdQA*A*8;Tl^qJQQEZXxg z@*v(KQ~;E0p~rx|d;Pxi0c+GY7tqza>-pJcZtnk?#X*zd1KE@C0|*#E2LAy>RIqyr SpzH_$0000 = DEFAULT, + private val platform: Platform, +) : TwoArgFunction() { + override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + val ws = LuaTable() + ws["save"] = save() + ws["list"] = list() + ws["create"] = create() + ws["load"] = load() + ws["download"] = download() + arg2.set("ws", ws) + arg2.get("package").get("loaded").set("ws", ws) + return ws + } + + internal inner class save : TwoArgFunction() { + + override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + val file = findFile(arg1) ?: return NIL + file.save(arg2.checkjstring()?.encodeToByteArray() ?: ByteArray(0)) + return NIL + } + } + + private fun findFile(arg: LuaValue): LocalFile? { + val filename = arg.checkjstring() ?: return null + return resources.firstOrNull { it.name == filename } + } + + internal inner class load : OneArgFunction() { + override fun call(arg: LuaValue): LuaValue { + val file = findFile(arg) ?: return NIL + val content = file.readAll().decodeToString() + return valueOf(content) + } + } + + internal inner class create : TwoArgFunction() { + + override fun call(@TinyArg("prefix") arg1: LuaValue, @TinyArg("extension") arg2: LuaValue): LuaValue { + val prefix = arg1.optjstring("new")!! + val ext = arg2.optjstring("")!! + + var nameAvailable = false + + var index = 0 + + var filename = "" + + while (!nameAvailable) { + filename = "$prefix-$index" + + if (findFile(valueOf(filename)) != null) { + index++ + } else { + nameAvailable = true + } + } + val fileneameWithExt = if (ext.isBlank()) { + filename + } else { + "$filename.$ext" + } + resources = resources + platform.createLocalFile(fileneameWithExt) + return valueOf(filename) + } + } + + internal inner class list : OneArgFunction() { + + override fun call(): LuaValue = super.call() + + override fun call(@TinyArg("extension") arg: LuaValue): LuaValue { + val ext = arg.optjstring(null).let { it?.lowercase() } + val result = LuaTable() + resources.forEach { + if ((ext == null || it.name.endsWith(ext))) { + result.insert(0, valueOf(it.name)) + } + } + return result + } + } + + internal inner class download : OneArgFunction() { + + override fun call(arg: LuaValue): LuaValue { + return NIL + } + } + + companion object { + var DEFAULT = emptyList() + } +} diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt index 7c51c337..4d828a8f 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt @@ -3,6 +3,7 @@ package com.github.minigdx.tiny.platform import com.github.minigdx.tiny.Pixel import com.github.minigdx.tiny.engine.GameLoop import com.github.minigdx.tiny.engine.GameOptions +import com.github.minigdx.tiny.file.LocalFile import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.graphic.FrameBuffer import com.github.minigdx.tiny.input.InputHandler @@ -94,4 +95,6 @@ interface Platform { * Create a SourceStream from a midi file. */ fun createSoundStream(name: String): SourceStream + + fun createLocalFile(name: String): LocalFile } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt index 65a7eb88..f0748b0c 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/GameScript.kt @@ -19,6 +19,8 @@ import com.github.minigdx.tiny.lua.StdLib import com.github.minigdx.tiny.lua.TinyBaseLib import com.github.minigdx.tiny.lua.TinyLib import com.github.minigdx.tiny.lua.Vec2Lib +import com.github.minigdx.tiny.lua.WorkspaceLib +import com.github.minigdx.tiny.platform.Platform import org.luaj.vm2.Globals import org.luaj.vm2.LoadState import org.luaj.vm2.LuaError @@ -43,6 +45,7 @@ class GameScript( override val name: String, val gameOptions: GameOptions, val inputHandler: InputHandler, + val platform: Platform, override val type: ResourceType, ) : GameResource { @@ -90,6 +93,7 @@ class GameScript( load(sprLib) load(JuiceLib()) load(NotesLib()) + load(WorkspaceLib(platform = platform)) this@GameScript.resourceAccess.customizeLuaGlobal(this) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/ResourceFactory.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/ResourceFactory.kt index e58206f3..f30425bf 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/ResourceFactory.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/resources/ResourceFactory.kt @@ -167,7 +167,7 @@ class ResourceFactory( canUseJarPrefix = !protectedResources.contains(resourceType), ), ).map { content -> - GameScript(version++, index, name, gameOptions, inputHandler, resourceType).apply { + GameScript(version++, index, name, gameOptions, inputHandler, platform, resourceType).apply { this.content = content } }.onEach { diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt index 30ada99f..65a368a7 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt @@ -2,6 +2,7 @@ package com.github.minigdx.tiny.platform.test import com.github.minigdx.tiny.engine.GameLoop import com.github.minigdx.tiny.engine.GameOptions +import com.github.minigdx.tiny.file.LocalFile import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.graphic.FrameBuffer import com.github.minigdx.tiny.input.InputHandler @@ -119,5 +120,14 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map return ObjectStream(data) } + override fun createLocalFile(name: String): LocalFile = object : LocalFile { + override val name: String = "name" + override val extension: String = "" + + override fun readAll(): ByteArray = ByteArray(0) + + override fun save(content: ByteArray) = Unit + } + fun saveAnimation(name: String) = toGif(name, frames) } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt new file mode 100644 index 00000000..2286ca16 --- /dev/null +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt @@ -0,0 +1,20 @@ +package com.github.minigdx.tiny.file + +import kotlinx.browser.localStorage + +class JsLocalFile( + override val name: String, + override val extension: String, +) : LocalFile { + + private fun computeFilename() = "$name.$extension" + + override fun readAll(): ByteArray { + val item = localStorage.getItem(computeFilename()) + return item?.encodeToByteArray() ?: ByteArray(0) + } + + override fun save(content: ByteArray) { + localStorage.setItem(computeFilename(), content.decodeToString()) + } +} diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt index 2672b6b3..918890f4 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt @@ -6,6 +6,8 @@ import com.github.minigdx.tiny.engine.GameLoop import com.github.minigdx.tiny.engine.GameOptions import com.github.minigdx.tiny.file.AjaxStream import com.github.minigdx.tiny.file.ImageDataStream +import com.github.minigdx.tiny.file.JsLocalFile +import com.github.minigdx.tiny.file.LocalFile import com.github.minigdx.tiny.file.SoundDataSourceStream import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.graphic.FrameBuffer @@ -110,4 +112,16 @@ class WebGlPlatform( override fun createSoundStream(name: String): SourceStream { return SoundDataSourceStream(name, soundManager, createByteArrayStream(name)) } + + override fun createLocalFile(name: String): LocalFile { + val (file, ext) = if (name.contains(".")) { + name.split(".") + } else { + listOf(name, "") + } + return JsLocalFile( + file, + ext, + ) + } } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/JvmLocalFile.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/JvmLocalFile.kt new file mode 100644 index 00000000..7792df2d --- /dev/null +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/JvmLocalFile.kt @@ -0,0 +1,22 @@ +package com.github.minigdx.tiny.file + +import java.io.File + +class JvmLocalFile( + name: String, + val workingDirectory: File, +) : LocalFile { + + private val file = File(name) + + override val name: String = file.nameWithoutExtension + + override val extension: String = file.extension + override fun readAll(): ByteArray { + return workingDirectory.resolve(file).readBytes() + } + + override fun save(content: ByteArray) { + workingDirectory.resolve(file).writeBytes(content) + } +} diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt index f7a4a71c..fd88db2c 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt @@ -6,6 +6,8 @@ import com.github.minigdx.tiny.engine.GameLoop import com.github.minigdx.tiny.engine.GameOptions import com.github.minigdx.tiny.file.FileStream import com.github.minigdx.tiny.file.InputStreamStream +import com.github.minigdx.tiny.file.JvmLocalFile +import com.github.minigdx.tiny.file.LocalFile import com.github.minigdx.tiny.file.SoundDataSourceStream import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.file.VirtualFileSystem @@ -352,6 +354,8 @@ class GlfwPlatform( } } + override fun createLocalFile(name: String): LocalFile = JvmLocalFile(name, workdirectory) + companion object { private const val FPS = 60 } diff --git a/tiny-web-editor/src/jsMain/kotlin/Main.kt b/tiny-web-editor/src/jsMain/kotlin/Main.kt index 1e39765f..9765a21f 100644 --- a/tiny-web-editor/src/jsMain/kotlin/Main.kt +++ b/tiny-web-editor/src/jsMain/kotlin/Main.kt @@ -2,6 +2,7 @@ import com.github.minigdx.tiny.engine.GameEngine import com.github.minigdx.tiny.engine.GameLoop import com.github.minigdx.tiny.engine.GameOptions import com.github.minigdx.tiny.file.CommonVirtualFileSystem +import com.github.minigdx.tiny.file.LocalFile import com.github.minigdx.tiny.file.SourceStream import com.github.minigdx.tiny.forEachIndexed import com.github.minigdx.tiny.getRootPath @@ -186,4 +187,5 @@ class EditorWebGlPlatform(val delegate: Platform) : Platform { name, ) override fun createSoundStream(name: String): SourceStream = delegate.createSoundStream(name) + override fun createLocalFile(name: String): LocalFile = delegate.createLocalFile(name) } From ad5a2640d549853970039ff2973a2f2b42fb0c9f Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Wed, 31 Jan 2024 13:28:54 +0100 Subject: [PATCH 27/48] Add comments on the LocalFile --- .../com/github/minigdx/tiny/file/LocalFile.kt | 13 ++++++++ .../github/minigdx/tiny/lua/WorkspaceLib.kt | 31 +++++++++++++++---- .../github/minigdx/tiny/platform/Platform.kt | 5 +++ .../github/minigdx/tiny/file/JsLocalFile.kt | 8 ++++- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/LocalFile.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/LocalFile.kt index 2f4405ea..a306a115 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/LocalFile.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/LocalFile.kt @@ -2,10 +2,23 @@ package com.github.minigdx.tiny.file interface LocalFile { + /** + * Name of the file, without the extension + */ val name: String + /** + * Extension. Can be blank + */ val extension: String + + /** + * Read the content of the file + */ fun readAll(): ByteArray + /** + * Save the content of the file + */ fun save(content: ByteArray) } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/WorkspaceLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/WorkspaceLib.kt index 06204ebb..5c39dc20 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/WorkspaceLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/WorkspaceLib.kt @@ -1,6 +1,8 @@ package com.github.minigdx.tiny.lua import com.github.mingdx.tiny.doc.TinyArg +import com.github.mingdx.tiny.doc.TinyCall +import com.github.mingdx.tiny.doc.TinyFunction import com.github.mingdx.tiny.doc.TinyLib import com.github.minigdx.tiny.file.LocalFile import com.github.minigdx.tiny.platform.Platform @@ -26,9 +28,14 @@ class WorkspaceLib( return ws } + @TinyFunction( + "Save the content into a local file, " + + "on desktop or in the local storage on the web platform.", + ) internal inner class save : TwoArgFunction() { - override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + @TinyCall("Save the content into the file name.") + override fun call(@TinyArg("name") arg1: LuaValue, @TinyArg("content") arg2: LuaValue): LuaValue { val file = findFile(arg1) ?: return NIL file.save(arg2.checkjstring()?.encodeToByteArray() ?: ByteArray(0)) return NIL @@ -40,24 +47,34 @@ class WorkspaceLib( return resources.firstOrNull { it.name == filename } } + @TinyFunction("Load and get the content of the file name") internal inner class load : OneArgFunction() { - override fun call(arg: LuaValue): LuaValue { + + @TinyCall("Load and get the content of the file name") + override fun call(@TinyArg("name") arg: LuaValue): LuaValue { val file = findFile(arg) ?: return NIL val content = file.readAll().decodeToString() return valueOf(content) } } + @TinyFunction("Create a local file. The name is generated so the name is unique.") internal inner class create : TwoArgFunction() { + @TinyCall("Create a local file with the prefix and the extension. The name of the file created.") override fun call(@TinyArg("prefix") arg1: LuaValue, @TinyArg("extension") arg2: LuaValue): LuaValue { val prefix = arg1.optjstring("new")!! val ext = arg2.optjstring("")!! - var nameAvailable = false + val (filename, filenameWithExt) = findAvailableName(prefix, ext) - var index = 0 + resources = resources + platform.createLocalFile(filenameWithExt) + return valueOf(filename) + } + private fun findAvailableName(prefix: String, ext: String): Pair { + var nameAvailable = false + var index = 0 var filename = "" while (!nameAvailable) { @@ -74,15 +91,17 @@ class WorkspaceLib( } else { "$filename.$ext" } - resources = resources + platform.createLocalFile(fileneameWithExt) - return valueOf(filename) + return Pair(filename, fileneameWithExt) } } + @TinyFunction("List all files available in the workspace.") internal inner class list : OneArgFunction() { + @TinyCall("List all files available in the workspace.") override fun call(): LuaValue = super.call() + @TinyCall("List all files available in the workspace and filter by the file extension.") override fun call(@TinyArg("extension") arg: LuaValue): LuaValue { val ext = arg.optjstring(null).let { it?.lowercase() } val result = LuaTable() diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt index 4d828a8f..ddfdeda6 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/platform/Platform.kt @@ -96,5 +96,10 @@ interface Platform { */ fun createSoundStream(name: String): SourceStream + /** + * Create a file using the name. + * + * @param: name of the file, with the extension, if any. + */ fun createLocalFile(name: String): LocalFile } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt index 2286ca16..769008f3 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt @@ -7,7 +7,13 @@ class JsLocalFile( override val extension: String, ) : LocalFile { - private fun computeFilename() = "$name.$extension" + private fun computeFilename(): String { + return if (extension.isBlank()) { + name + } else { + "$name.$extension" + } + } override fun readAll(): ByteArray { val item = localStorage.getItem(computeFilename()) From d298859dd236e40ff592e0522a11887babff54f5 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Thu, 1 Feb 2024 00:10:20 +0100 Subject: [PATCH 28/48] Refactor JsLocalFile on the web - use one name and extract extension from it - load by default every files from local storage --- .../kotlin/com/github/minigdx/tiny/Main.kt | 12 +++++++++++ .../github/minigdx/tiny/file/JsLocalFile.kt | 20 +++++++++---------- .../tiny/platform/webgl/WebGlPlatform.kt | 10 +--------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/Main.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/Main.kt index 248e2f07..38c99c5a 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/Main.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/Main.kt @@ -3,9 +3,12 @@ package com.github.minigdx.tiny import com.github.minigdx.tiny.engine.GameEngine import com.github.minigdx.tiny.engine.GameOptions import com.github.minigdx.tiny.file.CommonVirtualFileSystem +import com.github.minigdx.tiny.file.JsLocalFile import com.github.minigdx.tiny.log.StdOutLogger +import com.github.minigdx.tiny.lua.WorkspaceLib import com.github.minigdx.tiny.platform.webgl.WebGlPlatform import kotlinx.browser.document +import kotlinx.browser.localStorage import kotlinx.browser.window import kotlinx.dom.appendText import org.w3c.dom.Element @@ -99,6 +102,15 @@ fun setupGames(rootPath: String, tinyGameTag: HTMLCollection) { } game.appendChild(canvas) + WorkspaceLib.DEFAULT = (0 until localStorage.length).mapNotNull { index -> + val key = localStorage.key(index) + if (key != null) { + JsLocalFile(key) + } else { + null + } + } + val gameOptions = GameOptions( width = gameWidth, height = gameHeight, diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt index 769008f3..ebbcf99d 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt @@ -3,24 +3,24 @@ package com.github.minigdx.tiny.file import kotlinx.browser.localStorage class JsLocalFile( - override val name: String, - override val extension: String, + val filename: String, ) : LocalFile { - private fun computeFilename(): String { - return if (extension.isBlank()) { - name - } else { - "$name.$extension" - } + override val name: String + override val extension: String + + init { + val (name, ext) = ("$filename.").split(".") + this.name = name + this.extension = ext } override fun readAll(): ByteArray { - val item = localStorage.getItem(computeFilename()) + val item = localStorage.getItem(filename) return item?.encodeToByteArray() ?: ByteArray(0) } override fun save(content: ByteArray) { - localStorage.setItem(computeFilename(), content.decodeToString()) + localStorage.setItem(filename, content.decodeToString()) } } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt index 918890f4..45d6d04f 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/WebGlPlatform.kt @@ -114,14 +114,6 @@ class WebGlPlatform( } override fun createLocalFile(name: String): LocalFile { - val (file, ext) = if (name.contains(".")) { - name.split(".") - } else { - listOf(name, "") - } - return JsLocalFile( - file, - ext, - ) + return JsLocalFile(name) } } From 02f2a9ee9a4718a5095eee0f019e56685434b6d6 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Thu, 1 Feb 2024 00:10:39 +0100 Subject: [PATCH 29/48] Update debug.console to log tables --- .../com/github/minigdx/tiny/lua/DebugLib.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/DebugLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/DebugLib.kt index c6b7c7d4..51d372e9 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/DebugLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/DebugLib.kt @@ -88,6 +88,7 @@ private class DebugShape { val color = args.arg(3) return listOf(a.get("x"), a.get("y"), b.get("x"), b.get("y"), color) } + else -> { null } @@ -103,6 +104,7 @@ private class DebugShape { val color = args.arg(3) return listOf(a, b, color) } + 2 -> { val a = args.arg(1) val b = args.arg(2) @@ -112,10 +114,12 @@ private class DebugShape { listOf(a, b, LuaValue.NIL) } } + 1 -> { val a = args.arg(1) return listOf(a.get("x"), a.get("y"), LuaValue.NIL) } + else -> { null } @@ -204,9 +208,20 @@ class DebugLib(private val resourceAccess: GameResourceAccess) : TwoArgFunction( internal inner class console : OneArgFunction() { @TinyCall("Log a message into the console.") override fun call(@TinyArg("str") arg: LuaValue): LuaValue { - println(arg) + val str = formatValue(arg) + println(str) return NIL } + + private fun formatValue(arg: LuaValue): String = if (arg.istable()) { + val table = arg as LuaTable + val keys = table.keys() + val str = keys.map { it.optjstring("nil") + ":" + formatValue(table.get(it)) } + .joinToString(" ") + "table[$str]" + } else { + arg.toString() + } } @TinyFunction("Draw a rectangle on the screen", example = DEBUG_ENABLED_EXAMPLE) From 46740eb898c36c5bf8d8784837c25a0f382289e9 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Thu, 1 Feb 2024 00:10:57 +0100 Subject: [PATCH 30/48] Load files at the startup of the sfx editor --- tiny-cli/src/jvmMain/resources/sfx/game.lua | 72 +++++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 66a2a621..203648a5 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -61,7 +61,9 @@ function on_active_tab(current, prec) color = f.tip_color }) end - prec.data = data + if prec ~= nil then + prec.data = data + end -- restore the previous score if current.data ~= nil then @@ -87,7 +89,6 @@ function on_active_tab(current, prec) end active_tab = current - debug.console(active_tab.label) end local window = { @@ -178,7 +179,7 @@ function _init(w, h) if #files > 0 then for w in all(files) do local tab = widgets.createTab({ - x = 0, + x = new_tab_x, width = 2 * 16 + 8, status = 0, label = w, @@ -196,7 +197,6 @@ function _init(w, h) label = file, on_active_tab = on_active_tab }) - table.insert(tabs, tab) new_tab_x = new_tab_x + tab.width end @@ -211,6 +211,68 @@ function _init(w, h) on_active_tab = on_active_tab, new_tab = true }) + -- + init_faders(tabs) +end + +function extract(inputString) + local pattern = "([a-zA-Z]+)%(([^%)]+)%)" + local wave, note = inputString:match(pattern) + return wave, note +end + +function split(inputString) + local result = {} + for token in string.gmatch(inputString, "[^%-]+") do + table.insert(result, token) + end + return result +end + +function init_faders(tabs) + local index = 1 + + local notes = {} + for k, v in pairs(labels) do + notes[v] = k + end + + local colors = {} + for v in all(waves) do + colors[v.type] = v.color + end + + for t in all(tabs) do + local f = t.label + local content = ws.load(f) + local saved = split(content) + local result = {} + for index, f in ipairs(faders) do + local s = saved[index] + + local data = { + wave = "", + note = 0, + value = 0, + color = 0 + } + + if s ~= "(0)" and s ~= "*" then + local wave, note = extract(s) + data = { + wave = wave, + note = note, + value = notes[note], + color = colors[wave], + } + end + + table.insert(result, data) + end + t.data = result + end + on_active_tab(tabs[1]) + end function generate_score() @@ -231,9 +293,7 @@ function _update() widgets._update() if ctrl.pressed(keys.space) then - local score = generate_score() - debug.console(score) sfx.sfx(score, 220) end From 2e71ab1ff487d95870299697839023ce21b0347f Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Thu, 1 Feb 2024 10:26:51 +0100 Subject: [PATCH 31/48] Read only the local file generated by tiny on the web platform (ie: don't read local storage created by others scripts) --- tiny-cli/src/jvmMain/resources/sfx/game.lua | 50 ++++++++++--------- .../com/github/minigdx/tiny/file/LocalFile.kt | 2 +- .../github/minigdx/tiny/lua/WorkspaceLib.kt | 2 +- .../kotlin/com/github/minigdx/tiny/Main.kt | 4 +- .../github/minigdx/tiny/file/JsLocalFile.kt | 9 ++-- .../github/minigdx/tiny/file/JvmLocalFile.kt | 9 +++- 6 files changed, 42 insertions(+), 34 deletions(-) diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 203648a5..6b53b1ce 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -183,6 +183,7 @@ function _init(w, h) width = 2 * 16 + 8, status = 0, label = w, + content = ws.load(w), on_active_tab = on_active_tab }) table.insert(tabs, tab) @@ -243,33 +244,34 @@ function init_faders(tabs) end for t in all(tabs) do - local f = t.label - local content = ws.load(f) - local saved = split(content) - local result = {} - for index, f in ipairs(faders) do - local s = saved[index] - - local data = { - wave = "", - note = 0, - value = 0, - color = 0 - } - - if s ~= "(0)" and s ~= "*" then - local wave, note = extract(s) - data = { - wave = wave, - note = note, - value = notes[note], - color = colors[wave], + local content = t.content + if content then + local saved = split(content) + local result = {} + for index, f in ipairs(faders) do + local s = saved[index] + + local data = { + wave = "", + note = 0, + value = 0, + color = 0 } - end - table.insert(result, data) + if s ~= "(0)" and s ~= "*" then + local wave, note = extract(s) + data = { + wave = wave, + note = note, + value = notes[note], + color = colors[wave] + } + end + + table.insert(result, data) + end + t.data = result end - t.data = result end on_active_tab(tabs[1]) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/LocalFile.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/LocalFile.kt index a306a115..1736febe 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/LocalFile.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/file/LocalFile.kt @@ -15,7 +15,7 @@ interface LocalFile { /** * Read the content of the file */ - fun readAll(): ByteArray + fun readAll(): ByteArray? /** * Save the content of the file diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/WorkspaceLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/WorkspaceLib.kt index 5c39dc20..d28db59a 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/WorkspaceLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/WorkspaceLib.kt @@ -53,7 +53,7 @@ class WorkspaceLib( @TinyCall("Load and get the content of the file name") override fun call(@TinyArg("name") arg: LuaValue): LuaValue { val file = findFile(arg) ?: return NIL - val content = file.readAll().decodeToString() + val content = file.readAll()?.decodeToString() ?: return NIL return valueOf(content) } } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/Main.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/Main.kt index 38c99c5a..7fad06fb 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/Main.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/Main.kt @@ -104,8 +104,8 @@ fun setupGames(rootPath: String, tinyGameTag: HTMLCollection) { WorkspaceLib.DEFAULT = (0 until localStorage.length).mapNotNull { index -> val key = localStorage.key(index) - if (key != null) { - JsLocalFile(key) + if (key != null && key.startsWith("tiny")) { + JsLocalFile(key.replaceFirst("tiny-", "")) } else { null } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt index ebbcf99d..0417cf67 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/file/JsLocalFile.kt @@ -4,6 +4,7 @@ import kotlinx.browser.localStorage class JsLocalFile( val filename: String, + val localStoragePrefix: String = "tiny", ) : LocalFile { override val name: String @@ -15,12 +16,12 @@ class JsLocalFile( this.extension = ext } - override fun readAll(): ByteArray { - val item = localStorage.getItem(filename) - return item?.encodeToByteArray() ?: ByteArray(0) + override fun readAll(): ByteArray? { + val item = localStorage.getItem("$localStoragePrefix-$filename") + return item?.encodeToByteArray() ?: return null } override fun save(content: ByteArray) { - localStorage.setItem(filename, content.decodeToString()) + localStorage.setItem("$localStoragePrefix-$filename", content.decodeToString()) } } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/JvmLocalFile.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/JvmLocalFile.kt index 7792df2d..aec3f9ba 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/JvmLocalFile.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/file/JvmLocalFile.kt @@ -12,8 +12,13 @@ class JvmLocalFile( override val name: String = file.nameWithoutExtension override val extension: String = file.extension - override fun readAll(): ByteArray { - return workingDirectory.resolve(file).readBytes() + override fun readAll(): ByteArray? { + val resolved = workingDirectory.resolve(file) + return if (resolved.exists() && resolved.isFile) { + resolved.readBytes() + } else { + null + } } override fun save(content: ByteArray) { From cfb505a9757a3865f900ebb5597165dd6e2987de Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Fri, 2 Feb 2024 23:05:08 +0100 Subject: [PATCH 32/48] Update the documentation only on new tags --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf62d338..f05ef630 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,6 +41,7 @@ jobs: run: ./gradlew asciidoctor -Pversion="${{github.ref_name}}" - name: Copy generated content into gh-pages. uses: peaceiris/actions-gh-pages@v3 + if: startsWith(github.ref, 'refs/tags/') with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./tiny-doc/build/docs/asciidoc From 381ba0be562ddaa77e8fa69ccddc964b3f2a8c4c Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Sun, 4 Feb 2024 15:42:58 +0100 Subject: [PATCH 33/48] Parse the new format for SFX --- .../com/github/minigdx/tiny/lua/NotesLib.kt | 9 +++ .../com/github/minigdx/tiny/lua/SfxLib.kt | 74 ++++++++++++++++++ .../com/github/minigdx/tiny/sound/Song.kt | 6 ++ .../minigdx/tiny/sound/WaveGenerator.kt | 20 ++--- .../github/minigdx/tiny/lua/MusicLibTest.kt | 20 ----- .../com/github/minigdx/tiny/lua/SfxLibTest.kt | 78 +++++++++++++++++++ 6 files changed, 177 insertions(+), 30 deletions(-) create mode 100644 tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt delete mode 100644 tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/MusicLibTest.kt create mode 100644 tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt index 6190ef4e..92b0cce2 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt @@ -177,6 +177,15 @@ enum class Note(val frequency: Float, val index: Int) { As8(7458.62f, OCTAVE_8 + 11), Bb8(7458.62f, OCTAVE_8 + 11), B8(7902.13f, OCTAVE_8 + 12), + ; + + companion object { + private val notesPerIndex = Note.values().distinctBy { it.index }.sortedBy { it.index }.toTypedArray() + + fun fromIndex(noteIndex: Int): Note { + return notesPerIndex[noteIndex - 1] + } + } } @TinyLib( diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index 5cf45d0b..c8ee9f3a 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -8,11 +8,14 @@ import com.github.minigdx.tiny.Percent import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.engine.GameResourceAccess import com.github.minigdx.tiny.resources.Sound +import com.github.minigdx.tiny.sound.Beat import com.github.minigdx.tiny.sound.NoiseWave +import com.github.minigdx.tiny.sound.Pattern import com.github.minigdx.tiny.sound.PulseWave import com.github.minigdx.tiny.sound.SawToothWave import com.github.minigdx.tiny.sound.SilenceWave import com.github.minigdx.tiny.sound.SineWave +import com.github.minigdx.tiny.sound.Song import com.github.minigdx.tiny.sound.SquareWave import com.github.minigdx.tiny.sound.TriangleWave import com.github.minigdx.tiny.sound.WaveGenerator @@ -181,6 +184,75 @@ class SfxLib( } } + fun convertToWave(note: String, duration: Seconds): WaveGenerator { + val wave = note.substring(0, 2).toInt(16) + val noteIndex = note.substring(2, 4).toInt(16) + val volume = note.substring(4, 6).toInt(16) / 255f + + return when (wave) { + 1 -> SineWave(Note.fromIndex(noteIndex), duration, volume) + 2 -> SquareWave(Note.fromIndex(noteIndex), duration, volume) + 3 -> TriangleWave(Note.fromIndex(noteIndex), duration, volume) + 4 -> NoiseWave(Note.fromIndex(noteIndex), duration, volume) + 5 -> PulseWave(Note.fromIndex(noteIndex), duration, volume) + 6 -> SawToothWave(Note.fromIndex(noteIndex), duration, volume) + else -> SilenceWave(duration) + } + } + + fun convertScoreToSong(score: String): Song { + val lines = score.lines() + if (lines.isEmpty()) { + throw IllegalArgumentException( + "The content of the score is empty. Can't convert it into a song. " + + "Check if the score is not empty or correctly loaded!", + ) + } + + val header = lines.first() + if (!header.startsWith(TINY_SFX_HEADER)) { + throw IllegalArgumentException( + "The '$TINY_SFX_HEADER' is missing from the fist line of the score. " + + "Is the score a valid score?", + ) + } + + val (_, nbPattern, bpm) = header.split(" ") + + val duration = 60f / bpm.toFloat() / 4f + + // Map + val patterns = lines.drop(1).take(nbPattern.toInt()).mapIndexed { indexPattern, pattern -> + val beatsStr = pattern.split(" ") + val beats = convertToBeats(beatsStr, duration) + Pattern(indexPattern + 1, beats) + }.associateBy { it.index } + + val patternOrder = lines.drop(nbPattern.toInt() + 1).firstOrNull() + val orders = if (patternOrder.isNullOrBlank()) { + listOf(1) + } else { + patternOrder.split(" ").map { it.toInt() } + } + + val patternsOrdered = orders.map { patterns[it]!! } + + return Song(bpm.toInt(), patterns, patternsOrdered) + } + + private fun convertToBeats(beatsStr: List, duration: Seconds): List { + val beats = beatsStr + .asSequence() + .mapIndexed { index, beat -> + val notes = beat.split(":") + .asSequence() + .filter { it.isNotBlank() } + .map { note -> convertToWave(note, duration) } + Beat(index + 1, notes.toList()) + } + return beats.toList() + } + companion object { private val acceptedTypes = setOf("sine", "noise", "pulse", "triangle", "saw", "square") @@ -225,5 +297,7 @@ class SfxLib( return waves } + + private const val TINY_SFX_HEADER = "tiny-sfx" } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt new file mode 100644 index 00000000..54dbdc1f --- /dev/null +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt @@ -0,0 +1,6 @@ +package com.github.minigdx.tiny.sound + +data class Beat(val index: Int, val notes: List) + +data class Pattern(val index: Int, val beats: List) +data class Song(val bpm: Int, val patterns: Map, val music: List) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index 616d5da5..d01587e9 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -44,16 +44,6 @@ sealed class WaveGenerator(val note: Note, val duration: Seconds, val volume: Pe } } -class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { - override fun generate(sample: Int): Float { - val angle: Float = sin(angle(sample)) - val phase = (angle * 2f) - 1f - return phase - } - - override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SawToothWave(note, duration, volume) -} - class SineWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { override fun generate(sample: Int): Float { return sin(angle(sample)) @@ -112,6 +102,16 @@ class PulseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGen override fun copy(duration: Seconds, volume: Percent): WaveGenerator = PulseWave(note, duration, volume) } +class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { + override fun generate(sample: Int): Float { + val angle: Float = sin(angle(sample)) + val phase = (angle * 2f) - 1f + return phase + } + + override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SawToothWave(note, duration, volume) +} + class SilenceWave(duration: Seconds) : WaveGenerator(Note.C0, duration, 1.0f) { override val isSilence: Boolean = true diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/MusicLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/MusicLibTest.kt deleted file mode 100644 index ee315135..00000000 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/MusicLibTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.minigdx.tiny.lua - -import kotlin.test.Test -import kotlin.test.assertEquals - -class MusicLibTest { - - fun trim(str: String): String { - val lastIndex = str.lastIndexOf(')') - if (lastIndex < 0) return str - return str.substring(0, lastIndex + 2) - } - - @Test - fun trimMusic() { - val str = "*-*-sine(Eb)-*-*-" - - assertEquals("*-*-sine(Eb)-", trim(str)) - } -} diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt new file mode 100644 index 00000000..dab9691e --- /dev/null +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt @@ -0,0 +1,78 @@ +package com.github.minigdx.tiny.lua + +import com.github.minigdx.tiny.engine.GameResourceAccess +import com.github.minigdx.tiny.graphic.ColorPalette +import com.github.minigdx.tiny.graphic.FrameBuffer +import com.github.minigdx.tiny.resources.GameLevel +import com.github.minigdx.tiny.resources.GameScript +import com.github.minigdx.tiny.resources.Sound +import com.github.minigdx.tiny.resources.SpriteSheet +import com.github.minigdx.tiny.sound.SilenceWave +import com.github.minigdx.tiny.sound.SineWave +import com.github.minigdx.tiny.sound.WaveGenerator +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SfxLibTest { + + private val mockResources = object : GameResourceAccess { + override val bootSpritesheet: SpriteSheet? = null + override val frameBuffer: FrameBuffer = FrameBuffer(10, 10, ColorPalette(listOf("#FFFFFF"))) + override fun spritesheet(index: Int): SpriteSheet? = null + override fun spritesheet(sheet: SpriteSheet) = Unit + override fun level(index: Int): GameLevel? = null + override fun sound(index: Int): Sound? = null + override fun script(name: String): GameScript? = null + override fun note(wave: WaveGenerator) = Unit + override fun sfx(waves: List) = Unit + } + + fun trim(str: String): String { + val lastIndex = str.lastIndexOf(')') + if (lastIndex < 0) return str + return str.substring(0, lastIndex + 2) + } + + @Test + fun trimMusic() { + val str = "*-*-sine(Eb)-*-*-" + + assertEquals("*-*-sine(Eb)-", trim(str)) + } + + @Test + fun scoreToSong() { + val score = """tiny-sfx 2 120 + |0101FF:0202FF 0101FF:0202FF + |0101FF:0202FF 0101FF:0202FF + |1 2 1 + """.trimMargin() + + val lib = SfxLib(mockResources) + val song = lib.convertScoreToSong(score) + + assertEquals(120, song.bpm) + // patterns by index + assertEquals(2, song.patterns.size) + // patterns ordered by usage + assertEquals(3, song.music.size) + } + + @Test + fun convertToNote() { + val lib = SfxLib(mockResources) + val wave = lib.convertToWave("0101FF", 0.1f) + assertTrue(wave::class == SineWave::class) + assertEquals(Note.C0, wave.note) + } + + @Test + fun convertToNoteWithSilence() { + val lib = SfxLib(mockResources) + val wave = lib.convertToWave("FFFFFF", 0.1f) + assertTrue(wave::class == SilenceWave::class) + assertTrue(wave.isSilence) + assertEquals(0.1f, wave.duration) + } +} From 51673c0f2e4e938682c4acdaca3b883cfd844fbd Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Sun, 4 Feb 2024 18:33:35 +0100 Subject: [PATCH 34/48] Convert a song into a lua table to ease the convertion --- .../com/github/minigdx/tiny/lua/SfxLib.kt | 31 +++++++++++++++++++ .../minigdx/tiny/sound/WaveGenerator.kt | 16 ++++++++++ 2 files changed, 47 insertions(+) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index c8ee9f3a..26f9a4db 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -44,6 +44,7 @@ class SfxLib( ctrl.set("noise", noise()) ctrl.set("pulse", pulse()) ctrl.set("saw", sawtooth()) + ctrl.set("to_table", toTable()) ctrl.set("sfx", sfx()) arg2.set("sfx", ctrl) arg2.get("package").get("loaded").set("sfx", ctrl) @@ -164,6 +165,36 @@ class SfxLib( } } + inner class toTable : OneArgFunction() { + + private fun Beat.toLuaTable(): LuaTable { + val beat = LuaTable() + notes.forEach { wave -> + beat.set("type", wave.name) + beat.set("note", wave.note.index) + } + return beat + } + + override fun call(arg: LuaValue): LuaValue { + val score = arg.optjstring(null) ?: return NIL + val song = convertScoreToSong(score) + + val patterns = LuaTable() + song.patterns.forEach { (index, pattern) -> + val beats = LuaTable() + pattern.beats.forEach { beat -> + beats.insert(beat.index, beat.toLuaTable()) + } + patterns.insert(index, beats) + } + val result = LuaTable() + result["bpm"] = valueOf(song.bpm) + result["patterns"] = patterns + return result + } + } + inner class sfx : TwoArgFunction() { override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index d01587e9..811832df 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -38,6 +38,8 @@ sealed class WaveGenerator(val note: Note, val duration: Seconds, val volume: Pe abstract fun copy(duration: Seconds, volume: Percent): WaveGenerator + abstract val name: String + companion object { internal const val PI = kotlin.math.PI.toFloat() internal const val TWO_PI = 2.0f * PI @@ -50,6 +52,8 @@ class SineWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGene } override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SineWave(note, duration, volume) + + override val name: String = "sine" } class SquareWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -63,6 +67,8 @@ class SquareWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGe } override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SquareWave(note, duration, volume) + + override val name: String = "square" } class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -73,6 +79,8 @@ class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : Wave } override fun copy(duration: Seconds, volume: Percent): WaveGenerator = TriangleWave(note, duration, volume) + + override val name: String = "triangle" } class NoiseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -86,6 +94,8 @@ class NoiseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGen } override fun copy(duration: Seconds, volume: Percent): WaveGenerator = NoiseWave(note, duration, volume) + + override val name: String = "noise" } class PulseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -100,6 +110,8 @@ class PulseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGen } override fun copy(duration: Seconds, volume: Percent): WaveGenerator = PulseWave(note, duration, volume) + + override val name: String = "pulse" } class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -110,6 +122,8 @@ class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : Wave } override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SawToothWave(note, duration, volume) + + override val name: String = "sawtooth" } class SilenceWave(duration: Seconds) : WaveGenerator(Note.C0, duration, 1.0f) { @@ -120,4 +134,6 @@ class SilenceWave(duration: Seconds) : WaveGenerator(Note.C0, duration, 1.0f) { } override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SilenceWave(duration) + + override val name: String = " " } From 4f63bffc58d0afa6d9be0ee3029f4f669c23d139 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 5 Feb 2024 12:00:00 +0100 Subject: [PATCH 35/48] Read and play the new SFX format file --- .../github/minigdx/tiny/engine/GameEngine.kt | 12 ++ .../minigdx/tiny/engine/GameResourceAccess.kt | 4 + .../com/github/minigdx/tiny/lua/SfxLib.kt | 138 +++++++++--------- .../com/github/minigdx/tiny/sound/Song.kt | 20 ++- .../github/minigdx/tiny/sound/SoundManager.kt | 130 ++++++++++++++++- .../minigdx/tiny/sound/WaveGenerator.kt | 6 +- .../com/github/minigdx/tiny/lua/SfxLibTest.kt | 29 +--- .../minigdx/tiny/sound/SoundConverterTest.kt | 36 +++++ .../platform/webgl/PicoAudioSoundMananger.kt | 6 +- .../platform/glfw/JavaMidiSoundManager.kt | 5 +- 10 files changed, 281 insertions(+), 105 deletions(-) create mode 100644 tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt index 4c77d44f..58329c72 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt @@ -24,6 +24,7 @@ import com.github.minigdx.tiny.resources.ResourceType.GAME_SOUND import com.github.minigdx.tiny.resources.ResourceType.GAME_SPRITESHEET import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.resources.SpriteSheet +import com.github.minigdx.tiny.sound.Song import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.WaveGenerator import kotlinx.coroutines.CoroutineScope @@ -95,7 +96,10 @@ class GameEngine( private val debugActions = mutableListOf() private val notes = mutableListOf() + + @Deprecated("use [song] instead") private var sfx: List? = null + private var song: Song? = null private var longuestDuration: Seconds = 0f private lateinit var scripts: Array @@ -331,6 +335,9 @@ class GameEngine( sfx?.run { soundManager.playSfx(this) } sfx = null + song?.run { soundManager.playSong(this) } + song = null + // Fixed step simulation accumulator += delta if (accumulator >= REFRESH_LIMIT) { @@ -488,10 +495,15 @@ class GameEngine( notes.add(wave) } + @Deprecated("use sfx(song) instead") override fun sfx(waves: List) { sfx = waves } + override fun sfx(song: Song) { + this.song = song + } + override fun script(name: String): GameScript? { return scripts .drop(1) // drop the _boot.lua diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt index 7de355a9..49b361f6 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt @@ -5,6 +5,7 @@ import com.github.minigdx.tiny.resources.GameLevel import com.github.minigdx.tiny.resources.GameScript import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.resources.SpriteSheet +import com.github.minigdx.tiny.sound.Song import com.github.minigdx.tiny.sound.WaveGenerator sealed interface DebugAction @@ -58,8 +59,11 @@ interface GameResourceAccess { /** * Play the sfx represented by this list of waves. */ + @Deprecated("use sfx(song) instead") fun sfx(waves: List) + fun sfx(song: Song) + /** * Find a script by its name. */ diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index 26f9a4db..23fd5687 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -215,75 +215,6 @@ class SfxLib( } } - fun convertToWave(note: String, duration: Seconds): WaveGenerator { - val wave = note.substring(0, 2).toInt(16) - val noteIndex = note.substring(2, 4).toInt(16) - val volume = note.substring(4, 6).toInt(16) / 255f - - return when (wave) { - 1 -> SineWave(Note.fromIndex(noteIndex), duration, volume) - 2 -> SquareWave(Note.fromIndex(noteIndex), duration, volume) - 3 -> TriangleWave(Note.fromIndex(noteIndex), duration, volume) - 4 -> NoiseWave(Note.fromIndex(noteIndex), duration, volume) - 5 -> PulseWave(Note.fromIndex(noteIndex), duration, volume) - 6 -> SawToothWave(Note.fromIndex(noteIndex), duration, volume) - else -> SilenceWave(duration) - } - } - - fun convertScoreToSong(score: String): Song { - val lines = score.lines() - if (lines.isEmpty()) { - throw IllegalArgumentException( - "The content of the score is empty. Can't convert it into a song. " + - "Check if the score is not empty or correctly loaded!", - ) - } - - val header = lines.first() - if (!header.startsWith(TINY_SFX_HEADER)) { - throw IllegalArgumentException( - "The '$TINY_SFX_HEADER' is missing from the fist line of the score. " + - "Is the score a valid score?", - ) - } - - val (_, nbPattern, bpm) = header.split(" ") - - val duration = 60f / bpm.toFloat() / 4f - - // Map - val patterns = lines.drop(1).take(nbPattern.toInt()).mapIndexed { indexPattern, pattern -> - val beatsStr = pattern.split(" ") - val beats = convertToBeats(beatsStr, duration) - Pattern(indexPattern + 1, beats) - }.associateBy { it.index } - - val patternOrder = lines.drop(nbPattern.toInt() + 1).firstOrNull() - val orders = if (patternOrder.isNullOrBlank()) { - listOf(1) - } else { - patternOrder.split(" ").map { it.toInt() } - } - - val patternsOrdered = orders.map { patterns[it]!! } - - return Song(bpm.toInt(), patterns, patternsOrdered) - } - - private fun convertToBeats(beatsStr: List, duration: Seconds): List { - val beats = beatsStr - .asSequence() - .mapIndexed { index, beat -> - val notes = beat.split(":") - .asSequence() - .filter { it.isNotBlank() } - .map { note -> convertToWave(note, duration) } - Beat(index + 1, notes.toList()) - } - return beats.toList() - } - companion object { private val acceptedTypes = setOf("sine", "noise", "pulse", "triangle", "saw", "square") @@ -329,6 +260,75 @@ class SfxLib( return waves } + fun convertToWave(note: String, duration: Seconds): WaveGenerator { + val wave = note.substring(0, 2).toInt(16) + val noteIndex = note.substring(2, 4).toInt(16) + val volume = note.substring(4, 6).toInt(16) / 255f + + return when (wave) { + 1 -> SineWave(Note.fromIndex(noteIndex), duration, volume) + 2 -> SquareWave(Note.fromIndex(noteIndex), duration, volume) + 3 -> TriangleWave(Note.fromIndex(noteIndex), duration, volume) + 4 -> NoiseWave(Note.fromIndex(noteIndex), duration, volume) + 5 -> PulseWave(Note.fromIndex(noteIndex), duration, volume) + 6 -> SawToothWave(Note.fromIndex(noteIndex), duration, volume) + else -> SilenceWave(duration) + } + } + + fun convertScoreToSong(score: String): Song { + val lines = score.lines() + if (lines.isEmpty()) { + throw IllegalArgumentException( + "The content of the score is empty. Can't convert it into a song. " + + "Check if the score is not empty or correctly loaded!", + ) + } + + val header = lines.first() + if (!header.startsWith(TINY_SFX_HEADER)) { + throw IllegalArgumentException( + "The '$TINY_SFX_HEADER' is missing from the fist line of the score. " + + "Is the score a valid score?", + ) + } + + val (_, nbPattern, bpm) = header.split(" ") + + val duration = 60f / bpm.toFloat() / 4f + + // Map + val patterns = lines.drop(1).take(nbPattern.toInt()).mapIndexed { indexPattern, pattern -> + val beatsStr = pattern.split(" ") + val beats = convertToBeats(beatsStr, duration) + Pattern(indexPattern + 1, beats) + }.associateBy { it.index } + + val patternOrder = lines.drop(nbPattern.toInt() + 1).firstOrNull() + val orders = if (patternOrder.isNullOrBlank()) { + listOf(1) + } else { + patternOrder.split(" ").map { it.toInt() } + } + + val patternsOrdered = orders.map { patterns[it]!! } + + return Song(bpm.toInt(), patterns, patternsOrdered) + } + + private fun convertToBeats(beatsStr: List, duration: Seconds): List { + val beats = beatsStr + .asSequence() + .mapIndexed { index, beat -> + val notes = beat.split(":") + .asSequence() + .filter { it.isNotBlank() } + .map { note -> convertToWave(note, duration) } + Beat(index + 1, notes.toList()) + } + return beats.toList() + } + private const val TINY_SFX_HEADER = "tiny-sfx" } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt index 54dbdc1f..be95ff5d 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt @@ -1,6 +1,24 @@ package com.github.minigdx.tiny.sound +import com.github.minigdx.tiny.Seconds + +/** + * A beat is the reprensentation of one moment in a music. It can be composed of multiple notes of multiple + * kind of wave. + */ data class Beat(val index: Int, val notes: List) +/** + * A pattern is a part of a song. A song is compose of multiple pattern played in a specific order. + */ data class Pattern(val index: Int, val beats: List) -data class Song(val bpm: Int, val patterns: Map, val music: List) + +/** + * A song is a group of pattern. + */ +data class Song(val bpm: Int, val patterns: Map, val music: List) { + + val durationOfBeat: Seconds = (bpm / 60f / 4f) + + val numberOfBeats = music.count() * 32 +} diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index 38474eab..376467c8 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -2,6 +2,8 @@ package com.github.minigdx.tiny.sound import com.github.minigdx.tiny.Seconds import com.github.minigdx.tiny.input.InputHandler +import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE +import kotlin.math.min interface Sound { fun play() @@ -81,7 +83,14 @@ abstract class SoundManager { // crossover if (prec != null && i <= crossover) { - sampleMixed = (sampleMixed + fadeOut(prec!!.generate(lastSample + i), lastSample + i, lastSample, lastSample + crossover)) + sampleMixed = ( + sampleMixed + fadeOut( + prec!!.generate(lastSample + i), + lastSample + i, + lastSample, + lastSample + crossover, + ) + ) } result[currentIndex++] = sampleMixed } @@ -120,9 +129,128 @@ abstract class SoundManager { } } + fun playSong(song: Song) { + val mix = createBufferFromSong(song) + + playBuffer(mix) + } + + private val converter = SoundConverter() + + fun createBufferFromSong(song: Song): FloatArray { + val numberOfSamplesPerBeat = (song.durationOfBeat * SAMPLE_RATE).toInt() + val strips = converter.prepateStrip(song) + val buffers = strips.map { (kind, strip) -> + kind to converter.createStrip(numberOfSamplesPerBeat, strip) + }.toMap() + + val numberOfTotalSamples = numberOfSamplesPerBeat * (song.numberOfBeats + 1) + + val mix = FloatArray(numberOfTotalSamples) + (0 until numberOfTotalSamples).forEach { sample -> + var result = 0f + buffers.forEach { (_, line) -> + result += line[sample] + } + mix[sample] = result / buffers.size.toFloat() + } + return mix + } + companion object { const val SAMPLE_RATE = 44100 private const val FADE_OUT_DURATION: Seconds = 0.5f } } + +class SoundConverter { + + internal fun prepateStrip(song: Song): Map> { + // Create a line per WaveGenerator kind. + val musicPerType: MutableMap> = mutableMapOf() + val beats = song.music.flatMap { pattern -> pattern.beats } + val silence = SilenceWave(song.durationOfBeat) + beats.forEachIndexed { index, beat -> + beat.notes.forEach { + val waves = musicPerType.getOrPut(it.name) { Array(song.numberOfBeats + 1) { silence } } + waves[index] = it + } + } + return musicPerType + } + + internal fun createStrip(numberOfSamplesPerBeat: Int, waves: Array): FloatArray { + // 1/4 of a beat is used to fade + val fader = Fader(0.25f * numberOfSamplesPerBeat / SAMPLE_RATE.toFloat()) + + val result = FloatArray(waves.size * numberOfSamplesPerBeat) + val cursor = Cursor() + + // Create the first wave. + val firstBeat = waves.first() + (0 until numberOfSamplesPerBeat).forEach { sample -> + val volume = firstBeat.volume + val value = firstBeat.generate(cursor.current) + val sampled = value * volume + result[cursor.absolute] = sampled + cursor.advance() + } + + // crossfade the current wave with the previous one + (1 until waves.size).forEach { + val a = waves[it - 1] + val b = waves[it] + + cursor.next() + + (0 until numberOfSamplesPerBeat).forEach { sample -> + val sampled = fader.fadeWith(cursor.previous, a, cursor.current, b) + result[cursor.absolute] = sampled + cursor.advance() + } + } + + return result + } +} + +class Cursor(var previous: Int = 0, var current: Int = 0, var absolute: Int = 0) { + fun next(): Cursor { + previous = current + current = 0 + return this + } + + fun advance(): Int { + val r = current + current++ + absolute++ + return r + } +} + +class Fader(duration: Seconds) { + + private val cuteoff = (duration * SAMPLE_RATE).toInt() + + fun fadeWith( + previousSample: Int, + previous: WaveGenerator?, + currentSample: Int, + current: WaveGenerator?, + ): Float { + val prec = previous?.generate(previousSample) ?: 0f + val precVol = previous?.volume ?: 1f + val cur = current?.generate(currentSample) ?: 0f + val curVol = current?.volume ?: 1f + + val alpha = if (previous?.note == current?.note) { + 1f + } else { + 1f - min(cur / cuteoff, 1f) + } + + return cur * curVol + prec * alpha * precVol + } +} diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index 811832df..c66ba70e 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -8,7 +8,11 @@ import kotlin.math.abs import kotlin.math.sin import kotlin.random.Random -sealed class WaveGenerator(val note: Note, val duration: Seconds, val volume: Percent) { +sealed class WaveGenerator( + val note: Note, + val duration: Seconds, + val volume: Percent, +) { val period = SAMPLE_RATE.toFloat() / note.frequency diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt index dab9691e..e041d3c3 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt @@ -1,33 +1,13 @@ package com.github.minigdx.tiny.lua -import com.github.minigdx.tiny.engine.GameResourceAccess -import com.github.minigdx.tiny.graphic.ColorPalette -import com.github.minigdx.tiny.graphic.FrameBuffer -import com.github.minigdx.tiny.resources.GameLevel -import com.github.minigdx.tiny.resources.GameScript -import com.github.minigdx.tiny.resources.Sound -import com.github.minigdx.tiny.resources.SpriteSheet import com.github.minigdx.tiny.sound.SilenceWave import com.github.minigdx.tiny.sound.SineWave -import com.github.minigdx.tiny.sound.WaveGenerator import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class SfxLibTest { - private val mockResources = object : GameResourceAccess { - override val bootSpritesheet: SpriteSheet? = null - override val frameBuffer: FrameBuffer = FrameBuffer(10, 10, ColorPalette(listOf("#FFFFFF"))) - override fun spritesheet(index: Int): SpriteSheet? = null - override fun spritesheet(sheet: SpriteSheet) = Unit - override fun level(index: Int): GameLevel? = null - override fun sound(index: Int): Sound? = null - override fun script(name: String): GameScript? = null - override fun note(wave: WaveGenerator) = Unit - override fun sfx(waves: List) = Unit - } - fun trim(str: String): String { val lastIndex = str.lastIndexOf(')') if (lastIndex < 0) return str @@ -49,8 +29,7 @@ class SfxLibTest { |1 2 1 """.trimMargin() - val lib = SfxLib(mockResources) - val song = lib.convertScoreToSong(score) + val song = SfxLib.convertScoreToSong(score) assertEquals(120, song.bpm) // patterns by index @@ -61,16 +40,14 @@ class SfxLibTest { @Test fun convertToNote() { - val lib = SfxLib(mockResources) - val wave = lib.convertToWave("0101FF", 0.1f) + val wave = SfxLib.convertToWave("0101FF", 0.1f) assertTrue(wave::class == SineWave::class) assertEquals(Note.C0, wave.note) } @Test fun convertToNoteWithSilence() { - val lib = SfxLib(mockResources) - val wave = lib.convertToWave("FFFFFF", 0.1f) + val wave = SfxLib.convertToWave("FFFFFF", 0.1f) assertTrue(wave::class == SilenceWave::class) assertTrue(wave.isSilence) assertEquals(0.1f, wave.duration) diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt new file mode 100644 index 00000000..324f72ed --- /dev/null +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt @@ -0,0 +1,36 @@ +package com.github.minigdx.tiny.sound + +import com.github.minigdx.tiny.lua.Note +import kotlin.test.Test +import kotlin.test.assertEquals + +class SoundConverterTest { + + @Test + fun createStrip() { + val result = SoundConverter().createStrip( + 5, + arrayListOf( + PulseWave(Note.A4, 0.1f), + PulseWave(Note.A4, 0.1f), + SilenceWave(0.1f), + ), + ) + + assertEquals(15, result.size) + } + + @Test + fun prepareStrip() { + val sine = SineWave(Note.C0, 0.1f) + val pulse = PulseWave(Note.C0, 0.1f) + val pattern = Pattern(1, listOf(Beat(1, listOf(sine, pulse)))) + val song = Song(120, mapOf(pattern.index to pattern), listOf(pattern, pattern)) + + val result = SoundConverter().prepateStrip(song) + + assertEquals(2, result.size) + assertEquals(65, result[sine.name]!!.size) + assertEquals(65, result[pulse.name]!!.size) + } +} diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index 1ce0e6e8..454a4851 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -4,7 +4,6 @@ import com.github.minigdx.tiny.input.InputHandler import com.github.minigdx.tiny.lua.SfxLib import com.github.minigdx.tiny.sound.Sound import com.github.minigdx.tiny.sound.SoundManager -import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE import org.khronos.webgl.Float32Array import org.khronos.webgl.set @@ -57,9 +56,8 @@ class PicoAudioSoundMananger : SoundManager() { override suspend fun createSfxSound(bytes: ByteArray): Sound { val score = bytes.decodeToString() - val duration = 60f / 120f / 4.0f - val waves = SfxLib.convertScoreToWaves(score, duration) - val buffer = convertBuffer(createScoreBuffer(waves)) + val song = SfxLib.convertScoreToSong(score) + val buffer = convertBuffer(createBufferFromSong(song)) return SfxSound(buffer, this) } diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index f4a532f1..9e8acc9c 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -123,10 +123,9 @@ class JavaMidiSoundManager : SoundManager() { override suspend fun createSfxSound(bytes: ByteArray): Sound { val score = bytes.decodeToString() - val duration = 60f / 120f / 4.0f - val waves = SfxLib.convertScoreToWaves(score, duration) + val waves = SfxLib.convertScoreToSong(score) - val buffer = convertBuffer(createScoreBuffer(waves)) + val buffer = convertBuffer(createBufferFromSong(waves)) return SfxSound(buffer) } From d8f7a5b9e64c84e333a11a54b5a5f67f68d90c23 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 5 Feb 2024 15:18:37 +0100 Subject: [PATCH 36/48] Add counter on the SFX app. - counter for the bpm of the song - counter for the actual pattern --- tiny-cli/sfx.aseprite | Bin 1573 -> 1853 bytes tiny-cli/src/jvmMain/resources/sfx/game.lua | 36 +++++++++ tiny-cli/src/jvmMain/resources/sfx/sfx.png | Bin 1234 -> 1629 bytes .../src/jvmMain/resources/sfx/widgets.lua | 70 +++++++++++++++++- 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/tiny-cli/sfx.aseprite b/tiny-cli/sfx.aseprite index 374b8054f1df0e615b24ca3c5e3c49362a1fe3c3..f99482732a7607499ef7224ef83d6c3b8b65e8ce 100644 GIT binary patch delta 965 zcmV;$13LVr480BlJqM8jegeG)v4Rf+f2;!l00kfd000000RI92000000002q05AY} zob8-3a?~&sKz$KPhGFQr1;a2Dl-vS38j4&ZT^brL0RI3w(_^32yq`Sy5MTRm-+zSg=a;I*f0#6P zM&iAn|8nQur>8z`Cu0DU=zTwp`y>xEEykpCXq`)KCv(o#JKVb*au?QKQ@r!{4)-nt zyi>6Y?{M$<;GK%yc!zt(rt~hq){|#m(l~gvq8>+UL*%y&xM&=R z`$m7#Ppva4b~N65%o5(d;ofmcb6Mlv`**L+^CQNN#(R%h#M?96J3e_HYPGmuib?)x z+pKs`y#2yY0001h4zAvW+XffM=MUGW*EPnKZT4e3HOaTqyV~{&;bsYWf3rN4=U>CL z*Ealq)_tt-@k@}J>BK)@4nGI$_(@p3!Mlyoc-L?+pDUJmlboC3UUaVJowQwYY{0jxJZt5-+O=G5+p3SHcZ2Tr#!7iDUo-X`L)s7T z`swyAr1pKI{G( zxeeg|Lv8~A0D#2ke`hYe;{lFF`s}&a_p6M?dygN0g*bO-e4m(Cy!ZG4Sn&1+@8FDh zdxLjyM!dbjJ2)fW-ryaa5$}2=^-MS`*86LF=?6F(wS2RdkM`2^+d;9<_ n)av+ab$)BUn|*xm@dL2nT^FIQ0%v9GI{()F07v65FvY{<;baFg delta 683 zcmV;c0#yCI4y6nNB?gfKegdThv4Rf+f0P0M00kfd000000RI92000000002q0385$ zob8-3PQ)+}MST%Ugb+QqKnPJ#atu0J4njl2C7|FU6cjX^z{m?mvzG15jGc_V{3lK0 zcs7aSw|1QDI_KQc*|pb%`=vGC7}c@&3hR>xec@i$Q0L|K&HJwpQG}ehkCyx`f7)V! z-ErI>+r%Jk06Gw^W$fQe*EQA+9!Mg7HrH=N!$c=;WZY=NM-n^8&OC6+j|EqVF&k*n&;&4bkfOmS} z4d9S-pyXZZ4ZPb2V;0~dbpYPwf5ChaNPu_kSU3mw=0@T`s2ktaf_FbyItTaWg?c3M zF1_#LM9gi?w{EkDwQq25Zm18n2=zk!+nY;u{|LFQ`PS_fu=Wh@9R{3vBCQ|&kw&k4 zORW8Zp8x;=000aguKX?l0NW6KOT89gEgLm`b-GvV!P8`6y)U_7(qE}hJ zN-nP9U4wh@ZVsq#HTSaHYTl*WR`9Omn8rQoRq9x6-np800{7B+dh47WtV_-%Zdr9K z%WX^6UrPO>)`w`lCATE*f7LvzjdP`Yxl-ApkEwT!?s0R0J#6nNwXsdO#?lkc9JDs4 zl&r7pRq6==2BZf(K@%w@8O-$IqF`nTzM*B zV?=8$x%;_vyCD+imbP(d_Kw+JlK*c}oCX*j=DOv7p~*Wz`-S&i89C04@?IL!C$}M%$8BUXK6H^^nbSTfqVuWQ5&Q2BwfFH zAx<8DwN=60dk^ExxkwQaiVqz~b`Ca8>r{MnuHPE3S33Tm7!6GDpfsh2~g9I=j zlE8WyjVCGJpMS;2L$cBL8{i;g0_&kTFuu7JgB=?<@(CcH0{r2{p5MrfrnSY84kpT7 zzj~qXw@~TGCxE&DI*cZp0A3Hp8Guz{44}ec_1A{jFQlC|!T;#al|k)u382Jb@1P+a z5G=={=gAxv31CKF!6*sP2MM5{?;z}Z`#gV}TjvKgD1W#SF+DH8%enl;$BUF7f}sT8 z$Q0npxqkIRY;K*8RU8D&&Ypfh{rp?E$$5FeaX}w>+`t#M-!}GDcQ-Qz!AjRiDW&25>?D+c;%Oc{)e<3smteAB@bo+ zygvH*F2I~*kRPRP(Wwm`? z=Cxy|_8&7G&m@2pr&2^j7CS4K<%>bTXcEW+yetW{X%#H2rz8+jUejN@aVtA}x_M%L zI#U#B2(vyMbl^T3G7ErBk<-?o{bzrNMjK*N?m_EBVIpIO_nfMQKwl?ckpM5ygo$$kP@ zsL4Mkz~_~zIyjpiwDW?yED4k)g|0P!mw!RN)@8O|p8JxEuTM{Ex^y|>%ZoSnpTBf@ z5*J^e2J_|n%}*SsoH290Awf(IF&*}dUQ|*_5X1fBVwf~AJb_bRj{D)%u<+)R< zdGO~hU7j5HUG=)6F2UZEF2FF&odqZ+fyTjKHTBnGI`Da@>JH2c@Sz8PmD%90Q-2lw zUwr#XmY?d~v{|pVUmcC60bAGlZN~!+QCW9k9fE)F?I&^U*ZI41@9ifM_uIXVw*7fs zfYq^*&G}W*`Q)GuEu|)3%Bl5{9u*6 z5b<;#esk-5Y;K)@rb*sp`K-3(7g;~h&Hf-@eq+PBTNmfcs&0aq2XL42O;NT#Z~Z_! z`w3uPa2J=A1!*3bb+<^aE8k@M`MM8 zISn{(nyzelF)+K7@49}Vn?X$DnFa)t;RDVXSU>**irLvjz$l;=00000NkvXXu0mjf DtMR_! delta 984 zcmV;}11J354AKdZFn;6bOpLBP> z9~{0vdL>z>=h6GTuP%#-=&kfq)bQ^ghuI|Bw=DHm`mJreBY($$3CUw&R=(Gz-bz0m z>}|%{^~;qS9=(1O-+mVLvAo)yb9$aIk4?ai&wCA(7cYxz0P`8YNCM1B4kObyWZ>68|@3i>5 zOP1Q+0X|U<%zuyKxY+V)bare&51?c81m@4d-e%joFwnyH0P-oor=PdHBWH}81iExJ zS!Bf-fLXHe31CW1s?&!?u-V+ z0yv|sV3Y(F!}99GP7Ml500nIaVcXlMo`b#3SYCbD_J3~FvJ`QAUw)l?`HRn+wC{qh z1Ygg605j*-+Rg06(W^905;)k~j9Y6rJM;Z!_wU|Lzy4ltGT@u9K1M`DllDLU{7dTx zqbtE*Bnix(7e}u~o=05@{)mVyBI5qt+qD8958kHj+tkJ2uR@{#0H95~cT()$ndvSy zERxCH3V*zNC&i;@*|`KzeE-q2k@afYk5lclji_AXUOPeE=hR3E4(ubx+(p_^%zN#yyer{_4PW z4Eij4d@n0L8tuTl8+ub*sB;M*#d=DyK9eV2@qfzSGzkp5@>l5&m%5C;9ja+2w%JEH2+dC0IY#pAwzrE9s ztt-b#Vdrl*4)Mv*LBO-6K|ZKRvITkY=eBqHr~VFFj>R@|8nBoI z#t!~LQ-2kv124j$a$r<|cRlzAIUD?N)-YJpfce1S*lVpi9xz2^<-$A!|36=U9BY;1 zF2`b+$?p6tseN-$jh@mtKg#OD53rMg1;mq(1sxcG3-=#Z)#ac+N Date: Mon, 5 Feb 2024 16:21:01 +0100 Subject: [PATCH 37/48] Load song into lua --- .../github/minigdx/tiny/engine/GameEngine.kt | 10 ---------- .../minigdx/tiny/engine/GameResourceAccess.kt | 6 ------ .../com/github/minigdx/tiny/lua/SfxLib.kt | 17 +++++++++-------- .../com/github/minigdx/tiny/sound/Song.kt | 4 ++-- .../github/minigdx/tiny/sound/SoundManager.kt | 4 ++-- .../com/github/minigdx/tiny/lua/GfxLibTest.kt | 4 +++- .../com/github/minigdx/tiny/lua/SfxLibTest.kt | 5 ++++- .../com/github/minigdx/tiny/lua/StdLibTest.kt | 3 ++- .../minigdx/tiny/sound/SoundConverterTest.kt | 4 ++-- 9 files changed, 24 insertions(+), 33 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt index 58329c72..d3b70b87 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameEngine.kt @@ -97,8 +97,6 @@ class GameEngine( private val notes = mutableListOf() - @Deprecated("use [song] instead") - private var sfx: List? = null private var song: Song? = null private var longuestDuration: Seconds = 0f @@ -332,9 +330,6 @@ class GameEngine( notes.clear() longuestDuration = 0f - sfx?.run { soundManager.playSfx(this) } - sfx = null - song?.run { soundManager.playSong(this) } song = null @@ -495,11 +490,6 @@ class GameEngine( notes.add(wave) } - @Deprecated("use sfx(song) instead") - override fun sfx(waves: List) { - sfx = waves - } - override fun sfx(song: Song) { this.song = song } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt index 49b361f6..1b547275 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/engine/GameResourceAccess.kt @@ -56,12 +56,6 @@ interface GameResourceAccess { */ fun note(wave: WaveGenerator) - /** - * Play the sfx represented by this list of waves. - */ - @Deprecated("use sfx(song) instead") - fun sfx(waves: List) - fun sfx(song: Song) /** diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index 23fd5687..b85055ac 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -170,8 +170,10 @@ class SfxLib( private fun Beat.toLuaTable(): LuaTable { val beat = LuaTable() notes.forEach { wave -> - beat.set("type", wave.name) - beat.set("note", wave.note.index) + val note = LuaTable() + note.set("type", wave.name) + note.set("note", wave.note.index) + beat.insert(0, note) } return beat } @@ -190,6 +192,7 @@ class SfxLib( } val result = LuaTable() result["bpm"] = valueOf(song.bpm) + result["volume"] = valueOf(song.volume.toDouble()) result["patterns"] = patterns return result } @@ -198,10 +201,8 @@ class SfxLib( inner class sfx : TwoArgFunction() { override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { - val bpm = arg2.optint(120) - val duration = 60 / bpm.toFloat() / 4 val score = arg1.optjstring("")!! - val waves = convertScoreToWaves(score, duration) + val waves = convertScoreToSong(score) resourceAccess.sfx(waves) return NIL } @@ -293,13 +294,13 @@ class SfxLib( ) } - val (_, nbPattern, bpm) = header.split(" ") + val (_, nbPattern, bpm, volume) = header.split(" ") val duration = 60f / bpm.toFloat() / 4f // Map val patterns = lines.drop(1).take(nbPattern.toInt()).mapIndexed { indexPattern, pattern -> - val beatsStr = pattern.split(" ") + val beatsStr = pattern.trim().split(" ") val beats = convertToBeats(beatsStr, duration) Pattern(indexPattern + 1, beats) }.associateBy { it.index } @@ -313,7 +314,7 @@ class SfxLib( val patternsOrdered = orders.map { patterns[it]!! } - return Song(bpm.toInt(), patterns, patternsOrdered) + return Song(bpm.toInt(), volume.toInt() / 255f, patterns, patternsOrdered) } private fun convertToBeats(beatsStr: List, duration: Seconds): List { diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt index be95ff5d..9c5142ea 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt @@ -3,7 +3,7 @@ package com.github.minigdx.tiny.sound import com.github.minigdx.tiny.Seconds /** - * A beat is the reprensentation of one moment in a music. It can be composed of multiple notes of multiple + * A beat is the representation of one moment in a music. It can be composed of multiple notes of multiple * kind of wave. */ data class Beat(val index: Int, val notes: List) @@ -16,7 +16,7 @@ data class Pattern(val index: Int, val beats: List) /** * A song is a group of pattern. */ -data class Song(val bpm: Int, val patterns: Map, val music: List) { +data class Song(val bpm: Int, val volume: Float, val patterns: Map, val music: List) { val durationOfBeat: Seconds = (bpm / 60f / 4f) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index 376467c8..af3ece79 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -193,7 +193,7 @@ class SoundConverter { val volume = firstBeat.volume val value = firstBeat.generate(cursor.current) val sampled = value * volume - result[cursor.absolute] = sampled + result[sample] = sampled cursor.advance() } @@ -206,7 +206,7 @@ class SoundConverter { (0 until numberOfSamplesPerBeat).forEach { sample -> val sampled = fader.fadeWith(cursor.previous, a, cursor.current, b) - result[cursor.absolute] = sampled + result[sample] = sampled cursor.advance() } } diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt index 736c85a0..5cc04679 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/GfxLibTest.kt @@ -7,6 +7,7 @@ import com.github.minigdx.tiny.resources.GameLevel import com.github.minigdx.tiny.resources.GameScript import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.resources.SpriteSheet +import com.github.minigdx.tiny.sound.Song import com.github.minigdx.tiny.sound.WaveGenerator import org.luaj.vm2.LuaValue.Companion.valueOf import kotlin.test.Test @@ -23,7 +24,8 @@ class GfxLibTest { override fun sound(index: Int): Sound? = null override fun script(name: String): GameScript? = null override fun note(wave: WaveGenerator) = Unit - override fun sfx(waves: List) = Unit + + override fun sfx(song: Song) = Unit } @Test diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt index e041d3c3..a79847d4 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/SfxLibTest.kt @@ -23,7 +23,7 @@ class SfxLibTest { @Test fun scoreToSong() { - val score = """tiny-sfx 2 120 + val score = """tiny-sfx 2 120 255 |0101FF:0202FF 0101FF:0202FF |0101FF:0202FF 0101FF:0202FF |1 2 1 @@ -32,10 +32,13 @@ class SfxLibTest { val song = SfxLib.convertScoreToSong(score) assertEquals(120, song.bpm) + assertEquals(1f, song.volume) // patterns by index assertEquals(2, song.patterns.size) // patterns ordered by usage assertEquals(3, song.music.size) + + assertEquals(song.patterns[1]!!.beats.size, 2) } @Test diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt index 1debf387..8ca507df 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/StdLibTest.kt @@ -11,6 +11,7 @@ import com.github.minigdx.tiny.resources.GameScript import com.github.minigdx.tiny.resources.ResourceType import com.github.minigdx.tiny.resources.Sound import com.github.minigdx.tiny.resources.SpriteSheet +import com.github.minigdx.tiny.sound.Song import com.github.minigdx.tiny.sound.WaveGenerator import org.luaj.vm2.LuaValue.Companion.valueOf import org.luaj.vm2.LuaValue.Companion.varargsOf @@ -38,7 +39,7 @@ class StdLibTest { override fun sound(index: Int): Sound? = null override fun script(name: String): GameScript? = null override fun note(wave: WaveGenerator) = Unit - override fun sfx(waves: List) = Unit + override fun sfx(song: Song) = Unit } private val gameOptions = GameOptions( diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt index 324f72ed..4afb985f 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt @@ -10,7 +10,7 @@ class SoundConverterTest { fun createStrip() { val result = SoundConverter().createStrip( 5, - arrayListOf( + arrayOf( PulseWave(Note.A4, 0.1f), PulseWave(Note.A4, 0.1f), SilenceWave(0.1f), @@ -25,7 +25,7 @@ class SoundConverterTest { val sine = SineWave(Note.C0, 0.1f) val pulse = PulseWave(Note.C0, 0.1f) val pattern = Pattern(1, listOf(Beat(1, listOf(sine, pulse)))) - val song = Song(120, mapOf(pattern.index to pattern), listOf(pattern, pattern)) + val song = Song(120, 1f, mapOf(pattern.index to pattern), listOf(pattern, pattern)) val result = SoundConverter().prepateStrip(song) From da2b5292bd904a8894c783f34f55d5441edb8c71 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 5 Feb 2024 22:38:11 +0100 Subject: [PATCH 38/48] Fix issue with all with a "dictionary": all values are now returned. --- .../kotlin/com/github/minigdx/tiny/lua/StdLib.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt index adec1b10..364425da 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt @@ -119,18 +119,18 @@ class StdLib( override fun invoke(args: Varargs): Varargs { val table = args.checktable(1)!! - index += 1 + val keys = table.keys() + if (index >= keys.size) return NONE - val luaValue = table[index] - if (luaValue.isnil()) return NONE + val key = keys[index++] + val result = table.get(key) - // Return only the value. - return varargsOf(arrayOf(luaValue)) + return result } } val table = args.checktable(1)!! // iterator, object to iterate, seed value. - return varargsOf(iterator, table, valueOf(0)) + return varargsOf(iterator, table) } } From a9d75169aee25b21a4d9e6314dd724253b9b2744 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Mon, 5 Feb 2024 22:38:28 +0100 Subject: [PATCH 39/48] sfx: Allow mutiple values on a fader. --- tiny-cli/src/jvmMain/resources/sfx/game.lua | 90 ++++++------------- .../src/jvmMain/resources/sfx/widgets.lua | 38 +++++--- 2 files changed, 52 insertions(+), 76 deletions(-) diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 3059211f..1b7d7964 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -11,37 +11,42 @@ local labels = {"C0", "Db0", "D0", "Eb0", "E0", "F0", "Gb0", "G0", "Ab0", "A0", local waves = {{ type = "sine", - color = 9 + color = 9, + index = 1 }, { type = "noise", - color = 4 + color = 4, + index = 2 }, { type = "pulse", - color = 10 + color = 10, + index = 3 }, { type = "triangle", - color = 13 + color = 13, + index = 4 }, { type = "saw", - color = 11 + color = 11, + index = 5 }, { type = "square", - color = 15 + color = 15, + index = 6 }} +local bpm = nil +local patterns = nil local faders = {} local current_wave = waves[1] function on_fader_update(fader, value) - fader.value = math.ceil(value) - fader.data = { - note = labels[fader.value], - wave = current_wave.type, - value = fader.value, - color = current_wave.color - } - fader.label = labels[fader.value] - fader.tip_color = current_wave.color + widgets.setFaderValue( + fader, + current_wave.index, + math.ceil(value), + current_wave.color + ) end function on_active_button(current, prec) @@ -153,23 +158,22 @@ function _init(w, h) on_active_button = on_play_button }) - - widgets.createCounter({ + patterns = widgets.createCounter({ x = 10, y = 112, value = 1, - label = "pattern", + label = "pattern" -- on_left = on_decrease_bpm, -- on_right = on_increase_bpm, }) - widgets.createCounter({ + bpm = widgets.createCounter({ x = 10, y = 112 + 24, value = 120, label = "bpm", on_left = on_decrease_bpm, - on_right = on_increase_bpm, + on_right = on_increase_bpm }) -- faders @@ -219,7 +223,7 @@ function _init(w, h) width = 2 * 16 + 8, status = 0, label = w, - content = ws.load(w), + content = sfx.to_table(ws.load(w)), on_active_tab = on_active_tab }) table.insert(tabs, tab) @@ -252,20 +256,6 @@ function _init(w, h) init_faders(tabs) end -function extract(inputString) - local pattern = "([a-zA-Z]+)%(([^%)]+)%)" - local wave, note = inputString:match(pattern) - return wave, note -end - -function split(inputString) - local result = {} - for token in string.gmatch(inputString, "[^%-]+") do - table.insert(result, token) - end - return result -end - function init_faders(tabs) local index = 1 @@ -280,33 +270,9 @@ function init_faders(tabs) end for t in all(tabs) do - local content = t.content - if content then - local saved = split(content) - local result = {} - for index, f in ipairs(faders) do - local s = saved[index] - - local data = { - wave = "", - note = 0, - value = 0, - color = 0 - } - - if s ~= "(0)" and s ~= "*" then - local wave, note = extract(s) - data = { - wave = wave, - note = note, - value = notes[note], - color = colors[wave] - } - end - - table.insert(result, data) - end - t.data = result + local song = t.content + if song then + t.data = song end end on_active_tab(tabs[1]) diff --git a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua index 70eda44c..c0434a24 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua @@ -6,6 +6,7 @@ local Fader = { min_value = 0, max_value = 10, value = nil, + values = nil, tip_color = 9, disabled_color = 7, label = "", @@ -15,6 +16,11 @@ local Fader = { end } +local FaderValue = { + value = 0, + color = 0 +} + local Button = { x = 0, y = 0, @@ -81,8 +87,17 @@ factory.createButton = function(value) return result end -factory.createFader = function(value) +factory.setFaderValue = function(fader, index, value, color) + if fader.values == nil then + fader.values = {} + end + fader.values[index] = { + value = value, + color = color + } +end +factory.createFader = function(value) local result = new(Fader, value) table.insert(widgets, result) table.insert(faders, result) @@ -259,20 +274,15 @@ function draw_tab(tab) end function draw_fader(f) - local y = f.height - 4 - - if f.value then - y = f.height - ((f.value - f.min_value) / (f.max_value - f.min_value) * f.height) - end - local tipy = f.y + y - - if f.value > f.min_value then - local linex = f.x + f.width * 0.5 - gfx.dither(0xA5A5) - shape.line(linex, tipy, linex, f.y + f.height, 7) - gfx.dither() - shape.rectf(f.x, tipy, f.width, 4, f.tip_color) + if f.values ~= nil and next(f.values) then + for v in all(f.values) do + local y = f.height - ((v.value - f.min_value) / (f.max_value - f.min_value) * f.height) + local tipy = f.y + y + shape.rectf(f.x, tipy, f.width, 4, v.color) + end else + local y = f.height - (0 / (f.max_value - f.min_value) * f.height) + local tipy = f.y + y shape.rectf(f.x, tipy, f.width, 4, f.disabled_color) end From cb60d35e4f58a92bc93bb36bd86d0df6d8b34ade Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Tue, 6 Feb 2024 09:40:21 +0100 Subject: [PATCH 40/48] Play the minimal song by removing ending silence to limit the thread blocking --- tiny-cli/src/jvmMain/resources/sfx/game.lua | 49 +++++++--- .../src/jvmMain/resources/sfx/widgets.lua | 13 ++- .../com/github/minigdx/tiny/lua/SfxLib.kt | 55 ++---------- .../com/github/minigdx/tiny/sound/Song.kt | 2 +- .../github/minigdx/tiny/sound/SoundManager.kt | 90 +++++-------------- .../minigdx/tiny/sound/SoundConverterTest.kt | 26 +++++- .../platform/webgl/PicoAudioSoundMananger.kt | 14 +-- .../platform/glfw/JavaMidiSoundManager.kt | 13 +-- 8 files changed, 114 insertions(+), 148 deletions(-) diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 1b7d7964..2971d559 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -41,12 +41,7 @@ local faders = {} local current_wave = waves[1] function on_fader_update(fader, value) - widgets.setFaderValue( - fader, - current_wave.index, - math.ceil(value), - current_wave.color - ) + widgets.setFaderValue(fader, current_wave.index, math.ceil(value), current_wave.color) end function on_active_button(current, prec) @@ -108,7 +103,8 @@ end function on_play_button() local score = generate_score() - sfx.sfx(score, 220) + debug.console(score) + sfx.sfx(score) end function on_save_button() @@ -279,16 +275,45 @@ function init_faders(tabs) end +function to_hex(number) + local hexString = string.format("%X", number) + + -- Add a leading zero if the number is below 16 + if number < 16 then + hexString = "0" .. hexString + end + + return hexString +end + function generate_score() - local score = "" + local score = "tiny-sfx 1 " .. bpm.value .. " 255\n" + + -- write patterns + local strip = "" for f in all(faders) do - if f.data ~= nil and f.data.note ~= nil then - score = score .. f.data.wave .. "(" .. f.data.note .. ")-" + local beat = "" + if f.values ~= nil and next(f.values) then + debug.console(f.values) + for k, v in pairs(f.values) do + debug.console("key "..k) + debug.console(v) + if #beat > 0 then + beat = beat .. ":" + end + beat = beat .. to_hex(k) .. to_hex(v.value) .. to_hex(255) + end else - score = score .. "*-" + beat = "0000FF" end + + strip = strip .. beat .. " " end + + score = score .. strip .. "\n" + -- write patterns order + score = score .. "1" return score end @@ -298,7 +323,7 @@ function _update() if ctrl.pressed(keys.space) then local score = generate_score() - sfx.sfx(score, 220) + sfx.sfx(score) end local new_wave = current_wave diff --git a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua index c0434a24..50999406 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua @@ -91,10 +91,15 @@ factory.setFaderValue = function(fader, index, value, color) if fader.values == nil then fader.values = {} end - fader.values[index] = { - value = value, - color = color - } + + if value <= 0 then + fader.values[index] = nil + else + fader.values[index] = { + value = value, + color = color + } + end end factory.createFader = function(value) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index b85055ac..6dfd4aa0 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -218,49 +218,6 @@ class SfxLib( companion object { - private val acceptedTypes = setOf("sine", "noise", "pulse", "triangle", "saw", "square") - - private fun extractWaveType(str: String): String? { - if (str == "*") return str - - val type = str.substringBefore("(") - return if (acceptedTypes.contains(type)) { - type - } else { - null - } - } - - private fun extractNote(str: String): Note { - val note = str.substringAfter("(").substringBefore(")") - return Note.valueOf(note) - } - - private fun trim(str: String): String { - val lastIndex = str.lastIndexOf(')') - if (lastIndex < 0) return str - return str.substring(0, lastIndex + 2) - } - - fun convertScoreToWaves(score: String, duration: Seconds): List { - val parts = trim(score).split("-") - val waves = parts.mapNotNull { - val wave = extractWaveType(it) - when (wave) { - "*" -> SilenceWave(duration) - "sine" -> SineWave(extractNote(it), duration) - "triangle" -> TriangleWave(extractNote(it), duration) - "square" -> SquareWave(extractNote(it), duration) - "saw" -> SawToothWave(extractNote(it), duration) - "noise" -> NoiseWave(extractNote(it), duration) - "pulse" -> PulseWave(extractNote(it), duration) - else -> null - } - } - - return waves - } - fun convertToWave(note: String, duration: Seconds): WaveGenerator { val wave = note.substring(0, 2).toInt(16) val noteIndex = note.substring(2, 4).toInt(16) @@ -268,11 +225,11 @@ class SfxLib( return when (wave) { 1 -> SineWave(Note.fromIndex(noteIndex), duration, volume) - 2 -> SquareWave(Note.fromIndex(noteIndex), duration, volume) - 3 -> TriangleWave(Note.fromIndex(noteIndex), duration, volume) - 4 -> NoiseWave(Note.fromIndex(noteIndex), duration, volume) - 5 -> PulseWave(Note.fromIndex(noteIndex), duration, volume) - 6 -> SawToothWave(Note.fromIndex(noteIndex), duration, volume) + 2 -> NoiseWave(Note.fromIndex(noteIndex), duration, volume) + 3 -> PulseWave(Note.fromIndex(noteIndex), duration, volume) + 4 -> TriangleWave(Note.fromIndex(noteIndex), duration, volume) + 5 -> SawToothWave(Note.fromIndex(noteIndex), duration, volume) + 6 -> SquareWave(Note.fromIndex(noteIndex), duration, volume) else -> SilenceWave(duration) } } @@ -296,7 +253,7 @@ class SfxLib( val (_, nbPattern, bpm, volume) = header.split(" ") - val duration = 60f / bpm.toFloat() / 4f + val duration = 60f / bpm.toFloat() / 8f // Map val patterns = lines.drop(1).take(nbPattern.toInt()).mapIndexed { indexPattern, pattern -> diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt index 9c5142ea..b306b1ac 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/Song.kt @@ -18,7 +18,7 @@ data class Pattern(val index: Int, val beats: List) */ data class Song(val bpm: Int, val volume: Float, val patterns: Map, val music: List) { - val durationOfBeat: Seconds = (bpm / 60f / 4f) + val durationOfBeat: Seconds = (60f / bpm / 8f) val numberOfBeats = music.count() * 32 } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index af3ece79..b9c8f06e 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -27,15 +27,7 @@ abstract class SoundManager { if (notes.isEmpty()) return val result = createNotesBuffer(longestDuration, notes) - playBuffer(result) - } - - fun playSfx(notes: List) { - if (notes.isEmpty()) return - - val result = createScoreBuffer(notes) - - playBuffer(result) + playBuffer(result, result.size) } protected fun createNotesBuffer( @@ -53,58 +45,10 @@ abstract class SoundManager { return result } - protected fun createScoreBuffer(notes: List): FloatArray { - var currentIndex = 0 - - fun merge(head: WaveGenerator, tail: List): List { - if (tail.isEmpty()) { - return listOf(head) - } - - val next = tail.first() - return if (next.isSame(head)) { - merge(head.copy(head.duration + next.duration, head.volume), tail.drop(1)) - } else { - listOf(head) + merge(next, tail.drop(1)) - } - } - - val mergedNotes = merge(notes.first(), notes.drop(1)) + SilenceWave(0.1f) - - var prec: WaveGenerator? = null - var lastSample = 0 - val result = FloatArray((mergedNotes.sumOf { it.duration.toDouble() } * SAMPLE_RATE).toInt()) - mergedNotes.forEach { note -> - val crossover = (0.05f * SAMPLE_RATE).toInt() - val mixedNotes = listOf(note) - val noteSamples = (SAMPLE_RATE * note.duration).toInt() - for (i in 0 until noteSamples) { - var sampleMixed = mix(i, mixedNotes) - - // crossover - if (prec != null && i <= crossover) { - sampleMixed = ( - sampleMixed + fadeOut( - prec!!.generate(lastSample + i), - lastSample + i, - lastSample, - lastSample + crossover, - ) - ) - } - result[currentIndex++] = sampleMixed - } - - prec = note - lastSample = noteSamples - } - return result - } - /** * @param buffer byte array representing the sound. Each sample is represented with a float from -1.0f to 1.0f */ - abstract fun playBuffer(buffer: FloatArray) + abstract fun playBuffer(buffer: FloatArray, numberOfSamples: Int) private fun mix(sample: Int, notes: List): Float { var result = 0f @@ -130,16 +74,16 @@ abstract class SoundManager { } fun playSong(song: Song) { - val mix = createBufferFromSong(song) + val (mix, numberOfSamples) = createBufferFromSong(song) - playBuffer(mix) + playBuffer(mix, numberOfSamples) } private val converter = SoundConverter() - fun createBufferFromSong(song: Song): FloatArray { + fun createBufferFromSong(song: Song): Pair { val numberOfSamplesPerBeat = (song.durationOfBeat * SAMPLE_RATE).toInt() - val strips = converter.prepateStrip(song) + val (lastBeat, strips) = converter.prepareStrip(song) val buffers = strips.map { (kind, strip) -> kind to converter.createStrip(numberOfSamplesPerBeat, strip) }.toMap() @@ -154,7 +98,7 @@ abstract class SoundManager { } mix[sample] = result / buffers.size.toFloat() } - return mix + return mix to lastBeat * numberOfSamplesPerBeat } companion object { @@ -166,18 +110,24 @@ abstract class SoundManager { class SoundConverter { - internal fun prepateStrip(song: Song): Map> { + internal fun prepareStrip(song: Song): Pair>> { // Create a line per WaveGenerator kind. val musicPerType: MutableMap> = mutableMapOf() + // All beats of this music. val beats = song.music.flatMap { pattern -> pattern.beats } val silence = SilenceWave(song.durationOfBeat) + var lastBeat = 0 beats.forEachIndexed { index, beat -> - beat.notes.forEach { + val validNotes = beat.notes.filterNot { it.isSilence } + validNotes.forEach { val waves = musicPerType.getOrPut(it.name) { Array(song.numberOfBeats + 1) { silence } } waves[index] = it } + if (validNotes.isNotEmpty()) { + lastBeat = index + 1 + } } - return musicPerType + return lastBeat to musicPerType } internal fun createStrip(numberOfSamplesPerBeat: Int, waves: Array): FloatArray { @@ -189,11 +139,11 @@ class SoundConverter { // Create the first wave. val firstBeat = waves.first() - (0 until numberOfSamplesPerBeat).forEach { sample -> + (0 until numberOfSamplesPerBeat).forEach { _ -> val volume = firstBeat.volume val value = firstBeat.generate(cursor.current) val sampled = value * volume - result[sample] = sampled + result[cursor.absolute] = sampled cursor.advance() } @@ -204,9 +154,9 @@ class SoundConverter { cursor.next() - (0 until numberOfSamplesPerBeat).forEach { sample -> + (0 until numberOfSamplesPerBeat).forEach { _ -> val sampled = fader.fadeWith(cursor.previous, a, cursor.current, b) - result[sample] = sampled + result[cursor.absolute] = sampled cursor.advance() } } diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt index 4afb985f..0977bb27 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt @@ -27,8 +27,32 @@ class SoundConverterTest { val pattern = Pattern(1, listOf(Beat(1, listOf(sine, pulse)))) val song = Song(120, 1f, mapOf(pattern.index to pattern), listOf(pattern, pattern)) - val result = SoundConverter().prepateStrip(song) + val (lastBeat, result) = SoundConverter().prepareStrip(song) + assertEquals(1, lastBeat) + assertEquals(2, result.size) + assertEquals(65, result[sine.name]!!.size) + assertEquals(65, result[pulse.name]!!.size) + } + + @Test + fun prepareStripWithSilence() { + val sine = SineWave(Note.C0, 0.1f) + val silence = SilenceWave(0.1f) + val pulse = PulseWave(Note.C0, 0.1f) + val pattern = Pattern( + 1, + listOf( + Beat(1, listOf(sine)), + Beat(2, listOf(silence)), + Beat(3, listOf(pulse)), + ), + ) + val song = Song(120, 1f, mapOf(pattern.index to pattern), listOf(pattern, pattern)) + + val (lastBeat, result) = SoundConverter().prepareStrip(song) + + assertEquals(6, lastBeat) assertEquals(2, result.size) assertEquals(65, result[sine.name]!!.size) assertEquals(65, result[pulse.name]!!.size) diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index 454a4851..f981727a 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -57,7 +57,8 @@ class PicoAudioSoundMananger : SoundManager() { override suspend fun createSfxSound(bytes: ByteArray): Sound { val score = bytes.decodeToString() val song = SfxLib.convertScoreToSong(score) - val buffer = convertBuffer(createBufferFromSong(song)) + val (buf, length) = createBufferFromSong(song) + val buffer = convertBuffer(buf, length) return SfxSound(buffer, this) } @@ -67,14 +68,15 @@ class PicoAudioSoundMananger : SoundManager() { return PicoAudioSound(audio, smf) } - override fun playBuffer(buffer: FloatArray) { - val result = convertBuffer(buffer) + override fun playBuffer(buffer: FloatArray, numberOfSamples: Int) { + val result = convertBuffer(buffer, numberOfSamples) playSfxBuffer(result) } - private fun convertBuffer(buffer: FloatArray): Float32Array { - val result = Float32Array(buffer.size) - buffer.forEachIndexed { index, byte -> + private fun convertBuffer(buffer: FloatArray, length: Int): Float32Array { + val result = Float32Array(length) + (0 until length).forEach { index -> + val byte = buffer[index] result[index] = byte } return result diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index 9e8acc9c..92b661d0 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -125,19 +125,22 @@ class JavaMidiSoundManager : SoundManager() { val score = bytes.decodeToString() val waves = SfxLib.convertScoreToSong(score) - val buffer = convertBuffer(createBufferFromSong(waves)) + val (buf, length) = createBufferFromSong(waves) + val buffer = convertBuffer(buf, length) return SfxSound(buffer) } - override fun playBuffer(buffer: FloatArray) { - bufferQueue.offer(convertBuffer(buffer)) + override fun playBuffer(buffer: FloatArray, numberOfSamples: Int) { + bufferQueue.offer(convertBuffer(buffer, numberOfSamples)) } private fun convertBuffer( audioBuffer: FloatArray, + length: Int, ): ByteArray { - val buffer = ByteArray(audioBuffer.size * 2) - audioBuffer.forEachIndexed { i, sample -> + val buffer = ByteArray(length * 2) + (0 until length).forEach { i -> + val sample = audioBuffer[i] val sampleValue: Float = (sample * Short.MAX_VALUE) val clippedValue = sampleValue.coerceIn(Short.MIN_VALUE.toFloat(), Short.MAX_VALUE.toFloat()) val result = clippedValue.toInt().toShort() From 6bfe0eb8a078eef7c899fd867d44c914cc23a269 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Tue, 6 Feb 2024 21:08:26 +0100 Subject: [PATCH 41/48] Restore the context when changing tab. --- tiny-cli/src/jvmMain/resources/sfx/game.lua | 97 +++++++++---------- .../src/jvmMain/resources/sfx/widgets.lua | 5 + .../com/github/minigdx/tiny/lua/SfxLib.kt | 11 ++- .../minigdx/tiny/sound/WaveGenerator.kt | 16 +++ 4 files changed, 73 insertions(+), 56 deletions(-) diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 2971d559..0faeffc0 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -12,28 +12,34 @@ local labels = {"C0", "Db0", "D0", "Eb0", "E0", "F0", "Gb0", "G0", "Ab0", "A0", local waves = {{ type = "sine", color = 9, - index = 1 + index = 1, + overlay = 16 +}, { + type = "square", + color = 15, + index = 2, + overlay = 21, +}, { + type = "triangle", + color = 13, + index = 3, + overlay = 19 }, { type = "noise", color = 4, - index = 2 + index = 4, + overlay = 17 }, { type = "pulse", color = 10, - index = 3 -}, { - type = "triangle", - color = 13, - index = 4 + index = 5, + overlay = 18 }, { type = "saw", color = 11, - index = 5 -}, { - type = "square", - color = 15, + overlay = 20, index = 6 -}} +},} local bpm = nil local patterns = nil @@ -51,40 +57,39 @@ end local active_tab = nil function on_active_tab(current, prec) - local data = {} - -- save the current score - for f in all(faders) do - table.insert(data, { - wave = f.data.wave, - note = f.data.note, - value = f.value, - color = f.tip_color - }) - end if prec ~= nil then - prec.data = data + local score = generate_score() + prec.content = sfx.to_table(score) end - -- restore the previous score - if current.data ~= nil then - local data = current.data + -- restore the previous score of the current tab + if current.content ~= nil then + local data = current.content + bpm.value = data["bpm"] + -- always get the first pattern + local beats = data["patterns"][1] for k, f in ipairs(faders) do - f.data = data[k] - f.value = data[k].value - f.label = labels[f.value] - f.tip_color = data[k].color + widgets.resetFaderValue(f) + if beats[k] ~= nil then + for b in all(beats[k]) do + if b.index > 0 then + local w = waves[b.index] + widgets.setFaderValue(f, b.index, b.note, w.color) + else + -- set silence value + widgets.resetFaderValue(f) + end + end + else + -- set silence value + widgets.resetFaderValue(f) + end end else + bpm.value = 120 -- no data, reset to 0 for k, f in ipairs(faders) do - f.value = 0 - f.label = "" - f.data = { - wave = "", - note = 0, - value = 0, - color = 0 - } + widgets.resetFaderValue(f) end end @@ -103,7 +108,6 @@ end function on_play_button() local score = generate_score() - debug.console(score) sfx.sfx(score) end @@ -189,12 +193,13 @@ function _init(w, h) table.insert(faders, f) end + -- buttons for i = #waves - 1, 0, -1 do local w = widgets.createButton({ x = 10, y = 250 - i * 16, - overlay = 16 + i, + overlay = waves[i + 1].overlay, data = { wave = waves[i + 1] }, @@ -265,14 +270,7 @@ function init_faders(tabs) colors[v.type] = v.color end - for t in all(tabs) do - local song = t.content - if song then - t.data = song - end - end on_active_tab(tabs[1]) - end function to_hex(number) @@ -295,17 +293,14 @@ function generate_score() for f in all(faders) do local beat = "" if f.values ~= nil and next(f.values) then - debug.console(f.values) for k, v in pairs(f.values) do - debug.console("key "..k) - debug.console(v) if #beat > 0 then beat = beat .. ":" end beat = beat .. to_hex(k) .. to_hex(v.value) .. to_hex(255) end else - beat = "0000FF" + beat = "0000FF" end strip = strip .. beat .. " " diff --git a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua index 50999406..62ee13dc 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua @@ -39,6 +39,7 @@ local Tab = { width = 0, height = 8, label = "+", + content = nil, status = 0, -- 0 : inactive ; 1 : active new_tab = false } @@ -102,6 +103,10 @@ factory.setFaderValue = function(fader, index, value, color) end end +factory.resetFaderValue = function(fader) + fader.values = {} +end + factory.createFader = function(value) local result = new(Fader, value) table.insert(widgets, result) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index 6dfd4aa0..c35a48f8 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -172,6 +172,7 @@ class SfxLib( notes.forEach { wave -> val note = LuaTable() note.set("type", wave.name) + note.set("index", wave.index) note.set("note", wave.note.index) beat.insert(0, note) } @@ -225,11 +226,11 @@ class SfxLib( return when (wave) { 1 -> SineWave(Note.fromIndex(noteIndex), duration, volume) - 2 -> NoiseWave(Note.fromIndex(noteIndex), duration, volume) - 3 -> PulseWave(Note.fromIndex(noteIndex), duration, volume) - 4 -> TriangleWave(Note.fromIndex(noteIndex), duration, volume) - 5 -> SawToothWave(Note.fromIndex(noteIndex), duration, volume) - 6 -> SquareWave(Note.fromIndex(noteIndex), duration, volume) + 2 -> SquareWave(Note.fromIndex(noteIndex), duration, volume) + 3 -> TriangleWave(Note.fromIndex(noteIndex), duration, volume) + 4 -> NoiseWave(Note.fromIndex(noteIndex), duration, volume) + 5 -> PulseWave(Note.fromIndex(noteIndex), duration, volume) + 6 -> SawToothWave(Note.fromIndex(noteIndex), duration, volume) else -> SilenceWave(duration) } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt index c66ba70e..630893a0 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/WaveGenerator.kt @@ -44,6 +44,8 @@ sealed class WaveGenerator( abstract val name: String + abstract val index: Int + companion object { internal const val PI = kotlin.math.PI.toFloat() internal const val TWO_PI = 2.0f * PI @@ -58,6 +60,8 @@ class SineWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGene override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SineWave(note, duration, volume) override val name: String = "sine" + + override val index: Int = 1 } class SquareWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -73,6 +77,8 @@ class SquareWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGe override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SquareWave(note, duration, volume) override val name: String = "square" + + override val index: Int = 2 } class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -85,6 +91,8 @@ class TriangleWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : Wave override fun copy(duration: Seconds, volume: Percent): WaveGenerator = TriangleWave(note, duration, volume) override val name: String = "triangle" + + override val index: Int = 3 } class NoiseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -100,6 +108,8 @@ class NoiseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGen override fun copy(duration: Seconds, volume: Percent): WaveGenerator = NoiseWave(note, duration, volume) override val name: String = "noise" + + override val index: Int = 4 } class PulseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -116,6 +126,8 @@ class PulseWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGen override fun copy(duration: Seconds, volume: Percent): WaveGenerator = PulseWave(note, duration, volume) override val name: String = "pulse" + + override val index: Int = 5 } class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : WaveGenerator(note, duration, volume) { @@ -128,6 +140,8 @@ class SawToothWave(note: Note, duration: Seconds, volume: Percent = 1.0f) : Wave override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SawToothWave(note, duration, volume) override val name: String = "sawtooth" + + override val index: Int = 6 } class SilenceWave(duration: Seconds) : WaveGenerator(Note.C0, duration, 1.0f) { @@ -140,4 +154,6 @@ class SilenceWave(duration: Seconds) : WaveGenerator(Note.C0, duration, 1.0f) { override fun copy(duration: Seconds, volume: Percent): WaveGenerator = SilenceWave(duration) override val name: String = " " + + override val index: Int = 0 } From ac641dbd480442583dab5bc826b61bd647075ee0 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Wed, 7 Feb 2024 22:50:51 +0100 Subject: [PATCH 42/48] Switch to other patterns --- tiny-cli/src/jvmMain/resources/sfx/game.lua | 161 +++++++++++++----- .../src/jvmMain/resources/sfx/widgets.lua | 4 + 2 files changed, 121 insertions(+), 44 deletions(-) diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 0faeffc0..97ce1ab1 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -14,11 +14,11 @@ local waves = {{ color = 9, index = 1, overlay = 16 -}, { +}, { type = "square", color = 15, index = 2, - overlay = 21, + overlay = 21 }, { type = "triangle", color = 13, @@ -39,22 +39,76 @@ local waves = {{ color = 11, overlay = 20, index = 6 -},} +}} local bpm = nil local patterns = nil local faders = {} local current_wave = waves[1] +local active_tab = nil + function on_fader_update(fader, value) widgets.setFaderValue(fader, current_wave.index, math.ceil(value), current_wave.color) + + local actual_pattern = active_tab.content["patterns"][patterns.value] + + if actual_pattern == nil then + actual_pattern = {} + active_tab.content["patterns"][patterns.value] = actual_pattern + end + local beat = actual_pattern[fader.index] + + if beat == nil then + beat = {} + actual_pattern[fader.index] = beat + + end + + local note = beat[current_wave.index] + if note == nil then + local n = { + note = 0, + index = current_wave.index, + type = 0 + } + beat[current_wave.index] = n + end + + beat[current_wave.index].index = current_wave.index + beat[current_wave.index].note = math.ceil(value) + end function on_active_button(current, prec) current_wave = current.data.wave end -local active_tab = nil +function active_pattern(index, data) + local beats = data["patterns"][index] + if beats == nil then + beats = {} + data["patterns"][index] = beats + end + + for k, f in ipairs(faders) do + widgets.resetFaderValue(f) + if beats[k] ~= nil then + for b in all(beats[k]) do + if b.index > 0 then + local w = waves[b.index] + widgets.setFaderValue(f, b.index, b.note, w.color) + else + -- set silence value + widgets.resetFaderValue(f) + end + end + else + -- set silence value + widgets.resetFaderValue(f) + end + end +end function on_active_tab(current, prec) if prec ~= nil then @@ -67,30 +121,15 @@ function on_active_tab(current, prec) local data = current.content bpm.value = data["bpm"] -- always get the first pattern - local beats = data["patterns"][1] - for k, f in ipairs(faders) do - widgets.resetFaderValue(f) - if beats[k] ~= nil then - for b in all(beats[k]) do - if b.index > 0 then - local w = waves[b.index] - widgets.setFaderValue(f, b.index, b.note, w.color) - else - -- set silence value - widgets.resetFaderValue(f) - end - end - else - -- set silence value - widgets.resetFaderValue(f) - end - end + active_pattern(1, data) else bpm.value = 120 + patterns.value = 1 -- no data, reset to 0 for k, f in ipairs(faders) do widgets.resetFaderValue(f) end + current.content = sfx.to_table(generate_score()) end active_tab = current @@ -107,7 +146,7 @@ function on_new_tab(tab) end function on_play_button() - local score = generate_score() + local score = generate_score(patterns.value) sfx.sfx(score) end @@ -124,6 +163,16 @@ function on_increase_bpm(counter) counter.value = math.min(220, counter.value + 5) end +function on_previous_patterns(counter) + counter.value = math.max(counter.value - 1, 1) + active_pattern(counter.value, active_tab.content) +end + +function on_next_patterns(counter) + counter.value = math.min(counter.value + 1, 10) + active_pattern(counter.value, active_tab.content) +end + function _init(w, h) widgets.on_new_tab = on_new_tab @@ -162,9 +211,9 @@ function _init(w, h) x = 10, y = 112, value = 1, - label = "pattern" - -- on_left = on_decrease_bpm, - -- on_right = on_increase_bpm, + label = "pattern", + on_left = on_previous_patterns, + on_right = on_next_patterns }) bpm = widgets.createCounter({ @@ -193,7 +242,6 @@ function _init(w, h) table.insert(faders, f) end - -- buttons for i = #waves - 1, 0, -1 do local w = widgets.createButton({ @@ -284,31 +332,56 @@ function to_hex(number) return hexString end -function generate_score() - local score = "tiny-sfx 1 " .. bpm.value .. " 255\n" +function generate_score(played_pattern) - -- write patterns + -- new file. So there is not content yet. + if active_tab.content == nil then + local new_pattern = {} + + for index = 1, 32 do + local beat = {} + beat[1] = { + type = 0, + index = 0, + note = 1 + } + + new_pattern[index] = beat + end - local strip = "" - for f in all(faders) do - local beat = "" - if f.values ~= nil and next(f.values) then - for k, v in pairs(f.values) do - if #beat > 0 then - beat = beat .. ":" + active_tab.content = {} + active_tab.content.patterns = {} + active_tab.content.patterns[1] = new_pattern + end + local p = active_tab.content["patterns"] + local score = "tiny-sfx " .. #p .. " " .. bpm.value .. " 255\n" + -- write patterns + for patterns in all(active_tab.content["patterns"]) do + local strip = "" + for index = 1, 32 do + local beatStr = "" + local beat = patterns[index] + if beat == nil then + beatStr = beatStr .. "0000FF" + else + for note in all(beat) do + beatStr = beatStr .. to_hex(note.index) .. to_hex(note.note) .. to_hex(255) .. ":" end - beat = beat .. to_hex(k) .. to_hex(v.value) .. to_hex(255) + beatStr = beatStr:sub(1, -2) end - else - beat = "0000FF" + strip = strip .. beatStr .. " " end - - strip = strip .. beat .. " " + -- + score = score .. strip .. "\n" end - score = score .. strip .. "\n" -- write patterns order - score = score .. "1" + if played_pattern == nil then + -- TODO: in music mode, generate patterns + played_pattern = 1 + end + score = score .. played_pattern + return score end diff --git a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua index 62ee13dc..3c44568b 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/widgets.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/widgets.lua @@ -12,6 +12,7 @@ local Fader = { label = "", type = "fader", data = nil, + index = 0, on_value_update = function(fader, value) end } @@ -111,6 +112,9 @@ factory.createFader = function(value) local result = new(Fader, value) table.insert(widgets, result) table.insert(faders, result) + + result.index = #faders + return result end From 29519aa119406a035077e14718432103a0d974eb Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Wed, 7 Feb 2024 23:50:43 +0100 Subject: [PATCH 43/48] Allow creation of music --- tiny-cli/src/jvmMain/resources/sfx/game.lua | 112 +++++++++++++++--- .../src/jvmMain/resources/sfx/widgets.lua | 28 +++-- .../com/github/minigdx/tiny/lua/SfxLib.kt | 2 +- 3 files changed, 117 insertions(+), 25 deletions(-) diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 97ce1ab1..d52afa99 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -140,13 +140,24 @@ local window = { height = 0 } +local fader_mode = true +local switch_mode = nil + +local fader_widgets = {} +local music_widgets = {} + function on_new_tab(tab) local filename = ws.create("sfx", "sfx") tab.label = filename end function on_play_button() - local score = generate_score(patterns.value) + local score = nil + if fader_mode then + score = generate_score(patterns.value) + else + score = generate_score() + end sfx.sfx(score) end @@ -173,6 +184,38 @@ function on_next_patterns(counter) active_pattern(counter.value, active_tab.content) end +function on_decrease_pattern(counter) + counter.value = math.max(counter.value - 1, 1) +end + +function on_increase_pattern(counter) + counter.value = math.min(counter.value + 1, #active_tab.content["patterns"]) +end + + + +function on_switch_mode() + fader_mode = not fader_mode + if fader_mode then + switch_mode.overlay = 24 + for w in all(fader_widgets) do + w.enabled = true + end + for w in all(music_widgets) do + w.enabled = false + end + else + switch_mode.overlay = 25 + for w in all(fader_widgets) do + w.enabled = false + end + for w in all(music_widgets) do + w.enabled = true + end + end + +end + function _init(w, h) widgets.on_new_tab = on_new_tab @@ -188,6 +231,7 @@ function _init(w, h) on_active_button = on_play_button }) + widgets.createButton({ x = 10, y = 16 + 2 + 16, @@ -199,12 +243,12 @@ function _init(w, h) on_active_button = on_save_button }) - widgets.createButton({ + switch_mode = widgets.createButton({ x = 10, y = 16 + 2 + 16 + 2 + 16, overlay = 24, grouped = false, - on_active_button = on_play_button + on_active_button = on_switch_mode }) patterns = widgets.createCounter({ @@ -216,6 +260,8 @@ function _init(w, h) on_right = on_next_patterns }) + table.insert(fader_widgets, patterns) + bpm = widgets.createCounter({ x = 10, y = 112 + 24, @@ -225,6 +271,8 @@ function _init(w, h) on_right = on_increase_bpm }) + table.insert(fader_widgets, bpm) + -- faders for i = 1, 32 do local f = widgets.createFader({ @@ -240,8 +288,9 @@ function _init(w, h) on_value_update = on_fader_update }) table.insert(faders, f) + table.insert(fader_widgets, f) end - + -- buttons for i = #waves - 1, 0, -1 do local w = widgets.createButton({ @@ -253,12 +302,34 @@ function _init(w, h) }, on_active_button = on_active_button }) + + table.insert(fader_widgets, w) if i == 0 then w.status = 2 end end + -- music buttons + for x=1,8 do + for y=1,8 do + local w = widgets.createCounter({ + x = 28 + x * 48, + y = y * 32, + value = nil, + enabled = false, + index = x + (y -1) * 8, + label = "pattern", + on_left = on_decrease_pattern, + on_right = on_increase_pattern + }) + + if x == 1 and y == 1 then + w.value = 1 + end + music_widgets[x + (y-1) * 8] = w + end + end -- tabs local files = ws.list() @@ -377,8 +448,16 @@ function generate_score(played_pattern) -- write patterns order if played_pattern == nil then - -- TODO: in music mode, generate patterns - played_pattern = 1 + played_pattern = "" + local stop = false + for w in all(music_widgets) do + if w.value == 0 then + stop = true + end + if(not stop) then + played_pattern = played_pattern..w.value.." " + end + end end score = score .. played_pattern @@ -401,17 +480,20 @@ function _draw() gfx.cls(2) -- background for tabs shape.rectf(0, 0, window.width, 8, 1) - -- octave limits - local per_octave = math.floor((256 - 18) / 9) -- height / nb octaves - for octave = 9, 0, -1 do - local y = 34 + (256 - 18) - octave * per_octave - gfx.dither(0x1010) - shape.line(36, y - 2, 36 + 32 * 12, y - 2, 3) + + if fader_mode then + -- octave limits + local per_octave = math.floor((256 - 18) / 9) -- height / nb octaves + for octave = 9, 0, -1 do + local y = 34 + (256 - 18) - octave * per_octave + gfx.dither(0x1010) + shape.line(36, y - 2, 36 + 32 * 12, y - 2, 3) + gfx.dither() + print(" Date: Wed, 7 Feb 2024 23:57:56 +0100 Subject: [PATCH 44/48] bug fix when switching tab --- tiny-cli/src/jvmMain/resources/sfx/game.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index d52afa99..5c3e879f 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -48,6 +48,12 @@ local current_wave = waves[1] local active_tab = nil +local fader_mode = true +local switch_mode = nil + +local fader_widgets = {} +local music_widgets = {} + function on_fader_update(fader, value) widgets.setFaderValue(fader, current_wave.index, math.ceil(value), current_wave.color) @@ -111,6 +117,10 @@ function active_pattern(index, data) end function on_active_tab(current, prec) + + fader_mode = false + on_switch_mode() + if prec ~= nil then local score = generate_score() prec.content = sfx.to_table(score) @@ -140,11 +150,7 @@ local window = { height = 0 } -local fader_mode = true -local switch_mode = nil -local fader_widgets = {} -local music_widgets = {} function on_new_tab(tab) local filename = ws.create("sfx", "sfx") From 43b1ae0e29e81426e0eabb78ce29f8b55e26488f Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Thu, 8 Feb 2024 00:02:26 +0100 Subject: [PATCH 45/48] Fix tests --- .../com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt | 2 +- .../kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt index 65a368a7..a9fa333e 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/platform/test/HeadlessPlatform.kt @@ -96,7 +96,7 @@ class HeadlessPlatform(override val gameOptions: GameOptions, val resources: Map } } - override fun playBuffer(buffer: FloatArray) = Unit + override fun playBuffer(buffer: FloatArray, numberOfSamples: Int) = Unit } } diff --git a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt index 0977bb27..6859dbc4 100644 --- a/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt +++ b/tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/sound/SoundConverterTest.kt @@ -29,7 +29,7 @@ class SoundConverterTest { val (lastBeat, result) = SoundConverter().prepareStrip(song) - assertEquals(1, lastBeat) + assertEquals(2, lastBeat) assertEquals(2, result.size) assertEquals(65, result[sine.name]!!.size) assertEquals(65, result[pulse.name]!!.size) From ed70cc8036388bd9eb12f1390c25ce07309dc170 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Thu, 8 Feb 2024 21:12:24 +0100 Subject: [PATCH 46/48] Add example on the sound generation --- .../com/github/minigdx/tiny/lua/SfxLib.kt | 14 +++---- .../github/minigdx/tiny/lua/SfxLibExamples.kt | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLibExamples.kt diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index db4ea52b..cfd0296b 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -43,7 +43,7 @@ class SfxLib( ctrl.set("triangle", triangle()) ctrl.set("noise", noise()) ctrl.set("pulse", pulse()) - ctrl.set("saw", sawtooth()) + ctrl.set("sawtooth", sawtooth()) ctrl.set("to_table", toTable()) ctrl.set("sfx", sfx()) arg2.set("sfx", ctrl) @@ -77,32 +77,32 @@ class SfxLib( abstract fun wave(note: Note, duration: Seconds, volume: Percent): WaveGenerator } - @TinyFunction("Generate and play a sine wave sound.") + @TinyFunction("Generate and play a sine wave sound.", example = SFX_WAVE_EXAMPLE) inner class sine : WaveFunction() { override fun wave(note: Note, duration: Seconds, volume: Percent) = SineWave(note, duration, volume) } - @TinyFunction("Generate and play a sawtooth wave sound.") + @TinyFunction("Generate and play a sawtooth wave sound.", example = SFX_WAVE_EXAMPLE) inner class sawtooth : WaveFunction() { override fun wave(note: Note, duration: Seconds, volume: Percent) = SawToothWave(note, duration, volume) } - @TinyFunction("Generate and play a square wave sound.") + @TinyFunction("Generate and play a square wave sound.", example = SFX_WAVE_EXAMPLE) inner class square : WaveFunction() { override fun wave(note: Note, duration: Seconds, volume: Percent) = SquareWave(note, duration, volume) } - @TinyFunction("Generate and play a triangle wave sound.") + @TinyFunction("Generate and play a triangle wave sound.", example = SFX_WAVE_EXAMPLE) inner class triangle : WaveFunction() { override fun wave(note: Note, duration: Seconds, volume: Percent) = TriangleWave(note, duration, volume) } - @TinyFunction("Generate and play a noise wave sound.") + @TinyFunction("Generate and play a noise wave sound.", example = SFX_WAVE_EXAMPLE) inner class noise : WaveFunction() { override fun wave(note: Note, duration: Seconds, volume: Percent) = NoiseWave(note, duration, volume) } - @TinyFunction("Generate and play a pulse wave sound.") + @TinyFunction("Generate and play a pulse wave sound.", example = SFX_WAVE_EXAMPLE) inner class pulse : WaveFunction() { override fun wave(note: Note, duration: Seconds, volume: Percent) = PulseWave(note, duration, volume) } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLibExamples.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLibExamples.kt new file mode 100644 index 00000000..f586a1f1 --- /dev/null +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLibExamples.kt @@ -0,0 +1,41 @@ +package com.github.minigdx.tiny.lua + +//language=Lua +const val SFX_WAVE_EXAMPLE = """ +local waves = { + { name = "sine", fun = sfx.sine, note = notes.C6 }, + { name = "pulse", fun = sfx.pulse, note = notes.C6 }, + { name = "triangle", fun = sfx.triangle, note = notes.C6 }, + { name = "noise", fun = sfx.noise, note = notes.C8 }, + { name = "sawtooth", fun = sfx.sawtooth, note = notes.C6 }, + { name = "square", fun = sfx.square, note = notes.C6 }, +} + +local index = 1 + +function new_index(index, tableSize) + return ((index - 1) % tableSize) + 1 +end + +function _update() + if ctrl.pressed(keys.left) then + index =new_index(index - 1, #waves) + elseif ctrl.pressed(keys.right) then + index = new_index(index + 1, #waves) + end + + if ctrl.pressed(keys.space) then + waves[index].fun( waves[index].note) + end +end + +function _draw() + gfx.cls() + local str = "<<-- "..waves[index].name .. " -->>" + print(str, 128 - math.floor(#str * 4 * 0.5), 128 ) + + + local space = "(hit space to play a note)" + print(space, 128 - math.floor(#space * 4 * 0.5), 142) +end +""" From 385f4a1ebf3680136e8bd33d7180e8708a681e95 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Thu, 8 Feb 2024 21:49:49 +0100 Subject: [PATCH 47/48] Update the documentation of SFX --- .../com/github/minigdx/tiny/lua/SfxLib.kt | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt index cfd0296b..681098e8 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SfxLib.kt @@ -27,7 +27,23 @@ import org.luaj.vm2.lib.TwoArgFunction import kotlin.math.max import kotlin.math.min -@TinyLib("sfx", "Sound API to play/loop/stop a sound.") +@TinyLib( + "sfx", + """Sound API to play/loop/stop a sound. +A sound can be an SFX sound, generated using the tiny-cli sfx command or a MIDI file. +Please note that a SFX sound will produce the same sound whatever platform and whatever computer +as the sound is generated. + +A MIDI sound will depend of the MIDI synthesizer available on the machine. + +WARNING: Because of browser behaviour, a sound can *only* be played only after the first +user interaction. + +Avoid to start a music or a sound at the beginning of the game. +Before it, force the player to hit a key or click by adding an interactive menu +or by starting the sound as soon as the player is moving. +""", +) class SfxLib( private val resourceAccess: GameResourceAccess, // When validating the script, don't play sound @@ -79,31 +95,37 @@ class SfxLib( @TinyFunction("Generate and play a sine wave sound.", example = SFX_WAVE_EXAMPLE) inner class sine : WaveFunction() { + @TinyCall("Generate and play a sound using one note.") override fun wave(note: Note, duration: Seconds, volume: Percent) = SineWave(note, duration, volume) } @TinyFunction("Generate and play a sawtooth wave sound.", example = SFX_WAVE_EXAMPLE) inner class sawtooth : WaveFunction() { + @TinyCall("Generate and play a sound using one note.") override fun wave(note: Note, duration: Seconds, volume: Percent) = SawToothWave(note, duration, volume) } @TinyFunction("Generate and play a square wave sound.", example = SFX_WAVE_EXAMPLE) inner class square : WaveFunction() { + @TinyCall("Generate and play a sound using one note.") override fun wave(note: Note, duration: Seconds, volume: Percent) = SquareWave(note, duration, volume) } @TinyFunction("Generate and play a triangle wave sound.", example = SFX_WAVE_EXAMPLE) inner class triangle : WaveFunction() { + @TinyCall("Generate and play a sound using one note.") override fun wave(note: Note, duration: Seconds, volume: Percent) = TriangleWave(note, duration, volume) } @TinyFunction("Generate and play a noise wave sound.", example = SFX_WAVE_EXAMPLE) inner class noise : WaveFunction() { + @TinyCall("Generate and play a sound using one note.") override fun wave(note: Note, duration: Seconds, volume: Percent) = NoiseWave(note, duration, volume) } @TinyFunction("Generate and play a pulse wave sound.", example = SFX_WAVE_EXAMPLE) inner class pulse : WaveFunction() { + @TinyCall("Generate and play a sound using one note.") override fun wave(note: Note, duration: Seconds, volume: Percent) = PulseWave(note, duration, volume) } From dcf27cbd5db4af3722fab6fe7aaedbb2dbc95daa Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Thu, 8 Feb 2024 22:00:09 +0100 Subject: [PATCH 48/48] Add tiny-cli sfx command documentation --- tiny-doc/src/docs/asciidoc/tiny-cli.adoc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tiny-doc/src/docs/asciidoc/tiny-cli.adoc b/tiny-doc/src/docs/asciidoc/tiny-cli.adoc index eb628bfa..b0188c75 100644 --- a/tiny-doc/src/docs/asciidoc/tiny-cli.adoc +++ b/tiny-doc/src/docs/asciidoc/tiny-cli.adoc @@ -228,6 +228,24 @@ Once the server is running, you can access the game in your web browser by navig NOTE: The tiny serve command is intended for local testing only and should not be used to serve your game online. When you are ready to publish your game, you should upload the exported game files to a web server and serve them from there. +=== the `tiny-cli sfx` command + +The tiny-cli sfx command is used to start a SFX edtor to generate sound to use in your `🧸 Tiny` game. + +==== Usage + +To use the `tiny-cli sfx` command, follow the syntax: + +[source,shell] +tiny-cli sfx + +The command can be used to edit sounds used by your `🧸 Tiny` game. + +[source,shell] +tiny-cli sfx + +The editor will be run with all SFX sounds from your game loaded. + === the `tiny-cli lib` command The tiny-cli lib command is used to download and add Lua libraries to your `🧸 Tiny` game. This command provides an easy way to enhance the functionality of your game by incorporating libraries.

!2S?ZJJLR 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%0AnkH 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 literal 0 HcmV?d00001 diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt index b18cdf76..1fe70b26 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/sound/SoundManager.kt @@ -30,8 +30,21 @@ interface SoundManager { return result } + fun getFadeOutIndex(longestDuration: Seconds): Int { + return ((longestDuration - FADE_OUT_DURATION) * SAMPLE_RATE).toInt() + } + + fun fadeOut(sample: Float, index: Int, fadeOutIndex: Int, endIndex: Int): Float { + return if (index < fadeOutIndex) { + sample + } else { + sample * (endIndex - index) / (endIndex - fadeOutIndex).toFloat() + } + } + companion object { const val SAMPLE_RATE = 44100 + private const val FADE_OUT_DURATION: Seconds = 0.5f } } diff --git a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt index 4c3f213c..0dba3258 100644 --- a/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt +++ b/tiny-engine/src/jsMain/kotlin/com/github/minigdx/tiny/platform/webgl/PicoAudioSoundMananger.kt @@ -43,6 +43,7 @@ class PicoAudioSoundMananger : SoundManager { private fun toAudioBuffer(notes: List, longestDuration: Seconds): AudioBuffer { val numSamples = (longestDuration * SAMPLE_RATE).toInt() + val fadeOutIndex = getFadeOutIndex(longestDuration) val audioBuffer = audioContext.createBuffer( 1, @@ -51,9 +52,9 @@ class PicoAudioSoundMananger : SoundManager { ) val channel = audioBuffer.getChannelData(0) - val result = Float32Array((SAMPLE_RATE * longestDuration).toInt()) + val result = Float32Array(numSamples) (0 until numSamples).forEach { index -> - val signal = mix(index, notes) + val signal = fadeOut(mix(index, notes), index, fadeOutIndex, numSamples) result[index] = signal } channel.set(result) diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt index 9f290bb1..dd6ac880 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/JavaMidiSoundManager.kt @@ -7,12 +7,13 @@ import com.github.minigdx.tiny.sound.SoundManager import com.github.minigdx.tiny.sound.SoundManager.Companion.SAMPLE_RATE import com.github.minigdx.tiny.sound.WaveGenerator import java.io.ByteArrayInputStream +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.BlockingQueue import javax.sound.midi.MidiSystem import javax.sound.midi.Sequencer import javax.sound.midi.Sequencer.LOOP_CONTINUOUSLY import javax.sound.sampled.AudioFormat import javax.sound.sampled.AudioSystem -import javax.sound.sampled.SourceDataLine import kotlin.experimental.and class JavaMidiSound(private val data: ByteArray) : MidiSound { @@ -55,25 +56,38 @@ class JavaMidiSound(private val data: ByteArray) : MidiSound { class JavaMidiSoundManager : SoundManager { - private lateinit var notesLine: SourceDataLine - - private var buffer = ByteArray(0) + // When closing the application, switch isActive to false to stop the background thread. + private var isActive = true + + private val bufferQueue: BlockingQueue = ArrayBlockingQueue(10) + + private val backgroundAudio = object : Thread() { + override fun run() { + val notesLine = AudioSystem.getSourceDataLine( + AudioFormat( + AudioFormat.Encoding.PCM_SIGNED, + SoundManager.SAMPLE_RATE.toFloat(), + 16, + 1, // TODO: set 2 to get Stereo + 2, + SoundManager.SAMPLE_RATE.toFloat(), + false, + ), + ) + + notesLine.open() + notesLine.start() + + while (isActive) { + val nextBuffer = bufferQueue.take() + notesLine.write(nextBuffer, 0, nextBuffer.size) + } + notesLine.close() + } + } override fun initSoundManager(inputHandler: InputHandler) { - notesLine = AudioSystem.getSourceDataLine( - AudioFormat( - AudioFormat.Encoding.PCM_SIGNED, - SoundManager.SAMPLE_RATE.toFloat(), - 16, - 1, // TODO: set 2 to get Stereo - 2, - SoundManager.SAMPLE_RATE.toFloat(), - false, - ), - ) - - notesLine.open() - notesLine.start() + backgroundAudio.start() } override suspend fun createSound(data: ByteArray): MidiSound { @@ -83,10 +97,12 @@ class JavaMidiSoundManager : SoundManager { override fun playNotes(notes: List, longestDuration: Seconds) { if (notes.isEmpty()) return - buffer = ByteArray((longestDuration * SAMPLE_RATE).toInt() * 2) val numSamples: Int = (SAMPLE_RATE * longestDuration).toInt() + val buffer = ByteArray(numSamples * 2) + val fadeOutIndex = getFadeOutIndex(longestDuration) + for (i in 0 until numSamples) { - val sample = mix(i, notes) + val sample = fadeOut(mix(i, notes), i, fadeOutIndex, numSamples) val sampleValue: Float = (sample * Short.MAX_VALUE) val clippedValue = sampleValue.coerceIn(Short.MIN_VALUE.toFloat(), Short.MAX_VALUE.toFloat()) @@ -96,7 +112,6 @@ class JavaMidiSoundManager : SoundManager { buffer[2 * i + 1] = (result.toInt().shr(8) and 0xFF).toByte() } - // generate the byte array - notesLine.write(buffer, 0, buffer.size) + bufferQueue.offer(buffer) } } From b12356e5cca4b1c4c91947ed7f7c970065e33496 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Tue, 23 Jan 2024 08:48:27 +0100 Subject: [PATCH 08/48] Add all notes from C0 to B8 --- .../com/github/minigdx/tiny/lua/NotesLib.kt | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt index a91989c6..e100d0b3 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/NotesLib.kt @@ -1,5 +1,6 @@ package com.github.minigdx.tiny.lua +import com.github.mingdx.tiny.doc.TinyLib import org.luaj.vm2.LuaTable import org.luaj.vm2.LuaValue import org.luaj.vm2.lib.TwoArgFunction @@ -58,8 +59,121 @@ enum class Note(val frequency: Float) { As2(116.54f), Bb2(116.54f), B2(123.47f), + + C3(130.81f), + Cs3(138.59f), + Db3(138.59f), + D3(146.83f), + Ds3(155.56f), + Eb3(155.56f), + E3(164.81f), + F3(174.61f), + Fs3(185.00f), + Gb3(185.00f), + G3(196.00f), + Gs3(207.65f), + Ab3(207.65f), + A3(220.00f), + As3(233.08f), + Bb3(233.08f), + B3(246.94f), + + C4(261.63f), + Cs4(277.18f), + Db4(277.18f), + D4(293.66f), + Ds4(311.13f), + Eb4(311.13f), + E4(329.63f), + F4(349.23f), + Fs4(369.99f), + Gb4(369.99f), + G4(392.00f), + Gs4(415.30f), + Ab4(415.30f), + A4(440.00f), + As4(466.16f), + Bb4(466.16f), + B4(493.88f), + + C5(523.25f), + Cs5(554.37f), + Db5(554.37f), + D5(587.33f), + Ds5(622.25f), + Eb5(622.25f), + E5(659.26f), + F5(698.46f), + Fs5(739.99f), + Gb5(739.99f), + G5(783.99f), + Gs5(830.61f), + Ab5(830.61f), + A5(880.00f), + As5(932.33f), + Bb5(932.33f), + B5(987.77f), + + C6(1046.50f), + Cs6(1108.73f), + Db6(1108.73f), + D6(1174.66f), + Ds6(1244.51f), + Eb6(1244.51f), + E6(1318.51f), + F6(1396.91f), + Fs6(1479.98f), + Gb6(1479.98f), + G6(1567.98f), + Gs6(1661.22f), + Ab6(1661.22f), + A6(1760.00f), + As6(1864.66f), + Bb6(1864.66f), + B6(1975.53f), + + C7(2093.00f), + Cs7(2217.46f), + Db7(2217.46f), + D7(2349.32f), + Ds7(2489.02f), + Eb7(2489.02f), + E7(2637.02f), + F7(2793.83f), + Fs7(2959.96f), + Gb7(2959.96f), + G7(3135.96f), + Gs7(3322.44f), + Ab7(3322.44f), + A7(3520.00f), + As7(3729.31f), + Bb7(3729.31f), + B7(3951.07f), + + C8(4186.01f), + Cs8(4434.92f), + Db8(4434.92f), + D8(4698.63f), + Ds8(4978.03f), + Eb8(4978.03f), + E8(5274.04f), + F8(5587.65f), + Fs8(5919.91f), + Gb8(5919.91f), + G8(6271.93f), + Gs8(6644.88f), + Ab8(6644.88f), + A8(7040.00f), + As8(7458.62f), + Bb8(7458.62f), + B8(7902.13f), } +@TinyLib( + "notes", + "List all notes from C0 to B8. " + + "Please note that bemols are the note with b (ie: Gb2) while sharps are the note with s (ie: As3).", +) class NotesLib : TwoArgFunction() { override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { From 44a9ed3dfba850f761d70585f87d2d2d45b94078 Mon Sep 17 00:00:00 2001 From: David Wursteisen Date: Wed, 24 Jan 2024 17:16:25 +0100 Subject: [PATCH 09/48] Generate a sound by using a list of notes. --- .../minigdx/tiny/cli/command/MainCommand.kt | 1 + .../minigdx/tiny/cli/command/RunCommand.kt | 4 +- .../minigdx/tiny/cli/command/SfxCommand.kt | 154 +++++++++ tiny-cli/src/jvmMain/resources/sfx/_tiny.json | 37 +- tiny-cli/src/jvmMain/resources/sfx/game.lua | 118 ++++--- tiny-cli/src/jvmMain/resources/sfx/mouse.lua | 4 +- .../src/jvmMain/resources/sfx/tiny-export.zip | Bin 214135 -> 0 bytes .../src/jvmMain/resources/sfx/widgets.lua | 78 +++++ .../github/minigdx/tiny/engine/GameEngine.kt | 30 +- .../minigdx/tiny/engine/GameResourceAccess.kt | 6 + .../com/github/minigdx/tiny/lua/NotesLib.kt | 320 +++++++++--------- .../com/github/minigdx/tiny/lua/SfxLib.kt | 4 +- .../github/minigdx/tiny/platform/Platform.kt | 4 +- .../minigdx/tiny/resources/GameScript.kt | 13 +- .../minigdx/tiny/resources/ResourceFactory.kt | 16 +- .../github/minigdx/tiny/sound/SoundManager.kt | 2 + .../minigdx/tiny/sound/WaveGenerator.kt | 6 + .../github/minigdx/tiny/lua/MusicLibTest.kt | 20 ++ .../tiny/platform/test/HeadlessPlatform.kt | 5 +- .../platform/webgl/PicoAudioSoundMananger.kt | 4 + .../tiny/platform/webgl/WebGlPlatform.kt | 4 +- .../minigdx/tiny/file/InputStreamStream.kt | 6 +- .../tiny/platform/glfw/GlfwPlatform.kt | 15 +- .../platform/glfw/JavaMidiSoundManager.kt | 25 +- tiny-web-editor/src/jsMain/kotlin/Main.kt | 6 +- 25 files changed, 602 insertions(+), 280 deletions(-) create mode 100644 tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt delete mode 100644 tiny-cli/src/jvmMain/resources/sfx/tiny-export.zip create mode 100644 tiny-cli/src/jvmMain/resources/sfx/widgets.lua create mode 100644 tiny-engine/src/commonTest/kotlin/com/github/minigdx/tiny/lua/MusicLibTest.kt diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/MainCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/MainCommand.kt index 31ce6b96..5d9e9f94 100644 --- a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/MainCommand.kt +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/MainCommand.kt @@ -14,6 +14,7 @@ class MainCommand : CliktCommand(invokeWithoutSubcommand = true) { ServeCommand(), LibCommand(), PaletteCommand(), + SfxCommand(), ) } diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt index 261cb31c..ce80f11b 100644 --- a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/RunCommand.kt @@ -18,8 +18,8 @@ import java.io.File class RunCommand : CliktCommand(name = "run", help = "Run your game.") { val gameDirectory by argument(help = "The directory containing all game information") - .file(mustExist = true, canBeDir = true, canBeFile = false) - .default(File(".")) + .file(mustExist = true, canBeDir = true, canBeFile = false) + .default(File(".")) fun isOracleOrOpenJDK(): Boolean { val vendor = System.getProperty("java.vendor")?.lowercase() diff --git a/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt new file mode 100644 index 00000000..280d3861 --- /dev/null +++ b/tiny-cli/src/jvmMain/kotlin/com/github/minigdx/tiny/cli/command/SfxCommand.kt @@ -0,0 +1,154 @@ +package com.github.minigdx.tiny.cli.command + +import com.github.ajalt.clikt.core.Abort +import com.github.ajalt.clikt.core.CliktCommand +import com.github.minigdx.tiny.cli.config.GameParameters +import com.github.minigdx.tiny.engine.GameEngine +import com.github.minigdx.tiny.engine.GameResourceAccess +import com.github.minigdx.tiny.file.CommonVirtualFileSystem +import com.github.minigdx.tiny.log.StdOutLogger +import com.github.minigdx.tiny.lua.Note +import com.github.minigdx.tiny.lua.errorLine +import com.github.minigdx.tiny.platform.glfw.GlfwPlatform +import com.github.minigdx.tiny.render.LwjglGLRender +import com.github.minigdx.tiny.sound.NoiseWave +import com.github.minigdx.tiny.sound.PulseWave +import com.github.minigdx.tiny.sound.SilenceWave +import com.github.minigdx.tiny.sound.SineWave +import com.github.minigdx.tiny.sound.SoundManager +import com.github.minigdx.tiny.sound.TriangleWave +import kotlinx.serialization.json.decodeFromStream +import org.luaj.vm2.Globals +import org.luaj.vm2.LuaError +import org.luaj.vm2.LuaTable +import org.luaj.vm2.LuaValue +import org.luaj.vm2.lib.TwoArgFunction +import java.io.File + +class MusicLib(private val soundManager: SoundManager) : TwoArgFunction() { + override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + val ctrl = LuaTable() + ctrl.set("play", play()) + arg2.set("music", ctrl) + arg2.get("package").get("loaded").set("music", ctrl) + return ctrl + } + + private fun extractNote(str: String): Note { + val note = str.substringAfter("(").substringBefore(")") + return Note.valueOf(note) + } + + private val acceptedTypes = setOf("sine", "noise", "pulse", "triangle") + + private fun extractWaveType(str: String): String? { + if (str == "*") return str + + val type = str.substringBefore("(") + return if (acceptedTypes.contains(type)) { + type + } else { + null + } + } + + inner class play : TwoArgFunction() { + + fun trim(str: String): String { + val lastIndex = str.lastIndexOf(')') + if (lastIndex < 0) return str + return str.substring(0, lastIndex + 2) + } + + override fun call(arg1: LuaValue, arg2: LuaValue): LuaValue { + val bpm = arg2.optint(120) + val duration = 60 / bpm.toFloat() / 4 + val score = arg1.optjstring("")!! + val parts = trim(score).split("-") + val waves = parts.mapNotNull { + val wave = extractWaveType(it) + when (wave) { + "*" -> SilenceWave(duration) + "sine" -> SineWave(extractNote(it), duration) + "triangle" -> TriangleWave(extractNote(it), duration) + "noise" -> NoiseWave(extractNote(it), duration) + "pulse" -> PulseWave(extractNote(it), duration) + else -> null + } + } + soundManager.playSfx(waves) + return NIL + } + } +} + +private val customizeGlobal: GameResourceAccess.(Globals) -> Unit = { global -> + val engine = (this as? GameEngine) + if (engine != null) { + global.load(MusicLib(engine.soundManager)) + } +} + +class SfxCommand : CliktCommand(name = "sfx", help = "Start the SFX Editor") { + fun isOracleOrOpenJDK(): Boolean { + val vendor = System.getProperty("java.vendor")?.lowercase() + return vendor?.contains("oracle") == true || vendor?.contains("eclipse") == true || vendor?.contains("openjdk") == true + } + + fun isMacOS(): Boolean { + val os = System.getProperty("os.name").lowercase() + return os.contains("mac") || os.contains("darwin") + } + + override fun run() { + if (isMacOS() && isOracleOrOpenJDK()) { + echo("\uD83D\uDEA7 === The Tiny CLI on Mac with require a special option.") + echo("\uD83D\uDEA7 === If the application crash âž¡ use the command 'tiny-cli-mac' instead.") + } + + try { + val configFile = SfxCommand::class.java.getResourceAsStream("/sfx/_tiny.json") + if (configFile == null) { + echo( + "\uD83D\uDE2D No _tiny.json found! Can't run the game without. " + + "The tiny-cli command doesn't seems to be bundled correctly. You might want to report an issue.", + ) + throw Abort() + } + val gameParameters = GameParameters.JSON.decodeFromStream(configFile) + + val logger = StdOutLogger("tiny-cli") + val vfs = CommonVirtualFileSystem() + val gameOption = gameParameters.toGameOptions() + GameEngine( + gameOptions = gameOption, + platform = GlfwPlatform( + gameOption, + logger, + vfs, + File("."), + LwjglGLRender(logger, gameOption), + jarResourcePrefix = "/sfx", + ), + vfs = vfs, + logger = logger, + customizeGlobal, + ).main() + } catch (ex: Exception) { + echo( + "\uD83E\uDDE8 An unexpected exception occurred. " + + "The application will stop. " + + "It might be a bug in Tiny. " + + "If so, please report it.", + ) + when (ex) { + is LuaError -> { + val (nb, line) = ex.errorLine() ?: (null to null) + echo("Error found line $nb:$line") + } + } + echo() + ex.printStackTrace() + } + } +} diff --git a/tiny-cli/src/jvmMain/resources/sfx/_tiny.json b/tiny-cli/src/jvmMain/resources/sfx/_tiny.json index 12f05a68..765e37fc 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/_tiny.json +++ b/tiny-cli/src/jvmMain/resources/sfx/_tiny.json @@ -1,36 +1 @@ -{ - "version": "V1", - "name": "Tiny SFX Sequencer", - "resolution": { - "width": 512, - "height": 288 - }, - "sprites": { - "width": 8, - "height": 8 - }, - "hideMouseCursor": true, - "zoom": 2, - "colors": [ - "#000000", - "#1D2B53", - "#7E2553", - "#008751", - "#AB5236", - "#5F574F", - "#C2C3C7", - "#FFF1E8", - "#FF004D", - "#FFA300", - "#FFEC27", - "#00E436", - "#29ADFF", - "#83769C", - "#FF77A8", - "#FFCCAA" - ], - "scripts": [ - "game.lua", - "mouse.lua" - ] -} \ No newline at end of file +{"version":"V1","name":"Tiny SFX Sequencer","resolution":{"width":512,"height":288},"sprites":{"width":8,"height":8},"zoom":2,"colors":["#000000","#1D2B53","#7E2553","#008751","#AB5236","#5F574F","#C2C3C7","#FFF1E8","#FF004D","#FFA300","#FFEC27","#00E436","#29ADFF","#83769C","#FF77A8","#FFCCAA"],"scripts":["game.lua","mouse.lua","widgets.lua"],"hideMouseCursor":true} \ No newline at end of file diff --git a/tiny-cli/src/jvmMain/resources/sfx/game.lua b/tiny-cli/src/jvmMain/resources/sfx/game.lua index 6cd93b08..eb8b8776 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/game.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/game.lua @@ -1,30 +1,53 @@ local mouse = require("mouse") +local widgets = require("widgets") -local triggers = {} - -local Trigger = { - tick = 0, - note = 0, - width = 8, - height = 4, - active = false, - player = false, +local labels = { + "C0", "Db0", "D0", "Eb0", "E0", "F0", "Gb0", "G0", "Ab0", "A0", "Bb0", "B0", + "C1", "Db1", "D1", "Eb1", "E1", "F1", "Gb1", "G1", "Ab1", "A1", "Bb1", "B1", + "C2", "Db2", "D2", "Eb2", "E2", "F2", "Gb2", "G2", "Ab2", "A2", "Bb2", "B2", + "C3", "Db3", "D3", "Eb3", "E3", "F3", "Gb3", "G3", "Ab3", "A3", "Bb3", "B3", + "C4", "Db4", "D4", "Eb4", "E4", "F4", "Gb4", "G4", "Ab4", "A4", "Bb4", "B4", + "C5", "Db5", "D5", "Eb5", "E5", "F5", "Gb5", "G5", "Ab5", "A5", "Bb5", "B5", + "C6", "Db6", "D6", "Eb6", "E6", "F6", "Gb6", "G6", "Ab6", "A6", "Bb6", "B6", + "C7", "Db7", "D7", "Eb7", "E7", "F7", "Gb7", "G7", "Ab7", "A7", "Bb7", "B7", + "C8", "Db8", "D8", "Eb8", "E8", "F8", "Gb8", "G8", "Ab8", "A8", "Bb8", "B8" } -local metronome = { - tick = 0 -} +local waves = { + {type = "sine", color = 9}, + {type = "noise", color = 8}, + {type = "triangle", color = 7}, + {type = "pulse", color = 6} + +} + +local current_wave = waves[1] + +function on_fader_update(fader, value) + fader.value = math.ceil(value) + fader.data = { + note = labels[fader.value], + wave = current_wave.type, + } + fader.label = labels[fader.value] + fader.tip_color = current_wave.color +end + +local faders = {} function _init() - for x = 1, 10 do - for y = 1, 40 do - table.insert(triggers, new(Trigger, { - tick = x, - note = y, - x = x * 10, - y = y * 6 - })) - end + for i = 1, 32 do + local f = widgets.createFader({ + x = 10 + i * 12, + y = 10, + height = 246, + label = labels[0], + value = 0, + max_value = #labels, + data = {note = labels[0]}, + on_value_update = on_fader_update + }) + table.insert(faders, f) end end @@ -37,44 +60,41 @@ function on_click(x, y) end end -function tick_updated() - for i = 1, #triggers do - local trigger = triggers[i] - if metronome.tick == trigger.tick and trigger.active then - sfx.noise(trigger.note, 0.1666) +function _update() + mouse._update(widgets.on_click) + widgets._update() + + if ctrl.pressed(keys.space) then + local score = "" + + for f in all(faders) do + if f.data ~= nil and f.data.note ~= nil then + score = score .. f.data.wave.."(" .. f.data.note .. ")-" + else + score = score .. "*-" + end end + music.play(score, 220) end -end -function _update() - mouse._update(on_click) - - if tiny.frame % 10 == 0 then - metronome.tick = metronome.tick + 1 - if (metronome.tick > 10) then - metronome.tick = 1 + local new_wave = current_wave + if ctrl.pressed(keys.up) then + for i=1,#waves do + if waves[i].type == current_wave.type then + local next_index = (i % #waves) + 1 + new_wave = waves[next_index] + end end - tick_updated() + current_wave = new_wave end end function _draw() gfx.cls() - for i = 1, #triggers do - local trigger = triggers[i] - local color = 7 - if trigger.tick == metronome.tick then - color = 9 - end - if(trigger.active) then - -- gfx.dither(0xA5A5) - shape.rectf(trigger.x, trigger.y, trigger.width, trigger.height, 8) - else - shape.rect(trigger.x, trigger.y, trigger.width, trigger.height, color) - -- gfx.dither() - end - end + widgets._draw() mouse._draw() + + print(current_wave.type) end diff --git a/tiny-cli/src/jvmMain/resources/sfx/mouse.lua b/tiny-cli/src/jvmMain/resources/sfx/mouse.lua index e2441846..e3d5fbef 100644 --- a/tiny-cli/src/jvmMain/resources/sfx/mouse.lua +++ b/tiny-cli/src/jvmMain/resources/sfx/mouse.lua @@ -13,9 +13,9 @@ mouse._update = function(on_click) mouse.x = pos.x mouse.y = pos.y - local clicked = ctrl.touched(0) + local clicked = ctrl.touching(0) if clicked then - on_click(clicked.x, clicked.y) + on_click(pos.x, pos.y) end end diff --git a/tiny-cli/src/jvmMain/resources/sfx/tiny-export.zip b/tiny-cli/src/jvmMain/resources/sfx/tiny-export.zip deleted file mode 100644 index 12f570cbde2c0d8f31bb8659b351c743fb8618b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214135 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