function err = Snd(command,signal,rate,sampleSize)
% err = Snd(command,[signal],[rate],[sampleSize])
%
% Old Sound driver for Psychtoolbox. USE OF THIS DRIVER IS DEPRECATED FOR
% ALL BUT THE MOST TRIVIAL PURPOSES! Only useful for simple feedback tones,
% and indirectly by Eyelink's calibration routines. Does not mix well with
% simultaneous use of PsychPortAudio() or Screen() movie playback functions!
%
% Have a look at the help for PsychPortAudio ("help PsychPortAudio" and
% "help InitializePsychSound") for an introduction into the new sound
% driver, which is recommended for most purposes.
%
% Snd() is a rather dumb and primitive wrapper around the PsychPortAudio()
% driver. It uses PsychPortAudio's most basic functionality to achieve
% "sort of ok" sound playback. The driver is used in high-latency,
% low-timing precision mode, so Snd()'s audio playback timing will likely
% be very unreliable.
%
% Alternatively you can create an empty file named 'Snd_use_oldstyle.txt' in
% the PsychtoolboxConfigDir() folder, ie., [PsychtoolboxConfigDir 'Snd_use_oldstyle.txt']
% This will enable the old-style implementation of Snd(), which is equally
% shoddy and works as follows:
%
% While Snd used to use a special purpose low level driver on MacOS-9 which
% was well suited for cognitive science, Snd for all other operating
% systems (Windows, MacOS-X, Linux) just calls into Matlab's Sound()
% function which is of varying - but usually pretty poor - quality in most
% implementations of Matlab. There are many bugs, latency- and timing
% problems associated with the use of Snd.
%
% GNU/OCTAVE: If you don't use the PsychPortAudio based Snd() function, then
% you must install the optional octave "audio" package from Octave-Forge,
% as of Octave 3.0.5, otherwise the required sound() function won't be
% available and this function will fail!
%
% Audio device sharing for interop with PsychPortAudio:
% -----------------------------------------------------
%
% If you want to use PsychPortAudio and Snd() simultaneously, or one of the
% functions that indirectly use Snd(), e.g., Beeper() for simple beep tones,
% or Eyelink's auditory feedback during tracker setup and recalibration, which
% in turn uses Beeper() and thereby Snd(), then try this:
%
% 1. Open a suitable PsychPortAudio audio device, possibly also a slave audio
%    device and get a pahandle to it, e.g., pahandle = PsychPortAudio('Open',...);
%    or PsychPortAudio('OpenSlave', ...) for a slave device.
%
% 2. Now open Snd(), passing in this device handle for use as Snd() output device:
%    Snd('Open', pahandle);
%
%    If you want to repeatedly call Beeper(), or use auditory feedback from Eyelink,
%    which itself repeatedly calls Beeper(), then you should open the shared pahandle
%    via Snd('Open', pahandle, 1); - This will prevent Snd('Close') from having any
%    effect, so Beeper() won't close the Snd() driver after one beep, and Eyelink
%    will be able to emit multiple auditory feedback tones, not just a single one.
%
% 3. Proceed as usual, e.g., Snd('Play', ...) or Beeper(...), etc. Snd() will
%    use the pahandle audio device for playback, and pahandle can also be used
%    by PsychPortAudio calls directly for precisely controlled sound.
%
% 4. At the end of a session, you could forcefully detach Snd() from the pahandle
%    via a call to Snd('Close', 1).
%
% Supported functions:
% --------------------
%
% Snd('Play', signal [, rate][, sampleSize]) plays a sound.
%
% rate=Snd('DefaultRate') returns the default sampling rate in Hz, which
% currently is 22254.5454545454 Hz on all platforms for the old style sound
% implementation, and the default device sampling rate if PsychPortAudio is
% used. This default may change in the future, so please either specify a
% rate, or use this function to get the default rate. (This default is
% suboptimal on any system except long dead MacOS-9, but kept for backwards
% compatibility!).
%
% The optional 'sampleSize' argument used with Snd('Play') is only retained
% for backwards compatibility and has no meaning, unless you opt in to use
% the old-style implementation on Matlab with some operating systems. - It
% is checked for correctness, but other than that it is ignored. Allowable
% values are either 8 or 16.
%
% oldverbosity = Snd('Verbosity' [, verbosity]);
% - Query current level of verbosity, optionally set a new 'verbosity' level.
%
% Snd('Open') opens the channel, which stays open until you call
% Snd('Close'). Snd('Play',...) automatically opens the channel if it isn't
% already open. You can use Snd('Open', pahandle); to share an existing
% PsychPortAudio audio device handle 'pahandle' with Snd() for optimal
% interoperation. A Snd('Close') of such a shared 'pahandle' would not close the
% handle, but it would close Snd()'s further use of it. If you call
% Snd('Open', pahandle, 1); then a Snd('Close') will not have any effect, ie. the
% pahandle not only stays open, but also continues to be shared and open for use
% by Snd(). See instructions above.
%
% Snd('Close') immediately stops all sound and closes the channel, unless you
% specified a shared pahandle with PsychPortAudio via Snd('Open', pahandle, 1);
% earlier. Calling Snd('Close', 1) will always really close the channel.
%
% Snd('Wait') waits until the sound is done playing.
%
% isPlaying=Snd('IsPlaying') returns true if any sound is playing, and
% false (0) otherwise.
%
% Snd('Quiet') stops the sound currently playing, but leaves the channel open.
%
% "signal" must be a numeric array of samples.
%
% Your "signal" data should lie between -1 and 1 (smaller to play more
% softly). If the "signal" array has one row then it's played monaurally,
% through both speakers. If it has two rows then it's played in stereo.
% (Snd has no provision for changing which speaker(s), or the volume, used
% by a named snd resource, so use READSND to get the snd into an array,
% and supply the appropriately modified array to Snd.)
%
% "rate" is the rate (in Hz) at which the samples in "signal" should be
% played. We suggest you always specify the "rate" parameter. If not
% specified, the sample "rate", on all platforms, defaults to OS9's
% standard hardware sample rate of 22254.5454545454 Hz. That value is
% returned by Snd('DefaultRate'). Other values can be specified.
%
% OSX & WIN: "samplesize". Snd accepts the sampleSize argument and passes
% it to the Matlab SOUND command.  SOUND (and therefore Snd also) obeys the
% specified sampleSize value, either 8 or 16, only if it is supported by
% your computer hardware.
%
% Snd('Play',sin(0:10000)); % play 22 KHz/(2*pi)=3.5 kHz tone
% Snd('Play',[sin(1:20000) zeros(1,10000);zeros(1,10000) sin(1:20000)]); % stereo
% Snd('Wait');              % wait until end of all sounds currently in channel
% Snd('Quiet');             % stop the sound and flush the queue
%
% For most of the commands, the returned value is zero when successful, and
% a nonzero error number when Snd fails.
%
% Snd('Play', signal) takes some time to open the channel, if it isn't
% already open, and allocate a snd structure for your sound. This overhead
% of the call to Snd, if you call it in the middle of a movie, may be
% perceptible as a pause in the movie, which would be bad. However, the
% actual playing of the sound, asynchronously, is a background process that
% usually has very little overhead. So, even if you want a sound to begin
% after the movie starts, you should create a soundtrack for your entire
% movie duration (possibly including long silences), and call Snd to set
% the sound going before you start your movie. (Thanks to Liz Ching for
% raising the issue.)
%
% NOTE: We suggest you always specify the "rate" parameter. If not
% specified, the sample rate, on all platforms, defaults to OS9's
% standard hardware sample rate of 22254.5454545454 Hz. That value is returned
% by Snd('DefaultRate').
%
% See also PsychPortAudio, Beeper, AUDIOPLAYER, PLAY, MakeBeep, READSND, and WRITESND.

