root/src/kits/game/GameSoundBuffer.cpp
//------------------------------------------------------------------------------
//      Copyright (c) 2001-2002, Haiku
//
//      Permission is hereby granted, free of charge, to any person obtaining a
//      copy of this software and associated documentation files (the "Software"),
//      to deal in the Software without restriction, including without limitation
//      the rights to use, copy, modify, merge, publish, distribute, sublicense,
//      and/or sell copies of the Software, and to permit persons to whom the
//      Software is furnished to do so, subject to the following conditions:
//
//      The above copyright notice and this permission notice shall be included in
//      all copies or substantial portions of the Software.
//
//      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//      IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//      FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//      AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//      LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//      FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
//      DEALINGS IN THE SOFTWARE.
//
//      File Name:              GameSoundBuffer.h
//      Author:                 Christopher ML Zumwalt May (zummy@users.sf.net)
//      Description:    Interface to a single sound, managed by the GameSoundDevice.
//------------------------------------------------------------------------------


#include "GameSoundBuffer.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <MediaRoster.h>
#include <MediaAddOn.h>
#include <MediaTheme.h>
#include <TimeSource.h>
#include <BufferGroup.h>

#include "GameProducer.h"
#include "GameSoundDevice.h"
#include "StreamingGameSound.h"
#include "GSUtility.h"

// Sound Buffer Utility functions ----------------------------------------
template<typename T, int32 min, int32 middle, int32 max>
static inline void
ApplyMod(T* data, int64 index, float* pan)
{
        data[index * 2] = clamp<T, min, max>(float(data[index * 2] - middle)
                * pan[0] + middle);
        data[index * 2 + 1] = clamp<T, min, max>(float(data[index * 2 + 1] - middle)
                * pan[1] + middle);
}


// GameSoundBuffer -------------------------------------------------------
GameSoundBuffer::GameSoundBuffer(const gs_audio_format * format)
        :
        fLooping(false),
        fIsConnected(false),
        fIsPlaying(false),
        fGain(1.0),
        fPan(0.0),
        fPanLeft(1.0),
        fPanRight(1.0),
        fGainRamp(NULL),
        fPanRamp(NULL)
{
        fConnection = new Connection;
        fNode = new GameProducer(this, format);

        fFrameSize = get_sample_size(format->format) * format->channel_count;

        fFormat = *format;
}


// Play must stop before the distructor is called; otherwise, a fatal
// error occures if the playback is in a subclass.
GameSoundBuffer::~GameSoundBuffer()
{
        BMediaRoster* roster = BMediaRoster::Roster();

        if (fIsConnected) {
                // Ordinarily we'd stop *all* of the nodes in the chain at this point.
                // However, one of the nodes is the System Mixer, and stopping the Mixer
                // is a Bad Idea (tm). So, we just disconnect from it, and release our
                // references to the nodes that we're using.  We *are* supposed to do
                // that even for global nodes like the Mixer.
                roster->Disconnect(fConnection->producer.node, fConnection->source,
                        fConnection->consumer.node, fConnection->destination);

                roster->ReleaseNode(fConnection->producer);
                roster->ReleaseNode(fConnection->consumer);
        }

        delete fGainRamp;
        delete fPanRamp;

        delete fConnection;
        delete fNode;
}


const gs_audio_format &
GameSoundBuffer::Format() const
{
        return fFormat;
}


bool
GameSoundBuffer::IsLooping() const
{
        return fLooping;
}


void
GameSoundBuffer::SetLooping(bool looping)
{
        fLooping = looping;
}


float
GameSoundBuffer::Gain() const
{
        return fGain;
}


status_t
GameSoundBuffer::SetGain(float gain, bigtime_t duration)
{
        if (gain < 0.0 || gain > 1.0)
                return B_BAD_VALUE;

        delete fGainRamp;
        fGainRamp = NULL;

        if (duration > 100000)
                fGainRamp  = InitRamp(&fGain, gain, fFormat.frame_rate, duration);
        else
                fGain = gain;

        return B_OK;
}


float
GameSoundBuffer::Pan() const
{
        return fPan;
}


status_t
GameSoundBuffer::SetPan(float pan, bigtime_t duration)
{
        if (pan < -1.0 || pan > 1.0)
                return B_BAD_VALUE;

        delete fPanRamp;
        fPanRamp = NULL;

        if (duration < 100000) {
                fPan = pan;

                if (fPan < 0.0) {
                        fPanLeft = 1.0;
                        fPanRight = 1.0 + fPan;
                } else {
                        fPanRight = 1.0;
                        fPanLeft = 1.0 - fPan;
                }
        } else
                fPanRamp = InitRamp(&fPan, pan, fFormat.frame_rate, duration);

        return B_OK;
}


