#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <ctype.h>
#include <math.h>

#include <oslib/osbyte.h>
#include <oslib/osmodule.h>
#include <oslib/osfile.h>
#include <oslib/osfind.h>
#include <oslib/osgbpb.h>
#include <oslib/sound.h>
#include <oslib/toolbox.h>
#include <oslib/macros.h>
#include "datavox.h"

#include "options.h"

#include "ztypes.h"
#include "control.h"
#include "operand.h"
#include "fileio.h"
#include "osdepend.h"
#include "riscosio.h"
#include "riscoswimp.h"
#include "text.h"
#include "rosound.h"
#include "event.h"
#include "v6.h"
#include "v6ro.h"

#include "quetzal.h"
#include "blorb.h"
#include "blorblow.h"

typedef struct sound_header
{
    struct sound_header *next;
    unsigned id;
    unsigned usage;
    double frequency;
    unsigned length;
    unsigned loop_pos;
    unsigned loop_len;
    unsigned repeats;
    unsigned dummy;
    byte data[];
} SampleHeader;

#define usage_SAMP 1
#define usage_SONG 2

static SampleHeader *load_sample(int effect, int urgent);
static void load_music(int effect, int urgent);
static void start_sample(int effect, int vol, int repeats, int routine);
static void start_music(int effect, int vol, int repeats, int routine);
static void stop_effects(void);
static void stop_music(void);
static void lose_effect(int effect);
static uint32 sound_type(int sound);

#define aiff_FORM 0x464F524D
#define aiff_AIFF 0x41494646
#define aiff_COMM 0x434F4D4D
#define aiff_SSND 0x53534E44
#define aiff_MARK 0x4D41524B
#define aiff_INST 0x494E5354
#define type_SONG 0x534F4E47
#define type_MOD  0x4D4F4420
#define type_OGGV 0x4F474756

#define mod_NAME 0
#define mod_SAMPLES 20
#define SAMPLE_SIZE 30
#define mod_SONGLENGTH 950
#define mod_SONGPOSITIONS 952
#define mod_M_K_MARKER 1080
#define mod_PATTERNS 1084
#define PATTERN_SIZE 1024

#define samp_NAME     0
#define samp_LEN     22
#define samp_TUNE    24
#define samp_VOL     25
#define samp_REPPOS  26
#define samp_REPLEN  28

static SampleHeader *loaded_samples;
static byte *log_table;

static SampleHeader *playing_sample;

static int current_music;
static int datavox_channel;

static int total_voices;
static int samples_channel;
static int beep_channel;

static int old_beep_channel;
static int old_voice[8];
static int old_stereo[8];

static bool speech_started;

static bool no_sound; /* No sound at all (sound system disabled) */
static bool sound_enabled; /* More than bleeps (requested by Z-code) */
static bool have_samples;
static bool have_music;
static osbool have_16bit;

static unsigned total_sounds;

#ifdef ALLOW_SPEECH
bool use_speech = 1;
char speech_buffer[SPEECHBUFSIZE + 1];
int speech_ptr;
#endif

int sound_no_input;
static os_t first_play_end; /* Time first play will finish */
static os_t effect_play_start;
static os_t music_play_start;

static int callback_pending;

static int callback_routine;
static int callback_time;

static int music_callback_routine;
static int music_repeats, music_repeats_to_go;

static int *music_interrupt_handler;

#ifdef ZSPEC11DRAFT
void z_sound_data(unsigned sound, unsigned array)
{
    if (!total_sounds)
    {
        conditional_jump(FALSE);
        return;
    }

    if (sound==0)
    {
        set_word(array, total_sounds);
        set_word(array+2, bb_get_release_num(blorb_map));
        conditional_jump(TRUE);
    }
    else
    {
        uint32 type = sound_type(sound);
        if (type == type_SONG || type == type_MOD)
            type = 2;
        else
            type = 1;

        if (type != 0)
        {
            int pos = -1;
            if (type == 1)
            {
                if (playing_sample && playing_sample->id == sound)
                    pos = (os_read_monotonic_time() - effect_play_start) / 10;
            }
            else
            {
                if (current_music == sound && (qtm_song_status() & 7) == 7)
                    pos = (os_read_monotonic_time() - music_play_start) / 10;
            }
            if (pos > 32767) pos = 32767;
            set_word(array, (int) type);
            set_word(array+2, pos);
        }
        conditional_jump(type != 0);
    }
}
#endif

static SampleHeader *sample_loaded(int id)
{
    for (SampleHeader *s = loaded_samples; s; s = s->next)
        if (s->id == id) return s;

    return NULL;
}

static int tidy_samples(void)
{
    int tidied = 0;
    SampleHeader **prev, *s;

    prev = &loaded_samples;
    s = *prev;

    while (s)
    {
        if (s->usage == 0)
        {
            *prev = s->next;
            osmodule_free(s);
            tidied = 1;
            s = *prev;
        }
        else
        {
            prev = &s->next;
            s = *prev;
        }
    }

    return tidied;
}

