File: BasicSoundPhaseShiftDemo.m

package info (click to toggle)
psychtoolbox-3 3.0.19.14.dfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 86,796 kB
  • sloc: ansic: 176,245; cpp: 20,103; objc: 5,393; sh: 2,753; python: 1,397; php: 384; makefile: 193; java: 113
file content (278 lines) | stat: -rw-r--r-- 12,441 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
function BasicSoundPhaseShiftDemo(showit, targetChannel)
% BasicSoundPhaseShiftDemo([showit=1][, targetChannel=1])
%
% Demonstrates how one can play back a phase-shifted sine tone, with dynamically
% adjustable phase shift, free of audible artifacts during phase shift change.
%
% This uses PsychPortAudio's realtime mixing and some trigonometric math to
% synthesize a cosine wave of selectable phase in realtime from the weighted sum
% of a pair of a cosine + sine wave with different relative amplitudes.
%
% The sine wave is put into one sound channel. A cosine wave of same frequency
% (but 90 degrees phase-shifted) is put in another sound channel. Both sound
% channels are mixed together in realtime by PsychPortAudio, with controllable
% relative volume (== amplitude == mix-weight) for each of the two channels, and
% the result of this two-channel weighted mix is output to the 2nd audio output
% channel of the actual soundcard, creating a synthesized phase-shifted cosine
% wave of same frequency. Changing the relative channel volumes changes the phase
% shift of the cosine output wave in realtime, without artifacts / discontinuities.
% The proper relative volume levels are computed from the desired phase shift and
% set in the internal subfunction updatePhase() by the magic of math.
%
% A second reference sine wave is output to the 1st audio channel of the soundcard,
% if 'targetChannel' == 1, so one can drive two speakers / transducers, one outputting
% a phase-shifted sine wave, relative to th other speaker / transducer. For testing
% purposes, one can set 'targetChannel' == 2 to mix the reference sine-wave with the
% phase-shifted cosine wave both into output speaker channel 2, to simulate potential
% constructive / destructive interference between reference and phase-shifted sine
% wave. Setting 'showit' == 1 will capture the audio signal send to both soundcard
% channel and visualize it in a Psychtoolbox onscreen window, for illustrative purposes
% and for basic debugging. If you want to run a real auditory experiments, you'd use
% 'targetChannel' == 1 and some external microphones and oscillograph to check for
% proper audio output independently of PsychPortAudio, Psychtoolbox and your computer.
%
% One use case for this are studies as described under this link...
% https://psychtoolbox.discourse.group/t/can-i-change-the-phase-of-a-sine-wave-with-psychportaudio/4086
% ...more specifically experiments like this one about air conduction and bone conduction
% of sound:
%
% https://www.researchgate.net/publication/6535047_Simultaneous_cancellation_of_air_and_bone_conduction_tones_at_two_frequencies_Extension_of_the_famous_experiment_by_von_Bekesy
%
% Have a look at BasicAMAndMixScheduleDemo on how to apply amplitude modulation (AM)
% of signals by attaching AM modulator slave devices to the pafixedsine and pashiftsine
% slave devices used here. A suitable time series of AM envelope values would allow to
% gate the output sine waves.
%
% This demo was sponsored by a paid support request. Thanks!
%
% Usage:
%
% ESCAPE key ends the demo.
%
% Left and right cursor keys allow to shift phase interactively.
%
% Optional parameters:
%
% 'showit' If set to 1, visualize actual output signals, channel 1 in red,
%          channel 2 in green. Defaults to 1 for showing output.
%
% 'targetChannel' To which channel should the fixed phase reference signal be
%                 output? 1 = channel 1 (default). 2 = channel 2 will output into
%                 the same channel as the phase-shifted signal, so both signals
%                 will show constructive or destructive interference, depending
%                 on phase-shift of the 2nd signal which always goes to channel 2.

