Skip to content

Instantly share code, notes, and snippets.

@LucasAlfare
Last active September 6, 2022 19:28
Show Gist options
  • Save LucasAlfare/e55a78f33d5919402b18baa44cf0add1 to your computer and use it in GitHub Desktop.
Save LucasAlfare/e55a78f33d5919402b18baa44cf0add1 to your computer and use it in GitHub Desktop.
Helper that I created in order to extract better information from MIDI files about notes, chords and rests (and their durations). I should include timming convertions in the future, btw feel free to help improve.
/**
* @author Francisco Lucas (a.k.a. LucasAlfare); 06/09/2022.
*/
import java.io.File
import javax.sound.midi.MidiSystem
import javax.sound.midi.ShortMessage
fun main() {
processedNotesFrom("test.mid")
.forEach {
println(it)
}
}
/**
* This function takes a MIDI file and parses its info into a list of
* [Element]. [Element] is an entity that can represent a Note, a Chord
* or a Rest.
*
* This is made in order to keep the info easy to understand, making possible
* to know what is each king of separated element in the MIDI structure.
*
* @param midiFilePathname the path from a valid MIDI file, in [String] format.
*
* @return a list of [Element] items, which could be [Note], [Chord] or [Rest].
*/
fun processedNotesFrom(midiFilePathname: String): MutableList<Any> {
val notes = mutableListOf<Any>()
val rests = mutableListOf<Any>()
val chords = mutableListOf<Any>()
val final = mutableListOf<Any>()
fun buildNotesFromFile() {
// loads a MIDI file into a sequence object
val sequence = MidiSystem.getSequence(File(midiFilePathname))
val tmpNotesMap = mutableMapOf<Int, Any>()
// iterates over the tracks of the loaded sequence
sequence.tracks.forEach { track ->
// iterates over events of the current track
for (i in 0 until track.size()) {
val event = track.get(i)
val message = event.message
if (message is ShortMessage) {
when (message.command) {
ShortMessage.NOTE_ON -> {
val currentKey = message.data1
tmpNotesMap[currentKey] = Note(key = currentKey, on = event.tick)
}
ShortMessage.NOTE_OFF -> {
val currentKey = message.data1
val tmpNote = tmpNotesMap[currentKey] as Note
tmpNote.off = event.tick
notes += tmpNote
}
else -> {}
}
}
}
}
}
fun buildRests() {
var lastOffTick = 0L
notes.forEach {
(it as Note) // exposing expected type in order force smart cast
if (it.on > lastOffTick) {
rests += Rest(lastOffTick, it.on)
}
lastOffTick = it.off
}
}
fun buildChords() {
var lastOnTick = 0L
var lastOffTick = 0L
notes.forEachIndexed { index, element ->
(element as Note) // exposing expected type in order force smart cast
if (element.on == lastOnTick && element.off == lastOffTick) {
@Suppress("UNCHECKED_CAST")
chords += Chord(
listOf(element, notes[index - 1]) as MutableList<Note>
)
}
lastOnTick = element.on
lastOffTick = element.off
}
}
fun buildFinal() {
val notesRestsChords = mutableListOf<Any>()
notesRestsChords.addAll(notes)
notesRestsChords.addAll(rests)
notesRestsChords.addAll(chords)
notesRestsChords.sortBy {
when (it) {
is Note -> it.on
is Rest -> it.on
else -> (it as Chord).on
}
}
notesRestsChords.forEach {
if (it is Chord) {
final.removeLast()
final.removeLast()
}
final += it
}
}
buildNotesFromFile()
buildRests()
buildChords()
buildFinal()
return final
}
class Note(var key: Int = 0, var on: Long = 0, var off: Long = 0) {
// just for convenience
private val names = arrayOf("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")
private val octave = (key / 12) - 1
override fun toString(): String {
return "Note(name=${names[key % 12]}${octave}, on=$on, off=$off)"
}
}
class Rest(var on: Long = 0, var off: Long = 0) {
override fun toString() = "Rest(on=$on, off=$off)"
}
@Suppress("MemberVisibilityCanBePrivate")
class Chord(
val notes: MutableList<Note> = mutableListOf(),
var on: Long = notes.first().on,
var off: Long = notes.first().off
) {
init {
notes.sortBy { it.key }
}
override fun toString() = "Chord(notes=$notes, on=$on, off=$off)"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment