
commonMain.dev.atsushieno.ktmidi.Midi1Music.kt Maven / Gradle / Ivy
@file:Suppress("unused")
package dev.atsushieno.ktmidi
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport
@OptIn(ExperimentalJsExport::class)
class Midi1Music {
@JsExport.Ignore
internal class SmfDeltaTimeComputer: DeltaTimeComputer() {
override fun messageToDeltaTime(message: Midi1Event) = message.deltaTime
override fun isTempoMessage(message: Midi1Event) =
message.message.statusCode.toUnsigned() == Midi1Status.META && message.message.msb.toInt() == MidiMetaType.TEMPO
override fun getTempoValue(message: Midi1Event): Int {
val e = message.message as Midi1CompoundMessage
return getSmfTempo(e.extraData!!, e.extraDataOffset)
}
}
companion object {
const val DEFAULT_TEMPO = 500000
/**
* Calculates ticks per seconds for SMPTE for an SMF delta time division specification raw value.
* `frameRate` should be one of 24,25,29, and 30. We dare to use UByte as the argument type to indicate that the argument is NOT the raw negative number in deltaTimeSpec.
*/
fun getSmpteTicksPerSeconds(smfDeltaTimeSpec: Int) =
getSmpteTicksPerSeconds((-smfDeltaTimeSpec shr 8).toUByte(), smfDeltaTimeSpec and 0xFF)
private fun getSmpteTicksPerSeconds(nominalFrameRate: UByte, ticksPerFrame: Int) = getActualSmpteFrameRate(nominalFrameRate).toInt() * ticksPerFrame
// The valid values for SMPTE frameRate are 24, 25, 29, and 30, but 29 means 30 frames per second.
private fun getActualSmpteFrameRate(nominalFrameRate: UByte) =
if (nominalFrameRate == 29.toUByte()) 30u else nominalFrameRate
// Note that the default tempo expects that a quarter note in 0.5 sec. (in 120bpm)
fun getSmpteDurationInSeconds(smfDeltaTimeSpec: Int, ticks: Int, tempo: Int = DEFAULT_TEMPO, tempoRatio: Double = 1.0): Double =
tempo.toDouble() / 250_000 * ticks / getSmpteTicksPerSeconds(smfDeltaTimeSpec) / tempoRatio
// Note that the default tempo expects that a quarter note in 0.5 sec. (in 120bpm)
fun getSmpteTicksForSeconds(smfDeltaTimeSpec: Int, duration: Double, tempo: Int = DEFAULT_TEMPO, tempoRatio: Double = 1.0): Int =
(duration * tempoRatio / tempo * 250_000 * getSmpteTicksPerSeconds(smfDeltaTimeSpec)).toInt()
fun getSmfTempo(data: ByteArray, offset: Int): Int {
if (data.size < offset + 2)
throw IndexOutOfBoundsException("data array must be longer than argument offset $offset + 2")
return (data[offset].toUnsigned() shl 16) + (data[offset + 1].toUnsigned() shl 8) + data[offset + 2]
}
fun getSmfBpm(data: ByteArray, offset: Int): Double {
return 60000000.0 / getSmfTempo(data, offset)
}
private val calc = SmfDeltaTimeComputer()
fun filterEvents(messages: Iterable, filter: (Midi1Event) -> Boolean) =
calc.filterEvents(messages, filter).map { p -> Midi1Event(p.duration.value, p.value.message) }
fun getTotalPlayTimeMilliseconds(messages: Iterable, deltaTimeSpec: Int) = calc.getTotalPlayTimeMilliseconds(messages, deltaTimeSpec)
fun getPlayTimeMillisecondsAtTick(messages: Iterable, ticks: Int, deltaTimeSpec: Int) = calc.getPlayTimeMillisecondsAtTick(messages, ticks, deltaTimeSpec)
}
val tracks: MutableList = mutableListOf()
var deltaTimeSpec: Int = 0
var format: Byte = 0
fun addTrack(track: Midi1Track) {
this.tracks.add(track)
}
fun filterEvents(filter: (Midi1Event) -> Boolean): Iterable {
if (format != 0.toByte())
return mergeTracks().filterEvents(filter)
return filterEvents(tracks[0].events, filter).asIterable()
}
fun getTotalTicks(): Int {
if (format != 0.toByte())
return mergeTracks().getTotalTicks()
return tracks[0].events.sumOf { m: Midi1Event -> m.deltaTime }
}
fun getTotalPlayTimeMilliseconds(): Int {
if (format != 0.toByte())
return mergeTracks().getTotalPlayTimeMilliseconds()
return getTotalPlayTimeMilliseconds(tracks[0].events, deltaTimeSpec)
}
fun getTimePositionInMillisecondsForTick(ticks: Int): Int {
if (format != 0.toByte())
return mergeTracks().getTimePositionInMillisecondsForTick(ticks)
return getPlayTimeMillisecondsAtTick(tracks[0].events, ticks, deltaTimeSpec)
}
init {
this.format = 1
}
}
class Midi1Track(val events: MutableList = mutableListOf()) {
@Deprecated("Use events property instead", ReplaceWith("events"))
val messages: MutableList
get() = events
}
class Midi1Event(val deltaTime: Int, val message: Midi1Message) {
companion object {
fun encode7BitLength(length: Int): Sequence =
sequence {
var v = length
while (v >= 0x80) {
yield((v % 0x80 + 0x80).toByte())
v /= 0x80
}
yield (v.toByte())
}
}
@Deprecated("Use message property instead (you might need casting to Midi1CompoundMessage)")
val event: Midi1Message by lazy { message }
override fun toString(): String = "[$deltaTime:$message]"
}
interface Midi1Message {
companion object {
@Deprecated("Use convert(bytes, index, size, sysExChunkProcessor. It's better if you supply Midi1SysExChunkProcessor() (it is null by default for backward compatibility).", ReplaceWith("convert(bytes, index, size, null)"))
fun convert(bytes: ByteArray, index: Int, size: Int): Sequence = convert(bytes, index, size, null)
fun convert(bytes: ByteArray, index: Int, size: Int,
sysExChunkProcessor: Midi1SysExChunkProcessor? = Midi1SysExChunkProcessor()
): Sequence = convert(bytes.drop(index).take(size), sysExChunkProcessor)
fun convert(bytes: List,
sysExChunkProcessor: Midi1SysExChunkProcessor? = Midi1SysExChunkProcessor()
): Sequence = sequence {
if (sysExChunkProcessor == null)
yieldAll(convertInternal(bytes))
else
sysExChunkProcessor.process(bytes)
.map { convertInternal(it) }
.forEach { yieldAll(it) }
}
private fun convertInternal(bytes: List): Sequence = sequence {
var i = 0
val size = bytes.size
val end = bytes.size
while (i < end) {
if (bytes[i].toUnsigned() == 0xF0) {
yield(Midi1CompoundMessage(0xF0, 0, 0, bytes.drop(i).take(size).toByteArray()))
i += size
} else {
if (end < i + fixedDataSize(bytes[i]))
throw Midi1Exception("Received data was incomplete to build MIDI status message for '${bytes[i]}' status.")
val z = fixedDataSize(bytes[i])
yield(
Midi1SimpleMessage(
bytes[i].toUnsigned(),
(if (z > 0) bytes[i + 1].toUnsigned() else 0),
(if (z > 1) bytes[i + 2].toUnsigned() else 0),
)
)
i += z + 1
}
}
}
fun fixedDataSize(statusByte: Byte): Byte =
when ((statusByte.toUnsigned() and 0xF0)) {
0xF0 -> {
when (statusByte.toUnsigned()) {
0xF1, 0xF3 -> 1
0xF2 -> 2
else -> 0
}
} // including 0xF7, 0xFF
0xC0, 0xD0 -> 1
else -> 2
}
}
val value: Int
/// Contains status code, and channel for Midi1SimpleMessage.
val statusByte: Byte
get() = (value and 0xFF).toByte()
/// Contains channel status (80-E0), Fn for System messages, or meta event in SMF.
val statusCode: Byte
get() =
when (statusByte.toUnsigned()) {
Midi1Status.META,
Midi1Status.SYSEX,
Midi1Status.SYSEX_END -> this.statusByte
else -> (value and 0xF0).toByte()
}
@Deprecated("Use statusCode property instead", ReplaceWith("statusCode"))
val eventType: Byte
get() = statusCode
val msb: Byte
get() = ((value and 0xFF00) shr 8).toByte()
val lsb: Byte
get() = ((value and 0xFF0000) shr 16).toByte()
val metaType: Byte
get() = msb
val channel: Byte
get() = (value and 0x0F).toByte()
}
data class Midi1SimpleMessage(override val value: Int) : Midi1Message {
constructor(type: Int, arg1: Int, arg2: Int)
: this((type.toUInt() + (arg1.toUInt() shl 8) + (arg2.toUInt() shl 16)).toInt())
}
class Midi1CompoundMessage : Midi1Message {
constructor (
type: Int,
arg1: Int,
arg2: Int,
extraData: ByteArray? = null,
extraOffset: Int = 0,
extraLength: Int = extraData?.size ?: 0
){
this.value = (type.toUInt() + (arg1.toUInt() shl 8) + (arg2.toUInt() shl 16)).toInt()
this.extraData = extraData
this.extraDataOffset = extraOffset
this.extraDataLength = extraLength
}
// A simple (non-F0h, non-FFh) message could be represented in a 32-bit int
final override val value: Int
// They are used by SysEx and meta events (if used for SMF)
// `extraData` *might* contain EndSysEx (F7) byte.
val extraData: ByteArray?
val extraDataOffset: Int
val extraDataLength: Int
override fun toString(): String {
return value.toString(16)
}
}
fun Midi1Music.mergeTracks() : Midi1Music =
Midi1TrackMerger(this).getMergedMessages()
internal class Midi1TrackMerger(private var source: Midi1Music) {
internal fun getMergedMessages(): Midi1Music {
var l = mutableListOf()
for (track in source.tracks) {
var delta = 0
for (mev in track.events) {
delta += mev.deltaTime
l.add(Midi1Event(delta, mev.message))
}
}
if (l.size == 0) {
val ret = Midi1Music().apply {
format = 0
addTrack(Midi1Track())
}
ret.deltaTimeSpec = source.deltaTimeSpec // empty (why did you need to sort your song file?)
return ret
}
// Simple sorter does not work as expected.
// For example, it does not always preserve event orders on the same channels when the delta time
// of event B after event A is 0. It could be sorted either as A->B or B->A, which is no-go for
// MIDI messages. For example, "ProgramChange at Xmsec. -> NoteOn at Xmsec." must not be sorted as
// "NoteOn at Xmsec. -> ProgramChange at Xmsec.".
//
// To resolve this issue, we have to sort "chunk" of events, not all single events themselves, so
// that order of events in the same chunk is preserved.
// i.e. [AB] at 48 and [CDE] at 0 should be sorted as [CDE] [AB].
val indexList = mutableListOf()
var prev = -1
var i = 0
while (i < l.size) {
if (l[i].deltaTime != prev) {
indexList.add(i)
prev = l[i].deltaTime
}
i++
}
val idxOrdered = indexList.sortedBy { n -> l[n].deltaTime }
// now build a new event list based on the sorted blocks.
val l2 = mutableListOf()
var idx: Int
i = 0
while (i < idxOrdered.size) {
idx = idxOrdered[i]
prev = l[idx].deltaTime
while (idx < l.size && l[idx].deltaTime == prev) {
l2.add(l[idx])
idx++
}
i++
}
l = l2
// now messages should be sorted correctly.
var waitToNext = l[0].deltaTime
i = 0
while (i < l.size - 1) {
val tmp = l[i + 1].deltaTime - l[i].deltaTime
l[i] = Midi1Event(waitToNext, l[i].message)
waitToNext = tmp
i++
}
l[l.size - 1] = Midi1Event(waitToNext, l[l.size - 1].message)
val music = Midi1Music()
music.deltaTimeSpec = source.deltaTimeSpec
music.format = 0
music.tracks.add(Midi1Track(l))
return music
}
}
fun Midi1Track.splitTracksByChannel(deltaTimeSpec: Byte) : Midi1Music =
Midi1TrackSplitter(events, deltaTimeSpec).split()
open class Midi1TrackSplitter(private val source: MutableList, private val deltaTimeSpec: Byte) {
companion object {
fun split(source: MutableList, deltaTimeSpec: Byte): Midi1Music {
return Midi1TrackSplitter(source, deltaTimeSpec).split()
}
}
private val tracks = HashMap()
internal class SplitTrack(val trackID: Int) {
private var totalDeltaTime: Int
val track: Midi1Track = Midi1Track()
fun addMessage(deltaInsertAt: Int, e: Midi1Event) {
val e2 = Midi1Event(deltaInsertAt - totalDeltaTime, e.message)
track.events.add(e2)
totalDeltaTime = deltaInsertAt
}
init {
totalDeltaTime = 0
}
}
private fun getTrack(track: Int): SplitTrack {
var t = tracks[track]
if (t == null) {
t = SplitTrack(track)
tracks[track] = t
}
return t
}
// Override it to customize track dispatcher. It would be
// useful to split note messages out from non-note ones,
// to ease data reading.
open fun getTrackId(e: Midi1Event): Int {
return when (e.message.statusCode.toUnsigned()) {
Midi1Status.META, Midi1Status.SYSEX, Midi1Status.SYSEX_END -> -1
else -> e.message.channel.toUnsigned()
}
}
fun split(): Midi1Music {
var totalDeltaTime = 0
for (e in source) {
totalDeltaTime += e.deltaTime
val id: Int = getTrackId(e)
getTrack(id).addMessage(totalDeltaTime, e)
}
val m = Midi1Music()
m.deltaTimeSpec = deltaTimeSpec.toInt()
for (t in tracks.values)
m.tracks.add(t.track)
return m
}
init {
val mtr = SplitTrack(-1)
tracks[-1] = mtr
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy