/*
 * asap.c - ASAP engine
 *
 * Copyright (C) 2005-2010  Piotr Fusik
 *
 * This file is part of ASAP (Another Slight Atari Player),
 * see http://asap.sourceforge.net
 *
 * ASAP is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 *
 * ASAP is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with ASAP; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "codeclib.h"
#include "asap_internal.h"

static byte s_memory[65536];
static ASAP_ModuleInfo s_module_info;

#ifdef ASAP_ONLY_INFO

#define GET_PLAYER(name)  NULL

#else

#define GET_PLAYER(name)  GET_RESOURCE(name, obx)

FUNC(int, ASAP_GetByte, (P(ASAP_State PTR, ast), P(int, addr)))
{
    switch (addr & 0xff1f) {
    case 0xd014:
        return ast _ module_info->ntsc ? 0xf : 1;
    case 0xd20a:
    case 0xd21a:
        return PokeySound_GetRandom(ast, addr, ast _ cycle);
    case 0xd20e:
        return ast _ irqst;
    case 0xd21e:
        if (ast _ extra_pokey_mask != 0) {
            /* interrupts in the extra POKEY not emulated at the moment */
            return 0xff;
        }
        return ast _ irqst;
    case 0xd20c:
    case 0xd21c:
    case 0xd20f: /* just because some SAP files rely on this */
    case 0xd21f:
        return 0xff;
    case 0xd40b:
    case 0xd41b:
        return ast _ scanline_number >> 1;
    default:
        return dGetByte(addr);
    }
}

FUNC(void, ASAP_PutByte, (P(ASAP_State PTR, ast), P(int, addr), P(int, data)))
{
    if ((addr >> 8) == 0xd2) {
        if ((addr & (ast _ extra_pokey_mask + 0xf)) == 0xe) {
            ast _ irqst |= data ^ 0xff;
#define SET_TIMER_IRQ(ch) \
            if ((data & ast _ irqst & ch) != 0) { \
                if (ast _ timer##ch##_cycle == NEVER) { \
                    V(int, t) = ast _ base_pokey.tick_cycle##ch; \
                    while (t < ast _ cycle) \
                        t += ast _ base_pokey.period_cycles##ch; \
                    ast _ timer##ch##_cycle = t; \
                    if (ast _ nearest_event_cycle > t) \
                        ast _ nearest_event_cycle = t; \
                } \
            } \
            else \
                ast _ timer##ch##_cycle = NEVER;
            SET_TIMER_IRQ(1);
            SET_TIMER_IRQ(2);
            SET_TIMER_IRQ(4);
        }
        else
            PokeySound_PutByte(ast, addr, data);
    }
    else if ((addr & 0xff0f) == 0xd40a) {
        if (ast _ cycle <= ast _ next_scanline_cycle - 8)
            ast _ cycle = ast _ next_scanline_cycle - 8;
        else
            ast _ cycle = ast _ next_scanline_cycle + 106;
    }
    else if ((addr & 0xff00) == ast _ module_info->covox_addr) {
        V(PokeyState PTR, pst);
        addr &= 3;
        if (addr == 0 || addr == 3)
            pst = ADDRESSOF ast _ base_pokey;
        else
            pst = ADDRESSOF ast _ extra_pokey;
        pst _ delta_buffer[CYCLE_TO_SAMPLE(ast _ cycle)] += (data - UBYTE(ast _ covox[addr])) << DELTA_SHIFT_COVOX;
        ast _ covox[addr] = CAST(byte) (data);
    }
    else if ((addr & 0xff1f) == 0xd01f) {
        V(int, sample) = CYCLE_TO_SAMPLE(ast _ cycle);
        V(int, delta);
        data &= 8;
        /* NOT data - ast _ consol; reverse to the POKEY sound */
        delta = (ast _ consol - data) << DELTA_SHIFT_GTIA;
        ast _ consol = data;
        ast _ base_pokey.delta_buffer[sample] += delta;
        ast _ extra_pokey.delta_buffer[sample] += delta;
    }
    else
        dPutByte(addr, data);
}

#endif /* ASAP_ONLY_INFO */

#define UWORD(array, index)  (UBYTE(array[index]) + (UBYTE(array[(index) + 1]) << 8))

#ifndef ASAP_ONLY_SAP

#ifndef ASAP_ONLY_INFO

#ifndef JAVA
#include "players.h"
#endif

#define CMR_BASS_TABLE_OFFSET  0x70f

CONST_ARRAY(byte, cmr_bass_table)
    0x5C, 0x56, 0x50, 0x4D, 0x47, 0x44, 0x41, 0x3E,
    0x38, 0x35, CAST(byte) (0x88), 0x7F, 0x79, 0x73, 0x6C, 0x67,
    0x60, 0x5A, 0x55, 0x51, 0x4C, 0x48, 0x43, 0x3F,
    0x3D, 0x39, 0x34, 0x33, 0x30, 0x2D, 0x2A, 0x28,
    0x25, 0x24, 0x21, 0x1F, 0x1E
END_CONST_ARRAY;

#endif /* ASAP_ONLY_INFO */

CONST_ARRAY(int, perframe2fastplay)
    312, 312 / 2, 312 / 3, 312 / 4
END_CONST_ARRAY;

/* Loads native module (anything except SAP) and 6502 player routine. */
PRIVATE FUNC(abool, load_native, (
    P(ASAP_State PTR, ast), P(ASAP_ModuleInfo PTR, module_info),
    P(CONST BYTEARRAY, module), P(int, module_len), P(RESOURCE, player)))
{
#ifndef ASAP_ONLY_INFO
    V(int, player_last_byte);
#endif
    V(int, music_last_byte);
    V(int, block_len);
    if ((UBYTE(module[0]) != 0xff || UBYTE(module[1]) != 0xff)
     && (module[0] != 0 || module[1] != 0)) /* some CMC and clones start with zeros */
        return FALSE;
    module_info _ music = UWORD(module, 2);
#ifndef ASAP_ONLY_INFO
    module_info _ player = UWORD(player, 2);
    player_last_byte = UWORD(player, 4);
    if (module_info _ music <= player_last_byte)
        return FALSE;
#endif
    music_last_byte = UWORD(module, 4);
    if (module_info _ music <= 0xd7ff && music_last_byte >= 0xd000)
        return FALSE;
    block_len = music_last_byte + 1 - module_info _ music;
    if (6 + block_len != module_len) {
        V(int, info_addr);
        V(int, info_len);
        if (module_info _ type != ASAP_TYPE_RMT || 11 + block_len > module_len)
            return FALSE;
        /* allow optional info for Raster Music Tracker */
        info_addr = UWORD(module, 6 + block_len);
        if (info_addr != module_info _ music + block_len)
            return FALSE;
        info_len = UWORD(module, 8 + block_len) + 1 - info_addr;
        if (10 + block_len + info_len != module_len)
            return FALSE;
    }
#ifndef ASAP_ONLY_INFO
    if (ast != NULL) {
        COPY_ARRAY(ast _ memory, module_info _ music, module, 6, block_len);
        COPY_ARRAY(ast _ memory, module_info _ player, player, 6, player_last_byte + 1 - module_info _ player);
    }
#endif
    return TRUE;
}

PRIVATE FUNC(void, set_song_duration, (P(ASAP_ModuleInfo PTR, module_info), P(int, player_calls)))
{
    module_info _ durations[module_info _ songs] = TO_INT(player_calls * module_info _ fastplay * 114000.0 / 1773447);
    module_info _ songs++;
}

#define SEEN_THIS_CALL  1
#define SEEN_BEFORE     2
#define SEEN_REPEAT     3

PRIVATE FUNC(void, parse_cmc_song, (P(ASAP_ModuleInfo PTR, module_info), P(CONST BYTEARRAY, module), P(int, pos)))
{
    V(int, tempo) = UBYTE(module[0x19]);
    V(int, player_calls) = 0;
    V(int, rep_start_pos) = 0;
    V(int, rep_end_pos) = 0;
    V(int, rep_times) = 0;
    NEW_ARRAY(byte, seen, 0x55);
    INIT_ARRAY(seen);
    while (pos >= 0 && pos < 0x55) {
        V(int, p1);
        V(int, p2);
        V(int, p3);
        if (pos == rep_end_pos && rep_times > 0) {
            for (p1 = 0; p1 < 0x55; p1++)
                if (seen[p1] == SEEN_THIS_CALL || seen[p1] == SEEN_REPEAT)
                    seen[p1] = 0;
            rep_times--;
            pos = rep_start_pos;
        }
        if (seen[pos] != 0) {
            if (seen[pos] != SEEN_THIS_CALL)
                module_info _ loops[module_info _ songs] = TRUE;
            break;
        }
        seen[pos] = SEEN_THIS_CALL;
        p1 = UBYTE(module[0x206 + pos]);
        p2 = UBYTE(module[0x25b + pos]);
        p3 = UBYTE(module[0x2b0 + pos]);
        if (p1 == 0xfe || p2 == 0xfe || p3 == 0xfe) {
            pos++;
            continue;
        }
        p1 >>= 4;
        if (p1 == 8)
            break;
        if (p1 == 9) {
            pos = p2;
            continue;
        }
        if (p1 == 0xa) {
            pos -= p2;
            continue;
        }
        if (p1 == 0xb) {
            pos += p2;
            continue;
        }
        if (p1 == 0xc) {
            tempo = p2;
            pos++;
            continue;
        }
        if (p1 == 0xd) {
            pos++;
            rep_start_pos = pos;
            rep_end_pos = pos + p2;
            rep_times = p3 - 1;
            continue;
        }
        if (p1 == 0xe) {
            module_info _ loops[module_info _ songs] = TRUE;
            break;
        }
        p2 = rep_times > 0 ? SEEN_REPEAT : SEEN_BEFORE;
        for (p1 = 0; p1 < 0x55; p1++)
            if (seen[p1] == SEEN_THIS_CALL)
                seen[p1] = CAST(byte) p2;
        player_calls += tempo * (module_info _ type == ASAP_TYPE_CM3 ? 48 : 64);
        pos++;
    }
    set_song_duration(module_info, player_calls);
}

PRIVATE FUNC(abool, parse_cmc, (
    P(ASAP_State PTR, ast), P(ASAP_ModuleInfo PTR, module_info),
    P(CONST BYTEARRAY, module), P(int, module_len), P(int, type), P(RESOURCE, player)))
{
    V(int, last_pos);
    V(int, pos);
    if (module_len < 0x306)
        return FALSE;
    module_info _ type = type;
    if (!load_native(ast, module_info, module, module_len, player))
        return FALSE;
#ifndef ASAP_ONLY_INFO
    if (ast != NULL && type == ASAP_TYPE_CMR)
        COPY_ARRAY(ast _ memory, 0x500 + CMR_BASS_TABLE_OFFSET, cmr_bass_table, 0, sizeof(cmr_bass_table));
#endif
    last_pos = 0x54;
    while (--last_pos >= 0) {
        if (UBYTE(module[0x206 + last_pos]) < 0xb0
         || UBYTE(module[0x25b + last_pos]) < 0x40
         || UBYTE(module[0x2b0 + last_pos]) < 0x40)
            break;
        if (module_info _ channels == 2) {
            if (UBYTE(module[0x306 + last_pos]) < 0xb0
             || UBYTE(module[0x35b + last_pos]) < 0x40
             || UBYTE(module[0x3b0 + last_pos]) < 0x40)
                break;
        }
    }
    module_info _ songs = 0;
    parse_cmc_song(module_info, module, 0);
    for (pos = 0; pos < last_pos && module_info _ songs < ASAP_SONGS_MAX; pos++)
        if (UBYTE(module[0x206 + pos]) == 0x8f || UBYTE(module[0x206 + pos]) == 0xef)
            parse_cmc_song(module_info, module, pos + 1);
    return TRUE;
}

