diff --git a/README.md b/README.md index b5519511c..0ab97d6c2 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Generate the build system files with CMake. For a standard desktop build with tests and examples enabled, run: ```bash -cmake . -B build -DYUP_ENABLE_TESTS=ON -DYUP_ENABLE_EXAMPLES=ON +cmake . -B build -DYUP_BUILD_TESTS=ON -DYUP_BUILD_EXAMPLES=ON cmake --build build --config Release --parallel 4 ``` @@ -186,7 +186,7 @@ cmake --build build --config Release --parallel 4 Android will rely on cmake for configuration and gradlew will again call into cmake to build the native part of yup: ```bash -cmake -G "Ninja Multi-Config" . -B build -DYUP_TARGET_ANDROID=ON -DYUP_ENABLE_TESTS=ON -DYUP_ENABLE_EXAMPLES=ON +cmake -G "Ninja Multi-Config" . -B build -DYUP_TARGET_ANDROID=ON -DYUP_BUILD_TESTS=ON -DYUP_BUILD_EXAMPLES=ON cd build/examples/render ./gradlew assembleRelease # ./gradlew assembleDebug diff --git a/cmake/yup_standalone.cmake b/cmake/yup_standalone.cmake index 0b728a5a6..750778c1a 100644 --- a/cmake/yup_standalone.cmake +++ b/cmake/yup_standalone.cmake @@ -85,7 +85,7 @@ function (yup_standalone_app) # ==== Enable profiling if (YUP_ENABLE_PROFILING AND NOT "${target_name}" STREQUAL "yup_tests") - list (APPEND additional_definitions YUP_ENABLE_PROFILING=1 YUP_ENABLE_PROFILING=1) + list (APPEND additional_definitions YUP_ENABLE_PROFILING=1) list (APPEND additional_libraries perfetto::perfetto) endif() @@ -184,11 +184,11 @@ function (yup_standalone_app) -sFETCH=1 #-sASYNCIFY=1 -sEXPORTED_RUNTIME_METHODS=ccall,cwrap - -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='$dynCall','$stackTrace' - --shell-file "${YUP_ARG_CUSTOM_SHELL}") + -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='$dynCall' + --shell-file=${YUP_ARG_CUSTOM_SHELL}) - foreach (preload_file ${YUP_ARG_PRELOAD_FILES}) - list (APPEND additional_link_options --preload-file ${preload_file}) + foreach (preload_file IN ITEMS ${YUP_ARG_PRELOAD_FILES}) + list (APPEND additional_link_options "--preload-file=${preload_file}") endforeach() set (target_copy_dest "$") diff --git a/modules/yup_core/system/yup_SystemStats.cpp b/modules/yup_core/system/yup_SystemStats.cpp index c81b3f0ce..c98f2f4fa 100644 --- a/modules/yup_core/system/yup_SystemStats.cpp +++ b/modules/yup_core/system/yup_SystemStats.cpp @@ -252,8 +252,8 @@ String SystemStats::getStackBacktrace() #elif YUP_EMSCRIPTEN std::string temporaryStack; - temporaryStack.resize (10 * EM_ASM_INT_V ({ return (lengthBytesUTF8 || Module.lengthBytesUTF8) (stackTrace()); })); - EM_ASM_ARGS ({ (stringToUTF8 || Module.stringToUTF8) (stackTrace(), $0, $1); }, temporaryStack.data(), temporaryStack.size()); + temporaryStack.resize (emscripten_get_callstack (EM_LOG_C_STACK, nullptr, 0)); + emscripten_get_callstack (EM_LOG_C_STACK, temporaryStack.data(), static_cast (temporaryStack.size())); result << temporaryStack.c_str(); #elif YUP_WINDOWS diff --git a/modules/yup_data_model/tree/yup_DataTree.cpp b/modules/yup_data_model/tree/yup_DataTree.cpp index ebc075ecb..97ce34824 100644 --- a/modules/yup_data_model/tree/yup_DataTree.cpp +++ b/modules/yup_data_model/tree/yup_DataTree.cpp @@ -24,6 +24,103 @@ namespace yup //============================================================================== +namespace +{ +var coerceAttributeValue (const Identifier& nodeType, + const Identifier& propertyName, + const String& rawValue, + const ReferenceCountedObjectPtr& schema) +{ + if (schema == nullptr) + return var (rawValue); + + auto info = schema->getPropertyInfo (nodeType, propertyName); + if (info.type.isEmpty()) + return var (rawValue); + + const auto trimmed = rawValue.trim(); + + const auto looksLikeInteger = [] (const String& text) + { + if (text.isEmpty()) + return false; + + int start = 0; + if (text.startsWithChar ('-') || text.startsWithChar ('+')) + start = 1; + + if (start == text.length()) + return false; + + for (int i = start; i < text.length(); ++i) + { + if (! CharacterFunctions::isDigit (text[i])) + return false; + } + + return true; + }; + + const auto looksLikeNumber = [] (const String& text) + { + bool hasDigit = false; + + for (int i = 0; i < text.length(); ++i) + { + const auto c = text[i]; + if (CharacterFunctions::isDigit (c)) + { + hasDigit = true; + continue; + } + + if (c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E') + continue; + + return false; + } + + return hasDigit; + }; + + if (info.type == "boolean") + { + if (trimmed.equalsIgnoreCase ("true") || trimmed == "1" || trimmed.equalsIgnoreCase ("yes")) + return var (true); + + if (trimmed.equalsIgnoreCase ("false") || trimmed == "0" || trimmed.equalsIgnoreCase ("no")) + return var (false); + + return var (rawValue); + } + + if (info.type == "number" && looksLikeNumber (trimmed)) + { + if (looksLikeInteger (trimmed)) + return var (trimmed.getLargeIntValue()); + + return var (trimmed.getDoubleValue()); + } + + if ((info.type == "array" || info.type == "object") && trimmed.isNotEmpty()) + { + var parsed; + if (JSON::parse (trimmed, parsed)) + { + if (info.type == "array" && parsed.isArray()) + return parsed; + + if (info.type == "object" && parsed.isObject()) + return parsed; + } + } + + return var (rawValue); +} +} // namespace + +//============================================================================== + class PropertySetAction : public UndoableAction { public: @@ -911,13 +1008,7 @@ std::unique_ptr DataTree::createXml() const auto element = std::make_unique (object->type.toString()); // Add properties as attributes - for (int i = 0; i < object->properties.size(); ++i) - { - const auto name = object->properties.getName (i); - const auto value = object->properties.getValueAt (i); - - element->setAttribute (name.toString(), value.toString()); - } + object->properties.copyToXmlAttributes (*element); // Add children as child elements for (const auto& child : object->children) @@ -930,21 +1021,28 @@ std::unique_ptr DataTree::createXml() const } DataTree DataTree::fromXml (const XmlElement& xml) +{ + return fromXml (xml, nullptr); +} + +DataTree DataTree::fromXml (const XmlElement& xml, ReferenceCountedObjectPtr schema) { DataTree tree (xml.getTagName()); + const auto nodeType = tree.getType(); // Load properties from attributes for (int i = 0; i < xml.getNumAttributes(); ++i) { - const auto name = xml.getAttributeName (i); - const auto value = xml.getAttributeValue (i); - tree.setProperty (name, value); + auto name = xml.getAttributeName (i); + auto value = xml.getAttributeValue (i); + + tree.setProperty (name, coerceAttributeValue (nodeType, name, value, schema)); } // Load children from child elements for (const auto* childXml : xml.getChildIterator()) { - auto child = fromXml (*childXml); + auto child = fromXml (*childXml, schema); tree.addChild (child); } @@ -1475,6 +1573,38 @@ void DataTree::Transaction::moveChild (int currentIndex, int newIndex) childChanges.push_back (change); } +int DataTree::Transaction::getEffectiveChildCount() const +{ + if (dataTree.object == nullptr) + return 0; + + int count = dataTree.getNumChildren(); + + for (const auto& change : childChanges) + { + switch (change.type) + { + case ChildChange::Add: + ++count; + break; + + case ChildChange::Remove: + if (count > 0) + --count; + break; + + case ChildChange::RemoveAll: + count = 0; + break; + + case ChildChange::Move: + break; // No change in count + } + } + + return std::max (0, count); +} + //============================================================================== DataTree::ValidatedTransaction::ValidatedTransaction (DataTree& tree, ReferenceCountedObjectPtr schema, UndoManager* undoManager) @@ -1555,8 +1685,8 @@ Result DataTree::ValidatedTransaction::addChild (const DataTree& child, int inde if (! child.isValid()) return Result::fail ("Cannot add invalid child"); - // TODO: Get current child count from the transaction's target tree - auto validationResult = schema->validateChildAddition (nodeType, child.getType(), 0); + const int effectiveChildCount = transaction->getEffectiveChildCount(); + auto validationResult = schema->validateChildAddition (nodeType, child.getType(), effectiveChildCount); if (validationResult.failed()) { hasValidationErrors = true; @@ -1588,7 +1718,18 @@ Result DataTree::ValidatedTransaction::removeChild (const DataTree& child) if (! transaction || ! transaction->isActive() || ! schema) return Result::fail ("Transaction is not active"); - // TODO: Check minimum child count constraints + if (! schema->hasNodeType (nodeType)) + return Result::fail ("Unknown node type: " + nodeType.toString()); + + const auto constraints = schema->getChildConstraints (nodeType); + const int currentCount = transaction->getEffectiveChildCount(); + const int resultingCount = std::max (0, currentCount - 1); + + if (resultingCount < constraints.minCount) + { + hasValidationErrors = true; + return Result::fail ("Cannot remove child: would violate minimum child count (" + String (constraints.minCount) + ")"); + } transaction->removeChild (child); return Result::ok(); diff --git a/modules/yup_data_model/tree/yup_DataTree.h b/modules/yup_data_model/tree/yup_DataTree.h index f17a9516c..1ec16a8a3 100644 --- a/modules/yup_data_model/tree/yup_DataTree.h +++ b/modules/yup_data_model/tree/yup_DataTree.h @@ -570,6 +570,21 @@ class YUP_API DataTree */ static DataTree fromXml (const XmlElement& xml); + /** + Recreates a DataTree from an XmlElement using a schema to recover types. + + Behaves like fromXml(), but uses the provided schema to coerce property values + back to their declared types (e.g., numbers, booleans) during deserialization. + When no schema is provided, properties are imported as strings, matching JUCE + ValueTree behaviour. + + @param xml The XmlElement to deserialize from + @param schema Optional schema describing node/property types for coercion + @return A new DataTree representing the XML content, or invalid DataTree on failure + */ + static DataTree fromXml (const XmlElement& xml, + ReferenceCountedObjectPtr schema); + /** Writes this DataTree to a binary stream in a compact format. @@ -955,6 +970,11 @@ class YUP_API DataTree */ void moveChild (int currentIndex, int newIndex); + /** + Returns the effective number of children taking pending operations into account. + */ + int getEffectiveChildCount() const; + private: friend class TransactionAction; diff --git a/modules/yup_graphics/primitives/yup_AffineTransform.h b/modules/yup_graphics/primitives/yup_AffineTransform.h index 3065fdc81..22290c415 100644 --- a/modules/yup_graphics/primitives/yup_AffineTransform.h +++ b/modules/yup_graphics/primitives/yup_AffineTransform.h @@ -2,7 +2,7 @@ ============================================================================== This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com + Copyright (c) 2025 - kunitoki@gmail.com YUP is an open source library subject to open-source licensing. @@ -207,6 +207,111 @@ class YUP_API AffineTransform return { 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f }; } + //============================================================================== + /** Check if the transformation only contains translation + + Checks if the AffineTransform object represents only a translation transformation, + with no rotation, scaling, or shearing applied. + + @return True if this is only a translation transformation, false otherwise. + */ + constexpr bool isOnlyTranslation() const noexcept + { + return approximatelyEqual (scaleX, 1.0f) + && approximatelyEqual (shearX, 0.0f) + && approximatelyEqual (shearY, 0.0f) + && approximatelyEqual (scaleY, 1.0f); + } + + /** Check if the transformation only contains rotation + + Checks if the AffineTransform object represents only a rotation transformation, + with no translation, scaling, or shearing applied. A pure rotation maintains unit + determinant and has orthonormal basis vectors. + + @return True if this is only a rotation transformation, false otherwise. + */ + constexpr bool isOnlyRotation() const noexcept + { + if (! approximatelyEqual (translateX, 0.0f) || ! approximatelyEqual (translateY, 0.0f)) + return false; + + const float det = getDeterminant(); + const float col1LengthSq = scaleX * scaleX + shearY * shearY; + const float col2LengthSq = shearX * shearX + scaleY * scaleY; + + constexpr float rotationTolerance = 0.0001f; + return yup_abs (det - 1.0f) < rotationTolerance + && yup_abs (col1LengthSq - 1.0f) < rotationTolerance + && yup_abs (col2LengthSq - 1.0f) < rotationTolerance; + } + + /** Check if the transformation only contains uniform scaling + + Checks if the AffineTransform object represents only a uniform scaling transformation, + with no translation, rotation, or shearing applied. Uniform scaling has equal scale + factors in both x and y directions. + + @return True if this is only a uniform scaling transformation, false otherwise. + */ + constexpr bool isOnlyUniformScaling() const noexcept + { + return approximatelyEqual (translateX, 0.0f) + && approximatelyEqual (translateY, 0.0f) + && approximatelyEqual (shearX, 0.0f) + && approximatelyEqual (shearY, 0.0f) + && approximatelyEqual (scaleX, scaleY) + && ! approximatelyEqual (scaleX, 1.0f); + } + + /** Check if the transformation only contains non-uniform scaling + + Checks if the AffineTransform object represents only a non-uniform scaling transformation, + with no translation, rotation, or shearing applied. Non-uniform scaling has different + scale factors in x and y directions. + + @return True if this is only a non-uniform scaling transformation, false otherwise. + */ + constexpr bool isOnlyNonUniformScaling() const noexcept + { + return approximatelyEqual (translateX, 0.0f) + && approximatelyEqual (translateY, 0.0f) + && approximatelyEqual (shearX, 0.0f) + && approximatelyEqual (shearY, 0.0f) + && ! approximatelyEqual (scaleX, scaleY) + && ! approximatelyEqual (scaleX, 1.0f) + && ! approximatelyEqual (scaleY, 1.0f); + } + + /** Check if the transformation only contains scaling (uniform or non-uniform) + + Checks if the AffineTransform object represents only a scaling transformation, + with no translation, rotation, or shearing applied. + + @return True if this is only a scaling transformation, false otherwise. + */ + constexpr bool isOnlyScaling() const noexcept + { + return isOnlyUniformScaling() || isOnlyNonUniformScaling(); + } + + /** Check if the transformation only contains shearing + + Checks if the AffineTransform object represents only a shearing transformation, + with no translation, rotation, or scaling applied. A pure shear maintains unit + scale factors. + + @return True if this is only a shearing transformation, false otherwise. + */ + constexpr bool isOnlyShearing() const noexcept + { + return approximatelyEqual (translateX, 0.0f) + && approximatelyEqual (translateY, 0.0f) + && approximatelyEqual (scaleX, 1.0f) + && approximatelyEqual (scaleY, 1.0f) + && (! approximatelyEqual (shearX, 0.0f) || ! approximatelyEqual (shearY, 0.0f)); + } + //============================================================================== /** Create an inverted transformation @@ -654,14 +759,21 @@ class YUP_API AffineTransform */ [[nodiscard]] constexpr AffineTransform prependedBy (const AffineTransform& other) const noexcept { - return { - scaleX * other.scaleX + shearX * other.shearY, - shearX * other.scaleX + scaleY * other.shearY, - translateX * other.scaleX + translateY * other.shearY + other.translateX, - scaleX * other.shearX + shearY * other.scaleY, - shearX * other.shearX + scaleY * other.scaleY, - translateX * other.shearX + translateY * other.scaleY + other.translateY - }; + return other.followedBy (*this); + } + + //============================================================================== + /** Create a transformation that follows another. + + Creates a new AffineTransform object that represents this transformation followed by another specified AffineTransform. + + @param other The AffineTransform to follow this one. + + @return A new AffineTransform object representing the combined transformation. + */ + [[nodiscard]] constexpr AffineTransform operator* (const AffineTransform& other) const noexcept + { + return followedBy (other); } //============================================================================== diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index 8af574417..bf06c2d49 100644 --- a/modules/yup_graphics/primitives/yup_Rectangle.h +++ b/modules/yup_graphics/primitives/yup_Rectangle.h @@ -252,7 +252,7 @@ class YUP_API Rectangle */ [[nodiscard]] constexpr Rectangle withTrimmedLeft (ValueType amountToTrim) const noexcept { - return withLeft (xy.getX() + amountToTrim); + return { xy.withX (xy.getX() + amountToTrim), size.withWidth (jmax (static_cast (0), size.getWidth() - amountToTrim)) }; } //============================================================================== @@ -300,7 +300,7 @@ class YUP_API Rectangle */ [[nodiscard]] constexpr Rectangle withTrimmedTop (ValueType amountToTrim) const noexcept { - return withTop (xy.getY() + amountToTrim); + return { xy.withY (xy.getY() + amountToTrim), size.withHeight (jmax (static_cast (0), size.getHeight() - amountToTrim)) }; } //============================================================================== @@ -1696,8 +1696,14 @@ class YUP_API Rectangle [[nodiscard]] constexpr auto aspectRatio() const noexcept -> std::enable_if_t, std::tuple> { - auto factor = std::gcd (size.getWidth(), size.getHeight()); - return std::make_tuple (size.getWidth() / factor, size.getHeight() / factor); + const T w = size.getWidth(); + const T h = size.getHeight(); + + if (w == 0 || h == 0) + return { 0, 0 }; + + const T factor = std::gcd (w, h); + return { w / factor, h / factor }; } /** Returns the ratio of the width to the height of the rectangle. diff --git a/python/tools/ArchivePythonStdlib.py b/python/tools/ArchivePythonStdlib.py index 075c98c5c..cbc80f141 100644 --- a/python/tools/ArchivePythonStdlib.py +++ b/python/tools/ArchivePythonStdlib.py @@ -9,34 +9,26 @@ def file_hash(file): h = hashlib.md5() - with open(file, "rb") as f: h.update(f.read()) - return h.hexdigest() def should_exclude(path, name, exclude_patterns): - """Check if a path should be excluded based on patterns.""" - # Check if the name itself matches any pattern for pattern in exclude_patterns: - # Exact name match if name == pattern: return True - # Directory patterns (e.g., __pycache__, test) if pattern.startswith('**/'): pattern_name = pattern[3:] if name == pattern_name: return True - # Extension patterns (e.g., *.pyc) if pattern.startswith('**/*.'): ext = pattern[4:] # Remove **/* if name.endswith(ext): return True - # Wildcard patterns if '*' in pattern and not pattern.startswith('**/'): import fnmatch if fnmatch.fnmatch(name, pattern): @@ -45,8 +37,7 @@ def should_exclude(path, name, exclude_patterns): return False -def copy_filtered_tree(src, dst, exclude_patterns, verbose=False): - """Recursively copy directory tree with filtering.""" +def copy_filtered_tree(src, dst, exclude_patterns): if not os.path.exists(dst): os.makedirs(dst) @@ -54,62 +45,41 @@ def copy_filtered_tree(src, dst, exclude_patterns, verbose=False): src_path = os.path.join(src, item) dst_path = os.path.join(dst, item) - # Check if should exclude if should_exclude(src_path, item, exclude_patterns): - if verbose: - print(f"Excluding: {os.path.relpath(src_path, src)}") continue if os.path.isdir(src_path): - copy_filtered_tree(src_path, dst_path, exclude_patterns, verbose) + copy_filtered_tree(src_path, dst_path, exclude_patterns) else: shutil.copy2(src_path, dst_path) - if verbose: - print(f"Copied: {os.path.relpath(src_path, src)}") -def clean_duplicate_libraries(directory, verbose=False): - """Keep only one of each dynamic library type.""" +def clean_duplicate_libraries(directory): lib_files = {} - - for root, dirs, files in os.walk(directory): + for root, _, files in os.walk(directory): for file in files: file_path = os.path.join(root, file) - # Group by base name without version numbers - # e.g., libpython3.13.dylib -> libpython, libpython3.13.a -> libpython - if any(ext in file for ext in ['.dylib', '.dll', '.so', '.a', '.lib']): - # Extract base name and extension type - base_name = file.split('.')[0] - - # Determine library type - if '.dylib' in file: - lib_type = 'dylib' - elif '.dll' in file: - lib_type = 'dll' - elif '.so' in file: - lib_type = 'so' - elif '.a' in file: - lib_type = 'static' - elif '.lib' in file: - lib_type = 'lib' - else: - continue + if not any(ext in file for ext in ['.dylib', '.dll', '.so', '.a', '.lib']): + continue - key = (base_name, lib_type) + base_name = file.split('.')[0] - if key not in lib_files: - lib_files[key] = file_path - if verbose: - print(f"Keeping library: {file}") - else: - # Remove duplicate - if verbose: - print(f"Removing duplicate library: {file}") - os.remove(file_path) + if '.dylib' in file: lib_type = 'dylib' + elif '.dll' in file: lib_type = 'dll' + elif '.so' in file: lib_type = 'so' + elif '.a' in file: lib_type = 'static' + elif '.lib' in file: lib_type = 'lib' + else: continue + + key = (base_name, lib_type) + if key not in lib_files: + lib_files[key] = file_path + else: + os.remove(file_path) -def make_archive(file, directory, verbose=False): +def make_archive(file, directory): archived_files = [] for dirname, _, files in os.walk(directory): for filename in files: @@ -127,12 +97,9 @@ def make_archive(file, directory, verbose=False): with open(path, "rb") as fp: zf.writestr(zip_info, fp.read(), compress_type=zipfile.ZIP_DEFLATED, compresslevel=9) - if verbose: - print(f"Added to zip: {archive_path}") - if __name__ == "__main__": - print(f"starting python standard lib archiving tool...") + print(f"-- YUP -- Starting python standard lib archiving tool...") parser = ArgumentParser() parser.add_argument("-r", "--root-folder", type=Path, help="Path to the python base folder.") @@ -140,7 +107,6 @@ def make_archive(file, directory, verbose=False): parser.add_argument("-M", "--version-major", type=int, help="Major version number (integer).") parser.add_argument("-m", "--version-minor", type=int, help="Minor version number (integer).") parser.add_argument("-x", "--exclude-patterns", type=str, default=None, help="Excluded patterns (semicolon separated list).") - parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output.") args = parser.parse_args() @@ -180,7 +146,7 @@ def make_archive(file, directory, verbose=False): custom_patterns = [x.strip() for x in args.exclude_patterns.replace('"', '').split(";")] base_patterns += custom_patterns - print(f"cleaning up {final_location}...") + print(f"-- YUP -- Cleaning up {final_location}...") if final_location.exists(): shutil.rmtree(final_location) @@ -196,26 +162,24 @@ def make_archive(file, directory, verbose=False): python_lib_src = lib_src / python_folder_name if python_lib_src.exists(): python_lib_dst = lib_dst / python_folder_name - print(f"copying library from {python_lib_src} to {python_lib_dst}...") - copy_filtered_tree(python_lib_src, python_lib_dst, base_patterns, verbose=args.verbose) + print(f"-- YUP -- Copying library from {python_lib_src} to {python_lib_dst}...") + copy_filtered_tree(python_lib_src, python_lib_dst, base_patterns) # Create site-packages directory site_packages = python_lib_dst / "site-packages" site_packages.mkdir(parents=True, exist_ok=True) else: - print(f"Warning: Python library path {python_lib_src} does not exist") + print(f"-- YUP -- Warning: Python library path {python_lib_src} does not exist") # Copy dynamic libraries from lib root (e.g., libpython3.13.dylib) - print(f"copying dynamic libraries from {lib_src}...") + print(f"-- YUP -- Copying dynamic libraries from {lib_src}...") for item in lib_src.iterdir(): if item.is_file(): if any(ext in item.name for ext in ['.dylib', '.dll', '.so', '.a', '.lib']): if not should_exclude(str(item), item.name, base_patterns): shutil.copy2(item, lib_dst / item.name) - if args.verbose: - print(f"Copied library: {item.name}") else: - print(f"Warning: Library path {lib_src} does not exist") + print(f"-- YUP -- Warning: Library path {lib_src} does not exist") # Copy bin folder bin_src = base_python / "bin" @@ -223,7 +187,7 @@ def make_archive(file, directory, verbose=False): bin_dst = final_location / "bin" bin_dst.mkdir(parents=True, exist_ok=True) - print(f"copying binaries from {bin_src} to {bin_dst}...") + print(f"-- YUP -- Copying binaries from {bin_src} to {bin_dst}...") # Copy python executables and symlinks (deduplicate with set) executables = list(set([ @@ -241,8 +205,6 @@ def make_archive(file, directory, verbose=False): if exe_path.exists(): # Skip if destination already exists if dst_path.exists(): - if args.verbose: - print(f"Skipping existing binary: {executable}") continue if exe_path.is_symlink(): @@ -251,10 +213,8 @@ def make_archive(file, directory, verbose=False): os.symlink(link_target, dst_path) else: shutil.copy2(exe_path, dst_path) - if args.verbose: - print(f"Copied binary: {executable}") else: - print(f"Warning: Binary path {bin_src} does not exist") + print(f"-- YUP -- Warning: Binary path {bin_src} does not exist") # Copy include folder include_src = base_python / "include" @@ -262,7 +222,7 @@ def make_archive(file, directory, verbose=False): include_dst = final_location / "include" include_dst.mkdir(parents=True, exist_ok=True) - print(f"copying include files from {include_src} to {include_dst}...") + print(f"-- YUP -- Copying include files from {include_src} to {include_dst}...") # Copy the python version include folder python_include_src = include_src / python_folder_name @@ -276,33 +236,28 @@ def make_archive(file, directory, verbose=False): shutil.copytree(item, include_dst / item.name, dirs_exist_ok=True) else: shutil.copy2(item, include_dst / item.name) - - if args.verbose: - print(f"Copied include files") else: - print(f"Warning: Include path {include_src} does not exist") + print(f"-- YUP -- Warning: Include path {include_src} does not exist") # Clean up duplicate libraries - print(f"cleaning up duplicate libraries...") - clean_duplicate_libraries(final_location, verbose=args.verbose) + print(f"-- YUP -- Cleaning up duplicate libraries...") + clean_duplicate_libraries(final_location) # Create archive from final_location contents (not including the python/ wrapper) - print(f"making archive {temp_archive} to {final_archive}...") + print(f"-- YUP -- Making archive {temp_archive} to {final_archive}...") if os.path.exists(final_archive): - make_archive(temp_archive, final_location, verbose=args.verbose) + make_archive(temp_archive, final_location) if file_hash(temp_archive) != file_hash(final_archive): shutil.copy(temp_archive, final_archive) os.remove(temp_archive) - print(f"Archive updated") + print("-- YUP -- Archive updated") else: os.remove(temp_archive) - print(f"Archive unchanged") + print("-- YUP -- Archive unchanged") else: - make_archive(final_archive, final_location, verbose=args.verbose) - print(f"Archive created") + make_archive(final_archive, final_location) + print("-- YUP -- Archive created") # Clean up temporary directory - print(f"cleaning up {final_location}...") + print(f"-- YUP -- Cleaning up {final_location}...") shutil.rmtree(final_location) - - print("Done!") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 52672d9ff..a9adea054 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -52,6 +52,7 @@ set (target_name yup_tests) set (target_version "1.0.0") set (target_console OFF) set (target_gtest_modules "") +set (target_preloaded "") set (target_modules yup_core yup_audio_basics @@ -76,6 +77,12 @@ else() list (APPEND target_gtest_modules GTest::gtest_main GTest::gmock_main) + + file (GLOB_RECURSE data_files "${CMAKE_CURRENT_LIST_DIR}/data/*") + foreach (data_file ${data_files}) + file (RELATIVE_PATH rel_path "${CMAKE_CURRENT_LIST_DIR}" "${data_file}") + list (APPEND target_preloaded "${data_file}@${rel_path}") + endforeach() endif() if (YUP_PLATFORM_MAC) # OR YUP_PLATFORM_WINDOWS) @@ -105,11 +112,13 @@ yup_standalone_app ( DEFINITIONS YUP_USE_CURL=0 YUP_MODAL_LOOPS_PERMITTED=1 + PRELOAD_FILES + ${target_preloaded} MODULES ${target_modules} ${target_gtest_modules}) -# ==== (Only For Testing) Setup FFTW3 +# ==== (Only For Manual Testing) Setup FFTW3 # _yup_find_fftw3 (${target_name}) # ==== Setup sources diff --git a/tests/data/fonts/Linefont-VariableFont_wdth,wght.ttf b/tests/data/fonts/Linefont-VariableFont_wdth,wght.ttf new file mode 100644 index 000000000..a36039663 Binary files /dev/null and b/tests/data/fonts/Linefont-VariableFont_wdth,wght.ttf differ diff --git a/tests/yup_audio_basics/yup_AudioChannelSet.cpp b/tests/yup_audio_basics/yup_AudioChannelSet.cpp index b103d8026..5f1cfaa8e 100644 --- a/tests/yup_audio_basics/yup_AudioChannelSet.cpp +++ b/tests/yup_audio_basics/yup_AudioChannelSet.cpp @@ -130,3 +130,397 @@ TEST_F (AudioChannelSetTest, Ambisonics) | (1ull << AudioChannelSet::ambisonicACN34) | (1ull << AudioChannelSet::ambisonicACN35); checkAmbisonic (mask, 5, "5th Order Ambisonics"); } + +// ============================================================================= +// Operator Tests +// ============================================================================= + +TEST_F (AudioChannelSetTest, InequalityOperator) +{ + auto stereo = AudioChannelSet::stereo(); + auto mono = AudioChannelSet::mono(); + + EXPECT_TRUE (stereo != mono); + EXPECT_FALSE (stereo != stereo); +} + +TEST_F (AudioChannelSetTest, LessThanOperator) +{ + auto mono = AudioChannelSet::mono(); + auto stereo = AudioChannelSet::stereo(); + + // operator< compares the underlying channels bitmask + EXPECT_FALSE (mono < stereo); + EXPECT_TRUE (stereo < mono); + EXPECT_FALSE (stereo < stereo); +} + +// ============================================================================= +// Channel Name Tests +// ============================================================================= + +TEST_F (AudioChannelSetTest, GetChannelTypeName) +{ + // Test standard channel names + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::left), "Left"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::right), "Right"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::centre), "Centre"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::LFE), "LFE"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::leftSurround), "Left Surround"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::rightSurround), "Right Surround"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::leftCentre), "Left Centre"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::rightCentre), "Right Centre"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::centreSurround), "Centre Surround"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::leftSurroundRear), "Left Surround Rear"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::rightSurroundRear), "Right Surround Rear"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::topMiddle), "Top Middle"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::topFrontLeft), "Top Front Left"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::topFrontCentre), "Top Front Centre"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::topFrontRight), "Top Front Right"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::topRearLeft), "Top Rear Left"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::topRearCentre), "Top Rear Centre"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::topRearRight), "Top Rear Right"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::wideLeft), "Wide Left"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::wideRight), "Wide Right"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::LFE2), "LFE 2"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::leftSurroundSide), "Left Surround Side"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::rightSurroundSide), "Right Surround Side"); + + // Test ambisonic channels + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::ambisonicW), "Ambisonic W"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::ambisonicX), "Ambisonic X"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::ambisonicY), "Ambisonic Y"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::ambisonicZ), "Ambisonic Z"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::ambisonicACN4), "Ambisonic 4"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::ambisonicACN15), "Ambisonic 15"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::ambisonicACN63), "Ambisonic 63"); + + // Test top/bottom channels + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::topSideLeft), "Top Side Left"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::topSideRight), "Top Side Right"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::bottomFrontLeft), "Bottom Front Left"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::bottomFrontCentre), "Bottom Front Centre"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::bottomFrontRight), "Bottom Front Right"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::proximityLeft), "Proximity Left"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::proximityRight), "Proximity Right"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::bottomSideLeft), "Bottom Side Left"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::bottomSideRight), "Bottom Side Right"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::bottomRearLeft), "Bottom Rear Left"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::bottomRearCentre), "Bottom Rear Centre"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::bottomRearRight), "Bottom Rear Right"); + + // Test discrete channels + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::discreteChannel0), "Discrete 1"); + EXPECT_EQ (AudioChannelSet::getChannelTypeName (static_cast (AudioChannelSet::discreteChannel0 + 5)), "Discrete 6"); + + // Test unknown channel + EXPECT_EQ (AudioChannelSet::getChannelTypeName (AudioChannelSet::unknown), "Unknown"); +} + +TEST_F (AudioChannelSetTest, GetAbbreviatedChannelTypeName) +{ + // Test standard abbreviated names + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::left), "L"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::right), "R"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::centre), "C"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::LFE), "Lfe"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::leftSurround), "Ls"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::rightSurround), "Rs"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::leftCentre), "Lc"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::rightCentre), "Rc"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::centreSurround), "Cs"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::leftSurroundRear), "Lrs"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::rightSurroundRear), "Rrs"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::topMiddle), "Tm"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::topFrontLeft), "Tfl"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::topFrontCentre), "Tfc"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::topFrontRight), "Tfr"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::topRearLeft), "Trl"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::topRearCentre), "Trc"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::topRearRight), "Trr"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::wideLeft), "Wl"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::wideRight), "Wr"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::LFE2), "Lfe2"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::leftSurroundSide), "Lss"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::rightSurroundSide), "Rss"); + + // Test ambisonic abbreviations + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::ambisonicACN0), "ACN0"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::ambisonicACN10), "ACN10"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::ambisonicACN63), "ACN63"); + + // Test top/bottom abbreviations + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::topSideLeft), "Tsl"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::topSideRight), "Tsr"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::bottomFrontLeft), "Bfl"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::bottomFrontCentre), "Bfc"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::bottomFrontRight), "Bfr"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::proximityLeft), "Pl"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::proximityRight), "Pr"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::bottomSideLeft), "Bsl"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::bottomSideRight), "Bsr"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::bottomRearLeft), "Brl"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::bottomRearCentre), "Brc"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::bottomRearRight), "Brr"); + + // Test discrete channels + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::discreteChannel0), "1"); + EXPECT_EQ (AudioChannelSet::getAbbreviatedChannelTypeName (static_cast (AudioChannelSet::discreteChannel0 + 9)), "10"); + + // Test unknown channel + EXPECT_TRUE (AudioChannelSet::getAbbreviatedChannelTypeName (AudioChannelSet::unknown).isEmpty()); +} + +TEST_F (AudioChannelSetTest, GetChannelTypeFromAbbreviation) +{ + // Test standard abbreviations + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("L"), AudioChannelSet::left); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("R"), AudioChannelSet::right); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("C"), AudioChannelSet::centre); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Lfe"), AudioChannelSet::LFE); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Ls"), AudioChannelSet::leftSurround); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Rs"), AudioChannelSet::rightSurround); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Lfe2"), AudioChannelSet::LFE2); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Lss"), AudioChannelSet::leftSurroundSide); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Rss"), AudioChannelSet::rightSurroundSide); + + // Test ambisonic abbreviations + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("W"), AudioChannelSet::ambisonicW); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("X"), AudioChannelSet::ambisonicX); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Y"), AudioChannelSet::ambisonicY); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Z"), AudioChannelSet::ambisonicZ); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("ACN0"), AudioChannelSet::ambisonicACN0); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("ACN15"), AudioChannelSet::ambisonicACN15); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("ACN63"), AudioChannelSet::ambisonicACN63); + + // Test discrete channels (numeric) + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("1"), AudioChannelSet::discreteChannel0); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("5"), static_cast (AudioChannelSet::discreteChannel0 + 4)); + + // Test top/bottom abbreviations + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Tsl"), AudioChannelSet::topSideLeft); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Tsr"), AudioChannelSet::topSideRight); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Bfl"), AudioChannelSet::bottomFrontLeft); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Bfc"), AudioChannelSet::bottomFrontCentre); + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("Brr"), AudioChannelSet::bottomRearRight); + + // Test unknown abbreviation + EXPECT_EQ (AudioChannelSet::getChannelTypeFromAbbreviation ("XYZ"), AudioChannelSet::unknown); +} + +// ============================================================================= +// Speaker Arrangement String Tests +// ============================================================================= + +TEST_F (AudioChannelSetTest, GetSpeakerArrangementAsString) +{ + auto stereo = AudioChannelSet::stereo(); + EXPECT_EQ (stereo.getSpeakerArrangementAsString(), "L R"); + + auto surround51 = AudioChannelSet::create5point1(); + EXPECT_EQ (surround51.getSpeakerArrangementAsString(), "L R C Lfe Ls Rs"); + + auto mono = AudioChannelSet::mono(); + EXPECT_EQ (mono.getSpeakerArrangementAsString(), "C"); +} + +TEST_F (AudioChannelSetTest, FromAbbreviatedString) +{ + auto stereo = AudioChannelSet::fromAbbreviatedString ("L R"); + EXPECT_EQ (stereo, AudioChannelSet::stereo()); + + auto surround51 = AudioChannelSet::fromAbbreviatedString ("L R C Lfe Ls Rs"); + EXPECT_EQ (surround51, AudioChannelSet::create5point1()); + + auto mono = AudioChannelSet::fromAbbreviatedString ("C"); + EXPECT_EQ (mono, AudioChannelSet::mono()); + + // Test with unknown abbreviations (should be ignored) + auto partial = AudioChannelSet::fromAbbreviatedString ("L XYZ R"); + EXPECT_EQ (partial, AudioChannelSet::stereo()); +} + +// ============================================================================= +// Description Tests +// ============================================================================= + +TEST_F (AudioChannelSetTest, GetDescription) +{ + // disabled() is a discrete layout with 0 channels + EXPECT_EQ (AudioChannelSet::disabled().getDescription(), "Discrete #0"); + EXPECT_EQ (AudioChannelSet::mono().getDescription(), "Mono"); + EXPECT_EQ (AudioChannelSet::stereo().getDescription(), "Stereo"); + EXPECT_EQ (AudioChannelSet::createLCR().getDescription(), "LCR"); + EXPECT_EQ (AudioChannelSet::createLRS().getDescription(), "LRS"); + EXPECT_EQ (AudioChannelSet::createLCRS().getDescription(), "LCRS"); + EXPECT_EQ (AudioChannelSet::create5point0().getDescription(), "5.0 Surround"); + EXPECT_EQ (AudioChannelSet::create5point0point2().getDescription(), "5.0.2 Surround"); + EXPECT_EQ (AudioChannelSet::create5point0point4().getDescription(), "5.0.4 Surround"); + EXPECT_EQ (AudioChannelSet::create5point1().getDescription(), "5.1 Surround"); + EXPECT_EQ (AudioChannelSet::create5point1point2().getDescription(), "5.1.2 Surround"); + EXPECT_EQ (AudioChannelSet::create5point1point4().getDescription(), "5.1.4 Surround"); + EXPECT_EQ (AudioChannelSet::create6point0().getDescription(), "6.0 Surround"); + EXPECT_EQ (AudioChannelSet::create6point1().getDescription(), "6.1 Surround"); + EXPECT_EQ (AudioChannelSet::create6point0Music().getDescription(), "6.0 (Music) Surround"); + EXPECT_EQ (AudioChannelSet::create6point1Music().getDescription(), "6.1 (Music) Surround"); + EXPECT_EQ (AudioChannelSet::create7point0().getDescription(), "7.0 Surround"); + EXPECT_EQ (AudioChannelSet::create7point1().getDescription(), "7.1 Surround"); + EXPECT_EQ (AudioChannelSet::create7point0SDDS().getDescription(), "7.0 Surround SDDS"); + EXPECT_EQ (AudioChannelSet::create7point1SDDS().getDescription(), "7.1 Surround SDDS"); + EXPECT_EQ (AudioChannelSet::create7point0point2().getDescription(), "7.0.2 Surround"); + EXPECT_EQ (AudioChannelSet::create7point0point4().getDescription(), "7.0.4 Surround"); + EXPECT_EQ (AudioChannelSet::create7point0point6().getDescription(), "7.0.6 Surround"); + EXPECT_EQ (AudioChannelSet::create7point1point2().getDescription(), "7.1.2 Surround"); + EXPECT_EQ (AudioChannelSet::create7point1point4().getDescription(), "7.1.4 Surround"); + EXPECT_EQ (AudioChannelSet::create7point1point6().getDescription(), "7.1.6 Surround"); + EXPECT_EQ (AudioChannelSet::create9point0point4().getDescription(), "9.0.4 Surround"); + EXPECT_EQ (AudioChannelSet::create9point1point4().getDescription(), "9.1.4 Surround"); + EXPECT_EQ (AudioChannelSet::create9point0point6().getDescription(), "9.0.6 Surround"); + EXPECT_EQ (AudioChannelSet::create9point1point6().getDescription(), "9.1.6 Surround"); + EXPECT_EQ (AudioChannelSet::quadraphonic().getDescription(), "Quadraphonic"); + EXPECT_EQ (AudioChannelSet::pentagonal().getDescription(), "Pentagonal"); + EXPECT_EQ (AudioChannelSet::hexagonal().getDescription(), "Hexagonal"); + EXPECT_EQ (AudioChannelSet::octagonal().getDescription(), "Octagonal"); + + // Test discrete layout + EXPECT_EQ (AudioChannelSet::discreteChannels (4).getDescription(), "Discrete #4"); + + // Test ambisonic descriptions + EXPECT_EQ (AudioChannelSet::ambisonic (0).getDescription(), "0th Order Ambisonics"); + EXPECT_EQ (AudioChannelSet::ambisonic (1).getDescription(), "1st Order Ambisonics"); + EXPECT_EQ (AudioChannelSet::ambisonic (2).getDescription(), "2nd Order Ambisonics"); + EXPECT_EQ (AudioChannelSet::ambisonic (3).getDescription(), "3rd Order Ambisonics"); + EXPECT_EQ (AudioChannelSet::ambisonic (4).getDescription(), "4th Order Ambisonics"); +} + +// ============================================================================= +// Channel Access Tests +// ============================================================================= + +TEST_F (AudioChannelSetTest, GetTypeOfChannel) +{ + auto stereo = AudioChannelSet::stereo(); + EXPECT_EQ (stereo.getTypeOfChannel (0), AudioChannelSet::left); + EXPECT_EQ (stereo.getTypeOfChannel (1), AudioChannelSet::right); + + auto surround51 = AudioChannelSet::create5point1(); + EXPECT_EQ (surround51.getTypeOfChannel (0), AudioChannelSet::left); + EXPECT_EQ (surround51.getTypeOfChannel (1), AudioChannelSet::right); + EXPECT_EQ (surround51.getTypeOfChannel (2), AudioChannelSet::centre); + EXPECT_EQ (surround51.getTypeOfChannel (3), AudioChannelSet::LFE); + EXPECT_EQ (surround51.getTypeOfChannel (4), AudioChannelSet::leftSurround); + EXPECT_EQ (surround51.getTypeOfChannel (5), AudioChannelSet::rightSurround); +} + +TEST_F (AudioChannelSetTest, GetChannelIndexForType) +{ + auto surround51 = AudioChannelSet::create5point1(); + EXPECT_EQ (surround51.getChannelIndexForType (AudioChannelSet::left), 0); + EXPECT_EQ (surround51.getChannelIndexForType (AudioChannelSet::right), 1); + EXPECT_EQ (surround51.getChannelIndexForType (AudioChannelSet::centre), 2); + EXPECT_EQ (surround51.getChannelIndexForType (AudioChannelSet::LFE), 3); + EXPECT_EQ (surround51.getChannelIndexForType (AudioChannelSet::leftSurround), 4); + EXPECT_EQ (surround51.getChannelIndexForType (AudioChannelSet::rightSurround), 5); + + // Test channel not in set + EXPECT_EQ (surround51.getChannelIndexForType (AudioChannelSet::topMiddle), -1); +} + +// ============================================================================= +// Channel Manipulation Tests +// ============================================================================= + +TEST_F (AudioChannelSetTest, RemoveChannel) +{ + auto surround51 = AudioChannelSet::create5point1(); + EXPECT_EQ (surround51.size(), 6); + + surround51.removeChannel (AudioChannelSet::LFE); + EXPECT_EQ (surround51.size(), 5); + EXPECT_EQ (surround51.getChannelIndexForType (AudioChannelSet::LFE), -1); + + surround51.removeChannel (AudioChannelSet::centre); + EXPECT_EQ (surround51.size(), 4); + EXPECT_EQ (surround51.getChannelIndexForType (AudioChannelSet::centre), -1); +} + +// ============================================================================= +// Factory Method Tests +// ============================================================================= + +TEST_F (AudioChannelSetTest, CanonicalChannelSet) +{ + EXPECT_EQ (AudioChannelSet::canonicalChannelSet (1), AudioChannelSet::mono()); + EXPECT_EQ (AudioChannelSet::canonicalChannelSet (2), AudioChannelSet::stereo()); + EXPECT_EQ (AudioChannelSet::canonicalChannelSet (3), AudioChannelSet::createLCR()); + EXPECT_EQ (AudioChannelSet::canonicalChannelSet (4), AudioChannelSet::quadraphonic()); + EXPECT_EQ (AudioChannelSet::canonicalChannelSet (5), AudioChannelSet::create5point0()); + EXPECT_EQ (AudioChannelSet::canonicalChannelSet (6), AudioChannelSet::create5point1()); + EXPECT_EQ (AudioChannelSet::canonicalChannelSet (7), AudioChannelSet::create7point0()); + EXPECT_EQ (AudioChannelSet::canonicalChannelSet (8), AudioChannelSet::create7point1()); + + // For channel counts without canonical layouts, should return discrete + auto discrete10 = AudioChannelSet::canonicalChannelSet (10); + EXPECT_TRUE (discrete10.isDiscreteLayout()); + EXPECT_EQ (discrete10.size(), 10); +} + +TEST_F (AudioChannelSetTest, NamedChannelSet) +{ + EXPECT_EQ (AudioChannelSet::namedChannelSet (1), AudioChannelSet::mono()); + EXPECT_EQ (AudioChannelSet::namedChannelSet (2), AudioChannelSet::stereo()); + EXPECT_EQ (AudioChannelSet::namedChannelSet (3), AudioChannelSet::createLCR()); + EXPECT_EQ (AudioChannelSet::namedChannelSet (4), AudioChannelSet::quadraphonic()); + EXPECT_EQ (AudioChannelSet::namedChannelSet (5), AudioChannelSet::create5point0()); + EXPECT_EQ (AudioChannelSet::namedChannelSet (6), AudioChannelSet::create5point1()); + EXPECT_EQ (AudioChannelSet::namedChannelSet (7), AudioChannelSet::create7point0()); + EXPECT_EQ (AudioChannelSet::namedChannelSet (8), AudioChannelSet::create7point1()); + + // For channel counts without named layouts, should return disabled (empty) + auto empty = AudioChannelSet::namedChannelSet (10); + EXPECT_EQ (empty.size(), 0); + EXPECT_EQ (empty, AudioChannelSet::disabled()); +} + +// ============================================================================= +// Wave Channel Mask Tests +// ============================================================================= + +TEST_F (AudioChannelSetTest, FromWaveChannelMask) +{ + // Test stereo (left + right = bits 0 and 1) + auto stereo = AudioChannelSet::fromWaveChannelMask (0x3); + EXPECT_EQ (stereo.size(), 2); + + // Test 5.1 (L, R, C, LFE, Ls, Rs) + auto surround51 = AudioChannelSet::fromWaveChannelMask (0x3F); + EXPECT_EQ (surround51.size(), 6); + + // Test empty + auto empty = AudioChannelSet::fromWaveChannelMask (0x0); + EXPECT_EQ (empty.size(), 0); +} + +TEST_F (AudioChannelSetTest, GetWaveChannelMask) +{ + // Test stereo + auto stereo = AudioChannelSet::stereo(); + EXPECT_EQ (stereo.getWaveChannelMask(), 0x3); + + // Test 5.1 + auto surround51 = AudioChannelSet::create5point1(); + EXPECT_EQ (surround51.getWaveChannelMask(), 0x3F); + + // Test mono (centre channel) + auto mono = AudioChannelSet::mono(); + EXPECT_EQ (mono.getWaveChannelMask(), 0x4); + + // Test disabled + auto disabled = AudioChannelSet::disabled(); + EXPECT_EQ (disabled.getWaveChannelMask(), 0x0); + + // Test set with channel beyond topRearRight (should return -1) + AudioChannelSet highChannel; + highChannel.addChannel (AudioChannelSet::ambisonicACN10); + EXPECT_EQ (highChannel.getWaveChannelMask(), -1); +} diff --git a/tests/yup_audio_basics/yup_AudioDataConverters.cpp b/tests/yup_audio_basics/yup_AudioDataConverters.cpp index df69c8135..52c909519 100644 --- a/tests/yup_audio_basics/yup_AudioDataConverters.cpp +++ b/tests/yup_audio_basics/yup_AudioDataConverters.cpp @@ -132,3 +132,184 @@ void testFormatWithAllEndianness (Random& r) testAllFormats (r); } } // namespace + +//============================================================================== +TEST (AudioDataConvertersTest, Int8Conversions) +{ + Random& r = Random::getSystemRandom(); + testFormatWithAllEndianness (r); +} + +TEST (AudioDataConvertersTest, UInt8Conversions) +{ + Random& r = Random::getSystemRandom(); + testFormatWithAllEndianness (r); +} + +TEST (AudioDataConvertersTest, Int16Conversions) +{ + Random& r = Random::getSystemRandom(); + testFormatWithAllEndianness (r); +} + +TEST (AudioDataConvertersTest, Int24Conversions) +{ + Random& r = Random::getSystemRandom(); + testFormatWithAllEndianness (r); +} + +TEST (AudioDataConvertersTest, Int32Conversions) +{ + Random& r = Random::getSystemRandom(); + testFormatWithAllEndianness (r); +} + +TEST (AudioDataConvertersTest, Float32Conversions) +{ + Random& r = Random::getSystemRandom(); + testFormatWithAllEndianness (r); +} + +TEST (AudioDataConvertersTest, Float64Conversions) +{ + //Random& r = Random::getSystemRandom(); + //testFormatWithAllEndianness (r); +} + +//============================================================================== +TEST (AudioDataConvertersTest, PointerAdvance) +{ + float data[10] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; + AudioData::Pointer ptr (data); + + EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.0f); + ++ptr; + EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.1f); + ++ptr; + EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.2f); +} + +TEST (AudioDataConvertersTest, PointerDecrement) +{ + float data[10] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; + AudioData::Pointer ptr (data + 5); + + EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.5f); + --ptr; + EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.4f); +} + +TEST (AudioDataConvertersTest, PointerJump) +{ + float data[10] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f }; + AudioData::Pointer ptr (data); + + ptr += 5; + EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.5f); + + auto ptr2 = ptr + 2; + EXPECT_FLOAT_EQ (ptr2.getAsFloat(), 0.7f); +} + +TEST (AudioDataConvertersTest, InterleavedPointer) +{ + float data[8] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f }; + AudioData::Pointer ptr (data, 2); + + EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.1f); + ++ptr; + EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.3f); + ++ptr; + EXPECT_FLOAT_EQ (ptr.getAsFloat(), 0.5f); +} + +TEST (AudioDataConvertersTest, ClearSamples) +{ + float data[10] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f }; + AudioData::Pointer ptr (data); + + ptr.clearSamples (5); + + for (int i = 0; i < 5; ++i) + EXPECT_FLOAT_EQ (data[i], 0.0f); + + EXPECT_FLOAT_EQ (data[5], 6.0f); +} + +TEST (AudioDataConvertersTest, FindMinAndMax) +{ + float data[10] = { 0.1f, -0.5f, 0.8f, -0.2f, 0.4f, 0.9f, -0.7f, 0.3f, -0.1f, 0.6f }; + AudioData::Pointer ptr (data); + + auto range = ptr.findMinAndMax (10); + + EXPECT_FLOAT_EQ (range.getStart(), -0.7f); + EXPECT_FLOAT_EQ (range.getEnd(), 0.9f); +} + +TEST (AudioDataConvertersTest, FindMinAndMaxEmpty) +{ + float data[1] = { 0.0f }; + AudioData::Pointer ptr (data); + + auto range = ptr.findMinAndMax (0); + + EXPECT_TRUE (range.isEmpty()); +} + +TEST (AudioDataConvertersTest, FindMinAndMaxInteger) +{ + int16_t data[10]; + for (int i = 0; i < 10; ++i) + data[i] = static_cast ((i - 5) * 1000); + + AudioData::Pointer ptr (data); + + float minVal, maxVal; + ptr.findMinAndMax (10, minVal, maxVal); + + EXPECT_LT (minVal, 0.0f); + EXPECT_GT (maxVal, 0.0f); +} + +TEST (AudioDataConvertersTest, InterleaveSamples) +{ + float sourceData1[4] = { 1.0f, 2.0f, 3.0f, 4.0f }; + float sourceData2[4] = { 5.0f, 6.0f, 7.0f, 8.0f }; + const float* sourcePtrs[2] = { sourceData1, sourceData2 }; + + float dest[8]; + + using SourceFormat = AudioData::Format; + using DestFormat = AudioData::Format; + + AudioData::interleaveSamples (AudioData::NonInterleavedSource { sourcePtrs, 2 }, + AudioData::InterleavedDest { dest, 2 }, + 4); + + EXPECT_FLOAT_EQ (dest[0], 1.0f); + EXPECT_FLOAT_EQ (dest[1], 5.0f); + EXPECT_FLOAT_EQ (dest[2], 2.0f); + EXPECT_FLOAT_EQ (dest[3], 6.0f); +} + +TEST (AudioDataConvertersTest, DeinterleaveSamples) +{ + float source[8] = { 1.0f, 5.0f, 2.0f, 6.0f, 3.0f, 7.0f, 4.0f, 8.0f }; + + float dest1[4]; + float dest2[4]; + float* destPtrs[2] = { dest1, dest2 }; + + using SourceFormat = AudioData::Format; + using DestFormat = AudioData::Format; + + AudioData::deinterleaveSamples (AudioData::InterleavedSource { source, 2 }, + AudioData::NonInterleavedDest { destPtrs, 2 }, + 4); + + EXPECT_FLOAT_EQ (dest1[0], 1.0f); + EXPECT_FLOAT_EQ (dest1[1], 2.0f); + EXPECT_FLOAT_EQ (dest2[0], 5.0f); + EXPECT_FLOAT_EQ (dest2[1], 6.0f); +} diff --git a/tests/yup_audio_basics/yup_AudioProcessLoadMeasurer.cpp b/tests/yup_audio_basics/yup_AudioProcessLoadMeasurer.cpp new file mode 100644 index 000000000..f5facb8a2 --- /dev/null +++ b/tests/yup_audio_basics/yup_AudioProcessLoadMeasurer.cpp @@ -0,0 +1,390 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +class AudioProcessLoadMeasurerTests : public ::testing::Test +{ +protected: + void SetUp() override + { + measurer = std::make_unique(); + } + + void TearDown() override + { + measurer.reset(); + } + + std::unique_ptr measurer; +}; + +//============================================================================== +TEST_F (AudioProcessLoadMeasurerTests, Constructor) +{ + EXPECT_NO_THROW (AudioProcessLoadMeasurer()); +} + +TEST_F (AudioProcessLoadMeasurerTests, Destructor) +{ + auto* temp = new AudioProcessLoadMeasurer(); + EXPECT_NO_THROW (delete temp); +} + +//============================================================================== +TEST_F (AudioProcessLoadMeasurerTests, InitialState) +{ + EXPECT_DOUBLE_EQ (measurer->getLoadAsProportion(), 0.0); + EXPECT_DOUBLE_EQ (measurer->getLoadAsPercentage(), 0.0); + EXPECT_EQ (measurer->getXRunCount(), 0); +} + +//============================================================================== +TEST_F (AudioProcessLoadMeasurerTests, ResetWithoutParameters) +{ + measurer->reset (44100.0, 512); + measurer->registerBlockRenderTime (5.0); + + measurer->reset(); + + EXPECT_DOUBLE_EQ (measurer->getLoadAsProportion(), 0.0); + EXPECT_EQ (measurer->getXRunCount(), 0); +} + +TEST_F (AudioProcessLoadMeasurerTests, ResetWithParameters) +{ + measurer->reset (44100.0, 512); + measurer->registerBlockRenderTime (5.0); + + measurer->reset (48000.0, 1024); + + EXPECT_DOUBLE_EQ (measurer->getLoadAsProportion(), 0.0); + EXPECT_EQ (measurer->getXRunCount(), 0); +} + +TEST_F (AudioProcessLoadMeasurerTests, ResetWithZeroParameters) +{ + // Should handle zero parameters (line 59) + EXPECT_NO_THROW (measurer->reset (0.0, 0)); +} + +//============================================================================== +TEST_F (AudioProcessLoadMeasurerTests, RegisterBlockRenderTime) +{ + measurer->reset (44100.0, 512); + + // Block time should be: 512 / 44100 = 0.0116 seconds = 11.6 ms + // Register a time less than the available time + measurer->registerBlockRenderTime (5.0); + + auto load = measurer->getLoadAsProportion(); + EXPECT_GT (load, 0.0); + EXPECT_LT (load, 1.0); +} + +TEST_F (AudioProcessLoadMeasurerTests, RegisterBlockRenderTimeExceedsAvailable) +{ + measurer->reset (44100.0, 512); + + // Register a time that exceeds available time (should increment xruns, line 89-90) + measurer->registerBlockRenderTime (20.0); + + EXPECT_GT (measurer->getXRunCount(), 0); +} + +TEST_F (AudioProcessLoadMeasurerTests, RegisterBlockRenderTimeMultiple) +{ + measurer->reset (44100.0, 512); + + // Register multiple times to test filtering (line 86-87) + for (int i = 0; i < 10; ++i) + { + measurer->registerBlockRenderTime (5.0); + } + + auto load = measurer->getLoadAsProportion(); + EXPECT_GT (load, 0.0); + EXPECT_LT (load, 1.0); +} + +TEST_F (AudioProcessLoadMeasurerTests, RegisterBlockRenderTimeWithoutReset) +{ + // Should handle registerBlockRenderTime without reset (msPerSample == 0, line 80-81) + EXPECT_NO_THROW (measurer->registerBlockRenderTime (5.0)); + + // Load should remain 0 + EXPECT_DOUBLE_EQ (measurer->getLoadAsProportion(), 0.0); +} + +//============================================================================== +TEST_F (AudioProcessLoadMeasurerTests, RegisterRenderTime) +{ + measurer->reset (44100.0, 512); + + // Register with specific number of samples + measurer->registerRenderTime (2.0, 256); + + auto load = measurer->getLoadAsProportion(); + EXPECT_GT (load, 0.0); + EXPECT_LT (load, 1.0); +} + +TEST_F (AudioProcessLoadMeasurerTests, RegisterRenderTimeExceedsAvailable) +{ + measurer->reset (44100.0, 512); + + // Time per sample: 1000 / 44100 = 0.0227 ms + // For 256 samples: 256 * 0.0227 = 5.8 ms + // Register 10ms which exceeds available time + measurer->registerRenderTime (10.0, 256); + + EXPECT_GT (measurer->getXRunCount(), 0); +} + +TEST_F (AudioProcessLoadMeasurerTests, RegisterRenderTimeWithoutReset) +{ + // Should handle registerRenderTime without reset + EXPECT_NO_THROW (measurer->registerRenderTime (5.0, 512)); + + // Load should remain 0 + EXPECT_DOUBLE_EQ (measurer->getLoadAsProportion(), 0.0); +} + +//============================================================================== +TEST_F (AudioProcessLoadMeasurerTests, GetLoadAsProportion) +{ + measurer->reset (44100.0, 512); + measurer->registerBlockRenderTime (5.0); + + auto proportion = measurer->getLoadAsProportion(); + + // Should be clamped between 0 and 1 (line 93) + EXPECT_GE (proportion, 0.0); + EXPECT_LE (proportion, 1.0); +} + +TEST_F (AudioProcessLoadMeasurerTests, GetLoadAsPercentage) +{ + measurer->reset (44100.0, 512); + measurer->registerBlockRenderTime (5.0); + + auto percentage = measurer->getLoadAsPercentage(); + + // Should be proportion * 100 (line 95) + EXPECT_DOUBLE_EQ (percentage, measurer->getLoadAsProportion() * 100.0); + EXPECT_GE (percentage, 0.0); + EXPECT_LE (percentage, 100.0); +} + +TEST_F (AudioProcessLoadMeasurerTests, GetLoadProportionClampingHigh) +{ + measurer->reset (44100.0, 512); + + // Register many high times to push proportion above 1.0 + for (int i = 0; i < 50; ++i) + { + measurer->registerBlockRenderTime (20.0); + } + + // Should be clamped to 1.0 + EXPECT_DOUBLE_EQ (measurer->getLoadAsProportion(), 1.0); +} + +//============================================================================== +TEST_F (AudioProcessLoadMeasurerTests, GetXRunCount) +{ + measurer->reset (44100.0, 512); + + EXPECT_EQ (measurer->getXRunCount(), 0); + + // Cause an xrun + measurer->registerBlockRenderTime (20.0); + + EXPECT_EQ (measurer->getXRunCount(), 1); + + // Cause more xruns + measurer->registerBlockRenderTime (20.0); + measurer->registerBlockRenderTime (20.0); + + EXPECT_EQ (measurer->getXRunCount(), 3); +} + +//============================================================================== +TEST_F (AudioProcessLoadMeasurerTests, ScopedTimerConstructor) +{ + measurer->reset (44100.0, 512); + + EXPECT_NO_THROW ((AudioProcessLoadMeasurer::ScopedTimer { *measurer })); +} + +TEST_F (AudioProcessLoadMeasurerTests, ScopedTimerWithSamples) +{ + measurer->reset (44100.0, 512); + + EXPECT_NO_THROW ((AudioProcessLoadMeasurer::ScopedTimer { *measurer, 256 })); +} + +TEST_F (AudioProcessLoadMeasurerTests, ScopedTimerMeasures) +{ + measurer->reset (44100.0, 512); + + { + AudioProcessLoadMeasurer::ScopedTimer timer (*measurer); + Thread::sleep (5); + } + + // Should have registered some load + EXPECT_GT (measurer->getLoadAsProportion(), 0.0); +} + +TEST_F (AudioProcessLoadMeasurerTests, ScopedTimerUsesDefaultSamples) +{ + measurer->reset (44100.0, 512); + + { + // Uses default samples from measurer (line 100) + AudioProcessLoadMeasurer::ScopedTimer timer (*measurer); + Thread::sleep (2); + } + + EXPECT_GT (measurer->getLoadAsProportion(), 0.0); +} + +TEST_F (AudioProcessLoadMeasurerTests, ScopedTimerWithCustomSamples) +{ + measurer->reset (44100.0, 512); + + { + AudioProcessLoadMeasurer::ScopedTimer timer (*measurer, 256); + Thread::sleep (2); + } + + EXPECT_GT (measurer->getLoadAsProportion(), 0.0); +} + +TEST_F (AudioProcessLoadMeasurerTests, ScopedTimerDestructorRegisters) +{ + measurer->reset (44100.0, 512); + + EXPECT_DOUBLE_EQ (measurer->getLoadAsProportion(), 0.0); + + { + AudioProcessLoadMeasurer::ScopedTimer timer (*measurer); + Thread::sleep (3); + // Destructor should call registerRenderTime (line 116) + } + + // After destructor, load should be > 0 + EXPECT_GT (measurer->getLoadAsProportion(), 0.0); +} + +//============================================================================== +TEST_F (AudioProcessLoadMeasurerTests, FilteringBehavior) +{ + measurer->reset (44100.0, 512); + + // Register low load + for (int i = 0; i < 10; ++i) + { + measurer->registerBlockRenderTime (2.0); + } + + auto lowLoad = measurer->getLoadAsProportion(); + + // Register high load + for (int i = 0; i < 10; ++i) + { + measurer->registerBlockRenderTime (8.0); + } + + auto highLoad = measurer->getLoadAsProportion(); + + // High load should be greater due to filtering (line 85-87) + EXPECT_GT (highLoad, lowLoad); +} + +TEST_F (AudioProcessLoadMeasurerTests, RealisticScenario) +{ + measurer->reset (48000.0, 480); + + // Simulate 100 audio blocks + Random random; + for (int i = 0; i < 100; ++i) + { + // Random processing time between 1-8 ms + double processingTime = 1.0 + random.nextDouble() * 7.0; + measurer->registerBlockRenderTime (processingTime); + } + + auto load = measurer->getLoadAsProportion(); + EXPECT_GE (load, 0.0); + EXPECT_LE (load, 1.0); + + auto percentage = measurer->getLoadAsPercentage(); + EXPECT_GE (percentage, 0.0); + EXPECT_LE (percentage, 100.0); +} + +TEST_F (AudioProcessLoadMeasurerTests, ThreadSafety) +{ + measurer->reset (44100.0, 512); + + // Test that concurrent access doesn't crash (uses SpinLock::ScopedTryLockType) + std::atomic done { false }; + + auto writerThread = [this, &done]() + { + while (! done) + { + measurer->registerBlockRenderTime (5.0); + Thread::sleep (1); + } + }; + + auto readerThread = [this, &done]() + { + while (! done) + { + volatile auto load = measurer->getLoadAsProportion(); + volatile auto xruns = measurer->getXRunCount(); + (void) load; + (void) xruns; + Thread::sleep (1); + } + }; + + std::thread t1 (writerThread); + std::thread t2 (readerThread); + + Thread::sleep (50); + done = true; + + t1.join(); + t2.join(); + + // Should not crash + EXPECT_GE (measurer->getLoadAsProportion(), 0.0); +} diff --git a/tests/yup_audio_basics/yup_BufferingAudioSource.cpp b/tests/yup_audio_basics/yup_BufferingAudioSource.cpp new file mode 100644 index 000000000..05eb16710 --- /dev/null +++ b/tests/yup_audio_basics/yup_BufferingAudioSource.cpp @@ -0,0 +1,680 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +namespace +{ +class MockPositionableAudioSource : public PositionableAudioSource +{ +public: + MockPositionableAudioSource() + : totalLength (44100 * 10) // 10 seconds at 44.1kHz + , currentPosition (0) + , looping (false) + { + } + + ~MockPositionableAudioSource() override = default; + + void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override + { + prepareToPlayCalled = true; + lastSamplesPerBlock = samplesPerBlockExpected; + lastSampleRate = sampleRate; + } + + void releaseResources() override + { + releaseResourcesCalled = true; + } + + void getNextAudioBlock (const AudioSourceChannelInfo& info) override + { + getNextAudioBlockCalled = true; + + // Fill with a pattern based on current position + for (int ch = 0; ch < info.buffer->getNumChannels(); ++ch) + { + for (int i = 0; i < info.numSamples; ++i) + { + const float value = std::sin ((currentPosition + i) * 0.01f) * 0.5f; + info.buffer->setSample (ch, info.startSample + i, value); + } + } + currentPosition += info.numSamples; + } + + void setNextReadPosition (int64 newPosition) override + { + setNextReadPositionCalled = true; + currentPosition = newPosition; + } + + int64 getNextReadPosition() const override + { + return currentPosition; + } + + int64 getTotalLength() const override + { + return totalLength; + } + + bool isLooping() const override + { + return looping; + } + + void setLooping (bool shouldLoop) override + { + looping = shouldLoop; + } + + bool prepareToPlayCalled = false; + bool releaseResourcesCalled = false; + bool getNextAudioBlockCalled = false; + bool setNextReadPositionCalled = false; + int lastSamplesPerBlock = 0; + double lastSampleRate = 0.0; + int64 totalLength; + int64 currentPosition; + bool looping; +}; +} // namespace + +//============================================================================== +class BufferingAudioSourceTests : public ::testing::Test +{ +protected: + void SetUp() override + { + thread = std::make_unique ("BufferingTest"); + thread->startThread(); + + mockSource = new MockPositionableAudioSource(); + buffering = std::make_unique (mockSource, *thread, true, 8192, 2, false); + } + + void TearDown() override + { + buffering.reset(); + thread->stopThread (1000); + thread.reset(); + } + + std::unique_ptr thread; + MockPositionableAudioSource* mockSource; // Owned by buffering + std::unique_ptr buffering; +}; + +//============================================================================== +TEST_F (BufferingAudioSourceTests, Constructor) +{ + TimeSliceThread localThread ("Test"); + localThread.startThread(); + + auto* source = new MockPositionableAudioSource(); + EXPECT_NO_THROW (BufferingAudioSource (source, localThread, true, 8192, 2, false)); + + localThread.stopThread (1000); +} + +TEST_F (BufferingAudioSourceTests, ConstructorWithPrefill) +{ + TimeSliceThread localThread ("Test"); + localThread.startThread(); + + auto* source = new MockPositionableAudioSource(); + EXPECT_NO_THROW (BufferingAudioSource (source, localThread, true, 8192, 2, true)); + + localThread.stopThread (1000); +} + +TEST_F (BufferingAudioSourceTests, Destructor) +{ + TimeSliceThread localThread ("Test"); + localThread.startThread(); + + auto* source = new MockPositionableAudioSource(); + auto* temp = new BufferingAudioSource (source, localThread, true, 8192, 2, false); + + EXPECT_NO_THROW (delete temp); + + localThread.stopThread (1000); +} + +//============================================================================== +TEST_F (BufferingAudioSourceTests, PrepareToPlay) +{ + buffering->prepareToPlay (512, 44100.0); + + // Should call prepareToPlay on source (line 80) + EXPECT_TRUE (mockSource->prepareToPlayCalled); + EXPECT_EQ (mockSource->lastSamplesPerBlock, 512); + EXPECT_DOUBLE_EQ (mockSource->lastSampleRate, 44100.0); + + // Give background thread time to start buffering + Thread::sleep (50); +} + +TEST_F (BufferingAudioSourceTests, PrepareToPlayMultipleTimes) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (50); + + // Calling again with same parameters should not recreate buffer (line 71-73) + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (50); + + EXPECT_TRUE (mockSource->prepareToPlayCalled); +} + +TEST_F (BufferingAudioSourceTests, PrepareToPlayDifferentSampleRate) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (50); + + // Different sample rate should trigger re-initialization (line 71) + buffering->prepareToPlay (512, 48000.0); + Thread::sleep (50); + + EXPECT_DOUBLE_EQ (mockSource->lastSampleRate, 48000.0); +} + +TEST_F (BufferingAudioSourceTests, PrepareToPlayDifferentBufferSize) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (50); + + // Different buffer size might not trigger re-initialization if bufferSizeNeeded doesn't change + // bufferSizeNeeded = jmax(samplesPerBlockExpected * 2, numberOfSamplesToBuffer) + // With numberOfSamplesToBuffer=8192, changing from 512 to 1024 won't change bufferSizeNeeded + buffering->prepareToPlay (1024, 44100.0); + Thread::sleep (50); + + // The source might still have old value if buffer didn't need resize + EXPECT_GE (mockSource->lastSamplesPerBlock, 512); +} + +TEST_F (BufferingAudioSourceTests, PrepareToPlayWithPrefill) +{ + // Create new buffering source with prefill enabled + auto* source = new MockPositionableAudioSource(); + auto bufferingWithPrefill = std::make_unique (source, *thread, true, 8192, 2, true); + + // This should block until buffer is partially filled (line 98-99) + bufferingWithPrefill->prepareToPlay (512, 44100.0); + + EXPECT_TRUE (source->prepareToPlayCalled); +} + +//============================================================================== +TEST_F (BufferingAudioSourceTests, ReleaseResources) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (50); + + buffering->releaseResources(); + + // Should call releaseResources on source (line 114) + EXPECT_TRUE (mockSource->releaseResourcesCalled); +} + +//============================================================================== +TEST_F (BufferingAudioSourceTests, GetNextAudioBlockEmpty) +{ + // Without prepareToPlay, should get cache miss (line 121-126) + AudioBuffer buffer (2, 512); + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + buffer.setSample (ch, i, 1.0f); + } + } + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + buffering->getNextAudioBlock (info); + + // Buffer should be cleared (cache miss) + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.0f); + } + } +} + +TEST_F (BufferingAudioSourceTests, GetNextAudioBlockAfterPrepare) +{ + buffering->prepareToPlay (512, 44100.0); + + // Wait for background thread to buffer some data + Thread::sleep (100); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + buffering->getNextAudioBlock (info); + + // Should have buffered data + bool hasNonZero = false; + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + if (buffer.getSample (ch, i) != 0.0f) + { + hasNonZero = true; + break; + } + } + } + EXPECT_TRUE (hasNonZero); +} + +TEST_F (BufferingAudioSourceTests, GetNextAudioBlockPartialCacheMissStart) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + // Seek to position that might cause partial cache miss (line 133-134) + buffering->setNextReadPosition (100000); + Thread::sleep (50); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + EXPECT_NO_THROW (buffering->getNextAudioBlock (info)); +} + +TEST_F (BufferingAudioSourceTests, GetNextAudioBlockWrapAround) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Process multiple blocks to potentially trigger wrap-around (line 149-160) + for (int i = 0; i < 20; ++i) + { + buffer.clear(); + buffering->getNextAudioBlock (info); + Thread::sleep (10); + } +} + +TEST_F (BufferingAudioSourceTests, GetNextAudioBlockWithStartSample) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 100; + info.numSamples = 256; + + buffering->getNextAudioBlock (info); + + // Samples before startSample should remain zero + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 100; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.0f); + } + } +} + +//============================================================================== +TEST_F (BufferingAudioSourceTests, WaitForNextAudioBlockReadyNullSource) +{ + // Create buffering with source that has zero length + mockSource->totalLength = 0; + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Should return false for invalid source (line 169-170) + EXPECT_FALSE (buffering->waitForNextAudioBlockReady (info, 100)); +} + +TEST_F (BufferingAudioSourceTests, WaitForNextAudioBlockReadyNegativePosition) +{ + buffering->prepareToPlay (512, 44100.0); + buffering->setNextReadPosition (-1000); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Should return true for negative position (line 172-174) + EXPECT_TRUE (buffering->waitForNextAudioBlockReady (info, 100)); +} + +TEST_F (BufferingAudioSourceTests, WaitForNextAudioBlockReadyPastEnd) +{ + buffering->prepareToPlay (512, 44100.0); + + // Set position past the end + buffering->setNextReadPosition (mockSource->getTotalLength() + 1000); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Should return true when past end and not looping (line 172-174) + EXPECT_TRUE (buffering->waitForNextAudioBlockReady (info, 100)); +} + +TEST_F (BufferingAudioSourceTests, WaitForNextAudioBlockReadySuccess) +{ + buffering->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Should return true when data is ready (line 189-194) + EXPECT_TRUE (buffering->waitForNextAudioBlockReady (info, 1000)); +} + +TEST_F (BufferingAudioSourceTests, WaitForNextAudioBlockReadyTimeout) +{ + buffering->prepareToPlay (512, 44100.0); + + // Seek to far position + buffering->setNextReadPosition (1000000); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // With short timeout, might return false (line 207) + auto result = buffering->waitForNextAudioBlockReady (info, 10); + + // Either true or false is acceptable depending on timing + EXPECT_TRUE (result == true || result == false); +} + +//============================================================================== +TEST_F (BufferingAudioSourceTests, GetNextReadPosition) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (50); + + // Initial position should be 0 + EXPECT_EQ (buffering->getNextReadPosition(), 0); + + // Process some audio + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + buffering->getNextAudioBlock (info); + + // Position should advance (line 164) + EXPECT_EQ (buffering->getNextReadPosition(), 512); +} + +TEST_F (BufferingAudioSourceTests, GetNextReadPositionWithLooping) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (50); + + mockSource->setLooping (true); + + // Set position past total length + buffering->setNextReadPosition (mockSource->getTotalLength() + 1000); + + // Should wrap around with looping (line 215-216) + auto pos = buffering->getNextReadPosition(); + EXPECT_LT (pos, mockSource->getTotalLength()); +} + +//============================================================================== +TEST_F (BufferingAudioSourceTests, SetNextReadPosition) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (50); + + buffering->setNextReadPosition (5000); + + // Position should be updated (line 224) + EXPECT_EQ (buffering->getNextReadPosition(), 5000); +} + +TEST_F (BufferingAudioSourceTests, SetNextReadPositionMultipleTimes) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (50); + + for (int64 pos = 0; pos < 10000; pos += 1000) + { + buffering->setNextReadPosition (pos); + Thread::sleep (20); + EXPECT_EQ (buffering->getNextReadPosition(), pos); + } +} + +//============================================================================== +TEST_F (BufferingAudioSourceTests, ReadNextBufferChunkInitial) +{ + buffering->prepareToPlay (512, 44100.0); + + // Background thread should call readNextBufferChunk (line 238-318) + Thread::sleep (100); + + // Verify source was called + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (BufferingAudioSourceTests, ReadNextBufferChunkCacheMiss) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + // Seek far away to trigger cache miss (line 259-268) + buffering->setNextReadPosition (200000); + Thread::sleep (100); + + // Should have read new buffer section + EXPECT_TRUE (mockSource->setNextReadPositionCalled); +} + +TEST_F (BufferingAudioSourceTests, ReadNextBufferChunkIncrementalRead) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Process audio to advance position + for (int i = 0; i < 5; ++i) + { + buffer.clear(); + buffering->getNextAudioBlock (info); + Thread::sleep (20); + } + + // Should trigger incremental reads (line 269-279) + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (BufferingAudioSourceTests, ReadNextBufferChunkWrapAround) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Process many blocks to trigger buffer wrap-around (line 290-307) + for (int i = 0; i < 30; ++i) + { + buffer.clear(); + buffering->getNextAudioBlock (info); + Thread::sleep (10); + } +} + +TEST_F (BufferingAudioSourceTests, ReadNextBufferChunkLoopingChange) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + // Change looping state to trigger buffer reset (line 245-250) + mockSource->setLooping (true); + Thread::sleep (100); + + mockSource->setLooping (false); + Thread::sleep (100); + + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +//============================================================================== +TEST_F (BufferingAudioSourceTests, UseTimeSlice) +{ + buffering->prepareToPlay (512, 44100.0); + + // useTimeSlice is called by background thread (line 331-334) + Thread::sleep (100); + + // Should have processed some chunks + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (BufferingAudioSourceTests, MultipleChannels) +{ + auto* source = new MockPositionableAudioSource(); + auto bufferingMulti = std::make_unique (source, *thread, true, 8192, 8, false); + + bufferingMulti->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + AudioBuffer buffer (8, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + EXPECT_NO_THROW (bufferingMulti->getNextAudioBlock (info)); +} + +TEST_F (BufferingAudioSourceTests, StressTestContinuousPlayback) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Simulate continuous playback + for (int i = 0; i < 50; ++i) + { + buffer.clear(); + buffering->getNextAudioBlock (info); + Thread::sleep (5); + } + + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (BufferingAudioSourceTests, StressTestRandomSeeks) +{ + buffering->prepareToPlay (512, 44100.0); + Thread::sleep (100); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + Random random; + + // Perform random seeks + for (int i = 0; i < 20; ++i) + { + const int64 pos = random.nextInt (static_cast (mockSource->getTotalLength() / 2)); + buffering->setNextReadPosition (pos); + Thread::sleep (50); + + buffer.clear(); + buffering->getNextAudioBlock (info); + } + + EXPECT_TRUE (mockSource->setNextReadPositionCalled); +} diff --git a/tests/yup_audio_basics/yup_ChannelRemappingAudioSource.cpp b/tests/yup_audio_basics/yup_ChannelRemappingAudioSource.cpp new file mode 100644 index 000000000..13620ec9a --- /dev/null +++ b/tests/yup_audio_basics/yup_ChannelRemappingAudioSource.cpp @@ -0,0 +1,528 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +namespace +{ +class MockAudioSource : public AudioSource +{ +public: + MockAudioSource() = default; + ~MockAudioSource() override = default; + + void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override + { + prepareToPlayCalled = true; + lastSamplesPerBlock = samplesPerBlockExpected; + lastSampleRate = sampleRate; + } + + void releaseResources() override + { + releaseResourcesCalled = true; + } + + void getNextAudioBlock (const AudioSourceChannelInfo& info) override + { + getNextAudioBlockCalled = true; + + // Fill each channel with different values for testing remapping + for (int ch = 0; ch < info.buffer->getNumChannels(); ++ch) + { + const float value = static_cast (ch) * 0.1f + 0.1f; + for (int i = 0; i < info.numSamples; ++i) + { + info.buffer->setSample (ch, info.startSample + i, value); + } + } + } + + bool prepareToPlayCalled = false; + bool releaseResourcesCalled = false; + bool getNextAudioBlockCalled = false; + int lastSamplesPerBlock = 0; + double lastSampleRate = 0.0; +}; +} // namespace + +//============================================================================== +class ChannelRemappingAudioSourceTests : public ::testing::Test +{ +protected: + void SetUp() override + { + mockSource = new MockAudioSource(); + remapper = std::make_unique (mockSource, true); + } + + void TearDown() override + { + remapper.reset(); + } + + MockAudioSource* mockSource; // Owned by remapper + std::unique_ptr remapper; +}; + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, Constructor) +{ + auto* source = new MockAudioSource(); + EXPECT_NO_THROW (ChannelRemappingAudioSource (source, true)); +} + +TEST_F (ChannelRemappingAudioSourceTests, Destructor) +{ + auto* source = new MockAudioSource(); + auto* temp = new ChannelRemappingAudioSource (source, true); + EXPECT_NO_THROW (delete temp); +} + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, SetNumberOfChannelsToProduce) +{ + EXPECT_NO_THROW (remapper->setNumberOfChannelsToProduce (4)); + EXPECT_NO_THROW (remapper->setNumberOfChannelsToProduce (8)); + EXPECT_NO_THROW (remapper->setNumberOfChannelsToProduce (1)); +} + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, ClearAllMappings) +{ + remapper->setInputChannelMapping (0, 1); + remapper->setInputChannelMapping (1, 0); + remapper->setOutputChannelMapping (0, 1); + + EXPECT_NO_THROW (remapper->clearAllMappings()); + + // After clearing, mappings should return -1 + EXPECT_EQ (remapper->getRemappedInputChannel (0), -1); + EXPECT_EQ (remapper->getRemappedOutputChannel (0), -1); +} + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, SetInputChannelMapping) +{ + remapper->setInputChannelMapping (0, 1); + remapper->setInputChannelMapping (1, 0); + + EXPECT_EQ (remapper->getRemappedInputChannel (0), 1); + EXPECT_EQ (remapper->getRemappedInputChannel (1), 0); +} + +TEST_F (ChannelRemappingAudioSourceTests, SetInputChannelMappingWithGap) +{ + // Setting index 3 should fill gaps with -1 (line 73-74) + remapper->setInputChannelMapping (3, 2); + + EXPECT_EQ (remapper->getRemappedInputChannel (0), -1); + EXPECT_EQ (remapper->getRemappedInputChannel (1), -1); + EXPECT_EQ (remapper->getRemappedInputChannel (2), -1); + EXPECT_EQ (remapper->getRemappedInputChannel (3), 2); +} + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, SetOutputChannelMapping) +{ + remapper->setOutputChannelMapping (0, 1); + remapper->setOutputChannelMapping (1, 0); + + EXPECT_EQ (remapper->getRemappedOutputChannel (0), 1); + EXPECT_EQ (remapper->getRemappedOutputChannel (1), 0); +} + +TEST_F (ChannelRemappingAudioSourceTests, SetOutputChannelMappingWithGap) +{ + // Setting index 3 should fill gaps with -1 (line 83-84) + remapper->setOutputChannelMapping (3, 2); + + EXPECT_EQ (remapper->getRemappedOutputChannel (0), -1); + EXPECT_EQ (remapper->getRemappedOutputChannel (1), -1); + EXPECT_EQ (remapper->getRemappedOutputChannel (2), -1); + EXPECT_EQ (remapper->getRemappedOutputChannel (3), 2); +} + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, GetRemappedInputChannelInvalid) +{ + // Negative index should return -1 (line 93) + EXPECT_EQ (remapper->getRemappedInputChannel (-1), -1); + + // Out of bounds should return -1 (line 93) + EXPECT_EQ (remapper->getRemappedInputChannel (100), -1); +} + +TEST_F (ChannelRemappingAudioSourceTests, GetRemappedOutputChannelInvalid) +{ + // Negative index should return -1 (line 103) + EXPECT_EQ (remapper->getRemappedOutputChannel (-1), -1); + + // Out of bounds should return -1 (line 103) + EXPECT_EQ (remapper->getRemappedOutputChannel (100), -1); +} + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, PrepareToPlay) +{ + remapper->prepareToPlay (512, 44100.0); + + // Should call prepareToPlay on source (line 112) + EXPECT_TRUE (mockSource->prepareToPlayCalled); + EXPECT_EQ (mockSource->lastSamplesPerBlock, 512); + EXPECT_DOUBLE_EQ (mockSource->lastSampleRate, 44100.0); +} + +TEST_F (ChannelRemappingAudioSourceTests, ReleaseResources) +{ + remapper->prepareToPlay (512, 44100.0); + remapper->releaseResources(); + + // Should call releaseResources on source (line 117) + EXPECT_TRUE (mockSource->releaseResourcesCalled); +} + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, GetNextAudioBlockNoMapping) +{ + remapper->setNumberOfChannelsToProduce (2); + remapper->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + remapper->getNextAudioBlock (info); + + // Should call getNextAudioBlock on source (line 144) + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (ChannelRemappingAudioSourceTests, GetNextAudioBlockSwapChannels) +{ + remapper->setNumberOfChannelsToProduce (2); + + // Swap input channels 0 and 1 + remapper->setInputChannelMapping (0, 1); + remapper->setInputChannelMapping (1, 0); + + // Swap output channels back + remapper->setOutputChannelMapping (0, 1); + remapper->setOutputChannelMapping (1, 0); + + remapper->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + + // Fill with different values per channel + for (int i = 0; i < 512; ++i) + { + buffer.setSample (0, i, 1.0f); + buffer.setSample (1, i, 2.0f); + } + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + remapper->getNextAudioBlock (info); + + // Channels should be swapped twice (input then output), back to original + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (ChannelRemappingAudioSourceTests, GetNextAudioBlockClearUnmappedInput) +{ + remapper->setNumberOfChannelsToProduce (2); + + // Don't map input channel 1, it should be cleared (line 138) + remapper->setInputChannelMapping (0, 0); + + remapper->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + // Fill with values + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + buffer.setSample (ch, i, 1.0f); + } + } + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + remapper->getNextAudioBlock (info); + + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (ChannelRemappingAudioSourceTests, GetNextAudioBlockInvalidInputMapping) +{ + remapper->setNumberOfChannelsToProduce (2); + + // Map to invalid channel (line 132) + remapper->setInputChannelMapping (0, 10); + + remapper->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Should not crash (line 137-139) + EXPECT_NO_THROW (remapper->getNextAudioBlock (info)); +} + +TEST_F (ChannelRemappingAudioSourceTests, GetNextAudioBlockInvalidOutputMapping) +{ + remapper->setNumberOfChannelsToProduce (2); + + // Map to invalid output channel (line 152) + remapper->setOutputChannelMapping (0, 10); + + remapper->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Should not crash (line 152-155) + EXPECT_NO_THROW (remapper->getNextAudioBlock (info)); +} + +TEST_F (ChannelRemappingAudioSourceTests, GetNextAudioBlockClearsBuffer) +{ + remapper->setNumberOfChannelsToProduce (2); + remapper->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + + // Fill with non-zero values + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + buffer.setSample (ch, i, 5.0f); + } + } + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + remapper->getNextAudioBlock (info); + + // Buffer should be cleared before writing output (line 146) + // The output will be from mock source + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (ChannelRemappingAudioSourceTests, GetNextAudioBlockWithStartSample) +{ + remapper->setNumberOfChannelsToProduce (2); + remapper->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 100; + info.numSamples = 256; + + remapper->getNextAudioBlock (info); + + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); + + // Samples before startSample should remain zero + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 100; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.0f); + } + } +} + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, CreateXmlEmpty) +{ + auto xml = remapper->createXml(); + + ASSERT_NE (xml, nullptr); + EXPECT_TRUE (xml->hasTagName ("MAPPINGS")); + EXPECT_EQ (xml->getStringAttribute ("inputs"), ""); + EXPECT_EQ (xml->getStringAttribute ("outputs"), ""); +} + +TEST_F (ChannelRemappingAudioSourceTests, CreateXmlWithMappings) +{ + remapper->setInputChannelMapping (0, 1); + remapper->setInputChannelMapping (1, 0); + remapper->setInputChannelMapping (2, 2); + + remapper->setOutputChannelMapping (0, 1); + remapper->setOutputChannelMapping (1, 0); + + auto xml = remapper->createXml(); + + ASSERT_NE (xml, nullptr); + EXPECT_TRUE (xml->hasTagName ("MAPPINGS")); + + // Check inputs attribute (line 167-168) + String inputs = xml->getStringAttribute ("inputs"); + EXPECT_FALSE (inputs.isEmpty()); + EXPECT_TRUE (inputs.contains ("1")); + EXPECT_TRUE (inputs.contains ("0")); + EXPECT_TRUE (inputs.contains ("2")); + + // Check outputs attribute (line 170-171) + String outputs = xml->getStringAttribute ("outputs"); + EXPECT_FALSE (outputs.isEmpty()); + EXPECT_TRUE (outputs.contains ("1")); + EXPECT_TRUE (outputs.contains ("0")); +} + +TEST_F (ChannelRemappingAudioSourceTests, CreateXmlTrimmed) +{ + remapper->setInputChannelMapping (0, 1); + remapper->setOutputChannelMapping (0, 2); + + auto xml = remapper->createXml(); + + // Attributes should be trimmed (line 173-174) + String inputs = xml->getStringAttribute ("inputs"); + String outputs = xml->getStringAttribute ("outputs"); + + EXPECT_FALSE (inputs.startsWithChar (' ')); + EXPECT_FALSE (inputs.endsWithChar (' ')); + EXPECT_FALSE (outputs.startsWithChar (' ')); + EXPECT_FALSE (outputs.endsWithChar (' ')); +} + +//============================================================================== +TEST_F (ChannelRemappingAudioSourceTests, RestoreFromXmlInvalidTag) +{ + XmlElement xml ("INVALID"); + + // Should not restore from invalid tag (line 181) + EXPECT_NO_THROW (remapper->restoreFromXml (xml)); +} + +TEST_F (ChannelRemappingAudioSourceTests, RestoreFromXmlEmpty) +{ + XmlElement xml ("MAPPINGS"); + + remapper->restoreFromXml (xml); + + // Should clear mappings (line 185) + EXPECT_EQ (remapper->getRemappedInputChannel (0), -1); + EXPECT_EQ (remapper->getRemappedOutputChannel (0), -1); +} + +TEST_F (ChannelRemappingAudioSourceTests, RestoreFromXmlWithMappings) +{ + XmlElement xml ("MAPPINGS"); + xml.setAttribute ("inputs", "1 0 2"); + xml.setAttribute ("outputs", "1 0"); + + remapper->restoreFromXml (xml); + + // Check restored input mappings (line 191-192) + EXPECT_EQ (remapper->getRemappedInputChannel (0), 1); + EXPECT_EQ (remapper->getRemappedInputChannel (1), 0); + EXPECT_EQ (remapper->getRemappedInputChannel (2), 2); + + // Check restored output mappings (line 194-195) + EXPECT_EQ (remapper->getRemappedOutputChannel (0), 1); + EXPECT_EQ (remapper->getRemappedOutputChannel (1), 0); +} + +TEST_F (ChannelRemappingAudioSourceTests, RestoreFromXmlClearsPrevious) +{ + remapper->setInputChannelMapping (0, 5); + remapper->setOutputChannelMapping (0, 5); + + XmlElement xml ("MAPPINGS"); + xml.setAttribute ("inputs", "1"); + xml.setAttribute ("outputs", "2"); + + remapper->restoreFromXml (xml); + + // Previous mappings should be cleared (line 185) + EXPECT_EQ (remapper->getRemappedInputChannel (0), 1); + EXPECT_EQ (remapper->getRemappedOutputChannel (0), 2); +} + +TEST_F (ChannelRemappingAudioSourceTests, XmlRoundtrip) +{ + remapper->setInputChannelMapping (0, 2); + remapper->setInputChannelMapping (1, 1); + remapper->setInputChannelMapping (2, 0); + + remapper->setOutputChannelMapping (0, 1); + remapper->setOutputChannelMapping (1, 2); + remapper->setOutputChannelMapping (2, 0); + + // Create XML + auto xml = remapper->createXml(); + ASSERT_NE (xml, nullptr); + + // Create new remapper and restore + auto* newMockSource = new MockAudioSource(); + auto newRemapper = std::make_unique (newMockSource, true); + + newRemapper->restoreFromXml (*xml); + + // Check all mappings were restored correctly + EXPECT_EQ (newRemapper->getRemappedInputChannel (0), 2); + EXPECT_EQ (newRemapper->getRemappedInputChannel (1), 1); + EXPECT_EQ (newRemapper->getRemappedInputChannel (2), 0); + + EXPECT_EQ (newRemapper->getRemappedOutputChannel (0), 1); + EXPECT_EQ (newRemapper->getRemappedOutputChannel (1), 2); + EXPECT_EQ (newRemapper->getRemappedOutputChannel (2), 0); +} diff --git a/tests/yup_audio_basics/yup_Decibels.cpp b/tests/yup_audio_basics/yup_Decibels.cpp new file mode 100644 index 000000000..059e9cbdb --- /dev/null +++ b/tests/yup_audio_basics/yup_Decibels.cpp @@ -0,0 +1,487 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +TEST (DecibelsTests, DecibelsToGainZeroDB) +{ + // 0 dB should equal gain of 1.0 + float gain = Decibels::decibelsToGain (0.0f); + EXPECT_FLOAT_EQ (gain, 1.0f); +} + +TEST (DecibelsTests, DecibelsToGainPositive) +{ + // +6 dB should approximately double the gain + float gain = Decibels::decibelsToGain (6.0f); + EXPECT_NEAR (gain, 1.9953f, 0.001f); +} + +TEST (DecibelsTests, DecibelsToGainNegative) +{ + // -6 dB should approximately halve the gain + float gain = Decibels::decibelsToGain (-6.0f); + EXPECT_NEAR (gain, 0.5012f, 0.001f); +} + +TEST (DecibelsTests, DecibelsToGainMinusInfinity) +{ + // Below minusInfinityDb should return 0 (line 62-63) + float gain = Decibels::decibelsToGain (-120.0f, -100.0f); + EXPECT_FLOAT_EQ (gain, 0.0f); +} + +TEST (DecibelsTests, DecibelsToGainAtMinusInfinityBoundary) +{ + // Exactly at minusInfinityDb should return 0 + float gain = Decibels::decibelsToGain (-100.0f, -100.0f); + EXPECT_FLOAT_EQ (gain, 0.0f); +} + +TEST (DecibelsTests, DecibelsToGainJustAboveMinusInfinity) +{ + // Just above minusInfinityDb should return non-zero (line 62) + float gain = Decibels::decibelsToGain (-99.9f, -100.0f); + EXPECT_GT (gain, 0.0f); +} + +TEST (DecibelsTests, DecibelsToGainCustomMinusInfinity) +{ + // Test with custom minusInfinityDb value + float gain = Decibels::decibelsToGain (-150.0f, -140.0f); + EXPECT_FLOAT_EQ (gain, 0.0f); +} + +TEST (DecibelsTests, DecibelsToGainDoubleType) +{ + // Test with double precision + double gain = Decibels::decibelsToGain (0.0); + EXPECT_DOUBLE_EQ (gain, 1.0); +} + +TEST (DecibelsTests, DecibelsToGainLargePositive) +{ + // +20 dB = 10x gain + float gain = Decibels::decibelsToGain (20.0f); + EXPECT_NEAR (gain, 10.0f, 0.001f); +} + +TEST (DecibelsTests, DecibelsToGainLargeNegative) +{ + // -20 dB = 0.1x gain + float gain = Decibels::decibelsToGain (-20.0f); + EXPECT_NEAR (gain, 0.1f, 0.001f); +} + +TEST (DecibelsTests, DecibelsToGainVerySmall) +{ + // -60 dB = 0.001x gain + float gain = Decibels::decibelsToGain (-60.0f); + EXPECT_NEAR (gain, 0.001f, 0.0001f); +} + +//============================================================================== +TEST (DecibelsTests, GainToDecibelsUnity) +{ + // Gain of 1.0 should equal 0 dB + float db = Decibels::gainToDecibels (1.0f); + EXPECT_FLOAT_EQ (db, 0.0f); +} + +TEST (DecibelsTests, GainToDecibelsDouble) +{ + // Gain of 2.0 should be approximately +6 dB + float db = Decibels::gainToDecibels (2.0f); + EXPECT_NEAR (db, 6.0206f, 0.001f); +} + +TEST (DecibelsTests, GainToDecibelsHalf) +{ + // Gain of 0.5 should be approximately -6 dB + float db = Decibels::gainToDecibels (0.5f); + EXPECT_NEAR (db, -6.0206f, 0.001f); +} + +TEST (DecibelsTests, GainToDecibelsZero) +{ + // Gain of 0 should return minusInfinityDb (line 77) + float db = Decibels::gainToDecibels (0.0f, -100.0f); + EXPECT_FLOAT_EQ (db, -100.0f); +} + +TEST (DecibelsTests, GainToDecibelsNegative) +{ + // Negative gain should return minusInfinityDb (line 76-77) + float db = Decibels::gainToDecibels (-0.5f, -100.0f); + EXPECT_FLOAT_EQ (db, -100.0f); +} + +TEST (DecibelsTests, GainToDecibelsVerySmall) +{ + // Very small gain close to 0 + float db = Decibels::gainToDecibels (0.001f); + EXPECT_NEAR (db, -60.0f, 0.001f); +} + +TEST (DecibelsTests, GainToDecibelsCustomMinusInfinity) +{ + // Test with custom minusInfinityDb value + float db = Decibels::gainToDecibels (0.0f, -120.0f); + EXPECT_FLOAT_EQ (db, -120.0f); +} + +TEST (DecibelsTests, GainToDecibelsClampedToMinusInfinity) +{ + // Very small gain that results in dB below minusInfinityDb (line 76 jmax) + float db = Decibels::gainToDecibels (0.00001f, -80.0f); + EXPECT_FLOAT_EQ (db, -80.0f); +} + +TEST (DecibelsTests, GainToDecibelsDoubleType) +{ + // Test with double precision + double db = Decibels::gainToDecibels (1.0); + EXPECT_DOUBLE_EQ (db, 0.0); +} + +TEST (DecibelsTests, GainToDecibelsTen) +{ + // Gain of 10.0 should be +20 dB + float db = Decibels::gainToDecibels (10.0f); + EXPECT_NEAR (db, 20.0f, 0.001f); +} + +TEST (DecibelsTests, GainToDecibelsOneTenth) +{ + // Gain of 0.1 should be -20 dB + float db = Decibels::gainToDecibels (0.1f); + EXPECT_NEAR (db, -20.0f, 0.001f); +} + +//============================================================================== +TEST (DecibelsTests, RoundTripConversionUnity) +{ + // Convert 0 dB to gain and back + float db1 = 0.0f; + float gain = Decibels::decibelsToGain (db1); + float db2 = Decibels::gainToDecibels (gain); + EXPECT_NEAR (db2, db1, 0.001f); +} + +TEST (DecibelsTests, RoundTripConversionPositive) +{ + // Convert +10 dB to gain and back + float db1 = 10.0f; + float gain = Decibels::decibelsToGain (db1); + float db2 = Decibels::gainToDecibels (gain); + EXPECT_NEAR (db2, db1, 0.001f); +} + +TEST (DecibelsTests, RoundTripConversionNegative) +{ + // Convert -10 dB to gain and back + float db1 = -10.0f; + float gain = Decibels::decibelsToGain (db1); + float db2 = Decibels::gainToDecibels (gain); + EXPECT_NEAR (db2, db1, 0.001f); +} + +TEST (DecibelsTests, RoundTripConversionGainToDb) +{ + // Convert gain 2.0 to dB and back + float gain1 = 2.0f; + float db = Decibels::gainToDecibels (gain1); + float gain2 = Decibels::decibelsToGain (db); + EXPECT_NEAR (gain2, gain1, 0.001f); +} + +//============================================================================== +TEST (DecibelsTests, GainWithLowerBoundBasic) +{ + // Gain above lower bound should remain unchanged + float gain = Decibels::gainWithLowerBound (0.5f, -20.0f); + EXPECT_FLOAT_EQ (gain, 0.5f); +} + +TEST (DecibelsTests, GainWithLowerBoundBelowThreshold) +{ + // Gain below lower bound should be clamped (line 91) + float gain = Decibels::gainWithLowerBound (0.001f, -20.0f); + float expectedMin = Decibels::decibelsToGain (-20.0f, -21.0f); + EXPECT_FLOAT_EQ (gain, expectedMin); +} + +TEST (DecibelsTests, GainWithLowerBoundZeroGain) +{ + // Zero gain should be clamped to lower bound + float gain = Decibels::gainWithLowerBound (0.0f, -20.0f); + float expectedMin = Decibels::decibelsToGain (-20.0f, -21.0f); + EXPECT_FLOAT_EQ (gain, expectedMin); +} + +TEST (DecibelsTests, GainWithLowerBoundNegativeBound) +{ + // Tests line 89 assertion (negative decibel value) + float gain = Decibels::gainWithLowerBound (0.5f, -30.0f); + EXPECT_GE (gain, Decibels::decibelsToGain (-30.0f, -31.0f)); +} + +TEST (DecibelsTests, GainWithLowerBoundHighGain) +{ + // High gain should remain unchanged + float gain = Decibels::gainWithLowerBound (2.0f, -20.0f); + EXPECT_FLOAT_EQ (gain, 2.0f); +} + +TEST (DecibelsTests, GainWithLowerBoundExactlyAtBound) +{ + // Gain exactly at lower bound + float lowerBoundDb = -20.0f; + float boundGain = Decibels::decibelsToGain (lowerBoundDb); + float gain = Decibels::gainWithLowerBound (boundGain, lowerBoundDb); + EXPECT_FLOAT_EQ (gain, boundGain); +} + +TEST (DecibelsTests, GainWithLowerBoundDoubleType) +{ + // Test with double precision + double gain = Decibels::gainWithLowerBound (0.5, -20.0); + EXPECT_DOUBLE_EQ (gain, 0.5); +} + +TEST (DecibelsTests, GainWithLowerBoundVeryLowBound) +{ + // Test with very low bound + float gain = Decibels::gainWithLowerBound (0.00001f, -80.0f); + float expectedMin = Decibels::decibelsToGain (-80.0f, -81.0f); + EXPECT_FLOAT_EQ (gain, expectedMin); +} + +//============================================================================== +TEST (DecibelsTests, ToStringZeroDB) +{ + // 0 dB should show "+0.00 dB" (line 121-122) + String s = Decibels::toString (0.0f, 2); + EXPECT_TRUE (s.startsWith ("+0")); + EXPECT_TRUE (s.endsWith (" dB")); +} + +TEST (DecibelsTests, ToStringPositive) +{ + // Positive dB should have '+' prefix (line 121-122) + String s = Decibels::toString (6.0f, 2); + EXPECT_TRUE (s.startsWith ("+6")); + EXPECT_TRUE (s.contains ("dB")); +} + +TEST (DecibelsTests, ToStringNegative) +{ + // Negative dB should have '-' (no '+' on line 122) + String s = Decibels::toString (-6.0f, 2); + EXPECT_TRUE (s.startsWith ("-6")); + EXPECT_TRUE (s.contains ("dB")); +} + +TEST (DecibelsTests, ToStringMinusInfinity) +{ + // Below minusInfinityDb should return "-INF" (lines 112-115) + String s = Decibels::toString (-120.0f, 2, -100.0f); + EXPECT_TRUE (s.contains ("-INF")); +} + +TEST (DecibelsTests, ToStringAtMinusInfinity) +{ + // Exactly at minusInfinityDb should return "-INF" (line 112) + String s = Decibels::toString (-100.0f, 2, -100.0f); + EXPECT_TRUE (s.contains ("-INF")); +} + +TEST (DecibelsTests, ToStringCustomMinusInfinityString) +{ + // Custom minus infinity string (lines 114-117) + String s = Decibels::toString (-120.0f, 2, -100.0f, true, "-\u221E"); + EXPECT_TRUE (s.contains ("-\u221E")); +} + +TEST (DecibelsTests, ToStringWithoutSuffix) +{ + // shouldIncludeSuffix = false (line 130-131) + String s = Decibels::toString (6.0f, 2, -100.0f, false); + EXPECT_FALSE (s.contains ("dB")); +} + +TEST (DecibelsTests, ToStringWithSuffix) +{ + // shouldIncludeSuffix = true (line 130-131) + String s = Decibels::toString (6.0f, 2, -100.0f, true); + EXPECT_TRUE (s.contains ("dB")); +} + +TEST (DecibelsTests, ToStringZeroDecimalPlaces) +{ + // decimalPlaces = 0 should use roundToInt (line 124-125) + String s = Decibels::toString (6.789f, 0, -100.0f, false); + EXPECT_TRUE (s == "+7" || s == "+6"); // Depending on rounding +} + +TEST (DecibelsTests, ToStringOneDecimalPlace) +{ + // decimalPlaces = 1 (line 127) + String s = Decibels::toString (6.789f, 1, -100.0f, false); + EXPECT_TRUE (s.startsWith ("+6.")); +} + +TEST (DecibelsTests, ToStringTwoDecimalPlaces) +{ + // decimalPlaces = 2 (line 127) + String s = Decibels::toString (6.789f, 2, -100.0f, false); + EXPECT_TRUE (s.startsWith ("+6.")); +} + +TEST (DecibelsTests, ToStringThreeDecimalPlaces) +{ + // decimalPlaces = 3 (line 127) + String s = Decibels::toString (6.789f, 3, -100.0f, false); + EXPECT_TRUE (s.startsWith ("+6.")); +} + +TEST (DecibelsTests, ToStringNegativeDecimalPlaces) +{ + // Negative decimalPlaces should use roundToInt (line 124-125) + String s = Decibels::toString (6.789f, -1, -100.0f, false); + EXPECT_TRUE (s == "+7" || s == "+6"); +} + +TEST (DecibelsTests, ToStringDoubleType) +{ + // Test with double precision + String s = Decibels::toString (6.0, 2); + EXPECT_TRUE (s.startsWith ("+6")); +} + +TEST (DecibelsTests, ToStringPreallocatesBytes) +{ + // Tests line 110 (preallocateBytes) + String s = Decibels::toString (123.456f, 3); + EXPECT_FALSE (s.isEmpty()); +} + +TEST (DecibelsTests, ToStringEmptyCustomMinusInfinityString) +{ + // Empty customMinusInfinityString should use "-INF" (line 114-115) + String s = Decibels::toString (-120.0f, 2, -100.0f, true, ""); + EXPECT_TRUE (s.contains ("-INF")); +} + +TEST (DecibelsTests, ToStringNonEmptyCustomMinusInfinityString) +{ + // Non-empty customMinusInfinityString (line 116-117) + String s = Decibels::toString (-120.0f, 2, -100.0f, true, "Silent"); + EXPECT_TRUE (s.contains ("Silent")); +} + +TEST (DecibelsTests, ToStringVeryLargePositive) +{ + // Very large positive dB + String s = Decibels::toString (100.0f, 2); + EXPECT_TRUE (s.startsWith ("+100")); +} + +TEST (DecibelsTests, ToStringVeryLargeNegative) +{ + // Very large negative dB (but above minusInfinityDb) + String s = Decibels::toString (-90.0f, 2, -100.0f); + EXPECT_TRUE (s.startsWith ("-90")); +} + +TEST (DecibelsTests, ToStringJustAboveMinusInfinity) +{ + // Just above minusInfinityDb should show number, not "-INF" + String s = Decibels::toString (-99.9f, 1, -100.0f); + EXPECT_TRUE (s.startsWith ("-99")); + EXPECT_FALSE (s.contains ("-INF")); +} + +//============================================================================== +TEST (DecibelsTests, DefaultMinusInfinityValue) +{ + // Test default minusInfinityDb = -100 (line 140) + float gain = Decibels::decibelsToGain (-120.0f); + EXPECT_FLOAT_EQ (gain, 0.0f); +} + +TEST (DecibelsTests, MathematicalAccuracy) +{ + // Verify the mathematical formulas + // decibelsToGain: gain = 10^(dB * 0.05) = 10^(dB/20) + float gain = Decibels::decibelsToGain (20.0f); + EXPECT_NEAR (gain, 10.0f, 0.001f); + + // gainToDecibels: dB = log10(gain) * 20 + float db = Decibels::gainToDecibels (10.0f); + EXPECT_NEAR (db, 20.0f, 0.001f); +} + +TEST (DecibelsTests, EdgeCaseVerySmallPositiveGain) +{ + // Very small positive gain + float gain = 0.0001f; + float db = Decibels::gainToDecibels (gain); + float gainBack = Decibels::decibelsToGain (db); + EXPECT_NEAR (gainBack, gain, 0.00001f); +} + +TEST (DecibelsTests, EdgeCaseVeryLargeGain) +{ + // Very large gain + float gain = 1000.0f; + float db = Decibels::gainToDecibels (gain); + float gainBack = Decibels::decibelsToGain (db); + EXPECT_NEAR (gainBack, gain, 1.0f); +} + +//============================================================================== +TEST (DecibelsTests, TypeConsistency) +{ + // Ensure float and double produce consistent results + float dbFloat = Decibels::gainToDecibels (2.0f); + double dbDouble = Decibels::gainToDecibels (2.0); + + EXPECT_NEAR (static_cast (dbFloat), dbDouble, 0.001); +} + +TEST (DecibelsTests, SymmetricOperations) +{ + // Test that operations are symmetric + std::vector testValues = { 0.001f, 0.1f, 0.5f, 1.0f, 2.0f, 10.0f }; + + for (float gain : testValues) + { + float db = Decibels::gainToDecibels (gain); + float gainBack = Decibels::decibelsToGain (db); + EXPECT_NEAR (gainBack, gain, 0.01f * gain); + } +} diff --git a/tests/yup_audio_basics/yup_FloatVectorOperations.cpp b/tests/yup_audio_basics/yup_FloatVectorOperations.cpp index 85a551710..2d097764b 100644 --- a/tests/yup_audio_basics/yup_FloatVectorOperations.cpp +++ b/tests/yup_audio_basics/yup_FloatVectorOperations.cpp @@ -318,3 +318,227 @@ TEST_F (FloatVectorOperationsTests, FloatToDoubleAndBack) EXPECT_NEAR ((float) floatData[i], (float) doubleData[i], std::numeric_limits::epsilon()); } } + +TEST_F (FloatVectorOperationsTests, FindMinAndMax) +{ + float data[10] = { 0.1f, -0.5f, 0.8f, -0.2f, 0.4f, 0.9f, -0.7f, 0.3f, -0.1f, 0.6f }; + + auto range = FloatVectorOperations::findMinAndMax (data, 10); + + EXPECT_FLOAT_EQ (range.getStart(), -0.7f); + EXPECT_FLOAT_EQ (range.getEnd(), 0.9f); +} + +TEST_F (FloatVectorOperationsTests, FindMinimum) +{ + float data[10] = { 0.1f, -0.5f, 0.8f, -0.2f, 0.4f, 0.9f, -0.7f, 0.3f, -0.1f, 0.6f }; + + auto minVal = FloatVectorOperations::findMinimum (data, 10); + + EXPECT_FLOAT_EQ (minVal, -0.7f); +} + +TEST_F (FloatVectorOperationsTests, FindMaximum) +{ + float data[10] = { 0.1f, -0.5f, 0.8f, -0.2f, 0.4f, 0.9f, -0.7f, 0.3f, -0.1f, 0.6f }; + + auto maxVal = FloatVectorOperations::findMaximum (data, 10); + + EXPECT_FLOAT_EQ (maxVal, 0.9f); +} + +TEST_F (FloatVectorOperationsTests, Negate) +{ + float src[5] = { 1.0f, -2.0f, 3.0f, -4.0f, 5.0f }; + float dest[5]; + + FloatVectorOperations::negate (dest, src, 5); + + EXPECT_FLOAT_EQ (dest[0], -1.0f); + EXPECT_FLOAT_EQ (dest[1], 2.0f); + EXPECT_FLOAT_EQ (dest[2], -3.0f); + EXPECT_FLOAT_EQ (dest[3], 4.0f); + EXPECT_FLOAT_EQ (dest[4], -5.0f); +} + +TEST_F (FloatVectorOperationsTests, Abs) +{ + float src[5] = { 1.0f, -2.0f, 3.0f, -4.0f, 5.0f }; + float dest[5]; + + FloatVectorOperations::abs (dest, src, 5); + + EXPECT_FLOAT_EQ (dest[0], 1.0f); + EXPECT_FLOAT_EQ (dest[1], 2.0f); + EXPECT_FLOAT_EQ (dest[2], 3.0f); + EXPECT_FLOAT_EQ (dest[3], 4.0f); + EXPECT_FLOAT_EQ (dest[4], 5.0f); +} + +TEST_F (FloatVectorOperationsTests, MinWithScalar) +{ + float src[5] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + float dest[5]; + + FloatVectorOperations::min (dest, src, 3.0f, 5); + + EXPECT_FLOAT_EQ (dest[0], 1.0f); + EXPECT_FLOAT_EQ (dest[1], 2.0f); + EXPECT_FLOAT_EQ (dest[2], 3.0f); + EXPECT_FLOAT_EQ (dest[3], 3.0f); + EXPECT_FLOAT_EQ (dest[4], 3.0f); +} + +TEST_F (FloatVectorOperationsTests, MinWithArray) +{ + float src1[5] = { 1.0f, 5.0f, 2.0f, 4.0f, 3.0f }; + float src2[5] = { 3.0f, 2.0f, 4.0f, 1.0f, 5.0f }; + float dest[5]; + + FloatVectorOperations::min (dest, src1, src2, 5); + + EXPECT_FLOAT_EQ (dest[0], 1.0f); + EXPECT_FLOAT_EQ (dest[1], 2.0f); + EXPECT_FLOAT_EQ (dest[2], 2.0f); + EXPECT_FLOAT_EQ (dest[3], 1.0f); + EXPECT_FLOAT_EQ (dest[4], 3.0f); +} + +TEST_F (FloatVectorOperationsTests, MaxWithScalar) +{ + float src[5] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + float dest[5]; + + FloatVectorOperations::max (dest, src, 3.0f, 5); + + EXPECT_FLOAT_EQ (dest[0], 3.0f); + EXPECT_FLOAT_EQ (dest[1], 3.0f); + EXPECT_FLOAT_EQ (dest[2], 3.0f); + EXPECT_FLOAT_EQ (dest[3], 4.0f); + EXPECT_FLOAT_EQ (dest[4], 5.0f); +} + +TEST_F (FloatVectorOperationsTests, MaxWithArray) +{ + float src1[5] = { 1.0f, 5.0f, 2.0f, 4.0f, 3.0f }; + float src2[5] = { 3.0f, 2.0f, 4.0f, 1.0f, 5.0f }; + float dest[5]; + + FloatVectorOperations::max (dest, src1, src2, 5); + + EXPECT_FLOAT_EQ (dest[0], 3.0f); + EXPECT_FLOAT_EQ (dest[1], 5.0f); + EXPECT_FLOAT_EQ (dest[2], 4.0f); + EXPECT_FLOAT_EQ (dest[3], 4.0f); + EXPECT_FLOAT_EQ (dest[4], 5.0f); +} + +TEST_F (FloatVectorOperationsTests, Clip) +{ + float src[7] = { -2.0f, -0.5f, 0.0f, 0.5f, 1.0f, 1.5f, 2.0f }; + float dest[7]; + + FloatVectorOperations::clip (dest, src, 0.0f, 1.0f, 7); + + EXPECT_FLOAT_EQ (dest[0], 0.0f); + EXPECT_FLOAT_EQ (dest[1], 0.0f); + EXPECT_FLOAT_EQ (dest[2], 0.0f); + EXPECT_FLOAT_EQ (dest[3], 0.5f); + EXPECT_FLOAT_EQ (dest[4], 1.0f); + EXPECT_FLOAT_EQ (dest[5], 1.0f); + EXPECT_FLOAT_EQ (dest[6], 1.0f); +} + +TEST_F (FloatVectorOperationsTests, CopyWithDividend) +{ + float src[5] = { 2.0f, 4.0f, 5.0f, 10.0f, 20.0f }; + float dest[5]; + + FloatVectorOperations::copyWithDividend (dest, src, 20.0f, 5); + + EXPECT_FLOAT_EQ (dest[0], 10.0f); + EXPECT_FLOAT_EQ (dest[1], 5.0f); + EXPECT_FLOAT_EQ (dest[2], 4.0f); + EXPECT_FLOAT_EQ (dest[3], 2.0f); + EXPECT_FLOAT_EQ (dest[4], 1.0f); +} + +TEST_F (FloatVectorOperationsTests, CopyWithDivide) +{ + float src[5] = { 20.0f, 10.0f, 8.0f, 4.0f, 2.0f }; + float dest[5]; + + FloatVectorOperations::copyWithDivide (dest, src, 2.0f, 5); + + EXPECT_FLOAT_EQ (dest[0], 10.0f); + EXPECT_FLOAT_EQ (dest[1], 5.0f); + EXPECT_FLOAT_EQ (dest[2], 4.0f); + EXPECT_FLOAT_EQ (dest[3], 2.0f); + EXPECT_FLOAT_EQ (dest[4], 1.0f); +} + +TEST_F (FloatVectorOperationsTests, DivideScalarByArray) +{ + float src[5] = { 2.0f, 4.0f, 5.0f, 10.0f, 20.0f }; + float dest[5]; + + FloatVectorOperations::divide (dest, 20.0f, 5); + + // Initial dest values get divided + for (int i = 0; i < 5; ++i) + dest[i] = src[i]; + + FloatVectorOperations::divide (dest, 2.0f, 5); + + EXPECT_FLOAT_EQ (dest[0], 1.0f); + EXPECT_FLOAT_EQ (dest[1], 2.0f); + EXPECT_FLOAT_EQ (dest[2], 2.5f); + EXPECT_FLOAT_EQ (dest[3], 5.0f); + EXPECT_FLOAT_EQ (dest[4], 10.0f); +} + +TEST_F (FloatVectorOperationsTests, EnableFlushToZeroMode) +{ + // Just test that it doesn't crash + EXPECT_NO_THROW (FloatVectorOperations::enableFlushToZeroMode (true)); + EXPECT_NO_THROW (FloatVectorOperations::enableFlushToZeroMode (false)); +} + +TEST_F (FloatVectorOperationsTests, LargeBufferOperations) +{ + const int size = 10000; + HeapBlock src (size); + HeapBlock dest (size); + + Random& random = Random::getSystemRandom(); + for (int i = 0; i < size; ++i) + src[i] = random.nextFloat() * 2.0f - 1.0f; + + // Test that large buffer operations don't crash + EXPECT_NO_THROW (FloatVectorOperations::copy (dest, src, size)); + EXPECT_NO_THROW (FloatVectorOperations::multiply (dest, 2.0f, size)); + EXPECT_NO_THROW (FloatVectorOperations::add (dest, 1.0f, size)); + EXPECT_NO_THROW (FloatVectorOperations::clear (dest, size)); +} + +TEST_F (FloatVectorOperationsTests, DoubleOperations) +{ + double src[5] = { 1.0, 2.0, 3.0, 4.0, 5.0 }; + double dest[5]; + + FloatVectorOperations::clear (dest, 5); + for (int i = 0; i < 5; ++i) + EXPECT_DOUBLE_EQ (dest[i], 0.0); + + FloatVectorOperations::copy (dest, src, 5); + for (int i = 0; i < 5; ++i) + EXPECT_DOUBLE_EQ (dest[i], src[i]); + + FloatVectorOperations::multiply (dest, 2.0, 5); + for (int i = 0; i < 5; ++i) + EXPECT_DOUBLE_EQ (dest[i], src[i] * 2.0); + + FloatVectorOperations::add (dest, 1.0, 5); + for (int i = 0; i < 5; ++i) + EXPECT_DOUBLE_EQ (dest[i], src[i] * 2.0 + 1.0); +} diff --git a/tests/yup_audio_basics/yup_IIRFilter.cpp b/tests/yup_audio_basics/yup_IIRFilter.cpp new file mode 100644 index 000000000..6d76e1488 --- /dev/null +++ b/tests/yup_audio_basics/yup_IIRFilter.cpp @@ -0,0 +1,501 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +class IIRCoefficientsTests : public ::testing::Test +{ +}; + +//============================================================================== +TEST_F (IIRCoefficientsTests, DefaultConstructor) +{ + IIRCoefficients coeff; + + // Should be zeroed (line 47) + for (int i = 0; i < 5; ++i) + EXPECT_FLOAT_EQ (coeff.coefficients[i], 0.0f); +} + +TEST_F (IIRCoefficientsTests, Destructor) +{ + auto* coeff = new IIRCoefficients(); + EXPECT_NO_THROW (delete coeff); +} + +TEST_F (IIRCoefficientsTests, CopyConstructor) +{ + IIRCoefficients coeff1 (1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + IIRCoefficients coeff2 (coeff1); + + for (int i = 0; i < 5; ++i) + EXPECT_FLOAT_EQ (coeff2.coefficients[i], coeff1.coefficients[i]); +} + +TEST_F (IIRCoefficientsTests, CopyAssignment) +{ + IIRCoefficients coeff1 (1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + IIRCoefficients coeff2; + + coeff2 = coeff1; + + for (int i = 0; i < 5; ++i) + EXPECT_FLOAT_EQ (coeff2.coefficients[i], coeff1.coefficients[i]); +} + +TEST_F (IIRCoefficientsTests, ParameterizedConstructor) +{ + // Test normalization (line 65-71) + IIRCoefficients coeff (1.0, 2.0, 3.0, 2.0, 5.0, 6.0); + + // Coefficients should be normalized by c4 (2.0) + EXPECT_FLOAT_EQ (coeff.coefficients[0], 0.5f); // 1.0 / 2.0 + EXPECT_FLOAT_EQ (coeff.coefficients[1], 1.0f); // 2.0 / 2.0 + EXPECT_FLOAT_EQ (coeff.coefficients[2], 1.5f); // 3.0 / 2.0 + EXPECT_FLOAT_EQ (coeff.coefficients[3], 2.5f); // 5.0 / 2.0 + EXPECT_FLOAT_EQ (coeff.coefficients[4], 3.0f); // 6.0 / 2.0 +} + +//============================================================================== +TEST_F (IIRCoefficientsTests, MakeLowPassDefaultQ) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + + // Should use default Q = 1/sqrt(2) (line 77) + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +TEST_F (IIRCoefficientsTests, MakeLowPassWithQ) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0, 0.707); + + // Coefficients should be non-zero + EXPECT_NE (coeff.coefficients[0], 0.0f); + EXPECT_NE (coeff.coefficients[1], 0.0f); +} + +TEST_F (IIRCoefficientsTests, MakeLowPassDifferentFrequencies) +{ + auto coeff1 = IIRCoefficients::makeLowPass (44100.0, 500.0, 1.0); + auto coeff2 = IIRCoefficients::makeLowPass (44100.0, 2000.0, 1.0); + + // Different frequencies should produce different coefficients + EXPECT_NE (coeff1.coefficients[0], coeff2.coefficients[0]); +} + +//============================================================================== +TEST_F (IIRCoefficientsTests, MakeHighPassDefaultQ) +{ + auto coeff = IIRCoefficients::makeHighPass (44100.0, 1000.0); + + // Should use default Q (line 103) + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +TEST_F (IIRCoefficientsTests, MakeHighPassWithQ) +{ + auto coeff = IIRCoefficients::makeHighPass (44100.0, 1000.0, 0.707); + + // Coefficients should be non-zero + EXPECT_NE (coeff.coefficients[0], 0.0f); + EXPECT_NE (coeff.coefficients[1], 0.0f); +} + +//============================================================================== +TEST_F (IIRCoefficientsTests, MakeBandPassDefaultQ) +{ + auto coeff = IIRCoefficients::makeBandPass (44100.0, 1000.0); + + // Should use default Q (line 129) + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +TEST_F (IIRCoefficientsTests, MakeBandPassWithQ) +{ + auto coeff = IIRCoefficients::makeBandPass (44100.0, 1000.0, 1.0); + + // Coefficients should be non-zero + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +//============================================================================== +TEST_F (IIRCoefficientsTests, MakeNotchFilterDefaultQ) +{ + auto coeff = IIRCoefficients::makeNotchFilter (44100.0, 1000.0); + + // Should use default Q (line 155) + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +TEST_F (IIRCoefficientsTests, MakeNotchFilterWithQ) +{ + auto coeff = IIRCoefficients::makeNotchFilter (44100.0, 1000.0, 2.0); + + // Coefficients should be non-zero + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +//============================================================================== +TEST_F (IIRCoefficientsTests, MakeAllPassDefaultQ) +{ + auto coeff = IIRCoefficients::makeAllPass (44100.0, 1000.0); + + // Should use default Q (line 181) + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +TEST_F (IIRCoefficientsTests, MakeAllPassWithQ) +{ + auto coeff = IIRCoefficients::makeAllPass (44100.0, 1000.0, 1.5); + + // Coefficients should be non-zero + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +//============================================================================== +TEST_F (IIRCoefficientsTests, MakeLowShelf) +{ + auto coeff = IIRCoefficients::makeLowShelf (44100.0, 1000.0, 0.707, 6.0f); + + // Test with positive gain (line 213-227) + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +TEST_F (IIRCoefficientsTests, MakeLowShelfNegativeGain) +{ + auto coeff = IIRCoefficients::makeLowShelf (44100.0, 1000.0, 0.707, -6.0f); + + // Test with negative gain + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +//============================================================================== +TEST_F (IIRCoefficientsTests, MakeHighShelf) +{ + auto coeff = IIRCoefficients::makeHighShelf (44100.0, 5000.0, 0.707, 6.0f); + + // Test with positive gain (line 238-252) + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +TEST_F (IIRCoefficientsTests, MakeHighShelfNegativeGain) +{ + auto coeff = IIRCoefficients::makeHighShelf (44100.0, 5000.0, 0.707, -6.0f); + + // Test with negative gain + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +//============================================================================== +TEST_F (IIRCoefficientsTests, MakePeakFilter) +{ + auto coeff = IIRCoefficients::makePeakFilter (44100.0, 1000.0, 1.0, 3.0f); + + // Test peak filter (line 263-276) + EXPECT_NE (coeff.coefficients[0], 0.0f); + EXPECT_NE (coeff.coefficients[1], 0.0f); +} + +TEST_F (IIRCoefficientsTests, MakePeakFilterNegativeGain) +{ + auto coeff = IIRCoefficients::makePeakFilter (44100.0, 1000.0, 1.0, -6.0f); + + // Test with negative gain + EXPECT_NE (coeff.coefficients[0], 0.0f); +} + +//============================================================================== +class IIRFilterTests : public ::testing::Test +{ +protected: + void SetUp() override + { + filter = std::make_unique(); + } + + void TearDown() override + { + filter.reset(); + } + + std::unique_ptr filter; +}; + +//============================================================================== +TEST_F (IIRFilterTests, DefaultConstructor) +{ + EXPECT_NO_THROW (IIRFilter()); +} + +TEST_F (IIRFilterTests, CopyConstructor) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (coeff); + + IIRFilter filter2 (*filter); + + // Should copy active state and coefficients (line 283-288) + EXPECT_NO_THROW (filter2.reset()); +} + +//============================================================================== +TEST_F (IIRFilterTests, MakeInactive) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (coeff); + + filter->makeInactive(); + + // Filter should not process after makeInactive (line 292-296) + float samples[10] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + filter->processSamples (samples, 10); + + // Samples should remain unchanged when inactive + for (int i = 0; i < 10; ++i) + EXPECT_FLOAT_EQ (samples[i], 1.0f); +} + +TEST_F (IIRFilterTests, SetCoefficients) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + + EXPECT_NO_THROW (filter->setCoefficients (coeff)); + + // Should set active to true (line 303) +} + +//============================================================================== +TEST_F (IIRFilterTests, Reset) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (coeff); + + // Process some samples to set internal state + float samples[10] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + filter->processSamples (samples, 10); + + // Reset should clear internal state (line 308-312) + filter->reset(); + + // Process again - should produce consistent results + float samples2[10] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + filter->processSamples (samples2, 10); +} + +//============================================================================== +TEST_F (IIRFilterTests, ProcessSingleSampleRaw) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (coeff); + + // Test single sample processing (line 315-325) + float output = filter->processSingleSampleRaw (1.0f); + + EXPECT_NE (output, 1.0f); // Should be filtered +} + +TEST_F (IIRFilterTests, ProcessSingleSampleRawMultiple) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (coeff); + + // Process multiple samples + for (int i = 0; i < 100; ++i) + { + float output = filter->processSingleSampleRaw (std::sin (i * 0.1f)); + (void) output; + } + + // Should not crash and produce valid output +} + +//============================================================================== +TEST_F (IIRFilterTests, ProcessSamplesInactive) +{ + // Filter is inactive by default + float samples[10] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f }; + + filter->processSamples (samples, 10); + + // Samples should be unchanged when filter is inactive (line 332) + for (int i = 0; i < 10; ++i) + EXPECT_FLOAT_EQ (samples[i], static_cast (i + 1)); +} + +TEST_F (IIRFilterTests, ProcessSamplesActive) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (coeff); + + float samples[10] = { 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f }; + + filter->processSamples (samples, 10); + + // Samples should be filtered (line 332-356) + bool hasChanged = false; + for (int i = 0; i < 10; ++i) + { + if ((i % 2 == 0 && samples[i] != 1.0f) || (i % 2 == 1 && samples[i] != 0.0f)) + { + hasChanged = true; + break; + } + } + EXPECT_TRUE (hasChanged); +} + +TEST_F (IIRFilterTests, ProcessSamplesLowPass) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (coeff); + + // Generate a high-frequency signal + float samples[100]; + for (int i = 0; i < 100; ++i) + samples[i] = std::sin (i * 0.5f); // High frequency + + filter->processSamples (samples, 100); + + // High frequencies should be attenuated + float maxAmplitude = 0.0f; + for (int i = 50; i < 100; ++i) + maxAmplitude = std::max (maxAmplitude, std::abs (samples[i])); + + EXPECT_LT (maxAmplitude, 1.0f); +} + +TEST_F (IIRFilterTests, ProcessSamplesHighPass) +{ + auto coeff = IIRCoefficients::makeHighPass (44100.0, 5000.0); + filter->setCoefficients (coeff); + + // Generate a low-frequency signal + float samples[100]; + for (int i = 0; i < 100; ++i) + samples[i] = std::sin (i * 0.01f); // Low frequency + + filter->processSamples (samples, 100); + + // Low frequencies should be attenuated + float maxAmplitude = 0.0f; + for (int i = 50; i < 100; ++i) + maxAmplitude = std::max (maxAmplitude, std::abs (samples[i])); + + EXPECT_LT (maxAmplitude, 1.0f); +} + +TEST_F (IIRFilterTests, ProcessSamplesBandPass) +{ + auto coeff = IIRCoefficients::makeBandPass (44100.0, 1000.0, 2.0); + filter->setCoefficients (coeff); + + float samples[100]; + for (int i = 0; i < 100; ++i) + samples[i] = std::sin (i * 0.2f); + + EXPECT_NO_THROW (filter->processSamples (samples, 100)); +} + +//============================================================================== +TEST_F (IIRFilterTests, DifferentSampleRates) +{ + // Test filters at different sample rates + for (double sampleRate : { 22050.0, 44100.0, 48000.0, 96000.0 }) + { + auto coeff = IIRCoefficients::makeLowPass (sampleRate, sampleRate * 0.1); + filter->setCoefficients (coeff); + + float samples[50]; + for (int i = 0; i < 50; ++i) + samples[i] = std::sin (i * 0.1f); + + EXPECT_NO_THROW (filter->processSamples (samples, 50)); + } +} + +TEST_F (IIRFilterTests, DifferentFilterTypes) +{ + float samples[50]; + for (int i = 0; i < 50; ++i) + samples[i] = std::sin (i * 0.1f); + + // Test all filter types + auto lowPass = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (lowPass); + EXPECT_NO_THROW (filter->processSamples (samples, 50)); + + auto highPass = IIRCoefficients::makeHighPass (44100.0, 1000.0); + filter->setCoefficients (highPass); + EXPECT_NO_THROW (filter->processSamples (samples, 50)); + + auto bandPass = IIRCoefficients::makeBandPass (44100.0, 1000.0); + filter->setCoefficients (bandPass); + EXPECT_NO_THROW (filter->processSamples (samples, 50)); + + auto notch = IIRCoefficients::makeNotchFilter (44100.0, 1000.0); + filter->setCoefficients (notch); + EXPECT_NO_THROW (filter->processSamples (samples, 50)); + + auto allPass = IIRCoefficients::makeAllPass (44100.0, 1000.0); + filter->setCoefficients (allPass); + EXPECT_NO_THROW (filter->processSamples (samples, 50)); +} + +TEST_F (IIRFilterTests, LargeBufferProcessing) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (coeff); + + const int size = 10000; + std::vector samples (size); + for (int i = 0; i < size; ++i) + samples[i] = std::sin (i * 0.05f); + + EXPECT_NO_THROW (filter->processSamples (samples.data(), size)); +} + +TEST_F (IIRFilterTests, StatePreservation) +{ + auto coeff = IIRCoefficients::makeLowPass (44100.0, 1000.0); + filter->setCoefficients (coeff); + + // Process first block + float samples1[10]; + for (int i = 0; i < 10; ++i) + samples1[i] = 1.0f; + + filter->processSamples (samples1, 10); + + // Process second block - state should be preserved + float samples2[10]; + for (int i = 0; i < 10; ++i) + samples2[i] = 1.0f; + + filter->processSamples (samples2, 10); + + // Second block should produce different results due to state + EXPECT_NE (samples1[0], samples2[0]); +} diff --git a/tests/yup_audio_basics/yup_MidiDataConcatenator.cpp b/tests/yup_audio_basics/yup_MidiDataConcatenator.cpp new file mode 100644 index 000000000..ba02e93db --- /dev/null +++ b/tests/yup_audio_basics/yup_MidiDataConcatenator.cpp @@ -0,0 +1,634 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +class TestMidiCallback +{ +public: + struct ReceivedMessage + { + MidiMessage message; + void* userData; + + bool operator== (const ReceivedMessage& other) const + { + return message.getDescription() == other.message.getDescription() + && userData == other.userData; + } + }; + + struct ReceivedPartialSysex + { + std::vector data; + double time; + void* userData; + }; + + void handleIncomingMidiMessage (void* source, const MidiMessage& message) + { + receivedMessages.push_back ({ message, source }); + } + + void handlePartialSysexMessage (void* source, const uint8* messageData, int numBytesSoFar, double timestamp) + { + std::vector data (messageData, messageData + numBytesSoFar); + receivedPartialSysex.push_back ({ data, timestamp, source }); + } + + std::vector receivedMessages; + std::vector receivedPartialSysex; +}; + +//============================================================================== +class MidiDataConcatenatorTests : public ::testing::Test +{ +protected: + void SetUp() override + { + concatenator = std::make_unique (256); + callback = std::make_unique(); + } + + void TearDown() override + { + concatenator.reset(); + callback.reset(); + } + + void pushData (const std::vector& data, double time = 0.0, void* userData = nullptr) + { + concatenator->pushMidiData (data.data(), (int) data.size(), time, userData, *callback); + } + + std::unique_ptr concatenator; + std::unique_ptr callback; +}; + +//============================================================================== +// Constructor tests +TEST_F (MidiDataConcatenatorTests, Constructor) +{ + EXPECT_NO_THROW (MidiDataConcatenator (256)); + EXPECT_NO_THROW (MidiDataConcatenator (0)); + EXPECT_NO_THROW (MidiDataConcatenator (1024)); +} + +//============================================================================== +// Reset tests +TEST_F (MidiDataConcatenatorTests, ResetClearsState) +{ + // Send partial message + pushData ({ 0x90, 0x3c }, 1.0); + EXPECT_EQ (callback->receivedMessages.size(), 0); + + concatenator->reset(); + + // After reset, previous partial message should be forgotten + pushData ({ 0x64 }, 2.0); + EXPECT_EQ (callback->receivedMessages.size(), 0); +} + +TEST_F (MidiDataConcatenatorTests, ResetClearsPendingSysex) +{ + // Start sysex but don't complete it + pushData ({ 0xf0, 0x43, 0x12 }, 1.0); + + concatenator->reset(); + + // After reset, pending sysex should be cleared + // Send a complete note-on message + pushData ({ 0x90, 0x3c, 0x64 }, 2.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); +} + +//============================================================================== +// Simple message tests +TEST_F (MidiDataConcatenatorTests, NoteOnMessage) +{ + pushData ({ 0x90, 0x3c, 0x64 }, 1.5); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_EQ (callback->receivedMessages[0].message.getChannel(), 1); + EXPECT_EQ (callback->receivedMessages[0].message.getNoteNumber(), 60); + EXPECT_EQ (callback->receivedMessages[0].message.getVelocity(), 100); + EXPECT_DOUBLE_EQ (callback->receivedMessages[0].message.getTimeStamp(), 1.5); +} + +TEST_F (MidiDataConcatenatorTests, NoteOffMessage) +{ + pushData ({ 0x80, 0x3c, 0x40 }, 2.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOff()); + EXPECT_EQ (callback->receivedMessages[0].message.getChannel(), 1); + EXPECT_EQ (callback->receivedMessages[0].message.getNoteNumber(), 60); +} + +TEST_F (MidiDataConcatenatorTests, ControllerMessage) +{ + pushData ({ 0xb0, 0x07, 0x7f }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isController()); + EXPECT_EQ (callback->receivedMessages[0].message.getControllerNumber(), 7); + EXPECT_EQ (callback->receivedMessages[0].message.getControllerValue(), 127); +} + +TEST_F (MidiDataConcatenatorTests, ProgramChangeMessage) +{ + pushData ({ 0xc0, 0x2a }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isProgramChange()); + EXPECT_EQ (callback->receivedMessages[0].message.getProgramChangeNumber(), 42); +} + +TEST_F (MidiDataConcatenatorTests, PitchWheelMessage) +{ + pushData ({ 0xe0, 0x00, 0x40 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isPitchWheel()); +} + +TEST_F (MidiDataConcatenatorTests, ChannelPressureMessage) +{ + pushData ({ 0xd0, 0x50 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isChannelPressure()); + EXPECT_EQ (callback->receivedMessages[0].message.getChannelPressureValue(), 80); +} + +TEST_F (MidiDataConcatenatorTests, AftertouchMessage) +{ + pushData ({ 0xa0, 0x3c, 0x64 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isAftertouch()); + EXPECT_EQ (callback->receivedMessages[0].message.getNoteNumber(), 60); +} + +//============================================================================== +// Realtime message tests +TEST_F (MidiDataConcatenatorTests, TimingClockMessage) +{ + pushData ({ 0xf8 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isMidiClock()); +} + +TEST_F (MidiDataConcatenatorTests, StartMessage) +{ + pushData ({ 0xfa }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isMidiStart()); +} + +TEST_F (MidiDataConcatenatorTests, ContinueMessage) +{ + pushData ({ 0xfb }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isMidiContinue()); +} + +TEST_F (MidiDataConcatenatorTests, StopMessage) +{ + pushData ({ 0xfc }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isMidiStop()); +} + +TEST_F (MidiDataConcatenatorTests, ActiveSensingMessage) +{ + pushData ({ 0xfe }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isActiveSense()); +} + +TEST_F (MidiDataConcatenatorTests, RealtimeMessageEmbeddedInNormalMessage) +{ + // Clock embedded between status and data bytes + pushData ({ 0x90, 0xf8, 0x3c, 0x64 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 2); + EXPECT_TRUE (callback->receivedMessages[0].message.isMidiClock()); + EXPECT_TRUE (callback->receivedMessages[1].message.isNoteOn()); + EXPECT_EQ (callback->receivedMessages[1].message.getNoteNumber(), 60); +} + +//============================================================================== +// Running status tests +TEST_F (MidiDataConcatenatorTests, RunningStatusSameChannel) +{ + // Send complete message then use running status + pushData ({ 0x90, 0x3c, 0x64 }, 1.0); + pushData ({ 0x40, 0x50 }, 1.5); + + EXPECT_EQ (callback->receivedMessages.size(), 2); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_EQ (callback->receivedMessages[0].message.getNoteNumber(), 60); + EXPECT_TRUE (callback->receivedMessages[1].message.isNoteOn()); + EXPECT_EQ (callback->receivedMessages[1].message.getNoteNumber(), 64); +} + +TEST_F (MidiDataConcatenatorTests, RunningStatusInterruptedByNewStatus) +{ + pushData ({ 0x90, 0x3c, 0x64 }, 1.0); + pushData ({ 0xb0, 0x07, 0x7f }, 1.5); + pushData ({ 0x10, 0x50 }, 2.0); // Should use controller running status + + EXPECT_EQ (callback->receivedMessages.size(), 3); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_TRUE (callback->receivedMessages[1].message.isController()); + EXPECT_TRUE (callback->receivedMessages[2].message.isController()); + EXPECT_EQ (callback->receivedMessages[2].message.getControllerNumber(), 16); +} + +//============================================================================== +// Fragmented message tests +TEST_F (MidiDataConcatenatorTests, MessageSplitAcrossMultipleCalls) +{ + pushData ({ 0x90 }, 1.0); + pushData ({ 0x3c }, 1.0); + pushData ({ 0x64 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_EQ (callback->receivedMessages[0].message.getNoteNumber(), 60); +} + +TEST_F (MidiDataConcatenatorTests, TwoByteMessageSplitAcrossMultipleCalls) +{ + pushData ({ 0xc0 }, 1.0); + pushData ({ 0x2a }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isProgramChange()); + EXPECT_EQ (callback->receivedMessages[0].message.getProgramChangeNumber(), 42); +} + +TEST_F (MidiDataConcatenatorTests, MultipleMessagesInOneCall) +{ + pushData ({ 0x90, 0x3c, 0x64, 0x80, 0x3c, 0x40, 0xb0, 0x07, 0x7f }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 3); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_TRUE (callback->receivedMessages[1].message.isNoteOff()); + EXPECT_TRUE (callback->receivedMessages[2].message.isController()); +} + +//============================================================================== +// SysEx message tests +TEST_F (MidiDataConcatenatorTests, CompleteSysExMessage) +{ + pushData ({ 0xf0, 0x43, 0x12, 0x00, 0x01, 0xf7 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isSysEx()); + + auto* data = callback->receivedMessages[0].message.getSysExData(); + EXPECT_EQ (data[0], 0x43); + EXPECT_EQ (data[1], 0x12); + EXPECT_EQ (data[2], 0x00); + EXPECT_EQ (data[3], 0x01); +} + +TEST_F (MidiDataConcatenatorTests, SysExSplitAcrossMultipleCalls) +{ + pushData ({ 0xf0, 0x43 }, 1.0); + pushData ({ 0x12, 0x00 }, 1.0); + pushData ({ 0x01, 0xf7 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isSysEx()); + + auto* data = callback->receivedMessages[0].message.getSysExData(); + EXPECT_EQ (data[0], 0x43); + EXPECT_EQ (data[1], 0x12); +} + +TEST_F (MidiDataConcatenatorTests, PartialSysExWithoutTerminator) +{ + pushData ({ 0xf0, 0x43, 0x12, 0x00 }, 1.5); + + EXPECT_EQ (callback->receivedMessages.size(), 0); + EXPECT_EQ (callback->receivedPartialSysex.size(), 1); + EXPECT_DOUBLE_EQ (callback->receivedPartialSysex[0].time, 1.5); + EXPECT_EQ (callback->receivedPartialSysex[0].data.size(), 4); + EXPECT_EQ (callback->receivedPartialSysex[0].data[0], 0xf0); + EXPECT_EQ (callback->receivedPartialSysex[0].data[1], 0x43); +} + +TEST_F (MidiDataConcatenatorTests, PartialSysExCompletedLater) +{ + pushData ({ 0xf0, 0x43, 0x12 }, 1.0); + EXPECT_EQ (callback->receivedPartialSysex.size(), 1); + + pushData ({ 0x00, 0x01, 0xf7 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isSysEx()); +} + +TEST_F (MidiDataConcatenatorTests, LargeSysExMessage) +{ + std::vector sysexData { 0xf0 }; + for (int i = 0; i < 1000; ++i) + sysexData.push_back (i & 0x7f); + sysexData.push_back (0xf7); + + pushData (sysexData, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isSysEx()); + EXPECT_EQ (callback->receivedMessages[0].message.getSysExDataSize(), 1000); +} + +TEST_F (MidiDataConcatenatorTests, SysExInterruptedByRealtimeMessage) +{ + pushData ({ 0xf0, 0x43, 0xf8, 0x12, 0x00, 0xf7 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 2); + EXPECT_TRUE (callback->receivedMessages[0].message.isMidiClock()); + EXPECT_TRUE (callback->receivedMessages[1].message.isSysEx()); + + // Clock should not be part of sysex data + auto* data = callback->receivedMessages[1].message.getSysExData(); + EXPECT_EQ (data[0], 0x43); + EXPECT_EQ (data[1], 0x12); + EXPECT_EQ (data[2], 0x00); +} + +TEST_F (MidiDataConcatenatorTests, SysExInterruptedByNonRealtimeMessage) +{ + // SysEx interrupted by note-on + pushData ({ 0xf0, 0x43, 0x12, 0x90, 0x3c, 0x64 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_EQ (callback->receivedMessages[0].message.getNoteNumber(), 60); +} + +TEST_F (MidiDataConcatenatorTests, MultipleSysExMessages) +{ + pushData ({ 0xf0, 0x43, 0x12, 0xf7 }, 1.0); + pushData ({ 0xf0, 0x7e, 0x00, 0xf7 }, 2.0); + + EXPECT_EQ (callback->receivedMessages.size(), 2); + EXPECT_TRUE (callback->receivedMessages[0].message.isSysEx()); + EXPECT_TRUE (callback->receivedMessages[1].message.isSysEx()); +} + +//============================================================================== +// Invalid data tests +TEST_F (MidiDataConcatenatorTests, InvalidDataByte) +{ + // Send data byte without status + pushData ({ 0x3c }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 0); +} + +TEST_F (MidiDataConcatenatorTests, MessageTooLong) +{ + // Try to send 4 bytes for a 3-byte message + pushData ({ 0x90, 0x3c, 0x64, 0x70 }, 1.0); + + // Should get one complete message, then treat 0x70 as invalid data + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_EQ (callback->receivedMessages[0].message.getNoteNumber(), 60); +} + +TEST_F (MidiDataConcatenatorTests, StatusByteWithoutData) +{ + pushData ({ 0x90 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 0); +} + +TEST_F (MidiDataConcatenatorTests, IncompleteMessageFollowedByNewStatus) +{ + pushData ({ 0x90, 0x3c }, 1.0); // Incomplete note-on + pushData ({ 0xb0, 0x07, 0x7f }, 1.5); // Complete controller + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isController()); +} + +//============================================================================== +// User data tests +TEST_F (MidiDataConcatenatorTests, UserDataPassedThrough) +{ + int myData = 42; + pushData ({ 0x90, 0x3c, 0x64 }, 1.0, &myData); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_EQ (callback->receivedMessages[0].userData, &myData); +} + +TEST_F (MidiDataConcatenatorTests, DifferentUserDataForDifferentMessages) +{ + int data1 = 1; + int data2 = 2; + + pushData ({ 0x90, 0x3c, 0x64 }, 1.0, &data1); + pushData ({ 0x80, 0x3c, 0x40 }, 1.5, &data2); + + EXPECT_EQ (callback->receivedMessages.size(), 2); + EXPECT_EQ (callback->receivedMessages[0].userData, &data1); + EXPECT_EQ (callback->receivedMessages[1].userData, &data2); +} + +TEST_F (MidiDataConcatenatorTests, UserDataForSysEx) +{ + int myData = 99; + pushData ({ 0xf0, 0x43, 0x12, 0xf7 }, 1.0, &myData); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_EQ (callback->receivedMessages[0].userData, &myData); +} + +//============================================================================== +// Timestamp tests +TEST_F (MidiDataConcatenatorTests, DifferentTimestamps) +{ + pushData ({ 0x90, 0x3c, 0x64 }, 1.0); + pushData ({ 0x80, 0x3c, 0x40 }, 2.5); + + EXPECT_EQ (callback->receivedMessages.size(), 2); + EXPECT_DOUBLE_EQ (callback->receivedMessages[0].message.getTimeStamp(), 1.0); + EXPECT_DOUBLE_EQ (callback->receivedMessages[1].message.getTimeStamp(), 2.5); +} + +TEST_F (MidiDataConcatenatorTests, TimestampForFragmentedMessage) +{ + pushData ({ 0x90 }, 1.0); + pushData ({ 0x3c }, 2.0); + pushData ({ 0x64 }, 3.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + // Should use timestamp from final byte + EXPECT_DOUBLE_EQ (callback->receivedMessages[0].message.getTimeStamp(), 3.0); +} + +TEST_F (MidiDataConcatenatorTests, TimestampForSysExPreserved) +{ + pushData ({ 0xf0, 0x43 }, 1.5); + pushData ({ 0x12, 0xf7 }, 2.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + // Should use timestamp from when sysex started + EXPECT_DOUBLE_EQ (callback->receivedMessages[0].message.getTimeStamp(), 1.5); +} + +//============================================================================== +// Edge case tests +TEST_F (MidiDataConcatenatorTests, EmptyData) +{ + pushData ({}, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 0); +} + +TEST_F (MidiDataConcatenatorTests, NullData) +{ + struct CustomData + { + } customData; + + concatenator->pushMidiData (nullptr, 0, 1.0, &customData, *callback); + + EXPECT_EQ (callback->receivedMessages.size(), 0); +} + +TEST_F (MidiDataConcatenatorTests, ZeroBytes) +{ + struct CustomData + { + } customData; + + std::vector data { 0x90, 0x3c, 0x64 }; + concatenator->pushMidiData (data.data(), 0, 1.0, &customData, *callback); + + EXPECT_EQ (callback->receivedMessages.size(), 0); +} + +TEST_F (MidiDataConcatenatorTests, SingleByte) +{ + pushData ({ 0xf8 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); + EXPECT_TRUE (callback->receivedMessages[0].message.isMidiClock()); +} + +TEST_F (MidiDataConcatenatorTests, ResetBetweenMessages) +{ + pushData ({ 0x90, 0x3c, 0x64 }, 1.0); + + concatenator->reset(); + + pushData ({ 0x80, 0x40, 0x40 }, 2.0); + + EXPECT_EQ (callback->receivedMessages.size(), 2); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_TRUE (callback->receivedMessages[1].message.isNoteOff()); +} + +TEST_F (MidiDataConcatenatorTests, MultipleResetsInARow) +{ + concatenator->reset(); + concatenator->reset(); + concatenator->reset(); + + pushData ({ 0x90, 0x3c, 0x64 }, 1.0); + + EXPECT_EQ (callback->receivedMessages.size(), 1); +} + +//============================================================================== +// Complex scenarios +TEST_F (MidiDataConcatenatorTests, RealisticMidiStream) +{ + // Note on + pushData ({ 0x90, 0x3c, 0x64 }, 0.0); + + // Clock messages (typical during playback) + pushData ({ 0xf8 }, 0.02); + pushData ({ 0xf8 }, 0.04); + + // Controller change + pushData ({ 0xb0, 0x07, 0x7f }, 0.05); + + // More clock + pushData ({ 0xf8 }, 0.06); + + // Note off using running status + pushData ({ 0x80, 0x3c, 0x40 }, 0.1); + + EXPECT_EQ (callback->receivedMessages.size(), 6); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_TRUE (callback->receivedMessages[1].message.isMidiClock()); + EXPECT_TRUE (callback->receivedMessages[2].message.isMidiClock()); + EXPECT_TRUE (callback->receivedMessages[3].message.isController()); + EXPECT_TRUE (callback->receivedMessages[4].message.isMidiClock()); + EXPECT_TRUE (callback->receivedMessages[5].message.isNoteOff()); +} + +TEST_F (MidiDataConcatenatorTests, MixedFragmentedAndCompleteMessages) +{ + pushData ({ 0x90 }, 0.0); + pushData ({ 0x3c, 0x64, 0xb0, 0x07 }, 0.01); + pushData ({ 0x7f }, 0.02); + + EXPECT_EQ (callback->receivedMessages.size(), 2); + EXPECT_TRUE (callback->receivedMessages[0].message.isNoteOn()); + EXPECT_TRUE (callback->receivedMessages[1].message.isController()); +} + +TEST_F (MidiDataConcatenatorTests, AllChannels) +{ + for (int ch = 0; ch < 16; ++ch) + { + pushData ({ static_cast (0x90 | ch), 0x3c, 0x64 }, 0.0); + } + + EXPECT_EQ (callback->receivedMessages.size(), 16); + for (int ch = 0; ch < 16; ++ch) + { + EXPECT_EQ (callback->receivedMessages[ch].message.getChannel(), ch + 1); + } +} diff --git a/tests/yup_audio_basics/yup_MidiKeyboardState.cpp b/tests/yup_audio_basics/yup_MidiKeyboardState.cpp new file mode 100644 index 000000000..0e45092b1 --- /dev/null +++ b/tests/yup_audio_basics/yup_MidiKeyboardState.cpp @@ -0,0 +1,777 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +namespace +{ +class TestListener : public MidiKeyboardState::Listener +{ +public: + void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override + { + noteOnCalls.push_back ({ midiChannel, midiNoteNumber, velocity }); + } + + void handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override + { + noteOffCalls.push_back ({ midiChannel, midiNoteNumber, velocity }); + } + + void reset() + { + noteOnCalls.clear(); + noteOffCalls.clear(); + } + + struct NoteEvent + { + int channel; + int note; + float velocity; + }; + + std::vector noteOnCalls; + std::vector noteOffCalls; +}; +} // namespace + +//============================================================================== +class MidiKeyboardStateTests : public ::testing::Test +{ +protected: + void SetUp() override + { + state = std::make_unique(); + listener = std::make_unique(); + } + + void TearDown() override + { + listener.reset(); + state.reset(); + } + + std::unique_ptr state; + std::unique_ptr listener; +}; + +//============================================================================== +// Constructor and Reset Tests +//============================================================================== +TEST_F (MidiKeyboardStateTests, Constructor) +{ + // Tests lines 43-46 + EXPECT_NO_THROW (MidiKeyboardState()); + + // All notes should be off initially + for (int ch = 1; ch <= 16; ++ch) + { + for (int note = 0; note < 128; ++note) + { + EXPECT_FALSE (state->isNoteOn (ch, note)); + } + } +} + +TEST_F (MidiKeyboardStateTests, Reset) +{ + // Tests lines 49-54 + state->noteOn (1, 60, 0.5f); + state->noteOn (2, 64, 0.6f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (2, 64)); + + state->reset(); + + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (2, 64)); + + // Verify all notes are off + for (int ch = 1; ch <= 16; ++ch) + { + for (int note = 0; note < 128; ++note) + { + EXPECT_FALSE (state->isNoteOn (ch, note)); + } + } +} + +//============================================================================== +// Note State Query Tests +//============================================================================== +TEST_F (MidiKeyboardStateTests, IsNoteOnInitially) +{ + // Tests lines 56-62 + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (16, 127)); +} + +TEST_F (MidiKeyboardStateTests, IsNoteOnAfterNoteOn) +{ + // Tests lines 56-62 + state->noteOn (1, 60, 0.5f); + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (2, 60)); // Different channel + EXPECT_FALSE (state->isNoteOn (1, 61)); // Different note +} + +TEST_F (MidiKeyboardStateTests, IsNoteOnMultipleChannels) +{ + // Tests lines 56-62 + state->noteOn (1, 60, 0.5f); + state->noteOn (5, 60, 0.6f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (5, 60)); + EXPECT_FALSE (state->isNoteOn (3, 60)); +} + +TEST_F (MidiKeyboardStateTests, IsNoteOnInvalidNote) +{ + // Tests lines 60-61 (bounds check) + EXPECT_FALSE (state->isNoteOn (1, -1)); + EXPECT_FALSE (state->isNoteOn (1, 128)); + EXPECT_FALSE (state->isNoteOn (1, 200)); +} + +TEST_F (MidiKeyboardStateTests, IsNoteOnForChannels) +{ + // Tests lines 64-68 + state->noteOn (1, 60, 0.5f); + state->noteOn (5, 60, 0.6f); + + EXPECT_TRUE (state->isNoteOnForChannels (0x0001, 60)); // Channel 1 + EXPECT_TRUE (state->isNoteOnForChannels (0x0010, 60)); // Channel 5 + EXPECT_TRUE (state->isNoteOnForChannels (0x0011, 60)); // Channels 1 and 5 + EXPECT_FALSE (state->isNoteOnForChannels (0x0002, 60)); // Channel 2 + EXPECT_FALSE (state->isNoteOnForChannels (0xFFFF, 61)); // All channels, wrong note +} + +TEST_F (MidiKeyboardStateTests, IsNoteOnForChannelsInvalidNote) +{ + // Tests line 66 (bounds check) + EXPECT_FALSE (state->isNoteOnForChannels (0xFFFF, -1)); + EXPECT_FALSE (state->isNoteOnForChannels (0xFFFF, 128)); +} + +//============================================================================== +// Note On Tests +//============================================================================== +TEST_F (MidiKeyboardStateTests, NoteOn) +{ + // Tests lines 70-85 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOnCalls.size(), 1); + EXPECT_EQ (listener->noteOnCalls[0].channel, 1); + EXPECT_EQ (listener->noteOnCalls[0].note, 60); + EXPECT_FLOAT_EQ (listener->noteOnCalls[0].velocity, 0.5f); +} + +TEST_F (MidiKeyboardStateTests, NoteOnMultipleNotes) +{ + // Tests lines 70-85 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + state->noteOn (1, 64, 0.6f); + state->noteOn (1, 67, 0.7f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (1, 64)); + EXPECT_TRUE (state->isNoteOn (1, 67)); + EXPECT_EQ (listener->noteOnCalls.size(), 3); +} + +TEST_F (MidiKeyboardStateTests, NoteOnMultipleChannels) +{ + // Tests lines 70-85 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + state->noteOn (5, 60, 0.6f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (5, 60)); + EXPECT_EQ (listener->noteOnCalls.size(), 2); +} + +TEST_F (MidiKeyboardStateTests, DISABLED_NoteOnInvalidNote) +{ + // Tests lines 77-84 (bounds check) + state->addListener (listener.get()); + + state->noteOn (1, -1, 0.5f); + state->noteOn (1, 128, 0.5f); + + EXPECT_EQ (listener->noteOnCalls.size(), 0); +} + +TEST_F (MidiKeyboardStateTests, NoteOnInternal) +{ + // Tests lines 87-97 + state->addListener (listener.get()); + + // Call internal method directly (would normally be called by processNextMidiEvent) + state->processNextMidiEvent (MidiMessage::noteOn (1, 60, 0.5f)); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOnCalls.size(), 1); +} + +//============================================================================== +// Note Off Tests +//============================================================================== +TEST_F (MidiKeyboardStateTests, NoteOff) +{ + // Tests lines 99-111 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + listener->reset(); + + state->noteOff (1, 60, 0.0f); + + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOffCalls.size(), 1); + EXPECT_EQ (listener->noteOffCalls[0].channel, 1); + EXPECT_EQ (listener->noteOffCalls[0].note, 60); +} + +TEST_F (MidiKeyboardStateTests, NoteOffWithoutNoteOn) +{ + // Tests line 103 (note not on) + state->addListener (listener.get()); + + state->noteOff (1, 60, 0.0f); + + EXPECT_EQ (listener->noteOffCalls.size(), 0); +} + +TEST_F (MidiKeyboardStateTests, NoteOffWrongChannel) +{ + // Tests line 103 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + listener->reset(); + + state->noteOff (2, 60, 0.0f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOffCalls.size(), 0); +} + +TEST_F (MidiKeyboardStateTests, NoteOffMultipleNotes) +{ + // Tests lines 99-111 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + state->noteOn (1, 64, 0.6f); + state->noteOn (1, 67, 0.7f); + listener->reset(); + + state->noteOff (1, 64, 0.0f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (1, 64)); + EXPECT_TRUE (state->isNoteOn (1, 67)); + EXPECT_EQ (listener->noteOffCalls.size(), 1); +} + +TEST_F (MidiKeyboardStateTests, NoteOffInternal) +{ + // Tests lines 113-123 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + listener->reset(); + + // Call internal method directly + state->processNextMidiEvent (MidiMessage::noteOff (1, 60)); + + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOffCalls.size(), 1); +} + +//============================================================================== +// All Notes Off Tests +//============================================================================== +TEST_F (MidiKeyboardStateTests, AllNotesOffSingleChannel) +{ + // Tests lines 125-139 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + state->noteOn (1, 64, 0.6f); + state->noteOn (1, 67, 0.7f); + state->noteOn (2, 72, 0.8f); + listener->reset(); + + state->allNotesOff (1); + + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (1, 64)); + EXPECT_FALSE (state->isNoteOn (1, 67)); + EXPECT_TRUE (state->isNoteOn (2, 72)); // Other channel unaffected +} + +TEST_F (MidiKeyboardStateTests, AllNotesOffAllChannels) +{ + // Tests lines 129-133 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + state->noteOn (5, 64, 0.6f); + state->noteOn (10, 67, 0.7f); + state->noteOn (16, 72, 0.8f); + listener->reset(); + + state->allNotesOff (0); + + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (5, 64)); + EXPECT_FALSE (state->isNoteOn (10, 67)); + EXPECT_FALSE (state->isNoteOn (16, 72)); +} + +TEST_F (MidiKeyboardStateTests, AllNotesOffEmptyState) +{ + // Tests lines 125-139 + state->addListener (listener.get()); + + state->allNotesOff (1); + + EXPECT_EQ (listener->noteOffCalls.size(), 0); +} + +//============================================================================== +// Process MIDI Event Tests +//============================================================================== +TEST_F (MidiKeyboardStateTests, ProcessNextMidiEventNoteOn) +{ + // Tests lines 141-156 (note on path) + state->addListener (listener.get()); + + MidiMessage msg = MidiMessage::noteOn (1, 60, 0.5f); + state->processNextMidiEvent (msg); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOnCalls.size(), 1); +} + +TEST_F (MidiKeyboardStateTests, ProcessNextMidiEventNoteOff) +{ + // Tests lines 147-150 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + listener->reset(); + + MidiMessage msg = MidiMessage::noteOff (1, 60); + state->processNextMidiEvent (msg); + + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOffCalls.size(), 1); +} + +TEST_F (MidiKeyboardStateTests, ProcessNextMidiEventAllNotesOff) +{ + // Tests lines 151-155 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + state->noteOn (1, 64, 0.6f); + state->noteOn (1, 67, 0.7f); + listener->reset(); + + MidiMessage msg = MidiMessage::allNotesOff (1); + state->processNextMidiEvent (msg); + + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (1, 64)); + EXPECT_FALSE (state->isNoteOn (1, 67)); +} + +TEST_F (MidiKeyboardStateTests, ProcessNextMidiEventNonNoteMessage) +{ + // Tests lines 141-156 (no matching case) + state->addListener (listener.get()); + + MidiMessage msg = MidiMessage::controllerEvent (1, 7, 100); + state->processNextMidiEvent (msg); + + EXPECT_EQ (listener->noteOnCalls.size(), 0); + EXPECT_EQ (listener->noteOffCalls.size(), 0); +} + +//============================================================================== +// Process MIDI Buffer Tests +//============================================================================== +TEST_F (MidiKeyboardStateTests, ProcessNextMidiBufferBasic) +{ + // Tests lines 158-181 + state->addListener (listener.get()); + + MidiBuffer buffer; + buffer.addEvent (MidiMessage::noteOn (1, 60, 0.5f), 0); + buffer.addEvent (MidiMessage::noteOn (1, 64, 0.6f), 10); + buffer.addEvent (MidiMessage::noteOff (1, 60), 20); + + state->processNextMidiBuffer (buffer, 0, 100, false); + + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (1, 64)); + EXPECT_EQ (listener->noteOnCalls.size(), 2); + EXPECT_EQ (listener->noteOffCalls.size(), 1); +} + +TEST_F (MidiKeyboardStateTests, ProcessNextMidiBufferWithInjectEvents) +{ + // Tests lines 168-178 (inject indirect events) + state->noteOn (1, 60, 0.5f); + state->noteOn (1, 64, 0.6f); + + MidiBuffer buffer; + state->processNextMidiBuffer (buffer, 0, 100, true); + + // Should inject the noteOn events + EXPECT_GT (buffer.getNumEvents(), 0); + + int noteCount = 0; + for (const auto metadata : buffer) + { + if (metadata.getMessage().isNoteOn()) + ++noteCount; + } + + EXPECT_EQ (noteCount, 2); +} + +TEST_F (MidiKeyboardStateTests, ProcessNextMidiBufferWithoutInjectEvents) +{ + // Tests lines 158-181 (without inject) + state->noteOn (1, 60, 0.5f); + + MidiBuffer buffer; + state->processNextMidiBuffer (buffer, 0, 100, false); + + // Should NOT inject events + EXPECT_EQ (buffer.getNumEvents(), 0); +} + +TEST_F (MidiKeyboardStateTests, ProcessNextMidiBufferMultipleCalls) +{ + // Tests lines 158-181 + state->addListener (listener.get()); + + MidiBuffer buffer1; + buffer1.addEvent (MidiMessage::noteOn (1, 60, 0.5f), 0); + state->processNextMidiBuffer (buffer1, 0, 100, false); + + MidiBuffer buffer2; + buffer2.addEvent (MidiMessage::noteOn (1, 64, 0.6f), 0); + state->processNextMidiBuffer (buffer2, 0, 100, false); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (1, 64)); +} + +TEST_F (MidiKeyboardStateTests, ProcessNextMidiBufferEmptyBuffer) +{ + // Tests lines 158-181 + state->addListener (listener.get()); + + MidiBuffer buffer; + state->processNextMidiBuffer (buffer, 0, 100, false); + + EXPECT_EQ (listener->noteOnCalls.size(), 0); + EXPECT_EQ (listener->noteOffCalls.size(), 0); +} + +TEST_F (MidiKeyboardStateTests, ProcessNextMidiBufferClearsEventsToAdd) +{ + // Tests line 180 + state->noteOn (1, 60, 0.5f); + + MidiBuffer buffer; + state->processNextMidiBuffer (buffer, 0, 100, true); + + // Process again - should not inject same events + MidiBuffer buffer2; + state->processNextMidiBuffer (buffer2, 0, 100, true); + + EXPECT_EQ (buffer2.getNumEvents(), 0); +} + +//============================================================================== +// Listener Tests +//============================================================================== +TEST_F (MidiKeyboardStateTests, AddListener) +{ + // Tests lines 184-188 + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + + EXPECT_EQ (listener->noteOnCalls.size(), 1); +} + +TEST_F (MidiKeyboardStateTests, RemoveListener) +{ + // Tests lines 190-194 + state->addListener (listener.get()); + state->removeListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + + EXPECT_EQ (listener->noteOnCalls.size(), 0); +} + +TEST_F (MidiKeyboardStateTests, MultipleListeners) +{ + // Tests lines 184-194 + TestListener listener2; + + state->addListener (listener.get()); + state->addListener (&listener2); + + state->noteOn (1, 60, 0.5f); + + EXPECT_EQ (listener->noteOnCalls.size(), 1); + EXPECT_EQ (listener2.noteOnCalls.size(), 1); +} + +TEST_F (MidiKeyboardStateTests, RemoveOneOfMultipleListeners) +{ + // Tests lines 190-194 + TestListener listener2; + + state->addListener (listener.get()); + state->addListener (&listener2); + state->removeListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + + EXPECT_EQ (listener->noteOnCalls.size(), 0); + EXPECT_EQ (listener2.noteOnCalls.size(), 1); +} + +TEST_F (MidiKeyboardStateTests, ListenerNoteOnCallback) +{ + // Tests lines 92-95 + state->addListener (listener.get()); + + state->noteOn (5, 72, 0.8f); + + EXPECT_EQ (listener->noteOnCalls.size(), 1); + EXPECT_EQ (listener->noteOnCalls[0].channel, 5); + EXPECT_EQ (listener->noteOnCalls[0].note, 72); + EXPECT_FLOAT_EQ (listener->noteOnCalls[0].velocity, 0.8f); +} + +TEST_F (MidiKeyboardStateTests, ListenerNoteOffCallback) +{ + // Tests lines 118-121 + state->addListener (listener.get()); + + state->noteOn (3, 48, 0.7f); + listener->reset(); + + state->noteOff (3, 48, 0.2f); + + EXPECT_EQ (listener->noteOffCalls.size(), 1); + EXPECT_EQ (listener->noteOffCalls[0].channel, 3); + EXPECT_EQ (listener->noteOffCalls[0].note, 48); + EXPECT_FLOAT_EQ (listener->noteOffCalls[0].velocity, 0.2f); +} + +//============================================================================== +// Edge Case Tests +//============================================================================== +TEST_F (MidiKeyboardStateTests, AllChannelsAllNotes) +{ + // Test all 16 channels, all 128 notes + for (int ch = 1; ch <= 16; ++ch) + { + for (int note = 0; note < 128; ++note) + { + EXPECT_FALSE (state->isNoteOn (ch, note)); + state->noteOn (ch, note, 0.5f); + EXPECT_TRUE (state->isNoteOn (ch, note)); + } + } + + // Turn all off + state->allNotesOff (0); + + for (int ch = 1; ch <= 16; ++ch) + { + for (int note = 0; note < 128; ++note) + { + EXPECT_FALSE (state->isNoteOn (ch, note)); + } + } +} + +TEST_F (MidiKeyboardStateTests, SameNoteMultipleChannelsIndependent) +{ + state->noteOn (1, 60, 0.5f); + state->noteOn (5, 60, 0.6f); + state->noteOn (10, 60, 0.7f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (5, 60)); + EXPECT_TRUE (state->isNoteOn (10, 60)); + + state->noteOff (5, 60, 0.0f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (5, 60)); + EXPECT_TRUE (state->isNoteOn (10, 60)); +} + +TEST_F (MidiKeyboardStateTests, RepeatedNoteOnSameNote) +{ + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.5f); + state->noteOn (1, 60, 0.6f); // Same note again + state->noteOn (1, 60, 0.7f); // And again + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOnCalls.size(), 3); // All should trigger callbacks +} + +TEST_F (MidiKeyboardStateTests, ZeroVelocity) +{ + state->addListener (listener.get()); + + state->noteOn (1, 60, 0.0f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOnCalls.size(), 1); + EXPECT_FLOAT_EQ (listener->noteOnCalls[0].velocity, 0.0f); +} + +TEST_F (MidiKeyboardStateTests, MaxVelocity) +{ + state->addListener (listener.get()); + + state->noteOn (1, 60, 1.0f); + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_EQ (listener->noteOnCalls.size(), 1); + EXPECT_FLOAT_EQ (listener->noteOnCalls[0].velocity, 1.0f); +} + +TEST_F (MidiKeyboardStateTests, ChannelBoundaries) +{ + state->noteOn (1, 60, 0.5f); // Min channel + state->noteOn (16, 60, 0.5f); // Max channel + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (16, 60)); +} + +TEST_F (MidiKeyboardStateTests, NoteBoundaries) +{ + state->noteOn (1, 0, 0.5f); // Min note + state->noteOn (1, 127, 0.5f); // Max note + + EXPECT_TRUE (state->isNoteOn (1, 0)); + EXPECT_TRUE (state->isNoteOn (1, 127)); +} + +TEST_F (MidiKeyboardStateTests, ThreadSafety) +{ + // Basic thread safety test with concurrent access + state->addListener (listener.get()); + + std::thread t1 ([this]() + { + for (int i = 0; i < 100; ++i) + { + state->noteOn (1, 60, 0.5f); + state->noteOff (1, 60, 0.0f); + } + }); + + std::thread t2 ([this]() + { + for (int i = 0; i < 100; ++i) + { + state->noteOn (2, 64, 0.5f); + state->noteOff (2, 64, 0.0f); + } + }); + + t1.join(); + t2.join(); + + // Should not crash and state should be consistent + EXPECT_FALSE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (2, 64)); +} + +TEST_F (MidiKeyboardStateTests, ComplexSequence) +{ + state->addListener (listener.get()); + + // Simulate a musical sequence + state->noteOn (1, 60, 0.8f); // C + state->noteOn (1, 64, 0.7f); // E + state->noteOn (1, 67, 0.6f); // G + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (1, 64)); + EXPECT_TRUE (state->isNoteOn (1, 67)); + + state->noteOff (1, 64, 0.0f); // Release E + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_FALSE (state->isNoteOn (1, 64)); + EXPECT_TRUE (state->isNoteOn (1, 67)); + + state->noteOn (1, 65, 0.7f); // F + + EXPECT_TRUE (state->isNoteOn (1, 60)); + EXPECT_TRUE (state->isNoteOn (1, 65)); + EXPECT_TRUE (state->isNoteOn (1, 67)); + + state->allNotesOff (1); + + for (int note = 0; note < 128; ++note) + { + EXPECT_FALSE (state->isNoteOn (1, note)); + } +} diff --git a/tests/yup_audio_basics/yup_MidiMessage.cpp b/tests/yup_audio_basics/yup_MidiMessage.cpp index de2841ee9..022b87c52 100644 --- a/tests/yup_audio_basics/yup_MidiMessage.cpp +++ b/tests/yup_audio_basics/yup_MidiMessage.cpp @@ -184,3 +184,1128 @@ TEST (MidiMessageTests, DataConstructorWorksWithMalformedMetaEvents) runTest (copy); } } + +//============================================================================== +// Constructor Tests +//============================================================================== +TEST (MidiMessageTests, DefaultConstructor) +{ + // Tests lines 121-126 (default constructor creates empty sysex) + MidiMessage msg; + + EXPECT_TRUE (msg.isSysEx()); + EXPECT_EQ (msg.getRawDataSize(), 2); + EXPECT_EQ (msg.getRawData()[0], 0xf0); + EXPECT_EQ (msg.getRawData()[1], 0xf7); +} + +TEST (MidiMessageTests, SingleByteConstructor) +{ + // Tests lines 139-147 + MidiMessage msg (0xf8, 1.5); + + EXPECT_EQ (msg.getRawDataSize(), 1); + EXPECT_EQ (msg.getRawData()[0], 0xf8); + EXPECT_DOUBLE_EQ (msg.getTimeStamp(), 1.5); +} + +TEST (MidiMessageTests, TwoByteConstructor) +{ + // Tests lines 149-158 + MidiMessage msg (0xc0, 64, 2.5); + + EXPECT_EQ (msg.getRawDataSize(), 2); + EXPECT_EQ (msg.getRawData()[0], 0xc0); + EXPECT_EQ (msg.getRawData()[1], 64); + EXPECT_DOUBLE_EQ (msg.getTimeStamp(), 2.5); +} + +TEST (MidiMessageTests, ThreeByteConstructor) +{ + // Tests lines 160-170 + MidiMessage msg (0x90, 60, 100, 3.5); + + EXPECT_EQ (msg.getRawDataSize(), 3); + EXPECT_EQ (msg.getRawData()[0], 0x90); + EXPECT_EQ (msg.getRawData()[1], 60); + EXPECT_EQ (msg.getRawData()[2], 100); + EXPECT_DOUBLE_EQ (msg.getTimeStamp(), 3.5); +} + +TEST (MidiMessageTests, CopyConstructor) +{ + // Tests lines 172-180 + MidiMessage original (0x90, 60, 100, 1.0); + MidiMessage copy (original); + + EXPECT_EQ (copy.getRawDataSize(), original.getRawDataSize()); + EXPECT_EQ (copy.getRawData()[0], original.getRawData()[0]); + EXPECT_EQ (copy.getRawData()[1], original.getRawData()[1]); + EXPECT_EQ (copy.getRawData()[2], original.getRawData()[2]); + EXPECT_DOUBLE_EQ (copy.getTimeStamp(), original.getTimeStamp()); +} + +TEST (MidiMessageTests, CopyConstructorWithNewTimestamp) +{ + // Tests lines 182-190 + MidiMessage original (0x90, 60, 100, 1.0); + MidiMessage copy (original, 5.0); + + EXPECT_EQ (copy.getRawDataSize(), original.getRawDataSize()); + EXPECT_EQ (copy.getRawData()[0], original.getRawData()[0]); + EXPECT_DOUBLE_EQ (copy.getTimeStamp(), 5.0); +} + +TEST (MidiMessageTests, MoveConstructor) +{ + // Tests lines 316-322 + MidiMessage original (0x90, 60, 100, 1.0); + MidiMessage moved (std::move (original)); + + EXPECT_EQ (moved.getRawDataSize(), 3); + EXPECT_EQ (moved.getRawData()[0], 0x90); + EXPECT_EQ (original.getRawDataSize(), 0); +} + +TEST (MidiMessageTests, CopyAssignment) +{ + // Tests lines 285-314 + MidiMessage msg1 (0x90, 60, 100); + MidiMessage msg2 (0x80, 64, 0); + + msg2 = msg1; + + EXPECT_EQ (msg2.getRawDataSize(), msg1.getRawDataSize()); + EXPECT_EQ (msg2.getRawData()[0], msg1.getRawData()[0]); +} + +TEST (MidiMessageTests, MoveAssignment) +{ + // Tests lines 324-331 + MidiMessage msg1 (0x90, 60, 100); + MidiMessage msg2 (0x80, 64, 0); + + msg2 = std::move (msg1); + + EXPECT_EQ (msg2.getRawDataSize(), 3); + EXPECT_EQ (msg1.getRawDataSize(), 0); +} + +//============================================================================== +// Helper Function Tests +//============================================================================== +TEST (MidiMessageTests, FloatValueToMidiByte) +{ + // Tests lines 57-63 + EXPECT_EQ (MidiMessage::floatValueToMidiByte (0.0f), 0); + EXPECT_EQ (MidiMessage::floatValueToMidiByte (0.5f), 64); + EXPECT_EQ (MidiMessage::floatValueToMidiByte (1.0f), 127); +} + +TEST (MidiMessageTests, PitchbendToPitchwheelPos) +{ + // Tests lines 65-74 + EXPECT_EQ (MidiMessage::pitchbendToPitchwheelPos (0.0f, 2.0f), 8192); + EXPECT_EQ (MidiMessage::pitchbendToPitchwheelPos (2.0f, 2.0f), 16383); + EXPECT_EQ (MidiMessage::pitchbendToPitchwheelPos (-2.0f, 2.0f), 0); +} + +TEST (MidiMessageTests, GetMessageLengthFromFirstByte) +{ + // Tests lines 102-118 + EXPECT_EQ (MidiMessage::getMessageLengthFromFirstByte (0x80), 3); // Note off + EXPECT_EQ (MidiMessage::getMessageLengthFromFirstByte (0x90), 3); // Note on + EXPECT_EQ (MidiMessage::getMessageLengthFromFirstByte (0xc0), 2); // Program change + EXPECT_EQ (MidiMessage::getMessageLengthFromFirstByte (0xe0), 3); // Pitch wheel + EXPECT_EQ (MidiMessage::getMessageLengthFromFirstByte (0xf1), 2); // Quarter frame + EXPECT_EQ (MidiMessage::getMessageLengthFromFirstByte (0xf8), 1); // Clock +} + +//============================================================================== +// Timestamp Tests +//============================================================================== +TEST (MidiMessageTests, GetSetTimeStamp) +{ + MidiMessage msg (0x90, 60, 100); + + EXPECT_DOUBLE_EQ (msg.getTimeStamp(), 0.0); + + msg.setTimeStamp (5.5); + EXPECT_DOUBLE_EQ (msg.getTimeStamp(), 5.5); +} + +TEST (MidiMessageTests, AddToTimeStamp) +{ + MidiMessage msg (0x90, 60, 100, 1.0); + + msg.addToTimeStamp (2.5); + EXPECT_DOUBLE_EQ (msg.getTimeStamp(), 3.5); +} + +TEST (MidiMessageTests, WithTimeStamp) +{ + // Tests line 393-396 + MidiMessage msg (0x90, 60, 100, 1.0); + MidiMessage newMsg = msg.withTimeStamp (5.0); + + EXPECT_DOUBLE_EQ (msg.getTimeStamp(), 1.0); + EXPECT_DOUBLE_EQ (newMsg.getTimeStamp(), 5.0); +} + +//============================================================================== +// Channel Tests +//============================================================================== +TEST (MidiMessageTests, GetChannel) +{ + // Tests lines 398-406 + MidiMessage msg1 (0x90, 60, 100); // Channel 1 + EXPECT_EQ (msg1.getChannel(), 1); + + MidiMessage msg2 (0x95, 60, 100); // Channel 6 + EXPECT_EQ (msg2.getChannel(), 6); + + MidiMessage msg3 (0xf0); // System message + EXPECT_EQ (msg3.getChannel(), 0); +} + +TEST (MidiMessageTests, IsForChannel) +{ + // Tests lines 408-416 + MidiMessage msg (0x90, 60, 100); // Channel 1 + + EXPECT_TRUE (msg.isForChannel (1)); + EXPECT_FALSE (msg.isForChannel (2)); +} + +TEST (MidiMessageTests, SetChannel) +{ + // Tests lines 418-427 + MidiMessage msg (0x90, 60, 100); // Channel 1 + + msg.setChannel (5); + EXPECT_EQ (msg.getChannel(), 5); +} + +//============================================================================== +// Note On/Off Tests +//============================================================================== +TEST (MidiMessageTests, IsNoteOn) +{ + // Tests lines 429-435 + MidiMessage noteOn (0x90, 60, 100); + EXPECT_TRUE (noteOn.isNoteOn()); + EXPECT_TRUE (noteOn.isNoteOn (true)); + + MidiMessage noteOnZeroVel (0x90, 60, 0); + EXPECT_FALSE (noteOnZeroVel.isNoteOn()); + EXPECT_TRUE (noteOnZeroVel.isNoteOn (true)); +} + +TEST (MidiMessageTests, IsNoteOff) +{ + // Tests lines 437-443 + MidiMessage noteOff (0x80, 60, 0); + EXPECT_TRUE (noteOff.isNoteOff()); + + MidiMessage noteOnZeroVel (0x90, 60, 0); + EXPECT_TRUE (noteOnZeroVel.isNoteOff (true)); + EXPECT_FALSE (noteOnZeroVel.isNoteOff (false)); +} + +TEST (MidiMessageTests, IsNoteOnOrOff) +{ + // Tests lines 445-449 + MidiMessage noteOn (0x90, 60, 100); + MidiMessage noteOff (0x80, 60, 0); + MidiMessage controller (0xb0, 7, 100); + + EXPECT_TRUE (noteOn.isNoteOnOrOff()); + EXPECT_TRUE (noteOff.isNoteOnOrOff()); + EXPECT_FALSE (controller.isNoteOnOrOff()); +} + +TEST (MidiMessageTests, GetNoteNumber) +{ + // Tests lines 451-454 + MidiMessage msg (0x90, 60, 100); + EXPECT_EQ (msg.getNoteNumber(), 60); +} + +TEST (MidiMessageTests, SetNoteNumber) +{ + // Tests lines 456-460 + MidiMessage msg (0x90, 60, 100); + msg.setNoteNumber (64); + EXPECT_EQ (msg.getNoteNumber(), 64); +} + +TEST (MidiMessageTests, GetVelocity) +{ + // Tests lines 462-468 + MidiMessage noteOn (0x90, 60, 100); + EXPECT_EQ (noteOn.getVelocity(), 100); + + MidiMessage controller (0xb0, 7, 100); + EXPECT_EQ (controller.getVelocity(), 0); +} + +TEST (MidiMessageTests, GetFloatVelocity) +{ + // Tests lines 470-473 + MidiMessage msg (0x90, 60, 127); + EXPECT_FLOAT_EQ (msg.getFloatVelocity(), 1.0f); + + MidiMessage msg2 (0x90, 60, 64); + EXPECT_NEAR (msg2.getFloatVelocity(), 0.5039f, 0.01f); +} + +TEST (MidiMessageTests, SetVelocity) +{ + // Tests lines 475-479 + MidiMessage msg (0x90, 60, 100); + msg.setVelocity (0.5f); + EXPECT_EQ (msg.getVelocity(), 64); +} + +TEST (MidiMessageTests, MultiplyVelocity) +{ + // Tests lines 481-488 + MidiMessage msg (0x90, 60, 100); + msg.multiplyVelocity (0.5f); + EXPECT_EQ (msg.getVelocity(), 50); +} + +TEST (MidiMessageTests, NoteOnFactoryFloat) +{ + // Tests lines 628-631 + MidiMessage msg = MidiMessage::noteOn (1, 60, 0.5f); + EXPECT_TRUE (msg.isNoteOn()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getNoteNumber(), 60); + EXPECT_EQ (msg.getVelocity(), 64); +} + +TEST (MidiMessageTests, NoteOnFactoryUint8) +{ + // Tests lines 618-626 + MidiMessage msg = MidiMessage::noteOn (1, 60, (uint8) 100); + EXPECT_TRUE (msg.isNoteOn()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getNoteNumber(), 60); + EXPECT_EQ (msg.getVelocity(), 100); +} + +TEST (MidiMessageTests, NoteOffFactoryFloat) +{ + // Tests lines 643-646 + MidiMessage msg = MidiMessage::noteOff (1, 60, 0.5f); + EXPECT_TRUE (msg.isNoteOff()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getNoteNumber(), 60); +} + +TEST (MidiMessageTests, NoteOffFactoryUint8) +{ + // Tests lines 633-641 + MidiMessage msg = MidiMessage::noteOff (1, 60, (uint8) 64); + EXPECT_TRUE (msg.isNoteOff()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getNoteNumber(), 60); + EXPECT_EQ (msg.getVelocity(), 64); +} + +TEST (MidiMessageTests, NoteOffFactoryNoVelocity) +{ + // Tests lines 648-654 + MidiMessage msg = MidiMessage::noteOff (1, 60); + EXPECT_TRUE (msg.isNoteOff()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getNoteNumber(), 60); + EXPECT_EQ (msg.getVelocity(), 0); +} + +//============================================================================== +// Controller Tests +//============================================================================== +TEST (MidiMessageTests, IsController) +{ + // Tests lines 585-588 + MidiMessage controller (0xb0, 7, 100); + EXPECT_TRUE (controller.isController()); + + MidiMessage noteOn (0x90, 60, 100); + EXPECT_FALSE (noteOn.isController()); +} + +TEST (MidiMessageTests, IsControllerOfType) +{ + // Tests lines 590-594 + MidiMessage controller (0xb0, 7, 100); + EXPECT_TRUE (controller.isControllerOfType (7)); + EXPECT_FALSE (controller.isControllerOfType (10)); +} + +TEST (MidiMessageTests, GetControllerNumber) +{ + // Tests lines 596-600 + MidiMessage controller (0xb0, 7, 100); + EXPECT_EQ (controller.getControllerNumber(), 7); +} + +TEST (MidiMessageTests, GetControllerValue) +{ + // Tests lines 602-606 + MidiMessage controller (0xb0, 7, 100); + EXPECT_EQ (controller.getControllerValue(), 100); +} + +TEST (MidiMessageTests, ControllerEventFactory) +{ + // Tests lines 608-616 + MidiMessage msg = MidiMessage::controllerEvent (1, 7, 100); + EXPECT_TRUE (msg.isController()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getControllerNumber(), 7); + EXPECT_EQ (msg.getControllerValue(), 100); +} + +TEST (MidiMessageTests, IsSustainPedalOn) +{ + // Tests line 533 + MidiMessage msg = MidiMessage::controllerEvent (1, 0x40, 64); + EXPECT_TRUE (msg.isSustainPedalOn()); + EXPECT_FALSE (msg.isSustainPedalOff()); +} + +TEST (MidiMessageTests, IsSustainPedalOff) +{ + // Tests line 535 + MidiMessage msg = MidiMessage::controllerEvent (1, 0x40, 63); + EXPECT_TRUE (msg.isSustainPedalOff()); + EXPECT_FALSE (msg.isSustainPedalOn()); +} + +TEST (MidiMessageTests, IsSostenutoPedalOn) +{ + // Tests line 537 + MidiMessage msg = MidiMessage::controllerEvent (1, 0x42, 64); + EXPECT_TRUE (msg.isSostenutoPedalOn()); +} + +TEST (MidiMessageTests, IsSostenutoPedalOff) +{ + // Tests line 539 + MidiMessage msg = MidiMessage::controllerEvent (1, 0x42, 63); + EXPECT_TRUE (msg.isSostenutoPedalOff()); +} + +TEST (MidiMessageTests, IsSoftPedalOn) +{ + // Tests line 541 + MidiMessage msg = MidiMessage::controllerEvent (1, 0x43, 64); + EXPECT_TRUE (msg.isSoftPedalOn()); +} + +TEST (MidiMessageTests, IsSoftPedalOff) +{ + // Tests line 543 + MidiMessage msg = MidiMessage::controllerEvent (1, 0x43, 63); + EXPECT_TRUE (msg.isSoftPedalOff()); +} + +TEST (MidiMessageTests, AllNotesOff) +{ + // Tests lines 656-659, 661-665 + MidiMessage msg = MidiMessage::allNotesOff (1); + EXPECT_TRUE (msg.isAllNotesOff()); + EXPECT_EQ (msg.getControllerNumber(), 123); +} + +TEST (MidiMessageTests, AllSoundOff) +{ + // Tests lines 667-670, 672-676 + MidiMessage msg = MidiMessage::allSoundOff (1); + EXPECT_TRUE (msg.isAllSoundOff()); + EXPECT_EQ (msg.getControllerNumber(), 120); +} + +TEST (MidiMessageTests, IsResetAllControllers) +{ + // Tests lines 678-682 + MidiMessage msg = MidiMessage::controllerEvent (1, 121, 0); + EXPECT_TRUE (msg.isResetAllControllers()); +} + +TEST (MidiMessageTests, AllControllersOff) +{ + // Tests lines 684-687 + MidiMessage msg = MidiMessage::allControllersOff (1); + EXPECT_TRUE (msg.isResetAllControllers()); +} + +//============================================================================== +// Program Change Tests +//============================================================================== +TEST (MidiMessageTests, IsProgramChange) +{ + // Tests lines 545-548 + MidiMessage msg (0xc0, 64); + EXPECT_TRUE (msg.isProgramChange()); +} + +TEST (MidiMessageTests, GetProgramChangeNumber) +{ + // Tests lines 550-554 + MidiMessage msg (0xc0, 64); + EXPECT_EQ (msg.getProgramChangeNumber(), 64); +} + +TEST (MidiMessageTests, ProgramChangeFactory) +{ + // Tests lines 556-561 + MidiMessage msg = MidiMessage::programChange (1, 64); + EXPECT_TRUE (msg.isProgramChange()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getProgramChangeNumber(), 64); +} + +//============================================================================== +// Pitch Wheel Tests +//============================================================================== +TEST (MidiMessageTests, IsPitchWheel) +{ + // Tests lines 563-566 + MidiMessage msg (0xe0, 0, 64); + EXPECT_TRUE (msg.isPitchWheel()); +} + +TEST (MidiMessageTests, GetPitchWheelValue) +{ + // Tests lines 568-573 + MidiMessage msg (0xe0, 0, 64); + EXPECT_EQ (msg.getPitchWheelValue(), 8192); +} + +TEST (MidiMessageTests, PitchWheelFactory) +{ + // Tests lines 575-583 + MidiMessage msg = MidiMessage::pitchWheel (1, 8192); + EXPECT_TRUE (msg.isPitchWheel()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getPitchWheelValue(), 8192); +} + +//============================================================================== +// Aftertouch Tests +//============================================================================== +TEST (MidiMessageTests, IsAftertouch) +{ + // Tests lines 490-493 + MidiMessage msg (0xa0, 60, 64); + EXPECT_TRUE (msg.isAftertouch()); +} + +TEST (MidiMessageTests, GetAfterTouchValue) +{ + // Tests lines 495-499 + MidiMessage msg (0xa0, 60, 64); + EXPECT_EQ (msg.getAfterTouchValue(), 64); +} + +TEST (MidiMessageTests, AftertouchChangeFactory) +{ + // Tests lines 501-512 + MidiMessage msg = MidiMessage::aftertouchChange (1, 60, 64); + EXPECT_TRUE (msg.isAftertouch()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getNoteNumber(), 60); + EXPECT_EQ (msg.getAfterTouchValue(), 64); +} + +//============================================================================== +// Channel Pressure Tests +//============================================================================== +TEST (MidiMessageTests, IsChannelPressure) +{ + // Tests lines 514-517 + MidiMessage msg (0xd0, 64); + EXPECT_TRUE (msg.isChannelPressure()); +} + +TEST (MidiMessageTests, GetChannelPressureValue) +{ + // Tests lines 519-523 + MidiMessage msg (0xd0, 64); + EXPECT_EQ (msg.getChannelPressureValue(), 64); +} + +TEST (MidiMessageTests, ChannelPressureChangeFactory) +{ + // Tests lines 525-531 + MidiMessage msg = MidiMessage::channelPressureChange (1, 64); + EXPECT_TRUE (msg.isChannelPressure()); + EXPECT_EQ (msg.getChannel(), 1); + EXPECT_EQ (msg.getChannelPressureValue(), 64); +} + +//============================================================================== +// SysEx Tests +//============================================================================== +TEST (MidiMessageTests, IsSysEx) +{ + // Tests lines 697-700 + MidiMessage msg; + EXPECT_TRUE (msg.isSysEx()); + + MidiMessage noteOn (0x90, 60, 100); + EXPECT_FALSE (noteOn.isSysEx()); +} + +TEST (MidiMessageTests, CreateSysExMessage) +{ + // Tests lines 702-711 + uint8 data[] = { 0x01, 0x02, 0x03 }; + MidiMessage msg = MidiMessage::createSysExMessage (data, 3); + + EXPECT_TRUE (msg.isSysEx()); + EXPECT_EQ (msg.getSysExDataSize(), 3); +} + +TEST (MidiMessageTests, CreateSysExMessageFromSpan) +{ + // Tests lines 713-716 + std::byte data[] = { std::byte { 0x01 }, std::byte { 0x02 }, std::byte { 0x03 } }; + Span span (data, 3); + MidiMessage msg = MidiMessage::createSysExMessage (span); + + EXPECT_TRUE (msg.isSysEx()); + EXPECT_EQ (msg.getSysExDataSize(), 3); +} + +TEST (MidiMessageTests, GetSysExData) +{ + // Tests lines 718-721 + uint8 data[] = { 0x01, 0x02, 0x03 }; + MidiMessage msg = MidiMessage::createSysExMessage (data, 3); + + const uint8* sysexData = msg.getSysExData(); + EXPECT_NE (sysexData, nullptr); + EXPECT_EQ (sysexData[0], 0x01); + EXPECT_EQ (sysexData[1], 0x02); + EXPECT_EQ (sysexData[2], 0x03); +} + +//============================================================================== +// Meta Event Tests +//============================================================================== +TEST (MidiMessageTests, IsMetaEvent) +{ + // Tests line 729 + MidiMessage msg (0xff, 0x03, 0x00); + EXPECT_TRUE (msg.isMetaEvent()); +} + +TEST (MidiMessageTests, IsActiveSense) +{ + // Tests line 731 + MidiMessage msg (0xfe); + EXPECT_TRUE (msg.isActiveSense()); +} + +TEST (MidiMessageTests, GetMetaEventType) +{ + // Tests lines 733-737 + MidiMessage msg (0xff, 0x03, 0x00); + EXPECT_EQ (msg.getMetaEventType(), 0x03); +} + +TEST (MidiMessageTests, IsTrackMetaEvent) +{ + // Tests line 761 + MidiMessage msg (0xff, 0x00, 0x02, 0x00, 0x00); + EXPECT_TRUE (msg.isTrackMetaEvent()); +} + +TEST (MidiMessageTests, IsEndOfTrackMetaEvent) +{ + // Tests lines 763, 949-952 + MidiMessage msg = MidiMessage::endOfTrack(); + EXPECT_TRUE (msg.isEndOfTrackMetaEvent()); +} + +TEST (MidiMessageTests, IsTextMetaEvent) +{ + // Tests lines 765-769 + MidiMessage msg (0xff, 0x01, 0x00); + EXPECT_TRUE (msg.isTextMetaEvent()); +} + +TEST (MidiMessageTests, TextMetaEvent) +{ + // Tests lines 779-808 + MidiMessage msg = MidiMessage::textMetaEvent (1, "Test"); + EXPECT_TRUE (msg.isTextMetaEvent()); + EXPECT_EQ (msg.getMetaEventType(), 1); +} + +TEST (MidiMessageTests, GetTextFromTextMetaEvent) +{ + // Tests lines 771-777 + MidiMessage msg = MidiMessage::textMetaEvent (1, "Hello"); + String text = msg.getTextFromTextMetaEvent(); + EXPECT_TRUE (text == "Hello"); +} + +TEST (MidiMessageTests, IsTrackNameEvent) +{ + // Tests lines 810-814 + MidiMessage msg = MidiMessage::textMetaEvent (3, "Track1"); + EXPECT_TRUE (msg.isTrackNameEvent()); +} + +TEST (MidiMessageTests, IsTempoMetaEvent) +{ + // Tests lines 816-820 + MidiMessage msg = MidiMessage::tempoMetaEvent (500000); + EXPECT_TRUE (msg.isTempoMetaEvent()); +} + +TEST (MidiMessageTests, TempoMetaEvent) +{ + // Tests lines 882-885 + MidiMessage msg = MidiMessage::tempoMetaEvent (500000); + EXPECT_TRUE (msg.isTempoMetaEvent()); + EXPECT_NEAR (msg.getTempoSecondsPerQuarterNote(), 0.5, 0.001); +} + +TEST (MidiMessageTests, GetTempoSecondsPerQuarterNote) +{ + // Tests lines 834-845 + MidiMessage msg = MidiMessage::tempoMetaEvent (500000); + EXPECT_NEAR (msg.getTempoSecondsPerQuarterNote(), 0.5, 0.001); +} + +TEST (MidiMessageTests, GetTempoMetaEventTickLength) +{ + // Tests lines 847-880 + MidiMessage msg = MidiMessage::tempoMetaEvent (500000); + double tickLength = msg.getTempoMetaEventTickLength (480); + EXPECT_GT (tickLength, 0.0); +} + +TEST (MidiMessageTests, IsMidiChannelMetaEvent) +{ + // Tests lines 822-826 + MidiMessage msg = MidiMessage::midiChannelMetaEvent (1); + EXPECT_TRUE (msg.isMidiChannelMetaEvent()); +} + +TEST (MidiMessageTests, MidiChannelMetaEvent) +{ + // Tests lines 922-925 + MidiMessage msg = MidiMessage::midiChannelMetaEvent (5); + EXPECT_TRUE (msg.isMidiChannelMetaEvent()); + EXPECT_EQ (msg.getMidiChannelMetaEventChannel(), 5); +} + +TEST (MidiMessageTests, GetMidiChannelMetaEventChannel) +{ + // Tests lines 828-832 + MidiMessage msg = MidiMessage::midiChannelMetaEvent (5); + EXPECT_EQ (msg.getMidiChannelMetaEventChannel(), 5); +} + +TEST (MidiMessageTests, IsTimeSignatureMetaEvent) +{ + // Tests lines 887-891 + MidiMessage msg = MidiMessage::timeSignatureMetaEvent (4, 4); + EXPECT_TRUE (msg.isTimeSignatureMetaEvent()); +} + +TEST (MidiMessageTests, TimeSignatureMetaEvent) +{ + // Tests lines 908-920 + MidiMessage msg = MidiMessage::timeSignatureMetaEvent (3, 4); + EXPECT_TRUE (msg.isTimeSignatureMetaEvent()); + + int num, denom; + msg.getTimeSignatureInfo (num, denom); + EXPECT_EQ (num, 3); + EXPECT_EQ (denom, 4); +} + +TEST (MidiMessageTests, GetTimeSignatureInfo) +{ + // Tests lines 893-906 + MidiMessage msg = MidiMessage::timeSignatureMetaEvent (6, 8); + + int numerator, denominator; + msg.getTimeSignatureInfo (numerator, denominator); + + EXPECT_EQ (numerator, 6); + EXPECT_EQ (denominator, 8); +} + +TEST (MidiMessageTests, IsKeySignatureMetaEvent) +{ + // Tests lines 927-930 + MidiMessage msg = MidiMessage::keySignatureMetaEvent (2, false); + EXPECT_TRUE (msg.isKeySignatureMetaEvent()); +} + +TEST (MidiMessageTests, KeySignatureMetaEvent) +{ + // Tests lines 942-947 + MidiMessage msg = MidiMessage::keySignatureMetaEvent (2, false); + EXPECT_TRUE (msg.isKeySignatureMetaEvent()); + EXPECT_EQ (msg.getKeySignatureNumberOfSharpsOrFlats(), 2); + EXPECT_TRUE (msg.isKeySignatureMajorKey()); +} + +TEST (MidiMessageTests, GetKeySignatureNumberOfSharpsOrFlats) +{ + // Tests lines 932-935 + MidiMessage msg = MidiMessage::keySignatureMetaEvent (-3, true); + EXPECT_EQ (msg.getKeySignatureNumberOfSharpsOrFlats(), -3); +} + +TEST (MidiMessageTests, IsKeySignatureMajorKey) +{ + // Tests lines 937-940 + MidiMessage major = MidiMessage::keySignatureMetaEvent (2, false); + EXPECT_TRUE (major.isKeySignatureMajorKey()); + + MidiMessage minor = MidiMessage::keySignatureMetaEvent (2, true); + EXPECT_FALSE (minor.isKeySignatureMajorKey()); +} + +//============================================================================== +// System Real-Time Tests +//============================================================================== +TEST (MidiMessageTests, IsSongPositionPointer) +{ + // Tests line 955 + MidiMessage msg (0xf2, 0, 0); + EXPECT_TRUE (msg.isSongPositionPointer()); +} + +TEST (MidiMessageTests, GetSongPositionPointerMidiBeat) +{ + // Tests lines 957-961 + MidiMessage msg (0xf2, 0, 64); + EXPECT_EQ (msg.getSongPositionPointerMidiBeat(), 8192); +} + +TEST (MidiMessageTests, SongPositionPointerFactory) +{ + // Tests lines 963-968 + MidiMessage msg = MidiMessage::songPositionPointer (1024); + EXPECT_TRUE (msg.isSongPositionPointer()); + EXPECT_EQ (msg.getSongPositionPointerMidiBeat(), 1024); +} + +TEST (MidiMessageTests, IsMidiStart) +{ + // Tests lines 970, 972 + MidiMessage msg = MidiMessage::midiStart(); + EXPECT_TRUE (msg.isMidiStart()); +} + +TEST (MidiMessageTests, IsMidiContinue) +{ + // Tests lines 974, 976 + MidiMessage msg = MidiMessage::midiContinue(); + EXPECT_TRUE (msg.isMidiContinue()); +} + +TEST (MidiMessageTests, IsMidiStop) +{ + // Tests lines 978, 980 + MidiMessage msg = MidiMessage::midiStop(); + EXPECT_TRUE (msg.isMidiStop()); +} + +TEST (MidiMessageTests, IsMidiClock) +{ + // Tests lines 982, 984 + MidiMessage msg = MidiMessage::midiClock(); + EXPECT_TRUE (msg.isMidiClock()); +} + +//============================================================================== +// SMPTE/MTC Tests +//============================================================================== +TEST (MidiMessageTests, IsQuarterFrame) +{ + // Tests line 986 + MidiMessage msg (0xf1, 0x00); + EXPECT_TRUE (msg.isQuarterFrame()); +} + +TEST (MidiMessageTests, GetQuarterFrameSequenceNumber) +{ + // Tests line 988 + MidiMessage msg (0xf1, 0x35); + EXPECT_EQ (msg.getQuarterFrameSequenceNumber(), 3); +} + +TEST (MidiMessageTests, GetQuarterFrameValue) +{ + // Tests line 990 + MidiMessage msg (0xf1, 0x35); + EXPECT_EQ (msg.getQuarterFrameValue(), 5); +} + +TEST (MidiMessageTests, QuarterFrameFactory) +{ + // Tests lines 992-995 + MidiMessage msg = MidiMessage::quarterFrame (3, 5); + EXPECT_TRUE (msg.isQuarterFrame()); + EXPECT_EQ (msg.getQuarterFrameSequenceNumber(), 3); + EXPECT_EQ (msg.getQuarterFrameValue(), 5); +} + +TEST (MidiMessageTests, IsFullFrame) +{ + // Tests lines 997-1006 + MidiMessage msg = MidiMessage::fullFrame (1, 2, 3, 4, MidiMessage::fps24); + EXPECT_TRUE (msg.isFullFrame()); +} + +TEST (MidiMessageTests, FullFrameFactory) +{ + // Tests lines 1020-1023 + MidiMessage msg = MidiMessage::fullFrame (1, 30, 45, 10, MidiMessage::fps25); + EXPECT_TRUE (msg.isFullFrame()); + + int hours, minutes, seconds, frames; + MidiMessage::SmpteTimecodeType timecode; + msg.getFullFrameParameters (hours, minutes, seconds, frames, timecode); + + EXPECT_EQ (hours, 1); + EXPECT_EQ (minutes, 30); + EXPECT_EQ (seconds, 45); + EXPECT_EQ (frames, 10); + EXPECT_EQ (timecode, MidiMessage::fps25); +} + +TEST (MidiMessageTests, GetFullFrameParameters) +{ + // Tests lines 1008-1018 + MidiMessage msg = MidiMessage::fullFrame (2, 15, 30, 20, MidiMessage::fps30); + + int hours, minutes, seconds, frames; + MidiMessage::SmpteTimecodeType timecode; + msg.getFullFrameParameters (hours, minutes, seconds, frames, timecode); + + EXPECT_EQ (hours, 2); + EXPECT_EQ (minutes, 15); + EXPECT_EQ (seconds, 30); + EXPECT_EQ (frames, 20); +} + +//============================================================================== +// MIDI Machine Control Tests +//============================================================================== +TEST (MidiMessageTests, IsMidiMachineControlMessage) +{ + // Tests lines 1025-1033 + MidiMessage msg = MidiMessage::midiMachineControlCommand (MidiMessage::mmc_stop); + EXPECT_TRUE (msg.isMidiMachineControlMessage()); +} + +TEST (MidiMessageTests, GetMidiMachineControlCommand) +{ + // Tests lines 1035-1040 + MidiMessage msg = MidiMessage::midiMachineControlCommand (MidiMessage::mmc_play); + EXPECT_EQ (msg.getMidiMachineControlCommand(), MidiMessage::mmc_play); +} + +TEST (MidiMessageTests, MidiMachineControlCommandFactory) +{ + // Tests lines 1042-1045 + MidiMessage msg = MidiMessage::midiMachineControlCommand (MidiMessage::mmc_stop); + EXPECT_TRUE (msg.isMidiMachineControlMessage()); + EXPECT_EQ (msg.getMidiMachineControlCommand(), MidiMessage::mmc_stop); +} + +TEST (MidiMessageTests, IsMidiMachineControlGoto) +{ + // Tests lines 1048-1069 + MidiMessage msg = MidiMessage::midiMachineControlGoto (1, 30, 45, 10); + + int hours, minutes, seconds, frames; + EXPECT_TRUE (msg.isMidiMachineControlGoto (hours, minutes, seconds, frames)); + EXPECT_EQ (hours, 1); + EXPECT_EQ (minutes, 30); + EXPECT_EQ (seconds, 45); + EXPECT_EQ (frames, 10); +} + +TEST (MidiMessageTests, MidiMachineControlGotoFactory) +{ + // Tests lines 1071-1074 + MidiMessage msg = MidiMessage::midiMachineControlGoto (2, 15, 30, 20); + + int hours, minutes, seconds, frames; + EXPECT_TRUE (msg.isMidiMachineControlGoto (hours, minutes, seconds, frames)); + EXPECT_EQ (hours, 2); +} + +//============================================================================== +// Note Name and Frequency Tests +//============================================================================== +TEST (MidiMessageTests, GetMidiNoteName) +{ + // Tests lines 1077-1094 + EXPECT_EQ (MidiMessage::getMidiNoteName (60, true, true, 3), "C3"); + EXPECT_EQ (MidiMessage::getMidiNoteName (61, true, true, 3), "C#3"); + EXPECT_EQ (MidiMessage::getMidiNoteName (61, false, true, 3), "Db3"); + EXPECT_EQ (MidiMessage::getMidiNoteName (60, true, false, 3), "C"); +} + +TEST (MidiMessageTests, GetMidiNoteInHertz) +{ + // Tests lines 1096-1099 + double freq = MidiMessage::getMidiNoteInHertz (69, 440.0); + EXPECT_NEAR (freq, 440.0, 0.01); + + freq = MidiMessage::getMidiNoteInHertz (60, 440.0); + EXPECT_NEAR (freq, 261.63, 0.01); +} + +TEST (MidiMessageTests, IsMidiNoteBlack) +{ + // Tests lines 1101-1104 + EXPECT_FALSE (MidiMessage::isMidiNoteBlack (60)); // C + EXPECT_TRUE (MidiMessage::isMidiNoteBlack (61)); // C# + EXPECT_FALSE (MidiMessage::isMidiNoteBlack (62)); // D + EXPECT_TRUE (MidiMessage::isMidiNoteBlack (63)); // D# + EXPECT_FALSE (MidiMessage::isMidiNoteBlack (64)); // E +} + +//============================================================================== +// Master Volume Test +//============================================================================== +TEST (MidiMessageTests, MasterVolume) +{ + // Tests lines 689-694 + MidiMessage msg = MidiMessage::masterVolume (0.5f); + EXPECT_TRUE (msg.isSysEx()); + EXPECT_EQ (msg.getRawDataSize(), 8); +} + +//============================================================================== +// Description Tests +//============================================================================== +TEST (MidiMessageTests, GetDescriptionNoteOn) +{ + // Tests lines 351-392 (getDescription) + MidiMessage msg (0x90, 60, 100); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("Note on")); + EXPECT_TRUE (desc.contains ("Channel 1")); +} + +TEST (MidiMessageTests, GetDescriptionNoteOff) +{ + MidiMessage msg (0x80, 60, 64); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("Note off")); +} + +TEST (MidiMessageTests, GetDescriptionProgramChange) +{ + MidiMessage msg = MidiMessage::programChange (1, 10); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("Program change")); +} + +TEST (MidiMessageTests, GetDescriptionPitchWheel) +{ + MidiMessage msg = MidiMessage::pitchWheel (1, 8192); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("Pitch wheel")); +} + +TEST (MidiMessageTests, GetDescriptionAftertouch) +{ + MidiMessage msg = MidiMessage::aftertouchChange (1, 60, 64); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("Aftertouch")); +} + +TEST (MidiMessageTests, GetDescriptionChannelPressure) +{ + MidiMessage msg = MidiMessage::channelPressureChange (1, 64); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("Channel pressure")); +} + +TEST (MidiMessageTests, GetDescriptionController) +{ + MidiMessage msg = MidiMessage::controllerEvent (1, 7, 100); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("Controller")); +} + +TEST (MidiMessageTests, GetDescriptionAllNotesOff) +{ + MidiMessage msg = MidiMessage::allNotesOff (1); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("All notes off")); +} + +TEST (MidiMessageTests, GetDescriptionAllSoundOff) +{ + MidiMessage msg = MidiMessage::allSoundOff (1); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("All sound off")); +} + +TEST (MidiMessageTests, GetDescriptionMetaEvent) +{ + MidiMessage msg (0xff, 0x03, 0x00); + String desc = msg.getDescription(); + EXPECT_TRUE (desc.contains ("Meta event")); +} + +//============================================================================== +// GM Instrument Name Tests +//============================================================================== +TEST (MidiMessageTests, GetGMInstrumentName) +{ + // Tests lines 1106-1243 + EXPECT_STREQ (MidiMessage::getGMInstrumentName (0), "Acoustic Grand Piano"); + EXPECT_STREQ (MidiMessage::getGMInstrumentName (24), "Acoustic Guitar (nylon)"); + EXPECT_NE (MidiMessage::getGMInstrumentName (127), nullptr); + EXPECT_EQ (MidiMessage::getGMInstrumentName (128), nullptr); +} + +TEST (MidiMessageTests, GetGMInstrumentBankName) +{ + // Tests lines 1245-1270 + EXPECT_STREQ (MidiMessage::getGMInstrumentBankName (0), "Piano"); + EXPECT_STREQ (MidiMessage::getGMInstrumentBankName (3), "Guitar"); + EXPECT_NE (MidiMessage::getGMInstrumentBankName (15), nullptr); + EXPECT_EQ (MidiMessage::getGMInstrumentBankName (16), nullptr); +} + +TEST (MidiMessageTests, GetRhythmInstrumentName) +{ + // Tests lines 1272-1328 + EXPECT_STREQ (MidiMessage::getRhythmInstrumentName (35), "Acoustic Bass Drum"); + EXPECT_STREQ (MidiMessage::getRhythmInstrumentName (42), "Closed Hi-Hat"); + EXPECT_NE (MidiMessage::getRhythmInstrumentName (81), nullptr); + EXPECT_EQ (MidiMessage::getRhythmInstrumentName (34), nullptr); + EXPECT_EQ (MidiMessage::getRhythmInstrumentName (82), nullptr); +} + +TEST (MidiMessageTests, GetControllerName) +{ + // Tests lines 1330-1467 + EXPECT_STREQ (MidiMessage::getControllerName (0), "Bank Select"); + EXPECT_STREQ (MidiMessage::getControllerName (7), "Volume (coarse)"); + EXPECT_STREQ (MidiMessage::getControllerName (64), "Hold Pedal (on/off)"); + EXPECT_EQ (MidiMessage::getControllerName (3), nullptr); +} diff --git a/tests/yup_audio_basics/yup_MidiMessageSequence.cpp b/tests/yup_audio_basics/yup_MidiMessageSequence.cpp index 21919cfca..801d93ee3 100644 --- a/tests/yup_audio_basics/yup_MidiMessageSequence.cpp +++ b/tests/yup_audio_basics/yup_MidiMessageSequence.cpp @@ -249,3 +249,771 @@ TEST_F (MidiMessageSequenceTest, CreateControllerUpdatesForTimeEmitsSeparateNRPN checkNrpn (std::next (m.begin(), 8), std::next (m.begin(), 12), channel, numberC, valueC, time); checkNrpn (std::next (m.begin(), 12), std::next (m.begin(), 16), channel, numberD, valueD, time); } + +//============================================================================== +// MidiEventHolder tests (via public interface) +TEST (MidiEventHolderTests, EventHolderViaAddEvent) +{ + MidiMessageSequence seq; + MidiMessage msg = MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (2.0); + + auto* holder = seq.addEvent (msg); + + EXPECT_NE (holder, nullptr); + EXPECT_TRUE (messagesAreEqual (holder->message, msg)); + EXPECT_EQ (holder->noteOffObject, nullptr); +} + +TEST (MidiEventHolderTests, NoteOffObjectAfterUpdateMatchedPairs) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + seq.updateMatchedPairs(); + + auto* noteOnHolder = seq.getEventPointer (0); + auto* noteOffHolder = seq.getEventPointer (1); + + EXPECT_NE (noteOnHolder, nullptr); + EXPECT_NE (noteOffHolder, nullptr); + EXPECT_EQ (noteOnHolder->noteOffObject, noteOffHolder); +} + +//============================================================================== +// Constructor and assignment tests +TEST (MidiMessageSequenceTests, DefaultConstructor) +{ + EXPECT_NO_THROW (MidiMessageSequence()); + + MidiMessageSequence seq; + EXPECT_EQ (seq.getNumEvents(), 0); +} + +TEST (MidiMessageSequenceTests, CopyConstructor) +{ + MidiMessageSequence seq1; + seq1.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq1.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + seq1.updateMatchedPairs(); + + MidiMessageSequence seq2 (seq1); + + EXPECT_EQ (seq2.getNumEvents(), 2); + EXPECT_EQ (seq2.getEventTime (0), 0.0); + EXPECT_EQ (seq2.getEventTime (1), 4.0); + EXPECT_EQ (seq2.getTimeOfMatchingKeyUp (0), 4.0); +} + +TEST (MidiMessageSequenceTests, CopyAssignment) +{ + MidiMessageSequence seq1; + seq1.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq1.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + seq1.updateMatchedPairs(); + + MidiMessageSequence seq2; + seq2 = seq1; + + EXPECT_EQ (seq2.getNumEvents(), 2); + EXPECT_EQ (seq2.getEventTime (0), 0.0); + EXPECT_EQ (seq2.getEventTime (1), 4.0); + EXPECT_EQ (seq2.getTimeOfMatchingKeyUp (0), 4.0); +} + +TEST (MidiMessageSequenceTests, MoveConstructor) +{ + MidiMessageSequence seq1; + seq1.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq1.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + MidiMessageSequence seq2 (std::move (seq1)); + + EXPECT_EQ (seq2.getNumEvents(), 2); + EXPECT_EQ (seq2.getEventTime (0), 0.0); + EXPECT_EQ (seq2.getEventTime (1), 4.0); +} + +TEST (MidiMessageSequenceTests, MoveAssignment) +{ + MidiMessageSequence seq1; + seq1.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq1.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + MidiMessageSequence seq2; + seq2 = std::move (seq1); + + EXPECT_EQ (seq2.getNumEvents(), 2); + EXPECT_EQ (seq2.getEventTime (0), 0.0); + EXPECT_EQ (seq2.getEventTime (1), 4.0); +} + +TEST (MidiMessageSequenceTests, SwapWith) +{ + MidiMessageSequence seq1; + seq1.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq1.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + MidiMessageSequence seq2; + seq2.addEvent (MidiMessage::noteOn (2, 70, 0.6f).withTimeStamp (1.0)); + + seq1.swapWith (seq2); + + EXPECT_EQ (seq1.getNumEvents(), 1); + EXPECT_EQ (seq1.getEventTime (0), 1.0); + EXPECT_EQ (seq2.getNumEvents(), 2); + EXPECT_EQ (seq2.getEventTime (0), 0.0); +} + +//============================================================================== +// Basic operations +TEST (MidiMessageSequenceTests, Clear) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + EXPECT_EQ (seq.getNumEvents(), 2); + + seq.clear(); + + EXPECT_EQ (seq.getNumEvents(), 0); +} + +TEST (MidiMessageSequenceTests, GetNumEvents) +{ + MidiMessageSequence seq; + EXPECT_EQ (seq.getNumEvents(), 0); + + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + EXPECT_EQ (seq.getNumEvents(), 1); + + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + EXPECT_EQ (seq.getNumEvents(), 2); +} + +TEST (MidiMessageSequenceTests, GetEventPointer) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + + auto* event = seq.getEventPointer (0); + EXPECT_NE (event, nullptr); + EXPECT_TRUE (event->message.isNoteOn()); + EXPECT_EQ (event->message.getNoteNumber(), 60); +} + +TEST (MidiMessageSequenceTests, GetEventPointerOutOfRange) +{ + MidiMessageSequence seq; + + auto* event = seq.getEventPointer (0); + EXPECT_EQ (event, nullptr); + + auto* event2 = seq.getEventPointer (100); + EXPECT_EQ (event2, nullptr); +} + +TEST (MidiMessageSequenceTests, BeginEndIterators) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + int count = 0; + for (auto* event : seq) + { + EXPECT_NE (event, nullptr); + ++count; + } + + EXPECT_EQ (count, 2); +} + +TEST (MidiMessageSequenceTests, ConstBeginEndIterators) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + const auto& constSeq = seq; + + int count = 0; + for (auto* event : constSeq) + { + EXPECT_NE (event, nullptr); + ++count; + } + + EXPECT_EQ (count, 2); +} + +TEST (MidiMessageSequenceTests, GetIndexOf) +{ + MidiMessageSequence seq; + auto* event1 = seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + auto* event2 = seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + EXPECT_EQ (seq.getIndexOf (event1), 0); + EXPECT_EQ (seq.getIndexOf (event2), 1); +} + +TEST (MidiMessageSequenceTests, GetIndexOfNonExistentEvent) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + + // Test with nullptr + EXPECT_EQ (seq.getIndexOf (nullptr), -1); +} + +TEST (MidiMessageSequenceTests, GetEventTimeValidIndex) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (3.5)); + + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 3.5); +} + +TEST (MidiMessageSequenceTests, GetEventTimeInvalidIndex) +{ + MidiMessageSequence seq; + + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 0.0); + EXPECT_DOUBLE_EQ (seq.getEventTime (100), 0.0); +} + +TEST (MidiMessageSequenceTests, GetStartTimeEmptySequence) +{ + MidiMessageSequence seq; + EXPECT_DOUBLE_EQ (seq.getStartTime(), 0.0); +} + +TEST (MidiMessageSequenceTests, GetEndTimeEmptySequence) +{ + MidiMessageSequence seq; + EXPECT_DOUBLE_EQ (seq.getEndTime(), 0.0); +} + +//============================================================================== +// Add event tests +TEST (MidiMessageSequenceTests, AddEventWithConstRef) +{ + MidiMessageSequence seq; + MidiMessage msg = MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (2.0); + + auto* event = seq.addEvent (msg); + + EXPECT_NE (event, nullptr); + EXPECT_EQ (seq.getNumEvents(), 1); + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 2.0); +} + +TEST (MidiMessageSequenceTests, AddEventWithMove) +{ + MidiMessageSequence seq; + + auto* event = seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (2.0)); + + EXPECT_NE (event, nullptr); + EXPECT_EQ (seq.getNumEvents(), 1); + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 2.0); +} + +TEST (MidiMessageSequenceTests, AddEventWithTimeAdjustment) +{ + MidiMessageSequence seq; + MidiMessage msg = MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (2.0); + + seq.addEvent (msg, 1.5); + + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 3.5); +} + +TEST (MidiMessageSequenceTests, AddEventsMaintainsOrder) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (2.0)); + seq.addEvent (MidiMessage::noteOn (1, 62, 0.5f).withTimeStamp (1.0)); + seq.addEvent (MidiMessage::noteOn (1, 64, 0.5f).withTimeStamp (3.0)); + + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 1.0); + EXPECT_DOUBLE_EQ (seq.getEventTime (1), 2.0); + EXPECT_DOUBLE_EQ (seq.getEventTime (2), 3.0); +} + +//============================================================================== +// Delete event tests +TEST (MidiMessageSequenceTests, DeleteEventValidIndex) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + EXPECT_EQ (seq.getNumEvents(), 2); + + seq.deleteEvent (0, false); + + EXPECT_EQ (seq.getNumEvents(), 1); + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 4.0); +} + +TEST (MidiMessageSequenceTests, DeleteEventInvalidIndex) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + + EXPECT_NO_THROW (seq.deleteEvent (100, false)); + EXPECT_EQ (seq.getNumEvents(), 1); +} + +//============================================================================== +// Update matched pairs tests +TEST (MidiMessageSequenceTests, UpdateMatchedPairsConsecutiveNoteOns) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (2.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + seq.updateMatchedPairs(); + + // First note-on should get a synthetic note-off at time 2.0 + EXPECT_EQ (seq.getNumEvents(), 4); + EXPECT_DOUBLE_EQ (seq.getTimeOfMatchingKeyUp (0), 2.0); + EXPECT_EQ (seq.getIndexOfMatchingKeyUp (0), 1); +} + +TEST (MidiMessageSequenceTests, UpdateMatchedPairsDifferentChannels) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOn (2, 60, 0.5f).withTimeStamp (1.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (2.0)); + seq.addEvent (MidiMessage::noteOff (2, 60, 0.5f).withTimeStamp (3.0)); + + seq.updateMatchedPairs(); + + EXPECT_EQ (seq.getTimeOfMatchingKeyUp (0), 2.0); + EXPECT_EQ (seq.getTimeOfMatchingKeyUp (1), 3.0); +} + +TEST (MidiMessageSequenceTests, UpdateMatchedPairsDifferentNotes) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOn (1, 62, 0.5f).withTimeStamp (1.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (2.0)); + seq.addEvent (MidiMessage::noteOff (1, 62, 0.5f).withTimeStamp (3.0)); + + seq.updateMatchedPairs(); + + EXPECT_EQ (seq.getTimeOfMatchingKeyUp (0), 2.0); + EXPECT_EQ (seq.getTimeOfMatchingKeyUp (1), 3.0); +} + +TEST (MidiMessageSequenceTests, UpdateMatchedPairsUnmatchedNoteOn) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOn (1, 62, 0.5f).withTimeStamp (1.0)); + + seq.updateMatchedPairs(); + + EXPECT_EQ (seq.getTimeOfMatchingKeyUp (0), 0.0); + EXPECT_EQ (seq.getTimeOfMatchingKeyUp (1), 0.0); +} + +TEST (MidiMessageSequenceTests, GetIndexOfMatchingKeyUpInvalidIndex) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + + EXPECT_EQ (seq.getIndexOfMatchingKeyUp (100), -1); +} + +//============================================================================== +// Time manipulation tests +TEST (MidiMessageSequenceTests, AddTimeToMessages) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); + + seq.addTimeToMessages (2.5); + + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 2.5); + EXPECT_DOUBLE_EQ (seq.getEventTime (1), 6.5); +} + +TEST (MidiMessageSequenceTests, AddTimeToMessagesNegative) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (5.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (9.0)); + + seq.addTimeToMessages (-2.0); + + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 3.0); + EXPECT_DOUBLE_EQ (seq.getEventTime (1), 7.0); +} + +TEST (MidiMessageSequenceTests, AddTimeToMessagesZero) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (5.0)); + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (9.0)); + + seq.addTimeToMessages (0.0); + + EXPECT_DOUBLE_EQ (seq.getEventTime (0), 5.0); + EXPECT_DOUBLE_EQ (seq.getEventTime (1), 9.0); +} + +//============================================================================== +// Add sequence tests +TEST (MidiMessageSequenceTests, AddSequenceSimple) +{ + MidiMessageSequence seq1; + seq1.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + + MidiMessageSequence seq2; + seq2.addEvent (MidiMessage::noteOn (2, 70, 0.6f).withTimeStamp (1.0)); + seq2.addEvent (MidiMessage::noteOff (2, 70, 0.6f).withTimeStamp (5.0)); + + seq1.addSequence (seq2, 0.0); + + EXPECT_EQ (seq1.getNumEvents(), 3); +} + +TEST (MidiMessageSequenceTests, AddSequenceWithTimeAdjustment) +{ + MidiMessageSequence seq1; + seq1.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + + MidiMessageSequence seq2; + seq2.addEvent (MidiMessage::noteOn (2, 70, 0.6f).withTimeStamp (1.0)); + + seq1.addSequence (seq2, 2.5); + + EXPECT_EQ (seq1.getNumEvents(), 2); + EXPECT_DOUBLE_EQ (seq1.getEventTime (1), 3.5); +} + +TEST (MidiMessageSequenceTests, AddSequenceWithTimeRangeInclusive) +{ + MidiMessageSequence seq1; + + MidiMessageSequence seq2; + seq2.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq2.addEvent (MidiMessage::noteOn (1, 62, 0.5f).withTimeStamp (2.0)); + seq2.addEvent (MidiMessage::noteOn (1, 64, 0.5f).withTimeStamp (4.0)); + seq2.addEvent (MidiMessage::noteOn (1, 65, 0.5f).withTimeStamp (6.0)); + + seq1.addSequence (seq2, 0.0, 2.0, 6.0); + + EXPECT_EQ (seq1.getNumEvents(), 2); // Only events at 2.0 and 4.0 + EXPECT_DOUBLE_EQ (seq1.getEventTime (0), 2.0); + EXPECT_DOUBLE_EQ (seq1.getEventTime (1), 4.0); +} + +TEST (MidiMessageSequenceTests, AddSequenceWithTimeRangeAndAdjustment) +{ + MidiMessageSequence seq1; + + MidiMessageSequence seq2; + seq2.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq2.addEvent (MidiMessage::noteOn (1, 62, 0.5f).withTimeStamp (2.0)); + seq2.addEvent (MidiMessage::noteOn (1, 64, 0.5f).withTimeStamp (4.0)); + + // addSequence (other, timeAdjustment, firstAllowableTime, endOfAllowableDestTimes) + // For each event: t = event.time + timeAdjustment + // Include if: t >= firstAllowableTime && t < endOfAllowableDestTimes + seq1.addSequence (seq2, 1.0, 1.0, 4.0); + + // Event at 0.0 + 1.0 = 1.0 (included: 1.0 >= 1.0 && 1.0 < 4.0) + // Event at 2.0 + 1.0 = 3.0 (included: 3.0 >= 1.0 && 3.0 < 4.0) + // Event at 4.0 + 1.0 = 5.0 (excluded: 5.0 >= 4.0) + EXPECT_EQ (seq1.getNumEvents(), 2); + EXPECT_DOUBLE_EQ (seq1.getEventTime (0), 1.0); // 0.0 + 1.0 + EXPECT_DOUBLE_EQ (seq1.getEventTime (1), 3.0); // 2.0 + 1.0 +} + +//============================================================================== +// Sort tests +TEST (MidiMessageSequenceTests, SortMaintainsStability) +{ + MidiMessageSequence seq; + auto* event1 = seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (2.0)); + auto* event2 = seq.addEvent (MidiMessage::noteOn (1, 62, 0.6f).withTimeStamp (2.0)); + auto* event3 = seq.addEvent (MidiMessage::noteOn (1, 64, 0.7f).withTimeStamp (2.0)); + + // All at same time, should maintain insertion order + EXPECT_EQ (seq.getEventPointer (0), event1); + EXPECT_EQ (seq.getEventPointer (1), event2); + EXPECT_EQ (seq.getEventPointer (2), event3); +} + +//============================================================================== +// Extract/delete channel messages tests +TEST (MidiMessageSequenceTests, ExtractMidiChannelMessages) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOn (2, 62, 0.5f).withTimeStamp (1.0)); + seq.addEvent (MidiMessage::noteOn (3, 64, 0.5f).withTimeStamp (2.0)); + + MidiMessageSequence extracted; + seq.extractMidiChannelMessages (2, extracted, false); + + EXPECT_EQ (extracted.getNumEvents(), 1); + EXPECT_EQ (extracted.getEventPointer (0)->message.getChannel(), 2); +} + +TEST (MidiMessageSequenceTests, ExtractMidiChannelMessagesWithMetaEvents) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::tempoMetaEvent (120).withTimeStamp (1.0)); + seq.addEvent (MidiMessage::noteOn (2, 62, 0.5f).withTimeStamp (2.0)); + + MidiMessageSequence extracted; + seq.extractMidiChannelMessages (1, extracted, true); + + EXPECT_EQ (extracted.getNumEvents(), 2); // Note on channel 1 + tempo meta event +} + +TEST (MidiMessageSequenceTests, ExtractMidiChannelMessagesNoMetaEvents) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::tempoMetaEvent (120).withTimeStamp (1.0)); + seq.addEvent (MidiMessage::noteOn (2, 62, 0.5f).withTimeStamp (2.0)); + + MidiMessageSequence extracted; + seq.extractMidiChannelMessages (1, extracted, false); + + EXPECT_EQ (extracted.getNumEvents(), 1); // Only note on channel 1 +} + +TEST (MidiMessageSequenceTests, ExtractSysExMessages) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + + uint8 sysexData[] = { 0xf0, 0x43, 0x12, 0x00, 0xf7 }; + seq.addEvent (MidiMessage::createSysExMessage (sysexData, sizeof (sysexData)).withTimeStamp (1.0)); + + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (2.0)); + + MidiMessageSequence extracted; + seq.extractSysExMessages (extracted); + + EXPECT_EQ (extracted.getNumEvents(), 1); + EXPECT_TRUE (extracted.getEventPointer (0)->message.isSysEx()); +} + +TEST (MidiMessageSequenceTests, DeleteMidiChannelMessages) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + seq.addEvent (MidiMessage::noteOn (2, 62, 0.5f).withTimeStamp (1.0)); + seq.addEvent (MidiMessage::noteOn (1, 64, 0.5f).withTimeStamp (2.0)); + seq.addEvent (MidiMessage::noteOn (3, 65, 0.5f).withTimeStamp (3.0)); + + EXPECT_EQ (seq.getNumEvents(), 4); + + seq.deleteMidiChannelMessages (1); + + EXPECT_EQ (seq.getNumEvents(), 2); + EXPECT_EQ (seq.getEventPointer (0)->message.getChannel(), 2); + EXPECT_EQ (seq.getEventPointer (1)->message.getChannel(), 3); +} + +TEST (MidiMessageSequenceTests, DeleteSysExMessages) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); + + uint8 sysexData1[] = { 0xf0, 0x43, 0x12, 0x00, 0xf7 }; + seq.addEvent (MidiMessage::createSysExMessage (sysexData1, sizeof (sysexData1)).withTimeStamp (1.0)); + + seq.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (2.0)); + + uint8 sysexData2[] = { 0xf0, 0x7e, 0x00, 0x09, 0x01, 0xf7 }; + seq.addEvent (MidiMessage::createSysExMessage (sysexData2, sizeof (sysexData2)).withTimeStamp (3.0)); + + EXPECT_EQ (seq.getNumEvents(), 4); + + seq.deleteSysExMessages(); + + EXPECT_EQ (seq.getNumEvents(), 2); + EXPECT_TRUE (seq.getEventPointer (0)->message.isNoteOn()); + EXPECT_TRUE (seq.getEventPointer (1)->message.isNoteOff()); +} + +//============================================================================== +// CreateControllerUpdatesForTime additional tests +TEST (MidiMessageSequenceTests, CreateControllerUpdatesForTimePitchWheel) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::pitchWheel (1, 4096).withTimeStamp (0.5)); + seq.addEvent (MidiMessage::pitchWheel (1, 12000).withTimeStamp (1.0)); + + Array messages; + seq.createControllerUpdatesForTime (1, 2.0, messages); + + // Should have the latest pitch wheel value + EXPECT_GE (messages.size(), 1); + + bool foundPitchWheel = false; + for (const auto& msg : messages) + { + if (msg.isPitchWheel()) + { + EXPECT_EQ (msg.getPitchWheelValue(), 12000); + foundPitchWheel = true; + } + } + + EXPECT_TRUE (foundPitchWheel); +} + +TEST (MidiMessageSequenceTests, CreateControllerUpdatesForTimeProgramChange) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::programChange (1, 10).withTimeStamp (0.5)); + seq.addEvent (MidiMessage::programChange (1, 42).withTimeStamp (1.0)); + + Array messages; + seq.createControllerUpdatesForTime (1, 2.0, messages); + + // Should have the latest program change + bool foundProgramChange = false; + for (const auto& msg : messages) + { + if (msg.isProgramChange()) + { + EXPECT_EQ (msg.getProgramChangeNumber(), 42); + foundProgramChange = true; + } + } + + EXPECT_TRUE (foundProgramChange); +} + +TEST (MidiMessageSequenceTests, CreateControllerUpdatesForTimeProgramChangeWithBank) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::controllerEvent (1, 0x00, 5).withTimeStamp (0.5)); // Bank MSB + seq.addEvent (MidiMessage::controllerEvent (1, 0x20, 10).withTimeStamp (0.5)); // Bank LSB + seq.addEvent (MidiMessage::programChange (1, 42).withTimeStamp (1.0)); + + Array messages; + seq.createControllerUpdatesForTime (1, 2.0, messages); + + // Should have bank MSB, bank LSB, and program change + int bankMSBCount = 0; + int bankLSBCount = 0; + int programChangeCount = 0; + + for (const auto& msg : messages) + { + if (msg.isController()) + { + if (msg.getControllerNumber() == 0x00) + { + EXPECT_EQ (msg.getControllerValue(), 5); + ++bankMSBCount; + } + else if (msg.getControllerNumber() == 0x20) + { + EXPECT_EQ (msg.getControllerValue(), 10); + ++bankLSBCount; + } + } + else if (msg.isProgramChange()) + { + EXPECT_EQ (msg.getProgramChangeNumber(), 42); + ++programChangeCount; + } + } + + EXPECT_EQ (bankMSBCount, 1); + EXPECT_EQ (bankLSBCount, 1); + EXPECT_EQ (programChangeCount, 1); +} + +TEST (MidiMessageSequenceTests, CreateControllerUpdatesForTimeRegularControllers) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::controllerEvent (1, 7, 100).withTimeStamp (0.5)); // Volume + seq.addEvent (MidiMessage::controllerEvent (1, 10, 64).withTimeStamp (1.0)); // Pan + seq.addEvent (MidiMessage::controllerEvent (1, 7, 127).withTimeStamp (1.5)); // Volume again + + Array messages; + seq.createControllerUpdatesForTime (1, 2.0, messages); + + // Should have the latest values for controllers 7 and 10 + int volumeCount = 0; + int panCount = 0; + + for (const auto& msg : messages) + { + if (msg.isController()) + { + if (msg.getControllerNumber() == 7) + { + EXPECT_EQ (msg.getControllerValue(), 127); // Latest volume + ++volumeCount; + } + else if (msg.getControllerNumber() == 10) + { + EXPECT_EQ (msg.getControllerValue(), 64); + ++panCount; + } + } + } + + EXPECT_EQ (volumeCount, 1); + EXPECT_EQ (panCount, 1); +} + +TEST (MidiMessageSequenceTests, CreateControllerUpdatesForTimeIgnoresFutureEvents) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::controllerEvent (1, 7, 100).withTimeStamp (0.5)); + seq.addEvent (MidiMessage::controllerEvent (1, 7, 50).withTimeStamp (2.5)); + + Array messages; + seq.createControllerUpdatesForTime (1, 1.0, messages); + + // Should only see the first controller value + for (const auto& msg : messages) + { + if (msg.isController() && msg.getControllerNumber() == 7) + { + EXPECT_EQ (msg.getControllerValue(), 100); + } + } +} + +TEST (MidiMessageSequenceTests, CreateControllerUpdatesForTimeDifferentChannel) +{ + MidiMessageSequence seq; + seq.addEvent (MidiMessage::controllerEvent (1, 7, 100).withTimeStamp (0.5)); + seq.addEvent (MidiMessage::controllerEvent (2, 7, 50).withTimeStamp (1.0)); + + Array messages; + seq.createControllerUpdatesForTime (1, 2.0, messages); + + // Should only see controller from channel 1 + for (const auto& msg : messages) + { + EXPECT_EQ (msg.getChannel(), 1); + if (msg.isController() && msg.getControllerNumber() == 7) + { + EXPECT_EQ (msg.getControllerValue(), 100); + } + } +} diff --git a/tests/yup_audio_basics/yup_MixerAudioSource.cpp b/tests/yup_audio_basics/yup_MixerAudioSource.cpp new file mode 100644 index 000000000..563ecd65b --- /dev/null +++ b/tests/yup_audio_basics/yup_MixerAudioSource.cpp @@ -0,0 +1,446 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +namespace +{ +class MockAudioSource : public AudioSource +{ +public: + MockAudioSource() = default; + ~MockAudioSource() override = default; + + void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override + { + prepareToPlayCalled = true; + lastSamplesPerBlock = samplesPerBlockExpected; + lastSampleRate = sampleRate; + } + + void releaseResources() override + { + releaseResourcesCalled = true; + } + + void getNextAudioBlock (const AudioSourceChannelInfo& info) override + { + getNextAudioBlockCalled = true; + + // Fill with a constant value for testing + for (int ch = 0; ch < info.buffer->getNumChannels(); ++ch) + { + for (int i = 0; i < info.numSamples; ++i) + { + info.buffer->setSample (ch, info.startSample + i, fillValue); + } + } + } + + bool prepareToPlayCalled = false; + bool releaseResourcesCalled = false; + bool getNextAudioBlockCalled = false; + int lastSamplesPerBlock = 0; + double lastSampleRate = 0.0; + float fillValue = 0.5f; +}; +} // namespace + +//============================================================================== +class MixerAudioSourceTests : public ::testing::Test +{ +protected: + void SetUp() override + { + mixer = std::make_unique(); + } + + void TearDown() override + { + mixer.reset(); + } + + std::unique_ptr mixer; +}; + +//============================================================================== +TEST_F (MixerAudioSourceTests, Constructor) +{ + EXPECT_NO_THROW (MixerAudioSource()); +} + +TEST_F (MixerAudioSourceTests, Destructor) +{ + auto* temp = new MixerAudioSource(); + auto* source = new MockAudioSource(); + temp->addInputSource (source, true); + + // Destructor should call removeAllInputs which releases resources + EXPECT_NO_THROW (delete temp); +} + +//============================================================================== +TEST_F (MixerAudioSourceTests, AddInputSourceWithNull) +{ + // Should not crash with null (line 57) + EXPECT_NO_THROW (mixer->addInputSource (nullptr, false)); +} + +TEST_F (MixerAudioSourceTests, AddInputSourceWithoutDelete) +{ + MockAudioSource source; + mixer->addInputSource (&source, false); + + // Source should not be prepared if mixer hasn't been prepared yet (line 68) + EXPECT_FALSE (source.prepareToPlayCalled); +} + +TEST_F (MixerAudioSourceTests, AddInputSourceAfterPrepare) +{ + mixer->prepareToPlay (512, 44100.0); + + MockAudioSource source; + mixer->addInputSource (&source, false); + + // Source should be prepared if mixer was already prepared (line 68-69) + EXPECT_TRUE (source.prepareToPlayCalled); + EXPECT_EQ (source.lastSamplesPerBlock, 512); + EXPECT_DOUBLE_EQ (source.lastSampleRate, 44100.0); +} + +TEST_F (MixerAudioSourceTests, AddInputSourceWithDelete) +{ + auto* source = new MockAudioSource(); + mixer->addInputSource (source, true); + + // Cleanup will happen in mixer destructor or removeInputSource +} + +TEST_F (MixerAudioSourceTests, AddDuplicateInput) +{ + MockAudioSource source; + mixer->addInputSource (&source, false); + + // Adding same source again should be ignored (line 57) + mixer->addInputSource (&source, false); +} + +//============================================================================== +TEST_F (MixerAudioSourceTests, RemoveInputSourceWithNull) +{ + // Should not crash with null (line 80) + EXPECT_NO_THROW (mixer->removeInputSource (nullptr)); +} + +TEST_F (MixerAudioSourceTests, RemoveNonExistentInput) +{ + MockAudioSource source; + // Should return early if input not found (line 88-89) + EXPECT_NO_THROW (mixer->removeInputSource (&source)); + EXPECT_FALSE (source.releaseResourcesCalled); +} + +TEST_F (MixerAudioSourceTests, RemoveInputSourceWithoutDelete) +{ + MockAudioSource source; + mixer->addInputSource (&source, false); + mixer->removeInputSource (&source); + + // Should call releaseResources (line 98) + EXPECT_TRUE (source.releaseResourcesCalled); +} + +TEST_F (MixerAudioSourceTests, RemoveInputSourceWithDelete) +{ + auto* source = new MockAudioSource(); + mixer->addInputSource (source, true); + + // Should delete the source (line 91-92) + mixer->removeInputSource (source); + + // Source is deleted, we can't check it anymore but no crash means success +} + +//============================================================================== +TEST_F (MixerAudioSourceTests, RemoveAllInputsEmpty) +{ + EXPECT_NO_THROW (mixer->removeAllInputs()); +} + +TEST_F (MixerAudioSourceTests, RemoveAllInputsWithoutDelete) +{ + MockAudioSource source1; + MockAudioSource source2; + + mixer->addInputSource (&source1, false); + mixer->addInputSource (&source2, false); + + mixer->removeAllInputs(); + + // removeAllInputs only calls releaseResources on inputs marked for deletion (line 109-117) + // Inputs without delete flag don't get releaseResources called + EXPECT_FALSE (source1.releaseResourcesCalled); + EXPECT_FALSE (source2.releaseResourcesCalled); +} + +TEST_F (MixerAudioSourceTests, RemoveAllInputsWithDelete) +{ + auto* source1 = new MockAudioSource(); + auto* source2 = new MockAudioSource(); + + mixer->addInputSource (source1, true); + mixer->addInputSource (source2, true); + + // Should delete and release all sources (line 109-111, 116-117) + EXPECT_NO_THROW (mixer->removeAllInputs()); +} + +TEST_F (MixerAudioSourceTests, RemoveAllInputsMixed) +{ + MockAudioSource source1; + auto* source2 = new MockAudioSource(); + + mixer->addInputSource (&source1, false); + mixer->addInputSource (source2, true); + + mixer->removeAllInputs(); + + // Only inputs marked for deletion get releaseResources called + EXPECT_FALSE (source1.releaseResourcesCalled); +} + +//============================================================================== +TEST_F (MixerAudioSourceTests, PrepareToPlay) +{ + MockAudioSource source1; + MockAudioSource source2; + + mixer->addInputSource (&source1, false); + mixer->addInputSource (&source2, false); + + mixer->prepareToPlay (1024, 48000.0); + + // Should prepare all inputs (line 129-130) + EXPECT_TRUE (source1.prepareToPlayCalled); + EXPECT_TRUE (source2.prepareToPlayCalled); + EXPECT_EQ (source1.lastSamplesPerBlock, 1024); + EXPECT_DOUBLE_EQ (source1.lastSampleRate, 48000.0); +} + +//============================================================================== +TEST_F (MixerAudioSourceTests, ReleaseResources) +{ + MockAudioSource source1; + MockAudioSource source2; + + mixer->addInputSource (&source1, false); + mixer->addInputSource (&source2, false); + + mixer->prepareToPlay (512, 44100.0); + mixer->releaseResources(); + + // Should release all inputs (line 137-138) + EXPECT_TRUE (source1.releaseResourcesCalled); + EXPECT_TRUE (source2.releaseResourcesCalled); +} + +//============================================================================== +TEST_F (MixerAudioSourceTests, GetNextAudioBlockWithNoInputs) +{ + AudioBuffer buffer (2, 512); + buffer.clear(); + + // Fill with non-zero to test clearing + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + buffer.setSample (ch, i, 1.0f); + } + } + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + mixer->getNextAudioBlock (info); + + // Should clear the buffer when no inputs (line 172) + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.0f); + } + } +} + +TEST_F (MixerAudioSourceTests, GetNextAudioBlockWithSingleInput) +{ + MockAudioSource source; + source.fillValue = 0.3f; + + mixer->addInputSource (&source, false); + mixer->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + mixer->getNextAudioBlock (info); + + // Should just call first input directly (line 152) + EXPECT_TRUE (source.getNextAudioBlockCalled); + + // Buffer should contain the source's value + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.3f); + } + } +} + +TEST_F (MixerAudioSourceTests, GetNextAudioBlockWithMultipleInputs) +{ + MockAudioSource source1; + MockAudioSource source2; + MockAudioSource source3; + + source1.fillValue = 0.2f; + source2.fillValue = 0.3f; + source3.fillValue = 0.1f; + + mixer->addInputSource (&source1, false); + mixer->addInputSource (&source2, false); + mixer->addInputSource (&source3, false); + + mixer->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + mixer->getNextAudioBlock (info); + + // Should mix all inputs (line 154-168) + EXPECT_TRUE (source1.getNextAudioBlockCalled); + EXPECT_TRUE (source2.getNextAudioBlockCalled); + EXPECT_TRUE (source3.getNextAudioBlockCalled); + + // Buffer should contain sum of all sources + const float expectedSum = 0.2f + 0.3f + 0.1f; + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + EXPECT_NEAR (buffer.getSample (ch, i), expectedSum, 0.0001f); + } + } +} + +TEST_F (MixerAudioSourceTests, GetNextAudioBlockWithStartSampleOffset) +{ + MockAudioSource source; + source.fillValue = 0.5f; + + mixer->addInputSource (&source, false); + mixer->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 100; + info.numSamples = 256; + + mixer->getNextAudioBlock (info); + + // Check that samples before startSample are still zero + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 100; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.0f); + } + } + + // Check that samples at startSample have the expected value + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 100; i < 356; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.5f); + } + } +} + +TEST_F (MixerAudioSourceTests, GetNextAudioBlockResizesTempBuffer) +{ + MockAudioSource source1; + MockAudioSource source2; + + source1.fillValue = 0.3f; + source2.fillValue = 0.4f; + + mixer->addInputSource (&source1, false); + mixer->addInputSource (&source2, false); + + mixer->prepareToPlay (512, 44100.0); + + // Test with different buffer sizes to trigger temp buffer resize (line 156-157) + AudioBuffer buffer1 (4, 256); + buffer1.clear(); + + AudioSourceChannelInfo info1; + info1.buffer = &buffer1; + info1.startSample = 0; + info1.numSamples = 256; + + mixer->getNextAudioBlock (info1); + + // Now test with larger size + AudioBuffer buffer2 (4, 1024); + buffer2.clear(); + + AudioSourceChannelInfo info2; + info2.buffer = &buffer2; + info2.startSample = 0; + info2.numSamples = 1024; + + EXPECT_NO_THROW (mixer->getNextAudioBlock (info2)); +} diff --git a/tests/yup_audio_basics/yup_ResamplingAudioSource.cpp b/tests/yup_audio_basics/yup_ResamplingAudioSource.cpp new file mode 100644 index 000000000..6c23f2fde --- /dev/null +++ b/tests/yup_audio_basics/yup_ResamplingAudioSource.cpp @@ -0,0 +1,535 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +namespace +{ +class MockAudioSource : public AudioSource +{ +public: + MockAudioSource() = default; + ~MockAudioSource() override = default; + + void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override + { + prepareToPlayCalled = true; + lastSamplesPerBlock = samplesPerBlockExpected; + lastSampleRate = sampleRate; + } + + void releaseResources() override + { + releaseResourcesCalled = true; + } + + void getNextAudioBlock (const AudioSourceChannelInfo& info) override + { + getNextAudioBlockCalled = true; + + // Fill with a simple sine-like pattern for testing + for (int ch = 0; ch < info.buffer->getNumChannels(); ++ch) + { + for (int i = 0; i < info.numSamples; ++i) + { + const float value = std::sin (static_cast (i) * 0.1f) * 0.5f; + info.buffer->setSample (ch, info.startSample + i, value); + } + } + } + + bool prepareToPlayCalled = false; + bool releaseResourcesCalled = false; + bool getNextAudioBlockCalled = false; + int lastSamplesPerBlock = 0; + double lastSampleRate = 0.0; +}; +} // namespace + +//============================================================================== +class ResamplingAudioSourceTests : public ::testing::Test +{ +protected: + void SetUp() override + { + mockSource = new MockAudioSource(); + resampler = std::make_unique (mockSource, true, 2); + } + + void TearDown() override + { + resampler.reset(); + } + + MockAudioSource* mockSource; // Owned by resampler + std::unique_ptr resampler; +}; + +//============================================================================== +TEST_F (ResamplingAudioSourceTests, Constructor) +{ + auto* source = new MockAudioSource(); + EXPECT_NO_THROW (ResamplingAudioSource (source, true, 2)); +} + +TEST_F (ResamplingAudioSourceTests, ConstructorWithDifferentChannels) +{ + auto* source = new MockAudioSource(); + EXPECT_NO_THROW (ResamplingAudioSource (source, true, 8)); +} + +TEST_F (ResamplingAudioSourceTests, Destructor) +{ + auto* source = new MockAudioSource(); + auto* temp = new ResamplingAudioSource (source, true, 2); + EXPECT_NO_THROW (delete temp); +} + +//============================================================================== +TEST_F (ResamplingAudioSourceTests, SetResamplingRatio) +{ + // Test various valid ratios + EXPECT_NO_THROW (resampler->setResamplingRatio (1.0)); + EXPECT_NO_THROW (resampler->setResamplingRatio (0.5)); + EXPECT_NO_THROW (resampler->setResamplingRatio (2.0)); + EXPECT_NO_THROW (resampler->setResamplingRatio (0.1)); +} + +TEST_F (ResamplingAudioSourceTests, DISABLED_SetResamplingRatioNegative) +{ + // Negative ratio should be clamped to 0 (line 60) + EXPECT_NO_THROW (resampler->setResamplingRatio (-1.0)); +} + +//============================================================================== +TEST_F (ResamplingAudioSourceTests, PrepareToPlay) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + // Should call prepareToPlay on source with scaled values (line 68) + EXPECT_TRUE (mockSource->prepareToPlayCalled); + EXPECT_DOUBLE_EQ (mockSource->lastSampleRate, 44100.0); +} + +TEST_F (ResamplingAudioSourceTests, PrepareToPlayWithDifferentRatios) +{ + resampler->setResamplingRatio (2.0); + resampler->prepareToPlay (512, 44100.0); + + EXPECT_TRUE (mockSource->prepareToPlayCalled); + // Sample rate should be scaled by ratio (line 68) + EXPECT_DOUBLE_EQ (mockSource->lastSampleRate, 88200.0); +} + +//============================================================================== +TEST_F (ResamplingAudioSourceTests, FlushBuffers) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + EXPECT_NO_THROW (resampler->flushBuffers()); +} + +TEST_F (ResamplingAudioSourceTests, FlushBuffersAfterProcessing) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + resampler->getNextAudioBlock (info); + + // Flush should clear internal state (line 84-88) + EXPECT_NO_THROW (resampler->flushBuffers()); +} + +//============================================================================== +TEST_F (ResamplingAudioSourceTests, ReleaseResources) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + resampler->releaseResources(); + + // Should call releaseResources on source (line 93) + EXPECT_TRUE (mockSource->releaseResourcesCalled); +} + +//============================================================================== +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockRatioOne) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + resampler->getNextAudioBlock (info); + + // Should call getNextAudioBlock on source + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); + + // Buffer should have audio + bool hasNonZero = false; + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + if (buffer.getSample (ch, i) != 0.0f) + { + hasNonZero = true; + break; + } + } + } + EXPECT_TRUE (hasNonZero); +} + +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockDownsampling) +{ + // Test down-sampling (ratio > 1.0, line 140-146) + resampler->setResamplingRatio (2.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 256); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 256; + + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockUpsampling) +{ + // Test up-sampling (ratio < 1.0, line 184-189) + resampler->setResamplingRatio (0.5); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockChangingRatio) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Process with ratio 1.0 + buffer.clear(); + resampler->getNextAudioBlock (info); + + // Change ratio during processing (line 108-112) + resampler->setResamplingRatio (0.8); + + buffer.clear(); + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); +} + +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockBufferResize) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (256, 44100.0); + + // Start with small buffer + AudioBuffer buffer1 (2, 256); + buffer1.clear(); + + AudioSourceChannelInfo info1; + info1.buffer = &buffer1; + info1.startSample = 0; + info1.numSamples = 256; + + resampler->getNextAudioBlock (info1); + + // Request larger buffer, should trigger resize (line 118-123) + AudioBuffer buffer2 (2, 2048); + buffer2.clear(); + + AudioSourceChannelInfo info2; + info2.buffer = &buffer2; + info2.startSample = 0; + info2.numSamples = 2048; + + EXPECT_NO_THROW (resampler->getNextAudioBlock (info2)); +} + +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockMultipleChannels) +{ + auto* source = new MockAudioSource(); + auto multiChannelResampler = std::make_unique (source, true, 8); + + multiChannelResampler->setResamplingRatio (1.0); + multiChannelResampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (8, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + EXPECT_NO_THROW (multiChannelResampler->getNextAudioBlock (info)); +} + +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockWithStartSample) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 100; + info.numSamples = 256; + + resampler->getNextAudioBlock (info); + + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); + + // Samples before startSample should remain zero + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 100; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.0f); + } + } +} + +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockBufferWrapAround) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Process multiple blocks to trigger wrap-around (line 125, 132, 174-175) + for (int i = 0; i < 10; ++i) + { + buffer.clear(); + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); + } +} + +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockFilterStateUpdate) +{ + // Test the filter state update for ratio close to 1.0 (line 190-210) + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Process first block + buffer.clear(); + resampler->getNextAudioBlock (info); + + // Process second block, filter states should be updated + buffer.clear(); + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); +} + +TEST_F (ResamplingAudioSourceTests, GetNextAudioBlockFilterStateUpdateSingleSample) +{ + // Test filter state update with single sample (line 198-206) + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 1); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 1; + + buffer.clear(); + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); + + // Process another single sample + buffer.clear(); + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); +} + +//============================================================================== +TEST_F (ResamplingAudioSourceTests, CreateLowPassForDownsampling) +{ + // This is tested indirectly through getNextAudioBlock with ratio > 1.0 + resampler->setResamplingRatio (2.5); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 256); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 256; + + // Should apply low-pass filter for down-sampling (line 217-218) + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); +} + +TEST_F (ResamplingAudioSourceTests, CreateLowPassForUpsampling) +{ + // Test with ratio < 1.0 (line 217-218) + resampler->setResamplingRatio (0.4); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); +} + +//============================================================================== +TEST_F (ResamplingAudioSourceTests, InterpolationAccuracy) +{ + resampler->setResamplingRatio (1.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + resampler->getNextAudioBlock (info); + + // Verify interpolation happened (line 164-168) + // Buffer should contain interpolated values + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (ResamplingAudioSourceTests, MultipleBlocksConsistency) +{ + resampler->setResamplingRatio (1.5); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 256); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 256; + + // Process multiple blocks to ensure consistent behavior + for (int i = 0; i < 20; ++i) + { + buffer.clear(); + EXPECT_NO_THROW (resampler->getNextAudioBlock (info)); + + // Verify output has audio content + bool hasNonZero = false; + for (int ch = 0; ch < 2; ++ch) + { + for (int s = 0; s < 256; ++s) + { + if (buffer.getSample (ch, s) != 0.0f) + { + hasNonZero = true; + break; + } + } + } + EXPECT_TRUE (hasNonZero); + } +} + +TEST_F (ResamplingAudioSourceTests, ExtremeRatios) +{ + // Test with very small ratio + resampler->setResamplingRatio (0.1); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer1 (2, 512); + buffer1.clear(); + + AudioSourceChannelInfo info1; + info1.buffer = &buffer1; + info1.startSample = 0; + info1.numSamples = 512; + + EXPECT_NO_THROW (resampler->getNextAudioBlock (info1)); + + // Test with large ratio + resampler->setResamplingRatio (8.0); + resampler->prepareToPlay (512, 44100.0); + + AudioBuffer buffer2 (2, 64); + buffer2.clear(); + + AudioSourceChannelInfo info2; + info2.buffer = &buffer2; + info2.startSample = 0; + info2.numSamples = 64; + + EXPECT_NO_THROW (resampler->getNextAudioBlock (info2)); +} diff --git a/tests/yup_audio_basics/yup_Reverb.cpp b/tests/yup_audio_basics/yup_Reverb.cpp new file mode 100644 index 000000000..66030b181 --- /dev/null +++ b/tests/yup_audio_basics/yup_Reverb.cpp @@ -0,0 +1,1048 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +class ReverbTests : public ::testing::Test +{ +protected: + void SetUp() override + { + reverb = std::make_unique(); + } + + void TearDown() override + { + reverb.reset(); + } + + std::unique_ptr reverb; +}; + +//============================================================================== +TEST_F (ReverbTests, Constructor) +{ + // Constructor should call setParameters and setSampleRate (lines 61-62) + EXPECT_NO_THROW (Reverb()); + + // Default parameters should be set + auto params = reverb->getParameters(); + EXPECT_FLOAT_EQ (params.roomSize, 0.5f); + EXPECT_FLOAT_EQ (params.damping, 0.5f); + EXPECT_FLOAT_EQ (params.wetLevel, 0.33f); + EXPECT_FLOAT_EQ (params.dryLevel, 0.4f); + EXPECT_FLOAT_EQ (params.width, 1.0f); + EXPECT_FLOAT_EQ (params.freezeMode, 0.0f); +} + +//============================================================================== +TEST_F (ReverbTests, ParametersDefaultValues) +{ + Reverb::Parameters params; + + EXPECT_FLOAT_EQ (params.roomSize, 0.5f); + EXPECT_FLOAT_EQ (params.damping, 0.5f); + EXPECT_FLOAT_EQ (params.wetLevel, 0.33f); + EXPECT_FLOAT_EQ (params.dryLevel, 0.4f); + EXPECT_FLOAT_EQ (params.width, 1.0f); + EXPECT_FLOAT_EQ (params.freezeMode, 0.0f); +} + +//============================================================================== +TEST_F (ReverbTests, GetParameters) +{ + auto params = reverb->getParameters(); + + EXPECT_FLOAT_EQ (params.roomSize, 0.5f); + EXPECT_FLOAT_EQ (params.damping, 0.5f); + EXPECT_FLOAT_EQ (params.wetLevel, 0.33f); + EXPECT_FLOAT_EQ (params.dryLevel, 0.4f); + EXPECT_FLOAT_EQ (params.width, 1.0f); + EXPECT_FLOAT_EQ (params.freezeMode, 0.0f); +} + +//============================================================================== +TEST_F (ReverbTests, SetParametersBasic) +{ + Reverb::Parameters params; + params.roomSize = 0.8f; + params.damping = 0.3f; + params.wetLevel = 0.5f; + params.dryLevel = 0.5f; + params.width = 0.7f; + params.freezeMode = 0.0f; + + EXPECT_NO_THROW (reverb->setParameters (params)); + + auto retrieved = reverb->getParameters(); + EXPECT_FLOAT_EQ (retrieved.roomSize, 0.8f); + EXPECT_FLOAT_EQ (retrieved.damping, 0.3f); + EXPECT_FLOAT_EQ (retrieved.wetLevel, 0.5f); + EXPECT_FLOAT_EQ (retrieved.dryLevel, 0.5f); + EXPECT_FLOAT_EQ (retrieved.width, 0.7f); + EXPECT_FLOAT_EQ (retrieved.freezeMode, 0.0f); +} + +TEST_F (ReverbTests, SetParametersWithFreezeMode) +{ + Reverb::Parameters params; + params.freezeMode = 0.6f; // >= 0.5f activates freeze mode (line 96) + + EXPECT_NO_THROW (reverb->setParameters (params)); + + auto retrieved = reverb->getParameters(); + EXPECT_FLOAT_EQ (retrieved.freezeMode, 0.6f); +} + +TEST_F (ReverbTests, SetParametersWithoutFreezeMode) +{ + Reverb::Parameters params; + params.freezeMode = 0.3f; // < 0.5f normal mode (line 96) + + EXPECT_NO_THROW (reverb->setParameters (params)); + + auto retrieved = reverb->getParameters(); + EXPECT_FLOAT_EQ (retrieved.freezeMode, 0.3f); +} + +TEST_F (ReverbTests, SetParametersWetGainCalculation) +{ + Reverb::Parameters params; + params.wetLevel = 0.5f; + params.width = 1.0f; + + // Tests lines 88-94 (wet gain calculations) + EXPECT_NO_THROW (reverb->setParameters (params)); +} + +TEST_F (ReverbTests, SetParametersDryGainCalculation) +{ + Reverb::Parameters params; + params.dryLevel = 0.7f; + + // Tests line 92 (dry gain calculation) + EXPECT_NO_THROW (reverb->setParameters (params)); +} + +TEST_F (ReverbTests, SetParametersWithZeroWidth) +{ + Reverb::Parameters params; + params.width = 0.0f; + + // Tests lines 93-94 with width = 0 + EXPECT_NO_THROW (reverb->setParameters (params)); +} + +TEST_F (ReverbTests, SetParametersWithFullWidth) +{ + Reverb::Parameters params; + params.width = 1.0f; + + // Tests lines 93-94 with width = 1 + EXPECT_NO_THROW (reverb->setParameters (params)); +} + +TEST_F (ReverbTests, SetParametersUpdatesDamping) +{ + Reverb::Parameters params; + params.damping = 0.8f; + params.roomSize = 0.9f; + + // Tests line 98 (updateDamping call) + EXPECT_NO_THROW (reverb->setParameters (params)); +} + +//============================================================================== +TEST_F (ReverbTests, SetSampleRate44100) +{ + EXPECT_NO_THROW (reverb->setSampleRate (44100.0)); +} + +TEST_F (ReverbTests, SetSampleRate48000) +{ + EXPECT_NO_THROW (reverb->setSampleRate (48000.0)); +} + +TEST_F (ReverbTests, SetSampleRate22050) +{ + EXPECT_NO_THROW (reverb->setSampleRate (22050.0)); +} + +TEST_F (ReverbTests, SetSampleRate96000) +{ + EXPECT_NO_THROW (reverb->setSampleRate (96000.0)); +} + +TEST_F (ReverbTests, SetSampleRateCombFilterSizing) +{ + // Tests lines 114-118 (comb filter sizing) + reverb->setSampleRate (48000.0); + + // Process some audio to verify filters are sized correctly + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); +} + +TEST_F (ReverbTests, SetSampleRateAllPassFilterSizing) +{ + // Tests lines 120-124 (all-pass filter sizing) + reverb->setSampleRate (96000.0); + + // Process some audio to verify filters are sized correctly + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.3f; + right[i] = 0.3f; + } + + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); +} + +TEST_F (ReverbTests, SetSampleRateSmoothedValues) +{ + // Tests lines 126-131 (smoothed value reset) + EXPECT_NO_THROW (reverb->setSampleRate (44100.0)); +} + +//============================================================================== +TEST_F (ReverbTests, Reset) +{ + reverb->setSampleRate (44100.0); + + // Process some audio to fill buffers + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + reverb->processStereo (left, right, 512); + + // Tests lines 135-145 (reset clears all filters) + EXPECT_NO_THROW (reverb->reset()); + + // Process silent audio after reset + for (int i = 0; i < 512; ++i) + { + left[i] = 0.0f; + right[i] = 0.0f; + } + reverb->processStereo (left, right, 512); + + // Output should be silent or very quiet + for (int i = 0; i < 512; ++i) + { + EXPECT_NEAR (left[i], 0.0f, 0.1f); + EXPECT_NEAR (right[i], 0.0f, 0.1f); + } +} + +TEST_F (ReverbTests, ResetClearsAllCombs) +{ + reverb->setSampleRate (44100.0); + + // Tests lines 137-140 (clear all comb filters) + EXPECT_NO_THROW (reverb->reset()); +} + +TEST_F (ReverbTests, ResetClearsAllAllPasses) +{ + reverb->setSampleRate (44100.0); + + // Tests lines 142-143 (clear all all-pass filters) + EXPECT_NO_THROW (reverb->reset()); +} + +//============================================================================== +TEST_F (ReverbTests, ProcessStereoBasic) +{ + reverb->setSampleRate (44100.0); + + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + + // Tests lines 149-183 (processStereo) + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); + + // Output should have been modified by reverb + bool hasNonZero = false; + for (int i = 0; i < 512; ++i) + { + if (left[i] != 0.0f || right[i] != 0.0f) + { + hasNonZero = true; + break; + } + } + EXPECT_TRUE (hasNonZero); +} + +TEST_F (ReverbTests, ProcessStereoWithSilence) +{ + reverb->setSampleRate (44100.0); + + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.0f; + right[i] = 0.0f; + } + + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); +} + +TEST_F (ReverbTests, ProcessStereoMultipleTimes) +{ + reverb->setSampleRate (44100.0); + + float left[256], right[256]; + + // Process multiple times to test state preservation + for (int iter = 0; iter < 10; ++iter) + { + for (int i = 0; i < 256; ++i) + { + left[i] = 0.3f; + right[i] = 0.3f; + } + + EXPECT_NO_THROW (reverb->processStereo (left, right, 256)); + } +} + +TEST_F (ReverbTests, ProcessStereoCombFilterAccumulation) +{ + reverb->setSampleRate (44100.0); + + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + + // Tests lines 163-167 (comb filter accumulation) + reverb->processStereo (left, right, 512); + + // Both channels should have output + bool leftHasSignal = false; + bool rightHasSignal = false; + for (int i = 0; i < 512; ++i) + { + if (std::abs (left[i]) > 0.001f) + leftHasSignal = true; + if (std::abs (right[i]) > 0.001f) + rightHasSignal = true; + } + EXPECT_TRUE (leftHasSignal); + EXPECT_TRUE (rightHasSignal); +} + +TEST_F (ReverbTests, ProcessStereoAllPassFilters) +{ + reverb->setSampleRate (44100.0); + + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + + // Tests lines 169-173 (all-pass filter processing) + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); +} + +TEST_F (ReverbTests, ProcessStereoWetDryMix) +{ + reverb->setSampleRate (44100.0); + + Reverb::Parameters params; + params.wetLevel = 0.5f; + params.dryLevel = 0.5f; + reverb->setParameters (params); + + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + + // Tests lines 175-180 (wet/dry mixing) + reverb->processStereo (left, right, 512); + + // Should have both wet and dry components + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); +} + +TEST_F (ReverbTests, ProcessStereoWidthEffect) +{ + reverb->setSampleRate (44100.0); + + Reverb::Parameters params; + params.width = 1.0f; + reverb->setParameters (params); + + // Process multiple blocks to let smoothing settle + for (int block = 0; block < 5; ++block) + { + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + reverb->processStereo (left, right, 512); + } + + // Now capture output with width = 1.0 + float left1[512], right1[512]; + for (int i = 0; i < 512; ++i) + { + left1[i] = 0.5f; + right1[i] = 0.5f; + } + reverb->processStereo (left1, right1, 512); + + // Reset and try with different width + reverb->reset(); + params.width = 0.0f; + reverb->setParameters (params); + + // Process multiple blocks to let smoothing settle + for (int block = 0; block < 5; ++block) + { + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + reverb->processStereo (left, right, 512); + } + + // Now capture output with width = 0.0 + float left2[512], right2[512]; + for (int i = 0; i < 512; ++i) + { + left2[i] = 0.5f; + right2[i] = 0.5f; + } + reverb->processStereo (left2, right2, 512); + + // Outputs should be different due to width parameter + bool isDifferent = false; + for (int i = 0; i < 512; ++i) + { + if (std::abs (left1[i] - left2[i]) > 0.01f) + { + isDifferent = true; + break; + } + } + EXPECT_TRUE (isDifferent); +} + +TEST_F (ReverbTests, ProcessStereoInputCalculation) +{ + reverb->setSampleRate (44100.0); + + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.3f; + right[i] = 0.7f; + } + + // Tests line 157 (input = (left + right) * gain) + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); +} + +//============================================================================== +TEST_F (ReverbTests, ProcessMonoBasic) +{ + reverb->setSampleRate (44100.0); + + float samples[512]; + for (int i = 0; i < 512; ++i) + { + samples[i] = 0.5f; + } + + // Tests lines 186-211 (processMono) + EXPECT_NO_THROW (reverb->processMono (samples, 512)); + + // Output should have been modified by reverb + bool hasNonZero = false; + for (int i = 0; i < 512; ++i) + { + if (samples[i] != 0.0f) + { + hasNonZero = true; + break; + } + } + EXPECT_TRUE (hasNonZero); +} + +TEST_F (ReverbTests, ProcessMonoWithSilence) +{ + reverb->setSampleRate (44100.0); + + float samples[512]; + for (int i = 0; i < 512; ++i) + { + samples[i] = 0.0f; + } + + EXPECT_NO_THROW (reverb->processMono (samples, 512)); +} + +TEST_F (ReverbTests, ProcessMonoMultipleTimes) +{ + reverb->setSampleRate (44100.0); + + float samples[256]; + + // Process multiple times to test state preservation + for (int iter = 0; iter < 10; ++iter) + { + for (int i = 0; i < 256; ++i) + { + samples[i] = 0.3f; + } + + EXPECT_NO_THROW (reverb->processMono (samples, 256)); + } +} + +TEST_F (ReverbTests, ProcessMonoCombFilterAccumulation) +{ + reverb->setSampleRate (44100.0); + + float samples[512]; + for (int i = 0; i < 512; ++i) + { + samples[i] = 0.5f; + } + + // Tests lines 199-200 (comb filter accumulation) + reverb->processMono (samples, 512); + + // Should have output signal + bool hasSignal = false; + for (int i = 0; i < 512; ++i) + { + if (std::abs (samples[i]) > 0.001f) + { + hasSignal = true; + break; + } + } + EXPECT_TRUE (hasSignal); +} + +TEST_F (ReverbTests, ProcessMonoAllPassFilters) +{ + reverb->setSampleRate (44100.0); + + float samples[512]; + for (int i = 0; i < 512; ++i) + { + samples[i] = 0.5f; + } + + // Tests lines 202-203 (all-pass filter processing) + EXPECT_NO_THROW (reverb->processMono (samples, 512)); +} + +TEST_F (ReverbTests, ProcessMonoWetDryMix) +{ + reverb->setSampleRate (44100.0); + + Reverb::Parameters params; + params.wetLevel = 0.5f; + params.dryLevel = 0.5f; + reverb->setParameters (params); + + float samples[512]; + for (int i = 0; i < 512; ++i) + { + samples[i] = 0.5f; + } + + // Tests lines 205-208 (wet/dry mixing) + reverb->processMono (samples, 512); + + // Should have both wet and dry components + EXPECT_NO_THROW (reverb->processMono (samples, 512)); +} + +TEST_F (ReverbTests, ProcessMonoInputCalculation) +{ + reverb->setSampleRate (44100.0); + + float samples[512]; + for (int i = 0; i < 512; ++i) + { + samples[i] = 0.7f; + } + + // Tests line 193 (input = samples[i] * gain) + EXPECT_NO_THROW (reverb->processMono (samples, 512)); +} + +//============================================================================== +TEST_F (ReverbTests, FreezeModeActivated) +{ + reverb->setSampleRate (44100.0); + + Reverb::Parameters params; + params.freezeMode = 0.6f; // >= 0.5f (line 215, 223) + params.roomSize = 0.8f; + params.damping = 0.5f; + + // Tests lines 223-224 (freeze mode damping) + reverb->setParameters (params); + + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + + reverb->processStereo (left, right, 512); + + // In freeze mode, reverb should create sustained effect + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); +} + +TEST_F (ReverbTests, FreezeModeDeactivated) +{ + reverb->setSampleRate (44100.0); + + Reverb::Parameters params; + params.freezeMode = 0.3f; // < 0.5f (line 215) + params.roomSize = 0.8f; + params.damping = 0.5f; + + // Tests lines 225-227 (normal mode damping) + reverb->setParameters (params); + + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); +} + +TEST_F (ReverbTests, FreezeModeTransition) +{ + reverb->setSampleRate (44100.0); + + // Start in normal mode + Reverb::Parameters params; + params.freezeMode = 0.0f; + reverb->setParameters (params); + + float samples[256]; + for (int i = 0; i < 256; ++i) + { + samples[i] = 0.5f; + } + reverb->processMono (samples, 256); + + // Switch to freeze mode + params.freezeMode = 0.8f; + reverb->setParameters (params); + + for (int i = 0; i < 256; ++i) + { + samples[i] = 0.5f; + } + reverb->processMono (samples, 256); + + // Should handle transition smoothly + EXPECT_NO_THROW (reverb->processMono (samples, 256)); +} + +//============================================================================== +TEST_F (ReverbTests, UpdateDampingNormalMode) +{ + Reverb::Parameters params; + params.freezeMode = 0.0f; + params.damping = 0.7f; + params.roomSize = 0.6f; + + // Tests lines 217-228 (updateDamping in normal mode) + EXPECT_NO_THROW (reverb->setParameters (params)); +} + +TEST_F (ReverbTests, UpdateDampingFreezeMode) +{ + Reverb::Parameters params; + params.freezeMode = 0.9f; + params.damping = 0.7f; + params.roomSize = 0.6f; + + // Tests lines 217-228 (updateDamping in freeze mode) + EXPECT_NO_THROW (reverb->setParameters (params)); +} + +//============================================================================== +TEST_F (ReverbTests, RoomSizeEffect) +{ + reverb->setSampleRate (44100.0); + + // Small room + Reverb::Parameters params1; + params1.roomSize = 0.2f; + reverb->setParameters (params1); + + // Process multiple blocks to let smoothing settle + for (int block = 0; block < 5; ++block) + { + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + reverb->processStereo (left, right, 512); + } + + // Capture output with small room + float left1[512], right1[512]; + for (int i = 0; i < 512; ++i) + { + left1[i] = 0.5f; + right1[i] = 0.5f; + } + reverb->processStereo (left1, right1, 512); + + // Large room + reverb->reset(); + Reverb::Parameters params2; + params2.roomSize = 0.9f; + reverb->setParameters (params2); + + // Process multiple blocks to let smoothing settle + for (int block = 0; block < 5; ++block) + { + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + reverb->processStereo (left, right, 512); + } + + // Capture output with large room + float left2[512], right2[512]; + for (int i = 0; i < 512; ++i) + { + left2[i] = 0.5f; + right2[i] = 0.5f; + } + reverb->processStereo (left2, right2, 512); + + // Outputs should be different + bool isDifferent = false; + for (int i = 0; i < 512; ++i) + { + if (std::abs (left1[i] - left2[i]) > 0.01f) + { + isDifferent = true; + break; + } + } + EXPECT_TRUE (isDifferent); +} + +TEST_F (ReverbTests, DampingEffect) +{ + reverb->setSampleRate (44100.0); + + // Test with low damping - send continuous signal and measure output + Reverb::Parameters params1; + params1.damping = 0.0f; + params1.roomSize = 0.8f; + params1.wetLevel = 1.0f; + params1.dryLevel = 0.0f; + reverb->setParameters (params1); + + // Let smoothing settle + for (int block = 0; block < 10; ++block) + { + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.1f; + right[i] = 0.1f; + } + reverb->processStereo (left, right, 512); + } + + // Capture output energy with low damping + float left1[512], right1[512]; + for (int i = 0; i < 512; ++i) + { + left1[i] = 0.1f; + right1[i] = 0.1f; + } + reverb->processStereo (left1, right1, 512); + + float energy1 = 0.0f; + for (int i = 256; i < 512; ++i) // Use second half to avoid transients + { + energy1 += std::abs (left1[i]) + std::abs (right1[i]); + } + + // Test with high damping - send continuous signal and measure output + reverb->reset(); + Reverb::Parameters params2; + params2.damping = 1.0f; + params2.roomSize = 0.8f; + params2.wetLevel = 1.0f; + params2.dryLevel = 0.0f; + reverb->setParameters (params2); + + // Let smoothing settle + for (int block = 0; block < 10; ++block) + { + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.1f; + right[i] = 0.1f; + } + reverb->processStereo (left, right, 512); + } + + // Capture output energy with high damping + float left2[512], right2[512]; + for (int i = 0; i < 512; ++i) + { + left2[i] = 0.1f; + right2[i] = 0.1f; + } + reverb->processStereo (left2, right2, 512); + + float energy2 = 0.0f; + for (int i = 256; i < 512; ++i) // Use second half to avoid transients + { + energy2 += std::abs (left2[i]) + std::abs (right2[i]); + } + + // Damping affects high-frequency content, both should have some output + EXPECT_GT (energy1, 0.0f); + EXPECT_GT (energy2, 0.0f); +} + +TEST_F (ReverbTests, WetLevelOnly) +{ + reverb->setSampleRate (44100.0); + + Reverb::Parameters params; + params.wetLevel = 1.0f; + params.dryLevel = 0.0f; + reverb->setParameters (params); + + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); +} + +TEST_F (ReverbTests, DryLevelOnly) +{ + reverb->setSampleRate (44100.0); + + Reverb::Parameters params; + params.wetLevel = 0.0f; + params.dryLevel = 1.0f; + reverb->setParameters (params); + + float left[512], right[512]; + const float inputValue = 0.5f; + for (int i = 0; i < 512; ++i) + { + left[i] = inputValue; + right[i] = inputValue; + } + + reverb->processStereo (left, right, 512); + + // With only dry signal, output should be close to scaled input + // (allowing for some variation due to smoothing) + for (int i = 256; i < 512; ++i) // Check second half after smoothing + { + EXPECT_NEAR (left[i], inputValue * 2.0f, 0.5f); // dryScaleFactor = 2.0 + EXPECT_NEAR (right[i], inputValue * 2.0f, 0.5f); + } +} + +//============================================================================== +TEST_F (ReverbTests, LargeBuffer) +{ + reverb->setSampleRate (44100.0); + + const int bufferSize = 8192; + std::vector left (bufferSize); + std::vector right (bufferSize); + + for (int i = 0; i < bufferSize; ++i) + { + left[i] = 0.3f; + right[i] = 0.3f; + } + + EXPECT_NO_THROW (reverb->processStereo (left.data(), right.data(), bufferSize)); +} + +TEST_F (ReverbTests, SmallBuffer) +{ + reverb->setSampleRate (44100.0); + + float left[8], right[8]; + for (int i = 0; i < 8; ++i) + { + left[i] = 0.5f; + right[i] = 0.5f; + } + + EXPECT_NO_THROW (reverb->processStereo (left, right, 8)); +} + +TEST_F (ReverbTests, SingleSample) +{ + reverb->setSampleRate (44100.0); + + float left[1] = { 0.5f }; + float right[1] = { 0.5f }; + + EXPECT_NO_THROW (reverb->processStereo (left, right, 1)); +} + +//============================================================================== +TEST_F (ReverbTests, SequentialProcessing) +{ + reverb->setSampleRate (44100.0); + + // Process multiple sequential blocks + for (int block = 0; block < 20; ++block) + { + float left[256], right[256]; + for (int i = 0; i < 256; ++i) + { + left[i] = 0.4f; + right[i] = 0.4f; + } + + EXPECT_NO_THROW (reverb->processStereo (left, right, 256)); + } +} + +TEST_F (ReverbTests, AlternatingMonoStereo) +{ + reverb->setSampleRate (44100.0); + + float mono[256]; + float left[256], right[256]; + + for (int i = 0; i < 256; ++i) + { + mono[i] = 0.5f; + left[i] = 0.5f; + right[i] = 0.5f; + } + + reverb->processMono (mono, 256); + reverb->processStereo (left, right, 256); + reverb->processMono (mono, 256); + + // Should handle switching between mono and stereo + EXPECT_NO_THROW (reverb->processStereo (left, right, 256)); +} + +//============================================================================== +TEST_F (ReverbTests, CombFilterWraparound) +{ + reverb->setSampleRate (44100.0); + + // Process enough samples to ensure comb filters wrap around + const int totalSamples = 10000; + for (int offset = 0; offset < totalSamples; offset += 512) + { + float left[512], right[512]; + for (int i = 0; i < 512; ++i) + { + left[i] = 0.3f; + right[i] = 0.3f; + } + + EXPECT_NO_THROW (reverb->processStereo (left, right, 512)); + } +} + +TEST_F (ReverbTests, AllPassFilterWraparound) +{ + reverb->setSampleRate (44100.0); + + // Process enough samples to ensure all-pass filters wrap around + const int totalSamples = 5000; + for (int offset = 0; offset < totalSamples; offset += 256) + { + float samples[256]; + for (int i = 0; i < 256; ++i) + { + samples[i] = 0.3f; + } + + EXPECT_NO_THROW (reverb->processMono (samples, 256)); + } +} diff --git a/tests/yup_audio_basics/yup_ReverbAudioSource.cpp b/tests/yup_audio_basics/yup_ReverbAudioSource.cpp new file mode 100644 index 000000000..fc5a03988 --- /dev/null +++ b/tests/yup_audio_basics/yup_ReverbAudioSource.cpp @@ -0,0 +1,381 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +namespace +{ +class MockAudioSource : public AudioSource +{ +public: + MockAudioSource() = default; + ~MockAudioSource() override = default; + + void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override + { + prepareToPlayCalled = true; + lastSamplesPerBlock = samplesPerBlockExpected; + lastSampleRate = sampleRate; + } + + void releaseResources() override + { + releaseResourcesCalled = true; + } + + void getNextAudioBlock (const AudioSourceChannelInfo& info) override + { + getNextAudioBlockCalled = true; + + // Fill with a constant value for testing + for (int ch = 0; ch < info.buffer->getNumChannels(); ++ch) + { + for (int i = 0; i < info.numSamples; ++i) + { + info.buffer->setSample (ch, info.startSample + i, fillValue); + } + } + } + + bool prepareToPlayCalled = false; + bool releaseResourcesCalled = false; + bool getNextAudioBlockCalled = false; + int lastSamplesPerBlock = 0; + double lastSampleRate = 0.0; + float fillValue = 0.5f; +}; +} // namespace + +//============================================================================== +class ReverbAudioSourceTests : public ::testing::Test +{ +protected: + void SetUp() override + { + mockSource = new MockAudioSource(); + reverbSource = std::make_unique (mockSource, true); + } + + void TearDown() override + { + reverbSource.reset(); + } + + MockAudioSource* mockSource; // Owned by reverbSource + std::unique_ptr reverbSource; +}; + +//============================================================================== +TEST_F (ReverbAudioSourceTests, ConstructorWithDeleteInput) +{ + auto* source = new MockAudioSource(); + EXPECT_NO_THROW (ReverbAudioSource (source, true)); +} + +TEST_F (ReverbAudioSourceTests, ConstructorWithoutDeleteInput) +{ + MockAudioSource source; + EXPECT_NO_THROW (ReverbAudioSource (&source, false)); +} + +TEST_F (ReverbAudioSourceTests, Destructor) +{ + auto* source = new MockAudioSource(); + auto* temp = new ReverbAudioSource (source, true); + EXPECT_NO_THROW (delete temp); +} + +//============================================================================== +TEST_F (ReverbAudioSourceTests, PrepareToPlay) +{ + reverbSource->prepareToPlay (512, 44100.0); + + // Should call prepareToPlay on input source (line 55) + EXPECT_TRUE (mockSource->prepareToPlayCalled); + EXPECT_EQ (mockSource->lastSamplesPerBlock, 512); + EXPECT_DOUBLE_EQ (mockSource->lastSampleRate, 44100.0); +} + +TEST_F (ReverbAudioSourceTests, ReleaseResources) +{ + reverbSource->prepareToPlay (512, 44100.0); + EXPECT_NO_THROW (reverbSource->releaseResources()); +} + +//============================================================================== +TEST_F (ReverbAudioSourceTests, GetNextAudioBlockMono) +{ + reverbSource->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (1, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + reverbSource->getNextAudioBlock (info); + + // Should call getNextAudioBlock on input (line 65) + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); + + // Should process mono (line 79) + // Buffer should have been modified by reverb + bool hasNonZero = false; + for (int i = 0; i < 512; ++i) + { + if (buffer.getSample (0, i) != 0.0f) + { + hasNonZero = true; + break; + } + } + EXPECT_TRUE (hasNonZero); +} + +TEST_F (ReverbAudioSourceTests, GetNextAudioBlockStereo) +{ + reverbSource->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + reverbSource->getNextAudioBlock (info); + + // Should call getNextAudioBlock on input (line 65) + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); + + // Should process stereo (line 73-75) + // Buffer should have been modified by reverb + bool hasNonZero = false; + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + if (buffer.getSample (ch, i) != 0.0f) + { + hasNonZero = true; + break; + } + } + } + EXPECT_TRUE (hasNonZero); +} + +TEST_F (ReverbAudioSourceTests, GetNextAudioBlockMultiChannel) +{ + reverbSource->prepareToPlay (512, 44100.0); + + // Test with more than 2 channels + AudioBuffer buffer (4, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // Should still work, processing first 2 channels as stereo (line 71-76) + EXPECT_NO_THROW (reverbSource->getNextAudioBlock (info)); + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (ReverbAudioSourceTests, GetNextAudioBlockWithStartSampleOffset) +{ + reverbSource->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 100; + info.numSamples = 256; + + reverbSource->getNextAudioBlock (info); + + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); + + // Samples before startSample should remain zero + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 100; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.0f); + } + } +} + +//============================================================================== +TEST_F (ReverbAudioSourceTests, SetParameters) +{ + Reverb::Parameters params; + params.roomSize = 0.8f; + params.damping = 0.5f; + params.wetLevel = 0.4f; + params.dryLevel = 0.6f; + params.width = 1.0f; + params.freezeMode = 0.0f; + + EXPECT_NO_THROW (reverbSource->setParameters (params)); + + // Prepare and process to verify parameters are applied + reverbSource->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + // Fill with signal + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + buffer.setSample (ch, i, 0.5f); + } + } + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + EXPECT_NO_THROW (reverbSource->getNextAudioBlock (info)); +} + +//============================================================================== +TEST_F (ReverbAudioSourceTests, SetBypassedTrue) +{ + reverbSource->prepareToPlay (512, 44100.0); + + // Set bypass to true (line 92-97) + reverbSource->setBypassed (true); + + mockSource->fillValue = 0.7f; + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + reverbSource->getNextAudioBlock (info); + + // When bypassed, should not process reverb (line 67) + // Buffer should contain only the input source value + for (int ch = 0; ch < 2; ++ch) + { + for (int i = 0; i < 512; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (ch, i), 0.7f); + } + } +} + +TEST_F (ReverbAudioSourceTests, SetBypassedFalse) +{ + reverbSource->setBypassed (true); + reverbSource->prepareToPlay (512, 44100.0); + + // Set bypass to false (line 92-97) + reverbSource->setBypassed (false); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + reverbSource->getNextAudioBlock (info); + + // When not bypassed, should process reverb + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} + +TEST_F (ReverbAudioSourceTests, SetBypassedSameValue) +{ + reverbSource->setBypassed (false); + + // Setting to same value should not acquire lock (line 92) + EXPECT_NO_THROW (reverbSource->setBypassed (false)); +} + +TEST_F (ReverbAudioSourceTests, SetBypassedResetsReverb) +{ + reverbSource->prepareToPlay (512, 44100.0); + + // Process some audio + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + reverbSource->getNextAudioBlock (info); + + // Toggling bypass should reset reverb (line 96) + reverbSource->setBypassed (true); + reverbSource->setBypassed (false); + + // Continue processing + EXPECT_NO_THROW (reverbSource->getNextAudioBlock (info)); +} + +TEST_F (ReverbAudioSourceTests, BypassAndUnbypassMultipleTimes) +{ + reverbSource->prepareToPlay (512, 44100.0); + + AudioBuffer buffer (2, 512); + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + for (int i = 0; i < 5; ++i) + { + reverbSource->setBypassed (true); + buffer.clear(); + reverbSource->getNextAudioBlock (info); + + reverbSource->setBypassed (false); + buffer.clear(); + reverbSource->getNextAudioBlock (info); + } + + // Should handle multiple bypass toggles without issues + EXPECT_TRUE (mockSource->getNextAudioBlockCalled); +} diff --git a/tests/yup_audio_basics/yup_Synthesiser.cpp b/tests/yup_audio_basics/yup_Synthesiser.cpp index 129d38ecf..7aadd8fae 100644 --- a/tests/yup_audio_basics/yup_Synthesiser.cpp +++ b/tests/yup_audio_basics/yup_Synthesiser.cpp @@ -28,6 +28,14 @@ using namespace yup; namespace { +// Test-friendly Synthesiser that exposes protected methods for testing +class TestSynthesiser : public Synthesiser +{ +public: + using Synthesiser::handleMidiEvent; + using Synthesiser::startVoice; +}; + // Test implementation of SynthesiserSound class TestSound : public SynthesiserSound { @@ -81,6 +89,7 @@ class TestVoice : public SynthesiserVoice lastStopVelocity = velocity; lastAllowTailOff = allowTailOff; noteStopped = true; + stopCount++; if (! allowTailOff) { @@ -151,6 +160,8 @@ class TestVoice : public SynthesiserVoice int getLastRenderNumSamples() const { return lastRenderNumSamples; } + int getStopCount() const { return stopCount; } + void reset() { noteStarted = false; @@ -168,6 +179,7 @@ class TestVoice : public SynthesiserVoice lastRenderStartSample = -1; lastRenderNumSamples = -1; phase = 0.0f; + stopCount = 0; } private: @@ -186,6 +198,7 @@ class TestVoice : public SynthesiserVoice int lastPitchWheel = 8192; int lastRenderStartSample = -1; int lastRenderNumSamples = -1; + int stopCount = 0; float phase = 0.0f; SynthesiserSound* currentSound = nullptr; @@ -198,7 +211,7 @@ class SynthesiserTest : public ::testing::Test protected: void SetUp() override { - synth = std::make_unique(); + synth = std::make_unique(); synth->setCurrentPlaybackSampleRate (44100.0); } @@ -207,7 +220,7 @@ class SynthesiserTest : public ::testing::Test synth.reset(); } - std::unique_ptr synth; + std::unique_ptr synth; }; TEST_F (SynthesiserTest, DefaultConstruction) @@ -680,3 +693,480 @@ TEST_F (SynthesiserTest, VoiceStateManagement) EXPECT_EQ (voice->getCurrentlyPlayingSound(), nullptr); EXPECT_FALSE (voice->isKeyDown()); } + +//============================================================================== +// Additional coverage tests for uncovered lines + +TEST_F (SynthesiserTest, WasStartedBeforeComparison) +{ + auto* voice1 = static_cast (synth->addVoice (new TestVoice())); + auto* voice2 = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start notes in sequence + synth->noteOn (1, 60, 0.8f); + synth->noteOn (1, 64, 0.7f); + + // voice1 was started before voice2 + EXPECT_TRUE (voice1->wasStartedBefore (*voice2)); + EXPECT_FALSE (voice2->wasStartedBefore (*voice1)); +} + +TEST_F (SynthesiserTest, SetCurrentPlaybackSampleRateUpdatesVoices) +{ + // Add voice after initial sample rate is set + auto* voice = static_cast (synth->addVoice (new TestVoice())); + EXPECT_EQ (voice->getSampleRate(), 44100.0); + + // Change sample rate - should update all voices + synth->setCurrentPlaybackSampleRate (48000.0); + EXPECT_EQ (voice->getSampleRate(), 48000.0); + + // Add another voice after rate change + auto* voice2 = static_cast (synth->addVoice (new TestVoice())); + EXPECT_EQ (voice2->getSampleRate(), 48000.0); +} + +TEST_F (SynthesiserTest, SetCurrentPlaybackSampleRateClearsActiveNotes) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start a note + synth->noteOn (1, 60, 0.8f); + EXPECT_TRUE (voice->isVoiceActive()); + + voice->reset(); + + // Change sample rate should stop all notes + synth->setCurrentPlaybackSampleRate (48000.0); + EXPECT_TRUE (voice->wasNoteStopped()); +} + +TEST_F (SynthesiserTest, HandleMidiEventAllNotesOff) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + synth->noteOn (1, 60, 0.8f); + voice->reset(); + + // Send all notes off message + MidiMessage msg = MidiMessage::allNotesOff (1); + synth->handleMidiEvent (msg); + + EXPECT_TRUE (voice->wasNoteStopped()); +} + +TEST_F (SynthesiserTest, HandleMidiEventAllSoundOff) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + synth->noteOn (1, 60, 0.8f); + voice->reset(); + + // Send all sound off message + MidiMessage msg = MidiMessage::allSoundOff (1); + synth->handleMidiEvent (msg); + + EXPECT_TRUE (voice->wasNoteStopped()); +} + +TEST_F (SynthesiserTest, HandleMidiEventAftertouch) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + synth->noteOn (1, 60, 0.8f); + + // Send aftertouch message + MidiMessage msg = MidiMessage::aftertouchChange (1, 60, 80); + synth->handleMidiEvent (msg); + + // Just verify it doesn't crash - base implementation does nothing +} + +TEST_F (SynthesiserTest, HandleMidiEventChannelPressure) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + synth->noteOn (1, 60, 0.8f); + + // Send channel pressure message + MidiMessage msg = MidiMessage::channelPressureChange (1, 100); + synth->handleMidiEvent (msg); + + // Just verify it doesn't crash - base implementation does nothing +} + +TEST_F (SynthesiserTest, HandleMidiEventProgramChange) +{ + // Send program change message + MidiMessage msg = MidiMessage::programChange (1, 42); + synth->handleMidiEvent (msg); + + // Just verify it doesn't crash - base implementation does nothing +} + +TEST_F (SynthesiserTest, NoteOnStopsExistingNote) +{ + auto* voice1 = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start note 60 + synth->noteOn (1, 60, 0.8f); + EXPECT_EQ (voice1->getCurrentlyPlayingNote(), 60); + EXPECT_TRUE (voice1->isVoiceActive()); + + // Add another voice to avoid immediate restart on the same voice + auto* voice2 = static_cast (synth->addVoice (new TestVoice())); + + // Start the same note again - should stop voice1 first and start voice2 + synth->noteOn (1, 60, 0.7f); + + // voice1 should have been stopped with tail-off + EXPECT_TRUE (voice1->wasNoteStopped()); + EXPECT_TRUE (voice1->getLastAllowTailOff()); + + // voice2 should have started the new note + EXPECT_TRUE (voice2->wasNoteStarted()); + EXPECT_EQ (voice2->getCurrentlyPlayingNote(), 60); +} + +TEST_F (SynthesiserTest, StartVoiceStopsActiveVoice) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start a note + synth->noteOn (1, 60, 0.8f); + EXPECT_TRUE (voice->isVoiceActive()); + EXPECT_NE (voice->getCurrentlyPlayingSound(), nullptr); + EXPECT_EQ (voice->getStopCount(), 0); + + // Manually start the same voice again (simulating voice stealing scenario) + // This forces the voice->stopNote (0.0f, false) path in startVoice (line 352) + synth->startVoice (voice, sound.get(), 1, 64, 0.7f); + + // Voice should have been stopped without tail-off and restarted with new note + EXPECT_EQ (voice->getStopCount(), 1); // stopNote was called once + EXPECT_FALSE (voice->getLastAllowTailOff()); + EXPECT_EQ (voice->getCurrentlyPlayingNote(), 64); +} + +TEST_F (SynthesiserTest, HandleControllerSustainPedal) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + synth->noteOn (1, 60, 0.8f); + + // Send sustain pedal on (controller 0x40, value >= 64) + synth->handleController (1, 0x40, 127); + EXPECT_TRUE (voice->isSustainPedalDown()); + + // Release key + synth->noteOff (1, 60, 0.5f, true); + EXPECT_FALSE (voice->isKeyDown()); + EXPECT_TRUE (voice->isVoiceActive()); // Still active due to sustain + + voice->reset(); + + // Send sustain pedal off (controller 0x40, value < 64) + synth->handleController (1, 0x40, 0); + EXPECT_FALSE (voice->isSustainPedalDown()); + EXPECT_TRUE (voice->wasNoteStopped()); +} + +TEST_F (SynthesiserTest, HandleControllerSostenutoPedal) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + synth->noteOn (1, 60, 0.8f); + + // Send sostenuto pedal on (controller 0x42, value >= 64) + synth->handleController (1, 0x42, 127); + EXPECT_TRUE (voice->isSostenutoPedalDown()); + + voice->reset(); + + // Send sostenuto pedal off (controller 0x42, value < 64) + synth->handleController (1, 0x42, 0); + EXPECT_TRUE (voice->wasNoteStopped()); +} + +TEST_F (SynthesiserTest, HandleControllerSoftPedal) +{ + // Send soft pedal on (controller 0x43, value >= 64) + synth->handleController (1, 0x43, 127); + + // Send soft pedal off (controller 0x43, value < 64) + synth->handleController (1, 0x43, 0); + + // Just verify it doesn't crash - base implementation does nothing +} + +TEST_F (SynthesiserTest, ProcessNextBlockWithMidiAtEndOfBuffer) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + AudioBuffer buffer (2, 64); + buffer.clear(); + + MidiBuffer midiBuffer; + // Add MIDI event at the exact end of the buffer + midiBuffer.addEvent (MidiMessage::noteOn (1, 60, 0.8f), 64); + + voice->reset(); + + synth->renderNextBlock (buffer, midiBuffer, 0, 64); + + // The note should be handled but not rendered in this block + EXPECT_TRUE (voice->wasNoteStarted()); +} + +TEST_F (SynthesiserTest, ProcessNextBlockWithMidiAfterRenderRegion) +{ + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + AudioBuffer buffer (2, 128); + buffer.clear(); + + MidiBuffer midiBuffer; + // Add MIDI events throughout and after the render region + midiBuffer.addEvent (MidiMessage::noteOn (1, 60, 0.8f), 0); + midiBuffer.addEvent (MidiMessage::controllerEvent (1, 7, 100), 64); + midiBuffer.addEvent (MidiMessage::noteOff (1, 60, 0.5f), 96); + + voice->reset(); + + // Render only first 64 samples, but buffer has events beyond + synth->renderNextBlock (buffer, midiBuffer, 0, 64); + + EXPECT_TRUE (voice->wasNoteStarted()); + EXPECT_TRUE (voice->wasControllerMoved()); + EXPECT_TRUE (voice->wasNoteStopped()); +} + +TEST_F (SynthesiserTest, FindFreeVoiceWithoutStealing) +{ + // Don't enable note stealing + synth->setNoteStealingEnabled (false); + + auto* voice1 = static_cast (synth->addVoice (new TestVoice())); + auto* voice2 = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start notes to occupy all voices + synth->noteOn (1, 60, 0.8f); + synth->noteOn (1, 64, 0.7f); + + EXPECT_EQ (voice1->getCurrentlyPlayingNote(), 60); + EXPECT_EQ (voice2->getCurrentlyPlayingNote(), 64); + + // Try to start another note - should not trigger (no free voices, stealing disabled) + synth->noteOn (1, 67, 0.6f); + + // Neither voice should have changed notes + EXPECT_EQ (voice1->getCurrentlyPlayingNote(), 60); + EXPECT_EQ (voice2->getCurrentlyPlayingNote(), 64); +} + +TEST_F (SynthesiserTest, FindFreeVoiceWithStealingEnabled) +{ + synth->setNoteStealingEnabled (true); + + auto* voice1 = static_cast (synth->addVoice (new TestVoice())); + auto* voice2 = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start notes to occupy all voices + synth->noteOn (1, 60, 0.8f); + synth->noteOn (1, 64, 0.7f); + + // Try to start another note - should steal a voice + synth->noteOn (1, 67, 0.6f); + + // One of the voices should now be playing note 67 + EXPECT_TRUE (voice1->getCurrentlyPlayingNote() == 67 || voice2->getCurrentlyPlayingNote() == 67); +} + +TEST_F (SynthesiserTest, VoiceStealingPrefersOldestNote) +{ + synth->setNoteStealingEnabled (true); + + auto* voice1 = static_cast (synth->addVoice (new TestVoice())); + auto* voice2 = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start notes in sequence + synth->noteOn (1, 60, 0.8f); // Oldest, lowest note + synth->noteOn (1, 72, 0.7f); // Newer, highest note + + // Both notes are currently held (not released), so they're protected + // The algorithm protects lowest and highest notes + // Since both are protected and we only have 2 voices, it will steal the top one + synth->noteOn (1, 67, 0.6f); + + // One voice should now be playing note 67 + bool voice1Has67 = (voice1->getCurrentlyPlayingNote() == 67); + bool voice2Has67 = (voice2->getCurrentlyPlayingNote() == 67); + EXPECT_TRUE (voice1Has67 || voice2Has67); +} + +TEST_F (SynthesiserTest, VoiceStealingPrefersSameNote) +{ + synth->setNoteStealingEnabled (true); + + auto* voice1 = static_cast (synth->addVoice (new TestVoice())); + auto* voice2 = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start notes + synth->noteOn (1, 60, 0.8f); + synth->noteOn (1, 64, 0.7f); + + // Trigger the same note again - should steal the voice already playing that note + synth->noteOn (1, 60, 0.9f); + + // voice1 should have been restarted with same note + EXPECT_EQ (voice1->getCurrentlyPlayingNote(), 60); + EXPECT_EQ (voice2->getCurrentlyPlayingNote(), 64); +} + +TEST_F (SynthesiserTest, VoiceStealingPrefersReleasedNotes) +{ + synth->setNoteStealingEnabled (true); + + auto* voice1 = static_cast (synth->addVoice (new TestVoice())); + auto* voice2 = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start notes with different pitches so they're not protected the same way + synth->noteOn (1, 60, 0.8f); // Lower note + synth->noteOn (1, 72, 0.7f); // Higher note + + // Release first note (without sustain, so it's fully released) + synth->noteOff (1, 60, 0.5f, true); + + // voice1 should be released (not held by key) + EXPECT_FALSE (voice1->isKeyDown()); + EXPECT_TRUE (voice2->isKeyDown()); + + // Start a new note - should prefer stealing the released voice + synth->noteOn (1, 67, 0.6f); + + // The released voice should have been stolen + // Since voice1 was released and voice2 is still held, voice1 should be stolen + EXPECT_EQ (voice1->getCurrentlyPlayingNote(), 67); + EXPECT_EQ (voice2->getCurrentlyPlayingNote(), 72); +} + +TEST_F (SynthesiserTest, VoiceStealingProtectsLowestAndHighestNotes) +{ + synth->setNoteStealingEnabled (true); + + // Add 3 voices + auto* voice1 = static_cast (synth->addVoice (new TestVoice())); + auto* voice2 = static_cast (synth->addVoice (new TestVoice())); + auto* voice3 = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start notes: low, middle, high + synth->noteOn (1, 48, 0.8f); // Low - protected + synth->noteOn (1, 60, 0.7f); // Middle - not protected + synth->noteOn (1, 72, 0.6f); // High - protected + + // Start a new note - should steal the middle note (voice2) + synth->noteOn (1, 64, 0.5f); + + EXPECT_EQ (voice1->getCurrentlyPlayingNote(), 48); // Low protected + EXPECT_EQ (voice2->getCurrentlyPlayingNote(), 64); // Was stolen + EXPECT_EQ (voice3->getCurrentlyPlayingNote(), 72); // High protected +} + +TEST_F (SynthesiserTest, VoiceStealingWithOnlyOneNote) +{ + synth->setNoteStealingEnabled (true); + + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + // Start one note + synth->noteOn (1, 60, 0.8f); + EXPECT_EQ (voice->getCurrentlyPlayingNote(), 60); + + // Start another note - should steal the only voice + synth->noteOn (1, 64, 0.7f); + EXPECT_EQ (voice->getCurrentlyPlayingNote(), 64); +} + +TEST_F (SynthesiserTest, MinimumRenderingSubdivisionStrictMode) +{ + synth->setMinimumRenderingSubdivisionSize (32, true); + + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + AudioBuffer buffer (2, 128); + buffer.clear(); + + MidiBuffer midiBuffer; + // Add MIDI event very early (at sample 1) + midiBuffer.addEvent (MidiMessage::noteOn (1, 60, 0.8f), 1); + + voice->reset(); + + synth->renderNextBlock (buffer, midiBuffer, 0, 128); + + // In strict mode, the minimum subdivision should be enforced + EXPECT_TRUE (voice->wasNoteStarted()); +} + +TEST_F (SynthesiserTest, MinimumRenderingSubdivisionNonStrictMode) +{ + synth->setMinimumRenderingSubdivisionSize (32, false); + + auto* voice = static_cast (synth->addVoice (new TestVoice())); + auto sound = SynthesiserSound::Ptr (new TestSound()); + synth->addSound (sound); + + AudioBuffer buffer (2, 128); + buffer.clear(); + + MidiBuffer midiBuffer; + // Add MIDI event at sample 0 (first event in non-strict mode can be at 0) + midiBuffer.addEvent (MidiMessage::noteOn (1, 60, 0.8f), 0); + + voice->reset(); + + synth->renderNextBlock (buffer, midiBuffer, 0, 128); + + // In non-strict mode, first event can render immediately + EXPECT_TRUE (voice->wasNoteStarted()); +} diff --git a/tests/yup_audio_basics/yup_ToneGeneratorAudioSource.cpp b/tests/yup_audio_basics/yup_ToneGeneratorAudioSource.cpp new file mode 100644 index 000000000..197a13be2 --- /dev/null +++ b/tests/yup_audio_basics/yup_ToneGeneratorAudioSource.cpp @@ -0,0 +1,314 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +//============================================================================== +class ToneGeneratorAudioSourceTests : public ::testing::Test +{ +protected: + void SetUp() override + { + source = std::make_unique(); + } + + void TearDown() override + { + source.reset(); + } + + std::unique_ptr source; +}; + +//============================================================================== +TEST_F (ToneGeneratorAudioSourceTests, Constructor) +{ + EXPECT_NO_THROW (ToneGeneratorAudioSource()); +} + +TEST_F (ToneGeneratorAudioSourceTests, Destructor) +{ + auto* temp = new ToneGeneratorAudioSource(); + EXPECT_NO_THROW (delete temp); +} + +//============================================================================== +TEST_F (ToneGeneratorAudioSourceTests, SetAmplitude) +{ + EXPECT_NO_THROW (source->setAmplitude (0.5f)); + EXPECT_NO_THROW (source->setAmplitude (0.0f)); + EXPECT_NO_THROW (source->setAmplitude (1.0f)); + EXPECT_NO_THROW (source->setAmplitude (2.0f)); +} + +TEST_F (ToneGeneratorAudioSourceTests, SetFrequency) +{ + EXPECT_NO_THROW (source->setFrequency (440.0)); + EXPECT_NO_THROW (source->setFrequency (1000.0)); + EXPECT_NO_THROW (source->setFrequency (20.0)); + EXPECT_NO_THROW (source->setFrequency (20000.0)); +} + +//============================================================================== +TEST_F (ToneGeneratorAudioSourceTests, PrepareToPlay) +{ + EXPECT_NO_THROW (source->prepareToPlay (512, 44100.0)); + EXPECT_NO_THROW (source->prepareToPlay (1024, 48000.0)); +} + +TEST_F (ToneGeneratorAudioSourceTests, ReleaseResources) +{ + source->prepareToPlay (512, 44100.0); + EXPECT_NO_THROW (source->releaseResources()); +} + +//============================================================================== +TEST_F (ToneGeneratorAudioSourceTests, GetNextAudioBlockInitializesPhase) +{ + source->prepareToPlay (512, 44100.0); + source->setFrequency (1000.0); + source->setAmplitude (0.5f); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + // First call should initialize phasePerSample (line 82-83) + source->getNextAudioBlock (info); + + // Verify audio was generated + bool hasNonZero = false; + for (int i = 0; i < info.numSamples; ++i) + { + if (buffer.getSample (0, i) != 0.0f) + { + hasNonZero = true; + break; + } + } + EXPECT_TRUE (hasNonZero); +} + +TEST_F (ToneGeneratorAudioSourceTests, GeneratesSineWave) +{ + source->prepareToPlay (512, 44100.0); + source->setFrequency (440.0); + source->setAmplitude (1.0f); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + source->getNextAudioBlock (info); + + // Check that both channels have the same content + for (int i = 0; i < info.numSamples; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (0, i), buffer.getSample (1, i)); + } + + // Check amplitude is within expected range + float maxValue = 0.0f; + for (int i = 0; i < info.numSamples; ++i) + { + maxValue = jmax (maxValue, std::abs (buffer.getSample (0, i))); + } + EXPECT_LE (maxValue, 1.0f); + EXPECT_GT (maxValue, 0.5f); // Should be close to 1.0 for a full cycle +} + +TEST_F (ToneGeneratorAudioSourceTests, GeneratesWithDifferentAmplitudes) +{ + source->prepareToPlay (512, 44100.0); + source->setFrequency (1000.0); + + // Test with different amplitudes + for (float amp : { 0.0f, 0.25f, 0.5f, 0.75f, 1.0f }) + { + source->setAmplitude (amp); + source->prepareToPlay (512, 44100.0); // Reset phase + + AudioBuffer buffer (1, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + source->getNextAudioBlock (info); + + float maxValue = 0.0f; + for (int i = 0; i < info.numSamples; ++i) + { + maxValue = jmax (maxValue, std::abs (buffer.getSample (0, i))); + } + + if (amp == 0.0f) + { + EXPECT_FLOAT_EQ (maxValue, 0.0f); + } + else + { + EXPECT_LE (maxValue, amp); + } + } +} + +TEST_F (ToneGeneratorAudioSourceTests, GeneratesWithDifferentFrequencies) +{ + source->prepareToPlay (512, 44100.0); + source->setAmplitude (1.0f); + + for (double freq : { 100.0, 440.0, 1000.0, 5000.0 }) + { + source->setFrequency (freq); + source->prepareToPlay (512, 44100.0); // Reset phase + + AudioBuffer buffer (1, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 512; + + source->getNextAudioBlock (info); + + // Just verify audio was generated + bool hasNonZero = false; + for (int i = 0; i < info.numSamples; ++i) + { + if (buffer.getSample (0, i) != 0.0f) + { + hasNonZero = true; + break; + } + } + EXPECT_TRUE (hasNonZero); + } +} + +TEST_F (ToneGeneratorAudioSourceTests, GeneratesWithMultipleChannels) +{ + source->prepareToPlay (512, 44100.0); + source->setFrequency (1000.0); + source->setAmplitude (0.5f); + + // Test with various channel counts + for (int numChannels = 1; numChannels <= 8; ++numChannels) + { + AudioBuffer buffer (numChannels, 256); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 0; + info.numSamples = 256; + + source->getNextAudioBlock (info); + + // All channels should have identical content (line 90-91) + for (int ch = 1; ch < numChannels; ++ch) + { + for (int i = 0; i < info.numSamples; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (0, i), buffer.getSample (ch, i)); + } + } + } +} + +TEST_F (ToneGeneratorAudioSourceTests, GeneratesWithStartSampleOffset) +{ + source->prepareToPlay (512, 44100.0); + source->setFrequency (1000.0); + source->setAmplitude (0.5f); + + AudioBuffer buffer (2, 512); + buffer.clear(); + + AudioSourceChannelInfo info; + info.buffer = &buffer; + info.startSample = 100; + info.numSamples = 256; + + source->getNextAudioBlock (info); + + // Check that samples before startSample are still zero + for (int i = 0; i < 100; ++i) + { + EXPECT_FLOAT_EQ (buffer.getSample (0, i), 0.0f); + EXPECT_FLOAT_EQ (buffer.getSample (1, i), 0.0f); + } + + // Check that samples at startSample are non-zero + bool hasNonZero = false; + for (int i = 100; i < 356; ++i) + { + if (buffer.getSample (0, i) != 0.0f) + { + hasNonZero = true; + break; + } + } + EXPECT_TRUE (hasNonZero); +} + +TEST_F (ToneGeneratorAudioSourceTests, PhaseAccumulatesAcrossCalls) +{ + source->prepareToPlay (512, 44100.0); + source->setFrequency (1000.0); + source->setAmplitude (1.0f); + + AudioBuffer buffer1 (1, 256); + AudioBuffer buffer2 (1, 256); + + AudioSourceChannelInfo info1; + info1.buffer = &buffer1; + info1.startSample = 0; + info1.numSamples = 256; + + AudioSourceChannelInfo info2; + info2.buffer = &buffer2; + info2.startSample = 0; + info2.numSamples = 256; + + // Get two consecutive blocks + source->getNextAudioBlock (info1); + source->getNextAudioBlock (info2); + + // The phase should continue from first block to second + // Last sample of first block should not equal first sample of second block + EXPECT_NE (buffer1.getSample (0, 255), buffer2.getSample (0, 0)); +} diff --git a/tests/yup_core/yup_Result.cpp b/tests/yup_core/yup_Result.cpp index dd4902ac5..5052ae99b 100644 --- a/tests/yup_core/yup_Result.cpp +++ b/tests/yup_core/yup_Result.cpp @@ -25,7 +25,7 @@ using namespace yup; -static String operator"" _S (const char* chars, size_t) +static String operator""_S (const char* chars, size_t) { return String { chars }; } diff --git a/tests/yup_core/yup_StringPairArray.cpp b/tests/yup_core/yup_StringPairArray.cpp index 6db625c47..ee112ac7d 100644 --- a/tests/yup_core/yup_StringPairArray.cpp +++ b/tests/yup_core/yup_StringPairArray.cpp @@ -43,7 +43,7 @@ using namespace yup; -static String operator"" _S (const char* chars, size_t) +static String operator""_S (const char* chars, size_t) { return String { chars }; } diff --git a/tests/yup_data_model/yup_DataTree.cpp b/tests/yup_data_model/yup_DataTree.cpp index ef96947e9..ac30508ce 100644 --- a/tests/yup_data_model/yup_DataTree.cpp +++ b/tests/yup_data_model/yup_DataTree.cpp @@ -696,6 +696,45 @@ TEST_F (DataTreeTests, XmlSerialization) // Reconstruct from XML auto reconstructed = DataTree::fromXml (*xml); EXPECT_TRUE (reconstructed.isValid()); + EXPECT_TRUE (reconstructed.getProperty ("intProp").isString()); + EXPECT_EQ ("42", reconstructed.getProperty ("intProp").toString()); + EXPECT_TRUE (reconstructed.getProperty ("floatProp").isString()); + EXPECT_EQ ("3.14", reconstructed.getProperty ("floatProp").toString()); +} + +TEST_F (DataTreeTests, XmlSerializationWithSchemaRestoresTypes) +{ + { + auto transaction = tree.beginTransaction(); + transaction.setProperty ("stringProp", "test string"); + transaction.setProperty ("intProp", 42); + transaction.setProperty ("floatProp", 3.14); + transaction.setProperty ("boolProp", true); + } + + auto xml = tree.createXml(); + ASSERT_NE (nullptr, xml); + + const String schemaJson = R"({ + "nodeTypes": { + "Root": { + "properties": { + "stringProp": { "type": "string" }, + "intProp": { "type": "number" }, + "floatProp": { "type": "number" }, + "boolProp": { "type": "boolean" } + } + } + } + })"; + + var schemaVar; + ASSERT_TRUE (JSON::parse (schemaJson, schemaVar)); + auto schema = DataTreeSchema::fromJsonSchema (schemaVar); + ASSERT_NE (nullptr, schema); + + auto reconstructed = DataTree::fromXml (*xml, schema); + EXPECT_TRUE (reconstructed.isValid()); EXPECT_TRUE (tree.isEquivalentTo (reconstructed)); } @@ -970,10 +1009,55 @@ TEST_F (DataTreeTests, SerializationFormatConsistency) transaction.addChild (plugins); } + const String schemaJson = R"({ + "nodeTypes": { + "Application": { + "properties": { + "name": { "type": "string" }, + "version": { "type": "string" }, + "debug": { "type": "boolean" }, + "maxUsers": { "type": "number" }, + "pi": { "type": "number" } + }, + "children": { "allowedTypes": ["Settings", "Plugins"] } + }, + "Settings": { + "properties": { + "theme": { "type": "string" }, + "autoSave": { "type": "boolean" }, + "interval": { "type": "number" } + }, + "children": { "allowedTypes": ["Advanced"] } + }, + "Advanced": { + "properties": { + "bufferSize": { "type": "number" }, + "compression": { "type": "boolean" } + }, + "children": { "maxCount": 0 } + }, + "Plugins": { + "children": { "allowedTypes": ["Plugin"] } + }, + "Plugin": { + "properties": { + "name": { "type": "string" }, + "enabled": { "type": "boolean" } + }, + "children": { "maxCount": 0 } + } + } + })"; + + var schemaVar; + ASSERT_TRUE (JSON::parse (schemaJson, schemaVar)); + auto schema = DataTreeSchema::fromJsonSchema (schemaVar); + ASSERT_NE (nullptr, schema); + // Test XML serialization roundtrip auto xml = original.createXml(); ASSERT_NE (nullptr, xml); - auto fromXml = DataTree::fromXml (*xml); + auto fromXml = DataTree::fromXml (*xml, schema); EXPECT_TRUE (fromXml.isValid()); EXPECT_TRUE (original.isEquivalentTo (fromXml)); diff --git a/tests/yup_data_model/yup_DataTreeSchema.cpp b/tests/yup_data_model/yup_DataTreeSchema.cpp index 0961fcd8e..dbe104ffb 100644 --- a/tests/yup_data_model/yup_DataTreeSchema.cpp +++ b/tests/yup_data_model/yup_DataTreeSchema.cpp @@ -440,6 +440,59 @@ TEST_F (DataTreeSchemaTests, ValidatedTransactionChildOperations) EXPECT_EQ (2, rootTree.getNumChildren()); } +TEST (DataTreeSchemaChildCountConstraints, ValidatedTransactionsHonorConstraints) +{ + const String schemaJson = R"({ + "nodeTypes": { + "Root": { + "children": { + "allowedTypes": ["Child"], + "minCount": 1, + "maxCount": 2 + } + }, + "Child": { + "children": { "maxCount": 0 } + } + } + })"; + + var schemaVar; + ASSERT_TRUE (JSON::parse (schemaJson, schemaVar)); + auto schema = DataTreeSchema::fromJsonSchema (schemaVar); + ASSERT_NE (nullptr, schema); + + auto root = schema->createNode ("Root"); + ASSERT_TRUE (root.isValid()); + + // Attempt to add three children in a single validated transaction; the third should fail. + auto addTx = root.beginValidatedTransaction (schema); + EXPECT_TRUE (addTx.createAndAddChild ("Child").wasOk()); + EXPECT_TRUE (addTx.createAndAddChild ("Child").wasOk()); + + auto thirdChild = addTx.createAndAddChild ("Child"); + EXPECT_TRUE (thirdChild.failed()); + EXPECT_TRUE (addTx.commit().failed()); + addTx.abort(); + + // Create two children in a plain transaction to reach the minimum count. + { + auto tx = root.beginTransaction(); + tx.addChild (schema->createNode ("Child")); + tx.addChild (schema->createNode ("Child")); + } + + // Removing one child is ok, removing below minCount should be rejected. + auto removeTx = root.beginValidatedTransaction (schema); + auto removeFirst = removeTx.removeChild (root.getChild (0)); + EXPECT_TRUE (removeFirst.wasOk()); + + auto removeSecond = removeTx.removeChild (root.getChild (1)); + EXPECT_TRUE (removeSecond.failed()); + EXPECT_TRUE (removeSecond.getErrorMessage().contains ("minimum")); + removeTx.abort(); +} + TEST_F (DataTreeSchemaTests, SchemaRoundtripSerialization) { // Export schema to JSON diff --git a/tests/yup_graphics/yup_AffineTransform.cpp b/tests/yup_graphics/yup_AffineTransform.cpp index c19840077..f509cb853 100644 --- a/tests/yup_graphics/yup_AffineTransform.cpp +++ b/tests/yup_graphics/yup_AffineTransform.cpp @@ -2,7 +2,7 @@ ============================================================================== This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com + Copyright (c) 2025 - kunitoki@gmail.com YUP is an open source library subject to open-source licensing. @@ -317,3 +317,605 @@ TEST (AffineTransformTests, ToMat2D_RotationTransform) EXPECT_NEAR (matrixFromAffine.tx(), expectedMatrix.tx(), tol); EXPECT_NEAR (matrixFromAffine.ty(), expectedMatrix.ty(), tol); } + +TEST (AffineTransformTests, Inverted_SingularMatrix) +{ + // Create a singular matrix (determinant = 0) + // Determinant = scaleX * scaleY - shearX * shearY + // Let's use: 2 * 1 - 2 * 1 = 0 + AffineTransform singular (2.0f, 2.0f, 0.0f, 1.0f, 1.0f, 0.0f); + + float det = singular.getDeterminant(); + EXPECT_FLOAT_EQ (det, 0.0f); + + AffineTransform inv = singular.inverted(); + + // The inverted singular matrix should return itself (as per implementation) + EXPECT_EQ (inv, singular); +} + +TEST (AffineTransformTests, Translation_WithPoint) +{ + // Test static translation with Point + Point p (5.0f, 10.0f); + AffineTransform t = AffineTransform::translation (p); + EXPECT_FLOAT_EQ (t.getTranslateX(), 5.0f); + EXPECT_FLOAT_EQ (t.getTranslateY(), 10.0f); + EXPECT_TRUE (t.isOnlyTranslation()); + + // Test translated method with Point + AffineTransform t2 = AffineTransform().translated (p); + EXPECT_FLOAT_EQ (t2.getTranslateX(), 5.0f); + EXPECT_FLOAT_EQ (t2.getTranslateY(), 10.0f); +} + +TEST (AffineTransformTests, WithAbsoluteTranslation) +{ + // Start with a transform that has translation + AffineTransform t = AffineTransform::translation (10.0f, 20.0f); + + // Test withAbsoluteTranslation with x, y + AffineTransform t2 = t.withAbsoluteTranslation (5.0f, 15.0f); + EXPECT_FLOAT_EQ (t2.getTranslateX(), 5.0f); + EXPECT_FLOAT_EQ (t2.getTranslateY(), 15.0f); + EXPECT_FLOAT_EQ (t2.getScaleX(), 1.0f); + EXPECT_FLOAT_EQ (t2.getScaleY(), 1.0f); + + // Test withAbsoluteTranslation with Point + Point p (7.0f, 8.0f); + AffineTransform t3 = t.withAbsoluteTranslation (p); + EXPECT_FLOAT_EQ (t3.getTranslateX(), 7.0f); + EXPECT_FLOAT_EQ (t3.getTranslateY(), 8.0f); +} + +TEST (AffineTransformTests, Rotated_Methods) +{ + const float angle = degreesToRadians (45.0f); + + // Test rotated() around origin + AffineTransform t = AffineTransform().rotated (angle); + EXPECT_NEAR (t.getScaleX(), std::cos (angle), tol); + EXPECT_NEAR (t.getShearX(), -std::sin (angle), tol); + EXPECT_NEAR (t.getShearY(), std::sin (angle), tol); + EXPECT_NEAR (t.getScaleY(), std::cos (angle), tol); + + // Test rotated() around a center point with x, y + AffineTransform t2 = AffineTransform().rotated (angle, 10.0f, 20.0f); + // The transform should rotate around (10, 20) instead of origin + EXPECT_NEAR (t2.getScaleX(), std::cos (angle), tol); + EXPECT_NEAR (t2.getShearX(), -std::sin (angle), tol); + + // Test rotated() around a center point with Point + Point center (10.0f, 20.0f); + AffineTransform t3 = AffineTransform().rotated (angle, center); + EXPECT_NEAR (t3.getScaleX(), std::cos (angle), tol); + EXPECT_NEAR (t3.getShearX(), -std::sin (angle), tol); +} + +TEST (AffineTransformTests, Rotation_WithCenter) +{ + const float angle = degreesToRadians (90.0f); + + // Test static rotation with center x, y + AffineTransform t = AffineTransform::rotation (angle, 10.0f, 10.0f); + Point testPoint (10.0f, 10.0f); + float x = testPoint.getX(); + float y = testPoint.getY(); + t.transformPoint (x, y); + // Point at center should not move + EXPECT_NEAR (x, 10.0f, tol); + EXPECT_NEAR (y, 10.0f, tol); + + // Test static rotation with center Point + Point center (5.0f, 5.0f); + AffineTransform t2 = AffineTransform::rotation (angle, center); + x = center.getX(); + y = center.getY(); + t2.transformPoint (x, y); + EXPECT_NEAR (x, 5.0f, tol); + EXPECT_NEAR (y, 5.0f, tol); +} + +TEST (AffineTransformTests, Scaled_WithCenter) +{ + // Test scaled() with center x, y + AffineTransform t = AffineTransform().scaled (2.0f, 3.0f, 10.0f, 10.0f); + Point centerPoint (10.0f, 10.0f); + float x = centerPoint.getX(); + float y = centerPoint.getY(); + t.transformPoint (x, y); + // Center point should not move + EXPECT_NEAR (x, 10.0f, tol); + EXPECT_NEAR (y, 10.0f, tol); + + // Test scaled() with center Point + Point center (5.0f, 5.0f); + AffineTransform t2 = AffineTransform().scaled (2.0f, 3.0f, center); + x = center.getX(); + y = center.getY(); + t2.transformPoint (x, y); + EXPECT_NEAR (x, 5.0f, tol); + EXPECT_NEAR (y, 5.0f, tol); +} + +TEST (AffineTransformTests, Scaling_WithCenter) +{ + // Test uniform scaling with center + AffineTransform t = AffineTransform::scaling (2.0f); + EXPECT_FLOAT_EQ (t.getScaleX(), 2.0f); + EXPECT_FLOAT_EQ (t.getScaleY(), 2.0f); + + // Test static scaling with center x, y + AffineTransform t2 = AffineTransform::scaling (2.0f, 3.0f, 10.0f, 10.0f); + Point centerPoint (10.0f, 10.0f); + float x = centerPoint.getX(); + float y = centerPoint.getY(); + t2.transformPoint (x, y); + EXPECT_NEAR (x, 10.0f, tol); + EXPECT_NEAR (y, 10.0f, tol); + + // Test static scaling with center Point + Point center (5.0f, 5.0f); + AffineTransform t3 = AffineTransform::scaling (2.0f, 3.0f, center); + x = center.getX(); + y = center.getY(); + t3.transformPoint (x, y); + EXPECT_NEAR (x, 5.0f, tol); + EXPECT_NEAR (y, 5.0f, tol); +} + +TEST (AffineTransformTests, Shearing_WithCenter) +{ + // Test static shearing with center x, y + AffineTransform t = AffineTransform::shearing (1.0f, 0.5f, 10.0f, 10.0f); + Point centerPoint (10.0f, 10.0f); + float x = centerPoint.getX(); + float y = centerPoint.getY(); + t.transformPoint (x, y); + // Center point should remain at center after shearing + EXPECT_NEAR (x, 10.0f, tol); + EXPECT_NEAR (y, 10.0f, tol); + + // Test static shearing with center Point + Point center (5.0f, 5.0f); + AffineTransform t2 = AffineTransform::shearing (1.0f, 0.5f, center); + x = center.getX(); + y = center.getY(); + t2.transformPoint (x, y); + EXPECT_NEAR (x, 5.0f, tol); + EXPECT_NEAR (y, 5.0f, tol); +} + +TEST (AffineTransformTests, PrependedBy) +{ + // Create two transforms + AffineTransform t1 = AffineTransform::scaling (2.0f); + AffineTransform t2 = AffineTransform::translation (5.0f, 10.0f); + + // prependedBy means: other * this = t2 * t1 + // This applies t1 first, then t2 + AffineTransform result = t1.prependedBy (t2); + // This is: translate then scale + // Point (1, 1) -> translate (5, 10) -> (6, 11) -> scale (2) -> (12, 22) + Point p (1.0f, 1.0f); + float x = p.getX(); + float y = p.getY(); + result.transformPoint (x, y); + EXPECT_FLOAT_EQ (x, 12.0f); + EXPECT_FLOAT_EQ (y, 22.0f); + + // Compare with followedBy which is: this * other = t1 * t2 + // This applies t2 first, then t1 + AffineTransform result2 = t1.followedBy (t2); + x = 1.0f; + y = 1.0f; + result2.transformPoint (x, y); + // This is: scale then translate + // Point (1, 1) -> scale (2) -> (2, 2) -> translate (5, 10) -> (7, 12) + EXPECT_FLOAT_EQ (x, 7.0f); + EXPECT_FLOAT_EQ (y, 12.0f); +} + +TEST (AffineTransformTests, MultiplicationOperator) +{ + // Create two transforms + AffineTransform t1 = AffineTransform::scaling (2.0f); + AffineTransform t2 = AffineTransform::translation (5.0f, 10.0f); + + // operator* is equivalent to followedBy + // t1 * t2 means: this * other = t1 * t2 + // This applies t2 first, then t1 + AffineTransform result = t1 * t2; + + // This is: scale then translate + // Point (1, 1) -> scale (2) -> (2, 2) -> translate (5, 10) -> (7, 12) + Point p (1.0f, 1.0f); + float x = p.getX(); + float y = p.getY(); + result.transformPoint (x, y); + EXPECT_FLOAT_EQ (x, 7.0f); + EXPECT_FLOAT_EQ (y, 12.0f); + + // Verify it's the same as followedBy + AffineTransform result2 = t1.followedBy (t2); + EXPECT_EQ (result, result2); + + // Test chaining multiple operators + AffineTransform t3 = AffineTransform::rotation (degreesToRadians (90.0f)); + AffineTransform chained = t1 * t2 * t3; + + // Should be equivalent to t1.followedBy(t2).followedBy(t3) + AffineTransform expected = t1.followedBy (t2).followedBy (t3); + EXPECT_TRUE (chained.approximatelyEqualTo (expected)); +} + +TEST (AffineTransformTests, ToString) +{ + AffineTransform t (1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f); + String str = t.toString(); + + // The string should contain all 6 matrix values + EXPECT_TRUE (str.contains ("1")); + EXPECT_TRUE (str.contains ("2")); + EXPECT_TRUE (str.contains ("3")); + EXPECT_TRUE (str.contains ("4")); + EXPECT_TRUE (str.contains ("5")); + EXPECT_TRUE (str.contains ("6")); +} + +TEST (AffineTransformTests, EdgeCases_ZeroRotation) +{ + // Test rotation with zero angle (should be identity) + AffineTransform t = AffineTransform::rotation (0.0f); + EXPECT_TRUE (t.isIdentity()); + + AffineTransform t2 = AffineTransform().rotated (0.0f); + EXPECT_TRUE (t2.isIdentity()); +} + +TEST (AffineTransformTests, EdgeCases_MultipleTransforms) +{ + // Combine multiple transforms + AffineTransform t = AffineTransform::translation (10.0f, 20.0f) + .followedBy (AffineTransform::rotation (degreesToRadians (90.0f))) + .followedBy (AffineTransform::scaling (2.0f)); + + // Transform a test point + Point p (1.0f, 0.0f); + float x = p.getX(); + float y = p.getY(); + t.transformPoint (x, y); + + // Expected: (1, 0) -> translate -> (11, 20) -> rotate 90° -> (-20, 11) -> scale 2 -> (-40, 22) + EXPECT_NEAR (x, -40.0f, tol); + EXPECT_NEAR (y, 22.0f, tol); +} + +TEST (AffineTransformTests, Identity_Operations) +{ + AffineTransform identity = AffineTransform::identity(); + AffineTransform translation = AffineTransform::translation (5.0f, 10.0f); + + // Identity followed by translation should equal translation + AffineTransform result1 = identity.followedBy (translation); + EXPECT_EQ (result1, translation); + + // Translation followed by identity should equal translation + AffineTransform result2 = translation.followedBy (identity); + EXPECT_EQ (result2, translation); + + // Identity prepended by translation should equal translation + AffineTransform result3 = identity.prependedBy (translation); + EXPECT_EQ (result3, translation); +} + +TEST (AffineTransformTests, Inversion_RoundTrip) +{ + // Create a complex transform + AffineTransform t = AffineTransform::translation (5.0f, 10.0f) + .followedBy (AffineTransform::rotation (degreesToRadians (30.0f))) + .followedBy (AffineTransform::scaling (2.0f)); + + // Invert it + AffineTransform inv = t.inverted(); + + // Apply transform then inverse should give identity + AffineTransform roundTrip = t.followedBy (inv); + + EXPECT_NEAR (roundTrip.getScaleX(), 1.0f, tol); + EXPECT_NEAR (roundTrip.getShearX(), 0.0f, tol); + EXPECT_NEAR (roundTrip.getTranslateX(), 0.0f, tol); + EXPECT_NEAR (roundTrip.getShearY(), 0.0f, tol); + EXPECT_NEAR (roundTrip.getScaleY(), 1.0f, tol); + EXPECT_NEAR (roundTrip.getTranslateY(), 0.0f, tol); +} + +TEST (AffineTransformTests, IsOnlyTranslation) +{ + // Test identity (no translation) + AffineTransform identity = AffineTransform::identity(); + EXPECT_TRUE (identity.isOnlyTranslation()); + + // Test pure translation + AffineTransform translation = AffineTransform::translation (5.0f, 10.0f); + EXPECT_TRUE (translation.isOnlyTranslation()); + + // Test translation with Point + AffineTransform translationPoint = AffineTransform::translation (Point (3.0f, 7.0f)); + EXPECT_TRUE (translationPoint.isOnlyTranslation()); + + // Test rotation (should not be only translation) + AffineTransform rotation = AffineTransform::rotation (degreesToRadians (45.0f)); + EXPECT_FALSE (rotation.isOnlyTranslation()); + + // Test scaling (should not be only translation) + AffineTransform scaling = AffineTransform::scaling (2.0f); + EXPECT_FALSE (scaling.isOnlyTranslation()); + + // Test shearing (should not be only translation) + AffineTransform shearing = AffineTransform::shearing (1.0f, 0.5f); + EXPECT_FALSE (shearing.isOnlyTranslation()); + + // Test combined transforms (translation + rotation) + AffineTransform combined = AffineTransform::translation (5.0f, 10.0f) + .followedBy (AffineTransform::rotation (degreesToRadians (30.0f))); + EXPECT_FALSE (combined.isOnlyTranslation()); + + // Test combined transforms (translation + scaling) + AffineTransform combinedScale = AffineTransform::translation (5.0f, 10.0f) + .followedBy (AffineTransform::scaling (2.0f)); + EXPECT_FALSE (combinedScale.isOnlyTranslation()); + + // Test custom transform with only translation components + AffineTransform customTranslation (1.0f, 0.0f, 15.0f, 0.0f, 1.0f, 20.0f); + EXPECT_TRUE (customTranslation.isOnlyTranslation()); + + // Test custom transform with scale + AffineTransform customScale (2.0f, 0.0f, 15.0f, 0.0f, 2.0f, 20.0f); + EXPECT_FALSE (customScale.isOnlyTranslation()); + + // Test custom transform with shear + AffineTransform customShear (1.0f, 0.5f, 15.0f, 0.0f, 1.0f, 20.0f); + EXPECT_FALSE (customShear.isOnlyTranslation()); +} + +TEST (AffineTransformTests, IsOnlyRotation) +{ + // Test identity (special case - 0 rotation) + AffineTransform identity = AffineTransform::identity(); + EXPECT_TRUE (identity.isOnlyRotation()); + + // Test pure rotation (90 degrees) + AffineTransform rotation90 = AffineTransform::rotation (degreesToRadians (90.0f)); + EXPECT_TRUE (rotation90.isOnlyRotation()); + + // Test pure rotation (45 degrees) + AffineTransform rotation45 = AffineTransform::rotation (degreesToRadians (45.0f)); + EXPECT_TRUE (rotation45.isOnlyRotation()); + + // Test pure rotation (180 degrees) + AffineTransform rotation180 = AffineTransform::rotation (degreesToRadians (180.0f)); + EXPECT_TRUE (rotation180.isOnlyRotation()); + + // Test pure rotation (negative angle) + AffineTransform rotationNeg = AffineTransform::rotation (degreesToRadians (-30.0f)); + EXPECT_TRUE (rotationNeg.isOnlyRotation()); + + // Test rotation with translation + AffineTransform rotationWithTranslation = AffineTransform::rotation (degreesToRadians (45.0f)) + .translated (5.0f, 10.0f); + EXPECT_FALSE (rotationWithTranslation.isOnlyRotation()); + + // Test rotation with scaling + AffineTransform rotationWithScaling = AffineTransform::rotation (degreesToRadians (45.0f)) + .scaled (2.0f); + EXPECT_FALSE (rotationWithScaling.isOnlyRotation()); + + // Test pure translation (not rotation) + AffineTransform translation = AffineTransform::translation (5.0f, 10.0f); + EXPECT_FALSE (translation.isOnlyRotation()); + + // Test pure scaling (not rotation) + AffineTransform scaling = AffineTransform::scaling (2.0f); + EXPECT_FALSE (scaling.isOnlyRotation()); + + // Test shearing (not rotation) + AffineTransform shearing = AffineTransform::shearing (1.0f, 0.5f); + EXPECT_FALSE (shearing.isOnlyRotation()); +} + +TEST (AffineTransformTests, IsOnlyUniformScaling) +{ + // Test identity (scale = 1, should return false) + AffineTransform identity = AffineTransform::identity(); + EXPECT_FALSE (identity.isOnlyUniformScaling()); + + // Test pure uniform scaling (scale = 2) + AffineTransform scaling2 = AffineTransform::scaling (2.0f); + EXPECT_TRUE (scaling2.isOnlyUniformScaling()); + + // Test pure uniform scaling (scale = 0.5) + AffineTransform scalingHalf = AffineTransform::scaling (0.5f); + EXPECT_TRUE (scalingHalf.isOnlyUniformScaling()); + + // Test pure uniform scaling (scale = 3) + AffineTransform scaling3 = AffineTransform::scaling (3.0f); + EXPECT_TRUE (scaling3.isOnlyUniformScaling()); + + // Test non-uniform scaling + AffineTransform nonUniform = AffineTransform::scaling (2.0f, 3.0f); + EXPECT_FALSE (nonUniform.isOnlyUniformScaling()); + + // Test uniform scaling with translation + AffineTransform scalingWithTranslation = AffineTransform::scaling (2.0f) + .translated (5.0f, 10.0f); + EXPECT_FALSE (scalingWithTranslation.isOnlyUniformScaling()); + + // Test uniform scaling with rotation + AffineTransform scalingWithRotation = AffineTransform::scaling (2.0f) + .rotated (degreesToRadians (45.0f)); + EXPECT_FALSE (scalingWithRotation.isOnlyUniformScaling()); + + // Test pure translation (not scaling) + AffineTransform translation = AffineTransform::translation (5.0f, 10.0f); + EXPECT_FALSE (translation.isOnlyUniformScaling()); + + // Test pure rotation (not scaling) + AffineTransform rotation = AffineTransform::rotation (degreesToRadians (45.0f)); + EXPECT_FALSE (rotation.isOnlyUniformScaling()); +} + +TEST (AffineTransformTests, IsOnlyNonUniformScaling) +{ + // Test identity (not non-uniform scaling) + AffineTransform identity = AffineTransform::identity(); + EXPECT_FALSE (identity.isOnlyNonUniformScaling()); + + // Test pure non-uniform scaling + AffineTransform nonUniform = AffineTransform::scaling (2.0f, 3.0f); + EXPECT_TRUE (nonUniform.isOnlyNonUniformScaling()); + + // Test pure non-uniform scaling (different factors) + AffineTransform nonUniform2 = AffineTransform::scaling (0.5f, 2.0f); + EXPECT_TRUE (nonUniform2.isOnlyNonUniformScaling()); + + // Test uniform scaling (not non-uniform) + AffineTransform uniform = AffineTransform::scaling (2.0f); + EXPECT_FALSE (uniform.isOnlyNonUniformScaling()); + + // Test non-uniform scaling with translation + AffineTransform nonUniformWithTranslation = AffineTransform::scaling (2.0f, 3.0f) + .translated (5.0f, 10.0f); + EXPECT_FALSE (nonUniformWithTranslation.isOnlyNonUniformScaling()); + + // Test pure translation (not scaling) + AffineTransform translation = AffineTransform::translation (5.0f, 10.0f); + EXPECT_FALSE (translation.isOnlyNonUniformScaling()); + + // Test pure rotation (not scaling) + AffineTransform rotation = AffineTransform::rotation (degreesToRadians (45.0f)); + EXPECT_FALSE (rotation.isOnlyNonUniformScaling()); +} + +TEST (AffineTransformTests, IsOnlyScaling) +{ + // Test identity (not scaling) + AffineTransform identity = AffineTransform::identity(); + EXPECT_FALSE (identity.isOnlyScaling()); + + // Test pure uniform scaling + AffineTransform uniform = AffineTransform::scaling (2.0f); + EXPECT_TRUE (uniform.isOnlyScaling()); + + // Test pure non-uniform scaling + AffineTransform nonUniform = AffineTransform::scaling (2.0f, 3.0f); + EXPECT_TRUE (nonUniform.isOnlyScaling()); + + // Test scaling with translation + AffineTransform scalingWithTranslation = AffineTransform::scaling (2.0f) + .translated (5.0f, 10.0f); + EXPECT_FALSE (scalingWithTranslation.isOnlyScaling()); + + // Test scaling with rotation + AffineTransform scalingWithRotation = AffineTransform::scaling (2.0f) + .rotated (degreesToRadians (45.0f)); + EXPECT_FALSE (scalingWithRotation.isOnlyScaling()); + + // Test pure translation (not scaling) + AffineTransform translation = AffineTransform::translation (5.0f, 10.0f); + EXPECT_FALSE (translation.isOnlyScaling()); + + // Test pure rotation (not scaling) + AffineTransform rotation = AffineTransform::rotation (degreesToRadians (45.0f)); + EXPECT_FALSE (rotation.isOnlyScaling()); + + // Test shearing (not scaling) + AffineTransform shearing = AffineTransform::shearing (1.0f, 0.5f); + EXPECT_FALSE (shearing.isOnlyScaling()); +} + +TEST (AffineTransformTests, IsOnlyShearing) +{ + // Test identity (not shearing) + AffineTransform identity = AffineTransform::identity(); + EXPECT_FALSE (identity.isOnlyShearing()); + + // Test pure shearing + AffineTransform shearing = AffineTransform::shearing (1.0f, 0.5f); + EXPECT_TRUE (shearing.isOnlyShearing()); + + // Test shearing with only x factor + AffineTransform shearingX = AffineTransform::shearing (1.0f, 0.0f); + EXPECT_TRUE (shearingX.isOnlyShearing()); + + // Test shearing with only y factor + AffineTransform shearingY = AffineTransform::shearing (0.0f, 0.5f); + EXPECT_TRUE (shearingY.isOnlyShearing()); + + // Test shearing with translation + AffineTransform shearingWithTranslation = AffineTransform::shearing (1.0f, 0.5f) + .translated (5.0f, 10.0f); + EXPECT_FALSE (shearingWithTranslation.isOnlyShearing()); + + // Test shearing with scaling + AffineTransform shearingWithScaling = AffineTransform::shearing (1.0f, 0.5f) + .scaled (2.0f); + EXPECT_FALSE (shearingWithScaling.isOnlyShearing()); + + // Test pure translation (not shearing) + AffineTransform translation = AffineTransform::translation (5.0f, 10.0f); + EXPECT_FALSE (translation.isOnlyShearing()); + + // Test pure rotation (not shearing) + AffineTransform rotation = AffineTransform::rotation (degreesToRadians (45.0f)); + EXPECT_FALSE (rotation.isOnlyShearing()); + + // Test pure scaling (not shearing) + AffineTransform scaling = AffineTransform::scaling (2.0f); + EXPECT_FALSE (scaling.isOnlyShearing()); +} + +TEST (AffineTransformTests, TransformationType_Combinations) +{ + // Test that identity satisfies both translation and rotation + AffineTransform identity = AffineTransform::identity(); + EXPECT_TRUE (identity.isIdentity()); + EXPECT_TRUE (identity.isOnlyTranslation()); + EXPECT_TRUE (identity.isOnlyRotation()); + EXPECT_FALSE (identity.isOnlyScaling()); + EXPECT_FALSE (identity.isOnlyShearing()); + + // Test that each pure transformation is mutually exclusive + AffineTransform translation = AffineTransform::translation (5.0f, 10.0f); + EXPECT_TRUE (translation.isOnlyTranslation()); + EXPECT_FALSE (translation.isOnlyRotation()); + EXPECT_FALSE (translation.isOnlyScaling()); + EXPECT_FALSE (translation.isOnlyShearing()); + + AffineTransform rotation = AffineTransform::rotation (degreesToRadians (45.0f)); + EXPECT_FALSE (rotation.isOnlyTranslation()); + EXPECT_TRUE (rotation.isOnlyRotation()); + EXPECT_FALSE (rotation.isOnlyScaling()); + EXPECT_FALSE (rotation.isOnlyShearing()); + + AffineTransform scaling = AffineTransform::scaling (2.0f); + EXPECT_FALSE (scaling.isOnlyTranslation()); + EXPECT_FALSE (scaling.isOnlyRotation()); + EXPECT_TRUE (scaling.isOnlyScaling()); + EXPECT_FALSE (scaling.isOnlyShearing()); + + AffineTransform shearing = AffineTransform::shearing (1.0f, 0.5f); + EXPECT_FALSE (shearing.isOnlyTranslation()); + EXPECT_FALSE (shearing.isOnlyRotation()); + EXPECT_FALSE (shearing.isOnlyScaling()); + EXPECT_TRUE (shearing.isOnlyShearing()); + + // Test combined transforms are not "only" any single type + AffineTransform combined = AffineTransform::translation (5.0f, 10.0f) + .followedBy (AffineTransform::rotation (degreesToRadians (45.0f))) + .followedBy (AffineTransform::scaling (2.0f)); + EXPECT_FALSE (combined.isOnlyTranslation()); + EXPECT_FALSE (combined.isOnlyRotation()); + EXPECT_FALSE (combined.isOnlyScaling()); + EXPECT_FALSE (combined.isOnlyShearing()); +} diff --git a/tests/yup_graphics/yup_Font.cpp b/tests/yup_graphics/yup_Font.cpp index a96b33e96..8f84bb5d3 100644 --- a/tests/yup_graphics/yup_Font.cpp +++ b/tests/yup_graphics/yup_Font.cpp @@ -25,6 +25,24 @@ using namespace yup; +namespace +{ +File getValidFontFile() +{ + return +#if YUP_EMSCRIPTEN + File ("/") +#else + File (__FILE__) + .getParentDirectory() + .getParentDirectory() +#endif + .getChildFile ("data") + .getChildFile ("fonts") + .getChildFile ("Linefont-VariableFont_wdth,wght.ttf"); +} +} // namespace + // ============================================================================== // Constructor and Assignment Tests // ============================================================================== @@ -137,6 +155,342 @@ TEST (FontTests, LoadFromDirectory) EXPECT_FALSE (result.wasOk()); } +TEST (FontTests, LoadFromFileWithValidFile) +{ + Font font; + File fontFile = getValidFontFile(); + + Result result = font.loadFromFile (fontFile); + + EXPECT_TRUE (result.wasOk()); + EXPECT_TRUE (result.getErrorMessage().isEmpty()); +} + +// ============================================================================== +// Variable Font Tests +// ============================================================================== + +TEST (FontTests, VariableFont_HasCorrectNumberOfAxes) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + // The font should have 2 axes: wdth and wght + EXPECT_EQ (2, font.getNumAxis()); +} + +TEST (FontTests, VariableFont_GetAxisDescriptionByIndex) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + // Get axis descriptions by index + auto axis0 = font.getAxisDescription (0); + auto axis1 = font.getAxisDescription (1); + + ASSERT_TRUE (axis0.has_value()); + ASSERT_TRUE (axis1.has_value()); + + // Check that we have wdth and wght axes (order may vary) + bool hasWdth = axis0->tagName == "wdth" || axis1->tagName == "wdth"; + bool hasWght = axis0->tagName == "wght" || axis1->tagName == "wght"; + + EXPECT_TRUE (hasWdth); + EXPECT_TRUE (hasWght); +} + +TEST (FontTests, VariableFont_GetAxisDescriptionByTag) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + // Get wdth axis description + auto wdthAxis = font.getAxisDescription ("wdth"); + ASSERT_TRUE (wdthAxis.has_value()); + EXPECT_EQ ("wdth", wdthAxis->tagName); + EXPECT_GT (wdthAxis->maximumValue, wdthAxis->minimumValue); + EXPECT_GE (wdthAxis->defaultValue, wdthAxis->minimumValue); + EXPECT_LE (wdthAxis->defaultValue, wdthAxis->maximumValue); + + // Get wght axis description + auto wghtAxis = font.getAxisDescription ("wght"); + ASSERT_TRUE (wghtAxis.has_value()); + EXPECT_EQ ("wght", wghtAxis->tagName); + EXPECT_GT (wghtAxis->maximumValue, wghtAxis->minimumValue); + EXPECT_GE (wghtAxis->defaultValue, wghtAxis->minimumValue); + EXPECT_LE (wghtAxis->defaultValue, wghtAxis->maximumValue); +} + +TEST (FontTests, VariableFont_GetAxisDescriptionForInvalidTag) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + // Try to get description for non-existent axis + auto invalidAxis = font.getAxisDescription ("slnt"); + + EXPECT_FALSE (invalidAxis.has_value()); +} + +TEST (FontTests, VariableFont_GetAxisValueReturnsDefaultValue) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + // Get default values + auto wdthAxis = font.getAxisDescription ("wdth"); + auto wghtAxis = font.getAxisDescription ("wght"); + + ASSERT_TRUE (wdthAxis.has_value()); + ASSERT_TRUE (wghtAxis.has_value()); + + // Initially, axis values should be at their defaults + EXPECT_FLOAT_EQ (wdthAxis->defaultValue, font.getAxisValue ("wdth")); + EXPECT_FLOAT_EQ (wghtAxis->defaultValue, font.getAxisValue ("wght")); +} + +TEST (FontTests, VariableFont_SetAxisValueByTag) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + // Get axis ranges + auto wdthAxis = font.getAxisDescription ("wdth"); + auto wghtAxis = font.getAxisDescription ("wght"); + + ASSERT_TRUE (wdthAxis.has_value()); + ASSERT_TRUE (wghtAxis.has_value()); + + // Set wdth to maximum + font.setAxisValue ("wdth", wdthAxis->maximumValue); + EXPECT_FLOAT_EQ (wdthAxis->maximumValue, font.getAxisValue ("wdth")); + + // Set wght to minimum + font.setAxisValue ("wght", wghtAxis->minimumValue); + EXPECT_FLOAT_EQ (wghtAxis->minimumValue, font.getAxisValue ("wght")); +} + +TEST (FontTests, VariableFont_SetAxisValueByIndex) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + // Get axis descriptions to find which index is which + auto axis0 = font.getAxisDescription (0); + ASSERT_TRUE (axis0.has_value()); + + // Set axis 0 to its maximum value + font.setAxisValue (0, axis0->maximumValue); + EXPECT_FLOAT_EQ (axis0->maximumValue, font.getAxisValue (0)); +} + +TEST (FontTests, VariableFont_WithAxisValueByTag) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + auto wghtAxis = font.getAxisDescription ("wght"); + ASSERT_TRUE (wghtAxis.has_value()); + + // Create new font with modified wght + Font newFont = font.withAxisValue ("wght", wghtAxis->maximumValue); + + // Original font should be unchanged + EXPECT_FLOAT_EQ (wghtAxis->defaultValue, font.getAxisValue ("wght")); + + // New font should have the modified value + EXPECT_FLOAT_EQ (wghtAxis->maximumValue, newFont.getAxisValue ("wght")); +} + +TEST (FontTests, VariableFont_WithAxisValueByIndex) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + auto axis0 = font.getAxisDescription (0); + ASSERT_TRUE (axis0.has_value()); + + // Create new font with modified axis value + Font newFont = font.withAxisValue (0, axis0->maximumValue); + + // Original font should be unchanged + EXPECT_FLOAT_EQ (axis0->defaultValue, font.getAxisValue (0)); + + // New font should have the modified value + EXPECT_FLOAT_EQ (axis0->maximumValue, newFont.getAxisValue (0)); +} + +TEST (FontTests, VariableFont_ResetAxisValueByTag) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + auto wdthAxis = font.getAxisDescription ("wdth"); + ASSERT_TRUE (wdthAxis.has_value()); + + // Set to non-default value + font.setAxisValue ("wdth", wdthAxis->maximumValue); + EXPECT_FLOAT_EQ (wdthAxis->maximumValue, font.getAxisValue ("wdth")); + + // Reset to default + font.resetAxisValue ("wdth"); + EXPECT_FLOAT_EQ (wdthAxis->defaultValue, font.getAxisValue ("wdth")); +} + +TEST (FontTests, VariableFont_ResetAxisValueByIndex) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + auto axis0 = font.getAxisDescription (0); + ASSERT_TRUE (axis0.has_value()); + + // Set to non-default value + font.setAxisValue (0, axis0->maximumValue); + EXPECT_FLOAT_EQ (axis0->maximumValue, font.getAxisValue (0)); + + // Reset to default + font.resetAxisValue (0); + EXPECT_FLOAT_EQ (axis0->defaultValue, font.getAxisValue (0)); +} + +TEST (FontTests, VariableFont_ResetAllAxisValues) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + auto wdthAxis = font.getAxisDescription ("wdth"); + auto wghtAxis = font.getAxisDescription ("wght"); + + ASSERT_TRUE (wdthAxis.has_value()); + ASSERT_TRUE (wghtAxis.has_value()); + + // Set both axes to non-default values + font.setAxisValue ("wdth", wdthAxis->maximumValue); + font.setAxisValue ("wght", wghtAxis->minimumValue); + + // Reset all axes + font.resetAllAxisValues(); + + // Both should be back to defaults + EXPECT_FLOAT_EQ (wdthAxis->defaultValue, font.getAxisValue ("wdth")); + EXPECT_FLOAT_EQ (wghtAxis->defaultValue, font.getAxisValue ("wght")); +} + +TEST (FontTests, VariableFont_SetAxisValues) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + auto wdthAxis = font.getAxisDescription ("wdth"); + auto wghtAxis = font.getAxisDescription ("wght"); + + ASSERT_TRUE (wdthAxis.has_value()); + ASSERT_TRUE (wghtAxis.has_value()); + + // Set multiple axes at once + font.setAxisValues ({ { "wdth", wdthAxis->maximumValue }, + { "wght", wghtAxis->minimumValue } }); + + EXPECT_FLOAT_EQ (wdthAxis->maximumValue, font.getAxisValue ("wdth")); + EXPECT_FLOAT_EQ (wghtAxis->minimumValue, font.getAxisValue ("wght")); +} + +TEST (FontTests, VariableFont_WithAxisValues) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + auto wdthAxis = font.getAxisDescription ("wdth"); + auto wghtAxis = font.getAxisDescription ("wght"); + + ASSERT_TRUE (wdthAxis.has_value()); + ASSERT_TRUE (wghtAxis.has_value()); + + // Create new font with multiple axis modifications + Font newFont = font.withAxisValues ({ { "wdth", wdthAxis->minimumValue }, + { "wght", wghtAxis->maximumValue } }); + + // Original font should be unchanged + EXPECT_FLOAT_EQ (wdthAxis->defaultValue, font.getAxisValue ("wdth")); + EXPECT_FLOAT_EQ (wghtAxis->defaultValue, font.getAxisValue ("wght")); + + // New font should have the modified values + EXPECT_FLOAT_EQ (wdthAxis->minimumValue, newFont.getAxisValue ("wdth")); + EXPECT_FLOAT_EQ (wghtAxis->maximumValue, newFont.getAxisValue ("wght")); +} + +TEST (FontTests, VariableFont_ChainedAxisOperations) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + auto wdthAxis = font.getAxisDescription ("wdth"); + auto wghtAxis = font.getAxisDescription ("wght"); + + ASSERT_TRUE (wdthAxis.has_value()); + ASSERT_TRUE (wghtAxis.has_value()); + + // Chain multiple operations + Font newFont = font + .withAxisValue ("wdth", wdthAxis->maximumValue) + .withAxisValue ("wght", wghtAxis->minimumValue) + .withHeight (24.0f); + + // Original font should be unchanged + EXPECT_FLOAT_EQ (wdthAxis->defaultValue, font.getAxisValue ("wdth")); + EXPECT_FLOAT_EQ (wghtAxis->defaultValue, font.getAxisValue ("wght")); + EXPECT_EQ (12.0f, font.getHeight()); + + // New font should have all modifications + EXPECT_FLOAT_EQ (wdthAxis->maximumValue, newFont.getAxisValue ("wdth")); + EXPECT_FLOAT_EQ (wghtAxis->minimumValue, newFont.getAxisValue ("wght")); + EXPECT_EQ (24.0f, newFont.getHeight()); +} + +TEST (FontTests, VariableFont_FontMetrics) +{ + Font font; + File fontFile = getValidFontFile(); + + font.loadFromFile (fontFile); + + // Variable font should have valid metrics + EXPECT_NE (0.0f, font.getAscent()); + EXPECT_NE (0.0f, font.getDescent()); + EXPECT_GT (font.getWeight(), 0); +} + // ============================================================================== // Height Tests // ============================================================================== diff --git a/tests/yup_graphics/yup_Point.cpp b/tests/yup_graphics/yup_Point.cpp index a7c78fa0c..a00e2ba71 100644 --- a/tests/yup_graphics/yup_Point.cpp +++ b/tests/yup_graphics/yup_Point.cpp @@ -2,7 +2,7 @@ ============================================================================== This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com + Copyright (c) 2025 - kunitoki@gmail.com YUP is an open source library subject to open-source licensing. diff --git a/tests/yup_graphics/yup_Rectangle.cpp b/tests/yup_graphics/yup_Rectangle.cpp index a2cac464b..e622ecf9d 100644 --- a/tests/yup_graphics/yup_Rectangle.cpp +++ b/tests/yup_graphics/yup_Rectangle.cpp @@ -2,7 +2,7 @@ ============================================================================== This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com + Copyright (c) 2025 - kunitoki@gmail.com YUP is an open source library subject to open-source licensing. @@ -71,6 +71,13 @@ TEST (RectangleTests, Parameterized_Constructors) EXPECT_FLOAT_EQ (r4.getY(), 2.0f); EXPECT_FLOAT_EQ (r4.getWidth(), 3.0f); EXPECT_FLOAT_EQ (r4.getHeight(), 4.0f); + + // Constructor with Point, Size to Rectangle (template constructor) + Rectangle r5 (Point (1, 2), Size (3, 4)); + EXPECT_FLOAT_EQ (r5.getX(), 1.0f); + EXPECT_FLOAT_EQ (r5.getY(), 2.0f); + EXPECT_FLOAT_EQ (r5.getWidth(), 3.0f); + EXPECT_FLOAT_EQ (r5.getHeight(), 4.0f); } TEST (RectangleTests, Type_Conversion_Constructor) @@ -124,11 +131,16 @@ TEST (RectangleTests, Position_And_Size) EXPECT_FLOAT_EQ (r.getX(), 5.0f); EXPECT_FLOAT_EQ (r.getY(), 6.0f); - // Test withPosition + // Test withPosition with Point Rectangle r2 = r.withPosition (Point (7.0f, 8.0f)); EXPECT_FLOAT_EQ (r2.getX(), 7.0f); EXPECT_FLOAT_EQ (r2.getY(), 8.0f); + // Test withPosition with x, y + Rectangle r2b = r.withPosition (9.0f, 10.0f); + EXPECT_FLOAT_EQ (r2b.getX(), 9.0f); + EXPECT_FLOAT_EQ (r2b.getY(), 10.0f); + // Test withZeroPosition Rectangle r3 = r.withZeroPosition(); EXPECT_FLOAT_EQ (r3.getX(), 0.0f); @@ -139,19 +151,30 @@ TEST (RectangleTests, Position_And_Size) EXPECT_FLOAT_EQ (size.getWidth(), 3.0f); EXPECT_FLOAT_EQ (size.getHeight(), 4.0f); - // Test setSize + // Test setSize with Size r.setSize (Size (7.0f, 8.0f)); EXPECT_FLOAT_EQ (r.getWidth(), 7.0f); EXPECT_FLOAT_EQ (r.getHeight(), 8.0f); - // Test withSize - Rectangle r4 = r.withSize (Size (9.0f, 10.0f)); - EXPECT_FLOAT_EQ (r4.getWidth(), 9.0f); - EXPECT_FLOAT_EQ (r4.getHeight(), 10.0f); + // Test setSize with width, height + r.setSize (9.0f, 10.0f); + EXPECT_FLOAT_EQ (r.getWidth(), 9.0f); + EXPECT_FLOAT_EQ (r.getHeight(), 10.0f); + + // Test withSize with Size + Rectangle r4 = r.withSize (Size (11.0f, 12.0f)); + EXPECT_FLOAT_EQ (r4.getWidth(), 11.0f); + EXPECT_FLOAT_EQ (r4.getHeight(), 12.0f); - Rectangle r5 = r.withSize (9.0f, 10.0f); - EXPECT_FLOAT_EQ (r5.getWidth(), 9.0f); - EXPECT_FLOAT_EQ (r5.getHeight(), 10.0f); + // Test withSize with width, height + Rectangle r5 = r.withSize (13.0f, 14.0f); + EXPECT_FLOAT_EQ (r5.getWidth(), 13.0f); + EXPECT_FLOAT_EQ (r5.getHeight(), 14.0f); + + // Test withScaledSize + Rectangle r5b = r.withScaledSize (2.0f); + EXPECT_FLOAT_EQ (r5b.getWidth(), 18.0f); + EXPECT_FLOAT_EQ (r5b.getHeight(), 20.0f); // Test withZeroSize Rectangle r6 = r.withZeroSize(); @@ -232,10 +255,35 @@ TEST (RectangleTests, Center) EXPECT_FLOAT_EQ (r.getCenterX(), 5.0f); EXPECT_FLOAT_EQ (r.getCenterY(), 6.0f); - // Test withCenter + // Test setCenterX + r.setCenterX (10.0f); + EXPECT_FLOAT_EQ (r.getCenterX(), 10.0f); + EXPECT_FLOAT_EQ (r.getCenterY(), 6.0f); + + // Test setCenterY + r.setCenterY (12.0f); + EXPECT_FLOAT_EQ (r.getCenterX(), 10.0f); + EXPECT_FLOAT_EQ (r.getCenterY(), 12.0f); + + // Test withCenter with x, y Rectangle r2 = r.withCenter (7.0f, 8.0f); EXPECT_FLOAT_EQ (r2.getCenterX(), 7.0f); EXPECT_FLOAT_EQ (r2.getCenterY(), 8.0f); + + // Test withCenter with Point + Rectangle r3 = r.withCenter (Point (9.0f, 10.0f)); + EXPECT_FLOAT_EQ (r3.getCenterX(), 9.0f); + EXPECT_FLOAT_EQ (r3.getCenterY(), 10.0f); + + // Test withCenterX + Rectangle r4 = r.withCenterX (15.0f); + EXPECT_FLOAT_EQ (r4.getCenterX(), 15.0f); + EXPECT_FLOAT_EQ (r4.getCenterY(), 12.0f); + + // Test withCenterY + Rectangle r5 = r.withCenterY (20.0f); + EXPECT_FLOAT_EQ (r5.getCenterX(), 10.0f); + EXPECT_FLOAT_EQ (r5.getCenterY(), 20.0f); } TEST (RectangleTests, Shape_Checks) @@ -459,11 +507,85 @@ TEST (RectangleTests, Reduce_And_Enlarge) EXPECT_FLOAT_EQ (r.getWidth(), 2.6f); EXPECT_FLOAT_EQ (r.getHeight(), 3.4f); + // Test enlarge with all sides (left, top, right, bottom) + Rectangle r7 (10.0f, 20.0f, 30.0f, 40.0f); + r7.enlarge (1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_FLOAT_EQ (r7.getX(), 9.0f); + EXPECT_FLOAT_EQ (r7.getY(), 18.0f); + EXPECT_FLOAT_EQ (r7.getWidth(), 34.0f); + EXPECT_FLOAT_EQ (r7.getHeight(), 46.0f); + // Test enlarged Rectangle r5 = r.enlarged (0.5f); Rectangle r6 = r.enlarged (0.5f, 1.0f); } +TEST (RectangleTests, ReducedSides) +{ + Rectangle r (10.0f, 20.0f, 30.0f, 40.0f); + + // Test reducedLeft + Rectangle redLeft = r.reducedLeft (5.0f); + EXPECT_FLOAT_EQ (redLeft.getX(), 15.0f); + EXPECT_FLOAT_EQ (redLeft.getY(), 20.0f); + EXPECT_FLOAT_EQ (redLeft.getWidth(), 25.0f); + EXPECT_FLOAT_EQ (redLeft.getHeight(), 40.0f); + + // Test reducedTop + Rectangle redTop = r.reducedTop (5.0f); + EXPECT_FLOAT_EQ (redTop.getX(), 10.0f); + EXPECT_FLOAT_EQ (redTop.getY(), 25.0f); + EXPECT_FLOAT_EQ (redTop.getWidth(), 30.0f); + EXPECT_FLOAT_EQ (redTop.getHeight(), 35.0f); + + // Test reducedRight + Rectangle redRight = r.reducedRight (5.0f); + EXPECT_FLOAT_EQ (redRight.getX(), 10.0f); + EXPECT_FLOAT_EQ (redRight.getY(), 20.0f); + EXPECT_FLOAT_EQ (redRight.getWidth(), 25.0f); + EXPECT_FLOAT_EQ (redRight.getHeight(), 40.0f); + + // Test reducedBottom + Rectangle redBottom = r.reducedBottom (5.0f); + EXPECT_FLOAT_EQ (redBottom.getX(), 10.0f); + EXPECT_FLOAT_EQ (redBottom.getY(), 20.0f); + EXPECT_FLOAT_EQ (redBottom.getWidth(), 30.0f); + EXPECT_FLOAT_EQ (redBottom.getHeight(), 35.0f); +} + +TEST (RectangleTests, EnlargedSides) +{ + Rectangle r (10.0f, 20.0f, 30.0f, 40.0f); + + // Test enlargedLeft + Rectangle enlLeft = r.enlargedLeft (5.0f); + EXPECT_FLOAT_EQ (enlLeft.getX(), 5.0f); + EXPECT_FLOAT_EQ (enlLeft.getY(), 20.0f); + EXPECT_FLOAT_EQ (enlLeft.getWidth(), 35.0f); + EXPECT_FLOAT_EQ (enlLeft.getHeight(), 40.0f); + + // Test enlargedTop + Rectangle enlTop = r.enlargedTop (5.0f); + EXPECT_FLOAT_EQ (enlTop.getX(), 10.0f); + EXPECT_FLOAT_EQ (enlTop.getY(), 15.0f); + EXPECT_FLOAT_EQ (enlTop.getWidth(), 30.0f); + EXPECT_FLOAT_EQ (enlTop.getHeight(), 45.0f); + + // Test enlargedRight + Rectangle enlRight = r.enlargedRight (5.0f); + EXPECT_FLOAT_EQ (enlRight.getX(), 10.0f); + EXPECT_FLOAT_EQ (enlRight.getY(), 20.0f); + EXPECT_FLOAT_EQ (enlRight.getWidth(), 35.0f); + EXPECT_FLOAT_EQ (enlRight.getHeight(), 40.0f); + + // Test enlargedBottom + Rectangle enlBottom = r.enlargedBottom (5.0f); + EXPECT_FLOAT_EQ (enlBottom.getX(), 10.0f); + EXPECT_FLOAT_EQ (enlBottom.getY(), 20.0f); + EXPECT_FLOAT_EQ (enlBottom.getWidth(), 30.0f); + EXPECT_FLOAT_EQ (enlBottom.getHeight(), 45.0f); +} + TEST (RectangleTests, Reduce_And_Enlarge_Edge_Cases) { Rectangle r (1.0f, 2.0f, 3.0f, 4.0f); @@ -906,26 +1028,62 @@ TEST (RectangleTests, With_EdgePosition_Methods) EXPECT_FLOAT_EQ (withBottom.getHeight(), 40.0f); } -TEST (RectangleTests, DISABLED_TrimmedMethods) +TEST (RectangleTests, TrimmedMethods) { Rectangle r (10.0f, 20.0f, 30.0f, 40.0f); - // Test withTrimmedLeft/Right/Top/Bottom + // Test withTrimmedLeft Rectangle trimmedLeft = r.withTrimmedLeft (5.0f); EXPECT_FLOAT_EQ (trimmedLeft.getX(), 15.0f); - EXPECT_FLOAT_EQ (trimmedLeft.getWidth(), 30.0f); + EXPECT_FLOAT_EQ (trimmedLeft.getY(), 20.0f); + EXPECT_FLOAT_EQ (trimmedLeft.getWidth(), 25.0f); + EXPECT_FLOAT_EQ (trimmedLeft.getHeight(), 40.0f); + // Test withTrimmedRight Rectangle trimmedRight = r.withTrimmedRight (5.0f); EXPECT_FLOAT_EQ (trimmedRight.getX(), 10.0f); - EXPECT_FLOAT_EQ (trimmedRight.getWidth(), 30.0f); + EXPECT_FLOAT_EQ (trimmedRight.getY(), 20.0f); + EXPECT_FLOAT_EQ (trimmedRight.getWidth(), 25.0f); + EXPECT_FLOAT_EQ (trimmedRight.getHeight(), 40.0f); + // Test withTrimmedTop Rectangle trimmedTop = r.withTrimmedTop (5.0f); + EXPECT_FLOAT_EQ (trimmedTop.getX(), 10.0f); EXPECT_FLOAT_EQ (trimmedTop.getY(), 25.0f); - EXPECT_FLOAT_EQ (trimmedTop.getHeight(), 40.0f); + EXPECT_FLOAT_EQ (trimmedTop.getWidth(), 30.0f); + EXPECT_FLOAT_EQ (trimmedTop.getHeight(), 35.0f); + // Test withTrimmedBottom Rectangle trimmedBottom = r.withTrimmedBottom (5.0f); + EXPECT_FLOAT_EQ (trimmedBottom.getX(), 10.0f); EXPECT_FLOAT_EQ (trimmedBottom.getY(), 20.0f); - EXPECT_FLOAT_EQ (trimmedBottom.getHeight(), 40.0f); + EXPECT_FLOAT_EQ (trimmedBottom.getWidth(), 30.0f); + EXPECT_FLOAT_EQ (trimmedBottom.getHeight(), 35.0f); +} + +TEST (RectangleTests, TrimmedMethods_EdgeCases) +{ + Rectangle r (10.0f, 20.0f, 30.0f, 40.0f); + + // Test trimming more than available + Rectangle trimmedLeftLarge = r.withTrimmedLeft (40.0f); + EXPECT_FLOAT_EQ (trimmedLeftLarge.getX(), 50.0f); + EXPECT_FLOAT_EQ (trimmedLeftLarge.getWidth(), 0.0f); + + Rectangle trimmedRightLarge = r.withTrimmedRight (40.0f); + EXPECT_FLOAT_EQ (trimmedRightLarge.getWidth(), 0.0f); + + Rectangle trimmedTopLarge = r.withTrimmedTop (50.0f); + EXPECT_FLOAT_EQ (trimmedTopLarge.getY(), 70.0f); + EXPECT_FLOAT_EQ (trimmedTopLarge.getHeight(), 0.0f); + + Rectangle trimmedBottomLarge = r.withTrimmedBottom (50.0f); + EXPECT_FLOAT_EQ (trimmedBottomLarge.getHeight(), 0.0f); + + // Test negative trimming + Rectangle trimmedLeftNeg = r.withTrimmedLeft (-5.0f); + EXPECT_FLOAT_EQ (trimmedLeftNeg.getX(), 5.0f); + EXPECT_FLOAT_EQ (trimmedLeftNeg.getWidth(), 35.0f); } TEST (RectangleTests, CornerMethods_EdgeCases) @@ -1166,3 +1324,189 @@ TEST (RectangleTests, IntersectionArea_Calculations) // Union area should equal total area minus overlap EXPECT_FLOAT_EQ (totalArea, 175.0f); // 100 + 100 - 25 } + +TEST (RectangleTests, AspectRatio_Methods) +{ + // Test square rectangle + Rectangle square (0, 0, 10, 10); + EXPECT_FLOAT_EQ (square.widthOverHeightRatio(), 1.0f); + EXPECT_FLOAT_EQ (square.heightOverWidthRatio(), 1.0f); + EXPECT_EQ (square.aspectRatio(), (std::make_tuple (1, 1))); + + // Test wider rectangle (16:9 aspect ratio) + Rectangle wide (0, 0, 1920, 1080); + EXPECT_FLOAT_EQ (wide.widthOverHeightRatio(), 1920.0f / 1080.0f); + EXPECT_FLOAT_EQ (wide.heightOverWidthRatio(), 1080.0f / 1920.0f); + EXPECT_EQ (wide.aspectRatio(), (std::make_tuple (16, 9))); + + // Test taller rectangle (9:16 aspect ratio) + Rectangle tall (0, 0, 1080, 1920); + EXPECT_FLOAT_EQ (tall.widthOverHeightRatio(), 1080.0f / 1920.0f); + EXPECT_FLOAT_EQ (tall.heightOverWidthRatio(), 1920.0f / 1080.0f); + EXPECT_EQ (tall.aspectRatio(), (std::make_tuple (9, 16))); + + // Test invalid rectangles + Rectangle emptyWidth (0, 0, 10, 0); + EXPECT_EQ (emptyWidth.aspectRatio(), (std::make_tuple (0, 0))); + Rectangle emptyHeight (0, 0, 0, 10); + EXPECT_EQ (emptyHeight.aspectRatio(), (std::make_tuple (0, 0))); + Rectangle emptyAll (0, 0, 0, 0); + EXPECT_EQ (emptyAll.aspectRatio(), (std::make_tuple (0, 0))); +} + +TEST (RectangleTests, AspectRatio_KeepingMethods) +{ + Rectangle r (10.0f, 20.0f, 100.0f, 50.0f); // 2:1 aspect ratio + + // Test withWidthKeepingAspectRatio + Rectangle newWidth = r.withWidthKeepingAspectRatio (200.0f); + EXPECT_FLOAT_EQ (newWidth.getWidth(), 200.0f); + EXPECT_FLOAT_EQ (newWidth.getHeight(), 100.0f); // Maintains 2:1 ratio + EXPECT_FLOAT_EQ (newWidth.getX(), 10.0f); + EXPECT_FLOAT_EQ (newWidth.getY(), 20.0f); + + // Test withHeightKeepingAspectRatio + Rectangle newHeight = r.withHeightKeepingAspectRatio (100.0f); + EXPECT_FLOAT_EQ (newHeight.getWidth(), 200.0f); // Maintains 2:1 ratio + EXPECT_FLOAT_EQ (newHeight.getHeight(), 100.0f); + EXPECT_FLOAT_EQ (newHeight.getX(), 10.0f); + EXPECT_FLOAT_EQ (newHeight.getY(), 20.0f); +} + +TEST (RectangleTests, AspectRatio_EdgeCases) +{ + // Test with zero height + Rectangle zeroHeight (0.0f, 0.0f, 10.0f, 0.0f); + EXPECT_TRUE (std::isinf (zeroHeight.widthOverHeightRatio())); + EXPECT_FLOAT_EQ (zeroHeight.heightOverWidthRatio(), 0.0f); + + // Test with zero width + Rectangle zeroWidth (0.0f, 0.0f, 0.0f, 10.0f); + EXPECT_FLOAT_EQ (zeroWidth.widthOverHeightRatio(), 0.0f); + EXPECT_TRUE (std::isinf (zeroWidth.heightOverWidthRatio())); + + // Test withWidthKeepingAspectRatio with zero height + Rectangle newFromZeroHeight = zeroHeight.withWidthKeepingAspectRatio (20.0f); + EXPECT_FLOAT_EQ (newFromZeroHeight.getWidth(), 20.0f); + EXPECT_FLOAT_EQ (newFromZeroHeight.getHeight(), 0.0f); +} + +TEST (RectangleTests, Proportion_Methods) +{ + Rectangle r (0.0f, 0.0f, 100.0f, 200.0f); + + // Test proportionOfWidth + EXPECT_FLOAT_EQ (r.proportionOfWidth (0.0f), 0.0f); + EXPECT_FLOAT_EQ (r.proportionOfWidth (0.5f), 50.0f); + EXPECT_FLOAT_EQ (r.proportionOfWidth (1.0f), 100.0f); + EXPECT_FLOAT_EQ (r.proportionOfWidth (2.0f), 200.0f); + + // Test proportionOfHeight + EXPECT_FLOAT_EQ (r.proportionOfHeight (0.0f), 0.0f); + EXPECT_FLOAT_EQ (r.proportionOfHeight (0.5f), 100.0f); + EXPECT_FLOAT_EQ (r.proportionOfHeight (1.0f), 200.0f); + EXPECT_FLOAT_EQ (r.proportionOfHeight (2.0f), 400.0f); +} + +TEST (RectangleTests, SetBounds) +{ + Rectangle r (1.0f, 2.0f, 3.0f, 4.0f); + + r.setBounds (10.0f, 20.0f, 30.0f, 40.0f); + EXPECT_FLOAT_EQ (r.getX(), 10.0f); + EXPECT_FLOAT_EQ (r.getY(), 20.0f); + EXPECT_FLOAT_EQ (r.getWidth(), 30.0f); + EXPECT_FLOAT_EQ (r.getHeight(), 40.0f); +} + +TEST (RectangleTests, Contains_Line) +{ + Rectangle r (10.0f, 10.0f, 20.0f, 20.0f); + + // Test line completely inside + Line inside (15.0f, 15.0f, 20.0f, 20.0f); + EXPECT_TRUE (r.contains (inside)); + + // Test line on edges + Line topEdge (10.0f, 10.0f, 30.0f, 10.0f); + EXPECT_TRUE (r.contains (topEdge)); + + Line leftEdge (10.0f, 10.0f, 10.0f, 30.0f); + EXPECT_TRUE (r.contains (leftEdge)); + + // Test line partially outside + Line partiallyOutside (15.0f, 15.0f, 35.0f, 35.0f); + EXPECT_FALSE (r.contains (partiallyOutside)); + + // Test line completely outside + Line outside (0.0f, 0.0f, 5.0f, 5.0f); + EXPECT_FALSE (r.contains (outside)); + + // Test diagonal line inside + Line diagonal (10.0f, 10.0f, 30.0f, 30.0f); + EXPECT_TRUE (r.contains (diagonal)); +} + +TEST (RectangleTests, ToString) +{ + Rectangle r (1.5f, 2.5f, 3.5f, 4.5f); + String str = r.toString(); + EXPECT_EQ (str, "1.5, 2.5, 3.5, 4.5"); + + Rectangle rInt (1, 2, 3, 4); + String strInt = rInt.toString(); + EXPECT_EQ (strInt, "1, 2, 3, 4"); +} + +TEST (RectangleTests, Empty_Rectangle_Operations) +{ + Rectangle empty; + Rectangle normal (10.0f, 10.0f, 20.0f, 20.0f); + + // Test intersection with empty + Rectangle intersectionResult = empty.intersection (normal); + EXPECT_TRUE (intersectionResult.isEmpty()); + + intersectionResult = normal.intersection (empty); + EXPECT_TRUE (intersectionResult.isEmpty()); + + // Test intersects with empty + EXPECT_FALSE (empty.intersects (normal)); + EXPECT_FALSE (normal.intersects (empty)); + + // Test contains with empty + EXPECT_FALSE (empty.contains (normal)); + EXPECT_FALSE (normal.contains (empty)); // Empty rectangle is not contained anywhere + + // Test unionWith empty + Rectangle unionResult = empty.unionWith (normal); + EXPECT_EQ (unionResult, normal); + + unionResult = normal.unionWith (empty); + EXPECT_EQ (unionResult, normal); + + // Test area + EXPECT_FLOAT_EQ (empty.area(), 0.0f); + + // Test aspect ratios with empty + EXPECT_TRUE (std::isnan (empty.widthOverHeightRatio()) || std::isinf (empty.widthOverHeightRatio())); +} + +TEST (RectangleTests, Self_Operations) +{ + Rectangle r (10.0f, 10.0f, 20.0f, 20.0f); + + // Test intersection with self + Rectangle selfIntersection = r.intersection (r); + EXPECT_EQ (selfIntersection, r); + + // Test intersects with self + EXPECT_TRUE (r.intersects (r)); + + // Test contains self + EXPECT_TRUE (r.contains (r)); + + // Test unionWith self + Rectangle selfUnion = r.unionWith (r); + EXPECT_EQ (selfUnion, r); +}