<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program 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.
*
* 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, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*
* This file turns a VP8 keyframe into a picture.
*/
namespace seekquarry\yioop\library\av_processing;
/**
* Vp8Tables the constant tables the VP8 standard publishes, gathered in one
* place so the decoding code reads as arithmetic rather than as data.
*/
final class Vp8Tables
{
/**
* DEFAULT_COEFF_PROBS is how likely each coded value is when a frame
* starts. A frame may change these afterwards, but a keyframe begins from
* them.
* @var mixed
*/
public const DEFAULT_COEFF_PROBS = [
[
[
[128,128,128,128,128,128,128,128,128,128,128],
[128,128,128,128,128,128,128,128,128,128,128],
[128,128,128,128,128,128,128,128,128,128,128]
],
[
[253,136,254,255,228,219,128,128,128,128,128],
[189,129,242,255,227,213,255,219,128,128,128],
[106,126,227,252,214,209,255,255,128,128,128]
],
[
[1,98,248,255,236,226,255,255,128,128,128],
[181,133,238,254,221,234,255,154,128,128,128],
[78,134,202,247,198,180,255,219,128,128,128]
],
[
[1,185,249,255,243,255,128,128,128,128,128],
[184,150,247,255,236,224,128,128,128,128,128],
[77,110,216,255,236,230,128,128,128,128,128]
],
[
[1,101,251,255,241,255,128,128,128,128,128],
[170,139,241,252,236,209,255,255,128,128,128],
[37,116,196,243,228,255,255,255,128,128,128]
],
[
[1,204,254,255,245,255,128,128,128,128,128],
[207,160,250,255,238,128,128,128,128,128,128],
[102,103,231,255,211,171,128,128,128,128,128]
],
[
[1,152,252,255,240,255,128,128,128,128,128],
[177,135,243,255,234,225,128,128,128,128,128],
[80,129,211,255,194,224,128,128,128,128,128]
],
[
[1,1,255,128,128,128,128,128,128,128,128],
[246,1,255,128,128,128,128,128,128,128,128],
[255,128,128,128,128,128,128,128,128,128,128]
]
],
[
[
[198,35,237,223,193,187,162,160,145,155,62],
[131,45,198,221,172,176,220,157,252,221,1],
[68,47,146,208,149,167,221,162,255,223,128]
],
[
[1,149,241,255,221,224,255,255,128,128,128],
[184,141,234,253,222,220,255,199,128,128,128],
[81,99,181,242,176,190,249,202,255,255,128]
],
[
[1,129,232,253,214,197,242,196,255,255,128],
[99,121,210,250,201,198,255,202,128,128,128],
[23,91,163,242,170,187,247,210,255,255,128]
],
[
[1,200,246,255,234,255,128,128,128,128,128],
[109,178,241,255,231,245,255,255,128,128,128],
[44,130,201,253,205,192,255,255,128,128,128]
],
[
[1,132,239,251,219,209,255,165,128,128,128],
[94,136,225,251,218,190,255,255,128,128,128],
[22,100,174,245,186,161,255,199,128,128,128]
],
[
[1,182,249,255,232,235,128,128,128,128,128],
[124,143,241,255,227,234,128,128,128,128,128],
[35,77,181,251,193,211,255,205,128,128,128]
],
[
[1,157,247,255,236,231,255,255,128,128,128],
[121,141,235,255,225,227,255,255,128,128,128],
[45,99,188,251,195,217,255,224,128,128,128]
],
[
[1,1,251,255,213,255,128,128,128,128,128],
[203,1,248,255,255,128,128,128,128,128,128],
[137,1,177,255,224,255,128,128,128,128,128]
]
],
[
[
[253,9,248,251,207,208,255,192,128,128,128],
[175,13,224,243,193,185,249,198,255,255,128],
[73,17,171,221,161,179,236,167,255,234,128]
],
[
[1,95,247,253,212,183,255,255,128,128,128],
[239,90,244,250,211,209,255,255,128,128,128],
[155,77,195,248,188,195,255,255,128,128,128]
],
[
[1,24,239,251,218,219,255,205,128,128,128],
[201,51,219,255,196,186,128,128,128,128,128],
[69,46,190,239,201,218,255,228,128,128,128]
],
[
[1,191,251,255,255,128,128,128,128,128,128],
[223,165,249,255,213,255,128,128,128,128,128],
[141,124,248,255,255,128,128,128,128,128,128]
],
[
[1,16,248,255,255,128,128,128,128,128,128],
[190,36,230,255,236,255,128,128,128,128,128],
[149,1,255,128,128,128,128,128,128,128,128]
],
[
[1,226,255,128,128,128,128,128,128,128,128],
[247,192,255,128,128,128,128,128,128,128,128],
[240,128,255,128,128,128,128,128,128,128,128]
],
[
[1,134,252,255,255,128,128,128,128,128,128],
[213,62,250,255,255,128,128,128,128,128,128],
[55,93,255,128,128,128,128,128,128,128,128]
],
[
[128,128,128,128,128,128,128,128,128,128,128],
[128,128,128,128,128,128,128,128,128,128,128],
[128,128,128,128,128,128,128,128,128,128,128]
]
],
[
[
[202,24,213,235,186,191,220,160,240,175,255],
[126,38,182,232,169,184,228,174,255,187,128],
[61,46,138,219,151,178,240,170,255,216,128]
],
[
[1,112,230,250,199,191,247,159,255,255,128],
[166,109,228,252,211,215,255,174,128,128,128],
[39,77,162,232,172,180,245,178,255,255,128]
],
[
[1,52,220,246,198,199,249,220,255,255,128],
[124,74,191,243,183,193,250,221,255,255,128],
[24,71,130,219,154,170,243,182,255,255,128]
],
[
[1,182,225,249,219,240,255,224,128,128,128],
[149,150,226,252,216,205,255,171,128,128,128],
[28,108,170,242,183,194,254,223,255,255,128]
],
[
[1,81,230,252,204,203,255,192,128,128,128],
[123,102,209,247,188,196,255,233,128,128,128],
[20,95,153,243,164,173,255,203,128,128,128]
],
[
[1,222,248,255,216,213,128,128,128,128,128],
[168,175,246,252,235,205,255,255,128,128,128],
[47,116,215,255,211,212,255,255,128,128,128]
],
[
[1,121,236,253,212,214,255,255,128,128,128],
[141,84,213,252,201,202,255,219,128,128,128],
[42,80,160,240,162,185,255,205,128,128,128]
],
[
[1,1,255,128,128,128,128,128,128,128,128],
[244,1,255,128,128,128,128,128,128,128,128],
[238,1,255,128,128,128,128,128,128,128,128]
]
]
];
/**
* COEFF_UPDATE_PROBS is how likely a frame is to change each of those, read
* before any change is read.
* @var mixed
*/
public const COEFF_UPDATE_PROBS = [
[
[
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[176,246,255,255,255,255,255,255,255,255,255],
[223,241,252,255,255,255,255,255,255,255,255],
[249,253,253,255,255,255,255,255,255,255,255]
],
[
[255,244,252,255,255,255,255,255,255,255,255],
[234,254,254,255,255,255,255,255,255,255,255],
[253,255,255,255,255,255,255,255,255,255,255]
],
[
[255,246,254,255,255,255,255,255,255,255,255],
[239,253,254,255,255,255,255,255,255,255,255],
[254,255,254,255,255,255,255,255,255,255,255]
],
[
[255,248,254,255,255,255,255,255,255,255,255],
[251,255,254,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,253,254,255,255,255,255,255,255,255,255],
[251,254,254,255,255,255,255,255,255,255,255],
[254,255,254,255,255,255,255,255,255,255,255]
],
[
[255,254,253,255,254,255,255,255,255,255,255],
[250,255,254,255,254,255,255,255,255,255,255],
[254,255,255,255,255,255,255,255,255,255,255]
],
[
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
]
],
[
[
[217,255,255,255,255,255,255,255,255,255,255],
[225,252,241,253,255,255,254,255,255,255,255],
[234,250,241,250,253,255,253,254,255,255,255]
],
[
[255,254,255,255,255,255,255,255,255,255,255],
[223,254,254,255,255,255,255,255,255,255,255],
[238,253,254,254,255,255,255,255,255,255,255]
],
[
[255,248,254,255,255,255,255,255,255,255,255],
[249,254,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,253,255,255,255,255,255,255,255,255,255],
[247,254,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,253,254,255,255,255,255,255,255,255,255],
[252,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,254,254,255,255,255,255,255,255,255,255],
[253,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,254,253,255,255,255,255,255,255,255,255],
[250,255,255,255,255,255,255,255,255,255,255],
[254,255,255,255,255,255,255,255,255,255,255]
],
[
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
]
],
[
[
[186,251,250,255,255,255,255,255,255,255,255],
[234,251,244,254,255,255,255,255,255,255,255],
[251,251,243,253,254,255,254,255,255,255,255]
],
[
[255,253,254,255,255,255,255,255,255,255,255],
[236,253,254,255,255,255,255,255,255,255,255],
[251,253,253,254,254,255,255,255,255,255,255]
],
[
[255,254,254,255,255,255,255,255,255,255,255],
[254,254,254,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,254,255,255,255,255,255,255,255,255,255],
[254,254,255,255,255,255,255,255,255,255,255],
[254,255,255,255,255,255,255,255,255,255,255]
],
[
[255,255,255,255,255,255,255,255,255,255,255],
[254,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
]
],
[
[
[248,255,255,255,255,255,255,255,255,255,255],
[250,254,252,254,255,255,255,255,255,255,255],
[248,254,249,253,255,255,255,255,255,255,255]
],
[
[255,253,253,255,255,255,255,255,255,255,255],
[246,253,253,255,255,255,255,255,255,255,255],
[252,254,251,254,254,255,255,255,255,255,255]
],
[
[255,254,252,255,255,255,255,255,255,255,255],
[248,254,253,255,255,255,255,255,255,255,255],
[253,255,254,254,255,255,255,255,255,255,255]
],
[
[255,251,254,255,255,255,255,255,255,255,255],
[245,251,254,255,255,255,255,255,255,255,255],
[253,253,254,255,255,255,255,255,255,255,255]
],
[
[255,251,253,255,255,255,255,255,255,255,255],
[252,253,254,255,255,255,255,255,255,255,255],
[255,254,255,255,255,255,255,255,255,255,255]
],
[
[255,252,255,255,255,255,255,255,255,255,255],
[249,255,254,255,255,255,255,255,255,255,255],
[255,255,254,255,255,255,255,255,255,255,255]
],
[
[255,255,253,255,255,255,255,255,255,255,255],
[250,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
],
[
[255,255,255,255,255,255,255,255,255,255,255],
[254,255,255,255,255,255,255,255,255,255,255],
[255,255,255,255,255,255,255,255,255,255,255]
]
]
];
/**
* BMODE_PROBS is how likely each way of guessing a four by four block is,
* given how its neighbors above and to the left were guessed.
* @var mixed
*/
public const BMODE_PROBS = [
[
[231,120,48,89,115,113,120,152,112],
[152,179,64,126,170,118,46,70,95], [175,69,143,80,85,82,72,155,103],
[56,58,10,171,218,189,17,13,152], [144,71,10,38,171,213,144,34,26],
[114,26,17,163,44,195,21,10,173], [121,24,80,195,26,62,44,64,85],
[170,46,55,19,136,160,33,206,71], [63,20,8,114,114,208,12,9,226],
[81,40,11,96,182,84,29,16,36]
],
[
[134,183,89,137,98,101,106,165,148],
[72,187,100,130,157,111,32,75,80], [66,102,167,99,74,62,40,234,128],
[41,53,9,178,241,141,26,8,107], [104,79,12,27,217,255,87,17,7],
[74,43,26,146,73,166,49,23,157], [65,38,105,160,51,52,31,115,128],
[87,68,71,44,114,51,15,186,23], [47,41,14,110,182,183,21,17,194],
[66,45,25,102,197,189,23,18,22]
],
[
[88,88,147,150,42,46,45,196,205], [43,97,183,117,85,38,35,179,61],
[39,53,200,87,26,21,43,232,171], [56,34,51,104,114,102,29,93,77],
[107,54,32,26,51,1,81,43,31], [39,28,85,171,58,165,90,98,64],
[34,22,116,206,23,34,43,166,73], [68,25,106,22,64,171,36,225,114],
[34,19,21,102,132,188,16,76,124], [62,18,78,95,85,57,50,48,51]
],
[
[193,101,35,159,215,111,89,46,111],
[60,148,31,172,219,228,21,18,111],
[112,113,77,85,179,255,38,120,114], [40,42,1,196,245,209,10,25,109],
[100,80,8,43,154,1,51,26,71], [88,43,29,140,166,213,37,43,154],
[61,63,30,155,67,45,68,1,209], [142,78,78,16,255,128,34,197,171],
[41,40,5,102,211,183,4,1,221], [51,50,17,168,209,192,23,25,82]
],
[
[125,98,42,88,104,85,117,175,82], [95,84,53,89,128,100,113,101,45],
[75,79,123,47,51,128,81,171,1], [57,17,5,71,102,57,53,41,49],
[115,21,2,10,102,255,166,23,6], [38,33,13,121,57,73,26,1,85],
[41,10,67,138,77,110,90,47,114], [101,29,16,10,85,128,101,196,26],
[57,18,10,102,102,213,34,20,43], [117,20,15,36,163,128,68,1,26]
],
[
[138,31,36,171,27,166,38,44,229], [67,87,58,169,82,115,26,59,179],
[63,59,90,180,59,166,93,73,154], [40,40,21,116,143,209,34,39,175],
[57,46,22,24,128,1,54,17,37], [47,15,16,183,34,223,49,45,183],
[46,17,33,183,6,98,15,32,183], [65,32,73,115,28,128,23,128,205],
[40,3,9,115,51,192,18,6,223], [87,37,9,115,59,77,64,21,47]
],
[
[104,55,44,218,9,54,53,130,226], [64,90,70,205,40,41,23,26,57],
[54,57,112,184,5,41,38,166,213], [30,34,26,133,152,116,10,32,134],
[75,32,12,51,192,255,160,43,51], [39,19,53,221,26,114,32,73,255],
[31,9,65,234,2,15,1,118,73], [88,31,35,67,102,85,55,186,85],
[56,21,23,111,59,205,45,37,192], [55,38,70,124,73,102,1,34,98]
],
[
[102,61,71,37,34,53,31,243,192], [69,60,71,38,73,119,28,222,37],
[68,45,128,34,1,47,11,245,171], [62,17,19,70,146,85,55,62,70],
[75,15,9,9,64,255,184,119,16], [37,43,37,154,100,163,85,160,1],
[63,9,92,136,28,64,32,201,85], [86,6,28,5,64,255,25,248,1],
[56,8,17,132,137,255,55,116,128], [58,15,20,82,135,57,26,121,40]
],
[
[164,50,31,137,154,133,25,35,218], [51,103,44,131,131,123,31,6,158],
[86,40,64,135,148,224,45,183,128], [22,26,17,131,240,154,14,1,209],
[83,12,13,54,192,255,68,47,28], [45,16,21,91,64,222,7,1,197],
[56,21,39,155,60,138,23,102,213], [85,26,85,85,128,128,32,146,171],
[18,11,7,63,144,171,4,4,246], [35,27,10,146,174,171,12,26,128]
],
[
[190,80,35,99,180,80,126,54,45], [85,126,47,87,176,51,41,20,32],
[101,75,128,139,118,146,116,128,85], [56,41,15,176,236,85,37,9,62],
[146,36,19,30,171,255,97,27,20], [71,30,17,119,118,255,17,18,138],
[101,38,60,138,55,70,43,26,142], [138,45,61,62,219,1,81,188,64],
[32,41,20,117,151,142,20,21,163], [112,19,12,61,195,128,48,4,24]
]
];
/**
* DC_QLOOKUP is the step the first value of a block is scaled by, one entry
* for each quantizer the format allows.
* @var mixed
*/
public const DC_QLOOKUP
= [
4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14, 15, 16, 17, 17, 18, 19,
20, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 46,
47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 93, 95, 96, 98, 100,
101, 102, 104, 106, 108, 110, 112, 114, 116, 118, 122, 124, 126,
128,
130,132,134,136,138,140,143,145,148,151,154,157];
/**
* AC_QLOOKUP is the step the rest of a block's values are scaled by, one
* entry per quantizer.
* @var mixed
*/
public const AC_QLOOKUP
= [
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55,
56, 57, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86,
88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116,
119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 155,
158, 161, 164, 167, 170, 173, 177, 181, 185, 189, 193, 197, 201,
205, 209, 213, 217, 221, 225, 229, 234, 239, 245, 249, 254, 259,
264, 269, 274, 279,
284];
/**
* COEFF_BAND is which group of probabilities each place in a block reads
* with. Places near the start of a block behave differently from those near
* the end, so they are grouped.
* @var mixed
*/
public const COEFF_BAND = [0,1,2,3,6,4,5,6,6,6,6,6,6,6,6,7];
}
/**
* Vp8BoolDecoder vP8 boolean entropy decoder (a numbered internet standard 6386
* section 7). A binary arithmetic coder: each call splits the current range
* according to a probability and returns which side of the split the value
* falls on.
*/
final class Vp8BoolDecoder
{
/**
* $bytes stores the part of the frame being read. VP8 splits a frame's
* values across parts that can be read side by side.
* @var string
*/
private string $bytes;
/**
* $position stores how far into those bytes the reading has gone.
* @var int
*/
private int $position;
/**
* $end stores where this part stops. Reading past it gives zeros.
* @var int
*/
private int $end;
/**
* $range stores how wide the range of values still in play is. Each bit
* read narrows it by how likely that bit was.
* @var int
*/
private int $range = 255;
/**
* $range_position stores where in that range the number being read sits.
* @var int
*/
private int $range_position = 0;
/**
* $bit_count stores how many bits of the current byte are still unread, so
* the reader knows when to take in another.
* @var int
*/
private int $bit_count = 0;
/**
* __construct sets up a reader over one arithmetic coded part of a frame.
*
* @param string $bytes the bytes to read
* @param int $offset where in the file to start reading
* @param int $length how many bytes to read
*/
public function __construct(string $bytes, int $offset, int $length)
{
$this->bytes = $bytes;
$this->position = $offset;
$this->end = $offset + $length;
if ($length < 2 || $this->end > strlen($bytes)) {
throw new VideoException('VP8 partition is truncated');
}
$this->range_position = (ord($bytes[$this->position]) << 8) |
ord($bytes[$this->position + 1]);
$this->position += 2;
}
/**
* readOneBit reads one decision, given how likely a zero is.
*
* @param int $prob the probability a value is read with
* @return int what was read
*/
public function readOneBit(int $prob): int
{
$split = 1 + ((($this->range - 1) * $prob) >> 8);
$big_split = $split << 8;
if ($this->range_position >= $big_split) {
$result = 1;
$this->range -= $split;
$this->range_position -= $big_split;
} else {
$result = 0;
$this->range = $split;
}
while ($this->range < 128) {
$this->range_position <<= 1;
$this->range <<= 1;
if (++$this->bit_count === 8) {
$this->bit_count = 0;
$this->range_position |= ($this->position < $this->end)
? ord($this->bytes[$this->position]) : 0;
$this->position++;
}
}
return $result;
}
/**
* literal reads a plain number of the given width, its highest bit first.
* VP8 writes most of its numbers through the probabilities, but a few
* fields are written straight, and this reads those.
*
* @param int $bits the reader the stream's bits are taken from
* @return int what was read
*/
public function literal(int $bits): int
{
$variant = 0;
for ($i = 0; $i < $bits; $i++) {
$variant = ($variant << 1) | $this->readOneBit(128);
}
return $variant;
}
/**
* readSignedValue reads such a plain number and then a bit saying whether
* it is negative, which is how VP8 writes the nudges it applies to a
* quantizer or to the smoothing.
*
* @param int $bits the reader the stream's bits are taken from
* @return int what was read
*/
public function readSignedValue(int $bits): int
{
$variant = $this->literal($bits);
return $this->readOneBit(128) ? -$variant : $variant;
}
/**
* readFlag reads one plain bit, where either value is equally likely.
*
* @return int what was read
*/
public function readFlag(): int
{
return $this->readOneBit(128);
}
/**
* walkTree walk a token tree. Nodes are pairs; a negative entry is the leaf
* value encoded as -(value + 1). read from
*
* @param array $tree the tree of choices a value is
* @param array $probs the probabilities values are read with
* @param int $node where in the tree the reading is
* @return int what was read
*/
public function walkTree(array $tree, array $probs, int $node = 0): int
{
$guard = 0;
while (true) {
$next = $tree[$node][$this->readOneBit($probs[$node])];
if ($next < 0) {
return -$next - 1;
}
$node = $next;
if (++$guard > 32) {
throw new VideoException('malformed VP8 token tree');
}
}
}
}
/**
* Vp8Decoder vP8 keyframe decoder. Keyframes need no reference frames, so
* motion vectors, inter modes and reference buffers are all absent: every
* macroblock is predicted from its own already-reconstructed neighbors. What
* remains is the frame header, the intra mode tree, the coefficient tokens
* spread across one to eight partitions, two inverse transforms, ten sub-block
* predictors and the deblocking filter.
*/
final class Vp8Decoder
{
/**
* smooth3 average of three neighboring samples, weighted toward the middle
* one. This is how the format smooths an edge it predicts from.
*
* @param int $first sample before the middle one
* @param int $middle sample the result sits on
* @param int $last sample after the middle one
* @return int the smoothed value
*/
private static function smooth3($first, $middle, $last)
{
return ($first + 2 * $middle + $last + 2) >> 2;
}
/**
* smooth2 average of two neighboring samples, rounded upward.
*
* @param int $first one sample
* @param int $second the sample beside it
* @return int the average
*/
private static function smooth2($first, $second)
{
return ($first + $second + 1) >> 1;
}
/**
* DC_PRED is guessing a whole block as one value, the average of its
* neighbors.
* @var mixed
*/
private const DC_PRED = 0;
/**
* V_PRED is guessing a block from the row of samples above it.
* @var mixed
*/
private const V_PRED = 1;
/**
* H_PRED is guessing a block from the column of samples to its left.
* @var mixed
*/
private const H_PRED = 2;
/**
* TM_PRED is guessing a block from the row above and the column to the left
* together, following the slope between them.
* @var mixed
*/
private const TM_PRED = 3;
/**
* B_PRED is guessing a macroblock in sixteen smaller squares, each with a
* way of its own.
* @var mixed
*/
private const B_PRED = 4;
/**
* B_DC is sub-block modes, in specification order.
* @var mixed
*/
private const B_DC = 0;
/**
* B_TM is guessing a four by four block from the row above and the column
* to the left together, following the slope between them.
* @var mixed
*/
private const B_TM = 1;
/**
* B_VE is guessing a block straight down from the row above it.
* @var mixed
*/
private const B_VE = 2;
/**
* B_HE is guessing a block straight across from the column to its left.
* @var mixed
*/
private const B_HE = 3;
/**
* B_LD is guessing a block along a slope running down and to the left.
* @var mixed
*/
private const B_LD = 4;
/**
* B_RD is guessing a block along a slope running down and to the right.
* @var mixed
*/
private const B_RD = 5;
/**
* B_VR marks guessing a block along a steep slope that leans to
* the right.
* @var mixed
*/
private const B_VR = 6;
/**
* B_VL is guessing a block along a steep slope leaning left.
* @var mixed
*/
private const B_VL = 7;
/**
* B_HD is guessing a block along a shallow slope leaning down.
* @var mixed
*/
private const B_HD = 8;
/**
* B_HU is guessing a block along a shallow slope leaning up.
* @var mixed
*/
private const B_HU = 9;
/**
* YMODE_TREE is the tree of choices that says how a whole
* macroblock of brightness was guessed, for a frame that stands on
* its own. A leaf of the tree holds one less than the way it
* stands for, made negative, which is how a leaf is told from a
* branch.
*/
private const YMODE_TREE = [
[-(self::B_PRED + 1), 1],
[2, 3],
[-(self::DC_PRED + 1), -(self::V_PRED + 1)],
[-(self::H_PRED + 1), -(self::TM_PRED + 1)],
];
/**
* YMODE_PROBS is how likely each way of guessing a whole macroblock of
* brightness is.
* @var mixed
*/
private const YMODE_PROBS = [145, 156, 163, 128];
/**
* UVMODE_TREE is the tree of choices that says how the color planes of a
* macroblock were guessed.
* @var mixed
*/
private const UVMODE_TREE = [
[-(self::DC_PRED + 1), 1],
[-(self::V_PRED + 1), 2],
[-(self::H_PRED + 1), -(self::TM_PRED + 1)],
];
/**
* UVMODE_PROBS is how likely each of those choices is.
* @var mixed
*/
private const UVMODE_PROBS = [142, 114, 183];
/**
* BMODE_TREE is the tree of choices that says how one four by four block
* was guessed.
* @var mixed
*/
private const BMODE_TREE = [
[-(self::B_DC + 1), 1],
[-(self::B_TM + 1), 2],
[-(self::B_VE + 1), 3],
[4, 6],
[-(self::B_HE + 1), 5],
[-(self::B_RD + 1), -(self::B_VR + 1)],
[-(self::B_LD + 1), 7],
[-(self::B_VL + 1), 8],
[-(self::B_HD + 1), -(self::B_HU + 1)],
];
/**
* SEGMENT_TREE is the tree of choices that says which segment a macroblock
* belongs to.
* @var mixed
*/
private const SEGMENT_TREE = [
[1, 2],
[-(0 + 1), -(1 + 1)],
[-(2 + 1), -(3 + 1)],
];
/**
* YMODE_TO_BMODE is the sixteen by sixteen mode a whole macroblock
* contributes to its neighbors' context.
*/
private const YMODE_TO_BMODE = [
self::DC_PRED => self::B_DC,
self::V_PRED => self::B_VE,
self::H_PRED => self::B_HE,
self::TM_PRED => self::B_TM,
];
/**
* COEFF_TREE is the tree of choices that reads one coded value of a block,
* from whether there is a value at all up to how large it is.
* @var mixed
*/
private const COEFF_TREE = [
/* leaf -1 marks end of block */
[-1, 1],
/* literal 0 */
[-(0 + 2), 2],
/* literal 1 */
[-(1 + 2), 3],
[4, 6],
[-(2 + 2), 5],
[-(3 + 2), -(4 + 2)],
[7, 8],
/* categories 1 and 2 */
[-(11 + 2), -(12 + 2)],
[9, 10],
/* categories 3 and 4 */
[-(13 + 2), -(14 + 2)],
/* categories 5 and 6 */
[-(15 + 2), -(16 + 2)],
];
/**
* CAT_PROBS is extra-bit probabilities and base values for the six
* magnitude categories.
*/
private const CAT_PROBS = [
[159], [165, 145], [173, 148, 140], [176, 155, 140, 135],
[180, 157, 141, 134, 130],
[254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129],
];
/**
* CAT_BASE is the smallest value each of the six large-value groups stands
* for; the bits that follow say how much larger.
* @var mixed
*/
private const CAT_BASE = [5, 7, 11, 19, 35, 67];
/**
* $frame_width stores how wide the frame is, in pixels, as its header says.
* @var int
*/
private int $frame_width = 0;
/**
* $frame_height stores how tall the frame is, in pixels.
* @var int
*/
private int $frame_height = 0;
/**
* $macroblock_width stores how many macroblocks the frame is across. A
* macroblock
* is the sixteen by sixteen square VP8 works in.
* @var int
*/
private int $macroblock_width = 0;
/**
* $macroblock_height stores how many macroblocks the frame is down.
* @var int
*/
private int $macroblock_height = 0;
/**
* $segment_enabled stores whether the frame is split into segments that
* carry their own quantizer and smoothing.
* @var bool
*/
private bool $segment_enabled = false;
/**
* $segment_absolute stores whether a segment's settings replace the frame's
* or are added to them.
* @var bool
*/
private bool $segment_absolute = false;
/**
* $segment_quant stores the quantizer each segment uses.
* @var array
*/
private array $segment_quant = [0, 0, 0, 0];
/**
* $segment_filter stores how strongly each segment's edges are smoothed.
* @var array
*/
private array $segment_filter = [0, 0, 0, 0];
/**
* $segment_probs stores how likely each segment is, used while reading
* which segment a macroblock belongs to.
* @var array
*/
private array $segment_probs = [255, 255, 255];
/**
* $update_segment_map stores whether this frame says afresh which
* macroblock belongs to which segment.
* @var bool
*/
private bool $update_segment_map = false;
/**
* $filter_simple stores whether the simpler of VP8's two edge smoothings is
* used.
* @var bool
*/
private bool $filter_simple = false;
/**
* $filter_level stores how strongly block edges are smoothed. Zero leaves
* them alone.
* @var int
*/
private int $filter_level = 0;
/**
* $sharpness stores how much that smoothing is held back at sharp edges.
* @var int
*/
private int $sharpness = 0;
/**
* $smoothing_delta_enabled stores whether the smoothing is nudged by how a
* macroblock was guessed.
* @var bool
*/
private bool $smoothing_delta_enabled = false;
/**
* $reference_smoothing_deltas stores those nudges, by which frame a
* macroblock
* leans on.
* @var array
*/
private array $reference_smoothing_deltas = [0, 0, 0, 0];
/**
* $mode_smoothing_deltas stores those nudges, by how a macroblock was
* guessed.
* @var array
*/
private array $mode_smoothing_deltas = [0, 0, 0, 0];
/**
* $value_probs stores how likely each value is, read from the frame's
* header and used for every block after it.
* @var array
*/
private array $value_probs = [];
/**
* $skip_enabled stores whether the frame says which macroblocks carry no
* values at all.
* @var bool
*/
private bool $skip_enabled = false;
/**
* $skip_prob stores how likely a macroblock is to be skipped that way.
* @var int
*/
private int $skip_prob = 0;
/**
* $quantizer_steps stores the steps each segment's values are
* scaled by: one for the first brightness value, one for the
* rest, two more for the second brightness plane, and two for the
* color planes.
* @var array
*/
private array $quant = [];
/**
* $planes stores the brightness and color values of the picture being
* built.
* @var array
*/
private array $planes = [];
/**
* $strides stores how many values one row of each plane takes.
* @var array
*/
private array $strides = [];
/**
* $offsets stores where the picture proper starts inside each plane, since
* the decoder works in whole macroblocks.
* @var array
*/
private array $offsets = [];
/**
* $macroblock_y_mode stores how the brightness of each macroblock was
* guessed. A
* neighbor's guess is needed while reading the next.
* @var array
*/
private array $macroblock_y_mode = [];
/**
* $macroblock_segment stores which segment each macroblock belongs to.
* @var array
*/
private array $macroblock_segment = [];
/**
* $macroblock_skip stores whether each macroblock carried any values.
* @var array
*/
private array $macroblock_skip = [];
/**
* $macroblock_has_values stores whether each macroblock's values were
* read, which
* the smoothing looks at to decide whether an edge needs work.
* @var array
*/
private array $macroblock_has_values = [];
/**
* $block_modes stores how each of the sixteen smaller squares of a
* macroblock
* was guessed, where the macroblock was split.
* @var array
*/
private array $block_modes = [];
/**
* decodeKeyframe decode a VP8 keyframe.
*
* @return VideoPicture what was read
* @param string $frame the frame being built
*/
public function decodeKeyframe(string $frame): VideoPicture
{
if (strlen($frame) < 10) {
throw new VideoException('VP8 frame is too short');
}
$tag = ord($frame[0]) | (ord($frame[1]) << 8) | (ord($frame[2]) << 16);
if (($tag & 1) !== 0) {
throw new VideoException('only VP8 keyframes can be decoded');
}
$first_part_size = ($tag >> 5) & 0x7FFFF;
if (substr($frame, 3, 3) !== "\x9d\x01\x2a") {
throw new VideoException('missing VP8 keyframe start code');
}
$wide = (ord($frame[6]) | (ord($frame[7]) << 8)) & 0x3FFF;
$header = (ord($frame[8]) | (ord($frame[9]) << 8)) & 0x3FFF;
if ($wide < 1 || $header < 1 || $wide > 16383 || $header > 16383) {
throw new VideoException('bad VP8 frame size');
}
$this->frame_width = $wide;
$this->frame_height = $header;
$this->macroblock_width = intdiv($wide + 15, 16);
$this->macroblock_height = intdiv($header + 15, 16);
$header_start = 10;
if ($header_start + $first_part_size > strlen($frame)) {
throw new VideoException('VP8 first partition runs past the frame');
}
$chunk = new Vp8BoolDecoder($frame, $header_start, $first_part_size);
$this->parseHeader($chunk);
$partitions = $this->splitPartitions($frame, $header_start
+ $first_part_size, $chunk);
$this->allocatePlanes();
$this->decodeMacroblocks($chunk, $partitions);
$this->loopFilter();
return $this->toPicture();
}
/* header */
/**
* parseHeader reads the frame header: how the picture is split into
* segments, how strongly it is filtered, how it is quantized, and the
* probabilities its coefficients are coded with. from
*
* @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
*/
private function parseHeader(Vp8BoolDecoder $chunk): void
{
/* color space */
$chunk->readFlag();
/* clamping type */
$chunk->readFlag();
$this->segment_enabled = $chunk->readFlag() === 1;
$this->update_segment_map = false;
$this->segment_probs = [255, 255, 255];
if ($this->segment_enabled) {
$this->parseSegmentation($chunk);
} else {
$this->segment_quant = [0, 0, 0, 0];
$this->segment_filter = [0, 0, 0, 0];
}
$this->filter_simple = $chunk->readFlag() === 1;
$this->filter_level = $chunk->literal(6);
$this->sharpness = $chunk->literal(3);
$this->smoothing_delta_enabled = $chunk->readFlag() === 1;
if ($this->smoothing_delta_enabled && $chunk->readFlag() === 1) {
for ($i = 0; $i < 4; $i++) {
if ($chunk->readFlag() === 1) {
$this->reference_smoothing_deltas[$i] = $chunk
->readSignedValue(6);
}
}
for ($i = 0; $i < 4; $i++) {
if ($chunk->readFlag() === 1) {
$this->mode_smoothing_deltas[$i] = $chunk
->readSignedValue(6);
}
}
}
$this->partition_count = 1 << $chunk->literal(2);
$this->parseQuantisers($chunk);
/* refresh entropy probabilities */
$chunk->readFlag();
$this->value_probs = Vp8Tables::DEFAULT_COEFF_PROBS;
for ($i = 0; $i < 4; $i++) {
for ($j = 0; $j < 8; $j++) {
for ($k = 0; $k < 3; $k++) {
for ($span
= 0; $span < 11; $span++) {
$chance
= Vp8Tables::COEFF_UPDATE_PROBS[$i][$j][$k][$span];
if ($chunk->readOneBit($chance) === 1) {
$this->value_probs[$i][$j][$k][$span]
= $chunk->literal(8);
}
}
}
}
}
$this->skip_enabled = $chunk->readFlag() === 1;
$this->skip_prob = $this->skip_enabled ? $chunk->literal(8) : 0;
}
/**
* $partition_count stores how many parts the frame's values are split
* across, one, two, four or eight.
* @var int
*/
private int $partition_count = 1;
/**
* parseSegmentation reads how the picture is split into segments and what
* quantizer and filter strength each one carries. from
*
* @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
*/
private function parseSegmentation(Vp8BoolDecoder $chunk): void
{
$this->update_segment_map = $chunk->readFlag() === 1;
$update_data = $chunk->readFlag() === 1;
if ($update_data) {
$this->segment_absolute = $chunk->readFlag() === 1;
for ($i = 0; $i < 4; $i++) {
$this->segment_quant[$i] = $chunk->readFlag() === 1
? $chunk->readSignedValue(7) : 0;
}
for ($i = 0; $i < 4; $i++) {
$this->segment_filter[$i] = $chunk->readFlag() === 1
? $chunk->readSignedValue(6) : 0;
}
}
if ($this->update_segment_map) {
for ($i = 0; $i < 3; $i++) {
$this->segment_probs[$i] = $chunk->readFlag() === 1
? $chunk->literal(8)
: 255;
}
}
}
/**
* parseQuantisers works out the numbers each coefficient is multiplied by,
* for every segment and for each kind of block. from
*
* @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
*/
private function parseQuantisers(Vp8BoolDecoder $chunk): void
{
$base = $chunk->literal(7);
$deltas = [];
foreach (['ydc', 'y2dc', 'y2ac', 'uvdc', 'uvac'] as $k) {
$deltas[$k] = $chunk->readFlag() === 1 ? $chunk
->readSignedValue(4) : 0;
}
$this->quant = [];
for ($seg = 0; $seg < 4; $seg++) {
$quality = $base;
if ($this->segment_enabled) {
$quality = $this->segment_absolute
? $this->segment_quant[$seg]
: $base + $this->segment_quant[$seg];
}
$quality = max(0, min(127, $quality));
$clamp = static fn(int $variant): int => max(0, min(127, $variant));
$brightness_first_value =
Vp8Tables::DC_QLOOKUP[$clamp($quality + $deltas['ydc'])];
$brightness_other_values = Vp8Tables::AC_QLOOKUP[$quality];
$second_brightness_first_value
= Vp8Tables::DC_QLOOKUP[$clamp($quality + $deltas['y2dc'])] * 2;
$second_brightness_other_values =
(int) (Vp8Tables::AC_QLOOKUP[$clamp($quality
+ $deltas['y2ac'])] * 155 / 100);
if ($second_brightness_other_values < 8) {
$second_brightness_other_values = 8;
}
$color_first_value = Vp8Tables::DC_QLOOKUP[$clamp($quality +
$deltas['uvdc'])];
if ($color_first_value > 132) {
$color_first_value = 132;
}
$color_other_values = Vp8Tables::AC_QLOOKUP[$clamp($quality +
$deltas['uvac'])];
$this->quant[$seg] = [$brightness_first_value,
$brightness_other_values, $second_brightness_first_value,
$second_brightness_other_values, $color_first_value,
$color_other_values];
}
}
/**
* splitPartitions from
*
* @return Vp8BoolDecoder[] what was read
* @param string $frame the frame being built
* @param int $offset how far into the file to read from
* @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
*/
private function splitPartitions(string $frame, int $offset,
Vp8BoolDecoder $chunk): array
{
$total = $this->partition_count;
$sizes_length = 3 * ($total - 1);
$data_start = $offset + $sizes_length;
if ($data_start > strlen($frame)) {
throw new VideoException('VP8 partition sizes run past the frame');
}
$parts = [];
$position = $data_start;
for ($i = 0; $i < $total; $i++) {
if ($i < $total - 1) {
$position = $offset + 3 * $i;
$size
= ord($frame[$position]) | (ord($frame[$position
+ 1]) << 8) | (ord($frame[$position + 2]) << 16);
} else {
$size = strlen($frame) - $position;
}
if ($size < 0 || $position + $size > strlen($frame)) {
throw new VideoException('VP8 partition runs past the frame');
}
$parts[] = new Vp8BoolDecoder($frame, $position, $size);
$position += $size;
}
return $parts;
}
/* planes */
/**
* allocatePlanes sets up the sample planes with a border, so prediction at
* the picture's edge reads the values the format says it should.
*/
private function allocatePlanes(): void
{
$line_width = $this->macroblock_width * 16;
$left_half = $this->macroblock_height * 16;
$code_word = $this->macroblock_width * 8;
$channel = $this->macroblock_height * 8;
/* one row of border above and eight columns either side, so the top */
/* row reads 127, the left column reads 129, and the four above-right */
/* samples past the right edge always exist */
foreach ([[0, $line_width, $left_half], [1, $code_word, $channel],
[2, $code_word,
$channel]] as [$plane_at, $wide,
$header]) {
$stride = $wide + 16;
$this->strides[$plane_at] = $stride;
$this->offsets[$plane_at] = $stride + 8;
$plane = array_fill(0, $stride * ($header + 1), 129);
for ($across = 0; $across < $stride; $across++) {
/* the row above the frame */
$plane[$across] = 127;
}
$this->planes[$plane_at] = $plane;
}
}
/**
* sampleAt works out where one sample sits in a plane. for color
*
* @param int $plane_at which plane, zero for brightness and one or two
* @param int $across how far across the block
* @param int $down how far down the block
* @return int what was read
*/
private function sampleAt(int $plane_at, int $across, int $down): int
{
return $this->offsets[$plane_at] + $down *
$this->strides[$plane_at] + $across;
}
/** replicate the right-hand edge of a luma row band so above-right always
exists */
/**
* extendRight repeats the rightmost sample of a row of macroblocks past the
* picture, so prediction that reaches up and to the right always has
* something to read.
*
* @param int $macroblock_y which macroblock down
*/
private function extendRight(int $macroblock_y): void
{
$stride = $this->strides[0];
$wide = $this->macroblock_width * 16;
for ($down =
$macroblock_y * 16; $down < $macroblock_y * 16 + 16; $down++) {
$last = $this->planes[0][$this->sampleAt(0, $wide - 1, $down)];
$base = $this->sampleAt(0, $wide, $down);
for ($k = 0; $k < 8; $k++) {
$this->planes[0][$base + $k] = $last;
}
}
}
/* macroblock loop */
/**
* decodeMacroblocks decodes every macroblock: its modes, its coefficients,
* and the samples that come out of them. from
*
* @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
* @param array $partitions the parts the frame's values are split across
*/
private function decodeMacroblocks(
Vp8BoolDecoder $chunk, array $partitions): void
{
$macroblocks_across = $this->macroblock_width;
$macroblocks_down = $this->macroblock_height;
$this->block_modes = array_fill(0, ($macroblocks_across * 4) *
($macroblocks_down * 4), self::B_DC);
$this->macroblock_y_mode = array_fill(0, $macroblocks_across *
$macroblocks_down, self::DC_PRED);
$this->macroblock_segment = array_fill(0, $macroblocks_across *
$macroblocks_down, 0);
$this->macroblock_skip = array_fill(0, $macroblocks_across *
$macroblocks_down, 0);
$this->macroblock_has_values = array_fill(0, $macroblocks_across *
$macroblocks_down, 0);
/* non-zero-coefficient contexts: four luma columns, two each for the */
/* chroma planes, one for the second-order block */
$top_value_count = array_fill(0, $macroblocks_across, array_fill(0,
9, 0));
$left_value_count = array_fill(0, 9, 0);
for ($macroblock_y = 0; $macroblock_y <
$macroblocks_down; $macroblock_y++) {
$left_value_count = array_fill(0, 9, 0);
$part = $partitions[$macroblock_y % count($partitions)];
for ($macroblock_x = 0; $macroblock_x <
$macroblocks_across; $macroblock_x++) {
$position = $macroblock_y * $macroblocks_across + $macroblock_x;
$segment = 0;
if ($this->segment_enabled && $this->update_segment_map) {
$segment
= $chunk->walkTree(self::SEGMENT_TREE,
$this->segment_probs);
}
$this->macroblock_segment[$position] = $segment;
$skip = $this->skip_enabled ? $chunk->readOneBit($this
->skip_prob) : 0;
$this->macroblock_skip[$position] = $skip;
$y_mode = $chunk->walkTree(self::YMODE_TREE, self::YMODE_PROBS);
$this->macroblock_y_mode[$position] = $y_mode;
$this->readSubModes($chunk, $macroblock_x, $macroblock_y,
$y_mode);
$color_mode = $chunk->walkTree(self::UVMODE_TREE,
self::UVMODE_PROBS);
$values = null;
$has_values = false;
if ($skip === 0) {
[$values, $has_values] = $this->decodeCoefficients(
$part, $macroblock_x, $y_mode, $segment,
$top_value_count,
$left_value_count
);
} else {
for ($i = 0; $i < 8; $i++) {
$top_value_count[$macroblock_x][$i] = 0;
$left_value_count[$i] = 0;
}
if ($y_mode !== self::B_PRED) {
$top_value_count[$macroblock_x][8] = 0;
$left_value_count[8] = 0;
}
}
$this->macroblock_has_values[$position] = $has_values ? 1 : 0;
$this->reconstruct($macroblock_x, $macroblock_y, $y_mode,
$color_mode, $values);
}
$this->extendRight($macroblock_y);
}
}
/**
* readSubModes reads the prediction modes of a macroblock, one for the
* whole block or one for each four sample square. from
*
* @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
* @param int $macroblock_x which macroblock across
* @param int $macroblock_y which macroblock down
* @param int $y_mode which way the brightness block is guessed
*/
private function readSubModes(Vp8BoolDecoder $chunk, int $macroblock_x,
int $macroblock_y,
int $y_mode): void
{
$stride = $this->macroblock_width * 4;
$block_start_x = $macroblock_x * 4;
$block_start_y = $macroblock_y * 4;
if ($y_mode !== self::B_PRED) {
$matches = self::YMODE_TO_BMODE[$y_mode];
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$this->block_modes[($block_start_y + $down) * $stride +
$block_start_x + $across]
= $matches;
}
}
return;
}
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
/* outside the frame the context is the flat predictor */
$above = ($block_start_y + $down === 0)
? self::B_DC
: $this->block_modes[($block_start_y + $down - 1) * $stride
+ $block_start_x + $across];
$left = ($block_start_x + $across === 0)
? self::B_DC
: $this->block_modes[($block_start_y + $down) * $stride
+ $block_start_x + $across - 1];
$mode = $chunk->walkTree(
self::BMODE_TREE, Vp8Tables::BMODE_PROBS[$above][$left]);
$this->block_modes[($block_start_y + $down) * $stride +
$block_start_x + $across]
= $mode;
}
}
}
/* coefficients */
/**
* decodeCoefficients coefficient] from carried
*
* @return array [dequantized blocks, any non-zero
* @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
* @param int $macroblock_x which macroblock across
* @param int $y_mode which way the brightness block is guessed
* @param int $segment which segment of the frame the block belongs to
* @param array $top_value_count how many values the blocks above carried
* @param array $left_value_count how many values the blocks to the left
*/
private function decodeCoefficients(
Vp8BoolDecoder $chunk, int $macroblock_x, int $y_mode, int $segment,
array &$top_value_count, array &$left_value_count
): array {
[$brightness_first_value, $brightness_other_values,
$second_brightness_first_value, $second_brightness_other_values,
$color_first_value, $color_other_values] = $this
->quant[$segment];
$has_second_brightness = $y_mode !== self::B_PRED;
$blocks = [];
$any = false;
if ($has_second_brightness) {
$context =
$top_value_count[$macroblock_x][8] + $left_value_count[8];
$second_brightness = array_fill(0, 16, 0);
$total = $this->decodeBlock($chunk, $second_brightness, 1,
$context, 0, $second_brightness_first_value,
$second_brightness_other_values);
$top_value_count[$macroblock_x][8] = $left_value_count[8] =
$total > 0 ? 1 : 0;
if ($total > 0) {
$any = true;
}
$blocks['y2'] = self::inverseWalsh($second_brightness);
}
$type = $has_second_brightness ? 0 : 3;
$first = $has_second_brightness ? 1 : 0;
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$context = $top_value_count[$macroblock_x][$across] +
$left_value_count[$down];
$block_at = array_fill(0, 16, 0);
$total = $this->decodeBlock(
$chunk, $block_at, $type, $context, $first,
$brightness_first_value, $brightness_other_values);
$top_value_count[$macroblock_x][$across] =
$left_value_count[$down] = $total > 0 ?
1 : 0;
if ($total > 0) {
$any = true;
}
$blocks['y'][$down * 4 + $across] = $block_at;
}
}
foreach ([1, 2] as $plane_at) {
for ($down = 0; $down < 2; $down++) {
for ($across = 0; $across < 2; $across++) {
$ti = 4 + ($plane_at - 1) * 2 + $across;
$li = 4 + ($plane_at - 1) * 2 + $down;
$context = $top_value_count[$macroblock_x][$ti] +
$left_value_count[$li];
$block_at = array_fill(0, 16, 0);
$total = $this->decodeBlock(
$chunk, $block_at, 2, $context, 0,
$color_first_value, $color_other_values);
$top_value_count[$macroblock_x][$ti] =
$left_value_count[$li] =
$total > 0 ? 1 : 0;
if ($total > 0) {
$any = true;
}
$blocks['uv'][$plane_at][$down * 2 + $across] = $block_at;
}
}
}
return [$blocks, $any];
}
/**
* decodeBlock one block of coefficient tokens, dequantized into $written in
* raster order. from
*
* @return int 0 when the block ends immediately, otherwise where it ended
* @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
* @param array $written filled in with what was read
* @param int $type which kind
* @param int $context which set of probabilities to read with
* @param int $first the first one
* @param int $first_step the step the first value is scaled by
* @param int $other_steps the step the rest are scaled by
*/
private function decodeBlock(
Vp8BoolDecoder $chunk, array
&$written, int $type, int $context, int $first, int $first_step,
int $other_steps
): int {
$probs_for_type = $this->value_probs[$type];
$block_order = H264Scan::ZZ4;
$i = $first;
$skip_end_of_block = false;
$last_non_zero = 0;
while ($i < 16) {
$probs = $probs_for_type[Vp8Tables::COEFF_BAND[$i]][$context];
if (!$skip_end_of_block && $chunk->readOneBit($probs[0]) === 0) {
break;
}
$skip_end_of_block = false;
if ($chunk->readOneBit($probs[1]) === 0) {
/* a zero coefficient: the next token cannot be an end of block
*/
$context = 0;
$skip_end_of_block = true;
$i++;
continue;
}
if ($chunk->readOneBit($probs[2]) === 0) {
$value = 1;
$context = 1;
} else {
$context = 2;
if ($chunk->readOneBit($probs[3]) === 0) {
$value = $chunk->readOneBit($probs[4]) === 0 ? 2 : 3
+ $chunk->readOneBit($probs[5]);
} else {
if ($chunk->readOneBit($probs[6]) === 0) {
$block_kind = $chunk
->readOneBit($probs[7]) === 0 ? 0 : 1;
} elseif ($chunk->readOneBit($probs[8]) === 0) {
$block_kind = $chunk
->readOneBit($probs[9]) === 0 ? 2 : 3;
} else {
$block_kind = $chunk
->readOneBit($probs[10]) === 0 ? 4 : 5;
}
$value = self::CAT_BASE[$block_kind];
$extra = 0;
foreach (self::CAT_PROBS[$block_kind] as $position) {
$extra = ($extra << 1) | $chunk->readOneBit($position);
}
$value += $extra;
}
}
if ($chunk->readOneBit(128) === 1) {
$value = -$value;
}
$written[$block_order[$i]] = $value * ($i === 0
? $first_step : $other_steps);
$last_non_zero = $i + 1;
$i++;
}
return $i > $first ? max($last_non_zero, $i) : 0;
}
/**
* COS_MINUS1 is transforms.
* @var mixed
*/
private const COS_MINUS1 = 20091;
/**
* SIN is the sines the transform multiplies by, held as whole numbers so
* the arithmetic stays exact.
* @var mixed
*/
private const SIN = 35468;
/**
* inverseDct the four by four inverse transform of a numbered internet
* standard 6386
*
* @param array $self_contained whether the block stands on its own
* @return array what was read
*/
private static function inverseDct(array $self_contained): array
{
$held = array_fill(0, 16, 0);
for ($i = 0; $i < 4; $i++) {
$left_one = $self_contained[$i] + $self_contained[$i + 8];
$bit_one = $self_contained[$i] - $self_contained[$i + 8];
$held_one = ($self_contained[$i + 4] * self::SIN) >> 16;
$held_two = $self_contained[$i + 12] + (($self_contained[$i + 12] *
self::COS_MINUS1) >> 16);
$corner_one = $held_one - $held_two;
$held_one = $self_contained[$i + 4] + (($self_contained[$i + 4] *
self::COS_MINUS1) >> 16);
$held_two = ($self_contained[$i + 12] * self::SIN) >> 16;
$difference_one = $held_one + $held_two;
$held[$i] = $left_one + $difference_one;
$held[$i + 12] = $left_one - $difference_one;
$held[$i + 4] = $bit_one + $corner_one;
$held[$i + 8] = $bit_one - $corner_one;
}
$written = array_fill(0, 16, 0);
for ($i = 0; $i < 4; $i++) {
$origin = $i * 4;
$left_one = $held[$origin] + $held[$origin + 2];
$bit_one = $held[$origin] - $held[$origin + 2];
$held_one = ($held[$origin + 1] * self::SIN) >> 16;
$held_two = $held[$origin + 3]
+ (($held[$origin + 3] * self::COS_MINUS1) >> 16);
$corner_one = $held_one - $held_two;
$held_one = $held[$origin + 1]
+ (($held[$origin + 1] * self::COS_MINUS1) >> 16);
$held_two = ($held[$origin + 3] * self::SIN) >> 16;
$difference_one = $held_one + $held_two;
$written[$origin] = ($left_one + $difference_one + 4) >> 3;
$written[$origin + 3] = ($left_one - $difference_one + 4) >> 3;
$written[$origin + 1] = ($bit_one + $corner_one + 4) >> 3;
$written[$origin + 2] = ($bit_one - $corner_one + 4) >> 3;
}
return $written;
}
/**
* inverseWalsh the second-order Walsh-a transform of sums and differences
* alone transform applied to the luma DC block @param array $self_contained
* whether the block stands on its own @return array what was read.
*/
private static function inverseWalsh(array $self_contained): array
{
$held = array_fill(0, 16, 0);
for ($i = 0; $i < 4; $i++) {
$left_one = $self_contained[$i] + $self_contained[$i + 12];
$bit_one = $self_contained[$i + 4] + $self_contained[$i + 8];
$corner_one = $self_contained[$i + 4] - $self_contained[$i + 8];
$difference_one = $self_contained[$i] - $self_contained[$i + 12];
$held[$i] = $left_one + $bit_one;
$held[$i + 4] = $corner_one + $difference_one;
$held[$i + 8] = $left_one - $bit_one;
$held[$i + 12] = $difference_one - $corner_one;
}
$written = array_fill(0, 16, 0);
for ($i = 0; $i < 4; $i++) {
$origin = $i * 4;
$left_one = $held[$origin] + $held[$origin + 3];
$bit_one = $held[$origin + 1] + $held[$origin + 2];
$corner_one = $held[$origin + 1] - $held[$origin + 2];
$difference_one = $held[$origin] - $held[$origin + 3];
$written[$origin] = ($left_one + $bit_one + 3) >> 3;
$written[$origin + 1] = ($corner_one + $difference_one + 3) >> 3;
$written[$origin + 2] = ($left_one - $bit_one + 3) >> 3;
$written[$origin + 3] = ($difference_one - $corner_one + 3) >> 3;
}
return $written;
}
/**
* holdInsideByte holds a sample inside the range a byte can carry.
*
* @param int $variant which form of the format is in use
* @return int what was read
*/
private static function holdInsideByte(int $variant): int
{
return $variant < 0 ? 0 : ($variant > 255 ? 255 : $variant);
}
/* reconstruction */
/**
* reconstruct turns the prediction and the coefficients of a macroblock
* into samples in the planes.
*
* @param int $macroblock_x which macroblock across
* @param int $macroblock_y which macroblock down
* @param int $y_mode which way the brightness block is guessed
* @param int $color_mode which way the color blocks are guessed
* @param array $values the values the block was coded as
*/
private function reconstruct(int $macroblock_x, int $macroblock_y,
int $y_mode,
int $color_mode,
?array $values): void
{
if ($y_mode === self::B_PRED) {
$this->reconstructB($macroblock_x, $macroblock_y, $values);
} else {
$this->predictBlock(
0, $macroblock_x * 16, $macroblock_y * 16, 16, $y_mode,
$macroblock_x > 0, $macroblock_y > 0);
if ($values !== null) {
$first_value = $values['y2'];
for ($byte = 0; $byte < 16; $byte++) {
$block_at = $values['y'][$byte];
$block_at[0] = $first_value[$byte];
$this->addResidual(0, $macroblock_x * 16
+ ($byte & 3) * 4, $macroblock_y * 16
+ ($byte >> 2) * 4, $block_at);
}
}
}
foreach ([1, 2] as $plane_at) {
$this->predictBlock(
$plane_at, $macroblock_x * 8, $macroblock_y * 8, 8,
$color_mode, $macroblock_x > 0,
$macroblock_y > 0);
if ($values !== null) {
for ($byte = 0; $byte < 4; $byte++) {
$this->addResidual(
$plane_at,
$macroblock_x * 8 + ($byte & 1) * 4,
$macroblock_y * 8 + ($byte >> 1) * 4,
$values['uv'][$plane_at][$byte]
);
}
}
}
}
/**
* addResidual transforms one block of coefficients and adds the result to
* the prediction already in the plane. for color
*
* @param int $plane_at which plane, zero for brightness and one or two
* @param int $block_x how far across the frame the block starts
* @param int $block_y how far down the frame the block starts
* @param array $block the block's values
*/
private function addResidual(int $plane_at, int $block_x, int $block_y,
array $block): void
{
foreach ($block as $variant) {
if ($variant !== 0) {
$run = self::inverseDct($block);
$stride = $this->strides[$plane_at];
for ($down = 0; $down < 4; $down++) {
$row = $this->sampleAt($plane_at, $block_x,
$block_y + $down);
for ($across = 0; $across < 4; $across++) {
$this->planes[$plane_at][$row + $across] =
self::holdInsideByte($this
->planes[$plane_at][$row + $across]
+ $run[$down * 4 + $across]);
}
}
return;
}
}
}
/**
* predictBlock whole-block prediction for sixteen by sixteen luma and eight
* by eight chroma for color
*
* @param int $plane_at which plane, zero for brightness and one or two
* @param int $block_x how far across the frame the block starts
* @param int $block_y how far down the frame the block starts
* @param int $size how many bytes
* @param int $mode which way the block is guessed from its neighbors
* @param bool $has_left whether the block to the left is there to read
* @param bool $has_top whether the block above is there to read
*/
private function predictBlock(int $plane_at, int $block_x, int $block_y,
int $size,
int $mode, bool $has_left, bool $has_top): void
{
$stride = $this->strides[$plane_at];
$plane = &$this->planes[$plane_at];
$above = [];
$left = [];
for ($i = 0; $i < $size; $i++) {
$above[$i] = $plane[$this->sampleAt($plane_at, $block_x + $i,
$block_y - 1)];
$left[$i] = $plane[$this->sampleAt($plane_at, $block_x - 1,
$block_y +
$i)];
}
$corner = $plane[$this->sampleAt($plane_at, $block_x - 1,
$block_y - 1)];
switch ($mode) {
case self::DC_PRED:
if ($has_top && $has_left) {
$first_value = (array_sum($above) + array_sum($left) +
$size)
>> (int) (log($size, 2) + 1);
} elseif ($has_top) {
$first_value = (array_sum($above) + $size / 2)
>> (int) log($size, 2);
} elseif ($has_left) {
$first_value = (array_sum($left) + $size /
2) >> (int) log($size, 2);
} else {
$first_value = 128;
}
$first_value = (int) $first_value;
for ($down = 0; $down < $size; $down++) {
$row = $this->sampleAt($plane_at, $block_x,
$block_y + $down);
for ($across = 0; $across < $size; $across++) {
$plane[$row + $across] = $first_value;
}
}
break;
case self::V_PRED:
for ($down = 0; $down < $size; $down++) {
$row = $this->sampleAt($plane_at, $block_x,
$block_y + $down);
for ($across = 0; $across < $size; $across++) {
$plane[$row + $across] = $above[$across];
}
}
break;
case self::H_PRED:
for ($down = 0; $down < $size; $down++) {
$row = $this->sampleAt($plane_at, $block_x,
$block_y + $down);
for ($across = 0; $across < $size; $across++) {
$plane[$row + $across] = $left[$down];
}
}
break;
case self::TM_PRED:
for ($down = 0; $down < $size; $down++) {
$row = $this->sampleAt($plane_at, $block_x,
$block_y + $down);
for ($across = 0; $across < $size; $across++) {
$plane[$row + $across]
= self::holdInsideByte(
$left[$down] + $above[$across] - $corner);
}
}
break;
default:
throw new VideoException("bad VP8 prediction mode $mode");
}
unset($plane);
}
/**
* reconstructB rebuilds a macroblock predicted in four sample squares, each
* with its own mode.
*
* @param int $macroblock_x which macroblock across
* @param int $macroblock_y which macroblock down
* @param array $values the values the block was coded as
*/
private function reconstructB(int $macroblock_x, int $macroblock_y,
?array $values): void
{
$stride = $this->strides[0];
$block_stride = $this->macroblock_width * 4;
$wide = $this->macroblock_width * 16;
/* the four samples above and to the right of the macroblock, reused by
*/
/* every sub-block in the right-hand column whatever its row */
$top_right = [];
for ($k = 0; $k < 4; $k++) {
$top_right[$k] = ($macroblock_x === $this
->macroblock_width - 1 && $macroblock_y > 0)
? $this->planes[0][$this->sampleAt(0, $wide - 1,
$macroblock_y * 16 - 1)]
: $this->planes[0][$this->sampleAt(0,
$macroblock_x * 16 + 16 + $k,
$macroblock_y * 16
- 1)];
}
for ($block_y = 0; $block_y < 4; $block_y++) {
for ($block_x = 0; $block_x < 4; $block_x++) {
$sample_x = $macroblock_x * 16 + $block_x * 4;
$sample_y = $macroblock_y * 16 + $block_y * 4;
$mode
= $this
->block_modes[($macroblock_y * 4 + $block_y) *
$block_stride + $macroblock_x * 4
+ $block_x];
$above_run = [];
for ($k = 0; $k < 4; $k++) {
$above_run[$k]
= $this->planes[0][$this->sampleAt(0, $sample_x + $k,
$sample_y - 1)];
}
if ($block_x === 3) {
for ($k = 0; $k < 4; $k++) {
$above_run[4 + $k] = $top_right[$k];
}
} else {
for ($k = 0; $k < 4; $k++) {
$above_run[4 + $k]
= $this->planes[0][
$this->sampleAt(0, $sample_x + 4 + $k,
$sample_y - 1)];
}
}
$left_run = [];
for ($k = 0; $k < 4; $k++) {
$left_run[$k]
= $this->planes[0][$this->sampleAt(0, $sample_x - 1,
$sample_y + $k)];
}
$prior = $this->planes[0][$this->sampleAt(0, $sample_x - 1,
$sample_y - 1)];
$guess = self::predictSubBlock(
$mode, $above_run, $left_run, $prior);
for ($down = 0; $down < 4; $down++) {
$row = $this->sampleAt(0, $sample_x, $sample_y + $down);
for ($across = 0; $across < 4; $across++) {
$this->planes[0][$row + $across]
= $guess[$down * 4 + $across];
}
}
if ($values !== null) {
$this->addResidual(0, $sample_x, $sample_y,
$values['y'][$block_y * 4
+ $block_x]);
}
}
}
}
/**
* predictSubBlock the ten sub-block predictors of a numbered internet
* standard 6386. $above_run holds the four samples above plus four above-
* right, $left_run the four to the left, $prior the corner.
*
* @param int $mode which way the block is guessed from its neighbors
* @param array $above_run how many the row above skipped
* @param array $left_run how many the column to the left skipped
* @param int $prior what was read before
* @return array what was read
*/
private static function predictSubBlock(int $mode, array $above_run,
array $left_run, int $prior): array
{
$below = array_fill(0, 16, 0);
$set
= static function (int $run, int $chunk,
int $variant) use (&$below): void {
$below[$run * 4 + $chunk] = $variant;
};
$average_of_two = static fn(int $amount, int $byte): int
=> ($amount + $byte + 1) >> 1;
$average_of_three = static fn(int $amount, int $byte, int $chunk): int
=> ($amount + 2 * $byte + $chunk + 2) >> 2;
/* E indexes the left column, corner and above row as one run */
$east
= [$left_run[3], $left_run[2], $left_run[1], $left_run[0], $prior,
$above_run[0], $above_run[1], $above_run[2], $above_run[3]];
switch ($mode) {
case self::B_DC:
$sum = $above_run[0] + $above_run[1] + $above_run[2]
+ $above_run[3] + $left_run[0] + $left_run[1] + $left_run[2]
+ $left_run[3] + 4;
$below = array_fill(0, 16, $sum >> 3);
break;
case self::B_TM:
for ($run = 0; $run < 4; $run++) {
for ($chunk = 0; $chunk < 4; $chunk++) {
$set($run, $chunk, self::holdInsideByte($left_run[$run]
+ $above_run[$chunk] - $prior));
}
}
break;
case self::B_VE:
$variant = [
$average_of_three($prior, $above_run[0], $above_run[1]),
$average_of_three($above_run[0], $above_run[1],
$above_run[2]),
$average_of_three($above_run[1], $above_run[2],
$above_run[3]),
$average_of_three($above_run[2], $above_run[3],
$above_run[4]),
];
for ($run = 0; $run < 4; $run++) {
for ($chunk = 0; $chunk < 4; $chunk++) {
$set($run, $chunk, $variant[$chunk]);
}
}
break;
case self::B_HE:
$variant = [
$average_of_three($prior, $left_run[0], $left_run[1]),
$average_of_three($left_run[0], $left_run[1], $left_run[2]),
$average_of_three($left_run[1], $left_run[2], $left_run[3]),
$average_of_three($left_run[2], $left_run[3], $left_run[3]),
];
for ($run = 0; $run < 4; $run++) {
for ($chunk = 0; $chunk < 4; $chunk++) {
$set($run, $chunk, $variant[$run]);
}
}
break;
case self::B_LD:
for ($run = 0; $run < 4; $run++) {
for ($chunk = 0; $chunk < 4; $chunk++) {
$i = $run + $chunk;
$set($run, $chunk, $i < 6
? $average_of_three(
$above_run[$i], $above_run[$i + 1],
$above_run[$i + 2])
: $average_of_three($above_run[6], $above_run[7],
$above_run[7]));
}
}
break;
case self::B_RD:
for ($run = 0; $run < 4; $run++) {
for ($chunk = 0; $chunk < 4; $chunk++) {
$i = $chunk - $run + 3;
$set($run, $chunk, $average_of_three($east[$i], $east[$i
+ 1], $east[$i + 2]));
}
}
break;
case self::B_VR:
$set(3, 0, $average_of_three($east[1], $east[2], $east[3]));
$set(2, 0, $average_of_three($east[2], $east[3], $east[4]));
$set(3, 1, $average_of_three($east[3], $east[4], $east[5]));
$set(1, 0, $average_of_three($east[3], $east[4], $east[5]));
$set(2, 1, $average_of_two($east[4], $east[5]));
$set(0, 0, $average_of_two($east[4], $east[5]));
$set(3, 2, $average_of_three($east[4], $east[5], $east[6]));
$set(1, 1, $average_of_three($east[4], $east[5], $east[6]));
$set(2, 2, $average_of_two($east[5], $east[6]));
$set(0, 1, $average_of_two($east[5], $east[6]));
$set(3, 3, $average_of_three($east[5], $east[6], $east[7]));
$set(1, 2, $average_of_three($east[5], $east[6], $east[7]));
$set(2, 3, $average_of_two($east[6], $east[7]));
$set(0, 2, $average_of_two($east[6], $east[7]));
$set(1, 3, $average_of_three($east[6], $east[7], $east[8]));
$set(0, 3, $average_of_two($east[7], $east[8]));
break;
case self::B_VL:
$set(0, 0, $average_of_two($above_run[0], $above_run[1]));
$set(1, 0, $average_of_three($above_run[0], $above_run[1],
$above_run[2]));
$set(2, 0, $average_of_two($above_run[1], $above_run[2]));
$set(0, 1, $average_of_two($above_run[1], $above_run[2]));
$set(1, 1, $average_of_three($above_run[1], $above_run[2],
$above_run[3]));
$set(3, 0, $average_of_three($above_run[1], $above_run[2],
$above_run[3]));
$set(2, 1, $average_of_two($above_run[2], $above_run[3]));
$set(0, 2, $average_of_two($above_run[2], $above_run[3]));
$set(3, 1, $average_of_three($above_run[2], $above_run[3],
$above_run[4]));
$set(1, 2, $average_of_three($above_run[2], $above_run[3],
$above_run[4]));
$set(2, 2, $average_of_two($above_run[3], $above_run[4]));
$set(0, 3, $average_of_two($above_run[3], $above_run[4]));
$set(3, 2, $average_of_three($above_run[3], $above_run[4],
$above_run[5]));
$set(1, 3, $average_of_three($above_run[3], $above_run[4],
$above_run[5]));
$set(2, 3, $average_of_three($above_run[4], $above_run[5],
$above_run[6]));
$set(3, 3, $average_of_three($above_run[5], $above_run[6],
$above_run[7]));
break;
case self::B_HD:
$set(3, 0, $average_of_two($east[0], $east[1]));
$set(3, 1, $average_of_three($east[0], $east[1], $east[2]));
$set(2, 0, $average_of_two($east[1], $east[2]));
$set(3, 2, $average_of_two($east[1], $east[2]));
$set(2, 1, $average_of_three($east[1], $east[2], $east[3]));
$set(3, 3, $average_of_three($east[1], $east[2], $east[3]));
$set(2, 2, $average_of_two($east[2], $east[3]));
$set(1, 0, $average_of_two($east[2], $east[3]));
$set(2, 3, $average_of_three($east[2], $east[3], $east[4]));
$set(1, 1, $average_of_three($east[2], $east[3], $east[4]));
$set(1, 2, $average_of_two($east[3], $east[4]));
$set(0, 0, $average_of_two($east[3], $east[4]));
$set(1, 3, $average_of_three($east[3], $east[4], $east[5]));
$set(0, 1, $average_of_three($east[3], $east[4], $east[5]));
$set(0, 2, $average_of_three($east[4], $east[5], $east[6]));
$set(0, 3, $average_of_three($east[5], $east[6], $east[7]));
break;
case self::B_HU:
$set(0, 0, $average_of_two($left_run[0], $left_run[1]));
$set(0, 1, $average_of_three($left_run[0], $left_run[1],
$left_run[2]));
$set(0, 2, $average_of_two($left_run[1], $left_run[2]));
$set(1, 0, $average_of_two($left_run[1], $left_run[2]));
$set(0, 3, $average_of_three($left_run[1], $left_run[2],
$left_run[3]));
$set(1, 1, $average_of_three($left_run[1], $left_run[2],
$left_run[3]));
$set(1, 2, $average_of_two($left_run[2], $left_run[3]));
$set(2, 0, $average_of_two($left_run[2], $left_run[3]));
$set(1, 3, $average_of_three($left_run[2], $left_run[3],
$left_run[3]));
$set(2, 1, $average_of_three($left_run[2], $left_run[3],
$left_run[3]));
foreach ([[2, 2], [2, 3], [3, 0], [3, 1], [3, 2],
[3, 3]] as [$run, $checksum]) {
$set($run, $checksum, $left_run[3]);
}
break;
default:
throw new VideoException("bad VP8 sub-block mode $mode");
}
return $below;
}
/* loop filter */
/**
* loopFilter smooths the sample values either side of every block edge, so
* that quantization does not leave a visible seam.
*/
private function loopFilter(): void
{
if ($this->filter_level === 0) {
return;
}
for ($macroblock_y = 0; $macroblock_y < $this
->macroblock_height; $macroblock_y++) {
for ($macroblock_x = 0; $macroblock_x < $this
->macroblock_width; $macroblock_x++) {
$position = $macroblock_y * $this
->macroblock_width + $macroblock_x;
$level = $this->filterLevelFor($position);
if ($level === 0) {
continue;
}
$interior = $level;
if ($this->sharpness > 0) {
$interior >>= ($this->sharpness > 4) ? 2 : 1;
if ($interior > 9 - $this->sharpness) {
$interior = 9 - $this->sharpness;
}
}
if ($interior < 1) {
$interior = 1;
}
$macroblock_limit = 2 * ($level + 2) + $interior;
$b_limit = 2 * $level + $interior;
$sharp_edge = $level >= 40 ? 2 : ($level >= 15 ? 1 : 0);
$inner = $this->macroblock_has_values[$position] === 1
|| $this->macroblock_y_mode[$position] === self::B_PRED;
$this->filterMacroblock(
$macroblock_x, $macroblock_y, $macroblock_limit,
$b_limit, $interior,
$sharp_edge, $inner);
}
}
}
/**
* filterLevelFor works out how strongly one macroblock should be filtered,
* given the frame's setting and any adjustment its segment or mode carries.
*
* @param int $position where the block sits in the frame
* @return int what was read
*/
private function filterLevelFor(int $position): int
{
$level = $this->filter_level;
if ($this->segment_enabled) {
$seg = $this->macroblock_segment[$position];
$level = $this->segment_absolute
? $this->segment_filter[$seg]
: $level + $this->segment_filter[$seg];
$level = max(0, min(63, $level));
}
if ($this->smoothing_delta_enabled) {
/* intra frame */
$level += $this->reference_smoothing_deltas[0];
if ($this->macroblock_y_mode[$position] === self::B_PRED) {
$level += $this->mode_smoothing_deltas[0];
}
$level = max(0, min(63, $level));
}
return $level;
}
/**
* filterMacroblock filters the edges of one macroblock, the outer ones more
* widely than the ones inside it.
*
* @param int $macroblock_x which macroblock across
* @param int $macroblock_y which macroblock down
* @param int $macroblock_limit how far a value at a macroblock edge may be
* moved
* @param int $b_limit how far a value inside a macroblock may be moved
* @param int $interior how far an inside value may be moved
* @param int $sharp_edge whether the edge is too sharp to smooth
* @param bool $inner whether the inside edges are smoothed too
*/
private function filterMacroblock(
int $macroblock_x, int $macroblock_y, int $macroblock_limit,
int $b_limit, int $interior,
int $sharp_edge,
bool $inner
): void {
/* the simple filter touches luma only */
$planes = $this->filter_simple
? [[0, 16, 16]]
: [[0, 16, 16], [1, 8, 8], [2, 8, 8]];
foreach ($planes as [$plane_at, $size, $length]) {
$stride = $this->strides[$plane_at];
$block_x = $macroblock_x * $size;
$block_y = $macroblock_y * $size;
$step = ($plane_at === 0) ? 4 : 4;
$inners = ($plane_at === 0) ? [4, 8, 12] : [4];
if ($macroblock_x > 0) {
for ($k = 0; $k < $length; $k++) {
$this->filterEdge($plane_at, $this->sampleAt($plane_at,
$block_x, $block_y
+ $k), 1, $macroblock_limit, $interior, $sharp_edge,
true);
}
}
if ($inner) {
foreach ($inners as $step_across) {
for ($k = 0; $k < $length; $k++) {
$this->filterEdge($plane_at, $this->sampleAt($plane_at,
$block_x + $step_across, $block_y
+ $k), 1, $b_limit, $interior, $sharp_edge, false);
}
}
}
if ($macroblock_y > 0) {
for ($k = 0; $k < $length; $k++) {
$this->filterEdge($plane_at, $this->sampleAt($plane_at,
$block_x
+ $k, $block_y), $stride, $macroblock_limit, $interior,
$sharp_edge, true);
}
}
if ($inner) {
foreach ($inners as $step_down) {
for ($k = 0; $k < $length; $k++) {
$this->filterEdge($plane_at, $this->sampleAt($plane_at,
$block_x + $k, $block_y
+ $step_down), $stride, $b_limit, $interior,
$sharp_edge, false);
}
}
}
}
}
/**
* holdInsideSignedByte holds a value inside the range a signed byte can
* carry.
*
* @param int $variant which form of the format is in use
* @return int what was read
*/
private static function holdInsideSignedByte(int $variant): int
{
return $variant < -128 ? -128 : ($variant > 127 ? 127 : $variant);
}
/**
* filterEdge filters one line of samples crossing a block edge. for color
* above
*
* @param int $plane_at which plane, zero for brightness and one or two
* @param int $base the value the rest are measured from
* @param int $step how far to move each time
* @param int $limit the most that may be read
* @param int $interior how far an inside value may be moved
* @param int $sharp_edge_threshold the sharpness an edge is left alone
* @param bool $macroblock_edge whether this is the edge between macroblocks
*/
private function filterEdge(
int $plane_at, int $base, int $step, int $limit, int $interior,
int $sharp_edge_threshold, bool $macroblock_edge
): void {
$plane = &$this->planes[$plane_at];
$before_edge_three = $plane[$base - 4 * $step];
$before_edge_two = $plane[$base - 3 * $step];
$before_edge_one = $plane[$base - 2 * $step];
$before_edge = $plane[$base - $step];
$after_edge = $plane[$base];
$after_edge_one = $plane[$base + $step];
$after_edge_two = $plane[$base + 2 * $step];
$after_edge_three = $plane[$base + 3 * $step];
if ($this->filter_simple) {
if ((abs($before_edge - $after_edge) * 2 +
intdiv(abs($before_edge_one - $after_edge_one),
2)) <= $limit) {
self::commonAdjust($plane, $base, $step, true);
}
unset($plane);
return;
}
$ok = (abs($before_edge - $after_edge) * 2 +
intdiv(abs($before_edge_one - $after_edge_one),
2)) <= $limit
&& abs($before_edge_three - $before_edge_two) <= $interior &&
abs($before_edge_two - $before_edge_one) <= $interior
&& abs($before_edge_one - $before_edge) <= $interior &&
abs($after_edge_three - $after_edge_two) <= $interior
&& abs($after_edge_two - $after_edge_one) <= $interior &&
abs($after_edge_one - $after_edge) <=
$interior;
if (!$ok) {
unset($plane);
return;
}
$high_edge = abs($before_edge_one - $before_edge) >
$sharp_edge_threshold || abs($after_edge_one -
$after_edge) > $sharp_edge_threshold;
if (!$macroblock_edge) {
$amount = self::commonAdjust($plane, $base, $step, $high_edge);
if (!$high_edge) {
$amount = ($amount + 1) >> 1;
$plane[$base + $step] = self::shiftBackFromSigned(
self::shiftToSigned($after_edge_one) - $amount);
$plane[$base - 2 * $step] = self::shiftBackFromSigned(
self::shiftToSigned($before_edge_one) + $amount);
}
unset($plane);
return;
}
if ($high_edge) {
self::commonAdjust($plane, $base, $step, true);
unset($plane);
return;
}
$before_edge_signed_one = self::shiftToSigned($before_edge_one);
$before_edge_signed = self::shiftToSigned($before_edge);
$after_edge_signed = self::shiftToSigned($after_edge);
$after_edge_signed_one = self::shiftToSigned($after_edge_one);
$across_edge = self::holdInsideSignedByte(
$before_edge_signed_one - $after_edge_signed_one);
$wide = self::holdInsideSignedByte($across_edge
+ 3 * ($after_edge_signed - $before_edge_signed));
$amount = (27 * $wide + 63) >> 7;
$plane[$base] = self::shiftBackFromSigned($after_edge_signed - $amount);
$plane[$base - $step] =
self::shiftBackFromSigned($before_edge_signed + $amount);
$amount = (18 * $wide + 63) >> 7;
$plane[$base + $step] =
self::shiftBackFromSigned($after_edge_signed_one - $amount);
$plane[$base - 2 * $step] =
self::shiftBackFromSigned($before_edge_signed_one + $amount);
$amount = (9 * $wide + 63) >> 7;
$plane[$base + 2 * $step] = self::shiftBackFromSigned(
self::shiftToSigned($after_edge_two) - $amount);
$plane[$base - 3 * $step] = self::shiftBackFromSigned(
self::shiftToSigned($before_edge_two) + $amount);
unset($plane);
}
/**
* shiftToSigned shifts a sample so that its middle value sits at zero,
* which is how the filter arithmetic is defined.
*
* @param int $variant which form of the format is in use
* @return int what was read
*/
private static function shiftToSigned(int $variant): int
{
return $variant - 128;
}
/**
* shiftBackFromSigned shifts a filtered value back into the range a sample
* uses.
*
* @param int $variant which form of the format is in use
* @return int what was read
*/
private static function shiftBackFromSigned(int $variant): int
{
return self::holdInsideByte(self::holdInsideSignedByte($variant) + 128);
}
/**
* commonAdjust moves the two samples either side of an edge toward each
* other by the amount the filter calls for.
*
* @param array & $plane zero for luma, one and two for the chroma planes
* @param int $base the value the rest are measured from
* @param int $step how far to move each time
* @param bool $use_outer whether the outer setting is used
* @return int what was read
*/
private static function commonAdjust(array
&$plane, int $base, int $step, bool $use_outer): int
{
$before_edge_one = self::shiftToSigned($plane[$base - 2 * $step]);
$before_edge = self::shiftToSigned($plane[$base - $step]);
$after_edge = self::shiftToSigned($plane[$base]);
$after_edge_one = self::shiftToSigned($plane[$base + $step]);
$amount = self::holdInsideSignedByte(($use_outer
? self::holdInsideSignedByte($before_edge_one -
$after_edge_one) : 0)
+ 3 * ($after_edge - $before_edge));
$field = self::holdInsideSignedByte($amount + 4) >> 3;
$entry = self::holdInsideSignedByte($amount + 3) >> 3;
$plane[$base] = self::shiftBackFromSigned($after_edge - $field);
$plane[$base - $step] = self::shiftBackFromSigned($before_edge +
$entry);
return $field;
}
/* output */
/**
* toPicture hands back the decoded planes as a picture.
*
* @return VideoPicture what was read
*/
private function toPicture(): VideoPicture
{
$line_width = $this->macroblock_width * 16;
$left_half = $this->macroblock_height * 16;
$code_word = $this->macroblock_width * 8;
$channel = $this->macroblock_height * 8;
$luma = array_fill(0, $line_width * $left_half, 0);
for ($down = 0; $down < $left_half; $down++) {
$source = $this->sampleAt(0, 0, $down);
$target = $down * $line_width;
for ($across = 0; $across < $line_width; $across++) {
$luma[$target + $across] = $this->planes[0][$source + $across];
}
}
$blue = array_fill(0, $code_word * $channel, 0);
$red = array_fill(0, $code_word * $channel, 0);
for ($down = 0; $down < $channel; $down++) {
$sum_one = $this->sampleAt(1, 0, $down);
$sum_two = $this->sampleAt(2, 0, $down);
$target = $down * $code_word;
for ($across = 0; $across < $code_word; $across++) {
$blue[$target + $across] = $this->planes[1][$sum_one + $across];
$red[$target + $across] = $this->planes[2][$sum_two + $across];
}
}
return new VideoPicture(
$luma, $blue, $red, $this->frame_width, $this->frame_height,
$line_width,
$code_word, 0, 0);
}
}