MidiPlayer/src/com/eatenalive3/midiplayer/Play.java

514 lines
14 KiB
Java

package com.eatenalive3.midiplayer;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequence;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.Track;
public class Play {
public static Synthesizer synthesizer;
public static int instrument = 0;
public static MidiChannel piano;
public static boolean backwards = false;
public static boolean invert = false;
public static int multipleNotes = 0;
public static int roundUpOrDown = 1;
public static int lagBufferTime = 100;
public static int[] pix = new int[RenderSong.HEIGHT];
int[] keySignature = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
boolean[] keyIgnore = { false, false, false, false, false, false, false, false, false, false, false, false };
public static BotSocket socket;
private HashMap<Long, Long> tempo = new HashMap<Long, Long>();
private HashMap<Long, List<Note>> notes = new HashMap<Long, List<Note>>();
private long trackPosition = 0;
private long maxTrackLength = 0;
private double tempoMultiplier = 1;
private long dynamicTempo;
public final int resolution;
public static boolean sustain = false;
public int transpose = 0;
public File mid;
public Play(File mid, String ks) throws InvalidMidiDataException, IOException {
this.mid = mid;
Sequence sequence = MidiSystem.getSequence(mid);
Track[] tracks = sequence.getTracks();
resolution = sequence.getResolution();
int[] songKeySig = new int[12];
for (int trackNumber = 0; trackNumber < tracks.length; trackNumber++) {
Track track = tracks[trackNumber];
boolean pedal = false;
double volumeControl = 1;
for (int i = 0; i < track.size(); i++) {
MidiEvent event = track.get(i);
MidiMessage message = event.getMessage();
if (message instanceof MetaMessage) {
byte[] msg = ((MetaMessage) message).getMessage();
byte[] data = ((MetaMessage) message).getData();
if (msg.length > 1 && msg[1] == 0x51) {
long p1 = (data[0] & 0xFF) << 16;
long p2 = (data[1] & 0xFF) << 8;
long p3 = (data[2] & 0xFF);
tempo.put(event.getTick(), (long) ((p1 + p2 + p3) * 1000 / resolution)); // divide by tempoMultiplier in realtime
}
} else if (message instanceof ShortMessage) {
ShortMessage sm = (ShortMessage) message;
if (sm.getChannel() == 9) { // percussion channel -- ignore
continue;
}
int cmd = sm.getCommand();
if (cmd == 176) {
if (sm.getData1() == 64) {
pedal = sm.getData2() <= 64;
} else if (sm.getData1() == 7) {
// apparently people don't want this. Or at least, Karm doesn't. Lol
//volumeControl = sm.getData2() / 127.0;
}
} else if (cmd == 0x90 || cmd == 0x80) {
final long tick = event.getTick();
if (event.getTick() > maxTrackLength)
maxTrackLength = event.getTick();
double vol = volumeControl * sm.getData2() / 127.0;
if (vol == 0) // a bit of a hack, but eh :P
cmd = 0x80;
int key = sm.getData1();
songKeySig[key % 12]++; // C is 0
if (cmd != 0x80 || !pedal) {
if (notes.get(tick) == null) {
notes.put(tick, new ArrayList<Note>());
}
notes.get(tick).add(new Note(vol, key - 12, trackNumber, cmd == 0x90, ((ShortMessage) message).getChannel()));
}
}
}
}
}
// remove duplicate notes immediately in the list
for (List<Note> noteList : notes.values()) {
Note[] notez = noteList.toArray(new Note[0]);
for (int i = notez.length - 1; i > 0; i--) {
for (int j = i - 1; j >= 0; j--) {
if (notez[i].note == notez[j].note) {
if (notez[i].noteOn)
noteList.remove(notez[j]);
else
noteList.remove(notez[i]);
break;
}
}
}
}
Object[] sig = getKeySignature(songKeySig);
MidiPlayer.log((String) sig[0]);
// don't mess with anything beyond here.
char type;
int value = 0, valueOriginal = 0;
Note n = (Note) sig[1];
String cont = n.toString();
type = Character.toLowerCase(cont.charAt(0));
int sharpOrFlat = cont.contains("#") ? 1 : 0;
boolean major = n.trackNumber == 1;
switch (type) {
case 'c':
value = 0;
break;
case 'd':
value = 2;
break;
case 'e':
value = 4;
break;
case 'f':
value = 5;
break;
case 'g':
value = 7;
break;
case 'a':
value = 9;
break;
case 'b':
value = 11;
break;
default:
value = 1;
}
value += sharpOrFlat;
if (!major) {
int[] base = { 1, 3, 6, 8, 10 };
for (int i = 0; i < 5; i++) {
int poss = value + base[i];
if (poss > 11)
poss -= 12;
if (poss < 0)
poss += 12;
keySignature[poss] = 1;
}
} else {
int[] base = { 2, 4, 6, 9, 11 };
for (int i = 0; i < 5; i++) {
int poss = value + base[i];
if (poss > 11)
poss -= 12;
if (poss < 0)
poss += 12;
keySignature[poss] = 1;
}
}
switch (type) {
case 'c':
valueOriginal = 0;
break;
case 'd':
valueOriginal = 2;
break;
case 'e':
valueOriginal = 4;
break;
case 'f':
valueOriginal = 5;
break;
case 'g':
valueOriginal = 7;
break;
case 'a':
valueOriginal = 9;
break;
case 'b':
valueOriginal = 11;
break;
default:
valueOriginal = 1;
}
valueOriginal += sharpOrFlat;
if (major) {// TODO
int[] base = { 1, 3, 6, 8, 10 };
for (int i = 0; i < 5; i++) {
int poss = valueOriginal + base[i];
if (poss > 11)
poss -= 12;
if (poss < 0)
poss += 12;
keyIgnore[poss] = true;
}
} else {
int[] base = { 2, 4, 6, 9, 11 };
for (int i = 0; i < 5; i++) {
int poss = valueOriginal + base[i];
if (poss > 11)
poss -= 12;
if (poss < 0)
poss += 12;
keyIgnore[poss] = true;
}
}
}
public void play(double tmpo, double offset, int transp) {
offset = Math.max(0, Math.min(offset, 1));
if (tmpo == 0)
tmpo = 0.01;
dynamicTempo = (long) (500000 * 1000 / resolution); // used so that the tempo can change without any issues
transpose = transp;
tempoMultiplier = tmpo;
setPosition(offset);
long now = System.nanoTime(), previous = now, elapsed = 0;
long nanoPerFrame = (long) (dynamicTempo / tempoMultiplier);
while (trackPosition < maxTrackLength + 1) {
now = System.nanoTime();
elapsed += now - previous;
previous = now;
while (elapsed > nanoPerFrame) {
MidiPlayer.setSlider(trackPosition * 100.0 / maxTrackLength);
elapsed -= nanoPerFrame;
nanoPerFrame = (long) (dynamicTempo / tempoMultiplier);
long tpos = backwards ? maxTrackLength - trackPosition++ : trackPosition++;
List<Note> noteList = notes.get(tpos);
if (tempo.get(tpos) != null) {
dynamicTempo = tempo.get(tpos);
nanoPerFrame = (long) (dynamicTempo / tempoMultiplier);
}
if (noteList == null)
continue;
String send = "";
for (Note note : noteList) {
int n = note.note;
int not = n % 12;
if (not < 0)
not = 12 + not;
if (invert && !keyIgnore[not % 12])
n += roundUpOrDown * keySignature[not % 12];
n += transpose;
Note transposed = new Note(note.volume, n, note.trackNumber, note.noteOn, note.instrument);
if ((sustain && !backwards) || transposed.noteOn) { // !backwards because sustain messes up otherise
send += "," + transposed.toJSON(0);
}
if (n < 100 && n > 0) { // good to have some bounds checking just in case
if (transposed.noteOn ^ backwards)
pix[200 - 2 * n] = 1000;
else
pix[200 - 2 * n] = 0;
}
}
if (!send.isEmpty()) {
String snd = send;
for (int i = 0; i < multipleNotes; i++) {
snd += send;
}
socket.send(String.format(Locale.US, "[{\"m\":\"n\",\"t\":%d,\"n\":[%s]}]", System.currentTimeMillis() + BotSocket.serverTimeOffset
+ lagBufferTime, snd.substring(1)));
}
}
Thread.yield();
}
if (trackPosition != maxTrackLength + 1000) {
MidiPlayer.resetPlay();
}
}
public void setTempo(double amt) {
if (amt == 0)
amt = 0.01;
tempoMultiplier = amt;
}
public void stop() {
trackPosition = maxTrackLength + 1000;
pix = new int[RenderSong.HEIGHT];
}
public void setPosition(double pos) {
trackPosition = (long) (maxTrackLength * Math.max(0, Math.min(1, pos)));
for (long i = trackPosition; i >= 0; i--) {
long tpos = backwards ? maxTrackLength - trackPosition : trackPosition;
if (tempo.get(tpos) != null) {
dynamicTempo = tempo.get(tpos);
break;
}
}
pix = new int[RenderSong.HEIGHT];
}
public static Object[] getKeySignature(int[] notes) {
int total = 0;
boolean[] song = new boolean[12];
for (int q = 0; q < 5; q++) {
int min = 0;
for (int i = 0; i < notes.length; i++) {
total += notes[i];
if (notes[i] < notes[min] && !song[i]) {
min = i;
}
}
song[min] = true;
}
if (total == 0) {
return new Object[] { "Could not determine song length.", new Note(0), 0 };
}
for (int i = 0; i < notes.length; i++) {
song[i] = !song[i];
}
for (int i = 0; i < 12; i++) {
if (song[i] && song[(i + 2) % 12] && song[(i + 4) % 12] && song[(i + 5) % 12] && song[(i + 7) % 12] && song[(i + 9) % 12] && song[(i + 11) % 12]) {
double proportion = ((notes[(i + 12 - 4) % 12]) * 1.0 / total);
Note maj = new Note(i);
Note min = new Note((i + 12 - 3) % 12);
if (proportion < 0.5 / 100.0) {
return new Object[] {
"I think the key signature is " + maj + " major. (minorness: " + String.format(Locale.US, "%.1f%%)", proportion * 10000 / 0.5),
maj, (Integer) i };
}
return new Object[] {
"I think the key signature is " + min + " minor. (minorness: " + String.format(Locale.US, "%.1f%%)", proportion * 10000 / 0.5), min,
(Integer) i };
}
}
// oh noes, that didn't work. Let's try it a different way.
int[] sums = new int[12];
for (int i = 0; i < 12; i++) {
for (int q = 0; q < 12; q++) {
if (keySig[0][i][q]) {
sums[i] += notes[i];
}
}
}
int max = 0;
for (int i = 0; i < 12; i++) {
System.out.println(sums[i]);
if (sums[i] > sums[max]) {
max = i;
}
}
double proportion = ((notes[(max + 12 - 3) % 12]) * 1.0 / total);
Note maj = new Note((max) % 12);
Note min = new Note((max + 12 - 3) % 12);
double confidence = sums[max] * 100.0 / total;
if (proportion < 0.5 / 100.0) {
return new Object[] {
"[LOW " + String.format(Locale.US, "%.1f%%", confidence) + " CONFIDENCE] I think the key signature is " + maj + " major. (minorness: "
+ String.format(Locale.US, "%.1f%%)", proportion * 10000 / 0.5), maj, (Integer) max };
}
return new Object[] {
"[LOW " + String.format(Locale.US, "%.1f%%", confidence) + " CONFIDENCE] I think the key signature is " + min + " minor. (minorness: "
+ String.format(Locale.US, "%.1f%%)", proportion * 10000 / 0.5), min, (Integer) max };
}
public static boolean[][][] keySig = new boolean[2][12][12];
static {
// generate Major and Minor key signatures
final int[] minorBase = { 1, 3, 6, 8, 10 };
final int[] majorBase = { 2, 4, 6, 9, 11 };
for (int i = 0; i < 12; i++) {
for (int j = 0; j < 5; j++) {
int poss = i + majorBase[j];
if (poss > 11)
poss -= 12;
if (poss < 0)
poss += 12;
keySig[0][i][poss] = true;
}
for (int j = 0; j < 5; j++) {
int poss = i + minorBase[j];
if (poss > 11)
poss -= 12;
if (poss < 0)
poss += 12;
keySig[1][i][poss] = true;
}
}
}
public void playLocally(double tmpo, double offset, int transp) {
offset = Math.max(0, Math.min(offset, 1));
if (tmpo == 0)
tmpo = 0.01;
dynamicTempo = (long) (500000 * 1000 / resolution); // used so that the tempo can change without any issues
transpose = transp;
tempoMultiplier = tmpo;
setPosition(offset);
long now = System.nanoTime(), previous = now, elapsed = 0;
long nanoPerFrame = (long) (dynamicTempo / tempoMultiplier);
while (trackPosition < maxTrackLength + 1) {
now = System.nanoTime();
elapsed += now - previous;
previous = now;
while (elapsed > nanoPerFrame) {
MidiPlayer.setSlider(trackPosition * 100.0 / maxTrackLength);
elapsed -= nanoPerFrame;
nanoPerFrame = (long) (dynamicTempo / tempoMultiplier);
long tpos = backwards ? maxTrackLength - trackPosition++ : trackPosition++;
List<Note> noteList = notes.get(tpos);
if (tempo.get(tpos) != null) {
dynamicTempo = tempo.get(tpos);
nanoPerFrame = (long) (dynamicTempo / tempoMultiplier);
}
if (noteList == null)
continue;
String send = "";
for (Note note : noteList) {
int n = note.note;
int not = n % 12;
if (not < 0)
not = 12 + not;
if (invert && !keyIgnore[not % 12])
n += roundUpOrDown * keySignature[not % 12];
n += transpose + 12; // add an octave because it's just not right
Note transposed = new Note(note.volume, n, note.trackNumber, note.noteOn, note.instrument);
if ((sustain && !backwards) || transposed.noteOn) { // !backwards because sustain messes up otherise
send += "," + transposed.toJSON(0);
}
if (n < 100 && n > 0) { // good to have some bounds checking just in case
if (transposed.noteOn ^ backwards)
pix[200 - 2 * n] = 1000;
else
pix[200 - 2 * n] = 0;
}
piano.programChange(synthesizer.getAvailableInstruments()[note.instrument].getPatch().getProgram());
if (transposed.noteOn)
piano.noteOn(transposed.note, (int) (transposed.volume * 127));
else
piano.noteOff(transposed.note);
}
}
Thread.yield();
}
if (trackPosition != maxTrackLength + 1000) {
MidiPlayer.resetPlay();
}
}
}