Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add testing direct comparison to reference #28

Merged
merged 4 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ dist/
/fuzz/*.qoa
/assets/*.gif
*.prof
/output*
__debug_bin*
/*.wav
/*.qoa
TODO
/raw_output.txt
32 changes: 32 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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!

Expand Down
4 changes: 3 additions & 1 deletion cmd/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ func convertAudio(inputFile, outputFile string) {
"samplerate(hz)", pcmBuffer.Format.SampleRate,
"samples/channel", numSamples,
"bit depth", wavDecoder.SampleBitDepth(),
"size", formatSize(len(inputData)))
"size", formatSize(len(inputData)),
"duration", fmt.Sprintf("%v sec", numSamples/uint32(pcmBuffer.Format.SampleRate)),
)
if wavDecoder.SampleBitDepth() > 16 {
logger.Warn("Bit depth is greater than 16, this may result in loss of precision and sound quality!")
}
Expand Down
162 changes: 162 additions & 0 deletions compare.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#!/bin/bash

set -ou pipefail

usage() {
echo "Usage: $0 <file|directory|archive>"
exit 1
}

# Function to handle regular files
process_file() {
local file=$1
echo "Processing file: $file"
song_filename=$(basename "$file")
song_name="${song_filename%.*}"

result=$(qoaconv "$file" "/data/output_qoa/$song_name.qoa")
if [ $? -ne 0 ]; then
echo "qoaconv,$file,error: see raw_output.txt" >> /data/raw_output.txt
else
echo "$result" >> /data/raw_output.txt
fi

result=$(goqoa convert -v "$file" "/data/output_qoa/$song_name.qoa")
if [ $? -ne 0 ]; then
echo "goqoa,$file,error: see raw_output.txt" >> /data/raw_output.txt
else
echo "$result" >> /data/raw_output.txt
fi
}

# Function to handle directories
process_directory() {
local dir=$1
echo "Processing directory: $dir"
for file in "$dir"/*.wav; do
process_file "$file"
done
}

# 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=""

# 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+="qoaconv,$file,$psnr,$channels,$sample_rate,$duration,$size,$bitrate\n"
fi
if [[ "$line" =~ channels= ]]; then
file=$(echo "$line" | awk '{print $2}')
file=$(basename "$file")
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
if [[ "$line" =~ error ]]; then
csv+="${line},---,---,---,---,---\n"
fi
done < "$raw_output"

echo "${csv}"
}

# Use gum to print the CSV as a table
csv_data=$(extract_and_format_csv)
if [ -z "${csv_data+x}" ]; then
echo "No data found..."
echo "Check raw_output.txt"
exit 1
fi
echo -e "$csv_data" | gum table --print \
--border.foreground "#DDB6F2" \
--cell.foreground="#FAE3B0" \
--header.foreground="#96CDFB"
Loading