Scheduled service maintenance on November 22


On Friday, November 22, 2024, between 06:00 CET and 18:00 CET, GIN services will undergo planned maintenance. Extended service interruptions should be expected. We will try to keep downtimes to a minimum, but recommend that users avoid critical tasks, large data uploads, or DOI requests during this time.

We apologize for any inconvenience.

sound_chirp.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import numpy as np
  2. import time
  3. from scipy.signal import lfilter
  4. from functools import reduce
  5. import os
  6. import threading
  7. import random
  8. class SoundController:
  9. # https://python-sounddevice.readthedocs.io/en/0.3.15/api/streams.html#sounddevice.OutputStream
  10. default_cfg = {
  11. "device": [1, 26],
  12. "n_channels": 10,
  13. "sounds": {
  14. "noise": {"amp": 0.2, "channels": [6, 8]},
  15. "background": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": True, "channels": [3, 8]},
  16. "target": {"freq": 1320, "amp": 0.1, "duration": 0.05, "harmonics": True, "channels": [3, 8]},
  17. "distractor1": {"freq": 860, "amp": 0.15, "duration": 0.05, "harmonics": True, "channels": [6, 8], "enabled": False},
  18. "distractor2": {"freq": 1060, "amp": 0.25, "duration": 0.05, "harmonics": True, "channels": [6, 8], "enabled": False},
  19. "distractor3": {"freq": 1320, "amp": 0.2, "duration": 0.05, "harmonics": True, "channels": [6, 8], "enabled": False}
  20. },
  21. "pulse_duration": 0.05,
  22. "sample_rate": 44100,
  23. "latency": 0.25,
  24. "volume": 0.7,
  25. "roving": 5.0,
  26. "file_path": "sounds.csv"
  27. }
  28. commutator = {
  29. -1: 'noise',
  30. 0: 'silence',
  31. 1: 'background',
  32. 2: 'target',
  33. 3: 'distractor1',
  34. 4: 'distractor2',
  35. 5: 'distractor3',
  36. 6: 'distractor4',
  37. 7: 'distractor5'
  38. }
  39. @classmethod
  40. def get_pure_tone(cls, freq, duration, sample_rate=44100):
  41. x = np.linspace(0, duration * freq * 2*np.pi, int(duration*sample_rate), dtype=np.float32)
  42. return np.sin(x)
  43. @classmethod
  44. def get_harm_stack(cls, base_freq, duration, threshold=1500, sample_rate=44100):
  45. harmonics = [x * base_freq for x in np.arange(20) + 2 if x * base_freq < threshold] # first 20 enouch
  46. freqs = [base_freq] + harmonics
  47. x = np.linspace(0, duration, int(sample_rate * duration))
  48. y = reduce(lambda x, y: x + y, [(1./(i+1)) * np.sin(base_freq * 2 * np.pi * x) for i, base_freq in enumerate(freqs)])
  49. return y / y.max() # norm to -1 to 1
  50. @classmethod
  51. def get_cos_window(cls, tone, win_duration, sample_rate=44100):
  52. x = np.linspace(0, np.pi/2, int(win_duration * sample_rate), dtype=np.float32)
  53. onset = np.sin(x)
  54. middle = np.ones(len(tone) - 2 * len(x))
  55. offset = np.cos(x)
  56. return np.concatenate([onset, middle, offset])
  57. @classmethod
  58. def get_tone_stack(cls, cfg):
  59. # silence
  60. silence = np.zeros(2, dtype='float32')
  61. sounds = {'silence': np.column_stack([silence for x in range(cfg['n_channels'])])}
  62. # noise
  63. filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])
  64. filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])
  65. noise = np.random.randn(int(0.25 * cfg['sample_rate'])) # 250ms of noise
  66. noise = lfilter(filter_a, filter_b, noise)
  67. noise = noise / np.abs(noise).max() * cfg['sounds']['noise']['amp']
  68. noise = noise.astype(np.float32)
  69. empty = np.zeros((len(noise), cfg['n_channels']), dtype='float32')
  70. for ch in cfg['sounds']['noise']['channels']:
  71. empty[:, ch-1] = noise
  72. sounds['noise'] = empty
  73. # all other sounds
  74. for key, snd in cfg['sounds'].items():
  75. if key == 'noise' or ('enabled' in snd and not snd['enabled']):
  76. continue # skip noise or unused sounds
  77. if 'harmonics' in snd and snd['harmonics']:
  78. tone = cls.get_harm_stack(snd['freq'], snd['duration'], sample_rate=cfg['sample_rate']) * cfg['volume']
  79. else:
  80. tone = cls.get_pure_tone(snd['freq'], snd['duration'], cfg['sample_rate']) * cfg['volume']
  81. tone = tone * cls.get_cos_window(tone, 0.01, cfg['sample_rate']) # onset / offset
  82. tone = tone * snd['amp'] # amplitude
  83. sound = np.zeros([len(tone), cfg['n_channels']], dtype='float32')
  84. for j in snd['channels']:
  85. sound[:, j-1] = tone
  86. sounds[key] = sound
  87. return sounds
  88. @classmethod
  89. def run(cls, selector, status, cfg, commutator):
  90. """
  91. selector mp.Value object to set the sound to be played
  92. status mp.Value object to stop the loop
  93. """
  94. import sounddevice as sd # must be inside the function
  95. import numpy as np
  96. import time
  97. import soundfile as sf # chirp changes
  98. sounds = cls.get_tone_stack(cfg)
  99. sd.default.device = cfg['device']
  100. sd.default.samplerate = cfg['sample_rate']
  101. data, fs = sf.read(cfg['wav_file'], dtype='float32') #chirp changes
  102. chirp = np.zeros((len(data),cfg['n_channels']),dtype='float32') #chirp changes
  103. chirp[:,cfg['sounds']['target']['channels'][0]-1] = 0.07*data #chirp changes, amplitude for chirp=0.07, click=1.0
  104. chirp[:,6] = 0.07*data #chirp changes, amplitude for chirp=0.07, click=1.0
  105. stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=cfg['n_channels'], dtype='float32', blocksize=256)
  106. stream.start()
  107. next_beat = time.time() + cfg['latency']
  108. with open(cfg['file_path'], 'w') as f:
  109. f.write("time,id\n")
  110. while status.value > 0:
  111. if status.value == 2 or (status.value == 1 and selector.value == -1): # running state or masking noise
  112. t0 = time.time()
  113. if t0 < next_beat:
  114. #time.sleep(0.0001) # not to spin the wheels too much
  115. if stream.write_available > 2:
  116. stream.write(sounds['silence']) # silence
  117. continue
  118. roving = 10**((np.random.rand() * cfg['roving'] - cfg['roving']/2.0)/20.)
  119. roving = roving if int(selector.value) > -1 else 1 # no roving for noise
  120. # stream.write(sounds[commutator[int(selector.value)]] * roving) # chirp changes (uncomment->comment)
  121. stream.write(chirp) # chirp changes
  122. if status.value == 2:
  123. with open(cfg['file_path'], 'a') as f:
  124. f.write(",".join([str(x) for x in (t0, selector.value)]) + "\n")
  125. next_beat += cfg['latency']
  126. if stream.write_available > 2:
  127. stream.write(sounds['silence']) # silence
  128. else: # idle state
  129. next_beat = time.time() + cfg['latency']
  130. time.sleep(0.005)
  131. stream.stop()
  132. print('Sound stopped')
  133. class ContinuousSoundStream:
  134. default_cfg = {
  135. 'wav_file': os.path.join('..', 'assets', 'stream1.wav'),
  136. 'chunk_duration': 20,
  137. 'chunk_offset': 2
  138. }
  139. def __init__(self, cfg):
  140. from scipy.io import wavfile
  141. import sounddevice as sd
  142. self.cfg = cfg
  143. self.stopped = False
  144. self.samplerate, self.data = wavfile.read(cfg['wav_file'])
  145. self.stream = sd.OutputStream(samplerate=self.samplerate, channels=2, dtype=self.data.dtype)
  146. def start(self):
  147. self._th = threading.Thread(target=self.update, args=())
  148. self._th.start()
  149. def stop(self):
  150. self.stopped = True
  151. self._th.join()
  152. print('Continuous sound stream released')
  153. def update(self):
  154. self.stream.start()
  155. print('Continuous sound stream started at %s Hz' % (self.samplerate))
  156. offset = int(self.cfg['chunk_offset'] * self.samplerate)
  157. chunk = int(self.cfg['chunk_duration'] * self.samplerate)
  158. while not self.stopped:
  159. start_idx = offset + np.random.randint(self.data.shape[0] - 2 * offset - chunk)
  160. end_idx = start_idx + chunk
  161. self.stream.write(self.data[start_idx:end_idx])
  162. self.stream.stop()
  163. class SoundControllerPR:
  164. default_cfg = {
  165. "device": [1, 26],
  166. "n_channels": 10,
  167. "sounds": {
  168. "noise": {"amp": 0.2, "duration": 2.0, "channels": [6, 8]},
  169. "target": {"freq": 660, "amp": 0.1, "duration": 2.0},
  170. },
  171. "sample_rate": 44100,
  172. "volume": 0.7,
  173. "file_path": "sounds.csv"
  174. }
  175. def __init__(self, status, cfg):
  176. import sounddevice as sd # must be inside the function
  177. import numpy as np
  178. import time
  179. sd.default.device = cfg['device']
  180. sd.default.samplerate = cfg['sample_rate']
  181. self.stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=cfg['n_channels'], dtype='float32', blocksize=256)
  182. self.stream.start()
  183. self.timers = []
  184. self.status = status
  185. self.cfg = cfg
  186. # noise (not assigned to channels)
  187. filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])
  188. filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])
  189. noise = np.random.randn(int(cfg['sounds']['noise']['duration'] * cfg['sample_rate']))
  190. noise = lfilter(filter_a, filter_b, noise)
  191. noise = noise / np.abs(noise).max() * cfg['sounds']['noise']['amp']
  192. noise = noise.astype(np.float32)
  193. # target (not assigned to channels)
  194. sample_rate = cfg['sample_rate']
  195. target_cfg = cfg['sounds']['target']
  196. tone = SoundController.get_pure_tone(target_cfg['freq'], target_cfg['duration'], sample_rate=cfg['sample_rate'])
  197. tone = tone * SoundController.get_cos_window(tone, target_cfg['window'], sample_rate=cfg['sample_rate'])
  198. if target_cfg['number'] > 1:
  199. silence = np.zeros( int(target_cfg['iti'] * cfg['sample_rate']) )
  200. tone_with_iti = np.concatenate([tone, silence])
  201. target = np.concatenate([tone_with_iti for i in range(target_cfg['number'] - 1)])
  202. target = np.concatenate([target, tone])
  203. else:
  204. target = tone
  205. target = target * target_cfg['amp'] # amplitude
  206. #snd = cfg['sounds']['target']
  207. #target = SoundController.get_pure_tone(snd['freq'], snd['duration'], cfg['sample_rate']) * cfg['volume']
  208. #target = target * SoundController.get_cos_window(target, 0.01, cfg['sample_rate']) # onset / offset
  209. #target = target * snd['amp'] # amplitude
  210. self.sounds = {'noise': noise, 'target': target}
  211. def target(self, hd_angle):
  212. to_play = np.zeros((len(self.sounds['target']), self.cfg['n_channels']), dtype='float32')
  213. channel = random.choice(self.cfg['sounds']['target']['channels']) # random speaker!
  214. to_play[:, channel-1] = self.sounds['target']
  215. t0 = time.time()
  216. with open(self.cfg['file_path'], 'a') as f:
  217. f.write(",".join([str(x) for x in (t0, 2, channel)]) + "\n")
  218. self.stream.write(to_play)
  219. def noise(self):
  220. to_play = np.zeros((len(self.sounds['noise']), self.cfg['n_channels']), dtype='float32')
  221. for ch in self.cfg['sounds']['noise']['channels']:
  222. to_play[:, ch-1] = self.sounds['noise']
  223. ch1 = self.cfg['sounds']['noise']['channels'][0]
  224. t0 = time.time()
  225. with open(self.cfg['file_path'], 'a') as f:
  226. f.write(",".join([str(x) for x in (t0, -1, ch1)]) + "\n")
  227. self.stream.write(to_play)
  228. def play_non_blocking(self, sound_id, hd_angle=0):
  229. if sound_id == 'target':
  230. tf = threading.Timer(0, self.target, args=[hd_angle])
  231. elif sound_id == 'noise':
  232. tf = threading.Timer(0, self.noise, args=[])
  233. tf.start()
  234. self.timers.append(tf)
  235. def stop(self):
  236. for t in self.timers:
  237. t.cancel()
  238. self.stream.stop()