PRIVATE FUNC(abool, is_dlt_track_empty, (P(CONST BYTEARRAY, module), P(int, pos)))
{
    return UBYTE(module[0x2006 + pos]) >= 0x43
        && UBYTE(module[0x2106 + pos]) >= 0x40
        && UBYTE(module[0x2206 + pos]) >= 0x40
        && UBYTE(module[0x2306 + pos]) >= 0x40;
}

PRIVATE FUNC(abool, is_dlt_pattern_end, (P(CONST BYTEARRAY, module), P(int, pos), P(int, i)))
{
    V(int, ch);
    for (ch = 0; ch < 4; ch++) {
        V(int, pattern) = UBYTE(module[0x2006 + (ch << 8) + pos]);
        if (pattern < 64) {
            V(int, offset) = 6 + (pattern << 7) + (i << 1);
            if ((module[offset] & 0x80) == 0 && (module[offset + 1] & 0x80) != 0)
                return TRUE;
        }
    }
    return FALSE;
}

PRIVATE FUNC(void, parse_dlt_song, (
    P(ASAP_ModuleInfo PTR, module_info), P(CONST BYTEARRAY, module),
    P(BOOLARRAY, seen), P(int, pos)))
{
    V(int, player_calls) = 0;
    V(abool, loop) = FALSE;
    V(int, tempo) = 6;
    while (pos < 128 && !seen[pos] && is_dlt_track_empty(module, pos))
        seen[pos++] = TRUE;
    module_info _ song_pos[module_info _ songs] = CAST(byte) pos;
    while (pos < 128) {
        V(int, p1);
        if (seen[pos]) {
            loop = TRUE;
            break;
        }
        seen[pos] = TRUE;
        p1 = module[0x2006 + pos];
        if (p1 == 0x40 || is_dlt_track_empty(module, pos))
            break;
        if (p1 == 0x41)
            pos = UBYTE(module[0x2086 + pos]);
        else if (p1 == 0x42)
            tempo = UBYTE(module[0x2086 + pos++]);
        else {
            V(int, i);
            for (i = 0; i < 64 && !is_dlt_pattern_end(module, pos, i); i++)
                player_calls += tempo;
            pos++;
        }
    }
    if (player_calls > 0) {
        module_info _ loops[module_info _ songs] = loop;
        set_song_duration(module_info, player_calls);
    }
}

PRIVATE FUNC(abool, parse_dlt, (
    P(ASAP_State PTR, ast), P(ASAP_ModuleInfo PTR, module_info),
    P(CONST BYTEARRAY, module), P(int, module_len)))
{
    V(int, pos);
    NEW_ARRAY(abool, seen, 128);
    if (module_len == 0x2c06) {
        if (ast != NULL)
            ast _ memory[0x4c00] = 0;
    }
    else if (module_len != 0x2c07)
        return FALSE;
    module_info _ type = ASAP_TYPE_DLT;
    if (!load_native(ast, module_info, module, module_len, GET_PLAYER(dlt))
     || module_info _ music != 0x2000) {
        return FALSE;
    }
    INIT_ARRAY(seen);
    module_info _ songs = 0;
    for (pos = 0; pos < 128 && module_info _ songs < ASAP_SONGS_MAX; pos++) {
        if (!seen[pos])
            parse_dlt_song(module_info, module, seen, pos);
    }
    return module_info _ songs > 0;
}

PRIVATE FUNC(void, parse_mpt_song, (
    P(ASAP_ModuleInfo PTR, module_info), P(CONST BYTEARRAY, module),
    P(BOOLARRAY, global_seen), P(int, song_len), P(int, pos)))
{
    V(int, addr_to_offset) = UWORD(module, 2) - 6;
    V(int, tempo) = UBYTE(module[0x1cf]);
    V(int, player_calls) = 0;
    NEW_ARRAY(byte, seen, 256);
    NEW_ARRAY(int, pattern_offset, 4);
    NEW_ARRAY(int, blank_rows, 4);
    NEW_ARRAY(int, blank_rows_counter, 4);
    INIT_ARRAY(seen);
    INIT_ARRAY(blank_rows);
    while (pos < song_len) {
        V(int, i);
        V(int, ch);
        V(int, pattern_rows);
        if (seen[pos] != 0) {
            if (seen[pos] != SEEN_THIS_CALL)
                module_info _ loops[module_info _ songs] = TRUE;
            break;
        }
        seen[pos] = SEEN_THIS_CALL;
        global_seen[pos] = TRUE;
        i = UBYTE(module[0x1d0 + pos * 2]);
        if (i == 0xff) {
            pos = UBYTE(module[0x1d1 + pos * 2]);
            continue;
        }
        for (ch = 3; ch >= 0; ch--) {
            i = UBYTE(module[0x1c6 + ch]) + (UBYTE(module[0x1ca + ch]) << 8) - addr_to_offset;
            i = UBYTE(module[i + pos * 2]);
            if (i >= 0x40)
                break;
            i <<= 1;
            i = UWORD(module, 0x46 + i);
            pattern_offset[ch] = i == 0 ? 0 : i - addr_to_offset;
            blank_rows_counter[ch] = 0;
        }
        if (ch >= 0)
            break;
        for (i = 0; i < song_len; i++)
            if (seen[i] == SEEN_THIS_CALL)
                seen[i] = SEEN_BEFORE;
        for (pattern_rows = UBYTE(module[0x1ce]); --pattern_rows >= 0; ) {
            for (ch = 3; ch >= 0; ch--) {
                if (pattern_offset[ch] == 0 || --blank_rows_counter[ch] >= 0)
                    continue;
                for (;;) {
                    i = UBYTE(module[pattern_offset[ch]++]);
                    if (i < 0x40 || i == 0xfe)
                        break;
                    if (i < 0x80)
                        continue;
                    if (i < 0xc0) {
                        blank_rows[ch] = i - 0x80;
                        continue;
                    }
                    if (i < 0xd0)
                        continue;
                    if (i < 0xe0) {
                        tempo = i - 0xcf;
                        continue;
                    }
                    pattern_rows = 0;
                }
                blank_rows_counter[ch] = blank_rows[ch];
            }
            player_calls += tempo;
        }
        pos++;
    }
    if (player_calls > 0)
        set_song_duration(module_info, player_calls);
}

PRIVATE FUNC(abool, parse_mpt, (
    P(ASAP_State PTR, ast), P(ASAP_ModuleInfo PTR, module_info),
    P(CONST BYTEARRAY, module), P(int, module_len)))
{
    V(int, track0_addr);
    V(int, pos);
    V(int, song_len);
    /* seen[i] == TRUE if the track position i has been processed */
    NEW_ARRAY(abool, global_seen, 256);
    if (module_len < 0x1d0)
        return FALSE;
    module_info _ type = ASAP_TYPE_MPT;
    if (!load_native(ast, module_info, module, module_len, GET_PLAYER(mpt)))
        return FALSE;
    track0_addr = UWORD(module, 2) + 0x1ca;
    if (UBYTE(module[0x1c6]) + (UBYTE(module[0x1ca]) << 8) != track0_addr)
        return FALSE;
    /* Calculate the length of the first track. Address of the second track minus
       address of the first track equals the length of the first track in bytes.
       Divide by two to get number of track positions. */
    song_len = (UBYTE(module[0x1c7]) + (UBYTE(module[0x1cb]) << 8) - track0_addr) >> 1;
    if (song_len > 0xfe)
        return FALSE;
    INIT_ARRAY(global_seen);
    module_info _ songs = 0;
    for (pos = 0; pos < song_len && module_info _ songs < ASAP_SONGS_MAX; pos++) {
        if (!global_seen[pos]) {
            module_info _ song_pos[module_info _ songs] = CAST(byte) pos;
            parse_mpt_song(module_info, module, global_seen, song_len, pos);
        }
    }
    return module_info _ songs > 0;
}

CONST_ARRAY(byte, rmt_volume_silent)
    16, 8, 4, 3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1
END_CONST_ARRAY;

PRIVATE FUNC(int, rmt_instrument_frames, (
    P(CONST BYTEARRAY, module), P(int, instrument),
    P(int, volume), P(int, volume_frame), P(abool, extra_pokey)))
{
    V(int, addr_to_offset) = UWORD(module, 2) - 6;
    V(int, per_frame) = module[0xc];
    V(int, player_call);
    V(int, player_calls);
    V(int, index);
    V(int, index_end);
    V(int, index_loop);
    V(int, volume_slide_depth);
    V(int, volume_min);
    V(int, volume_slide);
    V(abool, silent_loop);
    instrument = UWORD(module, 0xe) - addr_to_offset + (instrument << 1);
    if (module[instrument + 1] == 0)
        return 0;
    instrument = UWORD(module, instrument) - addr_to_offset;
    player_calls = player_call = volume_frame * per_frame;
    index = UBYTE(module[instrument]) + 1 + player_call * 3;
    index_end = UBYTE(module[instrument + 2]) + 3;
    index_loop = UBYTE(module[instrument + 3]);
    if (index_loop >= index_end)
        return 0; /* error */
    volume_slide_depth = UBYTE(module[instrument + 6]);
    volume_min = UBYTE(module[instrument + 7]);
    if (index >= index_end)
        index = (index - index_end) % (index_end - index_loop) + index_loop;
    else {
        do {
            V(int, vol) = module[instrument + index];
            if (extra_pokey)
                vol >>= 4;
            if ((vol & 0xf) >= rmt_volume_silent[volume])
                player_calls = player_call + 1;
            player_call++;
            index += 3;
        } while (index < index_end);
    }
    if (volume_slide_depth == 0)
        return player_calls / per_frame;
    volume_slide = 128;
    silent_loop = FALSE;
    for (;;) {
        V(int, vol);
        if (index >= index_end) {
            if (silent_loop)
                break;
            silent_loop = TRUE;
            index = index_loop;
        }
        vol = module[instrument + index];
        if (extra_pokey)
            vol >>= 4;
        if ((vol & 0xf) >= rmt_volume_silent[volume]) {
            player_calls = player_call + 1;
            silent_loop = FALSE;
        }
        player_call++;
        index += 3;
        volume_slide -= volume_slide_depth;
        if (volume_slide < 0) {
            volume_slide += 256;
            if (--volume <= volume_min)
                break;
        }
    }
    return player_calls / per_frame;
}

