From 4949274305f45dc4f83fc4c84ee2e6cd34036a18 Mon Sep 17 00:00:00 2001 From: braheezy Date: Sat, 13 Apr 2024 13:49:15 -0700 Subject: [PATCH] feat: add testing direct comparison to reference --- Dockerfile | 32 ++++++++++++ README.md | 11 +++-- compare.sh | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 Dockerfile create mode 100755 compare.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5d1d0ab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM fedora:latest as builder + +RUN dnf install -y gcc make curl-devel alsa-lib-devel git golang && \ + dnf clean all + +WORKDIR /app + +RUN git clone https://github.com/phoboslab/qoa.git --depth 1 && \ + cd qoa && \ + curl -L https://github.com/mackron/dr_libs/raw/master/dr_mp3.h -o dr_mp3.h && \ + curl -L https://github.com/mackron/dr_libs/raw/master/dr_flac.h -o dr_flac.h && \ + curl -L https://github.com/floooh/sokol/raw/master/sokol_audio.h -o sokol_audio.h && \ + make conv + +COPY go.mod . +RUN go mod download + +COPY . . +RUN go build -o goqoa . + +FROM fedora:latest + +RUN dnf install -y alsa-lib-devel file unzip https://github.com/charmbracelet/gum/releases/download/v0.13.0/gum-0.13.0-1.x86_64.rpm && \ + dnf clean all + +COPY --from=builder /app/goqoa /usr/bin/ +COPY --from=builder /app/qoa/qoaconv /usr/bin/ +COPY --from=builder /app/compare.sh /app/ + +ENV TERM=xterm-256color + +ENTRYPOINT ["/app/compare.sh"] diff --git a/README.md b/README.md index 64cdd19..2b247db 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,10 @@ Then you can `make build` to get a binary. `make test` will run Go unit tests. -## Reference Testing +### Reference Testing This is a rewrite of the QOA implementation, not a transpile of or a CGO wrapper to `qoa.h`. It's a simple enough encoding that the code can be compared side-by-side to ensure the same algorithm has been implemented. -To further examine fidelity, the `check_spec.sh` script is used. It does the following: +To further examine fidelity, the `check_spec.sh` script can be used. It does the following: - If required, fetch the [sample pack from the QOA website](https://qoaformat.org/samples/) - Grab random WAV files from the pack - `goqoa convert` the file to QOA format and compare against the QOA file created by the reference author @@ -92,7 +92,11 @@ The check uses `cmp` to check each byte in each produced file. For an unknown re - `check_spec.h` to check a small amount of bytes for a small amount of files - `check_spec.sh -a` to fully check all 150 songs and record `failures` -## Fuzz Testing +The `Dockerfile` can also be used to compare against the reference. It builds and installs both `goqoa` and `qoaconv` and provides an entrypoint script to convert WAV file(s) with both tools, then summarize the results. + + docker build . -t qoacompare:latest && docker run --rm -it -v `pwd`:/data qoacompare /data/test_ultra_new.wav + +### Fuzz Testing The `qoa` package has a fuzz unit test to examine the `Encode()` and `Decode()` functions. `fuzz/create_fuzzy_files.py` generates valid QOA files with random data. @@ -120,6 +124,7 @@ And the quality of the encoded file didn't go down: - After: ![before-after](./assets/after-quality.png) --- + ## Disclaimer I have never written software that deals with audio files before. I saw a post about QOA on HackerNews and found the name amusing. There were many ports to other languages, but Go was not listed. So here we are! diff --git a/compare.sh b/compare.sh new file mode 100755 index 0000000..8dfff2b --- /dev/null +++ b/compare.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +set -eou pipefail + +usage() { + echo "Usage: $0 " + exit 1 +} + +# Function to handle regular files +process_file() { + local file=$1 + echo "Processing file: $file" + song_filename=$(basename "$file") + song_name="${song_filename%.*}" + qoaconv "$file" "/data/output_qoa/$song_name.qoa" &>> /data/raw_output.txt + goqoa convert -v "$file" "/data/output_goqoa/$song_name.go.qoa" &>> /data/raw_output.txt +} + +# Function to handle directories +process_directory() { + local dir=$1 + echo "Processing directory: $dir" + # Place your directory processing logic here +} + +# Function to handle archive files +process_archive() { + local archive=$1 + echo "Processing archive: $archive" + # The name of the downloaded samples zip from the QOA website + spec_zip=/data/qoa_test_samples_2023_02_18.zip + + if [ ! -f "$spec_zip" ]; then + echo "Can't find $spec_zip" + exit 1 + fi + + num_songs=10 + # Extract random songs to test + selected_songs=$(unzip -Z1 "$spec_zip" '*.wav' -x '*.qoa.wav' | shuf -n "$num_songs") + + for song in $selected_songs; do + song_filename=$(basename "$song") + song_name="${song_filename%.*}" + # Get the song from the zip file + unzip -j -qq $spec_zip "*$song_name*" -d "$temp_dir" + + qoaconv "$temp_dir/$song_name.wav" "/data/output_qoa/$song_name.qoa" &>> /data/raw_output.txt + goqoa convert -v "$temp_dir/$song_name.wav" "/data/output_goqoa/$song_name.go.qoa" &>> /data/raw_output.txt + done + rm -rf "$temp_dir" + + grep --color=always -i psnr /data/raw_output.txt +} + +if [ -z "${1+x}" ]; then + usage +fi +file_path=$1 + +if [ ! -e "$file_path" ]; then + echo "The specified file or directory does not exist." + exit 1 +fi + + +mkdir -p /data/output_qoa +mkdir -p /data/output_goqoa +rm -f /data/raw_output.txt &>/dev/null +temp_dir=$(mktemp -d) + +if [ -f "$file_path" ]; then + # Check if it's an archive + if file "$file_path" | grep -qE 'Zip archive data'; then + process_archive "$file_path" + else + process_file "$file_path" + fi +elif [ -d "$file_path" ]; then + process_directory "$file_path" +else + echo "Unsupported file type." + exit 1 +fi + +# Function to extract and format data as CSV +extract_and_format_csv() { + local raw_output="/data/raw_output.txt" + local csv="Encoder,File,PSNR,Channels,Sample Rate,Duration,Size,Bitrate\n" + + local file="" + local channels="" + local sample_rate="" + local duration="" + local size="" + local bitrate="" + local psnr="" + + # local in_qoa=0 + # local in_goqoa=0 + + # Assuming that each entry follows the format shown in your example + while IFS= read -r line; do + if [[ "$line" =~ channels: ]]; then + channels=$(echo "$line" | awk -F'channels: ' '{print $2}' | awk '{print $1}' | sed 's/,*$//g') + sample_rate=$(echo "$line" | awk -F'samplerate: ' '{print $2}' | awk '{print $1, $2}'| sed 's/,*$//g') + duration=$(echo "$line" | awk -F'duration: ' '{print $2}') + fi + if [[ "$line" =~ ^/data/output_.* ]]; then + file=$(echo "$line" | awk '{print $1}' | sed 's/:$//') + file=$(basename "$file") + size=$(echo "$line" | awk -F'size: ' '{print $2}' | awk '{print $1, $2}') + bitrate=$(echo "$line" | awk -F'size: ' '{print $2}' | awk '{print $6, $7}' | sed 's/,*$//g') + psnr=$(echo "$line" | awk -F'psnr: ' '{print $2}' | awk '{print $1, $2}') + csv+="qoa,$file,$psnr,$channels,$sample_rate,$duration,$size,$bitrate\n" + fi + if [[ "$line" =~ channels= ]]; then + file=$(echo "$line" | awk '{print $2}') + channels=$(echo "$line" | awk -F'channels=' '{print $2}' | awk '{print $1}') + sample_rate=$(echo "$line" | awk -F'samplerate(hz)=' '{print $1}' | awk '{print $4}' | cut -d'=' -f2) + sample_rate="${sample_rate} hz" + duration=$(echo "$line" | awk -F'duration=' '{print $2}' | sed 's/"//g') + fi + if [[ "$line" =~ bitrate= ]]; then + size=$(echo "$line" | awk -F'size=' '{print $2}' | awk '{print $1, $2}' | sed 's/"//g') + bitrate=$(echo "$line" | awk -F'bitrate=' '{print $2}' | awk '{print $1, $2}' | sed 's/"//g') + psnr=$(echo "$line" | awk -F'psnr=' '{print $2}' | awk '{print $1}') + psnr="${psnr} db" + csv+="goqoa,$file,$psnr,$channels,$sample_rate,$duration,$size,$bitrate\n" + fi + done < "$raw_output" + + echo "${csv}" +} + +# Use gum to print the CSV as a table +csv_data=$(extract_and_format_csv) +echo -e "$csv_data" | gum table --print \ + --border.foreground "#DDB6F2" \ + --cell.foreground="#FAE3B0" \ + --header.foreground="#96CDFB"