chore: add gitignore, documentation, and LC3 conformance test software

- Add .gitignore for Python, virtual environments, testing artifacts, IDE files, and LC3 test outputs including SQAM audio files
- Add AGENTS.md with project context for LC3 implementation testing
- Add LC3.TS.p5.pdf test specification document
- Add LC3 conformance interoperability test software V1.0.8 with script readme and reference binary symlink
This commit is contained in:
2026-02-27 14:56:35 +01:00
commit 2845458d3f
49 changed files with 4252 additions and 0 deletions

112
.gitignore vendored Normal file
View File

@@ -0,0 +1,112 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.so
# Virtual environments
.venv/
venv/
env/
# Distribution / packaging
dist/
build/
*.egg-info/
*.egg
# Testing
.pytest_cache/
.coverage
htmlcov/
# Poetry
poetry.lock
# Conformance test outputs
results/
work/
test_items/
SoX/
*.html
*.bin
*.lc3
*.log
# IDE
.idea/
.vscode/
*.swp
*~
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/01.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/02.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/03.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/04.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/05.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/06.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/07.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/08.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/09.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/10.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/11.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/12.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/13.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/14.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/15.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/16.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/17.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/18.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/19.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/20.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/21.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/22.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/23.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/24.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/25.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/26.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/27.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/28.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/29.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/30.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/31.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/32.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/33.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/34.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/35.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/36.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/37.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/38.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/39.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/40.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/41.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/42.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/43.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/44.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/45.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/46.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/47.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/48.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/49.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/50.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/51.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/52.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/53.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/54.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/55.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/56.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/57.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/58.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/59.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/60.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/61.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/62.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/63.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/64.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/65.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/66.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/67.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/68.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/69.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/70.flac
LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/SQAM/EBU SQAM.m3u

4
AGENTS.md Normal file
View File

@@ -0,0 +1,4 @@
- we want to test the lc3 implementation of our bumble-auracast repo that shall be in this exact workspace
- LC3.TS.p5.pdf is a description of the test cases
- letter_from_consultant.txt is a letter from a consultant
- check the readme if further context is needed

BIN
LC3.TS.p5.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
/home/pstruebi/repos/lc3_quali/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/LC3.exe

View File

@@ -0,0 +1,392 @@
***************************************************************************************************************
Low Complexity Communication Codec - LC3 Conformance Interoperability Test Software Release V1.0.8 2024/07/01
(C) 2021 Copyright Ericsson AB and Fraunhofer Gesellschaft zur Foerderung
der angewandten Forschung e.V. for its Fraunhofer IIS.
This software and/or program is protected by copyright law and international
treaties and shall solely be used as set out in the
BLUETOOTH SPECIAL INTEREST GROUP LC3 CONFORMANCE INTEROPERABILTITY
TEST SOFTWARE END USER LICENSE AGREEMENT
(EULA, see https://btprodspecificationrefs.blob.core.windows.net/eula-lc3/Bluetooth-SIG-LC3-EULA.pdf)
No copying, distribution, or use other than as expressly provided in the EULA
is hereby authorized by implication, estoppel or otherwise.
All rights not expressly granted are reserved.
**************************************************************************************************************
================================
LC3 Conformance script V0.6.3
================================
Changelog:
0.6.3 - Updated download link to SQAM EBU test items.
Added configuration files for LC3 HFP SWB test
0.6.2 - Added additional information on usage of gstPEAQ
0.6.1 - Fixed bitrate in band limitation test (48 kHz 92 kbps -> 96 kbps)
- Removed obsolete prepare step for 7 kHz and 96 kHz files that was initially used for
the sanity-checks test as this test was already removed from the script
- Removed test_error_detection as it is not featured in the LC3 TS
- Fixed bitrate in test_low_pass from 80 kbps to 96 kbps as 80 kbps no longer supported for 48 kHz
- Updated default enabled tests in .cfg files
- Added additional BAND_LIMITS array for 7.5 ms to account for different bitrates
0.6 - Updated copyright header
0.5.5 - imporved logging, small fixes
- changed scientific to decimal notation in html file
- simplyfied energy calculation
- set max. abs. diff. threshold to 0.00148
- set rms as decoder metric and peaq as encoder/encdec metric
- renamed test_high_pass test_low_pass
- added -system_sox option
0.5.4 Fixed file alignment for files with same length
0.5.3 Fixed bandlimit test as wrong input items was used
0.5.2 Added frame_ms option
0.5.1 Better command logging
0.5.0 Initial release
Pre-requisites
==============
- python3
- python numpy module
- SoX (http://sox.sourceforge.net), Windows binary (sox-14.4.2-win32.zip), downloaded automatically
- An ITU-BS.1387 (PEAQ - advanced) implementation.
In case the PEAQ - advanced implementation is not available, gstPEAQ [3] can be used instead. In order to build and install gstPEAQ, use the following commands:
- Download/clone the release: https://github.com/HSU-ANT/gstpeaq/archive/refs/tags/version-0.6.1.zip
- ./autogen.sh
- make
- make install (optional step, installs gstPEAQ in /usr/bin/)
The PEAQ binary is linked into the src/ directory: ./src/peaq is the executable. In case you cannot install it (but just build with “make”), you might need to specify the path to the shared PEAQ library manually:
./src/peaq --gst-plugin-load ./src/.libs/libgstpeaq.so --advanced "{reference}" "{test}"
After building GstPEAQ, enter the path to the PEAQ binary and the ODG regular expression in the LC3 conformance configuration files:
peaq_bin = ./src/peaq --gst-plugin-load ./src/.libs/libgstpeaq.so --advanced "{reference}" "{test}"
peaq_odg_regex = Objective Difference Grade:\s+(-?\d+.\d+)
In case packages are missing, install the following ones:
- libgstreamer-1.0.so.0
- libgstreamer-0.10.so.0
- git2cl
- gtkdocize
- libgstreamer-plugins-base1.0-dev
- libgstreamer-plugins-bad1.0-dev
- gstreamer1.0-plugins-base
- gstreamer1.0-plugins-good
- gstreamer1.0-plugins-bad
- gstreamer1.0-plugins-ugly
- gstreamer1.0-libav
- gstreamer1.0-tools
- gstreamer1.0-x
- gstreamer1.0-alsa
- gstreamer1.0-gl
- gstreamer1.0-gtk3
- gstreamer1.0-qt5
- gstreamer1.0-pulseaudio
The binary can be used as follows: peaq [--advanced] {REFFILE} {TESTFILE}
- On non-Windows platforms: Wine; Win32 is the reference platform to ensure
bit exact behavior
- On Windows: Cygwin and the following packages installed through Cygwin:
-python3
-numpy
-curl
-gcc
To-Do's on first time usage
============================
If you are running the script for the very first time, please make sure to do the following things:
-replace "peaq_binary" in desired configuration file by path to PEAQ executables
-adjust PEAQ regular expression in the configuration file to find ODG of PEAQ output
-create a folder called 'LC3_bin_current' in the same folder as the conformance script and put the reference
executable provided by Fraunhofer & Ericsson in there, i.e. your structure should look like './LC3_bin_current/LC3.exe'
-set paths to encoder and decoder executables under test in the configuration file [globals]
Usage of the script:
====================
python3 conformanceCheck.py [-h] [-v] [-w WORKERS] [-keep] [-system_sox] CONFIG
LC3 conformance tool - checks if a vendor implementation of the LC3 codec is
conforming to the binary provided by Fraunhofer & Ericsson using PEAQ and RMS metrics.
optional arguments:
-h, --help show this help message and exit
-v Activate verbose output
-keep Keep all files produced in the test run
-w Number of workers (threads) for multithreaded execution. Equals number of CPU cores by default.
-system_sox Use SoX installed on system instead of Windows binary with Wine
The script requires a configuration file which contains paths to executables and
operating points to be tested. Each test configuration is indicated by a
configuration name in squared brackets. The configuration to be tested is
selected by 'enabled_tests=CONFIG'. A detailed description of the configuration
file can be found in this Readme.
On Windows the script must be executed from Cygwin!
Usage of configuration file
============================
The configuration file is separated in sections by square brackets. Within each
section, variables can be set by 'variable=value'. Text behind a hash # is a
comment and will be ignored.
In the [globals] section, general parameters are defined, e.g. which
configurations to process, paths and command line for the test executables.
This section also specifies the command line for the PEAQ executable and the
regular expression to its output. The example lists all parameters:
[globals]
enabled_tests=Aprofile # configurations to be tested
encoder = CutEnc.exe {input} {output} {bitrate} {options} # test encoder command line
decoder = CutDec.exe {input} {output} {options} # test decoder command line
peaq_bin = PQevalAudio {reference} {test} # PEAQ command line
peaq_odg_regex = Objective Difference Grade: (-?\d+\.\d+) # regular expression parsing ODG
frame_ms = 10 # Frame size: 10 ms or 7.5 ms
Please note that the user is allowed to change the order of the parameters in {}-brackets above. The script does not care about the order of those parameters.
After the globals section, a number of [test] sections can be
specified describing an individual test for a profile, including operating
points and threshold criteria. The following parameters define a test set:
[Aprofile] # configuration label
# mode can be: encode, decoder, encdec
# mode, samplingrate, bitrates
configs = encode, 16000, 32000 # SQ sender
encode, 24000, 48000 # HQ sender
test_sqam = 1 # regular sqam test, testing set of files with conditions
test_band_limiting = 0 # test band limited signal, e.g. nb signal at 48 kHz
test_low_pass = 0 # test for low pass filter of codec for 20 kHz signal
- default rms threshold is -89 dB RMS and 0.00148 Max.Abs.Diff
- default odg threshold is 0.06. In case gstPEAQ is used as PEAQ - advanced alternative, the odg threshold shall be changed to 0.07 [2] in conformanceCheck.py:136
- each line in configs=... defines a new operating point and must be indented
- sampling rate can be 8000, 16000, 24000, 32000, 44100, 48000
- bitrate can be set as single integer or in form of start:step:end
for example, configure bitrates 16000, 24000, 32000:
'configs = decode,44100,16000:8000:32000'
- only one mode (e.g. encode) is allowed per [test] section. To test more modes another [test] section has to be added.
By default, all test modes are active and all thresholds are set to their default value. The user is able to deactivate certain tests and to adjust the thresholds.
Limitations
===========
The user can set up a test with a preferred sampling rate and bit rate. However
it is not advised to test narrow band signals at a bitrate higher than 64
kBit/s since most PEAQ implementations have trouble with such signals.
Therefore, the conformance script will reject such configurations.
What does the script produce?
=============================
1.) Command line output with 'passed' or 'failed' results
encoder/encdec test:
- passed: if Delta ODG < threshold
for all tests
- failed: if Delta ODG > threshold
for any test
decoder test:
- passed: if Max.Abs.Diff < threshold and RMS < threshold
for all tests
- failed: if Max.Abs.Diff > threshold or RMS > threshold
for any test
2.) Detailed results are saved in html files. For each configuration, the
following columns are displayed depending on test mode:
- Mode: encode, decode or encdec, etc...
- Item: name of SQAM-item
- Sampling rate
- Bitrate
- Delta ODG (threshold): Absolute Difference between ODG (PEAQ - BS.1387)
of reference and test.
- Max. Abs. Diff. (threshold): Maximum of absolute difference between all
samples of reference and test.
- RMS (threshold) [dB]: Root Mean Square of difference between reference
and test in dB.
- RMS reached (threshold) [bits]: Reached RMS criteria in bits.
If a certain threshold has not been passed for the ODG or RMS criteria, the
respective cell will appear marked as red. Otherwise it will appear in blue.
The result files will contain the current date and time as well as the section
name that was selected in the configuration file.
Each test set will also contain statistics displaying the percentage of passed operating points.
How is conformance measured?
============================
For each test configuration given by 'Mode','Sampling rate' and 'Bitrate' in
the config-file, the script will perform a conformance test on specified
EBU-SQAM items, downloaded from https://tech.ebu.ch/publications/sqamcd. The
stereo items are downmixed to mono ((Ch1 + Ch2) / 2) before further processing.
'Mode' specifies whether encoder or decoder is tested. Mode=encdec runs
encode and decoder in a row (ref_ref.wav vs. tst_tst.wav).
The flow chart below shows exemplary how the conformance metric Delta_ODG is
measured for a certain sqam-item with sampling rate(fs), bitrate(br) and
Mode=encoder
sqam-item
|
resample(fs)
|
sqam-item low pass(20kHz)
| |
resample(fs) reference_encoder(br)
| |
low pass(20kHz) reference_decoder
| |
resample(48kHz) resample(48kHz)
| |
align <-> align
| |
orig.wav ------- ref_ref.wav ------> ODG_ref_ref = PEAQ(orig.wav,ref_ref.wav)---,
|
sqam-item |
resample(fs) |
sqam-item low pass(20kHz) |
resample(fs) 'encoder_under_test'(br) |
low pass(20kHz) reference_decoder -(+)-> abs() -> Delta_ODG(encoder) |
resample(48kHz) resample(48kHz) |
align <-> align |
| | |
orig.wav tst_ref.wav ------> ODG_tst_ref = PEAQ(orig.wav,tst_ref.wav)---'
The conformance can either be verified by a PEAQ tool using the objective
difference grade (for encoder/encdec tests)
or by the provided RMS tool (for decoder tests). The RMS tool checks the root mean
square error and the maximum absolute difference between two files and
calculates the k-criteria that should not be lower than a given threshold. The
tool implements the RMS conformance according to the Bluetooth A2DP test
specification, section 6.4.1 [1].
Exemplary flowchart of for RMS conformance metric with sampling rate(fs),
bitrate(br) and Mode=encoder:
sqam-item sqam-item
resample(fs) resample(fs)
low pass(20kHz) low pass(20kHz)
reference_encoder(br) 'encoder_under_test'(br)
reference_decoder reference_decoder
align <-> align
| |
tst_ref.wav ---------- ref_ref.wav ------> RMS( ref_ref - tst_ref)
The default SQAM conformance test can be switched on/off by setting test_sqam = 1/0 in the configuration file.
File alignment:
To compensate small delays between reference and test file before
evaluation with PEAQ/RMS, the files are aligned.
A maximum delay of 322 samples can be compensated
and the sample sizes of reference and test file must not differ
more than 480 samples. The reference binary produces
a wave file with the original sample size by zero padding.
How does the RMS tool work?
===========================
Usage:
./rms file1.wav file2.wav [k]
Where k is an optional parameter to lower the conformance thresholds in the
range of 1 to 16.
The RMS tool compares two wave files and calculates the following values:
- Maximum absolute difference of two samples
- Overall RMS (Root Mean Squared) value in dB
- Segmental SNR in dB
The segment length for the segmental SNR is set to 320 samples for all sampling
rates.
More detailed descriptions of the RMS calculations can be found in [1] and [2].
The RMS tool compares the calculated values with the respective thresholds. The
threshold can be lowered by giving an external [k] parameter. If the given RMS
threshold k was not reached, the RMS tool calculates the threshold k' that
would have been reached.
Test for signals higher than 20kHz
==================================
This test is designed to verify the low pass behaviour of LC3 for frequencies
above 20kHz. For that, the signal White_Noise_HP20 is generated, which consists of
white noise above 20kHz. The energy of the test signal is calculated as shown in
the flowchart below. The metric En is calculated only for the encoder mode and a
bitrate(br) of 80 kbps for the item White_Noise_HP20.
White_Noise_HP20
'encoder_under_test'(br)
reference_decoder
|
tst_ref.wav ------> En = 10 * log10 ( sum( tst_ref ^2) )
In order to pass the conformance, En must be below the threshold (70 dB) independently
of the other metrics.
An additional table in the output file lists the tested configurations:
- Mode: encode, decode or encdec
- Item: name of SQAM-item
- Sampling rate
- Bitrate
- Energy (threshold) in dB
The test can be switched on/off by setting test_low_pass = 1/0.
Test for band limited signals
=============================
To test the bandlimited signals, the item SQAM/Female_Speech_German is
resampled and low pass filtered at the following configurations:
Samplingrate | Bandwidth | Bitrate
--------------+-------------------+---------
16000 | NB | 32000
24000 | NB, WB | 48000
32000 | NB, WB, SSWB | 64000
48000 | NB, WB, SSWB, SWB | 96000
The generated items are then processed using the delta_ODG and RMS criteria as
mentioned above. Since the bandwidth detector is part of the encoder, only the encoder is tested.
The reference decoder is used to decode both bitstreams (ref_bin, tst_bin).
This test can be switched on/off by setting test_band_limiting = 1/0.
For the ODG evaluation, the bandlimited file is used as the reference.
References
==========
[1] Bluetooth A2DP Test Spec, https://www.bluetooth.org/docman/handlers/DownloadDoc.ashx?doc_id=40353
[2] LC3 TS, https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=502301
[3] gstPEAQ v0.6.1 https://github.com/HSU-ANT/gstpeaq/archive/refs/tags/version-0.6.1.zip
Troubleshooting
===============
Be careful when using quotes in additional arguments passed to the codec under
test, for example:
RIGHT SYNTAX: var = -f pattern.txt
WRONG SYNTAX: var = '-f pattern.txt'

View File