PRIVATE FUNC(void, parse_rmt_song, (
    P(ASAP_ModuleInfo PTR, module_info), P(CONST BYTEARRAY, module),
    P(BOOLARRAY, global_seen), P(int, song_len), P(int, pos_shift), P(int, pos)))
{
    V(int, ch);
    V(int, addr_to_offset) = UWORD(module, 2) - 6;
    V(int, tempo) = UBYTE(module[0xb]);
    V(int, frames) = 0;
    V(int, song_offset) = UWORD(module, 0x14) - addr_to_offset;
    V(int, pattern_lo_offset) = UWORD(module, 0x10) - addr_to_offset;
    V(int, pattern_hi_offset) = UWORD(module, 0x12) - addr_to_offset;
    V(int, instrument_frames);
    NEW_ARRAY(byte, seen, 256);
    NEW_ARRAY(int, pattern_begin, 8);
    NEW_ARRAY(int, pattern_offset, 8);
    NEW_ARRAY(int, blank_rows, 8);
    NEW_ARRAY(int, instrument_no, 8);
    NEW_ARRAY(int, instrument_frame, 8);
    NEW_ARRAY(int, volume_value, 8);
    NEW_ARRAY(int, volume_frame, 8);
    INIT_ARRAY(seen);
    INIT_ARRAY(instrument_no);
    INIT_ARRAY(instrument_frame);
    INIT_ARRAY(volume_value);
    INIT_ARRAY(volume_frame);
    while (pos < song_len) {
        V(int, i);
        V(int, pattern_rows);
        if (seen[pos] != 0) {
            if (seen[pos] != SEEN_THIS_CALL)
                module_info _ loops[module_info _ songs] = TRUE;
            break;
        }
        seen[pos] = SEEN_THIS_CALL;
        global_seen[pos] = TRUE;
        if (UBYTE(module[song_offset + (pos << pos_shift)]) == 0xfe) {
            pos = UBYTE(module[song_offset + (pos << pos_shift) + 1]);
            continue;
        }
        for (ch = 0; ch < 1 << pos_shift; ch++) {
            i = UBYTE(module[song_offset + (pos << pos_shift) + ch]);
            if (i == 0xff)
                blank_rows[ch] = 256;
            else {
                pattern_offset[ch] = pattern_begin[ch] = UBYTE(module[pattern_lo_offset + i])
                    + (UBYTE(module[pattern_hi_offset + i]) << 8) - addr_to_offset;
                blank_rows[ch] = 0;
            }
        }
        for (i = 0; i < song_len; i++)
            if (seen[i] == SEEN_THIS_CALL)
                seen[i] = SEEN_BEFORE;
        for (pattern_rows = UBYTE(module[0xa]); --pattern_rows >= 0; ) {
            for (ch = 0; ch < 1 << pos_shift; ch++) {
                if (--blank_rows[ch] > 0)
                    continue;
                for (;;) {
                    i = UBYTE(module[pattern_offset[ch]++]);
                    if ((i & 0x3f) < 62) {
                        i += UBYTE(module[pattern_offset[ch]++]) << 8;
                        if ((i & 0x3f) != 61) {
                            instrument_no[ch] = i >> 10;
                            instrument_frame[ch] = frames;
                        }
                        volume_value[ch] = (i >> 6) & 0xf;
                        volume_frame[ch] = frames;
                        break;
                    }
                    if (i == 62) {
                        blank_rows[ch] = UBYTE(module[pattern_offset[ch]++]);
                        break;
                    }
                    if ((i & 0x3f) == 62) {
                        blank_rows[ch] = i >> 6;
                        break;
                    }
                    if ((i & 0xbf) == 63) {
                        tempo = UBYTE(module[pattern_offset[ch]++]);
                        continue;
                    }
                    if (i == 0xbf) {
                        pattern_offset[ch] = pattern_begin[ch] + UBYTE(module[pattern_offset[ch]]);
                        continue;
                    }
                    /* assert(i == 0xff); */
                    pattern_rows = -1;
                    break;
                }
                if (pattern_rows < 0)
                    break;
            }
            if (pattern_rows >= 0)
                frames += tempo;
        }
        pos++;
    }
    instrument_frames = 0;
    for (ch = 0; ch < 1 << pos_shift; ch++) {
        V(int, frame) = instrument_frame[ch];
        frame += rmt_instrument_frames(module, instrument_no[ch], volume_value[ch], volume_frame[ch] - frame, ch >= 4);
        if (instrument_frames < frame)
            instrument_frames = frame;
    }
    if (frames > instrument_frames) {
        if (frames - instrument_frames > 100)
            module_info _ loops[module_info _ songs] = FALSE;
        frames = instrument_frames;
    }
    if (frames > 0)
        set_song_duration(module_info, frames);
}

PRIVATE FUNC(abool, parse_rmt, (
    P(ASAP_State PTR, ast), P(ASAP_ModuleInfo PTR, module_info),
    P(CONST BYTEARRAY, module), P(int, module_len)))
{
    V(int, per_frame);
    V(int, pos_shift);
    V(int, song_len);
    V(int, pos);
    NEW_ARRAY(abool, global_seen, 256);
    if (module_len < 0x30 || module[6] != CHARCODE('R') || module[7] != CHARCODE('M')
     || module[8] != CHARCODE('T') || module[0xd] != 1)
        return FALSE;
    switch (CAST(char) module[9]) {
    case CHARCODE('4'):
        pos_shift = 2;
        break;
    case CHARCODE('8'):
        module_info _ channels = 2;
        pos_shift = 3;
        break;
    default:
        return FALSE;
    }
    per_frame = module[0xc];
    if (per_frame < 1 || per_frame > 4)
        return FALSE;
    module_info _ type = ASAP_TYPE_RMT;
    if (!load_native(ast, module_info, module, module_len,
        module_info _ channels == 2 ? GET_PLAYER(rmt8) : GET_PLAYER(rmt4)))
        return FALSE;
    song_len = UWORD(module, 4) + 1 - UWORD(module, 0x14);
    if (pos_shift == 3 && (song_len & 4) != 0
     && UBYTE(module[6 + UWORD(module, 4) - UWORD(module, 2) - 3]) == 0xfe)
        song_len += 4;
    song_len >>= pos_shift;
    if (song_len >= 0x100)
        return FALSE;
    INIT_ARRAY(global_seen);
    module_info _ songs = 0;
    for (pos = 0; pos < song_len && module_info _ songs < ASAP_SONGS_MAX; pos++) {
        if (!global_seen[pos]) {
            module_info _ song_pos[module_info _ songs] = CAST(byte) pos;
            parse_rmt_song(module_info, module, global_seen, song_len, pos_shift, pos);
        }
    }
    /* must set fastplay after song durations calculations, so they assume 312 */
    module_info _ fastplay = perframe2fastplay[per_frame - 1];
    module_info _ player = 0x600;
    return module_info _ songs > 0;
}

PRIVATE FUNC(void, parse_tmc_song, (
    P(ASAP_ModuleInfo PTR, module_info), P(CONST BYTEARRAY, module), P(int, pos)))
{
    V(int, addr_to_offset) = UWORD(module, 2) - 6;
    V(int, tempo) = UBYTE(module[0x24]) + 1;
    V(int, frames) = 0;
    NEW_ARRAY(int, pattern_offset, 8);
    NEW_ARRAY(int, blank_rows, 8);
    while (UBYTE(module[0x1a6 + 15 + pos]) < 0x80) {
        V(int, ch);
        V(int, pattern_rows);
        for (ch = 7; ch >= 0; ch--) {
            V(int, pat) = UBYTE(module[0x1a6 + 15 + pos - 2 * ch]);
            pattern_offset[ch] = UBYTE(module[0xa6 + pat]) + (UBYTE(module[0x126 + pat]) << 8) - addr_to_offset;
            blank_rows[ch] = 0;
        }
        for (pattern_rows = 64; --pattern_rows >= 0; ) {
            for (ch = 7; ch >= 0; ch--) {
                if (--blank_rows[ch] >= 0)
                    continue;
                for (;;) {
                    V(int, i) = UBYTE(module[pattern_offset[ch]++]);
                    if (i < 0x40) {
                        pattern_offset[ch]++;
                        break;
                    }
                    if (i == 0x40) {
                        i = UBYTE(module[pattern_offset[ch]++]);
                        if ((i & 0x7f) == 0)
                            pattern_rows = 0;
                        else
                            tempo = (i & 0x7f) + 1;
                        if (i >= 0x80)
                            pattern_offset[ch]++;
                        break;
                    }
                    if (i < 0x80) {
                        i = module[pattern_offset[ch]++] & 0x7f;
                        if (i == 0)
                            pattern_rows = 0;
                        else
                            tempo = i + 1;
                        pattern_offset[ch]++;
                        break;
                    }
                    if (i < 0xc0)
                        continue;
                    blank_rows[ch] = i - 0xbf;
                    break;
                }
            }
            frames += tempo;
        }
        pos += 16;
    }
    if (UBYTE(module[0x1a6 + 14 + pos]) < 0x80)
        module_info _ loops[module_info _ songs] = TRUE;
    set_song_duration(module_info, frames);
}

PRIVATE FUNC(abool, parse_tmc, (
    P(ASAP_State PTR, ast), P(ASAP_ModuleInfo PTR, module_info),
    P(CONST BYTEARRAY, module), P(int, module_len)))
{
    V(int, i);
    V(int, last_pos);
    if (module_len < 0x1d0)
        return FALSE;
    module_info _ type = ASAP_TYPE_TMC;
    if (!load_native(ast, module_info, module, module_len, GET_PLAYER(tmc)))
        return FALSE;
    module_info _ channels = 2;
    i = 0;
    /* find first instrument */
    while (module[0x66 + i] == 0) {
        if (++i >= 64)
            return FALSE; /* no instrument */
    }
    last_pos = (UBYTE(module[0x66 + i]) << 8) + UBYTE(module[0x26 + i])
        - UWORD(module, 2) - 0x1b0;
    if (0x1b5 + last_pos >= module_len)
        return FALSE;
    /* skip trailing jumps */
    do {
        if (last_pos <= 0)
            return FALSE; /* no pattern to play */
        last_pos -= 16;
    } while (UBYTE(module[0x1b5 + last_pos]) >= 0x80);
    module_info _ songs = 0;
    parse_tmc_song(module_info, module, 0);
    for (i = 0; i < last_pos && module_info _ songs < ASAP_SONGS_MAX; i += 16)
        if (UBYTE(module[0x1b5 + i]) >= 0x80)
            parse_tmc_song(module_info, module, i + 16);
    /* must set fastplay after song durations calculations, so they assume 312 */
    i = module[0x25];
    if (i < 1 || i > 4)
        return FALSE;
    if (ast != NULL)
        ast _ tmc_per_frame = module[0x25];
    module_info _ fastplay = perframe2fastplay[i - 1];
    return TRUE;
}

