Continuing ticket #2: initial test with Symbian sound device implementation. Callback works, but not in full duplex

git-svn-id: https://svn.pjsip.org/repos/pjproject/trunk@1428 74dad513-b988-da41-8d7b-12977e46ad98
diff --git a/build.symbian/symsndtest.mmp b/build.symbian/symsndtest.mmp
new file mode 100644
index 0000000..ad659f9
--- /dev/null
+++ b/build.symbian/symsndtest.mmp
@@ -0,0 +1,26 @@
+TARGET 		symsndtest.exe
+TARGETTYPE 	exe
+UID		0x100039CE 0x10004287
+VENDORID	0x70000001
+
+SOURCEPATH	..\pjsip-apps\src\symsndtest
+
+MACRO		PJ_M_I386=1
+MACRO		PJ_SYMBIAN=1
+MACRO		PJ_DLL=1
+
+// Test files
+
+SOURCE	app_main.cpp
+SOURCE	main_symbian.cpp
+
+SYSTEMINCLUDE	..\pjlib\include
+SYSTEMINCLUDE	..\pjmedia\include
+
+SYSTEMINCLUDE	\epoc32\include
+SYSTEMINCLUDE	\epoc32\include\libc
+
+LIBRARY		charconv.lib euser.lib estlib.lib eexe.lib
+LIBRARY		symbian_audio.lib pjlib.lib ecrt0.lib
+CAPABILITY	None
+
diff --git a/pjmedia/src/pjmedia/symbian_sound.cpp b/pjmedia/src/pjmedia/symbian_sound.cpp
index c2db57b..ee49f48 100644
--- a/pjmedia/src/pjmedia/symbian_sound.cpp
+++ b/pjmedia/src/pjmedia/symbian_sound.cpp
@@ -115,13 +115,21 @@
 }
 
 
+/*
+ * 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));
+}
+ 
 //////////////////////////////////////////////////////////////////////////////
 //
 
 /*
  * Implementation: Symbian Input Stream.
  */
-class CPjAudioInputEngine : public MMdaAudioInputStreamCallback
+class CPjAudioInputEngine : public CBase, MMdaAudioInputStreamCallback
 {
 public:
     enum State
@@ -143,7 +151,7 @@
     pj_status_t StartRecord();
     void Stop();
 
-public:
+private:
     State		     state_;
     pjmedia_snd_stream	    *parentStrm_;
     pjmedia_snd_rec_cb	     recCb_;
@@ -158,6 +166,7 @@
 			pjmedia_snd_rec_cb rec_cb,
 			void *user_data);
     void ConstructL();
+    TPtr8 & GetFrame();
     
 public:
     virtual void MaiscOpenComplete(TInt aError);
@@ -171,7 +180,7 @@
 					 pjmedia_snd_rec_cb rec_cb,
 					 void *user_data)
     : state_(STATE_INACTIVE), parentStrm_(parent_strm), recCb_(rec_cb), 
-      iInputStream_(NULL), iStreamBuffer_(NULL), iFramePtr_(NULL, 0),
+      iInputStream_(NULL), iStreamBuffer_(NULL), iFramePtr_(0, 0),
       userData_(user_data), lastError_(KErrNone), timeStamp_(0)
 {
 }
