cache: add track1 definition and loader

I am able to play the resulting midi file with javax.sound.midi.Sequencer
This commit is contained in:
Adam
2017-04-03 12:26:30 -04:00
parent b71ada8ff8
commit cafa2b3189
5 changed files with 688 additions and 3 deletions

View File

@@ -0,0 +1,30 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.cache.definitions;
public class Track1Definition
{
public byte[] midi; // midi file contents
}

View File

@@ -0,0 +1,461 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.cache.definitions.loaders;
import net.runelite.cache.definitions.Track1Definition;
import net.runelite.cache.io.InputStream;
import net.runelite.cache.io.OutputStream;
public class Track1Loader
{
public Track1Definition load(byte[] b)
{
Track1Definition def = new Track1Definition();
load(def, new InputStream(b));
return def;
}
private void load(Track1Definition def, InputStream var1)
{
// Some of the names are from https://www.rune-server.ee/runescape-development/rs-503-client-server/snippets/311669-rs-music-file-structure-conversion.html
var1.setOffset(var1.getLength() - 3);
int tracks = var1.readUnsignedByte();
int division = var1.readUnsignedShort();
int offset = 14 + tracks * 10;
var1.setOffset(0);
int tempoOpcodes = 0;
int ctrlChangeOpcodes = 0;
int noteOnOpcodes = 0;
int noteOffOpcodes = 0;
int wheelChangeOpcodes = 0;
int chnnlAfterTchOpcodes = 0;
int keyAfterTchOpcodes = 0;
int progmChangeOpcodes = 0;
int var13;
int opcode;
int var15;
for (var13 = 0; var13 < tracks; ++var13)
{
opcode = -1;
while (true)
{
var15 = var1.readUnsignedByte();
if (var15 != opcode)
{
++offset;
}
opcode = var15 & 15;
if (var15 == 7)
{
break;
}
if (var15 == 23)
{
++tempoOpcodes;
}
else if (opcode == 0)
{
++noteOnOpcodes;
}
else if (opcode == 1)
{
++noteOffOpcodes;
}
else if (opcode == 2)
{
++ctrlChangeOpcodes;
}
else if (opcode == 3)
{
++wheelChangeOpcodes;
}
else if (opcode == 4)
{
++chnnlAfterTchOpcodes;
}
else if (opcode == 5)
{
++keyAfterTchOpcodes;
}
else
{
if (opcode != 6)
{
throw new RuntimeException();
}
++progmChangeOpcodes;
}
}
}
offset += 5 * tempoOpcodes;
offset += 2 * (noteOnOpcodes + noteOffOpcodes + ctrlChangeOpcodes + wheelChangeOpcodes + keyAfterTchOpcodes);
offset += chnnlAfterTchOpcodes + progmChangeOpcodes;
var13 = var1.getOffset();
opcode = tracks + tempoOpcodes + ctrlChangeOpcodes + noteOnOpcodes + noteOffOpcodes + wheelChangeOpcodes + chnnlAfterTchOpcodes + keyAfterTchOpcodes + progmChangeOpcodes;
for (var15 = 0; var15 < opcode; ++var15)
{
var1.readVarInt();
}
offset += var1.getOffset() - var13;
var15 = var1.getOffset();
int var16 = 0;
int var17 = 0;
int var18 = 0;
int var19 = 0;
int var20 = 0;
int var21 = 0;
int var22 = 0;
int var23 = 0;
int var24 = 0;
int var25 = 0;
int var26 = 0;
int var27 = 0;
int var28 = 0;
int var29;
for (var29 = 0; var29 < ctrlChangeOpcodes; ++var29)
{
var28 = var28 + var1.readUnsignedByte() & 127;
if (var28 != 0 && var28 != 32)
{
if (var28 == 1)
{
++var16;
}
else if (var28 == 33)
{
++var17;
}
else if (var28 == 7)
{
++var18;
}
else if (var28 == 39)
{
++var19;
}
else if (var28 == 10)
{
++var20;
}
else if (var28 == 42)
{
++var21;
}
else if (var28 == 99)
{
++var22;
}
else if (var28 == 98)
{
++var23;
}
else if (var28 == 101)
{
++var24;
}
else if (var28 == 100)
{
++var25;
}
else if (var28 != 64 && var28 != 65 && var28 != 120 && var28 != 121 && var28 != 123)
{
++var27;
}
else
{
++var26;
}
}
else
{
++progmChangeOpcodes;
}
}
var29 = 0;
int var30 = var1.getOffset();
var1.skip(var26);
int var31 = var1.getOffset();
var1.skip(keyAfterTchOpcodes);
int var32 = var1.getOffset();
var1.skip(chnnlAfterTchOpcodes);
int var33 = var1.getOffset();
var1.skip(wheelChangeOpcodes);
int var34 = var1.getOffset();
var1.skip(var16);
int var35 = var1.getOffset();
var1.skip(var18);
int var36 = var1.getOffset();
var1.skip(var20);
int var37 = var1.getOffset();
var1.skip(noteOnOpcodes + noteOffOpcodes + keyAfterTchOpcodes);
int var38 = var1.getOffset();
var1.skip(noteOnOpcodes);
int var39 = var1.getOffset();
var1.skip(var27);
int var40 = var1.getOffset();
var1.skip(noteOffOpcodes);
int var41 = var1.getOffset();
var1.skip(var17);
int var42 = var1.getOffset();
var1.skip(var19);
int var43 = var1.getOffset();
var1.skip(var21);
int var44 = var1.getOffset();
var1.skip(progmChangeOpcodes);
int var45 = var1.getOffset();
var1.skip(wheelChangeOpcodes);
int var46 = var1.getOffset();
var1.skip(var22);
int var47 = var1.getOffset();
var1.skip(var23);
int var48 = var1.getOffset();
var1.skip(var24);
int var49 = var1.getOffset();
var1.skip(var25);
int var50 = var1.getOffset();
var1.skip(tempoOpcodes * 3);
def.midi = new byte[offset];
OutputStream var51 = new OutputStream(def.midi);
var51.writeInt(1297377380); // MThd header
var51.writeInt(6); // length of header
var51.writeShort(tracks > 1 ? 1 : 0); // format
var51.writeShort(tracks); // tracks
var51.writeShort(division); // division
var1.setOffset(var13);
int var52 = 0;
int var53 = 0;
int var54 = 0;
int var55 = 0;
int var56 = 0;
int var57 = 0;
int var58 = 0;
int[] var59 = new int[128];
var28 = 0;
label361:
for (int var60 = 0; var60 < tracks; ++var60)
{
var51.writeInt(1297379947); // MTrk
var51.skip(4); // length gets written here later
int var61 = var51.getOffset();
int var62 = -1;
while (true)
{
while (true)
{
int var63 = var1.readVarInt();
var51.writeVarInt(var63); // delta time
int var64 = var1.getArray()[var29++] & 255;
boolean var65 = var64 != var62;
var62 = var64 & 15;
if (var64 == 7)
{
//if (var65) -- client has this if, but it causes broken midi to be produced
{
var51.writeByte(255);
}
var51.writeByte(47); // type - end of track
var51.writeByte(0); // length
var51.writeLengthFromMark(var51.getOffset() - var61);
continue label361;
}
if (var64 == 23)
{
//if (var65) -- client has this if, but it causes broken midi to be produced
{
var51.writeByte(255); // meta event FF
}
var51.writeByte(81); // type - set tempo
var51.writeByte(3); // length
var51.writeByte(var1.getArray()[var50++]);
var51.writeByte(var1.getArray()[var50++]);
var51.writeByte(var1.getArray()[var50++]);
}
else
{
var52 ^= var64 >> 4;
if (var62 == 0)
{
if (var65)
{
var51.writeByte(144 + var52);
}
var53 += var1.getArray()[var37++];
var54 += var1.getArray()[var38++];
var51.writeByte(var53 & 127);
var51.writeByte(var54 & 127);
}
else if (var62 == 1)
{
if (var65)
{
var51.writeByte(128 + var52);
}
var53 += var1.getArray()[var37++];
var55 += var1.getArray()[var40++];
var51.writeByte(var53 & 127);
var51.writeByte(var55 & 127);
}
else if (var62 == 2)
{
if (var65)
{
var51.writeByte(176 + var52);
}
var28 = var28 + var1.getArray()[var15++] & 127;
var51.writeByte(var28);
byte var66;
if (var28 != 0 && var28 != 32)
{
if (var28 == 1)
{
var66 = var1.getArray()[var34++];
}
else if (var28 == 33)
{
var66 = var1.getArray()[var41++];
}
else if (var28 == 7)
{
var66 = var1.getArray()[var35++];
}
else if (var28 == 39)
{
var66 = var1.getArray()[var42++];
}
else if (var28 == 10)
{
var66 = var1.getArray()[var36++];
}
else if (var28 == 42)
{
var66 = var1.getArray()[var43++];
}
else if (var28 == 99)
{
var66 = var1.getArray()[var46++];
}
else if (var28 == 98)
{
var66 = var1.getArray()[var47++];
}
else if (var28 == 101)
{
var66 = var1.getArray()[var48++];
}
else if (var28 == 100)
{
var66 = var1.getArray()[var49++];
}
else if (var28 != 64 && var28 != 65 && var28 != 120 && var28 != 121 && var28 != 123)
{
var66 = var1.getArray()[var39++];
}
else
{
var66 = var1.getArray()[var30++];
}
}
else
{
var66 = var1.getArray()[var44++];
}
int var67 = var66 + var59[var28];
var59[var28] = var67;
var51.writeByte(var67 & 127);
}
else if (var62 == 3)
{
if (var65)
{
var51.writeByte(224 + var52);
}
var56 += var1.getArray()[var45++];
var56 += var1.getArray()[var33++] << 7;
var51.writeByte(var56 & 127);
var51.writeByte(var56 >> 7 & 127);
}
else if (var62 == 4)
{
if (var65)
{
var51.writeByte(208 + var52);
}
var57 += var1.getArray()[var32++];
var51.writeByte(var57 & 127);
}
else if (var62 == 5)
{
if (var65)
{
var51.writeByte(160 + var52);
}
var53 += var1.getArray()[var37++];
var58 += var1.getArray()[var31++];
var51.writeByte(var53 & 127);
var51.writeByte(var58 & 127);
}
else
{
if (var62 != 6)
{
throw new RuntimeException();
}
if (var65)
{
var51.writeByte(192 + var52);
}
var51.writeByte(var1.getArray()[var44++]);
}
}
}
}
}
}
}

