/* This file is part of jpcsp. Jpcsp 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. Jpcsp 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 Jpcsp. If not, see <http://www.gnu.org/licenses/>. */ package jpcsp.HLE.modules; import static jpcsp.HLE.kernel.types.SceKernelErrors.ERROR_MP3_DECODING_ERROR; import static jpcsp.HLE.kernel.types.SceKernelErrors.ERROR_MP3_ID_NOT_RESERVED; import static jpcsp.HLE.kernel.types.SceKernelErrors.ERROR_MP3_INVALID_ID; import static jpcsp.HLE.modules.sceAudiocodec.PSP_CODEC_MP3; import static jpcsp.util.Utilities.endianSwap32; import static jpcsp.util.Utilities.readUnaligned16; import static jpcsp.util.Utilities.readUnaligned32; import jpcsp.HLE.CanBeNull; import jpcsp.HLE.CheckArgument; import jpcsp.HLE.HLEFunction; import jpcsp.HLE.HLELogging; import jpcsp.HLE.HLEModule; import jpcsp.HLE.SceKernelErrorException; import jpcsp.HLE.TPointer; import jpcsp.HLE.TPointer32; import jpcsp.Memory; import jpcsp.HLE.Modules; import jpcsp.HLE.kernel.types.SceKernelErrors; import jpcsp.HLE.kernel.types.pspFileBuffer; import jpcsp.HLE.modules.sceAudiocodec.AudiocodecInfo; import jpcsp.media.codec.CodecFactory; import jpcsp.media.codec.mp3.Mp3Decoder; import jpcsp.media.codec.mp3.Mp3Header; import jpcsp.util.Utilities; import org.apache.log4j.Logger; public class sceMp3 extends HLEModule { public static Logger log = Modules.getLogger("sceMp3"); private Mp3Info[] ids; private static final int ID3 = 0x00334449; // "ID3" private static final int TAG_Xing = 0x676E6958; // "Xing" private static final int TAG_Info = 0x6F666E49; // "Info" private static final int TAG_VBRI = 0x49524256; // "VBRI" private static final int infoTagOffsets[][] = {{17, 32}, {9, 17}}; private static final int mp3DecodeDelay = 4000; // Microseconds private static final int maxSamplesBytesStereo = 0x1200; @Override public void start() { ids = new Mp3Info[2]; for (int i = 0; i < ids.length; i++) { ids[i] = new Mp3Info(i); } super.start(); } public int checkId(int id) { if (ids == null || ids.length == 0) { throw new SceKernelErrorException(ERROR_MP3_INVALID_ID); } if (id < 0 || id >= ids.length) { throw new SceKernelErrorException(ERROR_MP3_INVALID_ID); } return id; } public int checkInitId(int id) { id = checkId(id); if (!ids[id].isReserved()) { throw new SceKernelErrorException(ERROR_MP3_ID_NOT_RESERVED); } return id; } public Mp3Info getMp3Info(int id) { return ids[id]; } public static boolean isMp3Magic(int magic) { return (magic & 0xE0FF) == 0xE0FF; } public static class Mp3Info extends AudiocodecInfo { // // The Buffer layout is the following: // - mp3BufSize: maximum buffer size, cannot be changed // - mp3InputBufSize: the number of bytes available for reading // - mp3InputFileReadPos: the index of the first byte available for reading // - mp3InputBufWritePos: the index of the first byte available for writing // (i.e. the index of the first byte after the last byte // available for reading) // The buffer is cyclic, i.e. the byte following the last byte is the first byte. // The following conditions are always true: // - 0 <= mp3InputFileReadPos < mp3BufSize // - 0 <= mp3InputBufWritePos < mp3BufSize // - mp3InputFileReadPos + mp3InputBufSize == mp3InputBufWritePos // or (for cyclic buffer) // mp3InputFileReadPos + mp3InputBufSize == mp3InputBufWritePos + mp3BufSize // // For example: // [................R..........W.......] // | +-> mp3InputBufWritePos // +-> mp3InputFileReadPos // <----------> mp3InputBufSize // <-----------------------------------> mp3BufSize // // mp3BufSize = 8192 // mp3InputFileReadPos = 4096 // mp3InputBufWritePos = 6144 // mp3InputBufSize = 2048 // // MP3 Frame Header (4 bytes): // - Bits 31 to 21: Frame sync (all 1); // - Bits 20 to 19: MPEG Audio version; // - Bits 18 and 17: Layer; // - Bit 16: Protection bit; // - Bits 15 to 12: Bitrate; // - Bits 11 and 10: Sample rate; // - Bit 9: Padding; // - Bit 8: Reserved; // - Bits 7 and 6: Channels; // - Bits 5 and 4: Channel extension; // - Bit 3: Copyrigth; // - Bit 2: Original; // - Bits 1 and 0: Emphasis. // // NOTE: sceMp3 is only capable of handling MPEG Version 1 Layer III data. // // The PSP is always reserving this size at the beginning of the input buffer private static final int reservedBufferSize = 0x5C0; private static final int minimumInputBufferSize = reservedBufferSize; private boolean reserved; private pspFileBuffer inputBuffer; private int bufferAddr; private int outputAddr; private int outputSize; private int sumDecodedSamples; private int halfBufferSize; private int outputIndex; private int loopNum; private int startPos; private long endPos; private int sampleRate; private int bitRate; private int maxSamples; private int channels; private int version; private int numberOfFrames; public Mp3Info(int id) { super(id); } public boolean isReserved() { return reserved; } public void reserve(int bufferAddr, int bufferSize, int outputAddr, int outputSize, long startPos, long endPos) { reserved = true; this.bufferAddr = bufferAddr; this.outputAddr = outputAddr; this.outputSize = outputSize; this.startPos = (int) startPos; this.endPos = endPos; inputBuffer = new pspFileBuffer(bufferAddr + reservedBufferSize, bufferSize - reservedBufferSize, 0, this.startPos); inputBuffer.setFileMaxSize((int) endPos); loopNum = -1; // Looping indefinitely by default initCodec(); halfBufferSize = (bufferSize - reservedBufferSize) >> 1; } @Override public void release() { reserved = false; } @Override public void initCodec() { codec = CodecFactory.getCodec(PSP_CODEC_MP3); setCodecInitialized(false); } public int notifyAddStream(int bytesToAdd) { bytesToAdd = Math.min(bytesToAdd, getWritableBytes()); if (log.isTraceEnabled()) { log.trace(String.format("notifyAddStream inputBuffer %s: %s", inputBuffer, Utilities.getMemoryDump(inputBuffer.getWriteAddr(), bytesToAdd))); } inputBuffer.notifyWrite(bytesToAdd); return 0; } public pspFileBuffer getInputBuffer() { return inputBuffer; } public boolean isStreamDataNeeded() { boolean isDataNeeded; if (inputBuffer.isFileEnd()) { isDataNeeded = false; } else { isDataNeeded = getWritableBytes() > 0; } return isDataNeeded; } public int getSumDecodedSamples() { return sumDecodedSamples; } public int decode(TPointer32 outputBufferAddress) { int result; int decodeOutputAddr = outputAddr + outputIndex; if (inputBuffer.isFileEnd() && inputBuffer.getCurrentSize() <= 0) { int outputBytes = codec.getNumberOfSamples() * 4; Memory mem = Memory.getInstance(); mem.memset(decodeOutputAddr, (byte) 0, outputBytes); result = outputBytes; } else { int decodeInputAddr = inputBuffer.getReadAddr(); int decodeInputLength = inputBuffer.getReadSize(); // Reaching the end of the input buffer (wrapping to its beginning)? if (decodeInputLength < minimumInputBufferSize && decodeInputLength < inputBuffer.getCurrentSize()) { // Concatenate the input into a temporary buffer Memory mem = Memory.getInstance(); mem.memcpy(bufferAddr, decodeInputAddr, decodeInputLength); int wrapLength = Math.min(inputBuffer.getCurrentSize(), minimumInputBufferSize) - decodeInputLength; mem.memcpy(bufferAddr + decodeInputLength, inputBuffer.getAddr(), wrapLength); decodeInputAddr = bufferAddr; decodeInputLength += wrapLength; } if (log.isDebugEnabled()) { log.debug(String.format("Decoding from 0x%08X, length=0x%X to 0x%08X, inputBuffer %s", decodeInputAddr, decodeInputLength, decodeOutputAddr, inputBuffer)); } result = codec.decode(decodeInputAddr, decodeInputLength, decodeOutputAddr); if (result < 0) { result = ERROR_MP3_DECODING_ERROR; } else { int readSize = result; int samples = codec.getNumberOfSamples(); int outputBytes = samples * outputChannels * 2; inputBuffer.notifyRead(readSize); sumDecodedSamples += samples; // Update index in output buffer for next decode() outputIndex += outputBytes; if (outputIndex + outputBytes > outputSize) { // No space enough to store the same amount of output bytes, // reset to beginning of output buffer outputIndex = 0; } result = outputBytes; } if (inputBuffer.isFileEnd() && loopNum != 0) { if (inputBuffer.getCurrentSize() < minimumInputBufferSize || (inputBuffer.getFilePosition() - inputBuffer.getCurrentSize()) > endPos) { if (log.isDebugEnabled()) { log.debug(String.format("Looping loopNum=%d", loopNum)); } if (loopNum > 0) { loopNum--; } resetPlayPosition(0); } } } outputBufferAddress.setValue(decodeOutputAddr); return result; } public int getWritableBytes() { int writeSize = inputBuffer.getNoFileWriteSize(); // Never return more than halfBufferSize (tested on PSP using JpcspTrace), // even when 2*halfBufferSize would be free. if (writeSize >= halfBufferSize) { return halfBufferSize; } return 0; } public int getLoopNum() { return loopNum; } public void setLoopNum(int loopNum) { this.loopNum = loopNum; } public int resetPlayPosition(int position) { inputBuffer.reset(0, startPos); sumDecodedSamples = 0; return 0; } private void parseMp3FrameHeader() { Memory mem = Memory.getInstance(); int startAddr = inputBuffer.getAddr(); int headerAddr = startAddr; int header = readUnaligned32(mem, headerAddr); // Skip the ID3 tags if ((header & 0x00FFFFFF) == ID3) { int size = endianSwap32(readUnaligned32(mem, startAddr + 6)); // Highest bit of each byte has to be ignored (format: 0x7F7F7F7F) size = (size & 0x7F) | ((size & 0x7F00) >> 1) | ((size & 0x7F0000) >> 2) | ((size & 0x7F000000) >> 3); if (log.isDebugEnabled()) { log.debug(String.format("Skipping ID3 of size 0x%X", size)); } inputBuffer.notifyRead(10 + size); headerAddr = startAddr + 10 + size; header = readUnaligned32(mem, headerAddr); } if (!isMp3Magic(header)) { log.error(String.format("Invalid MP3 header 0x%08X", header)); return; } header = Utilities.endianSwap32(header); if (log.isDebugEnabled()) { log.debug(String.format("Mp3 header: 0x%08X", header)); } Mp3Header mp3Header = new Mp3Header(); Mp3Decoder.decodeHeader(mp3Header, header); version = mp3Header.version; channels = mp3Header.nbChannels; sampleRate = mp3Header.sampleRate; bitRate = mp3Header.bitRate; maxSamples = mp3Header.maxSamples; parseInfoTag(headerAddr + 4 + infoTagOffsets[mp3Header.lsf][mp3Header.nbChannels - 1]); parseVbriTag(headerAddr + 4 + 32); } private void parseInfoTag(int addr) { Memory mem = Memory.getInstance(); int tag = readUnaligned32(mem, addr); if (tag == TAG_Xing || tag == TAG_Info) { int numberOfBytes = 0; int flags = endianSwap32(readUnaligned32(mem, addr + 4)); addr += 8; if ((flags & 0x1) != 0) { numberOfFrames = endianSwap32(readUnaligned32(mem, addr)); addr += 4; } if ((flags & 0x2) != 0) { numberOfBytes = endianSwap32(readUnaligned32(mem, addr)); addr += 4; } if (log.isDebugEnabled()) { log.debug(String.format("Found TAG 0x%08X, numberOfFrames=%d, numberOfBytes=0x%X", tag, numberOfFrames, numberOfBytes)); } } } private void parseVbriTag(int addr) { Memory mem = Memory.getInstance(); int tag = readUnaligned32(mem, addr); if (tag == TAG_VBRI) { int version = readUnaligned16(mem, addr + 4); if (version == 1) { int numberOfBytes = endianSwap32(readUnaligned32(mem, addr + 10)); numberOfFrames = endianSwap32(readUnaligned32(mem, addr + 14)); if (log.isDebugEnabled()) { log.debug(String.format("Found TAG 0x%08X, numberOfFrames=%d, numberOfBytes=0x%X", tag, numberOfFrames, numberOfBytes)); } } } } public void init() { parseMp3FrameHeader(); codec.init(0, channels, outputChannels, 0); sumDecodedSamples = 0; } public int getChannelNum() { return channels; } public int getSampleRate() { return sampleRate; } public int getBitRate() { return bitRate; } public int getMaxSamples() { return maxSamples; } public int getVersion() { return version; } public int getNumberOfFrames() { return numberOfFrames; } } public int getFreeMp3Id() { int id = -1; for (int i = 0; i < ids.length; i++) { if (!ids[i].isReserved()) { id = i; break; } } if (id < 0) { return -1; } return id; } @HLEFunction(nid = 0x07EC321A, version = 150, checkInsideInterrupt = true) public int sceMp3ReserveMp3Handle(@CanBeNull TPointer parameters) { long startPos = 0; long endPos = 0; int bufferAddr = 0; int bufferSize = 0; int outputAddr = 0; int outputSize = 0; if (parameters.isNotNull()) { startPos = parameters.getValue64(0); // Audio data frame start position. endPos = parameters.getValue64(8); // Audio data frame end position. bufferAddr = parameters.getValue32(16); // Input AAC data buffer. bufferSize = parameters.getValue32(20); // Input AAC data buffer size. outputAddr = parameters.getValue32(24); // Output PCM data buffer. outputSize = parameters.getValue32(28); // Output PCM data buffer size. if (bufferAddr == 0 || outputAddr == 0) { return SceKernelErrors.ERROR_MP3_INVALID_ADDRESS; } if (startPos < 0 || startPos > endPos) { return SceKernelErrors.ERROR_MP3_INVALID_PARAMETER; } if (bufferSize < 8192 || outputSize < maxSamplesBytesStereo * 2) { return SceKernelErrors.ERROR_MP3_INVALID_PARAMETER; } } if (log.isDebugEnabled()) { log.debug(String.format("sceMp3ReserveMp3Handle parameters: startPos=0x%X, endPos=0x%X, " + "bufferAddr=0x%08X, bufferSize=0x%X, outputAddr=0x%08X, outputSize=0x%X", startPos, endPos, bufferAddr, bufferSize, outputAddr, outputSize)); } int id = getFreeMp3Id(); if (id < 0) { return id; } ids[id].reserve(bufferAddr, bufferSize, outputAddr, outputSize, startPos, endPos); return id; } @HLEFunction(nid = 0x0DB149F4, version = 150, checkInsideInterrupt = true) public int sceMp3NotifyAddStreamData(@CheckArgument("checkInitId") int id, int bytesToAdd) { return getMp3Info(id).notifyAddStream(bytesToAdd); } @HLEFunction(nid = 0x2A368661, version = 150, checkInsideInterrupt = true) public int sceMp3ResetPlayPosition(@CheckArgument("checkInitId") int id) { return getMp3Info(id).resetPlayPosition(0); } @HLELogging(level="info") @HLEFunction(nid = 0x35750070, version = 150, checkInsideInterrupt = true) public int sceMp3InitResource() { return 0; } @HLELogging(level="info") @HLEFunction(nid = 0x3C2FA058, version = 150, checkInsideInterrupt = true) public int sceMp3TermResource() { return 0; } @HLEFunction(nid = 0x3CEF484F, version = 150, checkInsideInterrupt = true) public int sceMp3SetLoopNum(@CheckArgument("checkInitId") int id, int loopNum) { getMp3Info(id).setLoopNum(loopNum); return 0; } @HLEFunction(nid = 0x44E07129, version = 150, checkInsideInterrupt = true) public int sceMp3Init(@CheckArgument("checkId") int id) { Mp3Info mp3Info = getMp3Info(id); mp3Info.init(); if (log.isInfoEnabled()) { log.info(String.format("Initializing Mp3 data: channels=%d, samplerate=%dkHz, bitrate=%dkbps.", mp3Info.getChannelNum(), mp3Info.getSampleRate(), mp3Info.getBitRate())); } return 0; } @HLEFunction(nid = 0x7F696782, version = 150, checkInsideInterrupt = true) public int sceMp3GetMp3ChannelNum(@CheckArgument("checkInitId") int id) { return getMp3Info(id).getChannelNum(); } @HLEFunction(nid = 0x8F450998, version = 150, checkInsideInterrupt = true) public int sceMp3GetSamplingRate(@CheckArgument("checkInitId") int id) { Mp3Info mp3Info = getMp3Info(id); if (log.isDebugEnabled()) { log.debug(String.format("sceMp3GetSamplingRate returning 0x%X", mp3Info.getSampleRate())); } return mp3Info.getSampleRate(); } @HLEFunction(nid = 0xA703FE0F, version = 150, checkInsideInterrupt = true) public int sceMp3GetInfoToAddStreamData(@CheckArgument("checkInitId") int id, @CanBeNull TPointer32 writeAddr, @CanBeNull TPointer32 writableBytesAddr, @CanBeNull TPointer32 readOffsetAddr) { Mp3Info info = getMp3Info(id); writeAddr.setValue(info.getInputBuffer().getWriteAddr()); writableBytesAddr.setValue(info.getWritableBytes()); readOffsetAddr.setValue(info.getInputBuffer().getFilePosition()); if (log.isDebugEnabled()) { log.debug(String.format("sceMp3GetInfoToAddStreamData returning writeAddr=0x%08X, writableBytes=0x%X, readOffset=0x%X", writeAddr.getValue(), writableBytesAddr.getValue(), readOffsetAddr.getValue())); } return 0; } @HLEFunction(nid = 0xD021C0FB, version = 150, checkInsideInterrupt = true) public int sceMp3Decode(@CheckArgument("checkInitId") int id, TPointer32 bufferAddress) { int result = getMp3Info(id).decode(bufferAddress); if (log.isDebugEnabled()) { log.debug(String.format("sceMp3Decode bufferAddress=%s(0x%08X) returning 0x%X", bufferAddress, bufferAddress.getValue(), result)); } if (result >= 0) { Modules.ThreadManForUserModule.hleKernelDelayThread(mp3DecodeDelay, false); } return result; } @HLEFunction(nid = 0xD0A56296, version = 150, checkInsideInterrupt = true) public boolean sceMp3CheckStreamDataNeeded(@CheckArgument("checkInitId") int id) { return getMp3Info(id).isStreamDataNeeded(); } @HLEFunction(nid = 0xF5478233, version = 150, checkInsideInterrupt = true) public int sceMp3ReleaseMp3Handle(@CheckArgument("checkInitId") int id) { getMp3Info(id).release(); return 0; } @HLEFunction(nid = 0x354D27EA, version = 150) public int sceMp3GetSumDecodedSample(@CheckArgument("checkInitId") int id) { int sumDecodedSamples = getMp3Info(id).getSumDecodedSamples(); if (log.isDebugEnabled()) { log.debug(String.format("sceMp3GetSumDecodedSample returning 0x%X", sumDecodedSamples)); } return sumDecodedSamples; } @HLEFunction(nid = 0x87677E40, version = 150, checkInsideInterrupt = true) public int sceMp3GetBitRate(@CheckArgument("checkInitId") int id) { return getMp3Info(id).getBitRate(); } @HLEFunction(nid = 0x87C263D1, version = 150, checkInsideInterrupt = true) public int sceMp3GetMaxOutputSample(@CheckArgument("checkInitId") int id) { Mp3Info mp3Info = getMp3Info(id); if (log.isDebugEnabled()) { log.debug(String.format("sceMp3GetMaxOutputSample returning 0x%X", mp3Info.getMaxSamples())); } return mp3Info.getMaxSamples(); } @HLEFunction(nid = 0xD8F54A51, version = 150, checkInsideInterrupt = true) public int sceMp3GetLoopNum(@CheckArgument("checkInitId") int id) { return getMp3Info(id).getLoopNum(); } @HLEFunction(nid = 0x3548AEC8, version = 150) public int sceMp3GetFrameNum(@CheckArgument("checkInitId") int id) { return getMp3Info(id).getNumberOfFrames(); } @HLEFunction(nid = 0xAE6D2027, version = 150) public int sceMp3GetVersion(@CheckArgument("checkInitId") int id) { return getMp3Info(id).getVersion(); } @HLEFunction(nid = 0x0840E808, version = 150, checkInsideInterrupt = true) public int sceMp3ResetPlayPosition2(@CheckArgument("checkInitId") int id, int position) { return getMp3Info(id).resetPlayPosition(position); } @HLEFunction(nid = 0x1B839B83 , version = 620) public int sceMp3LowLevelInit(@CheckArgument("checkInitId") int id, int unknown) { Mp3Info mp3Info = getMp3Info(id); // Always output in stereo, even if the input is mono mp3Info.getCodec().init(0, 2, 2, 0); return 0; } @HLEFunction(nid = 0xE3EE2C81, version = 620) public int sceMp3LowLevelDecode(@CheckArgument("checkInitId") int id, TPointer sourceAddr, TPointer32 sourceBytesConsumedAddr, TPointer samplesAddr, TPointer32 sampleBytesAddr) { Mp3Info mp3Info = getMp3Info(id); int result = mp3Info.getCodec().decode(sourceAddr.getAddress(), 10000, samplesAddr.getAddress()); if (log.isDebugEnabled()) { log.debug(String.format("sceMp3LowLevelDecode result=0x%08X, samples=0x%X", result, mp3Info.getCodec().getNumberOfSamples())); } if (result < 0) { return SceKernelErrors.ERROR_MP3_LOW_LEVEL_DECODING_ERROR; } sourceBytesConsumedAddr.setValue(result); sampleBytesAddr.setValue(mp3Info.getCodec().getNumberOfSamples() * 4); return 0; } }