@@ -0,0 +1,94 @@
# This file contains settings needed for conformanceCheck.py
# You can add/remove several bitrates or sampling rates from the sections. Make sure that all paths are correct.
# glossary
# odg objective difference grade
[globals]
enabled_tests = ATAS_encode, ATAS_decode, ATAS_encdec, ATAM_encode, ATAM_decode, ATAM_encdec
frame_ms = 10
encoder = ./LC3.exe -E -frame_ms {frame_ms} {options} "{input}" "{output}" {bitrate}
decoder = ./LC3.exe -D {options} "{input}" "{output}"
peaq_bin = peaq_binary "{reference}" "{test}"
peaq_odg_regex = odg:\s+(-?\d+.\d+)
[ATAS_encode]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 8000, 24000
encode, 16000, 32000
encode, 32000, 64000
[ATAS_decode]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 8000, 24000
decode, 16000, 32000
decode, 32000, 64000
[ATAS_encdec]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 8000, 24000
encdec, 16000, 32000
encdec, 32000, 64000
[ATAM_encode]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 48000, 80000
encode, 48000, 96000
encode, 48000, 124000
[ATAM_decode]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 48000, 80000
decode, 48000, 96000
decode, 48000, 124000
[ATAM_encdec]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 48000, 80000
encdec, 48000, 96000
encdec, 48000, 124000

View File

@@ -0,0 +1,104 @@
# This file contains settings needed for conformanceCheck.py
# You can add/remove several bitrates or sampling rates from the sections. Make sure that all paths are correct.
# glossary
# odg objective difference grade
[globals]
enabled_tests = ATAS_encode, ATAS_decode, ATAS_encdec, ATAM_encode, ATAM_decode, ATAM_encdec
frame_ms = 7.5
encoder = ./LC3.exe -E -frame_ms {frame_ms} {options} "{input}" "{output}" {bitrate}
decoder = ./LC3.exe -D {options} "{input}" "{output}"
peaq_bin = peaq_binary "{reference}" "{test}"
peaq_odg_regex = odg:\s+(-?\d+.\d+)
[ATAS_encode]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 16000, 32000
encode, 32000, 64000
[ATAS_decode]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 16000, 32000
decode, 32000, 64000
[ATAS_encdec]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 16000, 32000
encdec, 32000, 64000
[ATAM_encode]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 48000, 80000
encode, 48000, 96000
encode, 48000, 124800
[ATAM_decode]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 48000, 80000
decode, 48000, 96000
decode, 48000, 124800
[ATAM_encdec]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 48000, 80000
encdec, 48000, 96000
encdec, 48000, 124800

View File

