From 2f895bf195a5041fc533b52cff3c0ea9e3ea7414 Mon Sep 17 00:00:00 2001 From: Nanashi Date: Mon, 24 Jun 2024 21:53:57 +0900 Subject: [PATCH] Add: Support tssln (#172) * Add: Support importing tssln * Add: Add basic export * Code: gradlew ktlintFormat * Change: Use yarn --- .npmrc | 1 + core/.npmrc | 1 + core/build.gradle.kts | 3 + .../core/exception/IllegalFileException.kt | 2 + core/src/main/kotlin/core/external/Common.kt | 2 + .../main/kotlin/core/external/Resources.kt | 3 + .../main/kotlin/core/external/ValueTree.kt | 65 ++++ core/src/main/kotlin/core/io/Tssln.kt | 289 ++++++++++++++++++ core/src/main/kotlin/core/model/Format.kt | 13 + .../format_templates/template.tssln.json | 1 + kotlin-js-store/yarn.lock | 12 + src/jsMain/kotlin/ui/OutputFormatSelector.kt | 3 + src/jsMain/kotlin/ui/Resources.kt | 3 + src/jsMain/kotlin/ui/strings/Strings.kt | 7 + src/jsMain/resources/images/voisona.png | Bin 0 -> 16907 bytes 15 files changed, 405 insertions(+) create mode 100644 .npmrc create mode 100644 core/.npmrc create mode 100644 core/src/main/kotlin/core/external/ValueTree.kt create mode 100644 core/src/main/kotlin/core/io/Tssln.kt create mode 100644 core/src/main/resources/format_templates/template.tssln.json create mode 100644 src/jsMain/resources/images/voisona.png diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..41583e36 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/core/.npmrc b/core/.npmrc new file mode 100644 index 00000000..41583e36 --- /dev/null +++ b/core/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 38b55d13..63c1b944 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -24,6 +24,9 @@ kotlin { implementation(npm("uuid", "8.3.2")) implementation(npm("midi-file", "1.2.4")) implementation(npm("js-yaml", "4.1.0")) + implementation( + npm("@sevenc-nanashi/valuetree-ts", "npm:@jsr/sevenc-nanashi__valuetree-ts@0.2.0"), + ) } } } diff --git a/core/src/main/kotlin/core/exception/IllegalFileException.kt b/core/src/main/kotlin/core/exception/IllegalFileException.kt index 5d8c4881..356f46db 100644 --- a/core/src/main/kotlin/core/exception/IllegalFileException.kt +++ b/core/src/main/kotlin/core/exception/IllegalFileException.kt @@ -17,4 +17,6 @@ sealed class IllegalFileException(message: String) : Throwable(message) { ) class IllegalMidiFile : IllegalFileException("Cannot parse this file as a MIDI file.") + + class IllegalTsslnFile : IllegalFileException("Cannot parse this file as a tssln file.") } diff --git a/core/src/main/kotlin/core/external/Common.kt b/core/src/main/kotlin/core/external/Common.kt index 5d1515be..d6153954 100644 --- a/core/src/main/kotlin/core/external/Common.kt +++ b/core/src/main/kotlin/core/external/Common.kt @@ -1,3 +1,5 @@ package core.external external fun require(module: String): dynamic + +external fun structuredClone(obj: T): T diff --git a/core/src/main/kotlin/core/external/Resources.kt b/core/src/main/kotlin/core/external/Resources.kt index 0e49107c..18c41666 100644 --- a/core/src/main/kotlin/core/external/Resources.kt +++ b/core/src/main/kotlin/core/external/Resources.kt @@ -13,6 +13,9 @@ object Resources { val musicXmlTemplate: String get() = require("./format_templates/template.musicxml").default as String + val tsslnTemplate: Array + get() = + require("./format_templates/template.tssln.json") as Array val svpTemplate: String get() = require("./format_templates/template.svp").default as String diff --git a/core/src/main/kotlin/core/external/ValueTree.kt b/core/src/main/kotlin/core/external/ValueTree.kt new file mode 100644 index 00000000..62c18c78 --- /dev/null +++ b/core/src/main/kotlin/core/external/ValueTree.kt @@ -0,0 +1,65 @@ +package core.external + +import org.khronos.webgl.Uint8Array + +fun createValueTree(): ValueTree { + return js("{type: '', attributes: {}, children: []}").unsafeCast() +} + +fun baseVariantType(): dynamic { + return js("({type: '', value: undefined})") +} + +fun String.toVariantType(): dynamic { + val value = baseVariantType() + value.type = "string" + value.value = this + + return value +} +fun Int.toVariantType(): dynamic { + val value = baseVariantType() + value.type = "int" + value.value = this + + return value +} +fun Double.toVariantType(): dynamic { + val value = baseVariantType() + value.type = "double" + value.value = this + + return value +} +fun Boolean.toVariantType(): dynamic { + val value = baseVariantType() + if (this) { + value.type = "boolTrue" + value.value = true + } else { + value.type = "boolFalse" + value.value = false + } + + return value +} +fun Uint8Array.toVariantType(): dynamic { + val value = baseVariantType() + value.type = "binary" + value.value = this + + return value +} + +external interface ValueTree { + var type: String + var attributes: dynamic + var children: Array +} + +@JsModule("@sevenc-nanashi/valuetree-ts") +@JsNonModule +external object ValueTreeTs { + fun parseValueTree(text: Uint8Array): ValueTree + fun dumpValueTree(tree: ValueTree): Uint8Array +} diff --git a/core/src/main/kotlin/core/io/Tssln.kt b/core/src/main/kotlin/core/io/Tssln.kt new file mode 100644 index 00000000..9855d226 --- /dev/null +++ b/core/src/main/kotlin/core/io/Tssln.kt @@ -0,0 +1,289 @@ +package core.io + +import core.exception.IllegalFileException +import core.external.Resources +import core.external.ValueTree +import core.external.ValueTreeTs +import core.external.createValueTree +import core.external.structuredClone +import core.external.toVariantType +import core.model.ExportResult +import core.model.Format +import core.model.ImportParams +import core.model.ImportWarning +import core.model.Note +import core.model.Project +import core.model.TICKS_IN_BEAT +import core.model.Tempo +import core.model.TimeSignature +import core.model.Track +import core.util.nameWithoutExtension +import core.util.readAsArrayBuffer +import org.khronos.webgl.Uint8Array +import org.w3c.files.Blob +import org.w3c.files.File +import kotlin.math.floor + +object Tssln { + suspend fun parse(file: File, params: ImportParams): Project { + val blob = file.readAsArrayBuffer() + val valueTree = ValueTreeTs.parseValueTree( + Uint8Array(blob), + ) + + if (valueTree.type != "TSSolution") { + throw IllegalFileException.IllegalTsslnFile() + } + + val trackTrees = + valueTree.children.first { it.type == "Tracks" }.children.filter { it.attributes.Type.value == 0 } + + val masterTrackResult = parseMasterTrack(trackTrees.first()) + val tempos = masterTrackResult.first + val timeSignatures = masterTrackResult.second + + val tracks = parseTracks(trackTrees, params) + + val warnings = mutableListOf() + + return Project( + format = format, + inputFiles = listOf(file), + name = file.nameWithoutExtension, + tracks = tracks, + tempos = tempos, + timeSignatures = timeSignatures, + measurePrefix = 1, + importWarnings = warnings, + ) + } + + private fun parseTracks(trackTrees: List, params: ImportParams): List { + return trackTrees.mapIndexed { trackIndex, trackTree -> + val trackName = trackTree.attributes.Name.value as String + val pluginData = trackTree.attributes.PluginData.value as Uint8Array + val pluginDataTree = ValueTreeTs.parseValueTree(pluginData) + + if (pluginDataTree.type != "StateInformation") { + throw IllegalFileException.IllegalTsslnFile() + } + + val songTree = pluginDataTree.children.first { it.type == "Song" } + val scoreTree = songTree.children.first { it.type == "Score" } + + val notes = mutableListOf() + + for ((noteIndex, noteTree) in scoreTree.children.withIndex()) { + if (noteTree.type != "Note") { + continue + } + + val pitchStep = noteTree.attributes.PitchStep.value as Int + val pitchOctave = noteTree.attributes.PitchOctave.value as Int + val rawLyric = (noteTree.attributes.Lyric.value as String) + val lyric = phonemePartPattern.replace(rawLyric, "").takeUnless { it.isBlank() } ?: params.defaultLyric + + val phoneme = (noteTree.attributes.Phoneme.value as String).replace(",", "") + + val tickOn = (noteTree.attributes.Clock.value as Int) + val tickOff = tickOn + (noteTree.attributes.Duration.value as Int) + + notes.add( + Note( + id = noteIndex, + key = pitchOctave * 12 + pitchStep + 12, + lyric = lyric, + phoneme = phoneme, + tickOn = (tickOn / TICK_RATE).toLong(), + tickOff = (tickOff / TICK_RATE).toLong(), + ), + ) + } + + Track( + id = trackIndex, + name = trackName, + notes = notes, + ) + } + } + + private fun parseMasterTrack(trackTree: ValueTree): Pair, List> { + val pluginData = trackTree.attributes.PluginData.value as Uint8Array + val pluginDataTree = ValueTreeTs.parseValueTree(pluginData) + if (pluginDataTree.type != "StateInformation") { + throw IllegalFileException.IllegalTsslnFile() + } + + val songTree = pluginDataTree.children.first { it.type == "Song" } + + val tempoTree = songTree.children.first { it.type == "Tempo" } + + val tempos = tempoTree.children.map { + Tempo( + ((it.attributes.Clock.value as Int) / TICK_RATE).toLong(), + it.attributes.Tempo.value as Double, + ) + } + + val timeSignaturesTree = songTree.children.first { it.type == "Beat" } + + val timeSignatures = mutableListOf() + + var currentBeatIndex = 0 + var currentMeasureIndex = 0 + var beatLength = 4.0 + + for ( + timeSignatureTree in timeSignaturesTree.children.sortedBy { + it.attributes.Clock.value as Int + } + ) { + val numerator = timeSignatureTree.attributes.Beats.value as Int + val denominator = timeSignatureTree.attributes.BeatType.value as Int + val clock = timeSignatureTree.attributes.Clock.value as Int + val beatIndex = floor(clock / TICK_RATE / TICKS_IN_BEAT).toInt() + + val beatNum = beatIndex - currentBeatIndex + + if (beatNum < 0) { + throw IllegalFileException.IllegalTsslnFile() + } + + val measureIndex = (currentMeasureIndex + beatNum / beatLength).toInt() + beatLength = numerator.toDouble() / denominator * 4 + + currentBeatIndex = beatIndex + currentMeasureIndex = measureIndex + + timeSignatures.add( + TimeSignature( + measureIndex, + numerator, + denominator, + ), + ) + } + + return Pair(tempos, timeSignatures) + } + + fun generate(project: Project): ExportResult { + val baseJson = Resources.tsslnTemplate + + val baseTree = ValueTreeTs.parseValueTree(Uint8Array(baseJson)) + + val tracksTree = baseTree.children.first { it.type == "Tracks" } + val baseTrack = tracksTree.children.first { it.attributes.Type.value == 0 } + + val tracks = generateTracks(baseTrack, project) + tracksTree.children = tracks.toTypedArray() + + val result = ValueTreeTs.dumpValueTree(baseTree) + + return ExportResult( + Blob(arrayOf(result)), + project.name + ".tssln", + listOf(), + ) + } + + private fun generateTracks(baseTrack: ValueTree, project: Project): List { + return project.tracks.map { track -> + val trackTree = structuredClone(baseTrack) + trackTree.attributes.Name = (track.name).toVariantType() + + val pluginData = trackTree.attributes.PluginData.value as Uint8Array + val pluginDataTree = ValueTreeTs.parseValueTree(pluginData) + + val songTree = pluginDataTree.children.first { it.type == "Song" } + + val tempoTree = songTree.children.first { it.type == "Tempo" } + val timeSignaturesTree = songTree.children.first { it.type == "Beat" } + + tempoTree.children = (generateTempos(project.tempos)).toTypedArray() + timeSignaturesTree.children = (generateTimeSignatures(project.timeSignatures)).toTypedArray() + + val scoreTree = songTree.children.first { it.type == "Score" } + val notes = generateNotes(track) + val baseChildren = structuredClone(scoreTree.children) + + val newChildren = baseChildren.toMutableList() + newChildren.addAll(notes) + + scoreTree.children = newChildren.toTypedArray() + + trackTree.attributes.PluginData = (ValueTreeTs.dumpValueTree(pluginDataTree)).toVariantType() + + trackTree + } + } + + private fun generateTempos(tempos: List): List { + return tempos.map { + val tempoTree = createValueTree() + tempoTree.type = "Sound" + val attributes: dynamic = js("{}") + attributes.Clock = ((it.tickPosition * TICK_RATE).toInt()).toVariantType() + attributes.Tempo = (it.bpm).toVariantType() + tempoTree.attributes = attributes + + tempoTree + } + } + + private fun generateTimeSignatures(timeSignatures: List): List { + val timeSignatureTrees = mutableListOf() + + var currentBeat = 0 + var currentMeasure = 0 + var beatLength = 4.0 + + for (timeSignature in timeSignatures) { + val timeSignatureTree = createValueTree() + timeSignatureTree.type = "Time" + val attributes: dynamic = js("{}") + + val numMeasure = timeSignature.measurePosition - currentMeasure + val numBeat = numMeasure * beatLength + currentBeat += numBeat.toInt() + + beatLength = timeSignature.numerator.toDouble() / timeSignature.denominator * 4 + currentMeasure = timeSignature.measurePosition + + attributes.Clock = (currentBeat * TICKS_IN_BEAT * TICK_RATE).toInt().toVariantType() + attributes.Beats = timeSignature.numerator.toVariantType() + attributes.BeatType = timeSignature.denominator.toVariantType() + + timeSignatureTree.attributes = attributes + + timeSignatureTrees.add(timeSignatureTree) + } + + return timeSignatureTrees + } + + private fun generateNotes(track: Track): List { + return track.notes.map { + val noteTree = createValueTree() + noteTree.type = "Note" + val attributes: dynamic = js("{}") + attributes.PitchStep = (it.key % 12).toVariantType() + attributes.PitchOctave = (it.key / 12 - 1).toVariantType() + attributes.Lyric = it.lyric.toVariantType() + attributes.Phoneme = (it.phoneme ?: "").toVariantType() + attributes.Clock = (it.tickOn * TICK_RATE).toInt().toVariantType() + attributes.Syllabic = 0.toVariantType() + attributes.Duration = ((it.tickOff - it.tickOn) * TICK_RATE).toInt().toVariantType() + noteTree.attributes = attributes + + noteTree + } + } + + private const val TICK_RATE = 2.0 + + private val phonemePartPattern = Regex("""\[.+?]""") + + private val format = Format.Tssln +} diff --git a/core/src/main/kotlin/core/model/Format.kt b/core/src/main/kotlin/core/model/Format.kt index 47606b31..118c6948 100644 --- a/core/src/main/kotlin/core/model/Format.kt +++ b/core/src/main/kotlin/core/model/Format.kt @@ -173,6 +173,17 @@ enum class Format( }, possibleLyricsTypes = listOf(RomajiCv, KanaCv), ), + Tssln( + "tssln", + parser = { files, params -> + core.io.Tssln.parse(files.first(), params) + }, + generator = { project, _ -> + core.io.Tssln.generate(project) + }, + possibleLyricsTypes = listOf(KanaCv, RomajiCv), + availableFeaturesForGeneration = listOf(ConvertPhonemes), + ), UfData( "ufdata", parser = { files, params -> @@ -217,6 +228,7 @@ enum class Format( Dv, Ppsf, StandardMid, + Tssln, UfData, ) @@ -234,6 +246,7 @@ enum class Format( S5p, Dv, StandardMid, + Tssln, UfData, ) diff --git a/core/src/main/resources/format_templates/template.tssln.json b/core/src/main/resources/format_templates/template.tssln.json new file mode 100644 index 00000000..31fd146f --- /dev/null +++ b/core/src/main/resources/format_templates/template.tssln.json @@ -0,0 +1 @@ +[84,83,83,111,108,117,116,105,111,110,0,1,1,86,101,114,115,105,111,110,79,102,65,112,112,70,105,108,101,83,97,118,101,100,0,1,10,5,49,46,49,48,46,49,46,48,0,1,3,80,108,97,121,67,111,110,116,114,111,108,0,1,4,76,111,111,112,0,1,1,3,76,111,111,112,83,116,97,114,116,0,1,9,4,0,0,0,0,0,0,0,0,76,111,111,112,69,110,100,0,1,9,4,0,0,0,0,0,0,0,0,80,108,97,121,80,111,115,105,116,105,111,110,0,1,9,4,0,0,0,0,0,0,0,0,0,84,114,97,99,107,115,0,0,1,1,84,114,97,99,107,0,1,6,84,121,112,101,0,1,5,1,0,0,0,0,78,97,109,101,0,1,9,5,83,105,110,103,101,114,49,0,83,116,97,116,101,0,1,5,1,0,0,0,0,86,111,108,117,109,101,0,1,9,4,0,0,0,0,0,0,0,0,80,97,110,0,1,9,4,0,0,0,0,0,0,0,0,80,108,117,103,105,110,68,97,116,97,0,2,50,4,8,83,116,97,116,101,73,110,102,111,114,109,97,116,105,111,110,0,1,1,86,101,114,115,105,111,110,79,102,65,112,112,70,105,108,101,83,97,118,101,100,0,1,10,5,49,46,49,48,46,49,46,48,0,1,10,83,111,110,103,69,100,105,116,111,114,0,1,2,69,100,105,116,111,114,87,105,100,116,104,0,1,5,1,14,5,0,0,69,100,105,116,111,114,72,101,105,103,104,116,0,1,5,1,232,1,0,0,0,67,111,110,116,114,111,108,80,97,110,101,108,83,116,97,116,117,115,0,1,4,81,117,97,110,116,105,122,97,116,105,111,110,0,1,5,1,8,0,0,0,82,101,99,111,114,100,78,111,116,101,0,1,1,2,82,101,99,111,114,100,84,101,109,112,111,0,1,1,2,69,100,105,116,84,111,111,108,0,1,5,1,2,0,0,0,0,65,100,106,117,115,116,84,111,111,108,66,97,114,83,116,97,116,117,115,0,1,2,77,97,105,110,80,97,110,101,108,0,1,5,1,255,255,255,255,83,117,98,80,97,110,101,108,0,1,5,1,1,0,0,0,0,77,97,105,110,80,97,110,101,108,83,116,97,116,117,115,0,1,4,83,99,97,108,101,88,95,86,50,0,1,9,4,1,0,0,0,0,0,20,64,83,99,97,108,101,89,0,1,9,4,0,0,0,0,0,0,224,63,83,99,114,111,108,108,88,0,1,9,4,0,0,0,0,0,0,128,61,83,99,114,111,108,108,89,0,1,9,4,42,70,13,127,155,98,180,191,0,80,97,110,101,108,67,111,110,116,114,111,108,108,101,114,83,116,97,116,117,115,0,1,4,84,101,109,112,111,80,97,110,101,108,0,1,1,2,66,101,97,116,80,97,110,101,108,0,1,1,3,75,101,121,80,97,110,101,108,0,1,1,2,68,121,110,97,109,105,99,115,80,97,110,101,108,0,1,1,2,0,86,111,105,99,101,73,110,102,111,114,109,97,116,105,111,110,0,1,5,67,104,97,114,97,99,116,101,114,78,97,109,101,0,1,8,5,67,104,105,115,45,65,0,86,111,105,99,101,70,105,108,101,78,97,109,101,0,1,36,5,110,105,116,101,99,104,45,106,112,95,106,97,95,74,80,95,102,48,48,56,95,115,118,115,115,46,116,115,110,118,111,105,99,101,0,76,97,110,103,117,97,103,101,0,1,7,5,106,97,95,74,80,0,86,111,105,99,101,86,101,114,115,105,111,110,0,1,7,5,50,46,48,46,48,0,65,99,116,105,118,101,65,102,116,101,114,84,104,105,115,86,101,114,115,105,111,110,0,1,9,5,49,46,55,46,49,46,50,0,1,2,69,109,111,116,105,111,110,76,105,115,116,0,0,1,1,69,109,111,116,105,111,110,0,1,2,76,97,98,101,108,0,1,8,5,78,111,114,109,97,108,0,82,97,116,105,111,0,1,9,4,0,0,0,0,0,0,240,63,0,78,101,117,114,97,108,86,111,99,111,100,101,114,76,105,115,116,0,0,0,71,108,111,98,97,108,80,97,114,97,109,101,116,101,114,115,0,1,5,71,108,111,98,97,108,84,117,110,101,0,1,9,4,0,0,0,0,0,0,0,0,71,108,111,98,97,108,86,105,98,65,109,112,0,1,9,4,0,0,0,0,0,0,240,63,71,108,111,98,97,108,86,105,98,70,114,113,0,1,9,4,0,0,0,0,0,0,240,63,71,108,111,98,97,108,65,108,112,104,97,0,1,9,4,0,0,0,0,0,0,0,0,71,108,111,98,97,108,72,117,115,107,121,0,1,9,4,0,0,0,0,0,0,0,0,0,83,111,110,103,0,0,1,3,84,101,109,112,111,0,0,1,1,83,111,117,110,100,0,1,2,67,108,111,99,107,0,1,5,1,0,0,0,0,84,101,109,112,111,0,1,9,4,0,0,0,0,0,0,94,64,0,66,101,97,116,0,0,1,1,84,105,109,101,0,1,3,67,108,111,99,107,0,1,5,1,0,0,0,0,66,101,97,116,115,0,1,5,1,4,0,0,0,66,101,97,116,84,121,112,101,0,1,5,1,4,0,0,0,0,83,99,111,114,101,0,0,1,2,75,101,121,0,1,3,67,108,111,99,107,0,1,5,1,0,0,0,0,70,105,102,116,104,115,0,1,5,1,0,0,0,0,77,111,100,101,0,1,5,1,0,0,0,0,0,68,121,110,97,109,105,99,115,0,1,2,67,108,111,99,107,0,1,5,1,0,0,0,0,86,97,108,117,101,0,1,5,1,5,0,0,0,0,80,97,114,97,109,101,116,101,114,0,1,1,86,111,99,111,100,101,114,76,111,103,70,48,0,1,9,4,0,0,0,0,0,0,0,0,0,83,105,103,110,101,114,67,111,110,102,105,103,0,0,0,0,71,85,73,83,116,97,116,117,115,0,1,3,83,99,97,108,101,88,0,1,9,4,0,0,0,0,0,0,89,64,83,99,97,108,101,89,0,1,9,4,1,0,0,0,0,0,224,63,71,114,105,100,77,111,100,101,0,1,5,1,1,0,0,0,0] \ No newline at end of file diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 5e5515af..6c7b4b0c 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -280,6 +280,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jsr/sevenc-nanashi__binaryseeker@^1.0.0": + version "1.0.0" + resolved "https://npm.jsr.io/~/11/@jsr/sevenc-nanashi__binaryseeker/1.0.0.tgz#4C4A478A37281DB5CF13018A6DAB5532C34C1995" + integrity sha512-aaVVAuCXw4YwZVeqOd4IzqvzVD8iA4ODl0Ta31m87sjGm3mQxo5/Whg+LDFcFekUUve78JKXgjWSX1DckwbO0Q== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -467,6 +472,13 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@sevenc-nanashi/valuetree-ts@npm:@jsr/sevenc-nanashi__valuetree-ts@0.2.0": + version "0.2.0" + resolved "https://npm.jsr.io/~/11/@jsr/sevenc-nanashi__valuetree-ts/0.2.0.tgz#C54B3DEB79C6EDB635DFF2D1B9E96CA29D513BA8" + integrity sha512-dKhOyTjwUxAlzOzynXhrk41Jb+dYkO/GhffXQoeo5UW/Vh62JfA6N0QKmoy004HJ1sv5SMsMD5Toqqrg9+yRKg== + dependencies: + "@jsr/sevenc-nanashi__binaryseeker" "^1.0.0" + "@socket.io/component-emitter@~3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" diff --git a/src/jsMain/kotlin/ui/OutputFormatSelector.kt b/src/jsMain/kotlin/ui/OutputFormatSelector.kt index 99509790..98aeb50c 100644 --- a/src/jsMain/kotlin/ui/OutputFormatSelector.kt +++ b/src/jsMain/kotlin/ui/OutputFormatSelector.kt @@ -8,6 +8,7 @@ import core.model.Format.Ppsf import core.model.Format.S5p import core.model.Format.StandardMid import core.model.Format.Svp +import core.model.Format.Tssln import core.model.Format.UfData import core.model.Format.Ust import core.model.Format.Ustx @@ -173,6 +174,7 @@ private val Format.description: String? Ppsf -> null StandardMid -> Strings.StandardMidDescription UfData -> Strings.UfDataFormatDescription + Tssln -> Strings.VoiSonaFormatDescription }?.let { string(it) } private val Format.iconPath: String? @@ -191,6 +193,7 @@ private val Format.iconPath: String? Ppsf -> null StandardMid -> Resources.standardMidiIcon UfData -> Resources.ufdataIcon + Tssln -> Resources.tsslnIcon } external interface OutputFormatSelectorProps : Props { diff --git a/src/jsMain/kotlin/ui/Resources.kt b/src/jsMain/kotlin/ui/Resources.kt index efb538ac..9ddd39ac 100644 --- a/src/jsMain/kotlin/ui/Resources.kt +++ b/src/jsMain/kotlin/ui/Resources.kt @@ -37,4 +37,7 @@ object Resources { val ufdataIcon: String get() = core.external.require("./images/ufdata.png").default as String + + val tsslnIcon: String + get() = core.external.require("./images/voisona.png").default as String } diff --git a/src/jsMain/kotlin/ui/strings/Strings.kt b/src/jsMain/kotlin/ui/strings/Strings.kt index fffbda55..ff0a85f3 100644 --- a/src/jsMain/kotlin/ui/strings/Strings.kt +++ b/src/jsMain/kotlin/ui/strings/Strings.kt @@ -725,6 +725,13 @@ enum class Strings( ru = "Стандартный MIDI-файл", fr = "Fichier MIDI standard", ), + VoiSonaFormatDescription( + en = "Project for VoiSona", + ja = "VoiSona プロジェクト", + zhCN = "VoiSona 工程", + ru = "Проект для VoiSona", + fr = "Projet pour VoiSona", + ), ExportButton( en = "Export", ja = "エクスポート", diff --git a/src/jsMain/resources/images/voisona.png b/src/jsMain/resources/images/voisona.png new file mode 100644 index 0000000000000000000000000000000000000000..bcadc2ec38134da4a9e3199e0227062cdd7c2e8d GIT binary patch literal 16907 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 literal 0 HcmV?d00001