static void cancel_sound_callback(void)
{
    wimp_poll_flags flags;

    /* Enable null events */
    event_get_mask(&flags);
    event_set_mask(flags | wimp_MASK_NULL);

    callback_pending=FALSE;
}

static bool callback_check(wimp_event_no event_code, wimp_block *event,
                           toolbox_block *id, void *handle)
{
    NOT_USED(event_code); NOT_USED(event); NOT_USED(id); NOT_USED(handle);

    if (callback_pending && os_read_monotonic_time() >= callback_time)
    {
        if (h_type==V6)
            switchoutput(+1);

        cancel_sound_callback();

        playing_sample->usage &=~ usage_SAMP;
        playing_sample = NULL;

        if (callback_routine)
            call_interrupt(callback_routine);

        if (h_type==V6)
            switchoutput(-1);
    }

    return 0;
}

static void arrange_sound_callback(int routine, int delay)
{
    wimp_poll_flags flags;

    /* Enable null events */
    event_get_mask(&flags);
    event_set_mask(flags &~ wimp_MASK_NULL);

    if (h_type >= V5) callback_routine=routine;
    callback_time=os_read_monotonic_time()+delay;

    callback_pending=TRUE;
}

static bool music_pollword_handler(wimp_event_no event_code, wimp_block *event,
                                   toolbox_block *id, void *handle)
{
    NOT_USED(event_code); NOT_USED(event); NOT_USED(id); NOT_USED(handle);

    if (music_repeats_to_go != INT_MAX)
        music_repeats_to_go -= *music_interrupt_handler;

    *music_interrupt_handler = 0;

    if (music_repeats_to_go <= 0)
    {
        qtm_stop();
        if (music_callback_routine)
        {
            if (h_type==V6)
                switchoutput(+1);

            call_interrupt(music_callback_routine);

            if (h_type==V6)
                switchoutput(-1);
        }
    }
    else if (music_repeats_to_go == 1)
    {
        qtm_music_options(2, 2, SKIP, SKIP); /* No repeat next time */
    }

    return 1;
}

static void install_music_interrupt(void)
{
    extern byte music_interrupt_start, music_interrupt_end;
    int len;
    wimp_poll_flags flags;

    len = &music_interrupt_end - &music_interrupt_start;

    music_interrupt_handler = osmodule_alloc(len);
    memcpy(music_interrupt_handler, &music_interrupt_start, len);
    xos_synchronise_code_areas(1, music_interrupt_handler,
                                  music_interrupt_handler + (len/4) - 1);

    event_register_wimp_handler(event_ANY, wimp_POLLWORD_NON_ZERO,
                                music_pollword_handler, 0);

    pollword = music_interrupt_handler;
    event_get_mask(&flags);
    event_set_mask(flags | wimp_GIVEN_POLLWORD);
}

static bool deviceclaim_handler(wimp_message *message, void *handle)
{
    NOT_USED(handle);

    if (message->data.device.major == 6)
    {
        message->action = message_DEVICE_IN_USE;
        message->your_ref = message->my_ref;
        strcpy(message->data.device.info, msgs_lookup_1("SndInUse", GameTitle()));
        message->size = 28 + ((strlen(message->data.device.info)+4)&~3);
        wimp_send_message(wimp_USER_MESSAGE, message, message->sender);
        return 1;
    }

    return 0;
}

static uint32 sound_type(int effect)
{
    bb_err_t err;
    bb_result_t res;
    if (!blorb_map || effect < 3)
        return 0;
    err = bb_load_resource_snd(blorb_map, bb_method_DontLoad, &res, effect, NULL);
    if (err)
        return 0;
    return blorb_map->chunks[res.chunknum].type;
}

static bool is_music(int effect)
{
    uint32 type = sound_type(effect);

    if (type == aiff_FORM)
        return FALSE;
    else
        return TRUE; // unknown treated as music; errors are reported on load
}

static double pitch_ratio(void)
{
    int rate;

    if (have_16bit)
    {
        soundsamplerate_read_current(&rate);
        return rate * (48.0E-6/1024.0);
    }
    else
    {
        sound_configure(SKIP, SKIP, SKIP, SKIP, SKIP,
                        SKIP, SKIP, &rate, SKIP, SKIP);
        return 48.0 / rate;
    }
}

static double duration_ratio(void)
{
    double r = pitch_ratio();
    int length;

    sound_configure(SKIP, SKIP, SKIP, SKIP, SKIP,
                    SKIP, &length, SKIP, SKIP, SKIP);
    return r * 208.0 / length;
}

static void adjusted_sound(int channel_no, int amplitude, int pitch, int duration)
{
    int length;
    int period;

    sound_configure(SKIP, SKIP, SKIP, SKIP, SKIP,
                    SKIP, &length, &period, SKIP, SKIP);

    if (period != 48 || length != 208)
    {
        pitch = (int) (pitch - 0x1000*(log(pitch_ratio())/log(2.0)) + 0.5);
        duration = (int) (duration * duration_ratio() + 0.5);
    }

    if (duration == 255) duration = 254;

    sound_control(channel_no, amplitude, pitch, duration);
}

