Skip to content

Instantly share code, notes, and snippets.

@delaneyj
Last active June 30, 2025 20:32
Show Gist options
  • Save delaneyj/62c96507f1e609a4c45dca30f3c73d3a to your computer and use it in GitHub Desktop.
Save delaneyj/62c96507f1e609a4c45dca30f3c73d3a to your computer and use it in GitHub Desktop.
take home test

Audio Game Programmer Take Home - Synthesizer Project

This is a C++ synthesizer project built with FMOD for audio processing. It includes a command-line interface for controlling various synthesizer parameters.

Project Structure

takehome/
├── src/               # Source code
├── tests/             # Unit tests with mocks
├── lib/fmod/          # FMOD library (not included in gist)
├── CMakeLists.txt     # CMake configuration
├── Taskfile.yml       # Task runner configuration
└── README.md          # Project documentation

File: README.md

# FMOD Synthesizer

A simple command-line synthesizer built with FMOD for C++17.

## Features

- Multiple waveforms: sine, square, triangle, sawtooth (up/down), and white noise
- LFO (Low Frequency Oscillator) for pitch modulation
- Interactive CLI interface
- Real-time parameter control
- Cross-platform support (Linux, Windows, macOS)

## Prerequisites

- FMOD Core API (version 2.02.x or compatible - included in lib/fmod)
- C++20 compatible compiler:
  - Linux: Clang 20 (recommended) or GCC 11+
  - Windows: MSVC 2019+, MinGW-w64, or Clang
  - macOS: Xcode/Apple Clang or LLVM Clang
- CMake 3.10 or higher

## Building

### Linux

#### Using Taskfile (Recommended)

