commonMain.dev.atsushieno.ktmidi.Midi1Player.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ktmidi-jvm Show documentation
Show all versions of ktmidi-jvm Show documentation
Kotlin Multiplatform library for MIDI 1.0 and MIDI 2.0
The newest version!
package dev.atsushieno.ktmidi
import kotlinx.coroutines.Runnable
import kotlin.math.max
internal class Midi1EventLooper(var messages: List, private val timer: MidiPlayerTimer,
private val deltaTimeSpec: Int) : MidiEventLooper(timer) {
override fun getContextDeltaTimeInSeconds(m: Midi1Event): Double {
return if (deltaTimeSpec < 0)
Midi1Music.getSmpteDurationInSeconds(deltaTimeSpec, m.deltaTime, currentTempo, tempoRatio)
else
currentTempo.toDouble() / 1_000_000 * m.deltaTime / deltaTimeSpec / tempoRatio
}
override fun getDurationOfEvent(m: Midi1Event) = m.deltaTime
override fun isEventIndexAtEnd(): Boolean = eventIdx == messages.size
override fun getNextEvent(): Midi1Event = messages[eventIdx]
override fun updateTempoAndTimeSignatureIfApplicable(m: Midi1Event) {
if (m.message.statusByte.toUnsigned() == 0xFF) {
val e = m.message as Midi1CompoundMessage
if (e.msb.toInt() == MidiMetaType.TEMPO)
currentTempo = Midi1Music.getSmfTempo(e.extraData!!, e.extraDataOffset)
else if (e.msb.toInt() == MidiMetaType.TIME_SIGNATURE && e.extraDataLength == 4) {
currentTimeSignature.clear()
currentTimeSignature.addAll(e.extraData!!.drop(e.extraDataOffset).take(e.extraDataLength))
}
}
}
val messageHandlers = mutableListOf()
override fun onEvent(m: Midi1Event) {
for (er in messageHandlers)
er.onEvent(m)
}
override fun mute() {
for (i in 0..15)
onEvent(Midi1Event(0, Midi1SimpleMessage(i + MidiChannelStatus.CC, MidiCC.ALL_SOUND_OFF, 0)))
}
}
fun interface OnMidi1EventListener {
fun onEvent(m: Midi1Event)
}
@Deprecated("Use OnMidi1EventListener")
fun interface OnMidiMessageListener {
fun onMessage(m: MidiMessage)
}
// Provides asynchronous player control.
class Midi1Player : MidiPlayer {
companion object {
suspend fun create(music: Midi1Music, access: MidiAccess, timer: MidiPlayerTimer = SimpleAdjustingMidiPlayerTimer()) =
Midi1Player(music, access.openOutput(access.outputs.first().id), timer, true)
}
constructor(music: Midi1Music, output: MidiOutput, timer: MidiPlayerTimer = SimpleAdjustingMidiPlayerTimer(), shouldDisposeOutput: Boolean = false)
: super(output, shouldDisposeOutput) {
this.music = music
events = music.mergeTracks().tracks[0].events
looper = Midi1EventLooper(events, timer, music.deltaTimeSpec)
looper.starting = Runnable {
// all control reset on all channels.
for (i in 0..15) {
buffer[0] = (i + MidiChannelStatus.CC).toByte()
buffer[1] = MidiCC.RESET_ALL_CONTROLLERS.toByte()
buffer[2] = 0
output.send(buffer, 0, 3, 0)
}
}
val listener = object : OnMidi1EventListener {
override fun onEvent(e: Midi1Event) {
val m = e.message
when (m.statusCode.toUnsigned()) {
MidiChannelStatus.NOTE_OFF,
MidiChannelStatus.NOTE_ON -> {
if (mutedChannels.contains(m.channel.toUnsigned()))
return // ignore messages for the masked channel.
}
Midi1Status.SYSEX -> {
val s = m as Midi1CompoundMessage
if (buffer.size <= m.extraDataLength)
buffer = ByteArray(max(m.extraDataLength + 1, buffer.size * 2))
buffer[0] = m.statusByte
m.extraData!!.copyInto(buffer, 1, s.extraDataOffset, s.extraDataLength)
output.send(buffer, 0, s.extraDataLength + 1, 0)
return
}
Midi1Status.SYSEX_END -> {
val s = m as Midi1CompoundMessage
if (buffer.size < m.extraDataLength)
buffer = ByteArray(max(m.extraDataLength, buffer.size * 2))
m.extraData!!.copyInto(buffer, 0, s.extraDataOffset, s.extraDataLength)
output.send(buffer, 0, s.extraDataLength, 0)
return
}
Midi1Status.META -> {
// do nothing.
return
}
}
val size = Midi1Message.fixedDataSize(m.statusByte)
buffer[0] = m.statusByte
buffer[1] = m.msb
buffer[2] = m.lsb
output.send(buffer, 0, size + 1, 0)
}
}
addOnEventListener(listener)
}
private val music: Midi1Music
private var buffer = ByteArray(0x100)
internal var events: MutableList
final override val looper: Midi1EventLooper
fun addOnEventListener(listener: OnMidi1EventListener) {
looper.messageHandlers.add(listener)
}
fun removeOnEventListener(listener: OnMidi1EventListener) {
looper.messageHandlers.remove(listener)
}
override val positionInMilliseconds: Long
get() = music.getTimePositionInMillisecondsForTick(playDeltaTime).toLong()
override val totalPlayTimeMilliseconds: Int
get() = Midi1Music.getTotalPlayTimeMilliseconds(events, music.deltaTimeSpec)
override fun seek(ticks: Int) {
looper.seek(SimpleMidi1SeekProcessor(ticks), ticks)
}
override fun setMutedChannels(mutedChannels: Iterable) {
this.mutedChannels = mutedChannels.toList()
// additionally send all sound off for the muted channels.
for (ch in 0..15)
if (!mutedChannels.contains(ch))
output.send(arrayOf((0xB0 + ch).toByte(), 120, 0).toByteArray(), 0, 3, 0)
}
}
internal class SimpleMidi1SeekProcessor(ticks: Int) : SeekProcessor {
private var seekTo: Int = ticks
private var current: Int = 0
override fun filterEvent(evt: Midi1Event): SeekFilterResult {
current += evt.deltaTime
if (current >= seekTo)
return SeekFilterResult.PASS_AND_TERMINATE
when (evt.message.statusCode.toUnsigned()) {
MidiChannelStatus.NOTE_ON, MidiChannelStatus.NOTE_OFF -> return SeekFilterResult.BLOCK
}
return SeekFilterResult.PASS
}
}