# headers.py
# Generate headers for AMY
import os
import sys
import glob
try:
    import numpy as np
except ImportError:
    # The gamma9001 generator (`python3 -m amy.headers gamma9001`, run by the
    # Tulip build's fs_create.py inside the bare ESP-IDF python env) is
    # stdlib-only; everything else in this file needs numpy.
    np = None
import amy

from . import constants

def _read_wavetables(wavetable_files):
    # Lazy import: soundfile is only needed when (optionally) regenerating the PCM /
    # wavetable headers. Keeping it out of module scope means `python3 -m amy.headers`
    # (and therefore `make web`) does not require soundfile to be installed.
    import soundfile as sf
    tables = []
    wavetable_len = None
    for wavfile in wavetable_files:
        with open(wavfile, 'rb') as f:
            wav_data, samplerate = sf.read(f, dtype='int16')
        if getattr(wav_data, "ndim", 1) != 1:
            raise ValueError(f"{wavfile} is not mono")
        wav_data = wav_data.astype(np.int16)
        if wavetable_len is None:
            wavetable_len = len(wav_data)
        elif len(wav_data) != wavetable_len:
            raise ValueError(f"{wavfile} length {len(wav_data)} != {wavetable_len}")
        tables.append((wavfile, wav_data))
    return tables, wavetable_len


def generate_amy_pcm_header(sample_set, name, pcm_AMY_SAMPLE_RATE=22050, wavetable_files=None):
    from sf2utils.sf2parse import Sf2File
    import resampy
    import struct
    # Download the sf2 file here
    # https://github.com/vigliensoni/soundfonts/blob/master/hs_tr808/HS-TR-808-Drums.sf2
    fn = "sounds/HS-TR-808-Drums.sf2"
    offsets = []
    offset = 0
    int16s = []
    samples = []
    sample_counter = 0
    my_sample_counter = 0
    orig_map = {}
    try:
        sf2 = Sf2File(open(fn, 'rb'))
    except Exception as e:
        print("For PCM presets, download the sf2 files first. See comment in amy.headers.generate_amy_pcm_header()")
        return
    for sample in sf2.samples[:-1]:
        if(sample_counter in sample_set):
            samples.append(sample)
            orig_map[my_sample_counter] = sample_counter
            my_sample_counter += 1
        sample_counter += 1
 
    for sample in samples:
        try:
            s = {}
            s["name"] = sample.name
            floaty =(np.frombuffer(bytes(sample.raw_sample_data),dtype='int16'))/32768.0
            resampled = resampy.resample(floaty, sample.sample_rate, pcm_AMY_SAMPLE_RATE)
            # Make sure the float value doesn't cause overflow in int.  resampling can cause overshoot.
            samples_int16 = np.int16(np.minimum(32767.0, np.maximum(-32768.0, resampled*32768.0)))
            #floats.append(resampled)
            int16s.append(samples_int16)
            s["offset"] = offset 
            s["length"] = resampled.shape[0]
            s["loopstart"] = int(float(sample.start_loop) / float(sample.sample_rate / pcm_AMY_SAMPLE_RATE))
            s["loopend"] = int(float(sample.end_loop) / float(sample.sample_rate / pcm_AMY_SAMPLE_RATE))
            s["midinote"] = sample.original_pitch
            offset = offset + resampled.shape[0]
            offsets.append(s)
        except AttributeError:
            print("skipping %s" % (sample.name))
    
    all_samples = np.hstack(int16s)
    wavetable_tables = []
    wavetable_len = 0
    if wavetable_files is not None and len(wavetable_files) > 0:
        wavetable_tables, wavetable_len = _read_wavetables(wavetable_files)
    wavetable_count = len(wavetable_tables)
    base_samples = len(offsets)
    base_length = all_samples.shape[0]

    p = open("src/pcm_%s.h" % (name), "w")
    p.write("// Automatically generated by amy.headers.generate_pcm_header()\n")
    p.write("#ifndef __PCM_H\n#define __PCM_H\n")
    p.write("#define PCM_AMY_SAMPLE_RATE %d\n" % (pcm_AMY_SAMPLE_RATE))
    p.write("#define PCM_BASE_SAMPLES %d\n" % (base_samples))
    p.write("#define PCM_BASE_LENGTH %d\n" % (base_length))
    if wavetable_count > 0:
        p.write("#if defined(AMY_WAVETABLE)\n")
        p.write("#define PCM_WAVETABLE_BASE PCM_BASE_SAMPLES\n")
        p.write("#define PCM_WAVETABLE_SAMPLES %d\n" % (wavetable_count))
        p.write("#define PCM_WAVETABLE_LEN %d\n" % (wavetable_len))
        p.write("#define PCM_LENGTH (PCM_BASE_LENGTH + (PCM_WAVETABLE_SAMPLES * PCM_WAVETABLE_LEN))\n")
        p.write("#define PCM_MAP_ENTRIES (PCM_BASE_SAMPLES + PCM_WAVETABLE_SAMPLES)\n")
        p.write("#else\n")
        p.write("#define PCM_WAVETABLE_BASE PCM_BASE_SAMPLES\n")
        p.write("#define PCM_WAVETABLE_SAMPLES 0\n")
        p.write("#define PCM_WAVETABLE_LEN 0\n")
        p.write("#define PCM_LENGTH PCM_BASE_LENGTH\n")
        p.write("#define PCM_MAP_ENTRIES PCM_BASE_SAMPLES\n")
        p.write("#endif\n")
    else:
        p.write("#define PCM_WAVETABLE_BASE PCM_BASE_SAMPLES\n")
        p.write("#define PCM_WAVETABLE_SAMPLES 0\n")
        p.write("#define PCM_WAVETABLE_LEN 0\n")
        p.write("#define PCM_LENGTH PCM_BASE_LENGTH\n")
        p.write("#define PCM_MAP_ENTRIES PCM_BASE_SAMPLES\n")
    p.write("#include \"pcm_samples_%s.h\"\n" % (name))
    p.write("const uint16_t pcm_samples = PCM_MAP_ENTRIES;\n")
    p.write("const uint16_t pcm_wavetable_base = PCM_WAVETABLE_BASE;\n")
    p.write("const uint16_t pcm_wavetable_samples = PCM_WAVETABLE_SAMPLES;\n")
    p.write("const uint32_t pcm_wavetable_len = PCM_WAVETABLE_LEN;\n")
    p.write("const pcm_map_t pcm_map[PCM_MAP_ENTRIES] PROGMEM = {\n")
    for i,o in enumerate(offsets):
        p.write("    /* [%d] %d */ {%d, %d, %d, %d, %d}, /* %s */\n" %(i, orig_map[i], o["offset"], o["length"], o["loopstart"], o["loopend"], o["midinote"], o["name"]))
    if wavetable_count > 0:
        p.write("#if defined(AMY_WAVETABLE)\n")
        for i, (wavfile, wav_data) in enumerate(wavetable_tables):
            wav_index = base_samples + i
            wav_offset = base_length + i * wavetable_len
            wav_name = os.path.basename(wavfile)
            p.write("    /* [%d] WT */ {%d, %d, %d, %d, %d}, /* %s */\n" % (
                wav_index, wav_offset, wavetable_len, 0, wavetable_len, 69, wav_name))
        p.write("#endif\n")
    p.write("};\n")
    p.write("\n#endif  // __PCM_H\n")
    p.close()

    p = open("src/pcm_samples_%s.h" % (name), 'w')
    p.write("// Automatically generated by amy.headers.generate_pcm_header()\n")
    p.write("#ifndef __PCM_SAMPLES_H\n#define __PCM_SAMPLES_H\n")
    p.write("const int16_t pcm[PCM_LENGTH] PROGMEM = {")

    columns = 16
    for i, x in enumerate(all_samples):
        if (i % columns) == 0:
            p.write("\n")
        p.write(" %6d," % x)
    p.write("\n")
    if wavetable_count > 0:
        p.write("#if defined(AMY_WAVETABLE)\n")
        for wavfile, wav_data in wavetable_tables:
            p.write("    // %s" % (wavfile))
            for i, x in enumerate(wav_data):
                if (i % columns) == 0:
                    p.write("\n")
                p.write(" %6d," % x)
        p.write("\n")
        p.write("#endif\n")
    p.write("};\n")
    p.write("\n#endif  // __PCM_SAMPLES_H\n")


def generate_pcm_headers():
    tiny = [0, 3, 8, 11, 14, 16, 17, 18, 20, 23, 25]
    wavetable_files = sorted(glob.glob('sounds/wavetables/*.WAV'))
    generate_amy_pcm_header(tiny, "tiny", wavetable_files=wavetable_files)


# ------------- Gamma9001 drum banks (sounds/gamma9001) -------------
#
# The Gamma9001 sample set (Koblo Tokyo drum machines + the 808 SoundFont
# gap-fill + the AG-10 Power Kit) lives as 22050 Hz mono wavs in
# sounds/gamma9001/, described by its manifest.json. It generates:
#
#  - src/pcm_gamma808.h + src/pcm_samples_gamma808.h: the TR-808 bank baked
#    the same way as pcm_tiny.h (a drop-in replacement for it, wavetables
#    included), for builds that want the full 808 kit as their ROM set.
#  - src/pcm_gamma9001.h + build/drums.bin: the remaining banks as a
#    pcm_map_t table over a raw int16 blob. Only meaningful when GAMMA9001
#    is defined; the blob is linked into the wasm on web, or flashed as a
#    partition and mmapped on ESP32-S3 (Tulip / AMYboard).
#
# Unlike the sf2 path this needs no optional deps: the wavs are already at
# PCM_AMY_SAMPLE_RATE and read with the stdlib wave module.

GAMMA9001_PRESET_BASE = 256   # gamma presets live at BASE+index, clear of any ROM set
GAMMA9001_ROM_BANK = 'tr808'  # this bank is baked; the rest go in drums.bin


def _read_wav_mono16(path, expect_sr=None):
    # stdlib-only (no numpy): returns an array('h') of samples.
    import wave as wavmod
    from array import array
    w = wavmod.open(path)
    assert w.getnchannels() == 1 and w.getsampwidth() == 2, path
    if expect_sr is not None:
        assert w.getframerate() == expect_sr, f'{path}: {w.getframerate()} != {expect_sr}'
    data = array('h')
    data.frombytes(w.readframes(w.getnframes()))
    if sys.byteorder == 'big':
        data.byteswap()  # wav data is little-endian
    return data


def _read_wavetables_stdlib(wavetable_files):
    # Like _read_wavetables but without the soundfile dependency.
    tables = []
    wavetable_len = None
    for wavfile in wavetable_files:
        wav_data = _read_wav_mono16(wavfile)
        if wavetable_len is None:
            wavetable_len = len(wav_data)
        elif len(wav_data) != wavetable_len:
            raise ValueError(f"{wavfile} length {len(wav_data)} != {wavetable_len}")
        tables.append((wavfile, wav_data))
    return tables, wavetable_len


def _int16_le_bytes(data):
    # array('h') holds native-order values; emit little-endian bytes.
    if sys.byteorder == 'big':
        data = data[:]
        data.byteswap()
    return data.tobytes()


def _write_int16_carray(p, data, column=15):
    for i in range(int(len(data) / column)):
        p.write("    %s,\n" % (",".join([("%d" % (d)).ljust(8) for d in data[i * column:(i + 1) * column]])))
    rem = int(len(data) % column)
    if rem != 0:
        p.write("    %s,\n" % (",".join([("%d" % (d)).ljust(8) for d in data[-rem:]])))


def generate_gamma9001_headers(sounds_dir='sounds/gamma9001', bin_path='build/drums.bin',
                               pcm_AMY_SAMPLE_RATE=22050):
    import json
    manifest = json.load(open(os.path.join(sounds_dir, 'manifest.json')))
    rom = [m for m in manifest if m['bank'] == GAMMA9001_ROM_BANK]
    bin_entries = [m for m in manifest if m['bank'] != GAMMA9001_ROM_BANK]

    # ---- baked ROM bank (pcm_tiny.h drop-in format) ----
    wavetable_files = sorted(glob.glob('sounds/wavetables/*.WAV'))
    wavetable_tables, wavetable_len = _read_wavetables_stdlib(wavetable_files)
    wavetable_count = len(wavetable_tables)
    datas, offset = [], 0
    for m in rom:
        data = _read_wav_mono16(os.path.join(sounds_dir, m['file']), pcm_AMY_SAMPLE_RATE)
        m['_offset'], m['_data'] = offset, data
        offset += len(data)
    base_length = offset

    p = open("src/pcm_gamma808.h", "w")
    p.write("// Automatically generated by amy.headers.generate_gamma9001_headers()\n")
    p.write("#ifndef __PCM_H\n#define __PCM_H\n")
    p.write("#define PCM_AMY_SAMPLE_RATE %d\n" % pcm_AMY_SAMPLE_RATE)
    p.write("#define PCM_BASE_SAMPLES %d\n" % len(rom))
    p.write("#define PCM_BASE_LENGTH %d\n" % base_length)
    p.write("#if defined(AMY_WAVETABLE)\n")
    p.write("#define PCM_WAVETABLE_BASE PCM_BASE_SAMPLES\n")
    p.write("#define PCM_WAVETABLE_SAMPLES %d\n" % wavetable_count)
    p.write("#define PCM_WAVETABLE_LEN %d\n" % wavetable_len)
    p.write("#define PCM_LENGTH (PCM_BASE_LENGTH + (PCM_WAVETABLE_SAMPLES * PCM_WAVETABLE_LEN))\n")
    p.write("#define PCM_MAP_ENTRIES (PCM_BASE_SAMPLES + PCM_WAVETABLE_SAMPLES)\n")
    p.write("#else\n")
    p.write("#define PCM_WAVETABLE_BASE PCM_BASE_SAMPLES\n")
    p.write("#define PCM_WAVETABLE_SAMPLES 0\n")
    p.write("#define PCM_WAVETABLE_LEN 0\n")
    p.write("#define PCM_LENGTH PCM_BASE_LENGTH\n")
    p.write("#define PCM_MAP_ENTRIES PCM_BASE_SAMPLES\n")
    p.write("#endif\n")
    p.write("#include \"pcm_samples_gamma808.h\"\n")
    p.write("const uint16_t pcm_samples = PCM_MAP_ENTRIES;\n")
    p.write("const uint16_t pcm_wavetable_base = PCM_WAVETABLE_BASE;\n")
    p.write("const uint16_t pcm_wavetable_samples = PCM_WAVETABLE_SAMPLES;\n")
    p.write("const uint32_t pcm_wavetable_len = PCM_WAVETABLE_LEN;\n")
    p.write("const pcm_map_t pcm_map[PCM_MAP_ENTRIES] PROGMEM = {\n")
    for i, m in enumerate(rom):
        p.write("    /* [%d] */ {%d, %d, 0, 0, %d}, /* %s */\n" % (
            i, m['_offset'], len(m['_data']), m['root'], m['name']))
    p.write("#if defined(AMY_WAVETABLE)\n")
    for i, (wavfile, wav_data) in enumerate(wavetable_tables):
        p.write("    /* [%d] WT */ {%d, %d, %d, %d, %d}, /* %s */\n" % (
            len(rom) + i, base_length + i * wavetable_len, wavetable_len, 0, wavetable_len, 69,
            os.path.basename(wavfile)))
    p.write("#endif\n")
    p.write("};\n")
    p.write("\n#endif  // __PCM_H\n")
    p.close()

    p = open("src/pcm_samples_gamma808.h", 'w')
    p.write("// Automatically generated by amy.headers.generate_gamma9001_headers()\n")
    p.write("#ifndef __PCM_SAMPLES_H\n#define __PCM_SAMPLES_H\n")
    p.write("const int16_t pcm[PCM_LENGTH] PROGMEM = {\n")
    for m in rom:
        _write_int16_carray(p, m['_data'])
    p.write("#if defined(AMY_WAVETABLE)\n")
    for wavfile, wav_data in wavetable_tables:
        p.write("    // %s\n" % wavfile)
        _write_int16_carray(p, wav_data)
    p.write("#endif\n")
    p.write("};\n")
    p.write("\n#endif  // __PCM_SAMPLES_H\n")
    p.close()

    # ---- drums.bin + map for the remaining banks ----
    os.makedirs(os.path.dirname(bin_path), exist_ok=True)
    b = open(bin_path, 'wb')
    offset = 0
    for m in bin_entries:
        data = _read_wav_mono16(os.path.join(sounds_dir, m['file']), pcm_AMY_SAMPLE_RATE)
        b.write(_int16_le_bytes(data))
        m['_offset'], m['_frames'] = offset, len(data)
        offset += len(data)
    b.close()

    p = open("src/pcm_gamma9001.h", "w")
    p.write("// Automatically generated by amy.headers.generate_gamma9001_headers()\n")
    p.write("// Map of the Gamma9001 drum banks over the raw int16 blob in drums.bin.\n")
    p.write("// The blob is provided at runtime via gamma9001_pcm (linked-in on web,\n")
    p.write("// mmapped from a flash partition on ESP32-S3). Only used when GAMMA9001\n")
    p.write("// is defined.\n")
    p.write("#ifndef __PCM_GAMMA9001_H\n#define __PCM_GAMMA9001_H\n")
    p.write("#define GAMMA9001_PRESET_BASE %d\n" % GAMMA9001_PRESET_BASE)
    p.write("#define GAMMA9001_NUM_SAMPLES %d\n" % len(bin_entries))
    p.write("#define GAMMA9001_SAMPLE_RATE %d\n" % pcm_AMY_SAMPLE_RATE)
    p.write("#define GAMMA9001_BIN_FRAMES %d\n" % offset)
    p.write("const pcm_map_t gamma9001_map[GAMMA9001_NUM_SAMPLES] PROGMEM = {\n")
    for i, m in enumerate(bin_entries):
        p.write("    /* [%d] preset %d */ {%d, %d, 0, 0, %d}, /* %s: %s */\n" % (
            i, GAMMA9001_PRESET_BASE + i, m['_offset'], m['_frames'], m['root'],
            m['bank'], m['name']))
    p.write("};\n")
    p.write("\n#endif  // __PCM_GAMMA9001_H\n")
    p.close()

    # drums.bin as a linkable C array, for targets that bake the banks into the
    # binary (the wasm build). ESP32-S3 flashes drums.bin as a partition instead.
    c_path = os.path.join(os.path.dirname(bin_path), 'drums_bin.c')
    p = open(c_path, 'w')
    p.write("// Automatically generated by amy.headers.generate_gamma9001_headers()\n")
    p.write("// The Gamma9001 drums.bin blob as C data; see src/pcm_gamma9001.h for the map.\n")
    p.write("#include <stdint.h>\n")
    p.write("const int16_t gamma9001_pcm_data[%d] = {\n" % offset)
    for m in bin_entries:
        data = _read_wav_mono16(os.path.join(sounds_dir, m['file']), pcm_AMY_SAMPLE_RATE)
        p.write("    // %s: %s\n" % (m['bank'], m['name']))
        _write_int16_carray(p, data)
    p.write("};\n")
    p.close()

    print("gamma9001: %d ROM samples -> pcm_gamma808.h, %d samples (%.2f MB) -> %s + pcm_gamma9001.h + %s"
          % (len(rom), len(bin_entries), offset * 2 / 1e6, bin_path, c_path))

