diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index d32f015..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f1ae41..db48df7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,8 +16,7 @@ jobs: matrix: os: [ubuntu-latest] # , macos-latest, windows-latest] ocaml-compiler: - - "5.3.0" # Latest stable - - "5.2.0" + - "5.3.0" runs-on: ${{ matrix.os }} @@ -25,29 +24,30 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Checkout raven-ml/raven - uses: actions/checkout@v4 - with: - repository: raven-ml/raven - path: raven - - name: Set up OCaml ${{ matrix.ocaml-compiler }} on ${{ matrix.os }} uses: ocaml/setup-ocaml@v3 with: ocaml-compiler: ${{ matrix.ocaml-compiler }} dune-cache: true - # temporary, waiting for the conf-soxr package to be published to opam repo - name: Install libsoxr-dev run: | sudo apt-get update sudo apt-get install -y libsoxr-dev shell: bash - - name: Pin raven packages from working directory - run: opam pin add ./raven --working-dir --yes + - name: Install libsndfile, libsamplerate and librubberband + run: | + sudo apt-get install libsndfile-dev libsamplerate0-dev librubberband-dev + + - name: Install llvm # required for rune + run: | + sudo apt-get install libllvm-20-ocaml-dev libllvm20 llvm-20 llvm-20-dev llvm-20-doc llvm-20-examples llvm-20-runtime + + - name: Lock the dependencies + run: opam exec -- dune pkg lock shell: bash - - name: Install SoundML - run: opam install . --yes --confirm-level=unsafe-yes - shell: bash \ No newline at end of file + - name: Build SoundML + run: opam exec -- dune build + shell: bash diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 12051ba..e446594 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,7 +18,7 @@ jobs: - name: Set up OCaml for linting uses: ocaml/setup-ocaml@v3 with: - ocaml-compiler: "5.2" + ocaml-compiler: "5.3" dune-cache: true - name: Run OCaml Lint & Format Check - uses: ocaml/setup-ocaml/lint-fmt@v2 \ No newline at end of file + uses: ocaml/setup-ocaml/lint-fmt@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 394dd42..6b61abf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,8 +17,7 @@ jobs: matrix: os: [ubuntu-latest] # , macos-latest, windows-latest] ocaml-compiler: - - "5.3.0" # Latest stable - - "5.2.0" + - "5.3.0" include: - os: ubuntu-latest ocaml-compiler: "5.3.0" @@ -30,12 +29,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Checkout raven-ml/raven - uses: actions/checkout@v4 - with: - repository: raven-ml/raven - path: raven - - name: Set up OCaml ${{ matrix.ocaml-compiler }} on ${{ matrix.os }} uses: ocaml/setup-ocaml@v3 with: @@ -55,16 +48,20 @@ jobs: sudo apt-get install -y libsoxr-dev shell: bash + - name: Install libsndfile, libsamplerate and librubberband + run: | + sudo apt-get install libsndfile-dev libsamplerate0-dev librubberband-dev + + - name: Install llvm # required for rune + run: | + sudo apt-get install libllvm-20-ocaml-dev libllvm20 llvm-20 llvm-20-dev llvm-20-doc llvm-20-examples llvm-20-runtime + - name: Install FFmpeg CLI (to generate test data) run: sudo apt-get install -y ffmpeg shell: bash - - name: Pin raven packages from working directory - run: opam pin add ./raven --working-dir --yes - shell: bash - - - name: Install SoundML dependencies (with test) - run: opam install . --deps-only --with-test --yes --confirm-level=unsafe-yes --verbose + - name: Lock dune packages + run: opam exec -- dune pkg lock shell: bash - name: Run tests diff --git a/.gitignore b/.gitignore index 73d270e..16797bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store _build dune.lock +dev-tools.locks dataset/ cache/ report/ @@ -10,4 +11,5 @@ report/ wav/ mp3/ bench/.DS_Store -databench/ \ No newline at end of file +databench/ +.DS_Store diff --git a/bench/.DS_Store b/bench/.DS_Store deleted file mode 100644 index 09b57b6..0000000 Binary files a/bench/.DS_Store and /dev/null differ diff --git a/bench/read/bench.py b/bench/read/bench.py deleted file mode 100644 index 5bdccf1..0000000 --- a/bench/read/bench.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 - -import os -import sys -import time -import argparse -import librosa -import numpy as np - -MIB_DIVISOR = 1024.0 * 1024.0 - - -def find_audio_files(root_dir, extension, max_files): - filepaths = [] - extension = extension.lower() - - count = 0 - try: - for dirpath, _, filenames in os.walk(root_dir, topdown=True, onerror=None): - relevant_filenames = [f for f in filenames if f.lower().endswith(extension)] - for filename in relevant_filenames: - if count < max_files: - filepaths.append(os.path.join(dirpath, filename)) - count += 1 - else: - return filepaths - if count >= max_files: - break - - except OSError: - pass - - return filepaths - - -def get_file_size(filename) -> float: - if not os.path.isfile(filename): - return -1.0 - size = os.path.getsize(filename) - if size <= 0: - return 0.0 - return size / MIB_DIVISOR - - -def benchmark_read(filename, target_sr) -> tuple[float, float]: - size = get_file_size(filename) - if size is None or size <= 0.0: - return 0.0, 0.0 - - sample_rate = target_sr if target_sr is not None and target_sr > 0 else None - - try: - start_time = time.perf_counter() - _audio, _sr = librosa.load( - filename, sr=sample_rate, mono=False, dtype=np.float32 - ) - end_time = time.perf_counter() - duration = end_time - start_time - if not isinstance(_audio, np.ndarray) or _audio.size == 0: - return -1.0, -1.0 - - return duration, size - except FileNotFoundError: - return -1.0, -1.0 - - -def run_benchmark(root_dir, sample_rate, extension, max_files): - all_files = find_audio_files(root_dir, extension, max_files) - nfound = len(all_files) - - if nfound == 0: - sys.exit(0) - - warmup_count = min(5, nfound) - if warmup_count > 0: - warmup_files = all_files[:warmup_count] - for f in warmup_files: - _ = benchmark_read(f, sample_rate) - - total_time = 0.0 - total_size = 0.0 - - files_to_process = all_files - - for filename in files_to_process: - duration, size = benchmark_read(filename, sample_rate) - - if duration > 0 and size > 0: - total_time += duration - total_size += size - - if total_time > 0 and total_size > 0: - avg_speed = total_size / total_time - print(f"{avg_speed:.5f}") - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("root_directory", help=argparse.SUPPRESS) - parser.add_argument("sample_rate", type=int, help=argparse.SUPPRESS) - parser.add_argument("format", help=argparse.SUPPRESS) - parser.add_argument("max_files", type=int, help=argparse.SUPPRESS) - - if len(sys.argv) != 5: - print( - f"Usage: {sys.argv[0]} ", - file=sys.stderr, - ) - sys.exit(1) - - args = parser.parse_args() - if not os.path.isdir(args.root_directory): - sys.exit(1) - - if args.sample_rate < 0: - sys.exit(1) - - if args.max_files <= 0: - sys.exit(1) - - ext = args.format.lstrip(".") - - run_benchmark(args.root_directory, args.sample_rate, ext, args.max_files) - - -if __name__ == "__main__": - main() diff --git a/bench/read/bench_read.sh b/bench/read/bench_read.sh deleted file mode 100755 index f5cb1fb..0000000 --- a/bench/read/bench_read.sh +++ /dev/null @@ -1,238 +0,0 @@ -#!/bin/bash - -OCAML_CMD="_build/default/bench/read/perf.exe" -PYTHON_CMD="python3 bench.py" - -if [[ "$OSTYPE" == "linux-gnu"* ]]; then - CACHE_CLEAR_CMD="sync; echo 3 > /proc/sys/vm/drop_caches" -elif [[ "$OSTYPE" == "darwin"* ]]; then - CACHE_CLEAR_CMD="purge" -else - CACHE_CLEAR_CMD="" -fi - -if [ "$#" -ne 6 ]; then - echo "Usage: $0 " - echo " : 'ocaml' or 'python'" - exit 1 -fi - -MODE=$1 -shift - -NUM_ITERATIONS=$1 -ROOT_DIR=$2 -SAMPLE_RATE=$3 -FORMAT=$4 -MAX_FILES=$5 - -if [[ "$MODE" != "ocaml" && "$MODE" != "python" ]]; then - echo "Error: must be either 'ocaml' or 'python'. You provided '$MODE'." - echo "Usage: $0 " - exit 1 -fi - -if ! [[ "$NUM_ITERATIONS" =~ ^[1-9][0-9]*$ ]]; then - echo "Error: ('$NUM_ITERATIONS') must be a positive integer." >&2 - exit 1 -fi - -BENCH_CMD="" -if [[ "$MODE" == "ocaml" ]]; then - BENCH_CMD="$OCAML_CMD" -elif [[ "$MODE" == "python" ]]; then - BENCH_CMD="$PYTHON_CMD" -fi - -results_array=() -memory_array=() -valid_run_count=0 - -echo "Starting reading test for: $MODE" -echo "Command to run: $BENCH_CMD \"$ROOT_DIR\" \"$SAMPLE_RATE\" \"$FORMAT\" \"$MAX_FILES\"" -echo "Number of iterations: $NUM_ITERATIONS" -echo "OS detected: $OSTYPE" -echo "--------------------------------------------------" - -for (( i=1; i<=NUM_ITERATIONS; i++ )); do - echo "Iteration $i / $NUM_ITERATIONS" - - # Clear cache based on OS - if [[ -n "$CACHE_CLEAR_CMD" ]]; then - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - if sudo bash -c "$CACHE_CLEAR_CMD"; then - sleep 1.5 - else - echo "Warning: Failed to clear cache on Linux. Continuing without cache clearing." >&2 - fi - elif [[ "$OSTYPE" == "darwin"* ]]; then - if sudo $CACHE_CLEAR_CMD; then - sleep 1.5 - else - echo "Warning: Failed to clear cache on macOS. Continuing without cache clearing." >&2 - fi - fi - else - echo "Warning: Cache clearing not supported on this OS. Continuing without cache clearing." >&2 - fi - - result=$( $BENCH_CMD "$ROOT_DIR" "$SAMPLE_RATE" "$FORMAT" "$MAX_FILES" ) - exit_status=$? - - if [ $exit_status -ne 0 ]; then - continue - fi - - if [[ ! "$result" =~ ^[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?$ ]]; then - continue - fi - - echo "Result: $result MiB/s" - - if [[ -n "$CACHE_CLEAR_CMD" ]]; then - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - sudo bash -c "$CACHE_CLEAR_CMD" >/dev/null 2>&1 - sleep 1.5 - elif [[ "$OSTYPE" == "darwin"* ]]; then - sudo $CACHE_CLEAR_CMD >/dev/null 2>&1 - sleep 1.5 - fi - fi - - temp_time_file=$(mktemp) - memory_mib="" - - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS: use /usr/bin/time with -l flag for memory stats - /usr/bin/time -l -o "$temp_time_file" $BENCH_CMD "$ROOT_DIR" "$SAMPLE_RATE" "$FORMAT" "$MAX_FILES" >/dev/null 2>&1 - memory_exit_status=$? - - if [ $memory_exit_status -eq 0 ] && [ -f "$temp_time_file" ]; then - memory_bytes=$(grep "maximum resident set size" "$temp_time_file" | awk '{print $1}') - if [[ "$memory_bytes" =~ ^[0-9]+$ ]]; then - memory_mib=$(echo "scale=6; $memory_bytes / 1048576" | bc -l) - fi - fi - elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - # Linux: use /usr/bin/time with -v flag for verbose memory stats - /usr/bin/time -v -o "$temp_time_file" $BENCH_CMD "$ROOT_DIR" "$SAMPLE_RATE" "$FORMAT" "$MAX_FILES" >/dev/null 2>&1 - memory_exit_status=$? - - if [ $memory_exit_status -eq 0 ] && [ -f "$temp_time_file" ]; then - memory_kb=$(grep "Maximum resident set size" "$temp_time_file" | awk '{print $NF}') - if [[ "$memory_kb" =~ ^[0-9]+$ ]]; then - memory_mib=$(echo "scale=6; $memory_kb / 1024" | bc -l) - fi - fi - fi - - rm -f "$temp_time_file" - - if [[ -n "$memory_mib" ]]; then - echo "Memory: $memory_mib MiB" - fi - - if [ $exit_status -ne 0 ]; then - continue - fi - - results_array+=("$result") - if [[ -n "$memory_mib" ]]; then - memory_array+=("$memory_mib") - fi - ((valid_run_count++)) - sleep 1 -done - -echo "--------------------------------------------------" - -num_results=${#results_array[@]} -num_memory_results=${#memory_array[@]} - -if [ "$num_results" -eq 0 ]; then - exit 1 -fi - -stats=$(printf "%s\n" "${results_array[@]}" | awk ' - NF == 0 { next } - { - if ($1 ~ /^[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?$/) { - sum += $1; - sumsq += $1*$1; - count++; - } - } - END { - if (count > 0) { - mean = sum / count; - if (count > 1) { - variance = (sumsq - (sum*sum)/count) / (count-1); - if (variance < 1e-12) variance = 0; - stdev = sqrt(variance); - } else { - stdev = 0; # Standard deviation is undefined/0 for a single point - } - printf "%.6f %.6f %d", mean, stdev, count; - } else { - print "NaN NaN 0"; - } - } -') -read -r mean stdev count <<< "$stats" - -if [ "$num_memory_results" -gt 0 ]; then - memory_stats=$(printf "%s\n" "${memory_array[@]}" | awk ' - NF == 0 { next } - { - if ($1 ~ /^[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?$/) { - sum += $1; - sumsq += $1*$1; - count++; - } - } - END { - if (count > 0) { - mean = sum / count; - if (count > 1) { - variance = (sumsq - (sum*sum)/count) / (count-1); - if (variance < 1e-12) variance = 0; - stdev = sqrt(variance); - } else { - stdev = 0; # Standard deviation is undefined/0 for a single point - } - printf "%.6f %.6f %d", mean, stdev, count; - } else { - print "NaN NaN 0"; - } - } - ') - read -r memory_mean memory_stdev memory_count <<< "$memory_stats" -fi - -if [[ -z "$mean" || -z "$stdev" || -z "$count" || "$count" -eq 0 ]]; then - echo "Error: Failed to calculate statistics. Awk output: '$stats'" >&2 - exit 1 -fi - -echo "Performance Test Summary ($MODE):" -echo "-------------------------" -echo "Command: $BENCH_CMD \"$ROOT_DIR\" \"$SAMPLE_RATE\" \"$FORMAT\" \"$MAX_FILES\"" -printf "Mean Speed (MiB/s): %.6f\n" "$mean" -if [ "$count" -gt 1 ]; then - printf "Std Dev Speed: %.6f\n" "$stdev" -else - printf "Std Dev Speed: N/A (requires >= 2 data points)\n" -fi - -if [ "$num_memory_results" -gt 0 ] && [[ -n "$memory_mean" && -n "$memory_stdev" && -n "$memory_count" && "$memory_count" -gt 0 ]]; then - printf "Mean Memory (MiB): %.6f\n" "$memory_mean" - if [ "$memory_count" -gt 1 ]; then - printf "Std Dev Memory: %.6f\n" "$memory_stdev" - else - printf "Std Dev Memory: N/A (requires >= 2 data points)\n" - fi -else - echo "Memory statistics: Not available (OS not supported or measurement failed)" -fi - -exit 0 diff --git a/bench/read/both.png b/bench/read/both.png deleted file mode 100644 index ef5c67a..0000000 Binary files a/bench/read/both.png and /dev/null differ diff --git a/bench/read/dune b/bench/read/dune deleted file mode 100644 index 22797c1..0000000 --- a/bench/read/dune +++ /dev/null @@ -1,3 +0,0 @@ -(executable - (name perf) - (libraries unix soundml)) diff --git a/bench/read/mp3.png b/bench/read/mp3.png deleted file mode 100644 index 1edbede..0000000 Binary files a/bench/read/mp3.png and /dev/null differ diff --git a/bench/read/perf.ml b/bench/read/perf.ml deleted file mode 100644 index 1753e06..0000000 --- a/bench/read/perf.ml +++ /dev/null @@ -1,125 +0,0 @@ -open Printf -open Unix - -let mb_divisor = 1024. *. 1024. - -let is_ext_file filename ext = - String.lowercase_ascii (Filename.extension filename) = ext - -let find_ext_files root_dir ext = - let rec find acc dir = - try - let dh = opendir dir in - try - let rec loop acc = - match readdir dh with - | exception End_of_file -> - closedir dh ; acc - | "." | ".." -> - loop acc - | entry -> ( - let full_path = Filename.concat dir entry in - try - match (stat full_path).st_kind with - | S_REG when is_ext_file full_path ext -> - loop (full_path :: acc) - | S_DIR -> - loop (find acc full_path) - | _ -> - loop acc - with Unix_error (_, _, _) -> loop acc ) - in - loop acc - with ex -> - closedir dh ; - eprintf "\nError reading directory '%s': %s\n%!" dir - (Printexc.to_string ex) ; - acc - with Unix_error (e, _, p) -> - eprintf "\nError opening directory '%s': %s\n%!" p (error_message e) ; - acc - in - find [] root_dir - -let get_file_size filename = - try - let stats = stat filename in - if stats.st_kind = S_REG then Ok (float_of_int stats.st_size /. mb_divisor) - else Error (sprintf "Not a regular file: %s" filename) - with - | Unix_error (e, _, _) -> - Error (sprintf "Cannot stat file '%s': %s" filename (error_message e)) - | Sys_error msg -> - Error (sprintf "System error statting '%s': %s" filename msg) - -let benchmark_read device kind filename sample_rate = - match get_file_size filename with - | Error msg -> - Error (filename, msg) - | Ok size_mb -> ( - if size_mb <= 0.0 then Error (filename, "Incorrect file size") - else - try - let res_typ = - match sample_rate with 0 -> Io.NONE | _ -> Io.SOXR_HQ - in - let start_time = Unix.gettimeofday () in - let _audio = - Soundml.Io.read ~res_typ ~sample_rate device kind filename - in - let end_time = Unix.gettimeofday () in - let duration = end_time -. start_time in - Ok (duration, size_mb) - with ex -> Error (filename, Printexc.to_string ex) ) - -let run_benchmark device root sample_rate extension max_files = - let kind = Rune.Float32 in - let all_files = find_ext_files root extension in - let all_files = - List.filteri (fun i _ -> if i >= max_files then false else true) all_files - in - let total_files = List.length all_files in - if total_files = 0 then exit 0 ; - let warmup_count = min 5 total_files in - ( if warmup_count > 0 then - let warmup_files = List.filteri (fun i _ -> i < warmup_count) all_files in - List.iter - (fun f -> - match benchmark_read device kind f sample_rate with - | Ok _ -> - () - | Error _ -> - () ) - warmup_files ) ; - let total_time = ref 0.0 in - let total_size = ref 0.0 in - List.iter - (fun filename -> - match benchmark_read device kind filename sample_rate with - | Ok (duration, size_mb) -> - total_time := !total_time +. duration ; - total_size := !total_size +. size_mb - | Error _ -> - () ) - all_files ; - if !total_time > 0.0 && !total_size > 0.0 then - let avg_speed = !total_size /. !total_time in - printf "%.5f\n" avg_speed - -let () = - if Array.length Sys.argv <> 5 then - eprintf "Usage: %s \n" - Sys.argv.(0) - else - let device = Rune.c in - let root_dir = Sys.argv.(1) in - let sample_rate = int_of_string Sys.argv.(2) in - let extension = Sys.argv.(3) in - let max_files = int_of_string Sys.argv.(4) in - if not (Sys.file_exists root_dir && Sys.is_directory root_dir) then - eprintf "Can't read directory: %s.\n" root_dir - else - try run_benchmark device root_dir sample_rate extension max_files - with ex -> - eprintf "An unexpected error occurred: %s\n" (Printexc.to_string ex) ; - exit 1 diff --git a/bench/read/wav.png b/bench/read/wav.png deleted file mode 100644 index 6f4922c..0000000 Binary files a/bench/read/wav.png and /dev/null differ diff --git a/bench/stft/.gitignore b/bench/stft/.gitignore deleted file mode 100644 index ed8ebf5..0000000 --- a/bench/stft/.gitignore +++ /dev/null @@ -1 +0,0 @@ -__pycache__ \ No newline at end of file diff --git a/bench/stft/dune b/bench/stft/dune deleted file mode 100644 index 44ecac1..0000000 --- a/bench/stft/dune +++ /dev/null @@ -1,3 +0,0 @@ -(executable - (name perf) - (libraries core core_bench soundml)) diff --git a/bench/stft/perf.ml b/bench/stft/perf.ml deleted file mode 100644 index 26a5d5e..0000000 --- a/bench/stft/perf.ml +++ /dev/null @@ -1,15 +0,0 @@ -open! Core -open! Core_bench -open Soundml - -let path = Sys_unix.getcwd () ^ "/bench/stft/wav_stereo_44100hz_1s.wav" - -let audio, _ = Io.read ~res_typ:Io.NONE Rune.c Rune.Float32 path - -let main () = - Command_unix.run - (Bench.make_command - [ Bench.Test.create ~name:"float32" (fun () -> - ignore (Transform.stft ~n_fft:2048 ~hop_length:512 audio) ) ] ) - -let () = main () diff --git a/bench/stft/perf.py b/bench/stft/perf.py deleted file mode 100644 index a3c0780..0000000 --- a/bench/stft/perf.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest -import librosa -import numpy as np -import os - -AUDIO_FILE_PATH = os.path.join(os.getcwd(), "bench/stft/wav_stereo_44100hz_1s.wav") - -STFT_CONFIGURATIONS = [ - {"n_fft": 2048, "win_length": 2048, "hop_length": 512, "window_type": "hann"} -] - -y_f32, sr_f32 = librosa.load(AUDIO_FILE_PATH, sr=None, mono=True, dtype=np.float32) -y_f64, sr_f64 = librosa.load(AUDIO_FILE_PATH, sr=None, mono=True, dtype=np.float64) -SIGNAL_LENGTH = len(y_f32) - -@pytest.mark.parametrize("config", STFT_CONFIGURATIONS) -@pytest.mark.parametrize("precision", ["float32", "float64"]) -def test_librosa_stft(benchmark, config, precision): - if precision == "float32": - audio_data = y_f32 - elif precision == "float64": - audio_data = y_f64 - else: - raise ValueError("Invalid precision") - - result = benchmark( - librosa.stft, - y=audio_data, - n_fft=config["n_fft"], - hop_length=config["hop_length"], - win_length=config["win_length"], - window=config["window_type"], - center=False - ) - assert result is not None diff --git a/bench/stft/stft_comparison_float32.svg b/bench/stft/stft_comparison_float32.svg deleted file mode 100644 index 4325132..0000000 --- a/bench/stft/stft_comparison_float32.svg +++ /dev/null @@ -1,302 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/bench/stft/stft_comparison_float64.svg b/bench/stft/stft_comparison_float64.svg deleted file mode 100644 index 371921c..0000000 --- a/bench/stft/stft_comparison_float64.svg +++ /dev/null @@ -1,289 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/bench/stft/wav_stereo_44100hz_1s.wav b/bench/stft/wav_stereo_44100hz_1s.wav deleted file mode 100644 index ee2ad04..0000000 Binary files a/bench/stft/wav_stereo_44100hz_1s.wav and /dev/null differ diff --git a/dune-project b/dune-project index 318922c..a1791d8 100644 --- a/dune-project +++ b/dune-project @@ -1,4 +1,4 @@ -(lang dune 3.18) +(lang dune 3.17) (name soundml) @@ -15,6 +15,14 @@ (documentation https://soundml.dev) +(pin + (url "git+https://github.com/raven-ml/raven.git") + (package (name nx))) + +(pin + (url "git+https://github.com/raven-ml/raven.git") + (package (name rune))) + (package (name soundml) (synopsis "An OCaml library to embed sound processing in your applications") @@ -22,7 +30,7 @@ "SoundML is a library built on top of Rune to analyse sounds files. It can read, write audio, extract various features from audio files and much more.") (depends (ocaml - (>= 5.2.0)) + (>= 5.3.0)) dune (conf-sndfile :build) (conf-rubberband :build) diff --git a/soundml.opam b/soundml.opam index fedd962..9af38c7 100644 --- a/soundml.opam +++ b/soundml.opam @@ -11,8 +11,8 @@ homepage: "https://github.com/gabyfle/SoundML" doc: "https://soundml.dev" bug-reports: "https://github.com/gabyfle/SoundML/issues" depends: [ - "ocaml" {>= "5.2.0"} - "dune" {>= "3.18"} + "ocaml" {>= "5.3.0"} + "dune" {>= "3.17"} "conf-sndfile" {build} "conf-rubberband" {build} "conf-samplerate" {build} @@ -38,4 +38,3 @@ build: [ ] ] dev-repo: "git+https://github.com/gabyfle/SoundML.git" -x-maintenance-intent: ["(latest)"] diff --git a/src/transform.ml b/src/transform.ml index 18a8b01..0c56846 100644 --- a/src/transform.ml +++ b/src/transform.ml @@ -23,25 +23,27 @@ let stft ?(window = `Hanning) ?(win_length = 2048) ?(center = true) ~n_fft ~hop_length (x : (float, 'a, 'dev) Rune.t) = (* Input validation *) if n_fft <= 0 then invalid_arg "n_fft must be positive" ; - if hop_length <= 0 then invalid_arg "hop_size must be positive" ; + if hop_length <= 0 then invalid_arg "hop_length must be positive" ; if win_length <= 0 then invalid_arg "win_length must be positive" ; if win_length > n_fft then invalid_arg "win_length cannot be larger than n_fft" ; let device = Rune.device x in - let fft_window = - Window.get window device (Rune.dtype x) ~fftbins:true win_length - in + let dtype = Rune.dtype x in + (* 1. Create window and pad it if necessary to n_fft *) + let fft_window = Window.get window device dtype ~fftbins:true win_length in let fft_window = if win_length < n_fft then - Utils.pad_center fft_window ~size:n_fft ~pad_value:0.0 + let pad_width = (n_fft - win_length) / 2 in + Rune.pad [|(pad_width, n_fft - win_length - pad_width)|] 0.0 fft_window else fft_window in + (* 2. Pad the input signal for centering *) let x_padded = if center then ( - let x_shape = Rune.shape x in let pad_width = n_fft / 2 in - let padding = Array.make (Array.length x_shape) (0, 0) in - padding.(Array.length x_shape - 1) <- (pad_width, pad_width) ; + let rank = Rune.ndim x in + let padding = Array.make rank (0, 0) in + padding.(rank - 1) <- (pad_width, pad_width) ; Rune.pad padding 0.0 x ) else x in @@ -51,14 +53,32 @@ let stft ?(window = `Hanning) ?(win_length = 2048) ?(center = true) ~n_fft invalid_arg (Printf.sprintf "n_fft=%d is too large for input signal of length=%d" n_fft signal_length ) ; - let y_frames = - Utils.frame ~axis:(-1) ~frame_length:n_fft ~hop_length x_padded + (* 3. Calculate number of frames *) + let num_frames = 1 + ((signal_length - n_fft) / hop_length) in + let frame_indices = Rune.arange device Rune.int32 0 num_frames 1 in + (* 4. Define the function to be vmapped *) + let process_single_frame frame_index = + let start = Rune.mul_s frame_index (Int32.of_int hop_length) in + let start_reshaped = Rune.unsqueeze ~axes:[|1|] start in + let offsets = Rune.arange device Rune.int32 0 n_fft 1 in + let indices = Rune.add start_reshaped offsets in + (* Dynamic slice using take. Assuming take flattens the taken dimensions. *) + let frame_flat = Rune.take ~axis:(-1) indices x_padded in + (* Reshape back to (..., num_frames, n_fft) using -1 for num_frames *) + let x_rank = Rune.ndim x_padded in + let other_dims = Array.sub (Rune.shape x_padded) 0 (x_rank - 1) in + let target_shape = Array.append other_dims [|-1; n_fft|] in + let frame = Rune.reshape target_shape frame_flat in + (* Apply window. *) + let windowed_frame = Rune.mul frame fft_window in + (* FFT on the last axis *) + Rune.rfft ~axis:(-1) windowed_frame in - let frames_shape = Rune.shape y_frames in - let frames_ndim = Array.length frames_shape in - let window_shape = Array.make frames_ndim 1 in - window_shape.(frames_ndim - 2) <- n_fft ; - let fft_window_reshaped = Rune.reshape window_shape fft_window in - let windowed_frames = Rune.mul y_frames fft_window_reshaped in - let stft_result = Rune.rfft ~axis:(-2) windowed_frames in - stft_result + (* 5. vmap the function over the frame indices *) + let stft_matrix = Rune.vmap process_single_frame frame_indices in + (* 6. Transpose to (..., freq, time) by moving the first axis (frames) to the + end *) + let rank = Rune.ndim stft_matrix in + let axes = Array.init rank (fun i -> if i = rank - 1 then 0 else i + 1) in + let stft_transposed = Rune.transpose ~axes stft_matrix in + stft_transposed diff --git a/src/utils.ml b/src/utils.ml index 51dabbe..e213cf1 100644 --- a/src/utils.ml +++ b/src/utils.ml @@ -153,80 +153,6 @@ let outer op x y = (* Apply the operation - broadcasting will handle the rest *) op x_reshaped y_reshaped -let frame ?(axis = -1) (x : ('a, 'b, 'dev) Rune.t) ~frame_length ~hop_length : - ('a, 'b, 'dev) Rune.t = - if frame_length <= 0 then - raise (Invalid_argument "frame_length must be positive") ; - if hop_length < 1 then - raise (Invalid_argument (Printf.sprintf "Invalid hop_length: %d" hop_length)) ; - let device = Rune.device x in - let x = Rune.to_nx x in - let ndim = Nx.ndim x in - let shape = Nx.shape x in - let axis_resolved = if axis < 0 then ndim + axis else axis in - if axis_resolved < 0 || axis_resolved >= ndim then - raise (Invalid_argument "axis out of bounds") ; - if shape.(axis_resolved) < frame_length then - raise - (Invalid_argument - (Printf.sprintf "Input is too short (n=%d) for frame_length=%d" - shape.(axis_resolved) frame_length ) ) ; - (* Ensure we have a contiguous array for as_strided to work correctly *) - let x_contiguous = if Nx.is_c_contiguous x then x else Nx.copy x in - (* Calculate the number of frames *) - let n_frames = ((shape.(axis_resolved) - frame_length) / hop_length) + 1 in - let out_shape = - let shape_arr = Array.make (ndim + 1) 0 in - for i = 0 to axis_resolved - 1 do - shape_arr.(i) <- shape.(i) - done ; - if axis < 0 then ( - (* Negative axes: frame_length, then n_frames *) - shape_arr.(axis_resolved) <- frame_length ; - shape_arr.(axis_resolved + 1) <- n_frames ) - else ( - (* Positive axes: n_frames, then frame_length *) - shape_arr.(axis_resolved) <- n_frames ; - shape_arr.(axis_resolved + 1) <- frame_length ) ; - for i = axis_resolved + 1 to ndim - 1 do - shape_arr.(i + 1) <- shape.(i) - done ; - shape_arr - in - let x_strides = Nx.strides x_contiguous in - let itemsize = Nx.itemsize x_contiguous in - let out_strides = Array.make (Array.length out_shape) 0 in - if axis < 0 then ( - (* Copy strides for dimensions before the framed axis *) - for i = 0 to axis_resolved - 1 do - out_strides.(i) <- x_strides.(i) / itemsize - done ; - (* Frame dimension stride: 1 element along the framed axis *) - out_strides.(axis_resolved) <- x_strides.(axis_resolved) / itemsize ; - (* Hop dimension stride: hop_length elements along the framed axis *) - out_strides.(axis_resolved + 1) <- - x_strides.(axis_resolved) / itemsize * hop_length ; - (* Copy remaining strides (if any) *) - for i = axis_resolved + 1 to ndim - 1 do - out_strides.(i + 1) <- x_strides.(i) / itemsize - done ) - else ( - (* Positive axes: [n_frames, frame_length] order *) - for i = 0 to axis_resolved - 1 do - out_strides.(i) <- x_strides.(i) / itemsize - done ; - (* Hop dimension stride: hop_length elements along the framed axis *) - out_strides.(axis_resolved) <- - x_strides.(axis_resolved) / itemsize * hop_length ; - (* Frame dimension stride: 1 element along the framed axis *) - out_strides.(axis_resolved + 1) <- x_strides.(axis_resolved) / itemsize ; - (* Copy remaining strides *) - for i = axis_resolved + 1 to ndim - 1 do - out_strides.(i + 1) <- x_strides.(i) / itemsize - done ) ; - let strided = Nx.as_strided out_shape out_strides ~offset:0 x_contiguous in - Rune.of_nx device strided - let pad_center signal ~size ~pad_value = let signal_shape = Rune.shape signal in let signal_length = signal_shape.(0) in diff --git a/src/utils.mli b/src/utils.mli index df8be28..c4e9195 100644 --- a/src/utils.mli +++ b/src/utils.mli @@ -27,59 +27,6 @@ (** {2 Signal Framing and Windowing} *) -val frame : - ?axis:int - -> ('a, 'b, 'dev) Rune.t - -> frame_length:int - -> hop_length:int - -> ('a, 'b, 'dev) Rune.t -(** [frame signal ~frame_length ~hop_length] slices a signal into overlapping frames. - - This function creates overlapping windows of the input signal, which is essential - for Short-Time Fourier Transform (STFT) and other windowed analysis techniques. - The implementation uses efficient stride manipulation to avoid copying data. - - @param axis The axis along which to frame (default: -1, last axis) - @param signal Input signal to frame - @param frame_length Length of each frame in samples (must be > 0) - @param hop_length Number of samples to advance between frames (must be >= 1) - @return Framed view of the input signal with one additional dimension - - @raise Stdlib.Invalid_argument if frame_length <= 0 - @raise Stdlib.Invalid_argument if hop_length < 1 - @raise Stdlib.Invalid_argument if signal is too short for even one frame - @raise Stdlib.Invalid_argument if axis is out of bounds - - {3 Examples} - - Basic signal framing: - {[ - let signal = Rune.arange Rune.float32 0.0 10.0 1.0 in - let frames = Utils.frame signal ~frame_length:3 ~hop_length:2 in - (* frames has shape [3; 4] with overlapping windows *) - ]} - - Frame a stereo signal: - {[ - let stereo = Rune.ones Rune.float32 [|2; 1000|] in - let frames = Utils.frame stereo ~frame_length:512 ~hop_length:256 in - (* frames has shape [2; 512; 3] - frames along last axis *) - ]} - - Frame along first axis: - {[ - let signal = Rune.arange Rune.float32 0.0 100.0 1.0 in - let frames = Utils.frame ~axis:0 signal ~frame_length:10 ~hop_length:5 in - (* frames along first dimension *) - ]} - - {3 Performance Notes} - - - Creates a view of the original data, not a copy (O(1) memory) - - The returned tensor shares memory with the input - - Modifications to frames affect the original signal - - Most efficient when frame_length and hop_length are powers of 2 *) - val pad_center : ('a, 'b, 'dev) Rune.t -> size:int -> pad_value:'a -> ('a, 'b, 'dev) Rune.t (** [pad_center signal ~size ~pad_value] pads a signal to center it. diff --git a/test/dune b/test/dune index 33b509d..9134e32 100644 --- a/test/dune +++ b/test/dune @@ -14,18 +14,18 @@ (action (run %{test}))) -(tests - (names test_stft) - (libraries alcotest str yojson nx rune nx.io soundml) - (package soundml) - (deps - generate_vectors.py - generate_audio.sh - (source_tree audio)) - (action - (progn - (system "mkdir -p audio") - (system "bash %{dep:generate_audio.sh}") - (system "mkdir -p vectors") - (system "python3 %{dep:generate_vectors.py}") - (run %{test})))) +;(tests +; (names test_stft) +; (libraries alcotest str yojson nx rune nx.io soundml) +; (package soundml) +; (deps +; generate_vectors.py +; generate_audio.sh +; (source_tree audio)) +; (action +; (progn +; (system "mkdir -p audio") +; (system "bash %{dep:generate_audio.sh}") +; (system "mkdir -p vectors") +; (system "python3 %{dep:generate_vectors.py}") +; (run %{test})))) diff --git a/test/test_utils.ml b/test/test_utils.ml index 1272229..a652a47 100644 --- a/test/test_utils.ml +++ b/test/test_utils.ml @@ -19,15 +19,6 @@ (* *) (*****************************************************************************) -(* Helper function to replace slice_ranges *) -let slice_ranges starts stops tensor = - let indices = - Array.map2 - (fun start stop -> Rune.R (start, stop)) - (Array.of_list starts) (Array.of_list stops) - in - Rune.slice (Array.to_list indices) tensor - open Soundml type data = (float, Rune.float32_elt, [`c]) Rune.t @@ -316,28 +307,6 @@ module Test_melfreq = struct ; Alcotest.test_case "custom" `Quick test_custom ] end -(* module Test_unwrap = struct let test_1d () = let p = Rune.create Rune.c - Rune.float32 [|8|] [|0.; 0.1; 0.2; 5.0; 5.1; 5.2; -0.1; -0.2|] in let - expected = Rune.create Rune.c Rune.float32 [|8|] [| 0. ; 0.1 ; 0.2 ; - -1.283185 ; -1.1831851 ; -1.0831852 ; -0.09999905 ; -0.19999905 |] in let - actual = Utils.unwrap p in Alcotest.check data_testable "unwrap_1d" expected - actual - - let test_2d_axis0 () = let p = Rune.create Rune.c Rune.float32 [|2; 3|] [|0.; - 0.1; 6.2; 0.; 0.1; 6.2|] in let expected = Rune.create Rune.c Rune.float32 - [|2; 3|] [|0.; 0.1; 6.2; 0.; 0.1; 6.2|] in let actual = Utils.unwrap ~axis:0 - p in Alcotest.check data_testable "unwrap_2d_axis0" expected actual - - let test_2d_axis1 () = let p = Rune.create Rune.c Rune.float32 [|2; 3|] [|0.; - 0.1; 6.2; 0.; 0.1; 6.2|] in let expected = Rune.create Rune.c Rune.float32 - [|2; 3|] [|0.; 0.1; -0.08318615; 0.; 0.1; -0.08318615|] in let actual = - Utils.unwrap ~axis:1 p in Alcotest.check data_testable "unwrap_2d_axis1" - expected actual - - let suite = [ Alcotest.test_case "1d" `Quick test_1d ; Alcotest.test_case - "2d_axis0" `Quick test_2d_axis0 ; Alcotest.test_case "2d_axis1" `Quick - test_2d_axis1 ] end*) - module Test_outer = struct let test_add () = let x = Rune.create Rune.c Rune.float32 [|3|] [|1.; 2.; 3.|] in @@ -471,236 +440,9 @@ module Test_convert = struct ; Alcotest.test_case "db_to_power" `Quick test_db_to_power ] end -module Test_frame = struct - (* Helper function to create random data for testing *) - let create_random_data shape seed = - Random.init seed ; - let size = Array.fold_left ( * ) 1 shape in - let data = Array.init size (fun _ -> Random.float 2.0 -. 1.0) in - Rune.create Rune.c Rune.float32 shape data - - (* Test 1D framing with parametrized frame_length, hop_length, and axis *) - let test_frame1d frame_length hop_length axis () = - let y = create_random_data [|32|] 42 in - let y_frame = Utils.frame y ~frame_length ~hop_length ~axis in - let y_frame_adj = if axis = -1 then Rune.transpose y_frame else y_frame in - let num_frames = (Rune.shape y_frame_adj).(0) in - for i = 0 to num_frames - 1 do - let frame_i = Rune.get [i] y_frame_adj in - let start_idx = i * hop_length in - let end_idx = min (start_idx + frame_length) 32 in - let expected_slice = slice_ranges [start_idx] [end_idx] y in - Alcotest.check data_testable - (Printf.sprintf "frame1d_%d_%d_%d_frame_%d" frame_length hop_length axis - i ) - expected_slice frame_i - done - - (* Test 2D framing with parametrized frame_length, hop_length, axis, and array - order *) - let test_frame2d frame_length hop_length axis is_fortran_order () = - let y_base = create_random_data [|16; 32|] 123 in - let y = - if is_fortran_order then - (* Simulate Fortran order by transposing *) - Rune.transpose y_base - else y_base - in - let y_frame = Utils.frame y ~frame_length ~hop_length ~axis in - let y_frame_adj, y_adj = - if axis = -1 then (Rune.transpose y_frame, Rune.transpose y) - else (y_frame, y) - in - let num_frames = (Rune.shape y_frame_adj).(0) in - for i = 0 to num_frames - 1 do - let frame_i = Rune.get [i] y_frame_adj in - let start_idx = i * hop_length in - let end_idx = min (start_idx + frame_length) (Rune.shape y_adj).(0) in - let expected_slice = slice_ranges [start_idx] [end_idx] y_adj in - Alcotest.check data_testable - (Printf.sprintf "frame2d_%d_%d_%d_%b_frame_%d" frame_length hop_length - axis is_fortran_order i ) - expected_slice frame_i - done - - (* Test framing with 0-stride (padding) *) - let test_frame_0stride () = - let x = Rune.arange Rune.c Rune.float32 0 10 1 in - let xpad = Rune.expand [|1; 10|] x in - let xpad2 = Rune.reshape [|1; 10|] x in - let xf = Utils.frame x ~frame_length:3 ~hop_length:1 in - let xfpad = Utils.frame xpad ~frame_length:3 ~hop_length:1 in - let xfpad2 = Utils.frame xpad2 ~frame_length:3 ~hop_length:1 in - (* Check that shapes are correctly different due to extra dimensions *) - let xf_shape = Rune.shape xf in - let xfpad_shape = Rune.shape xfpad in - let xfpad2_shape = Rune.shape xfpad2 in - (* xf should be [3; 8] for axis=-1 *) - Alcotest.check (Alcotest.array Alcotest.int) "xf_shape" [|3; 8|] xf_shape ; - (* xfpad should be [1; 3; 8] - preserving the extra dimension *) - Alcotest.check - (Alcotest.array Alcotest.int) - "xfpad_shape" [|1; 3; 8|] xfpad_shape ; - (* xfpad2 should be same as xfpad *) - Alcotest.check - (Alcotest.array Alcotest.int) - "xfpad2_shape" [|1; 3; 8|] xfpad2_shape ; - (* Check that the core data is the same by comparing xf with xfpad[0] *) - let xfpad_squeezed = Rune.get [0] xfpad in - Alcotest.check data_testable "frame_0stride_xf_vs_xfpad_data" xf - xfpad_squeezed ; - let xfpad2_squeezed = Rune.get [0] xfpad2 in - Alcotest.check data_testable "frame_0stride_xf_vs_xfpad2_data" xf - xfpad2_squeezed - - (* Test high-dimensional framing *) - let test_frame_highdim frame_length hop_length ndim () = - let shape = Array.make ndim 20 in - let x = create_random_data shape 456 in - let xf = Utils.frame x ~frame_length ~hop_length in - let first_dim_size = (Rune.shape x).(0) in - for i = 0 to first_dim_size - 1 do - let x_i = Rune.get [i] x in - let xf0 = Utils.frame x_i ~frame_length ~hop_length in - let xf_i = Rune.get [i] xf in - Alcotest.check data_testable - (Printf.sprintf "frame_highdim_%d_%d_%d_slice_%d" frame_length - hop_length ndim i ) - xf0 xf_i - done - - (* Test target axis framing *) - let test_frame_targetaxis in_shape axis expected_out_shape () = - let x = Rune.zeros Rune.c Rune.float32 in_shape in - let xf = Utils.frame x ~frame_length:10 ~hop_length:2 ~axis in - let actual_shape = Rune.shape xf in - Alcotest.check - (Alcotest.array Alcotest.int) - "frame_targetaxis_shape" expected_out_shape actual_shape - - (* Test error cases *) - let test_frame_too_short axis () = - let x = Rune.arange Rune.c Rune.float32 0 16 1 in - let expected_exn = - Invalid_argument "Input is too short (n=16) for frame_length=17" - in - Alcotest.check_raises "frame_too_short" expected_exn (fun () -> - ignore (Utils.frame x ~frame_length:17 ~hop_length:1 ~axis) ) - - let test_frame_bad_hop () = - let x = Rune.arange Rune.c Rune.float32 0 16 1 in - let expected_exn = Invalid_argument "Invalid hop_length: 0" in - Alcotest.check_raises "frame_bad_hop" expected_exn (fun () -> - ignore (Utils.frame x ~frame_length:4 ~hop_length:0) ) - - (* Generate parametrized test cases *) - let frame1d_tests = - let frame_lengths = [4; 8] in - let hop_lengths = [2; 4] in - let axes = [0; -1] in - List.fold_left - (fun acc frame_length -> - List.fold_left - (fun acc hop_length -> - List.fold_left - (fun acc axis -> - let test_name = - Printf.sprintf "frame1d_fl%d_hl%d_ax%d" frame_length - hop_length axis - in - (test_name, `Quick, test_frame1d frame_length hop_length axis) - :: acc ) - acc axes ) - acc hop_lengths ) - [] frame_lengths - - let frame2d_tests = - let frame_lengths = [4; 8] in - let hop_lengths = [2; 4] in - let test_cases = - [(-1, true); (* Fortran-like order *) (0, false) (* C order *)] - in - List.fold_left - (fun acc frame_length -> - List.fold_left - (fun acc hop_length -> - List.fold_left - (fun acc (axis, fortran_order) -> - let test_name = - Printf.sprintf "frame2d_fl%d_hl%d_ax%d_%s" frame_length - hop_length axis - (if fortran_order then "fortran" else "c") - in - ( test_name - , `Quick - , test_frame2d frame_length hop_length axis fortran_order ) - :: acc ) - acc test_cases ) - acc hop_lengths ) - [] frame_lengths - - let frame_highdim_tests = - let frame_lengths = [5; 10] in - let hop_lengths = [1; 2] in - let ndims = [2; 3; 4; 5] in - List.fold_left - (fun acc frame_length -> - List.fold_left - (fun acc hop_length -> - List.fold_left - (fun acc ndim -> - let test_name = - Printf.sprintf "frame_highdim_fl%d_hl%d_nd%d" frame_length - hop_length ndim - in - ( test_name - , `Quick - , test_frame_highdim frame_length hop_length ndim ) - :: acc ) - acc ndims ) - acc hop_lengths ) - [] frame_lengths - - let frame_targetaxis_tests = - let test_cases = - [ ([|20; 20; 20; 20|], 0, [|6; 10; 20; 20; 20|]) - ; ([|20; 20; 20; 20|], 1, [|20; 6; 10; 20; 20|]) - ; ([|20; 20; 20; 20|], 2, [|20; 20; 6; 10; 20|]) - ; ([|20; 20; 20; 20|], 3, [|20; 20; 20; 6; 10|]) - ; ([|20; 20; 20; 20|], -1, [|20; 20; 20; 10; 6|]) - ; ([|20; 20; 20; 20|], -2, [|20; 20; 10; 6; 20|]) - ; ([|20; 20; 20; 20|], -3, [|20; 10; 6; 20; 20|]) - ; ([|20; 20; 20; 20|], -4, [|10; 6; 20; 20; 20|]) ] - in - List.mapi - (fun i (in_shape, axis, out_shape) -> - let test_name = Printf.sprintf "frame_targetaxis_%d" i in - (test_name, `Quick, test_frame_targetaxis in_shape axis out_shape) ) - test_cases - - let frame_error_tests = - let axes = [0; -1] in - List.mapi - (fun _ axis -> - let test_name = Printf.sprintf "frame_too_short_ax%d" axis in - (test_name, `Quick, test_frame_too_short axis) ) - axes - - let suite = - List.flatten - [ frame1d_tests - ; frame2d_tests - ; [("frame_0stride", `Quick, test_frame_0stride)] - ; frame_highdim_tests - ; frame_targetaxis_tests - ; [("frame_bad_hop", `Quick, test_frame_bad_hop)] @ frame_error_tests ] -end - let () = Alcotest.run "SoundML Utils Tests" [ ("Pad Center", Test_pad_center.suite) ; ("Mel Frequencies", Test_melfreq.suite) - ; (*"Unwrap", Test_unwrap.suite*) - ("Outer", Test_outer.suite) - ; ("Conversions", Test_convert.suite) - ; ("Frame", Test_frame.suite) ] + ; ("Outer", Test_outer.suite) + ; ("Conversions", Test_convert.suite) ]