Commits

Erik Byström committed 0bbeb5b

Inital commit

  • Participants

Comments (0)

Files changed (27)

+TinySynth
+
+This is my version of a music package for 4k java games. The synth
+is about 800 bytes compressed with a song of 10 patterns.
+
+/erik.bystrom@gmail.com
+
+
+Configuration
+To setup the number of pattern and tracks to use, please edit TinySynthConfig
+and TinySynthPlayer. Most things are defined as constants in those two files.
+
+
+Usage
+
+F1			Play a song from the start
+F2			Stop playback
+F3			Play from current sequence
+F5			Load a song
+F6			Save a song (save often, the tracker is not stable)
+
+TAB		Switch between sequence, pattern and instrument edit
+SPACE		Off note
+bksp		Delete note and step +1
+delete	Delete note
+A-Z		Set notes
+
+Alt+arrow keys will always affect the sequence editor.
+Ctrl+arrow keys changes the values in the sequence editor and instrument editor.
+If shift is pressed, larger steps will be taken in the value.
+
+When a song is saved, a file with the extension .java is also saved. It contains
+the song as java code in unicode format. Please paste the contents of that file
+into the bottom of F.java to play the song.
+
+It's very important to understand that the instrument transpose is in base 16.
+Which means that to shift one octave you enter 16, two octaves 32. This also
+means that 12,13,14,15 is not used and will cause the player to crash.
+
+
+File Format
+
+It's a very simple file format. First comes all the sequences, then all
+instruments and lastly all patterns. Take a look at F.java or 
+TinySynthConfig.java for example code of how to read the data.
+<project default="jar">
+	<target name="compile">
+		<mkdir dir="bin"/>
+		<javac srcdir="src" destdir="bin" debug="true" />
+	</target>
+	
+	<target name="jar" depends="compile">
+		<jar destfile="tracker.jar" basedir="bin" includes="**/*.class">
+			<manifest>
+				<attribute name="Main-Class" value="tracker.Tracker" />
+			</manifest>
+		</jar>
+	</target>
+</project>

portal.4ks

Binary file added.

src/tracker/TinySynthConfig.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import tracker.Unicode.Writer;
+import tracker.config.IntegerProperty;
+import tracker.config.Property;
+import tracker.config.StringProperty;
+import tracker.config.TrackerCallbacks;
+import tracker.config.TrackerConfig;
+import tracker.core.Cursor;
+import tracker.core.model.Pattern;
+import tracker.core.model.Song;
+
+public class TinySynthConfig extends TrackerConfig implements TrackerCallbacks {
+	private static final int INSTRUMENT_SIZE = 32;
+	private static final int OCTAVE_OFFSET = 2;
+	private final TinySynthPlayer player;
+
+	public TinySynthConfig() {
+		super("TinySynth", 16, 10, 4, 32);
+
+		addProperty(new StringProperty(TinySynthPlayer.OSC_TYPE, "Osc Type", "SINE", "SQUARE", "NOISE"));
+		addProperty(new IntegerProperty(TinySynthPlayer.PITCH, "Transpose", 0, 255, 0));
+		addProperty(new IntegerProperty(TinySynthPlayer.VOLUME, "Volume", 0, 64, 64));
+		addProperty(new IntegerProperty(TinySynthPlayer.DELAY, "Delay", 0, 255, 16));
+		addProperty(new IntegerProperty(TinySynthPlayer.DELAY_FEEDBACK, "Delay Feedback", 0, 127, 32));
+		addProperty(new IntegerProperty(TinySynthPlayer.DELAY_MIX, "Delay Mix", 0, 127, 32));
+		addProperty(new IntegerProperty(TinySynthPlayer.ENV_ATTACK, "Attack", 0, 128, 10));
+		addProperty(new IntegerProperty(TinySynthPlayer.ENV_DECAY, "Decay", 0, 128, 10));
+
+		setCallbacks(this);
+
+		player = new TinySynthPlayer();
+	}
+
+	@Override
+	public void start(final Cursor cursor, final Song song) {
+		player.start(cursor, convert(song));
+	}
+
+	@Override
+	public void stop() {
+		player.stop();
+	}
+
+	@Override
+	public void updated(final Song song) {
+		player.use(convert(song));
+	}
+
+	private int[][] convert(final Song song) {
+		try {
+			final ByteArrayOutputStream out = new ByteArrayOutputStream();
+			convert(out, song);
+			out.close();
+
+			final byte[] array = out.toByteArray();
+			final int[][] output = new int[array.length / 32][32];
+
+			for (int i = 0; i < array.length; i++) {
+				output[i >> 5][i & 0x1f] = array[i];
+			}
+
+			return output;
+		} catch (final IOException e) {
+			return null;
+		}
+	}
+
+	@Override
+	public Song load(final File file) throws Exception {
+		final Song song = new Song(this);
+		final FileInputStream in = new FileInputStream(file);
+
+		readSequences(in, song);
+		readInstruments(in, song);
+		readPattern(in, song);
+
+		in.close();
+		return song;
+	}
+
+	private void readPattern(final FileInputStream in, final Song song) throws IOException {
+		final int length = getPatternLength();
+		for (int i = 0; i < getPatterns(); i++) {
+			final Pattern pattern = song.getPattern(i);
+			final byte[] bytes = new byte[length];
+			in.read(bytes);
+			for (int j = 0; j < bytes.length; j++) {
+				int n = bytes[j];
+				if (n < 2) {
+					pattern.getNotes()[j] = n;
+				} else {
+					n += OCTAVE_OFFSET * 16;
+					pattern.getNotes()[j] = (2 + ((n - 2) % 16) + 12 * ((n - 2) / 16));
+				}
+
+			}
+		}
+	}
+
+	private void readInstruments(final FileInputStream in, final Song song) throws IOException {
+		final int[][] instruments = song.getInstruments();
+		for (int i = 0; i < getTracks(); i++) {
+			final byte[] bytes = new byte[INSTRUMENT_SIZE];
+			final int[] instrument = instruments[i];
+			Arrays.fill(instrument, 0);
+			in.read(bytes);
+
+			int j = 0;
+			for (final Property p : getInstrument()) {
+				instrument[j++] = bytes[p.bit()];
+			}
+		}
+	}
+
+	private void readSequences(final FileInputStream in, final Song song) throws IOException {
+		for (int i = 0; i < getTracks(); i++) {
+			final int[] sequence = song.getSequenceForTrack(i);
+			final byte[] bytes = new byte[sequence.length];
+			in.read(bytes);
+			for (int j = 0; j < bytes.length; j++) {
+				sequence[j] = bytes[j];
+			}
+		}
+	}
+
+	@Override
+	public void save(final File file, final Song song) throws Exception {
+		final FileOutputStream out = new FileOutputStream(file);
+		convert(out, song);
+		out.close();
+
+		final Writer writer = new Unicode.Writer();
+		final FileInputStream source = new FileInputStream(file);
+
+		while (true) {
+			final int value = source.read();
+			if (value < 0) {
+				break;
+			}
+			writer.write8(value);
+		}
+		source.close();
+
+		final FileOutputStream dest = new FileOutputStream(new File(file.toString() + ".java"));
+
+		// write song
+		final String data = String.format("private static final String song_data=\"%s\";", writer.asString());
+		dest.write(data.getBytes());
+
+		// write frequencies
+		final float WAVE_BUFFER = TinySynthPlayer.WAVE_BUFFER;
+		final float SAMPLE_RATE = TinySynthPlayer.SAMPLE_RATE;
+
+		final int[] rate = { (int) (65.41f * WAVE_BUFFER / SAMPLE_RATE), (int) (69.30f * WAVE_BUFFER / SAMPLE_RATE),
+				(int) (73.42f * WAVE_BUFFER / SAMPLE_RATE), (int) (77.78f * WAVE_BUFFER / SAMPLE_RATE),
+				(int) (82.41f * WAVE_BUFFER / SAMPLE_RATE), (int) (87.31f * WAVE_BUFFER / SAMPLE_RATE),
+				(int) (92.50f * WAVE_BUFFER / SAMPLE_RATE), (int) (98.00f * WAVE_BUFFER / SAMPLE_RATE),
+				(int) (103.83f * WAVE_BUFFER / SAMPLE_RATE), (int) (110.00f * WAVE_BUFFER / SAMPLE_RATE),
+				(int) (116.54f * WAVE_BUFFER / SAMPLE_RATE), (int) (123.47f * WAVE_BUFFER / SAMPLE_RATE) };
+
+		final Writer f = new Unicode.Writer();
+		for (int i = 0; i < rate.length; i++) {
+			f.write16(rate[i]);
+		}
+		final String freq = String.format("\nprivate static final String frequencies = \"%s\";", f.asString());
+		dest.write(freq.getBytes());
+
+		dest.close();
+	}
+
+	private void convert(final OutputStream out, final Song song) throws IOException {
+		writeSequences(out, song);
+		writeInstruments(out, song);
+		writePatterns(out, song);
+	}
+
+	private void writePatterns(final OutputStream out, final Song song) throws IOException {
+		final int length = getPatternLength();
+		for (int i = 0; i < getPatterns(); i++) {
+			final Pattern pattern = song.getPattern(i);
+			final byte[] bytes = new byte[length];
+			for (int j = 0; j < bytes.length; j++) {
+				int n = pattern.getNotes()[j];
+				if (n < 2) {
+					bytes[j] = (byte) n;
+				} else {
+					n -= OCTAVE_OFFSET * 12;
+					bytes[j] = (byte) (2 + ((n - 2) % 12) + 16 * ((n - 2) / 12));
+				}
+			}
+			out.write(bytes);
+		}
+	}
+
+	private void writeSequences(final OutputStream out, final Song song) throws IOException {
+		for (int i = 0; i < getTracks(); i++) {
+			final int[] sequence = song.getSequenceForTrack(i);
+			final byte[] bytes = new byte[sequence.length];
+			for (int j = 0; j < bytes.length; j++) {
+				bytes[j] = (byte) sequence[j];
+			}
+			out.write(bytes);
+		}
+	}
+
+	private void writeInstruments(final OutputStream out, final Song song) throws IOException {
+		final int[][] instruments = song.getInstruments();
+		for (int i = 0; i < getTracks(); i++) {
+			final int[] instrument = instruments[i];
+			final byte[] bytes = new byte[INSTRUMENT_SIZE];
+			Arrays.fill(bytes, (byte) 0);
+
+			int j = 0;
+			for (final Property p : getInstrument()) {
+				bytes[p.bit()] = (byte) instrument[j++];
+			}
+
+			out.write(bytes);
+		}
+	}
+}

