Listen to midi
This commit is contained in:
@@ -9,6 +9,7 @@ import nl.astraeus.vst.sampleRate
|
|||||||
import org.khronos.webgl.ArrayBuffer
|
import org.khronos.webgl.ArrayBuffer
|
||||||
import org.khronos.webgl.Float32Array
|
import org.khronos.webgl.Float32Array
|
||||||
import org.khronos.webgl.Int32Array
|
import org.khronos.webgl.Int32Array
|
||||||
|
import org.khronos.webgl.Uint8Array
|
||||||
import org.khronos.webgl.get
|
import org.khronos.webgl.get
|
||||||
import org.khronos.webgl.set
|
import org.khronos.webgl.set
|
||||||
import org.w3c.dom.MessageEvent
|
import org.w3c.dom.MessageEvent
|
||||||
@@ -79,6 +80,13 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
}
|
}
|
||||||
is ArrayBuffer -> {
|
is ArrayBuffer -> {
|
||||||
}
|
}
|
||||||
|
is Uint8Array -> {
|
||||||
|
val data32 = Int32Array(data.length)
|
||||||
|
for (i in 0 until data.length) {
|
||||||
|
data32[i] = (data[i].toInt() and 0xff)
|
||||||
|
}
|
||||||
|
playMidi(data32)
|
||||||
|
}
|
||||||
is Int32Array -> {
|
is Int32Array -> {
|
||||||
playMidi(data)
|
playMidi(data)
|
||||||
}
|
}
|
||||||
@@ -102,8 +110,12 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0x90 -> {
|
0x80 -> {
|
||||||
|
if (bytes.length >= 2) {
|
||||||
|
val note = bytes[1]
|
||||||
|
|
||||||
|
noteOff(note)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +125,6 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
for (i in 0 until POLYPHONICS) {
|
for (i in 0 until POLYPHONICS) {
|
||||||
if (notes[i].note == note) {
|
if (notes[i].note == note) {
|
||||||
notes[i].retrigger(velocity)
|
notes[i].retrigger(velocity)
|
||||||
//console.log("Note retriggered", notes[i])
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +135,9 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
velocity
|
velocity
|
||||||
)
|
)
|
||||||
notes[i].state = NoteState.ON
|
notes[i].state = NoteState.ON
|
||||||
console.log("Playing note", notes[i])
|
|
||||||
|
val n = Note.fromMidi(note)
|
||||||
|
console.log("Playing note: ${n.sharp} (${n.freq})")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +147,6 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
for (i in 0 until POLYPHONICS) {
|
for (i in 0 until POLYPHONICS) {
|
||||||
if (notes[i].note == note && notes[i].state == NoteState.ON) {
|
if (notes[i].note == note && notes[i].state == NoteState.ON) {
|
||||||
notes[i].state = NoteState.RELEASED
|
notes[i].state = NoteState.RELEASED
|
||||||
//console.log("Released note", notes[i])
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,14 +175,17 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
|||||||
note.releaseSamples--
|
note.releaseSamples--
|
||||||
targetVolume *= (note.releaseSamples / 10000f)
|
targetVolume *= (note.releaseSamples / 10000f)
|
||||||
}
|
}
|
||||||
note.actualVolume += (targetVolume - note.actualVolume) * 0.01f
|
note.actualVolume += (targetVolume - note.actualVolume) * 0.005f
|
||||||
|
|
||||||
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
|
if (note.state == NoteState.RELEASED && note.actualVolume <= 0) {
|
||||||
note.state = NoteState.OFF
|
note.state = NoteState.OFF
|
||||||
}
|
}
|
||||||
|
|
||||||
left[i] = left[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume
|
left[i] = left[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume * 0.3f
|
||||||
right[i] = right[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume
|
right[i] = right[i] + sin(note.cycleOffset * PI2).toFloat() * note.actualVolume * 0.3f
|
||||||
|
|
||||||
|
//left[i] = left[i] + if (note.cycleOffset < 0.5) { 0.3f } else { -0.3f } * note.actualVolume //sin(note.cycleOffset * PI2).toFloat() * note.actualVolume * 0.3f
|
||||||
|
//right[i] = right[i] + if (note.cycleOffset < 0.5) { 0.3f } else { -0.3f } * note.actualVolume //sin(note.cycleOffset * PI2).toFloat() * note.actualVolume * 0.3f
|
||||||
|
|
||||||
note.cycleOffset += sampleDelta
|
note.cycleOffset += sampleDelta
|
||||||
if (note.cycleOffset > 1f) {
|
if (note.cycleOffset > 1f) {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ kotlin {
|
|||||||
browser {
|
browser {
|
||||||
commonWebpackConfig {
|
commonWebpackConfig {
|
||||||
outputFileName = "vst-chip-worklet-ui.js"
|
outputFileName = "vst-chip-worklet-ui.js"
|
||||||
//cssSupport.enabled = true
|
|
||||||
sourceMaps = true
|
sourceMaps = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package nl.astraeus.vst
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User: rnentjes
|
* User: rnentjes
|
||||||
@@ -15,17 +16,17 @@ enum class Note(
|
|||||||
val flat: String
|
val flat: String
|
||||||
) {
|
) {
|
||||||
NONE("---", "---"),
|
NONE("---", "---"),
|
||||||
NO02("",""),
|
No02("C--","C--"),
|
||||||
NO03("",""),
|
NO03("C#-","Db-"),
|
||||||
NO04("",""),
|
NO04("D--","D--"),
|
||||||
NO05("",""),
|
NO05("D#-","Eb-"),
|
||||||
NO06("",""),
|
NO06("E--","E--"),
|
||||||
NO07("",""),
|
NO07("F--","F--"),
|
||||||
NO08("",""),
|
NO08("F#-","Gb-"),
|
||||||
NO09("",""),
|
NO09("G--","G--"),
|
||||||
NO10("",""),
|
NO10("G#-","Ab-"),
|
||||||
NO11("",""),
|
NO11("A#-","Bb-"),
|
||||||
NO12("",""),
|
NO12("B--","B--"),
|
||||||
C0("C-0","C-0"),
|
C0("C-0","C-0"),
|
||||||
C0s("C#0","Db0"),
|
C0s("C#0","Db0"),
|
||||||
D0("D-0","D-0"),
|
D0("D-0","D-0"),
|
||||||
@@ -152,7 +153,7 @@ enum class Note(
|
|||||||
;
|
;
|
||||||
|
|
||||||
// 69 = A4.ordinal
|
// 69 = A4.ordinal
|
||||||
val freq: Double = 440.0 * 2.0.pow((ordinal - 69)/12.0)
|
val freq: Double = round(440.0 * 2.0.pow((ordinal - 69)/12.0) * 100.0) / 100.0
|
||||||
val cycleLength: Double = 1.0 / freq
|
val cycleLength: Double = 1.0 / freq
|
||||||
var sampleDelta: Double = 0.0
|
var sampleDelta: Double = 0.0
|
||||||
|
|
||||||
@@ -162,14 +163,12 @@ enum class Note(
|
|||||||
result = min(result, G9.ordinal)
|
result = min(result, G9.ordinal)
|
||||||
result = max(result, C0.ordinal)
|
result = max(result, C0.ordinal)
|
||||||
|
|
||||||
entries.firstOrNull { it.ordinal == result } ?: this
|
fromMidi(result)
|
||||||
} else {
|
} else {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var sampleRate: Int = 44100
|
|
||||||
|
|
||||||
fun fromMidi(midi: Int): Note {
|
fun fromMidi(midi: Int): Note {
|
||||||
// todo: add check
|
// todo: add check
|
||||||
return entries[midi]
|
return entries[midi]
|
||||||
@@ -177,10 +176,16 @@ enum class Note(
|
|||||||
|
|
||||||
fun updateSampleRate(rate: Int) {
|
fun updateSampleRate(rate: Int) {
|
||||||
println("Setting sample rate to $rate")
|
println("Setting sample rate to $rate")
|
||||||
sampleRate = rate
|
|
||||||
for (note in Note.entries) {
|
for (note in Note.entries) {
|
||||||
note.sampleDelta = (1.0 / sampleRate.toDouble()) / note.cycleLength
|
note.sampleDelta = (1.0 / rate.toDouble()) / note.cycleLength
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// freq = 10Hz
|
||||||
|
// cycleLength = 0.1
|
||||||
|
// sampleRate = 48000
|
||||||
|
// sampleDelta = 4800
|
||||||
|
|
||||||
|
// (1.0 / freq) * sampleRate
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package nl.astraeus.vst.chip
|
|||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
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.midi.Midi
|
||||||
import nl.astraeus.vst.chip.view.MainView
|
import nl.astraeus.vst.chip.view.MainView
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
Komponent.create(document.body!!, MainView)
|
Komponent.create(document.body!!, MainView)
|
||||||
|
|
||||||
Broadcaster.start()
|
Broadcaster.start()
|
||||||
|
|
||||||
|
Midi.start()
|
||||||
}
|
}
|
||||||
|
|||||||
89
src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Midi.kt
Normal file
89
src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Midi.kt
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package nl.astraeus.vst.chip.midi
|
||||||
|
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
||||||
|
import nl.astraeus.vst.chip.view.MainView
|
||||||
|
|
||||||
|
external class MIDIInput {
|
||||||
|
val connection: String
|
||||||
|
val id: String
|
||||||
|
val manufacturer: String
|
||||||
|
val name: String
|
||||||
|
val state: String
|
||||||
|
val type: String
|
||||||
|
val version: String
|
||||||
|
var onmidimessage: (dynamic) -> Unit
|
||||||
|
var onstatechange: (dynamic) -> Unit
|
||||||
|
|
||||||
|
fun open()
|
||||||
|
fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
external class MIDIOutput {
|
||||||
|
val connection: String
|
||||||
|
val id: String
|
||||||
|
val manufacturer: String
|
||||||
|
val name: String
|
||||||
|
val state: String
|
||||||
|
val type: String
|
||||||
|
val version: String
|
||||||
|
|
||||||
|
fun send(message: dynamic)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Midi {
|
||||||
|
var inputs = mutableListOf<MIDIInput>()
|
||||||
|
var outputs = mutableListOf<MIDIInput>()
|
||||||
|
var currentInput: MIDIInput? = null
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
val navigator = window.navigator.asDynamic()
|
||||||
|
|
||||||
|
navigator.requestMIDIAccess().then(
|
||||||
|
{ midiAccess ->
|
||||||
|
val inp = midiAccess.inputs
|
||||||
|
val outp = midiAccess.outputs
|
||||||
|
|
||||||
|
console.log("Midi inputs:", inputs)
|
||||||
|
console.log("Midi outputs:", outputs)
|
||||||
|
|
||||||
|
inp.forEach() { input ->
|
||||||
|
console.log("Midi input:", input)
|
||||||
|
inputs.add(input)
|
||||||
|
console.log("Name: ${(input as? MIDIInput)?.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
outp.forEach() { output ->
|
||||||
|
console.log("Midi output:", output)
|
||||||
|
outputs.add(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
MainView.requestUpdate()
|
||||||
|
},
|
||||||
|
{ e ->
|
||||||
|
println("Failed to get MIDI access - $e")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInput(input: MIDIInput) {
|
||||||
|
console.log("Setting input", input)
|
||||||
|
currentInput?.close()
|
||||||
|
|
||||||
|
currentInput = input
|
||||||
|
|
||||||
|
currentInput?.onstatechange = { message ->
|
||||||
|
console.log("State change:", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentInput?.onmidimessage = { message ->
|
||||||
|
console.log("Midi message:", message)
|
||||||
|
VstChipWorklet.postMessage(
|
||||||
|
message.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentInput?.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,14 +7,19 @@ 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.html.FlowContent
|
||||||
|
import kotlinx.html.P
|
||||||
import kotlinx.html.a
|
import kotlinx.html.a
|
||||||
|
import kotlinx.html.br
|
||||||
import kotlinx.html.classes
|
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.hr
|
||||||
|
import kotlinx.html.js.onChangeFunction
|
||||||
import kotlinx.html.js.onClickFunction
|
import kotlinx.html.js.onClickFunction
|
||||||
import kotlinx.html.js.onMouseDownFunction
|
import kotlinx.html.js.onMouseDownFunction
|
||||||
import kotlinx.html.js.onMouseUpFunction
|
import kotlinx.html.js.onMouseUpFunction
|
||||||
|
import kotlinx.html.option
|
||||||
|
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
|
||||||
@@ -27,7 +32,9 @@ import nl.astraeus.komp.Komponent
|
|||||||
import nl.astraeus.vst.Note
|
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.channel.Broadcaster
|
||||||
|
import nl.astraeus.vst.chip.midi.Midi
|
||||||
import org.khronos.webgl.Int32Array
|
import org.khronos.webgl.Int32Array
|
||||||
|
import org.w3c.dom.HTMLSelectElement
|
||||||
|
|
||||||
object MainView : Komponent() {
|
object MainView : Komponent() {
|
||||||
private var messages: MutableList<String> = ArrayList()
|
private var messages: MutableList<String> = ArrayList()
|
||||||
@@ -68,31 +75,27 @@ object MainView : Komponent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
a {
|
+ "Midi input: "
|
||||||
href = "#"
|
select {
|
||||||
+"Send broadcast test message"
|
for (mi in Midi.inputs) {
|
||||||
onClickFunction = {
|
option {
|
||||||
Broadcaster.send("Test message")
|
+mi.name
|
||||||
}
|
value = mi.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
|
||||||
a {
|
onChangeFunction = { event ->
|
||||||
href = "#"
|
val target = event.target as HTMLSelectElement
|
||||||
+"Note on"
|
val selected = Midi.inputs.find { it.id == target.value }
|
||||||
onClickFunction = {
|
if (selected != null) {
|
||||||
VstChipWorklet.postMessage("test_on")
|
Midi.setInput(selected)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
a {
|
|
||||||
href = "#"
|
|
||||||
+"Note off"
|
|
||||||
onClickFunction = {
|
|
||||||
VstChipWorklet.postMessage("test_off")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
br {}
|
||||||
|
|
||||||
hr {}
|
hr {}
|
||||||
|
|
||||||
repeat(9) {
|
repeat(9) {
|
||||||
|
|||||||
Reference in New Issue
Block a user