/* * Copyright (c) 2003-onwards Shaven Puppy Ltd * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * 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. * * * Neither the name of 'Shaven Puppy' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * 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 com.shavenpuppy.jglib.tools; import java.io.*; import java.util.*; import javax.sound.sampled.*; import com.shavenpuppy.jglib.Wave; /** * $Id: SoundPacker.java,v 1.17 2011/04/18 23:27:44 cix_foo Exp $ * * The Sound Packer recursively collects .wav files in a directory and loads * their wave data into a single, large buffer and outputs an enormous Wave object. * * @author $Author: cix_foo $ * @version $Revision: 1.17 $ */ public class SoundPacker { /** The output */ private OutputStream outputStream; /** Soundbank name, eg. mysounds.soundbank */ private String soundBank; /** Frequency */ private int frequency; /** Target frequency */ private int targetFrequency; /** Current offset */ private int offset; /** Format */ private int format; /** Divisor for offset */ private int divisor; /** Target format */ private int targetFormat; /** Write the XML only, don't pack the sounds together */ private boolean xmlonly; /** Add the "streamed" tag */ private boolean streamed; /** OGGs */ private List oggs = new ArrayList(); /** Clips */ private List clips = new ArrayList(); /** * A clip */ private static class Clip { final String name, soundBank; final int offset, len, divisor; Clip(String name, String soundBank, int offset, int len, int divisor) { this.name = name; this.soundBank = soundBank; this.offset = offset; this.len = len; this.divisor = divisor; System.out.println(name+" target size : "+(len >> divisor)); } void writeXML(Writer writer) throws IOException { writer.write("\t\t<clip name=\""+name+"\" soundbank=\""+soundBank+"\" offset=\""+(offset >> divisor)+"\" length=\""+(len >> divisor)+"\"/>\n"); } } /** * An ogg */ private static class OGG { final String name; final boolean streamed; OGG(String name, boolean streamed) { this.name = name; this.streamed = streamed; } void writeXML(Writer writer) throws IOException { writer.write("\t<ogg name=\""+name+".ogg\" url=\"classpath:"+name+".ogg\" streamed=\""+streamed+"\"/>\n"); } } private String decodeFormat() { switch (targetFormat) { case Wave.MONO_16BIT: return "MONO_16BIT"; case Wave.MONO_8BIT: return "MONO_8BIT"; case Wave.STEREO_16BIT: return "STEREO_16BIT"; case Wave.STEREO_8BIT: return "STEREO_8BIT"; default: assert false; return "Error - unknown format"; } } /** * Pack a directory * @param dir The name of the directory */ private void pack(String dir) throws Exception { File fDir = new File(dir); if (fDir.isFile()) { doWave(fDir); } else { File[] files = fDir.listFiles(); if (files == null) { return; } for (int i = 0; i < files.length; i ++) { if (files[i].isDirectory()) { System.out.println("------> Recursing into "+files[i]); pack(files[i].getPath()); } else if (files[i].getName().endsWith(".wav")) { doWave(files[i]); } } } } /** * Do a wave file */ private void doWave(File waveFile) throws Exception { System.out.print("Processing "+waveFile+".. "); AudioInputStream ais = AudioSystem.getAudioInputStream(new BufferedInputStream(new FileInputStream(waveFile))); AudioFormat audioFormat = ais.getFormat(); if (audioFormat.getSampleRate() != frequency) { System.out.println("Wrong frequency - ignoring"); return; } int type = 0; if (audioFormat.getChannels() == 1) { if (audioFormat.getSampleSizeInBits() == 8) { type = Wave.MONO_8BIT; } else if (audioFormat.getSampleSizeInBits() == 16) { type = Wave.MONO_16BIT; } else { assert false : "Illegal sample size"+audioFormat.getSampleSizeInBits()+" in "+waveFile; } } else if (audioFormat.getChannels() == 2) { if (audioFormat.getSampleSizeInBits() == 8) { type = Wave.STEREO_8BIT; } else if (audioFormat.getSampleSizeInBits() == 16) { type = Wave.STEREO_16BIT; } else { assert false : "Illegal sample size: "+audioFormat.getSampleSizeInBits()+" in "+waveFile; } } else { assert false : "Only mono or stereo is supported"; } if (type != format) { System.out.println("Wrong format - skipping..."); return; } int length = (int)(ais.getFrameLength() * audioFormat.getFrameSize()); if (xmlonly) { // Simply remember this ogg int pos = waveFile.getCanonicalPath().lastIndexOf('.'); int slashpos = waveFile.getCanonicalPath().lastIndexOf("\\"); String name = waveFile.getCanonicalPath().substring(slashpos + 1, pos); OGG ogg = new OGG(name, streamed); oggs.add(ogg); System.out.println(); return; } /* System.out.println("Format : "+audioFormat); System.out.println(audioFormat.getChannels()+" channels"); System.out.println(audioFormat.getFrameRate()+" frame rate"); System.out.println(audioFormat.getFrameSize()+" frame size"); System.out.println(audioFormat.getSampleRate()+" sample rate"); System.out.println(audioFormat.getSampleSizeInBits()+" sample size"); */ byte[] buf = new byte[length]; int read = 0, total = 0; while ( (read = ais.read(buf, total, buf.length - total)) != -1 && total < buf.length) { total += read; // System.out.println("Read "+total+" bytes of "+buf.length); } System.out.print(" length: "+buf.length+" .. "); System.out.println("\nTrimming - channels:"+audioFormat.getChannels()+" sampleSize:"+audioFormat.getSampleSizeInBits()); // Trim zeros at the end if (audioFormat.getChannels() == 2) { if (audioFormat.getSampleSizeInBits() == 16) { assert buf.length % 4 == 0; buf = trim16S(buf, 100); assert buf.length % 4 == 0; } else if (audioFormat.getSampleSizeInBits() == 8) { assert buf.length % 2 == 0; buf = trimS(buf, 5); assert buf.length % 2 == 0; } else { assert false; } } else if (audioFormat.getChannels() == 1) { if (audioFormat.getSampleSizeInBits() == 16) { assert buf.length % 2 == 0; buf = trim16(buf, 100); assert buf.length % 2 == 0; } else if (audioFormat.getSampleSizeInBits() == 8) { buf = trim(buf, 5); } else { assert false; } } else { assert false; } System.out.println(); int divTarg = frequency / targetFrequency; int targDiv = targetFrequency / frequency; int addition = 0, mask = 0xFFFFFFFF; switch (divTarg) { case 0: // Upsampling, so pad with extra data switch (targDiv) { case 0: case 1: assert false; case 2: // Add an extra sample addition = (audioFormat.getSampleSizeInBits() * audioFormat.getChannels()) / 8; break; case 4: // Add 3 extra samples addition = 3 * (audioFormat.getSampleSizeInBits() * audioFormat.getChannels()) / 8; break; default: assert false; } case 1: addition = 0; break; case 2: // Remove a sample mask = 0xFFFFFFF0; /* switch (format) { case Wave.STEREO_16BIT: mask = 0xFFFFFFF8; break; case Wave.STEREO_8BIT: mask = 0xFFFFFFFC; break; case Wave.MONO_16BIT: mask = 0xFFFFFFFC; break; case Wave.MONO_8BIT: mask = 0xFFFFFFFE; break; default: assert false; } */ break; case 4: mask = 0xFFFFFFF0; /* // Remove 3 samples switch (format) { case Wave.STEREO_16BIT: mask = 0xFFFFFFF0; break; case Wave.STEREO_8BIT: mask = 0xFFFFFFF8; break; case Wave.MONO_16BIT: mask = 0xFFFFFFF8; break; case Wave.MONO_8BIT: mask = 0xFFFFFFFC; break; default: assert false; } */ break; } int newLen = (buf.length & mask) + addition; if (newLen != buf.length) { System.out.println("Resized to "+newLen+" (mask "+Long.toString((mask & 0x7FFFFFFF), 16)+", addition="+addition); byte[] newBuf = new byte[newLen]; System.arraycopy(buf, 0, newBuf, 0, newLen); buf = newBuf; } outputStream.write(buf); int pos = waveFile.getCanonicalPath().lastIndexOf('.'); int slashpos = waveFile.getCanonicalPath().lastIndexOf("\\"); String name = waveFile.getCanonicalPath().substring(slashpos + 1, pos) + ".soundclip"; System.out.println(name); Clip clip = new Clip(name, soundBank, offset, buf.length, divisor); offset += buf.length; clips.add(clip); } byte[] trim(byte[] buf, int threshold) { for (int i = buf.length; --i >= 0; ) { if (buf[i] > threshold || buf[i] < -threshold) { if (i + 1 == buf.length) { System.out.print("No trimming needed"); return buf; } byte[] ret = new byte[i + 1]; System.arraycopy(buf, 0, ret, 0, i + 1); System.out.print("Trimmed down to "+(i+1)+" bytes"); return ret; } } System.out.print("Warning: dummy sound"); return new byte[2]; // Dummy sound } byte[] trim16(byte[] buf, int threshold) { for (int i = buf.length - 2; i >= 0; i -= 2) { int value = ((buf[i] << 8) | (0xFF & buf[i + 1])); if (value > threshold || value < -threshold) { if (i + 2 == buf.length) { System.out.print("No trimming needed"); return buf; } byte[] ret = new byte[i + 2]; System.arraycopy(buf, 0, ret, 0, i + 2); System.out.print("Trimmed down to "+(i+2)+" bytes"); return ret; } } System.out.print("Warning: dummy sound"); return new byte[4]; // Dummy sound } byte[] trimS(byte[] buf, int threshold) { for (int i = buf.length - 2; i >= 0; i -= 2) { if ((buf[i] > threshold || buf[i] < -threshold) || (buf[i + 1] > threshold || buf[i + 1] < -threshold)) { if (i + 2 == buf.length) { System.out.print("No trimming needed"); return buf; } byte[] ret = new byte[i + 2]; System.arraycopy(buf, 0, ret, 0, i + 2); System.out.print("Trimmed down to "+(i+2)+" bytes"); return ret; } } System.out.print("Warning: dummy sound"); return new byte[4]; // Dummy sound } byte[] trim16S(byte[] buf, int threshold) { for (int i = buf.length - 4; i >= 0; i -= 4) { int valueL = ((buf[i] << 8) | (0xFF & buf[i + 1])); int valueR = ((buf[i + 2] << 8) | (0xFF & buf[i + 3])); if (valueL > threshold || valueL < -threshold || valueR > threshold || valueR < -threshold) { if (i + 4 == buf.length) { System.out.print("No trimming needed"); return buf; } byte[] ret = new byte[i + 4]; System.arraycopy(buf, 0, ret, 0, i + 4); System.out.print("Trimmed down to "+(i+4)+" bytes"); return ret; } } System.out.print("Warning: dummy sound"); return new byte[8]; // Dummy sound } /** * C'tor */ public SoundPacker(String rootDir, String outputDir, String output, String outputXML, int frequency, int targetFrequency, boolean stereo, boolean targetStereo, boolean eightBit, boolean xmlonly, boolean streamed) { try { System.out.println("Base output dir:"+outputDir); System.out.println("Output files:"+output+"|"+outputXML); this.frequency = frequency; this.targetFrequency = targetFrequency; this.xmlonly = xmlonly; this.streamed = streamed; if (stereo && eightBit) { format = Wave.STEREO_8BIT; } else if (stereo) { format = Wave.STEREO_16BIT; } else if (eightBit) { format = Wave.MONO_8BIT; } else { format = Wave.MONO_16BIT; } if (targetStereo && eightBit) { targetFormat = Wave.STEREO_8BIT; } else if (targetStereo) { targetFormat = Wave.STEREO_16BIT; } else if (eightBit) { targetFormat = Wave.MONO_8BIT; } else { targetFormat = Wave.MONO_16BIT; } this.soundBank = output + ".soundbank"; int freqDiv = frequency / targetFrequency; int freqDiv2 = targetFrequency / frequency; switch (freqDiv) { case 0: switch (freqDiv2) { case 0: case 1: assert false; break; case 2: divisor = -1; break; case 4: divisor = -2; break; case 8: divisor = -3; break; } break; case 1: divisor = 0; break; case 2: divisor = 1; break; case 4: divisor = 2; break; case 8: divisor = 3; break; } if (stereo && !targetStereo) { divisor ++; } else if (!stereo && targetStereo) { divisor --; } String outputFilename = outputDir + File.separator + output+".raw"; // Create the directory structure to the output file if it doesn't already exist File parentDir = new File(outputFilename).getAbsoluteFile().getParentFile(); parentDir.mkdirs(); if (!xmlonly) { outputStream = new BufferedOutputStream(new FileOutputStream(outputFilename)); } pack(rootDir); if (!xmlonly) { outputStream.flush(); outputStream.close(); } BufferedWriter bw = new BufferedWriter(new FileWriter(outputDir + File.separator + outputXML)); bw.write("<?xml version='1.0' encoding='utf-8'?>\n<resources>\n"); if (xmlonly) { for (Iterator i = oggs.iterator(); i.hasNext(); ) { OGG ogg = (OGG) i.next(); ogg.writeXML(bw); } } else { // bw.write("\t<ogg name=\""+output+".ogg\" url=\"classpath:"+output+".ogg\" length=\""+(offset >> divisor)+"\"/>\n"); bw.write("\t<ogg name=\""+output+".ogg\" url=\"classpath:"+output+".ogg\" />\n"); bw.write("\t<soundbank\n\t\tname=\""+soundBank+"\"\n"); bw.write("\t\tformat=\""+decodeFormat()+"\"\n"); bw.write("\t\tfrequency=\""+targetFrequency+"\"\n"); bw.write("\t\turl=\"resource:"+output+".ogg\"\n\t>\n"); for (Iterator i = clips.iterator(); i.hasNext(); ) { Clip clip = (Clip) i.next(); clip.writeXML(bw); } bw.write("\t</soundbank>\n"); } bw.write("</resources>\n"); bw.flush(); bw.close(); } catch (Exception e) { e.printStackTrace(System.err); } } /** * Usage: * SoundPacker <root input dir> <output file> <output xml> * @param args */ public static void main(String[] args) { if (args.length < 4) { System.err.println("Usage: "); System.err.println("\tSoundPacker <root input dir/file> <root output dir> <output file> <output xml> <frequency> <targetFrequency> [stereo/[stereo|mono]] [8bit] [xmlonly] [streamed]"); System.exit(-1); } List lArgs = Arrays.asList(args); boolean stereo, targetStereo; stereo = lArgs.contains("stereo/mono") || lArgs.contains("stereo/stereo") || lArgs.contains("stereo"); targetStereo = lArgs.contains("mono/stereo") || lArgs.contains("stereo/stereo") || lArgs.contains("stereo"); new SoundPacker(args[0], args[1], args[2], args[3], Integer.parseInt(args[4]), Integer.parseInt(args[4]), stereo, targetStereo, lArgs.contains("8bit"), lArgs.contains("xmlonly"), lArgs.contains("streamed")); } }