This is a C++ synthesizer project built with FMOD for audio processing. It includes a command-line interface for controlling various synthesizer parameters.
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
# 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
- 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
# 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
-
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
-
Run the synthesizer:
build\Release\synth.exe
-
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
-
Run the synthesizer:
build\synth.exe
# 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
# Using default Apple Clang
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
# Run
./build/synth
The FMOD library is included in lib/fmod/
directory. If you need to download a different version:
- Download FMOD Core API from https://www.fmod.com/download
- Extract to
lib/fmod/
directory in project root - Directory structure should be:
lib/fmod/ ├── api/ │ └── core/ │ ├── inc/ (headers) │ └── lib/ │ ├── x86_64/ (Linux .so files) │ ├── x64/ (Windows .lib/.dll files) │ └── ...
- The build system automatically copies the appropriate FMOD DLL to the output directory
- Use
fmod.dll
for Release builds andfmodL.dll
for Debug builds - Ensure the DLL architecture (x86/x64) matches your build target
task
- Show all available taskstask build-clang
- Build with Clang (recommended)task build
- Build with default compilertask run
- Build and runtask clean
- Clean build artifactstask rebuild
- Clean and rebuildtask debug
- Build with debug symbolstask test
- Run with test commandstask play
- Play a tone (default: 440 Hz for 2 seconds)task play FREQ=8143 DURATION=5
- Play 8143 Hz for 5 secondstask check-fmod
- Check FMOD installationtask dev
- Watch files and rebuild (requires entr)task format
- Format code (requires clang-format)task lint
- Run static analysis (requires clang-tidy)
The synthesizer provides an interactive command-line interface. Available commands:
play <frequency>
- Play a note at the specified frequency (Hz)stop
- Stop playingwave <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 LFOlfo wave <type>
- Set LFO waveform (sine, triangle, square, sawtooth)status
- Show current settingshelp
- Display help messagequit
- Exit the program
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!
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
- 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
std::numbers
for mathematical constantsstd::format
for string formattingstd::ranges
algorithmsstd::from_chars
for float parsingconsteval
for compile-time computationstd::chrono
literals<concepts>
header (prepared for future use)- Designated initializers
[[maybe_unused]]
attributes
- 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
(orfmodL.dll
for Debug) fromlib/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)
- 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
- 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
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()
{
"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"
}
]
}
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"
#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;
}
#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);
#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;
}
#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};
};
#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;
}
#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>; }
};
#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;
}
}
# 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"
)
#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();
}
#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"});
}
#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
}
#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);
}
#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;
}
}
@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 ..
@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 ..