/* * Copyright (C) 2014 James Lawrence. * * This file is part of LibLab. * * LibLab 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 com.sqrt.liblab.codec; import com.sqrt.liblab.entry.audio.Audio; import com.sqrt.liblab.entry.audio.AudioInputStream; import com.sqrt.liblab.entry.audio.Jump; import com.sqrt.liblab.entry.audio.Region; import com.sqrt.liblab.io.DataSource; import java.io.IOException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; public class AudioCodec extends EntryCodec<Audio> { protected Audio _read(final DataSource source) throws IOException { String ext = source.getName().toLowerCase().substring(source.getName().length() - 3); Audio audio = new Audio(source.container, source.getName()); if (ext.equals("wav") || ext.equals("imc")) { audio.stream = new McmpStream(source); // VIMA decompression stream... parseSoundHeader(audio, source); } else if (ext.equals("imu")) { final int headerSize = parseSoundHeader(audio, source); audio.stream = new AudioInputStream() { public void seek(int pos) throws IOException { source.position(pos + headerSize); } public int read() throws IOException { return source.getUByte(); } public int read(byte[] dst, int off, int len) throws IOException { source.get(dst, off, len); return len; } }; } else { System.err.println("Currently unsupported audio ext: " + ext); return null; } return audio; } public DataSource write(Audio source) throws IOException { throw new UnsupportedOperationException(); // Todo } public String[] getFileExtensions() { return new String[]{"wav", "imu", "imc"}; } public Class<Audio> getEntryClass() { return Audio.class; } private int parseSoundHeader(Audio audio, final DataSource source) throws IOException { List<UnresolvedJump> unresolvedJumps = new LinkedList<UnresolvedJump>(); int tag = source.getInt(); if (tag == (('R' << 24) | ('I' << 16) | ('F' << 8) | 'F')) { Region main = new Region(); audio.regions.add(main); main.offset = 0; source.skip(18); audio.channels = source.get(); source.skip(1); audio.sampleRate = source.getIntLE(); source.skip(6); audio.bits = source.get(); source.skip(5); main.length = source.getIntLE(); } else if (tag == (('i' << 24) | ('M' << 16) | ('U' << 8) | 'S')) { int size; final int headerStart = (int) source.position(); source.skip(12); Map<Integer, String> unmappedComments = new HashMap<Integer, String>(); do { tag = source.getInt(); switch (tag) { case (('F' << 24) | ('R' << 16) | ('M' << 8) | 'T'): source.skip(12); audio.bits = source.getInt(); audio.sampleRate = source.getInt(); audio.channels = source.getInt(); break; case (('T' << 24) | ('E' << 16) | ('X' << 8) | 'T'): size = source.getInt(); int offset = source.getInt(); String s = source.getString(size - 4); if(!unmappedComments.containsKey(offset)) unmappedComments.put(offset, s); else unmappedComments.put(offset, unmappedComments.get(offset) + "\n" + s); break; case (('S' << 24) | ('T' << 16) | ('O' << 8) | 'P'): size = source.getInt(); int off = source.getInt(); source.skip(size-4); break; case (('R' << 24) | ('E' << 16) | ('G' << 8) | 'N'): source.skip(4); Region r = new Region(); r.offset = source.getInt(); r.length = source.getInt(); audio.regions.add(r); break; case (('J' << 24) | ('U' << 16) | ('M' << 8) | 'P'): source.skip(4); Jump j = new Jump(); UnresolvedJump uj = new UnresolvedJump(); unresolvedJumps.add(uj); uj.wrap = j; uj.offset = source.getInt(); uj.dest = source.getInt(); j.hookId = source.getInt(); j.fadeDelay = source.getInt(); break; case (('D' << 24) | ('A' << 16) | ('T' << 8) | 'A'): source.skip(4); break; default: System.err.println("Unknown MAP tag: " + tag); } } while (tag != (('D' << 24) | ('A' << 16) | ('T' << 8) | 'A')); final int headerSize = (int) (source.position() - headerStart); for (Region r : audio.regions) { r.comments = unmappedComments.remove(r.offset); r.offset -= headerSize; } // resolve jumps... for (UnresolvedJump uj : unresolvedJumps) { uj.offset -= headerSize; uj.dest -= headerSize; boolean foundSource = false; boolean foundTarget = false; for(Region r: audio.regions) { if(r.offset == uj.offset) { r.jumps.add(uj.wrap); foundSource = true; } if(r.offset == uj.dest) { uj.wrap.target = r; foundTarget = true; } if(foundSource && foundTarget) break; } if(!foundSource || !foundTarget) System.err.println("Couldn't locate target or source for jump!"); } if(!unmappedComments.isEmpty()) { System.err.println("Unmappable comments: "); for(Map.Entry<Integer,String> set: unmappedComments.entrySet()) System.err.println("\t" + set.getKey() + ": " + set.getValue()); } return headerSize; } else { System.err.println("Invalid sound header: " + tagToString(tag)); } return 44; } private class UnresolvedJump { Jump wrap; int offset, dest; } } class McmpStream extends AudioInputStream { private DataSource source; private CompressionEntry[] compEntries; private int entryIdx = 0, bufOff; private byte[] buf; private int headerSize; public McmpStream(DataSource src) throws IOException { this.source = new NullPaddedDataSource(src); if (source.getInt() != (('M' << 24) | ('C' << 16) | ('M' << 8) | 'P')) throw new IOException("Invalid MCMP format :S"); int numCompItems = source.getUShort(); int offset = (int) (source.position() + (numCompItems * 9) + 2); numCompItems--; source.skip(5); compEntries = new CompressionEntry[numCompItems]; for (int i = 0; i < compEntries.length; i++) compEntries[i] = new CompressionEntry(); headerSize = source.getInt(); offset += headerSize; int i; for (i = 0; i < numCompItems; i++) { compEntries[i].codec = source.get(); compEntries[i].decompressedSize = source.getInt(); compEntries[i].compressedSize = source.getInt(); compEntries[i].offset = offset; offset += compEntries[i].compressedSize; } int sizeCodecs = source.getUShort(); for (i = 0; i < numCompItems; i++) { compEntries[i].offset += sizeCodecs; } source.skip(sizeCodecs); headerSize += (int) source.position(); } public synchronized void seek(int pos) throws IOException { int calc = 0; for (int i = 0; i < compEntries.length; i++) { if(pos >= calc && pos < calc + compEntries[i].decompressedSize) { if(entryIdx != i) buf = null; // Need a new buffer entryIdx = i; bufOff = pos - calc; break; } calc += compEntries[i].decompressedSize; } } public synchronized void reset() { entryIdx = 0; bufOff = 0; buf = null; } public synchronized int read() throws IOException { fillBuffer(); if (buf == null) return -1; // EOF return buf[bufOff++] & 0xff; } public synchronized int read(byte[] dest, int off, int len) throws IOException { int read = 0; while (read < len) { fillBuffer(); if (buf == null) return read == 0 ? -1 : read; int toRead = Math.min(len - read, buf.length - bufOff); System.arraycopy(buf, bufOff, dest, off + read, toRead); bufOff += toRead; read += toRead; } return read; } public void close() { reset(); } private void fillBuffer() throws IOException { if (buf == null || bufOff >= buf.length) { buf = nextEntry(); bufOff = 0; } } private synchronized byte[] nextEntry() throws IOException { if (entryIdx >= compEntries.length) return null; CompressionEntry entry = compEntries[entryIdx++]; source.position(entry.offset); return Vima.decompress(source, entry.decompressedSize).toByteArray(); } } class CompressionEntry { public byte codec; public int decompressedSize; public int compressedSize; public int offset; public String toString() { return offset + " - " + offset + decompressedSize; } } class NullPaddedDataSource extends DataSource { private DataSource source; public NullPaddedDataSource(DataSource source) { super(source.container, source.getName()); this.source = source; } public void position(long pos) throws IOException { source.position(pos); } public long position() throws IOException { return source.position(); } public long length() { return source.length(); } @Override public void get(byte[] b, int off, int len) throws IOException { source.get(b, off, len); } @Override public void put(byte[] b, int off, int len) throws IOException { source.put(b, off, len); } public byte get() throws IOException { return position() < length() ? source.get() : 0; } @Override public void put(byte b) throws IOException { source.put(b); } public int hashCode() { return source.hashCode() + 1; } }