Compare commits
8
Commits
9931e3830a
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a23a1525a2 | ||
|
|
b6dcb80a73 | ||
|
|
17228f8539 | ||
|
|
6b1ebc3cfe | ||
|
|
781cb3047f | ||
|
|
cd2b89bd73 | ||
|
|
4ebee28e4e | ||
|
|
ae4ad82136 |
Binary file not shown.
@@ -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).
|
||||
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.
|
||||
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 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
|
||||
|
||||
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:
|
||||
- `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.
|
||||
- `level_3` Containing the baseline implementation of the required processing chain for Level 3.
|
||||
|
||||
Each level contains:
|
||||
- a module file (e.g., `level_1/level_1.py`)
|
||||
- a dedicated `core/` directory
|
||||
- a dedicated `material/` directory
|
||||
```
|
||||
root/
|
||||
│
|
||||
├── source/
|
||||
│ ├── level_1/
|
||||
│ ├── level_2/
|
||||
│ ├── level_3/
|
||||
│ ├── core/
|
||||
│ └── material/
|
||||
│
|
||||
├── report/
|
||||
├── README.md
|
||||
└── LICENSE
|
||||
```
|
||||
|
||||
- `core`
|
||||
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
|
||||
### `source/`
|
||||
|
||||
- `report` Directory that contains the TeX files for the report
|
||||
- `root directory files` Like Readme.md, LICENSE, etc...
|
||||
Contains all implementation code.
|
||||
|
||||
#### `level_x/`
|
||||
|
||||
### Notes on Repository structure and Development Workflow
|
||||
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.
|
||||
Each level directory contains:
|
||||
|
||||
- `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 (Sequence Segmentation Control)
|
||||
- Filterbank (MDCT analysis) and inverse filterbank (IMDCT synthesis)
|
||||
- End-to-end encoder/decoder functions:
|
||||
- SSC
|
||||
- MDCT / IMDCT filterbank
|
||||
- Quantizer / dequantizer
|
||||
- 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()`
|
||||
- `i_aac_coder_1()`
|
||||
- Demo function:
|
||||
- Demo:
|
||||
- `demo_aac_1()`
|
||||
|
||||
Tests (current status):
|
||||
- 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`
|
||||
### Testing Coverage
|
||||
|
||||
### 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
|
||||
python -m level_1 <input.wav> <output.wav>
|
||||
python -m level_x <input.wav> <output.wav>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
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.
|
||||
In order to run the demo functionality you should be inside the `source/` directory.
|
||||
- Overall SNR (dB)
|
||||
- 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
|
||||
pytest -v
|
||||
```
|
||||
|
||||
To run only `level_1/tests` or a specific test file:
|
||||
Run specific level tests:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
## Disclaimer
|
||||
---
|
||||
|
||||
This project was developed solely for educational purposes.
|
||||
It is provided "as is", without any express or implied warranties.
|
||||
# Reproducibility
|
||||
|
||||
- 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.
|
||||
This implementation should not be used in production environments.
|
||||
|
||||
|
||||
@@ -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 |
Binary file not shown.
@@ -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
@@ -26,14 +26,91 @@ 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_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)
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -122,7 +199,10 @@ def aac_pack_frame_f_to_seq_channels(frame_type: FrameType, frame_f: FrameF) ->
|
||||
# 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.
|
||||
|
||||
@@ -139,6 +219,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
|
||||
filename_in : Union[str, Path]
|
||||
Input WAV filename.
|
||||
Assumption: stereo audio, sampling rate 48 kHz.
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -165,8 +247,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
|
||||
aac_seq: AACSeq1 = []
|
||||
prev_frame_type: FrameType = "OLS"
|
||||
|
||||
win_type: WinType = WIN_TYPE
|
||||
|
||||
if verbose:
|
||||
print("Encoding ", end="", flush=True)
|
||||
for i in range(K):
|
||||
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)
|
||||
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)
|
||||
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,
|
||||
"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]) -> AACSeq2:
|
||||
def aac_coder_2(
|
||||
filename_in: Union[str, Path],
|
||||
verbose: bool = False
|
||||
) -> AACSeq2:
|
||||
"""
|
||||
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]
|
||||
Input WAV filename (stereo, 48 kHz).
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -238,6 +330,8 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
|
||||
aac_seq: AACSeq2 = []
|
||||
prev_frame_type: FrameType = "OLS"
|
||||
|
||||
if verbose:
|
||||
print("Encoding ", end="", flush=True)
|
||||
for i in range(K):
|
||||
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)
|
||||
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)
|
||||
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
|
||||
|
||||
# Unpack to per-channel (as you already do in Level 1)
|
||||
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)
|
||||
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)
|
||||
@@ -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},
|
||||
}
|
||||
)
|
||||
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
|
||||
|
||||
return aac_seq
|
||||
@@ -15,6 +15,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# Imports
|
||||
from typing import Final
|
||||
|
||||
from core.aac_types import WinType
|
||||
|
||||
# Filterbank
|
||||
@@ -28,4 +30,12 @@ WIN_TYPE: WinType = "SIN"
|
||||
# ------------------------------------------------------------
|
||||
PRED_ORDER = 4
|
||||
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
@@ -9,16 +9,9 @@
|
||||
# cchoutou@ece.auth.gr
|
||||
#
|
||||
# Description:
|
||||
# Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
# Keeps the same functional behavior as the original level_1 implementation:
|
||||
# - 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
|
||||
# - Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
# - Level 2 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
#
|
||||
# Note:
|
||||
# This core module returns the reconstructed samples. Writing to disk is kept
|
||||
# in level_x demos.
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,14 +22,27 @@ 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 *
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
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()).
|
||||
|
||||
@@ -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().
|
||||
filename_out : Union[str, Path]
|
||||
Output WAV filename. Assumption: 48 kHz, stereo.
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
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
|
||||
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"]
|
||||
@@ -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)
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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).
|
||||
|
||||
@@ -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().
|
||||
filename_out : Union[str, Path]
|
||||
Output WAV filename.
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
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
|
||||
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"]
|
||||
@@ -250,8 +279,167 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from core.aac_utils import mdct, imdct
|
||||
from core.aac_types import *
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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):
|
||||
start = 448 + 128 * j
|
||||
seg = x_ch[start:start + 256] * wS # (256,)
|
||||
X_esh[:, j] = _mdct(seg) # (128,)
|
||||
X_esh[:, j] = mdct(seg) # (128,)
|
||||
|
||||
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:
|
||||
# start = 448 + 128*j, j=0..7 (50% overlap)
|
||||
for j in range(8):
|
||||
seg = _imdct(X_esh[:, j]) * wS # (256,)
|
||||
seg = imdct(X_esh[:, j]) * wS # (256,)
|
||||
start = 448 + 128 * j
|
||||
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:
|
||||
@@ -385,8 +318,8 @@ def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -
|
||||
|
||||
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
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
xL = _imdct(frame_F[:, 0]) * w
|
||||
xR = _imdct(frame_F[:, 1]) * w
|
||||
xL = imdct(frame_F[:, 0]) * w
|
||||
xR = imdct(frame_F[:, 1]) * w
|
||||
|
||||
out = np.empty((2048, 2), dtype=np.float64)
|
||||
out[:, 0] = xL
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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).
|
||||
|
||||
|
||||
+12
-47
@@ -30,9 +30,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
from scipy.io import loadmat
|
||||
|
||||
from core.aac_utils import load_b219_tables
|
||||
from core.aac_configuration import PRED_ORDER, QUANT_STEP, QUANT_MAX
|
||||
from core.aac_types import *
|
||||
|
||||
@@ -40,43 +38,8 @@ from core.aac_types import *
|
||||
# Private helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
_B219_CACHE: dict[str, FloatArray] | None = None
|
||||
|
||||
|
||||
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:
|
||||
def _band_ranges(k_count: int) -> BandRanges:
|
||||
"""
|
||||
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]])
|
||||
Each tuple is (start_k, end_k) inclusive.
|
||||
"""
|
||||
tables = _load_b219_tables()
|
||||
tables = load_b219_tables()
|
||||
if k_count == 1024:
|
||||
tbl = tables["B219a"]
|
||||
elif k_count == 128:
|
||||
@@ -103,7 +66,7 @@ def _band_ranges_for_kcount(k_count: int) -> BandRanges:
|
||||
start = tbl[:, 1].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:
|
||||
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)
|
||||
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)
|
||||
|
||||
for s, e in bands:
|
||||
@@ -384,7 +347,7 @@ def _apply_itns_iir(y: MdctCoeffs, a_q: MdctCoeffs) -> MdctCoeffs:
|
||||
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).
|
||||
|
||||
@@ -411,7 +374,9 @@ def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
|
||||
sw = _compute_sw(x)
|
||||
|
||||
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_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]:
|
||||
@@ -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)
|
||||
|
||||
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
|
||||
|
||||
@@ -478,7 +443,7 @@ def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChan
|
||||
else:
|
||||
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,):
|
||||
y_out = y_vec
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
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
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -280,3 +335,77 @@ Level 2 adds:
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -19,44 +19,70 @@ import numpy as np
|
||||
import pytest
|
||||
import soundfile as sf
|
||||
|
||||
from core.aac_coder import aac_coder_1, aac_coder_2, aac_read_wav_stereo_48k
|
||||
from core.aac_decoder import aac_decoder_1, aac_decoder_2, aac_remove_padding
|
||||
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_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_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()
|
||||
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.
|
||||
|
||||
Length (in seconds) must be provided via indirect parametrization.
|
||||
"""
|
||||
length = float(request.param)
|
||||
rng = np.random.default_rng(123)
|
||||
fs = 48000
|
||||
|
||||
# ~1 second of audio (kept small for test speed).
|
||||
n = fs
|
||||
n = int(fs * length)
|
||||
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)
|
||||
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
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def test_aac_read_wav_stereo_48k_roundtrip(tmp_stereo_wav: Path) -> None:
|
||||
"""
|
||||
Contract test for aac_read_wav_stereo_48k():
|
||||
- Reads stereo WAV
|
||||
- Returns float64 array with shape (N,2)
|
||||
- Returns fs = 48000
|
||||
"""
|
||||
x, fs = aac_read_wav_stereo_48k(tmp_stereo_wav)
|
||||
@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:
|
||||
x, fs = aac_read_wav_stereo_48k(mk_random_stereo_wav)
|
||||
|
||||
assert int(fs) == 48000
|
||||
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:
|
||||
"""
|
||||
Contract test for aac_remove_padding():
|
||||
- Removes 'hop' samples from start and end.
|
||||
"""
|
||||
hop = 1024
|
||||
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:
|
||||
"""
|
||||
aac_remove_padding must raise if y_pad is shorter than 2*hop.
|
||||
"""
|
||||
hop = 1024
|
||||
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
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_aac_coder_seq_schema_and_shapes(tmp_stereo_wav: Path) -> None:
|
||||
"""
|
||||
Module-level contract test:
|
||||
Ensure aac_seq_1 follows the expected schema and per-frame shapes.
|
||||
"""
|
||||
aac_seq: AACSeq1 = aac_coder_1(tmp_stereo_wav)
|
||||
|
||||
@pytest.mark.parametrize("mk_random_stereo_wav", [0.5], indirect=True)
|
||||
def test_aac_coder_seq_schema_and_shapes(mk_random_stereo_wav: Path) -> None:
|
||||
aac_seq: AACSeq1 = aac_coder_1(mk_random_stereo_wav)
|
||||
|
||||
assert isinstance(aac_seq, list)
|
||||
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:
|
||||
assert isinstance(fr, dict)
|
||||
|
||||
# Required keys
|
||||
assert "frame_type" in fr
|
||||
assert "win_type" 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 chr_f.shape == (1024, 1)
|
||||
|
||||
|
||||
def test_end_to_end_aac_coder_decoder_high_snr(tmp_stereo_wav: Path, tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize("mk_actual_stereo_wav", [0.5], indirect=True)
|
||||
def test_level_1_gain_close_to_one(
|
||||
mk_actual_stereo_wav: Path,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
End-to-end test:
|
||||
Encode + decode and check SNR is very high (numerical-noise only).
|
||||
|
||||
The threshold is intentionally loose to avoid fragility across platforms/BLAS.
|
||||
Guardrail: decoded signal should not have a large global gain mismatch.
|
||||
"""
|
||||
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)
|
||||
assert int(fs) == 48000
|
||||
|
||||
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)
|
||||
|
||||
# Basic sanity: output file exists and is readable
|
||||
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
|
||||
|
||||
# SNR against returned array (file should match closely, but we do not require it here).
|
||||
snr = snr_db(x_ref, x_hat)
|
||||
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:
|
||||
"""
|
||||
Module-level contract test (Level 2):
|
||||
Ensure aac_seq_2 follows the expected schema and per-frame shapes, including tns_coeffs.
|
||||
"""
|
||||
aac_seq: AACSeq2 = aac_coder_2(tmp_stereo_wav)
|
||||
@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:
|
||||
aac_seq: AACSeq2 = aac_coder_2(mk_random_stereo_wav)
|
||||
|
||||
assert isinstance(aac_seq, list)
|
||||
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":
|
||||
assert frame_f.shape == (128, 8)
|
||||
assert coeffs.shape[0] == 4
|
||||
assert coeffs.shape[1] == 8
|
||||
assert coeffs.shape == (4, 8)
|
||||
else:
|
||||
assert frame_f.shape == (1024, 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):
|
||||
Encode + decode and check SNR remains very high.
|
||||
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
|
||||
|
||||
Level 2 is still floating-point (TNS is reversible), so reconstruction
|
||||
should remain numerical-noise only.
|
||||
"""
|
||||
x_ref, fs = sf.read(str(tmp_stereo_wav), always_2d=True)
|
||||
out_wav = tmp_path / "decoded_level3.wav"
|
||||
aac_seq_2: AACSeq2 = aac_coder_2(mk_actual_stereo_wav)
|
||||
y_hat: StereoSignal = aac_decoder_2(aac_seq_2, 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_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)
|
||||
assert int(fs) == 48000
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -17,7 +17,7 @@ from typing import Sequence
|
||||
import pytest
|
||||
|
||||
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 *
|
||||
|
||||
# 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()
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
from core.aac_ssc import aac_SSC
|
||||
from core.aac_ssc import aac_ssc
|
||||
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)
|
||||
|
||||
out1 = aac_SSC(frame_t, next_attack, "LSS")
|
||||
out1 = aac_ssc(frame_t, next_attack, "LSS")
|
||||
assert out1 == "ESH"
|
||||
|
||||
out2 = aac_SSC(frame_t, next_attack, "LPS")
|
||||
out2 = aac_ssc(frame_t, next_attack, "LPS")
|
||||
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)
|
||||
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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@@ -230,5 +230,5 @@ def test_threshold_s_must_exceed_1e_3() -> None:
|
||||
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)
|
||||
|
||||
out = aac_SSC(frame_t, next_t, "OLS")
|
||||
out = aac_ssc(frame_t, next_t, "OLS")
|
||||
assert out == "OLS"
|
||||
@@ -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_types import *
|
||||
|
||||
EPS = 1e-12
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper utilities
|
||||
@@ -194,3 +195,132 @@ def test_tns_outputs_are_finite() -> None:
|
||||
out_esh, coeffs_esh = aac_tns(frame_F_esh, "ESH")
|
||||
assert np.isfinite(out_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
|
||||
|
||||
@@ -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
|
||||
@@ -26,14 +26,91 @@ 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_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)
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -122,7 +199,10 @@ def aac_pack_frame_f_to_seq_channels(frame_type: FrameType, frame_f: FrameF) ->
|
||||
# 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.
|
||||
|
||||
@@ -139,6 +219,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
|
||||
filename_in : Union[str, Path]
|
||||
Input WAV filename.
|
||||
Assumption: stereo audio, sampling rate 48 kHz.
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -165,8 +247,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
|
||||
aac_seq: AACSeq1 = []
|
||||
prev_frame_type: FrameType = "OLS"
|
||||
|
||||
win_type: WinType = WIN_TYPE
|
||||
|
||||
if verbose:
|
||||
print("Encoding ", end="", flush=True)
|
||||
for i in range(K):
|
||||
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)
|
||||
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)
|
||||
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,
|
||||
"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]) -> AACSeq2:
|
||||
def aac_coder_2(
|
||||
filename_in: Union[str, Path],
|
||||
verbose: bool = False
|
||||
) -> AACSeq2:
|
||||
"""
|
||||
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]
|
||||
Input WAV filename (stereo, 48 kHz).
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -238,6 +330,8 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
|
||||
aac_seq: AACSeq2 = []
|
||||
prev_frame_type: FrameType = "OLS"
|
||||
|
||||
if verbose:
|
||||
print("Encoding ", end="", flush=True)
|
||||
for i in range(K):
|
||||
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)
|
||||
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)
|
||||
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
|
||||
|
||||
# Unpack to per-channel (as you already do in Level 1)
|
||||
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)
|
||||
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)
|
||||
@@ -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},
|
||||
}
|
||||
)
|
||||
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
|
||||
|
||||
return aac_seq
|
||||
@@ -15,6 +15,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# Imports
|
||||
from typing import Final
|
||||
|
||||
from core.aac_types import WinType
|
||||
|
||||
# Filterbank
|
||||
@@ -28,4 +30,12 @@ WIN_TYPE: WinType = "SIN"
|
||||
# ------------------------------------------------------------
|
||||
PRED_ORDER = 4
|
||||
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)
|
||||
@@ -9,16 +9,9 @@
|
||||
# cchoutou@ece.auth.gr
|
||||
#
|
||||
# Description:
|
||||
# Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
# Keeps the same functional behavior as the original level_1 implementation:
|
||||
# - 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
|
||||
# - Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
# - Level 2 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
#
|
||||
# Note:
|
||||
# This core module returns the reconstructed samples. Writing to disk is kept
|
||||
# in level_x demos.
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,14 +22,27 @@ 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 *
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
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()).
|
||||
|
||||
@@ -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().
|
||||
filename_out : Union[str, Path]
|
||||
Output WAV filename. Assumption: 48 kHz, stereo.
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
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
|
||||
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"]
|
||||
@@ -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)
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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).
|
||||
|
||||
@@ -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().
|
||||
filename_out : Union[str, Path]
|
||||
Output WAV filename.
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
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
|
||||
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"]
|
||||
@@ -250,8 +279,167 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from core.aac_utils import mdct, imdct
|
||||
from core.aac_types import *
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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):
|
||||
start = 448 + 128 * j
|
||||
seg = x_ch[start:start + 256] * wS # (256,)
|
||||
X_esh[:, j] = _mdct(seg) # (128,)
|
||||
X_esh[:, j] = mdct(seg) # (128,)
|
||||
|
||||
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:
|
||||
# start = 448 + 128*j, j=0..7 (50% overlap)
|
||||
for j in range(8):
|
||||
seg = _imdct(X_esh[:, j]) * wS # (256,)
|
||||
seg = imdct(X_esh[:, j]) * wS # (256,)
|
||||
start = 448 + 128 * j
|
||||
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:
|
||||
@@ -385,8 +318,8 @@ def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -
|
||||
|
||||
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
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
xL = _imdct(frame_F[:, 0]) * w
|
||||
xR = _imdct(frame_F[:, 1]) * w
|
||||
xL = imdct(frame_F[:, 0]) * w
|
||||
xR = imdct(frame_F[:, 1]) * w
|
||||
|
||||
out = np.empty((2048, 2), dtype=np.float64)
|
||||
out[:, 0] = xL
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
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
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -280,3 +335,77 @@ Level 2 adds:
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -20,6 +20,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from tabnanny import verbose
|
||||
from typing import Union
|
||||
|
||||
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_read_wav_stereo_48k
|
||||
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
|
||||
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:
|
||||
@@ -73,7 +74,7 @@ def aac_decoder_1(aac_seq_1: AACSeq1, filename_out: Union[str, Path]) -> StereoS
|
||||
StereoSignal
|
||||
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)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -381,12 +381,15 @@ def decode_huff(huff_sec, huff_LUT):
|
||||
while b:
|
||||
N += 1
|
||||
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
|
||||
escape_word = huff_sec[streamIndex:streamIndex + N4]
|
||||
escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2)
|
||||
nTupleDec[idx] = escape_value
|
||||
streamIndex += N4 + 1
|
||||
# We already consumed the delimiter above; now consume only N4 bits.
|
||||
streamIndex += N4
|
||||
# Apply signs again
|
||||
nTupleDec[escIndex] *= nTupleSign[escIndex]
|
||||
|
||||
|
||||
@@ -26,14 +26,91 @@ 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_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)
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -122,7 +199,10 @@ def aac_pack_frame_f_to_seq_channels(frame_type: FrameType, frame_f: FrameF) ->
|
||||
# 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.
|
||||
|
||||
@@ -139,6 +219,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
|
||||
filename_in : Union[str, Path]
|
||||
Input WAV filename.
|
||||
Assumption: stereo audio, sampling rate 48 kHz.
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -165,8 +247,8 @@ def aac_coder_1(filename_in: Union[str, Path]) -> AACSeq1:
|
||||
aac_seq: AACSeq1 = []
|
||||
prev_frame_type: FrameType = "OLS"
|
||||
|
||||
win_type: WinType = WIN_TYPE
|
||||
|
||||
if verbose:
|
||||
print("Encoding ", end="", flush=True)
|
||||
for i in range(K):
|
||||
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)
|
||||
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)
|
||||
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,
|
||||
"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]) -> AACSeq2:
|
||||
def aac_coder_2(
|
||||
filename_in: Union[str, Path],
|
||||
verbose: bool = False
|
||||
) -> AACSeq2:
|
||||
"""
|
||||
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]
|
||||
Input WAV filename (stereo, 48 kHz).
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -238,6 +330,8 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
|
||||
aac_seq: AACSeq2 = []
|
||||
prev_frame_type: FrameType = "OLS"
|
||||
|
||||
if verbose:
|
||||
print("Encoding ", end="", flush=True)
|
||||
for i in range(K):
|
||||
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)
|
||||
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)
|
||||
frame_f_stereo = aac_filter_bank(frame_t, frame_type, WIN_TYPE)
|
||||
|
||||
# Unpack to per-channel (as you already do in Level 1)
|
||||
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)
|
||||
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)
|
||||
@@ -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},
|
||||
}
|
||||
)
|
||||
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
|
||||
|
||||
return aac_seq
|
||||
@@ -15,6 +15,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# Imports
|
||||
from typing import Final
|
||||
|
||||
from core.aac_types import WinType
|
||||
|
||||
# Filterbank
|
||||
@@ -28,4 +30,12 @@ WIN_TYPE: WinType = "SIN"
|
||||
# ------------------------------------------------------------
|
||||
PRED_ORDER = 4
|
||||
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)
|
||||
@@ -9,16 +9,9 @@
|
||||
# cchoutou@ece.auth.gr
|
||||
#
|
||||
# Description:
|
||||
# Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
# Keeps the same functional behavior as the original level_1 implementation:
|
||||
# - 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
|
||||
# - Level 1 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
# - Level 2 AAC decoder orchestration (inverse of aac_coder_1()).
|
||||
#
|
||||
# Note:
|
||||
# This core module returns the reconstructed samples. Writing to disk is kept
|
||||
# in level_x demos.
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,14 +22,27 @@ 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 *
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
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()).
|
||||
|
||||
@@ -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().
|
||||
filename_out : Union[str, Path]
|
||||
Output WAV filename. Assumption: 48 kHz, stereo.
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
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
|
||||
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"]
|
||||
@@ -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)
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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).
|
||||
|
||||
@@ -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().
|
||||
filename_out : Union[str, Path]
|
||||
Output WAV filename.
|
||||
verbose : bool
|
||||
Optional argument to print encoding status
|
||||
|
||||
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
|
||||
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"]
|
||||
@@ -250,8 +279,167 @@ def aac_decoder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# ------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from core.aac_utils import mdct, imdct
|
||||
from core.aac_types import *
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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):
|
||||
start = 448 + 128 * j
|
||||
seg = x_ch[start:start + 256] * wS # (256,)
|
||||
X_esh[:, j] = _mdct(seg) # (128,)
|
||||
X_esh[:, j] = mdct(seg) # (128,)
|
||||
|
||||
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:
|
||||
# start = 448 + 128*j, j=0..7 (50% overlap)
|
||||
for j in range(8):
|
||||
seg = _imdct(X_esh[:, j]) * wS # (256,)
|
||||
seg = imdct(X_esh[:, j]) * wS # (256,)
|
||||
start = 448 + 128 * j
|
||||
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:
|
||||
@@ -385,8 +318,8 @@ def aac_filter_bank(frame_T: FrameT, frame_type: FrameType, win_type: WinType) -
|
||||
|
||||
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
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
xL = _imdct(frame_F[:, 0]) * w
|
||||
xR = _imdct(frame_F[:, 1]) * w
|
||||
xL = imdct(frame_F[:, 0]) * w
|
||||
xR = imdct(frame_F[:, 1]) * w
|
||||
|
||||
out = np.empty((2048, 2), dtype=np.float64)
|
||||
out[:, 0] = xL
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -30,9 +30,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
from scipy.io import loadmat
|
||||
|
||||
from core.aac_utils import load_b219_tables
|
||||
from core.aac_configuration import PRED_ORDER, QUANT_STEP, QUANT_MAX
|
||||
from core.aac_types import *
|
||||
|
||||
@@ -40,43 +38,8 @@ from core.aac_types import *
|
||||
# Private helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
_B219_CACHE: dict[str, FloatArray] | None = None
|
||||
|
||||
|
||||
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:
|
||||
def _band_ranges(k_count: int) -> BandRanges:
|
||||
"""
|
||||
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]])
|
||||
Each tuple is (start_k, end_k) inclusive.
|
||||
"""
|
||||
tables = _load_b219_tables()
|
||||
tables = load_b219_tables()
|
||||
if k_count == 1024:
|
||||
tbl = tables["B219a"]
|
||||
elif k_count == 128:
|
||||
@@ -103,7 +66,7 @@ def _band_ranges_for_kcount(k_count: int) -> BandRanges:
|
||||
start = tbl[:, 1].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:
|
||||
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)
|
||||
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)
|
||||
|
||||
for s, e in bands:
|
||||
@@ -384,7 +347,7 @@ def _apply_itns_iir(y: MdctCoeffs, a_q: MdctCoeffs) -> MdctCoeffs:
|
||||
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).
|
||||
|
||||
@@ -411,7 +374,9 @@ def _tns_one_vector(x: MdctCoeffs) -> tuple[MdctCoeffs, MdctCoeffs]:
|
||||
sw = _compute_sw(x)
|
||||
|
||||
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_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]:
|
||||
@@ -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)
|
||||
|
||||
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
|
||||
|
||||
@@ -478,7 +443,7 @@ def aac_tns(frame_F_in: FrameChannelF, frame_type: FrameType) -> Tuple[FrameChan
|
||||
else:
|
||||
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,):
|
||||
y_out = y_vec
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
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
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -280,3 +335,77 @@ Level 2 adds:
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -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_read_wav_stereo_48k
|
||||
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)
|
||||
@@ -51,7 +51,7 @@ def aac_coder_2(filename_in: Union[str, Path]) -> AACSeq2:
|
||||
AACSeq2
|
||||
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:
|
||||
@@ -72,7 +72,7 @@ def i_aac_coder_2(aac_seq_2: AACSeq2, filename_out: Union[str, Path]) -> StereoS
|
||||
StereoSignal
|
||||
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.
@@ -381,12 +381,15 @@ def decode_huff(huff_sec, huff_LUT):
|
||||
while b:
|
||||
N += 1
|
||||
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
|
||||
escape_word = huff_sec[streamIndex:streamIndex + N4]
|
||||
escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2)
|
||||
nTupleDec[idx] = escape_value
|
||||
streamIndex += N4 + 1
|
||||
# We already consumed the delimiter above; now consume only N4 bits.
|
||||
streamIndex += N4
|
||||
# Apply signs again
|
||||
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 |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
"""
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
BIN
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -381,12 +381,15 @@ def decode_huff(huff_sec, huff_LUT):
|
||||
while b:
|
||||
N += 1
|
||||
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
|
||||
escape_word = huff_sec[streamIndex:streamIndex + N4]
|
||||
escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2)
|
||||
nTupleDec[idx] = escape_value
|
||||
streamIndex += N4 + 1
|
||||
# We already consumed the delimiter above; now consume only N4 bits.
|
||||
streamIndex += N4
|
||||
# Apply signs again
|
||||
nTupleDec[escIndex] *= nTupleSign[escIndex]
|
||||
|
||||
|
||||
+4
-1
@@ -1,4 +1,7 @@
|
||||
[pytest]
|
||||
pythonpath = .
|
||||
testpaths =
|
||||
core/tests
|
||||
core/tests
|
||||
|
||||
filterwarnings =
|
||||
error::RuntimeWarning
|
||||
@@ -2,4 +2,5 @@ numpy
|
||||
scipy
|
||||
scipy-stubs
|
||||
soundfile
|
||||
pytest
|
||||
pytest
|
||||
matplotlib
|
||||
Reference in New Issue
Block a user