TamaTac & SfxEngine + Sprite tools (#24)

This commit is contained in:
Shadowtrance
2026-02-14 06:26:23 +10:00
committed by GitHub
parent 9ba1d460bd
commit 0b22f67a82
56 changed files with 8282 additions and 0 deletions
@@ -0,0 +1,601 @@
/**
* @file SfxDefinitions.h
* @brief 52 curated SFX definitions for SfxEngine.
*
* Implements SfxEngine::loadSound() — mapping SfxId values to note sequences.
*/
#pragma once
#include "SfxEngine.h"
#include "esp_log.h"
// Safety macro to prevent buffer overflow
#define ADD_SFX_NOTE(seq, idx, ...) do { \
if ((idx) < MAX_SFX_NOTES) { \
SfxSequenceNote n = __VA_ARGS__; \
(seq)[(idx)++] = n; \
} else { \
ESP_LOGE("SfxDef", "SFX note overflow at index %d", (idx)); \
} \
} while(0)
inline void SfxEngine::loadSound(SfxId sound) {
sequenceIndex_ = 0;
sequenceDelaySamples_ = 0;
sequenceLength_ = 0;
// Stop all voices
for (int i = 0; i < NUM_VOICES; i++) {
voices_[i].active = false;
voices_[i].envStage = EnvelopeStage::Off;
}
switch (sound) {
// ── UI Sounds ────────────────────────────────────────────────────────
case SfxId::Click:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 80, 30, 0, SfxWaveType::Square, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::Confirm:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 80, 0, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 79, 120, 70, SfxWaveType::Square, 0.4f});
sequenceLength_ = i;
}
break;
case SfxId::Cancel:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 80, 0, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 60, 120, 70, SfxWaveType::Square, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::Error:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 48, 150, 0, SfxWaveType::Sawtooth, 0.4f});
ADD_SFX_NOTE(sequence_, i, {1, 51, 150, 0, SfxWaveType::Sawtooth, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::MenuOpen:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 60, 40, 0, SfxWaveType::Pulse25, 0.25f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 60, 30, SfxWaveType::Pulse25, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::MenuClose:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 40, 0, SfxWaveType::Pulse25, 0.25f});
ADD_SFX_NOTE(sequence_, i, {0, 60, 60, 30, SfxWaveType::Pulse25, 0.2f});
sequenceLength_ = i;
}
break;
case SfxId::Toggle:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 84, 50, 0, SfxWaveType::Pulse12, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::Slider:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 15, 0, SfxWaveType::Pulse25, 0.2f});
sequenceLength_ = i;
}
break;
case SfxId::Tab:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 67, 50, 0, SfxWaveType::Square, 0.25f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 70, 40, SfxWaveType::Square, 0.3f});
sequenceLength_ = i;
}
break;
// ── TamaTac Pet Sounds ───────────────────────────────────────────────
case SfxId::Feed:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 80, 0, SfxWaveType::Pulse25, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 76, 80, 80, SfxWaveType::Pulse25, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 79, 100, 80, SfxWaveType::Pulse25, 0.4f});
sequenceLength_ = i;
}
break;
case SfxId::Play:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 70, 0, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {1, 60, 280, 0, SfxWaveType::Triangle, 0.2f});
ADD_SFX_NOTE(sequence_, i, {0, 76, 70, 70, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 79, 70, 70, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 100, 70, SfxWaveType::Square, 0.35f});
sequenceLength_ = i;
}
break;
case SfxId::Medicine:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 200, 0, SfxWaveType::Triangle, 0.4f});
ADD_SFX_NOTE(sequence_, i, {1, 79, 200, 50, SfxWaveType::Sine, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::Sleep:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 79, 200, 0, SfxWaveType::Triangle, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 76, 200, 180, SfxWaveType::Triangle, 0.3f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 300, 180, SfxWaveType::Triangle, 0.25f});
sequenceLength_ = i;
}
break;
case SfxId::Clean:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 84, 50, 0, SfxWaveType::Pulse12, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 79, 50, 40, SfxWaveType::Pulse12, 0.3f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 80, 40, SfxWaveType::Pulse12, 0.25f});
sequenceLength_ = i;
}
break;
case SfxId::Evolve:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 60, 150, 0, SfxWaveType::Square, 0.4f});
ADD_SFX_NOTE(sequence_, i, {1, 48, 600, 0, SfxWaveType::Triangle, 0.25f});
ADD_SFX_NOTE(sequence_, i, {0, 64, 150, 130, SfxWaveType::Square, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 67, 150, 130, SfxWaveType::Square, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 400, 130, SfxWaveType::Square, 0.5f});
sequenceLength_ = i;
}
break;
case SfxId::Sick:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 100, 0, SfxWaveType::Square, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 60, 100, 150, SfxWaveType::Square, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 100, 150, SfxWaveType::Square, 0.4f});
sequenceLength_ = i;
}
break;
case SfxId::Death:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 67, 250, 0, SfxWaveType::Triangle, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 64, 250, 220, SfxWaveType::Triangle, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 60, 250, 220, SfxWaveType::Triangle, 0.3f});
ADD_SFX_NOTE(sequence_, i, {0, 55, 350, 220, SfxWaveType::Triangle, 0.25f});
ADD_SFX_NOTE(sequence_, i, {0, 48, 500, 300, SfxWaveType::Triangle, 0.2f});
sequenceLength_ = i;
}
break;
// ── Game Sounds ──────────────────────────────────────────────────────
case SfxId::Coin:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 83, 60, 0, SfxWaveType::Square, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 88, 150, 50, SfxWaveType::Square, 0.35f});
sequenceLength_ = i;
}
break;
case SfxId::Powerup:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 60, 80, 0, SfxWaveType::Pulse25, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 64, 80, 60, SfxWaveType::Pulse25, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 67, 80, 60, SfxWaveType::Pulse25, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 80, 60, SfxWaveType::Pulse25, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 76, 80, 60, SfxWaveType::Pulse25, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 79, 80, 60, SfxWaveType::Pulse25, 0.45f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 200, 60, SfxWaveType::Pulse25, 0.5f});
sequenceLength_ = i;
}
break;
case SfxId::Jump:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 60, 30, 0, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 65, 30, 25, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 70, 30, 25, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 75, 30, 25, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 80, 40, 25, SfxWaveType::Square, 0.3f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 50, 30, SfxWaveType::Square, 0.25f});
sequenceLength_ = i;
}
break;
case SfxId::Land:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 36, 80, 0, SfxWaveType::Triangle, 0.5f});
ADD_SFX_NOTE(sequence_, i, {1, 40, 60, 0, SfxWaveType::Noise, 0.15f});
sequenceLength_ = i;
}
break;
case SfxId::Laser:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 96, 25, 0, SfxWaveType::Pulse25, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 90, 25, 20, SfxWaveType::Pulse25, 0.38f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 25, 20, SfxWaveType::Pulse25, 0.36f});
ADD_SFX_NOTE(sequence_, i, {0, 78, 30, 20, SfxWaveType::Pulse25, 0.34f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 40, 25, SfxWaveType::Pulse25, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::Explosion:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 48, 50, 0, SfxWaveType::Noise, 0.5f});
ADD_SFX_NOTE(sequence_, i, {1, 30, 300, 0, SfxWaveType::Triangle, 0.45f});
ADD_SFX_NOTE(sequence_, i, {0, 36, 150, 40, SfxWaveType::Noise, 0.45f});
ADD_SFX_NOTE(sequence_, i, {0, 30, 200, 100, SfxWaveType::Noise, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 24, 250, 150, SfxWaveType::Noise, 0.25f});
sequenceLength_ = i;
}
break;
case SfxId::Hurt:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 60, 80, 0, SfxWaveType::Sawtooth, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 48, 120, 60, SfxWaveType::Sawtooth, 0.35f});
sequenceLength_ = i;
}
break;
case SfxId::Warp:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 48, 40, 0, SfxWaveType::Sine, 0.3f});
ADD_SFX_NOTE(sequence_, i, {1, 36, 350, 0, SfxWaveType::Noise, 0.12f});
ADD_SFX_NOTE(sequence_, i, {0, 55, 40, 35, SfxWaveType::Sine, 0.32f});
ADD_SFX_NOTE(sequence_, i, {0, 62, 40, 35, SfxWaveType::Sine, 0.34f});
ADD_SFX_NOTE(sequence_, i, {0, 69, 40, 35, SfxWaveType::Sine, 0.36f});
ADD_SFX_NOTE(sequence_, i, {0, 76, 40, 35, SfxWaveType::Sine, 0.38f});
ADD_SFX_NOTE(sequence_, i, {0, 83, 50, 35, SfxWaveType::Sine, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 90, 60, 40, SfxWaveType::Sine, 0.3f});
ADD_SFX_NOTE(sequence_, i, {0, 96, 80, 50, SfxWaveType::Sine, 0.25f});
sequenceLength_ = i;
}
break;
case SfxId::Pickup:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 84, 60, 0, SfxWaveType::Pulse12, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 88, 60, 50, SfxWaveType::Pulse12, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 91, 100, 50, SfxWaveType::Pulse12, 0.35f});
sequenceLength_ = i;
}
break;
case SfxId::OneUp:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 100, 0, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 76, 100, 80, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 79, 100, 80, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 250, 80, SfxWaveType::Square, 0.45f});
ADD_SFX_NOTE(sequence_, i, {1, 60, 450, 0, SfxWaveType::Triangle, 0.2f});
sequenceLength_ = i;
}
break;
case SfxId::BrickHit:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 76, 20, 0, SfxWaveType::Pulse25, 0.25f});
sequenceLength_ = i;
}
break;
// ── Notifications ────────────────────────────────────────────────────
case SfxId::Alert:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 84, 80, 0, SfxWaveType::Square, 0.45f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 80, 120, SfxWaveType::Square, 0.45f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 80, 120, SfxWaveType::Square, 0.45f});
sequenceLength_ = i;
}
break;
case SfxId::Notify:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 79, 100, 0, SfxWaveType::Sine, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 150, 90, SfxWaveType::Sine, 0.4f});
ADD_SFX_NOTE(sequence_, i, {1, 67, 250, 0, SfxWaveType::Triangle, 0.15f});
sequenceLength_ = i;
}
break;
case SfxId::Success:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 100, 0, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 79, 100, 80, SfxWaveType::Square, 0.4f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 200, 80, SfxWaveType::Square, 0.45f});
ADD_SFX_NOTE(sequence_, i, {1, 60, 400, 0, SfxWaveType::Triangle, 0.2f});
sequenceLength_ = i;
}
break;
// ── Drum Kit ─────────────────────────────────────────────────────────
case SfxId::Kick:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 48, 15, 0, SfxWaveType::Triangle, 0.6f});
ADD_SFX_NOTE(sequence_, i, {0, 36, 60, 10, SfxWaveType::Triangle, 0.55f});
sequenceLength_ = i;
}
break;
case SfxId::Snare:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 60, 50, 0, SfxWaveType::Noise, 0.35f});
ADD_SFX_NOTE(sequence_, i, {1, 48, 30, 0, SfxWaveType::Triangle, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::HiHat:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 80, 15, 0, SfxWaveType::Noise, 0.2f});
sequenceLength_ = i;
}
break;
case SfxId::Crash:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 250, 0, SfxWaveType::Noise, 0.3f});
sequenceLength_ = i;
}
break;
// ── Synth Percussion ─────────────────────────────────────────────────
case SfxId::SynthKick:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 55, 10, 0, SfxWaveType::Sine, 0.6f});
ADD_SFX_NOTE(sequence_, i, {0, 36, 120, 8, SfxWaveType::Sine, 0.55f});
sequenceLength_ = i;
}
break;
case SfxId::SynthSnare:
// MetalNoise -> Noise substitution
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 60, 80, 0, SfxWaveType::Noise, 0.35f});
ADD_SFX_NOTE(sequence_, i, {1, 55, 40, 0, SfxWaveType::Triangle, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::SynthHat:
// MetalNoise -> Noise substitution
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 84, 30, 0, SfxWaveType::Noise, 0.25f});
sequenceLength_ = i;
}
break;
case SfxId::SynthTom:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 60, 15, 0, SfxWaveType::Triangle, 0.5f});
ADD_SFX_NOTE(sequence_, i, {0, 48, 100, 12, SfxWaveType::Triangle, 0.45f});
sequenceLength_ = i;
}
break;
case SfxId::SynthRim:
// MetalNoise -> Noise substitution
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 25, 0, SfxWaveType::Noise, 0.35f});
ADD_SFX_NOTE(sequence_, i, {1, 84, 15, 0, SfxWaveType::Pulse12, 0.25f});
sequenceLength_ = i;
}
break;
// ── Extra Effects ────────────────────────────────────────────────────
case SfxId::Zap:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 96, 15, 0, SfxWaveType::Square, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 15, 10, SfxWaveType::Square, 0.30f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 15, 10, SfxWaveType::Square, 0.25f});
ADD_SFX_NOTE(sequence_, i, {0, 60, 20, 10, SfxWaveType::Square, 0.20f});
sequenceLength_ = i;
}
break;
case SfxId::Blip:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 90, 20, 0, SfxWaveType::Square, 0.3f});
sequenceLength_ = i;
}
break;
case SfxId::Chirp:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 20, 0, SfxWaveType::Pulse25, 0.3f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 30, 15, SfxWaveType::Pulse25, 0.25f});
sequenceLength_ = i;
}
break;
case SfxId::Whoosh:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 48, 30, 0, SfxWaveType::Noise, 0.15f});
ADD_SFX_NOTE(sequence_, i, {0, 60, 30, 25, SfxWaveType::Noise, 0.20f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 30, 25, SfxWaveType::Noise, 0.25f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 40, 25, SfxWaveType::Noise, 0.20f});
sequenceLength_ = i;
}
break;
case SfxId::Ding:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 84, 300, 0, SfxWaveType::Sine, 0.4f});
ADD_SFX_NOTE(sequence_, i, {1, 91, 200, 0, SfxWaveType::Sine, 0.2f});
sequenceLength_ = i;
}
break;
case SfxId::RisingWhoosh:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 36, 30, 0, SfxWaveType::Noise, 0.15f});
ADD_SFX_NOTE(sequence_, i, {0, 48, 30, 25, SfxWaveType::Noise, 0.20f});
ADD_SFX_NOTE(sequence_, i, {0, 60, 30, 25, SfxWaveType::Noise, 0.25f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 40, 25, SfxWaveType::Noise, 0.30f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 50, 30, SfxWaveType::Noise, 0.25f});
sequenceLength_ = i;
}
break;
case SfxId::FallingWhoosh:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 84, 30, 0, SfxWaveType::Noise, 0.30f});
ADD_SFX_NOTE(sequence_, i, {0, 72, 30, 25, SfxWaveType::Noise, 0.25f});
ADD_SFX_NOTE(sequence_, i, {0, 60, 30, 25, SfxWaveType::Noise, 0.20f});
ADD_SFX_NOTE(sequence_, i, {0, 48, 40, 25, SfxWaveType::Noise, 0.15f});
ADD_SFX_NOTE(sequence_, i, {0, 36, 50, 30, SfxWaveType::Noise, 0.10f});
sequenceLength_ = i;
}
break;
case SfxId::GlitchHit:
// MetalNoise -> Noise substitution
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 48, 20, 0, SfxWaveType::Noise, 0.45f});
ADD_SFX_NOTE(sequence_, i, {1, 36, 80, 0, SfxWaveType::Sawtooth, 0.40f});
ADD_SFX_NOTE(sequence_, i, {0, 96, 15, 15, SfxWaveType::Pulse12, 0.30f});
ADD_SFX_NOTE(sequence_, i, {0, 60, 40, 10, SfxWaveType::Noise, 0.25f});
sequenceLength_ = i;
}
break;
case SfxId::DigitalBurst:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 25, 0, SfxWaveType::Pulse12, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 84, 25, 20, SfxWaveType::Pulse12, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 96, 25, 20, SfxWaveType::Pulse12, 0.35f});
ADD_SFX_NOTE(sequence_, i, {0, 108, 30, 20, SfxWaveType::Pulse12, 0.30f});
sequenceLength_ = i;
}
break;
case SfxId::RetroBell:
// HarmonicSquare -> Square substitution
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 84, 200, 0, SfxWaveType::Square, 0.15f});
ADD_SFX_NOTE(sequence_, i, {1, 91, 180, 0, SfxWaveType::Pulse25, 0.10f});
sequenceLength_ = i;
}
break;
// ── Jingles ──────────────────────────────────────────────────────────
// Rising arpeggio (C4-E4-G4-C5-E5-G5-C6) with sustained bass (C3)
case SfxId::LevelUp:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 60, 100, 0, SfxWaveType::Square, 0.28f}); // C4
ADD_SFX_NOTE(sequence_, i, {1, 48, 800, 0, SfxWaveType::Triangle, 0.16f}); // C3 bass pad
ADD_SFX_NOTE(sequence_, i, {0, 64, 100, 80, SfxWaveType::Square, 0.28f}); // E4
ADD_SFX_NOTE(sequence_, i, {0, 67, 100, 80, SfxWaveType::Square, 0.28f}); // G4
ADD_SFX_NOTE(sequence_, i, {0, 72, 150, 80, SfxWaveType::Square, 0.32f}); // C5
ADD_SFX_NOTE(sequence_, i, {0, 76, 100, 130, SfxWaveType::Square, 0.32f}); // E5
ADD_SFX_NOTE(sequence_, i, {0, 79, 100, 80, SfxWaveType::Square, 0.32f}); // G5
ADD_SFX_NOTE(sequence_, i, {0, 84, 300, 80, SfxWaveType::Square, 0.40f}); // C6 (hold)
sequenceLength_ = i;
}
break;
// Descending chromatic line (C5-B4-A4-G4-F4-E4-C4) with bass drone
case SfxId::GameOver:
{
int i = 0;
ADD_SFX_NOTE(sequence_, i, {0, 72, 200, 0, SfxWaveType::Square, 0.30f}); // C5
ADD_SFX_NOTE(sequence_, i, {1, 60, 800, 0, SfxWaveType::Triangle, 0.15f}); // C4 bass drone
ADD_SFX_NOTE(sequence_, i, {0, 71, 200, 180, SfxWaveType::Square, 0.28f}); // B4
ADD_SFX_NOTE(sequence_, i, {0, 69, 200, 180, SfxWaveType::Square, 0.27f}); // A4
ADD_SFX_NOTE(sequence_, i, {0, 67, 200, 180, SfxWaveType::Square, 0.25f}); // G4
ADD_SFX_NOTE(sequence_, i, {1, 55, 600, 0, SfxWaveType::Triangle, 0.15f}); // G3 bass shift
ADD_SFX_NOTE(sequence_, i, {0, 65, 250, 180, SfxWaveType::Square, 0.24f}); // F4
ADD_SFX_NOTE(sequence_, i, {0, 64, 400, 230, SfxWaveType::Square, 0.22f}); // E4
ADD_SFX_NOTE(sequence_, i, {0, 60, 600, 380, SfxWaveType::Square, 0.18f}); // C4 (hold)
sequenceLength_ = i;
}
break;
case SfxId::None:
default:
sequenceLength_ = 0;
break;
}
// Start sequence
if (sequenceLength_ > 0) {
sequenceDelaySamples_ = (sequence_[0].delayMs * SAMPLE_RATE) / 1000;
}
}
#undef ADD_SFX_NOTE
+341
View File
@@ -0,0 +1,341 @@
/**
* @file SfxEngine.h
* @brief Lightweight SFX-only audio engine for ESP32/Tactility apps (~1.5KB RAM)
*
* A stripped-down version of SoundEngine focused on sound effects only.
* No background music, no advanced synthesis (FM, filters, pluck, delay).
*
* Features:
* - 4 simultaneous voices (polyphony)
* - Waveforms: Square, Pulse (25%/12.5%/75%), Triangle, Sawtooth, Sine, Noise, RetroNoise
* - ADSR envelope + special types (Punch, Flare, Swell, Twang, Decay)
* - Effects: Vibrato, Pitch Sweep
* - 52 curated predefined sound effects
* - Queue-based triggering (thread-safe)
* - Persistent audio task
*
* Usage:
* SfxEngine* engine = new SfxEngine();
* engine->start();
* engine->play(SfxId::Coin);
* engine->playNote(0, 72, 200);
* engine->stop();
* delete engine;
*/
#pragma once
#include <cstdint>
#include <tactility/device.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
//==============================================================================
// Configuration
//==============================================================================
#ifndef SFX_ENGINE_NUM_VOICES
#define SFX_ENGINE_NUM_VOICES 4
#endif
#ifndef SFX_ENGINE_SAMPLE_RATE
#define SFX_ENGINE_SAMPLE_RATE 16000
#endif
#ifndef SFX_ENGINE_BUFFER_SAMPLES
#define SFX_ENGINE_BUFFER_SAMPLES 256
#endif
//==============================================================================
// Types
//==============================================================================
enum class SfxWaveType : uint8_t {
Square, // 50% duty cycle
Pulse25, // 25% duty cycle (NES-style)
Pulse12, // 12.5% duty cycle
Pulse75, // 75% duty cycle (bright, hollow)
Triangle,
Sawtooth,
Sine,
Noise, // White noise (random)
RetroNoise // NES-style LFSR noise
};
enum class SfxId : uint8_t {
None = 0,
// UI Sounds (9)
Click, Confirm, Cancel, Error,
MenuOpen, MenuClose, Toggle, Slider, Tab,
// TamaTac Pet Sounds (8)
Feed, Play, Medicine, Sleep, Clean, Evolve, Sick, Death,
// Game Sounds (11)
Coin, Powerup, Jump, Land, Laser, Explosion,
Hurt, Warp, Pickup, OneUp, BrickHit,
// Notifications (3)
Alert, Notify, Success,
// Drum Kit (4)
Kick, Snare, HiHat, Crash,
// Synth Percussion (5)
SynthKick, SynthSnare, SynthHat, SynthTom, SynthRim,
// Extra Effects (10)
Zap, Blip, Chirp, Whoosh, Ding,
RisingWhoosh, FallingWhoosh, GlitchHit, DigitalBurst, RetroBell,
// Jingles (2)
LevelUp, GameOver
};
enum class SfxEnvelopeType : uint8_t {
ADSR, // Standard attack-decay-sustain-release
Punch, // Constant volume with boost at start
Flare, // Exponential rise then decay
Swell, // Linear fade-in
Twang, // Extremely fast decay
Decay // Linear decay to zero
};
// Compact note for SFX sequences
struct SfxSequenceNote {
uint8_t voice;
uint8_t pitch;
uint16_t durationMs;
uint16_t delayMs;
SfxWaveType wave;
float volume;
// Optional ADSR override (all zero = use defaults)
uint16_t attackMs = 0;
uint16_t decayMs = 0;
float sustain = 0.0f;
uint16_t releaseMs = 0;
SfxEnvelopeType envType = SfxEnvelopeType::ADSR;
constexpr SfxSequenceNote()
: voice(0), pitch(0), durationMs(0), delayMs(0),
wave(SfxWaveType::Square), volume(0.0f) {}
constexpr SfxSequenceNote(uint8_t v, uint8_t p, uint16_t dur, uint16_t del,
SfxWaveType w, float vol,
uint16_t att = 0, uint16_t dec = 0, float sus = 0.0f,
uint16_t rel = 0, SfxEnvelopeType et = SfxEnvelopeType::ADSR)
: voice(v), pitch(p), durationMs(dur), delayMs(del), wave(w), volume(vol),
attackMs(att), decayMs(dec), sustain(sus), releaseMs(rel), envType(et) {}
};
//==============================================================================
// SfxEngine Class
//==============================================================================
class SfxEngine {
public:
SfxEngine() = default;
~SfxEngine() { stop(); }
SfxEngine(const SfxEngine&) = delete;
SfxEngine& operator=(const SfxEngine&) = delete;
SfxEngine(SfxEngine&&) = delete;
SfxEngine& operator=(SfxEngine&&) = delete;
// Lifecycle
bool start();
void stop();
bool isRunning() const { return running_; }
// SFX API
void play(SfxId sound);
void stopAllSounds();
// Manual note API
void playNote(uint8_t voice, uint8_t midiNote, uint16_t durationMs,
SfxWaveType wave = SfxWaveType::Square, float volume = 0.5f);
void stopVoice(uint8_t voice);
// Settings
void setEnabled(bool enabled) { enabled_ = enabled; }
bool isEnabled() const { return enabled_; }
void setVolume(float vol) { masterVolume_ = (vol < 0) ? 0 : (vol > 1.0f) ? 1.0f : vol; }
float getVolume() const { return masterVolume_; }
// Volume presets (consistent with SoundEngine naming)
enum class VolumePreset { Quiet, Normal, Loud };
void applyVolumePreset(VolumePreset preset);
// Polyphonic gate (consistent with SoundEngine)
void setPolyphonicGateEnabled(bool enabled) { polyphonicGateEnabled_ = enabled; }
bool isPolyphonicGateEnabled() const { return polyphonicGateEnabled_; }
void setSoftGateThreshold(float threshold) {
softGateThreshold_ = (threshold < 0.5f) ? 0.5f : (threshold > 1.0f) ? 1.0f : threshold;
}
float getSoftGateThreshold() const { return softGateThreshold_; }
// Auto-normalization (consistent with SoundEngine)
void setAutoNormalization(bool enabled) { autoNormalize_ = enabled; }
bool isAutoNormalizationEnabled() const { return autoNormalize_; }
void setTargetRms(float rms) {
targetRms_ = (rms < 0.1f) ? 0.1f : (rms > 0.8f) ? 0.8f : rms;
}
float getTargetRms() const { return targetRms_; }
void setRmsSmoothing(float smoothing) {
rmsSmoothing_ = (smoothing < 0.9f) ? 0.9f : (smoothing > 0.99999f) ? 0.99999f : smoothing;
}
float getRmsSmoothing() const { return rmsSmoothing_; }
static constexpr int NUM_VOICES = SFX_ENGINE_NUM_VOICES;
static constexpr int SAMPLE_RATE = SFX_ENGINE_SAMPLE_RATE;
private:
static constexpr const char* TAG = "SfxEngine";
static constexpr int BUFFER_SAMPLES = SFX_ENGINE_BUFFER_SAMPLES;
static constexpr int MAX_SFX_NOTES = 64;
//--------------------------------------------------------------------------
// Voice State
//--------------------------------------------------------------------------
enum class EnvelopeStage { Off, Attack, Decay, Sustain, Release };
struct Voice {
// Oscillator
SfxWaveType wave = SfxWaveType::Square;
float phase = 0.0f;
float baseFreq = 0.0f;
float currentFreq = 0.0f;
// Timing
int samplePos = 0;
int totalSamples = 0;
// Envelope
SfxEnvelopeType envType = SfxEnvelopeType::ADSR;
EnvelopeStage envStage = EnvelopeStage::Off;
float envLevel = 0.0f;
int envSamplePos = 0;
int attackSamples = 0;
int decaySamples = 0;
float sustainLevel = 0.7f;
int releaseSamples = 0;
// Volume
float volume = 0.5f;
// Pitch effects
float vibratoDepth = 0.0f;
float vibratoRate = 0.0f;
float vibratoPhase = 0.0f;
float pitchSweep = 0.0f;
bool active = false;
};
Voice voices_[NUM_VOICES] = {};
//--------------------------------------------------------------------------
// SFX Sequence
//--------------------------------------------------------------------------
SfxSequenceNote sequence_[MAX_SFX_NOTES] = {};
int sequenceLength_ = 0;
int sequenceIndex_ = 0;
int sequenceDelaySamples_ = 0;
//--------------------------------------------------------------------------
// Queue Messages
//--------------------------------------------------------------------------
enum class MsgType : uint8_t {
PlaySound, PlayNote, StopVoice, StopAll
};
struct PlayNoteMsg {
uint8_t voice;
uint8_t midiNote;
uint16_t durationMs;
SfxWaveType wave;
float volume;
};
struct QueueMsg {
MsgType type;
union {
SfxId sfxId;
PlayNoteMsg note;
uint8_t voiceIndex;
};
QueueMsg() : type(MsgType::StopAll), sfxId(SfxId::None) {}
};
//--------------------------------------------------------------------------
// State
//--------------------------------------------------------------------------
Device* i2sDevice_ = nullptr;
TaskHandle_t task_ = nullptr;
SemaphoreHandle_t stopSemaphore_ = nullptr; // Signaled when audio task exits
QueueHandle_t msgQueue_ = nullptr;
volatile bool running_ = false;
volatile bool enabled_ = true;
volatile float masterVolume_ = 0.5f;
// Polyphonic gate
volatile bool polyphonicGateEnabled_ = true;
volatile float softGateThreshold_ = 0.95f;
// Auto-normalization
volatile bool autoNormalize_ = true;
volatile float targetRms_ = 0.35f;
volatile float rmsSmoothing_ = 0.999f;
// Noise generator state (per-instance, audio task only)
uint16_t lfsr_ = 0xACE1;
uint16_t retroLfsr_ = 0xACE1;
// Auto-normalization working state (audio task only)
float currentRms_ = 0.0f;
int rmsCalcCounter_ = 0;
float cachedNormGain_ = 1.0f;
// Audio buffer (member to avoid stack pressure in audio task)
int16_t audioBuffer_[BUFFER_SAMPLES * 2] = {};
//--------------------------------------------------------------------------
// Audio Generation
//--------------------------------------------------------------------------
static float midiToFreq(uint8_t midi);
float generateNoise();
float generateRetroNoise();
float oscillator(SfxWaveType wave, float phase);
void updateEnvelope(Voice& v);
float generateVoiceSample(Voice& v);
void fillStereoBuffer(int16_t* buf, int samples);
float applyPolyphonicGate(float mix, int activeVoices);
float applyAutoNormalization(float mix);
float applyBrickWallLimiter(float sample);
//--------------------------------------------------------------------------
// Sound Loading
//--------------------------------------------------------------------------
void loadSound(SfxId sound);
void triggerNote(Voice& v, uint8_t midiNote, uint16_t durationMs,
SfxWaveType wave, float volume, SfxEnvelopeType envType,
uint16_t attackMs = 0, uint16_t decayMs = 0,
float sustain = 0.0f, uint16_t releaseMs = 0);
void processSequence();
//--------------------------------------------------------------------------
// Task
//--------------------------------------------------------------------------
static void audioTaskFunc(void* param);
};