@@ -0,0 +1,101 @@
# This file contains settings needed for conformanceCheck.py
# You can add/remove several bitrates or sampling rates from the sections. Make sure that all paths are correct.
# glossary
# odg objective difference grade
[globals]
enabled_tests = ATAM_encode_optional, ATAM_decode_optional, ATAM_encdec_optional, ATAS_encode_optional, ATAS_decode_optional, ATAS_encdec_optional
frame_ms = 10
encoder = ./LC3.exe -E -frame_ms {frame_ms} {options} "{input}" "{output}" {bitrate}
decoder = ./LC3.exe -D {options} "{input}" "{output}"
peaq_bin = peaq_binary "{reference}" "{test}"
peaq_odg_regex = odg:\s+(-?\d+.\d+)
[ATAS_encode_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 8000, 24000
[ATAS_decode_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 8000, 24000
[ATAS_encdec_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 8000, 24000
[ATAM_encode_optional]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 44100, 79380
encode, 44100, 95550
encode, 44100, 123480
[ATAM_decode_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 44100, 79380
decode, 44100, 95550
decode, 44100, 123480
[ATAM_encdec_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 44100, 79380
encdec, 44100, 95550
encdec, 44100, 123480

View File

@@ -0,0 +1,101 @@
# This file contains settings needed for conformanceCheck.py
# You can add/remove several bitrates or sampling rates from the sections. Make sure that all paths are correct.
# glossary
# odg objective difference grade
[globals]
enabled_tests = ATAM_encode_optional, ATAM_decode_optional, ATAM_encdec_optional, ATAS_encode_optional, ATAS_decode_optional, ATAS_encdec_optional
frame_ms = 7.5
encoder = ./LC3.exe -E -frame_ms {frame_ms} {options} "{input}" "{output}" {bitrate}
decoder = ./LC3.exe -D {options} "{input}" "{output}"
peaq_bin = peaq_binary "{reference}" "{test}"
peaq_odg_regex = odg:\s+(-?\d+.\d+)
[ATAS_encode_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 8000, 27734
[ATAS_decode_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 8000, 27734
[ATAS_encdec_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 8000, 27734
[ATAM_encode_optional]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 44100, 79380
encode, 44100, 95060
encode, 44100, 123480
[ATAM_decode_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 44100, 79380
decode, 44100, 95060
decode, 44100, 123480
[ATAM_encdec_optional]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 44100, 79380
encdec, 44100, 95060
encdec, 44100, 123480

View File

@@ -0,0 +1,95 @@
# This file contains settings needed for conformanceCheck.py
# You can add/remove several bitrates or sampling rates from the sections. Make sure that all paths are correct.
# glossary
# odg objective difference grade
[globals]
enabled_tests = HAS_encode, HAS_decode, HAS_encdec, HAM_encode, HAM_decode, HAM_encdec
frame_ms = 10
encoder = ./LC3.exe -E -frame_ms {frame_ms} {options} "{input}" "{output}" {bitrate}
decoder = ./LC3.exe -D {options} "{input}" "{output}"
peaq_bin = peaq_binary "{reference}" "{test}"
peaq_odg_regex = odg:\s+(-?\d+.\d+)
[HAS_encode]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 16000, 32000
[HAS_decode]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 16000, 32000
[HAS_encdec]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 16000, 32000
[HAM_encode]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 24000, 48000
[HAM_decode]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 24000, 48000
[HAM_encdec]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 24000, 48000

View File

@@ -0,0 +1,95 @@
# This file contains settings needed for conformanceCheck.py
# You can add/remove several bitrates or sampling rates from the sections. Make sure that all paths are correct.
# glossary
# odg objective difference grade
[globals]
enabled_tests = HAS_encode, HAS_decode, HAS_encdec, HAM_encode, HAM_decode, HAM_encdec
frame_ms = 7.5
encoder = ./LC3.exe -E -frame_ms {frame_ms} {options} "{input}" "{output}" {bitrate}
decoder = ./LC3.exe -D {options} "{input}" "{output}"
peaq_bin = peaq_binary "{reference}" "{test}"
peaq_odg_regex = odg:\s+(-?\d+.\d+)
[HAS_encode]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 16000, 32000
[HAS_decode]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 16000, 32000
[HAS_encdec]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 16000, 32000
[HAM_encode]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 24000, 48000
[HAM_decode]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 24000, 48000
[HAM_encdec]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encdec, 24000, 48000

View File

@@ -0,0 +1,43 @@
# This file contains settings needed for conformanceCheck.py
# You can add/remove several bitrates or sampling rates from the sections. Make sure that all paths are correct.
# glossary
# odg objective difference grade
[globals]
enabled_tests = HFP_encode, HFP_decode
frame_ms = 7.5
encoder = ./LC3.exe -E -frame_ms {frame_ms} {options} "{input}" "{output}" {bitrate}
decoder = ./LC3.exe -D {options} "{input}" "{output}"
peaq_bin = peaq_binary "{reference}" "{test}"
peaq_odg_regex = odg:\s+(-?\d+.\d+)
[HFP_encode]
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 32000, 61867
[HFP_decode]
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = decode, 32000, 61867

View File

@@ -0,0 +1,47 @@
# LC3 TS p5 - ENC test cases, 10ms frame duration, no 44.1 kHz
# Run from LC3_Conformance_Interoperability_Script/ with:
# poetry run python conformanceCheck.py conf_lc3ts_p5_enc_10ms.cfg
#
# Covers: LC3/ENC/NB/BV-01-C, WB/BV-01-C, SSWB/BV-01-C, SWB/BV-01-C,
# FB/BV-01-C, FB/BV-02-C, FB/BV-03-C
[globals]
enabled_tests = ENC_narrow_10ms, ENC_fb_10ms
frame_ms = 10
encoder = python /home/pstruebi/repos/lc3_quali/qualification/lc3_encode.py -frame_ms {frame_ms} {options} "{input}" "{output}" {bitrate}
decoder = LC3_bin_current/LC3.exe -D {options} "{input}" "{output}"
peaq_bin = /tmp/gstpeaq-version-0.6.1/src/peaq --gst-plugin-load /tmp/gstpeaq-version-0.6.1/src/.libs/libgstpeaq.so --advanced "{reference}" "{test}"
peaq_odg_regex = Objective Difference Grade:\s+(-?\d+\.\d+)
[ENC_narrow_10ms]
# NB/BV-01-C WB/BV-01-C SSWB/BV-01-C SWB/BV-01-C
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 8000, 24000
encode, 16000, 32000
encode, 24000, 48000
encode, 32000, 64000
[ENC_fb_10ms]
# FB/BV-01-C FB/BV-02-C FB/BV-03-C
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 48000, 80000
encode, 48000, 96000
encode, 48000, 124000

View File

@@ -0,0 +1,70 @@
# LC3 TS p5 - ENC test cases, 7.5ms frame duration, no 44.1 kHz
# Run from LC3_Conformance_Interoperability_Script/ with:
# poetry run python conformanceCheck.py conf_lc3ts_p5_enc_75ms.cfg
#
# Covers: LC3/ENC/NB/BV-02-C, WB/BV-02-C, SSWB/BV-02-C, SWB/BV-02-C,
# SWB/BV-03-C, FB/BV-07-C, FB/BV-08-C, FB/BV-09-C
[globals]
enabled_tests = ENC_nb_75ms, ENC_narrow_75ms, ENC_hfp_75ms, ENC_fb_75ms
frame_ms = 7.5
encoder = python /home/pstruebi/repos/lc3_quali/qualification/lc3_encode.py -frame_ms {frame_ms} {options} "{input}" "{output}" {bitrate}
decoder = LC3_bin_current/LC3.exe -D {options} "{input}" "{output}"
peaq_bin = /tmp/gstpeaq-version-0.6.1/src/peaq --gst-plugin-load /tmp/gstpeaq-version-0.6.1/src/.libs/libgstpeaq.so --advanced "{reference}" "{test}"
peaq_odg_regex = Objective Difference Grade:\s+(-?\d+\.\d+)
[ENC_nb_75ms]
# NB/BV-02-C — no band-limiting test per optional cfg
# test modes
test_sqam = 1
test_band_limiting = 0
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 8000, 27734
[ENC_narrow_75ms]
# WB/BV-02-C SSWB/BV-02-C SWB/BV-02-C
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 16000, 32000
encode, 24000, 48000
encode, 32000, 64000
[ENC_hfp_75ms]
# SWB/BV-03-C — HFP profile bitrate (61867 bps)
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 0
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 32000, 61867
[ENC_fb_75ms]
# FB/BV-07-C FB/BV-08-C FB/BV-09-C
# test modes
test_sqam = 1
test_band_limiting = 1
test_low_pass = 1
test_rate_switching = 0
# Mode, Samplingrate, Bitrate
configs = encode, 48000, 80000
encode, 48000, 96000
encode, 48000, 124800

View File

@@ -0,0 +1,817 @@
#!/usr/bin/env python3
#
# #/************************************************************************************************************
# Low Complexity Communication Codec - LC3 Conformance Interoperability Test Software Release V1.0.8 2024/07/01
#
# (C) 2021 Copyright Ericsson AB and Fraunhofer Gesellschaft zur Foerderung
# der angewandten Forschung e.V. for its Fraunhofer IIS.
#
# This software and/or program is protected by copyright law and international
# treaties and shall solely be used as set out in the
# BLUETOOTH SPECIAL INTEREST GROUP LC3 CONFORMANCE INTEROPERABILTITY
# TEST SOFTWARE END USER LICENSE AGREEMENT
# (EULA, see https://btprodspecificationrefs.blob.core.windows.net/eula-lc3/Bluetooth-SIG-LC3-EULA.pdf)
#
# No copying, distribution, or use other than as expressly provided in the EULA
# is hereby authorized by implication, estoppel or otherwise.
# All rights not expressly granted are reserved.
# *************************************************************************************************************/
#
# Interoperability/Conformance Script V.0.6.3
#
# Changelog:
# Changelog moved to Readme
import argparse
import configparser
import datetime
import hashlib
import io
import itertools
import logging
import math
import os
import pathlib
import re
import shlex
import shutil
import struct
import subprocess
import sys
import wave
import zipfile
import filecmp
from concurrent.futures import ThreadPoolExecutor
try:
import numpy
except ImportError:
sys.exit('Numpy missing! Try running "pip3 install numpy".')
VERSION = '0.6.3'
LICENSE = '******************************************************************************************************************\n' \
'* Low Complexity Communication Codec - LC3 Conformance Interoperability Test Software Release V1.0.8 2024/07/01 *\n' \
'* *\n' \
'* (C) 2021 Copyright Ericsson AB and Fraunhofer Gesellschaft zur Foerderung *\n' \
'* der angewandten Forschung e.V. for its Fraunhofer IIS. *\n' \
'* *\n' \
'* This software and/or program is protected by copyright law and international *\n' \
'* treaties and shall solely be used as set out in the *\n' \
'* BLUETOOTH SPECIAL INTEREST GROUP LC3 CONFORMANCE INTEROPERABILTITY *\n' \
'* TEST SOFTWARE END USER LICENSE AGREEMENT *\n' \
'* (EULA, see https://btprodspecificationrefs.blob.core.windows.net/eula-lc3/Bluetooth-SIG-LC3-EULA.pdf). *\n' \
'* *\n' \
'* No copying, distribution, or use other than as expressly provided in the EULA *\n' \
'* is hereby authorized by implication, estoppel or otherwise. *\n' \
'* All rights not expressly granted are reserved. *\n' \
'* *\n' \
'* Interoperability/Conformance Script V.{} *\n' \
'******************************************************************************************************************\n' \
# constants
MAX_DELAY = 322 # maximum in samples for 7.5ms framing at 44.1 kHz
MAX_SAMPLES_PER_FRAME = 480
SAMPLERATES = [8000, 16000, 24000, 32000, 44100, 48000]
SQAM_URL = 'https://qc.ebu.io/testmaterial/523/1/download/'
SQAM_SHA256 = '7d6fcd0fc42354637291792534b61bf129612f221f8efef97b62e8942a8686aa'
SOX_URL = 'https://sourceforge.net/projects/sox/files/sox/14.4.2/sox-14.4.2-win32.zip'
SOX_SHA256 = '8072cc147cf1a3b3713b8b97d6844bb9389e211ab9e1101e432193fad6ae6662'
SOX_EXE = pathlib.Path('SoX', 'sox-14.4.2', 'sox.exe')
RMS_EXE = './rms' if sys.platform != 'cygwin' else './rms.exe'
REFERENCE_ENCODER = 'LC3_bin_current/LC3.exe -E -q {options} "{input}" "{output}" {bitrate}'
REFERENCE_DECODER = 'LC3_bin_current/LC3.exe -D -q {options} "{input}" "{output}"'
ITEM_DIR = pathlib.Path('test_items')
ITEMS = { # start, frag, SQAM name
'ABBA': (7, 8, '69.flac'),
'Castanets': (0, 8, '27.flac'),
'Eddie_Rabbitt': (0, 8, '70.flac'),
'Female_Speech_German': (0, 8, '53.flac'),
'Glockenspiel': (0, 10, '35.flac'),
'Piano_Schubert': (0, 8, '60.flac'),
'Violoncello': (0, 10, '10.flac'),
'Harpsichord': (39, 9, '40.flac'),
'Male_Speech_English': (0, 8, '50.flac')
}
ITEM_HIGH_PASS = 'White_Noise_HP20'
ITEM_BAND_LIMIT = 'Female_Speech_German'
ITEM_RATE_SWITCHING = 'ABBA'
# Sampling rate, band width, bit rate
BAND_LIMITS_10MS = {
16000: ([8000], 32000),
24000: ([8000, 16000], 48000),
32000: ([8000, 16000, 24000], 64000),
44100: ([8000, 16000, 24000, 32000], 95550),
48000: ([8000, 16000, 24000, 32000], 96000)
}
BAND_LIMITS_75MS = {
16000: ([8000], 32000),
24000: ([8000, 16000], 48000),
32000: ([8000, 16000, 24000], 64000),
44100: ([8000, 16000, 24000, 32000], 95060),
48000: ([8000, 16000, 24000, 32000], 96000)
}
BANDWIDTHS = {8000: 'nb', 16000: 'wb', 24000: 'sswb', 32000: 'swb', 48000: 'fb'}
# config default values
DEFAULTS = {
'configs': [],
'high_pass_eng_threshold': 70,
'metric': ('peaq', 'rms'),
'test_band_limiting ': True,
'test_low_pass': True,
'test_rate_switching': True,
'test_sqam': True,
}
METRICS = {
'encode': 'peaq',
'decode': 'rms',
'encdec': 'peaq'
}
TEST_MODES = ['encode', 'decode', 'encdec', 'rate_switching_enc', 'rate_switching_ff', 'rate_switching_dec']
for mode in TEST_MODES:
DEFAULTS[mode + '_odg_threshold'] = 0.07
DEFAULTS[mode + '_rms_threshold'] = 14
DEFAULTS[mode + '_mad_threshold'] = 0.00148 #1/(2**(14 - 4.6))
# html output stuff
HEADER_ALL = ['Mode', 'Item', 'Samplingrate', 'Bitrate']
HEADER_PEAQ = ['ODG Ref', 'Delta ODG (threshold)']
HEADER_RMS = ['Max. Abs. Diff (threshold)', 'RMS (threshold) [dB]', 'RMS reached (threshold) [bits]']
HEADER_OPT = {'peaqrms':HEADER_PEAQ + HEADER_RMS, 'peaq':HEADER_PEAQ, 'rms': HEADER_RMS}
HEADER_ED = ['Error Patterns match']
HEADER_HP20 = ['Energy (threshold) [dB]']
HEADER_CAP = ['Capabilities confirmed']
HEADER = {
'test_band_limiting' : ('Band-limited signals', HEADER_OPT),
'test_low_pass' : ('Signals above 20kHz', HEADER_HP20),
'test_rate_switching' : ('Bitrate switching', HEADER_OPT),
'test_sqam' : ('SQAM items', HEADER_OPT),
}
HTML_HEAD = ('<!DOCTYPE html><head><title>{title} Report</title><style>{style}</style></head><body>'
'<h2>Conformance test for "{title}" (Frame Size {frame_ms} ms) {state}!</h2>')
HTML_TABLE_HEAD = '<div><table><tr><h3>{title}</h3></tr>\n'
HTML_TABLE_TAIL = '</table></div>'
HTML_TAIL = '</body>'
STYLE = ('body {font-family:sans-serif; color:#f8f8f2; background-color:#272822; font-size:80%} div {border:1px solid '
'#8f908a; border-radius:4px; overflow:hidden; display:table; margin-left:30px; margin-bottom:30px} h2 {text-a'
'lign:left; margin-left:30px} h3 {text-align:left; margin:4px} table {border-spacing:0px} th {padding:4px} td'
' {padding:4px} tr:nth-child(even) {background-color:rgba(255,255,255,0.1)} td.pass {background-color:rgba(0,'
'192,255,0.4)} td.fail {background-color:rgba(255,0,0,0.4)} td.warn {background-color:rgba(214,137,16,0.4)}')
# convenience wrapper for os.makedirs
def makedirs(path):
os.makedirs(str(path), exist_ok=True)
return path
# returns true if path is a file
def is_file(path):
return os.path.isfile(str(path))
# Run command and return output. cmd can be string or list. Commands with .exe suffix are automatically
# called with wine unless wine=False. Set unicode=False to get binary output. Set hard_fail=False to
# to ignore nonzero return codes.
def call(cmd, wine=True, unicode=True, hard_fail=True, log_output=True):
if isinstance(cmd, str):
cmd = [x for x in shlex.split(cmd) if x]
if sys.platform != 'cygwin' and wine and cmd[0].lower().endswith('.exe'):
cmd = ['wine'] + cmd
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=unicode)
out = p.communicate()[0] or (b'', '')[unicode]
quoted_cmd = ' '.join(map(shlex.quote, cmd))
now = datetime.datetime.now().strftime('%H:%M:%S')
logging.debug('[{}] '.format(now) + quoted_cmd)
if unicode and log_output:
logging.debug(out)
if hard_fail and p.returncode != 0:
raise OSError(quoted_cmd + ' failed!')
return out
# return url as as bytes object, validate against hash
def download(url, sha256=None):
try:
buf = call('curl --silent -L "{}"'.format(url), unicode=False)
except OSError:
sys.exit('Failed to download {}!'.format(url))
if sha256 and hashlib.sha256(buf).hexdigest() != sha256:
sys.exit('Failed to validate hash for {}!'.format(url))
return buf
def download_sox():
if not is_file(SOX_EXE):
print('Downloading SoX ...')
buf = download(SOX_URL, SOX_SHA256)
zipfile.ZipFile(io.BytesIO(buf)).extractall(str(SOX_EXE.parent.parent))
if sys.platform == 'cygwin':
call('chmod -R +x "{}"'.format(SOX_EXE.parent))
def exe_exists(exe, wine=False):
try:
out = call(exe, wine=wine, hard_fail=False)
except OSError:
return False
return not (wine and out.startswith('wine: ')) # detect wine: cannot find
def check_system(globvars):
if sys.platform == 'win32':
sys.exit('This script should run under cygwin')
if not exe_exists('curl'):
sys.exit('Curl not found')
if sys.platform != 'cygwin' and not exe_exists('wine'):
sys.exit("Wine not found")
if not exe_exists('gcc'):
sys.exit("Gcc not found")
if not exe_exists(globvars['peaq_bin'], wine=True):
sys.exit('{} not found. \nPlease install a PEAQ compliant tool (e.g. PEAQ ITU-BS.1387) and adjust config file'
.format(globvars['peaq_bin']))
if not exe_exists(globvars['encoder'], wine=True):
sys.exit('{} not found. \nPlease provide the test implementation of the LC3 encoder'
.format(globvars['encoder']))
if not exe_exists(globvars['decoder'], wine=True):
sys.exit('{} not found. \nPlease provide the test implementation of the LC3 decoder'
.format(globvars['decoder']))
def regex_search(expr, s):
if not re.search(expr, s):
sys.exit('No match for regular expression "{}"!'.format(expr))
return re.search(expr, s).group(1)
# calculates the max xcorr of the two vectors
def align_vec(x1, x2):
# trims second vector(tst) to be in sync with first(ref)
res = []
# normalize to max of int16
a = numpy.float32(x1) / 32767
# padd with zeros in beginning
x2 = (0,)*MAX_DELAY + x2
b = numpy.float32(x2) / 32767
for i in range(2*MAX_DELAY + 1):
xlen = min(len(a),len(b)) - i
xx = numpy.dot(a[0:xlen], b[i:xlen+i])
res.append(xx)
lag = numpy.array(res).argmax()
x2 = x2[lag:]
# padd/trim second vector(tst)
logging.debug('[{}] Compensated delay: {} samples'.format(datetime.datetime.now().strftime('%H:%M:%S'), MAX_DELAY - lag))
if len(x1)>len(x2):
x2 = x2 + (0,)*(len(x1)-len(x2))
else:
x2 = x2[:len(x1)]
return x2
# convert byte objects to signed int16
def byte_to_float(b, frames, channels):
return struct.unpack("%ih" % (frames * channels), b)
# trim/padd file_2 to be in sync with file_1 and write to file_2.aligned.wav
def align_files(file_1, file_2, file_2_out):
logging.debug('[{}] File alignment:\nFile to be aligned: "{}"\nReference file: "{}"\nAligned output file: "{}"'.format(datetime.datetime.now().strftime('%H:%M:%S'), file_2,file_1,file_2_out))
file_1, file_2, file_2_out = str(file_1), str(file_2), str(file_2_out)
# read in audio files
with wave.open(file_1, 'rb') as wf1, wave.open(file_2, 'rb') as wf2:
if abs(wf1.getnframes() - wf2.getnframes()) > MAX_SAMPLES_PER_FRAME:
print('The difference between the number of samples in {} and {} is higher than {}'.format(file_1,file_2, MAX_SAMPLES_PER_FRAME))
print('{}: {} samples'.format(file_1,wf1.getnframes()))
print('{}: {} samples'.format(file_2,wf2.getnframes()))
exit()
b1 = wf1.readframes(wf1.getnframes())
b2 = wf2.readframes(wf2.getnframes())
x1 = byte_to_float(b1, wf1.getnframes(), wf1.getnchannels())
x2 = byte_to_float(b2, wf2.getnframes(), wf2.getnchannels())
par2 = wf2.getparams()
# measure cross correlation -> delay between files and return trimmed vector
y2 = align_vec(x1, x2)
# write output file
with wave.open(file_2_out, 'wb') as wf2:
wf2.setparams(par2)
wf2.setnframes(len(y2))
b2 = struct.pack("%ih" % len(y2), *y2)
wf2.writeframes(b2)
def build_tools():
call('gcc rms.c -o rms -lm')
# call sox with args in repeatable mode, lazy skips execution if output already exists
def sox(*args, lazy=False):
wavs = [x for x in map(str, args) if x.endswith('.wav')]
if not (lazy and os.path.isfile(wavs[-1])): # last .wav is assumed to be output
call('{} -R {}'.format(SOX_EXE, ' '.join(map(str, args))))
def resample(infile, outfile, fs, lazy=False):
sox(infile, outfile, 'rate -vs', fs, lazy=lazy)
def low_pass(infile, outfile, fs, fc, lazy=False):
tmpfile = infile.with_suffix('.{}k.wav'.format(fc // 1000))
resample(infile, tmpfile, fc, lazy=lazy)
resample(tmpfile, outfile, fs, lazy=lazy)
# apply func to list of argumets,
def thread_executor(func, args, workers):
list(ThreadPoolExecutor(workers).map(lambda x: func(*x), args)) # list() to collect futures
def prepare_items(workers):
fade_in, fade_out = 0.5, 0.7
sqam_dir = pathlib.Path('SQAM')
item_dir = makedirs(ITEM_DIR)
if not sqam_dir.exists():
print('Downloading test items ...')
buf = download(SQAM_URL, SQAM_SHA256)
zipfile.ZipFile(io.BytesIO(buf)).extractall(str(sqam_dir))
def trim(name, start, end, iname):
infile = sqam_dir / iname
outfile = item_dir / (name + '.wav')
tmpfile = outfile.with_suffix('.tmp.wav')
sox(infile, tmpfile, 'trim', start, end, 'remix -', lazy=True)
wf = wave.open(str(tmpfile))
length = wf.getnframes() / wf.getframerate()
sox(tmpfile, outfile, 'fade', fade_in, length, fade_out, lazy=True)
def resamp(name, fs):
infile = item_dir / (name + '.wav')
outfile = item_dir / '{}_{}.wav'.format(name, fs)
resample(infile, outfile, fs, lazy=True)
def lpass20k(name, fs):
infile = item_dir / '{}_{}.wav'.format(name, fs)
outfile = infile.with_suffix('.lp20.wav')
low_pass(infile, outfile, fs, 40000, lazy=True)
def blimit(fs, br, bw):
infile = item_dir / '{}_{}.wav'.format(ITEM_BAND_LIMIT, fs)
outfile = item_dir / '{}_{}_{}.wav'.format(ITEM_BAND_LIMIT, fs, BANDWIDTHS[bw])
low_pass(infile, outfile, fs, bw, lazy=True)
misc = [
# HP20 item with 4 seconds of white noise above 20kHz
lambda : sox('-n -r 48000 -c 1 -b 16', item_dir / (ITEM_HIGH_PASS + '.wav'), 'synth 4 white fir hp_fir_coef.txt', lazy=True),
# rate switching item
lambda: sox(item_dir / (ITEM_RATE_SWITCHING + '.wav'), item_dir / (ITEM_RATE_SWITCHING + '_16000.wav'), 16000, lazy=True),
]
print('Preparing test items ...')
thread_executor(trim, ((name, st, fr, iname) for name, (st, fr, iname) in ITEMS.items()), workers)
thread_executor(resamp, itertools.product(ITEMS, SAMPLERATES), workers)
thread_executor(lpass20k, itertools.product(ITEMS, (f for f in SAMPLERATES if f >= 44100)), workers)
thread_executor(blimit, ((fs, br, bw) for fs, (bws, br) in BAND_LIMITS_10MS.items() for bw in bws), workers)
thread_executor(lambda x: x(), ([f] for f in misc), workers)
def parse_config(path):
def strip_comment(line):
return line.split('#', 1)[0].strip()
def split_list(line):
return [x.strip() for x in strip_comment(line).split(',')]
def parse_conf_line(line):
try:
mode, fs, br = split_list(line)
if int(fs) not in SAMPLERATES:
sys.exit('Unsupported sampling rate: {}!'.format(fs))
if ':' in br:
br_start, br_step, br_stop = map(int, br.split(':'))
fs, br = int(fs), list(range(br_start, br_stop + 1, br_step))
else:
fs, br = int(fs), [int(br)]
if fs == 8000 and max(br) > 64000:
sys.exit('Narrowband (8 kHz) must use bitrates below 64 kBit/s')
return mode, fs, br
except ValueError:
sys.exit('Syntax error in test config "{}"!'.format(line))
if not os.path.isfile(path):
sys.exit('No such file: ' + path)
glob_keys = ['enabled_tests', 'encoder', 'decoder', 'peaq_bin', 'peaq_odg_regex', 'frame_ms']
bool_keys = ['test_sqam', 'test_band_limiting',
'test_low_pass', 'test_rate_switching']
str_keys = ['options']
globvars, tests = {}, {}
try:
parser = configparser.ConfigParser()
parser.read(path)
# parse global section
for key in glob_keys:
globvars[key] = strip_comment(parser['globals'][key])
globvars['enabled_tests'] = split_list(parser['globals']['enabled_tests'])
for key in parser['globals']:
if key not in glob_keys:
sys.exit('Unknown key "{}" in config'.format(key))
# parse test sections
for test in globvars['enabled_tests']:
tests[test] = DEFAULTS.copy()
for key in parser[test]:
val = strip_comment(parser[test][key])
if key in bool_keys:
tests[test][key] = val == '1'
elif key in str_keys:
tests[test][key] = val
elif key == 'configs':
tests[test]['configs'] = [parse_conf_line(l) for l in parser[test]['configs'].splitlines()]
mode = tests[test]['configs'][0][0]
if set([mode]) != set([l[0] for l in tests[test]['configs']]):
sys.exit('multiple modes in one test not allowed!')
tests[test]['metric'] = METRICS[mode]
elif key.endswith('_threshold') and key in DEFAULTS:
try:
tests[test][key] = float(val)
except ValueError:
sys.exit('Invalid number in config: {} = {}'.format(key, val))
else:
sys.exit('Unknown key "{}" in config'.format(key))
tests[test].update(globvars)
except KeyError as e:
sys.exit('Missing "{}" in config'.format(e.args[0]))
except configparser.DuplicateOptionError as e:
sys.exit('Duplicate key "{}" in config'.format(e.args[1]))
return globvars, tests
def compare_wav_energy(infile, reference, test, config, *_):
eng = calc_energy(test)
thresh = DEFAULTS['high_pass_eng_threshold']
ok = eng <= thresh
return ok, [(eng, ('fail', 'pass')[ok], thresh)]
def check_odg(mode, config, odg_ref, odg_tst, odg_thr_key):
odg_diff = abs(odg_ref - odg_tst)
odg_diff_thr = config[mode + '_odg_threshold']
ok = odg_diff <= odg_diff_thr
result = [(odg_ref, '', None), (odg_diff, ('fail', 'pass')[ok], odg_diff_thr)]
if odg_thr_key:
odg_thr = config[odg_thr_key]
result.append((odg_tst, ('warn', 'none')[odg_tst >= odg_thr], odg_thr))
return ok, result
def check_rms(mode, config, rms, bits, diff):
rms_bits = config[mode + '_rms_threshold']
rms_thr = 20 * math.log10(2 ** (-rms_bits + 1) / 12 ** 0.5)
diff_thr = config[mode + '_mad_threshold']
ok_rms, ok_diff = rms <= rms_thr, diff <= diff_thr
result = [(diff, ('fail', 'pass')[ok_diff], diff_thr),
(rms, ('fail', 'pass')[ok_rms], rms_thr),
(bits, ('warn', 'none')[bits >= rms_bits], rms_bits)]
return ok_rms and ok_diff, result
def compare_wav(infile, reference, test, config, rms_thr_key, odg_thr_key=None):
ok_peaq, result_peaq = False, []
ok_rms, result_rms = False, []
ref_al = reference.with_suffix('.aligned.wav')
tst_al = test.with_suffix('.aligned.wav')
align_files(infile, reference, ref_al)
align_files(infile, test, tst_al)
if 'peaq' in config['metric']:
in48 = infile.with_suffix('.48k.wav')
ref48 = ref_al.with_suffix('.48k.wav')
tst48 = tst_al.with_suffix('.48k.wav')
resample(infile, in48, 48000)
resample(ref_al, ref48, 48000)
resample(tst_al, tst48, 48000)
# calculate odg between input and reference / test
out_ref = call(config['peaq_bin'].format(reference=in48, test=ref48))
odg_ref = float(regex_search(config['peaq_odg_regex'], out_ref))
out_tst = call(config['peaq_bin'].format(reference=in48, test=tst48))
odg_tst = float(regex_search(config['peaq_odg_regex'], out_tst))
ok_peaq, result_peaq = check_odg(mode, config, odg_ref, odg_tst, odg_thr_key)
if 'rms' in config['metric']:
# calculate rms between reference and test
out = call('{} {} {} {}'.format(RMS_EXE, ref_al, tst_al, config[rms_thr_key]))
diff_samp = int(regex_search(r'different samples\s+: (\d+)', out))
if diff_samp != 0:
rms = float(regex_search(r'Overall RMS value\s+: (\S+) dB ---', out))
diff = float(regex_search(r'Maximum difference\s+: (\S+) ---', out))
bits = int(regex_search(r'RMS criteria\s+: (\d+) bit', out))
else:
rms, diff, bits = -999, 0, 24
ok_rms, result_rms = check_rms(mode, config, rms, bits, diff)
return ok_peaq or ok_rms, result_peaq + result_rms
# create file names for test
def make_files(files, work_dir, test, mode, item, fs, br):
test_dir = makedirs(work_dir / test)
protoyp = '{}_{}_{}_{}_'.format(mode, item, fs, br)
return tuple(test_dir / (protoyp + f) for f in files)
# permutate test configs
def sqam_configs(config, items=ITEMS, lp20=False, bitrates=None, modes=None):
for mode, fs, brs in config['configs']:
if modes and mode not in modes:
continue
for item, br in itertools.product(items, bitrates or brs):
infile = ITEM_DIR / '{}_{}.wav'.format(item, fs)
if fs in (44100, 48000) and lp20:
infile = infile.with_suffix('.lp20.wav')
yield mode, item, fs, br, infile
def print_test(test, item, fs, br):
print('Testing', test, '-', item, 'at', fs, 'Hz and', br, 'bit/s ...')
# apply test func to list of tests, multithreadded
def test_executor(func, tests, workers):
return {cfg:res for cfg, res in ThreadPoolExecutor(workers).map(lambda x: func(*x), tests)}
def test_rate_switching(work_dir, test, config, workers):
def func(mode, item, fs, br, infile):
if mode == 'encode':
print_test('bitrate switching ' + mode, item, fs, "swf_encoder.dat")
cfg = ('rate_switching_enc', item, fs, br)
file_names = ['tst.bin', 'ref.bin', 'ref_ref.wav', 'tst_ref.wav']
tst_bin, ref_bin, ref_ref, tst_ref = make_files(file_names, work_dir, test, *cfg)
call(config['encoder'].format(input=infile, output=tst_bin, bitrate='swf_encoder.dat', frame_ms=config['frame_ms'], options=''))
call(REFERENCE_ENCODER.format(input=infile, output=ref_bin, bitrate='swf_encoder.dat', options='-frame_ms ' + config['frame_ms']))
call(REFERENCE_DECODER.format(input=tst_bin, output=tst_ref, options=''))
call(REFERENCE_DECODER.format(input=ref_bin, output=ref_ref, options=''))
return cfg, compare_wav(infile, ref_ref, tst_ref, config, 'rate_switching_enc_rms_threshold')
if mode == 'decode':
print_test('bitrate switching ' + mode, item, fs, "swf_decoder.dat")
cfg = ('rate_switching_dec', item, fs, br)
file_names = ['ref.bin', 'ref_ref.wav', 'ref_tst.wav']
ref_bin, ref_ref, ref_tst = make_files(file_names, work_dir, test, *cfg)
call(REFERENCE_ENCODER.format(input=infile, output=ref_bin, bitrate='swf_decoder.dat', options='-frame_ms ' + config['frame_ms']))
call(REFERENCE_DECODER.format(input=ref_bin, output=ref_ref, options=''))
call(config['decoder'].format(input=ref_bin, output=ref_tst, options=''))
return cfg, compare_wav(infile, ref_ref, ref_tst, config, 'rate_switching_dec_rms_threshold')
tests = sqam_configs(config, modes=['encode', 'decode'], bitrates=['NA'])
result = test_executor(func, tests, workers)
fs, br = 16000, 'NA'
print_test('bitrate switching (first frame)', ITEM_RATE_SWITCHING, fs, br)
infile = ITEM_DIR / '{}_{}.wav'.format(ITEM_RATE_SWITCHING, fs)
cfg = ('rate_switching_ff', ITEM_RATE_SWITCHING, fs, br)
file_names = ['ref.bin', 'tst.bin', 'ref_ref.wav', 'tst_tst.wav']
ref_bin, tst_bin, ref_ref, tst_tst = make_files(file_names, work_dir, test, *cfg)
call(config['encoder'].format(input=infile, output=tst_bin, bitrate='swfFirstFrame.dat', frame_ms=config['frame_ms'], options=''))
call(config['decoder'].format(input=tst_bin, output=tst_tst, options=''))
call(config['encoder'].format(input=infile, output=ref_bin, bitrate=16000, frame_ms=config['frame_ms'], options=''))
call(config['decoder'].format(input=tst_bin, output=ref_ref, options=''))
result[cfg] = compare_wav(infile, ref_ref, tst_tst, config, 'rate_switching_ff_rms_threshold')
return result
def test_item(work_dir, test, config, infile, mode, item, fs, br, compare=compare_wav):
file_names = ['ref.bin', 'tst.bin', 'ref_ref.wav', 'tst_tst.wav', 'ref_tst.wav', 'tst_ref.wav']
file_tuple = make_files(file_names, work_dir, test, mode, item, fs, br)
ref_bin, tst_bin, ref_ref, tst_tst, ref_tst, tst_ref = file_tuple
call(REFERENCE_ENCODER.format(input=infile, output=ref_bin, bitrate=br, options='-frame_ms ' + config['frame_ms']))
call(REFERENCE_DECODER.format(input=ref_bin, output=ref_ref, options=''))
if mode.startswith('encode'):
call(config['encoder'].format(input=infile, output=tst_bin, bitrate=br, frame_ms=config['frame_ms'], options=''))
call(REFERENCE_DECODER.format(input=tst_bin, output=tst_ref, options=''))
return compare(infile, ref_ref, tst_ref, config, 'encode_rms_threshold')
if mode.startswith('decode'):
call(config['decoder'].format(input=ref_bin, output=ref_tst, options=''))
return compare(infile, ref_ref, ref_tst, config, 'decode_rms_threshold')
if mode.startswith('encdec'):
call(config['encoder'].format(input=infile, output=tst_bin, bitrate=br, frame_ms=config['frame_ms'], options=''))
call(config['decoder'].format(input=tst_bin, output=tst_tst, options=''))
return compare(infile, ref_ref, tst_tst, config, 'encdec_rms_threshold')
def test_sqam(work_dir, test, config, workers):
def func(mode, item, fs, br, infile):
print_test('sqam ' + mode, item, fs, br)
cfg = (mode, item, fs, br)
return cfg, test_item(work_dir, test, config, infile, *cfg)
return test_executor(func, sqam_configs(config, lp20=True), workers)
def test_low_pass(work_dir, test, config, workers):
if set(mode for mode, *_ in config['configs']) & {'encode', 'encdec'}:
fs, br = 48000, 96000
print_test('high pass filter', ITEM_HIGH_PASS, fs, br)
cfg = ('encode', ITEM_HIGH_PASS, fs, br)
infile = ITEM_DIR / (ITEM_HIGH_PASS + '.wav')
res = test_item(work_dir, test, config, infile, *cfg, compare=compare_wav_energy)
return {cfg:res}
return {}
def test_band_limiting(work_dir, test, config, workers):
infile = ITEM_DIR / (ITEM_BAND_LIMIT + '.wav')
def func(fs, br, bw):
item = '{}_{}_{}'.format(ITEM_BAND_LIMIT, bw, BANDWIDTHS[bw])
item_in = ITEM_DIR / '{}_{}_{}.wav'.format(ITEM_BAND_LIMIT, fs, BANDWIDTHS[bw])
print_test('bandwidth detector (encoder)', item, fs, br)
cfg = ('encoder', item, fs, br)
file_names = ['tst.bin', 'ref.bin', 'ref_ref.wav', 'tst_ref.wav']
tst_bin, ref_bin, ref_ref, tst_ref = make_files(file_names, work_dir, test, *cfg)
call(config['encoder'].format(input=item_in, output=tst_bin, bitrate=br, frame_ms=config['frame_ms'], options=''))
call(REFERENCE_ENCODER.format(input=item_in, output=ref_bin, bitrate=br, options='-frame_ms ' + config['frame_ms']))
call(REFERENCE_DECODER.format(input=tst_bin, output=tst_ref, options=''))
call(REFERENCE_DECODER.format(input=ref_bin, output=ref_ref, options=''))
return cfg, compare_wav(item_in, ref_ref, tst_ref, config, 'encode_rms_threshold')
rates = set(fs for mode, fs, _ in config['configs'] if mode in ('encode', 'encdec'))
if config['frame_ms'] == '10':
bandlimits_array = BAND_LIMITS_10MS
else:
bandlimits_array = BAND_LIMITS_75MS
tests = [(fs, br, bw) for fs, (bws, br) in bandlimits_array.items() for bw in bws if fs in rates]
return test_executor(func, tests, workers)
def calc_energy(test):
with wave.open(str(test), 'rb') as tst_wf:
bytes_tst = tst_wf.readframes(tst_wf.getnframes())
tst = byte_to_float(bytes_tst, tst_wf.getnframes(), tst_wf.getnchannels())
eng = sum(numpy.square(tst))
return 10 * math.log10(eng)
def check_results(results):
return all(ok for test in results for ok, _ in results[test].values())
def fstr(x, d_num):
# converts float to string in format 0.00000XXX wihtout scientific notation
# d_num is the number of non-zero digits (number of X in 0.00000XXX)
if type(x) == float:
if x == 0:
return '0'
if abs(x) < 1:
e=int(numpy.floor(numpy.log10(abs(x))))
fff='{{:.{}f}}'.format(-e+d_num-1)
out = fff.format(x)
while out[-1] == '0':
out = out[:-1]
if abs(x) > 1:
fff='{{:.{}f}}'.format(d_num)
out = fff.format(x)
if out[-3:] == '.00':
out = out[:-3]
if out[-2:] == '.0':
out = out[:-2]
if out[-1:] == '0' and out[-3] == '.':
out = out[:-1]
return out
else:
return str(x)
def table_stats(results, header):
header = header[len(HEADER_ALL):] # extract column values
passed = round(100 * sum(ok for ok, _ in results.values()) / (len(results) or 1))
head = ' - {}%'.format(passed)
stats = ['worst value'] + [''] * (len(HEADER_ALL) - 1)
if header in (HEADER_PEAQ + HEADER_RMS, HEADER_PEAQ):
stats += ['', fstr(max(x[1][1][0] for x in results.values()),3)]
if header in (HEADER_PEAQ + HEADER_RMS, HEADER_RMS):
pos = 0 if header == HEADER_RMS else 2
stats += [fstr(max(x[1][pos + 0][0] for x in results.values()),3)]
stats += [fstr(max(x[1][pos + 1][0] for x in results.values()),3)]
stats += [ str(min(x[1][pos + 2][0] for x in results.values()))]
return head, stats
def write_table(htmlfile, results, config, title, header):
header = HEADER_ALL + (header if type(header) == list else header[''.join(config['metric'])])
percent, stats = table_stats(results, header)
htmlfile.write(HTML_TABLE_HEAD.format(title=title + percent))
htmlfile.write('<tr>' + ''.join('<th>{}</th>'.format(x) for x in header) + '</tr>\n')
htmlfile.write('<tr>' + ''.join('<td>{}</td>'.format(x) for x in stats) + '</tr>\n')
for conf, (_, result) in sorted(results.items()):
htmlfile.write('<tr>' + ''.join('<td>{}</td>'.format(x) for x in conf))
for value, clazz, thresh in result:
thresh = ' ({})'.format(fstr(thresh,3)) if thresh != None else ''
htmlfile.write('<td class={}>{}{}</td>'.format(clazz, fstr(value,3), thresh))
htmlfile.write('</tr>\n')
htmlfile.write(HTML_TABLE_TAIL)
def save_html(results, path, title, config):
state = 'passed' if check_results(results) else 'failed'
with open(path, 'w') as htmlfile:
htmlfile.write(HTML_HEAD.format(title=title, style=STYLE, state=state, frame_ms=config['frame_ms']))
for mode in sorted(results):
name, header = HEADER[mode]
write_table(htmlfile, results[mode], config, name, header)
htmlfile.write(HTML_TAIL)
def is_valid_mode(mode, config):
enc_test_modes = ['test_band_limiting', 'test_low_pass']
dec_test_modes = []
enc_modes = set(m[0] for m in config['configs'])
check1 = enc_modes == {'encode'} and mode in dec_test_modes
check2 = enc_modes == {'decode'} and mode in enc_test_modes
return config[mode] and not check1 and not check2
def init_logging(verbose):
time_stamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')
logfile = 'conformanceCheck_{}.log'.format(time_stamp)
file_handler = logging.FileHandler(filename=str(logfile))
file_handler.setLevel(logging.DEBUG)
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(logging.DEBUG if verbose else logging.WARNING)
handlers = [file_handler, stdout_handler]
formater = '[%(levelname)s] %(message)s'
logging.basicConfig(level=logging.DEBUG, handlers=handlers, format=formater)
def printLicense():
print(LICENSE.format(VERSION))
def main():
parser = argparse.ArgumentParser(description='Low Complexity Communication Codec - LC3: \n'
'Conformance Interoperability Test Software\n'
'Interoperability/Conformance Script V.{}\n'.format(VERSION))
parser.add_argument('-v', action="store_true", dest='verbose', help='Activate verbose output')
parser.add_argument('-w', dest='workers', type=int, default=os.cpu_count(), help='Number of worker threads')
parser.add_argument('-keep', action="store_true", dest='keep_files', help='Keep all files (+log) produced in the test run')
parser.add_argument('config', help='Conformance config file')
parser.add_argument('-system_sox', action='store_true', help='Use system sox')
args = parser.parse_args()
global SOX_EXE
if args.system_sox:
SOX_EXE = 'sox'
time_stamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')
work_dir = makedirs(pathlib.Path('lc3_conformance_' + time_stamp))
try:
printLicense()
init_logging(args.verbose)
globvars, tests = parse_config(args.config)
check_system(globvars)
build_tools()
if not args.system_sox:
download_sox()
prepare_items(args.workers)
test_modes = {'test_band_limiting': test_band_limiting, 'test_low_pass': test_low_pass,
'test_rate_switching': test_rate_switching,
'test_sqam': test_sqam}
for test, config in tests.items():
results = {}
for mode in sorted(test_modes):
if is_valid_mode(mode, config):
results[mode] = test_modes[mode](work_dir, test, config, args.workers)
if not results:
sys.exit('Please select at least one test in the configuration file or adjust the modes (enc, dec, encdec) to match the respective tests.')
save_html(results, '{}_{}.html'.format(test, time_stamp), test, config)
state = 'passed' if check_results(results) else 'failed'
print('\nConformance test for "{}" (Frame Size {} ms) {}!\n'.format(test, config['frame_ms'], state))
print('For detailed results see {}_{}.html\n'.format(test, time_stamp))
finally:
logging.shutdown()
if not args.keep_files:
shutil.rmtree(str(work_dir), ignore_errors=True)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('\rExiting. Please wait while workers shut down ...')
except OSError as E:
sys.exit('Subprocess failed! See log for details. ' + str(E))
except FileNotFoundError as E:
sys.exit(E.strerror)

View File

@@ -0,0 +1,151 @@
-0.00010968676843379478
0.00005431917347608037
-0.000051082511104212565
0.000033090058654987615
0.0000012180962877078343
-0.000050576591551987379
0.00011033530292350572
-0.00017237523647613498
0.00022572834402419191
-0.00025788419566593593
0.00025671619995357555
-0.00021280762191889079
0.00012182736622123847
0.000013487353119705741
-0.00018215664717268618
0.00036490349565818851
-0.00053560148118214298
0.00066406374228566067
-0.00072025997213799456
0.00067927695399019221
-0.0005263950667558821
0.00026153149035769001
0.000097942005694032721
-0.00051650232065528342
0.000942595754798013
-0.0013137275915458288
0.0015641899881759774
-0.0016345716111797026
0.0014818334806956934
-0.0010886259472978989
0.00047035415058114605
0.00032167684607813617
-0.0012022997837112142
0.0020594707818591966
-0.0027661838280301104
0.0031975759999191252
-0.0032484445454629431
0.0028522646683416209
-0.0019960810890632245
0.00073046876818530927
0.00082846247389389209
-0.0025061131548799551
0.0040857323899662762
-0.0053329618598611031
0.0060261733592492624
-0.0059891074015557709
0.0051215685380124705
-0.0034237006125660942
0.0010097126897021665
0.001892218397952388
-0.0049552781940841346
0.0077847269522773
-0.0099613293463695074
0.011093684838798704
-0.010873497764854731
0.0091270026954435104
-0.005855871880794724
0.0012612069012025036
0.0042539309194091907
-0.010105894743346817
0.015575403675176167
-0.019872766077169198
0.022217486664870837
-0.021924275142074089
0.018486004667595462
-0.011643923007918394
0.001436689701241618
0.011778783260454947
-0.027338524876713927
0.044314760357622829
-0.061588805909313329
0.077946094978857627
-0.09218479590408693
0.10322648981023855
-0.11021761019920319
0.11261081967126403
-0.11021761019920319
0.10322648981023855
-0.09218479590408693
0.077946094978857627
-0.061588805909313329
0.044314760357622829
-0.027338524876713927
0.011778783260454947
0.001436689701241618
-0.011643923007918394
0.018486004667595462
-0.021924275142074089
0.022217486664870837
-0.019872766077169198
0.015575403675176167
-0.010105894743346817
0.0042539309194091907
0.0012612069012025036
-0.005855871880794724
0.0091270026954435104
-0.010873497764854731
0.011093684838798704
-0.0099613293463695074
0.0077847269522773
-0.0049552781940841346
0.001892218397952388
0.0010097126897021665
-0.0034237006125660942
0.0051215685380124705
-0.0059891074015557709
0.0060261733592492624
-0.0053329618598611031
0.0040857323899662762
-0.0025061131548799551
0.00082846247389389209
0.00073046876818530927
-0.0019960810890632245
0.0028522646683416209
-0.0032484445454629431
0.0031975759999191252
-0.0027661838280301104
0.0020594707818591966
-0.0012022997837112142
0.00032167684607813617
0.00047035415058114605
-0.0010886259472978989
0.0014818334806956934
-0.0016345716111797026
0.0015641899881759774
-0.0013137275915458288
0.000942595754798013
-0.00051650232065528342
0.000097942005694032721
0.00026153149035769001
-0.0005263950667558821
0.00067927695399019221
-0.00072025997213799456
0.00066406374228566067
-0.00053560148118214298
0.00036490349565818851
-0.00018215664717268618
0.000013487353119705741
0.00012182736622123847
-0.00021280762191889079
0.00025671619995357555
-0.00025788419566593593
0.00022572834402419191
-0.00017237523647613498
0.00011033530292350572
-0.000050576591551987379
0.0000012180962877078343
0.000033090058654987615
-0.000051082511104212565
0.00005431917347608037
-0.00010968676843379478

View File

@@ -0,0 +1,363 @@
/****************************************************************************************************************
* Low Complexity Communication Codec - LC3 Conformance Interoperability Test Software Release V1.0.5 2021/10/01 *
* *
* (C) 2021 Copyright Ericsson AB and Fraunhofer Gesellschaft zur Foerderung *
* der angewandten Forschung e.V. for its Fraunhofer IIS. *
* *
* This software and/or program is protected by copyright law and international *
* treaties and shall solely be used as set out in the *
* BLUETOOTH SPECIAL INTEREST GROUP LC3 CONFORMANCE INTEROPERABILTITY *
* TEST SOFTWARE END USER LICENSE AGREEMENT *
* (EULA, see https://btprodspecificationrefs.blob.core.windows.net/eula-lc3/Bluetooth-SIG-LC3-EULA.pdf) *
* *
* No copying, distribution, or use other than as expressly provided in the EULA *
* is hereby authorized by implication, estoppel or otherwise. *
* All rights not expressly granted are reserved. *
****************************************************************************************************************/
#include "tinywavein_c.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <float.h>
#include <math.h>
/* Global defines */
/* K = 16 bit */
#define RMS_MAX_BUF 1024
#define SCALE_16 (1 << 15)
#define SCALE_24 (1 << 23)
#define MAX_DIFF 0.000061035
#define MAX_RMS -101.1008
#define SEGMENT_LENGTH 320
#define SSNR_LOW_THR -50.0
#define SSNR_HIGH_THR -15.0
#define MAX_ABS_DIFF_BLUETOOTH 0.00148f
/* Function declarations */
static void printResult(char *inputFilename1, char *inputFilename2, int totalSamples1, float diffMax, float rms, float ssnr, int segmentLength, int differentSamples, float maxDiffThr, float maxRmsThr);
static void calculateRms(char *inputFilename1, char *inputFilename2, int *differentSamples, int *totalSamples1, int *totalSamples2, double *rmsOut, float *maxDiffOut);
static void calculateSegmentalSnr(char *inputFilename1, char *inputFilename2, float *ssnrOut);
static int checkRmsReached(float rms);
static void printUsage(void);
static void calculateThr(int k, float *maxDiffThr, float *maxRmsThr);
int main(int ac, char *av[])
{
char *inputFilename1 = NULL, *inputFilename2 = NULL;
int totalSamples1 = 0, totalSamples2 = 0, differentSamples = 0, k = 0;
float diffMax = 0, ssnr = 0, maxDiffThr = 0, maxRmsThr = 0;
double rms = 0;
if(ac < 3)
{
printf(" Not enough input arguments!\n");
printUsage();
}
if(ac == 4)
{
k = atoi(av[3]);
if(k <= 0 || k > 16)
{
printf(" Parameter k has to be in range of 1 ... 16!\n");
exit(1);
} else {
printf(" Using k = %d as threshold!\n", k);
}
}
if (ac > 4)
{
printf(" Too many input arguments!\n\n");
printUsage();
}
inputFilename1 = av[1]; /* Reference */
inputFilename2 = av[2]; /* Codec under test */
if(k != 0)
{
calculateThr(k, &maxDiffThr, &maxRmsThr);
} else {
maxDiffThr = MAX_DIFF;
maxRmsThr = MAX_RMS;
}
calculateRms(inputFilename1, inputFilename2, &differentSamples, &totalSamples1, &totalSamples2, &rms, &diffMax);
calculateSegmentalSnr(inputFilename1, inputFilename2, &ssnr);
printResult(inputFilename1, inputFilename2, totalSamples1, diffMax, rms, ssnr, SEGMENT_LENGTH, differentSamples, maxDiffThr, maxRmsThr);
return 0;
}
void calculateThr(int k, float *maxDiffThr, float *maxRmsThr)
{
*maxDiffThr = 1 / (pow(2, (k - 2)));
if (k == 14)
{
*maxDiffThr = MAX_ABS_DIFF_BLUETOOTH;
}
*maxRmsThr = 20 * log10(pow(2, -(k - 1)) / sqrt(12));
}
void printResult(char *inputFilename1, char *inputFilename2, int totalSamples1, float diffMax, float rms, float ssnr, int segmentLength, int differentSamples, float maxDiffThr, float maxRmsThr)
{
char *maxDiffReached, *maxRmsReached;
char *rmsFail, *diffFail;
int rmsReached = 0;
if(diffMax < maxDiffThr)
{
maxDiffReached = "REACHED";
diffFail = "PASSED";
} else {
maxDiffReached = "NOT REACHED";
diffFail = "FAILED";
}
if(rms < maxRmsThr)
{
maxRmsReached = "REACHED";
rmsFail = "PASSED";
} else {
maxRmsReached = "NOT REACHED";
rmsFail = "FAILED";
}
printf("Comparing files: %s and %s \n\n", inputFilename1, inputFilename2);
printf(" Number of samples compared : %d\n", totalSamples1);
printf(" Number of different samples : %d\n", differentSamples);
if(differentSamples == 0)
{
printf("\n Input files match exactly!\n\n");
}
if(differentSamples != 0)
{
printf(" Maximum difference : %e ---- %s ---- (threshold for 16-bit resolution is %e)\n", diffMax, maxDiffReached, maxDiffThr);
printf(" Overall RMS value : %f dB ---- %s ---- (threshold for 16-bit resolution is %f dB)\n", rms, maxRmsReached, maxRmsThr);
printf(" Average SNR value : %f dB (%d samples per segment)\n\n", ssnr, segmentLength);
printf("---- Test on RMS criteria : %s\n", rmsFail);
printf("---- Test on max. abs. diff criteria : %s\n", diffFail);
rmsReached = checkRmsReached(rms);
printf("---- Reached RMS criteria : %d bit\n", rmsReached);
}
}
void calculateRms(char *inputFilename1, char *inputFilename2, int *differentSamples, int *totalSamples1, int *totalSamples2, double *rmsOut, float *maxDiffOut)
{
/* Calculate RMS */
int nSamples1 = 0, nSamples2 = 0, scale = 0, i;
unsigned int sampleRate1 = 0, sampleRate2 = 0, nLength1 = 0, nLength2 = 0, nSamplesRead1 = 0, nSamplesRead2 = 0;
float sample_buf1_scaled[RMS_MAX_BUF], sample_buf2_scaled[RMS_MAX_BUF];
short sample_buf1[RMS_MAX_BUF], sample_buf2[RMS_MAX_BUF];
WAVEFILEIN *in_file1, *in_file2;
short nChannels1 = 0, nChannels2 = 0, bipsIn1 = 0, bipsIn2 = 0;
float diffMax = 0.0, rms = 0.0;
in_file1 = OpenWav(inputFilename1, &sampleRate1, &nChannels1, &nLength1, &bipsIn1);
in_file2 = OpenWav(inputFilename2, &sampleRate2, &nChannels2, &nLength2, &bipsIn2);
if(in_file1 == NULL || in_file2 == NULL)
{
printf("Error opening wave files!\n");
exit(1);
}
if(bipsIn1 == 16)
{
scale = SCALE_16;
} else if (bipsIn1 == 24)
{
scale = SCALE_24;
} else {
printf("Bits per sample of input files is not supported!\n");
exit(1);
}
nSamples1 = sampleRate1 / 100;
nSamples2 = sampleRate2 / 100;
while(1)
{
nSamplesRead1 = 0;
nSamplesRead2 = 0;
ReadWavShort(in_file1, sample_buf1, nSamples1 * nChannels1, &nSamplesRead1);
ReadWavShort(in_file2, sample_buf2, nSamples2 * nChannels2, &nSamplesRead2);
for(i = 0; i < (int) nSamplesRead1; i++)
{
sample_buf1_scaled[i] = (float)sample_buf1[i] / scale;
sample_buf2_scaled[i] = (float)sample_buf2[i] / scale;
}
for(i = 0; i < nSamplesRead1; i++)
{
/* Get maximum difference */
if(fabsf((sample_buf1_scaled[i] - sample_buf2_scaled[i])) > diffMax)
{
diffMax = fabsf((sample_buf1_scaled[i] - sample_buf2_scaled[i]));
}
if((sample_buf1_scaled[i] - sample_buf2_scaled[i]) != 0)
{
rms += (sample_buf1_scaled[i] - sample_buf2_scaled[i]) * (sample_buf1_scaled[i] - sample_buf2_scaled[i]);
*differentSamples = *differentSamples + 1;
}
}
if (nSamplesRead1 != (nSamples1 * nChannels1))
{
*totalSamples1 = *totalSamples1 + nSamplesRead1 * nChannels1;
*totalSamples2 = *totalSamples2 + nSamplesRead2 * nChannels2;
break;
}
*totalSamples1 = *totalSamples1 + nSamples1 * nChannels1;
*totalSamples2 = *totalSamples2 + nSamples2 * nChannels2;
}
rms = rms / *totalSamples1;
rms = sqrt(rms);
rms = 20.0 * log10(rms);
*rmsOut = rms;
*maxDiffOut = diffMax;
CloseWavIn(in_file1);
CloseWavIn(in_file2);
}
void calculateSegmentalSnr(char *inputFilename1, char *inputFilename2, float *ssnrOut)
{
float nom = 0, denom = 0, pow1 = 0, ss = 0, ssnr = 0;
int skip = 0;
float sample_buf1_scaled[RMS_MAX_BUF], sample_buf2_scaled[RMS_MAX_BUF];
short sample_buf1[RMS_MAX_BUF], sample_buf2[RMS_MAX_BUF];
WAVEFILEIN *in_file1, *in_file2;
short nChannels1 = 0, nChannels2 = 0, bipsIn1 = 0, bipsIn2 = 0;
int scale = 0, segmentLength = 0, nSegments = 0, i;
unsigned int sampleRate1 = 0, sampleRate2 = 0, nLength1 = 0, nLength2 = 0, nSamplesRead1 = 0, nSamplesRead2 = 0;
in_file1 = OpenWav(inputFilename1, &sampleRate1, &nChannels1, &nLength1, &bipsIn1);
in_file2 = OpenWav(inputFilename2, &sampleRate2, &nChannels2, &nLength2, &bipsIn2);
if(in_file1 == NULL || in_file2 == NULL)
{
printf("Error opening wave files!\n");
exit(1);
}
if(bipsIn1 == 16)
{
scale = SCALE_16;
} else if (bipsIn1 == 24)
{
scale = SCALE_24;
} else {
printf("Bits per sample of input files is not supported!\n");
exit(1);
}
segmentLength = SEGMENT_LENGTH;
while(1)
{
nom = 0;
denom = 0;
skip = 0;
pow1 = 0;
nSamplesRead1 = 0;
nSamplesRead2 = 0;
ReadWavShort(in_file1, sample_buf1, segmentLength, &nSamplesRead1);
ReadWavShort(in_file2, sample_buf2, segmentLength, &nSamplesRead2);
for(i = 0; i < (int) nSamplesRead1; i++)
{
sample_buf1_scaled[i] = (float)sample_buf1[i] / scale;
sample_buf2_scaled[i] = (float)sample_buf2[i] / scale;
}
/* Check if singal power is in the range of [-50 dB ... -15 dB] */
for(i = 0; i < nSamplesRead1; i++)
{
pow1 += (sample_buf1_scaled[i] * sample_buf1_scaled[i]);
}
pow1 = 10 * log10(pow1/ (float) nSamplesRead1);
if(pow1 < SSNR_LOW_THR || pow1 > SSNR_HIGH_THR)
{
skip = 1;
}
if(skip == 0)
{
nSegments++;
for(i = 0; i < nSamplesRead1; i++)
{
nom += sample_buf1_scaled[i] * sample_buf1_scaled[i];
denom += (sample_buf1_scaled[i] - sample_buf2_scaled[i]) * (sample_buf1_scaled[i] - sample_buf2_scaled[i]);
}
denom += (float) nSamplesRead1 * (float) pow(10, -13);
ss = log10(1.0 + nom/denom);
ssnr += ss;
}
if (nSamplesRead1 != (segmentLength * nChannels1))
{
break;
}
}
ssnr = ssnr / (float) nSegments;
ssnr = 10.0 * log10(pow(10, ssnr) - 1.0);
*ssnrOut = ssnr;
CloseWavIn(in_file1);
CloseWavIn(in_file2);
}
int checkRmsReached(float rms)
{
int i;
float currentThr;
for(i = 16; i > 0; i--)
{
currentThr = 20.0 * log10(pow(2, -(i - 1)) / sqrt(12.0));
if(rms < currentThr)
{
break;
}
}
return i;
}
void printUsage(void)
{
printf(" RMS tool to calculate RMS, max. abs. difference and segmental SNR value between to wave files.\n");
printf(" Usage: rms ref.wav test.wav [k]\n");
printf(" The test is done in regards to k = 16, i.e. a 16-bit resolution as default. The user can set k to a range from 1 ... 16 bits. The segment length is set 320 samples.\n");
exit(0);
}

View File

@@ -0,0 +1,860 @@
/****************************************************************************************************************
* Low Complexity Communication Codec - LC3 Conformance Interoperability Test Software Release V1.0.5 2021/10/01 *
* *
* (C) 2021 Copyright Ericsson AB and Fraunhofer Gesellschaft zur Foerderung *
* der angewandten Forschung e.V. for its Fraunhofer IIS. *
* *
* This software and/or program is protected by copyright law and international *
* treaties and shall solely be used as set out in the *
* BLUETOOTH SPECIAL INTEREST GROUP LC3 CONFORMANCE INTEROPERABILTITY *
* TEST SOFTWARE END USER LICENSE AGREEMENT *
* (EULA, see https://btprodspecificationrefs.blob.core.windows.net/eula-lc3/Bluetooth-SIG-LC3-EULA.pdf) *
* *
* No copying, distribution, or use other than as expressly provided in the EULA *
* is hereby authorized by implication, estoppel or otherwise. *
* All rights not expressly granted are reserved. *
****************************************************************************************************************/
#ifndef __TINYWAVEIN_C_H__
#define __TINYWAVEIN_C_H__
/*#define TWI_SUPPORT_BWF*/
/* #define PRINT_HDR */ /* debug functionality to print header info */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/***** Interface *********************************************************/
#ifndef TWI_UINT64
#if ( __STDC_VERSION__ >= 199901L || ( defined(_MSC_VER) &&_MSC_VER >= 1600 ) )
#include <stdint.h>
#define TWI_UINT64 uint64_t
#elif ( defined(_MSC_VER) &&_MSC_VER < 1600 )
typedef unsigned __int64 TWI_UINT64;
#elif ( defined(__arm__) )
#define TWI_UINT64 uint64_t
#else
#error "C99 or later required for 64-bit unsigned integer!"
#endif
#endif
typedef struct WAVEFILEIN WAVEFILEIN;
#ifdef TWI_SUPPORT_BWF
typedef struct WAVEIN_LOUDNESSINFO WAVEIN_LOUDNESSINFO;
#endif
#define __TWI_SUCCESS (0)
#define __TWI_ERROR (-1)
/*!
* \brief Read header from a WAVEfile. Host endianess is handled accordingly.
* \return WAVEFILEIN handle on success and NULL on failure.
*
* This open can cope with RF64 files, it returns samplesInFile == UINT_MAX when fileSize > 4 GB
*/
static WAVEFILEIN* OpenWav(
const char* filename,
unsigned int* samplerate,
short* channels,
unsigned int* samplesInFile,
short* bps
);
/* read normalized 16-bit values in the range +32767..-32768 */
static int ReadWavShort(
WAVEFILEIN* self,
short sampleBuffer[],
unsigned int nSamplesToRead,
unsigned int* nSamplesRead
);
/* read normalized 24-bit values in the range +8388607..-8388608 */
static int ReadWavInt(
WAVEFILEIN* self,
int sampleBuffer[],
unsigned int nSamplesToRead,
unsigned int* nSamplesRead
);
/* read normalized single-precision values in the range +-1.0f */
static int ReadWavFloat(
WAVEFILEIN* self,
float sampleBuffer[],
unsigned int nSamplesToRead,
unsigned int* nSamplesRead
);
#ifdef TWI_SUPPORT_BWF
/* read loudness information from BWF file */
static void ReadBWF(
WAVEFILEIN* self,
WAVEIN_LOUDNESSINFO** wavInLoudness
);
#endif
/* close reader - always call when done reading to free resources */
static int CloseWavIn(WAVEFILEIN* self);
/* reset read pointer to first audio sample */
static int ResetWavIn(WAVEFILEIN* self);
/***** Implementation *********************************************************/
#if defined(__i386__) || defined(_M_IX86) || defined(__x86_64__) || \
defined(_M_X64) || defined(__arm__) || defined(__xtensa__) || defined(__aarch64__) || defined(__EMSCRIPTEN__)
#define __TWI_LE /* _T_iny _W_ave _I_n _L_ittle _E_ndian */
#endif
#if defined(__POWERPC__)
#define __TWI_BE /* _T_iny _W_ave _I_n _B_ig _E_ndian */
#endif
#if !defined(__TWI_LE) && !defined(__TWI_BE)
#error unknown processor
#endif
/*--- local types/structs ----------------------------------*/
#ifdef TWI_SUPPORT_BWF
struct WAVEIN_LOUDNESSINFO {
float loudnessVal;
float loudnessRange;
float maxTruePeakLevel;
float maxMomentaryLoudnes;
float maxShortTermLoudness;
};
#endif
struct WAVEFILEIN {
FILE* theFile;
fpos_t dataChunkPos;
TWI_UINT64 position; /* in pcm samples */
TWI_UINT64 length; /* in pcm samples */
unsigned int bps;
#ifdef TWI_SUPPORT_BWF
WAVEIN_LOUDNESSINFO* loudnessInfo;
#endif
};
typedef struct
{
short formatType; /* WAVE_FORMAT_PCM = 0x0001, etc. */
short channelCount; /* 1 = mono, 2 = stereo, etc. */
unsigned int sampleRate; /* 32000, 44100, 48000, etc. */
unsigned int bytesPerSecond; /* only important for compressed formats */
short blockAlignment; /* container size (in bytes) of one set of samples */
short bitsPerSample; /* valid bits per sample 16, 20 or 24 */
/* short extraFormatBytes ; */
} SWavInfo;
#ifdef TWI_SUPPORT_BWF
typedef struct {
unsigned char description[256];
unsigned char originator[32];
unsigned char originatorReference[32];
unsigned char originatorDate[10]; /* ASCII: <<yyyy:mm:dd>> */
unsigned char originationTime[8]; /* ASCII: <<hh:mm:ss>> */
unsigned int timeReferenceLow;
unsigned int timeReferenceHigh;
unsigned short version;
unsigned char UMID[64]; /* Binary Bytes of SMPTE UMID */
signed short loudnessVal;
signed short loudnessRange;
signed short maxTruePeakLevel;
signed short maxMomentaryLoudnes;
signed short maxShortTermLoudness;
unsigned char Reserved[180];
unsigned char codingHistory; /* ASCII: <<History coding>> */
} SBwfWav;
#endif
typedef struct {
char chunkID[4];
unsigned int chunkSize;
/* long dataOffset ; */ /* never used */
} SChunk;
/* local wrapper, always returns correct endian */
static size_t fread_LE(void *ptr, size_t size, size_t nmemb, FILE *stream);
#ifdef TWI_SUPPORT_BWF
static int __ReadBextChunk( WAVEFILEIN* self, unsigned int* chunkSize );
#endif
static int __ReadDs64Chunk( WAVEFILEIN* self, unsigned int* chunkSize );
#ifdef __TWI_BE
static short BigEndian16(short v);
static int BigEndian32(int v);
/* static TWI_UINT64 BigEndian64(TWI_UINT64); */
#endif
/*!
* \brief Read header from a WAVEfile. Host endianess is handled accordingly.
* \return handle on success and NULL on failure.
*
* This open can cope with RF64 files, it returns samplesInFile == UINT_MAX when fileSize > 4 GB
*/
static WAVEFILEIN* OpenWav(
const char* filename,
unsigned int* samplerate,
short* channels,
unsigned int* samplesInFile,
short* bps
)
{
WAVEFILEIN* self = NULL;
size_t tmp_return_val;
SChunk fmt_chunk, data_chunk;
char tmpchunkID[4];
int offset;
unsigned int dmy;
char tmpFormat[4];
SWavInfo wavinfo = {0, 0, 0, 0, 0, 0};
/* pseudo use to avoid unused symbols */
(void)ReadWavShort;
(void)ReadWavInt;
(void)ReadWavFloat;
#ifdef TWI_SUPPORT_BWF
(void)ReadBWF;
#endif
(void)ResetWavIn;
(void)(tmp_return_val);
/* param check */
if (!filename) goto bail;
if (!samplerate) goto bail;
if (!channels) goto bail;
if (!samplesInFile) goto bail;
if (!bps) goto bail;
self = (WAVEFILEIN*)calloc(1, sizeof(WAVEFILEIN));
if (!self) goto bail; /* return NULL; */
self->theFile = fopen(filename, "rb");
if (!self->theFile) goto bail;
/* read RIFF-chunk */
if (fread(tmpFormat, 1, 4, self->theFile) != 4) {
goto bail;
}
if (strncmp("RIFF", tmpFormat, 4)) {
if (strncmp("RF64", tmpFormat, 4)) {
goto bail;
}
}
/* Read RIFF size. Ignored. */
fread_LE(&dmy, 4, 1, self->theFile);
/* read WAVE-chunk */
if (fread(tmpFormat, 1, 4, self->theFile) != 4) {
goto bail;
}
if (strncmp("WAVE", tmpFormat, 4)) {
goto bail;
}
/* read format/bext-chunk */
if (fread(tmpchunkID, 1, 4, self->theFile) != 4) {
goto bail;
}
/* read Chunk loop */
/* skip some potential chunks up to fmt chunk, but read ds64 and bext */
while ( strncmp("fmt ", tmpchunkID, 4) != 0 ) {
unsigned int chunkSize = 0;
/* stop when we can't read the chunk length */
if (fread_LE(&chunkSize, 1, 4, self->theFile) != 4) {
goto bail;
}
#ifdef TWI_SUPPORT_BWF
if ( strncmp("bext", tmpchunkID, 4) == 0 ) {
int err = __ReadBextChunk( self, &chunkSize );
if (err != __TWI_SUCCESS) goto bail;
}
#endif
if ( strncmp("ds64", tmpchunkID, 4) == 0 ) {
int err = __ReadDs64Chunk( self, &chunkSize );
if (err != __TWI_SUCCESS) goto bail;
}
/* skip remaining chunk data */
while (chunkSize > 0) {
int nulbuf;
if (fread_LE(&nulbuf, 1, 1, self->theFile) != 1) {
goto bail;
}
chunkSize -= 1;
}
/* read next chunk header */
if (fread(tmpchunkID, 1, 4, self->theFile) != 4) {
goto bail;
}
}
/* after the above while() we should now be at the fmt-chunk */
if (fread_LE(&fmt_chunk.chunkSize, 4, 1, self->theFile) != 1) {
goto bail;
}
/* read fmt info */
fread_LE(&(wavinfo.formatType), 2, 1, self->theFile);
fread_LE(&(wavinfo.channelCount), 2, 1, self->theFile);
fread_LE(&(wavinfo.sampleRate), 4, 1, self->theFile);
fread_LE(&(wavinfo.bytesPerSecond), 4, 1, self->theFile);
fread_LE(&(wavinfo.blockAlignment), 2, 1, self->theFile);
fread_LE(&(wavinfo.bitsPerSample), 2, 1, self->theFile);
if (wavinfo.formatType == -2) { // WAVE_FORMAT_EXTENSIBLE
fseek(self->theFile, 8, SEEK_CUR); // skip channel mask
fread_LE(&(wavinfo.formatType), 2, 1, self->theFile); // part of GUID
fseek(self->theFile, 14, SEEK_CUR); // skip rest of GUID
offset = fmt_chunk.chunkSize - 40;
}
else {
offset = fmt_chunk.chunkSize - 16;
}
if (wavinfo.formatType == 0x0001) { // WAVE_FORMAT_PCM
if((wavinfo.bitsPerSample != 16) && (wavinfo.bitsPerSample != 24)) {
/* we do only support 16 and 24 bit PCM audio */
goto bail;
}
}
else if(wavinfo.formatType == 0x0003) { // WAVE_FORMAT_IEEE_FLOAT
if(wavinfo.bitsPerSample != 32) {
/* we do only support 32 bit IEEE float audio */
goto bail;
}
}
else {
/* we support only formatType 0x01 and 0x03 */
goto bail;
}
/* Skip rest of fmt header if any. */
for (; offset > 0; offset--) {
tmp_return_val = fread(&dmy, 1, 1, self->theFile);
}
/* endless chunk reading loop, this loop exits when we reach the "data" chunk or eof */
do {
int tmpChunkSize = 0;
/* Read data chunk ID */
if (fread(data_chunk.chunkID, 1, 4, self->theFile) != 4) {
goto bail;
}
/* Read chunk length */
if (fread_LE(&tmpChunkSize, 4, 1, self->theFile) != 1) {
goto bail;
}
/* Check for data chunk signature. */
if (strncmp("data", data_chunk.chunkID, 4) == 0) {
data_chunk.chunkSize = tmpChunkSize;
break;
}
/* unused 1 byte present, if size is odd */
/* see https://www.daubnet.com/en/file-format-riff */
if ( tmpChunkSize%2 ){
tmpChunkSize++;
}
/* Jump over non data chunk. */
for (;tmpChunkSize > 0; tmpChunkSize--) {
tmp_return_val = fread(&dmy, 1, 1, self->theFile);
}
} while (!feof(self->theFile));
fgetpos(self->theFile, &self->dataChunkPos);
/* we should now be at the data chunk at the start of PCM data */
*samplerate = wavinfo.sampleRate;
*channels = wavinfo.channelCount;
if (data_chunk.chunkSize == 0xffffffff) {
*samplesInFile = 0xffffffff; /* for RF64 we return "-1" */
} else {
*samplesInFile = data_chunk.chunkSize / wavinfo.channelCount;
*samplesInFile /= ((wavinfo.bitsPerSample + 7) / 8);
}
*bps = wavinfo.bitsPerSample;
self->position = 0;
self->bps = wavinfo.bitsPerSample;
if (data_chunk.chunkSize == 0xffffffff) {
if(self->length == 0) {
long dataChunkStart = ftell(self->theFile);
fseek(self->theFile, 0, SEEK_END);
long dataChunkEnd = ftell(self->theFile);
fseek(self->theFile,dataChunkStart, SEEK_SET);
self->length = (dataChunkEnd - dataChunkStart);
} else {
self->length *= wavinfo.channelCount;
}
} else {
self->length = *samplesInFile * wavinfo.channelCount;
}
return self;
bail:
if ( NULL != self ) {
free(self);
}
return NULL;
}
#ifdef TWI_SUPPORT_BWF
static int __ReadBextChunk( WAVEFILEIN* self, unsigned int* chunkSize )
{
unsigned int bextSize = *chunkSize;
self->loudnessInfo = (WAVEIN_LOUDNESSINFO *) calloc(1, sizeof(WAVEIN_LOUDNESSINFO));
if (self->loudnessInfo == NULL) return __TWI_ERROR;
if (bextSize>=602) { /* minimum size bext-data, w/o 'CodingHistory' */
int i;
signed short readBuf=0;
signed int nulbuf=0;
/* first skip all descriptive data */
for(i=0; i<412; i++) {
if (fread_LE(&nulbuf, 1, 1, self->theFile) != 1){
return __TWI_ERROR;
}
bextSize -=1;
}
/* second, read loudness data */
fread_LE(&readBuf, 2, 1, self->theFile);
bextSize -=2;
self->loudnessInfo->loudnessVal = (float)readBuf * 0.01f;
fread_LE(&readBuf, 2, 1, self->theFile);
bextSize -=2;
self->loudnessInfo->loudnessRange = (float)readBuf * 0.01f;
fread_LE(&readBuf, 2, 1, self->theFile);
bextSize -=2;
self->loudnessInfo->maxTruePeakLevel = (float)readBuf * 0.01f;
fread_LE(&readBuf, 2, 1, self->theFile);
bextSize -=2;
self->loudnessInfo->maxMomentaryLoudnes = (float)readBuf * 0.01f;
fread_LE(&readBuf, 2, 1, self->theFile);
bextSize -=2;
self->loudnessInfo->maxShortTermLoudness = (float)readBuf * 0.01f;
/* skip reserved data */
for(i=0; i<180; i++) {
if (fread_LE(&nulbuf, 1, 1, self->theFile) != 1){
return __TWI_ERROR;
}
bextSize -= 1;
}
}
*chunkSize = bextSize;
return __TWI_SUCCESS;
}
static void ReadBWF(
WAVEFILEIN* self,
WAVEIN_LOUDNESSINFO** wavInLoudness
)
{
*wavInLoudness = self->loudnessInfo;
}
#endif
static int __ReadDs64Chunk( WAVEFILEIN* self, unsigned int* chunkSize )
{
unsigned int nulbuf_hi = 0;
unsigned int nulbuf_lo = 0;
int ds64Size = *chunkSize;
if (ds64Size < 28) return __TWI_ERROR;
/* skip RIFF size low+high */
if (fread_LE(&nulbuf_lo, 4, 1, self->theFile) != 1) return __TWI_ERROR;
if (fread_LE(&nulbuf_hi, 4, 1, self->theFile) != 1) return __TWI_ERROR;
ds64Size -= 8;
#ifdef PRINT_HDR
printf("ds64:riff size %li\n", ((TWI_UINT64)nulbuf_hi << 32) + (TWI_UINT64)nulbuf_lo);
#endif
/* skip datasize size low+high */
if (fread_LE(&nulbuf_lo, 4, 1, self->theFile) != 1) return __TWI_ERROR;
if (fread_LE(&nulbuf_hi, 4, 1, self->theFile) != 1) return __TWI_ERROR;
ds64Size -= 8;
#ifdef PRINT_HDR
printf("ds64:data size %li\n", ((TWI_UINT64)nulbuf_hi << 32) + (TWI_UINT64)nulbuf_lo);
#endif
/* read sampleCount */
if (fread_LE(&nulbuf_lo, 4, 1, self->theFile) != 1) return __TWI_ERROR;
if (fread_LE(&nulbuf_hi, 4, 1, self->theFile) != 1) return __TWI_ERROR;
self->length = ((TWI_UINT64)nulbuf_hi << 32) + (TWI_UINT64)nulbuf_lo;
ds64Size -= 8;
#ifdef PRINT_HDR
printf("ds64:sample count %li\n", self->length);
#endif
/* skip tablesize */
if (fread_LE(&nulbuf_lo, 4, 1, self->theFile) != 1) return __TWI_ERROR;
ds64Size -= 4;
#ifdef PRINT_HDR
printf("ds64:table size %i\n", nulbuf_lo);
#endif
/* any table entries are implicitly skipped outside */
if (ds64Size < 0) return __TWI_ERROR;
*chunkSize = ds64Size;
return __TWI_SUCCESS;
}
static int __ReadSample16(
WAVEFILEIN* self,
int* sample,
int scale
)
{
size_t cnt;
short v = 0;
cnt = fread(&v, 2, 1, self->theFile);
if (cnt != 1) {
return __TWI_ERROR;
}
self->position += 1;
#ifdef __TWI_BE
v = BigEndian16(v);
#endif
if ((scale - 16) > 0)
*sample = v << (scale - 16);
else
*sample = v >> (16 - scale);
return __TWI_SUCCESS;
}
static int __ReadSample24(
WAVEFILEIN* self,
int* sample,
int scale
)
{
size_t cnt;
int v = 0;
cnt = fread(&v, 3, 1, self->theFile);
if (cnt != 1) {
return __TWI_ERROR;
}
self->position += 1;
#ifdef __TWI_BE
v = BigEndian32(v);
#endif
if (v >= 0x800000) {
v |= 0xff000000;
}
if ((scale - 24) > 0)
*sample = v << (scale - 24);
else
*sample = v >> (24 - scale);
return __TWI_SUCCESS;
}
static int __ReadSample32(
WAVEFILEIN* self,
float* sample
)
{
size_t cnt;
union fl_int {
float v_float;
int v_int;
};
union fl_int v;
cnt = fread(&v, 4, 1, self->theFile);
if (cnt != 1) {
return __TWI_ERROR;
}
self->position += 1;
#ifdef __TWI_BE
v.v_int = BigEndian32(v.v_int);
#endif
*sample = v.v_float;
return __TWI_SUCCESS;
}
static int __ReadSampleInternal(
WAVEFILEIN* self,
int* sample,
int scale
)
{
int err;
if (!self) {
return __TWI_ERROR;
}
switch (self->bps) {
case 16:
err = __ReadSample16(self, sample, scale);
break;
case 24:
err = __ReadSample24(self, sample, scale);
break;
default:
err = __TWI_ERROR;
break;
}
return err;
}
/* not fully tested */
/* this function returns normalized values in the range +32767..-32768 */
static int ReadWavShort(
WAVEFILEIN* self,
short sampleBuffer[],
unsigned int nSamplesToRead,
unsigned int* nSamplesRead
)
{
unsigned int i;
int err = __TWI_SUCCESS;
if (!sampleBuffer) return __TWI_ERROR;
/* check if we have enough samples left, if not,
set nSamplesToRead to number of samples left. */
if (self->position + nSamplesToRead > self->length) {
nSamplesToRead = self->length - self->position;
}
for (i=0; i< nSamplesToRead; i++) {
if(self->bps == 32)
{
float tmp;
err = __ReadSample32(self, &tmp);
if (err != __TWI_SUCCESS) return err;
sampleBuffer[i] = (int)(tmp * 32768.0f);
}
else
{
int tmp;
err = __ReadSampleInternal(self, &tmp, 16);
if (err != __TWI_SUCCESS) return err;
sampleBuffer[i] = (short)tmp;
}
*nSamplesRead += 1;
}
return __TWI_SUCCESS;
}
/* not fully tested */
/* this function returns normalized values in the range +8388607..-8388608 */
static int ReadWavInt(
WAVEFILEIN* self,
int sampleBuffer[],
unsigned int nSamplesToRead,
unsigned int* nSamplesRead
)
{
unsigned int i;
int err = __TWI_SUCCESS;
if (!sampleBuffer) return __TWI_ERROR;
/* check if we have enough samples left, if not,
set nSamplesToRead to number of samples left. */
if (self->position + nSamplesToRead > self->length) {
nSamplesToRead = self->length - self->position;
}
for (i = 0; i < nSamplesToRead; i++) {
if (self->bps == 32)
{
float tmp;
err = __ReadSample32(self, &tmp);
if (err != __TWI_SUCCESS) return err;
sampleBuffer[i] = (int)(tmp * 8388608.0f);
}
else
{
int tmp;
err = __ReadSampleInternal(self, &tmp, 24);
if (err != __TWI_SUCCESS) return err;
sampleBuffer[i] = tmp;
}
*nSamplesRead += 1;
}
return __TWI_SUCCESS;
}
/* this function returns normalized values in the range +-1.0 */
static int ReadWavFloat(
WAVEFILEIN* self,
float sampleBuffer[],
unsigned int nSamplesToRead,
unsigned int* nSamplesRead
)
{
unsigned i;
int err = __TWI_SUCCESS;
if (!sampleBuffer) return __TWI_ERROR;
/* check if we have enough samples left, if not,
set nSamplesToRead to number of samples left. */
if (self->position + nSamplesToRead > self->length) {
nSamplesToRead = self->length - self->position;
}
for (i=0; i< nSamplesToRead; i++) {
int tmp;
if(self->bps == 32)
{
err = __ReadSample32(self, &sampleBuffer[i]);
if (err != __TWI_SUCCESS) return err;
}
else
{
err = __ReadSampleInternal(self, &tmp, 24);
if (err != __TWI_SUCCESS) return err;
sampleBuffer[i] = (float)tmp / 8388608.0f;
}
*nSamplesRead += 1;
}
return __TWI_SUCCESS;
}
static int CloseWavIn(WAVEFILEIN* self)
{
if (self) {
if (self->theFile) {
fclose(self->theFile);
}
}
free(self);
return __TWI_SUCCESS;
}
static int ResetWavIn(WAVEFILEIN* self)
{
if (self) {
if (self->theFile) {
fsetpos(self->theFile, &self->dataChunkPos);
self->position = 0;
}
}
return __TWI_SUCCESS;
}
/*------------- local subs ----------------*/
static size_t fread_LE(void *ptr, size_t size, size_t nmemb, FILE *stream)
{
#ifdef __TWI_LE
return fread(ptr, size, nmemb, stream);
#endif
#ifdef __TWI_BE
unsigned char x[sizeof(int)];
unsigned char* y = (unsigned char*)ptr;
int i;
int len;
len = fread(x, size, nmemb, stream);
for (i = 0; i < size * nmemb; i++) {
*y++ = x[size * nmemb - i - 1];
}
return len;
#endif
}
#ifdef __TWI_BE
static short BigEndian16(short v)
{
short a = (v & 0x0ff);
short b = (v & 0x0ff00) >> 8;
return a << 8 | b;
}
static int BigEndian32(int v)
{
int a = (v & 0x0ff);
int b = (v & 0x0ff00) >> 8;
int c = (v & 0x0ff0000) >> 16;
int d = (v & 0xff000000) >> 24;
return a << 24 | b << 16 | c << 8 | d;
}
#endif
#endif /* __TINYWAVEIN_C_H__ */

View File

@@ -0,0 +1,153 @@
/*************************************************************************************************************
Low Complexity Communication Codec - LC3 Conformance Interoperability Test Software Release V1.0.3 2021/06/17
(C) 2021 Copyright Ericsson AB and Fraunhofer Gesellschaft zur Foerderung
der angewandten Forschung e.V. for its Fraunhofer IIS.
This software and/or program is protected by copyright law and international
treaties and shall solely be used as set out in the
BLUETOOTH SPECIAL INTEREST GROUP LC3 CONFORMANCE INTEROPERABILTITY
TEST SOFTWARE END USER LICENSE AGREEMENT
(EULA, see https://btprodspecificationrefs.blob.core.windows.net/eula-lc3/Bluetooth-SIG-LC3-EULA.pdf)
No copying, distribution, or use other than as expressly provided in the EULA
is hereby authorized by implication, estoppel or otherwise.
All rights not expressly granted are reserved.
**************************************************************************************************************/
Fixed Point Reference Executable
Encoder Software V1.6.1B
Decoder Software V1.6.1B
Description
-----------
The software uses fixed-point arithmetic utilizing the ITU-T STL2009 including
the latest updates introduced by 3GPP.
Features
--------
- Supported configurations:
8 kHz, 24000 bps (10 ms), 27734 bps (7.5 ms)
16 kHz, 32000 bps (10 ms and 7.5 ms)
24 kHz, 48000 bps (10 ms and 7.5 ms)
32 kHz, 64000 bps (10 ms and 7.5 ms), 61867 bps for 7.5 ms in HFP
44.1 kHz, 79380 bps, 95550 (95060 bps for 7.5 ms) bps and 123480 bps (10 ms and 7.5 ms)
48 kHz, 80000 bps, 96000 bps, 124000 bps (10 ms) and 124800 bps (7.5 ms)
- Frame duration of 10 ms and 7.5 ms
- Multichannel support by multi-mono coding
- Packet loss concealment: Standard
Changelog
---------
- V.1.6.1 2021-06-07
- Core coder
- Removed support of multichannel audio
- Reverted bitstream header to format as in version V1.4.17
- Added description of missing field in bitstream header (signal_len_red)
- Corrected size of signal_len field in bitstream header description
- Updated tinwavein_c.h to most current version
- Robustness fixes based on code fuzzing
- V.1.6.0 2021-05-25
- Core coder
- Updated supported configurations in LC3 Bluetooth reference binary to match
LC3 TS section 4 (Test Cases)
- Removed WMOPS and memory analysis for better performance
- Robustness fixes based on code fuzzing
- Dynamic memory optimizations
Usage
-----
The following example commands explain the usage of the LC3 binary. A
complete list is available by calling ./LC3 -h.
To call encoder and decoder at the same time
./LC3 INPUT.wav OUTPUT.wav BITRATE
To call encoder
./LC3 -E INPUT.wav OUTPUT.bin BITRATE
To call decoder
./LC3 -D INPUT.bin OUTPUT.wav
To specify bitrate switching file instead of fixed bitrate
./LC3 INPUT.wav OUTPUT.wav FILE
where FILE is a binary file containing the bitrate as a
sequence of 64-bit values.
To disable frame counter (quiet mode)
./LC3 -q INPUT.wav OUTPUT.wav BITRATE
To activate verbose mode (print switching commands)
./LC3 -v INPUT.wav OUTPUT.wav BITRATE
To use the G192 bitstream format
./LC3 -E -formatG192 INPUT.wav OUTPUT.g192 BITRATE
./LC3 -D -formatG192 INPUT.g192 OUTPUT.wav
Note that an additional file OUTPUT.cfg will be created by the encoder.
Note that an additional file INPUT.cfg is expected by the decoder.
To explicitly specify the configuration file, the flag -cfgG192
FILE can be used, where FILE is the path to the configuration file.
Note that the same flags (-formatG192 and -cfgG192) shall be used
for encoding as well as for decoding,
To call decoder with frame loss simulations
./LC3 -D -epf <FILE> INPUT.bin OUTPUT.wav
where <FILE> is a binary file containing a sequence of
16-bit values, non-zero values indicating a frame loss
To write error detection pattern (from arithmetic decoder) into a 16-bit binary file
./LC3 -D -edf <FILE> INPUT.bin OUTPUT.wav
where <FILE> is a binary file containing a sequence of
16-bit values, non-zero values indicating a detected frame loss
To set the frame size
./LC3 -frame_ms <FRAME_MS> INPUT.wav OUTPUT.wav BITRATE
where <FRAME_MS> is either 10 or 7.5. The default value is 10 ms.
The parameter bitrate also allows the usage of switching files
instead of a fixed number.
For the bitrate switching, each value in the switching file
represents a frame's bitrate.
Switching files can be created with 'gen_rate_profile' available
from ITU-T G191. If the switching file is shorter than the input it
is looped from the beginning. The switching files contain data
stored as little-endian 64-bit values.
Binary File Format
------------------
Note: The binary file format is intended only for testing purposes. It is
not specified in any other location than in this Readme file and may change in the future.
The file starts with a config header structure as follows.
Field Bytes Content
------------------------------------------------------------
file_id 2 file identifier, value 0xcc1c
header_size 2 total config header size in bytes
samplingrate 2 sampling frequency / 100
bitrate 2 bitrate / 100
channels 2 number of channels
frame_ms 2 frame duration in ms * 100
RFU 2 reserved for future use
signal_len 2 input signal length in samples
signal_len_red 2 signal_len >> 16
All fields are stored in little-endian byte order. The config header could
be extended in the future so readers should seek up to header_size to skip any
unknown fields.
The header is immediately followed by a series of coded audio frames, where
each frame consists of a two-byte frame length information and the current
coded frame.
Note that when reading a bitstream, the LC3 reference binary calculates the signal length as follows:
uint32_t length = (uint32_t)signal_len | ((uint32_t)signal_len_red << 16);

View File

@@ -0,0 +1,64 @@
***************************************************************************************************************
Low Complexity Communication Codec - LC3 Conformance Interoperability Test Software Release V1.0.8 2024/07/01
(C) 2021 Copyright Ericsson AB and Fraunhofer Gesellschaft zur Foerderung
der angewandten Forschung e.V. for its Fraunhofer IIS.
This software and/or program is protected by copyright law and international
treaties and shall solely be used as set out in the
BLUETOOTH SPECIAL INTEREST GROUP LC3 CONFORMANCE INTEROPERABILTITY
TEST SOFTWARE END USER LICENSE AGREEMENT
(EULA, see https://btprodspecificationrefs.blob.core.windows.net/eula-lc3/Bluetooth-SIG-LC3-EULA.pdf)
No copying, distribution, or use other than as expressly provided in the EULA
is hereby authorized by implication, estoppel or otherwise.
All rights not expressly granted are reserved.
**************************************************************************************************************
This package contains the Conformance Interoperability Test Software for the Low Complexity Communication Codec LC3
as described in the LC3 Test Suite LC3.TS.p01.
The following structure outlines the content of this package:
/LC3_Reference_Binary/LC3.exe : LC3 Fixed Point Reference binary for Windows 32-bit
/LC3_Conformance_Interoperability_Script/ : LC3 conformance script (conformanceCheck.py) and helper files
Please refer to the respective Readme files for more information:
/LC3_Reference_Binary/Readme.txt : Readme for LC3 Reference Binary
/LC3_Conformance_Interoperability_Script/ : Readme for LC3 Python Conformance Script
Changelog
---------
- V.1.0.8 2024/07/01
- Fixed link to EBU SQAM test items in LC3 conformance script
- Added new configuration file to cover LC3 HFP Superwideband configuration
- V.1.0.7 2024/03/11
- Added description on usage of gstPEAQ
- V.1.0.6 2022/01/11
- Added optional encoder-decoder chain tests to .cfg files
- V.1.0.5 2021/10/01
- Fixed uninitialized variables in rms.c (required for conformanceCheck.py)
- V.1.0.4 2021/08/20
- Fixes in conformanceCheck.py and configuration files
see conformance script Readme for more information
- V.1.0.3 2021/06/17
- Corrected copyright header (removed software version number(s) from header and placed it below)
- LC3.exe bitexact to V.1.0.2
- V.1.0.2 2021/06/15
- Updates on reference binary readme:
removed obsolete RFU field from bitstream header description
- V.1.0.1 2021/06/07
- Updates on reference binary and readme:
see binary readme for more information
- V.1.0.0 2021/05/25
- Initial Release

125
README.md Normal file
View File

@@ -0,0 +1,125 @@
# LC3 Encoder Qualification
Tests the `lc3py` encoder from `bumble-auracast` against the LC3 TS p5 conformance suite.
**Scope: 15 ENC test cases, no 44.1 kHz** (lc3py does not support 44.1 kHz).
---
## 1 — System packages
```bash
# Enable 32-bit architecture (required for wine32 to run LC3.exe)
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y \
wine \
gcc \
sox \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgstreamer-plugins-bad1.0-dev \
autoconf automake libtool
sudo apt-get install -y wine32
# One-time: initialize a 32-bit Wine prefix (LC3.exe is a 32-bit Windows binary)
WINEARCH=win32 WINEPREFIX=~/.wine32 wineboot
```
> The runner script (`run_enc_tests.py`) sets `WINEARCH=win32 WINEPREFIX=~/.wine32` automatically.
---
## 2 — Build gstPEAQ (one-time)
gstPEAQ v0.6.1 is the open-source PEAQ implementation used as the ODG metric tool.
```bash
cd /tmp
wget https://github.com/HSU-ANT/gstpeaq/archive/refs/tags/version-0.6.1.tar.gz -O gstpeaq-0.6.1.tar.gz
tar xzf gstpeaq-0.6.1.tar.gz
cd gstpeaq-version-0.6.1
# Patch build system to skip gtk-doc (not needed for the binary)
touch ChangeLog
sed -i 's/^GTK_DOC_CHECK/#GTK_DOC_CHECK/' configure.ac
echo "" > gtk-doc.make
sed -i '81s/CLEANFILES +=/CLEANFILES =/' doc/Makefile.am
sed -i 's/GTK_DOC_CHECK(1.10,--flavour no-tmpl)/: # GTK_DOC_CHECK disabled/' configure
autoreconf -i --force
./configure
make -C src # build only the binary, skip doc
```
The binary and plugin will be at:
- `/tmp/gstpeaq-version-0.6.1/src/peaq`
- `/tmp/gstpeaq-version-0.6.1/src/.libs/libgstpeaq.so`
> These paths are already set in `conf_lc3ts_p5_enc_10ms.cfg` and `conf_lc3ts_p5_enc_75ms.cfg`.
> If you build gstPEAQ elsewhere, update `peaq_bin` in both cfg files.
---
## 3 — Set up LC3 reference binary (one-time)
```bash
cd /home/pstruebi/repos/lc3_quali
SCRIPT_DIR=LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script
mkdir -p $SCRIPT_DIR/LC3_bin_current
ln -sf $(pwd)/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/LC3.exe \
$SCRIPT_DIR/LC3_bin_current/LC3.exe
```
---
## 4 — Install Python venv
```bash
cd /home/pstruebi/repos/lc3_quali
poetry install
```
---
## 5 — Run
```bash
poetry run python qualification/run_enc_tests.py
```
Results are written to `qualification/results/DD_MM_YY_HH_MM/`:
- **`SUMMARY.html`** — consultant-ready: all 15 TC IDs with PASS/FAIL
- `ENC_*_<ts>.html` — detail reports (ODG per SQAM item)
- `conformanceCheck_<ts>.log`
- `lc3_conformance_<ts>/` — audio comparison WAVs (`ref_ref.wav` vs `tst_ref.wav`)
---
## Test Cases (15 total)
### 10ms frame duration (7 cases)
| Test Case ID | kHz | kbps | Config |
|---|---|---|---|
| LC3/ENC/NB/BV-01-C | 8 | 24 | 10ms |
| LC3/ENC/WB/BV-01-C | 16 | 32 | 10ms |
| LC3/ENC/SSWB/BV-01-C | 24 | 48 | 10ms |
| LC3/ENC/SWB/BV-01-C | 32 | 64 | 10ms |
| LC3/ENC/FB/BV-01-C | 48 | 80 | 10ms |
| LC3/ENC/FB/BV-02-C | 48 | 96 | 10ms |
| LC3/ENC/FB/BV-03-C | 48 | 124 | 10ms |
### 7.5ms frame duration (8 cases)
| Test Case ID | kHz | kbps | Config |
|---|---|---|---|
| LC3/ENC/NB/BV-02-C | 8 | 27.734 | 7.5ms |
| LC3/ENC/WB/BV-02-C | 16 | 32 | 7.5ms |
| LC3/ENC/SSWB/BV-02-C | 24 | 48 | 7.5ms |
| LC3/ENC/SWB/BV-02-C | 32 | 64 | 7.5ms |
| LC3/ENC/SWB/BV-03-C | 32 | 61.867 | 7.5ms |
| LC3/ENC/FB/BV-07-C | 48 | 80 | 7.5ms |
| LC3/ENC/FB/BV-08-C | 48 | 96 | 7.5ms |
| LC3/ENC/FB/BV-09-C | 48 | 124.8 | 7.5ms |

4
lc3_encode.py Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python3
"""Redirect — encoder implementation lives in qualification/lc3_encode.py."""
import pathlib, runpy
runpy.run_path(str(pathlib.Path(__file__).parent / 'qualification' / 'lc3_encode.py'), run_name='__main__')

13
lc3_quali.code-workspace Normal file
View File

@@ -0,0 +1,13 @@
{
"folders": [
{
"path": "."
},
{
"path": "../bumble-auracast"
}
],
"settings": {
"python.languageServer": "None"
}
}

View File

@@ -0,0 +1,7 @@
LC3/ENC/NB/BV-01-C Encoder, Narrow Band, 8 kHz, 10 ms
LC3/ENC/WB/BV-01-C Encoder, Wideband, 16 kHz, 10 ms
LC3/ENC/SSWB/BV-01-C Encoder, Semi-Superwideband, 24 kHz, 10 ms
LC3/ENC/SWB/BV-01-C Encoder, Superwideband, 32 kHz, 10 ms
LC3/ENC/FB/BV-01-C Encoder, Full Band, 48 kHz, 80 kbps, 10 ms
LC3/ENC/FB/BV-02-C Encoder, Full Band, 48 kHz, 96 kbps, 10 ms
LC3/ENC/FB/BV-03-C Encoder, Full Band, 48 kHz, 124 kbps, 10 ms

2
poetry.toml Normal file
View File

@@ -0,0 +1,2 @@
[virtualenvs]
in-project = true

15
pyproject.toml Normal file
View File

@@ -0,0 +1,15 @@
[tool.poetry]
name = "lc3-quali"
version = "0.1.0"
description = "LC3 conformance qualification tests for bumble-auracast"
authors = []
package-mode = false
[tool.poetry.dependencies]
python = ">=3.11"
lc3py = {git = "ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git", rev = "ce2e41faf8c06d038df9f32504c61109a14130be"}
numpy = ">=1.24"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""LC3 encoder wrapper for conformanceCheck.py.
Called by conformanceCheck.py as:
python lc3_encode.py -frame_ms <ms> [options] "<input.wav>" "<output.lc3>" <bitrate_bps>
Bitstream format: 18-byte header (LC3 TS §3.2.8.2) + per-frame 2-byte size prefix + LC3 payload.
"""
import lc3
import struct
import sys
import wave
def main():
args = sys.argv[1:]
frame_ms = 10.0
positional = []
i = 0
while i < len(args):
if args[i] == '-frame_ms' and i + 1 < len(args):
try:
frame_ms = float(args[i + 1])
except ValueError:
print(f'Usage: {sys.argv[0]} -frame_ms <ms> [opts] <input.wav> <output.lc3> <bitrate_bps>',
file=sys.stderr)
sys.exit(1)
i += 2
elif args[i].startswith('-'):
i += 1 # skip unknown flags (e.g. -q)
else:
positional.append(args[i])
i += 1
if len(positional) < 3:
print(f'Usage: {sys.argv[0]} -frame_ms <ms> [opts] <input.wav> <output.lc3> <bitrate_bps>',
file=sys.stderr)
sys.exit(1)
wav_path = positional[0]
lc3_path = positional[1]
bitrate_bps = int(positional[2])
frame_duration_us = int(frame_ms * 1000)
with wave.open(wav_path, 'rb') as wavfile:
samplerate = wavfile.getframerate()
nchannels = wavfile.getnchannels()
bit_depth = wavfile.getsampwidth() * 8
stream_length = wavfile.getnframes()
enc = lc3.Encoder(frame_duration_us, samplerate, nchannels)
frame_size = enc.get_frame_bytes(bitrate_bps)
frame_length = enc.get_frame_samples()
resolved_bitrate = enc.resolve_bitrate(frame_size)
with open(lc3_path, 'wb') as f_lc3:
f_lc3.write(struct.pack(
'=HHHHHHHI',
0xcc1c, # signature
18, # header size
samplerate // 100, # sample rate / 100
resolved_bitrate // 100, # bitrate / 100
nchannels,
int(frame_ms * 100), # frame duration * 100
0, # reserved
stream_length, # total PCM samples
))
for _ in range(0, stream_length, frame_length):
pcm = wavfile.readframes(frame_length)
encoded = enc.encode(pcm, frame_size, bit_depth=bit_depth)
f_lc3.write(struct.pack('=H', frame_size))
f_lc3.write(encoded)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,293 @@
#!/usr/bin/env python3
"""Run LC3 ENC conformance tests and collect all outputs into a timestamped results folder.
Usage (from lc3_quali root):
poetry run python qualification/run_enc_tests.py
Each run creates:
qualification/results/DD_MM_YY_HH_MM/
SUMMARY.html ← consultant-ready: TC IDs + PASS/FAIL
testcases_list.txt ← reference list copy
ENC_narrow_10ms_<ts>.html ← detail reports per section
ENC_fb_10ms_<ts>.html
...
conformanceCheck_<ts>.log
lc3_conformance_<ts>/ ← audio comparison files (kept)
ENC_narrow_10ms/
encode_ABBA_8000_24000_ref_ref.wav ← reference encoder → reference decoder
encode_ABBA_8000_24000_tst_ref.wav ← OUR encoder → reference decoder
...
"""
import datetime
import os
import pathlib
import re
import shutil
import subprocess
import sys
QUAL_DIR = pathlib.Path(__file__).resolve().parent
REPO_ROOT = QUAL_DIR.parent
CONFORMANCE_DIR = (
REPO_ROOT
/ 'LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01'
/ 'LC3_Conformance_Interoperability_Script'
)
RESULTS_BASE = QUAL_DIR / 'results'
CFGS = [
'conf_lc3ts_p5_enc_10ms.cfg',
'conf_lc3ts_p5_enc_75ms.cfg',
]
# Maps cfg section name → {(sample_rate_hz, bitrate_bps): (tc_id, description)}
# TC IDs as per LC3 TS p5
# TC_ORDER: band-grouped display order — (band_label, section, fs_hz, br_bps)
TC_ORDER = [
('NB', 'ENC_narrow_10ms', 8000, 24000),
('NB', 'ENC_nb_75ms', 8000, 27734),
('WB', 'ENC_narrow_10ms', 16000, 32000),
('WB', 'ENC_narrow_75ms', 16000, 32000),
('SSWB', 'ENC_narrow_10ms', 24000, 48000),
('SSWB', 'ENC_narrow_75ms', 24000, 48000),
('SWB', 'ENC_narrow_10ms', 32000, 64000),
('SWB', 'ENC_narrow_75ms', 32000, 64000),
('SWB', 'ENC_hfp_75ms', 32000, 61867),
('FB', 'ENC_fb_10ms', 48000, 80000),
('FB', 'ENC_fb_10ms', 48000, 96000),
('FB', 'ENC_fb_10ms', 48000, 124000),
('FB', 'ENC_fb_75ms', 48000, 80000),
('FB', 'ENC_fb_75ms', 48000, 96000),
('FB', 'ENC_fb_75ms', 48000, 124800),
]
TC_MAP = {
'ENC_narrow_10ms': {
(8000, 24000): ('LC3/ENC/NB/BV-01-C', 'NB, 8 kHz, 24 kbps, 10 ms'),
(16000, 32000): ('LC3/ENC/WB/BV-01-C', 'WB, 16 kHz, 32 kbps, 10 ms'),
(24000, 48000): ('LC3/ENC/SSWB/BV-01-C', 'SSWB, 24 kHz, 48 kbps, 10 ms'),
(32000, 64000): ('LC3/ENC/SWB/BV-01-C', 'SWB, 32 kHz, 64 kbps, 10 ms'),
},
'ENC_fb_10ms': {
(48000, 80000): ('LC3/ENC/FB/BV-01-C', 'FB, 48 kHz, 80 kbps, 10 ms'),
(48000, 96000): ('LC3/ENC/FB/BV-02-C', 'FB, 48 kHz, 96 kbps, 10 ms'),
(48000, 124000): ('LC3/ENC/FB/BV-03-C', 'FB, 48 kHz, 124 kbps, 10 ms'),
},
'ENC_nb_75ms': {
(8000, 27734): ('LC3/ENC/NB/BV-02-C', 'NB, 8 kHz, 27.734 kbps, 7.5 ms'),
},
'ENC_narrow_75ms': {
(16000, 32000): ('LC3/ENC/WB/BV-02-C', 'WB, 16 kHz, 32 kbps, 7.5 ms'),
(24000, 48000): ('LC3/ENC/SSWB/BV-02-C', 'SSWB, 24 kHz, 48 kbps, 7.5 ms'),
(32000, 64000): ('LC3/ENC/SWB/BV-02-C', 'SWB, 32 kHz, 64 kbps, 7.5 ms'),
},
'ENC_hfp_75ms': {
(32000, 61867): ('LC3/ENC/SWB/BV-03-C', 'SWB, 32 kHz, 61.867 kbps, 7.5 ms'),
},
'ENC_fb_75ms': {
(48000, 80000): ('LC3/ENC/FB/BV-07-C', 'FB, 48 kHz, 80 kbps, 7.5 ms'),
(48000, 96000): ('LC3/ENC/FB/BV-08-C', 'FB, 48 kHz, 96 kbps, 7.5 ms'),
(48000, 124800): ('LC3/ENC/FB/BV-09-C', 'FB, 48 kHz, 124.8 kbps, 7.5 ms'),
},
}
SUMMARY_STYLE = """
body { font-family: sans-serif; max-width: 960px; margin: 2em auto; color: #222; }
h1 { margin-bottom: 0.2em; }
.meta { color: #555; font-size: 0.9em; margin-bottom: 1.5em; }
.banner { font-size: 1.3em; font-weight: bold; padding: 0.5em 1em;
border-radius: 4px; display: inline-block; margin-bottom: 1.5em; }
.pass-banner { background: #d4edda; color: #155724; border: 2px solid #28a745; }
.fail-banner { background: #f8d7da; color: #721c24; border: 2px solid #dc3545; }
table { border-collapse: collapse; width: 100%; margin-bottom: 2em; }
th { background: #343a40; color: #fff; padding: 0.6em 0.8em; text-align: left; }
td { padding: 0.5em 0.8em; border-bottom: 1px solid #dee2e6; font-size: 0.92em; }
tr:nth-child(even) { background: #f8f9fa; }
.pass { color: #155724; font-weight: bold; }
.fail { color: #721c24; font-weight: bold; }
.notrun { color: #856404; font-weight: bold; }
code { font-family: monospace; font-size: 0.88em; }
.note { background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px;
padding: 0.8em 1em; font-size: 0.88em; margin-top: 1em; }
.group-header td { background: #e9ecef; font-weight: bold; color: #343a40;
padding: 0.35em 0.8em; font-size: 0.85em; letter-spacing: 0.05em; }
"""
def section_from_stem(stem: str) -> str:
"""Extract section name from HTML file stem.
conformanceCheck writes: {section}_{YYYY-MM-DD}_{HH-MM}.html
e.g. ENC_narrow_10ms_2026-02-27_14-30
Last two underscore-parts are date and time → strip them.
"""
parts = stem.split('_')
return '_'.join(parts[:-2])
def parse_html_results(html_path: pathlib.Path) -> dict:
"""Return {(fs_hz, br_bps): 'PASS'|'FAIL'} by parsing the conformance HTML table."""
content = html_path.read_text(encoding='utf-8', errors='replace')
row_re = re.compile(
r'<tr><td>encode</td><td>[^<]+</td>'
r'<td>(\d+)</td><td>(\d+)</td>(.*?)</tr>',
re.DOTALL,
)
cell_class_re = re.compile(r'class=(pass|fail)')
config_rows: dict = {}
for m in row_re.finditer(content):
fs, br = int(m.group(1)), int(m.group(2))
classes = [c.group(1) for c in cell_class_re.finditer(m.group(3))]
config_rows.setdefault((fs, br), []).append(
bool(classes) and all(c == 'pass' for c in classes)
)
return {k: ('PASS' if all(v) else 'FAIL') for k, v in config_rows.items()}
def generate_summary(run_dir: pathlib.Path, html_files: list, run_ts: str) -> bool:
"""Parse detail HTMLs, build SUMMARY.html. Returns True if all TCs passed."""
# Collect per-(section, fs, br) results
section_results: dict = {}
for hp in html_files:
section = section_from_stem(hp.stem)
if section in TC_MAP:
section_results[section] = parse_html_results(hp)
# Build ordered table rows (band-grouped via TC_ORDER)
tc_rows = [] # (group_label|None, tc_id, desc, result, detail_html)
overall_pass = True
prev_band = None
for band, section, fs, br in TC_ORDER:
tc_id, desc = TC_MAP[section][(fs, br)]
if section in section_results:
result = section_results[section].get((fs, br), 'NOT RUN')
else:
result = 'NOT RUN'
detail_html = next(
(h.name for h in html_files if section_from_stem(h.stem) == section),
'#',
)
if result != 'PASS':
overall_pass = False
group_header = band if band != prev_band else None
prev_band = band
tc_rows.append((group_header, tc_id, desc, result, detail_html))
banner_cls = 'pass-banner' if overall_pass else 'fail-banner'
banner_txt = '&#10003; ALL 15 TEST CASES PASSED' if overall_pass else '&#10007; FAILURES DETECTED — see detail reports'
rows_html = ''
for group_header, tc_id, desc, result, detail in tc_rows:
if group_header:
rows_html += (
f'<tr class="group-header">'
f'<td colspan="4">{group_header}</td>'
f'</tr>\n'
)
res_cls = {'PASS': 'pass', 'FAIL': 'fail'}.get(result, 'notrun')
rows_html += (
f'<tr>'
f'<td><code>{tc_id}</code></td>'
f'<td>{desc}</td>'
f'<td class="{res_cls}">{result}</td>'
f'<td><a href="{detail}">Detail&nbsp;&rarr;</a></td>'
f'</tr>\n'
)
html = f"""<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8">
<title>LC3 ENC Conformance Summary — {run_ts}</title>
<style>{SUMMARY_STYLE}</style>
</head><body>
<h1>LC3 Encoder Conformance Summary</h1>
<div class="meta">
<strong>Run:</strong> {run_ts} &nbsp;|&nbsp;
<strong>Standard:</strong> LC3 TS p5 &nbsp;|&nbsp;
<strong>Scope:</strong> ENC — 15 test cases &nbsp;|&nbsp;
<strong>Pass criterion:</strong> PEAQ ODG &Delta; &le; &minus;0.07
</div>
<div class="banner {banner_cls}">{banner_txt}</div>
<table>
<thead>
<tr>
<th>Test Case ID (LC3 TS p5)</th>
<th>Operating Point</th>
<th>Result</th>
<th>Detail Report</th>
</tr>
</thead>
<tbody>
{rows_html} </tbody>
</table>
<div class="note">
<strong>Audio comparison files</strong> are in <code>lc3_conformance_*/&lt;section&gt;/</code>:<br>
&bull; <code>encode_&lt;ITEM&gt;_&lt;fs&gt;_&lt;br&gt;_ref_ref.wav</code>
&mdash; Reference encoder &rarr; Reference decoder &nbsp;(<em>golden reference</em>)<br>
&bull; <code>encode_&lt;ITEM&gt;_&lt;fs&gt;_&lt;br&gt;_tst_ref.wav</code>
&mdash; <strong>Our encoder</strong> &rarr; Reference decoder &nbsp;(<em>device under test output</em>)<br>
SQAM test items used: ABBA, Castanets, Eddie_Rabbitt, Female_Speech_German,
Glockenspiel, Piano_Schubert, Violoncello, Harpsichord, Male_Speech_English.
</div>
</body></html>"""
(run_dir / 'SUMMARY.html').write_text(html, encoding='utf-8')
return overall_pass
def collect_outputs(run_dir: pathlib.Path) -> list:
"""Move HTML/log/work-dir files from conformance dir into run_dir. Returns list of HTML paths."""
html_files = []
for item in list(CONFORMANCE_DIR.iterdir()):
if item.suffix == '.html' or item.suffix == '.log' or item.name.startswith('lc3_conformance_'):
dest = run_dir / item.name
shutil.move(str(item), str(dest))
if item.suffix == '.html':
html_files.append(dest)
return html_files
def main() -> None:
run_ts = datetime.datetime.now().strftime('%d_%m_%y_%H_%M')
run_dir = RESULTS_BASE / f'lc3_quali_{run_ts}'
run_dir.mkdir(parents=True, exist_ok=True)
print(f'Results folder: {run_dir}')
wine_env = os.environ.copy()
wine_env['WINEARCH'] = 'win32'
wine_env['WINEPREFIX'] = str(pathlib.Path.home() / '.wine32')
subprocess_ok = True
for cfg in CFGS:
print(f'\n{"=" * 60}')
print(f'Running: {cfg}')
print('=' * 60)
r = subprocess.run(
[sys.executable, 'conformanceCheck.py', cfg, '-keep', '-system_sox'],
cwd=CONFORMANCE_DIR,
env=wine_env,
)
if r.returncode != 0:
print(f'ERROR: conformanceCheck.py exited with code {r.returncode}', file=sys.stderr)
subprocess_ok = False
html_files = collect_outputs(run_dir)
shutil.copy(str(QUAL_DIR / 'testcases_list.txt'), str(run_dir / 'testcases_list.txt'))
overall_pass = generate_summary(run_dir, html_files, run_ts)
print(f'\n{"=" * 60}')
print(f'Subprocess OK : {subprocess_ok}')
print(f'Conformance : {"ALL PASSED" if overall_pass else "FAILURES — check SUMMARY.html"}')
print(f'Results folder: {run_dir}')
print('=' * 60)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,23 @@
# LC3 ENC Conformance Test Cases
# Scope: Encoder only, no 44.1 kHz (lc3py does not support 44.1 kHz)
# Total: 15 cases
# Format: TestCaseID | Sample Rate (Hz) | Bit Rate (bps) | Frame Duration (ms)
#
# 10ms frame duration (7 cases)
LC3/ENC/NB/BV-01-C | 8000 | 24000 | 10
LC3/ENC/WB/BV-01-C | 16000 | 32000 | 10
LC3/ENC/SSWB/BV-01-C | 24000 | 48000 | 10
LC3/ENC/SWB/BV-01-C | 32000 | 64000 | 10
LC3/ENC/FB/BV-01-C | 48000 | 80000 | 10
LC3/ENC/FB/BV-02-C | 48000 | 96000 | 10
LC3/ENC/FB/BV-03-C | 48000 | 124000 | 10
#
# 7.5ms frame duration (8 cases)
LC3/ENC/NB/BV-02-C | 8000 | 27734 | 7.5
LC3/ENC/WB/BV-02-C | 16000 | 32000 | 7.5
LC3/ENC/SSWB/BV-02-C | 24000 | 48000 | 7.5
LC3/ENC/SWB/BV-02-C | 32000 | 64000 | 7.5
LC3/ENC/SWB/BV-03-C | 32000 | 61867 | 7.5
LC3/ENC/FB/BV-07-C | 48000 | 80000 | 7.5
LC3/ENC/FB/BV-08-C | 48000 | 96000 | 7.5
LC3/ENC/FB/BV-09-C | 48000 | 124800 | 7.5

24
testcases_list.txt Normal file
View File

@@ -0,0 +1,24 @@
# Moved to qualification/testcases_list.txt
# LC3 ENC Conformance Test Cases
# Scope: Encoder only, no 44.1 kHz (lc3py does not support 44.1 kHz)
# Total: 15 cases
# Format: TestCaseID | Sample Rate (Hz) | Bit Rate (bps) | Frame Duration (ms)
#
# 10ms frame duration (7 cases)
LC3/ENC/NB/BV-01-C | 8000 | 24000 | 10
LC3/ENC/WB/BV-01-C | 16000 | 32000 | 10
LC3/ENC/SSWB/BV-01-C | 24000 | 48000 | 10
LC3/ENC/SWB/BV-01-C | 32000 | 64000 | 10
LC3/ENC/FB/BV-01-C | 48000 | 80000 | 10
LC3/ENC/FB/BV-02-C | 48000 | 96000 | 10
LC3/ENC/FB/BV-03-C | 48000 | 124000 | 10
#
# 7.5ms frame duration (8 cases)
LC3/ENC/NB/BV-02-C | 8000 | 27734 | 7.5
LC3/ENC/WB/BV-02-C | 16000 | 32000 | 7.5
LC3/ENC/SSWB/BV-02-C | 24000 | 48000 | 7.5
LC3/ENC/SWB/BV-02-C | 32000 | 64000 | 7.5
LC3/ENC/SWB/BV-03-C | 32000 | 61867 | 7.5
LC3/ENC/FB/BV-07-C | 48000 | 80000 | 7.5
LC3/ENC/FB/BV-08-C | 48000 | 96000 | 7.5
LC3/ENC/FB/BV-09-C | 48000 | 124800 | 7.5