blob: 4f8f1d047b3a9d33ba28b6e85d7152cee18b3e74 [file] [log] [blame]
/* $Id$ */
/*
* Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <pjmedia/sound.h>
#include <pjmedia/alaw_ulaw.h>
#include <pjmedia/errno.h>
#include <pj/assert.h>
#include <pj/log.h>
#include <pj/os.h>
#include <e32msgqueue.h>
#include <sounddevice.h>
#include <APSClientSession.h>
//////////////////////////////////////////////////////////////////////////////
//
#define THIS_FILE "symbian_sound_aps.cpp"
#define BYTES_PER_SAMPLE 2
#define POOL_NAME "SymbianSoundAps"
#define POOL_SIZE 512
#define POOL_INC 512
#if 1
# define TRACE_(st) PJ_LOG(3, st)
#else
# define TRACE_(st)
#endif
static pjmedia_snd_dev_info symbian_snd_dev_info =
{
"Symbian Sound Device (APS)",
1,
1,
8000
};
/* App UID to open global APS queues to communicate with the APS server. */
extern TPtrC APP_UID;
/* Default setting for loudspeaker */
static pj_bool_t act_loudspeaker = PJ_FALSE;
/* Forward declaration of CPjAudioEngine */
class CPjAudioEngine;
/*
* PJMEDIA Sound Stream instance
*/
struct pjmedia_snd_stream
{
// Pool
pj_pool_t *pool;
// Common settings.
pjmedia_dir dir;
unsigned clock_rate;
unsigned channel_count;
unsigned samples_per_frame;
// Audio engine
CPjAudioEngine *engine;
};
static pj_pool_factory *snd_pool_factory;
/*
* Utility: print sound device error
*/
static void snd_perror(const char *title, TInt rc)
{
PJ_LOG(1,(THIS_FILE, "%s (error code=%d)", title, rc));
}
//////////////////////////////////////////////////////////////////////////////
//
/**
* Abstract class for handler of callbacks from APS client.
*/
class MQueueHandlerObserver
{
public:
virtual void InputStreamInitialized(const TInt aStatus) = 0;
virtual void OutputStreamInitialized(const TInt aStatus) = 0;
virtual void NotifyError(const TInt aError) = 0;
virtual void RecCb(TAPSCommBuffer &buffer) = 0;
virtual void PlayCb(TAPSCommBuffer &buffer) = 0;
};
/**
* Handler for communication and data queue.
*/
class CQueueHandler : public CActive
{
public:
// Types of queue handler
enum TQueueHandlerType {
ERecordCommQueue,
EPlayCommQueue,
ERecordQueue,
EPlayQueue
};
// The order corresponds to the APS Server state, do not change!
enum TState {
EAPSPlayerInitialize = 1,
EAPSRecorderInitialize = 2,
EAPSPlayData = 3,
EAPSRecordData = 4,
EAPSPlayerInitComplete = 5,
EAPSRecorderInitComplete = 6
};
static CQueueHandler* NewL(MQueueHandlerObserver* aObserver,
RMsgQueue<TAPSCommBuffer>* aQ,
TQueueHandlerType aType)
{
CQueueHandler* self = new (ELeave) CQueueHandler(aObserver, aQ, aType);
CleanupStack::PushL(self);
self->ConstructL();
CleanupStack::Pop(self);
return self;
}
// Destructor
~CQueueHandler() { Cancel(); }
// Start listening queue event
void Start() {
iQ->NotifyDataAvailable(iStatus);
SetActive();
}
private:
// Constructor
CQueueHandler(MQueueHandlerObserver* aObserver,
RMsgQueue<TAPSCommBuffer>* aQ,
TQueueHandlerType aType)
: CActive(CActive::EPriorityHigh),
iQ(aQ), iObserver(aObserver), iType(aType)
{
CActiveScheduler::Add(this);
// use lower priority for comm queues
if ((iType == ERecordCommQueue) || (iType == EPlayCommQueue))
SetPriority(CActive::EPriorityStandard);
}
// Second phase constructor
void ConstructL() {}
// Inherited from CActive
void DoCancel() { iQ->CancelDataAvailable(); }
void RunL() {
if (iStatus != KErrNone) {
iObserver->NotifyError(iStatus.Int());
return;
}
TAPSCommBuffer buffer;
TInt ret = iQ->Receive(buffer);
if (ret != KErrNone) {
iObserver->NotifyError(ret);
return;
}
switch (iType) {
case ERecordQueue:
if (buffer.iCommand == EAPSRecordData) {
iObserver->RecCb(buffer);
}
break;
// Callbacks from the APS main thread
case EPlayCommQueue:
switch (buffer.iCommand) {
case EAPSPlayData:
if (buffer.iStatus == KErrUnderflow) {
iObserver->PlayCb(buffer);
}
break;
case EAPSPlayerInitialize:
iObserver->NotifyError(buffer.iStatus);
break;
case EAPSPlayerInitComplete:
iObserver->OutputStreamInitialized(buffer.iStatus);
break;
case EAPSRecorderInitComplete:
iObserver->InputStreamInitialized(buffer.iStatus);
break;
default:
iObserver->NotifyError(buffer.iStatus);
break;
}
break;
// Callbacks from the APS recorder thread
case ERecordCommQueue:
switch (buffer.iCommand) {
// The APS recorder thread will only report errors
// through this handler. All other callbacks will be
// sent from the APS main thread through EPlayCommQueue
case EAPSRecorderInitialize:
if (buffer.iStatus == KErrNone) {
iObserver->InputStreamInitialized(buffer.iStatus);
break;
}
case EAPSRecordData:
iObserver->NotifyError(buffer.iStatus);
break;
default:
break;
}
break;
default:
break;
}
// issue next request
iQ->NotifyDataAvailable(iStatus);
SetActive();
}
// Data
RMsgQueue<TAPSCommBuffer> *iQ; // (not owned)
MQueueHandlerObserver *iObserver; // (not owned)
TQueueHandlerType iType;
};
/*
* Implementation: Symbian Input & Output Stream.
*/
class CPjAudioEngine : public CBase, MQueueHandlerObserver
{
public:
enum State
{
STATE_NULL,
STATE_READY,
STATE_STREAMING
};
~CPjAudioEngine();
static CPjAudioEngine *NewL(pjmedia_snd_stream *parent_strm,
pjmedia_dir dir,
pjmedia_snd_rec_cb rec_cb,
pjmedia_snd_play_cb play_cb,
void *user_data);
TInt StartL();
void Stop();
TInt ActivateSpeaker(TBool active);
private:
CPjAudioEngine(pjmedia_snd_stream *parent_strm,
pjmedia_dir dir,
pjmedia_snd_rec_cb rec_cb,
pjmedia_snd_play_cb play_cb,
void *user_data);
void ConstructL();
TInt InitPlayL();
TInt InitRecL();
TInt StartStreamL();
// Inherited from MQueueHandlerObserver
virtual void InputStreamInitialized(const TInt aStatus);
virtual void OutputStreamInitialized(const TInt aStatus);
virtual void NotifyError(const TInt aError);
virtual void RecCb(TAPSCommBuffer &buffer);
virtual void PlayCb(TAPSCommBuffer &buffer);
State state_;
pjmedia_snd_stream *parentStrm_;
pjmedia_dir dir_;
pjmedia_snd_rec_cb recCb_;
pjmedia_snd_play_cb playCb_;
void *userData_;
pj_uint32_t TsPlay_;
pj_uint32_t TsRec_;
RAPSSession iSession;
TAPSInitSettings iSettings;
RMsgQueue<TAPSCommBuffer> iReadQ;
RMsgQueue<TAPSCommBuffer> iReadCommQ;
RMsgQueue<TAPSCommBuffer> iWriteQ;
RMsgQueue<TAPSCommBuffer> iWriteCommQ;
CQueueHandler *iPlayCommHandler;
CQueueHandler *iRecCommHandler;
CQueueHandler *iRecHandler;
};
CPjAudioEngine* CPjAudioEngine::NewL(pjmedia_snd_stream *parent_strm,
pjmedia_dir dir,
pjmedia_snd_rec_cb rec_cb,
pjmedia_snd_play_cb play_cb,
void *user_data)
{
CPjAudioEngine* self = new (ELeave) CPjAudioEngine(parent_strm, dir,
rec_cb, play_cb,
user_data);
CleanupStack::PushL(self);
self->ConstructL();
CleanupStack::Pop(self);
return self;
}
CPjAudioEngine::CPjAudioEngine(pjmedia_snd_stream *parent_strm,
pjmedia_dir dir,
pjmedia_snd_rec_cb rec_cb,
pjmedia_snd_play_cb play_cb,
void *user_data)
: state_(STATE_NULL),
parentStrm_(parent_strm),
dir_(dir),
recCb_(rec_cb),
playCb_(play_cb),
userData_(user_data),
iPlayCommHandler(0),
iRecCommHandler(0),
iRecHandler(0)
{
}
CPjAudioEngine::~CPjAudioEngine()
{
Stop();
delete iPlayCommHandler;
iPlayCommHandler = NULL;
delete iRecCommHandler;
iRecCommHandler = NULL;
iSession.Close();
if (state_ == STATE_READY) {
if (dir_ != PJMEDIA_DIR_PLAYBACK) {
iReadQ.Close();
iReadCommQ.Close();
}
iWriteQ.Close();
iWriteCommQ.Close();
}
}
TInt CPjAudioEngine::InitPlayL()
{
if (state_ == STATE_STREAMING || state_ == STATE_READY)
return 0;
TInt err = iSession.InitializePlayer(iSettings);
if (err != KErrNone) {
snd_perror("Failed to initialize player", err);
return err;
}
// Open message queues for the output stream
TBuf<128> buf2 = iSettings.iGlobal;
buf2.Append(_L("PlayQueue"));
TBuf<128> buf3 = iSettings.iGlobal;
buf3.Append(_L("PlayCommQueue"));
while (iWriteQ.OpenGlobal(buf2))
User::After(10);
while (iWriteCommQ.OpenGlobal(buf3))
User::After(10);
// Construct message queue handler
iPlayCommHandler = CQueueHandler::NewL(this,
&iWriteCommQ,
CQueueHandler::EPlayCommQueue);
// Start observing APS callbacks on output stream message queue
iPlayCommHandler->Start();
return 0;
}
TInt CPjAudioEngine::InitRecL()
{
if (state_ == STATE_STREAMING || state_ == STATE_READY)
return 0;
// Initialize input stream device
TInt err = iSession.InitializeRecorder(iSettings);
if (err != KErrNone) {
snd_perror("Failed to initialize recorder", err);
return err;
}
TBuf<128> buf1 = iSettings.iGlobal;
buf1.Append(_L("RecordQueue"));
TBuf<128> buf4 = iSettings.iGlobal;
buf4.Append(_L("RecordCommQueue"));
// Must wait for APS thread to finish creating message queues
// before we can open and use them.
while (iReadQ.OpenGlobal(buf1))
User::After(10);
while (iReadCommQ.OpenGlobal(buf4))
User::After(10);
// Construct message queue handlers
iRecCommHandler = CQueueHandler::NewL(this,
&iReadCommQ,
CQueueHandler::ERecordCommQueue);
// Start observing APS callbacks from on input stream message queue
iRecCommHandler->Start();
return 0;
}
TInt CPjAudioEngine::StartL()
{
TInt err = iSession.Connect();
if (err != KErrNone && err != KErrAlreadyExists)
return err;
if (state_ == STATE_READY)
return StartStreamL();
// Even if only capturer are opened, playback thread of APS Server need
// to be run(?). Since some messages will be delivered via play comm queue.
return InitPlayL();
}
void CPjAudioEngine::Stop()
{
iSession.Stop();
delete iRecHandler;
iRecHandler = NULL;
state_ = STATE_READY;
}
void CPjAudioEngine::ConstructL()
{
iSettings.iFourCC = TFourCC(KMCPFourCCIdG711);
iSettings.iGlobal = APP_UID;
iSettings.iPriority = TMdaPriority(100);
iSettings.iPreference = TMdaPriorityPreference(0x05210001);
iSettings.iSettings.iChannels = EMMFMono;
iSettings.iSettings.iSampleRate = EMMFSampleRate8000Hz;
iSettings.iSettings.iVolume = 0;
}
TInt CPjAudioEngine::StartStreamL()
{
if (state_ == STATE_STREAMING)
return 0;
iSession.SetCng(EFalse);
iSession.SetVadMode(EFalse);
iSession.SetPlc(EFalse);
iSession.SetEncoderMode(EALawOr20ms);
iSession.SetDecoderMode(EALawOr20ms);
iSession.ActivateLoudspeaker(act_loudspeaker);
// Not only playback
if (dir_ != PJMEDIA_DIR_PLAYBACK) {
iRecHandler = CQueueHandler::NewL(this, &iReadQ,
CQueueHandler::ERecordQueue);
iRecHandler->Start();
iSession.Read();
}
// Not only capture
if (dir_ != PJMEDIA_DIR_CAPTURE) {
iSession.Write();
}
state_ = STATE_STREAMING;
return 0;
}
// Inherited from MQueueHandlerObserver
void CPjAudioEngine::InputStreamInitialized(const TInt aStatus)
{
TRACE_((THIS_FILE, "InputStreamInitialized %d", aStatus));
state_ = STATE_READY;
if (aStatus == KErrNone) {
StartStreamL();
}
}
void CPjAudioEngine::OutputStreamInitialized(const TInt aStatus)
{
TRACE_((THIS_FILE, "OutputStreamInitialized %d", aStatus));
if (aStatus == KErrNone) {
if (dir_ == PJMEDIA_DIR_PLAYBACK) {
state_ = STATE_READY;
// Only playback, start directly
StartStreamL();
} else
InitRecL();
}
}
void CPjAudioEngine::NotifyError(const TInt aError)
{
snd_perror("Error from CQueueHandler", aError);
}
void CPjAudioEngine::RecCb(TAPSCommBuffer &buffer)
{
pj_int16_t buf[160];
pj_assert(buffer.iBuffer[0] == 1 && buffer.iBuffer[1] == 0);
for (int i=0; i<160; ++i)
buf[i] = pjmedia_alaw2linear(buffer.iBuffer[i+2]);
recCb_(userData_, 0, buf, sizeof(buf));
}
void CPjAudioEngine::PlayCb(TAPSCommBuffer &buffer)
{
pj_int16_t buf[160];
playCb_(userData_, 0, buf, sizeof(buf));
buffer.iCommand = CQueueHandler::EAPSPlayData;
buffer.iStatus = 0;
buffer.iBuffer.Zero();
buffer.iBuffer.Append(1);
buffer.iBuffer.Append(0);
for (int i=0; i<160; ++i)
buffer.iBuffer.Append(pjmedia_linear2alaw(buf[i]));
iWriteQ.Send(buffer);
}
TInt CPjAudioEngine::ActivateSpeaker(TBool active)
{
if (state_ == STATE_READY || state_ == STATE_STREAMING) {
iSession.ActivateLoudspeaker(active);
return KErrNone;
}
return KErrNotReady;
}
//////////////////////////////////////////////////////////////////////////////
//
/*
* Initialize sound subsystem.
*/
PJ_DEF(pj_status_t) pjmedia_snd_init(pj_pool_factory *factory)
{
snd_pool_factory = factory;
return PJ_SUCCESS;
}
/*
* Get device count.
*/
PJ_DEF(int) pjmedia_snd_get_dev_count(void)
{
/* Always return 1 */
return 1;
}
/*
* Get device info.
*/
PJ_DEF(const pjmedia_snd_dev_info*) pjmedia_snd_get_dev_info(unsigned index)
{
/* Always return the default sound device */
if (index == (unsigned)-1)
index = 0;
PJ_ASSERT_RETURN(index==0, NULL);
return &symbian_snd_dev_info;
}
static pj_status_t sound_open(pjmedia_dir dir,
unsigned clock_rate,
unsigned channel_count,
unsigned samples_per_frame,
unsigned bits_per_sample,
pjmedia_snd_rec_cb rec_cb,
pjmedia_snd_play_cb play_cb,
void *user_data,
pjmedia_snd_stream **p_snd_strm)
{
pj_pool_t *pool;
pjmedia_snd_stream *strm;
PJ_ASSERT_RETURN(p_snd_strm, PJ_EINVAL);
PJ_ASSERT_RETURN(clock_rate == 8000 && channel_count == 1 &&
bits_per_sample == 16, PJ_ENOTSUP);
PJ_ASSERT_RETURN((dir == PJMEDIA_DIR_CAPTURE_PLAYBACK && rec_cb && play_cb)
|| (dir == PJMEDIA_DIR_CAPTURE && rec_cb && !play_cb)
|| (dir == PJMEDIA_DIR_PLAYBACK && !rec_cb && play_cb),
PJ_EINVAL);
pool = pj_pool_create(snd_pool_factory, POOL_NAME, POOL_SIZE, POOL_INC,
NULL);
if (!pool)
return PJ_ENOMEM;
strm = (pjmedia_snd_stream*) pj_pool_zalloc(pool,
sizeof(pjmedia_snd_stream));
strm->dir = dir;
strm->pool = pool;
strm->clock_rate = clock_rate;
strm->channel_count = channel_count;
strm->samples_per_frame = samples_per_frame;
// Create the audio engine.
TRAPD(err, strm->engine = CPjAudioEngine::NewL(strm, strm->dir,
rec_cb, play_cb,
user_data));
if (err != KErrNone) {
pj_pool_release(pool);
return PJ_RETURN_OS_ERROR(err);
}
// Done.
*p_snd_strm = strm;
return PJ_SUCCESS;
}
/*
* Open sound recorder stream.
*/
PJ_DEF(pj_status_t) pjmedia_snd_open_rec( int index,
unsigned clock_rate,
unsigned channel_count,
unsigned samples_per_frame,
unsigned bits_per_sample,
pjmedia_snd_rec_cb rec_cb,
void *user_data,
pjmedia_snd_stream **p_snd_strm)
{
if (index < 0) index = 0;
PJ_ASSERT_RETURN(index == 0, PJ_EINVAL);
return sound_open(PJMEDIA_DIR_CAPTURE, clock_rate, channel_count,
samples_per_frame, bits_per_sample, rec_cb, NULL,
user_data, p_snd_strm);
}
PJ_DEF(pj_status_t) pjmedia_snd_open_player( int index,
unsigned clock_rate,
unsigned channel_count,
unsigned samples_per_frame,
unsigned bits_per_sample,
pjmedia_snd_play_cb play_cb,
void *user_data,
pjmedia_snd_stream **p_snd_strm )
{
if (index < 0) index = 0;
PJ_ASSERT_RETURN(index == 0, PJ_EINVAL);
return sound_open(PJMEDIA_DIR_PLAYBACK, clock_rate, channel_count,
samples_per_frame, bits_per_sample, NULL, play_cb,
user_data, p_snd_strm);
}
PJ_DEF(pj_status_t) pjmedia_snd_open( int rec_id,
int play_id,
unsigned clock_rate,
unsigned channel_count,
unsigned samples_per_frame,
unsigned bits_per_sample,
pjmedia_snd_rec_cb rec_cb,
pjmedia_snd_play_cb play_cb,
void *user_data,
pjmedia_snd_stream **p_snd_strm)
{
if (rec_id < 0) rec_id = 0;
if (play_id < 0) play_id = 0;
PJ_ASSERT_RETURN(play_id == 0 && rec_id == 0, PJ_EINVAL);
return sound_open(PJMEDIA_DIR_CAPTURE_PLAYBACK, clock_rate, channel_count,
samples_per_frame, bits_per_sample, rec_cb, play_cb,
user_data, p_snd_strm);
}
/*
* Get stream info.
*/
PJ_DEF(pj_status_t) pjmedia_snd_stream_get_info(pjmedia_snd_stream *strm,
pjmedia_snd_stream_info *pi)
{
PJ_ASSERT_RETURN(strm && pi, PJ_EINVAL);
pj_bzero(pi, sizeof(*pi));
pi->dir = strm->dir;
pi->play_id = 0;
pi->rec_id = 0;
pi->clock_rate = strm->clock_rate;
pi->channel_count = strm->channel_count;
pi->samples_per_frame = strm->samples_per_frame;
pi->bits_per_sample = BYTES_PER_SAMPLE * 8;
// latencies approximation (in samples)
pi->rec_latency = strm->samples_per_frame * 2;
pi->play_latency = strm->samples_per_frame * 2;
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_snd_stream_start(pjmedia_snd_stream *stream)
{
PJ_ASSERT_RETURN(stream != NULL, PJ_EINVAL);
if (stream->engine) {
TInt err = stream->engine->StartL();
if (err != KErrNone)
return PJ_RETURN_OS_ERROR(err);
}
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_snd_stream_stop(pjmedia_snd_stream *stream)
{
PJ_ASSERT_RETURN(stream != NULL, PJ_EINVAL);
if (stream->engine) {
stream->engine->Stop();
}
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_snd_stream_close(pjmedia_snd_stream *stream)
{
pj_pool_t *pool;
PJ_ASSERT_RETURN(stream != NULL, PJ_EINVAL);
if (stream->engine) {
delete stream->engine;
stream->engine = NULL;
}
pool = stream->pool;
if (pool) {
stream->pool = NULL;
pj_pool_release(pool);
}
return PJ_SUCCESS;
}
PJ_DEF(pj_status_t) pjmedia_snd_deinit(void)
{
/* Nothing to do */
return PJ_SUCCESS;
}
/*
* Set sound latency.
*/
PJ_DEF(pj_status_t) pjmedia_snd_set_latency(unsigned input_latency,
unsigned output_latency)
{
/* Nothing to do */
PJ_UNUSED_ARG(input_latency);
PJ_UNUSED_ARG(output_latency);
return PJ_SUCCESS;
}
/*
* Activate/deactivate loudspeaker.
*/
PJ_DEF(pj_status_t) pjmedia_snd_aps_activate_loudspeaker(
pjmedia_snd_stream *stream,
pj_bool_t active)
{
if (stream == NULL) {
act_loudspeaker = active;
} else {
if (stream->engine == NULL)
return PJ_EINVAL;
TInt err = stream->engine->ActivateSpeaker(active);
if (err != KErrNone)
return PJ_RETURN_OS_ERROR(err);
}
return PJ_SUCCESS;
}