为什么我的呈现音频刺激的脚本具有不可预测的行为?

why my script to present audio stimuli has an unpredictable behaviour?

我正在尝试 运行 使用 python 进行一个简单的实验。我想展示两种不同类型的音频刺激,一种是高音,一种是低音。较高的音调具有固定的持续时间 200ms,而较低的音调成对出现,第一个具有固定的持续时间 250ms,第二个具有可变的持续时间,可以采用以下值 [.4, .6, .8, 1, 1.2].我需要知道刺激开始和结束的时间(机器),以及它们的持续时间(精度不是最重要的问题,我有 ~ 10ms 的容忍度),因此我记录了这些信息

我正在使用库 audiomath 来创建和呈现刺激,并且我已经创建了几个自定义函数来管理任务的其他方面。我有 3 个脚本:一个在其中定义函数,一个在其中为每个主题设置实验的特定参数(source),一个带有 main()

我的问题是 main() 工作不稳定:有时它会工作,有时它似乎进入了无限循环并且出现了某种声音并且从不停止播放。关键是这种行为似乎真的是随机的,即使使用完全相同的参数,问题也会在不同的试验中出现,或者根本不会出现。

这是我的代码:

源文件

#%%imports
from exp_funcs import tone440Hz, tone880Hz
import numpy as np

#%%global var
n_long = 10
n_short = 10
short_duration = .2
long_durations = [.4, .6, .8, 1, 1.2]
#%%calculations
n_tot = n_long + n_long
trial_types = ['short_blink'] * n_short + ['long_blink'] * n_long
sounds = [tone880Hz] * n_short + [tone440Hz] * n_long
np.random.seed(10)
durations = [short_duration] * n_short + [el for el in np.random.choice(long_durations, n_long)]
durations = [.5 if el < .2 else el for el in durations]
cue_duration = [.25] * n_tot
spacing = [1.25] * n_tot
np.random.seed(10)
iti = [el for el in (3 + np.random.normal(0, .25, n_tot))]

函数

import numpy as np
import audiomath as am
import time
import pandas as pd
TWO_PI = 2.0 * np.pi
    
@am.Synth(fs=22050)
def tone880Hz(fs, sampleIndices, channelIndices):
    timeInSeconds = sampleIndices / fs
    return np.sin(TWO_PI * 880 * timeInSeconds)

@am.Synth(fs=22050)
def tone440Hz(fs, sampleIndices, channelIndices):
    timeInSeconds = sampleIndices / fs
    return np.sin(TWO_PI * 440 * timeInSeconds)

def short_blink(sound, duration):
        p = am.Player(sound) 
        init = time.time()
        while time.time() < init + duration:
            p.Play()
        end = time.time()
        p.Stop()
        print(f'start {init} end {end} duration {end - init}')
        return(init, end, end - init)
    
def long_blink(sound, duration, cue_duration, spacing):
    p = am.Player(sound) 
    i_ = time.time()
    while time.time() < i_ + cue_duration:
        p.Play()
    p.Stop()
    time.sleep(spacing)
    init = time.time()
    while time.time() < init + duration:
        p.Play()
    end = time.time()
    p.Stop()
    print(f'start {init} end {end} duration {end - init}')
    return(init, end, end - init)
    
def run_trial(ttype, sound, duration, cue_duration, spacing):
    if ttype == 'short_blink':
        init, end, effective_duration = short_blink(sound, duration)
    else:
        init, end, effective_duration = long_blink(sound, duration,
                                     cue_duration, spacing)
    otp_df = pd.DataFrame([[ttype, init, end, effective_duration]],
                          columns = ['trial type', 'start', 'stop',
                                     'effective duration'])
    return(otp_df)

主要

import pandas as pd
import sys
import getopt
import os
import time
import random
from exp_funcs import run_trial
from pathlib import PurePath


def main(argv):
   try:
      opts, args = getopt.getopt(argv,'hs:o:',['help', 'source_file=', 'output_directory='])
   except getopt.GetoptError:
      print ('experiment.py -s source file -o output directory')
      sys.exit(2)
   for opt, arg in opts:
      if opt == '-h':
         print ('experiment.py -s source file')
         sys.exit()
      elif opt in ("-s", "--source_file"):
         source_file = arg
      elif opt in ("-o", "--output_directory"):
         output_dir = arg
   os.chdir(os.getcwd())
   if not os.path.isfile(f'{source_file}.py'):
        raise FileNotFoundError('{source_file} does not exist')
   else:
        source = __import__('source')
   complete_param = list(zip(source.trial_types,
                             source.sounds,
                             source.durations,
                             source.cue_duration,
                             source.spacing, 
                             source.iti))
   # shuffle_param = random.sample(complete_param, len(complete_param))
   shuffle_param = complete_param
   dfs = []
   for ttype, sound, duration, cue_duration, spacing, iti in shuffle_param:
       time.sleep(iti)
       df = run_trial(ttype, sound, duration, cue_duration, spacing)
       dfs.append(df)
   dfs = pd.concat(dfs)
   dfs.to_csv(PurePath(f'{output_dir}/{source_file}.csv'), index = False)
   
