mirror of
https://github.com/OldUnreal/libxmp.git
synced 2026-04-02 21:37:43 -07:00
509 lines
24 KiB
Plaintext
509 lines
24 KiB
Plaintext
Dear Amiga mod playback software author,
|
|
|
|
you are receiving this note because your software is implementing playback
|
|
capabilities for an important format, the Amiga protracker 1.x-3.x mod format.
|
|
|
|
As you probably are aware, most of these modules were composed on the Amiga
|
|
computer for playback on Amiga system. The Amiga hardware playback system is
|
|
unlike the PC environment, and needs some special care if authentic-seeming
|
|
Amiga mod playback is desired.
|
|
|
|
Typical playback software for the various sample tracker formats is architected
|
|
roughly like this:
|
|
|
|
[ Player routine ] --> [ Mixer and resampler ] --> Output
|
|
|
|
The player routine component produces information about the samples, their
|
|
sampling frequencies and volume levels (and filtering, panning, etc. depending
|
|
on format) to the mixer and resampler component, which uses the player
|
|
routine's instructions to generate the final output by resampling the sample
|
|
stream from memory and mixing it to physical output channels. I'm going to
|
|
focus exclusively to the Mixer and resampler component here.
|
|
|
|
A typical implementation of the resampler uses linear interpolation, or some
|
|
higher-order polynomial interpolation, splines, or even sinc-based
|
|
"theoretically correct" resampling with some 8-tap FIR or more. Unfortunately,
|
|
all of them miss the mark as far as Amiga is concerned. For typical mod player,
|
|
the most accurate emulation of Amiga output occurs when *no* interpolation is
|
|
used at all. However, there seems to be nothing special about the mixing --
|
|
simply adding the resampled channels together suffices.
|
|
|
|
Why the Amiga is different?
|
|
---------------------------
|
|
|
|
To understand what goes on with the Amiga sound chip, Paula, is to understand that
|
|
Paula does no interpolation of any kind. Paula's output is strictly a pulse wave,
|
|
produced on 3546895 Hz frequency, which is the Paula clock rate for PAL systems.
|
|
|
|
The Amiga period values, typically valued somewhere between 120 and 800, count Paula
|
|
ticks. For instance, a period value of 400 means that Paula waits 400 ticks holding
|
|
the output constant, and then changes to the next sample.
|
|
|
|
To simulate this perfectly, we would produce an output sample stream at that
|
|
3.55 MHz rate, one sample per tick, and then simply hold the output constant
|
|
while counting down to zero from the period value, then switch to next sample,
|
|
reset period clock back to original value, and so on. This stream would be
|
|
very accurate representation of the Paula's output.
|
|
|
|
Aliasing in Paula output
|
|
------------------------
|
|
|
|
It is also worth it to understand that playing back a sample from Paula
|
|
replaces the original waveform with its smoothly undulating shapes with nothing
|
|
but hard-edged pulses. This will have an impact to the sound, of course, and is
|
|
called aliasing distortion. It's called aliasing, because copies of the
|
|
original sound's frequency spectrum repeat throughout the entire audible part
|
|
of audio spectrum and beyond, theoretically up to infinity.
|
|
|
|
This may surprise those who believe that because Amiga's maximum sampling rate
|
|
is some 28867 Hz, the maximum frequency produced by Amiga is threrefore limited
|
|
to 14.5 kHz or so. However, this is merely the highest frequency that may be
|
|
produced without aliasing.
|
|
|
|
In addition, some misguided people have tried to run their mod players at 28867
|
|
Hz mixing frequency based on some kind of confused idea of what the mixing rate
|
|
means for Amiga. This won't work either.
|
|
|
|
The analog filters -- more complications
|
|
----------------------------------------
|
|
|
|
After leaving the chip, the sound enters a 6 dB/oct resistor+capacitor (RC)
|
|
low-pass filter tuned at approximately 5 kHz. I will call this component "fixed
|
|
filter" in this document. The purpose of this filter is probably to remove some
|
|
of that aliasing which is inherent in the pulse synthesis. Additionally, it is
|
|
possible to engage a low-pass 12 dB/oct Butterworth filter tuned at
|
|
approximately 3.2 kHz by turning the Amiga power LED brighter with a special
|
|
protracker command. I will call this component "LED filter".
|
|
|
|
These filters should probably be understood as reconstruction filters, because
|
|
their purpose is to attempt to "reconstruct" the original smoothly undulating
|
|
waveform after a pulse-based version of it has been generated by some D/A
|
|
converter. However, proper reconstruction filters should probably feature
|
|
steeper cut-offs than just 6 or 12 dB, and they should be dynamic; their tuning
|
|
should follow the sampling rate, as it is the sampling rate that decides where
|
|
the first aliased frequency will occur. (As an example, some Atari ST models
|
|
have a DAC chip which is fed through DMA at any of the four possible sampling
|
|
rates, and the analog reconstruction filter's cutoff also is chosen based on
|
|
the sampling rate.)
|
|
|
|
Regardless, this is the hand we have been dealt with, and in order to
|
|
accurately simulate Amiga 500 audio playback, we thus need to emulate these two
|
|
filters in addition of producing the right kind of output stream from Paula
|
|
emulation.
|
|
|
|
No interpolation -- use resampling!
|
|
-----------------------------------
|
|
|
|
To properly resample audio, we need to achieve two things, often done simultaneously:
|
|
|
|
* remove all components in the audio stream above some selected frequency, usually
|
|
about 20 kHz.
|
|
* decimate the signal by discarding samples until we obtain output at the correct rate.
|
|
For 44.1 kHz output we need to discard approximately 79 of every 80 samples.
|
|
|
|
Decimation can be safely done only after the lowpass step has been completed.
|
|
If decimation is done without lowpass filtering, some extra aliasing shall be
|
|
introduced to the remaining audio. What happens is that all frequencies in the
|
|
spectrum now alias in the audible range, making the ultrasonic sounds produced by
|
|
the pulse-wave synthesis to appear as additional noise in the mod playback.
|
|
|
|
Fortunately, the previous paragraph contains the answer to the problem. It is
|
|
possible to make simulated Paula's output consist of "bandlimited steps", or
|
|
BLEPs as they are called. The output is done by simulating Paula at 3.55 MHz
|
|
clock rate and replacing the hard edges of Paula's pulses by BLEPs, which are
|
|
carefully constructed in such a fashion to not contain those difficult frequencies
|
|
above 21 kHz or so.
|
|
|
|
Because the input stream does not contain frequencies near or above nyquist
|
|
frequency of the final sampling rate, all we have to do is to build an abstract
|
|
version of Paula's output signal that we can evalute to real sample whenever it
|
|
is needed. The abstract version consists two parts: the output level of
|
|
"genuine" Paula with hard-edged pulses, and the states of various BLEPs which
|
|
which are then used to correct the "hard edges" of Paula's waveform to the
|
|
smooth curves of the BLEPs instead.
|
|
|
|
The mixing of one blep thus has the following procedure, roughly:
|
|
|
|
* adjust the output level of Paula to the new value.
|
|
* start a new blep, and scale it in such a fashion that its amplitude is the difference
|
|
between new output level and the old level.
|
|
|
|
If we ask the synthesis function to produce a sample at this moment, it would still
|
|
report the old level. But as we clock Paula forward, and ask again, the blep gradually
|
|
changes the output level from the old value to the new value. At some point we are done
|
|
with the BLEP, and then the output is simply constant at the new output level.
|
|
|
|
This leaves the filters, but they are easy. Because the output is done by
|
|
mixing bleps, we can implement the analog filters in the bleps themselves, by
|
|
filtering them with digital models of the analog filters to produce versions of
|
|
BLEPs that have treble components reduced to match Amiga 500's operation with
|
|
LED filter on and off. This may seem somewhat miraculous, but this is how it
|
|
works.
|
|
|
|
The implementation
|
|
------------------
|
|
|
|
We need to compute the blep tables. Firstly we need a function that contains
|
|
all the frequencies in the world. One such function is called the delta
|
|
function. We define it as follows:
|
|
|
|
delta(x) = 0 if x != 0
|
|
delta(x) = 1 if x = 0.
|
|
|
|
If we low-pass filter the delta, it morphs into a sinc function. sinc function
|
|
is a special one, it has all the frequencies from 0 to some given cutoff value.
|
|
The cutoff is determined by the rate the sinc is oscillating in.
|
|
|
|
f(x, rate) = sin(x * pi / rate) / (x * pi / rate) if x != 0
|
|
f(0, rate) = 1 if x = 0
|
|
|
|
With rate = 1.0 the sinc does no filtering at all, and becomes a delta function
|
|
in effect. If we plot the sinc function by asking the value of sinc at each
|
|
integer value, it is zero everywhere except 1 at 0. To do no filtering at all
|
|
is the same as having the cutoff at exactly half of the sampling rate. If the
|
|
sinc is to remove everything above, say, 21 kHz, rate = 3.55e6 / 21e3 / 2.
|
|
|
|
An ideal sinc filter extends from negative infinity to the positive infinity.
|
|
Because a practical implementation is needed, the sinc function must be
|
|
truncated at some point. The truncation affects the width of the stopband of
|
|
the sinc function -- short functions begin filtering earlier and are not as
|
|
steeply filtering as long filters. I've found that 2048 point sinc functions
|
|
are sufficient to perform filtering for Paula, so we'd sample the sinc function
|
|
from f(-1024, rate) to f(1023, rate) and thus yield 2048 values.
|
|
|
|
Because 2048 point function is quite short, there is an issue at the edges,
|
|
where the sinc is still oscillating. Some windowing function is needed to
|
|
smooth the sinc properly at edges to zero. The Hann window, or the raised
|
|
cosine window is the simplest. It is defined as hann(x, length) = 0.5 - 0.5 *
|
|
cos(x / length * 2 * pi). For this, the length would be 2048, and the values of
|
|
x would go from 0 to 2047.
|
|
|
|
The result should look like a symmetric "water-drop" with few small side lobes.
|
|
If you have some plotting software, you can see the sinc function at this
|
|
point. gnuplot suffices, you can type something like:
|
|
|
|
set xrange [-1024:1023]
|
|
plot sin(x * pi / 84) / (x * pi / 84) * (0.5 - 0.5 * cos((x + 1024) / 2048 * 2 * pi))
|
|
|
|
The sinc table is final mixing frequency independent
|
|
-----------------------------------------------------
|
|
|
|
The only things that go into calculating the sinc table are the Paula clock
|
|
frequency and the frequency of the cutoff. As long as calls to obtaining output
|
|
occur at rates greater or equal to 44.1 kHz, the table works. With rates below
|
|
about 20 kHz, the blep table itself will produce aliasing.
|
|
|
|
The function is sampled at Paula clock rate, which is constant; it therefore
|
|
also has a fixed duration in seconds. For 2048 points, it's 2048 paula ticks or
|
|
roughly 0.6 milliseconds. From this it follows that at 44.1 kHz sampling
|
|
frequency, about 25 samples of the table make it into the output.
|
|
|
|
The table must however be longer than merely 25 samples because depending on
|
|
the phase where the blep starts, ie. paula pulse occurs, it's always different
|
|
values of the table that need to be chosen, so all of them become used at some
|
|
point. (However, in truth the table does not need to be that precise, it could
|
|
well be sampled at a lower frequency without compromising output quality
|
|
noticeably. Perhaps only about 16 different phase values are needed, but now it
|
|
has 80 (as many phases as there are clocks between samples).)
|
|
|
|
Filtering the sinc
|
|
------------------
|
|
|
|
The minimum-phase reconstruction is implemented in cepstral domain in the
|
|
included program. My knowledge of the operations involved is too limited to
|
|
explain why or how it works, but after we do this, the filter shape changes in
|
|
such a way that as much as possible of the filter occurs "early on" and the
|
|
filter "settles" for longer period after an initial large transition. So,
|
|
before minimum phase reconstruction, the filter is a symmetric waterdrop-like
|
|
wave, with pre- and postringing at the peak, but after the reconstruction the
|
|
peak moves to the start of filter, and the filter displays decaying oscillation
|
|
for most of its duration. The upshot of the reconstruction is that it allows
|
|
our IIR filters the longest period to settle after the initial excitation by
|
|
the large part of the peak, thus allowing us to truncate them earlier.
|
|
|
|
There has been some talk that minimum-phase reconstructions are in general
|
|
better audio filters than linear-phase filters (which are symmetric by the
|
|
midpoint), because they do not display the preringing that is perceptible to
|
|
ear if it occurs for too long. The increased postringing is not an issue, as it
|
|
often occurs in nature, and thus the ear "expects" it. However, the issue is
|
|
made complicated by the fact that phase relationships are by definition
|
|
modified by minimum-phase reconstructions. At any case, this choice makes no
|
|
audible difference in this case, because in both cases the ringing is tuned to
|
|
occur at frequencies above 20 kHz, and the phase delays are almost linear
|
|
across the entire audible band.
|
|
|
|
The fixed filter
|
|
----------------
|
|
|
|
We should apply the IIR filters to the sinc function before proceeding to build the
|
|
band-limited step. The 6 dB/oct filter is an IIR filter that is computed
|
|
according to the following formula:
|
|
|
|
output(n) = b0 * input(n) + (1 - b0) * output(n - 1)
|
|
|
|
that is, the output is a linear combination of input and the value of output in the
|
|
last iteration.
|
|
|
|
The b0 determines the cutoff value and is computed as follows:
|
|
|
|
omega = 2 * pi * 5000 / sampling_rate
|
|
b0 = 1 / (1 + 1 / omega)
|
|
|
|
As a reminder, the cutoff frequency would be 5.0 kHz and sampling rate is 3.55
|
|
MHz because we will operate the blep that is "sampled" at 3.55 MHz. The rest of
|
|
the problem here is just applying the previous formula to filtering the BLEP.
|
|
|
|
Amiga 1200 current leakage simulation
|
|
-------------------------------------
|
|
|
|
It seems that Amiga 1200 output response attenuated by some 0.2 - 0.3 dB near
|
|
20 kHz. This would seem to be due to the LED filter even though it isn't
|
|
enabled, because this effect does not manifest when turning the LED filter on.
|
|
It is difficult to be sure, though, because of limited resolution and the
|
|
extreme attenuation, noise floor gets very close to signal. Regardless, the
|
|
effect can be modelled with a 32 kHz lowpass RC filter.
|
|
|
|
A word of caution
|
|
-----------------
|
|
|
|
To correctly apply the filter, we first need to extend the sinc we computed by
|
|
imagining that it is preceded by, say, 10 000 constant values of the first
|
|
value of the filter, and these are run through the filter to ensure that the
|
|
filter is properly settled for the base level of the blep. This is especially
|
|
important if the first value of the sinc is not zero, or if the filter is
|
|
reused between computations for any reason.
|
|
|
|
Additionally, to validate that the length of the blep is sufficient, it would
|
|
be helpful to examine how much the IIR filter oscillates at the end of the
|
|
table by computing the value of
|
|
|
|
filter(last_output_value) - last_output_value
|
|
|
|
If the filter has not yet damped, the difference will not be zero, and depending of
|
|
the oscillation magnitude, artifacts could be introduced.
|
|
|
|
The LED filter
|
|
--------------
|
|
|
|
If no LED filtering emulation is desired, the next BLEP can be left uncomputed.
|
|
To simulate the LED filter, we clone the previous sinc function for applying
|
|
the LED filter on. The model for LED filter requires application of a biquad
|
|
filter that simulates the 2nd order Butterwoth lowpass filter.
|
|
|
|
The formula for biquad is as follows:
|
|
|
|
output(n) = b0 * input(n) + b1 * input(n-1) + b2 * input(n-2)
|
|
- a1 * output(n - 1) - a2 * output(n - 2)
|
|
|
|
That is, two samples of past input history and two samples of past output history and
|
|
the current sample are required. The coefficients are derived using bilinear transform
|
|
in the Python source; feel free to look at the gory details for computing b0, b1, b2,
|
|
a1 and a2 from there.
|
|
|
|
So, anyway, we should go and filter our copy of the previous sinc function
|
|
with the biquad formula, and then proceed to figuring out how to convert the
|
|
filtered sincs to bandlimited steps.
|
|
|
|
The step-function
|
|
-----------------
|
|
|
|
Step is our representation of Paula's pulses. We could define it as follows:
|
|
|
|
f(x) = 0 if x < 0
|
|
f(x) = 1 if x >= 0
|
|
|
|
The band-limited step
|
|
---------------------
|
|
|
|
Convolution in DSP is the application of a filter in the time domain. It is
|
|
basically like computing a dot product between two vectors. The other "vector"
|
|
is the tabularised version of the convolution function, and the other is the
|
|
sample data around the point we wish to compute convolution at. We would be
|
|
talking about 2048-dimensional vector here, if vectors were otherwise an
|
|
appropriate metaphor.
|
|
|
|
In order to bandlimit a step, we can calculate a representation of the step
|
|
sample stream. We start with the entire sample buffer at zero and calculate
|
|
the convolution. It is all zero as expected.
|
|
|
|
Then we introduce the edge, or the first sample of the step into the sample
|
|
buffer. The convolution's value is now equal to the first value in the
|
|
convolution function. As more samples are introduced, the value of convolution
|
|
is the sum of the values in the convolution function over the area of the step
|
|
function. Thus, the bandlimited step is in effect just a numeric integral of
|
|
the convolution function.
|
|
|
|
We should take care to scale the step accordingly. If you are dealing with
|
|
16-bit sample data, you would probably want to scale the step in such a fashion
|
|
that it starts at, say, 0 and ends at 65536. Or, as I often prefer, it starts
|
|
at -65536 and ends at 0. This is just to make the blep evaluation code simple.
|
|
The downside with starting with negative number is that the sign takes 1 byte
|
|
to encode in the resultant C source. For the example program, the blep
|
|
resolution is 17 bits and the blep runs from a positive value to zero.
|
|
|
|
Sample code in C (but for mono output)
|
|
--------------------------------------
|
|
|
|
/* 131072 to 0, 2048 entries */
|
|
#define BLEP_SCALE 17
|
|
#define BLEP_SIZE 2048
|
|
|
|
/* the step function */
|
|
int32_t sine_integral[BLEP_SIZE] = { precomputed table };
|
|
|
|
/* the instantenous value of Paula output */
|
|
static int16_t global_output_level;
|
|
|
|
/* count of simultaneous bleps to keep track of */
|
|
static unsigned int active_bleps;
|
|
|
|
/* the structure that holds data of bleps */
|
|
typedef struct {
|
|
int16_t level;
|
|
int16_t age;
|
|
} blep_state_t;
|
|
|
|
/* place to keep our bleps in. MAX_BLEPS should be
|
|
defined as a BLEP_SIZE / MINIMUM_EVENT_INTERVAL.
|
|
For Paula, minimum event interval could be even 1, but it makes sense to
|
|
limit it to some higher value such as 16. */
|
|
static blep_state_t blepstate[MAX_BLEPS];
|
|
|
|
/* return output simulated as series of bleps */
|
|
int16_t
|
|
output_sample(int filter_table) {
|
|
int i;
|
|
int32_t output;
|
|
|
|
output = global_output_level << BLEP_SCALE;
|
|
for (i = 0; i < active_bleps; i += 1)
|
|
output -= winsinc_integral[filter_table][blepstate[i].age] * blepstate[i].level;
|
|
output >>= BLEP_SCALE;
|
|
|
|
if (output < -32768)
|
|
output = -32768;
|
|
else if (output > 32767)
|
|
output = 32767;
|
|
|
|
return output;
|
|
}
|
|
|
|
In the code where new Paula samples get added to the system, you would so
|
|
something like this:
|
|
|
|
function input_sample(int16_t sample) {
|
|
if (sample != global_output_level) {
|
|
/* Start a new blep: level is the difference, age (or phase) is 0 clocks. */
|
|
if (active_bleps > MAX_BLEPS - 1) {
|
|
fprintf(stderr, "warning: active blep list truncated!\n");
|
|
active_bleps = MAX_BLEPS - 1;
|
|
}
|
|
/* Make room for new blep */
|
|
memmove(&blepstate[1], &blepstate[0], sizeof(blepstate[0]) * active_bleps);
|
|
/* Update state to account for the new blep */
|
|
active_bleps += 1;
|
|
blepstate[0].age = 0;
|
|
blepstate[0].level = sample - global_output_level;
|
|
global_output_level = sample;
|
|
}
|
|
}
|
|
|
|
And while no events occur, you would call the clock() function to advance the bleps.
|
|
|
|
void clock(unsigned int cycles) {
|
|
for (i = 0; i < active_bleps; i += 1) {
|
|
blepstate[i].age += cycles;
|
|
if (blepstate[i].age >= BLEP_SIZE) {
|
|
active_bleps = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
This code is not very optimized in that it constantly keeps blepstate as a
|
|
sorted list. It is possible to implement it more efficiently, for instance by
|
|
keeping occurence times instead of ages, but that complicates the output sample
|
|
function. The right optimization of the code might depend on whether periods
|
|
are small or large compared to sampling intervals. For Paula, periods are
|
|
typically much larger than sampling intervals, so code like this might be
|
|
better tradeoff. For modern computers, it takes quite little time, so I leave
|
|
the optimizing for people who are that way inclined. Typical length of the blep
|
|
list can be short, only 5-6 entries if the sample rates are low or occur
|
|
conveniently otherwise. At the maximum, Paula is constantly producing pulse
|
|
waves but it's probably sensible to limit maximum period to 124 unless Paula
|
|
DMA fetch starvation is simulated.
|
|
|
|
A typical sequence of events would look like this. Assuming that we sample output
|
|
at exactly 80 paula clock ticks apart, and that playback period is, say 140, we'd do
|
|
calls like this:
|
|
|
|
input_sample(samplebuf[0]);
|
|
clock(80);
|
|
*output++ = output_sample();
|
|
clock(60);
|
|
input_sample(samplebuf[1]);
|
|
clock(20);
|
|
*output++ = output_sample();
|
|
clock(80);
|
|
*output++ = output_sample();
|
|
clock(40);
|
|
input_Sample(samplebuf[2]);
|
|
...
|
|
|
|
Results and analysis
|
|
--------------------
|
|
|
|
I have hi-fi recordings of Amiga 500 and 1200 playing Sainahi Circles with both
|
|
LED filter on and off. This analysis was largerly based on the sampling work by
|
|
pyksy on #amigaexotic at IRCnet. Big thanks to pyksy for his work! In addition,
|
|
Melkor (kohina board) sent me a hi-fi sampling of his Amiga 4000 playing Sainahi
|
|
Circles.
|
|
|
|
The results are summarised as number of png pictures that display the simulated
|
|
and real responses side-by-side and their residual (difference) spectrums. The
|
|
units are in Hz and dB. There is a constant offset that follows from the fact
|
|
that the hi-fi samplings used 32-bit floating point samples, whose value range
|
|
is different from the integer-coded PCM data, yielding the largest part of the
|
|
difference. The rest is explained by the different recording levels used
|
|
between setups.
|
|
|
|
The files called a{1200,500,4000}_{on,off}.png shows the simulated response of
|
|
UADE and real Amiga superimposed. comb{1200,500,4000}_{on,off}.png show the
|
|
difference between two in dB units. These are the same pictures as before, but
|
|
with the spectra substracted, allowing for more precise evaluation of the
|
|
differences between simulated and real output.
|
|
|
|
We can see that UADE and Amiga produce same features closely, thus essentially
|
|
validating the model used for output synthesis.
|
|
|
|
We can observe a good cutoff starting around 20 kHz and quickly extinguishing
|
|
response by approximately 23 kHz. This should yield satisfactory results for
|
|
all sampling frequencies above and including 44.1 kHz. However, I personally
|
|
recommend 48 kHz as default output frequency because it works without
|
|
resampling on majority of cards, while 44.1 kHz output often may need a
|
|
resampling step on either software or hardware.
|
|
|
|
For LED off cases, the residual spectrum is largerly just noise. There is,
|
|
however a small 0.3 dB boost around 0 to 3 kHz. This can be seen on Amiga 500,
|
|
but not on Amiga 1200. In both Amiga 500 and Amiga 1200, the LED filter also
|
|
displayed this boost, and it was compensated for by adjusting the Butterworth
|
|
filter with a small resonance term.
|
|
|
|
Despite adjustments, the LED filter case is not perfect. The residual spectrum
|
|
is roughly flat to 3 kHz, thanks to manually fitting the filter, but after that
|
|
there is some extra attenuation which is then compensated by something else on
|
|
the analog filter. For Amiga 500 case, the simulation hits noise floor and can
|
|
not yield anything meaningful above 15 kHz, but those measurements also show
|
|
the same general pattern.
|
|
|
|
Overall, I think the model yields accuracy to about 0.5 dB for all cases, which
|
|
should be sufficient for all practical purposes. Especially the Amiga 1200 case
|
|
seems well modelled.
|
|
|
|
Author
|
|
------
|
|
|
|
Antti S. Lankila <alankila@bel.fi>
|
|
|
|
feel free to send feedback, comments and spam.
|