1function s = wv_audio(x, fs, scale)
2% WV_AUDIO A signal packed for the page's audio player.
3%
4% The samples are divided by `scale` and sent as 16-bit integers, which keeps
5% the JSON about a third the size of full-precision numbers and loses nothing
6% that a loudspeaker could reproduce. Pass the same scale to every clip in a
7% comparison, so that "louder" on the page means louder in the computation;
8% with no scale the clip is normalised to its own peak. A short fade at each
9% end keeps the player from clicking; it is applied to every clip alike.
10%
11% Browsers are only obliged to play sample rates from 8 kHz up, and a coarse
12% simulation's own rate can be lower, so such a clip is first raised to a
13% whole multiple of its rate by band-limited interpolation (zero-padding its
14% spectrum), which adds nothing and changes nothing below its Nyquist
15% frequency.
16x = x(:)';
17if fs < 8000
18 k = ceil(8000 / fs);
19 x = wv_upsample(x, k);
20 fs = fs * k;
21end
22if nargin < 3 || scale <= 0
23 scale = max(abs(x));
24 if scale == 0
25 scale = 1;
26 end
27end
28n = numel(x);
29nf = min(floor(n / 4), round(0.004 * fs));
30if nf > 1
31 r = 0.5 - 0.5 * cos(pi * (0:nf-1) / nf);
32 x(1:nf) = x(1:nf) .* r;
33 x(n-nf+1:n) = x(n-nf+1:n) .* fliplr(r);
34end
35y = round(29000 * x / scale);
36y(y > 32767) = 32767;
37y(y < -32767) = -32767;
38s = struct();
39s.fs = fs;
40s.pcm = int16(y);
41end
43function y = wv_upsample(x, k)
44n = numel(x);
45X = fft(x);
46Y = zeros(1, k * n);
47h = floor(n / 2);
48Y(1:h + 1) = X(1:h + 1);
49Y(k * n - (n - h - 2):k * n) = X(h + 2:n);
50if mod(n, 2) == 0
51 % the Nyquist bin is shared between the two halves
52 Y(h + 1) = X(h + 1) / 2;
53 Y(k * n - h + 1) = X(h + 1) / 2;
54end
55y = real(ifft(Y)) * k;
56end