sound.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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(9600, dtype='float32')#int(cfg['sample_rate']/1000)
  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(cfg['latency'] * cfg['sample_rate'])) # it was 250ms of noise, now use cfg['latency'] instead of hardcoded 0.25
  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 scale(cls, orig_s_rate, target_s_rate, orig_data):
  90. factor = target_s_rate / orig_s_rate
  91. x_orig = np.linspace(0, int(factor * len(orig_data)), len(orig_data))
  92. x_target = np.linspace(0, int(factor * len(orig_data)), int(factor * len(orig_data)))
  93. return np.interp(x_target, x_orig, orig_data)
  94. @classmethod
  95. def run(cls, selector, status, cfg, commutator):
  96. """
  97. selector mp.Value object to set the sound to be played
  98. status mp.Value object to stop the loop
  99. """
  100. import sounddevice as sd # must be inside the function
  101. import soundfile as sf
  102. import numpy as np
  103. import time
  104. # this is a continuous noise shit
  105. if cfg['cont_noise']['enabled']:
  106. # cont_noise_s_rate, cont_noise_data = wavfile.read(cfg['cont_noise']['filepath'])
  107. cont_noise_data, cont_noise_s_rate = sf.read(cfg['cont_noise']['filepath'], dtype='float32')
  108. target_s_rate = cfg['sample_rate']
  109. orig_s_rate = cont_noise_s_rate
  110. if len(cont_noise_data.shape) > 1:
  111. orig_data = cont_noise_data[:, 0]
  112. else:
  113. orig_data = cont_noise_data
  114. cont_noise_target = cls.scale(orig_s_rate, target_s_rate, orig_data) * cfg['cont_noise']['amp']
  115. cont_noise_target = cont_noise_data * cfg['cont_noise']['amp']
  116. print(cont_noise_data.shape)
  117. c_noise_pointer = 0
  118. print(cfg['cont_noise']['amp'])
  119. # regular sounds
  120. sounds = cls.get_tone_stack(cfg)
  121. sd.default.device = cfg['device']
  122. sd.default.samplerate = cfg['sample_rate']
  123. stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=cfg['n_channels'], dtype='float32', blocksize=256)
  124. stream.start()
  125. next_beat = time.time() + cfg['latency']
  126. with open(cfg['file_path'], 'w') as f:
  127. f.write("time,id\n")
  128. while status.value > 0:
  129. if status.value == 2 or (status.value == 1 and selector.value == -1): # running state or masking noise
  130. t0 = time.time()
  131. if t0 < next_beat:
  132. #time.sleep(0.0001) # not to spin the wheels too much
  133. if stream.write_available > sounds['silence'].shape[0]:
  134. block_to_write = sounds['silence']
  135. if cfg['cont_noise']['enabled']:
  136. if c_noise_pointer + block_to_write.shape[0] > len(cont_noise_target):
  137. c_noise_pointer = 0
  138. cont_noise_block = cont_noise_target[c_noise_pointer:c_noise_pointer + block_to_write.shape[0]]
  139. for ch in cfg['cont_noise']['channels']:
  140. block_to_write[:, ch-1] += cont_noise_block
  141. c_noise_pointer += block_to_write.shape[0]
  142. stream.write(block_to_write) # silence
  143. continue
  144. roving = 10**((np.random.rand() * cfg['roving'] - cfg['roving']/2.0)/20.)
  145. roving = roving if int(selector.value) > -1 else 1 # no roving for noise
  146. block_to_write = sounds[commutator[int(selector.value)]] * roving # this is a 2D time x channels
  147. if cfg['cont_noise']['enabled']:
  148. if c_noise_pointer + block_to_write.shape[0] > len(cont_noise_target):
  149. c_noise_pointer = 0
  150. cont_noise_block = cont_noise_target[c_noise_pointer:c_noise_pointer + block_to_write.shape[0]]
  151. for ch in cfg['cont_noise']['channels']:
  152. block_to_write[:, ch-1] += cont_noise_block
  153. c_noise_pointer += block_to_write.shape[0]
  154. stream.write(block_to_write)
  155. if status.value == 2:
  156. with open(cfg['file_path'], 'a') as f:
  157. f.write(",".join([str(x) for x in (t0, selector.value)]) + "\n")
  158. next_beat += cfg['latency']
  159. if stream.write_available > 2:
  160. stream.write(sounds['silence']) # silence
  161. else: # idle state
  162. next_beat = time.time() + cfg['latency']
  163. time.sleep(0.005)
  164. stream.stop()
  165. print('Sound stopped')
  166. class ContinuousSoundStream:
  167. default_cfg = {
  168. 'wav_file': os.path.join('..', 'assets', 'stream1.wav'),
  169. 'chunk_duration': 20,
  170. 'chunk_offset': 2
  171. }
  172. def __init__(self, cfg):
  173. from scipy.io import wavfile
  174. import sounddevice as sd
  175. self.cfg = cfg
  176. self.stopped = False
  177. self.samplerate, self.data = wavfile.read(cfg['wav_file'])
  178. self.stream = sd.OutputStream(samplerate=self.samplerate, channels=2, dtype=self.data.dtype)
  179. def start(self):
  180. self._th = threading.Thread(target=self.update, args=())
  181. self._th.start()
  182. def stop(self):
  183. self.stopped = True
  184. self._th.join()
  185. print('Continuous sound stream released')
  186. def update(self):
  187. self.stream.start()
  188. print('Continuous sound stream started at %s Hz' % (self.samplerate))
  189. offset = int(self.cfg['chunk_offset'] * self.samplerate)
  190. chunk = int(self.cfg['chunk_duration'] * self.samplerate)
  191. while not self.stopped:
  192. start_idx = offset + np.random.randint(self.data.shape[0] - 2 * offset - chunk)
  193. end_idx = start_idx + chunk
  194. self.stream.write(self.data[start_idx:end_idx])
  195. self.stream.stop()
  196. class SoundControllerPR:
  197. default_cfg = {
  198. "device": [1, 26],
  199. "n_channels": 10,
  200. "sounds": {
  201. "noise": {"amp": 0.2, "duration": 2.0, "channels": [6, 8]},
  202. "target": {"freq": 660, "amp": 0.1, "duration": 2.0},
  203. },
  204. "sample_rate": 44100,
  205. "volume": 0.7,
  206. "file_path": "sounds.csv"
  207. }
  208. def __init__(self, status, cfg):
  209. import sounddevice as sd # must be inside the function
  210. import numpy as np
  211. import time
  212. sd.default.device = cfg['device']
  213. sd.default.samplerate = cfg['sample_rate']
  214. self.stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=cfg['n_channels'], dtype='float32', blocksize=256)
  215. self.stream.start()
  216. self.timers = []
  217. self.status = status
  218. self.cfg = cfg
  219. # noise (not assigned to channels)
  220. filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])
  221. filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])
  222. noise = np.random.randn(int(cfg['sounds']['noise']['duration'] * cfg['sample_rate']))
  223. noise = lfilter(filter_a, filter_b, noise)
  224. noise = noise / np.abs(noise).max() * cfg['sounds']['noise']['amp']
  225. noise = noise.astype(np.float32)
  226. # target (not assigned to channels)
  227. sample_rate = cfg['sample_rate']
  228. target_cfg = cfg['sounds']['target']
  229. tone = SoundController.get_pure_tone(target_cfg['freq'], target_cfg['duration'], sample_rate=cfg['sample_rate'])
  230. tone = tone * SoundController.get_cos_window(tone, target_cfg['window'], sample_rate=cfg['sample_rate'])
  231. if target_cfg['number'] > 1:
  232. silence = np.zeros( int(target_cfg['iti'] * cfg['sample_rate']) )
  233. tone_with_iti = np.concatenate([tone, silence])
  234. target = np.concatenate([tone_with_iti for i in range(target_cfg['number'] - 1)])
  235. target = np.concatenate([target, tone])
  236. else:
  237. target = tone
  238. target = target * target_cfg['amp'] # amplitude
  239. #snd = cfg['sounds']['target']
  240. #target = SoundController.get_pure_tone(snd['freq'], snd['duration'], cfg['sample_rate']) * cfg['volume']
  241. #target = target * SoundController.get_cos_window(target, 0.01, cfg['sample_rate']) # onset / offset
  242. #target = target * snd['amp'] # amplitude
  243. self.sounds = {'noise': noise, 'target': target}
  244. def target(self, hd_angle):
  245. to_play = np.zeros((len(self.sounds['target']), self.cfg['n_channels']), dtype='float32')
  246. channel = random.choice(self.cfg['sounds']['target']['channels']) # random speaker!
  247. to_play[:, channel-1] = self.sounds['target']
  248. t0 = time.time()
  249. with open(self.cfg['file_path'], 'a') as f:
  250. f.write(",".join([str(x) for x in (t0, 2, channel)]) + "\n")
  251. self.stream.write(to_play)
  252. def noise(self):
  253. to_play = np.zeros((len(self.sounds['noise']), self.cfg['n_channels']), dtype='float32')
  254. for ch in self.cfg['sounds']['noise']['channels']:
  255. to_play[:, ch-1] = self.sounds['noise']
  256. ch1 = self.cfg['sounds']['noise']['channels'][0]
  257. t0 = time.time()
  258. with open(self.cfg['file_path'], 'a') as f:
  259. f.write(",".join([str(x) for x in (t0, -1, ch1)]) + "\n")
  260. self.stream.write(to_play)
  261. def play_non_blocking(self, sound_id, hd_angle=0):
  262. if sound_id == 'target':
  263. tf = threading.Timer(0, self.target, args=[hd_angle])
  264. elif sound_id == 'noise':
  265. tf = threading.Timer(0, self.noise, args=[])
  266. tf.start()
  267. self.timers.append(tf)
  268. def stop(self):
  269. for t in self.timers:
  270. t.cancel()
  271. self.stream.stop()