PRIVATE FUNC(void, parse_tm2_song, (
    P(ASAP_ModuleInfo PTR, module_info), P(CONST BYTEARRAY, module), P(int, pos)))
{
    V(int, addr_to_offset) = UWORD(module, 2) - 6;
    V(int, tempo) = UBYTE(module[0x24]) + 1;
    V(int, player_calls) = 0;
    NEW_ARRAY(int, pattern_offset, 8);
    NEW_ARRAY(int, blank_rows, 8);
    for (;;) {
        V(int, ch);
        V(int, pattern_rows) = UBYTE(module[0x386 + 16 + pos]);
        if (pattern_rows == 0)
            break;
        if (pattern_rows >= 0x80) {
            module_info _ loops[module_info _ songs] = TRUE;
            break;
        }
        for (ch = 7; ch >= 0; ch--) {
            V(int, pat) = UBYTE(module[0x386 + 15 + pos - 2 * ch]);
            pattern_offset[ch] = UBYTE(module[0x106 + pat]) + (UBYTE(module[0x206 + pat]) << 8) - addr_to_offset;
            blank_rows[ch] = 0;
        }
        while (--pattern_rows >= 0) {
            for (ch = 7; ch >= 0; ch--) {
                if (--blank_rows[ch] >= 0)
                    continue;
                for (;;) {
                    V(int, i) = UBYTE(module[pattern_offset[ch]++]);
                    if (i == 0) {
                        pattern_offset[ch]++;
                        break;
                    }
                    if (i < 0x40) {
                        if (UBYTE(module[pattern_offset[ch]++]) >= 0x80)
                            pattern_offset[ch]++;
                        break;
                    }
                    if (i < 0x80) {
                        pattern_offset[ch]++;
                        break;
                    }
                    if (i == 0x80) {
                        blank_rows[ch] = UBYTE(module[pattern_offset[ch]++]);
                        break;
                    }
                    if (i < 0xc0)
                        break;
                    if (i < 0xd0) {
                        tempo = i - 0xbf;
                        continue;
                    }
                    if (i < 0xe0) {
                        pattern_offset[ch]++;
                        break;
                    }
                    if (i < 0xf0) {
                        pattern_offset[ch] += 2;
                        break;
                    }
                    if (i < 0xff) {
                        blank_rows[ch] = i - 0xf0;
                        break;
                    }
                    blank_rows[ch] = 64;
                    break;
                }
            }
            player_calls += tempo;
        }
        pos += 17;
    }
    set_song_duration(module_info, player_calls);
}

PRIVATE FUNC(abool, parse_tm2, (
    P(ASAP_State PTR, ast), P(ASAP_ModuleInfo PTR, module_info),
    P(CONST BYTEARRAY, module), P(int, module_len)))
{
    V(int, i);
    V(int, last_pos);
    V(int, c);
    if (module_len < 0x3a4)
        return FALSE;
    module_info _ type = ASAP_TYPE_TM2;
    if (!load_native(ast, module_info, module, module_len, GET_PLAYER(tm2)))
        return FALSE;
    i = module[0x25];
    if (i < 1 || i > 4)
        return FALSE;
    module_info _ fastplay = perframe2fastplay[i - 1];
    module_info _ player = 0x500;
    if (module[0x1f] != 0)
        module_info _ channels = 2;
    last_pos = 0xffff;
    for (i = 0; i < 0x80; i++) {
        V(int, instr_addr) = UBYTE(module[0x86 + i]) + (UBYTE(module[0x306 + i]) << 8);
        if (instr_addr != 0 && instr_addr < last_pos)
            last_pos = instr_addr;
    }
    for (i = 0; i < 0x100; i++) {
        V(int, pattern_addr) = UBYTE(module[0x106 + i]) + (UBYTE(module[0x206 + i]) << 8);
        if (pattern_addr != 0 && pattern_addr < last_pos)
            last_pos = pattern_addr;
    }
    last_pos -= UWORD(module, 2) + 0x380;
    if (0x386 + last_pos >= module_len)
        return FALSE;
    /* skip trailing stop/jump commands */
    do {
        if (last_pos <= 0)
            return FALSE;
        last_pos -= 17;
        c = UBYTE(module[0x386 + 16 + last_pos]);
    } while (c == 0 || c >= 0x80);
    module_info _ songs = 0;
    parse_tm2_song(module_info, module, 0);
    for (i = 0; i < last_pos && module_info _ songs < ASAP_SONGS_MAX; i += 17) {
        c = UBYTE(module[0x386 + 16 + i]);
        if (c == 0 || c >= 0x80)
            parse_tm2_song(module_info, module, i + 17);
    }
    return TRUE;
}

#endif /* ASAP_ONLY_SAP */

PRIVATE FUNC(abool, has_string_at, (P(CONST BYTEARRAY, module), P(int, module_index), P(STRING, s)))
{
    V(int, i);
    V(int, n) = strlen(s);
    for (i = 0; i < n; i++)
        if (module[module_index + i] != CHARCODEAT(s, i))
            return FALSE;
    return TRUE;
}

PRIVATE FUNC(STRING, parse_text, (P(OUT_STRING, dest), P(CONST BYTEARRAY, module), P(int, module_index)))
{
    V(int, i);
    if (module[module_index] != CHARCODE('"'))
        return NULL;
    if (has_string_at(module, module_index + 1, "<?>\""))
        return dest;
    for (i = 0; ; i++) {
        V(int, c) = module[module_index + 1 + i];
        if (c == CHARCODE('"'))
            break;
        if (c < 32 || c >= 127)
            return NULL;
    }
    BYTES_TO_STRING(dest, module, module_index + 1, i);
    return dest;
}

PRIVATE FUNC(int, parse_dec, (P(CONST BYTEARRAY, module), P(int, module_index), P(int, maxval)))
{
    V(int, r);
    if (module[module_index] == 0xd)
        return -1;
    for (r = 0;;) {
        V(int, c) = module[module_index++];
        if (c == 0xd)
            break;
        if (c < CHARCODE('0') || c > CHARCODE('9'))
            return -1;
        r = 10 * r + c - 48;
        if (r > maxval)
            return -1;
    }
    return r;
}

PRIVATE FUNC(int, parse_hex, (P(CONST BYTEARRAY, module), P(int, module_index)))
{
    V(int, r);
    if (module[module_index] == 0xd)
        return -1;
    for (r = 0;;) {
        V(int, c) = module[module_index++];
        if (c == 0xd)
            break;
        if (r > 0xfff)
            return -1;
        r <<= 4;
        if (c >= CHARCODE('0') && c <= CHARCODE('9'))
            r += c - CHARCODE('0');
        else if (c >= CHARCODE('A') && c <= CHARCODE('F'))
            r += c - CHARCODE('A') + 10;
        else if (c >= CHARCODE('a') && c <= CHARCODE('f'))
            r += c - CHARCODE('a') + 10;
        else
            return -1;
    }
    return r;
}

FUNC(int, ASAP_ParseDuration, (P(STRING, s)))
{
    V(int, i) = 0;
    V(int, r);
    V(int, d);
    V(int, n) = strlen(s);
#define PARSE_DIGIT(maxdig, retifnot) \
    if (i >= n) \
        return retifnot; \
    d = CHARCODEAT(s, i) - 48; \
    if (d < 0 || d > maxdig) \
        return -1; \
    i++;

    PARSE_DIGIT(9, -1);
    r = d;
    if (i < n) {
        d = CHARCODEAT(s, i) - 48;
        if (d >= 0 && d <= 9) {
            i++;
            r = 10 * r + d;
        }
        if (i < n && CHARAT(s, i) == ':') {
            i++;
            PARSE_DIGIT(5, -1);
            r = (6 * r + d) * 10;
            PARSE_DIGIT(9, -1);
            r += d;
        }
    }
    r *= 1000;
    if (i >= n)
        return r;
    if (CHARAT(s, i) != '.')
        return -1;
    i++;
    PARSE_DIGIT(9, -1);
    r += 100 * d;
    PARSE_DIGIT(9, r);
    r += 10 * d;
    PARSE_DIGIT(9, r);
    r += d;
    return r;
}

PRIVATE FUNC(abool, parse_sap_header, (
    P(ASAP_ModuleInfo PTR, module_info), P(CONST BYTEARRAY, module), P(int, module_len)))
{
    V(int, module_index);
    V(int, type) = 0;
    V(int, duration_index) = 0;
    if (!has_string_at(module, 0, "SAP\r\n"))
        return FALSE;
    module_info _ fastplay = -1;
    module_index = 5;
    while (UBYTE(module[module_index]) != 0xff) {
        if (module_index + 8 >= module_len)
            return FALSE;
#define TAG_IS(s)               has_string_at(module, module_index, s)
#ifdef C
#define SET_TEXT(v, i)          if (parse_text(v, module, module_index + i) == NULL) return FALSE
#else
#define SET_TEXT(v, i)          v = parse_text(v, module, module_index + i); if (v == NULL) return FALSE
#endif
#define SET_DEC(v, i, min, max) v = parse_dec(module, module_index + i, max); if (v < min) return FALSE
#define SET_HEX(v, i)           v = parse_hex(module, module_index + i)
        if (TAG_IS("AUTHOR ")) {
            SET_TEXT(module_info _ author, 7);
        }
        else if (TAG_IS("NAME ")) {
            SET_TEXT(module_info _ name, 5);
        }
        else if (TAG_IS("DATE ")) {
            SET_TEXT(module_info _ date, 5);
        }
        else if (TAG_IS("SONGS ")) {
            SET_DEC(module_info _ songs, 6, 1, ASAP_SONGS_MAX);
        }
        else if (TAG_IS("DEFSONG ")) {
            SET_DEC(module_info _ default_song, 8, 0, ASAP_SONGS_MAX - 1);
        }
        else if (TAG_IS("STEREO\r"))
            module_info _ channels = 2;
        else if (TAG_IS("NTSC\r"))
            module_info _ ntsc = TRUE;
        else if (TAG_IS("TIME ")) {
            V(int, i);
#ifdef C
            char s[ASAP_DURATION_CHARS];
#else
            V(STRING, s);
#endif
            module_index += 5;
            for (i = 0; module[module_index + i] != 0xd; i++) { }
            if (i > 5 && has_string_at(module, module_index + i - 5, " LOOP")) {
                module_info _ loops[duration_index] = TRUE;
                i -= 5;
            }
#ifdef C
            if (i >= ASAP_DURATION_CHARS)
                return FALSE;
#endif
            BYTES_TO_STRING(s, module, module_index, i);
            i = ASAP_ParseDuration(s);
            if (i < 0 || duration_index >= ASAP_SONGS_MAX)
                return FALSE;
            module_info _ durations[duration_index++] = i;
        }
        else if (TAG_IS("TYPE "))
            type = module[module_index + 5];
        else if (TAG_IS("FASTPLAY ")) {
            SET_DEC(module_info _ fastplay, 9, 1, 312);
        }
        else if (TAG_IS("MUSIC ")) {
            SET_HEX(module_info _ music, 6);
        }
        else if (TAG_IS("INIT ")) {
            SET_HEX(module_info _ init, 5);
        }
        else if (TAG_IS("PLAYER ")) {
            SET_HEX(module_info _ player, 7);
        }
        else if (TAG_IS("COVOX ")) {
            SET_HEX(module_info _ covox_addr, 6);
            if (module_info _ covox_addr != 0xd600)
                return FALSE;
            module_info _ channels = 2;
        }

        while (module[module_index++] != 0x0d) {
            if (module_index >= module_len)
                return FALSE;
        }
        if (module[module_index++] != 0x0a)
            return FALSE;
    }
    if (module_info _ default_song >= module_info _ songs)
        return FALSE;
    switch (type) {
    case CHARCODE('B'):
        if (module_info _ player < 0 || module_info _ init < 0)
            return FALSE;
        module_info _ type = ASAP_TYPE_SAP_B;
        break;
    case CHARCODE('C'):
        if (module_info _ player < 0 || module_info _ music < 0)
            return FALSE;
        module_info _ type = ASAP_TYPE_SAP_C;
        break;
    case CHARCODE('D'):
        if (module_info _ init < 0)
            return FALSE;
        module_info _ type = ASAP_TYPE_SAP_D;
        break;
    case CHARCODE('S'):
        if (module_info _ init < 0)
            return FALSE;
        module_info _ type = ASAP_TYPE_SAP_S;
        module_info _ fastplay = 78;
        break;
    default:
        return FALSE;
    }
    if (module_info _ fastplay < 0)
        module_info _ fastplay = module_info _ ntsc ? 262 : 312;
    else if (module_info _ ntsc && module_info _ fastplay > 262)
        return FALSE;
    if (UBYTE(module[module_index + 1]) != 0xff)
        return FALSE;
    module_info _ header_len = module_index;
    return TRUE;
}