1. Install Task (https://taskfile.dev):
   ```bash
   sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d
  1. Build and run:
    task build-clang  # Build with Clang (recommended)
    task build        # Build with default compiler
    task run          # Run the synthesizer
    task test         # Run with test commands
    task check-fmod   # Verify FMOD installation

Manual Build

# With Clang (recommended)
CC=clang-20 CXX=clang++-20 cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

# With default compiler
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

# Run
./build/synth

Windows

Using Visual Studio

  1. Run the build script:

    build-windows.bat

    Or manually:

    mkdir build
    cd build
    cmake -G "Visual Studio 17 2022" -A x64 ..
    cmake --build . --config Release
  2. Run the synthesizer:

    build\Release\synth.exe

Using MinGW

  1. Run the MinGW build script:

    build-windows-mingw.bat

    Or manually:

    mkdir build
    cd build
    cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release ..
    mingw32-make
  2. Run the synthesizer:

    build\synth.exe

Using CMake Presets (CMake 3.20+)

# List available presets
cmake --list-presets

# Build with Visual Studio
cmake --preset=windows-msvc-release
cmake --build --preset=windows-msvc-release

# Build with MinGW
cmake --preset=windows-mingw-release
cmake --build --preset=windows-mingw-release

macOS

# Using default Apple Clang
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

# Run
./build/synth

FMOD Setup

The FMOD library is included in lib/fmod/ directory. If you need to download a different version:

  1. Download FMOD Core API from https://www.fmod.com/download
  2. Extract to lib/fmod/ directory in project root
  3. Directory structure should be:
    lib/fmod/
    ├── api/
    │   └── core/
    │       ├── inc/          (headers)
    │       └── lib/
    │           ├── x86_64/   (Linux .so files)
    │           ├── x64/      (Windows .lib/.dll files)
    │           └── ...
    

Windows-specific Notes

  • The build system automatically copies the appropriate FMOD DLL to the output directory
  • Use fmod.dll for Release builds and fmodL.dll for Debug builds
  • Ensure the DLL architecture (x86/x64) matches your build target

Task Commands

  • task - Show all available tasks
  • task build-clang - Build with Clang (recommended)
  • task build - Build with default compiler
  • task run - Build and run
  • task clean - Clean build artifacts
  • task rebuild - Clean and rebuild
  • task debug - Build with debug symbols
  • task test - Run with test commands
  • task play - Play a tone (default: 440 Hz for 2 seconds)
  • task play FREQ=8143 DURATION=5 - Play 8143 Hz for 5 seconds
  • task check-fmod - Check FMOD installation
  • task dev - Watch files and rebuild (requires entr)
  • task format - Format code (requires clang-format)
  • task lint - Run static analysis (requires clang-tidy)

Usage

The synthesizer provides an interactive command-line interface. Available commands:

  • play <frequency> - Play a note at the specified frequency (Hz)
  • stop - Stop playing
  • wave <type> - Change waveform (sine, square, triangle, noise, sawup, sawdown)
  • volume <0-1> - Set volume (0.0 to 1.0)
  • lfo <rate> <depth> - Enable LFO with rate (0.1-20 Hz) and depth (0-1)
  • lfo off - Disable LFO
  • lfo wave <type> - Set LFO waveform (sine, triangle, square, sawtooth)
  • status - Show current settings
  • help - Display help message
  • quit - Exit the program

Example Session

FMOD Synthesizer v1.0
Type 'help' for commands

> play 440
Playing at 440 Hz
> wave square
Waveform set to square
> lfo 5 0.1
LFO enabled: rate=5 Hz, depth=0.1
> volume 0.5
Volume set to 0.5
> stop
Stopped
> quit
Goodbye!

LFO Details

The LFO modulates the oscillator frequency using the formula: modulated_frequency = base_frequency * (1 + lfo_output * depth)

  • Rate: Controls how fast the LFO oscillates (0.1-20 Hz)
  • Depth: Controls how much the pitch varies (0-1, where 1 = 100% modulation)
  • The LFO supports sine, triangle, square, and sawtooth waveforms

Technical Notes

  • Uses FMOD's built-in DSP oscillator (FMOD_DSP_TYPE_OSCILLATOR)
  • LFO is implemented manually with a phase accumulator for precise control
  • Update rate is approximately 100 Hz for smooth modulation
  • All audio processing happens on the main thread for simplicity

C++20 Features Used

  • std::numbers for mathematical constants
  • std::format for string formatting
  • std::ranges algorithms
  • std::from_chars for float parsing
  • consteval for compile-time computation
  • std::chrono literals
  • <concepts> header (prepared for future use)
  • Designated initializers
  • [[maybe_unused]] attributes

Troubleshooting

Windows

  • Missing MSVCP140.dll or similar: Install the latest Visual C++ Redistributables from Microsoft
  • FMOD.dll not found: The build system should copy it automatically. If not, manually copy fmod.dll (or fmodL.dll for Debug) from lib/fmod/api/core/lib/x64/ to your executable directory
  • LNK2019 linker errors: Ensure you're building for the correct architecture (x64 vs x86) that matches the FMOD libraries
  • CMake can't find FMOD: Check that the FMOD directory structure matches what's expected (see FMOD Setup section)

Linux

  • FMOD library not found: Run task check-fmod to verify installation. Ensure .so files have execute permissions
  • std::format not found: Some older compilers don't fully support std::format. Use Clang 20+ or GCC 13+
  • GLIBCXX version errors: Update your GCC/libstdc++ or use Clang with the provided build scripts

General

  • No sound output: Check your system volume and ensure FMOD initialized correctly (check console for error messages)
  • Crackling or distorted audio: Try reducing the LFO depth or adjusting the volume
  • High CPU usage: The synthesizer runs at ~100Hz update rate. This is normal for real-time audio

---

## File: CLAUDE.md

```markdown
## Code Guidelines

- Use all C++20 features, not just compliance
- Prefer enum class over traditional enum for type safety and scoping
- don't use templates where a non template will suffice
- compile with warnings as errors

## Testing

- fix any errors found when running tests

File: CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(FMODSynthesizer)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Enable all warnings and treat as errors
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    add_compile_options(-Wall -Wextra -Wpedantic -Werror)
    
    # Use libc++ with Clang by default
    if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
        option(USE_LIBCXX "Use libc++ instead of libstdc++" OFF)
        if(USE_LIBCXX)
            add_compile_options(-stdlib=libc++)
            add_link_options(-stdlib=libc++)
        endif()
    endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    add_compile_options(/W4 /WX)
    # Define NOMINMAX to avoid Windows.h min/max macros
    add_definitions(-DNOMINMAX)
    # Use multi-threaded runtime
    set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

# Use local FMOD library
set(FMOD_ROOT ${CMAKE_SOURCE_DIR}/lib/fmod)
set(FMOD_INCLUDE_DIR ${FMOD_ROOT}/api/core/inc)

# Detect platform and architecture
if(WIN32)
    if(CMAKE_SIZEOF_VOID_P EQUAL 8)
        set(FMOD_ARCH "x64")
    else()
        set(FMOD_ARCH "x86")
    endif()
    set(FMOD_LIB_SUFFIX "_vc.lib")
    set(FMOD_DLL_SUFFIX ".dll")
else()
    if(CMAKE_SIZEOF_VOID_P EQUAL 8)
        set(FMOD_ARCH "x86_64")
    else()
        set(FMOD_ARCH "x86")
    endif()
    set(FMOD_LIB_SUFFIX "")
    set(FMOD_DLL_SUFFIX "")
endif()

# Find FMOD library
if(WIN32)
    find_library(FMOD_LIBRARY
        NAMES fmod${FMOD_LIB_SUFFIX} fmodL${FMOD_LIB_SUFFIX}
        PATHS ${FMOD_ROOT}/api/core/lib/${FMOD_ARCH}
        NO_DEFAULT_PATH
    )
else()
    find_library(FMOD_LIBRARY
        NAMES fmod fmodL
        PATHS ${FMOD_ROOT}/api/core/lib/${FMOD_ARCH}
        NO_DEFAULT_PATH
    )
endif()

if(NOT FMOD_LIBRARY)
    message(FATAL_ERROR "FMOD library not found in ${FMOD_ROOT}/api/core/lib/${FMOD_ARCH}")
endif()

message(STATUS "Found FMOD: ${FMOD_LIBRARY}")
message(STATUS "FMOD include: ${FMOD_INCLUDE_DIR}")

# Option to build tests
option(BUILD_TESTS "Build tests" ON)

# Source files for library
set(LIB_SOURCES
    src/Synthesizer.cpp
    src/LFO.cpp
    src/CommandParser.cpp
)

# Create library for testing
add_library(synth_lib STATIC ${LIB_SOURCES})
target_include_directories(synth_lib PUBLIC 
    ${CMAKE_SOURCE_DIR}/src
    ${FMOD_INCLUDE_DIR}
)
if(WIN32)
    target_link_libraries(synth_lib ${FMOD_LIBRARY})
else()
    target_link_libraries(synth_lib ${FMOD_LIBRARY} pthread)
endif()

# Add coverage flags to library when building tests
if(BUILD_TESTS AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    target_compile_options(synth_lib PRIVATE -fprofile-instr-generate -fcoverage-mapping)
    target_link_options(synth_lib PRIVATE -fprofile-instr-generate -fcoverage-mapping)
endif()

# Create executable
add_executable(synth src/main.cpp)
target_link_libraries(synth synth_lib)

# Include directories
target_include_directories(synth PRIVATE 
    ${CMAKE_SOURCE_DIR}/src
    ${FMOD_INCLUDE_DIR}
)

# Copy FMOD library to build directory (for runtime)
if(WIN32)
    # On Windows, we need to copy the DLL, not the import library
    set(FMOD_DLL_NAME "fmod.dll")
    if(CMAKE_BUILD_TYPE STREQUAL "Debug")
        set(FMOD_DLL_NAME "fmodL.dll")
    endif()
    
    set(FMOD_DLL_PATH "${FMOD_ROOT}/api/core/lib/${FMOD_ARCH}/${FMOD_DLL_NAME}")
    
    if(EXISTS ${FMOD_DLL_PATH})
        add_custom_command(TARGET synth POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
            ${FMOD_DLL_PATH}
            $<TARGET_FILE_DIR:synth>
        )
    endif()
else()
    # On Linux/Unix, copy the shared library
    if(EXISTS ${FMOD_LIBRARY})
        add_custom_command(TARGET synth POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
            ${FMOD_LIBRARY}
            $<TARGET_FILE_DIR:synth>
        )
    endif()
endif()

# Enable testing
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

File: CMakePresets.json

{
    "version": 3,
    "cmakeMinimumRequired": {
        "major": 3,
        "minor": 20,
        "patch": 0
    },
    "configurePresets": [
        {
            "name": "default",
            "hidden": true,
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build/${presetName}",
            "cacheVariables": {
                "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
            }
        },
        {
            "name": "windows-base",
            "hidden": true,
            "inherits": "default",
            "condition": {
                "type": "equals",
                "lhs": "${hostSystemName}",
                "rhs": "Windows"
            }
        },
        {
            "name": "windows-msvc-debug",
            "displayName": "Windows MSVC Debug",
            "inherits": "windows-base",
            "generator": "Visual Studio 17 2022",
            "architecture": {
                "value": "x64",
                "strategy": "set"
            },
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug"
            }
        },
        {
            "name": "windows-msvc-release",
            "displayName": "Windows MSVC Release",
            "inherits": "windows-base",
            "generator": "Visual Studio 17 2022",
            "architecture": {
                "value": "x64",
                "strategy": "set"
            },
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        },
        {
            "name": "windows-mingw-debug",
            "displayName": "Windows MinGW Debug",
            "inherits": "windows-base",
            "generator": "MinGW Makefiles",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug"
            }
        },
        {
            "name": "windows-mingw-release",
            "displayName": "Windows MinGW Release",
            "inherits": "windows-base",
            "generator": "MinGW Makefiles",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        },
        {
            "name": "linux-gcc-debug",
            "displayName": "Linux GCC Debug",
            "inherits": "default",
            "condition": {
                "type": "equals",
                "lhs": "${hostSystemName}",
                "rhs": "Linux"
            },
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug",
                "CMAKE_C_COMPILER": "gcc",
                "CMAKE_CXX_COMPILER": "g++"
            }
        },
        {
            "name": "linux-gcc-release",
            "displayName": "Linux GCC Release",
            "inherits": "default",
            "condition": {
                "type": "equals",
                "lhs": "${hostSystemName}",
                "rhs": "Linux"
            },
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release",
                "CMAKE_C_COMPILER": "gcc",
                "CMAKE_CXX_COMPILER": "g++"
            }
        },
        {
            "name": "linux-clang-debug",
            "displayName": "Linux Clang Debug",
            "inherits": "default",
            "condition": {
                "type": "equals",
                "lhs": "${hostSystemName}",
                "rhs": "Linux"
            },
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug",
                "CMAKE_C_COMPILER": "clang-20",
                "CMAKE_CXX_COMPILER": "clang++-20"
            }
        },
        {
            "name": "linux-clang-release",
            "displayName": "Linux Clang Release",
            "inherits": "default",
            "condition": {
                "type": "equals",
                "lhs": "${hostSystemName}",
                "rhs": "Linux"
            },
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release",
                "CMAKE_C_COMPILER": "clang-20",
                "CMAKE_CXX_COMPILER": "clang++-20"
            }
        }
    ],
    "buildPresets": [
        {
            "name": "windows-msvc-debug",
            "configurePreset": "windows-msvc-debug"
        },
        {
            "name": "windows-msvc-release",
            "configurePreset": "windows-msvc-release"
        },
        {
            "name": "windows-mingw-debug",
            "configurePreset": "windows-mingw-debug"
        },
        {
            "name": "windows-mingw-release",
            "configurePreset": "windows-mingw-release"
        },
        {
            "name": "linux-gcc-debug",
            "configurePreset": "linux-gcc-debug"
        },
        {
            "name": "linux-gcc-release",
            "configurePreset": "linux-gcc-release"
        },
        {
            "name": "linux-clang-debug",
            "configurePreset": "linux-clang-debug"
        },
        {
            "name": "linux-clang-release",
            "configurePreset": "linux-clang-release"
        }
    ]
}

File: Taskfile.yml

version: '3'

vars:
  BUILD_DIR: build
  EXECUTABLE: synth
  CMAKE_BUILD_TYPE: Release

