function PsychPortAudioTimingTest(exactstart, deviceid, latbias, waitframes, useDPixx, triggerLevel, reqlatencyclass) % PsychPortAudioTimingTest([exactstart=1][, deviceid=-1][, latbias=0][, waitframes][, useDPixx=0][, triggerLevel=0.01][, reqlatencyclass=2]) % % Test script for sound onset timing reliability and sound onset % latency of the PsychPortAudio sound driver. % % This script configures the driver for low latency and high timing % precision, then executes ten trials where it tries to emit a beep sound, % starting in exact sync to a black-white transition on the display. % % You'll need measurement equipment to use this: A photo-diode attached to % the top-left corner of your CRT display, a microphone attached to your % speakers, some oszillograph to record and measure the signals from the % diode and microphone. % % Some parameters may need tweaking. Make sure you got a setup as described % in 'help InitializePsychSound' for best results. % % Optional parameters: % % 'exactstart' = 0 -- Start immediately, measure absolute latency. % = 1 -- Test accuracy of scheduled sound onset. (Default) % % 'deviceid' = -1 -- Auto-select optimal device (Default). % >=0 -- Select specified output device. See % PsychPortAudio('GetDevices') for a list of devices. % % 'latbias' = Hardware inherent latency bias. To be determined by % measurement - allows to PA to correct for it if provided. % Unit is seconds. Defaults to zero. % % 'waitframes' = Time to wait (in video refresh intervals) before emitting beep + flash. % Defaults to a typically safe value if omitted. % % 'useDPixx' = 1 -- Use DataPixx device to automatically measure the true % audio onset time wrt. to visual stimulus onset. % 0 -- Don't use DataPixx. This is the default. % % 'triggerLevel' = Sound signal amplitude for DataPixx to detect sound % onset. Defaults to 0.01 = 1% of max amplitude if % exactstart == 0, otherwise it is auto-detected by % calibration. This will likely need tweaking on your % setup. If the measured audio onset delta by DataPixx is % much lower (or almost zero) than the expected delta % reported by PsychPortAudio, then the triggerLevel may be % too low and you should try if slightly higher thresholds % help to discriminate signal from noise. Too high values % may cause a hang of the script. In practice, levels % between 0.01 and 0.1 should yield good results. Setting % the 'useDPixx' flag to 2 also plots the waveforms % captured by DataPixx, which may help in selection of the % optimal triggerLevel. % % 'reqlatencyclass' Override setting for reqlatencyclass parameter. By default, % reqlatencyclass = 2 is used, for exclusive device access for % low latency / high timing precision mode. if nargin < 7 || isempty(reqlatencyclass) % Request latency mode 2, a tad more aggressive than the default: reqlatencyclass = 2; end % Initialize driver, request low-latency preinit for reqlatencyclass > 1: InitializePsychSound(double(reqlatencyclass > 1)); % Force GetSecs and WaitSecs into memory to avoid latency later on: GetSecs; WaitSecs(0.1); % If 'exactstart' wasn't provided, assume user wants to test exact sync of % audio and video onset, instead of testing total onset latency: if nargin < 1 exactstart = []; end if isempty(exactstart) exactstart = 1; end if exactstart fprintf('Will test accuracy of scheduled sound onset, i.e. how well the driver manages to\n'); fprintf('emit sound at exactly the specified "when" deadline. Sound should start in exact\n'); fprintf('sync with display black-white transition (or at least very close - < 1 msec off).\n'); fprintf('The remaining bias can be corrected by providing the bias as "latbias" parameter\n'); fprintf('to this script. Variance of sound onset between trials should be very low, much\n'); fprintf('smaller than 1 millisecond on a well working system.\n\n'); else fprintf('Well test total latency for immediate start of sound playback, i.e., the "when"\n'); fprintf('parameter is set to zero. The difference between display black-white transition\n'); fprintf('and start of emitted sound will be the total system latency.\n\n'); end % Default to auto-selected default output device if none specified: if nargin < 2 deviceid = []; end if isempty(deviceid) deviceid = -1; end % Needs to determined via measurement once for each piece of audio % hardware: if nargin < 3 latbias = []; end if deviceid == -1 fprintf('Will use auto-selected default output device. This is the system default output\n'); fprintf('device in "normal" (=reliable but high latency) mode. In low-latency mode its the\n'); fprintf('device with the lowest inherent latency on your system (as determined by some internal\n'); fprintf('heuristic). If you are not satisfied with the results you may query the available devices\n'); fprintf('yourself via a call to devs = PsychPortAudio(''GetDevices''); and provide the device index\n'); fprintf('of a suitable device\n\n'); else fprintf('Selected the following output device (deviceid=%i) according to your spec:\n', deviceid); devs = PsychPortAudio('GetDevices'); for idx = 1:length(devs) if devs(idx).DeviceIndex == deviceid break; end end disp(devs(idx)); end % Requested output frequency, may need adaptation on some audio-hw: freq = 44100; % Must set this. 44.1 khz most likely to work, as shown by experience. Common rates: 96khz, 48khz, 44.1khz. buffersize = 0; % Pointless to set this. Auto-selected to be optimal. suggestedLatencySecs = []; if IsARM % ARM processor, probably the RaspberryPi SoC. This can not quite handle the % low latency settings of a Intel PC, so be more lenient: suggestedLatencySecs = 0.025; if isempty(latbias) latbias = 0.000593; fprintf('Choosing a latbias setting of 0.000593 secs or 0.593 msecs, assuming this is a RaspberryPi ARM SoC.\n'); end fprintf('Choosing a high suggestedLatencySecs setting of 25 msecs to account for lower performing ARM SoC.\n'); end if isempty(latbias) % Unknown system: Assume zero bias. User can override with measured values: fprintf('No "latbias" provided. Assuming zero bias. You''ll need to determine this via measurement for best results...\n'); latbias = 0; end if nargin < 4 waitframes = []; end if nargin < 5 useDPixx = []; end if isempty(useDPixx) useDPixx = 0; end if nargin < 6 % Default triggerLevel is "auto-trigger": triggerLevel = []; end % Open audio device for low-latency output: pahandle = PsychPortAudio('Open', deviceid, [], reqlatencyclass, freq, 2, buffersize, suggestedLatencySecs); % Tell driver about hardwares inherent latency, determined via calibration once: prelat = PsychPortAudio('LatencyBias', pahandle, latbias) %#ok postlat = PsychPortAudio('LatencyBias', pahandle) %#ok % Generate some beep sound 1000 Hz, 0.1 secs, 50% amplitude: mynoise(1,:) = 0.5 * MakeBeep(1000, 0.1, freq); mynoise(2,:) = mynoise(1,:); % Fill buffer with data: PsychPortAudio('FillBuffer', pahandle, mynoise); % Setup display: screenid = max(Screen('Screens')); % Shall we use the DataPixx for measurement? if useDPixx % Initialize audio capture subsystem of Datapixx: % 96 KhZ sampling rate, Mono capture: Average across channels (0), Audio % input is line in (2), Gain is 1.0 (1): DatapixxAudioKey('Open', 96000, 0, 2, 1); % Check settings by printing them: dpixstatus = Datapixx('GetMicrophoneStatus') %#ok if ~(exactstart && isempty(triggerLevel)) if isempty(triggerLevel) % Choose a default of 1% of max. signal amplitude: triggerLevel = 0.01; end fprintf('Using a trigger level for DataPixx of %f. This may need tweaking by you...\n', triggerLevel); DatapixxAudioKey('TriggerLevel', triggerLevel); end % DataPixx: Setup Screen imagingpipeline to support measurement via the PSYNC % video synchronization mode of DataPixx and Screen(): PsychImaging('PrepareConfiguration'); PsychImaging('AddTask', 'General', 'UseDataPixx'); win = PsychImaging('OpenWindow', screenid, 0); LoadIdentityClut(win); else % Default: No need for imaging pipeline: win = Screen('OpenWindow', screenid, 0); end ifi = Screen('GetFlipInterval', win); % Set waitframes to a good default, if none is provided by user: if isempty(waitframes) % We try to choose a waitframes that maximizes the chance of hitting % the onset deadline. We are conservative in our estimate, because a % few video refresh cycles hardly matter for this test, but increase % our chance of success without need for manual tuning by user: if isempty(suggestedLatencySecs) % Let's assume 12 msecs as a achievable latency by % default, then double it: waitframes = ceil((2 * 0.012) / ifi) + 1; else % Whatever was provided, then double it: waitframes = ceil((2 * suggestedLatencySecs) / ifi) + 1; end end fprintf('\n\nWaiting %i video refresh cycles before white-flash.\n', waitframes); % Auto-Selection of triggerLevel for Datapixx timestamping requested? if useDPixx && exactstart && isempty(triggerLevel) % Use auto-trigger mode. Tell the function how long the silence % interval at start of each trial is expected to be. This will be % used for calibration: We set it to 75% of the duration of the pause % between start of Datapixx recording and scheduled sound onset time: DatapixxAudioKey('AutoTriggerLevel', ifi * waitframes * 0.75); fprintf('Setting lead time of silence in Datapixx auto-trigger mode to %f msecs.\n', ifi * waitframes * 0.75 * 1000); end % Perform one warmup trial, to get the sound hardware fully up and running, % performing whatever lazy initialization only happens at real first use. % This "useless" warmup will allow for lower latency for start of playback % during actual use of the audio driver in the real trials: PsychPortAudio('Start', pahandle, 1, 0, 1); PsychPortAudio('Stop', pahandle, 1); % Ok, now the audio hardware is fully initialized and our driver is on % hot-standby, ready to start playback of any sound with minimal latency. % Wait for keystroke. KbStrokeWait; % Realtime scheduling: Can be used if otherwise timing is not good enough. % Priority(MaxPriority(win)); avdelay = []; tserror = []; % Ten measurement trials: for i=1:10 if useDPixx % Schedule start of audio capture on DataPixx at next Screen('Flip'): DatapixxAudioKey('CaptureAtFlip'); end % This flip clears the display to black and returns timestamp of black onset: % It also triggers start of audio recording by the DataPixx, if it is % used, so the DataPixx gets some lead-time before actual audio onset. [vbl1 visonset1]= Screen('Flip', win); % Prepare black white transition: Screen('FillRect', win, 255); Screen('DrawingFinished', win); % Compute tWhen onset time for wanted visual onset at >= tWhen: tWhen = vbl1 + (waitframes - 0.5) * ifi; if exactstart % Schedule start of audio at exactly the predicted visual stimulus % onset caused by the next flip command. tPredictedVisualOnset = PredictVisualOnsetForTime(win, tWhen); PsychPortAudio('Start', pahandle, 1, tPredictedVisualOnset, 0); end % Ok, the next flip will do a black-white transition... [vbl visual_onset t1] = Screen('Flip', win, tWhen); if ~exactstart % No test of scheduling, but of absolute latency: Start audio % playback immediately: PsychPortAudio('Start', pahandle, 1, 0, 0); end t2 = GetSecs; % Spin-Wait until hw reports the first sample is played... offset = 0; while offset == 0 status = PsychPortAudio('GetStatus', pahandle); offset = status.PositionSecs; t3=GetSecs; plat = status.PredictedLatency; fprintf('Predicted Latency: %6.6f msecs.\n', plat*1000); if offset > 0 break; end WaitSecs('YieldSecs', 0.001); end audio_onset = status.StartTime; %fprintf('Expected visual onset at %6.6f secs.\n', visual_onset); %fprintf('Sound started between %6.6f and %6.6f\n', t1, t2); %fprintf('Expected latency sound - visual = %6.6f\n', t2 - visual_onset); %fprintf('First sound buffer played at %6.6f\n', t3); fprintf('Flip delay = %6.6f secs. Flipend vs. VBL %6.6f\n', vbl - vbl1, t1-vbl); fprintf('Delay start vs. played: %6.6f secs, offset %f\n', t3 - t2, offset); fprintf('Buffersize %i, xruns = %i, playpos = %6.6f secs.\n', status.BufferSize, status.XRuns, status.PositionSecs); fprintf('Screen expects visual onset at %6.6f secs.\n', visual_onset); fprintf('PortAudio expects audio onset at %6.6f secs.\n', audio_onset); fprintf('Expected audio-visual delay is therefore %6.6f msecs.\n', (audio_onset - visual_onset)*1000.0); avdelay(end+1) = (audio_onset - visual_onset)*1000.0; if useDPixx % 'visonset1' is the GetSecs() time of start of capture on % DataPixx. 'audio_onset' is reported GetSecs() audio onset time % according to PsychPortAudio. % % 'expectedAudioDelta' is therefore the expected delay for the % measured audio onset by DataPixx: expectedAudioDelta = audio_onset - visonset1; % Retrieve true delay from DataPixx measurement and stop recording % on the device: [audiodata, measuredAudioDelta] = DatapixxAudioKey('GetResponse', [], [], 1); fprintf('DPixx: Expected audio onset delta is %6.6f secs.\n', expectedAudioDelta); fprintf('DPixx: Measured audio onset delta is %6.6f secs.\n', measuredAudioDelta); fprintf('DPixx: PsychPortAudio measured onset error is therefore %6.6f msecs.\n', 1000 * (measuredAudioDelta - expectedAudioDelta)); if ~isempty(measuredAudioDelta) tserror(end+1) = 1000 * (measuredAudioDelta - expectedAudioDelta); end if useDPixx > 1 figure; plot(audiodata); end end % Stop playback: PsychPortAudio('Stop', pahandle, 1); % Wait a bit... WaitSecs(0.3); Screen('FillRect', win, 0); telapsed = Screen('Flip', win) - visual_onset; %#ok WaitSecs(0.6); end % Done, close driver and display: Priority(0); if useDPixx % Close Datapixx audio subsystem: DatapixxAudioKey('Close'); end PsychPortAudio('Close'); Screen('CloseAll'); fprintf('\n\nSummary:\n'); fprintf('AV Error: %f msecs.\n', avdelay); fprintf('TS Error: %f msecs.\n', tserror); fprintf('\n\n'); % Done. Bye. return;