#include "core/audio/audio_effects.hpp" #include #include #include #include #include #include #include #include "core/audio/jail_audio.hpp" namespace AudioEffects { namespace { // --- Caps de cua --- constexpr float ECHO_TAIL_MS = 800.0F; constexpr float REVERB_TAIL_MS = 1500.0F; // --- Constants Freeverb --- // Delays de comb i allpass tunats para 44.1 kHz; los reescalem per // freqüència real de la font. constexpr int COMB_REFERENCE_RATE = 44100; constexpr std::array COMB_DELAYS_L = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617}; constexpr std::array ALLPASS_DELAYS_L = {556, 441, 341, 225}; constexpr int STEREO_SPREAD = 23; // Mapeig de Schroeder/Dattorro/Freeverb estàndard. constexpr float FIXED_GAIN = 0.015F; constexpr float SCALE_ROOM = 0.28F; constexpr float OFFSET_ROOM = 0.7F; constexpr float SCALE_DAMP = 0.4F; // --- Decodificació a float -1..1 --- // Suporta U8/S16, mono/estèreo. Mono es duplica a L i R (la cadena // d'efectes treballa siempre con dos canals per simplicitat). auto decodeToStereoFloat(const Ja::Sound& src, std::vector& left, std::vector& right) -> bool { const auto& spec = src.spec; const Uint8* buf = src.buffer.get(); if (buf == nullptr || src.length == 0) { return false; } int bytes_per_sample = 0; if (spec.format == SDL_AUDIO_S16) { bytes_per_sample = 2; } else if (spec.format == SDL_AUDIO_U8) { bytes_per_sample = 1; } else { std::cerr << "[AudioEffects] formato de sonido no soportado (solo U8 o S16)\n"; return false; } if (spec.channels < 1 || spec.channels > 2) { std::cerr << "[AudioEffects] el sonido debe ser mono o estéreo\n"; return false; } const std::size_t TOTAL_FRAMES = src.length / static_cast(bytes_per_sample * spec.channels); left.resize(TOTAL_FRAMES); right.resize(TOTAL_FRAMES); for (std::size_t i = 0; i < TOTAL_FRAMES; ++i) { float sample_l = 0.0F; float sample_r = 0.0F; if (spec.format == SDL_AUDIO_S16) { const auto* p = reinterpret_cast(buf + (i * spec.channels * 2)); sample_l = static_cast(p[0]) / 32768.0F; sample_r = (spec.channels == 2) ? static_cast(p[1]) / 32768.0F : sample_l; } else { // U8 const Uint8* p = buf + (i * spec.channels); sample_l = (static_cast(p[0]) - 128.0F) / 128.0F; sample_r = (spec.channels == 2) ? (static_cast(p[1]) - 128.0F) / 128.0F : sample_l; } left[i] = sample_l; right[i] = sample_r; } return true; } // Empaqueta dos canals float (-1..1) a S16 entrellaçat. void encodeStereoS16(const std::vector& left, const std::vector& right, std::vector& out) { const std::size_t LEN = left.size(); out.resize(LEN * 2 * sizeof(std::int16_t)); auto* dst = reinterpret_cast(out.data()); for (std::size_t i = 0; i < LEN; ++i) { const float L = std::clamp(left[i], -1.0F, 1.0F); const float R = std::clamp(right[i], -1.0F, 1.0F); dst[(i * 2) + 0] = static_cast(std::lround(L * 32767.0F)); dst[(i * 2) + 1] = static_cast(std::lround(R * 32767.0F)); } } // Reescala un delay de la taula de Freeverb para la freqüència real. auto scaledDelay(int reference_delay, int rate) -> int { const long SCALED = std::lround(static_cast(reference_delay) * static_cast(rate) / static_cast(COMB_REFERENCE_RATE)); return std::max(1, static_cast(SCALED)); } // --- Filtres bàsics --- struct Comb { std::vector buf; std::size_t idx{0}; float feedback{0.0F}; float damp1{0.0F}; float damp2{1.0F}; float store{0.0F}; void init(int delay, float fb, float damping) { buf.assign(static_cast(delay), 0.0F); idx = 0; feedback = fb; damp1 = damping; damp2 = 1.0F - damping; store = 0.0F; } auto tick(float in) -> float { const float OUT = buf[idx]; store = (OUT * damp2) + (store * damp1); buf[idx] = in + (store * feedback); idx = (idx + 1) % buf.size(); return OUT; } }; struct Allpass { std::vector buf; std::size_t idx{0}; void init(int delay) { buf.assign(static_cast(delay), 0.0F); idx = 0; } auto tick(float in) -> float { const float BUFOUT = buf[idx]; const float OUT = -in + BUFOUT; buf[idx] = in + (BUFOUT * 0.5F); idx = (idx + 1) % buf.size(); return OUT; } }; } // namespace auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional { std::vector left; std::vector right; if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; } const int RATE = src.spec.freq; const int DELAY_SAMPLES = std::max(1, static_cast(std::lround(params.delay_ms * 0.001F * static_cast(RATE)))); const auto TAIL_SAMPLES = static_cast(std::lround(ECHO_TAIL_MS * 0.001F * static_cast(RATE))); const float FEEDBACK = std::clamp(params.feedback, 0.0F, 0.95F); const float WET = std::clamp(params.wet, 0.0F, 1.0F); const float DRY = 1.0F - WET; const std::size_t INPUT_LEN = left.size(); const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES; std::vector ring_l(static_cast(DELAY_SAMPLES), 0.0F); std::vector ring_r(static_cast(DELAY_SAMPLES), 0.0F); std::size_t cursor = 0; std::vector out_l(TOTAL_LEN); std::vector out_r(TOTAL_LEN); for (std::size_t i = 0; i < TOTAL_LEN; ++i) { const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F; const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F; const float DELAYED_L = ring_l[cursor]; const float DELAYED_R = ring_r[cursor]; out_l[i] = (DRY * IN_L) + (WET * DELAYED_L); out_r[i] = (DRY * IN_R) + (WET * DELAYED_R); ring_l[cursor] = IN_L + (DELAYED_L * FEEDBACK); ring_r[cursor] = IN_R + (DELAYED_R * FEEDBACK); cursor = (cursor + 1) % static_cast(DELAY_SAMPLES); } ProcessedSound result; result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE}; encodeStereoS16(out_l, out_r, result.bytes); return result; } auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional { std::vector left; std::vector right; if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; } const int RATE = src.spec.freq; const auto TAIL_SAMPLES = static_cast(std::lround(REVERB_TAIL_MS * 0.001F * static_cast(RATE))); const float ROOM_SIZE = std::clamp(params.room_size, 0.0F, 1.0F); const float DAMPING = std::clamp(params.damping, 0.0F, 1.0F); const float WET = std::clamp(params.wet, 0.0F, 1.0F); const float DRY = 1.0F - WET; const float FEEDBACK = (ROOM_SIZE * SCALE_ROOM) + OFFSET_ROOM; // 0.7..0.98 const float DAMP1 = DAMPING * SCALE_DAMP; // 0..0.4 // Inicialitza los 8 comb filters per cada canal i los 4 allpass. std::array comb_l; std::array comb_r; for (std::size_t i = 0; i < COMB_DELAYS_L.size(); ++i) { comb_l[i].init(scaledDelay(COMB_DELAYS_L[i], RATE), FEEDBACK, DAMP1); comb_r[i].init(scaledDelay(COMB_DELAYS_L[i] + STEREO_SPREAD, RATE), FEEDBACK, DAMP1); } std::array allpass_l; std::array allpass_r; for (std::size_t i = 0; i < ALLPASS_DELAYS_L.size(); ++i) { allpass_l[i].init(scaledDelay(ALLPASS_DELAYS_L[i], RATE)); allpass_r[i].init(scaledDelay(ALLPASS_DELAYS_L[i] + STEREO_SPREAD, RATE)); } const std::size_t INPUT_LEN = left.size(); const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES; std::vector out_l(TOTAL_LEN); std::vector out_r(TOTAL_LEN); for (std::size_t i = 0; i < TOTAL_LEN; ++i) { const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F; const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F; const float MONO_INPUT = (IN_L + IN_R) * FIXED_GAIN; // 8 comb filters en paral·lel, sumats. float wet_l = 0.0F; float wet_r = 0.0F; for (std::size_t k = 0; k < comb_l.size(); ++k) { wet_l += comb_l[k].tick(MONO_INPUT); wet_r += comb_r[k].tick(MONO_INPUT); } // 4 allpass en sèrie. for (std::size_t k = 0; k < allpass_l.size(); ++k) { wet_l = allpass_l[k].tick(wet_l); wet_r = allpass_r[k].tick(wet_r); } out_l[i] = (DRY * IN_L) + (WET * wet_l); out_r[i] = (DRY * IN_R) + (WET * wet_r); } ProcessedSound result; result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE}; encodeStereoS16(out_l, out_r, result.bytes); return result; } } // namespace AudioEffects