' makdsnd.gfa  12 July 1991 modified 5 July 1992
' This program converts a *.tun file into a file of data which
' can be run by dosound (~xbios(32,L:...)). Pressing any key on
' the keyboard will interrupt the conversion and cause the data to
' be written out.  The name of the output file is  sound.dat
' programmed by Seymour Shlien in GFA Basic 3.5 on my 1040STE
' 624 Courtenay Avenue / Ottawa, Ontario, Canada  K2A 3B5
' The program is public domain and not for commercial use.
DIM notes_to_microbeats%(12) ! duration code (1 - 12) to microbeat value
DIM elapsed_microbeats%(3) !number of microbeats for each voice (bar lines)
DIM seq%(3) !note sequence number in each voice during play
DIM micro_beat%(3) ! accumulated microbeats for voice (for play)
DIM next_event%(3) ! time for next note to sound for voice (for play)
DIM play_stop%(3) ! kounter for stop playing -used in cue
DIM note_tick%(3) ! time a note was sounded. Used for envelope
DIM tone_envel%(300) ! loudness envelope 1 to 100 for each voice
DIM voice_end_flag%(3) ! flag to signal end of voice
DIM note%(3),duration%(3),draw_flag%(3) !last notes sounded
DIM kount%(2),vclen%(2),repeat_sign%(2),repeat_count%(2)
REM memory for storing music
DIM tnote&(2,1000),mubeat%(3)
DIM last_note_duration%(3) !need it if we correct an error with right button
DIM music_mem&(40000)
DIM music_cmp&(5000)
DIM volum(3),decay(3)
DEFMOUSE 0
CLS
voic%=1
voic1%=voic%-1
volum(0)=14
volum(1)=13
volum(2)=12
decay(0)=0.08
decay(1)=0.09
decay(2)=0.13
PRINT "This program converts a *.tun file"
PRINT "into an *.XBS file. Select the"
PRINT "desired file using the file"
PRINT "selector. The program converts this"
PRINT "file as it is playing it on the"
PRINT "Atari sound chip. It stops when it"
PRINT "plays the entire file or when"
PRINT "you hit the space bar. It then"
PRINT "records everything it played on"
PRINT "the file sound.xbs."
PRINT "... Hit space bar to continue."
DO UNTIL key_interrupt$<>""
  key_interrupt$=INKEY$
LOOP
FOR i%=0 TO 2
  offset=volum(i%)
  attenuation=decay(i%)
  @make_tone_envelope(i%,offset,attenuation)
NEXT i%
@read_music_data
@initialize_note_table
@read_tune_file
@play_tune
' @dosound
@write_sound
> PROCEDURE tnote_structure_doc
  ' tnote array stores the pitch and duration value of every note
  ' to be played for the three voices (tracks). The high byte contains
  ' the duration code and other special codes. The low byte contains
  ' the pitch code. The pitch code modulo 12 maps into the note letter
  ' c,c#,d,d#,... b. If the pitch code is zero then a rest is assumed.
  ' The duration code 0 to 12 map to note duration in the order that
  ' the note duration sprites appear in the menu. Other codes 13 to 15
  ' control repeats. Codes 16 and 17 are not fully implemented but allow
  ' the inclusion of long rests in a particular voice. Other codes are
  ' only used for the input /output files *.TUN for specifying key signature,
  ' time signature, and tempo.
RETURN
> PROCEDURE read_music_data
  @read_timconv_array
RETURN
> PROCEDURE read_timconv_array
  LOCAL i%
  REM for converting notes to microbeats
  FOR i%=0 TO 12
    READ notes_to_microbeats%(i%)
  NEXT i%
  DATA 3,6,12,18,24,36,48,72,96,144,2,4,8
  ' whole note = 96 microbeats
RETURN
'
'
'
> PROCEDURE yamaha_doc
  ' I was not very successful using the sound or wave commands in gfa
  ' basic to produce polyphonic music. (Its ok for chords). Furthermore
  ' the envelope feature on the Yamaha chip produced problems that I could
  ' not resolve. I therefore implemented the sound at a fairly low level
  ' using BIOS commands. The following functions interface with the xbios
  ' command 28. This is tricky since some of the registers are used for
  ' disk i/o and damage to the disk can result. I put in extra protection
  ' in this code which should never be commented out.
  ' I decided to implement the music production this way rather than
  ' using the xbios 32 play sequence command so that I can follow the
  ' production of each note. Even though I am controlling the amplitude
  ' envelope every 50 th of a second, as well as placing sprites on the
  ' screen, there is ample time left over.
  ' The make_tone_envelope function controls the linear rise and two
  ' linear decay modes of the amplitude. The envelope is put into a table.
  '
  ' The procedure play_next_notes, polls all three voices to see whether
  ' it is time to change the volume or pitch of a note. When it is
  ' time to change it finds the next note and sets the time to change
  ' that note based on the notes duration and the slowness of the music.
