switched to GPLv3 ONLY, because i don't trust FSF anymore
[knightmare.git] / music.d
blob27a42853652eba7f4a21f3e5f4dbdfc3d719044a
1 /* Sound support
2 * Copyright (c) 2000-2004 Russell Marks, Matan Ziv-Av, Philip Kendall
3 * Copyright (c) 2016 Fredrick Meunier
4 * Copyright (c) 2017 Ketmar // Invisible Vector
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 module music;
21 /* The AY white noise RNG algorithm is based on info from MAME's ay8910.c -
22 * MAME's licence explicitly permits free use of info (even encourages it).
24 import arsd.simpleaudio;
26 import iv.cmdcon;
27 import iv.vfs;
30 // ////////////////////////////////////////////////////////////////////////// //
31 private:
32 import iv.blipbuf;
34 public enum AYFreq = 1789773;
35 enum CPUSpeed = 3579545;
36 enum TSPerFrame = AYFreq/2/60; // NTSC; PAL is 50
38 __gshared volumeAY = 150; // [0..400]
39 __gshared oldVolumeAY = 0;
41 enum speakerType = 2;
43 public struct SpeakerEqConfig {
44 int bass;
45 double treble;
49 static immutable SpeakerEqConfig[4] speakerEqConfig = [
50 SpeakerEqConfig(200, -37.0), // TV
51 SpeakerEqConfig(1000, -67.0), // Beeper
52 SpeakerEqConfig(16, -8.0), // "normal" from bleepbuffer dox
53 SpeakerEqConfig(0, 0.0), // Unfiltered
57 alias BlipSynth = BlipSynthBase!(BlipBuffer.Good, 32767);
60 /* Must be <=127 for all channels; (40*3) = 120.
61 * (Now scaled up for 16-bit.)
63 enum AmplAYTone = 40*256; // three of these
66 final class AYEmu {
67 public:
68 // the AY steps down the external clock by 16 for tone and noise generators
69 enum AY_CLOCK_DIVISOR = 16;
71 private:
72 static immutable uint[16] toneLevels = (){
73 /* AY output doesn't match the claimed levels; these levels are based
74 * on the measurements posted to comp.sys.sinclair in Dec 2001 by
75 * Matthew Westcott, adjusted as I described in a followup to his post,
76 * then scaled to 0..0xffff.
78 static immutable ushort[16] levels = [
79 0x0000, 0x0385, 0x053D, 0x0770,
80 0x0AD7, 0x0FD5, 0x15B0, 0x230C,
81 0x2B4C, 0x43C1, 0x5A4B, 0x732F,
82 0x9204, 0xAFF1, 0xD921, 0xFFFF
85 uint[16] res;
86 // scale the values down to fit
87 foreach (immutable f; 0..16) res[f] = (levels[f]*AmplAYTone+0x8000)/0xffff;
88 return res;
89 }();
91 // bitmasks for envelope
92 enum AY_ENV_CONT = 8;
93 enum AY_ENV_ATTACK = 4;
94 enum AY_ENV_ALT = 2;
95 enum AY_ENV_HOLD = 1;
97 private:
98 uint[3] toneTick, toneHigh;
99 uint noiseTick;
100 uint toneCycles, envCycles;
101 uint envInternalTick, envTick;
102 uint[3] tonePeriod;
103 uint noisePeriod, envPeriod;
104 ubyte[16] registers; // local copy of the AY registers
105 BlipSynth[3] synth;
107 private:
108 int rng = 1;
109 int noiseToggle = 0;
110 int envFirst = 1, envRev = 0, envCounter = 15;
111 int[3] lastChanLevel = 0;
113 public:
114 int framesDone = 0;
116 public:
117 this () { reset(); }
119 /// call this after initializing blip buffer
120 void setupBlips () {
121 immutable double treble = speakerEqConfig[speakerType].treble;
122 foreach (ref BlipSynth sn; synth[]) {
123 if (sn is null) sn = new BlipSynth();
124 sn.setVolumeTreble(soundGetVolume(volumeAY), treble);
125 sn.output = blipBuf;
127 oldVolumeAY = volumeAY;
130 /// call this on machine reset
131 void reset () nothrow @trusted @nogc {
132 noiseTick = noisePeriod = 0;
133 envInternalTick = envTick = envPeriod = 0;
134 toneCycles = envCycles = 0;
135 toneTick[] = 0;
136 toneHigh[] = 0;
137 tonePeriod[] = 1;
138 rng = 1;
139 noiseToggle = 0;
140 envFirst = 1;
141 envRev = 0;
142 envCounter = 15;
143 framesDone = 0;
144 lastChanLevel[] = -1;
145 foreach (immutable f; 0..16) writeReg(f, 0, 0);
148 /// change AY register (immediate change)
149 void writeReg (ubyte reg, ubyte val, uint nowts) nothrow @trusted @nogc {
150 static immutable ubyte[16] ayvaluemask = [
151 0xff, 0x0f, 0xff, 0x0f, 0xff, 0x0f, 0x1f, 0xff,
152 0x1f, 0x1f, 0x1f, 0xff, 0xff, 0x0f, 0xff, 0xff,
154 reg &= 0x0f;
155 val &= ayvaluemask.ptr[reg&0x0f];
156 // update ay register
157 registers.ptr[reg] = val;
158 // fix things as needed for some register changes
159 switch (reg) {
160 case 0: case 1: case 2: case 3: case 4: case 5:
161 ubyte r = reg>>1;
162 // a zero-len period is the same as 1
163 tonePeriod.ptr[r] = (registers.ptr[reg&~1]|(registers.ptr[reg|1]&15)<<8);
164 if (!tonePeriod.ptr[r]) ++tonePeriod.ptr[r];
165 // important to get this right, otherwise e.g. Ghouls 'n' Ghosts has really scratchy, horrible-sounding vibrato
166 if (toneTick.ptr[r] >= tonePeriod.ptr[r]*2) toneTick.ptr[r] %= tonePeriod.ptr[r]*2;
167 break;
168 case 6:
169 noiseTick = 0;
170 noisePeriod = registers.ptr[reg]&31;
171 break;
172 case 11: case 12:
173 envPeriod = registers.ptr[11]|(registers.ptr[12]<<8);
174 break;
175 case 13:
176 envInternalTick = envTick = envCycles = 0;
177 envFirst = 1;
178 envRev = 0;
179 envCounter = (registers.ptr[13]&AY_ENV_ATTACK ? 0 : 15);
180 break;
181 default: break;
185 private void doTone (ubyte chan, int level, uint toneCount, ref int var) nothrow @trusted @nogc {
186 pragma(inline, true);
187 toneTick.ptr[chan] += toneCount;
188 if (toneTick.ptr[chan] >= tonePeriod.ptr[chan]) {
189 toneTick.ptr[chan] -= tonePeriod.ptr[chan];
190 toneHigh.ptr[chan] = !toneHigh.ptr[chan];
192 var = (level ? (toneHigh.ptr[chan] ? level : 0) : 0);
195 /// synthesize AY sound
196 void soundOverlay (int samples) nothrow @trusted @nogc {
197 if (samples < 1) return;
199 int[3] toneLevel = void;
200 int[3] lastChan = lastChanLevel[];
201 uint endts = framesDone+AYFreq*samples/SampleRate;
202 for (; framesDone < endts; framesDone += AY_CLOCK_DIVISOR) {
203 // the tone level if no enveloping is being used
204 foreach (immutable g; 0..3) toneLevel.ptr[g] = toneLevels.ptr[registers.ptr[8+g]&15];
206 // envelope
207 immutable int envshape = registers.ptr[13];
209 immutable int level = toneLevels.ptr[envCounter];
210 foreach (immutable g; 0..3) if (registers.ptr[8+g]&16) toneLevel.ptr[g] = level;
213 // envelope output counter gets incr'd every 16 AY cycles
214 envCycles += AY_CLOCK_DIVISOR;
215 int noiseCount = 0;
216 while (envCycles >= 16) {
217 envCycles -= 16;
218 ++noiseCount;
219 ++envTick;
220 while (envTick >= envPeriod) {
221 envTick -= envPeriod;
223 // do a 1/16th-of-period incr/decr if needed
224 if (envFirst || ((envshape&AY_ENV_CONT) && !(envshape&AY_ENV_HOLD))) {
225 if (envRev) {
226 envCounter -= (envshape&AY_ENV_ATTACK ? 1 : -1);
227 } else {
228 envCounter += (envshape&AY_ENV_ATTACK ? 1 : -1);
230 if (envCounter < 0 ) envCounter = 0;
231 if (envCounter > 15) envCounter = 15;
234 ++envInternalTick;
235 while (envInternalTick >= 16) {
236 envInternalTick -= 16;
238 // end of cycle
239 if (!(envshape & AY_ENV_CONT)) {
240 envCounter = 0;
241 } else {
242 if (envshape&AY_ENV_HOLD) {
243 if (envFirst && (envshape&AY_ENV_ALT)) envCounter = (envCounter ? 0 : 15);
244 } else {
245 // non-hold
246 if (envshape&AY_ENV_ALT) {
247 envRev = !envRev;
248 } else {
249 envCounter = (envshape&AY_ENV_ATTACK ? 0 : 15);
254 envFirst = 0;
257 // don't keep trying if period is zero
258 if (!envPeriod) break;
262 /* generate tone+noise... or neither.
263 * (if no tone/noise is selected, the chip just shoves the
264 * level out unmodified. This is used by some sample-playing
265 * stuff.)
267 toneCycles += AY_CLOCK_DIVISOR;
268 immutable int toneCount = toneCycles>>3;
269 toneCycles &= 7;
271 immutable ubyte mixer = registers.ptr[7];
272 foreach (immutable cidx, int cv; toneLevel[]) {
273 if ((mixer&(0x01U<<cast(ubyte)cidx)) == 0) doTone(cast(ubyte)cidx, cv, toneCount, cv);
274 if ((mixer&(0x08U<<cast(ubyte)cidx)) == 0 && noiseToggle) cv = 0;
275 // we can skip `lastChan[]` logic, but it saves us some cycles by skipping bleepsynth
276 if (lastChan.ptr[cidx] != cv) { synth.ptr[cidx].update(framesDone, cv); lastChan.ptr[cidx] = cv; }
279 // update noise RNG/filter
280 noiseTick += noiseCount;
281 while (noiseTick >= noisePeriod) {
282 noiseTick -= noisePeriod;
283 if ((rng&1)^(rng&2 ? 1 : 0)) noiseToggle = !noiseToggle;
284 // rng is 17-bit shift reg, bit 0 is output. input is bit 0 xor bit 3.
285 if (rng&1) rng ^= 0x24000;
286 rng >>= 1;
287 // don't keep trying if period is zero
288 if (!noisePeriod) break;
291 lastChanLevel[] = lastChan[];
296 // ////////////////////////////////////////////////////////////////////////// //
297 public __gshared BlipBuffer blipBuf;
298 public __gshared AYEmu sndvAY;
301 // ////////////////////////////////////////////////////////////////////////// //
302 double soundGetVolume (int volume) {
303 if (volume < 0) volume = 0; else if (volume > 400) volume = 400;
304 return volume/100.0;
308 // ////////////////////////////////////////////////////////////////////////// //
309 /// initialize sound system
310 public void soundInit () {
311 // initialize blip buffer
312 if (blipBuf is null) blipBuf = new BlipBuffer();
313 blipBuf.clockRate = AYFreq; // Hz
314 blipBuf.sampleRate = SampleRate;
315 blipBuf.bassFreq = speakerEqConfig[speakerType].bass;
316 // initialize A
317 if (sndvAY is null) sndvAY = new AYEmu();
318 sndvAY.setupBlips();
319 sndvAY.reset();
323 // ////////////////////////////////////////////////////////////////////////// //
324 public int soundRead (short[] buffer) nothrow @trusted {
325 return blipBuf.readSamples!(true, true)(buffer.ptr, cast(int)buffer.length/2)*2; // fake stereo
329 public void soundAdvanceSamples (uint count) nothrow @trusted {
330 sndvAY.soundOverlay(count);
331 if (sndvAY.framesDone) {
332 blipBuf.endFrame(sndvAY.framesDone);
333 sndvAY.framesDone = 0;
338 // ////////////////////////////////////////////////////////////////////////// //
339 public void soundResetAY () nothrow @trusted @nogc {
340 if (sndvAY !is null) sndvAY.reset();
341 blipBuf.clear();
345 // don't make the change immediately; record it for later, to be made by sound_frame() (via soundOverlay())
346 public void soundWriteAY (ubyte reg, ubyte val, uint nowts=0) nothrow @trusted @nogc {
347 if (sndvAY !is null) sndvAY.writeReg(reg, val, nowts);
351 // ////////////////////////////////////////////////////////////////////////// //
352 struct PSGFrameData {
353 ubyte[14] regs;
354 ushort mask; // bit0: register 0 was changed; and so on
355 ushort smpdelay; // wait this number of samples before next data chunk (can't be 0)
357 void clear () pure nothrow @safe @nogc { regs[] = 0; mask = 0; smpdelay = 0; }
359 bool opEquals() (in auto ref PSGFrameData frm) nothrow @trusted @nogc {
360 import std.math : abs;
361 if (frm.mask != mask) return false;
362 //if (frm.smpdelay != smpdelay) return false;
363 if (abs(cast(int)frm.smpdelay-cast(int)smpdelay) > 16) return false;
364 foreach (immutable ubyte b; 0..14) {
365 if (mask&(1<<b)) {
366 if (frm.regs.ptr[b] != regs.ptr[b]) return false;
369 return true;
373 __gshared PSGFrameData[][string] muzax;
376 // ////////////////////////////////////////////////////////////////////////// //
377 PSGFrameData[] loadPSGF (VFile fl) {
378 char[4] sign;
379 fl.rawReadExact(sign[]);
380 if (sign[] != "PSGF") assert(0, "invalid signature");
381 if (fl.readNum!ubyte != 0) assert(0, "invalid version");
383 uint flen = fl.readNum!uint;
384 if (flen < 1 || flen > 32767) assert(0, "invalid number of frames");
385 auto res = new PSGFrameData[](flen);
387 foreach (ref frm; res[]) {
388 fl.rawReadExact(frm.regs[]);
389 frm.mask = fl.readNum!ushort;
390 frm.smpdelay = fl.readNum!ushort;
393 //assert(fl.size == fl.tell);
394 //writeln(res.length, " frames loaded");
396 return res;
400 public void loadMusic () {
401 void loadit (string name) {
402 conwriteln("loading music '", name, "'...");
403 muzax[name] = loadPSGF(VFile("mus/"~name~".psgf"));
405 loadit("bgm1");
406 loadit("bgm2");
407 loadit("ending");
408 loadit("gameover");
409 loadit("miss");
410 loadit("powerup");
411 loadit("start");
415 // ////////////////////////////////////////////////////////////////////////// //
416 import core.atomic;
417 import core.thread;
418 import core.time;
420 import iv.follin.resampler;
421 import iv.follin.utils;
424 __gshared const(PSGFrameData)[] curmusic;
425 __gshared uint curmusframe;
426 __gshared bool curmuslooped;
428 __gshared const(short)[] cursound = null;
429 __gshared uint cursndpos = 0;
430 __gshared int cursndprio = -666;
431 __gshared const(short)[] newsound = null;
432 shared int wantnewsound = 0;
433 shared int cursndcomplete = 0;
435 shared int musloopcount = 0;
437 shared int wantnewmusic = 0;
438 __gshared bool newmuslooped = false;
439 __gshared const(PSGFrameData)[] newmus = null;
441 shared bool muspaused = false;
442 shared int musthreadstate = 0;
445 public __gshared bool xoptMusicOn = true;
446 public __gshared bool xoptSoundOn = true;
447 public __gshared bool isAudioFucked = false;
449 shared static this () {
450 conRegVar!xoptMusicOn("snd_music", "on/off music");
451 conRegVar!xoptSoundOn("snd_sound", "on/off sound");
452 conRegVar!volumeAY(0, 400, "mus_volume", "music volume");
456 // ////////////////////////////////////////////////////////////////////////// //
457 public @property int musicLoopCount () { return atomicLoad(musloopcount); } ///
459 public @property bool musicPaused () { return atomicLoad(muspaused); } ///
460 public @property void musicPaused (bool v) { atomicStore(muspaused, v); } ///
463 // ////////////////////////////////////////////////////////////////////////// //
464 public void musicNew (string name, bool looped) {
465 if (auto mp = name in muzax) {
466 while (cas(&wantnewmusic, 0, 1) != 0) {}
467 newmuslooped = looped;
468 newmus = *mp;
469 atomicStore(muspaused, false);
470 atomicStore(musloopcount, 0);
471 atomicStore(wantnewmusic, 2);
472 //conwriteln("queued new music: '", name, "' (looped: ", looped, ")");
473 } else {
474 musicAbort();
479 public void musicAbort () {
480 while (cas(&wantnewmusic, 0, 1) != 0) {}
481 newmuslooped = false;
482 newmus = null;
483 atomicStore(muspaused, false);
484 atomicStore(musloopcount, 0);
485 atomicStore(wantnewmusic, 2);
489 // ////////////////////////////////////////////////////////////////////////// //
490 public void soundPlay (int idx, int pan, int prio) {
491 if (idx < 0 || idx >= soundlist.length) { soundAbort(); return; }
492 while (cas(&wantnewsound, 0, 1) != 0) {}
493 bool allowed = (cursndprio < prio) || (atomicLoad(cursndcomplete) != 0);
494 if (!allowed && cursndprio == prio && cursound.length-cursndpos <= soundlist[idx].length) allowed = true;
495 if (allowed) {
496 newsound = soundlist[idx];
497 cursndprio = prio;
498 atomicStore(wantnewsound, 2);
499 } else {
500 //conwriteln("rejected sound #", idx, " due to priority: cur=", cursndprio, "; prio=", prio);
505 public void soundAbort () {
506 while (cas(&wantnewsound, 0, 1) != 0) {}
507 newsound = null;
508 cursndprio = -666;
509 atomicStore(cursndcomplete, 1);
510 atomicStore(wantnewsound, 2);
514 // ////////////////////////////////////////////////////////////////////////// //
515 //TODO: panning [0..319]
516 void mixSound (short[] buffer) {
517 //buffer[] = 0;
518 if (cursndpos >= cursound.length) {
519 atomicStore(cursndcomplete, 1);
520 return;
522 auto left = buffer.length/2;
523 if (left > cursound.length-cursndpos) left = cursound.length-cursndpos;
524 auto sp = cast(const(short)*)cursound.ptr+cursndpos;
525 auto dp = buffer.ptr;
526 foreach (immutable idx; 0..left) {
527 // left
528 int v = *dp;
529 v += *sp;
530 if (v < short.min) v = short.min; else if (v > short.max) v = short.max;
531 if (xoptSoundOn) *dp = cast(short)v;
532 ++dp;
533 // right
534 v = *dp;
535 v += *sp++;
536 if (v < short.min) v = short.min; else if (v > short.max) v = short.max;
537 if (xoptSoundOn) *dp = cast(short)v;
538 ++dp;
540 cursndpos += left;
544 void renderMusic (short[] buffer) {
545 // process volume changes
546 auto vay = volumeAY;
547 if (oldVolumeAY != volumeAY) sndvAY.setupBlips();
548 // is music paused?
549 if (atomicLoad(muspaused)) {
550 buffer[] = 0;
551 return;
553 // music complete?
554 if (curmusframe >= curmusic.length) {
555 if (curmusic.length > 0) {
556 atomicOp!"+="(musloopcount, 1);
557 if (!curmuslooped) curmusic = null;
559 if (!curmuslooped || curmusic.length == 0) {
560 buffer[] = 0;
561 return;
563 curmusframe = 0;
565 auto svbuf = buffer;
566 // fill buffer
567 while (buffer.length >= 2) {
568 auto rd = soundRead(buffer[]);
569 buffer = buffer[rd..$];
570 if (buffer.length > 0) {
571 if (curmusframe >= curmusic.length) {
572 atomicOp!"+="(musloopcount, 1);
573 if (!curmuslooped || curmusic.length == 0) {
574 curmusic = null;
575 break;
577 curmusframe = 0;
579 auto frm = &curmusic[curmusframe++];
580 foreach (immutable ubyte b; 0..14) {
581 if (frm.mask&(1U<<b)) soundWriteAY(b, frm.regs[b]);
583 soundAdvanceSamples(frm.smpdelay);
586 if (buffer.length) buffer[0..$] = 0;
587 // oops
588 if (!xoptMusicOn) svbuf[] = 0;
592 void musicThread () {
593 soundInit();
594 try {
595 auto ao = AudioOutput(0);
596 isAudioFucked = (ao.handle is null);
597 ao.fillData = (short[] buffer) {
598 //TODO: proper locking
599 if (atomicLoad(musthreadstate) == 666) {
600 buffer[] = 0;
601 ao.stop();
602 return;
604 // want new music?
605 if (atomicLoad(wantnewmusic) == 2) {
606 curmusic = newmus;
607 curmuslooped = newmuslooped;
608 atomicStore(musloopcount, 0);
609 soundResetAY();
610 curmusframe = 0;
611 atomicStore(wantnewmusic, 0);
612 //conwriteln("started new music, ", curmusic.length, " frames");
614 // want new sound?
615 if (atomicLoad(wantnewsound) == 2) {
616 cursound = newsound;
617 cursndpos = 0;
618 atomicStore(cursndcomplete, (cursound.length == 0 ? 1 : 0));
619 atomicStore(wantnewsound, 0);
621 renderMusic(buffer);
622 // mix digital sound channel
623 mixSound(buffer);
625 atomicStore(musthreadstate, 2);
626 ao.play();
627 atomicStore(musthreadstate, 667);
628 } catch (Exception e) {
629 isAudioFucked = true;
630 if (atomicLoad(musthreadstate) != 666) atomicStore(musthreadstate, 2);
631 for (;;) {
632 import core.thread;
633 import core.time;
634 if (atomicLoad(musthreadstate) == 666) break;
635 Thread.sleep(100.msecs);
637 atomicStore(musthreadstate, 667);
642 // ////////////////////////////////////////////////////////////////////////// //
643 public void musicStartThread () {
644 if (atomicLoad(musthreadstate)) return;
645 atomicStore(musthreadstate, 1);
646 auto trd = new Thread(&musicThread);
647 trd.isDaemon = true;
648 trd.start();
652 public void musicQuit () {
653 if (atomicLoad(musthreadstate)) {
654 atomicStore(musthreadstate, 666);
655 while (atomicLoad(musthreadstate) != 667) {}
660 // ////////////////////////////////////////////////////////////////////////// //
661 __gshared short[][] soundlist;
664 public void loadSounds () {
665 SpeexResampler srb;
667 auto fl = VFile("MYTH.SND");
668 fl.seek(9);
669 //auto hend = fl.readNum!ubyte;
670 //int hend = 219;
671 static struct SDir { uint ofs; ushort len; }
672 SDir[] dir;
673 scope(exit) delete dir;
674 int hend = 100;
675 int count = 0;
676 while (fl.tell < hend) {
677 auto w0 = fl.readNum!uint;
678 auto w1 = fl.readNum!ushort;
679 //conprintfln("%2d: 0x%08x 0x%04x %10d %5d", count, w0, w1, w0, w1);
680 if (count == 0) hend = w0;
681 dir ~= SDir(w0, w1);
682 ++count;
685 float[] fbufout;
686 scope(exit) delete fbufout;
687 fbufout.length = 44100*10;
689 //conwriteln(fl.tell, " : ", hend);
690 //assert(fl.tell == hend);
691 soundlist.length = dir.length;
692 foreach (immutable sndidx, ref snd; soundlist) {
693 byte[] xbuf;
694 scope(exit) delete xbuf;
695 // load sound
696 xbuf.length = dir[sndidx].len;
697 fl.seek(dir[sndidx].ofs);
698 fl.rawReadExact(xbuf);
699 // resample sound
700 srb.setup(1, /*11025/2*/6100, 44100, /*alsaRQuality*/4);
702 // convert to float
703 float[] fbufin;
704 scope(exit) delete fbufin;
705 fbufin.length = xbuf.length;
706 foreach (immutable idx; 0..xbuf.length) {
707 fbufin[idx] = cast(float)xbuf[idx]/128.0f;
710 SpeexResampler.Data srbdata;
711 uint inpos = 0;
712 for (;;) {
713 srbdata = srbdata.init; // just in case
714 srbdata.dataIn = fbufin[inpos..$];
715 srbdata.dataOut = fbufout[];
716 if (srb.process(srbdata) != 0) assert(0, "resampling error");
717 //{ import core.stdc.stdio; printf("inpos=%u; smpCount=%u; iu=%u; ou=%u\n", cast(uint)inpos, cast(uint)fbufin.length, cast(uint)srbdata.inputSamplesUsed, cast(uint)srbdata.outputSamplesUsed); }
718 if (srbdata.outputSamplesUsed) {
719 auto curpos = snd.length;
720 snd.length += srbdata.outputSamplesUsed;
721 tflFloat2Short(fbufout[0..srbdata.outputSamplesUsed], snd[curpos..curpos+srbdata.outputSamplesUsed]);
722 } else {
723 // no data consumed, no data produced, so we're done
724 if (inpos >= fbufin.length) break;
726 inpos += cast(uint)srbdata.inputSamplesUsed;
728 conwriteln("converted sound #", sndidx+1, " of ", soundlist.length, " (", snd.length, ")");