commit 2845458d3f66ccb20aa9c698a75f9f23db8e3ddc Author: pstruebi Date: Fri Feb 27 14:56:35 2026 +0100 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34e8390 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d9e7698 --- /dev/null +++ b/AGENTS.md @@ -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 diff --git a/LC3.TS.p5.pdf b/LC3.TS.p5.pdf new file mode 100644 index 0000000..27b8ddc Binary files /dev/null and b/LC3.TS.p5.pdf differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/LC3_bin_current/LC3.exe b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/LC3_bin_current/LC3.exe new file mode 120000 index 0000000..e805838 --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/LC3_bin_current/LC3.exe @@ -0,0 +1 @@ +/home/pstruebi/repos/lc3_quali/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/LC3.exe \ No newline at end of file diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/Readme.txt b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/Readme.txt new file mode 100644 index 0000000..57088df --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/Readme.txt @@ -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' diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_mandatory_10ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_mandatory_10ms.cfg new file mode 100644 index 0000000..a0f19b4 --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_mandatory_10ms.cfg @@ -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 diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_mandatory_75ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_mandatory_75ms.cfg new file mode 100644 index 0000000..3ef13a9 --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_mandatory_75ms.cfg @@ -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 \ No newline at end of file diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_optional_10ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_optional_10ms.cfg new file mode 100644 index 0000000..c4d02bc --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_optional_10ms.cfg @@ -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 diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_optional_75ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_optional_75ms.cfg new file mode 100644 index 0000000..40276aa --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_ATA_optional_75ms.cfg @@ -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 diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_HA_mandatory_10ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_HA_mandatory_10ms.cfg new file mode 100644 index 0000000..4e5fe24 --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_HA_mandatory_10ms.cfg @@ -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 diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_HA_mandatory_75ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_HA_mandatory_75ms.cfg new file mode 100644 index 0000000..274c26d --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_HA_mandatory_75ms.cfg @@ -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 diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_HFP_mandatory_75ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_HFP_mandatory_75ms.cfg new file mode 100644 index 0000000..4154bfa --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_HFP_mandatory_75ms.cfg @@ -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 diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_lc3ts_p5_enc_10ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_lc3ts_p5_enc_10ms.cfg new file mode 100644 index 0000000..6d1bdac --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_lc3ts_p5_enc_10ms.cfg @@ -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 diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_lc3ts_p5_enc_75ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_lc3ts_p5_enc_75ms.cfg new file mode 100644 index 0000000..fd718b2 --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conf_lc3ts_p5_enc_75ms.cfg @@ -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 diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conformanceCheck.py b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conformanceCheck.py new file mode 100755 index 0000000..f77cc16 --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/conformanceCheck.py @@ -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 = ('{title} Report' + '

Conformance test for "{title}" (Frame Size {frame_ms} ms) {state}!

') +HTML_TABLE_HEAD = '

{title}

