Skip to content

Commit

Permalink
Add: Add basic export
Browse files Browse the repository at this point in the history
  • Loading branch information
sevenc-nanashi committed Jun 23, 2024
1 parent 8bc4b4a commit 3b50731
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 48 deletions.
2 changes: 1 addition & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ kotlin {
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.1.2"),
npm("@sevenc-nanashi/valuetree-ts", "npm:@jsr/sevenc-nanashi__valuetree-ts@0.2.0"),
)
}
}
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/kotlin/core/external/Common.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package core.external

external fun require(module: String): dynamic

external fun <T>structuredClone(obj: T): T
3 changes: 3 additions & 0 deletions core/src/main/kotlin/core/external/Resources.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ object Resources {
val musicXmlTemplate: String
get() = require("./format_templates/template.musicxml").default as String

val tsslnTemplate: Array<Byte>
get() =
require("./format_templates/template.tssln.json") as Array<Byte>
val svpTemplate: String
get() = require("./format_templates/template.svp").default as String

Expand Down
51 changes: 50 additions & 1 deletion core/src/main/kotlin/core/external/ValueTree.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,56 @@ package core.external

import org.khronos.webgl.Uint8Array

external class ValueTree {
fun createValueTree(): ValueTree {
return js("{type: '', attributes: {}, children: []}").unsafeCast<ValueTree>()
}

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<ValueTree>
Expand Down
202 changes: 160 additions & 42 deletions core/src/main/kotlin/core/io/Tssln.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
package core.io

import core.exception.IllegalFileException
import core.external.Resources
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.external.createValueTree
import core.external.structuredClone
import core.external.toVariantType
import core.model.Format
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 org.khronos.webgl.Uint8Array
import core.util.nameWithoutExtension
import org.w3c.files.Blob
import kotlin.math.floor

object Tssln {
suspend fun parse(file: File, params: ImportParams): core.model.Project {
suspend fun parse(file: File, params: ImportParams): Project {
val blob = file.readAsArrayBuffer()
val valueTree = ValueTreeTs.parseValueTree(
Uint8Array(blob),
Expand All @@ -30,57 +36,58 @@ object Tssln {
throw IllegalFileException.IllegalTsslnFile()
}

val trackTrees = valueTree.children.first { it.type == "Tracks" }.children.filter { it.attributes.Type == 0 }
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<ImportWarning>()

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

private fun parseTracks(trackTrees: List<ValueTree>, params: ImportParams): List<core.model.Track> {
private fun parseTracks(trackTrees: List<ValueTree>, params: ImportParams): List<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)
val trackName = trackTree.attributes.Name.value as String
val pluginData = trackTree.attributes.PluginData.value as Uint8Array
val pluginDataTree = ValueTreeTs.parseValueTree(pluginData)

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

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

val notes = mutableListOf<Note>()

for ((noteIndex, noteNode) in scoreNode.children.withIndex()) {
if (noteNode.type != "Note") {
for ((noteIndex, noteTree) in scoreTree.children.withIndex()) {
if (noteTree.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 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, "")

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

val tickOn = (noteNode.attributes.Clock as Int)
val tickOff = tickOn + (noteNode.attributes.Duration as Int)
val tickOn = (noteTree.attributes.Clock.value as Int)
val tickOff = tickOn + (noteTree.attributes.Duration.value as Int)

notes.add(
Note(
Expand All @@ -103,51 +110,48 @@ object Tssln {
}
}

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") {
val pluginData = trackTree.attributes.PluginData.value as Uint8Array
val pluginDataTree = ValueTreeTs.parseValueTree(pluginData)
if (pluginDataTree.type != "StateInformation") {
throw IllegalFileException.IllegalTsslnFile()
}

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

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

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

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

val timeSignatures = mutableListOf<TimeSignature>()

var currentBeatIndex = 0
var currentMeasureIndex = 0
var beatLength = 4.0

for (timeSignatureNode in timeSignaturesNode.children.sortedBy {
it.attributes.Clock as Int
for (timeSignatureTree in timeSignaturesTree.children.sortedBy {
it.attributes.Clock.value as Int
}) {
val numerator = timeSignatureNode.attributes.Beats as Int
val denominator = timeSignatureNode.attributes.BeatType as Int
val clock = timeSignatureNode.attributes.Clock 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
val beatLength = numerator / denominator * 4

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

val measureIndex = currentMeasureIndex + beatNum / beatLength
val measureIndex = (currentMeasureIndex + beatNum / beatLength).toInt()
beatLength = numerator.toDouble() / denominator * 4

currentBeatIndex = beatIndex
currentMeasureIndex = measureIndex
Expand All @@ -164,6 +168,120 @@ object Tssln {
return Pair(tempos, timeSignatures)
}

fun generate(project: Project, features: List<FeatureConfig>): 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<ValueTree> {
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<Tempo>): List<ValueTree> {
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<TimeSignature>): List<ValueTree> {
val timeSignatureTrees = mutableListOf<ValueTree>()

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<ValueTree> {
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 val TICK_RATE = 2.0

private val phonemePartPattern = Regex("""\[.+?\]""")
Expand Down
Original file line number Diff line number Diff line change
@@ -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]
Loading

0 comments on commit 3b50731

Please sign in to comment.