diff --git a/.gitattributes b/.gitattributes index 1100936..7a88265 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ * text=auto eol=lf +*.bat text eol=crlf # Use Git LFS for binary files *.wav filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 3b6a86f..61a34eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.gradle/ .vs/ build/ -*.user \ No newline at end of file +target/ +*.user +*.profraw diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..943f0cb Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f398c33 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..65dcd68 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/rhubarb/Cargo.lock b/rhubarb/Cargo.lock new file mode 100644 index 0000000..71c0367 --- /dev/null +++ b/rhubarb/Cargo.lock @@ -0,0 +1,329 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dyn-clone" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b0705efd4599c15a38151f4721f7bc388306f61084d3bfd50bd07fbca5cb60" + +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-macro" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rhubarb-audio" +version = "0.1.0" +dependencies = [ + "assert_matches", + "dyn-clone", + "log", + "rstest", + "speculoos", +] + +[[package]] +name = "rstest" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c9dc66cc29792b663ffb5269be669f1613664e69ad56441fdb895c2347b930" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5015e68a0685a95ade3eee617ff7101ab6a3fc689203101ca16ebc16f2b89c66" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "speculoos" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f81684bbc3005f83b5c0a9e03eb52c8257b15370d62dcedf548964d5bfae2d" +dependencies = [ + "num", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" diff --git a/rhubarb/Cargo.toml b/rhubarb/Cargo.toml new file mode 100644 index 0000000..96a0082 --- /dev/null +++ b/rhubarb/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "rhubarb-audio", +] diff --git a/rhubarb/rhubarb-audio/Cargo.toml b/rhubarb/rhubarb-audio/Cargo.toml new file mode 100644 index 0000000..ec5cd42 --- /dev/null +++ b/rhubarb/rhubarb-audio/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rhubarb-audio" +version = "0.1.0" +edition = "2021" + +[dependencies] +dyn-clone = "1.0.10" +log = "0.4.17" + +[dev-dependencies] +assert_matches = "1.5.0" +rstest = "0.15.0" +speculoos = "0.10.0" diff --git a/rhubarb/rhubarb-audio/build.gradle.kts b/rhubarb/rhubarb-audio/build.gradle.kts new file mode 100644 index 0000000..d45f03a --- /dev/null +++ b/rhubarb/rhubarb-audio/build.gradle.kts @@ -0,0 +1,29 @@ +tasks.register("testCoverage") { + group = "Rhubarb" + doLast { + val environmentVariables = mapOf( + "RUSTFLAGS" to "-Cinstrument-coverage", + "LLVM_PROFILE_FILE" to File(project.projectDir, "rhubarb-audio-%p-%m.profraw").path, + ) + project.exec { + commandLine = listOf("cargo", "build") + environment(environmentVariables) + } + project.exec { + commandLine = listOf("cargo", "test") + environment(environmentVariables) + } + project.exec { + commandLine = listOf( + "grcov", + "--source-dir", ".", + "--binary-path", "../target/debug/deps", + "--output-type", "html", + "--branch", "--ignore-not-existing", + "--output-path", "../target/debug/coverage", + ".", + ) + } + project.delete(fileTree(project.projectDir) { include("*.profraw") }) + } +} diff --git a/rhubarb/rhubarb-audio/src/audio_clip.rs b/rhubarb/rhubarb-audio/src/audio_clip.rs new file mode 100644 index 0000000..e73170d --- /dev/null +++ b/rhubarb/rhubarb-audio/src/audio_clip.rs @@ -0,0 +1,179 @@ +use crate::audio_error::AudioError; +use dyn_clone::DynClone; +use std::fmt::Debug; +use std::time::Duration; + +const NANOS_PER_SEC: u32 = 1_000_000_000; + +/// An audio clip containing monaural sampled audio. +/// +/// Structs implementing this trait may read the audio data from disk, keep it in memory, or +/// generate it on the fly. +pub trait AudioClip: DynClone + Debug { + /// The number of audio frames in the audio clip. + fn len(&self) -> u64; + + /// The sampling rate in frames per second. + fn sampling_rate(&self) -> u32; + + /// Creates a new sample reader for reading from this audio clip. + fn create_sample_reader(&self) -> Result, AudioError>; + + /// Returns the duration of this audio clip. + fn duration(&self) -> Duration { + Duration::from_nanos( + (u128::from(self.len()) * u128::from(NANOS_PER_SEC) / u128::from(self.sampling_rate())) + as u64, + ) + } + + /// Indicates whether this audio clip is empty, that is, contains zero samples. + fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +/// Allows seeking within an [AudioClip] and reading its samples. +pub trait SampleReader: Debug { + /// The number of audio frames in the associated audio clip. + fn len(&self) -> u64; + + /// The current read position in frames. + fn position(&self) -> u64; + + /// Seeks to the specified position in frames. + /// + /// *Performance note:* Implementers of source sample readers should make sure that this method + /// doesn't perform any time-consuming work such as reading from the disk. This allows + /// downstream sample readers to unconditionally call `seek` upstream without worrying about + /// performance. + fn set_position(&mut self, position: u64); + + /// Attempts to read samples until the given buffer is full. + /// Errors if there are not enough samples left to fill the buffer. + fn read(&mut self, buffer: &mut [Sample]) -> Result<(), AudioError>; + + /// Indicates whether the associated audio clip is empty, that is, contains zero samples. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// The number of remaining samples before the end of the audio clip is reached. + fn remainder(&self) -> u64 { + self.len() - self.position() + } +} + +/// An audio sample in the range [-1, 1]. +pub type Sample = f32; + +#[cfg(test)] +mod tests { + use super::*; + use rstest::*; + use speculoos::prelude::*; + + mod audio_clip { + use super::*; + + #[derive(Clone, Debug)] + struct MockAudioClip { + len: u64, + sampling_rate: u32, + } + + impl AudioClip for MockAudioClip { + fn len(&self) -> u64 { + self.len + } + + fn sampling_rate(&self) -> u32 { + self.sampling_rate + } + + fn create_sample_reader(&self) -> Result, AudioError> { + todo!() + } + } + + #[rstest] + fn provides_duration() { + let mut clip = MockAudioClip { + len: 0, + sampling_rate: 44100, + }; + assert_that!(clip.duration()).is_equal_to(Duration::ZERO); + + clip.len = 4410; + assert_that!(clip.duration()).is_equal_to(Duration::from_millis(100)); + + clip.len = 1; + clip.sampling_rate = 1_000_000; + assert_that!(clip.duration()).is_equal_to(Duration::from_micros(1)); + } + + #[rstest] + fn provides_is_empty() { + let mut clip = MockAudioClip { + len: 0, + sampling_rate: 44100, + }; + assert_that!(clip.is_empty()).is_true(); + + clip.len = 1; + assert_that!(clip.is_empty()).is_false(); + + clip.sampling_rate = u32::MAX; + assert_that!(clip.is_empty()).is_false(); + } + } + + mod sample_reader { + use super::*; + + #[derive(Debug)] + struct MockSampleReader { + pos: u64, + len: u64, + } + + impl SampleReader for MockSampleReader { + fn len(&self) -> u64 { + self.len + } + + fn position(&self) -> u64 { + self.pos + } + + fn set_position(&mut self, _position: u64) { + todo!() + } + + fn read(&mut self, _buffer: &mut [Sample]) -> Result<(), AudioError> { + todo!() + } + } + + #[rstest] + fn provides_is_empty() { + let mut sample_reader = MockSampleReader { pos: 0, len: 0 }; + assert_that!(sample_reader.is_empty()).is_true(); + + sample_reader.len = 1; + assert_that!(sample_reader.is_empty()).is_false(); + } + + #[rstest] + fn provides_remainder() { + let mut sample_reader = MockSampleReader { pos: 0, len: 0 }; + assert_that!(sample_reader.remainder()).is_equal_to(0); + + sample_reader.len = 10; + assert_that!(sample_reader.remainder()).is_equal_to(10); + + sample_reader.pos = 8; + assert_that!(sample_reader.remainder()).is_equal_to(2); + } + } +} diff --git a/rhubarb/rhubarb-audio/src/audio_error.rs b/rhubarb/rhubarb-audio/src/audio_error.rs new file mode 100644 index 0000000..05610ef --- /dev/null +++ b/rhubarb/rhubarb-audio/src/audio_error.rs @@ -0,0 +1,161 @@ +use std::{ + error::Error, + fmt::{self, Display, Formatter}, + io::{self, ErrorKind}, +}; + +/// The error type for working with audio. +#[derive(Debug)] +pub enum AudioError { + /// This library doesn't know how to read audio files with the current file's extension. + UnsupportedFileType, + + /// The current file uses an unsupported feature, such as an audio codec or a sample format. + /// + /// Expects a string containing a more detailed error message. + UnsupportedFileFeature(String), + + /// The current file doesn't adhere to the expected file format. This could be because it got + /// corrupted during transfer or because it is a valid file that received the wrong file + /// extension. + /// + /// Expects a string containing a more detailed error message. + CorruptFile(String), + + /// An I/O error occurred trying to read from or write to the current file. + IoError(io::Error), +} + +impl Error for AudioError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + // Don't expose source. See https://stackoverflow.com/a/63620301/52041 + None + } +} + +impl Display for AudioError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedFileType => write!(f, "Unsupported audio file type."), + Self::UnsupportedFileFeature(message) => { + write!(f, "Unsupported audio file feature: {message}") + } + Self::CorruptFile(message) => { + write!(f, "Audio file appears to be corrupt: {message}") + } + Self::IoError(io_error) => write!(f, "Error reading audio file: {io_error}"), + } + } +} + +impl From for AudioError { + fn from(io_error: io::Error) -> Self { + if io_error.kind() == ErrorKind::UnexpectedEof { + Self::CorruptFile("Unexpected end of file.".to_owned()) + } else { + Self::IoError(io_error) + } + } +} + +impl PartialEq for AudioError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::UnsupportedFileType, Self::UnsupportedFileType) => true, + ( + Self::UnsupportedFileFeature(left_message), + Self::UnsupportedFileFeature(right_message), + ) => left_message == right_message, + (Self::CorruptFile(left_message), Self::CorruptFile(right_message)) => { + left_message == right_message + } + (Self::IoError(left_error), Self::IoError(right_error)) => { + left_error.kind() == right_error.kind() + } + _ => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use rstest::*; + use speculoos::prelude::*; + + #[rstest] + fn does_not_expose_source() { + let error = AudioError::UnsupportedFileType; + assert_that!(error.source()).is_none(); + + let error = AudioError::IoError(io::Error::from(ErrorKind::NotFound)); + assert_that!(error.source()).is_none(); + } + + #[rstest] + fn supports_display() { + let error = AudioError::UnsupportedFileType; + assert_that!(format!("{error}")).is_equal_to("Unsupported audio file type.".to_owned()); + + let error = AudioError::UnsupportedFileFeature("Spacial audio not supported.".to_owned()); + assert_that!(format!("{error}")) + .is_equal_to("Unsupported audio file feature: Spacial audio not supported.".to_owned()); + + let error = AudioError::CorruptFile("Checksum doesn't match.".to_owned()); + assert_that!(format!("{error}")) + .is_equal_to("Audio file appears to be corrupt: Checksum doesn't match.".to_owned()); + + let error = AudioError::IoError(io::Error::from(ErrorKind::NotFound)); + assert_that!(format!("{error}")) + .is_equal_to("Error reading audio file: entity not found".to_owned()); + } + + mod from_io_error { + use super::*; + + #[rstest] + fn converts_from_io_error() { + let error = AudioError::from(io::Error::from(ErrorKind::NotFound)); + assert_matches!( + error, + AudioError::IoError(io_error) if io_error.kind() == ErrorKind::NotFound + ); + } + + #[rstest] + fn treats_unexpected_eof_as_corrupt() { + let error = AudioError::from(io::Error::from(ErrorKind::UnexpectedEof)); + assert_matches!( + error, + AudioError::CorruptFile(message) if message == "Unexpected end of file." + ); + } + } + + #[rstest] + fn supports_partial_equality() { + assert_that!(AudioError::UnsupportedFileType).is_equal_to(AudioError::UnsupportedFileType); + assert_that!(AudioError::UnsupportedFileType) + .is_not_equal_to(AudioError::CorruptFile("".to_owned())); + + assert_that!(AudioError::CorruptFile("Foo".to_owned())) + .is_equal_to(AudioError::CorruptFile("Foo".to_owned())); + assert_that!(AudioError::CorruptFile("Foo".to_owned())) + .is_not_equal_to(AudioError::CorruptFile("Bar".to_owned())); + + assert_that!(AudioError::from(io::Error::from(ErrorKind::UnexpectedEof))) + .is_equal_to(AudioError::from(io::Error::from(ErrorKind::UnexpectedEof))); + assert_that!(AudioError::from(io::Error::new( + ErrorKind::UnexpectedEof, + "Foo" + ))) + .is_equal_to(AudioError::from(io::Error::new( + ErrorKind::UnexpectedEof, + "Bar", + ))); + assert_that!(AudioError::from(io::Error::from(ErrorKind::UnexpectedEof))).is_not_equal_to( + AudioError::from(io::Error::from(ErrorKind::ConnectionReset)), + ); + } +} diff --git a/rhubarb/rhubarb-audio/src/lib.rs b/rhubarb/rhubarb-audio/src/lib.rs new file mode 100644 index 0000000..84009f3 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/lib.rs @@ -0,0 +1,19 @@ +#![warn( + rust_2018_idioms, + missing_debug_implementations, + missing_docs, + clippy::cast_lossless, + clippy::checked_conversions, + clippy::ptr_as_ptr, + clippy::unnecessary_self_imports, + clippy::use_self +)] +#![allow(clippy::module_inception)] + +//! Audio library for use in Rhubarb Lip Sync. + +mod audio_clip; +mod audio_error; + +pub use audio_clip::{AudioClip, Sample, SampleReader}; +pub use audio_error::AudioError; diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..026d018 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "rhubarb-lip-sync" +include("rhubarb:rhubarb-audio")