src/tracker/TinySynthPlayer.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker;
+
+import java.util.Arrays;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.SourceDataLine;
+
+import tracker.core.Cursor;
+
+public class TinySynthPlayer implements Runnable {
+	public static final int NUM_TRACKS = 4;
+	public static final int SEQ_LENGTH = 16;
+	public static final int TRACK_LENGTH = 32;
+	public static final int INSTRUMENT_SIZE = 32;
+	public static final int SEQ_WRAP = SEQ_LENGTH * TRACK_LENGTH - 1;
+	//
+	public static final int SEQUENCE_OFFSET = 0;
+	public static final int INSTRUMENT_OFFSET = 2;
+	public static final int PATTERN_OFFSET = INSTRUMENT_OFFSET + NUM_TRACKS;
+	//
+	public static final int SAMPLE_RATE = 44100 / 2;
+	public static final int TICKS_PER_BEAT = 4;
+	public static final int TICKS_PER_MINUTE = 125 / 2;
+	//
+	public static final int SAMPLES_PER_TICK = 60 * SAMPLE_RATE / (TICKS_PER_BEAT * TICKS_PER_MINUTE);
+	public static final int BUFFER_SIZE = 2 * SAMPLES_PER_TICK;
+	public static final int WAVE_BUFFER = 65536;
+	public static final int WAVE_BUFFER_MASK = 65535;
+	public static final float WAVE_FREQ = WAVE_BUFFER / SAMPLE_RATE * 440f;
+	public static final int DELAY_SIZE = 1 << 18;
+	public static final int DELAY_MASK = DELAY_SIZE - 1;
+	//
+	public static final int FP = 16;
+	public static final int FP_S1 = (1 << (FP - 1)) - 2560;
+	public static final int FP_U1 = (1 << FP) - 2560;
+	//
+	public static final int PITCH = 0;
+	public static final int DELAY = 1;
+	public static final int DELAY_FEEDBACK = 2;
+	public static final int DELAY_MIX = 3;
+	public static final int ENV_RATE = 4; // +0 +1 +2=0
+	public static final int ENV_LEVEL = 6; // +0=0 +1 +2 +3
+	public static final int ENV_STAGE = 10;
+	public static final int DELAY_POS = 11;
+	public static final int OSC1_RATE = 12;
+	public static final int OSC1_PHASE = 13;
+	public static final int ARP_STAGE = 14;
+	public static final int ARP_SPEED = 15;
+	public static final int ARP_STEP_1 = 16; // = 0
+	public static final int ARP_STEP_2 = 17; // = diff
+	public static final int VOLUME = 18;
+	public static final int OSC_TYPE = 19;
+
+	//
+	public static final int ENV_ATTACK = ENV_RATE + 0;
+	public static final int ENV_DECAY = ENV_RATE + 1;
+
+	//
+	private Thread thread;
+	private int[][] song;
+	private final int[][] delay = new int[NUM_TRACKS][DELAY_SIZE];
+	private boolean quit;
+	private int sequence;
+
+	public void stop() {
+		if (thread != null) {
+			thread.interrupt();
+			thread = null;
+			quit = true;
+		}
+	}
+
+	public void start(final Cursor cursor, final int[][] song) {
+		use(song);
+
+		sequence = cursor.getSequence() * TRACK_LENGTH;
+		stop();
+		quit = false;
+		thread = new Thread(this);
+		thread.start();
+	}
+
+	public void use(final int[][] song) {
+		if (this.song != null) {
+			for (int track = 0; track < NUM_TRACKS; track++) {
+				final int[] src = this.song[INSTRUMENT_OFFSET + track];
+				final int[] dst = song[INSTRUMENT_OFFSET + track];
+
+				dst[OSC1_PHASE] = src[OSC1_PHASE];
+				dst[OSC1_RATE] = src[OSC1_RATE];
+				dst[ENV_STAGE] = src[ENV_STAGE];
+			}
+		}
+
+		for (int i = 0; i < delay.length; i++) {
+			Arrays.fill(delay[i], 0);
+		}
+
+		this.song = song;
+		for (int track = 0; track < NUM_TRACKS; track++) {
+			final int[] ins = song[INSTRUMENT_OFFSET + track];
+			ins[ENV_LEVEL + 1] = FP_U1;
+		}
+	}
+
+	public void run() {
+		// TODO: inline the wave generation
+		// TODO: lower sample rate if the tones drift too much
+		try {
+			// =====================================================================================================
+			// Setup audio
+			// =====================================================================================================
+			final AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, SAMPLE_RATE, 16, 1, 2, SAMPLE_RATE, false);
+			final SourceDataLine line = AudioSystem.getSourceDataLine(format);
+			line.open(format, BUFFER_SIZE);
+			line.start();
+
+			final byte[] out = new byte[BUFFER_SIZE];
+			int index, offset;
+
+			// =====================================================================================================
+			// Song data
+			// =====================================================================================================
+
+			final int[] rate = { (int) (65.41f * WAVE_BUFFER / SAMPLE_RATE), (int) (69.30f * WAVE_BUFFER / SAMPLE_RATE),
+					(int) (73.42f * WAVE_BUFFER / SAMPLE_RATE), (int) (77.78f * WAVE_BUFFER / SAMPLE_RATE),
+					(int) (82.41f * WAVE_BUFFER / SAMPLE_RATE), (int) (87.31f * WAVE_BUFFER / SAMPLE_RATE),
+					(int) (92.50f * WAVE_BUFFER / SAMPLE_RATE), (int) (98.00f * WAVE_BUFFER / SAMPLE_RATE),
+					(int) (103.83f * WAVE_BUFFER / SAMPLE_RATE), (int) (110.00f * WAVE_BUFFER / SAMPLE_RATE),
+					(int) (116.54f * WAVE_BUFFER / SAMPLE_RATE), (int) (123.47f * WAVE_BUFFER / SAMPLE_RATE) };
+
+			// Generate wave forms
+			final int[][] wave = new int[3][WAVE_BUFFER];
+			for (index = 0; index < WAVE_BUFFER; index++) {
+				// wave[0][index] = FP_S1 - ((index & (WAVE_BUFFER / 2)) << 1);
+				wave[0][index] = (int) (FP_S1 * (float) Math.sin(index * (2f * 3.1415f / WAVE_BUFFER)));
+				wave[1][index] = index < (WAVE_BUFFER >> 1) ? FP_S1 : -FP_S1;
+				wave[2][index] = (int) (FP_U1 * (float) Math.random()) - FP_S1;
+				// wave[1][index] = index - FP_S1;
+			}
+
+			// =====================================================================================================
+			// Only continue here if everything went well
+			// =====================================================================================================
+
+			while (!quit) {
+				for (int track = 0; track < NUM_TRACKS; track++) {
+					final int[] ins = song[INSTRUMENT_OFFSET + track];
+
+					// Setup envelope
+					final int pattern = song[SEQUENCE_OFFSET + (track >> 1)][((track & 1) * SEQ_LENGTH) + (sequence >> 5)];
+					final int note = song[PATTERN_OFFSET + pattern][sequence & 0x1f];
+
+					if (note == 1 && ins[ENV_STAGE] < (2 << FP)) {
+						ins[ENV_STAGE] = (2 << FP);
+					}
+					if (note > 1) {
+						ins[OSC1_RATE] = rate[(note - 2 + ins[PITCH]) & 0xf] << ((note - 2 + ins[PITCH]) >> 4);
+						ins[ENV_STAGE] = 0;
+					}
+					for (index = 0, offset = 0; index < SAMPLES_PER_TICK; index++, offset += 2) {
+						if (track == 0) {
+							// Clear output buffer
+							out[offset] = 0;
+							out[offset + 1] = 0;
+						}
+						// Generate envelope
+						final int stage = ins[ENV_STAGE] >> FP;
+						ins[ENV_STAGE] += ins[ENV_RATE + stage];
+
+						// Generate oscillators
+						ins[OSC1_PHASE] += ins[OSC1_RATE];
+
+						int value = wave[ins[OSC_TYPE]][ins[OSC1_PHASE] & WAVE_BUFFER_MASK];
+
+						// int value = (ins[OSC1_PHASE] & WAVE_BUFFER_MASK) - FP_S1;
+						// int value = ((ins[OSC1_PHASE] & WAVE_BUFFER_MASK) - FP_S1)
+						// - (((((ins[OSC1_PHASE] + ins[PULSE_WIDTH]) & WAVE_BUFFER_MASK) - FP_S1) * ins[PULSE_MIX]) >> 8);
+
+						final int env = ins[ENV_LEVEL + stage]
+								+ (int) ((long) (ins[ENV_LEVEL + stage + 1] - ins[ENV_LEVEL + stage]) * (ins[ENV_STAGE] & FP_U1) >> FP);
+						value = (((value * env) >> (FP + 2)) * ins[VOLUME]) >> 6;
+
+						// add delay
+						value += (int) ((long) (delay[track][(ins[DELAY_POS] - ((ins[DELAY] * SAMPLES_PER_TICK >> 1))) & DELAY_MASK]) * ins[DELAY_MIX]) / 128;
+						delay[track][ins[DELAY_POS]] = (int) ((long) (value * ins[DELAY_FEEDBACK]) / 128);
+
+						out[offset + 0] += (byte) value;
+						out[offset + 1] += (byte) (value >> 8);
+
+						ins[DELAY_POS] = (ins[DELAY_POS] + 1) & DELAY_MASK;
+					}
+				}
+				index = 0;
+				while (index < BUFFER_SIZE) {
+					index += line.write(out, index, BUFFER_SIZE - index);
+				}
+				sequence = (sequence + 1) & SEQ_WRAP;
+			}
+		} catch (final Exception e) {
+			e.printStackTrace();
+		}
+	}
+}