% 6/6/96    dgp Wrote SndPlay.
% 6/1/97    dgp Polished help text.
% 12/10/97  dhb Updated help.
% 2/4/98    dgp Wrote Snd, based on major update of VideoToolbox SndPlay1.c.
% 3/8/00    emw Added PC notes and code.
% 7/23/00   dgp Added notes about controlling volume of named snd, and updated
%               broken link to ResEdit.
% 4/13/02   dgp Warn that the two platforms have different default sampling rate,
%               and suggest that everyone routinely specify sampling rate to
%               make their programs platform-independent.
% 4/13/02   dgp Enhanced both OS9 and WIN versions so that 'DefaultRate'
%               returns the default sampling rate in Hz.
% 4/13/02   dgp Changed WIN code, so that sampling rate is now same on both platforms.
% 4/15/02   awi fixed WIN code.
% 5/30/02   awi Added sampleSize argument and documented.
%               SndTest would crash Matlab but the problem mysteriously vanished while editing
%               the Snd.m and SndTest.m files.  I've been unable to reproduce the error.
% 3/10/05   dgp Make it clear that the Snd mex is only available for OS9.
%               Mention AUDIOPLAYER, as suggested by Pascal Mamassian.
% 5/20/08    mk Explain that Snd() is deprecated --> Point to PsychPortAudio!
% 1/12/09    mk Make sure that 'signal' is a 2-row matrix in stereo, not 2
%               column.
% 6/01/09    mk Add compatibility with Octave-3.
% 9/03/12    mk Add new implementation via PsychPortAudio(), which is used
%               by default unless user opts out. Cleanup online help by
%               removal of obsolete and outdated information.
% 7/20/15    mk Make commands case insensitive, white-space cleanup, try to
%               make Snd('Open'); Snd('Close'); sequences work to avoid problems
%               like in PTB forum message #19284 on Linux or Windows.
% 1/19/16    mk Make more robust against failing InitializePsychSound. Fallback to sound().
% 7/11/19    mk Allow sharing pahandle with external code / piggyback onto existing pahandle
%               via Snd('Open', pahandle);
% 12/9/19    mk Add 'Verbosity' subcommand to be able to silence Snd() and PsychPortAudio() output.
% 08/10/21   mk Try to make Snd - pahandle sharing more reliable via new Snd('Open', pahandle, 1).
% 10/29/21   mk Some more help updates.