/*
 * sound
 *
 * Play a sound file or a note.
 *
 * argc = 1: argv[0] = note# (range 1 - 2)
 *
 *           Play note.
 *
 * argc = 2: argv[0] = ID# of sound file
 *           argv[1] = 1
 *
 *           Prepare a sound.

 * argc = 2: argv[0] = 0
 *           argv[1] = 3
 *
 *           Stop playing current sound.
 *
 * argc = 2: argv[0] = 0
 *           argv[1] = 4
 *
 *           Free allocated resources.
 *
 * argc = 3: argv[0] = ID# of sound file to replay.
 *           argv[1] = 2
 *           argv[2] = Volume (lower byte 1-8, or 0xFF (=8))
 *                     upper byte=no of repeats (in V5)
 *
 * argc = 4: argv[0] = ID# of sound file to replay.
 *           argv[1] = 2
 *           argv[2] = Volume/repeats
 *           argv[3] = End routine to call when sound has finished
 *
 */

void z_sound_effect(int argc, unsigned *argv)
{
    /*
    char buffer[256];
    zword_t bufferu[256];

    sprintf(buffer, "[Sound: %d ", argv[0]);
    native_string_to_unicode(bufferu, buffer, '?');
    display_string(bufferu);
    if (argc >= 2)
    {
    	sprintf(buffer, "%d ", argv[1]);
        native_string_to_unicode(bufferu, buffer, '?');
    	display_string(bufferu);
    }
    if (argc >= 3)
    {
    	sprintf(buffer, "%d ", argv[2]);
        native_string_to_unicode(bufferu, buffer, '?');
    	display_string(bufferu);
    }
    if (argc >= 4)
    {
    	sprintf(buffer, "%d ", argv[3]);
        native_string_to_unicode(bufferu, buffer, '?');
    	display_string(bufferu);
    }
    native_string_to_unicode(bufferu, "]", '?');
    display_string(bufferu);
    */

    /* Supply default parameters */

    if (argc < 4)
        argv[3] = 0;
    if (argc < 3)
        argv[2] = 4;
    if (argc < 2)
        argv[1] = 2;
    if (argc < 1)
        argv[0] = 1;

    /* Check global shutdown (due to sound system disabled) */
    if (no_sound)
        return;

    /* Check master enable (from Z-machine header) */
    if (argv[0] >= 3 && !sound_enabled)
        return;

    /* Wait for previous sounds to finish */

    if ((argv[0] >= 3 || argv[1] >= 3) && sound_no_input && playing_sample)
    {
        claim_null_events();

    	while (playing_sample && os_read_monotonic_time() - first_play_end < 0)
    	    poll();

    	release_null_events();
    }

    switch (argv[1])
    {
      case 1:
        if (argv[0] >= 3)
        {
            if (is_music(argv[0]))
                load_music(argv[0], 0);
            else
                load_sample(argv[0], 0);
        }
      	break;

      case 2:
      {
        int vol, repeats;

        /* Volume -1 is maximum (8) */
        vol = argv[2] & 0xFF;
        repeats = argv[2] >> 8;
        if (vol == 0xFF)
            vol = 8;
        if (repeats == 0xFF)
            repeats = -1;
        else if (repeats == 0)
            repeats = 1;

    	if (speech_started)
    	{
    	    speech_started=FALSE;
    	    spch_oldchannel();
    	}

        if (argv[0] == 1 || argv[0]==2)
        {
            if (sound_enabled)
                sound_stereo(beep_channel, 0);
            adjusted_sound(beep_channel, 0x12F+10*vol, argv[0]==1?0x5000:0x4955, 6);
        }
        else if (is_music(argv[0]))
            start_music(argv[0], vol, repeats, argv[3]);
        else
            start_sample(argv[0], vol, repeats, argv[3]);
        break;
      }

      case 3:
    	if (argv[0] == 0 && speech_started)
    	{
    	    speech_started=FALSE;
    	    spch_oldchannel();
    	}
    	if (argv[0] == 0 ||
    	    playing_sample && argv[0] == playing_sample->id)
            stop_effects();
    	if (argv[0] == 0 || argv[0] == current_music)
            stop_music();
        break;

      case 4:
    	if (argv[0] == 0 && speech_started)
    	{
    	    speech_started=FALSE;
    	    spch_oldchannel();
    	}
    	/* Lose music first to free up its samples */
        if (argv[0] == 0 || argv[0] == current_music)
            lose_music(TRUE);
    	if (argv[0] == 0)
            lose_all_effects();
        else
            lose_effect(argv[0]);
      	break;
    }

}

void beep(void)
{
    unsigned arg[4];

    arg[0]=1;

    z_sound_effect(1, arg);
}