src/tracker/Tracker.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker;
+
+import java.awt.GridLayout;
+import java.awt.KeyEventDispatcher;
+import java.awt.KeyboardFocusManager;
+import java.awt.event.KeyEvent;
+
+import javax.swing.JFrame;
+
+import tracker.config.TrackerConfig;
+import tracker.core.Cursor;
+import tracker.core.InstrumentController;
+import tracker.core.PatternController;
+import tracker.core.SequenceController;
+import tracker.core.TrackerController;
+import tracker.core.model.Song;
+import tracker.core.ui.InstrumentPanel;
+import tracker.core.ui.PatternPanel;
+import tracker.core.ui.SequencePanel;
+
+public class Tracker {
+	public static void main(final String[] args) {
+		new Tracker(new TinySynthConfig());
+	}
+
+	private final Cursor cursor;
+	private final Song song;
+
+	private final JFrame frame;
+	private final SequencePanel sequencePanel;
+	private final PatternPanel patternPanel;
+	private final InstrumentPanel instrumentPanel;
+
+	public Tracker(final TrackerConfig config) {
+		this.cursor = new Cursor(config);
+		this.song = new Song(config);
+
+		sequencePanel = new SequencePanel(config, cursor);
+		patternPanel = new PatternPanel(config, cursor);
+		instrumentPanel = new InstrumentPanel(config, cursor);
+
+		final SequenceController sequenceController = new SequenceController(sequencePanel, cursor);
+		final PatternController patternController = new PatternController(patternPanel, cursor, config);
+		final InstrumentController instrumentController = new InstrumentController(instrumentPanel, config, cursor);
+		final TrackerController controller = new TrackerController(config, cursor, sequenceController, patternController,
+				instrumentController);
+
+		controller.use(song);
+
+		frame = new JFrame(config.getName());
+		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+		frame.setSize(800, 600);
+		frame.getContentPane().setLayout(new GridLayout(1, 3));
+		frame.getContentPane().add(sequencePanel);
+		frame.getContentPane().add(patternPanel);
+		frame.getContentPane().add(instrumentPanel);
+		frame.setVisible(true);
+
+		final KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+		manager.addKeyEventDispatcher(new KeyEventDispatcher() {
+			public final boolean dispatchKeyEvent(final KeyEvent e) {
+				if (e.getID() == KeyEvent.KEY_PRESSED) {
+					controller.keyPressed(e);
+					controller.redraw();
+				} else if (e.getID() == KeyEvent.KEY_RELEASED) {
+					controller.keyReleased(e);
+					controller.redraw();
+				} else if (e.getID() == KeyEvent.KEY_TYPED) {
+					controller.keyTyped(e);
+					controller.redraw();
+				}
+				return false;
+			}
+		});
+	}
+}

