package lejos.nxt; import java.io.*; import lejos.util.Delay; /** * NXT sound routines. * */ public class Sound { private static final int RIFF_HDR_SIZE = 44; private static final int RIFF_RIFF_SIG = 0x52494646; private static final int RIFF_WAVE_SIG = 0x57415645; private static final int RIFF_FMT_SIG = 0x666d7420; private static final short RIFF_FMT_PCM = 0x0100; private static final short RIFF_FMT_1CHAN = 0x0100; private static final short RIFF_FMT_8BITS = 0x0800; private static final int RIFF_DATA_SIG = 0x64617461; public static final int VOL_MAX = 100; public static final String VOL_SETTING = "lejos.volume"; // Instruments (yes I know they don't sound anything like the names!) public final static int[] PIANO = new int[]{4, 25, 500, 7000, 5}; public final static int[] FLUTE = new int[]{10, 25, 2000, 1000, 25}; public final static int[] XYLOPHONE = new int[]{1, 8, 3000, 5000, 5}; private static int masterVolume = 0; /** * Static contstructor to force loading of system settings */ static { loadSettings(); } private Sound() { } /** * Play a system sound. * <TABLE BORDER=1> * <TR><TH>aCode</TH><TH>Resulting Sound</TH></TR> * <TR><TD>0</TD><TD>short beep</TD></TR> * <TR><TD>1</TD><TD>double beep</TD></TR> * <TR><TD>2</TD><TD>descending arpeggio</TD></TR> * <TR><TD>3</TD><TD>ascending arpeggio</TD></TR> * <TR><TD>4</TD><TD>long, low buzz</TD></TR> * </TABLE> */ public static int C2 = 523; public static void systemSound(boolean aQueued, int aCode) { if (aCode == 0) playTone(600, 200); else if (aCode == 1) { playTone(600, 150); pause(200); playTone(600, 150); pause(150); } else if (aCode == 2)// C major arpeggio for (int i = 4; i < 8; i++) { playTone(C2 * i / 4, 100); pause(100); } else if (aCode == 3) for (int i = 7; i > 3; i--) { playTone(C2 * i / 4, 100); pause(100); } else if (aCode == 4) { playTone(100, 500); pause(500); } } /** * Beeps once. */ public static void beep() { systemSound(true, 0); } /** * Beeps twice. */ public static void twoBeeps() { systemSound(true, 1); } /** * Downward tones. */ public static void beepSequence() { systemSound(true, 3); } /** * Upward tones. */ public static void beepSequenceUp() { systemSound(true, 2); } /** * Low buzz */ public static void buzz() { systemSound(true, 4); } public static void pause(int t) { Delay.msDelay(t); } /** * Returns the number of milliseconds remaining of the current tone or sample. * @return milliseconds remaining */ public static native int getTime(); /** * Plays a tone, given its frequency and duration. * @param aFrequency The frequency of the tone in Hertz (Hz). * @param aDuration The duration of the tone, in milliseconds. * @param aVolume The volume of the playback 100 corresponds to 100% */ static native void playFreq(int aFrequency, int aDuration, int aVolume); /** * Plays a tone, given its frequency and duration. * @param aFrequency The frequency of the tone in Hertz (Hz). * @param aDuration The duration of the tone, in milliseconds. * @param aVolume The volume of the playback 100 corresponds to 100% */ public static void playTone(int aFrequency, int aDuration, int aVolume) { if (aVolume >= 0) aVolume = (aVolume*masterVolume)/100; else aVolume = -aVolume; playFreq(aFrequency, aDuration, aVolume); } public static void playTone(int freq, int duration) { playTone(freq, duration, VOL_MAX); } /** * Internal method used to play sound sample from a file * @param page the start page of the file * @param offset the start of the samples in the file * @param len the length of the sample to play * @param freq the sampling frequency * @param vol the volume 100 corresponds to 100% */ static native void playSample(int page, int offset, int len, int freq, int vol); /** * Read an LSB format * @param d stream to read from * @return the read int * @throws java.io.IOException */ private static int readLSBInt(DataInputStream d) throws IOException { int val = d.readByte() & 0xff; val |= (d.readByte() & 0xff) << 8; val |= (d.readByte() & 0xff) << 16; val |= (d.readByte() & 0xff) << 24; return val; } /** * Play a wav file * @param file the 8-bit PWM (WAV) sample file * @param vol the volume percentage 0 - 100 * @return The number of milliseconds the sample will play for or < 0 if * there is an error. */ public static int playSample(File file, int vol) { // First check that we have a wave file. File must be at least 44 bytes // in size to contain a RIFF header. int offset = 0; int sampleRate = 0; int dataLen = 0; if (file.length() < RIFF_HDR_SIZE) return -9; // Now check for a RIFF header try { FileInputStream f = new FileInputStream(file); DataInputStream d = new DataInputStream(f); if (d.readInt() != RIFF_RIFF_SIG) return -1; // Skip chunk size d.readInt(); // Check we have a wave file if (d.readInt() != RIFF_WAVE_SIG) return -2; if (d.readInt() != RIFF_FMT_SIG) return -3; offset += 16; // Now check that the format is PCM, Mono 8 bits. Note that these // values are stored little endian. int sz = readLSBInt(d); if (d.readShort() != RIFF_FMT_PCM) return -4; if (d.readShort() != RIFF_FMT_1CHAN) return -5; sampleRate = readLSBInt(d); d.readInt(); d.readShort(); if (d.readShort() != RIFF_FMT_8BITS) return -6; // Skip any data in this chunk after the 16 bytes above sz -= 16; offset += 20 + sz; while (sz-- > 0) d.readByte(); // Skip optional chunks until we find a data sig (or we hit eof!) for(;;) { int chunk = d.readInt(); dataLen = readLSBInt(d); offset += 8; if (chunk == RIFF_DATA_SIG) break; // Skip to the start of the next chunk offset += dataLen; while(dataLen-- > 0) d.readByte(); } d.close(); } catch (IOException e) { return -8; } if (vol >= 0) vol = (vol*masterVolume)/100; else vol = -vol; playSample(file.getPage(), offset, dataLen, sampleRate, vol); return getTime(); } /** * Play a wav file * @param file the 8-bit PWM (WAV) sample file * @return The number of milliseconds the sample will play for or < 0 if * there is an error. */ public static int playSample(File file) { return playSample(file, VOL_MAX); } static int waitUntil(int t) { int t2; while ((t2 = (int) System.currentTimeMillis()) < t) Thread.yield(); return t2; } /** * Play a note with attack, decay, sustain and release shape. This function * allows the playing of a more musical sounding note. It uses a set of * supplied "instrument" parameters to define the shape of the notes * envelope. * @param inst Instrument definition * @param freq The note to play (in Hz) * @param len The duration of the note (in ms) */ public static void playNote(int[] inst, int freq, int len) { // Generate an enevlope controlled note. The instrument array contains // the shape of the envelope. The attack period, the decay period the // level to decay to and the level to decay to during the sustain part // of the note finally the length of the actual release period. All // periods are given in 2000th of a second units. This is because the // generation of a tone using the playTone function will often take // more than 1ms to execute. This means that the minimum tone segment // that we can reliably generate is 2ms and so we use this unit as the // basis of note generation. int segLen = inst[0]; // All volume settings are scaled by 100. int step = 8000 / segLen; int vol = 2000; // We do not really have fine grained enough timing so try and keep // things aligned as closely as possible to a tick by waiting here // before we start for the next tick. int t = waitUntil((int) System.currentTimeMillis() + 1); // Generate the attack profile from 20 to full volume len /= 2; for (int i = 0; i < segLen; i++) { Sound.playTone(freq, 10, vol / 100); vol += step; t = waitUntil(t + 2); } len -= segLen; // Now do the decay segLen = inst[1]; if (segLen > 0) { step = inst[2] / segLen; for (int i = 0; i < segLen; i++) { Sound.playTone(freq, 100, vol / 100); vol -= step; t = waitUntil(t + 2); } len -= segLen; } segLen = inst[4]; len -= segLen; // adjust length of the sustain and possibly the release to match the // requested note length if (len > 0) { step = inst[3] / len; for (int i = 0; i < len; i++) { Sound.playTone(freq, 100, vol / 100); vol -= step; t = waitUntil(t + 2); } } else segLen += len; // Finally do the release if (segLen > 0) { step = (vol - 1000) / segLen; for (int i = 0; i < segLen; i++) { Sound.playTone(freq, 2, vol / 100); vol -= step; t = waitUntil(t + 2); } } } /** * Set the master volume level * @param vol 0-100 */ public static void setVolume(int vol) { if (vol > VOL_MAX) vol = VOL_MAX; if (vol < 0) vol = 0; masterVolume = vol; } /** * Get the current master volume level * @return the current master volume 0-100 */ public static int getVolume() { return masterVolume; } /** * Load the current system settings associated with this class. Called * automatically to initialize the class. May be called if it is required * to reload any settings. */ public static void loadSettings() { masterVolume = SystemSettings.getIntSetting(VOL_SETTING, 80); } }