sound.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import numpy as np
  2. import time
  3. class SoundController:
  4. # https://python-sounddevice.readthedocs.io/en/0.3.15/api/streams.html#sounddevice.OutputStream
  5. default_cfg = {
  6. "device": [1, 26],
  7. "n_channels": 4,
  8. "sounds": [
  9. {"freq": 460, "amp": 0.13, "channels": [1, 3]},
  10. {"freq": 660, "amp": 0.05, "channels": [1, 3]},
  11. {"freq": 860, "amp": 0.15, "channels": [1, 3]},
  12. {"freq": 1060, "amp": 0.25, "channels": [1, 3]},
  13. {"freq": 1320, "amp": 0.2, "channels": [1, 3]},
  14. {"freq": 20000, "amp": 0.55, "channels": [1, 3]}
  15. ],
  16. "pulse_duration": 0.05,
  17. "sample_rate": 44100,
  18. "latency": 0.25,
  19. "volume": 0.7,
  20. "roving": 5.0,
  21. "file_path": "sounds.csv"
  22. }
  23. @classmethod
  24. def get_pure_tone(cls, freq, duration, sample_rate=44100):
  25. x = np.linspace(0, duration * freq * 2*np.pi, int(duration*sample_rate), dtype=np.float32)
  26. return np.sin(x)
  27. @classmethod
  28. def get_cos_window(cls, tone, win_duration, sample_rate=44100):
  29. x = np.linspace(0, np.pi/2, int(win_duration * sample_rate), dtype=np.float32)
  30. onset = np.sin(x)
  31. middle = np.ones(len(tone) - 2 * len(x))
  32. offset = np.cos(x)
  33. return np.concatenate([onset, middle, offset])
  34. @classmethod
  35. def get_tone_stack(cls, cfg):
  36. silence = np.zeros(2, dtype='float32')
  37. sounds = {0: np.column_stack([silence for x in range(cfg['n_channels'])])}
  38. for i, snd in enumerate(cfg['sounds']):
  39. tone = cls.get_pure_tone(snd['freq'], cfg['pulse_duration'], cfg['sample_rate']) * cfg['volume']
  40. tone = tone * cls.get_cos_window(tone, 0.01, cfg['sample_rate']) # onset / offset
  41. tone = tone * snd['amp']
  42. sound = np.zeros([len(tone), cfg['n_channels']], dtype='float32')
  43. for j in snd['channels']:
  44. sound[:, j-1] = tone
  45. sounds[i + 1] = sound
  46. #sounds[i + 1] = np.column_stack((nothing, tone, tone, tone))
  47. return sounds
  48. @classmethod
  49. def run(cls, selector, status, cfg):
  50. """
  51. selector mp.Value object to set the sound to be played
  52. status mp.Value object to stop the loop
  53. """
  54. import sounddevice as sd # must be inside the function
  55. import numpy as np
  56. import time
  57. sounds = cls.get_tone_stack(cfg)
  58. sd.default.device = cfg['device']
  59. sd.default.samplerate = cfg['sample_rate']
  60. stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=4, dtype='float32', blocksize=256)
  61. stream.start()
  62. next_beat = time.time() + cfg['latency']
  63. with open(cfg['file_path'], 'w') as f:
  64. f.write("time,id\n")
  65. while status.value > 0:
  66. if status.value == 2: # running state
  67. t0 = time.time()
  68. if t0 < next_beat:
  69. #time.sleep(0.0001) # not to spin the wheels too much
  70. if stream.write_available > 2:
  71. stream.write(sounds[0]) # silence
  72. continue
  73. roving = 10**((np.random.rand() * cfg['roving'] - cfg['roving']/2.0)/20.)
  74. stream.write(sounds[int(selector.value)] * roving)
  75. with open(cfg['file_path'], 'a') as f:
  76. f.write(",".join([str(x) for x in (t0, selector.value)]) + "\n")
  77. next_beat += cfg['latency']
  78. if stream.write_available > 2:
  79. stream.write(sounds[0]) # silence
  80. else: # idle state
  81. next_beat = time.time() + cfg['latency']
  82. time.sleep(0.05)
  83. stream.stop()
  84. print('Sound stopped')