gigatron/rom/Contrib/at67/tools/gtmidi
2025-01-28 19:17:01 +03:00
..
CMakeLists.txt init repo 2025-01-28 19:17:01 +03:00
gtmidi.cpp init repo 2025-01-28 19:17:01 +03:00
README.md init repo 2025-01-28 19:17:01 +03:00

gtmidi

Takes the bin output from Miditones https://github.com/LenShustek/miditones and generates source
code data that you can include in your projects for MIDI scores.

Building

  • CMake 3.7 or higher is required for building, has been tested on Windows with Visual Studio and gcc/mingw32
    and also built and tested under Linux.
  • A C++ compiler that supports modern STL.

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.

Segment Offset

The segment offset, (specified in hex), is the offset continually added to the start address to determine
the starting location of each new segment.

Segment Size

The segment size is the maximum number of bytes contained within each segment.

Line length

The line length specifies the maximum length of each line of output source code.

Timing Adjust

The timing adjust specifies a delta that attempts to adjust the overall timing to more closely match the original
overall timing. Normal values are {0.0 <-> 1.5}, results will vary depending on many factors, experimentation is key.

Details

The output format is very similar to the Miditones output format except for a few crucial differences.

  • The Gigatron's maximum channels, (tone generators), is limited to 4, so you must use the -t4 option
    with Miditones.
  • The Gigatron does not support volume or instrument changes, so you cannot use the -i or -v options
    with Miditones.
  • The wait or delay command has been changed from 2 bytes and 1ms resolution to a variable length 1 byte stream
    and 16.66667ms resolution; this has some important impacts.
    • Very short delays will either be rounded down to 0ms or rounded up to 16.6666667ms, this will affect
      timing, how much of a problem it causes is completely dependent on the MIDI score itself.
    • The limit of 2116ms maximum delay has been lifted, gtmidi and the GCL and vASM players both support a
      variable length byte stream of delays.
    • Sequences of Miditones delays are coalesced into a single delay and then saved as a variable length
      byte stream.
    • Zero length delays generated by Miditones are ignored.

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
    specification.
  • The MIDI player expects to be called at least once every 16.66666667ms, the simplest way to achieve this, is to wait
    for VBlank and then call PlayMidiSync; this will work perfectly unless other parts of your code spend more than
    one VBlank processing. If this is the case, then these hot spots will need to call PlayMidiAsync within their
    loops or inner code.

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
    from delays by the most significant bit of the command byte. The only commands that are supported by the Gigatron
    and hence gtmidi are the following:

    • Note On $9t $nn play note nn on tone generator t.
    • Note Off $8t stop playing on tone generator t.
    • Segment $D0 $nnnn contains the absolute 16bit address of the next segment.
    • Wait $nn waits nn x 16.666666667ms before processing the next command in the stream.
      $nn, (which is automatically generated by gtmidi), can be a variable length byte stream,
      so the maximum delay is only limited by Miditones maximum delay.
  • The Segment command, (0xD0) is a powerful (Gigatron only), command embedded within the MIDI byte stream,
    (generated automatically by gtmidi), that not only allows the MIDI data to be spread over multiple fragmented areas
    of memory, but also allows you to sequence and chain multiple MIDI streams together without writing any code.

$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
    next segment of MIDI data to process; in this example it would probably point back to the title MIDI byte stream.
    If the game_overMIDI byte stream did not fit in that section of memory, then 0xD0 commands would be used to chain
    multiple segments of the byte stream together.

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,
])