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

Add: Support tssln #172

Merged
merged 4 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io
1 change: 1 addition & 0 deletions core/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io
3 changes: 3 additions & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]"),
)
}
}
}
2 changes: 2 additions & 0 deletions core/src/main/kotlin/core/exception/IllegalFileException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
}
19 changes: 19 additions & 0 deletions core/src/main/kotlin/core/external/ValueTree.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package core.external

import org.khronos.webgl.Uint8Array

external class ValueTree {
var type: String
var attributes: dynamic
var children: Array<ValueTree>
}

@JsModule("@sevenc-nanashi/valuetree-ts")
@JsNonModule
external object ValueTreeTs {
fun parseValueTree(text: Uint8Array): ValueTree
fun dumpValueTree(tree: ValueTree): Uint8Array
}



172 changes: 172 additions & 0 deletions core/src/main/kotlin/core/io/Tssln.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package core.io

import core.exception.IllegalFileException
import core.external.ValueTree
import core.model.ExportResult
import core.model.FeatureConfig
import core.model.ImportParams
import core.util.readAsArrayBuffer
import org.w3c.files.File
import core.external.ValueTreeTs
import core.model.Format
import core.model.ImportWarning
import core.model.Note
import core.model.TICKS_IN_BEAT
import core.model.Tempo
import core.model.TimeSignature
import core.model.Track
import org.khronos.webgl.Uint8Array
import core.util.nameWithoutExtension
import kotlin.math.floor

object Tssln {
suspend fun parse(file: File, params: ImportParams): core.model.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 == 0 }

val masterTrackResult = parseMasterTrack(trackTrees.first())


val tracks = parseTracks(trackTrees, params)


val warnings = mutableListOf<ImportWarning>()

return core.model.Project(
format = format,
inputFiles = listOf(file),
name = file.nameWithoutExtension,
tracks = tracks,
tempos = masterTrackResult.first,
timeSignatures = masterTrackResult.second,
measurePrefix = 1,
importWarnings = warnings,
)
}