PRIVATE FUNC(abool, parse_sap, (
    P(ASAP_State PTR, ast), P(ASAP_ModuleInfo PTR, module_info),
    P(CONST BYTEARRAY, module), P(int, module_len)))
{
    V(int, module_index);
    if (!parse_sap_header(module_info, module, module_len))
        return FALSE;
    if (ast == NULL)
        return TRUE;
    module_index = module_info _ header_len + 2;
    while (module_index + 5 <= module_len) {
        V(int, start_addr) = UWORD(module, module_index);
        V(int, block_len) = UWORD(module, module_index + 2) + 1 - start_addr;
        if (block_len <= 0 || module_index + block_len > module_len)
            return FALSE;
        module_index += 4;
        COPY_ARRAY(ast _ memory, start_addr, module, module_index, block_len);
        module_index += block_len;
        if (module_index == module_len)
            return TRUE;
        if (module_index + 7 <= module_len
         && UBYTE(module[module_index]) == 0xff && UBYTE(module[module_index + 1]) == 0xff)
            module_index += 2;
    }
    return FALSE;
}

#define ASAP_EXT(c1, c2, c3) ((CHARCODE(c1) + (CHARCODE(c2) << 8) + (CHARCODE(c3) << 16)) | 0x202020)

PRIVATE FUNC(int, get_packed_ext, (P(STRING, filename)))
{
    V(int, i) = strlen(filename);
    V(int, ext) = 0;
    while (--i > 0) {
        V(char, c) = CHARAT(filename, i);
        if (c <= ' ' || c > 'z')
            return 0;
        if (c == '.')
            return ext | 0x202020;
        ext = (ext << 8) + CHARCODE(c);
    }
    return 0;
}

PRIVATE FUNC(abool, is_our_ext, (P(int, ext)))
{
    switch (ext) {
    case ASAP_EXT('S', 'A', 'P'):
#ifndef ASAP_ONLY_SAP
    case ASAP_EXT('C', 'M', 'C'):
    case ASAP_EXT('C', 'M', '3'):
    case ASAP_EXT('C', 'M', 'R'):
    case ASAP_EXT('C', 'M', 'S'):
    case ASAP_EXT('D', 'M', 'C'):
    case ASAP_EXT('D', 'L', 'T'):
    case ASAP_EXT('M', 'P', 'T'):
    case ASAP_EXT('M', 'P', 'D'):
    case ASAP_EXT('R', 'M', 'T'):
    case ASAP_EXT('T', 'M', 'C'):
    case ASAP_EXT('T', 'M', '8'):
    case ASAP_EXT('T', 'M', '2'):
#endif
        return TRUE;
    default:
        return FALSE;
    }
}

FUNC(abool, ASAP_IsOurFile, (P(STRING, filename)))
{
    V(int, ext) = get_packed_ext(filename);
    return is_our_ext(ext);
}

FUNC(abool, ASAP_IsOurExt, (P(STRING, ext)))
{
    return strlen(ext) == 3
        && is_our_ext(ASAP_EXT(CHARAT(ext, 0), CHARAT(ext, 1), CHARAT(ext, 2)));
}

PRIVATE FUNC(abool, parse_file, (
    P(ASAP_State PTR, ast), P(ASAP_ModuleInfo PTR, module_info),
    P(STRING, filename), P(CONST BYTEARRAY, module), P(int, module_len)))
{
    V(int, i);
    V(int, len) = strlen(filename);
    V(int, basename) = 0;
    V(int, ext) = -1;
    for (i = 0; i < len; i++) {
        V(char, c) = CHARAT(filename, i);
        if (c == '/' || c == '\\') {
            basename = i + 1;
            ext = -1;
        }
        else if (c == '.')
            ext = i;
    }
    if (ext < 0)
        return FALSE;
    EMPTY_STRING(module_info _ author);
    SUBSTRING(module_info _ name, filename, basename, ext - basename);
    EMPTY_STRING(module_info _ date);
    module_info _ channels = 1;
    module_info _ songs = 1;
    module_info _ default_song = 0;
    for (i = 0; i < ASAP_SONGS_MAX; i++) {
        module_info _ durations[i] = -1;
        module_info _ loops[i] = FALSE;
    }
    module_info _ ntsc = FALSE;
    module_info _ fastplay = 312;
    module_info _ music = -1;
    module_info _ init = -1;
    module_info _ player = -1;
    module_info _ covox_addr = -1;
    switch (get_packed_ext(filename)) {
    case ASAP_EXT('S', 'A', 'P'):
        return parse_sap(ast, module_info, module, module_len);
#ifndef ASAP_ONLY_SAP
    case ASAP_EXT('C', 'M', 'C'):
        return parse_cmc(ast, module_info, module, module_len, ASAP_TYPE_CMC, GET_PLAYER(cmc));
    case ASAP_EXT('C', 'M', '3'):
        return parse_cmc(ast, module_info, module, module_len, ASAP_TYPE_CM3, GET_PLAYER(cm3));
    case ASAP_EXT('C', 'M', 'R'):
        return parse_cmc(ast, module_info, module, module_len, ASAP_TYPE_CMR, GET_PLAYER(cmc));
    case ASAP_EXT('C', 'M', 'S'):
        module_info _ channels = 2;
        return parse_cmc(ast, module_info, module, module_len, ASAP_TYPE_CMS, GET_PLAYER(cms));
    case ASAP_EXT('D', 'M', 'C'):
        module_info _ fastplay = 156;
        return parse_cmc(ast, module_info, module, module_len, ASAP_TYPE_CMC, GET_PLAYER(cmc));
    case ASAP_EXT('D', 'L', 'T'):
        return parse_dlt(ast, module_info, module, module_len);
    case ASAP_EXT('M', 'P', 'T'):
        return parse_mpt(ast, module_info, module, module_len);
    case ASAP_EXT('M', 'P', 'D'):
        module_info _ fastplay = 156;
        return parse_mpt(ast, module_info, module, module_len);
    case ASAP_EXT('R', 'M', 'T'):
        return parse_rmt(ast, module_info, module, module_len);
    case ASAP_EXT('T', 'M', 'C'):
    case ASAP_EXT('T', 'M', '8'):
        return parse_tmc(ast, module_info, module, module_len);
    case ASAP_EXT('T', 'M', '2'):
        return parse_tm2(ast, module_info, module, module_len);
#endif
    default:
        return FALSE;
    }
}

FUNC(abool, ASAP_GetModuleInfo, (
    P(ASAP_ModuleInfo PTR, module_info), P(STRING, filename),
    P(CONST BYTEARRAY, module), P(int, module_len)))
{
    return parse_file(NULL, module_info, filename, module, module_len);
}

#ifndef ASAP_ONLY_INFO

FUNC(abool, ASAP_Load, (
    P(ASAP_State PTR, ast), P(STRING, filename),
    P(CONST BYTEARRAY, module), P(int, module_len)))
{
    /* Set up ast */
    ast _ memory = s_memory;
    ast _ module_info = &s_module_info;
    ast _ silence_cycles = 0;
    return parse_file(ast, ast _ module_info, filename, module, module_len);
}

FUNC(void, ASAP_DetectSilence, (P(ASAP_State PTR, ast), P(int, seconds)))
{
    ast _ silence_cycles = seconds * ASAP_MAIN_CLOCK(ast);
}

PRIVATE FUNC(void, call_6502, (P(ASAP_State PTR, ast), P(int, addr), P(int, max_scanlines)))
{
    ast _ cpu_pc = addr;
    /* put a CIM at 0xd20a and a return address on stack */
    dPutByte(0xd20a, 0xd2);
    dPutByte(0x01fe, 0x09);
    dPutByte(0x01ff, 0xd2);
    ast _ cpu_s = 0xfd;
    Cpu_RunScanlines(ast, max_scanlines);
}

/* 50 Atari frames for the initialization routine - some SAPs are self-extracting. */
#define SCANLINES_FOR_INIT  (50 * 312)

PRIVATE FUNC(void, call_6502_init, (P(ASAP_State PTR, ast), P(int, addr), P(int, a), P(int, x), P(int, y)))
{
    ast _ cpu_a = a & 0xff;
    ast _ cpu_x = x & 0xff;
    ast _ cpu_y = y & 0xff;
    call_6502(ast, addr, SCANLINES_FOR_INIT);
}

