function varargout = DatapixxAudioKey(cmd, varargin) % DatapixxAudioKey - A prototype implementation of a DataPixx % audio-key/voice-key. % % This allows to record audio from the Datapixx, optionally synchronized to % visual stimulus onset, optionally with simple but precise timestamping of % onset of audio signals. % % This is an experimental proof-of-concept prototype! Its name, behaviour % and interface may change without warning in future beta-releases! % % % Subfunctions and their meaning: % ------------------------------- % % DatapixxAudioKey('Open' [, sampleRate][, lrMode][, inputJack][, inputGain]); % - Open audio system for recording. This will stop any running audio % acquisition operations. Audio sampling will be performed at the given % optional 'sampleRate' between 8000 Hz and 96Khz with the given number of % audio input channels 'lrMode' (0 = Mono: Average of left and right % channel, 1 = Only left channel, 2 = Only right channel, 3 = Stereo). % Optionally select 'inputJack' as audio input (1=Microphone, 2=Line-In) % and 'inputGain' as input amplifier gain. If no 'inputJack' is specified % and no input is preselected, 'inputJack' will default to microphone % input. % % % DatapixxAudioKey('Close'); % - Close audio subsystem after stopping any running acquisition operations. % % [startTimeBox, startTimeGetSecs] = DatapixxAudioKey('CaptureNow' [, maxDurationSecs=30][, startOffsetSecs=0][,bufferBaseAddress=20e6] [, numBufferFrames=maxScheduleFrames]); % - Start audio capture immediately (ie., with minimum possible delay on % your system), return a 'startTimeBox' timestamp in Datapixx clock time of % when capture actually started or will start, taking the 'startOffsetSecs' % into account. You can also use the 2nd optional return argument % 'startTimeGetSecs' to get the same timestamp mapped to Psychtoolbox % GetSecs() time. For a more precise mapping to GetSecs time, you can % convert the 'startTimeBox' timestamp into Psychtoolbox GetSecs() time % with the proper mapping functions of PsychDataPixx (see "help % PsychDataPixx"). % % The optional 'startOffsetSecs' allows to delay the start of the capture % operation by 'startOffsetSecs' seconds. The optional 'maxDurationSecs' % allows to define an upper limit onto the duration of the audio capture % operation. The operation will stop automatically after the given number % of seconds. By default, capture will run for 30 seconds. For % robustness and efficiency, you should specify a reasonable upper limit if % possible. 'bufferBaseAddress' is the memory start address of the audio % buffer for capture, 'numBufferFrames' is the size of the buffer. See % "Datapixx SetAudioSchedule?" for more info. Leave at default settings if % possible. % % % DatapixxAudioKey('CaptureAtFlip'[, flipCount=next][, maxDurationSecs=30][, startOffsetSecs=0][,bufferBaseAddress=20e6] [, numBufferFrames=maxScheduleFrames]); % - Schedule start of audio capture synchronized to the visual stimulus % onset of a future Screen('Flip') or Screen('AsyncFlipBegin') command. % All parameters are identical to the ones for DatapixxAudioKey('CaptureNow',...), % except for the first optional parameter 'flipCount'. 'flipCount' defines % at which Screen('Flip') audio capture should be started. If omitted or % set to zero or [], capture will be triggered by the next flip command. % Otherwise it will be triggered by the flip command with the given % 'flipCount'. You can query the current flipCount via PsychDatapixx('FlipCount'). % % Example: Setting flipCount = PsychDatapixx('FlipCount') + 10; would start % audio capture at the 10'th invocation of a flip command from now. % % This function doesn't return a capture start timestamp as capture will % only happen in the future, so the timestamp is only available in the % future. % % % [audiodata, onsetTimeSecs] = DatapixxAudioKey('GetResponse'[, amountSecs=current][, blocking=1][, stopCapture=0]); % - Try to fetch available captured audiodata from a running capture % operation. Only call this function after a capture operation has been % started via DatapixxAudioKey('CaptureNow') or scheduled for start at a % certain flipCount via DatapixxAudioKey('CaptureAtFlip') and the flip is % imminent, i.e., after the flip command for that flip has been called % already. The function will error-out if you call it too early! % % The optional 'amountSecs' asks the driver to return exactly % 'amountSecs' worth of audio data. By default it will return whatever % is available at time of call. If 'amountSecs' is specified, but the % requested amount is not yet available, the optional 'blocking' flag will % define behaviour: If set to 1 (default), the driver will wait until the % specified amount becomes available. If set to 0, the driver will return % with empty [] return arguments so you can retry later. The optional % 'stopCapture' flag if set to 1 will stop audio capture, its default % setting of zero will keep capture running until manually stopped or until % it stops by itself. % % 'audiodata' is the vector or matrix of captured audiodata. 1 Row for mono % recording, or a 2-row matrix (one row for each audio channel) in stereo % recording modes. Each value is an audio signal sample in range [-1 ; 1]. % % 'onsetTimeSecs' is the time since start of audio capture when a certain % level of loudness (as set by DatapixxAudioKey('TriggerLevel')) was first % exceeded, e.g., due to onset of a vocal response of a subject. % % 'readOffset' is the sample index of the next sample that will be read on % the next call to this function, if any. % % % oldLevel = DatapixxAudioKey('TriggerLevel' [, newLevel]); % - Return old and optionally set new trigger threshold level for the % timestamping of onset of audio signals in DatapixxAudioKey('GetResponse'). % % 'oldLevel' is the current/old level. 'newLevel' is the optional new % level. Level can be between 0 and 1, with a default level of 0.1 for 10% % of max signal intensity as trigger level. % % % DatapixxAudioKey('AutoTriggerLevel', silenceSecs); % - Auto-Select trigger threshold level for audio onset timestamping. After % start of audio capture, there must be a period of silence, where the only % signal recorded is due to noise in the microphone/amplifiers etc, not due % to actual signal. You must provide the minimum duration of this time % period of silence as 'silenceSecs' parameter, e.g., 1.5 for 1.5 seconds % of guaranteed silence at start of recording. During recording, the % function will use this chunk of silence to compute an optimal trigger % level, which is 10% louder than the loudest sample in the silence block % and then use that triggerlevel as if DatapixxAudioKey('TriggerLevel') had % been called with that auto-selected level. % % % DatapixxAudioKey('StopCapture'); % - Stop audio capture as soon as possible. % % persistent dpixaudioin; if nargin < 1 || isempty(cmd) error('Required Subcommand missing or empty!'); end if isempty(dpixaudioin) || (dpixaudioin.refcount == 0) % Driver closed at startup: dpixaudioin.refcount = 0; dpixaudioin.samplerate = 0; dpixaudioin.pendingForFlip = -1; dpixaudioin.triggerLevel = 0.1; dpixaudioin.readOffset = 0; dpixaudioin.lrMode = 0; end if strcmpi(cmd, 'Open') % Open the audiokey: Performs basic init and setup of audio input % subsystem. if dpixaudioin.refcount > 0 error('Open: Tried to open DatapixxAudioKey, but audio key already open!'); end % Open device connection if not already open: PsychDataPixx('Open'); if PsychDataPixx('IsBusy') error('Open: Tried to open DatapixxAudioKey, but Datapixx is busy! Screen flip pending?'); end % Stop potentially running audio input schedule: Datapixx('StopMicrophoneSchedule'); % TODO FIXME: How to decide if this is needed or harmful? Datapixx('InitAudio'); % Disable loopback: Datapixx('DisableAudioLoopback'); % Execute: Datapixx('RegWrRd'); micstatus = Datapixx('GetMicrophoneStatus'); % Assign new optional sampleRate: if length(varargin) >= 1 && ~isempty(varargin{1}) dpixaudioin.samplerate = varargin{1}; else dpixaudioin.samplerate = micstatus.scheduleRate; end % Assign new optional lrMode: if length(varargin) >= 2 && ~isempty(varargin{2}) dpixaudioin.lrMode = varargin{2}; else dpixaudioin.lrMode = micstatus.lrMode; end % Assign new optional input source: if length(varargin) >= 3 && ~isempty(varargin{3}) inputjack = varargin{3}; else % Leave current input setting "as is": inputjack = micstatus.source; % If no input set at all, default to microphone input: if inputjack == 0 inputjack = 1; end end % Assign new optional input gain: if length(varargin) >= 4 && ~isempty(varargin{4}) inputgain = varargin{4}; else inputgain = micstatus.gain; end % Assign audio input source and input gain: Datapixx('SetMicrophoneSource', inputjack, inputgain); Datapixx('RegWrRd'); % Mark us as open: dpixaudioin.refcount = dpixaudioin.refcount + 1; return; end if strcmpi(cmd, 'Close') if dpixaudioin.refcount <= 0 % Noop: dpixaudioin.refcount = 0; return; end if PsychDataPixx('IsBusy') error('Close: Tried to close DatapixxAudioKey, but Datapixx is busy! Screen flip pending?'); end % Stop potentially running audio input schedule: Datapixx('StopMicrophoneSchedule'); % Execute: Datapixx('RegWrRd'); % Close our connection: PsychDataPixx('Close'); % Closed: dpixaudioin.refcount = 0; return; end % All following commands need the audiokey to be open: if dpixaudioin.refcount <= 0 error('Tried to use audiokey, but audiokey not open!'); end if strcmpi(cmd, 'CaptureNow') if dpixaudioin.pendingForFlip > -1 error('CaptureNow: Tried to start or engage audiokey, but audiokey already started or engaged!'); end if length(varargin) >= 1 && ~isempty(varargin{1}) maxScheduleFrames = varargin{1} * dpixaudioin.samplerate; else % Default to 30 seconds if nothing provided: maxScheduleFrames = 30 * dpixaudioin.samplerate; end if length(varargin) >= 2 && ~isempty(varargin{2}) scheduleOffsetSecs = varargin{2}; else scheduleOffsetSecs = 0; end if PsychDataPixx('IsBusy') error('CaptureNow: Datapixx is busy! Screen flip pending?'); end % Stop potentially running audio input schedule: Datapixx('StopMicrophoneSchedule'); Datapixx('RegWrRd'); % Init audio input schedule: if length(varargin) >= 3 Datapixx('SetMicrophoneSchedule', scheduleOffsetSecs, dpixaudioin.samplerate, maxScheduleFrames, dpixaudioin.lrMode, varargin{3:end}); else Datapixx('SetMicrophoneSchedule', scheduleOffsetSecs, dpixaudioin.samplerate, maxScheduleFrames, dpixaudioin.lrMode); end % Reset readOffset in audio capture buffer: dpixaudioin.readOffset = 0; % Start audio input schedule: Datapixx('StartMicrophoneSchedule'); % Execute: Datapixx('RegWrRd'); if nargout > 0 % Return boxtime of start of capture: tStartBox = Datapixx('GetTime'); varargout{1} = tStartBox + scheduleOffsetSecs; end if nargout > 1 % Return GetSecs mapped time as well: tStartGetSecs = PsychDataPixx('FastBoxsecsToGetsecs', tStartBox); varargout{2} = tStartGetSecs; end % Mark capture as started: dpixaudioin.pendingForFlip = 0; return; end if strcmpi(cmd, 'CaptureAtFlip') if dpixaudioin.pendingForFlip > -1 error('CaptureAtFlip: Tried to start or engage audiokey, but audiokey already started or engaged!'); end if length(varargin) < 1 || isempty(varargin{1}) || varargin{1} == 0 targetFlip = []; else targetFlip = varargin{1}; end if targetFlip <= PsychDataPixx('FlipCount') % targetFlip set to a count that is already over. Pointless! Set it % to "next flip" instead: targetFlip = []; end if length(varargin) >= 2 && ~isempty(varargin{2}) maxScheduleFrames = varargin{2} * dpixaudioin.samplerate; else % Default to 30 seconds if nothing provided: maxScheduleFrames = 30 * dpixaudioin.samplerate; end if length(varargin) >= 3 && ~isempty(varargin{3}) scheduleOffsetSecs = varargin{3}; else scheduleOffsetSecs = 0; end if PsychDataPixx('IsBusy') error('CaptureAtFlip: Datapixx is busy! Screen flip pending?'); end % TODO FIXME: Should stop and configuration of schedule be part of the % command packet to be executed at flip? Would provide the ability to % "preprogram" complex sequences of audio response collection, but make % overall design much more complex? % % For now we go with the simple solution: % Stop potentially running audio input schedule: Datapixx('StopMicrophoneSchedule'); % Init audio input schedule: if length(varargin) >= 4 Datapixx('SetMicrophoneSchedule', scheduleOffsetSecs, dpixaudioin.samplerate, maxScheduleFrames, dpixaudioin.lrMode, varargin{4:end}); else Datapixx('SetMicrophoneSchedule', scheduleOffsetSecs, dpixaudioin.samplerate, maxScheduleFrames, dpixaudioin.lrMode); end % Execute: Datapixx('RegWrRd'); % Reset readOffset in audio capture buffer: dpixaudioin.readOffset = 0; % Build command string for psync-triggered start of schedule at target % flipcount: cstr = 'Datapixx(''StartMicrophoneSchedule'');'; % Schedule cstr for execution at stimulus onset for the 'targetFlip'th % flip command execution: PsychDataPixx('ExecuteAtFlipCount', targetFlip, cstr); dpixaudioin.pendingForFlip = targetFlip; % Ready. return; end if strcmpi(cmd, 'StopCapture') if PsychDataPixx('IsBusy') error('StopCapture: Datapixx is busy! Screen flip pending?'); end % Submit stop command immediately: Datapixx('StopMicrophoneSchedule'); Datapixx('RegWrRd'); % Reset flag: dpixaudioin.pendingForFlip = -1; return; end if strcmpi(cmd, 'TriggerLevel') varargout{1} = dpixaudioin.triggerLevel; if length(varargin) >= 1 && ~isempty(varargin{1}) dpixaudioin.triggerLevel = varargin{1}; end return; end if strcmpi(cmd, 'AutoTriggerLevel') % Assign value in seconds as negative number: dpixaudioin.triggerLevel = -1 * varargin{1}; return; end if strcmpi(cmd, 'GetResponse') if dpixaudioin.pendingForFlip < 0 error('GetResponse: Tried to get response from audiokey, but audiokey not active or engaged!'); end % If audio recording was scheduled to start in sync with a certain % Screen flip, check if that specific target flip count has been % reached: if ~isempty(dpixaudioin.pendingForFlip) && (dpixaudioin.pendingForFlip > 0) && (PsychDataPixx('FlipCount') < dpixaudioin.pendingForFlip) % Audio key shall start recording at a certain flipcount which % likely hasn't been reached yet (according to PsychDataPixx('FlipCount')). % % Multiple options: % % 1 We're more than one flip away from startpoint. In that case no % point waiting for start, as usercode would first need to % execute some Screen('Flip/AsyncFlipBegin') command to make it % even possible to start audio capture. % % 2 We're exactly one flip away and... % a) No flip command submitted by usercode -> No way this is % gonna fly. % % b) Asyncflip command submitted: Flip will eventually happen and % get us going. This is the case if the device is marked as "busy % waiting for psync" and async flip is marked as active. % % In case 1 or 2a we can't wait for a response, whereas in case 2b % we can wait for a response as it will eventually happen. % Check for case 1: if PsychDataPixx('FlipCount') < dpixaudioin.pendingForFlip - 1 % More than 1 flip away. That's a no-no: fprintf('DatapixxAudioKey: Start of audio input scheduled for flipcount %i, but flipcount not yet reached.\n', dpixaudioin.pendingForFlip); fprintf('DatapixxAudioKey: Current count %i is more than 1 flip away! Impossible to "GetResponse" from device this way!\n', PsychDataPixx('FlipCount')); error('GetResponse: Tried to get response from audiokey, but audiokey not yet active and impossible to activate by this command-sequence!'); end % Check for case 2b: if PsychDataPixx('IsBusy') && Screen('GetWindowInfo', PsychDataPixx('WindowHandle'), 4) % Asyncflip with Psync op pending. The flip will eventually % happen at some point in the future, so we can enter a polling % loop to wait for it to happen. else % 1 flip away but no flip scheduled to actually reach that % trigger-flip: fprintf('DatapixxAudioKey: Start of audio input scheduled for flipcount %i, but flipcount not yet reached!\n', dpixaudioin.pendingForFlip); fprintf('DatapixxAudioKey: Current count %i. Impossible to "GetResponse" from device this way,\n', PsychDataPixx('FlipCount')); fprintf('DatapixxAudioKey: because no flip has been scheduled to reach the trigger flipcount!\n'); error('GetResponse: Tried to get response from audiokey, but audiokey not yet active and impossible to activate by this command-sequence!'); end end if length(varargin) < 2 || isempty(varargin{2}) blocking = 1; else blocking = varargin{2}; end % Ok, should be already running or it is at least certain that it will % eventually run due to trigger by psync mechanism. % % Enter a polling loop to wait until the requested minimum amount of % audio data is available: if length(varargin) >= 1 && ~isempty(varargin{1}) % Wait or poll for requested amount of audiodata: numFrames = ceil(varargin{1} * dpixaudioin.samplerate); while 1 Datapixx('RegWrRd'); micstatus = Datapixx('GetMicrophoneStatus'); if micstatus.newBufferFrames >= numFrames % Requested amount of audio data available: Exit loop. break; else % Insufficient amount. If this is a polling request, we % simply return no result: if ~blocking varargout{1} = []; varargout{2} = []; return; end end % Sleep a msec, then retry: WaitSecs('YieldSecs', 0.001); end else % Just get current status - current amount of audiodata: Datapixx('RegWrRd'); micstatus = Datapixx('GetMicrophoneStatus'); numFrames = -1; end % Either requested amount of audio data available or no specific amount % requested. Stop schedule if requested by usercode: if length(varargin) >= 3 && ~isempty(varargin{3}) && varargin{3} == 1 Datapixx('StopMicrophoneSchedule'); Datapixx('RegWrRd'); dpixaudioin.pendingForFlip = -1; micstatus = Datapixx('GetMicrophoneStatus'); end % Fetch all captured frames if no specific amount requested: if numFrames == -1 numFrames = micstatus.newBufferFrames; end % Retrieve Datapixx internal delay, so we can correct for it: inDelaySecs = Datapixx('GetMicrophoneGroupDelay', dpixaudioin.samplerate); % Retrieve recorded data: audiodata = Datapixx('ReadMicrophoneBuffer', ceil(numFrames), -1); % Compute timestamp in seconds since start of capture of when the % triggerLevel was exceeded the first time: if dpixaudioin.triggerLevel >= 0 % Specific triggerLevel given. Use "as is": triggerTime = (dpixaudioin.readOffset + min(find(abs(audiodata(1,:)) > dpixaudioin.triggerLevel))) / dpixaudioin.samplerate; %#ok else % Auto-Trigger mode. Leadtime of silence in seconds given. We can % use that many seconds of signal at the beginning, knowing it % represents silence, to determine an optimal triggerLevel. We set % a level that is 10% more than the "loudest" signal sample in the % "silence" lead-in block of samples: triggerLevel = 1.1 * max(abs(audiodata(1, 1:floor(dpixaudioin.samplerate * abs(dpixaudioin.triggerLevel))))); if triggerLevel > 0.9 warning('Psychtoolbox:DatapixxAudioKey:AutoTriggerHigh', 'Auto-Selected triggerLevel %f exceeds 90% of max possible signal amplitude. Strong noise or something wrong with your setup?!?', triggerLevel); end % Do trigger threshold seeking like above: triggerTime = (dpixaudioin.readOffset + min(find(abs(audiodata(1,:)) > triggerLevel))) / dpixaudioin.samplerate; %#ok end % Compensate for Datapix hw delays: triggerTime = triggerTime - inDelaySecs; % Increment readOffset into audio capture buffer: dpixaudioin.readOffset = dpixaudioin.readOffset + size(audiodata, 2); % Return all data: varargout{1} = audiodata; varargout{2} = triggerTime; varargout{3} = dpixaudioin.readOffset; return; end error('DatapixxAudioKey: Unknown subcommand provided!');