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