tasks:
  default:
    desc: Show available tasks
    cmds:
      - task --list

  setup:
    desc: Create build directory and configure project
    cmds:
      - mkdir -p {{.BUILD_DIR}}
      - cd {{.BUILD_DIR}} && cmake -DCMAKE_BUILD_TYPE={{.CMAKE_BUILD_TYPE}} ..
  
  setup-clang:
    desc: Configure project with Clang
    vars:
      CLANG_VERSION: '{{.CLANG_VERSION | default "20"}}'
    cmds:
      - mkdir -p {{.BUILD_DIR}}
      - |
        cd {{.BUILD_DIR}} && \
        CC=clang-{{.CLANG_VERSION}} \
        CXX=clang++-{{.CLANG_VERSION}} \
        cmake -DCMAKE_BUILD_TYPE={{.CMAKE_BUILD_TYPE}} ..
    sources:
      - CMakeLists.txt
    generates:
      - '{{.BUILD_DIR}}/CMakeCache.txt'

  build:
    desc: Build the synthesizer
    deps:
      - setup
    cmds:
      - cd {{.BUILD_DIR}} && make -j$(nproc)
  
  build-clang:
    desc: Build with Clang
    deps:
      - setup-clang
    cmds:
      - cd {{.BUILD_DIR}} && make -j$(nproc)
    sources:
      - src/**/*.cpp
      - src/**/*.h
      - CMakeLists.txt
    generates:
      - '{{.BUILD_DIR}}/{{.EXECUTABLE}}'

  run:
    desc: Run the synthesizer
    deps:
      - build
    cmds:
      - cd {{.BUILD_DIR}} && ./{{.EXECUTABLE}}

  clean:
    desc: Clean build artifacts
    cmds:
      - rm -rf {{.BUILD_DIR}}
      - rm -f *.profraw *.profdata
      - find . -name "*.gcov" -o -name "*.gcda" -o -name "*.gcno" | xargs rm -f

  rebuild:
    desc: Clean and rebuild everything
    cmds:
      - task: clean
      - task: build

  debug:
    desc: Build with debug symbols
    cmds:
      - task: clean
      - CMAKE_BUILD_TYPE=Debug task setup
      - cd {{.BUILD_DIR}} && make -j$(nproc)

  test:
    desc: Run the synthesizer with test commands
    deps:
      - build-clang
    cmds:
      - |
        cd {{.BUILD_DIR}} && echo -e "help\nplay 440\nwave square\nlfo 5 0.1\nstatus\nstop\nquit" | ./{{.EXECUTABLE}}

  play:
    desc: Play a tone for a specified duration
    deps:
      - build
    vars:
      FREQ: '{{.FREQ | default "440"}}'
      DURATION: '{{.DURATION | default "2"}}'
    cmds:
      - |
        echo "Playing {{.FREQ}} Hz for {{.DURATION}} seconds..."
        cd {{.BUILD_DIR}} && (
          echo "play {{.FREQ}} {{.DURATION}}"
          sleep $(echo "{{.DURATION}} + 0.5" | bc)
          echo "quit"
        ) | ./{{.EXECUTABLE}}

  check-fmod:
    desc: Check if FMOD is properly configured
    cmds:
      - |
        echo "Checking FMOD installation..."
        FMOD_PATH="lib/fmod"
        if [ -f "$FMOD_PATH/api/core/inc/fmod.hpp" ]; then
          echo "✓ FMOD headers found at $FMOD_PATH/api/core/inc/"
        else
          echo "✗ FMOD headers not found at $FMOD_PATH/api/core/inc/"
        fi
        if [ -f "$FMOD_PATH/api/core/lib/x86_64/libfmod.so" ] || [ -f "$FMOD_PATH/api/core/lib/x86_64/libfmodL.so" ]; then
          echo "✓ FMOD library found at $FMOD_PATH/api/core/lib/x86_64/"
        else
          echo "✗ FMOD library not found at $FMOD_PATH/api/core/lib/x86_64/"
        fi

  dev:
    desc: Build and run with file watching (requires entr)
    deps:
      - build
    cmds:
      - |
        find src -name "*.cpp" -o -name "*.h" | entr -c task rebuild run

  format:
    desc: Format source code (requires clang-format)
    cmds:
      - find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i

  lint:
    desc: Run static analysis (requires clang-tidy)
    cmds:
      - |
        cd {{.BUILD_DIR}} && \
        find ../src -name "*.cpp" | xargs clang-tidy -p . --checks='-*,modernize-*,performance-*,readability-*'

  build-tests:
    desc: Build unit tests
    deps:
      - setup-clang
    cmds:
      - cd {{.BUILD_DIR}} && make -j$(nproc) synth_tests

  test-unit:
    desc: Run unit tests
    deps:
      - build-tests
    cmds:
      - cd {{.BUILD_DIR}} && ./tests/synth_tests

  coverage:
    desc: Run tests with coverage
    deps:
      - build-tests
    vars:
      LLVM_VERSION: '{{.LLVM_VERSION | default "20"}}'
    cmds:
      - cd {{.BUILD_DIR}} && LLVM_PROFILE_FILE="coverage-%p.profraw" ./tests/synth_tests
      - cd {{.BUILD_DIR}} && llvm-profdata-{{.LLVM_VERSION}} merge -sparse coverage-*.profraw -o coverage.profdata
      - cd {{.BUILD_DIR}} && llvm-cov-{{.LLVM_VERSION}} report ./tests/synth_tests -instr-profile=coverage.profdata

  coverage-html:
    desc: Generate HTML coverage report
    deps:
      - coverage
    vars:
      LLVM_VERSION: '{{.LLVM_VERSION | default "20"}}'
    cmds:
      - cd {{.BUILD_DIR}} && llvm-cov-{{.LLVM_VERSION}} show ./tests/synth_tests -instr-profile=coverage.profdata -format=html -output-dir=coverage-report
      - echo "Coverage report generated in build/coverage-report/index.html"

File: src/main.cpp

#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <array>
#include <iomanip>
#include <optional>
#include <algorithm>
#include <format>
#include <charconv>
#include "Synthesizer.h"
#include "CommandParser.h"

using namespace std::chrono_literals;

void printHelp() {
    constexpr auto helpText = R"(
FMOD Synthesizer Commands:
  play <frequency> [duration] - Play note at frequency (Hz) for duration (seconds)
  stop                       - Stop playing
  wave <type>                - Set waveform: sine, square, triangle, noise, sawup, sawdown
  volume <0-1>               - Set volume (0.0 to 1.0)
  lfo <rate> <depth>         - Set LFO (rate: 0.1-20Hz, depth: 0-1)
  lfo off                    - Disable LFO
  lfo wave <type>            - Set LFO waveform: sine, triangle, square, sawtooth
  status                     - Show current settings
  help                       - Show this help
  quit                       - Exit program

)";
    std::cout << helpText;
}

void printStatus(const Synthesizer& synth) {
    std::cout << "\nCurrent Status:\n";
    std::cout << std::format("  Playing: {}\n", synth.isPlaying() ? "Yes" : "No");
    if (synth.isPlaying()) {
        std::cout << std::format("  Frequency: {} Hz\n", synth.getCurrentFrequency());
    }
    
    constexpr std::array waveNames{"sine", "square", "sawup", "sawdown", "triangle", "noise"};
    std::cout << std::format("  Waveform: {}\n", waveNames[static_cast<int>(synth.getCurrentWaveform())]);
}

