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).
24 import arsd
.simpleaudio
;
30 // ////////////////////////////////////////////////////////////////////////// //
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;
43 public struct SpeakerEqConfig
{
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
68 // the AY steps down the external clock by 16 for tone and noise generators
69 enum AY_CLOCK_DIVISOR
= 16;
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
86 // scale the values down to fit
87 foreach (immutable f
; 0..16) res
[f
] = (levels
[f
]*AmplAYTone
+0x8000)/0xffff;
91 // bitmasks for envelope
93 enum AY_ENV_ATTACK
= 4;
98 uint[3] toneTick
, toneHigh
;
100 uint toneCycles
, envCycles
;
101 uint envInternalTick
, envTick
;
103 uint noisePeriod
, envPeriod
;
104 ubyte[16] registers
; // local copy of the AY registers
110 int envFirst
= 1, envRev
= 0, envCounter
= 15;
111 int[3] lastChanLevel
= 0;
119 /// call this after initializing blip buffer
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
);
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;
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,
155 val
&= ayvaluemask
.ptr
[reg
&0x0f];
156 // update ay register
157 registers
.ptr
[reg
] = val
;
158 // fix things as needed for some register changes
160 case 0: case 1: case 2: case 3: case 4: case 5:
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;
170 noisePeriod
= registers
.ptr
[reg
]&31;
173 envPeriod
= registers
.ptr
[11]|
(registers
.ptr
[12]<<8);
176 envInternalTick
= envTick
= envCycles
= 0;
179 envCounter
= (registers
.ptr
[13]&AY_ENV_ATTACK ?
0 : 15);
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];
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
;
216 while (envCycles
>= 16) {
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
))) {
226 envCounter
-= (envshape
&AY_ENV_ATTACK ?
1 : -1);
228 envCounter
+= (envshape
&AY_ENV_ATTACK ?
1 : -1);
230 if (envCounter
< 0 ) envCounter
= 0;
231 if (envCounter
> 15) envCounter
= 15;
235 while (envInternalTick
>= 16) {
236 envInternalTick
-= 16;
239 if (!(envshape
& AY_ENV_CONT
)) {
242 if (envshape
&AY_ENV_HOLD
) {
243 if (envFirst
&& (envshape
&AY_ENV_ALT
)) envCounter
= (envCounter ?
0 : 15);
246 if (envshape
&AY_ENV_ALT
) {
249 envCounter
= (envshape
&AY_ENV_ATTACK ?
0 : 15);
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
267 toneCycles
+= AY_CLOCK_DIVISOR
;
268 immutable int toneCount
= toneCycles
>>3;
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;
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;
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
;
317 if (sndvAY
is null) sndvAY
= new AYEmu();
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();
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
{
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) {
366 if (frm
.regs
.ptr
[b
] != regs
.ptr
[b
]) return false;
373 __gshared PSGFrameData
[][string
] muzax
;
376 // ////////////////////////////////////////////////////////////////////////// //
377 PSGFrameData
[] loadPSGF (VFile fl
) {
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");
400 public void loadMusic () {
401 void loadit (string name
) {
402 conwriteln("loading music '", name
, "'...");
403 muzax
[name
] = loadPSGF(VFile("mus/"~name
~".psgf"));
415 // ////////////////////////////////////////////////////////////////////////// //
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
;
469 atomicStore(muspaused
, false);
470 atomicStore(musloopcount
, 0);
471 atomicStore(wantnewmusic
, 2);
472 //conwriteln("queued new music: '", name, "' (looped: ", looped, ")");
479 public void musicAbort () {
480 while (cas(&wantnewmusic
, 0, 1) != 0) {}
481 newmuslooped
= false;
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;
496 newsound
= soundlist
[idx
];
498 atomicStore(wantnewsound
, 2);
500 //conwriteln("rejected sound #", idx, " due to priority: cur=", cursndprio, "; prio=", prio);
505 public void soundAbort () {
506 while (cas(&wantnewsound
, 0, 1) != 0) {}
509 atomicStore(cursndcomplete
, 1);
510 atomicStore(wantnewsound
, 2);
514 // ////////////////////////////////////////////////////////////////////////// //
515 //TODO: panning [0..319]
516 void mixSound (short[] buffer
) {
518 if (cursndpos
>= cursound
.length
) {
519 atomicStore(cursndcomplete
, 1);
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
) {
530 if (v
< short.min
) v
= short.min
; else if (v
> short.max
) v
= short.max
;
531 if (xoptSoundOn
) *dp
= cast(short)v
;
536 if (v
< short.min
) v
= short.min
; else if (v
> short.max
) v
= short.max
;
537 if (xoptSoundOn
) *dp
= cast(short)v
;
544 void renderMusic (short[] buffer
) {
545 // process volume changes
547 if (oldVolumeAY
!= volumeAY
) sndvAY
.setupBlips();
549 if (atomicLoad(muspaused
)) {
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) {
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) {
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;
588 if (!xoptMusicOn
) svbuf
[] = 0;
592 void musicThread () {
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) {
605 if (atomicLoad(wantnewmusic
) == 2) {
607 curmuslooped
= newmuslooped
;
608 atomicStore(musloopcount
, 0);
611 atomicStore(wantnewmusic
, 0);
612 //conwriteln("started new music, ", curmusic.length, " frames");
615 if (atomicLoad(wantnewsound
) == 2) {
618 atomicStore(cursndcomplete
, (cursound
.length
== 0 ?
1 : 0));
619 atomicStore(wantnewsound
, 0);
622 // mix digital sound channel
625 atomicStore(musthreadstate
, 2);
627 atomicStore(musthreadstate
, 667);
628 } catch (Exception e
) {
629 isAudioFucked
= true;
630 if (atomicLoad(musthreadstate
) != 666) atomicStore(musthreadstate
, 2);
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
);
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 () {
667 auto fl
= VFile("MYTH.SND");
669 //auto hend = fl.readNum!ubyte;
671 static struct SDir
{ uint ofs
; ushort len
; }
673 scope(exit
) delete dir
;
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
;
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
) {
694 scope(exit
) delete xbuf
;
696 xbuf
.length
= dir
[sndidx
].len
;
697 fl
.seek(dir
[sndidx
].ofs
);
698 fl
.rawReadExact(xbuf
);
700 srb
.setup(1, /*11025/2*/6100, 44100, /*alsaRQuality*/4);
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
;
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
]);
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
, ")");