% History:
% 29-Oct-2021 mk  Written.

  if nargin < 1 || isempty(showit)
    showit = 1;
  end

  if nargin < 2 || isempty(targetChannel)
    targetChannel = 1;
  else
    if ~ismember(targetChannel, [1,2])
      error('targetChannel argument must be 1 or 2.');
    end
  end

  % Setup defaults:
  PsychDefaultSetup(2);

  % Define control keys:
  ESCAPE = KbName('ESCAPE');
  leftArrow = KbName('LeftArrow');
  rightArrow = KbName('RightArrow');

  % Perform basic initialization of the sound driver:
  InitializePsychSound;

  % Number of physical channels to use on the real soundcard:
  nrchannels = 2;

  % Frequency of waves:
  freq = 500;

  % Initial phase shift in degrees of cosine wave:
  phase = 90;

  % Open and start physical sound card for playback (1) in master mode (8) = (1+8),
  % with low-latency and high timing precision (1), with auto-selected default []
  % samplingRate for device, to consume and output mixed sound from slaves to
  % 'nrchannels' physical output channels:
  pamaster = PsychPortAudio('Open', [], 1 + 8, 1, [], nrchannels);

  % Retrieve auto-selected samplingRate:
  status = PsychPortAudio('GetStatus', pamaster);
  samplingRate = status.SampleRate;

  % Compute minimum length 'wavedur' seconds of a sound buffer with one or
  % more repetitions of the freq Hz sine wave. If you wanted sound signals
  % of defined length, e.g., also to allow things like applying an AM
  % envelope function to it, by use of AM modulator slave devices, you
  % could just set wavedur to the desired sound duration of the total sound
  % vector. This here is just for memory efficiency...
  %
  % For the minimum duration 'wavedur', make sure to increase wavedur to
  % generate multiple period repetitions of the (co)sine wave ih order to
  % make it fit an integral number of samples, in case one period would
  % need a non-integral number of samples. If nothing else, it may help
  % visualization or debugging / reasoning about it: The if statement is
  % executed, e.g., for combinatios of 'freq' 500 Hz and samplingRate of
  % 44100 samples/sec, where one period of a sine wave would require 88.2
  % samples, ie a non-integral number. Repeating the wave 5x by increasing
  % wavedur * 5, ends up with 88.2 * 5 = 441 samples creating one sound
  % playback buffer with a even number of samples.
  wavedur = 1 / freq;
  nsamples = wavedur * samplingRate;
  if rem(nsamples, 1)
    wavedur = wavedur * 1 / rem(nsamples, 1);
  end

  % Define input vector 'support' for the sin() and cos() functions. Playback of
  % a 'freq' Hz pure sine tone at a sampling rate of 'samplingRate'. We create a
  % waveform of 'wavedur' duration, so that full periods of the sine / cosine fit
  % nicely for inifinitely looped playback, without discontinuities at the beginning
  % and end of the vector. If you also wanted to amplitude modulate the signals with
  % some envelope function, you'd have to create a longer 'support' vector, which
  % repeats the waves for not just one period, but often enough to cover the whole
  % signal duration for the amplitude envelope:
  support = 2 * pi * freq * (0:round(wavedur * samplingRate-1)) / samplingRate;

  if showit
    paoutputcapture = PsychPortAudio('OpenSlave', pamaster, 2 + 64);
    PsychPortAudio('GetAudioData', paoutputcapture, 1);
    PsychPortAudio('Start', paoutputcapture);
    win = PsychImaging('OpenWindow', 0, 0, [0, 0, Screen('Windowsize', 0), 200], [], [], [], [], [], kPsychGUIWindow + kPsychGUIWindowWMPositioned);
  end

  % Start master, wait (1) for start, return sound onset time in startTime.
  % Slaves can be independently controlled in their timing, volume, content thereafter:
  startTime = PsychPortAudio('Start', pamaster, [], [], 1);

  % Create slave device for infinite duration fixed sine tone of freq Hz playback (1).
  % It will output one audio channel (1) via audio channel 'targetChannel' (targetChannel)
  % of the real soundcard:
  pafixedsine = PsychPortAudio('OpenSlave', pamaster, 1, 1, targetChannel);

  % Sine tone, freq Hz:
  PsychPortAudio('FillBuffer', pafixedsine, 0.5 * sin(support));

  % Start playback with infinite (0) repetition of the 1 second sound signal,
  % at time startTime + 1 second:
  PsychPortAudio('Start', pafixedsine, 0, startTime + 1);

  % Create slave device for infinite duration phase-shiftable cosine tone of freq Hz playback (1).
  % It will output via audio channel 2 of the real soundcard. For this, we let the pashiftsine
  % slave device have two (2) sound channels, both feeding their sound as a mix into the 2nd channel
  % of the pamaster ([2, 2]):
  pashiftsine = PsychPortAudio('OpenSlave', pamaster, 1, 2, [2, 2]);

  % First pashiftsine channel for the mix has a cos() wave, second channel has a sin() wave,
  % 90 degrees phase-shifted. Both waves will be mixed together and output via the 2nd
  % master channel, but with different amplitude / volume. The weighted sum of both 90 degrees
  % shifted waves yields a (co)sine wave of 'freq' Hz, with a phase shifted according to the
  % relative amplitude of both waves. See subfunction updatePhase() for the math behind this:
  srcmixtones = [cos(support) ; sin(support)];
  PsychPortAudio('FillBuffer', pashiftsine, srcmixtones);

  % updatePhase() computes proper relative volumes / amplitudes for the two waves and
  % assign it as channel-volumes to the pashiftsine virtual audio device, so the mix
  % results in a 'phase' shifted cosine wave of 'freq' Hz:
  updatePhase(phase, pashiftsine);

  % Start infinite playback of the sound wave, with a peak volume of 50% aka 0.5,
  % at time startTime + 1 second:
  PsychPortAudio('Volume', pashiftsine, 0.5);
  PsychPortAudio('Start', pashiftsine, 0, startTime + 1);

  % Loop for keyboard checking and phase adjustment:
  while 1
    [down, ~, keys] = KbCheck(-1);
    if down
      if keys(ESCAPE)
        break;
      end

      if keys(rightArrow)
        % Phase-shift increase by 1 degree:
        phase = mod(phase + 1, 360);
        updatePhase(phase, pashiftsine);
      end

      if keys(leftArrow)
        % Phase-shift decrease by 1 degree:
        phase = mod(phase - 1, 360);
        updatePhase(phase, pashiftsine);
      end
    end

    if showit
      % Get exactly 0.1 seconds of captured sound:
      recorded = PsychPortAudio('GetAudioData', paoutputcapture, [], 0.1, 0.1);

      % Plot both audio tracks into the window:
      xpos = 1:size(recorded, 2);
      xpos = [xpos xpos(end:-1:1)];
      recorded = [recorded(:, 1:end) recorded(:, end:-1:1)];
      Screen('FramePoly', win, [1 0 0], [xpos' , [100 + recorded(1,:) * 100]']);
      Screen('FramePoly', win, [0 1 0], [xpos' , [100 + recorded(2,:) * 100]']);

      % Show it: Do not sync us to stimulus onset, so we won't slow down to display
      % refresh rate.
      Screen('Flip', win, [], [], 1);
    else
      % Nap a bit, so phase doesn't change that quickly - 50 msecs should do:
      WaitSecs('YieldSecs', 0.050);
    end
  end

  % Stop and close down everything audio related:
  PsychPortAudio('Close');
  if showit
    % Close window:
    sca;
  end

  % Optional plotting code:
  % close all;
  % plot(1:length(recorded), recorded(1,:), 'r', 1:length(recorded), recorded(2,:), 'b');

  % Done, bye bye.