src/tracker/Unicode.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker;
+
+public class Unicode {
+	public static class Writer {
+		private final StringBuilder sb = new StringBuilder();
+		private Integer hi = null;
+
+		public Writer write8(final int value) {
+			if (hi == null) {
+				hi = value & 0xff;
+			} else {
+				printValue(sb, (hi << 8) | (value & 0xff));
+				hi = null;
+			}
+			return this;
+		}
+
+		public Writer write16(final int value) {
+			write8((value >> 8) & 0xff);
+			write8((value >> 0) & 0xff);
+			return this;
+		}
+
+		public String asString() {
+			if (hi != null) {
+				printValue(sb, (hi << 8));
+			}
+			return sb.toString();
+		}
+
+		@Override
+		public String toString() {
+			return sb.toString();
+		}
+	}
+
+	public static class Reader {
+		private final String source;
+		private int value;
+		private int index = 0;
+		private int shift = -8;
+
+		public Reader(final String source) {
+			this.source = source;
+		}
+
+		public int read8() {
+			if (shift < 0) {
+				value = source.charAt(index++);
+				shift = 8;
+			}
+			final int retval = (value >> shift) & 0xff;
+			shift -= 8;
+			return retval;
+		}
+	}
+
+	private static void printValue(final StringBuilder sb, final int value) {
+		switch (value) {
+		case 0x0008:
+			sb.append("\\b");
+			break;
+		case 0x0009:
+			sb.append("\\t");
+			break;
+		case 0x000a:
+			sb.append("\\n");
+			break;
+		case 0x000c:
+			sb.append("\\f");
+			break;
+		case 0x000d:
+			sb.append("\\r");
+			break;
+		case 0x0022:
+			sb.append("\\\"");
+			break;
+		case 0x0027:
+			sb.append("\\'");
+			break;
+		case 0x005c:
+			sb.append("\\\\");
+			break;
+		default: {
+			sb.append("\\u").append(String.format("%04x", value));
+			break;
+		}
+		}
+	}
+}

src/tracker/config/IntegerProperty.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.config;
+
+import tracker.core.Value;
+
+public class IntegerProperty implements Property {
+	private final int max;
+	private final int min;
+	private final String name;
+	private int value;
+	private final int bit;
+
+	public IntegerProperty(final int bit, final String name, final int min, final int max, final int def) {
+		this.bit = bit;
+		this.name = name;
+		this.min = min;
+		this.max = max;
+		this.value = def;
+	}
+
+	public int bit() {
+		return bit;
+	}
+
+	@Override
+	public String name() {
+		return name;
+	}
+
+	@Override
+	public int count() {
+		return max - min;
+	}
+
+	public Value value(final int n) {
+		this.value = n;
+		final int v = compute(n);
+		return new Value(v, String.valueOf(v));
+	}
+
+	@Override
+	public Value value() {
+		final int v = compute(value);
+		return new Value(v, String.valueOf(v));
+	}
+
+	private int compute(final int n) {
+		return min + n;
+	}
+}