static void start_sample(int effect, int vol, int repeats, int routine)
{
    SampleHeader *sample;
    int pitch;
    int duration, sound_duration;

    if (playing_sample)
    {
        cancel_sound_callback();
        playing_sample->usage &=~ usage_SAMP;
        playing_sample = NULL;
    }

    sample = load_sample(effect, 1);

    if (!sample)
    	return;

    datavox_channel=datavox_request_channel(samples_channel, 0x12345678);

    if (datavox_channel == 0)
    	return;

    datavox_set_memory(samples_channel, sample->data,
                                        sample->data+sample->length);

    datavox_type(samples_channel, 0);

    pitch=datavox_sample_to_pitch((int) (1000000.0/sample->frequency+0.5), 0);

    if (h_type >= V5)
        duration = repeats;
    else
    {
    	duration=sample->repeats;
    	if (duration == 0)
    	    duration = -1;
    }

    datavox_timed(datavox_channel, duration!=1);

    if (duration==-1)
    	sound_duration=duration=0xF0000000;
    else
    {
        double d = 20.0/sample->frequency
                   *duration
                   *sample->length;
        int length;
        sound_configure(SKIP, SKIP, SKIP, SKIP, SKIP,
                        SKIP, &length, SKIP, SKIP, SKIP);
        sound_duration = (int) (d * 48.0 / datavox_system_speed()
                                  * 208.0 / length); /* Round down */

        if (sound_duration == 255) sound_duration = 254;
        duration = (int) (d*5 + 0.5);
    }

    sound_stereo(samples_channel, 0);

    sound_attach_named_voice(samples_channel, "DataVox-Voice");

    sound_control(samples_channel, 0x12F+10*vol, pitch, sound_duration);

    sound_no_input=1;
    effect_play_start = os_read_monotonic_time();
    first_play_end = (os_t) (100.0 * sample->length / sample->frequency + 0.5);
    first_play_end += effect_play_start;
    playing_sample = sample;

    if (duration!=0xF0000000)
    	arrange_sound_callback(routine, duration);
}

static void start_music(int effect, int vol, int repeats, int routine)
{
    if (current_music != effect)
    	load_music(effect, 1);

    if (current_music != effect)
    	return;

    qtm_stop();

    music_play_start = os_read_monotonic_time();
    if (h_type >= V5)
    {
        music_repeats_to_go = repeats == -1 ? INT_MAX : repeats;
        music_callback_routine = routine;
    }
    else
        music_repeats_to_go = music_repeats;

    qtm_music_volume(vol * 8);
    qtm_music_options(7, music_repeats_to_go == 1 ? 2 : 0, SKIP, SKIP);
    qtm_music_interrupt(0, music_interrupt_handler+1, 0, SKIP);
    qtm_start();
}

typedef short MarkerID;

typedef struct
{
    MarkerID id;
    unsigned position;
} Marker;

static unsigned marker_pos(int id, Marker *table, int numMarks)
{
    for (int i=0; i < numMarks; i++)
        if (table[i].id == id)
            return table[i].position;

    return 0;
}