FUNC(void, ASAP_PlaySong, (P(ASAP_State PTR, ast), P(int, song), P(int, duration)))
{
    ast _ current_song = song;
    ast _ current_duration = duration;
    ast _ blocks_played = 0;
    ast _ silence_cycles_counter = ast _ silence_cycles;
    ast _ extra_pokey_mask = ast _ module_info->channels > 1 ? 0x10 : 0;
    ast _ consol = 8;
    ast _ covox[0] = CAST(byte) 0x80;
    ast _ covox[1] = CAST(byte) 0x80;
    ast _ covox[2] = CAST(byte) 0x80;
    ast _ covox[3] = CAST(byte) 0x80;
    PokeySound_Initialize(ast);
    ast _ cycle = 0;
    ast _ cpu_nz = 0;
    ast _ cpu_c = 0;
    ast _ cpu_vdi = 0;
    ast _ scanline_number = 0;
    ast _ next_scanline_cycle = 0;
    ast _ timer1_cycle = NEVER;
    ast _ timer2_cycle = NEVER;
    ast _ timer4_cycle = NEVER;
    ast _ irqst = 0xff;
    switch (ast _ module_info->type) {
    case ASAP_TYPE_SAP_B:
        call_6502_init(ast, ast _ module_info->init, song, 0, 0);
        break;
    case ASAP_TYPE_SAP_C:
#ifndef ASAP_ONLY_SAP
    case ASAP_TYPE_CMC:
    case ASAP_TYPE_CM3:
    case ASAP_TYPE_CMR:
    case ASAP_TYPE_CMS:
#endif
        call_6502_init(ast, ast _ module_info->player + 3, 0x70, ast _ module_info->music, ast _ module_info->music >> 8);
        call_6502_init(ast, ast _ module_info->player + 3, 0x00, song, 0);
        break;
    case ASAP_TYPE_SAP_D:
    case ASAP_TYPE_SAP_S:
        ast _ cpu_a = song;
        ast _ cpu_x = 0x00;
        ast _ cpu_y = 0x00;
        ast _ cpu_s = 0xff;
        ast _ cpu_pc = ast _ module_info->init;
        break;
#ifndef ASAP_ONLY_SAP
    case ASAP_TYPE_DLT:
        call_6502_init(ast, ast _ module_info->player + 0x100, 0x00, 0x00, ast _ module_info->song_pos[song]);
        break;
    case ASAP_TYPE_MPT:
        call_6502_init(ast, ast _ module_info->player, 0x00, ast _ module_info->music >> 8, ast _ module_info->music);
        call_6502_init(ast, ast _ module_info->player, 0x02, ast _ module_info->song_pos[song], 0);
        break;
    case ASAP_TYPE_RMT:
        call_6502_init(ast, ast _ module_info->player, ast _ module_info->song_pos[song], ast _ module_info->music, ast _ module_info->music >> 8);
        break;
    case ASAP_TYPE_TMC:
    case ASAP_TYPE_TM2:
        call_6502_init(ast, ast _ module_info->player, 0x70, ast _ module_info->music >> 8, ast _ module_info->music);
        call_6502_init(ast, ast _ module_info->player, 0x00, song, 0);
        ast _ tmc_per_frame_counter = 1;
        break;
#endif
    }
    ASAP_MutePokeyChannels(ast, 0);
}

FUNC(void, ASAP_MutePokeyChannels, (P(ASAP_State PTR, ast), P(int, mask)))
{
    PokeySound_Mute(ast, ADDRESSOF ast _ base_pokey, mask);
    PokeySound_Mute(ast, ADDRESSOF ast _ extra_pokey, mask >> 4);
}

FUNC(abool, call_6502_player, (P(ASAP_State PTR, ast)))
{
    V(int, player) = ast _ module_info->player;
    PokeySound_StartFrame(ast);
    switch (ast _ module_info->type) {
    case ASAP_TYPE_SAP_B:
        call_6502(ast, player, ast _ module_info->fastplay);
        break;
    case ASAP_TYPE_SAP_C:
#ifndef ASAP_ONLY_SAP
    case ASAP_TYPE_CMC:
    case ASAP_TYPE_CM3:
    case ASAP_TYPE_CMR:
    case ASAP_TYPE_CMS:
#endif
        call_6502(ast, player + 6, ast _ module_info->fastplay);
        break;
    case ASAP_TYPE_SAP_D:
        if (player >= 0) {
            V(int, s)= ast _ cpu_s;
#define PUSH_ON_6502_STACK(x)  dPutByte(0x100 + s, x); s = (s - 1) & 0xff
#define RETURN_FROM_PLAYER_ADDR  0xd200
            /* save 6502 state on 6502 stack */
            PUSH_ON_6502_STACK(ast _ cpu_pc >> 8);
            PUSH_ON_6502_STACK(ast _ cpu_pc & 0xff);
            PUSH_ON_6502_STACK(((ast _ cpu_nz | (ast _ cpu_nz >> 1)) & 0x80) + ast _ cpu_vdi + \
                ((ast _ cpu_nz & 0xff) == 0 ? Z_FLAG : 0) + ast _ cpu_c + 0x20);
            PUSH_ON_6502_STACK(ast _ cpu_a);
            PUSH_ON_6502_STACK(ast _ cpu_x);
            PUSH_ON_6502_STACK(ast _ cpu_y);
            /* RTS will jump to 6502 code that restores the state */
            PUSH_ON_6502_STACK((RETURN_FROM_PLAYER_ADDR - 1) >> 8);
            PUSH_ON_6502_STACK((RETURN_FROM_PLAYER_ADDR - 1) & 0xff);
            ast _ cpu_s = s;
            dPutByte(RETURN_FROM_PLAYER_ADDR, 0x68);     /* PLA */
            dPutByte(RETURN_FROM_PLAYER_ADDR + 1, 0xa8); /* TAY */
            dPutByte(RETURN_FROM_PLAYER_ADDR + 2, 0x68); /* PLA */
            dPutByte(RETURN_FROM_PLAYER_ADDR + 3, 0xaa); /* TAX */
            dPutByte(RETURN_FROM_PLAYER_ADDR + 4, 0x68); /* PLA */
            dPutByte(RETURN_FROM_PLAYER_ADDR + 5, 0x40); /* RTI */
            ast _ cpu_pc = player;
        }
        Cpu_RunScanlines(ast, ast _ module_info->fastplay);
        break;
    case ASAP_TYPE_SAP_S:
        Cpu_RunScanlines(ast, ast _ module_info->fastplay);
        {
            V(int, i) = dGetByte(0x45) - 1;
            dPutByte(0x45, i);
            if (i == 0)
                dPutByte(0xb07b, dGetByte(0xb07b) + 1);
        }
        break;
#ifndef ASAP_ONLY_SAP
    case ASAP_TYPE_DLT:
        call_6502(ast, player + 0x103, ast _ module_info->fastplay);
        break;
    case ASAP_TYPE_MPT:
    case ASAP_TYPE_RMT:
    case ASAP_TYPE_TM2:
        call_6502(ast, player + 3, ast _ module_info->fastplay);
        break;
    case ASAP_TYPE_TMC:
        if (--ast _ tmc_per_frame_counter <= 0) {
            ast _ tmc_per_frame_counter = ast _ tmc_per_frame;
            call_6502(ast, player + 3, ast _ module_info->fastplay);
        }
        else
            call_6502(ast, player + 6, ast _ module_info->fastplay);
        break;
#endif
    }
    PokeySound_EndFrame(ast, ast _ module_info->fastplay * 114);
    if (ast _ silence_cycles > 0) {
        if (PokeySound_IsSilent(ADDRESSOF ast _ base_pokey)
         && PokeySound_IsSilent(ADDRESSOF ast _ extra_pokey)) {
            ast _ silence_cycles_counter -= ast _ module_info->fastplay * 114;
            if (ast _ silence_cycles_counter <= 0)
                return FALSE;
        }
        else
            ast _ silence_cycles_counter = ast _ silence_cycles;
    }
    return TRUE;
}

FUNC(int, ASAP_GetPosition, (P(CONST ASAP_State PTR, ast)))
{
    return ast _ blocks_played * 10 / (ASAP_SAMPLE_RATE / 100);
}

FUNC(int, milliseconds_to_blocks, (P(int, milliseconds)))
{
    return milliseconds * (ASAP_SAMPLE_RATE / 100) / 10;
}

#ifndef ACTIONSCRIPT

FUNC(void, ASAP_Seek, (P(ASAP_State PTR, ast), P(int, position)))
{
    V(int, block) = milliseconds_to_blocks(position);
    if (block < ast _ blocks_played)
        ASAP_PlaySong(ast, ast _ current_song, ast _ current_duration);
    while (ast _ blocks_played + ast _ samples - ast _ sample_index < block) {
        ast _ blocks_played += ast _ samples - ast _ sample_index;
        call_6502_player(ast);
    }
    ast _ sample_index += block - ast _ blocks_played;
    ast _ blocks_played = block;
}

PRIVATE FUNC(void, serialize_int, (P(BYTEARRAY, buffer), P(int, offset), P(int, value)))
{
    buffer[offset] = TO_BYTE(value);
    buffer[offset + 1] = TO_BYTE(value >> 8);
    buffer[offset + 2] = TO_BYTE(value >> 16);
    buffer[offset + 3] = TO_BYTE(value >> 24);
}

FUNC(void, ASAP_GetWavHeader, (
    P(CONST ASAP_State PTR, ast), P(BYTEARRAY, buffer), P(ASAP_SampleFormat, format)))
{
    V(int, use_16bit) = format != ASAP_FORMAT_U8 ? 1 : 0;
    V(int, block_size) = ast _ module_info->channels << use_16bit;
    V(int, bytes_per_second) = ASAP_SAMPLE_RATE * block_size;
    V(int, total_blocks) = milliseconds_to_blocks(ast _ current_duration);
    V(int, n_bytes) = (total_blocks - ast _ blocks_played) * block_size;
    buffer[0] = CAST(byte) CHARCODE('R');
    buffer[1] = CAST(byte) CHARCODE('I');
    buffer[2] = CAST(byte) CHARCODE('F');
    buffer[3] = CAST(byte) CHARCODE('F');
    serialize_int(buffer, 4, n_bytes + 36);
    buffer[8] = CAST(byte) CHARCODE('W');
    buffer[9] = CAST(byte) CHARCODE('A');
    buffer[10] = CAST(byte) CHARCODE('V');
    buffer[11] = CAST(byte) CHARCODE('E');
    buffer[12] = CAST(byte) CHARCODE('f');
    buffer[13] = CAST(byte) CHARCODE('m');
    buffer[14] = CAST(byte) CHARCODE('t');
    buffer[15] = CAST(byte) CHARCODE(' ');
    buffer[16] = 16;
    buffer[17] = 0;
    buffer[18] = 0;
    buffer[19] = 0;
    buffer[20] = 1;
    buffer[21] = 0;
    buffer[22] = CAST(byte) ast _ module_info->channels;
    buffer[23] = 0;
    serialize_int(buffer, 24, ASAP_SAMPLE_RATE);
    serialize_int(buffer, 28, bytes_per_second);
    buffer[32] = CAST(byte) block_size;
    buffer[33] = 0;
    buffer[34] = CAST(byte) (8 << use_16bit);
    buffer[35] = 0;
    buffer[36] = CAST(byte) CHARCODE('d');
    buffer[37] = CAST(byte) CHARCODE('a');
    buffer[38] = CAST(byte) CHARCODE('t');
    buffer[39] = CAST(byte) CHARCODE('a');
    serialize_int(buffer, 40, n_bytes);
}

#endif /* ACTIONSCRIPT */

PRIVATE FUNC(int, ASAP_GenerateAt, (P(ASAP_State PTR, ast), P(VOIDPTR, buffer), P(int, buffer_offset), P(int, buffer_len), P(ASAP_SampleFormat, format)))
{
    V(int, block_shift);
    V(int, buffer_blocks);
    V(int, block);
    if (ast _ silence_cycles > 0 && ast _ silence_cycles_counter <= 0)
        return 0;
#ifdef ACTIONSCRIPT
    block_shift = 0;
#else
    block_shift = (ast _ module_info->channels - 1) + (format != ASAP_FORMAT_U8 ? 1 : 0);
#endif
    buffer_blocks = buffer_len >> block_shift;
    if (ast _ current_duration > 0) {
        V(int, total_blocks) = milliseconds_to_blocks(ast _ current_duration);
        if (buffer_blocks > total_blocks - ast _ blocks_played)
            buffer_blocks = total_blocks - ast _ blocks_played;
    }
    block = 0;
    do {
        V(int, blocks) = PokeySound_Generate(ast, CAST(BYTEARRAY) buffer,
            buffer_offset + (block << block_shift), buffer_blocks - block, format);
        ast _ blocks_played += blocks;
        block += blocks;
    } while (block < buffer_blocks && call_6502_player(ast));
    return block << block_shift;
}

