P*------------------------------- * * playlite.s * * This is the lite version of the music player -- * won't fill up your rasters and won't slow you * down. Fields are not available, and Shadow SID * is not copied over, although it is still available * and used to store information (instruments, frequencies, * etc.). This saves a lot on indexing and such, and * makes for a much more streamlined player (and makes * the full player more streamlined as well). * * The full variable tables are present however. This * makes life easier on the compiler, and also opens * the possibility of e.g. stereo SID files. * * Strategy: * Execute global macros * Decrement duration * If zero, then read in and process new data. Routines to * - Activate macros * - Load variables * - Load instruments * - Load duration/frequency values * Then process macros. If active, then jump into it. * Repeat for the other two voices! * * Note that values are always loaded into Shadow SID; if * Shadow SID is not active, then values are also copied * into real SID. Thus the original SID values are still * accessible by macros. * * Almost there! * * SLJ 11/25/96 * ORG $0FE0 DA MACBEGIN DA FIELDEND DS ^ * * Constants * SID = $D400 NUMINST = 14 ;14 Instruments total (arbitrary) * Player directives * See PROCDATA HOLD = $F0 ;Special note (hold current note) * * My little kernal jump table * KERNAL * JMP MYINIT ;Be sure to comment this out :) JMP MAINPLAY JMP INIT JMP SETIRQ JMP LOADINST ;Load instrument JMP GETP ;Get current voice pointer JMP SETP ;Set current voice pointer JMP V1GETP ;Get voice 1 pointer JMP V2GETP JMP V3GETP JMP V1SETP ;Set voice 1 pointer JMP V2SETP JMP V3SETP RETURN ;This is what macros call to exit. PLA ;Get the exit address STA MACLO,Y PLA STA MACHI,Y PLA ;Active macros RTS ;That's it! TXT 'lite v1.0 slj' :SNEAKY DS KERNAL+64-:SNEAKY ;Fill up to $1000 + 64 ERR *-1/$1040 ;Break if past $1000 + 64 * * Variables, tables, etc. * * * These are the variables and such that must stay fixed * so that the compiler and editor don't screw everything * up. * FREQLO DS 192 ;Low byte, frequencies * $1100 FREQHI DS 192 ;High byte freq, page boundary MARKERS DS 64 ;Table of 32 marker locations * $1200 MACADR DS 48 ;Macro starting addresses ;stored seqentially, i.e. ;lo hi lo hi ... thus may ;be jumped to via JMP() ;MUST start on page boundary GLOBVAR DS 16 ;Global variables SHADOW DS 25 ;Shadow SID DURTAB DS 24 ;Table of initial durations INSTAB ;Instrument table DS 14*9 ;126 bytes TEMPY DS 1 ;Temporary storage LOCOFF DS 1 ;(local) ariable offset ;8*current voice COFFSET DS 1 ;SID offset (0 7 14) of current voice CURVOICE DS 1 ;Current voice CURFIELD DS 1 ;Current field LASTNOTE DS 3 ;Last note read in for voice V1FIELD DS 1 ;Voice 1 field V2FIELD DS 1 ;Voice 2 field V3FIELD DS 1 ;field associated with Voice 3 DURATION DS 6 ;Duration for current field * $1300 TIMERUPT DA $42C6 ;Interrupt timer setting (1/60th sec.) STOPME DS 3 ;Stop this voice CURINST DS 6 ;Current instrument for this field FIELDS ;Pointers to start of fields. DA FIELD1 DA FIELD2 DA FIELD3 DS 6 MACBYTE1 DS 7 ;Active macros, lowest byte MACBYTE2 DS 7 ;1 for each field MACBYTE3 DS 7 ;MUST be sequential! LOCALVAR DS 32 ;3+1 sets of 8 local variables * Missing things: TIMES24 ;x*24 -- macro addr access DFB 0,24,48,72 MACLO DS 24 ;Global macro entry points ;Treated as field #0 DS 72 ;Macro entry/exit points low byte ;24 macros * 3 fields = 72 MACHI DS 24 ;Global entry/exit DS 72 ;Macro entry/exit addresses FOFFSET DS 3 ;Offset into fields REPLO DS 3 ;Repeats, lo address REPHI DS 3 ;Hi address REPTIMES DS 3 ;Number of times to repeat BITP ;Table of 2^(x mod 8) DFB 1,2,4,8,16,32,64,128 DFB 1,2,4,8,16,32,64,128 DFB 1,2,4,8,16,32,64,128 DIV8T7 ;x/8 * 7 (for MACBYTE indexing) DFB 0,0,0,0,0,0,0,0 DFB 7,7,7,7,7,7,7,7 DFB 14,14,14,14,14,14,14,14 SETIRQ SEI ;Otherwise, set up simple IRQ player JSR INIT LDA #IRQ STA $0315 LDA TIMERUPT STA $DC04 LDA TIMERUPT+1 STA $DC05 CLI RTS IRQ ;Simple IRQ player. * INC $D020 JSR MAINPLAY * DEC $D020 IRQBLAH JMP $EA31 * * (Merlin) Macros * * * The (music) macro handler. Shift bits until an active * macro is found, and go from there! If a macro active byte * is zero, then it may be skipped completely. * * Parameters: * ]1 -- Current voice (0 1 2 or 3) * (0 = global macros) * The correct index into macro address tables is * computed as 24*]1 * X is set to the current field * * On exit, Z is zet. * NEWMAC MAC LDA MACBYTE1+]1 ;Active macros -- 3 bytes LDY #24*]1-1 ;Start one lower L1 INY LSR ;Check BCC CONT1 ;Carry set -> call macro CALL1 JSR INITMAC ;Enters macros at initial point CONT1 BNE L1 ;Last line of INITMAC is PLA LDY #]1*24+7 ;Advance to next macro chunk ;(the INY will advance Y to Y+8) LDA MACBYTE2+]1 L2 INY LSR BCC CONT2 JSR INITMAC CONT2 BNE L2 LDY #]1*24+15 LDA MACBYTE3+]1 L3 INY LSR BCC CONT3 JSR INITMAC CONT3 BNE L3 <<< ;That's it! OLDMAC MAC ;Identical to above, with CALLMAC LDA MACBYTE1+]1 ;Active macros -- 3 bytes LDY #]1*24-1 L1 INY LSR ;Check BCC CONT1 ;Carry set -> call macro CALL1 JSR CALLMAC ;Enters macros at initial point CONT1 BNE L1 ;Last line of INITMAC is PLA LDY #]1*24+7 ;Advance to next macro chunk ;(the INY will advance Y to Y+8) LDA MACBYTE2+]1 L2 INY LSR BCC CONT2 JSR CALLMAC CONT2 BNE L2 LDY #]1*24+15 LDA MACBYTE3+]1 L3 INY LSR BCC CONT3 JSR CALLMAC CONT3 BNE L3 <<< ;That's it! * Subroutine to read in new data from the current field. * The field pointers are stored inside the routine, * so each voice stores its own pointers. To switch * fields within a voice, these pointers must be * swapped out and the new pointers swapped in. * * Parameters: * ]1 -- Current offset into the field data. * ]2 -- Current voice # (index for current note/dur) * (1 2 or 3) * ]3 -- SID offset for current voice (0 7 or 14) * * Due to space considerations (i.e. the lite version * probably shouldn't take up 3k :), this is now * a genuine procedure. As a result, the two * variables ]2 and ]3 must be set up elsewhere, * in curfield and coffset. Also required is * LOCOFF, which tells the current offset into * the local variables. * Player instructions SETVOL = $80 ;Set volume ACTGLOB = $81 ;Activate global macro DACTGLOB = $82 ;Deactivate global macro ACTLOC = $83 ;Activate local macro DACTLOC = $84 ;Deactivate local macro LINST = $85 ;Load instrument REPEAT = $86 ;Repeat loop begin ENDREP = $87 ;Go to last REPEAT and repeat! STOP = $88 ;Halts the field at that point GATEON = $89 ;Turn gate bit on GATEOFF = $8A ;Turn gate bit off RESTART = $8B ;Restart player (JMP INIT) JUMP = $8C ;Jump to marker JSUB = $8D ;Subroutine to marker RSUB = $8E ;Return from subroutine HICODE = $8F ;Always last instruction LOADGLOB = %11000000 ;Load global variable (mask) LOADLOC = %11100000 ;Load local variable * Table of handler routine addresses for above TABLE1 DA SETV DA ACTG DA DACTG DA ACTL DA DACTL DA LOADINST DA REP DA ENDR DA STO DA GON DA GOFF DA PLAINIT DA JMPMARK DA JSRMARK DA RTSMARK STACK DS 24 ;Stack for JSR, 4-deep STACKP DS 3 ;Stack index for each field PROCDATA IHANDLE CMP #LOADGLOB ;Load variable has bit 6 set BCC NOLOAD CMP #LOADLOC ;Less than => global load BCC GLOBAL ;Load local is probably the most ;frequent directive. AND #%00011111 ;Strip out the variable number CLC ADC LOCOFF ;Local variable offset TAY ;for current voice TXA STA LOCALVAR,Y ;voice*8th set of entries RTS GLOBAL AND #%00011111 TAY TXA STA GLOBVAR,Y RTS NOLOAD CMP #HICODE ;Restart on anything above BCS PLAINIT ASL ;Double it TAY LDA TABLE1,Y ;Get address of routine STA :JMP+1 LDA TABLE1+1,Y ;hi byte STA :JMP+2 :JMP JMP SETV PLAINIT PLA PLA * * Initialization routine * * Reset field pointers, macros * inactive, fields for voice 1 2 and 3 * INIT LDX #01 ;Start all durations at 1 STX DURATION STX DURATION+1 STX DURATION+2 DEX STX STOPME STX STOPME+1 STX STOPME+2 TXA LDX #3 ;Reset macros :L2 STA MACBYTE1,X STA MACBYTE2,X STA MACBYTE3,X DEX BPL :L2 STA SID+4 ;Clear gate bits STA SID+$0B STA SID+$12 STA STACKP LDA #8 STA STACKP+1 LDA #16 STA STACKP+2 DO 0 ;Not necessary for lite routine LDX #1 STX V1FIELD INX STX V2FIELD INX STX V3FIELD FIN ;----------------------- LDA FIELDS ;Initialize the field pointers LDX FIELDS+1 ;voice 1 JSR V1SETP LDA FIELDS+2 ;Voice 2 = field 2 LDX FIELDS+3 JSR V2SETP LDA FIELDS+4 LDX FIELDS+5 JSR V3SETP JMP VOICE1 ;Continue into player, skipping ;global macros. SETV ;Set volume LDA SHADOW+24 AND #$F0 STA SHADOW+24 TXA ORA SHADOW+24 STA SHADOW+24 STA SID+24 RTS REP ;Repeat begin LDY CURFIELD TXA STA REPTIMES-1,Y ;Number of times to repeat JSR GETP ;Get current pointer * GETP leaves Y unchanged * LDY CURFIELD ;returns (A X) = (lo hi) STA REPLO-1,Y ;Low byte TXA STA REPHI-1,Y ;Hi byte SNEAKY RTS ENDR ;Repeat LDX CURFIELD DEC REPTIMES-1,X BEQ SNEAKY LDY CURFIELD LDA REPLO-1,Y ;Restore index LDX REPHI-1,Y JMP SETP ;Set current voice to (A X 0) ACTG ;Activate global macro LDA BITP,X ;Get the bit! LDY DIV8T7,X ;x/8*7, gives MACBYTE1 2 or 3 ORA MACBYTE1,Y ;automatically STA MACBYTE1,Y ;Global=voice 0 TXA ;Reset entry address ASL TAY LDA MACADR,Y STA MACLO,X LDA MACADR+1,Y STA MACHI,X RTS DACTG ;Deactivate LDA BITP,X EOR #$FF LDY DIV8T7,X AND MACBYTE1,Y STA MACBYTE1,Y RTS ACTL ;Activate local macro LDA DIV8T7,X ;Macro byte offset CLC ADC CURFIELD ;offset further by current voice TAY LDA BITP,X ORA MACBYTE1,Y STA MACBYTE1,Y RTS DACTL LDA DIV8T7,X CLC ADC CURFIELD TAY LDA BITP,X EOR #$FF AND MACBYTE1,Y STA MACBYTE1,Y RTS STO ;Stop voice LDX CURFIELD STA STOPME-1,X ;And that's the end of that! RTS GON ;Turn gate on LDX COFFSET LDA SHADOW+4,X ORA #$01 STA SHADOW+4,X STA SID+4,X RTS GOFF ;Turn gate off LDX COFFSET LDA SHADOW+4,X AND #$FE STA SHADOW+4,X STA SID+4,X RTS JMPMARK ;X contains marker # index LDA MARKERS,X PHA LDA MARKERS+1,X TAX PLA JMP SETP JSRMARK LDY CURFIELD ;1 2 or 3 LDA STACKP-1,Y CLC ADC #2 STA STACKP-1,Y TAY LDA MARKERS,X PHA LDA MARKERS+1,X PHA JSR GETP STA STACK-1,Y ;lo TXA STA STACK-2,Y ;hi PLA ;Destination hi TAX PLA JMP SETP RTSMARK LDY CURFIELD LDA STACKP-1,Y SEC SBC #2 STA STACKP-1,Y TAY LDA STACK,Y TAX ;High byte LDA STACK+1,Y JMP SETP * * Load instrument routine * * On entry: X contains the instrument to be loaded * * On exit: Y is preserved * TIMES9 DFB 0,9,18,27,36,45,54,63,72,81,90,99,108,117 LOADINST STY TEMPY LDY CURVOICE ;1 2 or 3 TXA STA CURINST-1,Y LDA TIMES9,X TAX LDY COFFSET ;Current voice offset LDA INSTAB,X ;Pulse lo STA SHADOW+2,Y STA SID+2,Y LDA INSTAB+1,X ;Pulse hi STA SHADOW+3,Y STA SID+3,Y LDA INSTAB+2,X ;Control register STA SHADOW+4,Y STA SID+4,Y LDA INSTAB+3,X ;Attack/decay STA SHADOW+5,Y STA SID+5,Y LDA INSTAB+4,X ;Sustain/release STA SHADOW+6,Y STA SID+6,Y LDY CURVOICE ;1 2 or 3 LDA INSTAB+8,X ;Filter mode, 0 means no filter BEQ :NOFILTER ;Zero means no filter LDA SHADOW+$18 ;Volume, filter mode AND #$0F ORA INSTAB+8,X STA SHADOW+$18 STA SID+$18 LDA SHADOW+$17 ;Next up: resonance AND #$0F ;Other voices may be filtered ORA BITP-1,Y ORA INSTAB+7,X ;Grab resonance STA SHADOW+$17 STA SID+$17 LDA INSTAB+6,X ;Cutoff hi STA SHADOW+$16 STA SID+$16 LDA INSTAB+5,X ;Cutoff lo STA SHADOW+$15 STA SID+$15 LDY TEMPY RTS :NOFILTER ;Don't filter this voice LDA BITP-1,Y EOR #$FF ;Set bit to 0 AND SHADOW+$17 STA SHADOW+$17 STA SID+$17 LDY TEMPY RTS * * These routines will either fetch or set the current * data (field) pointers for voice1, voice2, and voice3. * Data is passed in/out via (A X) = (lo hi) * * The index is added into the pointer. * GETP ;Figure out which one to call LDA CURVOICE CMP #2 BEQ V2GETP BPL V3GETP V1GETP LDX V1POINT+5 LDA FOFFSET CLC ADC V1POINT+4 BCC :RTS INX :RTS RTS V2GETP LDX V2POINT+5 LDA FOFFSET+1 CLC ADC V2POINT+4 BCC :RTS INX :RTS RTS V3GETP LDX V3POINT+5 LDA FOFFSET+2 CLC ADC V3POINT+4 BCC :RTS INX :RTS RTS SETP ;Again figure which to use PHA LDA CURVOICE CMP #2 BEQ V2SETPLA ;Special branch BPL V3SETPLA V1SETPLA PLA V1SETP STA V1POINT+4 STX V1POINT+5 STA V1POINT+8 STX V1POINT+9 LDA #00 STA FOFFSET RTS V2SETPLA PLA V2SETP STA V2POINT+4 STX V2POINT+5 STA V2POINT+8 STX V2POINT+9 LDA #00 STA FOFFSET+1 RTS V3SETPLA PLA V3SETP STA V3POINT+4 STX V3POINT+5 STA V3POINT+8 STX V3POINT+9 LDA #00 STA FOFFSET+2 RTS * * Main interrupt routine: * If Shadow SID active, then copy it over. * DEC duration * If zero then read in new data * Process macros * Repeat for other voices * MAINPLAY * LDA COPYSHAD ;Zero means active * BNE GLOBMAC * LDX #24 *:L1 LDA SHADOW,X * STA SID,X * DEX * BPL :L1 GLOBMAC ;Take care of global macros LDX #00 ;1-6 are normal fields, 0 global STX CURFIELD STX LOCOFF ;Local variable offset >>> OLDMAC,0 VOICE1 LDA #08 STA LOCOFF LDX #00 STX COFFSET ;SID offset INX STX CURVOICE ;Current voice * LDX V1FIELD ;Current field voice 1 STX CURFIELD ;Current field * LDY TIMES24,X ;x*24 gives the field offset ;(because there are 24 macros!) ;for macro addresses. LDA STOPME BNE JV2 DEC DURATION-1,X BEQ V1LOOP NOUPD81 >>> OLDMAC,1 ;Otherwise deal with macros. JV2 JMP VOICE2 V1POINT V1LOOP LDY FOFFSET ;v1offset, v2offset, etc. ;An offset into the data; less ;self-modifying code this way ;(i.e. more saved cycles) :FP1 LDX $1000,Y ;First byte: note or data INY :FP2 LDA $1000,Y ;Second byte: inst or duration INY STY FOFFSET BNE :CONT1 INC :FP1+2 ;Increment the high bytes INC :FP2+2 ;Simple! :CONT1 TAY ;Y is now index for LNOTE BPL :LNOTE ;High bit set means instruction JSR PROCDATA LDA STOPME BEQ V1LOOP JMP VOICE2 :HOLD LDX CURFIELD JMP NOUPD81 :LNOTE ;Otherwise, load frequency and ;duration data ;X is note number i.e. freq index ;A is duration index ;Store in SID as well as Shadow SID LDA DURTAB,Y STA DURATION ;New duration CPX #HOLD ;Slur current note? BEQ :HOLD ;If so, then do it! STX LASTNOTE ;Last note read for v1, etc. LDA FREQLO,X STA SHADOW STA SID LDA FREQHI,X STA SHADOW+1 STA SID+1 LDX CURFIELD >>> NEWMAC,1 ;Restart macros VOICE2 LDX CURVOICE INX STX CURVOICE STX CURFIELD LDA STOPME+1 BNE JV3 LDA #16 STA LOCOFF LDA #07 STA COFFSET * LDX V2FIELD * STX CURFIELD * LDA TIMES24,X * TAY DEC DURATION-1,X BEQ V2LOOP NOUPD82 >>> OLDMAC,2 ;Otherwise deal with macros. JV3 JMP VOICE3 V2POINT V2LOOP LDY FOFFSET+1 ;v1offset, v2offset, etc. ;An offset into the data; less ;self-modifying code this way ;(i.e. more saved cycles) :FP1 LDX $1000,Y ;First byte: inst or duration INY :FP2 LDA $1000,Y ;Second byte: note or data INY STY FOFFSET+1 BNE :CONT1 INC :FP1+2 ;Increment the high bytes INC :FP2+2 ;Simple! :CONT1 TAY BPL :LNOTE ;High bit set means instruction JSR PROCDATA LDA STOPME+1 BEQ V2LOOP JMP VOICE3 :HOLD LDX CURFIELD JMP NOUPD82 :LNOTE ;Otherwise, load frequency and ;duration data ;X is note number i.e. freq index ;A is duration index ;Store in SID as well as Shadow SID LDA DURTAB,Y STA DURATION+1 ;New duration CPX #HOLD ;Slur current note? BEQ :HOLD ;If so, then do it! STX LASTNOTE+1 ;Last note read for v1, etc. LDA FREQLO,X STA SHADOW+7 STA SID+7 LDA FREQHI,X STA SHADOW+8 STA SID+8 LDX CURFIELD >>> NEWMAC,2 ;Restart macros VOICE3 LDA STOPME+2 BNE DONERTS LDX CURVOICE INX STX CURVOICE STX CURFIELD LDA #24 STA LOCOFF LDA #14 STA COFFSET * LDX V3FIELD * STX CURFIELD * LDA TIMES24,X * TAY DEC DURATION-1,X BEQ V3LOOP NOUPD83 >>> OLDMAC,3 ;Otherwise deal with macros. DONERTS RTS V3POINT V3LOOP LDY FOFFSET+2 ;v1offset, v2offset, etc. ;An offset into the data; less ;self-modifying code this way ;(i.e. more saved cycles) :FP1 LDX $1000,Y ;First byte: inst or duration INY :FP2 LDA $1000,Y ;Second byte: note or data INY STY FOFFSET+2 BNE :CONT1 INC :FP1+2 ;Increment the high bytes INC :FP2+2 ;Simple! :CONT1 TAY BPL :LNOTE ;High bit set means instruction JSR PROCDATA LDA STOPME+2 BEQ V3LOOP RTS :HOLD LDX CURFIELD JMP NOUPD83 :LNOTE ;Otherwise, load frequency and ;duration data ;X is note number i.e. freq index ;A is duration index ;Store in SID as well as Shadow SID LDA DURTAB,Y STA DURATION+2 ;New duration CPX #HOLD ;Slur current note? BEQ :HOLD ;If so, then do it! STX LASTNOTE+2 ;Last note read for v1, etc. LDA FREQLO,X STA SHADOW+14 STA SID+14 LDA FREQHI,X STA SHADOW+15 STA SID+15 LDX CURFIELD >>> NEWMAC,3 ;Restart macros RTS ;All done! * * The macro caller. There are two entry points, * INITMAC and CALLMAC. INITMAC will enter a macro * at the beginning. CALLMAC will enter a macro * where it last left off. * * On entry, Y is assume to contain the correct field * offset position in the macro address lookup table. * (i.e. tells which macro we are on, and in which field) * * In all cases A and Y are preserved. The last line is * PLA, thus the zero flag is set appropriately on exit. * INITMAC PHA LDX CURFIELD TYA ;We need to strip out the field SEC ;offset, i.e. subtract 24*n SBC TIMES24,X ASL ;Multiply by two because of how ;initial address table is set up. STA :ADDR+1 ;Store the correct location :ADDR JMP (MACADR) ;MACADR = Initial address, (lo,hi) CALLMAC PHA LDA MACHI,Y ;MACLO etc. contain last exit PHA ;locations. LDA MACLO,Y PHA RTS ;Neat, huh? MACBEGIN * * Now some simple song data -- just a scale. * FIELD1 DFB RESTART,RESTART FIELD2 DFB STOP,STOP FIELD3 DFB STOP,STOP FIELDEND