persistent ptb_snd_oldstyle;
persistent ptb_snd_injected;
persistent pahandle;
persistent verbose;

persistent endTime;
if isempty(endTime)
    endTime = 0;
    ptb_snd_injected = 0;
    verbose = 1;
end

% Default return value:
err = 0;

if nargin == 0
    error('Wrong number of arguments: see Snd.');
end

if strcmpi(command, 'Verbosity')
    if nargin ~= 2
        error('Snd: Called "Verbosity" without specifying verbosity level.');
    end

    err = verbose;
    verbose = signal;
    return;
end

% Snd('Open', pahandle [, noclose=0]) called to inject a pahandle of an already open
% PsychPortAudio device for sharing? If so, we piggyback onto pahandle
% for our audio playback:
if strcmpi(command,'Open') && nargin >= 2 && ~isempty(signal)
    % Close previous PsychPortAudio handle if it was not injected into us:
    if ~isempty(pahandle) && pahandle ~= signal && ~ptb_snd_injected
        PsychPortAudio('Close', pahandle);
    end

    % Assign 2nd argument as new pahandle:
    pahandle = signal;

    % Obviously we are not in old style mode here:
    ptb_snd_oldstyle = 0;

    % "noclose" flag provided as 1?
    if nargin >=3 && ~isempty(rate) && rate == 1
        % Mark this pahandle as injected and uncloseable by us. Neither the PPA
        % device, nor our association with it should be closed/removed, not even
        % if our caller calls Snd('Close'). This is useful for emitting repeated
        % beeps via Beeper(), as Beeper() would Snd('Close') after each beep, and
        % thereby remove the association:
        ptb_snd_injected = 2;

        if verbose
            fprintf('Snd(): Using PsychPortAudio via shared handle %i permanently.\n', pahandle);
        end
    else
        % Mark this pahandle as injected -- PPA device should not be closed by us
        % ever, but our Snd('Close') function can detach us from it, so it will
        % only be usable by PsychPortAudio directly again:
        ptb_snd_injected = 1;

        if verbose
            fprintf('Snd(): Using PsychPortAudio via shared handle %i, until you call Snd(''Close''); to unshare.\n', pahandle);
        end
    end

    endTime = 0;

    return;
end

