package org.jcodec.player.filters.audio; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import javax.sound.sampled.AudioFormat; import org.jcodec.common.AudioUtil; import org.jcodec.common.IntArrayList; import org.jcodec.common.model.AudioBuffer; import org.jcodec.common.model.AudioFrame; import org.jcodec.common.model.ChannelLabel; import org.jcodec.common.model.RationalLarge; import org.jcodec.common.tools.Debug; import org.jcodec.player.filters.MediaInfo.AudioInfo; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * Mixes incoming audio tracks and produces 16bit stereo or 5.1 output big * endian * * @author The JCodec project * */ public class AudioMixer implements AudioSource { public static final int NUM_FRAMES = 2048; private int sampleRate; private Pin[] pins; private int dstChannels; private AudioFormat dstFormat; private long curFrame; private FloatFrame[] nextFrame; private class FloatFrame { long startFrame; int frames; int pattern; ChannelLabel[] labels; float[] data; int framePos; public FloatFrame(long startFrame, int frames, int pattern, ChannelLabel[] labels, float[] data) { this.startFrame = startFrame; this.frames = frames; this.pattern = pattern; this.labels = labels; this.data = data; } } public class Pin { private int pattern; private AudioSource src; private float[] floatBuf; private ByteBuffer byteBuf; private int channels; private AudioInfo audioInfo; private ChannelLabel[] labels; private Pin(AudioSource src) throws IOException { this.src = src; this.audioInfo = src.getAudioInfo(); this.channels = audioInfo.getFormat().getChannels(); this.byteBuf = ByteBuffer.allocate(audioInfo.getFormat().getFrameSize() * 96000 * 2); this.floatBuf = new float[NUM_FRAMES * channels]; this.labels = audioInfo.getLabels(); this.pattern = 0; for (int i = 0; i < labels.length; i++) { if (labels[i] != null) pattern |= 1 << i; } } public void mute(int channel) { if (channel >= audioInfo.getFormat().getChannels()) throw new IllegalArgumentException("Invalid channel " + channel); pattern &= ~(1 << channel); } public void unmute(int channel) { if (channel >= audioInfo.getFormat().getChannels()) throw new IllegalArgumentException("Invalid channel " + channel); if (labels[channel] == null) { Debug.println("Can't unmute channel: no label"); return; } pattern |= 1 << channel; } public void toggle(int channel) { if (channel >= audioInfo.getFormat().getChannels()) throw new IllegalArgumentException("Invalid channel " + channel); if (labels[channel] == null) { Debug.println("Can't unmute channel: no label"); return; } pattern ^= 1 << channel; } public FloatFrame getFrame() throws IOException { AudioFrame frame = src.getFrame(byteBuf); if (frame == null) return null; int samples = AudioUtil.toFloat(audioInfo.getFormat(), frame.getData(), floatBuf); return new FloatFrame((frame.getPts() * sampleRate) / frame.getTimescale(), samples / channels, pattern, audioInfo.getLabels(), floatBuf); } public void close() throws IOException { src.close(); } public ChannelLabel[] getLabels() { return audioInfo.getLabels(); } public AudioSource getSource() { return src; } public int[] getSoloChannels() { IntArrayList result = new IntArrayList(); for (int i = 0; i < 32; i++) if (((pattern >> i) & 0x1) == 1) result.add(i); return result.toArray(); } public int getSampleRate() { return (int) audioInfo.getFormat().getSampleRate(); } } public AudioMixer(int channels, AudioSource... src) throws IOException { if (src.length < 1) throw new IllegalArgumentException("Must be at least one audio source"); pins = new Pin[src.length]; this.dstChannels = channels; for (int i = 0; i < src.length; i++) { pins[i] = new Pin(src[i]); } this.sampleRate = pins[0].getSampleRate(); for (int i = 0; i < pins.length; i++) { if (pins[i].getSampleRate() != sampleRate) throw new IllegalArgumentException("Sample rate conversion is not supported. Remove " + i + "th audio source (" + src[i].getAudioInfo().getFormat() + ")"); } dstFormat = new AudioFormat(sampleRate, 16, channels, true, false); } @Override public AudioInfo getAudioInfo() throws IOException { long maxDuration = Long.MIN_VALUE, maxFrames = Long.MIN_VALUE; for (Pin pin : pins) { AudioInfo ai = pin.getSource().getAudioInfo(); long duration = (ai.getDuration() * sampleRate) / ai.getTimescale(); if (duration > maxDuration) maxDuration = duration; if (ai.getNFrames() > maxFrames) maxFrames = ai.getNFrames(); } return new AudioInfo("sowt", sampleRate, maxDuration, maxFrames, "", null, dstFormat, dstChannels == 2 ? new ChannelLabel[] { ChannelLabel.STEREO_LEFT, ChannelLabel.STEREO_RIGHT } : new ChannelLabel[] { ChannelLabel.FRONT_LEFT, ChannelLabel.FRONT_RIGHT, ChannelLabel.CENTER, ChannelLabel.LFE, ChannelLabel.REAR_LEFT, ChannelLabel.REAR_RIGHT }); } @Override public synchronized AudioFrame getFrame(ByteBuffer buf) throws IOException { ByteBuffer out = buf.duplicate(); out.order(ByteOrder.LITTLE_ENDIAN); if (nextFrame == null) { nextFrame = new FloatFrame[pins.length]; for (int i = 0; i < pins.length; i++) nextFrame[i] = pins[i].getFrame(); } long minFrame = Long.MAX_VALUE; for (int i = 0; i < nextFrame.length; i++) { if (nextFrame[i] != null && nextFrame[i].startFrame < minFrame) minFrame = nextFrame[i].startFrame; } if (minFrame == Long.MAX_VALUE) return null; else if (minFrame > curFrame) curFrame = minFrame; long startFrame = curFrame; for (int leftPkt = NUM_FRAMES; leftPkt > 0;) { int mixedFrames = mix(out, nextFrame, curFrame, leftPkt); curFrame += mixedFrames; leftPkt -= mixedFrames; for (int i = 0; i < nextFrame.length; i++) { if (nextFrame[i] == null || nextFrame[i].startFrame + nextFrame[i].frames <= curFrame) nextFrame[i] = pins[i].getFrame(); } } out.flip(); return new AudioFrame(new AudioBuffer(out, dstFormat, NUM_FRAMES), startFrame, NUM_FRAMES, sampleRate, (int) (startFrame / NUM_FRAMES)); } static float[][] contributions = new float[][] { new float[] { 1, .7f, .7f, .7f, .7f, 1, 0, .7f, .7f }, new float[] { 1, 1, 0, .7f, 0, 1, 0, .7f, 0 }, new float[] { 1, 0, 1, 0, .7f, 1, 0, 0, .7f }, new float[] { 1, 1, 0, 1, 0, 0, 0, 0, 0 }, new float[] { 1, 0, 1, 0, 1, 0, 0, 0, 0 }, new float[] { 1, 1, 1, 0, 0, 1, 0, 0, 0 }, new float[] { 1, 1, 1, 0, 0, 0, 1, 0, 0 }, new float[] { .7f, .7f, 0, 0, 0, 0, 0, 1, 0 }, new float[] { .7f, 0, .7f, 0, 0, 0, 0, 0, 1 }, new float[] { 1, 1, 0, 0, 0, 1, 0, 0, 0 }, new float[] { 1, 0, 1, 0, 0, 1, 0, 0, 0 }, new float[] { .7f, 1, 1, 0, 0, 0, 0, .7f, .7f }, new float[] { .7f, .7f, 0, 0, 0, 0, 0, 1, 0 }, new float[] { .7f, 0, .7f, 0, 0, 0, 0, 0, 1 } }; private int mix(ByteBuffer out, FloatFrame[] in, long curFrame, int maxFrames) throws IOException { float[] sum = new float[dstChannels]; float[] mul = new float[dstChannels]; Arrays.fill(mul, 1f); int[] count = new int[dstChannels]; int contribOffset = (dstChannels == 1 ? 0 : (dstChannels == 2 ? 1 : 3)); int frame; for (frame = 0; frame < maxFrames; frame++, curFrame++) { for (int track = 0; track < in.length; track++) { if (in[track] == null || curFrame < in[track].startFrame) continue; if (curFrame >= in[track].startFrame + in[track].frames) return frame; FloatFrame floatFrame = in[track]; int channels = floatFrame.labels.length; for (int channel = 0; channel < channels; channel++) { float sample = floatFrame.data[floatFrame.framePos++]; if (((in[track].pattern >> channel) & 0x1) != 1) continue; float[] fs = contributions[in[track].labels[channel].ordinal()]; if (fs == null) throw new RuntimeException("Label " + in[track].labels[channel] + " is not supported"); for (int i = 0; i < dstChannels; i++) { float gain = fs[i + contribOffset] * sample; sum[i] += gain; mul[i] *= gain; count[i] += fs[i + contribOffset] != 0 ? 1 : 0; } } } for (int i = 0; i < dstChannels; i++) { float val = count[i] > 1 ? clamp1f(sum[i] - mul[i]) : sum[i]; int sample = floatToSigned16Pack(val); out.putShort((short) sample); count[i] = 0; sum[i] = 0f; mul[i] = 1f; } } return frame; } static int floatToSigned16Pack(float f) { return ((int) (f * 32768f)) & 0xffff; } public final static float clamp1f(float f) { if (f > 1f) return 1f; if (f < -1f) return -1f; return f; } @Override public boolean drySeek(RationalLarge second) throws IOException { boolean success = true; for (Pin pin : pins) { success &= pin.getSource().drySeek(second); } return success; } @Override public synchronized void seek(RationalLarge second) throws IOException { for (Pin pin : pins) pin.getSource().seek(second); nextFrame = null; curFrame = 0; } @Override public void close() throws IOException { for (Pin pin : pins) { pin.close(); } } public Pin[] getPins() { return pins; } }