TamaTac & SfxEngine + Sprite tools (#24)
This commit is contained in:
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user