% Already defined if we shall use the new-style or old-style
% implementation?
if isempty(ptb_snd_oldstyle)
    % Nope, check if the special "old style" marker file exists:
    if exist([PsychtoolboxConfigDir 'Snd_use_oldstyle.txt'], 'file')
        % User explicitely wants old-style implementation via
        % Matlab/Octave sound() function:
        ptb_snd_oldstyle = 1;

        if verbose
            fprintf('Snd(): Using Matlab/Octave sound() function for sound output.\n');
        end
    else
        % User wants new-style PsychPortAudio variant:
        ptb_snd_oldstyle = 0;

        if verbose
            fprintf('Snd(): Initializing PsychPortAudio driver for sound output.\n');
        end

        % Basic sound preinit:
        try
            InitializePsychSound;
        catch
            fprintf('Snd(): ERROR!\n');
            ple;
            psychlasterror('reset');
            fprintf('Snd(): PsychPortAudio initialization failed - See error messages above. Trying to use old sound() fallback instead.\n');
            ptb_snd_oldstyle = 1;
        end
    end
end

% Don't use PsychPortAudio backend if already a PPA device open on Linux or
% Windows, as that device will often have exclusive access, so we would
% fail here:
if ~(strcmpi(command,'Open') || strcmpi(command,'Quiet') || strcmpi(command,'Close')) && ...
    isempty(pahandle) && ~ptb_snd_oldstyle && ~IsOSX

    if ~verbose
        oldv = PsychPortAudio('Verbosity', 0);
        odc = PsychPortAudio('GetOpenDeviceCount');
        PsychPortAudio('Verbosity', oldv);
    else
        odc = PsychPortAudio('GetOpenDeviceCount');
    end

    if odc > 0
        if verbose
            fprintf('Snd(): PsychPortAudio already in use. Using old sound() fallback instead...\n');
        end

        ptb_snd_oldstyle = 1;
    end
end