RETURN
> PROCEDURE initialize_note_table
  ' Thanks ken
  LOCAL note|
  IF NOT u__init!
    u__init!=TRUE
    DIM u__period%(96)
    FOR note|=0 TO 12
      FOR o%=0 TO 7
        u__period%(12*o%+note|)=125000/(2^o%*440*(2^(note|/12))/(2^(10/12))/16)+0.5
      NEXT o%
    NEXT note|
  ENDIF
RETURN
> PROCEDURE set_tone_period(voice%,period%)
  LOCAL reg%,period_low%,period_hi%,reg_val%
  IF voice%<0 OR voice%>2
    GOTO set_tone_err
  ENDIF
  reg%=voice%*2
  period_low%=AND(period%,255)
  period_hi%=SHR(AND(period%,&HF00),8)
  period%=XBIOS(28,period_low%,reg%+128) !period% is not used anymore
  music_mem&(memptr%)=SHL(reg%,8)+period_low%
  INC memptr%
  reg%=reg%+1
  reg_val%=AND(XBIOS(28,0,reg%),&HF0) !must save hi bits
  reg_val%=OR(AND(period_hi%,15),reg_val%) !combine low and hi bits
  reg_val%=XBIOS(28,reg_val%,reg%+128) !output
  music_mem&(memptr%)=SHL(reg%,8)+reg_val%
  INC memptr%
  GOTO set_tone_return
set_tone_err:
  STOP
set_tone_return:
RETURN
> PROCEDURE disable_noise_channels
  LOCAL reg_val%
  reg_val%=XBIOS(28,0,7)
  reg_val%=OR(AND(reg_val%,192),56)
  reg_val=XBIOS(28,reg_val%,7+128)
  music_mem&(memptr%)=SHL(7,8)+reg_val%
  INC memptr%
RETURN
> PROCEDURE set_volume(voice%,level%)
  ' level between 0 and 15 for constant volume
  ' level 16 for waveform envelope
  LOCAL reg%,reg_val%
  IF voice%<0 OR voice>2 !check for legal voice number
    GOTO set_volume_err
  ENDIF
  IF level%>31 OR level%<0
    GOTO set_volume_err
  ENDIF
  reg%=8+voice%
  regval%=XBIOS(28,0,reg%)
  regval%=AND(regval%,&HE0) !save only top 3 bits
  regval%=OR(regval%,AND(level%,31))
  regval%=XBIOS(28,regval%,reg%+128)
  music_mem&(memptr%)=SHL&(reg%,8)+regval%
  INC memptr%
  GOTO set_volume_return
set_volume_err:
  STOP
set_volume_return:
RETURN
> PROCEDURE dosound
  LOCAL i%
  '  SPOKE &H484,PEEK(&H484) AND NOT 1
  addr%=VARPTR(music_mem&(0))
  PRINT addr%
  FOR i%=0 TO 200
    PRINT BYTE{addr%+i%};" ";
  NEXT i%
  ~XBIOS(32,L:VARPTR(music_mem&(0)))
RETURN
> PROCEDURE write_sound
  LOCAL addr%
  addr%=VARPTR(music_mem&(0))
  BSAVE "sound.xbs",addr%,memptr%*2
RETURN
> PROCEDURE make_tone_envelope(num%,offset,attenuation)
  LOCAL index%,val%,i%
  index%=num%*100
  FOR i%=0 TO 99
    val%=offset-i%*attenuation
    val%=MAX(val%,0)
    val%=MIN(val%,15)
    tone_envel%(i%+index%)=val%
  NEXT i%
