################################################################################
#
# File Description:
#   This python script generates c header and source files containing the 
#   attack and decay lookup-tables used by the system.
#
#   The attack LUT is a truncated rising rc curve and the decay LUT is a decaying
#   rc curve. The decay LUT is used for both the decay and release phases.
#
#   This script is invoked from the Makefile by:
#       $ make write_lut ATTACK_TARGET=xxx NUM_TIME_CONSTANTS=xxx
#   or
#       $ make plot_lut ATTACK_TARGET=xxx NUM_TIME_CONSTANTS=xxx
#
#   Default values are provided for the attack target and number of time constants,
#   so these are not mandatory to enter. However, if the user wants to experiment
#   with different curve shapes, a quick way to do it is to experimentally plot
#   some different shapes, and then when you have shapes that seem interesting
#   write the LUTs with the same values.
#
################################################################################

import argparse
import numpy as np
from matplotlib import pyplot as plt

# The output file paths for the lookup table header/source files to generate.
HEADER_OUTPUT_FILE_PATH = "../lib/lookup_tables.h"
SOURCE_OUTPUT_FILE_PATH = "../lib/lookup_tables.c"

# the lookup table size, must match the ADSR_LOOK_UP_TABLE_TABLE_SIZE and 
# NUM_INDEX_BITS_IN_ACCUMULATOR definitions in adsr.c, where ADSR_LOOK_UP_TABLE_TABLE_SIZE
# is 2^NUM_INDEX_BITS_IN_ACCUMULATOR
LUT_SIZE = 2**10

# arguments decide whether to plot the curves or write the c files, and allow the
# user to modify the shape of the curves
parser = argparse.ArgumentParser()

parser.add_argument(
    'action',
    help='the action to do, either graphically plot the LUT, or generate and write the c files',
    choices=['plot', 'write']
)

parser.add_argument(
    'attack_target', 
    help='the target value for the attack curve, useful range: [1..5]',
    nargs='?',
    default=3.0,
    type=float
)

parser.add_argument(
    'num_time_constants', 
    help='the number of time constants to extend the attack and decay curves, useful range: [3..10]',
    nargs='?',
    default=4.0,
    type=float
)

args = parser.parse_args()

# the size of the optional plotted figure
plt.rcParams['figure.figsize'] = (8.0, 6.0)

# the target for the attack curve, flattens out the attack curve like an analog ADSR
# which sets a target for the attack curve higher than the trip point
# Example: the attack target is often 15 volts, but the comparator trips at 10 volts
# bigger number means flatter attack curve, adjust to taste
ATTACK_TARGET = args.attack_target

# how far out to take the curves in the LUT, too short and the curves will not
# have time to decay gracefully, too long and all the action will be bunched up
# in the front of the LUT
NUM_TIME_CONSTANTS = args.num_time_constants

# the linear input to transform into the attack and decay curves
X = np.linspace(0, NUM_TIME_CONSTANTS, LUT_SIZE)

# generate the attack curve, range [0, 1], increasing truncated rc curve
y_a = 1 - np.exp(-X / ATTACK_TARGET)
y_a = y_a / y_a.max() # range [0, 1]

# generate the decay/release curve, range [0, 1], decreasing rc curve
y_d = np.exp(-X)
y_d = y_d - y_d.min() # end at zero
y_d = y_d / y_d.max() # range [0, 1]

if (args.action == 'plot'): # graphically plot the curves
    fig = plt.figure()
    ax = fig.add_subplot()

    ax.plot(np.linspace(0, LUT_SIZE-1, LUT_SIZE), y_a, label="attack")
    ax.plot(np.linspace(0, LUT_SIZE-1, LUT_SIZE), y_d, label="decay")

    plt.suptitle(f"Attack and decay lookup tables with {LUT_SIZE} points")
    plt.title(f'attack target: {ATTACK_TARGET}\nnum time constants: {NUM_TIME_CONSTANTS}')
    plt.xlabel("LUT index")
    plt.ylabel("value")
    plt.legend()

    plt.show()
elif (args.action == 'write'): # write the c-header file
    print(f"generating {HEADER_OUTPUT_FILE_PATH} file...")

    ATTACK_TABLE_TYPE = f'const float ADSR_ATTACK_TABLE[{LUT_SIZE}]'
    DECAY_TABLE_TYPE  = f'const float ADSR_DECAY_TABLE[{LUT_SIZE}]'

    h_file_content = '/* FILE AUTOMATICALLY GENERATED BY: /utils/lookup_table_gen.py */\n\n\
#ifndef LOOKUP_TABLES_H_INCLUDED\n\
#define LOOKUP_TABLES_H_INCLUDED\n\n\
// the attack curve lookup table\n\
extern ' + ATTACK_TABLE_TYPE + ';\n\n\
// the decay curve lookup table, used for decay and release curves\n\
extern ' + DECAY_TABLE_TYPE + ';\n\n\
#endif'

    with open(HEADER_OUTPUT_FILE_PATH, 'w') as writer:
        writer.write(h_file_content)

    # write the c-source file
    print(f"generating {SOURCE_OUTPUT_FILE_PATH} file...")

    source_beginning =  f'/* FILE AUTOMATICALLY GENERATED BY: /utils/lookup_table_gen.py */\n\n#include "lookup_tables.h" \n\n'

    end_of_lut = '\n};\n'

    with open(SOURCE_OUTPUT_FILE_PATH, 'w') as writer:
        writer.write(source_beginning)
        writer.write(ATTACK_TABLE_TYPE + ' = {\n')
        writer.writelines("\t%s,\n" % y for y in y_a)
        writer.write(end_of_lut + '\n')

        writer.write(DECAY_TABLE_TYPE + ' = {\n')
        writer.writelines("\t%s,\n" % y for y in y_d)
        writer.write(end_of_lut)

################################################################################
#
# File Description:
#   This python script generates c header and source files containing the 
#   attack and decay lookup-tables used by the system.
#
#   The attack LUT is a truncated rising rc curve and the decay LUT is a decaying
#   rc curve. The decay LUT is used for both the decay and release phases.
#
#   This script is invoked from the Makefile by:
#       $ make write_lut ATTACK_TARGET=xxx NUM_TIME_CONSTANTS=xxx
#   or
#       $ make plot_lut ATTACK_TARGET=xxx NUM_TIME_CONSTANTS=xxx
#
#   Default values are provided for the attack target and number of time constants,
#   so these are not mandatory to enter. However, if the user wants to experiment
#   with different curve shapes, a quick way to do it is to experimentally plot
#   some different shapes, and then when you have shapes that seem interesting
#   write the LUTs with the same values.
#
################################################################################

import argparse
import numpy as np
from matplotlib import pyplot as plt

# The output file paths for the lookup table header/source files to generate.
HEADER_OUTPUT_FILE_PATH = "../lib/lookup_tables.h"
SOURCE_OUTPUT_FILE_PATH = "../lib/lookup_tables.c"

# the lookup table size, must match the ADSR_LOOK_UP_TABLE_TABLE_SIZE and 
# NUM_INDEX_BITS_IN_ACCUMULATOR definitions in adsr.c, where ADSR_LOOK_UP_TABLE_TABLE_SIZE
# is 2^NUM_INDEX_BITS_IN_ACCUMULATOR
LUT_SIZE = 2**10

# arguments decide whether to plot the curves or write the c files, and allow the
# user to modify the shape of the curves
parser = argparse.ArgumentParser()

parser.add_argument(
    'action',
    help='the action to do, either graphically plot the LUT, or generate and write the c files',
    choices=['plot', 'write']
)

parser.add_argument(
    'attack_target', 
    help='the target value for the attack curve, useful range: [1..5]',
    nargs='?',
    default=3.0,
    type=float
)

parser.add_argument(
    'num_time_constants', 
    help='the number of time constants to extend the attack and decay curves, useful range: [3..10]',
    nargs='?',
    default=4.0,
    type=float
)

args = parser.parse_args()

# the size of the optional plotted figure
plt.rcParams['figure.figsize'] = (8.0, 6.0)

# the target for the attack curve, flattens out the attack curve like an analog ADSR
# which sets a target for the attack curve higher than the trip point
# Example: the attack target is often 15 volts, but the comparator trips at 10 volts
# bigger number means flatter attack curve, adjust to taste
ATTACK_TARGET = args.attack_target

# how far out to take the curves in the LUT, too short and the curves will not
# have time to decay gracefully, too long and all the action will be bunched up
# in the front of the LUT
NUM_TIME_CONSTANTS = args.num_time_constants

# the linear input to transform into the attack and decay curves
X = np.linspace(0, NUM_TIME_CONSTANTS, LUT_SIZE)

# generate the attack curve, range [0, 1], increasing truncated rc curve
y_a = 1 - np.exp(-X / ATTACK_TARGET)
y_a = y_a / y_a.max() # range [0, 1]

# generate the decay/release curve, range [0, 1], decreasing rc curve
y_d = np.exp(-X)
y_d = y_d - y_d.min() # end at zero
y_d = y_d / y_d.max() # range [0, 1]

