399 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			399 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# gtmidi
 | 
						|
Takes the bin output from Miditones https://github.com/LenShustek/miditones and generates source<br/>
 | 
						|
code data that you can include in your projects for MIDI scores.<br/>
 | 
						|
 | 
						|
## Building
 | 
						|
- CMake 3.7 or higher is required for building, has been tested on Windows with Visual Studio and gcc/mingw32<br/>
 | 
						|
  and also built and tested under Linux.<br/>
 | 
						|
- A C++ compiler that supports modern STL.<br/>
 | 
						|
 | 
						|
## Usage
 | 
						|
~~~
 | 
						|
gtmidi <input filename> <output filename> <midi name> <format 0, 1, 2, 3> <start address in hex>
 | 
						|
       <segment offset in hex> <int segment size> <int line length> <float timing adjust>
 | 
						|
~~~
 | 
						|
 | 
						|
## Format
 | 
						|
~~~
 | 
						|
Format: 0 = vCPU ASM, 1 = GCL, 2 = C/C++, 3 = Python
 | 
						|
~~~
 | 
						|
 | 
						|
## Start Address
 | 
						|
The start address, (**_specified in hex_**), is the address in RAM where the MIDI byte sequence will be loaded.<br/>
 | 
						|
 | 
						|
## Segment Offset
 | 
						|
The segment offset, (**_specified in hex_**), is the offset continually added to the start address to determine<br/>
 | 
						|
the starting location of each new segment.<br/>
 | 
						|
 | 
						|
## Segment Size
 | 
						|
The segment size is the maximum number of bytes contained within each segment.<br/>
 | 
						|
 | 
						|
## Line length
 | 
						|
The line length specifies the maximum length of each line of output source code.<br/>
 | 
						|
 | 
						|
## Timing Adjust
 | 
						|
The timing adjust specifies a delta that attempts to adjust the overall timing to more closely match the original<br/>
 | 
						|
overall timing. Normal values are {0.0 <-> 1.5}, results will vary depending on many factors, experimentation is key.<br/>
 | 
						|
 | 
						|
## Details
 | 
						|
The output format is very similar to the Miditones output format except for a few crucial differences.<br/>
 | 
						|
- The Gigatron's maximum channels, (tone generators), is limited to 4, so you **_must_** use the -t4 option<br/>
 | 
						|
  with Miditones.<br/>
 | 
						|
- The Gigatron does not support volume or instrument changes, so you **_cannot_** use the -i or -v options<br/>
 | 
						|
  with Miditones.<br/>
 | 
						|
- The wait or delay command has been changed from 2 bytes and 1ms resolution to a variable length 1 byte stream<br/>
 | 
						|
  and 16.66667ms resolution; this has some important impacts.<br/>
 | 
						|
   - Very short delays will either be rounded down to 0ms or rounded up to 16.6666667ms, this **_will_** affect<br/>
 | 
						|
     timing, how much of a problem it causes is completely dependent on the MIDI score itself.<br/>
 | 
						|
   - The limit of 2116ms maximum delay has been lifted, gtmidi and the GCL and vASM players both support a<br/>
 | 
						|
     variable length byte stream of delays.<br/>
 | 
						|
   - Sequences of Miditones delays are coalesced into a single delay and then saved as a variable length<br/>
 | 
						|
     byte stream.<br/>
 | 
						|
   - Zero length delays generated by Miditones are ignored.<br/>
 | 
						|
 | 
						|
## Miditones Usage
 | 
						|
~~~
 | 
						|
- miditones -t4 -b <filename>
 | 
						|
  generates a binary file with a maximum of 4 channels
 | 
						|
- miditones -t4 -b -s1 -pi <filename>
 | 
						|
  generates a 4 channel binary, prioritises track1 and disables percussion.
 | 
						|
~~~
 | 
						|
 | 
						|
## Example
 | 
						|
~~~
 | 
						|
- miditones -t4 -b -s1 -pi game_over
 | 
						|
- gtmidi game_over.bin game_over.gcl gameOver 1 0x08A0 0x0100 92 100 0.5
 | 
						|
- This would create a segmented GCL MIDI stream that would be downloaded into the unused areas
 | 
						|
  of video memory, segments are linked together by the 0xD0 Segment command, (see below), and
 | 
						|
  are automatically fetched and played in sequence by the MIDI player on the Gigatron.
 | 
						|
- The above sequence of commands and their parameters after much experimentation are the
 | 
						|
  preferred command lines for producing robust MIDI tracks, obviously subject to variations of
 | 
						|
  original MIDI quality, quality of Miditones conversion, timing errors, missed notes, etc.
 | 
						|
  Experimentation may still be required.
 | 
						|
