/* Copyright (C) 2015 J.F.Dockes
* 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 "songcastsender.h"
extern bool g_quitrequest;
// Decide if using a busyloop on blocking sources (rather than the timer)
static const bool optionbusy = true;
SongcastSender::SongcastSender(Environment& aEnv, OhmSender* aSender,
OhmSenderDriver* aDriver, const Brx& aUri,
AudioReader* audio, const string& codec,
bool paced)
: iEnv(aEnv)
, iSender(aSender)
, iDriver(aDriver)
, iUri(aUri)
, m_audio(audio)
, iTimer(aEnv, MakeFunctor(*this, &SongcastSender::TimerExpired), "SongcastSender")
, iMutex("SCSP")
, iPaused(true)
, iSpeed(kSpeedNormal)
, iIndex(0)
, iLastTimeUs(0)
, iTimeOffsetUs(0)
, iVerbose(false)
, iPaced(paced)
{
CalculatePacketBytes();
m_encoder = AudioEncoderFactory::CreateAudioEncoder(codec, audio, aDriver,
iPacketBytes);
LOGDEB("bytes per packet: " << iPacketBytes << endl);
LOGDEB("frames per packet: " << iPacketFrames << endl);
LOGDEB("usec per packet: "<< iPacketTime << endl);
}
// We return true if the main thread should pause.
bool SongcastSender::Start(TBool aEnabled)
{
iSender->SetEnabled(true);
iSender->SetTrack(iUri, Brx::Empty(), m_audio->sampleCount(), 0);
iSender->SetMetatext(Brn("SongcastSender repeated play"));
// It seems that both hijacking the main thread and using the
// timer with a short timeout (see TimerExpired()) work.
// Don't know what's best. The timer approach is closer to the original
// code and leaves the main thread free for control ops if needed.
// Otoh, if no data appears on the fifo, the timer thread is stuck in read
// and the upnp side stops working (no sender advertised).
// Maybe the best approach would be to start a separate thread and
// use busyreading. Using the main thread for now.
LOGDEB("SongcastSender::Start: Enabled " << aEnabled << " iPaused " <<
iPaused << endl);
if (aEnabled)
Play();
if (iPaced || !optionbusy) {
LOGDEB("SongcastSender::Start: using timers\n");
return true;
} else {
LOGDEB("SongcastSender::Start: using read-blocking loop\n");
busyRdWr();
return false;
}
}
void SongcastSender::Play()
{
iMutex.Wait();
if (iPaused) {
m_encoder->start();
if (m_audio)
m_audio->open();
iPaused = false;
if (iPaced || !optionbusy) {
iLastTimeUs = 0;
iTimeOffsetUs = 0;
iTimer.FireIn(kPeriodMs);
}
}
iMutex.Signal();
}
void SongcastSender::Stop()
{
iMutex.Wait();
if (!iPaused) {
m_encoder->finish();
if (m_audio)
m_audio->close();
iPaused = true;
}
iMutex.Signal();
}
void SongcastSender::PlayPause()
{
if (iPaused)
Play();
else
Stop();
}
void SongcastSender::Restart()
{
iMutex.Wait();
iIndex = 0;
iMutex.Signal();
}
TBool SongcastSender::Paused()
{
return (iPaused);
}
const string SongcastSender::CodecName()
{
return m_encoder->name();
}
void SongcastSender::CalculatePacketBytes()
{
// in order to let wavsender change the playback rate,
// we keep constant it's idea of how much audio time is in each packet,
// but vary the amount of data that is actually sent
// calculate the amount of time in each packet
TUint norm_bytes = (m_audio->sampleRate() * m_audio->bytesPerFrame() *
kPeriodMs) / 1000;
if (norm_bytes > kMaxPacketBytes) {
norm_bytes = kMaxPacketBytes;
}
TUint norm_packet_samples = norm_bytes / m_audio->bytesPerFrame();
iPacketTime = (norm_packet_samples*1000000/(m_audio->sampleRate()/10) + 5)/10;
// calculate the adjusted speed packet size
TUint bytes = (norm_bytes * iSpeed) / 100;
if (bytes > kMaxPacketBytes) {
bytes = kMaxPacketBytes;
}
iPacketFrames = bytes / m_audio->bytesPerFrame();
iPacketBytes = iPacketFrames * m_audio->bytesPerFrame();
}
void SongcastSender::busyRdWr()
{
LOGDEB("SongcastSender:busyRdWr: packetbytes " << iPacketBytes <<
" iPaused " << iPaused << endl);
while (true) {
if (g_quitrequest) {
return;
}
ssize_t nread = 0;
const unsigned char *cp = m_audio->data((size_t)iPacketBytes, nread);
if (cp == 0) {
return;
}
if (g_quitrequest) {
return;
}
m_encoder->encode(cp, nread, iPaused);
}
}
void SongcastSender::TimerExpired()
{
iMutex.Wait();
bool pause = iPaused || g_quitrequest;
ssize_t nread = 0;
const unsigned char *cp = m_audio->data((size_t)iPacketBytes, nread);
if (nread > 0) {
if ((size_t)nread < iPacketBytes) {
LOGDEB("SongcastSender::TimerExpired: requested " << iPacketBytes
<< " bytes, read " << nread << " bytes" << endl);
}
m_encoder->encode(cp, nread, pause);
} else if (pause) {
// The audio stream was paused and no data could be read from the
// audio source anymore. To notify the receivers, send an empty audio
// message with the halt flag set.
LOGDEB("SongcastSender::Send empty audio message with pause flag set\n");
m_encoder->encode(cp, 0, true);
}
if (!pause) {
TUint64 now = OsTimeInUs(iEnv.OsCtx());
if (!iPaced) {
// Means we're doing blocking reads on the source, and
// it's setting the pace. I'd like to actually use 0 here
// (ala qt processEvents()), but this appears to busyloop
// and not let the sender do its thing. Anyway, as long
// as we can read from the fifo in much less than (period-2),
// which should always be true, we should be ok.
// I can see not much difference between doing this or
// hijacking the main thread for busy read/write
iTimer.FireIn(2);
} else {
// skip the first packet, and any time the clock value wraps
if (iLastTimeUs && iLastTimeUs < now) {
// will contain the new time out in ms
TUint new_timer_ms = kPeriodMs;
// the difference in usec from where we should be
TInt32 diff = (TInt32)(now - iLastTimeUs) - iPacketTime;
// increment running offset
iTimeOffsetUs -= diff;
// determine new timer value based upon current offset from ideal
if (iTimeOffsetUs < -1000) {
// we are late
TInt32 time_offset_ms = iTimeOffsetUs/1000;
if (time_offset_ms < 1-(TInt32)kPeriodMs) {
// in case callback is severely late, we can only
// catch up so much
new_timer_ms = 1;
} else {
new_timer_ms = kPeriodMs + time_offset_ms;
}
} else if (iTimeOffsetUs > 1000) {
// we are early
new_timer_ms = kPeriodMs+1;
} else {
// we are about on time
new_timer_ms = kPeriodMs;
}
// set timer
iTimer.FireIn(new_timer_ms);
// logging
if (iVerbose) {
if (iTimeOffsetUs >= 1000)
printf ("tnow:%d tlast:%d actual:%4d diff:%4d offset:%5d timer:%d\n", (TUint)now, (TUint)iLastTimeUs, (TUint)(now-iLastTimeUs), diff, iTimeOffsetUs, new_timer_ms);
else
printf ("tnow:%d tlast:%d actual:%4d diff:%4d offset:%4d timer:%d\n", (TUint)now, (TUint)iLastTimeUs, (TUint)(now-iLastTimeUs), diff, iTimeOffsetUs, new_timer_ms);
}
} else {
iTimer.FireIn(kPeriodMs);
}
iLastTimeUs = now;
}
} else {
LOGDEB("SongcastSender::TimerExpired: Sender is paused. Stop firing\n");
m_encoder->finish();
if (m_audio)
m_audio->close();
}
iMutex.Signal();
}
SongcastSender::~SongcastSender()
{
iTimer.Cancel();
delete (iSender);
delete (iDriver);
}