kmidi
0.2.1indexedParse, build and analyze Standard MIDI files using a concise DSL; supports lenient/strict parsing for real-world quirks, note arc and polyphony analysis, time-denomination helpers, zero dependencies.
Parse, build and analyze Standard MIDI files using a concise DSL; supports lenient/strict parsing for real-world quirks, note arc and polyphony analysis, time-denomination helpers, zero dependencies.
A pragmatic, Kotlin Multiplatform library for parsing, building, and analyzing Standard MIDI files (SMF).
dependencies {
implementation("org.wysko:kmidi:0.2.1")
}
dependencies {
implementation 'org.wysko:kmidi:0.2.1'
}
<dependency>
<groupId>org.wysko</groupId>
<artifactId>kmidi-jvm</artifactId>
<version>0.2.1</version>
</dependency>
import org.wysko.kmidi.midi.reader.StandardMidiFileReader
import java.io.File
// Read from byte array
val bytes = File("song.mid").readBytes()
val midiFile = StandardMidiFileReader().readByteArray(bytes)
midiFile.tracks.forEach { track ->
println()
println()
track.notes.forEach { noteEvent ->
println()
}
}
File().inputStream().use { stream ->
midiFile = StandardMidiFileReader().readStream(stream)
}
Create MIDI files programmatically with the DSL:
The builder includes convenient extension functions for note durations:
// Example: Add a quarter note at tick 480 (1 quarter note from start)
add(NoteEvent.NoteOn(1.quarter, channel = 0, note = 60, velocity = 100))
// Add an eighth note 3 quarter notes in
add(NoteEvent.NoteOn(3.quarter + 1.eighth, channel = 0, note = 64, velocity = 100))
Extract insights from parsed MIDI files:
val midiFile = StandardMidiFileReader().readByteArray(bytes)
// Get note arcs (paired NoteOn/NoteOff events)
midiFile.tracks[0].arcs.forEach { arc ->
println("Note ${arc.note} plays from tick ${arc.start} to ${arc.end}")
println(" Duration: ${arc.end - arc.start} ticks")
println(" Velocity: ${arc.velocity}")
}
org.wysko.kmidi.midi.analysis.Polyphony
polyphonyData = Polyphony.analyze(midiFile)
println()
By default, kmidi uses lenient parsing to handle real-world MIDI files that may not strictly conform to the SMF specification:
// Lenient parsing (default) – permissive, allows file quirks
val lenientReader = StandardMidiFileReader(Policies.lenient)
val midiFile = lenientReader.readByteArray(bytes)
// Strict parsing – enforces SMF specification exactly
val strictReader = StandardMidiFileReader(Policies.strict)
val midiFile = strictReader.readByteArray(bytes)
You can also customize policies for specific use cases:
val customPolicies = StandardMidiFileReader.Policies(
allowRunningStatusAcrossNonMidiEvents = true,
allowTrackCountDiscrepancy = false,
coerceVelocityToRange = true,
ignoreBadChannelPrefixes = true,
ignoreBadKeySignatures = false,
ignoreIncompleteMetaEvents = true,
unexpectedEndOfFilePolicy = AllowDirty
)
val reader = StandardMidiFileReader(customPolicies)
The top-level container for a complete MIDI file:
data class StandardMidiFile(
val header: Header,
val tracks: List<Track>,
val tpq: Short // ticks per quarter note (shortcut to header.division)
)
A sequence of MIDI events:
data class Track(
val events: List<Event>
) {
val name: String? // First SequenceTrackName meta-event, if present
val notes: List<NoteEvent> // All note on/off events
val arcs: List<Arc> // Paired note on/off events
}
Events represent MIDI messages and metadata:
Represents a paired NoteOn/NoteOff event:
data class Arc(
val noteOn: NoteOn,
val noteOff: NoteOff
) {
val start: Int // tick where note starts
val end: Int // tick where note ends
note:
channel:
velocity:
}
# Run all tests
./gradlew jvmTest
# Run code quality checks
./gradlew detekt
# Generate Dokka documentation
./gradlew dokka
# Publish to local Maven repository (for testing)
./gradlew publishToMavenLocal
src/
├── commonMain/ # Multiplatform Kotlin code
│ └── kotlin/org/wysko/kmidi/
│ ├── midi/
│ │ ├── reader/ # MIDI file parsing
│ │ ├── builder/ # MIDI file construction DSL
│ │ ├── event/ # Event type definitions
│ │ ├── analysis/ # Analysis utilities
│ │ └── StandardMidiFile*.kt
│ └── stream/ # I/O abstractions
├── commonTest/ # Cross-platform tests
└── jvmTest/ # JVM-specific tests
└── resources/test_midi/ # Test MIDI files
This library implements the Standard MIDI File Specification 1.0 as defined by the International MIDI Association (IMA). It handles MIDI running status optimization, multiple track formats (0, 1, and 2), and both metrical and timecode-based time divisions.
Licensed under the Apache License, Version 2.0. See LICENSE file for details.
See GitHub Releases for version history.
Need help? Open an issue on GitHub.
import org.wysko.kmidi.midi.StandardMidiFileWriter
import org.wysko.kmidi.midi.builder.smf
import org.wysko.kmidi.midi.event.MetaEvent
import org.wysko.kmidi.midi.event.NoteEvent
import org.wysko.kmidi.midi.StandardMidiFile.Header.Format
val midiFile = smf {
format = Format.Format0
division = tpq(480) // 480 ticks per quarter note
track {
add(MetaEvent.SetTempo(0, 500000)) // 120 BPM
add(MetaEvent.TimeSignature(0, 4, 2, 24, 8))
// Add notes using time denomination helpers
add(NoteEvent.NoteOn(0.quarter, channel = 0, note = 60, velocity = 100))
add(NoteEvent.NoteOff(1.quarter, channel = 0, note = 60, velocity = 0))
add(NoteEvent.NoteOn(1.quarter, channel = 0, note = 64, velocity = 100))
add(NoteEvent.NoteOff(2.quarter, channel = 0, note = 64, velocity = 0))
}
}
// Write to file
val bytes = StandardMidiFileWriter().writeByteArray(midiFile)
File("output.mid").writeBytes(bytes)
Int.whole – whole note (4 quarter notes)Int.half – half note (2 quarter notes)Int.quarter – quarter noteInt.eighth – eighth noteInt.sixteenth – sixteenth noteInt.thirtySecond – thirty-second noteInt.quarterTriplet, Int.eighthTriplet, Int.sixteenthTriplet, Int.thirtySecondTripletNoteOn, NoteOff (note messages)Surfaced from shared tags and platforms — no rankings paid for.