if (args.action == 'plot'): # graphically plot the curves
    fig = plt.figure()
    ax = fig.add_subplot()

    ax.plot(np.linspace(0, LUT_SIZE-1, LUT_SIZE), y_a, label="attack")
    ax.plot(np.linspace(0, LUT_SIZE-1, LUT_SIZE), y_d, label="decay")

    plt.suptitle(f"Attack and decay lookup tables with {LUT_SIZE} points")
    plt.title(f'attack target: {ATTACK_TARGET}\nnum time constants: {NUM_TIME_CONSTANTS}')
    plt.xlabel("LUT index")
    plt.ylabel("value")
    plt.legend()

    plt.show()
elif (args.action == 'write'): # write the c-header file
    print(f"generating {HEADER_OUTPUT_FILE_PATH} file...")

    ATTACK_TABLE_TYPE = f'const float ADSR_ATTACK_TABLE[{LUT_SIZE}]'
    DECAY_TABLE_TYPE  = f'const float ADSR_DECAY_TABLE[{LUT_SIZE}]'

    h_file_content = '/* FILE AUTOMATICALLY GENERATED BY: /utils/lookup_table_gen.py */\n\n\
#ifndef LOOKUP_TABLES_H_INCLUDED\n\
#define LOOKUP_TABLES_H_INCLUDED\n\n\
// the attack curve lookup table\n\
extern ' + ATTACK_TABLE_TYPE + ';\n\n\
// the decay curve lookup table, used for decay and release curves\n\
extern ' + DECAY_TABLE_TYPE + ';\n\n\
#endif'

    with open(HEADER_OUTPUT_FILE_PATH, 'w') as writer:
        writer.write(h_file_content)

    # write the c-source file
    print(f"generating {SOURCE_OUTPUT_FILE_PATH} file...")

    source_beginning =  f'/* FILE AUTOMATICALLY GENERATED BY: /utils/lookup_table_gen.py */\n\n#include "lookup_tables.h" \n\n'

    end_of_lut = '\n};\n'

    with open(SOURCE_OUTPUT_FILE_PATH, 'w') as writer:
        writer.write(source_beginning)
        writer.write(ATTACK_TABLE_TYPE + ' = {\n')
        writer.writelines("\t%s,\n" % y for y in y_a)
        writer.write(end_of_lut + '\n')

        writer.write(DECAY_TABLE_TYPE + ' = {\n')
        writer.writelines("\t%s,\n" % y for y in y_d)
        writer.write(end_of_lut)

################################################################################ # # File Description: #   This python script generates c header and source files containing the #   attack and decay lookup-tables used by the system. # #   The attack LUT is a truncated rising rc curve and the decay LUT is a decaying #   rc curve. The decay LUT is used for both the decay and release phases. # #   This script is invoked from the Makefile by: #       $ make write_lut ATTACK_TARGET=xxx NUM_TIME_CONSTANTS=xxx #   or #       $ make plot_lut ATTACK_TARGET=xxx NUM_TIME_CONSTANTS=xxx # #   Default values are provided for the attack target and number of time constants, #   so these are not mandatory to enter. However, if the user wants to experiment #   with different curve shapes, a quick way to do it is to experimentally plot #   some different shapes, and then when you have shapes that seem interesting #   write the LUTs with the same values. # ################################################################################ # The output file paths for the lookup table header/source files to generate. # the lookup table size, must match the ADSR_LOOK_UP_TABLE_TABLE_SIZE and # NUM_INDEX_BITS_IN_ACCUMULATOR definitions in adsr.c, where ADSR_LOOK_UP_TABLE_TABLE_SIZE # is 2^NUM_INDEX_BITS_IN_ACCUMULATOR # arguments decide whether to plot the curves or write the c files, and allow the # user to modify the shape of the curves # the size of the optional plotted figure # the target for the attack curve, flattens out the attack curve like an analog ADSR # which sets a target for the attack curve higher than the trip point # Example: the attack target is often 15 volts, but the comparator trips at 10 volts # bigger number means flatter attack curve, adjust to taste # how far out to take the curves in the LUT, too short and the curves will not # have time to decay gracefully, too long and all the action will be bunched up # in the front of the LUT # the linear input to transform into the attack and decay curves # generate the attack curve, range [0, 1], increasing truncated rc curve # range [0, 1] # generate the decay/release curve, range [0, 1], decreasing rc curve # end at zero # range [0, 1] # graphically plot the curves # write the c-header file #ifndef LOOKUP_TABLES_H_INCLUDED\n\ #define LOOKUP_TABLES_H_INCLUDED\n\n\ #endif' # write the c-source file #include "lookup_tables.h" \n\n'