ref: 37069678c8a822cf43703ae627cdaa9768431e15
dir: /src/pt2_audio.c/
/* The "LED" filter and BLEP routines were coded by aciddose. ** Low-pass filter is based on https://bel.fi/alankila/modguide/interpolate.txt */ // for finding memory leaks in debug mode with Visual Studio #if defined _DEBUG && defined _MSC_VER #include <crtdbg.h> #endif #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <stdbool.h> #include <SDL2/SDL.h> #ifdef _WIN32 #include <io.h> #else #include <unistd.h> #endif #include <math.h> // sqrt(),tan(),M_PI #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <limits.h> #include "pt2_audio.h" #include "pt2_header.h" #include "pt2_helpers.h" #include "pt2_blep.h" #include "pt2_config.h" #include "pt2_tables.h" #include "pt2_palette.h" #include "pt2_textout.h" #include "pt2_visuals.h" #include "pt2_scopes.h" #define INITIAL_DITHER_SEED 0x12345000 #define DENORMAL_OFFSET 1e-10 typedef struct ledFilter_t { double dLed[4]; } ledFilter_t; typedef struct ledFilterCoeff_t { double dLed, dLedFb; } ledFilterCoeff_t; typedef struct voice_t { volatile bool active; const int8_t *data, *newData; int32_t length, newLength, pos; double dVolume, dDelta, dDeltaMul, dPhase, dLastDelta, dLastDeltaMul, dLastPhase, dPanL, dPanR; } paulaVoice_t; static volatile int8_t filterFlags; static volatile bool audioLocked; static int8_t defStereoSep; static bool amigaPanFlag, wavRenderingDone; static uint16_t ch1Pan, ch2Pan, ch3Pan, ch4Pan, oldPeriod; static int32_t sampleCounter, maxSamplesToMix, randSeed = INITIAL_DITHER_SEED; static uint32_t oldScopeDelta; static double *dMixBufferL, *dMixBufferR, *dMixBufferLUnaligned, *dMixBufferRUnaligned, dOldVoiceDelta, dOldVoiceDeltaMul; static double dPrngStateL, dPrngStateR; static blep_t blep[AMIGA_VOICES], blepVol[AMIGA_VOICES]; static lossyIntegrator_t filterLo, filterHi; static ledFilterCoeff_t filterLEDC; static ledFilter_t filterLED; static paulaVoice_t paula[AMIGA_VOICES]; static SDL_AudioDeviceID dev; // globalized bool forceMixerOff = false; int32_t samplesPerTick; bool intMusic(void); // defined in pt_modplayer.c void storeTempVariables(void); // defined in pt_modplayer.c void calcMod2WavTotalRows(void); static uint16_t bpm2SmpsPerTick(uint32_t bpm, uint32_t audioFreq) { uint32_t ciaVal; double dFreqMul; if (bpm == 0) return 0; ciaVal = (uint32_t)(1773447 / bpm); // yes, PT truncates here dFreqMul = ciaVal * (1.0 / CIA_PAL_CLK); int32_t smpsPerTick = (int32_t)((audioFreq * dFreqMul) + 0.5); return (uint16_t)smpsPerTick; } static void generateBpmTables(void) { for (uint32_t i = 32; i <= 255; i++) { audio.bpmTab[i-32] = bpm2SmpsPerTick(i, audio.audioFreq); audio.bpmTab28kHz[i-32] = bpm2SmpsPerTick(i, 28836); audio.bpmTab22kHz[i-32] = bpm2SmpsPerTick(i, 22168); } } void setLEDFilter(bool state) { editor.useLEDFilter = state; if (editor.useLEDFilter) filterFlags |= FILTER_LED_ENABLED; else filterFlags &= ~FILTER_LED_ENABLED; } void toggleLEDFilter(void) { editor.useLEDFilter ^= 1; if (editor.useLEDFilter) filterFlags |= FILTER_LED_ENABLED; else filterFlags &= ~FILTER_LED_ENABLED; } static void calcCoeffLED(double dSr, double dHz, ledFilterCoeff_t *filter) { static double dFb = 0.125; #ifndef NO_FILTER_FINETUNING /* 8bitbubsy: makes the filter curve sound (and look) much closer to the real deal. ** This has been tested against both an A500 and A1200 (real units). */ dFb *= 0.62; #endif if (dHz < dSr/2.0) filter->dLed = ((2.0 * M_PI) * dHz) / dSr; else filter->dLed = 1.0; filter->dLedFb = dFb + (dFb / (1.0 - filter->dLed)); // Q ~= 1/sqrt(2) (Butterworth) } void calcCoeffLossyIntegrator(double dSr, double dHz, lossyIntegrator_t *filter) { double dOmega = ((2.0 * M_PI) * dHz) / dSr; filter->b0 = 1.0 / (1.0 + (1.0 / dOmega)); filter->b1 = 1.0 - filter->b0; } static void clearLossyIntegrator(lossyIntegrator_t *filter) { filter->dBuffer[0] = 0.0; // L filter->dBuffer[1] = 0.0; // R } static void clearLEDFilter(ledFilter_t *filter) { filter->dLed[0] = 0.0; // L filter->dLed[1] = 0.0; filter->dLed[2] = 0.0; // R filter->dLed[3] = 0.0; } static inline void lossyIntegratorLED(ledFilterCoeff_t filterC, ledFilter_t *filter, double *dIn, double *dOut) { // left channel "LED" filter filter->dLed[0] += filterC.dLed * (dIn[0] - filter->dLed[0]) + filterC.dLedFb * (filter->dLed[0] - filter->dLed[1]) + DENORMAL_OFFSET; filter->dLed[1] += filterC.dLed * (filter->dLed[0] - filter->dLed[1]) + DENORMAL_OFFSET; dOut[0] = filter->dLed[1]; // right channel "LED" filter filter->dLed[2] += filterC.dLed * (dIn[1] - filter->dLed[2]) + filterC.dLedFb * (filter->dLed[2] - filter->dLed[3]) + DENORMAL_OFFSET; filter->dLed[3] += filterC.dLed * (filter->dLed[2] - filter->dLed[3]) + DENORMAL_OFFSET; dOut[1] = filter->dLed[3]; } void lossyIntegrator(lossyIntegrator_t *filter, double *dIn, double *dOut) { /* Low-pass filter implementation taken from: ** https://bel.fi/alankila/modguide/interpolate.txt ** ** This implementation has a less smooth cutoff curve compared to the old one, so it's ** maybe not the best. However, I stick to this one because it has a higher gain ** at the end of the curve (closer to real tested Amiga 500). It also sounds much closer when ** comparing whitenoise on an A500. */ // left channel low-pass filter->dBuffer[0] = (filter->b0 * dIn[0]) + (filter->b1 * filter->dBuffer[0]) + DENORMAL_OFFSET; dOut[0] = filter->dBuffer[0]; // right channel low-pass filter->dBuffer[1] = (filter->b0 * dIn[1]) + (filter->b1 * filter->dBuffer[1]) + DENORMAL_OFFSET; dOut[1] = filter->dBuffer[1]; } void lossyIntegratorHighPass(lossyIntegrator_t *filter, double *dIn, double *dOut) { double dLow[2]; lossyIntegrator(filter, dIn, dLow); dOut[0] = dIn[0] - dLow[0]; // left channel high-pass dOut[1] = dIn[1] - dLow[1]; // right channel high-pass } /* adejr/aciddose: these sin/cos approximations both use a 0..1 ** parameter range and have 'normalized' (1/2 = 0db) coeffs ** ** the coeffs are for LERP(x, x * x, 0.224) * sqrt(2) ** max_error is minimized with 0.224 = 0.0013012886 */ static double sinApx(double fX) { fX = fX * (2.0 - fX); return fX * 1.09742972 + fX * fX * 0.31678383; } static double cosApx(double fX) { fX = (1.0 - fX) * (1.0 + fX); return fX * 1.09742972 + fX * fX * 0.31678383; } void lockAudio(void) { if (dev != 0) SDL_LockAudioDevice(dev); audioLocked = true; } void unlockAudio(void) { if (dev != 0) SDL_UnlockAudioDevice(dev); audioLocked = false; } void clearPaulaAndScopes(void) { uint8_t i; double dOldPanL[4], dOldPanR[4]; // copy old pans for (i = 0; i < AMIGA_VOICES; i++) { dOldPanL[i] = paula[i].dPanL; dOldPanR[i] = paula[i].dPanR; } lockAudio(); memset(paula, 0, sizeof (paula)); unlockAudio(); // store old pans for (i = 0; i < AMIGA_VOICES; i++) { paula[i].dPanL = dOldPanL[i]; paula[i].dPanR = dOldPanR[i]; } clearScopes(); } void mixerUpdateLoops(void) // updates Paula loop (+ scopes) { moduleChannel_t *ch; moduleSample_t *s; for (uint8_t i = 0; i < AMIGA_VOICES; i++) { ch = &modEntry->channels[i]; if (ch->n_samplenum == editor.currSample) { s = &modEntry->samples[editor.currSample]; paulaSetData(i, ch->n_start + s->loopStart); paulaSetLength(i, s->loopLength / 2); } } } static void mixerSetVoicePan(uint8_t ch, uint16_t pan) // pan = 0..256 { double dPan; /* proper 'normalized' equal-power panning is (assuming pan left to right): ** L = cos(p * pi * 1/2) * sqrt(2); ** R = sin(p * pi * 1/2) * sqrt(2); */ dPan = pan * (1.0 / 256.0); // 0.0..1.0 paula[ch].dPanL = cosApx(dPan); paula[ch].dPanR = sinApx(dPan); } void mixerKillVoice(uint8_t ch) { paulaVoice_t *v; scopeChannelExt_t *s; v = &paula[ch]; s = &scopeExt[ch]; v->active = false; v->dVolume = 0.0; s->active = false; s->didSwapData = false; memset(&blep[ch], 0, sizeof (blep_t)); memset(&blepVol[ch], 0, sizeof (blep_t)); } void turnOffVoices(void) { for (uint8_t i = 0; i < AMIGA_VOICES; i++) mixerKillVoice(i); clearLossyIntegrator(&filterLo); clearLossyIntegrator(&filterHi); clearLEDFilter(&filterLED); resetAudioDithering(); editor.tuningFlag = false; } void paulaStopDMA(uint8_t ch) { scopeExt[ch].active = paula[ch].active = false; } void paulaStartDMA(uint8_t ch) { const int8_t *dat; int32_t length; paulaVoice_t *v; scopeChannel_t s, *sc; scopeChannelExt_t *se; // trigger voice v = &paula[ch]; dat = v->newData; if (dat == NULL) dat = &modEntry->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample length = v->newLength; if (length < 2) length = 2; // for safety v->dPhase = 0.0; v->pos = 0; v->data = dat; v->length = length; v->active = true; // trigger scope sc = &scope[ch]; se = &scopeExt[ch]; s = *sc; // cache it dat = se->newData; if (dat == NULL) dat = &modEntry->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample s.length = length; s.data = dat; s.pos = 0; s.posFrac = 0; // data/length is already set from replayer thread (important) s.loopFlag = se->newLoopFlag; s.loopStart = se->newLoopStart; se->didSwapData = false; se->active = true; *sc = s; // update it } void resetCachedMixerPeriod(void) { oldPeriod = 0; } void paulaSetPeriod(uint8_t ch, uint16_t period) { double dPeriodToDeltaDiv; paulaVoice_t *v; v = &paula[ch]; if (period == 0) { v->dDelta = 0.0; // confirmed behavior on real Amiga v->dDeltaMul = 1.0; // for BLEP synthesis setScopeDelta(ch, 0); return; } if (period < 113) period = 113; // confirmed behavior on real Amiga // if the new period was the same as the previous period, use cached deltas if (period == oldPeriod) { v->dDelta = dOldVoiceDelta; v->dDeltaMul = dOldVoiceDeltaMul; // for BLEP synthesis setScopeDelta(ch, oldScopeDelta); } else { // this period is not cached, calculate mixer/scope deltas #if SCOPE_HZ != 64 #error Scope Hz is not 64 (2^n), change rate calc. to use doubles+round in pt2_scope.c #endif oldPeriod = period; // if we are rendering pattern to sample (PAT2SMP), use different frequencies if (editor.isSMPRendering) dPeriodToDeltaDiv = editor.pat2SmpHQ ? (PAULA_PAL_CLK / 28836.0) : (PAULA_PAL_CLK / 22168.0); else dPeriodToDeltaDiv = audio.dPeriodToDeltaDiv; v->dDelta = dPeriodToDeltaDiv / period; v->dDeltaMul = 1.0 / v->dDelta; // for BLEP synthesis // cache these dOldVoiceDelta = v->dDelta; dOldVoiceDeltaMul = v->dDeltaMul; // for BLEP synthesis oldScopeDelta = (PAULA_PAL_CLK * (65536UL / SCOPE_HZ)) / period; setScopeDelta(ch, oldScopeDelta); } // for BLEP synthesis if (v->dLastDelta == 0.0) v->dLastDelta = v->dDelta; if (v->dLastDeltaMul == 0.0) v->dLastDeltaMul = v->dDeltaMul; } void paulaSetVolume(uint8_t ch, uint16_t vol) { vol &= 127; // confirmed behavior on real Amiga if (vol > 64) vol = 64; // confirmed behavior on real Amiga paula[ch].dVolume = vol * (1.0 / 64.0); } void paulaSetLength(uint8_t ch, uint16_t len) { if (len == 0) { len = 65535; /* confirmed behavior on real Amiga (also needed for safety) ** And yes, we have room for this, it will never overflow! */ } // our mixer works with bytes, not words. Multiply by two scopeExt[ch].newLength = paula[ch].newLength = len * 2; } void paulaSetData(uint8_t ch, const int8_t *src) { uint8_t smp; moduleSample_t *s; scopeChannelExt_t *se, tmp; smp = modEntry->channels[ch].n_samplenum; assert(smp <= 30); s = &modEntry->samples[smp]; // set voice data if (src == NULL) src = &modEntry->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample paula[ch].newData = src; // set external scope data se = &scopeExt[ch]; tmp = *se; // cache it tmp.newData = src; tmp.newLoopFlag = (s->loopStart + s->loopLength) > 2; tmp.newLoopStart = s->loopStart; *se = tmp; // update it } void toggleA500Filters(void) { if (filterFlags & FILTER_A500) { filterFlags &= ~FILTER_A500; displayMsg("FILTER MOD: A1200"); } else { filterFlags |= FILTER_A500; clearLossyIntegrator(&filterLo); displayMsg("FILTER MOD: A500"); } } void mixChannels(int32_t numSamples) { const int8_t *dataPtr; double dTempSample, dTempVolume; blep_t *bSmp, *bVol; paulaVoice_t *v; memset(dMixBufferL, 0, numSamples * sizeof (double)); memset(dMixBufferR, 0, numSamples * sizeof (double)); for (int32_t i = 0; i < AMIGA_VOICES; i++) { v = &paula[i]; if (!v->active) continue; bSmp = &blep[i]; bVol = &blepVol[i]; for (int32_t j = 0; j < numSamples; j++) { dataPtr = v->data; if (dataPtr == NULL) { dTempSample = 0.0; dTempVolume = 0.0; } else { dTempSample = dataPtr[v->pos] * (1.0 / 128.0); dTempVolume = v->dVolume; } if (dTempSample != bSmp->dLastValue) { if (v->dLastDelta > v->dLastPhase) { // div->mul trick: v->dLastDeltaMul is 1.0 / v->dLastDelta blepAdd(bSmp, v->dLastPhase * v->dLastDeltaMul, bSmp->dLastValue - dTempSample); } bSmp->dLastValue = dTempSample; } if (dTempVolume != bVol->dLastValue) { blepVolAdd(bVol, bVol->dLastValue - dTempVolume); bVol->dLastValue = dTempVolume; } if (bSmp->samplesLeft > 0) dTempSample += blepRun(bSmp); if (bVol->samplesLeft > 0) dTempVolume += blepRun(bVol); dTempSample *= dTempVolume; dMixBufferL[j] += dTempSample * v->dPanL; dMixBufferR[j] += dTempSample * v->dPanR; v->dPhase += v->dDelta; // PAT2SMP needs multi-step, so use while() here (will be only one iteration in normal mixing mode) while (v->dPhase >= 1.0) { v->dPhase -= 1.0; v->dLastPhase = v->dPhase; v->dLastDelta = v->dDelta; v->dLastDeltaMul = v->dDeltaMul; if (++v->pos >= v->length) { v->pos = 0; // re-fetch new Paula register values now v->length = v->newLength; v->data = v->newData; } } } } } void resetAudioDithering(void) { randSeed = INITIAL_DITHER_SEED; dPrngStateL = 0.0; dPrngStateR = 0.0; } static inline int32_t random32(void) { // LCG random 32-bit generator (quite good and fast) randSeed *= 134775813; randSeed++; return randSeed; } static inline void processMixedSamplesA1200(int32_t i, int16_t *out) { int32_t smp32; double dOut[2], dPrng; dOut[0] = dMixBufferL[i]; dOut[1] = dMixBufferR[i]; // don't process any low-pass filter since the cut-off is around 28-31kHz on A1200 // process "LED" filter if (filterFlags & FILTER_LED_ENABLED) lossyIntegratorLED(filterLEDC, &filterLED, dOut, dOut); // process high-pass filter lossyIntegratorHighPass(&filterHi, dOut, dOut); // normalize and flip phase (A500/A1200 has an inverted audio signal) dOut[0] *= -(INT16_MAX / AMIGA_VOICES); dOut[1] *= -(INT16_MAX / AMIGA_VOICES); // left channel - 1-bit triangular dithering (high-pass filtered) dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5 dOut[0] = (dOut[0] + dPrng) - dPrngStateL; dPrngStateL = dPrng; smp32 = (int32_t)dOut[0]; CLAMP16(smp32); out[0] = (int16_t)smp32; // right channel - 1-bit triangular dithering (high-pass filtered) dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5 dOut[1] = (dOut[1] + dPrng) - dPrngStateR; dPrngStateR = dPrng; smp32 = (int32_t)dOut[1]; CLAMP16(smp32); out[1] = (int16_t)smp32; } static inline void processMixedSamplesA500(int32_t i, int16_t *out) { int32_t smp32; double dOut[2], dPrng; dOut[0] = dMixBufferL[i]; dOut[1] = dMixBufferR[i]; // process low-pass filter lossyIntegrator(&filterLo, dOut, dOut); // process "LED" filter if (filterFlags & FILTER_LED_ENABLED) lossyIntegratorLED(filterLEDC, &filterLED, dOut, dOut); // process high-pass filter lossyIntegratorHighPass(&filterHi, dOut, dOut); dOut[0] *= -(INT16_MAX / AMIGA_VOICES); dOut[1] *= -(INT16_MAX / AMIGA_VOICES); // left channel - 1-bit triangular dithering (high-pass filtered) dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5 dOut[0] = (dOut[0] + dPrng) - dPrngStateL; dPrngStateL = dPrng; smp32 = (int32_t)dOut[0]; CLAMP16(smp32); out[0] = (int16_t)smp32; // right channel - 1-bit triangular dithering (high-pass filtered) dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5 dOut[1] = (dOut[1] + dPrng) - dPrngStateR; dPrngStateR = dPrng; smp32 = (int32_t)dOut[1]; CLAMP16(smp32); out[1] = (int16_t)smp32; } void outputAudio(int16_t *target, int32_t numSamples) { int16_t *outStream, out[2]; int32_t j; mixChannels(numSamples); if (editor.isSMPRendering) { // render to sample (PAT2SMP) for (j = 0; j < numSamples; j++) { processMixedSamplesA1200(j, out); editor.pat2SmpBuf[editor.pat2SmpPos++] = (int16_t)((out[0] + out[1]) >> 1); // mix to mono if (editor.pat2SmpPos >= MAX_SAMPLE_LEN) { editor.smpRenderingDone = true; updateWindowTitle(MOD_IS_MODIFIED); break; } } } else { // render to stream outStream = target; if (filterFlags & FILTER_A500) { for (j = 0; j < numSamples; j++) { processMixedSamplesA500(j, out); *outStream++ = out[0]; *outStream++ = out[1]; } } else { for (j = 0; j < numSamples; j++) { processMixedSamplesA1200(j, out); *outStream++ = out[0]; *outStream++ = out[1]; } } } } static void SDLCALL audioCallback(void *userdata, Uint8 *stream, int len) { int16_t *out; int32_t sampleBlock, samplesTodo; (void)userdata; if (forceMixerOff) // during MOD2WAV { memset(stream, 0, len); return; } out = (int16_t *)stream; sampleBlock = len >> 2; while (sampleBlock) { samplesTodo = (sampleBlock < sampleCounter) ? sampleBlock : sampleCounter; if (samplesTodo > 0) { outputAudio(out, samplesTodo); out += (uint32_t)samplesTodo * 2; sampleBlock -= samplesTodo; sampleCounter -= samplesTodo; } else { if (editor.songPlaying) intMusic(); sampleCounter = samplesPerTick; } } } static void calculateFilterCoeffs(void) { double dCutOffHz; /* Amiga 500 filter emulation, by aciddose ** ** First comes a static low-pass 6dB formed by the supply current ** from the Paula's mixture of channels A+B / C+D into the opamp with ** 0.1uF capacitor and 360 ohm resistor feedback in inverting mode biased by ** dac vRef (used to center the output). ** ** R = 360 ohm ** C = 0.1uF ** Low Hz = 4420.97~ = 1 / (2pi * 360 * 0.0000001) ** ** Under spice simulation the circuit yields -3dB = 4400Hz. ** In the Amiga 1200, the low-pass cutoff is 26kHz+, so the ** static low-pass filter is disabled in the mixer in A1200 mode. ** ** Next comes a bog-standard Sallen-Key filter ("LED") with: ** R1 = 10K ohm ** R2 = 10K ohm ** C1 = 6800pF ** C2 = 3900pF ** Q ~= 1/sqrt(2) ** ** This filter is optionally bypassed by an MPF-102 JFET chip when ** the LED filter is turned off. ** ** Under spice simulation the circuit yields -3dB = 2800Hz. ** 90 degrees phase = 3000Hz (so, should oscillate at 3kHz!) ** ** The buffered output of the Sallen-Key passes into an RC high-pass with: ** R = 1.39K ohm (1K ohm + 390 ohm) ** C = 22uF (also C = 330nF, for improved high-frequency) ** ** High Hz = 5.2~ = 1 / (2pi * 1390 * 0.000022) ** Under spice simulation the circuit yields -3dB = 5.2Hz. */ // Amiga 500 rev6 RC low-pass filter: const double dLp_R = 360.0; // R321 - 360 ohm resistor const double dLp_C = 1e-7; // C321 - 0.1uF capacitor dCutOffHz = 1.0 / ((2.0 * M_PI) * dLp_R * dLp_C); // ~4420.97Hz #ifndef NO_FILTER_FINETUNING dCutOffHz += 580.0; // 8bitbubsy: finetuning to better match A500 low-pass testing #endif calcCoeffLossyIntegrator(audio.dAudioFreq, dCutOffHz, &filterLo); // Amiga Sallen-Key "LED" filter: const double dLed_R1 = 10000.0; // R322 - 10K ohm resistor const double dLed_R2 = 10000.0; // R323 - 10K ohm resistor const double dLed_C1 = 6.8e-9; // C322 - 6800pF capacitor const double dLed_C2 = 3.9e-9; // C323 - 3900pF capacitor dCutOffHz = 1.0 / ((2.0 * M_PI) * sqrt(dLed_R1 * dLed_R2 * dLed_C1 * dLed_C2)); // ~3090.53Hz #ifndef NO_FILTER_FINETUNING dCutOffHz -= 300.0; // 8bitbubsy: finetuning to better match A500 & A1200 "LED" filter testing #endif calcCoeffLED(audio.dAudioFreq, dCutOffHz, &filterLEDC); // Amiga RC high-pass filter: const double dHp_R = 1000.0 + 390.0; // R324 - 1K ohm resistor + R325 - 390 ohm resistor const double dHp_C = 2.2e-5; // C334 - 22uF capacitor dCutOffHz = 1.0 / ((2.0 * M_PI) * dHp_R * dHp_C); // ~5.20Hz #ifndef NO_FILTER_FINETUNING dCutOffHz += 1.5; // 8bitbubsy: finetuning to better match A500 & A1200 high-pass testing #endif calcCoeffLossyIntegrator(audio.dAudioFreq, dCutOffHz, &filterHi); } void mixerCalcVoicePans(uint8_t stereoSeparation) { uint8_t scaledPanPos = (stereoSeparation * 128) / 100; ch1Pan = 128 - scaledPanPos; ch2Pan = 128 + scaledPanPos; ch3Pan = 128 + scaledPanPos; ch4Pan = 128 - scaledPanPos; mixerSetVoicePan(0, ch1Pan); mixerSetVoicePan(1, ch2Pan); mixerSetVoicePan(2, ch3Pan); mixerSetVoicePan(3, ch4Pan); } bool setupAudio(void) { SDL_AudioSpec want, have; want.freq = ptConfig.soundFrequency; want.format = AUDIO_S16; want.channels = 2; want.callback = audioCallback; want.userdata = NULL; want.samples = ptConfig.soundBufferSize; dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); if (dev == 0) { showErrorMsgBox("Unable to open audio device: %s", SDL_GetError()); return false; } if (have.freq < 32000) // lower than this is not safe for one-step mixer w/ BLEP { showErrorMsgBox("Unable to open audio: The audio output rate couldn't be used!"); return false; } if (have.format != want.format) { showErrorMsgBox("Unable to open audio: The sample format (signed 16-bit) couldn't be used!"); return false; } maxSamplesToMix = (int32_t)ceil((have.freq * 2.5) / 32.0); dMixBufferLUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double), 256); dMixBufferRUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double), 256); editor.mod2WavBuffer = (int16_t *)malloc(sizeof (int16_t) * maxSamplesToMix); if (dMixBufferLUnaligned == NULL || dMixBufferRUnaligned == NULL || editor.mod2WavBuffer == NULL) { showErrorMsgBox("Out of memory!"); return false; } dMixBufferL = (double *)ALIGN_PTR(dMixBufferLUnaligned, 256); dMixBufferR = (double *)ALIGN_PTR(dMixBufferRUnaligned, 256); audio.audioBufferSize = have.samples; ptConfig.soundFrequency = have.freq; audio.audioFreq = ptConfig.soundFrequency; audio.dAudioFreq = (double)ptConfig.soundFrequency; audio.dPeriodToDeltaDiv = PAULA_PAL_CLK / audio.dAudioFreq; mixerCalcVoicePans(ptConfig.stereoSeparation); defStereoSep = ptConfig.stereoSeparation; filterFlags = ptConfig.a500LowPassFilter ? FILTER_A500 : 0; calculateFilterCoeffs(); generateBpmTables(); samplesPerTick = 0; sampleCounter = 0; SDL_PauseAudioDevice(dev, false); return true; } void audioClose(void) { if (dev > 0) { SDL_PauseAudioDevice(dev, true); SDL_CloseAudioDevice(dev); dev = 0; } if (dMixBufferLUnaligned != NULL) { free(dMixBufferLUnaligned); dMixBufferLUnaligned = NULL; } if (dMixBufferRUnaligned != NULL) { free(dMixBufferRUnaligned); dMixBufferRUnaligned = NULL; } if (editor.mod2WavBuffer != NULL) { free(editor.mod2WavBuffer); editor.mod2WavBuffer = NULL; } } void mixerSetSamplesPerTick(int32_t val) { samplesPerTick = val; } void mixerClearSampleCounter(void) { sampleCounter = 0; } void toggleAmigaPanMode(void) { amigaPanFlag ^= 1; if (!amigaPanFlag) { mixerCalcVoicePans(defStereoSep); displayMsg("AMIGA PANNING OFF"); } else { mixerCalcVoicePans(100); displayMsg("AMIGA PANNING ON"); } } // PAT2SMP RELATED STUFF uint32_t getAudioFrame(int16_t *outStream) { int32_t smpCounter, samplesToMix; if (!intMusic()) wavRenderingDone = true; smpCounter = samplesPerTick; while (smpCounter > 0) { samplesToMix = smpCounter; if (samplesToMix > maxSamplesToMix) samplesToMix = maxSamplesToMix; outputAudio(outStream, samplesToMix); outStream += (uint32_t)samplesToMix * 2; smpCounter -= samplesToMix; } return (uint32_t)samplesPerTick * 2; // * 2 for stereo } static int32_t SDLCALL mod2WavThreadFunc(void *ptr) { uint32_t size, totalSampleCounter, totalRiffChunkLen; FILE *fOut; wavHeader_t wavHeader; fOut = (FILE *)ptr; if (fOut == NULL) return true; // skip wav header place, render data first fseek(fOut, sizeof (wavHeader_t), SEEK_SET); wavRenderingDone = false; totalSampleCounter = 0; while (editor.isWAVRendering && !wavRenderingDone && !editor.abortMod2Wav) { size = getAudioFrame(editor.mod2WavBuffer); if (size > 0) { fwrite(editor.mod2WavBuffer, sizeof (int16_t), size, fOut); totalSampleCounter += size; } editor.ui.updateMod2WavDialog = true; } if (totalSampleCounter & 1) fputc(0, fOut); // pad align byte if ((ftell(fOut) - 8) > 0) totalRiffChunkLen = ftell(fOut) - 8; else totalRiffChunkLen = 0; editor.ui.mod2WavFinished = true; editor.ui.updateMod2WavDialog = true; // go back and fill the missing WAV header fseek(fOut, 0, SEEK_SET); wavHeader.chunkID = 0x46464952; // "RIFF" wavHeader.chunkSize = totalRiffChunkLen; wavHeader.format = 0x45564157; // "WAVE" wavHeader.subchunk1ID = 0x20746D66; // "fmt " wavHeader.subchunk1Size = 16; wavHeader.audioFormat = 1; wavHeader.numChannels = 2; wavHeader.sampleRate = audio.audioFreq; wavHeader.bitsPerSample = 16; wavHeader.byteRate = wavHeader.sampleRate * wavHeader.numChannels * (wavHeader.bitsPerSample / 8); wavHeader.blockAlign = wavHeader.numChannels * (wavHeader.bitsPerSample / 8); wavHeader.subchunk2ID = 0x61746164; // "data" wavHeader.subchunk2Size = totalSampleCounter * (wavHeader.bitsPerSample / 8); fwrite(&wavHeader, sizeof (wavHeader_t), 1, fOut); fclose(fOut); return true; } bool renderToWav(char *fileName, bool checkIfFileExist) { FILE *fOut; struct stat statBuffer; if (checkIfFileExist) { if (stat(fileName, &statBuffer) == 0) { editor.ui.askScreenShown = true; editor.ui.askScreenType = ASK_MOD2WAV_OVERWRITE; pointerSetMode(POINTER_MODE_MSG1, NO_CARRY); setStatusMessage("OVERWRITE FILE?", NO_CARRY); renderAskDialog(); return false; } } if (editor.ui.askScreenShown) { editor.ui.askScreenShown = false; editor.ui.answerNo = false; editor.ui.answerYes = false; } fOut = fopen(fileName, "wb"); if (fOut == NULL) { displayErrorMsg("FILE I/O ERROR"); return false; } storeTempVariables(); calcMod2WavTotalRows(); restartSong(); editor.blockMarkFlag = false; pointerSetMode(POINTER_MODE_MSG2, NO_CARRY); setStatusMessage("RENDERING MOD...", NO_CARRY); editor.ui.disableVisualizer = true; editor.isWAVRendering = true; renderMOD2WAVDialog(); editor.abortMod2Wav = false; editor.mod2WavThread = SDL_CreateThread(mod2WavThreadFunc, NULL, fOut); if (editor.mod2WavThread != NULL) { SDL_DetachThread(editor.mod2WavThread); } else { editor.ui.disableVisualizer = false; editor.isWAVRendering = false; displayErrorMsg("THREAD ERROR"); pointerSetMode(POINTER_MODE_IDLE, DO_CARRY); statusAllRight(); return false; } return true; } // for MOD2WAV - ONLY used for a visual percentage counter, so accuracy is not important void calcMod2WavTotalRows(void) { bool pBreakFlag, posJumpAssert, calcingRows; int8_t n_pattpos[AMIGA_VOICES], n_loopcount[AMIGA_VOICES]; uint8_t modRow, pBreakPosition, ch, pos; int16_t modOrder; uint16_t modPattern; note_t *note; // for pattern loop memset(n_pattpos, 0, sizeof (n_pattpos)); memset(n_loopcount, 0, sizeof (n_loopcount)); modEntry->rowsCounter = 0; modEntry->rowsInTotal = 0; modRow = 0; modOrder = 0; modPattern = modEntry->head.order[0]; pBreakPosition = 0; posJumpAssert = false; pBreakFlag = false; calcingRows = true; memset(editor.rowVisitTable, 0, MOD_ORDERS * MOD_ROWS); while (calcingRows) { editor.rowVisitTable[(modOrder * MOD_ROWS) + modRow] = true; for (ch = 0; ch < AMIGA_VOICES; ch++) { note = &modEntry->patterns[modPattern][(modRow * AMIGA_VOICES) + ch]; if (note->command == 0x0B) // Bxx - Position Jump { modOrder = note->param - 1; pBreakPosition = 0; posJumpAssert = true; } else if (note->command == 0x0D) // Dxx - Pattern Break { pBreakPosition = (((note->param >> 4) * 10) + (note->param & 0x0F)); if (pBreakPosition > 63) pBreakPosition = 0; posJumpAssert = true; } else if (note->command == 0x0F && note->param == 0) // F00 - Set Speed 0 (stop) { calcingRows = false; break; } else if (note->command == 0x0E && (note->param >> 4) == 0x06) // E6x - Pattern Loop { pos = note->param & 0x0F; if (pos == 0) { n_pattpos[ch] = modRow; } else { // this is so ugly if (n_loopcount[ch] == 0) { n_loopcount[ch] = pos; pBreakPosition = n_pattpos[ch]; pBreakFlag = true; for (pos = pBreakPosition; pos <= modRow; pos++) editor.rowVisitTable[(modOrder * MOD_ROWS) + pos] = false; } else { if (--n_loopcount[ch]) { pBreakPosition = n_pattpos[ch]; pBreakFlag = true; for (pos = pBreakPosition; pos <= modRow; pos++) editor.rowVisitTable[(modOrder * MOD_ROWS) + pos] = false; } } } } } modRow++; modEntry->rowsInTotal++; if (pBreakFlag) { modRow = pBreakPosition; pBreakPosition = 0; pBreakFlag = false; } if (modRow >= MOD_ROWS || posJumpAssert) { modRow = pBreakPosition; pBreakPosition = 0; posJumpAssert = false; modOrder = (modOrder + 1) & 0x7F; if (modOrder >= modEntry->head.orderCount) { modOrder = 0; calcingRows = false; break; } modPattern = modEntry->head.order[modOrder]; if (modPattern > MAX_PATTERNS-1) modPattern = MAX_PATTERNS-1; } if (editor.rowVisitTable[(modOrder * MOD_ROWS) + modRow]) { // row has been visited before, we're now done! calcingRows = false; break; } } } void normalize32bitSigned(int32_t *sampleData, uint32_t sampleLength) { int32_t sample, sampleVolPeak; uint32_t i; double dGain; sampleVolPeak = 0; for (i = 0; i < sampleLength; i++) { sample = ABS(sampleData[i]); if (sampleVolPeak < sample) sampleVolPeak = sample; } if (sampleVolPeak >= INT32_MAX) return; // sample is already normalized // prevent division by zero! if (sampleVolPeak <= 0) sampleVolPeak = 1; dGain = (double)INT32_MAX / sampleVolPeak; for (i = 0; i < sampleLength; i++) { sample = (int32_t)(sampleData[i] * dGain); sampleData[i] = (int32_t)sample; } } void normalize16bitSigned(int16_t *sampleData, uint32_t sampleLength) { uint32_t i; int32_t sample, sampleVolPeak, gain; sampleVolPeak = 0; for (i = 0; i < sampleLength; i++) { sample = ABS(sampleData[i]); if (sampleVolPeak < sample) sampleVolPeak = sample; } if (sampleVolPeak >= INT16_MAX) return; // sample is already normalized if (sampleVolPeak < 1) return; gain = (INT16_MAX * 65536) / sampleVolPeak; for (i = 0; i < sampleLength; i++) sampleData[i] = (int16_t)((sampleData[i] * gain) >> 16); } void normalize8bitFloatSigned(float *fSampleData, uint32_t sampleLength) { uint32_t i; float fSample, fSampleVolPeak, fGain; fSampleVolPeak = 0.0f; for (i = 0; i < sampleLength; i++) { fSample = fabsf(fSampleData[i]); if (fSampleVolPeak < fSample) fSampleVolPeak = fSample; } if (fSampleVolPeak <= 0.0f) return; fGain = INT8_MAX / fSampleVolPeak; for (i = 0; i < sampleLength; i++) fSampleData[i] *= fGain; } void normalize8bitDoubleSigned(double *dSampleData, uint32_t sampleLength) { uint32_t i; double dSample, dSampleVolPeak, dGain; dSampleVolPeak = 0.0; for (i = 0; i < sampleLength; i++) { dSample = fabs(dSampleData[i]); if (dSampleVolPeak < dSample) dSampleVolPeak = dSample; } if (dSampleVolPeak <= 0.0) return; dGain = INT8_MAX / dSampleVolPeak; for (i = 0; i < sampleLength; i++) dSampleData[i] *= dGain; }