Skip to content

Commit

Permalink
Play the minimal song by removing ending silence to limit the thread …
Browse files Browse the repository at this point in the history
…blocking
  • Loading branch information
dwursteisen committed Feb 6, 2024
1 parent a9d7516 commit cb60d35
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 148 deletions.
49 changes: 37 additions & 12 deletions tiny-cli/src/jvmMain/resources/sfx/game.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
13 changes: 9 additions & 4 deletions tiny-cli/src/jvmMain/resources/sfx/widgets.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,61 +218,18 @@ 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<WaveGenerator> {
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)
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)
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)
}
}
Expand All @@ -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<Index, Pattern>
val patterns = lines.drop(1).take(nbPattern.toInt()).mapIndexed { indexPattern, pattern ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ data class Pattern(val index: Int, val beats: List<Beat>)
*/
data class Song(val bpm: Int, val volume: Float, val patterns: Map<Int, Pattern>, val music: List<Pattern>) {

val durationOfBeat: Seconds = (bpm / 60f / 4f)
val durationOfBeat: Seconds = (60f / bpm / 8f)

val numberOfBeats = music.count() * 32
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,7 @@ abstract class SoundManager {
if (notes.isEmpty()) return

val result = createNotesBuffer(longestDuration, notes)
playBuffer(result)
}

fun playSfx(notes: List<WaveGenerator>) {
if (notes.isEmpty()) return

val result = createScoreBuffer(notes)

playBuffer(result)
playBuffer(result, result.size)
}

protected fun createNotesBuffer(
Expand All @@ -53,58 +45,10 @@ abstract class SoundManager {
return result
}

protected fun createScoreBuffer(notes: List<WaveGenerator>): FloatArray {
var currentIndex = 0

fun merge(head: WaveGenerator, tail: List<WaveGenerator>): List<WaveGenerator> {
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<WaveGenerator>): Float {
var result = 0f
Expand All @@ -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<FloatArray, Int> {
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()
Expand All @@ -154,7 +98,7 @@ abstract class SoundManager {
}
mix[sample] = result / buffers.size.toFloat()
}
return mix
return mix to lastBeat * numberOfSamplesPerBeat
}

companion object {
Expand All @@ -166,18 +110,24 @@ abstract class SoundManager {

class SoundConverter {

internal fun prepateStrip(song: Song): Map<String, Array<WaveGenerator>> {
internal fun prepareStrip(song: Song): Pair<Int, Map<String, Array<WaveGenerator>>> {
// Create a line per WaveGenerator kind.
val musicPerType: MutableMap<String, Array<WaveGenerator>> = 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<WaveGenerator>): FloatArray {
Expand All @@ -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()
}

Expand All @@ -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()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit cb60d35

Please sign in to comment.