diff --git a/CMakeLists.txt b/CMakeLists.txt index 899674b..adc43e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,5 +33,12 @@ target_link_libraries(mp3packercpp PRIVATE mp3packercpp_core) target_include_directories(mp3packercpp PRIVATE src/cli) target_compile_definitions(mp3packercpp PRIVATE MP3PACKERCPP_VERSION="${PROJECT_VERSION}") +# --- Tests --- +option(MP3PACKERCPP_BUILD_TESTS "Build the test programs" ${PROJECT_IS_TOP_LEVEL}) +if(MP3PACKERCPP_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() + include(GNUInstallDirs) install(TARGETS mp3packercpp DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/core/huffman.cpp b/src/core/huffman.cpp index 7abab9d..ae10b71 100644 --- a/src/core/huffman.cpp +++ b/src/core/huffman.cpp @@ -5,6 +5,24 @@ #include #include +// The cost machinery keeps all 32 code tables side by side, so its two kernels are +// pure 32-lane vector work: eight vectors cover a whole row, and the only scalar +// work is deriving two table addresses from a packed key. Written by hand rather +// than left to the auto-vectoriser, which will not keep the accumulators live +// across the loop; that is worth 8-9% of a recompressing run on its own. Define +// MP3PACKER_NO_SIMD to build the portable form, which is what the kernels are held +// against. +// +// AArch64 with a GNU-style compiler only: the horizontal minimum the span kernel +// ends in has no 32-bit NEON encoding, so __ARM_NEON alone is not enough of a +// test, and MSVC spells its NEON header differently. Everything else builds the +// portable form, which measured about 9% behind on a recompressing run. +#if !defined(MP3PACKER_NO_SIMD) && defined(__aarch64__) && (defined(__ARM_NEON) || defined(__ARM_NEON__)) +#define MP3PACKER_NEON 1 +#include +#endif + + namespace mp3packer { struct SymbolCode { @@ -65,70 +83,333 @@ class HuffmanTableManager { } }; -static std::vector get_sf_bands(const uint32_t samplerate) { - if (samplerate == 48000) return { 0, 4, 8, 12, 16, 20, 24, 30, 36, 42, 50, 60, 72, 88, 106, 128, 156, 190, 230, 276, 330, 384, 576 }; - if (samplerate == 44100) return { 0, 4, 8, 12, 16, 20, 24, 30, 36, 44, 52, 62, 74, 90, 110, 134, 162, 196, 238, 288, 342, 418, 576 }; - if (samplerate == 32000) return { 0, 4, 8, 12, 16, 20, 24, 30, 36, 44, 54, 66, 82, 102, 126, 156, 194, 240, 296, 364, 448, 550, 576 }; - if (samplerate == 24000) return { 0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 114, 136, 162, 194, 232, 278, 332, 394, 464, 540, 576 }; +// The band tables are static rather than built per call: every one of decode, +// encode and the search wants them, so returning a vector by value cost three +// heap allocations per granule for data that never changes. +constexpr int kNumBands = 23; ///< long-block band boundaries, including 0 and 576 +constexpr int kNumBandsShort = 14; ///< short-block band boundaries + +using SfBands = int[kNumBands]; +using SfBandsShort = int[kNumBandsShort]; + +static constexpr SfBands sf_bands_48000 = { 0, 4, 8, 12, 16, 20, 24, 30, 36, 42, 50, 60, 72, 88, 106, 128, 156, 190, 230, 276, 330, 384, 576 }; +static constexpr SfBands sf_bands_44100 = { 0, 4, 8, 12, 16, 20, 24, 30, 36, 44, 52, 62, 74, 90, 110, 134, 162, 196, 238, 288, 342, 418, 576 }; +static constexpr SfBands sf_bands_32000 = { 0, 4, 8, 12, 16, 20, 24, 30, 36, 44, 54, 66, 82, 102, 126, 156, 194, 240, 296, 364, 448, 550, 576 }; +static constexpr SfBands sf_bands_24000 = { 0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 114, 136, 162, 194, 232, 278, 332, 394, 464, 540, 576 }; +static constexpr SfBands sf_bands_lsf = { 0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576 }; +static constexpr SfBands sf_bands_8000 = { 0, 12, 24, 36, 48, 60, 72, 88, 108, 132, 160, 192, 232, 280, 336, 400, 476, 566, 568, 570, 572, 574, 576 }; + +static constexpr SfBandsShort sf_bands_short_48000 = { 0, 4, 8, 12, 16, 22, 28, 38, 50, 64, 80, 100, 126, 192 }; +static constexpr SfBandsShort sf_bands_short_44100 = { 0, 4, 8, 12, 16, 22, 30, 40, 52, 66, 84, 106, 136, 192 }; +static constexpr SfBandsShort sf_bands_short_32000 = { 0, 4, 8, 12, 16, 22, 30, 42, 58, 78, 104, 138, 180, 192 }; +static constexpr SfBandsShort sf_bands_short_24000 = { 0, 4, 8, 12, 18, 26, 36, 48, 62, 80, 104, 136, 180, 192 }; +static constexpr SfBandsShort sf_bands_short_lsf = { 0, 4, 8, 12, 18, 24, 32, 42, 56, 74, 100, 132, 174, 192 }; +static constexpr SfBandsShort sf_bands_short_8000 = { 0, 8, 16, 24, 36, 52, 72, 96, 124, 160, 162, 164, 166, 192 }; + +static const int* get_sf_bands(const uint32_t samplerate) { + if (samplerate == 48000) return sf_bands_48000; + if (samplerate == 44100) return sf_bands_44100; + if (samplerate == 32000) return sf_bands_32000; + if (samplerate == 24000) return sf_bands_24000; if (samplerate == 22050 || samplerate == 16000 || samplerate == 12000 || samplerate == 11025) - return { 0, 6, 12, 18, 24, 30, 36, 44, 54, 66, 80, 96, 116, 140, 168, 200, 238, 284, 336, 396, 464, 522, 576 }; - if (samplerate == 8000) return { 0, 12, 24, 36, 48, 60, 72, 88, 108, 132, 160, 192, 232, 280, 336, 400, 476, 566, 568, 570, 572, 574, 576 }; - return { 0, 4, 8, 12, 16, 20, 24, 30, 36, 44, 52, 62, 74, 90, 110, 134, 162, 196, 238, 288, 342, 418, 576 }; + return sf_bands_lsf; + if (samplerate == 8000) return sf_bands_8000; + return sf_bands_44100; } -static std::vector get_sf_bands_short(const uint32_t samplerate) { - if (samplerate == 48000) return { 0, 4, 8, 12, 16, 22, 28, 38, 50, 64, 80, 100, 126, 192 }; - if (samplerate == 44100) return { 0, 4, 8, 12, 16, 22, 30, 40, 52, 66, 84, 106, 136, 192 }; - if (samplerate == 32000) return { 0, 4, 8, 12, 16, 22, 30, 42, 58, 78, 104, 138, 180, 192 }; - if (samplerate == 24000) return { 0, 4, 8, 12, 18, 26, 36, 48, 62, 80, 104, 136, 180, 192 }; +static const int* get_sf_bands_short(const uint32_t samplerate) { + if (samplerate == 48000) return sf_bands_short_48000; + if (samplerate == 44100) return sf_bands_short_44100; + if (samplerate == 32000) return sf_bands_short_32000; + if (samplerate == 24000) return sf_bands_short_24000; if (samplerate == 22050 || samplerate == 16000 || samplerate == 12000 || samplerate == 11025) - return { 0, 4, 8, 12, 18, 24, 32, 42, 56, 74, 100, 132, 174, 192 }; - if (samplerate == 8000) return { 0, 8, 16, 24, 36, 52, 72, 96, 124, 160, 162, 164, 166, 192 }; - return { 0, 4, 8, 12, 16, 22, 30, 40, 52, 66, 84, 106, 136, 192 }; + return sf_bands_short_lsf; + if (samplerate == 8000) return sf_bands_short_8000; + return sf_bands_short_44100; } +// --- decode tables --------------------------------------------------------- +namespace { + +/// One 8-bit prefix of a code table. A codeword of eight bits or fewer resolves +/// in a single lookup; anything longer hands back the tree node the first eight +/// bits reached, and the walk carries on from there. +struct DecodeEntry { + int16_t symbol = 0; ///< Decoded table value, unless length is kLongCode + uint8_t length = 0; ///< Codeword length in bits, or kLongCode to keep walking + uint16_t node = 0; ///< Tree index the first eight bits reached +}; + +/// Marks a prefix whose codeword is longer than eight bits. It cannot be a +/// length, and in particular is not zero: table 0 codes its single symbol in no +/// bits at all, so a zero length is a real one. +constexpr uint8_t kLongCode = 0xFF; + +using DecodeLut = DecodeEntry[256]; + +// --- big-value pair probe -------------------------------------------------- +// +// How many bits of the stream index the probe. Measured here rather than inherited: +// against a ten-bit table, eight bits costs 4.9%, nine 5.8%, eleven 4.7% and twelve +// 5.1%, each losing every one of eight interleaved pairs, with a ten-against-ten +// control at +0.15%. Narrower defers more codewords to a tree walk; wider stops +// fitting alongside the search's cost tables. +// +// Worth noting because the same idea in the Go implementation of this tool, measured +// on this same machine, found eleven bits a tie with ten. The tables there are the +// same size, so the difference is this program's other resident tables, not the +// cache: an inherited constant would have been the wrong one. +constexpr int kProbeBits = 10; +constexpr size_t kProbeSize = size_t{1} << kProbeBits; + +/// A resolved big-value pair: not the symbol, but the two magnitudes it stands +/// for, the codeword's length, and each magnitude's sign-bit count. +/// +/// The point of packing all of that into one entry is that decoding a pair becomes +/// a single load and some shifts. Reading a symbol and then looking up what it +/// means put two dependent loads on the critical path, once per pair, and the +/// second could not begin until the first had landed. +/// +/// bits 0-3 x, clamped to 15 +/// bits 4-7 y, clamped to 15 +/// bits 8-11 codeword length, which is at most kProbeBits here +/// bit 12 whether x takes a sign bit +/// bit 13 whether y takes a sign bit +/// bit 14 set when the codeword is longer than the probe +using PairEntry = uint16_t; +constexpr PairEntry kPairSlow = 1 << 14; + +using PairProbe = PairEntry[kProbeSize]; + +static const PairProbe* pair_probes() { + static const std::array, 32> tables = [] { + std::array, 32> all{}; + for (int idx = 0; idx < 32; ++idx) { + const int16_t* tab = huffman_tables[idx].table; + for (size_t prefix = 0; prefix < kProbeSize; ++prefix) { + int node = 0, used = 0; + for (;;) { + const int16_t v = tab[node]; + if (v >= 0) { // leaf: the codeword fits in the probe + const int x = (v >> 4) & 0xF, y = v & 0xF; + all[idx][prefix] = static_cast( + x | y << 4 | used << 8 | (x != 0) << 12 | (y != 0) << 13); + break; + } + if (used == kProbeBits) { // longer than the probe: walk it instead + all[idx][prefix] = kPairSlow; + break; + } + ++node; + if (prefix >> (kProbeBits - 1 - used) & 1) node -= v; + ++used; + } + } + } + return all; + }(); + return reinterpret_cast(tables.data()); +} + +/// One count1 quadruple, signs applied. Indexed by the codeword's non-zero pattern +/// together with the four bits that follow it, so the pattern and its signs come +/// out of a single lookup however many of those bits are really consumed. +struct Count1Quad { int16_t v[4]; }; + +static const Count1Quad* count1_quad_table() { + static const std::array table = [] { + std::array all{}; + for (int sym = 0; sym < 16; ++sym) { + for (int signs = 0; signs < 16; ++signs) { + Count1Quad q{}; + int bit = 3; // sign bits are taken from the top of the four + for (int i = 0; i < 4; ++i) { + if (sym & (8 >> i)) { + q.v[i] = (signs >> bit & 1) ? int16_t{-1} : int16_t{1}; + --bit; + } + } + all[sym << 4 | signs] = q; + } + } + return all; + }(); + return table.data(); +} + +/// How many sign bits a count1 quadruple with this non-zero pattern consumes. +static constexpr uint8_t count1_signs[16] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}; + +static const DecodeLut* decode_luts() { + static const std::array, 34> luts = [] { + std::array, 34> all{}; + for (int idx = 0; idx < 34; ++idx) { + const int16_t* tab = huffman_tables[idx].table; + for (int prefix = 0; prefix < 256; ++prefix) { + int node = 0, len = 0; + for (;;) { + const int16_t v = tab[node]; + if (v >= 0) { // leaf + all[idx][prefix] = {v, static_cast(len), 0}; + break; + } + ++node; + if (prefix >> (7 - len) & 1) node -= v; + ++len; + if (len == 8) { // longer than the prefix: resume at this node + all[idx][prefix] = {0, kLongCode, static_cast(node)}; + break; + } + } + } + } + return all; + }(); + return reinterpret_cast(luts.data()); +} + +} // namespace + HuffmanOptimizer::HuffmanOptimizer() = default; -std::vector HuffmanOptimizer::decode_quantized_coefficients( - const HuffmanConfig& config, BitstreamReader& reader, +void HuffmanOptimizer::decode_quantized_coefficients( + Spectrum& coeffs, const HuffmanConfig& config, BitstreamReader& reader, const uint32_t samplerate, const int max_huffman_bits) { - std::vector coeffs(576, 0); int out_off = 0; const auto sf_bands = get_sf_bands(samplerate); + const DecodeLut* luts = decode_luts(); + const PairProbe* probes = pair_probes(); + const Count1Quad* quads = count1_quad_table(); + // The bit position is carried in a local rather than in the reader: going + // through it for every bit of every symbol put a store-to-load turnaround on + // the loop's critical path. The reader is resynchronised at each exit. + size_t bp = reader.tell_bit(); const size_t bit_limit = (max_huffman_bits >= 0) - ? (reader.tell_bit() + static_cast(max_huffman_bits)) + ? (bp + static_cast(max_huffman_bits)) : SIZE_MAX; - // decode one full huffman symbol; never aborts mid-traversal (matches mp3packer's decode_big_quants) - auto decode_symbol = [&](const uint8_t table_idx) -> int16_t { - if (table_idx == 0) return 0; - const int16_t* tab = huffman_tables[table_idx].table; + // A peeked word only holds 57 bits whatever the misalignment, and a read that + // reaches past the end of the data has its own behaviour to reproduce, so the + // last few bytes are decoded a bit at a time instead. That is at most the final + // granule of a file. + const size_t fast_until = reader.size_bits() >= 57 ? reader.size_bits() - 57 : 0; + + // One pair, read a bit at a time through the reader. Identical in effect to the + // fast path, and the only thing that runs where the fast path's assumptions + // stop holding. + auto decode_pair_slow = [&](const uint8_t table_idx, const int linbits) { + reader.seek_bit(bp); int16_t got = 0; - while ((got = *tab++) < 0) { - if (reader.read_bits(1)) tab -= got; + if (table_idx != 0) { + const int16_t* tab = huffman_tables[table_idx].table; + while ((got = *tab++) < 0) { + if (reader.read_bits(1)) tab -= got; + } + } + int y = got & 0xF; + int x = (got >> 4) & 0xF; + if (x > 0) { + if (x == 15 && linbits > 0) x += static_cast(reader.read_bits(linbits)); + if (reader.read_bits(1)) x = -x; } - return got; + if (y > 0) { + if (y == 15 && linbits > 0) y += static_cast(reader.read_bits(linbits)); + if (reader.read_bits(1)) y = -y; + } + coeffs[out_off++] = static_cast(x); + coeffs[out_off++] = static_cast(y); + bp = reader.tell_bit(); }; - // big region: boundary checked only at the start of each pair; the symbol is then read whole + // big region: boundary checked only at the start of each pair; the symbol is + // then read whole (matches mp3packer's decode_big_quants). A whole pair — + // codeword, escape magnitudes and signs — is at most 47 bits, so it comes out + // of one peeked word rather than bit by bit, and the first eight bits usually + // resolve the codeword in a single table lookup. auto decode_region = [&](const int count_pairs, const uint8_t table_idx) -> bool { const int linbits = huffman_tables[table_idx].linbits; + const int16_t* tree = huffman_tables[table_idx].table; + const PairEntry* probe = probes[table_idx]; + // A magnitude of 15 escapes to linbits, but only for tables that have any. + // Putting the trigger out of range for the others folds "does this table + // escape" into the comparison the loop was making anyway. + const int escape = linbits > 0 ? 15 : 16; const int target = out_off + count_pairs * 2; - while (out_off < target && out_off < 575) { - if (table_idx != 0 && reader.tell_bit() >= bit_limit) return false; - const int got = decode_symbol(table_idx); - int y = got & 0xF; - int x = (got >> 4) & 0xF; - if (x > 0) { - if (x == 15 && linbits > 0) x += static_cast(reader.read_bits(linbits)); - if (reader.read_bits(1)) x = -x; + // A pair needs two coefficients, so the spectrum's own end bounds the region + // as much as the declared pair count does; the first even position at or + // past 575 is 576. Folding them together leaves one comparison per pair + // instead of two. + const int end = std::min(target, 576); + // Table 0 codes nothing: every pair is a pair of zeros and no bits are + // consumed, so the whole region is a fill rather than a walk. Region 2 uses + // it often on quiet material. + if (table_idx == 0) { + std::fill(coeffs.begin() + out_off, coeffs.begin() + end, int16_t{0}); + out_off = end; + return true; + } + while (out_off < end) { + if (bp >= bit_limit) return false; + if (bp > fast_until) { decode_pair_slow(table_idx, linbits); continue; } + uint64_t w = reader.peek_at(bp); + const PairEntry e = probe[w >> (64 - kProbeBits)]; + int used, x, y; + unsigned nx, ny; + if (e < kPairSlow) { + // The codeword length is taken out first: everything after this + // waits on the window having moved past the codeword, and nothing + // waits on the magnitudes. + used = e >> 8 & 0xF; + w <<= used; + x = e & 0xF; + y = e >> 4 & 0xF; + nx = e >> 12 & 1; + ny = e >> 13 & 1; + } else { + // One codeword in a hundred is longer than the probe. Walking it + // from the root is cheaper than keeping a table of positions to + // resume from — measured, in the implementation this follows. + int node = 0; + int sym = 0; + used = 0; + for (;;) { + const int16_t v = tree[node]; + if (v >= 0) { sym = v; break; } + ++node; + if (w >> (63 - used) & 1) node -= v; + ++used; + } + w <<= used; + x = (sym >> 4) & 0xF; + y = sym & 0xF; + nx = static_cast(x != 0); + ny = static_cast(y != 0); + } + // Neither a sign bit nor whether a value is zero can be predicted, so + // neither is branched on now that the sign counts arrive with the pair. + // Applying a sign is an xor and an add — v^-1+1 is -v, v^0+0 is v — and + // it is a no-op on zero for either sign bit, so only the bit advance + // depends on the value: a shift by the sign count, zero or one. + if (x == escape) { + x += static_cast(w >> (64 - linbits)); + w <<= linbits; + used += linbits; } - if (y > 0) { - if (y == 15 && linbits > 0) y += static_cast(reader.read_bits(linbits)); - if (reader.read_bits(1)) y = -y; + const int sx = static_cast(w >> 63); + x = (x ^ -sx) + sx; + w <<= nx; + used += static_cast(nx); + if (y == escape) { + y += static_cast(w >> (64 - linbits)); + w <<= linbits; + used += linbits; } + const int sy = static_cast(w >> 63); + y = (y ^ -sy) + sy; + used += static_cast(ny); coeffs[out_off++] = static_cast(x); coeffs[out_off++] = static_cast(y); + bp += static_cast(used); } return true; }; @@ -153,47 +434,290 @@ std::vector HuffmanOptimizer::decode_quantized_coefficients( bool cont = decode_region(r0_pairs, config.table0); if (cont) cont = decode_region(r1_pairs, config.table1); - if (cont) decode_region(r2_pairs, config.table2); + if (cont) cont = decode_region(r2_pairs, config.table2); - // count1 region: boundary checked only at the start of each quad - const uint8_t count1_table = config.count1_table_select ? 33 : 32; - while (out_off <= 572 && reader.tell_bit() < bit_limit) { - const int got = decode_symbol(count1_table); - coeffs[out_off++] = static_cast((got & 8) ? (reader.read_bits(1) ? -1 : 1) : 0); - coeffs[out_off++] = static_cast((got & 4) ? (reader.read_bits(1) ? -1 : 1) : 0); - coeffs[out_off++] = static_cast((got & 2) ? (reader.read_bits(1) ? -1 : 1) : 0); - coeffs[out_off++] = static_cast((got & 1) ? (reader.read_bits(1) ? -1 : 1) : 0); + // count1 region: boundary checked only at the start of each quad. No count1 + // codeword exceeds six bits, so the quadruple and its four possible signs come + // out of the same peeked word. + if (cont) { + const uint8_t count1_table = config.count1_table_select ? 33 : 32; + const DecodeEntry* lut = luts[count1_table]; + while (out_off <= 572 && bp < bit_limit) { + if (bp > fast_until) { // last bytes of the data: read a bit at a time + reader.seek_bit(bp); + const int16_t* tab = huffman_tables[count1_table].table; + int16_t got = 0; + while ((got = *tab++) < 0) { + if (reader.read_bits(1)) tab -= got; + } + for (int mask = 8; mask != 0; mask >>= 1) { + coeffs[out_off++] = static_cast((got & mask) ? (reader.read_bits(1) ? -1 : 1) : 0); + } + bp = reader.tell_bit(); + continue; + } + uint64_t w = reader.peek_at(bp); + const DecodeEntry& e = lut[w >> 56]; + if (e.length == kLongCode) break; // unreachable: no count1 codeword exceeds six bits + const int got = e.symbol; + w <<= e.length; + // One lookup covers the pattern and its signs together; the sign bits + // sit at the top of w now that the codeword has been shifted off. + const Count1Quad& q = quads[got << 4 | static_cast(w >> 60)]; + coeffs[out_off + 0] = q.v[0]; + coeffs[out_off + 1] = q.v[1]; + coeffs[out_off + 2] = q.v[2]; + coeffs[out_off + 3] = q.v[3]; + out_off += 4; + bp += static_cast(e.length) + count1_signs[got]; + } + } + reader.seek_bit(bp); + // Every coefficient below out_off was written before it could be read, so only + // the tail above it has to be cleared: zeroing the whole spectrum up front + // rewrote 1.1 kB per granule to no purpose, since a granule that codes half its + // coefficients leaves the rest untouched anyway. + std::fill(coeffs.begin() + out_off, coeffs.end(), int16_t{0}); +} + +// --- search cost machinery ------------------------------------------------- +namespace { +// +// The search never materialises a cost per pair per table. Two observations make +// that unnecessary: +// +// * A pair's cost under a table follows from its clamped magnitudes alone, so +// it is a lookup in a table built once for the whole program rather than +// arithmetic (and a Huffman-map lookup) repeated for 288 pairs × 32 tables +// on every granule. +// * Every region boundary the side info can express is a scalefactor band +// boundary, of which there are 23. So only the running per-table totals *at +// those boundaries* are ever asked about — 23 rows of 32 lanes, not 288 — +// and the cost of a span is one row subtracted from another. +// +// Region costs are then memoised by the shape of the span: spans below a +// boundary and spans between two boundaries do not move with big_values and are +// computed once per granule, and only the spans ending at big_values itself are +// recomputed per candidate. + +constexpr int kNumTables = 32; + +// Per-pair cost of a table that cannot represent the pair. Kept at the original +// value, along with the ">= kPenalty means the region is impossible" test, so +// the accept/reject boundary of the search is bit-for-bit the old one. A granule +// is at most 288 pairs, so a sum of penalties cannot overflow int32. +constexpr int32_t kPenalty = 10000; + +/// Costs are held pre-scaled by 1<> 4, sy = sym & 15; + for (int tab = 0; tab < kNumTables; ++tab) { + const auto& map = HuffmanTableManager::get_encoding_map(static_cast(tab)); + const int max_quant = max_quant_per_table[tab]; + if (max_quant < 0 || !map[sym].valid || sx > max_quant || sy > max_quant) { + t.pair[sym][tab] = kPenalty << kCostShift; + continue; + } + const int linbits = huffman_tables[tab].linbits; + int bits = map[sym].length; + if (sx == 15) bits += linbits; + if (sy == 15) bits += linbits; + if (sx != 0) bits += 1; + if (sy != 0) bits += 1; + t.pair[sym][tab] = bits << kCostShift; + } + } + // A magnitude m > 15 needs bit_length(m - 15) linbits; max_quant_per_table + // is exactly 15 + 2^linbits - 1 for every escaping table, so "needs more + // bits than the table has" is the original's m > max_quant test. + for (int need = 0; need < 16; ++need) { + for (int tab = 0; tab < kNumTables; ++tab) { + t.escape[need][tab] = (need <= linbits_per_table[tab] && max_quant_per_table[tab] >= 0) + ? 0 : (need > 0 ? kPenalty << kCostShift : 0); + } + } + return t; + }(); + return tables; +} + +/// Packs a pair into the two cost rows it needs: the clamped symbol, and how +/// many bits of magnitude sit above the escape threshold. +static inline uint32_t pair_key(const int x, const int y) { + const int ax = std::abs(x), ay = std::abs(y); + const uint32_t sym = static_cast(std::min(15, ax) << 4 | std::min(15, ay)); + const int m = std::max(ax, ay); + uint32_t need = 0; + if (m > 15) { + // bit length of m - 15, capped at the table's 16 rows + unsigned v = static_cast(m - 15); + while (v) { ++need; v >>= 1; } + need = std::min(need, 15u); + } + return sym | need << 8; +} + +/// Labels each table's lane, so that a plain minimum over packed +/// cost<> 8 & 0xF]; + b0 = vaddq_s32(b0, vld1q_s32(esc + 0)); + b1 = vaddq_s32(b1, vld1q_s32(esc + 4)); + b2 = vaddq_s32(b2, vld1q_s32(esc + 8)); + b3 = vaddq_s32(b3, vld1q_s32(esc + 12)); + b4 = vaddq_s32(b4, vld1q_s32(esc + 16)); + b5 = vaddq_s32(b5, vld1q_s32(esc + 20)); + b6 = vaddq_s32(b6, vld1q_s32(esc + 24)); + b7 = vaddq_s32(b7, vld1q_s32(esc + 28)); + } + a0 = vaddq_s32(a0, b0); a1 = vaddq_s32(a1, b1); + a2 = vaddq_s32(a2, b2); a3 = vaddq_s32(a3, b3); + a4 = vaddq_s32(a4, b4); a5 = vaddq_s32(a5, b5); + a6 = vaddq_s32(a6, b6); a7 = vaddq_s32(a7, b7); + } + vst1q_s32(acc + 0, a0); vst1q_s32(acc + 4, a1); + vst1q_s32(acc + 8, a2); vst1q_s32(acc + 12, a3); + vst1q_s32(acc + 16, a4); vst1q_s32(acc + 20, a5); + vst1q_s32(acc + 24, a6); vst1q_s32(acc + 28, a7); +#else + for (int i = 0; i < n; ++i) { + const uint32_t key = keys[i]; + const int32_t* base = ct.pair[key & 0xFF]; + if (key & 0xF00) { + const int32_t* esc = ct.escape[key >> 8 & 0xF]; + for (int t = 0; t < kNumTables; ++t) acc[t] += base[t] + esc[t]; + } else { + for (int t = 0; t < kNumTables; ++t) acc[t] += base[t]; + } } - return coeffs; +#endif } +/// Cheapest table for the span between two prefix rows, packed as +/// cost<(from); + const uint32_t* t = reinterpret_cast(to); + uint32x4_t d0 = vorrq_u32(vsubq_u32(vld1q_u32(t + 0), vld1q_u32(f + 0)), vld1q_u32(lane_index + 0)); + uint32x4_t d1 = vorrq_u32(vsubq_u32(vld1q_u32(t + 4), vld1q_u32(f + 4)), vld1q_u32(lane_index + 4)); + uint32x4_t d2 = vorrq_u32(vsubq_u32(vld1q_u32(t + 8), vld1q_u32(f + 8)), vld1q_u32(lane_index + 8)); + uint32x4_t d3 = vorrq_u32(vsubq_u32(vld1q_u32(t + 12), vld1q_u32(f + 12)), vld1q_u32(lane_index + 12)); + uint32x4_t d4 = vorrq_u32(vsubq_u32(vld1q_u32(t + 16), vld1q_u32(f + 16)), vld1q_u32(lane_index + 16)); + uint32x4_t d5 = vorrq_u32(vsubq_u32(vld1q_u32(t + 20), vld1q_u32(f + 20)), vld1q_u32(lane_index + 20)); + uint32x4_t d6 = vorrq_u32(vsubq_u32(vld1q_u32(t + 24), vld1q_u32(f + 24)), vld1q_u32(lane_index + 24)); + uint32x4_t d7 = vorrq_u32(vsubq_u32(vld1q_u32(t + 28), vld1q_u32(f + 28)), vld1q_u32(lane_index + 28)); + d0 = vminq_u32(d0, d1); d2 = vminq_u32(d2, d3); + d4 = vminq_u32(d4, d5); d6 = vminq_u32(d6, d7); + d0 = vminq_u32(d0, d2); d4 = vminq_u32(d4, d6); + return vminvq_u32(vminq_u32(d0, d4)); +#else + uint32_t best = UINT32_MAX; + for (int t = 0; t < kNumTables; ++t) { + const uint32_t k = static_cast(to[t] - from[t]) | lane_index[t]; + best = std::min(best, k); + } + return best; +#endif +} + +/// The cheapest two-region cover of every pair below a boundary: what it costs, +/// where region0 ends and the tables both regions use. None of it moves with +/// big_values, so it is settled once per boundary and only read thereafter. +struct PrefixSplit { + int32_t bits = 0; + int16_t k0 = 0; ///< boundary region0 ends at + int8_t t0 = 0; + int8_t t1 = 0; + bool ok = false; + bool done = false; +}; + +/// Working set of one search. Thread-local because the search runs once per +/// granule — hundreds of thousands of times per second — and these arrays are far +/// too large to keep reallocating. +struct SearchScratch { + uint32_t keys[288]; + + int32_t acc[kNumTables]; ///< totals over [0, big_values) + int32_t rows[kNumBands + 1][kNumTables]; ///< totals at each boundary; slot kNumBands is the short-block region0 boundary + bool row_valid[kNumBands + 1]; + + PrefixSplit prefix[kNumBands]; ///< best two-region cover below a boundary, fixed across candidates + uint32_t head[kNumBands + 1]; ///< span [0, boundary), fixed across candidates + bool head_done[kNumBands + 1]; + uint32_t mid[kNumBands + 1][kNumBands + 1]; ///< span between two boundaries, likewise fixed + bool mid_done[kNumBands + 1][kNumBands + 1]; + uint32_t tail[kNumBands + 1]; ///< span [boundary, big_values), recomputed per candidate + int tail_bv[kNumBands + 1]; + + int32_t c1_bits32[kNumCoefficients + 8]; + int32_t c1_bits33[kNumCoefficients + 8]; + bool c1_ok[kNumCoefficients + 8]; +}; + +} // namespace + HuffmanConfig HuffmanOptimizer::find_best_config( - const std::vector& coeffs, + const Spectrum& coeffs, const HuffmanConfig& orig_config, uint32_t samplerate) { HuffmanConfig best = orig_config; uint32_t min_total_bits = 0xFFFFFFFF; - const auto sf_bands = get_sf_bands(samplerate); - - // pre-calculate bit costs for each coefficient pair in every table - std::vector> pair_costs(288); - for (int pair = 0; pair < 288; ++pair) { - int x = std::abs(coeffs[static_cast(2 * pair)]); - int y = std::abs(coeffs[static_cast(2 * pair + 1)]); - for (int tab = 0; tab < 32; ++tab) { - if (max_quant_per_table[tab] == -1) { pair_costs[pair][tab] = 10000; continue; } - if (x > max_quant_per_table[tab] || y > max_quant_per_table[tab]) { pair_costs[pair][tab] = 10000; continue; } - const auto& map = HuffmanTableManager::get_encoding_map(static_cast(tab)); - int abs_x = std::min(15, x), abs_y = std::min(15, y); - int idx = (abs_x << 4) | abs_y; - if (!map[idx].valid) { pair_costs[pair][tab] = 10000; continue; } - int bits = map[idx].length; - if (abs_x == 15) bits += huffman_tables[tab].linbits; - if (x != 0) bits += 1; - if (abs_y == 15) bits += huffman_tables[tab].linbits; - if (y != 0) bits += 1; - pair_costs[pair][tab] = bits; - } - } + const int* sf_bands = get_sf_bands(samplerate); + const PairCosts& ct = pair_cost_tables(); + static thread_local SearchScratch sc; int last_bv_pair = 0; for (int pair = 287; pair >= 0; --pair) { @@ -214,99 +738,169 @@ HuffmanConfig HuffmanOptimizer::find_best_config( int max_possible_bv = (last_nonzero_coeff + 1) / 2; if (max_possible_bv < last_bv_pair) max_possible_bv = last_bv_pair; - std::vector> prefix_costs(static_cast(max_possible_bv + 1)); - for (int tab = 0; tab < 32; ++tab) prefix_costs[0][tab] = 0; - for (int pair = 0; pair < max_possible_bv; ++pair) { - for (int tab = 0; tab < 32; ++tab) { - const uint32_t cost = static_cast(pair_costs[pair][tab]); - prefix_costs[pair + 1][tab] = std::min(prefix_costs[pair][tab] + cost, static_cast(10000000)); - } + sc.keys[pair] = pair_key(coeffs[static_cast(2 * pair)], coeffs[static_cast(2 * pair + 1)]); } - auto get_best_region = [&](const int start_pair, const int end_pair, int& best_tab) -> uint32_t { - if (start_pair >= end_pair) { best_tab = 0; return 0; } - uint32_t best_cost = 0xFFFFFFFF; - best_tab = 0; - for (int tab = 0; tab < 32; ++tab) { - const uint32_t cost = prefix_costs[end_pair][tab] - prefix_costs[start_pair][tab]; - if (cost < best_cost) { - best_cost = cost; - best_tab = tab; - } - } - return best_cost; - }; - + // count1 cost as a backward recurrence: coding the tail from an even position + // is one quadruple plus the cost four coefficients later, so one walk serves + // every candidate big_values instead of a rescan per candidate. const auto& c1_arr_32 = HuffmanTableManager::get_encoding_map(32); const auto& c1_arr_33 = HuffmanTableManager::get_encoding_map(33); + for (int pos = kNumCoefficients; pos >= 2 * last_bv_pair; pos -= 2) { + if (pos >= last_nonzero_coeff) { + // Nothing above here needs coding at all. + sc.c1_bits32[pos] = 0; + sc.c1_bits33[pos] = 0; + sc.c1_ok[pos] = true; + continue; + } + if (pos > kNumCoefficients - 4) { + // A quadruple cannot start inside the last one, so a split here would + // leave the coefficients above it uncoded. That is a big_values the + // granule cannot use, not a free tail. + sc.c1_ok[pos] = false; + continue; + } + const int v = coeffs[pos], w = coeffs[pos+1], x = coeffs[pos+2], y = coeffs[pos+3]; + const int symbol = ((v != 0) << 3) | ((w != 0) << 2) | ((x != 0) << 1) | (y != 0); + if (!c1_arr_32[symbol].valid || c1_arr_32[symbol].length == 0 || !sc.c1_ok[pos + 4]) { + sc.c1_ok[pos] = false; + continue; + } + const int32_t signs = (v != 0) + (w != 0) + (x != 0) + (y != 0); + sc.c1_ok[pos] = true; + sc.c1_bits32[pos] = sc.c1_bits32[pos + 4] + c1_arr_32[symbol].length + signs; + sc.c1_bits33[pos] = sc.c1_bits33[pos + 4] + c1_arr_33[symbol].length + signs; + } - std::vector c1_min_bits(static_cast(max_possible_bv + 1), 0xFFFFFFFF); - std::vector c1_best_is_33(static_cast(max_possible_bv + 1), false); - - for (int bv = last_bv_pair; bv <= max_possible_bv; ++bv) { - int c1_bits_32 = 0, c1_bits_33 = 0; - int cur = bv * 2; - bool c1_possible = true; - while (cur <= 572) { - if (cur >= last_nonzero_coeff) break; - int v = coeffs[cur], w = coeffs[cur+1], x = coeffs[cur+2], y = coeffs[cur+3]; - int symbol = ((v != 0) << 3) | ((w != 0) << 2) | ((x != 0) << 1) | (y != 0); - if (!c1_arr_32[symbol].valid || c1_arr_32[symbol].length == 0) { c1_possible = false; break; } + // Boundary positions in pairs, and the order the accumulator reaches them. + // Short blocks put region0's boundary somewhere that is not a long-band + // boundary at all, so it gets a slot of its own. + int boundary[kNumBands + 1]; + for (int k = 0; k < kNumBands; ++k) boundary[k] = sf_bands[k] / 2; + boundary[kNumBands] = get_sf_bands_short(samplerate)[3] / 2 * 3; + int order[kNumBands + 1]; + for (int k = 0; k <= kNumBands; ++k) order[k] = k; + for (int i = 1; i <= kNumBands; ++i) { // insertion sort: only the short slot is out of place + const int s = order[i]; + int j = i - 1; + while (j >= 0 && boundary[order[j]] > boundary[s]) { order[j + 1] = order[j]; --j; } + order[j + 1] = s; + } - c1_bits_32 += c1_arr_32[symbol].length; - c1_bits_33 += c1_arr_33[symbol].length; + std::fill(std::begin(sc.acc), std::end(sc.acc), 0); + std::fill(std::begin(sc.row_valid), std::end(sc.row_valid), false); + std::fill(std::begin(sc.head_done), std::end(sc.head_done), false); + std::fill(std::begin(sc.tail_bv), std::end(sc.tail_bv), -1); + for (auto& row : sc.mid_done) std::fill(std::begin(row), std::end(row), false); + for (auto& p : sc.prefix) p = PrefixSplit{}; - if (v != 0) { c1_bits_32++; c1_bits_33++; } - if (w != 0) { c1_bits_32++; c1_bits_33++; } - if (x != 0) { c1_bits_32++; c1_bits_33++; } - if (y != 0) { c1_bits_32++; c1_bits_33++; } + // The accumulator walks the pairs once, in step with big_values, snapshotting + // each boundary as it passes: every prefix the search can ask about is either + // behind it or exactly at it. + int pos = 0, next_snap = 0; + auto advance = [&](const int to) { + while (next_snap <= kNumBands) { + const int slot = order[next_snap]; + const int at = boundary[slot]; + if (at > to) break; + if (at > pos) { accumulate_costs(sc.acc, sc.keys + pos, at - pos, ct); pos = at; } + std::copy(std::begin(sc.acc), std::end(sc.acc), std::begin(sc.rows[slot])); + sc.row_valid[slot] = true; + ++next_snap; + } + if (to > pos) { accumulate_costs(sc.acc, sc.keys + pos, to - pos, ct); pos = to; } + }; - cur += 4; + int bv = 0; + auto span_to_bv = [&](const int slot) { + if (sc.tail_bv[slot] != bv) { + sc.tail[slot] = best_span(sc.rows[slot], sc.acc); + sc.tail_bv[slot] = bv; } - if (c1_possible) { - bool use_33 = c1_bits_33 < c1_bits_32; - c1_min_bits[bv] = static_cast(use_33 ? c1_bits_33 : c1_bits_32); - c1_best_is_33[bv] = use_33; + return sc.tail[slot]; + }; + auto span_head = [&](const int slot) { + if (!sc.head_done[slot]) { + sc.head[slot] = best_span(sc.rows[0], sc.rows[slot]); + sc.head_done[slot] = true; } - } + return sc.head[slot]; + }; + auto span_mid = [&](const int a, const int b) { + if (!sc.mid_done[a][b]) { + sc.mid[a][b] = best_span(sc.rows[a], sc.rows[b]); + sc.mid_done[a][b] = true; + } + return sc.mid[a][b]; + }; + + // The best two-region cover below a boundary. Both spans it considers are + // between snapshots, so neither moves with big_values and the answer is + // computed the first time a candidate can reach the boundary at all. On a tie + // the lower region0 boundary wins, which is the order the enumeration it + // replaces would have found them in. + auto span_prefix = [&](const int k1) -> const PrefixSplit& { + PrefixSplit& p = sc.prefix[k1]; + if (p.done) return p; + p.done = true; + for (int k0 = std::max(1, k1 - 8); k0 <= std::min(k1 - 1, 16); ++k0) { + const uint32_t packed0 = span_head(k0); + const uint32_t bits0 = packed0 >> kCostShift; + if (bits0 >= static_cast(kPenalty)) continue; + const uint32_t packed1 = span_mid(k0, k1); + const uint32_t bits1 = packed1 >> kCostShift; + if (bits1 >= static_cast(kPenalty)) continue; + const int32_t total = static_cast(bits0 + bits1); + if (!p.ok || total < p.bits) { + p.ok = true; + p.bits = total; + p.k0 = static_cast(k0); + p.t0 = static_cast(packed0 & 31); + p.t1 = static_cast(packed1 & 31); + } + } + return p; + }; + if (orig_config.window_switching_flag) { // region boundaries are fixed by the standard for short/mixed blocks: // pure short (block_type=2, mixed=0): region0 = (sf_bands_short[3]/2)*3 pairs // mixed or start/end blocks: region0 = sf_bands[8]/2 pairs // only two tables (table0, table1); no region2; region0_count/region1_count unused. - int region0_boundary; - if (orig_config.block_type == 2 && !orig_config.mixed_block_flag) { - const auto sf_bands_s = get_sf_bands_short(samplerate); - region0_boundary = (sf_bands_s[3] / 2) * 3; - } else { - region0_boundary = sf_bands[8] / 2; - } + const int slot = (orig_config.block_type == 2 && !orig_config.mixed_block_flag) ? kNumBands : 8; + const int region0_boundary = boundary[slot]; - for (int bv = last_bv_pair; bv <= max_possible_bv; ++bv) { - if (c1_min_bits[bv] == 0xFFFFFFFF) continue; + for (bv = last_bv_pair; bv <= max_possible_bv; ++bv) { + if (!sc.c1_ok[2 * bv]) continue; + const bool use_33 = sc.c1_bits33[2 * bv] < sc.c1_bits32[2 * bv]; + const uint32_t c1_bits = static_cast(use_33 ? sc.c1_bits33[2 * bv] : sc.c1_bits32[2 * bv]); + advance(bv); - const int r0_end = std::min(bv, region0_boundary); - int t0; - const uint32_t r0_bits = get_best_region(0, r0_end, t0); - if (r0_bits >= 10000) continue; + // region0 saturating at big_values leaves region1 empty, which the + // original costed as zero bits with table 0. + const bool clip0 = region0_boundary >= bv; + const uint32_t packed0 = clip0 ? span_to_bv(0) : span_head(slot); + const uint32_t r0_bits = packed0 >> kCostShift; + if (r0_bits >= static_cast(kPenalty)) continue; - int t1; - const uint32_t r1_bits = get_best_region(r0_end, bv, t1); - if (r1_bits >= 10000) continue; + const uint32_t packed1 = clip0 ? 0u : span_to_bv(slot); + const uint32_t r1_bits = packed1 >> kCostShift; + if (r1_bits >= static_cast(kPenalty)) continue; - const uint32_t total = r0_bits + r1_bits + c1_min_bits[bv]; + const uint32_t total = r0_bits + r1_bits + c1_bits; if (total < min_total_bits) { min_total_bits = total; best = { orig_config.region0_count, orig_config.region1_count, static_cast(bv), - static_cast(t0), - static_cast(t1), + static_cast(packed0 & 31), + static_cast(packed1 & 31), uint8_t{0}, - c1_best_is_33[bv], + use_33, true, orig_config.block_type, orig_config.mixed_block_flag @@ -314,51 +908,98 @@ HuffmanConfig HuffmanOptimizer::find_best_config( } } } else { - for (int bv = last_bv_pair; bv <= max_possible_bv; ++bv) { - if (c1_min_bits[bv] == 0xFFFFFFFF) continue; - - for (int r0_idx = 0; r0_idx < 16; ++r0_idx) { - int r0_end = std::min(bv, sf_bands[r0_idx + 1] / 2); - int t0; - uint32_t r0_bits = get_best_region(0, r0_end, t0); - if (r0_bits >= 10000) continue; - - for (int r1_idx = 0; r1_idx < 8; ++r1_idx) { - int idx = std::min(r0_idx + r1_idx + 2, static_cast(sf_bands.size()) - 1); - int r1_end = std::min(bv, sf_bands[idx] / 2); - int t1; - uint32_t r1_bits = get_best_region(r0_end, r1_end, t1); - if (r1_bits >= 10000) continue; - - int t2; - uint32_t r2_bits = get_best_region(r1_end, bv, t2); - if (r2_bits >= 10000) continue; - - uint32_t total = r0_bits + r1_bits + r2_bits + c1_min_bits[bv]; - if (total < min_total_bits) { - min_total_bits = total; - best = { - static_cast(r0_idx), - static_cast(r1_idx), - static_cast(bv), - static_cast(t0), - static_cast(t1), - static_cast(t2), - c1_best_is_33[bv], - false, - uint8_t{0}, - false - }; - } + // Candidates are enumerated by shape rather than by (region0_count, + // region1_count). Every candidate splits the pairs below big_values into + // one, two or three regions at band boundaries, and which shape a + // (region0_count, region1_count) pair produces depends only on how many + // boundaries lie below big_values: + // + // nTail is that count, and because the boundaries are sorted, + // "boundary[k] >= big_values" — the test that used to saturate the two + // loops — is exactly "k >= nTail". + // + // What that buys is the three-region case. Regions 0 and 1 cover + // everything below their upper boundary and neither moves with + // big_values, so the cheapest pair of them is settled once per granule per + // boundary and only the tail is recomputed per candidate. The old nested + // loops re-derived it for every candidate: 113 combinations per + // big_values, where this is one per boundary. + // + // Ties are resolved exactly as the nested loops did — lowest total, then + // lowest region0_count, then lowest region1_count — which is why the + // prefix search below prefers the lowest region0 boundary on a tie and why + // the shapes are offered in that order. + int n_tail = 0; + for (bv = last_bv_pair; bv <= max_possible_bv; ++bv) { + if (!sc.c1_ok[2 * bv]) continue; + const bool use_33 = sc.c1_bits33[2 * bv] < sc.c1_bits32[2 * bv]; + const uint32_t c1_bits = static_cast(use_33 ? sc.c1_bits33[2 * bv] : sc.c1_bits32[2 * bv]); + advance(bv); + // big_values only grows, so this is carried across candidates rather + // than recounted. + while (n_tail < kNumBands && boundary[n_tail] < bv) ++n_tail; - // sf_bands is monotone, so r1_end saturates at bv and stays there for - // every larger r1_idx: region1/region2 would be identical, so stop here. - if (r1_end == bv) break; + uint32_t cand_total = UINT32_MAX; + int cand_r0 = 0, cand_r1 = 0, cand_t0 = 0, cand_t1 = 0, cand_t2 = 0; + const auto offer = [&](const uint32_t total, const int r0_idx, const int r1_idx, + const int t0, const int t1, const int t2) { + if (total > cand_total) return; + if (total == cand_total && (r0_idx > cand_r0 || (r0_idx == cand_r0 && r1_idx >= cand_r1))) return; + cand_total = total; + cand_r0 = r0_idx; cand_r1 = r1_idx; + cand_t0 = t0; cand_t1 = t1; cand_t2 = t2; + }; + + // One region: the first region0 boundary at or beyond big_values + // swallows every pair, and no larger region0_count can do anything + // different. Its region1_count is 0, since the inner loop broke at once. + const int only = std::max(0, n_tail - 1); + if (only < 16) { + const uint32_t packed = span_to_bv(0); + if ((packed >> kCostShift) < static_cast(kPenalty)) { + offer((packed >> kCostShift) + c1_bits, only, 0, static_cast(packed & 31), 0, 0); } + } + + // Two regions: region1 runs from a boundary up to big_values, which + // needs the smallest region1_count whose own boundary reaches it. + for (int k0 = 1; k0 <= std::min(n_tail - 1, 16); ++k0) { + const int r1_idx = std::max(0, n_tail - k0 - 1); + if (r1_idx > 7) continue; // region1_count cannot stretch that far + const uint32_t packed0 = span_head(k0); + if ((packed0 >> kCostShift) >= static_cast(kPenalty)) continue; + const uint32_t packed1 = span_to_bv(k0); + if ((packed1 >> kCostShift) >= static_cast(kPenalty)) continue; + offer((packed0 >> kCostShift) + (packed1 >> kCostShift) + c1_bits, + k0 - 1, r1_idx, static_cast(packed0 & 31), static_cast(packed1 & 31), 0); + } + + // Three regions: the pairs below a boundary are covered by regions 0 + // and 1 at their settled best, and region2 runs from there to + // big_values. + for (int k1 = 2; k1 < n_tail; ++k1) { + const PrefixSplit& p = span_prefix(k1); + if (!p.ok) continue; + const uint32_t packed2 = span_to_bv(k1); + if ((packed2 >> kCostShift) >= static_cast(kPenalty)) continue; + offer(static_cast(p.bits) + (packed2 >> kCostShift) + c1_bits, + p.k0 - 1, k1 - p.k0 - 1, p.t0, p.t1, static_cast(packed2 & 31)); + } - // same saturation argument for r0_end: once it hits bv, every larger r0_idx - // reproduces the exact same r0_bits/t0 and an immediately-saturated inner loop. - if (r0_end == bv) break; + if (cand_total < min_total_bits) { + min_total_bits = cand_total; + best = { + static_cast(cand_r0), + static_cast(cand_r1), + static_cast(bv), + static_cast(cand_t0), + static_cast(cand_t1), + static_cast(cand_t2), + use_33, + false, + uint8_t{0}, + false + }; } } } @@ -366,25 +1007,63 @@ HuffmanConfig HuffmanOptimizer::find_best_config( return best; } +int HuffmanOptimizer::probe_bits() { return kProbeBits; } + +bool HuffmanOptimizer::probe_pair(const int table_idx, const unsigned prefix, + int& x, int& y, int& length, int& signs) { + const PairEntry e = pair_probes()[table_idx][prefix]; + if (e >= kPairSlow) return false; + x = e & 0xF; + y = e >> 4 & 0xF; + length = e >> 8 & 0xF; + signs = (e >> 12 & 1) + (e >> 13 & 1); + return true; +} + void HuffmanOptimizer::encode_quantized_coefficients( - const std::vector& coeffs, const HuffmanConfig& config, + const Spectrum& coeffs, const HuffmanConfig& config, BitstreamWriter& writer, const uint32_t samplerate) { int cur = 0; const auto sf_bands = get_sf_bands(samplerate); - auto encode_pair = [&](const int x, const int y, const uint8_t table_idx) { - if (table_idx == 0) return; - const int linbits = huffman_tables[table_idx].linbits; - const int abs_x = std::min(15, std::abs(x)); - const int abs_y = std::min(15, std::abs(y)); + // The accumulator is carried in these two locals for the whole granule and + // handed back at the end, so appending a field is a shift and an or in + // registers rather than a call into the writer and a round trip through its + // state. A pair's codeword, escape magnitudes and signs come to at most 47 + // bits, so they are assembled in one word and committed in one step. + uint64_t acc; + int nacc; + writer.pending(acc, nacc); + auto emit = [&](const uint64_t word, const int n) { + if (nacc + n > 64) writer.store(acc, nacc); + acc |= (word & ((uint64_t{1} << n) - 1)) << (64 - nacc - n); + nacc += n; + }; + + auto encode_pair = [&](const int x, const int y, const std::array& map, const int linbits) { + const int mag_x = std::abs(x), mag_y = std::abs(y); + const int abs_x = std::min(15, mag_x), abs_y = std::min(15, mag_y); + const SymbolCode& code = map[static_cast(abs_x << 4 | abs_y)]; + if (!code.valid) return; + uint64_t word = code.code; + int n = code.length; + if (abs_x == 15 && linbits > 0) { word = word << linbits | static_cast(mag_x - 15); n += linbits; } + if (x != 0) { word = word << 1 | (x < 0 ? 1u : 0u); n += 1; } + if (abs_y == 15 && linbits > 0) { word = word << linbits | static_cast(mag_y - 15); n += linbits; } + if (y != 0) { word = word << 1 | (y < 0 ? 1u : 0u); n += 1; } + emit(word, n); + }; + + // Everything that depends only on the table is settled per region: the + // per-pair loop then touches one code entry and nothing else global. + auto encode_region = [&](const int pairs, const uint8_t table_idx) { + if (table_idx == 0) { cur += 2 * pairs; return; } // table 0 codes nothing const auto& map = HuffmanTableManager::get_encoding_map(table_idx); - const int idx = (abs_x << 4) | abs_y; - if (!map[idx].valid) return; - writer.write_bits(map[idx].code, map[idx].length); - if (abs_x == 15 && linbits > 0) writer.write_bits(static_cast(std::abs(x) - 15), linbits); - if (x != 0) writer.write_bits(x < 0 ? 1u : 0u, 1); - if (abs_y == 15 && linbits > 0) writer.write_bits(static_cast(std::abs(y) - 15), linbits); - if (y != 0) writer.write_bits(y < 0 ? 1u : 0u, 1); + const int linbits = huffman_tables[table_idx].linbits; + for (int i = 0; i < pairs; ++i) { + encode_pair(coeffs[cur], coeffs[cur + 1], map, linbits); + cur += 2; + } }; int r0_pairs, r1_pairs, r2_pairs; @@ -405,9 +1084,9 @@ void HuffmanOptimizer::encode_quantized_coefficients( r2_pairs = static_cast(config.big_values) - r0_pairs - r1_pairs; } - for (int i = 0; i < r0_pairs; ++i) { encode_pair(coeffs[cur], coeffs[cur+1], config.table0); cur += 2; } - for (int i = 0; i < r1_pairs; ++i) { encode_pair(coeffs[cur], coeffs[cur+1], config.table1); cur += 2; } - for (int i = 0; i < r2_pairs; ++i) { encode_pair(coeffs[cur], coeffs[cur+1], config.table2); cur += 2; } + encode_region(r0_pairs, config.table0); + encode_region(r1_pairs, config.table1); + encode_region(r2_pairs, config.table2); int last_nonzero = 0; for (int i = 575; i >= 0; --i) { @@ -419,18 +1098,29 @@ void HuffmanOptimizer::encode_quantized_coefficients( while (cur <= 572) { if (cur >= last_nonzero) break; const int v = coeffs[cur]; + const int w = coeffs[cur+1]; const int x = coeffs[cur+2]; const int y = coeffs[cur+3]; - const int w = coeffs[cur+1]; const int symbol = ((v != 0) << 3) | ((w != 0) << 2) | ((x != 0) << 1) | (y != 0); - if (!c1_map[symbol].valid || c1_map[symbol].length == 0) break; - writer.write_bits(c1_map[symbol].code, c1_map[symbol].length); - if (v != 0) writer.write_bits(v < 0 ? 1u : 0u, 1); - if (w != 0) writer.write_bits(w < 0 ? 1u : 0u, 1); - if (x != 0) writer.write_bits(x < 0 ? 1u : 0u, 1); - if (y != 0) writer.write_bits(y < 0 ? 1u : 0u, 1); + const SymbolCode& code = c1_map[static_cast(symbol)]; + if (!code.valid || code.length == 0) break; + // The quadruple's codeword and its signs, at most ten bits, go over in one + // step: a zero coefficient contributes no sign bit and shifts nothing. + uint64_t word = code.code; + int n = code.length; + if (v != 0) { word = word << 1 | (v < 0 ? 1u : 0u); ++n; } + if (w != 0) { word = word << 1 | (w < 0 ? 1u : 0u); ++n; } + if (x != 0) { word = word << 1 | (x < 0 ? 1u : 0u); ++n; } + if (y != 0) { word = word << 1 | (y < 0 ? 1u : 0u); ++n; } + emit(word, n); cur += 4; } + + writer.resume(acc, nacc); } } // namespace mp3packer + +/// Gives the tests the tree a big-value table is built from, so that the probe can +/// be held against it without publishing the table header. +const int16_t* huffman_tree_for_test(const int idx) { return mp3packer::huffman_tables[idx].table; } diff --git a/src/core/include/bitstream.hpp b/src/core/include/bitstream.hpp index 0ce123d..dc53acf 100644 --- a/src/core/include/bitstream.hpp +++ b/src/core/include/bitstream.hpp @@ -1,14 +1,50 @@ #ifndef MP3PACKERCPP_BITSTREAM_HPP #define MP3PACKERCPP_BITSTREAM_HPP -#include -#include +#include #include +#include +#include +#include + +#if defined(_MSC_VER) +#include // _byteswap_uint64 +#endif namespace mp3packer { +/** + * @brief Loads eight bytes most-significant-byte first. + */ +inline uint64_t load_be64(const uint8_t* p) { + uint64_t v; + std::memcpy(&v, p, sizeof v); +#if defined(_MSC_VER) + return _byteswap_uint64(v); +#else + return __builtin_bswap64(v); +#endif +} + +/** + * @brief Stores eight bytes most-significant-byte first. + */ +inline void store_be64(uint8_t* p, const uint64_t v) { +#if defined(_MSC_VER) + const uint64_t be = _byteswap_uint64(v); +#else + const uint64_t be = __builtin_bswap64(v); +#endif + std::memcpy(p, &be, sizeof be); +} + /** * @brief Utility class for reading arbitrary bits from a byte stream. + * + * Both directions of the bitstream work through a single 64-bit access per + * operation rather than walking bytes: no MP3 field is wider than 32 bits, and a + * whole coefficient pair fits in 47, so one load or read-modify-write covers any + * of them. */ class BitstreamReader { public: @@ -16,7 +52,44 @@ class BitstreamReader { * @brief Constructs a BitstreamReader wrapping the provided data vector. * @param data Reference to the vector of bytes to read from. */ - explicit BitstreamReader(const std::vector& data) : data_(data) {} + explicit BitstreamReader(const std::vector& data) + : BitstreamReader(data.data(), data.size()) {} + + /** + * @brief Constructs a BitstreamReader over a plain buffer. + * @param data Start of the bytes to read. + * @param size How many of them there are. + */ + BitstreamReader(const uint8_t* data, const size_t size) + : data_(data), size_(size) { + // The tail is mirrored into a zero-filled buffer so that a read near the + // end of the data is the same load as any other: reads past the end yield + // zero bits, which is how the byte-walking reader behaved, and what a + // truncated final frame needs. + last_word_ = static_cast(size_) - 8; + tail_from_ = size_ > 7 ? size_ - 7 : 0; + std::memset(pad_, 0, sizeof pad_); + if (size_ > tail_from_) std::memcpy(pad_, data_ + tail_from_, size_ - tail_from_); + } + + /** + * @brief Returns the 64 bits at an explicit bit position without consuming + * them, most significant bit first, zero-filled past the end. + * + * Only the top 57 bits are guaranteed to be present, which is more than the 47 + * a single coefficient pair can occupy. Taking a position rather than using the + * stored one lets a caller decoding a run of symbols keep its position in a + * register instead of round-tripping it through the reader. + */ + [[nodiscard]] uint64_t peek_at(const size_t pos) const { + size_t idx = pos >> 3; + const uint8_t* b = data_; + if (static_cast(idx) > last_word_) { + b = pad_; + idx = std::min(idx - tail_from_, static_cast(7)); + } + return load_be64(b + idx) << (pos & 7); + } /** * @brief Reads a specified number of bits from the stream. @@ -27,25 +100,26 @@ class BitstreamReader { if (num_bits == 0) return 0; if (num_bits > 32) throw std::invalid_argument("Cannot read more than 32 bits"); - uint32_t result = 0; - for (int i = 0; i < num_bits; ++i) { - const size_t byte_idx = bit_pos_ / 8; - const int bit_idx = 7 - (bit_pos_ % 8); - - if (byte_idx >= data_.size()) { - result <<= (num_bits - i); - bit_pos_ += static_cast(num_bits - i); - return result; - } - - if (data_[byte_idx] & (1 << bit_idx)) { - result |= (1 << (num_bits - 1 - i)); - } - bit_pos_++; + uint32_t result = static_cast(peek_at(bit_pos_) >> (64 - num_bits)); + const size_t total = size_ * 8; + if (bit_pos_ + static_cast(num_bits) > total) { + // A read that runs off the end of the data keeps the byte-walking + // reader's behaviour: the bits it did find stay at the top of the + // field and are then shifted up again by the number it did not, which + // is not the same as zero-filling them. Nothing downstream depends on + // the value — a frame reaching past the reservoir cannot be recompressed + // either way — but the bytes it eventually writes do. + const size_t avail = bit_pos_ < total ? total - bit_pos_ : 0; + const int missing = num_bits - static_cast(avail); + result = missing >= 32 ? 0 : result << missing; } + bit_pos_ += static_cast(num_bits); return result; } + /// Total number of bits in the underlying data. + [[nodiscard]] size_t size_bits() const { return size_ * 8; } + /** * @brief Seeks to a specific bit position in the stream. * @param pos Absolute bit position to seek to. @@ -59,12 +133,20 @@ class BitstreamReader { [[nodiscard]] size_t tell_bit() const { return bit_pos_; } private: - const std::vector& data_; ///< Reference to the byte stream being read - size_t bit_pos_ = 0; ///< Current absolute bit position + const uint8_t* data_; ///< Start of the byte stream being read + size_t size_; ///< Length of the byte stream + size_t bit_pos_ = 0; ///< Current absolute bit position + ptrdiff_t last_word_; ///< Highest byte index a whole word can be loaded from + size_t tail_from_; ///< Byte index that pad_[0] stands for + uint8_t pad_[16]; ///< Zero-filled copy of the final bytes }; /** * @brief Utility class for writing arbitrary bits to a dynamic byte stream. + * + * Pending bits are held in a register-sized accumulator and reach the buffer + * eight bytes at a time, so appending a field costs a shift and an or; nothing is + * ever read back out of the buffer. */ class BitstreamWriter { public: @@ -78,37 +160,111 @@ class BitstreamWriter { void write_bits(const uint32_t value, const int num_bits) { if (num_bits == 0) return; if (num_bits > 32) throw std::invalid_argument("Cannot write more than 32 bits"); + put(static_cast(value), num_bits); + } + + /** + * @brief Appends the low num_bits bits of value, most significant first. + * + * A store leaves under eight bits pending, so anything up to 57 bits lands in a + * single accumulator window; that covers every field in the bitstream, a + * coefficient pair included. + */ + void put(const uint64_t value, const int num_bits) { + if (accumulated_ + num_bits > 64) store(accumulator_, accumulated_); + accumulator_ |= (value & ((uint64_t{1} << num_bits) - 1)) << (64 - accumulated_ - num_bits); + accumulated_ += num_bits; + } - for (int i = 0; i < num_bits; ++i) { - const size_t byte_idx = bit_pos_ / 8; - const int bit_idx = 7 - (bit_pos_ % 8); + /** + * @brief Moves n bits from a reader to this writer without interpreting them. + * + * Used for scalefactors, which are re-emitted verbatim: their layout depends on + * scalefac_compress tables we only need to size, never to decode. The caller + * must have established that the whole span is inside the reader's data, since + * a read past the end has a value only field-by-field reading reproduces. + */ + void copy_from(BitstreamReader& reader, size_t num_bits) { + while (num_bits >= 32) { + put(reader.read_bits(32), 32); + num_bits -= 32; + } + if (num_bits > 0) put(reader.read_bits(static_cast(num_bits)), static_cast(num_bits)); + } - if (byte_idx >= data_.size()) { - data_.push_back(0); - } + /** + * @brief Takes the pending bits so a caller can append to them in registers. + * + * put() keeps its state in memory, so a long run of fields pays a load and a + * store of the accumulator per field, plus the store-to-load turnaround between + * one field and the next. A caller that takes the pending bits here, spills with + * store() whenever the next field would not fit, and hands them back with + * resume() pays none of that. The writer's own bits are not valid in between, so + * nothing else may write to it. + */ + void pending(uint64_t& acc, int& nacc) const { acc = accumulator_; nacc = accumulated_; } - if (value & (1 << (num_bits - 1 - i))) { - data_[byte_idx] |= (1 << bit_idx); - } - bit_pos_++; + /** + * @brief Commits the accumulator's whole bytes, leaving under eight bits in it. + * + * Eight bytes are always stored and only the complete ones committed, so the + * next store overwrites the remainder: every byte the writer returns is one it + * wrote itself, which is why the buffer needs slack but needs no zeroing. + */ + void store(uint64_t& acc, int& nacc) { + if (buf_.size() < committed_ + kSlack) { + buf_.resize(std::max(committed_ + kSlack, buf_.size() * 2)); } + store_be64(buf_.data() + committed_, acc); + const int whole = nacc >> 3; + committed_ += static_cast(whole); + // A field that exactly fills the accumulator leaves nothing over, and + // shifting a 64-bit value by 64 is undefined rather than zero. + acc = whole == 8 ? 0 : acc << whole * 8; + nacc -= whole * 8; } + /** + * @brief Returns the accumulator to the writer. + */ + void resume(const uint64_t acc, const int nacc) { accumulator_ = acc; accumulated_ = nacc; } + /** * @brief Gets the underlying byte vector containing the written data. - * @return Constant reference to the data vector. + * + * The pending bits are materialised without being committed, so a caller can + * read back what it has written so far and then carry on writing — which the + * verification pass does, once per granule. + * + * @return Constant reference to the data vector, zero-padded to a whole byte. */ - [[nodiscard]] const std::vector& data() const { return data_; } + [[nodiscard]] const std::vector& data() { + if (accumulated_ > 0) { + uint64_t acc = accumulator_; + int nacc = accumulated_; + const size_t committed = committed_; + store(acc, nacc); + committed_ = committed; // the partial byte stays pending + } + buf_.resize((tell_bit() + 7) / 8); + return buf_; + } /** * @brief Gets the current bit position. * @return The absolute bit index the writer is currently at. */ - [[nodiscard]] size_t tell_bit() const { return bit_pos_; } + [[nodiscard]] size_t tell_bit() const { return committed_ * 8 + static_cast(accumulated_); } private: - std::vector data_; ///< Dynamic byte stream being written - size_t bit_pos_ = 0; ///< Current absolute bit position + /// A store always writes a whole word, of which it commits only the complete + /// bytes, so the buffer carries this much room past the output. + static constexpr size_t kSlack = 8; + + std::vector buf_; ///< Committed bytes, plus slack for the next store + uint64_t accumulator_ = 0; ///< Pending bits, most significant first + int accumulated_ = 0; ///< How many of the accumulator's bits are pending + size_t committed_ = 0; ///< Bytes already committed to buf_ }; } // namespace mp3packer diff --git a/src/core/include/huffman.hpp b/src/core/include/huffman.hpp index ecaf179..1c796bc 100644 --- a/src/core/include/huffman.hpp +++ b/src/core/include/huffman.hpp @@ -2,11 +2,23 @@ #define MP3PACKERCPP_HUFFMAN_HPP #include "bitstream.hpp" +#include #include #include namespace mp3packer { +/// Number of quantized spectral values in one granule. +constexpr int kNumCoefficients = 576; + +/** + * @brief One granule's quantized coefficients. + * + * A fixed array rather than a vector: the coder runs once per granule, hundreds + * of thousands of times per second, and the caller reuses one buffer per worker. + */ +using Spectrum = std::array; + /** * @brief Represents the configuration of Huffman regions and tables for a granule. */ @@ -36,15 +48,16 @@ class HuffmanOptimizer { * The part2_3 bit limit is checked only at the start of each big-values pair and * each count1 quad, never mid-symbol. This matches mp3packer's decode behaviour. * + * @param coeffs Destination for the 576 coefficients, zero-filled past the + * last decoded value. * @param config The original Huffman configuration from the side info. * @param reader Bitstream reader positioned at the start of the Huffman data * (i.e. after scalefactors, at bit offset part2_length). * @param samplerate Sampling rate of the frame in Hz. * @param max_huffman_bits Maximum bits to consume (part2_3_length - part2_length). * Pass -1 to decode until big_values/count1 are exhausted. - * @return A vector of 576 integer coefficients (zero-padded beyond the last decoded value). */ - static std::vector decode_quantized_coefficients(const HuffmanConfig& config, BitstreamReader& reader, uint32_t samplerate, int max_huffman_bits = -1); + static void decode_quantized_coefficients(Spectrum& coeffs, const HuffmanConfig& config, BitstreamReader& reader, uint32_t samplerate, int max_huffman_bits = -1); /** * @brief Performs brute-force search to find the optimal Huffman table combination. @@ -53,7 +66,7 @@ class HuffmanOptimizer { * @param samplerate Sampling rate of the frame in Hz. * @return A new HuffmanConfig that yields the smallest bit size for the coefficients. */ - static HuffmanConfig find_best_config(const std::vector& coeffs, const HuffmanConfig& orig_config, uint32_t samplerate); + static HuffmanConfig find_best_config(const Spectrum& coeffs, const HuffmanConfig& orig_config, uint32_t samplerate); /** * @brief Re-encodes the coefficients into a bitstream using a given configuration. @@ -62,7 +75,27 @@ class HuffmanOptimizer { * @param writer Bitstream writer to output the compressed bits. * @param samplerate Sampling rate of the frame in Hz. */ - static void encode_quantized_coefficients(const std::vector& coeffs, const HuffmanConfig& config, BitstreamWriter& writer, uint32_t samplerate); + static void encode_quantized_coefficients(const Spectrum& coeffs, const HuffmanConfig& config, BitstreamWriter& writer, uint32_t samplerate); + + /** + * @brief Resolves one prefix of one big-value table exactly as the decoder does. + * + * Exposed for the tests: the decoder trusts the probe table for 99% of what it + * reads, and one wrong entry would decode a pair to the wrong magnitudes or + * desynchronise the region, so every entry is held against a walk of the tree. + * + * @param table_idx Big-value table index (0-31). + * @param prefix The next probe_bits() bits of the stream, most significant first. + * @param x Clamped magnitude of the first value, or unset if deferred. + * @param y Clamped magnitude of the second value, or unset if deferred. + * @param length Codeword length in bits, or unset if deferred. + * @param signs Number of sign bits the pair takes, or unset if deferred. + * @return False if the codeword is longer than the probe, which defers to a tree walk. + */ + static bool probe_pair(int table_idx, unsigned prefix, int& x, int& y, int& length, int& signs); + + /** @brief How many bits of the stream index the big-value probe. */ + static int probe_bits(); }; diff --git a/src/core/include/mp3_reader.hpp b/src/core/include/mp3_reader.hpp index a068626..dc15d68 100644 --- a/src/core/include/mp3_reader.hpp +++ b/src/core/include/mp3_reader.hpp @@ -2,9 +2,9 @@ #define MP3PACKERCPP_MP3_READER_HPP #include "types.hpp" -#include -#include #include +#include +#include namespace mp3packer { @@ -13,6 +13,10 @@ namespace mp3packer { * * It is responsible for bypassing ID3 tags, finding frame headers, * and parsing the side info to assemble complete Mp3Frame structs. + * + * The file is read once into memory and parsed from there: a frame needs three or + * four reads of a few bytes each, and going to the stream for them cost more than + * the parsing did. */ class Mp3Reader { public: @@ -30,6 +34,11 @@ class Mp3Reader { */ std::optional read_next_frame(); + /** + * @brief The whole file, which frames' payload spans point into. + */ + [[nodiscard]] const std::vector& data() const { return data_; } + /** * @brief Retrieves any junk data found before the first MP3 frame (e.g. ID3v2 tag). * @return Constant reference to the prefix junk data. @@ -43,10 +52,27 @@ class Mp3Reader { [[nodiscard]] const std::vector& get_end_junk() const { return end_junk_; } private: - std::ifstream file_; ///< File stream of the MP3 file - size_t file_size_; ///< Total size of the file in bytes + std::vector data_; ///< Whole file contents + size_t pos_ = 0; ///< Read cursor into data_ + bool exhausted_ = false; ///< Set once a read has run off the end std::vector start_junk_; ///< Buffer storing ID3v2 or pre-audio data std::vector end_junk_; ///< Buffer storing ID3v1 or post-audio data + + /** + * @brief Copies n bytes from the cursor, zero-filling anything past the end. + * + * Mirrors what a short std::ifstream::read left behind: the bytes that were + * there, the rest of a zero-initialised buffer untouched, and every later read + * failing too. + * @return False if the data ran out. + */ + bool take(uint8_t* dst, size_t n); + + /** + * @brief Advances the cursor over n bytes without copying them. + * @return False if the data ran out, in which case later reads fail too. + */ + bool skip(size_t n); /** * @brief Parses a 32-bit header into an Mp3Header struct. diff --git a/src/core/include/types.hpp b/src/core/include/types.hpp index a298e1b..60ad975 100644 --- a/src/core/include/types.hpp +++ b/src/core/include/types.hpp @@ -1,7 +1,7 @@ #ifndef MP3PACKERCPP_TYPES_HPP #define MP3PACKERCPP_TYPES_HPP -#include +#include #include namespace mp3packer { @@ -68,7 +68,6 @@ struct GrChInfo { bool preflag = false; ///< Pre-emphasis flag bool scalefac_scale = false; ///< Scale of the scalefactors bool count1table_select = false;///< True if count1 region uses Huffman table 33 (false → 32) - std::vector scalefactors{};///< Decoded scalefactors for the granule/channel }; /** @@ -83,15 +82,27 @@ struct SideInfo { GrChInfo gr[2][2]; ///< Granule and channel specific decoding info [granule][channel] }; +/// Largest side info there is: MPEG-1 stereo. +constexpr size_t kMaxSideInfoSize = 32; + /** * @brief Encapsulates a complete MP3 frame during the processing pipeline. + * + * A frame describes its bytes rather than owning copies of them. The reader holds + * the whole file, so the payload is a span into that, and the side info — at most + * 32 bytes — sits inline. Copying both into vectors per frame cost two allocations + * and a copy of the entire audio for a program that only ever reads them. */ struct Mp3Frame { - Mp3Header header{}; ///< Parsed frame header - SideInfo side_info{}; ///< Parsed side information - std::vector side_info_raw{};///< Raw bytes of the side info section - std::vector main_data_raw{};///< Raw bytes of the main data (payload) - std::vector raw_bytes{}; ///< Complete frame bytes as read from file + Mp3Header header{}; ///< Parsed frame header + SideInfo side_info{}; ///< Parsed side information + + uint8_t side_info_raw[kMaxSideInfoSize] = {}; ///< Raw bytes of the side info section + uint8_t side_info_size = 0; ///< How many of those bytes there are + + size_t main_data_offset = 0; ///< Where the payload starts in the reader's data + size_t main_data_size = 0; ///< Declared payload length, which may run past the data + size_t main_data_avail = 0; ///< How much of it the file actually holds }; } // namespace mp3packer diff --git a/src/core/mp3_reader.cpp b/src/core/mp3_reader.cpp index b6bb69e..a041003 100644 --- a/src/core/mp3_reader.cpp +++ b/src/core/mp3_reader.cpp @@ -1,33 +1,63 @@ #include "include/mp3_reader.hpp" #include "include/logger.hpp" #include "include/bitstream.hpp" +#include +#include +#include #include namespace mp3packer { -Mp3Reader::Mp3Reader(const std::string& filename) : file_(filename, std::ios::binary), file_size_(0), start_junk_(), end_junk_() { - if (!file_) { +Mp3Reader::Mp3Reader(const std::string& filename) : start_junk_(), end_junk_() { + std::ifstream file(filename, std::ios::binary); + if (!file) { throw std::runtime_error("Could not open file: " + filename); } - file_.seekg(0, std::ios::end); - file_size_ = static_cast(file_.tellg()); - file_.seekg(0, std::ios::beg); + file.seekg(0, std::ios::end); + const auto size = file.tellg(); + file.seekg(0, std::ios::beg); + if (size > 0) { + data_.resize(static_cast(size)); + file.read(reinterpret_cast(data_.data()), size); + data_.resize(static_cast(file.gcount())); + } skip_id3v2_tag(); } -Mp3Reader::~Mp3Reader() { - if (file_.is_open()) { - file_.close(); +Mp3Reader::~Mp3Reader() = default; + +bool Mp3Reader::skip(const size_t n) { + if (exhausted_) return false; + if (data_.size() - pos_ < n) { + pos_ = data_.size(); + exhausted_ = true; + return false; } + pos_ += n; + return true; } -void Mp3Reader::skip_id3v2_tag() { - uint8_t header[10]; - if (!file_.read(reinterpret_cast(header), 10)) { - file_.clear(); - file_.seekg(0, std::ios::beg); - return; +bool Mp3Reader::take(uint8_t* dst, const size_t n) { + if (exhausted_) { + std::memset(dst, 0, n); + return false; } + const size_t avail = data_.size() - pos_; + if (avail < n) { + std::memcpy(dst, data_.data() + pos_, avail); + std::memset(dst + avail, 0, n - avail); + pos_ = data_.size(); + exhausted_ = true; + return false; + } + std::memcpy(dst, data_.data() + pos_, n); + pos_ += n; + return true; +} + +void Mp3Reader::skip_id3v2_tag() { + if (data_.size() < 10) return; + const uint8_t* header = data_.data(); if (header[0] == 'I' && header[1] == 'D' && header[2] == '3') { const uint32_t size = (static_cast(header[6] & 0x7F) << 21) | @@ -41,11 +71,10 @@ void Mp3Reader::skip_id3v2_tag() { } DEBUG_LOG("Found ID3v2 tag of size " << total_tag_size << " bytes. Storing in start_junk..."); - file_.seekg(0, std::ios::beg); start_junk_.resize(total_tag_size); - file_.read(reinterpret_cast(start_junk_.data()), total_tag_size); - } else { - file_.seekg(0, std::ios::beg); + // A tag claiming more than the file holds leaves the rest zero and the data + // exhausted, so no frames are found — as before. + take(start_junk_.data(), total_tag_size); } } @@ -120,7 +149,7 @@ uint16_t Mp3Reader::calculate_frame_size(const Mp3Header& header) { std::optional Mp3Reader::read_next_frame() { uint8_t buf[4]; - while (file_.read(reinterpret_cast(buf), 4)) { + while (take(buf, 4)) { uint32_t header_bits = (static_cast(buf[0]) << 24) | (static_cast(buf[1]) << 16) | (static_cast(buf[2]) << 8) | @@ -128,18 +157,17 @@ std::optional Mp3Reader::read_next_frame() { auto header = parse_header(header_bits); if (!header) { // invalid header: assume start of trailing junk (e.g. ID3v1 tag) - file_.seekg(-4, std::ios::cur); - size_t current_pos = static_cast(file_.tellg()); - if (current_pos < file_size_) { - size_t remaining = file_size_ - current_pos; - end_junk_.resize(remaining); - file_.read(reinterpret_cast(end_junk_.data()), static_cast(remaining)); + pos_ -= 4; + if (pos_ < data_.size()) { + const size_t remaining = data_.size() - pos_; + end_junk_.assign(data_.begin() + static_cast(pos_), data_.end()); + pos_ = data_.size(); DEBUG_LOG("Found " << remaining << " bytes of junk at end of stream (e.g. ID3v1). Stored in end_junk."); } return std::nullopt; } - DEBUG_LOG("Successfully parsed valid MP3 header: " << std::hex << header_bits << std::dec << " at stream pos " << (static_cast(file_.tellg()) - 4)); + DEBUG_LOG("Successfully parsed valid MP3 header: " << std::hex << header_bits << std::dec << " at stream pos " << (pos_ - 4)); Mp3Frame frame; header->bitrate.frame_size = calculate_frame_size(*header); frame.header = *header; @@ -148,15 +176,15 @@ std::optional Mp3Reader::read_next_frame() { ? (header->channel_mode == ChannelMode::Mono ? 17 : 32) : (header->channel_mode == ChannelMode::Mono ? 9 : 17); - uint8_t crc_bytes[2] = {0, 0}; if (header->has_crc) { - file_.read(reinterpret_cast(crc_bytes), 2); + uint8_t crc_bytes[2] = {0, 0}; + take(crc_bytes, 2); } - frame.side_info_raw.resize(static_cast(side_info_size)); - file_.read(reinterpret_cast(frame.side_info_raw.data()), side_info_size); + frame.side_info_size = static_cast(side_info_size); + take(frame.side_info_raw, static_cast(side_info_size)); - BitstreamReader side_reader(frame.side_info_raw); + BitstreamReader side_reader(frame.side_info_raw, static_cast(side_info_size)); SideInfo& si = frame.side_info; const int num_channels = (header->channel_mode == ChannelMode::Mono) ? 1 : 2; const int num_granules = (header->version == MpegVersion::MPEG1) ? 2 : 1; @@ -227,17 +255,14 @@ std::optional Mp3Reader::read_next_frame() { const int crc_size = header->has_crc ? 2 : 0; const int main_data_size = static_cast(header->bitrate.frame_size) - 4 - crc_size - side_info_size; - frame.main_data_raw.resize(static_cast(main_data_size)); - file_.read(reinterpret_cast(frame.main_data_raw.data()), main_data_size); - - frame.raw_bytes.reserve(static_cast(4 + crc_size + side_info_size + main_data_size)); - frame.raw_bytes.insert(frame.raw_bytes.end(), buf, buf + 4); - if (header->has_crc) { - frame.raw_bytes.push_back(crc_bytes[0]); - frame.raw_bytes.push_back(crc_bytes[1]); + // The payload stays where it is; a short one at the end of a truncated file + // is zero-filled by whoever reads it, as a short read used to leave it. + frame.main_data_offset = pos_; + frame.main_data_size = static_cast(main_data_size); + frame.main_data_avail = exhausted_ ? 0 : std::min(frame.main_data_size, data_.size() - pos_); + if (!skip(frame.main_data_size)) { + // ran off the end: later reads fail as before } - frame.raw_bytes.insert(frame.raw_bytes.end(), frame.side_info_raw.begin(), frame.side_info_raw.end()); - frame.raw_bytes.insert(frame.raw_bytes.end(), frame.main_data_raw.begin(), frame.main_data_raw.end()); return frame; } diff --git a/src/core/packer.cpp b/src/core/packer.cpp index b59ae5f..f1c0422 100644 --- a/src/core/packer.cpp +++ b/src/core/packer.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace mp3packer { @@ -17,8 +18,8 @@ namespace mp3packer { Packer::Packer() = default; // Takes the original raw side_info bytes and sets only the main_data_begin field. -static std::vector patch_mdb(const std::vector& raw, const MpegVersion version, const uint16_t new_mdb) { - std::vector out = raw; +static std::vector patch_mdb(const uint8_t* raw, const size_t size, const MpegVersion version, const uint16_t new_mdb) { + std::vector out(raw, raw + size); if (version == MpegVersion::MPEG1) { // main_data_begin is 9 bits (bits 0-8 MSB first) out[0] = static_cast((new_mdb >> 1) & 0xFF); @@ -94,12 +95,12 @@ static uint32_t get_samplerate_index(const MpegVersion v, const uint32_t sr) { return 0; } -static bool is_xing_frame(const Mp3Frame& frame) { - std::vector full_frame; - full_frame.insert(full_frame.end(), frame.side_info_raw.begin(), frame.side_info_raw.end()); - full_frame.insert(full_frame.end(), frame.main_data_raw.begin(), frame.main_data_raw.end()); - if (full_frame.size() < 100) return false; - const std::string s(full_frame.begin(), full_frame.begin() + std::min(static_cast(100), full_frame.size())); +static bool is_xing_frame(const Mp3Frame& frame, const std::vector& file) { + std::string s; + s.append(reinterpret_cast(frame.side_info_raw), frame.side_info_size); + s.append(reinterpret_cast(file.data() + frame.main_data_offset), frame.main_data_avail); + if (s.size() < 100) return false; + s.resize(100); return (s.find("Xing") != std::string::npos || s.find("Info") != std::string::npos); } @@ -151,7 +152,7 @@ void Packer::process(const std::string& input_file, const std::string& output_fi std::vector all_frames; while (auto opt_frame = reader.read_next_frame()) { - all_frames.push_back(*opt_frame); + all_frames.push_back(std::move(*opt_frame)); // a copy here duplicated every frame's payload } if (all_frames.empty()) { @@ -160,7 +161,8 @@ void Packer::process(const std::string& input_file, const std::string& output_fi bool has_xing = false; // removed unused xing_frame here - if (is_xing_frame(all_frames[0])) { + const std::vector& file = reader.data(); + if (is_xing_frame(all_frames[0], file)) { has_xing = true; // do not erase it! we must keep it to preserve the lame tag! // all_frames.erase(all_frames.begin()); @@ -171,27 +173,42 @@ void Packer::process(const std::string& input_file, const std::string& output_fi DEBUG_LOG("Optimizing " << N << " audio frames..."); std::vector> optimized_main_data(N); - std::vector optimized_side_info(N); + // The new side info goes into the frame itself. A worker owns its frame, and a + // frame it gives up on keeps what it arrived with, which is what the verbatim + // path reads — so a parallel array of side info was a copy of something each + // frame already had, 200 bytes of it per frame. // plain uint8_t, not vector: worker threads write distinct indices concurrently, // and vector's packed-bit storage would make that a genuine data race (RMW on a // shared word) even though each thread only touches its own logical element. std::vector frame_was_optimized(N, 0); std::vector global_res; + size_t total_main = 0; + for (const auto& frame : all_frames) total_main += frame.main_data_size; + global_res.reserve(total_main); std::vector frame_main_starts(N); for (size_t i = 0; i < N; ++i) { frame_main_starts[i] = global_res.size(); - global_res.insert(global_res.end(), all_frames[i].main_data_raw.begin(), all_frames[i].main_data_raw.end()); + // One copy, out of the file and into the reservoir view. A payload the file + // does not hold in full is zero-filled to its declared length. + const auto& fr = all_frames[i]; + global_res.insert(global_res.end(), file.data() + fr.main_data_offset, + file.data() + fr.main_data_offset + fr.main_data_avail); + global_res.resize(frame_main_starts[i] + fr.main_data_size, 0x00); } std::atomic current_frame{0}; unsigned int active_threads = num_threads > 0 ? num_threads : std::max(1u, std::thread::hardware_concurrency()); auto worker = [&]() { + // One pair of spectra per worker, reused for every granule: the coder fills + // them and zeroes only the tail it did not write. + Spectrum coeffs{}, vcoeffs{}; while (true) { size_t i = current_frame.fetch_add(1); if (i >= N) break; + auto& frame = all_frames[i]; if (has_xing && i == 0) { // output frames are always written without CRC, so "Xing"/"Info" must be at // output mdr[0..3]. @@ -200,17 +217,19 @@ void Packer::process(const std::string& input_file, const std::string& output_fi // CRC file: "Xi"/"In" is in si[-2:], "ng"/"fo" at mdr[0]. // Prepend si[-2:] and copy standard Xing content (through LAME // extension if present), trimming trailing zeros to save space. - const auto& mdr = all_frames[i].main_data_raw; + const uint8_t* mdr = file.data() + all_frames[i].main_data_offset; + const size_t mdr_size = all_frames[i].main_data_avail; if (!all_frames[i].header.has_crc) { - optimized_main_data[i].assign(mdr.begin(), mdr.end()); + optimized_main_data[i].assign(mdr, mdr + mdr_size); } else { // CRC: recover "Xi"/"In" from the last 2 bytes of the original side_info - const auto& sir = all_frames[i].side_info_raw; - uint8_t b0 = (sir.size() >= 2) ? sir[sir.size() - 2] : 0x58; - uint8_t b1 = (sir.size() >= 1) ? sir[sir.size() - 1] : 0x69; + const uint8_t* sir = all_frames[i].side_info_raw; + const size_t sir_size = all_frames[i].side_info_size; + uint8_t b0 = (sir_size >= 2) ? sir[sir_size - 2] : 0x58; + uint8_t b1 = (sir_size >= 1) ? sir[sir_size - 1] : 0x69; // mdr[0..1]="ng"/"fo", content (flags, counts...) at mdr[2..] size_t keep = 2; - if (mdr.size() >= 6) { + if (mdr_size >= 6) { uint32_t fl = (static_cast(mdr[2]) << 24) | (static_cast(mdr[3]) << 16) | (static_cast(mdr[4]) << 8) | @@ -220,22 +239,18 @@ void Packer::process(const std::string& input_file, const std::string& output_fi if (fl & 2) keep += 4; // byte count if (fl & 4) keep += 100; // TOC if (fl & 8) keep += 4; // quality - if (mdr.size() >= keep + 4 && + if (mdr_size >= keep + 4 && mdr[keep] == 'L' && mdr[keep+1] == 'A' && mdr[keep+2] == 'M' && mdr[keep+3] == 'E') keep += 36; // LAME extension tag } - keep = std::min(keep, mdr.size()); + keep = std::min(keep, mdr_size); optimized_main_data[i] = {b0, b1}; - optimized_main_data[i].insert(optimized_main_data[i].end(), - mdr.begin(), mdr.begin() + static_cast(keep)); + optimized_main_data[i].insert(optimized_main_data[i].end(), mdr, mdr + keep); } - SideInfo clean_si{}; - optimized_side_info[i] = clean_si; + frame.side_info = SideInfo{}; continue; } - auto& frame = all_frames[i]; - int n_ch = (frame.header.channel_mode == ChannelMode::Mono) ? 1 : 2; int n_gr = (frame.header.version == MpegVersion::MPEG1) ? 2 : 1; @@ -270,35 +285,58 @@ void Packer::process(const std::string& input_file, const std::string& output_fi slen2 = slen2_tab[g.scalefac_compress]; } - std::vector scfs; + // Scalefactors are re-emitted exactly as they arrived, so only + // their total length matters. Copying the bits is what Go's + // reference implementation does; reading each field into a + // vector and writing it back cost an allocation per granule and + // some seventy calls through the bit machinery. + size_t sf_bits = 0; if (g.window_switching_flag && g.block_type == 2) { - if (g.mixed_block_flag) { - // 8 long-window sfb (0-7) at slen1 - for (int k = 0; k < 8; ++k) scfs.push_back(data_reader.read_bits(slen1)); - // short sfb 3-5: 3 bands × 3 windows at slen1 - for (int k = 0; k < 9; ++k) scfs.push_back(data_reader.read_bits(slen1)); - // short sfb 6-11: 6 bands × 3 windows at slen2 - for (int k = 0; k < 18; ++k) scfs.push_back(data_reader.read_bits(slen2)); - } else { - for (int k = 0; k < 18; ++k) scfs.push_back(data_reader.read_bits(slen1)); - for (int k = 18; k < 36; ++k) scfs.push_back(data_reader.read_bits(slen2)); - } + sf_bits = g.mixed_block_flag + ? static_cast(17 * slen1 + 18 * slen2) + : static_cast(18 * slen1 + 18 * slen2); } else { - for (int k = 0; k < 6; ++k) { - if (gr == 0 || !frame.side_info.scfsi[ch][0]) scfs.push_back(data_reader.read_bits(slen1)); - else scfs.push_back(0); - } - for (int k = 6; k < 11; ++k) { - if (gr == 0 || !frame.side_info.scfsi[ch][1]) scfs.push_back(data_reader.read_bits(slen1)); - else scfs.push_back(0); - } - for (int k = 11; k < 16; ++k) { - if (gr == 0 || !frame.side_info.scfsi[ch][2]) scfs.push_back(data_reader.read_bits(slen2)); - else scfs.push_back(0); - } - for (int k = 16; k < 21; ++k) { - if (gr == 0 || !frame.side_info.scfsi[ch][3]) scfs.push_back(data_reader.read_bits(slen2)); - else scfs.push_back(0); + // A band group whose scfsi bit is set in granule 1 reuses + // granule 0's values and occupies no bits at all. + if (gr == 0 || !frame.side_info.scfsi[ch][0]) sf_bits += static_cast(6 * slen1); + if (gr == 0 || !frame.side_info.scfsi[ch][1]) sf_bits += static_cast(5 * slen1); + if (gr == 0 || !frame.side_info.scfsi[ch][2]) sf_bits += static_cast(5 * slen2); + if (gr == 0 || !frame.side_info.scfsi[ch][3]) sf_bits += static_cast(5 * slen2); + } + + size_t out_start = writer.tell_bit(); + if (data_reader.tell_bit() + sf_bits <= data_reader.size_bits()) { + writer.copy_from(data_reader, sf_bits); + } else { + // The span reaches past the end of the reservoir data, where + // what a read returns depends on the width it was made at, so + // the fields are read and written one at a time as before. + int scfs[36] = {}; + if (g.window_switching_flag && g.block_type == 2) { + if (g.mixed_block_flag) { + for (int k = 0; k < 17; ++k) scfs[k] = static_cast(data_reader.read_bits(slen1)); + for (int k = 17; k < 35; ++k) scfs[k] = static_cast(data_reader.read_bits(slen2)); + for (int k = 0; k < 17; ++k) writer.write_bits(static_cast(scfs[k]), slen1); + for (int k = 17; k < 35; ++k) writer.write_bits(static_cast(scfs[k]), slen2); + } else { + for (int k = 0; k < 18; ++k) scfs[k] = static_cast(data_reader.read_bits(slen1)); + for (int k = 18; k < 36; ++k) scfs[k] = static_cast(data_reader.read_bits(slen2)); + for (int k = 0; k < 18; ++k) writer.write_bits(static_cast(scfs[k]), slen1); + for (int k = 18; k < 36; ++k) writer.write_bits(static_cast(scfs[k]), slen2); + } + } else { + static constexpr int group_first[4] = {0, 6, 11, 16}; + static constexpr int group_last[4] = {6, 11, 16, 21}; + for (int grp = 0; grp < 4; ++grp) { + const int slen = grp < 2 ? slen1 : slen2; + if (gr != 0 && frame.side_info.scfsi[ch][grp]) continue; + for (int k = group_first[grp]; k < group_last[grp]; ++k) { + scfs[k] = static_cast(data_reader.read_bits(slen)); + } + for (int k = group_first[grp]; k < group_last[grp]; ++k) { + writer.write_bits(static_cast(scfs[k]), slen); + } + } } } @@ -317,35 +355,9 @@ void Packer::process(const std::string& input_file, const std::string& output_fi }; int scalefac_bits = static_cast(data_reader.tell_bit() - gc_orig_start); int max_huff_bits = side_copy.gr[gr][ch].part2_3_length - scalefac_bits; - auto coeffs = HuffmanOptimizer::decode_quantized_coefficients(orig_cfg, data_reader, frame.header.samplerate, max_huff_bits); + HuffmanOptimizer::decode_quantized_coefficients(coeffs, orig_cfg, data_reader, frame.header.samplerate, max_huff_bits); auto best_cfg = HuffmanOptimizer::find_best_config(coeffs, orig_cfg, frame.header.samplerate); - // re-encode - size_t out_start = writer.tell_bit(); - if (g.window_switching_flag && g.block_type == 2) { - if (g.mixed_block_flag) { - for (int k = 0; k < 8; ++k) writer.write_bits(scfs[k], slen1); - for (int k = 8; k < 17; ++k) writer.write_bits(scfs[k], slen1); - for (int k = 17; k < 35; ++k) writer.write_bits(scfs[k], slen2); - } else { - for (int k = 0; k < 18; ++k) writer.write_bits(scfs[k], slen1); - for (int k = 18; k < 36; ++k) writer.write_bits(scfs[k], slen2); - } - } else { - for (int k = 0; k < 6; ++k) { - if (gr == 0 || !frame.side_info.scfsi[ch][0]) writer.write_bits(scfs[k], slen1); - } - for (int k = 6; k < 11; ++k) { - if (gr == 0 || !frame.side_info.scfsi[ch][1]) writer.write_bits(scfs[k], slen1); - } - for (int k = 11; k < 16; ++k) { - if (gr == 0 || !frame.side_info.scfsi[ch][2]) writer.write_bits(scfs[k], slen2); - } - for (int k = 16; k < 21; ++k) { - if (gr == 0 || !frame.side_info.scfsi[ch][3]) writer.write_bits(scfs[k], slen2); - } - } - size_t scalefac_end_bit = writer.tell_bit(); HuffmanOptimizer::encode_quantized_coefficients(coeffs, best_cfg, writer, frame.header.samplerate); @@ -354,7 +366,7 @@ void Packer::process(const std::string& input_file, const std::string& output_fi int huff_bits = static_cast(writer.tell_bit() - scalefac_end_bit); BitstreamReader vrdr(writer.data()); vrdr.seek_bit(scalefac_end_bit); - auto vcoeffs = HuffmanOptimizer::decode_quantized_coefficients(best_cfg, vrdr, frame.header.samplerate, huff_bits); + HuffmanOptimizer::decode_quantized_coefficients(vcoeffs, best_cfg, vrdr, frame.header.samplerate, huff_bits); for (int ci = 0; ci < 576; ++ci) { if (vcoeffs[ci] != coeffs[ci]) { optimization_failed = true; @@ -401,16 +413,23 @@ void Packer::process(const std::string& input_file, const std::string& output_fi } optimized_main_data[i] = new_main; - optimized_side_info[i] = frame.side_info; } }; - std::vector threads; - for (unsigned int t = 0; t < active_threads; ++t) { - threads.emplace_back(worker); - } - for (auto& thread : threads) { - thread.join(); + // Spawning and joining threads costs more than the work when there is no + // Huffman search to do: without -z a frame's worth of work is a slice copy, and + // sixteen threads were being created to share it. + if (active_threads == 1 || !this->recompress_huffman) { + worker(); + } else { + std::vector threads; + threads.reserve(active_threads); + for (unsigned int t = 0; t < active_threads; ++t) { + threads.emplace_back(worker); + } + for (auto& thread : threads) { + thread.join(); + } } DEBUG_LOG("Huffman optimization completed. Running reservoir constraint solver..."); @@ -426,15 +445,25 @@ void Packer::process(const std::string& input_file, const std::string& output_fi if (!out) { throw std::runtime_error("Could not open output file: " + output_file); } - + + // The file is assembled in memory and written once at the end. Writing it + // frame by frame meant three stream writes per frame, and the Xing patch-up + // then had to seek back through them. + std::vector out_bytes; + out_bytes.reserve(total_main + N * 40 + reader.get_start_junk().size() + reader.get_end_junk().size()); + (void)total_main; + const auto append = [&out_bytes](const uint8_t* p, const size_t n) { + out_bytes.insert(out_bytes.end(), p, p + n); + }; + // write start junk (id3v2) const auto& start_junk = reader.get_start_junk(); if (!start_junk.empty()) { - out.write(reinterpret_cast(start_junk.data()), static_cast(start_junk.size())); + append(start_junk.data(), start_junk.size()); } // track where the xing frame starts in the file - const size_t xing_header_file_pos = has_xing ? static_cast(out.tellp()) : SIZE_MAX; + const size_t xing_header_file_pos = has_xing ? out_bytes.size() : SIZE_MAX; // backward pass constraint solver std::vector required_carryover(N, 0); @@ -449,14 +478,20 @@ void Packer::process(const std::string& input_file, const std::string& output_fi current_req = std::max(0, space_needed - max_data_per_frame); required_carryover[i] = current_req; } - std::vector global_main_data; + // The reservoir is described rather than built. Deciding a frame's size and its + // reservoir offset takes lengths only, so the first pass records the pieces — + // each frame's data, and the run of zeros left at the end where the reservoir + // cannot be read back — and the second pass reads them straight out of the + // frames' own buffers. Concatenating them first was a second copy of the whole + // audio and an allocation to hold it. + size_t reservoir_bytes = 0; int current_reservoir = 0; // free space in past payloads std::vector chosen_bitrates(N); // pass 1: build global_main_data and calculate main_data_begin for (size_t i = 0; i < N; ++i) { - auto& side = optimized_side_info[i]; + auto& side = all_frames[i].side_info; int max_reservoir = (version == MpegVersion::MPEG1) ? 511 : 255; @@ -488,7 +523,7 @@ void Packer::process(const std::string& input_file, const std::string& output_fi optimized_main_data[0].resize(static_cast(data_size), 0x00); side.main_data_begin = static_cast(current_reservoir); - global_main_data.insert(global_main_data.end(), optimized_main_data[i].begin(), optimized_main_data[i].end()); + reservoir_bytes += optimized_main_data[i].size(); current_reservoir = projected_reservoir; chosen_bitrates[i] = bitrate_use; @@ -496,10 +531,9 @@ void Packer::process(const std::string& input_file, const std::string& output_fi current_reservoir = 0; } - if (current_reservoir > 0) { - std::vector end_pad(static_cast(current_reservoir), 0x00); - global_main_data.insert(global_main_data.end(), end_pad.begin(), end_pad.end()); - } + // Whatever the last frame leaves unread is emitted as zeros. + const size_t reservoir_tail_zeros = static_cast(std::max(0, current_reservoir)); + reservoir_bytes += reservoir_tail_zeros; // pass 2: write physical file // find the last frame that actually contains non-zero data @@ -515,17 +549,43 @@ void Packer::process(const std::string& input_file, const std::string& output_fi N_out = static_cast(i); } - size_t global_main_data_ptr = 0; + // A frame's slot is a window over that sequence and generally spans more than one + // piece — that is what a bit reservoir is for — so the read side is a cursor. + size_t cursor_frame = 0, cursor_offset = 0, cursor_read = 0; + const auto emit_reservoir = [&](size_t want) { + while (want > 0 && cursor_read < reservoir_bytes) { + if (cursor_frame < N) { + const auto& piece = optimized_main_data[cursor_frame]; + if (cursor_offset >= piece.size()) { + ++cursor_frame; + cursor_offset = 0; + continue; + } + const size_t take = std::min(want, piece.size() - cursor_offset); + append(piece.data() + cursor_offset, take); + cursor_offset += take; + cursor_read += take; + want -= take; + continue; + } + // Past the last frame's data: the tail the reservoir never gives back. + const size_t take = std::min(want, reservoir_bytes - cursor_read); + out_bytes.resize(out_bytes.size() + take, 0x00); + cursor_read += take; + want -= take; + } + return want; // whatever the reservoir could not supply + }; // audio frame byte positions in the output file (index 0 = first audio frame, i.e. frame 1 if has_xing) std::vector audio_frame_file_pos; for (size_t i = 0; i < N_out; ++i) { const auto& frame = all_frames[i]; - const auto& side = optimized_side_info[i]; + const auto& side = frame.side_info; if (has_xing && i > 0) - audio_frame_file_pos.push_back(static_cast(out.tellp())); + audio_frame_file_pos.push_back(out_bytes.size()); ChosenBitrate bitrate_use = chosen_bitrates[i]; @@ -551,41 +611,36 @@ void Packer::process(const std::string& input_file, const std::string& output_fi std::vector new_side; if (has_xing && i == 0) { - new_side.assign(frame.side_info_raw.size(), 0); + new_side.assign(frame.side_info_size, 0); } else if (frame_was_optimized[i]) { new_side = serialize_side_info(frame.header, side); } else { - new_side = patch_mdb(frame.side_info_raw, version, side.main_data_begin); + new_side = patch_mdb(frame.side_info_raw, frame.side_info_size, version, side.main_data_begin); } - out.write(reinterpret_cast(head), 4); - out.write(reinterpret_cast(new_side.data()), static_cast(new_side.size())); + append(head, 4); + append(new_side.data(), new_side.size()); - int data_size = bitrate_use.data_size; - int avail = static_cast(global_main_data.size() - global_main_data_ptr); - int write_len = std::min(data_size, avail); - - if (write_len > 0) { - out.write(reinterpret_cast(&global_main_data[global_main_data_ptr]), write_len); - global_main_data_ptr += static_cast(write_len); - } - - if (data_size > write_len) { - std::vector pad(static_cast(data_size - write_len), 0x00); - out.write(reinterpret_cast(pad.data()), static_cast(pad.size())); + const int data_size = bitrate_use.data_size; + const size_t short_by = emit_reservoir(static_cast(data_size)); + if (short_by > 0) { + out_bytes.resize(out_bytes.size() + short_by, 0x00); } } // save position before end_junk to calculate total mp3 stream size - size_t mp3_end_pos = static_cast(out.tellp()); + size_t mp3_end_pos = out_bytes.size(); // write end junk (id3v1) const auto& end_junk = reader.get_end_junk(); if (!end_junk.empty()) { - out.write(reinterpret_cast(end_junk.data()), static_cast(end_junk.size())); + append(end_junk.data(), end_junk.size()); } // patch xing header fields (bytes count + TOC) if present + const auto patch = [&out_bytes](const size_t at, const uint8_t* p, const size_t n) { + if (at + n <= out_bytes.size()) std::memcpy(out_bytes.data() + at, p, n); + }; if (has_xing) { // optimized_main_data[0] always has "Xing"/"Info" at bytes 0-3, flags at 4-7 // (regardless of whether the source was CRC or no-CRC) @@ -608,13 +663,12 @@ void Packer::process(const std::string& input_file, const std::string& output_fi if (flags & 1) field_offset += 4; // frames field: skip it if (flags & 2) { - out.seekp(static_cast(main_data_base + field_offset), std::ios::beg); uint8_t buf[4]; buf[0] = (total_mp3_bytes >> 24) & 0xFF; buf[1] = (total_mp3_bytes >> 16) & 0xFF; buf[2] = (total_mp3_bytes >> 8) & 0xFF; buf[3] = (total_mp3_bytes >> 0) & 0xFF; - out.write(reinterpret_cast(buf), 4); + patch(main_data_base + field_offset, buf, 4); field_offset += 4; } @@ -630,11 +684,15 @@ void Packer::process(const std::string& input_file, const std::string& output_fi int v = static_cast(256.0 * static_cast(pos) / static_cast(total_mp3_bytes)); toc[k] = static_cast(std::min(v, 255)); } - out.seekp(static_cast(main_data_base + field_offset), std::ios::beg); - out.write(reinterpret_cast(toc), 100); + patch(main_data_base + field_offset, toc, 100); } } } + + out.write(reinterpret_cast(out_bytes.data()), static_cast(out_bytes.size())); + if (!out) { + throw std::runtime_error("Could not write output file: " + output_file); + } } } // namespace mp3packer diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..13b5d88 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,8 @@ +# Dependency-free tests: each is a program that returns non-zero on failure. +add_executable(coder_test coder_test.cpp) +target_link_libraries(coder_test PRIVATE mp3packercpp_core) +add_test(NAME coder COMMAND coder_test) + +add_executable(bitstream_test bitstream_test.cpp) +target_include_directories(bitstream_test PRIVATE ${CMAKE_SOURCE_DIR}/src/core/include) +add_test(NAME bitstream COMMAND bitstream_test) diff --git a/tests/bitstream_test.cpp b/tests/bitstream_test.cpp new file mode 100644 index 0000000..e4bbd44 --- /dev/null +++ b/tests/bitstream_test.cpp @@ -0,0 +1,101 @@ +// Differential test: BitstreamReader/BitstreamWriter against straightforward +// byte-at-a-time implementations of the same contract. +// +// Two behaviours here are easy to break and expensive to debug from output bytes: +// a read that runs past the end of the data does not zero-fill — the bits it found +// stay at the top of the field and are shifted up again by the number it did not — +// and data() may be called mid-stream, materialising the pending bits without +// committing them, because the packer reads back what it has written to verify a +// granule and then carries on writing. +#include "bitstream.hpp" +#include +#include + +using namespace mp3packer; + +class OldReader { +public: + explicit OldReader(const std::vector& d) : data_(d) {} + uint32_t read_bits(int num_bits) { + if (num_bits == 0) return 0; + uint32_t result = 0; + for (int i = 0; i < num_bits; ++i) { + const size_t byte_idx = bit_pos_ / 8; + const int bit_idx = 7 - (bit_pos_ % 8); + if (byte_idx >= data_.size()) { result <<= (num_bits - i); bit_pos_ += size_t(num_bits - i); return result; } + if (data_[byte_idx] & (1 << bit_idx)) result |= (1 << (num_bits - 1 - i)); + bit_pos_++; + } + return result; + } + void seek_bit(size_t p) { bit_pos_ = p; } + size_t tell_bit() const { return bit_pos_; } +private: + const std::vector& data_; + size_t bit_pos_ = 0; +}; + +class OldWriter { +public: + void write_bits(uint32_t value, int num_bits) { + for (int i = 0; i < num_bits; ++i) { + const size_t byte_idx = bit_pos_ / 8; + const int bit_idx = 7 - (bit_pos_ % 8); + if (byte_idx >= data_.size()) data_.push_back(0); + if (value & (1u << (num_bits - 1 - i))) data_[byte_idx] |= (1 << bit_idx); + bit_pos_++; + } + } + const std::vector& data() const { return data_; } + size_t tell_bit() const { return bit_pos_; } +private: + std::vector data_; + size_t bit_pos_ = 0; +}; + +int main() { + std::mt19937 rng(1); + int fails = 0; + for (int trial = 0; trial < 3000 && fails < 10; ++trial) { + // random data, random reads (including past the end) + std::vector data(rng() % 40); + for (auto& b : data) b = uint8_t(rng()); + OldReader a(data); + BitstreamReader b(data); + for (int i = 0; i < 50; ++i) { + if (rng() % 8 == 0) { size_t p = rng() % (data.size() * 8 + 40); a.seek_bit(p); b.seek_bit(p); } + int n = int(rng() % 33); + uint32_t va = a.read_bits(n), vb = b.read_bits(n); + if (va != vb || a.tell_bit() != b.tell_bit()) { + printf("READ trial %d i %d size %zu n %d: %u vs %u pos %zu vs %zu\n", + trial, i, data.size(), n, va, vb, a.tell_bit(), b.tell_bit()); + ++fails; break; + } + } + + // random writes, compared as bytes plus a mid-stream data() read-back + OldWriter wa; + BitstreamWriter wb; + for (int i = 0; i < 60; ++i) { + int n = int(rng() % 33); + uint32_t v = rng(); + if (n < 32) v &= (1u << n) - 1; + wa.write_bits(v, n); + wb.write_bits(v, n); + if (rng() % 10 == 0) { + const auto& da = wa.data(); + const auto& db = wb.data(); + if (da != db || wa.tell_bit() != wb.tell_bit()) { + printf("WRITE-MID trial %d i %d: sizes %zu vs %zu bits %zu vs %zu\n", + trial, i, da.size(), db.size(), wa.tell_bit(), wb.tell_bit()); + for (size_t k = 0; k < std::min(da.size(), db.size()); ++k) + if (da[k] != db[k]) { printf(" first diff at byte %zu: %02x vs %02x\n", k, da[k], db[k]); break; } + ++fails; break; + } + } + } + if (wa.data() != wb.data()) { printf("WRITE-END trial %d differs\n", trial); ++fails; } + } + printf(fails ? "FAILURES: %d\n" : "ok\n", fails); + return fails != 0; +} diff --git a/tests/coder_test.cpp b/tests/coder_test.cpp new file mode 100644 index 0000000..76bf64e --- /dev/null +++ b/tests/coder_test.cpp @@ -0,0 +1,119 @@ +// Tests that the Huffman search only ever returns a coding that reproduces the +// spectrum it was given. +// +// The case that motivated this: a count1 quadruple cannot start inside the last +// one, so a big_values that leaves one, two or three coefficients above the final +// quadruple leaves them uncoded. The cost model used to treat such a tail as free, +// which made those big_values look cheapest whenever the top of the spectrum was +// coded at all; the encoder then dropped the coefficients. In the packer that shows +// up as a frame failing round-trip verification and being written verbatim, so the +// whole frame loses its recompression. +#include "huffman.hpp" +#include +using namespace mp3packer; + +static bool round_trips(const Spectrum& s, const HuffmanConfig& orig, int* bits_out, int* bv_out) { + const HuffmanConfig best = HuffmanOptimizer::find_best_config(s, orig, 44100); + BitstreamWriter w; + HuffmanOptimizer::encode_quantized_coefficients(s, best, w, 44100); + const int bits = static_cast(w.tell_bit()); + Spectrum back{}; + BitstreamReader r(w.data()); + HuffmanOptimizer::decode_quantized_coefficients(back, best, r, 44100, bits); + *bits_out = bits; + *bv_out = best.big_values; + return back == s; +} + +// Every entry of the big-value probe, against a walk of the same tree one bit at a +// time from the root. The decoder reads the table and never the tree for 99% of +// codewords, so a single wrong entry would decode a pair to the wrong magnitudes or +// desynchronise the rest of the region. +static int check_probe_tables() { + // The trees are the ones in huffman_tables.hpp, reached here through the same + // library the decoder uses; walking them needs only their shape. + extern const int16_t* huffman_tree_for_test(int idx); + const int bits = HuffmanOptimizer::probe_bits(); + int failures = 0; + for (int idx = 0; idx < 32 && failures < 10; ++idx) { + const int16_t* tab = huffman_tree_for_test(idx); + for (unsigned prefix = 0; prefix < (1u << bits); ++prefix) { + // Walk the tree, taking the prefix's bits most significant first. + int node = 0, used = 0, sym = -1; + while (used <= bits) { + const int16_t v = tab[node]; + if (v >= 0) { sym = v; break; } + if (used == bits) break; // longer than the probe + ++node; + if (prefix >> (bits - 1 - used) & 1) node -= v; + ++used; + } + int x = -1, y = -1, length = -1, signs = -1; + const bool resolved = HuffmanOptimizer::probe_pair(idx, prefix, x, y, length, signs); + if (sym < 0) { + if (resolved) { + printf("table %d prefix %u: resolved a codeword longer than the probe\n", idx, prefix); + ++failures; + } + continue; + } + const int wx = (sym >> 4) & 0xF, wy = sym & 0xF; + if (!resolved || x != wx || y != wy || length != used || + signs != (wx != 0) + (wy != 0)) { + printf("table %d prefix %u: entry (%d,%d) len %d signs %d, tree says (%d,%d) len %d\n", + idx, prefix, x, y, length, signs, wx, wy, used); + ++failures; + } + } + } + printf("probe tables vs trees: %s\n", failures ? "MISMATCH" : "all entries agree"); + return failures; +} + +int main() { + HuffmanConfig orig{}; + orig.big_values = 288; + orig.region0_count = 7; + orig.region1_count = 13; + orig.table0 = orig.table1 = orig.table2 = 1; + int failures = 0; + + // Case 1: a lone non-zero at the very top of the spectrum. + { + Spectrum s{}; + for (int i = 0; i < 40; ++i) s[i] = static_cast((i % 5) - 2); // some big values + s[574] = 1; + int bits = 0, bv = 0; + const bool ok = round_trips(s, orig, &bits, &bv); + printf("top coefficient at 574: big_values=%3d bits=%4d %s\n", bv, bits, ok ? "round-trips" : "DROPPED COEFFICIENTS"); + failures += !ok; + } + + // Case 2: both of the top two coded. + { + Spectrum s{}; + for (int i = 0; i < 40; ++i) s[i] = static_cast((i % 7) - 3); + s[574] = -1; + s[575] = 1; + int bits = 0, bv = 0; + const bool ok = round_trips(s, orig, &bits, &bv); + printf("top coefficients at 574,575: big_values=%3d bits=%4d %s\n", bv, bits, ok ? "round-trips" : "DROPPED COEFFICIENTS"); + failures += !ok; + } + + // Case 3: a dense tail of ones, ending at 575. + { + Spectrum s{}; + for (int i = 0; i < 20; ++i) s[i] = static_cast((i % 3) - 1); + for (int i = 560; i < 576; ++i) s[i] = static_cast((i % 2) ? 1 : -1); + int bits = 0, bv = 0; + const bool ok = round_trips(s, orig, &bits, &bv); + printf("dense tail through 575: big_values=%3d bits=%4d %s\n", bv, bits, ok ? "round-trips" : "DROPPED COEFFICIENTS"); + failures += !ok; + } + + failures += check_probe_tables(); + + printf(failures ? "FAIL (%d)\n" : "ok\n", failures); + return failures != 0; +}