<?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 class reads VP9's values. They are written as a range narrowed one
* bit at a time rather than as whole numbers.
*/
namespace seekquarry\yioop\library\av_processing;
/**
* Vp9BoolDecoder reads VP9's values, which are written as a range narrowed
* one bit at a time rather than as whole numbers.
*/
final class Vp9BoolDecoder
{
/**
* $bytes stores the part of the frame being read. VP9 splits a frame into
* parts that can be read side by side, and each gets a reader of its own.
* @var string
*/
private string $bytes;
/**
* $position stores how far into those bytes the reading has gone. It moves
* forward only as the range below needs more bits.
* @var int
*/
private int $position;
/**
* $end stores where this part stops. Reading past it gives zeros and sets
* the overran flag, which is how a wrong parse shows itself.
* @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, and it is widened again when
* it grows too small to split.
* @var int
*/
private int $range = 255;
/**
* $range_position stores where in that range the number being read sits.
* Comparing it against the split point is what decides each bit.
* @var int
*/
private int $range_position = 0;
/**
* $max_bits stores how many bits this part holds altogether. A part ends in
* zeros, so a parse that has drifted is caught by checking what is left.
* Read by bitsLeft(), paddingIsZero().
* @var int
*/
private int $max_bits = 0;
/**
* $held stores the byte being handed out a bit at a time.
* @var int
*/
private int $held = 0;
/**
* $spare stores bits of that byte still to hand out.
* @var int
*/
private int $spare = 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 $size how many bytes
*/
public function __construct(string $bytes, int $offset, int $size)
{
if ($size < 1 || $offset + $size > strlen($bytes)) {
throw new VideoException('VP9 partition is truncated');
}
$this->bytes = $bytes;
$this->position = $offset;
$this->end = $offset + $size;
$this->range_position = ord($bytes[$offset]);
$this->position++;
$this->max_bits = 8 * $size - 8;
if ($this->readOneBit(128) !== 0) {
throw new VideoException('VP9 partition marker bit is not zero');
}
}
/**
* $overran stores set once the decoder has been asked for more bits than
* the partition holds.
* @var bool
*/
public bool $overran = false;
/**
* nextBit takes the next bit from the stream, refilling a byte at a time.
*
* @return int what was read
*/
private function nextBit(): int
{
if ($this->max_bits > 0) {
$this->max_bits--;
} else {
$this->overran = true;
return 0;
}
if ($this->spare === 0) {
$this->held = ($this->position < $this->end)
? ord($this->bytes[$this->position]) : 0;
$this->position++;
$this->spare = 8;
}
$taken = ($this->held >> ($this->spare - 1)) & 1;
$this->spare--;
return $taken;
}
/**
* 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
{
$range = $this->range;
$value = $this->range_position;
$split = 1 + ((($range - 1) * $prob) >> 8);
if ($value < $split) {
$range = $split;
$result = 0;
} else {
$range -= $split;
$value -= $split;
$result = 1;
}
/*
Renormalize by pulling in whole bytes at a time. Reading one
byte and shifting several bits out of it costs far less than
fetching from the string once a bit.
*/
if ($range < 128) {
$held = $this->held;
$spare = $this->spare;
do {
if ($spare === 0) {
if ($this->position < $this->end) {
$held = ord($this->bytes[$this->position]);
} else {
$held = 0;
}
$this->position++;
$spare = 8;
}
$range <<= 1;
$value = ($value << 1) + (($held >> ($spare - 1)) & 1);
$spare--;
if ($this->max_bits > 0) {
$this->max_bits--;
} else {
$this->overran = true;
}
} while ($range < 128);
$this->held = $held;
$this->spare = $spare;
}
$this->range = $range;
$this->range_position = $value;
return $result;
}
/**
* literal reads a plain number of the given width, most significant bit
* first.
*
* @param int $bits the reader the stream's bits are taken from
* @return int what was read
*/
public function literal(int $bits): int
{
$value = 0;
for ($i = 0; $i < $bits; $i++) {
$value = ($value << 1) | $this->readOneBit(128);
}
return $value;
}
/**
* bitsLeft bits of the partition still unread
*
* @return int what was read
*/
public function bitsLeft(): int
{
return $this->max_bits;
}
/**
* paddingIsZero a partition ends with zero padding, so a parse that has
* gone wrong almost always shows up here.
*
* @return bool what was read
*/
public function paddingIsZero(): bool
{
if ($this->overran) {
/* the parse ran off the end, so it cannot be right */
return false;
}
while ($this->max_bits > 0) {
if ($this->nextBit() !== 0) {
return false;
}
}
return true;
}
}