@@ -180,13 +189,14 @@
 {
     Stop();
     delete iStreamBuffer_;
+    iStreamBuffer_ = NULL;
 }
 
 void CPjAudioInputEngine::ConstructL()
 {
-    iStreamBuffer_ = HBufC8::NewMaxL(parentStrm_->samples_per_frame *
-				     parentStrm_->channel_count * 
-				     BYTES_PER_SAMPLE);
+    iStreamBuffer_ = HBufC8::NewL(parentStrm_->samples_per_frame *
+				  parentStrm_->channel_count * 
+				  BYTES_PER_SAMPLE);
 }
 
 CPjAudioInputEngine *CPjAudioInputEngine::NewLC(pjmedia_snd_stream *parent,
@@ -241,18 +251,6 @@
     pj_assert(iStreamSettings.iChannels != 0 && 
 	      iStreamSettings.iSampleRate != 0);
 
-    // Create timeout timer to wait for Open to complete
-    RTimer timer;
-    TRequestStatus reqStatus;
-    TInt rc;
-    
-    rc = timer.CreateLocal();
-    if (rc != KErrNone) {
-    	delete iInputStream_;
-	iInputStream_ = NULL;
-	return PJ_RETURN_OS_ERROR(rc);
-    }
-
     PJ_LOG(4,(THIS_FILE, "Opening sound device for capture, "
     		         "clock rate=%d, channel count=%d..",
     		         parentStrm_->clock_rate, 
@@ -261,40 +259,7 @@
     // Open stream.
     lastError_ = KRequestPending;
     iInputStream_->Open(&iStreamSettings);
-
-    // Wait until callback is called.
-    if (lastError_ == KRequestPending) {
-	timer.After(reqStatus, 5 * 1000 * 1000);
-
-	do {
-	    User::WaitForAnyRequest();
-	} while (lastError_==KRequestPending && reqStatus==KRequestPending);
-	
-	if (reqStatus==KRequestPending)
-	    timer.Cancel();
-    }
-
-    // Close timer
-    timer.Close();
     
-    // Handle timeout
-    if (lastError_ == KRequestPending) {
-    	iInputStream_->Stop();
-    	delete iInputStream_;
-	iInputStream_ = NULL;
-	return PJ_ETIMEDOUT;
-    }
-    else if (lastError_ != KErrNone) {
-    	// Handle failure.
-	delete iInputStream_;
-	iInputStream_ = NULL;
-	return PJ_RETURN_OS_ERROR(lastError_);
-    }
-
-    // Feed the first frame.
-    iFramePtr_ = iStreamBuffer_->Des();
-    iInputStream_->ReadL(iFramePtr_);
-
     // Success
     PJ_LOG(4,(THIS_FILE, "Sound capture started."));
     return PJ_SUCCESS;
@@ -322,17 +287,44 @@
 }
 
 
+TPtr8 & CPjAudioInputEngine::GetFrame() 
+{
+    TInt l = parentStrm_->samples_per_frame *
+	     parentStrm_->channel_count * 
+	     BYTES_PER_SAMPLE;
+    iStreamBuffer_->Des().FillZ(l);
+    iFramePtr_.Set((TUint8*)(iStreamBuffer_->Ptr()), l, l);
+    return iFramePtr_;
+}
+
 void CPjAudioInputEngine::MaiscOpenComplete(TInt aError)
 {
     lastError_ = aError;
+    if (aError != KErrNone) {
+        snd_perror("Error in MaiscOpenComplete()", aError);
+    	return;
+    }
+
+    // set stream priority to normal and time sensitive
+    iInputStream_->SetPriority(EPriorityNormal, 
+    			       EMdaPriorityPreferenceTime);				
+
+    // Read the first frame.
+    TPtr8 & frm = GetFrame();
+    TRAPD(err2, iInputStream_->ReadL(frm));
+    if (err2) {
+    	PJ_LOG(4,(THIS_FILE, "Exception in iInputStream_->ReadL()"));
+    }
 }
 
 void CPjAudioInputEngine::MaiscBufferCopied(TInt aError, 
 					    const TDesC8 &aBuffer)
 {
     lastError_ = aError;
-    if (aError != KErrNone)
+    if (aError != KErrNone) {
+    	snd_perror("Error in MaiscBufferCopied()", aError);
 	return;
+    }
 
     // Call the callback.
     recCb_(userData_, timeStamp_, (void*)aBuffer.Ptr(), aBuffer.Size());
@@ -341,8 +333,11 @@
     timeStamp_ += (aBuffer.Size() * BYTES_PER_SAMPLE);
 
     // Record next frame
-    iFramePtr_ = iStreamBuffer_->Des();
-    iInputStream_->ReadL(iFramePtr_);
+    TPtr8 & frm = GetFrame();
+    TRAPD(err2, iInputStream_->ReadL(frm));
+    if (err2) {
+    	PJ_LOG(4,(THIS_FILE, "Exception in iInputStream_->ReadL()"));
+    }
 }
 
 
@@ -350,6 +345,9 @@
 {
     lastError_ = aError;
     state_ = STATE_INACTIVE;
+    if (aError != KErrNone) {
+    	snd_perror("Error in MaiscRecordComplete()", aError);
+    }
 }
 
 
@@ -361,7 +359,7 @@
  * Implementation: Symbian Output Stream.
  */
 
-class CPjAudioOutputEngine : public MMdaAudioOutputStreamCallback
+class CPjAudioOutputEngine : public CBase, MMdaAudioOutputStreamCallback
 {
 public:
     enum State
@@ -383,7 +381,7 @@
     pj_status_t StartPlay();
     void Stop();
 
-public:
+private:
     State		     state_;
     pjmedia_snd_stream	    *parentStrm_;
     pjmedia_snd_play_cb	     playCb_;
@@ -391,6 +389,7 @@
     CMdaAudioOutputStream   *iOutputStream_;
     TUint8		    *frameBuf_;
     unsigned		     frameBufSize_;
+    TPtrC8		     frame_;
     TInt		     lastError_;
     unsigned		     timestamp_;
 
@@ -479,22 +478,11 @@
     		         "clock rate=%d, channel count=%d..",
     		         parentStrm_->clock_rate, 
     		         parentStrm_->channel_count));
-    
+
     // Open stream.
     lastError_ = KRequestPending;
     iOutputStream_->Open(&iStreamSettings);
 
-    // Wait until callback is called.
-    while (lastError_ == KRequestPending)
-	pj_thread_sleep(100);
-
-    // Handle failure.
-    if (lastError_ != KErrNone) {
-	delete iOutputStream_;
-	iOutputStream_ = NULL;
-	return PJ_RETURN_OS_ERROR(lastError_);
-    }
-
     // Success
     PJ_LOG(4,(THIS_FILE, "Sound playback started"));
     return PJ_SUCCESS;
@@ -560,9 +548,11 @@
 	// subsequent calls to WriteL() will be issued in 
 	// MMdaAudioOutputStreamCallback::MaoscBufferCopied() 
 	// until whole data buffer is written.
-	TPtrC8 frame(frameBuf_, frameBufSize_);
-	iOutputStream_->WriteL(frame);
-    } 
+	frame_.Set(frameBuf_, frameBufSize_);
+	iOutputStream_->WriteL(frame_);
+    } else {
+    	snd_perror("Error in MaoscOpenComplete()", aError);
+    }
 }
 
 void CPjAudioOutputEngine::MaoscBufferCopied(TInt aError, 
@@ -586,8 +576,8 @@
 	timestamp_ += (frameBufSize_ / BYTES_PER_SAMPLE);
 
 	// Write to playback stream.
-	TPtrC8 frame(frameBuf_, frameBufSize_);
-	iOutputStream_->WriteL(frame);
+	frame_.Set(frameBuf_, frameBufSize_);
+	iOutputStream_->WriteL(frame_);
 
     } else if (aError==KErrAbort) {
 	// playing was aborted, due to call to CMdaAudioOutputStream::Stop()
@@ -596,6 +586,7 @@
 	// error writing data to output
 	lastError_ = aError;
 	state_ = STATE_INACTIVE;
+	snd_perror("Error in MaoscBufferCopied()", aError);
     }
 }
 
@@ -603,6 +594,9 @@
 {
     lastError_ = aError;
     state_ = STATE_INACTIVE;
+    if (aError != KErrNone) {
+    	snd_perror("Error in MaoscPlayComplete()", aError);
+    }
 }
 
 
@@ -655,6 +649,8 @@
     pj_pool_t *pool;
     pjmedia_snd_stream *strm;
 
+    if (index==-1) index = 0;
+    
     PJ_ASSERT_RETURN(index == 0, PJ_EINVAL);
     PJ_ASSERT_RETURN(clock_rate && channel_count && samples_per_frame &&
     		     bits_per_sample && rec_cb && p_snd_strm, PJ_EINVAL);
@@ -671,15 +667,9 @@
     strm->channel_count = channel_count;
     strm->samples_per_frame = samples_per_frame;
 
-    TMdaAudioDataSettings settings;
-    TInt clockRateCap, channelCountCap;
-
-    clockRateCap = get_clock_rate_cap(clock_rate);
-    channelCountCap = get_channel_cap(channel_count);
-
     PJ_ASSERT_RETURN(bits_per_sample == 16, PJ_EINVAL);
-    PJ_ASSERT_RETURN(clockRateCap != 0, PJ_EINVAL);
-    PJ_ASSERT_RETURN(channelCountCap != 0, PJ_EINVAL);
+    PJ_ASSERT_RETURN(get_clock_rate_cap(clock_rate) != 0, PJ_EINVAL);
+    PJ_ASSERT_RETURN(get_channel_cap(channel_count) != 0, PJ_EINVAL);
 
     // Create the input stream.
     TRAPD(err, strm->inEngine = CPjAudioInputEngine::NewL(strm, rec_cb, 
@@ -707,6 +697,8 @@
     pj_pool_t *pool;
     pjmedia_snd_stream *strm;
 
+    if (index == -1) index = 0;
+    
     PJ_ASSERT_RETURN(index == 0, PJ_EINVAL);
     PJ_ASSERT_RETURN(clock_rate && channel_count && samples_per_frame &&
     		     bits_per_sample && play_cb && p_snd_strm, PJ_EINVAL);
@@ -723,15 +715,9 @@
     strm->channel_count = channel_count;
     strm->samples_per_frame = samples_per_frame;
 
-    TMdaAudioDataSettings settings;
-    TInt clockRateCap, channelCountCap;
-
-    clockRateCap = get_clock_rate_cap(clock_rate);
-    channelCountCap = get_channel_cap(channel_count);
-
     PJ_ASSERT_RETURN(bits_per_sample == 16, PJ_EINVAL);
-    PJ_ASSERT_RETURN(clockRateCap != 0, PJ_EINVAL);
-    PJ_ASSERT_RETURN(channelCountCap != 0, PJ_EINVAL);
+    PJ_ASSERT_RETURN(get_clock_rate_cap(clock_rate) != 0, PJ_EINVAL);
+    PJ_ASSERT_RETURN(get_channel_cap(channel_count) != 0, PJ_EINVAL);
 
     // Create the output stream.
     TRAPD(err, strm->outEngine = CPjAudioOutputEngine::NewL(strm, play_cb, 
@@ -760,6 +746,9 @@
     pj_pool_t *pool;
     pjmedia_snd_stream *strm;
 
+    if (rec_id == -1) rec_id = 0;
+    if (play_id == -1) play_id = 0;
+    
     PJ_ASSERT_RETURN(rec_id == 0 && play_id == 0, PJ_EINVAL);
     PJ_ASSERT_RETURN(clock_rate && channel_count && samples_per_frame &&
     		     bits_per_sample && rec_cb && play_cb && p_snd_strm, 
@@ -777,15 +766,9 @@
     strm->channel_count = channel_count;
     strm->samples_per_frame = samples_per_frame;
 
-    TMdaAudioDataSettings settings;
-    TInt clockRateCap, channelCountCap;
-
-    clockRateCap = get_clock_rate_cap(clock_rate);
-    channelCountCap = get_channel_cap(channel_count);
-
     PJ_ASSERT_RETURN(bits_per_sample == 16, PJ_EINVAL);
-    PJ_ASSERT_RETURN(clockRateCap != 0, PJ_EINVAL);
-    PJ_ASSERT_RETURN(channelCountCap != 0, PJ_EINVAL);
+    PJ_ASSERT_RETURN(get_clock_rate_cap(clock_rate) != 0, PJ_EINVAL);
+    PJ_ASSERT_RETURN(get_channel_cap(channel_count) != 0, PJ_EINVAL);
 
     // Create the output stream.
     TRAPD(err, strm->outEngine = CPjAudioOutputEngine::NewL(strm, play_cb, 
@@ -795,6 +778,17 @@
 	return PJ_RETURN_OS_ERROR(err);
     }
 
+    // Create the input stream.
+    TRAPD(err1, strm->inEngine = CPjAudioInputEngine::NewL(strm, rec_cb, 
+    							   user_data));
+    if (err1 != KErrNone) {
+        strm->inEngine = NULL;
+        delete strm->outEngine;
+    	strm->outEngine = NULL;
+    	pj_pool_release(pool);	
+	return PJ_RETURN_OS_ERROR(err1);
+    }
+
     // Done.
     *p_snd_strm = strm;
     return PJ_SUCCESS;
@@ -807,18 +801,18 @@
     
     PJ_ASSERT_RETURN(stream != NULL, PJ_EINVAL);
     
-    if (stream->inEngine) {
-    	status = stream->inEngine->StartRecord();
-    	if (status != PJ_SUCCESS)
-    	    return status;
-    }
-    	
     if (stream->outEngine) {
     	status = stream->outEngine->StartPlay();
     	if (status != PJ_SUCCESS)
     	    return status;
     }
     
+    if (stream->inEngine) {
+    	status = stream->inEngine->StartRecord();
+    	if (status != PJ_SUCCESS)
+    	    return status;
+    }
+    	
     return PJ_SUCCESS;
 }
 
diff --git a/pjsip-apps/src/symsndtest/app_main.cpp b/pjsip-apps/src/symsndtest/app_main.cpp
new file mode 100644
index 0000000..dcd6ed8
--- /dev/null
+++ b/pjsip-apps/src/symsndtest/app_main.cpp
@@ -0,0 +1,338 @@
+/* $Id$ */
+/* 
+ * Copyright (C) 2003-2007 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 <pj/errno.h>
+#include <pj/os.h>
+#include <pj/log.h>
+#include <pj/string.h>
+#include <pj/unicode.h>
+#include <e32cons.h>
+
+#define THIS_FILE		"app_main.cpp"
+#define CLOCK_RATE		8000
+#define CHANNEL_COUNT		1
+#define PTIME			100
+#define SAMPLES_PER_FRAME	(2048)
+#define BITS_PER_SAMPLE		16
+
+
+extern CConsoleBase* console;
+
+static pj_caching_pool cp;
+static pjmedia_snd_stream *strm;
+static unsigned rec_cnt, play_cnt;
+static pj_time_val t_start;
+
+
+/* Logging callback */
+static void log_writer(int level, const char *buf, unsigned len)
+{
+    static wchar_t buf16[PJ_LOG_MAX_SIZE];
+
+    PJ_UNUSED_ARG(level);
+    
+    pj_ansi_to_unicode(buf, len, buf16, PJ_ARRAY_SIZE(buf16));
+
+    TPtrC16 aBuf((const TUint16*)buf16, (TInt)len);
+    console->Write(aBuf);
+}
+
+/* perror util */
+static void app_perror(const char *title, pj_status_t status) 
+{
+    char errmsg[PJ_ERR_MSG_SIZE];	
+    pj_strerror(status, errmsg, sizeof(errmsg));
+    PJ_LOG(1,(THIS_FILE, "Error: %s: %s", title, errmsg));
+}
+
+/* Application init */
+static pj_status_t app_init() 
+{
+    unsigned i, count;
+    pj_status_t status;
+   
+    /* Redirect log */
+    pj_log_set_log_func((void (*)(int,const char*,int)) &log_writer);
+    pj_log_set_decor(PJ_LOG_HAS_NEWLINE);
+    
+    /* Init pjlib */
+    status = pj_init();
+    if (status != PJ_SUCCESS) {
+    	app_perror("pj_init()", status);
+    	return status;
+    }
+
+    pj_caching_pool_init(&cp, NULL, 0);
+    
+    /* Init sound subsystem */
+    status = pjmedia_snd_init(&cp.factory);
+    if (status != PJ_SUCCESS) {
+    	app_perror("pjmedia_snd_init()", status);
+        pj_caching_pool_destroy(&cp);
+    	pj_shutdown();
+    	return status;
+    }
+    
+    count = pjmedia_snd_get_dev_count();
+    PJ_LOG(3,(THIS_FILE, "Device count: %d", count));
+    for (i=0; i<count; ++i) {
+    	const pjmedia_snd_dev_info *info;
+    	
+    	info = pjmedia_snd_get_dev_info(i);
+    	PJ_LOG(3, (THIS_FILE, "%d: %s %d/%d %dHz",
+    		   i, info->name, info->input_count, info->output_count,
+    		   info->default_samples_per_sec));	
+    }
+    
+    return PJ_SUCCESS;
+}
+
+
+/* Sound capture callback */
+static pj_status_t rec_cb(void *user_data, 
+			  pj_uint32_t timestamp,
+			  void *input,
+			  unsigned size) 
+{
+    PJ_UNUSED_ARG(user_data);
+    PJ_UNUSED_ARG(timestamp);
+    PJ_UNUSED_ARG(input);
+    PJ_UNUSED_ARG(size);
+    
+    ++rec_cnt;
+    return PJ_SUCCESS;
+}
+
+/* Play cb */
+static pj_status_t play_cb(void *user_data,
+			   pj_uint32_t timestamp,
+			   void *output,
+			   unsigned size) 
+{
+    PJ_UNUSED_ARG(user_data);
+    PJ_UNUSED_ARG(timestamp);
+    
+    pj_bzero(output, size);
+    
+    ++play_cnt;
+    return PJ_SUCCESS;	
+}
+
+/* Start sound */
+static pj_status_t snd_start(unsigned flag) 
+{
+    pj_status_t status;
+    
+    if (strm != NULL) {
+    	app_perror("snd already open", PJ_EINVALIDOP);
+    	return PJ_EINVALIDOP;
+    }
+    
+    if (flag==PJMEDIA_DIR_CAPTURE_PLAYBACK)
+    	status = pjmedia_snd_open(-1, -1, CLOCK_RATE, CHANNEL_COUNT,
+    				  SAMPLES_PER_FRAME, BITS_PER_SAMPLE,
+    				  &rec_cb, &play_cb, NULL, &strm);
+    else if (flag==PJMEDIA_DIR_CAPTURE)
+    	status = pjmedia_snd_open_rec(-1, CLOCK_RATE, CHANNEL_COUNT,
+    				      SAMPLES_PER_FRAME, BITS_PER_SAMPLE,
+    				      &rec_cb, NULL, &strm);
+    else
+    	status = pjmedia_snd_open_player(-1, CLOCK_RATE, CHANNEL_COUNT,
+    					 SAMPLES_PER_FRAME, BITS_PER_SAMPLE,
+    					 &play_cb, NULL, &strm);
+			 
+    if (status != PJ_SUCCESS) {
+    	app_perror("snd open", status);
+    	return status;
+    }
+
+    rec_cnt = play_cnt = 0;
+    pj_gettimeofday(&t_start);
+
+    status = pjmedia_snd_stream_start(strm);
+    if (status != PJ_SUCCESS) {
+    	app_perror("snd start", status);
+    	pjmedia_snd_stream_close(strm);
+    	strm = NULL;
+    	return status;
+    }
+
+    return PJ_SUCCESS;
+}
+
+/* Stop sound */
+static pj_status_t snd_stop() 
+{
+    pj_time_val now;
+    pj_status_t status;
+    
+    if (strm == NULL) {
+    	app_perror("snd not open", PJ_EINVALIDOP);
+    	return PJ_EINVALIDOP;
+    }
+    
+    status = pjmedia_snd_stream_close(strm);
+    strm = NULL;
+    
+    pj_gettimeofday(&now);
+    PJ_TIME_VAL_SUB(now, t_start);
+
+    PJ_LOG(3,(THIS_FILE, "Duration: %d.%03d", now.sec, now.msec));
+    PJ_LOG(3,(THIS_FILE, "Captured: %d", rec_cnt));
+    PJ_LOG(3,(THIS_FILE, "Played: %d", play_cnt));
+
+    return status;
+}
+
+/* Shutdown application */
+static void app_fini() 
+{
+    if (strm)
+    	snd_stop();
+    
+    pjmedia_snd_deinit();
+    pj_caching_pool_destroy(&cp);
+    pj_shutdown();
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+/*
+ * The interractive console UI
+ */
+#include <e32base.h>
+
+class ConsoleUI : public CActive 
+{
+public:
+    ConsoleUI(CActiveSchedulerWait *asw, CConsoleBase *con);
+
+    // Run console UI
+    void Run();
+
+    // Stop
+    void Stop();
+    
+protected:
+    // Cancel asynchronous read.
+    void DoCancel();
+
+    // Implementation: called when read has completed.
+    void RunL();
+    
+private:
+    CActiveSchedulerWait *asw_;
+    CConsoleBase *con_;
+};
+
+
+ConsoleUI::ConsoleUI(CActiveSchedulerWait *asw, CConsoleBase *con) 
+: CActive(EPriorityHigh), asw_(asw), con_(con)
+{
+    CActiveScheduler::Add(this);
+}
+
+// Run console UI
+void ConsoleUI::Run() 
+{
+    con_->Read(iStatus);
+    SetActive();
+}
+
+// Stop console UI
+void ConsoleUI::Stop() 
+{
+    DoCancel();
+}
+
+// Cancel asynchronous read.
+void ConsoleUI::DoCancel() 
+{
+    con_->ReadCancel();
+}
+
+static void PrintMenu() 
+{
+    PJ_LOG(3, (THIS_FILE, "\n\n"
+	    "Menu:\n"
+	    "  b    Start bidir sound\n"
+	    "  r    Start recorder\n"
+	    "  p    Start player\n"
+	    "  c    Stop & close sound\n"
+	    "  q    Quit\n"));
+}
+
+// Implementation: called when read has completed.
+void ConsoleUI::RunL() 
+{
+    TKeyCode kc = con_->KeyCode();
+    pj_bool_t reschedule = PJ_TRUE;
+    
+    switch (kc) {
+    case 'q':
+	    asw_->AsyncStop();
+	    reschedule = PJ_FALSE;
+	    break;
+    case 'b':
+    	snd_start(PJMEDIA_DIR_CAPTURE_PLAYBACK);
+	break;
+    case 'r':
+    	snd_start(PJMEDIA_DIR_CAPTURE);
+	break;
+    case 'p':
+    	snd_start(PJMEDIA_DIR_PLAYBACK);
+	break;
+    case 'c':
+    	snd_stop();
+	break;
+    default:
+	    PJ_LOG(3,(THIS_FILE, "Keycode '%c' (%d) is pressed",
+		      kc, kc));
+	    break;
+    }
+
+    PrintMenu();
+    
+    if (reschedule)
+	Run();
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+int app_main() 
+{
+    if (app_init() != PJ_SUCCESS)
+        return -1;
+    
+    // Run the UI
+    CActiveSchedulerWait *asw = new CActiveSchedulerWait;
+    ConsoleUI *con = new ConsoleUI(asw, console);
+    
+    con->Run();
+    
+    PrintMenu();
+    asw->Start();
+    
+    delete con;
+    delete asw;
+    
+    app_fini();
+    return 0;
+}
+
diff --git a/pjsip-apps/src/symsndtest/main_symbian.cpp b/pjsip-apps/src/symsndtest/main_symbian.cpp
new file mode 100644
index 0000000..8c4d802
--- /dev/null
+++ b/pjsip-apps/src/symsndtest/main_symbian.cpp
@@ -0,0 +1,153 @@
+/* $Id$ */
+/* 
+ * Copyright (C) 2003-2007 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 <e32std.h>
+#include <e32base.h>
+#include <e32std.h>
+#include <e32cons.h>
+#include <stdlib.h>
+
+
+//  Global Variables
+CConsoleBase* console;
+
+int app_main();
+
+
+////////////////////////////////////////////////////////////////////////////
+class MyTask : public CActive
+{
+public:
+    static MyTask *NewL(CActiveSchedulerWait *asw);
+    ~MyTask();
+    void Start();
+
+protected:
+    MyTask(CActiveSchedulerWait *asw);
+    void ConstructL();
+    virtual void RunL();
+    virtual void DoCancel();
+
+private:
+    RTimer timer_;
+    CActiveSchedulerWait *asw_;
+};
+
+MyTask::MyTask(CActiveSchedulerWait *asw)
+: CActive(EPriorityNormal), asw_(asw)
+{
+}
+
+MyTask::~MyTask() 
+{
+    timer_.Close();
+}
+
+void MyTask::ConstructL()
+{
+    timer_.CreateLocal();
+    CActiveScheduler::Add(this);
+}
+
+MyTask *MyTask::NewL(CActiveSchedulerWait *asw)
+{
+    MyTask *self = new (ELeave) MyTask(asw);
+    CleanupStack::PushL(self);
+
+    self->ConstructL();
+
+    CleanupStack::Pop(self);
+    return self;
+}
+
+void MyTask::Start()
+{
+    timer_.After(iStatus, 0);
+    SetActive();
+}
+
+void MyTask::RunL()
+{
+    int rc = app_main();
+    asw_->AsyncStop();
+}
+
+void MyTask::DoCancel()
+{
+
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+LOCAL_C void DoStartL()
+{
+    CActiveScheduler *scheduler = new (ELeave) CActiveScheduler;
+    CleanupStack::PushL(scheduler);
+    CActiveScheduler::Install(scheduler);
+
+    CActiveSchedulerWait *asw = new CActiveSchedulerWait;
+    CleanupStack::PushL(asw);
+    
+    MyTask *task = MyTask::NewL(asw);
+    task->Start();
+
+    asw->Start();
+    
+    delete task;
+    
+    CleanupStack::Pop(asw);
+    delete asw;
+    
+    CActiveScheduler::Install(NULL);
+    CleanupStack::Pop(scheduler);
+    delete scheduler;
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+
+// E32Main()
+GLDEF_C TInt E32Main()
+{
+    // Mark heap usage
+    __UHEAP_MARK;
+
+    // Create cleanup stack
+    CTrapCleanup* cleanup = CTrapCleanup::New();
+
+    // Create output console
+    TRAPD(createError, console = Console::NewL(_L("Console"), TSize(KConsFullScreen,KConsFullScreen)));
+    if (createError)
+        return createError;
+
+    TRAPD(startError, DoStartL());
+
+    console->Printf(_L("[press any key to close]\n"));
+    console->Getch();
+    
+    delete console;
+    delete cleanup;
+
+    CloseSTDLIB(); 
+
+    // Mark end of heap usage, detect memory leaks
+    __UHEAP_MARKEND;
+    return KErrNone;
+}
+