int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
    std::cout << "FMOD Synthesizer v1.0\n";
    std::cout << "Type 'help' for commands\n\n";
    
    Synthesizer synth;
    if (!synth.init()) {
        std::cerr << "Failed to initialize synthesizer\n";
        return 1;
    }
    
    auto lastTime = std::chrono::steady_clock::now();
    bool running = true;
    
    // For timed playback
    std::optional<std::chrono::steady_clock::time_point> playStartTime = std::nullopt;
    std::optional<float> playDuration = std::nullopt;
    
    // Ready for future command validation if needed
    
    // Set up fast I/O
    std::cin.tie(nullptr);
    std::ios_base::sync_with_stdio(false);
    
    while (running) {
        auto now = std::chrono::steady_clock::now();
        float deltaTime = std::chrono::duration<float>(now - lastTime).count();
        lastTime = now;
        
        synth.update(deltaTime);
        
        // Check for timed playback
        if (playStartTime.has_value() && playDuration.has_value() && synth.isPlaying()) {
            auto elapsed = std::chrono::duration<float>(now - *playStartTime).count();
            auto duration = *playDuration;
            if (elapsed >= duration) {
                synth.stop();
                std::cout << "Playback finished after " << duration << " seconds\n";
                playStartTime = std::nullopt;
                playDuration = std::nullopt;
            }
        }
        
        if (std::string line; std::getline(std::cin, line)) {
            if (auto cmdOpt = parseCommand(line); cmdOpt.has_value()) {
                const auto& [name, args] = *cmdOpt;
            
                if (name == "quit" || name == "exit") {
                running = false;
            }
                else if (name == "help") {
                printHelp();
            }
                else if (name == "play") {
                    if (args.empty()) {
                        std::cout << "Usage: play <frequency> [duration]\n";
                    } else {
                        float freq;
                        auto [ptr, ec] = std::from_chars(args[0].data(), args[0].data() + args[0].size(), freq);
                        if (ec == std::errc()) {
                            synth.play(freq);
                            playStartTime = std::chrono::steady_clock::now();
                            
                            if (args.size() > 1) {
                                float duration;
                                auto [ptr2, ec2] = std::from_chars(args[1].data(), args[1].data() + args[1].size(), duration);
                                if (ec2 == std::errc() && duration > 0) {
                                    playDuration = duration;
                                    std::cout << std::format("Playing at {} Hz for {} seconds\n", freq, duration);
                                } else {
                                    std::cout << std::format("Playing at {} Hz (invalid duration ignored)\n", freq);
                                    playDuration = std::nullopt;
                                }
                            } else {
                                std::cout << std::format("Playing at {} Hz\n", freq);
                                playDuration = std::nullopt;
                            }
                        } else {
                            std::cout << "Invalid frequency\n";
                        }
                    }
                }
                else if (name == "stop") {
                synth.stop();
                playStartTime = std::nullopt;
                playDuration = std::nullopt;
                std::cout << "Stopped\n";
            }
                else if (name == "wave") {
                    if (args.empty()) {
                    std::cout << "Usage: wave <sine|square|triangle|noise|sawup|sawdown>\n";
                } else {
                        auto waveType = args[0];
                    std::transform(waveType.begin(), waveType.end(), waveType.begin(), 
                                   [](unsigned char c) { return std::tolower(c); });
                    
                    if (auto waveformOpt = synth.parseWaveformType(waveType)) {
                        synth.setWaveform(*waveformOpt);
                        std::cout << "Waveform set to " << waveType << "\n";
                    } else {
                        std::cout << "Unknown waveform type\n";
                    }
                }
            }
                else if (name == "volume") {
                    if (args.empty()) {
                    std::cout << "Usage: volume <0-1>\n";
                } else {
                    float vol;
                        try {
                            vol = std::stof(args[0]);
                        synth.setVolume(vol);
                        std::cout << "Volume set to " << vol << "\n";
                        } catch (...) {
                            std::cout << "Invalid volume\n";
                        }
                }
            }
                else if (name == "lfo") {
                    if (args.empty()) {
                    std::cout << "Usage: lfo <rate> <depth> or lfo off or lfo wave <type>\n";
                    } else if (args[0] == "off") {
                    synth.enableLFO(false);
                    std::cout << "LFO disabled\n";
                    } else if (args[0] == "wave" && args.size() > 1) {
                        auto lfoWaveType = args[1];
                    std::transform(lfoWaveType.begin(), lfoWaveType.end(), lfoWaveType.begin(),
                                   [](unsigned char c) { return std::tolower(c); });
                    
                    static constexpr std::array<std::pair<std::string_view, LFO::WaveType>, 4> lfoWaveMap{{
                        {"sine", LFO::WaveType::Sine},
                        {"triangle", LFO::WaveType::Triangle},
                        {"square", LFO::WaveType::Square},
                        {"sawtooth", LFO::WaveType::Sawtooth}
                    }};
                    
                    if (auto it = std::find_if(lfoWaveMap.begin(), lfoWaveMap.end(),
                                               [&lfoWaveType](const auto& pair) { return pair.first == lfoWaveType; });
                        it != lfoWaveMap.end()) {
                        synth.setLFOWaveType(it->second);
                        std::cout << "LFO waveform set to " << lfoWaveType << "\n";
                    } else {
                        std::cout << "Unknown LFO waveform type\n";
                    }
                    } else if (args.size() >= 2) {
                        try {
                            float rate = std::stof(args[0]);
                            float depth = std::stof(args[1]);
                        synth.setLFORate(rate);
                        synth.setLFODepth(depth);
                        synth.enableLFO(true);
                        std::cout << "LFO enabled: rate=" << rate << " Hz, depth=" << depth << "\n";
                        } catch (...) {
                            std::cout << "Invalid LFO parameters\n";
                        }
                    }
            }
                else if (name == "status") {
                printStatus(synth);
            }
                else {
                    std::cout << "Unknown command. Type 'help' for commands.\n";
                }
            }
        }
        
        std::this_thread::sleep_for(10ms);
    }
    
    std::cout << "Goodbye!\n";
    return 0;
}

File: src/CommandParser.h

#pragma once
#include <string>
#include <string_view>
#include <vector>
#include <optional>

struct Command {
    std::string name;
    std::vector<std::string> args;
    
    // Three-way comparison not fully supported in libc++ 14
};

std::optional<Command> parseCommand(std::string_view line);

File: src/CommandParser.cpp

#include "CommandParser.h"
#include <sstream>
#include <algorithm>
#include <cctype>
#include <ranges>

std::optional<Command> parseCommand(std::string_view line) {
    std::istringstream iss{std::string(line)};
    Command cmd;
    
    if (!(iss >> cmd.name)) {
        return std::nullopt;
    }
    
    std::ranges::transform(cmd.name, cmd.name.begin(), 
                          [](unsigned char c) { return std::tolower(c); });
    
    std::string arg;
    while (iss >> arg) {
        cmd.args.push_back(std::move(arg));
    }
    
    return cmd;
}

File: src/Synthesizer.h

#pragma once
#include <fmod.hpp>
#include <string>
#include <memory>
#include <string_view>
#include <optional>
#include <concepts>
#include "LFO.h"

class Synthesizer {
public:
    enum class WaveformType : int {
        Sine = 0,
        Square = 1,
        Triangle = 4,
        Noise = 5,
        SawUp = 2,
        SawDown = 3
    };

    Synthesizer();
    ~Synthesizer();
    
    bool init();
    void shutdown();
    
    void play(float frequency);
    void stop();
    
    void setWaveform(WaveformType type);
    void setVolume(float volume);
    
    [[nodiscard]] std::optional<WaveformType> parseWaveformType(std::string_view name) const;
    
    void setLFORate(float rate);
    void setLFODepth(float depth);
    void setLFOWaveType(LFO::WaveType type);
    void enableLFO(bool enable);
    
    void update(float deltaTime);
    
    [[nodiscard]] bool isPlaying() const noexcept { return playing; }
    [[nodiscard]] float getCurrentFrequency() const noexcept { return baseFrequency; }
    [[nodiscard]] WaveformType getCurrentWaveform() const noexcept { return currentWaveform; }

private:
    FMOD::System* system{nullptr};
    FMOD::DSP* oscillator{nullptr};
    FMOD::Channel* channel{nullptr};
    FMOD::ChannelGroup* masterGroup{nullptr};
    
    LFO lfo;
    
    float baseFrequency{440.0f};
    float currentVolume{0.7f};
    bool playing{false};
    bool lfoEnabled{false};
    WaveformType currentWaveform{WaveformType::Sine};
};

File: src/Synthesizer.cpp

#include "Synthesizer.h"
#include <iostream>
#include <algorithm>
#include <array>
#include <utility>
#include <ranges>

Synthesizer::Synthesizer() = default;

Synthesizer::~Synthesizer() {
    shutdown();
}

bool Synthesizer::init() {
    FMOD_RESULT result;
    
    result = FMOD::System_Create(&system);
    if (result != FMOD_OK) {
        std::cerr << "Failed to create FMOD system: " << result << std::endl;
        return false;
    }
    
    result = system->init(32, FMOD_INIT_NORMAL, nullptr);
    if (result != FMOD_OK) {
        std::cerr << "Failed to initialize FMOD system: " << result << std::endl;
        return false;
    }
    
    result = system->createDSPByType(FMOD_DSP_TYPE_OSCILLATOR, &oscillator);
    if (result != FMOD_OK) {
        std::cerr << "Failed to create oscillator DSP: " << result << std::endl;
        return false;
    }
    
    oscillator->setParameterInt(FMOD_DSP_OSCILLATOR_TYPE, static_cast<int>(currentWaveform));
    oscillator->setParameterFloat(FMOD_DSP_OSCILLATOR_RATE, baseFrequency);
    
    system->getMasterChannelGroup(&masterGroup);
    
    return true;
}

