Skip to content

Commit

Permalink
Use midi-file
Browse files Browse the repository at this point in the history
  • Loading branch information
colin-weng-s committed May 21, 2024
1 parent f0c857f commit 080dec0
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 89 deletions.
2 changes: 1 addition & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ kotlin {
implementation(npm("jszip", "3.5.0"))
implementation(npm("encoding-japanese", "1.0.30"))
implementation(npm("uuid", "8.3.2"))
implementation(npm("midi-parser-js", "4.0.4"))
implementation(npm("midi-file", "1.2.4"))
implementation(npm("js-yaml", "4.1.0"))
}
}
Expand Down
6 changes: 3 additions & 3 deletions core/src/main/kotlin/Library.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

import com.sdercolin.utaformatix.data.Document
import core.io.UfData
import core.model.ProjectContainer
import core.model.ExportResult
import core.model.Format
import core.model.ImportParams
import core.model.JapaneseLyricsType
import core.model.ProjectContainer
import core.process.lyrics.japanese.analyseJapaneseLyricsTypeForProject
import core.process.lyrics.japanese.convertJapaneseLyrics as convertJapaneseLyricsBase
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
Expand All @@ -17,6 +16,7 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.w3c.files.File
import kotlin.js.Promise
import core.process.lyrics.japanese.convertJapaneseLyrics as convertJapaneseLyricsBase