static SampleHeader *load_aiff(int no, int urgent)
{
    SampleHeader *sound;
    bb_err_t err;
    bb_result_t res;
    bb_aux_sound_t *aux;
    int form_len, chunk_len;
    unsigned tmp, sustainLoopPlayMode, sustainLoopBegin, sustainLoopEnd;
    int have_COMM = FALSE, have_SSND = FALSE, have_MARK = FALSE, have_INST = FALSE;
    int pos;
    long data_pos, avail_data;
    int numChannels, numSampleFrames, sampleSize, numMarks;
    long double sampleRate;
    Marker *marker_table = NULL;

    err = bb_load_resource_snd(blorb_map, bb_method_FilePos, &res, no, &aux);
    if (err)
    {
        if (err == bb_err_NotFound)
        {
            char buffer[20];
            sprintf(buffer, "%d", no);
            warning_lookup_1("NoSndN", buffer);
        }
        else
            warning_lookup_1("BlorbErr", bb_err_to_string(err));
        return NULL;
    }
    if (blorb_map->chunks[res.chunknum].type != aiff_FORM) goto bad_aiff;

    clearerr(bfp);
    fseek(bfp, res.data.startpos, SEEK_SET);
    if (fget32(bfp) != aiff_FORM) goto bad_aiff;
    form_len = fget32(bfp);
    if (fget32(bfp) != aiff_AIFF) goto bad_aiff;

    pos = 4;
    while (pos <= form_len - 8 && !feof(bfp))
    {
        tmp = fget32(bfp);
        chunk_len = fget32(bfp);
        pos += 8;
        switch (tmp)
        {
          case aiff_COMM:
            if (have_COMM || chunk_len < 18) goto bad_aiff;
            numChannels = fget16(bfp);
            numSampleFrames = fget32(bfp);
            sampleSize = fget16(bfp);
            sampleRate = fget80(bfp);
            if (sampleSize < 1 || sampleSize > 32) goto bad_aiff;
            if (chunk_len > 18)
                fseek(bfp, chunk_len - 18L, SEEK_CUR);
            have_COMM = TRUE;
            pos += chunk_len;
            break;

          case aiff_MARK:
          {
            int togo;
            if (have_MARK || chunk_len < 2) goto bad_aiff;
            numMarks = fget16(bfp);
            marker_table = calloc(numMarks, sizeof(Marker));
            togo = chunk_len - 2;
            for (int i = 0; i < numMarks; i++)
            {
                int s;
                if (togo < 7) goto bad_aiff;
                togo -= 7;
                marker_table[i].id = fget16(bfp);
                marker_table[i].position = fget32(bfp);
                s = fgetc(bfp);
                if (!(s & 1)) s++;
                if (togo < s) goto bad_aiff;
                togo -= s;
                fseek(bfp, s, SEEK_CUR);
            }
            if (togo != 0) goto bad_aiff;
            have_MARK = TRUE;
            pos += chunk_len;
            break;
          }

          case aiff_INST:
            if (have_INST || chunk_len != 20) goto bad_aiff;
            fseek(bfp, 8, SEEK_CUR);
            sustainLoopPlayMode = fget16(bfp);
            sustainLoopBegin = fget16(bfp);
            sustainLoopEnd = fget16(bfp);
            fseek(bfp, 6, SEEK_CUR);
            have_INST = TRUE;
            pos += 20;
            break;

          case aiff_SSND:
            if (have_SSND || chunk_len < 8) goto bad_aiff;
            data_pos = pos + 8L + fget32(bfp);
            avail_data = (long) pos + chunk_len - data_pos;
            have_SSND = TRUE;
            fseek(bfp, chunk_len - 4L, SEEK_CUR);
            pos += chunk_len;
            break;

          default:
            fseek(bfp, chunk_len, SEEK_CUR);
            pos += chunk_len;
            break;
        }
        if (pos & 1) {
            fgetc(bfp);
            pos++;
        }
    }

    if (have_SSND && have_COMM)
    {
        os_error *e;
        int req_data;
        req_data = numSampleFrames * numChannels * ((sampleSize + 7) / 8);
        if (avail_data < req_data) goto bad_aiff;

        fseek(bfp, res.data.startpos + 8 + data_pos, SEEK_SET);
        do
            e = xosmodule_alloc(sizeof(SampleHeader) + numSampleFrames, (void **) &sound);
        while (e && urgent && release_memory());
        if (e)
        {
            if (urgent) warning_lookup("NoMemS");
            free(marker_table);
            return NULL;
        }
        if (numChannels == 1 && sampleSize <= 8)
        {
            /* Fast 8-bit mono case */
            byte *s = sound->data;
            fread(s, 1, numSampleFrames, bfp);
            for (int i = numSampleFrames; i; i--, s++)
                *s = log_table[*s << 5];
        }
        else for (int i = 0; i < numSampleFrames; i++)
        {
            for (int j = 0; j < numChannels; j++)
            {
                unsigned int sample;
                if (sampleSize <= 8)
                    sample = fgetc(bfp) << 24;
                else if (sampleSize <= 16)
                    sample = fget16(bfp) << 16;
                else if (sampleSize <= 24)
                    sample = fget24(bfp) << 8;
                else
                    sample = fget32(bfp);
                if (j == 0)
                    sound->data[i] = log_table[sample >> 19];
            }
        }
        pos += numSampleFrames * numChannels * (sampleSize >> 3);
    }

    if (have_SSND && have_INST && have_MARK && sustainLoopPlayMode != 0)
    {
        sustainLoopBegin = marker_pos(sustainLoopBegin, marker_table, numMarks);
        sustainLoopEnd = marker_pos(sustainLoopEnd, marker_table, numMarks);
    }
    else
        sustainLoopBegin = sustainLoopEnd = 0;

    free(marker_table);

    if (sustainLoopEnd <= sustainLoopBegin) sustainLoopEnd = sustainLoopBegin;

    if (have_SSND && have_COMM)
    {
        sound->id = no;
        sound->usage = 0;
        sound->length = numSampleFrames;
        sound->frequency = (double) sampleRate;
        sound->loop_pos = sustainLoopBegin;
        sound->loop_len = sustainLoopEnd - sustainLoopBegin;
        sound->repeats = aux ? aux->repeats : 1;

        return sound;
    }
  bad_aiff:
    free(marker_table);
    warning_lookup("AIFFErr");
    return NULL;
}

static SampleHeader *load_sample(int effect, int urgent)
{
    SampleHeader *sound;

    sound = sample_loaded(effect);
    if (!sound)
    {
        tidy_samples();

        if (blorb_map)
            sound = load_aiff(effect, urgent);

        if (sound)
        {
            sound->next = loaded_samples;
            loaded_samples = sound;
        }
    }

    if (sound)
        sound->usage |= usage_SAMP;

    return sound;
}

static int song_sampno(byte *p)
{
    if (p[0] != 'S' || p[1] != 'N' || p[2] != 'D')
        return 0;
    return atoi((char *) p+3);
}