void Synthesizer::shutdown() {
    if (channel) {
        channel->stop();
        channel = nullptr;
    }
    
    if (oscillator) {
        oscillator->release();
        oscillator = nullptr;
    }
    
    if (system) {
        system->close();
        system->release();
        system = nullptr;
    }
}

void Synthesizer::play(float frequency) {
    if (!system || !oscillator) return;
    
    baseFrequency = std::clamp(frequency, 20.0f, 20000.0f);
    
    if (channel) {
        channel->stop();
    }
    
    oscillator->setParameterFloat(FMOD_DSP_OSCILLATOR_RATE, baseFrequency);
    
    FMOD_RESULT result = system->playDSP(oscillator, masterGroup, false, &channel);
    if (result == FMOD_OK) {
        channel->setVolume(currentVolume);
        playing = true;
    } else {
        std::cerr << "Failed to play DSP: " << result << std::endl;
        playing = false;
    }
}

void Synthesizer::stop() {
    if (channel) {
        channel->stop();
        channel = nullptr;
        playing = false;
    }
}

void Synthesizer::setWaveform(WaveformType type) {
    currentWaveform = type;
    if (oscillator) {
        oscillator->setParameterInt(FMOD_DSP_OSCILLATOR_TYPE, static_cast<int>(type));
    }
}

void Synthesizer::setVolume(float volume) {
    currentVolume = std::clamp(volume, 0.0f, 1.0f);
    if (channel) {
        channel->setVolume(currentVolume);
    }
}

void Synthesizer::setLFORate(float rate) {
    lfo.setFrequency(rate);
}

void Synthesizer::setLFODepth(float depth) {
    lfo.setDepth(depth);
}

void Synthesizer::setLFOWaveType(LFO::WaveType type) {
    lfo.setWaveType(type);
}

void Synthesizer::enableLFO(bool enable) {
    lfoEnabled = enable;
    if (!enable) {
        lfo.reset();
    }
}

void Synthesizer::update(float deltaTime) {
    if (!system) return;
    
    if (playing && lfoEnabled) {
        if (auto lfoValue = lfo.update(deltaTime); oscillator) {
            auto modulatedFreq = std::clamp(baseFrequency * (1.0f + lfoValue), 20.0f, 20000.0f);
            oscillator->setParameterFloat(FMOD_DSP_OSCILLATOR_RATE, modulatedFreq);
        }
    }
    
    system->update();
}

std::optional<Synthesizer::WaveformType> Synthesizer::parseWaveformType(std::string_view name) const {
    static constexpr std::array<std::pair<std::string_view, WaveformType>, 6> waveformMap{{
        {"sine", WaveformType::Sine},
        {"square", WaveformType::Square},
        {"triangle", WaveformType::Triangle},
        {"noise", WaveformType::Noise},
        {"sawup", WaveformType::SawUp},
        {"sawdown", WaveformType::SawDown}
    }};
    
    auto it = std::ranges::find_if(waveformMap, [name](const auto& pair) { 
        return pair.first == name; 
    });
    
    if (it != waveformMap.end()) {
        return it->second;
    }
    return std::nullopt;
}

File: src/LFO.h

#pragma once
#include <cmath>
#include <array>
#include <numbers>
#include <concepts>

class LFO {
public:
    enum class WaveType {
        Sine,
        Triangle,
        Square,
        Sawtooth
    };

    LFO();
    
    float update(float deltaTime);
    
    void setFrequency(float freq);
    void setDepth(float d);
    void setWaveType(WaveType type);
    
    [[nodiscard]] float getFrequency() const noexcept { return frequency; }
    [[nodiscard]] float getDepth() const noexcept { return depth; }
    [[nodiscard]] WaveType getWaveType() const noexcept { return waveType; }
    
    void reset();

private:
    float phase{0.0f};
    float frequency{5.0f};
    float depth{0.0f};
    WaveType waveType{WaveType::Sine};
    
    [[nodiscard]] float generateWaveform() const noexcept;
    
    static consteval float twoPi() noexcept { return 2.0f * std::numbers::pi_v<float>; }
};

File: src/LFO.cpp

#include "LFO.h"
#include <algorithm>
#include <cmath>
#include <numbers>

LFO::LFO() = default;

float LFO::update(float deltaTime) {
    phase += frequency * deltaTime * twoPi();
    
    while (phase >= twoPi()) {
        phase -= twoPi();
    }
    
    return generateWaveform() * depth;
}

void LFO::setFrequency(float freq) {
    frequency = std::clamp(freq, 0.1f, 20.0f);
}

void LFO::setDepth(float d) {
    depth = std::clamp(d, 0.0f, 1.0f);
}

void LFO::setWaveType(WaveType type) {
    waveType = type;
}

void LFO::reset() {
    phase = 0.0f;
}

float LFO::generateWaveform() const noexcept {
    switch (waveType) {
        case WaveType::Sine:
            return std::sin(phase);
            
        case WaveType::Triangle: {
            auto normalized = phase / twoPi();
            if (normalized < 0.25f) {
                return normalized * 4.0f;
            } else if (normalized < 0.75f) {
                return 2.0f - (normalized * 4.0f);
            } else {
                return (normalized * 4.0f) - 4.0f;
            }
        }
        
        case WaveType::Square:
            return phase < std::numbers::pi_v<float> ? 1.0f : -1.0f;
            
        case WaveType::Sawtooth: {
            auto normalized = phase / twoPi();
            return 2.0f * normalized - 1.0f;
        }
        
        default:
            return 0.0f;
    }
}

File: tests/CMakeLists.txt

# Find required packages
find_package(GTest REQUIRED)
if(NOT WIN32)
    find_package(Threads REQUIRED)
endif()

# Coverage flags for Clang
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    set(COVERAGE_FLAGS -fprofile-instr-generate -fcoverage-mapping)
endif()

# Test sources
set(TEST_SOURCES
    test_main.cpp
    test_lfo.cpp
    test_synthesizer.cpp
    test_command_parser.cpp
)

# Create test executable
add_executable(synth_tests ${TEST_SOURCES})

# Set coverage flags and stdlib
target_compile_options(synth_tests PRIVATE ${COVERAGE_FLAGS})
target_link_options(synth_tests PRIVATE ${COVERAGE_FLAGS})

# Use same stdlib as main project
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND USE_LIBCXX)
    target_compile_options(synth_tests PRIVATE -stdlib=libc++)
    target_link_options(synth_tests PRIVATE -stdlib=libc++)
endif()

# Include directories
target_include_directories(synth_tests PRIVATE
    ${CMAKE_SOURCE_DIR}/src
    ${CMAKE_CURRENT_SOURCE_DIR}/mocks
    ${FMOD_INCLUDE_DIR}
)

# Link libraries
if(WIN32)
    target_link_libraries(synth_tests
        synth_lib
        GTest::gtest
        GTest::gmock
    )
else()
    target_link_libraries(synth_tests
        synth_lib
        GTest::gtest
        GTest::gmock
        Threads::Threads
    )
endif()


# Add test
add_test(NAME synth_tests COMMAND synth_tests)

# Set test properties for better output
set_tests_properties(synth_tests PROPERTIES
    ENVIRONMENT "LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/coverage-%p.profraw"
)

File: tests/test_main.cpp

#include <gtest/gtest.h>
#include <gmock/gmock.h>

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    ::testing::InitGoogleMock(&argc, argv);
    return RUN_ALL_TESTS();
}

File: tests/test_command_parser.cpp

#include <gtest/gtest.h>
#include "CommandParser.h"

class CommandParserTest : public ::testing::Test {
protected:
    void expectCommand(const std::string& input, 
                      const std::string& expectedName, 
                      const std::vector<std::string>& expectedArgs) {
        auto result = parseCommand(input);
        ASSERT_TRUE(result.has_value()) << "Failed to parse: " << input;
        EXPECT_EQ(result->name, expectedName);
        EXPECT_EQ(result->args, expectedArgs);
    }
    