src/tracker/config/Property.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.config;
+
+import tracker.core.Value;
+
+public interface Property {
+	abstract String name();
+
+	abstract int count();
+
+	abstract Value value(int n);
+
+	abstract Value value();
+
+	abstract int bit();
+}

src/tracker/config/StringProperty.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.config;
+
+import tracker.core.Value;
+
+public class StringProperty implements Property {
+	private final String name;
+	private final String[] values;
+	private int value;
+	private final int bit;
+
+	public StringProperty(final int bit, final String name, final String... values) {
+		this.bit = bit;
+		this.name = name;
+		this.values = values;
+		value = 0;
+	}
+
+	public int bit() {
+		return bit;
+	}
+
+	@Override
+	public String name() {
+		return name;
+	}
+
+	@Override
+	public int count() {
+		return values.length;
+	}
+
+	@Override
+	public Value value(final int n) {
+		this.value = n;
+		return new Value(n, values[n]);
+	}
+
+	@Override
+	public Value value() {
+		return new Value(value, values[value]);
+	}
+}

src/tracker/config/TrackerCallbacks.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.config;
+
+import java.io.File;
+
+import tracker.core.Cursor;
+import tracker.core.model.Song;
+
+public interface TrackerCallbacks {
+	abstract void start(Cursor cursor, Song song);
+
+	abstract void stop();
+
+	abstract void updated(Song song);
+
+	abstract Song load(File file) throws Exception;
+
+	abstract void save(File file, Song song) throws Exception;
+}

