/******************************************************************************* * Copyright (c) 2015 - 2017 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *******************************************************************************/ package jsettlers.graphics.sound; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Random; import go.graphics.sound.ISoundDataRetriever; import go.graphics.sound.SoundPlayer; import jsettlers.common.CommonConstants; import jsettlers.common.map.shapes.MapRectangle; import jsettlers.common.position.ShortPoint2D; import jsettlers.graphics.map.MapDrawContext; import jsettlers.graphics.reader.bytereader.ByteReader; import jsettlers.graphics.startscreen.SettingsManager; /* * This class manages reading and playing of the sound file. * <p> * Some known sounds: * <p> * * 0 lumberjack * 1 (6 times): bricklayer <br> * 2 (3 times): digger <br> * 3 (twice): stonecutter <br> * 5: sawmiller <br> * 6: smith <br> * 7: smith <br> * 8 and 12: farmer <br> * 14: donkey <br> * 15: fisherman * 20: shipyard * 21: healer * 24: geologist * 25: bowman * 30: sword Soldier <br> * 31/32 (soldier ?) <br> * 33 Bowman <br> * 35 soldier killed <br> * 36: falling tree <br> * 39: pigs or brewer? <br> * 40: donkey or dying pig (butcher) <br> * 41: donkey <br> * 42: wind/mill: 2.5s <br> * 45: charcoal burner * 51: trigger for building destruction * 56: lock <br> * 57-59: notification sounds<br> * 62: Ui klick <br> * 67: desert * 68, 68b: Sea <br> * 69, 69b: Bird <br> * 70, 70b: Bird * 71: Water (river) <br> * 72 (+ alternaitves): moor <br> * 73: wind <br> * 74: crazy wind <br> * 75 (3 times): thunder <br> * 76 (2 times): rain <br> * 80: You are beeing attacked <br> * 81: Mill, <br> * 82: older mill, <br> * 83: even older mill <br> * 84: catapult <br> * 85: Arrow shooting <br> * 86 -90: canon shooting <br> * 91: fire <br> * 92: small fire on wood or building <br> * 93: collapsing building * 100-110: Attacked (same sound?) ? <br> * 106: announcement of missing tool * 111: 112: gong, <br> * 113: (4 times): amazone killed * 116: refused center of work displacement * 117: bees * * @author michael */ public class SoundManager { private static final int Z_STEPS_FOR_MAX_VOLUME = 50; private static final int SOUND_META_LENGTH = 16; private static final int SOUND_FILE_START = 0x24; private static final byte[] SOUND_FILE_MAGIC = new byte[] { 0x44, 0x15, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00 }; private static final int SEQUENCE_N = 118; /** * Sound ID when we are attacked. */ public static final int NOTIFY_ATTACKED = 80; /** * The lookup paths for the dat files. */ private static final ArrayList<File> lookupPaths = new ArrayList<>(); private final SoundPlayer player; private final Random random = new Random(); /** * The start positions of all the playable sounds. */ private int[][] soundStarts; private boolean initializing = false; private MapDrawContext map = null; private MapRectangle area = null; /** * Creates a new sound manager. * * @param player * The player to play sounds at. */ public SoundManager(SoundPlayer player) { this.player = player; initialize(); } /** * Reads the start indexes of the sounds. * * @param reader * The reader to read from. * @return An array of start indexes for each sound and it's variants. * @throws IOException * If the file could not be read. */ protected static int[][] getSoundStarts(ByteReader reader) throws IOException { int[] sequenceHeaderStarts = new int[SEQUENCE_N]; for (int i = 0; i < SEQUENCE_N; i++) { sequenceHeaderStarts[i] = reader.read32(); } int[][] playerIds = new int[SEQUENCE_N][]; for (int i = 0; i < SEQUENCE_N; i++) { reader.skipTo(sequenceHeaderStarts[i]); int alternativeCount = reader.read32(); int[] starts = new int[alternativeCount]; for (int j = 0; j < alternativeCount; j++) { starts[j] = reader.read32(); } playerIds[i] = starts; } return playerIds; } /** * Opens the sound file. * * @return The file reader. * @throws IOException * If the file could not be opened, */ protected static ByteReader openSoundFile() throws IOException { File sndFile = getSoundFile(); if (sndFile == null) { throw new IOException("Sound file not found."); } RandomAccessFile randomAccessFile = new RandomAccessFile(sndFile, "r"); ByteReader reader = new ByteReader(randomAccessFile); reader.assumeToRead(SOUND_FILE_MAGIC); reader.skipTo(SOUND_FILE_START); return reader; } private static File getSoundFile() { File sndFile = null; synchronized (lookupPaths) { for (File dir : lookupPaths) { File file = new File(dir, "Siedler3_00.dat"); if (file.exists()) { sndFile = file; break; } } } return sndFile; } /** * Plays a given sound. * * @param soundId * The sound id to play. * @param volume * The volume */ public void playSound(int soundId, float volume) { initialize(); if (soundStarts != null && soundId >= 0 && soundId < SEQUENCE_N) { int[] alternatives = soundStarts[soundId]; if (alternatives != null && alternatives.length > 0) { int rand = random.nextInt(alternatives.length); float soundVolume = SettingsManager.getInstance().getVolume() * volume; player.playSound(alternatives[rand], soundVolume, soundVolume); } } } public void playSound(int soundId, float volume, ShortPoint2D position) { playSound(soundId, volume, position.x, position.y); } /** * Plays a given sound at a given coordinate * * @param soundId * The sound id to play * @param volume * The volume * @param x * The x coordinate of the sound * @param y * The y coordinate of the sound */ public void playSound(int soundId, float volume, int x, int y) { if (map == null || map.getVisibleStatus(x, y) <= CommonConstants.FOG_OF_WAR_EXPLORED) { // only play sounds when fog of war level is higher than explored return; } initialize(); if (soundStarts != null && soundId >= 0 && soundId < SEQUENCE_N && area != null) { int[] alternatives = soundStarts[soundId]; if (alternatives != null && alternatives.length > 0) { int rand = random.nextInt(alternatives.length); float maxVolume = SettingsManager.getInstance().getVolume() * volume; int maxA = area.getLineLength(); // get screen area int maxB = area.getLines(); int b = y - area.getMinY(); int a = x - area.getLineStartX(b); float leftVolume, rightVolume; if (a < 0) { // volume depending on position right or left leftVolume = 0; rightVolume = 0; } else if (a < maxA / 4) { leftVolume = maxVolume * 4f * a / maxA; rightVolume = 0; } else if (a < 3 * maxA / 4) { leftVolume = maxVolume * (2f * a / maxA - .5f); rightVolume = maxVolume * (2f * (maxA - a) / maxA - .5f); } else if (a < maxA) { leftVolume = 0; rightVolume = maxVolume * 4f * (maxA - a) / maxA; } else { leftVolume = 0; rightVolume = 0; } float distanceVolume = Z_STEPS_FOR_MAX_VOLUME; if (b < 0) // volume depending on position up or down distanceVolume = 0; else if (b < maxB / 4) distanceVolume = 4f * Z_STEPS_FOR_MAX_VOLUME * b / maxB; else if (b >= maxB) distanceVolume = 0; else if (b >= 3 * maxB / 4) distanceVolume = 4f * Z_STEPS_FOR_MAX_VOLUME * (maxB - b) / maxB; distanceVolume /= maxA; // volume depending on zoom level if (distanceVolume > 1) { distanceVolume = 1; } leftVolume *= distanceVolume; rightVolume *= distanceVolume; player.playSound(alternatives[rand], leftVolume, rightVolume); } } } private void initialize() { synchronized (this) { if (initializing) { return; } initializing = true; } new Thread(() -> { try { loadSounds(); } catch (Throwable e) { e.printStackTrace(); } }, "sound loader").start(); } private void loadSounds() throws IOException { ByteReader reader = openSoundFile(); this.soundStarts = getSoundStarts(reader); player.setSoundDataRetriever(new SoundDataRetriever(reader)); } /** * Adds a sound file lookup path. * * @param file * The file path. */ public static void addLookupPath(File file) { synchronized (lookupPaths) { lookupPaths.add(file); } } /** * This class wraps an open {@link ByteReader} to a {@link ISoundDataRetriever}. * * @author Michael Zangl */ private static class SoundDataRetriever implements ISoundDataRetriever { private final ByteReader reader; /** * Create a new {@link SoundDataRetriever}. * * @param reader * The byte reader. */ SoundDataRetriever(ByteReader reader) { this.reader = reader; } @Override public synchronized short[] getSoundData(int soundStart) throws IOException { return SoundManager.getSoundData(reader, soundStart); } } /** * Reads the sound data from a byte reader. * * @param reader * The reader to read. * @param start * The sound start position. * @return The read sound data. * @throws IOException * If that sound could not be read. */ protected static short[] getSoundData(ByteReader reader, int start) throws IOException { reader.skipTo(start); int length = reader.read32() / 2 - SOUND_META_LENGTH; reader.read32(); reader.read32(); // mostly 22050 reader.read32(); // mostly 44100 reader.read32(); return loadSound(reader, length); } private static short[] loadSound(ByteReader reader, int length) throws IOException { if (length < 0) { return new short[0]; } short[] data = new short[length]; for (int i = 0; i < length; i++) { data[i] = (short) reader.read16signed(); } return data; } public void setMap(MapDrawContext map) { this.map = map; this.area = map.getScreenArea(); } }