Browse Source

merged conflicts

Andrey Sobolev 7 months ago
parent
commit
33602747d4

+ 6 - 7
SIT-PR.ipynb

@@ -497,16 +497,15 @@
    "metadata": {},
    "outputs": [
     {
-     "ename": "ValueError",
-     "evalue": "Wrong number of columns at line 3",
+     "ename": "IndexError",
+     "evalue": "too many indices for array: array is 1-dimensional, but 2 were indexed",
      "output_type": "error",
      "traceback": [
       "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
-      "\u001b[1;31mValueError\u001b[0m                                Traceback (most recent call last)",
-      "Cell \u001b[1;32mIn [12], line 5\u001b[0m\n\u001b[0;32m      2\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mSystemExit\u001b[39;00m(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mNothing recorded. No sense to continue.\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m      4\u001b[0m \u001b[38;5;66;03m# do pack data to HDF5\u001b[39;00m\n\u001b[1;32m----> 5\u001b[0m h5name \u001b[38;5;241m=\u001b[39m \u001b[43mpack\u001b[49m\u001b[43m(\u001b[49m\u001b[43msession_path\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m      6\u001b[0m trial \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0\u001b[39m\n",
-      "File \u001b[1;32mD:\\code\\runSIT\\..\\pipeline\\postprocessing\\pack.py:59\u001b[0m, in \u001b[0;36mpack\u001b[1;34m(session_path)\u001b[0m\n\u001b[0;32m     57\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(filename) \u001b[38;5;28;01mas\u001b[39;00m ff:\n\u001b[0;32m     58\u001b[0m     headers \u001b[38;5;241m=\u001b[39m ff\u001b[38;5;241m.\u001b[39mreadline()\n\u001b[1;32m---> 59\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mloadtxt\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfilename\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdelimiter\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m,\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mskiprows\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m     61\u001b[0m ds \u001b[38;5;241m=\u001b[39m raw\u001b[38;5;241m.\u001b[39mcreate_dataset(ds_name, data\u001b[38;5;241m=\u001b[39mdata)\n\u001b[0;32m     62\u001b[0m ds\u001b[38;5;241m.\u001b[39mattrs[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mheaders\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m headers\n",
-      "File \u001b[1;32mC:\\ProgramData\\Miniconda3\\envs\\P-SIT2\\lib\\site-packages\\numpy\\lib\\npyio.py:1159\u001b[0m, in \u001b[0;36mloadtxt\u001b[1;34m(fname, dtype, comments, delimiter, converters, skiprows, usecols, unpack, ndmin, encoding, max_rows, like)\u001b[0m\n\u001b[0;32m   1157\u001b[0m     words \u001b[38;5;241m=\u001b[39m usecols_getter(words)\n\u001b[0;32m   1158\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(words) \u001b[38;5;241m!=\u001b[39m ncols:\n\u001b[1;32m-> 1159\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[0;32m   1160\u001b[0m         \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mWrong number of columns at line \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mlineno\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m   1161\u001b[0m \u001b[38;5;66;03m# Convert each value according to its column, then pack it\u001b[39;00m\n\u001b[0;32m   1162\u001b[0m \u001b[38;5;66;03m# according to the dtype's nesting, and store it.\u001b[39;00m\n\u001b[0;32m   1163\u001b[0m chunk\u001b[38;5;241m.\u001b[39mappend(packer(convert_row(words)))\n",
-      "\u001b[1;31mValueError\u001b[0m: Wrong number of columns at line 3"
+      "\u001b[1;31mIndexError\u001b[0m                                Traceback (most recent call last)",
+      "\u001b[1;32m<ipython-input-12-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     77\u001b[0m         \u001b[0mevents\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[0mevents\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[0ms_start\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     78\u001b[0m         \u001b[0msounds\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0marray\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mf\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m'raw'\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m'sounds'\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---> 79\u001b[1;33m         \u001b[0msounds\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[0msounds\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[0ms_start\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     80\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     81\u001b[0m         \u001b[1;31m# squeeze - if session was interrupted, adjust times\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: too many indices for array: array is 1-dimensional, but 2 were indexed"
      ]
     }
    ],

+ 88 - 43
SIT.ipynb

@@ -32,6 +32,7 @@
     "from controllers.situtils import FPSTimes\n",
     "from controllers.camera import WebcamStream\n",
     "from controllers.video import VideoWriter\n",
+    "from controllers.microphones import MicrophoneController\n",
     "from controllers.position import PositionTrackerSingle, PositionTrackerDouble\n",
     "from controllers.sound import SoundController, ContinuousSoundStream\n",
     "from controllers.serial import MCSArduino, FakeArduino, SpeakerMotor, CableMotor\n",
@@ -43,6 +44,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -57,12 +59,15 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "cfg_filename = os.path.join('profiles', 'gokce_timeSIT_50_70.json')\n",
-    "\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",
     "#'gokce_timeSIT_90_108.json')\n",
     "\n",
-    "# cfg_filename = os.path.join('profiles', 'social_test_actual.json')\n",
-    "# cfg_filename = os.path.join('profiles', 'andrey_hippoSIT_SL.json')"
+    "# 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",
+    "# cfg_filename = os.path.join('profiles', 'andrey_ppcSIT_SL_009266.json')"
    ]
   },
   {
@@ -79,7 +84,8 @@
     "    cfg_local = json.load(json_file)\n",
     "\n",
     "for key in cfg.keys():\n",
-    "    cfg[key].update(cfg_local[key])\n",
+    "    if key in cfg_local: # only update if the key exists in the local config, otherwise keep default (important for backward compatibility with cfg files before microphones)\n",
+    "        cfg[key].update(cfg_local[key])\n",
     "cfg['experiment']['experiment_date'] = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')\n",
     "\n",
     "# print loaded settings\n",
@@ -100,6 +106,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -110,7 +117,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -124,6 +131,9 @@
     "\n",
     "# update paths (assuming this paths are relative to this notebook)\n",
     "cfg['video']['file_path'] = os.path.join(save_to, cfg['video']['file_path'])\n",
+    "cfg['video']['csv_path'] = os.path.join(save_to, cfg['video']['csv_path'])\n",
+    "cfg['microphones']['file_path'] = os.path.join(save_to, cfg['microphones']['file_path'])\n",
+    "cfg['microphones']['csv_path'] = os.path.join(save_to, cfg['microphones']['csv_path'])\n",
     "cfg['position']['file_path'] = os.path.join(save_to, cfg['position']['file_path'])\n",
     "cfg['position']['contour_path'] = os.path.join(save_to, cfg['position']['contour_path'])\n",
     "cfg['experiment']['file_path'] = os.path.join(save_to, cfg['experiment']['file_path'])\n",
@@ -131,6 +141,8 @@
     "cfg['sound']['file_path'] = os.path.join(save_to, cfg['sound']['file_path'])\n",
     "cfg['position']['background_light'] = os.path.join('assets', cfg['position']['background_light'])\n",
     "cfg['position']['background_dark'] = os.path.join('assets', cfg['position']['background_dark'])\n",
+    "if 'wav_file' in cfg['sound']:\n",
+    "    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",
     "    \n",
@@ -148,7 +160,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -158,7 +170,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -169,7 +181,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 7,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -184,7 +196,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -194,6 +206,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -210,9 +223,9 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 9,
    "metadata": {
-    "scrolled": false
+    "scrolled": true
    },
    "outputs": [
     {
@@ -222,6 +235,7 @@
       "Webcam stream 1024.0:768.0 at 30.00 FPS started\n",
       "Position tracker stopped\n",
       "Video writer stopped\n",
+      "Video writer stopped\n",
       "Camera released\n"
      ]
     }
@@ -258,10 +272,22 @@
     "vs = WebcamStream(cfg['camera'])\n",
     "vs.start()\n",
     "\n",
-    "# init video recorder\n",
-    "vw = VideoWriter(status, vs, cfg['video'])\n",
+    "# init video recorders\n",
+    "vw  = VideoWriter(status, vs, cfg['video'])  # recorder with infos\n",
     "vw.start()\n",
     "\n",
+    "cfg_video_raw = dict(cfg['video'])\n",
+    "cfg_video_raw['file_path']       = os.path.join(save_to, 'raw.avi')\n",
+    "cfg_video_raw['csv_path']        = os.path.join(save_to, 'raw.csv')\n",
+    "cfg_video_raw['frame_attr_name'] = 'frame_raw'\n",
+    "vwr = VideoWriter(status, vs, cfg_video_raw)  # recorder of raw video\n",
+    "vwr.start()\n",
+    "\n",
+    "# init microphone controller, if required\n",
+    "if cfg['microphones']['record_audio']:\n",
+    "    mc = mp.Process(target=MicrophoneController.run, args=(status,cfg[\"microphones\"]))\n",
+    "    mc.start()\n",
+    "\n",
     "# start position tracking\n",
     "pt = PositionTrackerSingle(status, vs, cfg['position']) if cfg['position']['single_agent'] else PositionTrackerDouble(status, vs, cfg['position'])\n",
     "pt.start()\n",
@@ -270,7 +296,7 @@
     "dc = SITDisplay(pt, cfg['video'])\n",
     "\n",
     "# playing sound in a separate process for performance\n",
-    "sc = mp.Process(target=SoundController.run, args=(sound, status, cfg['sound']))\n",
+    "sc = mp.Process(target=SoundController.run, args=(sound, status, cfg['sound'], SoundController.commutator))\n",
     "sc.start()\n",
     "\n",
     "cfg_pos = cfg['position']\n",
@@ -314,6 +340,7 @@
     "        \n",
     "        # ---------- rendering logic -----------\n",
     "        frame = vs.read()\n",
+    "                \n",
     "        if frame is None:\n",
     "            time.sleep(0.1)\n",
     "            continue # wait for the stream\n",
@@ -332,7 +359,6 @@
     "        if len(islands) > 0:  # target island X, Y\n",
     "            target = [i for i in islands if not i.is_distractor][0]\n",
     "            text_infos.append('Target: %.3f %.3f' % (target.x, target.y))\n",
-    "#         print(frame.shape)    \n",
     "        text_infos.append('Time: %.2f' % float(c_time - t_start) if t_start is not None else 'Time: Idle')  # stopwatch\n",
     "        text_infos.append('Trial: %.2f' % float(cfg_exp['trial_duration'] - (c_time - trial_start)) if phase == 1 else 'Trial: not started')\n",
     "        text_infos.append('Trial: %s' % trial)\n",
@@ -340,6 +366,7 @@
     "        text_infos.append('In target: %.2f' % float(c_time - target_since) if target_since is not None else 'In target: --')\n",
     "        text_infos.append('ITI distance: %.2f' % iti_distance if iti_distance > 0 else 'ITI distance: --')\n",
     "\n",
+    "        vs.frame_raw = frame  # save raw frame\n",
     "        frame, frame_to_save = dc.render(frame, status, islands=islands, angles=angles, text_infos=text_infos)\n",
     "            \n",
     "        # assign the frame back to the video stream for other controllers\n",
@@ -364,7 +391,8 @@
     "\n",
     "                    elif c_time - target_since > cfg_exp['target_duration']: # successful trial\n",
     "                        log_event(c_time, round(tgt.x, 4), round(tgt.y, 4), round(tgt.r, 4), trial, 1) # log trial success\n",
-    "                        board.feed()\n",
+    "                        tf = threading.Timer(0, board.feed, args=[])\n",
+    "                        tf.start()\n",
     "                        \n",
     "                        # init inter-trial interval and new trial\n",
     "                        trial_start = c_time + cfg_exp['iti_duration'] + 5 * np.random.rand()  # random b/w 20-25 sec\n",
@@ -521,7 +549,7 @@
     "                    timers.append(threading.Timer(event_t, switch_light, args=(pt, board)))\n",
     "                    \n",
     "                if cfg['experiment']['enable_motors']:\n",
-    "                    if not cfg['experiment']['phi_max'] == 0:\n",
+    "                    if not cfg['experiment']['phi_max'] == 0 and len(cfg['experiment']['timepoints']) > 0:\n",
     "                        direction = False if cfg['experiment']['phi_max'] > 0 else True\n",
     "                        t0, t1 = cfg['experiment']['timepoints'][0], cfg['experiment']['timepoints'][1]\n",
     "                        timers.append(threading.Timer(t0, motor_board.rotate, args=(direction, abs(cfg['experiment']['phi_max']), t1-t0)))\n",
@@ -580,15 +608,20 @@
     "    \n",
     "    cv2.destroyAllWindows()\n",
     "    sc.join()\n",
-    "    for ctrl in [pt, vw, vs]:\n",
+    "    for ctrl in [pt, vw, vwr, vs]:\n",
     "        ctrl.stop()\n",
     "\n",
+    "    # stop microphones recording process\n",
+    "    if cfg['microphones']['record_audio']:\n",
+    "        mc.join()\n",
+    "\n",
     "    if 'continuous' in cfg['sound']:\n",
     "        time.sleep(cfg['sound']['continuous']['end_sleep'])\n",
     "        cst.stop()"
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -597,35 +630,32 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 10,
    "metadata": {},
    "outputs": [],
    "source": [
     "session_path = save_to\n",
+    "#trial = 5\n",
     "#session_path = os.path.join('sessions', '2021-07-30_09-24-14')  # some particular session\n",
-    "#session_path = 'Y:\\\\Michael\\\\FreeBehaving\\\\SIT_sessions\\\\50_aSIT_2021-10-25_10-32-19'"
+    "#session_path = 'Y:\\\\Michael\\\\FreeBehaving\\\\SIT_sessions\\\\51_aSIT_2021-12-03_13-31-51'"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 11,
    "metadata": {},
    "outputs": [
     {
-     "ename": "SystemExit",
-     "evalue": "Nothing recorded. No sense to continue.",
+     "ename": "IndexError",
+     "evalue": "index 0 is out of bounds for axis 0 with size 0",
      "output_type": "error",
      "traceback": [
-      "An exception has occurred, use %tb to see the full traceback.\n",
-      "\u001b[1;31mSystemExit\u001b[0m\u001b[1;31m:\u001b[0m Nothing recorded. No sense to continue.\n"
-     ]
-    },
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "C:\\Users\\inhibition\\.conda\\envs\\runsit\\lib\\site-packages\\IPython\\core\\interactiveshell.py:3445: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.\n",
-      "  warn(\"To exit: use 'exit', 'quit', or Ctrl-D.\", stacklevel=1)\n"
+      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[1;31mIndexError\u001b[0m                                Traceback (most recent call last)",
+      "\u001b[1;32m<ipython-input-11-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"
      ]
     }
    ],
@@ -639,6 +669,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -719,12 +750,22 @@
     "\n",
     "# speed\n",
     "ax = fig.add_subplot(224)\n",
-    "ax.hist(tl[:, 3], bins=50, ec='black')\n",
-    "ax.set_xlabel('Speed, m/s', fontsize=14)\n",
+    "\n",
+    "s_rate = 100  # Hz\n",
+    "window = 60   # secs\n",
+    "step = 10     # secs\n",
+    "duration = tl[-1][0]\n",
+    "x_vals = np.arange(int(duration/step))\n",
+    "\n",
+    "inst_speed = [tl[x*step*s_rate:(x*step + window)*s_rate][:, 3].mean() for x in x_vals]\n",
+    "ax.plot(x_vals*step, inst_speed)\n",
+    "ax.set_ylabel('Speed, m/s', fontsize=14)\n",
+    "ax.set_xlabel('Time, s', fontsize=14)\n",
     "ax.set_title('Speed', fontsize=14)"
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -734,7 +775,9 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "scrolled": true
+   },
    "outputs": [],
    "source": [
     "h5name = os.path.join(session_path, experiment_id + '.h5')\n",
@@ -751,8 +794,10 @@
     "\n",
     "timepoints = cfg['experiment']['timepoints']\n",
     "s_duration = cfg['experiment']['session_duration']\n",
-    "\n",
-    "periods = [[0, s_duration], [0, timepoints[0]], [timepoints[1], timepoints[2]], [timepoints[3], s_duration]]\n",
+    "if len(timepoints) > 0:\n",
+    "    periods = [[0, s_duration], [0, timepoints[0]], [timepoints[1], timepoints[2]], [timepoints[3], s_duration]]\n",
+    "else:\n",
+    "    periods = [[0, s_duration]]\n",
     "\n",
     "# separate ALL, L, D, L'\n",
     "ds_names = ['performance_ALL', 'performance_L', 'performance_D', 'performance_Lp']\n",
@@ -771,9 +816,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "scrolled": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "fig = plt.figure(figsize=(4, 4))\n",
@@ -812,7 +855,9 @@
    "execution_count": null,
    "metadata": {},
    "outputs": [],
-   "source": []
+   "source": [
+    "\n"
+   ]
   }
  ],
  "metadata": {

+ 14 - 2
controllers/camera.ipynb

@@ -95,14 +95,26 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 3,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Webcam stream 1024.0:768.0 at 30.00 FPS started\n"
+      "Webcam stream 1024.0:768.0 at 30.00 FPS started\n",
+      "Camera released\n"
+     ]
+    },
+    {
+     "ename": "KeyboardInterrupt",
+     "evalue": "",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[1;31mKeyboardInterrupt\u001b[0m                         Traceback (most recent call last)",
+      "\u001b[1;32m<ipython-input-3-0e84daf67e4d>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m     10\u001b[0m             \u001b[0mcv2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mimshow\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'Webcam'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mframe\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     11\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 12\u001b[1;33m         \u001b[0mk\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcv2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwaitKey\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m33\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     13\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[0mk\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0mord\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'q'\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     14\u001b[0m             \u001b[1;32mbreak\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
+      "\u001b[1;31mKeyboardInterrupt\u001b[0m: "
      ]
     }
    ],

