diff --git a/.gitignore b/.gitignore index c82ce0d..35b4a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ bin/ ### .kotlin ### .kotlin kotlin-js-store +gradle.properties diff --git a/.idea/artifacts/midi_arrays_js_1_0_0_SNAPSHOT.xml b/.idea/artifacts/midi_arrays_js_1_0_0_SNAPSHOT.xml new file mode 100644 index 0000000..5fec0c2 --- /dev/null +++ b/.idea/artifacts/midi_arrays_js_1_0_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/artifacts/midi_arrays_jvm_1_0_0_SNAPSHOT.xml b/.idea/artifacts/midi_arrays_jvm_1_0_0_SNAPSHOT.xml new file mode 100644 index 0000000..bdf9596 --- /dev/null +++ b/.idea/artifacts/midi_arrays_jvm_1_0_0_SNAPSHOT.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/build/libs + + + + + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..cc50acf --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index af3b15e..a13e170 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,64 +1,108 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType plugins { - kotlin("multiplatform") version "2.1.0" + kotlin("multiplatform") version "2.0.21" + `maven-publish` + signing } group = "nl.astraeus" -version = "1.0.0-SNAPSHOT" +version = "0.1.0" repositories { mavenCentral() - maven { - url = uri("https://gitea.astraeus.nl/api/packages/rnentjes/maven") - } maven { url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven") } } kotlin { - jvmToolchain(17) jvm() js { - binaries.executable() - browser { - distribution { - outputDirectory.set(File("$projectDir/web/")) - } - } + browser() } sourceSets { val commonMain by getting { dependencies { - api("nl.astraeus:kotlin-simple-logging:1.1.1") - api("nl.astraeus:kotlin-css-generator:1.0.10") - - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0") + api("nl.astraeus:typed-byte-arrays:0.2.6") } } val commonTest by getting - val jvmMain by getting { - dependencies { - implementation("io.undertow:undertow-core:2.3.14.Final") - implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0") - - implementation("org.xerial:sqlite-jdbc:3.32.3.2") - implementation("com.zaxxer:HikariCP:4.0.3") - implementation("nl.astraeus:simple-jdbc-stats:1.6.1") { - exclude(group = "org.slf4j", module = "slf4j-api") - } - } - } - val jvmTest by getting { - dependencies { - } - } - val jsMain by getting { - dependencies { - implementation("nl.astraeus:kotlin-komponent:1.2.4") - } - } + val jvmMain by getting + val jvmTest by getting + val jsMain by getting val jsTest by getting } -} \ No newline at end of file +} + +extra["PUBLISH_GROUP_ID"] = group +extra["PUBLISH_VERSION"] = version +extra["PUBLISH_ARTIFACT_ID"] = name + +// Stub secrets to let the project sync and build without the publication values set up +val signingKeyId: String? by project +val signingPassword: String? by project +val signingSecretKeyRingFile: String? by project + +extra["signing.keyId"] = signingKeyId +extra["signing.password"] = signingPassword +extra["signing.secretKeyRingFile"] = signingSecretKeyRingFile + +val javadocJar by tasks.registering(Jar::class) { + archiveClassifier.set("javadoc") +} + +publishing { + repositories { + maven { + name = "gitea" + setUrl("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven") + + credentials { + val giteaUsername: String? by project + val giteaPassword: String? by project + + username = giteaUsername + password = giteaPassword + } + } + } + + // Configure all publications + publications.withType { + // Stub javadoc.jar artifact + artifact(javadocJar.get()) + + // Provide artifacts information requited by Maven Central + pom { + name.set("typed-byte-arrays") + description.set("Typed byte arrays") + url.set("https://gitea.astraeus.nl/rnentjes/typed-byte-arrays") + + licenses { + license { + name.set("MIT") + url.set("https://opensource.org/licenses/MIT") + } + } + developers { + developer { + id.set("rnentjes") + name.set("Rien Nentjes") + email.set("info@nentjes.com") + } + } + scm { + url.set("https://gitea.astraeus.nl/rnentjes/typed-byte-arrays") + } + } + } +} + +signing { + sign(publishing.publications) +} + +tasks.withType { + dependsOn(tasks.withType()) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index bd76f11..6cf07d9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" } -val REPO_NAME = "template" + rootProject.name = "midi-arrays" diff --git a/src/commonMain/kotlin/nl/astraeus/midi/message/MidiMessage.kt b/src/commonMain/kotlin/nl/astraeus/midi/message/MidiMessage.kt new file mode 100644 index 0000000..03ddef4 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/midi/message/MidiMessage.kt @@ -0,0 +1,42 @@ +package nl.astraeus.midi.message + +import nl.astraeus.tba.DataType +import nl.astraeus.tba.MutableByteArrayHandler +import nl.astraeus.tba.SlicedByteArray +import nl.astraeus.tba.Type +import nl.astraeus.tba.TypedByteArray +import nl.astraeus.tba.blob +import nl.astraeus.tba.double +import nl.astraeus.tba.long + +class MidiMessage() : TypedByteArray( + Type("type", DataType.LONG), + Type("generated", DataType.DOUBLE), + Type("playTime", DataType.DOUBLE), + Type("midi", DataType.BLOB, 240), +) { + var type by long("type") + var generated by double("generated") + var playTime by double("playTime") + var midi by blob("data") + + init { + this.type = MidiMessageTypes.MIDI_DATA.typeId + } + + + constructor(data: ByteArray): this() { + check(data.size == definition.size) { + "Invalid data size: ${data.size} != ${definition.size}" + } + + this.data = MutableByteArrayHandler(data) + } + + constructor(playTime: Double, midi: ByteArray): this() { + this.generated = getCurrentTime() + this.playTime = playTime + this.midi = SlicedByteArray(midi) + } + +} diff --git a/src/commonMain/kotlin/nl/astraeus/midi/message/MidiMessageTypes.kt b/src/commonMain/kotlin/nl/astraeus/midi/message/MidiMessageTypes.kt new file mode 100644 index 0000000..b224181 --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/midi/message/MidiMessageTypes.kt @@ -0,0 +1,41 @@ +package nl.astraeus.midi.message + + +inline fun String.encodeToNumber(): T { + val cls = T::class + + val length = when(cls) { + Byte::class -> 1 + Short::class -> 2 + Int::class -> 4 + Long::class -> 8 + else -> throw IllegalArgumentException("Unsupported type") + } + + var count = 0 + var result = 0L + for (ch in this) { + result = result shl 8 + result += ch.code and 0xff + + if (count++ == length) { + break + } + } + + return when(cls) { + Byte::class -> result.toByte() as T + Short::class -> result.toShort() as T + Int::class -> result.toInt() as T + Long::class -> result as T + else -> throw IllegalArgumentException("Unsupported type") + } +} + +enum class MidiMessageTypes( + val type: String, + val typeId: Long, +) { + NONE("Unknown", 0L), + MIDI_DATA("Timestamped midi data", "TimeMidi".encodeToNumber()), +} diff --git a/src/commonMain/kotlin/nl/astraeus/midi/message/Time.kt b/src/commonMain/kotlin/nl/astraeus/midi/message/Time.kt new file mode 100644 index 0000000..a1b148c --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/midi/message/Time.kt @@ -0,0 +1,3 @@ +package nl.astraeus.midi.message + +expect fun getCurrentTime(): Double diff --git a/src/jsMain/kotlin/nl/astraeus/midi/message/Time.js.kt b/src/jsMain/kotlin/nl/astraeus/midi/message/Time.js.kt new file mode 100644 index 0000000..f255784 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/midi/message/Time.js.kt @@ -0,0 +1,3 @@ +package nl.astraeus.midi.message + +actual fun getCurrentTime(): Double = js("performance.now()").unsafeCast() diff --git a/src/jvmMain/kotlin/nl/astraeus/midi/message/Time.jvm.kt b/src/jvmMain/kotlin/nl/astraeus/midi/message/Time.jvm.kt new file mode 100644 index 0000000..a70ff06 --- /dev/null +++ b/src/jvmMain/kotlin/nl/astraeus/midi/message/Time.jvm.kt @@ -0,0 +1,5 @@ +package nl.astraeus.midi.message + +actual fun getCurrentTime(): Double { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/tmpl/Main.kt b/src/jvmMain/kotlin/tmpl/Main.kt deleted file mode 100644 index db76ced..0000000 --- a/src/jvmMain/kotlin/tmpl/Main.kt +++ /dev/null @@ -1,37 +0,0 @@ -package tmpl - -import com.zaxxer.hikari.HikariConfig -import nl.astraeus.logger.Logger -import tmpl.db.Database - -val log = Logger() - -fun main() { - Thread.currentThread().uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e -> - log.warn(e) { - e.message - } - } - - Runtime.getRuntime().addShutdownHook( - object : Thread() { - override fun run() { - Database.vacuumDatabase() - Database.closeDatabase() - } - } - ) - - Class.forName("nl.astraeus.jdbc.Driver") - Database.initialize(HikariConfig().apply { - driverClassName = "nl.astraeus.jdbc.Driver" - jdbcUrl = "jdbc:stat:webServerPort=6001:jdbc:sqlite:data/midi-arrays.db" - username = "sa" - password = "" - maximumPoolSize = 25 - isAutoCommit = false - - validate() - }) - -} diff --git a/src/jvmMain/kotlin/tmpl/db/Database.kt b/src/jvmMain/kotlin/tmpl/db/Database.kt deleted file mode 100644 index eec7b94..0000000 --- a/src/jvmMain/kotlin/tmpl/db/Database.kt +++ /dev/null @@ -1,73 +0,0 @@ -package tmpl.db - -import com.zaxxer.hikari.HikariConfig -import com.zaxxer.hikari.HikariDataSource -import java.sql.Connection -import java.util.* -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.collections.set -import kotlin.use - -private val currentConnection = ThreadLocal() - -fun transaction( - block: (Connection) -> T -): T { - val hasConnection = currentConnection.get() != null - var oldConnection: Connection? = null - - if (!hasConnection) { - currentConnection.set(Database.getConnection()) - } - - val connection = currentConnection.get() - - try { - val result = block(connection) - - connection.commit() - - return result - } finally { - if (!hasConnection) { - currentConnection.set(oldConnection) - connection.close() - } - } -} - -object Database { - - var ds: HikariDataSource? = null - - fun initialize(config: HikariConfig) { - val properties = Properties() - properties["journal_mode"] = "WAL" - - config.dataSourceProperties = properties - config.addDataSourceProperty("cachePrepStmts", "true") - config.addDataSourceProperty("prepStmtCacheSize", "250") - config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048") - - ds = HikariDataSource(config) - Migrations.databaseVersionTableCreated = AtomicBoolean(false) - Migrations.updateDatabaseIfNeeded() - } - - fun getConnection() = ds?.connection ?: error("Database has not been initialized!") - - fun vacuumDatabase() { - getConnection().use { - it.autoCommit = true - - it.prepareStatement("VACUUM").use { ps -> - ps.executeUpdate() - } - } - } - - fun closeDatabase() { - ds?.close() - } - -} diff --git a/src/jvmMain/kotlin/tmpl/db/Migrations.kt b/src/jvmMain/kotlin/tmpl/db/Migrations.kt deleted file mode 100644 index cfc8f3a..0000000 --- a/src/jvmMain/kotlin/tmpl/db/Migrations.kt +++ /dev/null @@ -1,105 +0,0 @@ -package tmpl.db - -import tmpl.log -import java.sql.Connection -import java.sql.SQLException -import java.sql.Timestamp -import java.util.concurrent.atomic.AtomicBoolean - -sealed class Migration { - class Query( - val query: String - ) : Migration() { - override fun toString(): String { - return query - } - } - - class Code( - val code: (Connection) -> Unit - ) : Migration() { - override fun toString(): String { - return code.toString() - } - } -} - -val DATABASE_MIGRATIONS = arrayOf( - Migration.Query( - """ - CREATE TABLE DATABASE_VERSION ( - ID INTEGER PRIMARY KEY, - QUERY TEXT, - EXECUTED TIMESTAMP - ) - """.trimIndent() - ), - Migration.Query("SELECT sqlite_version()"), -) - -object Migrations { - var databaseVersionTableCreated = AtomicBoolean(false) - - fun updateDatabaseIfNeeded() { - try { - transaction { con -> - con.prepareStatement( - """ - SELECT MAX(ID) FROM DATABASE_VERSION - """.trimIndent() - ).use { ps -> - ps.executeQuery().use { rs -> - databaseVersionTableCreated.compareAndSet(false, true) - - if(rs.next()) { - val maxId = rs.getInt(1) - - for (index in maxId + 1.. - log.debug { - "Executing migration index - [DATABASE_MIGRATIONS[index]]" - } - val description = when( - val migration = DATABASE_MIGRATIONS[index] - ) { - is Migration.Query -> { - con.prepareStatement(migration.query).use { ps -> - ps.execute() - } - - migration.query - } - is Migration.Code -> { - migration.code(con) - - migration.code.toString() - } - } - con.prepareStatement("INSERT INTO DATABASE_VERSION VALUES (?, ?, ?)").use { ps -> - ps.setInt(1, index) - ps.setString(2, description) - ps.setTimestamp(3, Timestamp(System.currentTimeMillis())) - - ps.execute() - } - } - } - -}