/* $Header: d:/cvsroot/tads/html/win32/tadssnd.h,v 1.2 1999/05/17 02:52:25 MJRoberts Exp $ */

/* 
 *   Copyright (c) 1998 by Michael J. Roberts.  All Rights Reserved.
 *   
 *   Please see the accompanying license file, LICENSE.TXT, for information
 *   on using and copying this software.  
 */
/*
Name
  tadssnd.h - TADS sound object
Function
  
Notes
  
Modified
  01/10/98 MJRoberts  - Creation
*/

#ifndef TADSSND_H
#define TADSSND_H

#include <math.h>

#ifndef TADSHTML_H
#include "tadshtml.h"
#endif


/*------------------------------------------------------------------------ */
/*
 *   Audio Controller - provides global control over certain aspects of sound
 *   playback.  This is an abstract interface that must be provided to sound
 *   objects when they're created.  
 */
class CTadsAudioControl
{
public:
    /* reference control */
    virtual void audioctl_add_ref() = 0;
    virtual void audioctl_release() = 0;

    /* get the current muting status */
    virtual int get_mute_sound() = 0;

    /* 
     *   Register/unregister a playing sound.  When the controller needs to
     *   change a global playback parameter globally (such as the muting
     *   status), it'll inform each registered sound object of the change.  
     */
    virtual void register_active_sound(class CTadsAudioPlayer *sound) = 0;
    virtual void unregister_active_sound(class CTadsAudioPlayer *sound) = 0;
};

/*
 *   The generic sound player interface.  System sound player objects can
 *   register themselves with the audio controller to receive notifications
 *   of changes to global system sound settings.  
 */
class CTadsAudioPlayer
{
public:
    CTadsAudioPlayer(HWND hwnd,
                     void (*done_func)(void *, int), void *done_func_ctx);
    virtual ~CTadsAudioPlayer();

    /* is the track still playing? */
    virtual int is_playing() = 0;

    /* get the starting time of playback, in terms of GetTickCount() time */
    DWORD get_start_time() const { return start_time_; }

    /* terminate playback */
    virtual void stop(int sync) = 0;

    /* have we been terminated? */
    virtual int is_stopped() = 0;

    /* 
     *   Get the real-time playback length of the sound, in milliseconds.
     *   Return 0 if it's not possible to determine the length of the track.
     */
    virtual long get_track_len_ms() = 0;

    /* receive notification of a change in muting status */
    virtual void on_mute_change(int mute) = 0;

    /* set the playback volume - 0..10000 (silence to unattenuated) */
    virtual void set_audio_volume(int vol) = 0;

    /* 
     *   Immediately call the 'done' callback.  The callback should never be
     *   invoked more than once per play, so if the callback has already been
     *   invoked, this should do nothing. 
     */
    virtual void call_done_callback() = 0;

    /* note that we're starting a background fade on this player */
    virtual void note_background_fade() { }

    /* reference counting */
    virtual void AddRef() = 0;
    virtual void Release() = 0;

    /* get our start and stop event handles */
    HANDLE get_start_evt() const { return start_evt_; }
    HANDLE get_stop_evt() const { return stop_evt_; }

    /* 
     *   Process the window message we use to handle a 'done' notification.
     *   When the background thread finishes playing a sound, it posts a
     *   message to our associated window; the window in turn calls this
     *   method, which finally calls the callback function provided when the
     *   sound was initiated.  We go through this extra message step, rather
     *   than calling the callback directly from the playback thread, because
     *   we want to ensure that the callback is only ever invoked from the
     *   main thread.  
     */
    void on_sound_done(int repeat_count);

    /* send the "sound done" notification to our owner window */
    void send_done_message(int repeat_count);

    /* 
     *   Synchronize on stop.  This waits until playback has stopped, the
     *   'done' callback has been invoked, and any background thread has
     *   terminated.
     */
    void sync_on_stop();

protected:
    /* 
     *   Playback tracking events: we set started_evt_ when playback starts,
     *   and we set ended_evt_ when playback ends.  These allow other objects
     *   in other threads (e.g., volume control threads) to keep track of
     *   when we start and stop, for better timing coordination.
     */
    HANDLE start_evt_;
    HANDLE stop_evt_;

    /* callback event: we set this when we invoke the 'done' callback */
    HANDLE cb_evt_;

