Browse Source

fixes to the sound presentation loop - include continuous sound stream part 1

asobolev 2 months ago
parent
commit
aefb15ddf4

+ 66 - 38
SIT.ipynb

@@ -60,12 +60,13 @@
    "source": [
     "# cfg_filename = os.path.join('profiles', 'mouse_freq.json')\n",
     "# cfg_filename = os.path.join('profiles', 'implanted_multiSIT_660_1320.json')\n",
-    "cfg_filename = os.path.join('profiles', 'implanted_timeSIT_50_100.json')\n",
+    "#cfg_filename = os.path.join('profiles', 'implanted_timeSIT_50_100.json')\n",
+    "# cfg_filename = os.path.join('profiles', 'passive_FDA.json')\n",
     "# cfg_filename = os.path.join('profiles', 'gokce_timeSIT_50_100.json')\n",
-    "# cfg_filename = os.path.join('profiles', 'gokce_timeSIT_90_99.json')\n",
+    "# cfg_filename = os.path.join('profiles', 'gokce_timeSIT_90_120.json')\n",
     "# gokce_timeSIT_90_108.json')\n",
     "\n",
-    "# cfg_filename = os.path.join('profiles', 'gokce_socialSIT.json')\n",
+    "cfg_filename = os.path.join('profiles', 'gokce_socialSIT.json')\n",
     "# cfg_filename = os.path.join('profiles', 'miguel_socialSIT_fireface.json')\n",
     "# cfg_filename = os.path.join('profiles', 'miguel_socialSIT_fireface_chirp.json')\n",
     "\n",
@@ -117,46 +118,46 @@
       "text/plain": [
        "    0 Microsoft Sound Mapper - Input, MME (2 in, 0 out)\n",
        ">   1 Line 1/2 (M-Audio Delta 410), MME (2 in, 0 out)\n",
-       "    2 ADAT 1 (1+2) (RME Fireface UFX), MME (2 in, 0 out)\n",
-       "    3 Analog (9+10) (RME Fireface UFX, MME (2 in, 0 out)\n",
-       "    4 Analog (5+6) (RME Fireface UFX), MME (2 in, 0 out)\n",
-       "    5 S/PDIF (M-Audio Delta 410), MME (2 in, 0 out)\n",
-       "    6 Monitor (M-Audio Delta 410), MME (2 in, 0 out)\n",
-       "    7 Analog (1+2) (RME Fireface UFX), MME (2 in, 0 out)\n",
-       "    8 Analog (3+4) (RME Fireface UFX), MME (2 in, 0 out)\n",
-       "    9 Analog (11+12) (RME Fireface UF, MME (2 in, 0 out)\n",
-       "   10 Analog (7+8) (RME Fireface UFX), MME (2 in, 0 out)\n",
-       "   11 AES (RME Fireface UFX), MME (2 in, 0 out)\n",
-       "   12 Multichannel (M-Audio Delta 410, MME (2 in, 0 out)\n",
+       "    2 S/PDIF (M-Audio Delta 410), MME (2 in, 0 out)\n",
+       "    3 Monitor (M-Audio Delta 410), MME (2 in, 0 out)\n",
+       "    4 Multichannel (M-Audio Delta 410, MME (2 in, 0 out)\n",
+       "    5 ADAT 1 (1+2) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "    6 Analog (9+10) (RME Fireface UFX, MME (2 in, 0 out)\n",
+       "    7 Analog (5+6) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "    8 Analog (1+2) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "    9 Analog (3+4) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "   10 Analog (11+12) (RME Fireface UF, MME (2 in, 0 out)\n",
+       "   11 Analog (7+8) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "   12 AES (RME Fireface UFX), MME (2 in, 0 out)\n",
        "   13 Microsoft Sound Mapper - Output, MME (0 in, 2 out)\n",
        "<  14 Speakers (3- Realtek High Defin, MME (0 in, 2 out)\n",
-       "   15 Analog (11+12) (RME Fireface UF, MME (0 in, 2 out)\n",
-       "   16 Line 1/2 (M-Audio Delta 410), MME (0 in, 2 out)\n",
-       "   17 Line 3/4 (M-Audio Delta 410), MME (0 in, 2 out)\n",
-       "   18 Line 7/8 (M-Audio Delta 410), MME (0 in, 2 out)\n",
-       "   19 Line 5/6 (M-Audio Delta 410), MME (0 in, 2 out)\n",
-       "   20 Analog (7+8) (RME Fireface UFX), MME (0 in, 2 out)\n",
-       "   21 S/PDIF (M-Audio Delta 410), MME (0 in, 2 out)\n",
-       "   22 AES (RME Fireface UFX), MME (0 in, 2 out)\n",
-       "   23 Speakers (RME Fireface UFX), MME (0 in, 2 out)\n",
-       "   24 Multichannel (M-Audio Delta 410, MME (0 in, 2 out)\n",
+       "   15 Line 1/2 (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   16 Line 3/4 (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   17 Line 7/8 (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   18 Line 5/6 (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   19 S/PDIF (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   20 Multichannel (M-Audio Delta 410, MME (0 in, 2 out)\n",
+       "   21 Analog (11+12) (RME Fireface UF, MME (0 in, 2 out)\n",
+       "   22 Analog (7+8) (RME Fireface UFX), MME (0 in, 2 out)\n",
+       "   23 AES (RME Fireface UFX), MME (0 in, 2 out)\n",
+       "   24 Speakers (RME Fireface UFX), MME (0 in, 2 out)\n",
        "   25 ADAT 1 (1+2) (RME Fireface UFX), MME (0 in, 2 out)\n",
        "   26 Analog (5+6) (RME Fireface UFX), MME (0 in, 2 out)\n",
        "   27 Analog (3+4) (RME Fireface UFX), MME (0 in, 2 out)\n",
        "   28 Analog (9+10) (RME Fireface UFX, MME (0 in, 2 out)\n",
        "   29 Primary Sound Capture Driver, Windows DirectSound (2 in, 0 out)\n",
        "   30 Line 1/2 (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
-       "   31 ADAT 1 (1+2) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
-       "   32 Analog (9+10) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
-       "   33 Analog (5+6) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
-       "   34 S/PDIF (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
-       "   35 Monitor (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
-       "   36 Analog (1+2) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
-       "   37 Analog (3+4) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
-       "   38 Analog (11+12) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
-       "   39 Analog (7+8) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
-       "   40 AES (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
-       "   41 Multichannel (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
+       "   31 S/PDIF (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
+       "   32 Monitor (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
+       "   33 Multichannel (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
+       "   34 ADAT 1 (1+2) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   35 Analog (9+10) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   36 Analog (5+6) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   37 Analog (1+2) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   38 Analog (3+4) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   39 Analog (11+12) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   40 Analog (7+8) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   41 AES (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
        "   42 Primary Sound Driver, Windows DirectSound (0 in, 2 out)\n",
        "   43 Speakers (3- Realtek High Definition Audio), Windows DirectSound (0 in, 2 out)\n",
        "   44 Analog (11+12) (RME Fireface UFX), Windows DirectSound (0 in, 2 out)\n",
@@ -283,6 +284,8 @@
     "    cfg['sound']['wav_file'] = os.path.join('assets', cfg['sound']['wav_file'])\n",
     "if 'continuous' in cfg['sound']:\n",
     "    cfg['sound']['continuous']['wav_file'] = os.path.join('assets', cfg['sound']['continuous']['wav_file'])\n",
+    "if 'cont_noise' in cfg['sound']:\n",
+    "    cfg['sound']['cont_noise']['filepath'] = os.path.join('assets', cfg['sound']['cont_noise']['filepath'])\n",
     "    \n",
     "# Saves all parameters to a JSON file with the user-defined \"Experiment ID\" as filename\n",
     "with open(os.path.join(save_to, experiment_id + '.json'), 'w') as f:\n",
@@ -365,6 +368,31 @@
    "metadata": {
     "scrolled": true
    },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'filepath': 'assets\\\\chirp_rate192KHz_100ms_2000Hz_30000Hz.wav',\n",
+       " 'amp': 0.5,\n",
+       " 'channels': [3],\n",
+       " 'enabled': True}"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cfg['sound']['cont_noise']"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {
+    "scrolled": true
+   },
    "outputs": [
     {
      "name": "stdout",
@@ -784,7 +812,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 13,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -796,7 +824,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 14,
    "metadata": {},
    "outputs": [
     {
@@ -806,7 +834,7 @@
      "traceback": [
       "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
       "\u001b[1;31mIndexError\u001b[0m                                Traceback (most recent call last)",
-      "\u001b[1;32m<ipython-input-13-df6d003c4298>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m      3\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[1;31m# do pack data to HDF5\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[0mh5name\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mpack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msession_path\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      6\u001b[0m \u001b[0mtrial\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
+      "\u001b[1;32m<ipython-input-14-df6d003c4298>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m      3\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[1;31m# do pack data to HDF5\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[0mh5name\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mpack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msession_path\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      6\u001b[0m \u001b[0mtrial\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;32mD:\\runSIT\\..\\pipeline\\postprocessing\\pack.py\u001b[0m in \u001b[0;36mpack\u001b[1;34m(session_path)\u001b[0m\n\u001b[0;32m    165\u001b[0m         \u001b[1;31m# head direction\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    166\u001b[0m         \u001b[0mtemp_tl\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcolumn_stack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mpos_at_freq\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mx_smooth\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my_smooth\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspeed\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 167\u001b[1;33m         \u001b[0mhd\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mhead_direction\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtemp_tl\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m    168\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    169\u001b[0m         \u001b[1;31m# trial numbers\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;32mD:\\runSIT\\..\\pipeline\\postprocessing\\head_direction.py\u001b[0m in \u001b[0;36mhead_direction\u001b[1;34m(tl, hd_update_speed)\u001b[0m\n\u001b[0;32m     22\u001b[0m     \u001b[0mcrit\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwhere\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdiff\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0midle_idxs\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     23\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 24\u001b[1;33m     \u001b[0midle_periods\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[1;33m(\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m)\u001b[0m  \u001b[1;31m# first idle period\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     25\u001b[0m     \u001b[1;32mfor\u001b[0m \u001b[0mi\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mpoint\u001b[0m \u001b[1;32min\u001b[0m \u001b[0menumerate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m-\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     26\u001b[0m         \u001b[0midx_start\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mi\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;31mIndexError\u001b[0m: index 0 is out of bounds for axis 0 with size 0"

+ 1 - 1
controllers/display.ipynb

@@ -40,7 +40,7 @@
     "        \n",
     "        # mask unused areas\n",
     "        frame = cv2.bitwise_and(src1=input_frame, src2=self.pt.mask)\n",
-    "        #self.masked_frame = frame.copy()\n",
+    "        self.masked_frame = frame.copy()\n",
     "\n",
     "        # size of the arena and status indicator\n",
     "        cv2.circle(frame, (self.pt.cfg['arena_x'], self.pt.cfg['arena_y']), self.pt.cfg['arena_radius'], COLORS['red'], 2)\n",

File diff suppressed because it is too large
+ 137 - 6
controllers/sound.ipynb


+ 51 - 5
controllers/sound.py

@@ -65,14 +65,14 @@ class SoundController:
     @classmethod
     def get_tone_stack(cls, cfg):
         # silence
-        silence = np.zeros(2, dtype='float32')
+        silence = np.zeros(9600, dtype='float32')#int(cfg['sample_rate']/1000)
         sounds = {'silence': np.column_stack([silence for x in range(cfg['n_channels'])])}
 
         # noise
         filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])
         filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])
 
-        noise = np.random.randn(int(0.25 * cfg['sample_rate']))  # 250ms of noise
+        noise = np.random.randn(int(cfg['latency'] * cfg['sample_rate']))  # it was 250ms of noise, now use cfg['latency'] instead of hardcoded 0.25
         noise = lfilter(filter_a, filter_b, noise)
         noise = noise / np.abs(noise).max() * cfg['sounds']['noise']['amp']
         noise = noise.astype(np.float32)
@@ -101,6 +101,13 @@ class SoundController:
 
         return sounds
         
+    @classmethod
+    def scale(cls, orig_s_rate, target_s_rate, orig_data):
+        factor = target_s_rate / orig_s_rate
+        x_orig   = np.linspace(0, int(factor * len(orig_data)), len(orig_data))
+        x_target = np.linspace(0, int(factor * len(orig_data)), int(factor * len(orig_data)))
+        return np.interp(x_target, x_orig, orig_data)
+    
     @classmethod
     def run(cls, selector, status, cfg, commutator):
         """
@@ -108,9 +115,28 @@ class SoundController:
         status          mp.Value object to stop the loop
         """
         import sounddevice as sd  # must be inside the function
+        import soundfile as sf
         import numpy as np
         import time
         
+        # this is a continuous noise shit
+        if cfg['cont_noise']['enabled']:
+#             cont_noise_s_rate, cont_noise_data = wavfile.read(cfg['cont_noise']['filepath'])
+            cont_noise_data, cont_noise_s_rate = sf.read(cfg['cont_noise']['filepath'], dtype='float32')
+            target_s_rate = cfg['sample_rate']
+            orig_s_rate   = cont_noise_s_rate
+            if len(cont_noise_data.shape) > 1:
+                orig_data = cont_noise_data[:, 0]
+            else:
+                orig_data = cont_noise_data
+            cont_noise_target = cls.scale(orig_s_rate, target_s_rate, orig_data) * cfg['cont_noise']['amp']
+            cont_noise_target = cont_noise_data * cfg['cont_noise']['amp']
+            print(cont_noise_data.shape)
+            c_noise_pointer = 0
+            
+            print(cfg['cont_noise']['amp'])
+        
+        # regular sounds
         sounds = cls.get_tone_stack(cfg)
 
         sd.default.device = cfg['device']
@@ -127,13 +153,33 @@ class SoundController:
                 t0 = time.time()
                 if t0 < next_beat:
                     #time.sleep(0.0001)  # not to spin the wheels too much
-                    if stream.write_available > 2:
-                        stream.write(sounds['silence'])  # silence
+                    if stream.write_available > sounds['silence'].shape[0]:
+                        block_to_write = sounds['silence']
+                        if cfg['cont_noise']['enabled']:
+                            if c_noise_pointer + block_to_write.shape[0] > len(cont_noise_target):
+                                c_noise_pointer = 0
+                            cont_noise_block = cont_noise_target[c_noise_pointer:c_noise_pointer + block_to_write.shape[0]]
+                            for ch in cfg['cont_noise']['channels']:
+                                block_to_write[:, ch-1] += cont_noise_block
+                            c_noise_pointer += block_to_write.shape[0]
+
+                        stream.write(block_to_write)  # silence
                     continue
 
                 roving = 10**((np.random.rand() * cfg['roving'] - cfg['roving']/2.0)/20.)
                 roving = roving if int(selector.value) > -1 else 1  # no roving for noise
-                stream.write(sounds[commutator[int(selector.value)]] * roving)
+                block_to_write = sounds[commutator[int(selector.value)]] * roving  # this is a 2D time x channels
+
+                if cfg['cont_noise']['enabled']:
+                    if c_noise_pointer + block_to_write.shape[0] > len(cont_noise_target):
+                        c_noise_pointer = 0
+                    cont_noise_block = cont_noise_target[c_noise_pointer:c_noise_pointer + block_to_write.shape[0]]
+                    for ch in cfg['cont_noise']['channels']:
+                        block_to_write[:, ch-1] += cont_noise_block
+                    c_noise_pointer += block_to_write.shape[0]
+                    
+                stream.write(block_to_write)
+                
                 if status.value == 2:
                     with open(cfg['file_path'], 'a') as f:
                         f.write(",".join([str(x) for x in (t0, selector.value)]) + "\n")

File diff suppressed because it is too large
+ 1042 - 0
controllers/sound_chirp.ipynb


+ 301 - 0
controllers/sound_chirp.py

@@ -0,0 +1,301 @@
+import numpy as np
+import time
+from scipy.signal import lfilter
+from functools import reduce
+
+import os
+import threading
+import random
+
+class SoundController:
+    # https://python-sounddevice.readthedocs.io/en/0.3.15/api/streams.html#sounddevice.OutputStream
+    
+    default_cfg = {
+        "device": [1, 26],
+        "n_channels": 10,
+        "sounds": {
+            "noise": {"amp": 0.2, "channels": [6, 8]},
+            "background": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": True, "channels": [3, 8]},
+            "target": {"freq": 1320, "amp": 0.1, "duration": 0.05, "harmonics": True, "channels": [3, 8]}, 
+            "distractor1": {"freq": 860, "amp": 0.15, "duration": 0.05, "harmonics": True, "channels": [6, 8], "enabled": False},
+            "distractor2": {"freq": 1060, "amp": 0.25, "duration": 0.05, "harmonics": True, "channels": [6, 8], "enabled": False},
+            "distractor3": {"freq": 1320, "amp": 0.2, "duration": 0.05, "harmonics": True, "channels": [6, 8], "enabled": False}
+        },
+        "pulse_duration": 0.05,
+        "sample_rate": 44100,
+        "latency": 0.25,
+        "volume": 0.7,
+        "roving": 5.0,
+        "file_path": "sounds.csv"
+    }
+    
+    commutator = {
+        -1: 'noise',
+        0:  'silence',
+        1:  'background',
+        2:  'target',
+        3:  'distractor1',
+        4:  'distractor2',
+        5:  'distractor3',
+        6:  'distractor4',
+        7:  'distractor5'
+    }
+    
+    @classmethod
+    def get_pure_tone(cls, freq, duration, sample_rate=44100):
+        x = np.linspace(0, duration * freq * 2*np.pi, int(duration*sample_rate), dtype=np.float32)
+        return np.sin(x)
+
+    @classmethod
+    def get_harm_stack(cls, base_freq, duration, threshold=1500, sample_rate=44100):
+        harmonics = [x * base_freq for x in np.arange(20) + 2 if x * base_freq < threshold]  # first 20 enouch
+        freqs = [base_freq] + harmonics
+        x = np.linspace(0, duration, int(sample_rate * duration))
+        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)])
+        return y / y.max()  # norm to -1 to 1
+    
+    @classmethod
+    def get_cos_window(cls, tone, win_duration, sample_rate=44100):
+        x = np.linspace(0, np.pi/2, int(win_duration * sample_rate), dtype=np.float32)
+        onset =  np.sin(x)
+        middle = np.ones(len(tone) - 2 * len(x))
+        offset = np.cos(x)
+        return np.concatenate([onset, middle, offset])
+
+    @classmethod
+    def get_tone_stack(cls, cfg):
+        # silence
+        silence = np.zeros(2, dtype='float32')
+        sounds = {'silence': np.column_stack([silence for x in range(cfg['n_channels'])])}
+
+        # noise
+        filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])
+        filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])
+
+        noise = np.random.randn(int(0.25 * cfg['sample_rate']))  # 250ms of noise
+        noise = lfilter(filter_a, filter_b, noise)
+        noise = noise / np.abs(noise).max() * cfg['sounds']['noise']['amp']
+        noise = noise.astype(np.float32)
+        empty = np.zeros((len(noise), cfg['n_channels']), dtype='float32')
+        for ch in cfg['sounds']['noise']['channels']:
+            empty[:, ch-1] = noise
+        sounds['noise'] = empty
+        
+        # all other sounds
+        for key, snd in cfg['sounds'].items():
+            if key == 'noise' or ('enabled' in snd and not snd['enabled']):
+                continue  # skip noise or unused sounds
+                
+            if 'harmonics' in snd and snd['harmonics']:
+                tone = cls.get_harm_stack(snd['freq'], snd['duration'], sample_rate=cfg['sample_rate']) * cfg['volume']
+            else:
+                tone = cls.get_pure_tone(snd['freq'], snd['duration'], cfg['sample_rate']) * cfg['volume']
+            tone = tone * cls.get_cos_window(tone, 0.01, cfg['sample_rate'])  # onset / offset
+            tone = tone * snd['amp']  # amplitude
+            
+            sound = np.zeros([len(tone), cfg['n_channels']], dtype='float32')
+            for j in snd['channels']:
+                sound[:, j-1] = tone
+           
+            sounds[key] = sound
+
+        return sounds
+        
+    @classmethod
+    def run(cls, selector, status, cfg, commutator):
+        """
+        selector        mp.Value object to set the sound to be played
+        status          mp.Value object to stop the loop
+        """
+        import sounddevice as sd  # must be inside the function
+        import numpy as np
+        import time
+        
+        import soundfile as sf # chirp changes
+        
+        
+        sounds = cls.get_tone_stack(cfg)
+
+        sd.default.device = cfg['device']
+        sd.default.samplerate = cfg['sample_rate']
+        
+        data, fs = sf.read(cfg['wav_file'], dtype='float32') #chirp changes
+        chirp = np.zeros((len(data),cfg['n_channels']),dtype='float32') #chirp changes
+        chirp[:,cfg['sounds']['target']['channels'][0]-1] = 0.07*data #chirp changes, amplitude for chirp=0.07, click=1.0
+        chirp[:,6] = 0.07*data #chirp changes, amplitude for chirp=0.07, click=1.0
+        
+        stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=cfg['n_channels'], dtype='float32', blocksize=256)
+        stream.start()
+
+        next_beat = time.time() + cfg['latency']
+        with open(cfg['file_path'], 'w') as f:
+            f.write("time,id\n")
+
+        while status.value > 0:
+            if status.value == 2 or (status.value == 1 and selector.value == -1):  # running state or masking noise
+                t0 = time.time()
+                if t0 < next_beat:
+                    #time.sleep(0.0001)  # not to spin the wheels too much
+                    if stream.write_available > 2:
+                        stream.write(sounds['silence'])  # silence
+                    continue
+
+                roving = 10**((np.random.rand() * cfg['roving'] - cfg['roving']/2.0)/20.)
+                roving = roving if int(selector.value) > -1 else 1  # no roving for noise
+#                 stream.write(sounds[commutator[int(selector.value)]] * roving) # chirp changes (uncomment->comment)
+                stream.write(chirp) # chirp changes
+                if status.value == 2:
+                    with open(cfg['file_path'], 'a') as f:
+                        f.write(",".join([str(x) for x in (t0, selector.value)]) + "\n")
+
+                next_beat += cfg['latency']
+                
+                if stream.write_available > 2:
+                    stream.write(sounds['silence'])  # silence
+            
+            else:  # idle state
+                next_beat = time.time() + cfg['latency']
+                time.sleep(0.005)
+                
+        stream.stop()
+        print('Sound stopped')
+
+        
+class ContinuousSoundStream:
+   
+    default_cfg = {
+        'wav_file': os.path.join('..', 'assets', 'stream1.wav'),
+        'chunk_duration': 20,
+        'chunk_offset': 2
+    }
+    
+    def __init__(self, cfg):
+        from scipy.io import wavfile
+        import sounddevice as sd
+
+        self.cfg = cfg
+        self.stopped = False
+        self.samplerate, self.data = wavfile.read(cfg['wav_file'])
+        self.stream = sd.OutputStream(samplerate=self.samplerate, channels=2, dtype=self.data.dtype)
+
+    def start(self):
+        self._th = threading.Thread(target=self.update, args=())
+        self._th.start()
+
+    def stop(self):
+        self.stopped = True
+        self._th.join()
+        print('Continuous sound stream released')
+            
+    def update(self):
+        self.stream.start()
+        print('Continuous sound stream started at %s Hz' % (self.samplerate))
+        
+        offset = int(self.cfg['chunk_offset'] * self.samplerate)
+        chunk =  int(self.cfg['chunk_duration'] * self.samplerate)
+        
+        while not self.stopped:
+            start_idx = offset + np.random.randint(self.data.shape[0] - 2 * offset - chunk)
+            end_idx = start_idx + chunk
+            self.stream.write(self.data[start_idx:end_idx])
+            
+        self.stream.stop()
+        
+        
+class SoundControllerPR:
+    
+    default_cfg = {
+        "device": [1, 26],
+        "n_channels": 10,
+        "sounds": {
+            "noise": {"amp": 0.2, "duration": 2.0, "channels": [6, 8]},
+            "target": {"freq": 660, "amp": 0.1, "duration": 2.0}, 
+        },
+        "sample_rate": 44100,
+        "volume": 0.7,
+        "file_path": "sounds.csv"
+    }
+        
+    def __init__(self, status, cfg):
+        import sounddevice as sd  # must be inside the function
+        import numpy as np
+        import time
+
+        sd.default.device = cfg['device']
+        sd.default.samplerate = cfg['sample_rate']
+        self.stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=cfg['n_channels'], dtype='float32', blocksize=256)
+        self.stream.start()
+
+        self.timers = []
+        self.status = status
+        self.cfg = cfg
+        
+        # noise (not assigned to channels)
+        filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])
+        filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])
+
+        noise = np.random.randn(int(cfg['sounds']['noise']['duration'] * cfg['sample_rate']))
+        noise = lfilter(filter_a, filter_b, noise)
+        noise = noise / np.abs(noise).max() * cfg['sounds']['noise']['amp']
+        noise = noise.astype(np.float32)
+
+        # target (not assigned to channels)
+        sample_rate = cfg['sample_rate']
+        target_cfg = cfg['sounds']['target']
+
+        tone = SoundController.get_pure_tone(target_cfg['freq'], target_cfg['duration'], sample_rate=cfg['sample_rate'])
+        tone = tone * SoundController.get_cos_window(tone, target_cfg['window'], sample_rate=cfg['sample_rate'])
+
+        if target_cfg['number'] > 1:
+            silence = np.zeros( int(target_cfg['iti'] * cfg['sample_rate']) )
+            tone_with_iti = np.concatenate([tone, silence])
+            target = np.concatenate([tone_with_iti for i in range(target_cfg['number'] - 1)])
+            target = np.concatenate([target, tone])
+        else:
+            target = tone
+            
+        target = target * target_cfg['amp']  # amplitude
+       
+        #snd = cfg['sounds']['target']
+        #target = SoundController.get_pure_tone(snd['freq'], snd['duration'], cfg['sample_rate']) * cfg['volume']
+        #target = target * SoundController.get_cos_window(target, 0.01, cfg['sample_rate'])  # onset / offset
+        #target = target * snd['amp']  # amplitude
+        
+        self.sounds = {'noise': noise, 'target': target}
+        
+    def target(self, hd_angle):
+        to_play = np.zeros((len(self.sounds['target']), self.cfg['n_channels']), dtype='float32')
+        channel = random.choice(self.cfg['sounds']['target']['channels'])  # random speaker!
+        
+        to_play[:, channel-1] = self.sounds['target']
+            
+        t0 = time.time()
+        with open(self.cfg['file_path'], 'a') as f:
+            f.write(",".join([str(x) for x in (t0, 2, channel)]) + "\n")
+        
+        self.stream.write(to_play)
+        
+    def noise(self):
+        to_play = np.zeros((len(self.sounds['noise']), self.cfg['n_channels']), dtype='float32')
+        for ch in self.cfg['sounds']['noise']['channels']:
+            to_play[:, ch-1] = self.sounds['noise']
+        
+        ch1 = self.cfg['sounds']['noise']['channels'][0]
+        t0 = time.time()
+        with open(self.cfg['file_path'], 'a') as f:
+            f.write(",".join([str(x) for x in (t0, -1, ch1)]) + "\n")
+        
+        self.stream.write(to_play)
+            
+    def play_non_blocking(self, sound_id, hd_angle=0):
+        if sound_id == 'target':
+            tf = threading.Timer(0, self.target, args=[hd_angle])
+        elif sound_id == 'noise':
+            tf = threading.Timer(0, self.noise, args=[])
+        tf.start()
+        self.timers.append(tf)
+        
+    def stop(self):
+        for t in self.timers:
+            t.cancel()
+        self.stream.stop()