+ 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",

+ 625 - 0
controllers/microphones.ipynb

@@ -0,0 +1,625 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import queue\n",
+    "import sys\n",
+    "\n",
+    "import sounddevice as sd\n",
+    "import soundfile as sf\n",
+    "import numpy  # Make sure NumPy is loaded before it is used in the callback\n",
+    "assert numpy  # avoid \"imported but unused\" message (W0611)\n",
+    "\n",
+    "from situtils import FPSTimes\n",
+    "\n",
+    "import threading\n",
+    "\n",
+    "import time"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Class method version"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Overwriting microphones.py\n"
+     ]
+    }
+   ],
+   "source": [
+    "%%writefile microphones.py \n",
+    "#^IMPORTANT: essential to make multiprocessing work\n",
+    "\n",
+    "import queue\n",
+    "import sys\n",
+    "\n",
+    "import sounddevice as sd\n",
+    "import soundfile as sf\n",
+    "import numpy  # Make sure NumPy is loaded before it is used in the callback\n",
+    "assert numpy  # avoid \"imported but unused\" message (W0611)\n",
+    "\n",
+    "from situtils import FPSTimes\n",
+    "\n",
+    "import time\n",
+    "\n",
+    "class MicrophoneController(FPSTimes):\n",
+    "    # https://python-sounddevice.readthedocs.io/en/0.3.15/examples.html#recording-with-arbitrary-duration\n",
+    "    \n",
+    "    @staticmethod\n",
+    "    def callback(indata, frames, time, status):\n",
+    "        \"\"\"This is called (from a separate thread) for each audio block.\"\"\"\n",
+    "        if status:\n",
+    "            print(status, file=sys.stderr)\n",
+    "        MicrophoneController.queue.put(indata.copy())\n",
+    "        \n",
+    "        \n",
+    "    @classmethod\n",
+    "    def run(cls, status, cfg):\n",
+    "        # MicrophoneController.initialize(cfg)\n",
+    "        print(\"Running.\")\n",
+    "        import sounddevice as sd  # must be inside the function\n",
+    "        \n",
+    "        if cfg['channel_selectors']: # make it work without ASIO\n",
+    "            # https://python-sounddevice.readthedocs.io/en/0.3.15/api/platform-specific-settings.html\n",
+    "            asio_in = sd.AsioSettings(channel_selectors=cfg['channel_selectors'])\n",
+    "        else:\n",
+    "            asio_in = None\n",
+    "\n",
+    "        MicrophoneController.queue = queue.Queue() # kind of a hack\n",
+    "\n",
+    "        stream = sd.InputStream(samplerate=cfg['sample_rate'], device=cfg['device'], channels=cfg['number_channels'], callback=MicrophoneController.callback, extra_settings = asio_in)\n",
+    " \n",
+    "        filename = cfg['file_path']\n",
+    "        file = sf.SoundFile(filename, mode='w', samplerate=cfg['sample_rate'], channels=cfg['number_channels'],subtype='PCM_32') # 'w': overwrite mode, 'x': raises error if file exists\n",
+    "\n",
+    "        # experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\n",
+    "        with file as f:\n",
+    "            while status.value > 0:\n",
+    "                try:\n",
+    "                    if status.value == 2:\n",
+    "\n",
+    "                        # start stream if not active yet\n",
+    "                        if not stream.active:\n",
+    "                            print(\"Audio input stream started.\")\n",
+    "                            t0 = time.time()\n",
+    "                            stream.start()\n",
+    "                            with open(cfg['csv_path'], 'a') as f_csv:\n",
+    "                                f_csv.write(\",\".join([str(x) for x in (t0,)]) + \"\\n\")\n",
+    "\n",
+    "                        f.write(MicrophoneController.queue.get())\n",
+    "\n",
+    "                    else:\n",
+    "                        time.sleep(0.005)\n",
+    "                except KeyboardInterrupt:\n",
+    "                    stream.stop()\n",
+    "                    stream.close()\n",
+    "                    break\n",
+    "        "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Instance version"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Writing microphones_instance.py\n"
+     ]
+    }
+   ],
+   "source": [
+    "%%writefile microphones_instance.py \n",
+    "#^IMPORTANT: essential to make multiprocessing work\n",
+    "\n",
+    "import queue\n",
+    "import sys\n",
+    "\n",
+    "import sounddevice as sd\n",
+    "import soundfile as sf\n",
+    "import numpy  # Make sure NumPy is loaded before it is used in the callback\n",
+    "assert numpy  # avoid \"imported but unused\" message (W0611)\n",
+    "\n",
+    "from situtils import FPSTimes\n",
+    "\n",
+    "import time\n",
+    "\n",
+    "import threading\n",
+    "\n",
+    "class MicrophoneControllerInstance(FPSTimes):\n",
+    "    # https://python-sounddevice.readthedocs.io/en/0.3.15/examples.html#recording-with-arbitrary-duration\n",
+    "    def __init__(self, status, cfg):\n",
+    "        import sounddevice as sd  # must be inside the function? TODO\n",
+    "        self.cfg = cfg\n",
+    "        self.samplerate = cfg['sample_rate']\n",
+    "        self.device = cfg['device']\n",
+    "        self.channels = cfg['number_channels']\n",
+    "        self.status = status\n",
+    "        \n",
+    "        if cfg['channel_selectors']: # make it work without ASIO\n",
+    "            # https://python-sounddevice.readthedocs.io/en/0.3.15/api/platform-specific-settings.html\n",
+    "            self.channel_selectors = cfg['channel_selectors']\n",
+    "            asio_in = sd.AsioSettings(channel_selectors=self.channel_selectors)\n",
+    "        else:\n",
+    "            asio_in = None\n",
+    "\n",
+    "        MicrophoneControllerInstance.queue = queue.Queue() # kind of a hack\n",
+    "        \n",
+    "        self.stream = sd.InputStream(samplerate=self.samplerate, device=self.device, channels=self.channels, callback=self.callback, extra_settings = asio_in)\n",
+    "\n",
+    "        self.filename = cfg['file_path']\n",
+    "        self.file = sf.SoundFile(self.filename, mode='w', samplerate=self.samplerate, channels=self.channels) # 'w': overwrite mode, 'x': raises error if file exists\n",
+    "        \n",
+    "    \n",
+    "    def start(self):\n",
+    "        self._th = threading.Thread(target=self.run, args=())\n",
+    "        self._th.start()\n",
+    "\n",
+    "    def stop(self):\n",
+    "        time.sleep(0.2)   # wait until device is released\n",
+    "        self._th.join()\n",
+    "        print('Microphones recording stopped')\n",
+    "\n",
+    "    def start_stream(self):\n",
+    "        self.stream.start()\n",
+    "\n",
+    "    def stop_stream(self):\n",
+    "        self.stream.stop()\n",
+    "        self.stream.close()\n",
+    "\n",
+    "    # Used in all versions\n",
+    "    @staticmethod\n",
+    "    def callback(indata, frames, time, status):\n",
+    "        \"\"\"This is called (from a separate thread) for each audio block.\"\"\"\n",
+    "        if status:\n",
+    "            print(status, file=sys.stderr)\n",
+    "        MicrophoneControllerInstance.queue.put(indata.copy())\n",
+    "    \n",
+    "    # instance method\n",
+    "    def run(self):\n",
+    "        import sounddevice as sd  # must be inside the function? TODO\n",
+    "        # experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\n",
+    "        with self.file as file:\n",
+    "            while self.status.value > 0:\n",
+    "                if self.status.value == 2:\n",
+    "                    \n",
+    "                    # start stream if not active yet\n",
+    "                    if not self.stream.active:\n",
+    "                        print(\"Audio input stream started.\")\n",
+    "                        self.stream.start()\n",
+    "                    \n",
+    "                    file.write(MicrophoneControllerInstance.queue.get())\n",
+    "        \n",
+    "        self.stop_stream()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Test microphones"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Import config file"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 47,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'record_audio': True,\n",
+       " 'sample_rate': 44000,\n",
+       " 'device': 1,\n",
+       " 'number_channels': 2,\n",
+       " 'channel_selectors': False,\n",
+       " 'file_path': 'audio_new.mat5'}"
+      ]
+     },
+     "execution_count": 47,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "import json\n",
+    "import os\n",
+    "import multiprocess as mp\n",
+    "\n",
+    "# cfg_filename = os.path.join('..','profiles', 'miguel_socialSIT_test.json')\n",
+    "cfg_filename = os.path.join('..','profiles', 'miguel_socialSIT_test_lord_sith.json')\n",
+    "with open(cfg_filename) as json_file:\n",
+    "    cfg = json.load(json_file)\n",
+    "\n",
+    "cfg[\"microphones\"]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Test version using instance methods and no parallelization -> Works!"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Recording started\n",
+      "Audio input stream started.\n",
+      "Recording stopped\n"
+     ]
+    }
+   ],
+   "source": [
+    "# experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\n",
+    "status = mp.Value('i', 1)\n",
+    "\n",
+    "mc = MicrophoneControllerInstance(status, cfg[\"microphones\"])\n",
+    "try:\n",
+    "    status.value = 2\n",
+    "    print(\"Recording started\")\n",
+    "    mc.run()\n",
+    "except KeyboardInterrupt:\n",
+    "    status.value = 0\n",
+    "    mc.stop_stream()\n",
+    "    print(\"Recording stopped\")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Test with class method instead of instance method -> Works!"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Running.\n",
+      "Audio input stream started.\n"
+     ]
+    },
+    {
+     "ename": "TypeError",
+     "evalue": "write() argument must be str, not numpy.ndarray",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[1;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
+      "Cell \u001b[1;32mIn[22], line 4\u001b[0m\n\u001b[0;32m      2\u001b[0m \u001b[39m# experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\u001b[39;00m\n\u001b[0;32m      3\u001b[0m status \u001b[39m=\u001b[39m mp\u001b[39m.\u001b[39mValue(\u001b[39m'\u001b[39m\u001b[39mi\u001b[39m\u001b[39m'\u001b[39m, \u001b[39m2\u001b[39m)\n\u001b[1;32m----> 4\u001b[0m MicrophoneController\u001b[39m.\u001b[39;49mrun(status,cfg[\u001b[39m\"\u001b[39;49m\u001b[39mmicrophones\u001b[39;49m\u001b[39m\"\u001b[39;49m])\n\u001b[0;32m      5\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m\"\u001b[39m\u001b[39mRecording stopped\u001b[39m\u001b[39m\"\u001b[39m)\n",
+      "File \u001b[1;32mn:\\Miguel\\runSIT\\controllers\\microphones.py:77\u001b[0m, in \u001b[0;36mMicrophoneController.run\u001b[1;34m(cls, status, cfg)\u001b[0m\n\u001b[0;32m     75\u001b[0m         \u001b[39mprint\u001b[39m(\u001b[39m\"\u001b[39m\u001b[39mAudio input stream started.\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[0;32m     76\u001b[0m         stream\u001b[39m.\u001b[39mstart()\n\u001b[1;32m---> 77\u001b[0m     \u001b[39mprint\u001b[39m(MicrophoneController\u001b[39m.\u001b[39mqueue\u001b[39m.\u001b[39mget())\n\u001b[0;32m     79\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m     80\u001b[0m     time\u001b[39m.\u001b[39msleep(\u001b[39m0.01\u001b[39m)\n",
+      "\u001b[1;31mTypeError\u001b[0m: write() argument must be str, not numpy.ndarray"
+     ]
+    }
+   ],
+   "source": [
+    "from microphones import MicrophoneController\n",
+    "# experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\n",
+    "status = mp.Value('i', 2)\n",
+    "MicrophoneController.run(status,cfg[\"microphones\"])\n",
+    "print(\"Recording stopped\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Test with multiprocessing using class method -> Works!"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 83,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from microphones import MicrophoneController # IMPORTANT: class must be imported from separate .py file, not from the notebook\n",
+    "\n",
+    "# experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\n",
+    "status = mp.Value('i', 1)\n",
+    "\n",
+    "mc = mp.Process(target=MicrophoneController.run, args=(status,cfg[\"microphones\"]))\n",
+    "mc.start()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 84,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<Process name='Process-12' pid=11076 parent=11092 started>"
+      ]
+     },
+     "execution_count": 84,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "mc"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 85,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Recording started\n"
+     ]
+    }
+   ],
+   "source": [
+    "status.value = 2\n",
+    "\n",
+    "print(\"Recording started\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 86,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "status.value = 0\n",
+    "\n",
+    "mc.join()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Test with multiprocessing using instance method -> Error!"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "ename": "TypeError",
+     "evalue": "cannot pickle '_cffi_backend.__CDataOwnGC' object",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[1;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
+      "Cell \u001b[1;32mIn[23], line 9\u001b[0m\n\u001b[0;32m      6\u001b[0m microphoneController \u001b[39m=\u001b[39m MicrophoneControllerInstance(status, cfg[\u001b[39m\"\u001b[39m\u001b[39mmicrophones\u001b[39m\u001b[39m\"\u001b[39m])\n\u001b[0;32m      8\u001b[0m mc \u001b[39m=\u001b[39m mp\u001b[39m.\u001b[39mProcess(target\u001b[39m=\u001b[39mmicrophoneController\u001b[39m.\u001b[39mrun, args\u001b[39m=\u001b[39m())\n\u001b[1;32m----> 9\u001b[0m mc\u001b[39m.\u001b[39;49mstart()\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\multiprocess\\process.py:121\u001b[0m, in \u001b[0;36mBaseProcess.start\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    118\u001b[0m \u001b[39massert\u001b[39;00m \u001b[39mnot\u001b[39;00m _current_process\u001b[39m.\u001b[39m_config\u001b[39m.\u001b[39mget(\u001b[39m'\u001b[39m\u001b[39mdaemon\u001b[39m\u001b[39m'\u001b[39m), \\\n\u001b[0;32m    119\u001b[0m        \u001b[39m'\u001b[39m\u001b[39mdaemonic processes are not allowed to have children\u001b[39m\u001b[39m'\u001b[39m\n\u001b[0;32m    120\u001b[0m _cleanup()\n\u001b[1;32m--> 121\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_popen \u001b[39m=\u001b[39m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_Popen(\u001b[39mself\u001b[39;49m)\n\u001b[0;32m    122\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_sentinel \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_popen\u001b[39m.\u001b[39msentinel\n\u001b[0;32m    123\u001b[0m \u001b[39m# Avoid a refcycle if the target function holds an indirect\u001b[39;00m\n\u001b[0;32m    124\u001b[0m \u001b[39m# reference to the process object (see bpo-30775)\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\multiprocess\\context.py:224\u001b[0m, in \u001b[0;36mProcess._Popen\u001b[1;34m(process_obj)\u001b[0m\n\u001b[0;32m    222\u001b[0m \u001b[39m@staticmethod\u001b[39m\n\u001b[0;32m    223\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39m_Popen\u001b[39m(process_obj):\n\u001b[1;32m--> 224\u001b[0m     \u001b[39mreturn\u001b[39;00m _default_context\u001b[39m.\u001b[39;49mget_context()\u001b[39m.\u001b[39;49mProcess\u001b[39m.\u001b[39;49m_Popen(process_obj)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\multiprocess\\context.py:336\u001b[0m, in \u001b[0;36mSpawnProcess._Popen\u001b[1;34m(process_obj)\u001b[0m\n\u001b[0;32m    333\u001b[0m \u001b[39m@staticmethod\u001b[39m\n\u001b[0;32m    334\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39m_Popen\u001b[39m(process_obj):\n\u001b[0;32m    335\u001b[0m     \u001b[39mfrom\u001b[39;00m \u001b[39m.\u001b[39;00m\u001b[39mpopen_spawn_win32\u001b[39;00m \u001b[39mimport\u001b[39;00m Popen\n\u001b[1;32m--> 336\u001b[0m     \u001b[39mreturn\u001b[39;00m Popen(process_obj)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\multiprocess\\popen_spawn_win32.py:93\u001b[0m, in \u001b[0;36mPopen.__init__\u001b[1;34m(self, process_obj)\u001b[0m\n\u001b[0;32m     91\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m     92\u001b[0m     reduction\u001b[39m.\u001b[39mdump(prep_data, to_child)\n\u001b[1;32m---> 93\u001b[0m     reduction\u001b[39m.\u001b[39;49mdump(process_obj, to_child)\n\u001b[0;32m     94\u001b[0m \u001b[39mfinally\u001b[39;00m:\n\u001b[0;32m     95\u001b[0m     set_spawning_popen(\u001b[39mNone\u001b[39;00m)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\multiprocess\\reduction.py:63\u001b[0m, in \u001b[0;36mdump\u001b[1;34m(obj, file, protocol, *args, **kwds)\u001b[0m\n\u001b[0;32m     61\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mdump\u001b[39m(obj, file, protocol\u001b[39m=\u001b[39m\u001b[39mNone\u001b[39;00m, \u001b[39m*\u001b[39margs, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwds):\n\u001b[0;32m     62\u001b[0m \u001b[39m    \u001b[39m\u001b[39m'''Replacement for pickle.dump() using ForkingPickler.'''\u001b[39;00m\n\u001b[1;32m---> 63\u001b[0m     ForkingPickler(file, protocol, \u001b[39m*\u001b[39;49margs, \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49mkwds)\u001b[39m.\u001b[39;49mdump(obj)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:394\u001b[0m, in \u001b[0;36mPickler.dump\u001b[1;34m(self, obj)\u001b[0m\n\u001b[0;32m    392\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mdump\u001b[39m(\u001b[39mself\u001b[39m, obj): \u001b[39m#NOTE: if settings change, need to update attributes\u001b[39;00m\n\u001b[0;32m    393\u001b[0m     logger\u001b[39m.\u001b[39mtrace_setup(\u001b[39mself\u001b[39m)\n\u001b[1;32m--> 394\u001b[0m     StockPickler\u001b[39m.\u001b[39;49mdump(\u001b[39mself\u001b[39;49m, obj)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:487\u001b[0m, in \u001b[0;36m_Pickler.dump\u001b[1;34m(self, obj)\u001b[0m\n\u001b[0;32m    485\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mproto \u001b[39m>\u001b[39m\u001b[39m=\u001b[39m \u001b[39m4\u001b[39m:\n\u001b[0;32m    486\u001b[0m     \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mframer\u001b[39m.\u001b[39mstart_framing()\n\u001b[1;32m--> 487\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49msave(obj)\n\u001b[0;32m    488\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mwrite(STOP)\n\u001b[0;32m    489\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mframer\u001b[39m.\u001b[39mend_framing()\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:388\u001b[0m, in \u001b[0;36mPickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    386\u001b[0m     msg \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mCan\u001b[39m\u001b[39m'\u001b[39m\u001b[39mt pickle \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m: attribute lookup builtins.generator failed\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m GeneratorType\n\u001b[0;32m    387\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(msg)\n\u001b[1;32m--> 388\u001b[0m StockPickler\u001b[39m.\u001b[39;49msave(\u001b[39mself\u001b[39;49m, obj, save_persistent_id)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:603\u001b[0m, in \u001b[0;36m_Pickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    599\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(\u001b[39m\"\u001b[39m\u001b[39mTuple returned by \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m must have \u001b[39m\u001b[39m\"\u001b[39m\n\u001b[0;32m    600\u001b[0m                         \u001b[39m\"\u001b[39m\u001b[39mtwo to six elements\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m reduce)\n\u001b[0;32m    602\u001b[0m \u001b[39m# Save the reduce() output and finally memoize the object\u001b[39;00m\n\u001b[1;32m--> 603\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49msave_reduce(obj\u001b[39m=\u001b[39;49mobj, \u001b[39m*\u001b[39;49mrv)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:717\u001b[0m, in \u001b[0;36m_Pickler.save_reduce\u001b[1;34m(self, func, args, state, listitems, dictitems, state_setter, obj)\u001b[0m\n\u001b[0;32m    715\u001b[0m \u001b[39mif\u001b[39;00m state \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m    716\u001b[0m     \u001b[39mif\u001b[39;00m state_setter \u001b[39mis\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m--> 717\u001b[0m         save(state)\n\u001b[0;32m    718\u001b[0m         write(BUILD)\n\u001b[0;32m    719\u001b[0m     \u001b[39melse\u001b[39;00m:\n\u001b[0;32m    720\u001b[0m         \u001b[39m# If a state_setter is specified, call it instead of load_build\u001b[39;00m\n\u001b[0;32m    721\u001b[0m         \u001b[39m# to update obj's with its previous state.\u001b[39;00m\n\u001b[0;32m    722\u001b[0m         \u001b[39m# First, push state_setter and its tuple of expected arguments\u001b[39;00m\n\u001b[0;32m    723\u001b[0m         \u001b[39m# (obj, state) onto the stack.\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:388\u001b[0m, in \u001b[0;36mPickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    386\u001b[0m     msg \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mCan\u001b[39m\u001b[39m'\u001b[39m\u001b[39mt pickle \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m: attribute lookup builtins.generator failed\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m GeneratorType\n\u001b[0;32m    387\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(msg)\n\u001b[1;32m--> 388\u001b[0m StockPickler\u001b[39m.\u001b[39;49msave(\u001b[39mself\u001b[39;49m, obj, save_persistent_id)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:560\u001b[0m, in \u001b[0;36m_Pickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    558\u001b[0m f \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mdispatch\u001b[39m.\u001b[39mget(t)\n\u001b[0;32m    559\u001b[0m \u001b[39mif\u001b[39;00m f \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m--> 560\u001b[0m     f(\u001b[39mself\u001b[39;49m, obj)  \u001b[39m# Call unbound method with explicit self\u001b[39;00m\n\u001b[0;32m    561\u001b[0m     \u001b[39mreturn\u001b[39;00m\n\u001b[0;32m    563\u001b[0m \u001b[39m# Check private dispatch table if any, or else\u001b[39;00m\n\u001b[0;32m    564\u001b[0m \u001b[39m# copyreg.dispatch_table\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:1186\u001b[0m, in \u001b[0;36msave_module_dict\u001b[1;34m(pickler, obj)\u001b[0m\n\u001b[0;32m   1183\u001b[0m     \u001b[39mif\u001b[39;00m is_dill(pickler, child\u001b[39m=\u001b[39m\u001b[39mFalse\u001b[39;00m) \u001b[39mand\u001b[39;00m pickler\u001b[39m.\u001b[39m_session:\n\u001b[0;32m   1184\u001b[0m         \u001b[39m# we only care about session the first pass thru\u001b[39;00m\n\u001b[0;32m   1185\u001b[0m         pickler\u001b[39m.\u001b[39m_first_pass \u001b[39m=\u001b[39m \u001b[39mFalse\u001b[39;00m\n\u001b[1;32m-> 1186\u001b[0m     StockPickler\u001b[39m.\u001b[39;49msave_dict(pickler, obj)\n\u001b[0;32m   1187\u001b[0m     logger\u001b[39m.\u001b[39mtrace(pickler, \u001b[39m\"\u001b[39m\u001b[39m# D2\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[0;32m   1188\u001b[0m \u001b[39mreturn\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:972\u001b[0m, in \u001b[0;36m_Pickler.save_dict\u001b[1;34m(self, obj)\u001b[0m\n\u001b[0;32m    969\u001b[0m     \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mwrite(MARK \u001b[39m+\u001b[39m DICT)\n\u001b[0;32m    971\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mmemoize(obj)\n\u001b[1;32m--> 972\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_batch_setitems(obj\u001b[39m.\u001b[39;49mitems())\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:998\u001b[0m, in \u001b[0;36m_Pickler._batch_setitems\u001b[1;34m(self, items)\u001b[0m\n\u001b[0;32m    996\u001b[0m     \u001b[39mfor\u001b[39;00m k, v \u001b[39min\u001b[39;00m tmp:\n\u001b[0;32m    997\u001b[0m         save(k)\n\u001b[1;32m--> 998\u001b[0m         save(v)\n\u001b[0;32m    999\u001b[0m     write(SETITEMS)\n\u001b[0;32m   1000\u001b[0m \u001b[39melif\u001b[39;00m n:\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:388\u001b[0m, in \u001b[0;36mPickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    386\u001b[0m     msg \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mCan\u001b[39m\u001b[39m'\u001b[39m\u001b[39mt pickle \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m: attribute lookup builtins.generator failed\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m GeneratorType\n\u001b[0;32m    387\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(msg)\n\u001b[1;32m--> 388\u001b[0m StockPickler\u001b[39m.\u001b[39;49msave(\u001b[39mself\u001b[39;49m, obj, save_persistent_id)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:560\u001b[0m, in \u001b[0;36m_Pickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    558\u001b[0m f \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mdispatch\u001b[39m.\u001b[39mget(t)\n\u001b[0;32m    559\u001b[0m \u001b[39mif\u001b[39;00m f \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m--> 560\u001b[0m     f(\u001b[39mself\u001b[39;49m, obj)  \u001b[39m# Call unbound method with explicit self\u001b[39;00m\n\u001b[0;32m    561\u001b[0m     \u001b[39mreturn\u001b[39;00m\n\u001b[0;32m    563\u001b[0m \u001b[39m# Check private dispatch table if any, or else\u001b[39;00m\n\u001b[0;32m    564\u001b[0m \u001b[39m# copyreg.dispatch_table\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:1427\u001b[0m, in \u001b[0;36msave_instancemethod0\u001b[1;34m(pickler, obj)\u001b[0m\n\u001b[0;32m   1424\u001b[0m \u001b[39m@register\u001b[39m(MethodType)\n\u001b[0;32m   1425\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39msave_instancemethod0\u001b[39m(pickler, obj):\n\u001b[0;32m   1426\u001b[0m     logger\u001b[39m.\u001b[39mtrace(pickler, \u001b[39m\"\u001b[39m\u001b[39mMe1: \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m\"\u001b[39m, obj)\n\u001b[1;32m-> 1427\u001b[0m     pickler\u001b[39m.\u001b[39;49msave_reduce(MethodType, (obj\u001b[39m.\u001b[39;49m\u001b[39m__func__\u001b[39;49m, obj\u001b[39m.\u001b[39;49m\u001b[39m__self__\u001b[39;49m), obj\u001b[39m=\u001b[39;49mobj)\n\u001b[0;32m   1428\u001b[0m     logger\u001b[39m.\u001b[39mtrace(pickler, \u001b[39m\"\u001b[39m\u001b[39m# Me1\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[0;32m   1429\u001b[0m     \u001b[39mreturn\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:692\u001b[0m, in \u001b[0;36m_Pickler.save_reduce\u001b[1;34m(self, func, args, state, listitems, dictitems, state_setter, obj)\u001b[0m\n\u001b[0;32m    690\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m    691\u001b[0m     save(func)\n\u001b[1;32m--> 692\u001b[0m     save(args)\n\u001b[0;32m    693\u001b[0m     write(REDUCE)\n\u001b[0;32m    695\u001b[0m \u001b[39mif\u001b[39;00m obj \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m    696\u001b[0m     \u001b[39m# If the object is already in the memo, this means it is\u001b[39;00m\n\u001b[0;32m    697\u001b[0m     \u001b[39m# recursive. In this case, throw away everything we put on the\u001b[39;00m\n\u001b[0;32m    698\u001b[0m     \u001b[39m# stack, and fetch the object back from the memo.\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:388\u001b[0m, in \u001b[0;36mPickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    386\u001b[0m     msg \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mCan\u001b[39m\u001b[39m'\u001b[39m\u001b[39mt pickle \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m: attribute lookup builtins.generator failed\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m GeneratorType\n\u001b[0;32m    387\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(msg)\n\u001b[1;32m--> 388\u001b[0m StockPickler\u001b[39m.\u001b[39;49msave(\u001b[39mself\u001b[39;49m, obj, save_persistent_id)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:560\u001b[0m, in \u001b[0;36m_Pickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    558\u001b[0m f \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mdispatch\u001b[39m.\u001b[39mget(t)\n\u001b[0;32m    559\u001b[0m \u001b[39mif\u001b[39;00m f \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m--> 560\u001b[0m     f(\u001b[39mself\u001b[39;49m, obj)  \u001b[39m# Call unbound method with explicit self\u001b[39;00m\n\u001b[0;32m    561\u001b[0m     \u001b[39mreturn\u001b[39;00m\n\u001b[0;32m    563\u001b[0m \u001b[39m# Check private dispatch table if any, or else\u001b[39;00m\n\u001b[0;32m    564\u001b[0m \u001b[39m# copyreg.dispatch_table\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:887\u001b[0m, in \u001b[0;36m_Pickler.save_tuple\u001b[1;34m(self, obj)\u001b[0m\n\u001b[0;32m    885\u001b[0m \u001b[39mif\u001b[39;00m n \u001b[39m<\u001b[39m\u001b[39m=\u001b[39m \u001b[39m3\u001b[39m \u001b[39mand\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mproto \u001b[39m>\u001b[39m\u001b[39m=\u001b[39m \u001b[39m2\u001b[39m:\n\u001b[0;32m    886\u001b[0m     \u001b[39mfor\u001b[39;00m element \u001b[39min\u001b[39;00m obj:\n\u001b[1;32m--> 887\u001b[0m         save(element)\n\u001b[0;32m    888\u001b[0m     \u001b[39m# Subtle.  Same as in the big comment below.\u001b[39;00m\n\u001b[0;32m    889\u001b[0m     \u001b[39mif\u001b[39;00m \u001b[39mid\u001b[39m(obj) \u001b[39min\u001b[39;00m memo:\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:388\u001b[0m, in \u001b[0;36mPickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    386\u001b[0m     msg \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mCan\u001b[39m\u001b[39m'\u001b[39m\u001b[39mt pickle \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m: attribute lookup builtins.generator failed\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m GeneratorType\n\u001b[0;32m    387\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(msg)\n\u001b[1;32m--> 388\u001b[0m StockPickler\u001b[39m.\u001b[39;49msave(\u001b[39mself\u001b[39;49m, obj, save_persistent_id)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:603\u001b[0m, in \u001b[0;36m_Pickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    599\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(\u001b[39m\"\u001b[39m\u001b[39mTuple returned by \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m must have \u001b[39m\u001b[39m\"\u001b[39m\n\u001b[0;32m    600\u001b[0m                         \u001b[39m\"\u001b[39m\u001b[39mtwo to six elements\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m reduce)\n\u001b[0;32m    602\u001b[0m \u001b[39m# Save the reduce() output and finally memoize the object\u001b[39;00m\n\u001b[1;32m--> 603\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49msave_reduce(obj\u001b[39m=\u001b[39;49mobj, \u001b[39m*\u001b[39;49mrv)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:717\u001b[0m, in \u001b[0;36m_Pickler.save_reduce\u001b[1;34m(self, func, args, state, listitems, dictitems, state_setter, obj)\u001b[0m\n\u001b[0;32m    715\u001b[0m \u001b[39mif\u001b[39;00m state \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m    716\u001b[0m     \u001b[39mif\u001b[39;00m state_setter \u001b[39mis\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m--> 717\u001b[0m         save(state)\n\u001b[0;32m    718\u001b[0m         write(BUILD)\n\u001b[0;32m    719\u001b[0m     \u001b[39melse\u001b[39;00m:\n\u001b[0;32m    720\u001b[0m         \u001b[39m# If a state_setter is specified, call it instead of load_build\u001b[39;00m\n\u001b[0;32m    721\u001b[0m         \u001b[39m# to update obj's with its previous state.\u001b[39;00m\n\u001b[0;32m    722\u001b[0m         \u001b[39m# First, push state_setter and its tuple of expected arguments\u001b[39;00m\n\u001b[0;32m    723\u001b[0m         \u001b[39m# (obj, state) onto the stack.\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:388\u001b[0m, in \u001b[0;36mPickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    386\u001b[0m     msg \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mCan\u001b[39m\u001b[39m'\u001b[39m\u001b[39mt pickle \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m: attribute lookup builtins.generator failed\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m GeneratorType\n\u001b[0;32m    387\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(msg)\n\u001b[1;32m--> 388\u001b[0m StockPickler\u001b[39m.\u001b[39;49msave(\u001b[39mself\u001b[39;49m, obj, save_persistent_id)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:560\u001b[0m, in \u001b[0;36m_Pickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    558\u001b[0m f \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mdispatch\u001b[39m.\u001b[39mget(t)\n\u001b[0;32m    559\u001b[0m \u001b[39mif\u001b[39;00m f \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m--> 560\u001b[0m     f(\u001b[39mself\u001b[39;49m, obj)  \u001b[39m# Call unbound method with explicit self\u001b[39;00m\n\u001b[0;32m    561\u001b[0m     \u001b[39mreturn\u001b[39;00m\n\u001b[0;32m    563\u001b[0m \u001b[39m# Check private dispatch table if any, or else\u001b[39;00m\n\u001b[0;32m    564\u001b[0m \u001b[39m# copyreg.dispatch_table\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:1186\u001b[0m, in \u001b[0;36msave_module_dict\u001b[1;34m(pickler, obj)\u001b[0m\n\u001b[0;32m   1183\u001b[0m     \u001b[39mif\u001b[39;00m is_dill(pickler, child\u001b[39m=\u001b[39m\u001b[39mFalse\u001b[39;00m) \u001b[39mand\u001b[39;00m pickler\u001b[39m.\u001b[39m_session:\n\u001b[0;32m   1184\u001b[0m         \u001b[39m# we only care about session the first pass thru\u001b[39;00m\n\u001b[0;32m   1185\u001b[0m         pickler\u001b[39m.\u001b[39m_first_pass \u001b[39m=\u001b[39m \u001b[39mFalse\u001b[39;00m\n\u001b[1;32m-> 1186\u001b[0m     StockPickler\u001b[39m.\u001b[39;49msave_dict(pickler, obj)\n\u001b[0;32m   1187\u001b[0m     logger\u001b[39m.\u001b[39mtrace(pickler, \u001b[39m\"\u001b[39m\u001b[39m# D2\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[0;32m   1188\u001b[0m \u001b[39mreturn\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:972\u001b[0m, in \u001b[0;36m_Pickler.save_dict\u001b[1;34m(self, obj)\u001b[0m\n\u001b[0;32m    969\u001b[0m     \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mwrite(MARK \u001b[39m+\u001b[39m DICT)\n\u001b[0;32m    971\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mmemoize(obj)\n\u001b[1;32m--> 972\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_batch_setitems(obj\u001b[39m.\u001b[39;49mitems())\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:998\u001b[0m, in \u001b[0;36m_Pickler._batch_setitems\u001b[1;34m(self, items)\u001b[0m\n\u001b[0;32m    996\u001b[0m     \u001b[39mfor\u001b[39;00m k, v \u001b[39min\u001b[39;00m tmp:\n\u001b[0;32m    997\u001b[0m         save(k)\n\u001b[1;32m--> 998\u001b[0m         save(v)\n\u001b[0;32m    999\u001b[0m     write(SETITEMS)\n\u001b[0;32m   1000\u001b[0m \u001b[39melif\u001b[39;00m n:\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:388\u001b[0m, in \u001b[0;36mPickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    386\u001b[0m     msg \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mCan\u001b[39m\u001b[39m'\u001b[39m\u001b[39mt pickle \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m: attribute lookup builtins.generator failed\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m GeneratorType\n\u001b[0;32m    387\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(msg)\n\u001b[1;32m--> 388\u001b[0m StockPickler\u001b[39m.\u001b[39;49msave(\u001b[39mself\u001b[39;49m, obj, save_persistent_id)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:603\u001b[0m, in \u001b[0;36m_Pickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    599\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(\u001b[39m\"\u001b[39m\u001b[39mTuple returned by \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m must have \u001b[39m\u001b[39m\"\u001b[39m\n\u001b[0;32m    600\u001b[0m                         \u001b[39m\"\u001b[39m\u001b[39mtwo to six elements\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m reduce)\n\u001b[0;32m    602\u001b[0m \u001b[39m# Save the reduce() output and finally memoize the object\u001b[39;00m\n\u001b[1;32m--> 603\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49msave_reduce(obj\u001b[39m=\u001b[39;49mobj, \u001b[39m*\u001b[39;49mrv)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:717\u001b[0m, in \u001b[0;36m_Pickler.save_reduce\u001b[1;34m(self, func, args, state, listitems, dictitems, state_setter, obj)\u001b[0m\n\u001b[0;32m    715\u001b[0m \u001b[39mif\u001b[39;00m state \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m    716\u001b[0m     \u001b[39mif\u001b[39;00m state_setter \u001b[39mis\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m--> 717\u001b[0m         save(state)\n\u001b[0;32m    718\u001b[0m         write(BUILD)\n\u001b[0;32m    719\u001b[0m     \u001b[39melse\u001b[39;00m:\n\u001b[0;32m    720\u001b[0m         \u001b[39m# If a state_setter is specified, call it instead of load_build\u001b[39;00m\n\u001b[0;32m    721\u001b[0m         \u001b[39m# to update obj's with its previous state.\u001b[39;00m\n\u001b[0;32m    722\u001b[0m         \u001b[39m# First, push state_setter and its tuple of expected arguments\u001b[39;00m\n\u001b[0;32m    723\u001b[0m         \u001b[39m# (obj, state) onto the stack.\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:388\u001b[0m, in \u001b[0;36mPickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    386\u001b[0m     msg \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mCan\u001b[39m\u001b[39m'\u001b[39m\u001b[39mt pickle \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m: attribute lookup builtins.generator failed\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m GeneratorType\n\u001b[0;32m    387\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(msg)\n\u001b[1;32m--> 388\u001b[0m StockPickler\u001b[39m.\u001b[39;49msave(\u001b[39mself\u001b[39;49m, obj, save_persistent_id)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:560\u001b[0m, in \u001b[0;36m_Pickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    558\u001b[0m f \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mdispatch\u001b[39m.\u001b[39mget(t)\n\u001b[0;32m    559\u001b[0m \u001b[39mif\u001b[39;00m f \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m--> 560\u001b[0m     f(\u001b[39mself\u001b[39;49m, obj)  \u001b[39m# Call unbound method with explicit self\u001b[39;00m\n\u001b[0;32m    561\u001b[0m     \u001b[39mreturn\u001b[39;00m\n\u001b[0;32m    563\u001b[0m \u001b[39m# Check private dispatch table if any, or else\u001b[39;00m\n\u001b[0;32m    564\u001b[0m \u001b[39m# copyreg.dispatch_table\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:1186\u001b[0m, in \u001b[0;36msave_module_dict\u001b[1;34m(pickler, obj)\u001b[0m\n\u001b[0;32m   1183\u001b[0m     \u001b[39mif\u001b[39;00m is_dill(pickler, child\u001b[39m=\u001b[39m\u001b[39mFalse\u001b[39;00m) \u001b[39mand\u001b[39;00m pickler\u001b[39m.\u001b[39m_session:\n\u001b[0;32m   1184\u001b[0m         \u001b[39m# we only care about session the first pass thru\u001b[39;00m\n\u001b[0;32m   1185\u001b[0m         pickler\u001b[39m.\u001b[39m_first_pass \u001b[39m=\u001b[39m \u001b[39mFalse\u001b[39;00m\n\u001b[1;32m-> 1186\u001b[0m     StockPickler\u001b[39m.\u001b[39;49msave_dict(pickler, obj)\n\u001b[0;32m   1187\u001b[0m     logger\u001b[39m.\u001b[39mtrace(pickler, \u001b[39m\"\u001b[39m\u001b[39m# D2\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[0;32m   1188\u001b[0m \u001b[39mreturn\u001b[39;00m\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:972\u001b[0m, in \u001b[0;36m_Pickler.save_dict\u001b[1;34m(self, obj)\u001b[0m\n\u001b[0;32m    969\u001b[0m     \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mwrite(MARK \u001b[39m+\u001b[39m DICT)\n\u001b[0;32m    971\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mmemoize(obj)\n\u001b[1;32m--> 972\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_batch_setitems(obj\u001b[39m.\u001b[39;49mitems())\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:998\u001b[0m, in \u001b[0;36m_Pickler._batch_setitems\u001b[1;34m(self, items)\u001b[0m\n\u001b[0;32m    996\u001b[0m     \u001b[39mfor\u001b[39;00m k, v \u001b[39min\u001b[39;00m tmp:\n\u001b[0;32m    997\u001b[0m         save(k)\n\u001b[1;32m--> 998\u001b[0m         save(v)\n\u001b[0;32m    999\u001b[0m     write(SETITEMS)\n\u001b[0;32m   1000\u001b[0m \u001b[39melif\u001b[39;00m n:\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\site-packages\\dill\\_dill.py:388\u001b[0m, in \u001b[0;36mPickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    386\u001b[0m     msg \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mCan\u001b[39m\u001b[39m'\u001b[39m\u001b[39mt pickle \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m: attribute lookup builtins.generator failed\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m GeneratorType\n\u001b[0;32m    387\u001b[0m     \u001b[39mraise\u001b[39;00m PicklingError(msg)\n\u001b[1;32m--> 388\u001b[0m StockPickler\u001b[39m.\u001b[39;49msave(\u001b[39mself\u001b[39;49m, obj, save_persistent_id)\n",
+      "File \u001b[1;32md:\\miniconda3\\lib\\pickle.py:578\u001b[0m, in \u001b[0;36m_Pickler.save\u001b[1;34m(self, obj, save_persistent_id)\u001b[0m\n\u001b[0;32m    576\u001b[0m reduce \u001b[39m=\u001b[39m \u001b[39mgetattr\u001b[39m(obj, \u001b[39m\"\u001b[39m\u001b[39m__reduce_ex__\u001b[39m\u001b[39m\"\u001b[39m, \u001b[39mNone\u001b[39;00m)\n\u001b[0;32m    577\u001b[0m \u001b[39mif\u001b[39;00m reduce \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m--> 578\u001b[0m     rv \u001b[39m=\u001b[39m reduce(\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mproto)\n\u001b[0;32m    579\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m    580\u001b[0m     reduce \u001b[39m=\u001b[39m \u001b[39mgetattr\u001b[39m(obj, \u001b[39m\"\u001b[39m\u001b[39m__reduce__\u001b[39m\u001b[39m\"\u001b[39m, \u001b[39mNone\u001b[39;00m)\n",
+      "\u001b[1;31mTypeError\u001b[0m: cannot pickle '_cffi_backend.__CDataOwnGC' object"
+     ]
+    }
+   ],
+   "source": [
+    "from microphones_instance import MicrophoneControllerInstance\n",
+    "\n",
+    "# experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\n",
+    "status = mp.Value('i', 1)\n",
+    "\n",
+    "microphoneController = MicrophoneControllerInstance(status, cfg[\"microphones\"])\n",
+    "\n",
+    "mc = mp.Process(target=microphoneController.run, args=())\n",
+    "mc.start()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "status.value = 2\n",
+    "\n",
+    "print(\"Recording started\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "status.value = 0\n",
+    "\n",
+    "mc.join()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Test with threading (instance methods) -> Works!"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\n",
+    "status = mp.Value('i', 1)\n",
+    "\n",
+    "mc = MicrophoneControllerInstance(status,cfg[\"microphones\"])\n",
+    "\n",
+    "mc.start()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Audio input stream started.\n"
+     ]
+    }
+   ],
+   "source": [
+    "status.value = 2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import time"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "status.value = 0"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Microphones recording stopped\n"
+     ]
+    }
+   ],
+   "source": [
+    "mc.stop()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.8"
+  },
+  "vscode": {
+   "interpreter": {
+    "hash": "af8259ad5c1c9c7a69bd6ea085234cf8fd3a6a37a71ca551828b314c4d89b0ad"
+   }
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}

