#define FLEXT_ATTRIBUTES 1 #include #if !defined(FLEXT_VERSION) || (FLEXT_VERSION < 406) #error You need at least flext version 0.4.6 #endif #include #include #include #include #define SFPLAY_VERSION "0.0.2" // default buffer settings #define MINBUF 8000 // frames = samples per channel #define MAXBUF 10000 // frames = samples per channel #define SAFETY 2000 // minimum buffer at seek or file change, frames = samples per channel // chunk to read from file at once #define RDBUF 16384 // in samples (not frames) (must be > 16) class sfplay: public flext_dsp { FLEXT_HEADER_S(sfplay,flext_dsp,Setup) public: sfplay(int ch); ~sfplay(); virtual bool Init(); void ms_file(const AtomList &fl); void mg_file(AtomList &fl); void ms_pos(const AtomList &pos); void mg_pos(AtomList &pos); // virtual void m_dsp(int n,t_signalvec const *insigs,t_signalvec const *outsigs); virtual void m_signal(int n,t_sample *const *insigs,t_sample *const *outsigs); void ms_maxbuf(int b); private: static void threadfun(thr_params *); bool Work(); void Signal(); bool SafetyBuf(); ThrMutex filemtx,bufmtx; std::string filename; SNDFILE *sndfile; SF_INFO info; t_sample **buf; sf_count_t pos; int rptr,wptr; bool run,eof,loop,reported; int minbuf,maxbuf,safety; static void Setup(t_classid c); FLEXT_CALLVAR_V(mg_file,ms_file) FLEXT_ATTRVAR_B(run) FLEXT_ATTRVAR_B(loop) FLEXT_CALLVAR_V(mg_pos,ms_pos) FLEXT_ATTRVAR_I(minbuf) FLEXT_ATTRGET_I(maxbuf) FLEXT_CALLSET_I(ms_maxbuf) FLEXT_ATTRVAR_I(safety) }; FLEXT_NEW_DSP_1("sfplay~",sfplay,int0) static const t_symbol *sym_loop,*sym_eof,*sym_underflow; static flext::ThrMutex qumtx; static flext::ThrCond cond; typedef std::set Queue; typedef std::list List; static Queue queue; static List list; static pthread_t thrid; static float *rbuf = NULL; void sfplay::Signal() { qumtx.Lock(); Queue::iterator it = queue.find(this); if(it == queue.end()) { // this is not already in the queue queue.insert(this); list.push_back(this); } qumtx.Unlock(); cond.Signal(); } void sfplay::Setup(t_classid c) { // cache some symbols sym_loop = MakeSymbol("loop"); sym_eof = MakeSymbol("eof"); sym_underflow = MakeSymbol("underflow"); // start file helper thread LaunchThread(threadfun,NULL); // add methods and attributes FLEXT_CADDATTR_VAR(c,"file",mg_file,ms_file); FLEXT_CADDATTR_VAR1(c,"run",run); FLEXT_CADDATTR_VAR1(c,"loop",loop); FLEXT_CADDATTR_VAR(c,"pos",mg_pos,ms_pos); FLEXT_CADDATTR_VAR1(c,"minbuf",minbuf); FLEXT_CADDATTR_VAR(c,"maxbuf",maxbuf,ms_maxbuf); FLEXT_CADDATTR_VAR1(c,"safety",safety); post("sfplay~ - a simple sound file player"); post("Version " SFPLAY_VERSION " - (c)2004 Thomas Grill"); post(""); } sfplay::sfplay(int ch): sndfile(NULL), buf(NULL), rptr(0),wptr(0),pos(0), run(false),eof(false),loop(false),reported(false), minbuf(MINBUF),maxbuf(MAXBUF),safety(SAFETY) { AddOutSignal(ch?ch:2); buf = new t_sample *[ChannelsOut()]; for(int i = 0; i < ChannelsOut(); ++i) buf[i] = NULL; } sfplay::~sfplay() { qumtx.Lock(); queue.erase(this); for(List::iterator it = list.begin(); it != list.end(); ++it) if(*it == this) { list.erase(it); break; } qumtx.Unlock(); filemtx.Lock(); if(sndfile) sf_close(sndfile); filemtx.Unlock(); bufmtx.Lock(); for(int i = 0; i < ChannelsOut(); ++i) if(buf[i]) FreeAligned(buf[i]); delete[] buf; bufmtx.Unlock(); } bool sfplay::Init() { if(!flext_dsp::Init()) return false; // check if already initialized by attribute setting (maxbuf) ms_maxbuf(maxbuf); return true; } /*! Reduce current buffer filling to safety size (for better latency) \note should only be used when file stream changes! (data discontinuity) \note bufmtx must be locked by caller \note thread worker should be signalled */ bool sfplay::SafetyBuf() { int fill = wptr-rptr; if(fill < 0) fill += maxbuf; if(fill > safety) { // really reduce fill = safety; wptr = rptr+fill; if(wptr >= maxbuf) wptr -= maxbuf; return true; } else return false; } bool sfplay::Work() { int want = rptr-16-wptr; if(want < 0) want += maxbuf; if(!want || !sndfile || eof) return false; // nothing to do // rptr can be increased by m_dsp in the meantime but that's ok int frames,written = 0; { // read from file filemtx.Lock(); const int rdframes = RDBUF/info.channels; frames = want > rdframes?rdframes:want; const sf_count_t rf = sf_readf_float(sndfile,rbuf,frames); pos += rf; if(rf < frames) { // end of file frames = (int)rf; if(loop) { sf_seek(sndfile,pos = 0,SEEK_SET); ToOutAnything(GetOutAttr(),sym_loop,0,NULL); } else // eof messaging is done in dsp function eof = true; } filemtx.Unlock(); } { // copy to ring buffer // cache some variables const int chns = ChannelsOut(),stride = info.channels; const int cm = std::min(stride,chns); const float *rb = rbuf; bufmtx.Lock(); while(frames) { int c,f = frames; if(wptr+f > maxbuf) f = maxbuf-wptr; if(stride == 1) { CopySamples(buf[0]+wptr,rb,f); c = 1; } else for(c = 0; c < cm; ++c) { t_sample *b = buf[c]+wptr; const float *r = rb+c; if(stride == 2) for(int i = 0; i < f; ++i,++b,r += 2) *b = *r; else for(int i = 0; i < f; ++i,++b,r += stride) *b = *r; } for(; c < chns; ++c) ZeroSamples(buf[c]+wptr,f); rb += f*stride; wptr += f; if(wptr >= maxbuf) wptr -= maxbuf; frames -= f; written += f; want -= f; } bufmtx.Unlock(); } return written < want; // need more? } void sfplay::threadfun(thr_params *) { // should run at least at same priority as DSP thread // if run at a higher priority file access could disturb audio // maybe it's best to run it a the same priority then.... // RelPriority(+1); rbuf = (float *)NewAligned(RDBUF*sizeof(float)); for(;;) { qumtx.Lock(); List::iterator it = list.begin(); sfplay *th; if(it == list.end()) th = NULL; else { th = *it; queue.erase(th); list.erase(it); } qumtx.Unlock(); if(!th) cond.Wait(); else if(th->Work()) { // need more qumtx.Lock(); queue.insert(th); list.push_back(th); // re-add at end qumtx.Unlock(); } } } void sfplay::ms_file(const AtomList &args) { filename.clear(); for(int i = 0; i < args.Count(); ++i) if(IsString(args[i])) { if(filename.size()) filename += " "; filename += GetString(args[i]); } // else: ignore atom { filemtx.Lock(); if(sndfile) sf_close(sndfile); if(filename.size()) { sndfile = sf_open(filename.c_str(),SFM_READ,&info); eof = false; } else sndfile = NULL; pos = 0; filemtx.Unlock(); } { bufmtx.Lock(); if(sndfile) { SafetyBuf(); Signal(); } else filename.clear(); bufmtx.Unlock(); } } void sfplay::mg_file(AtomList &args) { if(filename.size()) { args(1); SetString(args[0],filename.c_str()); } else args(0); } void sfplay::ms_pos(const AtomList &args) { if(sndfile && args.Count() == 1 && CanbeInt(args[0])) { filemtx.Lock(); pos = GetAInt(args[0]); if(sf_seek(sndfile,pos,SEEK_SET) < 0) pos = 0; filemtx.Unlock(); } } void sfplay::mg_pos(AtomList &args) { if(sndfile) { args(1); SetInt(args[0],pos); } else args(0); } /* void sfplay::m_dsp(int n,t_signalvec const *insigs,t_signalvec const *outsigs) { } */ void sfplay::m_signal(int n,t_sample *const *insigs,t_sample *const *outsigs) { // bufmtx.Lock(); const int chns = ChannelsOut(); int fill = wptr-rptr; if(fill < 0) fill += maxbuf; const bool uflow = fill < n; if(sndfile && fill && run) { // ok! int fn = n; if(uflow) fn = fill; if(rptr+fn <= maxbuf) { for(int c = 0; c < chns; ++c) CopySamples(outsigs[c],buf[c]+rptr,fn); if((rptr += fn) >= maxbuf) rptr -= maxbuf; } else { const int r = maxbuf-rptr,rest = fn-r; for(int c = 0; c < chns; ++c) { CopySamples(outsigs[c],buf[c]+rptr,r); CopySamples(outsigs[c]+r,buf[c],rest); } rptr = rest; } if(uflow) for(int c = 0; c < chns; ++c) ZeroSamples(outsigs[c]+fn,n-fn); if(fill-n < minbuf) Signal(); } else { // no buffer or not running for(int c = 0; c < chns; ++c) ZeroSamples(outsigs[c],n); if(run && eof) { run = false; ToOutAnything(GetOutAttr(),sym_eof,0,NULL); } } // bufmtx.Unlock(); if(run && uflow && !eof) { if(!reported) { ToOutAnything(GetOutAttr(),sym_underflow,0,NULL); reported = true; } } else reported = false; } void sfplay::ms_maxbuf(int mxb) { bufmtx.Lock(); if(!buf[0]) { // not initialized yet (can happen with maxbuf attribute at creation time) for(int i = 0; i < ChannelsOut(); ++i) buf[i] = static_cast(NewAligned(sizeof(t_sample)*mxb)); maxbuf = mxb; } else if(maxbuf != mxb) { // already initialized... need to copy buffer contents! int fill = wptr-rptr; if(fill < 0) fill += maxbuf; if(fill > mxb) fill = mxb; int r = maxbuf-rptr; if(r > mxb) r = mxb; for(int i = 0; i < ChannelsOut(); ++i) { t_sample *obuf = buf[i]; buf[i] = static_cast(NewAligned(sizeof(t_sample)*mxb)); CopySamples(buf[i],obuf+rptr,r); if(fill > r) CopySamples(buf[i]+r,obuf,fill-r); FreeAligned(obuf); } rptr = 0,wptr = fill; maxbuf = mxb; } bufmtx.Unlock(); }