if __name__ == "__main__":
   main(sys.argv[1:])

3个文件在同一个目录下,我在目录下用终端浏览,运行主要如下python experiment.py -s source -o /whatever/output/directory。 任何帮助将不胜感激

这也是一个 big/complex 程序,希望在 Whosebug 上获得有关非特定“不稳定”行为的帮助。您需要将其归结为一个行为异常的小型可重现示例。如果它有时有效而不是其他,则系统地关注导致它失败的条件。我确实尝试 运行 整个事情,但是在修复了一些丢失的导入之后,仍然存在未指定的“源文件”内容的问题。

所以我不知道你的具体问题是什么。但是,从 audiomath 和一般实时性能的角度来看,我当然可以确定一些您不应该做的事情:

  1. 虽然 Player 个实例被设计为在时间关键时刻播放、停止或操作,但它们(默认情况下)并非设计为在时间关键时刻创建和销毁.如果你想快速 create/destroy 它们,预先初始化一个持久的 Stream() 实例,并在创建 Player 时将其作为 stream 参数传递,如 https://audiomath.readthedocs.io/en/release/auto/Examples.html#play-sounds

  2. 如果您正在使用 Synth 实例,您可以利用它们的 .duration 属性,而不是在 while 循环中显式检查时钟。例如可以设置tone880Hz.duration = 0.5,然后与p.Play(wait=True)同步播放声音。你的时钟观察 while 循环的一个大问题是它们目前是“忙等待”循环,这会扰乱 CPU,可能会导致你的声音偶尔中断(Python' s 多线程远非完美)。但是,在解决此问题之前,您应该知道...

  3. 策略“Play(),等待,睡眠,Play()”永远不会实现一种刺激相对于另一种刺激的精确时间安排。首先,无论何时您在任何软件中发出播放声音的命令,在命令和声音的物理开始之间都不可避免地存在非零(并且随机变化!)延迟。其次,sleep() 不太可能像您认为的那样精确。这既适用于您一直用来创建间隙的 sleep(),也适用于 Play(wait=True) 将在内部使用的 sleep()。睡眠实现暂停操作“至少”指定的时间量,但它们不保证上限。这是非常依赖于硬件和 OS 的;在某些 Windows 系统上,您甚至可能会发现粒度 永远不会 比 10 毫秒更好。

如果你真的想使用 Synth 方法,我想你可以在程序上将间隙编程到 tone440Hz()tone880Hz() 的函数定义中,访问 cue_durationdurationspacing 作为全局变量(事实上,当你这样做的时候,为什么不把频率也做成一个全局变量,并且只写一个函数)。但是我看不出这有什么很大的优势,无论是在性能上还是在代码可维护性上。

我要做的是预初始化以下内容(一次,在程序开始时):

max_duration = 1  # length, in seconds, of the longest continuous tone we'll need

tone440Hz = am.Sound(fs=22050).GenerateWaveform(freq_hz=440, duration_msec=max_duration*1000)
tone880Hz = am.Sound(fs=22050).GenerateWaveform(freq_hz=880, duration_msec=max_duration*1000)

m = am.Stream()

然后使用您想要的参数将每个“长时间眨眼”刺激组合成静态 Sound。 这将确保音调和间隙持续时间精确:

s = tone440Hz[:cue_duration] % spacing % tone440Hz[:duration]

为了获得最佳的实时性能,您可以使用不同的参数预先计算出一整套这些刺激。或者,如果事实证明这些组合操作 (slicing and splicing) 发生得足够快,您可能会决定可以在试用时在 long_blink() 函数中不这样做。

无论哪种方式,在试用时播放刺激:

p = am.Player(s, stream=m)  # to make Player() initialization fast, use a pre-initialized Stream() instance
p.Play(wait=True)

最后:在实现这个过程中,从头开始——从简单开始,在复合之前测试几个简单案例的性能。