@JsExport
fun parseVsqx(file: File): Promise<ProjectContainer> = parse(listOf(file), Format.Vsqx)
Expand Down Expand Up @@ -124,7 +124,7 @@ fun convertJapaneseLyrics(
project: ProjectContainer,
fromType: JapaneseLyricsType,
targetType: JapaneseLyricsType,
convertVowelConnections: Boolean
convertVowelConnections: Boolean,
): ProjectContainer {
val baseProject = project.project
val newProject = core.model.Project(
Expand Down
39 changes: 14 additions & 25 deletions core/src/main/kotlin/core/io/Mid.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,47 +20,36 @@ import org.w3c.files.File

object Mid {

private fun customInterpreter(
metaType: Byte,
arrayBuffer: dynamic,
): dynamic {
// we need to handle 0x20 `Channel Prefix` meta event,
// otherwise the parser will break and all the following data get shifted
if (metaType != 0x20.toByte()) return false
return arrayOf(arrayBuffer.readInt(1))
}

suspend fun parseMidi(file: File): dynamic {
val bytes = file.readAsArrayBuffer()
val midiParser = core.external.require("midi-parser-js")
midiParser.customInterpreter = ::customInterpreter
return midiParser.parse(Uint8Array(bytes))
val midiParser = core.external.require("midi-file")
return midiParser.parseMidi(Uint8Array(bytes))
}

fun parseMasterTrack(
timeDivision: Int,
masterTrack: dynamic,
events: Array<dynamic>,
measurePrefix: Int,
warnings: MutableList<ImportWarning>,
): Triple<List<Tempo>, List<TimeSignature>, Long> {
val events = masterTrack.event as Array<dynamic>
var tickPosition = 0
val tickCounter = TickCounter()
val rawTempos = mutableListOf<Tempo>()
val rawTimeSignatures = mutableListOf<TimeSignature>()
for (event in events) {
tickPosition += MidiUtil.convertInputTimeToStandardTime(event.deltaTime as Int, timeDivision)
when (MidiUtil.MetaType.parse(event.metaType as? Byte)) {
MidiUtil.MetaType.Tempo -> {
when (event.type as String) {
"setTempo" -> {
rawTempos.add(
Tempo(
tickPosition.toLong(),
MidiUtil.convertMidiTempoToBpm(event.data as Int),
MidiUtil.convertMidiTempoToBpm(event.microsecondsPerBeat as Int),
),
)
}
MidiUtil.MetaType.TimeSignature -> {
val (numerator, denominator) = MidiUtil.parseMidiTimeSignature(event.data)
"timeSignature" -> {
val numerator = event.numerator as Int
val denominator = event.denominator as Int
tickCounter.goToTick(tickPosition.toLong(), numerator, denominator)
rawTimeSignatures.add(
TimeSignature(
Expand Down Expand Up @@ -131,15 +120,15 @@ object Mid {
return counter.tick
}

fun extractVsqTextsFromMetaEvents(midiTracks: Array<dynamic>): List<String> {
fun extractVsqTextsFromMetaEvents(midiTracks: Array<Array<dynamic>>): List<String> {
return midiTracks.drop(1)
.map { track ->
(track.event as Array<dynamic>)
track
.fold("") { accumulator, element ->
val metaType = MidiUtil.MetaType.parse(element.metaType as? Byte)
if (metaType != MidiUtil.MetaType.Text) accumulator
val metaType = element.type as String
if (metaType != "text") accumulator
else {
var text = element.data as String
var text = element.text as String
text = text.asByteTypedArray().decode("SJIS")
text = text.drop(3)
text = text.drop(text.indexOf(':') + 1)
Expand Down
61 changes: 27 additions & 34 deletions core/src/main/kotlin/core/io/StandardMid.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package core.io

import core.exception.IllegalFileException
import core.external.Encoding
import core.model.DEFAULT_LYRIC
import core.model.ExportResult
Expand All @@ -25,11 +24,8 @@ object StandardMid {

suspend fun parse(file: File): Project {
val midi = Mid.parseMidi(file)
if (midi == false) {
throw IllegalFileException.IllegalMidiFile()
}
val timeDivision = midi.timeDivision as Int
val midiTracks = midi.track as Array<dynamic>
val timeDivision = midi.header.ticksPerBeat as Int
val midiTracks = midi.tracks as Array<Array<dynamic>>

val warnings = mutableListOf<ImportWarning>()
val (tempos, timeSignatures, tickPrefix) = Mid.parseMasterTrack(
Expand Down Expand Up @@ -59,7 +55,7 @@ object StandardMid {
id: Int,
timeDivision: Int,
tickPrefix: Long,
midiTrack: dynamic,
events: Array<dynamic>,
): Track {
var trackName = "Track ${id + 1}"
val notes = mutableListOf<Note>()
Expand All @@ -70,7 +66,6 @@ object StandardMid {

val pendingNotesHeadsWithLyric = mutableMapOf<Int, Note>()

val events = midiTrack.event as Array<dynamic>
for (event in events) {
val delta = MidiUtil.convertInputTimeToStandardTime(event.deltaTime as Int, timeDivision)
if (delta > 0) {
Expand All @@ -85,45 +80,43 @@ object StandardMid {
pendingNoteHead = null
}
tickPosition += delta
when (MidiUtil.MetaType.parse(event.metaType as? Byte)) {
MidiUtil.MetaType.Lyric -> {
val lyricBytes = (event.data as String).asByteTypedArray()
when (event.type as String) {
"lyrics" -> {
val lyricBytes = (event.text as String).asByteTypedArray()
val detectedEncoding = Encoding.detect(lyricBytes)
pendingLyric = lyricBytes.decode(detectedEncoding)
}
MidiUtil.MetaType.Text -> {
"text" -> {
if (pendingLyric == null) {
val textBytes = (event.data as String).asByteTypedArray()
val textBytes = (event.text as String).asByteTypedArray()
val detectedEncoding = Encoding.detect(textBytes)
pendingLyric = textBytes.decode(detectedEncoding)
}
}
MidiUtil.MetaType.TrackName -> {
val trackNameBytes = (event.data as String).asByteTypedArray()
"trackName" -> {
val trackNameBytes = (event.text as String).asByteTypedArray()
val detectedEncoding = Encoding.detect(trackNameBytes)
trackName = trackNameBytes.decode(detectedEncoding)
}
else -> when (MidiUtil.EventType.parse(event.type as? Byte)) {
MidiUtil.EventType.NoteOn -> {
val channel = event.channel as Int
val key = event.data[0] as Int
pendingNoteHead = Note(
id = 0,
tickOn = tickPosition,
tickOff = tickPosition,
key = key,
lyric = DEFAULT_LYRIC,
) to channel
}
MidiUtil.EventType.NoteOff -> {
val channel = event.channel as Int
pendingNotesHeadsWithLyric[channel]?.let {
notes += it.copy(tickOff = tickPosition)
}
pendingNotesHeadsWithLyric.remove(channel)
"noteOn" -> {
val channel = event.channel as Int
val key = event.noteNumber as Int
pendingNoteHead = Note(
id = 0,
tickOn = tickPosition,
tickOff = tickPosition,
key = key,
lyric = DEFAULT_LYRIC,
) to channel
}
"noteOff" -> {
val channel = event.channel as Int
pendingNotesHeadsWithLyric[channel]?.let {
notes += it.copy(tickOff = tickPosition)
}
else -> Unit
pendingNotesHeadsWithLyric.remove(channel)
}
else -> Unit
}
}
return Track(
Expand Down
7 changes: 2 additions & 5 deletions core/src/main/kotlin/core/io/VsqLike.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ object VsqLike {

suspend fun match(file: File): Boolean {
val midi = Mid.parseMidi(file)
if (midi == false) {
return false
}
val midiTracks = midi.track as Array<dynamic>
val tracksAsText = Mid.extractVsqTextsFromMetaEvents(midiTracks).filter { it.isNotEmpty() }
if (tracksAsText.isEmpty()) return false
Expand All @@ -51,8 +48,8 @@ object VsqLike {

suspend fun parse(file: File, format: Format, params: ImportParams): Project {
val midi = Mid.parseMidi(file)
val midiTracks = midi.track as Array<dynamic>
val timeDivision = midi.timeDivision as Int
val timeDivision = midi.header.ticksPerBeat as Int
val midiTracks = midi.tracks as Array<Array<dynamic>>
val warnings = mutableListOf<ImportWarning>()
val tracksAsText = Mid.extractVsqTextsFromMetaEvents(midiTracks).filter { it.isNotEmpty() }
val measurePrefix = getMeasurePrefix(tracksAsText.first())
Expand Down
17 changes: 0 additions & 17 deletions core/src/main/kotlin/core/util/MidiUtil.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package core.util

import kotlin.math.pow

object MidiUtil {

private const val StandardTimeDivision = 480
Expand All @@ -15,10 +13,6 @@ object MidiUtil {
NoteOn(0x09);

fun getStatusByte(channel: Int) = ((value.toInt() shl 4) or channel).toByte()

companion object {
fun parse(value: Byte?): EventType? = values().find { it.value == value }
}
}

enum class MetaType(val value: Byte) {
Expand All @@ -30,10 +24,6 @@ object MidiUtil {
EndOfTrack(0x2f);

val eventHeaderBytes get() = listOf(0xff.toByte(), value)

companion object {
fun parse(value: Byte?): MetaType? = values().find { it.value == value }
}
}

fun convertMidiTempoToBpm(midiTempo: Int) =
Expand All @@ -42,13 +32,6 @@ object MidiUtil {
fun convertBpmToMidiTempo(bpm: Double) =
(1000 * 1000 * 60 / bpm).toInt()

fun parseMidiTimeSignature(data: dynamic): Pair<Int, Int> {
data as Array<Int>
val numerator = data[0]
val denominator = (2f.pow(data[1])).toInt()
return numerator to denominator
}

fun generateMidiTimeSignatureBytes(numerator: Int, denominator: Int): List<Byte> {
return listOf(
numerator.toByte(),
Expand Down
8 changes: 4 additions & 4 deletions kotlin-js-store/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2626,10 +2626,10 @@ micromatch@^4.0.2:
braces "^3.0.2"
picomatch "^2.3.1"

midi-[email protected].4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/midi-parser-js/-/midi-parser-js-4.0.4.tgz#4b0af504e1fdc78409d27fb17473fbb61eee53cc"
integrity sha512-yCCUNemjDET40SPslUbAyi7lClSS+ttcBAWNY337kJouRFsEtIql38Fp4p5xfODkltFvuqHJL9JytnZIrzpJbw==
midi-[email protected].4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/midi-file/-/midi-file-1.2.4.tgz#e5803a8fc79cdd1692ac6ef6b1491043b397eb87"
integrity sha512-B5SnBC6i2bwJIXTY9MElIydJwAmnKx+r5eJ1jknTLetzLflEl0GWveuBB6ACrQpecSRkOB6fhTx1PwXk2BVxnA==

[email protected], "mime-db@>= 1.43.0 < 2":
version "1.52.0"
Expand Down

0 comments on commit 080dec0

Please sign in to comment.