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:
112
.gitignore
vendored
Normal file
112
.gitignore
vendored
Normal 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
4
AGENTS.md
Normal 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
BIN
LC3.TS.p5.pdf
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
/home/pstruebi/repos/lc3_quali/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/LC3.exe
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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__ */
|
||||
Binary file not shown.
@@ -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);
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
125
README.md
Normal file
125
README.md
Normal 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
4
lc3_encode.py
Normal 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
13
lc3_quali.code-workspace
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../bumble-auracast"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"python.languageServer": "None"
|
||||
}
|
||||
}
|
||||
7
letter_from_consultant.txt
Normal file
7
letter_from_consultant.txt
Normal 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
2
poetry.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[virtualenvs]
|
||||
in-project = true
|
||||
15
pyproject.toml
Normal file
15
pyproject.toml
Normal 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"
|
||||
79
qualification/lc3_encode.py
Normal file
79
qualification/lc3_encode.py
Normal 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()
|
||||
293
qualification/run_enc_tests.py
Normal file
293
qualification/run_enc_tests.py
Normal 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 = '✓ ALL 15 TEST CASES PASSED' if overall_pass else '✗ 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 →</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} |
|
||||
<strong>Standard:</strong> LC3 TS p5 |
|
||||
<strong>Scope:</strong> ENC — 15 test cases |
|
||||
<strong>Pass criterion:</strong> PEAQ ODG Δ ≤ −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_*/<section>/</code>:<br>
|
||||
• <code>encode_<ITEM>_<fs>_<br>_ref_ref.wav</code>
|
||||
— Reference encoder → Reference decoder (<em>golden reference</em>)<br>
|
||||
• <code>encode_<ITEM>_<fs>_<br>_tst_ref.wav</code>
|
||||
— <strong>Our encoder</strong> → Reference decoder (<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()
|
||||
23
qualification/testcases_list.txt
Normal file
23
qualification/testcases_list.txt
Normal 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
24
testcases_list.txt
Normal 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
|
||||
Reference in New Issue
Block a user