+ 67 - 0
controllers/microphones.py

@@ -0,0 +1,67 @@
+#^IMPORTANT: essential to make multiprocessing work
+
+import queue
+import sys
+
+import sounddevice as sd
+import soundfile as sf
+import numpy  # Make sure NumPy is loaded before it is used in the callback
+assert numpy  # avoid "imported but unused" message (W0611)
+
+from situtils import FPSTimes
+
+import time
+
+class MicrophoneController(FPSTimes):
+    # https://python-sounddevice.readthedocs.io/en/0.3.15/examples.html#recording-with-arbitrary-duration
+    
+    @staticmethod
+    def callback(indata, frames, time, status):
+        """This is called (from a separate thread) for each audio block."""
+        if status:
+            print(status, file=sys.stderr)
+        MicrophoneController.queue.put(indata.copy())
+        
+        
+    @classmethod
+    def run(cls, status, cfg):
+        # MicrophoneController.initialize(cfg)
+        print("Running.")
+        import sounddevice as sd  # must be inside the function
+        
+        if cfg['channel_selectors']: # make it work without ASIO
+            # https://python-sounddevice.readthedocs.io/en/0.3.15/api/platform-specific-settings.html
+            asio_in = sd.AsioSettings(channel_selectors=cfg['channel_selectors'])
+        else:
+            asio_in = None
+
+        MicrophoneController.queue = queue.Queue() # kind of a hack
+
+        stream = sd.InputStream(samplerate=cfg['sample_rate'], device=cfg['device'], channels=cfg['number_channels'], callback=MicrophoneController.callback, extra_settings = asio_in)
+ 
+        filename = cfg['file_path']
+        file = sf.SoundFile(filename, mode='w', samplerate=cfg['sample_rate'], channels=cfg['number_channels'],subtype='PCM_32') # 'w': overwrite mode, 'x': raises error if file exists
+
+        # experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped
+        with file as f:
+            while status.value > 0:
+                try:
+                    if status.value == 2:
+
+                        # start stream if not active yet
+                        if not stream.active:
+                            print("Audio input stream started.")
+                            t0 = time.time()
+                            stream.start()
+                            with open(cfg['csv_path'], 'a') as f_csv:
+                                f_csv.write(",".join([str(x) for x in (t0,)]) + "\n")
+
+                        f.write(MicrophoneController.queue.get())
+
+                    else:
+                        time.sleep(0.005)
+                except KeyboardInterrupt:
+                    stream.stop()
+                    stream.close()
+                    break
+        

