Created
March 25, 2016 19:15
-
-
Save Hadyn/2a5bcc3084d463f8423e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.jagex; | |
import com.jagex.io.Buffer; | |
/** | |
* Created by hadyn on 5/1/2015. | |
*/ | |
public class MidiCodec { | |
private static final int EVENT_NOTE_ON = 0x0; | |
private static final int EVENT_NOTE_OFF = 0x1; | |
private static final int EVENT_CONTROL_CHANGE = 0x2; | |
private static final int EVENT_PITCH_BEND = 0x3; | |
private static final int EVENT_CHANNEL_PRESSURE = 0x4; | |
private static final int EVENT_KEY_PRESSURE = 0x5; | |
private static final int EVENT_PROGRAM_CHANGE = 0x6; | |
private static final int EVENT_END_OF_TRACK = 0x7; | |
private static final int EVENT_SET_TEMPO = 0x07 | 0x10; | |
private static final int CONTROLLER_BANK_SELECT = 0; | |
private static final int CONTROLLER_BANK_SELECT_LSB = 32; | |
private static final int CONTROLLER_MODULATION_WHEEL = 1; | |
private static final int CONTROLLER_MODULATION_WHEEL_LSB = 33; | |
private static final int CONTROLLER_CHANNEL_VOLUME = 7; | |
private static final int CONTROLLER_CHANNEL_VOLUME_LSB = 39; | |
private static final int CONTROLLER_PAN = 10; | |
private static final int CONTROLLER_PAN_LSB = 42; | |
private static final int CONTROLLER_NRPN = 99; | |
private static final int CONTROLLER_NRPN_LSB = 98; | |
private static final int CONTROLLER_RPN = 101; | |
private static final int CONTROLLER_RPN_LSB = 100; | |
private static final int CONTROLLER_DAMPER_PEDAL = 64; | |
private static final int CONTROLLER_PORTAMENDO = 65; | |
private static final int CONTROLLER_ALL_SOUND_OFF = 120; | |
private static final int CONTROLLER_RESET_ALL_CONTROLLERS = 121; | |
private static final int CONTROLLER_ALL_NOTES_OFF = 123; | |
private static final int TYPE_END_OF_TRACK = 0x2f; | |
private static final int TYPE_SET_TEMPO = 0x51; | |
byte[] data; | |
public MidiCodec() {} | |
void decode(Buffer input, boolean strict) { | |
// Read the footer which contains the amount | |
// of tracks and time division | |
input.position(input.capacity() - 3); | |
int tracks = input.getUInt8(); | |
int division = input.getUInt16(); | |
int length = 14 + tracks * 10; | |
input.position(0); | |
int tempoCount = 0; | |
int controlChangeCount = 0; | |
int noteOnCount = 0; | |
int noteOffCount = 0; | |
int pitchBendCount = 0; | |
int channelPressureCount = 0; | |
int keyPressureCount = 0; | |
int programChangeCount = 0; | |
// Parse and count each of the event opcodes | |
int i; | |
int j; | |
int track = 0; | |
while(track < tracks) { | |
i = -1; | |
while(true) { | |
j = input.getUInt8(); | |
if(strict || i != j) { | |
++length; | |
} | |
i = j & 0xf; | |
if(j == EVENT_END_OF_TRACK) { | |
++track; | |
break; | |
} | |
if(j == EVENT_SET_TEMPO) { | |
++tempoCount; | |
} else if(i == EVENT_NOTE_ON) { | |
++noteOnCount; | |
} else if(i == EVENT_NOTE_OFF) { | |
++noteOffCount; | |
} else if(i == EVENT_CONTROL_CHANGE) { | |
++controlChangeCount; | |
} else if(i == EVENT_PITCH_BEND) { | |
++pitchBendCount; | |
} else if(i == EVENT_CHANNEL_PRESSURE) { | |
++channelPressureCount; | |
} else if(i == EVENT_KEY_PRESSURE) { | |
++keyPressureCount; | |
} else { | |
if(i != EVENT_PROGRAM_CHANGE) { | |
throw new RuntimeException(); | |
} | |
++programChangeCount; | |
} | |
} | |
} | |
// Append the amount of data for each event | |
length += 5 * tempoCount; | |
length += 2 * (noteOnCount + noteOffCount + controlChangeCount + pitchBendCount + keyPressureCount); | |
length += channelPressureCount + programChangeCount; | |
// TODO: Eventually remove this shit | |
int eventLengthOffset = input.position(); | |
// Parse the delta time for each event | |
int deltaTimeStart = input.position(); | |
int eventCount = tracks + tempoCount + controlChangeCount + noteOnCount + | |
noteOffCount + pitchBendCount + channelPressureCount + | |
keyPressureCount + programChangeCount; | |
for(int l = 0; l < eventCount; l++) { | |
input.getVariableLength(); | |
} | |
// Append the data length of the event delta times | |
// to the length of the decompressed output | |
length += input.position() - deltaTimeStart; | |
// Mark where the controller data begins | |
int controllerOffset = input.position(); | |
// Count each of the controller changes | |
int modulationWheelCount = 0; | |
int modulationWheelMsbCount = 0; | |
int channelVolumeCount = 0; | |
int channelVolumeLsbCount = 0; | |
int panCount = 0; | |
int panLsbCount = 0; | |
int nrpnCount = 0; | |
int nrpnLsbCount = 0; | |
int rpnCount = 0; | |
int rpnLsbCount = 0; | |
int var26 = 0; | |
int toggleCount = 0; | |
int controller = 0; | |
for(int k = 0; k < controlChangeCount; k++) { | |
controller = controller + input.getUInt8() & 127; | |
if(controller != CONTROLLER_BANK_SELECT && controller != CONTROLLER_BANK_SELECT_LSB) { | |
if(controller == CONTROLLER_MODULATION_WHEEL) { | |
++modulationWheelCount; | |
} else if(controller == CONTROLLER_MODULATION_WHEEL_LSB) { | |
++modulationWheelMsbCount; | |
} else if(controller == CONTROLLER_CHANNEL_VOLUME) { | |
++channelVolumeCount; | |
} else if(controller == CONTROLLER_CHANNEL_VOLUME_LSB) { | |
++channelVolumeLsbCount; | |
} else if(controller == CONTROLLER_PAN) { | |
++panCount; | |
} else if(controller == CONTROLLER_PAN_LSB) { | |
++panLsbCount; | |
} else if(controller == CONTROLLER_NRPN) { | |
++nrpnCount; | |
} else if(controller == CONTROLLER_NRPN_LSB) { | |
++nrpnLsbCount; | |
} else if(controller == CONTROLLER_RPN) { | |
++rpnCount; | |
} else if(controller == CONTROLLER_RPN_LSB) { | |
++rpnLsbCount; | |
} else if(controller != CONTROLLER_DAMPER_PEDAL && controller != CONTROLLER_PORTAMENDO && | |
controller != CONTROLLER_ALL_SOUND_OFF && controller != CONTROLLER_RESET_ALL_CONTROLLERS && | |
controller != CONTROLLER_ALL_NOTES_OFF) { | |
++toggleCount; | |
} else { | |
++var26; | |
} | |
} else { | |
++programChangeCount; | |
} | |
} | |
// Calculate each of the offsets for all of the data to encode | |
// the midi file with. | |
int opcodeOffset = 0; | |
int toggleOffset = input.position(); | |
input.skip(var26); | |
int keyPressureOffset = input.position(); | |
input.skip(keyPressureCount); | |
int channelPressureOffset = input.position(); | |
input.skip(channelPressureCount); | |
int var33 = input.position(); | |
input.skip(pitchBendCount); | |
int modulationWheelOffset = input.position(); | |
input.skip(modulationWheelCount); | |
int channelVolumeOffset = input.position(); | |
input.skip(channelVolumeCount); | |
int panOffset = input.position(); | |
input.skip(panCount); | |
int keyOffset = input.position(); | |
input.skip(noteOnCount + noteOffCount + keyPressureCount); | |
int onVelocityOffset = input.position(); | |
input.skip(noteOnCount); | |
int effectOffset = input.position(); | |
input.skip(toggleCount); | |
int offVelocityOffset = input.position(); | |
input.skip(noteOffCount); | |
int modulationWheelLsbOffset = input.position(); | |
input.skip(modulationWheelMsbCount); | |
int channelVolumeLsbOffset = input.position(); | |
input.skip(channelVolumeLsbCount); | |
int panLsbOffset = input.position(); | |
input.skip(panLsbCount); | |
int programChangeOffset = input.position(); | |
input.skip(programChangeCount); | |
int var45 = input.position(); | |
input.skip(pitchBendCount); | |
int nrpnOffset = input.position(); | |
input.skip(nrpnCount); | |
int nrpnLsbOffset = input.position(); | |
input.skip(nrpnLsbCount); | |
int rpnOffset = input.position(); | |
input.skip(rpnCount); | |
int rpnLsbOffset = input.position(); | |
input.skip(rpnLsbCount); | |
int tempoSetOffset = input.position(); | |
input.skip(tempoCount * 3); | |
// Create a buffer to put the encoded midi file | |
data = new byte[length]; | |
Buffer output = new Buffer(data); | |
// Encode the header chunk to the output buffer, big endian | |
// 0: int32 -> 'MThd' | |
// 4: int32 -> chunk_len | |
// 8: int16 -> format | |
// 10: int16 -> tracks | |
// 12: int16 -> division | |
// | |
// format notes | |
// ------------------------------------------------------------ | |
// format 0 -> Single track | |
// format 1 -> One or more simultaneous tracks | |
// format 2 -> One or more independent tracks | |
// | |
// division notes | |
// ------------------------------------------------------------ | |
// For the division value the MSB determines what the division | |
// value defines, if the MSB | |
output.putInt32(0x4d546864); | |
output.putInt32(6); | |
output.putInt16(tracks > 1 ? 1 : 0); | |
output.putInt16(tracks); | |
output.putInt16(division); | |
// Position the buffer to read the event lengths | |
input.position(eventLengthOffset); | |
// Encode each of the track chunks to the output buffer | |
// int32 -> 'MTtk' | |
// varlen -> delta_time | |
int[] controllers = new int[128]; | |
controller = 0; | |
int channel = 0; | |
int key = 0; | |
int onVelocity = 0; | |
int offVelocity = 0; | |
int pitch = 0; | |
int channelPressure = 0; | |
int keyPressure = 0; | |
// Write each of the tracks | |
track = 0; | |
while(track < tracks) { | |
output.putInt32(0x4d54726b); | |
output.skip(4); // Skip writing the length until the end | |
int chunkStart = output.position(); | |
int type = -1; | |
// Write each of the events | |
while(true) { | |
int deltaTimeOffset = input.getVariableLength(); | |
output.putVariableLength(deltaTimeOffset); | |
int opcode = input.getUInt8(opcodeOffset++); | |
boolean updated = strict || type != opcode; | |
type = opcode & 0xf; | |
// Meta events | |
// -------------------------------------------------------------------------------- | |
// Both 'end_of_track' and 'set_tempo' are meta events and are denoted with the first | |
// nibble of their opcode being 0b0111, however the set tempo is annotated with | |
// the 0x80 flag to differentiate the opcodes so that the meta event byte header | |
// isn't written on change. | |
// | |
// Format: | |
// | |
// 0xff <meta_type : int8> <length : varlen> <data : int8[length]> | |
// | |
// | |
// System exclusive events | |
// ------------------------------------------------------------------------------- | |
// In this implementation system exclusive events are not encoded. | |
// | |
// Midi event | |
// ------------------------------------------------------------------------------- | |
// Note on, note off, control change, pitch bend, channel pressure, key pressure, | |
// and program change are midi events. The LS nibble of the encoded opcode represents | |
// the event type while opcode may have encoded the 0x10 flag which represents a change | |
// in the target channel when encoding the event. | |
// | |
// When encoding events unless in strict mode the codec will not encode the event type, this | |
// will only occur if the event type changes or target channel is updated. | |
// | |
// Opcode format: | |
// | |
// xxxctttt | |
// | |
// - x is unused | |
// - c is the channel control, if it is set to one it flips the channel (0 or 1) currently being used. | |
// - t is the event type. | |
// | |
// Encode an end of track event | |
if(opcode == EVENT_END_OF_TRACK) { | |
if(updated) { | |
output.putInt8(0xff); | |
} | |
output.putInt8(TYPE_END_OF_TRACK); // Event type | |
output.putInt8(0); // Length | |
// Encode the length of the track to the buffer and stop parsing the | |
// current track | |
output.putIntLength(output.position() - chunkStart); | |
track++; | |
break; | |
} | |
if(opcode == EVENT_SET_TEMPO) { | |
if(updated) { | |
output.putInt8(0xff); | |
} | |
output.putInt8(TYPE_SET_TEMPO); // Type | |
output.putInt8(3); // Length | |
output.putInt8(input.getUInt8(tempoSetOffset++)); | |
output.putInt8(input.getUInt8(tempoSetOffset++)); | |
output.putInt8(input.getUInt8(tempoSetOffset++)); | |
} else { | |
channel ^= opcode >> 4; | |
if(type == EVENT_NOTE_ON) { | |
// Note on | |
if(updated) { | |
output.putInt8(0b10010000 + channel); | |
} | |
key += input.getInt8(keyOffset++); | |
onVelocity += input.getInt8(onVelocityOffset++); | |
output.putInt8(key & 127); | |
output.putInt8(onVelocity & 127); | |
} else if(type == EVENT_NOTE_OFF) { | |
// Note off | |
if(updated) { | |
output.putInt8(0b10000000 + channel); | |
} | |
key += input.getInt8(keyOffset++); | |
offVelocity += input.getInt8(offVelocityOffset++); | |
output.putInt8(key & 127); | |
output.putInt8(offVelocity & 127); | |
} else if(type == EVENT_CONTROL_CHANGE) { | |
// Controller change | |
if(updated) { | |
output.putInt8(0b10110000 + channel); | |
} | |
controller = controller + input.getInt8(controllerOffset++) & 127; | |
output.putInt8(controller); | |
byte delta; | |
if(controller != CONTROLLER_BANK_SELECT && controller != CONTROLLER_BANK_SELECT_LSB) { | |
if(controller == CONTROLLER_MODULATION_WHEEL) { | |
delta = input.getInt8(modulationWheelOffset++); | |
} else if(controller == CONTROLLER_MODULATION_WHEEL_LSB) { | |
delta = input.getInt8(modulationWheelLsbOffset++); | |
} else if(controller == CONTROLLER_CHANNEL_VOLUME) { | |
delta = input.getInt8(channelVolumeOffset++); | |
} else if(controller == CONTROLLER_CHANNEL_VOLUME_LSB) { | |
delta = input.getInt8(channelVolumeLsbOffset++); | |
} else if(controller == CONTROLLER_PAN) { | |
delta = input.getInt8(panOffset++); | |
} else if(controller == CONTROLLER_PAN_LSB) { | |
delta = input.getInt8(panLsbOffset++); | |
} else if(controller == CONTROLLER_NRPN) { | |
delta = input.getInt8(nrpnOffset++); | |
} else if(controller == CONTROLLER_NRPN_LSB) { | |
delta = input.getInt8(nrpnLsbOffset++); | |
} else if(controller == CONTROLLER_RPN) { | |
delta = input.getInt8(rpnOffset++); | |
} else if(controller == CONTROLLER_RPN_LSB) { | |
delta = input.getInt8(rpnLsbOffset++); | |
} else if(controller != CONTROLLER_DAMPER_PEDAL && controller != CONTROLLER_PORTAMENDO && | |
controller != CONTROLLER_ALL_SOUND_OFF && controller != CONTROLLER_RESET_ALL_CONTROLLERS && | |
controller != CONTROLLER_ALL_NOTES_OFF) { | |
delta = input.getInt8(effectOffset++); | |
} else { | |
delta = input.getInt8(toggleOffset++); | |
} | |
} else { | |
delta = input.getInt8(programChangeOffset++); | |
} | |
int value = delta + controllers[controller]; | |
controllers[controller] = value; | |
output.putInt8(value & 127); | |
} else if(type == EVENT_PITCH_BEND) { | |
// Pitch bend change | |
if(updated) { | |
output.putInt8(0b11100000 + channel); | |
} | |
pitch += input.getInt8(var45++); | |
pitch += input.getInt8(var33++) << 7; | |
output.putInt8(pitch & 127); | |
output.putInt8(pitch >> 7 & 127); | |
} else if(type == EVENT_CHANNEL_PRESSURE) { | |
// Channel pressure (after touch) | |
if(updated) { | |
output.putInt8(0b11010000 + channel); | |
} | |
channelPressure += input.getInt8(channelPressureOffset++); | |
output.putInt8(channelPressure & 127); | |
} else if(type == EVENT_KEY_PRESSURE) { | |
// Key pressure | |
if(updated) { | |
output.putInt8(0b10100000 + channel); | |
} | |
key += input.getInt8(keyOffset++); | |
keyPressure += input.getInt8(keyPressureOffset++); | |
output.putInt8(key & 127); | |
output.putInt8(keyPressure & 127); | |
} else { | |
if(type != EVENT_PROGRAM_CHANGE) { | |
throw new RuntimeException("Unknown event type"); | |
} | |
// Program change | |
if(updated) { | |
output.putInt8(0b11000000 + channel); | |
} | |
output.putInt8(input.getInt8(programChangeOffset++)); | |
} | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment