/* * Universal Media Server, for streaming any media to DLNA * compatible renderers based on the http://www.ps3mediaserver.org. * Copyright (C) 2012 UMS developers. * * This program is a 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; version 2 * of the License only. * * This program 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 this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms.util; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Locale; import net.pms.PMS; import net.pms.configuration.FormatConfiguration; import net.pms.dlna.DLNAMediaAudio; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.DLNAThumbnail; import net.pms.image.ImageFormat; import net.pms.image.ImagesUtil.ScaleType; import org.apache.commons.lang3.StringUtils; import org.jaudiotagger.tag.FieldKey; import org.jaudiotagger.tag.Tag; import org.jaudiotagger.tag.id3.ID3v1Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is a utility class for audio related methods. */ public final class AudioUtils { private static final Logger LOGGER = LoggerFactory.getLogger(AudioUtils.class); // No instantiation private AudioUtils() { } /** * Checks if a given {@link Tag} supports a given {@link FieldKey}. * * @param tag the {@link Tag} to check for support * @param key the {@link FieldKey} to check for support for * * @return The result */ public static boolean tagSupportsFieldKey(Tag tag, FieldKey key) { try { tag.getFirst(key); return true; } catch (UnsupportedOperationException e) { return false; } } /** * Due to mencoder/ffmpeg bug we need to manually remap audio channels for LPCM * output. This function generates argument for channels/pan audio filters * * @param audioTrack DLNAMediaAudio resource * @return argument for -af option or null if we can't remap to desired numberOfOutputChannels */ public static String getLPCMChannelMappingForMencoder(DLNAMediaAudio audioTrack) { // for reference // Channel Arrangement for Multi Channel Audio Formats // http://avisynth.org/mediawiki/GetChannel // http://flac.sourceforge.net/format.html#frame_header // http://msdn.microsoft.com/en-us/windows/hardware/gg463006.aspx#E6C // http://labs.divx.com/node/44 // http://lists.mplayerhq.hu/pipermail/mplayer-users/2006-October/063511.html // // Format Ch.0 Ch.1 Ch.2 Ch.3 Ch.4 Ch.5 ch.6 ch.7 // 1.0 WAV/FLAC/MP3/WMA FC // 2.0 WAV/FLAC/MP3/WMA FL FR // 4.0 WAV/FLAC/MP3/WMA FL FR SL SR // 5.0 WAV/FLAC/MP3/WMA FL FR FC SL SR // 5.1 WAV/FLAC/MP3/WMA FL FR FC LFE SL SR // 5.1 PCM (mencoder) FL FR SR FC SL LFE // 7.1 PCM (mencoder) FL SL RR SR FR LFE RL FC // 5.1 AC3 FL FC FR SL SR LFE // 5.1 DTS/AAC FC FL FR SL SR LFE // 5.1 AIFF FL SL FC FR SR LFE // // FL : Front Left // FC : Front Center // FR : Front Right // SL : Surround Left // SR : Surround Right // LFE : Low Frequency Effects (Sub) String mixer = null; int numberOfInputChannels = audioTrack.getAudioProperties().getNumberOfChannels(); if (numberOfInputChannels == 6) { // 5.1 // we are using PCM output and have to manually remap channels because of MEncoder's incorrect PCM mappings // (as of r34814 / SB28) // as of MEncoder r34814 '-af pan' do nothing (LFE is missing from right channel) // same thing for AC3 transcoding. Thats why we should always use 5.1 output on PS3MS configuration // and leave stereo downmixing to PS3! // mixer for 5.1 => 2.0 mixer = "pan=2:1:0:0:1:0:1:0.707:0.707:1:0:1:1"; mixer = "channels=6:6:0:0:1:1:2:5:3:2:4:4:5:3"; } else if (numberOfInputChannels == 8) { // 7.1 // remap and leave 7.1 // inputs to PCM encoder are FL:0 FR:1 RL:2 RR:3 FC:4 LFE:5 SL:6 SR:7 mixer = "channels=8:8:0:0:1:4:2:7:3:5:4:1:5:3:6:6:7:2"; } // do nothing for stereo tracks return mixer; } /** * Parses the old RealAudio 1.0 and 2.0 formats that's not supported by * neither {@link org.jaudiotagger} nor MediaInfo. Returns {@code false} if * {@code channel} isn't one of these formats or the parsing fails. * <p> * Primary references: * <ul> * <li><a href="https://wiki.multimedia.cx/index.php/RealMedia">RealAudio on * MultimediaWiki</a></li> * <li><a * href="https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/rmdec.c" * >FFmpeg rmdec.c</a></li> * </ul> * * @param channel the {@link Channel} containing the input. Size will only * be parsed if {@code channel} is a {@link FileChannel} * instance. * @param media the {@link DLNAMediaInfo} instance to write the parsing * results to. * @return {@code true} if the {@code channel} input is in RealAudio 1.0 or * 2.0 format and the parsing succeeds; false otherwise */ public static boolean parseRealAudio(ReadableByteChannel channel, DLNAMediaInfo media) { final byte[] magicBytes = {0x2E, 0x72, 0x61, (byte) 0xFD}; ByteBuffer buffer = ByteBuffer.allocate(8); buffer.order(ByteOrder.BIG_ENDIAN); DLNAMediaAudio audio = new DLNAMediaAudio(); try { int count = channel.read(buffer); if (count < 4) { LOGGER.trace("Input is too short to be RealAudio"); return false; } buffer.flip(); byte[] signature = new byte[4]; buffer.get(signature); if (!Arrays.equals(magicBytes, signature)) { if (LOGGER.isTraceEnabled()) { LOGGER.trace( "Input signature ({}) mismatches RealAudio version 1.0 or 2.0", new String(signature, StandardCharsets.US_ASCII) ); } return false; } media.setContainer(FormatConfiguration.RA); short version = buffer.getShort(); int reportedHeaderSize = 0; int reportedDataSize = 0; if (version == 3) { audio.setCodecA("RealAudio 14.4"); audio.getAudioProperties().setNumberOfChannels(1); audio.getAudioProperties().setSampleFrequency(8000); short headerSize = buffer.getShort(); buffer = ByteBuffer.allocate(headerSize); channel.read(buffer); buffer.flip(); buffer.position(8); int bytesPerMinute = buffer.getShort() & 0xFFFF; reportedDataSize = buffer.getInt(); byte b = buffer.get(); if (b != 0) { byte[] title = new byte[b & 0xFF]; buffer.get(title); String titleString = new String(title, StandardCharsets.US_ASCII); audio.setSongname(titleString); audio.setAudioTrackTitleFromMetadata(titleString); } b = buffer.get(); if (b != 0) { byte[] artist = new byte[b & 0xFF]; buffer.get(artist); audio.setArtist(new String(artist, StandardCharsets.US_ASCII)); } audio.setBitRate(bytesPerMinute * 8 / 60); media.setBitrate(bytesPerMinute * 8 / 60); } else if (version == 4 || version == 5) { buffer = ByteBuffer.allocate(14); channel.read(buffer); buffer.flip(); buffer.get(signature); if (!".ra4".equals(new String(signature, StandardCharsets.US_ASCII))) { LOGGER.debug("Invalid RealAudio 2.0 signature \"{}\"", new String(signature, StandardCharsets.US_ASCII)); return false; } reportedDataSize = buffer.getInt(); buffer.getShort(); //skip version repeated reportedHeaderSize = buffer.getInt(); buffer = ByteBuffer.allocate(reportedHeaderSize); channel.read(buffer); buffer.flip(); buffer.getShort(); // skip codec flavor buffer.getInt(); // skip coded frame size buffer.getInt(); // skip unknown long bytesPerMinute = buffer.getInt() & 0xFFFFFFFFL; buffer.getInt(); // skip unknown buffer.getShort(); // skip sub packet buffer.getShort(); // skip frame size buffer.getShort(); // skip sub packet size buffer.getShort(); // skip unknown if (version == 5) { buffer.position(buffer.position() + 6); // skip unknown } short sampleRate = buffer.getShort(); buffer.getShort(); // skip unknown short sampleSize = buffer.getShort(); short nrChannels = buffer.getShort(); byte[] fourCC; if (version == 4) { buffer.position(buffer.get() + buffer.position()); // skip interleaver id fourCC = new byte[buffer.get()]; buffer.get(fourCC); } else { buffer.getFloat(); // skip deinterlace id fourCC = new byte[4]; buffer.get(fourCC); } String fourCCString = new String(fourCC, StandardCharsets.US_ASCII).toLowerCase(Locale.ROOT); switch (fourCCString) { case "lpcJ": audio.setCodecA("RealAudio 14.4"); break; case "28_8": audio.setCodecA("RealAudio 28.8"); break; case "dnet": audio.setCodecA(FormatConfiguration.AC3); break; case "sipr": audio.setCodecA("Sipro"); break; case "cook": audio.setCodecA(FormatConfiguration.COOK); case "atrc": audio.setCodecA(FormatConfiguration.ATRAC); case "ralf": audio.setCodecA(FormatConfiguration.REALAUDIO_LOSSLESS); case "raac": audio.setCodecA(FormatConfiguration.AAC); case "racp": audio.setCodecA(FormatConfiguration.AAC_HE); default: LOGGER.debug("Unknown RealMedia codec FourCC \"{}\" - parsing failed", fourCCString); return false; } if (buffer.hasRemaining()) { parseRealAudioMetaData(buffer, audio, version); } audio.setBitRate((int) (bytesPerMinute * 8 / 60)); media.setBitrate((int) (bytesPerMinute * 8 / 60)); audio.setBitsperSample(sampleSize); audio.getAudioProperties().setNumberOfChannels(nrChannels); audio.getAudioProperties().setSampleFrequency(sampleRate); } else { LOGGER.error("Could not parse RealAudio format - unknown format version {}", version); return false; } media.getAudioTracksList().add(audio); long fileSize = 0; if (channel instanceof FileChannel) { fileSize = ((FileChannel) channel).size(); media.setSize(fileSize); } // Duration is estimated based on bitrate and might not be accurate if (audio.getBitRate() > 0) { int dataSize; if (fileSize > 0 && reportedHeaderSize > 0) { int fullHeaderSize = reportedHeaderSize + (version == 3 ? 8 : 16); if (reportedDataSize > 0) { dataSize = (int) Math.min(reportedDataSize, fileSize - fullHeaderSize); } else { dataSize = (int) (fileSize - fullHeaderSize); } } else { dataSize = reportedDataSize; } media.setDuration((double) dataSize / audio.getBitRate() * 8); } } catch (IOException e) { LOGGER.debug("Error while trying to parse RealAudio version 1 or 2: {}", e.getMessage()); LOGGER.trace("", e); return false; } if ( PMS.getConfiguration() != null && !PMS.getConfiguration().getAudioThumbnailMethod().equals(CoverSupplier.NONE) && ( StringUtils.isNotBlank(media.getFirstAudioTrack().getSongname()) || StringUtils.isNotBlank(media.getFirstAudioTrack().getArtist()) ) ) { ID3v1Tag tag = new ID3v1Tag(); if (StringUtils.isNotBlank(media.getFirstAudioTrack().getSongname())) { tag.setTitle(media.getFirstAudioTrack().getSongname()); } if (StringUtils.isNotBlank(media.getFirstAudioTrack().getArtist())) { tag.setArtist(media.getFirstAudioTrack().getArtist()); } try { media.setThumb(DLNAThumbnail.toThumbnail( CoverUtil.get().getThumbnail(tag), 640, 480, ScaleType.MAX, ImageFormat.SOURCE, false )); } catch (IOException e) { LOGGER.error( "An error occurred while generating thumbnail for RealAudio source: [\"{}\", \"{}\"]", tag.getFirstTitle(), tag.getFirstArtist() ); } } media.setThumbready(true); return true; } private static void parseRealAudioMetaData(ByteBuffer buffer, DLNAMediaAudio audio, short version) { buffer.position(buffer.position() + (version == 4 ? 3 : 4)); // skip unknown byte b = buffer.get(); if (b != 0) { byte[] title = new byte[Math.min(b & 0xFF, buffer.remaining())]; buffer.get(title); String titleString = new String(title, StandardCharsets.US_ASCII); audio.setSongname(titleString); audio.setAudioTrackTitleFromMetadata(titleString); } if (buffer.hasRemaining()) { b = buffer.get(); if (b != 0) { byte[] artist = new byte[Math.min(b & 0xFF, buffer.remaining())]; buffer.get(artist); audio.setArtist(new String(artist, StandardCharsets.US_ASCII)); } } } }