status_t
GameSoundBuffer::GetAttributes(gs_attribute * attributes,
        size_t attributeCount)
{
        for (size_t i = 0; i < attributeCount; i++) {
                switch (attributes[i].attribute) {
                        case B_GS_GAIN:
                                attributes[i].value = fGain;
                                if (fGainRamp)
                                        attributes[i].duration = fGainRamp->duration;
                                break;

                        case B_GS_PAN:
                                attributes[i].value = fPan;
                                if (fPanRamp)
                                        attributes[i].duration = fPanRamp->duration;
                                break;

                        case B_GS_LOOPING:
                                attributes[i].value = (fLooping) ? -1.0 : 0.0;
                                attributes[i].duration = bigtime_t(0);
                                break;

                        default:
                                attributes[i].value = 0.0;
                                attributes[i].duration = bigtime_t(0);
                                break;
                }
        }

        return B_OK;
}


status_t
GameSoundBuffer::SetAttributes(gs_attribute * attributes,
        size_t attributeCount)
{
        status_t error = B_OK;

        for (size_t i = 0; i < attributeCount; i++) {
                switch (attributes[i].attribute) {
                        case B_GS_GAIN:
                                error = SetGain(attributes[i].value, attributes[i].duration);
                                break;

                        case B_GS_PAN:
                                error = SetPan(attributes[i].value, attributes[i].duration);
                                break;

                        case B_GS_LOOPING:
                                fLooping = bool(attributes[i].value);
                                break;

                        default:
                                break;
                }
        }

        return error;
}


void
GameSoundBuffer::Play(void * data, int64 frames)
{
        // Mh... should we add some locking?
        if (!fIsPlaying)
                return;

        if (fFormat.channel_count == 2) {
                float pan[2];
                pan[0] = fPanRight * fGain;
                pan[1] = fPanLeft * fGain;

                FillBuffer(data, frames);

                switch (fFormat.format) {
                        case gs_audio_format::B_GS_U8:
                        {
                                for (int64 i = 0; i < frames; i++) {
                                        ApplyMod<uint8, 0, 128, UINT8_MAX>((uint8*)data, i, pan);
                                        UpdateMods();
                                }

                                break;
                        }

                        case gs_audio_format::B_GS_S16:
                        {
                                for (int64 i = 0; i < frames; i++) {
                                        ApplyMod<int16, INT16_MIN, 0, INT16_MAX>((int16*)data, i,
                                                pan);
                                        UpdateMods();
                                }

                                break;
                        }

                        case gs_audio_format::B_GS_S32:
                        {
                                for (int64 i = 0; i < frames; i++) {
                                        ApplyMod<int32, INT32_MIN, 0, INT32_MAX>((int32*)data, i,
                                                pan);
                                        UpdateMods();
                                }

                                break;
                        }

                        case gs_audio_format::B_GS_F:
                        {
                                for (int64 i = 0; i < frames; i++) {
                                        ApplyMod<float, -1, 0, 1>((float*)data, i, pan);
                                        UpdateMods();
                                }

                                break;
                        }
                }
        } else if (fFormat.channel_count == 1) {
                // FIXME the output should be stereo, and we could pan mono sounds
                // here. But currently the output has the same number of channels as
                // the sound and we can't do this.
                // FIXME also, we don't handle the gain here.
                FillBuffer(data, frames);
        } else
                debugger("Invalid number of channels.");

}


void
GameSoundBuffer::UpdateMods()
{
        // adjust the gain if needed
        if (fGainRamp) {
                if (ChangeRamp(fGainRamp)) {
                        delete fGainRamp;
                        fGainRamp = NULL;
                }
        }

        // adjust the ramp if needed
        if (fPanRamp) {
                if (ChangeRamp(fPanRamp)) {
                        delete fPanRamp;
                        fPanRamp = NULL;
                } else {
                        if (fPan < 0.0) {
                                fPanLeft = 1.0;
                                fPanRight = 1.0 + fPan;
                        } else {
                                fPanRight = 1.0;
                                fPanLeft = 1.0 - fPan;
                        }
                }
        }
}


void
GameSoundBuffer::Reset()
{
        fGain = 1.0;
        delete fGainRamp;
        fGainRamp = NULL;

        fPan = 0.0;
        fPanLeft = 1.0;
        fPanRight = 1.0;

        delete fPanRamp;
        fPanRamp = NULL;

        fLooping = false;
}


