/PsychHardware/DatapixxToolbox/DatapixxBasic/DatapixxAudioKey.m
MATLAB | 519 lines | 227 code | 75 blank | 217 comment | 46 complexity | 3eed6ae55789f14f42e1164b256a3975 MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause, LGPL-2.0
- 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('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, '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 (dpixaudioin.pendingForFlip > 0) && (PsychDataPixx('FlipCount') < dpixaudioin.pendingForFlip)
- if (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
- % TODO FIXME: Does this make sense to use here?
- 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:
- triggerTime = (dpixaudioin.readOffset + min(find(abs(audiodata(1,:)) > dpixaudioin.triggerLevel))) / dpixaudioin.samplerate; %#ok<MXFND>
- 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!');