Browse Source

Merge branch 'microphones_dev' of miguel_bengala/runSIT into master

Beautiful
Andrey Sobolev 1 year ago
parent
commit
db9cfc3b48
4 changed files with 718 additions and 2 deletions
  1. 15 2
      SIT.ipynb
  2. 627 0
      controllers/microphones.ipynb
  3. 67 0
      controllers/microphones.py
  4. 9 0
      profiles/default.json

+ 15 - 2
SIT.ipynb

@@ -32,6 +32,7 @@
     "from controllers.situtils import FPSTimes\n",
     "from controllers.situtils import FPSTimes\n",
     "from controllers.camera import WebcamStream\n",
     "from controllers.camera import WebcamStream\n",
     "from controllers.video import VideoWriter\n",
     "from controllers.video import VideoWriter\n",
+    "from controllers.microphones import MicrophoneController\n",
     "from controllers.position import PositionTrackerSingle, PositionTrackerDouble\n",
     "from controllers.position import PositionTrackerSingle, PositionTrackerDouble\n",
     "from controllers.sound import SoundController, ContinuousSoundStream\n",
     "from controllers.sound import SoundController, ContinuousSoundStream\n",
     "from controllers.serial import MCSArduino, FakeArduino, SpeakerMotor, CableMotor\n",
     "from controllers.serial import MCSArduino, FakeArduino, SpeakerMotor, CableMotor\n",
@@ -79,7 +80,8 @@
     "    cfg_local = json.load(json_file)\n",
     "    cfg_local = json.load(json_file)\n",
     "\n",
     "\n",
     "for key in cfg.keys():\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",
     "cfg['experiment']['experiment_date'] = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')\n",
     "\n",
     "\n",
     "# print loaded settings\n",
     "# print loaded settings\n",
@@ -125,6 +127,8 @@
     "# update paths (assuming this paths are relative to this notebook)\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']['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['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']['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['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']['file_path'] = os.path.join(save_to, cfg['experiment']['file_path'])\n",
@@ -263,6 +267,11 @@
     "vw = VideoWriter(status, vs, cfg['video'])\n",
     "vw = VideoWriter(status, vs, cfg['video'])\n",
     "vw.start()\n",
     "vw.start()\n",
     "\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",
     "# start position tracking\n",
     "pt = PositionTrackerSingle(status, vs, cfg['position']) if cfg['position']['single_agent'] else PositionTrackerDouble(status, vs, cfg['position'])\n",
     "pt = PositionTrackerSingle(status, vs, cfg['position']) if cfg['position']['single_agent'] else PositionTrackerDouble(status, vs, cfg['position'])\n",
     "pt.start()\n",
     "pt.start()\n",
@@ -584,6 +593,10 @@
     "    for ctrl in [pt, vw, vs]:\n",
     "    for ctrl in [pt, vw, vs]:\n",
     "        ctrl.stop()\n",
     "        ctrl.stop()\n",
     "\n",
     "\n",
+    "    # stop microphones recording process\n",
+    "    if cfg['microphones']['record_audio']:\n",
+    "        mc.join()\n",
+    "\n",
     "    if 'continuous' in cfg['sound']:\n",
     "    if 'continuous' in cfg['sound']:\n",
     "        time.sleep(cfg['sound']['continuous']['end_sleep'])\n",
     "        time.sleep(cfg['sound']['continuous']['end_sleep'])\n",
     "        cst.stop()"
     "        cst.stop()"
@@ -840,7 +853,7 @@
    "name": "python",
    "name": "python",
    "nbconvert_exporter": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
    "pygments_lexer": "ipython3",
-   "version": "3.8.8"
+   "version": "3.10.8"
   }
   }
  },
  },
  "nbformat": 4,
  "nbformat": 4,

+ 627 - 0
controllers/microphones.ipynb

@@ -0,0 +1,627 @@
+{
+ "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:\n",
+    "                                f.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"
+   ]
+  },
+  {
+   "attachments": {},
+   "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\")"
+   ]
+  },
+  {
+   "attachments": {},
+   "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.10.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:
+                                f.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
+        

+ 9 - 0
profiles/default.json

@@ -13,6 +13,15 @@
         "save_contours": false,
         "save_contours": false,
         "csv_path": "video.csv"
         "csv_path": "video.csv"
     },
     },
+    "microphones": {
+        "record_audio": false,
+        "sample_rate": 192000,
+        "device": 58,
+        "number_channels": 4,
+        "channel_selectors": [8,9,10,11],
+        "file_path": "audio.mat5",
+        "csv_path": "microphones.csv"
+    },
     "position": {
     "position": {
         "single_agent": true,
         "single_agent": true,
         "history_duration": 5,
         "history_duration": 5,