~~~
 | 
						|
 | 
						|
## MIDI Player
 | 
						|
- There is currently a MIDI player written in vCPU ASM and GCL that implements all the functionality within this<br/>
 | 
						|
  specification.<br/>
 | 
						|
- The MIDI player expects to be called at least once every 16.66666667ms, the simplest way to achieve this, is to wait<br/>
 | 
						|
  for **_VBlank_** and then call **_PlayMidiSync_**; this will work perfectly unless other parts of your code spend more than<br/>
 | 
						|
  one VBlank processing. If this is the case, then these hot spots will need to call **_PlayMidiAsync_** within their<br/>
 | 
						|
  loops or inner code.<br/>
 | 
						|
 | 
						|
**_GCL Player_**
 | 
						|
~~~
 | 
						|
gcl0x
 | 
						|
 | 
						|
[def { ResetAudio -- reset audio hardware }
 | 
						|
  0 midiCommand=
 | 
						|
  0 midiDelay=
 | 
						|
  0 midiNote=
 | 
						|
  0 frameCountPrev=
 | 
						|
  $01FC midiChannel=
 | 
						|
  $8000 midiStreamPtr=
 | 
						|
  $01FA scratch= 
 | 
						|
 | 
						|
  3 ii=
 | 
						|
  [do
 | 
						|
    $FA scratch<.   { reset low byte }
 | 
						|
    $0300 scratch:  { Doke $0300 into wavA, wavX}  
 | 
						|
    scratch<++      { scratch + 0x0002 }
 | 
						|
    scratch<++
 | 
						|
    $0000 scratch:  { Doke $0000 into keyL, keyH }
 | 
						|
    scratch<++      { scratch + 0x0002 }
 | 
						|
    scratch<++
 | 
						|
    scratch:        { Doke vAC into oscL, oscH }
 | 
						|
    scratch>++      { scratch + 0x0100 }
 | 
						|
 | 
						|
  ii 1- ii=
 | 
						|
  if>=0 loop]  
 | 
						|
 | 
						|
  ret
 | 
						|
] ResetAudio=
 | 
						|
 | 
						|
 | 
						|
[def { PlayMidiAsync -- play MIDI stream asynchronous to VBlank; use this one in processing loops that take longer than a VSync period }
 | 
						|
  push
 | 
						|
  [\frameCount, frameCountPrev- 
 | 
						|
    if<>0 PlayMidiSync!
 | 
						|
    \frameCount, frameCountPrev=]         { if frameCount has changed call PlayMidiSync }
 | 
						|
    pop ret
 | 
						|
] PlayMidiAsync=
 | 
						|
 | 
						|
 | 
						|
