some guard code for FlashLoader(utm)
[zxemut.git] / sound.d
blob0ad8f87321d127b50b8bd8fcb68c700baf05f3bd
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 sound;
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 iv.cmdcon;
26 import zxinfo;
27 import emuconfig;
28 version(linux) {
29 import soundalsa;
30 } version(Windows) {
31 import soundshitdows;
35 // ////////////////////////////////////////////////////////////////////////// //
36 public __gshared bool sndvEnabled = false; // are we currently using the sound card?
37 __gshared bool sndvSkipSynthV = false; // set to `true` to skip synthesis part; useful for player rewinding
39 public @property bool sndvSkipSynth () { return sndvSkipSynthV; }
40 public @property sndvSkipSynth (bool v) {
41 if (sndvSkipSynthV != v) {
42 sndvSkipSynthV = v;
43 soundReinit();
48 // ////////////////////////////////////////////////////////////////////////// //
49 private:
50 import iv.blipbuf;
52 //alias BlipSynth = BlipSynthBase!(BlipBuffer.Good, 65535);
53 alias BlipSynth = BlipSynthBase!(BlipBuffer.Good, 32767);
56 __gshared bool sndvRealStereo;
57 __gshared uint sndvLastSampleRate;
59 /* assume all three tone channels together match the beeper volume (ish).
60 * Must be <=127 for all channels; 50+2+(24*3) = 124.
61 * (Now scaled up for 16-bit.)
63 enum AmplBeeper = 50*256;
64 enum AmplTape = 2*256;
65 enum AmplAYTone = 24*256; // three of these
67 /* max. number of sub-frame AY port writes allowed;
68 * given the number of port writes theoretically possible in a
69 * 50th I think this should be plenty.
71 __gshared int sndvLastSilentLevelL = int.min;
72 __gshared int sndvLastSilentLevelR = int.min;
74 __gshared uint sndvFrameSize; // number samples in one sound frame
77 final class AYEmu {
78 private:
79 static immutable uint[16] toneLevels = (){
80 /* AY output doesn't match the claimed levels; these levels are based
81 * on the measurements posted to comp.sys.sinclair in Dec 2001 by
82 * Matthew Westcott, adjusted as I described in a followup to his post,
83 * then scaled to 0..0xffff.
85 static immutable int[16] levels = [
86 0x0000, 0x0385, 0x053D, 0x0770,
87 0x0AD7, 0x0FD5, 0x15B0, 0x230C,
88 0x2B4C, 0x43C1, 0x5A4B, 0x732F,
89 0x9204, 0xAFF1, 0xD921, 0xFFFF
92 uint[16] res;
93 // scale the values down to fit
94 foreach (immutable f; 0..16) res[f] = (levels[f]*AmplAYTone+0x8000)/0xffff;
95 return res;
96 }();
98 private:
99 Config.Stereo stereoType = Config.Stereo.ABC;
101 uint[3] toneTick, toneHigh;
102 uint noiseTick;
103 uint toneCycles, envCycles;
104 uint envInternalTick, envTick;
105 uint[3] tonePeriod;
106 uint noisePeriod, envPeriod;
108 // local copy of the AY registers
109 ubyte[16] registers;
111 BlipSynth synthA, synthB, synthC;
112 BlipSynth rsynthA, rsynthB, rsynthC;
113 BlipSynth mrsynth;
115 static struct ChangeRec {
116 uint tstates;
117 ubyte reg, val;
120 ChangeRec[] regChangelog;
121 uint rclCount;
123 private:
124 int rng = 1;
125 int noiseToggle = 0;
126 int envFirst = 1, envRev = 0, envCounter = 15;
128 public:
129 this () {
130 regChangelog.length = 16384;
131 reset();
134 // call this after initializing blip buffer
135 void setupBlips () {
136 immutable double treble = speakerEqConfig[Config.speakerType].treble;
138 if (synthA is null) synthA = new BlipSynth();
139 synthA.setVolumeTreble(soundGetVolume(Config.volumeAY), treble);
141 if (synthB is null) synthB = new BlipSynth();
142 synthB.setVolumeTreble(soundGetVolume(Config.volumeAY), treble);
144 if (synthC is null) synthC = new BlipSynth();
145 synthC.setVolumeTreble(soundGetVolume(Config.volumeAY), treble);
147 /* important to override these settings if not using stereo
148 * (it would probably be confusing to mess with the stereo
149 * settings in settings_current though, which is why we make copies
150 * rather than using the real ones).
153 rsynthA = null;
154 rsynthB = null;
155 rsynthC = null;
157 stereoType = Config.stereoType;
158 if (stereoType != Config.Stereo.None) {
159 enum MidMultiplier = 0.7;
161 // another mid synth for right channel, slightly muted
162 if (mrsynth is null) mrsynth = new BlipSynth();
163 mrsynth.setOutputVolumeTreble(rightBlipBuf, soundGetVolume(Config.volumeAY)*MidMultiplier, treble);
165 BlipSynth l, r, m;
166 // attach the BlipSynth's we've already created as appropriate, and create one more BlipSynth for the middle channel's right buffer
167 enum SetupMixin(string mode) =
168 "case Config.Stereo."~mode~":
169 l = synth"~mode[0]~";
170 m = synth"~mode[1]~";
171 r = synth"~mode[2]~";
172 rsynth"~mode[1]~" = mrsynth;
173 break;";
174 final switch (stereoType) {
175 mixin(SetupMixin!"ACB");
176 mixin(SetupMixin!"ABC");
177 mixin(SetupMixin!"BAC");
178 mixin(SetupMixin!"BCA");
179 mixin(SetupMixin!"CAB");
180 mixin(SetupMixin!"CBA");
181 case Config.Stereo.None: assert(0);
184 l.output = leftBlipBuf;
185 r.output = rightBlipBuf;
186 // middle synth should sound in left and right channels, slightly muted
187 // mrsynth will take care of right channel
188 m.setOutputVolumeTreble(leftBlipBuf, soundGetVolume(Config.volumeAY)*MidMultiplier, treble);
189 } else {
190 synthA.output = leftBlipBuf;
191 synthB.output = leftBlipBuf;
192 synthC.output = leftBlipBuf;
196 /// call this on machine reset
197 void reset () nothrow @trusted @nogc {
198 noiseTick = noisePeriod = 0;
199 envInternalTick = envTick = envPeriod = 0;
200 toneCycles = envCycles = 0;
201 toneTick[] = 0;
202 toneHigh[] = 0;
203 tonePeriod[] = 1;
204 rclCount = 0;
205 rng = 1;
206 noiseToggle = 0;
207 envFirst = 1;
208 envRev = 0;
209 envCounter = 15;
210 foreach (immutable f; 0..16) writeReg(f, 0, 0);
213 /// don't make the change immediately; record it for later, to be made by sound_frame() (via soundOverlay())
214 void writeReg (ubyte reg, ubyte val, uint nowts) nothrow @trusted @nogc {
215 if (rclCount < regChangelog.length) {
216 static immutable ubyte[16] ayvaluemask = [
217 0xff, 0x0f, 0xff, 0x0f, 0xff, 0x0f, 0x1f, 0xff,
218 0x1f, 0x1f, 0x1f, 0xff, 0xff, 0x0f, 0xff, 0xff,
220 regChangelog.ptr[rclCount].tstates = nowts;
221 regChangelog.ptr[rclCount].reg = reg&0x0f;
222 regChangelog.ptr[rclCount].val = val&ayvaluemask.ptr[reg&0x0f];
223 ++rclCount;
227 private void doTone(ubyte chan) (int level, uint toneCount, ref int var) nothrow @trusted @nogc {
228 toneTick.ptr[chan] += toneCount;
229 if (toneTick.ptr[chan] >= tonePeriod.ptr[chan]) {
230 toneTick.ptr[chan] -= tonePeriod.ptr[chan];
231 toneHigh.ptr[chan] = !toneHigh.ptr[chan];
233 var = (level ? (toneHigh.ptr[chan] ? level : 0) : 0);
236 void soundOverlay () nothrow @trusted @nogc {
237 // bitmasks for envelope
238 enum AY_ENV_CONT = 8;
239 enum AY_ENV_ATTACK = 4;
240 enum AY_ENV_ALT = 2;
241 enum AY_ENV_HOLD = 1;
243 // the AY steps down the external clock by 16 for tone and noise generators
244 enum AY_CLOCK_DIVISOR = 16;
245 // all Spectrum models and clones with an AY seem to count down the master clock by 2 to drive the AY
246 enum AY_CLOCK_RATIO = 2;
248 int[3] toneLevel;
249 int mixer, envshape;
250 int level;
251 uint rclPos;
252 uint rclLeft = rclCount;
253 int reg, r;
254 int chan1, chan2, chan3;
255 int lastChan1 = 0, lastChan2 = 0, lastChan3 = 0;
256 uint toneCount, noiseCount;
258 rclCount = 0;
260 // If no AY chip, don't produce any AY sound (!)
262 if( !( periph_is_active( PERIPH_TYPE_FULLER) ||
263 periph_is_active( PERIPH_TYPE_MELODIK ) ||
264 machine_current.capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_AY ) )
265 return;
268 for (uint curts = 0; curts < Config.machine.timings.tstatesPerFrame; curts += AY_CLOCK_DIVISOR*AY_CLOCK_RATIO) {
269 // update ay registers
270 while (rclPos < rclLeft && curts >= regChangelog.ptr[rclPos].tstates) {
271 reg = regChangelog.ptr[rclPos].reg;
272 registers.ptr[reg] = regChangelog.ptr[rclPos].val;
273 ++rclPos;
274 // fix things as needed for some register changes
275 switch (reg) {
276 case 0: case 1: case 2: case 3: case 4: case 5:
277 r = reg>>1;
278 // a zero-len period is the same as 1
279 tonePeriod.ptr[r] = (registers.ptr[reg&~1]|(registers.ptr[reg|1]&15)<<8);
280 if (!tonePeriod.ptr[r]) ++tonePeriod.ptr[r];
281 // important to get this right, otherwise e.g. Ghouls 'n' Ghosts has really scratchy, horrible-sounding vibrato
282 if (toneTick.ptr[r] >= tonePeriod.ptr[r]*2) toneTick.ptr[r] %= tonePeriod.ptr[r]*2;
283 break;
284 case 6:
285 noiseTick = 0;
286 noisePeriod = registers.ptr[reg]&31;
287 break;
288 case 11: case 12:
289 envPeriod = registers.ptr[11]|(registers.ptr[12]<<8);
290 break;
291 case 13:
292 envInternalTick = envTick = envCycles = 0;
293 envFirst = 1;
294 envRev = 0;
295 envCounter = (registers.ptr[13]&AY_ENV_ATTACK ? 0 : 15);
296 break;
297 default: break;
301 // the tone level if no enveloping is being used
302 foreach (immutable g; 0..3) toneLevel.ptr[g] = toneLevels.ptr[registers.ptr[8+g]&15];
304 // envelope
305 envshape = registers.ptr[13];
306 level = toneLevels.ptr[envCounter];
308 foreach (immutable g; 0..3) if (registers.ptr[8+g]&16) toneLevel.ptr[g] = level;
310 // envelope output counter gets incr'd every 16 AY cycles
311 envCycles += AY_CLOCK_DIVISOR;
312 noiseCount = 0;
313 while (envCycles >= 16) {
314 envCycles -= 16;
315 ++noiseCount;
316 ++envTick;
317 while (envTick >= envPeriod) {
318 envTick -= envPeriod;
320 // do a 1/16th-of-period incr/decr if needed
321 if (envFirst || ((envshape&AY_ENV_CONT) && !(envshape&AY_ENV_HOLD))) {
322 if (envRev) {
323 envCounter -= (envshape&AY_ENV_ATTACK ? 1 : -1);
324 } else {
325 envCounter += (envshape&AY_ENV_ATTACK ? 1 : -1);
327 if (envCounter < 0 ) envCounter = 0;
328 if (envCounter > 15) envCounter = 15;
331 ++envInternalTick;
332 while (envInternalTick >= 16) {
333 envInternalTick -= 16;
335 // end of cycle
336 if (!(envshape & AY_ENV_CONT)) {
337 envCounter = 0;
338 } else {
339 if (envshape&AY_ENV_HOLD) {
340 if (envFirst && (envshape&AY_ENV_ALT)) envCounter = (envCounter ? 0 : 15);
341 } else {
342 // non-hold
343 if (envshape&AY_ENV_ALT) {
344 envRev = !envRev;
345 } else {
346 envCounter = (envshape&AY_ENV_ATTACK ? 0 : 15);
351 envFirst = 0;
354 // don't keep trying if period is zero
355 if (!envPeriod) break;
359 /* generate tone+noise... or neither.
360 * (if no tone/noise is selected, the chip just shoves the
361 * level out unmodified. This is used by some sample-playing
362 * stuff.)
364 chan1 = toneLevel.ptr[0];
365 chan2 = toneLevel.ptr[1];
366 chan3 = toneLevel.ptr[2];
367 mixer = registers.ptr[7];
369 toneCycles += AY_CLOCK_DIVISOR;
370 toneCount = toneCycles>>3;
371 toneCycles &= 7;
373 if ((mixer&1) == 0) {
374 level = chan1;
375 doTone!0(level, toneCount, chan1);
377 if ((mixer&0x08) == 0 && noiseToggle) chan1 = 0;
379 if ((mixer&2) == 0) {
380 level = chan2;
381 doTone!1(level, toneCount, chan2);
383 if ((mixer&0x10) == 0 && noiseToggle) chan2 = 0;
385 if ((mixer&4) == 0) {
386 level = chan3;
387 doTone!2(level, toneCount, chan3);
389 if ((mixer&0x20) == 0 && noiseToggle) chan3 = 0;
391 if (lastChan1 != chan1) {
392 if (Config.sndAYEnabled && !sndvSkipSynthV) {
393 synthA.update(curts, chan1);
394 if (rsynthA !is null) rsynthA.update(curts, chan1);
396 lastChan1 = chan1;
399 if (lastChan2 != chan2) {
400 if (Config.sndAYEnabled && !sndvSkipSynthV) {
401 synthB.update(curts, chan2);
402 if (rsynthB !is null) rsynthB.update(curts, chan2);
404 lastChan2 = chan2;
407 if (lastChan3 != chan3) {
408 if (Config.sndAYEnabled && !sndvSkipSynthV) {
409 synthC.update(curts, chan3);
410 if (rsynthC !is null) rsynthC.update(curts, chan3);
412 lastChan3 = chan3;
415 // update noise RNG/filter
416 noiseTick += noiseCount;
417 while (noiseTick >= noisePeriod) {
418 noiseTick -= noisePeriod;
419 if ((rng&1)^(rng&2 ? 1 : 0)) noiseToggle = !noiseToggle;
420 // rng is 17-bit shift reg, bit 0 is output. input is bit 0 xor bit 3.
421 if (rng&1) rng ^= 0x24000;
422 rng >>= 1;
423 // don't keep trying if period is zero
424 if (!noisePeriod) break;
431 // ////////////////////////////////////////////////////////////////////////// //
432 __gshared BlipBuffer leftBlipBuf;
433 __gshared BlipBuffer rightBlipBuf;
434 __gshared BlipBuffer.Sample[] samples;
435 __gshared BlipSynth lsynthBeeper, rsynthBeeper;
436 __gshared BlipSynth lsynthTape, rsynthTape;
437 __gshared AYEmu sndvAY;
440 // ////////////////////////////////////////////////////////////////////////// //
441 double soundGetVolume (int volume) {
442 if (volume < 0) volume = 0; else if (volume > 400) volume = 400;
443 return volume/100.0;
447 // returns the emulation speed adjusted processor speed
448 uint effectiveProcessorSpeed () nothrow @trusted @nogc { return cast(uint)(cast(ulong)Config.machine.cpuSpeed*Config.emuSpeed/100); }
451 // ////////////////////////////////////////////////////////////////////////// //
452 bool initBlipBuffer (ref BlipBuffer buf) {
453 if (buf is null) buf = new BlipBuffer();
454 buf.clockRate = effectiveProcessorSpeed;
455 /* Allow up to 1s of playback buffer - this allows us to cope with slowing
456 down to 2% of speed where a single Speccy frame generates just under 1s
457 of sound */
458 if (buf.setSampleRate(Config.sampleRate, 1000) != buf.Result.OK) return false;
459 buf.bassFreq = speakerEqConfig[Config.speakerType].bass;
460 return true;
464 void initBlip (BlipBuffer buf, ref BlipSynth synth, ushort volume) {
465 if (synth is null) synth = new BlipSynth();
466 synth.setOutputVolumeTreble(buf, soundGetVolume(volume), speakerEqConfig[Config.speakerType].treble);
470 // ////////////////////////////////////////////////////////////////////////// //
471 int isGoodEmuSpeed () {
472 enum MIN_SPEED_PERCENTAGE = 2;
473 enum MAX_SPEED_PERCENTAGE = 500;
474 return (Config.emuSpeed >= MIN_SPEED_PERCENTAGE && Config.emuSpeed <= MAX_SPEED_PERCENTAGE);
478 /// initialize sound system
479 public void soundInit (bool resetAY=true) {
480 if (sndvEnabled) soundrvDeinit();
481 sndvEnabled = false;
483 /* Allow sound as long as emulation speed is greater than 2%
484 (less than that and a single Speccy frame generates more
485 than a seconds worth of sound which is bigger than the
486 maximum BlipBuffer of 1 second) */
487 if (!isGoodEmuSpeed()) return;
489 // only try for stereo if we need it
490 if (!soundrvInit(true, Config.sampleRate)) return; // always stereo
492 sndvLastSampleRate = Config.sampleRate;
493 sndvRealStereo = (Config.stereoType != Config.Stereo.None);
495 if (sndvAY is null) sndvAY = new AYEmu();
497 sndvEnabled = true;
498 soundSetupSamplers();
499 if (resetAY) sndvAY.reset();
503 /// shutdown sound system
504 public void soundDeinit () {
505 if (sndvEnabled) {
506 soundrvDeinit();
507 sndvEnabled = false;
512 /// doesn't reset AY state; should be called when ANY sound config changed
513 public void soundReinit (bool resetAY=false) {
514 if (!isGoodEmuSpeed()) {
515 if (sndvEnabled) soundrvDeinit();
516 sndvEnabled = false;
517 return;
520 if (!sndvEnabled || sndvLastSampleRate != Config.sampleRate) { soundInit(); return; }
522 sndvRealStereo = (Config.stereoType != Config.Stereo.None);
523 soundSetupSamplers();
524 if (resetAY) sndvAY.reset();
528 /// doesn't reset AY state
529 public void soundForceReinit (bool resetAY=false) {
530 if (!sndvEnabled) return;
531 soundInit(resetAY);
535 // ////////////////////////////////////////////////////////////////////////// //
536 void soundSetupSamplers () {
537 if (!initBlipBuffer(leftBlipBuf)) assert(0, "cannot initialize blip buffers");
538 if (!initBlipBuffer(rightBlipBuf)) assert(0, "cannot initialize blip buffers");
540 initBlip(leftBlipBuf, lsynthBeeper, Config.volumeBeeper);
541 initBlip(rightBlipBuf, rsynthBeeper, Config.volumeBeeper); // just in case
543 initBlip(leftBlipBuf, lsynthTape, Config.volumeTape);
544 initBlip(rightBlipBuf, rsynthTape, Config.volumeTape); // just in case
546 /* Adjust relative processor speed to deal with adjusting sound generation
547 frequency against emulation speed (more flexible than adjusting generated
548 sample rate) */
549 float hz = cast(float)effectiveProcessorSpeed/Config.machine.timings.tstatesPerFrame;
551 //{ import iv.cmdcon; conwriteln("hz=", cast(float)Config.machine.cpuSpeed/Config.machine.timings.tstatesPerFrame); }
552 //{ import iv.cmdcon; conwriteln("hz=", cast(float)Config.machine.aySpeed/Config.machine.timings.tstatesPerFrame); }
554 // size of audio data we will get from running a single Spectrum frame
555 sndvFrameSize = cast(int)(cast(float)Config.sampleRate/hz);
556 ++sndvFrameSize;
558 //{ import iv.cmdcon; conwriteln("hz=", hz, "; frsz=", sndvFrameSize); }
560 samples.assumeSafeAppend;
561 samples.length = sndvFrameSize*2/*sndvChans*/;
563 sndvLastSilentLevelL = int.min;
564 sndvLastSilentLevelR = int.min;
566 sndvAY.setupBlips();
570 // ////////////////////////////////////////////////////////////////////////// //
571 public void soundBlankFrame () nothrow @trusted {
572 if (!sndvEnabled || sndvSkipSynthV) return;
573 leftBlipBuf.clear!true();
574 if (sndvRealStereo) rightBlipBuf.clear!true();
575 samples[0..sndvFrameSize*2/*sndvChans*/] = 0;
576 soundrvFrame(samples.ptr, sndvFrameSize);
580 /// send next sound frame to sound card
581 /// return `true` if frame was silent
582 public bool soundFrame(bool checkSilent=false) () nothrow @trusted {
583 if (!sndvEnabled) return true;
585 if (sndvSkipSynthV) return false;
587 // overlay AY sound
588 sndvAY.soundOverlay();
590 leftBlipBuf.endFrame(Config.machine.timings.tstatesPerFrame);
591 //conwriteln("avail: ", leftBlipBuf.samplesAvail);
592 //samples[0..sound_framesiz*sound_channels] = 0;
594 int count;
595 if (sndvRealStereo) {
596 rightBlipBuf.endFrame(Config.machine.timings.tstatesPerFrame);
597 // read left channel into even samples, right channel into odd samples: LRLRLRLRLR...
598 count = leftBlipBuf.readSamples!true(samples.ptr, sndvFrameSize);
599 auto c1 = rightBlipBuf.readSamples!true(samples.ptr+1, count);
600 if (c1 != count) assert(0, "blip synth fucked");
601 } else {
602 count = leftBlipBuf.readSamples!(true, true)(samples.ptr, sndvFrameSize); // fake stereo
605 /* check for a silent frame.
606 * bit nasty, but it's the only way to be sure. :-)
607 * We check pre-fade, and make a separate check for having faded-out
608 * later. This is to avoid problems with beeper `silence' which is
609 * really a constant high/low level (something similar is also
610 * possible with the AY).
612 * To cope with beeper and arguably-buggy .ay files, we have to treat
613 * *any* non-varying level as silence. Fair enough in a way, as it
614 * will indeed be silent, but a bit of a pain.
616 static if (checkSilent) {
617 bool silent = true;
618 short* ptr = samples.ptr;
619 // stereo anyway
620 auto chkl = *ptr++;
621 auto chkr = *ptr++;
622 if (chkl != sndvLastSilentLevelL || chkr != sndvLastSilentLevelR) {
623 silent = false;
624 } else {
625 foreach (immutable _; 1..count) {
626 if (*ptr++ != chkl) { silent = false; break; }
627 if (*ptr++ != chkr) { silent = false; break; }
630 // even if they're all the same, it doesn't count if the level's changed since last time...
631 // save last sample for comparison next time
632 sndvLastSilentLevelL = samples[count-2];
633 sndvLastSilentLevelR = samples[count-1];
634 } else {
635 enum silent = false;
638 soundrvFrame(samples.ptr, count);
640 return silent;
644 // ////////////////////////////////////////////////////////////////////////// //
645 /// call this after machine reset
646 public void soundResetAY () nothrow @trusted @nogc {
647 if (sndvAY !is null) sndvAY.reset();
651 /// don't make the change immediately; record it for later, to be made by sound_frame() (via soundOverlay())
652 public void soundWriteAY (ubyte reg, ubyte val, uint nowts) nothrow @trusted @nogc {
653 if (sndvEnabled && sndvAY !is null) sndvAY.writeReg(reg, val, nowts);
656 /// when something was written to ULA port, this should be called
657 /// on is: bit0 is tape microphone bit, bit1 is speaker bit, i.e.: (!!(b&0x10)<<1)+((!(b&0x8))|tape_microphone)
658 public void soundWriteBeeper (uint at_tstates, int on) nothrow @trusted @nogc {
659 static immutable int[4] beeperAmpl = [ 0, AmplTape, AmplBeeper, AmplBeeper+AmplTape ];
661 if (!sndvEnabled || (!Config.sndBeeperEnabled && !Config.sndTapeEnabled)) return;
663 if (sndvSkipSynthV) return;
666 if (tape_is_playing) {
667 // timex machines have no loading noise
668 if (!/*settings_current.*/sound_load || machine_current.timex) on = on&0x02;
669 } else {
670 // ULA book says that MIC only isn't enough to drive the speaker as output voltage is below the 1.4v threshold
671 if (on == 1) on = 0;
675 if (Config.sndTapeEnabled) {
676 int val = (on&0x01 ? AmplBeeper : 0);
677 lsynthTape.update(at_tstates, val);
678 if (sndvRealStereo) rsynthTape.update(at_tstates, val);
681 if (Config.sndBeeperEnabled) {
682 // ULA book says that MIC only isn't enough to drive the speaker as output voltage is below the 1.4v threshold
683 if (on == 1) on = 0;
684 int val = beeperAmpl[on];
685 lsynthBeeper.update(at_tstates, val);
686 if (sndvRealStereo) rsynthBeeper.update(at_tstates, val);