src/tracker/config/TrackerConfig.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TrackerConfig {
+	private final String name;
+	private final int patternLength;
+	private final int sequences;
+	private final int patterns;
+	private final int tracks;
+
+	private final List<Property> instrumentList = new ArrayList<Property>();
+	private TrackerCallbacks callbacks;
+
+	public TrackerConfig(final String name, final int sequences, final int patterns, final int tracks, final int patternLength) {
+		this.name = name;
+		this.sequences = sequences;
+		this.patterns = patterns;
+		this.tracks = tracks;
+		this.patternLength = patternLength;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	protected void addProperty(final Property p) {
+		instrumentList.add(p);
+	}
+
+	protected void setCallbacks(final TrackerCallbacks callbacks) {
+		this.callbacks = callbacks;
+	}
+
+	public int getPatternLength() {
+		return patternLength;
+	}
+
+	public int getSequences() {
+		return sequences;
+	}
+
+	public int getPatterns() {
+		return patterns;
+	}
+
+	public int getTracks() {
+		return tracks;
+	}
+
+	public TrackerCallbacks getCallbacks() {
+		return callbacks;
+	}
+
+	public List<Property> getInstrument() {
+		return instrumentList;
+	}
+}

src/tracker/core/Cursor.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core;
+
+import tracker.config.TrackerConfig;
+
+public class Cursor {
+	private final TrackerConfig config;
+	private int row;
+	private int sequence;
+	private int setting;
+	private int track;
+
+	public Cursor(final TrackerConfig config) {
+		this.config = config;
+		track = 0;
+		row = 0;
+		sequence = 0;
+		setting = 0;
+	}
+
+	public void changeRow(final int n) {
+		setRow(row + n);
+	}
+
+	public void changeSequence(final int n) {
+		setSequence(sequence + n);
+	}
+
+	public void changeTrack(final int n) {
+		setTrack(track + n);
+	}
+
+	public void changeSetting(final int n) {
+		setSetting(setting + n);
+	}
+
+	public boolean currentTrack(final int track) {
+		return this.track == track;
+	}
+
+	public int getRow() {
+		return row;
+	}
+
+	public int getSequence() {
+		return sequence;
+	}
+
+	public int getTrack() {
+		return track;
+	}
+
+	public int getSetting() {
+		return setting;
+	}
+
+	public void setRow(final int row) {
+		this.row = row % config.getPatternLength();
+		if (this.row < 0) {
+			this.row = config.getPatternLength() - 1;
+		}
+	}
+
+	public void setSequence(final int sequence) {
+		this.sequence = sequence % config.getSequences();
+		if (this.sequence < 0) {
+			this.sequence = config.getSequences() - 1;
+		}
+	}
+
+	public void setTrack(final int track) {
+		this.track = track % config.getTracks();
+		if (this.track < 0) {
+			this.track = config.getTracks() - 1;
+		}
+	}
+
+	public void setSetting(final int setting) {
+		final int size = config.getInstrument().size();
+		this.setting = setting % size;
+		if (this.setting < 0) {
+			this.setting = size - 1;
+		}
+	}
+
+	@Override
+	public String toString() {
+		return track + ":" + row;
+	}
+}

src/tracker/core/InstrumentController.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core;
+
+import java.awt.event.KeyEvent;
+import java.util.HashMap;
+import java.util.Map;
+
+import tracker.config.TrackerConfig;
+import tracker.core.model.Song;
+import tracker.core.ui.InstrumentPanel;
+
+public class InstrumentController implements ViewController {
+	private final InstrumentPanel panel;
+	private final TrackerConfig config;
+	private final Cursor cursor;
+
+	private Song song;
+
+	public InstrumentController(final InstrumentPanel panel, final TrackerConfig config, final Cursor cursor) {
+		this.panel = panel;
+		this.config = config;
+		this.cursor = cursor;
+	}
+
+	@Override
+	public void setSelected(final boolean selected) {
+		panel.setSelected(selected);
+	}
+
+	public void onInput(final KeyEvent event) {
+		if (handleCursor(event)) {
+			panel.repaint();
+			return;
+		}
+	}
+
+	private void change(final Cursor cursor, final int n) {
+		final int setting = cursor.getSetting();
+		final int count = config.getInstrument().get(setting).count();
+
+		final int[] instruments = song.getInstruments(cursor);
+		instruments[setting] += n;
+
+		if (instruments[setting] < 0) {
+			instruments[setting] = 0;
+		}
+		if (instruments[setting] >= count) {
+			instruments[setting] = count - 1;
+		}
+	}
+
+	private boolean handleCursor(final KeyEvent event) {
+		if (event.getID() != KeyEvent.KEY_RELEASED) {
+			return false;
+		}
+
+		final int step = event.isShiftDown() ? 4 : 1;
+		if (event.isControlDown()) {
+			final int setting = cursor.getSetting();
+			switch (event.getKeyCode()) {
+			case KeyEvent.VK_UP:
+				change(cursor, step);
+				return true;
+			case KeyEvent.VK_DOWN:
+				change(cursor, -step);
+				return true;
+			case KeyEvent.VK_LEFT:
+				song.getInstruments(cursor)[setting] = 0;
+				return true;
+			case KeyEvent.VK_RIGHT:
+				final int count = config.getInstrument().get(setting).count();
+				song.getInstruments(cursor)[setting] = count - 1;
+				return true;
+			}
+		}
+
+		switch (event.getKeyCode()) {
+		case KeyEvent.VK_UP:
+			cursor.changeSetting(-step);
+			return true;
+		case KeyEvent.VK_DOWN:
+			cursor.changeSetting(step);
+			return true;
+		case KeyEvent.VK_LEFT:
+			cursor.changeTrack(-1);
+			return true;
+		case KeyEvent.VK_RIGHT:
+			cursor.changeTrack(1);
+			return true;
+		}
+		return false;
+	}
+
+	public void use(final Song song) {
+		this.song = song;
+		panel.use(song);
+	}
+
+	@Override
+	public void redraw() {
+		panel.repaint();
+	}
+
+	private static final Map<Integer, Integer> KEYS = new HashMap<Integer, Integer>();
+	static {
+		KEYS.put(KeyEvent.VK_Z, 0);
+		KEYS.put(KeyEvent.VK_S, 1);
+		KEYS.put(KeyEvent.VK_X, 2);
+		KEYS.put(KeyEvent.VK_D, 3);
+		KEYS.put(KeyEvent.VK_C, 4);
+		KEYS.put(KeyEvent.VK_V, 5);
+		KEYS.put(KeyEvent.VK_G, 6);
+		KEYS.put(KeyEvent.VK_B, 7);
+		KEYS.put(KeyEvent.VK_H, 8);
+		KEYS.put(KeyEvent.VK_N, 9);
+		KEYS.put(KeyEvent.VK_J, 10);
+		KEYS.put(KeyEvent.VK_M, 11);
+		KEYS.put(KeyEvent.VK_Q, 12);
+		KEYS.put(KeyEvent.VK_2, 13);
+		KEYS.put(KeyEvent.VK_W, 14);
+		KEYS.put(KeyEvent.VK_3, 15);
+		KEYS.put(KeyEvent.VK_E, 16);
+		KEYS.put(KeyEvent.VK_R, 17);
+		KEYS.put(KeyEvent.VK_5, 18);
+		KEYS.put(KeyEvent.VK_T, 19);
+		KEYS.put(KeyEvent.VK_6, 20);
+		KEYS.put(KeyEvent.VK_Y, 21);
+		KEYS.put(KeyEvent.VK_7, 22);
+		KEYS.put(KeyEvent.VK_U, 23);
+	}
+
+}

src/tracker/core/PatternController.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core;
+
+import java.awt.event.KeyEvent;
+import java.util.HashMap;
+import java.util.Map;
+
+import tracker.config.TrackerConfig;
+import tracker.core.model.Song;
+import tracker.core.ui.PatternPanel;
+
+public class PatternController implements ViewController {
+	private final PatternPanel panel;
+	private final Cursor cursor;
+	private Song song;
+	private int octave = 3;
+
+	public PatternController(final PatternPanel panel, final Cursor cursor, final TrackerConfig config) {
+		this.panel = panel;
+		this.cursor = cursor;
+	}
+
+	@Override
+	public void setSelected(final boolean selected) {
+		panel.setSelected(selected);
+	}
+
+	public void onInput(final KeyEvent event) {
+		if (handleCursor(event)) {
+			panel.repaint();
+			return;
+		}
+	}
+
+	private boolean handleCursor(final KeyEvent event) {
+		if (event.getID() != KeyEvent.KEY_RELEASED) {
+			return false;
+		}
+
+		if (KEYS.containsKey(event.getKeyCode())) {
+			song.setNote(cursor, KEYS.get(event.getKeyCode()) + 12 * octave);
+			cursor.changeRow(1);
+			return true;
+		}
+
+		final int step = event.isShiftDown() ? 4 : 1;
+		switch (event.getKeyCode()) {
+		case KeyEvent.VK_SPACE:
+			song.setNote(cursor, 1);
+			cursor.changeRow(step);
+			return true;
+		case KeyEvent.VK_BACK_SPACE:
+			song.setNote(cursor, 0);
+			cursor.changeRow(step);
+			return true;
+		case KeyEvent.VK_DELETE:
+			song.setNote(cursor, 0);
+			return true;
+		case KeyEvent.VK_UP:
+			cursor.changeRow(-step);
+			return true;
+		case KeyEvent.VK_DOWN:
+			cursor.changeRow(step);
+			return true;
+		case KeyEvent.VK_LEFT:
+			if (event.isControlDown()) {
+				octave = Math.max(0, octave - 1);
+			} else {
+				cursor.changeTrack(-1);
+			}
+			return true;
+		case KeyEvent.VK_RIGHT:
+			if (event.isControlDown()) {
+				octave = Math.min(15, octave + 1);
+			} else {
+				cursor.changeTrack(1);
+			}
+			return true;
+		}
+		return false;
+	}
+
+	public void use(final Song song) {
+		this.song = song;
+		panel.use(song);
+	}
+
+	@Override
+	public void redraw() {
+		panel.repaint();
+	}
+
+	private static final Map<Integer, Integer> KEYS = new HashMap<Integer, Integer>();
+	static {
+		KEYS.put(KeyEvent.VK_Z, 2);
+		KEYS.put(KeyEvent.VK_S, 3);
+		KEYS.put(KeyEvent.VK_X, 4);
+		KEYS.put(KeyEvent.VK_D, 5);
+		KEYS.put(KeyEvent.VK_C, 6);
+		KEYS.put(KeyEvent.VK_V, 7);
+		KEYS.put(KeyEvent.VK_G, 8);
+		KEYS.put(KeyEvent.VK_B, 9);
+		KEYS.put(KeyEvent.VK_H, 10);
+		KEYS.put(KeyEvent.VK_N, 11);
+		KEYS.put(KeyEvent.VK_J, 12);
+		KEYS.put(KeyEvent.VK_M, 13);
+		KEYS.put(KeyEvent.VK_Q, 14);
+		KEYS.put(KeyEvent.VK_2, 15);
+		KEYS.put(KeyEvent.VK_W, 16);
+		KEYS.put(KeyEvent.VK_3, 17);
+		KEYS.put(KeyEvent.VK_E, 18);
+		KEYS.put(KeyEvent.VK_R, 19);
+		KEYS.put(KeyEvent.VK_5, 20);
+		KEYS.put(KeyEvent.VK_T, 21);
+		KEYS.put(KeyEvent.VK_6, 22);
+		KEYS.put(KeyEvent.VK_Y, 23);
+		KEYS.put(KeyEvent.VK_7, 24);
+		KEYS.put(KeyEvent.VK_U, 25);
+	}
+
+}

src/tracker/core/SequenceController.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core;
+
+import java.awt.event.KeyEvent;
+
+import tracker.core.model.Song;
+import tracker.core.ui.SequencePanel;
+
+/**
+ * @author Erik Byström
+ */
+public class SequenceController implements ViewController {
+	private final SequencePanel panel;
+	private final Cursor cursor;
+	private Song song;
+
+	public SequenceController(final SequencePanel panel, final Cursor cursor) {
+		this.panel = panel;
+		this.cursor = cursor;
+	}
+
+	@Override
+	public void setSelected(final boolean selected) {
+		panel.setSelected(selected);
+	}
+
+	public void onInput(final KeyEvent event) {
+		if (handleCursor(event)) {
+			panel.repaint();
+			return;
+		}
+	}
+
+	private boolean handleCursor(final KeyEvent event) {
+		if (event.getID() != KeyEvent.KEY_RELEASED) {
+			return false;
+		}
+
+		if (!event.isAltDown() && !event.isControlDown()) {
+			switch (event.getKeyCode()) {
+			case KeyEvent.VK_UP:
+				cursor.changeSequence(-1);
+				return true;
+			case KeyEvent.VK_DOWN:
+				cursor.changeSequence(1);
+				return true;
+			case KeyEvent.VK_LEFT:
+				cursor.changeTrack(-1);
+				return true;
+			case KeyEvent.VK_RIGHT:
+				cursor.changeTrack(1);
+				return true;
+			}
+		}
+		if (event.isControlDown()) {
+			switch (event.getKeyCode()) {
+			case KeyEvent.VK_UP:
+				song.changePattern(cursor, 1);
+				return true;
+			case KeyEvent.VK_DOWN:
+				song.changePattern(cursor, -1);
+				return true;
+			}
+		}
+		return false;
+	}
+
+	@Override
+	public void use(final Song song) {
+		this.song = song;
+		panel.use(song);
+	}
+
+	@Override
+	public void redraw() {
+		panel.repaint();
+	}
+}

src/tracker/core/TrackerController.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core;
+
+import java.awt.Component;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.io.File;
+
+import javax.swing.JFileChooser;
+import javax.swing.filechooser.FileFilter;
+
+import tracker.config.TrackerCallbacks;
+import tracker.config.TrackerConfig;
+import tracker.core.model.Song;
+
+@SuppressWarnings("serial")
+public class TrackerController extends Component implements KeyListener {
+	private final TrackerConfig config;
+	private final Cursor cursor;
+	private final ViewController[] controllers;
+
+	private int activeView = 0;
+	private Song song;
+	private JFileChooser fileChooser;
+
+	private boolean blocked = false;
+
+	public TrackerController(final TrackerConfig config, final Cursor cursor, final ViewController... controllers) {
+		this.config = config;
+		this.cursor = cursor;
+		this.controllers = controllers;
+		setFocusable(true);
+
+		fileChooser = new JFileChooser(System.getProperty("user.dir"));
+		fileChooser.setDialogTitle("4traK");
+		fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+		fileChooser.setFileFilter(new FileFilter() {
+			@Override
+			public boolean accept(final File f) {
+				return f.getName().endsWith(".4ks");
+			}
+
+			@Override
+			public String getDescription() {
+				return "4traK song";
+			}
+		});
+	}
+
+	@Override
+	public void keyPressed(final KeyEvent event) {
+		if (blocked) {
+			return;
+		}
+		controllers[activeView].onInput(event);
+	}
+
+	@Override
+	public void keyReleased(final KeyEvent event) {
+		if (blocked) {
+			return;
+		}
+
+		final TrackerCallbacks callbacks = config.getCallbacks();
+		if (callbacks == null) {
+			System.out.println("No TrackerCallback configured");
+		} else {
+			if (event.getKeyCode() == KeyEvent.VK_F1) {
+				if (!event.isShiftDown()) {
+					cursor.setSequence(0);
+				}
+				callbacks.start(cursor, song);
+			} else if (event.getKeyCode() == KeyEvent.VK_F2) {
+				callbacks.stop();
+			} else if (event.getKeyCode() == KeyEvent.VK_F3) {
+				callbacks.start(cursor, song);
+			} else if (event.getKeyCode() == KeyEvent.VK_F5) {
+				System.out.println("Loading");
+				try {
+					blocked = true;
+					if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
+						try {
+							System.out.println("Loading file");
+							use(callbacks.load(fileChooser.getSelectedFile()));
+						} catch (final Exception e) {
+							e.printStackTrace();
+						}
+					}
+				} finally {
+					blocked = false;
+				}
+			} else if (event.getKeyCode() == KeyEvent.VK_F6) {
+				System.out.println("Saving");
+				try {
+					blocked = true;
+					if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
+						try {
+							callbacks.save(fileChooser.getSelectedFile(), song);
+						} catch (final Exception e) {
+							e.printStackTrace();
+						}
+					}
+				} finally {
+					blocked = false;
+				}
+			}
+		}
+
+		if (event.isAltDown()) {
+			switch (event.getKeyCode()) {
+			case KeyEvent.VK_LEFT:
+				song.changePattern(cursor, -1);
+				redraw();
+				return;
+			case KeyEvent.VK_RIGHT:
+				song.changePattern(cursor, 1);
+				redraw();
+				return;
+			case KeyEvent.VK_UP:
+				cursor.changeSequence(-1);
+				redraw();
+				return;
+			case KeyEvent.VK_DOWN:
+				cursor.changeSequence(1);
+				redraw();
+				return;
+			}
+		}
+
+		if (event.getKeyCode() == KeyEvent.VK_TAB) {
+			activeView += (event.isShiftDown() ? -1 : 1);
+			if (activeView < 0) {
+				activeView = controllers.length - 1;
+			}
+			if (activeView > controllers.length - 1) {
+				activeView = 0;
+			}
+			for (int i = 0; i < controllers.length; i++) {
+				controllers[i].setSelected(i == activeView);
+			}
+		} else {
+			controllers[activeView].onInput(event);
+		}
+	}
+
+	@Override
+	public void keyTyped(final KeyEvent event) {
+	}
+
+	public void use(final Song song) {
+		this.song = song;
+		config.getCallbacks().updated(song);
+		for (int i = 0; i < controllers.length; i++) {
+			controllers[i].use(song);
+		}
+	}
+
+	public void redraw() {
+		config.getCallbacks().updated(song);
+		for (int i = 0; i < controllers.length; i++) {
+			controllers[i].redraw();
+		}
+	}
+}

