-
-
Save ftsf/223b0fc761339b3c23dda7dd891514d9 to your computer and use it in GitHub Desktop.
code for loading m8i / m8s files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import strutils | |
import strformat | |
import bitops | |
when not defined(js): | |
import streams | |
else: | |
import jsstream | |
const instrumentSize* = 357 | |
type InstrumentType* = enum | |
WAVSYNTH = 0 | |
MACROSYN = 1 | |
SAMPLE = 2 | |
MIDIOUT = 3 | |
FMSYNTH = 4 | |
NONE = 0xFF | |
type WavShape* = enum | |
PULSE12 = 0 | |
PULSE25 = 1 | |
PULSE50 = 2 | |
PULSE75 = 3 | |
SAW = 4 | |
TRIANGLE = 5 | |
SINE = 6 | |
NOISE_PITCHED = 7 | |
NOISE = 8 | |
OVERFLOW = 9 | |
type MacroShape* {.pure.} = enum | |
CSAW | |
MORPH | |
SAW_SQUARE | |
SINE_TRIANGLE | |
BUZZ | |
SQUARE_SUB | |
SAW_SUB | |
SQUARE_SYNC | |
SAW_SYNC | |
TRIPLE_SAW | |
TRIPLE_SQUARE | |
TRIPLE_TRIANGLE | |
TRIPLE_SIN | |
TRIPLE_RNG | |
SAW_SWARM | |
SAW_COMB | |
TOY | |
DIGITAL_FILTER_LP | |
DIGITAL_FILTER_PK | |
DIGITAL_FILTER_BP | |
DIGITAL_FILTER_HP | |
VOSIM | |
VOWEL | |
VOWEL_FOF | |
HARMONICS | |
FM | |
FEEDBACK_FM | |
CHAOTIC_FEEDBACK_FM | |
PLUCKED | |
BOWED | |
BLOWN | |
STRUCK_BELL | |
STRUCK_DRUM | |
KICK | |
CYMBAL | |
SNARE | |
WAVETABLES | |
WAVE_MAP | |
WAV_LINE | |
WAV_PARAPHONIC | |
FILTERED_NOISE | |
TWIN_PEAKS_NOISE | |
CLOCKED_NOISE | |
GRANULAR_CLOUD | |
PARTICLE_NOISE | |
type MacroInstrument* = object | |
shape*: int | |
timbre*: int | |
color*: int | |
degrade*: int | |
redux*: int | |
type WavInstrument* = object | |
shape*: int | |
size*: int | |
mult*: int | |
warp*: int | |
mirror*: int | |
type SampleInstrument* = object | |
samplePath*: string | |
fineTune*: int | |
detune*: int | |
playMode*: int | |
slices*: int | |
start*: int | |
loopStart*: int | |
length*: int | |
degrade*: int | |
type LimitType* {.pure.} = enum | |
CLIP | |
SIN | |
FOLD | |
WRAP | |
POST | |
POSTAD = "POST: AD" | |
type Env* = object | |
dest*: int | |
amount*: int | |
attack*: int | |
hold*: int | |
decay*: int | |
retrigger*: int | |
type LFO* = object | |
shape*: int | |
dest*: int | |
triggerMode*: int | |
freq*: int | |
amount*: int | |
retrigger*: int | |
type FMWave* {.pure.} = enum | |
SIN | |
SW2 | |
SW3 | |
SW4 | |
SW5 | |
SW6 | |
TRI | |
SAW | |
SQR | |
PUL | |
IMP | |
NOI | |
proc FMAlgoStr*(x: SomeInteger): string = | |
case x: | |
of 0x00: "A>B>C>D" | |
of 0x01: "[A+B]>C>D" | |
of 0x02: "[A>B+C]>D" | |
of 0x03: "[A>B+A>C]>D" | |
of 0x04: "[A+B+C]>D" | |
of 0x05: "[A>B>C]+D" | |
of 0x06: "[A>B>C]+[A>B>D]" | |
of 0x07: "[A>B]+[C>D]" | |
of 0x08: "[A>B]+[A>C]+[A>D]" | |
of 0x09: "[A>B]+[A>C]+D" | |
of 0x0A: "[A>B]+C+D" | |
of 0x0B: "A+B+C+D" | |
else: "" | |
type FMInstrument* = object | |
algo*: int | |
wave*: array[4, FMWave] | |
ratio*: array[4, int] | |
ratioFine*: array[4, int] | |
level*: array[4, int] | |
fb*: array[4, int] | |
modA*: array[4, int] | |
modB*: array[4, int] | |
mods*: array[4, int] | |
type CommonSettings* = object | |
filter*: int | |
cutoff*: int | |
res*: int | |
amp*: int | |
lim*: int | |
pan*: int | |
dry*: int | |
cho*: int | |
del*: int | |
rev*: int | |
env*: array[2,Env] | |
lfo*: array[2,LFO] | |
type | |
FileType* {.pure.} = enum | |
Song = 0x0 | |
Inst = 0x1 | |
Theme = 0x2 | |
Scale = 0x3 | |
M8Version* = object | |
versionStr*: string | |
fileType*: FileType | |
majorVersion*: int | |
minorVersion*: int | |
patchVersion*: int | |
Groove* = object | |
data: array[16, int] | |
Phrase* = object | |
rows*: array[16, array[9, int]] | |
empty*: bool | |
Chain* = object | |
rows*: array[16, array[2, int]] | |
empty*: bool | |
Table* = object | |
rows*: array[16, array[8, int]] | |
Scale* = object | |
name*: string | |
notes*: array[12, bool] | |
offsets*: array[12, (int,int)] | |
Song* = ref object | |
currentChain*: uint8 | |
currentPhrase*: uint8 | |
currentInstrument*: uint8 | |
version*: M8Version | |
directory*: string | |
transpose*: int | |
tempo*: float33 | |
quantize*: bool | |
projectName*: string | |
midiSyncInputMode*: int | |
midiSyncInputTransport*: int | |
midiSyncOutputMode*: int | |
midiSyncOutputTransport*: int | |
midiRecordNoteChannel*: int | |
midiRecordVelocity*: bool | |
midiRecordDelayKill*: bool | |
midiControlChannel*: int | |
midiSongRowCueChannel*: int | |
trackMidiInputChannel*: array[8, int] | |
trackMidiInputInstrument*: array[8, int] | |
trackMidiInputProgramChange*: bool | |
mixerMainVolume*: int | |
mixerTrackVolume*: array[8, int] | |
mixerChorusVolume*: int | |
mixerDelayVolume*: int | |
mixerReverbVolume*: int | |
mixerAnalogInputVolume*: int | |
mixerUSBInputVolume*: int | |
mixerAnalogInputChorus*: int | |
mixerUSBInputChorus*: int | |
mixerAnalogInputDelay*: int | |
mixerUSBInputDelay*: int | |
mixerAnalogInputReverb*: int | |
mixerUSBInputReverb*: int | |
grooves*: array[32, Groove] | |
songOrder*: array[256, array[8, int]] | |
phrases*: array[255, Phrase] | |
chains*: array[255, Chain] | |
tables*: array[256, Table] | |
scales*: array[16, Scale] | |
instruments*: array[128, Instrument] | |
lastInstrument*: array[8, uint8] | |
lastSongRow*: int | |
currentTrack*: int | |
songCursor*: (int,int) | |
chainCursor*: (int,int) | |
phraseCursor*: (int,int) | |
Instrument* = ref object | |
case kind*: InstrumentType | |
of FMSYNTH: fm*: FMInstrument | |
of WAVSYNTH: wavsyn*: WavInstrument | |
of MACROSYN: macrosyn*: MacroInstrument | |
of SAMPLE: sample*: SampleInstrument | |
else: | |
discard | |
version*: M8Version | |
name*: string | |
author*: string | |
transpose*: bool | |
tableTick*: int | |
volume*: int | |
pitch*: int | |
fineTune*: int | |
common*: CommonSettings | |
tableData*: Table | |
type LfoShape* = enum | |
TRI | |
SIN | |
RAMP_DOWN | |
RAMP_UP | |
EXP_DN | |
EXP_UP | |
SQR_DN | |
SQR_UP | |
RANDOM | |
DRUNK | |
TRI_T | |
RAMPD_T | |
RAMPU_T | |
EXPD_T | |
EXPU_T | |
SQ_D_T | |
SQ_U_T | |
RAND_T | |
DRNK_T | |
type LfoTriggerMode* {.pure.} = enum | |
FREE | |
RETRIG | |
HOLD | |
ONCE | |
type FMModDest* {.pure.} = enum | |
LEV | |
RAT | |
PIT | |
FBK | |
proc FMModStr*(x: int): string = | |
return case x: | |
of 0x01: &"1>LEV" | |
of 0x02: &"2>LEV" | |
of 0x03: &"3>LEV" | |
of 0x04: &"4>LEV" | |
of 0x05: &"1>RAT" | |
of 0x06: &"2>RAT" | |
of 0x07: &"3>RAT" | |
of 0x08: &"4>RAT" | |
of 0x09: &"1>PIT" | |
of 0x0A: &"2>PIT" | |
of 0x0B: &"3>PIT" | |
of 0x0C: &"4>PIT" | |
of 0x0D: &"1>FBK" | |
of 0x0E: &"2>FBK" | |
of 0x0F: &"3>FBK" | |
of 0x10: &"4>FBK" | |
else: "-----" | |
type FMOp* = object | |
ratio*: uint8 | |
ratioFine*: uint8 | |
level*: uint8 | |
fb*: uint8 | |
modA*, modB*: int | |
type EnvDestWav* {.pure.} = enum | |
OFF | |
VOLUME | |
PITCH | |
SIZE | |
MULT | |
WARP | |
MIRROR | |
CUTOFF | |
RES | |
AMP | |
PAN | |
type EnvDestSample* {.pure.} = enum | |
OFF | |
VOLUME | |
PITCH | |
LOOP_ST | |
LENGTH | |
DEGRADE | |
CUTOFF | |
RES | |
AMP | |
PAN | |
type EnvDestFM* {.pure.} = enum | |
OFF | |
VOLUME | |
PITCH | |
MOD1 | |
MOD2 | |
MOD3 | |
MOD4 | |
CUTOFF | |
RES | |
AMP | |
PAN | |
type EnvDestMacro* {.pure.} = enum | |
OFF | |
VOLUME | |
PITCH | |
TIMBRE | |
COLOR | |
DEGRADE | |
REDUX | |
CUTOFF | |
RES | |
AMP | |
PAN | |
type FilterTypeWAVPreLPHP* {.pure.} = enum | |
OFF | |
LOWPASS | |
HIGHPASS | |
BANDPASS | |
BANDSTOP | |
WAV_LP | |
WAV_HP | |
WAV_BP | |
WAV_BS | |
type FilterTypeWAV* {.pure.} = enum | |
OFF | |
LOWPASS | |
HIGHPASS | |
BANDPASS | |
BANDSTOP | |
LP2HP = "LP>HP" | |
WAV_LP | |
WAV_HP | |
WAV_BP | |
WAV_BS | |
type FilterType* {.pure.} = enum | |
OFF | |
LOWPASS | |
HIGHPASS | |
BANDPASS | |
BANDSTOP | |
LP2HP = "LP>HP" | |
type CommonCmd* {.pure.} = enum | |
ARP | |
CHA | |
DEL | |
GRV | |
HOP | |
KIL | |
RAN | |
RET | |
REP | |
NTH | |
PSL | |
PVB | |
PVX | |
SED | |
TBL | |
THO | |
TIC | |
TPO | |
VMV | |
XCM | |
XCF | |
XCW | |
XCR | |
XDT | |
XDF | |
XDW | |
XDR | |
XRS | |
XRD | |
XRM | |
XRF | |
XRW | |
XRZ | |
VCH | |
VDE | |
VRE | |
VT1 | |
VT2 | |
VT3 | |
VT4 | |
VT5 | |
VT6 | |
VT7 | |
VT8 | |
DJF | |
type CommonCmd25* {.pure.} = enum | |
ARP | |
CHA | |
DEL | |
GRV | |
HOP | |
KIL | |
RAN | |
RET | |
REP | |
NTH | |
PSL | |
PBN | |
PVB | |
PVX | |
SCA | |
SCG | |
SED | |
SNG | |
TBL | |
THO | |
TIC | |
TPO | |
TSP | |
VMV | |
XCM | |
XCF | |
XCW | |
XCR | |
XDT | |
XDF | |
XDW | |
XDR | |
XRS | |
XRD | |
XRM | |
XRF | |
XRW | |
XRZ | |
VCH | |
VDE | |
VRE | |
VT1 | |
VT2 | |
VT3 | |
VT4 | |
VT5 | |
VT6 | |
VT7 | |
VT8 | |
DJF | |
IVO | |
ICH | |
IDE | |
IRE | |
IV2 | |
IC2 | |
ID2 | |
IR2 | |
USB | |
type MacrosynCmd* {.pure.} = enum | |
VOL = 0x80 | |
PIT | |
FIN | |
OSC | |
TBR | |
COL | |
DEG | |
RED | |
FLT | |
CUT | |
RES | |
AMP | |
LIM | |
PAN | |
DRY | |
SCH | |
SDL | |
SRV | |
EA1 | |
AT1 | |
HO1 | |
DE1 | |
ET1 | |
EA2 | |
AT2 | |
HO2 | |
DE2 | |
ET2 | |
LA1 | |
LF1 | |
LT1 | |
LA2 | |
LF2 | |
LT2 | |
TRG | |
XX1 | |
XX2 | |
XX3 | |
type WavsynCmd* {.pure.} = enum | |
VOL = 0x80 | |
PIT | |
FIN | |
OSC | |
SIZ | |
MUL | |
WRP | |
MIR | |
FLT | |
CUT | |
RES | |
AMP | |
LIM | |
PAN | |
DRY | |
SCH | |
SDL | |
SRV | |
EA1 | |
AT1 | |
HO1 | |
DE1 | |
ET1 | |
EA2 | |
AT2 | |
HO2 | |
DE2 | |
ET2 | |
LA1 | |
LF1 | |
LT1 | |
LA2 | |
LF2 | |
LT2 | |
XX1 | |
XX2 | |
XX3 | |
type FMCmd* {.pure.} = enum | |
VOL = 0x80 | |
PIT | |
FIN | |
ALG | |
FM1 | |
FM2 | |
FM3 | |
FM4 | |
FLT | |
CUT | |
RES | |
AMP | |
LIM | |
PAN | |
DRY | |
SCH | |
SDL | |
SRV | |
EA1 | |
AT1 | |
HO1 | |
DE1 | |
ET1 | |
EA2 | |
AT2 | |
HO2 | |
DE2 | |
ET2 | |
LA1 | |
LF1 | |
LT1 | |
LA2 | |
LF2 | |
LT2 | |
FMP | |
XX1 = "---" | |
XX2 = "---" | |
XX3 = "---" | |
type SampleCmd* {.pure.} = enum | |
VOL = 0x80 | |
PIT | |
FIN | |
PLY | |
STA | |
LOP | |
LEN | |
DEG | |
FLT | |
CUT | |
RES | |
AMP | |
LIM | |
PAN | |
DRY | |
SCH | |
SDL | |
SRV | |
EA1 | |
AT1 | |
HO1 | |
DE1 | |
ET1 | |
EA2 | |
AT2 | |
HO2 | |
DE2 | |
ET2 | |
LA1 | |
LF1 | |
LT1 | |
LA2 | |
LF2 | |
LT2 | |
SLI | |
XX2 = "---" | |
XX3 = "---" | |
type SamplePlayMode* = enum | |
FWD | |
REV | |
FWDLOOP | |
REVLOOP | |
FWD_PP | |
REV_PP | |
OSC | |
OSC_REV | |
OSC_PP | |
proc M8Signed*(x: uint8): int = | |
if x >= 0x80: | |
result = (x.int-256) | |
else: | |
result = x.int | |
proc readStrFF*(fp: auto, length: int): string = | |
var i = 0 | |
while i < length: | |
i.inc() | |
let ch = fp.readChar() | |
if ch == 0xff.char or ch == '\0': | |
break | |
result &= ch | |
while i < length: | |
discard fp.readChar() | |
i.inc() | |
proc writeStrFF*(fp: auto, str: string, length: int) = | |
# writes string padded with 0xff | |
for i in 0..<length: | |
if i < str.len: | |
fp.write(str[i]) | |
else: | |
fp.write(0xff.char) | |
proc writeStr00*(fp: auto, str: string, length: int) = | |
# writes string padded with 0x00 | |
for i in 0..<length: | |
if i < str.len: | |
fp.write(str[i]) | |
else: | |
fp.write(0x00.char) | |
proc writeUint8*(fp: auto, x: SomeOrdinal) = | |
fp.write(x.uint8) | |
proc `$`*(self: M8Version): string = | |
result = &"{self.fileType}:{self.majorVersion.toHex(2)}.{self.minorVersion.toHex(2)}.{self.patchVersion.toHex(2)}" | |
proc `==`*(a,b: M8Version): bool = | |
a.majorVersion == b.majorVersion and a.minorVersion == b.minorVersion and a.patchVersion == b.patchVersion | |
proc `<`*(a,b: M8Version): bool = | |
if a.majorVersion < b.majorVersion: | |
return true | |
elif a.minorVersion < b.minorVersion: | |
return true | |
elif a.patchVersion < b.patchVersion: | |
return true | |
return false | |
proc `<=`*(a,b: M8Version): bool = | |
if a == b: | |
return true | |
return a < b | |
proc version*(major,minor,patch: int, fileType: FileType = Inst): M8Version = | |
result.versionStr = "M8VERSION" | |
result.majorVersion = major | |
result.minorVersion = minor | |
result.patchVersion = patch | |
result.fileType = fileType | |
const vFmWavesAdded = version(0x00,0x01,0x40) | |
const vTablePaddingAdded = version(0x00,0x01,0x40) | |
const vTwoLFOs = version(0x00,0x01,0x40) | |
const v25 = version(0x00,0x02,0x50) | |
const vLPHPAdded = version(0x00,0x02,0x51) | |
proc readEnv*(fp: auto): Env = | |
result.dest = fp.readUint8().int | |
result.amount = fp.readUint8().int | |
result.attack = fp.readUint8().int | |
result.hold = fp.readUint8().int | |
result.decay = fp.readUint8().int | |
result.retrigger = fp.readUint8().int | |
proc writeEnv*(fp: auto, env: Env) = | |
fp.writeUint8(env.dest) | |
fp.writeUint8(env.amount) | |
fp.writeUint8(env.attack) | |
fp.writeUint8(env.hold) | |
fp.writeUint8(env.decay) | |
fp.writeUint8(env.retrigger) | |
proc readLFO*(fp: auto): LFO = | |
result.shape = fp.readUint8().int | |
result.dest = fp.readUint8().int | |
result.triggerMode = fp.readUint8().int | |
result.freq = fp.readUint8().int | |
result.amount = fp.readUint8().int | |
result.retrigger = fp.readUint8().int | |
proc writeLFO*(fp: auto, lfo: LFO) = | |
fp.writeUint8(lfo.shape) | |
fp.writeUint8(lfo.dest) | |
fp.writeUint8(lfo.triggerMode) | |
fp.writeUint8(lfo.freq) | |
fp.writeUint8(lfo.amount) | |
fp.writeUint8(lfo.retrigger) | |
proc readCommon*(fp: auto, version: M8Version): CommonSettings = | |
result.filter = fp.readUint8().int | |
result.cutoff = fp.readUint8().int | |
result.res = fp.readUint8().int | |
result.amp = fp.readUint8().int | |
result.lim = fp.readUint8().int | |
result.pan = fp.readUint8().int | |
result.dry = fp.readUint8().int | |
result.cho = fp.readUint8().int | |
result.del = fp.readUint8().int | |
result.rev = fp.readUint8().int | |
for i in 0..<2: | |
result.env[i] = fp.readEnv() | |
let lfoCount = if version >= vTwoLFOs: 2 else: 1 | |
for i in 0..<lfoCount: | |
result.lfo[i] = fp.readLFO() | |
proc writeCommon*(fp: auto, instr: Instrument) = | |
fp.writeUint8(instr.common.filter) | |
fp.writeUint8(instr.common.cutoff) | |
fp.writeUint8(instr.common.res) | |
fp.writeUint8(instr.common.amp) | |
fp.writeUint8(instr.common.lim) | |
fp.writeUint8(instr.common.pan) | |
fp.writeUint8(instr.common.dry) | |
fp.writeUint8(instr.common.cho) | |
fp.writeUint8(instr.common.del) | |
fp.writeUint8(instr.common.rev) | |
for i in 0..<2: | |
fp.writeEnv(instr.common.env[i]) | |
let lfoCount = if instr.version >= vTwoLFOs: 2 else: 1 | |
for i in 0..<lfoCount: | |
fp.writeLFO(instr.common.lfo[i]) | |
proc readMacrosyn*(fp: auto, version: M8Version): MacroInstrument = | |
result.shape = fp.readUint8().int | |
result.timbre = fp.readUint8().int | |
result.color = fp.readUint8().int | |
result.degrade = fp.readUint8().int | |
result.redux = fp.readUint8().int | |
proc writeMacrosyn*(fp: auto, m: MacroInstrument) = | |
fp.writeUint8(m.shape) | |
fp.writeUint8(m.timbre) | |
fp.writeUint8(m.color) | |
fp.writeUint8(m.degrade) | |
fp.writeUint8(m.redux) | |
proc readWavsynth*(fp: auto, version: M8Version): WavInstrument = | |
result.shape = fp.readUint8().int | |
result.size = fp.readUint8().int | |
result.mult = fp.readUint8().int | |
result.warp = fp.readUint8().int | |
result.mirror = fp.readUint8().int | |
proc writeWavsynth*(fp: auto, wav: WavInstrument) = | |
fp.writeUint8(wav.shape) | |
fp.writeUint8(wav.size) | |
fp.writeUint8(wav.mult) | |
fp.writeUint8(wav.warp) | |
fp.writeUint8(wav.mirror) | |
proc readSample*(fp: auto, version: M8Version): SampleInstrument = | |
result.playMode = fp.readUint8().int | |
result.slices = fp.readUint8().int | |
result.start = fp.readUint8().int | |
result.loopStart = fp.readUint8().int | |
result.length = fp.readUint8().int | |
result.degrade = fp.readUint8().int | |
proc writeSample*(fp: auto, sample: SampleInstrument) = | |
fp.writeUint8(sample.playMode) | |
fp.writeUint8(sample.slices) | |
fp.writeUint8(sample.start) | |
fp.writeUint8(sample.loopStart) | |
fp.writeUint8(sample.length) | |
fp.writeUint8(sample.degrade) | |
proc readFM*(fp: auto, version: M8Version): FMInstrument = | |
result.algo = fp.readUint8().int | |
var opData: array[4, FMOp] | |
if version >= vFmWavesAdded: | |
for op in 0..<4: | |
result.wave[op] = fp.readUint8().FMWave | |
for op in 0..<4: | |
result.ratio[op] = fp.readUint8().int | |
result.ratioFine[op] = fp.readUint8().int | |
for op in 0..<4: | |
result.level[op] = fp.readUint8().int | |
result.fb[op] = fp.readUint8().int | |
for op in 0..<4: | |
result.modA[op] = fp.readUint8().int | |
for op in 0..<4: | |
result.modB[op] = fp.readUint8().int | |
for i in 0..<4: | |
result.mods[i] = fp.readUint8().int | |
proc writeFM*(fp: auto, fm: FMInstrument) = | |
fp.writeUint8(fm.algo) | |
for op in 0..<4: | |
fp.writeUint8(fm.wave[op]) | |
for op in 0..<4: | |
fp.writeUint8(fm.ratio[op]) | |
fp.writeUint8(fm.ratioFine[op]) | |
for op in 0..<4: | |
fp.writeUint8(fm.level[op]) | |
fp.writeUint8(fm.fb[op]) | |
for op in 0..<4: | |
fp.writeUint8(fm.modA[op]) | |
for op in 0..<4: | |
fp.writeUint8(fm.modB[op]) | |
for op in 0..<4: | |
fp.writeUint8(fm.mods[op]) | |
proc readM8Version*(fp: auto): M8Version = | |
var versionStr = fp.readStrFF(9) | |
discard fp.readUint8() | |
var versionPatch = fp.readUint8().int | |
var versionMinor = fp.readUint8().int | |
var versionMajor = fp.readUint8().int | |
var fileType = (fp.readUint8() shr 4).int | |
#echo &"'{versionStr}' ft: {fileType.toHex(2)} {versionMajor.toHex(2)}.{versionMinor.toHex(2)}.{versionPatch.toHex(2)}" | |
result.versionStr = versionStr | |
result.fileType = fileType.FileType | |
result.majorVersion = versionMajor | |
result.minorVersion = versionMinor | |
result.patchVersion = versionPatch | |
proc writeM8Version*(fp: auto, version: M8Version) = | |
fp.writeStr00(version.versionStr, 9) | |
fp.writeUint8(0) | |
fp.writeUint8(version.patchVersion) | |
fp.writeUint8(version.minorVersion) | |
fp.writeUint8(version.majorVersion) | |
fp.writeUint8(version.fileType.int shl 4) | |
echo "writeM8Version done" | |
proc readScale*(fp: auto, version: M8Version): Scale = | |
# each scale is 0x2a (42) bytes | |
# the enabled notes are stored in the first two bytes as a bit map | |
result = Scale() | |
let noteMap = fp.readUint16() | |
for i in 0..<12: | |
result.notes[i] = noteMap.testBit(i) | |
for i in 0..<12: | |
result.offsets[i] = (fp.readUInt8().M8Signed, fp.readUInt8().M8Signed) | |
result.name = fp.readStrFF(16) | |
proc writeScale*(fp: auto, scale: Scale) = | |
var noteMap: uint16 | |
for i in 0..<12: | |
noteMap.setBit(i, scale.notes[i]) | |
fp.write(noteMap) | |
proc readSong*(fp: auto, version: M8Version): Song = | |
echo "readSong version ", version | |
result = Song() | |
result.currentChain = 0xff | |
result.version = version | |
result.directory = fp.readStrFF(128) | |
echo result.directory | |
result.transpose = fp.readUint8().int | |
var tempo1 = fp.readUint8() | |
var tempo2 = fp.readUint8() | |
var tempo3 = fp.readUint8() | |
var tempo4 = fp.readUint8() | |
echo &"tempo {tempo1}, {tempo2}, {tempo3}, {tempo4}" | |
result.tempo = 0 | |
result.quantize = fp.readUint8().bool | |
result.projectName = fp.readStrFF(12) | |
echo result.projectName | |
result.midiSyncInputMode = fp.readUint8().int | |
result.midiSyncInputTransport = fp.readUint8().int | |
result.midiSyncOutputMode = fp.readUint8().int | |
result.midiSyncOutputTransport = fp.readUint8().int | |
result.midiRecordNoteChannel = fp.readUint8().int | |
result.midiRecordVelocity = fp.readUint8().bool | |
result.midiRecordDelayKill = fp.readUint8().bool | |
result.midiControlChannel = fp.readUint8().int | |
result.midiSongRowCueChannel = fp.readUint8().int | |
for i in 0..<8: | |
result.trackMidiInputChannel[i] = fp.readUint8().int | |
result.trackMidiInputInstrument[i] = fp.readUint8().int | |
result.trackMidiInputProgramChange = fp.readUint8().bool | |
result.mixerMainVolume = fp.readUint8().int | |
for i in 0..<8: | |
result.mixerTrackVolume[i] = fp.readUint8().int | |
result.mixerChorusVolume = fp.readUint8().int | |
result.mixerDelayVolume = fp.readUint8().int | |
result.mixerReverbVolume = fp.readUint8().int | |
result.mixerAnalogInputVolume = fp.readUint8().int | |
result.mixerUSBInputVolume = fp.readUint8().int | |
result.mixerAnalogInputChorus = fp.readUint8().int | |
result.mixerUSBInputChorus = fp.readUint8().int | |
result.mixerAnalogInputDelay = fp.readUint8().int | |
result.mixerUSBInputDelay = fp.readUint8().int | |
result.mixerAnalogInputReverb = fp.readUint8().int | |
result.mixerUSBInputReverb = fp.readUint8().int | |
for i in 0..<32: | |
discard fp.readUint8() | |
for i in 0..<32: | |
echo "GROOVE ", i.toHex(2) | |
for j in 0..<16: | |
result.grooves[i].data[j] = fp.readUint8().int | |
if result.grooves[i].data[j] != 0xff: | |
echo j.toHex(2) & ": ", result.grooves[i].data[j].toHex(2) | |
echo " " | |
echo "SONG DATA ", fp.getPosition().toHex(8) | |
for i in 0..<256: | |
var row = i.toHex(2) & ": " | |
var anyEntries = false | |
for j in 0..<8: | |
result.songOrder[i][j] = fp.readUint8().int | |
if result.songOrder[i][j] == 0xff: | |
row &= "--" & " " | |
else: | |
row &= result.songOrder[i][j].toHex(2) & " " | |
anyEntries = true | |
result.lastSongRow = i | |
if anyEntries: | |
echo row | |
echo "PHRASE DATA ", fp.getPosition().toHex(8) | |
for i in 0..<255: | |
var anyEntries = false | |
for j in 0..<16: | |
for k in 0..<9: | |
result.phrases[i].rows[j][k] = fp.readUint8().int | |
if result.phrases[i].rows[j][0] != 0xff: | |
anyEntries = true | |
if anyEntries: | |
echo "PHRASE ", i.toHex(2) | |
else: | |
result.phrases[i].empty = true | |
echo "CHAIN DATA ", fp.getPosition().toHex(8) | |
for i in 0..<255: | |
var anyEntries = false | |
var notEmpty = false | |
for j in 0..<16: | |
for k in 0..<2: | |
result.chains[i].rows[j][k] = fp.readUint8().int | |
if result.chains[i].rows[j][0] != 0xff: | |
anyEntries = true | |
if notEmpty == false and not result.phrases[result.chains[i].rows[j][0]].empty: | |
notEmpty = true | |
if anyEntries: | |
echo "CHAIN ", i.toHex(2) | |
if not notEmpty: | |
result.chains[i].empty = true | |
echo "TABLE DATA ", fp.getPosition().toHex(8) | |
for i in 0..<256: | |
result.tables[i] = fp.readTable() | |
echo "INSTRUMENT DATA ", fp.getPosition().toHex(8) | |
for i in 0..<128: | |
# each instrument is 207 bytes * 128 = 26496 | |
var pos = fp.getPosition() | |
result.instruments[i] = fp.readInstr(version, fromSong = true) | |
if result.instruments[i].kind != None: | |
echo pos.toHex(8), ": ", i.toHex(2), " ", result.instruments[i].kind, " '", result.instruments[i].name, "'" | |
else: | |
#echo pos.toHex(8), ": ", i.toHex(2), " ", result.instruments[i].kind | |
discard | |
result.instruments[i].tableData = result.tables[i] | |
if version >= v25: | |
fp.setPosition(0x1aa7e) | |
echo "SCALES DATA ", fp.getPosition().toHex(8) | |
for i in 0..<16: | |
result.scales[i] = fp.readScale(version) | |
echo result.scales[i].name | |
echo result.scales[i].notes | |
echo result.scales[i].offsets | |
proc readInstr*(fp: auto, version: M8Version, fromSong: bool = false): Instrument = | |
var startPos = if not fromSong: fp.getPosition() - 14 else: fp.getPosition() | |
var kind = fp.readUint8().InstrumentType | |
result = Instrument(kind: kind) | |
result.version = version | |
result.name = fp.readStrFF(12) | |
result.transpose = fp.readUint8().bool | |
result.tableTick = fp.readUint8().int | |
result.volume = fp.readUint8().int | |
result.pitch = fp.readUint8().int | |
result.fineTune = fp.readUint8().int | |
case kind: | |
of FMSYNTH: | |
result.fm = readFM(fp, version) | |
of WAVSYNTH: | |
result.wavsyn = readWavsynth(fp, version) | |
of MACROSYN: | |
result.macrosyn = readMacrosyn(fp, version) | |
of SAMPLE: | |
result.sample = readSample(fp, version) | |
else: | |
discard | |
result.common = readCommon(fp, version) | |
let samplePathPos = startPos + (if fromSong: 0x56 else: 0x5D + 8) | |
fp.setPosition(samplePathPos) | |
if kind == SAMPLE: | |
result.sample.samplePath = fp.readStrFF(127) | |
if fromSong: | |
fp.setPosition(startPos + 0xD7) | |
else: | |
fp.setPosition(startPos + 0xDD) | |
if version >= vTablePaddingAdded: | |
# some padding before tables | |
for i in 0..<8: | |
discard fp.readUint8() | |
result.tableData = fp.readTable() | |
proc readTable*(fp: auto): Table = | |
for i in 0..<16: | |
for j in 0..<8: | |
result.rows[i][j] = fp.readUint8().int | |
proc writeTable*(fp: auto, table: Table) = | |
for i in 0..<16: | |
for j in 0..<8: | |
fp.writeUint8(table.rows[i][j]) | |
proc writeInstr*(fp: auto, instr: Instrument) = | |
var startPos = fp.getPosition() - 14 | |
# allow us to save an instrument to file | |
fp.writeUint8(instr.kind) | |
fp.writeStr00(instr.name, 12) | |
fp.writeUint8(instr.transpose) | |
fp.writeUint8(instr.tableTick) | |
fp.writeUint8(instr.volume) | |
fp.writeUint8(instr.pitch) | |
fp.writeUint8(instr.fineTune) | |
case instr.kind: | |
of FMSYNTH: | |
fp.writeFM(instr.fm) | |
of WAVSYNTH: | |
fp.writeWavsynth(instr.wavsyn) | |
of SAMPLE: | |
fp.writeSample(instr.sample) | |
of MACROSYN: | |
fp.writeMacrosyn(instr.macrosyn) | |
else: | |
discard | |
fp.writeCommon(instr) | |
fp.setPosition(startPos + 0x5D + 8) | |
if instr.kind == SAMPLE: | |
fp.writeStr00(instr.sample.samplePath, 127) | |
# tables | |
fp.setPosition(startPos + 0xDD) | |
if instr.version >= vTablePaddingAdded: | |
for i in 0..<8: | |
fp.writeUint8(0'u8) | |
fp.writeTable(instr.tableData) | |
proc write*(fp: auto, version: M8Version) = | |
fp.write("M8VERSION") | |
fp.write(0.uint8) | |
fp.write(version.patchVersion.uint8) | |
fp.write(version.minorVersion.uint8) | |
fp.write(version.majorVersion.uint8) | |
fp.write((version.fileType.int shl 4).uint8) | |
proc writeFM*(fp: auto, m8i: Instrument) = | |
fp.write(m8i.fm.algo.uint8) | |
for op in 0..<4: | |
fp.write(m8i.fm.wave[op].uint8) | |
for op in 0..<4: | |
fp.write(m8i.fm.ratio[op].uint8) | |
fp.write(m8i.fm.ratioFine[op].uint8) | |
for op in 0..<4: | |
fp.write(m8i.fm.level[op].uint8) | |
fp.write(m8i.fm.fb[op].uint8) | |
for op in 0..<4: | |
fp.write(m8i.fm.modA[op].uint8) | |
for op in 0..<4: | |
fp.write(m8i.fm.modB[op].uint8) | |
for op in 0..<4: | |
fp.write(m8i.fm.mods[op].uint8) | |
proc writeCommon*(fp: auto, m8i: Instrument) = | |
fp.write(m8i.common.filter.uint8) | |
fp.write(m8i.common.cutoff.uint8) | |
fp.write(m8i.common.res.uint8) | |
fp.write(m8i.common.amp.uint8) | |
fp.write(m8i.common.lim.uint8) | |
fp.write(m8i.common.pan.uint8) | |
fp.write(m8i.common.dry.uint8) | |
fp.write(m8i.common.cho.uint8) | |
fp.write(m8i.common.del.uint8) | |
fp.write(m8i.common.rev.uint8) | |
for i in 0..<2: | |
fp.write(m8i.common.env[i].dest.uint8) | |
fp.write(m8i.common.env[i].amount.uint8) | |
fp.write(m8i.common.env[i].attack.uint8) | |
fp.write(m8i.common.env[i].hold.uint8) | |
fp.write(m8i.common.env[i].decay.uint8) | |
fp.write(m8i.common.env[i].retrigger.uint8) | |
for i in 0..<2: | |
fp.write(m8i.common.lfo[i].shape.uint8) | |
fp.write(m8i.common.lfo[i].dest.uint8) | |
fp.write(m8i.common.lfo[i].triggerMode.uint8) | |
fp.write(m8i.common.lfo[i].freq.uint8) | |
fp.write(m8i.common.lfo[i].amount.uint8) | |
fp.write(m8i.common.lfo[i].retrigger.uint8) | |
proc write*(fp: auto, m8i: Instrument) = | |
fp.write(m8i.kind.uint8) | |
for i in 0..<12: | |
if i < m8i.name.len: | |
fp.write(m8i.name[i].char) | |
else: | |
fp.write(0.char) | |
fp.write(m8i.transpose.uint8) | |
fp.write(m8i.tableTick.uint8) | |
fp.write(m8i.volume.uint8) | |
fp.write(m8i.pitch.uint8) | |
fp.write(m8i.fineTune.uint8) | |
case m8i.kind: | |
of FMSYNTH: | |
fp.writeFM(m8i) | |
else: | |
discard | |
fp.writeCommon(m8i) | |
# sample name | |
if m8i.kind == SAMPLE: | |
# jump to 0x5D + 8 | |
fp.setPosition(0x5D + 8) | |
for i in 0..<127: | |
if i < m8i.sample.samplePath.len: | |
fp.write(m8i.sample.samplePath[i].char) | |
else: | |
fp.write(0.char) | |
# jump to 0xDD | |
fp.setPosition(0xDD) | |
for j in 0..<8: | |
fp.write(0.uint8) | |
# table data | |
for i in 0..<16: | |
for j in 0..<8: | |
fp.write(m8i.tableData.rows[i][j].uint8) | |
proc noteStr*(note: int): string = | |
let oct = note div 12 | |
let key = note mod 12 | |
result = case key: | |
of 0: "C-" | |
of 1: "C#" | |
of 2: "D-" | |
of 3: "D#" | |
of 4: "E-" | |
of 5: "F-" | |
of 6: "F#" | |
of 7: "G-" | |
of 8: "G#" | |
of 9: "A-" | |
of 10: "A#" | |
of 11: "B-" | |
else: "?" | |
result &= $oct | |
proc getCmdName*(x: int, instr: Instrument): string = | |
case x: | |
of 0xFF: | |
"---" | |
of 0x00..0x1E: | |
if instr.version >= v25: | |
$x.CommonCmd25 | |
else: | |
$x.CommonCmd | |
of 0x80..0xFE: | |
case instr.kind: | |
of MACROSYN: | |
$x.MacrosynCmd | |
of FMSYNTH: | |
$x.FMCmd | |
of WAVSYNTH: | |
$x.WavsynCmd | |
of SAMPLE: | |
$x.SampleCmd | |
else: | |
"UIK" | |
else: | |
"NFI" | |
proc toFilterType*(x: int, instr: Instrument): string = | |
return case instr.kind: | |
of WAVSYNTH: | |
if instr.version >= vLPHPAdded: | |
$x.FilterTypeWAV | |
else: | |
$x.FilterTypeWAVPreLPHP | |
of FMSYNTH, MACROSYN, SAMPLE: | |
$x.FilterType | |
else: | |
"---" | |
when isMainModule: | |
import os | |
import streams | |
proc row(label: string, values: varargs[string, `$`]) = | |
write(stdout, label) | |
write(stdout, ": ") | |
for v in values: | |
write(stdout, v) | |
write(stdout, " ") | |
write(stdout, "\n") | |
template hex1(x: untyped): untyped = | |
x.toHex(1) | |
template hex2(x: untyped): untyped = | |
x.toHex(2) | |
proc renderInstr(instr: Instrument) = | |
row("kind", instr.kind) | |
row("name", instr.name) | |
row("transpose", instr.transpose) | |
row("table tic", instr.tableTick) | |
row("volume", instr.volume) | |
row("pitch", instr.pitch) | |
row("fineTune", instr.fineTune) | |
echo "" | |
echo "COMMON" | |
row("filter", instr.common.filter.FilterTypeFM) | |
row("cutoff", instr.common.cutoff) | |
row("res", instr.common.res) | |
row("amp", instr.common.amp) | |
row("lim", instr.common.lim.LimitType) | |
row("pan", instr.common.pan) | |
row("dry", instr.common.dry) | |
row("cho", instr.common.cho) | |
row("del", instr.common.del) | |
row("rev", instr.common.rev) | |
echo "" | |
case instr.kind: | |
of FMSYNTH: | |
echo "FM" | |
row("algo", instr.fm.algo.FMAlgoStr) | |
row("wave", instr.fm.wave) | |
row("ratio", instr.fm.ratio) | |
row("ratioFine", instr.fm.ratioFine) | |
row("level", instr.fm.level) | |
row("fb", instr.fm.fb) | |
row("mod ", instr.fm.modA[0].FMModStr, instr.fm.modA[1].FMModStr, instr.fm.modA[2].FMModStr, instr.fm.modA[3].FMModStr) | |
row(" ", instr.fm.modB[0].FMModStr, instr.fm.modB[1].FMModStr, instr.fm.modB[2].FMModStr, instr.fm.modB[3].FMModStr) | |
row("mods", instr.fm.mods) | |
echo "" | |
else: | |
discard | |
echo "TABLE" | |
echo(" N V FX1 FX2 FX3 ") | |
for i in 0..<16: | |
var transpose = instr.tableData.rows[i][0] | |
var volume = instr.tableData.rows[i][1] | |
var cmd1 = instr.tableData.rows[i][2] | |
var cmd1Value = instr.tableData.rows[i][3] | |
var cmd2 = instr.tableData.rows[i][4] | |
var cmd2Value = instr.tableData.rows[i][5] | |
var cmd3 = instr.tableData.rows[i][6] | |
var cmd3Value = instr.tableData.rows[i][7] | |
let cmd1Str = if cmd1 != 0xff: cmd1.getCmdName(instr) else: "---" | |
let cmd2Str = if cmd2 != 0xff: cmd2.getCmdName(instr) else: "---" | |
let cmd3Str = if cmd3 != 0xff: cmd3.getCmdName(instr) else: "---" | |
echo &"{i.hex1} {transpose.hex2} {volume.hex2} {cmd1Str}{cmd1Value.hex2} {cmd2Str}{cmd2Value.hex2} {cmd3Str}{cmd3Value.hex2}" | |
proc renderSong(song: Song) = | |
discard | |
proc main() = | |
# parse file and dump data | |
if paramCount() == 0: | |
echo "usage: m8i file.m8i" | |
quit(1) | |
let fp = newFileStream(paramStr(1), fmRead) | |
let version = fp.readM8Version() | |
row("version", version) | |
if version.fileType == FileType.Inst: | |
let instr = fp.readInstr(version) | |
renderInstr(instr) | |
elif version.fileType == FileType.Scale: | |
let scale = fp.readScale(version) | |
renderScale(scale) | |
elif version.fileType == FileType.Song: | |
let song = fp.readSong(version) | |
renderSong(song) | |
for i, instr in song.instruments: | |
if instr.kind != None: | |
echo "INSTRUMENT ", i.toHex(2) | |
renderInstr(instr) | |
var outversion = v25 | |
outversion.fileType = Inst | |
var outfp = newFileStream(instr.name & ".m8i", fmWrite) | |
outfp.saveM8Version(outversion) | |
outfp.writeInstr(instr) | |
outfp.close() | |
fp.close() | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment