mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-17 00:56:07 +09:00
MIDI input test
Former-commit-id: 8ec51637782a2c4185716f3164c837477e648975
This commit is contained in:
323
src/net/torvald/terrarum/StateMidiInputTest.kt
Normal file
323
src/net/torvald/terrarum/StateMidiInputTest.kt
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
package net.torvald.terrarum
|
||||||
|
|
||||||
|
import net.torvald.terrarum.Terrarum.STATE_ID_TEST_INPUT
|
||||||
|
import net.torvald.terrarum.gameactors.roundInt
|
||||||
|
import net.torvald.terrarum.gameworld.toUint
|
||||||
|
import net.torvald.terrarum.virtualcomputer.terminal.ALException
|
||||||
|
import org.lwjgl.BufferUtils
|
||||||
|
import org.lwjgl.openal.AL
|
||||||
|
import org.lwjgl.openal.AL10
|
||||||
|
import org.newdawn.slick.GameContainer
|
||||||
|
import org.newdawn.slick.Graphics
|
||||||
|
import org.newdawn.slick.state.BasicGameState
|
||||||
|
import org.newdawn.slick.state.StateBasedGame
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.ArrayList
|
||||||
|
import javax.sound.midi.*
|
||||||
|
import kotlin.experimental.and
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Midi input test for Spieluhr (this game's version of Note Block)
|
||||||
|
*
|
||||||
|
* Spieluhrs can make sound ranged from C1 to C6
|
||||||
|
* (61 keys, which is the most common Midi Keyboard configuration)
|
||||||
|
*
|
||||||
|
* There is some latency if you are on Windows. Mac and Linux should be okay
|
||||||
|
* because real musicians use Mac anyway, for a reason.
|
||||||
|
*
|
||||||
|
* Created by SKYHi14 on 2017-03-17.
|
||||||
|
*/
|
||||||
|
class StateMidiInputTest : BasicGameState() {
|
||||||
|
|
||||||
|
var midiKeyboard: MidiDevice? = null
|
||||||
|
val beeperSlave = BeeperSlave()
|
||||||
|
|
||||||
|
val preferredDeviceList = arrayOf(
|
||||||
|
"USB MIDI"
|
||||||
|
)
|
||||||
|
val avoidedDeviceList = arrayOf(
|
||||||
|
"Real Time Sequencer"
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val midiDevInfo = MidiSystem.getMidiDeviceInfo()
|
||||||
|
|
||||||
|
midiDevInfo.forEach {
|
||||||
|
//println(it)
|
||||||
|
|
||||||
|
val device = MidiSystem.getMidiDevice(it)
|
||||||
|
try {
|
||||||
|
if (!avoidedDeviceList.contains(device.deviceInfo.name)) {
|
||||||
|
device.transmitter // test if tranmitter available
|
||||||
|
//println("Transmitter: $it")
|
||||||
|
|
||||||
|
midiKeyboard = device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e: MidiUnavailableException) {
|
||||||
|
//println("(no transmitter found)")
|
||||||
|
}
|
||||||
|
|
||||||
|
//println()
|
||||||
|
}
|
||||||
|
|
||||||
|
//midiKeyboard = MidiSystem.getMidiDevice()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun init(container: GameContainer?, game: StateBasedGame?) {
|
||||||
|
if (midiKeyboard != null) {
|
||||||
|
midiKeyboard!!.open()
|
||||||
|
midiKeyboard!!.transmitter.receiver = MidiInputReceiver(beeperSlave)
|
||||||
|
println("Opened Midi device ${midiKeyboard!!.deviceInfo.name}")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
println("Midi keyboard not found, using computer keyboard as a controller.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(container: GameContainer?, game: StateBasedGame?, delta: Int) {
|
||||||
|
beeperSlave.runBeepQueueManager(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getID() = STATE_ID_TEST_INPUT
|
||||||
|
|
||||||
|
override fun render(container: GameContainer, game: StateBasedGame, g: Graphics) {
|
||||||
|
g.font = Terrarum.fontGame
|
||||||
|
g.drawString("Listening from ${midiKeyboard!!.deviceInfo.name}", 10f, 10f)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MidiInputReceiver(val slave: BeeperSlave) : Receiver {
|
||||||
|
override fun send(message: MidiMessage, timeStamp: Long) {
|
||||||
|
//println("MIDI Event ${message}")
|
||||||
|
val parsedEvent = ParseMidiMessage(message)
|
||||||
|
println(parsedEvent ?: "Don't care")
|
||||||
|
if (parsedEvent != null) {
|
||||||
|
if (!parsedEvent.isNoteOff) {
|
||||||
|
slave.enqueueBeep(100, parsedEvent.frequency())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BeeperSlave {
|
||||||
|
|
||||||
|
///////////////////
|
||||||
|
// BEEPER DRIVER //
|
||||||
|
///////////////////
|
||||||
|
|
||||||
|
private val beepMaxLen = 10000
|
||||||
|
// let's regard it as a tracker...
|
||||||
|
private val beepQueue = ArrayList<Pair<Int, Double>>()
|
||||||
|
private var beepCursor = -1
|
||||||
|
private var beepQueueLineExecTimer: Millisec = 0
|
||||||
|
private var beepQueueFired = false
|
||||||
|
|
||||||
|
fun update(delta: Int) {
|
||||||
|
runBeepQueueManager(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runBeepQueueManager(delta: Int) {
|
||||||
|
// start emitTone queue
|
||||||
|
if (beepQueue.size > 0 && beepCursor == -1) {
|
||||||
|
beepCursor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// advance emitTone queue
|
||||||
|
if (beepCursor >= 0 && beepQueueLineExecTimer >= beepQueueGetLenOfPtn(beepCursor)) {
|
||||||
|
beepQueueLineExecTimer -= beepQueueGetLenOfPtn(beepCursor)
|
||||||
|
beepCursor += 1
|
||||||
|
beepQueueFired = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete emitTone queue
|
||||||
|
if (beepCursor >= beepQueue.size) {
|
||||||
|
clearBeepQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// actually play queue
|
||||||
|
if (beepCursor >= 0 && beepQueue.size > 0 && !beepQueueFired) {
|
||||||
|
playTone(beepQueue[beepCursor].first, beepQueue[beepCursor].second)
|
||||||
|
beepQueueFired = true
|
||||||
|
|
||||||
|
// delete sources that is finished. AL is limited to 256 sources. If you exceed it,
|
||||||
|
// we won't get any more sounds played.
|
||||||
|
AL10.alSourcei(oldBeepSource, AL10.AL_BUFFER, 0)
|
||||||
|
AL10.alDeleteSources(oldBeepSource)
|
||||||
|
AL10.alDeleteBuffers(oldBeepBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beepQueueFired) beepQueueLineExecTimer += delta
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBeepQueue() {
|
||||||
|
beepQueue.clear()
|
||||||
|
beepCursor = -1
|
||||||
|
beepQueueLineExecTimer = 0
|
||||||
|
|
||||||
|
//AL.destroy()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueueBeep(duration: Int, freq: Double) {
|
||||||
|
beepQueue.add(Pair(Math.min(duration, beepMaxLen), freq))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun beepQueueGetLenOfPtn(ptnIndex: Int) = beepQueue[ptnIndex].first
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// TONE GENERATOR //
|
||||||
|
////////////////////
|
||||||
|
|
||||||
|
private val sampleRate = 44100
|
||||||
|
private var beepSource: Int = -1
|
||||||
|
private var beepBuffer: Int = -1
|
||||||
|
private var oldBeepSource: Int = -1
|
||||||
|
private var oldBeepBuffer: Int = -1
|
||||||
|
var audioData: ByteBuffer? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param duration : milliseconds
|
||||||
|
* @param rampUp
|
||||||
|
* @param rampDown
|
||||||
|
*
|
||||||
|
* ,---. (true, true) ,---- (true, false) ----. (false, true) ----- (false, false)
|
||||||
|
*/
|
||||||
|
private fun makeAudioData(duration: Millisec, freq: Double,
|
||||||
|
rampUp: Boolean = true, rampDown: Boolean = true): ByteBuffer {
|
||||||
|
val audioData = BufferUtils.createByteBuffer(duration.times(sampleRate).div(1000))
|
||||||
|
|
||||||
|
val realDuration = duration * sampleRate / 1000
|
||||||
|
val chopSize = freq / sampleRate
|
||||||
|
|
||||||
|
val amp = Math.max(4600.0 / freq, 1.0)
|
||||||
|
val nHarmonics = if (freq >= 22050.0) 1
|
||||||
|
else if (freq >= 11025.0) 2
|
||||||
|
else if (freq >= 5512.5) 3
|
||||||
|
else if (freq >= 2756.25) 4
|
||||||
|
else if (freq >= 1378.125) 5
|
||||||
|
else if (freq >= 689.0625) 6
|
||||||
|
else 7
|
||||||
|
|
||||||
|
val transitionThre = 974.47218
|
||||||
|
|
||||||
|
// TODO volume ramping?
|
||||||
|
if (freq == 0.0) {
|
||||||
|
for (x in 0..realDuration - 1) {
|
||||||
|
audioData.put(0x00.toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (freq < transitionThre) { // chopper generator (for low freq)
|
||||||
|
for (x in 0..realDuration - 1) {
|
||||||
|
var sine: Double = amp * Math.cos(Math.PI * 2 * x * chopSize)
|
||||||
|
if (sine > 0.79) sine = 0.79
|
||||||
|
else if (sine < -0.79) sine = -0.79
|
||||||
|
audioData.put(
|
||||||
|
(0.5 + 0.5 * sine).times(0xFF).roundInt().toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // harmonics generator (for high freq)
|
||||||
|
for (x in 0..realDuration - 1) {
|
||||||
|
var sine: Double = 0.0
|
||||||
|
for (k in 1..nHarmonics) { // mix only odd harmonics in order to make a squarewave
|
||||||
|
sine += Math.sin(Math.PI * 2 * (2*k - 1) * chopSize * x) / (2*k - 1)
|
||||||
|
}
|
||||||
|
audioData.put(
|
||||||
|
(0.5 + 0.5 * sine).times(0xFF).roundInt().toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audioData.rewind()
|
||||||
|
|
||||||
|
return audioData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playTone(leninmilli: Int, freq: Double) {
|
||||||
|
audioData = makeAudioData(leninmilli, freq)
|
||||||
|
|
||||||
|
|
||||||
|
if (!AL.isCreated()) AL.create()
|
||||||
|
|
||||||
|
|
||||||
|
// Clear error stack.
|
||||||
|
AL10.alGetError()
|
||||||
|
|
||||||
|
oldBeepBuffer = beepBuffer
|
||||||
|
beepBuffer = AL10.alGenBuffers()
|
||||||
|
checkALError()
|
||||||
|
|
||||||
|
try {
|
||||||
|
AL10.alBufferData(beepBuffer, AL10.AL_FORMAT_MONO8, audioData, sampleRate)
|
||||||
|
checkALError()
|
||||||
|
|
||||||
|
oldBeepSource = beepSource
|
||||||
|
beepSource = AL10.alGenSources()
|
||||||
|
checkALError()
|
||||||
|
|
||||||
|
try {
|
||||||
|
AL10.alSourceQueueBuffers(beepSource, beepBuffer)
|
||||||
|
checkALError()
|
||||||
|
|
||||||
|
AL10.alSource3f(beepSource, AL10.AL_POSITION, 0f, 0f, 1f)
|
||||||
|
AL10.alSourcef(beepSource, AL10.AL_REFERENCE_DISTANCE, 1f)
|
||||||
|
AL10.alSourcef(beepSource, AL10.AL_MAX_DISTANCE, 1f)
|
||||||
|
AL10.alSourcef(beepSource, AL10.AL_GAIN, 0.3f)
|
||||||
|
checkALError()
|
||||||
|
|
||||||
|
AL10.alSourcePlay(beepSource)
|
||||||
|
checkALError()
|
||||||
|
}
|
||||||
|
catch (e: ALException) {
|
||||||
|
AL10.alDeleteSources(beepSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e: ALException) {
|
||||||
|
AL10.alDeleteSources(beepSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom implementation of Util.checkALError() that uses our custom exception.
|
||||||
|
private fun checkALError() {
|
||||||
|
val errorCode = AL10.alGetError()
|
||||||
|
if (errorCode != AL10.AL_NO_ERROR) {
|
||||||
|
throw ALException(errorCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ParseMidiMessage {
|
||||||
|
operator fun invoke(message: MidiMessage): MidiKeyEvent? {
|
||||||
|
val bytes = message.message
|
||||||
|
val header = bytes[0].toUint().ushr(4) // 0b0000 - 0b1111
|
||||||
|
if (header == 0b1000) { // note off
|
||||||
|
return MidiKeyEvent(true, bytes[1].toInt(), bytes[2].toInt()) // no need for uint()
|
||||||
|
}
|
||||||
|
else if (header == 0b1001) { // note on
|
||||||
|
return MidiKeyEvent(false, bytes[1].toInt(), bytes[2].toInt()) // no need for uint()
|
||||||
|
}
|
||||||
|
else { // don't care
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MidiKeyEvent(val isNoteOff: Boolean, val key: Int, val velocity: Int) {
|
||||||
|
override fun toString() = "${if (isNoteOff) "Off" else "On "} $key v$velocity"
|
||||||
|
/**
|
||||||
|
* @param tuning frequency of middle A (default: 440.0)
|
||||||
|
*/
|
||||||
|
fun frequency(tuning: Double = 440.0): Double {
|
||||||
|
val a3 = 69 // midi note number for middle A
|
||||||
|
|
||||||
|
return tuning * Math.pow(2.0, (key - a3) / 12.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ class StateTestingLightning : BasicGameState() {
|
|||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getID() = Terrarum.STATE_ID_TEST_LIGHTNING_GFX
|
override fun getID() = Terrarum.STATE_ID_TEST_GFX
|
||||||
|
|
||||||
|
|
||||||
private var timer = 0
|
private var timer = 0
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class StateUITest : BasicGameState() {
|
|||||||
ui.update(container, delta)
|
ui.update(container, delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getID() = Terrarum.STATE_ID_TEST_UI
|
override fun getID() = Terrarum.STATE_ID_TEST_UI1
|
||||||
|
|
||||||
override fun render(container: GameContainer, game: StateBasedGame, g: Graphics) {
|
override fun render(container: GameContainer, game: StateBasedGame, g: Graphics) {
|
||||||
ui.render(container, game, g)
|
ui.render(container, game, g)
|
||||||
|
|||||||
@@ -131,11 +131,13 @@ object Terrarum : StateBasedGame(GAME_NAME) {
|
|||||||
val STATE_ID_CONFIG_CALIBRATE = 0x11
|
val STATE_ID_CONFIG_CALIBRATE = 0x11
|
||||||
|
|
||||||
val STATE_ID_TEST_FONT = 0x100
|
val STATE_ID_TEST_FONT = 0x100
|
||||||
val STATE_ID_TEST_LIGHTNING_GFX = 0x101
|
val STATE_ID_TEST_GFX = 0x101
|
||||||
val STATE_ID_TEST_TTY = 0x102
|
val STATE_ID_TEST_TTY = 0x102
|
||||||
val STATE_ID_TEST_BLUR = 0x103
|
val STATE_ID_TEST_BLUR = 0x103
|
||||||
val STATE_ID_TEST_SHADER = 0x104
|
val STATE_ID_TEST_SHADER = 0x104
|
||||||
val STATE_ID_TEST_UI = 0x105
|
val STATE_ID_TEST_INPUT = 0x106
|
||||||
|
|
||||||
|
val STATE_ID_TEST_UI1 = 0x110
|
||||||
|
|
||||||
val STATE_ID_TOOL_NOISEGEN = 0x200
|
val STATE_ID_TOOL_NOISEGEN = 0x200
|
||||||
val STATE_ID_TOOL_RUMBLE_DIAGNOSIS = 0x201
|
val STATE_ID_TOOL_RUMBLE_DIAGNOSIS = 0x201
|
||||||
@@ -279,13 +281,14 @@ object Terrarum : StateBasedGame(GAME_NAME) {
|
|||||||
//addState(StateTestingLightning())
|
//addState(StateTestingLightning())
|
||||||
//addState(StateSplash())
|
//addState(StateSplash())
|
||||||
//addState(StateMonitorCheck())
|
//addState(StateMonitorCheck())
|
||||||
addState(StateFontTester())
|
//addState(StateFontTester())
|
||||||
//addState(StateNoiseTexGen())
|
//addState(StateNoiseTexGen())
|
||||||
//addState(StateBlurTest())
|
//addState(StateBlurTest())
|
||||||
//addState(StateShaderTest())
|
//addState(StateShaderTest())
|
||||||
//addState(StateNoiseTester())
|
//addState(StateNoiseTester())
|
||||||
//addState(StateUITest())
|
//addState(StateUITest())
|
||||||
//addState(StateControllerRumbleTest())
|
//addState(StateControllerRumbleTest())
|
||||||
|
addState(StateMidiInputTest())
|
||||||
|
|
||||||
//ingame = StateInGame(); addState(ingame)
|
//ingame = StateInGame(); addState(ingame)
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ class TerrarumComputer(peripheralSlots: Int) {
|
|||||||
|
|
||||||
|
|
||||||
if (!isHalted) {
|
if (!isHalted) {
|
||||||
driveBeepQueueManager(delta)
|
runBeepQueueManager(delta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ class TerrarumComputer(peripheralSlots: Int) {
|
|||||||
private var beepQueueLineExecTimer: Millisec = 0
|
private var beepQueueLineExecTimer: Millisec = 0
|
||||||
private var beepQueueFired = false
|
private var beepQueueFired = false
|
||||||
|
|
||||||
private fun driveBeepQueueManager(delta: Int) {
|
private fun runBeepQueueManager(delta: Int) {
|
||||||
// start emitTone queue
|
// start emitTone queue
|
||||||
if (beepQueue.size > 0 && beepCursor == -1) {
|
if (beepQueue.size > 0 && beepCursor == -1) {
|
||||||
beepCursor = 0
|
beepCursor = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user