+ 5 - 5
controllers/position.ipynb

@@ -129,7 +129,7 @@
     "\n",
     "            if self.status.value == 2 and self.positions_in_px is not None:\n",
     "                self.save_position()\n",
-    "                self.save_contours()                  \n",
+    "                #self.save_contours()                  \n",
     "\n",
     "    # generic interface\n",
     "\n",
@@ -187,8 +187,8 @@
     "        \n",
     "        with open(cfg['file_path'], 'w') as f:\n",
     "            f.write(\"time,x,y\\n\")\n",
-    "        with open(cfg['contour_path'], 'w') as f:\n",
-    "            f.write(\"x:y,...\\n\")\n",
+    "        #with open(cfg['contour_path'], 'w') as f:\n",
+    "        #    f.write(\"x:y,...\\n\")\n",
     "            \n",
     "    def detect_position(self, frame):\n",
     "        masked_frame = cv2.bitwise_and(src1=frame, src2=self.mask)\n",
@@ -325,8 +325,8 @@
     "       \n",
     "        with open(cfg['file_path'], 'w') as f:\n",
     "            f.write(\"time,x1,y1,x2,y2\\n\")\n",
-    "        with open(cfg['contour_path'], 'w') as f:\n",
-    "            f.write(\"x:y,...\\n\")\n",
+    "        #with open(cfg['contour_path'], 'w') as f:\n",
+    "        #    f.write(\"x:y,...\\n\")\n",
     "\n",
     "    def detect_position(self, frame):\n",
     "        masked_frame = cv2.bitwise_and(src1=frame, src2=self.mask)\n",