if strcmpi(command,'Play')
    if nargin > 4
        error('Wrong number of arguments: see Snd.');
    end

    if nargin == 4
        if isempty(sampleSize)
            sampleSize = 16;
        elseif ~((sampleSize == 8) || (sampleSize == 16))
            error('sampleSize must be either 8 or 16.');
        end
    else
        sampleSize = 16;
    end

    if nargin < 3
        rate = [];
    end

    if nargin < 2
        error('Wrong number of arguments: see Snd.');
    end

    if size(signal,1) > size(signal,2)
        error('signal must be a 2 rows by n column matrix for stereo sounds.');
    end

    if isempty(rate)
        if ptb_snd_oldstyle
            % Old MacOS-9 style default:
            rate = 22254.5454545454;
        else
            % Let PPA decide itself:
            rate = [];
        end
    end

    if ptb_snd_oldstyle
        % Old-Style implementation via sound() function:

        WaitSecs(endTime-GetSecs); % Wait until any ongoing sound is done.

        % Octave special-case:
        if IsOctave
            if exist('sound') %#ok<EXIST>
                sound(signal',rate);
            else
                % Unavailable: Try to load the package, assuming its
                % installed but not auto-loaded:
                try
                    pkg('load','audio');
                catch %#ok<CTCH>
                end

                % Retry...
                if exist('sound') %#ok<EXIST>
                    sound(signal',rate);
                else
                    warning('Required Octave command sound() is not available. Install and "pkg load audio" the "audio" package from Octave-Forge!'); %#ok<WNTAG>
                end
            end
        else
            sound(signal',rate,sampleSize);
        end

        % Estimate 'endTime' for playback:
        endTime=GetSecs+length(signal)/rate;
    else
        % New-Style via PsychPortAudio:
        if ~isempty(pahandle)
            % PPA playback device already open.

            % Wait blocking until end of its playback:
            PsychPortAudio('Stop', pahandle, 1, 1);

            % Correct sampling rate set?
            props = PsychPortAudio('GetStatus', pahandle);
            if ~isempty(rate) && (abs(props.SampleRate - rate) > 1.0)
                % Device sampleRate not within tolerance of 1 Hz to
                % requested samplingrate.
                if ~ptb_snd_injected
                    % Close it, so it can get reopened with proper rate:
                    PsychPortAudio('Close', pahandle);
                    pahandle = [];
                else
                    % Should change samplerate, but can not, as this is a shared
                    % pahandle:
                    if verbose
                        fprintf('Snd(): Shared PsychPortAudio handle. Can not change sample rate from current %f Hz to %f Hz as requested!\n', ...
                                props.SampleRate, rate);
                    end
                end
            end
        end

        if isempty(pahandle)
            % Be silent during driver/device init:
            oldverbosity = PsychPortAudio('Verbosity', 2);

            % Open our own 'pahandle' sound device: Auto-Selected output
            % device [], playback only (1),
            % high-latency/low-timing-precision mode (0), at given
            % samplerate (rate), in stereo (2):
            pahandle = PsychPortAudio('Open', [], 1, 0, rate, 2);

            % Restore standard level of verbosity:
            PsychPortAudio('Verbosity', oldverbosity);

            if ~IsOSX && verbose
                fprintf('Snd(): PsychPortAudio will be blocked for use by your own code until you call Snd(''Close'');\n');
                fprintf('Snd(): If you want to use PsychPortAudio and Snd in the same session, make sure to open your\n');
                fprintf('Snd(): stimulation sound device via calls to, e.g., pahandle = PsychPortAudio(''Open'', ...);\n');
                fprintf('Snd(): *before* the first call to Snd() or any function that might use Snd(), e.g., Beeper()\n');
                fprintf('Snd(): or the Eyelink functions with auditory feedback. Call Snd(''Open'', pahandle) next, to\n');
                fprintf('Snd(): share that pahandle audio device with Snd(), Beeper() et al. for optimal collaboration.\n');
                fprintf('Snd(): Consider using PsychPortAudio(''OpenSlave'', ...); on a master device and pass that slave\n');
                fprintf('Snd(): handle to Snd(''Open'', ...) if you want to allow Snd() to operate fully independently.\n');
                fprintf('Snd(): You may want to read ''help Snd'' about other points to consider wrt. pahandle sharing.\n\n');
            end
        end

        % Make signal stereo if it isn't already:
        if size(signal, 1) < 2
            sndbuff = [signal ; signal];
        else
            sndbuff = signal;
        end

        % Play it:
        PsychPortAudio('FillBuffer', pahandle, sndbuff);
        PsychPortAudio('Start', pahandle, 1, 0, 1);
    end

elseif strcmpi(command,'Wait')
    if nargin>1
        error('Wrong number of arguments: see Snd.');
    end

    if ~isempty(pahandle)
        % Wait blocking until end of playback:
        PsychPortAudio('Stop', pahandle, 1, 1);
    else
        WaitSecs(endTime-GetSecs); % Wait until any ongoing sound is done.
    end
    err=0;

elseif strcmpi(command,'IsPlaying')
    if nargin>1
        error('Wrong number of arguments: see Snd.');
    end

    if ~isempty(pahandle)
        props = PsychPortAudio('GetStatus', pahandle);
        err = props.Active;
    else
        if endTime>GetSecs
            err=1;
        else
            err=0;
        end
    end

elseif strcmpi(command,'Quiet') || strcmpi(command,'Close')
    if nargin > 2
        error('Wrong number of arguments: see Snd.');
    end

    if ~isempty(pahandle)
        % Stop playback asap, wait for stop:
        PsychPortAudio('Stop', pahandle, 2, 1);

        % Close command?
        if strcmpi(command,'Close') && ((ptb_snd_injected ~= 2) || (nargin == 2 && signal == 1))
            if ~ptb_snd_injected
                % Close it:
                PsychPortAudio('Close', pahandle);
            end

            pahandle = [];
            ptb_snd_injected = 0;
        end
    else
        if ~IsOctave
            clear playsnd; % Stop any ongoing sound.
        end
    end
    endTime=0;
    err=0;

elseif strcmpi(command,'DefaultRate')
    if nargin>1
        error('Wrong number of arguments: see Snd.');
    end

    if ptb_snd_oldstyle
        % Old style - old hard-coded default:
        err = 22254.5454545454; % default sampling rate in Hz.
    else
        % Audio device open?
        if isempty(pahandle)
            % No: Fake it - Use the most common sampling rate:
            err = 44100;
        else
            % Yes: Query its current sampling rate:
            s = PsychPortAudio('GetStatus', pahandle);
            err = s.SampleRate;
        end
    end

elseif strcmpi(command,'Open')
    endTime=0;
else
    PsychPortAudio('Close');
    pahandle = [];
    error(['unknown command "' command '"']);
end

return;