File diff suppressed because it is too large
+ 1031 - 0
controllers/sound_chirp2.ipynb


+ 301 - 0
controllers/sound_chirp2.py

@@ -0,0 +1,301 @@
+import numpy as np
+import time
+from scipy.signal import lfilter
+from functools import reduce
+
+import os
+import threading
+import random
+
+class SoundController:
+    # https://python-sounddevice.readthedocs.io/en/0.3.15/api/streams.html#sounddevice.OutputStream
+    
+    default_cfg = {
+        "device": [1, 26],
+        "n_channels": 10,
+        "sounds": {
+            "noise": {"amp": 0.2, "channels": [6, 8]},
+            "background": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": True, "channels": [3, 8]},
+            "target": {"freq": 1320, "amp": 0.1, "duration": 0.05, "harmonics": True, "channels": [3, 8]}, 
+            "distractor1": {"freq": 860, "amp": 0.15, "duration": 0.05, "harmonics": True, "channels": [6, 8], "enabled": False},
+            "distractor2": {"freq": 1060, "amp": 0.25, "duration": 0.05, "harmonics": True, "channels": [6, 8], "enabled": False},
+            "distractor3": {"freq": 1320, "amp": 0.2, "duration": 0.05, "harmonics": True, "channels": [6, 8], "enabled": False}
+        },
+        "pulse_duration": 0.05,
+        "sample_rate": 44100,
+        "latency": 0.25,
+        "volume": 0.7,
+        "roving": 5.0,
+        "file_path": "sounds.csv"
+    }
+    
+    commutator = {
+        -1: 'noise',
+        0:  'silence',
+        1:  'background',
+        2:  'target',
+        3:  'distractor1',
+        4:  'distractor2',
+        5:  'distractor3',
+        6:  'distractor4',
+        7:  'distractor5'
+    }
+    
+    @classmethod
+    def get_pure_tone(cls, freq, duration, sample_rate=44100):
+        x = np.linspace(0, duration * freq * 2*np.pi, int(duration*sample_rate), dtype=np.float32)
+        return np.sin(x)
+
+    @classmethod
+    def get_harm_stack(cls, base_freq, duration, threshold=1500, sample_rate=44100):
+        harmonics = [x * base_freq for x in np.arange(20) + 2 if x * base_freq < threshold]  # first 20 enouch
+        freqs = [base_freq] + harmonics
+        x = np.linspace(0, duration, int(sample_rate * duration))
+        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)])
+        return y / y.max()  # norm to -1 to 1
+    
+    @classmethod
+    def get_cos_window(cls, tone, win_duration, sample_rate=44100):
+        x = np.linspace(0, np.pi/2, int(win_duration * sample_rate), dtype=np.float32)
+        onset =  np.sin(x)
+        middle = np.ones(len(tone) - 2 * len(x))
+        offset = np.cos(x)
+        return np.concatenate([onset, middle, offset])
+
+    @classmethod
+    def get_tone_stack(cls, cfg):
+        # silence
+        silence = np.zeros(2, dtype='float32')
+        sounds = {'silence': np.column_stack([silence for x in range(cfg['n_channels'])])}
+
+        # noise
+        filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])
+        filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])
+
+        noise = np.random.randn(int(0.25 * cfg['sample_rate']))  # 250ms of noise
+        noise = lfilter(filter_a, filter_b, noise)
+        noise = noise / np.abs(noise).max() * cfg['sounds']['noise']['amp']
+        noise = noise.astype(np.float32)
+        empty = np.zeros((len(noise), cfg['n_channels']), dtype='float32')
+        for ch in cfg['sounds']['noise']['channels']:
+            empty[:, ch-1] = noise
+        sounds['noise'] = empty
+        
+        # all other sounds
+        for key, snd in cfg['sounds'].items():
+            if key == 'noise' or ('enabled' in snd and not snd['enabled']):
+                continue  # skip noise or unused sounds
+                
+            if 'harmonics' in snd and snd['harmonics']:
+                tone = cls.get_harm_stack(snd['freq'], snd['duration'], sample_rate=cfg['sample_rate']) * cfg['volume']
+            else:
+                tone = cls.get_pure_tone(snd['freq'], snd['duration'], cfg['sample_rate']) * cfg['volume']
+            tone = tone * cls.get_cos_window(tone, 0.01, cfg['sample_rate'])  # onset / offset
+            tone = tone * snd['amp']  # amplitude
+            
+            sound = np.zeros([len(tone), cfg['n_channels']], dtype='float32')
+            for j in snd['channels']:
+                sound[:, j-1] = tone
+           
+            sounds[key] = sound
+
+        return sounds
+        
+    @classmethod
+    def run(cls, selector, status, cfg, commutator):
+        """
+        selector        mp.Value object to set the sound to be played
+        status          mp.Value object to stop the loop
+        """
+        import sounddevice as sd  # must be inside the function
+        import numpy as np
+        import time
+        
+        import soundfile as sf # chirp changes
+        
+        
+        sounds = cls.get_tone_stack(cfg)
+
+        sd.default.device = cfg['device']
+        sd.default.samplerate = cfg['sample_rate']
+        
+        data, fs = sf.read(cfg['wav_file'], dtype='float32') #chirp changes
+        chirp = np.zeros((len(data),cfg['n_channels']),dtype='float32') #chirp changes
+        chirp[:,cfg['sounds']['target']['channels'][0]-1] = 0.07*data #chirp changes, amplitude for chirp=0.07, click=1.0
+        chirp[:,6] = 0.07*data #chirp changes, amplitude for chirp=0.07, click=1.0
+        
+        stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=cfg['n_channels'], dtype='float32', blocksize=256)
+        stream.start()
+
+        next_beat = time.time() + cfg['latency']
+        with open(cfg['file_path'], 'w') as f:
+            f.write("time,id\n")
+
+        while status.value > 0:
+            if status.value == 2 or (status.value == 1 and selector.value == -1):  # running state or masking noise
+                t0 = time.time()
+                if t0 < next_beat:
+                    #time.sleep(0.0001)  # not to spin the wheels too much
+                    if stream.write_available > 2:
+                        stream.write(sounds['silence'])  # silence
+                    continue
+
+                roving = 10**((np.random.rand() * cfg['roving'] - cfg['roving']/2.0)/20.)
+                roving = roving if int(selector.value) > -1 else 1  # no roving for noise
+#                 stream.write(sounds[commutator[int(selector.value)]] * roving) # chirp changes (uncomment->comment)
+                stream.write(chirp) # chirp changes
+                if status.value == 2:
+                    with open(cfg['file_path'], 'a') as f:
+                        f.write(",".join([str(x) for x in (t0, selector.value)]) + "\n")
+
+                next_beat += cfg['latency']
+                
+                if stream.write_available > 2:
+                    stream.write(sounds['silence'])  # silence
+            
+            else:  # idle state
+                next_beat = time.time() + cfg['latency']
+                time.sleep(0.005)
+                
+        stream.stop()
+        print('Sound stopped')
+
+        
+class ContinuousSoundStream:
+   
+    default_cfg = {
+        'wav_file': os.path.join('..', 'assets', 'stream1.wav'),
+        'chunk_duration': 20,
+        'chunk_offset': 2
+    }
+    
+    def __init__(self, cfg):
+        from scipy.io import wavfile
+        import sounddevice as sd
+
+        self.cfg = cfg
+        self.stopped = False
+        self.samplerate, self.data = wavfile.read(cfg['wav_file'])
+        self.stream = sd.OutputStream(samplerate=self.samplerate, channels=2, dtype=self.data.dtype)
+
+    def start(self):
+        self._th = threading.Thread(target=self.update, args=())
+        self._th.start()
+
+    def stop(self):
+        self.stopped = True
+        self._th.join()
+        print('Continuous sound stream released')
+            
+    def update(self):
+        self.stream.start()
+        print('Continuous sound stream started at %s Hz' % (self.samplerate))
+        
+        offset = int(self.cfg['chunk_offset'] * self.samplerate)
+        chunk =  int(self.cfg['chunk_duration'] * self.samplerate)
+        
+        while not self.stopped:
+            start_idx = offset + np.random.randint(self.data.shape[0] - 2 * offset - chunk)
+            end_idx = start_idx + chunk
+            self.stream.write(self.data[start_idx:end_idx])
+            
+        self.stream.stop()
+        
+        
+class SoundControllerPR:
+    
+    default_cfg = {
+        "device": [1, 26],
+        "n_channels": 10,
+        "sounds": {
+            "noise": {"amp": 0.2, "duration": 2.0, "channels": [6, 8]},
+            "target": {"freq": 660, "amp": 0.1, "duration": 2.0}, 
+        },
+        "sample_rate": 44100,
+        "volume": 0.7,
+        "file_path": "sounds.csv"
+    }
+        
+    def __init__(self, status, cfg):
+        import sounddevice as sd  # must be inside the function
+        import numpy as np
+        import time
+
+        sd.default.device = cfg['device']
+        sd.default.samplerate = cfg['sample_rate']
+        self.stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=cfg['n_channels'], dtype='float32', blocksize=256)
+        self.stream.start()
+
+        self.timers = []
+        self.status = status
+        self.cfg = cfg
+        
+        # noise (not assigned to channels)
+        filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])
+        filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])
+
+        noise = np.random.randn(int(cfg['sounds']['noise']['duration'] * cfg['sample_rate']))
+        noise = lfilter(filter_a, filter_b, noise)
+        noise = noise / np.abs(noise).max() * cfg['sounds']['noise']['amp']
+        noise = noise.astype(np.float32)
+
+        # target (not assigned to channels)
+        sample_rate = cfg['sample_rate']
+        target_cfg = cfg['sounds']['target']
+
+        tone = SoundController.get_pure_tone(target_cfg['freq'], target_cfg['duration'], sample_rate=cfg['sample_rate'])
+        tone = tone * SoundController.get_cos_window(tone, target_cfg['window'], sample_rate=cfg['sample_rate'])
+
+        if target_cfg['number'] > 1:
+            silence = np.zeros( int(target_cfg['iti'] * cfg['sample_rate']) )
+            tone_with_iti = np.concatenate([tone, silence])
+            target = np.concatenate([tone_with_iti for i in range(target_cfg['number'] - 1)])
+            target = np.concatenate([target, tone])
+        else:
+            target = tone
+            
+        target = target * target_cfg['amp']  # amplitude
+       
+        #snd = cfg['sounds']['target']
+        #target = SoundController.get_pure_tone(snd['freq'], snd['duration'], cfg['sample_rate']) * cfg['volume']
+        #target = target * SoundController.get_cos_window(target, 0.01, cfg['sample_rate'])  # onset / offset
+        #target = target * snd['amp']  # amplitude
+        
+        self.sounds = {'noise': noise, 'target': target}
+        
+    def target(self, hd_angle):
+        to_play = np.zeros((len(self.sounds['target']), self.cfg['n_channels']), dtype='float32')
+        channel = random.choice(self.cfg['sounds']['target']['channels'])  # random speaker!
+        
+        to_play[:, channel-1] = self.sounds['target']
+            
+        t0 = time.time()
+        with open(self.cfg['file_path'], 'a') as f:
+            f.write(",".join([str(x) for x in (t0, 2, channel)]) + "\n")
+        
+        self.stream.write(to_play)
+        
+    def noise(self):
+        to_play = np.zeros((len(self.sounds['noise']), self.cfg['n_channels']), dtype='float32')
+        for ch in self.cfg['sounds']['noise']['channels']:
+            to_play[:, ch-1] = self.sounds['noise']
+        
+        ch1 = self.cfg['sounds']['noise']['channels'][0]
+        t0 = time.time()
+        with open(self.cfg['file_path'], 'a') as f:
+            f.write(",".join([str(x) for x in (t0, -1, ch1)]) + "\n")
+        
+        self.stream.write(to_play)
+            
+    def play_non_blocking(self, sound_id, hd_angle=0):
+        if sound_id == 'target':
+            tf = threading.Timer(0, self.target, args=[hd_angle])
+        elif sound_id == 'noise':
+            tf = threading.Timer(0, self.noise, args=[])
+        tf.start()
+        self.timers.append(tf)
+        
+    def stop(self):
+        for t in self.timers:
+            t.cancel()
+        self.stream.stop()

File diff suppressed because it is too large
+ 44 - 21
passive.ipynb


+ 11 - 1
profiles/default.json

@@ -99,6 +99,14 @@
                 "enabled": false
             }
         },
+        "cont_noise": {
+            "filepath": "stream2.wav",
+            "amp": 0.1,
+            "channels": [
+                3
+            ],
+            "enabled": true
+        },
         "pulse_duration": 0.05,
         "sample_rate": 192000,
         "latency": 0.25,
@@ -131,8 +139,10 @@
         ],
         "iti_distance": 2.0,
         "iti_duration": 20,
+        "silence_before": 0,
+        "silence_after": 0,
         "punishment_duration": 10,
-        "distractor_islands": 1,
+        "distractor_islands": 0,
         "distractor_fail": false,
         "enable_motors": false,
         "n_pellets": 1