+ 20 - 7
controllers/serial.ipynb

@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 2,
    "id": "82162890",
    "metadata": {},
    "outputs": [],
@@ -14,7 +14,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 3,
    "id": "2fc2905a",
    "metadata": {},
    "outputs": [],
@@ -48,7 +48,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 4,
    "id": "fe78b7d4",
    "metadata": {},
    "outputs": [],
@@ -59,6 +59,7 @@
     "    pin_TTL_2 = 5\n",
     "    pin_LED_lights = 7\n",
     "    pin_feeder = 8\n",
+    "    pin_feeder_power = 4\n",
     "    \n",
     "    def __init__(self, *args, **kwargs):\n",
     "        self.last_cmd = False  # False - Arduino LOW, True - Arduino HIGH\n",
@@ -75,10 +76,16 @@
     "        self.is_light_off = not self.is_light_off\n",
     "        self.digital[MCSArduino.pin_LED_lights].write(self.is_light_off)\n",
     "        \n",
-    "    def feed(self):\n",
-    "        self.digital[MCSArduino.pin_feeder].write(True)\n",
-    "        time.sleep(0.02)\n",
-    "        self.digital[MCSArduino.pin_feeder].write(False)\n",
+    "    def feed(self, n_pellets = 1):\n",
+    "        self.digital[MCSArduino.pin_feeder_power].write(True)\n",
+    "        time.sleep(0.5)\n",
+    "        for n in range(n_pellets):\n",
+    "            self.digital[MCSArduino.pin_feeder].write(True)\n",
+    "            time.sleep(0.02)\n",
+    "            self.digital[MCSArduino.pin_feeder].write(False)\n",
+    "            time.sleep(1.0)\n",
+    "        time.sleep(0.5)\n",
+    "        self.digital[MCSArduino.pin_feeder_power].write(False)\n",
     "        \n",
     "        \n",
     "class FakeArduino():\n",
