function [Sched,GetData]=playaudio_unfold(buflen,unfold_fn,S,Snk,varargin)
% playaudio_async - Play stream of audio data asynchronously
%
% playaudio :: 
%    N:natural      ~'buflen',
%    (S->[[C,N]],S) ~'unfolding function',
%    sink(C,R)      ~'sink for C channels',
%    options {
%       maxbuf  :: natural/2*max(size(X))   ~'maximum buffer size';
%       hook    :: (iterator(S)->iterator(T)) ~'fn to build iterator';
%       onstart :: A -> action      ~'called BEFORE timer starts';
%       onstop  :: A -> action      ~'called AFTER timer stops';
%       onfinish:: A -> action      ~'called when end of signal reached';
%       defer   :: bool / 0         ~'if 1, don't start the timer'
%    }
% -> sched(S) ~'scheduler api functions',
%    (S -> [[N]]) ~'function to recover audio from iterator state'.
%
% iterator(S) ::= cell {(S->action S)~'state transformer', S~'initial state'}.
%
% sched(S) ::= struct {
%		dispose   :: unit -> action unit;
%		isrunning :: unit -> action bool;
%		startat	 :: real -> action unit;
%		start		 :: unit -> action unit;
%		stop		 :: unit -> action unit;
%		rewind	 :: unit -> action unit;
%		getstate	 :: unit -> action S;
%		setstate	 :: S    -> action unit
% }.
%
% The 'hook' option gives the caller an opportunity to elaborate on the
% 'iterator' used to drive the audio playback. The 'iterator' is a cell
% array contain a state transformer function and initial state. The caller
% can use this to build a more complex iterator the does other things
% for each buffer of samples.
%
% The third return value is a function which can be used in a state
% transformer function to recover a buffer of audio samples from the
% current state.
%
% NB: all the audio buffers must be the same size for this to work.
% NB: the rewind function should only be called when the timer is stopped.

	N=buflen;
	opts=options('onstop',@nop,'onstart',@nop,'onfinish',@nop, ...
		'period_adjust',0.9,'preload',2,'sched',@iterate_timed,varargin{:});

	it=feval(getparam(opts,'hook',@id),{@playbuf,S});

	maxbuf=getparam(opts,'maxbuf',2*N);
	sync_delta=getparam(opts,'sync_delta',0.02);
	sync_offset=getparam(opts,'sync_offset',0);

	L=construct(Snk);
	write=L.writer(maxbuf);
	period=(N/rate(Snk))*opts.period_adjust;

	Sched=opts.sched(it{1},it{2},period, ...
		'exec_mode','fixedRate', 'busy_mode','drop', opts, ...
		'onstart',@onstart,'onstop',@onstop); 
	getstate=Sched.getstate;
	odispose=Sched.dispose;
	Sched.atend=@()isempty(getstate());
	Sched.dispose=@dispose;
	Sched.setGain=L.setGain;

	GetData=@head;

	function dispose
		odispose();
		L.dispose();
	end

	function onstart(S),
		opts.onstart(S);
		L.start();
		for i=1:opts.preload
			write(zeros(maxbuf,1));
		end
	end

	function onstop(S),
		write(zeros(maxbuf,1));
		L.stop();
		opts.onstop(S);
	end

	function S=playbuf(S)
		[y,S]=unfold_fn(S);
		n=size(y,2);
		if n>0, write(y); end
	end
end

