A pragmatic, Kotlin Multiplatform library for parsing, building, and analyzing Standard MIDI files (SMF).
- Parse MIDI files – Read Standard MIDI files with configurable parsing modes (lenient vs strict)
- Build MIDI files – Create MIDI files programmatically using a Kotlin DSL
- Zero dependencies – Pure Kotlin, no external runtime dependencies
- Multiplatform ready – Built on Kotlin Multiplatform (JVM primary)
- Analyze MIDI data – Extract insights like polyphony, arcs (note on/off pairs), and timing analysis
- Pragmatic spec compliance – Handles real-world MIDI files with variations and quirks
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)
// Access tracks and events
midiFile.tracks.forEach { track ->
println("Track: ${track.name}")
println("Notes: ${track.notes.size}")
track.notes.forEach { noteEvent ->
println(" Note: ${noteEvent.note}, Tick: ${noteEvent.tick}, Channel: ${noteEvent.channel}")
}
}
// Or read from a stream (memory-efficient for large files)
File("song.mid").inputStream().use { stream ->
val midiFile = StandardMidiFileReader().readStream(stream)
}Create MIDI files programmatically with the DSL:
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)The builder includes convenient extension functions for note durations:
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.thirtySecondTriplet
// 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}")
}
// Analyze polyphony (simultaneous notes)
import org.wysko.kmidi.midi.analysis.Polyphony
val polyphonyData = Polyphony.analyze(midiFile)
println("Max simultaneous notes: ${polyphonyData.maxPolyphony}")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)- Allows running status across non-MIDI events
- Tolerates track count discrepancies
- Coerces velocity values to valid range
- Ignores invalid channel prefixes
- Handles incomplete meta-events gracefully
- Recovers from unexpected end-of-file conditions
- Enforces SMF specification 1.0 exactly
- Rejects any non-conformant data
- Throws exceptions on spec violations
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:
- NoteEvent –
NoteOn,NoteOff(note messages) - ControlChangeEvent – CC messages (e.g., volume, pan)
- ProgramEvent – instrument changes
- PitchWheelChangeEvent – pitch bend
- ChaussePressureEvent – channel pressure
- PolyphonicKeyPressureEvent – key pressure
- SysexEvent – system exclusive messages
- MetaEvent – metadata (tempo, time signature, key signature, text, etc.)
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
val note: Byte // MIDI note number (0-127)
val channel: Byte // MIDI channel (0-15)
val velocity: Byte // note on velocity
}- Kotlin 1.9.24+
- Gradle 8.0+
- JDK 8+
# Run all tests
./gradlew jvmTest
# Run code quality checks
./gradlew detekt
# Generate Dokka documentation
./gradlew dokka
# Publish to local Maven repository (for testing)
./gradlew publishToMavenLocalsrc/
├── 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.
- Standard MIDI File Specification 1.0
- MIDI Wikipedia Page
- kmidi on Maven Central
See GitHub Releases for version history.
Need help? Open an issue on GitHub.