32blit / 32blit-sdk

32blit SDK
https://32blit.com
MIT License
190 stars 67 forks source link

Request: MIDI playback #189

Open mylogon341 opened 4 years ago

mylogon341 commented 4 years ago

I'm not sure if this is even possible without the correct hardware or whatever but this would make music playback easy + small.

tinwhisker commented 4 years ago

Easiest is to use a tracker.

Midi is certainly doable, but the trade-off is requiring a soundfont, which you could drop on your as card. (Might be able to convert one down small enough that you could stuff it permanently on the external flash)

Might have a play with the idea 👍

Gadgetoid commented 4 years ago

I used DefleMask (http://www.deflemask.com/) in Commodore 64 mode for the terrible demo "song". I don't find trackers particularly intuitive versus using a piano roll and producing MIDI output, though.

I wrote the monstrosity below which extracts the raw note values from DefleMask .dmf files. It doesn't handle any of the instrument data, but our envelope generators and available voices align pretty closely with the SID chip so it's not hard to port across.

#!/usr/bin/env python3

import argparse
import pathlib
import math
import zlib
from construct import this, len_, If, Struct, Probe, Rebuild, Const, Int8ul, Int8sl, Int16ul, Int16sl, Int32ul, Array, PrefixedArray, Computed, Switch, GreedyBytes, Enum, PaddedString, PascalString

MODE_PAL  = 985248
MODE_NTSC = 1022727
MIDI_A4 = 69
FREQ_A4 = 440.

def compute_system_total_channels(ctx):
    """Compute the number of channels in the system"""
    return {
        dmf_system.SYSTEM_GENESIS: 10,
        dmf_system.SYSTEM_GENESIS_EXT_CH3: 13,
        dmf_system.SYSTEM_SMS: 4,
        dmf_system.SYSTEM_GAMEBOY: 4,
        dmf_system.SYSTEM_PCENGINE: 6,
        dmf_system.SYSTEM_NES: 5,
        dmf_system.SYSTEM_C64_8580: 3,
        dmf_system.SYSTEM_C64_6581: 3,
        dmf_system.SYSTEM_YM2151: 13,
        dmf_system.SYSTEM_C64_8580_MODE: 3,
    }[ctx.system]

def compute_pattern_matrix_length(ctx):
    system_total_channels = compute_system_total_channels(ctx)
    return system_total_channels * ctx.rows_in_pattern_matrix

# Modes as per docs are wrong, see: http://www.deflemask.com/forum/general/dmf-specs/30/
dmf_system = Enum(
    Int8ul,
    SYSTEM_GENESIS=0x02,
    SYSTEM_GENESIS_EXT_CH3=0x12,
    SYSTEM_SMS=0x03,
    SYSTEM_GAMEBOY=0x04,
    SYSTEM_PCENGINE=0x05,
    SYSTEM_NES=0x06,
    SYSTEM_C64_8580=0x07,
    SYSTEM_C64_6581=0x17,
    SYSTEM_YM2151=0x08,

    SYSTEM_C64_8580_MODE=0x47
)

dmf_frames_mode = Enum(
    Int8ul,
    PAL = 0,
    NTSC = 1
)

dmf_instrument_mode = Enum(
    Int8ul,
    STANDARD = 0,
    FM = 1
)

dmf_note = Enum(Int16ul,
    Empty = 0,
    CSharp = 1,
    D = 2,
    DSharp = 3,
    E = 4,
    F = 5,
    FSharp = 6,
    G = 7,
    GSharp = 8,
    A = 9,
    ASharp = 10,
    B = 11,
    C = 12,
    Off = 100
)

dmf_instrument_c64 = Struct(
    "instrument_name" / PascalString(Int8ul, encoding="utf-8"),
    "instrument_mode" / dmf_instrument_mode,
    # Volume Macro
    "macro_volume" / PrefixedArray(Int8ul, Int32ul),
    "macro_volume_loop" / If(len_(this.macro_volume) > 0, Int8sl),
    # Arpeggio Macro
    "macro_arpeggio" / PrefixedArray(Int8ul, Int32ul),
    "macro_arpeggio_loop" / If(len_(this.macro_arpeggio) > 0, Int8sl),
    "macro_arpeggio_mode" / Int8ul,   # 0 = Normal, 1 = Fixed
    # Duty/Noise Macro
    "macro_duty" / PrefixedArray(Int8ul, Int32ul),
    "macro_duty_loop" / If(len_(this.macro_duty) > 0, Int8sl),
    # Wavetable Macro
    "macro_wavetable" / PrefixedArray(Int8ul, Int32ul),
    "macro_wavetable_loop" / If(len_(this.macro_wavetable) > 0, Int8sl),
    # Voices
    "triangle" / Int8ul,
    "saw" / Int8ul,
    "pulse" / Int8ul,
    "noise" / Int8ul,
    "attack" / Int8ul,
    "decay" / Int8ul,
    "sustain" / Int8ul,
    "release" / Int8ul,
    "pulse_width" / Int8ul,
    "ring_mod_en" / Int8ul,
    "sync_mod_en" / Int8ul,
    "to_filter" / Int8ul,
    "vmtfc_en" / Int8ul,         # Volume Macro To Filter Cutoff Enabled?
    "use_ins_filter" / Int8ul,
    # Filter Globals
    "resonance" / Int8ul,
    "cutoff" / Int8ul,
    "high_pass" / Int8ul,
    "band_pass" / Int8ul,
    "low_pass" / Int8ul,
    "ch3_off" / Int8ul
)

dmf_wavetable = Struct(
    "data" / PrefixedArray(Int32ul, Int32ul)
)

dmf_pattern = Struct(
    "effects_columns" / Int8ul,
    "patterns" / Array(this._.rows_in_pattern_matrix, Struct(
        "rows" / Array(this._._.rows_per_pattern, Struct(
            "note" / dmf_note,
            "octave" / Int16ul,
            "volume" / Int16sl,
            "effects" / Array(this._._.effects_columns, Struct(
                "effect_code" / Int16sl,
                "effect_value" / Int16sl
            )),
            "instrument" / Int16sl,
        )) 
    ))
)

dmf_container = Struct(
    # Format Flags
    Const(u".DelekDefleMask.", PaddedString(16, encoding="utf-8")),
    Const(0x18, Int8ul),           # File version, must be 24 (0x18) for DefleMask v0.12.0

    # System Set
    "system" / dmf_system,
    "system_total_channels" / Computed(compute_system_total_channels),

    # Visual Information
    "song_name" / PascalString(Int8ul, encoding="utf-8"),
    "song_author" / PascalString(Int8ul, encoding="utf-8"),
    "highlight_a" / Int8ul,       # Highlight A in patterns?
    "highlight_b" / Int8ul,       # Highlight B in patterns?

    # Module Information
    "time_base" / Int8ul,
    "tick_time_1" / Int8ul,
    "tick_time_2" / Int8ul,
    "frames_mode" / dmf_frames_mode,
    "custom_hz" / Int8ul,
    "custom_hz_1" / Int8ul,
    "custom_hz_2" / Int8ul,
    "custom_hz_3" / Int8ul,
    "rows_per_pattern" / Int32ul,
    "rows_in_pattern_matrix" / Int8ul,
    "pattern_matrix_length" / Computed(compute_pattern_matrix_length),

    # Pattern Matrix Values
    "pattern_matrix" / Array(this.pattern_matrix_length, Int8ul),

    # Instrument Data
    "instruments" / Switch(this.system, {
        dmf_system.SYSTEM_C64_8580_MODE: PrefixedArray(Int8ul, dmf_instrument_c64)
    }),

    # Wavetable Data
    "wavetables" / PrefixedArray(Int8ul, dmf_wavetable),

    # Patterns Data
    "channels" / Array(this.system_total_channels, dmf_pattern)
)

parser = argparse.ArgumentParser()
parser.add_argument("input_file", type=pathlib.Path, help="Input file")
parser.add_argument("--out", type=pathlib.Path, help="Output file")

args = parser.parse_args()

data = zlib.decompress(open(args.input_file, 'rb').read())

result = dmf_container.parse(data)

print(result.system_total_channels, result.rows_in_pattern_matrix, result.rows_per_pattern)

output = [[], [], []]

def note_to_midi(octave, note):
    if note == 0: # Note Empty
        return -1
    if note == 100: # Note Off
        return -2
    return (octave * 12) + (note - 1) + 13

def midi_to_pitch(midi_number):
    if midi_number == -1:
        return 0
    if midi_number == -2:
        return -1
    return FREQ_A4 * 2 ** ((midi_number - MIDI_A4) * (1./12.))

for pattern in range(result.rows_in_pattern_matrix):
    for row in range(result.rows_per_pattern):
        for channel in range(result.system_total_channels):
            data = result.channels[channel].patterns[pattern].rows[row]
            pitch = midi_to_pitch(note_to_midi(int(data.octave), int(data.note)))
            pitch = int(round(pitch, 0))
            output[channel] += [pitch]
            if data.note != dmf_note.Empty:
                print(f"{data.note}{data.octave} = {pitch}")

for channel in [0, 1, 2]:
    print(f"int16_t channel{channel}[{128*3}] = {{")
    for x in range(0, 128 * 3, 128):
        print(", ".join([str(n) for n in output[channel][x:x+128]]) + ", ")
    print("};")
exit(0)

for ci, channel in enumerate(result.channels):
    print(f"Channel {ci:d}")
    for pi, pattern in enumerate(channel.patterns):
        print(f"Pattern {pi:d}")
        for row in pattern.rows:
            if row.note != dmf_note.Empty:
                print(f"{row.note}{row.octave}")
            else:
                print("--")

#with open(args.out, 'wb') as output_file:
#    output_file.write(data)