\n' +HTML_TABLE_TAIL = '
' +HTML_TAIL = '' +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('' + ''.join('{}'.format(x) for x in header) + '\n') + htmlfile.write('' + ''.join('{}'.format(x) for x in stats) + '\n') + for conf, (_, result) in sorted(results.items()): + htmlfile.write('' + ''.join('{}'.format(x) for x in conf)) + for value, clazz, thresh in result: + thresh = ' ({})'.format(fstr(thresh,3)) if thresh != None else '' + htmlfile.write('{}{}'.format(clazz, fstr(value,3), thresh)) + htmlfile.write('\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) diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/hp_fir_coef.txt b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/hp_fir_coef.txt new file mode 100644 index 0000000..3b0ca88 --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/hp_fir_coef.txt @@ -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 \ No newline at end of file diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/rms b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/rms new file mode 100755 index 0000000..46f3440 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/rms differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/rms.c b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/rms.c new file mode 100644 index 0000000..85886d1 --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/rms.c @@ -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 +#include +#include +#include +#include + +/* 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); +} + diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/tinywavein_c.h b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/tinywavein_c.h new file mode 100644 index 0000000..d5f04dd --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Conformance_Interoperability_Script/tinywavein_c.h @@ -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 +#include +#include + +/***** Interface *********************************************************/ + +#ifndef TWI_UINT64 + #if ( __STDC_VERSION__ >= 199901L || ( defined(_MSC_VER) &&_MSC_VER >= 1600 ) ) + #include + #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: <> */ + unsigned char originationTime[8]; /* ASCII: <> */ + 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: <> */ +} 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__ */ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/LC3.exe b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/LC3.exe new file mode 100755 index 0000000..da7962b Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/LC3.exe differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/Readme.txt b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/Readme.txt new file mode 100644 index 0000000..a50e95d --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/LC3_Reference_Binary/Readme.txt @@ -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 INPUT.bin OUTPUT.wav + where 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 INPUT.bin OUTPUT.wav + where 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 INPUT.wav OUTPUT.wav BITRATE + where 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); diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/Readme.txt b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/Readme.txt new file mode 100644 index 0000000..a9a35a7 --- /dev/null +++ b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/Readme.txt @@ -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 diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/._LC3_Conformance_Interoperability_Script b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/._LC3_Conformance_Interoperability_Script new file mode 100755 index 0000000..c41d5b6 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/._LC3_Conformance_Interoperability_Script differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/._LC3_Reference_Binary b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/._LC3_Reference_Binary new file mode 100755 index 0000000..c41d5b6 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/._LC3_Reference_Binary differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/._Readme.txt b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/._Readme.txt new file mode 100644 index 0000000..f022c6f Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/._Readme.txt differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._Readme.txt b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._Readme.txt new file mode 100644 index 0000000..0614890 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._Readme.txt differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_mandatory_10ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_mandatory_10ms.cfg new file mode 100644 index 0000000..074e14a Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_mandatory_10ms.cfg differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_mandatory_75ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_mandatory_75ms.cfg new file mode 100644 index 0000000..3a64eda Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_mandatory_75ms.cfg differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_optional_10ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_optional_10ms.cfg new file mode 100644 index 0000000..68cb0a9 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_optional_10ms.cfg differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_optional_75ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_optional_75ms.cfg new file mode 100644 index 0000000..758d225 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_ATA_optional_75ms.cfg differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_HA_mandatory_10ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_HA_mandatory_10ms.cfg new file mode 100644 index 0000000..58f4fe2 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_HA_mandatory_10ms.cfg differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_HA_mandatory_75ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_HA_mandatory_75ms.cfg new file mode 100644 index 0000000..585018d Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_HA_mandatory_75ms.cfg differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_HFP_mandatory_75ms.cfg b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_HFP_mandatory_75ms.cfg new file mode 100644 index 0000000..8bf03a3 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conf_HFP_mandatory_75ms.cfg differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conformanceCheck.py b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conformanceCheck.py new file mode 100755 index 0000000..ca3fb63 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._conformanceCheck.py differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._hp_fir_coef.txt b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._hp_fir_coef.txt new file mode 100644 index 0000000..c41d5b6 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._hp_fir_coef.txt differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._rms.c b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._rms.c new file mode 100644 index 0000000..d66d4ca Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._rms.c differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._tinywavein_c.h b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._tinywavein_c.h new file mode 100644 index 0000000..1f11ca1 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Conformance_Interoperability_Script/._tinywavein_c.h differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Reference_Binary/._LC3.exe b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Reference_Binary/._LC3.exe new file mode 100755 index 0000000..c41d5b6 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Reference_Binary/._LC3.exe differ diff --git a/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Reference_Binary/._Readme.txt b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Reference_Binary/._Readme.txt new file mode 100644 index 0000000..810ef74 Binary files /dev/null and b/LC3_conformance_interoperability_test_software_V1.0.8_2024-07-01/__MACOSX/LC3_Reference_Binary/._Readme.txt differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d47a19b --- /dev/null +++ b/README.md @@ -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_*_.html` — detail reports (ODG per SQAM item) +- `conformanceCheck_.log` +- `lc3_conformance_/` — 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 | diff --git a/lc3_encode.py b/lc3_encode.py new file mode 100644 index 0000000..e3b6c5b --- /dev/null +++ b/lc3_encode.py @@ -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__') diff --git a/lc3_quali.code-workspace b/lc3_quali.code-workspace new file mode 100644 index 0000000..bcff89b --- /dev/null +++ b/lc3_quali.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../bumble-auracast" + } + ], + "settings": { + "python.languageServer": "None" + } +} \ No newline at end of file diff --git a/letter_from_consultant.txt b/letter_from_consultant.txt new file mode 100644 index 0000000..a6332f6 --- /dev/null +++ b/letter_from_consultant.txt @@ -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 \ No newline at end of file diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..86d6935 --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/qualification/lc3_encode.py b/qualification/lc3_encode.py new file mode 100644 index 0000000..61f1885 --- /dev/null +++ b/qualification/lc3_encode.py @@ -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 [options] "" "" + +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 [opts] ', + 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 [opts] ', + 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() diff --git a/qualification/run_enc_tests.py b/qualification/run_enc_tests.py new file mode 100644 index 0000000..38d7d7c --- /dev/null +++ b/qualification/run_enc_tests.py @@ -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_.html ← detail reports per section + ENC_fb_10ms_.html + ... + conformanceCheck_.log + lc3_conformance_/ ← 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'encode[^<]+' + r'(\d+)(\d+)(.*?)', + 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'' + f'{group_header}' + f'\n' + ) + res_cls = {'PASS': 'pass', 'FAIL': 'fail'}.get(result, 'notrun') + rows_html += ( + f'' + f'{tc_id}' + f'{desc}' + f'{result}' + f'Detail →' + f'\n' + ) + + html = f""" + + + LC3 ENC Conformance Summary — {run_ts} + + +

LC3 Encoder Conformance Summary

+
+ Run: {run_ts}  |  + Standard: LC3 TS p5  |  + Scope: ENC — 15 test cases  |  + Pass criterion: PEAQ ODG Δ ≤ −0.07 +
+ + + + + + + + + + + + +{rows_html} +
Test Case ID (LC3 TS p5)Operating PointResultDetail Report
+ +
+ Audio comparison files are in lc3_conformance_*/<section>/:
+ • encode_<ITEM>_<fs>_<br>_ref_ref.wav + — Reference encoder → Reference decoder  (golden reference)
+ • encode_<ITEM>_<fs>_<br>_tst_ref.wav + — Our encoder → Reference decoder  (device under test output)
+ SQAM test items used: ABBA, Castanets, Eddie_Rabbitt, Female_Speech_German, + Glockenspiel, Piano_Schubert, Violoncello, Harpsichord, Male_Speech_English. +
+""" + + (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() diff --git a/qualification/testcases_list.txt b/qualification/testcases_list.txt new file mode 100644 index 0000000..1494589 --- /dev/null +++ b/qualification/testcases_list.txt @@ -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 diff --git a/testcases_list.txt b/testcases_list.txt new file mode 100644 index 0000000..85dae2c --- /dev/null +++ b/testcases_list.txt @@ -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