static void load_blorb_music(int effect, int urgent)
{
    os_error *e;
    byte *mem;
    bool song;
    bb_err_t err;
    bb_result_t res;
    bb_aux_sound_t *aux;
    unsigned type;

    err = bb_load_resource_snd(blorb_map, bb_method_FilePos, &res, effect, &aux);
    if (err)
    {
        warning_lookup_1("BlorbErr", bb_err_to_string(err));
        return;
    }

    // Main sound type check - all unknown sound types end up here
    type = (unsigned) blorb_map->chunks[res.chunknum].type;
    if (type != type_SONG && type != type_MOD)
    {
        static int warned;
        if (!warned)
        {
            char buffer[5];
            buffer[0] = (type >> 24) & 0xFF;
            buffer[1] = (type >> 16) & 0xFF;
            buffer[2] = (type >> 8) & 0xFF;
            buffer[3] = type & 0xFF;
            buffer[4] = 0;
            warning_lookup_1("BadSndType", buffer);
            warned = TRUE;
        }
        return;
    }
    lose_music(FALSE);

    song = type == type_SONG;
    if (!song)
        tidy_samples();

    if (type != type_SONG && type != type_MOD)
        return;

    /* We need 1 extra byte for a song to get QTM to load it */
    do
        e = xosmodule_alloc((int) res.length + song, (void **) &mem);
    while (e && urgent && release_memory());
    if (e)
    {
        if (urgent) warning_lookup("NoMemS");
        return;
    }

    if (res.length < mod_PATTERNS ||
        fseek(bfp, res.data.startpos, SEEK_SET) ||
        fread(mem, 1, (size_t) res.length, bfp) < res.length)
    {
        warning_lookup("MusicErr");
        return;
    }

    if (song)
    {
        int i;
        for (i=1; i<32; i++)
        {
            byte *p = mem + mod_SAMPLES + (i-1) * SAMPLE_SIZE;
            int instno;
            /* Blank out sample, so QTM won't attempt to load it */
            p[samp_LEN] = 0;
            p[samp_LEN+1] = 0;
            p[samp_REPPOS] = 0;
            p[samp_REPPOS+1] = 0;
            p[samp_REPLEN] = 0;
            p[samp_REPLEN+1] = 1;
            /* Note all wanted samples */
            instno = song_sampno(p);
            if (instno > 0)
            {
                SampleHeader *s=sample_loaded(instno);
                if (s) s->usage |= usage_SONG;
            }
        }
        mem[res.length] = 0;
    }

    e = xqtmload_memory(-1, mem);
    if (e)
    {
        SampleHeader *s;
        for (s = loaded_samples; s; s = s->next)
            s->usage &=~ usage_SONG;
        xosmodule_free(mem);
        warning_lookup_1("BlorbErr", e->errmess);
        return;
    }


    current_music=effect;
    music_repeats = aux ? aux->repeats : 1;
    if (music_repeats == 0) music_repeats = INT_MAX;

    if (song)
    {
        int i;

        /* Lose all unwanted samples to free memory */
        tidy_samples();

        for (i=1; i<32; i++)
        {
            byte *p = mem + mod_SAMPLES + (i-1) * SAMPLE_SIZE;
            SampleHeader *s;
            int instno = song_sampno(p);
            if (instno > 0)
            {
                s = sample_loaded(instno);
                if (!s)
                {
                    s = load_aiff(instno, urgent);
                    if (s)
                    {
                        s->next = loaded_samples;
                        loaded_samples = s;
                    }
                }
                if (s)
                {
                    int loop_pos = s->loop_pos;
                    int loop_len = s->loop_len;
                    if (loop_len == 0)
                    {
                        loop_pos = 0;
                        loop_len = 2;
                    }
                    else
                        loop_pos += 2;

                    s->usage |= usage_SONG;
                    s->dummy = 0;
                    qtm_register_sample(i, s->data - 2, s->length + 2,
                                        loop_pos, loop_len,
                                        p[samp_VOL], p[samp_TUNE]);
                }
                else
                {
                    lose_music(1);
                    if (urgent) warning_lookup("NoMemS");
                    return;
                }
            }
        }
    }
}

static void load_music(int effect, int urgent)
{
    if (current_music == effect)
    	return;

    load_blorb_music(effect, urgent);
}

static void stop_effects(void)
{
    if (datavox_channel)
    {
        sound_control(samples_channel, 0, 0, 0);
        datavox_unset(datavox_channel);
    	datavox_deallocate_channel(samples_channel, 0x12345678);
        datavox_channel=0;
    }
}

static void stop_music(void)
{
    if (current_music)
    {
        void *handler;

        qtm_stop();

        handler = qtm_music_interrupt(0, (void *) -1, 0, SKIP);
        if (handler == music_interrupt_handler+1)
            qtm_music_interrupt(0, 0, 0, SKIP);
    }
}

void lose_all_effects(void)
{
    SampleHeader *s;

    stop_effects();

    for (s = loaded_samples; s; s = s->next)
        s->usage &=~ usage_SAMP;

    tidy_samples();
}

