Skip to content

Commit

Permalink
Merge pull request #28 from braheezy/compare
Browse files Browse the repository at this point in the history
feat: add testing direct comparison to reference
  • Loading branch information
braheezy authored Apr 13, 2024
2 parents ffd9763 + 8f691ff commit 1068937
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 4 deletions.
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"

0 comments on commit 1068937

Please sign in to comment.