def cos_lut(table_size, harmonics_weights, harmonics_phases=None):
    if harmonics_phases is None:
        harmonics_phases = np.zeros(len(harmonics_weights))
    table = np.zeros(table_size)
    phases = np.arange(table_size) * 2 * np.pi / table_size
    for harmonic_number, harmonic_weight in enumerate(harmonics_weights):
        table += harmonic_weight * np.cos(
            phases * harmonic_number + harmonics_phases[harmonic_number])
    return table


# A LUTset is a list of LUTentries describing downsampled versions of the same
# basic waveform, sorted with the longest (highest-bandwidth) first.
def create_lutset(LUTentry, harmonic_weights, harmonic_phases=None, 
                  length_factor=8, bandwidth_factor=None):
    if bandwidth_factor is None:
        bandwidth_factor = np.sqrt(0.5)
    """Create an ordered list of LUTs with decreasing harmonic content.

    These can then be used in interp_from_lutset to make an adaptive-bandwidth
    interpolation.

    Args:
        harmonic_weights: vector of amplitudes for cosine harmonic components.
        harmonic_phases: initial phases for each harmonic, in radians. Zero 
            (default) indicates cosine phase.
        length_factor: Each table's length is at least this factor times the order
            of the highest harmonic it contains. Thus, this is a lower bound on the
            number of samples per cycle for the highest harmonic. Higher factors make
            the interpolation easier.
        bandwidth_factor: Target ratio between the highest harmonics in successive
            table entries. Default is sqrt(0.5), so after two tables, bandwidth is
            reduced by 1/2 (and length with follow).

    Returns:
        A list of LUTentry objects, sorted in decreasing order of the highest 
        harmonic they contain. Each LUT's length is a power of 2, and as small as
        possible while respecting the length_factor for the highest contained 
        harmonic.
    """
    if harmonic_phases is None:
        harmonic_phases = np.zeros(len(harmonic_weights))
    # Calculate the length of the longest LUT we need. Must be a power of 2, 
    # must have at least length_factor * highest_harmonic samples.
    # Harmonic 0 (dc) doesn't count.
    float_num_harmonics = float(len(harmonic_weights))
    lutsets = []
    done = False
    # harmonic 0 is DC; there's no point in generating that table.
    while float_num_harmonics >= 2:
        num_harmonics = int(round(float_num_harmonics))
        highest_harmonic = num_harmonics - 1    # because zero doesn't count.
        lut_size = int(2 ** np.ceil(np.log(length_factor * highest_harmonic) / np.log(2)))
        lutsets.append(LUTentry(
                table=cos_lut(lut_size, harmonic_weights[:num_harmonics], 
                                harmonic_phases[:num_harmonics]),    # / lut_size,
                highest_harmonic=highest_harmonic))
        float_num_harmonics = bandwidth_factor * float_num_harmonics
    return lutsets


def make_table(min_val, max_val, fn, table_size=257, dtype=None):
    if dtype is None:
        dtype = np.int16
    # The table includes the final value, so the actual number of steps is table_size - 1.
    steps = table_size - 1
    stepsize = (max_val - min_val) / steps
    return fn(np.arange(min_val, max_val + stepsize, stepsize)).astype(dtype)


def create_exp2_lut(npts):
    exp2_int16_fn = lambda x: np.round(-32768.0 * (np.exp2(x) - 1.0))
    return make_table(0, 1, exp2_int16_fn, table_size=npts, dtype=np.int16)


def create_log2_lut(npts):
    log2_int16_fn = lambda x: np.round(-32768.0 * (np.log2(x + 1.0)))
    return make_table(0, 1, log2_int16_fn, table_size=npts, dtype=np.int16)


def write_lutset_to_h(filename, variable_base, lutset):
    """Savi out a lutset as a C-compatible header file."""
    num_luts = len(lutset)
    with open(filename, "w") as f:
        f.write("// Automatically-generated LUTset\n")
        f.write("#ifndef LUTSET_{:s}_DEFINED\n".format(variable_base.upper()))
        f.write("#define LUTSET_{:s}_DEFINED\n".format(variable_base.upper()))
        f.write("\n")
        # Define the structure.
        f.write("#ifndef LUTENTRY_DEFINED\n")
        f.write("#define LUTENTRY_DEFINED\n")
        f.write("typedef struct {\n")
        f.write("    const float *table;\n")
        f.write("    int table_size;\n")
        f.write("    int highest_harmonic;\n")
        f.write("} lut_entry;\n")
        f.write("#endif // LUTENTRY_DEFINED\n")
        f.write("\n")
        # Define the content of the individual tables.
        samples_per_row = 8
        for i in range(num_luts):
            table_size = len(lutset[i].table)
            f.write("const float {:s}_lutable_{:d}[{:d}] PROGMEM = {{\n".format(
                variable_base, i, table_size))
            for row_start in range(0, table_size, samples_per_row):
                for sample_index in range(row_start,
                        min(row_start + samples_per_row, table_size)):
                    f.write("{:f},".format(lutset[i].table[sample_index]))
                f.write("\n")
            f.write("};\n")
            f.write("\n")
        # Define the table of LUTs.
        f.write("lut_entry {:s}_lutset[{:d}] = {{\n".format(
            variable_base, num_luts + 1))
        for i in range(num_luts):
            f.write("    {{{:s}_lutable_{:d}, {:d}, {:d}}},\n".format(
                variable_base, i, len(lutset[i].table), 
                lutset[i].highest_harmonic))
        # Final entry is null to indicate end of table.
        f.write("    {NULL, 0, 0},\n")
        f.write("};\n")
        f.write("\n")
        f.write("#endif // LUTSET_x_DEFINED\n")
    print("wrote", filename)


def write_fxpt_lutable(f, lutable, name, samples_per_row=8):
    """Write a single lutable to an open file."""
    table_size = len(lutable)
    scale_factor = np.max(np.abs(lutable.astype(float)))
    f.write("const int16_t {:s}[{:d}] PROGMEM = {{\n".format(
        name, table_size))
    for row_start in range(0, table_size, samples_per_row):
        for sample_index in range(row_start,
                                  min(row_start + samples_per_row, table_size)):
            f.write("{:d},".format(
                min(32767,
                    max(-32768,
                        int(round(32768 / scale_factor * lutable[sample_index]))))))
        f.write("\n")
    f.write("};\n")
    f.write("\n")
    return scale_factor


