1
0
Просмотр исходного кода

Add microphones controller, notebook and .py

miguel_bengala 1 год назад
Родитель
Сommit
5bca8c0797
2 измененных файлов с 688 добавлено и 0 удалено
  1. 624 0
      controllers/microphones.ipynb
  2. 64 0
      controllers/microphones.py

+ 624 - 0
controllers/microphones.ipynb

@@ -0,0 +1,624 @@
+{
+ "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": 2,
+   "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",
+    "                            stream.start()\n",
+    "\n",
+    "                        f.write(MicrophoneController.queue.get())\n",
+    "\n",
+    "                    else:\n",
+    "                        time.sleep(0.01)\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
+}

+ 64 - 0
controllers/microphones.py

@@ -0,0 +1,64 @@
+#^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.")
+                            stream.start()
+
+                        f.write(MicrophoneController.queue.get())
+
+                    else:
+                        time.sleep(0.01)
+                except KeyboardInterrupt:
+                    stream.stop()
+                    stream.close()
+                    break
+