From 00b60cd740c916330b4989e5e172578428f81cc3 Mon Sep 17 00:00:00 2001 From: Nanashi Date: Wed, 22 May 2024 07:57:54 +0900 Subject: [PATCH] Use midi-file instead of midi-parser-js (#165) * Change: Use midi-file * Fix: Fix importing assets * Fix: Run kotlinUpgradeYarnLock * Fix: Fix resolver * Change: Make ESModule conditional * Delete: Delete workaround * Delete: Delete unused dependency * Revert: Re-add workaround * Revert: Revert unrelated changes * Update: Update yarn.lock * Change: Re-throw error * Fix: Fix error * Fix: It throws string, not an Error * Fix tasks about resources copy --------- Co-authored-by: colin.weng --- build.gradle.kts | 7 ++- core/build.gradle.kts | 2 +- core/src/main/kotlin/Library.kt | 6 +- core/src/main/kotlin/core/io/Mid.kt | 44 +++++++-------- core/src/main/kotlin/core/io/StandardMid.kt | 61 +++++++++------------ core/src/main/kotlin/core/io/VsqLike.kt | 7 +-- core/src/main/kotlin/core/util/MidiUtil.kt | 17 ------ kotlin-js-store/yarn.lock | 8 +-- 8 files changed, 62 insertions(+), 90 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4efc360..1a95f89 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -84,8 +84,13 @@ val copyCoreResources by tasks.register("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) } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index aa40701..38b55d1 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -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")) } } diff --git a/core/src/main/kotlin/Library.kt b/core/src/main/kotlin/Library.kt index a0cd00a..7979227 100644 --- a/core/src/main/kotlin/Library.kt +++ b/core/src/main/kotlin/Library.kt @@ -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 @@ -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 = parse(listOf(file), Format.Vsqx) @@ -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( diff --git a/core/src/main/kotlin/core/io/Mid.kt b/core/src/main/kotlin/core/io/Mid.kt index 57a39dc..b1bbc0c 100644 --- a/core/src/main/kotlin/core/io/Mid.kt +++ b/core/src/main/kotlin/core/io/Mid.kt @@ -1,5 +1,6 @@ package core.io +import core.exception.IllegalFileException import core.model.ImportWarning import core.model.Project import core.model.Tempo @@ -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, measurePrefix: Int, warnings: MutableList, ): Triple, List, Long> { - val events = masterTrack.event as Array var tickPosition = 0 val tickCounter = TickCounter() val rawTempos = mutableListOf() val rawTimeSignatures = mutableListOf() 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( @@ -131,15 +125,15 @@ object Mid { return counter.tick } - fun extractVsqTextsFromMetaEvents(midiTracks: Array): List { + fun extractVsqTextsFromMetaEvents(midiTracks: Array>): List { return midiTracks.drop(1) .map { track -> - (track.event as Array) + 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) diff --git a/core/src/main/kotlin/core/io/StandardMid.kt b/core/src/main/kotlin/core/io/StandardMid.kt index dde01c9..d3fa348 100644 --- a/core/src/main/kotlin/core/io/StandardMid.kt +++ b/core/src/main/kotlin/core/io/StandardMid.kt @@ -1,6 +1,5 @@ package core.io -import core.exception.IllegalFileException import core.external.Encoding import core.model.DEFAULT_LYRIC import core.model.ExportResult @@ -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 + val timeDivision = midi.header.ticksPerBeat as Int + val midiTracks = midi.tracks as Array> val warnings = mutableListOf() val (tempos, timeSignatures, tickPrefix) = Mid.parseMasterTrack( @@ -59,7 +55,7 @@ object StandardMid { id: Int, timeDivision: Int, tickPrefix: Long, - midiTrack: dynamic, + events: Array, ): Track { var trackName = "Track ${id + 1}" val notes = mutableListOf() @@ -70,7 +66,6 @@ object StandardMid { val pendingNotesHeadsWithLyric = mutableMapOf() - val events = midiTrack.event as Array for (event in events) { val delta = MidiUtil.convertInputTimeToStandardTime(event.deltaTime as Int, timeDivision) if (delta > 0) { @@ -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( diff --git a/core/src/main/kotlin/core/io/VsqLike.kt b/core/src/main/kotlin/core/io/VsqLike.kt index eedb24d..a912bf9 100644 --- a/core/src/main/kotlin/core/io/VsqLike.kt +++ b/core/src/main/kotlin/core/io/VsqLike.kt @@ -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 val tracksAsText = Mid.extractVsqTextsFromMetaEvents(midiTracks).filter { it.isNotEmpty() } if (tracksAsText.isEmpty()) return false @@ -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 - val timeDivision = midi.timeDivision as Int + val timeDivision = midi.header.ticksPerBeat as Int + val midiTracks = midi.tracks as Array> val warnings = mutableListOf() val tracksAsText = Mid.extractVsqTextsFromMetaEvents(midiTracks).filter { it.isNotEmpty() } val measurePrefix = getMeasurePrefix(tracksAsText.first()) diff --git a/core/src/main/kotlin/core/util/MidiUtil.kt b/core/src/main/kotlin/core/util/MidiUtil.kt index d74f19d..2958c25 100644 --- a/core/src/main/kotlin/core/util/MidiUtil.kt +++ b/core/src/main/kotlin/core/util/MidiUtil.kt @@ -1,7 +1,5 @@ package core.util -import kotlin.math.pow - object MidiUtil { private const val StandardTimeDivision = 480 @@ -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) { @@ -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) = @@ -42,13 +32,6 @@ object MidiUtil { fun convertBpmToMidiTempo(bpm: Double) = (1000 * 1000 * 60 / bpm).toInt() - fun parseMidiTimeSignature(data: dynamic): Pair { - data as Array - val numerator = data[0] - val denominator = (2f.pow(data[1])).toInt() - return numerator to denominator - } - fun generateMidiTimeSignatureBytes(numerator: Int, denominator: Int): List { return listOf( numerator.toByte(), diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index d318db4..5e5515a 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2626,10 +2626,10 @@ micromatch@^4.0.2: braces "^3.0.2" picomatch "^2.3.1" -midi-parser-js@4.0.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-file@1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/midi-file/-/midi-file-1.2.4.tgz#e5803a8fc79cdd1692ac6ef6b1491043b397eb87" + integrity sha512-B5SnBC6i2bwJIXTY9MElIydJwAmnKx+r5eJ1jknTLetzLflEl0GWveuBB6ACrQpecSRkOB6fhTx1PwXk2BVxnA== mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0"