def write_lutset_to_h_as_fxpt(filename, variable_base, lutset):
    """Save out a lutset as a C-compatible header file using ints."""
    import math
    num_luts = len(lutset)
    with open(filename, "w") as f:
        f.write("// Automatically-generated LUTset\n")
        f.write("#ifndef LUTSET_{:s}_FXPT_DEFINED\n".format(variable_base.upper()))
        f.write("#define LUTSET_{:s}_FXPT_DEFINED\n".format(variable_base.upper()))
        f.write("\n")
        # Define the structure.
        f.write("#ifndef LUTENTRY_FXPT_DEFINED\n")
        f.write("#define LUTENTRY_FXPT_DEFINED\n")
        f.write("typedef struct {\n")
        f.write("    const int16_t *table;\n")
        f.write("    int table_size;\n")
        f.write("    int log_2_table_size;\n")
        f.write("    int highest_harmonic;\n")
        f.write("    float scale_factor;\n")
        f.write("} lut_entry_fxpt;\n")
        f.write("#endif // LUTENTRY_FXPT_DEFINED\n")
        f.write("\n")
        # Define the content of the individual tables.
        scale_factors = []
        for i in range(num_luts):
            scale_factor = write_fxpt_lutable(
                f, lutset[i].table,
                '{:s}_fxpt_lutable_{:d}'.format(variable_base, i)
            )
            scale_factors.append(scale_factor)
        # Define the table of LUTs.
        f.write("lut_entry_fxpt {:s}_fxpt_lutset[{:d}] = {{\n".format(
            variable_base, num_luts + 1))
        for i in range(num_luts):
            table_size = len(lutset[i].table)
            # Provide the shift size corresponding to the lutset.
            log_2_table_size = int(round(math.log(table_size) / math.log(2.0)))
            f.write("    {{{:s}_fxpt_lutable_{:d}, {:d}, {:d}, {:d}, {:f}}},\n".format(
                variable_base, i, table_size, log_2_table_size,
                lutset[i].highest_harmonic, scale_factors[i]))
        # Final entry is null to indicate end of table.
        f.write("    {NULL, 0, 0, 0, 0.0},\n")
        f.write("};\n")
        f.write("\n")
        f.write("#endif // LUTSET_x_DEFINED\n")
    print("wrote", filename)


def make_log2_exp2_luts(filename):
    """Write the fixed-point exp2 and log2 lookup tables."""
    variable_base = 'exp_lut'
    with open(filename, "w") as f:
        f.write("// Automatically-generated LUTset\n")
        f.write("#ifndef LUTSET_{:s}_FXPT_DEFINED\n".format(variable_base.upper()))
        f.write("#define LUTSET_{:s}_FXPT_DEFINED\n".format(variable_base.upper()))
        f.write("\n")
        # Define the content of the individual tables.
        write_fxpt_lutable(f, create_log2_lut(257), 'log2_fxpt_lutable')
        write_fxpt_lutable(f, create_exp2_lut(257), 'exp2_fxpt_lutable')
        f.write("\n")
        f.write("#endif // LUTSET_x_DEFINED\n")
    print("wrote", filename)


def make_clipping_lut(filename):
    # Soft clipping lookup table scratchpad.
    SAMPLE_MAX = 32767
    linear_proportion = 0.9  # I tried 0.6 and you could hear the difference but not enough to matter.
    LIN_MAX = int(round(linear_proportion * 32768))  # 29491
    NONLIN_RANGE = round(1.5 * (32767 - LIN_MAX))  # size of nonlinearity lookup table = 4915

    clipping_lookup_table = np.arange(LIN_MAX + NONLIN_RANGE)

    for x in range(NONLIN_RANGE):
        x_dash = float(x) / NONLIN_RANGE
        clipping_lookup_table[x + LIN_MAX] = LIN_MAX + int(np.floor(NONLIN_RANGE * (x_dash - x_dash * x_dash * x_dash / 3.0)))

    with open(filename, "w") as f:
        f.write("// Automatically generated.\n// Clipping lookup table\n")
        f.write("#ifndef __CLIPPING_TABLE\n#define __CLIPPING_TABLE\n")
        f.write("#define FIRST_NONLIN %d\n" % LIN_MAX)
        f.write("#define NONLIN_RANGE %d\n" % NONLIN_RANGE)
        f.write("// First sample value beyond end of table (just clip to max).\n")
        f.write("#define FIRST_HARDCLIP (FIRST_NONLIN + NONLIN_RANGE)\n")
        f.write("const uint16_t clipping_lookup_table[NONLIN_RANGE] PROGMEM = {\n")
        samples_per_row = 8
        for row_start in range(0, NONLIN_RANGE, samples_per_row):
            for sample in range(row_start, min(NONLIN_RANGE, row_start + samples_per_row)):
                f.write("%d," % clipping_lookup_table[LIN_MAX + sample])
            f.write("\n")
        f.write("};\n")
        f.write("#endif\n")
    print("wrote", filename)


def make_piano_patch():
    import amy
    # This just allocates the 20 oscs needed for a INTERP_PARTIALS patch
    # dpwe wants to add a `num_suboscs` field to fix this behavior soon
    amy.send(chorus=0) # Piano sounds weird with chorus on
    amy.send(osc=0, wave=amy.INTERP_PARTIALS, preset=0, amp='1,0,0,0')  # Parent osc amp applies, disconnect vel and eg0.
    #amy.send(osc=20, wave=amy.PARTIAL)  # it's not 20, but it doesn't matter, voice is reallocated by interp_partials.c
    return 25  # We now use up to 24 partials per voice + 1 control osc.

amyboard_patch_cc_defs = [
    'ic7,1,0.001,7,0.1,i%iv0a%vZ',                     # channel level (CTL osc amp)
    'ic74,1,20,8000,0,i%iv0F%vZ',                      # VCF freq
    'ic71,1,0.5,16,0,i%iv0R%vZ',                       # VCF resonance
    'ic76,1,0.1,20,0,i%iv1f%vZ',                       # LFO freq
    'ic77,1,0,4,0.001,i%iv2f,,,,,%vZi%iv3f,,,,,%vZ',   # LFO -> osc A/B depth
    'ic93,1,0,1,0.1,k%vZ',                             # chorus level
    'ic73,0,0,1000,0,i%iv0A%v,1,,,,0Z',                # ADSR attack
    'ic75,1,0,2000,50,i%iv0A,1,%v,,,0Z',               # ADSR decay
    'ic79,0,0,1,0,i%iv0A,1,,%v,,0Z',                   # ADSR sustain
    'ic72,1,0,8000,50,i%iv0A,1,,,%v,0Z',               # ADSR release
    'ic91,1,0,1,0.1,h%vZ',                             # reverb level
    'ic94,0,0,2,0,M%vZ',                               # echo level
]

def make_amyboard_patch():
    import amy
    # Modified for "silent control osc"
    ctl_osc = 0
    lfo_osc = 1
    osc_a = 2
    osc_b = 3
    message = amy.message(osc=ctl_osc, wave=amy.SILENT, mod_source=lfo_osc, chained_osc=osc_a,
                          filter_type=amy.FILTER_LPF24, filter_freq={'const': 200.0, 'note': 1.0, 'eg1': 5.0},
                          resonance=0.7, bp0='0,1,1000,0.2,100,0', bp1='0,1,1000,0.2,1000,0')
    message += amy.message(osc=osc_a, wave=amy.PULSE, amp={'vel': 0, 'eg0': 0}, mod_source=lfo_osc, chained_osc=osc_b)
    message += amy.message(osc=osc_b, wave=amy.SAW_DOWN, amp={'vel': 0, 'eg0': 0}, mod_source=lfo_osc, freq={'const': constants.ZERO_LOGFREQ_IN_HZ / 2})
    message += amy.message(osc=lfo_osc, wave=amy.TRIANGLE, amp={'vel': 0}, freq={'const': 4.0, 'note': 0, 'bend': 0}, bp0='0,1.0,10000,0')
    message += amy.message(eq=[0, 0, 0], chorus='0,,0.5,0.5')
    for cc_def in amyboard_patch_cc_defs:
        message += cc_def + 'Z'
    return 4, message # 4 oscs