    /* the background thread handle */
    HANDLE thread_hdl_;
    DWORD thread_id_;

    /* starting time, as a GetTickCount() time */
    DWORD start_time_;

    /* owner window */
    HWND hwnd_;

    /* the 'done' callback function */
    void (*done_func_)(void *ctx, int repeat_count);
    void *done_func_ctx_;
};

/* ------------------------------------------------------------------------ */
/*
 *   Volume control.  Each player has an associated volume control, which
 *   mediates muting, fade in, and fade out.  
 */
class CTadsAudioVolumeControl
{
public:
    CTadsAudioVolumeControl(CTadsAudioPlayer *player,
                            int init_vol, int fading_in, int muted);
    virtual ~CTadsAudioVolumeControl();

    /* reference counting - for thread safety, use interlocked inc/dec */
    void AddRef()
    {
        InterlockedIncrement(&refcnt_);
    }
    void Release()
    {
        if (InterlockedDecrement(&refcnt_) == 0)
            delete this;
    }

    /*
     *   Set the base playback volume.  This is the volume we use when we're
     *   not muting or fading.  By default, we play back at 100% volume.
     *   This sets the volume on a 0..100 scale, where 0 is silence and 100
     *   is unattenuated.  
     */
    void set_base_volume(int level);

    /* mute the sound */
    void mute(int muted);

    /* 
     *   Adjust the fade-in or fade-out level.  The actual playback level is
     *   the lesser of the current fade-in and fade-out levels, so that if we
     *   are simultaneously fading in and out, we'll have a nice smooth
     *   triangle envelope that peaks at the intersection of the two fade
     *   curves.  If we have multiple concurrent fades in the same direction,
     *   we'll keep the fade smooth by using the LEADING fade: when fading
     *   in, we'll ignore a new fade-in level that's less than the previous
     *   fade-in level, and when fading out we'll ignore new levels that are
     *   higher than previous levels.  (Of course, muting always takes
     *   precedence over fading.)
     *   
     *   The level is given in the range 0..10000, where 0 is silence and
     *   10000 is unattenuated.  
     */
    void set_fade_in(int level);
    void set_fade_out(int level);

private:
    /* 
     *   Update the playback volume level to match the current parameters -
     *   this computes the volume level based on the current fade and mute
     *   settings, and sets it in the underlying playback object 
     */
    void update_level();

    /* the base volume level (0-100) */
    int base_vol_;

    /* are we muted? */
    int muted_;

    /* current fade-in and fade-out levels */
    int fade_in_level_;
    int fade_out_level_;

    /* we are a reference-counted object */
    LONG refcnt_;

    /* critical section object, for synchronization */
    CRITICAL_SECTION critsec_;

    /* the underlying audio player object */
    CTadsAudioPlayer *player_;
};


/* ------------------------------------------------------------------------ */
/*
 *   Fader control 
 */
class CTadsAudioFader
{
public:
    CTadsAudioFader(CTadsAudioPlayer *player,
                    CTadsAudioVolumeControl *volctl, long ms);
    virtual ~CTadsAudioFader();

    /* 
     *   start the fade - call this after creating the object to initiate the
     *   background thread; after this is called, the fade will run by itself
     */
    void start_fade();

    /* reference counting */
    void AddRef() { InterlockedIncrement(&refcnt_); }
    void Release()
    {
        if (InterlockedDecrement(&refcnt_) == 0)
            delete this;
    }

protected:
    /* 
     *   Wait until the appointed start time for the fade action.  Return
     *   true if the fade should proceed, false if not. 
     */
    virtual int wait_until_ready();

    /* 
     *   Do any final action after the fade is finished.  'ok' is the status
     *   returned from wait_until_ready() - it indicates whether or not we
     *   actually did the fade.  
     */
    virtual void after_fade(int ok) { }

    /* 
     *   Get the volume at the given point in the fade.  The step is given as
     *   a fraction of the time in the fade, from 0.0 to 1.0.  Returns a
     *   value from 0 (total silence) to 10000 (full volume). 
     */
    virtual int get_step_vol(double step) = 0;

    /* set the volume in the volume control */
    virtual void set_volctl_volume(int vol) = 0;

