Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/AUDIO.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ flowchart TD
SpatialProcessor[SpatialAudioProcessor]
end

subgraph SdlStream["SDL3 Audio Stream (main thread)"]
subgraph SdlStream["SDL3 Audio Stream (audio thread)"]
AudioDevice[DAC Output / Speakers]
Mixer[Stereo Interleaved Mixer]
end
Expand All @@ -47,7 +47,7 @@ flowchart TD
### Thread Safety & Zero-Allocation Streaming
The real-time audio thread must never block or allocate heap memory (`malloc`/`new`).
- **`AudioRingBuffer<SimpleAudioRequest, 64>`**: A lock-free single-producer single-consumer ring buffer transfers sound trigger requests from the game and physics threads to the audio thread.
- **Main-Thread Streaming**: PCM is generated synchronously in `AudioEngine::listen` on the main thread and written to the SDL3 audio stream; friction sources are plain per-body structs with no cross-thread state (only the debug/override level uses an atomic float).
- **Decoupled Audio Thread Streaming**: PCM is generated asynchronously in `AudioEngine::renderAudio` via SDL3's `SDL_AudioStreamCallback` on a dedicated audio thread. Main-thread hitches (such as heavy shader compilation or window resizing) do not interrupt audio playback. The main thread's `AudioEngine::listen` only performs microsecond-scale state updates under a fast mutex.
- **Mutex-Protected Song State**: Fast `std::mutex` guards protect song reference swaps and parameter updates between frames.

---
Expand Down
28 changes: 26 additions & 2 deletions include/weird-audio/AudioEngine.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once

#include <atomic>
#include <cstdint>
#include <mutex>
#include <SDL3/SDL.h>
#include <vector>

Expand All @@ -18,6 +20,11 @@ namespace WeirdEngine
float currentVolume = 0.0f; // For pulsing size
float currentFriction = 0.0f; // For static/jitter
std::vector<float> waveform; // For oscilloscope effects (snapshot of last 256 samples)

AudioData()
: waveform(256, 0.0f)
{
}
};

class AudioEngine
Expand All @@ -38,23 +45,33 @@ namespace WeirdEngine

void setAudioStream(SDL_AudioStream* stream)
{
std::lock_guard<std::mutex> lock(m_audioMutex);
m_audioStream = stream;
}

uint32_t getSampleRate() const;
uint8_t getChannels() const;

// Per-frame scene audio update and PCM generation
// SDL3 Audio stream callback invoked on the dedicated audio thread
static void SDLCALL audioStreamCallback(void* userdata, SDL_AudioStream* stream, int additional_amount,
int total_amount);

// Generates PCM audio data directly on the audio thread
void renderAudio(SDL_AudioStream* stream, int additional_amount);

// Per-frame scene audio synchronization from main thread
void listen(Scene& scene);

// Volume & Mute controls
void mute()
{
std::lock_guard<std::mutex> lock(m_audioMutex);
m_settings.mute = true;
}

void unmute()
{
std::lock_guard<std::mutex> lock(m_audioMutex);
m_settings.mute = false;
}

Expand All @@ -65,6 +82,7 @@ namespace WeirdEngine

void setMasterVolume(float vol)
{
std::lock_guard<std::mutex> lock(m_audioMutex);
m_settings.masterVolume = vol;
}

Expand Down Expand Up @@ -97,6 +115,7 @@ namespace WeirdEngine
// Spatial Audio Setting
void setSpatialAudioEnabled(bool enabled)
{
std::lock_guard<std::mutex> lock(m_audioMutex);
m_settings.enableSpatialAudio = enabled;
m_physicsEngine.setSpatialAudioEnabled(enabled);
}
Expand All @@ -107,13 +126,15 @@ namespace WeirdEngine
}

// Visualizer data
const AudioData& getAudioData() const
AudioData getAudioData() const
{
std::lock_guard<std::mutex> lock(m_audioMutex);
return m_visualSnapshot;
}

float getAudioVolume() const
{
std::lock_guard<std::mutex> lock(m_audioMutex);
return m_visualSnapshot.currentVolume;
}

Expand All @@ -124,8 +145,11 @@ namespace WeirdEngine
PhysicsAudioEngine m_physicsEngine;
SdfMusicEngine m_musicEngine;

mutable std::mutex m_audioMutex;
SDL_AudioStream* m_audioStream = nullptr;
AudioData m_visualSnapshot;
std::vector<float> m_mixBuffer;
double m_audioTime = 0.0;

// Master bus DC-blocking filter state (per channel)
float m_dcBlockerX[2] = {0.0f, 0.0f};
Expand Down
27 changes: 23 additions & 4 deletions include/weird-audio/SdfMusicEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,15 @@ namespace WeirdEngine

// Re-sample the shape at the 8 compass points (call after modifying shape parameters in real time)
void resampleShape();
const ShapeMusicalParams& getShapeParameters() const
ShapeMusicalParams getShapeParameters() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_shapeParams;
}

const InstrumentRack& getInstrumentRack() const
InstrumentRack getInstrumentRack() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_rack;
}

Expand All @@ -174,68 +176,82 @@ namespace WeirdEngine

void setTrackToggles(const MusicTrackToggles& toggles)
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
m_tracks = toggles;
}
const MusicTrackToggles& getTrackToggles() const
MusicTrackToggles getTrackToggles() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_tracks;
}

TrackPlayState getTrackPlayState(MusicTrack track) const;
float getSurgeLevel() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_surgeLevel;
}
float getDuckingLevel() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_ducking;
}

// Motion & Domain Fill Inspection
float getMotionLevel() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_motionLevel;
}
float getMotionNorm() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_motionNorm;
}
float getFillRatio() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_fillRatio;
}
float getTempoFromMotion() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_tempoFromMotion;
}
float getVolumeFromFill() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_volumeFromFill;
}

// Surface Complexity Inspection (scale-normalized curvature spread of the SDF surface)
float getComplexityLevel() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_complexityLevel;
}
float getComplexityNorm() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_complexityNorm;
}
float getComplexitySaturation() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_complexitySaturation;
}
// Higher values make the normalized stat cooler (more headroom for complex shapes)
void setComplexitySaturation(float saturation)
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
m_complexitySaturation = std::max(0.1f, saturation);
}

// Tension derived from surface complexity: drives dissonance, brightness and
// articulation independently of motion (tempo). 0 = calm, 1 = tense.
float getTension() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_tensionFromComplexity;
}

Expand All @@ -246,15 +262,18 @@ namespace WeirdEngine
// Current playhead position in beats
float getPlayheadBeat() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_currentBeat;
}

bool isPlaying() const
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
return m_playing;
}
void setPlaying(bool playing)
{
std::lock_guard<std::recursive_mutex> lock(m_songMutex);
m_playing = playing;
}

Expand All @@ -266,7 +285,7 @@ namespace WeirdEngine

std::shared_ptr<SdfSong> m_currentSong;
std::shared_ptr<SdfSong> m_queuedSong;
std::mutex m_songMutex;
mutable std::recursive_mutex m_songMutex;

// Sequencer time tracking
float m_currentBeat = 0.0f;
Expand Down
Loading
Loading