# Default PCM patch mappings for MIDI CH 10 (drums) patches.
# (used to be in patches.c)

AMY_MIDI_DRUMS_LOWEST_NOTE = 35
AMY_MIDI_DRUMS_HIGHEST_NOTE = 81

# drumkit[midi_note - AMY_MIDI_DRUMS_LOWEST_NOTE] == {pcm_patch_number, base_midi_note}

# PCM presets available (from pcm_tiny.h):
#  [0]  808-MARACA    root=89
#  [1]  808-KIK 4     root=39
#  [2]  808-SNR 4     root=45
#  [3]  808-SNR 7     root=52
#  [4]  808-SNR 10    root=51
#  [5]  808-SNR 12    root=41
#  [6]  808-C-HAT1    root=53
#  [7]  808-O-HAT1    root=56
#  [8]  808-LTOM M    root=61
#  [9]  808-DRYCLP    root=94
#  [10] 808-CWBELL    root=69
# DRUMKIT[midi_note_number - AMY_MIDI_DRUMS_LOWEST_NOTE] == (pcm_preset_number, base_midi_note)
DRUMKIT = [
    (1, 39),   # 35 Acoustic Bass Drum -> 808-KIK
    (1, 39),   # 36 Bass Drum 1        -> 808-KIK
    (4, 51),   # 37 Side Stick         -> 808-SNR 10 (rimshot-like)
    (2, 45),   # 38 Acoustic Snare     -> 808-SNR 4
    (9, 94),   # 39 Hand Clap          -> 808-DRYCLP
    (5, 41),   # 40 Electric Snare     -> 808-SNR 12
    (8, 56),   # 41 Low Floor Tom      -> 808-LTOM (pitched down)
    (6, 53),   # 42 Closed Hi Hat      -> 808-C-HAT1
    (8, 61),   # 43 High Floor Tom     -> 808-LTOM (root)
    (7, 61),   # 44 Pedal Hi-Hat       -> 808-O-HAT1 (short)
    (8, 56),   # 45 Low Tom            -> 808-LTOM (pitched down)
    (7, 56),   # 46 Open Hi-Hat        -> 808-O-HAT1
    (8, 63),   # 47 Low-Mid Tom        -> 808-LTOM (slightly up)
    (8, 68),   # 48 Hi Mid Tom         -> 808-LTOM (pitched up)
    (7, 46),   # 49 Crash Cymbal 1     -> 808-O-HAT1 (pitched down for wash)
    (8, 73),   # 50 High Tom           -> 808-LTOM (pitched up high)
    (7, 51),   # 51 Ride Cymbal 1      -> 808-O-HAT1 (pitched down)
    (7, 48),   # 52 Chinese Cymbal     -> 808-O-HAT1 (pitched down)
    (6, 47),   # 53 Ride Bell          -> 808-C-HAT1 (pitched down, bell-like)
    (0, 79),   # 54 Tambourine         -> 808-MARACA (pitched down)
    (7, 46),   # 55 Splash Cymbal      -> 808-O-HAT1 (pitched down)
    (10, 69),  # 56 Cowbell            -> 808-CWBELL
    (7, 48),   # 57 Crash Cymbal 2     -> 808-O-HAT1 (pitched down)
    (-1, -1),  # 58 Vibraslap
    (7, 53),   # 59 Ride Cymbal 2      -> 808-O-HAT1 (pitched down)
    (-1, -1),  # 60 Hi Bongo
    (-1, -1),  # 61 Low Bongo
    (-1, -1),  # 62 Mute Hi Conga
    (-1, -1),  # 63 Open Hi Conga
    (-1, -1),  # 64 Low Conga
    (8, 73),   # 65 High Timbale       -> 808-LTOM (pitched up)
    (8, 63),   # 66 Low Timbale        -> 808-LTOM (slightly up)
    (10, 76),  # 67 High Agogo         -> 808-CWBELL (pitched up)
    (10, 64),  # 68 Low Agogo          -> 808-CWBELL (pitched down)
    (0, 79),   # 69 Cabasa             -> 808-MARACA (pitched down)
    (0, 89),   # 70 Maracas            -> 808-MARACA (root)
    (-1, -1),  # 71 Short Whistle
    (-1, -1),  # 72 Long Whistle
    (-1, -1),  # 73 Short Guiro
    (-1, -1),  # 74 Long Guiro
    (-1, -1),  # 75 Claves
    (10, 76),  # 76 Hi Wood Block      -> 808-CWBELL (pitched up)
    (10, 64),  # 77 Low Wood Block     -> 808-CWBELL (pitched down)
    (-1, -1),  # 78 Mute Cuica
    (-1, -1),  # 79 Open Cuica
    (-1, -1),  # 80 Mute Triangle
    (-1, -1),  # 81 Open Triangle
]


# GM drum map for the Gamma9001 baked TR-808 ROM bank (pcm_gamma808.h).
# Uses ONLY ROM presets 0-18 so channel 10 works even when the drums.bin
# banks aren't mounted. Preset order/roots from pcm_gamma808.h:
#  [0-2] BD 1/2/3   [3] clap  [4] clave  [5-7] conga hi/lo/mid  [8] cowbell
#  [9] CH  [10] OH  [11] shaker  [12-14] snare 1(45)/2(52)/3(41)
#  [15] rimshot(90) [16] tom lo(61) [17] tom hi(67) [18] cymbal(59)
GAMMA_DRUMKIT = [
    (0, 60),    # 35 Acoustic Bass Drum -> BD 1 (the long 3s boom)
    (2, 60),    # 36 Bass Drum 1        -> BD 3 (short + punchy, closest to the old kit's kick)
    (15, 90),   # 37 Side Stick         -> rimshot
    (12, 45),   # 38 Acoustic Snare     -> snare 1
    (3, 60),    # 39 Hand Clap          -> clap
    (14, 41),   # 40 Electric Snare     -> snare 3
    (16, 56),   # 41 Low Floor Tom      -> tom lo (down)
    (9, 60),    # 42 Closed Hi Hat      -> CH
    (16, 59),   # 43 High Floor Tom     -> tom lo (down a bit)
    (9, 57),    # 44 Pedal Hi-Hat       -> CH (darker)
    (16, 61),   # 45 Low Tom            -> tom lo (root)
    (10, 60),   # 46 Open Hi-Hat        -> OH
    (17, 64),   # 47 Low-Mid Tom        -> tom hi (down)
    (17, 67),   # 48 Hi Mid Tom         -> tom hi (root)
    (18, 59),   # 49 Crash Cymbal 1     -> cymbal (root)
    (17, 70),   # 50 High Tom           -> tom hi (up)
    (18, 66),   # 51 Ride Cymbal 1      -> cymbal (up, shorter)
    (18, 54),   # 52 Chinese Cymbal     -> cymbal (down, trashy)
    (8, 67),    # 53 Ride Bell          -> cowbell (up)
    (11, 57),   # 54 Tambourine         -> shaker (down)
    (18, 63),   # 55 Splash Cymbal      -> cymbal (up)
    (8, 60),    # 56 Cowbell            -> cowbell
    (18, 57),   # 57 Crash Cymbal 2     -> cymbal (down)
    (-1, -1),   # 58 Vibraslap
    (18, 69),   # 59 Ride Cymbal 2      -> cymbal (up more)
    (5, 65),    # 60 Hi Bongo           -> conga hi (up)
    (5, 61),    # 61 Low Bongo          -> conga hi (up a little)
    (7, 60),    # 62 Mute Hi Conga      -> conga mid
    (5, 60),    # 63 Open Hi Conga      -> conga hi
    (6, 60),    # 64 Low Conga          -> conga lo
    (17, 69),   # 65 High Timbale       -> tom hi (up)
    (16, 63),   # 66 Low Timbale        -> tom lo (up)
    (8, 64),    # 67 High Agogo         -> cowbell (up)
    (8, 57),    # 68 Low Agogo          -> cowbell (down)
    (11, 60),   # 69 Cabasa             -> shaker
    (11, 62),   # 70 Maracas            -> shaker (up)
    (-1, -1),   # 71 Short Whistle
    (-1, -1),   # 72 Long Whistle
    (-1, -1),   # 73 Short Guiro
    (-1, -1),   # 74 Long Guiro
    (4, 60),    # 75 Claves             -> clave
    (4, 64),    # 76 Hi Wood Block      -> clave (up)
    (4, 57),    # 77 Low Wood Block     -> clave (down)
    (-1, -1),   # 78 Mute Cuica
    (-1, -1),   # 79 Open Cuica
    (-1, -1),   # 80 Mute Triangle
    (-1, -1),   # 81 Open Triangle
]


# ---- Gamma9001 drum kits: patches 384+ (MIDI program change bank MSB 3) ----
#
# Each kit is a GM 35-81 map like DRUMKIT, but defined by sample *file* so it
# survives preset renumbering; files resolve to preset numbers through
# sounds/gamma9001/manifest.json (tr808 -> baked ROM index, others ->
# GAMMA9001_PRESET_BASE + drums.bin index). Kit 0 (TR-808) is GAMMA_DRUMKIT,
# which uses only ROM presets so the default kit works without drums.bin.
#
# Machine kits share one GM latin/aux percussion section, GS-style.
def _kit(core):
    # dict(base) + update instead of {**a, **b}: this file is frozen into the
    # Tulip firmware and mpy-cross does not parse dict-display unpacking.
    kit = dict(GAMMA_SHARED_PERC)
    kit.update(core)
    return kit

GAMMA_SHARED_PERC = {
    54: ('acoustic/tambourine-short.wav', 60),
    60: ('percussion/bongo-003.wav', 62),
    61: ('percussion/bongo-003.wav', 57),
    62: ('percussion/african-conga-003.wav', 60),
    63: ('percussion/conga-001.wav', 63),
    64: ('percussion/conga-001.wav', 57),
    65: ('percussion/timbales-001.wav', 60),
    66: ('percussion/timbales-004.wav', 57),
    67: ('linn9000/cowbell-1.wav', 64),
    68: ('linn9000/cowbell-1.wav', 57),
    69: ('percussion/simple-shaker.wav', 60),
    70: ('percussion/simple-shaker-002.wav', 60),
    71: ('percussion/wistle-001.wav', 60),
    72: ('percussion/wistle-001.wav', 55),
    75: ('tr808/tr-808-clave.wav', 60),
    76: ('tr808/tr-808-clave.wav', 64),
    77: ('tr808/tr-808-clave.wav', 57),
    80: ('percussion/triangle-001.wav', 60),
    81: ('percussion/triangle-002.wav', 60),
}

GAMMA_KIT_909 = _kit({
    35: ('tr909/909bd-lo.wav', 60), 36: ('tr909/909bd.wav', 60),
    37: ('tr909/909rim.wav', 60), 38: ('tr909/909sd.wav', 60),
    39: ('tr909/909clap.wav', 60), 40: ('tr909/909sd-short.wav', 60),
    41: ('tr909/909tom-lo.wav', 57), 42: ('tr909/909hh.wav', 60),
    43: ('tr909/909tom-lo.wav', 60), 44: ('tr909/909hh-short.wav', 60),
    45: ('tr909/909tom-mid.wav', 57), 46: ('tr909/909oh.wav', 60),
    47: ('tr909/909tom-mid.wav', 60), 48: ('tr909/909tom-hi.wav', 60),
    49: ('tr909/909crash.wav', 60), 50: ('tr909/909tom-hi.wav', 63),
    51: ('tr909/909ride.wav', 60), 52: ('tr909/909crash.wav', 54),
    53: ('tr909/909ride.wav', 66), 55: ('tr909/909crash.wav', 64),
    56: ('linn9000/cowbell-1.wav', 60), 57: ('tr909/909crash.wav', 57),
    59: ('tr909/909ride.wav', 57),
})

GAMMA_KIT_LINN = _kit({
    35: ('linn9000/bassdrum-1.wav', 57), 36: ('linn9000/bassdrum-1.wav', 60),
    37: ('linn9000/rimshoot-1.wav', 60), 38: ('linn9000/snaredrum-1.wav', 60),
    39: ('tr808/tr-808-clap.wav', 60), 40: ('linn9000/snaredrum-1.wav', 57),
    41: ('linn9000/tomtom-2.wav', 55), 42: ('linn9000/hihat-closed-3.wav', 60),
    43: ('linn9000/tomtom-2.wav', 58), 44: ('linn9000/hihat-closed-3.wav', 57),
    45: ('linn9000/tomtom-2.wav', 60), 46: ('linn9000/hihat-open-2.wav', 60),
    47: ('linn9000/tomtom-2.wav', 62), 48: ('linn9000/tomtom-2.wav', 64),
    49: ('linn9000/cymbal-2.wav', 60), 50: ('linn9000/tomtom-2.wav', 67),
    51: ('linn9000/cymbal-2.wav', 66), 52: ('linn9000/cymbal-2.wav', 54),
    53: ('linn9000/cowbell-1.wav', 67), 54: ('linn9000/taburine-1.wav', 60),
    55: ('linn9000/cymbal-2.wav', 63), 56: ('linn9000/cowbell-1.wav', 60),
    57: ('linn9000/cymbal-2.wav', 57), 59: ('linn9000/cymbal-2.wav', 69),
    60: ('linn9000/bongo-3.wav', 63), 61: ('linn9000/bongo-3.wav', 57),
})

GAMMA_KIT_MR12 = _kit({
    35: ('mr12/mr-12-kick.wav', 57), 36: ('mr12/mr-12-kick.wav', 60),
    37: ('mr12/mr-12-snr.wav', 66), 38: ('mr12/mr-12-snr.wav', 60),
    39: ('mr12/mr-12-snr.wav', 63), 40: ('mr12/mr-12-snr.wav', 57),
    41: ('mr12/mr-12-kick.wav', 63), 42: ('mr12/mr-12-hhc.wav', 60),
    43: ('mr12/mr-12-kick.wav', 66), 44: ('mr12/mr-12-hhc.wav', 57),
    45: ('mr12/mr-12-kick.wav', 69), 46: ('mr12/mr-12-hho.wav', 60),
    47: ('mr12/mr-12-kick.wav', 72), 48: ('mr12/mr-12-kick.wav', 75),
    49: ('mr12/mr-12-hho.wav', 55), 50: ('mr12/mr-12-kick.wav', 78),
    51: ('mr12/mr-12-hho.wav', 64), 52: ('mr12/mr-12-hho.wav', 50),
    53: ('mr12/mr-12-hhc.wav', 52), 55: ('mr12/mr-12-hho.wav', 60),
    56: ('linn9000/cowbell-1.wav', 60), 57: ('mr12/mr-12-hho.wav', 52),
    59: ('mr12/mr-12-hho.wav', 67),
})

GAMMA_KIT_SYNTHETICS = _kit({
    35: ('synthetics/tokyo-deep-bd.wav', 60), 36: ('synthetics/tokyo-jet-bd.wav', 60),
    37: ('synthetics/click-07.wav', 60), 38: ('synthetics/static-07.wav', 60),
    39: ('synthetics/wooden-03.wav', 60), 40: ('synthetics/hizz-06.wav', 60),
    41: ('synthetics/blop-02.wav', 55), 42: ('synthetics/click-05.wav', 60),
    43: ('synthetics/blop-02.wav', 58), 44: ('synthetics/click-06.wav', 60),
    45: ('synthetics/blop-02.wav', 61), 46: ('synthetics/pew-03.wav', 60),
    47: ('synthetics/boink-04.wav', 58), 48: ('synthetics/boink-04.wav', 62),
    49: ('synthetics/metallic-09.wav', 57), 50: ('synthetics/boink-04.wav', 66),
    51: ('synthetics/metallic-16.wav', 60), 52: ('synthetics/metallic-09.wav', 52),
    53: ('synthetics/metallic-16.wav', 66), 55: ('synthetics/metallic-09.wav', 63),
    56: ('synthetics/wooden-01.wav', 60), 57: ('synthetics/metallic-09.wav', 60),
    59: ('synthetics/metallic-16.wav', 55),
})

GAMMA_KIT_POWER = _kit({
    # Power Kit roots are their GM notes, so base == root plays untransposed
    35: ('power/power-kick-real.wav', 35), 36: ('power/power-kick-metal.wav', 36),
    37: ('power/power-sidestick.wav', 37), 38: ('power/power-snare.wav', 38),
    39: ('power/power-clap.wav', 39), 40: ('power/power-snare-gated.wav', 40),
    41: ('power/power-tom-1.wav', 41), 42: ('power/power-hh-tight.wav', 42),
    43: ('power/power-tom-2.wav', 43), 44: ('power/power-hh-pedal.wav', 44),
    45: ('power/power-tom-3.wav', 45), 46: ('power/power-hh-open.wav', 46),
    47: ('power/power-tom-4.wav', 47), 48: ('power/power-tom-5.wav', 48),
    49: ('power/power-crash.wav', 49), 50: ('power/power-tom-6.wav', 50),
    51: ('power/power-ride.wav', 51), 52: ('power/power-crash.wav', 44),
    53: ('power/power-ride-cup.wav', 53), 55: ('power/power-splash.wav', 55),
    56: ('power/power-cowbell.wav', 56), 57: ('power/power-crash.wav', 46),
    59: ('power/power-ride.wav', 48),
})

GAMMA_KIT_PERCUSSION = _kit({
    35: ('percussion/noice-kick.wav', 60), 36: ('percussion/noice-kick.wav', 63),
    37: ('percussion/tabla-002.wav', 60), 38: ('percussion/old-snare.wav', 60),
    39: ('percussion/tamp-002.wav', 60), 40: ('percussion/old-snare.wav', 57),
    41: ('percussion/timpani-001.wav', 55), 42: ('percussion/simple-shaker.wav', 60),
    43: ('percussion/timpani-001.wav', 58), 44: ('percussion/simple-shaker.wav', 57),
    45: ('percussion/timpani-001.wav', 60), 46: ('percussion/simple-shaker-002.wav', 60),
    47: ('percussion/tabla-003.wav', 58), 48: ('percussion/tabla-003.wav', 62),
    49: ('percussion/triangle-002.wav', 55), 50: ('percussion/tabla-003.wav', 66),
    51: ('percussion/triangle-002.wav', 60), 52: ('percussion/timpani-002.wav', 50),
    53: ('percussion/triangle-001.wav', 64), 55: ('percussion/triangle-002.wav', 64),
    56: ('linn9000/cowbell-1.wav', 60), 57: ('percussion/triangle-002.wav', 50),
    59: ('percussion/triangle-001.wav', 57),
})

# (kit name, GM map) in program-change order within bank MSB 3. Kit 0 is the
# tuple-based GAMMA_DRUMKIT (ROM presets only) and is handled specially.
GAMMA_KITS = [
    ('TR-909', GAMMA_KIT_909),
    ('Linn 9000', GAMMA_KIT_LINN),
    ('MR-12', GAMMA_KIT_MR12),
    ('Tokyo Synthetics', GAMMA_KIT_SYNTHETICS),
    ('Power', GAMMA_KIT_POWER),
    ('Percussion', GAMMA_KIT_PERCUSSION),
]


_GAMMA_PRESET_RMS = None

def _gamma_preset_rms():
    """preset number -> loudest-100ms RMS in dBFS, for kit level balancing."""
    global _GAMMA_PRESET_RMS
    if _GAMMA_PRESET_RMS is not None:
        return _GAMMA_PRESET_RMS
    import json, math
    manifest = json.load(open('sounds/gamma9001/manifest.json'))
    table = {}
    rom_i = bin_i = 0
    for m in manifest:
        preset = rom_i if m['bank'] == GAMMA9001_ROM_BANK else GAMMA9001_PRESET_BASE + bin_i
        if m['bank'] == GAMMA9001_ROM_BANK:
            rom_i += 1
        else:
            bin_i += 1
        d = _read_wav_mono16('sounds/gamma9001/' + m['file'])
        x = np.asarray(d, dtype=np.float32) / 32768.0
        n = 2205  # 100ms at 22050
        if len(x) < n:
            x = np.pad(x, (0, n - len(x)))
        env = np.convolve(x * x, np.ones(n) / n, mode='valid')
        table[preset] = 20.0 * math.log10(float(np.sqrt(env.max())) + 1e-9)
    _GAMMA_PRESET_RMS = table
    return table


def gamma_kit_to_drumkit(kit):
    """Resolve a {gm_note: (file, base_note)} kit to DRUMKIT-style tuples."""
    import json
    manifest = json.load(open('sounds/gamma9001/manifest.json'))
    file_to_preset = {}
    rom_i = bin_i = 0
    for m in manifest:
        if m['bank'] == GAMMA9001_ROM_BANK:
            file_to_preset[m['file']] = rom_i
            rom_i += 1
        else:
            file_to_preset[m['file']] = GAMMA9001_PRESET_BASE + bin_i
            bin_i += 1
    out = []
    for note in range(AMY_MIDI_DRUMS_LOWEST_NOTE, AMY_MIDI_DRUMS_HIGHEST_NOTE + 1):
        if note in kit:
            fname, base = kit[note]
            out.append((file_to_preset[fname], base))
        else:
            out.append((-1, -1))
    return out


def make_drums_patch(drumkit=None, synth_flags=None, balance=False):
    if drumkit is None:
        drumkit = DRUMKIT
    if synth_flags is None:
        # The Gamma9001 kit patches bake NOTES_VIA_MIDI in so that just loading
        # the patch is enough (`amy.send(synth=11, patch=386, num_voices=6)`);
        # the legacy 258 kit keeps its historical flags (callers set their own).
        synth_flags = amy.SYNTH_FLAGS_NOTES_VIA_MIDI | amy.SYNTH_FLAGS_IGNORE_NOTE_OFFS
    # Set up a bunch of midi_note_cmd mappings for CH10 drums
    message = amy.message(osc=0, wave=amy.PCM, amp=5, synth_flags=synth_flags)
    message += amy.message(synth_flags=synth_flags)
    # Level balancing for the Gamma9001 kits: the source samples were never
    # level-matched as a kit (the Koblo kick is a full-scale 3s boom, the hats
    # 6-8 dB quieter than the old set), so bake a per-note amp into each
    # mapping that lands every voice at a consistent loudness target.
    rms_table = _gamma_preset_rms() if balance else {}
    for midi_note in range(AMY_MIDI_DRUMS_LOWEST_NOTE, AMY_MIDI_DRUMS_HIGHEST_NOTE + 1):
        pcm_preset_number, base_midi_note = drumkit[midi_note - AMY_MIDI_DRUMS_LOWEST_NOTE]
        if pcm_preset_number >= 0:
            kwargs = {}
            rms = rms_table.get(pcm_preset_number)
            if rms is not None:
                # kicks louder than the rest; toms in between
                if midi_note in (35, 36):
                    target = -8.0
                elif midi_note in (41, 43, 45, 47, 48, 50):
                    target = -11.0
                else:
                    target = -14.0
                gain = 10.0 ** ((target - rms) / 20.0)
                gain = min(3.0, max(0.33, gain))
                kwargs['amp'] = round(5.0 * gain, 2)
            message += amy.message(
                midi_note_cmd=(
                    ("%d,0,0,1,0," % midi_note)
                    + amy.message(
                        synth=8888,
                        preset=pcm_preset_number,
                        note=base_midi_note,
                        vel=9999,
                        **kwargs
                    ).replace('8888', '%i').replace('9999', '%v')
                )
            )
    return 1, message  # 1 osc per voice


def make_patches(filename):
    def nothing(message):
        return
    from . import juno, fm
    num_oscs =[]
    # Don't make any noise
    amy.override_send = nothing

    # 0-127 Juno, 128-255 DX7, 256 piano, 257 amyboard, 258 legacy GM drums,
    # 259-383 reserved (NULL), 384-390 Gamma9001 drum kits (MIDI bank MSB 3,
    # program = kit number).
    GAMMA_KIT_PATCH_BASE = 384
    TOTAL_NUM_PATCHES = GAMMA_KIT_PATCH_BASE + 1 + len(GAMMA_KITS)

    with open(filename, "w") as f:
        f.write("// Automatically generated.\n// DX7 and juno 106 and custom patch table\n")
        f.write("#ifndef __PATCHESH\n#define __PATCHESH\n")
        f.write("#define _PATCHES_NUM_BUILTIN %d\n" % TOTAL_NUM_PATCHES)
        f.write("static const char * const patch_commands[%d] PROGMEM = {\n" % TOTAL_NUM_PATCHES)
        # Do juno
        for i in range(128):
            amy.log_patch()
            j = juno.JunoPatch()
            j.set_patch(i)
            f.write("\t/* %d: Juno %s */ \"%s\",\n" % (i, j.name, amy.retrieve_patch()))  
            num_oscs.append(6)
        # Do dx7
        for i in range(128):
            amy.log_patch()
            p = fm.AMYPatch.from_dx7(fm.DX7Patch.from_patch_number(i))
            p.send_to_AMY(reset=False)
            f.write("\t/* %d: DX7 %s */ \"%s\",\n" % (i+128, p.name, amy.retrieve_patch()))  
            num_oscs.append(8)

        # do piano
        amy.log_patch()
        num_osc_piano = make_piano_patch()
        f.write("\t/* 256: dpwe piano */ \"%s\",\n" % (amy.retrieve_patch()))  
        num_oscs.append(num_osc_piano)

        # do amyboard patch
        num_osc_amyboard, patch_string  = make_amyboard_patch()
        f.write("\t/* 257: amyboard default */ \"%s\",\n" % (patch_string))
        num_oscs.append(num_osc_amyboard)

        # do MIDI drums via midi_note_cmd patch. The preset numbers baked into
        # this string index the compiled-in ROM sample set, which is
        # build-conditional (amy.c: pcm_gamma808.h under GAMMA9001, else
        # pcm_tiny.h) -- so emit a matching 258 for each ROM. The GAMMA9001
        # variant reuses kit 384's GM map but keeps 258's historical flags
        # (no baked NOTES_VIA_MIDI; callers set their own).
        num_osc_drums, patch_string  = make_drums_patch(synth_flags=amy.SYNTH_FLAGS_IGNORE_NOTE_OFFS)
        _, gamma_patch_string = make_drums_patch(GAMMA_DRUMKIT, synth_flags=amy.SYNTH_FLAGS_IGNORE_NOTE_OFFS, balance=True)
        f.write("#ifdef GAMMA9001\n")
        f.write("\t/* 258: MIDI drums (gamma808 ROM) */ \"%s\",\n" % (gamma_patch_string))
        f.write("#else\n")
        f.write("\t/* 258: MIDI drums (pcm_tiny ROM) */ \"%s\",\n" % (patch_string))
        f.write("#endif\n")
        num_oscs.append(num_osc_drums)

        # 259-383 are reserved so the Gamma9001 drum kits land on their own
        # MIDI program-change bank: patch = 384 + kit = 128 * MSB(3) + program.
        f.write("\t/* 259-383: reserved */\n")
        for i in range(259, GAMMA_KIT_PATCH_BASE):
            f.write("\tNULL,\n")
            num_oscs.append(0)

        # kit 0: the baked TR-808 bank (ROM presets only; works without drums.bin)
        num_osc_drums, patch_string = make_drums_patch(GAMMA_DRUMKIT, balance=True)
        f.write("\t/* %d: drum kit 0 TR-808 (Gamma9001) */ \"%s\",\n" % (GAMMA_KIT_PATCH_BASE, patch_string))
        num_oscs.append(num_osc_drums)
        # kits 1+: the drums.bin banks
        for k, (kit_name, kit) in enumerate(GAMMA_KITS):
            num_osc_drums, patch_string = make_drums_patch(gamma_kit_to_drumkit(kit), balance=True)
            f.write("\t/* %d: drum kit %d %s (Gamma9001) */ \"%s\",\n" % (
                GAMMA_KIT_PATCH_BASE + 1 + k, k + 1, kit_name, patch_string))
            num_oscs.append(num_osc_drums)

        f.write("};\n")
        f.write("const uint16_t patch_oscs[%d] PROGMEM = {\n" % TOTAL_NUM_PATCHES)
        for i in num_oscs:
            f.write("%d," % (i))
        f.write("\n};\n#endif\n")
    amy.override_send = None
    print("wrote", filename)


def write_vector_as_c(f, data, name, dtype='uint8_t', items_per_row=20):
    num_name = "NUM_%s" % name.upper()
    f.write("#define %s %d\n" % (num_name, len(data)))
    f.write("const %s %s[%s] PROGMEM = {\n" % (dtype, name, num_name))
    for start in range(0, len(data), items_per_row):
        data_row = data[start : start + items_per_row]
        f.write("    " + ", ".join("%d" % d for d in data_row) + ",\n")
    f.write("};\n\n")


def make_interp_partials(filename, data_dict):
    """Write the json data from experiments/piano_heterodyne.ipynb into a C header."""
    import itertools
    # data_dict is {instrument_tag: {'sample_times_ms': [], 'notes': [], 'velocities': [], 'num_harmonics': xx, 'harmonics_freq': [], 'harmonics_mags': []}, ...}
    with open(filename, "w") as f:
        f.write("// Automatically generated.\n// Piano interpolated partials data table\n")
        f.write("#ifndef __INTERP_PARTIALS_H\n#define __INTERP_PARTIALS_H\n\n")
        f.write("#define NUM_INTERP_PARTIALS_PRESETS %d\n\n" % len(data_dict))
        map_contents = ""
        for tag, data in data_dict.items():
            for varname, dtype in [('sample_times_ms', 'uint16_t'),
                                   ('velocities', 'uint8_t'),
                                   ('notes', 'uint8_t'),
                                   ('num_harmonics', 'uint8_t'),
                                   ('harmonics_freq', 'uint16_t')]:
                write_vector_as_c(f, data[varname], tag + "_" + varname, dtype=dtype)
            harmonics_mags = list(itertools.chain(*data['harmonics_mags']))
            write_vector_as_c(f, harmonics_mags, tag + "_harmonics_mags")
            TAG = tag.upper()
            map_contents += """    {
        NUM_%s_SAMPLE_TIMES_MS,
        %s_sample_times_ms,
        NUM_%s_VELOCITIES,
        %s_velocities,
        NUM_%s_NOTES,
        %s_notes,
        %s_num_harmonics,
        %s_harmonics_freq,
        %s_harmonics_mags,
    },
""" % (TAG, tag, TAG, tag, TAG, tag, tag, tag, tag)

        f.write("const interp_partials_voice_t interp_partials_map[NUM_INTERP_PARTIALS_PRESETS] PROGMEM = {\n")
        f.write(map_contents)
        f.write("};\n\n")
        f.write("#endif // ndef __INTERP_PARTIALS_H\n\n")

    print("wrote", filename)


"""
    Generate all the headers except for the partials headers
"""
def generate_all():
    from . import fm
    import collections
    import json
    # Implement the multiple lookup tables.
    # A LUT is stored as an array of values (table) and the harmonic number of the
    # highest harmonic they contain (i.e., the number of cycles it completes in the
    # entire table, so must be <= len(table)/2.)
    LUTentry = collections.namedtuple('LUTentry', ['table', 'highest_harmonic'])

    # Impulses.
    #impulse_lutset = create_lutset(LUTentry, np.ones(128))
    ##write_lutset_to_h('src/impulse_lutset.h', 'impulse', impulse_lutset)
    #write_lutset_to_h_as_fxpt('src/impulse_lutset_fxpt.h', 'impulse', impulse_lutset)

    # Saw_up.
    saw_lutset = create_lutset(LUTentry, [0] + list(-1 / np.arange(1, 256)), -np.pi/2 * np.ones(256))
    #write_lutset_to_h('src/saw_lutset.h', 'saw', saw_lutset)
    write_lutset_to_h_as_fxpt('src/saw_lutset_fxpt.h', 'saw', saw_lutset)

    # Triangle wave lutset
    n_harms = 64
    coefs = (np.arange(n_harms) % 2) * (
        np.maximum(1, np.arange(n_harms, dtype=float))**(-2))
    triangle_lutset = create_lutset(LUTentry, coefs, np.arange(len(coefs)) * -np.pi / 2)
    #write_lutset_to_h('src/triangle_lutset.h', 'triangle', triangle_lutset)
    write_lutset_to_h_as_fxpt('src/triangle_lutset_fxpt.h', 'triangle', triangle_lutset)

    # Sinusoid "lutset" (only one table)
    sine_lutset = create_lutset(LUTentry, np.array([0, 1]),  harmonic_phases = -np.pi / 2 * np.ones(2), length_factor=256)
    #write_lutset_to_h('src/sine_lutset.h', 'sine', sine_lutset)
    write_lutset_to_h_as_fxpt('src/sine_lutset_fxpt.h', 'sine', sine_lutset)

    # log2/exp2 LUTs
    make_log2_exp2_luts('src/log2_exp2_fxpt_lutable.h')

    # Clipping LUT
    make_clipping_lut('src/clipping_lookup_table.h')

    # PCM LUT
    try:
        generate_pcm_headers()
    except ImportError:
        print("If you want to regenerate the PCM headers (not required!) you need to `pip install resampy sf2utils audioop-lts`.")

    # Juno & FM patches
    make_patches("src/patches.h")

    # interp_partials data table
    make_interp_partials("src/interp_partials.h",
                         {'piano': json.load(open("experiments/piano-params.json", "r"))})

    # Wavetable samples are appended to tiny PCM headers in generate_pcm_headers().


def main():
    if 'gamma9001' in sys.argv:
        generate_gamma9001_headers()
        return
    print("Generating all headers needed for AMY...")
    generate_all()
    print("Done.")

if __name__ == '__main__':
    main()