src/tracker/core/Value.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core;
+
+/**
+ * @author Erik Byström
+ * 
+ */
+public class Value {
+	private final int value;
+	private final String text;
+
+	public Value(final int value, final String text) {
+		this.value = value;
+		this.text = text;
+	}
+
+	public int value() {
+		return value;
+	}
+
+	public String text() {
+		return text;
+	}
+
+	@Override
+	public String toString() {
+		return text();
+	}
+}

src/tracker/core/ViewController.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core;
+
+import java.awt.event.KeyEvent;
+
+import tracker.core.model.Song;
+
+public interface ViewController {
+	abstract void onInput(KeyEvent event);
+
+	abstract void setSelected(boolean selected);
+
+	abstract void use(Song song);
+
+	abstract void redraw();
+}

src/tracker/core/model/Pattern.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core.model;
+
+import tracker.config.TrackerConfig;
+
+/**
+ * @author Erik Byström
+ * 
+ */
+public class Pattern {
+	private final int[] notes;
+
+	public Pattern(final TrackerConfig config) {
+		notes = new int[config.getPatternLength()];
+	}
+
+	public int[] getNotes() {
+		return notes;
+	}
+}

src/tracker/core/model/Song.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core.model;
+
+import tracker.config.TrackerConfig;
+import tracker.core.Cursor;
+import tracker.core.Value;
+
+public class Song {
+	private final TrackerConfig config;
+
+	private final Pattern[] patterns;
+	private final int[][] matrix;
+	private final int[][] instrument;
+
+	public Song(final TrackerConfig config) {
+		this.config = config;
+
+		instrument = new int[config.getTracks()][config.getInstrument().size()];
+		matrix = new int[config.getTracks()][config.getSequences()];
+		patterns = new Pattern[config.getPatterns()];
+		for (int i = 0; i < patterns.length; i++) {
+			patterns[i] = new Pattern(config);
+		}
+		for (int i = 0; i < instrument.length; i++) {
+			for (int j = 0; j < instrument[i].length; j++) {
+				final Value value = config.getInstrument().get(j).value();
+				instrument[i][j] = value.value();
+			}
+		}
+	}
+
+	public int[] getSequenceForTrack(final int n) {
+		return matrix[n];
+	}
+
+	public Pattern getPatternForTrack(final int track, final Cursor cursor) {
+		final int[] sequence = getSequenceForTrack(track);
+		return getPattern(sequence[cursor.getSequence()]);
+	}
+
+	public Pattern getPattern(final int pattern) {
+		return patterns[pattern % patterns.length];
+	}
+
+	public void changePattern(final Cursor cursor, final int n) {
+		final int[] sequence = getSequenceForTrack(cursor.getTrack());
+		sequence[cursor.getSequence()] += n;
+		if (sequence[cursor.getSequence()] < 0) {
+			sequence[cursor.getSequence()] = config.getSequences() - 1;
+		}
+		if (sequence[cursor.getSequence()] >= config.getSequences()) {
+			sequence[cursor.getSequence()] = 0;
+		}
+
+	}
+
+	public void setNote(final Cursor cursor, final int note) {
+		final Pattern pattern = getPatternForTrack(cursor.getTrack(), cursor);
+		pattern.getNotes()[cursor.getRow()] = note;
+	}
+
+	public int getCursorPattern(final Cursor cursor) {
+		return getSequenceForTrack(cursor.getTrack())[cursor.getSequence()];
+	}
+
+	public int[][] getInstruments() {
+		return instrument;
+	}
+
+	public int[] getInstruments(final Cursor cursor) {
+		return instrument[cursor.getTrack()];
+	}
+}