void lose_effect(int id)
{
    if (playing_sample && playing_sample->id == id)
        stop_effects();

    for (SampleHeader *s = loaded_samples; s; s = s->next)
        if (s->id == id)
            s->usage &=~ usage_SAMP;

    tidy_samples();
}

void lose_music(bool tidy)
{
    if (current_music)
    {
        qtm_clear(0);

        /* QTM appears to muck up the number of voices after a clear */
        sound_configure(total_voices, 0, 0, 0, 0,
                        SKIP, SKIP, SKIP, SKIP, SKIP);

        for (SampleHeader *s = loaded_samples; s; s = s->next)
            s->usage &=~ usage_SONG;

        current_music=0;
    }

    if (tidy)
        tidy_samples();
}

static void create_log_table(void)
{
    byte *a, *b;
    int v;

    log_table = (byte *)malloc(8192);
    if (!log_table)
        fatal_lookup("NoMem");

    /* Work from both ends */
    a = log_table;
    b = a + 8192;

    *a++ = v = 0x00; /* Zero is a special case */

    for (int step = 1; ; step <<= 1)
    {
        for (int point = 16; point; point--)
        {
            v += 0x02;
            if (v > 0xFE) v = 0xFE;
            for (int count = step; count != 0; count--)
            {
                *a++ = v;
                *--b = v | 1;
                if (a >= b) return;
            }
        }
    }
}

static int claim_state;

static bool inuse_handler(wimp_message *message, void *handle)
{
    if (message->your_ref == (int) handle)
    {
        claim_state = 1;
        warning_lookup_1("SndUnavail", message->data.device.info);
        return 1;
    }

    return 0;
}

static bool claim_bounce_handler(wimp_event_no event_code, wimp_block *event,
                                 toolbox_block *id, void *handle)
{
    NOT_USED(event_code); NOT_USED(id);

    if (event->message.action == message_DEVICE_CLAIM &&
        event->message.my_ref == (int) handle)
    {
        claim_state = 2;
        return 1;
    }

    return 0;
}

static int claim_sound_system(void)
{
    wimp_message m;

    m.action = message_DEVICE_CLAIM;
    m.your_ref = 0;
    m.data.device.major = 6;
    m.data.device.minor = 0;
    strcpy(m.data.device.info, msgs_lookup("_TaskName"));
    m.size = 28 + ((strlen(m.data.device.info) + 4) &~3);
    wimp_send_message(wimp_USER_MESSAGE_RECORDED, &m, wimp_BROADCAST);

    event_register_message_handler(message_DEVICE_IN_USE, inuse_handler, (void *) m.my_ref);
    event_register_wimp_handler(event_ANY, wimp_USER_MESSAGE_ACKNOWLEDGE, claim_bounce_handler, (void *) m.my_ref);

    claim_state = 0;

    do
        poll();
    while (claim_state == 0);

    if (claim_state == 2)
    {
        event_register_message_handler(message_DEVICE_CLAIM, deviceclaim_handler, 0);
        return 1;
    }
    else
        return 0;
}

static void shutdown_sound(void)
{
    int i;
    if (have_music)
    {
        void *handler;

        handler = qtm_music_interrupt(0, (void *) -1, 0, SKIP);
        if (handler == music_interrupt_handler+1)
            qtm_music_interrupt(0, 0, 0, SKIP);

        osmodule_free(music_interrupt_handler);
        qtm_sound_control(0, -1, -1, SKIP, SKIP);
    }
    osbyte_write(osbyte_VAR_BELL_CHANNEL, old_beep_channel);
    sound_configure(1, 0, 0, 0, 0, SKIP, SKIP, SKIP, SKIP, SKIP);
    for (i=1; i<=8; i++)
    {
        xsound_attach_voice(i, old_voice[i-1], SKIP, SKIP);
        xsound_stereo(i, old_stereo[i-1], SKIP);
    }
}

