/** * alsachan.cpp * This file is part of the YATE Project http://YATE.null.ro * * Alsa driver * * Copyright (C) 2005 Pablo Sampere * Derived from osschan.cpp Copyright (C) Null Team * * 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #if defined(__linux__) #include #elif defined (__FreeBSD__) #include #else #include #endif #define ALSA_PCM_NEW_HW_PARAMS_API #include #define MIN_SWITCH_TIME 600000 using namespace TelEngine; namespace { // anonymous static Mutex s_mutex; class AlsaDevice; class AlsaSource : public ThreadedSource { public: AlsaSource(AlsaDevice* dev); ~AlsaSource(); bool init(); virtual void run(); virtual void cleanup(); private: AlsaDevice* m_device; unsigned m_brate; unsigned m_total; }; class AlsaConsumer : public DataConsumer { public: AlsaConsumer(AlsaDevice* dev); ~AlsaConsumer(); bool init(); virtual void Consume(const DataBlock &data, unsigned long tStamp); private: AlsaDevice* m_device; unsigned m_total; }; class AlsaDevice : public RefObject { public: AlsaDevice(const String& dev, bool init=false); ~AlsaDevice(); bool timePassed(void); bool open(); int write(void *buffer, int frames); int read(void *buffer, int frames); inline bool closed() const { return m_closed; } int m_fd; private: String m_dev; String m_dev_in; String m_dev_out; String m_initdata; bool m_closed; snd_pcm_t *m_handle_in; snd_pcm_t *m_handle_out; u_int64_t m_lastTime; }; class AlsaChan : public CallEndpoint { public: AlsaChan(const String& dev); ~AlsaChan(); bool init(); virtual void disconnected(bool final, const char *reason); void answer(); inline void setTarget(const char* target = 0) { m_target = target; } inline const String& getTarget() const { return m_target; } private: String m_dev; String m_target; }; class AlsaHandler : public MessageHandler { public: AlsaHandler(const char *name) : MessageHandler(name) { } virtual bool received(Message &msg); }; class StatusHandler : public MessageHandler { public: StatusHandler() : MessageHandler("engine.status") { } virtual bool received(Message &msg); }; class DropHandler : public MessageHandler { public: DropHandler(const char *name) : MessageHandler(name) { } virtual bool received(Message &msg); }; class MasqHandler : public MessageHandler { public: MasqHandler(const char *name, int prio) : MessageHandler(name,prio) { } virtual bool received(Message &msg); }; class AttachHandler : public MessageHandler { public: AttachHandler() : MessageHandler("chan.attach") { } virtual bool received(Message &msg); }; class AlsaPlugin : public Plugin { public: AlsaPlugin(); virtual void initialize(); virtual bool isBusy() const; private: AlsaHandler *m_handler; }; AlsaChan *s_chan = 0; AlsaDevice* s_dev = 0; bool AlsaSource::init() { m_brate = 16000; m_total = 0; start("AlsaSource",Thread::High); return true; } AlsaSource::AlsaSource(AlsaDevice* dev) : m_device(0) { Debug(DebugNote,"AlsaSource::AlsaSource(%p) [%p]",dev,this); dev->ref(); m_device = dev; } AlsaSource::~AlsaSource() { Debug(DebugNote,"AlsaSource::~AlsaSource() [%p] total=%u",this,m_total); m_device->deref(); } void AlsaSource::run() { int r = 0; //u_int64_t tpos = Time::now(); DataBlock data(0,(m_brate*20)/1000); do { if (m_device->closed()) { m_device->open(); Thread::yield(); r = 1; continue; } r = m_device->read(data.data(), data.length()/2); if (r <= 0) { Thread::yield(); r = 1; continue; } if(((unsigned int)r)*2==data.length()) Forward(data); else{ DataBlock d2(data.data(),r*2); Forward(d2); } /* int64_t dly = tpos - Time::now(); if (dly > 0) { Debug("AlsaSource",DebugWarn,"Sleeping for " FMT64 " usec",dly); Thread::usleep((unsigned long)dly); } */ m_total += r; //tpos += (r*(u_int64_t)1000000/m_brate); } while (r > 0); Debug(DebugWarn,"AlsaSource [%p] end of data",this); } void AlsaSource::cleanup() { Debug(DebugNote,"AlsaSource [%p] cleanup, total=%u",this,m_total); } bool AlsaConsumer::init() { m_total = 0; return true; } AlsaConsumer::AlsaConsumer(AlsaDevice* dev) : m_device(0) { Debug(DebugNote,"AlsaConsumer::AlsaConsumer(%p) [%p]",dev,this); dev->ref(); m_device = dev; } AlsaConsumer::~AlsaConsumer() { Debug(DebugNote,"AlsaConsumer::~AlsaConsumer() [%p] total=%u",this,m_total); m_device->deref(); } void AlsaConsumer::Consume(const DataBlock &data, unsigned long tStamp) { if (m_device->closed() || data.null()) return; m_device->write(data.data(),data.length()/2); m_total += data.length(); } AlsaChan::AlsaChan(const String& dev) : CallEndpoint("alsa"), m_dev(dev) { Debug(DebugNote,"AlsaChan::AlsaChan dev [%s] [%p]",dev.c_str(),this); s_chan = this; } AlsaChan::~AlsaChan() { Debug(DebugNote,"AlsaChan::~AlsaChan() [%p]",this); setTarget(); setSource(); setConsumer(); s_chan = 0; } bool AlsaChan::init() { if (s_dev) return false; AlsaDevice* dev = new AlsaDevice(m_dev,true); if (dev->closed()) { dev->deref(); return false; } AlsaSource* source = new AlsaSource(dev); dev->deref(); if (!source->init()) { source->deref(); return false; } setSource(source); source->deref(); AlsaConsumer* cons = new AlsaConsumer(dev); if (!cons->init()) { cons->deref(); setSource(); return false; } setConsumer(cons); cons->deref(); return true; } AlsaDevice::AlsaDevice(const String& dev,bool init) : m_dev(dev), m_dev_in(dev), m_dev_out(dev), m_closed(true), m_handle_in(0), m_handle_out(0) { Debug(DebugNote,"AlsaDevice::AlsaDevice('%s') [%p]",m_dev.c_str(),this); if (!s_dev) s_dev = this; int p = dev.find('/'); if (p>0) { m_dev_in = dev.substr(0,p); int q = dev.substr(p+1).find('/'); m_dev_out = dev.substr(p+1,q); if (m_dev_out.null()) m_dev_out = m_dev_in; if(q>0) m_initdata = dev.substr(p+2+q); } if (init) open(); }; bool AlsaDevice::open() { int err; snd_pcm_hw_params_t *hw_params = NULL; unsigned int rate_in=8000; unsigned int rate_out=8000; int direction=0; snd_pcm_uframes_t period_size_in = 20 * 4; snd_pcm_uframes_t buffer_size_in= period_size_in * 16; snd_pcm_uframes_t period_size_out = 20 * 4; snd_pcm_uframes_t buffer_size_out= period_size_out * 16; snd_pcm_sw_params_t *swparams = NULL; Debug(DebugNote, "Opening ALSA input device\n"); Lock lock(s_mutex); if ((err = snd_pcm_open (&m_handle_in, m_dev_in.c_str(), SND_PCM_STREAM_CAPTURE, 0)) < 0) { Debug(DebugWarn, "cannot open audio device %s (%s)\n", m_dev.c_str(),snd_strerror (err)); return false; } if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) { Debug(DebugWarn, "cannot allocate hardware parameter structure (%s)\n", snd_strerror (err)); return false; } if ((err = snd_pcm_hw_params_any (m_handle_in, hw_params)) < 0) Debug(DebugWarn, "cannot initialize hardware parameter structure (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_access (m_handle_in, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) Debug(DebugWarn, "cannot set access type (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_format (m_handle_in, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) Debug(DebugWarn, "cannot set sample format (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_rate_near (m_handle_in, hw_params, &rate_in, &direction)) < 0) Debug(DebugWarn, "cannot set sample rate (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_channels (m_handle_in, hw_params, 1)) < 0) Debug(DebugWarn, "cannot set channel count (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params (m_handle_in, hw_params)) < 0) Debug(DebugWarn, "cannot set parameters (%s)\n", snd_strerror (err)); snd_pcm_hw_params_free (hw_params); Debug(DebugNote, "Opening ALSA output device\n"); if ((err = snd_pcm_open (&m_handle_out, m_dev_out.c_str(), SND_PCM_STREAM_PLAYBACK, 0)) < 0) { Debug(DebugWarn, "cannot open audio device %s (%s)\n", m_dev.c_str(), snd_strerror (err)); return false; } if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) { Debug(DebugWarn, "cannot allocate hardware parameter structure (%s)\n", snd_strerror (err)); return false; } if ((err = snd_pcm_hw_params_any (m_handle_out, hw_params)) < 0) Debug(DebugWarn, "cannot initialize hardware parameter structure (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_access (m_handle_out, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) Debug(DebugWarn, "cannot set access type (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_format (m_handle_out, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) Debug(DebugWarn, "cannot set sample format (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_rate_near (m_handle_out, hw_params, &rate_out, 0)) < 0) Debug(DebugWarn, "cannot set sample rate (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_channels (m_handle_out, hw_params, 1)) < 0) Debug(DebugWarn, "cannot set channel count (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_period_size_near(m_handle_out, hw_params, &period_size_out, &direction)) < 0) Debug(DebugWarn, "cannot set period size (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params_set_buffer_size_near(m_handle_out, hw_params, &buffer_size_out)) < 0) Debug(DebugWarn, "cannot set buffer size (%s)\n", snd_strerror (err)); if ((err = snd_pcm_hw_params (m_handle_out, hw_params)) < 0) Debug(DebugWarn, "cannot set parameters (%s)\n", snd_strerror (err)); snd_pcm_hw_params_free (hw_params); snd_pcm_uframes_t val; snd_pcm_sw_params_alloca(&swparams); snd_pcm_sw_params_current(m_handle_out, swparams); if ((err = snd_pcm_sw_params_get_start_threshold( swparams, &val)) < 0) Debug(DebugWarn, "cannot get start threshold: (%s)\n", snd_strerror (err)); if ((err = snd_pcm_sw_params_get_stop_threshold( swparams, &val)) < 0) Debug(DebugWarn, "cannot get stop threshold: (%s)\n", snd_strerror (err)); if ((err = snd_pcm_sw_params_get_boundary( swparams, &val)) < 0) Debug(DebugWarn, "cannot get boundary: (%s)\n", snd_strerror (err)); if ((err = snd_pcm_sw_params_set_silence_threshold(m_handle_out, swparams, 0)) < 0) Debug(DebugWarn, "cannot set silence threshold: (%s)\n", snd_strerror (err)); if ((err = snd_pcm_sw_params_set_silence_size(m_handle_out, swparams, 0)) < 0) Debug(DebugWarn, "cannot set silence size: (%s)\n", snd_strerror (err)); if ((err = snd_pcm_sw_params(m_handle_out, swparams)) < 0) Debug(DebugWarn, "cannot set sw param: (%s)\n", snd_strerror (err)); Debug(DebugNote, "Alsa(%s/%s) %u/%u %u/%u %u/%u\n", m_dev_in.c_str(),m_dev_out.c_str(),rate_in,rate_out,(unsigned int)period_size_in,(unsigned int)period_size_out,(unsigned int)buffer_size_in,(unsigned int)buffer_size_out); m_closed = false; m_lastTime = Time::now() + MIN_SWITCH_TIME; return true; } AlsaDevice::~AlsaDevice() { Debug(DebugNote,"AlsaDevice::~AlsaDevice [%p]",this); m_closed = true; if (m_handle_in) { snd_pcm_drop (m_handle_in); snd_pcm_close (m_handle_in); m_handle_in = 0; } if (m_handle_out) { snd_pcm_drop (m_handle_out); snd_pcm_close (m_handle_out); m_handle_out = 0; } if (s_dev == this) s_dev = 0; } int AlsaDevice::read(void *buffer, int frames) { if (closed() || !m_handle_in) return 0; int rc = ::snd_pcm_readi(m_handle_in, buffer, frames); if (rc <= 0) { int err = rc; if (err == -EPIPE) { /* under-run */ Debug(DebugWarn, "ALSA read underrun: %s\n", snd_strerror(err)); err = snd_pcm_prepare(m_handle_in); if (err < 0) Debug(DebugWarn, "ALSA read can't recover from underrun, prepare failed: %s\n", snd_strerror(err)); return 0; } else if (err == -ESTRPIPE) { while ((err = snd_pcm_resume(m_handle_in)) == -EAGAIN) sleep(1); /* wait until the suspend flag is released */ if (err < 0) { err = snd_pcm_prepare(m_handle_in); if (err < 0) Debug(DebugWarn, "ALSA read can't recover from suspend, prepare failed: %s\n", snd_strerror(err)); } return 0; } return err; } return rc; } int AlsaDevice::write(void *buffer, int frames) { if (closed() || !m_handle_out) return 0; int rc = snd_pcm_writei(m_handle_out, buffer, frames); if (rc == -EPIPE) { Debug(DebugWarn, "ALSA write underrun occurred\n"); snd_pcm_prepare(m_handle_out); Debug(DebugWarn, "ALSA write underrun fix frame 1\n"); rc = snd_pcm_writei(m_handle_out, buffer, frames); if (rc == -EPIPE) snd_pcm_prepare(m_handle_out); Debug(DebugWarn, "ALSA write underrun fix frame 2\n"); //to catch-up missed time rc = snd_pcm_writei(m_handle_out, buffer, frames); if (rc == -EPIPE) snd_pcm_prepare(m_handle_out); } else if (rc < 0) { Debug(DebugWarn,"ALSA error from writei: %s\n",::snd_strerror(rc)); } else if (rc != (int)frames) { Debug(DebugWarn,"ALSA short write, writei wrote %d frames\n", rc); } return rc; } bool AlsaDevice::timePassed(void) { return Time::now() > m_lastTime; } void AlsaChan::disconnected(bool final, const char *reason) { Debugger debug("AlsaChan::disconnected()"," '%s' [%p]",reason,this); setTarget(); } void AlsaChan::answer() { Message* m = new Message("call.answered"); m->addParam("module","alsa"); String tmp("alsa/"); tmp += m_dev; m->addParam("id",tmp); if (m_target) m->addParam("targetid",m_target); Engine::enqueue(m); } bool AlsaHandler::received(Message &msg) { String dest(msg.getValue("callto")); if (dest.null()) return false; Regexp r("^alsa/\\(.*\\)$"); if (!dest.matches(r)) return false; if (s_chan) { msg.setParam("error","busy"); return false; } AlsaChan *chan = new AlsaChan(dest.matchString(1).c_str()); if (!chan->init()) { chan->destruct(); return false; } CallEndpoint* ch = static_cast(msg.userData()); Debug(DebugInfo,"We are routing to device '%s'",dest.matchString(1).c_str()); if (ch && chan->connect(ch,msg.getValue("reason"))) { chan->setTarget(msg.getValue("id")); msg.setParam("peerid",dest); msg.setParam("targetid",dest); chan->answer(); chan->deref(); } else { const char *direct = msg.getValue("direct"); if (direct) { Message m("call.execute"); m.addParam("module","alsa"); m.addParam("id",dest); m.addParam("caller",dest); m.addParam("callto",direct); m.userData(chan); if (Engine::dispatch(m)) { chan->setTarget(m.getValue("targetid")); msg.addParam("targetid",chan->getTarget()); chan->deref(); return true; } Debug(DebugInfo,"Alsa outgoing call not accepted!"); chan->destruct(); return false; } const char *targ = msg.getValue("target"); if (!targ) { Debug(DebugWarn,"Alsa outgoing call with no target!"); chan->destruct(); return false; } Message m("call.route"); m.addParam("module","alsa"); m.addParam("id",dest); m.addParam("caller",dest); m.addParam("called",targ); if (Engine::dispatch(m)) { m = "call.execute"; m.addParam("callto",m.retValue()); m.retValue() = 0; m.userData(chan); if (Engine::dispatch(m)) { chan->setTarget(m.getValue("targetid")); msg.addParam("targetid",chan->getTarget()); chan->deref(); return true; } Debug(DebugInfo,"Alsa outgoing call not accepted!"); } else Debug(DebugWarn,"Alsa outgoing call but no route!"); chan->destruct(); return false; } return true; } bool StatusHandler::received(Message &msg) { const char *sel = msg.getValue("module"); if (sel && ::strcmp(sel,"alsa")) return false; msg.retValue() << "name=alsa,type=misc;alsachan=" << (s_chan != 0 ) << "\n"; return false; } bool DropHandler::received(Message &msg) { String id(msg.getValue("id")); if (id.null() || id.startsWith("alsa/")) { if (s_chan) { Debug("AlsaDropper",DebugInfo,"Dropping call"); s_chan->disconnect(); } return !id.null(); } return false; } bool MasqHandler::received(Message &msg) { String id(msg.getValue("id")); if (msg.getParam("message") && id.startsWith("alsa/")) { msg = msg.getValue("message"); msg.clearParam("message"); if (s_chan) { msg.addParam("targetid",s_chan->getTarget()); msg.userData(s_chan); } } return false; } bool AttachHandler::received(Message &msg) { if (s_dev) return false; int more = 2; String src(msg.getValue("source")); if (src.null()) more--; else { if (!src.startSkip("alsa/",false)) src = ""; } String cons(msg.getValue("consumer")); if (cons.null()) more--; else { if (!cons.startSkip("alsa/",false)) cons = ""; } if (src.null() && cons.null()) return false; if (src && cons && (src != cons)) { Debug(DebugWarn,"Alsa asked to attach source '%s' and consumer '%s'",src.c_str(),cons.c_str()); return false; } DataEndpoint *dd = static_cast(msg.userObject("DataEndpoint")); if (!dd) { CallEndpoint *ch = static_cast(msg.userObject("CallEndpoint")); if (ch) dd = ch->setEndpoint(); } if (!dd) { Debug(DebugWarn,"Alsa attach request with no control or data channel!"); return false; } AlsaDevice* dev = new AlsaDevice(src ? src : cons,true); if (dev->closed()) { dev->deref(); return false; } if (src) { AlsaSource* s = new AlsaSource(dev); if (s->init()) dd->setSource(s); s->deref(); } if (cons) { AlsaConsumer* c = new AlsaConsumer(dev); if (c->init()) dd->setConsumer(c); c->deref(); } dev->deref(); // Stop dispatching if we handled all requested return !more; } AlsaPlugin::AlsaPlugin() : m_handler(0) { Output("Loaded module AlsaChan"); } void AlsaPlugin::initialize() { Output("Initializing module AlsaChan"); if (!m_handler) { m_handler = new AlsaHandler("call.execute"); Engine::install(new DropHandler("call.drop")); Engine::install(new MasqHandler("chan.masquerade",10)); Engine::install(m_handler); Engine::install(new StatusHandler); Engine::install(new AttachHandler); } } bool AlsaPlugin::isBusy() const { return (s_dev != 0); } INIT_PLUGIN(AlsaPlugin); }; // anonymous namespace /* vi: set ts=8 sw=4 sts=4 noet: */