{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Overwriting sound.py\n"
     ]
    }
   ],
   "source": [
    "%%writefile sound.py\n",
    "import numpy as np\n",
    "import time\n",
    "from scipy.signal import lfilter\n",
    "\n",
    "\n",
    "class SoundController:\n",
    "    # https://python-sounddevice.readthedocs.io/en/0.3.15/api/streams.html#sounddevice.OutputStream\n",
    "    \n",
    "    default_cfg = {\n",
    "        \"device\": [1, 26],\n",
    "        \"n_channels\": 10,\n",
    "        \"sounds\": {\n",
    "            \"noise\": {\"amp\": 0.5, \"channels\": [6]},\n",
    "            \"background\": {\"freq\": 10000, \"amp\": 0.23, \"channels\": [6]},\n",
    "            \"target\": {\"freq\": 660, \"amp\": 0.15, \"channels\": [6]}, \n",
    "            \"distractor1\": {\"freq\": 860, \"amp\": 0.15, \"channels\": [1, 3], \"enabled\": False},\n",
    "            \"distractor2\": {\"freq\": 1060, \"amp\": 0.25, \"channels\": [1, 3], \"enabled\": False},\n",
    "            \"distractor3\": {\"freq\": 1320, \"amp\": 0.2, \"channels\": [1, 3], \"enabled\": False}\n",
    "        },\n",
    "        \"pulse_duration\": 0.05,\n",
    "        \"sample_rate\": 44100,\n",
    "        \"latency\": 0.25,\n",
    "        \"volume\": 0.7,\n",
    "        \"roving\": 5.0,\n",
    "        \"file_path\": \"sounds.csv\"\n",
    "    }\n",
    "    \n",
    "    @classmethod\n",
    "    def get_pure_tone(cls, freq, duration, sample_rate=44100):\n",
    "        x = np.linspace(0, duration * freq * 2*np.pi, int(duration*sample_rate), dtype=np.float32)\n",
    "        return np.sin(x)\n",
    "\n",
    "    @classmethod\n",
    "    def get_cos_window(cls, tone, win_duration, sample_rate=44100):\n",
    "        x = np.linspace(0, np.pi/2, int(win_duration * sample_rate), dtype=np.float32)\n",
    "        onset =  np.sin(x)\n",
    "        middle = np.ones(len(tone) - 2 * len(x))\n",
    "        offset = np.cos(x)\n",
    "        return np.concatenate([onset, middle, offset])\n",
    "\n",
    "    @classmethod\n",
    "    def get_tone_stack(cls, cfg):\n",
    "        # silence\n",
    "        silence = np.zeros(2, dtype='float32')\n",
    "        sounds = {'silence': np.column_stack([silence for x in range(cfg['n_channels'])])}\n",
    "\n",
    "        # noise\n",
    "        filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])\n",
    "        filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])\n",
    "\n",
    "        noise = np.random.randn(int(0.25 * cfg['sample_rate']))  # 250ms of noise\n",
    "        noise = lfilter(filter_a, filter_b, noise)\n",
    "        noise = noise / np.abs(noise).max() * cfg['sounds']['noise']['amp']\n",
    "        noise = noise.astype(np.float32)\n",
    "        empty = np.zeros((len(noise), cfg['n_channels']), dtype='float32')\n",
    "        for ch in cfg['sounds']['noise']['channels']:\n",
    "            empty[:, ch-1] = noise\n",
    "        sounds['noise'] = empty\n",
    "        \n",
    "        # all other sounds\n",
    "        for key, snd in cfg['sounds'].items():\n",
    "            if key == 'noise' or ('enabled' in snd and not snd['enabled']):\n",
    "                continue  # skip noise or unused sounds\n",
    "                \n",
    "            tone = cls.get_pure_tone(snd['freq'], cfg['pulse_duration'], cfg['sample_rate']) * cfg['volume']\n",
    "            tone = tone * cls.get_cos_window(tone, 0.01, cfg['sample_rate'])  # onset / offset\n",
    "            tone = tone * snd['amp']  # amplitude\n",
    "            \n",
    "            sound = np.zeros([len(tone), cfg['n_channels']], dtype='float32')\n",
    "            for j in snd['channels']:\n",
    "                sound[:, j-1] = tone\n",
    "           \n",
    "            sounds[key] = sound\n",
    "\n",
    "        return sounds\n",
    "        \n",
    "    @classmethod\n",
    "    def run(cls, selector, status, cfg):\n",
    "        \"\"\"\n",
    "        selector        mp.Value object to set the sound to be played\n",
    "        status          mp.Value object to stop the loop\n",
    "        \"\"\"\n",
    "        import sounddevice as sd  # must be inside the function\n",
    "        import numpy as np\n",
    "        import time\n",
    "        \n",
    "        commutator = {\n",
    "            -1: 'noise',\n",
    "            0:  'silence',\n",
    "            1:  'background',\n",
    "            2:  'target',\n",
    "            3:  'distractor1',\n",
    "            4:  'distractor2',\n",
    "            5:  'distractor3'\n",
    "        }\n",
    "        \n",
    "        sounds = cls.get_tone_stack(cfg)\n",
    "\n",
    "        sd.default.device = cfg['device']\n",
    "        sd.default.samplerate = cfg['sample_rate']\n",
    "        stream = sd.OutputStream(samplerate=cfg['sample_rate'], channels=cfg['n_channels'], dtype='float32', blocksize=256)\n",
    "        stream.start()\n",
    "\n",
    "        next_beat = time.time() + cfg['latency']\n",
    "        with open(cfg['file_path'], 'w') as f:\n",
    "            f.write(\"time,id\\n\")\n",
    "\n",
    "        while status.value > 0:\n",
    "            if status.value == 2:  # running state\n",
    "                t0 = time.time()\n",
    "                if t0 < next_beat:\n",
    "                    #time.sleep(0.0001)  # not to spin the wheels too much\n",
    "                    if stream.write_available > 2:\n",
    "                        stream.write(sounds['silence'])  # silence\n",
    "                    continue\n",
    "\n",
    "                roving = 10**((np.random.rand() * cfg['roving'] - cfg['roving']/2.0)/20.)\n",
    "                roving = roving if int(selector.value) > -1 else 1  # no roving for noise\n",
    "                stream.write(sounds[commutator[int(selector.value)]] * roving)\n",
    "                with open(cfg['file_path'], 'a') as f:\n",
    "                    f.write(\",\".join([str(x) for x in (t0, selector.value)]) + \"\\n\")\n",
    "\n",
    "                next_beat += cfg['latency']\n",
    "                \n",
    "                if stream.write_available > 2:\n",
    "                    stream.write(sounds['silence'])  # silence\n",
    "            \n",
    "            else:  # idle state\n",
    "                next_beat = time.time() + cfg['latency']\n",
    "                time.sleep(0.05)\n",
    "                \n",
    "        stream.stop()\n",
    "        print('Sound stopped')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import time, os\n",
    "import multiprocess as mp\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "from scipy.signal import lfilter\n",
    "from sound import SoundController"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Building sound stack"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import time, os\n",
    "from sound import SoundController\n",
    "\n",
    "cfg = SoundController.default_cfg\n",
    "sounds = SoundController.get_tone_stack(cfg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 21792 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "_ = plt.plot(sounds['noise'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Roving and onset window"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sound import SoundController\n",
    "\n",
    "duration = 0.05\n",
    "freq = 440\n",
    "\n",
    "tone = SoundController.get_pure_tone(freq, duration)\n",
    "tone = tone * SoundController.get_cos_window(tone, 0.01)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<matplotlib.lines.Line2D at 0xab1e490>]"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 41468 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "plt.plot(tone)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Building noise"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from scipy.signal import lfilter\n",
    "\n",
    "sample_rate = 44100\n",
    "\n",
    "filter_a = np.array([0.0075, 0.0225, 0.0225, 0.0075])\n",
    "filter_b = np.array([1.0000,-2.1114, 1.5768,-0.4053])\n",
    "\n",
    "noise = np.random.randn(sample_rate)\n",
    "noise = lfilter(filter_a, filter_b, noise)\n",
    "noise = noise / np.abs(noise).max() * 0.5\n",
    "noise = noise.astype(np.float32)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<matplotlib.lines.Line2D at 0xab7c730>]"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 16972 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "plt.plot(noise)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Testing sound pulses without MP"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import time, os\n",
    "import multiprocess as mp\n",
    "from sound import SoundController\n",
    "\n",
    "# sound selector: 0 - silence, 1 - tone 1, 2 - tone 2\n",
    "selector = mp.Value('i', -1)\n",
    "\n",
    "# loggin status: 1 - idle, 2 - running, 0 - stopped\n",
    "status = mp.Value('i', 2)\n",
    "\n",
    "cfg = SoundController.default_cfg\n",
    "#cfg['device'] = [1, 26]  # 'M-Audio Delta ASIO'\n",
    "#SoundController.run(selector, status, cfg)\n",
    "\n",
    "# nothing happens for a second\n",
    "time.sleep(1)\n",
    "\n",
    "status.value = 2\n",
    "for i in range(6):\n",
    "    time.sleep(1)\n",
    "    selector.value = -1 if selector.value == 1 else 1\n",
    "\n",
    "# stop\n",
    "status.value = 0\n",
    "time.sleep(0.2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Testing sound pulses with MP"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import time, os\n",
    "import multiprocess as mp\n",
    "from sound import SoundController\n",
    "\n",
    "# sound selector: 0 - silence, 1 - tone 1, 2 - tone 2\n",
    "selector = mp.Value('i', 1)\n",
    "\n",
    "# loggin status: 1 - idle, 2 - running, 0 - stopped\n",
    "status = mp.Value('i', 1)\n",
    "\n",
    "cfg = SoundController.default_cfg\n",
    "cfg['device'] = [1, 26]  # 'M-Audio Delta ASIO'\n",
    "sc = mp.Process(target=SoundController.run, args=(selector, status, cfg))\n",
    "sc.start()\n",
    "\n",
    "# nothing happens for a second\n",
    "time.sleep(1)\n",
    "\n",
    "status.value = 2\n",
    "for i in range(10):\n",
    "    time.sleep(1)\n",
    "    selector.value = -1 if selector.value == 1 else 1\n",
    "\n",
    "# stop\n",
    "status.value = 0\n",
    "time.sleep(0.2)\n",
    "sc.join()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<matplotlib.lines.Line2D at 0x8703850>]"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 19620 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "ds = np.loadtxt('test_sound_log.csv', delimiter=',', skiprows=1)\n",
    "plt.plot(np.diff(ds[:, 0]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Sounddevice playground"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{'name': 'M-Audio Delta ASIO',\n",
       "  'hostapi': 2,\n",
       "  'max_input_channels': 6,\n",
       "  'max_output_channels': 10,\n",
       "  'default_low_input_latency': 0.0014512471655328798,\n",
       "  'default_low_output_latency': 0.0014512471655328798,\n",
       "  'default_high_input_latency': 0.09287981859410431,\n",
       "  'default_high_output_latency': 0.09287981859410431,\n",
       "  'default_samplerate': 44100.0}]"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import sounddevice as sd\n",
    "[x for x in sd.query_devices() if x['name'].find('ASIO') > 0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sounddevice as sd\n",
    "import numpy as np\n",
    "import keyboard  # using module keyboard\n",
    "\n",
    "sd.default.device = [1, 26]\n",
    "sd.default.samplerate = 44100\n",
    "stream = sd.OutputStream(samplerate=44100, channels=10, dtype='float32')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3rd channel - left arena speaker\n",
    "# 1st channel - right arena speaker"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "duration = 0.5\n",
    "\n",
    "x1 = np.linspace(0, duration * 220 * 2*np.pi, int(duration*44100), dtype=np.float32)\n",
    "x2 = np.linspace(0, duration * 440 * 2*np.pi, int(duration*44100), dtype=np.float32)\n",
    "y1 = np.sin(x1)\n",
    "y2 = np.sin(x2)\n",
    "sil = np.zeros(len(x1), dtype=np.float32)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "stream.start()\n",
    "\n",
    "try:\n",
    "    while True:  # making a loop\n",
    "        if keyboard.is_pressed('q'):  # if key 'q' is pressed \n",
    "            break  # finishing the loop\n",
    "\n",
    "        stream.write(np.column_stack([y1, sil, sil, sil, sil, sil, sil, y2, sil, sil]) * 0.8)\n",
    "        #stream.write(np.column_stack([y2, y2, y2, y2, y2, y2, y2, y2, y2, y2]) * 0.8)\n",
    "    \n",
    "finally:\n",
    "    stream.stop()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
