From 8df6a4fff6a43fd71daf1287db442a5cc8c439ef Mon Sep 17 00:00:00 2001 From: rnentjes Date: Sat, 13 Jul 2024 16:44:33 +0200 Subject: [PATCH] Also search on name when setting midi port --- .../audio_worklet_js_1_0_0_SNAPSHOT.xml | 2 +- .../audio_worklet_jvm_1_0_0_SNAPSHOT.xml | 2 +- .idea/artifacts/common_js_1_0_0_SNAPSHOT.xml | 2 +- .idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml | 2 +- .run/Main [jvm].run.xml | 2 +- audio-worklet/build.gradle.kts | 4 +- .../nl/astraeus/vst/chip/ChipProcessor.kt | 393 ---- .../nl/astraeus/vst/string/StringProcessor.kt | 264 +++ build.gradle.kts | 2 +- .../commonMain/kotlin/nl/astraeus/vst/Note.kt | 4 + .../nl/astraeus/vst/string/PhysicalString.kt | 80 + .../astraeus/vst/string/PhysicalString.js.kt | 5 + .../astraeus/vst/string/PhysicalString.jvm.kt | 5 + settings.gradle.kts | 2 +- .../kotlin/nl/astraeus/vst/chip/PatchDTO.kt | 34 - .../kotlin/nl/astraeus/vst/string/PatchDTO.kt | 24 + .../vst/{chip => string}/logger/Logger.kt | 2 +- .../vst/chip/audio/AudioContextHandler.kt | 10 - .../astraeus/vst/chip/audio/VstChipWorklet.kt | 203 --- .../vst/{chip => string}/Externals.kt | 2 +- .../nl/astraeus/vst/{chip => string}/Main.kt | 10 +- .../vst/string/audio/AudioContextHandler.kt | 10 + .../vst/{chip => string}/audio/AudioModule.kt | 8 +- .../vst/string/audio/VstStringWorklet.kt | 123 ++ .../vst/{chip => string}/midi/Broadcaster.kt | 2 +- .../vst/{chip => string}/midi/Midi.kt | 8 +- .../vst/{chip => string}/view/MainView.kt | 257 +-- .../vst/string/view/PhysicalStringView.kt | 109 ++ .../astraeus/vst/string/view/WaveformView.kt | 56 + .../{chip => string}/ws/WebsocketClient.kt | 14 +- src/jsMain/resources/index.html | 6 - src/jvmMain/java/BarWavesCanvas.java | 1622 +++++++++++++++++ .../vst/{chip => string}/GenerateId.kt | 2 +- .../nl/astraeus/vst/{chip => string}/Main.kt | 10 +- .../astraeus/vst/{chip => string}/Settings.kt | 4 +- .../vst/{chip => string}/db/BaseDao.kt | 4 +- .../vst/{chip => string}/db/Database.kt | 2 +- .../vst/{chip => string}/db/Entity.kt | 2 +- .../vst/{chip => string}/db/Migrations.kt | 4 +- .../vst/{chip => string}/db/PatchDao.kt | 2 +- .../vst/{chip => string}/db/PatchEntity.kt | 2 +- .../db/PatchEntityQueryProvider.kt | 2 +- .../vst/{chip => string}/web/Index.kt | 6 +- .../{chip => string}/web/RequestHandler.kt | 10 +- .../vst/{chip => string}/web/Session.kt | 2 +- 45 files changed, 2387 insertions(+), 934 deletions(-) delete mode 100644 audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt create mode 100644 audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/string/StringProcessor.kt create mode 100644 common/src/commonMain/kotlin/nl/astraeus/vst/string/PhysicalString.kt create mode 100644 common/src/jsMain/kotlin/nl/astraeus/vst/string/PhysicalString.js.kt create mode 100644 common/src/jvmMain/kotlin/nl/astraeus/vst/string/PhysicalString.jvm.kt delete mode 100644 src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt create mode 100644 src/commonMain/kotlin/nl/astraeus/vst/string/PatchDTO.kt rename src/commonMain/kotlin/nl/astraeus/vst/{chip => string}/logger/Logger.kt (96%) delete mode 100644 src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioContextHandler.kt delete mode 100644 src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt rename src/jsMain/kotlin/nl/astraeus/vst/{chip => string}/Externals.kt (90%) rename src/jsMain/kotlin/nl/astraeus/vst/{chip => string}/Main.kt (66%) create mode 100644 src/jsMain/kotlin/nl/astraeus/vst/string/audio/AudioContextHandler.kt rename src/jsMain/kotlin/nl/astraeus/vst/{chip => string}/audio/AudioModule.kt (89%) create mode 100644 src/jsMain/kotlin/nl/astraeus/vst/string/audio/VstStringWorklet.kt rename src/jsMain/kotlin/nl/astraeus/vst/{chip => string}/midi/Broadcaster.kt (98%) rename src/jsMain/kotlin/nl/astraeus/vst/{chip => string}/midi/Midi.kt (94%) rename src/jsMain/kotlin/nl/astraeus/vst/{chip => string}/view/MainView.kt (54%) create mode 100644 src/jsMain/kotlin/nl/astraeus/vst/string/view/PhysicalStringView.kt create mode 100644 src/jsMain/kotlin/nl/astraeus/vst/string/view/WaveformView.kt rename src/jsMain/kotlin/nl/astraeus/vst/{chip => string}/ws/WebsocketClient.kt (87%) delete mode 100644 src/jsMain/resources/index.html create mode 100644 src/jvmMain/java/BarWavesCanvas.java rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/GenerateId.kt (90%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/Main.kt (84%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/Settings.kt (97%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/db/BaseDao.kt (98%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/db/Database.kt (98%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/db/Entity.kt (87%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/db/Migrations.kt (96%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/db/PatchDao.kt (94%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/db/PatchEntity.kt (84%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/db/PatchEntityQueryProvider.kt (97%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/web/Index.kt (87%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/web/RequestHandler.kt (95%) rename src/jvmMain/kotlin/nl/astraeus/vst/{chip => string}/web/Session.kt (53%) diff --git a/.idea/artifacts/audio_worklet_js_1_0_0_SNAPSHOT.xml b/.idea/artifacts/audio_worklet_js_1_0_0_SNAPSHOT.xml index 48401c3..f0c6900 100644 --- a/.idea/artifacts/audio_worklet_js_1_0_0_SNAPSHOT.xml +++ b/.idea/artifacts/audio_worklet_js_1_0_0_SNAPSHOT.xml @@ -2,7 +2,7 @@ $PROJECT_DIR$/audio-worklet/build/libs - + \ No newline at end of file diff --git a/.idea/artifacts/audio_worklet_jvm_1_0_0_SNAPSHOT.xml b/.idea/artifacts/audio_worklet_jvm_1_0_0_SNAPSHOT.xml index 06b6fe8..d7beaf8 100644 --- a/.idea/artifacts/audio_worklet_jvm_1_0_0_SNAPSHOT.xml +++ b/.idea/artifacts/audio_worklet_jvm_1_0_0_SNAPSHOT.xml @@ -2,7 +2,7 @@ $PROJECT_DIR$/audio-worklet/build/libs - + \ No newline at end of file diff --git a/.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml b/.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml index d3d29c0..468ef31 100644 --- a/.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml +++ b/.idea/artifacts/common_js_1_0_0_SNAPSHOT.xml @@ -2,7 +2,7 @@ $PROJECT_DIR$/common/build/libs - + \ No newline at end of file diff --git a/.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml b/.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml index 1d6f5d0..d4560ce 100644 --- a/.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml +++ b/.idea/artifacts/common_jvm_1_0_0_SNAPSHOT.xml @@ -2,7 +2,7 @@ $PROJECT_DIR$/common/build/libs - + \ No newline at end of file diff --git a/.run/Main [jvm].run.xml b/.run/Main [jvm].run.xml index 4378084..5d42efd 100644 --- a/.run/Main [jvm].run.xml +++ b/.run/Main [jvm].run.xml @@ -4,7 +4,7 @@ diff --git a/audio-worklet/build.gradle.kts b/audio-worklet/build.gradle.kts index 6eb7ea2..8b6fdf4 100644 --- a/audio-worklet/build.gradle.kts +++ b/audio-worklet/build.gradle.kts @@ -21,13 +21,13 @@ kotlin { browser { commonWebpackConfig { - outputFileName = "vst-chip-worklet.js" + outputFileName = "vst-string-worklet.js" sourceMaps = true } webpackTask { output.libraryTarget = KotlinWebpackOutput.Target.VAR - output.library = "vstChipWorklet" + output.library = "vstStringWorklet" } distribution { diff --git a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt deleted file mode 100644 index 0c7ec2b..0000000 --- a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/chip/ChipProcessor.kt +++ /dev/null @@ -1,393 +0,0 @@ -@file:OptIn(ExperimentalJsExport::class) - -package nl.astraeus.vst.chip - -import nl.astraeus.vst.ADSR -import nl.astraeus.vst.AudioWorkletProcessor -import nl.astraeus.vst.Note -import nl.astraeus.vst.currentTime -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 -import kotlin.math.PI -import kotlin.math.min -import kotlin.math.sin - -val POLYPHONICS = 10 -val PI2 = PI * 2 - -@ExperimentalJsExport -@JsExport -class PlayingNote( - val note: Int, - var velocity: Int = 0 -) { - fun retrigger(velocity: Int) { - this.velocity = velocity - sample = 0 - noteStart = currentTime - noteRelease = null - } - - var noteStart = currentTime - var noteRelease: Double? = null - var cycleOffset = 0.0 - var sample = 0 - var actualVolume = 0f -} - -enum class Waveform { - SINE, - SQUARE, - TRIANGLE, - SAWTOOTH -} - -@ExperimentalJsExport -@JsExport -enum class RecordingState { - STOPPED, - WAITING_TO_START, - RECORDING -} - -@ExperimentalJsExport -@JsExport -class VstChipProcessor : AudioWorkletProcessor() { - var midiChannel = 0 - val notes = Array(POLYPHONICS) { null } - - var waveform = Waveform.SINE.ordinal - var volume = 0.75f - var dutyCycle = 0.5 - var fmFreq = 0.0 - var fmAmp = 0.0 - var amFreq = 0.0 - var amAmp = 0.0 - - var attack = 0.1 - var decay = 0.2 - var sustain = 0.5 - var release = 0.2 - - val sampleLength = 1 / sampleRate.toDouble() - - val recordingBuffer = Float32Array(sampleRate / 60) - var recordingState = RecordingState.STOPPED - var recordingSample = 0 - var recordingStart = 0 - - init { - this.port.onmessage = ::handleMessage - Note.updateSampleRate(sampleRate) - } - - private fun handleMessage(message: MessageEvent) { - //console.log("VstChipProcessor: Received message:", message.data) - - val data = message.data - - try { - when (data) { - is String -> { - when(data) { - "start_recording" -> { - port.postMessage(recordingBuffer) - if (recordingState == RecordingState.STOPPED) { - recordingState = RecordingState.WAITING_TO_START - recordingSample = 0 - } - } - 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") - } - } - } - } - - 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) - } - - else -> - console.error("Don't kow how to handle message", message) - } - } catch(e: Exception) { - console.log(e.message, e) - } - } - - 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 - } - - 0x4a -> { - fmFreq = value / 127.0 - } - - 0x4b -> { - fmAmp = value / 127.0 - } - - 0x4c -> { - amFreq = value / 127.0 - } - 0x4d -> { - amAmp = value / 127.0 - } - - 0x49 -> { - attack = value / 127.0 - } - - 0x4b -> { - decay = value / 127.0 - } - - 0x46 -> { - sustain = value / 127.0 - } - - 0x48 -> { - release = value / 127.0 - } - - 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 noteOn(note: Int, velocity: Int) { - for (i in 0 until POLYPHONICS) { - if (notes[i]?.note == note) { - notes[i]?.retrigger(velocity) - return - } - } - for (i in 0 until POLYPHONICS) { - if (notes[i] == null) { - notes[i] = PlayingNote( - note, - velocity - ) - break - } - } - } - - private fun noteOff(note: Int) { - for (i in 0 until POLYPHONICS) { - if (notes[i]?.note == note) { - notes[i]?.noteRelease = currentTime - break - } - } - } - - override fun process ( - inputs: Array>, - outputs: Array>, - parameters: dynamic - ) : Boolean { - val samples = outputs[0][0].length - - val left = outputs[0][0] - val right = outputs[0][1] - - var lowestNote = 200 - for (note in notes) { - if (note != null) { - lowestNote = min(lowestNote, note.note) - } - } - if (lowestNote == 200 && recordingState == RecordingState.WAITING_TO_START) { - recordingState = RecordingState.RECORDING - recordingSample = 0 - recordingStart = 0 - } - - for ((index, note) in notes.withIndex()) { - if (note != null) { - val sampleDelta = Note.fromMidi(note.note).sampleDelta - - for (i in 0 until samples) { - var targetVolume = note.velocity / 127f - targetVolume *= ADSR.calculate( - attack, - decay, - sustain, - release, - note.noteStart, - currentTime, - note.noteRelease - ).toFloat() - note.actualVolume += (targetVolume - note.actualVolume) * 0.01f - - if (note.noteRelease != null && note.actualVolume <= 0.01) { - notes[index] = null - } - - var cycleOffset = note.cycleOffset - val fmModulation = sampleDelta * sin( fmFreq * 20f * PI2 * (note.sample / sampleRate.toDouble())).toFloat() * fmAmp - val amModulation = 1f + (sin(sampleLength * amFreq * 10f * PI2 * note.sample) * amAmp).toFloat() - - cycleOffset = if (cycleOffset < dutyCycle) { - cycleOffset / dutyCycle / 2.0 - } else { - 0.5 + ((cycleOffset -dutyCycle) / (1.0 - dutyCycle) / 2.0) - } - - val waveValue: Float = when (waveform) { - 0 -> { - sin(cycleOffset * PI2).toFloat() - } - 1 -> { - if (cycleOffset < 0.5) { 1f } else { -1f } - } - 2 -> when { - cycleOffset < 0.25 -> 4 * cycleOffset - cycleOffset < 0.75 -> 2 - 4 * cycleOffset - else -> 4 * cycleOffset - 4 - }.toFloat() - 3 -> { - ((cycleOffset * 2f) - 1f).toFloat() - } - else -> { - if (cycleOffset < 0.5) { 1f } else { -1f } - } - } - - left[i] = left[i] + waveValue * note.actualVolume * volume * amModulation - right[i] = right[i] + waveValue * note.actualVolume * volume * amModulation - - note.cycleOffset += sampleDelta + fmModulation - if (note.cycleOffset > 1f) { - note.cycleOffset -= 1f - if (note.note == lowestNote && recordingState == RecordingState.WAITING_TO_START) { - recordingState = RecordingState.RECORDING - recordingSample = 0 - recordingStart = i - } - } - note.sample++ - } - } - } - - if (recordingState == RecordingState.RECORDING) { - for (i in recordingStart until samples) { - recordingBuffer[recordingSample] = (left[i] + right[i]) / 2f - if (recordingSample < recordingBuffer.length - 1) { - recordingSample++ - } else { - recordingState = RecordingState.STOPPED - } - } - recordingStart = 0 - } - - return true - } -} - -fun main() { - registerProcessor("vst-chip-processor", VstChipProcessor::class.js) - - println("VstChipProcessor registered!") -} diff --git a/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/string/StringProcessor.kt b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/string/StringProcessor.kt new file mode 100644 index 0000000..a7aeaea --- /dev/null +++ b/audio-worklet/src/jsMain/kotlin/nl/astraeus/vst/string/StringProcessor.kt @@ -0,0 +1,264 @@ +@file:OptIn(ExperimentalJsExport::class) + +package nl.astraeus.vst.string + +import nl.astraeus.vst.AudioWorkletProcessor +import nl.astraeus.vst.Note +import nl.astraeus.vst.currentTime +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 +import kotlin.math.PI +import kotlin.math.min + +val POLYPHONICS = 10 +val PI2 = PI * 2 + +@ExperimentalJsExport +@JsExport +class PlayingNote( + val note: Int, + var velocity: Int = 0 +) { + fun retrigger(velocity: Int) { + this.velocity = velocity + sample = 0 + noteStart = currentTime + noteRelease = null + } + + var noteStart = currentTime + var noteRelease: Double? = null + var cycleOffset = 0.0 + var sample = 0 + var actualVolume = 0f +} + +@ExperimentalJsExport +@JsExport +enum class RecordingState { + STOPPED, + WAITING_TO_START, + RECORDING +} + +@ExperimentalJsExport +@JsExport +class VstStringProcessor : AudioWorkletProcessor() { + var midiChannel = 0 + + var volume = 0.75f + var damping = 0.996 + + val recordingBuffer = Float32Array(sampleRate / 60) + var recordingState = RecordingState.STOPPED + var recordingSample = 0 + var recordingStart = 0 + + val strings = Array(POLYPHONICS) { + PhysicalString(sampleRate, damping) + } + + init { + this.port.onmessage = ::handleMessage + Note.updateSampleRate(sampleRate) + } + + private fun handleMessage(message: MessageEvent) { + //console.log("VstChipProcessor: Received message:", message.data) + + val data = message.data + + try { + when (data) { + is String -> { + when (data) { + "start_recording" -> { + port.postMessage(recordingBuffer) + if (recordingState == RecordingState.STOPPED) { + recordingState = RecordingState.WAITING_TO_START + recordingSample = 0 + } + } + + else -> + if (data.startsWith("set_channel")) { + val parts = data.split('\n') + if (parts.size == 2) { + midiChannel = parts[1].toInt() + println("Setting channel: $midiChannel") + } + } + } + } + + 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) + } + + else -> + console.error("Don't kow how to handle message", message) + } + } catch (e: Exception) { + console.log(e.message, e) + } + } + + 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) + } + } + + 0xb0 -> { + if (bytes.length == 3) { + val knob = bytes[1] + val value = bytes[2] + + when (knob) { + 7 -> { + volume = value / 127f + } + + 0x47 -> { + damping = 0.8 + value / 127.0 + } + } + } + } + } + } + } + + private fun noteOn(midiNote: Int, velocity: Int) { + val note = Note.fromMidi(midiNote) + for (i in 0 until POLYPHONICS) { + if (strings[i].currentNote == note) { + strings[i].pluck(note, velocity / 127.0) + strings[i].damping = damping + return + } + } + for (i in 0 until POLYPHONICS) { + if (strings[i].available) { + strings[i].pluck(note, velocity / 127.0) + strings[i].damping = damping + break + } + } + } + + private fun noteOff(midiNote: Int) { + val note = Note.fromMidi(midiNote) + for (i in 0 until POLYPHONICS) { + if (strings[i].currentNote.ordinal == note.ordinal) { + strings[i].available = true + strings[i].damping = damping * 0.9 + } + } + } + + override fun process( + inputs: Array>, + outputs: Array>, + parameters: dynamic + ): Boolean { + val samples = outputs[0][0].length + + val left = outputs[0][0] + val right = outputs[0][1] + + var lowestNote = 200 + + for (string in strings) { + if (string.available) { + lowestNote = min(lowestNote, string.currentNote.ordinal) + } + } + + if (lowestNote == 200 && recordingState == RecordingState.WAITING_TO_START) { + recordingState = RecordingState.RECORDING + recordingSample = 0 + recordingStart = 0 + } + + for (string in strings) { + for (i in 0 until samples) { + val waveValue: Float = string.tick().toFloat() + + left[i] = left[i] + waveValue * volume + right[i] = right[i] + waveValue * volume + + if (lowestNote == string.currentNote.ordinal && recordingState == RecordingState.WAITING_TO_START && string.index == 0) { + recordingState = RecordingState.RECORDING + recordingSample = 0 + recordingStart = 0 + } + } + } + + if (recordingState == RecordingState.RECORDING) { + for (i in recordingStart until samples) { + recordingBuffer[recordingSample] = (left[i] + right[i]) / 2f + if (recordingSample < recordingBuffer.length - 1) { + recordingSample++ + } else { + recordingState = RecordingState.STOPPED + } + } + recordingStart = 0 + } + + return true + } +} + +fun main() { + registerProcessor("vst-string-processor", VstStringProcessor::class.js) + + println("VstStringProcessor registered!") +} diff --git a/build.gradle.kts b/build.gradle.kts index f2d74f9..e172502 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ kotlin { binaries.executable() browser { commonWebpackConfig { - outputFileName = "vst-chip-worklet-ui.js" + outputFileName = "vst-string-worklet-ui.js" sourceMaps = true } diff --git a/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt b/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt index ca08cde..41970fa 100644 --- a/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt +++ b/common/src/commonMain/kotlin/nl/astraeus/vst/Note.kt @@ -1,5 +1,7 @@ package nl.astraeus.vst +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport import kotlin.math.max import kotlin.math.min import kotlin.math.pow @@ -11,6 +13,8 @@ import kotlin.math.round * Time: 11:50 */ +@ExperimentalJsExport +@JsExport enum class Note( val sharp: String, val flat: String diff --git a/common/src/commonMain/kotlin/nl/astraeus/vst/string/PhysicalString.kt b/common/src/commonMain/kotlin/nl/astraeus/vst/string/PhysicalString.kt new file mode 100644 index 0000000..1ba91f7 --- /dev/null +++ b/common/src/commonMain/kotlin/nl/astraeus/vst/string/PhysicalString.kt @@ -0,0 +1,80 @@ +package nl.astraeus.vst.string + +import nl.astraeus.vst.Note +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport +import kotlin.math.round + +expect fun randomDouble(): Double + +@ExperimentalJsExport +@JsExport +class PhysicalString( + val sampleRate: Int, + var damping: Double, +) { + val sampleLength = 1.0 / sampleRate.toDouble() + val maxLength = sampleRate / Note.G9.freq + var length = 1 + val buffer = Array(maxLength.toInt() + 1) { 0.0 } + var sample = 0 + var index = 0 + var remaining = 0.0 + var currentNote = Note.C4 + var available = true + + fun pluck(note: Note, velocity: Double, smoothing: Int = 0) { + available = false + currentNote = note + length = round(sampleRate / note.freq).toInt() + sample = 0 + index = 0 + + for (i in 0.. sampleLength) { + remaining -= sampleLength + tick() + } + } + + fun tick(): Double { + val result = buffer[index] + + var newValue = 0.0 + newValue += getValueFromBuffer(index + 1) * 0.2 + newValue += getValueFromBuffer(index + 2) * 0.3 + newValue += getValueFromBuffer(index + 3) * 0.3 + newValue += getValueFromBuffer(index + 4) * 0.2 +// newValue += getValueFromBuffer(index + 5) * 0.2 +// newValue += getValueFromBuffer(index + 6) * 0.3 + newValue *= damping + + buffer[index] = newValue + + index = (index + 1) % length + + return result + } + + private fun getValueFromBuffer(index: Int): Double { + return buffer[(index + length) % length] + } +} diff --git a/common/src/jsMain/kotlin/nl/astraeus/vst/string/PhysicalString.js.kt b/common/src/jsMain/kotlin/nl/astraeus/vst/string/PhysicalString.js.kt new file mode 100644 index 0000000..8736db1 --- /dev/null +++ b/common/src/jsMain/kotlin/nl/astraeus/vst/string/PhysicalString.js.kt @@ -0,0 +1,5 @@ +package nl.astraeus.vst.string + +import kotlin.random.Random + +actual fun randomDouble(): Double = Random.nextDouble() diff --git a/common/src/jvmMain/kotlin/nl/astraeus/vst/string/PhysicalString.jvm.kt b/common/src/jvmMain/kotlin/nl/astraeus/vst/string/PhysicalString.jvm.kt new file mode 100644 index 0000000..c1ec7e2 --- /dev/null +++ b/common/src/jvmMain/kotlin/nl/astraeus/vst/string/PhysicalString.jvm.kt @@ -0,0 +1,5 @@ +package nl.astraeus.vst.string + +actual fun randomDouble(): Double { + TODO("Not yet implemented") +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ab45cf9..69893ea 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ apply(from = "settings.common.gradle.kts") -rootProject.name = "vst-chip" +rootProject.name = "vst-string" include(":common") include(":audio-worklet") diff --git a/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt b/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt deleted file mode 100644 index 408fc1e..0000000 --- a/src/commonMain/kotlin/nl/astraeus/vst/chip/PatchDTO.kt +++ /dev/null @@ -1,34 +0,0 @@ -package nl.astraeus.vst.chip - -import kotlin.js.JsName - -data class PatchDTO( - @JsName("waveform") - val waveform: Int = 0, - @JsName("midiId") - val midiId: String = "", - @JsName("midiName") - val midiName: String = "", - @JsName("midiChannel") - var midiChannel: Int = 0, - @JsName("volume") - var volume: Double = 0.75, - @JsName("dutyCycle") - var dutyCycle: Double = 0.5, - @JsName("fmModFreq") - var fmModFreq: Double = 0.0, - @JsName("fmModAmp") - var fmModAmp: Double = 0.0, - @JsName("amModFreq") - var amModFreq: Double = 0.0, - @JsName("amModAmp") - var amModAmp: Double = 0.0, - @JsName("attack") - var attack: Double = 0.1, - @JsName("decay") - var decay: Double = 0.2, - @JsName("sustain") - var sustain: Double = 0.5, - @JsName("release") - var release: Double = 0.2, -) diff --git a/src/commonMain/kotlin/nl/astraeus/vst/string/PatchDTO.kt b/src/commonMain/kotlin/nl/astraeus/vst/string/PatchDTO.kt new file mode 100644 index 0000000..e9633af --- /dev/null +++ b/src/commonMain/kotlin/nl/astraeus/vst/string/PatchDTO.kt @@ -0,0 +1,24 @@ +package nl.astraeus.vst.string + +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport +import kotlin.js.JsName + +@ExperimentalJsExport +@JsExport +data class PatchDTO( + @JsName("midiId") + val midiId: String = "", + @JsName("midiName") + val midiName: String = "", + @JsName("midiChannel") + var midiChannel: Int = 0, + @JsName("volume") + var volume: Double = 0.75, + @JsName("damping") + var damping: Double = 0.5, + @JsName("delay") + var delay: Double = 0.0, + @JsName("delayDepth") + var delayDepth: Double = 0.0, +) diff --git a/src/commonMain/kotlin/nl/astraeus/vst/chip/logger/Logger.kt b/src/commonMain/kotlin/nl/astraeus/vst/string/logger/Logger.kt similarity index 96% rename from src/commonMain/kotlin/nl/astraeus/vst/chip/logger/Logger.kt rename to src/commonMain/kotlin/nl/astraeus/vst/string/logger/Logger.kt index cd71970..e60eadd 100644 --- a/src/commonMain/kotlin/nl/astraeus/vst/chip/logger/Logger.kt +++ b/src/commonMain/kotlin/nl/astraeus/vst/string/logger/Logger.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip.logger +package nl.astraeus.vst.string.logger val log = Logger diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioContextHandler.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioContextHandler.kt deleted file mode 100644 index 3db72b1..0000000 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioContextHandler.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nl.astraeus.vst.chip.audio - -import nl.astraeus.vst.chip.AudioContext - -object AudioContextHandler { - val audioContext: dynamic = AudioContext() - - - -} \ No newline at end of file diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt b/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt deleted file mode 100644 index 807ccc8..0000000 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/VstChipWorklet.kt +++ /dev/null @@ -1,203 +0,0 @@ -package nl.astraeus.vst.chip.audio - -import nl.astraeus.vst.chip.PatchDTO -import nl.astraeus.vst.chip.view.MainView -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", - "vst-chip-processor" -) { - var waveform: Int = 0 - set(value) { - field = value - postMessage("waveform\n$value") - } - var midiChannel = 0 - set(value) { - check(value in 0..15) { - "Midi channel must be between 0 and 15." - } - field = value - postMessage("set_channel\n${midiChannel}") - } - var volume = 0.75 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 7, (value * 127).toInt()) - ) - } - var dutyCycle = 0.5 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x47, (value * 127).toInt()) - ) - } - var fmModFreq = 0.0 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x4a, (value * 127).toInt()) - ) - } - var fmModAmp = 0.0 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt()) - ) - } - var amModFreq = 0.0 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x4c, (value * 127).toInt()) - ) - } - var amModAmp = 0.0 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x4d, (value * 127).toInt()) - ) - } - - var attack = 0.1 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x49, (value * 127).toInt()) - ) - } - var decay = 0.2 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x4b, (value * 127).toInt()) - ) - } - var sustain = 0.5 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x46, (value * 127).toInt()) - ) - } - var release = 0.2 - set(value) { - field = value - super.postMessage( - uInt8ArrayOf(0xb0 + midiChannel, 0x48, (value * 127).toInt()) - ) - } - - var recording: Float32Array? = null - - override fun onMessage(message: MessageEvent) { - //console.log("Message from worklet: ", message) - - val data = message.data - if (data is Float32Array) { - this.recording = data - WaveformView.requestUpdate() - } - } - - fun postDirectlyToWorklet(msg: Any) { - super.postMessage(msg) - } - - 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] - - handleIncomingMidi(knob, value) - } else { - super.postMessage(msg) - } - } else { - super.postMessage(msg) - } - } - - private fun handleIncomingMidi(knob: Byte, value: Byte) { - when (knob) { - 0x46.toByte() -> { - volume = value / 127.0 - MainView.requestUpdate() - } - - 0x4a.toByte() -> { - dutyCycle = value / 127.0 - MainView.requestUpdate() - } - - 0x4b.toByte() -> { - fmModFreq = value / 127.0 - MainView.requestUpdate() - } - - 0x4c.toByte() -> { - fmModAmp = value / 127.0 - MainView.requestUpdate() - } - - 0x47.toByte() -> { - amModFreq = value / 127.0 - MainView.requestUpdate() - } - - 0x48.toByte() -> { - amModAmp = value / 127.0 - MainView.requestUpdate() - } - } - } - - fun load(patch: PatchDTO) { - waveform = patch.waveform - midiChannel = patch.midiChannel - volume = patch.volume - dutyCycle = patch.dutyCycle - fmModFreq = patch.fmModFreq - fmModAmp = patch.fmModAmp - amModFreq = patch.amModFreq - amModAmp = patch.amModAmp - attack = patch.attack - decay = patch.decay - sustain = patch.sustain - release = patch.release - } - - fun save(): PatchDTO { - return PatchDTO( - waveform = waveform, - midiChannel = midiChannel, - volume = volume, - dutyCycle = dutyCycle, - fmModFreq = fmModFreq, - fmModAmp = fmModAmp, - amModFreq = amModFreq, - amModAmp = amModAmp, - attack = attack, - decay = decay, - sustain = sustain, - release = release - ) - } - -} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/Externals.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/Externals.kt similarity index 90% rename from src/jsMain/kotlin/nl/astraeus/vst/chip/Externals.kt rename to src/jsMain/kotlin/nl/astraeus/vst/string/Externals.kt index c476f6b..84b07b8 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/Externals.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/Externals.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip +package nl.astraeus.vst.string external class AudioContext { var sampleRate: Int diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/Main.kt similarity index 66% rename from src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt rename to src/jsMain/kotlin/nl/astraeus/vst/string/Main.kt index d15f86e..7c08d4f 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/Main.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/Main.kt @@ -1,12 +1,12 @@ -package nl.astraeus.vst.chip +package nl.astraeus.vst.string import kotlinx.browser.document import nl.astraeus.komp.Komponent import nl.astraeus.komp.UnsafeMode -import nl.astraeus.vst.chip.logger.log -import nl.astraeus.vst.chip.midi.Midi -import nl.astraeus.vst.chip.view.MainView -import nl.astraeus.vst.chip.ws.WebsocketClient +import nl.astraeus.vst.string.logger.log +import nl.astraeus.vst.string.midi.Midi +import nl.astraeus.vst.string.view.MainView +import nl.astraeus.vst.string.ws.WebsocketClient import nl.astraeus.vst.ui.css.CssSettings fun main() { diff --git a/src/jsMain/kotlin/nl/astraeus/vst/string/audio/AudioContextHandler.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/audio/AudioContextHandler.kt new file mode 100644 index 0000000..fcfa5ee --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/audio/AudioContextHandler.kt @@ -0,0 +1,10 @@ +package nl.astraeus.vst.string.audio + +import nl.astraeus.vst.string.AudioContext + +object AudioContextHandler { + val audioContext: dynamic = AudioContext() + + + +} \ No newline at end of file diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioModule.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/audio/AudioModule.kt similarity index 89% rename from src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioModule.kt rename to src/jsMain/kotlin/nl/astraeus/vst/string/audio/AudioModule.kt index f5e835a..248489c 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/audio/AudioModule.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/audio/AudioModule.kt @@ -1,8 +1,8 @@ -package nl.astraeus.vst.chip.audio +package nl.astraeus.vst.string.audio -import nl.astraeus.vst.chip.AudioWorkletNode -import nl.astraeus.vst.chip.AudioWorkletNodeParameters -import nl.astraeus.vst.chip.audio.AudioContextHandler.audioContext +import nl.astraeus.vst.string.AudioWorkletNode +import nl.astraeus.vst.string.AudioWorkletNodeParameters +import nl.astraeus.vst.string.audio.AudioContextHandler.audioContext import org.w3c.dom.MessageEvent import org.w3c.dom.MessagePort diff --git a/src/jsMain/kotlin/nl/astraeus/vst/string/audio/VstStringWorklet.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/audio/VstStringWorklet.kt new file mode 100644 index 0000000..eba59bf --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/audio/VstStringWorklet.kt @@ -0,0 +1,123 @@ +@file:OptIn(ExperimentalJsExport::class) + +package nl.astraeus.vst.string.audio + +import nl.astraeus.vst.string.PatchDTO +import nl.astraeus.vst.string.view.MainView +import nl.astraeus.vst.string.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 VstStringWorklet : AudioNode( + "/vst-string-worklet.js", + "vst-string-processor" +) { + var midiChannel = 0 + set(value) { + check(value in 0..15) { + "Midi channel must be between 0 and 15." + } + field = value + postMessage("set_channel\n${midiChannel}") + } + var volume = 0.75 + set(value) { + field = value + super.postMessage( + uInt8ArrayOf(0xb0 + midiChannel, 7, (value * 127).toInt()) + ) + } + var damping = 0.996 + set(value) { + field = value + super.postMessage( + uInt8ArrayOf(0xb0 + midiChannel, 0x47, ((value - 0.8) * 127).toInt()) + ) + } + var delay = 0.0 + set(value) { + field = value + super.postMessage( + uInt8ArrayOf(0xb0 + midiChannel, 0x4e, (value * 127).toInt()) + ) + } + var delayDepth = 0.0 + set(value) { + field = value + super.postMessage( + uInt8ArrayOf(0xb0 + midiChannel, 0x4f, (value * 127).toInt()) + ) + } + + var recording: Float32Array? = null + + override fun onMessage(message: MessageEvent) { + //console.log("Message from worklet: ", message) + + val data = message.data + if (data is Float32Array) { + this.recording = data + WaveformView.requestUpdate() + } + } + + fun postDirectlyToWorklet(msg: Any) { + super.postMessage(msg) + } + + 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] + + handleIncomingMidi(knob, value) + } else { + super.postMessage(msg) + } + } else { + super.postMessage(msg) + } + } + + private fun handleIncomingMidi(knob: Byte, value: Byte) { + when (knob) { + 0x46.toByte() -> { + volume = value / 127.0 + MainView.requestUpdate() + } + + 0x4a.toByte() -> { + damping = value / 127.0 + MainView.requestUpdate() + } + } + } + + fun load(patch: PatchDTO) { + midiChannel = patch.midiChannel + volume = patch.volume + damping = patch.damping + delay = patch.delay + delayDepth = patch.delayDepth + } + + fun save(): PatchDTO { + return PatchDTO( + midiChannel = midiChannel, + volume = volume, + damping = damping, + delay = delay, + delayDepth = delayDepth, + ) + } + +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Broadcaster.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/midi/Broadcaster.kt similarity index 98% rename from src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Broadcaster.kt rename to src/jsMain/kotlin/nl/astraeus/vst/string/midi/Broadcaster.kt index ddcf9e3..5f59439 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Broadcaster.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/midi/Broadcaster.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalJsExport::class) -package nl.astraeus.vst.chip.midi +package nl.astraeus.vst.string.midi import kotlinx.browser.window import org.khronos.webgl.Uint8Array diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Midi.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/midi/Midi.kt similarity index 94% rename from src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Midi.kt rename to src/jsMain/kotlin/nl/astraeus/vst/string/midi/Midi.kt index 1d969ef..f2665e8 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/midi/Midi.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/midi/Midi.kt @@ -1,8 +1,8 @@ -package nl.astraeus.vst.chip.midi +package nl.astraeus.vst.string.midi import kotlinx.browser.window -import nl.astraeus.vst.chip.audio.VstChipWorklet -import nl.astraeus.vst.chip.view.MainView +import nl.astraeus.vst.string.audio.VstStringWorklet +import nl.astraeus.vst.string.view.MainView import org.khronos.webgl.Uint8Array import org.khronos.webgl.get @@ -125,7 +125,7 @@ object Midi { hex.append(" ") } console.log("Midi message:", hex) - VstChipWorklet.postMessage( + VstStringWorklet.postMessage( message.data ) } diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/view/MainView.kt similarity index 54% rename from src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt rename to src/jsMain/kotlin/nl/astraeus/vst/string/view/MainView.kt index e358a8c..5657840 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/view/MainView.kt @@ -1,9 +1,8 @@ -package nl.astraeus.vst.chip.view +@file:OptIn(ExperimentalJsExport::class) + +package nl.astraeus.vst.string.view -import kotlinx.browser.window import kotlinx.html.InputType -import kotlinx.html.canvas -import kotlinx.html.classes import kotlinx.html.div import kotlinx.html.h1 import kotlinx.html.input @@ -32,72 +31,30 @@ import nl.astraeus.css.style.Style import nl.astraeus.css.style.cls import nl.astraeus.komp.HtmlBuilder import nl.astraeus.komp.Komponent -import nl.astraeus.komp.currentElement -import nl.astraeus.vst.chip.audio.VstChipWorklet -import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel -import nl.astraeus.vst.chip.midi.Midi -import nl.astraeus.vst.chip.ws.WebsocketClient -import nl.astraeus.vst.ui.components.KnobComponent +import nl.astraeus.vst.string.PhysicalString +import nl.astraeus.vst.string.audio.VstStringWorklet +import nl.astraeus.vst.string.audio.VstStringWorklet.midiChannel +import nl.astraeus.vst.string.midi.Midi +import nl.astraeus.vst.string.ws.WebsocketClient +import nl.astraeus.vst.ui.components.ExpKnobComponent import nl.astraeus.vst.ui.css.Css 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 import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLSelectElement -object WaveformView: Komponent() { - - init { - window.requestAnimationFrame(::onAnimationFrame) - } - - fun onAnimationFrame(time: Double) { - if (MainView.started) { - VstChipWorklet.postMessage("start_recording") - } - - window.requestAnimationFrame(::onAnimationFrame) - } - - override fun HtmlBuilder.render() { - div { - if (VstChipWorklet.recording != null) { - canvas { - width = "1000" - height = "400" - val ctx = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D - val data = VstChipWorklet.recording - if (ctx != null && data != null) { - val width = ctx.canvas.width.toDouble() - val height = ctx.canvas.height.toDouble() - val halfHeight = height / 2.0 - - ctx.lineWidth = 2.0 - ctx.clearRect(0.0, 0.0, width, height) - val step = 1000.0 / data.length - ctx.beginPath() - ctx.strokeStyle = "rgba(0, 255, 255, 0.5)" - ctx.moveTo(0.0, halfHeight) - for (i in 0 until data.length) { - ctx.lineTo(i * step, halfHeight - data[i] * halfHeight) - } - ctx.stroke() - } - } - } - } - } - -} - object MainView : Komponent(), CssName { private var messages: MutableList = ArrayList() var started = false + val playString = PhysicalStringView( + PhysicalString( + sampleRate = 48000, + damping = 0.996, + ) + ) init { css() @@ -119,7 +76,7 @@ object MainView : Komponent(), CssName { div(StartButtonCss.name) { +"START" onClickFunction = { - VstChipWorklet.create { + VstStringWorklet.create { started = true requestUpdate() WebsocketClient.send("LOAD\n") @@ -130,7 +87,7 @@ object MainView : Komponent(), CssName { } } h1 { - +"VST Chip" + +"VST Guitar" } div { span { @@ -162,11 +119,11 @@ object MainView : Komponent(), CssName { +"channel:" input { type = InputType.number - value = VstChipWorklet.midiChannel.toString() + value = midiChannel.toString() onInputFunction = { event -> val target = event.target as HTMLInputElement println("onInput channel: $target") - VstChipWorklet.midiChannel = target.value.toInt() + VstStringWorklet.midiChannel = target.value.toInt() } } } @@ -175,7 +132,7 @@ object MainView : Komponent(), CssName { span(ButtonBarCss.name) { +"SAVE" onClickFunction = { - val patch = VstChipWorklet.save().copy( + val patch = VstStringWorklet.save().copy( midiId = Midi.currentInput?.id ?: "", midiName = Midi.currentInput?.name ?: "" ) @@ -186,189 +143,29 @@ object MainView : Komponent(), CssName { span(ButtonBarCss.name) { +"STOP" onClickFunction = { - VstChipWorklet.postDirectlyToWorklet( + VstStringWorklet.postDirectlyToWorklet( uInt8ArrayOf(0xb0 + midiChannel, 123, 0) ) } } } - div { - span(ButtonBarCss.name) { - +"Sine" - if (VstChipWorklet.waveform == 0) { - classes += SelectedCss.name - } - onClickFunction = { - VstChipWorklet.waveform = 0 - requestUpdate() - } - } - span(ButtonBarCss.name) { - +"Square" - if (VstChipWorklet.waveform == 1) { - classes += SelectedCss.name - } - onClickFunction = { - VstChipWorklet.waveform = 1 - requestUpdate() - } - } - span(ButtonBarCss.name) { - +"Triangle" - if (VstChipWorklet.waveform == 2) { - classes += SelectedCss.name - } - onClickFunction = { - VstChipWorklet.waveform = 2 - requestUpdate() - } - } - span(ButtonBarCss.name) { - +"Sawtooth" - if (VstChipWorklet.waveform == 3) { - classes += SelectedCss.name - } - onClickFunction = { - VstChipWorklet.waveform = 3 - requestUpdate() - } - } - } div(ControlsCss.name) { include( - KnobComponent( - value = VstChipWorklet.volume, + ExpKnobComponent( + value = VstStringWorklet.volume, label = "Volume", - minValue = 0.0, + minValue = 0.005, maxValue = 1.0, - step = 2.0 / 127.0, + step = 5.0 / 127.0, width = 100, height = 120, ) { value -> - VstChipWorklet.volume = value - } - ) - include( - KnobComponent( - value = VstChipWorklet.dutyCycle, - label = "Duty cycle", - minValue = 0.0, - maxValue = 1.0, - step = 2.0 / 127.0, - width = 100, - height = 120, - ) { value -> - VstChipWorklet.dutyCycle = value - } - ) - include( - KnobComponent( - value = VstChipWorklet.fmModFreq, - label = "FM Freq", - minValue = 0.0, - maxValue = 1.0, - step = 2.0 / 127.0, - width = 100, - height = 120, - ) { value -> - VstChipWorklet.fmModFreq = value - } - ) - include( - KnobComponent( - value = VstChipWorklet.fmModAmp, - label = "FM Ampl", - minValue = 0.0, - maxValue = 1.0, - step = 2.0 / 127.0, - width = 100, - height = 120, - ) { value -> - VstChipWorklet.fmModAmp = value - } - ) - include( - KnobComponent( - value = VstChipWorklet.amModFreq, - label = "AM Freq", - minValue = 0.0, - maxValue = 1.0, - step = 2.0 / 127.0, - width = 100, - height = 120, - ) { value -> - VstChipWorklet.amModFreq = value - } - ) - include( - KnobComponent( - value = VstChipWorklet.amModAmp, - label = "AM Ampl", - minValue = 0.0, - maxValue = 1.0, - step = 2.0 / 127.0, - width = 100, - height = 120, - ) { value -> - VstChipWorklet.amModAmp = value - } - ) - } - div(ControlsCss.name) { - include( - KnobComponent( - value = VstChipWorklet.attack, - label = "Attack", - minValue = 0.0, - maxValue = 1.0, - step = 2.0 / 127.0, - width = 100, - height = 120, - ) { value -> - VstChipWorklet.attack = value - } - ) - include( - KnobComponent( - value = VstChipWorklet.decay, - label = "Decay", - minValue = 0.0, - maxValue = 1.0, - step = 2.0 / 127.0, - width = 100, - height = 120, - ) { value -> - VstChipWorklet.decay = value - } - ) - include( - KnobComponent( - value = VstChipWorklet.sustain, - label = "Sustain", - minValue = 0.0, - maxValue = 1.0, - step = 2.0 / 127.0, - width = 100, - height = 120, - ) { value -> - VstChipWorklet.sustain = value - } - ) - include( - KnobComponent( - value = VstChipWorklet.release, - label = "Release", - minValue = 0.0, - maxValue = 1.0, - step = 2.0 / 127.0, - width = 100, - height = 120, - ) { value -> - VstChipWorklet.release = value + VstStringWorklet.volume = value } ) } include(WaveformView) + include(playString) } } diff --git a/src/jsMain/kotlin/nl/astraeus/vst/string/view/PhysicalStringView.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/view/PhysicalStringView.kt new file mode 100644 index 0000000..26480f0 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/view/PhysicalStringView.kt @@ -0,0 +1,109 @@ +package nl.astraeus.vst.string.view + +import kotlinx.browser.window +import kotlinx.html.canvas +import kotlinx.html.div +import kotlinx.html.js.onClickFunction +import kotlinx.html.span +import nl.astraeus.komp.HtmlBuilder +import nl.astraeus.komp.Komponent +import nl.astraeus.komp.currentElement +import nl.astraeus.vst.Note +import nl.astraeus.vst.string.PhysicalString +import nl.astraeus.vst.string.audio.VstStringWorklet +import nl.astraeus.vst.string.view.MainView.ControlsCss +import nl.astraeus.vst.ui.components.KnobComponent +import nl.astraeus.vst.util.formatDouble +import org.w3c.dom.CanvasRenderingContext2D +import org.w3c.dom.HTMLCanvasElement + +class PhysicalStringView( + val string: PhysicalString +) : Komponent() { + var context: CanvasRenderingContext2D? = null + var interval: Int = -1 + var lastUpdateTime: Double = window.performance.now() + + init { + window.requestAnimationFrame(::onAnimationFrame) + interval = window.setInterval({ + if (context?.canvas?.isConnected == true) { + val now: Double = window.performance.now() + val time = now - lastUpdateTime + lastUpdateTime = now + + string.update(time) + } else { + window.clearInterval(interval) + } + }, 1) + } + + private fun onAnimationFrame(time: Double) { + if (MainView.started) { + draw() + } + + window.requestAnimationFrame(::onAnimationFrame) + } + + override fun HtmlBuilder.render() { + div { + div(ControlsCss.name) { + include( + KnobComponent( + value = VstStringWorklet.damping, + label = "Damping", + minValue = 0.8, + maxValue = 1.0, + step = 0.2 / 127.0, + width = 100, + height = 120, + renderer = { formatDouble(it, 3) } + ) { value -> + VstStringWorklet.damping = value + } + ) + } + div { + span { + +"Play C3" + onClickFunction = { + string.pluck(Note.C3, 1.0) + } + } + span { + +"Play C4" + onClickFunction = { + string.pluck(Note.C4, 1.0) + } + } + } + canvas { + width = "1000" + height = "400" + context = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D + } + } + } + + private fun draw() { + val ctx = context + if (ctx != null) { + val width = ctx.canvas.width.toDouble() + val height = ctx.canvas.height.toDouble() + val halfHeight = height / 2.0 + + ctx.lineWidth = 2.0 + ctx.clearRect(0.0, 0.0, width, height) + val step = width / string.length + ctx.beginPath() + ctx.strokeStyle = "rgba(0, 255, 255, 0.5)" + for (i in 0 until string.length) { + ctx.moveTo(i * step, halfHeight) + ctx.lineTo(i * step, halfHeight + string.buffer[i] * halfHeight) + } + ctx.stroke() + } + } +} diff --git a/src/jsMain/kotlin/nl/astraeus/vst/string/view/WaveformView.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/view/WaveformView.kt new file mode 100644 index 0000000..a642340 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/view/WaveformView.kt @@ -0,0 +1,56 @@ +package nl.astraeus.vst.string.view + +import kotlinx.browser.window +import kotlinx.html.canvas +import kotlinx.html.div +import nl.astraeus.komp.HtmlBuilder +import nl.astraeus.komp.Komponent +import nl.astraeus.komp.currentElement +import nl.astraeus.vst.string.audio.VstStringWorklet +import org.khronos.webgl.get +import org.w3c.dom.CanvasRenderingContext2D +import org.w3c.dom.HTMLCanvasElement + +object WaveformView : Komponent() { + + init { + window.requestAnimationFrame(::onAnimationFrame) + } + + fun onAnimationFrame(time: Double) { + if (MainView.started) { + VstStringWorklet.postMessage("start_recording") + } + + window.requestAnimationFrame(::onAnimationFrame) + } + + override fun HtmlBuilder.render() { + div { + if (VstStringWorklet.recording != null) { + canvas { + width = "1000" + height = "400" + val ctx = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D + val data = VstStringWorklet.recording + if (ctx != null && data != null) { + val width = ctx.canvas.width.toDouble() + val height = ctx.canvas.height.toDouble() + val halfHeight = height / 2.0 + + ctx.lineWidth = 2.0 + ctx.clearRect(0.0, 0.0, width, height) + val step = 1000.0 / data.length + ctx.beginPath() + ctx.strokeStyle = "rgba(0, 255, 255, 0.5)" + ctx.moveTo(0.0, halfHeight) + for (i in 0 until data.length) { + ctx.lineTo(i * step, halfHeight - data[i] * halfHeight) + } + ctx.stroke() + } + } + } + } + } +} \ No newline at end of file diff --git a/src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt b/src/jsMain/kotlin/nl/astraeus/vst/string/ws/WebsocketClient.kt similarity index 87% rename from src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt rename to src/jsMain/kotlin/nl/astraeus/vst/string/ws/WebsocketClient.kt index b58b43b..12285e6 100644 --- a/src/jsMain/kotlin/nl/astraeus/vst/chip/ws/WebsocketClient.kt +++ b/src/jsMain/kotlin/nl/astraeus/vst/string/ws/WebsocketClient.kt @@ -1,10 +1,10 @@ -package nl.astraeus.vst.chip.ws +package nl.astraeus.vst.string.ws import kotlinx.browser.window -import nl.astraeus.vst.chip.PatchDTO -import nl.astraeus.vst.chip.audio.VstChipWorklet -import nl.astraeus.vst.chip.midi.Midi -import nl.astraeus.vst.chip.view.MainView +import nl.astraeus.vst.string.PatchDTO +import nl.astraeus.vst.string.audio.VstStringWorklet +import nl.astraeus.vst.string.midi.Midi +import nl.astraeus.vst.string.view.MainView import org.w3c.dom.MessageEvent import org.w3c.dom.WebSocket import org.w3c.dom.events.Event @@ -17,7 +17,7 @@ object WebsocketClient { close() websocket = if (window.location.hostname.contains("localhost") || window.location.hostname.contains("192.168")) { - WebSocket("ws://${window.location.hostname}:9000/ws") + WebSocket("ws://${window.location.hostname}:${window.location.port}/ws") } else { WebSocket("wss://${window.location.hostname}/ws") } @@ -87,7 +87,7 @@ object WebsocketClient { val patch = JSON.parse(patchJson) Midi.setInput(patch.midiId, patch.midiName) - VstChipWorklet.load(patch) + VstStringWorklet.load(patch) MainView.requestUpdate() } } diff --git a/src/jsMain/resources/index.html b/src/jsMain/resources/index.html deleted file mode 100644 index 22e3c6e..0000000 --- a/src/jsMain/resources/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/jvmMain/java/BarWavesCanvas.java b/src/jvmMain/java/BarWavesCanvas.java new file mode 100644 index 0000000..cb12599 --- /dev/null +++ b/src/jvmMain/java/BarWavesCanvas.java @@ -0,0 +1,1622 @@ +// BarWaves.java (C) 2001 by Paul Falstad, www.falstad.com + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.SourceDataLine; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.Random; +import java.util.Vector; + +class BarWavesCanvas extends Canvas { + BarWavesFrame pg; + + BarWavesCanvas(BarWavesFrame p) { + pg = p; + } + + public Dimension getPreferredSize() { + return new Dimension(300, 400); + } + + public void update(Graphics g) { + pg.updateBarWaves(g); + } + + public void paint(Graphics g) { + pg.updateBarWaves(g); + } +}; + +class BarWavesLayout implements LayoutManager { + public BarWavesLayout() { + } + + public void addLayoutComponent(String name, Component c) { + } + + public void removeLayoutComponent(Component c) { + } + + public Dimension preferredLayoutSize(Container target) { + return new Dimension(500, 500); + } + + public Dimension minimumLayoutSize(Container target) { + return new Dimension(100, 100); + } + + public void layoutContainer(Container target) { + int barwidth = 0; + int i; + for (i = 1; i < target.getComponentCount(); i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + if (d.width > barwidth) { + barwidth = d.width; + } + } + } + Insets insets = target.insets(); + int targetw = target.size().width - insets.left - insets.right; + int cw = targetw - barwidth; + int targeth = target.size().height - (insets.top + insets.bottom); + target.getComponent(0).move(insets.left, insets.top); + target.getComponent(0).resize(cw, targeth); + cw += insets.left; + int h = insets.top; + for (i = 1; i < target.getComponentCount(); i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + if (m instanceof Scrollbar) { + d.width = barwidth; + } + if (m instanceof Label) { + h += d.height / 5; + d.width = barwidth; + } + m.move(cw, h); + m.resize(d.width, d.height); + h += d.height; + } + } + } +}; + +class BarWavesFrame extends Frame + implements ComponentListener, ActionListener, AdjustmentListener, + MouseMotionListener, MouseListener, ItemListener { + + Thread engine = null; + + Dimension winSize; + Image dbimage; + + Random random; + int maxTerms = 50; + int modeCount; + int maxMaxTerms = 90; + int sampleCount; + double modeTable[][]; + double modeNorms[]; + public static final double epsilon = .0000001; + public static final double epsilon2 = .003; + + public String getAppletInfo() { + return "BarWaves by Paul Falstad"; + } + + Button sineButton; + Button blankButton; + Checkbox stoppedCheck; + Checkbox soundCheck; + Choice modeChooser; + Choice setupChooser; + Vector setupList; + Setup setup; + Choice displayChooser; + Scrollbar dampingBar; + Scrollbar speedBar; + Scrollbar loadBar; + Scrollbar baseFreqBar; + Scrollbar stiffnessBar; + double magcoef[]; + double dampcoef[]; + double phasecoef[]; + double phasecoefcos[]; + double phasecoefadj[]; + double omega[]; + static final double pi = 3.14159265358979323846; + double step; + double func[]; + double funci[]; + int thickness[]; + int xpoints[], ypoints[]; + int selectedCoef; + int magnitudesY; + static final int SEL_NONE = 0; + static final int SEL_FUNC = 1; + static final int SEL_MAG = 2; + static final int MODE_SHAPE = 0; + static final int MODE_FORCE = 1; + static final int MODE_THICKNESS = 2; + static final int DISP_PHASE = 0; + static final int DISP_PHASECOS = 1; + static final int DISP_MODES = 2; + static final int BOUND_HINGED = 0; + static final int BOUND_FREE = 1; + static final int BOUND_CLAMPED = 2; + int selection; + int dragX, dragY; + boolean dragging; + boolean java2present; + double t; + int pause; + Color gray1 = new Color(76, 76, 76); + Color gray2 = new Color(127, 127, 127); + + int getrand(int x) { + int q = random.nextInt(); + if (q < 0) { + q = -q; + } + return q % x; + } + + BarWavesCanvas cv; + + BarWavesFrame() { + super("Bar Waves Applet"); + } + + public void init() { + java2present = true; + if (System.getProperty("java.version").indexOf("1.1") == 0) { + java2present = false; + } + setupList = new Vector(); + Setup s = new FreeBarSetup(); + while (s != null) { + setupList.addElement(s); + s = s.createNext(); + } + selectedCoef = -1; + setLayout(new BarWavesLayout()); + cv = new BarWavesCanvas(this); + cv.addComponentListener(this); + cv.addMouseMotionListener(this); + cv.addMouseListener(this); + add(cv); + + setupChooser = new Choice(); + int i; + for (i = 0; i != setupList.size(); i++) { + setupChooser.add("Setup: " + + ((Setup) setupList.elementAt(i)).getName()); + } + setup = (Setup) setupList.elementAt(0); + setupChooser.addItemListener(this); + add(setupChooser); + + add(sineButton = new Button("Fundamental")); + sineButton.addActionListener(this); + add(blankButton = new Button("Clear")); + blankButton.addActionListener(this); + stoppedCheck = new Checkbox("Stopped"); + stoppedCheck.addItemListener(this); + add(stoppedCheck); + soundCheck = new Checkbox("Sound", false); + soundCheck.addItemListener(this); + add(soundCheck); + + modeChooser = new Choice(); + modeChooser.add("Mouse = Shape bar"); + modeChooser.add("Mouse = Apply static force"); + modeChooser.addItemListener(this); + add(modeChooser); + + displayChooser = new Choice(); + displayChooser.add("Display Phases"); + displayChooser.add("Display Phase Cosines"); + displayChooser.add("Display Modes"); + displayChooser.addItemListener(this); + add(displayChooser); + + add(new Label("Simulation Speed", Label.CENTER)); + add(speedBar = new Scrollbar(Scrollbar.HORIZONTAL, 166, 1, 24, 300)); + speedBar.addAdjustmentListener(this); + + add(new Label("Damping", Label.CENTER)); + add(dampingBar = new Scrollbar(Scrollbar.HORIZONTAL, 10, 1, 0, 400)); + dampingBar.addAdjustmentListener(this); + + add(new Label("Resolution", Label.CENTER)); + add(loadBar = new Scrollbar(Scrollbar.HORIZONTAL, + maxTerms, 1, 40, maxMaxTerms + )); + loadBar.addAdjustmentListener(this); + setLoadCount(); + + add(new Label("Base Frequency", Label.CENTER)); + add(baseFreqBar = new Scrollbar(Scrollbar.HORIZONTAL, + 84, 12, 30, 168 + )); + baseFreqBar.addAdjustmentListener(this); + baseFreqBar.disable(); + + add(new Label("String Stiffness", Label.CENTER)); + add(stiffnessBar = new Scrollbar(Scrollbar.HORIZONTAL, 10, 1, 0, 100)); + stiffnessBar.addAdjustmentListener(this); + stiffnessBar.disable(); + + try { + String param = null; //applet.getParameter("PAUSE"); + if (param != null) { + pause = Integer.parseInt(param); + } + } catch (Exception e) { + } + + magcoef = new double[maxMaxTerms]; + phasecoef = new double[maxMaxTerms]; + phasecoefcos = new double[maxMaxTerms]; + phasecoefadj = new double[maxMaxTerms]; + func = new double[maxMaxTerms + 1]; + funci = new double[maxMaxTerms + 1]; + xpoints = new int[4]; + ypoints = new int[4]; + + random = new Random(); + setDamping(); + reinit(); + cv.setBackground(Color.black); + cv.setForeground(Color.lightGray); + resize(500, 500); + handleResize(); + show(); + } + + void reinit() { + doFundamental(); + } + + void handleResize() { + Dimension d = winSize = cv.getSize(); + if (winSize.width == 0) { + return; + } + dbimage = createImage(d.width, d.height); + } + + void doFundamental() { + doBlank(); + magcoef[0] = 1; + if (soundCheck.getState()) { + doPlay(); + } + } + + void doBlank() { + int x; + for (x = 0; x <= sampleCount; x++) { + func[x] = 0; + } + transform(true); + } + + void transform(boolean novel) { + int x, y; + t = 0; + for (y = 0; y != modeCount; y++) { + double a = 0; + double b = 0; + for (x = 1; x != sampleCount; x++) { + a += modeTable[x][y] * func[x]; + b -= modeTable[x][y] * funci[x]; + } + a /= modeNorms[y]; + b /= omega[y] * modeNorms[y]; + if (a < epsilon && a > -epsilon) { + a = 0; + } + if (b < epsilon && b > -epsilon) { + b = 0; + } + if (novel) { + b = 0; + } + double r = java.lang.Math.sqrt(a * a + b * b); + magcoef[y] = r; + double ph2 = java.lang.Math.atan2(b, a); + phasecoefadj[y] = ph2; + phasecoef[y] = ph2; + } + } + + int getPanelHeight() { + return winSize.height / 3; + } + + void centerString(Graphics g, String s, int y) { + FontMetrics fm = g.getFontMetrics(); + g.drawString(s, (winSize.width - fm.stringWidth(s)) / 2, y); + } + + public void paint(Graphics g) { + cv.repaint(); + } + + public void updateBarWaves(Graphics realg) { + if (!java2present) { + centerString(realg, "Need java2 for this applet.", 100); + return; + } + if (winSize == null || winSize.width == 0) { + return; + } + Graphics g = dbimage.getGraphics(); + boolean allQuiet = true; + if (!stoppedCheck.getState()) { + int val = speedBar.getValue() - 100; + //if (val > 40) + //val += getrand(10); + double tadd = java.lang.Math.exp(val / 20.) * (.1 / 50); + // add random crap into the time to avoid aliasing + tadd *= 1 + getrand(300) * (.00191171); + t += tadd; + } + g.setColor(cv.getBackground()); + g.fillRect(0, 0, winSize.width, winSize.height); + g.setColor(cv.getForeground()); + int i; + int panelHeight = getPanelHeight(); + int midy = panelHeight / 2; + int halfPanel = panelHeight / 2; + double ymult = .75 * halfPanel; + for (i = -1; i <= 1; i++) { + g.setColor((i == 0) ? gray2 : gray1); + g.drawLine(0, midy + (i * (int) ymult), + winSize.width, midy + (i * (int) ymult) + ); + } + g.setColor(gray2); + g.drawLine(winSize.width / 2, midy - (int) ymult, + winSize.width / 2, midy + (int) ymult + ); + int sampStart = (setup.leftBoundary() == BOUND_FREE) ? 1 : 0; + int sampEnd = sampleCount - + ((setup.rightBoundary() == BOUND_FREE) ? 1 : 0); + if (dragging && selection == SEL_FUNC) { + g.setColor(Color.cyan); + allQuiet = true; + for (i = sampStart; i <= sampEnd; i++) { + int x = winSize.width * i / sampleCount; + int y = midy - (int) (ymult * func[i]); + drawBarPiece(g, x, y, i, sampStart); + } + } + if (!stoppedCheck.getState() && !dragging) { + for (i = 0; i != modeCount; i++) { + magcoef[i] *= dampcoef[i]; + } + } + + double magcoefdisp[] = magcoef; + double phasecoefdisp[] = phasecoef; + double phasecoefcosdisp[] = phasecoefcos; + + if (!(dragging && selection == SEL_FUNC)) { + g.setColor(Color.white); + int j; + for (j = 0; j != modeCount; j++) { + if (magcoef[j] < epsilon && magcoef[j] > -epsilon) { + magcoef[j] = phasecoef[j] = phasecoefadj[j] = 0; + continue; + } + allQuiet = false; + phasecoef[j] = (omega[j] * t + phasecoefadj[j]) % (2 * pi); + if (phasecoef[j] > pi) { + phasecoef[j] -= 2 * pi; + } else if (phasecoef[j] < -pi) { + phasecoef[j] += 2 * pi; + } + phasecoefcos[j] = java.lang.Math.cos(phasecoef[j]); + } + + for (i = sampStart; i <= sampEnd; i++) { + int x = winSize.width * i / sampleCount; + double dy = 0; + for (j = 0; j != modeCount; j++) { + dy += magcoefdisp[j] * + modeTable[i][j] * phasecoefcosdisp[j]; + } + func[i] = dy; + int y = midy - (int) (ymult * dy); + drawBarPiece(g, x, y, i, sampStart); + } + if (setup.getThickness() == 0) { + if (setup.leftBoundary() == BOUND_FREE) { + drawPin(g, 1, midy, ymult); + } + if (setup.rightBoundary() == BOUND_FREE) { + drawPin(g, sampleCount - 1, midy, ymult); + } + } + } + if (selectedCoef != -1 && !dragging && + ( + magcoefdisp[selectedCoef] > .04 || + magcoefdisp[selectedCoef] < -.04 + )) { + g.setColor(Color.yellow); + ymult *= magcoefdisp[selectedCoef]; + for (i = sampStart; i <= sampEnd; i++) { + int x = winSize.width * i / sampleCount; + double dy = modeTable[i][selectedCoef] * + phasecoefcosdisp[selectedCoef]; + int y = midy - (int) (ymult * dy); + drawBarPiece(g, x, y, i, sampStart); + } + } + if (selectedCoef != -1) { + int f = getFreq(selectedCoef); + g.setColor(Color.yellow); + centerString(g, f + " Hz", panelHeight); + } else if (soundCheck.getState()) { + int f = getFreq(0); + g.setColor(Color.white); + centerString(g, "Fundamental = " + f + " Hz", panelHeight); + } + int termWidth = getTermWidth(); + ymult = .6 * halfPanel; + g.setColor(Color.white); + if (displayChooser.getSelectedIndex() == DISP_PHASE || + displayChooser.getSelectedIndex() == DISP_PHASECOS) { + magnitudesY = panelHeight; + } else { + magnitudesY = panelHeight * 2; + } + midy = magnitudesY + (panelHeight / 2) + (int) ymult / 2; + g.setColor(gray2); + g.drawLine(0, midy, winSize.width, midy); + g.setColor(gray1); + g.drawLine(0, midy - (int) ymult, winSize.width, midy - (int) ymult); + g.drawLine(0, midy + (int) ymult, winSize.width, midy + (int) ymult); + g.drawLine(0, midy - (int) ymult / 4, winSize.width, midy - (int) ymult / 4); + g.drawLine(0, midy + (int) ymult / 4, winSize.width, midy + (int) ymult / 4); + int dotSize = termWidth - 3; + if (dotSize < 3) { + dotSize = 3; + } + for (i = 0; i != modeCount; i++) { + int t = termWidth * i + termWidth / 2; + int y = midy - (int) (logcoef(magcoefdisp[i]) * ymult); + g.setColor(i == selectedCoef ? Color.yellow : Color.white); + g.drawLine(t, midy, t, y); + g.fillOval(t - dotSize / 2, y - dotSize / 2, dotSize, dotSize); + } + + if (displayChooser.getSelectedIndex() == DISP_PHASE || + displayChooser.getSelectedIndex() == DISP_PHASECOS) { + g.setColor(Color.white); + boolean cosines = + displayChooser.getSelectedIndex() == DISP_PHASECOS; + ymult = .75 * halfPanel; + midy = ((panelHeight * 5) / 2); + for (i = -2; i <= 2; i++) { + if (cosines && (i == 1 || i == -1)) { + continue; + } + g.setColor((i == 0) ? gray2 : gray1); + g.drawLine(0, midy + (i * (int) ymult) / 2, + winSize.width, midy + (i * (int) ymult) / 2 + ); + } + if (!cosines) { + ymult /= pi; + } + for (i = 0; i != modeCount; i++) { + int t = termWidth * i + termWidth / 2; + double ph = (cosines) ? phasecoefcosdisp[i] : phasecoefdisp[i]; + if (magcoef[i] > -epsilon2 / 4 && magcoefdisp[i] < epsilon2 / 4) { + ph = 0; + } + int y = midy - (int) (ph * ymult); + g.setColor(i == selectedCoef ? Color.yellow : Color.white); + g.drawLine(t, midy, t, y); + g.fillOval(t - dotSize / 2, y - dotSize / 2, dotSize, dotSize); + } + } else if (displayChooser.getSelectedIndex() == DISP_MODES) { + int sqw = (winSize.width - 25) / 3; + int sqh = (int) (sqw / pi); + int topY = panelHeight; + int leftX = 0; + int ox, oy = -1; + for (i = 0; i != modeCount; i++) { + if (!( + magcoefdisp[i] > .06 || + magcoefdisp[i] < -.06 + )) { + continue; + } + g.setColor(gray2); + int centerX = leftX + sqw / 2; + int centerY = topY + sqh / 2; + g.drawLine(leftX, centerY, leftX + sqw, centerY); + g.drawLine(centerX, topY, centerX, topY + sqh); + g.setColor(i == selectedCoef ? Color.yellow : Color.white); + g.drawRect(leftX, topY, sqw, sqh); + ox = -1; + ymult = sqh * .5 * magcoefdisp[i]; + int j; + for (j = sampStart; j <= sampEnd; j++) { + int x = leftX + sqw * j / sampleCount; + double dy = modeTable[j][i] * phasecoefcosdisp[i]; + int y = centerY - (int) (ymult * dy); + if (ox != -1) { + g.drawLine(ox, oy, x, y); + } + ox = x; + oy = y; + } + leftX += sqw + 10; + if (leftX + sqw > winSize.width) { + leftX = 0; + topY += sqh + 10; + if (topY + sqh > panelHeight * 2) { + break; + } + } + } + } + realg.drawImage(dbimage, 0, 0, this); + if (!stoppedCheck.getState() && !allQuiet) { + cv.repaint(pause); + } + } + + void drawPin(Graphics g, int pos, int midy, double ymult) { + int x = winSize.width * pos / sampleCount; + g.setColor(gray2); + g.drawLine(x, (int) (midy - ymult), + x, (int) (midy + ymult) + ); + g.setColor(Color.white); + g.fillOval(x - 2, midy - (int) (func[pos] * ymult) - 2, 5, 5); + } + + int getTermWidth() { + int termWidth = winSize.width / modeCount; + int maxTermWidth = winSize.width / 30; + if (termWidth > maxTermWidth) { + termWidth = maxTermWidth; + } + termWidth &= ~1; + return termWidth; + } + + void getVelocities() { + int k, j; + for (j = 0; j != sampleCount; j++) { + double dy = 0; + for (k = 0; k != modeCount; k++) { + dy += magcoef[k] * modeTable[j][k] * + java.lang.Math.sin(phasecoef[k]) * omega[k]; + } + funci[j] = -dy; + } + } + + void drawBarPiece(Graphics g, int x, int y, int i, int sampStart) { + int thick = setup.getThickness(); + xpoints[0] = xpoints[3]; + ypoints[0] = ypoints[3]; + xpoints[1] = xpoints[2]; + ypoints[1] = ypoints[2]; + xpoints[2] = x; + ypoints[2] = y - thick; + xpoints[3] = x; + ypoints[3] = y + thick; + if (i != sampStart) { + if (thick == 0) { + g.drawLine(xpoints[0], ypoints[0], xpoints[2], ypoints[2]); + } else { + g.fillPolygon(xpoints, ypoints, 4); + } + } + } + + void edit(MouseEvent e) { + if (selection == SEL_NONE) { + return; + } + int x = e.getX(); + int y = e.getY(); + switch (selection) { + case SEL_MAG: + editMag(x, y); + break; + case SEL_FUNC: + editFunc(x, y); + break; + } + } + + void editMag(int x, int y) { + if (selectedCoef == -1) { + return; + } + int panelHeight = getPanelHeight(); + double ymult = .6 * panelHeight / 2; + double midy = magnitudesY + (panelHeight / 2) + (int) ymult / 2; + double coef = -(y - midy) / ymult; + coef = unlogcoef(coef); + if (coef < -1) { + coef = -1; + } + if (coef > 1) { + coef = 1; + } + if (magcoef[selectedCoef] == coef) { + return; + } + magcoef[selectedCoef] = coef; + cv.repaint(pause); + } + + void editFunc(int x, int y) { + if (modeChooser.getSelectedIndex() == MODE_FORCE) { + editFuncForce(x, y); + return; + } + if (dragX == x) { + editFuncPoint(x, y); + dragY = y; + } else { + // need to draw a line from old x,y to new x,y and + // call editFuncPoint for each point on that line. yuck. + int x1 = (x < dragX) ? x : dragX; + int y1 = (x < dragX) ? y : dragY; + int x2 = (x > dragX) ? x : dragX; + int y2 = (x > dragX) ? y : dragY; + dragX = x; + dragY = y; + for (x = x1; x <= x2; x++) { + y = y1 + (y2 - y1) * (x - x1) / (x2 - x1); + editFuncPoint(x, y); + } + } + } + + double logep2 = 0; + + double logcoef(double x) { + if (x >= .25 || x <= -.25) { + return x; + } + x *= 4; + double ep2 = epsilon2; + int sign = (x < 0) ? -1 : 1; + x *= sign; + if (x < ep2) { + return 0; + } + if (logep2 == 0) { + logep2 = -java.lang.Math.log(2 * ep2); + } + return .25 * sign * (java.lang.Math.log(x + ep2) + logep2) / logep2; + } + + double unlogcoef(double x) { + if (x >= .25 || x <= -.25) { + return x; + } + double ep2 = epsilon2; + int sign = (x < 0) ? -1 : 1; + return .25 * sign * (java.lang.Math.exp(4 * x * sign * logep2 - logep2) - ep2); + } + + void editFuncPoint(int x, int y) { + int panelHeight = getPanelHeight(); + int midy = panelHeight / 2; + int halfPanel = panelHeight / 2; + int periodWidth = winSize.width; + double ymult = .75 * halfPanel; + int lox = x * sampleCount / periodWidth; + int hix = ((x + 1) * sampleCount - 1) / periodWidth; + double val = (midy - y) / ymult; + if (val > 1) { + val = 1; + } + if (val < -1) { + val = -1; + } + if (lox < 1) { + lox = 1; + } + if (hix >= sampleCount) { + hix = sampleCount - 1; + } + for (; lox <= hix; lox++) { + if (modeChooser.getSelectedIndex() == MODE_THICKNESS) { + thickness[lox] = (midy < y) ? (y - midy) * 2 : (midy - y) * 2; + if (thickness[lox] == 0) { + thickness[lox] = 1; + } + } else { + func[lox] = val; + funci[lox] = 0; + } + } + func[sampleCount] = func[0]; + cv.repaint(pause); + if (soundCheck.getState() == false) { + transform(false); + } + } + + void editFuncForce(int x, int y) { + int panelHeight = getPanelHeight(); + int midy = panelHeight / 2; + int halfPanel = panelHeight / 2; + int periodWidth = winSize.width; + double ymult = .75 * halfPanel; + int ax = x * sampleCount / periodWidth; + double val = (midy - y) / ymult; + if (val > 1) { + val = 1; + } + if (val < -1) { + val = -1; + } + if (ax < 1 || ax >= sampleCount) { + return; + } + + // solve equation A x = b, using diagonalized A, + // A = M Lambda M^-1. x = M Lambda^-1 M^-1 b. b is + // all zeros except at ax where it is 1. + double q[] = new double[modeCount]; + int i, j; + + // multiply b by Lambda^-1 M^-1. M^-1 has eigenvectors as rows + // (with the norm of each eigenvector divided out). Lambda^-1 + // is a diagonal matrix with 1/lambdas as elements (lambda=omega^2) + for (i = 0; i != modeCount; i++) { + q[i] = modeTable[ax][i] / (omega[i] * omega[i] * modeNorms[i]); + } + + // multiply q by M to get result. M has eigenvectors as columns. + for (i = 0; i != sampleCount; i++) { + double dy = 0; + for (j = 0; j != modeCount; j++) { + dy += q[j] * modeTable[i][j]; + } + func[i] = dy; + } + + // ok now we just scale the whole thing so we get the answer + // we want at ax. + double mult = val / func[ax]; + for (i = 0; i <= sampleCount; i++) { + func[i] *= mult; + funci[i] = 0; + } + + cv.repaint(pause); + if (soundCheck.getState() == false) { + transform(true); + } + } + + public void componentHidden(ComponentEvent e) { + } + + public void componentMoved(ComponentEvent e) { + } + + public void componentShown(ComponentEvent e) { + cv.repaint(pause); + } + + public void componentResized(ComponentEvent e) { + handleResize(); + cv.repaint(pause); + } + + public void actionPerformed(ActionEvent e) { + if (e.getSource() == sineButton) { + doFundamental(); + cv.repaint(); + } + if (e.getSource() == blankButton) { + doBlank(); + cv.repaint(); + } + } + + public void adjustmentValueChanged(AdjustmentEvent e) { + System.out.print(((Scrollbar) e.getSource()).getValue() + "\n"); + if (e.getSource() == dampingBar || e.getSource() == speedBar) { + setDamping(); + } + if (e.getSource() == loadBar) { + setLoadCount(); + } + if (e.getSource() == stiffnessBar) { + genModes(); + } + cv.repaint(pause); + } + + public boolean handleEvent(Event ev) { + if (ev.id == Event.WINDOW_DESTROY) { + //if (applet == null) dispose(); + //else applet.destroyFrame(); + return true; + } + return super.handleEvent(ev); + } + + void setLoadCount() { + setup = (Setup) + setupList.elementAt(setupChooser.getSelectedIndex()); + sampleCount = maxTerms = loadBar.getValue(); + step = pi / sampleCount; + int x, y; + thickness = new int[sampleCount + 1]; + int i; + for (i = 0; i <= sampleCount; i++) { + thickness[i] = 5; + } + genModes(); + setDamping(); + } + + void setDamping() { + int i; + dampcoef = new double[modeCount]; + double tadd = java.lang.Math.exp((speedBar.getValue() - 100) / 20.) * (.1 / 50); + for (i = 0; i != modeCount; i++) { + double damper = java.lang.Math.exp(dampingBar.getValue() / 40 - 3) * 30; + if (dampingBar.getValue() <= 2) { + damper = 0; + } + double damp2 = omega[i] * java.lang.Math.sqrt( + java.lang.Math.sqrt(1 + damper * damper / (omega[i] * omega[i])) - 1); + dampcoef[i] = java.lang.Math.exp(-damp2 * tadd * .004); + } + } + + public void mouseDragged(MouseEvent e) { + dragging = true; + edit(e); + } + + public void mouseMoved(MouseEvent e) { + if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { + return; + } + int x = e.getX(); + int y = e.getY(); + dragX = x; + dragY = y; + int panelHeight = getPanelHeight(); + int oldCoef = selectedCoef; + selectedCoef = -1; + selection = 0; + if (y < panelHeight) { + selection = SEL_FUNC; + } + if (y >= magnitudesY && y < magnitudesY + panelHeight) { + int termWidth = getTermWidth(); + selectedCoef = x / termWidth; + if (selectedCoef >= modeCount) { + selectedCoef = -1; + } + if (selectedCoef != -1) { + selection = SEL_MAG; + } + } + if (selectedCoef != oldCoef) { + cv.repaint(pause); + } + } + + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2 && selectedCoef != -1) { + int i; + for (i = 0; i != modeCount; i++) { + if (selectedCoef != i) { + magcoef[i] = 0; + } + } + magcoef[selectedCoef] = 1; + cv.repaint(pause); + } + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + if (!dragging && selectedCoef != -1) { + selectedCoef = -1; + cv.repaint(pause); + } + } + + public void mousePressed(MouseEvent e) { + if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0) { + return; + } + if (selection == SEL_FUNC) { + getVelocities(); + } + dragging = true; + edit(e); + } + + public void mouseReleased(MouseEvent e) { + if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0) { + return; + } + if (dragging && selection == SEL_FUNC) { + if (modeChooser.getSelectedIndex() == MODE_THICKNESS) { + genModes(); + } else { + transform(false); + if (soundCheck.getState()) { + doPlay(); + } + } + } + if (dragging && selection == SEL_MAG && soundCheck.getState()) { + doPlay(); + } + dragging = false; + cv.repaint(pause); + } + + public void itemStateChanged(ItemEvent e) { + if (e.getItemSelectable() == stoppedCheck) { + cv.repaint(pause); + return; + } + if (e.getItemSelectable() == soundCheck) { + if (soundCheck.getState()) { + speedBar.setValue(250); + dampingBar.setValue(170); + baseFreqBar.enable(); + setDamping(); + doPlay(); + } else { + baseFreqBar.disable(); + } + } + if (e.getItemSelectable() == displayChooser) { + cv.repaint(pause); + } + if (e.getItemSelectable() == setupChooser) { + setLoadCount(); + if (setup instanceof StiffStringSetup) { + stiffnessBar.enable(); + } else { + stiffnessBar.disable(); + } + } + } + + void dodiff(double matrix[][], int r, int i, int n, double mult) { + if (i < 1 && setup.leftBoundary() == BOUND_HINGED) { + return; + } + if (i > sampleCount - 1 && setup.rightBoundary() == BOUND_HINGED) { + return; + } + + if (n == 2 && !(setup instanceof StringSetup)) { + if (i <= 1 && setup.leftBoundary() == BOUND_FREE) { + return; + } + if (i >= sampleCount - 1 && setup.rightBoundary() == BOUND_FREE) { + return; + } + } + + if (n > 0) { + dodiff(matrix, r, i - 1, n - 2, -mult); + dodiff(matrix, r, i + 1, n - 2, -mult); + dodiff(matrix, r, i, n - 2, mult * 2); + return; + } + if (i >= 1 && i <= sampleCount - 1) { + matrix[r][i] += mult; + } + } + + void genModes() { + int n = sampleCount - 1; + double matrix[][] = new double[n + 1][n + 1]; + double d[] = new double[n + 1]; + double e[] = new double[n + 1]; + int i, j; + for (i = 1; i <= n; i++) { + setup.doMatrixStep(matrix, i, n); + } + + if (setup instanceof StringSetup) { + if (setup.leftBoundary() == BOUND_FREE) { + matrix[1][1]--; + } + if (setup.rightBoundary() == BOUND_FREE) { + matrix[n][n]--; + } + } + + for (j = 1; j <= n; j++) { + for (i = 1; i <= n; i++) { + System.out.print(matrix[i][j] + " "); + } + System.out.print("\n"); + } + + tred2(matrix, n, d, e); + tqli(d, e, n, matrix); + + modeCount = sampleCount - 1; + omega = new double[modeCount]; + + // now get the eigenvalues and sort them + int omegamap[] = new int[sampleCount]; + for (i = j = 0; i != n; i++) { + if (d[i + 1] < 1e-8) { + modeCount--; + continue; + } + omega[j] = java.lang.Math.sqrt(d[i + 1]); + omegamap[j] = i; + j++; + } + + int si, sj; + // sort the omegas + for (si = 1; si < modeCount; si++) { + double v = omega[si]; + int vm = omegamap[si]; + sj = si; + while (omega[sj - 1] > v) { + omega[sj] = omega[sj - 1]; + omegamap[sj] = omegamap[sj - 1]; + sj--; + if (sj <= 0) { + break; + } + } + omega[sj] = v; + omegamap[sj] = vm; + } + + modeTable = new double[sampleCount + 1][modeCount]; + modeNorms = new double[modeCount]; + for (i = 0; i != modeCount; i++) { + int om = omegamap[i] + 1; + double maxf = 0; + for (j = 0; j != sampleCount; j++) { + modeTable[j][i] = matrix[j][om]; + if (modeTable[j][i] > maxf) { + maxf = modeTable[j][i]; + } + if (-modeTable[j][i] > maxf) { + maxf = -modeTable[j][i]; + } + } + modeNorms[i] = 1 / (maxf * maxf); + for (j = 0; j != sampleCount; j++) { + modeTable[j][i] /= maxf; + } + } + + double mult = 1 / omega[0]; + for (i = 0; i != modeCount; i++) { + omega[i] *= mult; + } + } + + + void tred2(double a[][], int n, double d[], double e[]) { + int l, k, j, i; + double scale, hh, h, g, f; + + // this loop gets faster as i decreases + for (i = n; i >= 2; i--) { + l = i - 1; + h = scale = 0.0; + if (l > 1) { + for (k = 1; k <= l; k++) { + scale += java.lang.Math.abs(a[i][k]); + } + if (scale == 0.0) { + e[i] = a[i][l]; + } else { + for (k = 1; k <= l; k++) { + a[i][k] /= scale; + h += a[i][k] * a[i][k]; + } + f = a[i][l]; + g = (f >= 0.0 ? -java.lang.Math.sqrt(h) : java.lang.Math.sqrt(h)); + e[i] = scale * g; + h -= f * g; + a[i][l] = f - g; + f = 0.0; + for (j = 1; j <= l; j++) { + a[j][i] = a[i][j] / h; + g = 0.0; + for (k = 1; k <= j; k++) { + g += a[j][k] * a[i][k]; + } + for (k = j + 1; k <= l; k++) { + g += a[k][j] * a[i][k]; + } + e[j] = g / h; + f += e[j] * a[i][j]; + } + hh = f / (h + h); + for (j = 1; j <= l; j++) { + f = a[i][j]; + e[j] = g = e[j] - hh * f; + for (k = 1; k <= j; k++) { + a[j][k] -= (f * e[k] + g * a[i][k]); + } + } + } + } else { + e[i] = a[i][l]; + } + d[i] = h; + } + d[1] = 0.0; + e[1] = 0.0; + /* Contents of this loop can be omitted if eigenvectors not + wanted except for statement d[i]=a[i][i]; */ + // speed decreases as i increases + for (i = 1; i <= n; i++) { + l = i - 1; + if (d[i] != 0) { + for (j = 1; j <= l; j++) { + g = 0.0; + for (k = 1; k <= l; k++) { + g += a[i][k] * a[k][j]; + } + for (k = 1; k <= l; k++) { + a[k][j] -= g * a[k][i]; + } + } + } + d[i] = a[i][i]; + a[i][i] = 1.0; + for (j = 1; j <= l; j++) { + a[j][i] = a[i][j] = 0.0; + } + } + } + + // this is from Numerical Recipes in C. It finds the eigenvalues + // and eigenvectors of an nxn tridiagonal symmetric matrix specified + // by d[] and e[]. + void tqli(double d[], double e[], int n, double z[][]) { + int m, l, iter, i, k; + double s, r, p, g, f, dd, c, b; + + for (i = 2; i <= n; i++) { + e[i - 1] = e[i]; + } + e[n] = 0.0; + // faster as l increases + for (l = 1; l <= n; l++) { + iter = 0; + do { + for (m = l; m <= n - 1; m++) { + dd = java.lang.Math.abs(d[m]) + java.lang.Math.abs(d[m + 1]); + if ((double) (java.lang.Math.abs(e[m]) + dd) == dd) { + break; + } + } + if (m != l) { + if (iter++ == 30) { + System.out.print("Too many iterations in tqli\n"); + } + g = (d[l + 1] - d[l]) / (2.0 * e[l]); + r = pythag(g, 1.0); + g = d[m] - d[l] + e[l] / (g + SIGN(r, g)); + s = c = 1.0; + p = 0.0; + for (i = m - 1; i >= l; i--) { + f = s * e[i]; + b = c * e[i]; + e[i + 1] = (r = pythag(f, g)); + if (r == 0.0) { + d[i + 1] -= p; + e[m] = 0.0; + break; + } + s = f / r; + c = g / r; + g = d[i + 1] - p; + r = (d[i] - g) * s + 2.0 * c * b; + d[i + 1] = g + (p = s * r); + g = c * r - b; + for (k = 1; k <= n; k++) { + f = z[k][i + 1]; + z[k][i + 1] = s * z[k][i] + c * f; + z[k][i] = c * z[k][i] - s * f; + } + } + if (r == 0.0 && i >= l) { + continue; + } + d[l] -= p; + e[l] = g; + e[m] = 0.0; + } + } while (m != l); + } + } + + double SIGN(double a, double b) { + return b >= 0 ? java.lang.Math.abs(a) : -java.lang.Math.abs(a); + } + + double SQR(double a) { + return a * a; + } + + double pythag(double a, double b) { + double absa, absb; + absa = java.lang.Math.abs(a); + absb = java.lang.Math.abs(b); + if (absa > absb) { + return absa * java.lang.Math.sqrt(1.0 + SQR(absb / absa)); + } else { + return (absb == 0.0 ? 0.0 : absb * java.lang.Math.sqrt(1.0 + SQR(absa / absb))); + } + } + + abstract class Setup { + abstract String getName(); + + abstract Setup createNext(); + + abstract int leftBoundary(); + + abstract int rightBoundary(); + + int getThickness() { + return 3; + } + + void doMatrixStep(double matrix[][], int i, int n) { + dodiff(matrix, i, i, 4, 1); + } + } + + ; + + class FreeBarSetup extends Setup { + String getName() { + return "bar, free"; + } + + Setup createNext() { + return new HingedBarSetup(); + } + + int leftBoundary() { + return BOUND_FREE; + } + + int rightBoundary() { + return BOUND_FREE; + } + } + + ; + + class HingedBarSetup extends Setup { + String getName() { + return "bar, hinged"; + } + + Setup createNext() { + return new ClampedBarSetup(); + } + + int leftBoundary() { + return BOUND_HINGED; + } + + int rightBoundary() { + return BOUND_HINGED; + } + } + + ; + + class ClampedBarSetup extends Setup { + String getName() { + return "bar, clamped"; + } + + Setup createNext() { + return new ClampedFreeBarSetup(); + } + + int leftBoundary() { + return BOUND_CLAMPED; + } + + int rightBoundary() { + return BOUND_CLAMPED; + } + } + + ; + + class ClampedFreeBarSetup extends Setup { + String getName() { + return "bar, clamped/free"; + } + + Setup createNext() { + return new HingedClampedBarSetup(); + } + + int leftBoundary() { + return BOUND_CLAMPED; + } + + int rightBoundary() { + return BOUND_FREE; + } + } + + ; + + class HingedClampedBarSetup extends Setup { + String getName() { + return "bar, hinged/clamped"; + } + + Setup createNext() { + return new StringSetup(); + } + + int leftBoundary() { + return BOUND_HINGED; + } + + int rightBoundary() { + return BOUND_CLAMPED; + } + } + + ; + + class StringSetup extends Setup { + String getName() { + return "string, pinned"; + } + + void doMatrixStep(double matrix[][], int i, int n) { + dodiff(matrix, i, i, 2, 1); + } + + Setup createNext() { + return new String1FreeSetup(); + } + + int leftBoundary() { + return BOUND_HINGED; + } + + int rightBoundary() { + return BOUND_HINGED; + } + + int getThickness() { + return 0; + } + } + + ; + + class String1FreeSetup extends StringSetup { + String getName() { + return "string, pinned/free"; + } + + Setup createNext() { + return new String2FreeSetup(); + } + + int rightBoundary() { + return BOUND_FREE; + } + } + + ; + + class String2FreeSetup extends String1FreeSetup { + String getName() { + return "string, free/free"; + } + + Setup createNext() { + return new StiffStringSetup(); + } + + int leftBoundary() { + return BOUND_FREE; + } + } + + ; + + class StiffStringSetup extends StringSetup { + String getName() { + return "stiff string, pinned"; + } + + void doMatrixStep(double matrix[][], int i, int n) { + dodiff(matrix, i, i, 2, 1); + double stiff = stiffnessBar.getValue() * .1; + dodiff(matrix, i, i, 4, stiff); + } + + Setup createNext() { + return new StiffStringClampedSetup(); + } + } + + ; + + class StiffStringClampedSetup extends StiffStringSetup { + String getName() { + return "stiff string, clamped"; + } + + Setup createNext() { + return null; + } + + int leftBoundary() { + return BOUND_CLAMPED; + } + + int rightBoundary() { + return BOUND_CLAMPED; + } + } + + ; + + double sndmin, sndmax; + + int getFreq(int n) { + double stepsize = java.lang.Math.log(2) / 12; + double freq = java.lang.Math.exp(baseFreqBar.getValue() * stepsize); + return (int) (freq * omega[n]); + } + + void doPlay() { + final int rate = 22000; + final int sampcount = rate; + + byte b[] = new byte[sampcount]; + + double stepsize = java.lang.Math.log(2) / 12; + double freq = java.lang.Math.exp(baseFreqBar.getValue() * stepsize); + double n = 2 * pi * freq / rate; + n /= omega[0]; + // filter out frequencies above Nyquist freq + double maxomega = pi / n; + int m = modeCount; + while (m > 0 && omega[m - 1] > maxomega) { + m--; + } + if (m == 0) { + return; + } + // filter out frequencies less than 20 Hz (we do that so that + // they do not throw off the bounds checking of the waveform) + int m0 = 0; + // freq = rate*omega*n/(2*pi) + double minomega = 20 * 2 * pi / (rate * n); + while (m0 < m && omega[m0] < minomega) { + m0++; + } + if (m0 == m) { + return; + } + boolean failed; + int i; + int sampWindow = rate / 40; + int offset = 0; + double lastscale = 1000; + double mag[] = new double[modeCount]; + for (i = 0; i != modeCount; i++) { + mag[i] = magcoef[i]; + } + do { + failed = false; + double mn = (-sndmin > sndmax) ? -sndmin : sndmax; + if (mn < .02) { + mn = .02; + } + double scale = 126 / mn; + if (scale > lastscale) { + scale = lastscale; + } + sndmin = sndmax = 0; + for (i = 0; i != sampWindow; i++) { + double dy = 0; + int j; + int ii = i + offset; + for (j = m0; j != m; j++) { + dy += mag[j] * java.lang.Math.sin(ii * n * omega[j]) * scale; + } + if (dy < sndmin) { + sndmin = dy; + } + if (dy > sndmax) { + sndmax = dy; + } + b[ii] = (byte) dy; + if (dy < -127 || dy > 127) { + failed = true; + } + } + sndmin /= scale; + sndmax /= scale; + if (failed) { + continue; + } + offset += sampWindow; + for (i = 0; i != modeCount; i++) { + mag[i] *= dampcoef[i]; + } + if (offset >= sampcount) { + break; + } + } while (true); + + AudioFormat format = new AudioFormat(rate, 8, 1, true, true); + DataLine.Info info = new DataLine.Info( + SourceDataLine.class, + format + ); + SourceDataLine line = null; + try { + line = (SourceDataLine) AudioSystem.getLine(info); + line.open(format, sampcount); + } catch (Exception e) { + e.printStackTrace(); + } + line.start(); + line.write(b, 0, sampcount); + cv.repaint(); + } +}; diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/GenerateId.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/GenerateId.kt similarity index 90% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/GenerateId.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/GenerateId.kt index d1e72a8..428caa2 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/GenerateId.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/GenerateId.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip +package nl.astraeus.vst.string import java.security.SecureRandom diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/Main.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/Main.kt similarity index 84% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/Main.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/Main.kt index 7b42e14..dd2b8f6 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/Main.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/Main.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip +package nl.astraeus.vst.string import com.zaxxer.hikari.HikariConfig import io.undertow.Undertow @@ -6,10 +6,10 @@ import io.undertow.UndertowOptions import io.undertow.server.session.InMemorySessionManager import io.undertow.server.session.SessionAttachmentHandler import io.undertow.server.session.SessionCookieConfig -import nl.astraeus.vst.chip.db.Database -import nl.astraeus.vst.chip.logger.LogLevel -import nl.astraeus.vst.chip.logger.Logger -import nl.astraeus.vst.chip.web.RequestHandler +import nl.astraeus.vst.string.db.Database +import nl.astraeus.vst.string.logger.LogLevel +import nl.astraeus.vst.string.logger.Logger +import nl.astraeus.vst.string.web.RequestHandler fun main() { Logger.level = LogLevel.DEBUG diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/Settings.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/Settings.kt similarity index 97% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/Settings.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/Settings.kt index 9a76c92..7680d99 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/Settings.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/Settings.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip +package nl.astraeus.vst.string import java.io.File import java.io.FileInputStream @@ -6,7 +6,7 @@ import java.util.* object Settings { var runningAsRoot: Boolean = false - var port = 9000 + var port = 9004 var sslPort = 8443 var connectionTimeout = 30000 diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/BaseDao.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/BaseDao.kt similarity index 98% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/db/BaseDao.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/db/BaseDao.kt index 4ea410f..725fe9a 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/BaseDao.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/BaseDao.kt @@ -1,7 +1,7 @@ -package nl.astraeus.vst.chip.db +package nl.astraeus.vst.string.db import kotlinx.datetime.Instant -import nl.astraeus.vst.chip.logger.log +import nl.astraeus.vst.string.logger.log import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.Timestamp diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Database.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/Database.kt similarity index 98% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Database.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/db/Database.kt index 2276275..8f62db2 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Database.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/Database.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip.db +package nl.astraeus.vst.string.db import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Entity.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/Entity.kt similarity index 87% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Entity.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/db/Entity.kt index 6c82712..9e32f19 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Entity.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/Entity.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip.db +package nl.astraeus.vst.string.db interface Entity { fun getPK(): Array diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Migrations.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/Migrations.kt similarity index 96% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Migrations.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/db/Migrations.kt index 4207f92..754ad3b 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/Migrations.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/Migrations.kt @@ -1,6 +1,6 @@ -package nl.astraeus.vst.chip.db +package nl.astraeus.vst.string.db -import nl.astraeus.vst.chip.logger.log +import nl.astraeus.vst.string.logger.log import java.sql.Connection import java.sql.SQLException import java.sql.Timestamp diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchDao.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/PatchDao.kt similarity index 94% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchDao.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/db/PatchDao.kt index e24d5ba..c74b208 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchDao.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/PatchDao.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip.db +package nl.astraeus.vst.string.db object PatchDao : BaseDao() { diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchEntity.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/PatchEntity.kt similarity index 84% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchEntity.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/db/PatchEntity.kt index 80825d3..1276e7d 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchEntity.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/PatchEntity.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip.db +package nl.astraeus.vst.string.db import kotlinx.datetime.Clock import kotlinx.datetime.Instant diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchEntityQueryProvider.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/PatchEntityQueryProvider.kt similarity index 97% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchEntityQueryProvider.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/db/PatchEntityQueryProvider.kt index cc3650c..3bea56e 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/db/PatchEntityQueryProvider.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/db/PatchEntityQueryProvider.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip.db +package nl.astraeus.vst.string.db import java.sql.ResultSet import java.sql.Types diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Index.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/web/Index.kt similarity index 87% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Index.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/web/Index.kt index 25cda93..d7c91b9 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Index.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/web/Index.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip.web +package nl.astraeus.vst.string.web import kotlinx.html.body import kotlinx.html.head @@ -14,12 +14,12 @@ fun generateIndex(patch: String?): String { if (patch == null) { result.appendHTML(true).html { head { - title { +"VST Chip" } + title { +"VST String" } } body { script { type = "application/javascript" - src = "/vst-chip-worklet-ui.js" + src = "/vst-string-worklet-ui.js" } } } diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/web/RequestHandler.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/web/RequestHandler.kt similarity index 95% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/web/RequestHandler.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/web/RequestHandler.kt index c36c8e1..4b6aaca 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/web/RequestHandler.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/web/RequestHandler.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip.web +package nl.astraeus.vst.string.web import io.undertow.Handlers.websocket import io.undertow.server.HttpHandler @@ -16,10 +16,10 @@ import io.undertow.websockets.core.BufferedTextMessage import io.undertow.websockets.core.WebSocketChannel import io.undertow.websockets.core.WebSockets import io.undertow.websockets.spi.WebSocketHttpExchange -import nl.astraeus.vst.chip.db.PatchDao -import nl.astraeus.vst.chip.db.PatchEntity -import nl.astraeus.vst.chip.db.transaction -import nl.astraeus.vst.chip.generateId +import nl.astraeus.vst.string.db.PatchDao +import nl.astraeus.vst.string.db.PatchEntity +import nl.astraeus.vst.string.db.transaction +import nl.astraeus.vst.string.generateId import java.nio.file.Paths class WebsocketHandler( diff --git a/src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Session.kt b/src/jvmMain/kotlin/nl/astraeus/vst/string/web/Session.kt similarity index 53% rename from src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Session.kt rename to src/jvmMain/kotlin/nl/astraeus/vst/string/web/Session.kt index eb0c91b..068e1c9 100644 --- a/src/jvmMain/kotlin/nl/astraeus/vst/chip/web/Session.kt +++ b/src/jvmMain/kotlin/nl/astraeus/vst/string/web/Session.kt @@ -1,4 +1,4 @@ -package nl.astraeus.vst.chip.web +package nl.astraeus.vst.string.web class VstSession( val patchId: String