RETURN
> PROCEDURE play_tune
  ' The procedure plays song number numb%.
  ' The base_time% is set to the current time in ticks.
  ' The function next_note is called repeatedly, to sound the next
  ' note when it is time to do so. If a note has been sounded, we
  ' draw it on the staff.
  LOCAL i%,note_xposition%,k%
  skoll%=0 ! rem scrolling is off
  ' find voices
  IF speed%>0
    slowness=135/speed% !comment this statement when debugging
  ELSE
    slowness=2
  ENDIF
  base_time%=TIMER
  FOR i%=1 TO 3
    seq%(i%)=0
    repeat_count%(i%-1)=0
    repeat_sign%(i%-1)=0
    play_stop%(i%-1)=vclen%(i%-1)
    elapsed_microbeats%(i%-1)=0
    micro_beat%(i%)=0
    next_bar_line%=bar_length%
    next_event%(i%)=base_time%+slowness*micro_beat%(i%)
    IF seq%(i%)<=play_stop%(i%-1)
      voice_end_flag%(i%)=0
    ELSE
      voice_end_flag%(i%)=1
    ENDIF
    @set_volume(i%-1,10)
    @set_tone_period(i%-1,5)
  NEXT i%
  memptr%=0
  k%=0
  '
  ' ready to start
  @disable_noise_channels
  REPEAT
    @play_next_notes
    ' the rest of this code is used for drawing the notes on the staff
    ' when necessary.
    PAUSE 1 ! we have lots of time to spare
    music_mem&(memptr%)=SHL&(255,8)+1
    ' TO GET THE RIGHT SPEED DON'T INCREMENT EVERY TIME. don't know why.
    IF MOD(k%,5)<>1
      INC memptr%
    ENDIF
    INC k%
    i%=voice_end_flag%(1)+voice_end_flag%(2)+voice_end_flag%(3)
    key_interrupt$=INKEY$
  UNTIL i%=3 OR key_interrupt$<>""
  FOR i%=0 TO 2
    set_volume(i%,0)
    IF seq%(i%+1)<=vclen%(i%)
      kount%(i%)=seq%(i%+1) !play_next_notes overshoots
    ELSE
      kount%(i%)=vclen%(i%)
    ENDIF
  NEXT i%
  tlen%=duration%(voic%)
  music_mem&(memptr%)=SHL&(255,8)
  INC memptr%
  PRINT "Recording ";memptr%;" words on sound.xbs file";
RETURN
> PROCEDURE play_next_notes
  LOCAL i%,j%
  ' this procedure sequences through the notes in the tnote array and
  ' sounds them on the Yamaha chip using the xbios function 28. The array
  ' voice_end_flag indicates whether a particular voice has finished.
  ' The next_event% array indicates the next time tick to process the
  ' next note for the particular voice. The tick number was determined
  ' from the previous note duration (and slowness parameter).
  ' If the note was sounded, we store the key and duration for that voice
  ' in the arrays note% and duration%. We set the draw_flag to tell
  ' the procedure play_tune that we have a note to be displayed on the
  ' staff.
  ' this code applies the envelope function to the amplitude.
  t%=TIMER
  FOR i%=1 TO 3
    index%=SHR(t%,2)-note_tick%(i%)
    IF index%>0 AND index%<100
      j%=i%-1
      set_volume(j%,tone_envel%(index%+j%*100))
    ENDIF
  NEXT i%
  '
  FOR i%=1 TO 3 !check if it is time to sound next note for all voices
    IF voice_end_flag%(i%)<1
      IF t%>=next_event%(i%)
        tlen%=-1
        ' find the next note of finite duration and bypass any control
        ' codes
        DO UNTIL tlen%>=0 AND tlen%<13 OR seq%(i%)>play_stop%(i%-1) OR tlen%>14
          seq%(i%)=seq%(i%)+1
          ne%=tnote&(i%-1,seq%(i%))
          IF seq%(i%)>play_stop%(i%-1)
            voice_end_flag%(i%)=1
            set_volume(i%-1,0)
            tlen%=0
          ELSE
            tlen%=SHR(ne%,8)
          ENDIF
          IF tlen%=13
            repeat_sign%(i%-1)=seq%(i%)
            repeat_count%(i%-1)=0
          ENDIF
          IF tlen%=14 AND repeat_count%(i%-1)<1
            seq%(i%)=repeat_sign%(i%-1)
            INC repeat_count%(i%-1)
          ENDIF
          IF tlen%=16
            ' necessary to resync using a variable size rest
            low_byte%=ne% MOD 256
            elapsed_microbeats%(i%-1)=elapsed_microbeats%(i%-1)+low_byte%*96
            micro_beat%(i%)=micro_beat%(i%)+low_byte%*96
          ENDIF
          IF tlen%=17
            ' necessary to resync using a variable size rest
            low_byte%=ne% MOD 256
            elapsed_microbeats%(i%-1)=elapsed_microbeats%(i%-1)+low_byte%
            micro_beat%(i%)=micro_beat%(i%)+low_byte%
          ENDIF
        LOOP
        IF tlen%=15 AND repeat_count%(i%-1)>0
          skip_to_repeat_sign(i%)
        ENDIF
        IF seq%(i%)>1000
          voice_end_flag%(i%)=1
        ENDIF
        IF voice_end_flag%(i%)=1
          GOTO play_next_voice
        ENDIF
        IF tlen%>13
          next_event%(i%)=base_time%+micro_beat%(i%)*slowness
          set_volume(i%-1,0)
          note_tick%(i%)=0
          GOTO play_next_voice
        ENDIF
        nte%=ne% MOD 256 !get and sound note
        IF nte%<96 AND nte%>5
          set_tone_period(i%-1,u__period%(nte%))
          note_tick%(i%)=SHR(t%,2)
        ENDIF
        ' set time to end note
        micro_beat%(i%)=micro_beat%(i%)+notes_to_microbeats%(tlen%)
        next_event%(i%)=base_time%+micro_beat%(i%)*slowness
      ENDIF
    ENDIF
  play_next_voice:
  NEXT i% ! next voice