    void expectNoCommand(const std::string& input) {
        auto result = parseCommand(input);
        EXPECT_FALSE(result.has_value()) << "Should not parse: " << input;
    }
};

// Test basic command parsing
TEST_F(CommandParserTest, SimpleCommands) {
    expectCommand("play", "play", {});
    expectCommand("stop", "stop", {});
    expectCommand("help", "help", {});
    expectCommand("quit", "quit", {});
    expectCommand("status", "status", {});
}

// Test commands with arguments
TEST_F(CommandParserTest, CommandsWithArgs) {
    expectCommand("play 440", "play", {"440"});
    expectCommand("play 440 5", "play", {"440", "5"});
    expectCommand("wave sine", "wave", {"sine"});
    expectCommand("volume 0.5", "volume", {"0.5"});
    expectCommand("lfo 5 0.1", "lfo", {"5", "0.1"});
    expectCommand("lfo off", "lfo", {"off"});
    expectCommand("lfo wave triangle", "lfo", {"wave", "triangle"});
}

// Test case insensitivity for command names
TEST_F(CommandParserTest, CaseInsensitivity) {
    expectCommand("PLAY", "play", {});
    expectCommand("Play", "play", {});
    expectCommand("pLaY", "play", {});
    expectCommand("STOP", "stop", {});
    expectCommand("WaVe sine", "wave", {"sine"});
}

// Test that arguments preserve case
TEST_F(CommandParserTest, ArgumentsCaseSensitive) {
    expectCommand("wave SINE", "wave", {"SINE"});
    expectCommand("wave SiNe", "wave", {"SiNe"});
}

// Test whitespace handling
TEST_F(CommandParserTest, WhitespaceHandling) {
    expectCommand("  play  ", "play", {});
    expectCommand("play   440", "play", {"440"});
    expectCommand("  play   440   5  ", "play", {"440", "5"});
    expectCommand("\tplay\t440", "play", {"440"});
    expectCommand("play\t\t440\t\t5", "play", {"440", "5"});
}

// Test empty and whitespace-only input
TEST_F(CommandParserTest, EmptyInput) {
    expectNoCommand("");
    expectNoCommand(" ");
    expectNoCommand("   ");
    expectNoCommand("\t");
    expectNoCommand("\n");
    expectNoCommand(" \t \n ");
}

