1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
|
/*
* Paula.cpp
* ---------
* Purpose: Emulating the Amiga's sound chip, Paula, by implementing resampling using band-limited steps (BLEPs)
* Notes : The BLEP table generator code is a translation of Antti S. Lankila's original Python code.
* Authors: OpenMPT Devs
* Antti S. Lankila
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "Paula.h"
#include "TinyFFT.h"
#include "Tables.h"
#include "mpt/base/numbers.hpp"
#include <complex>
#include <numeric>
OPENMPT_NAMESPACE_BEGIN
namespace Paula
{
namespace
{
MPT_NOINLINE std::vector<double> KaiserFIR(int numTaps, double cutoff, double beta)
{
const double izeroBeta = Izero(beta);
const double kPi = 4.0 * std::atan(1.0) * cutoff;
const double xDiv = 1.0 / ((numTaps / 2) * (numTaps / 2));
const int numTapsDiv2 = numTaps / 2;
std::vector<double> result(numTaps);
for(int i = 0; i < numTaps; i++)
{
double fsinc;
if(i == numTapsDiv2)
{
fsinc = 1.0;
} else
{
const double x = i - numTapsDiv2;
const double xPi = x * kPi;
// - sinc - - Kaiser window - -sinc-
fsinc = std::sin(xPi) * Izero(beta * std::sqrt(1 - x * x * xDiv)) / (izeroBeta * xPi);
}
result[i] = fsinc * cutoff;
}
return result;
}
MPT_NOINLINE void FIR_MinPhase(std::vector<double> &table, const TinyFFT &fft)
{
std::vector<std::complex<double>> cepstrum(fft.Size());
MPT_ASSERT(cepstrum.size() >= table.size());
for(size_t i = 0; i < table.size(); i++)
cepstrum[i] = table[i];
// Compute the real cepstrum: fft -> abs + ln -> ifft -> real
fft.FFT(cepstrum);
for(auto &v : cepstrum)
v = std::log(std::abs(v));
fft.IFFT(cepstrum);
fft.Normalize(cepstrum);
// Window the cepstrum in such a way that anticausal components become rejected
for(size_t i = 1; i < cepstrum.size() / 2; i++)
{
cepstrum[i] *= 2;
cepstrum[i + cepstrum.size() / 2] *= 0;
}
// Now cancel the previous steps: fft -> exp -> ifft -> real
fft.FFT(cepstrum);
for(auto &v : cepstrum)
v = std::exp(v);
fft.IFFT(cepstrum);
fft.Normalize(cepstrum);
for(size_t i = 0; i < table.size(); i++)
table[i] = cepstrum[i].real();
}
class BiquadFilter
{
double b0, b1, b2, a1, a2, x1 = 0.0, x2 = 0.0, y1 = 0.0, y2 = 0.0;
double Filter(double x0)
{
double y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
x2 = x1;
x1 = x0;
y2 = y1;
y1 = y0;
return y0;
}
public:
BiquadFilter(double b0_, double b1_, double b2_, double a1_, double a2_)
: b0(b0_), b1(b1_), b2(b2_), a1(a1_), a2(a2_)
{ }
std::vector<double> Run(std::vector<double> table)
{
x1 = 0.0;
x2 = 0.0;
y1 = 0.0;
y2 = 0.0;
// Initialize filter to stable state
for(int i = 0; i < 10000; i++)
Filter(table[0]);
// Now run the filter
for(auto &v : table)
v = Filter(v);
return table;
}
};
// Observe: a and b are reversed here. To be absolutely clear:
// a is the nominator and b is the denominator. :-/
BiquadFilter ZTransform(double a0, double a1, double a2, double b0, double b1, double b2, double fc, double fs)
{
// Prewarp s - domain coefficients
const double wp = 2.0 * fs * std::tan(mpt::numbers::pi * fc / fs);
a2 /= wp * wp;
a1 /= wp;
b2 /= wp * wp;
b1 /= wp;
// Compute bilinear transform and return it
const double bd = 4 * b2 * fs * fs + 2 * b1 * fs + b0;
return BiquadFilter(
(4 * a2 * fs * fs + 2 * a1 * fs + a0) / bd,
(2 * a0 - 8 * a2 * fs * fs) / bd,
(4 * a2 * fs * fs - 2 * a1 * fs + a0) / bd,
(2 * b0 - 8 * b2 * fs * fs) / bd,
(4 * b2 * fs * fs - 2 * b1 * fs + b0) / bd);
}
BiquadFilter MakeRCLowpass(double sampleRate, double freq)
{
const double omega = (2.0 * mpt::numbers::pi) * freq / sampleRate;
const double term = 1 + 1 / omega;
return BiquadFilter(1 / term, 0.0, 0.0, -1.0 + 1.0 / term, 0.0);
}
BiquadFilter MakeButterworth(double fs, double fc, double res_dB = 0)
{
// 2nd-order Butterworth s-domain coefficients are:
//
// b0 = 1.0 b1 = 0 b2 = 0
// a0 = 1 a1 = sqrt(2) a2 = 1
//
// by tweaking the a1 parameter, some resonance can be produced.
const double res = std::pow(10.0, (-res_dB / 10.0 / 2.0));
return ZTransform(1, 0, 0, 1, std::sqrt(2) * res, 1, fc, fs);
}
MPT_NOINLINE void Integrate(std::vector<double> &table)
{
const double total = std::accumulate(table.begin(), table.end(), 0.0);
double startVal = -total;
for(auto &v : table)
{
startVal += v;
v = startVal;
}
}
MPT_NOINLINE void Quantize(const std::vector<double> &in, Paula::BlepArray &quantized)
{
MPT_ASSERT(in.size() == Paula::BLEP_SIZE);
constexpr int fact = 1 << Paula::BLEP_SCALE;
const double cv = fact / (in.back() - in.front());
for(int i = 0; i < Paula::BLEP_SIZE; i++)
{
double val = in[i] * cv;
#ifdef MPT_INTMIXER
val = mpt::round(val);
#endif
quantized[i] = static_cast<mixsample_t>(-val);
}
}
} // namespace
void BlepTables::InitTables()
{
constexpr double sampleRate = Paula::PAULA_HZ;
// Because Amiga only has 84 dB SNR, the noise floor is low enough with -90 dB.
// A500 model uses slightly lower-quality kaiser window to obtain slightly
// steeper stopband attenuation. The fixed filters attenuates the sidelobes by
// 12 dB, compensating for the worse performance of the kaiser window.
// 21 kHz stopband is not fully attenuated by 22 kHz. If the sampling frequency
// is 44.1 kHz, all frequencies above 22 kHz will alias over 20 kHz, thus inaudible.
// The output should be aliasingless for 48 kHz sampling frequency.
auto unfilteredA500 = KaiserFIR(Paula::BLEP_SIZE, 21000.0 / sampleRate * 2.0, 8.0);
auto unfilteredA1200 = KaiserFIR(Paula::BLEP_SIZE, 21000.0 / sampleRate * 2.0, 9.0);
// Move filtering effects to start to allow IIRs more time to settle
constexpr size_t padSize = 8;
constexpr int fftSize = static_cast<int>(mpt::bit_width(size_t(Paula::BLEP_SIZE)) + mpt::bit_width(padSize) - 2);
const TinyFFT fft(fftSize);
FIR_MinPhase(unfilteredA500, fft);
FIR_MinPhase(unfilteredA1200, fft);
// Make digital models for the filters on Amiga 500 and 1200.
auto filterFixed5kHz = MakeRCLowpass(sampleRate, 4900.0);
// The leakage filter seems to reduce treble in both models a bit
// The A500 filter seems to be well modelled only with a 4.9 kHz
// filter although the component values would suggest 5 kHz filter.
auto filterLeakage = MakeRCLowpass(sampleRate, 32000.0);
auto filterLED = MakeButterworth(sampleRate, 3275.0, -0.70);
// Apply fixed filter to A500
auto amiga500Off = filterFixed5kHz.Run(unfilteredA500);
// Produce the filtered outputs
auto amiga1200Off = filterLeakage.Run(unfilteredA1200);
// Produce LED filters
auto amiga500On = filterLED.Run(amiga500Off);
auto amiga1200On = filterLED.Run(amiga1200Off);
// Integrate to produce blep
Integrate(amiga500Off);
Integrate(amiga500On);
Integrate(amiga1200Off);
Integrate(amiga1200On);
Integrate(unfilteredA1200);
// Quantize and scale
Quantize(amiga500Off, WinSincIntegral[A500Off]);
Quantize(amiga500On, WinSincIntegral[A500On]);
Quantize(amiga1200Off, WinSincIntegral[A1200Off]);
Quantize(amiga1200On, WinSincIntegral[A1200On]);
Quantize(unfilteredA1200, WinSincIntegral[Unfiltered]);
}
const Paula::BlepArray &BlepTables::GetAmigaTable(Resampling::AmigaFilter amigaType, bool enableFilter) const
{
if(amigaType == Resampling::AmigaFilter::A500)
return enableFilter ? WinSincIntegral[A500On] : WinSincIntegral[A500Off];
if(amigaType == Resampling::AmigaFilter::A1200)
return enableFilter ? WinSincIntegral[A1200On] : WinSincIntegral[A1200Off];
return WinSincIntegral[Unfiltered];
}
// we do not initialize blepState here
// cppcheck-suppress uninitMemberVar
State::State(uint32 sampleRate)
{
double amigaClocksPerSample = static_cast<double>(PAULA_HZ) / sampleRate;
numSteps = static_cast<int>(amigaClocksPerSample / MINIMUM_INTERVAL);
stepRemainder = SamplePosition::FromDouble(amigaClocksPerSample - numSteps * MINIMUM_INTERVAL);
remainder = SamplePosition(0);
}
void State::Reset()
{
remainder = SamplePosition(0);
activeBleps = 0;
firstBlep = MAX_BLEPS / 2u;
globalOutputLevel = 0;
}
void State::InputSample(int16 sample)
{
if(sample != globalOutputLevel)
{
// Start a new blep: level is the difference, age (or phase) is 0 clocks.
firstBlep = (firstBlep - 1u) % MAX_BLEPS;
if(activeBleps < std::size(blepState))
activeBleps++;
blepState[firstBlep].age = 0;
blepState[firstBlep].level = sample - globalOutputLevel;
globalOutputLevel = sample;
}
}
// Return output simulated as series of bleps
int State::OutputSample(const BlepArray &WinSincIntegral)
{
int output = globalOutputLevel * (1 << Paula::BLEP_SCALE);
uint32 lastBlep = firstBlep + activeBleps;
for(uint32 i = firstBlep; i != lastBlep; i++)
{
const auto &blep = blepState[i % MAX_BLEPS];
output -= WinSincIntegral[blep.age] * blep.level;
}
output /= (1 << (Paula::BLEP_SCALE - 2)); // - 2 to compensate for the fact that we reduced the input sample bit depth
return output;
}
// Advance the simulation by given number of clock ticks
void State::Clock(int cycles)
{
uint32 lastBlep = firstBlep + activeBleps;
for(uint32 i = firstBlep; i != lastBlep; i++)
{
auto &blep = blepState[i % MAX_BLEPS];
blep.age += static_cast<uint16>(cycles);
if(blep.age >= Paula::BLEP_SIZE)
{
activeBleps = static_cast<uint16>(i - firstBlep);
return;
}
}
}
}
OPENMPT_NAMESPACE_END
|