private fun parseTracks(trackTrees: List<ValueTree>, params: ImportParams): List<core.model.Track> {
return trackTrees.mapIndexed { trackIndex, trackTree ->
val trackName = trackTree.attributes.Name as String
val pluginData = trackTree.attributes.PluginData as Uint8Array
val pluginDataNode = ValueTreeTs.parseValueTree(pluginData)

if (pluginDataNode.type != "StateInformation") {
throw IllegalFileException.IllegalTsslnFile()
}

val songNode = pluginDataNode.children.first { it.type == "Song" }
val scoreNode = songNode.children.first { it.type == "Score" }

val notes = mutableListOf<Note>()

for ((noteIndex, noteNode) in scoreNode.children.withIndex()) {
if (noteNode.type != "Note") {
continue
}

val pitchStep = noteNode.attributes.PitchStep as Int
val pitchOctave = noteNode.attributes.PitchOctave as Int
val rawLyric = noteNode.attributes.Lyric as String
val lyric = phonemePartPattern.replace(rawLyric, "")

val phoneme = (noteNode.attributes.Phoneme as String).replace(",", "")

val tickOn = (noteNode.attributes.Clock as Int)
val tickOff = tickOn + (noteNode.attributes.Duration 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,
)
}
}

suspend fun generate(project: core.model.Project, features: List<FeatureConfig>): ExportResult {
throw NotImplementedError("Not implemented")
}

private fun parseMasterTrack(trackTree: ValueTree): Pair<List<Tempo>, List<TimeSignature>> {
val pluginData = trackTree.attributes.PluginData as Uint8Array
val pluginDataNode = ValueTreeTs.parseValueTree(pluginData)
if (pluginDataNode.type != "StateInformation") {
throw IllegalFileException.IllegalTsslnFile()
}

val songNode = pluginDataNode.children.first { it.type == "Song" }

val tempoNode = songNode.children.first { it.type == "Tempo" }

val tempos = tempoNode.children.map {
Tempo(
((it.attributes.Clock as Int) / TICK_RATE).toLong(),
it.attributes.Tempo as Double,
)
}

val timeSignaturesNode = songNode.children.first { it.type == "Beat" }

val timeSignatures = mutableListOf<TimeSignature>()

var currentBeatIndex = 0
var currentMeasureIndex = 0

for (timeSignatureNode in timeSignaturesNode.children.sortedBy {
it.attributes.Clock as Int
}) {
val numerator = timeSignatureNode.attributes.Beats as Int
val denominator = timeSignatureNode.attributes.BeatType as Int
val clock = timeSignatureNode.attributes.Clock as Int
val beatIndex = floor(clock / TICK_RATE / TICKS_IN_BEAT).toInt()

val beatNum = beatIndex - currentBeatIndex
val beatLength = numerator / denominator * 4

if (beatNum < 0) {
throw IllegalFileException.IllegalTsslnFile()
}

val measureIndex = currentMeasureIndex + beatNum / beatLength

currentBeatIndex = beatIndex
currentMeasureIndex = measureIndex

timeSignatures.add(
TimeSignature(
measureIndex,
numerator,
denominator,
),
)
}

return Pair(tempos, timeSignatures)
}

private val TICK_RATE = 2.0

private val phonemePartPattern = Regex("""\[.+?\]""")

private val format = Format.Tssln
}
13 changes: 13 additions & 0 deletions core/src/main/kotlin/core/model/Format.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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, features ->
core.io.Tssln.generate(project, features)
},
possibleLyricsTypes = listOf(KanaCv, RomajiCv),
availableFeaturesForGeneration = listOf(ConvertPitch, ConvertPhonemes),
),
UfData(
"ufdata",
parser = { files, params ->
Expand Down Expand Up @@ -217,6 +228,7 @@ enum class Format(
Dv,
Ppsf,
StandardMid,
Tssln,
UfData,
)

Expand All @@ -234,6 +246,7 @@ enum class Format(
S5p,
Dv,
StandardMid,
Tssln,
UfData,
)

Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
kotlin.code.style=official
kotlin.js.compiler=ir
kotlin.daemon.jvmargs=-Xmx4G
kotlin.js.yarn=false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to quit yarn to use jsr packages (Kotlin/JS does not recognize .npmrc with yarn)

Copy link
Owner

Choose a reason for hiding this comment

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

Okay let's just use npm. I'm always uncomfortable with the yarn.lock file as well 😂
Can we remove those yarn-specific files?

Copy link
Contributor Author

@sevenc-nanashi sevenc-nanashi Jun 23, 2024

Choose a reason for hiding this comment

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

I deleted yarn.lock but it spawns again somehow...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Somehow it started to work, so I'm reverting this change.

12 changes: 12 additions & 0 deletions kotlin-js-store/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -467,6 +472,13 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"

"@sevenc-nanashi/valuetree-ts@npm:@jsr/[email protected]":
version "0.1.2"
resolved "https://npm.jsr.io/~/11/@jsr/sevenc-nanashi__valuetree-ts/0.1.2.tgz#AFACC4F569FB0BD59D55A1EC5E45C30324F6F0A9"
integrity sha512-PacPrHHNWb3V5ao1YvrWZ/xzggMTYEwyzTfDbM4cnHN5gorcKuH8iEgz2NrL7WQuelH2QbTQkH5HyB4qHpR40Q==
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"
Expand Down
3 changes: 3 additions & 0 deletions src/jsMain/kotlin/ui/OutputFormatSelector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import core.model.Format.VocaloidMid
import core.model.Format.Vpr
import core.model.Format.Vsq
import core.model.Format.Vsqx
import core.model.Format.Tssln
import core.model.ImportWarning
import core.model.Project
import csstype.FontWeight
Expand Down Expand Up @@ -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?
Expand All @@ -191,6 +193,7 @@ private val Format.iconPath: String?
Ppsf -> null
StandardMid -> Resources.standardMidiIcon
UfData -> Resources.ufdataIcon
Tssln -> Resources.tsslnIcon
}

external interface OutputFormatSelectorProps : Props {
Expand Down
3 changes: 3 additions & 0 deletions src/jsMain/kotlin/ui/Resources.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
7 changes: 7 additions & 0 deletions src/jsMain/kotlin/ui/strings/Strings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "エクスポート",
Expand Down
Loading