// Test multiple arguments
TEST_F(CommandParserTest, MultipleArguments) {
    expectCommand("test a b c d e", "test", {"a", "b", "c", "d", "e"});
    expectCommand("cmd 1 2 3 4 5 6 7 8 9 10", "cmd", 
                  {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"});
}

// Test special characters in arguments
TEST_F(CommandParserTest, SpecialCharactersInArgs) {
    expectCommand("play 440.5", "play", {"440.5"});
    expectCommand("play -440", "play", {"-440"});
    expectCommand("test arg1=value", "test", {"arg1=value"});
    expectCommand("test path/to/file", "test", {"path/to/file"});
}

// Test very long commands
TEST_F(CommandParserTest, LongCommands) {
    std::string longArg(100, 'a');
    expectCommand("test " + longArg, "test", {longArg});
    
    std::string veryLongCommand = "test";
    std::vector<std::string> expectedArgs;
    for (int i = 0; i < 50; ++i) {
        veryLongCommand += " arg" + std::to_string(i);
        expectedArgs.push_back("arg" + std::to_string(i));
    }
    expectCommand(veryLongCommand, "test", expectedArgs);
}

// Test command-like arguments
TEST_F(CommandParserTest, CommandLikeArguments) {
    expectCommand("lfo wave play", "lfo", {"wave", "play"});
    expectCommand("test stop start", "test", {"stop", "start"});
}

// Test numeric command names
TEST_F(CommandParserTest, NumericCommands) {
    expectCommand("123", "123", {});
    expectCommand("123 456", "123", {"456"});
}

// Test quoted arguments (not supported, but should work as separate args)
TEST_F(CommandParserTest, QuotedArguments) {
    // Since we don't support quotes, they become part of the arguments
    expectCommand("test \"hello world\"", "test", {"\"hello", "world\""});
    expectCommand("test 'single quotes'", "test", {"'single", "quotes'"});
}

// Test edge case: command with no space before args
TEST_F(CommandParserTest, NoSpaceBeforeArgs) {
    // This should still work because >> operator handles it
    expectCommand("play440", "play440", {});
    // But this creates separate tokens
    expectCommand("play 440", "play", {"440"});
}

// Test Unicode/special characters
TEST_F(CommandParserTest, UnicodeCharacters) {
    // Basic ASCII should work
    expectCommand("test @#$%", "test", {"@#$%"});
    expectCommand("test ()", "test", {"()"});
    expectCommand("test []", "test", {"[]"});
}

// Test boundary conditions
TEST_F(CommandParserTest, BoundaryConditions) {
    // Single character command
    expectCommand("p", "p", {});
    expectCommand("p 1", "p", {"1"});
    
    // Single character arguments
    expectCommand("test a", "test", {"a"});
    expectCommand("test a b c", "test", {"a", "b", "c"});
}

// Test real-world command scenarios
TEST_F(CommandParserTest, RealWorldCommands) {
    // Play command variations
    expectCommand("play 440", "play", {"440"});
    expectCommand("play 440 2.5", "play", {"440", "2.5"});
    expectCommand("play 8143 5", "play", {"8143", "5"});
    
    // Wave command variations
    expectCommand("wave sine", "wave", {"sine"});
    expectCommand("wave square", "wave", {"square"});
    expectCommand("wave triangle", "wave", {"triangle"});
    
    // LFO command variations
    expectCommand("lfo 5 0.1", "lfo", {"5", "0.1"});
    expectCommand("lfo off", "lfo", {"off"});
    expectCommand("lfo wave sine", "lfo", {"wave", "sine"});
    
    // Volume command
    expectCommand("volume 0.5", "volume", {"0.5"});
    expectCommand("volume 1", "volume", {"1"});
    expectCommand("volume 0", "volume", {"0"});
}

File: tests/test_synthesizer.cpp

#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "Synthesizer.h"

using ::testing::_;
using ::testing::Return;
using ::testing::DoAll;
using ::testing::SetArgPointee;

// Test fixture for Synthesizer tests
class SynthesizerTest : public ::testing::Test {
protected:
    Synthesizer synth;
};

// Test parseWaveformType function
TEST_F(SynthesizerTest, ParseWaveformType_ValidTypes) {
    auto result = synth.parseWaveformType("sine");
    ASSERT_TRUE(result.has_value());
    EXPECT_EQ(result.value(), Synthesizer::WaveformType::Sine);
    
    result = synth.parseWaveformType("square");
    ASSERT_TRUE(result.has_value());
    EXPECT_EQ(result.value(), Synthesizer::WaveformType::Square);
    
    result = synth.parseWaveformType("triangle");
    ASSERT_TRUE(result.has_value());
    EXPECT_EQ(result.value(), Synthesizer::WaveformType::Triangle);
    
    result = synth.parseWaveformType("noise");
    ASSERT_TRUE(result.has_value());
    EXPECT_EQ(result.value(), Synthesizer::WaveformType::Noise);
    
    result = synth.parseWaveformType("sawup");
    ASSERT_TRUE(result.has_value());
    EXPECT_EQ(result.value(), Synthesizer::WaveformType::SawUp);
    
    result = synth.parseWaveformType("sawdown");
    ASSERT_TRUE(result.has_value());
    EXPECT_EQ(result.value(), Synthesizer::WaveformType::SawDown);
}

TEST_F(SynthesizerTest, ParseWaveformType_InvalidTypes) {
    auto result = synth.parseWaveformType("invalid");
    EXPECT_FALSE(result.has_value());
    
    result = synth.parseWaveformType("");
    EXPECT_FALSE(result.has_value());
    
    result = synth.parseWaveformType("SINE"); // Case sensitive
    EXPECT_FALSE(result.has_value());
}

TEST_F(SynthesizerTest, ParseWaveformType_EdgeCases) {
    auto result = synth.parseWaveformType("sin"); // Partial match
    EXPECT_FALSE(result.has_value());
    
    result = synth.parseWaveformType("sine "); // With space
    EXPECT_FALSE(result.has_value());
    
    result = synth.parseWaveformType(" sine"); // With space
    EXPECT_FALSE(result.has_value());
}

// Test initial state
TEST_F(SynthesizerTest, InitialState) {
    EXPECT_FALSE(synth.isPlaying());
    EXPECT_EQ(synth.getCurrentFrequency(), 440.0f);
    EXPECT_EQ(synth.getCurrentWaveform(), Synthesizer::WaveformType::Sine);
}

// Test LFO parameter setting
TEST_F(SynthesizerTest, LFOParameters) {
    // These should not crash even without FMOD initialized
    synth.setLFORate(5.0f);
    synth.setLFODepth(0.5f);
    synth.setLFOWaveType(LFO::WaveType::Triangle);
    synth.enableLFO(true);
    synth.enableLFO(false);
    
    // Test that methods complete without crashing
    EXPECT_TRUE(true);
}

// Test volume setting
TEST_F(SynthesizerTest, VolumeControl) {
    // Test normal range
    synth.setVolume(0.5f);
    // No way to get volume back without FMOD, but it shouldn't crash
    
    // Test clamping
    synth.setVolume(-0.5f); // Should clamp to 0
    synth.setVolume(1.5f);  // Should clamp to 1
    
    EXPECT_TRUE(true); // If we get here, no crash occurred
}

// Test waveform setting
TEST_F(SynthesizerTest, WaveformSetting) {
    // Test all waveform types
    synth.setWaveform(Synthesizer::WaveformType::Sine);
    EXPECT_EQ(synth.getCurrentWaveform(), Synthesizer::WaveformType::Sine);
    
    synth.setWaveform(Synthesizer::WaveformType::Square);
    EXPECT_EQ(synth.getCurrentWaveform(), Synthesizer::WaveformType::Square);
    
    synth.setWaveform(Synthesizer::WaveformType::Triangle);
    EXPECT_EQ(synth.getCurrentWaveform(), Synthesizer::WaveformType::Triangle);
    
    synth.setWaveform(Synthesizer::WaveformType::Noise);
    EXPECT_EQ(synth.getCurrentWaveform(), Synthesizer::WaveformType::Noise);
    
    synth.setWaveform(Synthesizer::WaveformType::SawUp);
    EXPECT_EQ(synth.getCurrentWaveform(), Synthesizer::WaveformType::SawUp);
    
    synth.setWaveform(Synthesizer::WaveformType::SawDown);
    EXPECT_EQ(synth.getCurrentWaveform(), Synthesizer::WaveformType::SawDown);
}

// Test frequency bounds
TEST_F(SynthesizerTest, FrequencyBounds) {
    // Without FMOD, play() won't work, but we can test the frequency tracking
    
    // The frequency should be stored even if FMOD isn't initialized
    // This tests the internal state management
    EXPECT_EQ(synth.getCurrentFrequency(), 440.0f); // Default
}

// Test update without crash
TEST_F(SynthesizerTest, UpdateWithoutCrash) {
    // Update should handle null FMOD system gracefully
    synth.update(0.016f); // ~60 FPS
    
    // Enable LFO and update
    synth.enableLFO(true);
    synth.setLFORate(5.0f);
    synth.setLFODepth(0.1f);
    synth.update(0.016f);
    
    EXPECT_TRUE(true); // If we get here, no crash occurred
}

// Test edge case: very high update rate
TEST_F(SynthesizerTest, HighUpdateRate) {
    synth.enableLFO(true);
    
    // Simulate very small time steps
    for (int i = 0; i < 1000; ++i) {
        synth.update(0.0001f); // 0.1ms steps
    }
    
    EXPECT_TRUE(true); // No crash
}

// Test edge case: very low update rate
TEST_F(SynthesizerTest, LowUpdateRate) {
    synth.enableLFO(true);
    
    // Simulate large time step
    synth.update(1.0f); // 1 second step
    
    EXPECT_TRUE(true); // No crash
}

File: tests/test_lfo.cpp

#include <gtest/gtest.h>
#include <cmath>
#include <numbers>
#include "LFO.h"

class LFOTest : public ::testing::Test {
protected:
    LFO lfo;
    
    static constexpr float EPSILON = 1e-6f;
    
    bool floatEquals(float a, float b) {
        return std::abs(a - b) < EPSILON;
    }
};

TEST_F(LFOTest, DefaultConstructor) {
    EXPECT_EQ(lfo.getFrequency(), 5.0f);
    EXPECT_EQ(lfo.getDepth(), 0.0f);
    EXPECT_EQ(lfo.getWaveType(), LFO::WaveType::Sine);
}

TEST_F(LFOTest, SetFrequency) {
    // Test normal values
    lfo.setFrequency(10.0f);
    EXPECT_EQ(lfo.getFrequency(), 10.0f);
    
    // Test clamping - below minimum
    lfo.setFrequency(0.05f);
    EXPECT_EQ(lfo.getFrequency(), 0.1f);
    
    // Test clamping - above maximum
    lfo.setFrequency(25.0f);
    EXPECT_EQ(lfo.getFrequency(), 20.0f);
    
    // Test edge cases
    lfo.setFrequency(0.1f);
    EXPECT_EQ(lfo.getFrequency(), 0.1f);
    
    lfo.setFrequency(20.0f);
    EXPECT_EQ(lfo.getFrequency(), 20.0f);
}

TEST_F(LFOTest, SetDepth) {
    // Test normal values
    lfo.setDepth(0.5f);
    EXPECT_EQ(lfo.getDepth(), 0.5f);
    
    // Test clamping - below minimum
    lfo.setDepth(-0.5f);
    EXPECT_EQ(lfo.getDepth(), 0.0f);
    
    // Test clamping - above maximum
    lfo.setDepth(1.5f);
    EXPECT_EQ(lfo.getDepth(), 1.0f);
    
    // Test edge cases
    lfo.setDepth(0.0f);
    EXPECT_EQ(lfo.getDepth(), 0.0f);
    
    lfo.setDepth(1.0f);
    EXPECT_EQ(lfo.getDepth(), 1.0f);
}

TEST_F(LFOTest, SetWaveType) {
    lfo.setWaveType(LFO::WaveType::Triangle);
    EXPECT_EQ(lfo.getWaveType(), LFO::WaveType::Triangle);
    
    lfo.setWaveType(LFO::WaveType::Square);
    EXPECT_EQ(lfo.getWaveType(), LFO::WaveType::Square);
    
    lfo.setWaveType(LFO::WaveType::Sawtooth);
    EXPECT_EQ(lfo.getWaveType(), LFO::WaveType::Sawtooth);
    
    lfo.setWaveType(LFO::WaveType::Sine);
    EXPECT_EQ(lfo.getWaveType(), LFO::WaveType::Sine);
}

TEST_F(LFOTest, Reset) {
    // Set up LFO with some values and advance it
    lfo.setFrequency(10.0f);
    lfo.setDepth(0.5f);
    lfo.update(0.1f); // Advance phase
    
    // Reset should bring phase back to 0
    lfo.reset();
    
    // First update should return 0 for sine wave (sin(0) = 0)
    float value = lfo.update(0.0f);
    EXPECT_NEAR(value, 0.0f, EPSILON);
}

TEST_F(LFOTest, SineWaveGeneration) {
    lfo.setWaveType(LFO::WaveType::Sine);
    lfo.setFrequency(1.0f); // 1 Hz for easy calculation
    lfo.setDepth(1.0f);
    
    // Test key points of sine wave
    // At t=0, phase=0, sin(0)=0
    EXPECT_NEAR(lfo.update(0.0f), 0.0f, EPSILON);
    
    // At t=0.25s, phase=π/2, sin(π/2)=1
    EXPECT_NEAR(lfo.update(0.25f), 1.0f, EPSILON);
    
    // At t=0.25s, phase=π, sin(π)=0
    EXPECT_NEAR(lfo.update(0.25f), 0.0f, EPSILON);
    
    // At t=0.25s, phase=3π/2, sin(3π/2)=-1
    EXPECT_NEAR(lfo.update(0.25f), -1.0f, EPSILON);
    
    // At t=0.25s, phase=2π (wraps to 0), sin(0)=0
    EXPECT_NEAR(lfo.update(0.25f), 0.0f, EPSILON);
}

TEST_F(LFOTest, SquareWaveGeneration) {
    lfo.setWaveType(LFO::WaveType::Square);
    lfo.setFrequency(1.0f);
    lfo.setDepth(1.0f);
    
    // First half of period should be 1
    EXPECT_EQ(lfo.update(0.0f), 1.0f);
    EXPECT_EQ(lfo.update(0.25f), 1.0f);
    
    // Second half of period should be -1
    EXPECT_EQ(lfo.update(0.25f), -1.0f);
    EXPECT_EQ(lfo.update(0.25f), -1.0f);
    
    // Back to 1 after full period
    EXPECT_EQ(lfo.update(0.25f), 1.0f);
}

TEST_F(LFOTest, TriangleWaveGeneration) {
    lfo.setWaveType(LFO::WaveType::Triangle);
    lfo.setFrequency(1.0f);
    lfo.setDepth(1.0f);
    
    // Triangle wave should go: 0 -> 1 -> 0 -> -1 -> 0
    EXPECT_NEAR(lfo.update(0.0f), 0.0f, EPSILON);      // Start at 0
    EXPECT_NEAR(lfo.update(0.25f), 1.0f, EPSILON);     // Peak at 0.25
    EXPECT_NEAR(lfo.update(0.25f), 0.0f, EPSILON);     // Back to 0 at 0.5
    EXPECT_NEAR(lfo.update(0.25f), -1.0f, EPSILON);    // Trough at 0.75
    EXPECT_NEAR(lfo.update(0.25f), 0.0f, EPSILON);     // Back to 0 at 1.0
}

TEST_F(LFOTest, SawtoothWaveGeneration) {
    lfo.setWaveType(LFO::WaveType::Sawtooth);
    lfo.setFrequency(1.0f);
    lfo.setDepth(1.0f);
    
    // Sawtooth should go from -1 to 1 linearly
    EXPECT_NEAR(lfo.update(0.0f), -1.0f, EPSILON);     // Start at -1
    EXPECT_NEAR(lfo.update(0.25f), -0.5f, EPSILON);    // Quarter way
    EXPECT_NEAR(lfo.update(0.25f), 0.0f, EPSILON);     // Half way
    EXPECT_NEAR(lfo.update(0.25f), 0.5f, EPSILON);     // Three quarters
    
    // Should jump back to -1 (with small phase advance)
    float value = lfo.update(0.24f);
    EXPECT_NEAR(value, 0.96f, 0.1f); // Close to 1
    
    value = lfo.update(0.01f); // Small step to wrap
    EXPECT_TRUE(value < -0.9f); // Should have wrapped to near -1
}

TEST_F(LFOTest, DepthScaling) {
    lfo.setWaveType(LFO::WaveType::Sine);
    lfo.setFrequency(1.0f);
    
    // Test with 50% depth
    lfo.setDepth(0.5f);
    lfo.reset();
    lfo.update(0.0f);
    EXPECT_NEAR(lfo.update(0.25f), 0.5f, EPSILON); // sin(π/2) * 0.5 = 0.5
    
    // Test with 0% depth
    lfo.setDepth(0.0f);
    lfo.reset();
    EXPECT_EQ(lfo.update(0.25f), 0.0f); // Should always return 0
}

TEST_F(LFOTest, PhaseWrapping) {
    lfo.setFrequency(10.0f); // 10 Hz
    lfo.setDepth(1.0f);
    
    // Run for multiple cycles to ensure phase wraps correctly
    for (int i = 0; i < 100; ++i) {
        float value = lfo.update(0.01f); // 10ms steps
        EXPECT_GE(value, -1.0f);
        EXPECT_LE(value, 1.0f);
    }
}

TEST_F(LFOTest, FrequencyAccuracy) {
    lfo.setWaveType(LFO::WaveType::Square);
    lfo.setFrequency(2.0f); // 2 Hz
    lfo.setDepth(1.0f);
    
    // Count transitions over 1 second
    int transitions = 0;
    float lastValue = lfo.update(0.0f);
    
    for (int i = 1; i <= 100; ++i) {
        float value = lfo.update(0.01f); // 10ms steps
        if (value != lastValue) {
            transitions++;
            lastValue = value;
        }
    }
    
    // Should have 4 transitions for 2 Hz square wave (2 full cycles)
    EXPECT_EQ(transitions, 4);
}

TEST_F(LFOTest, ZeroFrequency) {
    lfo.setFrequency(0.0f); // Should be clamped to 0.1f
    lfo.setDepth(1.0f);
    
    // Even at minimum frequency, phase should advance
    float value1 = lfo.update(0.0f);
    float value2 = lfo.update(1.0f); // 1 second later
    
    // Values should be different (phase advanced)
    EXPECT_NE(value1, value2);
}

File: tests/mocks/mock_fmod.h

#pragma once
#include <gmock/gmock.h>
#include <fmod.hpp>

// Mock FMOD classes for testing

class MockSystem : public FMOD::System {
public:
    MOCK_METHOD(FMOD_RESULT, init, (int maxchannels, FMOD_INITFLAGS flags, void* extradriverdata), (override));
    MOCK_METHOD(FMOD_RESULT, close, (), (override));
    MOCK_METHOD(FMOD_RESULT, release, (), (override));
    MOCK_METHOD(FMOD_RESULT, update, (), (override));
    MOCK_METHOD(FMOD_RESULT, createDSPByType, (FMOD_DSP_TYPE type, FMOD::DSP** dsp), (override));
    MOCK_METHOD(FMOD_RESULT, playDSP, (FMOD::DSP* dsp, FMOD::ChannelGroup* channelgroup, bool paused, FMOD::Channel** channel), (override));
    MOCK_METHOD(FMOD_RESULT, getMasterChannelGroup, (FMOD::ChannelGroup** channelgroup), (override));
};

class MockDSP : public FMOD::DSP {
public:
    MOCK_METHOD(FMOD_RESULT, release, (), (override));
    MOCK_METHOD(FMOD_RESULT, setParameterInt, (int index, int value), (override));
    MOCK_METHOD(FMOD_RESULT, setParameterFloat, (int index, float value), (override));
};

class MockChannel : public FMOD::Channel {
public:
    MOCK_METHOD(FMOD_RESULT, stop, (), (override));
    MOCK_METHOD(FMOD_RESULT, setVolume, (float volume), (override));
    MOCK_METHOD(FMOD_RESULT, isPlaying, (bool* isplaying), (override));
};

class MockChannelGroup : public FMOD::ChannelGroup {
public:
    // Add methods if needed
};

// Factory function for creating FMOD System in tests
namespace FMOD {
    inline FMOD_RESULT System_Create(FMOD::System** system) {
        *system = new MockSystem();
        return FMOD_OK;
    }
}

File: build-windows.bat

@echo off
REM Windows build script for FMOD Synthesizer

echo Building FMOD Synthesizer for Windows...

REM Create build directory
if not exist build mkdir build
cd build

REM Configure with CMake
echo Configuring project...
cmake -G "Visual Studio 17 2022" -A x64 ..
if errorlevel 1 (
    echo CMake configuration failed!
    exit /b 1
)

REM Build the project
echo Building project...
cmake --build . --config Release
if errorlevel 1 (
    echo Build failed!
    exit /b 1
)

echo.
echo Build complete! Executable is at: build\Release\synth.exe
echo.

REM Check if FMOD DLL was copied
if exist Release\fmod.dll (
    echo FMOD DLL found in output directory.
) else (
    echo WARNING: FMOD DLL not found in output directory!
    echo Please ensure fmod.dll is in the same directory as synth.exe
)

cd ..

File: build-windows-mingw.bat

@echo off
REM Windows build script for FMOD Synthesizer using MinGW

echo Building FMOD Synthesizer for Windows (MinGW)...

REM Create build directory
if not exist build mkdir build
cd build

REM Configure with CMake
echo Configuring project...
cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release ..
if errorlevel 1 (
    echo CMake configuration failed!
    exit /b 1
)

REM Build the project
echo Building project...
mingw32-make -j%NUMBER_OF_PROCESSORS%
if errorlevel 1 (
    echo Build failed!
    exit /b 1
)

echo.
echo Build complete! Executable is at: build\synth.exe
echo.

REM Check if FMOD DLL was copied
if exist fmod.dll (
    echo FMOD DLL found in output directory.
) else (
    echo WARNING: FMOD DLL not found in output directory!
    echo Please ensure fmod.dll is in the same directory as synth.exe
)

cd ..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment