Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use midi-file instead of midi-parser-js #165

Merged
merged 14 commits into from
May 21, 2024
7 changes: 6 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,13 @@ val copyCoreResources by tasks.register<Copy>("copyCoreResources") {
include("**/*.*")
}
into("build/js/packages/utaformatix/kotlin/")
mustRunAfter("jsProductionExecutableCompileSync")
mustRunAfter("jsDevelopmentExecutableCompileSync")
}
tasks.named("jsProcessResources") {
tasks.named("jsBrowserProductionWebpack") {
dependsOn(copyCoreResources)
}
tasks.named("jsBrowserDevelopmentRun") {
dependsOn(copyCoreResources)
}

Expand Down
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
44 changes: 19 additions & 25 deletions core/src/main/kotlin/core/io/Mid.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package core.io

import core.exception.IllegalFileException
import core.model.ImportWarning
import core.model.Project
import core.model.Tempo
Expand All @@ -20,47 +21,40 @@ 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")
try {
return midiParser.parseMidi(Uint8Array(bytes))
} catch (e: dynamic) { // parseMidi throws a string, not an Error
throw IllegalFileException.IllegalMidiFile()
}
}

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 +125,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()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the new library handle unexpected input?
If it throws an error, we could catch it and rethrow the IllegalFileException.IllegalMidiFile() with the original error as the cause (passed by constructor)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it throws string (not Error). I changed to catch dynamic so that the error will be handled

}
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
Loading