diff --git a/.gitignore b/.gitignore index 31e5e8b..31ceaf4 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,5 @@ venv.bak/ .mypy_cache/ .dmypy.json dmypy.json + +config_files.txt diff --git a/portiloop/notebooks/test_sounds.ipynb b/portiloop/notebooks/test_sounds.ipynb new file mode 100644 index 0000000..ae3844f --- /dev/null +++ b/portiloop/notebooks/test_sounds.ipynb @@ -0,0 +1,301 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import alsaaudio\n", + "import time\n", + "from multiprocessing import Process\n", + "from threading import Thread\n", + "from pathlib import Path\n", + "from threading import Thread, Lock\n", + "import wave" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['null',\n", + " 'jack',\n", + " 'pulse',\n", + " 'custom',\n", + " 'default',\n", + " 'sysdefault:CARD=excelsiorcard',\n", + " 'dmix:CARD=excelsiorcard,DEV=0',\n", + " 'dmix:CARD=excelsiorcard,DEV=3',\n", + " 'dsnoop:CARD=excelsiorcard,DEV=0',\n", + " 'dsnoop:CARD=excelsiorcard,DEV=3',\n", + " 'hw:CARD=excelsiorcard,DEV=0',\n", + " 'hw:CARD=excelsiorcard,DEV=3',\n", + " 'plughw:CARD=excelsiorcard,DEV=0',\n", + " 'plughw:CARD=excelsiorcard,DEV=3',\n", + " 'usbstream:CARD=excelsiorcard']" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "alsaaudio.pcms()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class Dummy:\n", + " def __getattr__(self, attr):\n", + " return lambda *args, **kwargs: None" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "class SleepSpindleRealTimeStimulator():\n", + " def __init__(self, soundname=None, lsl_streamer=Dummy(), stimulation_delay=0.0, inter_stim_delay=0.0):\n", + " \"\"\"\n", + " params: \n", + " stimulation_delay (float): simple delay between a detection and a stimulation\n", + " inter_stim_delay (float): time to wait between a stimulation and the next detection \n", + " \"\"\"\n", + " if soundname is None:\n", + " self.soundname = 'stimulus.wav' # CHANGE HERE TO THE SOUND THAT YOU WANT. ONLY ADD THE FILE NAME, NOT THE ENTIRE PATH\n", + " else:\n", + " self.soundname = soundname\n", + "# self._sound = Path(\".\").parent.parent / 'sounds' / self.soundname\n", + " self._sound = f\"../sounds/{self.soundname}\"\n", + " self._thread = None\n", + " self._lock = Lock()\n", + " self.last_detected_ts = time.time()\n", + " self.wait_t = 0.4 # 400 ms\n", + " self.delayer = None\n", + " self.lsl_streamer = lsl_streamer\n", + "\n", + " # Initialize Alsa stuff\n", + " # Open WAV file and set PCM device\n", + " with wave.open(str(self._sound), 'rb') as f: \n", + " device = 'custom'\n", + " \n", + " self.duration = f.getnframes() / float(f.getframerate())\n", + " \n", + " format = None\n", + "\n", + " # 8bit is unsigned in wav files\n", + " if f.getsampwidth() == 1:\n", + " format = alsaaudio.PCM_FORMAT_U8\n", + " # Otherwise we assume signed data, little endian\n", + " elif f.getsampwidth() == 2:\n", + " format = alsaaudio.PCM_FORMAT_S16_LE\n", + " elif f.getsampwidth() == 3:\n", + " format = alsaaudio.PCM_FORMAT_S24_3LE\n", + " elif f.getsampwidth() == 4:\n", + " format = alsaaudio.PCM_FORMAT_S32_LE\n", + " else:\n", + " raise ValueError('Unsupported format')\n", + "\n", + " self.periodsize = f.getframerate() // 8\n", + "\n", + " try:\n", + " self.pcm = alsaaudio.PCM(channels=f.getnchannels(), rate=f.getframerate(), format=format, periodsize=self.periodsize, device=device)\n", + " except alsaaudio.ALSAAudioError as e:\n", + "# print(\"WARNING: Could not open ALSA device as it is already playing a sound. To test stimulation, stop recording and try again.\")\n", + " self.pcm = Dummy()\n", + "\n", + " raise e\n", + " \n", + " # Store data in list to avoid reopening the file\n", + " self.wav_list = []\n", + " while True:\n", + " data = f.readframes(self.periodsize) \n", + " if data:\n", + " self.wav_list.append(data)\n", + " else: \n", + " break\n", + " \n", + " print(f\"DEBUG: Stimulator will play sound {self.soundname}, duration: {self.duration:.3f} seconds\")\n", + "\n", + "\n", + " def play_sound(self):\n", + " '''\n", + " Open the wav file and play a sound\n", + " '''\n", + " print(len(self.wav_list[0]))\n", + " for data in self.wav_list:\n", + " self.pcm.write(data) \n", + " \n", + " # Added this to make sure the thread does not stop before the sound is done playing\n", + " time.sleep(self.duration)\n", + " \n", + " def stimulate(self, detection_signal):\n", + " for sig in detection_signal:\n", + " # We detect a stimulation\n", + " if sig:\n", + " # Record time of stimulation\n", + " ts = time.time()\n", + " \n", + " # Check if time since last stimulation is long enough\n", + " if ts - self.last_detected_ts > self.wait_t:\n", + " if not isinstance(self.delayer, Dummy):\n", + " # If we have a delayer, notify it\n", + " self.delayer.detected()\n", + " # Send the LSL marer for the fast stimulation \n", + " self.send_stimulation(\"FAST_STIM\", False)\n", + " else:\n", + " self.send_stimulation(\"STIM\", True)\n", + "\n", + " self.last_detected_ts = ts\n", + "\n", + " def send_stimulation(self, lsl_text, sound):\n", + " # Send lsl stimulation\n", + " self.lsl_streamer.push_marker(lsl_text)\n", + " # Send sound to patient\n", + " if sound:\n", + " with self._lock:\n", + " if self._thread is None: \n", + " self._thread = Thread(target=self._t_sound, daemon=True)\n", + " self._thread.start()\n", + "\n", + " \n", + " def _t_sound(self):\n", + " self.play_sound()\n", + " with self._lock:\n", + " self._thread = None\n", + " \n", + " def test_stimulus(self):\n", + " with self._lock:\n", + " if self._thread is None:\n", + " self._thread = Thread(target=self._t_sound, daemon=True)\n", + " self._thread.start()\n", + "\n", + " def add_delayer(self, delayer):\n", + " self.delayer = delayer\n", + " self.delayer.stimulate = lambda: self.send_stimulation(\"DELAY_STIM\", True)\n", + "\n", + " def __del__(self):\n", + " print(\"DEBUG: releasing PCM\")\n", + " del self.pcm" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "sound = 'stimulus.wav'" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def sound_process():\n", + "\n", + " stimulator = SleepSpindleRealTimeStimulator(soundname=sound)\n", + " stimulator.play_sound()\n", + " time.sleep(1)\n", + " stimulator.play_sound()\n", + " del stimulator\n", + " print(\"Done\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DEBUG: Stimulator will play sound stimulus.wav, duration: 1.588 seconds\n", + "22048\n", + "22048\n", + "DEBUG: releasing PCM\n", + "Done\n" + ] + } + ], + "source": [ + "sound_process()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DEBUG: Stimulator will play sound stimulus.wav, duration: 1.588 seconds\n", + "22048\n", + "22048\n", + "DEBUG: releasing PCM\n", + "Done\n" + ] + } + ], + "source": [ + "sound_proc = Process(target=sound_process)\n", + "sound_proc.start()\n", + "sound_proc.join()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "sound_proc.join()" + ] + }, + { + "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.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/portiloop/notebooks/tests.ipynb b/portiloop/notebooks/tests.ipynb index b7ae889..bb20df0 100644 --- a/portiloop/notebooks/tests.ipynb +++ b/portiloop/notebooks/tests.ipynb @@ -10,7 +10,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e24b5e02465d4d10af827cceaa9eebf9", + "model_id": "2e7a7c5d244f46899bc564c1986d59e9", "version_major": 2, "version_minor": 0 }, @@ -25,11 +25,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "['bias', 'disabled', 'disabled', 'disabled']\n", - "indigo-bunny-portiloop\n", - "DEBUG:/home/mendel/portiloop-software/portiloop/sounds/stimul_15ms.wav\n", - "PID capture: 4194. Kill this process if program crashes before end of execution.\n", - "Saving metadata to /home/mendel/workspace/edf_recording/recording_metadata.json\n" + "PID start process: 5121. Kill this process if program crashes before end of execution.\n", + "PID capture: 5133. Kill this process if program crashes before end of execution.\n", + "Average frequency: 249.95013153215078 Hz for 1754 samples\n" ] } ], diff --git a/portiloop/setup_files/asound.conf b/portiloop/setup_files/asound.conf new file mode 100644 index 0000000..33c9aba --- /dev/null +++ b/portiloop/setup_files/asound.conf @@ -0,0 +1,36 @@ +pcm.!default { + type plug + slave.pcm "softvol" +} + +pcm.dmixer { + type dmix + ipc_key 1024 + slave { + pcm "hw:0,0" # Replace with your sound card device + period_time 0 + period_size 1024 + buffer_size 8192 + rate 48000 + } + bindings { + 0 0 + 1 1 + } +} + +ctl.dmixer { + type hw + card 0 # Replace with your sound card device +} + +pcm.softvol { + type softvol + slave { + pcm "dmixer" + } + control { + name "SoftMaster" + card 0 # Replace with your sound card device + } +} diff --git a/portiloop/setup_files/get_setup_files.sh b/portiloop/setup_files/get_setup_files.sh new file mode 100755 index 0000000..c34fa7b --- /dev/null +++ b/portiloop/setup_files/get_setup_files.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +output_file="config_files.txt" + +# List of files to concatenate +file_list=("/etc/asound.conf" "/etc/systemd/system/create_ap.service" "/etc/systemd/system/jupyter.service" "/etc/systemd/system/setup_tables.service" "/usr/local/bin/create_ap0.sh" "/etc/hostapd/hostapd.conf" "/etc/dnsmasq.conf" "/usr/local/bin/setup_tables.sh") + +# Create an empty output file +echo -n > "$output_file" + +# Loop through each file +for file_name in "${file_list[@]}"; do + echo "File: $(basename "$file_name")" >> "$output_file" # Add file name + echo "---------------------------------------------------" >> "$output_file" + cat "$file_name" >> "$output_file" # Concatenate file content + echo >> "$output_file" +done + +echo "Concatenation completed. Output file: $output_file" diff --git a/portiloop/sounds/AA_100SE.wav b/portiloop/sounds/AA_100SE.wav new file mode 100644 index 0000000..5cfc4f2 --- /dev/null +++ b/portiloop/sounds/AA_100SE.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34aac89d6a42c79b18539157bc83de6b769423d299dde6cdafaf9e8ea3cd2b35 +size 16424 diff --git a/portiloop/sounds/shortest_1_100ms (1).wav b/portiloop/sounds/shortest_1_100ms (1).wav new file mode 100644 index 0000000..8250911 --- /dev/null +++ b/portiloop/sounds/shortest_1_100ms (1).wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13db9fc120ea96fe8fabb41a280f75a0757e524eef883e27098bc498d6570761 +size 8864 diff --git a/portiloop/src/capture.py b/portiloop/src/capture.py index 2abe4bf..cc7b84b 100644 --- a/portiloop/src/capture.py +++ b/portiloop/src/capture.py @@ -27,6 +27,8 @@ from IPython.display import clear_output, display import ipywidgets as widgets import socket +from pathlib import Path + PORTILOOP_ID = f"{socket.gethostname()}-portiloop" @@ -113,7 +115,7 @@ def start_capture( q_msg, pause_value ): - print(capture_dictionary['channel_states']) +# print(f"DEBUG: Channel states: {capture_dictionary['channel_states']}") # Initialize data frontend fake_filename = RECORDING_PATH / 'test_recording.csv' @@ -131,9 +133,9 @@ def start_capture( 'filtered': filter, 'markers': detector is not None, } - print(PORTILOOP_ID) +# print(f"DEBUG: Portiloop ID: {PORTILOOP_ID}") lsl_streamer = LSLStreamer(streams, capture_dictionary['nb_channels'], capture_dictionary['frequency'], id=PORTILOOP_ID) if capture_dictionary['lsl'] else Dummy() - stimulator = stimulator_cls(lsl_streamer=lsl_streamer) if stimulator_cls is not None else None + stimulator = stimulator_cls(soundname=capture_dictionary['detection_sound'], lsl_streamer=lsl_streamer) if stimulator_cls is not None else None # Initialize filtering pipeline if filter: @@ -189,7 +191,7 @@ def start_capture( new_name = f"{name}_metadata.json" # Join the components back together into the new file path metadata_path = os.path.join(dirname, new_name) - print(f"Saving metadata to {metadata_path}") +# print(f"DEBUG: Saving metadata to {metadata_path}") with open(metadata_path, "w") as f: json.dump(metadata, f, indent=4) @@ -271,8 +273,6 @@ def start_capture( # close the frontend capture_frontend.close() recorder.close_recording_file() - if stimulator is not None: - stimulator.close() del lsl_streamer del stimulation_delayer @@ -291,7 +291,7 @@ def __init__(self, detector_cls=None, stimulator_cls=None): frontend = Frontend(self.version) self.nb_channels = frontend.get_version() - print(f"Current hardware: ADS1299 {self.nb_channels} channels | Portiloop Version: {self.version}") +# print(f"DEBUG: Current hardware: ADS1299 {self.nb_channels} channels | Portiloop Version: {self.version}") # General default parameters self.frequency = 250 @@ -327,6 +327,7 @@ def __init__(self, detector_cls=None, stimulator_cls=None): self.signal_labels = [f"ch{i+1}" for i in range(self.nb_channels)] self.channel_states = ['bias'] + ['disabled' for _ in range(self.nb_channels - 1)] self.channel_detection = 2 + self.detection_sound = "stimul_15ms.wav" # Delayer parameters self.spindle_detection_mode = 'Fast' @@ -338,18 +339,18 @@ def __init__(self, detector_cls=None, stimulator_cls=None): self.detector_cls = detector_cls self.stimulator_cls = stimulator_cls - if ADS: try: mixers = alsaaudio.mixers() if len(mixers) <= 0: warnings.warn(f"No ALSA mixer found.") self.mixer = DummyAlsaMixer() - elif 'PCM' in mixers : - self.mixer = alsaaudio.Mixer(control='PCM') +# elif 'PCM' in mixers : +# self.mixer = alsaaudio.Mixer(control='PCM') else: - self.mixer = alsaaudio.Mixer() + self.mixer = alsaaudio.Mixer(control='SoftMaster', device='dmixer') except ALSAAudioError as e: + print(e) warnings.warn(f"No ALSA mixer found. Volume control will not be available from notebook.") self.mixer = DummyAlsaMixer() @@ -379,6 +380,16 @@ def __init__(self, detector_cls=None, stimulator_cls=None): style={'description_width': 'initial'} ) + sound_dir = Path.home() / 'portiloop-software' / 'portiloop' / 'sounds' + options = [(sound[:-4], sound) for sound in os.listdir(sound_dir) if sound[-4:] == ".wav"] + self.b_sound_detect = widgets.Dropdown( + options=options, + value="stimul_15ms.wav", + description='Sound:', + disabled=False, + style={'description_width': 'initial'} + ) + self.b_accordion_channels = widgets.Accordion( children=[ widgets.GridBox([widgets.Label(f"CH{i+1}") for i in range(self.nb_channels)] + self.chann_buttons, @@ -402,7 +413,7 @@ def __init__(self, detector_cls=None, stimulator_cls=None): description='Detection', disabled=True, button_style='', # 'success', 'info', 'warning', 'danger' or '' - tooltips=['Detector and stimulator active', 'Detector and stimulator paused'], + tooltips=['Detector and stimulator paused', 'Detector and stimulator active'], ) self.b_clock = widgets.ToggleButtons( @@ -410,8 +421,7 @@ def __init__(self, detector_cls=None, stimulator_cls=None): description='Clock:', disabled=False, button_style='', # 'success', 'info', 'warning', 'danger' or '' - tooltips=['Use Coral clock (very precise, not very timely)', - 'Use ADS clock (not very precise, very timely)'], + tooltips=['Use ADS clock (not very precise, very timely)', 'Use Coral clock (very precise, not very timely)'], ) self.b_power_line = widgets.ToggleButtons( @@ -707,6 +717,7 @@ def callback(change): self.b_display.observe(self.on_b_display, 'value') self.b_filename.observe(self.on_b_filename, 'value') self.b_channel_detect.observe(self.on_b_channel_detect, 'value') + self.b_sound_detect.observe(self.on_b_sound_detect, 'value') self.b_spindle_mode.observe(self.on_b_spindle_mode, 'value') self.b_spindle_freq.observe(self.on_b_spindle_freq, 'value') self.b_power_line.observe(self.on_b_power_line, 'value') @@ -723,6 +734,7 @@ def callback(change): self.b_pause.observe(self.on_b_pause, 'value') self.b_stim_delay.observe(self.on_b_delay, 'value') self.b_inter_stim_delay.observe(self.on_b_inter_delay, 'value') + def __del__(self): @@ -731,6 +743,7 @@ def __del__(self): def display_buttons(self): display(widgets.VBox([self.b_accordion_channels, self.b_channel_detect, + self.b_sound_detect, self.b_frequency, self.b_duration, self.b_filename, @@ -779,6 +792,7 @@ def enable_buttons(self): self.b_test_impedance.disabled = False self.b_stim_delay.disabled = False self.b_inter_stim_delay.disabled = False + self.b_sound_detect.disabled = False def disable_buttons(self): self.b_frequency.disabled = True @@ -813,6 +827,11 @@ def disable_buttons(self): self.b_test_impedance.disabled = True self.b_stim_delay.disabled = True self.b_inter_stim_delay.disabled = True + self.b_sound_detect.disabled = True + + + def on_b_sound_detect(self, value): + self.detection_sound = value['new'] def on_b_channel_detect(self, value): self.channel_detection = value['new'] @@ -854,7 +873,7 @@ def on_b_capture(self, value): } self.width_display = 5 * self.frequency # Display 5 seconds of signal - + self._t_capture = Process(target=start_capture, args=(detector_cls, stimulator_cls, @@ -862,6 +881,7 @@ def on_b_capture(self, value): self.q_msg, self.pause_value,)) self._t_capture.start() + print(f"PID start process: {self._t_capture.pid}. Kill this process if program crashes before end of execution.") elif val == 'Stop': self.q_msg.put('STOP') assert self._t_capture is not None @@ -1026,7 +1046,7 @@ def on_b_inter_delay(self, value): self.inter_stim_delay = val def run_test_stimulus(self): - stimulator_class = self.stimulator_cls() + stimulator_class = self.stimulator_cls(soundname=self.detection_sound) stimulator_class.test_stimulus() del stimulator_class diff --git a/portiloop/src/stimulation.py b/portiloop/src/stimulation.py index 5f21b91..6f24774 100644 --- a/portiloop/src/stimulation.py +++ b/portiloop/src/stimulation.py @@ -44,14 +44,17 @@ def test_stimulus(self): # Example implementation for sleep spindles class SleepSpindleRealTimeStimulator(Stimulator): - def __init__(self, lsl_streamer=Dummy(), stimulation_delay=0.0, inter_stim_delay=0.0): + def __init__(self, soundname=None, lsl_streamer=Dummy(), stimulation_delay=0.0, inter_stim_delay=0.0): """ params: stimulation_delay (float): simple delay between a detection and a stimulation inter_stim_delay (float): time to wait between a stimulation and the next detection """ - self._sound = Path(__file__).parent.parent / 'sounds' / 'stimul_15ms.wav' - print(f"DEBUG:{self._sound}") + if soundname is None: + self.soundname = 'stimulus.wav' # CHANGE HERE TO THE SOUND THAT YOU WANT. ONLY ADD THE FILE NAME, NOT THE ENTIRE PATH + else: + self.soundname = soundname + self._sound = Path(__file__).parent.parent / 'sounds' / self.soundname self._thread = None self._lock = Lock() self.last_detected_ts = time.time() @@ -62,8 +65,10 @@ def __init__(self, lsl_streamer=Dummy(), stimulation_delay=0.0, inter_stim_delay # Initialize Alsa stuff # Open WAV file and set PCM device with wave.open(str(self._sound), 'rb') as f: - device = 'default' - + device = 'softvol' + + self.duration = f.getnframes() / float(f.getframerate()) + format = None # 8bit is unsigned in wav files @@ -84,8 +89,8 @@ def __init__(self, lsl_streamer=Dummy(), stimulation_delay=0.0, inter_stim_delay try: self.pcm = alsaaudio.PCM(channels=f.getnchannels(), rate=f.getframerate(), format=format, periodsize=self.periodsize, device=device) except alsaaudio.ALSAAudioError as e: - print("WARNING: Could not open ALSA device as it is already playing a sound. To test stimulation, stop recording and try again.") self.pcm = Dummy() + raise e # Store data in list to avoid reopening the file self.wav_list = [] @@ -94,14 +99,21 @@ def __init__(self, lsl_streamer=Dummy(), stimulation_delay=0.0, inter_stim_delay if data: self.wav_list.append(data) else: - break + break + +# print(f"DEBUG: Stimulator will play sound {self.soundname}, duration: {self.duration:.3f} seconds") + def play_sound(self): ''' Open the wav file and play a sound ''' + self.end = time.time() for data in self.wav_list: self.pcm.write(data) + + # Added this to make sure the thread does not stop before the sound is done playing + time.sleep(self.duration) def stimulate(self, detection_signal): for sig in detection_signal: @@ -139,16 +151,20 @@ def _t_sound(self): self._thread = None def test_stimulus(self): + start = time.time() with self._lock: if self._thread is None: self._thread = Thread(target=self._t_sound, daemon=True) self._thread.start() + +# print(f"DEBUG: Stimulation delay: {((self.end - start) * 1000):.2f}ms") def add_delayer(self, delayer): self.delayer = delayer self.delayer.stimulate = lambda: self.send_stimulation("DELAY_STIM", True) - def close(self): + def __del__(self): +# print("DEBUG: releasing PCM") del self.pcm diff --git a/portiloop/src/utils.py b/portiloop/src/utils.py index 522ea3d..94ed0c4 100644 --- a/portiloop/src/utils.py +++ b/portiloop/src/utils.py @@ -128,13 +128,15 @@ def close_recording_file(self): physical_max=phys_max, physical_min=phys_min,)) self.filename = str(self.filename) - print(f"Saving to {self.filename}") + print(f"Saving to {self.filename} ...") with warnings.catch_warnings(): warnings.simplefilter("ignore") highlevel.write_edf(str(self.filename), data, signal_headers) os.remove(self.csv_filename) + + print("Done.") def add_recording_data(self, data): self.writing_buffer += data