// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.resource.mus; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.StringTokenizer; import org.infinity.resource.key.ResourceEntry; import org.infinity.resource.sound.AudioBuffer; import org.infinity.resource.sound.AudioFactory; import org.infinity.util.io.FileManager; import org.infinity.util.io.StreamUtils; public class Entry { // Caches AudioBuffer objects for faster reload private static final LinkedHashMap<Path, AudioBuffer> BufferCache = new LinkedHashMap<>(100); private static final long MAX_CACHE_SIZE = getMaxCacheSize(); private static long currentCacheSize = 0L; private final List<Entry> entryList; private final ResourceEntry entry; private final String line; private final String dir; private AudioBuffer audioBuffer, endBuffer; private String name; private int nextnr; /** * Clears the whole AudioBuffer cache */ public static void clearCache() { BufferCache.clear(); currentCacheSize = 0L; } // adds an AudioBuffer object to the cache private static void addCacheEntry(Path path, String name, AudioBuffer buffer) { if (name != null && buffer != null) { while (currentCacheSize + buffer.getAudioData().length > MAX_CACHE_SIZE && !BufferCache.isEmpty()) { Iterator<Path> iter = BufferCache.keySet().iterator(); if (iter.hasNext()) { AudioBuffer ab = BufferCache.get(iter.next()); iter.remove(); currentCacheSize -= ab.getAudioData().length; } } BufferCache.put(getCacheKey(path, name), buffer); currentCacheSize += buffer.getAudioData().length; } } // returns a cached AudioBuffer object or null if none found private static AudioBuffer getCacheEntry(Path path, String name) { if (name != null) { Path key = getCacheKey(path, name); if (BufferCache.containsKey(key)) { AudioBuffer ab = BufferCache.get(key); return ab; } } return null; } // internally used to create a valid cache key private static Path getCacheKey(Path path, String name) { Path key = null; if (name != null) { name = name.toUpperCase(Locale.ENGLISH); key = (path != null) ? path.resolve(name) : FileManager.resolve(name); key = key.toAbsolutePath(); } return key; } private static long getMaxCacheSize() { // use max. 1/10th of max. available memory or 100MB for caching AudioBuffer objects long memSize = Runtime.getRuntime().maxMemory(); if (memSize == Long.MAX_VALUE || memSize < (long)(256*1024*1024)) { return (long)(32*1024*1024); } else { return Math.max(memSize / 8L, (long)(256*1024*1024)); } } public Entry(ResourceEntry entry, String dir, List<Entry> entries, String line, int nr) { this.entry = entry; this.dir = dir; this.entryList = entries; this.line = line; this.nextnr = nr + 1; } public void close() { audioBuffer = null; endBuffer = null; nextnr = -1; } @Override public String toString() { return line; } public AudioBuffer getEndBuffer() { return endBuffer; } public int getNextNr() { return nextnr; } public AudioBuffer getAudioBuffer() { return audioBuffer; } public void init() throws IOException { StringTokenizer st = new StringTokenizer(line); name = st.nextToken(); audioBuffer = getAudioBuffer(name); while (st.hasMoreTokens()) { String command = st.nextToken(); if (command.equalsIgnoreCase("@TAG")) { String next = st.nextToken(); if (next.equalsIgnoreCase("END")) { nextnr = -1; } else { endBuffer = getAudioBuffer(next); } } else { if (command.equalsIgnoreCase(dir)) { command = st.nextToken(); } if (name.equalsIgnoreCase(command)) { nextnr--; } else { for (int i = 0; i < entryList.size(); i++) { if (entryList.get(i) == null) { nextnr = -1; break; } else if (entryList.get(i).name.equalsIgnoreCase(command)) { nextnr = i; break; } } } } } } private AudioBuffer getAudioBuffer(String fileName) throws IOException { // audio file can reside in a number of different locations Path acmFile = FileManager.query(entry.getActualPath().getParent(), dir, dir + fileName + ".acm"); if (!Files.isRegularFile(acmFile)) { acmFile = FileManager.query(entry.getActualPath().getParent(), fileName + ".acm"); } if (!Files.isRegularFile(acmFile) && fileName.toUpperCase(Locale.ENGLISH).startsWith("MX")) { acmFile = FileManager.query(entry.getActualPath().getParent(), fileName.substring(0, 6), fileName + ".acm"); } if (!Files.isRegularFile(acmFile)) { throw new IOException("Could not find " + fileName); } // simplest case: grab AudioBuffer from cache AudioBuffer audio = getCacheEntry(acmFile, fileName); if (audio != null) { return audio; } try (InputStream is = StreamUtils.getInputStream(acmFile)) { byte[] buffer = new byte[(int)Files.size(acmFile)]; int bytesRead = is.read(buffer); if (bytesRead > 0) { // ignore # channels in header (only ACM will be affected) audio = AudioFactory.getAudioBuffer(buffer, 0, AudioBuffer.AudioOverride.overrideChannels(2)); if (audio != null) { addCacheEntry(acmFile.getParent(), fileName, audio); } } else { throw new IOException("Unexpected end of file"); } } catch (IOException e) { throw new IOException("Error reading " + fileName); } return audio; } }