8bitworkshop/presets/sms-sms-libcv/PSGlib.c

557 lines
20 KiB
C

/* **************************************************
PSGlib - C programming library for the SEGA PSG
( part of devkitSMS - github.com/sverx/devkitSMS )
************************************************** */
// http://www.smspower.org/forums/16925-PSGToolAVGMToPSGConvertor
// https://github.com/sverx/PSGlib/blob/master/tools/src/vgm2psg.c
// http://steveproxna.blogspot.com/2017/11/devkitsms-programming-sample.html
// http://battleofthebits.org/lyceum/View/Vortex+Tracker+II/
#include "PSGlib.h"
#define PSGDataPort #0x7f
#define PSGLatch #0x80
#define PSGData #0x40
#define PSGChannel0 #0b00000000
#define PSGChannel1 #0b00100000
#define PSGChannel2 #0b01000000
#define PSGChannel3 #0b01100000
#define PSGVolumeData #0b00010000
#define PSGWait #0x38
#define PSGSubString #0x08
#define PSGLoop #0x01
#define PSGEnd #0x00
/* define PSGPort (SDCC z80 syntax) */
__sfr __at 0x7F PSGPort;
// fundamental vars
unsigned char PSGMusicStatus; // are we playing a background music?
void *PSGMusicStart; // the pointer to the beginning of music
void *PSGMusicPointer; // the pointer to the current
void *PSGMusicLoopPoint; // the pointer to the loop begin
unsigned char PSGMusicSkipFrames; // the frames we need to skip
unsigned char PSGLoopFlag; // the tune should loop or not (flag)
unsigned char PSGMusicLastLatch; // the last PSG music latch
unsigned char PSGMusicVolumeAttenuation; // the volume attenuation applied to the tune (0-15)
// decompression vars
unsigned char PSGMusicSubstringLen; // lenght of the substring we are playing
void *PSGMusicSubstringRetAddr; // return to this address when substring is over
// volume/frequence buffering
unsigned char PSGChan0Volume; // the volume for channel 0
unsigned char PSGChan1Volume; // the volume for channel 1
unsigned char PSGChan2Volume; // the volume for channel 2
unsigned char PSGChan3Volume; // the volume for channel 3
unsigned char PSGChan2LowTone; // the low tone bits for channels 2
unsigned char PSGChan2HighTone; // the high tone bits for channel 2
unsigned char PSGChan3LowTone; // the tone bits for channels 3
// flags for channels 2-3 access
unsigned char PSGChannel2SFX; // !0 means channel 2 is allocated to SFX
unsigned char PSGChannel3SFX; // !0 means channel 3 is allocated to SFX
// volume/frequence buffering for SFX
unsigned char PSGSFXChan2Volume; // the volume for SFX channel 2
unsigned char PSGSFXChan3Volume; // the volume for SFX channel 3
// fundamental vars for SFX
unsigned char PSGSFXStatus; // are we playing a SFX?
void *PSGSFXStart; // the pointer to the beginning of SFX
void *PSGSFXPointer; // the pointer to the current address
void *PSGSFXLoopPoint; // the pointer to the loop begin
unsigned char PSGSFXSkipFrames; // the frames we need to skip
unsigned char PSGSFXLoopFlag; // the SFX should loop or not (flag)
// decompression vars for SFX
unsigned char PSGSFXSubstringLen; // lenght of the substring we are playing
void *PSGSFXSubstringRetAddr; // return to this address when substring is over
void PSGStop (void) {
/* *********************************************************************
stops the music (leaving the SFX on, if it's playing)
*/
if (PSGMusicStatus) {
PSGPort=PSGLatch|PSGChannel0|PSGVolumeData|0x0F; // latch channel 0, volume=0xF (silent)
PSGPort=PSGLatch|PSGChannel1|PSGVolumeData|0x0F; // latch channel 1, volume=0xF (silent)
if (!PSGChannel2SFX)
PSGPort=PSGLatch|PSGChannel2|PSGVolumeData|0x0F; // latch channel 2, volume=0xF (silent)
if (!PSGChannel3SFX)
PSGPort=PSGLatch|PSGChannel3|PSGVolumeData|0x0F; // latch channel 3, volume=0xF (silent)
PSGMusicStatus=PSG_STOPPED;
}
}
void PSGResume (void) {
/* *********************************************************************
resume the previously playing music
*/
if (!PSGMusicStatus) {
PSGPort=PSGLatch|PSGChannel0|PSGVolumeData|PSGChan0Volume; // restore channel 0 volume
PSGPort=PSGLatch|PSGChannel1|PSGVolumeData|PSGChan1Volume; // restore channel 1 volume
if (!PSGChannel2SFX) {
PSGPort=PSGLatch|PSGChannel2|(PSGChan2LowTone&0x0F); // restore channel 2 frequency
PSGPort=PSGChan2HighTone&0x3F;
PSGPort=PSGLatch|PSGChannel2|PSGVolumeData|PSGChan2Volume; // restore channel 2 volume
}
if (!PSGChannel3SFX) {
PSGPort=PSGLatch|PSGChannel3|(PSGChan3LowTone&0x0F); // restore channel 3 frequency
PSGPort=PSGLatch|PSGChannel3|PSGVolumeData|PSGChan3Volume; // restore channel 3 volume
}
PSGMusicStatus=PSG_PLAYING;
}
}
void PSGPlay (void *song) {
/* *********************************************************************
receives the address of the PSG to start playing (continuously)
*/
PSGStop();
PSGLoopFlag=1;
PSGMusicStart=song; // store the begin point of music
PSGMusicPointer=song; // set music pointer to begin of music
PSGMusicLoopPoint=song; // looppointer points to begin too
PSGMusicSkipFrames=0; // reset the skip frames
PSGMusicSubstringLen=0; // reset the substring len (for compression)
PSGMusicLastLatch=PSGLatch|PSGChannel0|PSGVolumeData|0x0F; // latch channel 0, volume=0xF (silent)
PSGMusicStatus=PSG_PLAYING;
}
void PSGCancelLoop (void) {
/* *********************************************************************
sets the currently looping music to no more loops after the current
*/
PSGLoopFlag=0;
}
void PSGPlayNoRepeat (void *song) {
/* *********************************************************************
receives the address of the PSG to start playing (once)
*/
PSGPlay(song);
PSGLoopFlag=0;
}
unsigned char PSGGetStatus (void) {
/* *********************************************************************
returns the current status of music
*/
return(PSGMusicStatus);
}
void PSGSilenceChannels (void) {
/* *********************************************************************
silence all the PSG channels
*/
PSGPort=PSGLatch|PSGChannel0|PSGVolumeData|0x0F;
PSGPort=PSGLatch|PSGChannel1|PSGVolumeData|0x0F;
PSGPort=PSGLatch|PSGChannel2|PSGVolumeData|0x0F;
PSGPort=PSGLatch|PSGChannel3|PSGVolumeData|0x0F;
}
void PSGRestoreVolumes (void) {
/* *********************************************************************
restore the PSG channels volumes (if a tune or an SFX uses them!)
*/
if (PSGMusicStatus) {
PSGPort=PSGLatch|PSGChannel0|PSGVolumeData|((PSGChan0Volume+PSGMusicVolumeAttenuation>15)?15:PSGChan0Volume+PSGMusicVolumeAttenuation);
PSGPort=PSGLatch|PSGChannel1|PSGVolumeData|((PSGChan1Volume+PSGMusicVolumeAttenuation>15)?15:PSGChan1Volume+PSGMusicVolumeAttenuation);
}
if (PSGChannel2SFX)
PSGPort=PSGLatch|PSGChannel2|PSGVolumeData|PSGSFXChan2Volume;
else if (PSGMusicStatus)
PSGPort=PSGLatch|PSGChannel2|PSGVolumeData|((PSGChan2Volume+PSGMusicVolumeAttenuation>15)?15:PSGChan2Volume+PSGMusicVolumeAttenuation);
if (PSGChannel3SFX)
PSGPort=PSGLatch|PSGChannel3|PSGVolumeData|PSGSFXChan3Volume;
else if (PSGMusicStatus)
PSGPort=PSGLatch|PSGChannel3|PSGVolumeData|((PSGChan3Volume+PSGMusicVolumeAttenuation>15)?15:PSGChan3Volume+PSGMusicVolumeAttenuation);
}
void PSGSetMusicVolumeAttenuation (unsigned char attenuation) {
/* *********************************************************************
sets the volume attenuation for the music (0-15)
*/
PSGMusicVolumeAttenuation=attenuation;
if (PSGMusicStatus) {
PSGPort=PSGLatch|PSGChannel0|PSGVolumeData|((PSGChan0Volume+PSGMusicVolumeAttenuation>15)?15:PSGChan0Volume+PSGMusicVolumeAttenuation);
PSGPort=PSGLatch|PSGChannel1|PSGVolumeData|((PSGChan1Volume+PSGMusicVolumeAttenuation>15)?15:PSGChan1Volume+PSGMusicVolumeAttenuation);
if (!PSGChannel2SFX)
PSGPort=PSGLatch|PSGChannel2|PSGVolumeData|((PSGChan2Volume+PSGMusicVolumeAttenuation>15)?15:PSGChan2Volume+PSGMusicVolumeAttenuation);
if (!PSGChannel3SFX)
PSGPort=PSGLatch|PSGChannel3|PSGVolumeData|((PSGChan3Volume+PSGMusicVolumeAttenuation>15)?15:PSGChan3Volume+PSGMusicVolumeAttenuation);
}
}
void PSGSFXStop (void) {
/* *********************************************************************
stops the SFX (leaving the music on, if it's playing)
*/
if (PSGSFXStatus) {
if (PSGChannel2SFX) {
if (PSGMusicStatus) {
PSGPort=PSGLatch|PSGChannel2|(PSGChan2LowTone&0x0F);
PSGPort=PSGChan2HighTone&0x3F;
PSGPort=PSGLatch|PSGChannel2|PSGVolumeData|(((PSGChan2Volume&0x0F)+PSGMusicVolumeAttenuation>15)?15:(PSGChan2Volume&0x0F)+PSGMusicVolumeAttenuation);
} else {
PSGPort=PSGLatch|PSGChannel2|PSGVolumeData|0x0F;
}
PSGChannel2SFX=PSG_STOPPED;
}
if (PSGChannel3SFX) {
if (PSGMusicStatus) {
PSGPort=PSGLatch|PSGChannel3|(PSGChan3LowTone&0x0F);
PSGPort=PSGLatch|PSGChannel3|PSGVolumeData|(((PSGChan3Volume&0x0F)+PSGMusicVolumeAttenuation>15)?15:(PSGChan3Volume&0x0F)+PSGMusicVolumeAttenuation);
} else {
PSGPort=PSGLatch|PSGChannel3|PSGVolumeData|0x0F;
}
PSGChannel3SFX=PSG_STOPPED;
}
PSGSFXStatus=PSG_STOPPED;
}
}
void PSGSFXPlay (void *sfx, unsigned char channels) {
/* *********************************************************************
receives the address of the SFX to start and the mask that indicates
which channel(s) the SFX will use
*/
PSGSFXStop();
PSGSFXLoopFlag=0;
PSGSFXStart=sfx; // store begin of SFX
PSGSFXPointer=sfx; // set the pointer to begin of SFX
PSGSFXLoopPoint=sfx; // looppointer points to begin too
PSGSFXSkipFrames=0; // reset the skip frames
PSGSFXSubstringLen=0; // reset the substring len
PSGChannel2SFX=(channels&SFX_CHANNEL2)?PSG_PLAYING:PSG_STOPPED;
PSGChannel3SFX=(channels&SFX_CHANNEL3)?PSG_PLAYING:PSG_STOPPED;
PSGSFXStatus=PSG_PLAYING;
}
void PSGSFXCancelLoop (void) {
/* *********************************************************************
sets the currently looping SFX to no more loops after the current
*/
PSGSFXLoopFlag=0;
}
unsigned char PSGSFXGetStatus (void) {
/* *********************************************************************
returns the current SFX status
*/
return(PSGSFXStatus);
}
void PSGSFXPlayLoop (void *sfx, unsigned char channels) {
/* *********************************************************************
receives the address of the SFX to start continuously and the mask
that indicates which channel(s) the SFX will use
*/
PSGSFXPlay(sfx, channels);
PSGSFXLoopFlag=1;
}
void PSGFrame (void) {
/* *********************************************************************
processes a music frame
*/
__asm
ld a,(_PSGMusicStatus) ; check if we have got to play a tune
or a
ret z
ld a,(_PSGMusicSkipFrames) ; check if we have got to skip frames
or a
jp nz,_skipFrame
ld hl,(_PSGMusicPointer) ; read current address
_intLoop:
ld b,(hl) ; load PSG byte (in B)
inc hl ; point to next byte
ld a,(_PSGMusicSubstringLen) ; read substring len
or a
jr z,_continue ; check if it is 0 (we are not in a substring)
dec a ; decrease len
ld (_PSGMusicSubstringLen),a ; save len
jr nz,_continue
ld hl,(_PSGMusicSubstringRetAddr) ; substring is over, retrieve return address
_continue:
ld a,b ; copy PSG byte into A
cp PSGLatch ; is it a latch?
jr c,_noLatch ; if < $80 then it is NOT a latch
ld (_PSGMusicLastLatch),a ; it is a latch - save it in "LastLatch"
; we have got the latch PSG byte both in A and in B
; and we have to check if the value should pass to PSG or not
bit 4,a ; test if it is a volume
jr nz,_latch_Volume ; jump if volume data
bit 6,a ; test if the latch it is for channels 0-1 or for 2-3
jp z,_send2PSG_A ; send data to PSG if it is for channels 0-1
; we have got the latch (tone, chn 2 or 3) PSG byte both in A and in B
; and we have to check if the value should be passed to PSG or not
bit 5,a ; test if tone it is for channel 2 or 3
jr z,_ifchn2 ; jump if channel 2
ld (_PSGChan3LowTone),a ; save tone LOW data
ld a,(_PSGChannel3SFX) ; channel 3 free?
or a
jp nz,_intLoop
ld a,(_PSGChan3LowTone)
and #3 ; test if channel 3 is set to use the frequency of channel 2
cp #3
jr nz,_send2PSG_B ; if channel 3 does not use frequency of channel 2 jump
ld a,(_PSGSFXStatus) ; test if an SFX is playing
or a
jr z,_send2PSG_B ; if no SFX is playing jump
ld (_PSGChannel3SFX),a ; otherwise mark channel 3 as occupied
ld a,PSGLatch|PSGChannel3|PSGVolumeData|#0x0F ; and silence channel 3
out (PSGDataPort),a
jp _intLoop
_ifchn2:
ld (_PSGChan2LowTone),a ; save tone LOW data
ld a,(_PSGChannel2SFX) ; channel 2 free?
or a
jr z,_send2PSG_B
jp _intLoop
_latch_Volume:
bit 6,a ; test if the latch it is for channels 0-1 or for 2-3
jr nz,_latch_Volume_23 ; volume is for channel 2 or 3
bit 5,a ; test if volume it is for channel 0 or 1
jr z,_chn0 ; jump for channel 0
ld (_PSGChan1Volume),a ; save volume data
jp _sendVolume2PSG_A
_chn0:
ld (_PSGChan0Volume),a ; save volume data
jp _sendVolume2PSG_A
_latch_Volume_23:
bit 5,a ; test if volume it is for channel 2 or 3
jr z,_chn2 ; jump for channel 2
ld (_PSGChan3Volume),a ; save volume data
ld a,(_PSGChannel3SFX) ; channel 3 free?
or a
jr z,_sendVolume2PSG_B
jp _intLoop
_chn2:
ld (_PSGChan2Volume),a ; save volume data
ld a,(_PSGChannel2SFX) ; channel 2 free?
or a
jr z,_sendVolume2PSG_B
jp _intLoop
_skipFrame:
dec a
ld (_PSGMusicSkipFrames),a
ret
_noLatch:
cp PSGData
jr c,_command ; if < $40 then it is a command
; it is a data
ld a,(_PSGMusicLastLatch) ; retrieve last latch
jp _output_NoLatch
_command:
cp PSGWait
jr z,_done ; no additional frames
jr c,_otherCommands ; other commands?
and #0x07 ; take only the last 3 bits for skip frames
ld (_PSGMusicSkipFrames),a ; we got additional frames
_done:
ld (_PSGMusicPointer),hl ; save current address
ret ; frame done
_otherCommands:
cp PSGSubString
jr nc,_substring
cp PSGEnd
jr z,_musicLoop
cp PSGLoop
jr z,_setLoopPoint
; ***************************************************************************
; we should never get here!
; if we do, it means the PSG file is probably corrupted, so we just RET
; ***************************************************************************
ret
_send2PSG_B:
ld a,b
_send2PSG_A:
out (PSGDataPort),a ; output the byte
jp _intLoop
_sendVolume2PSG_B:
ld a,b
_sendVolume2PSG_A:
ld c,a ; save the PSG command byte
and #0x0F ; keep lower nibble
ld b,a ; save value
ld a,(_PSGMusicVolumeAttenuation) ; load volume attenuation
add a,b ; add value
cp #0x0F ; check overflow
jr c,_no_overflow ; if it is <=15 then ok
ld a,#0x0F ; else, reset to 15
_no_overflow:
ld b,a ; save new attenuated volume value
ld a,c ; retrieve PSG command
and #0xF0 ; keep upper nibble
or b ; set attenuated volume
out (PSGDataPort),a ; output the byte
jp _intLoop
_output_NoLatch:
; we got the last latch in A and the PSG data in B
; and we have to check if the value should pass to PSG or not
; note that non-latch commands can be only contain frequencies (no volumes)
; for channels 0,1,2 only (no noise)
bit 6,a ; test if the latch it is for channels 0-1 or for chn 2
jr nz,_high_part_Tone ; it is tone data for channel 2
jp _send2PSG_B ; otherwise, it is for chn 0 or 1 so we have done!
_setLoopPoint:
ld (_PSGMusicLoopPoint),hl
jp _intLoop
_musicLoop:
ld a,(_PSGLoopFlag) ; looping requested?
or a
jp z,_PSGStop ; No:stop it! (tail call optimization)
ld hl,(_PSGMusicLoopPoint)
jp _intLoop
_substring:
sub PSGSubString-4 ; len is value - $08 + 4
ld (_PSGMusicSubstringLen),a ; save len
ld c,(hl) ; load substring address (offset)
inc hl
ld b,(hl)
inc hl
ld (_PSGMusicSubstringRetAddr),hl ; save return address
ld hl,(_PSGMusicStart)
add hl,bc ; make substring current
jp _intLoop
_high_part_Tone:
; we got the last latch in A and the PSG data in B
; and we have to check if the value should pass to PSG or not
; PSG data can only be for channel 2, here
ld a,b ; move PSG data in A
ld (_PSGChan2HighTone),a ; save channel 2 tone HIGH data
ld a,(_PSGChannel2SFX) ; channel 2 free?
or a
jr z,_send2PSG_B
jp _intLoop
__endasm;
}
void PSGSFXFrame (void) {
/* ********************************************************************
processes a SFX frame
*/
__asm
ld a,(_PSGSFXStatus) ; check if we have got to play SFX
or a
ret z
ld a,(_PSGSFXSkipFrames) ; check if we have got to skip frames
or a
jp nz,_skipSFXFrame
ld hl,(_PSGSFXPointer) ; read current SFX address
_intSFXLoop:
ld b,(hl) ; load a byte in B, temporary
inc hl ; point to next byte
ld a,(_PSGSFXSubstringLen) ; read substring len
or a ; check if it is 0 (we are not in a substring)
jr z,_SFXcontinue
dec a ; decrease len
ld (_PSGSFXSubstringLen),a ; save len
jr nz,_SFXcontinue
ld hl,(_PSGSFXSubstringRetAddr) ; substring over, retrieve return address
_SFXcontinue:
ld a,b ; restore byte
cp PSGData
jp c,_SFXcommand ; if less than $40 then it is a command
bit 4,a ; check if it is a volume byte
jr z,_SFXoutbyte ; if not, output it
bit 5,a ; check if it is volume for channel 2 or channel 3
jr nz,_SFXvolumechn3
ld (_PSGSFXChan2Volume),a
jr _SFXoutbyte
_SFXvolumechn3:
ld (_PSGSFXChan3Volume),a
_SFXoutbyte:
out (PSGDataPort),a ; output the byte
jp _intSFXLoop
_skipSFXFrame:
dec a
ld (_PSGSFXSkipFrames),a
ret
_SFXcommand:
cp PSGWait
jr z,_SFXdone ; no additional frames
jr c,_SFXotherCommands ; other commands?
and #0x07 ; take only the last 3 bits for skip frames
ld (_PSGSFXSkipFrames),a ; we got additional frames to skip
_SFXdone:
ld (_PSGSFXPointer),hl ; save current address
ret ; frame done
_SFXotherCommands:
cp PSGSubString
jr nc,_SFXsubstring
cp PSGEnd
jr z,_sfxLoop
cp PSGLoop
jr z,_SFXsetLoopPoint
; ***************************************************************************
; we should never get here!
; if we do, it means the PSG SFX file is probably corrupted, so we just RET
; ***************************************************************************
ret
_SFXsetLoopPoint:
ld (_PSGSFXLoopPoint),hl
jp _intSFXLoop
_sfxLoop:
ld a,(_PSGSFXLoopFlag) ; is it a looping SFX?
or a
jp z,_PSGSFXStop ; No:stop it! (tail call optimization)
ld hl,(_PSGSFXLoopPoint)
ld (_PSGSFXPointer),hl
jp _intSFXLoop
_SFXsubstring:
sub PSGSubString-4 ; len is value - $08 + 4
ld (_PSGSFXSubstringLen),a ; save len
ld c,(hl) ; load substring address (offset)
inc hl
ld b,(hl)
inc hl
ld (_PSGSFXSubstringRetAddr),hl ; save return address
ld hl,(_PSGSFXStart)
add hl,bc ; make substring current
jp _intSFXLoop
__endasm;
}