@@ -167,6 +174,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "id": "56d33a31",
    "metadata": {},
@@ -304,6 +312,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "id": "ff6d72c0",
    "metadata": {},
@@ -352,6 +361,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "id": "89b10d38",
    "metadata": {},
@@ -405,6 +415,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "id": "e6df6e15",
    "metadata": {},
@@ -571,6 +582,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "id": "fdb584fe",
    "metadata": {},
@@ -666,6 +678,7 @@
    "source": []
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "id": "ae2fe188",
    "metadata": {},

+ 17 - 17
controllers/sound.ipynb

@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [
     {
@@ -46,6 +46,18 @@
     "        \"file_path\": \"sounds.csv\"\n",
     "    }\n",
     "    \n",
+    "    commutator = {\n",
+    "        -1: 'noise',\n",
+    "        0:  'silence',\n",
+    "        1:  'background',\n",
+    "        2:  'target',\n",
+    "        3:  'distractor1',\n",
+    "        4:  'distractor2',\n",
+    "        5:  'distractor3',\n",
+    "        6:  'distractor4',\n",
+    "        7:  'distractor5'\n",
+    "    }\n",
+    "        \n",
     "    @classmethod\n",
     "    def get_pure_tone(cls, freq, duration, sample_rate=44100):\n",
     "        x = np.linspace(0, duration * freq * 2*np.pi, int(duration*sample_rate), dtype=np.float32)\n",
@@ -107,7 +119,7 @@
     "        return sounds\n",
     "        \n",
     "    @classmethod\n",
-    "    def run(cls, selector, status, cfg):\n",
+    "    def run(cls, selector, status, cfg, commutator):\n",
     "        \"\"\"\n",
     "        selector        mp.Value object to set the sound to be played\n",
     "        status          mp.Value object to stop the loop\n",
@@ -116,18 +128,6 @@
     "        import numpy as np\n",
     "        import time\n",
     "        \n",
-    "        commutator = {\n",
-    "            -1: 'noise',\n",
-    "            0:  'silence',\n",
-    "            1:  'background',\n",
-    "            2:  'target',\n",
-    "            3:  'distractor1',\n",
-    "            4:  'distractor2',\n",
-    "            5:  'distractor3',\n",
-    "            6:  'distractor4',\n",
-    "            7:  'distractor5'\n",
-    "        }\n",
-    "        \n",
     "        sounds = cls.get_tone_stack(cfg)\n",
     "\n",
     "        sd.default.device = cfg['device']\n",
@@ -162,7 +162,7 @@
     "            \n",
     "            else:  # idle state\n",
     "                next_beat = time.time() + cfg['latency']\n",
-    "                time.sleep(0.05)\n",
+    "                time.sleep(0.005)\n",
     "                \n",
     "        stream.stop()\n",
     "        stream.close()\n",
@@ -1022,7 +1022,7 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3 (ipykernel)",
+   "display_name": "Python 3",
    "language": "python",
    "name": "python3"
   },
@@ -1036,7 +1036,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.13"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,

+ 14 - 14
controllers/sound.py

@@ -29,6 +29,18 @@ class SoundController:
         "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)
@@ -90,7 +102,7 @@ class SoundController:
         return sounds
         
     @classmethod
-    def run(cls, selector, status, cfg):
+    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
@@ -99,18 +111,6 @@ class SoundController:
         import numpy as np
         import time
         
-        commutator = {
-            -1: 'noise',
-            0:  'silence',
-            1:  'background',
-            2:  'target',
-            3:  'distractor1',
-            4:  'distractor2',
-            5:  'distractor3',
-            6:  'distractor4',
-            7:  'distractor5'
-        }
-        
         sounds = cls.get_tone_stack(cfg)
 
         sd.default.device = cfg['device']
@@ -145,7 +145,7 @@ class SoundController:
             
             else:  # idle state
                 next_beat = time.time() + cfg['latency']
-                time.sleep(0.05)
+                time.sleep(0.005)
                 
         stream.stop()
         stream.close()

+ 18 - 13
controllers/video.ipynb

@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 2,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -18,7 +18,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -26,7 +26,7 @@
     "    # precise timing https://stackoverflow.com/questions/42565297/precise-loop-timing-in-python\n",
     "    \n",
     "    default_cfg = {\n",
-    "        'fps': 20,\n",
+    "        'fps': 30,\n",
     "        'file_path': 'test_video.avi',\n",
     "    }\n",
     "    \n",
@@ -65,15 +65,18 @@
     "                    continue  # TODO sleep here?\n",
     "\n",
     "                #frame = self.video_stream.read()\n",
-    "                frame = self.video_stream.frame_with_infos\n",
+    "                frame = getattr(self.video_stream, self.cfg['frame_attr_name'])\n",
     "                if frame is not None:\n",
     "                    self.count()  # count FPS\n",
+    "                    t0 = time.time()\n",
     "                    self.out.write(frame)\n",
-    "\n",
+    "                    with open(self.cfg['csv_path'], 'a') as f:\n",
+    "                        f.write(\",\".join([str(x) for x in (t0,)]) + \"\\n\")\n",
+    "                        \n",
     "                    frames_missed = np.floor((time.time() - next_frame) * self.cfg['fps'])\n",
     "                    next_frame += self.latency * frames_missed + self.latency\n",
     "            else:\n",
-    "                time.sleep(0.05)\n",
+    "                time.sleep(0.005)\n",
     "            \n",
     "        self.out.release()"
    ]
