Skip to content

Instantly share code, notes, and snippets.

@ISSOtm
Last active June 26, 2019 02:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ISSOtm/5cf96e4cf385ca048fc0f190a29688f0 to your computer and use it in GitHub Desktop.
Save ISSOtm/5cf96e4cf385ca048fc0f190a29688f0 to your computer and use it in GitHub Desktop.
Prototype for a Game Boy sample player, eventually ended up at https://github.com/ISSOtm/smooth-player
; Here's some technical explanation about Game Boy sample playing:
; The "usual" way of playing sound samples on the Game Boy is to use its wave channel
; Basically, let that channel play its 32 4-bit samples, then refill it
; Problem: to refill the channel, you have to disable it then restart it
; However, when doing so, the "sample buffer" is set to 0 **and not updated**
; This means the channel outputs a spike as its first sample, creating a buzzing sound
;
; Why? That's because of how the Game Boy generates sound: each channel produces
; a digital value between 0 and 15, which is fed to a DAC (Digital to Analog Converter)
; which converts it to an analog value I don't know the range of
; And finally, all analog values get sent to the SO1 and SO2 terminals, where they get
; mixed together and scaled based on NR51 and NR50 respectively, then sent to speakers
; The problem is that DIGITAL zero maps to ANALOG maximum; therefore the channel
; doesn't play silence when starting up. (The GB's APU appears to be poorly designed)
;
; What this means is that using CH3 inherently has this spiky problem
; No solution has been found to use another channel to compensate,
; so we need to think outside the box...
; The solution used here is to make all osund channels play a DC offset (a constant)
; but a different one for each channel; then, we pick which ones get added to reach
; any constant, by selecting which channels are fed to the mixed via NR51
; This gives 4-bit PCM at a selectable frequency (nice!) that also does stereo (ooh!)
; but that hogs all sound channels (ah.) and requires more CPU (erm)
StartSample::
; Prevent sample from playing while in inconsistent state
xor a
ldh [rTAC], a
ld a, l
ldh [hSampleReadPtr], a
ld a, h
ldh [hSampleReadPtr+1], a
; We need to reset the APU quickly to reset the pulse channels' phases
xor a
ldh [rAUDENA], a
ld a, $80
ldh [rAUDENA], a
; As far as currently known, the values of all sound registers are unknown after powering on
; Therefore it must be done first
; Disconnect all channels to play silence while we're setting up
xor a
ldh [rAUDTERM], a
; ~ Setting up CH3 ~
; CH3 can be made to output a constant value without trickery
; We just define its sample to be, well, a constant wave
; We do want to do this as early as possible, because the first sample CH3 puts out
; Disable channel to unlock buffer
xor a
ldh [rAUD3ENA], a
ld a, $CC
ld bc, 16 << 8 | LOW(_AUD3WAVERAM)
.writeWave
ldh [c], a
inc c
dec b
jr nz, .writeWave
; Re-enable channel so it can play
ld a, $80
ldh [rAUD3ENA], a
; Retrigger channel so it starts playing
; ld a, $80
ldh [rAUD3HIGH], a
; ~ Setting up CH4 ~
; CH4 will output digital 0 == analog max
; This is done by turning on its DAC, but not the LFSR circuitry
; The DAC is turned on by writing a non-zero value to the envelope, the LFSR by "restarting" the channel via NR44
ld a, $F0
ldh [rAUD4ENV], a
; ~ Setting up CH1 and CH2 ~
; Those two are more complicated, because we can't use the same trick
; as for CH4, and they will keep playing squares
; The trick is to keep restarting them so they always play the same offset
; It's here that we hit a difficulty, though: two duty cycles start the channel
; at digital 0, which is NOT what we want;
; instead, we want to stay in the "1" part of the square wave
; So we use duty cycle 25% or 50%
; We will also obviously need the frequency to be as low as possible
; And finally, we want CH1 to output digital 9, and CH2 digital 10
ld a, $90
ldh [rAUD1ENV], a
ld a, $A0
ldh [rAUD2ENV], a
xor a
ldh [rAUD1LOW], a
ldh [rAUD2LOW], a
; Don't write to rAUDxHIGH, that's done by the sample player
; Enable timer interrupt
ldh a, [rIE]
or IEF_TIMER
ldh [rIE], a
xor a
ldh [rTMA], a
; Make a sample trigger right after, to ensure the pulse channels don't
ld a, $FF
ldh [rTIMA], a
; Start counting down
ld a, $04
ldh [rTAC], a
ret
; Reset phase of pulse channels to make them play constant offsets
; Done first as it may be part of the process of setting up the
ld a, $80
ldh [rAUD1HIGH], a
ldh [rAUD2HIGH], a
; Play one byte of the sound sample
ldh a, [hSampleReadPtr]
ld l, a
ldh a, [hSampleReadPtr+1]
ld h, a
ld a, [hli]
ldh [rAUDTERM], a
ld a, l
ldh [hSampleReadPtr], a
ld a, h
ldh [hSampleReadPtr+1], a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment