1
0

6 Incheckningar 33602747d4 ... 93f1a21a0f

Upphovsman SHA1 Meddelande Datum
  asobolev 93f1a21a0f merged 9 månader sedan
  asobolev 8832d3f2bf continuous sound stream part 2 9 månader sedan
  asobolev aefb15ddf4 fixes to the sound presentation loop - include continuous sound stream part 1 9 månader sedan
  asobolev 3ff530dc8e added silence after the all trials 11 månader sedan
  asobolev 8c878d9921 added silence before the trials start 11 månader sedan
  asobolev 5305417a5a randomized sound presentations + removed lights ON at the end 1 år sedan

+ 221 - 37
SIT.ipynb

@@ -44,7 +44,6 @@
    ]
   },
   {
-   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -59,12 +58,16 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "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",
+    "# cfg_filename = os.path.join('profiles', 'mouse_freq.json')\n",
+    "# cfg_filename = os.path.join('profiles', 'implanted_multiSIT_660_1320.json')\n",
+    "#cfg_filename = os.path.join('profiles', 'implanted_timeSIT_50_100.json')\n",
+    "# cfg_filename = os.path.join('profiles', 'passive_FDA.json')\n",
+    "# cfg_filename = os.path.join('profiles', 'gokce_timeSIT_50_100.json')\n",
+    "# cfg_filename = os.path.join('profiles', 'gokce_timeSIT_90_120.json')\n",
+    "# gokce_timeSIT_90_108.json')\n",
     "\n",
-    "# cfg_filename = os.path.join('profiles', 'gokce_socialSIT.json')\n",
-    "#cfg_filename = os.path.join('profiles', 'miguel_socialSIT_fireface.json')\n",
+    "cfg_filename = os.path.join('profiles', 'gokce_socialSIT.json')\n",
+    "# cfg_filename = os.path.join('profiles', 'miguel_socialSIT_fireface.json')\n",
     "# cfg_filename = os.path.join('profiles', 'miguel_socialSIT_fireface_chirp.json')\n",
     "\n",
     "# cfg_filename = os.path.join('profiles', 'andrey_ppcSIT_SL_009266.json')"
@@ -106,7 +109,143 @@
    ]
   },
   {
-   "attachments": {},
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "    0 Microsoft Sound Mapper - Input, MME (2 in, 0 out)\n",
+       ">   1 Line 1/2 (M-Audio Delta 410), MME (2 in, 0 out)\n",
+       "    2 S/PDIF (M-Audio Delta 410), MME (2 in, 0 out)\n",
+       "    3 Monitor (M-Audio Delta 410), MME (2 in, 0 out)\n",
+       "    4 Multichannel (M-Audio Delta 410, MME (2 in, 0 out)\n",
+       "    5 ADAT 1 (1+2) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "    6 Analog (9+10) (RME Fireface UFX, MME (2 in, 0 out)\n",
+       "    7 Analog (5+6) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "    8 Analog (1+2) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "    9 Analog (3+4) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "   10 Analog (11+12) (RME Fireface UF, MME (2 in, 0 out)\n",
+       "   11 Analog (7+8) (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "   12 AES (RME Fireface UFX), MME (2 in, 0 out)\n",
+       "   13 Microsoft Sound Mapper - Output, MME (0 in, 2 out)\n",
+       "<  14 Speakers (3- Realtek High Defin, MME (0 in, 2 out)\n",
+       "   15 Line 1/2 (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   16 Line 3/4 (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   17 Line 7/8 (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   18 Line 5/6 (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   19 S/PDIF (M-Audio Delta 410), MME (0 in, 2 out)\n",
+       "   20 Multichannel (M-Audio Delta 410, MME (0 in, 2 out)\n",
+       "   21 Analog (11+12) (RME Fireface UF, MME (0 in, 2 out)\n",
+       "   22 Analog (7+8) (RME Fireface UFX), MME (0 in, 2 out)\n",
+       "   23 AES (RME Fireface UFX), MME (0 in, 2 out)\n",
+       "   24 Speakers (RME Fireface UFX), MME (0 in, 2 out)\n",
+       "   25 ADAT 1 (1+2) (RME Fireface UFX), MME (0 in, 2 out)\n",
+       "   26 Analog (5+6) (RME Fireface UFX), MME (0 in, 2 out)\n",
+       "   27 Analog (3+4) (RME Fireface UFX), MME (0 in, 2 out)\n",
+       "   28 Analog (9+10) (RME Fireface UFX, MME (0 in, 2 out)\n",
+       "   29 Primary Sound Capture Driver, Windows DirectSound (2 in, 0 out)\n",
+       "   30 Line 1/2 (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
+       "   31 S/PDIF (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
+       "   32 Monitor (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
+       "   33 Multichannel (M-Audio Delta 410), Windows DirectSound (2 in, 0 out)\n",
+       "   34 ADAT 1 (1+2) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   35 Analog (9+10) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   36 Analog (5+6) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   37 Analog (1+2) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   38 Analog (3+4) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   39 Analog (11+12) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   40 Analog (7+8) (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   41 AES (RME Fireface UFX), Windows DirectSound (2 in, 0 out)\n",
+       "   42 Primary Sound Driver, Windows DirectSound (0 in, 2 out)\n",
+       "   43 Speakers (3- Realtek High Definition Audio), Windows DirectSound (0 in, 2 out)\n",
+       "   44 Analog (11+12) (RME Fireface UFX), Windows DirectSound (0 in, 2 out)\n",
+       "   45 Line 1/2 (M-Audio Delta 410), Windows DirectSound (0 in, 2 out)\n",
+       "   46 Line 3/4 (M-Audio Delta 410), Windows DirectSound (0 in, 2 out)\n",
+       "   47 Line 7/8 (M-Audio Delta 410), Windows DirectSound (0 in, 2 out)\n",
+       "   48 Line 5/6 (M-Audio Delta 410), Windows DirectSound (0 in, 2 out)\n",
+       "   49 Analog (7+8) (RME Fireface UFX), Windows DirectSound (0 in, 2 out)\n",
+       "   50 S/PDIF (M-Audio Delta 410), Windows DirectSound (0 in, 2 out)\n",
+       "   51 AES (RME Fireface UFX), Windows DirectSound (0 in, 2 out)\n",
+       "   52 Speakers (RME Fireface UFX), Windows DirectSound (0 in, 2 out)\n",
+       "   53 Multichannel (M-Audio Delta 410), Windows DirectSound (0 in, 2 out)\n",
+       "   54 ADAT 1 (1+2) (RME Fireface UFX), Windows DirectSound (0 in, 2 out)\n",
+       "   55 Analog (5+6) (RME Fireface UFX), Windows DirectSound (0 in, 2 out)\n",
+       "   56 Analog (3+4) (RME Fireface UFX), Windows DirectSound (0 in, 2 out)\n",
+       "   57 Analog (9+10) (RME Fireface UFX), Windows DirectSound (0 in, 2 out)\n",
+       "   58 ASIO Fireface USB, ASIO (16 in, 16 out)\n",
+       "   59 M-Audio Delta ASIO, ASIO (6 in, 10 out)\n",
+       "   60 Analog (11+12) (RME Fireface UFX), Windows WASAPI (0 in, 2 out)\n",
+       "   61 Line 1/2 (M-Audio Delta 410), Windows WASAPI (0 in, 2 out)\n",
+       "   62 Line 3/4 (M-Audio Delta 410), Windows WASAPI (0 in, 2 out)\n",
+       "   63 Line 7/8 (M-Audio Delta 410), Windows WASAPI (0 in, 2 out)\n",
+       "   64 Line 5/6 (M-Audio Delta 410), Windows WASAPI (0 in, 2 out)\n",
+       "   65 Analog (7+8) (RME Fireface UFX), Windows WASAPI (0 in, 2 out)\n",
+       "   66 S/PDIF (M-Audio Delta 410), Windows WASAPI (0 in, 2 out)\n",
+       "   67 AES (RME Fireface UFX), Windows WASAPI (0 in, 2 out)\n",
+       "   68 Speakers (3- Realtek High Definition Audio), Windows WASAPI (0 in, 2 out)\n",
+       "   69 Speakers (RME Fireface UFX), Windows WASAPI (0 in, 2 out)\n",
+       "   70 Multichannel (M-Audio Delta 410), Windows WASAPI (0 in, 2 out)\n",
+       "   71 ADAT 1 (1+2) (RME Fireface UFX), Windows WASAPI (0 in, 2 out)\n",
+       "   72 Analog (5+6) (RME Fireface UFX), Windows WASAPI (0 in, 2 out)\n",
+       "   73 Analog (3+4) (RME Fireface UFX), Windows WASAPI (0 in, 2 out)\n",
+       "   74 Analog (9+10) (RME Fireface UFX), Windows WASAPI (0 in, 2 out)\n",
+       "   75 ADAT 1 (1+2) (RME Fireface UFX), Windows WASAPI (2 in, 0 out)\n",
+       "   76 Analog (9+10) (RME Fireface UFX), Windows WASAPI (2 in, 0 out)\n",
+       "   77 Line 1/2 (M-Audio Delta 410), Windows WASAPI (2 in, 0 out)\n",
+       "   78 Analog (5+6) (RME Fireface UFX), Windows WASAPI (2 in, 0 out)\n",
+       "   79 S/PDIF (M-Audio Delta 410), Windows WASAPI (2 in, 0 out)\n",
+       "   80 Monitor (M-Audio Delta 410), Windows WASAPI (2 in, 0 out)\n",
+       "   81 Analog (1+2) (RME Fireface UFX), Windows WASAPI (2 in, 0 out)\n",
+       "   82 Analog (3+4) (RME Fireface UFX), Windows WASAPI (2 in, 0 out)\n",
+       "   83 Analog (11+12) (RME Fireface UFX), Windows WASAPI (2 in, 0 out)\n",
+       "   84 Analog (7+8) (RME Fireface UFX), Windows WASAPI (2 in, 0 out)\n",
+       "   85 AES (RME Fireface UFX), Windows WASAPI (2 in, 0 out)\n",
+       "   86 Multichannel (M-Audio Delta 410), Windows WASAPI (2 in, 0 out)\n",
+       "   87 Line in at front panel (black) (Line in at front panel (black)), Windows WDM-KS (2 in, 0 out)\n",
+       "   88 Line in at rear panel (Blue) (Line in at rear panel (Blue)), Windows WDM-KS (2 in, 0 out)\n",
+       "   89 Speakers (Realtek HD Audio output), Windows WDM-KS (0 in, 2 out)\n",
+       "   90 Stereo Mix (Realtek HD Audio Stereo input), Windows WDM-KS (2 in, 0 out)\n",
+       "   91 Line 1/2 (Delta 410 1/2), Windows WDM-KS (0 in, 2 out)\n",
+       "   92 Line 1/2 (Delta 410 1/2), Windows WDM-KS (2 in, 0 out)\n",
+       "   93 Line 3/4 (Delta 410 3/4), Windows WDM-KS (0 in, 2 out)\n",
+       "   94 Line 5/6 (Delta 410 5/6), Windows WDM-KS (0 in, 2 out)\n",
+       "   95 Line 7/8 (Delta 410 7/8), Windows WDM-KS (0 in, 2 out)\n",
+       "   96 Monitor (Delta\t410 Monitor), Windows WDM-KS (2 in, 0 out)\n",
+       "   97 Multichannel (Delta 410 Multi), Windows WDM-KS (0 in, 10 out)\n",
+       "   98 Multichannel (Delta 410 Multi), Windows WDM-KS (6 in, 0 out)\n",
+       "   99 S/PDIF 1 (Delta 410 SPDIF), Windows WDM-KS (0 in, 2 out)\n",
+       "  100 S/PDIF 2 (Delta 410 SPDIF), Windows WDM-KS (0 in, 2 out)\n",
+       "  101 S/PDIF (Delta 410 SPDIF), Windows WDM-KS (2 in, 0 out)\n",
+       "  102 Analog (1+2) (Fireface Analog (1+2)), Windows WDM-KS (0 in, 8 out)\n",
+       "  103 Analog (1+2) (Fireface Analog (1+2)), Windows WDM-KS (2 in, 0 out)\n",
+       "  104 Analog (3+4) (Fireface Analog (3+4)), Windows WDM-KS (0 in, 2 out)\n",
+       "  105 Analog (3+4) (Fireface Analog (3+4)), Windows WDM-KS (2 in, 0 out)\n",
+       "  106 Analog (5+6) (Fireface Analog (5+6)), Windows WDM-KS (0 in, 2 out)\n",
+       "  107 Analog (5+6) (Fireface Analog (5+6)), Windows WDM-KS (2 in, 0 out)\n",
+       "  108 Analog (7+8) (Fireface Analog (7+8)), Windows WDM-KS (0 in, 2 out)\n",
+       "  109 Analog (7+8) (Fireface Analog (7+8)), Windows WDM-KS (2 in, 0 out)\n",
+       "  110 Analog (9+10) (Fireface Analog (9+10)), Windows WDM-KS (0 in, 2 out)\n",
+       "  111 Analog (9+10) (Fireface Analog (9+10)), Windows WDM-KS (2 in, 0 out)\n",
+       "  112 Analog (11+12) (Fireface Analog (11+12)), Windows WDM-KS (0 in, 2 out)\n",
+       "  113 Analog (11+12) (Fireface Analog (11+12)), Windows WDM-KS (2 in, 0 out)\n",
+       "  114 AES (Fireface AES), Windows WDM-KS (0 in, 2 out)\n",
+       "  115 AES (Fireface AES), Windows WDM-KS (2 in, 0 out)\n",
+       "  116 ADAT 1 (1+2) (Fireface ADAT 1 (1+2)), Windows WDM-KS (0 in, 2 out)\n",
+       "  117 ADAT 1 (1+2) (Fireface ADAT 1 (1+2)), Windows WDM-KS (2 in, 0 out)"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sd.query_devices() "
+   ]
+  },
+  {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -117,7 +256,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -145,6 +284,8 @@
     "    cfg['sound']['wav_file'] = os.path.join('assets', cfg['sound']['wav_file'])\n",
     "if 'continuous' in cfg['sound']:\n",
     "    cfg['sound']['continuous']['wav_file'] = os.path.join('assets', cfg['sound']['continuous']['wav_file'])\n",
+    "if 'cont_noise' in cfg['sound']:\n",
+    "    cfg['sound']['cont_noise']['filepath'] = os.path.join('assets', cfg['sound']['cont_noise']['filepath'])\n",
     "    \n",
     "# Saves all parameters to a JSON file with the user-defined \"Experiment ID\" as filename\n",
     "with open(os.path.join(save_to, experiment_id + '.json'), 'w') as f:\n",
@@ -160,17 +301,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 7,
    "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"
+    "    duration_total = cfg_exp['session_duration'] + cfg_exp['silence_before'] + cfg_exp['silence_after']\n",
+    "    return time.time() > t_start + duration_total if t_start is not None else False"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -181,7 +323,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 9,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -196,7 +338,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 10,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -206,7 +348,6 @@
    ]
   },
   {
-   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -223,7 +364,32 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 11,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'filepath': 'assets\\\\chirp_rate192KHz_100ms_2000Hz_30000Hz.wav',\n",
+       " 'amp': 0.5,\n",
+       " 'channels': [3],\n",
+       " 'enabled': True}"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "cfg['sound']['cont_noise']"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
    "metadata": {
     "scrolled": true
    },
@@ -454,8 +620,20 @@
     "                sound.value = 0\n",
     "                punishment_since = None\n",
     "\n",
-    "            if c_time > trial_start and iti_distance > cfg_exp['iti_distance'] and \\\n",
-    "                    isl_factory.last_tgt_x is not None and not pt.is_inside(isl_factory.last_tgt_x, isl_factory.last_tgt_y, cfg_exp['target_radius']):\n",
+    "            # animal is not inside the island\n",
+    "            if isl_factory.last_tgt_x is not None:\n",
+    "                is_inside_island = pt.is_inside(isl_factory.last_tgt_x, isl_factory.last_tgt_y, cfg_exp['target_radius'])\n",
+    "            else:\n",
+    "                is_inside_island = False\n",
+    "\n",
+    "            # silence after the recording\n",
+    "            in_silence = c_time < t_start + cfg_exp['silence_before'] or\\\n",
+    "                c_time > t_start + cfg_exp['silence_before'] + cfg_exp['session_duration']\n",
+    "            \n",
+    "            # enough time in ITI\n",
+    "            # animal is not sitting in ITI\n",
+    "            # not yet silence period after\n",
+    "            if c_time > trial_start and iti_distance > cfg_exp['iti_distance'] and not is_inside_island and not in_silence:\n",
     "                ## TODO refactor this + trial init below into a function\n",
     "                # init_new_trial\n",
     "                islands = isl_factory.generate_islands(c_time - t_start)\n",
@@ -529,20 +707,25 @@
     "            if status.value == 1: # start the session\n",
     "                if t_start is None:\n",
     "                    t_start = c_time\n",
-    "                trial_start = c_time\n",
+    "                trial_start = c_time + cfg_exp['silence_before']\n",
     "                status.value = 2\n",
-    "                \n",
+    "\n",
     "                # init_new_trial\n",
-    "                islands = isl_factory.generate_islands(c_time - t_start)\n",
-    "                log_islands(islands)  # log island(s) positions\n",
-    "                sound.value = 1\n",
-    "                phase = 1\n",
-    "                trial += 1\n",
+    "                if cfg_exp['silence_before'] == 0:\n",
+    "                    islands = isl_factory.generate_islands(c_time - t_start)\n",
+    "                    log_islands(islands)  # log island(s) positions\n",
+    "                    sound.value = 1\n",
+    "                    phase = 1\n",
+    "                    trial += 1\n",
     "\n",
-    "                # log trial start\n",
-    "                tgt = [i for i in islands if not i.is_distractor][0]\n",
-    "                log_event(c_time, round(tgt.x, 4), round(tgt.y, 4), round(tgt.r, 4), trial, 0)\n",
-    "                \n",
+    "                    # log trial start\n",
+    "                    tgt = [i for i in islands if not i.is_distractor][0]\n",
+    "                    log_event(c_time, round(tgt.x, 4), round(tgt.y, 4), round(tgt.r, 4), trial, 0)\n",
+    "                    \n",
+    "                else:  # or start with ITI to record in silence before\n",
+    "                    sound.value = 0\n",
+    "                    phase = 2\n",
+    "                    \n",
     "                # init light events\n",
     "                timers = []\n",
     "                for event_t in cfg_exp['light_events']:\n",
@@ -621,7 +804,6 @@
    ]
   },
   {
-   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -630,7 +812,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 13,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -642,7 +824,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 14,
    "metadata": {},
    "outputs": [
     {
@@ -652,7 +834,7 @@
      "traceback": [
       "\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;32m<ipython-input-14-df6d003c4298>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m      3\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[1;31m# do pack data to HDF5\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[0mh5name\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mpack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msession_path\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      6\u001b[0m \u001b[0mtrial\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;32mD:\\runSIT\\..\\pipeline\\postprocessing\\pack.py\u001b[0m in \u001b[0;36mpack\u001b[1;34m(session_path)\u001b[0m\n\u001b[0;32m    165\u001b[0m         \u001b[1;31m# head direction\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    166\u001b[0m         \u001b[0mtemp_tl\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcolumn_stack\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mpos_at_freq\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mx_smooth\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my_smooth\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mspeed\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 167\u001b[1;33m         \u001b[0mhd\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mhead_direction\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtemp_tl\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m    168\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    169\u001b[0m         \u001b[1;31m# trial numbers\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;32mD:\\runSIT\\..\\pipeline\\postprocessing\\head_direction.py\u001b[0m in \u001b[0;36mhead_direction\u001b[1;34m(tl, hd_update_speed)\u001b[0m\n\u001b[0;32m     22\u001b[0m     \u001b[0mcrit\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mwhere\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdiff\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0midle_idxs\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     23\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 24\u001b[1;33m     \u001b[0midle_periods\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[1;33m(\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m)\u001b[0m  \u001b[1;31m# first idle period\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     25\u001b[0m     \u001b[1;32mfor\u001b[0m \u001b[0mi\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mpoint\u001b[0m \u001b[1;32min\u001b[0m \u001b[0menumerate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m-\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     26\u001b[0m         \u001b[0midx_start\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0midle_idxs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcrit\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mi\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;31mIndexError\u001b[0m: index 0 is out of bounds for axis 0 with size 0"
@@ -669,7 +851,6 @@
    ]
   },
   {
-   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -765,7 +946,6 @@
    ]
   },
   {
-   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -776,7 +956,7 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "scrolled": true
+    "scrolled": false
    },
    "outputs": [],
    "source": [
@@ -816,7 +996,9 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "scrolled": true
+   },
    "outputs": [],
    "source": [
     "fig = plt.figure(figsize=(4, 4))\n",
@@ -848,7 +1030,9 @@
    "execution_count": null,
    "metadata": {},
    "outputs": [],
-   "source": []
+   "source": [
+    "# import this\n"
+   ]
   },
   {
    "cell_type": "code",

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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 166 - 274
controllers/sound.ipynb


+ 54 - 8
controllers/sound.py

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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1042 - 0
controllers/sound_chirp.ipynb


+ 301 - 0
controllers/sound_chirp.py

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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1031 - 0
controllers/sound_chirp2.ipynb


+ 301 - 0
controllers/sound_chirp2.py

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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 79 - 42
passive.ipynb


+ 11 - 1
profiles/default.json

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