end

function updatePhase(phase, pahandle)
  % Convert phase in degrees into radians:
  shift = phase * pi / 180;

  % Compute relative amplitudes / volumes for both waves, for a resulting
  % wave of amplitude 1.0 and 'phase' degrees (aka shift radians) shift:
  %
  % From https://cpb-us-e1.wpmucdn.com/cobblearning.net/dist/d/1007/files/2013/03/Linear-Combinations-and-Sum-Difference-1xsp3e8.pdf
  % we learn the following trick:
  %
  % Given a1 and a2, there is the following equivalence for the weighted sum of
  % a sine and cosine wave of identical frequency x = 2*pi*freq*t:
  %
  % a1 * cos(x) + a2 * sin(x) = A * cos(x - shift) with
  % A = sqrt(a1^2 + a2^2); and shift = arctan(a2 / a1)
  %
  % so for a desired 'shift' it follows that
  % a2 / a1 = tan(shift)  and as we know that tan(shift) = sin(shift) / cos(shift)
  %
  % therefore:
  %
  % a2 = sin(shift), and a1 = cos(shift), and
  % A = sqrt(sin(shift)^2 + cos(shift)^2) = sqrt(1) = 1, ie. A = 1.
  %
  a1 = cos(shift);
  a2 = sin(shift);

  % Assign new volumes / amplitudes atomically. As channel 1 of pahandle contains
  % a cos(x) wave and channel 2 contains a sin(x) wave, the mixing in PsychPortAudio
  % will create the weighted sum a1 * cos(x) + a2 * sin(x), which is equivalent
  % to an audio signal of cos(x - shift), ie. a cosine wave with 'shift' phase shift:
  PsychPortAudio('Volume', pahandle, [], [a1, a2]);
  fprintf('New phase: %f degrees.\n', phase);
end