Coverage Summary for Class: PlayerEventKt (com.vsevolodganin.clicktrack.player)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
PlayerEventKt |
0%
(0/8)
|
0%
(0/12)
|
0%
(0/28)
|
0%
(0/213)
|
PlayerEventKt$startingAt$1 |
0%
(0/1)
|
|
0%
(0/2)
|
0%
(0/48)
|
PlayerEventKt$toPlayerEvents$2 |
0%
(0/1)
|
0%
(0/6)
|
0%
(0/10)
|
0%
(0/84)
|
PlayerEventKt$toPlayerEvents$3 |
0%
(0/1)
|
0%
(0/6)
|
0%
(0/11)
|
0%
(0/92)
|
PlayerEventKt$withDuration$1 |
0%
(0/1)
|
0%
(0/4)
|
0%
(0/10)
|
0%
(0/122)
|
Total |
0%
(0/12)
|
0%
(0/28)
|
0%
(0/61)
|
0%
(0/559)
|
package com.vsevolodganin.clicktrack.player
import com.vsevolodganin.clicktrack.model.AbstractPolyrhythm
import com.vsevolodganin.clicktrack.model.BeatsPerMinuteOffset
import com.vsevolodganin.clicktrack.model.ClickSoundType
import com.vsevolodganin.clicktrack.model.ClickTrack
import com.vsevolodganin.clicktrack.model.Cue
import com.vsevolodganin.clicktrack.model.NoteEvent
import com.vsevolodganin.clicktrack.model.TwoLayerPolyrhythm
import com.vsevolodganin.clicktrack.model.interval
import com.vsevolodganin.clicktrack.utils.collection.toRoundRobin
import com.vsevolodganin.clicktrack.utils.math.Rational
import com.vsevolodganin.clicktrack.utils.math.ZERO
import com.vsevolodganin.clicktrack.utils.math.compareTo
import com.vsevolodganin.clicktrack.utils.math.over
import com.vsevolodganin.clicktrack.utils.math.times
import com.vsevolodganin.clicktrack.utils.math.toRational
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
class PlayerEvent(
val duration: Duration,
val soundType: ClickSoundType?,
) {
fun copy(duration: Duration = this.duration, soundType: ClickSoundType? = this.soundType) = PlayerEvent(
duration = duration,
soundType = soundType,
)
}
fun ClickTrack.toPlayerEvents(): Sequence<PlayerEvent> {
return cues.asSequence().flatMap { it.toPlayerEvents(tempoOffset) }
}
fun Cue.toPlayerEvents(tempoOffset: BeatsPerMinuteOffset): Sequence<PlayerEvent> {
val tempo = bpm + tempoOffset
val polyrhythm = AbstractPolyrhythm(
pattern1 = listOf(NoteEvent(timeSignature.noteCount.toRational(), NoteEvent.Type.NOTE)),
pattern2 = pattern.events,
)
val bpmInterval = tempo.interval
return sequence {
if (polyrhythm.untilFirst > Rational.ZERO) {
yield(delayEvent(bpmInterval * polyrhythm.untilFirst))
}
for (column in polyrhythm.columns) {
val soundType = when {
column.indices.contains(0) -> ClickSoundType.STRONG
else -> ClickSoundType.WEAK
}
yield(
soundEvent(
duration = bpmInterval * column.untilNext,
soundType = soundType,
),
)
}
}.withDuration(durationAsTimeWithBpmOffset(tempoOffset))
}
fun TwoLayerPolyrhythm.toPlayerEvents(): Sequence<PlayerEvent> {
if (!isPlayable()) {
return emptySequence()
}
val tempo = bpm
val layer1NoteLength = 1.toRational()
val layer2NoteLength = layer1 over layer2
val polyrhythm = AbstractPolyrhythm(
pattern1 = List(layer1) { NoteEvent(layer1NoteLength, NoteEvent.Type.NOTE) },
pattern2 = List(layer2) { NoteEvent(layer2NoteLength, NoteEvent.Type.NOTE) },
)
return sequence {
val bpmInterval = tempo.interval
if (polyrhythm.untilFirst > Rational.ZERO) {
yield(delayEvent(bpmInterval * polyrhythm.untilFirst))
}
for (column in polyrhythm.columns) {
val soundType = when {
column.indices.contains(0) -> ClickSoundType.STRONG
else -> ClickSoundType.WEAK
}
yield(
soundEvent(
duration = bpmInterval * column.untilNext,
soundType = soundType,
),
)
}
}
}
fun Sequence<PlayerEvent>.startingAt(startAt: Duration): Sequence<PlayerEvent> {
var runningDuration = Duration.ZERO
var dropCount = 0
for (event in this) {
if (runningDuration >= startAt) {
break
}
dropCount += 1
runningDuration += event.duration
}
return sequence {
yield(delayEvent(duration = (runningDuration - startAt).coerceAtLeast(Duration.ZERO)))
yieldAll(drop(dropCount))
}
}
fun Sequence<PlayerEvent>.loop(loop: Boolean): Sequence<PlayerEvent> {
return if (loop) toRoundRobin() else this
}
private fun Sequence<PlayerEvent>.withDuration(duration: Duration): Sequence<PlayerEvent> {
return sequence {
val soundEventsCycledIterator = toRoundRobin().iterator()
var runningDuration = Duration.ZERO
while (true) {
val next = soundEventsCycledIterator.next()
runningDuration += next.duration
if (runningDuration + Const.CLICK_MIN_DELTA >= duration) {
if (runningDuration != duration) {
yield(next.copy(duration = next.duration - (runningDuration - duration)))
} else {
yield(next)
}
return@sequence
} else {
yield(next)
}
}
}
}
private fun soundEvent(duration: Duration, soundType: ClickSoundType) = PlayerEvent(duration, soundType)
private fun delayEvent(duration: Duration) = PlayerEvent(duration, null)
private object Const {
val CLICK_MIN_DELTA = 1.milliseconds
}