Compare commits

..
8 Commits
Author SHA1 Message Date
hoo2 a23a1525a2 Update Readme.md 2026-02-22 02:19:56 +02:00
hoo2 b6dcb80a73 Deliverable version 2026-02-22 02:09:38 +02:00
hoo2 17228f8539 Level 3: small fixes and report 2026-02-22 01:39:12 +02:00
hoo2 6b1ebc3cfe Level 3: Fix alpha miss-calculation that affected SNR 2026-02-16 00:10:05 +02:00
hoo2 781cb3047f FIX: decode_huff off-by-one bug 2026-02-15 21:19:59 +02:00
hoo2 cd2b89bd73 Level 3: First positive SNR version 2026-02-15 21:16:54 +02:00
hoo2 4ebee28e4e Level 3: A first failed attempt of encoding-decoding -> SNR=0 2026-02-09 01:58:21 +02:00
hoo2 ae4ad82136 Level 2: Psychoacoustic first version 2026-02-08 22:53:52 +02:00
81 changed files with 13909 additions and 982 deletions
BIN
View File
Binary file not shown.
+260 -62
View File
@@ -1,114 +1,312 @@
# AAC Encoder/Decoder Assignment (Multimedia) # AAC Encoder/Decoder Assignment (Multimedia AUTh)
## About ## Overview
This repository contains a staged implementation of a simplified AAC-like audio coder/decoder pipeline, developed in the context of the Multimedia course at Aristotle University of Thessaloniki (AUTh). This repository contains a staged implementation of a simplified AAC-like audio encoder/decoder pipeline, developed in the context of the **Multimedia** course at Aristotle University of Thessaloniki (AUTh).
The project is organized into incremental levels, where each level introduces additional functionality and requirements (e.g., segmentation control, filterbanks, and progressively more complete encoding/decoding stages).
The purpose of this work is to implement the specified processing chain faithfully to the assignment specification, validate correctness with structured tests, and maintain a clean, reproducible project structure throughout development. The project follows a progressive, level-based structure:
- **Level 1:** Core analysis/synthesis pipeline
- **Level 2:** Full transform-domain encoding with quantization
- **Level 3:** Psychoacoustic modeling and perceptual coding enhancements
The goal of this work is to:
- Faithfully implement the processing chain specified in the assignment
- Validate correctness using structured and reproducible tests
- Maintain a clean and reproducible project architecture
- Ensure separation between development logic and submission packaging
---
## System Architecture
The implemented pipeline follows a simplified AAC-style structure:
```
Input WAV
SSC (Segmentation Control)
Filterbank (MDCT)
[TNS / Psychoacoustic Model] (Level 3)
Quantization & Coding (Level 2+)
Bitstream Structuring
-----------------------------------------
Inverse Quantization
Inverse Filterbank (IMDCT)
OLA Reconstruction
Output WAV
```
Each level progressively enables more blocks of this pipeline.
---
## Repository Structure ## Repository Structure
The repository is organized into source code, project requirements and report files: The repository is organized into source code, material, and report files.
- `source` ```
Under `source` directory there are: root/
- `level_1` Containing the baseline implementation of the required processing chain for Level 1.
- `level_2` Containing the baseline implementation of the required processing chain for Level 2. ├── source/
- `level_3` Containing the baseline implementation of the required processing chain for Level 3. ├── level_1/
│ ├── level_2/
Each level contains: │ ├── level_3/
- a module file (e.g., `level_1/level_1.py`) │ ├── core/
- a dedicated `core/` directory │ └── material/
- a dedicated `material/` directory
├── report/
├── README.md
└── LICENSE
```
- `core` ### `source/`
This directory contains the actual implementation, which is referenced in each one of the `level_x/core/` directories.
- `material`
This directory contains the actual given helper material files
- `report` Directory that contains the TeX files for the report Contains all implementation code.
- `root directory files` Like Readme.md, LICENSE, etc...
#### `level_x/`
### Notes on Repository structure and Development Workflow Each level directory contains:
One of the project requirements was to deliver `level_x` directories containing all the necessary files, without referencing any other external files and libraries.
This requirement introduces copies and is considered error-prone.
In order to avoid that we centralized the development of the project inside `core` directory.
Each level directory contains a references(hard-links) to the files of both `core` and `material` folders.
This way we keep the instructor happy while avoiding the nightmare of code redundancy.
- `level_x.py` (main module entry point)
- `core/` (hard-links to shared implementation)
- `material/` (hard-links to required helper material)
- `tests/` (level-specific tests)
## Level Descriptions Each level is **self-contained** to satisfy submission requirements.
### Level 1 #### `core/`
**Goal:** Implement the core analysis/synthesis chain for Level 1 as defined in the assignment specification. This directory contains the centralized implementation of:
Implemented components (current status): - SSC
- SSC (Sequence Segmentation Control) - MDCT / IMDCT filterbank
- Filterbank (MDCT analysis) and inverse filterbank (IMDCT synthesis) - Quantizer / dequantizer
- End-to-end encoder/decoder functions: - Psychoacoustic model
- TNS
- Bitstream handling
- Encoder/decoder pipelines
All development happens here.
Each `level_x/core/` directory references these files using **hard links**, ensuring:
- No code duplication
- No synchronization errors
- Clean development workflow
#### `material/`
Contains helper files provided by the assignment:
- Sample audio
- Reference data
- Required constants or auxiliary files
---
## Development Workflow Design
One of the project requirements was to deliver `level_x` directories containing all required files, without referencing external directories.
Naively copying files across levels would introduce:
- Code redundancy
- High maintenance cost
- Risk of inconsistencies
- Debugging complexity
To avoid this:
- All implementation lives in `source/core/`
- Each `level_x` directory contains hard-links to `core/` and `material/`
This ensures:
- Single source of truth
- Clean modular structure
- Instructor-compliant submission format
- Safe iterative development
---
# Level Descriptions
---
## Level 1 Core Transform Pipeline
### Goal
Implement the baseline transform-domain analysis/synthesis chain.
### Implemented Components
- Sequence Segmentation Control (SSC)
- MDCT analysis filterbank
- IMDCT synthesis filterbank
- Overlap-Add (OLA) reconstruction
- End-to-end encoder/decoder:
- `aac_coder_1()` - `aac_coder_1()`
- `i_aac_coder_1()` - `i_aac_coder_1()`
- Demo function: - Demo:
- `demo_aac_1()` - `demo_aac_1()`
Tests (current status): ### Testing Coverage
- Module-level tests for SSC
- Module-level tests for filterbank and inverse filterbank (including OLA-based reconstruction checks)
- Internal consistency tests for MDCT/IMDCT
- Module-level tests for `aac_coder_1` / `i_aac_coder_1`
### Level 2 - SSC unit tests
- MDCT / IMDCT correctness tests
- Perfect reconstruction validation
- OLA consistency tests
- Encoder/decoder integration tests
**Goal:** ... This level ensures transform-domain correctness and signal integrity.
---
### Level 3 ## Level 2 Quantization and Coding
**Goal:** ... ### Goal
## How to Run Extend Level 1 by implementing transform-domain quantization and coding.
In order to run the demo functionality you should be inside the `source/level_x` directory. ### Implemented Components
### Run Level 1 Demo - Scalar quantization
- Dequantization
- Basic bitstream formatting
- Integration into encoder/decoder pipeline:
- `aac_coder_2()`
- `i_aac_coder_2()`
- Demo:
- `demo_aac_2()`
Run the Level 1 demo by providing an input WAV file and an output WAV file: ### Validation
- SNR-based quality evaluation
- Consistency tests between quantizer and inverse quantizer
- End-to-end reconstruction tests
This level introduces compression and controlled signal degradation.
---
## Level 3 Psychoacoustic Model & Perceptual Coding
### Goal
Incorporate perceptual modeling to improve compression efficiency.
### Implemented Components
- Psychoacoustic model
- Masking threshold estimation
- TNS (Temporal Noise Shaping)
- Adaptive quantization
- Full encoding/decoding pipeline:
- `aac_coder_3()`
- `i_aac_coder_3()`
- Demo:
- `demo_aac_3()`
### Validation
- Perceptual improvements compared to Level 2
- Stability tests
- End-to-end evaluation
This level approximates a simplified perceptual AAC-like encoder.
---
# How to Run
All commands assume you are inside:
```
source/
```
---
## Run Level Demo
Navigate to the desired level:
```
cd source/level_x
```
Run:
```bash ```bash
python -m level_1 <input.wav> <output.wav> python -m level_x <input.wav> <output.wav>
``` ```
Example: Example:
```bash ```bash
python -m level_1 material/LicorDeCalandraca.wav material/LicorDeCalandraca_out.wav python -m level_1 material/LicorDeCalandraca.wav material/LicorDeCalandraca_out.wav
``` ```
The demo prints the overall SNR (in dB) between the original and reconstructed audio.
### How to Run Tests The demo prints:
Tests are written and can get executed using `pytest` and are organized per level. - Overall SNR (dB)
In order to run the demo functionality you should be inside the `source/` directory. - Processing information
The repository includes a `pytest.ini` file inside the `source/` directory. ---
This file explicitly sets the Python module search path so that imports such as the followings
work consistently when running tests from the command line. # Running Tests
Tests are written using `pytest`.
A `pytest.ini` file is included in `source/` to ensure proper module resolution.
From inside `source/`:
From inside `source/`, run all tests:
```bash ```bash
pytest -v pytest -v
``` ```
To run only `level_1/tests` or a specific test file: Run specific level tests:
```bash ```bash
pytest -v level_1/tests pytest -v level_1/tests
pytest -v level_2/tests
pytest -v level_3/tests
```
Run a specific test file:
```bash
pytest -v level_1/tests/test_SSC.py pytest -v level_1/tests/test_SSC.py
``` ```
## Disclaimer ---
This project was developed solely for educational purposes. # Reproducibility
It is provided "as is", without any express or implied warranties.
- Python version: 3.x
- Tests validated with `pytest`
- No external dependencies beyond assignment requirements
- Deterministic pipeline execution
---
# Disclaimer
This project was developed solely for educational purposes as part of the Multimedia course at AUTh.
It is provided **"as is"**, without any express or implied warranties.
The author assumes no responsibility for any misuse, data loss, security incidents, or damages resulting from the use of this software. The author assumes no responsibility for any misuse, data loss, security incidents, or damages resulting from the use of this software.
This implementation should not be used in production environments. This implementation should not be used in production environments.
+6
View File
@@ -0,0 +1,6 @@
# Report related files
*.aux
*.out
*.log
*.synctex.gz
_minted-report/*
Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
View File
Binary file not shown.
+586
View File
@@ -0,0 +1,586 @@
%
% !TEX TS-program = xelatex
% !TEX encoding = UTF-8 Unicode
% !TEX spellcheck = el-GR
%
% Information Systems Security assignment report
%
% Requires compilation with XeLaTeX
%
% authors:
% Χρήστος Χουτουρίδης ΑΕΜ 8997
% cchoutou@ece.auth.gr
% Options:
%
% 1) mainlang=<language>
% Default: english
% Set the default language of the document which affects hyphenations,
% localization (section, dates, etc...)
%
% example: \documentclass[mainlang=greek]{AUThReport}
%
% 2) <language>
% Add hyphenation and typesetting support for other languages
% Currently supports: english, greek, german, frenc
%
% example: \documentclass[english, greek]{AUThReport}
%
% 3) short: Requests a shorter title for the document
% Default: no short
%
% example: \documentclass[short]{AUThReport}
%
\documentclass[a4paper, 11pt, mainlang=greek, english]{AUThReport/AUThReport}
\CurrentDate{\today}
% Greek report document setup suggestions
%---------------------------------
% Document configuration
\AuthorName{Χρήστος Χουτουρίδης}
\AuthorAEM{8997}
\AuthorMail{cchoutou@ece.auth.gr}
%\CoAuthorName{CoAuthor Name}
%\CoAuthorAEM{AEM}
%\CoAuthorMail{CoAuthor Mail}
% \WorkGroup{Ομάδα Χ}
\DocTitle{Εργασία Εξαμήνου}
\DocSubTitle{Απλοποιημένη κωδικοποίηση-αποκωδικοποίηση AAC}
\Department{Τμήμα ΗΜΜΥ. Τομέας Ηλεκτρονικής}
\ClassName{Συστήματα Πολυμέσων και Εικονική Πραγματικότητα}
\InstructorName{Αναστάσιος Ντελόπουλος}
\InstructorMail{antelopo@ece.auth.gr}
%\CoInstructorName{}
%\CoInstructorMail{}
% Local package requirements
%---------------------------------
%\usepackage{tabularx}
%\usepackage{array}
%\usepackage{commath}
\usepackage{amsmath, amssymb, amsfonts}
\usepackage{graphicx}
\usepackage{caption}
\usepackage{subcaption}
\usepackage{float}
\captionsetup[figure]{name=Εικόνα}
% Requires: -shell-escape compile argument
\usepackage{minted}
\usepackage{xcolor} %
\setminted[python]{
fontsize=\small,
breaklines,
autogobble,
baselinestretch=1.1,
tabsize=2,
numbersep=8pt,
startinline,
gobble=0
}
\setminted[bash]{
fontsize=\small,
breaklines,
autogobble,
baselinestretch=1.1,
tabsize=2,
numbersep=8pt,
startinline,
gobble=0
}
\newcommand{\repo}{https://git.hoo2.net/hoo2/Multimedia_AAC_Project}
\begin{document}
% Request a title page or header
\InsertTitle
\section{Εισαγωγή}
Η παρούσα εργασία αφορά την υλοποίηση ενός απλοποιημένου κωδικοποιητή και αποκωδικοποιητή ήχου κατά το πρότυπο \textbf{Advanced Audio Coding} (AAC).
Το AAC αποτελεί μία μέθοδο κωδικοποίησης μετασχηματισμού (transform coding) η οποία συνδυάζει ανάλυση στο πεδίο της συχνότητας, ψυχοακουστικό μοντέλο και κωδικοποίηση εντροπίας, με στόχο την αποδοτική συμπίεση ηχητικών σημάτων υψηλής ποιότητας.
Η βασική αρχή λειτουργίας του βασίζεται στη μείωση της πλεονάζουσας πληροφορίας μέσω του μετασχηματισμού \textbf{MDCT} και στην εκμετάλλευση των ιδιοτήτων της ανθρώπινης ακοής ώστε να επιτρέπεται ελεγχόμενη απώλεια πληροφορίας που δεν είναι αντιληπτή.
Στο πλαίσιο της εργασίας υλοποιούμε μία \textit{απλοποιημένη εκδοχή} του προτύπου, όπως αυτή περιγράφεται στην εκφώνηση, παραλείποντας ορισμένες βαθμίδες όπως το Mid/Side stereo και το Bit Reservoir, διατηρώντας όμως τον βασικό κορμό της αλυσίδας κωδικοποίησης.
Συγκεκριμένα, αναπτύσσουμε διαδοχικά τις βαθμίδες Sequence Segmentation Control, Filterbank (MDCT/IMDCT), Temporal Noise Shaping, Psychoacoustic Model, Quantization και Huffman coding, καθώς και τις αντίστροφες διαδικασίες αποκωδικοποίησης.
Στην παρούσα αναφορά περιγράφουμε τη θεωρητική βάση κάθε βαθμίδας, τον τρόπο υλοποίησής της και τα αποτελέσματα που προκύπτουν από την πειραματική αξιολόγηση του συστήματος.
\subsection{Παραδοτέα}
Τα παραδοτέα της εργασίας αποτελούνται από:
\begin{itemize}
\item Την παρούσα αναφορά.
\item Τον κεντρικό κατάλογο \texttt{source} που περιέχει τους ζητούμενους από την εκφώνηση καταλόγους \texttt{level\_<i>}, $i=1,2,3$ με τον κώδικα της εφαρμογής.
Ο καθένας περιέχει ένα αντίστοιχο script \texttt{level\_<i>.py} που καλεί την demonstration συνάρτηση του αντίστοιχου level.
Επίσης ο κατάλογος \texttt{source} περιέχει και τον κατάλογο \texttt{core}, όπου έγινε η κεντρική ανάπτυξη του κώδικά και των tests.
\item Το \href{\repo}{σύνδεσμο} με το αποθετήριο που περιέχει όλο το project με τον κώδικα της εφαρμογής και της παρούσας αναφοράς.
\end{itemize}
\section{Υλοποίηση}
Η υλοποίηση οργανώνεται \textbf{αρθρωτά}, με σαφή διαχωρισμό των επιμέρους λειτουργικών βαθμίδων, ώστε κάθε στάδιο της κωδικοποίησης και της αποκωδικοποίησης να μπορεί να ελεγχθεί και να επαληθευτεί ανεξάρτητα.
Η δομή αυτή μας επιτρέπει να αναπτύξουμε σταδιακά τρία επίπεδα ολοκλήρωσης, από την πλήρως αναστρέψιμη μετασχηματιστική κωδικοποίηση έως την πλήρη απωλεστική κωδικοποίηση με ψυχοακουστικό έλεγχο και κωδικοποίηση εντροπίας.
Οι επιμέρους βαθμίδες του κωδικοποιητή και του αποκωδικοποιητή υλοποιήθηκαν ως \textbf{ανεξάρτητα modules}, τα οποία επαναχρησιμοποιούνται σε κάθε κατάλογο \texttt{level\_<i>}.
Αν και η δομή του παραδοτέου δίνει την εντύπωση ότι οι κατάλογοι των επιπέδων περιέχουν αντίγραφα των ίδιων αρχείων, στην πράξη αυτό δεν ισχύει.
Η αρχική μας προσέγγιση ήταν η ύπαρξη ενός κεντρικού καταλόγου με κοινή υλοποίηση για όλα τα επίπεδα, ωστόσο οι περιορισμοί της εκφώνησης δεν επέτρεπαν τέτοια οργάνωση.
Αντί της χρήσης υπο-αποθετηρίων, επιλέξαμε τη λύση των hard links, ώστε τα αρχεία του κοινού πυρήνα να εμφανίζονται σε κάθε κατάλογο επιπέδου χωρίς να υπάρχει πραγματικός διπλασιασμός του κώδικα.
Με τον τρόπο αυτό διατηρούμε \textbf{ένα ενιαίο σημείο που συντηρείται ο κώδικας} και αποφεύγουμε αποκλίσεις μεταξύ των επιπέδων.
Κάθε module αναπτύχθηκε ανεξάρτητα, με εκτεταμένη χρήση αυτοματοποιημένων δοκιμών.
Η ανάπτυξη ακολούθησε προσέγγιση \textbf{Test-Driven Development}, όπου τα tests σχεδιάστηκαν με βάση το δημόσιο interface κάθε module και περιγράφουν τη λειτουργική του συμπεριφορά.
Τα tests έχουν λειτουργικό και συμβατικό χαρακτήρα (functional and contract-based) και ελέγχουν σχήματα δεδομένων, αριθμητική σταθερότητα, οριακές περιπτώσεις και ιδιότητες αναστρεψιμότητας.
Η πρακτική αυτή μας επέτρεψε να εκτελούμε εύκολα \textbf{regression tests} μετά από κάθε τροποποίηση, διασφαλίζοντας ότι δεν εισάγονται ανεπιθύμητες παρενέργειες σε άλλα μέρη του συστήματος.
Δημιουργήσαμε επίσης wrapper module για το δοθέν module κωδικοποίησης Huffman.
Κατά την ενσωμάτωσή του \textbf{εντοπίστηκε σφάλμα τύπου off-by-one} στον μηχανισμό αποκωδικοποίησης escape τιμών του codebook 11.
Το σφάλμα αφορούσε τον χειρισμό του bit διαχωρισμού (delimiter) στη μορφή κωδικοποίησης των escape τιμών.
Συγκεκριμένα, κατά την αποκωδικοποίηση μετρούνταν σωστά τα N διαδοχικά bits τιμής ‘1’, όμως το επόμενο bit ‘0’, που λειτουργεί ως διαχωριστής, δεν παραλειπόταν πριν την ανάγνωση των N+4 bits του payload.
Ως αποτέλεσμα, το bit διαχωρισμού συμπεριλαμβανόταν εσφαλμένα στα bits της escape τιμής, οδηγώντας σε μετατόπιση κατά ένα bit και σε λανθασμένη ανακατασκευή του μεγέθους.
Το σφάλμα αυτό δεν προκαλούσε εξαίρεση εκτέλεσης, αλλά αλλοίωνε τις διαφορές των scalefactors.
Η διόρθωση συνίσταται στη σωστή κατανάλωση του bit διαχωρισμού πριν την ανάγνωση των N+4 bits της escape τιμής.
Η προτεινόμενη αλλαγή κοινοποιήθηκε στους διδάσκοντες και έγινε αποδεκτή.
Συνοπτικά, τα βασικά modules του συστήματος είναι τα ακόλουθα:
\begin{itemize}
\item \textbf{\texttt{aac\_ssc}}: Υλοποίηση της βαθμίδας Sequence Segmentation Control.
\item \textbf{\texttt{aac\_filterbank}}: Υλοποίηση MDCT/IMDCT και διαχείριση παραθύρων.
\item \textbf{\texttt{aac\_tns}}: Υλοποίηση Temporal Noise Shaping.
\item \textbf{\texttt{aac\_psycho}}: Υλοποίηση ψυχοακουστικού μοντέλου και υπολογισμός SMR.
\item \textbf{\texttt{aac\_quantizer}}: Υλοποίηση μη ομοιόμορφου κβαντιστή και αντίστροφης κβάντισης.
\item \textbf{\texttt{aac\_huffman}}: Διαχείριση κωδικοποίησης και αποκωδικοποίησης Huffman.
\item \textbf{\texttt{aac\_coder} / \texttt{aac\_decoder}}: Σύνθεση της πλήρους ροής κωδικοποίησης και αποκωδικοποίησης.
\item \textbf{\texttt{aac\_utils}}: Βοηθητικές συναρτήσεις και μετρικές αξιολόγησης.
\end{itemize}
Για την επιτυχή εκτέλεση του κώδικα απαιτείται η εγκατάσταση των εξαρτήσεων.
Για το λόγο αυτό από τον κεντρικό κατάλογο πρέπει να εκτελεστεί:
\begin{minted}{bash}
pip install -r requirements.txt
\end{minted}
Τα αυτοματοποιημένα tests δεν απαιτούνται από την εκφώνηση και δεν εκτελούνται από τα scripts των επιπέδων.
Παρ' όλα αυτά, ο αναγνώστης μπορεί να τα εκτελέσει από τον κεντρικό κατάλογο του project:
\begin{minted}{bash}
pytest -v
# to run a specific module (for example filterbank)
pytest -v core/tests/test_filterbank.py
\end{minted}
\section{Level 1 -- SSC και MDCT}
\subsection{Απαιτήσεις Level 1}
Στο πρώτο επίπεδο ζητείται η υλοποίηση ενός πλήρως αναστρέψιμου συστήματος κωδικοποίησης--αποκωδικοποίησης βασισμένου στον μετασχηματισμό MDCT.
Το επίπεδο αυτό δεν περιλαμβάνει ψυχοακουστικό μοντέλο, κβάντιση ή κωδικοποίηση εντροπίας.
Στόχος είναι η σωστή υλοποίηση του Sequence Segmentation Control και του Filterbank, έτσι ώστε \textbf{η ανακατασκευή του σήματος να είναι αριθμητικά ταυτόσημη με το αρχικό}, μέχρι αριθμητική ακρίβεια κινητής υποδιαστολής.
Το Level 1 λειτουργεί ως θεμέλιο για τα επόμενα επίπεδα, καθώς οποιοδήποτε σφάλμα στο μετασχηματιστικό στάδιο θα μεταφερόταν και θα ενισχυόταν στα επόμενα στάδια.
\subsection{Modules που χρησιμοποιούνται}
Για το Level 1 χρησιμοποιούνται τα εξής modules:
\begin{itemize}
\item \textbf{\texttt{aac\_ssc}} για την επιλογή τύπου frame.
\item \textbf{\texttt{aac\_filterbank}} για MDCT/IMDCT και OLA.
\item \textbf{\texttt{aac\_coder\_1}} και \textbf{\texttt{aac\_decoder\_1}} για τη σύνθεση της ροής.
\item \textbf{\texttt{aac\_utils}} για βοηθητικές συναρτήσεις και υπολογισμό SNR.
\end{itemize}
\subsection{Sequence Segmentation Control}
Η βαθμίδα SSC υλοποιεί τον μηχανισμό επιλογής τύπου παραθύρου (OLS, LSS, ESH, LPS) με βάση την ανίχνευση spikes (attack detection).
Η ανίχνευση βασίζεται στον υπολογισμό της ενέργειας σε υπο-τμήματα μήκους 128 δειγμάτων και στον λόγο διαδοχικών ενεργειών.
Η υλοποίηση είναι πλήρως συμμετρική για τα δύο κανάλια και ο τελικός τύπος frame προκύπτει από συγχώνευση των δύο καναλιών σύμφωνα με τον πίνακα μετάβασης.
Επίσης, δόθηκε ιδιαίτερη προσοχή στη αριθμητική σταθερότητα, ώστε να αποφεύγονται διαιρέσεις με μηδενική ενέργεια.
\subsection{Filterbank και MDCT}
Το module \texttt{aac\_filterbank} υλοποιεί τον MDCT και τον αντίστροφό του IMDCT σε διακριτή μορφή.
Χρησιμοποιούνται παράθυρα τύπου SIN και KBD, σύμφωνα με τον τύπο frame.
Η υλοποίηση διαχειρίζεται σωστά τις μεταβάσεις μεταξύ παραθύρων, εφαρμόζοντας overlap-add (OLA) ώστε να επιτυγχάνεται perfect reconstruction σε steady state.
Τόσο η ιδιότητα γραμμικότητας του μετασχηματισμού, όσο και η σχέση ενίσχυσης:
\[
\text{MDCT}(\text{IMDCT}(X)) \approx 2X
\]
ελέγχθηκαν αριθμητικά στο πλαίσιο των tests.
Ενδεικτικά να αναφαίρουμε το παράδειγμα στον έλεγχος της σχέσης ενίσχυσης:
\begin{minted}{python}
X: MdctCoeffs = rng.normal(size=K)
x: TimeSignal = imdct(X)
X_hat: MdctCoeffs = mdct(x)
_assert_allclose(X_hat, 2.0 * X, rtol=tolerance, atol=tolerance)
\end{minted}
\subsection{Φιλοσοφία Ελέγχου Ορθότητας}
Ως γνωστόν, στον προγραμματισμό και στο quality assurance, \textbf{δεν μπορεί να υπάρχει απόλυτη απόδειξη ορθότητας}.
Υπάρχει μόνο μείωση της πιθανότητας σφάλματος μέσω στοχευμένων ελέγχων.
Για το Level 1 ελέγχθηκαν:
\begin{itemize}
\item Ιδιότητες γραμμικότητας MDCT/IMDCT.
\item Απουσία NaN/Inf σε τυχαίες εισόδους.
\item Σωστή διαχείριση μεταβάσεων παραθύρων.
\item Ορθή συμπεριφορά SSC σε edge cases.
\item End-to-end ταυτότητα σήματος με υψηλό SNR.
\end{itemize}
Τα tests μπορούν να εκτελεστούν με:
\begin{minted}{bash}
pytest -v core/tests/test_filterbank.py
pytest -v core/tests/test_ssc.py
pytest -v core/tests/test_utils.py
\end{minted}
Η επιτυχής εκτέλεση των παραπάνω tests διασφαλίζει ότι το Level 1 αποτελεί αριθμητικά σταθερό θεμέλιο για τα επόμενα επίπεδα.
\subsection{Demo και Αποτελέσματα}
Η demonstration του Level 1 μπορεί να εκτελεστεί με:
\begin{minted}{bash}
cd level_1
python -m level_1 material/LicorDeCalandraca.wav material/LicorDeCalandraca_out_l1.wav
\end{minted}
Η έξοδος που προέκυψε ήταν:
\begin{minted}{text}
Encoding ...................... done
Decoding ...................... done
SNR = 257.437 dB
\end{minted}
Η τιμή SNR άνω των 250 dB \textbf{υποδηλώνει πρακτικά αριθμητική ταυτότητα μεταξύ αρχικού και ανακατασκευασμένου σήματος}.
Η απόκλιση οφείλεται αποκλειστικά σε \textbf{αριθμητική ακρίβεια κινητής υποδιαστολής} και όχι σε δομικό σφάλμα της υλοποίησης.
Το αποτέλεσμα αυτό επιβεβαιώνει ότι η υλοποίηση του Filterbank και του SSC είναι συνεπής και πλήρως αναστρέψιμη.
\section{Level 2 Temporal Noise Shaping}
\subsection{Απαιτήσεις Level 2}
Στο δεύτερο επίπεδο ζητείται η ενσωμάτωση της βαθμίδας \textbf{Temporal Noise Shaping (TNS)} στη ροή κωδικοποίησης.
Το TNS εφαρμόζεται στο πεδίο των συντελεστών MDCT και βασίζεται σε γραμμική πρόβλεψη.
Στόχος της βαθμίδας είναι η χρονική ανακατανομή του σφάλματος κβάντισης, ώστε το αντιληπτό σφάλμα να κατανέμεται χρονικά με πιο ευνοϊκό τρόπο.
Στο Level 2 η διαδικασία παραμένει πλήρως αναστρέψιμη.
Η κβάντιση των συντελεστών πρόβλεψης γίνεται με συγκεκριμένο βήμα και σε περιορισμένο εύρος τιμών, ώστε να διασφαλίζεται η σταθερότητα του αντίστροφου φίλτρου.
\subsection{Modules που χρησιμοποιούνται}
Για το Level 2 χρησιμοποιούνται:
\begin{itemize}
\item \textbf{\texttt{aac\_ssc}}.
\item \textbf{\texttt{aac\_filterbank}}.
\item \textbf{\texttt{aac\_tns}}.
\item \textbf{\texttt{aac\_coder\_2}} και \textbf{\texttt{aac\_decoder\_2}}.
\item \textbf{\texttt{aac\_utils}}.
\end{itemize}
Το νέο στοιχείο σε σχέση με το Level 1 είναι το module \texttt{aac\_tns}, το οποίο εφαρμόζεται μετά τον MDCT και πριν από την αντίστροφη διαδικασία στον αποκωδικοποιητή.
\subsection{Υλοποίηση της βαθμίδας TNS}
Για κάθε frame υπολογίζονται συντελεστές γραμμικής πρόβλεψης τάξης \texttt{PRED\_ORDER} (4).
Στην περίπτωση ESH, η διαδικασία εφαρμόζεται ανεξάρτητα σε κάθε ένα από τα 8 υπο-frames.
Οι συντελεστές πρόβλεψης κβαντίζονται με σταθερό βήμα \texttt{QUANT\_STEP} (0.1) και περιορίζονται στο διάστημα $[-\texttt{QUANT\_MAX}(-0.7), +\texttt{QUANT\_MAX}(+0.7)]$.
Το φίλτρο που ορίζεται από τους συντελεστές $\{a_k\}$ έχει μορφή:
\[
H(z) = 1 - \sum_{k=1}^{p} a_k z^{-k}.
\]
Στον κωδικοποιητή το φίλτρο εφαρμόζεται σε μορφή \textbf{FIR}.
Κάθε νέος συντελεστής MDCT προκύπτει αφαιρώντας γραμμικό συνδυασμό προηγούμενων συντελεστών.
Η επιλογή FIR μορφής εξασφαλίζει αριθμητική σταθερότητα, καθώς δεν υπάρχει ανατροφοδότηση.
Στον αποκωδικοποιητή εφαρμόζεται το αντίστροφο φίλτρο:
\[
H^{-1}(z) = \frac{1}{1 - \sum_{k=1}^{p} a_k z^{-k}},
\]
το οποίο έχει μορφή \textbf{IIR}.
Η αντιστροφή υλοποιείται αναδρομικά, επαναφέροντας τους αρχικούς συντελεστές MDCT.
Η διαδικασία αυτή είναι πλήρως αναστρέψιμη υπό την προϋπόθεση ότι το IIR φίλτρο είναι σταθερό.
Η σταθερότητα απαιτεί όλες οι ρίζες του πολυωνύμου:
\[
z^p - a_1 z^{p-1} - \dots - a_p = 0
\]
να βρίσκονται \textbf{εντός του μοναδιαίου κύκλου}.
Ακόμη και μικρή υπέρβαση της μοναδιαίας ακτίνας θα οδηγούσε σε εκθετική ενίσχυση κατά την αναδρομική εφαρμογή του φίλτρου και σε αριθμητική υπερχείλιση.
\subsection{Φιλοσοφία Ελέγχου Ορθότητας}
Όπως και στο Level 1, δεν υπάρχει απόλυτη μαθηματική απόδειξη ορθότητας.
Για τη βαθμίδα TNS ελέγχθηκαν:
\begin{itemize}
\item Σωστά σχήματα εξόδου για long και ESH frames.
\item Κβάντιση πάνω στο επιθυμητό πλέγμα τιμών.
\item Περιορισμός συντελεστών στο επιτρεπτό εύρος.
\item Ρητός έλεγχος σταθερότητας μέσω υπολογισμού ριζών.
\item Ιδιότητα round-trip: $\text{iTNS}(\text{TNS}(X)) \approx X$.
\item Απουσία NaN/Inf σε τυχαίες και οριακές εισόδους.
\end{itemize}
Ο έλεγχος σταθερότητας υλοποιείται αριθμητικά με την εξής λογική:
\begin{minted}{python}
poly = np.empty(p + 1)
poly[0] = 1.0
poly[1:] = -a_q
roots = np.roots(poly)
margin = 1e-12
assert np.all(np.abs(roots) < (1.0 - margin))
\end{minted}
Η ιδιότητα αντιστρεψιμότητας ελέγχεται μέσω round-trip ελέγχου:
\begin{minted}{python}
frame_F_tns, coeffs = aac_tns(frame_F_in, frame_type)
frame_F_hat = aac_i_tns(frame_F_tns, frame_type, coeffs)
np.testing.assert_allclose(frame_F_hat, frame_F_in)
\end{minted}
Οι παραπάνω έλεγχοι δεν αποδεικνύουν μαθηματικά την ορθότητα, αλλά διασφαλίζουν ότι το ζεύγος FIR/IIR λειτουργεί ως ακριβές αντίστροφο εντός αριθμητικής ακρίβειας και ότι η κβάντιση δεν εισάγει αστάθεια.
Τα ακριβή tests μπορούν να εκτελεστούν με:
\begin{minted}{bash}
pytest -v core/tests/test_tns.py
\end{minted}
\subsection{Demo και Αποτελέσματα}
Η demonstration του Level 2 εκτελείται με:
\begin{minted}{bash}
cd level_2
python -m level_2 material/LicorDeCalandraca.wav material/LicorDeCalandraca_out_l2.wav
\end{minted}
Η έξοδος που προέκυψε ήταν:
\begin{minted}{text}
Encoding ...................... done
Decoding ...................... done
SNR = 257.437 dB
\end{minted}
Η τιμή SNR είναι πρακτικά ταυτόσημη με εκείνη του Level 1.
Το αποτέλεσμα αυτό επιβεβαιώνει ότι η ενσωμάτωση του TNS \textbf{δεν εισάγει απώλεια πληροφορίας στο συγκεκριμένο στάδιο}.
Η υλοποίηση του FIR/IIR ζεύγους και ο έλεγχος σταθερότητας εξασφαλίζουν \textbf{πλήρη αναστρεψιμότητα} της διαδικασίας.
\section{Level 3 – Πλήρης Απωλεστική Κωδικοποίηση}
\subsection{Απαιτήσεις Level 3}
Στο τρίτο επίπεδο ζητείται η υλοποίηση της \textbf{πλήρους απωλεστικής αλυσίδας κωδικοποίησης}.
Προστίθενται το ψυχοακουστικό μοντέλο, ο μη ομοιόμορφος κβαντιστής και η κωδικοποίηση Huffman.
Σε αντίθεση με τα προηγούμενα επίπεδα, το Level 3 \textbf{δεν είναι πλέον πλήρως αναστρέψιμο}.
Η απώλεια πληροφορίας είναι συνειδητή και ελεγχόμενη, με βάση τις αρχές της ψυχοακουστικής απόκρυψης.
Στόχος είναι η μείωση του bitrate διατηρώντας αποδεκτή αντιληπτή ποιότητα.
\subsection{Modules που χρησιμοποιούνται}
Για το Level 3 χρησιμοποιούνται όλα τα modules του συστήματος:
\begin{itemize}
\item \textbf{\texttt{aac\_ssc}}.
\item \textbf{\texttt{aac\_filterbank}}.
\item \textbf{\texttt{aac\_tns}}.
\item \textbf{\texttt{aac\_psycho}}.
\item \textbf{\texttt{aac\_quantizer}}.
\item \textbf{\texttt{aac\_huffman}}.
\item \textbf{\texttt{aac\_coder\_3}} και \textbf{\texttt{aac\_decoder\_3}}.
\item \textbf{\texttt{aac\_utils}}.
\end{itemize}
Το Level 3 αποτελεί σύνθεση όλων των προηγούμενων βαθμίδων με προσθήκη κβάντισης και κωδικοποίησης εντροπίας.
\subsection{Ψυχοακουστικό Μοντέλο}
Το module \texttt{aac\_psycho} υπολογίζει το Signal-to-Mask Ratio (SMR) για κάθε Bark band.
Η διαδικασία βασίζεται στους πίνακες B219a και B219b για long και short frames αντίστοιχα.
Υπολογίζεται η ενέργεια ανά band, εφαρμόζεται spreading function και κατώφλι απόλυτης ακουστότητας.
Το SMR καθορίζει το επιτρεπτό σφάλμα κβάντισης σε κάθε band.
Δόθηκε ιδιαίτερη προσοχή:
\begin{itemize}
\item Στην αποφυγή διαίρεσης με μηδέν μέσω χρήσης μικρού όρου \texttt{EPS}.
\item Στον έλεγχο ότι όλες οι ενδιάμεσες ποσότητες παραμένουν πεπερασμένες.
\end{itemize}
\subsection{Κβαντιστής και Scalefactors}
Το module \texttt{aac\_quantizer} εφαρμόζει μη ομοιόμορφη κβάντιση των συντελεστών MDCT.
Για κάθε band επιλέγεται scalefactor που ικανοποιεί το αντίστοιχο SMR.
Οι scalefactors κωδικοποιούνται διαφορικά (DPCM).
Η ανακατασκευή γίνεται αθροιστικά και εφαρμόζεται εκθετικός παράγοντας της μορφής:
\[
\hat{X} = \text{sign}(S) |S|^{4/3} \times 2^{\alpha/4}.
\]
Η συγκεκριμένη μορφή καθιστά το σύστημα \textbf{ιδιαίτερα ευαίσθητο σε αριθμητικά σφάλματα}.
Για τον λόγο αυτό χρησιμοποιήθηκαν:
\begin{itemize}
\item Έλεγχοι πεπερασμένων τιμών (finite checks).
\item Έλεγχος αποφυγής overflow.
\item Προστασία με \texttt{EPS} σε παρονομαστές.
\end{itemize}
\subsection{Κωδικοποίηση Huffman}
Οι κβαντισμένοι συντελεστές και τα DPCM scalefactors κωδικοποιούνται με Huffman.
Το bitstream ανακατασκευάζεται πλήρως στον αποκωδικοποιητή.
Ιδιαίτερη προσοχή δόθηκε:
\begin{itemize}
\item Στη σωστή \textbf{διαχείριση escape τιμών}.
\item Στην ορθή \textbf{κατανάλωση bitstreams}.
\item Στην \textbf{αποφυγή out-of-bounds} προσπελάσεων.
\end{itemize}
\subsection{Φιλοσοφία Ελέγχου Ορθότητας}
Το Level 3 \textbf{δεν μπορεί να ελεγχθεί με κριτήριο ταυτότητας}.
Ελέγχθηκε με κριτήρια λειτουργικής ορθότητας και αριθμητικής σταθερότητας.
Συγκεκριμένα ελέγχθηκαν:
\begin{itemize}
\item Ορθότητα DPCM ανακατασκευής scalefactors.
\item Συνεπής αντιστοίχιση ESH packing και unpacking.
\item Απουσία NaN/Inf σε όλη τη ροή.
\item Απουσία εκθετικής υπερχείλισης.
\item Έλεγχος μηδενικού lag μεταξύ αρχικού και ανακατασκευασμένου σήματος.
\item Έλεγχος μη ανταλλαγής καναλιών (L/R swap detection).
\item Έλεγχος απουσίας clipping.
\item Έλεγχος διατήρησης συνολικού gain.
\end{itemize}
Ο \textbf{έλεγχος lag} υλοποιείται μέσω εκτίμησης χρονικής μετατόπισης.
Ο έλεγχος καναλιών διασφαλίζει ότι η \textbf{ενέργεια του αριστερού και δεξιού καναλιού δεν έχει ανταλλαγεί}.
Οι έλεγχοι αυτοί διασφαλίζουν ότι η απώλεια ποιότητας οφείλεται αποκλειστικά στην κβάντιση και όχι σε δομικό σφάλμα.
Τα tests μπορούν να εκτελεστούν με:
\begin{minted}{bash}
pytest -v core/tests/test_psycho.py
pytest -v core/tests/test_quantizer.py
pytest -v core/tests/test_huffman.py
pytest -v core/tests/test_aac_coder_decoder.py
\end{minted}
\subsection{Demo και Αποτελέσματα}
Η demonstration του Level 3 εκτελείται με:
\begin{minted}{bash}
cd level_3
python -m level_3 material/LicorDeCalandraca.wav material/LicorDeCalandraca_out_l3.wav material/aac_seq_3.mat
\end{minted}
Η έξοδος που προέκυψε ήταν:
\begin{minted}{text}
Storing coded sequence to material/aac_seq_3.mat
Encoding ...................... done
Decoding ...................... done
SNR = 9.454 dB
Bitrate (coded) = 216710.22 bits/s
Compression ratio = 7.0881
\end{minted}
Παρατηρούμε πως η τιμή SNR είναι σημαντικά χαμηλότερη σε σχέση με τα προηγούμενα επίπεδα, γεγονός αναμενόμενο λόγω απωλεστικής κβάντισης.
Παρά τη μείωση του SNR, ελέγχθηκε ότι:
\begin{itemize}
\item Δεν υπάρχει χρονική μετατόπιση (lag).
\item Δεν υπάρχει ανταλλαγή καναλιών.
\item Δεν εμφανίζεται clipping.
\item Δεν υπάρχει μη ελεγχόμενη ενίσχυση (gain drift).
\end{itemize}
\begin{figure}[!ht]
\centering
\includegraphics[width=0.75\linewidth]{figures/bitrate_per_frame.png}
\caption{Κατανομή του bitrate ανά frame.
Παρατηρείται σημαντική διακύμανση που αντανακλά την προσαρμοστική φύση της κωδικοποίησης.}
\label{fig:bitrate_per_frame}
\end{figure}
\begin{figure}[!ht]
\centering
\includegraphics[width=0.75\linewidth]{figures/compression_per_frame.png}
\caption{Λόγος συμπίεσης ανά frame.
Η μεταβολή του compression ratio επιβεβαιώνει τη δυναμική κατανομή bit ανάλογα με το περιεχόμενο του σήματος.}
\label{fig:compression_per_frame}
\end{figure}
Η χαμηλή τιμή SNR δεν αντικατοπτρίζει πλήρως την αντιληπτή ποιότητα, καθώς το ψυχοακουστικό μοντέλο επιτρέπει μεγάλο ενεργειακό σφάλμα σε περιοχές όπου το σφάλμα καλύπτεται από το φαινόμενο απόκρυψης.
Η παρατηρούμενη απώλεια αφορά κυρίως λεπτομέρειες υψηλών συχνοτήτων, χωρίς δομική αλλοίωση του σήματος.
Το αποτέλεσμα επιβεβαιώνει ότι η υλοποίηση είναι αριθμητικά σταθερή και ότι η απώλεια ποιότητας είναι αποτέλεσμα ελεγχόμενης κβάντισης και όχι υλοποιητικού σφάλματος.
Όπως φαίνεται στην Εικόνα~\ref{fig:bitrate_per_frame}, παρατηρείται σημαντική διακύμανση του bitrate μεταξύ διαδοχικών frames, με τιμές που κυμαίνονται περίπου \textbf{μεταξύ 160 kbps και 250 kbps} και σποραδικές κορυφές υψηλότερων τιμών.
Η συμπεριφορά αυτή είναι αναμενόμενη, καθώς το ψυχοακουστικό μοντέλο κατανέμει περισσότερα bits σε χρονικές περιοχές με αυξημένη ενεργειακή ή φασματική πολυπλοκότητα.
Αντίστοιχα, στην Εικόνα~\ref{fig:compression_per_frame} παρατηρούμε ότι ο λόγος συμπίεσης μεταβάλλεται δυναμικά, με \textbf{τυπικές τιμές μεταξύ 6:1 και 9:1}.
Οι χαμηλότερες τιμές λόγου συμπίεσης αντιστοιχούν σε frames όπου απαιτούνται περισσότερα bits για την ικανοποίηση του SMR, ενώ οι υψηλότερες τιμές εμφανίζονται σε περιοχές μικρότερης φασματικής πυκνότητας.
Η δυναμική αυτή συμπεριφορά επιβεβαιώνει ότι \textbf{η κωδικοποίηση δεν είναι σταθερού ρυθμού} (CBR), αλλά προσαρμοστική ως προς το περιεχόμενο του σήματος.
Η συνολική μέση τιμή συμπίεσης περίπου \textbf{7:1} επιτυγχάνεται μέσω της συνδυασμένης λειτουργίας ψυχοακουστικής κβάντισης και κωδικοποίησης εντροπίας, χωρίς να παρατηρούνται δομικά σφάλματα όπως χρονική μετατόπιση, ανταλλαγή καναλιών ή ανεξέλεγκτη ενίσχυση.
\section{Συμπεράσματα}
Στην παρούσα εργασία υλοποιήσαμε μία απλοποιημένη αλλά πλήρως λειτουργική αλυσίδα κωδικοποίησης και αποκωδικοποίησης τύπου AAC, ακολουθώντας σταδιακή προσέγγιση τριών επιπέδων.
Ξεκινήσαμε από μία αυστηρά αναστρέψιμη μετασχηματιστική κωδικοποίηση με MDCT και Sequence Segmentation Control, επεκτείναμε το σύστημα με Temporal Noise Shaping διατηρώντας την αριθμητική αντιστρεψιμότητα και ολοκληρώσαμε με την ενσωμάτωση ψυχοακουστικού μοντέλου, μη ομοιόμορφης κβάντισης και κωδικοποίησης εντροπίας.
Η \textbf{αρθρωτή σχεδίαση} και η \textbf{εκτεταμένη χρήση αυτοματοποιημένων δοκιμών μας επέτρεψαν να ελέγχουμε ανεξάρτητα κάθε βαθμίδα και να περιορίζουμε συστηματικά την πιθανότητα σφαλμάτων}.
Δώσαμε ιδιαίτερη έμφαση στη \textbf{αριθμητική σταθερότητα} του συστήματος.
Εφαρμόσαμε ελέγχους σταθερότητας φίλτρων, προστασίες τύπου \texttt{EPS} σε παρονομαστές, ελέγχους πεπερασμένων τιμών, καθώς και ελέγχους απουσίας χρονικής μετατόπισης, ανταλλαγής καναλιών και clipping.
Η φιλοσοφία αυτή αποδείχθηκε κρίσιμη στο Level 3, όπου μικρά αριθμητικά σφάλματα μπορούν να ενισχυθούν εκθετικά μέσω της απο-κβάντισης.
Η συστηματική προσέγγιση testing και οι round-trip έλεγχοι διασφάλισαν ότι η παρατηρούμενη απώλεια ποιότητας οφείλεται αποκλειστικά στη σχεδιασμένη κβάντιση και όχι σε υλοποιητικά σφάλματα.
Τα αποτελέσματα έδειξαν ότι το σύστημα επιτυγχάνει \textbf{λόγο συμπίεσης περίπου 7:1}, με μέσο \textbf{bitrate περίπου 216 kbps}, διατηρώντας αποδεκτή αντιληπτή ποιότητα.
Η ανάλυση ανά frame κατέδειξε ότι η κατανομή bit είναι δυναμική και εξαρτάται από τη φασματική πολυπλοκότητα του σήματος, γεγονός που επιβεβαιώνει τη σωστή λειτουργία του ψυχοακουστικού μοντέλου.
Η σημαντική πτώση του SNR στο Level 3 είναι αναμενόμενη σε ενεργειακούς όρους και δεν αντανακλά πλήρως την αντιληπτή ποιότητα, καθώς το σφάλμα κατανέμεται σύμφωνα με τα φαινόμενα απόκρυψης.
Συνολικά, η υλοποίηση επιβεβαιώνει ότι ακόμη και ένα απλοποιημένο μοντέλο AAC μπορεί να επιτύχει ουσιαστική συμπίεση, εφόσον η σχεδίαση είναι προσεκτική και αριθμητικά συνεπής.
\end{document}
+261 -20
View File
@@ -26,14 +26,91 @@ from pathlib import Path
from typing import Union from typing import Union
import soundfile as sf import soundfile as sf
from scipy.io import savemat
from core.aac_configuration import WIN_TYPE from core.aac_configuration import WIN_TYPE
from core.aac_filterbank import aac_filter_bank from core.aac_filterbank import aac_filter_bank
from core.aac_ssc import aac_SSC from core.aac_ssc import aac_ssc
from core.aac_tns import aac_tns from core.aac_tns import aac_tns
from core.aac_psycho import aac_psycho
from core.aac_quantizer import aac_quantizer # assumes your quantizer file is core/aac_quantizer.py
from core.aac_huffman import aac_encode_huff
from core.aac_utils import get_table, band_limits
from material.huff_utils import load_LUT
from core.aac_types import * from core.aac_types import *
# -----------------------------------------------------------------------------
# Helpers for thresholds (T(b))
# -----------------------------------------------------------------------------
def _band_slices_from_table(frame_type: FrameType) -> list[tuple[int, int]]:
"""
Return inclusive (lo, hi) band slices derived from TableB219.
"""
table, _ = get_table(frame_type)
wlow, whigh, _bval, _qthr_db = band_limits(table)
return [(int(lo), int(hi)) for lo, hi in zip(wlow, whigh)]
def _thresholds_from_smr(
frame_F_ch: FrameChannelF,
frame_type: FrameType,
SMR: FloatArray,
) -> FloatArray:
"""
Compute thresholds T(b) = P(b) / SMR(b), where P(b) is band energy.
Shapes:
- Long: returns (NB, 1)
- ESH: returns (NB, 8)
"""
bands = _band_slices_from_table(frame_type)
NB = len(bands)
X = np.asarray(frame_F_ch, dtype=np.float64)
SMR = np.asarray(SMR, dtype=np.float64)
if frame_type == "ESH":
if X.shape != (128, 8):
raise ValueError("For ESH, frame_F_ch must have shape (128, 8).")
if SMR.shape != (NB, 8):
raise ValueError(f"For ESH, SMR must have shape ({NB}, 8).")
T = np.zeros((NB, 8), dtype=np.float64)
for j in range(8):
Xj = X[:, j]
for b, (lo, hi) in enumerate(bands):
P = float(np.sum(Xj[lo : hi + 1] ** 2))
smr = float(SMR[b, j])
T[b, j] = 0.0 if smr <= 1e-12 else (P / smr)
return T
# Long
if X.shape == (1024,):
Xv = X
elif X.shape == (1024, 1):
Xv = X[:, 0]
else:
raise ValueError("For non-ESH, frame_F_ch must be shape (1024,) or (1024, 1).")
if SMR.shape == (NB,):
SMRv = SMR
elif SMR.shape == (NB, 1):
SMRv = SMR[:, 0]
else:
raise ValueError(f"For non-ESH, SMR must be shape ({NB},) or ({NB}, 1).")
T = np.zeros((NB, 1), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
P = float(np.sum(Xv[lo : hi + 1] ** 2))
smr = float(SMRv[b])
T[b, 0] = 0.0 if smr <= 1e-12 else (P / smr)
return T
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public helpers (useful for level_x demo wrappers) # Public helpers (useful for level_x demo wrappers)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -122,7 +199,10 @@ def aac_pack_frame_f_to_seq_channels(frame_type: FrameType, frame_f: FrameF) ->
# Level 1 encoder # Level 1 encoder
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1: def aac_coder_1(
filename_in: Union[str, Path],
verbose: bool = False
) -> AACSeq1:
""" """
Level-1 AAC encoder. Level-1 AAC encoder.
@@ -139,6 +219,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
filename_in : Union[str, Path] filename_in : Union[str, Path]
Input WAV filename. Input WAV filename.
Assumption: stereo audio, sampling rate 48 kHz. Assumption: stereo audio, sampling rate 48 kHz.
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -165,8 +247,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
aac_seq: AACSeq1 = [] aac_seq: AACSeq1 = []
prev_frame_type: FrameType = "OLS" prev_frame_type: FrameType = "OLS"
win_type: WinType = WIN_TYPE if verbose:
print("Encoding ", end="", flush=True)
for i in range(K): for i in range(K):
start = i * hop start = i * hop
@@ -182,24 +264,32 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64) tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail]) next_t = np.vstack([next_t, tail])
frame_type = aac_SSC(frame_t, next_t, prev_frame_type) frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
frame_f = aac_filter_bank(frame_t, frame_type, win_type) frame_f = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f) chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f)
aac_seq.append({ aac_seq.append({
"frame_type": frame_type, "frame_type": frame_type,
"win_type": win_type, "win_type": WIN_TYPE,
"chl": {"frame_F": chl_f}, "chl": {"frame_F": chl_f},
"chr": {"frame_F": chr_f}, "chr": {"frame_F": chr_f},
}) })
prev_frame_type = frame_type prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
return aac_seq return aac_seq
def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2: def aac_coder_2(
filename_in: Union[str, Path],
verbose: bool = False
) -> AACSeq2:
""" """
Level-2 AAC encoder (Level 1 + TNS). Level-2 AAC encoder (Level 1 + TNS).
@@ -207,6 +297,8 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
---------- ----------
filename_in : Union[str, Path] filename_in : Union[str, Path]
Input WAV filename (stereo, 48 kHz). Input WAV filename (stereo, 48 kHz).
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -238,6 +330,8 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
aac_seq: AACSeq2 = [] aac_seq: AACSeq2 = []
prev_frame_type: FrameType = "OLS" prev_frame_type: FrameType = "OLS"
if verbose:
print("Encoding ", end="", flush=True)
for i in range(K): for i in range(K):
start = i * hop start = i * hop
@@ -250,21 +344,12 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64) tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail]) next_t = np.vstack([next_t, tail])
frame_type = aac_SSC(frame_t, next_t, prev_frame_type) frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
# Level 1 analysis (packed stereo container) # Level 1 analysis (packed stereo container)
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE) frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
# Unpack to per-channel (as you already do in Level 1) chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f_stereo)
if frame_type == "ESH":
chl_f = np.empty((128, 8), dtype=np.float64)
chr_f = np.empty((128, 8), dtype=np.float64)
for j in range(8):
chl_f[:, j] = frame_f_stereo[:, 2 * j + 0]
chr_f[:, j] = frame_f_stereo[:, 2 * j + 1]
else:
chl_f = frame_f_stereo[:, 0:1].astype(np.float64, copy=False)
chr_f = frame_f_stereo[:, 1:2].astype(np.float64, copy=False)
# Level 2: apply TNS per channel # Level 2: apply TNS per channel
chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type) chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type)
@@ -278,7 +363,163 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
"chr": {"frame_F": chr_f_tns, "tns_coeffs": chr_tns_coeffs}, "chr": {"frame_F": chr_f_tns, "tns_coeffs": chr_tns_coeffs},
} }
) )
prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
return aac_seq
def aac_coder_3(
filename_in: Union[str, Path],
filename_aac_coded: Union[str, Path] | None = None,
verbose: bool = False,
) -> AACSeq3:
"""
Level-3 AAC encoder (Level 2 + Psycho + Quantizer + Huffman).
Parameters
----------
filename_in : Union[str, Path]
Input WAV filename (stereo, 48 kHz).
filename_aac_coded : Union[str, Path] | None
Optional .mat filename to store aac_seq_3 (assignment convenience).
verbose : bool
Optional argument to print encoding status
Returns
-------
AACSeq3
Encoded AAC sequence (Level 3 payload schema).
"""
filename_in = Path(filename_in)
x, _ = aac_read_wav_stereo_48k(filename_in)
hop = 1024
win = 2048
pad_pre = np.zeros((hop, 2), dtype=np.float64)
pad_post = np.zeros((hop, 2), dtype=np.float64)
x_pad = np.vstack([pad_pre, x, pad_post])
K = int((x_pad.shape[0] - win) // hop + 1)
if K <= 0:
raise ValueError("Input too short for framing.")
# Load Huffman LUTs once.
huff_LUT_list = load_LUT()
aac_seq: AACSeq3 = []
prev_frame_type: FrameType = "OLS"
# Psycho model needs per-channel history (prev1, prev2) of 2048-sample frames.
prev1_L = np.zeros((2048,), dtype=np.float64)
prev2_L = np.zeros((2048,), dtype=np.float64)
prev1_R = np.zeros((2048,), dtype=np.float64)
prev2_R = np.zeros((2048,), dtype=np.float64)
if verbose:
print("Encoding ", end="", flush=True)
for i in range(K):
start = i * hop
frame_t: FrameT = x_pad[start : start + win, :]
if frame_t.shape != (win, 2):
raise ValueError("Internal framing error: frame_t has wrong shape.")
next_t = x_pad[start + hop : start + hop + win, :]
if next_t.shape[0] < win:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail])
frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
# Analysis filterbank (stereo packed)
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f_stereo)
# TNS per channel
chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type)
chr_f_tns, chr_tns_coeffs = aac_tns(chr_f, frame_type)
# Psychoacoustic model per channel (time-domain)
frame_L = np.asarray(frame_t[:, 0], dtype=np.float64)
frame_R = np.asarray(frame_t[:, 1], dtype=np.float64)
SMR_L = aac_psycho(frame_L, frame_type, prev1_L, prev2_L)
SMR_R = aac_psycho(frame_R, frame_type, prev1_R, prev2_R)
# Thresholds T(b) (stored, not entropy-coded)
T_L = _thresholds_from_smr(chl_f_tns, frame_type, SMR_L)
T_R = _thresholds_from_smr(chr_f_tns, frame_type, SMR_R)
# Quantizer per channel
S_L, sfc_L, G_L = aac_quantizer(chl_f_tns, frame_type, SMR_L)
S_R, sfc_R, G_R = aac_quantizer(chr_f_tns, frame_type, SMR_R)
# Huffman-code ONLY the DPCM differences for b>0.
# sfc[0] corresponds to alpha(0)=G and is stored separately in the frame.
sfc_L_dpcm = np.asarray(sfc_L, dtype=np.int64)[1:, ...]
sfc_R_dpcm = np.asarray(sfc_R, dtype=np.int64)[1:, ...]
# sfc_L_stream, cb_sfc_L = aac_encode_huff(sfc_L_dpcm.reshape(-1, order="F"), huff_LUT_list, force_codebook=11)
# sfc_R_stream, cb_sfc_R = aac_encode_huff(sfc_R_dpcm.reshape(-1, order="F"), huff_LUT_list, force_codebook=11)
sfc_L_stream, cb_sfc_L = aac_encode_huff(sfc_L_dpcm.reshape(-1, order="F"), huff_LUT_list)
sfc_R_stream, cb_sfc_R = aac_encode_huff(sfc_R_dpcm.reshape(-1, order="F"), huff_LUT_list)
if cb_sfc_L != 11 or cb_sfc_R != 11:
raise ValueError(f"Illegal codebook value for frame: {i}: cb_sfc_l={cb_sfc_L}, cb_sfc_r={cb_sfc_R}.")
mdct_L_stream, cb_L = aac_encode_huff(np.asarray(S_L, dtype=np.int64).reshape(-1), huff_LUT_list)
mdct_R_stream, cb_R = aac_encode_huff(np.asarray(S_R, dtype=np.int64).reshape(-1), huff_LUT_list)
# Typed dict construction helps static analyzers validate the schema.
frame_out: AACSeq3Frame = {
"frame_type": frame_type,
"win_type": WIN_TYPE,
"chl": {
"tns_coeffs": np.asarray(chl_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_L, dtype=np.float64),
"G": G_L,
"sfc": sfc_L_stream,
"stream": mdct_L_stream,
"codebook": int(cb_L),
},
"chr": {
"tns_coeffs": np.asarray(chr_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_R, dtype=np.float64),
"G": G_R,
"sfc": sfc_R_stream,
"stream": mdct_R_stream,
"codebook": int(cb_R),
},
}
aac_seq.append(frame_out)
# Update psycho history (shift register)
prev2_L = prev1_L
prev1_L = frame_L
prev2_R = prev1_R
prev1_R = frame_R
prev_frame_type = frame_type prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
# Optional: store to .mat for the assignment wrapper
if filename_aac_coded is not None:
filename_aac_coded = Path(filename_aac_coded)
savemat(
str(filename_aac_coded),
{"aac_seq_3": np.array(aac_seq, dtype=object)},
do_compression=True,
)
return aac_seq
return aac_seq
+11 -1
View File
@@ -15,6 +15,8 @@
from __future__ import annotations from __future__ import annotations
# Imports # Imports
from typing import Final
from core.aac_types import WinType from core.aac_types import WinType
# Filterbank # Filterbank
@@ -28,4 +30,12 @@ WIN_TYPE: WinType = "SIN"
# ------------------------------------------------------------ # ------------------------------------------------------------
PRED_ORDER = 4 PRED_ORDER = 4
QUANT_STEP = 0.1 QUANT_STEP = 0.1
QUANT_MAX = 0.7 # 4-bit symmetric with step 0.1 -> clamp to [-0.7, +0.7] QUANT_MAX = 0.7 # 4-bit symmetric with step 0.1 -> clamp to [-0.7, +0.7]
# -----------------------------------------------------------------------------
# Psycho
# -----------------------------------------------------------------------------
NMT_DB: Final[float] = 6.0 # Noise Masking Tone (dB)
TMN_DB: Final[float] = 18.0 # Tone Masking Noise (dB)
+205 -17
View File
@@ -9,16 +9,9 @@
# cchoutou@ece.auth.gr # cchoutou@ece.auth.gr
# #
# Description: # Description:
# Level 1 AAC decoder orchestration (inverse of aac_coder_1()). # - Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
# Keeps the same functional behavior as the original level_1 implementation: # - Level 2 AAC decoder orchestration (inverse of aac_coder_1()).
# - Re-pack per-channel spectra into FrameF expected by aac_i_filter_bank()
# - IMDCT synthesis per frame
# - Overlap-add with hop=1024
# - Remove encoder boundary padding: hop at start and hop at end
# #
# Note:
# This core module returns the reconstructed samples. Writing to disk is kept
# in level_x demos.
# ------------------------------------------------------------ # ------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
@@ -29,14 +22,27 @@ import soundfile as sf
from core.aac_filterbank import aac_i_filter_bank from core.aac_filterbank import aac_i_filter_bank
from core.aac_tns import aac_i_tns from core.aac_tns import aac_i_tns
from core.aac_quantizer import aac_i_quantizer
from core.aac_huffman import aac_decode_huff
from core.aac_utils import get_table, band_limits
from material.huff_utils import load_LUT
from core.aac_types import * from core.aac_types import *
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public helpers (useful for level_x demo wrappers) # Helper for NB
# -----------------------------------------------------------------------------
def _nbands(frame_type: FrameType) -> int:
table, _ = get_table(frame_type)
wlow, _whigh, _bval, _qthr_db = band_limits(table)
return int(len(wlow))
# -----------------------------------------------------------------------------
# Public helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_unpack_seq_channels_to_frame_f(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF: def aac_unpack_seq_channels(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF:
""" """
Re-pack per-channel spectra from the Level-1 AACSeq1 schema into the stereo Re-pack per-channel spectra from the Level-1 AACSeq1 schema into the stereo
FrameF container expected by aac_i_filter_bank(). FrameF container expected by aac_i_filter_bank().
@@ -109,10 +115,14 @@ def aac_remove_padding(y_pad: StereoSignal, hop: int = 1024) -> StereoSignal:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Level 1 decoder (core) # Level 1 decoder
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoSignal: def aac_decoder_1(
aac_seq_1: AACSeq1,
filename_out: Union[str, Path],
verbose: bool = False
) -> StereoSignal:
""" """
Level-1 AAC decoder (inverse of aac_coder_1()). Level-1 AAC decoder (inverse of aac_coder_1()).
@@ -128,6 +138,8 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
Encoded sequence as produced by aac_coder_1(). Encoded sequence as produced by aac_coder_1().
filename_out : Union[str, Path] filename_out : Union[str, Path]
Output WAV filename. Assumption: 48 kHz, stereo. Output WAV filename. Assumption: 48 kHz, stereo.
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -146,6 +158,8 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
n_pad = (K - 1) * hop + win n_pad = (K - 1) * hop + win
y_pad: StereoSignal = np.zeros((n_pad, 2), dtype=np.float64) y_pad: StereoSignal = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_1): for i, fr in enumerate(aac_seq_1):
frame_type: FrameType = fr["frame_type"] frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"] win_type: WinType = fr["win_type"]
@@ -153,21 +167,32 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
chl_f = np.asarray(fr["chl"]["frame_F"], dtype=np.float64) chl_f = np.asarray(fr["chl"]["frame_F"], dtype=np.float64)
chr_f = np.asarray(fr["chr"]["frame_F"], dtype=np.float64) chr_f = np.asarray(fr["chr"]["frame_F"], dtype=np.float64)
frame_f: FrameF = aac_unpack_seq_channels_to_frame_f(frame_type, chl_f, chr_f) frame_f: FrameF = aac_unpack_seq_channels(frame_type, chl_f, chr_f)
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type) # (2048, 2) frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type) # (2048, 2)
start = i * hop start = i * hop
y_pad[start:start + win, :] += frame_t_hat y_pad[start:start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y: StereoSignal = aac_remove_padding(y_pad, hop=hop) y: StereoSignal = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
# Level 1 assumption: 48 kHz output. # Level 1 assumption: 48 kHz output.
sf.write(str(filename_out), y, 48000) sf.write(str(filename_out), y, 48000)
return y return y
def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoSignal: # -----------------------------------------------------------------------------
# Level 2 decoder
# -----------------------------------------------------------------------------
def aac_decoder_2(
aac_seq_2: AACSeq2,
filename_out: Union[str, Path],
verbose: bool = False
) -> StereoSignal:
""" """
Level-2 AAC decoder (inverse of aac_coder_2). Level-2 AAC decoder (inverse of aac_coder_2).
@@ -185,6 +210,8 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
Encoded sequence as produced by aac_coder_2(). Encoded sequence as produced by aac_coder_2().
filename_out : Union[str, Path] filename_out : Union[str, Path]
Output WAV filename. Output WAV filename.
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -203,6 +230,8 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
n_pad = (K - 1) * hop + win n_pad = (K - 1) * hop + win
y_pad = np.zeros((n_pad, 2), dtype=np.float64) y_pad = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_2): for i, fr in enumerate(aac_seq_2):
frame_type: FrameType = fr["frame_type"] frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"] win_type: WinType = fr["win_type"]
@@ -250,8 +279,167 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
start = i * hop start = i * hop
y_pad[start : start + win, :] += frame_t_hat y_pad[start : start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y = aac_remove_padding(y_pad, hop=hop) y = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
sf.write(str(filename_out), y, 48000) sf.write(str(filename_out), y, 48000)
return y return y
def aac_decoder_3(
aac_seq_3: AACSeq3,
filename_out: Union[str, Path],
verbose: bool = False,
) -> StereoSignal:
"""
Level-3 AAC decoder (inverse of aac_coder_3).
Steps per frame:
- Huffman decode scalefactors (sfc) using codebook 11
- Huffman decode MDCT symbols (stream) using stored codebook
- iQuantizer -> MDCT coefficients after TNS
- iTNS using stored predictor coefficients
- IMDCT filterbank -> time domain
- Overlap-add, remove padding, write WAV
Parameters
----------
aac_seq_3 : AACSeq3
Encoded sequence as produced by aac_coder_3.
filename_out : Union[str, Path]
Output WAV filename.
verbose : bool
Optional argument to print encoding status
Returns
-------
StereoSignal
Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64.
"""
filename_out = Path(filename_out)
hop = 1024
win = 2048
K = len(aac_seq_3)
if K <= 0:
raise ValueError("aac_seq_3 must contain at least one frame.")
# Load Huffman LUTs once.
huff_LUT_list = load_LUT()
n_pad = (K - 1) * hop + win
y_pad = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_3):
frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"]
NB = _nbands(frame_type)
# We store G separately, so Huffman stream contains only (NB-1) DPCM differences.
sfc_len = (NB - 1) * (8 if frame_type == "ESH" else 1)
# -------------------------
# Left channel
# -------------------------
tns_L = np.asarray(fr["chl"]["tns_coeffs"], dtype=np.float64)
G_L = fr["chl"]["G"]
sfc_bits_L = fr["chl"]["sfc"]
mdct_bits_L = fr["chl"]["stream"]
cb_L = int(fr["chl"]["codebook"])
sfc_dec_L = aac_decode_huff(sfc_bits_L, 11, huff_LUT_list)[:sfc_len].astype(np.int64, copy=False)
if frame_type == "ESH":
sfc_dpcm_L = sfc_dec_L.reshape(NB - 1, 8, order="F")
sfc_L = np.zeros((NB, 8), dtype=np.int64)
Gv = np.asarray(G_L, dtype=np.float64).reshape(1, 8)
sfc_L[0, :] = Gv[0, :].astype(np.int64)
sfc_L[1:, :] = sfc_dpcm_L
else:
sfc_dpcm_L = sfc_dec_L.reshape(NB - 1, 1, order="F")
sfc_L = np.zeros((NB, 1), dtype=np.int64)
sfc_L[0, 0] = int(float(G_L))
sfc_L[1:, :] = sfc_dpcm_L
# MDCT symbols: codebook 0 means "all-zero section"
if cb_L == 0:
S_dec_L = np.zeros((1024,), dtype=np.int64)
else:
S_tmp_L = aac_decode_huff(mdct_bits_L, cb_L, huff_LUT_list).astype(np.int64, copy=False)
# Tuple coding may produce extra trailing symbols; caller knows the true length (1024).
# Also guard against short outputs by zero-padding.
if S_tmp_L.size < 1024:
S_dec_L = np.zeros((1024,), dtype=np.int64)
S_dec_L[: S_tmp_L.size] = S_tmp_L
else:
S_dec_L = S_tmp_L[:1024]
S_L = S_dec_L.reshape(1024, 1)
Xq_L = aac_i_quantizer(S_L, sfc_L, G_L, frame_type)
X_L = aac_i_tns(Xq_L, frame_type, tns_L)
# -------------------------
# Right channel
# -------------------------
tns_R = np.asarray(fr["chr"]["tns_coeffs"], dtype=np.float64)
G_R = fr["chr"]["G"]
sfc_bits_R = fr["chr"]["sfc"]
mdct_bits_R = fr["chr"]["stream"]
cb_R = int(fr["chr"]["codebook"])
sfc_dec_R = aac_decode_huff(sfc_bits_R, 11, huff_LUT_list)[:sfc_len].astype(np.int64, copy=False)
if frame_type == "ESH":
sfc_dpcm_R = sfc_dec_R.reshape(NB - 1, 8, order="F")
sfc_R = np.zeros((NB, 8), dtype=np.int64)
Gv = np.asarray(G_R, dtype=np.float64).reshape(1, 8)
sfc_R[0, :] = Gv[0, :].astype(np.int64)
sfc_R[1:, :] = sfc_dpcm_R
else:
sfc_dpcm_R = sfc_dec_R.reshape(NB - 1, 1, order="F")
sfc_R = np.zeros((NB, 1), dtype=np.int64)
sfc_R[0, 0] = int(float(G_R))
sfc_R[1:, :] = sfc_dpcm_R
if cb_R == 0:
S_dec_R = np.zeros((1024,), dtype=np.int64)
else:
S_tmp_R = aac_decode_huff(mdct_bits_R, cb_R, huff_LUT_list).astype(np.int64, copy=False)
if S_tmp_R.size < 1024:
S_dec_R = np.zeros((1024,), dtype=np.int64)
S_dec_R[: S_tmp_R.size] = S_tmp_R
else:
S_dec_R = S_tmp_R[:1024]
S_R = S_dec_R.reshape(1024, 1)
Xq_R = aac_i_quantizer(S_R, sfc_R, G_R, frame_type)
X_R = aac_i_tns(Xq_R, frame_type, tns_R)
# Re-pack to stereo container and inverse filterbank
frame_f = aac_unpack_seq_channels(frame_type, np.asarray(X_L), np.asarray(X_R))
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type)
start = i * hop
y_pad[start : start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
sf.write(str(filename_out), y, 48000)
return y
+8 -75
View File
@@ -14,6 +14,7 @@
# ------------------------------------------------------------ # ------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from core.aac_utils import mdct, imdct
from core.aac_types import * from core.aac_types import *
from scipy.signal.windows import kaiser from scipy.signal.windows import kaiser
@@ -186,74 +187,6 @@ def _window_sequence(frame_type: FrameType, win_type: WinType) -> Window:
raise ValueError(f"Invalid frame_type for long window sequence: {frame_type!r}") raise ValueError(f"Invalid frame_type for long window sequence: {frame_type!r}")
def _mdct(s: TimeSignal) -> MdctCoeffs:
"""
MDCT (direct form) as specified in the assignment.
Parameters
----------
s : TimeSignal
Windowed time samples, 1-D array of length N (N = 2048 or 256).
Returns
-------
MdctCoeffs
MDCT coefficients, 1-D array of length N/2.
Definition
----------
X[k] = 2 * sum_{n=0..N-1} s[n] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
s = np.asarray(s, dtype=np.float64).reshape(-1)
N = int(s.shape[0])
if N not in (2048, 256):
raise ValueError("MDCT input length must be 2048 or 256.")
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(N // 2, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, N/2)
X = 2.0 * (s @ C) # (N/2,)
return X
def _imdct(X: MdctCoeffs) -> TimeSignal:
"""
IMDCT (direct form) as specified in the assignment.
Parameters
----------
X : MdctCoeffs
MDCT coefficients, 1-D array of length K (K = 1024 or 128).
Returns
-------
TimeSignal
Reconstructed time samples, 1-D array of length N = 2K.
Definition
----------
s[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
X = np.asarray(X, dtype=np.float64).reshape(-1)
K = int(X.shape[0])
if K not in (1024, 128):
raise ValueError("IMDCT input length must be 1024 or 128.")
N = 2 * K
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(K, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, K)
s = (2.0 / N) * (C @ X) # (N,)
return s
def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameChannelF: def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameChannelF:
""" """
ESH analysis for one channel. ESH analysis for one channel.
@@ -279,7 +212,7 @@ def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameCha
for j in range(8): for j in range(8):
start = 448 + 128 * j start = 448 + 128 * j
seg = x_ch[start:start + 256] * wS # (256,) seg = x_ch[start:start + 256] * wS # (256,)
X_esh[:, j] = _mdct(seg) # (128,) X_esh[:, j] = mdct(seg) # (128,)
return X_esh return X_esh
@@ -344,7 +277,7 @@ def _i_filter_bank_esh_channel(X_esh: FrameChannelF, win_type: WinType) -> Frame
# Each short IMDCT returns 256 samples. Place them at: # Each short IMDCT returns 256 samples. Place them at:
# start = 448 + 128*j, j=0..7 (50% overlap) # start = 448 + 128*j, j=0..7 (50% overlap)
for j in range(8): for j in range(8):
seg = _imdct(X_esh[:, j]) * wS # (256,) seg = imdct(X_esh[:, j]) * wS # (256,)
start = 448 + 128 * j start = 448 + 128 * j
out[start:start + 256] += seg out[start:start + 256] += seg
@@ -352,7 +285,7 @@ def _i_filter_bank_esh_channel(X_esh: FrameChannelF, win_type: WinType) -> Frame
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public Function prototypes (Level 1) # Public Function prototypes
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -> FrameF: def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -> FrameF:
@@ -385,8 +318,8 @@ def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -
if frame_type in ("OLS", "LSS", "LPS"): if frame_type in ("OLS", "LSS", "LPS"):
w = _window_sequence(frame_type, win_type) # length 2048 w = _window_sequence(frame_type, win_type) # length 2048
XL = _mdct(xL * w) # length 1024 XL = mdct(xL * w) # length 1024
XR = _mdct(xR * w) # length 1024 XR = mdct(xR * w) # length 1024
out = np.empty((1024, 2), dtype=np.float64) out = np.empty((1024, 2), dtype=np.float64)
out[:, 0] = XL out[:, 0] = XL
out[:, 1] = XR out[:, 1] = XR
@@ -430,8 +363,8 @@ def aac_i_filter_bank(frame_F: FrameF, frame_type: FrameType, win_type: WinType)
w = _window_sequence(frame_type, win_type) w = _window_sequence(frame_type, win_type)
xL = _imdct(frame_F[:, 0]) * w xL = imdct(frame_F[:, 0]) * w
xR = _imdct(frame_F[:, 1]) * w xR = imdct(frame_F[:, 1]) * w
out = np.empty((2048, 2), dtype=np.float64) out = np.empty((2048, 2), dtype=np.float64)
out[:, 0] = xL out[:, 0] = xL
+112
View File
@@ -0,0 +1,112 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Huffman wrappers (Level 3)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Thin wrappers around the provided Huffman utilities (material/huff_utils.py)
# so that the API matches the assignment text.
#
# Exposed API (assignment):
# huff_sec, huff_codebook = aac_encode_huff(coeff_sec, huff_LUT_list, force_codebook)
# dec_coeffs = aac_decode_huff(huff_sec, huff_codebook, huff_LUT_list)
#
# Notes:
# - Huffman coding operates on tuples. Therefore, decode(encode(x)) may return
# extra trailing symbols due to tuple padding. The AAC decoder knows the
# true section length from side information (band limits) and truncates.
# ------------------------------------------------------------
from __future__ import annotations
from typing import Any
import numpy as np
from material.huff_utils import encode_huff, decode_huff
def aac_encode_huff(
coeff_sec: np.ndarray,
huff_LUT_list: list[dict[str, Any]],
force_codebook: int | None = None,
) -> tuple[str, int]:
"""
Huffman-encode a section of coefficients (MDCT symbols or scalefactors).
Parameters
----------
coeff_sec : np.ndarray
Coefficient section to be encoded. Any shape is accepted; the input
is flattened and treated as a 1-D sequence of int64 symbols.
huff_LUT_list : list[dict[str, Any]]
List of Huffman Look-Up Tables (LUTs) as returned by material.load_LUT().
Index corresponds to codebook id (typically 1..11, with 0 reserved).
force_codebook : int | None
If provided, forces the use of this Huffman codebook. In the assignment,
scalefactors are encoded with codebook 11. For MDCT coefficients, this
argument is usually omitted (auto-selection).
Returns
-------
tuple[str, int]
(huff_sec, huff_codebook)
- huff_sec: bitstream as a string of '0'/'1'
- huff_codebook: codebook id used by the encoder
"""
coeff_sec_arr = np.asarray(coeff_sec, dtype=np.int64).reshape(-1)
if force_codebook is None:
# Provided utility returns (bitstream, codebook) in the auto-selection case.
huff_sec, huff_codebook = encode_huff(coeff_sec_arr, huff_LUT_list)
return str(huff_sec), int(huff_codebook)
# Provided utility returns ONLY the bitstream when force_codebook is set.
cb = int(force_codebook)
huff_sec = encode_huff(coeff_sec_arr, huff_LUT_list, force_codebook=cb)
return str(huff_sec), cb
def aac_decode_huff(
huff_sec: str | np.ndarray,
huff_codebook: int,
huff_LUT: list[dict[str, Any]],
) -> np.ndarray:
"""
Huffman-decode a bitstream using the specified codebook.
Parameters
----------
huff_sec : str | np.ndarray
Huffman bitstream. Typically a string of '0'/'1'. If an array is provided,
it is passed through to the provided decoder.
huff_codebook : int
Codebook id that was returned by aac_encode_huff.
Codebook 0 represents an all-zero section.
huff_LUT : list[dict[str, Any]]
Huffman LUT list as returned by material.load_LUT().
Returns
-------
np.ndarray
Decoded coefficients as a 1-D np.int64 array.
Note: Due to tuple coding, the decoded array may contain extra trailing
padding symbols. The caller must truncate to the known section length.
"""
cb = int(huff_codebook)
if cb == 0:
# Codebook 0 represents an all-zero section. The decoded length is not
# recoverable from the bitstream alone; the caller must expand/truncate.
return np.zeros((0,), dtype=np.int64)
if cb < 0 or cb >= len(huff_LUT):
raise ValueError(f"Invalid Huffman codebook index: {cb}")
lut = huff_LUT[cb]
dec = decode_huff(huff_sec, lut)
return np.asarray(dec, dtype=np.int64).reshape(-1)
+441
View File
@@ -0,0 +1,441 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Psychoacoustic Model
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Psychoacoustic model for ONE channel, based on the assignment notes (Section 2.4).
#
# Public API:
# SMR = aac_psycho(frame_T, frame_type, frame_T_prev_1, frame_T_prev_2)
#
# Output:
# - For long frames ("OLS", "LSS", "LPS"): SMR has shape (69,)
# - For short frames ("ESH"): SMR has shape (42, 8) (one column per subframe)
#
# Notes:
# - Uses Bark band tables from material/TableB219.mat:
# * B219a for long windows (69 bands, N=2048 FFT, N/2=1024 bins)
# * B219b for short windows (42 bands, N=256 FFT, N/2=128 bins)
# - Applies a Hann window in time domain before FFT magnitude/phase extraction.
# - Implements:
# spreading function -> band spreading -> tonality index -> masking thresholds -> SMR.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from core.aac_utils import band_limits, get_table
from core.aac_configuration import NMT_DB, TMN_DB
from core.aac_types import *
# -----------------------------------------------------------------------------
# Spreading function
# -----------------------------------------------------------------------------
def _spreading_matrix(bval: BandValueArray) -> FloatArray:
"""
Compute the spreading function matrix between psychoacoustic bands.
The spreading function describes how energy in one critical band masks
nearby bands. The formula follows the assignment pseudo-code.
Parameters
----------
bval : BandValueArray
Bark value per band, shape (B,).
Returns
-------
FloatArray
Spreading matrix S of shape (B, B), where:
S[bb, b] quantifies the contribution of band bb masking band b.
"""
bval = np.asarray(bval, dtype=np.float64).reshape(-1)
B = int(bval.shape[0])
spread = np.zeros((B, B), dtype=np.float64)
for b in range(B):
for bb in range(B):
# tmpx depends on direction (asymmetric spreading)
if bb >= b:
tmpx = 3.0 * (bval[bb] - bval[b])
else:
tmpx = 1.5 * (bval[bb] - bval[b])
# tmpz uses the "min(..., 0)" nonlinearity exactly as in the notes
tmpz = 8.0 * min((tmpx - 0.5) ** 2 - 2.0 * (tmpx - 0.5), 0.0)
tmpy = 15.811389 + 7.5 * (tmpx + 0.474) - 17.5 * np.sqrt(1.0 + (tmpx + 0.474) ** 2)
# Clamp very small values (below -100 dB) to 0 contribution
if tmpy < -100.0:
spread[bb, b] = 0.0
else:
spread[bb, b] = 10.0 ** ((tmpz + tmpy) / 10.0)
return spread
# -----------------------------------------------------------------------------
# Windowing + FFT feature extraction
# -----------------------------------------------------------------------------
def _hann_window(N: int) -> FloatArray:
"""
Hann window as specified in the notes:
w[n] = 0.5 - 0.5*cos(2*pi*(n + 0.5)/N)
Parameters
----------
N : int
Window length.
Returns
-------
FloatArray
1-D array of shape (N,), dtype float64.
"""
n = np.arange(N, dtype=np.float64)
return 0.5 - 0.5 * np.cos((2.0 * np.pi / N) * (n + 0.5))
def _r_phi_from_time(x: FrameChannelT, N: int) -> tuple[FloatArray, FloatArray]:
"""
Compute FFT magnitude r(w) and phase phi(w) for bins w = 0 .. N/2-1.
Processing:
1) Apply Hann window in time domain.
2) Compute N-point FFT.
3) Keep only the positive-frequency bins [0 .. N/2-1].
Parameters
----------
x : FrameChannelT
Time-domain samples, shape (N,).
N : int
FFT size (2048 or 256).
Returns
-------
r : FloatArray
Magnitude spectrum for bins 0 .. N/2-1, shape (N/2,).
phi : FloatArray
Phase spectrum for bins 0 .. N/2-1, shape (N/2,).
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
if x.shape[0] != N:
raise ValueError(f"Expected time vector of length {N}, got {x.shape[0]}.")
w = _hann_window(N)
X = np.fft.fft(x * w, n=N)
Xp = X[: N // 2]
r = np.abs(Xp).astype(np.float64, copy=False)
phi = np.angle(Xp).astype(np.float64, copy=False)
return r, phi
def _predictability(
r: FloatArray,
phi: FloatArray,
r_m1: FloatArray,
phi_m1: FloatArray,
r_m2: FloatArray,
phi_m2: FloatArray,
) -> FloatArray:
"""
Compute predictability c(w) per spectral bin.
The notes define:
r_pred(w) = 2*r_{-1}(w) - r_{-2}(w)
phi_pred(w) = 2*phi_{-1}(w) - phi_{-2}(w)
c(w) = |X(w) - X_pred(w)| / (r(w) + |r_pred(w)|)
where X(w) is represented in polar form using r(w), phi(w).
Parameters
----------
r, phi : FloatArray
Current magnitude and phase, shape (N/2,).
r_m1, phi_m1 : FloatArray
Previous magnitude and phase, shape (N/2,).
r_m2, phi_m2 : FloatArray
Pre-previous magnitude and phase, shape (N/2,).
Returns
-------
FloatArray
Predictability c(w), shape (N/2,).
"""
r_pred = 2.0 * r_m1 - r_m2
phi_pred = 2.0 * phi_m1 - phi_m2
num = np.sqrt(
(r * np.cos(phi) - r_pred * np.cos(phi_pred)) ** 2
+ (r * np.sin(phi) - r_pred * np.sin(phi_pred)) ** 2
)
den = r + np.abs(r_pred) + 1e-12 # avoid division-by-zero without altering behavior
return (num / den).astype(np.float64, copy=False)
# -----------------------------------------------------------------------------
# Band-domain aggregation
# -----------------------------------------------------------------------------
def _band_energy_and_pred(
r: FloatArray,
c: FloatArray,
wlow: BandIndexArray,
whigh: BandIndexArray,
) -> tuple[FloatArray, FloatArray]:
"""
Aggregate spectral bin quantities into psychoacoustic bands.
Definitions (notes):
e(b) = sum_{w=wlow(b)..whigh(b)} r(w)^2
c_num(b) = sum_{w=wlow(b)..whigh(b)} c(w) * r(w)^2
The band predictability c(b) is later computed after spreading as:
cb(b) = ct(b) / ecb(b)
Parameters
----------
r : FloatArray
Magnitude spectrum, shape (N/2,).
c : FloatArray
Predictability per bin, shape (N/2,).
wlow, whigh : BandIndexArray
Band limits (inclusive indices), shape (B,).
Returns
-------
e_b : FloatArray
Band energies e(b), shape (B,).
c_num_b : FloatArray
Weighted predictability numerators c_num(b), shape (B,).
"""
r2 = (r * r).astype(np.float64, copy=False)
B = int(wlow.shape[0])
e_b = np.zeros(B, dtype=np.float64)
c_num_b = np.zeros(B, dtype=np.float64)
for b in range(B):
a = int(wlow[b])
z = int(whigh[b])
seg_r2 = r2[a : z + 1]
e_b[b] = float(np.sum(seg_r2))
c_num_b[b] = float(np.sum(c[a : z + 1] * seg_r2))
return e_b, c_num_b
def _psycho_window(
time_x: FrameChannelT,
prev1_x: FrameChannelT,
prev2_x: FrameChannelT,
*,
N: int,
table: BarkTable,
) -> FloatArray:
"""
Compute SMR for one FFT analysis window (N=2048 for long, N=256 for short).
This implements the pipeline described in the notes:
- FFT magnitude/phase
- predictability per bin
- band energies and predictability
- band spreading
- tonality index tb(b)
- masking threshold (noise + threshold in quiet)
- SMR(b) = e(b) / np(b)
Parameters
----------
time_x : FrameChannelT
Current time-domain samples, shape (N,).
prev1_x : FrameChannelT
Previous time-domain samples, shape (N,).
prev2_x : FrameChannelT
Pre-previous time-domain samples, shape (N,).
N : int
FFT size.
table : BarkTable
Psychoacoustic band table (B219a or B219b).
Returns
-------
FloatArray
SMR per band, shape (B,).
"""
wlow, whigh, bval, qthr_db = band_limits(table)
spread = _spreading_matrix(bval)
# FFT features for current and history windows
r, phi = _r_phi_from_time(time_x, N)
r_m1, phi_m1 = _r_phi_from_time(prev1_x, N)
r_m2, phi_m2 = _r_phi_from_time(prev2_x, N)
# Predictability per bin
c_w = _predictability(r, phi, r_m1, phi_m1, r_m2, phi_m2)
# Aggregate into psycho bands
e_b, c_num_b = _band_energy_and_pred(r, c_w, wlow, whigh)
# Spread energies and predictability across bands:
# ecb(b) = sum_bb e(bb) * S(bb, b)
# ct(b) = sum_bb c_num(bb) * S(bb, b)
ecb = spread.T @ e_b
ct = spread.T @ c_num_b
# Band predictability after spreading: cb(b) = ct(b) / ecb(b)
cb = ct / (ecb + 1e-12)
# Normalized energy term:
# en(b) = ecb(b) / sum_bb S(bb, b)
spread_colsum = np.sum(spread, axis=0)
en = ecb / (spread_colsum + 1e-12)
# Tonality index (clamped to [0, 1])
tb = -0.299 - 0.43 * np.log(np.maximum(cb, 1e-12))
tb = np.clip(tb, 0.0, 1.0)
# Required SNR per band (dB): interpolate between TMN and NMT
snr_b = tb * TMN_DB + (1.0 - tb) * NMT_DB
bc = 10.0 ** (-snr_b / 10.0)
# Noise masking threshold estimate (power domain)
nb = en * bc
# Threshold in quiet (convert from dB to power domain):
# qthr_power = eps * (N/2) * 10^(qthr_db/10)
qthr_power = np.finfo('float').eps * (N / 2.0) * (10.0 ** (qthr_db / 10.0))
# Final masking threshold per band:
# np(b) = max(nb(b), qthr(b))
npart = np.maximum(nb, qthr_power)
# Signal-to-mask ratio:
# SMR(b) = e(b) / np(b)
smr = e_b / (npart + 1e-12)
return smr.astype(np.float64, copy=False)
# -----------------------------------------------------------------------------
# ESH window slicing (match filterbank conventions)
# -----------------------------------------------------------------------------
def _esh_subframes(x_2048: FrameChannelT) -> list[FrameChannelT]:
"""
Extract the 8 overlapping 256-sample short windows used by AAC ESH.
The project convention (matching the filterbank) is:
start_j = 448 + 128*j, for j = 0..7
subframe_j = x[start_j : start_j + 256]
This selects the central 1152-sample region [448, 1600) and produces
8 windows with 50% overlap.
Parameters
----------
x_2048 : FrameChannelT
Time-domain channel frame, shape (2048,).
Returns
-------
list[FrameChannelT]
List of 8 subframes, each of shape (256,).
"""
x_2048 = np.asarray(x_2048, dtype=np.float64).reshape(-1)
if x_2048.shape[0] != 2048:
raise ValueError("ESH requires 2048-sample input frames.")
subs: list[FrameChannelT] = []
for j in range(8):
start = 448 + 128 * j
subs.append(x_2048[start : start + 256])
return subs
# -----------------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------------
def aac_psycho(
frame_T: FrameChannelT,
frame_type: FrameType,
frame_T_prev_1: FrameChannelT,
frame_T_prev_2: FrameChannelT,
) -> FloatArray:
"""
Psychoacoustic model for ONE channel.
Parameters
----------
frame_T : FrameChannelT
Current time-domain channel frame, shape (2048,).
For "ESH", the 8 short windows are derived internally.
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
frame_T_prev_1 : FrameChannelT
Previous time-domain channel frame, shape (2048,).
frame_T_prev_2 : FrameChannelT
Pre-previous time-domain channel frame, shape (2048,).
Returns
-------
FloatArray
Signal-to-Mask Ratio (SMR), per psychoacoustic band.
- If frame_type == "ESH": shape (42, 8)
- Else: shape (69,)
"""
frame_T = np.asarray(frame_T, dtype=np.float64).reshape(-1)
frame_T_prev_1 = np.asarray(frame_T_prev_1, dtype=np.float64).reshape(-1)
frame_T_prev_2 = np.asarray(frame_T_prev_2, dtype=np.float64).reshape(-1)
if frame_T.shape[0] != 2048 or frame_T_prev_1.shape[0] != 2048 or frame_T_prev_2.shape[0] != 2048:
raise ValueError("aac_psycho expects 2048-sample frames for current/prev1/prev2.")
table, N = get_table(frame_type)
# Long frame types: compute one SMR vector (69 bands)
if frame_type != "ESH":
return _psycho_window(frame_T, frame_T_prev_1, frame_T_prev_2, N=N, table=table)
# ESH: compute 8 SMR vectors (42 bands each), one per short subframe.
#
# The notes use short-window history for predictability:
# - For j=0: use previous frame's subframes (7, 6)
# - For j=1: use current subframe 0 and previous frame's subframe 7
# - For j>=2: use current subframes (j-1, j-2)
#
# This matches the "within-frame history" convention commonly used in
# simplified psycho models for ESH.
cur_subs = _esh_subframes(frame_T)
prev1_subs = _esh_subframes(frame_T_prev_1)
B = int(table.shape[0]) # expected 42
smr_out = np.zeros((B, 8), dtype=np.float64)
for j in range(8):
if j == 0:
x_m1 = prev1_subs[7]
x_m2 = prev1_subs[6]
elif j == 1:
x_m1 = cur_subs[0]
x_m2 = prev1_subs[7]
else:
x_m1 = cur_subs[j - 1]
x_m2 = cur_subs[j - 2]
smr_out[:, j] = _psycho_window(cur_subs[j], x_m1, x_m2, N=256, table=table)
return smr_out
+600
View File
@@ -0,0 +1,600 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Quantizer / iQuantizer (Level 3)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Implements AAC quantizer and inverse quantizer for one channel.
# Based on assignment section 2.6 (Eq. 12-15).
#
# Notes:
# - Bit reservoir is not implemented (assignment simplification).
# - Scalefactor bands are assumed equal to psychoacoustic bands
# (Table B.2.1.9a / B.2.1.9b from TableB219.mat).
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from core.aac_utils import get_table, band_limits
from core.aac_types import *
# -----------------------------------------------------------------------------
# Constants (assignment)
# -----------------------------------------------------------------------------
MAGIC_NUMBER: float = 0.4054
EPS: float = 1e-12
MAX_SF_DELTA:int = 60
# -----------------------------------------------------------------------------
# Helpers: ESH packing/unpacking (128x8 <-> 1024x1)
# -----------------------------------------------------------------------------
def _esh_pack(x_128x8: FloatArray) -> FloatArray:
"""
Pack ESH coefficients (128 x 8) into a single long vector (1024 x 1).
Packing order:
Columns are concatenated in subframe order (0..7), column-major.
Parameters
----------
x_128x8 : FloatArray
ESH coefficients, shape (128, 8).
Returns
-------
FloatArray
Packed coefficients, shape (1024, 1).
"""
x_128x8 = np.asarray(x_128x8, dtype=np.float64)
if x_128x8.shape != (128, 8):
raise ValueError("ESH pack expects shape (128, 8).")
return x_128x8.reshape(1024, 1, order="F")
def _esh_unpack(x_1024x1: FloatArray) -> FloatArray:
"""
Unpack a packed ESH vector (1024 elements) back to shape (128, 8).
Parameters
----------
x_1024x1 : FloatArray
Packed ESH vector, shape (1024,) or (1024, 1) after flattening.
Returns
-------
FloatArray
Unpacked ESH coefficients, shape (128, 8).
"""
x_1024x1 = np.asarray(x_1024x1, dtype=np.float64).reshape(-1)
if x_1024x1.shape[0] != 1024:
raise ValueError("ESH unpack expects 1024 elements.")
return x_1024x1.reshape(128, 8, order="F")
# -----------------------------------------------------------------------------
# Core quantizer formulas (Eq. 12, Eq. 13)
# -----------------------------------------------------------------------------
def _quantize_symbol(x: FloatArray, alpha: float) -> QuantizedSymbols:
"""
Quantize MDCT coefficients to integer symbols S(k).
Implements Eq. (12):
S(k) = sgn(X(k)) * int( (|X(k)| * 2^(-alpha/4))^(3/4) + MAGIC_NUMBER )
Parameters
----------
x : FloatArray
MDCT coefficients for a contiguous set of spectral lines.
Shape: (N,)
alpha : float
Scalefactor gain for the corresponding scalefactor band.
Returns
-------
QuantizedSymbols
Quantized symbols S(k) as int64, shape (N,).
"""
x = np.asarray(x, dtype=np.float64)
scale = 2.0 ** (-0.25 * float(alpha))
ax = np.abs(x) * scale
y = np.power(ax, 0.75, dtype=np.float64)
# "int" in the assignment corresponds to truncation.
q = np.floor(y + MAGIC_NUMBER).astype(np.int64)
return (np.sign(x).astype(np.int64) * q).astype(np.int64)
def _dequantize_symbol(S: QuantizedSymbols, alpha: float) -> FloatArray:
"""
Inverse quantizer (dequantization of symbols).
Implements Eq. (13):
Xhat(k) = sgn(S(k)) * |S(k)|^(4/3) * 2^(alpha/4)
Parameters
----------
S : QuantizedSymbols
Quantized symbols S(k), int64, shape (N,).
alpha : float
Scalefactor gain for the corresponding scalefactor band.
Returns
-------
FloatArray
Reconstructed MDCT coefficients Xhat(k), float64, shape (N,).
"""
S = np.asarray(S, dtype=np.int64)
scale = 2.0 ** (0.25 * float(alpha))
aS = np.abs(S).astype(np.float64)
y = np.power(aS, 4.0 / 3.0, dtype=np.float64)
return (np.sign(S).astype(np.float64) * y * scale).astype(np.float64)
# -----------------------------------------------------------------------------
# Alpha initialization (Eq. 14)
# -----------------------------------------------------------------------------
def _initial_alpha_hat(X: "FloatArray", MQ: int = 8191) -> int:
"""
Compute the initial scalefactor estimate alpha_hat for a frame.
The assignment proposes the following first approximation (Equation 14):
alpha_hat = (16/3) * log2( max_k(|X(k)|)^(3/4) / MQ )
where max_k runs over all MDCT coefficients of the frame (not per band),
and MQ is the maximum quantization level parameter (2*MQ + 1 levels).
Parameters
----------
X : FloatArray
MDCT coefficients of one frame (or one ESH subframe), shape (N,).
MQ : int
Quantizer parameter (default 8191, as per assignment).
Returns
-------
int
Integer alpha_hat (rounded to nearest integer).
"""
x_max = float(np.max(np.abs(X)))
if x_max <= 0.0:
return 0
alpha_hat = (16.0 / 3.0) * np.log2((x_max ** (3.0 / 4.0)) / float(MQ))
return int(np.round(alpha_hat))
# -----------------------------------------------------------------------------
# Band utilities
# -----------------------------------------------------------------------------
def _band_slices(frame_type: FrameType) -> list[tuple[int, int]]:
"""
Return scalefactor band ranges [wlow, whigh] (inclusive) for the given frame type.
These are derived from the psychoacoustic tables (TableB219),
and map directly to MDCT indices:
- long: 0..1023
- short (ESH subframe): 0..127
Parameters
----------
frame_type : FrameType
Frame type ("OLS", "LSS", "ESH", "LPS").
Returns
-------
list[tuple[int, int]]
List of (lo, hi) inclusive index pairs for each band.
"""
table, _Nfft = get_table(frame_type)
wlow, whigh, _bval, _qthr_db = band_limits(table)
bands: list[tuple[int, int]] = []
for lo, hi in zip(wlow, whigh):
bands.append((int(lo), int(hi)))
return bands
def _band_energy(x: FloatArray, lo: int, hi: int) -> float:
"""
Compute energy of a spectral segment x[lo:hi+1].
Parameters
----------
x : FloatArray
MDCT coefficient vector.
lo, hi : int
Inclusive index range.
Returns
-------
float
Sum of squares (energy) within the band.
"""
sec = x[lo : hi + 1]
return float(np.sum(sec * sec))
def _psychoacoustic_threshold(
X: FloatArray,
SMR_col: FloatArray,
bands: list[tuple[int, int]],
) -> FloatArray:
"""
Compute psychoacoustic thresholds T(b) per band.
Uses:
P(b) = sum_{k in band} X(k)^2
T(b) = P(b) / SMR(b)
Parameters
----------
X : FloatArray
MDCT coefficients for a frame (long) or one ESH subframe (short).
SMR_col : FloatArray
SMR values for this frame/subframe, shape (NB,).
bands : list[tuple[int, int]]
Band index ranges.
Returns
-------
FloatArray
Threshold vector T(b), shape (NB,).
"""
nb = len(bands)
T = np.zeros((nb,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
P = _band_energy(X, lo, hi)
smr = float(SMR_col[b])
if smr <= EPS:
T[b] = 0.0
else:
T[b] = P / smr
return T
# -----------------------------------------------------------------------------
# Alpha selection per band + neighbor-difference constraint
# -----------------------------------------------------------------------------
def _best_alpha_for_band(
X: "FloatArray", lo: int, hi: int, T_b: float,
alpha_hat: int, alpha_prev: int, alpha_min: int, alpha_max: int,
) -> int:
"""
Determine the band-wise scalefactor alpha(b) following the assignment.
Procedure:
- Start from a frame-wise initial estimate alpha_hat.
- Iteratively increase alpha(b) by 1 as long as the quantization error power
stays below the psychoacoustic threshold T(b): P_e(b) = sum_{k in band} ( X(k) - Xhat(k) )^2
- Stop increasing alpha(b) if the neighbor constraint would be violated: |alpha(b) - alpha(b-1)| <= 60
When processing bands sequentially (low -> high), this becomes: alpha(b) <= alpha_prev + 60
Notes:
- This function does not decrease alpha if the initial value already violates
the threshold; the assignment only specifies iterative increase.
Parameters
----------
X : FloatArray
Full MDCT vector of the current (sub)frame, shape (N,).
lo, hi : int
Band index bounds (inclusive), defining the band slice.
T_b : float
Threshold T(b) for this band.
alpha_hat : int
Initial frame-wise estimate (Equation 14).
alpha_prev : int
Previously selected alpha for band b-1 (neighbor constraint reference).
alpha_min, alpha_max : int
Safeguard bounds for alpha.
Returns
-------
int
Selected integer alpha(b).
"""
if T_b <= 0.0:
return int(alpha_hat)
Xsec = X[lo : hi + 1]
# Neighbor constraint (sequential processing): alpha(b) <= alpha_prev + 60
alpha_limit = min(int(alpha_max), int(alpha_prev) + MAX_SF_DELTA)
# Start from alpha_hat, clamped to feasible range
alpha = int(alpha_hat)
alpha = max(int(alpha_min), min(alpha, int(alpha_limit)))
# Evaluate at current alpha
Ssec = _quantize_symbol(Xsec, alpha)
Xhat = _dequantize_symbol(Ssec, alpha)
Pe = float(np.sum((Xsec - Xhat) ** 2))
# If already above threshold, return current alpha (no decrease step specified)
if Pe > T_b:
return alpha
# Increase alpha while still under threshold and within constraints
while True:
alpha_next = alpha + 1
if alpha_next > alpha_limit:
break
Ssec = _quantize_symbol(Xsec, alpha_next)
Xhat = _dequantize_symbol(Ssec, alpha_next)
Pe_next = float(np.sum((Xsec - Xhat) ** 2))
if Pe_next > T_b:
break
alpha = alpha_next
return alpha
# -----------------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------------
def aac_quantizer(
frame_F: FrameChannelF,
frame_type: FrameType,
SMR: FloatArray,
) -> tuple[QuantizedSymbols, ScaleFactors, GlobalGain]:
"""
AAC quantizer for one channel (Level 3).
Quantizes MDCT coefficients (after TNS) using band-wise scalefactors derived
from psychoacoustic thresholds computed via SMR.
The implementation follows the assignment procedure:
- Compute an initial frame-wise alpha_hat using Equation (14), based on the
maximum MDCT coefficient magnitude of the (sub)frame.
- For each band b, increase alpha(b) by 1 while the quantization error power
P_e(b) stays below the threshold T(b).
- Enforce the neighbor constraint |alpha(b) - alpha(b-1)| <= 60 during the
band-by-band search (no post-processing needed).
Parameters
----------
frame_F : FrameChannelF
MDCT coefficients after TNS, one channel.
Shapes:
- Long frames: (1024,) or (1024, 1)
- ESH: (128, 8)
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
SMR : FloatArray
Signal-to-Mask Ratio per band.
Shapes:
- Long: (NB,) or (NB, 1)
- ESH: (NB, 8)
Returns
-------
S : QuantizedSymbols
Quantized symbols S(k), packed as shape (1024, 1) for all frame types.
For ESH, the 8 subframes are packed in column-major subframe layout.
sfc : ScaleFactors
DPCM-coded scalefactors:
sfc(0) = alpha(0) = G
sfc(b) = alpha(b) - alpha(b-1), for b > 0
Shapes:
- Long: (NB, 1)
- ESH: (NB, 8)
G : GlobalGain
Global gain G = alpha(0).
- Long: scalar float
- ESH: array shape (1, 8), dtype float64
"""
bands = _band_slices(frame_type)
NB = len(bands)
X = np.asarray(frame_F, dtype=np.float64)
SMR = np.asarray(SMR, dtype=np.float64)
# -------------------------------------------------------------------------
# ESH: 8 short subframes, each of length 128
# -------------------------------------------------------------------------
if frame_type == "ESH":
if X.shape != (128, 8):
raise ValueError("For ESH, frame_F must have shape (128, 8).")
if SMR.shape != (NB, 8):
raise ValueError(f"For ESH, SMR must have shape ({NB}, 8).")
S_out: QuantizedSymbols = np.zeros((1024, 1), dtype=np.int64)
sfc: ScaleFactors = np.zeros((NB, 8), dtype=np.int64)
G_arr = np.zeros((1, 8), dtype=np.float64)
# Packed output view: (128, 8) with column-major layout
S_pack = S_out[:, 0].reshape(128, 8, order="F")
for j in range(8):
Xj = X[:, j].reshape(128)
SMRj = SMR[:, j].reshape(NB)
# Compute psychoacoustic threshold T(b) for this subframe
T = _psychoacoustic_threshold(Xj, SMRj, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xj)
# Band-wise scalefactors alpha(b)
alpha = np.zeros((NB,), dtype=np.int64)
alpha_prev = int(alpha_hat)
for b, (lo, hi) in enumerate(bands):
alpha_b = _best_alpha_for_band(
X=Xj,
lo=lo,
hi=hi,
T_b=float(T[b]),
alpha_hat=int(alpha_hat),
alpha_prev=int(alpha_prev),
alpha_min=-4096,
alpha_max=4096,
)
alpha[b] = int(alpha_b)
alpha_prev = int(alpha_b)
# DPCM-coded scalefactors
G_arr[0, j] = float(alpha[0])
sfc[0, j] = int(alpha[0])
for b in range(1, NB):
sfc[b, j] = int(alpha[b] - alpha[b - 1])
# Quantize MDCT coefficients band-by-band
Sj = np.zeros((128,), dtype=np.int64)
for b, (lo, hi) in enumerate(bands):
Sj[lo : hi + 1] = _quantize_symbol(Xj[lo : hi + 1], float(alpha[b]))
# Store subframe in packed output
S_pack[:, j] = Sj
return S_out, sfc, G_arr
# -------------------------------------------------------------------------
# Long frames: OLS / LSS / LPS, length 1024
# -------------------------------------------------------------------------
if X.shape == (1024,):
Xv = X
elif X.shape == (1024, 1):
Xv = X[:, 0]
else:
raise ValueError("For non-ESH, frame_F must have shape (1024,) or (1024, 1).")
if SMR.shape == (NB,):
SMRv = SMR
elif SMR.shape == (NB, 1):
SMRv = SMR[:, 0]
else:
raise ValueError(f"For non-ESH, SMR must have shape ({NB},) or ({NB}, 1).")
# Compute psychoacoustic threshold T(b) for the long frame
T = _psychoacoustic_threshold(Xv, SMRv, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xv)
# Band-wise scalefactors alpha(b)
alpha = np.zeros((NB,), dtype=np.int64)
alpha_prev = int(alpha_hat)
for b, (lo, hi) in enumerate(bands):
alpha_b = _best_alpha_for_band(
X=Xv,
lo=lo,
hi=hi,
T_b=float(T[b]),
alpha_hat=int(alpha_hat),
alpha_prev=int(alpha_prev),
alpha_min=-4096,
alpha_max=4096,
)
alpha[b] = int(alpha_b)
alpha_prev = int(alpha_b)
# DPCM-coded scalefactors
sfc_out: ScaleFactors = np.zeros((NB, 1), dtype=np.int64)
sfc_out[0, 0] = int(alpha[0])
for b in range(1, NB):
sfc_out[b, 0] = int(alpha[b] - alpha[b - 1])
G: float = float(alpha[0])
# Quantize MDCT coefficients band-by-band
S_vec = np.zeros((1024,), dtype=np.int64)
for b, (lo, hi) in enumerate(bands):
S_vec[lo : hi + 1] = _quantize_symbol(Xv[lo : hi + 1], float(alpha[b]))
return S_vec.reshape(1024, 1), sfc_out, G
def aac_i_quantizer(
S: QuantizedSymbols,
sfc: ScaleFactors,
G: GlobalGain,
frame_type: FrameType,
) -> FrameChannelF:
"""
Inverse quantizer (iQuantizer) for one channel.
Reconstructs MDCT coefficients from quantized symbols and DPCM scalefactors.
Parameters
----------
S : QuantizedSymbols
Quantized symbols, shape (1024, 1) (or any array with 1024 elements).
sfc : ScaleFactors
DPCM-coded scalefactors.
Shapes:
- Long: (NB, 1)
- ESH: (NB, 8)
G : GlobalGain
Global gain (not strictly required if sfc includes sfc(0)=alpha(0)).
Present for API compatibility with the assignment.
frame_type : FrameType
AAC frame type.
Returns
-------
FrameChannelF
Reconstructed MDCT coefficients:
- ESH: (128, 8)
- Long: (1024, 1)
"""
bands = _band_slices(frame_type)
NB = len(bands)
S_flat = np.asarray(S, dtype=np.int64).reshape(-1)
if S_flat.shape[0] != 1024:
raise ValueError("S must contain 1024 symbols.")
if frame_type == "ESH":
sfc = np.asarray(sfc, dtype=np.int64)
if sfc.shape != (NB, 8):
raise ValueError(f"For ESH, sfc must have shape ({NB}, 8).")
S_128x8 = _esh_unpack(S_flat)
Xrec = np.zeros((128, 8), dtype=np.float64)
for j in range(8):
alpha = np.zeros((NB,), dtype=np.int64)
alpha[0] = int(sfc[0, j])
for b in range(1, NB):
alpha[b] = int(alpha[b - 1] + sfc[b, j])
Xj = np.zeros((128,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
Xj[lo : hi + 1] = _dequantize_symbol(S_128x8[lo : hi + 1, j].astype(np.int64), float(alpha[b]))
Xrec[:, j] = Xj
return Xrec
sfc = np.asarray(sfc, dtype=np.int64)
if sfc.shape != (NB, 1):
raise ValueError(f"For non-ESH, sfc must have shape ({NB}, 1).")
alpha = np.zeros((NB,), dtype=np.int64)
alpha[0] = int(sfc[0, 0])
for b in range(1, NB):
alpha[b] = int(alpha[b - 1] + sfc[b, 0])
Xrec = np.zeros((1024,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
Xrec[lo : hi + 1] = _dequantize_symbol(S_flat[lo : hi + 1], float(alpha[b]))
return Xrec.reshape(1024, 1)
-60
View File
@@ -1,60 +0,0 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - SNR dB calculator
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# This module implements SNR calculation in dB
# ------------------------------------------------------------
from __future__ import annotations
from core.aac_types import StereoSignal
import numpy as np
def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Compute overall SNR (dB) over all samples and channels after aligning lengths.
Parameters
----------
x_ref : StereoSignal
Reference stereo stream.
x_hat : StereoSignal
Reconstructed stereo stream.
Returns
-------
float
SNR in dB.
- Returns +inf if noise power is zero.
- Returns -inf if signal power is zero.
"""
x_ref = np.asarray(x_ref, dtype=np.float64)
x_hat = np.asarray(x_hat, dtype=np.float64)
if x_ref.ndim == 1:
x_ref = x_ref.reshape(-1, 1)
if x_hat.ndim == 1:
x_hat = x_hat.reshape(-1, 1)
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
x_ref = x_ref[:n, :c]
x_hat = x_hat[:n, :c]
err = x_ref - x_hat
ps = float(np.sum(x_ref * x_ref))
pn = float(np.sum(err * err))
if pn <= 0.0:
return float("inf")
if ps <= 0.0:
return float("-inf")
return float(10.0 * np.log10(ps / pn))
+2 -2
View File
@@ -173,10 +173,10 @@ def _stereo_merge(ft_l: FrameType, ft_r: FrameType) -> FrameType:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public Function prototypes (Level 1) # Public Function prototypes
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_SSC(frame_T: FrameT, next_frame_T: FrameT, prev_frame_type: FrameType) -> FrameType: def aac_ssc(frame_T: FrameT, next_frame_T: FrameT, prev_frame_type: FrameType) -> FrameType:
""" """
Sequence Segmentation Control (SSC). Sequence Segmentation Control (SSC).
+12 -47
View File
@@ -30,9 +30,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Tuple from typing import Tuple
import numpy as np from core.aac_utils import load_b219_tables
from scipy.io import loadmat
from core.aac_configuration import PRED_ORDER, QUANT_STEP, QUANT_MAX from core.aac_configuration import PRED_ORDER, QUANT_STEP, QUANT_MAX
from core.aac_types import * from core.aac_types import *
@@ -40,43 +38,8 @@ from core.aac_types import *
# Private helpers # Private helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
_B219_CACHE: dict[str, FloatArray] | None = None
def _band_ranges(k_count: int) -> BandRanges:
def _load_b219_tables() -> dict[str, FloatArray]:
"""
Load TableB219.mat and cache the contents.
The project layout guarantees that a 'material' directory is discoverable
from the current working directory (tests and level_123 entrypoints).
Returns
-------
dict[str, FloatArray]
Keys:
- "B219a": long bands table (for K=1024 MDCT lines)
- "B219b": short bands table (for K=128 MDCT lines)
"""
global _B219_CACHE
if _B219_CACHE is not None:
return _B219_CACHE
mat_path = Path("material") / "TableB219.mat"
if not mat_path.exists():
raise FileNotFoundError("Could not locate material/TableB219.mat in the current working directory.")
d = loadmat(str(mat_path))
if "B219a" not in d or "B219b" not in d:
raise ValueError("TableB219.mat missing required variables B219a and/or B219b.")
_B219_CACHE = {
"B219a": np.asarray(d["B219a"], dtype=np.float64),
"B219b": np.asarray(d["B219b"], dtype=np.float64),
}
return _B219_CACHE
def _band_ranges_for_kcount(k_count: int) -> BandRanges:
""" """
Return Bark band index ranges [start, end] (inclusive) for the given MDCT line count. Return Bark band index ranges [start, end] (inclusive) for the given MDCT line count.
@@ -92,7 +55,7 @@ def _band_ranges_for_kcount(k_count: int) -> BandRanges:
BandRanges (list[tuple[int, int]]) BandRanges (list[tuple[int, int]])
Each tuple is (start_k, end_k) inclusive. Each tuple is (start_k, end_k) inclusive.
""" """
tables = _load_b219_tables() tables = load_b219_tables()
if k_count == 1024: if k_count == 1024:
tbl = tables["B219a"] tbl = tables["B219a"]
elif k_count == 128: elif k_count == 128:
@@ -103,7 +66,7 @@ def _band_ranges_for_kcount(k_count: int) -> BandRanges:
start = tbl[:, 1].astype(int) start = tbl[:, 1].astype(int)
end = tbl[:, 2].astype(int) end = tbl[:, 2].astype(int)
ranges: list[tuple[int, int]] = [(int(s), int(e)) for s, e in zip(start, end)] ranges: BandRanges = [(int(s), int(e)) for s, e in zip(start, end)]
for s, e in ranges: for s, e in ranges:
if s < 0 or e < s or e >= k_count: if s < 0 or e < s or e >= k_count:
@@ -154,7 +117,7 @@ def _compute_sw(x: MdctCoeffs) -> MdctCoeffs:
x = np.asarray(x, dtype=np.float64).reshape(-1) x = np.asarray(x, dtype=np.float64).reshape(-1)
k_count = int(x.shape[0]) k_count = int(x.shape[0])
bands = _band_ranges_for_kcount(k_count) bands = _band_ranges(k_count)
sw = np.zeros(k_count, dtype=np.float64) sw = np.zeros(k_count, dtype=np.float64)
for s, e in bands: for s, e in bands:
@@ -384,7 +347,7 @@ def _apply_itns_iir(y: MdctCoeffs, a_q: MdctCoeffs) -> MdctCoeffs:
return x_hat return x_hat
def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]: def _tns_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
""" """
TNS for a single MDCT vector (one long frame or one short subframe). TNS for a single MDCT vector (one long frame or one short subframe).
@@ -411,7 +374,9 @@ def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
sw = _compute_sw(x) sw = _compute_sw(x)
eps = 1e-12 eps = 1e-12
xw = np.where(sw > eps, x / sw, 0.0) xw = np.zeros_like(x, dtype=np.float64)
mask = sw > eps
np.divide(x, sw, out=xw, where=mask)
a = _lpc_coeffs(xw, PRED_ORDER) a = _lpc_coeffs(xw, PRED_ORDER)
a_q = _quantize_coeffs(a) a_q = _quantize_coeffs(a)
@@ -425,7 +390,7 @@ def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public Functions (Level 2) # Public Functions
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChannelF, TnsCoeffs]: def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChannelF, TnsCoeffs]:
@@ -465,7 +430,7 @@ def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChan
a_out = np.empty((PRED_ORDER, 8), dtype=np.float64) a_out = np.empty((PRED_ORDER, 8), dtype=np.float64)
for j in range(8): for j in range(8):
y[:, j], a_out[:, j] = _tns_one_vector(x[:, j]) y[:, j], a_out[:, j] = _tns_vector(x[:, j])
return y, a_out return y, a_out
@@ -478,7 +443,7 @@ def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChan
else: else:
raise ValueError('For non-ESH, frame_F_in must have shape (1024,) or (1024, 1).') raise ValueError('For non-ESH, frame_F_in must have shape (1024,) or (1024, 1).')
y_vec, a_q = _tns_one_vector(x_vec) y_vec, a_q = _tns_vector(x_vec)
if out_shape == (1024,): if out_shape == (1024,):
y_out = y_vec y_out = y_vec
+129
View File
@@ -193,6 +193,61 @@ Bark-band index ranges [start, end] (inclusive) for MDCT lines.
Used by TNS to map MDCT indices k to Bark bands. Used by TNS to map MDCT indices k to Bark bands.
""" """
BarkTable: TypeAlias = FloatArray
"""
Psychoacoustic Bark band table loaded from TableB219.mat.
Typical shapes:
- Long: (69, 6)
- Short: (42, 6)
"""
BandIndexArray: TypeAlias = NDArray[np.int_]
"""
Array of FFT bin indices per psychoacoustic band.
"""
BandValueArray: TypeAlias = FloatArray
"""
Per-band psychoacoustic values (e.g. Bark position, thresholds).
"""
# Quantizer-related semantic aliases
QuantizedSymbols: TypeAlias = NDArray[np.generic]
"""
Quantized MDCT symbols S(k).
Shapes:
- Always (1024, 1) at the quantizer output (ESH packed to 1024 symbols).
"""
ScaleFactors: TypeAlias = NDArray[np.generic]
"""
DPCM-coded scalefactors sfc(b) = alpha(b) - alpha(b-1).
Shapes:
- Long frames: (NB, 1)
- ESH frames: (NB, 8)
"""
GlobalGain: TypeAlias = float | NDArray[np.generic]
"""
Global gain G = alpha(0).
- Long frames: scalar float
- ESH frames: array shape (1, 8)
"""
# Huffman semantic aliases
HuffmanBitstream: TypeAlias = str
"""Huffman-coded bitstream stored as a string of '0'/'1'."""
HuffmanCodebook: TypeAlias = int
"""Huffman codebook id (e.g., 0..11)."""
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Level 1 AAC sequence payload types # Level 1 AAC sequence payload types
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -280,3 +335,77 @@ Level 2 adds:
and stores: and stores:
- per-channel "frame_F" after applying TNS. - per-channel "frame_F" after applying TNS.
""" """
# -----------------------------------------------------------------------------
# Level 3 AAC sequence payload types (Quantizer + Huffman)
# -----------------------------------------------------------------------------
class AACChannelFrameF3(TypedDict):
"""
Per-channel payload for aac_seq_3[i]["chl"] or ["chr"] (Level 3).
Keys
----
tns_coeffs:
Quantized TNS predictor coefficients for ONE channel.
Shapes:
- ESH: (PRED_ORDER, 8)
- else: (PRED_ORDER, 1)
T:
Psychoacoustic thresholds per band.
Shapes:
- ESH: (NB, 8)
- else: (NB, 1)
Note: Stored for completeness / debugging; not entropy-coded.
G:
Quantized global gains.
Shapes:
- ESH: (1, 8) (one per short subframe)
- else: scalar (or compatible np scalar)
sfc:
Huffman-coded scalefactor differences (DPCM sequence).
stream:
Huffman-coded MDCT quantized symbols S(k) (packed to 1024 symbols).
codebook:
Huffman codebook id used for MDCT symbols (stream).
(Scalefactors typically use fixed codebook 11 and do not need to store it.)
"""
tns_coeffs: TnsCoeffs
T: FloatArray
G: FloatArray | float
sfc: HuffmanBitstream
stream: HuffmanBitstream
codebook: HuffmanCodebook
class AACSeq3Frame(TypedDict):
"""
One frame dictionary element of aac_seq_3 (Level 3).
"""
frame_type: FrameType
win_type: WinType
chl: AACChannelFrameF3
chr: AACChannelFrameF3
AACSeq3: TypeAlias = List[AACSeq3Frame]
"""
AAC sequence for Level 3:
List of length K (K = number of frames).
Each element is a dict with keys:
- "frame_type", "win_type", "chl", "chr"
Level 3 adds (per channel):
- "tns_coeffs"
- "T" thresholds (not entropy-coded)
- "G" global gain(s)
- "sfc" Huffman-coded scalefactor differences
- "stream" Huffman-coded MDCT quantized symbols
- "codebook" Huffman codebook for MDCT symbols
"""
+306
View File
@@ -0,0 +1,306 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - AAC Utilities
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Shared utility functions used across AAC encoder/decoder levels.
#
# This module currently provides:
# - MDCT / IMDCT conversions
# - Signal-to-Noise Ratio (SNR) computation in dB
# - Loading and access helpers for psychoacoustic band tables
# (TableB219.mat, Tables B.2.1.9a / B.2.1.9b of the AAC specification)
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from pathlib import Path
from scipy.io import loadmat
from core.aac_types import *
# -----------------------------------------------------------------------------
# Global cached data
# -----------------------------------------------------------------------------
# Cached contents of TableB219.mat to avoid repeated disk I/O.
# Keys:
# - "B219a": long-window psychoacoustic bands (69 bands, FFT size 2048)
# - "B219b": short-window psychoacoustic bands (42 bands, FFT size 256)
B219_CACHE: dict[str, BarkTable] | None = None
# -----------------------------------------------------------------------------
# MDCT / IMDCT
# -----------------------------------------------------------------------------
def mdct(s: TimeSignal) -> MdctCoeffs:
"""
MDCT (direct form) as specified in the assignment.
Parameters
----------
s : TimeSignal
Windowed time samples, 1-D array of length N (N = 2048 or 256).
Returns
-------
MdctCoeffs
MDCT coefficients, 1-D array of length N/2.
Definition
----------
X[k] = 2 * sum_{n=0..N-1} s[n] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
s = np.asarray(s, dtype=np.float64).reshape(-1)
N = int(s.shape[0])
if N not in (2048, 256):
raise ValueError("MDCT input length must be 2048 or 256.")
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(N // 2, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, N/2)
X = 2.0 * (s @ C) # (N/2,)
return X
def imdct(X: MdctCoeffs) -> TimeSignal:
"""
IMDCT (direct form) as specified in the assignment.
Parameters
----------
X : MdctCoeffs
MDCT coefficients, 1-D array of length K (K = 1024 or 128).
Returns
-------
TimeSignal
Reconstructed time samples, 1-D array of length N = 2K.
Definition
----------
s[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
X = np.asarray(X, dtype=np.float64).reshape(-1)
K = int(X.shape[0])
if K not in (1024, 128):
raise ValueError("IMDCT input length must be 1024 or 128.")
N = 2 * K
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(K, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, K)
s = (2.0 / N) * (C @ X) # (N,)
return s
# -----------------------------------------------------------------------------
# Signal quality metrics
# -----------------------------------------------------------------------------
def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Compute the overall Signal-to-Noise Ratio (SNR) in dB.
The SNR is computed over all available samples and channels,
after conservatively aligning the two signals to their common
length and channel count.
Parameters
----------
x_ref : StereoSignal
Reference (original) signal.
Typical shape: (N, 2) for stereo.
x_hat : StereoSignal
Reconstructed or processed signal.
Typical shape: (M, 2) for stereo.
Returns
-------
float
SNR in dB.
- +inf if the noise power is zero (perfect reconstruction).
- -inf if the reference signal power is zero.
"""
x_ref = np.asarray(x_ref, dtype=np.float64)
x_hat = np.asarray(x_hat, dtype=np.float64)
# Ensure 2-D shape: (samples, channels)
if x_ref.ndim == 1:
x_ref = x_ref.reshape(-1, 1)
if x_hat.ndim == 1:
x_hat = x_hat.reshape(-1, 1)
# Align lengths and channel count conservatively
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
x_ref = x_ref[:n, :c]
x_hat = x_hat[:n, :c]
err = x_ref - x_hat
ps = float(np.sum(x_ref * x_ref)) # signal power
pn = float(np.sum(err * err)) # noise power
if pn <= 0.0:
return float("inf")
if ps <= 0.0:
return float("-inf")
return float(10.0 * np.log10(ps / pn))
def estimate_lag_mono(x_ref: TimeSignal, x_hat: TimeSignal, max_lag=4096):
"""
Estimate time lag between two mono signals.
Returns lag (positive means x_hat delayed).
"""
n = min(len(x_ref), len(x_hat))
x_ref = x_ref[:n]
x_hat = x_hat[:n]
corr = np.correlate(x_ref, x_hat, mode='full')
lags = np.arange(-n + 1, n)
center = n - 1
lo = max(0, center - max_lag)
hi = min(len(corr), center + max_lag + 1)
best = lo + int(np.argmax(corr[lo:hi]))
return int(lags[best])
def match_gain(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Least-squares gain g that best maps x_hat -> x_ref.
"""
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
r = x_ref[:n, :c].reshape(-1).astype(np.float64)
h = x_hat[:n, :c].reshape(-1).astype(np.float64)
denom = float(np.dot(h, h))
if denom <= 0.0:
return 1.0
return float(np.dot(r, h) / denom)
# -----------------------------------------------------------------------------
# Psychoacoustic band tables (TableB219.mat)
# -----------------------------------------------------------------------------
def load_b219_tables() -> dict[str, BarkTable]:
"""
Load and cache psychoacoustic band tables from TableB219.mat.
The assignment/project layout assumes that a 'material' directory
is available in the current working directory when running:
- tests
- level_1 / level_2 / level_3 entrypoints
This function loads the tables once and caches them for subsequent calls.
Returns
-------
dict[str, BarkTable]
Dictionary with the following entries:
- "B219a": long-window psychoacoustic table
(69 bands, FFT size 2048 / 1024 spectral lines)
- "B219b": short-window psychoacoustic table
(42 bands, FFT size 256 / 128 spectral lines)
"""
global B219_CACHE
if B219_CACHE is not None:
return B219_CACHE
mat_path = Path("material") / "TableB219.mat"
if not mat_path.exists():
raise FileNotFoundError(
"Could not locate material/TableB219.mat in the current working directory."
)
data = loadmat(str(mat_path))
if "B219a" not in data or "B219b" not in data:
raise ValueError(
"TableB219.mat missing required variables 'B219a' and/or 'B219b'."
)
B219_CACHE = {
"B219a": np.asarray(data["B219a"], dtype=np.float64),
"B219b": np.asarray(data["B219b"], dtype=np.float64),
}
return B219_CACHE
def get_table(frame_type: FrameType) -> tuple[BarkTable, int]:
"""
Select the appropriate psychoacoustic band table and FFT size
based on the AAC frame type.
Parameters
----------
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
Returns
-------
table : BarkTable
Psychoacoustic band table:
- B219a for long frames
- B219b for ESH short subframes
N : int
FFT size corresponding to the table:
- 2048 for long frames
- 256 for short frames (ESH)
"""
tables = load_b219_tables()
if frame_type == "ESH":
return tables["B219b"], 256
return tables["B219a"], 2048
def band_limits(
table: BarkTable,
) -> tuple[BandIndexArray, BandIndexArray, BandValueArray, BandValueArray]:
"""
Extract per-band metadata from a TableB2.1.9 psychoacoustic table.
The column layout follows the provided TableB219.mat file and the
AAC specification tables B.2.1.9a / B.2.1.9b.
Parameters
----------
table : BarkTable
Psychoacoustic band table (B219a or B219b).
Returns
-------
wlow : BandIndexArray
Lower FFT bin index (inclusive) for each band.
whigh : BandIndexArray
Upper FFT bin index (inclusive) for each band.
bval : BandValueArray
Bark-scale (or equivalent) band position values.
Used in the spreading function.
qthr_db : BandValueArray
Threshold in quiet for each band, in dB.
"""
wlow = table[:, 1].astype(int)
whigh = table[:, 2].astype(int)
bval = table[:, 4].astype(np.float64)
qthr_db = table[:, 5].astype(np.float64)
return wlow, whigh, bval, qthr_db
+278 -59
View File
@@ -19,44 +19,70 @@ import numpy as np
import pytest import pytest
import soundfile as sf import soundfile as sf
from core.aac_coder import aac_coder_1, aac_coder_2, aac_read_wav_stereo_48k from core.aac_coder import aac_coder_1, aac_coder_2, aac_coder_3, aac_read_wav_stereo_48k
from core.aac_decoder import aac_decoder_1, aac_decoder_2, aac_remove_padding from core.aac_decoder import aac_decoder_1, aac_decoder_2, aac_decoder_3, aac_remove_padding
from core.aac_utils import snr_db, estimate_lag_mono, match_gain
from core.aac_types import * from core.aac_types import *
from core.aac_snr_db import snr_db
# Helper "fixtures" for aac_coder_1 / i_aac_coder_1 # -----------------------------------------------------------------------------
# Fixtures (small wav logic)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@pytest.fixture(scope="session")
def wav_in_path() -> Path:
"""
Provided input WAV used for end-to-end tests.
Expected project layout:
source/material/LicorDeCalandraca.wav
"""
return Path(__file__).resolve().parents[2] / "material" / "LicorDeCalandraca.wav"
@pytest.fixture() @pytest.fixture()
def tmp_stereo_wav(tmp_path: Path) -> Path: def mk_random_stereo_wav(tmp_path: Path, request: pytest.FixtureRequest) -> Path:
""" """
Create a temporary 48 kHz stereo WAV with random samples. Create a temporary 48 kHz stereo WAV with random samples.
Length (in seconds) must be provided via indirect parametrization.
""" """
length = float(request.param)
rng = np.random.default_rng(123) rng = np.random.default_rng(123)
fs = 48000 fs = 48000
# ~1 second of audio (kept small for test speed). n = int(fs * length)
n = fs
x: StereoSignal = rng.normal(size=(n, 2)).astype(np.float64) x: StereoSignal = rng.normal(size=(n, 2)).astype(np.float64)
wav_path = tmp_path / "in.wav" wav_path = tmp_path / "in_random.wav"
sf.write(str(wav_path), x, fs) sf.write(str(wav_path), x, fs)
return wav_path return wav_path
@pytest.fixture()
def mk_actual_stereo_wav(tmp_path: Path, wav_in_path: Path, request: pytest.FixtureRequest) -> Path:
"""
Create a temporary 48 kHz stereo WAV by chopping from the provided material WAV.
Length can be overridden via indirect parametrization.
"""
length = float(getattr(request, "param", 0.25)) # seconds (default: small)
x, fs = aac_read_wav_stereo_48k(wav_in_path)
n = int(fs * length)
x_short = x[:n, :]
wav_path = tmp_path / "in_actual.wav"
sf.write(str(wav_path), x_short, fs)
return wav_path
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Helper-function tests # Helper-function tests
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_aac_read_wav_stereo_48k_roundtrip(tmp_stereo_wav: Path) -> None: @pytest.mark.parametrize("mk_random_stereo_wav", [2.0], indirect=True)
""" def test_aac_read_wav_stereo_48k_roundtrip(mk_random_stereo_wav: Path) -> None:
Contract test for aac_read_wav_stereo_48k(): x, fs = aac_read_wav_stereo_48k(mk_random_stereo_wav)
- Reads stereo WAV
- Returns float64 array with shape (N,2)
- Returns fs = 48000
"""
x, fs = aac_read_wav_stereo_48k(tmp_stereo_wav)
assert int(fs) == 48000 assert int(fs) == 48000
assert isinstance(x, np.ndarray) assert isinstance(x, np.ndarray)
@@ -67,10 +93,6 @@ def test_aac_read_wav_stereo_48k_roundtrip(tmp_stereo_wav: Path) -> None:
def test_aac_remove_padding_removes_hop_from_both_ends() -> None: def test_aac_remove_padding_removes_hop_from_both_ends() -> None:
"""
Contract test for aac_remove_padding():
- Removes 'hop' samples from start and end.
"""
hop = 1024 hop = 1024
n = 10000 n = 10000
@@ -82,9 +104,6 @@ def test_aac_remove_padding_removes_hop_from_both_ends() -> None:
def test_aac_remove_padding_errors_on_too_short_input() -> None: def test_aac_remove_padding_errors_on_too_short_input() -> None:
"""
aac_remove_padding must raise if y_pad is shorter than 2*hop.
"""
hop = 1024 hop = 1024
y_pad: StereoSignal = np.zeros((2 * hop - 1, 2), dtype=np.float64) y_pad: StereoSignal = np.zeros((2 * hop - 1, 2), dtype=np.float64)
@@ -95,12 +114,10 @@ def test_aac_remove_padding_errors_on_too_short_input() -> None:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Level 1 tests # Level 1 tests
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_aac_coder_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
""" @pytest.mark.parametrize("mk_random_stereo_wav", [0.5], indirect=True)
Module-level contract test: def test_aac_coder_seq_schema_and_shapes(mk_random_stereo_wav: Path) -> None:
Ensure aac_seq_1 follows the expected schema and per-frame shapes. aac_seq: AACSeq1 = aac_coder_1(mk_random_stereo_wav)
"""
aac_seq: AACSeq1 = aac_coder_1(tmp_stereo_wav)
assert isinstance(aac_seq, list) assert isinstance(aac_seq, list)
assert len(aac_seq) > 0 assert len(aac_seq) > 0
@@ -108,7 +125,6 @@ def test_aac_coder_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
for fr in aac_seq: for fr in aac_seq:
assert isinstance(fr, dict) assert isinstance(fr, dict)
# Required keys
assert "frame_type" in fr assert "frame_type" in fr
assert "win_type" in fr assert "win_type" in fr
assert "chl" in fr assert "chl" in fr
@@ -135,42 +151,56 @@ def test_aac_coder_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
assert chl_f.shape == (1024, 1) assert chl_f.shape == (1024, 1)
assert chr_f.shape == (1024, 1) assert chr_f.shape == (1024, 1)
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_end_to_end_aac_coder_decoder_high_snr(tmp_stereo_wav: Path, tmp_path: Path) -> None: def test_level_1_gain_close_to_one(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
""" """
End-to-end test: Guardrail: decoded signal should not have a large global gain mismatch.
Encode + decode and check SNR is very high (numerical-noise only).
The threshold is intentionally loose to avoid fragility across platforms/BLAS.
""" """
x_ref, fs = sf.read(str(tmp_stereo_wav), always_2d=True) x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_1: AACSeq1 = aac_coder_1(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_1(aac_seq_1, out_wav)
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
g = match_gain(x_ref, y_hat)
# print (f"g = {g}")
# Allow some slack but catch big scaling regressions
assert 0.75 <= g <= 1.25
@pytest.mark.parametrize("mk_random_stereo_wav", [0.5], indirect=True)
def test_end_to_end_aac_coder_decoder_high_snr(mk_random_stereo_wav: Path, tmp_path: Path) -> None:
x_ref, fs = sf.read(str(mk_random_stereo_wav), always_2d=True)
x_ref = np.asarray(x_ref, dtype=np.float64) x_ref = np.asarray(x_ref, dtype=np.float64)
assert int(fs) == 48000 assert int(fs) == 48000
out_wav = tmp_path / "out.wav" out_wav = tmp_path / "out.wav"
aac_seq = aac_coder_1(tmp_stereo_wav) aac_seq = aac_coder_1(mk_random_stereo_wav)
x_hat: StereoSignal = aac_decoder_1(aac_seq, out_wav) x_hat: StereoSignal = aac_decoder_1(aac_seq, out_wav)
# Basic sanity: output file exists and is readable
assert out_wav.exists() assert out_wav.exists()
x_hat_file, fs_hat = sf.read(str(out_wav), always_2d=True) _, fs_hat = sf.read(str(out_wav), always_2d=True)
assert int(fs_hat) == 48000 assert int(fs_hat) == 48000
# SNR against returned array (file should match closely, but we do not require it here).
snr = snr_db(x_ref, x_hat) snr = snr_db(x_ref, x_hat)
assert snr > 80.0 assert snr > 80.0
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Level 2 tests (new) # Level 2 tests
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_aac_coder_2_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None: @pytest.mark.parametrize("mk_random_stereo_wav", [0.5], indirect=True)
""" def test_aac_coder_2_seq_schema_and_shapes(mk_random_stereo_wav: Path) -> None:
Module-level contract test (Level 2): aac_seq: AACSeq2 = aac_coder_2(mk_random_stereo_wav)
Ensure aac_seq_2 follows the expected schema and per-frame shapes, including tns_coeffs.
"""
aac_seq: AACSeq2 = aac_coder_2(tmp_stereo_wav)
assert isinstance(aac_seq, list) assert isinstance(aac_seq, list)
assert len(aac_seq) > 0 assert len(aac_seq) > 0
@@ -194,27 +224,44 @@ def test_aac_coder_2_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
if frame_type == "ESH": if frame_type == "ESH":
assert frame_f.shape == (128, 8) assert frame_f.shape == (128, 8)
assert coeffs.shape[0] == 4 assert coeffs.shape == (4, 8)
assert coeffs.shape[1] == 8
else: else:
assert frame_f.shape == (1024, 1) assert frame_f.shape == (1024, 1)
assert coeffs.shape == (4, 1) assert coeffs.shape == (4, 1)
def test_end_to_end_level_2_high_snr(tmp_stereo_wav: Path, tmp_path: Path) -> None: @pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_level_2_gain_close_to_one(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
""" """
End-to-end test (Level 2): Guardrail: decoded signal should not have a large global gain mismatch.
Encode + decode and check SNR remains very high. """
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
Level 2 is still floating-point (TNS is reversible), so reconstruction out_wav = tmp_path / "decoded_level3.wav"
should remain numerical-noise only. aac_seq_2: AACSeq2 = aac_coder_2(mk_actual_stereo_wav)
""" y_hat: StereoSignal = aac_decoder_2(aac_seq_2, out_wav)
x_ref, fs = sf.read(str(tmp_stereo_wav), always_2d=True)
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
g = match_gain(x_ref, y_hat)
# print (f"g = {g}")
# Allow some slack but catch big scaling regressions
assert 0.75 <= g <= 1.25
@pytest.mark.parametrize("mk_random_stereo_wav", [0.5], indirect=True)
def test_end_to_end_level_2_high_snr(mk_random_stereo_wav: Path, tmp_path: Path) -> None:
x_ref, fs = sf.read(str(mk_random_stereo_wav), always_2d=True)
x_ref = np.asarray(x_ref, dtype=np.float64) x_ref = np.asarray(x_ref, dtype=np.float64)
assert int(fs) == 48000 assert int(fs) == 48000
out_wav = tmp_path / "out_l2.wav" out_wav = tmp_path / "out_l2.wav"
aac_seq = aac_coder_2(tmp_stereo_wav) aac_seq = aac_coder_2(mk_random_stereo_wav)
x_hat: StereoSignal = aac_decoder_2(aac_seq, out_wav) x_hat: StereoSignal = aac_decoder_2(aac_seq, out_wav)
assert out_wav.exists() assert out_wav.exists()
@@ -222,4 +269,176 @@ def test_end_to_end_level_2_high_snr(tmp_stereo_wav: Path, tmp_path: Path) -> No
assert int(fs_hat) == 48000 assert int(fs_hat) == 48000
snr = snr_db(x_ref, x_hat) snr = snr_db(x_ref, x_hat)
assert snr > 75.0 assert snr > 80.0
# -----------------------------------------------------------------------------
# Level 3 tests (Quantizer + Huffman)
# -----------------------------------------------------------------------------
def _assert_level3_frame_schema(frame: AACSeq3Frame) -> None:
assert "frame_type" in frame
assert "win_type" in frame
assert "chl" in frame
assert "chr" in frame
for ch_key in ("chl", "chr"):
ch = frame[ch_key] # type: ignore[index]
assert "tns_coeffs" in ch
assert "T" in ch
assert "G" in ch
assert "sfc" in ch
assert "stream" in ch
assert "codebook" in ch
assert isinstance(ch["sfc"], str)
assert isinstance(ch["stream"], str)
assert isinstance(ch["codebook"], int)
assert isinstance(ch["tns_coeffs"], np.ndarray)
assert isinstance(ch["T"], np.ndarray)
assert np.isscalar(ch["G"]) or isinstance(ch["G"], np.ndarray)
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_aac_coder_3_seq_schema_and_shapes(mk_actual_stereo_wav: Path) -> None:
"""
Uses a short WAV excerpt produced by mk_actual_stereo_wav.
0.11s is enough for a few frames at 48 kHz.
"""
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
assert isinstance(aac_seq_3, list)
assert len(aac_seq_3) > 0
for fr in aac_seq_3:
_assert_level3_frame_schema(fr)
frame_type = fr["frame_type"]
for ch_key in ("chl", "chr"):
ch = fr[ch_key] # type: ignore[index]
tns = np.asarray(ch["tns_coeffs"])
if frame_type == "ESH":
assert tns.ndim == 2
assert tns.shape[1] == 8
else:
assert tns.ndim == 2
assert tns.shape[1] == 1
T = np.asarray(ch["T"])
if frame_type == "ESH":
assert T.ndim == 2
assert T.shape[1] == 8
else:
assert T.ndim == 2
assert T.shape[1] == 1
G = ch["G"]
if frame_type == "ESH":
assert isinstance(G, np.ndarray)
assert np.asarray(G).shape == (1, 8)
else:
assert np.isscalar(G)
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_level_3_estimated_lag(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
"""
Check that the estimated lag between reference and decoded
signal is small (zero), to prevent catastrophic misalignment.
"""
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_3(aac_seq_3, out_wav)
# Use only common length
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
lag_L = estimate_lag_mono(x_ref[:, 0], y_hat[:, 0], max_lag=4096)
lag_R = estimate_lag_mono(x_ref[:, 1], y_hat[:, 1], max_lag=4096)
# We allow zero latency
assert abs(lag_L) == 0
assert abs(lag_R) == 0
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_level_3_not_lr_swapped(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
"""
Ensure the decoded stereo channels are not swapped.
If channels are swapped, SNR against the original drops a lot,
while SNR against swapped reference increases.
"""
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_3(aac_seq_3, out_wav)
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
snr_normal = snr_db(x_ref, y_hat)
snr_swapped = snr_db(x_ref[:, [1, 0]], y_hat)
# If the decoded output were swapped, snr_swapped would be significantly higher.
assert snr_normal >= snr_swapped
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_level_3_gain_close_to_one(
mk_actual_stereo_wav: Path,
tmp_path: Path,
) -> None:
"""
Guardrail: decoded signal should not have a large global gain mismatch.
"""
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_3(aac_seq_3, out_wav)
n = min(x_ref.shape[0], y_hat.shape[0])
x_ref = x_ref[:n, :]
y_hat = y_hat[:n, :]
g = match_gain(x_ref, y_hat)
# Allow some slack but catch big scaling regressions
assert 0.75 <= g <= 1.25
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
def test_end_to_end_level_3_high_snr(mk_actual_stereo_wav: Path, tmp_path: Path) -> None:
"""
End-to-end Level 3 using a small WAV excerpt produced by mk_actual_stereo_wav.
"""
x_ref, fs = aac_read_wav_stereo_48k(mk_actual_stereo_wav)
assert int(fs) == 48000
out_wav = tmp_path / "decoded_level3.wav"
aac_seq_3: AACSeq3 = aac_coder_3(mk_actual_stereo_wav)
y_hat: StereoSignal = aac_decoder_3(aac_seq_3, out_wav)
n = min(x_ref.shape[0], y_hat.shape[0])
s = snr_db(x_ref[:n, :], y_hat[:n, :])
assert s > 8.0
+1 -1
View File
@@ -17,7 +17,7 @@ from typing import Sequence
import pytest import pytest
from core.aac_filterbank import aac_filter_bank, aac_i_filter_bank from core.aac_filterbank import aac_filter_bank, aac_i_filter_bank
from core.aac_snr_db import snr_db from core.aac_utils import snr_db
from core.aac_types import * from core.aac_types import *
# Helper fixtures for filterbank # Helper fixtures for filterbank
@@ -1,117 +0,0 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Filterbank internal (mdct) Tests
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Tests for Filterbank internal MDCT/IMDCT functionality.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_filterbank import _imdct, _mdct
from core.aac_types import FloatArray, TimeSignal, MdctCoeffs
def _assert_allclose(a: FloatArray, b: FloatArray, *, rtol: float, atol: float) -> None:
"""
Helper for consistent tolerances across tests.
"""
np.testing.assert_allclose(a, b, rtol=rtol, atol=atol)
def _estimate_gain(y: MdctCoeffs, x: MdctCoeffs) -> float:
"""
Estimate scalar gain g such that y ~= g*x in least-squares sense.
"""
denom = float(np.dot(x, x))
if denom == 0.0:
return 0.0
return float(np.dot(y, x) / denom)
tolerance = 1e-10
@pytest.mark.parametrize("N", [256, 2048])
def test_mdct_imdct_mdct_identity_up_to_gain(N: int) -> None:
"""
Consistency test in coefficient domain:
mdct(imdct(X)) ~= g * X
For the chosen (non-orthonormal) scaling, g is expected to be close to 2.
"""
rng = np.random.default_rng(0)
K = N // 2
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
x: TimeSignal = _imdct(X)
X_hat: MdctCoeffs = _mdct(x)
g = _estimate_gain(X_hat, X)
_assert_allclose(X_hat, g * X, rtol=tolerance, atol=tolerance)
_assert_allclose(np.array([g], dtype=np.float64), np.array([2.0], dtype=np.float64), rtol=tolerance, atol=tolerance)
@pytest.mark.parametrize("N", [256, 2048])
def test_mdct_linearity(N: int) -> None:
"""
Linearity test:
mdct(a*x + b*y) == a*mdct(x) + b*mdct(y)
"""
rng = np.random.default_rng(1)
x: TimeSignal = rng.normal(size=N).astype(np.float64)
y: TimeSignal = rng.normal(size=N).astype(np.float64)
a = 0.37
b = -1.12
left: MdctCoeffs = _mdct(a * x + b * y)
right: MdctCoeffs = a * _mdct(x) + b * _mdct(y)
_assert_allclose(left, right, rtol=tolerance, atol=tolerance)
@pytest.mark.parametrize("N", [256, 2048])
def test_imdct_linearity(N: int) -> None:
"""
Linearity test for IMDCT:
imdct(a*X + b*Y) == a*imdct(X) + b*imdct(Y)
"""
rng = np.random.default_rng(2)
K = N // 2
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
Y: MdctCoeffs = rng.normal(size=K).astype(np.float64)
a = -0.5
b = 2.0
left: TimeSignal = _imdct(a * X + b * Y)
right: TimeSignal = a * _imdct(X) + b * _imdct(Y)
_assert_allclose(left, right, rtol=tolerance, atol=tolerance)
@pytest.mark.parametrize("N", [256, 2048])
def test_mdct_imdct_outputs_are_finite(N: int) -> None:
"""
Sanity test: no NaN/inf on random inputs.
"""
rng = np.random.default_rng(3)
K = N // 2
x: TimeSignal = rng.normal(size=N).astype(np.float64)
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
X1 = _mdct(x)
x1 = _imdct(X)
assert np.isfinite(X1).all()
assert np.isfinite(x1).all()
+139
View File
@@ -0,0 +1,139 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Huffman Wrapper Tests (Level 3)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Contract tests for the Huffman coding stage, using the provided
# Huffman utilities (material/huff_utils.py).
#
# The Huffman encoder/decoder itself is GIVEN by the assignment and
# is not re-implemented here. These tests only verify that:
#
# - The wrapper functions (aac_encode_huff / aac_decode_huff) expose
# the API described in the assignment.
# - Forced codebook selection works as expected (e.g. scalefactors).
# - Tuple-based Huffman coding semantics are respected.
#
# Notes on tuple coding:
# Huffman coding operates on tuples of symbols. As a result,
# decode(encode(x)) may return extra trailing symbols due to padding.
# The AAC decoder always knows the true section length (from band limits)
# and truncates accordingly. Therefore, these tests only enforce that
# the decoded PREFIX matches the original data.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_huffman import aac_encode_huff, aac_decode_huff
from material.huff_utils import load_LUT
# -----------------------------------------------------------------------------
# Fixtures
# -----------------------------------------------------------------------------
@pytest.fixture(scope="module")
def huff_LUT():
"""
Load Huffman Look-Up Tables (LUTs) once per test module.
The LUTs are provided by the assignment (huffCodebooks.mat) via
material.huff_utils.load_LUT().
"""
return load_LUT()
# -----------------------------------------------------------------------------
# Roundtrip (prefix) tests
# -----------------------------------------------------------------------------
@pytest.mark.parametrize(
"coeff_sec",
[
np.array([1, -1, 2, -2, 0, 0, 3], dtype=np.int64),
np.array([0, 0, 0, 0], dtype=np.int64),
np.array([5], dtype=np.int64),
np.array([-3, -3, -3, -3], dtype=np.int64),
],
)
def test_huffman_roundtrip_prefix_matches(
coeff_sec: np.ndarray,
huff_LUT,
) -> None:
"""
Contract test for Huffman encode/decode.
Guarantees:
- Encoding followed by decoding does not crash.
- The decoded output has at least as many symbols as the input.
- The prefix of the decoded output matches the original coefficients.
Rationale:
Huffman tuple coding may introduce padding, so exact length equality
is NOT required or expected.
"""
huff_sec, cb = aac_encode_huff(coeff_sec, huff_LUT)
dec = aac_decode_huff(huff_sec, cb, huff_LUT)
if cb == 0:
# Codebook 0 represents an all-zero section.
assert np.all(coeff_sec == 0)
assert dec.size == 0
return
assert dec.size >= coeff_sec.size
np.testing.assert_array_equal(dec[: coeff_sec.size], coeff_sec)
# -----------------------------------------------------------------------------
# Forced codebook tests
# -----------------------------------------------------------------------------
def test_huffman_force_codebook_returns_requested_codebook(huff_LUT) -> None:
"""
Verify forced codebook selection.
According to the assignment, scalefactors must be encoded using
Huffman codebook 11. This test checks that:
- The requested codebook is actually used.
- The decoded prefix matches the original scalefactors.
"""
scalefactors = np.array([10, -2, 1, 0, -1, 3], dtype=np.int64)
huff_sec, cb = aac_encode_huff(
scalefactors,
huff_LUT,
force_codebook=11,
)
assert cb == 11
assert isinstance(huff_sec, str)
dec = aac_decode_huff(huff_sec, cb, huff_LUT)
assert dec.size >= scalefactors.size
np.testing.assert_array_equal(dec[: scalefactors.size], scalefactors)
# -----------------------------------------------------------------------------
# Error handling
# -----------------------------------------------------------------------------
def test_huffman_invalid_codebook_raises(huff_LUT) -> None:
"""
Decoding with an invalid Huffman codebook index must raise an error.
"""
with pytest.raises(Exception):
_ = aac_decode_huff(
huff_sec="010101",
huff_codebook=99,
huff_LUT=huff_LUT,
)
+253
View File
@@ -0,0 +1,253 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Psychoacoustic Model Tests
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
#
# Description:
# Contract + sanity tests for the psychoacoustic model (core.aac_psycho).
#
# These tests focus on:
# - output shapes per frame_type (long vs ESH),
# - numerical sanity (finite, non-negative),
# - deterministic behavior,
# - ESH central-region dependency (outer regions must not affect result),
# - basic input validation (length checks).
#
# We intentionally avoid asserting exact numeric values, because the model
# includes FFT operations and table-driven psychoacoustic parameters.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_psycho import aac_psycho
from core.aac_types import FrameChannelT, FrameType
# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------
def _make_frames(
*,
kind: str,
amp: float = 1.0,
seed: int = 0,
) -> tuple[FrameChannelT, FrameChannelT, FrameChannelT]:
"""
Create (current, prev1, prev2) 2048-sample frames for one channel.
Parameters
----------
kind : str
"noise" or "tone".
amp : float
Amplitude scaling (applied to all frames).
seed : int
RNG seed for reproducibility (noise case).
Returns
-------
tuple[FrameChannelT, FrameChannelT, FrameChannelT]
Three arrays of shape (2048,), dtype float64.
"""
if kind == "noise":
rng = np.random.default_rng(seed)
x2 = amp * rng.normal(size=2048).astype(np.float64)
x1 = amp * rng.normal(size=2048).astype(np.float64)
x0 = amp * rng.normal(size=2048).astype(np.float64)
return x0, x1, x2
if kind == "tone":
# A simple sinusoid which is identical across frames (highly predictable).
n = np.arange(2048, dtype=np.float64)
f0 = 13.0 # arbitrary normalized-bin-ish tone (not critical for these tests)
tone = amp * np.sin(2.0 * np.pi * f0 * n / 2048.0).astype(np.float64)
return tone, tone.copy(), tone.copy()
raise ValueError(f"Unknown kind: {kind!r}")
def _assert_finite_nonnegative(x: np.ndarray) -> None:
"""Utility assertions for psycho outputs."""
assert np.isfinite(x).all()
# SMR is a ratio of energies, it should not be negative.
assert np.min(x) >= 0.0
# -----------------------------------------------------------------------------
# Shape / contract tests
# -----------------------------------------------------------------------------
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_psycho_long_shapes(frame_type: FrameType) -> None:
"""
Contract test:
For long frame types, psycho returns SMR shape (69,).
"""
x0, x1, x2 = _make_frames(kind="noise", seed=1, amp=1.0)
smr = aac_psycho(x0, frame_type, x1, x2)
assert isinstance(smr, np.ndarray)
assert smr.shape == (69,)
_assert_finite_nonnegative(smr)
def test_psycho_esh_shape() -> None:
"""
Contract test:
For ESH, psycho returns SMR shape (42, 8).
"""
x0, x1, x2 = _make_frames(kind="noise", seed=2, amp=1.0)
smr = aac_psycho(x0, "ESH", x1, x2)
assert isinstance(smr, np.ndarray)
assert smr.shape == (42, 8)
_assert_finite_nonnegative(smr)
def test_psycho_is_deterministic_for_same_inputs() -> None:
"""
Determinism test:
Psycho must return the same output for identical inputs.
"""
x0, x1, x2 = _make_frames(kind="noise", seed=3, amp=1.0)
smr1 = aac_psycho(x0, "OLS", x1, x2)
smr2 = aac_psycho(x0, "OLS", x1, x2)
np.testing.assert_allclose(smr1, smr2, rtol=0.0, atol=0.0)
# -----------------------------------------------------------------------------
# ESH-specific behavior tests
# -----------------------------------------------------------------------------
def test_psycho_esh_ignores_outer_regions() -> None:
"""
Spec-driven behavior test:
In this project, ESH uses only the central region of the 2048-sample frame to
derive the 8 overlapping 256-sample subframes:
start = 448 + 128*j, j=0..7
Therefore, changing samples outside [448, 1600) must not affect the output.
"""
rng = np.random.default_rng(10)
# Build base frames (current and prev1) with identical central region.
center_cur = rng.normal(size=1152).astype(np.float64)
center_prev1 = rng.normal(size=1152).astype(np.float64)
cur_a = np.zeros(2048, dtype=np.float64)
cur_b = np.zeros(2048, dtype=np.float64)
prev1_a = np.zeros(2048, dtype=np.float64)
prev1_b = np.zeros(2048, dtype=np.float64)
cur_a[448:1600] = center_cur
cur_b[448:1600] = center_cur
prev1_a[448:1600] = center_prev1
prev1_b[448:1600] = center_prev1
# Modify only outer regions in the *_b variants.
cur_b[:448] = rng.normal(size=448)
cur_b[1600:] = rng.normal(size=448)
prev1_b[:448] = rng.normal(size=448)
prev1_b[1600:] = rng.normal(size=448)
# prev2 is irrelevant for the chosen ESH history convention; keep it fixed.
prev2 = rng.normal(size=2048).astype(np.float64)
smr_a = aac_psycho(cur_a, "ESH", prev1_a, prev2)
smr_b = aac_psycho(cur_b, "ESH", prev1_b, prev2)
np.testing.assert_allclose(smr_a, smr_b, rtol=0.0, atol=0.0)
def test_psycho_esh_columns_are_not_all_identical_for_random_input() -> None:
"""
Sanity test:
For random input, different ESH subframes should typically produce
different SMR columns (not a strict requirement, but a strong sanity signal).
We check that at least one column differs from another beyond a tiny tolerance.
"""
x0, x1, x2 = _make_frames(kind="noise", seed=11, amp=1.0)
smr = aac_psycho(x0, "ESH", x1, x2)
# Compare column 0 vs column 7; for random signals they should differ.
diff = np.max(np.abs(smr[:, 0] - smr[:, 7]))
assert diff > 1e-12
# -----------------------------------------------------------------------------
# Scaling sanity (avoid fragile numeric targets)
# -----------------------------------------------------------------------------
def test_psycho_long_smr_is_mostly_monotone_with_amplitude() -> None:
"""
Sanity test:
Increasing signal amplitude should not reduce the SMR for the vast majority
of Bark bands.
Due to the use of max(nb, qthr), a small fraction of bands close to the
threshold-in-quiet boundary may violate strict monotonicity. This is expected
behavior, so we test a percentage-based criterion instead of a strict one.
"""
x0, x1, x2 = _make_frames(kind="noise", seed=20, amp=1e3)
y0, y1, y2 = (2.0 * x0, 2.0 * x1, 2.0 * x2)
smr1 = aac_psycho(x0, "OLS", x1, x2)
smr2 = aac_psycho(y0, "OLS", y1, y2)
eps = 1e-12
nondecreasing = np.sum(smr2 + eps >= smr1)
ratio = nondecreasing / smr1.size
# Expect monotonic behavior for the overwhelming majority of bands.
assert ratio >= 0.95
def test_psycho_long_is_approximately_scale_invariant_at_high_level() -> None:
"""
Sanity test (robust):
At high levels, SMR should be approximately scale-invariant for most bands.
Some bands may deviate due to the max(nb, qthr) branch.
"""
x0, x1, x2 = _make_frames(kind="noise", seed=20, amp=1e3)
y0, y1, y2 = (2.0 * x0, 2.0 * x1, 2.0 * x2)
smr1 = aac_psycho(x0, "OLS", x1, x2)
smr2 = aac_psycho(y0, "OLS", y1, y2)
rel = np.abs(smr2 - smr1) / np.maximum(np.abs(smr1), 1e-12)
# Most bands should be close (<= 5%), but allow a small number of outliers.
close = np.sum(rel <= 5e-2)
assert close >= (smr1.size - 2) # allow up to 2 bands to deviate
# -----------------------------------------------------------------------------
# Input validation tests
# -----------------------------------------------------------------------------
def test_psycho_rejects_wrong_lengths() -> None:
"""
Contract test:
aac_psycho requires 2048-sample frames for current/prev1/prev2.
"""
x = np.zeros(2048, dtype=np.float64)
bad = np.zeros(2047, dtype=np.float64)
with pytest.raises(ValueError):
_ = aac_psycho(bad, "OLS", x, x)
with pytest.raises(ValueError):
_ = aac_psycho(x, "OLS", bad, x)
with pytest.raises(ValueError):
_ = aac_psycho(x, "OLS", x, bad)
+395
View File
@@ -0,0 +1,395 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Quantizer Tests
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Tests for Quantizer / iQuantizer module.
#
# These tests are deliberately "contract-oriented":
# - They validate shapes, dtypes and invariants that downstream stages
# (e.g., Huffman coding) depend on.
# - They do not attempt to validate psychoacoustic optimality (that would
# require a reference implementation and careful numerical baselines).
#
# Validates:
# - I/O shapes for long and ESH modes
# - DPCM scalefactor coding consistency (sfc)
# - ESH packing order of quantized symbols (128x8 <-> 1024)
# - Edge cases (zeros / near silence)
# - Sanity (finite outputs, no extreme numerical blow-up)
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_quantizer import aac_quantizer, aac_i_quantizer
from core.aac_utils import get_table, band_limits
from core.aac_types import FrameType
# Small epsilon to avoid divisions by zero in sanity ratios
EPS = 1e-12
# -----------------------------------------------------------------------------
# Helper utilities
# -----------------------------------------------------------------------------
def _nbands(frame_type: FrameType) -> int:
"""
Return number of scalefactor bands for the given frame type.
This is derived from TableB219 (psycho tables) via aac_utils helpers,
so the tests remain consistent even if tables are updated.
"""
table, _nfft = get_table(frame_type)
wlow, _whigh, _bval, _qthr = band_limits(table)
return int(len(wlow))
def _make_smr(frame_type: FrameType, seed: int = 0) -> np.ndarray:
"""
Create a strictly positive SMR array with the correct shape.
These tests are not about psycho correctness; they only need SMR > 0
to avoid division by zero and to make the quantizer's threshold logic
behave deterministically.
"""
rng = np.random.default_rng(seed)
NB = _nbands(frame_type)
if frame_type == "ESH":
# ESH uses 8 short windows, thus SMR has 8 columns.
return (1.0 + np.abs(rng.normal(size=(NB, 8)))).astype(np.float64)
# Long frames: use a column vector (NB, 1).
return (1.0 + np.abs(rng.normal(size=(NB, 1)))).astype(np.float64)
def _reconstruct_alpha_from_sfc(sfc: np.ndarray) -> np.ndarray:
"""
Reconstruct alpha(b) from DPCM-coded scalefactors sfc(b).
By definition in the assignment:
sfc(0) = alpha(0)
alpha(b) = alpha(b-1) + sfc(b) for b > 0
This reconstruction is useful to validate the internal consistency
of the produced scalefactor information.
"""
sfc = np.asarray(sfc, dtype=np.int64)
# Long frames: sfc shape (NB, 1)
if sfc.ndim == 2 and sfc.shape[1] == 1:
NB = sfc.shape[0]
alpha = np.zeros((NB,), dtype=np.int64)
alpha[0] = int(sfc[0, 0])
for b in range(1, NB):
alpha[b] = int(alpha[b - 1] + sfc[b, 0])
return alpha
# ESH frames: sfc shape (NB, 8)
if sfc.ndim == 2 and sfc.shape[1] == 8:
NB = sfc.shape[0]
alpha = np.zeros((NB, 8), dtype=np.int64)
alpha[0, :] = sfc[0, :]
for b in range(1, NB):
alpha[b, :] = alpha[b - 1, :] + sfc[b, :]
return alpha
raise ValueError("Unsupported sfc shape.")
# -----------------------------------------------------------------------------
# Shape / contract tests
# -----------------------------------------------------------------------------
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_quantizer_shapes_long(frame_type: FrameType) -> None:
"""
Contract test for long frames:
- Input: MDCT coefficients shape (1024, 1)
- Output S: always (1024, 1)
- Output sfc: (NB, 1)
- G: scalar float for long frames
- iQuantizer output: (1024, 1)
"""
NB = _nbands(frame_type)
rng = np.random.default_rng(1)
X = rng.normal(size=(1024, 1)).astype(np.float64)
SMR = _make_smr(frame_type, seed=2)
S, sfc, G = aac_quantizer(X, frame_type, SMR)
assert S.shape == (1024, 1)
assert sfc.shape == (NB, 1)
assert isinstance(G, (float, np.floating))
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
assert Xhat.shape == (1024, 1)
def test_quantizer_shapes_esh() -> None:
"""
Contract test for ESH frames:
- Input: MDCT coefficients shape (128, 8)
- Output S: packed to (1024, 1)
- Output sfc: (NB, 8)
- G: array shape (1, 8) for ESH (one gain per short window)
- iQuantizer output: (128, 8)
"""
frame_type: FrameType = "ESH"
NB = _nbands(frame_type)
rng = np.random.default_rng(3)
X = rng.normal(size=(128, 8)).astype(np.float64)
SMR = _make_smr(frame_type, seed=4)
S, sfc, G = aac_quantizer(X, frame_type, SMR)
assert S.shape == (1024, 1)
assert sfc.shape == (NB, 8)
assert isinstance(G, np.ndarray)
assert G.shape == (1, 8)
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
assert Xhat.shape == (128, 8)
# -----------------------------------------------------------------------------
# DPCM consistency tests
# -----------------------------------------------------------------------------
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_quantizer_dpcm_reconstructs_alpha_long(frame_type: FrameType) -> None:
"""
Verify the DPCM coding rule for long frames.
The quantizer returns:
sfc(0) = alpha(0)
sfc(b) = alpha(b) - alpha(b-1), b>0
Reconstruct alpha from sfc and check:
alpha(0) == sfc(0) == G
"""
rng = np.random.default_rng(5)
X = rng.normal(size=(1024, 1)).astype(np.float64)
SMR = _make_smr(frame_type, seed=6)
_S, sfc, G = aac_quantizer(X, frame_type, SMR)
alpha = _reconstruct_alpha_from_sfc(sfc)
assert int(sfc[0, 0]) == int(alpha[0])
assert float(alpha[0]) == float(G)
def test_quantizer_dpcm_reconstructs_alpha_esh() -> None:
"""
Verify the DPCM coding rule for ESH frames.
For each short window j:
sfc(0, j) = alpha(0, j) == G(0, j)
"""
frame_type: FrameType = "ESH"
rng = np.random.default_rng(7)
X = rng.normal(size=(128, 8)).astype(np.float64)
SMR = _make_smr(frame_type, seed=8)
_S, sfc, G = aac_quantizer(X, frame_type, SMR)
alpha = _reconstruct_alpha_from_sfc(sfc)
assert np.all(alpha[0, :] == sfc[0, :])
assert np.all(alpha[0, :] == G.reshape(-1))
# -----------------------------------------------------------------------------
# ESH packing order test
# -----------------------------------------------------------------------------
def test_quantizer_esh_packing_order_matches_iquantizer_layout() -> None:
"""
Verify ESH packing order.
The quantizer outputs S in packed shape (1024, 1). The expected packing
is column-major concatenation of the 8 short subframes.
This test constructs a deterministic input where each subframe column
has a distinct constant value. After quantize+inverse-quantize, the
reconstructed columns should remain distinguishable in the same order.
This primarily tests ordering, not exact numerical values.
"""
frame_type: FrameType = "ESH"
NB = _nbands(frame_type)
# Create 8 distinct subframes: column j is constant (j+1)
X = np.zeros((128, 8), dtype=np.float64)
for j in range(8):
X[:, j] = float(j + 1)
# Use very large SMR so thresholds are permissive and alpha changes are
# minimal. This helps keep the ordering signal strong.
SMR = np.ones((NB, 8), dtype=np.float64) * 1e6
S, sfc, G = aac_quantizer(X, frame_type, SMR)
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
# The average magnitude per column must be increasing with the original order.
col_means = np.mean(Xhat, axis=0)
assert np.all(np.diff(col_means) > 0.0)
# -----------------------------------------------------------------------------
# Edge cases: zeros and near-silence
# -----------------------------------------------------------------------------
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_quantizer_zero_input_long_is_finite(frame_type: FrameType) -> None:
"""
Edge case: zero MDCT coefficients should not produce NaN/Inf.
We do not require identity here (quantizer is lossy), but we require
the pipeline to remain numerically safe and produce finite outputs.
"""
NB = _nbands(frame_type)
X = np.zeros((1024, 1), dtype=np.float64)
SMR = np.ones((NB, 1), dtype=np.float64)
S, sfc, G = aac_quantizer(X, frame_type, SMR)
assert np.isfinite(S).all()
assert np.isfinite(sfc).all()
assert isinstance(G, (float, np.floating))
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
assert np.isfinite(Xhat).all()
def test_quantizer_zero_input_esh_is_finite() -> None:
"""
Edge case: same as above, for ESH mode.
"""
frame_type: FrameType = "ESH"
NB = _nbands(frame_type)
X = np.zeros((128, 8), dtype=np.float64)
SMR = np.ones((NB, 8), dtype=np.float64)
S, sfc, G = aac_quantizer(X, frame_type, SMR)
assert np.isfinite(S).all()
assert np.isfinite(sfc).all()
assert np.isfinite(G).all()
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
assert np.isfinite(Xhat).all()
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_quantizer_near_silence_long_is_finite(frame_type: FrameType) -> None:
"""
Edge case: extremely small values.
This stresses numerical guards (EPS usage) and ensures no invalid operations.
"""
NB = _nbands(frame_type)
X = (1e-15 * np.ones((1024, 1), dtype=np.float64))
SMR = np.ones((NB, 1), dtype=np.float64)
S, sfc, G = aac_quantizer(X, frame_type, SMR)
assert np.isfinite(S).all()
assert np.isfinite(sfc).all()
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
assert np.isfinite(Xhat).all()
def test_quantizer_near_silence_esh_is_finite() -> None:
"""
Edge case: extremely small values, ESH mode.
"""
frame_type: FrameType = "ESH"
NB = _nbands(frame_type)
X = (1e-15 * np.ones((128, 8), dtype=np.float64))
SMR = np.ones((NB, 8), dtype=np.float64)
S, sfc, G = aac_quantizer(X, frame_type, SMR)
assert np.isfinite(S).all()
assert np.isfinite(sfc).all()
assert np.isfinite(G).all()
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
assert np.isfinite(Xhat).all()
# -----------------------------------------------------------------------------
# Sanity: avoid catastrophic numerical blow-up
# -----------------------------------------------------------------------------
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_quantizer_sanity_no_extreme_blowup_long(frame_type: FrameType) -> None:
"""
Loose sanity guard.
The quantizer is lossy, but it should not produce reconstructions with
catastrophic peak/energy growth compared to the input.
"""
NB = _nbands(frame_type)
rng = np.random.default_rng(11)
X = rng.normal(size=(1024, 1)).astype(np.float64)
SMR = np.ones((NB, 1), dtype=np.float64) * 10.0
S, sfc, G = aac_quantizer(X, frame_type, SMR)
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
in_peak = float(np.max(np.abs(X)))
out_peak = float(np.max(np.abs(Xhat)))
peak_ratio = out_peak / (in_peak + EPS)
in_energy = float(np.sum(X * X))
out_energy = float(np.sum(Xhat * Xhat))
energy_ratio = out_energy / (in_energy + EPS)
# Very loose thresholds: only catch severe regressions.
assert peak_ratio < 100.0
assert energy_ratio < 1e4
def test_quantizer_sanity_no_extreme_blowup_esh() -> None:
"""
Same loose sanity guard for ESH mode.
"""
frame_type: FrameType = "ESH"
NB = _nbands(frame_type)
rng = np.random.default_rng(12)
X = rng.normal(size=(128, 8)).astype(np.float64)
SMR = np.ones((NB, 8), dtype=np.float64) * 10.0
S, sfc, G = aac_quantizer(X, frame_type, SMR)
Xhat = aac_i_quantizer(S, sfc, G, frame_type)
in_peak = float(np.max(np.abs(X)))
out_peak = float(np.max(np.abs(Xhat)))
peak_ratio = out_peak / (in_peak + EPS)
in_energy = float(np.sum(X * X))
out_energy = float(np.sum(Xhat * Xhat))
energy_ratio = out_energy / (in_energy + EPS)
assert peak_ratio < 100.0
assert energy_ratio < 1e4
-98
View File
@@ -1,98 +0,0 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - SNR dB Tests
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Basic tests for SNR calculation utility.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_snr_db import snr_db
from core.aac_types import StereoSignal
def test_snr_perfect_reconstruction_returns_inf() -> None:
"""
If x_hat == x_ref exactly, noise power is zero and SNR must be +inf.
"""
rng = np.random.default_rng(0)
x: StereoSignal = rng.normal(size=(1024, 2)).astype(np.float64)
snr = snr_db(x, x)
assert snr == float("inf")
def test_snr_zero_reference_returns_minus_inf() -> None:
"""
If reference signal is identically zero, signal power is zero
and SNR must be -inf (unless noise is also zero, which is degenerate).
"""
x_ref: StereoSignal = np.zeros((1024, 2), dtype=np.float64)
x_hat: StereoSignal = np.ones((1024, 2), dtype=np.float64)
snr = snr_db(x_ref, x_hat)
assert snr == float("-inf")
def test_snr_known_noise_level_matches_expected_value() -> None:
"""
Deterministic test with known signal and noise power.
Let:
x_ref = ones
x_hat = ones + noise
With noise variance sigma^2, expected SNR:
10 * log10(Ps / Pn)
"""
n = 1000
sigma = 0.1
x_ref: StereoSignal = np.ones((n, 2), dtype=np.float64)
noise = sigma * np.ones((n, 2), dtype=np.float64)
x_hat: StereoSignal = x_ref + noise
ps = float(np.sum(x_ref * x_ref))
pn = float(np.sum(noise * noise))
expected = 10.0 * np.log10(ps / pn)
snr = snr_db(x_ref, x_hat)
assert np.isclose(snr, expected, rtol=1e-12, atol=1e-12)
def test_snr_aligns_different_lengths_and_channels() -> None:
"""
The function must:
- align to minimum length
- align to minimum channel count
without crashing.
"""
rng = np.random.default_rng(1)
x_ref: StereoSignal = rng.normal(size=(1000, 2)).astype(np.float64)
x_hat: StereoSignal = rng.normal(size=(800, 1)).astype(np.float64)
snr = snr_db(x_ref, x_hat)
assert np.isfinite(snr)
def test_snr_accepts_1d_inputs() -> None:
"""
1-D inputs must be accepted and treated as single-channel signals.
"""
rng = np.random.default_rng(2)
x_ref = rng.normal(size=1024).astype(np.float64)
x_hat = x_ref + 0.01 * rng.normal(size=1024).astype(np.float64)
snr = snr_db(x_ref, x_hat)
assert np.isfinite(snr)
@@ -16,7 +16,7 @@ from __future__ import annotations
import numpy as np import numpy as np
from core.aac_ssc import aac_SSC from core.aac_ssc import aac_ssc
from core.aac_types import FrameT from core.aac_types import FrameT
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -117,10 +117,10 @@ def test_ssc_fixed_cases_prev_lss_and_lps() -> None:
next_attack = _next_frame_strong_attack(attack_left=True, attack_right=True) next_attack = _next_frame_strong_attack(attack_left=True, attack_right=True)
out1 = aac_SSC(frame_t, next_attack, "LSS") out1 = aac_ssc(frame_t, next_attack, "LSS")
assert out1 == "ESH" assert out1 == "ESH"
out2 = aac_SSC(frame_t, next_attack, "LPS") out2 = aac_ssc(frame_t, next_attack, "LPS")
assert out2 == "OLS" assert out2 == "OLS"
@@ -138,7 +138,7 @@ def test_prev_ols_next_not_esh_returns_ols() -> None:
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64) frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
next_t = _next_frame_no_attack() next_t = _next_frame_no_attack()
out = aac_SSC(frame_t, next_t, "OLS") out = aac_ssc(frame_t, next_t, "OLS")
assert out == "OLS" assert out == "OLS"
@@ -151,7 +151,7 @@ def test_prev_ols_next_esh_both_channels_returns_lss() -> None:
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64) frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
next_t = _next_frame_strong_attack(attack_left=True, attack_right=True) next_t = _next_frame_strong_attack(attack_left=True, attack_right=True)
out = aac_SSC(frame_t, next_t, "OLS") out = aac_ssc(frame_t, next_t, "OLS")
assert out == "LSS" assert out == "LSS"
@@ -165,11 +165,11 @@ def test_prev_ols_next_esh_one_channel_returns_lss() -> None:
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64) frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
next1_t = _next_frame_strong_attack(attack_left=True, attack_right=False) next1_t = _next_frame_strong_attack(attack_left=True, attack_right=False)
out1 = aac_SSC(frame_t, next1_t, "OLS") out1 = aac_ssc(frame_t, next1_t, "OLS")
assert out1 == "LSS" assert out1 == "LSS"
next2_t = _next_frame_strong_attack(attack_left=False, attack_right=True) next2_t = _next_frame_strong_attack(attack_left=False, attack_right=True)
out2 = aac_SSC(frame_t, next2_t, "OLS") out2 = aac_ssc(frame_t, next2_t, "OLS")
assert out2 == "LSS" assert out2 == "LSS"
@@ -182,7 +182,7 @@ def test_prev_esh_next_esh_both_channels_returns_esh() -> None:
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64) frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
next_t = _next_frame_strong_attack(attack_left=True, attack_right=True) next_t = _next_frame_strong_attack(attack_left=True, attack_right=True)
out = aac_SSC(frame_t, next_t, "ESH") out = aac_ssc(frame_t, next_t, "ESH")
assert out == "ESH" assert out == "ESH"
@@ -195,7 +195,7 @@ def test_prev_esh_next_not_esh_both_channels_returns_lps() -> None:
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64) frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
next_t = _next_frame_no_attack() next_t = _next_frame_no_attack()
out = aac_SSC(frame_t, next_t, "ESH") out = aac_ssc(frame_t, next_t, "ESH")
assert out == "LPS" assert out == "LPS"
@@ -209,11 +209,11 @@ def test_prev_esh_next_esh_one_channel_merged_is_esh() -> None:
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64) frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
next1_t = _next_frame_strong_attack(attack_left=True, attack_right=False) next1_t = _next_frame_strong_attack(attack_left=True, attack_right=False)
out1 = aac_SSC(frame_t, next1_t, "ESH") out1 = aac_ssc(frame_t, next1_t, "ESH")
assert out1 == "ESH" assert out1 == "ESH"
next2_t = _next_frame_strong_attack(attack_left=False, attack_right=True) next2_t = _next_frame_strong_attack(attack_left=False, attack_right=True)
out2 = aac_SSC(frame_t, next2_t, "ESH") out2 = aac_ssc(frame_t, next2_t, "ESH")
assert out2 == "ESH" assert out2 == "ESH"
@@ -230,5 +230,5 @@ def test_threshold_s_must_exceed_1e_3() -> None:
frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64) frame_t: FrameT = np.zeros((2048, 2), dtype=np.float64)
next_t = _next_frame_below_s_threshold(left=True, right=True, impulse_amp=0.01) next_t = _next_frame_below_s_threshold(left=True, right=True, impulse_amp=0.01)
out = aac_SSC(frame_t, next_t, "OLS") out = aac_ssc(frame_t, next_t, "OLS")
assert out == "OLS" assert out == "OLS"
+130
View File
@@ -26,6 +26,7 @@ from core.aac_configuration import PRED_ORDER, QUANT_MAX, QUANT_STEP
from core.aac_tns import aac_tns, aac_i_tns from core.aac_tns import aac_tns, aac_i_tns
from core.aac_types import * from core.aac_types import *
EPS = 1e-12
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Helper utilities # Helper utilities
@@ -194,3 +195,132 @@ def test_tns_outputs_are_finite() -> None:
out_esh, coeffs_esh = aac_tns(frame_F_esh, "ESH") out_esh, coeffs_esh = aac_tns(frame_F_esh, "ESH")
assert np.isfinite(out_esh).all() assert np.isfinite(out_esh).all()
assert np.isfinite(coeffs_esh).all() assert np.isfinite(coeffs_esh).all()
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_tns_zero_input_is_identity_long(frame_type: FrameType) -> None:
"""
Edge case: zero MDCT coefficients should remain zero after TNS and iTNS.
This checks that no NaN/Inf appears and the pipeline is numerically safe.
"""
frame_F_in = np.zeros((1024, 1), dtype=np.float64)
frame_F_tns, tns_coeffs = aac_tns(frame_F_in, frame_type)
assert np.isfinite(frame_F_tns).all()
assert np.isfinite(tns_coeffs).all()
assert np.all(frame_F_tns == 0.0)
frame_F_hat = aac_i_tns(frame_F_tns, frame_type, tns_coeffs)
assert np.isfinite(frame_F_hat).all()
assert np.all(frame_F_hat == 0.0)
def test_tns_zero_input_is_identity_esh() -> None:
"""
Edge case: zero MDCT coefficients should remain zero for ESH too.
"""
frame_F_in = np.zeros((128, 8), dtype=np.float64)
frame_F_tns, tns_coeffs = aac_tns(frame_F_in, "ESH")
assert np.isfinite(frame_F_tns).all()
assert np.isfinite(tns_coeffs).all()
assert np.all(frame_F_tns == 0.0)
frame_F_hat = aac_i_tns(frame_F_tns, "ESH", tns_coeffs)
assert np.isfinite(frame_F_hat).all()
assert np.all(frame_F_hat == 0.0)
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_tns_near_silence_is_finite_and_roundtrips(frame_type: FrameType) -> None:
"""
Edge case: extremely small values should not cause NaN/Inf,
and round-trip should remain close.
"""
frame_F_in = (1e-15 * np.ones((1024, 1), dtype=np.float64))
frame_F_tns, tns_coeffs = aac_tns(frame_F_in, frame_type)
assert np.isfinite(frame_F_tns).all()
assert np.isfinite(tns_coeffs).all()
frame_F_hat = aac_i_tns(frame_F_tns, frame_type, tns_coeffs)
assert np.isfinite(frame_F_hat).all()
np.testing.assert_allclose(frame_F_hat, frame_F_in, rtol=1e-6, atol=1e-12)
def test_tns_near_silence_esh_is_finite_and_roundtrips() -> None:
"""
Near-silence test for ESH mode.
"""
frame_F_in = (1e-15 * np.ones((128, 8), dtype=np.float64))
frame_F_tns, tns_coeffs = aac_tns(frame_F_in, "ESH")
assert np.isfinite(frame_F_tns).all()
assert np.isfinite(tns_coeffs).all()
frame_F_hat = aac_i_tns(frame_F_tns, "ESH", tns_coeffs)
assert np.isfinite(frame_F_hat).all()
np.testing.assert_allclose(frame_F_hat, frame_F_in, rtol=1e-6, atol=1e-12)
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_tns_accepts_flat_vector_shape_long(frame_type: FrameType) -> None:
"""
Contract test: for non-ESH, aac_tns must accept input shape (1024,)
in addition to (1024, 1), and preserve the shape convention.
"""
rng = np.random.default_rng(7)
frame_F_in = rng.normal(size=(1024,)).astype(np.float64)
frame_F_out, tns_coeffs = aac_tns(frame_F_in, frame_type)
assert frame_F_out.shape == (1024,)
assert tns_coeffs.shape == (PRED_ORDER, 1)
@pytest.mark.parametrize("frame_type", ["OLS", "LSS", "LPS"])
def test_tns_does_not_explode_peak_or_energy_long(frame_type: FrameType) -> None:
"""
Sanity: TNS should not cause extreme peak/energy blow-up on typical inputs.
This is a loose guard to catch regressions.
"""
rng = np.random.default_rng(8)
frame_F_in = rng.normal(size=(1024, 1)).astype(np.float64)
in_peak = float(np.max(np.abs(frame_F_in)))
in_energy = float(np.sum(frame_F_in * frame_F_in))
frame_F_out, _ = aac_tns(frame_F_in, frame_type)
out_peak = float(np.max(np.abs(frame_F_out)))
out_energy = float(np.sum(frame_F_out * frame_F_out))
peak_ratio = out_peak / (in_peak + EPS)
energy_ratio = out_energy / (in_energy + EPS)
assert peak_ratio < 50.0
assert energy_ratio < 2500.0
def test_tns_does_not_explode_peak_or_energy_esh() -> None:
"""
Sanity: same blow-up guard for ESH mode.
"""
rng = np.random.default_rng(9)
frame_F_in = rng.normal(size=(128, 8)).astype(np.float64)
in_peak = float(np.max(np.abs(frame_F_in)))
in_energy = float(np.sum(frame_F_in * frame_F_in))
frame_F_out, _ = aac_tns(frame_F_in, "ESH")
out_peak = float(np.max(np.abs(frame_F_out)))
out_energy = float(np.sum(frame_F_out * frame_F_out))
peak_ratio = out_peak / (in_peak + EPS)
energy_ratio = out_energy / (in_energy + EPS)
assert peak_ratio < 50.0
assert energy_ratio < 2500.0
+329
View File
@@ -0,0 +1,329 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - SNR dB Tests
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# - Basic tests for SNR calculation utility.
# - Contract and sanity tests for TableB219-related utilities.
#
# These tests do NOT validate the numerical correctness of the
# Bark tables themselves (they are given by the AAC spec),
# but instead ensure:
# - correct loading from disk,
# - correct table selection per frame type,
# - internal consistency of band limits,
# - correct caching behavior.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
import pytest
from core.aac_utils import mdct, imdct, snr_db, load_b219_tables, get_table, band_limits
from core.aac_types import *
tolerance = 1e-10
# mdct / imdct
# ------------------------------------------------------------
def _assert_allclose(a: FloatArray, b: FloatArray, *, rtol: float, atol: float) -> None:
"""
Helper for consistent tolerances across tests.
"""
np.testing.assert_allclose(a, b, rtol=rtol, atol=atol)
def _estimate_gain(y: MdctCoeffs, x: MdctCoeffs) -> float:
"""
Estimate scalar gain g such that y ~= g*x in least-squares sense.
"""
denom = float(np.dot(x, x))
if denom == 0.0:
return 0.0
return float(np.dot(y, x) / denom)
@pytest.mark.parametrize("N", [256, 2048])
def test_mdct_imdct_mdct_identity_up_to_gain(N: int) -> None:
"""
Consistency test in coefficient domain:
mdct(imdct(X)) ~= g * X
For the chosen (non-orthonormal) scaling, g is expected to be close to 2.
"""
rng = np.random.default_rng(0)
K = N // 2
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
x: TimeSignal = imdct(X)
X_hat: MdctCoeffs = mdct(x)
g = _estimate_gain(X_hat, X)
_assert_allclose(X_hat, g * X, rtol=tolerance, atol=tolerance)
_assert_allclose(np.array([g], dtype=np.float64), np.array([2.0], dtype=np.float64), rtol=tolerance, atol=tolerance)
@pytest.mark.parametrize("N", [256, 2048])
def test_mdct_linearity(N: int) -> None:
"""
Linearity test:
mdct(a*x + b*y) == a*mdct(x) + b*mdct(y)
"""
rng = np.random.default_rng(1)
x: TimeSignal = rng.normal(size=N).astype(np.float64)
y: TimeSignal = rng.normal(size=N).astype(np.float64)
a = 0.37
b = -1.12
left: MdctCoeffs = mdct(a * x + b * y)
right: MdctCoeffs = a * mdct(x) + b * mdct(y)
_assert_allclose(left, right, rtol=tolerance, atol=tolerance)
@pytest.mark.parametrize("N", [256, 2048])
def test_imdct_linearity(N: int) -> None:
"""
Linearity test for IMDCT:
imdct(a*X + b*Y) == a*imdct(X) + b*imdct(Y)
"""
rng = np.random.default_rng(2)
K = N // 2
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
Y: MdctCoeffs = rng.normal(size=K).astype(np.float64)
a = -0.5
b = 2.0
left: TimeSignal = imdct(a * X + b * Y)
right: TimeSignal = a * imdct(X) + b * imdct(Y)
_assert_allclose(left, right, rtol=tolerance, atol=tolerance)
@pytest.mark.parametrize("N", [256, 2048])
def test_mdct_imdct_outputs_are_finite(N: int) -> None:
"""
Sanity test: no NaN/inf on random inputs.
"""
rng = np.random.default_rng(3)
K = N // 2
x: TimeSignal = rng.normal(size=N).astype(np.float64)
X: MdctCoeffs = rng.normal(size=K).astype(np.float64)
X1 = mdct(x)
x1 = imdct(X)
assert np.isfinite(X1).all()
assert np.isfinite(x1).all()
# SNR
# ------------------------------------------------------------
def test_snr_perfect_reconstruction_returns_inf() -> None:
"""
If x_hat == x_ref exactly, noise power is zero and SNR must be +inf.
"""
rng = np.random.default_rng(0)
x: StereoSignal = rng.normal(size=(1024, 2)).astype(np.float64)
snr = snr_db(x, x)
assert snr == float("inf")
def test_snr_zero_reference_returns_minus_inf() -> None:
"""
If reference signal is identically zero, signal power is zero
and SNR must be -inf (unless noise is also zero, which is degenerate).
"""
x_ref: StereoSignal = np.zeros((1024, 2), dtype=np.float64)
x_hat: StereoSignal = np.ones((1024, 2), dtype=np.float64)
snr = snr_db(x_ref, x_hat)
assert snr == float("-inf")
def test_snr_known_noise_level_matches_expected_value() -> None:
"""
Deterministic test with known signal and noise power.
Let:
x_ref = ones
x_hat = ones + noise
With noise variance sigma^2, expected SNR:
10 * log10(Ps / Pn)
"""
n = 1000
sigma = 0.1
x_ref: StereoSignal = np.ones((n, 2), dtype=np.float64)
noise = sigma * np.ones((n, 2), dtype=np.float64)
x_hat: StereoSignal = x_ref + noise
ps = float(np.sum(x_ref * x_ref))
pn = float(np.sum(noise * noise))
expected = 10.0 * np.log10(ps / pn)
snr = snr_db(x_ref, x_hat)
assert np.isclose(snr, expected, rtol=1e-12, atol=1e-12)
def test_snr_aligns_different_lengths_and_channels() -> None:
"""
The function must:
- align to minimum length
- align to minimum channel count
without crashing.
"""
rng = np.random.default_rng(1)
x_ref: StereoSignal = rng.normal(size=(1000, 2)).astype(np.float64)
x_hat: StereoSignal = rng.normal(size=(800, 1)).astype(np.float64)
snr = snr_db(x_ref, x_hat)
assert np.isfinite(snr)
def test_snr_accepts_1d_inputs() -> None:
"""
1-D inputs must be accepted and treated as single-channel signals.
"""
rng = np.random.default_rng(2)
x_ref = rng.normal(size=1024).astype(np.float64)
x_hat = x_ref + 0.01 * rng.normal(size=1024).astype(np.float64)
snr = snr_db(x_ref, x_hat)
assert np.isfinite(snr)
# Table219b
# ------------------------------------------------------------
def test_load_b219_tables_returns_expected_keys() -> None:
"""
Contract test:
TableB219.mat must load successfully and expose both tables
required by the psychoacoustic model.
The AAC spec defines:
- B219a: long-frame Bark bands
- B219b: short-frame Bark bands
"""
tables = load_b219_tables()
assert isinstance(tables, dict)
assert "B219a" in tables
assert "B219b" in tables
def test_b219_table_shapes_are_correct() -> None:
"""
Sanity test:
Verify that the Bark tables have the expected number of bands
and sufficient columns.
Expected from AAC spec:
- B219a: 69 bands (long frames)
- B219b: 42 bands (short frames)
- At least 6 columns (as accessed by band_limits()).
"""
tables = load_b219_tables()
B219a = tables["B219a"]
assert B219a.ndim == 2
assert B219a.shape[0] == 69
assert B219a.shape[1] >= 6
B219b = tables["B219b"]
assert B219b.ndim == 2
assert B219b.shape[0] == 42
assert B219b.shape[1] >= 6
def test_get_table_returns_correct_fft_size() -> None:
"""
Interface test:
get_table(frame_type) must return both:
- the correct Bark table
- the correct FFT size N
This mapping is fundamental for the psychoacoustic model.
"""
table_long, N_long = get_table("OLS")
assert N_long == 2048
assert table_long.shape[0] == 69
table_short, N_short = get_table("ESH")
assert N_short == 256
assert table_short.shape[0] == 42
def test_band_limits_are_consistent_for_long_table() -> None:
"""
Sanity test for band limits (long frames):
For each Bark band:
- wlow <= whigh
- frequency indices stay within [0, N/2)
- all returned arrays have consistent lengths
"""
table, N = get_table("OLS")
wlow, whigh, bval, qthr = band_limits(table)
B = table.shape[0]
assert len(wlow) == B
assert len(whigh) == B
assert len(bval) == B
assert len(qthr) == B
for b in range(B):
assert 0 <= wlow[b] <= whigh[b]
assert whigh[b] < N // 2
def test_band_limits_are_consistent_for_short_table() -> None:
"""
Sanity test for band limits (short frames / ESH).
Same invariants as for long frames, but with FFT size N=256.
"""
table, N = get_table("ESH")
wlow, whigh, bval, qthr = band_limits(table)
B = table.shape[0]
assert len(wlow) == B
assert len(whigh) == B
for b in range(B):
assert 0 <= wlow[b] <= whigh[b]
assert whigh[b] < N // 2
def test_b219_tables_are_cached() -> None:
"""
Implementation test:
load_b219_tables() should cache the loaded tables so that
subsequent calls return the same object (identity check).
This avoids repeated disk I/O during psychoacoustic analysis.
"""
t1 = load_b219_tables()
t2 = load_b219_tables()
assert t1 is t2
+261 -20
View File
@@ -26,14 +26,91 @@ from pathlib import Path
from typing import Union from typing import Union
import soundfile as sf import soundfile as sf
from scipy.io import savemat
from core.aac_configuration import WIN_TYPE from core.aac_configuration import WIN_TYPE
from core.aac_filterbank import aac_filter_bank from core.aac_filterbank import aac_filter_bank
from core.aac_ssc import aac_SSC from core.aac_ssc import aac_ssc
from core.aac_tns import aac_tns from core.aac_tns import aac_tns
from core.aac_psycho import aac_psycho
from core.aac_quantizer import aac_quantizer # assumes your quantizer file is core/aac_quantizer.py
from core.aac_huffman import aac_encode_huff
from core.aac_utils import get_table, band_limits
from material.huff_utils import load_LUT
from core.aac_types import * from core.aac_types import *
# -----------------------------------------------------------------------------
# Helpers for thresholds (T(b))
# -----------------------------------------------------------------------------
def _band_slices_from_table(frame_type: FrameType) -> list[tuple[int, int]]:
"""
Return inclusive (lo, hi) band slices derived from TableB219.
"""
table, _ = get_table(frame_type)
wlow, whigh, _bval, _qthr_db = band_limits(table)
return [(int(lo), int(hi)) for lo, hi in zip(wlow, whigh)]
def _thresholds_from_smr(
frame_F_ch: FrameChannelF,
frame_type: FrameType,
SMR: FloatArray,
) -> FloatArray:
"""
Compute thresholds T(b) = P(b) / SMR(b), where P(b) is band energy.
Shapes:
- Long: returns (NB, 1)
- ESH: returns (NB, 8)
"""
bands = _band_slices_from_table(frame_type)
NB = len(bands)
X = np.asarray(frame_F_ch, dtype=np.float64)
SMR = np.asarray(SMR, dtype=np.float64)
if frame_type == "ESH":
if X.shape != (128, 8):
raise ValueError("For ESH, frame_F_ch must have shape (128, 8).")
if SMR.shape != (NB, 8):
raise ValueError(f"For ESH, SMR must have shape ({NB}, 8).")
T = np.zeros((NB, 8), dtype=np.float64)
for j in range(8):
Xj = X[:, j]
for b, (lo, hi) in enumerate(bands):
P = float(np.sum(Xj[lo : hi + 1] ** 2))
smr = float(SMR[b, j])
T[b, j] = 0.0 if smr <= 1e-12 else (P / smr)
return T
# Long
if X.shape == (1024,):
Xv = X
elif X.shape == (1024, 1):
Xv = X[:, 0]
else:
raise ValueError("For non-ESH, frame_F_ch must be shape (1024,) or (1024, 1).")
if SMR.shape == (NB,):
SMRv = SMR
elif SMR.shape == (NB, 1):
SMRv = SMR[:, 0]
else:
raise ValueError(f"For non-ESH, SMR must be shape ({NB},) or ({NB}, 1).")
T = np.zeros((NB, 1), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
P = float(np.sum(Xv[lo : hi + 1] ** 2))
smr = float(SMRv[b])
T[b, 0] = 0.0 if smr <= 1e-12 else (P / smr)
return T
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public helpers (useful for level_x demo wrappers) # Public helpers (useful for level_x demo wrappers)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -122,7 +199,10 @@ def aac_pack_frame_f_to_seq_channels(frame_type: FrameType, frame_f: FrameF) ->
# Level 1 encoder # Level 1 encoder
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1: def aac_coder_1(
filename_in: Union[str, Path],
verbose: bool = False
) -> AACSeq1:
""" """
Level-1 AAC encoder. Level-1 AAC encoder.
@@ -139,6 +219,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
filename_in : Union[str, Path] filename_in : Union[str, Path]
Input WAV filename. Input WAV filename.
Assumption: stereo audio, sampling rate 48 kHz. Assumption: stereo audio, sampling rate 48 kHz.
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -165,8 +247,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
aac_seq: AACSeq1 = [] aac_seq: AACSeq1 = []
prev_frame_type: FrameType = "OLS" prev_frame_type: FrameType = "OLS"
win_type: WinType = WIN_TYPE if verbose:
print("Encoding ", end="", flush=True)
for i in range(K): for i in range(K):
start = i * hop start = i * hop
@@ -182,24 +264,32 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64) tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail]) next_t = np.vstack([next_t, tail])
frame_type = aac_SSC(frame_t, next_t, prev_frame_type) frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
frame_f = aac_filter_bank(frame_t, frame_type, win_type) frame_f = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f) chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f)
aac_seq.append({ aac_seq.append({
"frame_type": frame_type, "frame_type": frame_type,
"win_type": win_type, "win_type": WIN_TYPE,
"chl": {"frame_F": chl_f}, "chl": {"frame_F": chl_f},
"chr": {"frame_F": chr_f}, "chr": {"frame_F": chr_f},
}) })
prev_frame_type = frame_type prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
return aac_seq return aac_seq
def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2: def aac_coder_2(
filename_in: Union[str, Path],
verbose: bool = False
) -> AACSeq2:
""" """
Level-2 AAC encoder (Level 1 + TNS). Level-2 AAC encoder (Level 1 + TNS).
@@ -207,6 +297,8 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
---------- ----------
filename_in : Union[str, Path] filename_in : Union[str, Path]
Input WAV filename (stereo, 48 kHz). Input WAV filename (stereo, 48 kHz).
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -238,6 +330,8 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
aac_seq: AACSeq2 = [] aac_seq: AACSeq2 = []
prev_frame_type: FrameType = "OLS" prev_frame_type: FrameType = "OLS"
if verbose:
print("Encoding ", end="", flush=True)
for i in range(K): for i in range(K):
start = i * hop start = i * hop
@@ -250,21 +344,12 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64) tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail]) next_t = np.vstack([next_t, tail])
frame_type = aac_SSC(frame_t, next_t, prev_frame_type) frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
# Level 1 analysis (packed stereo container) # Level 1 analysis (packed stereo container)
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE) frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
# Unpack to per-channel (as you already do in Level 1) chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f_stereo)
if frame_type == "ESH":
chl_f = np.empty((128, 8), dtype=np.float64)
chr_f = np.empty((128, 8), dtype=np.float64)
for j in range(8):
chl_f[:, j] = frame_f_stereo[:, 2 * j + 0]
chr_f[:, j] = frame_f_stereo[:, 2 * j + 1]
else:
chl_f = frame_f_stereo[:, 0:1].astype(np.float64, copy=False)
chr_f = frame_f_stereo[:, 1:2].astype(np.float64, copy=False)
# Level 2: apply TNS per channel # Level 2: apply TNS per channel
chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type) chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type)
@@ -278,7 +363,163 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
"chr": {"frame_F": chr_f_tns, "tns_coeffs": chr_tns_coeffs}, "chr": {"frame_F": chr_f_tns, "tns_coeffs": chr_tns_coeffs},
} }
) )
prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
return aac_seq
def aac_coder_3(
filename_in: Union[str, Path],
filename_aac_coded: Union[str, Path] | None = None,
verbose: bool = False,
) -> AACSeq3:
"""
Level-3 AAC encoder (Level 2 + Psycho + Quantizer + Huffman).
Parameters
----------
filename_in : Union[str, Path]
Input WAV filename (stereo, 48 kHz).
filename_aac_coded : Union[str, Path] | None
Optional .mat filename to store aac_seq_3 (assignment convenience).
verbose : bool
Optional argument to print encoding status
Returns
-------
AACSeq3
Encoded AAC sequence (Level 3 payload schema).
"""
filename_in = Path(filename_in)
x, _ = aac_read_wav_stereo_48k(filename_in)
hop = 1024
win = 2048
pad_pre = np.zeros((hop, 2), dtype=np.float64)
pad_post = np.zeros((hop, 2), dtype=np.float64)
x_pad = np.vstack([pad_pre, x, pad_post])
K = int((x_pad.shape[0] - win) // hop + 1)
if K <= 0:
raise ValueError("Input too short for framing.")
# Load Huffman LUTs once.
huff_LUT_list = load_LUT()
aac_seq: AACSeq3 = []
prev_frame_type: FrameType = "OLS"
# Psycho model needs per-channel history (prev1, prev2) of 2048-sample frames.
prev1_L = np.zeros((2048,), dtype=np.float64)
prev2_L = np.zeros((2048,), dtype=np.float64)
prev1_R = np.zeros((2048,), dtype=np.float64)
prev2_R = np.zeros((2048,), dtype=np.float64)
if verbose:
print("Encoding ", end="", flush=True)
for i in range(K):
start = i * hop
frame_t: FrameT = x_pad[start : start + win, :]
if frame_t.shape != (win, 2):
raise ValueError("Internal framing error: frame_t has wrong shape.")
next_t = x_pad[start + hop : start + hop + win, :]
if next_t.shape[0] < win:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail])
frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
# Analysis filterbank (stereo packed)
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f_stereo)
# TNS per channel
chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type)
chr_f_tns, chr_tns_coeffs = aac_tns(chr_f, frame_type)
# Psychoacoustic model per channel (time-domain)
frame_L = np.asarray(frame_t[:, 0], dtype=np.float64)
frame_R = np.asarray(frame_t[:, 1], dtype=np.float64)
SMR_L = aac_psycho(frame_L, frame_type, prev1_L, prev2_L)
SMR_R = aac_psycho(frame_R, frame_type, prev1_R, prev2_R)
# Thresholds T(b) (stored, not entropy-coded)
T_L = _thresholds_from_smr(chl_f_tns, frame_type, SMR_L)
T_R = _thresholds_from_smr(chr_f_tns, frame_type, SMR_R)
# Quantizer per channel
S_L, sfc_L, G_L = aac_quantizer(chl_f_tns, frame_type, SMR_L)
S_R, sfc_R, G_R = aac_quantizer(chr_f_tns, frame_type, SMR_R)
# Huffman-code ONLY the DPCM differences for b>0.
# sfc[0] corresponds to alpha(0)=G and is stored separately in the frame.
sfc_L_dpcm = np.asarray(sfc_L, dtype=np.int64)[1:, ...]
sfc_R_dpcm = np.asarray(sfc_R, dtype=np.int64)[1:, ...]
# sfc_L_stream, cb_sfc_L = aac_encode_huff(sfc_L_dpcm.reshape(-1, order="F"), huff_LUT_list, force_codebook=11)
# sfc_R_stream, cb_sfc_R = aac_encode_huff(sfc_R_dpcm.reshape(-1, order="F"), huff_LUT_list, force_codebook=11)
sfc_L_stream, cb_sfc_L = aac_encode_huff(sfc_L_dpcm.reshape(-1, order="F"), huff_LUT_list)
sfc_R_stream, cb_sfc_R = aac_encode_huff(sfc_R_dpcm.reshape(-1, order="F"), huff_LUT_list)
if cb_sfc_L != 11 or cb_sfc_R != 11:
raise ValueError(f"Illegal codebook value for frame: {i}: cb_sfc_l={cb_sfc_L}, cb_sfc_r={cb_sfc_R}.")
mdct_L_stream, cb_L = aac_encode_huff(np.asarray(S_L, dtype=np.int64).reshape(-1), huff_LUT_list)
mdct_R_stream, cb_R = aac_encode_huff(np.asarray(S_R, dtype=np.int64).reshape(-1), huff_LUT_list)
# Typed dict construction helps static analyzers validate the schema.
frame_out: AACSeq3Frame = {
"frame_type": frame_type,
"win_type": WIN_TYPE,
"chl": {
"tns_coeffs": np.asarray(chl_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_L, dtype=np.float64),
"G": G_L,
"sfc": sfc_L_stream,
"stream": mdct_L_stream,
"codebook": int(cb_L),
},
"chr": {
"tns_coeffs": np.asarray(chr_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_R, dtype=np.float64),
"G": G_R,
"sfc": sfc_R_stream,
"stream": mdct_R_stream,
"codebook": int(cb_R),
},
}
aac_seq.append(frame_out)
# Update psycho history (shift register)
prev2_L = prev1_L
prev1_L = frame_L
prev2_R = prev1_R
prev1_R = frame_R
prev_frame_type = frame_type prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
# Optional: store to .mat for the assignment wrapper
if filename_aac_coded is not None:
filename_aac_coded = Path(filename_aac_coded)
savemat(
str(filename_aac_coded),
{"aac_seq_3": np.array(aac_seq, dtype=object)},
do_compression=True,
)
return aac_seq
return aac_seq
+11 -1
View File
@@ -15,6 +15,8 @@
from __future__ import annotations from __future__ import annotations
# Imports # Imports
from typing import Final
from core.aac_types import WinType from core.aac_types import WinType
# Filterbank # Filterbank
@@ -28,4 +30,12 @@ WIN_TYPE: WinType = "SIN"
# ------------------------------------------------------------ # ------------------------------------------------------------
PRED_ORDER = 4 PRED_ORDER = 4
QUANT_STEP = 0.1 QUANT_STEP = 0.1
QUANT_MAX = 0.7 # 4-bit symmetric with step 0.1 -> clamp to [-0.7, +0.7] QUANT_MAX = 0.7 # 4-bit symmetric with step 0.1 -> clamp to [-0.7, +0.7]
# -----------------------------------------------------------------------------
# Psycho
# -----------------------------------------------------------------------------
NMT_DB: Final[float] = 6.0 # Noise Masking Tone (dB)
TMN_DB: Final[float] = 18.0 # Tone Masking Noise (dB)
+205 -17
View File
@@ -9,16 +9,9 @@
# cchoutou@ece.auth.gr # cchoutou@ece.auth.gr
# #
# Description: # Description:
# Level 1 AAC decoder orchestration (inverse of aac_coder_1()). # - Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
# Keeps the same functional behavior as the original level_1 implementation: # - Level 2 AAC decoder orchestration (inverse of aac_coder_1()).
# - Re-pack per-channel spectra into FrameF expected by aac_i_filter_bank()
# - IMDCT synthesis per frame
# - Overlap-add with hop=1024
# - Remove encoder boundary padding: hop at start and hop at end
# #
# Note:
# This core module returns the reconstructed samples. Writing to disk is kept
# in level_x demos.
# ------------------------------------------------------------ # ------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
@@ -29,14 +22,27 @@ import soundfile as sf
from core.aac_filterbank import aac_i_filter_bank from core.aac_filterbank import aac_i_filter_bank
from core.aac_tns import aac_i_tns from core.aac_tns import aac_i_tns
from core.aac_quantizer import aac_i_quantizer
from core.aac_huffman import aac_decode_huff
from core.aac_utils import get_table, band_limits
from material.huff_utils import load_LUT
from core.aac_types import * from core.aac_types import *
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public helpers (useful for level_x demo wrappers) # Helper for NB
# -----------------------------------------------------------------------------
def _nbands(frame_type: FrameType) -> int:
table, _ = get_table(frame_type)
wlow, _whigh, _bval, _qthr_db = band_limits(table)
return int(len(wlow))
# -----------------------------------------------------------------------------
# Public helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_unpack_seq_channels_to_frame_f(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF: def aac_unpack_seq_channels(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF:
""" """
Re-pack per-channel spectra from the Level-1 AACSeq1 schema into the stereo Re-pack per-channel spectra from the Level-1 AACSeq1 schema into the stereo
FrameF container expected by aac_i_filter_bank(). FrameF container expected by aac_i_filter_bank().
@@ -109,10 +115,14 @@ def aac_remove_padding(y_pad: StereoSignal, hop: int = 1024) -> StereoSignal:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Level 1 decoder (core) # Level 1 decoder
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoSignal: def aac_decoder_1(
aac_seq_1: AACSeq1,
filename_out: Union[str, Path],
verbose: bool = False
) -> StereoSignal:
""" """
Level-1 AAC decoder (inverse of aac_coder_1()). Level-1 AAC decoder (inverse of aac_coder_1()).
@@ -128,6 +138,8 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
Encoded sequence as produced by aac_coder_1(). Encoded sequence as produced by aac_coder_1().
filename_out : Union[str, Path] filename_out : Union[str, Path]
Output WAV filename. Assumption: 48 kHz, stereo. Output WAV filename. Assumption: 48 kHz, stereo.
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -146,6 +158,8 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
n_pad = (K - 1) * hop + win n_pad = (K - 1) * hop + win
y_pad: StereoSignal = np.zeros((n_pad, 2), dtype=np.float64) y_pad: StereoSignal = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_1): for i, fr in enumerate(aac_seq_1):
frame_type: FrameType = fr["frame_type"] frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"] win_type: WinType = fr["win_type"]
@@ -153,21 +167,32 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
chl_f = np.asarray(fr["chl"]["frame_F"], dtype=np.float64) chl_f = np.asarray(fr["chl"]["frame_F"], dtype=np.float64)
chr_f = np.asarray(fr["chr"]["frame_F"], dtype=np.float64) chr_f = np.asarray(fr["chr"]["frame_F"], dtype=np.float64)
frame_f: FrameF = aac_unpack_seq_channels_to_frame_f(frame_type, chl_f, chr_f) frame_f: FrameF = aac_unpack_seq_channels(frame_type, chl_f, chr_f)
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type) # (2048, 2) frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type) # (2048, 2)
start = i * hop start = i * hop
y_pad[start:start + win, :] += frame_t_hat y_pad[start:start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y: StereoSignal = aac_remove_padding(y_pad, hop=hop) y: StereoSignal = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
# Level 1 assumption: 48 kHz output. # Level 1 assumption: 48 kHz output.
sf.write(str(filename_out), y, 48000) sf.write(str(filename_out), y, 48000)
return y return y
def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoSignal: # -----------------------------------------------------------------------------
# Level 2 decoder
# -----------------------------------------------------------------------------
def aac_decoder_2(
aac_seq_2: AACSeq2,
filename_out: Union[str, Path],
verbose: bool = False
) -> StereoSignal:
""" """
Level-2 AAC decoder (inverse of aac_coder_2). Level-2 AAC decoder (inverse of aac_coder_2).
@@ -185,6 +210,8 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
Encoded sequence as produced by aac_coder_2(). Encoded sequence as produced by aac_coder_2().
filename_out : Union[str, Path] filename_out : Union[str, Path]
Output WAV filename. Output WAV filename.
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -203,6 +230,8 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
n_pad = (K - 1) * hop + win n_pad = (K - 1) * hop + win
y_pad = np.zeros((n_pad, 2), dtype=np.float64) y_pad = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_2): for i, fr in enumerate(aac_seq_2):
frame_type: FrameType = fr["frame_type"] frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"] win_type: WinType = fr["win_type"]
@@ -250,8 +279,167 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
start = i * hop start = i * hop
y_pad[start : start + win, :] += frame_t_hat y_pad[start : start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y = aac_remove_padding(y_pad, hop=hop) y = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
sf.write(str(filename_out), y, 48000) sf.write(str(filename_out), y, 48000)
return y return y
def aac_decoder_3(
aac_seq_3: AACSeq3,
filename_out: Union[str, Path],
verbose: bool = False,
) -> StereoSignal:
"""
Level-3 AAC decoder (inverse of aac_coder_3).
Steps per frame:
- Huffman decode scalefactors (sfc) using codebook 11
- Huffman decode MDCT symbols (stream) using stored codebook
- iQuantizer -> MDCT coefficients after TNS
- iTNS using stored predictor coefficients
- IMDCT filterbank -> time domain
- Overlap-add, remove padding, write WAV
Parameters
----------
aac_seq_3 : AACSeq3
Encoded sequence as produced by aac_coder_3.
filename_out : Union[str, Path]
Output WAV filename.
verbose : bool
Optional argument to print encoding status
Returns
-------
StereoSignal
Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64.
"""
filename_out = Path(filename_out)
hop = 1024
win = 2048
K = len(aac_seq_3)
if K <= 0:
raise ValueError("aac_seq_3 must contain at least one frame.")
# Load Huffman LUTs once.
huff_LUT_list = load_LUT()
n_pad = (K - 1) * hop + win
y_pad = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_3):
frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"]
NB = _nbands(frame_type)
# We store G separately, so Huffman stream contains only (NB-1) DPCM differences.
sfc_len = (NB - 1) * (8 if frame_type == "ESH" else 1)
# -------------------------
# Left channel
# -------------------------
tns_L = np.asarray(fr["chl"]["tns_coeffs"], dtype=np.float64)
G_L = fr["chl"]["G"]
sfc_bits_L = fr["chl"]["sfc"]
mdct_bits_L = fr["chl"]["stream"]
cb_L = int(fr["chl"]["codebook"])
sfc_dec_L = aac_decode_huff(sfc_bits_L, 11, huff_LUT_list)[:sfc_len].astype(np.int64, copy=False)
if frame_type == "ESH":
sfc_dpcm_L = sfc_dec_L.reshape(NB - 1, 8, order="F")
sfc_L = np.zeros((NB, 8), dtype=np.int64)
Gv = np.asarray(G_L, dtype=np.float64).reshape(1, 8)
sfc_L[0, :] = Gv[0, :].astype(np.int64)
sfc_L[1:, :] = sfc_dpcm_L
else:
sfc_dpcm_L = sfc_dec_L.reshape(NB - 1, 1, order="F")
sfc_L = np.zeros((NB, 1), dtype=np.int64)
sfc_L[0, 0] = int(float(G_L))
sfc_L[1:, :] = sfc_dpcm_L
# MDCT symbols: codebook 0 means "all-zero section"
if cb_L == 0:
S_dec_L = np.zeros((1024,), dtype=np.int64)
else:
S_tmp_L = aac_decode_huff(mdct_bits_L, cb_L, huff_LUT_list).astype(np.int64, copy=False)
# Tuple coding may produce extra trailing symbols; caller knows the true length (1024).
# Also guard against short outputs by zero-padding.
if S_tmp_L.size < 1024:
S_dec_L = np.zeros((1024,), dtype=np.int64)
S_dec_L[: S_tmp_L.size] = S_tmp_L
else:
S_dec_L = S_tmp_L[:1024]
S_L = S_dec_L.reshape(1024, 1)
Xq_L = aac_i_quantizer(S_L, sfc_L, G_L, frame_type)
X_L = aac_i_tns(Xq_L, frame_type, tns_L)
# -------------------------
# Right channel
# -------------------------
tns_R = np.asarray(fr["chr"]["tns_coeffs"], dtype=np.float64)
G_R = fr["chr"]["G"]
sfc_bits_R = fr["chr"]["sfc"]
mdct_bits_R = fr["chr"]["stream"]
cb_R = int(fr["chr"]["codebook"])
sfc_dec_R = aac_decode_huff(sfc_bits_R, 11, huff_LUT_list)[:sfc_len].astype(np.int64, copy=False)
if frame_type == "ESH":
sfc_dpcm_R = sfc_dec_R.reshape(NB - 1, 8, order="F")
sfc_R = np.zeros((NB, 8), dtype=np.int64)
Gv = np.asarray(G_R, dtype=np.float64).reshape(1, 8)
sfc_R[0, :] = Gv[0, :].astype(np.int64)
sfc_R[1:, :] = sfc_dpcm_R
else:
sfc_dpcm_R = sfc_dec_R.reshape(NB - 1, 1, order="F")
sfc_R = np.zeros((NB, 1), dtype=np.int64)
sfc_R[0, 0] = int(float(G_R))
sfc_R[1:, :] = sfc_dpcm_R
if cb_R == 0:
S_dec_R = np.zeros((1024,), dtype=np.int64)
else:
S_tmp_R = aac_decode_huff(mdct_bits_R, cb_R, huff_LUT_list).astype(np.int64, copy=False)
if S_tmp_R.size < 1024:
S_dec_R = np.zeros((1024,), dtype=np.int64)
S_dec_R[: S_tmp_R.size] = S_tmp_R
else:
S_dec_R = S_tmp_R[:1024]
S_R = S_dec_R.reshape(1024, 1)
Xq_R = aac_i_quantizer(S_R, sfc_R, G_R, frame_type)
X_R = aac_i_tns(Xq_R, frame_type, tns_R)
# Re-pack to stereo container and inverse filterbank
frame_f = aac_unpack_seq_channels(frame_type, np.asarray(X_L), np.asarray(X_R))
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type)
start = i * hop
y_pad[start : start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
sf.write(str(filename_out), y, 48000)
return y
+8 -75
View File
@@ -14,6 +14,7 @@
# ------------------------------------------------------------ # ------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from core.aac_utils import mdct, imdct
from core.aac_types import * from core.aac_types import *
from scipy.signal.windows import kaiser from scipy.signal.windows import kaiser
@@ -186,74 +187,6 @@ def _window_sequence(frame_type: FrameType, win_type: WinType) -> Window:
raise ValueError(f"Invalid frame_type for long window sequence: {frame_type!r}") raise ValueError(f"Invalid frame_type for long window sequence: {frame_type!r}")
def _mdct(s: TimeSignal) -> MdctCoeffs:
"""
MDCT (direct form) as specified in the assignment.
Parameters
----------
s : TimeSignal
Windowed time samples, 1-D array of length N (N = 2048 or 256).
Returns
-------
MdctCoeffs
MDCT coefficients, 1-D array of length N/2.
Definition
----------
X[k] = 2 * sum_{n=0..N-1} s[n] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
s = np.asarray(s, dtype=np.float64).reshape(-1)
N = int(s.shape[0])
if N not in (2048, 256):
raise ValueError("MDCT input length must be 2048 or 256.")
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(N // 2, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, N/2)
X = 2.0 * (s @ C) # (N/2,)
return X
def _imdct(X: MdctCoeffs) -> TimeSignal:
"""
IMDCT (direct form) as specified in the assignment.
Parameters
----------
X : MdctCoeffs
MDCT coefficients, 1-D array of length K (K = 1024 or 128).
Returns
-------
TimeSignal
Reconstructed time samples, 1-D array of length N = 2K.
Definition
----------
s[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
X = np.asarray(X, dtype=np.float64).reshape(-1)
K = int(X.shape[0])
if K not in (1024, 128):
raise ValueError("IMDCT input length must be 1024 or 128.")
N = 2 * K
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(K, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, K)
s = (2.0 / N) * (C @ X) # (N,)
return s
def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameChannelF: def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameChannelF:
""" """
ESH analysis for one channel. ESH analysis for one channel.
@@ -279,7 +212,7 @@ def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameCha
for j in range(8): for j in range(8):
start = 448 + 128 * j start = 448 + 128 * j
seg = x_ch[start:start + 256] * wS # (256,) seg = x_ch[start:start + 256] * wS # (256,)
X_esh[:, j] = _mdct(seg) # (128,) X_esh[:, j] = mdct(seg) # (128,)
return X_esh return X_esh
@@ -344,7 +277,7 @@ def _i_filter_bank_esh_channel(X_esh: FrameChannelF, win_type: WinType) -> Frame
# Each short IMDCT returns 256 samples. Place them at: # Each short IMDCT returns 256 samples. Place them at:
# start = 448 + 128*j, j=0..7 (50% overlap) # start = 448 + 128*j, j=0..7 (50% overlap)
for j in range(8): for j in range(8):
seg = _imdct(X_esh[:, j]) * wS # (256,) seg = imdct(X_esh[:, j]) * wS # (256,)
start = 448 + 128 * j start = 448 + 128 * j
out[start:start + 256] += seg out[start:start + 256] += seg
@@ -352,7 +285,7 @@ def _i_filter_bank_esh_channel(X_esh: FrameChannelF, win_type: WinType) -> Frame
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public Function prototypes (Level 1) # Public Function prototypes
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -> FrameF: def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -> FrameF:
@@ -385,8 +318,8 @@ def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -
if frame_type in ("OLS", "LSS", "LPS"): if frame_type in ("OLS", "LSS", "LPS"):
w = _window_sequence(frame_type, win_type) # length 2048 w = _window_sequence(frame_type, win_type) # length 2048
XL = _mdct(xL * w) # length 1024 XL = mdct(xL * w) # length 1024
XR = _mdct(xR * w) # length 1024 XR = mdct(xR * w) # length 1024
out = np.empty((1024, 2), dtype=np.float64) out = np.empty((1024, 2), dtype=np.float64)
out[:, 0] = XL out[:, 0] = XL
out[:, 1] = XR out[:, 1] = XR
@@ -430,8 +363,8 @@ def aac_i_filter_bank(frame_F: FrameF, frame_type: FrameType, win_type: WinType)
w = _window_sequence(frame_type, win_type) w = _window_sequence(frame_type, win_type)
xL = _imdct(frame_F[:, 0]) * w xL = imdct(frame_F[:, 0]) * w
xR = _imdct(frame_F[:, 1]) * w xR = imdct(frame_F[:, 1]) * w
out = np.empty((2048, 2), dtype=np.float64) out = np.empty((2048, 2), dtype=np.float64)
out[:, 0] = xL out[:, 0] = xL
+112
View File
@@ -0,0 +1,112 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Huffman wrappers (Level 3)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Thin wrappers around the provided Huffman utilities (material/huff_utils.py)
# so that the API matches the assignment text.
#
# Exposed API (assignment):
# huff_sec, huff_codebook = aac_encode_huff(coeff_sec, huff_LUT_list, force_codebook)
# dec_coeffs = aac_decode_huff(huff_sec, huff_codebook, huff_LUT_list)
#
# Notes:
# - Huffman coding operates on tuples. Therefore, decode(encode(x)) may return
# extra trailing symbols due to tuple padding. The AAC decoder knows the
# true section length from side information (band limits) and truncates.
# ------------------------------------------------------------
from __future__ import annotations
from typing import Any
import numpy as np
from material.huff_utils import encode_huff, decode_huff
def aac_encode_huff(
coeff_sec: np.ndarray,
huff_LUT_list: list[dict[str, Any]],
force_codebook: int | None = None,
) -> tuple[str, int]:
"""
Huffman-encode a section of coefficients (MDCT symbols or scalefactors).
Parameters
----------
coeff_sec : np.ndarray
Coefficient section to be encoded. Any shape is accepted; the input
is flattened and treated as a 1-D sequence of int64 symbols.
huff_LUT_list : list[dict[str, Any]]
List of Huffman Look-Up Tables (LUTs) as returned by material.load_LUT().
Index corresponds to codebook id (typically 1..11, with 0 reserved).
force_codebook : int | None
If provided, forces the use of this Huffman codebook. In the assignment,
scalefactors are encoded with codebook 11. For MDCT coefficients, this
argument is usually omitted (auto-selection).
Returns
-------
tuple[str, int]
(huff_sec, huff_codebook)
- huff_sec: bitstream as a string of '0'/'1'
- huff_codebook: codebook id used by the encoder
"""
coeff_sec_arr = np.asarray(coeff_sec, dtype=np.int64).reshape(-1)
if force_codebook is None:
# Provided utility returns (bitstream, codebook) in the auto-selection case.
huff_sec, huff_codebook = encode_huff(coeff_sec_arr, huff_LUT_list)
return str(huff_sec), int(huff_codebook)
# Provided utility returns ONLY the bitstream when force_codebook is set.
cb = int(force_codebook)
huff_sec = encode_huff(coeff_sec_arr, huff_LUT_list, force_codebook=cb)
return str(huff_sec), cb
def aac_decode_huff(
huff_sec: str | np.ndarray,
huff_codebook: int,
huff_LUT: list[dict[str, Any]],
) -> np.ndarray:
"""
Huffman-decode a bitstream using the specified codebook.
Parameters
----------
huff_sec : str | np.ndarray
Huffman bitstream. Typically a string of '0'/'1'. If an array is provided,
it is passed through to the provided decoder.
huff_codebook : int
Codebook id that was returned by aac_encode_huff.
Codebook 0 represents an all-zero section.
huff_LUT : list[dict[str, Any]]
Huffman LUT list as returned by material.load_LUT().
Returns
-------
np.ndarray
Decoded coefficients as a 1-D np.int64 array.
Note: Due to tuple coding, the decoded array may contain extra trailing
padding symbols. The caller must truncate to the known section length.
"""
cb = int(huff_codebook)
if cb == 0:
# Codebook 0 represents an all-zero section. The decoded length is not
# recoverable from the bitstream alone; the caller must expand/truncate.
return np.zeros((0,), dtype=np.int64)
if cb < 0 or cb >= len(huff_LUT):
raise ValueError(f"Invalid Huffman codebook index: {cb}")
lut = huff_LUT[cb]
dec = decode_huff(huff_sec, lut)
return np.asarray(dec, dtype=np.int64).reshape(-1)
+441
View File
@@ -0,0 +1,441 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Psychoacoustic Model
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Psychoacoustic model for ONE channel, based on the assignment notes (Section 2.4).
#
# Public API:
# SMR = aac_psycho(frame_T, frame_type, frame_T_prev_1, frame_T_prev_2)
#
# Output:
# - For long frames ("OLS", "LSS", "LPS"): SMR has shape (69,)
# - For short frames ("ESH"): SMR has shape (42, 8) (one column per subframe)
#
# Notes:
# - Uses Bark band tables from material/TableB219.mat:
# * B219a for long windows (69 bands, N=2048 FFT, N/2=1024 bins)
# * B219b for short windows (42 bands, N=256 FFT, N/2=128 bins)
# - Applies a Hann window in time domain before FFT magnitude/phase extraction.
# - Implements:
# spreading function -> band spreading -> tonality index -> masking thresholds -> SMR.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from core.aac_utils import band_limits, get_table
from core.aac_configuration import NMT_DB, TMN_DB
from core.aac_types import *
# -----------------------------------------------------------------------------
# Spreading function
# -----------------------------------------------------------------------------
def _spreading_matrix(bval: BandValueArray) -> FloatArray:
"""
Compute the spreading function matrix between psychoacoustic bands.
The spreading function describes how energy in one critical band masks
nearby bands. The formula follows the assignment pseudo-code.
Parameters
----------
bval : BandValueArray
Bark value per band, shape (B,).
Returns
-------
FloatArray
Spreading matrix S of shape (B, B), where:
S[bb, b] quantifies the contribution of band bb masking band b.
"""
bval = np.asarray(bval, dtype=np.float64).reshape(-1)
B = int(bval.shape[0])
spread = np.zeros((B, B), dtype=np.float64)
for b in range(B):
for bb in range(B):
# tmpx depends on direction (asymmetric spreading)
if bb >= b:
tmpx = 3.0 * (bval[bb] - bval[b])
else:
tmpx = 1.5 * (bval[bb] - bval[b])
# tmpz uses the "min(..., 0)" nonlinearity exactly as in the notes
tmpz = 8.0 * min((tmpx - 0.5) ** 2 - 2.0 * (tmpx - 0.5), 0.0)
tmpy = 15.811389 + 7.5 * (tmpx + 0.474) - 17.5 * np.sqrt(1.0 + (tmpx + 0.474) ** 2)
# Clamp very small values (below -100 dB) to 0 contribution
if tmpy < -100.0:
spread[bb, b] = 0.0
else:
spread[bb, b] = 10.0 ** ((tmpz + tmpy) / 10.0)
return spread
# -----------------------------------------------------------------------------
# Windowing + FFT feature extraction
# -----------------------------------------------------------------------------
def _hann_window(N: int) -> FloatArray:
"""
Hann window as specified in the notes:
w[n] = 0.5 - 0.5*cos(2*pi*(n + 0.5)/N)
Parameters
----------
N : int
Window length.
Returns
-------
FloatArray
1-D array of shape (N,), dtype float64.
"""
n = np.arange(N, dtype=np.float64)
return 0.5 - 0.5 * np.cos((2.0 * np.pi / N) * (n + 0.5))
def _r_phi_from_time(x: FrameChannelT, N: int) -> tuple[FloatArray, FloatArray]:
"""
Compute FFT magnitude r(w) and phase phi(w) for bins w = 0 .. N/2-1.
Processing:
1) Apply Hann window in time domain.
2) Compute N-point FFT.
3) Keep only the positive-frequency bins [0 .. N/2-1].
Parameters
----------
x : FrameChannelT
Time-domain samples, shape (N,).
N : int
FFT size (2048 or 256).
Returns
-------
r : FloatArray
Magnitude spectrum for bins 0 .. N/2-1, shape (N/2,).
phi : FloatArray
Phase spectrum for bins 0 .. N/2-1, shape (N/2,).
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
if x.shape[0] != N:
raise ValueError(f"Expected time vector of length {N}, got {x.shape[0]}.")
w = _hann_window(N)
X = np.fft.fft(x * w, n=N)
Xp = X[: N // 2]
r = np.abs(Xp).astype(np.float64, copy=False)
phi = np.angle(Xp).astype(np.float64, copy=False)
return r, phi
def _predictability(
r: FloatArray,
phi: FloatArray,
r_m1: FloatArray,
phi_m1: FloatArray,
r_m2: FloatArray,
phi_m2: FloatArray,
) -> FloatArray:
"""
Compute predictability c(w) per spectral bin.
The notes define:
r_pred(w) = 2*r_{-1}(w) - r_{-2}(w)
phi_pred(w) = 2*phi_{-1}(w) - phi_{-2}(w)
c(w) = |X(w) - X_pred(w)| / (r(w) + |r_pred(w)|)
where X(w) is represented in polar form using r(w), phi(w).
Parameters
----------
r, phi : FloatArray
Current magnitude and phase, shape (N/2,).
r_m1, phi_m1 : FloatArray
Previous magnitude and phase, shape (N/2,).
r_m2, phi_m2 : FloatArray
Pre-previous magnitude and phase, shape (N/2,).
Returns
-------
FloatArray
Predictability c(w), shape (N/2,).
"""
r_pred = 2.0 * r_m1 - r_m2
phi_pred = 2.0 * phi_m1 - phi_m2
num = np.sqrt(
(r * np.cos(phi) - r_pred * np.cos(phi_pred)) ** 2
+ (r * np.sin(phi) - r_pred * np.sin(phi_pred)) ** 2
)
den = r + np.abs(r_pred) + 1e-12 # avoid division-by-zero without altering behavior
return (num / den).astype(np.float64, copy=False)
# -----------------------------------------------------------------------------
# Band-domain aggregation
# -----------------------------------------------------------------------------
def _band_energy_and_pred(
r: FloatArray,
c: FloatArray,
wlow: BandIndexArray,
whigh: BandIndexArray,
) -> tuple[FloatArray, FloatArray]:
"""
Aggregate spectral bin quantities into psychoacoustic bands.
Definitions (notes):
e(b) = sum_{w=wlow(b)..whigh(b)} r(w)^2
c_num(b) = sum_{w=wlow(b)..whigh(b)} c(w) * r(w)^2
The band predictability c(b) is later computed after spreading as:
cb(b) = ct(b) / ecb(b)
Parameters
----------
r : FloatArray
Magnitude spectrum, shape (N/2,).
c : FloatArray
Predictability per bin, shape (N/2,).
wlow, whigh : BandIndexArray
Band limits (inclusive indices), shape (B,).
Returns
-------
e_b : FloatArray
Band energies e(b), shape (B,).
c_num_b : FloatArray
Weighted predictability numerators c_num(b), shape (B,).
"""
r2 = (r * r).astype(np.float64, copy=False)
B = int(wlow.shape[0])
e_b = np.zeros(B, dtype=np.float64)
c_num_b = np.zeros(B, dtype=np.float64)
for b in range(B):
a = int(wlow[b])
z = int(whigh[b])
seg_r2 = r2[a : z + 1]
e_b[b] = float(np.sum(seg_r2))
c_num_b[b] = float(np.sum(c[a : z + 1] * seg_r2))
return e_b, c_num_b
def _psycho_window(
time_x: FrameChannelT,
prev1_x: FrameChannelT,
prev2_x: FrameChannelT,
*,
N: int,
table: BarkTable,
) -> FloatArray:
"""
Compute SMR for one FFT analysis window (N=2048 for long, N=256 for short).
This implements the pipeline described in the notes:
- FFT magnitude/phase
- predictability per bin
- band energies and predictability
- band spreading
- tonality index tb(b)
- masking threshold (noise + threshold in quiet)
- SMR(b) = e(b) / np(b)
Parameters
----------
time_x : FrameChannelT
Current time-domain samples, shape (N,).
prev1_x : FrameChannelT
Previous time-domain samples, shape (N,).
prev2_x : FrameChannelT
Pre-previous time-domain samples, shape (N,).
N : int
FFT size.
table : BarkTable
Psychoacoustic band table (B219a or B219b).
Returns
-------
FloatArray
SMR per band, shape (B,).
"""
wlow, whigh, bval, qthr_db = band_limits(table)
spread = _spreading_matrix(bval)
# FFT features for current and history windows
r, phi = _r_phi_from_time(time_x, N)
r_m1, phi_m1 = _r_phi_from_time(prev1_x, N)
r_m2, phi_m2 = _r_phi_from_time(prev2_x, N)
# Predictability per bin
c_w = _predictability(r, phi, r_m1, phi_m1, r_m2, phi_m2)
# Aggregate into psycho bands
e_b, c_num_b = _band_energy_and_pred(r, c_w, wlow, whigh)
# Spread energies and predictability across bands:
# ecb(b) = sum_bb e(bb) * S(bb, b)
# ct(b) = sum_bb c_num(bb) * S(bb, b)
ecb = spread.T @ e_b
ct = spread.T @ c_num_b
# Band predictability after spreading: cb(b) = ct(b) / ecb(b)
cb = ct / (ecb + 1e-12)
# Normalized energy term:
# en(b) = ecb(b) / sum_bb S(bb, b)
spread_colsum = np.sum(spread, axis=0)
en = ecb / (spread_colsum + 1e-12)
# Tonality index (clamped to [0, 1])
tb = -0.299 - 0.43 * np.log(np.maximum(cb, 1e-12))
tb = np.clip(tb, 0.0, 1.0)
# Required SNR per band (dB): interpolate between TMN and NMT
snr_b = tb * TMN_DB + (1.0 - tb) * NMT_DB
bc = 10.0 ** (-snr_b / 10.0)
# Noise masking threshold estimate (power domain)
nb = en * bc
# Threshold in quiet (convert from dB to power domain):
# qthr_power = eps * (N/2) * 10^(qthr_db/10)
qthr_power = np.finfo('float').eps * (N / 2.0) * (10.0 ** (qthr_db / 10.0))
# Final masking threshold per band:
# np(b) = max(nb(b), qthr(b))
npart = np.maximum(nb, qthr_power)
# Signal-to-mask ratio:
# SMR(b) = e(b) / np(b)
smr = e_b / (npart + 1e-12)
return smr.astype(np.float64, copy=False)
# -----------------------------------------------------------------------------
# ESH window slicing (match filterbank conventions)
# -----------------------------------------------------------------------------
def _esh_subframes(x_2048: FrameChannelT) -> list[FrameChannelT]:
"""
Extract the 8 overlapping 256-sample short windows used by AAC ESH.
The project convention (matching the filterbank) is:
start_j = 448 + 128*j, for j = 0..7
subframe_j = x[start_j : start_j + 256]
This selects the central 1152-sample region [448, 1600) and produces
8 windows with 50% overlap.
Parameters
----------
x_2048 : FrameChannelT
Time-domain channel frame, shape (2048,).
Returns
-------
list[FrameChannelT]
List of 8 subframes, each of shape (256,).
"""
x_2048 = np.asarray(x_2048, dtype=np.float64).reshape(-1)
if x_2048.shape[0] != 2048:
raise ValueError("ESH requires 2048-sample input frames.")
subs: list[FrameChannelT] = []
for j in range(8):
start = 448 + 128 * j
subs.append(x_2048[start : start + 256])
return subs
# -----------------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------------
def aac_psycho(
frame_T: FrameChannelT,
frame_type: FrameType,
frame_T_prev_1: FrameChannelT,
frame_T_prev_2: FrameChannelT,
) -> FloatArray:
"""
Psychoacoustic model for ONE channel.
Parameters
----------
frame_T : FrameChannelT
Current time-domain channel frame, shape (2048,).
For "ESH", the 8 short windows are derived internally.
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
frame_T_prev_1 : FrameChannelT
Previous time-domain channel frame, shape (2048,).
frame_T_prev_2 : FrameChannelT
Pre-previous time-domain channel frame, shape (2048,).
Returns
-------
FloatArray
Signal-to-Mask Ratio (SMR), per psychoacoustic band.
- If frame_type == "ESH": shape (42, 8)
- Else: shape (69,)
"""
frame_T = np.asarray(frame_T, dtype=np.float64).reshape(-1)
frame_T_prev_1 = np.asarray(frame_T_prev_1, dtype=np.float64).reshape(-1)
frame_T_prev_2 = np.asarray(frame_T_prev_2, dtype=np.float64).reshape(-1)
if frame_T.shape[0] != 2048 or frame_T_prev_1.shape[0] != 2048 or frame_T_prev_2.shape[0] != 2048:
raise ValueError("aac_psycho expects 2048-sample frames for current/prev1/prev2.")
table, N = get_table(frame_type)
# Long frame types: compute one SMR vector (69 bands)
if frame_type != "ESH":
return _psycho_window(frame_T, frame_T_prev_1, frame_T_prev_2, N=N, table=table)
# ESH: compute 8 SMR vectors (42 bands each), one per short subframe.
#
# The notes use short-window history for predictability:
# - For j=0: use previous frame's subframes (7, 6)
# - For j=1: use current subframe 0 and previous frame's subframe 7
# - For j>=2: use current subframes (j-1, j-2)
#
# This matches the "within-frame history" convention commonly used in
# simplified psycho models for ESH.
cur_subs = _esh_subframes(frame_T)
prev1_subs = _esh_subframes(frame_T_prev_1)
B = int(table.shape[0]) # expected 42
smr_out = np.zeros((B, 8), dtype=np.float64)
for j in range(8):
if j == 0:
x_m1 = prev1_subs[7]
x_m2 = prev1_subs[6]
elif j == 1:
x_m1 = cur_subs[0]
x_m2 = prev1_subs[7]
else:
x_m1 = cur_subs[j - 1]
x_m2 = cur_subs[j - 2]
smr_out[:, j] = _psycho_window(cur_subs[j], x_m1, x_m2, N=256, table=table)
return smr_out
+600
View File
@@ -0,0 +1,600 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Quantizer / iQuantizer (Level 3)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Implements AAC quantizer and inverse quantizer for one channel.
# Based on assignment section 2.6 (Eq. 12-15).
#
# Notes:
# - Bit reservoir is not implemented (assignment simplification).
# - Scalefactor bands are assumed equal to psychoacoustic bands
# (Table B.2.1.9a / B.2.1.9b from TableB219.mat).
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from core.aac_utils import get_table, band_limits
from core.aac_types import *
# -----------------------------------------------------------------------------
# Constants (assignment)
# -----------------------------------------------------------------------------
MAGIC_NUMBER: float = 0.4054
EPS: float = 1e-12
MAX_SF_DELTA:int = 60
# -----------------------------------------------------------------------------
# Helpers: ESH packing/unpacking (128x8 <-> 1024x1)
# -----------------------------------------------------------------------------
def _esh_pack(x_128x8: FloatArray) -> FloatArray:
"""
Pack ESH coefficients (128 x 8) into a single long vector (1024 x 1).
Packing order:
Columns are concatenated in subframe order (0..7), column-major.
Parameters
----------
x_128x8 : FloatArray
ESH coefficients, shape (128, 8).
Returns
-------
FloatArray
Packed coefficients, shape (1024, 1).
"""
x_128x8 = np.asarray(x_128x8, dtype=np.float64)
if x_128x8.shape != (128, 8):
raise ValueError("ESH pack expects shape (128, 8).")
return x_128x8.reshape(1024, 1, order="F")
def _esh_unpack(x_1024x1: FloatArray) -> FloatArray:
"""
Unpack a packed ESH vector (1024 elements) back to shape (128, 8).
Parameters
----------
x_1024x1 : FloatArray
Packed ESH vector, shape (1024,) or (1024, 1) after flattening.
Returns
-------
FloatArray
Unpacked ESH coefficients, shape (128, 8).
"""
x_1024x1 = np.asarray(x_1024x1, dtype=np.float64).reshape(-1)
if x_1024x1.shape[0] != 1024:
raise ValueError("ESH unpack expects 1024 elements.")
return x_1024x1.reshape(128, 8, order="F")
# -----------------------------------------------------------------------------
# Core quantizer formulas (Eq. 12, Eq. 13)
# -----------------------------------------------------------------------------
def _quantize_symbol(x: FloatArray, alpha: float) -> QuantizedSymbols:
"""
Quantize MDCT coefficients to integer symbols S(k).
Implements Eq. (12):
S(k) = sgn(X(k)) * int( (|X(k)| * 2^(-alpha/4))^(3/4) + MAGIC_NUMBER )
Parameters
----------
x : FloatArray
MDCT coefficients for a contiguous set of spectral lines.
Shape: (N,)
alpha : float
Scalefactor gain for the corresponding scalefactor band.
Returns
-------
QuantizedSymbols
Quantized symbols S(k) as int64, shape (N,).
"""
x = np.asarray(x, dtype=np.float64)
scale = 2.0 ** (-0.25 * float(alpha))
ax = np.abs(x) * scale
y = np.power(ax, 0.75, dtype=np.float64)
# "int" in the assignment corresponds to truncation.
q = np.floor(y + MAGIC_NUMBER).astype(np.int64)
return (np.sign(x).astype(np.int64) * q).astype(np.int64)
def _dequantize_symbol(S: QuantizedSymbols, alpha: float) -> FloatArray:
"""
Inverse quantizer (dequantization of symbols).
Implements Eq. (13):
Xhat(k) = sgn(S(k)) * |S(k)|^(4/3) * 2^(alpha/4)
Parameters
----------
S : QuantizedSymbols
Quantized symbols S(k), int64, shape (N,).
alpha : float
Scalefactor gain for the corresponding scalefactor band.
Returns
-------
FloatArray
Reconstructed MDCT coefficients Xhat(k), float64, shape (N,).
"""
S = np.asarray(S, dtype=np.int64)
scale = 2.0 ** (0.25 * float(alpha))
aS = np.abs(S).astype(np.float64)
y = np.power(aS, 4.0 / 3.0, dtype=np.float64)
return (np.sign(S).astype(np.float64) * y * scale).astype(np.float64)
# -----------------------------------------------------------------------------
# Alpha initialization (Eq. 14)
# -----------------------------------------------------------------------------
def _initial_alpha_hat(X: "FloatArray", MQ: int = 8191) -> int:
"""
Compute the initial scalefactor estimate alpha_hat for a frame.
The assignment proposes the following first approximation (Equation 14):
alpha_hat = (16/3) * log2( max_k(|X(k)|)^(3/4) / MQ )
where max_k runs over all MDCT coefficients of the frame (not per band),
and MQ is the maximum quantization level parameter (2*MQ + 1 levels).
Parameters
----------
X : FloatArray
MDCT coefficients of one frame (or one ESH subframe), shape (N,).
MQ : int
Quantizer parameter (default 8191, as per assignment).
Returns
-------
int
Integer alpha_hat (rounded to nearest integer).
"""
x_max = float(np.max(np.abs(X)))
if x_max <= 0.0:
return 0
alpha_hat = (16.0 / 3.0) * np.log2((x_max ** (3.0 / 4.0)) / float(MQ))
return int(np.round(alpha_hat))
# -----------------------------------------------------------------------------
# Band utilities
# -----------------------------------------------------------------------------
def _band_slices(frame_type: FrameType) -> list[tuple[int, int]]:
"""
Return scalefactor band ranges [wlow, whigh] (inclusive) for the given frame type.
These are derived from the psychoacoustic tables (TableB219),
and map directly to MDCT indices:
- long: 0..1023
- short (ESH subframe): 0..127
Parameters
----------
frame_type : FrameType
Frame type ("OLS", "LSS", "ESH", "LPS").
Returns
-------
list[tuple[int, int]]
List of (lo, hi) inclusive index pairs for each band.
"""
table, _Nfft = get_table(frame_type)
wlow, whigh, _bval, _qthr_db = band_limits(table)
bands: list[tuple[int, int]] = []
for lo, hi in zip(wlow, whigh):
bands.append((int(lo), int(hi)))
return bands
def _band_energy(x: FloatArray, lo: int, hi: int) -> float:
"""
Compute energy of a spectral segment x[lo:hi+1].
Parameters
----------
x : FloatArray
MDCT coefficient vector.
lo, hi : int
Inclusive index range.
Returns
-------
float
Sum of squares (energy) within the band.
"""
sec = x[lo : hi + 1]
return float(np.sum(sec * sec))
def _psychoacoustic_threshold(
X: FloatArray,
SMR_col: FloatArray,
bands: list[tuple[int, int]],
) -> FloatArray:
"""
Compute psychoacoustic thresholds T(b) per band.
Uses:
P(b) = sum_{k in band} X(k)^2
T(b) = P(b) / SMR(b)
Parameters
----------
X : FloatArray
MDCT coefficients for a frame (long) or one ESH subframe (short).
SMR_col : FloatArray
SMR values for this frame/subframe, shape (NB,).
bands : list[tuple[int, int]]
Band index ranges.
Returns
-------
FloatArray
Threshold vector T(b), shape (NB,).
"""
nb = len(bands)
T = np.zeros((nb,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
P = _band_energy(X, lo, hi)
smr = float(SMR_col[b])
if smr <= EPS:
T[b] = 0.0
else:
T[b] = P / smr
return T
# -----------------------------------------------------------------------------
# Alpha selection per band + neighbor-difference constraint
# -----------------------------------------------------------------------------
def _best_alpha_for_band(
X: "FloatArray", lo: int, hi: int, T_b: float,
alpha_hat: int, alpha_prev: int, alpha_min: int, alpha_max: int,
) -> int:
"""
Determine the band-wise scalefactor alpha(b) following the assignment.
Procedure:
- Start from a frame-wise initial estimate alpha_hat.
- Iteratively increase alpha(b) by 1 as long as the quantization error power
stays below the psychoacoustic threshold T(b): P_e(b) = sum_{k in band} ( X(k) - Xhat(k) )^2
- Stop increasing alpha(b) if the neighbor constraint would be violated: |alpha(b) - alpha(b-1)| <= 60
When processing bands sequentially (low -> high), this becomes: alpha(b) <= alpha_prev + 60
Notes:
- This function does not decrease alpha if the initial value already violates
the threshold; the assignment only specifies iterative increase.
Parameters
----------
X : FloatArray
Full MDCT vector of the current (sub)frame, shape (N,).
lo, hi : int
Band index bounds (inclusive), defining the band slice.
T_b : float
Threshold T(b) for this band.
alpha_hat : int
Initial frame-wise estimate (Equation 14).
alpha_prev : int
Previously selected alpha for band b-1 (neighbor constraint reference).
alpha_min, alpha_max : int
Safeguard bounds for alpha.
Returns
-------
int
Selected integer alpha(b).
"""
if T_b <= 0.0:
return int(alpha_hat)
Xsec = X[lo : hi + 1]
# Neighbor constraint (sequential processing): alpha(b) <= alpha_prev + 60
alpha_limit = min(int(alpha_max), int(alpha_prev) + MAX_SF_DELTA)
# Start from alpha_hat, clamped to feasible range
alpha = int(alpha_hat)
alpha = max(int(alpha_min), min(alpha, int(alpha_limit)))
# Evaluate at current alpha
Ssec = _quantize_symbol(Xsec, alpha)
Xhat = _dequantize_symbol(Ssec, alpha)
Pe = float(np.sum((Xsec - Xhat) ** 2))
# If already above threshold, return current alpha (no decrease step specified)
if Pe > T_b:
return alpha
# Increase alpha while still under threshold and within constraints
while True:
alpha_next = alpha + 1
if alpha_next > alpha_limit:
break
Ssec = _quantize_symbol(Xsec, alpha_next)
Xhat = _dequantize_symbol(Ssec, alpha_next)
Pe_next = float(np.sum((Xsec - Xhat) ** 2))
if Pe_next > T_b:
break
alpha = alpha_next
return alpha
# -----------------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------------
def aac_quantizer(
frame_F: FrameChannelF,
frame_type: FrameType,
SMR: FloatArray,
) -> tuple[QuantizedSymbols, ScaleFactors, GlobalGain]:
"""
AAC quantizer for one channel (Level 3).
Quantizes MDCT coefficients (after TNS) using band-wise scalefactors derived
from psychoacoustic thresholds computed via SMR.
The implementation follows the assignment procedure:
- Compute an initial frame-wise alpha_hat using Equation (14), based on the
maximum MDCT coefficient magnitude of the (sub)frame.
- For each band b, increase alpha(b) by 1 while the quantization error power
P_e(b) stays below the threshold T(b).
- Enforce the neighbor constraint |alpha(b) - alpha(b-1)| <= 60 during the
band-by-band search (no post-processing needed).
Parameters
----------
frame_F : FrameChannelF
MDCT coefficients after TNS, one channel.
Shapes:
- Long frames: (1024,) or (1024, 1)
- ESH: (128, 8)
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
SMR : FloatArray
Signal-to-Mask Ratio per band.
Shapes:
- Long: (NB,) or (NB, 1)
- ESH: (NB, 8)
Returns
-------
S : QuantizedSymbols
Quantized symbols S(k), packed as shape (1024, 1) for all frame types.
For ESH, the 8 subframes are packed in column-major subframe layout.
sfc : ScaleFactors
DPCM-coded scalefactors:
sfc(0) = alpha(0) = G
sfc(b) = alpha(b) - alpha(b-1), for b > 0
Shapes:
- Long: (NB, 1)
- ESH: (NB, 8)
G : GlobalGain
Global gain G = alpha(0).
- Long: scalar float
- ESH: array shape (1, 8), dtype float64
"""
bands = _band_slices(frame_type)
NB = len(bands)
X = np.asarray(frame_F, dtype=np.float64)
SMR = np.asarray(SMR, dtype=np.float64)
# -------------------------------------------------------------------------
# ESH: 8 short subframes, each of length 128
# -------------------------------------------------------------------------
if frame_type == "ESH":
if X.shape != (128, 8):
raise ValueError("For ESH, frame_F must have shape (128, 8).")
if SMR.shape != (NB, 8):
raise ValueError(f"For ESH, SMR must have shape ({NB}, 8).")
S_out: QuantizedSymbols = np.zeros((1024, 1), dtype=np.int64)
sfc: ScaleFactors = np.zeros((NB, 8), dtype=np.int64)
G_arr = np.zeros((1, 8), dtype=np.float64)
# Packed output view: (128, 8) with column-major layout
S_pack = S_out[:, 0].reshape(128, 8, order="F")
for j in range(8):
Xj = X[:, j].reshape(128)
SMRj = SMR[:, j].reshape(NB)
# Compute psychoacoustic threshold T(b) for this subframe
T = _psychoacoustic_threshold(Xj, SMRj, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xj)
# Band-wise scalefactors alpha(b)
alpha = np.zeros((NB,), dtype=np.int64)
alpha_prev = int(alpha_hat)
for b, (lo, hi) in enumerate(bands):
alpha_b = _best_alpha_for_band(
X=Xj,
lo=lo,
hi=hi,
T_b=float(T[b]),
alpha_hat=int(alpha_hat),
alpha_prev=int(alpha_prev),
alpha_min=-4096,
alpha_max=4096,
)
alpha[b] = int(alpha_b)
alpha_prev = int(alpha_b)
# DPCM-coded scalefactors
G_arr[0, j] = float(alpha[0])
sfc[0, j] = int(alpha[0])
for b in range(1, NB):
sfc[b, j] = int(alpha[b] - alpha[b - 1])
# Quantize MDCT coefficients band-by-band
Sj = np.zeros((128,), dtype=np.int64)
for b, (lo, hi) in enumerate(bands):
Sj[lo : hi + 1] = _quantize_symbol(Xj[lo : hi + 1], float(alpha[b]))
# Store subframe in packed output
S_pack[:, j] = Sj
return S_out, sfc, G_arr
# -------------------------------------------------------------------------
# Long frames: OLS / LSS / LPS, length 1024
# -------------------------------------------------------------------------
if X.shape == (1024,):
Xv = X
elif X.shape == (1024, 1):
Xv = X[:, 0]
else:
raise ValueError("For non-ESH, frame_F must have shape (1024,) or (1024, 1).")
if SMR.shape == (NB,):
SMRv = SMR
elif SMR.shape == (NB, 1):
SMRv = SMR[:, 0]
else:
raise ValueError(f"For non-ESH, SMR must have shape ({NB},) or ({NB}, 1).")
# Compute psychoacoustic threshold T(b) for the long frame
T = _psychoacoustic_threshold(Xv, SMRv, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xv)
# Band-wise scalefactors alpha(b)
alpha = np.zeros((NB,), dtype=np.int64)
alpha_prev = int(alpha_hat)
for b, (lo, hi) in enumerate(bands):
alpha_b = _best_alpha_for_band(
X=Xv,
lo=lo,
hi=hi,
T_b=float(T[b]),
alpha_hat=int(alpha_hat),
alpha_prev=int(alpha_prev),
alpha_min=-4096,
alpha_max=4096,
)
alpha[b] = int(alpha_b)
alpha_prev = int(alpha_b)
# DPCM-coded scalefactors
sfc_out: ScaleFactors = np.zeros((NB, 1), dtype=np.int64)
sfc_out[0, 0] = int(alpha[0])
for b in range(1, NB):
sfc_out[b, 0] = int(alpha[b] - alpha[b - 1])
G: float = float(alpha[0])
# Quantize MDCT coefficients band-by-band
S_vec = np.zeros((1024,), dtype=np.int64)
for b, (lo, hi) in enumerate(bands):
S_vec[lo : hi + 1] = _quantize_symbol(Xv[lo : hi + 1], float(alpha[b]))
return S_vec.reshape(1024, 1), sfc_out, G
def aac_i_quantizer(
S: QuantizedSymbols,
sfc: ScaleFactors,
G: GlobalGain,
frame_type: FrameType,
) -> FrameChannelF:
"""
Inverse quantizer (iQuantizer) for one channel.
Reconstructs MDCT coefficients from quantized symbols and DPCM scalefactors.
Parameters
----------
S : QuantizedSymbols
Quantized symbols, shape (1024, 1) (or any array with 1024 elements).
sfc : ScaleFactors
DPCM-coded scalefactors.
Shapes:
- Long: (NB, 1)
- ESH: (NB, 8)
G : GlobalGain
Global gain (not strictly required if sfc includes sfc(0)=alpha(0)).
Present for API compatibility with the assignment.
frame_type : FrameType
AAC frame type.
Returns
-------
FrameChannelF
Reconstructed MDCT coefficients:
- ESH: (128, 8)
- Long: (1024, 1)
"""
bands = _band_slices(frame_type)
NB = len(bands)
S_flat = np.asarray(S, dtype=np.int64).reshape(-1)
if S_flat.shape[0] != 1024:
raise ValueError("S must contain 1024 symbols.")
if frame_type == "ESH":
sfc = np.asarray(sfc, dtype=np.int64)
if sfc.shape != (NB, 8):
raise ValueError(f"For ESH, sfc must have shape ({NB}, 8).")
S_128x8 = _esh_unpack(S_flat)
Xrec = np.zeros((128, 8), dtype=np.float64)
for j in range(8):
alpha = np.zeros((NB,), dtype=np.int64)
alpha[0] = int(sfc[0, j])
for b in range(1, NB):
alpha[b] = int(alpha[b - 1] + sfc[b, j])
Xj = np.zeros((128,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
Xj[lo : hi + 1] = _dequantize_symbol(S_128x8[lo : hi + 1, j].astype(np.int64), float(alpha[b]))
Xrec[:, j] = Xj
return Xrec
sfc = np.asarray(sfc, dtype=np.int64)
if sfc.shape != (NB, 1):
raise ValueError(f"For non-ESH, sfc must have shape ({NB}, 1).")
alpha = np.zeros((NB,), dtype=np.int64)
alpha[0] = int(sfc[0, 0])
for b in range(1, NB):
alpha[b] = int(alpha[b - 1] + sfc[b, 0])
Xrec = np.zeros((1024,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
Xrec[lo : hi + 1] = _dequantize_symbol(S_flat[lo : hi + 1], float(alpha[b]))
return Xrec.reshape(1024, 1)
-60
View File
@@ -1,60 +0,0 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - SNR dB calculator
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# This module implements SNR calculation in dB
# ------------------------------------------------------------
from __future__ import annotations
from core.aac_types import StereoSignal
import numpy as np
def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Compute overall SNR (dB) over all samples and channels after aligning lengths.
Parameters
----------
x_ref : StereoSignal
Reference stereo stream.
x_hat : StereoSignal
Reconstructed stereo stream.
Returns
-------
float
SNR in dB.
- Returns +inf if noise power is zero.
- Returns -inf if signal power is zero.
"""
x_ref = np.asarray(x_ref, dtype=np.float64)
x_hat = np.asarray(x_hat, dtype=np.float64)
if x_ref.ndim == 1:
x_ref = x_ref.reshape(-1, 1)
if x_hat.ndim == 1:
x_hat = x_hat.reshape(-1, 1)
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
x_ref = x_ref[:n, :c]
x_hat = x_hat[:n, :c]
err = x_ref - x_hat
ps = float(np.sum(x_ref * x_ref))
pn = float(np.sum(err * err))
if pn <= 0.0:
return float("inf")
if ps <= 0.0:
return float("-inf")
return float(10.0 * np.log10(ps / pn))
+2 -2
View File
@@ -173,10 +173,10 @@ def _stereo_merge(ft_l: FrameType, ft_r: FrameType) -> FrameType:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public Function prototypes (Level 1) # Public Function prototypes
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_SSC(frame_T: FrameT, next_frame_T: FrameT, prev_frame_type: FrameType) -> FrameType: def aac_ssc(frame_T: FrameT, next_frame_T: FrameT, prev_frame_type: FrameType) -> FrameType:
""" """
Sequence Segmentation Control (SSC). Sequence Segmentation Control (SSC).
+514
View File
@@ -0,0 +1,514 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Temporal Noise Shaping (TNS)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Temporal Noise Shaping (TNS) module (Level 2).
#
# Public API:
# frame_F_out, tns_coeffs = aac_tns(frame_F_in, frame_type)
# frame_F_out = aac_i_tns(frame_F_in, frame_type, tns_coeffs)
#
# Notes (per assignment):
# - TNS is applied per channel (not stereo).
# - For ESH, TNS is applied independently to each of the 8 short subframes.
# - Bark band tables are taken from TableB.2.1.9a (long) and TableB.2.1.9b (short)
# provided in TableB219.mat.
# - Predictor order is fixed to p = 4.
# - Coefficients are quantized with a 4-bit uniform symmetric quantizer, step = 0.1.
# - Forward TNS applies FIR: H_TNS(z) = 1 - a1 z^-1 - ... - ap z^-p
# - Inverse TNS applies the inverse IIR filter using the same quantized coefficients.
# ------------------------------------------------------------
from __future__ import annotations
from pathlib import Path
from typing import Tuple
from core.aac_utils import load_b219_tables
from core.aac_configuration import PRED_ORDER, QUANT_STEP, QUANT_MAX
from core.aac_types import *
# -----------------------------------------------------------------------------
# Private helpers
# -----------------------------------------------------------------------------
def _band_ranges(k_count: int) -> BandRanges:
"""
Return Bark band index ranges [start, end] (inclusive) for the given MDCT line count.
Parameters
----------
k_count : int
Number of MDCT lines:
- 1024 for long frames
- 128 for short subframes (ESH)
Returns
-------
BandRanges (list[tuple[int, int]])
Each tuple is (start_k, end_k) inclusive.
"""
tables = load_b219_tables()
if k_count == 1024:
tbl = tables["B219a"]
elif k_count == 128:
tbl = tables["B219b"]
else:
raise ValueError("TNS supports only k_count=1024 (long) or k_count=128 (short).")
start = tbl[:, 1].astype(int)
end = tbl[:, 2].astype(int)
ranges: BandRanges = [(int(s), int(e)) for s, e in zip(start, end)]
for s, e in ranges:
if s < 0 or e < s or e >= k_count:
raise ValueError("Invalid band table ranges for given k_count.")
return ranges
# -----------------------------------------------------------------------------
# Core DSP helpers
# -----------------------------------------------------------------------------
def _smooth_sw_inplace(sw: MdctCoeffs) -> None:
"""
Smooth Sw(k) to reduce discontinuities between adjacent Bark bands.
The assignment applies two passes:
- Backward: Sw(k) = (Sw(k) + Sw(k+1))/2
- Forward: Sw(k) = (Sw(k) + Sw(k-1))/2
Parameters
----------
sw : MdctCoeffs
1-D array of length K (float64). Modified in-place.
"""
k_count = int(sw.shape[0])
for k in range(k_count - 2, -1, -1):
sw[k] = 0.5 * (sw[k] + sw[k + 1])
for k in range(1, k_count):
sw[k] = 0.5 * (sw[k] + sw[k - 1])
def _compute_sw(x: MdctCoeffs) -> MdctCoeffs:
"""
Compute Sw(k) from band energies P(j) and apply boundary smoothing.
Parameters
----------
x : MdctCoeffs
1-D MDCT line array, length K.
Returns
-------
MdctCoeffs
Sw(k), 1-D array of length K, float64.
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
k_count = int(x.shape[0])
bands = _band_ranges(k_count)
sw = np.zeros(k_count, dtype=np.float64)
for s, e in bands:
seg = x[s : e + 1]
p_j = float(np.sum(seg * seg))
sw_val = float(np.sqrt(p_j))
sw[s : e + 1] = sw_val
_smooth_sw_inplace(sw)
return sw
def _autocorr(x: MdctCoeffs, p: int) -> MdctCoeffs:
"""
Autocorrelation r(m) for m=0..p.
Parameters
----------
x : MdctCoeffs
1-D signal.
p : int
Maximum lag.
Returns
-------
MdctCoeffs
r, shape (p+1,), float64.
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
n = int(x.shape[0])
r = np.zeros(p + 1, dtype=np.float64)
for m in range(p + 1):
r[m] = float(np.dot(x[m:], x[: n - m]))
return r
def _lpc_coeffs(xw: MdctCoeffs, p: int) -> MdctCoeffs:
"""
Solve Yule-Walker normal equations for LPC coefficients of order p.
Parameters
----------
xw : MdctCoeffs
1-D normalized sequence Xw(k).
p : int
Predictor order.
Returns
-------
MdctCoeffs
LPC coefficients a[0..p-1], shape (p,), float64.
"""
r = _autocorr(xw, p)
R = np.empty((p, p), dtype=np.float64)
for i in range(p):
for j in range(p):
R[i, j] = r[abs(i - j)]
rhs = r[1 : p + 1].reshape(p)
reg = 1e-12
R_reg = R + reg * np.eye(p, dtype=np.float64)
a = np.linalg.solve(R_reg, rhs)
return a
def _quantize_coeffs(a: MdctCoeffs) -> MdctCoeffs:
"""
Quantize LPC coefficients with uniform symmetric quantizer and clamp.
Parameters
----------
a : MdctCoeffs
LPC coefficient array, shape (p,).
Returns
-------
MdctCoeffs
Quantized coefficients, shape (p,), float64.
"""
a = np.asarray(a, dtype=np.float64).reshape(-1)
q = np.round(a / QUANT_STEP) * QUANT_STEP
q = np.clip(q, -QUANT_MAX, QUANT_MAX)
return q.astype(np.float64, copy=False)
def _is_inverse_stable(a_q: MdctCoeffs) -> bool:
"""
Check stability of the inverse TNS filter H_TNS^{-1}.
Forward filter:
H_TNS(z) = 1 - a1 z^-1 - ... - ap z^-p
Inverse filter poles are roots of:
A(z) = 1 - a1 z^-1 - ... - ap z^-p
Multiply by z^p:
z^p - a1 z^{p-1} - ... - ap = 0
Stability condition:
all roots satisfy |z| < 1.
Parameters
----------
a_q : MdctCoeffs
Quantized predictor coefficients, shape (p,).
Returns
-------
bool
True if stable, else False.
"""
a_q = np.asarray(a_q, dtype=np.float64).reshape(-1)
p = int(a_q.shape[0])
# Polynomial in z: z^p - a1 z^{p-1} - ... - ap
poly = np.empty(p + 1, dtype=np.float64)
poly[0] = 1.0
poly[1:] = -a_q
roots = np.roots(poly)
# Strictly inside unit circle for stability. Add tiny margin for numeric safety.
margin = 1e-12
return bool(np.all(np.abs(roots) < (1.0 - margin)))
def _stabilize_quantized_coeffs(a_q: MdctCoeffs) -> MdctCoeffs:
"""
Make quantized predictor coefficients stable for inverse filtering.
Policy:
- If already stable: return as-is.
- Else: iteratively shrink coefficients by gamma and re-quantize to the 0.1 grid.
- If still unstable after attempts: fall back to all-zero coefficients (disable TNS).
Parameters
----------
a_q : MdctCoeffs
Quantized predictor coefficients, shape (p,).
Returns
-------
MdctCoeffs
Stable quantized coefficients, shape (p,).
"""
a_q = np.asarray(a_q, dtype=np.float64).reshape(-1)
if _is_inverse_stable(a_q):
return a_q
# Try a few shrinking factors. Re-quantize after shrinking to keep coefficients on-grid.
gammas = (0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1)
for g in gammas:
cand = _quantize_coeffs(g * a_q)
if _is_inverse_stable(cand):
return cand
# Last resort: disable TNS for this vector
return np.zeros_like(a_q, dtype=np.float64)
def _apply_tns_fir(x: MdctCoeffs, a_q: MdctCoeffs) -> MdctCoeffs:
"""
Apply forward TNS FIR filter:
y[k] = x[k] - sum_{l=1..p} a_l * x[k-l]
Parameters
----------
x : MdctCoeffs
1-D MDCT lines, length K.
a_q : MdctCoeffs
Quantized LPC coefficients, shape (p,).
Returns
-------
MdctCoeffs
Filtered MDCT lines y, length K.
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
a_q = np.asarray(a_q, dtype=np.float64).reshape(-1)
p = int(a_q.shape[0])
k_count = int(x.shape[0])
y = np.zeros(k_count, dtype=np.float64)
for k in range(k_count):
acc = x[k]
for l in range(1, p + 1):
if k - l >= 0:
acc -= a_q[l - 1] * x[k - l]
y[k] = acc
return y
def _apply_itns_iir(y: MdctCoeffs, a_q: MdctCoeffs) -> MdctCoeffs:
"""
Apply inverse TNS IIR filter:
x_hat[k] = y[k] + sum_{l=1..p} a_l * x_hat[k-l]
Parameters
----------
y : MdctCoeffs
1-D MDCT lines after TNS, length K.
a_q : MdctCoeffs
Quantized LPC coefficients, shape (p,).
Returns
-------
MdctCoeffs
Reconstructed MDCT lines x_hat, length K.
"""
y = np.asarray(y, dtype=np.float64).reshape(-1)
a_q = np.asarray(a_q, dtype=np.float64).reshape(-1)
p = int(a_q.shape[0])
k_count = int(y.shape[0])
x_hat = np.zeros(k_count, dtype=np.float64)
for k in range(k_count):
acc = y[k]
for l in range(1, p + 1):
if k - l >= 0:
acc += a_q[l - 1] * x_hat[k - l]
x_hat[k] = acc
return x_hat
def _tns_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
"""
TNS for a single MDCT vector (one long frame or one short subframe).
Steps:
1) Compute Sw(k) from Bark band energies and smooth it.
2) Normalize: Xw(k) = X(k) / Sw(k) (safe when Sw=0).
3) Compute LPC coefficients (order p=PRED_ORDER) on Xw.
4) Quantize coefficients (4-bit symmetric, step QUANT_STEP).
5) Apply FIR filter on original X(k) using quantized coefficients.
Parameters
----------
x : MdctCoeffs
1-D MDCT vector.
Returns
-------
y : MdctCoeffs
TNS-processed MDCT vector (same length).
a_q : MdctCoeffs
Quantized LPC coefficients, shape (PRED_ORDER,).
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
sw = _compute_sw(x)
eps = 1e-12
xw = np.zeros_like(x, dtype=np.float64)
mask = sw > eps
np.divide(x, sw, out=xw, where=mask)
a = _lpc_coeffs(xw, PRED_ORDER)
a_q = _quantize_coeffs(a)
# Ensure inverse stability (assignment requirement)
a_q = _stabilize_quantized_coeffs(a_q)
y = _apply_tns_fir(x, a_q)
return y, a_q
# -----------------------------------------------------------------------------
# Public Functions
# -----------------------------------------------------------------------------
def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChannelF, TnsCoeffs]:
"""
Temporal Noise Shaping (TNS) for ONE channel.
Parameters
----------
frame_F_in : FrameChannelF
Per-channel MDCT coefficients.
Expected (typical) shapes:
- If frame_type == "ESH": (128, 8)
- Else: (1024, 1) or (1024,)
frame_type : FrameType
Frame type code ("OLS", "LSS", "ESH", "LPS").
Returns
-------
frame_F_out : FrameChannelF
Per-channel MDCT coefficients after applying TNS.
Same shape convention as input.
tns_coeffs : TnsCoeffs
Quantized TNS predictor coefficients.
Expected shapes:
- If frame_type == "ESH": (PRED_ORDER, 8)
- Else: (PRED_ORDER, 1)
"""
x = np.asarray(frame_F_in, dtype=np.float64)
if frame_type == "ESH":
if x.shape != (128, 8):
raise ValueError("For ESH, frame_F_in must have shape (128, 8).")
y = np.empty_like(x, dtype=np.float64)
a_out = np.empty((PRED_ORDER, 8), dtype=np.float64)
for j in range(8):
y[:, j], a_out[:, j] = _tns_vector(x[:, j])
return y, a_out
if x.shape == (1024,):
x_vec = x
out_shape = (1024,)
elif x.shape == (1024, 1):
x_vec = x[:, 0]
out_shape = (1024, 1)
else:
raise ValueError('For non-ESH, frame_F_in must have shape (1024,) or (1024, 1).')
y_vec, a_q = _tns_vector(x_vec)
if out_shape == (1024,):
y_out = y_vec
else:
y_out = y_vec.reshape(1024, 1)
a_out = a_q.reshape(PRED_ORDER, 1)
return y_out, a_out
def aac_i_tns(frame_F_in: FrameChannelF, frame_type: FrameType, tns_coeffs: TnsCoeffs) -> FrameChannelF:
"""
Inverse Temporal Noise Shaping (iTNS) for ONE channel.
Parameters
----------
frame_F_in : FrameChannelF
Per-channel MDCT coefficients after TNS.
Expected (typical) shapes:
- If frame_type == "ESH": (128, 8)
- Else: (1024, 1) or (1024,)
frame_type : FrameType
Frame type code ("OLS", "LSS", "ESH", "LPS").
tns_coeffs : TnsCoeffs
Quantized TNS predictor coefficients.
Expected shapes:
- If frame_type == "ESH": (PRED_ORDER, 8)
- Else: (PRED_ORDER, 1)
Returns
-------
FrameChannelF
Per-channel MDCT coefficients after inverse TNS.
Same shape convention as input frame_F_in.
"""
x = np.asarray(frame_F_in, dtype=np.float64)
a = np.asarray(tns_coeffs, dtype=np.float64)
if frame_type == "ESH":
if x.shape != (128, 8):
raise ValueError("For ESH, frame_F_in must have shape (128, 8).")
if a.shape != (PRED_ORDER, 8):
raise ValueError("For ESH, tns_coeffs must have shape (PRED_ORDER, 8).")
y = np.empty_like(x, dtype=np.float64)
for j in range(8):
y[:, j] = _apply_itns_iir(x[:, j], a[:, j])
return y
if a.shape != (PRED_ORDER, 1):
raise ValueError("For non-ESH, tns_coeffs must have shape (PRED_ORDER, 1).")
if x.shape == (1024,):
x_vec = x
out_shape = (1024,)
elif x.shape == (1024, 1):
x_vec = x[:, 0]
out_shape = (1024, 1)
else:
raise ValueError('For non-ESH, frame_F_in must have shape (1024,) or (1024, 1).')
y_vec = _apply_itns_iir(x_vec, a[:, 0])
if out_shape == (1024,):
return y_vec
return y_vec.reshape(1024, 1)
+129
View File
@@ -193,6 +193,61 @@ Bark-band index ranges [start, end] (inclusive) for MDCT lines.
Used by TNS to map MDCT indices k to Bark bands. Used by TNS to map MDCT indices k to Bark bands.
""" """
BarkTable: TypeAlias = FloatArray
"""
Psychoacoustic Bark band table loaded from TableB219.mat.
Typical shapes:
- Long: (69, 6)
- Short: (42, 6)
"""
BandIndexArray: TypeAlias = NDArray[np.int_]
"""
Array of FFT bin indices per psychoacoustic band.
"""
BandValueArray: TypeAlias = FloatArray
"""
Per-band psychoacoustic values (e.g. Bark position, thresholds).
"""
# Quantizer-related semantic aliases
QuantizedSymbols: TypeAlias = NDArray[np.generic]
"""
Quantized MDCT symbols S(k).
Shapes:
- Always (1024, 1) at the quantizer output (ESH packed to 1024 symbols).
"""
ScaleFactors: TypeAlias = NDArray[np.generic]
"""
DPCM-coded scalefactors sfc(b) = alpha(b) - alpha(b-1).
Shapes:
- Long frames: (NB, 1)
- ESH frames: (NB, 8)
"""
GlobalGain: TypeAlias = float | NDArray[np.generic]
"""
Global gain G = alpha(0).
- Long frames: scalar float
- ESH frames: array shape (1, 8)
"""
# Huffman semantic aliases
HuffmanBitstream: TypeAlias = str
"""Huffman-coded bitstream stored as a string of '0'/'1'."""
HuffmanCodebook: TypeAlias = int
"""Huffman codebook id (e.g., 0..11)."""
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Level 1 AAC sequence payload types # Level 1 AAC sequence payload types
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -280,3 +335,77 @@ Level 2 adds:
and stores: and stores:
- per-channel "frame_F" after applying TNS. - per-channel "frame_F" after applying TNS.
""" """
# -----------------------------------------------------------------------------
# Level 3 AAC sequence payload types (Quantizer + Huffman)
# -----------------------------------------------------------------------------
class AACChannelFrameF3(TypedDict):
"""
Per-channel payload for aac_seq_3[i]["chl"] or ["chr"] (Level 3).
Keys
----
tns_coeffs:
Quantized TNS predictor coefficients for ONE channel.
Shapes:
- ESH: (PRED_ORDER, 8)
- else: (PRED_ORDER, 1)
T:
Psychoacoustic thresholds per band.
Shapes:
- ESH: (NB, 8)
- else: (NB, 1)
Note: Stored for completeness / debugging; not entropy-coded.
G:
Quantized global gains.
Shapes:
- ESH: (1, 8) (one per short subframe)
- else: scalar (or compatible np scalar)
sfc:
Huffman-coded scalefactor differences (DPCM sequence).
stream:
Huffman-coded MDCT quantized symbols S(k) (packed to 1024 symbols).
codebook:
Huffman codebook id used for MDCT symbols (stream).
(Scalefactors typically use fixed codebook 11 and do not need to store it.)
"""
tns_coeffs: TnsCoeffs
T: FloatArray
G: FloatArray | float
sfc: HuffmanBitstream
stream: HuffmanBitstream
codebook: HuffmanCodebook
class AACSeq3Frame(TypedDict):
"""
One frame dictionary element of aac_seq_3 (Level 3).
"""
frame_type: FrameType
win_type: WinType
chl: AACChannelFrameF3
chr: AACChannelFrameF3
AACSeq3: TypeAlias = List[AACSeq3Frame]
"""
AAC sequence for Level 3:
List of length K (K = number of frames).
Each element is a dict with keys:
- "frame_type", "win_type", "chl", "chr"
Level 3 adds (per channel):
- "tns_coeffs"
- "T" thresholds (not entropy-coded)
- "G" global gain(s)
- "sfc" Huffman-coded scalefactor differences
- "stream" Huffman-coded MDCT quantized symbols
- "codebook" Huffman codebook for MDCT symbols
"""
+306
View File
@@ -0,0 +1,306 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - AAC Utilities
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Shared utility functions used across AAC encoder/decoder levels.
#
# This module currently provides:
# - MDCT / IMDCT conversions
# - Signal-to-Noise Ratio (SNR) computation in dB
# - Loading and access helpers for psychoacoustic band tables
# (TableB219.mat, Tables B.2.1.9a / B.2.1.9b of the AAC specification)
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from pathlib import Path
from scipy.io import loadmat
from core.aac_types import *
# -----------------------------------------------------------------------------
# Global cached data
# -----------------------------------------------------------------------------
# Cached contents of TableB219.mat to avoid repeated disk I/O.
# Keys:
# - "B219a": long-window psychoacoustic bands (69 bands, FFT size 2048)
# - "B219b": short-window psychoacoustic bands (42 bands, FFT size 256)
B219_CACHE: dict[str, BarkTable] | None = None
# -----------------------------------------------------------------------------
# MDCT / IMDCT
# -----------------------------------------------------------------------------
def mdct(s: TimeSignal) -> MdctCoeffs:
"""
MDCT (direct form) as specified in the assignment.
Parameters
----------
s : TimeSignal
Windowed time samples, 1-D array of length N (N = 2048 or 256).
Returns
-------
MdctCoeffs
MDCT coefficients, 1-D array of length N/2.
Definition
----------
X[k] = 2 * sum_{n=0..N-1} s[n] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
s = np.asarray(s, dtype=np.float64).reshape(-1)
N = int(s.shape[0])
if N not in (2048, 256):
raise ValueError("MDCT input length must be 2048 or 256.")
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(N // 2, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, N/2)
X = 2.0 * (s @ C) # (N/2,)
return X
def imdct(X: MdctCoeffs) -> TimeSignal:
"""
IMDCT (direct form) as specified in the assignment.
Parameters
----------
X : MdctCoeffs
MDCT coefficients, 1-D array of length K (K = 1024 or 128).
Returns
-------
TimeSignal
Reconstructed time samples, 1-D array of length N = 2K.
Definition
----------
s[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
X = np.asarray(X, dtype=np.float64).reshape(-1)
K = int(X.shape[0])
if K not in (1024, 128):
raise ValueError("IMDCT input length must be 1024 or 128.")
N = 2 * K
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(K, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, K)
s = (2.0 / N) * (C @ X) # (N,)
return s
# -----------------------------------------------------------------------------
# Signal quality metrics
# -----------------------------------------------------------------------------
def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Compute the overall Signal-to-Noise Ratio (SNR) in dB.
The SNR is computed over all available samples and channels,
after conservatively aligning the two signals to their common
length and channel count.
Parameters
----------
x_ref : StereoSignal
Reference (original) signal.
Typical shape: (N, 2) for stereo.
x_hat : StereoSignal
Reconstructed or processed signal.
Typical shape: (M, 2) for stereo.
Returns
-------
float
SNR in dB.
- +inf if the noise power is zero (perfect reconstruction).
- -inf if the reference signal power is zero.
"""
x_ref = np.asarray(x_ref, dtype=np.float64)
x_hat = np.asarray(x_hat, dtype=np.float64)
# Ensure 2-D shape: (samples, channels)
if x_ref.ndim == 1:
x_ref = x_ref.reshape(-1, 1)
if x_hat.ndim == 1:
x_hat = x_hat.reshape(-1, 1)
# Align lengths and channel count conservatively
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
x_ref = x_ref[:n, :c]
x_hat = x_hat[:n, :c]
err = x_ref - x_hat
ps = float(np.sum(x_ref * x_ref)) # signal power
pn = float(np.sum(err * err)) # noise power
if pn <= 0.0:
return float("inf")
if ps <= 0.0:
return float("-inf")
return float(10.0 * np.log10(ps / pn))
def estimate_lag_mono(x_ref: TimeSignal, x_hat: TimeSignal, max_lag=4096):
"""
Estimate time lag between two mono signals.
Returns lag (positive means x_hat delayed).
"""
n = min(len(x_ref), len(x_hat))
x_ref = x_ref[:n]
x_hat = x_hat[:n]
corr = np.correlate(x_ref, x_hat, mode='full')
lags = np.arange(-n + 1, n)
center = n - 1
lo = max(0, center - max_lag)
hi = min(len(corr), center + max_lag + 1)
best = lo + int(np.argmax(corr[lo:hi]))
return int(lags[best])
def match_gain(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Least-squares gain g that best maps x_hat -> x_ref.
"""
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
r = x_ref[:n, :c].reshape(-1).astype(np.float64)
h = x_hat[:n, :c].reshape(-1).astype(np.float64)
denom = float(np.dot(h, h))
if denom <= 0.0:
return 1.0
return float(np.dot(r, h) / denom)
# -----------------------------------------------------------------------------
# Psychoacoustic band tables (TableB219.mat)
# -----------------------------------------------------------------------------
def load_b219_tables() -> dict[str, BarkTable]:
"""
Load and cache psychoacoustic band tables from TableB219.mat.
The assignment/project layout assumes that a 'material' directory
is available in the current working directory when running:
- tests
- level_1 / level_2 / level_3 entrypoints
This function loads the tables once and caches them for subsequent calls.
Returns
-------
dict[str, BarkTable]
Dictionary with the following entries:
- "B219a": long-window psychoacoustic table
(69 bands, FFT size 2048 / 1024 spectral lines)
- "B219b": short-window psychoacoustic table
(42 bands, FFT size 256 / 128 spectral lines)
"""
global B219_CACHE
if B219_CACHE is not None:
return B219_CACHE
mat_path = Path("material") / "TableB219.mat"
if not mat_path.exists():
raise FileNotFoundError(
"Could not locate material/TableB219.mat in the current working directory."
)
data = loadmat(str(mat_path))
if "B219a" not in data or "B219b" not in data:
raise ValueError(
"TableB219.mat missing required variables 'B219a' and/or 'B219b'."
)
B219_CACHE = {
"B219a": np.asarray(data["B219a"], dtype=np.float64),
"B219b": np.asarray(data["B219b"], dtype=np.float64),
}
return B219_CACHE
def get_table(frame_type: FrameType) -> tuple[BarkTable, int]:
"""
Select the appropriate psychoacoustic band table and FFT size
based on the AAC frame type.
Parameters
----------
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
Returns
-------
table : BarkTable
Psychoacoustic band table:
- B219a for long frames
- B219b for ESH short subframes
N : int
FFT size corresponding to the table:
- 2048 for long frames
- 256 for short frames (ESH)
"""
tables = load_b219_tables()
if frame_type == "ESH":
return tables["B219b"], 256
return tables["B219a"], 2048
def band_limits(
table: BarkTable,
) -> tuple[BandIndexArray, BandIndexArray, BandValueArray, BandValueArray]:
"""
Extract per-band metadata from a TableB2.1.9 psychoacoustic table.
The column layout follows the provided TableB219.mat file and the
AAC specification tables B.2.1.9a / B.2.1.9b.
Parameters
----------
table : BarkTable
Psychoacoustic band table (B219a or B219b).
Returns
-------
wlow : BandIndexArray
Lower FFT bin index (inclusive) for each band.
whigh : BandIndexArray
Upper FFT bin index (inclusive) for each band.
bval : BandValueArray
Bark-scale (or equivalent) band position values.
Used in the spreading function.
qthr_db : BandValueArray
Threshold in quiet for each band, in dB.
"""
wlow = table[:, 1].astype(int)
whigh = table[:, 2].astype(int)
bval = table[:, 4].astype(np.float64)
qthr_db = table[:, 5].astype(np.float64)
return wlow, whigh, bval, qthr_db
+4 -3
View File
@@ -20,6 +20,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from tabnanny import verbose
from typing import Union from typing import Union
import soundfile as sf import soundfile as sf
@@ -28,7 +29,7 @@ from core.aac_types import AACSeq1, StereoSignal
from core.aac_coder import aac_coder_1 as core_aac_coder_1 from core.aac_coder import aac_coder_1 as core_aac_coder_1
from core.aac_coder import aac_read_wav_stereo_48k from core.aac_coder import aac_read_wav_stereo_48k
from core.aac_decoder import aac_decoder_1 as core_aac_decoder_1 from core.aac_decoder import aac_decoder_1 as core_aac_decoder_1
from core.aac_snr_db import snr_db from core.aac_utils import snr_db
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -52,7 +53,7 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
AACSeq1 AACSeq1
List of encoded frames (Level 1 schema). List of encoded frames (Level 1 schema).
""" """
return core_aac_coder_1(filename_in) return core_aac_coder_1(filename_in, verbose=True)
def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoSignal: def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoSignal:
@@ -73,7 +74,7 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
StereoSignal StereoSignal
Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64. Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64.
""" """
return core_aac_decoder_1(aac_seq_1, filename_out) return core_aac_decoder_1(aac_seq_1, filename_out, verbose=True)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+5 -2
View File
@@ -381,12 +381,15 @@ def decode_huff(huff_sec, huff_LUT):
while b: while b:
N += 1 N += 1
b = huff_sec[streamIndex + N] b = huff_sec[streamIndex + N]
streamIndex += N # Skip the N leading '1' bits AND the terminating '0' delimiter.
# The encoder writes: '1'*N + '0' + <N4 bits>
streamIndex += N +1
N4 = N + 4 N4 = N + 4
escape_word = huff_sec[streamIndex:streamIndex + N4] escape_word = huff_sec[streamIndex:streamIndex + N4]
escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2) escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2)
nTupleDec[idx] = escape_value nTupleDec[idx] = escape_value
streamIndex += N4 + 1 # We already consumed the delimiter above; now consume only N4 bits.
streamIndex += N4
# Apply signs again # Apply signs again
nTupleDec[escIndex] *= nTupleSign[escIndex] nTupleDec[escIndex] *= nTupleSign[escIndex]
+261 -20
View File
@@ -26,14 +26,91 @@ from pathlib import Path
from typing import Union from typing import Union
import soundfile as sf import soundfile as sf
from scipy.io import savemat
from core.aac_configuration import WIN_TYPE from core.aac_configuration import WIN_TYPE
from core.aac_filterbank import aac_filter_bank from core.aac_filterbank import aac_filter_bank
from core.aac_ssc import aac_SSC from core.aac_ssc import aac_ssc
from core.aac_tns import aac_tns from core.aac_tns import aac_tns
from core.aac_psycho import aac_psycho
from core.aac_quantizer import aac_quantizer # assumes your quantizer file is core/aac_quantizer.py
from core.aac_huffman import aac_encode_huff
from core.aac_utils import get_table, band_limits
from material.huff_utils import load_LUT
from core.aac_types import * from core.aac_types import *
# -----------------------------------------------------------------------------
# Helpers for thresholds (T(b))
# -----------------------------------------------------------------------------
def _band_slices_from_table(frame_type: FrameType) -> list[tuple[int, int]]:
"""
Return inclusive (lo, hi) band slices derived from TableB219.
"""
table, _ = get_table(frame_type)
wlow, whigh, _bval, _qthr_db = band_limits(table)
return [(int(lo), int(hi)) for lo, hi in zip(wlow, whigh)]
def _thresholds_from_smr(
frame_F_ch: FrameChannelF,
frame_type: FrameType,
SMR: FloatArray,
) -> FloatArray:
"""
Compute thresholds T(b) = P(b) / SMR(b), where P(b) is band energy.
Shapes:
- Long: returns (NB, 1)
- ESH: returns (NB, 8)
"""
bands = _band_slices_from_table(frame_type)
NB = len(bands)
X = np.asarray(frame_F_ch, dtype=np.float64)
SMR = np.asarray(SMR, dtype=np.float64)
if frame_type == "ESH":
if X.shape != (128, 8):
raise ValueError("For ESH, frame_F_ch must have shape (128, 8).")
if SMR.shape != (NB, 8):
raise ValueError(f"For ESH, SMR must have shape ({NB}, 8).")
T = np.zeros((NB, 8), dtype=np.float64)
for j in range(8):
Xj = X[:, j]
for b, (lo, hi) in enumerate(bands):
P = float(np.sum(Xj[lo : hi + 1] ** 2))
smr = float(SMR[b, j])
T[b, j] = 0.0 if smr <= 1e-12 else (P / smr)
return T
# Long
if X.shape == (1024,):
Xv = X
elif X.shape == (1024, 1):
Xv = X[:, 0]
else:
raise ValueError("For non-ESH, frame_F_ch must be shape (1024,) or (1024, 1).")
if SMR.shape == (NB,):
SMRv = SMR
elif SMR.shape == (NB, 1):
SMRv = SMR[:, 0]
else:
raise ValueError(f"For non-ESH, SMR must be shape ({NB},) or ({NB}, 1).")
T = np.zeros((NB, 1), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
P = float(np.sum(Xv[lo : hi + 1] ** 2))
smr = float(SMRv[b])
T[b, 0] = 0.0 if smr <= 1e-12 else (P / smr)
return T
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public helpers (useful for level_x demo wrappers) # Public helpers (useful for level_x demo wrappers)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -122,7 +199,10 @@ def aac_pack_frame_f_to_seq_channels(frame_type: FrameType, frame_f: FrameF) ->
# Level 1 encoder # Level 1 encoder
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1: def aac_coder_1(
filename_in: Union[str, Path],
verbose: bool = False
) -> AACSeq1:
""" """
Level-1 AAC encoder. Level-1 AAC encoder.
@@ -139,6 +219,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
filename_in : Union[str, Path] filename_in : Union[str, Path]
Input WAV filename. Input WAV filename.
Assumption: stereo audio, sampling rate 48 kHz. Assumption: stereo audio, sampling rate 48 kHz.
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -165,8 +247,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
aac_seq: AACSeq1 = [] aac_seq: AACSeq1 = []
prev_frame_type: FrameType = "OLS" prev_frame_type: FrameType = "OLS"
win_type: WinType = WIN_TYPE if verbose:
print("Encoding ", end="", flush=True)
for i in range(K): for i in range(K):
start = i * hop start = i * hop
@@ -182,24 +264,32 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64) tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail]) next_t = np.vstack([next_t, tail])
frame_type = aac_SSC(frame_t, next_t, prev_frame_type) frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
frame_f = aac_filter_bank(frame_t, frame_type, win_type) frame_f = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f) chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f)
aac_seq.append({ aac_seq.append({
"frame_type": frame_type, "frame_type": frame_type,
"win_type": win_type, "win_type": WIN_TYPE,
"chl": {"frame_F": chl_f}, "chl": {"frame_F": chl_f},
"chr": {"frame_F": chr_f}, "chr": {"frame_F": chr_f},
}) })
prev_frame_type = frame_type prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
return aac_seq return aac_seq
def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2: def aac_coder_2(
filename_in: Union[str, Path],
verbose: bool = False
) -> AACSeq2:
""" """
Level-2 AAC encoder (Level 1 + TNS). Level-2 AAC encoder (Level 1 + TNS).
@@ -207,6 +297,8 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
---------- ----------
filename_in : Union[str, Path] filename_in : Union[str, Path]
Input WAV filename (stereo, 48 kHz). Input WAV filename (stereo, 48 kHz).
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -238,6 +330,8 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
aac_seq: AACSeq2 = [] aac_seq: AACSeq2 = []
prev_frame_type: FrameType = "OLS" prev_frame_type: FrameType = "OLS"
if verbose:
print("Encoding ", end="", flush=True)
for i in range(K): for i in range(K):
start = i * hop start = i * hop
@@ -250,21 +344,12 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64) tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail]) next_t = np.vstack([next_t, tail])
frame_type = aac_SSC(frame_t, next_t, prev_frame_type) frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
# Level 1 analysis (packed stereo container) # Level 1 analysis (packed stereo container)
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE) frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
# Unpack to per-channel (as you already do in Level 1) chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f_stereo)
if frame_type == "ESH":
chl_f = np.empty((128, 8), dtype=np.float64)
chr_f = np.empty((128, 8), dtype=np.float64)
for j in range(8):
chl_f[:, j] = frame_f_stereo[:, 2 * j + 0]
chr_f[:, j] = frame_f_stereo[:, 2 * j + 1]
else:
chl_f = frame_f_stereo[:, 0:1].astype(np.float64, copy=False)
chr_f = frame_f_stereo[:, 1:2].astype(np.float64, copy=False)
# Level 2: apply TNS per channel # Level 2: apply TNS per channel
chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type) chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type)
@@ -278,7 +363,163 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
"chr": {"frame_F": chr_f_tns, "tns_coeffs": chr_tns_coeffs}, "chr": {"frame_F": chr_f_tns, "tns_coeffs": chr_tns_coeffs},
} }
) )
prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
return aac_seq
def aac_coder_3(
filename_in: Union[str, Path],
filename_aac_coded: Union[str, Path] | None = None,
verbose: bool = False,
) -> AACSeq3:
"""
Level-3 AAC encoder (Level 2 + Psycho + Quantizer + Huffman).
Parameters
----------
filename_in : Union[str, Path]
Input WAV filename (stereo, 48 kHz).
filename_aac_coded : Union[str, Path] | None
Optional .mat filename to store aac_seq_3 (assignment convenience).
verbose : bool
Optional argument to print encoding status
Returns
-------
AACSeq3
Encoded AAC sequence (Level 3 payload schema).
"""
filename_in = Path(filename_in)
x, _ = aac_read_wav_stereo_48k(filename_in)
hop = 1024
win = 2048
pad_pre = np.zeros((hop, 2), dtype=np.float64)
pad_post = np.zeros((hop, 2), dtype=np.float64)
x_pad = np.vstack([pad_pre, x, pad_post])
K = int((x_pad.shape[0] - win) // hop + 1)
if K <= 0:
raise ValueError("Input too short for framing.")
# Load Huffman LUTs once.
huff_LUT_list = load_LUT()
aac_seq: AACSeq3 = []
prev_frame_type: FrameType = "OLS"
# Psycho model needs per-channel history (prev1, prev2) of 2048-sample frames.
prev1_L = np.zeros((2048,), dtype=np.float64)
prev2_L = np.zeros((2048,), dtype=np.float64)
prev1_R = np.zeros((2048,), dtype=np.float64)
prev2_R = np.zeros((2048,), dtype=np.float64)
if verbose:
print("Encoding ", end="", flush=True)
for i in range(K):
start = i * hop
frame_t: FrameT = x_pad[start : start + win, :]
if frame_t.shape != (win, 2):
raise ValueError("Internal framing error: frame_t has wrong shape.")
next_t = x_pad[start + hop : start + hop + win, :]
if next_t.shape[0] < win:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail])
frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
# Analysis filterbank (stereo packed)
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f_stereo)
# TNS per channel
chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type)
chr_f_tns, chr_tns_coeffs = aac_tns(chr_f, frame_type)
# Psychoacoustic model per channel (time-domain)
frame_L = np.asarray(frame_t[:, 0], dtype=np.float64)
frame_R = np.asarray(frame_t[:, 1], dtype=np.float64)
SMR_L = aac_psycho(frame_L, frame_type, prev1_L, prev2_L)
SMR_R = aac_psycho(frame_R, frame_type, prev1_R, prev2_R)
# Thresholds T(b) (stored, not entropy-coded)
T_L = _thresholds_from_smr(chl_f_tns, frame_type, SMR_L)
T_R = _thresholds_from_smr(chr_f_tns, frame_type, SMR_R)
# Quantizer per channel
S_L, sfc_L, G_L = aac_quantizer(chl_f_tns, frame_type, SMR_L)
S_R, sfc_R, G_R = aac_quantizer(chr_f_tns, frame_type, SMR_R)
# Huffman-code ONLY the DPCM differences for b>0.
# sfc[0] corresponds to alpha(0)=G and is stored separately in the frame.
sfc_L_dpcm = np.asarray(sfc_L, dtype=np.int64)[1:, ...]
sfc_R_dpcm = np.asarray(sfc_R, dtype=np.int64)[1:, ...]
# sfc_L_stream, cb_sfc_L = aac_encode_huff(sfc_L_dpcm.reshape(-1, order="F"), huff_LUT_list, force_codebook=11)
# sfc_R_stream, cb_sfc_R = aac_encode_huff(sfc_R_dpcm.reshape(-1, order="F"), huff_LUT_list, force_codebook=11)
sfc_L_stream, cb_sfc_L = aac_encode_huff(sfc_L_dpcm.reshape(-1, order="F"), huff_LUT_list)
sfc_R_stream, cb_sfc_R = aac_encode_huff(sfc_R_dpcm.reshape(-1, order="F"), huff_LUT_list)
if cb_sfc_L != 11 or cb_sfc_R != 11:
raise ValueError(f"Illegal codebook value for frame: {i}: cb_sfc_l={cb_sfc_L}, cb_sfc_r={cb_sfc_R}.")
mdct_L_stream, cb_L = aac_encode_huff(np.asarray(S_L, dtype=np.int64).reshape(-1), huff_LUT_list)
mdct_R_stream, cb_R = aac_encode_huff(np.asarray(S_R, dtype=np.int64).reshape(-1), huff_LUT_list)
# Typed dict construction helps static analyzers validate the schema.
frame_out: AACSeq3Frame = {
"frame_type": frame_type,
"win_type": WIN_TYPE,
"chl": {
"tns_coeffs": np.asarray(chl_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_L, dtype=np.float64),
"G": G_L,
"sfc": sfc_L_stream,
"stream": mdct_L_stream,
"codebook": int(cb_L),
},
"chr": {
"tns_coeffs": np.asarray(chr_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_R, dtype=np.float64),
"G": G_R,
"sfc": sfc_R_stream,
"stream": mdct_R_stream,
"codebook": int(cb_R),
},
}
aac_seq.append(frame_out)
# Update psycho history (shift register)
prev2_L = prev1_L
prev1_L = frame_L
prev2_R = prev1_R
prev1_R = frame_R
prev_frame_type = frame_type prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
# Optional: store to .mat for the assignment wrapper
if filename_aac_coded is not None:
filename_aac_coded = Path(filename_aac_coded)
savemat(
str(filename_aac_coded),
{"aac_seq_3": np.array(aac_seq, dtype=object)},
do_compression=True,
)
return aac_seq
return aac_seq
+11 -1
View File
@@ -15,6 +15,8 @@
from __future__ import annotations from __future__ import annotations
# Imports # Imports
from typing import Final
from core.aac_types import WinType from core.aac_types import WinType
# Filterbank # Filterbank
@@ -28,4 +30,12 @@ WIN_TYPE: WinType = "SIN"
# ------------------------------------------------------------ # ------------------------------------------------------------
PRED_ORDER = 4 PRED_ORDER = 4
QUANT_STEP = 0.1 QUANT_STEP = 0.1
QUANT_MAX = 0.7 # 4-bit symmetric with step 0.1 -> clamp to [-0.7, +0.7] QUANT_MAX = 0.7 # 4-bit symmetric with step 0.1 -> clamp to [-0.7, +0.7]
# -----------------------------------------------------------------------------
# Psycho
# -----------------------------------------------------------------------------
NMT_DB: Final[float] = 6.0 # Noise Masking Tone (dB)
TMN_DB: Final[float] = 18.0 # Tone Masking Noise (dB)
+205 -17
View File
@@ -9,16 +9,9 @@
# cchoutou@ece.auth.gr # cchoutou@ece.auth.gr
# #
# Description: # Description:
# Level 1 AAC decoder orchestration (inverse of aac_coder_1()). # - Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
# Keeps the same functional behavior as the original level_1 implementation: # - Level 2 AAC decoder orchestration (inverse of aac_coder_1()).
# - Re-pack per-channel spectra into FrameF expected by aac_i_filter_bank()
# - IMDCT synthesis per frame
# - Overlap-add with hop=1024
# - Remove encoder boundary padding: hop at start and hop at end
# #
# Note:
# This core module returns the reconstructed samples. Writing to disk is kept
# in level_x demos.
# ------------------------------------------------------------ # ------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
@@ -29,14 +22,27 @@ import soundfile as sf
from core.aac_filterbank import aac_i_filter_bank from core.aac_filterbank import aac_i_filter_bank
from core.aac_tns import aac_i_tns from core.aac_tns import aac_i_tns
from core.aac_quantizer import aac_i_quantizer
from core.aac_huffman import aac_decode_huff
from core.aac_utils import get_table, band_limits
from material.huff_utils import load_LUT
from core.aac_types import * from core.aac_types import *
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public helpers (useful for level_x demo wrappers) # Helper for NB
# -----------------------------------------------------------------------------
def _nbands(frame_type: FrameType) -> int:
table, _ = get_table(frame_type)
wlow, _whigh, _bval, _qthr_db = band_limits(table)
return int(len(wlow))
# -----------------------------------------------------------------------------
# Public helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_unpack_seq_channels_to_frame_f(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF: def aac_unpack_seq_channels(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF:
""" """
Re-pack per-channel spectra from the Level-1 AACSeq1 schema into the stereo Re-pack per-channel spectra from the Level-1 AACSeq1 schema into the stereo
FrameF container expected by aac_i_filter_bank(). FrameF container expected by aac_i_filter_bank().
@@ -109,10 +115,14 @@ def aac_remove_padding(y_pad: StereoSignal, hop: int = 1024) -> StereoSignal:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Level 1 decoder (core) # Level 1 decoder
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoSignal: def aac_decoder_1(
aac_seq_1: AACSeq1,
filename_out: Union[str, Path],
verbose: bool = False
) -> StereoSignal:
""" """
Level-1 AAC decoder (inverse of aac_coder_1()). Level-1 AAC decoder (inverse of aac_coder_1()).
@@ -128,6 +138,8 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
Encoded sequence as produced by aac_coder_1(). Encoded sequence as produced by aac_coder_1().
filename_out : Union[str, Path] filename_out : Union[str, Path]
Output WAV filename. Assumption: 48 kHz, stereo. Output WAV filename. Assumption: 48 kHz, stereo.
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -146,6 +158,8 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
n_pad = (K - 1) * hop + win n_pad = (K - 1) * hop + win
y_pad: StereoSignal = np.zeros((n_pad, 2), dtype=np.float64) y_pad: StereoSignal = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_1): for i, fr in enumerate(aac_seq_1):
frame_type: FrameType = fr["frame_type"] frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"] win_type: WinType = fr["win_type"]
@@ -153,21 +167,32 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
chl_f = np.asarray(fr["chl"]["frame_F"], dtype=np.float64) chl_f = np.asarray(fr["chl"]["frame_F"], dtype=np.float64)
chr_f = np.asarray(fr["chr"]["frame_F"], dtype=np.float64) chr_f = np.asarray(fr["chr"]["frame_F"], dtype=np.float64)
frame_f: FrameF = aac_unpack_seq_channels_to_frame_f(frame_type, chl_f, chr_f) frame_f: FrameF = aac_unpack_seq_channels(frame_type, chl_f, chr_f)
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type) # (2048, 2) frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type) # (2048, 2)
start = i * hop start = i * hop
y_pad[start:start + win, :] += frame_t_hat y_pad[start:start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y: StereoSignal = aac_remove_padding(y_pad, hop=hop) y: StereoSignal = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
# Level 1 assumption: 48 kHz output. # Level 1 assumption: 48 kHz output.
sf.write(str(filename_out), y, 48000) sf.write(str(filename_out), y, 48000)
return y return y
def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoSignal: # -----------------------------------------------------------------------------
# Level 2 decoder
# -----------------------------------------------------------------------------
def aac_decoder_2(
aac_seq_2: AACSeq2,
filename_out: Union[str, Path],
verbose: bool = False
) -> StereoSignal:
""" """
Level-2 AAC decoder (inverse of aac_coder_2). Level-2 AAC decoder (inverse of aac_coder_2).
@@ -185,6 +210,8 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
Encoded sequence as produced by aac_coder_2(). Encoded sequence as produced by aac_coder_2().
filename_out : Union[str, Path] filename_out : Union[str, Path]
Output WAV filename. Output WAV filename.
verbose : bool
Optional argument to print encoding status
Returns Returns
------- -------
@@ -203,6 +230,8 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
n_pad = (K - 1) * hop + win n_pad = (K - 1) * hop + win
y_pad = np.zeros((n_pad, 2), dtype=np.float64) y_pad = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_2): for i, fr in enumerate(aac_seq_2):
frame_type: FrameType = fr["frame_type"] frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"] win_type: WinType = fr["win_type"]
@@ -250,8 +279,167 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
start = i * hop start = i * hop
y_pad[start : start + win, :] += frame_t_hat y_pad[start : start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y = aac_remove_padding(y_pad, hop=hop) y = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
sf.write(str(filename_out), y, 48000) sf.write(str(filename_out), y, 48000)
return y return y
def aac_decoder_3(
aac_seq_3: AACSeq3,
filename_out: Union[str, Path],
verbose: bool = False,
) -> StereoSignal:
"""
Level-3 AAC decoder (inverse of aac_coder_3).
Steps per frame:
- Huffman decode scalefactors (sfc) using codebook 11
- Huffman decode MDCT symbols (stream) using stored codebook
- iQuantizer -> MDCT coefficients after TNS
- iTNS using stored predictor coefficients
- IMDCT filterbank -> time domain
- Overlap-add, remove padding, write WAV
Parameters
----------
aac_seq_3 : AACSeq3
Encoded sequence as produced by aac_coder_3.
filename_out : Union[str, Path]
Output WAV filename.
verbose : bool
Optional argument to print encoding status
Returns
-------
StereoSignal
Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64.
"""
filename_out = Path(filename_out)
hop = 1024
win = 2048
K = len(aac_seq_3)
if K <= 0:
raise ValueError("aac_seq_3 must contain at least one frame.")
# Load Huffman LUTs once.
huff_LUT_list = load_LUT()
n_pad = (K - 1) * hop + win
y_pad = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_3):
frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"]
NB = _nbands(frame_type)
# We store G separately, so Huffman stream contains only (NB-1) DPCM differences.
sfc_len = (NB - 1) * (8 if frame_type == "ESH" else 1)
# -------------------------
# Left channel
# -------------------------
tns_L = np.asarray(fr["chl"]["tns_coeffs"], dtype=np.float64)
G_L = fr["chl"]["G"]
sfc_bits_L = fr["chl"]["sfc"]
mdct_bits_L = fr["chl"]["stream"]
cb_L = int(fr["chl"]["codebook"])
sfc_dec_L = aac_decode_huff(sfc_bits_L, 11, huff_LUT_list)[:sfc_len].astype(np.int64, copy=False)
if frame_type == "ESH":
sfc_dpcm_L = sfc_dec_L.reshape(NB - 1, 8, order="F")
sfc_L = np.zeros((NB, 8), dtype=np.int64)
Gv = np.asarray(G_L, dtype=np.float64).reshape(1, 8)
sfc_L[0, :] = Gv[0, :].astype(np.int64)
sfc_L[1:, :] = sfc_dpcm_L
else:
sfc_dpcm_L = sfc_dec_L.reshape(NB - 1, 1, order="F")
sfc_L = np.zeros((NB, 1), dtype=np.int64)
sfc_L[0, 0] = int(float(G_L))
sfc_L[1:, :] = sfc_dpcm_L
# MDCT symbols: codebook 0 means "all-zero section"
if cb_L == 0:
S_dec_L = np.zeros((1024,), dtype=np.int64)
else:
S_tmp_L = aac_decode_huff(mdct_bits_L, cb_L, huff_LUT_list).astype(np.int64, copy=False)
# Tuple coding may produce extra trailing symbols; caller knows the true length (1024).
# Also guard against short outputs by zero-padding.
if S_tmp_L.size < 1024:
S_dec_L = np.zeros((1024,), dtype=np.int64)
S_dec_L[: S_tmp_L.size] = S_tmp_L
else:
S_dec_L = S_tmp_L[:1024]
S_L = S_dec_L.reshape(1024, 1)
Xq_L = aac_i_quantizer(S_L, sfc_L, G_L, frame_type)
X_L = aac_i_tns(Xq_L, frame_type, tns_L)
# -------------------------
# Right channel
# -------------------------
tns_R = np.asarray(fr["chr"]["tns_coeffs"], dtype=np.float64)
G_R = fr["chr"]["G"]
sfc_bits_R = fr["chr"]["sfc"]
mdct_bits_R = fr["chr"]["stream"]
cb_R = int(fr["chr"]["codebook"])
sfc_dec_R = aac_decode_huff(sfc_bits_R, 11, huff_LUT_list)[:sfc_len].astype(np.int64, copy=False)
if frame_type == "ESH":
sfc_dpcm_R = sfc_dec_R.reshape(NB - 1, 8, order="F")
sfc_R = np.zeros((NB, 8), dtype=np.int64)
Gv = np.asarray(G_R, dtype=np.float64).reshape(1, 8)
sfc_R[0, :] = Gv[0, :].astype(np.int64)
sfc_R[1:, :] = sfc_dpcm_R
else:
sfc_dpcm_R = sfc_dec_R.reshape(NB - 1, 1, order="F")
sfc_R = np.zeros((NB, 1), dtype=np.int64)
sfc_R[0, 0] = int(float(G_R))
sfc_R[1:, :] = sfc_dpcm_R
if cb_R == 0:
S_dec_R = np.zeros((1024,), dtype=np.int64)
else:
S_tmp_R = aac_decode_huff(mdct_bits_R, cb_R, huff_LUT_list).astype(np.int64, copy=False)
if S_tmp_R.size < 1024:
S_dec_R = np.zeros((1024,), dtype=np.int64)
S_dec_R[: S_tmp_R.size] = S_tmp_R
else:
S_dec_R = S_tmp_R[:1024]
S_R = S_dec_R.reshape(1024, 1)
Xq_R = aac_i_quantizer(S_R, sfc_R, G_R, frame_type)
X_R = aac_i_tns(Xq_R, frame_type, tns_R)
# Re-pack to stereo container and inverse filterbank
frame_f = aac_unpack_seq_channels(frame_type, np.asarray(X_L), np.asarray(X_R))
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type)
start = i * hop
y_pad[start : start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
sf.write(str(filename_out), y, 48000)
return y
+8 -75
View File
@@ -14,6 +14,7 @@
# ------------------------------------------------------------ # ------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from core.aac_utils import mdct, imdct
from core.aac_types import * from core.aac_types import *
from scipy.signal.windows import kaiser from scipy.signal.windows import kaiser
@@ -186,74 +187,6 @@ def _window_sequence(frame_type: FrameType, win_type: WinType) -> Window:
raise ValueError(f"Invalid frame_type for long window sequence: {frame_type!r}") raise ValueError(f"Invalid frame_type for long window sequence: {frame_type!r}")
def _mdct(s: TimeSignal) -> MdctCoeffs:
"""
MDCT (direct form) as specified in the assignment.
Parameters
----------
s : TimeSignal
Windowed time samples, 1-D array of length N (N = 2048 or 256).
Returns
-------
MdctCoeffs
MDCT coefficients, 1-D array of length N/2.
Definition
----------
X[k] = 2 * sum_{n=0..N-1} s[n] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
s = np.asarray(s, dtype=np.float64).reshape(-1)
N = int(s.shape[0])
if N not in (2048, 256):
raise ValueError("MDCT input length must be 2048 or 256.")
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(N // 2, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, N/2)
X = 2.0 * (s @ C) # (N/2,)
return X
def _imdct(X: MdctCoeffs) -> TimeSignal:
"""
IMDCT (direct form) as specified in the assignment.
Parameters
----------
X : MdctCoeffs
MDCT coefficients, 1-D array of length K (K = 1024 or 128).
Returns
-------
TimeSignal
Reconstructed time samples, 1-D array of length N = 2K.
Definition
----------
s[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
X = np.asarray(X, dtype=np.float64).reshape(-1)
K = int(X.shape[0])
if K not in (1024, 128):
raise ValueError("IMDCT input length must be 1024 or 128.")
N = 2 * K
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(K, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, K)
s = (2.0 / N) * (C @ X) # (N,)
return s
def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameChannelF: def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameChannelF:
""" """
ESH analysis for one channel. ESH analysis for one channel.
@@ -279,7 +212,7 @@ def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameCha
for j in range(8): for j in range(8):
start = 448 + 128 * j start = 448 + 128 * j
seg = x_ch[start:start + 256] * wS # (256,) seg = x_ch[start:start + 256] * wS # (256,)
X_esh[:, j] = _mdct(seg) # (128,) X_esh[:, j] = mdct(seg) # (128,)
return X_esh return X_esh
@@ -344,7 +277,7 @@ def _i_filter_bank_esh_channel(X_esh: FrameChannelF, win_type: WinType) -> Frame
# Each short IMDCT returns 256 samples. Place them at: # Each short IMDCT returns 256 samples. Place them at:
# start = 448 + 128*j, j=0..7 (50% overlap) # start = 448 + 128*j, j=0..7 (50% overlap)
for j in range(8): for j in range(8):
seg = _imdct(X_esh[:, j]) * wS # (256,) seg = imdct(X_esh[:, j]) * wS # (256,)
start = 448 + 128 * j start = 448 + 128 * j
out[start:start + 256] += seg out[start:start + 256] += seg
@@ -352,7 +285,7 @@ def _i_filter_bank_esh_channel(X_esh: FrameChannelF, win_type: WinType) -> Frame
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public Function prototypes (Level 1) # Public Function prototypes
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -> FrameF: def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -> FrameF:
@@ -385,8 +318,8 @@ def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -
if frame_type in ("OLS", "LSS", "LPS"): if frame_type in ("OLS", "LSS", "LPS"):
w = _window_sequence(frame_type, win_type) # length 2048 w = _window_sequence(frame_type, win_type) # length 2048
XL = _mdct(xL * w) # length 1024 XL = mdct(xL * w) # length 1024
XR = _mdct(xR * w) # length 1024 XR = mdct(xR * w) # length 1024
out = np.empty((1024, 2), dtype=np.float64) out = np.empty((1024, 2), dtype=np.float64)
out[:, 0] = XL out[:, 0] = XL
out[:, 1] = XR out[:, 1] = XR
@@ -430,8 +363,8 @@ def aac_i_filter_bank(frame_F: FrameF, frame_type: FrameType, win_type: WinType)
w = _window_sequence(frame_type, win_type) w = _window_sequence(frame_type, win_type)
xL = _imdct(frame_F[:, 0]) * w xL = imdct(frame_F[:, 0]) * w
xR = _imdct(frame_F[:, 1]) * w xR = imdct(frame_F[:, 1]) * w
out = np.empty((2048, 2), dtype=np.float64) out = np.empty((2048, 2), dtype=np.float64)
out[:, 0] = xL out[:, 0] = xL
+112
View File
@@ -0,0 +1,112 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Huffman wrappers (Level 3)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Thin wrappers around the provided Huffman utilities (material/huff_utils.py)
# so that the API matches the assignment text.
#
# Exposed API (assignment):
# huff_sec, huff_codebook = aac_encode_huff(coeff_sec, huff_LUT_list, force_codebook)
# dec_coeffs = aac_decode_huff(huff_sec, huff_codebook, huff_LUT_list)
#
# Notes:
# - Huffman coding operates on tuples. Therefore, decode(encode(x)) may return
# extra trailing symbols due to tuple padding. The AAC decoder knows the
# true section length from side information (band limits) and truncates.
# ------------------------------------------------------------
from __future__ import annotations
from typing import Any
import numpy as np
from material.huff_utils import encode_huff, decode_huff
def aac_encode_huff(
coeff_sec: np.ndarray,
huff_LUT_list: list[dict[str, Any]],
force_codebook: int | None = None,
) -> tuple[str, int]:
"""
Huffman-encode a section of coefficients (MDCT symbols or scalefactors).
Parameters
----------
coeff_sec : np.ndarray
Coefficient section to be encoded. Any shape is accepted; the input
is flattened and treated as a 1-D sequence of int64 symbols.
huff_LUT_list : list[dict[str, Any]]
List of Huffman Look-Up Tables (LUTs) as returned by material.load_LUT().
Index corresponds to codebook id (typically 1..11, with 0 reserved).
force_codebook : int | None
If provided, forces the use of this Huffman codebook. In the assignment,
scalefactors are encoded with codebook 11. For MDCT coefficients, this
argument is usually omitted (auto-selection).
Returns
-------
tuple[str, int]
(huff_sec, huff_codebook)
- huff_sec: bitstream as a string of '0'/'1'
- huff_codebook: codebook id used by the encoder
"""
coeff_sec_arr = np.asarray(coeff_sec, dtype=np.int64).reshape(-1)
if force_codebook is None:
# Provided utility returns (bitstream, codebook) in the auto-selection case.
huff_sec, huff_codebook = encode_huff(coeff_sec_arr, huff_LUT_list)
return str(huff_sec), int(huff_codebook)
# Provided utility returns ONLY the bitstream when force_codebook is set.
cb = int(force_codebook)
huff_sec = encode_huff(coeff_sec_arr, huff_LUT_list, force_codebook=cb)
return str(huff_sec), cb
def aac_decode_huff(
huff_sec: str | np.ndarray,
huff_codebook: int,
huff_LUT: list[dict[str, Any]],
) -> np.ndarray:
"""
Huffman-decode a bitstream using the specified codebook.
Parameters
----------
huff_sec : str | np.ndarray
Huffman bitstream. Typically a string of '0'/'1'. If an array is provided,
it is passed through to the provided decoder.
huff_codebook : int
Codebook id that was returned by aac_encode_huff.
Codebook 0 represents an all-zero section.
huff_LUT : list[dict[str, Any]]
Huffman LUT list as returned by material.load_LUT().
Returns
-------
np.ndarray
Decoded coefficients as a 1-D np.int64 array.
Note: Due to tuple coding, the decoded array may contain extra trailing
padding symbols. The caller must truncate to the known section length.
"""
cb = int(huff_codebook)
if cb == 0:
# Codebook 0 represents an all-zero section. The decoded length is not
# recoverable from the bitstream alone; the caller must expand/truncate.
return np.zeros((0,), dtype=np.int64)
if cb < 0 or cb >= len(huff_LUT):
raise ValueError(f"Invalid Huffman codebook index: {cb}")
lut = huff_LUT[cb]
dec = decode_huff(huff_sec, lut)
return np.asarray(dec, dtype=np.int64).reshape(-1)
+441
View File
@@ -0,0 +1,441 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Psychoacoustic Model
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Psychoacoustic model for ONE channel, based on the assignment notes (Section 2.4).
#
# Public API:
# SMR = aac_psycho(frame_T, frame_type, frame_T_prev_1, frame_T_prev_2)
#
# Output:
# - For long frames ("OLS", "LSS", "LPS"): SMR has shape (69,)
# - For short frames ("ESH"): SMR has shape (42, 8) (one column per subframe)
#
# Notes:
# - Uses Bark band tables from material/TableB219.mat:
# * B219a for long windows (69 bands, N=2048 FFT, N/2=1024 bins)
# * B219b for short windows (42 bands, N=256 FFT, N/2=128 bins)
# - Applies a Hann window in time domain before FFT magnitude/phase extraction.
# - Implements:
# spreading function -> band spreading -> tonality index -> masking thresholds -> SMR.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from core.aac_utils import band_limits, get_table
from core.aac_configuration import NMT_DB, TMN_DB
from core.aac_types import *
# -----------------------------------------------------------------------------
# Spreading function
# -----------------------------------------------------------------------------
def _spreading_matrix(bval: BandValueArray) -> FloatArray:
"""
Compute the spreading function matrix between psychoacoustic bands.
The spreading function describes how energy in one critical band masks
nearby bands. The formula follows the assignment pseudo-code.
Parameters
----------
bval : BandValueArray
Bark value per band, shape (B,).
Returns
-------
FloatArray
Spreading matrix S of shape (B, B), where:
S[bb, b] quantifies the contribution of band bb masking band b.
"""
bval = np.asarray(bval, dtype=np.float64).reshape(-1)
B = int(bval.shape[0])
spread = np.zeros((B, B), dtype=np.float64)
for b in range(B):
for bb in range(B):
# tmpx depends on direction (asymmetric spreading)
if bb >= b:
tmpx = 3.0 * (bval[bb] - bval[b])
else:
tmpx = 1.5 * (bval[bb] - bval[b])
# tmpz uses the "min(..., 0)" nonlinearity exactly as in the notes
tmpz = 8.0 * min((tmpx - 0.5) ** 2 - 2.0 * (tmpx - 0.5), 0.0)
tmpy = 15.811389 + 7.5 * (tmpx + 0.474) - 17.5 * np.sqrt(1.0 + (tmpx + 0.474) ** 2)
# Clamp very small values (below -100 dB) to 0 contribution
if tmpy < -100.0:
spread[bb, b] = 0.0
else:
spread[bb, b] = 10.0 ** ((tmpz + tmpy) / 10.0)
return spread
# -----------------------------------------------------------------------------
# Windowing + FFT feature extraction
# -----------------------------------------------------------------------------
def _hann_window(N: int) -> FloatArray:
"""
Hann window as specified in the notes:
w[n] = 0.5 - 0.5*cos(2*pi*(n + 0.5)/N)
Parameters
----------
N : int
Window length.
Returns
-------
FloatArray
1-D array of shape (N,), dtype float64.
"""
n = np.arange(N, dtype=np.float64)
return 0.5 - 0.5 * np.cos((2.0 * np.pi / N) * (n + 0.5))
def _r_phi_from_time(x: FrameChannelT, N: int) -> tuple[FloatArray, FloatArray]:
"""
Compute FFT magnitude r(w) and phase phi(w) for bins w = 0 .. N/2-1.
Processing:
1) Apply Hann window in time domain.
2) Compute N-point FFT.
3) Keep only the positive-frequency bins [0 .. N/2-1].
Parameters
----------
x : FrameChannelT
Time-domain samples, shape (N,).
N : int
FFT size (2048 or 256).
Returns
-------
r : FloatArray
Magnitude spectrum for bins 0 .. N/2-1, shape (N/2,).
phi : FloatArray
Phase spectrum for bins 0 .. N/2-1, shape (N/2,).
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
if x.shape[0] != N:
raise ValueError(f"Expected time vector of length {N}, got {x.shape[0]}.")
w = _hann_window(N)
X = np.fft.fft(x * w, n=N)
Xp = X[: N // 2]
r = np.abs(Xp).astype(np.float64, copy=False)
phi = np.angle(Xp).astype(np.float64, copy=False)
return r, phi
def _predictability(
r: FloatArray,
phi: FloatArray,
r_m1: FloatArray,
phi_m1: FloatArray,
r_m2: FloatArray,
phi_m2: FloatArray,
) -> FloatArray:
"""
Compute predictability c(w) per spectral bin.
The notes define:
r_pred(w) = 2*r_{-1}(w) - r_{-2}(w)
phi_pred(w) = 2*phi_{-1}(w) - phi_{-2}(w)
c(w) = |X(w) - X_pred(w)| / (r(w) + |r_pred(w)|)
where X(w) is represented in polar form using r(w), phi(w).
Parameters
----------
r, phi : FloatArray
Current magnitude and phase, shape (N/2,).
r_m1, phi_m1 : FloatArray
Previous magnitude and phase, shape (N/2,).
r_m2, phi_m2 : FloatArray
Pre-previous magnitude and phase, shape (N/2,).
Returns
-------
FloatArray
Predictability c(w), shape (N/2,).
"""
r_pred = 2.0 * r_m1 - r_m2
phi_pred = 2.0 * phi_m1 - phi_m2
num = np.sqrt(
(r * np.cos(phi) - r_pred * np.cos(phi_pred)) ** 2
+ (r * np.sin(phi) - r_pred * np.sin(phi_pred)) ** 2
)
den = r + np.abs(r_pred) + 1e-12 # avoid division-by-zero without altering behavior
return (num / den).astype(np.float64, copy=False)
# -----------------------------------------------------------------------------
# Band-domain aggregation
# -----------------------------------------------------------------------------
def _band_energy_and_pred(
r: FloatArray,
c: FloatArray,
wlow: BandIndexArray,
whigh: BandIndexArray,
) -> tuple[FloatArray, FloatArray]:
"""
Aggregate spectral bin quantities into psychoacoustic bands.
Definitions (notes):
e(b) = sum_{w=wlow(b)..whigh(b)} r(w)^2
c_num(b) = sum_{w=wlow(b)..whigh(b)} c(w) * r(w)^2
The band predictability c(b) is later computed after spreading as:
cb(b) = ct(b) / ecb(b)
Parameters
----------
r : FloatArray
Magnitude spectrum, shape (N/2,).
c : FloatArray
Predictability per bin, shape (N/2,).
wlow, whigh : BandIndexArray
Band limits (inclusive indices), shape (B,).
Returns
-------
e_b : FloatArray
Band energies e(b), shape (B,).
c_num_b : FloatArray
Weighted predictability numerators c_num(b), shape (B,).
"""
r2 = (r * r).astype(np.float64, copy=False)
B = int(wlow.shape[0])
e_b = np.zeros(B, dtype=np.float64)
c_num_b = np.zeros(B, dtype=np.float64)
for b in range(B):
a = int(wlow[b])
z = int(whigh[b])
seg_r2 = r2[a : z + 1]
e_b[b] = float(np.sum(seg_r2))
c_num_b[b] = float(np.sum(c[a : z + 1] * seg_r2))
return e_b, c_num_b
def _psycho_window(
time_x: FrameChannelT,
prev1_x: FrameChannelT,
prev2_x: FrameChannelT,
*,
N: int,
table: BarkTable,
) -> FloatArray:
"""
Compute SMR for one FFT analysis window (N=2048 for long, N=256 for short).
This implements the pipeline described in the notes:
- FFT magnitude/phase
- predictability per bin
- band energies and predictability
- band spreading
- tonality index tb(b)
- masking threshold (noise + threshold in quiet)
- SMR(b) = e(b) / np(b)
Parameters
----------
time_x : FrameChannelT
Current time-domain samples, shape (N,).
prev1_x : FrameChannelT
Previous time-domain samples, shape (N,).
prev2_x : FrameChannelT
Pre-previous time-domain samples, shape (N,).
N : int
FFT size.
table : BarkTable
Psychoacoustic band table (B219a or B219b).
Returns
-------
FloatArray
SMR per band, shape (B,).
"""
wlow, whigh, bval, qthr_db = band_limits(table)
spread = _spreading_matrix(bval)
# FFT features for current and history windows
r, phi = _r_phi_from_time(time_x, N)
r_m1, phi_m1 = _r_phi_from_time(prev1_x, N)
r_m2, phi_m2 = _r_phi_from_time(prev2_x, N)
# Predictability per bin
c_w = _predictability(r, phi, r_m1, phi_m1, r_m2, phi_m2)
# Aggregate into psycho bands
e_b, c_num_b = _band_energy_and_pred(r, c_w, wlow, whigh)
# Spread energies and predictability across bands:
# ecb(b) = sum_bb e(bb) * S(bb, b)
# ct(b) = sum_bb c_num(bb) * S(bb, b)
ecb = spread.T @ e_b
ct = spread.T @ c_num_b
# Band predictability after spreading: cb(b) = ct(b) / ecb(b)
cb = ct / (ecb + 1e-12)
# Normalized energy term:
# en(b) = ecb(b) / sum_bb S(bb, b)
spread_colsum = np.sum(spread, axis=0)
en = ecb / (spread_colsum + 1e-12)
# Tonality index (clamped to [0, 1])
tb = -0.299 - 0.43 * np.log(np.maximum(cb, 1e-12))
tb = np.clip(tb, 0.0, 1.0)
# Required SNR per band (dB): interpolate between TMN and NMT
snr_b = tb * TMN_DB + (1.0 - tb) * NMT_DB
bc = 10.0 ** (-snr_b / 10.0)
# Noise masking threshold estimate (power domain)
nb = en * bc
# Threshold in quiet (convert from dB to power domain):
# qthr_power = eps * (N/2) * 10^(qthr_db/10)
qthr_power = np.finfo('float').eps * (N / 2.0) * (10.0 ** (qthr_db / 10.0))
# Final masking threshold per band:
# np(b) = max(nb(b), qthr(b))
npart = np.maximum(nb, qthr_power)
# Signal-to-mask ratio:
# SMR(b) = e(b) / np(b)
smr = e_b / (npart + 1e-12)
return smr.astype(np.float64, copy=False)
# -----------------------------------------------------------------------------
# ESH window slicing (match filterbank conventions)
# -----------------------------------------------------------------------------
def _esh_subframes(x_2048: FrameChannelT) -> list[FrameChannelT]:
"""
Extract the 8 overlapping 256-sample short windows used by AAC ESH.
The project convention (matching the filterbank) is:
start_j = 448 + 128*j, for j = 0..7
subframe_j = x[start_j : start_j + 256]
This selects the central 1152-sample region [448, 1600) and produces
8 windows with 50% overlap.
Parameters
----------
x_2048 : FrameChannelT
Time-domain channel frame, shape (2048,).
Returns
-------
list[FrameChannelT]
List of 8 subframes, each of shape (256,).
"""
x_2048 = np.asarray(x_2048, dtype=np.float64).reshape(-1)
if x_2048.shape[0] != 2048:
raise ValueError("ESH requires 2048-sample input frames.")
subs: list[FrameChannelT] = []
for j in range(8):
start = 448 + 128 * j
subs.append(x_2048[start : start + 256])
return subs
# -----------------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------------
def aac_psycho(
frame_T: FrameChannelT,
frame_type: FrameType,
frame_T_prev_1: FrameChannelT,
frame_T_prev_2: FrameChannelT,
) -> FloatArray:
"""
Psychoacoustic model for ONE channel.
Parameters
----------
frame_T : FrameChannelT
Current time-domain channel frame, shape (2048,).
For "ESH", the 8 short windows are derived internally.
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
frame_T_prev_1 : FrameChannelT
Previous time-domain channel frame, shape (2048,).
frame_T_prev_2 : FrameChannelT
Pre-previous time-domain channel frame, shape (2048,).
Returns
-------
FloatArray
Signal-to-Mask Ratio (SMR), per psychoacoustic band.
- If frame_type == "ESH": shape (42, 8)
- Else: shape (69,)
"""
frame_T = np.asarray(frame_T, dtype=np.float64).reshape(-1)
frame_T_prev_1 = np.asarray(frame_T_prev_1, dtype=np.float64).reshape(-1)
frame_T_prev_2 = np.asarray(frame_T_prev_2, dtype=np.float64).reshape(-1)
if frame_T.shape[0] != 2048 or frame_T_prev_1.shape[0] != 2048 or frame_T_prev_2.shape[0] != 2048:
raise ValueError("aac_psycho expects 2048-sample frames for current/prev1/prev2.")
table, N = get_table(frame_type)
# Long frame types: compute one SMR vector (69 bands)
if frame_type != "ESH":
return _psycho_window(frame_T, frame_T_prev_1, frame_T_prev_2, N=N, table=table)
# ESH: compute 8 SMR vectors (42 bands each), one per short subframe.
#
# The notes use short-window history for predictability:
# - For j=0: use previous frame's subframes (7, 6)
# - For j=1: use current subframe 0 and previous frame's subframe 7
# - For j>=2: use current subframes (j-1, j-2)
#
# This matches the "within-frame history" convention commonly used in
# simplified psycho models for ESH.
cur_subs = _esh_subframes(frame_T)
prev1_subs = _esh_subframes(frame_T_prev_1)
B = int(table.shape[0]) # expected 42
smr_out = np.zeros((B, 8), dtype=np.float64)
for j in range(8):
if j == 0:
x_m1 = prev1_subs[7]
x_m2 = prev1_subs[6]
elif j == 1:
x_m1 = cur_subs[0]
x_m2 = prev1_subs[7]
else:
x_m1 = cur_subs[j - 1]
x_m2 = cur_subs[j - 2]
smr_out[:, j] = _psycho_window(cur_subs[j], x_m1, x_m2, N=256, table=table)
return smr_out
+600
View File
@@ -0,0 +1,600 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Quantizer / iQuantizer (Level 3)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Implements AAC quantizer and inverse quantizer for one channel.
# Based on assignment section 2.6 (Eq. 12-15).
#
# Notes:
# - Bit reservoir is not implemented (assignment simplification).
# - Scalefactor bands are assumed equal to psychoacoustic bands
# (Table B.2.1.9a / B.2.1.9b from TableB219.mat).
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from core.aac_utils import get_table, band_limits
from core.aac_types import *
# -----------------------------------------------------------------------------
# Constants (assignment)
# -----------------------------------------------------------------------------
MAGIC_NUMBER: float = 0.4054
EPS: float = 1e-12
MAX_SF_DELTA:int = 60
# -----------------------------------------------------------------------------
# Helpers: ESH packing/unpacking (128x8 <-> 1024x1)
# -----------------------------------------------------------------------------
def _esh_pack(x_128x8: FloatArray) -> FloatArray:
"""
Pack ESH coefficients (128 x 8) into a single long vector (1024 x 1).
Packing order:
Columns are concatenated in subframe order (0..7), column-major.
Parameters
----------
x_128x8 : FloatArray
ESH coefficients, shape (128, 8).
Returns
-------
FloatArray
Packed coefficients, shape (1024, 1).
"""
x_128x8 = np.asarray(x_128x8, dtype=np.float64)
if x_128x8.shape != (128, 8):
raise ValueError("ESH pack expects shape (128, 8).")
return x_128x8.reshape(1024, 1, order="F")
def _esh_unpack(x_1024x1: FloatArray) -> FloatArray:
"""
Unpack a packed ESH vector (1024 elements) back to shape (128, 8).
Parameters
----------
x_1024x1 : FloatArray
Packed ESH vector, shape (1024,) or (1024, 1) after flattening.
Returns
-------
FloatArray
Unpacked ESH coefficients, shape (128, 8).
"""
x_1024x1 = np.asarray(x_1024x1, dtype=np.float64).reshape(-1)
if x_1024x1.shape[0] != 1024:
raise ValueError("ESH unpack expects 1024 elements.")
return x_1024x1.reshape(128, 8, order="F")
# -----------------------------------------------------------------------------
# Core quantizer formulas (Eq. 12, Eq. 13)
# -----------------------------------------------------------------------------
def _quantize_symbol(x: FloatArray, alpha: float) -> QuantizedSymbols:
"""
Quantize MDCT coefficients to integer symbols S(k).
Implements Eq. (12):
S(k) = sgn(X(k)) * int( (|X(k)| * 2^(-alpha/4))^(3/4) + MAGIC_NUMBER )
Parameters
----------
x : FloatArray
MDCT coefficients for a contiguous set of spectral lines.
Shape: (N,)
alpha : float
Scalefactor gain for the corresponding scalefactor band.
Returns
-------
QuantizedSymbols
Quantized symbols S(k) as int64, shape (N,).
"""
x = np.asarray(x, dtype=np.float64)
scale = 2.0 ** (-0.25 * float(alpha))
ax = np.abs(x) * scale
y = np.power(ax, 0.75, dtype=np.float64)
# "int" in the assignment corresponds to truncation.
q = np.floor(y + MAGIC_NUMBER).astype(np.int64)
return (np.sign(x).astype(np.int64) * q).astype(np.int64)
def _dequantize_symbol(S: QuantizedSymbols, alpha: float) -> FloatArray:
"""
Inverse quantizer (dequantization of symbols).
Implements Eq. (13):
Xhat(k) = sgn(S(k)) * |S(k)|^(4/3) * 2^(alpha/4)
Parameters
----------
S : QuantizedSymbols
Quantized symbols S(k), int64, shape (N,).
alpha : float
Scalefactor gain for the corresponding scalefactor band.
Returns
-------
FloatArray
Reconstructed MDCT coefficients Xhat(k), float64, shape (N,).
"""
S = np.asarray(S, dtype=np.int64)
scale = 2.0 ** (0.25 * float(alpha))
aS = np.abs(S).astype(np.float64)
y = np.power(aS, 4.0 / 3.0, dtype=np.float64)
return (np.sign(S).astype(np.float64) * y * scale).astype(np.float64)
# -----------------------------------------------------------------------------
# Alpha initialization (Eq. 14)
# -----------------------------------------------------------------------------
def _initial_alpha_hat(X: "FloatArray", MQ: int = 8191) -> int:
"""
Compute the initial scalefactor estimate alpha_hat for a frame.
The assignment proposes the following first approximation (Equation 14):
alpha_hat = (16/3) * log2( max_k(|X(k)|)^(3/4) / MQ )
where max_k runs over all MDCT coefficients of the frame (not per band),
and MQ is the maximum quantization level parameter (2*MQ + 1 levels).
Parameters
----------
X : FloatArray
MDCT coefficients of one frame (or one ESH subframe), shape (N,).
MQ : int
Quantizer parameter (default 8191, as per assignment).
Returns
-------
int
Integer alpha_hat (rounded to nearest integer).
"""
x_max = float(np.max(np.abs(X)))
if x_max <= 0.0:
return 0
alpha_hat = (16.0 / 3.0) * np.log2((x_max ** (3.0 / 4.0)) / float(MQ))
return int(np.round(alpha_hat))
# -----------------------------------------------------------------------------
# Band utilities
# -----------------------------------------------------------------------------
def _band_slices(frame_type: FrameType) -> list[tuple[int, int]]:
"""
Return scalefactor band ranges [wlow, whigh] (inclusive) for the given frame type.
These are derived from the psychoacoustic tables (TableB219),
and map directly to MDCT indices:
- long: 0..1023
- short (ESH subframe): 0..127
Parameters
----------
frame_type : FrameType
Frame type ("OLS", "LSS", "ESH", "LPS").
Returns
-------
list[tuple[int, int]]
List of (lo, hi) inclusive index pairs for each band.
"""
table, _Nfft = get_table(frame_type)
wlow, whigh, _bval, _qthr_db = band_limits(table)
bands: list[tuple[int, int]] = []
for lo, hi in zip(wlow, whigh):
bands.append((int(lo), int(hi)))
return bands
def _band_energy(x: FloatArray, lo: int, hi: int) -> float:
"""
Compute energy of a spectral segment x[lo:hi+1].
Parameters
----------
x : FloatArray
MDCT coefficient vector.
lo, hi : int
Inclusive index range.
Returns
-------
float
Sum of squares (energy) within the band.
"""
sec = x[lo : hi + 1]
return float(np.sum(sec * sec))
def _psychoacoustic_threshold(
X: FloatArray,
SMR_col: FloatArray,
bands: list[tuple[int, int]],
) -> FloatArray:
"""
Compute psychoacoustic thresholds T(b) per band.
Uses:
P(b) = sum_{k in band} X(k)^2
T(b) = P(b) / SMR(b)
Parameters
----------
X : FloatArray
MDCT coefficients for a frame (long) or one ESH subframe (short).
SMR_col : FloatArray
SMR values for this frame/subframe, shape (NB,).
bands : list[tuple[int, int]]
Band index ranges.
Returns
-------
FloatArray
Threshold vector T(b), shape (NB,).
"""
nb = len(bands)
T = np.zeros((nb,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
P = _band_energy(X, lo, hi)
smr = float(SMR_col[b])
if smr <= EPS:
T[b] = 0.0
else:
T[b] = P / smr
return T
# -----------------------------------------------------------------------------
# Alpha selection per band + neighbor-difference constraint
# -----------------------------------------------------------------------------
def _best_alpha_for_band(
X: "FloatArray", lo: int, hi: int, T_b: float,
alpha_hat: int, alpha_prev: int, alpha_min: int, alpha_max: int,
) -> int:
"""
Determine the band-wise scalefactor alpha(b) following the assignment.
Procedure:
- Start from a frame-wise initial estimate alpha_hat.
- Iteratively increase alpha(b) by 1 as long as the quantization error power
stays below the psychoacoustic threshold T(b): P_e(b) = sum_{k in band} ( X(k) - Xhat(k) )^2
- Stop increasing alpha(b) if the neighbor constraint would be violated: |alpha(b) - alpha(b-1)| <= 60
When processing bands sequentially (low -> high), this becomes: alpha(b) <= alpha_prev + 60
Notes:
- This function does not decrease alpha if the initial value already violates
the threshold; the assignment only specifies iterative increase.
Parameters
----------
X : FloatArray
Full MDCT vector of the current (sub)frame, shape (N,).
lo, hi : int
Band index bounds (inclusive), defining the band slice.
T_b : float
Threshold T(b) for this band.
alpha_hat : int
Initial frame-wise estimate (Equation 14).
alpha_prev : int
Previously selected alpha for band b-1 (neighbor constraint reference).
alpha_min, alpha_max : int
Safeguard bounds for alpha.
Returns
-------
int
Selected integer alpha(b).
"""
if T_b <= 0.0:
return int(alpha_hat)
Xsec = X[lo : hi + 1]
# Neighbor constraint (sequential processing): alpha(b) <= alpha_prev + 60
alpha_limit = min(int(alpha_max), int(alpha_prev) + MAX_SF_DELTA)
# Start from alpha_hat, clamped to feasible range
alpha = int(alpha_hat)
alpha = max(int(alpha_min), min(alpha, int(alpha_limit)))
# Evaluate at current alpha
Ssec = _quantize_symbol(Xsec, alpha)
Xhat = _dequantize_symbol(Ssec, alpha)
Pe = float(np.sum((Xsec - Xhat) ** 2))
# If already above threshold, return current alpha (no decrease step specified)
if Pe > T_b:
return alpha
# Increase alpha while still under threshold and within constraints
while True:
alpha_next = alpha + 1
if alpha_next > alpha_limit:
break
Ssec = _quantize_symbol(Xsec, alpha_next)
Xhat = _dequantize_symbol(Ssec, alpha_next)
Pe_next = float(np.sum((Xsec - Xhat) ** 2))
if Pe_next > T_b:
break
alpha = alpha_next
return alpha
# -----------------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------------
def aac_quantizer(
frame_F: FrameChannelF,
frame_type: FrameType,
SMR: FloatArray,
) -> tuple[QuantizedSymbols, ScaleFactors, GlobalGain]:
"""
AAC quantizer for one channel (Level 3).
Quantizes MDCT coefficients (after TNS) using band-wise scalefactors derived
from psychoacoustic thresholds computed via SMR.
The implementation follows the assignment procedure:
- Compute an initial frame-wise alpha_hat using Equation (14), based on the
maximum MDCT coefficient magnitude of the (sub)frame.
- For each band b, increase alpha(b) by 1 while the quantization error power
P_e(b) stays below the threshold T(b).
- Enforce the neighbor constraint |alpha(b) - alpha(b-1)| <= 60 during the
band-by-band search (no post-processing needed).
Parameters
----------
frame_F : FrameChannelF
MDCT coefficients after TNS, one channel.
Shapes:
- Long frames: (1024,) or (1024, 1)
- ESH: (128, 8)
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
SMR : FloatArray
Signal-to-Mask Ratio per band.
Shapes:
- Long: (NB,) or (NB, 1)
- ESH: (NB, 8)
Returns
-------
S : QuantizedSymbols
Quantized symbols S(k), packed as shape (1024, 1) for all frame types.
For ESH, the 8 subframes are packed in column-major subframe layout.
sfc : ScaleFactors
DPCM-coded scalefactors:
sfc(0) = alpha(0) = G
sfc(b) = alpha(b) - alpha(b-1), for b > 0
Shapes:
- Long: (NB, 1)
- ESH: (NB, 8)
G : GlobalGain
Global gain G = alpha(0).
- Long: scalar float
- ESH: array shape (1, 8), dtype float64
"""
bands = _band_slices(frame_type)
NB = len(bands)
X = np.asarray(frame_F, dtype=np.float64)
SMR = np.asarray(SMR, dtype=np.float64)
# -------------------------------------------------------------------------
# ESH: 8 short subframes, each of length 128
# -------------------------------------------------------------------------
if frame_type == "ESH":
if X.shape != (128, 8):
raise ValueError("For ESH, frame_F must have shape (128, 8).")
if SMR.shape != (NB, 8):
raise ValueError(f"For ESH, SMR must have shape ({NB}, 8).")
S_out: QuantizedSymbols = np.zeros((1024, 1), dtype=np.int64)
sfc: ScaleFactors = np.zeros((NB, 8), dtype=np.int64)
G_arr = np.zeros((1, 8), dtype=np.float64)
# Packed output view: (128, 8) with column-major layout
S_pack = S_out[:, 0].reshape(128, 8, order="F")
for j in range(8):
Xj = X[:, j].reshape(128)
SMRj = SMR[:, j].reshape(NB)
# Compute psychoacoustic threshold T(b) for this subframe
T = _psychoacoustic_threshold(Xj, SMRj, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xj)
# Band-wise scalefactors alpha(b)
alpha = np.zeros((NB,), dtype=np.int64)
alpha_prev = int(alpha_hat)
for b, (lo, hi) in enumerate(bands):
alpha_b = _best_alpha_for_band(
X=Xj,
lo=lo,
hi=hi,
T_b=float(T[b]),
alpha_hat=int(alpha_hat),
alpha_prev=int(alpha_prev),
alpha_min=-4096,
alpha_max=4096,
)
alpha[b] = int(alpha_b)
alpha_prev = int(alpha_b)
# DPCM-coded scalefactors
G_arr[0, j] = float(alpha[0])
sfc[0, j] = int(alpha[0])
for b in range(1, NB):
sfc[b, j] = int(alpha[b] - alpha[b - 1])
# Quantize MDCT coefficients band-by-band
Sj = np.zeros((128,), dtype=np.int64)
for b, (lo, hi) in enumerate(bands):
Sj[lo : hi + 1] = _quantize_symbol(Xj[lo : hi + 1], float(alpha[b]))
# Store subframe in packed output
S_pack[:, j] = Sj
return S_out, sfc, G_arr
# -------------------------------------------------------------------------
# Long frames: OLS / LSS / LPS, length 1024
# -------------------------------------------------------------------------
if X.shape == (1024,):
Xv = X
elif X.shape == (1024, 1):
Xv = X[:, 0]
else:
raise ValueError("For non-ESH, frame_F must have shape (1024,) or (1024, 1).")
if SMR.shape == (NB,):
SMRv = SMR
elif SMR.shape == (NB, 1):
SMRv = SMR[:, 0]
else:
raise ValueError(f"For non-ESH, SMR must have shape ({NB},) or ({NB}, 1).")
# Compute psychoacoustic threshold T(b) for the long frame
T = _psychoacoustic_threshold(Xv, SMRv, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xv)
# Band-wise scalefactors alpha(b)
alpha = np.zeros((NB,), dtype=np.int64)
alpha_prev = int(alpha_hat)
for b, (lo, hi) in enumerate(bands):
alpha_b = _best_alpha_for_band(
X=Xv,
lo=lo,
hi=hi,
T_b=float(T[b]),
alpha_hat=int(alpha_hat),
alpha_prev=int(alpha_prev),
alpha_min=-4096,
alpha_max=4096,
)
alpha[b] = int(alpha_b)
alpha_prev = int(alpha_b)
# DPCM-coded scalefactors
sfc_out: ScaleFactors = np.zeros((NB, 1), dtype=np.int64)
sfc_out[0, 0] = int(alpha[0])
for b in range(1, NB):
sfc_out[b, 0] = int(alpha[b] - alpha[b - 1])
G: float = float(alpha[0])
# Quantize MDCT coefficients band-by-band
S_vec = np.zeros((1024,), dtype=np.int64)
for b, (lo, hi) in enumerate(bands):
S_vec[lo : hi + 1] = _quantize_symbol(Xv[lo : hi + 1], float(alpha[b]))
return S_vec.reshape(1024, 1), sfc_out, G
def aac_i_quantizer(
S: QuantizedSymbols,
sfc: ScaleFactors,
G: GlobalGain,
frame_type: FrameType,
) -> FrameChannelF:
"""
Inverse quantizer (iQuantizer) for one channel.
Reconstructs MDCT coefficients from quantized symbols and DPCM scalefactors.
Parameters
----------
S : QuantizedSymbols
Quantized symbols, shape (1024, 1) (or any array with 1024 elements).
sfc : ScaleFactors
DPCM-coded scalefactors.
Shapes:
- Long: (NB, 1)
- ESH: (NB, 8)
G : GlobalGain
Global gain (not strictly required if sfc includes sfc(0)=alpha(0)).
Present for API compatibility with the assignment.
frame_type : FrameType
AAC frame type.
Returns
-------
FrameChannelF
Reconstructed MDCT coefficients:
- ESH: (128, 8)
- Long: (1024, 1)
"""
bands = _band_slices(frame_type)
NB = len(bands)
S_flat = np.asarray(S, dtype=np.int64).reshape(-1)
if S_flat.shape[0] != 1024:
raise ValueError("S must contain 1024 symbols.")
if frame_type == "ESH":
sfc = np.asarray(sfc, dtype=np.int64)
if sfc.shape != (NB, 8):
raise ValueError(f"For ESH, sfc must have shape ({NB}, 8).")
S_128x8 = _esh_unpack(S_flat)
Xrec = np.zeros((128, 8), dtype=np.float64)
for j in range(8):
alpha = np.zeros((NB,), dtype=np.int64)
alpha[0] = int(sfc[0, j])
for b in range(1, NB):
alpha[b] = int(alpha[b - 1] + sfc[b, j])
Xj = np.zeros((128,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
Xj[lo : hi + 1] = _dequantize_symbol(S_128x8[lo : hi + 1, j].astype(np.int64), float(alpha[b]))
Xrec[:, j] = Xj
return Xrec
sfc = np.asarray(sfc, dtype=np.int64)
if sfc.shape != (NB, 1):
raise ValueError(f"For non-ESH, sfc must have shape ({NB}, 1).")
alpha = np.zeros((NB,), dtype=np.int64)
alpha[0] = int(sfc[0, 0])
for b in range(1, NB):
alpha[b] = int(alpha[b - 1] + sfc[b, 0])
Xrec = np.zeros((1024,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
Xrec[lo : hi + 1] = _dequantize_symbol(S_flat[lo : hi + 1], float(alpha[b]))
return Xrec.reshape(1024, 1)
-60
View File
@@ -1,60 +0,0 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - SNR dB calculator
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# This module implements SNR calculation in dB
# ------------------------------------------------------------
from __future__ import annotations
from core.aac_types import StereoSignal
import numpy as np
def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Compute overall SNR (dB) over all samples and channels after aligning lengths.
Parameters
----------
x_ref : StereoSignal
Reference stereo stream.
x_hat : StereoSignal
Reconstructed stereo stream.
Returns
-------
float
SNR in dB.
- Returns +inf if noise power is zero.
- Returns -inf if signal power is zero.
"""
x_ref = np.asarray(x_ref, dtype=np.float64)
x_hat = np.asarray(x_hat, dtype=np.float64)
if x_ref.ndim == 1:
x_ref = x_ref.reshape(-1, 1)
if x_hat.ndim == 1:
x_hat = x_hat.reshape(-1, 1)
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
x_ref = x_ref[:n, :c]
x_hat = x_hat[:n, :c]
err = x_ref - x_hat
ps = float(np.sum(x_ref * x_ref))
pn = float(np.sum(err * err))
if pn <= 0.0:
return float("inf")
if ps <= 0.0:
return float("-inf")
return float(10.0 * np.log10(ps / pn))
+2 -2
View File
@@ -173,10 +173,10 @@ def _stereo_merge(ft_l: FrameType, ft_r: FrameType) -> FrameType:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public Function prototypes (Level 1) # Public Function prototypes
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_SSC(frame_T: FrameT, next_frame_T: FrameT, prev_frame_type: FrameType) -> FrameType: def aac_ssc(frame_T: FrameT, next_frame_T: FrameT, prev_frame_type: FrameType) -> FrameType:
""" """
Sequence Segmentation Control (SSC). Sequence Segmentation Control (SSC).
+12 -47
View File
@@ -30,9 +30,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Tuple from typing import Tuple
import numpy as np from core.aac_utils import load_b219_tables
from scipy.io import loadmat
from core.aac_configuration import PRED_ORDER, QUANT_STEP, QUANT_MAX from core.aac_configuration import PRED_ORDER, QUANT_STEP, QUANT_MAX
from core.aac_types import * from core.aac_types import *
@@ -40,43 +38,8 @@ from core.aac_types import *
# Private helpers # Private helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
_B219_CACHE: dict[str, FloatArray] | None = None
def _band_ranges(k_count: int) -> BandRanges:
def _load_b219_tables() -> dict[str, FloatArray]:
"""
Load TableB219.mat and cache the contents.
The project layout guarantees that a 'material' directory is discoverable
from the current working directory (tests and level_123 entrypoints).
Returns
-------
dict[str, FloatArray]
Keys:
- "B219a": long bands table (for K=1024 MDCT lines)
- "B219b": short bands table (for K=128 MDCT lines)
"""
global _B219_CACHE
if _B219_CACHE is not None:
return _B219_CACHE
mat_path = Path("material") / "TableB219.mat"
if not mat_path.exists():
raise FileNotFoundError("Could not locate material/TableB219.mat in the current working directory.")
d = loadmat(str(mat_path))
if "B219a" not in d or "B219b" not in d:
raise ValueError("TableB219.mat missing required variables B219a and/or B219b.")
_B219_CACHE = {
"B219a": np.asarray(d["B219a"], dtype=np.float64),
"B219b": np.asarray(d["B219b"], dtype=np.float64),
}
return _B219_CACHE
def _band_ranges_for_kcount(k_count: int) -> BandRanges:
""" """
Return Bark band index ranges [start, end] (inclusive) for the given MDCT line count. Return Bark band index ranges [start, end] (inclusive) for the given MDCT line count.
@@ -92,7 +55,7 @@ def _band_ranges_for_kcount(k_count: int) -> BandRanges:
BandRanges (list[tuple[int, int]]) BandRanges (list[tuple[int, int]])
Each tuple is (start_k, end_k) inclusive. Each tuple is (start_k, end_k) inclusive.
""" """
tables = _load_b219_tables() tables = load_b219_tables()
if k_count == 1024: if k_count == 1024:
tbl = tables["B219a"] tbl = tables["B219a"]
elif k_count == 128: elif k_count == 128:
@@ -103,7 +66,7 @@ def _band_ranges_for_kcount(k_count: int) -> BandRanges:
start = tbl[:, 1].astype(int) start = tbl[:, 1].astype(int)
end = tbl[:, 2].astype(int) end = tbl[:, 2].astype(int)
ranges: list[tuple[int, int]] = [(int(s), int(e)) for s, e in zip(start, end)] ranges: BandRanges = [(int(s), int(e)) for s, e in zip(start, end)]
for s, e in ranges: for s, e in ranges:
if s < 0 or e < s or e >= k_count: if s < 0 or e < s or e >= k_count:
@@ -154,7 +117,7 @@ def _compute_sw(x: MdctCoeffs) -> MdctCoeffs:
x = np.asarray(x, dtype=np.float64).reshape(-1) x = np.asarray(x, dtype=np.float64).reshape(-1)
k_count = int(x.shape[0]) k_count = int(x.shape[0])
bands = _band_ranges_for_kcount(k_count) bands = _band_ranges(k_count)
sw = np.zeros(k_count, dtype=np.float64) sw = np.zeros(k_count, dtype=np.float64)
for s, e in bands: for s, e in bands:
@@ -384,7 +347,7 @@ def _apply_itns_iir(y: MdctCoeffs, a_q: MdctCoeffs) -> MdctCoeffs:
return x_hat return x_hat
def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]: def _tns_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
""" """
TNS for a single MDCT vector (one long frame or one short subframe). TNS for a single MDCT vector (one long frame or one short subframe).
@@ -411,7 +374,9 @@ def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
sw = _compute_sw(x) sw = _compute_sw(x)
eps = 1e-12 eps = 1e-12
xw = np.where(sw > eps, x / sw, 0.0) xw = np.zeros_like(x, dtype=np.float64)
mask = sw > eps
np.divide(x, sw, out=xw, where=mask)
a = _lpc_coeffs(xw, PRED_ORDER) a = _lpc_coeffs(xw, PRED_ORDER)
a_q = _quantize_coeffs(a) a_q = _quantize_coeffs(a)
@@ -425,7 +390,7 @@ def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public Functions (Level 2) # Public Functions
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChannelF, TnsCoeffs]: def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChannelF, TnsCoeffs]:
@@ -465,7 +430,7 @@ def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChan
a_out = np.empty((PRED_ORDER, 8), dtype=np.float64) a_out = np.empty((PRED_ORDER, 8), dtype=np.float64)
for j in range(8): for j in range(8):
y[:, j], a_out[:, j] = _tns_one_vector(x[:, j]) y[:, j], a_out[:, j] = _tns_vector(x[:, j])
return y, a_out return y, a_out
@@ -478,7 +443,7 @@ def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChan
else: else:
raise ValueError('For non-ESH, frame_F_in must have shape (1024,) or (1024, 1).') raise ValueError('For non-ESH, frame_F_in must have shape (1024,) or (1024, 1).')
y_vec, a_q = _tns_one_vector(x_vec) y_vec, a_q = _tns_vector(x_vec)
if out_shape == (1024,): if out_shape == (1024,):
y_out = y_vec y_out = y_vec
+129
View File
@@ -193,6 +193,61 @@ Bark-band index ranges [start, end] (inclusive) for MDCT lines.
Used by TNS to map MDCT indices k to Bark bands. Used by TNS to map MDCT indices k to Bark bands.
""" """
BarkTable: TypeAlias = FloatArray
"""
Psychoacoustic Bark band table loaded from TableB219.mat.
Typical shapes:
- Long: (69, 6)
- Short: (42, 6)
"""
BandIndexArray: TypeAlias = NDArray[np.int_]
"""
Array of FFT bin indices per psychoacoustic band.
"""
BandValueArray: TypeAlias = FloatArray
"""
Per-band psychoacoustic values (e.g. Bark position, thresholds).
"""
# Quantizer-related semantic aliases
QuantizedSymbols: TypeAlias = NDArray[np.generic]
"""
Quantized MDCT symbols S(k).
Shapes:
- Always (1024, 1) at the quantizer output (ESH packed to 1024 symbols).
"""
ScaleFactors: TypeAlias = NDArray[np.generic]
"""
DPCM-coded scalefactors sfc(b) = alpha(b) - alpha(b-1).
Shapes:
- Long frames: (NB, 1)
- ESH frames: (NB, 8)
"""
GlobalGain: TypeAlias = float | NDArray[np.generic]
"""
Global gain G = alpha(0).
- Long frames: scalar float
- ESH frames: array shape (1, 8)
"""
# Huffman semantic aliases
HuffmanBitstream: TypeAlias = str
"""Huffman-coded bitstream stored as a string of '0'/'1'."""
HuffmanCodebook: TypeAlias = int
"""Huffman codebook id (e.g., 0..11)."""
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Level 1 AAC sequence payload types # Level 1 AAC sequence payload types
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -280,3 +335,77 @@ Level 2 adds:
and stores: and stores:
- per-channel "frame_F" after applying TNS. - per-channel "frame_F" after applying TNS.
""" """
# -----------------------------------------------------------------------------
# Level 3 AAC sequence payload types (Quantizer + Huffman)
# -----------------------------------------------------------------------------
class AACChannelFrameF3(TypedDict):
"""
Per-channel payload for aac_seq_3[i]["chl"] or ["chr"] (Level 3).
Keys
----
tns_coeffs:
Quantized TNS predictor coefficients for ONE channel.
Shapes:
- ESH: (PRED_ORDER, 8)
- else: (PRED_ORDER, 1)
T:
Psychoacoustic thresholds per band.
Shapes:
- ESH: (NB, 8)
- else: (NB, 1)
Note: Stored for completeness / debugging; not entropy-coded.
G:
Quantized global gains.
Shapes:
- ESH: (1, 8) (one per short subframe)
- else: scalar (or compatible np scalar)
sfc:
Huffman-coded scalefactor differences (DPCM sequence).
stream:
Huffman-coded MDCT quantized symbols S(k) (packed to 1024 symbols).
codebook:
Huffman codebook id used for MDCT symbols (stream).
(Scalefactors typically use fixed codebook 11 and do not need to store it.)
"""
tns_coeffs: TnsCoeffs
T: FloatArray
G: FloatArray | float
sfc: HuffmanBitstream
stream: HuffmanBitstream
codebook: HuffmanCodebook
class AACSeq3Frame(TypedDict):
"""
One frame dictionary element of aac_seq_3 (Level 3).
"""
frame_type: FrameType
win_type: WinType
chl: AACChannelFrameF3
chr: AACChannelFrameF3
AACSeq3: TypeAlias = List[AACSeq3Frame]
"""
AAC sequence for Level 3:
List of length K (K = number of frames).
Each element is a dict with keys:
- "frame_type", "win_type", "chl", "chr"
Level 3 adds (per channel):
- "tns_coeffs"
- "T" thresholds (not entropy-coded)
- "G" global gain(s)
- "sfc" Huffman-coded scalefactor differences
- "stream" Huffman-coded MDCT quantized symbols
- "codebook" Huffman codebook for MDCT symbols
"""
+306
View File
@@ -0,0 +1,306 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - AAC Utilities
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Shared utility functions used across AAC encoder/decoder levels.
#
# This module currently provides:
# - MDCT / IMDCT conversions
# - Signal-to-Noise Ratio (SNR) computation in dB
# - Loading and access helpers for psychoacoustic band tables
# (TableB219.mat, Tables B.2.1.9a / B.2.1.9b of the AAC specification)
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from pathlib import Path
from scipy.io import loadmat
from core.aac_types import *
# -----------------------------------------------------------------------------
# Global cached data
# -----------------------------------------------------------------------------
# Cached contents of TableB219.mat to avoid repeated disk I/O.
# Keys:
# - "B219a": long-window psychoacoustic bands (69 bands, FFT size 2048)
# - "B219b": short-window psychoacoustic bands (42 bands, FFT size 256)
B219_CACHE: dict[str, BarkTable] | None = None
# -----------------------------------------------------------------------------
# MDCT / IMDCT
# -----------------------------------------------------------------------------
def mdct(s: TimeSignal) -> MdctCoeffs:
"""
MDCT (direct form) as specified in the assignment.
Parameters
----------
s : TimeSignal
Windowed time samples, 1-D array of length N (N = 2048 or 256).
Returns
-------
MdctCoeffs
MDCT coefficients, 1-D array of length N/2.
Definition
----------
X[k] = 2 * sum_{n=0..N-1} s[n] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
s = np.asarray(s, dtype=np.float64).reshape(-1)
N = int(s.shape[0])
if N not in (2048, 256):
raise ValueError("MDCT input length must be 2048 or 256.")
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(N // 2, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, N/2)
X = 2.0 * (s @ C) # (N/2,)
return X
def imdct(X: MdctCoeffs) -> TimeSignal:
"""
IMDCT (direct form) as specified in the assignment.
Parameters
----------
X : MdctCoeffs
MDCT coefficients, 1-D array of length K (K = 1024 or 128).
Returns
-------
TimeSignal
Reconstructed time samples, 1-D array of length N = 2K.
Definition
----------
s[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
X = np.asarray(X, dtype=np.float64).reshape(-1)
K = int(X.shape[0])
if K not in (1024, 128):
raise ValueError("IMDCT input length must be 1024 or 128.")
N = 2 * K
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(K, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, K)
s = (2.0 / N) * (C @ X) # (N,)
return s
# -----------------------------------------------------------------------------
# Signal quality metrics
# -----------------------------------------------------------------------------
def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Compute the overall Signal-to-Noise Ratio (SNR) in dB.
The SNR is computed over all available samples and channels,
after conservatively aligning the two signals to their common
length and channel count.
Parameters
----------
x_ref : StereoSignal
Reference (original) signal.
Typical shape: (N, 2) for stereo.
x_hat : StereoSignal
Reconstructed or processed signal.
Typical shape: (M, 2) for stereo.
Returns
-------
float
SNR in dB.
- +inf if the noise power is zero (perfect reconstruction).
- -inf if the reference signal power is zero.
"""
x_ref = np.asarray(x_ref, dtype=np.float64)
x_hat = np.asarray(x_hat, dtype=np.float64)
# Ensure 2-D shape: (samples, channels)
if x_ref.ndim == 1:
x_ref = x_ref.reshape(-1, 1)
if x_hat.ndim == 1:
x_hat = x_hat.reshape(-1, 1)
# Align lengths and channel count conservatively
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
x_ref = x_ref[:n, :c]
x_hat = x_hat[:n, :c]
err = x_ref - x_hat
ps = float(np.sum(x_ref * x_ref)) # signal power
pn = float(np.sum(err * err)) # noise power
if pn <= 0.0:
return float("inf")
if ps <= 0.0:
return float("-inf")
return float(10.0 * np.log10(ps / pn))
def estimate_lag_mono(x_ref: TimeSignal, x_hat: TimeSignal, max_lag=4096):
"""
Estimate time lag between two mono signals.
Returns lag (positive means x_hat delayed).
"""
n = min(len(x_ref), len(x_hat))
x_ref = x_ref[:n]
x_hat = x_hat[:n]
corr = np.correlate(x_ref, x_hat, mode='full')
lags = np.arange(-n + 1, n)
center = n - 1
lo = max(0, center - max_lag)
hi = min(len(corr), center + max_lag + 1)
best = lo + int(np.argmax(corr[lo:hi]))
return int(lags[best])
def match_gain(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Least-squares gain g that best maps x_hat -> x_ref.
"""
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
r = x_ref[:n, :c].reshape(-1).astype(np.float64)
h = x_hat[:n, :c].reshape(-1).astype(np.float64)
denom = float(np.dot(h, h))
if denom <= 0.0:
return 1.0
return float(np.dot(r, h) / denom)
# -----------------------------------------------------------------------------
# Psychoacoustic band tables (TableB219.mat)
# -----------------------------------------------------------------------------
def load_b219_tables() -> dict[str, BarkTable]:
"""
Load and cache psychoacoustic band tables from TableB219.mat.
The assignment/project layout assumes that a 'material' directory
is available in the current working directory when running:
- tests
- level_1 / level_2 / level_3 entrypoints
This function loads the tables once and caches them for subsequent calls.
Returns
-------
dict[str, BarkTable]
Dictionary with the following entries:
- "B219a": long-window psychoacoustic table
(69 bands, FFT size 2048 / 1024 spectral lines)
- "B219b": short-window psychoacoustic table
(42 bands, FFT size 256 / 128 spectral lines)
"""
global B219_CACHE
if B219_CACHE is not None:
return B219_CACHE
mat_path = Path("material") / "TableB219.mat"
if not mat_path.exists():
raise FileNotFoundError(
"Could not locate material/TableB219.mat in the current working directory."
)
data = loadmat(str(mat_path))
if "B219a" not in data or "B219b" not in data:
raise ValueError(
"TableB219.mat missing required variables 'B219a' and/or 'B219b'."
)
B219_CACHE = {
"B219a": np.asarray(data["B219a"], dtype=np.float64),
"B219b": np.asarray(data["B219b"], dtype=np.float64),
}
return B219_CACHE
def get_table(frame_type: FrameType) -> tuple[BarkTable, int]:
"""
Select the appropriate psychoacoustic band table and FFT size
based on the AAC frame type.
Parameters
----------
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
Returns
-------
table : BarkTable
Psychoacoustic band table:
- B219a for long frames
- B219b for ESH short subframes
N : int
FFT size corresponding to the table:
- 2048 for long frames
- 256 for short frames (ESH)
"""
tables = load_b219_tables()
if frame_type == "ESH":
return tables["B219b"], 256
return tables["B219a"], 2048
def band_limits(
table: BarkTable,
) -> tuple[BandIndexArray, BandIndexArray, BandValueArray, BandValueArray]:
"""
Extract per-band metadata from a TableB2.1.9 psychoacoustic table.
The column layout follows the provided TableB219.mat file and the
AAC specification tables B.2.1.9a / B.2.1.9b.
Parameters
----------
table : BarkTable
Psychoacoustic band table (B219a or B219b).
Returns
-------
wlow : BandIndexArray
Lower FFT bin index (inclusive) for each band.
whigh : BandIndexArray
Upper FFT bin index (inclusive) for each band.
bval : BandValueArray
Bark-scale (or equivalent) band position values.
Used in the spreading function.
qthr_db : BandValueArray
Threshold in quiet for each band, in dB.
"""
wlow = table[:, 1].astype(int)
whigh = table[:, 2].astype(int)
bval = table[:, 4].astype(np.float64)
qthr_db = table[:, 5].astype(np.float64)
return wlow, whigh, bval, qthr_db
+3 -3
View File
@@ -28,7 +28,7 @@ from core.aac_types import AACSeq2, StereoSignal
from core.aac_coder import aac_coder_2 as core_aac_coder_2 from core.aac_coder import aac_coder_2 as core_aac_coder_2
from core.aac_coder import aac_read_wav_stereo_48k from core.aac_coder import aac_read_wav_stereo_48k
from core.aac_decoder import aac_decoder_2 as core_aac_decoder_2 from core.aac_decoder import aac_decoder_2 as core_aac_decoder_2
from core.aac_snr_db import snr_db from core.aac_utils import snr_db
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Public Level 2 API (wrappers) # Public Level 2 API (wrappers)
@@ -51,7 +51,7 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
AACSeq2 AACSeq2
List of encoded frames (Level 2 schema). List of encoded frames (Level 2 schema).
""" """
return core_aac_coder_2(filename_in) return core_aac_coder_2(filename_in, verbose=True)
def i_aac_coder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoSignal: def i_aac_coder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoSignal:
@@ -72,7 +72,7 @@ def i_aac_coder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
StereoSignal StereoSignal
Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64. Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64.
""" """
return core_aac_decoder_2(aac_seq_2, filename_out) return core_aac_decoder_2(aac_seq_2, filename_out, verbose=True)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
Binary file not shown.
Binary file not shown.
+5 -2
View File
@@ -381,12 +381,15 @@ def decode_huff(huff_sec, huff_LUT):
while b: while b:
N += 1 N += 1
b = huff_sec[streamIndex + N] b = huff_sec[streamIndex + N]
streamIndex += N # Skip the N leading '1' bits AND the terminating '0' delimiter.
# The encoder writes: '1'*N + '0' + <N4 bits>
streamIndex += N +1
N4 = N + 4 N4 = N + 4
escape_word = huff_sec[streamIndex:streamIndex + N4] escape_word = huff_sec[streamIndex:streamIndex + N4]
escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2) escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2)
nTupleDec[idx] = escape_value nTupleDec[idx] = escape_value
streamIndex += N4 + 1 # We already consumed the delimiter above; now consume only N4 bits.
streamIndex += N4
# Apply signs again # Apply signs again
nTupleDec[escIndex] *= nTupleSign[escIndex] nTupleDec[escIndex] *= nTupleSign[escIndex]
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

+525
View File
@@ -0,0 +1,525 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - AAC Coder (Core)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Level 1 AAC encoder orchestration.
# Keeps the same functional behavior as the original level_1 implementation:
# - Reads WAV via soundfile
# - Validates stereo and 48 kHz
# - Frames into 2048 samples with hop=1024 and zero padding at both ends
# - SSC decision uses next-frame attack detection
# - Filterbank analysis (MDCT)
# - Stores per-channel spectra in AACSeq1 schema:
# * ESH: (128, 8)
# * else: (1024, 1)
# ------------------------------------------------------------
from __future__ import annotations
from pathlib import Path
from typing import Union
import soundfile as sf
from scipy.io import savemat
from core.aac_configuration import WIN_TYPE
from core.aac_filterbank import aac_filter_bank
from core.aac_ssc import aac_ssc
from core.aac_tns import aac_tns
from core.aac_psycho import aac_psycho
from core.aac_quantizer import aac_quantizer # assumes your quantizer file is core/aac_quantizer.py
from core.aac_huffman import aac_encode_huff
from core.aac_utils import get_table, band_limits
from material.huff_utils import load_LUT
from core.aac_types import *
# -----------------------------------------------------------------------------
# Helpers for thresholds (T(b))
# -----------------------------------------------------------------------------
def _band_slices_from_table(frame_type: FrameType) -> list[tuple[int, int]]:
"""
Return inclusive (lo, hi) band slices derived from TableB219.
"""
table, _ = get_table(frame_type)
wlow, whigh, _bval, _qthr_db = band_limits(table)
return [(int(lo), int(hi)) for lo, hi in zip(wlow, whigh)]
def _thresholds_from_smr(
frame_F_ch: FrameChannelF,
frame_type: FrameType,
SMR: FloatArray,
) -> FloatArray:
"""
Compute thresholds T(b) = P(b) / SMR(b), where P(b) is band energy.
Shapes:
- Long: returns (NB, 1)
- ESH: returns (NB, 8)
"""
bands = _band_slices_from_table(frame_type)
NB = len(bands)
X = np.asarray(frame_F_ch, dtype=np.float64)
SMR = np.asarray(SMR, dtype=np.float64)
if frame_type == "ESH":
if X.shape != (128, 8):
raise ValueError("For ESH, frame_F_ch must have shape (128, 8).")
if SMR.shape != (NB, 8):
raise ValueError(f"For ESH, SMR must have shape ({NB}, 8).")
T = np.zeros((NB, 8), dtype=np.float64)
for j in range(8):
Xj = X[:, j]
for b, (lo, hi) in enumerate(bands):
P = float(np.sum(Xj[lo : hi + 1] ** 2))
smr = float(SMR[b, j])
T[b, j] = 0.0 if smr <= 1e-12 else (P / smr)
return T
# Long
if X.shape == (1024,):
Xv = X
elif X.shape == (1024, 1):
Xv = X[:, 0]
else:
raise ValueError("For non-ESH, frame_F_ch must be shape (1024,) or (1024, 1).")
if SMR.shape == (NB,):
SMRv = SMR
elif SMR.shape == (NB, 1):
SMRv = SMR[:, 0]
else:
raise ValueError(f"For non-ESH, SMR must be shape ({NB},) or ({NB}, 1).")
T = np.zeros((NB, 1), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
P = float(np.sum(Xv[lo : hi + 1] ** 2))
smr = float(SMRv[b])
T[b, 0] = 0.0 if smr <= 1e-12 else (P / smr)
return T
# -----------------------------------------------------------------------------
# Public helpers (useful for level_x demo wrappers)
# -----------------------------------------------------------------------------
def aac_read_wav_stereo_48k(filename_in: Union[str, Path]) -> tuple[StereoSignal, int]:
"""
Read a WAV file using soundfile and validate the Level-1 assumptions.
Parameters
----------
filename_in : Union[str, Path]
Input WAV filename.
Returns
-------
x : StereoSignal (np.ndarray)
Stereo samples as float64, shape (N, 2).
fs : int
Sampling rate (Hz). Must be 48000.
Raises
------
ValueError
If the input is not stereo or the sampling rate is not 48 kHz.
"""
filename_in = Path(filename_in)
x, fs = sf.read(str(filename_in), always_2d=True)
x = np.asarray(x, dtype=np.float64)
if x.shape[1] != 2:
raise ValueError("Input must be stereo (2 channels).")
if int(fs) != 48000:
raise ValueError("Input sampling rate must be 48 kHz.")
return x, int(fs)
def aac_pack_frame_f_to_seq_channels(frame_type: FrameType, frame_f: FrameF) -> tuple[FrameChannelF, FrameChannelF]:
"""
Convert the stereo FrameF returned by aac_filter_bank() into per-channel arrays
as required by the Level-1 AACSeq1 schema.
Parameters
----------
frame_type : FrameType
"OLS" | "LSS" | "ESH" | "LPS".
frame_f : FrameF
Output of aac_filter_bank():
- If frame_type != "ESH": shape (1024, 2)
- If frame_type == "ESH": shape (128, 16) packed as [L0 R0 L1 R1 ... L7 R7]
Returns
-------
chl_f : FrameChannelF
Left channel coefficients:
- ESH: shape (128, 8)
- else: shape (1024, 1)
chr_f : FrameChannelF
Right channel coefficients:
- ESH: shape (128, 8)
- else: shape (1024, 1)
"""
if frame_type == "ESH":
if frame_f.shape != (128, 16):
raise ValueError("For ESH, frame_f must have shape (128, 16).")
chl_f = np.empty((128, 8), dtype=np.float64)
chr_f = np.empty((128, 8), dtype=np.float64)
for j in range(8):
chl_f[:, j] = frame_f[:, 2 * j + 0]
chr_f[:, j] = frame_f[:, 2 * j + 1]
return chl_f, chr_f
# Non-ESH: store as (1024, 1) as required by the original Level-1 schema.
if frame_f.shape != (1024, 2):
raise ValueError("For OLS/LSS/LPS, frame_f must have shape (1024, 2).")
chl_f = frame_f[:, 0:1].astype(np.float64, copy=False)
chr_f = frame_f[:, 1:2].astype(np.float64, copy=False)
return chl_f, chr_f
# -----------------------------------------------------------------------------
# Level 1 encoder
# -----------------------------------------------------------------------------
def aac_coder_1(
filename_in: Union[str, Path],
verbose: bool = False
) -> AACSeq1:
"""
Level-1 AAC encoder.
This function preserves the behavior of the original level_1 implementation:
- Read stereo 48 kHz WAV
- Pad hop samples at start and hop samples at end
- Frame with win=2048, hop=1024
- Use SSC with next-frame lookahead
- Apply filterbank analysis
- Store per-channel coefficients using AACSeq1 schema
Parameters
----------
filename_in : Union[str, Path]
Input WAV filename.
Assumption: stereo audio, sampling rate 48 kHz.
verbose : bool
Optional argument to print encoding status
Returns
-------
AACSeq1
List of encoded frames (Level 1 schema).
"""
x, _ = aac_read_wav_stereo_48k(filename_in)
# The assignment assumes 48 kHz
hop = 1024
win = 2048
# Pad at the beginning to support the first overlap region.
# Tail padding is kept minimal; next-frame is padded on-the-fly when needed.
pad_pre = np.zeros((hop, 2), dtype=np.float64)
pad_post = np.zeros((hop, 2), dtype=np.float64)
x_pad = np.vstack([pad_pre, x, pad_post])
# Number of frames such that current frame fits; next frame will be padded if needed.
K = int((x_pad.shape[0] - win) // hop + 1)
if K <= 0:
raise ValueError("Input too short for framing.")
aac_seq: AACSeq1 = []
prev_frame_type: FrameType = "OLS"
if verbose:
print("Encoding ", end="", flush=True)
for i in range(K):
start = i * hop
frame_t: FrameT = x_pad[start:start + win, :]
if frame_t.shape != (win, 2):
# This should not happen due to K definition, but keep it explicit.
raise ValueError("Internal framing error: frame_t has wrong shape.")
next_t = x_pad[start + hop:start + hop + win, :]
# Ensure next_t is always (2048, 2) by zero-padding at the tail.
if next_t.shape[0] < win:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail])
frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
frame_f = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f)
aac_seq.append({
"frame_type": frame_type,
"win_type": WIN_TYPE,
"chl": {"frame_F": chl_f},
"chr": {"frame_F": chr_f},
})
prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
return aac_seq
def aac_coder_2(
filename_in: Union[str, Path],
verbose: bool = False
) -> AACSeq2:
"""
Level-2 AAC encoder (Level 1 + TNS).
Parameters
----------
filename_in : Union[str, Path]
Input WAV filename (stereo, 48 kHz).
verbose : bool
Optional argument to print encoding status
Returns
-------
AACSeq2
Encoded AAC sequence (Level 2 payload schema).
For each frame i:
- "frame_type": FrameType
- "win_type": WinType
- "chl"/"chr":
- "frame_F": FrameChannelF (after TNS)
- "tns_coeffs": TnsCoeffs
"""
filename_in = Path(filename_in)
x, _ = aac_read_wav_stereo_48k(filename_in)
# The assignment assumes 48 kHz
hop = 1024
win = 2048
pad_pre = np.zeros((hop, 2), dtype=np.float64)
pad_post = np.zeros((hop, 2), dtype=np.float64)
x_pad = np.vstack([pad_pre, x, pad_post])
K = int((x_pad.shape[0] - win) // hop + 1)
if K <= 0:
raise ValueError("Input too short for framing.")
aac_seq: AACSeq2 = []
prev_frame_type: FrameType = "OLS"
if verbose:
print("Encoding ", end="", flush=True)
for i in range(K):
start = i * hop
frame_t: FrameT = x_pad[start : start + win, :]
if frame_t.shape != (win, 2):
raise ValueError("Internal framing error: frame_t has wrong shape.")
next_t = x_pad[start + hop : start + hop + win, :]
if next_t.shape[0] < win:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail])
frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
# Level 1 analysis (packed stereo container)
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f_stereo)
# Level 2: apply TNS per channel
chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type)
chr_f_tns, chr_tns_coeffs = aac_tns(chr_f, frame_type)
aac_seq.append(
{
"frame_type": frame_type,
"win_type": WIN_TYPE,
"chl": {"frame_F": chl_f_tns, "tns_coeffs": chl_tns_coeffs},
"chr": {"frame_F": chr_f_tns, "tns_coeffs": chr_tns_coeffs},
}
)
prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
return aac_seq
def aac_coder_3(
filename_in: Union[str, Path],
filename_aac_coded: Union[str, Path] | None = None,
verbose: bool = False,
) -> AACSeq3:
"""
Level-3 AAC encoder (Level 2 + Psycho + Quantizer + Huffman).
Parameters
----------
filename_in : Union[str, Path]
Input WAV filename (stereo, 48 kHz).
filename_aac_coded : Union[str, Path] | None
Optional .mat filename to store aac_seq_3 (assignment convenience).
verbose : bool
Optional argument to print encoding status
Returns
-------
AACSeq3
Encoded AAC sequence (Level 3 payload schema).
"""
filename_in = Path(filename_in)
x, _ = aac_read_wav_stereo_48k(filename_in)
hop = 1024
win = 2048
pad_pre = np.zeros((hop, 2), dtype=np.float64)
pad_post = np.zeros((hop, 2), dtype=np.float64)
x_pad = np.vstack([pad_pre, x, pad_post])
K = int((x_pad.shape[0] - win) // hop + 1)
if K <= 0:
raise ValueError("Input too short for framing.")
# Load Huffman LUTs once.
huff_LUT_list = load_LUT()
aac_seq: AACSeq3 = []
prev_frame_type: FrameType = "OLS"
# Psycho model needs per-channel history (prev1, prev2) of 2048-sample frames.
prev1_L = np.zeros((2048,), dtype=np.float64)
prev2_L = np.zeros((2048,), dtype=np.float64)
prev1_R = np.zeros((2048,), dtype=np.float64)
prev2_R = np.zeros((2048,), dtype=np.float64)
if verbose:
print("Encoding ", end="", flush=True)
for i in range(K):
start = i * hop
frame_t: FrameT = x_pad[start : start + win, :]
if frame_t.shape != (win, 2):
raise ValueError("Internal framing error: frame_t has wrong shape.")
next_t = x_pad[start + hop : start + hop + win, :]
if next_t.shape[0] < win:
tail = np.zeros((win - next_t.shape[0], 2), dtype=np.float64)
next_t = np.vstack([next_t, tail])
frame_type = aac_ssc(frame_t, next_t, prev_frame_type)
# Analysis filterbank (stereo packed)
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
chl_f, chr_f = aac_pack_frame_f_to_seq_channels(frame_type, frame_f_stereo)
# TNS per channel
chl_f_tns, chl_tns_coeffs = aac_tns(chl_f, frame_type)
chr_f_tns, chr_tns_coeffs = aac_tns(chr_f, frame_type)
# Psychoacoustic model per channel (time-domain)
frame_L = np.asarray(frame_t[:, 0], dtype=np.float64)
frame_R = np.asarray(frame_t[:, 1], dtype=np.float64)
SMR_L = aac_psycho(frame_L, frame_type, prev1_L, prev2_L)
SMR_R = aac_psycho(frame_R, frame_type, prev1_R, prev2_R)
# Thresholds T(b) (stored, not entropy-coded)
T_L = _thresholds_from_smr(chl_f_tns, frame_type, SMR_L)
T_R = _thresholds_from_smr(chr_f_tns, frame_type, SMR_R)
# Quantizer per channel
S_L, sfc_L, G_L = aac_quantizer(chl_f_tns, frame_type, SMR_L)
S_R, sfc_R, G_R = aac_quantizer(chr_f_tns, frame_type, SMR_R)
# Huffman-code ONLY the DPCM differences for b>0.
# sfc[0] corresponds to alpha(0)=G and is stored separately in the frame.
sfc_L_dpcm = np.asarray(sfc_L, dtype=np.int64)[1:, ...]
sfc_R_dpcm = np.asarray(sfc_R, dtype=np.int64)[1:, ...]
# sfc_L_stream, cb_sfc_L = aac_encode_huff(sfc_L_dpcm.reshape(-1, order="F"), huff_LUT_list, force_codebook=11)
# sfc_R_stream, cb_sfc_R = aac_encode_huff(sfc_R_dpcm.reshape(-1, order="F"), huff_LUT_list, force_codebook=11)
sfc_L_stream, cb_sfc_L = aac_encode_huff(sfc_L_dpcm.reshape(-1, order="F"), huff_LUT_list)
sfc_R_stream, cb_sfc_R = aac_encode_huff(sfc_R_dpcm.reshape(-1, order="F"), huff_LUT_list)
if cb_sfc_L != 11 or cb_sfc_R != 11:
raise ValueError(f"Illegal codebook value for frame: {i}: cb_sfc_l={cb_sfc_L}, cb_sfc_r={cb_sfc_R}.")
mdct_L_stream, cb_L = aac_encode_huff(np.asarray(S_L, dtype=np.int64).reshape(-1), huff_LUT_list)
mdct_R_stream, cb_R = aac_encode_huff(np.asarray(S_R, dtype=np.int64).reshape(-1), huff_LUT_list)
# Typed dict construction helps static analyzers validate the schema.
frame_out: AACSeq3Frame = {
"frame_type": frame_type,
"win_type": WIN_TYPE,
"chl": {
"tns_coeffs": np.asarray(chl_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_L, dtype=np.float64),
"G": G_L,
"sfc": sfc_L_stream,
"stream": mdct_L_stream,
"codebook": int(cb_L),
},
"chr": {
"tns_coeffs": np.asarray(chr_tns_coeffs, dtype=np.float64),
"T": np.asarray(T_R, dtype=np.float64),
"G": G_R,
"sfc": sfc_R_stream,
"stream": mdct_R_stream,
"codebook": int(cb_R),
},
}
aac_seq.append(frame_out)
# Update psycho history (shift register)
prev2_L = prev1_L
prev1_L = frame_L
prev2_R = prev1_R
prev1_R = frame_R
prev_frame_type = frame_type
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
if verbose:
print(" done")
# Optional: store to .mat for the assignment wrapper
if filename_aac_coded is not None:
filename_aac_coded = Path(filename_aac_coded)
savemat(
str(filename_aac_coded),
{"aac_seq_3": np.array(aac_seq, dtype=object)},
do_compression=True,
)
return aac_seq
+41
View File
@@ -0,0 +1,41 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Configuration
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# This module contains the global configurations
#
# ------------------------------------------------------------
from __future__ import annotations
# Imports
from typing import Final
from core.aac_types import WinType
# Filterbank
# ------------------------------------------------------------
# Window type
# Options: "SIN", "KBD"
WIN_TYPE: WinType = "SIN"
# TNS
# ------------------------------------------------------------
PRED_ORDER = 4
QUANT_STEP = 0.1
QUANT_MAX = 0.7 # 4-bit symmetric with step 0.1 -> clamp to [-0.7, +0.7]
# -----------------------------------------------------------------------------
# Psycho
# -----------------------------------------------------------------------------
NMT_DB: Final[float] = 6.0 # Noise Masking Tone (dB)
TMN_DB: Final[float] = 18.0 # Tone Masking Noise (dB)
+445
View File
@@ -0,0 +1,445 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Inverse AAC Coder (Core)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# - Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
# - Level 2 AAC decoder orchestration (inverse of aac_coder_1()).
#
# ------------------------------------------------------------
from __future__ import annotations
from pathlib import Path
from typing import Union
import soundfile as sf
from core.aac_filterbank import aac_i_filter_bank
from core.aac_tns import aac_i_tns
from core.aac_quantizer import aac_i_quantizer
from core.aac_huffman import aac_decode_huff
from core.aac_utils import get_table, band_limits
from material.huff_utils import load_LUT
from core.aac_types import *
# -----------------------------------------------------------------------------
# Helper for NB
# -----------------------------------------------------------------------------
def _nbands(frame_type: FrameType) -> int:
table, _ = get_table(frame_type)
wlow, _whigh, _bval, _qthr_db = band_limits(table)
return int(len(wlow))
# -----------------------------------------------------------------------------
# Public helpers
# -----------------------------------------------------------------------------
def aac_unpack_seq_channels(frame_type: FrameType, chl_f: FrameChannelF, chr_f: FrameChannelF) -> FrameF:
"""
Re-pack per-channel spectra from the Level-1 AACSeq1 schema into the stereo
FrameF container expected by aac_i_filter_bank().
Parameters
----------
frame_type : FrameType
"OLS" | "LSS" | "ESH" | "LPS".
chl_f : FrameChannelF
Left channel coefficients:
- ESH: (128, 8)
- else: (1024, 1)
chr_f : FrameChannelF
Right channel coefficients:
- ESH: (128, 8)
- else: (1024, 1)
Returns
-------
FrameF
Stereo coefficients:
- ESH: (128, 16) packed as [L0 R0 L1 R1 ... L7 R7]
- else: (1024, 2)
"""
if frame_type == "ESH":
if chl_f.shape != (128, 8) or chr_f.shape != (128, 8):
raise ValueError("ESH channel frame_F must have shape (128, 8).")
frame_f = np.empty((128, 16), dtype=np.float64)
for j in range(8):
frame_f[:, 2 * j + 0] = chl_f[:, j]
frame_f[:, 2 * j + 1] = chr_f[:, j]
return frame_f
# Non-ESH: expected (1024, 1) per channel in Level-1 schema.
if chl_f.shape != (1024, 1) or chr_f.shape != (1024, 1):
raise ValueError("Non-ESH channel frame_F must have shape (1024, 1).")
frame_f = np.empty((1024, 2), dtype=np.float64)
frame_f[:, 0] = chl_f[:, 0]
frame_f[:, 1] = chr_f[:, 0]
return frame_f
def aac_remove_padding(y_pad: StereoSignal, hop: int = 1024) -> StereoSignal:
"""
Remove the boundary padding that the Level-1 encoder adds:
hop samples at start and hop samples at end.
Parameters
----------
y_pad : StereoSignal (np.ndarray)
Reconstructed padded stream, shape (N_pad, 2).
hop : int
Hop size in samples (default 1024).
Returns
-------
StereoSignal (np.ndarray)
Unpadded reconstructed stream, shape (N_pad - 2*hop, 2).
Raises
------
ValueError
If y_pad is too short to unpad.
"""
if y_pad.shape[0] < 2 * hop:
raise ValueError("Decoded stream too short to unpad.")
return y_pad[hop:-hop, :]
# -----------------------------------------------------------------------------
# Level 1 decoder
# -----------------------------------------------------------------------------
def aac_decoder_1(
aac_seq_1: AACSeq1,
filename_out: Union[str, Path],
verbose: bool = False
) -> StereoSignal:
"""
Level-1 AAC decoder (inverse of aac_coder_1()).
This function preserves the behavior of the original level_1 implementation:
- Reconstruct the full padded stream by overlap-adding K synthesized frames
- Remove hop padding at the beginning and hop padding at the end
- Write the reconstructed stereo WAV file (48 kHz)
- Return reconstructed stereo samples as float64
Parameters
----------
aac_seq_1 : AACSeq1
Encoded sequence as produced by aac_coder_1().
filename_out : Union[str, Path]
Output WAV filename. Assumption: 48 kHz, stereo.
verbose : bool
Optional argument to print encoding status
Returns
-------
StereoSignal
Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64.
"""
filename_out = Path(filename_out)
hop = 1024
win = 2048
K = len(aac_seq_1)
# Output includes the encoder padding region, so we reconstruct the full padded stream.
# For K frames: last frame starts at (K-1)*hop and spans win,
# so total length = (K-1)*hop + win.
n_pad = (K - 1) * hop + win
y_pad: StereoSignal = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_1):
frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"]
chl_f = np.asarray(fr["chl"]["frame_F"], dtype=np.float64)
chr_f = np.asarray(fr["chr"]["frame_F"], dtype=np.float64)
frame_f: FrameF = aac_unpack_seq_channels(frame_type, chl_f, chr_f)
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type) # (2048, 2)
start = i * hop
y_pad[start:start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y: StereoSignal = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
# Level 1 assumption: 48 kHz output.
sf.write(str(filename_out), y, 48000)
return y
# -----------------------------------------------------------------------------
# Level 2 decoder
# -----------------------------------------------------------------------------
def aac_decoder_2(
aac_seq_2: AACSeq2,
filename_out: Union[str, Path],
verbose: bool = False
) -> StereoSignal:
"""
Level-2 AAC decoder (inverse of aac_coder_2).
Behavior matches Level 1 decoder pipeline, with additional iTNS stage:
- Per frame/channel: inverse TNS using stored coefficients
- Re-pack to stereo frame_F
- IMDCT + windowing
- Overlap-add over frames
- Remove Level-1 padding (hop samples start/end)
- Write output WAV (48 kHz)
Parameters
----------
aac_seq_2 : AACSeq2
Encoded sequence as produced by aac_coder_2().
filename_out : Union[str, Path]
Output WAV filename.
verbose : bool
Optional argument to print encoding status
Returns
-------
StereoSignal
Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64.
"""
filename_out = Path(filename_out)
hop = 1024
win = 2048
K = len(aac_seq_2)
if K <= 0:
raise ValueError("aac_seq_2 must contain at least one frame.")
n_pad = (K - 1) * hop + win
y_pad = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_2):
frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"]
chl_f_tns = np.asarray(fr["chl"]["frame_F"], dtype=np.float64)
chr_f_tns = np.asarray(fr["chr"]["frame_F"], dtype=np.float64)
chl_coeffs = np.asarray(fr["chl"]["tns_coeffs"], dtype=np.float64)
chr_coeffs = np.asarray(fr["chr"]["tns_coeffs"], dtype=np.float64)
# Inverse TNS per channel
chl_f = aac_i_tns(chl_f_tns, frame_type, chl_coeffs)
chr_f = aac_i_tns(chr_f_tns, frame_type, chr_coeffs)
# Re-pack to the stereo container expected by aac_i_filter_bank
if frame_type == "ESH":
if chl_f.shape != (128, 8) or chr_f.shape != (128, 8):
raise ValueError("ESH channel frame_F must have shape (128, 8).")
frame_f: FrameF = np.empty((128, 16), dtype=np.float64)
for j in range(8):
frame_f[:, 2 * j + 0] = chl_f[:, j]
frame_f[:, 2 * j + 1] = chr_f[:, j]
else:
# Accept either (1024,1) or (1024,) from your internal convention.
if chl_f.shape == (1024,):
chl_col = chl_f.reshape(1024, 1)
elif chl_f.shape == (1024, 1):
chl_col = chl_f
else:
raise ValueError("Non-ESH left channel frame_F must be shape (1024,) or (1024, 1).")
if chr_f.shape == (1024,):
chr_col = chr_f.reshape(1024, 1)
elif chr_f.shape == (1024, 1):
chr_col = chr_f
else:
raise ValueError("Non-ESH right channel frame_F must be shape (1024,) or (1024, 1).")
frame_f = np.empty((1024, 2), dtype=np.float64)
frame_f[:, 0] = chl_col[:, 0]
frame_f[:, 1] = chr_col[:, 0]
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type)
start = i * hop
y_pad[start : start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
sf.write(str(filename_out), y, 48000)
return y
def aac_decoder_3(
aac_seq_3: AACSeq3,
filename_out: Union[str, Path],
verbose: bool = False,
) -> StereoSignal:
"""
Level-3 AAC decoder (inverse of aac_coder_3).
Steps per frame:
- Huffman decode scalefactors (sfc) using codebook 11
- Huffman decode MDCT symbols (stream) using stored codebook
- iQuantizer -> MDCT coefficients after TNS
- iTNS using stored predictor coefficients
- IMDCT filterbank -> time domain
- Overlap-add, remove padding, write WAV
Parameters
----------
aac_seq_3 : AACSeq3
Encoded sequence as produced by aac_coder_3.
filename_out : Union[str, Path]
Output WAV filename.
verbose : bool
Optional argument to print encoding status
Returns
-------
StereoSignal
Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64.
"""
filename_out = Path(filename_out)
hop = 1024
win = 2048
K = len(aac_seq_3)
if K <= 0:
raise ValueError("aac_seq_3 must contain at least one frame.")
# Load Huffman LUTs once.
huff_LUT_list = load_LUT()
n_pad = (K - 1) * hop + win
y_pad = np.zeros((n_pad, 2), dtype=np.float64)
if verbose:
print("Decoding ", end="", flush=True)
for i, fr in enumerate(aac_seq_3):
frame_type: FrameType = fr["frame_type"]
win_type: WinType = fr["win_type"]
NB = _nbands(frame_type)
# We store G separately, so Huffman stream contains only (NB-1) DPCM differences.
sfc_len = (NB - 1) * (8 if frame_type == "ESH" else 1)
# -------------------------
# Left channel
# -------------------------
tns_L = np.asarray(fr["chl"]["tns_coeffs"], dtype=np.float64)
G_L = fr["chl"]["G"]
sfc_bits_L = fr["chl"]["sfc"]
mdct_bits_L = fr["chl"]["stream"]
cb_L = int(fr["chl"]["codebook"])
sfc_dec_L = aac_decode_huff(sfc_bits_L, 11, huff_LUT_list)[:sfc_len].astype(np.int64, copy=False)
if frame_type == "ESH":
sfc_dpcm_L = sfc_dec_L.reshape(NB - 1, 8, order="F")
sfc_L = np.zeros((NB, 8), dtype=np.int64)
Gv = np.asarray(G_L, dtype=np.float64).reshape(1, 8)
sfc_L[0, :] = Gv[0, :].astype(np.int64)
sfc_L[1:, :] = sfc_dpcm_L
else:
sfc_dpcm_L = sfc_dec_L.reshape(NB - 1, 1, order="F")
sfc_L = np.zeros((NB, 1), dtype=np.int64)
sfc_L[0, 0] = int(float(G_L))
sfc_L[1:, :] = sfc_dpcm_L
# MDCT symbols: codebook 0 means "all-zero section"
if cb_L == 0:
S_dec_L = np.zeros((1024,), dtype=np.int64)
else:
S_tmp_L = aac_decode_huff(mdct_bits_L, cb_L, huff_LUT_list).astype(np.int64, copy=False)
# Tuple coding may produce extra trailing symbols; caller knows the true length (1024).
# Also guard against short outputs by zero-padding.
if S_tmp_L.size < 1024:
S_dec_L = np.zeros((1024,), dtype=np.int64)
S_dec_L[: S_tmp_L.size] = S_tmp_L
else:
S_dec_L = S_tmp_L[:1024]
S_L = S_dec_L.reshape(1024, 1)
Xq_L = aac_i_quantizer(S_L, sfc_L, G_L, frame_type)
X_L = aac_i_tns(Xq_L, frame_type, tns_L)
# -------------------------
# Right channel
# -------------------------
tns_R = np.asarray(fr["chr"]["tns_coeffs"], dtype=np.float64)
G_R = fr["chr"]["G"]
sfc_bits_R = fr["chr"]["sfc"]
mdct_bits_R = fr["chr"]["stream"]
cb_R = int(fr["chr"]["codebook"])
sfc_dec_R = aac_decode_huff(sfc_bits_R, 11, huff_LUT_list)[:sfc_len].astype(np.int64, copy=False)
if frame_type == "ESH":
sfc_dpcm_R = sfc_dec_R.reshape(NB - 1, 8, order="F")
sfc_R = np.zeros((NB, 8), dtype=np.int64)
Gv = np.asarray(G_R, dtype=np.float64).reshape(1, 8)
sfc_R[0, :] = Gv[0, :].astype(np.int64)
sfc_R[1:, :] = sfc_dpcm_R
else:
sfc_dpcm_R = sfc_dec_R.reshape(NB - 1, 1, order="F")
sfc_R = np.zeros((NB, 1), dtype=np.int64)
sfc_R[0, 0] = int(float(G_R))
sfc_R[1:, :] = sfc_dpcm_R
if cb_R == 0:
S_dec_R = np.zeros((1024,), dtype=np.int64)
else:
S_tmp_R = aac_decode_huff(mdct_bits_R, cb_R, huff_LUT_list).astype(np.int64, copy=False)
if S_tmp_R.size < 1024:
S_dec_R = np.zeros((1024,), dtype=np.int64)
S_dec_R[: S_tmp_R.size] = S_tmp_R
else:
S_dec_R = S_tmp_R[:1024]
S_R = S_dec_R.reshape(1024, 1)
Xq_R = aac_i_quantizer(S_R, sfc_R, G_R, frame_type)
X_R = aac_i_tns(Xq_R, frame_type, tns_R)
# Re-pack to stereo container and inverse filterbank
frame_f = aac_unpack_seq_channels(frame_type, np.asarray(X_L), np.asarray(X_R))
frame_t_hat: FrameT = aac_i_filter_bank(frame_f, frame_type, win_type)
start = i * hop
y_pad[start : start + win, :] += frame_t_hat
if verbose and (i % (K//20)) == 0:
print(".", end="", flush=True)
y = aac_remove_padding(y_pad, hop=hop)
if verbose:
print(" done")
sf.write(str(filename_out), y, 48000)
return y
+387
View File
@@ -0,0 +1,387 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Filterbank module
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Filterbank stage (MDCT/IMDCT), windowing, ESH packing/unpacking
#
# ------------------------------------------------------------
from __future__ import annotations
from core.aac_utils import mdct, imdct
from core.aac_types import *
from scipy.signal.windows import kaiser
# Private helpers for Filterbank
# ------------------------------------------------------------
def _sin_window(N: int) -> Window:
"""
Build a sinusoidal (SIN) window of length N.
The AAC sinusoid window is:
w[n] = sin(pi/N * (n + 0.5)), for 0 <= n < N
Parameters
----------
N : int
Window length in samples.
Returns
-------
Window
1-D array of shape (N, ) with dtype float64.
"""
n = np.arange(N, dtype=np.float64)
return np.sin((np.pi / N) * (n + 0.5))
def _kbd_window(N: int, alpha: float) -> Window:
"""
Build a Kaiser-Bessel-Derived (KBD) window of length N.
This follows the standard KBD construction used in AAC:
1) Build a Kaiser kernel of length (N/2 + 1).
2) Form the left half by cumulative summation, normalization, and sqrt.
3) Mirror the left half to form the right half (symmetric full-length window).
Notes
-----
- N must be even (AAC uses N=2048 for long and N=256 for short).
- The assignment specifies alpha=6 for long windows and alpha=4 for short windows.
- The Kaiser beta parameter is commonly taken as beta = pi * alpha for this context.
Parameters
----------
N : int
Window length in samples (must be even).
alpha : float
KBD alpha parameter.
Returns
-------
Window
1-D array of shape (N,) with dtype float64.
"""
half = N // 2
# Kaiser kernel length: half + 1 samples (0 .. half)
# beta = pi * alpha per the usual correspondence with the ISO definition
kernel = kaiser(half + 1, beta=np.pi * alpha).astype(np.float64)
csum = np.cumsum(kernel)
denom = csum[-1]
w_left = np.sqrt(csum[:-1] / denom) # length half, n = 0 .. half-1
w_right = w_left[::-1] # mirror for second half
return np.concatenate([w_left, w_right])
def _long_window(win_type: WinType) -> Window:
"""
Return the long AAC window (length 2048) for the selected window family.
Parameters
----------
win_type : WinType
Either "SIN" or "KBD".
Returns
-------
Window
1-D array of shape (2048,) with dtype float64.
"""
if win_type == "SIN":
return _sin_window(2048)
if win_type == "KBD":
# Assignment-specific alpha values
return _kbd_window(2048, alpha=6.0)
raise ValueError(f"Invalid win_type: {win_type!r}")
def _short_window(win_type: WinType) -> Window:
"""
Return the short AAC window (length 256) for the selected window family.
Parameters
----------
win_type : WinType
Either "SIN" or "KBD".
Returns
-------
Window
1-D array of shape (256,) with dtype float64.
"""
if win_type == "SIN":
return _sin_window(256)
if win_type == "KBD":
# Assignment-specific alpha values
return _kbd_window(256, alpha=4.0)
raise ValueError(f"Invalid win_type: {win_type!r}")
def _window_sequence(frame_type: FrameType, win_type: WinType) -> Window:
"""
Build the 2048-sample analysis/synthesis window for OLS/LSS/LPS.
In this assignment we assume a single window family is used globally
(no mixed KBD/SIN halves). Therefore, both the long and short windows
are drawn from the same family.
For frame_type:
- "OLS": return the long window Wl (2048).
- "LSS": construct [Wl_left(1024), ones(448), Ws_right(128), zeros(448)].
- "LPS": construct [zeros(448), Ws_left(128), ones(448), Wl_right(1024)].
Parameters
----------
frame_type : FrameType
One of "OLS", "LSS", "LPS".
win_type : WinType
Either "SIN" or "KBD".
Returns
-------
Window
1-D array of shape (2048,) with dtype float64.
"""
wL = _long_window(win_type) # length 2048
wS = _short_window(win_type) # length 256
if frame_type == "OLS":
return wL
if frame_type == "LSS":
# 0..1023: left half of long window
# 1024..1471: ones (448 samples)
# 1472..1599: right half of short window (128 samples)
# 1600..2047: zeros (448 samples)
out = np.zeros(2048, dtype=np.float64)
out[0:1024] = wL[0:1024]
out[1024:1472] = 1.0
out[1472:1600] = wS[128:256]
out[1600:2048] = 0.0
return out
if frame_type == "LPS":
# 0..447: zeros (448)
# 448..575: left half of short window (128)
# 576..1023: ones (448)
# 1024..2047: right half of long window (1024)
out = np.zeros(2048, dtype=np.float64)
out[0:448] = 0.0
out[448:576] = wS[0:128]
out[576:1024] = 1.0
out[1024:2048] = wL[1024:2048]
return out
raise ValueError(f"Invalid frame_type for long window sequence: {frame_type!r}")
def _filter_bank_esh_channel(x_ch: FrameChannelT, win_type: WinType) -> FrameChannelF:
"""
ESH analysis for one channel.
Parameters
----------
x_ch : FrameChannelT
Time-domain channel frame (expected shape: (2048,)).
win_type : WinType
Window family ("KBD" or "SIN").
Returns
-------
FrameChannelF
Array of shape (128, 8). Column j contains the 128 MDCT coefficients
of the j-th short window.
"""
wS = _short_window(win_type) # (256,)
X_esh = np.empty((128, 8), dtype=np.float64)
# ESH subwindows are taken from the central region:
# start positions: 448 + 128*j, j = 0..7
for j in range(8):
start = 448 + 128 * j
seg = x_ch[start:start + 256] * wS # (256,)
X_esh[:, j] = mdct(seg) # (128,)
return X_esh
def _unpack_esh(frame_F: FrameF) -> tuple[FrameChannelF, FrameChannelF]:
"""
Unpack ESH spectrum from shape (128, 16) into per-channel arrays (128, 8).
Parameters
----------
frame_F : FrameF
Packed ESH spectrum (expected shape: (128, 16)).
Returns
-------
left : FrameChannelF
Left channel spectrum, shape (128, 8).
right : FrameChannelF
Right channel spectrum, shape (128, 8).
Notes
-----
Inverse mapping of the packing used in aac_filter_bank():
packed[:, 2*j] = left[:, j]
packed[:, 2*j+1] = right[:, j]
"""
if frame_F.shape != (128, 16):
raise ValueError("ESH frame_F must have shape (128, 16).")
left = np.empty((128, 8), dtype=np.float64)
right = np.empty((128, 8), dtype=np.float64)
for j in range(8):
left[:, j] = frame_F[:, 2 * j + 0]
right[:, j] = frame_F[:, 2 * j + 1]
return left, right
def _i_filter_bank_esh_channel(X_esh: FrameChannelF, win_type: WinType) -> FrameChannelT:
"""
ESH synthesis for one channel.
Parameters
----------
X_esh : FrameChannelF
MDCT coefficients for 8 short windows (expected shape: (128, 8)).
win_type : WinType
Window family ("KBD" or "SIN").
Returns
-------
FrameChannelT
Time-domain channel contribution, shape (2048,).
This is already overlap-added internally for the 8 short blocks and
ready for OLA at the caller level.
"""
if X_esh.shape != (128, 8):
raise ValueError("X_esh must have shape (128, 8).")
wS = _short_window(win_type) # (256,)
out = np.zeros(2048, dtype=np.float64)
# Each short IMDCT returns 256 samples. Place them at:
# start = 448 + 128*j, j=0..7 (50% overlap)
for j in range(8):
seg = imdct(X_esh[:, j]) * wS # (256,)
start = 448 + 128 * j
out[start:start + 256] += seg
return out
# -----------------------------------------------------------------------------
# Public Function prototypes
# -----------------------------------------------------------------------------
def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -> FrameF:
"""
Filterbank stage (MDCT analysis).
Parameters
----------
frame_T : FrameT
Time-domain frame, stereo, shape (2048, 2).
frame_type : FrameType
Type of the frame under encoding ("OLS"|"LSS"|"ESH"|"LPS").
win_type : WinType
Window type ("KBD" or "SIN") used for the current frame.
Returns
-------
frame_F : FrameF
Frequency-domain MDCT coefficients:
- If frame_type in {"OLS","LSS","LPS"}: array shape (1024, 2)
containing MDCT coefficients for both channels.
- If frame_type == "ESH": contains 8 subframes, each subframe has shape (128,2),
placed in columns according to subframe order, i.e. overall shape (128, 16).
"""
if frame_T.shape != (2048, 2):
raise ValueError("frame_T must have shape (2048, 2).")
xL :FrameChannelT = frame_T[:, 0].astype(np.float64, copy=False)
xR :FrameChannelT = frame_T[:, 1].astype(np.float64, copy=False)
if frame_type in ("OLS", "LSS", "LPS"):
w = _window_sequence(frame_type, win_type) # length 2048
XL = mdct(xL * w) # length 1024
XR = mdct(xR * w) # length 1024
out = np.empty((1024, 2), dtype=np.float64)
out[:, 0] = XL
out[:, 1] = XR
return out
if frame_type == "ESH":
Xl = _filter_bank_esh_channel(xL, win_type) # (128, 8)
Xr = _filter_bank_esh_channel(xR, win_type) # (128, 8)
# Pack into (128, 16): each subframe as (128,2) placed in columns
out = np.empty((128, 16), dtype=np.float64)
for j in range(8):
out[:, 2 * j + 0] = Xl[:, j]
out[:, 2 * j + 1] = Xr[:, j]
return out
raise ValueError(f"Invalid frame_type: {frame_type!r}")
def aac_i_filter_bank(frame_F: FrameF, frame_type: FrameType, win_type: WinType) -> FrameT:
"""
Inverse filterbank (IMDCT synthesis).
Parameters
----------
frame_F : FrameF
Frequency-domain MDCT coefficients as produced by filter_bank().
frame_type : FrameType
Frame type ("OLS"|"LSS"|"ESH"|"LPS").
win_type : WinType
Window type ("KBD" or "SIN").
Returns
-------
frame_T : FrameT
Reconstructed time-domain frame, stereo, shape (2048, 2).
"""
if frame_type in ("OLS", "LSS", "LPS"):
if frame_F.shape != (1024, 2):
raise ValueError("For OLS/LSS/LPS, frame_F must have shape (1024, 2).")
w = _window_sequence(frame_type, win_type)
xL = imdct(frame_F[:, 0]) * w
xR = imdct(frame_F[:, 1]) * w
out = np.empty((2048, 2), dtype=np.float64)
out[:, 0] = xL
out[:, 1] = xR
return out
if frame_type == "ESH":
if frame_F.shape != (128, 16):
raise ValueError("For ESH, frame_F must have shape (128, 16).")
Xl, Xr = _unpack_esh(frame_F)
xL = _i_filter_bank_esh_channel(Xl, win_type)
xR = _i_filter_bank_esh_channel(Xr, win_type)
out = np.empty((2048, 2), dtype=np.float64)
out[:, 0] = xL
out[:, 1] = xR
return out
raise ValueError(f"Invalid frame_type: {frame_type!r}")
+112
View File
@@ -0,0 +1,112 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Huffman wrappers (Level 3)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Thin wrappers around the provided Huffman utilities (material/huff_utils.py)
# so that the API matches the assignment text.
#
# Exposed API (assignment):
# huff_sec, huff_codebook = aac_encode_huff(coeff_sec, huff_LUT_list, force_codebook)
# dec_coeffs = aac_decode_huff(huff_sec, huff_codebook, huff_LUT_list)
#
# Notes:
# - Huffman coding operates on tuples. Therefore, decode(encode(x)) may return
# extra trailing symbols due to tuple padding. The AAC decoder knows the
# true section length from side information (band limits) and truncates.
# ------------------------------------------------------------
from __future__ import annotations
from typing import Any
import numpy as np
from material.huff_utils import encode_huff, decode_huff
def aac_encode_huff(
coeff_sec: np.ndarray,
huff_LUT_list: list[dict[str, Any]],
force_codebook: int | None = None,
) -> tuple[str, int]:
"""
Huffman-encode a section of coefficients (MDCT symbols or scalefactors).
Parameters
----------
coeff_sec : np.ndarray
Coefficient section to be encoded. Any shape is accepted; the input
is flattened and treated as a 1-D sequence of int64 symbols.
huff_LUT_list : list[dict[str, Any]]
List of Huffman Look-Up Tables (LUTs) as returned by material.load_LUT().
Index corresponds to codebook id (typically 1..11, with 0 reserved).
force_codebook : int | None
If provided, forces the use of this Huffman codebook. In the assignment,
scalefactors are encoded with codebook 11. For MDCT coefficients, this
argument is usually omitted (auto-selection).
Returns
-------
tuple[str, int]
(huff_sec, huff_codebook)
- huff_sec: bitstream as a string of '0'/'1'
- huff_codebook: codebook id used by the encoder
"""
coeff_sec_arr = np.asarray(coeff_sec, dtype=np.int64).reshape(-1)
if force_codebook is None:
# Provided utility returns (bitstream, codebook) in the auto-selection case.
huff_sec, huff_codebook = encode_huff(coeff_sec_arr, huff_LUT_list)
return str(huff_sec), int(huff_codebook)
# Provided utility returns ONLY the bitstream when force_codebook is set.
cb = int(force_codebook)
huff_sec = encode_huff(coeff_sec_arr, huff_LUT_list, force_codebook=cb)
return str(huff_sec), cb
def aac_decode_huff(
huff_sec: str | np.ndarray,
huff_codebook: int,
huff_LUT: list[dict[str, Any]],
) -> np.ndarray:
"""
Huffman-decode a bitstream using the specified codebook.
Parameters
----------
huff_sec : str | np.ndarray
Huffman bitstream. Typically a string of '0'/'1'. If an array is provided,
it is passed through to the provided decoder.
huff_codebook : int
Codebook id that was returned by aac_encode_huff.
Codebook 0 represents an all-zero section.
huff_LUT : list[dict[str, Any]]
Huffman LUT list as returned by material.load_LUT().
Returns
-------
np.ndarray
Decoded coefficients as a 1-D np.int64 array.
Note: Due to tuple coding, the decoded array may contain extra trailing
padding symbols. The caller must truncate to the known section length.
"""
cb = int(huff_codebook)
if cb == 0:
# Codebook 0 represents an all-zero section. The decoded length is not
# recoverable from the bitstream alone; the caller must expand/truncate.
return np.zeros((0,), dtype=np.int64)
if cb < 0 or cb >= len(huff_LUT):
raise ValueError(f"Invalid Huffman codebook index: {cb}")
lut = huff_LUT[cb]
dec = decode_huff(huff_sec, lut)
return np.asarray(dec, dtype=np.int64).reshape(-1)
+441
View File
@@ -0,0 +1,441 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Psychoacoustic Model
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Psychoacoustic model for ONE channel, based on the assignment notes (Section 2.4).
#
# Public API:
# SMR = aac_psycho(frame_T, frame_type, frame_T_prev_1, frame_T_prev_2)
#
# Output:
# - For long frames ("OLS", "LSS", "LPS"): SMR has shape (69,)
# - For short frames ("ESH"): SMR has shape (42, 8) (one column per subframe)
#
# Notes:
# - Uses Bark band tables from material/TableB219.mat:
# * B219a for long windows (69 bands, N=2048 FFT, N/2=1024 bins)
# * B219b for short windows (42 bands, N=256 FFT, N/2=128 bins)
# - Applies a Hann window in time domain before FFT magnitude/phase extraction.
# - Implements:
# spreading function -> band spreading -> tonality index -> masking thresholds -> SMR.
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from core.aac_utils import band_limits, get_table
from core.aac_configuration import NMT_DB, TMN_DB
from core.aac_types import *
# -----------------------------------------------------------------------------
# Spreading function
# -----------------------------------------------------------------------------
def _spreading_matrix(bval: BandValueArray) -> FloatArray:
"""
Compute the spreading function matrix between psychoacoustic bands.
The spreading function describes how energy in one critical band masks
nearby bands. The formula follows the assignment pseudo-code.
Parameters
----------
bval : BandValueArray
Bark value per band, shape (B,).
Returns
-------
FloatArray
Spreading matrix S of shape (B, B), where:
S[bb, b] quantifies the contribution of band bb masking band b.
"""
bval = np.asarray(bval, dtype=np.float64).reshape(-1)
B = int(bval.shape[0])
spread = np.zeros((B, B), dtype=np.float64)
for b in range(B):
for bb in range(B):
# tmpx depends on direction (asymmetric spreading)
if bb >= b:
tmpx = 3.0 * (bval[bb] - bval[b])
else:
tmpx = 1.5 * (bval[bb] - bval[b])
# tmpz uses the "min(..., 0)" nonlinearity exactly as in the notes
tmpz = 8.0 * min((tmpx - 0.5) ** 2 - 2.0 * (tmpx - 0.5), 0.0)
tmpy = 15.811389 + 7.5 * (tmpx + 0.474) - 17.5 * np.sqrt(1.0 + (tmpx + 0.474) ** 2)
# Clamp very small values (below -100 dB) to 0 contribution
if tmpy < -100.0:
spread[bb, b] = 0.0
else:
spread[bb, b] = 10.0 ** ((tmpz + tmpy) / 10.0)
return spread
# -----------------------------------------------------------------------------
# Windowing + FFT feature extraction
# -----------------------------------------------------------------------------
def _hann_window(N: int) -> FloatArray:
"""
Hann window as specified in the notes:
w[n] = 0.5 - 0.5*cos(2*pi*(n + 0.5)/N)
Parameters
----------
N : int
Window length.
Returns
-------
FloatArray
1-D array of shape (N,), dtype float64.
"""
n = np.arange(N, dtype=np.float64)
return 0.5 - 0.5 * np.cos((2.0 * np.pi / N) * (n + 0.5))
def _r_phi_from_time(x: FrameChannelT, N: int) -> tuple[FloatArray, FloatArray]:
"""
Compute FFT magnitude r(w) and phase phi(w) for bins w = 0 .. N/2-1.
Processing:
1) Apply Hann window in time domain.
2) Compute N-point FFT.
3) Keep only the positive-frequency bins [0 .. N/2-1].
Parameters
----------
x : FrameChannelT
Time-domain samples, shape (N,).
N : int
FFT size (2048 or 256).
Returns
-------
r : FloatArray
Magnitude spectrum for bins 0 .. N/2-1, shape (N/2,).
phi : FloatArray
Phase spectrum for bins 0 .. N/2-1, shape (N/2,).
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
if x.shape[0] != N:
raise ValueError(f"Expected time vector of length {N}, got {x.shape[0]}.")
w = _hann_window(N)
X = np.fft.fft(x * w, n=N)
Xp = X[: N // 2]
r = np.abs(Xp).astype(np.float64, copy=False)
phi = np.angle(Xp).astype(np.float64, copy=False)
return r, phi
def _predictability(
r: FloatArray,
phi: FloatArray,
r_m1: FloatArray,
phi_m1: FloatArray,
r_m2: FloatArray,
phi_m2: FloatArray,
) -> FloatArray:
"""
Compute predictability c(w) per spectral bin.
The notes define:
r_pred(w) = 2*r_{-1}(w) - r_{-2}(w)
phi_pred(w) = 2*phi_{-1}(w) - phi_{-2}(w)
c(w) = |X(w) - X_pred(w)| / (r(w) + |r_pred(w)|)
where X(w) is represented in polar form using r(w), phi(w).
Parameters
----------
r, phi : FloatArray
Current magnitude and phase, shape (N/2,).
r_m1, phi_m1 : FloatArray
Previous magnitude and phase, shape (N/2,).
r_m2, phi_m2 : FloatArray
Pre-previous magnitude and phase, shape (N/2,).
Returns
-------
FloatArray
Predictability c(w), shape (N/2,).
"""
r_pred = 2.0 * r_m1 - r_m2
phi_pred = 2.0 * phi_m1 - phi_m2
num = np.sqrt(
(r * np.cos(phi) - r_pred * np.cos(phi_pred)) ** 2
+ (r * np.sin(phi) - r_pred * np.sin(phi_pred)) ** 2
)
den = r + np.abs(r_pred) + 1e-12 # avoid division-by-zero without altering behavior
return (num / den).astype(np.float64, copy=False)
# -----------------------------------------------------------------------------
# Band-domain aggregation
# -----------------------------------------------------------------------------
def _band_energy_and_pred(
r: FloatArray,
c: FloatArray,
wlow: BandIndexArray,
whigh: BandIndexArray,
) -> tuple[FloatArray, FloatArray]:
"""
Aggregate spectral bin quantities into psychoacoustic bands.
Definitions (notes):
e(b) = sum_{w=wlow(b)..whigh(b)} r(w)^2
c_num(b) = sum_{w=wlow(b)..whigh(b)} c(w) * r(w)^2
The band predictability c(b) is later computed after spreading as:
cb(b) = ct(b) / ecb(b)
Parameters
----------
r : FloatArray
Magnitude spectrum, shape (N/2,).
c : FloatArray
Predictability per bin, shape (N/2,).
wlow, whigh : BandIndexArray
Band limits (inclusive indices), shape (B,).
Returns
-------
e_b : FloatArray
Band energies e(b), shape (B,).
c_num_b : FloatArray
Weighted predictability numerators c_num(b), shape (B,).
"""
r2 = (r * r).astype(np.float64, copy=False)
B = int(wlow.shape[0])
e_b = np.zeros(B, dtype=np.float64)
c_num_b = np.zeros(B, dtype=np.float64)
for b in range(B):
a = int(wlow[b])
z = int(whigh[b])
seg_r2 = r2[a : z + 1]
e_b[b] = float(np.sum(seg_r2))
c_num_b[b] = float(np.sum(c[a : z + 1] * seg_r2))
return e_b, c_num_b
def _psycho_window(
time_x: FrameChannelT,
prev1_x: FrameChannelT,
prev2_x: FrameChannelT,
*,
N: int,
table: BarkTable,
) -> FloatArray:
"""
Compute SMR for one FFT analysis window (N=2048 for long, N=256 for short).
This implements the pipeline described in the notes:
- FFT magnitude/phase
- predictability per bin
- band energies and predictability
- band spreading
- tonality index tb(b)
- masking threshold (noise + threshold in quiet)
- SMR(b) = e(b) / np(b)
Parameters
----------
time_x : FrameChannelT
Current time-domain samples, shape (N,).
prev1_x : FrameChannelT
Previous time-domain samples, shape (N,).
prev2_x : FrameChannelT
Pre-previous time-domain samples, shape (N,).
N : int
FFT size.
table : BarkTable
Psychoacoustic band table (B219a or B219b).
Returns
-------
FloatArray
SMR per band, shape (B,).
"""
wlow, whigh, bval, qthr_db = band_limits(table)
spread = _spreading_matrix(bval)
# FFT features for current and history windows
r, phi = _r_phi_from_time(time_x, N)
r_m1, phi_m1 = _r_phi_from_time(prev1_x, N)
r_m2, phi_m2 = _r_phi_from_time(prev2_x, N)
# Predictability per bin
c_w = _predictability(r, phi, r_m1, phi_m1, r_m2, phi_m2)
# Aggregate into psycho bands
e_b, c_num_b = _band_energy_and_pred(r, c_w, wlow, whigh)
# Spread energies and predictability across bands:
# ecb(b) = sum_bb e(bb) * S(bb, b)
# ct(b) = sum_bb c_num(bb) * S(bb, b)
ecb = spread.T @ e_b
ct = spread.T @ c_num_b
# Band predictability after spreading: cb(b) = ct(b) / ecb(b)
cb = ct / (ecb + 1e-12)
# Normalized energy term:
# en(b) = ecb(b) / sum_bb S(bb, b)
spread_colsum = np.sum(spread, axis=0)
en = ecb / (spread_colsum + 1e-12)
# Tonality index (clamped to [0, 1])
tb = -0.299 - 0.43 * np.log(np.maximum(cb, 1e-12))
tb = np.clip(tb, 0.0, 1.0)
# Required SNR per band (dB): interpolate between TMN and NMT
snr_b = tb * TMN_DB + (1.0 - tb) * NMT_DB
bc = 10.0 ** (-snr_b / 10.0)
# Noise masking threshold estimate (power domain)
nb = en * bc
# Threshold in quiet (convert from dB to power domain):
# qthr_power = eps * (N/2) * 10^(qthr_db/10)
qthr_power = np.finfo('float').eps * (N / 2.0) * (10.0 ** (qthr_db / 10.0))
# Final masking threshold per band:
# np(b) = max(nb(b), qthr(b))
npart = np.maximum(nb, qthr_power)
# Signal-to-mask ratio:
# SMR(b) = e(b) / np(b)
smr = e_b / (npart + 1e-12)
return smr.astype(np.float64, copy=False)
# -----------------------------------------------------------------------------
# ESH window slicing (match filterbank conventions)
# -----------------------------------------------------------------------------
def _esh_subframes(x_2048: FrameChannelT) -> list[FrameChannelT]:
"""
Extract the 8 overlapping 256-sample short windows used by AAC ESH.
The project convention (matching the filterbank) is:
start_j = 448 + 128*j, for j = 0..7
subframe_j = x[start_j : start_j + 256]
This selects the central 1152-sample region [448, 1600) and produces
8 windows with 50% overlap.
Parameters
----------
x_2048 : FrameChannelT
Time-domain channel frame, shape (2048,).
Returns
-------
list[FrameChannelT]
List of 8 subframes, each of shape (256,).
"""
x_2048 = np.asarray(x_2048, dtype=np.float64).reshape(-1)
if x_2048.shape[0] != 2048:
raise ValueError("ESH requires 2048-sample input frames.")
subs: list[FrameChannelT] = []
for j in range(8):
start = 448 + 128 * j
subs.append(x_2048[start : start + 256])
return subs
# -----------------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------------
def aac_psycho(
frame_T: FrameChannelT,
frame_type: FrameType,
frame_T_prev_1: FrameChannelT,
frame_T_prev_2: FrameChannelT,
) -> FloatArray:
"""
Psychoacoustic model for ONE channel.
Parameters
----------
frame_T : FrameChannelT
Current time-domain channel frame, shape (2048,).
For "ESH", the 8 short windows are derived internally.
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
frame_T_prev_1 : FrameChannelT
Previous time-domain channel frame, shape (2048,).
frame_T_prev_2 : FrameChannelT
Pre-previous time-domain channel frame, shape (2048,).
Returns
-------
FloatArray
Signal-to-Mask Ratio (SMR), per psychoacoustic band.
- If frame_type == "ESH": shape (42, 8)
- Else: shape (69,)
"""
frame_T = np.asarray(frame_T, dtype=np.float64).reshape(-1)
frame_T_prev_1 = np.asarray(frame_T_prev_1, dtype=np.float64).reshape(-1)
frame_T_prev_2 = np.asarray(frame_T_prev_2, dtype=np.float64).reshape(-1)
if frame_T.shape[0] != 2048 or frame_T_prev_1.shape[0] != 2048 or frame_T_prev_2.shape[0] != 2048:
raise ValueError("aac_psycho expects 2048-sample frames for current/prev1/prev2.")
table, N = get_table(frame_type)
# Long frame types: compute one SMR vector (69 bands)
if frame_type != "ESH":
return _psycho_window(frame_T, frame_T_prev_1, frame_T_prev_2, N=N, table=table)
# ESH: compute 8 SMR vectors (42 bands each), one per short subframe.
#
# The notes use short-window history for predictability:
# - For j=0: use previous frame's subframes (7, 6)
# - For j=1: use current subframe 0 and previous frame's subframe 7
# - For j>=2: use current subframes (j-1, j-2)
#
# This matches the "within-frame history" convention commonly used in
# simplified psycho models for ESH.
cur_subs = _esh_subframes(frame_T)
prev1_subs = _esh_subframes(frame_T_prev_1)
B = int(table.shape[0]) # expected 42
smr_out = np.zeros((B, 8), dtype=np.float64)
for j in range(8):
if j == 0:
x_m1 = prev1_subs[7]
x_m2 = prev1_subs[6]
elif j == 1:
x_m1 = cur_subs[0]
x_m2 = prev1_subs[7]
else:
x_m1 = cur_subs[j - 1]
x_m2 = cur_subs[j - 2]
smr_out[:, j] = _psycho_window(cur_subs[j], x_m1, x_m2, N=256, table=table)
return smr_out
+600
View File
@@ -0,0 +1,600 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Quantizer / iQuantizer (Level 3)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Implements AAC quantizer and inverse quantizer for one channel.
# Based on assignment section 2.6 (Eq. 12-15).
#
# Notes:
# - Bit reservoir is not implemented (assignment simplification).
# - Scalefactor bands are assumed equal to psychoacoustic bands
# (Table B.2.1.9a / B.2.1.9b from TableB219.mat).
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from core.aac_utils import get_table, band_limits
from core.aac_types import *
# -----------------------------------------------------------------------------
# Constants (assignment)
# -----------------------------------------------------------------------------
MAGIC_NUMBER: float = 0.4054
EPS: float = 1e-12
MAX_SF_DELTA:int = 60
# -----------------------------------------------------------------------------
# Helpers: ESH packing/unpacking (128x8 <-> 1024x1)
# -----------------------------------------------------------------------------
def _esh_pack(x_128x8: FloatArray) -> FloatArray:
"""
Pack ESH coefficients (128 x 8) into a single long vector (1024 x 1).
Packing order:
Columns are concatenated in subframe order (0..7), column-major.
Parameters
----------
x_128x8 : FloatArray
ESH coefficients, shape (128, 8).
Returns
-------
FloatArray
Packed coefficients, shape (1024, 1).
"""
x_128x8 = np.asarray(x_128x8, dtype=np.float64)
if x_128x8.shape != (128, 8):
raise ValueError("ESH pack expects shape (128, 8).")
return x_128x8.reshape(1024, 1, order="F")
def _esh_unpack(x_1024x1: FloatArray) -> FloatArray:
"""
Unpack a packed ESH vector (1024 elements) back to shape (128, 8).
Parameters
----------
x_1024x1 : FloatArray
Packed ESH vector, shape (1024,) or (1024, 1) after flattening.
Returns
-------
FloatArray
Unpacked ESH coefficients, shape (128, 8).
"""
x_1024x1 = np.asarray(x_1024x1, dtype=np.float64).reshape(-1)
if x_1024x1.shape[0] != 1024:
raise ValueError("ESH unpack expects 1024 elements.")
return x_1024x1.reshape(128, 8, order="F")
# -----------------------------------------------------------------------------
# Core quantizer formulas (Eq. 12, Eq. 13)
# -----------------------------------------------------------------------------
def _quantize_symbol(x: FloatArray, alpha: float) -> QuantizedSymbols:
"""
Quantize MDCT coefficients to integer symbols S(k).
Implements Eq. (12):
S(k) = sgn(X(k)) * int( (|X(k)| * 2^(-alpha/4))^(3/4) + MAGIC_NUMBER )
Parameters
----------
x : FloatArray
MDCT coefficients for a contiguous set of spectral lines.
Shape: (N,)
alpha : float
Scalefactor gain for the corresponding scalefactor band.
Returns
-------
QuantizedSymbols
Quantized symbols S(k) as int64, shape (N,).
"""
x = np.asarray(x, dtype=np.float64)
scale = 2.0 ** (-0.25 * float(alpha))
ax = np.abs(x) * scale
y = np.power(ax, 0.75, dtype=np.float64)
# "int" in the assignment corresponds to truncation.
q = np.floor(y + MAGIC_NUMBER).astype(np.int64)
return (np.sign(x).astype(np.int64) * q).astype(np.int64)
def _dequantize_symbol(S: QuantizedSymbols, alpha: float) -> FloatArray:
"""
Inverse quantizer (dequantization of symbols).
Implements Eq. (13):
Xhat(k) = sgn(S(k)) * |S(k)|^(4/3) * 2^(alpha/4)
Parameters
----------
S : QuantizedSymbols
Quantized symbols S(k), int64, shape (N,).
alpha : float
Scalefactor gain for the corresponding scalefactor band.
Returns
-------
FloatArray
Reconstructed MDCT coefficients Xhat(k), float64, shape (N,).
"""
S = np.asarray(S, dtype=np.int64)
scale = 2.0 ** (0.25 * float(alpha))
aS = np.abs(S).astype(np.float64)
y = np.power(aS, 4.0 / 3.0, dtype=np.float64)
return (np.sign(S).astype(np.float64) * y * scale).astype(np.float64)
# -----------------------------------------------------------------------------
# Alpha initialization (Eq. 14)
# -----------------------------------------------------------------------------
def _initial_alpha_hat(X: "FloatArray", MQ: int = 8191) -> int:
"""
Compute the initial scalefactor estimate alpha_hat for a frame.
The assignment proposes the following first approximation (Equation 14):
alpha_hat = (16/3) * log2( max_k(|X(k)|)^(3/4) / MQ )
where max_k runs over all MDCT coefficients of the frame (not per band),
and MQ is the maximum quantization level parameter (2*MQ + 1 levels).
Parameters
----------
X : FloatArray
MDCT coefficients of one frame (or one ESH subframe), shape (N,).
MQ : int
Quantizer parameter (default 8191, as per assignment).
Returns
-------
int
Integer alpha_hat (rounded to nearest integer).
"""
x_max = float(np.max(np.abs(X)))
if x_max <= 0.0:
return 0
alpha_hat = (16.0 / 3.0) * np.log2((x_max ** (3.0 / 4.0)) / float(MQ))
return int(np.round(alpha_hat))
# -----------------------------------------------------------------------------
# Band utilities
# -----------------------------------------------------------------------------
def _band_slices(frame_type: FrameType) -> list[tuple[int, int]]:
"""
Return scalefactor band ranges [wlow, whigh] (inclusive) for the given frame type.
These are derived from the psychoacoustic tables (TableB219),
and map directly to MDCT indices:
- long: 0..1023
- short (ESH subframe): 0..127
Parameters
----------
frame_type : FrameType
Frame type ("OLS", "LSS", "ESH", "LPS").
Returns
-------
list[tuple[int, int]]
List of (lo, hi) inclusive index pairs for each band.
"""
table, _Nfft = get_table(frame_type)
wlow, whigh, _bval, _qthr_db = band_limits(table)
bands: list[tuple[int, int]] = []
for lo, hi in zip(wlow, whigh):
bands.append((int(lo), int(hi)))
return bands
def _band_energy(x: FloatArray, lo: int, hi: int) -> float:
"""
Compute energy of a spectral segment x[lo:hi+1].
Parameters
----------
x : FloatArray
MDCT coefficient vector.
lo, hi : int
Inclusive index range.
Returns
-------
float
Sum of squares (energy) within the band.
"""
sec = x[lo : hi + 1]
return float(np.sum(sec * sec))
def _psychoacoustic_threshold(
X: FloatArray,
SMR_col: FloatArray,
bands: list[tuple[int, int]],
) -> FloatArray:
"""
Compute psychoacoustic thresholds T(b) per band.
Uses:
P(b) = sum_{k in band} X(k)^2
T(b) = P(b) / SMR(b)
Parameters
----------
X : FloatArray
MDCT coefficients for a frame (long) or one ESH subframe (short).
SMR_col : FloatArray
SMR values for this frame/subframe, shape (NB,).
bands : list[tuple[int, int]]
Band index ranges.
Returns
-------
FloatArray
Threshold vector T(b), shape (NB,).
"""
nb = len(bands)
T = np.zeros((nb,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
P = _band_energy(X, lo, hi)
smr = float(SMR_col[b])
if smr <= EPS:
T[b] = 0.0
else:
T[b] = P / smr
return T
# -----------------------------------------------------------------------------
# Alpha selection per band + neighbor-difference constraint
# -----------------------------------------------------------------------------
def _best_alpha_for_band(
X: "FloatArray", lo: int, hi: int, T_b: float,
alpha_hat: int, alpha_prev: int, alpha_min: int, alpha_max: int,
) -> int:
"""
Determine the band-wise scalefactor alpha(b) following the assignment.
Procedure:
- Start from a frame-wise initial estimate alpha_hat.
- Iteratively increase alpha(b) by 1 as long as the quantization error power
stays below the psychoacoustic threshold T(b): P_e(b) = sum_{k in band} ( X(k) - Xhat(k) )^2
- Stop increasing alpha(b) if the neighbor constraint would be violated: |alpha(b) - alpha(b-1)| <= 60
When processing bands sequentially (low -> high), this becomes: alpha(b) <= alpha_prev + 60
Notes:
- This function does not decrease alpha if the initial value already violates
the threshold; the assignment only specifies iterative increase.
Parameters
----------
X : FloatArray
Full MDCT vector of the current (sub)frame, shape (N,).
lo, hi : int
Band index bounds (inclusive), defining the band slice.
T_b : float
Threshold T(b) for this band.
alpha_hat : int
Initial frame-wise estimate (Equation 14).
alpha_prev : int
Previously selected alpha for band b-1 (neighbor constraint reference).
alpha_min, alpha_max : int
Safeguard bounds for alpha.
Returns
-------
int
Selected integer alpha(b).
"""
if T_b <= 0.0:
return int(alpha_hat)
Xsec = X[lo : hi + 1]
# Neighbor constraint (sequential processing): alpha(b) <= alpha_prev + 60
alpha_limit = min(int(alpha_max), int(alpha_prev) + MAX_SF_DELTA)
# Start from alpha_hat, clamped to feasible range
alpha = int(alpha_hat)
alpha = max(int(alpha_min), min(alpha, int(alpha_limit)))
# Evaluate at current alpha
Ssec = _quantize_symbol(Xsec, alpha)
Xhat = _dequantize_symbol(Ssec, alpha)
Pe = float(np.sum((Xsec - Xhat) ** 2))
# If already above threshold, return current alpha (no decrease step specified)
if Pe > T_b:
return alpha
# Increase alpha while still under threshold and within constraints
while True:
alpha_next = alpha + 1
if alpha_next > alpha_limit:
break
Ssec = _quantize_symbol(Xsec, alpha_next)
Xhat = _dequantize_symbol(Ssec, alpha_next)
Pe_next = float(np.sum((Xsec - Xhat) ** 2))
if Pe_next > T_b:
break
alpha = alpha_next
return alpha
# -----------------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------------
def aac_quantizer(
frame_F: FrameChannelF,
frame_type: FrameType,
SMR: FloatArray,
) -> tuple[QuantizedSymbols, ScaleFactors, GlobalGain]:
"""
AAC quantizer for one channel (Level 3).
Quantizes MDCT coefficients (after TNS) using band-wise scalefactors derived
from psychoacoustic thresholds computed via SMR.
The implementation follows the assignment procedure:
- Compute an initial frame-wise alpha_hat using Equation (14), based on the
maximum MDCT coefficient magnitude of the (sub)frame.
- For each band b, increase alpha(b) by 1 while the quantization error power
P_e(b) stays below the threshold T(b).
- Enforce the neighbor constraint |alpha(b) - alpha(b-1)| <= 60 during the
band-by-band search (no post-processing needed).
Parameters
----------
frame_F : FrameChannelF
MDCT coefficients after TNS, one channel.
Shapes:
- Long frames: (1024,) or (1024, 1)
- ESH: (128, 8)
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
SMR : FloatArray
Signal-to-Mask Ratio per band.
Shapes:
- Long: (NB,) or (NB, 1)
- ESH: (NB, 8)
Returns
-------
S : QuantizedSymbols
Quantized symbols S(k), packed as shape (1024, 1) for all frame types.
For ESH, the 8 subframes are packed in column-major subframe layout.
sfc : ScaleFactors
DPCM-coded scalefactors:
sfc(0) = alpha(0) = G
sfc(b) = alpha(b) - alpha(b-1), for b > 0
Shapes:
- Long: (NB, 1)
- ESH: (NB, 8)
G : GlobalGain
Global gain G = alpha(0).
- Long: scalar float
- ESH: array shape (1, 8), dtype float64
"""
bands = _band_slices(frame_type)
NB = len(bands)
X = np.asarray(frame_F, dtype=np.float64)
SMR = np.asarray(SMR, dtype=np.float64)
# -------------------------------------------------------------------------
# ESH: 8 short subframes, each of length 128
# -------------------------------------------------------------------------
if frame_type == "ESH":
if X.shape != (128, 8):
raise ValueError("For ESH, frame_F must have shape (128, 8).")
if SMR.shape != (NB, 8):
raise ValueError(f"For ESH, SMR must have shape ({NB}, 8).")
S_out: QuantizedSymbols = np.zeros((1024, 1), dtype=np.int64)
sfc: ScaleFactors = np.zeros((NB, 8), dtype=np.int64)
G_arr = np.zeros((1, 8), dtype=np.float64)
# Packed output view: (128, 8) with column-major layout
S_pack = S_out[:, 0].reshape(128, 8, order="F")
for j in range(8):
Xj = X[:, j].reshape(128)
SMRj = SMR[:, j].reshape(NB)
# Compute psychoacoustic threshold T(b) for this subframe
T = _psychoacoustic_threshold(Xj, SMRj, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xj)
# Band-wise scalefactors alpha(b)
alpha = np.zeros((NB,), dtype=np.int64)
alpha_prev = int(alpha_hat)
for b, (lo, hi) in enumerate(bands):
alpha_b = _best_alpha_for_band(
X=Xj,
lo=lo,
hi=hi,
T_b=float(T[b]),
alpha_hat=int(alpha_hat),
alpha_prev=int(alpha_prev),
alpha_min=-4096,
alpha_max=4096,
)
alpha[b] = int(alpha_b)
alpha_prev = int(alpha_b)
# DPCM-coded scalefactors
G_arr[0, j] = float(alpha[0])
sfc[0, j] = int(alpha[0])
for b in range(1, NB):
sfc[b, j] = int(alpha[b] - alpha[b - 1])
# Quantize MDCT coefficients band-by-band
Sj = np.zeros((128,), dtype=np.int64)
for b, (lo, hi) in enumerate(bands):
Sj[lo : hi + 1] = _quantize_symbol(Xj[lo : hi + 1], float(alpha[b]))
# Store subframe in packed output
S_pack[:, j] = Sj
return S_out, sfc, G_arr
# -------------------------------------------------------------------------
# Long frames: OLS / LSS / LPS, length 1024
# -------------------------------------------------------------------------
if X.shape == (1024,):
Xv = X
elif X.shape == (1024, 1):
Xv = X[:, 0]
else:
raise ValueError("For non-ESH, frame_F must have shape (1024,) or (1024, 1).")
if SMR.shape == (NB,):
SMRv = SMR
elif SMR.shape == (NB, 1):
SMRv = SMR[:, 0]
else:
raise ValueError(f"For non-ESH, SMR must have shape ({NB},) or ({NB}, 1).")
# Compute psychoacoustic threshold T(b) for the long frame
T = _psychoacoustic_threshold(Xv, SMRv, bands)
# Frame-wise initial estimate alpha_hat (Equation 14)
alpha_hat = _initial_alpha_hat(Xv)
# Band-wise scalefactors alpha(b)
alpha = np.zeros((NB,), dtype=np.int64)
alpha_prev = int(alpha_hat)
for b, (lo, hi) in enumerate(bands):
alpha_b = _best_alpha_for_band(
X=Xv,
lo=lo,
hi=hi,
T_b=float(T[b]),
alpha_hat=int(alpha_hat),
alpha_prev=int(alpha_prev),
alpha_min=-4096,
alpha_max=4096,
)
alpha[b] = int(alpha_b)
alpha_prev = int(alpha_b)
# DPCM-coded scalefactors
sfc_out: ScaleFactors = np.zeros((NB, 1), dtype=np.int64)
sfc_out[0, 0] = int(alpha[0])
for b in range(1, NB):
sfc_out[b, 0] = int(alpha[b] - alpha[b - 1])
G: float = float(alpha[0])
# Quantize MDCT coefficients band-by-band
S_vec = np.zeros((1024,), dtype=np.int64)
for b, (lo, hi) in enumerate(bands):
S_vec[lo : hi + 1] = _quantize_symbol(Xv[lo : hi + 1], float(alpha[b]))
return S_vec.reshape(1024, 1), sfc_out, G
def aac_i_quantizer(
S: QuantizedSymbols,
sfc: ScaleFactors,
G: GlobalGain,
frame_type: FrameType,
) -> FrameChannelF:
"""
Inverse quantizer (iQuantizer) for one channel.
Reconstructs MDCT coefficients from quantized symbols and DPCM scalefactors.
Parameters
----------
S : QuantizedSymbols
Quantized symbols, shape (1024, 1) (or any array with 1024 elements).
sfc : ScaleFactors
DPCM-coded scalefactors.
Shapes:
- Long: (NB, 1)
- ESH: (NB, 8)
G : GlobalGain
Global gain (not strictly required if sfc includes sfc(0)=alpha(0)).
Present for API compatibility with the assignment.
frame_type : FrameType
AAC frame type.
Returns
-------
FrameChannelF
Reconstructed MDCT coefficients:
- ESH: (128, 8)
- Long: (1024, 1)
"""
bands = _band_slices(frame_type)
NB = len(bands)
S_flat = np.asarray(S, dtype=np.int64).reshape(-1)
if S_flat.shape[0] != 1024:
raise ValueError("S must contain 1024 symbols.")
if frame_type == "ESH":
sfc = np.asarray(sfc, dtype=np.int64)
if sfc.shape != (NB, 8):
raise ValueError(f"For ESH, sfc must have shape ({NB}, 8).")
S_128x8 = _esh_unpack(S_flat)
Xrec = np.zeros((128, 8), dtype=np.float64)
for j in range(8):
alpha = np.zeros((NB,), dtype=np.int64)
alpha[0] = int(sfc[0, j])
for b in range(1, NB):
alpha[b] = int(alpha[b - 1] + sfc[b, j])
Xj = np.zeros((128,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
Xj[lo : hi + 1] = _dequantize_symbol(S_128x8[lo : hi + 1, j].astype(np.int64), float(alpha[b]))
Xrec[:, j] = Xj
return Xrec
sfc = np.asarray(sfc, dtype=np.int64)
if sfc.shape != (NB, 1):
raise ValueError(f"For non-ESH, sfc must have shape ({NB}, 1).")
alpha = np.zeros((NB,), dtype=np.int64)
alpha[0] = int(sfc[0, 0])
for b in range(1, NB):
alpha[b] = int(alpha[b - 1] + sfc[b, 0])
Xrec = np.zeros((1024,), dtype=np.float64)
for b, (lo, hi) in enumerate(bands):
Xrec[lo : hi + 1] = _dequantize_symbol(S_flat[lo : hi + 1], float(alpha[b]))
return Xrec.reshape(1024, 1)
+217
View File
@@ -0,0 +1,217 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Sequence Segmentation Control module
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Sequence Segmentation Control module (SSC).
# Selects and returns the frame type based on input parameters.
# ------------------------------------------------------------
from __future__ import annotations
from typing import Dict, Tuple
from core.aac_types import FrameType, FrameT, FrameChannelT
import numpy as np
# -----------------------------------------------------------------------------
# Private helpers for SSC
# -----------------------------------------------------------------------------
# See Table 1 in mm-2025-hw-v0.1.pdf
STEREO_MERGE_TABLE: Dict[Tuple[FrameType, FrameType], FrameType] = {
("OLS", "OLS"): "OLS",
("OLS", "LSS"): "LSS",
("OLS", "ESH"): "ESH",
("OLS", "LPS"): "LPS",
("LSS", "OLS"): "LSS",
("LSS", "LSS"): "LSS",
("LSS", "ESH"): "ESH",
("LSS", "LPS"): "ESH",
("ESH", "OLS"): "ESH",
("ESH", "LSS"): "ESH",
("ESH", "ESH"): "ESH",
("ESH", "LPS"): "ESH",
("LPS", "OLS"): "LPS",
("LPS", "LSS"): "ESH",
("LPS", "ESH"): "ESH",
("LPS", "LPS"): "LPS",
}
def _detect_attack(next_frame_channel: FrameChannelT) -> bool:
"""
Detect whether the *next* frame (single channel) implies an attack, i.e. ESH
according to the assignment's criterion.
Parameters
----------
next_frame_channel : FrameChannelT
One channel of next_frame_T (expected shape: (2048,)).
Returns
-------
bool
True if an attack is detected (=> next frame predicted ESH), else False.
Notes
-----
The criterion is implemented as described in the spec:
1) Apply the high-pass filter:
H(z) = (1 - z^-1) / (1 - 0.5 z^-1)
implemented in the time domain as:
y[n] = x[n] - x[n-1] + 0.5*y[n-1]
2) Split y into 16 segments of length 128 and compute segment energies s[l].
3) Compute the ratio:
ds[l] = s[l] / s[l-1]
4) An attack exists if there exists l in {1..7} such that:
s[l] > 1e-3 and ds[l] > 10
"""
# Local alias; expected to be a 1-D array of length 2048.
x = next_frame_channel
# High-pass filter reference implementation (scalar recurrence).
y = np.zeros_like(x)
prev_x = 0.0
prev_y = 0.0
for n in range(x.shape[0]):
xn = float(x[n])
yn = (xn - prev_x) + 0.5 * prev_y
y[n] = yn
prev_x = xn
prev_y = yn
# Segment energies over 16 blocks of 128 samples.
s = np.empty(16, dtype=np.float64)
for l in range(16):
a = l * 128
b = (l + 1) * 128
seg = y[a:b]
s[l] = float(np.sum(seg * seg))
# ds[l] for l>=1. For l=0 not defined, keep 0.
ds = np.zeros(16, dtype=np.float64)
eps = 1e-12 # Avoid division by zero without materially changing the logic.
for l in range(1, 16):
ds[l] = s[l] / max(s[l - 1], eps)
# Spec: check l in {1..7}.
for l in range(1, 8):
if (s[l] > 1e-3) and (ds[l] > 10.0):
return True
return False
def _decide_frame_type(prev_frame_type: FrameType, attack: bool) -> FrameType:
"""
Decide the current frame type for a single channel based on the previous
frame type and whether the next frame is predicted to be ESH.
Rules (spec):
- If prev is "LSS" => current is "ESH"
- If prev is "LPS" => current is "OLS"
- If prev is "OLS" => current is "LSS" if attack else "OLS"
- If prev is "ESH" => current is "ESH" if attack else "LPS"
Parameters
----------
prev_frame_type : FrameType
Previous frame type (one of "OLS", "LSS", "ESH", "LPS").
attack : bool
True if the next frame is predicted ESH for this channel.
Returns
-------
FrameType
The per-channel decision for the current frame.
"""
if prev_frame_type == "LSS":
return "ESH"
if prev_frame_type == "LPS":
return "OLS"
if prev_frame_type == "OLS":
return "LSS" if attack else "OLS"
if prev_frame_type == "ESH":
return "ESH" if attack else "LPS"
raise ValueError(f"Invalid prev_frame_type: {prev_frame_type!r}")
def _stereo_merge(ft_l: FrameType, ft_r: FrameType) -> FrameType:
"""
Merge per-channel frame type decisions into one common frame type using
the stereo merge table from the spec.
Parameters
----------
ft_l : FrameType
Frame type decision for the left channel.
ft_r : FrameType
Frame type decision for the right channel.
Returns
-------
FrameType
The merged common frame type.
"""
try:
return STEREO_MERGE_TABLE[(ft_l, ft_r)]
except KeyError as e:
raise ValueError(f"Invalid stereo merge pair: {(ft_l, ft_r)}") from e
# -----------------------------------------------------------------------------
# Public Function prototypes
# -----------------------------------------------------------------------------
def aac_ssc(frame_T: FrameT, next_frame_T: FrameT, prev_frame_type: FrameType) -> FrameType:
"""
Sequence Segmentation Control (SSC).
Select and return the frame type for the current frame (i) based on:
- the current time-domain frame (stereo),
- the next time-domain frame (stereo), used for attack detection,
- the previous frame type.
Parameters
----------
frame_T : FrameT
Current time-domain frame i (expected shape: (2048, 2)).
next_frame_T : FrameT
Next time-domain frame (i+1), used to decide transitions to/from ESH
(expected shape: (2048, 2)).
prev_frame_type : FrameType
Frame type chosen for the previous frame (i-1).
Returns
-------
FrameType
One of: "OLS", "LSS", "ESH", "LPS".
"""
if frame_T.shape != (2048, 2):
raise ValueError("frame_T must have shape (2048, 2).")
if next_frame_T.shape != (2048, 2):
raise ValueError("next_frame_T must have shape (2048, 2).")
# Detect attack independently per channel on the next frame.
attack_l = _detect_attack(next_frame_T[:, 0])
attack_r = _detect_attack(next_frame_T[:, 1])
# Decide per-channel type based on shared prev_frame_type.
ft_l = _decide_frame_type(prev_frame_type, attack_l)
ft_r = _decide_frame_type(prev_frame_type, attack_r)
# Stereo merge as per the spec table.
return _stereo_merge(ft_l, ft_r)
+514
View File
@@ -0,0 +1,514 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Temporal Noise Shaping (TNS)
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Temporal Noise Shaping (TNS) module (Level 2).
#
# Public API:
# frame_F_out, tns_coeffs = aac_tns(frame_F_in, frame_type)
# frame_F_out = aac_i_tns(frame_F_in, frame_type, tns_coeffs)
#
# Notes (per assignment):
# - TNS is applied per channel (not stereo).
# - For ESH, TNS is applied independently to each of the 8 short subframes.
# - Bark band tables are taken from TableB.2.1.9a (long) and TableB.2.1.9b (short)
# provided in TableB219.mat.
# - Predictor order is fixed to p = 4.
# - Coefficients are quantized with a 4-bit uniform symmetric quantizer, step = 0.1.
# - Forward TNS applies FIR: H_TNS(z) = 1 - a1 z^-1 - ... - ap z^-p
# - Inverse TNS applies the inverse IIR filter using the same quantized coefficients.
# ------------------------------------------------------------
from __future__ import annotations
from pathlib import Path
from typing import Tuple
from core.aac_utils import load_b219_tables
from core.aac_configuration import PRED_ORDER, QUANT_STEP, QUANT_MAX
from core.aac_types import *
# -----------------------------------------------------------------------------
# Private helpers
# -----------------------------------------------------------------------------
def _band_ranges(k_count: int) -> BandRanges:
"""
Return Bark band index ranges [start, end] (inclusive) for the given MDCT line count.
Parameters
----------
k_count : int
Number of MDCT lines:
- 1024 for long frames
- 128 for short subframes (ESH)
Returns
-------
BandRanges (list[tuple[int, int]])
Each tuple is (start_k, end_k) inclusive.
"""
tables = load_b219_tables()
if k_count == 1024:
tbl = tables["B219a"]
elif k_count == 128:
tbl = tables["B219b"]
else:
raise ValueError("TNS supports only k_count=1024 (long) or k_count=128 (short).")
start = tbl[:, 1].astype(int)
end = tbl[:, 2].astype(int)
ranges: BandRanges = [(int(s), int(e)) for s, e in zip(start, end)]
for s, e in ranges:
if s < 0 or e < s or e >= k_count:
raise ValueError("Invalid band table ranges for given k_count.")
return ranges
# -----------------------------------------------------------------------------
# Core DSP helpers
# -----------------------------------------------------------------------------
def _smooth_sw_inplace(sw: MdctCoeffs) -> None:
"""
Smooth Sw(k) to reduce discontinuities between adjacent Bark bands.
The assignment applies two passes:
- Backward: Sw(k) = (Sw(k) + Sw(k+1))/2
- Forward: Sw(k) = (Sw(k) + Sw(k-1))/2
Parameters
----------
sw : MdctCoeffs
1-D array of length K (float64). Modified in-place.
"""
k_count = int(sw.shape[0])
for k in range(k_count - 2, -1, -1):
sw[k] = 0.5 * (sw[k] + sw[k + 1])
for k in range(1, k_count):
sw[k] = 0.5 * (sw[k] + sw[k - 1])
def _compute_sw(x: MdctCoeffs) -> MdctCoeffs:
"""
Compute Sw(k) from band energies P(j) and apply boundary smoothing.
Parameters
----------
x : MdctCoeffs
1-D MDCT line array, length K.
Returns
-------
MdctCoeffs
Sw(k), 1-D array of length K, float64.
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
k_count = int(x.shape[0])
bands = _band_ranges(k_count)
sw = np.zeros(k_count, dtype=np.float64)
for s, e in bands:
seg = x[s : e + 1]
p_j = float(np.sum(seg * seg))
sw_val = float(np.sqrt(p_j))
sw[s : e + 1] = sw_val
_smooth_sw_inplace(sw)
return sw
def _autocorr(x: MdctCoeffs, p: int) -> MdctCoeffs:
"""
Autocorrelation r(m) for m=0..p.
Parameters
----------
x : MdctCoeffs
1-D signal.
p : int
Maximum lag.
Returns
-------
MdctCoeffs
r, shape (p+1,), float64.
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
n = int(x.shape[0])
r = np.zeros(p + 1, dtype=np.float64)
for m in range(p + 1):
r[m] = float(np.dot(x[m:], x[: n - m]))
return r
def _lpc_coeffs(xw: MdctCoeffs, p: int) -> MdctCoeffs:
"""
Solve Yule-Walker normal equations for LPC coefficients of order p.
Parameters
----------
xw : MdctCoeffs
1-D normalized sequence Xw(k).
p : int
Predictor order.
Returns
-------
MdctCoeffs
LPC coefficients a[0..p-1], shape (p,), float64.
"""
r = _autocorr(xw, p)
R = np.empty((p, p), dtype=np.float64)
for i in range(p):
for j in range(p):
R[i, j] = r[abs(i - j)]
rhs = r[1 : p + 1].reshape(p)
reg = 1e-12
R_reg = R + reg * np.eye(p, dtype=np.float64)
a = np.linalg.solve(R_reg, rhs)
return a
def _quantize_coeffs(a: MdctCoeffs) -> MdctCoeffs:
"""
Quantize LPC coefficients with uniform symmetric quantizer and clamp.
Parameters
----------
a : MdctCoeffs
LPC coefficient array, shape (p,).
Returns
-------
MdctCoeffs
Quantized coefficients, shape (p,), float64.
"""
a = np.asarray(a, dtype=np.float64).reshape(-1)
q = np.round(a / QUANT_STEP) * QUANT_STEP
q = np.clip(q, -QUANT_MAX, QUANT_MAX)
return q.astype(np.float64, copy=False)
def _is_inverse_stable(a_q: MdctCoeffs) -> bool:
"""
Check stability of the inverse TNS filter H_TNS^{-1}.
Forward filter:
H_TNS(z) = 1 - a1 z^-1 - ... - ap z^-p
Inverse filter poles are roots of:
A(z) = 1 - a1 z^-1 - ... - ap z^-p
Multiply by z^p:
z^p - a1 z^{p-1} - ... - ap = 0
Stability condition:
all roots satisfy |z| < 1.
Parameters
----------
a_q : MdctCoeffs
Quantized predictor coefficients, shape (p,).
Returns
-------
bool
True if stable, else False.
"""
a_q = np.asarray(a_q, dtype=np.float64).reshape(-1)
p = int(a_q.shape[0])
# Polynomial in z: z^p - a1 z^{p-1} - ... - ap
poly = np.empty(p + 1, dtype=np.float64)
poly[0] = 1.0
poly[1:] = -a_q
roots = np.roots(poly)
# Strictly inside unit circle for stability. Add tiny margin for numeric safety.
margin = 1e-12
return bool(np.all(np.abs(roots) < (1.0 - margin)))
def _stabilize_quantized_coeffs(a_q: MdctCoeffs) -> MdctCoeffs:
"""
Make quantized predictor coefficients stable for inverse filtering.
Policy:
- If already stable: return as-is.
- Else: iteratively shrink coefficients by gamma and re-quantize to the 0.1 grid.
- If still unstable after attempts: fall back to all-zero coefficients (disable TNS).
Parameters
----------
a_q : MdctCoeffs
Quantized predictor coefficients, shape (p,).
Returns
-------
MdctCoeffs
Stable quantized coefficients, shape (p,).
"""
a_q = np.asarray(a_q, dtype=np.float64).reshape(-1)
if _is_inverse_stable(a_q):
return a_q
# Try a few shrinking factors. Re-quantize after shrinking to keep coefficients on-grid.
gammas = (0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1)
for g in gammas:
cand = _quantize_coeffs(g * a_q)
if _is_inverse_stable(cand):
return cand
# Last resort: disable TNS for this vector
return np.zeros_like(a_q, dtype=np.float64)
def _apply_tns_fir(x: MdctCoeffs, a_q: MdctCoeffs) -> MdctCoeffs:
"""
Apply forward TNS FIR filter:
y[k] = x[k] - sum_{l=1..p} a_l * x[k-l]
Parameters
----------
x : MdctCoeffs
1-D MDCT lines, length K.
a_q : MdctCoeffs
Quantized LPC coefficients, shape (p,).
Returns
-------
MdctCoeffs
Filtered MDCT lines y, length K.
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
a_q = np.asarray(a_q, dtype=np.float64).reshape(-1)
p = int(a_q.shape[0])
k_count = int(x.shape[0])
y = np.zeros(k_count, dtype=np.float64)
for k in range(k_count):
acc = x[k]
for l in range(1, p + 1):
if k - l >= 0:
acc -= a_q[l - 1] * x[k - l]
y[k] = acc
return y
def _apply_itns_iir(y: MdctCoeffs, a_q: MdctCoeffs) -> MdctCoeffs:
"""
Apply inverse TNS IIR filter:
x_hat[k] = y[k] + sum_{l=1..p} a_l * x_hat[k-l]
Parameters
----------
y : MdctCoeffs
1-D MDCT lines after TNS, length K.
a_q : MdctCoeffs
Quantized LPC coefficients, shape (p,).
Returns
-------
MdctCoeffs
Reconstructed MDCT lines x_hat, length K.
"""
y = np.asarray(y, dtype=np.float64).reshape(-1)
a_q = np.asarray(a_q, dtype=np.float64).reshape(-1)
p = int(a_q.shape[0])
k_count = int(y.shape[0])
x_hat = np.zeros(k_count, dtype=np.float64)
for k in range(k_count):
acc = y[k]
for l in range(1, p + 1):
if k - l >= 0:
acc += a_q[l - 1] * x_hat[k - l]
x_hat[k] = acc
return x_hat
def _tns_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
"""
TNS for a single MDCT vector (one long frame or one short subframe).
Steps:
1) Compute Sw(k) from Bark band energies and smooth it.
2) Normalize: Xw(k) = X(k) / Sw(k) (safe when Sw=0).
3) Compute LPC coefficients (order p=PRED_ORDER) on Xw.
4) Quantize coefficients (4-bit symmetric, step QUANT_STEP).
5) Apply FIR filter on original X(k) using quantized coefficients.
Parameters
----------
x : MdctCoeffs
1-D MDCT vector.
Returns
-------
y : MdctCoeffs
TNS-processed MDCT vector (same length).
a_q : MdctCoeffs
Quantized LPC coefficients, shape (PRED_ORDER,).
"""
x = np.asarray(x, dtype=np.float64).reshape(-1)
sw = _compute_sw(x)
eps = 1e-12
xw = np.zeros_like(x, dtype=np.float64)
mask = sw > eps
np.divide(x, sw, out=xw, where=mask)
a = _lpc_coeffs(xw, PRED_ORDER)
a_q = _quantize_coeffs(a)
# Ensure inverse stability (assignment requirement)
a_q = _stabilize_quantized_coeffs(a_q)
y = _apply_tns_fir(x, a_q)
return y, a_q
# -----------------------------------------------------------------------------
# Public Functions
# -----------------------------------------------------------------------------
def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChannelF, TnsCoeffs]:
"""
Temporal Noise Shaping (TNS) for ONE channel.
Parameters
----------
frame_F_in : FrameChannelF
Per-channel MDCT coefficients.
Expected (typical) shapes:
- If frame_type == "ESH": (128, 8)
- Else: (1024, 1) or (1024,)
frame_type : FrameType
Frame type code ("OLS", "LSS", "ESH", "LPS").
Returns
-------
frame_F_out : FrameChannelF
Per-channel MDCT coefficients after applying TNS.
Same shape convention as input.
tns_coeffs : TnsCoeffs
Quantized TNS predictor coefficients.
Expected shapes:
- If frame_type == "ESH": (PRED_ORDER, 8)
- Else: (PRED_ORDER, 1)
"""
x = np.asarray(frame_F_in, dtype=np.float64)
if frame_type == "ESH":
if x.shape != (128, 8):
raise ValueError("For ESH, frame_F_in must have shape (128, 8).")
y = np.empty_like(x, dtype=np.float64)
a_out = np.empty((PRED_ORDER, 8), dtype=np.float64)
for j in range(8):
y[:, j], a_out[:, j] = _tns_vector(x[:, j])
return y, a_out
if x.shape == (1024,):
x_vec = x
out_shape = (1024,)
elif x.shape == (1024, 1):
x_vec = x[:, 0]
out_shape = (1024, 1)
else:
raise ValueError('For non-ESH, frame_F_in must have shape (1024,) or (1024, 1).')
y_vec, a_q = _tns_vector(x_vec)
if out_shape == (1024,):
y_out = y_vec
else:
y_out = y_vec.reshape(1024, 1)
a_out = a_q.reshape(PRED_ORDER, 1)
return y_out, a_out
def aac_i_tns(frame_F_in: FrameChannelF, frame_type: FrameType, tns_coeffs: TnsCoeffs) -> FrameChannelF:
"""
Inverse Temporal Noise Shaping (iTNS) for ONE channel.
Parameters
----------
frame_F_in : FrameChannelF
Per-channel MDCT coefficients after TNS.
Expected (typical) shapes:
- If frame_type == "ESH": (128, 8)
- Else: (1024, 1) or (1024,)
frame_type : FrameType
Frame type code ("OLS", "LSS", "ESH", "LPS").
tns_coeffs : TnsCoeffs
Quantized TNS predictor coefficients.
Expected shapes:
- If frame_type == "ESH": (PRED_ORDER, 8)
- Else: (PRED_ORDER, 1)
Returns
-------
FrameChannelF
Per-channel MDCT coefficients after inverse TNS.
Same shape convention as input frame_F_in.
"""
x = np.asarray(frame_F_in, dtype=np.float64)
a = np.asarray(tns_coeffs, dtype=np.float64)
if frame_type == "ESH":
if x.shape != (128, 8):
raise ValueError("For ESH, frame_F_in must have shape (128, 8).")
if a.shape != (PRED_ORDER, 8):
raise ValueError("For ESH, tns_coeffs must have shape (PRED_ORDER, 8).")
y = np.empty_like(x, dtype=np.float64)
for j in range(8):
y[:, j] = _apply_itns_iir(x[:, j], a[:, j])
return y
if a.shape != (PRED_ORDER, 1):
raise ValueError("For non-ESH, tns_coeffs must have shape (PRED_ORDER, 1).")
if x.shape == (1024,):
x_vec = x
out_shape = (1024,)
elif x.shape == (1024, 1):
x_vec = x[:, 0]
out_shape = (1024, 1)
else:
raise ValueError('For non-ESH, frame_F_in must have shape (1024,) or (1024, 1).')
y_vec = _apply_itns_iir(x_vec, a[:, 0])
if out_shape == (1024,):
return y_vec
return y_vec.reshape(1024, 1)
+411
View File
@@ -0,0 +1,411 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Public Type Aliases
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# This module implements Public Type aliases
# ------------------------------------------------------------
from __future__ import annotations
from typing import List, Literal, TypeAlias, TypedDict
import numpy as np
from numpy.typing import NDArray
# -----------------------------------------------------------------------------
# Code enums (for readability; not intended to enforce shapes/lengths)
# -----------------------------------------------------------------------------
FrameType: TypeAlias = Literal["OLS", "LSS", "ESH", "LPS"]
"""
Frame type codes (AAC):
- "OLS": ONLY_LONG_SEQUENCE
- "LSS": LONG_START_SEQUENCE
- "ESH": EIGHT_SHORT_SEQUENCE
- "LPS": LONG_STOP_SEQUENCE
"""
WinType: TypeAlias = Literal["KBD", "SIN"]
"""
Window type codes (AAC):
- "KBD": Kaiser-Bessel-Derived
- "SIN": sinusoid
"""
ChannelKey: TypeAlias = Literal["chl", "chr"]
"""Channel dictionary keys used in Level payloads."""
# -----------------------------------------------------------------------------
# Array “semantic” aliases
#
# Goal: communicate meaning (time/frequency/window, stereo/channel) without
# forcing strict shapes in the type system.
# -----------------------------------------------------------------------------
FloatArray: TypeAlias = NDArray[np.float64]
"""
Generic float64 NumPy array.
Note:
- We standardize internal numeric computations to float64 for stability and
reproducibility. External I/O can still be float32, but we convert at the
boundaries.
"""
Window: TypeAlias = FloatArray
"""
Time-domain window (weighting sequence), 1-D.
Typical lengths in this assignment:
- Long: 2048
- Short: 256
- Window sequences for LSS/LPS are also 2048
Expected shape: (N,)
dtype: float64
"""
TimeSignal: TypeAlias = FloatArray
"""
Time-domain signal samples, typically 1-D.
Examples:
- Windowed MDCT input: shape (N,)
- IMDCT output: shape (N,)
dtype: float64
"""
StereoSignal: TypeAlias = FloatArray
"""
Time-domain stereo signal stream.
Expected (typical) shape: (N, 2)
- axis 0: time samples
- axis 1: channels [L, R]
dtype: float64
"""
MdctCoeffs: TypeAlias = FloatArray
"""
MDCT coefficient vector, typically 1-D.
Examples:
- Long: shape (1024,)
- Short: shape (128,)
dtype: float64
"""
MdctFrameChannel: TypeAlias = FloatArray
"""
Per-channel MDCT container used in Level-1/2 sequences.
Typical shapes:
- If frame_type in {"OLS","LSS","LPS"}: (1024, 1) or (1024,)
- If frame_type == "ESH": (128, 8) (8 short subframes for one channel)
dtype: float64
Notes
-----
Some parts of the assignment store long-frame coefficients as a column vector
(1024, 1) to match MATLAB conventions. Internally you may also use (1024,)
when convenient, but the semantic meaning is identical.
"""
TnsCoeffs: TypeAlias = FloatArray
"""
Quantized TNS predictor coefficients (one channel).
Typical shapes (Level 2):
- If frame_type == "ESH": (4, 8) (order p=4 for each of the 8 short subframes)
- Else: (4, 1) (order p=4 for the long frame)
dtype: float64
Notes
-----
The assignment uses a 4-bit uniform symmetric quantizer with step size 0.1.
We store the quantized coefficient values as float64 (typically multiples of 0.1)
to keep the pipeline simple and readable.
"""
FrameT: TypeAlias = FloatArray
"""
Time-domain frame (stereo), as used by the filterbank input/output.
Expected (typical) shape for stereo: (2048, 2)
- axis 0: time samples
- axis 1: channels [L, R]
dtype: float64
"""
FrameChannelT: TypeAlias = FloatArray
"""
Time-domain single-channel frame.
Expected (typical) shape: (2048,)
dtype: float64
"""
FrameF: TypeAlias = FloatArray
"""
Frequency-domain frame (MDCT coefficients), stereo container.
Typical shapes (Level 1):
- If frame_type in {"OLS","LSS","LPS"}: (1024, 2)
- If frame_type == "ESH": (128, 16)
Rationale for ESH (128, 16):
- 8 short subframes per channel => 8 * 2 = 16 columns total
- Each short subframe per stereo is (128, 2), flattened into columns
in subframe order: [sf0_L, sf0_R, sf1_L, sf1_R, ..., sf7_L, sf7_R]
dtype: float64
"""
FrameChannelF: TypeAlias = MdctFrameChannel
"""
Frequency-domain single-channel MDCT coefficients.
Typical shapes (Level 1/2):
- If frame_type in {"OLS","LSS","LPS"}: (1024, 1) or (1024,)
- If frame_type == "ESH": (128, 8)
dtype: float64
"""
BandRanges: TypeAlias = list[tuple[int, int]]
"""
Bark-band index ranges [start, end] (inclusive) for MDCT lines.
Used by TNS to map MDCT indices k to Bark bands.
"""
BarkTable: TypeAlias = FloatArray
"""
Psychoacoustic Bark band table loaded from TableB219.mat.
Typical shapes:
- Long: (69, 6)
- Short: (42, 6)
"""
BandIndexArray: TypeAlias = NDArray[np.int_]
"""
Array of FFT bin indices per psychoacoustic band.
"""
BandValueArray: TypeAlias = FloatArray
"""
Per-band psychoacoustic values (e.g. Bark position, thresholds).
"""
# Quantizer-related semantic aliases
QuantizedSymbols: TypeAlias = NDArray[np.generic]
"""
Quantized MDCT symbols S(k).
Shapes:
- Always (1024, 1) at the quantizer output (ESH packed to 1024 symbols).
"""
ScaleFactors: TypeAlias = NDArray[np.generic]
"""
DPCM-coded scalefactors sfc(b) = alpha(b) - alpha(b-1).
Shapes:
- Long frames: (NB, 1)
- ESH frames: (NB, 8)
"""
GlobalGain: TypeAlias = float | NDArray[np.generic]
"""
Global gain G = alpha(0).
- Long frames: scalar float
- ESH frames: array shape (1, 8)
"""
# Huffman semantic aliases
HuffmanBitstream: TypeAlias = str
"""Huffman-coded bitstream stored as a string of '0'/'1'."""
HuffmanCodebook: TypeAlias = int
"""Huffman codebook id (e.g., 0..11)."""
# -----------------------------------------------------------------------------
# Level 1 AAC sequence payload types
# -----------------------------------------------------------------------------
class AACChannelFrameF(TypedDict):
"""
Per-channel payload for aac_seq_1[i]["chl"] or ["chr"] (Level 1).
Keys
----
frame_F:
The MDCT coefficients for ONE channel.
Typical shapes:
- ESH: (128, 8) (8 short subframes)
- else: (1024, 1) or (1024,)
"""
frame_F: FrameChannelF
class AACSeq1Frame(TypedDict):
"""
One frame dictionary element of aac_seq_1 (Level 1).
"""
frame_type: FrameType
win_type: WinType
chl: AACChannelFrameF
chr: AACChannelFrameF
AACSeq1: TypeAlias = List[AACSeq1Frame]
"""
AAC sequence for Level 1:
List of length K (K = number of frames).
Each element is a dict with keys:
- "frame_type", "win_type", "chl", "chr"
"""
# -----------------------------------------------------------------------------
# Level 2 AAC sequence payload types (TNS)
# -----------------------------------------------------------------------------
class AACChannelFrameF2(TypedDict):
"""
Per-channel payload for aac_seq_2[i]["chl"] or ["chr"] (Level 2).
Keys
----
frame_F:
The TNS-processed MDCT coefficients for ONE channel.
Typical shapes:
- ESH: (128, 8)
- else: (1024, 1) or (1024,)
tns_coeffs:
Quantized TNS predictor coefficients for ONE channel.
Typical shapes:
- ESH: (PRED_ORDER, 8)
- else: (PRED_ORDER, 1)
"""
frame_F: FrameChannelF
tns_coeffs: TnsCoeffs
class AACSeq2Frame(TypedDict):
"""
One frame dictionary element of aac_seq_2 (Level 2).
"""
frame_type: FrameType
win_type: WinType
chl: AACChannelFrameF2
chr: AACChannelFrameF2
AACSeq2: TypeAlias = List[AACSeq2Frame]
"""
AAC sequence for Level 2:
List of length K (K = number of frames).
Each element is a dict with keys:
- "frame_type", "win_type", "chl", "chr"
Level 2 adds:
- per-channel "tns_coeffs"
and stores:
- per-channel "frame_F" after applying TNS.
"""
# -----------------------------------------------------------------------------
# Level 3 AAC sequence payload types (Quantizer + Huffman)
# -----------------------------------------------------------------------------
class AACChannelFrameF3(TypedDict):
"""
Per-channel payload for aac_seq_3[i]["chl"] or ["chr"] (Level 3).
Keys
----
tns_coeffs:
Quantized TNS predictor coefficients for ONE channel.
Shapes:
- ESH: (PRED_ORDER, 8)
- else: (PRED_ORDER, 1)
T:
Psychoacoustic thresholds per band.
Shapes:
- ESH: (NB, 8)
- else: (NB, 1)
Note: Stored for completeness / debugging; not entropy-coded.
G:
Quantized global gains.
Shapes:
- ESH: (1, 8) (one per short subframe)
- else: scalar (or compatible np scalar)
sfc:
Huffman-coded scalefactor differences (DPCM sequence).
stream:
Huffman-coded MDCT quantized symbols S(k) (packed to 1024 symbols).
codebook:
Huffman codebook id used for MDCT symbols (stream).
(Scalefactors typically use fixed codebook 11 and do not need to store it.)
"""
tns_coeffs: TnsCoeffs
T: FloatArray
G: FloatArray | float
sfc: HuffmanBitstream
stream: HuffmanBitstream
codebook: HuffmanCodebook
class AACSeq3Frame(TypedDict):
"""
One frame dictionary element of aac_seq_3 (Level 3).
"""
frame_type: FrameType
win_type: WinType
chl: AACChannelFrameF3
chr: AACChannelFrameF3
AACSeq3: TypeAlias = List[AACSeq3Frame]
"""
AAC sequence for Level 3:
List of length K (K = number of frames).
Each element is a dict with keys:
- "frame_type", "win_type", "chl", "chr"
Level 3 adds (per channel):
- "tns_coeffs"
- "T" thresholds (not entropy-coded)
- "G" global gain(s)
- "sfc" Huffman-coded scalefactor differences
- "stream" Huffman-coded MDCT quantized symbols
- "codebook" Huffman codebook for MDCT symbols
"""
+306
View File
@@ -0,0 +1,306 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - AAC Utilities
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Shared utility functions used across AAC encoder/decoder levels.
#
# This module currently provides:
# - MDCT / IMDCT conversions
# - Signal-to-Noise Ratio (SNR) computation in dB
# - Loading and access helpers for psychoacoustic band tables
# (TableB219.mat, Tables B.2.1.9a / B.2.1.9b of the AAC specification)
# ------------------------------------------------------------
from __future__ import annotations
import numpy as np
from pathlib import Path
from scipy.io import loadmat
from core.aac_types import *
# -----------------------------------------------------------------------------
# Global cached data
# -----------------------------------------------------------------------------
# Cached contents of TableB219.mat to avoid repeated disk I/O.
# Keys:
# - "B219a": long-window psychoacoustic bands (69 bands, FFT size 2048)
# - "B219b": short-window psychoacoustic bands (42 bands, FFT size 256)
B219_CACHE: dict[str, BarkTable] | None = None
# -----------------------------------------------------------------------------
# MDCT / IMDCT
# -----------------------------------------------------------------------------
def mdct(s: TimeSignal) -> MdctCoeffs:
"""
MDCT (direct form) as specified in the assignment.
Parameters
----------
s : TimeSignal
Windowed time samples, 1-D array of length N (N = 2048 or 256).
Returns
-------
MdctCoeffs
MDCT coefficients, 1-D array of length N/2.
Definition
----------
X[k] = 2 * sum_{n=0..N-1} s[n] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
s = np.asarray(s, dtype=np.float64).reshape(-1)
N = int(s.shape[0])
if N not in (2048, 256):
raise ValueError("MDCT input length must be 2048 or 256.")
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(N // 2, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, N/2)
X = 2.0 * (s @ C) # (N/2,)
return X
def imdct(X: MdctCoeffs) -> TimeSignal:
"""
IMDCT (direct form) as specified in the assignment.
Parameters
----------
X : MdctCoeffs
MDCT coefficients, 1-D array of length K (K = 1024 or 128).
Returns
-------
TimeSignal
Reconstructed time samples, 1-D array of length N = 2K.
Definition
----------
s[n] = (2/N) * sum_{k=0..N/2-1} X[k] * cos((2*pi/N) * (n + n0) * (k + 1/2)),
where n0 = (N/2 + 1)/2.
"""
X = np.asarray(X, dtype=np.float64).reshape(-1)
K = int(X.shape[0])
if K not in (1024, 128):
raise ValueError("IMDCT input length must be 1024 or 128.")
N = 2 * K
n0 = (N / 2.0 + 1.0) / 2.0
n = np.arange(N, dtype=np.float64) + n0
k = np.arange(K, dtype=np.float64) + 0.5
C = np.cos((2.0 * np.pi / N) * np.outer(n, k)) # (N, K)
s = (2.0 / N) * (C @ X) # (N,)
return s
# -----------------------------------------------------------------------------
# Signal quality metrics
# -----------------------------------------------------------------------------
def snr_db(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Compute the overall Signal-to-Noise Ratio (SNR) in dB.
The SNR is computed over all available samples and channels,
after conservatively aligning the two signals to their common
length and channel count.
Parameters
----------
x_ref : StereoSignal
Reference (original) signal.
Typical shape: (N, 2) for stereo.
x_hat : StereoSignal
Reconstructed or processed signal.
Typical shape: (M, 2) for stereo.
Returns
-------
float
SNR in dB.
- +inf if the noise power is zero (perfect reconstruction).
- -inf if the reference signal power is zero.
"""
x_ref = np.asarray(x_ref, dtype=np.float64)
x_hat = np.asarray(x_hat, dtype=np.float64)
# Ensure 2-D shape: (samples, channels)
if x_ref.ndim == 1:
x_ref = x_ref.reshape(-1, 1)
if x_hat.ndim == 1:
x_hat = x_hat.reshape(-1, 1)
# Align lengths and channel count conservatively
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
x_ref = x_ref[:n, :c]
x_hat = x_hat[:n, :c]
err = x_ref - x_hat
ps = float(np.sum(x_ref * x_ref)) # signal power
pn = float(np.sum(err * err)) # noise power
if pn <= 0.0:
return float("inf")
if ps <= 0.0:
return float("-inf")
return float(10.0 * np.log10(ps / pn))
def estimate_lag_mono(x_ref: TimeSignal, x_hat: TimeSignal, max_lag=4096):
"""
Estimate time lag between two mono signals.
Returns lag (positive means x_hat delayed).
"""
n = min(len(x_ref), len(x_hat))
x_ref = x_ref[:n]
x_hat = x_hat[:n]
corr = np.correlate(x_ref, x_hat, mode='full')
lags = np.arange(-n + 1, n)
center = n - 1
lo = max(0, center - max_lag)
hi = min(len(corr), center + max_lag + 1)
best = lo + int(np.argmax(corr[lo:hi]))
return int(lags[best])
def match_gain(x_ref: StereoSignal, x_hat: StereoSignal) -> float:
"""
Least-squares gain g that best maps x_hat -> x_ref.
"""
n = min(x_ref.shape[0], x_hat.shape[0])
c = min(x_ref.shape[1], x_hat.shape[1])
r = x_ref[:n, :c].reshape(-1).astype(np.float64)
h = x_hat[:n, :c].reshape(-1).astype(np.float64)
denom = float(np.dot(h, h))
if denom <= 0.0:
return 1.0
return float(np.dot(r, h) / denom)
# -----------------------------------------------------------------------------
# Psychoacoustic band tables (TableB219.mat)
# -----------------------------------------------------------------------------
def load_b219_tables() -> dict[str, BarkTable]:
"""
Load and cache psychoacoustic band tables from TableB219.mat.
The assignment/project layout assumes that a 'material' directory
is available in the current working directory when running:
- tests
- level_1 / level_2 / level_3 entrypoints
This function loads the tables once and caches them for subsequent calls.
Returns
-------
dict[str, BarkTable]
Dictionary with the following entries:
- "B219a": long-window psychoacoustic table
(69 bands, FFT size 2048 / 1024 spectral lines)
- "B219b": short-window psychoacoustic table
(42 bands, FFT size 256 / 128 spectral lines)
"""
global B219_CACHE
if B219_CACHE is not None:
return B219_CACHE
mat_path = Path("material") / "TableB219.mat"
if not mat_path.exists():
raise FileNotFoundError(
"Could not locate material/TableB219.mat in the current working directory."
)
data = loadmat(str(mat_path))
if "B219a" not in data or "B219b" not in data:
raise ValueError(
"TableB219.mat missing required variables 'B219a' and/or 'B219b'."
)
B219_CACHE = {
"B219a": np.asarray(data["B219a"], dtype=np.float64),
"B219b": np.asarray(data["B219b"], dtype=np.float64),
}
return B219_CACHE
def get_table(frame_type: FrameType) -> tuple[BarkTable, int]:
"""
Select the appropriate psychoacoustic band table and FFT size
based on the AAC frame type.
Parameters
----------
frame_type : FrameType
AAC frame type ("OLS", "LSS", "ESH", "LPS").
Returns
-------
table : BarkTable
Psychoacoustic band table:
- B219a for long frames
- B219b for ESH short subframes
N : int
FFT size corresponding to the table:
- 2048 for long frames
- 256 for short frames (ESH)
"""
tables = load_b219_tables()
if frame_type == "ESH":
return tables["B219b"], 256
return tables["B219a"], 2048
def band_limits(
table: BarkTable,
) -> tuple[BandIndexArray, BandIndexArray, BandValueArray, BandValueArray]:
"""
Extract per-band metadata from a TableB2.1.9 psychoacoustic table.
The column layout follows the provided TableB219.mat file and the
AAC specification tables B.2.1.9a / B.2.1.9b.
Parameters
----------
table : BarkTable
Psychoacoustic band table (B219a or B219b).
Returns
-------
wlow : BandIndexArray
Lower FFT bin index (inclusive) for each band.
whigh : BandIndexArray
Upper FFT bin index (inclusive) for each band.
bval : BandValueArray
Bark-scale (or equivalent) band position values.
Used in the spreading function.
qthr_db : BandValueArray
Threshold in quiet for each band, in dB.
"""
wlow = table[:, 1].astype(int)
whigh = table[:, 2].astype(int)
bval = table[:, 4].astype(np.float64)
qthr_db = table[:, 5].astype(np.float64)
return wlow, whigh, bval, qthr_db
+327
View File
@@ -0,0 +1,327 @@
# ------------------------------------------------------------
# AAC Coder/Decoder - Level 3 Wrappers + Demo
#
# Multimedia course at Aristotle University of
# Thessaloniki (AUTh)
#
# Author:
# Christos Choutouridis (ΑΕΜ 8997)
# cchoutou@ece.auth.gr
#
# Description:
# Level 3 wrapper module.
#
# This file provides:
# - Thin wrappers for Level 3 API functions (encode/decode) that delegate
# to the corresponding core implementations.
# - A demo function that runs end-to-end and computes:
# * SNR
# * bitrate (coded)
# * compression ratio
# - A small CLI entrypoint for convenience.
# ------------------------------------------------------------
from __future__ import annotations
from pathlib import Path
from typing import Optional, Tuple, Union
import os
import soundfile as sf
import numpy as np
import matplotlib.pyplot as plt
from core.aac_types import AACSeq3, StereoSignal
from core.aac_coder import aac_coder_3 as core_aac_coder_3
from core.aac_coder import aac_read_wav_stereo_48k
from core.aac_decoder import aac_decoder_3 as core_aac_decoder_3
from core.aac_utils import snr_db, estimate_lag_mono, match_gain
# Global variable to "pass" AACSeq3 without changing the demo_aac_e interface.
AAC_Seq_3: AACSeq3
# -----------------------------------------------------------------------------
# Helpers (Level 3 metrics)
# -----------------------------------------------------------------------------
def _wav_duration_seconds(wav_path: Path) -> float:
"""Return WAV duration in seconds using soundfile metadata."""
info = sf.info(str(wav_path))
if info.samplerate <= 0:
raise ValueError("Invalid samplerate in WAV header.")
if info.frames < 0:
raise ValueError("Invalid frame count in WAV header.")
return float(info.frames) / float(info.samplerate)
def _bitrate_before_from_file(wav_path: Path) -> float:
"""
Compute input bitrate (bits/s) from file size and duration.
Note:
This is a file-based bitrate estimate (includes WAV header), which is
acceptable for a simple compression ratio metric.
"""
duration = _wav_duration_seconds(wav_path)
if duration <= 0.0:
raise ValueError("Non-positive WAV duration.")
nbits = float(os.path.getsize(wav_path)) * 8.0
return nbits / duration
def _bitrate_after_from_aacseq(aac_seq_3: AACSeq3, duration_sec: float) -> float:
"""
Compute coded bitrate (bits/s) from Huffman streams stored in AACSeq3.
We count bits from:
- scalefactor Huffman bitstream ("sfc")
- MDCT symbols Huffman bitstream ("stream")
for both channels and all frames.
Note:
We intentionally ignore side-info overhead (frame_type, G, T, TNS coeffs,
codebook ids, etc.). This matches a common simplified metric in demos.
"""
if duration_sec <= 0.0:
raise ValueError("Non-positive duration for bitrate computation.")
total_bits = 0
for fr in aac_seq_3:
total_bits += len(fr["chl"]["sfc"])
total_bits += len(fr["chl"]["stream"])
total_bits += len(fr["chr"]["sfc"])
total_bits += len(fr["chr"]["stream"])
return float(total_bits) / float(duration_sec)
def _plot_frame_bitrate_and_compression(
aac_seq_3: AACSeq3,
wav_path: Union[str, Path],
fname_bitrate: Union[str, Path],
fname_comp: Union[str, Path],
) -> None:
"""
Compute and plot per-frame bitrate and compression ratio
for a Level 3 AAC sequence.
Parameters
----------
aac_seq_3 : list
Output of aac_coder_3 (list of frame dictionaries).
wav_path : str or Path
Path to original WAV file (PCM 48 kHz stereo).
fname_bitrate : str or Path
Path to original bitrate per frame plot output file.
fname_comp : str or Path
Path to original compression per frame plot output file.
"""
# Read WAV metadata
info = sf.info(str(wav_path))
samplerate = info.samplerate
total_samples = info.frames
total_duration = total_samples / samplerate
n_frames = len(aac_seq_3)
# AAC long-frame hop size is 1024 new samples per frame
samples_per_frame = 1024
duration_per_frame = samples_per_frame / samplerate
# Original bitrate (file-based estimate)
original_bits = os.path.getsize(wav_path) * 8.0
original_bitrate = original_bits / total_duration
frame_bitrates = []
frame_compression = []
for fr in aac_seq_3:
bits = 0
bits += len(fr["chl"]["sfc"])
bits += len(fr["chl"]["stream"])
bits += len(fr["chr"]["sfc"])
bits += len(fr["chr"]["stream"])
bitrate = bits / duration_per_frame
compression = original_bitrate / bitrate if bitrate > 0 else np.inf
frame_bitrates.append(bitrate)
frame_compression.append(compression)
frame_indices = np.arange(n_frames)
# Plot bitrate per frame and save to file
plt.figure(figsize=(6, 3), dpi=300)
plt.plot(frame_indices, frame_bitrates)
plt.xlabel("Frame index")
plt.ylabel("Bitrate (bits/s)")
plt.title("Bitrate (per-frame)")
plt.tight_layout()
plt.savefig(str(fname_bitrate))
plt.close()
# Plot compression ratio per frame and save to file
plt.figure(figsize=(6, 3), dpi=300)
plt.plot(frame_indices, frame_compression)
plt.xlabel("Frame index")
plt.ylabel("Compression Ratio")
plt.title("Compression Ratio (per-frame)")
plt.tight_layout()
plt.savefig(str(fname_comp))
plt.close()
# -----------------------------------------------------------------------------
# Public Level 3 API (wrappers)
# -----------------------------------------------------------------------------
def aac_coder_3(
filename_in: Union[str, Path],
filename_aac_coded: Optional[Union[str, Path]] = None,
) -> AACSeq3:
"""
Level-3 AAC encoder (wrapper).
Delegates to core implementation.
Parameters
----------
filename_in : Union[str, Path]
Input WAV filename.
Assumption: stereo audio, sampling rate 48 kHz.
filename_aac_coded : Optional[Union[str, Path]]
Optional filename to store the encoded AAC sequence (e.g., .mat).
Returns
-------
AACSeq3
List of encoded frames (Level 3 schema).
"""
return core_aac_coder_3(filename_in, filename_aac_coded, verbose=True)
def i_aac_coder_3(
aac_seq_3: AACSeq3,
filename_out: Union[str, Path],
) -> StereoSignal:
"""
Level-3 AAC decoder (wrapper).
Delegates to core implementation.
Parameters
----------
aac_seq_3 : AACSeq3
Encoded sequence as produced by aac_coder_3().
filename_out : Union[str, Path]
Output WAV filename. Assumption: 48 kHz, stereo.
Returns
-------
StereoSignal
Decoded audio samples (time-domain), stereo, shape (N, 2), dtype float64.
"""
return core_aac_decoder_3(aac_seq_3, filename_out, verbose=True)
# -----------------------------------------------------------------------------
# Demo (Level 3)
# -----------------------------------------------------------------------------
def demo_aac_3(
filename_in: Union[str, Path],
filename_out: Union[str, Path],
filename_aac_coded: Optional[Union[str, Path]] = None,
) -> Tuple[float, float, float]:
"""
Demonstration for the Level-3 codec.
Runs:
- aac_coder_3(filename_in, filename_aac_coded)
- i_aac_coder_3(aac_seq_3, filename_out)
and computes:
- total SNR between original and decoded audio
- coded bitrate (bits/s) based on Huffman streams
- compression ratio (bitrate_before / bitrate_after)
Parameters
----------
filename_in : Union[str, Path]
Input WAV filename (stereo, 48 kHz).
filename_out : Union[str, Path]
Output WAV filename (stereo, 48 kHz).
filename_aac_coded : Optional[Union[str, Path]]
Optional filename to store the encoded AAC sequence (e.g., .mat).
Returns
-------
Tuple[float, float, float]
(SNR_dB, bitrate_after_bits_per_s, compression_ratio)
"""
filename_in = Path(filename_in)
filename_out = Path(filename_out)
filename_aac_coded = Path(filename_aac_coded) if filename_aac_coded else None
# Read original audio (reference) with the same validation as the codec.
x_ref, fs_ref = aac_read_wav_stereo_48k(filename_in)
if int(fs_ref) != 48000:
raise ValueError("Input sampling rate must be 48 kHz.")
# Encode / decode
global AAC_Seq_3 # pick coder output
AAC_Seq_3 = aac_coder_3(filename_in, filename_aac_coded)
x_hat = i_aac_coder_3(AAC_Seq_3, filename_out)
# Optional sanity: ensure output file exists and is readable
_, fs_hat = sf.read(str(filename_out), always_2d=True)
if int(fs_hat) != 48000:
raise ValueError("Decoded output sampling rate must be 48 kHz.")
# Quality metrics
s = snr_db(x_ref, x_hat)
duration = _wav_duration_seconds(filename_in)
bitrate_before = _bitrate_before_from_file(filename_in)
bitrate_after = _bitrate_after_from_aacseq(AAC_Seq_3, duration)
compression = float("inf") if bitrate_after <= 0.0 else (bitrate_before / bitrate_after)
return float(s), float(bitrate_after), float(compression)
# -----------------------------------------------------------------------------
# CLI
# -----------------------------------------------------------------------------
if __name__ == "__main__":
# Example:
# cd level_3
# python -m level_3 input.wav output.wav
# for example:
# python -m level_3 material/LicorDeCalandraca.wav LicorDeCalandraca_out_l3.wav
# or
# python -m level_3 material/LicorDeCalandraca.wav LicorDeCalandraca_out_l3.wav aac_seq_3.mat
# or
# python -m level_3 material/LicorDeCalandraca.wav LicorDeCalandraca_out_l3.wav aac_seq_3.mat bitrate.png compression.png
import sys
if len(sys.argv) not in (3, 4, 5, 6):
raise SystemExit(
"Usage: python -m level_3 <input.wav> <output.wav> [aac_seq_3.mat] [bitrate_fname] [compression_fname]"
)
in_wav = Path(sys.argv[1])
out_wav = Path(sys.argv[2])
aac_mat = Path(sys.argv[3]) if len(sys.argv) == 4 else None
fname_bitrate = Path(sys.argv[4]) if len(sys.argv) == 5 else "bitrate_per_frame.png"
fname_comp = Path(sys.argv[5]) if len(sys.argv) == 6 else "compression_per_frame.png"
print(f"Encoding/Decoding {in_wav} to {out_wav}")
if aac_mat is not None:
print(f"Storing coded sequence to {aac_mat}")
snr, bitrate, compression = demo_aac_3(in_wav, out_wav, aac_mat)
# plot compresion / bitrate
_plot_frame_bitrate_and_compression(AAC_Seq_3, in_wav, fname_bitrate, fname_comp)
print(f"SNR = {snr:.3f} dB")
print(f"Bitrate (coded) = {bitrate:.2f} bits/s")
print(f"Compression ratio = {compression:.4f}")
+403
View File
@@ -0,0 +1,403 @@
import numpy as np
import scipy.io as sio
import os
# ------------------ LOAD LUT ------------------
def load_LUT(mat_filename=None):
"""
Loads the list of Huffman Codebooks (LUTs)
Returns:
huffLUT : list (index 1..11 used, index 0 unused)
"""
if mat_filename is None:
current_dir = os.path.dirname(os.path.abspath(__file__))
mat_filename = os.path.join(current_dir, "huffCodebooks.mat")
mat = sio.loadmat(mat_filename)
huffCodebooks_raw = mat['huffCodebooks'].squeeze()
huffCodebooks = []
for i in range(11):
huffCodebooks.append(np.array(huffCodebooks_raw[i]))
# Build inverse VLC tables
invTable = [None] * 11
for i in range(11):
h = huffCodebooks[i][:, 2].astype(int) # column 3
hlength = huffCodebooks[i][:, 1].astype(int) # column 2
hbin = []
for j in range(len(h)):
hbin.append(format(h[j], f'0{hlength[j]}b'))
invTable[i] = vlc_table(hbin)
# Build Huffman LUT dicts
huffLUT = [None] * 12 # index 0 unused
params = [
(4, 1, True),
(4, 1, True),
(4, 2, False),
(4, 2, False),
(2, 4, True),
(2, 4, True),
(2, 7, False),
(2, 7, False),
(2, 12, False),
(2, 12, False),
(2, 16, False),
]
for i, (nTupleSize, maxAbs, signed) in enumerate(params, start=1):
huffLUT[i] = {
'LUT': huffCodebooks[i-1],
'invTable': invTable[i-1],
'codebook': i,
'nTupleSize': nTupleSize,
'maxAbsCodeVal': maxAbs,
'signedValues': signed
}
return huffLUT
def vlc_table(code_array):
"""
codeArray: list of strings, each string is a Huffman codeword (e.g. '0101')
returns:
h : NumPy array of shape (num_nodes, 3)
columns:
[ next_if_0 , next_if_1 , symbol_index ]
"""
h = np.zeros((1, 3), dtype=int)
for code_index, code in enumerate(code_array, start=1):
word = [int(bit) for bit in code]
h_index = 0
for bit in word:
k = bit
next_node = h[h_index, k]
if next_node == 0:
h = np.vstack([h, [0, 0, 0]])
new_index = h.shape[0] - 1
h[h_index, k] = new_index
h_index = new_index
else:
h_index = next_node
h[h_index, 2] = code_index
return h
# ------------------ ENCODE ------------------
def encode_huff(coeff_sec, huff_LUT_list, force_codebook = None):
"""
Huffman-encode a sequence of quantized coefficients.
This function selects the appropriate Huffman codebook based on the
maximum absolute value of the input coefficients, encodes the coefficients
into a binary Huffman bitstream, and returns both the bitstream and the
selected codebook index.
This is the Python equivalent of the MATLAB `encodeHuff.m` function used
in audio/image coding (e.g., scale factor band encoding). The input
coefficient sequence is grouped into fixed-size tuples as defined by
the chosen Huffman LUT. Zero-padding may be applied internally.
Parameters
----------
coeff_sec : array_like of int
1-D array of quantized integer coefficients to encode.
Typically corresponds to a "section" or scale-factor band.
huff_LUT_list : list
List of Huffman lookup-table dictionaries as returned by `loadLUT()`.
Index 1..11 correspond to valid Huffman codebooks.
Index 0 is unused.
Returns
-------
huffSec : str
Huffman-encoded bitstream represented as a string of '0' and '1'
characters.
huffCodebook : int
Index (1..11) of the Huffman codebook used for encoding.
A value of 0 indicates a special all-zero section.
"""
if force_codebook is not None:
return huff_LUT_code_1(huff_LUT_list[force_codebook], coeff_sec)
maxAbsVal = np.max(np.abs(coeff_sec))
if maxAbsVal == 0:
huffCodebook = 0
huffSec = huff_LUT_code_0()
elif maxAbsVal == 1:
candidates = [1, 2]
huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
if len(huffSec1) <= len(huffSec2):
huffSec = huffSec1
huffCodebook = candidates[0]
else:
huffSec = huffSec2
huffCodebook = candidates[1]
elif maxAbsVal == 2:
candidates = [3, 4]
huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
if len(huffSec1) <= len(huffSec2):
huffSec = huffSec1
huffCodebook = candidates[0]
else:
huffSec = huffSec2
huffCodebook = candidates[1]
elif maxAbsVal in (3, 4):
candidates = [5, 6]
huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
if len(huffSec1) <= len(huffSec2):
huffSec = huffSec1
huffCodebook = candidates[0]
else:
huffSec = huffSec2
huffCodebook = candidates[1]
elif maxAbsVal in (5, 6, 7):
candidates = [7, 8]
huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
if len(huffSec1) <= len(huffSec2):
huffSec = huffSec1
huffCodebook = candidates[0]
else:
huffSec = huffSec2
huffCodebook = candidates[1]
elif maxAbsVal in (8, 9, 10, 11, 12):
candidates = [9, 10]
huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
if len(huffSec1) <= len(huffSec2):
huffSec = huffSec1
huffCodebook = candidates[0]
else:
huffSec = huffSec2
huffCodebook = candidates[1]
elif maxAbsVal in (13, 14, 15):
huffCodebook = 11
huffSec = huff_LUT_code_1(huff_LUT_list[huffCodebook], coeff_sec)
else:
huffCodebook = 11
huffSec = huff_LUT_code_ESC(huff_LUT_list[huffCodebook], coeff_sec)
return huffSec, huffCodebook
def huff_LUT_code_1(huff_LUT, coeff_sec):
LUT = huff_LUT['LUT']
nTupleSize = huff_LUT['nTupleSize']
maxAbsCodeVal = huff_LUT['maxAbsCodeVal']
signedValues = huff_LUT['signedValues']
numTuples = int(np.ceil(len(coeff_sec) / nTupleSize))
if signedValues:
coeff = coeff_sec + maxAbsCodeVal
base = 2 * maxAbsCodeVal + 1
else:
coeff = coeff_sec
base = maxAbsCodeVal + 1
coeffPad = np.zeros(numTuples * nTupleSize, dtype=int)
coeffPad[:len(coeff)] = coeff
huffSec = []
powers = base ** np.arange(nTupleSize - 1, -1, -1)
for i in range(numTuples):
nTuple = coeffPad[i*nTupleSize:(i+1)*nTupleSize]
huffIndex = int(np.abs(nTuple) @ powers)
hexVal = LUT[huffIndex, 2]
huffLen = LUT[huffIndex, 1]
bits = format(int(hexVal), f'0{int(huffLen)}b')
if signedValues:
huffSec.append(bits)
else:
signBits = ''.join('1' if v < 0 else '0' for v in nTuple)
huffSec.append(bits + signBits)
return ''.join(huffSec)
def huff_LUT_code_0():
return ''
def huff_LUT_code_ESC(huff_LUT, coeff_sec):
LUT = huff_LUT['LUT']
nTupleSize = huff_LUT['nTupleSize']
maxAbsCodeVal = huff_LUT['maxAbsCodeVal']
numTuples = int(np.ceil(len(coeff_sec) / nTupleSize))
base = maxAbsCodeVal + 1
coeffPad = np.zeros(numTuples * nTupleSize, dtype=int)
coeffPad[:len(coeff_sec)] = coeff_sec
huffSec = []
powers = base ** np.arange(nTupleSize - 1, -1, -1)
for i in range(numTuples):
nTuple = coeffPad[i*nTupleSize:(i+1)*nTupleSize]
lnTuple = nTuple.astype(float)
lnTuple[lnTuple == 0] = np.finfo(float).eps
N4 = np.maximum(0, np.floor(np.log2(np.abs(lnTuple))).astype(int))
N = np.maximum(0, N4 - 4)
esc = np.abs(nTuple) > 15
nTupleESC = nTuple.copy()
nTupleESC[esc] = np.sign(nTupleESC[esc]) * 16
huffIndex = int(np.abs(nTupleESC) @ powers)
hexVal = LUT[huffIndex, 2]
huffLen = LUT[huffIndex, 1]
bits = format(int(hexVal), f'0{int(huffLen)}b')
escSeq = ''
for k in range(nTupleSize):
if esc[k]:
escSeq += '1' * N[k]
escSeq += '0'
escSeq += format(abs(nTuple[k]) - (1 << N4[k]), f'0{N4[k]}b')
signBits = ''.join('1' if v < 0 else '0' for v in nTuple)
huffSec.append(bits + signBits + escSeq)
return ''.join(huffSec)
# ------------------ DECODE ------------------
def decode_huff(huff_sec, huff_LUT):
"""
Decode a Huffman-encoded stream.
Parameters
----------
huff_sec : array-like of int or str
Huffman encoded stream as a sequence of 0 and 1 (string or list/array).
huff_LUT : dict
Huffman lookup table with keys:
- 'invTable': inverse table (numpy array)
- 'codebook': codebook number
- 'nTupleSize': tuple size
- 'maxAbsCodeVal': maximum absolute code value
- 'signedValues': True/False
Returns
-------
decCoeffs : list of int
Decoded quantized coefficients.
"""
h = huff_LUT['invTable']
huffCodebook = huff_LUT['codebook']
nTupleSize = huff_LUT['nTupleSize']
maxAbsCodeVal = huff_LUT['maxAbsCodeVal']
signedValues = huff_LUT['signedValues']
# Convert string to array of ints
if isinstance(huff_sec, str):
huff_sec = np.array([int(b) for b in huff_sec])
eos = False
decCoeffs = []
streamIndex = 0
while not eos:
wordbit = 0
r = 0 # start at root
# Decode Huffman word using inverse table
while True:
b = huff_sec[streamIndex + wordbit]
wordbit += 1
rOld = r
r = h[rOld, b]
if h[r, 0] == 0 and h[r, 1] == 0:
symbolIndex = h[r, 2] - 1 # zero-based
streamIndex += wordbit
break
# Decode n-tuple magnitudes
if signedValues:
base = 2 * maxAbsCodeVal + 1
nTupleDec = []
tmp = symbolIndex
for p in reversed(range(nTupleSize)):
val = tmp // (base ** p)
nTupleDec.append(val - maxAbsCodeVal)
tmp = tmp % (base ** p)
nTupleDec = np.array(nTupleDec)
else:
base = maxAbsCodeVal + 1
nTupleDec = []
tmp = symbolIndex
for p in reversed(range(nTupleSize)):
val = tmp // (base ** p)
nTupleDec.append(val)
tmp = tmp % (base ** p)
nTupleDec = np.array(nTupleDec)
# Apply sign bits
nTupleSignBits = huff_sec[streamIndex:streamIndex + nTupleSize]
nTupleSign = -(np.sign(nTupleSignBits - 0.5))
streamIndex += nTupleSize
nTupleDec = nTupleDec * nTupleSign
# Handle escape sequences
escIndex = np.where(np.abs(nTupleDec) == 16)[0]
if huffCodebook == 11 and escIndex.size > 0:
for idx in escIndex:
N = 0
b = huff_sec[streamIndex]
while b:
N += 1
b = huff_sec[streamIndex + N]
# Skip the N leading '1' bits AND the terminating '0' delimiter.
# The encoder writes: '1'*N + '0' + <N4 bits>
streamIndex += N +1
N4 = N + 4
escape_word = huff_sec[streamIndex:streamIndex + N4]
escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2)
nTupleDec[idx] = escape_value
# We already consumed the delimiter above; now consume only N4 bits.
streamIndex += N4
# Apply signs again
nTupleDec[escIndex] *= nTupleSign[escIndex]
decCoeffs.extend(nTupleDec.tolist())
if streamIndex >= len(huff_sec):
eos = True
return decCoeffs
+5 -2
View File
@@ -381,12 +381,15 @@ def decode_huff(huff_sec, huff_LUT):
while b: while b:
N += 1 N += 1
b = huff_sec[streamIndex + N] b = huff_sec[streamIndex + N]
streamIndex += N # Skip the N leading '1' bits AND the terminating '0' delimiter.
# The encoder writes: '1'*N + '0' + <N4 bits>
streamIndex += N +1
N4 = N + 4 N4 = N + 4
escape_word = huff_sec[streamIndex:streamIndex + N4] escape_word = huff_sec[streamIndex:streamIndex + N4]
escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2) escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2)
nTupleDec[idx] = escape_value nTupleDec[idx] = escape_value
streamIndex += N4 + 1 # We already consumed the delimiter above; now consume only N4 bits.
streamIndex += N4
# Apply signs again # Apply signs again
nTupleDec[escIndex] *= nTupleSign[escIndex] nTupleDec[escIndex] *= nTupleSign[escIndex]
+4 -1
View File
@@ -1,4 +1,7 @@
[pytest] [pytest]
pythonpath = . pythonpath = .
testpaths = testpaths =
core/tests core/tests
filterwarnings =
error::RuntimeWarning
+2 -1
View File
@@ -2,4 +2,5 @@ numpy
scipy scipy
scipy-stubs scipy-stubs
soundfile soundfile
pytest pytest
matplotlib