package org.jcodec.codecs.prores; import static java.lang.Math.min; import static org.jcodec.codecs.prores.ProresConsts.QMAT_CHROMA_APCH; import static org.jcodec.codecs.prores.ProresConsts.QMAT_CHROMA_APCN; import static org.jcodec.codecs.prores.ProresConsts.QMAT_CHROMA_APCO; import static org.jcodec.codecs.prores.ProresConsts.QMAT_CHROMA_APCS; import static org.jcodec.codecs.prores.ProresConsts.QMAT_LUMA_APCH; import static org.jcodec.codecs.prores.ProresConsts.QMAT_LUMA_APCN; import static org.jcodec.codecs.prores.ProresConsts.QMAT_LUMA_APCO; import static org.jcodec.codecs.prores.ProresConsts.QMAT_LUMA_APCS; import static org.jcodec.codecs.prores.ProresConsts.dcCodebooks; import static org.jcodec.codecs.prores.ProresConsts.firstDCCodebook; import static org.jcodec.codecs.prores.ProresConsts.interlaced_scan; import static org.jcodec.codecs.prores.ProresConsts.levCodebooks; import static org.jcodec.codecs.prores.ProresConsts.progressive_scan; import static org.jcodec.codecs.prores.ProresConsts.runCodebooks; import static org.jcodec.common.dct.SimpleIDCT10Bit.fdct10; import static org.jcodec.common.model.ColorSpace.YUV422; import static org.jcodec.common.tools.MathUtil.log2; import static org.jcodec.common.tools.MathUtil.sign; import org.jcodec.common.VideoEncoder; import org.jcodec.common.io.BitWriter; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.model.ColorSpace; import org.jcodec.common.model.Picture8Bit; import org.jcodec.common.model.Rect; import org.jcodec.common.tools.ImageOP; import java.nio.ByteBuffer; /** * * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * Apple ProRes encoder * * @author The JCodec project * */ public class ProresEncoder extends VideoEncoder { private static final int LOG_DEFAULT_SLICE_MB_WIDTH = 3; private static final int DEFAULT_SLICE_MB_WIDTH = 1 << LOG_DEFAULT_SLICE_MB_WIDTH; public final static class Profile { public final static Profile PROXY = new Profile("PROXY", QMAT_LUMA_APCO, QMAT_CHROMA_APCO, "apco", 1000, 4, 8); public final static Profile LT = new Profile("LT", QMAT_LUMA_APCS, QMAT_CHROMA_APCS, "apcs", 2100, 1, 9); public final static Profile STANDARD = new Profile("STANDARD", QMAT_LUMA_APCN, QMAT_CHROMA_APCN, "apcn", 3500, 1, 6); public final static Profile HQ = new Profile("HQ", QMAT_LUMA_APCH, QMAT_CHROMA_APCH, "apch", 5400, 1, 6); private final static Profile[] _values = new Profile[] { PROXY, LT, STANDARD, HQ }; public static Profile[] values() { return _values; } public static Profile valueOf(String name) { String nameU = name.toUpperCase(); for (ProresEncoder.Profile profile2 : _values) { if (name.equals(nameU)) return profile2; } return null; } final String name; final int[] qmatLuma; final int[] qmatChroma; final public String fourcc; // Per 1024 pixels final int bitrate; final int firstQp; final int lastQp; private Profile(String name, int[] qmatLuma, int[] qmatChroma, String fourcc, int bitrate, int firstQp, int lastQp) { this.name = name; this.qmatLuma = qmatLuma; this.qmatChroma = qmatChroma; this.fourcc = fourcc; this.bitrate = bitrate; this.firstQp = firstQp; this.lastQp = lastQp; } }; protected Profile profile; private int[][] scaledLuma; private int[][] scaledChroma; private boolean interlaced; public ProresEncoder(String profile, boolean interlaced) { this(profile == null ? ProresEncoder.Profile.HQ : ProresEncoder.Profile.valueOf(profile), interlaced); } public ProresEncoder(Profile profile, boolean interlaced) { this.profile = profile; scaledLuma = scaleQMat(profile.qmatLuma, 1, 16); scaledChroma = scaleQMat(profile.qmatChroma, 1, 16); this.interlaced = interlaced; } private int[][] scaleQMat(int[] qmatLuma, int start, int count) { int[][] result = new int[count][]; for (int i = 0; i < count; i++) { result[i] = new int[qmatLuma.length]; for (int j = 0; j < qmatLuma.length; j++) result[i][j] = qmatLuma[j] * (i + start); } return result; } public static final void writeCodeword(BitWriter writer, Codebook codebook, int val) { int firstExp = ((codebook.switchBits + 1) << codebook.riceOrder); if (val >= firstExp) { val -= firstExp; val += (1 << codebook.expOrder); // Offset to zero int exp = log2(val); int zeros = exp - codebook.expOrder + codebook.switchBits + 1; for (int i = 0; i < zeros; i++) writer.write1Bit(0); writer.write1Bit(1); writer.writeNBit(val, exp); } else if (codebook.riceOrder > 0) { for (int i = 0; i < (val >> codebook.riceOrder); i++) writer.write1Bit(0); writer.write1Bit(1); writer.writeNBit(val & ((1 << codebook.riceOrder) - 1), codebook.riceOrder); } else { for (int i = 0; i < val; i++) writer.write1Bit(0); writer.write1Bit(1); } } private static final int qScale(int[] qMat, int ind, int val) { return val / qMat[ind]; } private static final int toGolumb(int val) { return (val << 1) ^ (val >> 31); } private static final int toGolumbSign(int val, int sign) { if (val == 0) return 0; return (val << 1) + sign; } private static final int diffSign(int val, int sign) { return (val >> 31) ^ sign; } public static final int getLevel(int val) { int sign = (val >> 31); return (val ^ sign) - sign; } static final void writeDCCoeffs(BitWriter bits, int[] qMat, int[] _in, int blocksPerSlice) { int prevDc = qScale(qMat, 0, _in[0] - 16384); writeCodeword(bits, firstDCCodebook, toGolumb(prevDc)); int code = 5, sign = 0, idx = 64; for (int i = 1; i < blocksPerSlice; i++, idx += 64) { int newDc = qScale(qMat, 0, _in[idx] - 16384); int delta = newDc - prevDc; int newCode = toGolumbSign(getLevel(delta), diffSign(delta, sign)); writeCodeword(bits, dcCodebooks[min(code, 6)], newCode); code = newCode; sign = delta >> 31; prevDc = newDc; } } static final void writeACCoeffs(BitWriter bits, int[] qMat, int[] _in, int blocksPerSlice, int[] scan, int maxCoeff) { int prevRun = 4; int prevLevel = 2; int run = 0; for (int i = 1; i < maxCoeff; i++) { int indp = scan[i]; for (int j = 0; j < blocksPerSlice; j++) { int val = qScale(qMat, indp, _in[(j << 6) + indp]); if (val == 0) run++; else { writeCodeword(bits, runCodebooks[min(prevRun, 15)], run); prevRun = run; run = 0; int level = getLevel(val); writeCodeword(bits, levCodebooks[min(prevLevel, 9)], level - 1); prevLevel = level; bits.write1Bit(sign(val)); } } } } static final void encodeOnePlane(BitWriter bits, int blocksPerSlice, int[] qMat, int[] scan, int[] _in) { writeDCCoeffs(bits, qMat, _in, blocksPerSlice); writeACCoeffs(bits, qMat, _in, blocksPerSlice, scan, 64); } private void dctOnePlane(int blocksPerSlice, byte[] _in, int[] out) { for (int i = 0; i < blocksPerSlice; i++) { fdct10(_in, i << 6, out); } } protected int encodeSlice(ByteBuffer out, int[][] scaledLuma, int[][] scaledChroma, int[] scan, int sliceMbCount, int mbX, int mbY, Picture8Bit result, int prevQp, int mbWidth, int mbHeight, boolean unsafe, int vStep, int vOffset) { Picture8Bit striped = splitSlice(result, mbX, mbY, sliceMbCount, unsafe, vStep, vOffset); int[][] ac = new int[][] { new int[sliceMbCount << 8], new int[sliceMbCount << 7], new int[sliceMbCount << 7] }; dctOnePlane(sliceMbCount << 2, striped.getPlaneData(0), ac[0]); dctOnePlane(sliceMbCount << 1, striped.getPlaneData(1), ac[1]); dctOnePlane(sliceMbCount << 1, striped.getPlaneData(2), ac[2]); int est = (sliceMbCount >> 2) * profile.bitrate; int low = est - (est >> 3); // 12% bitrate fluctuation int high = est + (est >> 3); int qp = prevQp; out.put((byte) (6 << 3)); // hdr size ByteBuffer fork = out.duplicate(); NIOUtils.skip(out, 5); int rem = out.position(); int[] sizes = new int[3]; encodeSliceData(out, scaledLuma[qp - 1], scaledChroma[qp - 1], scan, sliceMbCount, ac, qp, sizes); if (bits(sizes) > high && qp < profile.lastQp) { do { ++qp; out.position(rem); encodeSliceData(out, scaledLuma[qp - 1], scaledChroma[qp - 1], scan, sliceMbCount, ac, qp, sizes); } while (bits(sizes) > high && qp < profile.lastQp); } else if (bits(sizes) < low && qp > profile.firstQp) { do { --qp; out.position(rem); encodeSliceData(out, scaledLuma[qp - 1], scaledChroma[qp - 1], scan, sliceMbCount, ac, qp, sizes); } while (bits(sizes) < low && qp > profile.firstQp); } fork.put((byte) qp); fork.putShort((short) sizes[0]); fork.putShort((short) sizes[1]); return qp; } static final int bits(int[] sizes) { return sizes[0] + sizes[1] + sizes[2] << 3; } protected static final void encodeSliceData(ByteBuffer out, int[] qmatLuma, int[] qmatChroma, int[] scan, int sliceMbCount, int[][] ac, int qp, int[] sizes) { sizes[0] = onePlane(out, sliceMbCount << 2, qmatLuma, scan, ac[0]); sizes[1] = onePlane(out, sliceMbCount << 1, qmatChroma, scan, ac[1]); sizes[2] = onePlane(out, sliceMbCount << 1, qmatChroma, scan, ac[2]); } static final int onePlane(ByteBuffer out, int blocksPerSlice, int[] qmatLuma, int[] scan, int[] data) { int rem = out.position(); BitWriter bits = new BitWriter(out); encodeOnePlane(bits, blocksPerSlice, qmatLuma, scan, data); bits.flush(); return out.position() - rem; } protected void encodePicture(ByteBuffer out, int[][] scaledLuma, int[][] scaledChroma, int[] scan, Picture8Bit picture, int vStep, int vOffset) { int mbWidth = (picture.getWidth() + 15) >> 4; int shift = 4 + vStep; int round = (1 << shift) - 1; int mbHeight = (picture.getHeight() + round) >> shift; int qp = profile.firstQp; int nSlices = calcNSlices(mbWidth, mbHeight); writePictureHeader(LOG_DEFAULT_SLICE_MB_WIDTH, nSlices, out); ByteBuffer fork = out.duplicate(); NIOUtils.skip(out, nSlices << 1); int i = 0; int[] total = new int[nSlices]; for (int mbY = 0; mbY < mbHeight; mbY++) { int mbX = 0; int sliceMbCount = DEFAULT_SLICE_MB_WIDTH; while (mbX < mbWidth) { while (mbWidth - mbX < sliceMbCount) sliceMbCount >>= 1; int sliceStart = out.position(); boolean unsafeBottom = (picture.getHeight() % 16) != 0 && mbY == mbHeight - 1; boolean unsafeRight = (picture.getWidth() % 16) != 0 && mbX + sliceMbCount == mbWidth; qp = encodeSlice(out, scaledLuma, scaledChroma, scan, sliceMbCount, mbX, mbY, picture, qp, mbWidth, mbHeight, unsafeBottom || unsafeRight, vStep, vOffset); fork.putShort((short) (out.position() - sliceStart)); total[i++] = (short) (out.position() - sliceStart); mbX += sliceMbCount; } } } public static void writePictureHeader(int logDefaultSliceMbWidth, int nSlices, ByteBuffer out) { int headerLen = 8; out.put((byte) (headerLen << 3)); out.putInt(0); out.putShort((short) nSlices); out.put((byte) (logDefaultSliceMbWidth << 4)); } private int calcNSlices(int mbWidth, int mbHeight) { int nSlices = mbWidth >> LOG_DEFAULT_SLICE_MB_WIDTH; for (int i = 0; i < LOG_DEFAULT_SLICE_MB_WIDTH; i++) { nSlices += (mbWidth >> i) & 0x1; } return nSlices * mbHeight; } private Picture8Bit splitSlice(Picture8Bit result, int mbX, int mbY, int sliceMbCount, boolean unsafe, int vStep, int vOffset) { Picture8Bit out = Picture8Bit.create(sliceMbCount << 4, 16, YUV422); if (unsafe) { int mbHeightPix = 16 << vStep; Picture8Bit filled = Picture8Bit.create(sliceMbCount << 4, mbHeightPix, YUV422); ImageOP.subImageWithFillPic8(result, filled, new Rect(mbX << 4, mbY << (4 + vStep), sliceMbCount << 4, mbHeightPix)); split(filled, out, 0, 0, sliceMbCount, vStep, vOffset); } else { split(result, out, mbX, mbY, sliceMbCount, vStep, vOffset); } return out; } private void split(Picture8Bit _in, Picture8Bit out, int mbX, int mbY, int sliceMbCount, int vStep, int vOffset) { doSplit(_in.getPlaneData(0), out.getPlaneData(0), _in.getPlaneWidth(0), mbX, mbY, sliceMbCount, 0, vStep, vOffset); doSplit(_in.getPlaneData(1), out.getPlaneData(1), _in.getPlaneWidth(1), mbX, mbY, sliceMbCount, 1, vStep, vOffset); doSplit(_in.getPlaneData(2), out.getPlaneData(2), _in.getPlaneWidth(2), mbX, mbY, sliceMbCount, 1, vStep, vOffset); } private void doSplit(byte[] _in, byte[] out, int stride, int mbX, int mbY, int sliceMbCount, int chroma, int vStep, int vOffset) { int outOff = 0; int off = (mbY << 4) * (stride << vStep) + (mbX << (4 - chroma)) + stride * vOffset; stride <<= vStep; for (int i = 0; i < sliceMbCount; i++) { splitBlock(_in, stride, off, out, outOff); splitBlock(_in, stride, off + (stride << 3), out, outOff + (128 >> chroma)); if (chroma == 0) { splitBlock(_in, stride, off + 8, out, outOff + 64); splitBlock(_in, stride, off + (stride << 3) + 8, out, outOff + 192); } outOff += (256 >> chroma); off += (16 >> chroma); } } private void splitBlock(byte[] y, int stride, int off, byte[] out, int outOff) { for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) out[outOff++] = y[off++]; off += stride - 8; } } @Override public EncodedFrame encodeFrame8Bit(Picture8Bit pic, ByteBuffer buffer) { ByteBuffer out = buffer.duplicate(); ByteBuffer fork = out.duplicate(); int[] scan = interlaced ? interlaced_scan : progressive_scan; writeFrameHeader(out, new ProresConsts.FrameHeader(0, pic.getCroppedWidth(), pic.getCroppedHeight(), interlaced ? 1 : 0, true, scan, profile.qmatLuma, profile.qmatChroma, 2)); encodePicture(out, scaledLuma, scaledChroma, scan, pic, interlaced ? 1 : 0, 0); if (interlaced) encodePicture(out, scaledLuma, scaledChroma, scan, pic, interlaced ? 1 : 0, 1); out.flip(); fork.putInt(out.remaining()); return new EncodedFrame(out, true); } public static void writeFrameHeader(ByteBuffer outp, ProresConsts.FrameHeader header) { short headerSize = 148; outp.putInt(headerSize + 8 + header.payloadSize); outp.put(new byte[] { 'i', 'c', 'p', 'f' }); outp.putShort(headerSize); // header size outp.putShort((short) 0); outp.put(new byte[] { 'a', 'p', 'l', '0' }); outp.putShort((short) header.width); outp.putShort((short) header.height); outp.put((byte) (header.frameType == 0 ? 0x83 : 0x87)); // {10}(422){00}[{00}(frame),{01}(field)}{11} outp.put(new byte[] { 0, 2, 2, 6, 32, 0 }); outp.put((byte) 3); // flags2 writeQMat(outp, header.qMatLuma); writeQMat(outp, header.qMatChroma); } static final void writeQMat(ByteBuffer out, int[] qmat) { for (int i = 0; i < 64; i++) out.put((byte) qmat[i]); } @Override public ColorSpace[] getSupportedColorSpaces() { return new ColorSpace[] { ColorSpace.YUV422 }; } @Override public int estimateBufferSize(Picture8Bit frame) { return (3 * frame.getWidth() * frame.getHeight()) / 2; } }