int setup_sound(int effects)
{
    sound_state ss;
    int i;
    bb_err_t err;
    bb_result_t res;
    int sounds, min_snd, max_snd;

    /* If sound is enables, don't go through it again. */
    if (sound_enabled)
        return have_samples | (have_music << 1);

    /* If sound modules missing, or sound disabled, be utterly silent */
    no_sound = xsound_enable(sound_STATE_READ, &ss) || ss == sound_STATE_OFF;
    if (no_sound)
        return 0;

    beep_channel = osbyte_read(osbyte_VAR_BELL_CHANNEL);

    /* If nothing more than a bleep wanted, or we have no blorb, then we
     * stop here. */
    if (!effects || !blorb_map)
    	return 0;

    sound_enabled = TRUE;

    xsoundmode_read_configuration(&have_16bit, SKIP);

    err = bb_count_resources(blorb_map, bb_ID_Snd, &sounds, &min_snd, &max_snd);
    if (err) fatal_lookup_1("BlorbErr", bb_err_to_string(err));
    if (sounds)
    {
        int i;
        for (sounds=0, i=min_snd; i<=max_snd; i++)
        {
            int type;
            err = bb_load_resource_snd(blorb_map, bb_method_FilePos, &res, i, NULL);
            if (err == bb_err_NotFound) continue;
            else if (err) fatal_lookup_1("BlorbErr", bb_err_to_string(err));

            type = (unsigned) blorb_map->chunks[res.chunknum].type;
            if (type == aiff_FORM)
            {
                have_samples = TRUE; /* Hmm. It'll do */
                sounds++;
            }
            else if (type == type_SONG || type == type_MOD)
            {
                have_music = TRUE;
                sounds++;
            }
            else
            {
                static int badsnd_warned;
                if (!badsnd_warned)
                {
                    char buffer[5];
                    buffer[0] = (type >> 24) & 0xFF;
                    buffer[1] = (type >> 16) & 0xFF;
                    buffer[2] = (type >> 8) & 0xFF;
                    buffer[3] = type & 0xFF;
                    buffer[4] = 0;
                    warning_lookup_1("BadSndType", buffer);
                    badsnd_warned = TRUE;
                }
            }
        }
    }

    if (have_samples || have_music)
    {
        /* We DO have samples or music, so try to claim the sound system */
        if (!claim_sound_system())
        {
            /* Oh dear. */
            sound_enabled = FALSE;
            return 0;
        }
    }

    total_sounds = sounds;

    /* If we get this far, we've got the sound, and we're keeping it.
     * We're going to mess with the sound system good and proper.
     * To be polite, we'll adjust the system beep channel to be our
     * beep channel.
     */

    create_log_table();

    event_register_wimp_handler(event_ANY, wimp_NULL_REASON_CODE, callback_check, 0);

    if (have_samples)
        os_cli("RMEnsure DataVox 3.67 RMLoad <Zip2000$Dir>.Modules.DataVox");

    if (have_music)
        os_cli("RMEnsure QTMTracker 1.40 RMLoad <Zip2000$Dir>.Modules.QTMModule");

    if (have_music)
    {
        total_voices = 8;
        samples_channel = 6;
        beep_channel = 5;

        qtm_clear(0);
    }
    else
    {
        if (have_samples)
        {
            samples_channel = 2;
            total_voices = 2;
        }
        else
            total_voices = 1;

        beep_channel = 1;

    }

    sound_configure(total_voices, 0, 0, 0, 0,
                    SKIP, SKIP, SKIP, SKIP, SKIP);

    for (i=1; i<=8; i++)
    {
        sound_attach_voice(i, 0, SKIP, &old_voice[i-1]);
        old_stereo[i-1] = sound_stereo(i, -128);
    }

    atexit(shutdown_sound);
    if (have_music)
    {
        qtm_sound_control(total_voices, 1, -1, NULL, NULL);
        qtm_stereo(0, 0);
        install_music_interrupt();
    }

    sound_attach_named_voice(beep_channel, "WaveSynth-Beep");
    sound_stereo(beep_channel, 0);
    old_beep_channel = osbyte1(osbyte_VAR_BELL_CHANNEL, beep_channel, 0);

    return have_samples | (have_music << 1);
}

static void rma_release_memory(void)
{
    xos_change_dynamic_area(os_DYNAMIC_AREA_RMA, -16*1024*1024, SKIP);
}

static int samples_release_memory(void)
{
    if (tidy_samples())
    {
        rma_release_memory();
        return 1;
    }
    else
        return 0;
}

static int music_release_memory(void)
{
    /* If we think we have a current song, and QTM says it has an RMA
     * loaded track that isn't playing.
     */
    if (current_music && (qtm_song_status() & 7) == 3)
    {
        lose_music(TRUE);
        rma_release_memory();
        return 1;
    }
    else
        return 0;
}

int sound_release_memory(void)
{
    return samples_release_memory() ||
           music_release_memory();
}

#ifdef ALLOW_SPEECH
#define MAX_SAY 100

void flush_speech()
{
    char *start = speech_buffer, *end, *mid;
    char temp;

    spch_reset();
    speech_buffer[speech_ptr]='\0';

    /*
     * We are limited to 100? characters max for Speech! (Yuck)
     */

    for (;;)
    {
        while (isspace(*start) && isspace(*(start+1)))
        	start++;

        end = MIN(start + MAX_SAY, speech_buffer + speech_ptr - 1);

        if (end == speech_buffer + speech_ptr - 1)
        {
        	spch_say(start);
        	speech_ptr=0;
        	speech_started=TRUE;
        	return;
        }

    	/* Where to split? Try after full stops or question marks */
        for (mid = end; *mid != '.' && *mid != '?' && mid > start; mid--)
       	    continue;

    	/* Nope? Try after a space */
        if (mid == start)
            for (mid = end; !isspace(*mid) && mid > start; mid--)
        	continue;

    	/* What? Just split anyway */
        if (mid == start)
            mid = end - 1;

    	temp = *++mid;

        *mid = '\0';
        spch_sayw(start);
        *mid = temp;
        start = mid;
    }
}
#endif