@@ -87,16 +90,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [
     {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Webcam stream 960.0:720.0 at 20.00 FPS started\n",
-      "Camera released\n",
-      "Video writer stopped\n"
+     "ename": "NameError",
+     "evalue": "name 'WebcamStream' is not defined",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[1;31mNameError\u001b[0m                                 Traceback (most recent call last)",
+      "\u001b[1;32m<ipython-input-8-39fd0c07e1c2>\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# let's use a webcam stream\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[0mvs\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mWebcamStream\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mWebcamStream\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdefault_cfg\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[0mvs\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstart\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m  \u001b[1;31m# stream runs in a separate thread\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      7\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n",
+      "\u001b[1;31mNameError\u001b[0m: name 'WebcamStream' is not defined"
      ]
     }
    ],

+ 736 - 0
passive.ipynb

@@ -0,0 +1,736 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# include controllers to the path\n",
+    "import sys, os\n",
+    "sys.path.append(os.getcwd())\n",
+    "sys.path.append(os.path.join(os.getcwd(), 'controllers'))\n",
+    "\n",
+    "# include pipeline repo to compute performance\n",
+    "sys.path.append(os.path.join(os.getcwd(), '..'))\n",
+    "sys.path.append(os.path.join(os.getcwd(), '..', 'pipeline'))\n",
+    "sys.path.append(os.path.join(os.getcwd(), '..', 'pipeline', 'postprocessing'))\n",
+    "\n",
+    "import cv2\n",
+    "import threading\n",
+    "import math\n",
+    "import time\n",
+    "import random\n",
+    "import json\n",
+    "import datetime\n",
+    "import os, shutil\n",
+    "import numpy as np\n",
+    "import multiprocess as mp\n",
+    "\n",
+    "# controllers\n",
+    "import nbimporter\n",
+    "from controllers.situtils import FPSTimes\n",
+    "from controllers.camera import WebcamStream\n",
+    "from controllers.video import VideoWriter\n",
+    "from controllers.microphones import MicrophoneController\n",
+    "from controllers.position import PositionTrackerSingle, PositionTrackerDouble\n",
+    "from controllers.sound import SoundController, ContinuousSoundStream\n",
+    "from controllers.serial import MCSArduino, FakeArduino, SpeakerMotor, CableMotor\n",
+    "from controllers.display import SITDisplay\n",
+    "from controllers.island import IslandFactory\n",
+    "\n",
+    "from pipeline.postprocessing.pack import pack\n",
+    "from pipeline.postprocessing.performance import calculate_performance, dump_performance_to_H5"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Load experiment settings\n",
+    "\n",
+    "For every experimental cofiguration you can copy the original 'settings.json' file, build your own specific experimental preset, save it in this folder as e.g. 'settings_elena.json' and load it here instead of 'settings.json'."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "cfg_filename = os.path.join('profiles', 'passive.json')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 25,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "with open(os.path.join('profiles', 'default.json')) as json_file:\n",
+    "    cfg = json.load(json_file)\n",
+    "with open(cfg_filename) as json_file:\n",
+    "    cfg_local = json.load(json_file)\n",
+    "\n",
+    "for key in cfg.keys():\n",
+    "    if key in cfg_local: # only update if the key exists in the local config, otherwise keep default (important for backward compatibility with cfg files before microphones)\n",
+    "        cfg[key].update(cfg_local[key])\n",
+    "cfg['experiment']['experiment_date'] = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')\n",
+    "\n",
+    "# print loaded settings\n",
+    "#print(json.dumps(cfg, indent=4))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Initialize session folder\n",
+    "\n",
+    "Run the upcoming cell, to create a session folder and to save the chosen experimetal parameters to a JSON-file (\"experiment_id_parameters.json\"). The session folder will be created here where this notebook is located."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 26,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# This session's protocols will be saved to this folder\n",
+    "cfg_exp = cfg['experiment']\n",
+    "experiment_id = \"%s_%s_%s\" % (cfg_exp['subject'], cfg_exp['experiment_type'], cfg_exp['experiment_date'])\n",
+    "save_to = os.path.join('sessions', experiment_id)\n",
+    "             \n",
+    "if not os.path.exists(save_to):\n",
+    "    os.makedirs(save_to)\n",
+    "\n",
+    "# update paths (assuming this paths are relative to this notebook)\n",
+    "cfg['video']['file_path'] = os.path.join(save_to, cfg['video']['file_path'])\n",
+    "cfg['video']['csv_path'] = os.path.join(save_to, cfg['video']['csv_path'])\n",
+    "cfg['microphones']['file_path'] = os.path.join(save_to, cfg['microphones']['file_path'])\n",
+    "cfg['microphones']['csv_path'] = os.path.join(save_to, cfg['microphones']['csv_path'])\n",
+    "cfg['position']['file_path'] = os.path.join(save_to, cfg['position']['file_path'])\n",
+    "cfg['position']['contour_path'] = os.path.join(save_to, cfg['position']['contour_path'])\n",
+    "cfg['experiment']['file_path'] = os.path.join(save_to, cfg['experiment']['file_path'])\n",
+    "cfg['experiment']['islands_path'] = os.path.join(save_to, 'islands.csv')\n",
+    "cfg['sound']['file_path'] = os.path.join(save_to, cfg['sound']['file_path'])\n",
+    "cfg['position']['background_light'] = os.path.join('assets', cfg['position']['background_light'])\n",
+    "cfg['position']['background_dark'] = os.path.join('assets', cfg['position']['background_dark'])\n",
+    "if 'wav_file' in cfg['sound']:\n",
+    "    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",
+    "    \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",
+    "    json.dump(cfg, f, indent=4)\n",
+    "    \n",
+    "with open(cfg['experiment']['file_path'], 'w') as f:\n",
+    "    # state: 0 - trial start, 1 - trial success, 2 - trial fail\n",
+    "    f.write('time, target_x, target_y, target_r, trial, state\\n')\n",
+    "\n",
+    "with open(cfg['experiment']['islands_path'], 'w') as f:\n",
+    "    f.write('tgt_x, tgt_y, tgt_r, d1_x, d1_y, d1_r, d2_x, d2_y, d2_r, d3_x, d3_y, d3_r\\n')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 27,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def timeout(t_start):\n",
+    "    return time.time() - t_start > cfg_exp['session_duration'] if t_start is not None else False"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 28,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def log_event(*args):  # log start / end of a trial\n",
+    "    with open(cfg_exp['file_path'], 'a') as f:\n",
+    "        f.write(\",\".join([str(x) for x in args]) + \"\\n\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 29,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def log_islands(islands):  # log position of the islands\n",
+    "    sorted_islands = sorted(islands, key=lambda x: x.sound_id, reverse=False)\n",
+    "    args = [(i.x, i.y, i.r) for i in sorted_islands]\n",
+    "    to_dump = [x for xs in args for x in xs]\n",
+    "        \n",
+    "    with open(cfg_exp['islands_path'], 'a') as f:    \n",
+    "        f.write(\",\".join([str(round(x, 4)) for x in to_dump]) + \"\\n\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 30,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def switch_light(pt, board):\n",
+    "    pt.switch_background()\n",
+    "    board.switch_light()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Start the experiment\n",
+    "\n",
+    "This cell contains code for animal tracking. We hope that the comments provided in the code suffice to understand the individual steps and to adjust them to your own setup and needs, if necessary.\n",
+    "\n",
+    "- press 's' to start recording\n",
+    "- press 's' again to stop recording\n",
+    "- press 'q' to quit\n",
+    "\n",
+    "The experiment will stop automatically if the pre-defined session duration is reached."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 31,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Webcam stream 1024.0:768.0 at 30.00 FPS started\n",
+      "Position tracker stopped\n",
+      "Video writer stopped\n",
+      "Camera released\n"
+     ]
+    }
+   ],
+   "source": [
+    "# actual sound selector: -1 - noise, 0 - silence, 1 - foraging, 2 - target, 3 - distractor\n",
+    "sound = mp.Value('i', 1)\n",
+    "if 'noise_when_idle' in cfg_exp and cfg_exp['noise_when_idle']:\n",
+    "    sound.value = -1\n",
+    "\n",
+    "# experiment status: 1 - idle, 2 - running (recording, logging), 0 - stopped\n",
+    "status = mp.Value('i', 1)\n",
+    "\n",
+    "# init the sync with the acquisition system and feeder via Arduino\n",
+    "if cfg['experiment']['MCSArduinoPort'] == 'fake':\n",
+    "    board = FakeArduino()\n",
+    "else:\n",
+    "    board = MCSArduino(cfg['experiment']['MCSArduinoPort'])\n",
+    "\n",
+    "# init speaker motor control\n",
+    "if 'motors_port' in cfg['experiment'] and cfg['experiment']['enable_motors']:\n",
+    "    motor_board = SpeakerMotor(cfg['experiment']['motors_port'])\n",
+    "    \n",
+    "# init ephys cable rotation motor\n",
+    "if 'cable_motor_port' in cfg['experiment']:\n",
+    "    cable_board = CableMotor(cfg['experiment']['cable_motor_port'])\n",
+    "\n",
+    "# init continuous sound, if required\n",
+    "if 'continuous' in cfg['sound']:\n",
+    "    cst = ContinuousSoundStream(cfg['sound']['continuous'])\n",
+    "    cst.start()\n",
+    "    \n",
+    "# start the camera stream\n",
+    "vs = WebcamStream(cfg['camera'])\n",
+    "vs.start()\n",
+    "\n",
+    "# init video recorder\n",
+    "vw = VideoWriter(status, vs, cfg['video'])\n",
+    "vw.start()\n",
+    "\n",
+    "# init microphone controller, if required\n",
+    "if cfg['microphones']['record_audio']:\n",
+    "    mc = mp.Process(target=MicrophoneController.run, args=(status,cfg[\"microphones\"]))\n",
+    "    mc.start()\n",
+    "\n",
+    "# start position tracking\n",
+    "pt = PositionTrackerSingle(status, vs, cfg['position']) if cfg['position']['single_agent'] else PositionTrackerDouble(status, vs, cfg['position'])\n",
+    "pt.start()\n",
+    "# print(pt.mask.shape)\n",
+    "# init frame renderer\n",
+    "dc = SITDisplay(pt, cfg['video'])\n",
+    "\n",
+    "# playing sound in a separate process for performance\n",
+    "sounds = cfg['sound']['sounds']\n",
+    "commutator = {\n",
+    "    -1: 'noise',\n",
+    "    0:  'silence'\n",
+    "}\n",
+    "\n",
+    "sound_order = 1\n",
+    "for s_type in ('F', 'D', 'I', 'L'):\n",
+    "    keys = sorted([key for key in sounds.keys() if key.startswith(s_type)])\n",
+    "    for key in keys:\n",
+    "        commutator[sound_order] = key\n",
+    "        sound_order += 1\n",
+    "sc = mp.Process(target=SoundController.run, args=(sound, status, cfg['sound'], commutator))\n",
+    "sc.start()\n",
+    "\n",
+    "cfg_pos = cfg['position']\n",
+    "isl_factory = IslandFactory(cfg_pos['floor_r_in_meters'], cfg_pos['angle_compensation'], cfg['experiment'])\n",
+    "\n",
+    "timers = []\n",
+    "fps = FPSTimes()\n",
+    "names = ['camera', 'video', 'position', 'main']\n",
+    "distr_count = cfg['experiment']['distractor_islands']\n",
+    "trial = 0\n",
+    "rewards = 0\n",
+    "t_start = None\n",
+    "target_since = None\n",
+    "distractor_since = None\n",
+    "punishment_since = None\n",
+    "trial_start = time.time()\n",
+    "phase = 0  # 0 - idle, 1 - foraging, 2 - inter-trial interval\n",
+    "cfg_exp = cfg['experiment']\n",
+    "cfg_pos = cfg['position']\n",
+    "islands = []\n",
+    "iti_distance = 0.0\n",
+    "last_x, last_y = None, None\n",
+    "cfg_exp['trial_number'] = max([int(key) for key in commutator.keys()])  # equals number of diff sounds\n",
+    "\n",
+    "try:\n",
+    "    while trial <= cfg_exp['trial_number'] and not timeout(t_start):\n",
+    "        \n",
+    "        # ---------- rendering logic -----------\n",
+    "        frame = vs.read()\n",
+    "        if frame is None:\n",
+    "            time.sleep(0.1)\n",
+    "            continue # wait for the stream\n",
+    "            \n",
+    "        c_time = time.time()\n",
+    "        fps.count()\n",
+    "        angles, text_infos = [], []\n",
+    "        \n",
+    "        if not cfg_exp['target_angle'] == 'random':\n",
+    "            phi = isl_factory.phi_initial\n",
+    "            angles = [pt.correct_angle(phi), pt.correct_angle(phi) + np.deg2rad(cfg_exp['phi_max'])]\n",
+    "        \n",
+    "        for i, ctrl in enumerate([vs, vw, pt, fps]):  # FPS indicators\n",
+    "            text_infos.append('%s: %.2f FPS' % (names[i], ctrl.get_avg_fps()))\n",
+    "        \n",
+    "        if len(islands) > 0:  # target island X, Y\n",
+    "            target = [i for i in islands if not i.is_distractor][0]\n",
+    "            text_infos.append('Target: %.3f %.3f' % (target.x, target.y))\n",
+    "\n",
+    "        text_infos.append('Time: %.2f' % float(c_time - t_start) if t_start is not None else 'Time: Idle')  # stopwatch\n",
+    "        text_infos.append('Trial: %.2f' % float(cfg_exp['trial_duration'] - (c_time - trial_start)) if phase == 1 else 'Trial: not started')\n",
+    "        text_infos.append('Trial: %s' % trial)\n",
+    "        text_infos.append('Rewards: %s' % rewards)  # rewards\n",
+    "        text_infos.append('In target: %.2f' % float(c_time - target_since) if target_since is not None else 'In target: --')\n",
+    "        text_infos.append('ITI distance: %.2f' % iti_distance if iti_distance > 0 else 'ITI distance: --')\n",
+    "\n",
+    "        frame, frame_to_save = dc.render(frame, status, islands=islands, angles=angles, text_infos=text_infos)\n",
+    "            \n",
+    "        # assign the frame back to the video stream for other controllers\n",
+    "        vs.frame_with_infos = frame_to_save\n",
+    "        \n",
+    "        cv2.imshow('Press (s)-to start/stop, (q)-to end', frame)\n",
+    "\n",
+    "        # -------- experiment logic ---------------\n",
+    "        \n",
+    "        if status.value == 2 and c_time - trial_start > cfg_exp['trial_duration']:\n",
+    "            log_event(c_time, 0, 0, 0, trial, 1) # log trial end\n",
+    "            trial += 1\n",
+    "            if int(sound.value) < cfg_exp['trial_number']:\n",
+    "                sound.value += 1\n",
+    "            trial_start = c_time\n",
+    "            log_event(c_time, 0, 0, 0, trial, 0) # log trial start\n",
+    "        \n",
+    "        # -------- key press events ---------------\n",
+    "        \n",
+    "        k = cv2.waitKey(33)\n",
+    "        \n",
+    "        # light on/off\n",
+    "        if k == ord('l'):\n",
+    "            switch_light(pt, board)\n",
+    "        \n",
+    "        # motors\n",
+    "        if k == ord('d'):\n",
+    "            if cfg['experiment']['enable_motors']:\n",
+    "                motor_board.switch_diodes()\n",
+    "         \n",
+    "        # ephys cable on top\n",
+    "        if k == ord('i') and 'cable_motor_port' in cfg['experiment']:\n",
+    "            tf = threading.Timer(0, cable_board.turn_CCW, args=[])\n",
+    "            tf.start()\n",
+    "        if k == ord('o') and 'cable_motor_port' in cfg['experiment']:\n",
+    "            tf = threading.Timer(0, cable_board.turn_CW, args=[])\n",
+    "            tf.start()\n",
+    "            \n",
+    "        # feeding\n",
+    "        if k == ord('f'):\n",
+    "            tf = threading.Timer(0, board.feed, args=[])\n",
+    "            tf.start()\n",
+    "\n",
+    "        if k == ord('c'):\n",
+    "            f_name = cfg_pos['background_light'] if pt.is_light else cfg_pos['background_dark']\n",
+    "            cv2.imwrite(f_name, dc.masked_frame)\n",
+    "            tf = threading.Timer(0.2, pt.reload_background, args=[])\n",
+    "            tf.start()            \n",
+    "\n",
+    "        # quit the session\n",
+    "        if k == ord('q'):\n",
+    "            break\n",
+    "\n",
+    "        # start the experiment\n",
+    "        if k == ord('s'):\n",
+    "            board.start_or_stop()  # start/stop data acquisition\n",
+    "            c_time = time.time()   # more accurate time\n",
+    "            \n",
+    "            if cfg['experiment']['enable_motors']:\n",
+    "                if not motor_board.diodes_on:\n",
+    "                    motor_board.switch_diodes()\n",
+    "\n",
+    "            if status.value == 1: # start the session\n",
+    "                if t_start is None:\n",
+    "                    t_start = c_time\n",
+    "                trial_start = c_time\n",
+    "                status.value = 2\n",
+    "                \n",
+    "                # init_new_trial\n",
+    "                sound.value = 1\n",
+    "                phase = 1\n",
+    "                trial += 1\n",
+    "\n",
+    "                # log trial start\n",
+    "                log_event(c_time, 0, 0, 0, trial, 0) # log trial start\n",
+    "                \n",
+    "            elif status.value == 2:  # pause the session\n",
+    "                if len(islands) > 0:\n",
+    "                    tgt = [i for i in islands if not i.is_distractor][0]\n",
+    "                    x, y, r = round(tgt.x, 4), round(tgt.y, 4), round(tgt.r, 4)\n",
+    "                else:\n",
+    "                    x, y, r = 0, 0, 0\n",
+    "                log_event(c_time, x, y, r, trial, -1)  # log experiment break\n",
+    "                \n",
+    "                if 'noise_when_idle' in cfg_exp and cfg_exp['noise_when_idle']:\n",
+    "                    sound.value = -1\n",
+    "                status.value = 1\n",
+    "                phase = 0\n",
+    "                islands = []\n",
+    "                for t in timers:\n",
+    "                    t.cancel()\n",
+    "                    \n",
+    "        if k == ord('a'):\n",
+    "            sound.value = -1 if sound.value >= 0 else 0\n",
+    "            switch_light(pt, board)  # turn lights off \n",
+    "\n",
+    "finally:\n",
+    "    if status.value == 2:  # stop data acquisition, in case exited via timeout\n",
+    "        board.start_or_stop()\n",
+    "        if len(islands) > 0:\n",
+    "            tgt = [i for i in islands if not i.is_distractor][0]\n",
+    "            x, y, r = round(tgt.x, 4), round(tgt.y, 4), round(tgt.r, 4)\n",
+    "        else:  \n",
+    "            x, y, r = 0, 0, 0\n",
+    "        log_event(c_time, x, y, r, trial, -1)  # log experiment end\n",
+    "        \n",
+    "    status.value = 0\n",
+    "    time.sleep(0.01)\n",
+    "    for t in timers:\n",
+    "        t.cancel()\n",
+    "    \n",
+    "    if board.is_light_off:\n",
+    "        board.switch_light()  # turn light back on\n",
+    "        time.sleep(0.1)\n",
+    "    board.exit()\n",
+    "    \n",
+    "    if cfg['experiment']['enable_motors']:\n",
+    "        if motor_board.diodes_on:\n",
+    "            motor_board.switch_diodes()\n",
+    "        motor_board.exit()\n",
+    "    if 'cable_motor_port' in cfg['experiment']:\n",
+    "        cable_board.exit()\n",
+    "    \n",
+    "    cv2.destroyAllWindows()\n",
+    "    sc.join()\n",
+    "    for ctrl in [pt, vw, vs]:\n",
+    "        ctrl.stop()\n",
+    "\n",
+    "    # stop microphones recording process\n",
+    "    if cfg['microphones']['record_audio']:\n",
+    "        mc.join()\n",
+    "\n",
+    "    if 'continuous' in cfg['sound']:\n",
+    "        time.sleep(cfg['sound']['continuous']['end_sleep'])\n",
+    "        cst.stop()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Merge data in HDF5 file"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 32,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "session_path = save_to\n",
+    "#trial = 5\n",
+    "#session_path = os.path.join('sessions', '2021-07-30_09-24-14')  # some particular session\n",
+    "#session_path = 'Y:\\\\Michael\\\\FreeBehaving\\\\SIT_sessions\\\\51_aSIT_2021-12-03_13-31-51'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 33,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "D:\\runSIT\\..\\pipeline\\postprocessing\\pack.py:59: UserWarning: loadtxt: Empty input file: \"sessions\\000000_hippoSIT_2023-07-11_09-32-45\\islands.csv\"\n",
+      "  data = np.loadtxt(filename, delimiter=',', skiprows=1)\n"
+     ]
+    },
+    {
+     "ename": "IndexError",
+     "evalue": "index 0 is out of bounds for axis 0 with size 0",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[1;31mIndexError\u001b[0m                                Traceback (most recent call last)",
+      "\u001b[1;32m<ipython-input-33-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"
+     ]
+    }
+   ],
+   "source": [
+    "if not trial > 0:\n",
+    "    raise SystemExit('Nothing recorded. No sense to continue.')\n",
+    "\n",
+    "# do pack data to HDF5\n",
+    "h5name = pack(session_path)\n",
+    "trial = 0"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Plot sessions stats"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import h5py\n",
+    "import numpy as np\n",
+    "from scipy import signal"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "arena_r = 0.4  # in meters\n",
+    "\n",
+    "with h5py.File(h5name, 'r') as f:\n",
+    "    tl = np.array(f['processed']['timeline'])\n",
+    "    trial_idxs = np.array(f['processed']['trial_idxs'])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "fig = plt.figure(figsize=(12, 12))\n",
+    "\n",
+    "# trajectory and islands\n",
+    "ax = fig.add_subplot(221)\n",
+    "ax.scatter(tl[:, 1], tl[:, 2], s=1, alpha=0.1)  # positions\n",
+    "scat = ax.scatter(trial_idxs[:, 2], trial_idxs[:, 3], s=1000, facecolors='none', edgecolors='r')  # islands, radius approx.\n",
+    "ax.add_patch(plt.Circle((0, 0), arena_r, color='r', fill=False))\n",
+    "ax.set_aspect('equal')\n",
+    "ax.set_xlabel('X, m', fontsize=14)\n",
+    "ax.set_ylabel('Y, m', fontsize=14)\n",
+    "ax.set_title('Running', fontsize=14)\n",
+    "ax.grid()\n",
+    "\n",
+    "# occupancy\n",
+    "sigma = 0.1\n",
+    "lin_profile = np.linspace(-15, 15, 20)\n",
+    "bump = np.exp(-sigma * lin_profile**2)\n",
+    "bump /= np.trapz(bump)  # normalize to 1\n",
+    "kernel = bump[:, np.newaxis] * bump[np.newaxis, :]\n",
+    "occupancy_map, _, _ = np.histogram2d(tl[:, 1], tl[:, 2], bins=[40, 40], range=np.array([[-0.5, 0.5], [-0.5, 0.5]]))\n",
+    "occupancy_map = signal.convolve2d(occupancy_map, kernel, mode='same')\n",
+    "\n",
+    "ax = fig.add_subplot(222)\n",
+    "ax.imshow(occupancy_map.T, origin='lower', extent=(-0.5, 0.5, -0.5, 0.5), cmap='Blues')\n",
+    "ax.add_patch(plt.Circle((0, 0), arena_r, color='r', fill=False))\n",
+    "ax.set_xlabel('X, m', fontsize=14)\n",
+    "ax.set_title('Occupancy', fontsize=14)\n",
+    "ax.grid()\n",
+    "\n",
+    "# trials\n",
+    "durations = tl[trial_idxs[:, 1].astype(int)][:, 0] - tl[trial_idxs[:, 0].astype(int)][:, 0]\n",
+    "colors = ['red' if x == 1 else 'grey' for x in trial_idxs[:, 5]]\n",
+    "\n",
+    "ax = fig.add_subplot(223)\n",
+    "ax.barh(np.arange(len(trial_idxs)), durations, color=colors, align='center')\n",
+    "ax.set_xlabel('Time, s', fontsize=14)\n",
+    "ax.set_ylabel('Trial, #', fontsize=14)\n",
+    "ax.set_title('Trials', fontsize=14)\n",
+    "\n",
+    "# speed\n",
+    "ax = fig.add_subplot(224)\n",
+    "\n",
+    "s_rate = 100  # Hz\n",
+    "window = 60   # secs\n",
+    "step = 10     # secs\n",
+    "duration = tl[-1][0]\n",
+    "x_vals = np.arange(int(duration/step))\n",
+    "\n",
+    "inst_speed = [tl[x*step*s_rate:(x*step + window)*s_rate][:, 3].mean() for x in x_vals]\n",
+    "ax.plot(x_vals*step, inst_speed)\n",
+    "ax.set_ylabel('Speed, m/s', fontsize=14)\n",
+    "ax.set_xlabel('Time, s', fontsize=14)\n",
+    "ax.set_title('Speed', fontsize=14)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Performance"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [],
+   "source": [
+    "h5name = os.path.join(session_path, experiment_id + '.h5')\n",
+    "jsname = os.path.join(session_path, experiment_id + '.json')\n",
+    "\n",
+    "# loading position and trial data\n",
+    "with h5py.File(h5name, 'r') as f:\n",
+    "    tl = np.array(f['processed']['timeline'])  # time, X, Y, speed\n",
+    "    trial_idxs = np.array(f['processed']['trial_idxs'])  # idx start, idx end, X, Y, R, trial result (idx to tl)\n",
+    "\n",
+    "# loading session configuration\n",
+    "with open(jsname, 'r') as f:\n",
+    "    cfg = json.load(f)\n",
+    "\n",
+    "timepoints = cfg['experiment']['timepoints']\n",
+    "s_duration = cfg['experiment']['session_duration']\n",
+    "if len(timepoints) > 0:\n",
+    "    periods = [[0, s_duration], [0, timepoints[0]], [timepoints[1], timepoints[2]], [timepoints[3], s_duration]]\n",
+    "else:\n",
+    "    periods = [[0, s_duration]]\n",
+    "\n",
+    "# separate ALL, L, D, L'\n",
+    "ds_names = ['performance_ALL', 'performance_L', 'performance_D', 'performance_Lp']\n",
+    "ds_names = ['performance_ALL']  # only light\n",
+    "\n",
+    "for i, ds_name in enumerate(ds_names):\n",
+    "    t_start, t_end = periods[i]\n",
+    "    trial_starts = tl[trial_idxs[:, 0].astype(np.int32)][:, 0]\n",
+    "    trial_ends   = tl[trial_idxs[:, 1].astype(np.int32)][:, 0]\n",
+    "    tr_idxs = trial_idxs[(trial_starts >= t_start) & (trial_ends <= t_end)]\n",
+    "\n",
+    "    dataset = calculate_performance(tl, tr_idxs, cfg)\n",
+    "    dump_performance_to_H5(h5name, ds_name, dataset)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fig = plt.figure(figsize=(4, 4))\n",
+    "\n",
+    "with h5py.File(h5name, 'r') as f:\n",
+    "    perf = np.array(f['analysis']['performance_ALL'])\n",
+    "    x = perf[:, 6]\n",
+    "\n",
+    "ax = fig.add_subplot(111)\n",
+    "\n",
+    "ax.plot(x, perf[:, 0])  # performance\n",
+    "ax.plot(x, perf[:, 3])  # chance\n",
+    "ax.fill_between(x, perf[:, 0] + perf[:, 1], perf[:, 0] + perf[:, 2], alpha=0.4)\n",
+    "ax.fill_between(x, perf[:, 3] + perf[:, 4], perf[:, 3] + perf[:, 5], alpha=0.4)\n",
+    "ax.set_ylim(0, 110)\n",
+    "ax.set_xlim(0, 65)\n",
+    "ax.grid()\n",
+    "ax.set_title(experiment_id[-19:], fontsize=14)\n",
+    "ax.set_xlabel('Time, s', fontsize=14)\n",
+    "\n",
+    "if i == 0:\n",
+    "    ax.set_ylabel('Successful trials, %', fontsize=14)\n",
+    "        \n",
+    "fig.tight_layout()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.8"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}

+ 77 - 13
profiles/default.json

@@ -10,7 +10,23 @@
     "video": {
         "fps": 30,
         "file_path": "video.avi",
-        "save_contours": false
+        "frame_attr_name": "frame_with_infos",
+        "save_contours": false,
+        "csv_path": "video.csv"
+    },
+    "microphones": {
+        "record_audio": false,
+        "sample_rate": 192000,
+        "device": "ASIO Fireface USB",
+        "number_channels": 4,
+        "channel_selectors": [
+            8,
+            9,
+            10,
+            11
+        ],
+        "file_path": "audio.mat5",
+        "csv_path": "microphones.csv"
     },
     "position": {
         "single_agent": true,
@@ -35,17 +51,56 @@
         "flip_y": false
     },
     "sound": {
-        "device": [1, 26],
-        "n_channels": 10,
+        "device": "ASIO Fireface USB",
+        "n_channels": 16,
         "sounds": {
-            "noise": {"amp": 0.2, "channels": [6, 8]},
-            "background": {"freq": 660, "amp": 0.05, "duration": 0.05, "harmonics": true, "channels": [1, 8]},
-            "target": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": true, "channels": [2, 8]}, 
-            "distractor1": {"freq": 660, "amp": 0.05, "duration": 0.05, "harmonics": true, "channels": [3, 8], "enabled": true},
-            "distractor2": {"freq": 660, "amp": 0.1, "duration": 0.05, "harmonics": true, "channels": [2, 8], "enabled": false}
+            "noise": {
+                "amp": 0.2,
+                "channels": [
+                    6
+                ]
+            },
+            "background": {
+                "freq": 660,
+                "amp": 0.05,
+                "duration": 0.05,
+                "harmonics": true,
+                "channels": [
+                    5
+                ]
+            },
+            "target": {
+                "freq": 660,
+                "amp": 0.1,
+                "duration": 0.05,
+                "harmonics": true,
+                "channels": [
+                    4
+                ]
+            },
+            "distractor1": {
+                "freq": 660,
+                "amp": 0.05,
+                "duration": 0.05,
+                "harmonics": true,
+                "channels": [
+                    5
+                ],
+                "enabled": true
+            },
+            "distractor2": {
+                "freq": 660,
+                "amp": 0.1,
+                "duration": 0.05,
+                "harmonics": true,
+                "channels": [
+                    4
+                ],
+                "enabled": false
+            }
         },
         "pulse_duration": 0.05,
-        "sample_rate": 44100,
+        "sample_rate": 192000,
         "latency": 0.25,
         "volume": 0.7,
         "roving": 5.0,
@@ -63,14 +118,23 @@
         "experiment_type": "hippoSIT",
         "MCSArduinoPort": "COM10",
         "file_path": "events.csv",
-        "light_events": [9600, 92400],
+        "light_events": [
+            9600,
+            92400
+        ],
         "phi_max": 45,
-        "timepoints": [9600, 91200, 91800, 92400],
+        "timepoints": [
+            9600,
+            91200,
+            91800,
+            92400
+        ],
         "iti_distance": 2.0,
         "iti_duration": 20,
         "punishment_duration": 10,
         "distractor_islands": 1,
         "distractor_fail": false,
-        "enable_motors": false
+        "enable_motors": false,
+        "n_pellets": 1
     }
-}
+}