    /* static thread entrypoint */
    static DWORD thread_main(void *ctx)
    {
        /* the thread context is the 'this' object */
        CTadsAudioFader *self = (CTadsAudioFader *)ctx;
            
        /* run the thread-main member function */
        self->do_thread_main();

        /* release the thread's reference on the fader */
        self->Release();

        /* successful completion */
        return 0;
    }

    /* member function thread handler */
    void do_thread_main();

    /* duration of the fade, in milliseconds */
    long ms_;

    /* reference count */
    LONG refcnt_;

    /* the player associated with the fade */
    CTadsAudioPlayer *player_;

    /* volume control object for the track player */
    CTadsAudioVolumeControl *volctl_;
};

/*
 *   Fade In fader 
 */
class CTadsAudioFaderIn: public CTadsAudioFader
{
public:
    CTadsAudioFaderIn(CTadsAudioPlayer *player,
                      CTadsAudioVolumeControl *volctl, long ms)
        : CTadsAudioFader(player, volctl, ms) { }

protected:
    /* when fading in, the volume goes up on each step */
    virtual int get_step_vol(double step)
    {
        /* 
         *   Figure the point on the log10 scale from 1 to 10, then normalize
         *   to the 0-10000 range.  
         */
        return (int)(log10(1.0 + step*9.0)*10000.0);
    }

    /* set the volume in the volume control */
    virtual void set_volctl_volume(int vol) { volctl_->set_fade_in(vol); }
};

/*
 *   Base Fade Out fader 
 */
class CTadsAudioFaderOut: public CTadsAudioFader
{
public:
    CTadsAudioFaderOut(CTadsAudioPlayer *player,
                       CTadsAudioVolumeControl *volctl, long ms)
        : CTadsAudioFader(player, volctl, ms) { }

protected:
    /* when fading down, the volume goes down on each step */
    virtual int get_step_vol(double step)
    {
        /* 
         *   Figure the point on the log10 scale from 1 to 10, then normalize
         *   to the 0-10000 range. 
         */
        return (int)(log10(10.0 - step*9.0)*10000.0);
    }

    /* after the fade is finished, kill the playback in the player */
    virtual void after_fade(int ok);

    /* set the volume in the volume control */
    virtual void set_volctl_volume(int vol) { volctl_->set_fade_out(vol); }
};

/*
 *   Cancellation Fade Out fader.  This starts a fade-out immediately, then
 *   stops the underlying player once we get to silence.  
 */
class CTadsAudioFaderOutCxl: public CTadsAudioFaderOut
{
public:
    CTadsAudioFaderOutCxl(class CTadsAudioPlayer *player,
                          CTadsAudioVolumeControl *volctl, long ms)
        : CTadsAudioFaderOut(player, volctl, ms) { }
};


/*
 *   End-of-track Fade Out fader.  This waits for the duration of the track
 *   minus the fade time, then starts the fade-out.  
 */
class CTadsAudioFaderOutEnd: public CTadsAudioFaderOut
{
public:
    CTadsAudioFaderOutEnd(
        CTadsAudioPlayer *player, CTadsAudioVolumeControl *volctl,
        long ms, int crossfade)
        : CTadsAudioFaderOut(player, volctl, ms)
    {
        /* remember the cross-fade status */
        crossfade_ = crossfade;
    }

protected:
    /* 
     *   when fading out, we need to wait until 'ms' millseconds before the
     *   end of the track 
     */
    virtual int wait_until_ready();

    /* are we doing a cross-fade on the fade-out? */
    int crossfade_;
};


/* ------------------------------------------------------------------------ */
/*
 *   Base windows-specific sound object 
 */
class CTadsSound
{
public:
    CTadsSound();
    virtual ~CTadsSound();

    /* load from a sound resource loader */
    int load_from_res(class CHtmlSound *loader);

    /* cancel with fade-out */
    void cancel_sound(class CHtmlSysWin *win, int sync,
                      long fade_out_ms, int fade_in_bg);

    /* add a crossfade */
    void add_crossfade(class CHtmlSysWin *win, long ms);

    /* get/detach the player object */
    virtual class CTadsAudioPlayer *get_player() const = 0;
    virtual void detach_player() = 0;

protected:
    /* file information */
    CStringBuf fname_;
    unsigned long seek_pos_;
    unsigned long data_size_;

    /* volume control */
    CTadsAudioVolumeControl *volctl_;
};


#endif /* TADSSND_H */