[def { PlayMidiSync -- plays MIDI stream, use this after your main VBlank loop }
 | 
						|
  push
 | 
						|
  $01 \soundTimer=                        { keep pumping soundTimer, so that global sound stays alive }
 | 
						|
  [midiDelay 
 | 
						|
    if>0 midiDelay 1- midiDelay=          { if midiDelay>0 midiDelay-- if(midiDelay>0 return }
 | 
						|
    if>0 pop ret]
 | 
						|
 | 
						|
  [do
 | 
						|
    midiStreamPtr, midiCommand=           { midiCommand = Peek(midiStreamPtr) }
 | 
						|
    midiStreamPtr 1+ midiStreamPtr=       { midiStreamPtr++                   }
 | 
						|
    midiCommand $F0& command=
 | 
						|
    [command $80& if=0 midiCommand midiDelay= pop ret]
 | 
						|
    [command $90^ if=0 MidiStartNote! loop]
 | 
						|
    [command $80^ if=0 MidiEndNote!   loop]
 | 
						|
    [command $D0^ if=0 MidiSegment!   loop]
 | 
						|
  loop]
 | 
						|
] PlayMidiSync=
 | 
						|
 | 
						|
 | 
						|
[def { MidiEndNote -- ends a MIDI note }
 | 
						|
  push
 | 
						|
  midiCommand $03& scratch>.              { scratch high = channel }
 | 
						|
  $0 scratch<.                            { scratch low = $00      }
 | 
						|
  scratch midiChannel+ scratch=           { scratch += midiChannel }
 | 
						|
  $0000 scratch:                          { Doke(scratch, $0000)   }
 | 
						|
  pop ret
 | 
						|
] MidiEndNote=
 | 
						|
 | 
						|
 | 
						|
[def { MidiSegment -- jumps to a new MIDI segment }
 | 
						|
  push
 | 
						|
  midiStreamPtr; midiStreamPtr=
 | 
						|
  pop ret
 | 
						|
] MidiSegment=
 | 
						|
 | 
						|
 | 
						|
\vLR>++ ret
 | 
						|
$0300:
 | 
						|
[def { MidiStartNote -- starts a MIDI note }
 | 
						|
  push
 | 
						|
  \notesTable scratch=                    { scratch = \notesTable                  }
 | 
						|
  midiStreamPtr, 10- 1<< 2-               { vAC = (Peek(midiStreamPtr) - 10)*2 - 2 }
 | 
						|
  scratch+ scratch=                       { scratch = scratch + vAC                }
 | 
						|
  0? midiNote<.                           { midiNote low = LUP(scratch + 0)        }
 | 
						|
  scratch 1? midiNote>.                   { midiNote high = LUP(scratch + 1)       }
 | 
						|
  midiCommand $03& scratch>.              { scratch high = channel                 }
 | 
						|
  $0 scratch<.                            { scratch low = $00                      }
 | 
						|
  scratch midiChannel+ scratch=           { scratch += midiChannel                 }
 | 
						|
  midiNote scratch:                       { Doke(scratch, midiNote)                }
 | 
						|
  midiStreamPtr 1+ midiStreamPtr=         { midiStreamPtr++                        }
 | 
						|
  pop ret
 | 
						|
] MidiStartNote=
 | 
						|
 | 
						|
 | 
						|
{ Main }
 | 
						|
\vLR>++ ret
 | 
						|
$0400:
 | 
						|
{ Reset audio hardware }
 | 
						|
ResetAudio!
 | 
						|
 | 
						|
{ Loop forever }
 | 
						|
[do
 | 
						|
  { Wait for VBlank }
 | 
						|
  [do \frameCount, frameCountPrev- if=0 loop]
 | 
						|
  \frameCount, frameCountPrev=
 | 
						|
  PlayMidiSync!                           { play MIDI stream synchronous to VBlank }
 | 
						|
                                          { use this every VBlank                  }
 | 
						|
 | 
						|
  {PlayMidiAsync!}                        { play MIDI stream asynchronous to VBlank }
 | 
						|
loop]                                     { use this in long processing loops       }
 | 
						|
~~~
 | 
						|
 | 
						|
**_VASM Player_**
 | 
						|
~~~
 | 
						|
resetAudio      LDWI    0x0000
 | 
						|
                STW     midiCommand
 | 
						|
                STW     midiDelay
 | 
						|
                STW     midiNote
 | 
						|
                LDWI    giga_soundChan1 + 2 ; keyL, keyH
 | 
						|
                STW     midiChannel
 | 
						|
                STW     scratch
 | 
						|
                LDWI    title_screenMidi00  ; midi score
 | 
						|
                STW     midiStreamPtr
 | 
						|
 | 
						|
                LDI     0x04
 | 
						|
                ST      ii
 | 
						|
 | 
						|
resetA_loop     LDI     giga_soundChan1     ; reset low byte
 | 
						|
                ST      scratch
 | 
						|
                LDWI    0x0300              
 | 
						|
                DOKE    scratch             ; wavA and wavX
 | 
						|
                INC     scratch
 | 
						|
                INC     scratch    
 | 
						|
                LDWI    0x0000
 | 
						|
                DOKE    scratch             ; keyL and keyH
 | 
						|
                INC     scratch
 | 
						|
                INC     scratch
 | 
						|
                DOKE    scratch             ; oscL and oscH
 | 
						|
                INC     scratch + 1         ; increment high byte
 | 
						|
                LoopCounter ii resetA_loop
 | 
						|
                RET
 | 
						|
 | 
						|
 | 
						|
playMidiAsync   LD      giga_frameCount
 | 
						|
                SUBW    frameCountPrev
 | 
						|
                BEQ     playMV_exit
 | 
						|
                LD      giga_frameCount
 | 
						|
                STW     frameCountPrev
 | 
						|
                PUSH
 | 
						|
                CALL    playMidi
 | 
						|
                POP
 | 
						|
playMV_exit     RET
 | 
						|
 | 
						|
 | 
						|
playMidi        LDI     0x01                ; keep pumping soundTimer, so that global sound stays alive
 | 
						|
                ST      giga_soundTimer
 | 
						|
                LDW     midiDelay
 | 
						|
                BEQ     playM_process
 | 
						|
                SUBI    0x01
 | 
						|
                STW     midiDelay
 | 
						|
                BEQ     playM_process    
 | 
						|
                RET
 | 
						|
 | 
						|
playM_process   LDW     midiStreamPtr
 | 
						|
                PEEK                        ; get midi stream byte
 | 
						|
                STW     midiCommand
 | 
						|
                LDW     midiStreamPtr
 | 
						|
                ADDI    0x01
 | 
						|
                STW     midiStreamPtr
 | 
						|
                LDW     midiCommand
 | 
						|
                ANDI    0xF0
 | 
						|
                STW     scratch
 | 
						|
                XORI    0x90                ; check for start note
 | 
						|
                BNE     playM_endnote
 | 
						|
 | 
						|
                PUSH                    
 | 
						|
                CALL    midiStartNote       ; start note
 | 
						|
                POP
 | 
						|
                BRA     playM_process
 | 
						|
                
 | 
						|
playM_endnote   LDW     scratch 
 | 
						|
                XORI    0x80                ; check for end note
 | 
						|
                BNE     playM_segment
 | 
						|
 | 
						|
                PUSH
 | 
						|
                CALL    midiEndNote         ; end note
 | 
						|
                POP
 | 
						|
                BRA     playM_process
 | 
						|
 | 
						|
 | 
						|
playM_segment   LDW     scratch
 | 
						|
                XORI    0xD0                ; check for new segment
 | 
						|
                BNE     playM_delay
 | 
						|
 | 
						|
                PUSH
 | 
						|
                CALL    midiSegment         ; new midi segment
 | 
						|
                POP
 | 
						|
                BRA     playM_process
 | 
						|
 | 
						|
playM_delay     LDW     midiCommand         ; all that is left is delay
 | 
						|
                STW     midiDelay
 | 
						|
                RET
 | 
						|
 | 
						|
 | 
						|
midiStartNote   LDWI    giga_notesTable     ; note table in ROM
 | 
						|
                STW     scratch
 | 
						|
                LDW     midiStreamPtr       ; midi score
 | 
						|
                PEEK
 | 
						|
                SUBI    10
 | 
						|
                LSLW
 | 
						|
                SUBI    2
 | 
						|
                ADDW    scratch
 | 
						|
                STW     scratch
 | 
						|
                LUP     0x00                ; get ROM midi note low byte
 | 
						|
                ST      midiNote
 | 
						|
                LDW     scratch
 | 
						|
                LUP     0x01                ; get ROM midi note high byte
 | 
						|
                ST      midiNote + 1
 | 
						|
                LDW     midiCommand
 | 
						|
                ANDI    0x03                ; get channel
 | 
						|
                ST      scratch + 1
 | 
						|
                LDI     0x00
 | 
						|
                ST      scratch
 | 
						|
                LDW     scratch
 | 
						|
                ADDW    midiChannel         ; channel address
 | 
						|
                STW     scratch
 | 
						|
                LDW     midiNote
 | 
						|
                DOKE    scratch             ; set note
 | 
						|
                LDW     midiStreamPtr
 | 
						|
                ADDI    0x01                ; midiStreamPtr++
 | 
						|
                STW     midiStreamPtr
 | 
						|
                RET
 | 
						|
 | 
						|
 | 
						|
midiEndNote     LDW     midiCommand
 | 
						|
                ANDI    0x03                ; get channel
 | 
						|
                ST      scratch + 1
 | 
						|
                LDI     0x00
 | 
						|
                ST      scratch
 | 
						|
                LDW     scratch
 | 
						|
                ADDW    midiChannel         ; channel address
 | 
						|
                STW     scratch
 | 
						|
                LDWI    0x0000
 | 
						|
                DOKE    scratch             ; end note
 | 
						|
                RET
 | 
						|
 | 
						|
 | 
						|
midiSegment     LDW     midiStreamPtr       ; midi score
 | 
						|
                DEEK
 | 
						|
                STW     midiStreamPtr       ; 0xD0 new midi segment address
 | 
						|
                RET
 | 
						|
~~~
 | 
						|
 | 
						|
## Stream
 | 
						|
- The byte stream produced by Miditones is composed of a number of commands, these commands are differentiated<br/>
 | 
						|
  from delays by the most significant bit of the command byte. The only commands that are supported by the Gigatron<br/>
 | 
						|
  and hence gtmidi are the following:<br/>
 | 
						|
  - **_Note On_**        $9t $nn play note **_nn_** on tone generator **_t_**.<br/>
 | 
						|
  - **_Note Off_**       $8t stop playing on tone generator **_t_**.<br/>
 | 
						|
  - **_Segment_**        $D0 $nnnn contains the absolute 16bit address of the next segment.<br/>
 | 
						|
  - **_Wait_**           $nn waits **_nn_** x 16.666666667ms before processing the next command in the stream.<br/>
 | 
						|
                         $nn, (which is automatically generated by gtmidi), can be a variable length byte stream,<br/>
 | 
						|
                         so the maximum delay is only limited by Miditones maximum delay.<br/>
 | 
						|
 | 
						|
- The Segment command, (**_0xD0_**) is a powerful (**_Gigatron only_**), command embedded within the MIDI byte stream,<br/>
 | 
						|
  (generated automatically by gtmidi), that not only allows the MIDI data to be spread over multiple fragmented areas<br/>
 | 
						|
  of memory, but also allows you to sequence and chain multiple MIDI streams together without writing any code.<br/>
 | 
						|
~~~
 | 
						|
$08A0:
 | 
						|
[def
 | 
						|
  $90# $53# $91# $47# $07# $90# $52# $91# $46# $07# $90# $53# $91# $47# $07# $90# $52# $91# $46#
 | 
						|
  $07# $90# $53# $91# $47# $07# $90# $54# $91# $48# $07# $90# $53# $91# $47# $07# $90# $52#
 | 
						|
  $91# $46# $07# $90# $53# $91# $47# $1d# $80# $81# $d0# $a0# $09#
 | 
						|
] game_overMidi=
 | 
						|
~~~
 | 
						|
- The last command (**_$d0# $a0# $09#_**) within the game over MIDI byte stream is a Segment address that points to the<br/>
 | 
						|
  next segment of MIDI data to process; in this example it would probably point back to the title MIDI byte stream.<br/>
 | 
						|
  If the game_overMIDI byte stream did not fit in that section of memory, then 0xD0 commands would be used to chain<br/>
 | 
						|
  multiple segments of the byte stream together.<br/>
 | 
						|
 | 
						|
## Output
 | 
						|
- vCPU ASM
 | 
						|
~~~
 | 
						|
game_overMidi       EQU     0x8000
 | 
						|
game_overMidi       DB      0x90 0x53 0x91 0x47 0x07 0x90 0x52 0x91 0x46 0x07 0x90 0x53 0x91 0x47
 | 
						|
                    DB      0x07 0x90 0x52 0x91 0x46 0x07 0x90 0x53 0x91 0x47 0x07 0x90 0x54
 | 
						|
                    DB      0x91 0x48 0x07 0x90 0x53 0x91 0x47 0x07 0x90 0x52 0x91 0x46 0x07
 | 
						|
                    DB      0x90 0x53 0x91 0x47 0x1d 0x80 0x81 0xD0 0xA0 0x09
 | 
						|
~~~
 | 
						|
- GCL
 | 
						|
~~~
 | 
						|
$8000:
 | 
						|
[def
 | 
						|
  $90# $53# $91# $47# $07# $90# $52# $91# $46# $07# $90# $53# $91# $47# $07# $90# $52# $91# $46#
 | 
						|
  $07# $90# $53# $91# $47# $07# $90# $54# $91# $48# $07# $90# $53# $91# $47# $07# $90# $52#
 | 
						|
  $91# $46# $07# $90# $53# $91# $47# $1d# $80# $81# $d0# $a0# $09#
 | 
						|
] game_overMidi=
 | 
						|
~~~
 | 
						|
- C++
 | 
						|
~~~
 | 
						|
uint8_t game_overMidi[] =
 | 
						|
{
 | 
						|
    0x90,0x53,0x91,0x47,0x07,0x90,0x52,0x91,0x46,0x07,0x90,0x53,0x91,0x47,0x07,0x90,0x52,0x91,0x46,
 | 
						|
    0x07,0x90,0x53,0x91,0x47,0x07,0x90,0x54,0x91,0x48,0x07,0x90,0x53,0x91,0x47,0x07,0x90,0x52,
 | 
						|
    0x91,0x46,0x07,0x90,0x53,0x91,0x47,0x1d,0x80,0x81,0xD0,0xA0,0x09,
 | 
						|
};
 | 
						|
~~~
 | 
						|
 | 
						|
- Python
 | 
						|
~~~
 | 
						|
game_overMidi = bytearray([
 | 
						|
    0x90,0x53,0x91,0x47,0x07,0x90,0x52,0x91,0x46,0x07,0x90,0x53,0x91,0x47,0x07,0x90,0x52,0x91,0x46,
 | 
						|
    0x07,0x90,0x53,0x91,0x47,0x07,0x90,0x54,0x91,0x48,0x07,0x90,0x53,0x91,0x47,0x07,0x90,0x52,
 | 
						|
    0x91,0x46,0x07,0x90,0x53,0x91,0x47,0x1d,0x80,0x81,0xD0,0xA0,0x09,
 | 
						|
])
 | 
						|
~~~ |