FUNC(int, ASAP_Generate, (P(ASAP_State PTR, ast), P(VOIDPTR, buffer), P(int, buffer_len), P(ASAP_SampleFormat, format)))
{
    return ASAP_GenerateAt(ast, buffer, 0, buffer_len, format);
}

#endif /* ASAP_ONLY_INFO */

#ifdef C

abool ASAP_CanSetModuleInfo(const char *filename)
{
    int ext = get_packed_ext(filename);
    return ext == ASAP_EXT('S', 'A', 'P');
}

abool ASAP_ChangeExt(char *filename, const char *ext)
{
    char *dest = NULL;
    while (*filename != '\0') {
        if (*filename == '/' || *filename == '\\')
            dest = NULL;
        else if (*filename == '.')
            dest = filename + 1;
        filename++;
    }
    if (dest == NULL)
        return FALSE;
    strcpy(dest, ext);
    return TRUE;
}

static byte *put_string(byte *dest, const char *str)
{
    while (*str != '\0')
        *dest++ = *str++;
    return dest;
}

static byte *put_dec(byte *dest, int value)
{
    if (value >= 10) {
        dest = put_dec(dest, value / 10);
        value %= 10;
    }
    *dest++ = '0' + value;
    return dest;
}

static byte *put_text_tag(byte *dest, const char *tag, const char *value)
{
    dest = put_string(dest, tag);
    *dest++ = '"';
    if (*value == '\0')
        value = "<?>";
    while (*value != '\0') {
        if (*value < ' ' || *value > 'z' || *value == '"' || *value == '`')
            return NULL;
        *dest++ = *value++;
    }
    *dest++ = '"';
    *dest++ = '\r';
    *dest++ = '\n';
    return dest;
}

static byte *put_dec_tag(byte *dest, const char *tag, int value)
{
    dest = put_string(dest, tag);
    dest = put_dec(dest, value);
    *dest++ = '\r';
    *dest++ = '\n';
    return dest;
}

static byte *start_sap_header(byte *dest, const ASAP_ModuleInfo *module_info)
{
    dest = put_string(dest, "SAP\r\n");
    dest = put_text_tag(dest, "AUTHOR ", module_info->author);
    if (dest == NULL)
        return NULL;
    dest = put_text_tag(dest, "NAME ", module_info->name);
    if (dest == NULL)
        return NULL;
    dest = put_text_tag(dest, "DATE ", module_info->date);
    if (dest == NULL)
        return NULL;
    if (module_info->songs > 1) {
        dest = put_dec_tag(dest, "SONGS ", module_info->songs);
        if (module_info->default_song > 0)
            dest = put_dec_tag(dest, "DEFSONG ", module_info->default_song);
    }
    if (module_info->channels > 1)
        dest = put_string(dest, "STEREO\r\n");
    return dest;
}

static char *two_digits(char *s, int x)
{
    s[0] = '0' + x / 10;
    s[1] = '0' + x % 10;
    return s + 2;
}

void ASAP_DurationToString(char *s, int duration)
{
    if (duration >= 0 && duration < 100 * 60 * 1000) {
        int seconds = duration / 1000;
        s = two_digits(s, seconds / 60);
        *s++ = ':';
        s = two_digits(s, seconds % 60);
        duration %= 1000;
        if (duration != 0) {
            *s++ = '.';
            s = two_digits(s, duration / 10);
            duration %= 10;
            if (duration != 0)
                *s++ = '0' + duration;
        }
    }
    *s = '\0';
}

static byte *put_durations(byte *dest, const ASAP_ModuleInfo *module_info)
{
    int song;
    for (song = 0; song < module_info->songs; song++) {
        if (module_info->durations[song] < 0)
            break;
        dest = put_string(dest, "TIME ");
        ASAP_DurationToString((char *) dest, module_info->durations[song]);
        while (*dest != '\0')
            dest++;
        if (module_info->loops[song])
            dest = put_string(dest, " LOOP");
        *dest++ = '\r';
        *dest++ = '\n';
    }
    return dest;
}

int ASAP_SetModuleInfo(const ASAP_ModuleInfo *module_info, const BYTEARRAY module, int module_len, BYTEARRAY out_module)
{
    byte *dest;
    int i;
    if (memcmp(module, "SAP\r\n", 5) != 0)
        return -1;
    dest = start_sap_header(out_module, module_info);
    if (dest == NULL)
        return -1;
    i = 5;
    while (i < module_len && module[i] != 0xff) {
        if (memcmp(module + i, "AUTHOR ", 7) == 0
         || memcmp(module + i, "NAME ", 5) == 0
         || memcmp(module + i, "DATE ", 5) == 0
         || memcmp(module + i, "SONGS ", 6) == 0
         || memcmp(module + i, "DEFSONG ", 8) == 0
         || memcmp(module + i, "STEREO\r", 7) == 0
         || memcmp(module + i, "TIME ", 5) == 0) {
            while (i < module_len && module[i++] != 0x0a);
        }
        else {
            int b;
            do {
                b = module[i++];
                *dest++ = b;
            } while (i < module_len && b != 0x0a);
        }
    }
    dest = put_durations(dest, module_info);
    module_len -= i;
    memcpy(dest, module + i, module_len);
    dest += module_len;
    return dest - out_module;
}

#if !defined(ASAP_ONLY_SAP) && !defined(ASAP_ONLY_INFO)

#define RMT_INIT  0x0c80
#define TM2_INIT  0x1080

const char *ASAP_CanConvert(
    const char *filename, const ASAP_ModuleInfo *module_info,
    const BYTEARRAY module, int module_len)
{
    (void) filename;
    switch (module_info->type) {
    case ASAP_TYPE_SAP_B:
        if ((module_info->init == 0x3fb || module_info->init == 0x3f9) && module_info->player == 0x503)
            return "dlt";
        if (module_info->init == 0x4f3 || module_info->init == 0xf4f3 || module_info->init == 0x4ef)
            return module_info->fastplay == 156 ? "mpd" : "mpt";
        if (module_info->init == RMT_INIT)
            return "rmt";
        if ((module_info->init == 0x4f5 || module_info->init == 0xf4f5 || module_info->init == 0x4f2)
         || ((module_info->init == 0x4e7 || module_info->init == 0xf4e7 || module_info->init == 0x4e4) && module_info->fastplay == 156)
         || ((module_info->init == 0x4e5 || module_info->init == 0xf4e5 || module_info->init == 0x4e2) && (module_info->fastplay == 104 || module_info->fastplay == 78)))
            return "tmc";
        if (module_info->init == TM2_INIT)
            return "tm2";
        break;
    case ASAP_TYPE_SAP_C:
        if (module_info->player == 0x500 || module_info->player == 0xf500) {
            if (module_info->fastplay == 156)
                return "dmc";
            if (module_info->channels > 1)
                return "cms";
            if (module[module_len - 170] == 0x1e)
                return "cmr";
            if (module[module_len - 909] == 0x30)
                return "cm3";
            return "cmc";
        }
        break;
    case ASAP_TYPE_CMC:
    case ASAP_TYPE_CM3:
    case ASAP_TYPE_CMR:
    case ASAP_TYPE_CMS:
    case ASAP_TYPE_DLT:
    case ASAP_TYPE_MPT:
    case ASAP_TYPE_RMT:
    case ASAP_TYPE_TMC:
    case ASAP_TYPE_TM2:
        return "sap";
    default:
        break;
    }
    return NULL;
}

static byte *put_hex_tag(byte *dest, const char *tag, int value)
{
    int i;
    if (value < 0)
        return dest;
    dest = put_string(dest, tag);
    for (i = 12; i >= 0; i -= 4) {
        int digit = (value >> i) & 0xf;
        *dest++ = (byte) (digit + (digit < 10 ? '0' : 'A' - 10));
    }
    *dest++ = '\r';
    *dest++ = '\n';
    return dest;
}

static byte *put_sap_header(byte *dest, const ASAP_ModuleInfo *module_info, char type, int music, int init, int player)
{
    dest = start_sap_header(dest, module_info);
    if (dest == NULL)
        return NULL;
    dest = put_string(dest, "TYPE ");
    *dest++ = type;
    *dest++ = '\r';
    *dest++ = '\n';
    if (module_info->fastplay != 312)
        dest = put_dec_tag(dest, "FASTPLAY ", module_info->fastplay);
    dest = put_hex_tag(dest, "MUSIC ", music);
    dest = put_hex_tag(dest, "INIT ", init);
    dest = put_hex_tag(dest, "PLAYER ", player);
    dest = put_durations(dest, module_info);
    return dest;
}