View File

@@ -46,6 +46,12 @@ public class InputStream extends java.io.InputStream
this.buffer = ByteBuffer.wrap(buffer);
}
public byte[] getArray()
{
assert buffer.hasArray();
return buffer.array();
}
@Override
public String toString()
{
@@ -150,7 +156,9 @@ public class InputStream extends java.io.InputStream
int ch = this.readByte();
if (ch == 0)
{
break;
}
if (ch >= 128 && ch < 160)
{
@@ -168,7 +176,6 @@ public class InputStream extends java.io.InputStream
return sb.toString();
}
public String readStringOrNull()
{
if (this.peek() != 0)
@@ -182,6 +189,19 @@ public class InputStream extends java.io.InputStream
}
}
public int readVarInt()
{
byte var1 = this.readByte();
int var2;
for (var2 = 0; var1 < 0; var1 = this.readByte())
{
var2 = (var2 | var1 & 127) << 7;
}
return var2 | var1;
}
public byte[] getRemaining()
{
byte[] b = new byte[buffer.remaining()];

View File

@@ -30,7 +30,7 @@ import java.nio.ByteBuffer;
public final class OutputStream extends java.io.OutputStream
{
private ByteBuffer buffer;
public OutputStream(int capacity)
{
buffer = ByteBuffer.allocate(capacity);
@@ -41,6 +41,17 @@ public final class OutputStream extends java.io.OutputStream
this(16);
}
public OutputStream(byte[] b)
{
buffer = ByteBuffer.wrap(b);
}
public byte[] getArray()
{
assert buffer.hasArray();
return buffer.array();
}
private void ensureRemaining(int remaining)
{
while (remaining > buffer.remaining())
@@ -63,6 +74,11 @@ public final class OutputStream extends java.io.OutputStream
buffer.position(pos);
}
public int getOffset()
{
return buffer.position();
}
public void setOffset(int offset)
{
buffer.position(offset);
@@ -78,7 +94,7 @@ public final class OutputStream extends java.io.OutputStream
ensureRemaining(length);
buffer.put(b, offset, length);
}
public void writeByte(int i)
{
ensureRemaining(1);
@@ -112,6 +128,39 @@ public final class OutputStream extends java.io.OutputStream
buffer.putInt(i);
}
public void writeVarInt(int var1)
{
if ((var1 & -128) != 0)
{
if ((var1 & -16384) != 0)
{
if ((var1 & -2097152) != 0)
{
if ((var1 & -268435456) != 0)
{
this.writeByte(var1 >>> 28 | 128);
}
this.writeByte(var1 >>> 21 | 128);
}
this.writeByte(var1 >>> 14 | 128);
}
this.writeByte(var1 >>> 7 | 128);
}
this.writeByte(var1 & 127);
}
public void writeLengthFromMark(int var1)
{
this.getArray()[this.getOffset() - var1 - 4] = (byte) (var1 >> 24);
this.getArray()[this.getOffset() - var1 - 3] = (byte) (var1 >> 16);
this.getArray()[this.getOffset() - var1 - 2] = (byte) (var1 >> 8);
this.getArray()[this.getOffset() - var1 - 1] = (byte) var1;
}
public byte[] flip()
{
buffer.flip();

View File

@@ -0,0 +1,125 @@
/*
* Copyright (c) 2017, Adam <Adam@sigterm.info>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.cache;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequencer;
import net.runelite.cache.definitions.Track1Definition;
import net.runelite.cache.definitions.loaders.Track1Loader;
import net.runelite.cache.fs.Archive;
import net.runelite.cache.fs.Index;
import net.runelite.cache.fs.Store;
import net.runelite.cache.util.Djb2Manager;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Track1DumperTest
{
private static final Logger logger = LoggerFactory.getLogger(Track1DumperTest.class);
@Rule
public TemporaryFolder folder = StoreLocation.getTemporaryFolder();
@Test
public void test() throws IOException
{
File dumpDir = folder.newFolder();
int count = 0;
Djb2Manager djb2 = new Djb2Manager();
djb2.load();
try (Store store = new Store(StoreLocation.LOCATION))
{
store.load();
Index index = store.getIndex(IndexType.TRACK1);
for (Archive archive : index.getArchives())
{
assert archive.getFiles().size() == 1;
net.runelite.cache.fs.File file = archive.getFiles().get(0);
Track1Loader loader = new Track1Loader();
Track1Definition def = loader.load(file.getContents());
String name = djb2.getName(archive.getNameHash());
if (name == null)
{
name = "" + archive.getNameHash();
}
Files.write(def.midi, new File(dumpDir, name + ".midi"));
++count;
}
}
logger.info("Dumped {} sound tracks to {}", count, dumpDir);
}
@Test
@Ignore
public void play() throws Exception
{
// Obtains the default Sequencer connected to a default device.
Sequencer sequencer = MidiSystem.getSequencer();
// Opens the device, indicating that it should now acquire any
// system resources it requires and become operational.
sequencer.open();
try
{
// create a stream from a file
java.io.InputStream is = new FileInputStream(new File("C:\\rs\\cache\\track1\\scape main.midi"));
// Sets the current sequence on which the sequencer operates.
// The stream must point to MIDI file data.
sequencer.setSequence(is);
// Starts playback of the MIDI data in the currently loaded sequence.
sequencer.start();
while (sequencer.isRunning())
{
Thread.sleep(1000L);
}
}
finally
{
sequencer.close();
}
}
}