src/tracker/core/ui/ColorSet.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core.ui;
+
+import java.awt.Color;
+
+public class ColorSet {
+	private final Color textColor;
+	private final Color descColor;
+	private final Color cursorBackground;
+	private final Color cursorTrackSelect;
+	private final Color descColorHighlight;
+	private final Color borderColor;
+
+	public ColorSet(final Color textColor, final Color descColor, final Color descColorHighlight, final Color cursorBackground,
+			final Color cursorTrackSelect, final Color borderColor) {
+		this.textColor = textColor;
+		this.descColor = descColor;
+		this.descColorHighlight = descColorHighlight;
+		this.cursorBackground = cursorBackground;
+		this.cursorTrackSelect = cursorTrackSelect;
+		this.borderColor = borderColor;
+	}
+
+	public Color getTextColor() {
+		return textColor;
+	}
+
+	public Color getDescColor() {
+		return descColor;
+	}
+
+	protected Color getDescColorHighlight() {
+		return descColorHighlight;
+	}
+
+	public Color getCursorBackground() {
+		return cursorBackground;
+	}
+
+	public Color getCursorTrackSelect() {
+		return cursorTrackSelect;
+	}
+
+	public Color getBorderColor() {
+		return borderColor;
+	}
+}

src/tracker/core/ui/InstrumentPanel.java

+/*******************************************************************************
+ * Copyright (c) 2012 Erik Byström.
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ ******************************************************************************/
+package tracker.core.ui;
+
+import java.awt.Dimension;
+import java.awt.Graphics;
+
+import javax.swing.JPanel;
+
+import tracker.config.Property;
+import tracker.config.TrackerConfig;
+import tracker.core.Cursor;
+import tracker.core.model.Song;
+
+@SuppressWarnings("serial")
+public class InstrumentPanel extends JPanel {
+	private final Cursor cursor;
+	private final Sizer sizer;
+	private final TrackerConfig config;
+
+	private ColorSet color;
+	private Song song;
+
+	public InstrumentPanel(final TrackerConfig config, final Cursor cursor) {
+		this.config = config;
+		this.color = Ui.getColors(false);
+		this.cursor = cursor;
+
+		this.sizer = new Sizer(10 * Ui.BORDER, 125, Ui.ROW_HEIGHT);
+
+		final int width = sizer.width(1);
+		final int height = sizer.height(config.getInstrument().size());
+		final Dimension dimension = new Dimension(width, height);
+		setPreferredSize(dimension);
+	}