int ASAP_Convert(
    const char *filename, const ASAP_ModuleInfo *module_info,
    const BYTEARRAY module, int module_len, BYTEARRAY out_module)
{
    (void) filename;
    int out_len;
    byte *dest;
    int addr;
    int player;
    static const int tmc_player[4] = { 3, -9, -10, -10 };
    static const int tmc_init[4] = { -14, -16, -17, -17 };
    switch (module_info->type) {
    case ASAP_TYPE_SAP_B:
    case ASAP_TYPE_SAP_C:
        out_len = UWORD(module, module_info->header_len + 4) - UWORD(module, module_info->header_len + 2) + 7;
        if (out_len < 7 || module_info->header_len + out_len >= module_len)
            return -1;
        memcpy(out_module, module + module_info->header_len, out_len);
        return out_len;
    case ASAP_TYPE_CMC:
    case ASAP_TYPE_CM3:
    case ASAP_TYPE_CMR:
    case ASAP_TYPE_CMS:
        dest = put_sap_header(out_module, module_info, 'C', module_info->music, -1, module_info->player);
        if (dest == NULL)
            return -1;
        memcpy(dest, module, module_len);
        dest[0] = 0xff; /* some modules start with zeros */
        dest[1] = 0xff;
        dest += module_len;
        if (module_info->type == ASAP_TYPE_CM3) {
            memcpy(dest, cm3_obx + 2, sizeof(cm3_obx) - 2);
            dest += sizeof(cm3_obx) - 2;
        }
        else if (module_info->type == ASAP_TYPE_CMS) {
            memcpy(dest, cms_obx + 2, sizeof(cms_obx) - 2);
            dest += sizeof(cms_obx) - 2;
        }
        else {
            memcpy(dest, cmc_obx + 2, sizeof(cmc_obx) - 2);
            if (module_info->type == ASAP_TYPE_CMR)
                memcpy(dest + 4 + CMR_BASS_TABLE_OFFSET, cmr_bass_table, sizeof(cmr_bass_table));
            dest += sizeof(cmc_obx) - 2;
        }
        return dest - out_module;
    case ASAP_TYPE_DLT:
        if (module_info->songs != 1) {
            addr = module_info->player - 7 - module_info->songs;
            dest = put_sap_header(out_module, module_info, 'B', -1, module_info->player - 7, module_info->player + 0x103);
        }
        else {
            addr = module_info->player - 5;
            dest = put_sap_header(out_module, module_info, 'B', -1, addr, module_info->player + 0x103);
        }
        if (dest == NULL)
            return -1;
        memcpy(dest, module, module_len);
        if (module_len == 0x2c06) {
            dest[4] = 0;
            dest[5] = 0x4c;
            dest[0x2c06] = 0;
        }
        dest += 0x2c07;
        *dest++ = (byte) addr;
        *dest++ = (byte) (addr >> 8);
        *dest++ = dlt_obx[4];
        *dest++ = dlt_obx[5];
        if (module_info->songs != 1) {
            memcpy(dest, module_info->song_pos, module_info->songs);
            dest += module_info->songs;
            *dest++ = 0xaa; /* tax */
            *dest++ = 0xbc; /* ldy song2pos,x */
            *dest++ = (byte) addr;
            *dest++ = (byte) (addr >> 8);
        }
        else {
            *dest++ = 0xa0; /* ldy #0 */
            *dest++ = 0;
        }
        *dest++ = 0x4c; /* jmp init */
        *dest++ = (byte) module_info->player;
        *dest++ = (byte) ((module_info->player >> 8) + 1);
        memcpy(dest, dlt_obx + 6, sizeof(dlt_obx) - 6);
        dest += sizeof(dlt_obx) - 6;
        return dest - out_module;
    case ASAP_TYPE_MPT:
        if (module_info->songs != 1) {
            addr = module_info->player - 17 - module_info->songs;
            dest = put_sap_header(out_module, module_info, 'B', -1, module_info->player - 17, module_info->player + 3);
        }
        else {
            addr = module_info->player - 13;
            dest = put_sap_header(out_module, module_info, 'B', -1, addr, module_info->player + 3);
        }
        if (dest == NULL)
            return -1;
        memcpy(dest, module, module_len);
        dest += module_len;
        *dest++ = (byte) addr;
        *dest++ = (byte) (addr >> 8);
        *dest++ = mpt_obx[4];
        *dest++ = mpt_obx[5];
        if (module_info->songs != 1) {
            memcpy(dest, module_info->song_pos, module_info->songs);
            dest += module_info->songs;
            *dest++ = 0x48; /* pha */
        }
        *dest++ = 0xa0; /* ldy #<music */
        *dest++ = (byte) module_info->music;
        *dest++ = 0xa2; /* ldx #>music */
        *dest++ = (byte) (module_info->music >> 8);
        *dest++ = 0xa9; /* lda #0 */
        *dest++ = 0;
        *dest++ = 0x20; /* jsr player */
        *dest++ = (byte) module_info->player;
        *dest++ = (byte) (module_info->player >> 8);
        if (module_info->songs != 1) {
            *dest++ = 0x68; /* pla */
            *dest++ = 0xa8; /* tay */
            *dest++ = 0xbe; /* ldx song2pos,y */
            *dest++ = (byte) addr;
            *dest++ = (byte) (addr >> 8);
        }
        else {
            *dest++ = 0xa2; /* ldx #0 */
            *dest++ = 0;
        }
        *dest++ = 0xa9; /* lda #2 */
        *dest++ = 2;
        memcpy(dest, mpt_obx + 6, sizeof(mpt_obx) - 6);
        dest += sizeof(mpt_obx) - 6;
        return dest - out_module;
    case ASAP_TYPE_RMT:
        dest = put_sap_header(out_module, module_info, 'B', -1, RMT_INIT, module_info->player + 3);
        if (dest == NULL)
            return -1;
        memcpy(dest, module, module_len);
        dest += module_len;
        *dest++ = (byte) RMT_INIT;
        *dest++ = (byte) (RMT_INIT >> 8);
        if (module_info->songs != 1) {
            addr = RMT_INIT + 10 + module_info->songs;
            *dest++ = (byte) addr;
            *dest++ = (byte) (addr >> 8);
            *dest++ = 0xa8; /* tay */
            *dest++ = 0xb9; /* lda song2pos,y */
            *dest++ = (byte) (RMT_INIT + 11);
            *dest++ = (byte) ((RMT_INIT + 11) >> 8);
        }
        else {
            *dest++ = (byte) (RMT_INIT + 8);
            *dest++ = (byte) ((RMT_INIT + 8) >> 8);
            *dest++ = 0xa9; /* lda #0 */
            *dest++ = 0;
        }
        *dest++ = 0xa2; /* ldx #<music */
        *dest++ = (byte) module_info->music;
        *dest++ = 0xa0; /* ldy #>music */
        *dest++ = (byte) (module_info->music >> 8);
        *dest++ = 0x4c; /* jmp player */
        *dest++ = (byte) module_info->player;
        *dest++ = (byte) (module_info->player >> 8);
        if (module_info->songs != 1) {
            memcpy(dest, module_info->song_pos, module_info->songs);
            dest += module_info->songs;
        }
        if (module_info->channels == 1) {
            memcpy(dest, rmt4_obx + 2, sizeof(rmt4_obx) - 2);
            dest += sizeof(rmt4_obx) - 2;
        }
        else {
            memcpy(dest, rmt8_obx + 2, sizeof(rmt8_obx) - 2);
            dest += sizeof(rmt8_obx) - 2;
        }
        return dest - out_module;
    case ASAP_TYPE_TMC:
        player = module_info->player + tmc_player[module[0x25] - 1];
        addr = player + tmc_init[module[0x25] - 1];
        if (module_info->songs != 1)
            addr -= 3;
        dest = put_sap_header(out_module, module_info, 'B', -1, addr, player);
        if (dest == NULL)
            return -1;
        memcpy(dest, module, module_len);
        dest += module_len;
        *dest++ = (byte) addr;
        *dest++ = (byte) (addr >> 8);
        *dest++ = tmc_obx[4];
        *dest++ = tmc_obx[5];
        if (module_info->songs != 1)
            *dest++ = 0x48; /* pha */
        *dest++ = 0xa0; /* ldy #<music */
        *dest++ = (byte) module_info->music;
        *dest++ = 0xa2; /* ldx #>music */
        *dest++ = (byte) (module_info->music >> 8);
        *dest++ = 0xa9; /* lda #$70 */
        *dest++ = 0x70;
        *dest++ = 0x20; /* jsr player */
        *dest++ = (byte) module_info->player;
        *dest++ = (byte) (module_info->player >> 8);
        if (module_info->songs != 1) {
            *dest++ = 0x68; /* pla */
            *dest++ = 0xaa; /* tax */
            *dest++ = 0xa9; /* lda #0 */
            *dest++ = 0;
        }
        else {
            *dest++ = 0xa9; /* lda #$60 */
            *dest++ = 0x60;
        }
        switch (module[0x25]) {
        case 2:
            *dest++ = 0x06; /* asl 0 */
            *dest++ = 0;
            *dest++ = 0x4c; /* jmp player */
            *dest++ = (byte) module_info->player;
            *dest++ = (byte) (module_info->player >> 8);
            *dest++ = 0xa5; /* lda 0 */
            *dest++ = 0;
            *dest++ = 0xe6; /* inc 0 */
            *dest++ = 0;
            *dest++ = 0x4a; /* lsr @ */
            *dest++ = 0x90; /* bcc player+3 */
            *dest++ = 5;
            *dest++ = 0xb0; /* bcs player+6 */
            *dest++ = 6;
            break;
        case 3:
        case 4:
            *dest++ = 0xa0; /* ldy #1 */
            *dest++ = 1;
            *dest++ = 0x84; /* sty 0 */
            *dest++ = 0;
            *dest++ = 0xd0; /* bne player */
            *dest++ = 10;
            *dest++ = 0xc6; /* dec 0 */
            *dest++ = 0;
            *dest++ = 0xd0; /* bne player+6 */
            *dest++ = 12;
            *dest++ = 0xa0; /* ldy #3 */
            *dest++ = module[0x25];
            *dest++ = 0x84; /* sty 0 */
            *dest++ = 0;
            *dest++ = 0xd0; /* bne player+3 */
            *dest++ = 3;
            break;
        default:
            break;
        }
        memcpy(dest, tmc_obx + 6, sizeof(tmc_obx) - 6);
        dest += sizeof(tmc_obx) - 6;
        return dest - out_module;
    case ASAP_TYPE_TM2:
        dest = put_sap_header(out_module, module_info, 'B', -1, TM2_INIT, module_info->player + 3);
        if (dest == NULL)
            return -1;
        memcpy(dest, module, module_len);
        dest += module_len;
        *dest++ = (byte) TM2_INIT;
        *dest++ = (byte) (TM2_INIT >> 8);
        if (module_info->songs != 1) {
            *dest++ = (byte) (TM2_INIT + 16);
            *dest++ = (byte) ((TM2_INIT + 16) >> 8);
            *dest++ = 0x48; /* pha */
        }
        else {
            *dest++ = (byte) (TM2_INIT + 14);
            *dest++ = (byte) ((TM2_INIT + 14) >> 8);
        }
        *dest++ = 0xa0; /* ldy #<music */
        *dest++ = (byte) module_info->music;
        *dest++ = 0xa2; /* ldx #>music */
        *dest++ = (byte) (module_info->music >> 8);
        *dest++ = 0xa9; /* lda #$70 */
        *dest++ = 0x70;
        *dest++ = 0x20; /* jsr player */
        *dest++ = (byte) module_info->player;
        *dest++ = (byte) (module_info->player >> 8);
        if (module_info->songs != 1) {
            *dest++ = 0x68; /* pla */
            *dest++ = 0xaa; /* tax */
            *dest++ = 0xa9; /* lda #0 */
            *dest++ = 0;
        }
        else {
            *dest++ = 0xa9; /* lda #0 */
            *dest++ = 0;
            *dest++ = 0xaa; /* tax */
        }
        *dest++ = 0x4c; /* jmp player */
        *dest++ = (byte) module_info->player;
        *dest++ = (byte) (module_info->player >> 8);
        memcpy(dest, tm2_obx + 2, sizeof(tm2_obx) - 2);
        dest += sizeof(tm2_obx) - 2;
        return dest - out_module;
    default:
        return -1;
    }
}

#endif /* !defined(ASAP_ONLY_SAP) && !defined(ASAP_ONLY_INFO) */

static abool has_two_digits(const char *s)
{
    return s[0] >= '0' && s[0] <= '9' && s[1] >= '0' && s[1] <= '9';
}

/* "DD/MM/YYYY", "MM/YYYY", "YYYY" -> "YYYY" */
abool ASAP_DateToYear(const char *date, char *year)
{
    if (!has_two_digits(date))
        return FALSE;
    if (date[2] == '/') {
        date += 3;
        if (!has_two_digits(date))
            return FALSE;
        if (date[2] == '/') {
            date += 3;
            if (!has_two_digits(date))
                return FALSE;
        }
    }
    if (!has_two_digits(date + 2) || date[4] != '\0')
        return FALSE;
    memcpy(year, date, 5);
    return TRUE;
}

#endif /* C */
