/* ============================================================================ *
                 drum.c -- a simple drum machine using CAMD and RealTime
                                      By Talin.
                               ©1991 The Dreamers Guild
 * ============================================================================ */

#define MCLOCKS_PER_QNOTE   480

/* ============================================================================ *
                                        Notes
 * ============================================================================ */

#if 0
    What's next?
        -- program gadget for setup. Init strings? Standard init strings?
            -- a UI to compose a set of MIDI strings...?
        -- mute and solo gadgets. (Do as bevel boxes?)
        -- allow numpad keys to also work
        -- disallow key repeats.
        -- pattern length and resolution.
        -- do maestro calculations time correctly.
        -- solve maestro locating problem.
        -- seperate task to handle the actual timing chores
        -- recording as we go.
        -- add in toggle buttons (boopsi) for mute. (or just "roll-yer-own??)
        -- add in conductor setting and output link setting lists
                (use transport code...)
        -- hook up individual menu items.

    Menus:
        Project
            Load
            Save
        ?   Save Settings?
            Quit
            About
        Edit
            ?? Undo ??
            Erase Pattern
            Reverse Pattern
        Play
            Whole Song
            --------------
            Set Conductor...
            Set MIDI Output Port...
        Pattern
            Set Pattern Length...
            Set Drum Resolution >>  (Quarter note, etc...)

    Other ideas:
        -- a pattern arrangement dialogue...
        -- play backwards.
        -- ARexx port?

    WA_MenuHelp
    WA_NotifyDepth
#endif

/* ============================================================================ *
                                       Includes
 * ============================================================================ */

#include "std_headers.h"
#include <intuition/icclass.h>
#include <libraries/iffparse.h>
#include <libraries/asl.h>
#include <libraries/realtime.h>
#include <midi/mididefs.h>
#include <midi/camd.h>
#include <clib/asl_protos.h>
#include <clib/camd_protos.h>
#include <clib/realtime_protos.h>
#include <clib/alib_protos.h>
#include <pragmas/realtime_pragmas.h>
#include <pragmas/camd_pragmas.h>
#include <stdlib.h>
#include <math.h>

#include "arrows.h"                             /* increment arrows             */

/* ============================================================================ *
                                      Prototypes
 * ============================================================================ */

struct DrumColumn;
void draw_titlebar_scale( void );
void render_column( int column, int highlight );
void play_drum_note( struct DrumColumn *dc, BOOL note_on );
void preview_row( int row );
void step_play( int dir );

BOOL save_pattern( void );

void calc_tempo( void );
void calc_pattern_ticks( void );
void locate_to_tick ( long tick_count );

struct Gadget *make_arrow( struct NewGadget *ng, struct Gadget *prev, int min, int max, int current );
ULONG AddTailObject( Object *o, struct List *list);
void DisposeObjectList( struct List *l );
void shadow_drag( int col1, int col2 );

extern Class *initArrowButtonClass(void);
void freeArrowButtonClass(Class *sclass);

/* ============================================================================ *
                                       Defines
 * ============================================================================ */

#define clamp(a,b,c)    min( max( a, b ), c )   /* constrain b between a and c  */

#define APPNAME         "Drum Machine"
#define DRUM_ROWS       10
#define ROW_HEIGHT      14
#define MAX_MATRIX_WIDTH 400
#define MAX_COLUMNS     64
#define MAX_PATTERNS    128
#define MATRIX_X        224

enum gadget_ids {
    ID_MUTE = 1,                                /* mute button per row          */
    ID_SOLO,                                    /* solo button per row          */
    ID_PNAME,                                   /* name string per row          */
    ID_KEY,                                     /* key to play -- per row       */
    ID_KEYINC,                                  /* key increment arrows         */
    ID_HIT,                                     /* hit drum now...              */
    ID_FILE,                                    /* filename                     */
    ID_PLAY,                                    /* play button                  */
    ID_STOP,                                    /* stop button                  */
    ID_STEPFWD,                                 /* step fwd button              */
    ID_STEPBACK,                                /* step backwd button           */
    ID_CHANNEL,                                 /* channel slider               */
    ID_CHANNELINC,
    ID_TEMPO,                                   /* tempo slider                 */
    ID_VELOCITY,                                /* velocity slider              */
};

    /*  Normally, the columns of the drum grid are equal to a sixteenth note
        in duration, however this can be changed. Here are the values that
        the "drum resolution" value can take.
    */

enum drum_res {
    DRES_NOTE1=0,                               /* whole note                   */
    DRES_NOTE2,                                 /* half note                    */
    DRES_NOTE4,                                 /* quarter note                 */
    DRES_NOTE8,                                 /* eighth note                  */
    DRES_NOTE16,                                /* etc. */
    DRES_NOTE32,
    DRES_NOTE64,
    DRES_NOTE128,
};

    /*  Defines a single column of the drum pattern */

struct DrumColumn {
    UBYTE               note[ DRUM_ROWS ];
};

    /*  Defines the setup information for the drum kit */

struct DrumSetEntry {
    char                name[20];
    UBYTE               key;
    UBYTE               flags;
};

typedef struct DrumSetEntry DrumKit[ DRUM_ROWS ];

#define DRUMF_MUTE      (1<<0)

struct DrumPattern {
    UWORD               dpNumber;               /* pattern number               */
    UWORD               dpColumns,              /* pattern columns (global now) */
                        dpRows;                 /* pattern rows (constant now)  */
    DrumKit             dpSetup;                /* setup for pattern            */
    struct DrumColumn   dpGrid[ MAX_COLUMNS ];  /* actual pattern data          */
};

DrumKit                 ReadKit;

/* ============================================================================ *
                                       Globals
 * ============================================================================ */

struct Library          *IntuitionBase,
                        *GadToolsBase,
                        *AslBase,
                        *GfxBase,
                        *UtilityBase,
                        *CamdBase,
                        *RealTimeBase,
                        *IFFParseBase;

extern struct Library   *SysBase,
                        *DOSBase;

struct Window           *window;
struct RastPort         *rp;
struct VisualInfo       *vi;
struct DrawInfo         *dri;
UWORD                   *dri_pens;
struct FileRequester    *filereq;

BOOL                    running = TRUE;

struct MidiNode         *midi;
struct MidiLink         *out_link;

struct Gadget           *context_gadget,
                        *first_gadget,
                        *last_gadget,
                        *first_arrow,
                        *arrow_gadget;

struct Gadget           *key_gadgets[ DRUM_ROWS ],
                        *name_gadgets[ DRUM_ROWS ],
                        *keyinc_gadgets[ DRUM_ROWS ],
                        *channel_string,
                        *channel_arrow;

BOOL                    tempo_drag;

WORD                    drag_state,
                        drag_row,
                        drag_column,
                        drag_anchor_column;

#define DRAG_SET        1
#define DRAG_RESET      2

struct MinList          arrow_list;

struct TextAttr         topaz8 = { (STRPTR)"topaz.font", 8, FS_NORMAL, 0x0 };

struct BitMap           *off_bm;
struct RastPort         off_rp,
                        off_shine_rp,
                        off_shade_rp;

void                    do_quit( struct IntuiMessage *msg );
void                    do_about( struct IntuiMessage *msg );
void                    do_savepattern( struct IntuiMessage *msg );
void                    do_loadpattern( struct IntuiMessage *msg );
void                    do_clear( struct IntuiMessage *imsg );

struct NewMenu menu_list[] = {
    {   NM_TITLE,   "Project",          NULL,           NULL,   0,  NULL },
    {   NM_ITEM,    "Load Pattern...",  (UBYTE *)"L",   NULL,   0,  do_loadpattern },
    {   NM_ITEM,    "Load Song...",     (UBYTE *)"O",   ITEMENABLED,    0,  NULL },
    {   NM_ITEM,    "Import from MIDI File...",NULL,    ITEMENABLED,0,NULL },
    {   NM_ITEM,    NM_BARLABEL,        NULL,           NULL,   0L },
    {   NM_ITEM,    "Save Pattern...",  (UBYTE *)"S",   NULL,   0,  do_savepattern },
    {   NM_ITEM,    "Save Song...",     (UBYTE *)"W",   ITEMENABLED,0,NULL },
    {   NM_ITEM,    "Export to MIDI File...",NULL,      ITEMENABLED,0,NULL },
    {   NM_ITEM,    NM_BARLABEL,        NULL,           NULL,   0L },
    {   NM_ITEM,    "About...",         (UBYTE *)"?",   NULL,   0,  do_about },
    {   NM_ITEM,    "Quit",             (UBYTE *)"Q",   NULL,   0,  do_quit },
    {   NM_TITLE,   "Edit",             NULL,           NULL,   0,  NULL },
    {   NM_ITEM,    "Copy Pattern",     (UBYTE *)"C",   ITEMENABLED,    0,  NULL },
    {   NM_ITEM,    "Paste Pattern",    (UBYTE *)"V",   ITEMENABLED,    0,  NULL },
    {   NM_ITEM,    "Erase Pattern",    (UBYTE *)"E",   NULL,   0,  do_clear },
    {   NM_ITEM,    NM_BARLABEL,        NULL,           NULL,   0L },
    {   NM_ITEM,    "Reverse Pattern",  (UBYTE *)"R",   ITEMENABLED,    0,  NULL },
    {   NM_TITLE,   "Playback",         NULL,           NULL,           0,  NULL },
    {   NM_ITEM,    "Play",             NULL,           ITEMENABLED,    0,  NULL },
    {   NM_SUB,     "Whole Song",       (UBYTE *)"H",   CHECKIT | CHECKED,  0, NULL },
    {   NM_SUB,     "Pattern",          (UBYTE *)"P",   CHECKIT,            0, NULL },
    {   NM_ITEM,    NM_BARLABEL,        NULL,           NULL,   0L },
    {   NM_ITEM,    "Set Conductor...", (UBYTE *)"T",   ITEMENABLED,    0,  NULL },
    {   NM_ITEM,    "Set MIDI Output...",(UBYTE *)"M",  ITEMENABLED,    0,  NULL },
    {   NM_TITLE,   "Pattern",          NULL,           NULL,   0,  NULL },
    {   NM_ITEM,    "Set Pattern Length...",(UBYTE *)"G",ITEMENABLED,   0, NULL },
    {   NM_ITEM,    "Set Resolution...",NULL,           NULL,           0, NULL },
    {   NM_END,     NULL,               NULL,           0,      0,  NULL }
};

struct Menu             *menu_strip;

    /* drum matrix variables */

WORD                    matrix_top,
                        matrix_bottom,
                        matrix_height,
                        matrix_y_org;

WORD                    total_columns = 32;
                        column_width,
                        matrix_width;

struct DrumPattern      *pattern_table[ MAX_PATTERNS ];
struct DrumPattern      base_pattern,
                        *current_pattern = &base_pattern;

WORD                    solo_drum = -1;

WORD                    current_velocity = 64,
                        current_tempo = 100,
                        current_channel = 5;

Class                   *aclass = NULL;

    /* playback variables */

struct Player           *drum_player;

WORD                    playback_column = 0;

WORD                    clock_state;
struct Task             *main_task;

    /*  Values which are set before playing begins */

enum drum_res           drum_resolution = DRES_NOTE16; /* size of each column       */

UWORD                   mclocks_per_column;     /* metric clocks per column     */
LONG                    pattern_mclocks;        /* metric clocks per pattern    */

    /*  Values which can change during play */

UWORD                   tempo_rate;             /* ratio of mtime / real time   */

    /*  Values which are continuously incremented during play */

LONG                    current_pattern_start,  /* start time of current pattern */
                        current_pattern_end;    /* start time of next pattern   */

LONG                    clock_pos,              /* clock position               */
                        mclock_accumulator,     /* metric time accumulator      */
                        mclock_pos;             /* metric time position         */

LONG                    pattern_pos;            /* score position, in patterns  */
UWORD                   column_pos;             /* column position in pattern   */


#define ASM __asm
#define REG(x) register __## x

extern ULONG ASM TimeServer (REG(a1)struct pmTime *msg, REG(a2)struct Player *pi);

struct Hook myHook = {
    { NULL, NULL },
    TimeServer,
};

/* ============================================================================ *
                                    Glue Functions
 * ============================================================================ */

struct MidiNode *CreateMidi( Tag tag, ... )
{   return CreateMidiA( (struct TagItem *)&tag );
}

BOOL SetMidiAttrs( struct MidiNode *mi, Tag tag, ... )
{   return SetMidiAttrsA( mi, (struct TagItem *)&tag );
}

struct MidiLink *AddMidiLink( struct MidiNode *mi, LONG type, Tag tag, ... )
{   return AddMidiLinkA( mi, type, (struct TagItem *)&tag );
}

BOOL SetMidiLinkAttrs( struct MidiLink *mi, Tag tag, ... )
{   return SetMidiLinkAttrsA( mi, (struct TagItem *)&tag );
}

struct Player *CreatePlayer( Tag tag, ... )
{   return CreatePlayerA( (struct TagItem *)&tag );
}

BOOL SetPlayerAttrs( struct Player *pi, Tag tag, ... )
{   return SetPlayerAttrsA( pi, (struct TagItem *)&tag );
}

/* ============================================================================ *
                                    Error Function
 * ============================================================================ */

LONG ErrorF( struct Window *w, UBYTE *TextFormat, ... )
{   struct EasyStruct es;

    es.es_StructSize = sizeof(struct EasyStruct);
    es.es_Flags = 0;
    es.es_Title = APPNAME " Error Request";
    es.es_TextFormat = TextFormat;
    es.es_GadgetFormat = "Continue";

    return( EasyRequestArgs( w, &es, NULL, (APTR)( (&TextFormat) + 1) ));
}

struct Library *open_lib( char *name )
{   struct Library          *l;

    unless ( l = OpenLibrary( name, 0 ))
    {   ErrorF( NULL, "Can't open library:\n%s", name);
    }
    return l;
}

/* ============================================================================ *
                                     main routine
 * ============================================================================ */

void main( int argc, char *argv[] )
{   struct NewGadget    ng;
    short               row, column;
    BOOL                gadgets_added = FALSE;
    WORD                last_qualifier = 0;

    main_task = FindTask( 0 );

    calc_pattern_ticks();
    calc_tempo();
    base_pattern.dpColumns = total_columns;
    base_pattern.dpRows = DRUM_ROWS;
    base_pattern.dpNumber = 0;

    NewList( (struct List *)&arrow_list );

        /* open libraries */

    unless (IntuitionBase = open_lib( "intuition.library" )) LEAVE;
    unless (GadToolsBase = open_lib( "gadtools.library" )) LEAVE;
    unless (AslBase = open_lib( "asl.library" )) LEAVE;
    unless (GfxBase = open_lib( "graphics.library" )) LEAVE;
    unless (UtilityBase = open_lib( "utility.library" )) LEAVE;
    unless (CamdBase = open_lib( "camd.library" )) LEAVE;
    unless (RealTimeBase = open_lib( "realtime.library" )) LEAVE;
    unless (IFFParseBase = OpenLibrary( "iffparse.library", 0L )) LEAVE;

    aclass = initArrowButtonClass();            /* arrow buttons                */

        /* create the MIDI stuff */

    unless (midi = CreateMidi(
        MIDI_Name, "VU Meters",
        MIDI_ErrFilter, CMEF_All,
        TAG_DONE ))
    {   ErrorF( window, "Unable to create MIDI Node." );
        LEAVE;
    }

    unless (out_link = AddMidiLink ( midi, MLTYPE_Sender,
                MLINK_Location, "out.0",
                MLINK_Comment, "Drum Output",
                MLINK_Name, "Out",
                TAG_END ))
    {   ErrorF( window, "Unable to create MIDI link." );
        LEAVE;
    }

    unless (drum_player = CreatePlayer(
                PLAYER_Name, APPNAME,
                PLAYER_Conductor, "Main",
                PLAYER_Hook, &myHook,
                PLAYER_Priority, 0,
                TAG_DONE ))
    {   ErrorF( window, "Unable to create RealTime player." );
        LEAVE;
    }

        /* open the window */

    window = OpenWindowTags(
                NULL,
                WA_Width,   640,
                WA_InnerHeight, 178,
                WA_IDCMP,   IDCMP_CLOSEWINDOW | IDCMP_MOUSEBUTTONS |
                            IDCMP_MOUSEMOVE | IDCMP_MENUPICK |
                            IDCMP_GADGETUP | IDCMP_GADGETDOWN |
                            IDCMP_RAWKEY | IDCMP_ACTIVEWINDOW |
                            IDCMP_INACTIVEWINDOW | IDCMP_IDCMPUPDATE |
                            SLIDERIDCMP | BUTTONIDCMP, /* help ? */
                WA_Flags,   WFLG_DRAGBAR | WFLG_DEPTHGADGET |
                            WFLG_CLOSEGADGET | WFLG_REPORTMOUSE |
                            WFLG_NOCAREREFRESH | WFLG_ACTIVATE |
                            WFLG_SMART_REFRESH | WFLG_NEWLOOKMENUS,
                WA_Title,   "Drum Machine",
                WA_ScreenTitle, "©1992 Sylvan Technical Arts" );

    unless (window)
    {   ErrorF( window, "Unable to open window." );
        LEAVE;
    }

    unless (vi = GetVisualInfo( window->WScreen, TAG_DONE))
    {   ErrorF( window, "Unable to get screen information." );
        LEAVE;
    }

    unless (dri = GetScreenDrawInfo( window->WScreen ))
    {   ErrorF( window, "Unable to get screen information." );
        LEAVE;
    }

    rp = window->RPort;
    dri_pens = dri->dri_Pens;

        /* create off-screen bitmap */

    unless ( off_bm = AllocBitMap( 32, window->Height, rp->BitMap->Depth, 0, NULL ))
    {   ErrorF( window, "Unable to create off-screen bitmap." );
        LEAVE;
    }

    InitRastPort( &off_rp );
    off_rp.BitMap = off_bm;
    off_shine_rp = off_rp; SetAPen( &off_shine_rp, dri_pens[ SHINEPEN ] );
    off_shade_rp = off_rp; SetAPen( &off_shade_rp, dri_pens[ SHADOWPEN ] );

        /* now, create the gadgets */

    unless (last_gadget = context_gadget = CreateContext( &first_gadget ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }

    ng.ng_VisualInfo = vi;
    ng.ng_TextAttr = &topaz8;

    for (row = 0; row < DRUM_ROWS; row++)
    {
            /* initialize the drum kit while we're at it */

        current_pattern->dpSetup[ row ].name[0] = 0;
        current_pattern->dpSetup[ row ].key = 40 + row;
        current_pattern->dpSetup[ row ].flags = 0;

            /* general parameters for each row */

        ng.ng_TopEdge = window->BorderTop + 12 + row * ROW_HEIGHT;
        ng.ng_Height = ROW_HEIGHT - 1;
        ng.ng_UserData = (APTR)row;             /* user data is row #           */

            /* Mute Button */

        ng.ng_LeftEdge = 7;
        ng.ng_Width = 16;
        ng.ng_GadgetText = "M";
        ng.ng_Flags = PLACETEXT_IN;
        ng.ng_GadgetID = ID_MUTE;
        unless (last_gadget = CreateGadget( BUTTON_KIND, last_gadget, &ng,
                GA_Immediate, TRUE,
                TAG_DONE ))
        {   ErrorF( window, "Unable to create gadgets." );
            LEAVE;
        }

            /* solo button */

        ng.ng_LeftEdge = 24;
        ng.ng_GadgetText = "S";
        ng.ng_GadgetID = ID_SOLO;
        unless (last_gadget = CreateGadget( BUTTON_KIND, last_gadget, &ng,
                GA_Immediate, TRUE,
                TAG_DONE ))
        {   ErrorF( window, "Unable to create gadgets." );
            LEAVE;
        }

            /* preview button */

        ng.ng_LeftEdge = 198;
        ng.ng_GadgetText = "*";
        ng.ng_GadgetID = ID_HIT;
        unless (last_gadget = CreateGadget( BUTTON_KIND, last_gadget, &ng,
                GA_Immediate, TRUE,
                TAG_DONE ))
        {   ErrorF( window, "Unable to create gadgets." );
            LEAVE;
        }

            /* drum name */

        ng.ng_LeftEdge = 42;
        ng.ng_Width = 90;
        if (row == 0) ng.ng_GadgetText = "Name"; else ng.ng_GadgetText = NULL;
        ng.ng_Flags = PLACETEXT_ABOVE;
        ng.ng_GadgetID = ID_PNAME;
        unless (last_gadget = CreateGadget( STRING_KIND, last_gadget, &ng,
                GTST_MaxChars, 24,
                TAG_DONE ))
        {   ErrorF( window, "Unable to create gadgets." );
            LEAVE;
        }
        name_gadgets[ row ] = last_gadget;

            /* key number */

        ng.ng_LeftEdge = 135;
        ng.ng_Width = 38;
        if (row == 0) ng.ng_GadgetText = "   Note #";
        ng.ng_Flags = PLACETEXT_ABOVE;
        ng.ng_GadgetID = ID_KEY;
        unless (last_gadget = CreateGadget( INTEGER_KIND, last_gadget, &ng,
                GTIN_MaxChars, 3,
                GTIN_Number, current_pattern->dpSetup[ row ].key,
                TAG_DONE ))
        {   ErrorF( window, "Unable to create gadgets." );
            LEAVE;
        }
        key_gadgets[ row ] = last_gadget;

            /* key number increment */

        ng.ng_LeftEdge = 173;
        ng.ng_Width = 22;
        ng.ng_GadgetText = NULL;
        ng.ng_GadgetID = ID_KEYINC + (row << 8);

        unless (arrow_gadget = make_arrow( &ng, arrow_gadget, 1, 127, current_pattern->dpSetup[ row ].key ))
        {   ErrorF( window, "Unable to create gadgets." );
            LEAVE;
        }
        keyinc_gadgets[ row ] = arrow_gadget;

        if (first_arrow == NULL) first_arrow = arrow_gadget;
    }

        /* general parameters for bottom row */

    ng.ng_TopEdge = window->BorderTop + 22 + DRUM_ROWS * ROW_HEIGHT;
    ng.ng_Height = 14;
    ng.ng_UserData = NULL;

#if 0
        /* File Name indicator */

    ng.ng_LeftEdge = 8;
    ng.ng_Width = 147;
    ng.ng_GadgetText = NULL;
    ng.ng_Flags = PLACETEXT_IN;
    ng.ng_GadgetID = ID_FILE;
    unless (last_gadget = CreateGadget( TEXT_KIND, last_gadget, &ng,
            GTTX_Text,      "« No File »",
            GTTX_Border,    TRUE,
            GTTX_Clipped,   TRUE,
            TAG_DONE ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }
#endif

        /* Play button */

    ng.ng_LeftEdge = 7 /* 162 */;
    ng.ng_Width = 31;
    ng.ng_GadgetText = ">";
    ng.ng_Flags = PLACETEXT_IN;
    ng.ng_GadgetID = ID_PLAY;
    unless (last_gadget = CreateGadget( BUTTON_KIND, last_gadget, &ng, TAG_DONE ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }

        /* Stop button */

    ng.ng_LeftEdge = 38 /* 193 */;
    ng.ng_Width = 31;
    ng.ng_GadgetText = "[]";
    ng.ng_Flags = PLACETEXT_IN;
    ng.ng_GadgetID = ID_STOP;
    unless (last_gadget = CreateGadget( BUTTON_KIND, last_gadget, &ng, TAG_DONE ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }

        /* Step Fwd button */

    ng.ng_LeftEdge = 73 /* 228 */;
    ng.ng_Width = 22;
    ng.ng_GadgetText = "«";
    ng.ng_Flags = PLACETEXT_IN;
    ng.ng_GadgetID = ID_STEPBACK;
    unless (last_gadget = CreateGadget( BUTTON_KIND, last_gadget, &ng,
        GA_Immediate, TRUE,
        TAG_DONE ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }

        /* Stop button */

    ng.ng_LeftEdge = 95 /* 250 */;
    ng.ng_Width = 22;
    ng.ng_GadgetText = "»";
    ng.ng_Flags = PLACETEXT_IN;
    ng.ng_GadgetID = ID_STEPFWD;
    unless (last_gadget = CreateGadget( BUTTON_KIND, last_gadget, &ng,
        GA_Immediate, TRUE,
        TAG_DONE ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }

        /* Tempo Slider */

    ng.ng_LeftEdge = 197;
    ng.ng_Width = 95;
    ng.ng_GadgetText = "Tempo:   ";
    ng.ng_Flags = PLACETEXT_LEFT;
    ng.ng_GadgetID = ID_TEMPO;
    unless (last_gadget = CreateGadget( SLIDER_KIND, last_gadget, &ng,
                GTSL_Min,           10,
                GTSL_Max,           300,
                GTSL_Level,         current_tempo,
                GTSL_LevelFormat,   "%3.3ld",
                GTSL_MaxLevelLen,   3,
                GA_Immediate,       TRUE,
                GA_RelVerify,       TRUE,
                GA_FollowMouse,     TRUE,
                TAG_DONE ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }

        /* Channel Control */

    ng.ng_LeftEdge = 367 /* 370 */;
    ng.ng_Width = 38 /* 64 */;
    ng.ng_GadgetText = "Channel:";
    ng.ng_Flags = PLACETEXT_LEFT;
    ng.ng_GadgetID = ID_CHANNEL;
    unless (last_gadget = CreateGadget( INTEGER_KIND, last_gadget, &ng,
            GTIN_MaxChars, 2,
            GTIN_Number, current_channel + 1,
            TAG_DONE ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }
    channel_string = last_gadget;

        /* Channel increment */

    ng.ng_LeftEdge += ng.ng_Width;
    ng.ng_Width = 22;
    ng.ng_GadgetText = NULL;
    ng.ng_GadgetID = ID_CHANNELINC;

    unless (arrow_gadget = make_arrow( &ng, arrow_gadget, 0, 15, current_channel ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }
    channel_arrow = arrow_gadget;

        /* Velocity Slider */

    ng.ng_LeftEdge = 541 /* 545 */;
    ng.ng_Width = 88 /* 84 */;
    ng.ng_GadgetText = "Velocity:   ";
    ng.ng_Flags = PLACETEXT_LEFT;
    ng.ng_GadgetID = ID_VELOCITY;
    unless (last_gadget = CreateGadget( SLIDER_KIND, last_gadget, &ng,
                GTSL_Min,   1,
                GTSL_Max,   127,
                GTSL_Level, current_velocity,
                GTSL_MaxLevelLen,   3,
                GTSL_LevelFormat,   "%3.3ld",
                GA_RelVerify,       TRUE,
                TAG_DONE ))
    {   ErrorF( window, "Unable to create gadgets." );
        LEAVE;
    }

    AddGList( window, first_arrow, -1, -1, NULL );
    AddGList( window, first_gadget, -1, -1, NULL );

    gadgets_added = TRUE;
    GT_RefreshWindow( window, NULL );
    RefreshGList( first_arrow, window, NULL, -1 );

        /* render the non-gadget window contents */

    matrix_top = window->BorderTop + 2;
    matrix_bottom = window->BorderTop + 18 + row * ROW_HEIGHT;
    matrix_y_org = matrix_top + 16;
    matrix_height = matrix_bottom - matrix_top;

    column_width = clamp( 8, MAX_MATRIX_WIDTH / total_columns, 31 );
    matrix_width = column_width * total_columns;

    SetDrPt( rp, 0xaaaa );
    for (row = 0; row < DRUM_ROWS; row++)
    {   int             y = matrix_y_org + row * ROW_HEIGHT;

        SetAPen( rp, dri_pens[ SHADOWPEN ] );
        Move( rp, 214, y );
        Draw( rp, 635, y );

        SetAPen( rp, dri_pens[ SHINEPEN ] );
        Move( rp, 214, y + 1 );
        Draw( rp, 635, y + 1 );
    }
    SetDrPt( rp, (UWORD)-1 );

    for (column = 0; column <= total_columns; column++)
    {   render_column( column, TRUE );
    }
    draw_titlebar_scale( );

    SetAPen( rp, dri_pens[ SHADOWPEN ] );
    Move( rp, MATRIX_X, matrix_top - 1 );
    Draw( rp, MATRIX_X + total_columns * column_width, matrix_top - 1 );

    SetAPen( rp, dri_pens[ SHADOWPEN ] );
    Move( rp, window->BorderLeft, matrix_bottom );
    Draw( rp, 635, matrix_bottom );

    SetAPen( rp, dri_pens[ SHINEPEN ] );
    Move( rp, window->BorderLeft, matrix_bottom + 1 );
    Draw( rp, 635, matrix_bottom + 1 );

        /* Create menus */

    unless (menu_strip = CreateMenus( menu_list, GTMN_FrontPen, 0, TAG_END )) LEAVE;
    unless (LayoutMenus( menu_strip, vi, GTMN_NewLookMenus, TRUE, TAG_END ) ) LEAVE;
    SetMenuStrip( window, menu_strip );

        /*  Create ASL File requester */

    unless (filereq = AllocAslRequestTags( ASL_FileRequest,
            ASLFR_Window,       window,
            ASLFR_SleepWindow,  TRUE,
            ASLFR_RejectIcons,  TRUE,
            TAG_DONE ))
    {   ErrorF( window, "Unable to create file requester." );
        LEAVE;
    }

    while (running)
    {   struct IntuiMessage msg_copy;
        struct IntuiMessage *msg;
        struct Gadget       *g;
        struct TagItem      temp_tags[8],       /* hold tags after replymsg     */
                            *tag,
                            *taglist;
        WORD                gadget_id,
                            gadget_value;

        LONG signals;

        signals = Wait( (1 << window->UserPort->mp_SigBit)
                        | SIGBREAKF_CTRL_F
                        | SIGBREAKF_CTRL_E );

        if (signals & SIGBREAKF_CTRL_F)         /* clock signal                 */
        {
            column = playback_column;
            playback_column = column_pos;

            play_drum_note( &current_pattern->dpGrid[ playback_column ], TRUE );
            play_drum_note( &current_pattern->dpGrid[ playback_column ], FALSE );

            render_column( column, FALSE );
            render_column( playback_column, TRUE );
        }

        if (signals & SIGBREAKF_CTRL_E)         /* state change signal          */
        {
            clock_state = drum_player->pl_Source->cdt_State;

            switch (clock_state) {

            case CONDSTATE_STOPPED:

                break;

            case CONDSTATE_PAUSED:

                break;

            case CONDSTATE_LOCATE:

                render_column( playback_column, FALSE );
                locate_to_tick ( drum_player->pl_Source->cdt_ClockTime );
                playback_column = column_pos;

                SetPlayerAttrs( drum_player, PLAYER_Ready, TRUE, TAG_END );
                break;

            case CONDSTATE_RUNNING:

                render_column( playback_column, FALSE );
                locate_to_tick ( drum_player->pl_Source->cdt_ClockTime );
                playback_column = column_pos;

                SetPlayerAttrs( drum_player, PLAYER_Ready, TRUE, TAG_END );

                Signal( main_task, SIGBREAKF_CTRL_F );
                break;
            }
        }

        while (msg = GT_GetIMsg( window->UserPort ))
        {   msg_copy = *msg;

            if (msg_copy.Class == IDCMPUPDATE)
                CopyMem( msg->IAddress, temp_tags, sizeof temp_tags );

            GT_ReplyIMsg( msg );

            switch ( msg_copy.Class ) {
            case IDCMP_CLOSEWINDOW: LEAVE;

            case IDCMP_ACTIVEWINDOW:
            case IDCMP_INACTIVEWINDOW:
                draw_titlebar_scale( );
                break;

            case IDCMP_MENUPICK:
                if (drag_state)
                {   shadow_drag( drag_column, drag_anchor_column );
                    drag_state = 0;
                }

                while (msg_copy.Code != MENUNULL && msg_copy.IDCMPWindow)
                {   struct MenuItem *menuitem;

                    if (menuitem = ItemAddress( msg_copy.IDCMPWindow->MenuStrip,msg_copy.Code ))
                    {   void (*userdata)( struct IntuiMessage * );

                        if (userdata = (void (*)( struct IntuiMessage *))GTMENUITEM_USERDATA(menuitem))
                        {   (userdata)( &msg_copy );
                        }
                        msg_copy.Code = menuitem->NextSelect;
                    }
                    else break;
                }

                break;

            case IDCMP_IDCMPUPDATE:
                taglist = temp_tags;

                while (tag = NextTagItem( &taglist ))
                {
                    switch (tag->ti_Tag) {
                    case GA_ID: gadget_id = tag->ti_Data; break;
                    }
                }

                switch ( gadget_id & 0xff ) {
                case ID_KEYINC:

                    gadget_value = msg_copy.Code;
                    row = gadget_id >> 8;
                    current_pattern->dpSetup[ row ].key = gadget_value;
                    GT_SetGadgetAttrs( key_gadgets[ row ], window, NULL,
                            GTIN_Number, gadget_value,
                            TAG_DONE );

                    if (last_qualifier & (IEQUALIFIER_LALT|IEQUALIFIER_RALT))
                        preview_row( row );

                    break;

                case ID_CHANNELINC:
                    gadget_value = msg_copy.Code;
                    current_channel = clamp( 0, gadget_value, 15);
                    GT_SetGadgetAttrs( channel_string, window, NULL,
                            GTIN_Number, current_channel + 1,
                            TAG_DONE );
                    break;

                }
                break;

            case IDCMP_GADGETDOWN:
                g = (struct Gadget *)msg_copy.IAddress;
                row = (int)g->UserData;

                switch (g->GadgetID) {
                case ID_HIT:        preview_row( row ); break;
                case ID_STEPFWD:    step_play( 1 ); break;
                case ID_STEPBACK:   step_play( -1 ); break;
                case ID_TEMPO:      tempo_drag = TRUE; break;

                };
                break;

            case IDCMP_GADGETUP:
                g = (struct Gadget *)msg_copy.IAddress;
                row = (int)g->UserData;

                switch (g->GadgetID) {

                case ID_PLAY:
                    SetConductorState( drum_player, CONDSTATE_LOCATE, 0 );
                    break;

                case ID_STOP:
                    SetConductorState( drum_player, CONDSTATE_STOPPED, 0 );
                    break;

                case ID_KEY:
                    current_pattern->dpSetup[ row ].key =
                        ((struct StringInfo *)g->SpecialInfo)->LongInt;

                    SetGadgetAttrs( keyinc_gadgets[ row ], window, NULL,
                        ARROW_Current, current_pattern->dpSetup[ row ].key,
                        TAG_END );

                    break;

                case ID_CHANNEL:

                    current_channel =
                        clamp(0, ((struct StringInfo *)g->SpecialInfo)->LongInt - 1, 15);

                    GT_SetGadgetAttrs( key_gadgets[ row ], window, NULL,
                            GTIN_Number, current_channel + 1,
                            TAG_DONE );

                    SetGadgetAttrs( channel_arrow, window, NULL,
                        ARROW_Current, current_channel,
                        TAG_END );

                    break;

                case ID_PNAME:

                    strncpy(current_pattern->dpSetup[ row ].name,
                            ((struct StringInfo *)g->SpecialInfo)->Buffer,
                            sizeof current_pattern->dpSetup[ row ].name );
                    break;

                case ID_TEMPO:
                    current_tempo = msg_copy.Code;
                    tempo_drag = FALSE;
                    calc_tempo();
                    break;

                case ID_VELOCITY:
                    current_velocity = msg_copy.Code;
                    break;

                };
                break;

            case IDCMP_MOUSEMOVE:

                if (tempo_drag)
                {   current_tempo = msg_copy.Code;
                    calc_tempo();
                }
                else if (drag_state)
                {
                    column = msg_copy.MouseX - MATRIX_X + column_width / 2;
                    column /= column_width;

                    if (column >= 0 && column < total_columns && column != drag_column )
                    {   shadow_drag( drag_column, drag_anchor_column );
                        drag_column = column;
                        shadow_drag( drag_column, drag_anchor_column );
                    }
                }
                break;

            case IDCMP_MOUSEBUTTONS:

                if (msg_copy.Code == SELECTDOWN)
                {
                    row = msg_copy.MouseY - window->BorderTop - 12;
                    column = msg_copy.MouseX - MATRIX_X + column_width / 2;

                    if (row >= 0 && column >= 0)
                    {   row /= ROW_HEIGHT;
                        column /= column_width;

                        if (column == total_columns) column = 0;

                        if (row < DRUM_ROWS && column < total_columns)
                        {   struct DrumColumn   *dc = &current_pattern->dpGrid[ column ];

                            if (dc->note[ row ] > 0)
                            {   dc->note[ row ] = 0;
                                drag_state = DRAG_RESET;
                            }
                            else
                            {   dc->note[ row ] = current_velocity;
                                drag_state = DRAG_SET;
                            }

                            drag_column = drag_anchor_column = column;
                            drag_row = row;

                            render_column( column, TRUE );
                            if (column == 0) render_column( total_columns, FALSE );

                            if (dc->note[ row ] > 0 &&
                                (msg_copy.Qualifier & (IEQUALIFIER_LALT|IEQUALIFIER_RALT)))
                                    preview_row( row );
                        }
                    }
                }
                else if (msg_copy.Code == SELECTUP)
                {
                    if (drag_state && drag_column != drag_anchor_column)
                    {   int i;

                        shadow_drag( drag_column, drag_anchor_column );

                        if (drag_column < drag_anchor_column)
                        {   i = drag_column;
                            drag_column = drag_anchor_column;
                            drag_anchor_column = i;
                        }

                        for (i=drag_anchor_column; i<=drag_column; i++)
                        {   struct DrumColumn   *dc;

                            column = (i == total_columns) ? 0 : i;

                            dc = &current_pattern->dpGrid[ column ];

                            if (drag_state == DRAG_RESET) dc->note[ row ] = 0;
                            else dc->note[ row ] = current_velocity;

                            render_column( column, TRUE );
                            if (column == 0) render_column( total_columns, FALSE );
                        }
                    }
                    drag_state = 0;
                }
                break;

            case IDCMP_RAWKEY:

                if (drag_state)
                {   shadow_drag( drag_column, drag_anchor_column );
                    drag_state = 0;
                }

                last_qualifier = msg_copy.Qualifier;

                switch (msg_copy.Code) {
                case 0x4e:                      /* cursor forward               */
                    step_play( 1 );
                    break;

                case 0x4f:                      /* cursor backwards             */
                    step_play( -1 );
                    break;

                case 1:                         /* number keys are drums...     */
                case 2:
                case 3:
                case 4:
                case 5:
                case 6:
                case 7:
                case 8:
                case 9:
                case 10:
                    preview_row( msg_copy.Code - 1 );
                    /* Holding control key enters or deleted from score at current
                        playback position, minus a bit... */
                    break;
                };
            };
        }
    }
exitit:
    if (drum_player) DeletePlayer( drum_player );
    if (midi) DeleteMidi( midi );

    FreeBitMap( off_bm );

    if (filereq) FreeAslRequest( filereq );

    if (window) { ClearMenuStrip( window ); CloseWindow( window ); }
    if (menu_strip) FreeMenus( menu_strip );

/*  if (gadgets_added) RemoveGList( window, first_gadget, -1 ); */
    FreeGadgets( first_gadget );
    DisposeObjectList( (struct List *)&arrow_list );
    FreeScreenDrawInfo( window->WScreen, dri );
    FreeVisualInfo( vi );

    freeArrowButtonClass(aclass);               /* delete boopsi classes        */

    CloseLibrary( IFFParseBase );
    CloseLibrary( RealTimeBase );
    CloseLibrary( CamdBase );
    CloseLibrary( GfxBase );
    CloseLibrary( UtilityBase );
    CloseLibrary( AslBase );
    CloseLibrary( GadToolsBase );
    CloseLibrary( IntuitionBase );
}

/* ============================================================================ *
                                   Editing Routines
 * ============================================================================ */

    /* add an object to a list */

ULONG AddTailObject( Object *o, struct List *list)
{   return DoMethod( o, OM_ADDTAIL, list );
}

void DisposeObjectList( struct List *l )
{   APTR                *obj_state = (APTR)l->lh_Head,
                        *obj;

    while ( obj = NextObject( &obj_state ) )    /* iterate through list         */
    {   DoMethod( (Object *)obj, OM_REMOVE );   /* remove from list             */
        DisposeObject( obj );
    }
}

struct TagItem arrow_map[] = {
    ARROW_Current,  ICSPECIAL_CODE,
    TAG_END,
};

struct Gadget *make_arrow( struct NewGadget *ng, struct Gadget *prev, int min, int max, int current )
{   struct Gadget       *g;

    g = (struct Gadget *)NewObject(aclass,NULL,
            GA_Left,        ng->ng_LeftEdge,
            GA_Top,         ng->ng_TopEdge,
            GA_Width,       ng->ng_Width,
            GA_Height,      ng->ng_Height,
            GA_DrawInfo,    dri,
            GA_ID,          ng->ng_GadgetID,
            GA_UserData,    ng->ng_UserData,

            ARROW_Min,      min,
            ARROW_Max,      max,
            ARROW_Current,  current,
            ARROW_IncreaseRate, 5,
            ARROW_MaxRate,  20,

            ICA_TARGET,     ICTARGET_IDCMP,
            ICA_MAP,        arrow_map,

            prev ? GA_Previous : TAG_IGNORE, prev,
            TAG_END );

    if (g) AddTailObject( (Object *)g, (struct List *)&arrow_list );

    return g;
}

void draw_titlebar_scale( void )
{   int                 column;
    int                 y = window->BorderTop - 2;

    for (column = 0; column <= total_columns; column+= 2)
    {   int             x = MATRIX_X + column * column_width;
        int             h;

        if ((column & 0x07) == 0) h = 7;
        else if ((column & 0x03) == 0) h = 3;
        else h = 1;

        SetAPen( rp, dri_pens[ SHADOWPEN ] );
        Move( rp, x, y - h );
        Draw( rp, x, y );

        SetAPen( rp, dri_pens[ SHINEPEN ] );
        Move( rp, x + 1, y - h );
        Draw( rp, x + 1, y );
    }
}

void render_column( int column, int highlight )
{   int                 side_width = column_width / 2;
    int                 x = MATRIX_X + column * column_width;
    int                 row;
    struct DrumColumn   *dc = &current_pattern->dpGrid[ column >= total_columns ? 0 : column ];

    if (column != playback_column) highlight = FALSE;

    SetAPen( &off_rp, 0 );
    RectFill( &off_rp, 0, 0, column_width, matrix_height );
    SetAPen( &off_rp, dri_pens[ highlight ? HIGHLIGHTTEXTPEN : TEXTPEN ] );

    SetDrPt( &off_shine_rp, (x & 1) ? 0xaaaa : 0x5555 );
    SetDrPt( &off_shade_rp, (x & 1) ? 0xaaaa : 0x5555 );

        /* draw the individual rows */

    for (row = 0; row < DRUM_ROWS; row++)
    {   int             y = 16 + row * ROW_HEIGHT;

        Move( &off_shade_rp, 0, y );
        Draw( &off_shade_rp, column_width, y );

        Move( &off_shine_rp, 0, y + 1 );
        Draw( &off_shine_rp, column_width, y + 1 );
    }

    SetDrPt( &off_shine_rp, (UWORD)-1 );
    SetDrPt( &off_shade_rp, (UWORD)-1 );

    if (highlight)
    {   Move( &off_shine_rp, side_width - 1, 0 );
        Draw( &off_shine_rp, side_width - 1, matrix_height );
    }
    else
    {   Move( &off_shade_rp, side_width - 1, 0 );
        Draw( &off_shade_rp, side_width - 1, matrix_height );
    }
    Move( &off_shine_rp, side_width, 0 );
    Draw( &off_shine_rp, side_width, matrix_height );

    for (row = 0; row < DRUM_ROWS; row++)
    {   int             y = 16 + row * ROW_HEIGHT;
        int             hit,
                        dotsize;

        hit = dc->note[ row ];
        if (hit > 0)
        {   if (hit > 90) dotsize = 4;
            else if (hit > 60) dotsize = 3;
            else if (hit > 30) dotsize = 2;
            else dotsize = 1;

            RectFill( &off_rp,  side_width - dotsize - 1,
                                y - dotsize,
                                side_width + dotsize - 1,
                                y + dotsize );
        }
    }

    BltBitMapRastPort(  off_bm, 0, 0,
                        rp, x - side_width + 1, matrix_top,
                        side_width + side_width, matrix_height,
                        0xC0 );
}

void render_all( void )
{   int                 column;

    for (column = 0; column <= total_columns; column++)
    {   render_column( column, TRUE );
    }

    /* also render gadgets */
}

void set_kit_gadgets( void )
{   int                 row;

    for (row = 0; row < DRUM_ROWS; row++)
    {
        GT_SetGadgetAttrs( key_gadgets[ row ], window, NULL,
                GTIN_Number, current_pattern->dpSetup[ row ].key,
                TAG_DONE );

        SetGadgetAttrs( keyinc_gadgets[ row ], window, NULL,
            ARROW_Current, current_pattern->dpSetup[ row ].key,
            TAG_END );

        GT_SetGadgetAttrs( name_gadgets[ row ], window, NULL,
                GTST_String, current_pattern->dpSetup[ row ].name,
                TAG_DONE );
    }
}

void preview_row( int row )
{   struct DrumColumn   preview_col;

    memset( &preview_col, 0, sizeof preview_col );
    preview_col.note[ row ] = current_velocity;

    play_drum_note( &preview_col, TRUE );
    play_drum_note( &preview_col, FALSE );
}

void play_drum_note( struct DrumColumn *dc, BOOL note_on )
{   int                 row;
    MidiMsg             mm;

    mm.mm_Status = MS_NoteOn | current_channel;

    for (row = 0; row < DRUM_ROWS; row++)
    {   if (dc->note[ row ] > 0 && !(current_pattern->dpSetup[ row ].flags & DRUMF_MUTE))
        {
            mm.mm_Data1 = current_pattern->dpSetup[ row ].key;
            mm.mm_Data2 = note_on ? dc->note[ row ] : 0;

            PutMidiMsg ( out_link, &mm );
        }
    }
}

void step_play( int dir )
{   int column = playback_column;

    if (dir > 0)
    {   playback_column++;
        if (playback_column >= total_columns) playback_column = 0;
    }
    else
    {   playback_column--;
        if (playback_column < 0) playback_column = total_columns - 1;
    }

    play_drum_note( &current_pattern->dpGrid[ playback_column ], TRUE );
    play_drum_note( &current_pattern->dpGrid[ playback_column ], FALSE );

    render_column( column, FALSE );
    render_column( playback_column, TRUE );
}

void shadow_drag( int col1, int col2 )
{   int                 x1 = MATRIX_X + col1 * column_width,
                        x2 = MATRIX_X + col2 * column_width;
    int                 y = matrix_y_org + drag_row * ROW_HEIGHT;

    if (drag_column != drag_anchor_column)
    {   SetDrMd( rp, COMPLEMENT );
        Move( rp, x1, y );
        Draw( rp, x2, y );
        SetDrMd( rp, JAM2 );
    }
}

/* ============================================================================ *
                                 Save / Load routines
 * ============================================================================ */

    /* REM: Is the list of sounds local or global?
        Samples should be global,
        sounds should be local...
    */

    /*  Print error message from IFFParse */

void IFFError(char *filename, int writing, int error)
{   char                *msg;

    switch ( error ) {
    case -100:                  msg = "Can't open file."; break;
    case IFFERR_EOF:            return;
    case IFFERR_EOC:            msg = "File is Incomplete";
    case IFFERR_NOSCOPE:        return;

    case -101:
    case IFFERR_NOMEM:          msg = "Out of Memory"; break;

    case IFFERR_READ:           msg = "Read Error"; break;
    case IFFERR_WRITE:          msg = "Write Error"; break;
    case IFFERR_SEEK:           msg = "Read Error"; break;
    case IFFERR_MANGLED:        msg = "File is Corrupt"; break;
    case IFFERR_SYNTAX:         msg = "File is Incomplete"; break;
    case IFFERR_NOTIFF:         msg = "Not an IFF File"; break;

    case IFFERR_NOHOOK:
    case IFF_RETURN2CLIENT:
    default:                    msg = "Internal Error"; break;
    }

    if (filename)
    {   ErrorF(
            window,
            writing ? "Error Writing File '%s':\n%s." : "Error Reading File '%s':\n%s.",
            filename,
            msg );
    }
    else
    {   ErrorF(
            window,
            writing ? "Error Writing to Clipboard:\n%s." : "Error Reading from Clipboard:\n%s",
            msg );
    }

}

WORD read_kit( struct IFFHandle *iff, struct DrumPattern *dp )
{   char                *tbuf;
    struct ContextNode  *cn;
    WORD                error = FALSE;

    unless (cn = CurrentChunk (iff)) return -102;       /* internal error */
    if (tbuf = AllocMem( cn->cn_Size, 0L ))
    {   if ((error = ReadChunkBytes( iff, tbuf, cn->cn_Size )) >= 0)
        {
            CopyMem( tbuf, &ReadKit, MIN( cn->cn_Size, sizeof ReadKit ) );
            CopyMem( &ReadKit, dp->dpSetup, sizeof dp->dpSetup ); /* temporary */
            error = 0;
        }
        FreeMem( tbuf, cn->cn_Size );
    }
    return error;
}

WORD read_pattern( struct IFFHandle *iff, struct DrumPattern *dp )
{   char                *tbuf;
    struct ContextNode  *cn;
    WORD                error = FALSE;

    unless (cn = CurrentChunk (iff)) return -102;       /* internal error */
    if (tbuf = AllocMem( cn->cn_Size, 0L ))
    {   if ((error = ReadChunkBytes( iff, tbuf, cn->cn_Size )) >= 0)
        {   int         i,j;
            struct DrumPattern  *pat;
            char        *array;
            WORD        array_bytes;

            pat = (struct DrumPattern *)tbuf;
        /*  dp->dpNumber = pat->dp_Number;  */  /* wrong? */
            dp->dpColumns = MIN( pat->dpColumns, MAX_COLUMNS );
            dp->dpRows = DRUM_ROWS;

            array = tbuf + offsetof (struct DrumPattern, dpSetup);
            array_bytes = cn->cn_Size - offsetof( struct DrumPattern, dpSetup );

            memset( &dp->dpGrid, 0, sizeof dp->dpGrid );

            for ( i=0; i<dp->dpColumns; i++)
            {
                for (j=0; j<dp->dpRows; j++)
                {   WORD    val = 0;

                    if (i < pat->dpColumns && j < pat->dpRows)
                    {   val = array[j];
                    }
                    dp->dpGrid[i].note[j] = val;
                }
                array += dp->dpRows;
            }
            error = 0;
        }
        FreeMem( tbuf, cn->cn_Size );
    }
    return error;
}

WORD write_kit( struct IFFHandle *iff, struct DrumPattern *dp )
{   int                 size = sizeof dp->dpSetup;
    WORD                error;

    if (error = PushChunk( iff, 0L, 'DKIT', size )) return error;
    if ((error = WriteChunkBytes (iff, (APTR) &dp->dpSetup, size)) != size) return error;
    if (error = PopChunk (iff)) return error;
    return 0;
}

WORD write_pattern( struct IFFHandle *iff, struct DrumPattern *dp )
{   int                 size1 = offsetof( struct DrumPattern, dpSetup),
                        size2 = sizeof (struct DrumColumn) * dp->dpColumns;
    WORD                error;

    if (error = PushChunk( iff, 0L, 'PTRN', size1 + size2 )) return error;
    if ((error = WriteChunkBytes( iff, (APTR) dp, size1 )) < 0) return error;
    if ((error = WriteChunkBytes( iff, (APTR) &dp->dpGrid, size2 )) < 0) return error;
    if (error = PopChunk (iff)) return error;
    return 0;
}

#if 0
BOOL read_song( void )
{   ;
}

BOOL write_song( void )
{   ;
}
#endif

static LONG         cmus_chunks[] = {
    'DRUM', 'DKIT',
    'DRUM', 'PTRN',
};

BOOL load_file( BOOL all )
{   struct IFFHandle    *iff = NULL;
    BOOL                error = 0;
    BPTR                fh = NULL,
                        olddir,
                        dlock = NULL;

        /*  Bring up the file requester */

    if (!AslRequestTags( (APTR)filereq,
                    ASLFR_TitleText,    all ? "Load Song" : "Load Pattern",
                    ASLFR_PositiveText, "Load",
                    TAG_DONE ))
    {   return 0;
    }

        /*  Lock the directory they typed */

    unless (dlock = Lock( filereq->rf_Dir, ACCESS_READ ))
    {   ErrorF( window, "Can't find directory: '%s'.", filereq->rf_Dir );
    }

        /*  CD to that directory and open the file */

    olddir = CurrentDir( dlock );
    fh = Open( filereq->rf_File, MODE_OLDFILE );
    CurrentDir( olddir );

        /*  Init IFF Stream */

    unless (fh) { error = -100; LEAVE; }
    unless (iff = AllocIFF()) { error = -101; LEAVE; }
    iff->iff_Stream = fh;
    InitIFFasDOS (iff);

        /*  Read the pattern */

    if (error = OpenIFF (iff, IFFF_WRITE)) LEAVE;
    if (error = StopChunks (iff, cmus_chunks, elementsof (cmus_chunks) / 2)) LEAVE;

    for (;;)
    {   struct ContextNode  *cn;

        if (error = ParseIFF (iff, IFFPARSE_SCAN))
        {   if (error == IFFERR_EOF) break;
            LEAVE;
        }

        unless (cn = CurrentChunk (iff))    /* shouldn't happen             */
        {   error = IFFERR_EOC;
            LEAVE;
        }

        switch (cn->cn_ID) {
        case 'DKIT':
            if (error = read_kit( iff, current_pattern )) LEAVE;
            break;

        case 'PTRN':
            if (error = read_pattern( iff, current_pattern )) LEAVE;
            break;
        }
    }

exitit:

        /*  Clean up */

    if (iff)
    {   CloseIFF (iff);
        FreeIFF (iff);
    }
    if (fh) Close( fh );
    if (dlock) UnLock( dlock );

        /*  Report Error */

    if (error)
    {   IFFError( filereq->rf_File, FALSE, error);
    }

    render_all();
    set_kit_gadgets();

    return error;
}

BOOL save_file( BOOL all )
{   struct IFFHandle    *iff = NULL;
    BOOL                error = 0;
    BPTR                fh = NULL,
                        olddir,
                        dlock = NULL;

        /*  Bring up the file requester */

    if (!AslRequestTags( (APTR)filereq,
                    ASLFR_TitleText,    all ? "Save Song" : "Save Pattern",
                    ASLFR_PositiveText, "Save",
                    ASLFR_DoSaveMode,   TRUE,
                    TAG_DONE ))
    {   return 0;
    }

        /*  Lock the directory they typed */

    unless (dlock = Lock( filereq->rf_Dir, ACCESS_READ ))
    {   ErrorF( window, "Can't find directory: '%s'.", filereq->rf_Dir );
    }

        /*  CD to that directory and open the file */

    olddir = CurrentDir( dlock );
    fh = Open( filereq->rf_File, MODE_NEWFILE );
    CurrentDir( olddir );

        /*  Init IFF Stream */

    unless (fh) { error = -100; LEAVE; }
    unless (iff = AllocIFF()) { error = -101; LEAVE; }
    iff->iff_Stream = fh;
    InitIFFasDOS (iff);

        /*  Write the pattern */

    if (error = OpenIFF (iff, IFFF_WRITE)) LEAVE;
    if (error = PushChunk (iff, 'DRUM', 'FORM', IFFSIZE_UNKNOWN)) LEAVE;
    if (error = write_kit( iff, current_pattern )) LEAVE;
    if (error = write_pattern( iff, current_pattern )) LEAVE;
    if (error = PopChunk (iff)) LEAVE;

exitit:

        /*  Clean up */

    if (iff)
    {   CloseIFF (iff);
        FreeIFF (iff);
    }
    if (fh) Close( fh );
    if (dlock) UnLock( dlock );

        /*  Report Error */

    if (error)
    {   IFFError( filereq->rf_File, TRUE, error);
    }
    return error;
}

/* ============================================================================ *
                                    Menu Functions
 * ============================================================================ */

void do_loadpattern( struct IntuiMessage *imsg )
{
    load_file( FALSE );
}

void do_savepattern( struct IntuiMessage *imsg )
{
    save_file( FALSE );
}

void do_about( struct IntuiMessage *imsg )
{   struct EasyStruct   es;
    APTR                dummyarg;

    es.es_StructSize = sizeof(struct EasyStruct);
    es.es_Flags = 0;
    es.es_Title = APPNAME " Program Information";
    es.es_TextFormat = "Drum Machine CAMD/RealTime Example\nBy Talin\n©1992 Sylvan Technical Arts";
    es.es_GadgetFormat = "Continue";

    EasyRequestArgs( window, &es, NULL, (APTR)(&dummyarg) );
}

void do_quit( struct IntuiMessage *imsg )
{
    running = FALSE;
    imsg->IDCMPWindow = NULL;               /* end NextSelect chain             */
}

void do_clear( struct IntuiMessage *imsg )
{
    memset( &current_pattern->dpGrid, 0, sizeof current_pattern->dpGrid );
    render_all();
}

/* ============================================================================ *
                                  Playback routines
 * ============================================================================ */

    /*  This calculates how big, in clock terms, the columns and the overall
        pattern is.
    */

void calc_pattern_ticks( void )
{
        /* calculate number of metric clocks per column */

    mclocks_per_column = ((MCLOCKS_PER_QNOTE * 4) >> drum_resolution);

        /* calculate the number of metric clocks per pattern */

    pattern_mclocks = total_columns * mclocks_per_column;
}

    /*  Calculate the tempo multiplication factor. The tempo is specified
        in quarter notes per minute; We need to convert that to metric
        clocks per real clock, plus a scaling factor of 256 (for 8-bit fixed
        point accuracy).
    */

void calc_tempo( void )
{   tempo_rate = current_tempo * (MCLOCKS_PER_QNOTE << 8) / (TICK_FREQ * 60);
}

    /*  Locate to a particular metric time (in this case "locating" is fairly
        trivial, since all patterns are required to be the same length.
    */

void locate_to_mclock( long mclocks )
{   WORD                column;

        /*  Unlike a normal division, we want numbers less than zero to
            round down, not up. So a seperate case is required for negative
            clock times.
        */

    mclock_accumulator = mclocks << 8;

    clock_pos = (mclock_accumulator / tempo_rate);

    if (mclocks >= 0)
    {   pattern_pos = mclocks / pattern_mclocks;
    }
    else
    {   pattern_pos = (mclocks - pattern_mclocks + 1) / pattern_mclocks;
    }

        /*  Calculate the start and end of the current pattern in metric ticks */

    current_pattern_start = pattern_pos * pattern_mclocks;
    current_pattern_end   = current_pattern_start + pattern_mclocks;

        /*  Now, figure out which column we are on */

    column = (mclocks - current_pattern_start) / mclocks_per_column;

/*  if (REVERSE) column_pos = total_columns - (column + 1) */

    column_pos = column;
}

    /*  Locate to a particular real time. Since we assume that tempo is
        constant (no tempo changes) this is also trivial -- but needs to be
        done in extended precision if we are going to allow long songs
        without overflow.
    */

void locate_to_tick ( long tick_count )
{
        /* REM: do extended multiply in assy... */

    locate_to_mclock( (tick_count * tempo_rate) >> 8 );
}

#if 0
    /*  Play to a particular clock time. This is different than the above
        routine in that it assumes that the current clock position is
        less than but near to the new clock time, and thus calculates
        using incremental addition rather than a full divide. If we were
        doing a full-on sequencer, we would exploit this philosophy even
        more fully.

        Also, it signals the tasks if the column position changed.
        Thus, the task only needs to wake up when a drum beat needs to
        actually be played.
    */

void play_to_mclock( long new_mclocks )
{   WORD                column;

        /*  See if the clock counted past the end of the frame */

    while (new_mclocks >= current_pattern_end)
    {   pattern_pos++;
        current_pattern_start = current_pattern_end;
        current_pattern_end   = current_pattern_start + pattern_mclocks;
        /* mark for signal...? */
    }

    column = (clock_count - current_pattern_start) / mclocks_per_column;

/*  if (REVERSE) column = total_columns - (column + 1) */

    if (column != column_pos)
    {   column_pos = column;
    }
}
#endif
