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.
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).
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
) {
48 // ////////////////////////////////////////////////////////////////////////// //
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
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
93 // scale the values down to fit
94 foreach (immutable f
; 0..16) res
[f
] = (levels
[f
]*AmplAYTone
+0x8000)/0xffff;
99 Config
.Stereo stereoType
= Config
.Stereo
.ABC
;
101 uint[3] toneTick
, toneHigh
;
103 uint toneCycles
, envCycles
;
104 uint envInternalTick
, envTick
;
106 uint noisePeriod
, envPeriod
;
108 // local copy of the AY registers
111 BlipSynth synthA
, synthB
, synthC
;
112 BlipSynth rsynthA
, rsynthB
, rsynthC
;
115 static struct ChangeRec
{
120 ChangeRec
[] regChangelog
;
126 int envFirst
= 1, envRev
= 0, envCounter
= 15;
130 regChangelog
.length
= 16384;
134 // call this after initializing blip buffer
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).
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
);
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;
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
);
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;
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];
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;
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;
252 uint rclLeft
= rclCount
;
254 int chan1
, chan2
, chan3
;
255 int lastChan1
= 0, lastChan2
= 0, lastChan3
= 0;
256 uint toneCount
, noiseCount
;
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 ) )
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
;
274 // fix things as needed for some register changes
276 case 0: case 1: case 2: case 3: case 4: case 5:
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;
286 noisePeriod
= registers
.ptr
[reg
]&31;
289 envPeriod
= registers
.ptr
[11]|
(registers
.ptr
[12]<<8);
292 envInternalTick
= envTick
= envCycles
= 0;
295 envCounter
= (registers
.ptr
[13]&AY_ENV_ATTACK ?
0 : 15);
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];
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
;
313 while (envCycles
>= 16) {
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
))) {
323 envCounter
-= (envshape
&AY_ENV_ATTACK ?
1 : -1);
325 envCounter
+= (envshape
&AY_ENV_ATTACK ?
1 : -1);
327 if (envCounter
< 0 ) envCounter
= 0;
328 if (envCounter
> 15) envCounter
= 15;
332 while (envInternalTick
>= 16) {
333 envInternalTick
-= 16;
336 if (!(envshape
& AY_ENV_CONT
)) {
339 if (envshape
&AY_ENV_HOLD
) {
340 if (envFirst
&& (envshape
&AY_ENV_ALT
)) envCounter
= (envCounter ?
0 : 15);
343 if (envshape
&AY_ENV_ALT
) {
346 envCounter
= (envshape
&AY_ENV_ATTACK ?
0 : 15);
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
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;
373 if ((mixer
&1) == 0) {
375 doTone
!0(level
, toneCount
, chan1
);
377 if ((mixer
&0x08) == 0 && noiseToggle
) chan1
= 0;
379 if ((mixer
&2) == 0) {
381 doTone
!1(level
, toneCount
, chan2
);
383 if ((mixer
&0x10) == 0 && noiseToggle
) chan2
= 0;
385 if ((mixer
&4) == 0) {
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
);
399 if (lastChan2
!= chan2
) {
400 if (Config
.sndAYEnabled
&& !sndvSkipSynthV
) {
401 synthB
.update(curts
, chan2
);
402 if (rsynthB
!is null) rsynthB
.update(curts
, chan2
);
407 if (lastChan3
!= chan3
) {
408 if (Config
.sndAYEnabled
&& !sndvSkipSynthV
) {
409 synthC
.update(curts
, chan3
);
410 if (rsynthC
!is null) rsynthC
.update(curts
, 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;
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;
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
458 if (buf
.setSampleRate(Config
.sampleRate
, 1000) != buf
.Result
.OK
) return false;
459 buf
.bassFreq
= speakerEqConfig
[Config
.speakerType
].bass
;
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();
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();
498 soundSetupSamplers();
499 if (resetAY
) sndvAY
.reset();
503 /// shutdown sound system
504 public void soundDeinit () {
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();
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;
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
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
);
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
;
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;
588 sndvAY
.soundOverlay();
590 leftBlipBuf
.endFrame(Config
.machine
.timings
.tstatesPerFrame
);
591 //conwriteln("avail: ", leftBlipBuf.samplesAvail);
592 //samples[0..sound_framesiz*sound_channels] = 0;
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");
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
) {
618 short* ptr
= samples
.ptr
;
622 if (chkl
!= sndvLastSilentLevelL || chkr
!= sndvLastSilentLevelR
) {
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];
638 soundrvFrame(samples
.ptr
, count
);
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;
670 // ULA book says that MIC only isn't enough to drive the speaker as output voltage is below the 1.4v threshold
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
684 int val
= beeperAmpl
[on
];
685 lsynthBeeper
.update(at_tstates
, val
);
686 if (sndvRealStereo
) rsynthBeeper
.update(at_tstates
, val
);