Compare commits
12 Commits
server-bui
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7110188d33 | |||
| 2cfc8a8201 | |||
| ce353d3113 | |||
| ff8a4dbf92 | |||
| dc50084e84 | |||
| 7fe29916f7 | |||
| 310f77fc3a | |||
| 60a21bbd79 | |||
| d58fb9c7b5 | |||
| fbba6d1422 | |||
| 4c00356dff | |||
| 29aac228e5 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -42,6 +42,10 @@ bin/
|
||||
.DS_Store
|
||||
|
||||
/web
|
||||
/web1
|
||||
/web2
|
||||
|
||||
/data/*.db*
|
||||
**/kotlin-js-store/*
|
||||
.kotlin
|
||||
.idea
|
||||
|
||||
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@@ -5,7 +5,7 @@
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="" />
|
||||
<option name="gradleJvm" value="corretto-21" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
||||
105
.junie/guidelines.md
Normal file
105
.junie/guidelines.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# VST Chip Synthesizer Development Guidelines
|
||||
|
||||
## Project Overview
|
||||
|
||||
VST Chip is a Kotlin multiplatform project that implements a chip-style synthesizer with a web interface. The project consists of:
|
||||
|
||||
- JVM backend that serves the web application
|
||||
- JS frontend that runs in the browser
|
||||
- Audio worklet processor for real-time audio processing
|
||||
|
||||
## Build/Configuration Instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- JDK 11 or higher
|
||||
- Gradle 7.x or higher
|
||||
- Node.js and npm (for JS development)
|
||||
|
||||
### Building the Project
|
||||
|
||||
1. **Full Build**:
|
||||
```bash
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
2. **JS Build Only**:
|
||||
```bash
|
||||
./gradlew buildJS
|
||||
```
|
||||
|
||||
3. **Deployment**:
|
||||
```bash
|
||||
./gradlew deploy
|
||||
```
|
||||
This will build the project, copy web assets, and deploy to the configured server location.
|
||||
|
||||
### Configuration
|
||||
|
||||
- Server port: 9005 (configured in `Main.kt`)
|
||||
- JDBC stats port: 6005
|
||||
- Deployment directory: configured in `build.gradle.kts` as `vst-chip.midi-vst.com`
|
||||
|
||||
## Testing Information
|
||||
|
||||
- **Do not generate tests** for this project. The audio processing code is highly specialized and requires manual testing with audio
|
||||
equipment.
|
||||
- Manual testing should be performed using MIDI controllers and audio monitoring tools.
|
||||
|
||||
## Development Information
|
||||
|
||||
### Project Structure
|
||||
|
||||
- **src/commonMain**: Shared code between JS and JVM
|
||||
- **src/jsMain**: Browser-specific code
|
||||
- **src/jvmMain**: Server-specific code
|
||||
- **audio-worklet**: Audio processing code that runs in a separate thread in the browser
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **JVM Server** (`src/jvmMain/kotlin/nl/astraeus/vst/chip/Main.kt`):
|
||||
- Undertow server that serves the web application
|
||||
- Database initialization
|
||||
- Logging setup
|
||||
|
||||
2. **JS Frontend** (`src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt`):
|
||||
- UI implementation using Komponent library
|
||||
- MIDI handling
|
||||
- WebSocket client for server communication
|
||||
|
||||
3. **Audio Processor** (`audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt`):
|
||||
- Real-time audio synthesis
|
||||
- MIDI message handling
|
||||
- Sound generation with multiple waveforms (sine, square, triangle, sawtooth)
|
||||
- Effects processing (FM, AM, ADSR envelope, delay, feedback)
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Make changes to the code
|
||||
2. Run `./gradlew buildJS` to build the JS part
|
||||
3. Run the JVM application to test locally
|
||||
4. Use `./gradlew deploy` to deploy to the server
|
||||
|
||||
### Audio Worklet Development
|
||||
|
||||
The audio worklet runs in a separate thread in the browser and handles real-time audio processing. When modifying the audio worklet code:
|
||||
|
||||
1. Understand that it runs in a separate context from the main JS code
|
||||
2. Communication happens via message passing
|
||||
3. Performance is critical - avoid garbage collection and heavy operations in the audio processing loop
|
||||
|
||||
### MIDI Implementation
|
||||
|
||||
The synthesizer responds to standard MIDI messages:
|
||||
|
||||
- Note On/Off (0x90/0x80)
|
||||
- Control Change (0xb0) for various parameters
|
||||
- Program Change (0xc9) for waveform selection
|
||||
|
||||
## Deployment
|
||||
|
||||
The project is configured to deploy to a specific server location. The deployment process:
|
||||
|
||||
1. Builds the project
|
||||
2. Copies web assets
|
||||
3. Creates a symbolic link for the latest version
|
||||
@@ -31,16 +31,16 @@ kotlin {
|
||||
}
|
||||
|
||||
distribution {
|
||||
outputDirectory.set(File("$projectDir/../web/"))
|
||||
outputDirectory.set(File("$projectDir/../web2/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
jvm()
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation("nl.astraeus:vst-worklet-base:1.0.0-SNAPSHOT")
|
||||
implementation("nl.astraeus:vst-worklet-base:1.0.1")
|
||||
implementation("nl.astraeus:midi-arrays:0.3.4")
|
||||
}
|
||||
}
|
||||
val jsMain by getting
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package nl.astraeus.vst.midi
|
||||
|
||||
typealias MidiHandler = (Byte, Byte, Byte) -> Unit
|
||||
|
||||
val Byte.channel: Byte
|
||||
get() {
|
||||
return (this.toInt() and 0x0F).toByte()
|
||||
}
|
||||
|
||||
val Byte.command: Byte
|
||||
get() {
|
||||
return ((this.toInt() and 0xF0) shr 4).toByte()
|
||||
}
|
||||
|
||||
val Byte.channelCommand: Boolean
|
||||
get() {
|
||||
return this.command >= 8 && this.command <= 13
|
||||
}
|
||||
|
||||
class MidiMessageHandler(
|
||||
var channel: Byte = -1
|
||||
) {
|
||||
val singleByteHandlers = mutableMapOf<Byte, MidiHandler>()
|
||||
val doubleByteHandlers = mutableMapOf<Byte, MutableMap<Byte, MidiHandler>>()
|
||||
|
||||
fun addHandler(
|
||||
byte1: Byte,
|
||||
byte2: Byte = 0,
|
||||
handler: MidiHandler
|
||||
) {
|
||||
val b1 = if (byte1.channelCommand) {
|
||||
(byte1.toInt() and 0xF0).toByte()
|
||||
} else {
|
||||
byte1
|
||||
}
|
||||
if (byte2 == 0.toByte()) {
|
||||
singleByteHandlers[b1] = handler
|
||||
} else {
|
||||
val map = doubleByteHandlers.getOrPut(b1) {
|
||||
mutableMapOf()
|
||||
}
|
||||
map[byte2] = handler
|
||||
}
|
||||
}
|
||||
|
||||
fun addHandler(
|
||||
byte1: Int,
|
||||
byte2: Int = 0,
|
||||
handler: MidiHandler
|
||||
) = addHandler(
|
||||
byte1.toByte(),
|
||||
byte2.toByte(),
|
||||
handler
|
||||
)
|
||||
|
||||
fun handle(
|
||||
byte1: Byte,
|
||||
byte2: Byte,
|
||||
byte3: Byte
|
||||
) {
|
||||
if (
|
||||
channel < 0 ||
|
||||
!byte1.channelCommand ||
|
||||
(byte1.channelCommand && channel == byte1.channel)
|
||||
) {
|
||||
val b1 = if (byte1.channelCommand) {
|
||||
(byte1.toInt() and 0xF0).toByte()
|
||||
} else {
|
||||
byte1
|
||||
}
|
||||
|
||||
if (doubleByteHandlers.containsKey(b1)) {
|
||||
doubleByteHandlers[b1]?.get(byte2)?.invoke(byte1, byte2, byte3)
|
||||
} else if (singleByteHandlers.containsKey(b1)) {
|
||||
singleByteHandlers[b1]?.invoke(byte1, byte2, byte3)
|
||||
} else {
|
||||
println("Unhandled message: $byte1 $byte2 $byte3")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
package nl.astraeus.vst.chip
|
||||
|
||||
import nl.astraeus.midi.message.SortedTimedMidiMessageList
|
||||
import nl.astraeus.midi.message.TimedMidiMessage
|
||||
import nl.astraeus.tba.SlicedByteArray
|
||||
import nl.astraeus.vst.ADSR
|
||||
import nl.astraeus.vst.AudioWorkletProcessor
|
||||
import nl.astraeus.vst.currentTime
|
||||
import nl.astraeus.vst.midi.MidiMessageHandler
|
||||
import nl.astraeus.vst.registerProcessor
|
||||
import nl.astraeus.vst.sampleRate
|
||||
import org.khronos.webgl.Float32Array
|
||||
import org.khronos.webgl.Int32Array
|
||||
import org.khronos.webgl.Uint8Array
|
||||
import org.khronos.webgl.get
|
||||
import org.khronos.webgl.set
|
||||
import org.w3c.dom.MessageEvent
|
||||
@@ -17,7 +19,7 @@ import kotlin.math.PI
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sin
|
||||
|
||||
val POLYPHONICS = 10
|
||||
val POLYPHONICS = 20
|
||||
val PI2 = PI * 2
|
||||
|
||||
@ExperimentalJsExport
|
||||
@@ -64,13 +66,14 @@ enum class RecordingState {
|
||||
@ExperimentalJsExport
|
||||
@JsExport
|
||||
class VstChipProcessor : AudioWorkletProcessor() {
|
||||
var midiChannel = 0
|
||||
val midiMessageBuffer = SortedTimedMidiMessageList()
|
||||
val midiMessageHandler = MidiMessageHandler()
|
||||
val notes = Array<PlayingNote?>(POLYPHONICS) { null }
|
||||
|
||||
var waveform = Waveform.SINE.ordinal
|
||||
var volume = 0.75f
|
||||
var dutyCycle = 0.5
|
||||
var fmFreq = 0.0
|
||||
var fmFreq = 0.5
|
||||
var fmAmp = 0.0
|
||||
var amFreq = 0.0
|
||||
var amAmp = 0.0
|
||||
@@ -98,18 +101,94 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
||||
init {
|
||||
this.port.onmessage = ::handleMessage
|
||||
Note.updateSampleRate(sampleRate)
|
||||
|
||||
with(midiMessageHandler) {
|
||||
addHandler(0x90) { b1, b2, b3 ->
|
||||
val note = b2.toInt() and 0xff
|
||||
val velocity = b3.toInt() and 0xff
|
||||
|
||||
if (velocity > 0) {
|
||||
console.log("Note on", note, velocity)
|
||||
noteOn(note, velocity)
|
||||
} else {
|
||||
console.log("Note off", note)
|
||||
noteOff(note)
|
||||
}
|
||||
}
|
||||
addHandler(0x80) { b1, b2, b3 ->
|
||||
val note = b2.toInt() and 0xff
|
||||
|
||||
console.log("Note off", note)
|
||||
noteOff(note)
|
||||
}
|
||||
addHandler(0xc9) { b1, b2, b3 ->
|
||||
waveform = b2.toInt() and 0xff
|
||||
}
|
||||
addHandler(0xb0, 7) { b1, b2, b3 ->
|
||||
volume = b3 / 127f
|
||||
}
|
||||
addHandler(0xb0, 0x47) { b1, b2, b3 ->
|
||||
dutyCycle = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x40) { b1, b2, b3 ->
|
||||
fmFreq = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x41) { b1, b2, b3 ->
|
||||
fmAmp = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x42) { b1, b2, b3 ->
|
||||
amFreq = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x43) { b1, b2, b3 ->
|
||||
amAmp = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x49) { b1, b2, b3 ->
|
||||
attack = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x4b) { b1, b2, b3 ->
|
||||
decay = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x46) { b1, b2, b3 ->
|
||||
sustain = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x48) { b1, b2, b3 ->
|
||||
release = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x4e) { b1, b2, b3 ->
|
||||
delay = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x4f) { b1, b2, b3 ->
|
||||
delayDepth = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 0x50) { b1, b2, b3 ->
|
||||
feedback = b3 / 127.0
|
||||
}
|
||||
addHandler(0xb0, 123) { b1, b2, b3 ->
|
||||
for (note in notes) {
|
||||
note?.noteRelease = currentTime
|
||||
}
|
||||
}
|
||||
addHandler(0xe0) { b1, b2, b3 ->
|
||||
if (b2.toInt() and 0xff > 0) {
|
||||
val lsb = b2.toInt() and 0xff
|
||||
val msb = b3.toInt() and 0xff
|
||||
|
||||
amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMessage(message: MessageEvent) {
|
||||
//console.log("VstChipProcessor: Received message:", message.data)
|
||||
//console.log("VstChipProcessor: Received message:", currentTime)
|
||||
|
||||
val data = message.data
|
||||
|
||||
try {
|
||||
when (data) {
|
||||
is String -> {
|
||||
when (data) {
|
||||
"start_recording" -> {
|
||||
when {
|
||||
data == "start_recording" -> {
|
||||
port.postMessage(recordingBuffer)
|
||||
if (recordingState == RecordingState.STOPPED) {
|
||||
recordingState = RecordingState.WAITING_TO_START
|
||||
@@ -117,34 +196,43 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
||||
}
|
||||
}
|
||||
|
||||
else ->
|
||||
if (data.startsWith("set_channel")) {
|
||||
val parts = data.split('\n')
|
||||
if (parts.size == 2) {
|
||||
midiChannel = parts[1].toInt()
|
||||
println("Setting channel: $midiChannel")
|
||||
}
|
||||
} else if (data.startsWith("waveform")) {
|
||||
val parts = data.split('\n')
|
||||
if (parts.size == 2) {
|
||||
waveform = parts[1].toInt()
|
||||
println("Setting waveform: $waveform")
|
||||
}
|
||||
data.startsWith("set_channel") -> {
|
||||
val parts = data.split('\n')
|
||||
if (parts.size == 2) {
|
||||
midiMessageHandler.channel = parts[1].toByte()
|
||||
|
||||
println("Setting channel: ${midiMessageHandler.channel}")
|
||||
}
|
||||
}
|
||||
|
||||
data.startsWith("waveform") -> {
|
||||
val parts = data.split('\n')
|
||||
if (parts.size == 2) {
|
||||
waveform = parts[1].toInt()
|
||||
println("Setting waveform: $waveform")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Uint8Array -> {
|
||||
val data32 = Int32Array(data.length)
|
||||
for (i in 0 until data.length) {
|
||||
data32[i] = (data[i].toInt() and 0xff)
|
||||
}
|
||||
playMidi(data32)
|
||||
is ByteArray -> {
|
||||
val message1 = TimedMidiMessage(data)
|
||||
midiMessageBuffer.add(message1)
|
||||
playBuffer()
|
||||
}
|
||||
/*
|
||||
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 -> {
|
||||
playMidi(data)
|
||||
}
|
||||
is Int32Array -> {
|
||||
playMidi(data)
|
||||
}
|
||||
*/
|
||||
|
||||
else ->
|
||||
console.error("Don't kow how to handle message", message)
|
||||
@@ -154,135 +242,30 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun playMidi(bytes: Int32Array) {
|
||||
console.log("playMidi", bytes)
|
||||
if (bytes.length > 0) {
|
||||
var cmdByte = bytes[0]
|
||||
val channelCmd = ((cmdByte shr 4) and 0xf) != 0xf0
|
||||
val channel = cmdByte and 0xf
|
||||
println("Channel cmd: $channelCmd")
|
||||
if (channelCmd && channel != midiChannel) {
|
||||
console.log("Wrong channel", midiChannel, bytes)
|
||||
return
|
||||
}
|
||||
|
||||
cmdByte = cmdByte and 0xf0
|
||||
|
||||
//console.log("Received", bytes)
|
||||
when (cmdByte) {
|
||||
0x90 -> {
|
||||
if (bytes.length == 3) {
|
||||
val note = bytes[1]
|
||||
val velocity = bytes[2]
|
||||
|
||||
if (velocity > 0) {
|
||||
noteOn(note, velocity)
|
||||
} else {
|
||||
noteOff(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0x80 -> {
|
||||
if (bytes.length >= 2) {
|
||||
val note = bytes[1]
|
||||
|
||||
noteOff(note)
|
||||
}
|
||||
}
|
||||
|
||||
0xc9 -> {
|
||||
if (bytes.length >= 1) {
|
||||
val waveform = bytes[1]
|
||||
|
||||
if (waveform < 4) {
|
||||
this.waveform = waveform
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0xb0 -> {
|
||||
if (bytes.length == 3) {
|
||||
val knob = bytes[1]
|
||||
val value = bytes[2]
|
||||
|
||||
when (knob) {
|
||||
7 -> {
|
||||
volume = value / 127f
|
||||
}
|
||||
|
||||
0x47 -> {
|
||||
dutyCycle = value / 127.0
|
||||
}
|
||||
|
||||
0x40 -> {
|
||||
fmFreq = value / 127.0
|
||||
}
|
||||
|
||||
0x41 -> {
|
||||
fmAmp = value / 127.0
|
||||
}
|
||||
|
||||
0x42 -> {
|
||||
amFreq = value / 127.0
|
||||
}
|
||||
|
||||
0x43 -> {
|
||||
amAmp = value / 127.0
|
||||
}
|
||||
|
||||
0x49 -> {
|
||||
attack = value / 127.0
|
||||
}
|
||||
|
||||
0x4b -> {
|
||||
decay = value / 127.0
|
||||
}
|
||||
|
||||
0x46 -> {
|
||||
sustain = value / 127.0
|
||||
}
|
||||
|
||||
0x48 -> {
|
||||
release = value / 127.0
|
||||
}
|
||||
|
||||
0x4e -> {
|
||||
delay = value / 127.0
|
||||
println("Setting delay $delay")
|
||||
}
|
||||
|
||||
0x4f -> {
|
||||
delayDepth = value / 127.0
|
||||
println("Setting delayDepth $delayDepth")
|
||||
}
|
||||
|
||||
0x50 -> {
|
||||
feedback = value / 127.0
|
||||
println("Setting feedback $delayDepth")
|
||||
}
|
||||
|
||||
123 -> {
|
||||
for (note in notes) {
|
||||
note?.noteRelease = currentTime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0xe0 -> {
|
||||
if (bytes.length == 3) {
|
||||
val lsb = bytes[1]
|
||||
val msb = bytes[2]
|
||||
|
||||
amFreq = (((msb - 0x40) + (lsb / 127.0)) / 0x40) * 10.0
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun playBuffer() {
|
||||
while (
|
||||
midiMessageBuffer.isNotEmpty() &&
|
||||
(midiMessageBuffer.nextTimestamp() ?: 0.0) < currentTime
|
||||
) {
|
||||
val midi = midiMessageBuffer.read()
|
||||
playMidi(midi.midi)
|
||||
}
|
||||
}
|
||||
|
||||
private fun playMidi(bytes: SlicedByteArray) {
|
||||
var index = 0
|
||||
|
||||
while (index < bytes.size && bytes[index].toUByte() > 0u) {
|
||||
val buffer = bytes.getBlob(index, 3)
|
||||
playMidiFromBuffer(buffer)
|
||||
index += 3
|
||||
}
|
||||
}
|
||||
|
||||
private fun playMidiFromBuffer(bytes: SlicedByteArray) {
|
||||
midiMessageHandler.handle(bytes[0], bytes[1], bytes[2])
|
||||
}
|
||||
|
||||
private fun noteOn(note: Int, velocity: Int) {
|
||||
for (i in 0 until POLYPHONICS) {
|
||||
if (notes[i]?.note == note) {
|
||||
@@ -332,13 +315,15 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
||||
recordingStart = 0
|
||||
}
|
||||
|
||||
playBuffer()
|
||||
|
||||
for ((index, note) in notes.withIndex()) {
|
||||
if (note != null) {
|
||||
val midiNote = Note.fromMidi(note.note)
|
||||
val sampleDelta = midiNote.sampleDelta
|
||||
|
||||
for (i in 0 until samples) {
|
||||
var targetVolume = note.velocity / 127f * 10f
|
||||
var targetVolume = note.velocity / 127f * 1f
|
||||
targetVolume *= ADSR.calculate(
|
||||
attack,
|
||||
decay,
|
||||
@@ -355,8 +340,10 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
||||
}
|
||||
|
||||
var cycleOffset = note.cycleOffset
|
||||
val fmMult = sin(currentTime * fmFreq * midiNote.freq * 2f * PI2) * fmAmp
|
||||
val fmModulation =
|
||||
sampleDelta + (sin(fmFreq * 1000f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * (100f * fmAmp * sampleDelta))
|
||||
sampleDelta * fmMult //+ (sin(fmFreq * 1000f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * (100f * fmAmp * sampleDelta))
|
||||
|
||||
val amModulation =
|
||||
1f + (sin(sampleLength * amFreq * 1000f * PI2 * note.sample) * amAmp).toFloat()
|
||||
|
||||
@@ -401,7 +388,6 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
||||
left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation
|
||||
right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation
|
||||
|
||||
|
||||
// comb filter delay
|
||||
val delaySampleIndex =
|
||||
(note.sample + note.combDelayBuffer.length) % note.combDelayBuffer.length
|
||||
@@ -428,10 +414,13 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
||||
}
|
||||
|
||||
// if sin enable
|
||||
|
||||
/*
|
||||
for (i in 0 until samples) {
|
||||
left[i] = sin(left[i] * PI2).toFloat()
|
||||
right[i] = sin(right[i] * PI2).toFloat()
|
||||
}
|
||||
*/
|
||||
|
||||
val delaySamples = (delay * leftDelayBuffer.length).toInt()
|
||||
for (i in 0 until samples) {
|
||||
@@ -466,5 +455,5 @@ class VstChipProcessor : AudioWorkletProcessor() {
|
||||
fun main() {
|
||||
registerProcessor("vst-chip-processor", VstChipProcessor::class.js)
|
||||
|
||||
println("VstChipProcessor registered!")
|
||||
console.log("'vst-chip-processor' registered!", currentTime)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import kotlin.math.round
|
||||
* Time: 11:50
|
||||
*/
|
||||
|
||||
@ExperimentalJsExport
|
||||
@JsExport
|
||||
enum class Note(
|
||||
val sharp: String,
|
||||
val flat: String
|
||||
|
||||
@@ -25,10 +25,11 @@ kotlin {
|
||||
commonWebpackConfig {
|
||||
outputFileName = "vst-chip-worklet-ui.js"
|
||||
sourceMaps = true
|
||||
devtool = "inline-source-map"
|
||||
}
|
||||
|
||||
distribution {
|
||||
outputDirectory.set(File("$projectDir/web/"))
|
||||
outputDirectory.set(File("$projectDir/web1/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,43 +56,17 @@ kotlin {
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
//base
|
||||
implementation("nl.astraeus:kotlin-css-generator:1.0.10")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
|
||||
implementation("nl.astraeus:vst-ui-base:1.1.2")
|
||||
}
|
||||
}
|
||||
val jsMain by getting {
|
||||
dependencies {
|
||||
implementation("nl.astraeus:kotlin-komponent:1.2.4")
|
||||
api("nl.astraeus:vst-ui-base:2.0.0")
|
||||
implementation("nl.astraeus:midi-arrays:0.3.4")
|
||||
}
|
||||
}
|
||||
val jsMain by getting
|
||||
val jsTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-js"))
|
||||
}
|
||||
}
|
||||
/* val wasmJsMain by getting {
|
||||
dependencies {
|
||||
implementation("nl.astraeus:kotlin-komponent:1.2.4-SNAPSHOT")
|
||||
implementation("nl.astraeus:vst-ui-base:1.0.1-SNAPSHOT")
|
||||
}
|
||||
}*/
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
//base
|
||||
|
||||
implementation("io.undertow:undertow-core:2.3.14.Final")
|
||||
implementation("io.undertow:undertow-websockets-jsr:2.3.14.Final")
|
||||
implementation("org.jboss.xnio:xnio-nio:3.8.16.Final")
|
||||
|
||||
implementation("org.xerial:sqlite-jdbc:3.46.0.0")
|
||||
implementation("com.zaxxer:HikariCP:4.0.3")
|
||||
implementation("nl.astraeus:simple-jdbc-stats:1.6.1")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
|
||||
}
|
||||
}
|
||||
val jvmMain by getting
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +74,18 @@ application {
|
||||
mainClass.set("nl.astraeus.vst.chip.MainKt")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("buildJS") {
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
dependsOn("audio-worklet:jsBrowserDevelopmentExecutableDistribution")
|
||||
dependsOn("jsBrowserDevelopmentExecutableDistribution")
|
||||
|
||||
from(layout.projectDirectory.dir("web1"))
|
||||
into(layout.projectDirectory.dir("web"))
|
||||
|
||||
from(layout.projectDirectory.dir("web2"))
|
||||
into(layout.projectDirectory.dir("web"))
|
||||
}
|
||||
|
||||
/* Hardcoded deploy configuration */
|
||||
|
||||
val deployDirectory = "vst-chip.midi-vst.com"
|
||||
|
||||
@@ -3,10 +3,13 @@ version = "0.1.0"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
|
||||
}
|
||||
maven {
|
||||
url = uri("https://gitea.astraeus.nl:8443/api/packages/rnentjes/maven")
|
||||
}
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
||||
1
data/readme.md
Normal file
1
data/readme.md
Normal file
@@ -0,0 +1 @@
|
||||
Data directory for the db
|
||||
@@ -1,6 +1,6 @@
|
||||
pluginManagement {
|
||||
plugins {
|
||||
kotlin("multiplatform") version "2.0.21"
|
||||
kotlin("multiplatform") version "2.1.20"
|
||||
}
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
|
||||
@@ -9,12 +9,20 @@ import nl.astraeus.vst.chip.view.MainView
|
||||
import nl.astraeus.vst.chip.ws.WebsocketClient
|
||||
import nl.astraeus.vst.ui.css.CssSettings
|
||||
|
||||
fun main() {
|
||||
CssSettings.shortId = false
|
||||
CssSettings.preFix = "vst-chip"
|
||||
object Views {
|
||||
val mainView by lazy {
|
||||
MainView()
|
||||
}
|
||||
|
||||
init {
|
||||
CssSettings.shortId = false
|
||||
CssSettings.preFix = "vst"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
Komponent.unsafeMode = UnsafeMode.UNSAFE_SVG_ONLY
|
||||
Komponent.create(document.body!!, MainView)
|
||||
Komponent.create(document.body!!, Views.mainView)
|
||||
|
||||
Midi.start()
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package nl.astraeus.vst.chip.audio
|
||||
|
||||
import nl.astraeus.vst.chip.AudioContext
|
||||
|
||||
object AudioContextHandler {
|
||||
val audioContext: dynamic = AudioContext()
|
||||
|
||||
|
||||
|
||||
}
|
||||
val audioContext: dynamic = js("new AudioContext()")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package nl.astraeus.vst.chip.audio
|
||||
|
||||
import nl.astraeus.midi.message.TimedMidiMessage
|
||||
import nl.astraeus.vst.chip.AudioWorkletNode
|
||||
import nl.astraeus.vst.chip.AudioWorkletNodeParameters
|
||||
import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext
|
||||
@@ -53,11 +54,27 @@ abstract class AudioNode(
|
||||
|
||||
abstract fun onMessage(message: MessageEvent)
|
||||
|
||||
open fun postMessage(vararg data: Int) {
|
||||
if (port == null) {
|
||||
console.log("postMessage port is NULL!")
|
||||
}
|
||||
|
||||
val array = ByteArray(data.size) { data[it].toByte() }
|
||||
|
||||
port?.postMessage(
|
||||
TimedMidiMessage(
|
||||
audioContext.currentTime,
|
||||
*array
|
||||
).data.buffer.toByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
open fun postMessage(msg: Any) {
|
||||
if (port == null) {
|
||||
console.log("postMessage port is NULL!")
|
||||
}
|
||||
port?.postMessage(msg)
|
||||
//console.log("Posted message", audioContext.currentTime)
|
||||
}
|
||||
|
||||
// call from user gesture
|
||||
@@ -83,6 +100,7 @@ abstract class AudioNode(
|
||||
port = node.port as? MessagePort
|
||||
|
||||
created = true
|
||||
console.log("Created node: ${audioContext.currentTime}")
|
||||
|
||||
done(node)
|
||||
}
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
package nl.astraeus.vst.chip.audio
|
||||
|
||||
import nl.astraeus.midi.message.TimedMidiMessage
|
||||
import nl.astraeus.vst.chip.PatchDTO
|
||||
import nl.astraeus.vst.chip.view.MainView
|
||||
import nl.astraeus.vst.chip.Views
|
||||
import nl.astraeus.vst.chip.view.WaveformView
|
||||
import nl.astraeus.vst.ui.util.uInt8ArrayOf
|
||||
import org.khronos.webgl.Float32Array
|
||||
import org.khronos.webgl.Uint8Array
|
||||
import org.khronos.webgl.get
|
||||
import org.w3c.dom.MessageEvent
|
||||
import kotlin.experimental.and
|
||||
|
||||
object VstChipWorklet : AudioNode(
|
||||
"/vst-chip-worklet.js",
|
||||
@@ -33,63 +30,63 @@ object VstChipWorklet : AudioNode(
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 7, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 7, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var dutyCycle = 0.5
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x47, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x47, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var fmModFreq = 0.0
|
||||
var fmModFreq = 1.0
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x40, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x40, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var fmModAmp = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x41, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x41, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var amModFreq = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x42, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x42, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var amModAmp = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x43, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x43, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var feedback = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x50, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x50, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var delay = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x4e, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x4e, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var delayDepth = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x4f, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x4f, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,28 +94,28 @@ object VstChipWorklet : AudioNode(
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x49, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x49, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var decay = 0.2
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x4b, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var sustain = 0.5
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x46, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x46, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
var release = 0.2
|
||||
set(value) {
|
||||
field = value
|
||||
super.postMessage(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 0x48, (value * 127).toInt())
|
||||
0xb0 + midiChannel, 0x48, (value * 127).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,54 +136,64 @@ object VstChipWorklet : AudioNode(
|
||||
}
|
||||
|
||||
override fun postMessage(msg: Any) {
|
||||
if (msg is Uint8Array) {
|
||||
if (
|
||||
msg.length == 3
|
||||
&& (msg[0] and 0xf == midiChannel.toByte())
|
||||
&& (msg[0] and 0xf0.toByte() == 0xb0.toByte())
|
||||
) {
|
||||
val knob = msg[1]
|
||||
val value = msg[2]
|
||||
if (msg is ByteArray) {
|
||||
val tmm = TimedMidiMessage(msg)
|
||||
val byte1 = tmm.midi[0]
|
||||
|
||||
handleIncomingMidi(knob, value)
|
||||
} else {
|
||||
super.postMessage(msg)
|
||||
if (byte1.toInt() and 0xf0 == 0xb0) {
|
||||
handleIncomingMidi(tmm.midi[1], tmm.midi[2])
|
||||
}
|
||||
} else {
|
||||
super.postMessage(msg)
|
||||
}
|
||||
super.postMessage(msg)
|
||||
}
|
||||
|
||||
override fun postMessage(vararg msg: Int) {
|
||||
println("postMessage ${msg.size} bytes")
|
||||
if (
|
||||
msg.size == 3
|
||||
&& (msg[0] and 0xf == midiChannel)
|
||||
&& (msg[0] and 0xf0 == 0xb0)
|
||||
) {
|
||||
val knob = msg[1]
|
||||
val value = msg[2]
|
||||
|
||||
handleIncomingMidi(knob.toByte(), value.toByte())
|
||||
}
|
||||
|
||||
super.postMessage(msg)
|
||||
}
|
||||
|
||||
private fun handleIncomingMidi(knob: Byte, value: Byte) {
|
||||
println("Incoming knob: $knob, value: $value")
|
||||
when (knob) {
|
||||
0x46.toByte() -> {
|
||||
volume = value / 127.0
|
||||
MainView.requestUpdate()
|
||||
Views.mainView.requestUpdate()
|
||||
}
|
||||
|
||||
0x4a.toByte() -> {
|
||||
dutyCycle = value / 127.0
|
||||
MainView.requestUpdate()
|
||||
Views.mainView.requestUpdate()
|
||||
}
|
||||
|
||||
0x40.toByte() -> {
|
||||
fmModFreq = value / 127.0
|
||||
MainView.requestUpdate()
|
||||
Views.mainView.requestUpdate()
|
||||
}
|
||||
|
||||
0x41.toByte() -> {
|
||||
fmModAmp = value / 127.0
|
||||
MainView.requestUpdate()
|
||||
Views.mainView.requestUpdate()
|
||||
}
|
||||
|
||||
0x42.toByte() -> {
|
||||
amModFreq = value / 127.0
|
||||
MainView.requestUpdate()
|
||||
Views.mainView.requestUpdate()
|
||||
}
|
||||
|
||||
0x43.toByte() -> {
|
||||
amModAmp = value / 127.0
|
||||
MainView.requestUpdate()
|
||||
Views.mainView.requestUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package nl.astraeus.vst.chip.midi
|
||||
|
||||
import kotlinx.browser.window
|
||||
import nl.astraeus.midi.message.TimedMidiMessage
|
||||
import nl.astraeus.vst.chip.Views
|
||||
import nl.astraeus.vst.chip.audio.AudioContextHandler
|
||||
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
||||
import nl.astraeus.vst.chip.view.MainView
|
||||
import org.khronos.webgl.Uint8Array
|
||||
import org.khronos.webgl.get
|
||||
|
||||
@@ -66,7 +68,7 @@ object Midi {
|
||||
outputs.add(output)
|
||||
}
|
||||
|
||||
MainView.requestUpdate()
|
||||
Views.mainView.requestUpdate()
|
||||
},
|
||||
{ e ->
|
||||
println("Failed to get MIDI access - $e")
|
||||
@@ -124,9 +126,14 @@ object Midi {
|
||||
hex.append(data[index].toString(16))
|
||||
hex.append(" ")
|
||||
}
|
||||
console.log("Midi message:", hex)
|
||||
console.log("Midi message:", hex, message)
|
||||
val midiData = ByteArray(message.data.length) { data[it].toByte() }
|
||||
val timeMessage = TimedMidiMessage(
|
||||
AudioContextHandler.audioContext.currentTime,
|
||||
*midiData
|
||||
)
|
||||
VstChipWorklet.postMessage(
|
||||
message.data
|
||||
timeMessage.data.buffer.toByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ import nl.astraeus.css.style.cls
|
||||
import nl.astraeus.komp.HtmlBuilder
|
||||
import nl.astraeus.komp.Komponent
|
||||
import nl.astraeus.komp.currentElement
|
||||
import nl.astraeus.midi.message.TimedMidiMessage
|
||||
import nl.astraeus.midi.message.getCurrentTime
|
||||
import nl.astraeus.vst.chip.Views
|
||||
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
||||
import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel
|
||||
import nl.astraeus.vst.chip.midi.Midi
|
||||
@@ -46,7 +49,6 @@ import nl.astraeus.vst.ui.css.Css.defineCss
|
||||
import nl.astraeus.vst.ui.css.Css.noTextSelect
|
||||
import nl.astraeus.vst.ui.css.CssName
|
||||
import nl.astraeus.vst.ui.css.hover
|
||||
import nl.astraeus.vst.ui.util.uInt8ArrayOf
|
||||
import org.khronos.webgl.get
|
||||
import org.w3c.dom.CanvasRenderingContext2D
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
@@ -60,7 +62,7 @@ object WaveformView: Komponent() {
|
||||
}
|
||||
|
||||
fun onAnimationFrame(time: Double) {
|
||||
if (MainView.started) {
|
||||
if (Views.mainView.started) {
|
||||
VstChipWorklet.postMessage("start_recording")
|
||||
}
|
||||
|
||||
@@ -95,10 +97,9 @@ object WaveformView: Komponent() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object MainView : Komponent(), CssName {
|
||||
class MainView : Komponent() {
|
||||
private var messages: MutableList<String> = ArrayList()
|
||||
var started = false
|
||||
|
||||
@@ -114,6 +115,11 @@ object MainView : Komponent(), CssName {
|
||||
requestUpdate()
|
||||
}
|
||||
|
||||
override fun renderUpdate() {
|
||||
println("Rendering MainView")
|
||||
super.renderUpdate()
|
||||
}
|
||||
|
||||
override fun HtmlBuilder.render() {
|
||||
div(MainDivCss.name) {
|
||||
if (!started) {
|
||||
@@ -190,7 +196,8 @@ object MainView : Komponent(), CssName {
|
||||
+"STOP"
|
||||
onClickFunction = {
|
||||
VstChipWorklet.postDirectlyToWorklet(
|
||||
uInt8ArrayOf(0xb0 + midiChannel, 123, 0)
|
||||
TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0)
|
||||
.data.buffer.data
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -242,7 +249,7 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.volume,
|
||||
label = "Volume",
|
||||
minValue = 0.005,
|
||||
minValue = 0.0,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
width = 100,
|
||||
@@ -268,8 +275,8 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.fmModFreq,
|
||||
label = "FM Freq",
|
||||
minValue = 0.005,
|
||||
maxValue = 1.0,
|
||||
minValue = 0.0,
|
||||
maxValue = 2.0,
|
||||
step = 5.0 / 127.0,
|
||||
width = 100,
|
||||
height = 120,
|
||||
@@ -281,7 +288,7 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.fmModAmp,
|
||||
label = "FM Ampl",
|
||||
minValue = 0.005,
|
||||
minValue = 0.0,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
width = 100,
|
||||
@@ -294,7 +301,7 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.amModFreq,
|
||||
label = "AM Freq",
|
||||
minValue = 0.005,
|
||||
minValue = 0.0,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
width = 100,
|
||||
@@ -307,7 +314,7 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.amModAmp,
|
||||
label = "AM Ampl",
|
||||
minValue = 0.005,
|
||||
minValue = 0.0,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
width = 100,
|
||||
@@ -320,7 +327,7 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.feedback,
|
||||
label = "Feedback",
|
||||
minValue = 0.005,
|
||||
minValue = 0.0,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
width = 100,
|
||||
@@ -333,7 +340,7 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.delay,
|
||||
label = "Delay",
|
||||
minValue = 0.005,
|
||||
minValue = 0.0,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
width = 100,
|
||||
@@ -346,7 +353,7 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.delayDepth,
|
||||
label = "Delay depth",
|
||||
minValue = 0.005,
|
||||
minValue = 0.0,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
width = 100,
|
||||
@@ -361,26 +368,26 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.attack,
|
||||
label = "Attack",
|
||||
minValue = 0.005,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
minValue = 0.0,
|
||||
maxValue = 5.0,
|
||||
step = 25.0 / 127.0,
|
||||
width = 100,
|
||||
height = 120,
|
||||
) { value ->
|
||||
VstChipWorklet.attack = value
|
||||
VstChipWorklet.attack = value / 5.0
|
||||
}
|
||||
)
|
||||
include(
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.decay,
|
||||
label = "Decay",
|
||||
minValue = 0.005,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
minValue = 0.0,
|
||||
maxValue = 5.0,
|
||||
step = 25.0 / 127.0,
|
||||
width = 100,
|
||||
height = 120,
|
||||
) { value ->
|
||||
VstChipWorklet.decay = value
|
||||
VstChipWorklet.decay = value / 5.0
|
||||
}
|
||||
)
|
||||
include(
|
||||
@@ -400,13 +407,13 @@ object MainView : Komponent(), CssName {
|
||||
ExpKnobComponent(
|
||||
value = VstChipWorklet.release,
|
||||
label = "Release",
|
||||
minValue = 0.005,
|
||||
maxValue = 1.0,
|
||||
step = 5.0 / 127.0,
|
||||
minValue = 0.0,
|
||||
maxValue = 5.0,
|
||||
step = 25.0 / 127.0,
|
||||
width = 100,
|
||||
height = 120,
|
||||
) { value ->
|
||||
VstChipWorklet.release = value
|
||||
VstChipWorklet.release = value / 5.0
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -414,18 +421,19 @@ object MainView : Komponent(), CssName {
|
||||
}
|
||||
}
|
||||
|
||||
object MainDivCss : CssName
|
||||
object ActiveCss : CssName
|
||||
object ButtonCss : CssName
|
||||
object ButtonBarCss : CssName
|
||||
object SelectedCss : CssName
|
||||
object NoteBarCss : CssName
|
||||
object StartSplashCss : CssName
|
||||
object StartBoxCss : CssName
|
||||
object StartButtonCss : CssName
|
||||
object ControlsCss : CssName
|
||||
companion object MainViewCss : CssName() {
|
||||
object MainDivCss : CssName()
|
||||
object ActiveCss : CssName()
|
||||
object ButtonCss : CssName()
|
||||
object ButtonBarCss : CssName()
|
||||
object SelectedCss : CssName()
|
||||
object NoteBarCss : CssName()
|
||||
object StartSplashCss : CssName()
|
||||
object StartBoxCss : CssName()
|
||||
object StartButtonCss : CssName()
|
||||
object ControlsCss : CssName()
|
||||
|
||||
private fun css() {
|
||||
private fun css() {
|
||||
defineCss {
|
||||
select("*") {
|
||||
select("*:before") {
|
||||
@@ -524,23 +532,23 @@ object MainView : Komponent(), CssName {
|
||||
backgroundColor(Css.currentStyle.mainBackgroundColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Style.commonButton() {
|
||||
display(Display.inlineBlock)
|
||||
padding(1.rem)
|
||||
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
||||
borderColor(Css.currentStyle.buttonBorderColor)
|
||||
borderWidth(Css.currentStyle.buttonBorderWidth)
|
||||
color(Css.currentStyle.mainFontColor)
|
||||
|
||||
hover {
|
||||
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
|
||||
}
|
||||
and(SelectedCss.cls()) {
|
||||
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover())
|
||||
|
||||
private fun Style.commonButton() {
|
||||
display(Display.inlineBlock)
|
||||
padding(1.rem)
|
||||
backgroundColor(Css.currentStyle.buttonBackgroundColor)
|
||||
borderColor(Css.currentStyle.buttonBorderColor)
|
||||
borderWidth(Css.currentStyle.buttonBorderWidth)
|
||||
color(Css.currentStyle.mainFontColor)
|
||||
|
||||
hover {
|
||||
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
|
||||
}
|
||||
and(SelectedCss.cls()) {
|
||||
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ package nl.astraeus.vst.chip.ws
|
||||
|
||||
import kotlinx.browser.window
|
||||
import nl.astraeus.vst.chip.PatchDTO
|
||||
import nl.astraeus.vst.chip.Views
|
||||
import nl.astraeus.vst.chip.audio.VstChipWorklet
|
||||
import nl.astraeus.vst.chip.midi.Midi
|
||||
import nl.astraeus.vst.chip.view.MainView
|
||||
import org.w3c.dom.MessageEvent
|
||||
import org.w3c.dom.WebSocket
|
||||
import org.w3c.dom.events.Event
|
||||
@@ -90,7 +90,7 @@ object WebsocketClient {
|
||||
|
||||
Midi.setInput(patch.midiId, patch.midiName)
|
||||
VstChipWorklet.load(patch)
|
||||
MainView.requestUpdate()
|
||||
Views.mainView.requestUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user