Thursday, December 17, 2015

Day 9, Towards Multichannel Sid Programming (Part 2)

Yesterday I was going to find how to write this program:

    GOSUB SET-UP-SID
# we set up the continue variables here
# we want to have them visible in the main loop
    CO% = 1 #
# we can also initialize further variables here
    [...]
LOOP: 
# this is how we can emulate a while-true cycle in basic
    IF CO% = 0 GOTO END-LOOP 
# now we will read the notes and upload them to the correct channel
    [...]
# now we will wait 1/4 of a second
    [...]
# and after it we jump back
    GOTO LOOP
END-LOOP: 
    GOSUB RESET-SID
    END

SET-UP-SID:
# rem here we will set up the channels with the correct instruments
    # We already did this
    RETURN

RESET-SID:
# rem here we will clear the SID
    # We already did this
    RETURN

And now I am going to continue it.

Please note that this formalism has nothing to do with how someone else would approach compuer programming. In the good old Basic Days planning the software was done using Flow charts. Nowadays Flow charts aren't neither taught nor used, and someone expects you to just fill random methods provided by your framework or choice in random points of the application.

How do we wait for one second?

I'm choosing to tackle this problem next because I need to have a sort of slowdown in place before reading the notes and putting them into the SID registers. The Commodore 64 basic gives two commands to read the time: TIME and TIME$. Due to architectural limitations they will give the time from the last system boot (including when you soft-reset the computer with SYS 64738). In addition to it the TIME is stopped when the computer is serializing data. As a last limitation I can think is that TIME is given pace from the current video signal coming out from the Commodore (an NTSC commodore will have a different timing from a PAL commodore, but I have to check).

The difference between the two instructions is that TIME$ will print the time as seconds and it should be text while TIME will print the time as 60th of seconds and will be a number.

We can type this program in and see the two commands in action.

10 FOR I = 1 TO 24
20 PRINT TIME ; TIME$
30 REM WE WANT A SLOWDOWN BEFORE
31 REM PRINTING THE NEXT VALUE OF TIME
35 FOR J = 1 TO 100 : NEXT J
40 NEXT I

Armed with this insight we can speculate that we can store the current time in a variable, test if 60 jiffies have passed and print the new time.

10 PRINT "{CLR/HOME}" ; TIME$ , TIME
20 TT = TIME + 60
30 IF TT =< TIME GOTO 10
40 REM IF YOU PRESS ANY KEY THE PROGRAM WILL END
41 GET A$ : IF A$ <> "" THEN END
50 GOTO 30

This is not perfect but for now it will do.

You can see that we have to change the central loop

# since this is a value that does not change we calculate it 
# here, just once
    DT = INT(60 / 4)
LOOP:
    TT = TIME + DT
# this is how we can emulate a while-true cycle in basic
    IF CO% = 0 GOTO END-LOOP 
# now we will read the notes and upload them to the correct 
# channel
    [...]
# now we will wait 1/4 of a second
# and after it we jump back
CHECK-TIME:
    IF TT =< TIME GOTO LOOP
    GOTO CHECK-TIME

This does not complete all the reasoning about time because now we will have to test if the time has come for reading our notes in the SID.

Reading and playing the note data.

The following part is tricky, because we still need to do the following things: finalize the data structure, understand about which channel we are reading the following note, and understand

Reading the documentation for the SID we see that for playing a single note we need to insert two values for each channel, one for the LOW frequency and the other for the HIGH frequency. The two frequencies will be combined into a single note.

We also decide that the delay, for now, will be "quarters of second", so when the program jumps to loop we will need to check if the delay has stopped. Remember also that we want to test if the channel is still active.

So we need to change this representation:

[note-1-pitch],[note-1-delay]

to this one:

[note-1-low],[note-1-high],[note-1-delay]

And when we read the data we will just do

READ LO
READ HI
READ DL

But before reading it we have to decide if we need to read a note, and after reading that note we need to decide when we have to read it next.

Each channel will have a timer. Each will be initialized, before entering the loop, with the current time.

T1 = TIME

If T1 is lower or equals to the current TIME then we will make the program read its values

    IF T1 > TIME SKIP
    READ LO # read the low frequency
    READ HI # read the high frequency
    READ DL # read the delay.
    POKE L1,LO # we put in the Sid 1 low register the value read by low
    POKE H1,HL # same but with the high value
    T1 = TT + DT * (DL - 1) # this is when we will read the next note.
SKIP:
    # we skipped here and here we will test channel 2

    [...] 

What does it happen if we want to shut off the channel? Or if the channel was shut off before?
We use another variable: C1, that we will initialize to 1 at the beginning of the program. So our micro-block becomes:

    IF C1 = 0 SKIP
    IF T1 > TIME SKIP
    READ LO # read the low frequency
    READ HI # read the high frequency
    READ DL # read the delay.
    IF DL = -1 THEN C1 = 0 

    IF DL = -1 THEN SKIP
    POKE L1,LO # we put in the Sid 1 low register the value read by low
    POKE H1,HL # same but with the high value
    T1 = TT + DT * (DL - 1) # this is when we will read the next note.
SKIP:
    [...] #we skipped here and here we will test for C2 and C3


Now we could copy the block and do the same things again

    IF C1 = 0 SKIP_1
    IF T1 > TIME SKIP_1
    READ LO
    READ HI
    READ DL
    IF DL = -1 THEN C1 = 0 

    IF DL = -1 THEN SKIP_1
    POKE L1,LO
    POKE H1,HL
    T1 = TT + DT * (DL - 1)
SKIP_1:
    IF C2 = 0 SKIP_2
    IF T2 > TIME SKIP_2
    READ LO
    READ HI
    READ DL
    IF DL = -1 THEN C2 = 0 

    IF DL = -1 THEN SKIP_2
    POKE L2,LO
    POKE H2,HL
    T2 = TT + DT * (DL - 1)
SKIP_2:
    IF C3 = 0 SKIP_3
    IF T3 > TIME SKIP_3
    READ LO
    READ HI
    READ DL
    IF DL = -1 THEN C3 = 0 

    IF DL = -1 THEN SKIP_3
    POKE L3,LO
    POKE H3,HL
    T3 = TT + DT * (DL - 1)
SKIP_3:


However as you see this wall of text is unreadable, and as such it's prone to inserting errors. It's better to have a subroutine to handle the common parts of this block of code.

    # channel 1
    CS = C1 # status of the channel
    TS = T1 # time of the channel

    LT = L1 # low frequency register of the channel
    HT = H1 # high frequency register of the channel
    GOSUB COMMON_LOADER
    C1 = CS
    T1 = TS

    # channel 2

    CS = C2
    TS = T2

    LT = L2
    HT = H1
    GOSUB COMMON_LOADER
    C2 = CS

    T2 = TS
    # channel 3
    CS = C3
    TS = T3

    LT = L3 
    HT = H3
    GOSUB COMMON_LOADER
    C3 = CS
    T3 = TS

    [..]

COMMON_LOADER:
    IF CS = 0 SKIP
    IF TS > TIME SKIP
    READ LO 
    READ HI 
    READ DL #note that we set the "off" channel in CS
    IF DL = -1 THEN CS = 0 
    IF DL = -1 THEN SKIP 
# the values of LT and HT will be L1 and H1 
# in the first channel, L2, H2 and L3, H3 
# in the other two
    POKE LT,LO 
# we put in the Sid 1 low register the value read by low
    POKE HT,HL 
# same but with the high value
    TS = TT + DT * (DL - 1) # this is when we will read the next note.
SKIP:
    RETURN

Tomorrow I will type in the program.

No comments:

Post a Comment