A lossy audio codec family for .no_fft and .nadc files. No FFT is involved,
at any stage.
This README covers NFA2 and the MP3 converter. For the other algorithms, see:
- NFA1: sample-domain DPCM with a range coder
- NFA3: residual baskets
- NFA4: basket-only, no predictor
- NFA5: multiplicative (log-domain) predictor, lossy
- NFA6: blocked multiplicative (per-block log step), lossy
- No_FFT: polynomial-fit codec
make # host build -> codecs/linux/ubuntu/{nofft,acc}
make deb # Debian package -> build/nofft_<version>_amd64.deb
make windows # cross-build win64 (needs mingw-w64)
make checkver # assert control/changelog/man agree on the version
make test # both self tests, no input files needed
make check-ac # prove the C and C++ codecs agree byte for byte
make cleanconvert-all decodes an MP3 once (via ffmpeg) and writes every codec in one
pass, named after the output prefix:
nofft convert-all song.mp3 out
# out.poly.no_fft polynomial fit
# out.nfa1.nadc sample-domain DPCM
# out.nfa2.nadc block-refit predictor
# out.nfa3.nadc residual baskets
# out.nfa4.nadc basket-only
# out.nfa5.nadc multiplicative, lossy
# out.nfa6.nadc blocked multiplicative, lossyconvert writes a single codec selected with -algo. The two commands share
their flags: -bits N sets the acpcm quantiser width (default 6); -block N
and -policy never|always|drift shape NFA2 (and -block shapes NFA6); -s N
or -b N shape NFA4 (a step percent or an exact basket count, e.g. 170);
-set-e X and -set-err X shape NFA5 (base and log-domain quantum, default 2
and 0.01); -set-rate X auto-dials NFA5's err to a target bits/sample and
drives NFA6's per-block fit (default 2.6). -algo poly keeps the historical
polynomial positional arguments.
nofft convert song.mp3 song.no_fft -algo poly 20 4 f16
nofft convert song.mp3 song.nfa3.nadc -algo nfa3 -bits 6
nofft convert song.mp3 song.nfa5.nadc -algo nfa5 -set-err 0.005
nofft convert song.mp3 song.nfa5.nadc -algo nfa5 -set-rate 2.6 # auto-dial
nofft convert song.mp3 song.nfa6.nadc -algo nfa6 -set-rate 2.6
nofft convert-all song.mp3 out -b 170 # NFA4 with 170 basketsBoth need ffmpeg on PATH; the decoded float32 WAV is temporary and removed
on every path. convert-all reports one line per file plus a byte total, and
keeps going if one algorithm fails. The
converter guide has the full flag reference; see the
No_FFT codec notes for what convert does and does not suit.
NFA1 fits a1 once per channel and holds it for the whole file. When the
material changes character — a quiet passage into percussion, a tone into noise
— that one compromise gain costs bits for the rest of the file. NFA2 lets the
encoder replace a1 at fixed block boundaries and tell the decoder when it
did. It is enabled per encode with -block N (the refit window in frames) and
-policy never|always|drift (default drift; -policy needs -block):
nofft ac-roundtrip song.wav 6 -block 1024 -policy drift
nofft ac-encode song.wav song.nadc 6 -block 1024 -policy drift
nofft ac-roundtrip song.wav 6 -block 1024 # policy defaults to drift
nofft ac-info song.nadc # ... block=1024
nofft ac-decode song.nadc song.out.wav
nofft ac-play song.nadc | mpv -The library API is acpcm_encode2 with acpcm_p2_policy; the decoder needs
nothing new and reads both magics. ac-info and the encoder/roundtrip summary
echo block= and policy= so a stream's shape is visible without a hex
editor.
Magic NFA2, and the fixed header gains one field, so the length table starts
at 24 instead of 20:
0 char magic[4] "NFA2"
4 uint32 frames frames per channel
8 uint32 bits quantiser width, 2..24
12 uint32 channels
16 uint32 sample_rate
20 uint32 block_len frames per refit window, >= 1
24 uint32 len[c] byte length of each channel chunk
.. payload: the chunks, back to back
Inside a chunk, before the sample at i = k * block_len, the encoder sends one
adaptive binary flag; when the flag is 1 it is followed by a new 16-bit a1_q
(the same /16384 fixed point as the chunk header) that replaces the predictor
gain for the rest of the chunk. The flag has its own adaptive probability, so a
run of "no refit" costs close to zero bits. Everything else — chunk header
fields, seed sample, symbols — is byte-for-byte the NFA1 layout, and a
block_len of 0 in an NFA2 file is rejected rather than misparsed.
The refit value obeys the same quantise-once rule as every other header field:
the encoder takes the least-squares a1 over the just-finished block's source
pairs, clamps it to +/-0.999, rounds it into the 16-bit field, and then codes
that rounded integer, so both sides continue with exactly the same gain. A flat
block has nothing to learn and re-emits the current value.
When a refit happens is decided entirely by the encoder; the decoder only reads what was sent:
| Policy | Refit |
|---|---|
ACP2_NEVER |
never; the stream decodes bit-identical to NFA1 |
ACP2_ALWAYS |
at every block boundary |
ACP2_DRIFT |
only when the basket meter reports expensive residuals |
ACP2_DRIFT counts the just-finished block's residuals whose magnitude reaches
lim / 8 of the symbol range; when at least 1% of the block looks that
expensive, the next boundary refits. That is a magnitude meter, so on stable
material it senses busy blocks rather than a wrong predictor and fires more
often than it needs to — the cost is still small.
Measured at bits=6, block_len=1024 (./measure_acp2):
| Material | NFA1 bits/sample | NFA2 drift bits/sample | change | NFA1 SNR | NFA2 drift SNR |
|---|---|---|---|---|---|
| crafted tone->noise, 10 s (predictor must adapt) | 4.39 | 3.24 | -26% | 36.44 dB | 36.52 dB |
| stationary noise, 10 s | 6.06 | 6.08 | +0.3% | 35.54 dB | 35.49 dB |
| real stereo track, 10.4 M frames | 3.207 | 3.220 | +0.4% | 31.22 dB | 31.22 dB |
The win is exactly where a1 drifts, and it grows with the quantiser width
(-35% at bits=3, -17% at bits=12); on material whose predictor is already
right the refits cost a few tenths of a percent and nothing measurable in SNR.
ACP2_NEVER costs at most +0.0015 bits/sample for the flags themselves. The one
exception is bits=2, where the quantiser is too coarse to refit safely and
quality drops; skip refits below bits=3. Speed: decoding stays the same or
gets slightly faster (the shorter stream offsets the flag reads), encoding
costs up to ~9% when refits fire on real material and gets faster on
drifting material because the stream shrinks.
Reproduce the tables with:
g++ -std=c++17 -O2 -Wall -Wextra -Werror -pedantic \
-o measure_acp2 measure_acp2.cpp acpcm_inst.cpp nofft.cpp -lm
./measure_acp2Rolling guide of TAM: https://alg0z8n8its9lovely6tricks.blogspot.com/2024/08/tam-guide-of-features-smart-tricks.html
TELEGRAM: https://t.me/+N_TdOq7Ui2ZiOTM6 (Alg0Z).
ALG0Z RU: https://dzen.ru/alg0z
ALG0Z EN: https://alg0z.blogspot.com
ChangeLog: https://alg0z8n8its9lovely6tricks.blogspot.com/2023/09/tam-changelog.html
FORUM: https://www.neowin.net/forum/topic/1430114-tam/
E-MAIL: sark0y@protonmail.com
GITHUB: https://github.com/SarK0Y/TAM_RUSTy.git
YouTube: https://www.youtube.com/@evgeneyknyazhev968
Twitter_X: https://x.com/SarK0Y8
Donations: https://boosty.to/alg0z/donate https://zap-hosting.com/en/shop/donation/1f0c83845d810df04ca74e56238399f7/