mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 20:31:51 +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
|
||||
|
||||
@@ -56,7 +56,7 @@ class StateUITest : BasicGameState() {
|
||||
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) {
|
||||
ui.render(container, game, g)
|
||||
|
||||
@@ -131,11 +131,13 @@ object Terrarum : StateBasedGame(GAME_NAME) {
|
||||
val STATE_ID_CONFIG_CALIBRATE = 0x11
|
||||
|
||||
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_BLUR = 0x103
|
||||
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_RUMBLE_DIAGNOSIS = 0x201
|
||||
@@ -279,13 +281,14 @@ object Terrarum : StateBasedGame(GAME_NAME) {
|
||||
//addState(StateTestingLightning())
|
||||
//addState(StateSplash())
|
||||
//addState(StateMonitorCheck())
|
||||
addState(StateFontTester())
|
||||
//addState(StateFontTester())
|
||||
//addState(StateNoiseTexGen())
|
||||
//addState(StateBlurTest())
|
||||
//addState(StateShaderTest())
|
||||
//addState(StateNoiseTester())
|
||||
//addState(StateUITest())
|
||||
//addState(StateControllerRumbleTest())
|
||||
addState(StateMidiInputTest())
|
||||
|
||||
//ingame = StateInGame(); addState(ingame)
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ class TerrarumComputer(peripheralSlots: Int) {
|
||||
|
||||
|
||||
if (!isHalted) {
|
||||
driveBeepQueueManager(delta)
|
||||
runBeepQueueManager(delta)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@ class TerrarumComputer(peripheralSlots: Int) {
|
||||
private var beepQueueLineExecTimer: Millisec = 0
|
||||
private var beepQueueFired = false
|
||||
|
||||
private fun driveBeepQueueManager(delta: Int) {
|
||||
private fun runBeepQueueManager(delta: Int) {
|
||||
// start emitTone queue
|
||||
if (beepQueue.size > 0 && beepCursor == -1) {
|
||||
beepCursor = 0
|
||||
|
||||
Reference in New Issue
Block a user