status_t
GameSoundBuffer::Connect(media_node * consumer)
{
        BMediaRoster* roster = BMediaRoster::Roster();
        status_t err = roster->RegisterNode(fNode);

        if (err != B_OK)
                return err;

        // make sure the Media Roster knows that we're using the node
        err = roster->GetNodeFor(fNode->Node().node, &fConnection->producer);

        if (err != B_OK)
                return err;

        // connect to the mixer
        fConnection->consumer = *consumer;

        // set the producer's time source to be the "default" time source, which
        // the Mixer uses too.
        err = roster->GetTimeSource(&fConnection->timeSource);
        if (err != B_OK)
                return err;

        err = roster->SetTimeSourceFor(fConnection->producer.node,
                fConnection->timeSource.node);
        if (err != B_OK)
                return err;
        // got the nodes; now we find the endpoints of the connection
        media_input mixerInput;
        media_output soundOutput;
        int32 count = 1;
        err = roster->GetFreeOutputsFor(fConnection->producer, &soundOutput, 1,
                &count);

        if (err != B_OK)
                return err;
        count = 1;
        err = roster->GetFreeInputsFor(fConnection->consumer, &mixerInput, 1,
                &count);
        if (err != B_OK)
                return err;

        // got the endpoints; now we connect it!
        media_format format;
        format.type = B_MEDIA_RAW_AUDIO;
        format.u.raw_audio = media_raw_audio_format::wildcard;
        err = roster->Connect(soundOutput.source, mixerInput.destination, &format,
                &soundOutput, &mixerInput);
        if (err != B_OK)
                return err;

        // the inputs and outputs might have been reassigned during the
        // nodes' negotiation of the Connect().  That's why we wait until
        // after Connect() finishes to save their contents.
        fConnection->format = format;
        fConnection->source = soundOutput.source;
        fConnection->destination = mixerInput.destination;

        fIsConnected = true;
        return B_OK;
}


status_t
GameSoundBuffer::StartPlaying()
{
        if (fIsPlaying)
                return EALREADY;

        BMediaRoster* roster = BMediaRoster::Roster();
        BTimeSource* source = roster->MakeTimeSourceFor(fConnection->producer);

        // make sure we give the producer enough time to run buffers through
        // the node chain, otherwise it'll start up already late
        bigtime_t latency = 0;
        status_t status = roster->GetLatencyFor(fConnection->producer, &latency);
        if (status == B_OK) {
                status = roster->StartNode(fConnection->producer,
                        source->Now() + latency);
        }
        source->Release();

        fIsPlaying = true;

        return status;
}


status_t
GameSoundBuffer::StopPlaying()
{
        if (!fIsPlaying)
                return EALREADY;

        BMediaRoster* roster = BMediaRoster::Roster();
        roster->StopNode(fConnection->producer, 0, true);
                // synchronous stop

        Reset();
        fIsPlaying = false;

        return B_OK;
}


bool
GameSoundBuffer::IsPlaying()
{
        return fIsPlaying;
}


// SimpleSoundBuffer ------------------------------------------------------
SimpleSoundBuffer::SimpleSoundBuffer(const gs_audio_format * format,
        const void * data, int64 frames)
        :
        GameSoundBuffer(format),
        fPosition(0)
{
        fBufferSize = frames * fFrameSize;
        fBuffer = (char*)data;
}


SimpleSoundBuffer::~SimpleSoundBuffer()
{
        delete [] fBuffer;
}


void
SimpleSoundBuffer::Reset()
{
        GameSoundBuffer::Reset();
        fPosition = 0;
}


void
SimpleSoundBuffer::FillBuffer(void * data, int64 frames)
{
        char * buffer = (char*)data;
        size_t bytes = fFrameSize * frames;

        if (fPosition + bytes >= fBufferSize) {
                if (fPosition < fBufferSize) {
                        // copy the remaining frames
                        size_t remainder = fBufferSize - fPosition;
                        memcpy(buffer, &fBuffer[fPosition], remainder);

                        bytes -= remainder;
                        buffer += remainder;
                }

                if (fLooping) {
                        // restart the sound from the beginning
                        memcpy(buffer, fBuffer, bytes);
                        fPosition = bytes;
                        bytes = 0;
                } else {
                        fPosition = fBufferSize;
                }

                if (bytes > 0) {
                        // Fill the rest with silence
                        int middle = 0;
                        if (fFormat.format == gs_audio_format::B_GS_U8)
                                middle = 128;
                        memset(buffer, middle, bytes);
                }
        } else {
                memcpy(buffer, &fBuffer[fPosition], bytes);
                fPosition += bytes;
        }
}


// StreamingSoundBuffer ------------------------------------------------------
StreamingSoundBuffer::StreamingSoundBuffer(const gs_audio_format * format,
        const void * streamHook, size_t inBufferFrameCount, size_t inBufferCount)
        :
        GameSoundBuffer(format),
        fStreamHook(const_cast<void *>(streamHook))
{
        if (inBufferFrameCount != 0 && inBufferCount  != 0) {
                BBufferGroup *bufferGroup
                        = new BBufferGroup(inBufferFrameCount * fFrameSize, inBufferCount);
                fNode->SetBufferGroup(fConnection->source, bufferGroup);
        }
}


StreamingSoundBuffer::~StreamingSoundBuffer()
{
}


void
StreamingSoundBuffer::FillBuffer(void * buffer, int64 frames)
{
        BStreamingGameSound* object = (BStreamingGameSound*)fStreamHook;

        size_t bytes = fFrameSize * frames;
        object->FillBuffer(buffer, bytes);
}