RETURN
> PROCEDURE skip_to_repeat_sign(i%)
  LOCAL done%
  done%=0
  DO UNTIL voice_end_flag%(i%-1)=1 OR done%=1
    INC seq%(i%)
    ne%=tnote&(i%-1,seq%(i%))
    IF seq%(i%)>play_stop%(i%-1)
      voice_end_flag%(i%)=1
      set_volume(i%-1,0)
      tlen%=0
    ELSE
      tlen%=SHR(ne%,8)
    ENDIF
    IF tlen%=14
      done%=1
    ENDIF
  LOOP
RETURN
'
> PROCEDURE read_tune_file
  DEFFILL 0
  PBOX 400,0,639,30
  DEFFILL 1
  ' PRINT "Enter input file :";
  ' @tune_file_selector
  FILESELECT #"input file","*.tun","noname.tun",tun$
  PRINT "Input file = ";tun$
  IF EXIST(tun$)=-1
    OPEN "i",#1,tun$
    FOR i%=0 TO 2
      vclen%(i%)=0
      kount%(i%)=0
    NEXT i%
    i%=0
    l%=0
    FOR k%=1 TO LOF(#1)/2
      s$=INPUT$(1,#1)
      nlen%=ASC(s$)
      s$=INPUT$(1,#1)
      tnte%=ASC(s$)
      nc%=nlen%*256+tnte%
      IF nlen%>17
        IF nlen%=32
          kount%(i%)=l%
          vclen%(i%)=l%
          l%=0
          i%=i%+1
        ENDIF
        IF nlen%=36
          speed%=tnte%
        ENDIF
        IF nlen%=37
          volum(0)=tnte% MOD 16
          decay(0)=(tnte% DIV 16)/25
          @make_tone_envelope(0,volum(0),decay(0))
        ENDIF
        IF nlen%=38
          volum(1)=tnte% MOD 16
          decay(1)=(tnte% DIV 16)/25
          @make_tone_envelope(1,volum(1),decay(1))
        ENDIF
        IF nlen%=39
          volum(2)=tnte% MOD 16
          decay(2)=(tnte% DIV 16)/25
          @make_tone_envelope(2,volum(2),decay(2))
        ENDIF
      ELSE
        l%=l%+1
        tnote&(i%,l%)=nc%
      ENDIF
    NEXT k%
    CLOSE #1
    PRINT k%;" notes read"
    vclen%(voic1%)=kount%(voic1%)
  ENDIF
read_tune_return:
RETURN
> PROCEDURE assert_tun_extension
  k%=LEN(tun$)
  IF k%>12
    tun$=LEFT$(tun$,12)
  ENDIF
  k%=INSTR(tun$,".")
  IF (k%>0)
    tun$=LEFT$(tun$,k%-1)
  ENDIF
  tun$=tun$+".TUN"
RETURN
