Fx
This commit is contained in:
@@ -37,6 +37,8 @@ kotlin {
|
|||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":common"))
|
implementation(project(":common"))
|
||||||
|
|
||||||
|
implementation("nl.astraeus:vst-worklet-base:1.0.0-SNAPSHOT")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsMain by getting {
|
val jsMain by getting {
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package nl.astraeus.vst
|
|
||||||
|
|
||||||
import org.khronos.webgl.Float32Array
|
|
||||||
import org.w3c.dom.MessagePort
|
|
||||||
|
|
||||||
enum class AutomationRate(
|
|
||||||
val rate: String
|
|
||||||
) {
|
|
||||||
A_RATE("a-rate"),
|
|
||||||
K_RATE("k-rate")
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AudioParam {
|
|
||||||
var value: Double
|
|
||||||
var automationRate: AutomationRate
|
|
||||||
val defaultValue: Double
|
|
||||||
val minValue: Double
|
|
||||||
val maxValue: Double
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AudioParamMap {
|
|
||||||
operator fun get(name: String): AudioParam
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract external class AudioWorkletProcessor {
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/parameters) */
|
|
||||||
//val parameters: AudioParamMap;
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/port) */
|
|
||||||
@JsName("port")
|
|
||||||
val port: MessagePort
|
|
||||||
|
|
||||||
@JsName("process")
|
|
||||||
open fun process (
|
|
||||||
inputs: Array<Array<Float32Array>>,
|
|
||||||
outputs: Array<Array<Float32Array>>,
|
|
||||||
parameters: dynamic
|
|
||||||
) : Boolean { definedExternally }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
external fun registerProcessor(name: String, processorCtor: JsClass<*>)
|
|
||||||
external val sampleRate: Int
|
|
||||||
external val currentTime: Double
|
|
||||||
@@ -66,6 +66,11 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
}
|
}
|
||||||
var waveform = Waveform.SINE.ordinal
|
var waveform = Waveform.SINE.ordinal
|
||||||
var dutyCycle = 0.5
|
var dutyCycle = 0.5
|
||||||
|
var fmFreq = 0.0
|
||||||
|
var fmAmp = 0.0
|
||||||
|
var amFreq = 0.0
|
||||||
|
var amAmp = 0.0
|
||||||
|
val sampleLength = 1 / sampleRate.toDouble()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.port.onmessage = ::handleMessage
|
this.port.onmessage = ::handleMessage
|
||||||
@@ -106,6 +111,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
|
|
||||||
private fun playMidi(bytes: Int32Array) {
|
private fun playMidi(bytes: Int32Array) {
|
||||||
if (bytes.length > 0) {
|
if (bytes.length > 0) {
|
||||||
|
//console.log("Received", bytes)
|
||||||
when(bytes[0]) {
|
when(bytes[0]) {
|
||||||
0x90 -> {
|
0x90 -> {
|
||||||
if (bytes.length == 3) {
|
if (bytes.length == 3) {
|
||||||
@@ -119,6 +125,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
0x80 -> {
|
0x80 -> {
|
||||||
if (bytes.length >= 2) {
|
if (bytes.length >= 2) {
|
||||||
val note = bytes[1]
|
val note = bytes[1]
|
||||||
@@ -126,6 +133,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
noteOff(note)
|
noteOff(note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
0xc9 -> {
|
0xc9 -> {
|
||||||
if (bytes.length >= 1) {
|
if (bytes.length >= 1) {
|
||||||
val waveform = bytes[1]
|
val waveform = bytes[1]
|
||||||
@@ -135,16 +143,42 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
0xb0 -> {
|
0xb0 -> {
|
||||||
if (bytes.length == 3) {
|
if (bytes.length == 3) {
|
||||||
val knob = bytes[1]
|
val knob = bytes[1]
|
||||||
val value = bytes[2]
|
val value = bytes[2]
|
||||||
|
|
||||||
when(knob) {
|
when (knob) {
|
||||||
0x4a -> {
|
0x4a -> {
|
||||||
dutyCycle = value / 127.0
|
dutyCycle = value / 127.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0x4b -> {
|
||||||
|
fmFreq = value / 127.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0x4c -> {
|
||||||
|
fmAmp = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x47 -> {
|
||||||
|
amFreq = value / 127.0
|
||||||
|
}
|
||||||
|
|
||||||
|
0x48 -> {
|
||||||
|
amAmp = value / 127.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
0xe0 -> {
|
||||||
|
if (bytes.length == 3) {
|
||||||
|
val lsb = bytes[1]
|
||||||
|
val msb = bytes[2]
|
||||||
|
|
||||||
|
amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +201,7 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
notes[i].state = NoteState.ON
|
notes[i].state = NoteState.ON
|
||||||
|
|
||||||
val n = Note.fromMidi(note)
|
val n = Note.fromMidi(note)
|
||||||
console.log("Playing note: ${n.sharp} (${n.freq})")
|
//console.log("Playing note: ${n.sharp} (${n.freq})")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,13 +239,16 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
note.releaseSamples--
|
note.releaseSamples--
|
||||||
targetVolume *= (note.releaseSamples / 10000f)
|
targetVolume *= (note.releaseSamples / 10000f)
|
||||||
}
|
}
|
||||||
note.actualVolume += (targetVolume - note.actualVolume) * 0.001f
|
note.actualVolume += (targetVolume - note.actualVolume) * 0.0001f
|
||||||
|
|
||||||
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
|
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
|
||||||
note.state = NoteState.OFF
|
note.state = NoteState.OFF
|
||||||
}
|
}
|
||||||
|
|
||||||
val cycleOffset = note.cycleOffset
|
var cycleOffset = note.cycleOffset
|
||||||
|
val fmModulation = sin(sampleLength * fmFreq * 10f * PI2 * note.sample).toFloat() * fmAmp * 5f
|
||||||
|
val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat()
|
||||||
|
cycleOffset += fmModulation
|
||||||
|
|
||||||
val waveValue: Float = when (waveform) {
|
val waveValue: Float = when (waveform) {
|
||||||
0 -> {
|
0 -> {
|
||||||
@@ -233,13 +270,14 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
left[i] = left[i] + waveValue * note.actualVolume * 0.3f
|
left[i] = left[i] + waveValue * note.actualVolume * 0.3f * amModulation
|
||||||
right[i] = right[i] + waveValue * note.actualVolume * 0.3f
|
right[i] = right[i] + waveValue * note.actualVolume * 0.3f * amModulation
|
||||||
|
|
||||||
note.cycleOffset += sampleDelta
|
note.cycleOffset += sampleDelta
|
||||||
if (cycleOffset > 1f) {
|
if (cycleOffset > 1f) {
|
||||||
note.cycleOffset -= 1f
|
note.cycleOffset -= 1f
|
||||||
}
|
}
|
||||||
|
note.sample++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ enum class Note(
|
|||||||
;
|
;
|
||||||
|
|
||||||
// 69 = A4.ordinal
|
// 69 = A4.ordinal
|
||||||
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69)/12.0) * 10000.0) / 10000.0
|
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69)/12.0)) // * 10000.0) / 10000.0
|
||||||
val cycleLength: Double = 1.0 / freq
|
val cycleLength: Double = 1.0 / freq
|
||||||
var sampleDelta: Double = 0.0
|
var sampleDelta: Double = 0.0
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
package nl.astraeus.vst.chip
|
package nl.astraeus.vst.chip
|
||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
|
import kotlinx.browser.window
|
||||||
import nl.astraeus.komp.Komponent
|
import nl.astraeus.komp.Komponent
|
||||||
import nl.astraeus.vst.chip.channel.Broadcaster
|
import nl.astraeus.vst.chip.channel.Broadcaster
|
||||||
|
import nl.astraeus.vst.chip.channel.MidiMessage
|
||||||
import nl.astraeus.vst.chip.midi.Midi
|
import nl.astraeus.vst.chip.midi.Midi
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
import nl.astraeus.vst.chip.view.MainView
|
||||||
|
import org.khronos.webgl.Uint8Array
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
Komponent.create(document.body!!, MainView)
|
Komponent.create(document.body!!, MainView)
|
||||||
|
|
||||||
Broadcaster.start()
|
|
||||||
|
|
||||||
Midi.start()
|
Midi.start()
|
||||||
|
|
||||||
|
console.log("Performance", window.performance)
|
||||||
|
Broadcaster.getChannel(0).postMessage(
|
||||||
|
MidiMessage(
|
||||||
|
Uint8Array(arrayOf(0x80.toByte(), 60, 60)),
|
||||||
|
window.performance.now()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
window.setInterval({
|
||||||
|
Broadcaster.sync()
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class AudioModule(
|
|||||||
console.log("Module not yet loaded")
|
console.log("Module not yet loaded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class AudioNode(
|
abstract class AudioNode(
|
||||||
|
|||||||
@@ -1,23 +1,104 @@
|
|||||||
|
@file:OptIn(ExperimentalJsExport::class)
|
||||||
|
|
||||||
package nl.astraeus.vst.chip.channel
|
package nl.astraeus.vst.chip.channel
|
||||||
|
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
import kotlinx.browser.window
|
||||||
|
import org.khronos.webgl.Uint8Array
|
||||||
import org.w3c.dom.BroadcastChannel
|
import org.w3c.dom.BroadcastChannel
|
||||||
import org.w3c.dom.MessageEvent
|
import org.w3c.dom.MessageEvent
|
||||||
import kotlin.js.Date
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
enum class MessageType {
|
||||||
|
SYNC,
|
||||||
|
MIDI
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
class SyncMessage(
|
||||||
|
@JsName("timeOrigin")
|
||||||
|
val timeOrigin: Double = window.performance.asDynamic().timeOrigin,
|
||||||
|
@JsName("now")
|
||||||
|
val now: Double = window.performance.now()
|
||||||
|
) {
|
||||||
|
@JsName("type")
|
||||||
|
val type: String = MessageType.SYNC.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// time -> syncOrigin
|
||||||
|
// receive syn message
|
||||||
|
// syncOrigin = my timeOrigin - sync.timeOrigin
|
||||||
|
// - sync.timeOrigin = 50
|
||||||
|
// - my.timeOrigin = 100
|
||||||
|
// - syncOrigin = -50
|
||||||
|
|
||||||
|
// - sync.timeOrigin = 49
|
||||||
|
// - my.timeOrigin = 100
|
||||||
|
// - syncOrigin = update to -51
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
class MidiMessage(
|
||||||
|
@JsName("data")
|
||||||
|
val data: Uint8Array,
|
||||||
|
@JsName("timestamp")
|
||||||
|
val timestamp: dynamic = null,
|
||||||
|
@JsName("timeOrigin")
|
||||||
|
val timeOrigin: dynamic = window.performance.asDynamic().timeOrigin
|
||||||
|
) {
|
||||||
|
@JsName("type")
|
||||||
|
val type = MessageType.MIDI.name
|
||||||
|
}
|
||||||
|
|
||||||
|
object Sync {
|
||||||
|
var syncOrigin = 0.0
|
||||||
|
|
||||||
|
fun update(sync: SyncMessage) {
|
||||||
|
syncOrigin = min(syncOrigin, window.performance.asDynamic().timeOrigin - sync.timeOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun now(): Double = window.performance.now() + syncOrigin
|
||||||
|
}
|
||||||
|
|
||||||
object Broadcaster {
|
object Broadcaster {
|
||||||
|
val channels = mutableMapOf<Int, BroadcastChannel>()
|
||||||
|
|
||||||
val channel = BroadcastChannel("audio-worklet")
|
fun getChannel(channel: Int): BroadcastChannel = channels.getOrPut(channel) {
|
||||||
|
val bcChannel = BroadcastChannel("audio-worklet-$channel")
|
||||||
|
|
||||||
fun start() {
|
bcChannel.onmessage = { event ->
|
||||||
channel.onmessage = ::onMessage
|
onMessage(channel, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessage(event: MessageEvent) {
|
bcChannel
|
||||||
MainView.addMessage("Received message ${event.data} time ${Date().getTime()}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun send(message: String) {
|
private fun onMessage(channel: Int, event: MessageEvent) {
|
||||||
channel.postMessage(message)
|
val data: dynamic = event.data.asDynamic()
|
||||||
|
|
||||||
|
if (data.type == MessageType.SYNC.name) {
|
||||||
|
val syncMessage = SyncMessage(
|
||||||
|
data.timeOrigin,
|
||||||
|
data.now
|
||||||
|
)
|
||||||
|
Sync.update(syncMessage)
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"Received broadcast message on channel $channel",
|
||||||
|
event,
|
||||||
|
window.performance,
|
||||||
|
window.performance.now() - event.timeStamp.toDouble()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(channel: Int, message: Any) {
|
||||||
|
getChannel(channel).postMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sync() {
|
||||||
|
for (channel in channels.values) {
|
||||||
|
channel.postMessage(SyncMessage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,20 @@ external class MIDIOutput {
|
|||||||
val type: String
|
val type: String
|
||||||
val version: String
|
val version: String
|
||||||
|
|
||||||
fun send(message: dynamic)
|
fun send(message: dynamic, timestamp: dynamic)
|
||||||
|
|
||||||
|
fun open()
|
||||||
|
fun close()
|
||||||
}
|
}
|
||||||
|
|
||||||
object Midi {
|
object Midi {
|
||||||
|
var inputChannel: Int = -1
|
||||||
|
var outputChannel: Int = -1
|
||||||
|
|
||||||
var inputs = mutableListOf<MIDIInput>()
|
var inputs = mutableListOf<MIDIInput>()
|
||||||
var outputs = mutableListOf<MIDIInput>()
|
var outputs = mutableListOf<MIDIOutput>()
|
||||||
var currentInput: MIDIInput? = null
|
var currentInput: MIDIInput? = null
|
||||||
|
var currentOutput: MIDIOutput? = null
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
val navigator = window.navigator.asDynamic()
|
val navigator = window.navigator.asDynamic()
|
||||||
@@ -68,7 +75,7 @@ object Midi {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setInput(input: MIDIInput) {
|
fun setInput(input: MIDIInput?) {
|
||||||
console.log("Setting input", input)
|
console.log("Setting input", input)
|
||||||
currentInput?.close()
|
currentInput?.close()
|
||||||
|
|
||||||
@@ -94,4 +101,17 @@ object Midi {
|
|||||||
currentInput?.open()
|
currentInput?.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setOutput(output: MIDIOutput?) {
|
||||||
|
console.log("Setting output", output)
|
||||||
|
currentOutput?.close()
|
||||||
|
|
||||||
|
currentOutput = output
|
||||||
|
|
||||||
|
currentOutput?.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(data: Uint8Array, timestamp: dynamic? = null) {
|
||||||
|
currentOutput?.send(data, timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,38 +6,39 @@ import daw.style.Css.noTextSelect
|
|||||||
import daw.style.CssId
|
import daw.style.CssId
|
||||||
import daw.style.CssName
|
import daw.style.CssName
|
||||||
import daw.style.hover
|
import daw.style.hover
|
||||||
import kotlinx.html.FlowContent
|
import kotlinx.browser.window
|
||||||
import kotlinx.html.P
|
import kotlinx.html.InputType
|
||||||
import kotlinx.html.a
|
|
||||||
import kotlinx.html.br
|
|
||||||
import kotlinx.html.classes
|
|
||||||
import kotlinx.html.div
|
import kotlinx.html.div
|
||||||
import kotlinx.html.h1
|
import kotlinx.html.h1
|
||||||
import kotlinx.html.hr
|
import kotlinx.html.input
|
||||||
import kotlinx.html.js.onChangeFunction
|
import kotlinx.html.js.onChangeFunction
|
||||||
import kotlinx.html.js.onClickFunction
|
import kotlinx.html.js.onClickFunction
|
||||||
import kotlinx.html.js.onMouseDownFunction
|
|
||||||
import kotlinx.html.js.onMouseUpFunction
|
|
||||||
import kotlinx.html.option
|
import kotlinx.html.option
|
||||||
import kotlinx.html.select
|
import kotlinx.html.select
|
||||||
import kotlinx.html.span
|
import kotlinx.html.span
|
||||||
import nl.astraeus.css.properties.BoxSizing
|
import nl.astraeus.css.properties.BoxSizing
|
||||||
import nl.astraeus.css.properties.FontWeight
|
import nl.astraeus.css.properties.FontWeight
|
||||||
|
import nl.astraeus.css.properties.Position
|
||||||
|
import nl.astraeus.css.properties.Transform
|
||||||
|
import nl.astraeus.css.properties.em
|
||||||
|
import nl.astraeus.css.properties.hsla
|
||||||
import nl.astraeus.css.properties.prc
|
import nl.astraeus.css.properties.prc
|
||||||
import nl.astraeus.css.properties.px
|
import nl.astraeus.css.properties.px
|
||||||
import nl.astraeus.css.properties.rem
|
import nl.astraeus.css.properties.rem
|
||||||
|
import nl.astraeus.css.properties.vh
|
||||||
|
import nl.astraeus.css.properties.vw
|
||||||
import nl.astraeus.css.style.cls
|
import nl.astraeus.css.style.cls
|
||||||
import nl.astraeus.komp.HtmlBuilder
|
import nl.astraeus.komp.HtmlBuilder
|
||||||
import nl.astraeus.komp.Komponent
|
import nl.astraeus.komp.Komponent
|
||||||
import nl.astraeus.vst.Note
|
|
||||||
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
||||||
import nl.astraeus.vst.chip.channel.Broadcaster
|
|
||||||
import nl.astraeus.vst.chip.midi.Midi
|
import nl.astraeus.vst.chip.midi.Midi
|
||||||
import org.khronos.webgl.Int32Array
|
import org.khronos.webgl.Uint8Array
|
||||||
import org.w3c.dom.HTMLSelectElement
|
import org.w3c.dom.HTMLSelectElement
|
||||||
|
import org.w3c.performance.Performance
|
||||||
|
|
||||||
object MainView : Komponent() {
|
object MainView : Komponent() {
|
||||||
private var messages: MutableList<String> = ArrayList()
|
private var messages: MutableList<String> = ArrayList()
|
||||||
|
private var started = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
MainViewCss
|
MainViewCss
|
||||||
@@ -52,21 +53,14 @@ object MainView : Komponent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun HtmlBuilder.render() {
|
override fun HtmlBuilder.render() {
|
||||||
div {
|
div(MainViewCss.MainDivCss.name) {
|
||||||
h1 {
|
if (!started) {
|
||||||
+"VST Chip"
|
div(MainViewCss.StartSplashCss.name) {
|
||||||
}
|
div(MainViewCss.StartBoxCss.name) {
|
||||||
div {
|
div(MainViewCss.StartButtonCss.name) {
|
||||||
+"Hello, World!"
|
+"START"
|
||||||
}
|
|
||||||
div {
|
|
||||||
if (VstChipWorklet.created) {
|
|
||||||
+"Worklet created"
|
|
||||||
} else {
|
|
||||||
a {
|
|
||||||
href = "#"
|
|
||||||
+"Create worklet"
|
|
||||||
onClickFunction = {
|
onClickFunction = {
|
||||||
|
started = true
|
||||||
VstChipWorklet.create {
|
VstChipWorklet.create {
|
||||||
requestUpdate()
|
requestUpdate()
|
||||||
}
|
}
|
||||||
@@ -74,9 +68,22 @@ object MainView : Komponent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
+"VST Chip"
|
||||||
|
}
|
||||||
div {
|
div {
|
||||||
+ "Midi input: "
|
span {
|
||||||
|
+"Midi input: "
|
||||||
select {
|
select {
|
||||||
|
option {
|
||||||
|
+"None"
|
||||||
|
value = ""
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
+"Midi over Broadcast"
|
||||||
|
value = "midi-broadcast"
|
||||||
|
}
|
||||||
for (mi in Midi.inputs) {
|
for (mi in Midi.inputs) {
|
||||||
option {
|
option {
|
||||||
+mi.name
|
+mi.name
|
||||||
@@ -86,60 +93,113 @@ object MainView : Komponent() {
|
|||||||
|
|
||||||
onChangeFunction = { event ->
|
onChangeFunction = { event ->
|
||||||
val target = event.target as HTMLSelectElement
|
val target = event.target as HTMLSelectElement
|
||||||
|
if (target.value == "") {
|
||||||
|
Midi.setInput(null)
|
||||||
|
} else {
|
||||||
val selected = Midi.inputs.find { it.id == target.value }
|
val selected = Midi.inputs.find { it.id == target.value }
|
||||||
if (selected != null) {
|
if (selected != null) {
|
||||||
Midi.setInput(selected)
|
Midi.setInput(selected)
|
||||||
|
} else if (target.value == "midi-broadcast") {
|
||||||
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
br {}
|
|
||||||
|
|
||||||
hr {}
|
|
||||||
|
|
||||||
repeat(9) {
|
|
||||||
div(classes = MainViewCss.NoteBarCss.name) {
|
|
||||||
for (index in it*12+12..it*12+23) {
|
|
||||||
notePlayer(Note.entries[index])
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {}
|
|
||||||
|
|
||||||
for (message in messages) {
|
|
||||||
div {
|
|
||||||
+message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun FlowContent.notePlayer(note: Note) {
|
|
||||||
span {
|
span {
|
||||||
a(classes = MainViewCss.ButtonCss.name) {
|
+"channel:"
|
||||||
href = "#"
|
input {
|
||||||
+note.sharp
|
type = InputType.number
|
||||||
onMouseDownFunction = {
|
value = Midi.inputChannel.toString()
|
||||||
VstChipWorklet.postMessage(Int32Array(arrayOf(0x90, note.ordinal, 32)))
|
onChangeFunction = { event ->
|
||||||
|
val target = event.target as HTMLSelectElement
|
||||||
|
Midi.inputChannel = target.value.toInt()
|
||||||
}
|
}
|
||||||
onMouseUpFunction = {
|
|
||||||
VstChipWorklet.postMessage(Int32Array(arrayOf(0x90, note.ordinal, 0)))
|
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
onMouseOutFunction = {
|
|
||||||
VstChipWorklet.postMessage(Int32Array(arrayOf(0x90, note.ordinal, 0)))
|
|
||||||
}
|
}
|
||||||
*/
|
}
|
||||||
|
div {
|
||||||
|
span {
|
||||||
|
+"Midi output: "
|
||||||
|
select {
|
||||||
|
option {
|
||||||
|
+"None"
|
||||||
|
value = ""
|
||||||
|
}
|
||||||
|
option {
|
||||||
|
+"Midi over Broadcast"
|
||||||
|
value = "midi-broadcast"
|
||||||
|
}
|
||||||
|
for (mi in Midi.outputs) {
|
||||||
|
option {
|
||||||
|
+mi.name
|
||||||
|
value = mi.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeFunction = { event ->
|
||||||
|
val target = event.target as HTMLSelectElement
|
||||||
|
if (target.value == "") {
|
||||||
|
Midi.setOutput(null)
|
||||||
|
} else {
|
||||||
|
val selected = Midi.outputs.find { it.id == target.value }
|
||||||
|
if (selected != null) {
|
||||||
|
Midi.setOutput(selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
+"channel:"
|
||||||
|
input {
|
||||||
|
type = InputType.number
|
||||||
|
value = Midi.outputChannel.toString()
|
||||||
|
onChangeFunction = { event ->
|
||||||
|
val target = event.target as HTMLSelectElement
|
||||||
|
Midi.outputChannel = target.value.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
+"Send note on to output"
|
||||||
|
onClickFunction = {
|
||||||
|
val data = Uint8Array(
|
||||||
|
arrayOf(
|
||||||
|
0x90.toByte(),
|
||||||
|
0x3c.toByte(),
|
||||||
|
0x70.toByte()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Midi.send(data, window.performance.now() + 1000)
|
||||||
|
Midi.send(data, window.performance.now() + 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
+"Send note off to output"
|
||||||
|
onClickFunction = {
|
||||||
|
val data = Uint8Array(
|
||||||
|
arrayOf(
|
||||||
|
0x90.toByte(),
|
||||||
|
0x3c.toByte(),
|
||||||
|
0x0.toByte(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Midi.send(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object MainViewCss : CssId("main") {
|
object MainViewCss : CssId("main") {
|
||||||
|
object MainDivCss : CssName()
|
||||||
object ActiveCss : CssName()
|
object ActiveCss : CssName()
|
||||||
object ButtonCss : CssName()
|
object ButtonCss : CssName()
|
||||||
object NoteBarCss : CssName()
|
object NoteBarCss : CssName()
|
||||||
|
object StartSplashCss : CssName()
|
||||||
|
object StartBoxCss : CssName()
|
||||||
|
object StartButtonCss : CssName()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
defineCss {
|
defineCss {
|
||||||
@@ -155,9 +215,6 @@ object MainView : Komponent() {
|
|||||||
padding(0.px)
|
padding(0.px)
|
||||||
height(100.prc)
|
height(100.prc)
|
||||||
|
|
||||||
color(Css.currentStyle.mainFontColor)
|
|
||||||
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
|
||||||
|
|
||||||
fontFamily("JetbrainsMono, monospace")
|
fontFamily("JetbrainsMono, monospace")
|
||||||
fontSize(14.px)
|
fontSize(14.px)
|
||||||
fontWeight(FontWeight.bold)
|
fontWeight(FontWeight.bold)
|
||||||
@@ -180,6 +237,49 @@ object MainView : Komponent() {
|
|||||||
select(cls(NoteBarCss)) {
|
select(cls(NoteBarCss)) {
|
||||||
minHeight(4.rem)
|
minHeight(4.rem)
|
||||||
}
|
}
|
||||||
|
select(cls(MainDivCss)) {
|
||||||
|
margin(1.rem)
|
||||||
|
}
|
||||||
|
select("select") {
|
||||||
|
plain("appearance", "none")
|
||||||
|
border("0")
|
||||||
|
outline("0")
|
||||||
|
width(20.rem)
|
||||||
|
padding(0.5.rem, 2.rem, 0.5.rem, 0.5.rem)
|
||||||
|
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
|
||||||
|
background("right 0.8em center/1.4em")
|
||||||
|
backgroundColor(Css.currentStyle.inputBackgroundColor)
|
||||||
|
color(Css.currentStyle.entryFontColor)
|
||||||
|
borderRadius(0.25.em)
|
||||||
|
}
|
||||||
|
select(cls(StartSplashCss)) {
|
||||||
|
position(Position.fixed)
|
||||||
|
left(0.px)
|
||||||
|
top(0.px)
|
||||||
|
width(100.vw)
|
||||||
|
height(100.vh)
|
||||||
|
zIndex(100)
|
||||||
|
backgroundColor(hsla(32, 0, 50, 0.6))
|
||||||
|
|
||||||
|
select(cls(StartBoxCss)) {
|
||||||
|
position(Position.relative)
|
||||||
|
left(25.vw)
|
||||||
|
top(25.vh)
|
||||||
|
width(50.vw)
|
||||||
|
height(50.vh)
|
||||||
|
backgroundColor(hsla(0, 0, 50, 0.25))
|
||||||
|
|
||||||
|
select(cls(StartButtonCss)) {
|
||||||
|
position(Position.absolute)
|
||||||
|
left(50.prc)
|
||||||
|
top(50.prc)
|
||||||
|
transform(Transform("translate(-50%, -50%)"))
|
||||||
|
padding(1.rem)
|
||||||
|
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
||||||
|
cursor("pointer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user