main Module

Main module of the Audiometer application that combines the model and view of the application.

AudioPlayer

Source code in app\audio_player.py
class AudioPlayer:

    def __init__(self):
        """Creates an audio player that can play sine beeps at various frequencies, volumes and with various durations.
        Automatically detects current samplerate of selected sound device.
        """
        self.fs = self.get_device_samplerate()
        self.beep_duration = 10
        self.volume = 0
        self.frequency = 440
        self.stream = None
        self.is_playing = False

    def generate_tone(self)->np.array:
        """Generates a sine tone with current audio player settings.

        Returns:
            array: sine wave as numpy array
        """
        t = np.linspace(start=0, 
                        stop=self.beep_duration, 
                        num=int(self.fs * self.beep_duration), 
                        endpoint=False)
        tone = np.sin(2 * np.pi * self.frequency * t) * self.volume

        # Create fade-out envelope
        fade_duration = 0.003  # 3 ms fade-out
        fade_samples = int(self.fs * fade_duration)
        fade_out = np.linspace(1, 0, fade_samples)
        envelope = np.ones_like(tone)
        envelope[-fade_samples:] = fade_out

        # Apply the envelope to the tone
        tone = tone * envelope

        return tone

    def play_beep(self, frequency:int, volume:float, duration:int, channel:str='lr'):
        """Sets the frequency, volume and beep duration of the audio player and then plays a beep with those parameters.

        Args:
            frequency (int): frequency in Hz
            volume (float): volume multiplier (between 0 and 1)
            duration (int): duration of the beep in seconds
            channel (string): 'l', 'r' or 'lr' for only left, only right or both channels respectively
        """
        self.frequency = frequency
        self.volume = volume
        self.beep_duration = duration
        tone = self.generate_tone()
        if channel == 'l':
            sd.play(np.array([tone, np.zeros(len(tone))]).T, self.fs)
        elif channel == 'r':
            sd.play(np.array([np.zeros(len(tone)), tone]).T, self.fs)
        else:
            sd.play(tone, self.fs)

    def stop(self):
        """Stops the current playback.
        """
        sd.stop()

    def int_or_str(self, text: str)->int:
        """Helper function for argument parsing.
        """
        try:
            return int(text)
        except ValueError:
            return text

    def get_device_samplerate(self):
        """Gets current samplerate from the selected audio output device.

        Returns:
            float: samplerate of current sound device
        """
        parser = argparse.ArgumentParser(add_help=False)
        parser.add_argument(
            '-l', '--list-devices', action='store_true',
            help='show list of audio devices and exit')
        args, remaining = parser.parse_known_args()
        if args.list_devices:
            print(sd.query_devices())
            parser.exit(0)
        parser = argparse.ArgumentParser(
            description=__doc__,
            formatter_class=argparse.RawDescriptionHelpFormatter,
            parents=[parser])
        parser.add_argument(
            'frequency', nargs='?', metavar='FREQUENCY', type=float, default=500,
            help='frequency in Hz (default: %(default)s)')
        parser.add_argument(
            '-d', '--device', type=self.int_or_str,
            help='output device (numeric ID or substring)')
        parser.add_argument(
            '-a', '--amplitude', type=float, default=0.2,
            help='amplitude (default: %(default)s)')
        args = parser.parse_args(remaining)
        return sd.query_devices(args.device, 'output')['default_samplerate']

__init__

__init__()

Creates an audio player that can play sine beeps at various frequencies, volumes and with various durations. Automatically detects current samplerate of selected sound device.

Source code in app\audio_player.py
def __init__(self):
    """Creates an audio player that can play sine beeps at various frequencies, volumes and with various durations.
    Automatically detects current samplerate of selected sound device.
    """
    self.fs = self.get_device_samplerate()
    self.beep_duration = 10
    self.volume = 0
    self.frequency = 440
    self.stream = None
    self.is_playing = False

generate_tone

generate_tone()

Generates a sine tone with current audio player settings.

Returns:
  • array( array ) –

    sine wave as numpy array

Source code in app\audio_player.py
def generate_tone(self)->np.array:
    """Generates a sine tone with current audio player settings.

    Returns:
        array: sine wave as numpy array
    """
    t = np.linspace(start=0, 
                    stop=self.beep_duration, 
                    num=int(self.fs * self.beep_duration), 
                    endpoint=False)
    tone = np.sin(2 * np.pi * self.frequency * t) * self.volume

    # Create fade-out envelope
    fade_duration = 0.003  # 3 ms fade-out
    fade_samples = int(self.fs * fade_duration)
    fade_out = np.linspace(1, 0, fade_samples)
    envelope = np.ones_like(tone)
    envelope[-fade_samples:] = fade_out

    # Apply the envelope to the tone
    tone = tone * envelope

    return tone

get_device_samplerate

get_device_samplerate()

Gets current samplerate from the selected audio output device.

Returns:
  • float

    samplerate of current sound device

Source code in app\audio_player.py
def get_device_samplerate(self):
    """Gets current samplerate from the selected audio output device.

    Returns:
        float: samplerate of current sound device
    """
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        '-l', '--list-devices', action='store_true',
        help='show list of audio devices and exit')
    args, remaining = parser.parse_known_args()
    if args.list_devices:
        print(sd.query_devices())
        parser.exit(0)
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        parents=[parser])
    parser.add_argument(
        'frequency', nargs='?', metavar='FREQUENCY', type=float, default=500,
        help='frequency in Hz (default: %(default)s)')
    parser.add_argument(
        '-d', '--device', type=self.int_or_str,
        help='output device (numeric ID or substring)')
    parser.add_argument(
        '-a', '--amplitude', type=float, default=0.2,
        help='amplitude (default: %(default)s)')
    args = parser.parse_args(remaining)
    return sd.query_devices(args.device, 'output')['default_samplerate']

int_or_str

int_or_str(text)

Helper function for argument parsing.

Source code in app\audio_player.py
def int_or_str(self, text: str)->int:
    """Helper function for argument parsing.
    """
    try:
        return int(text)
    except ValueError:
        return text

play_beep

play_beep(frequency, volume, duration, channel='lr')

Sets the frequency, volume and beep duration of the audio player and then plays a beep with those parameters.

Parameters:
  • frequency (int) –

    frequency in Hz

  • volume (float) –

    volume multiplier (between 0 and 1)

  • duration (int) –

    duration of the beep in seconds

  • channel (string, default: 'lr' ) –

    'l', 'r' or 'lr' for only left, only right or both channels respectively

Source code in app\audio_player.py
def play_beep(self, frequency:int, volume:float, duration:int, channel:str='lr'):
    """Sets the frequency, volume and beep duration of the audio player and then plays a beep with those parameters.

    Args:
        frequency (int): frequency in Hz
        volume (float): volume multiplier (between 0 and 1)
        duration (int): duration of the beep in seconds
        channel (string): 'l', 'r' or 'lr' for only left, only right or both channels respectively
    """
    self.frequency = frequency
    self.volume = volume
    self.beep_duration = duration
    tone = self.generate_tone()
    if channel == 'l':
        sd.play(np.array([tone, np.zeros(len(tone))]).T, self.fs)
    elif channel == 'r':
        sd.play(np.array([np.zeros(len(tone)), tone]).T, self.fs)
    else:
        sd.play(tone, self.fs)

stop

stop()

Stops the current playback.

Source code in app\audio_player.py
def stop(self):
    """Stops the current playback.
    """
    sd.stop()

Calibration

Bases: Procedure

Source code in app\model.py
class Calibration(Procedure):

    def __init__(self, startlevel:int=60, signal_length:int=10, headphone_name:str="Sennheiser_HDA200", **additional_data):
        """Process for calibrating system.

        Args:
            startlevel (int, optional): starting level of procedure in dB HL. Defaults to 60.
            signal_length (int, optional): length of played signals in seconds. Defaults to 10.
            headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
            **additional_data: additional key/value pairs to be stored in CSV file after procedure is done
        """
        super().__init__(startlevel, signal_length, headphone_name=headphone_name, calibrate=False)      
        self.tempfile = self.create_temp_csv(id="", **additional_data) # create a temporary file to store level at frequencies
        self.generator = self.get_next_freq()
        self.dbspl = self.level + self.retspl[self.frequency]

    def get_next_freq(self):
        """Generator that goes through all frequencies twice.
        Changes self.side to 'r' after going through all frequencies the first time.

        Yields:
            int: frequency
        """
        self.side = 'l'
        frequency = 125
        while frequency <= 8000:
            yield frequency
            frequency *= 2

        frequency = 125
        self.side = 'r'
        while frequency <= 8000:
            yield frequency
            frequency *= 2

    def play_one_freq(self)->tuple:
        """Get the next frequency and play it.

        Returns:
            bool: False if no more frequencies left
            int: current frequency
            float: expected SPL value in dB
        """
        self.ap.stop()

        try:
            self.frequency = next(self.generator)
        except:
            return False, self.frequency, self.dbspl

        self.dbspl = self.level + self.retspl[self.frequency]
        print(f"Side {self.side} at {self.frequency} Hz: The SPL value should be {self.dbspl} dB.")
        self.ap.play_beep(self.frequency, self.dbhl_to_volume(self.level), self.signal_length, self.side)
        if self.frequency >= 8000 and self.side == 'r':
            return False, self.frequency, self.dbspl
        else:
            return True, self.frequency, self.dbspl

    def repeat_freq(self):
        """Repeats the last played frequency.
        """
        self.ap.stop()
        print(f" Repeating side {self.side} at {self.frequency} Hz: The SPL value should be {self.dbspl} dB.")
        self.ap.play_beep(self.frequency, self.dbhl_to_volume(self.level), self.signal_length, self.side)

    def set_calibration_value(self, measured_value:float):
        """Rights the given calibration value into temporary CSV file

        Args:
            measured_value (float): measured SPL value in dB
        """
        value = measured_value - self.dbspl
        self.add_to_temp_csv(str(value), str(self.frequency), self.side, self.tempfile)

    def finish_calibration(self):
        """Makes a permanent CSV file from the temporary file that overwrites calibration.csv.

        Args:
            temp_filename (str): name of temporary CSV file
        """
        self.ap.stop()
        # read temp file
        with open(self.tempfile, mode='r', newline='') as temp_file:
            dict_reader = csv.DictReader(temp_file)
            rows = list(dict_reader)

        filename = "calibration.csv"

        with open(filename, mode='w', newline='') as final_file:
            dict_writer = csv.DictWriter(final_file, fieldnames=self.freq_bands)
            dict_writer.writeheader()
            dict_writer.writerows(rows)

        print("Datei gespeicher als " + filename)

    def stop_playing(self):
        """Stops the audio player.
        """
        self.ap.stop()

__init__

__init__(startlevel=60, signal_length=10, headphone_name='Sennheiser_HDA200', **additional_data)

Process for calibrating system.

Parameters:
  • startlevel (int, default: 60 ) –

    starting level of procedure in dB HL. Defaults to 60.

  • signal_length (int, default: 10 ) –

    length of played signals in seconds. Defaults to 10.

  • headphone_name (str, default: 'Sennheiser_HDA200' ) –

    Name of headphone model being used. Defaults to Sennheiser_HDA200.

  • **additional_data

    additional key/value pairs to be stored in CSV file after procedure is done

Source code in app\model.py
def __init__(self, startlevel:int=60, signal_length:int=10, headphone_name:str="Sennheiser_HDA200", **additional_data):
    """Process for calibrating system.

    Args:
        startlevel (int, optional): starting level of procedure in dB HL. Defaults to 60.
        signal_length (int, optional): length of played signals in seconds. Defaults to 10.
        headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
        **additional_data: additional key/value pairs to be stored in CSV file after procedure is done
    """
    super().__init__(startlevel, signal_length, headphone_name=headphone_name, calibrate=False)      
    self.tempfile = self.create_temp_csv(id="", **additional_data) # create a temporary file to store level at frequencies
    self.generator = self.get_next_freq()
    self.dbspl = self.level + self.retspl[self.frequency]

finish_calibration

finish_calibration()

Makes a permanent CSV file from the temporary file that overwrites calibration.csv.

Parameters:
  • temp_filename (str) –

    name of temporary CSV file

Source code in app\model.py
def finish_calibration(self):
    """Makes a permanent CSV file from the temporary file that overwrites calibration.csv.

    Args:
        temp_filename (str): name of temporary CSV file
    """
    self.ap.stop()
    # read temp file
    with open(self.tempfile, mode='r', newline='') as temp_file:
        dict_reader = csv.DictReader(temp_file)
        rows = list(dict_reader)

    filename = "calibration.csv"

    with open(filename, mode='w', newline='') as final_file:
        dict_writer = csv.DictWriter(final_file, fieldnames=self.freq_bands)
        dict_writer.writeheader()
        dict_writer.writerows(rows)

    print("Datei gespeicher als " + filename)

get_next_freq

get_next_freq()

Generator that goes through all frequencies twice. Changes self.side to 'r' after going through all frequencies the first time.

Yields:
  • int

    frequency

Source code in app\model.py
def get_next_freq(self):
    """Generator that goes through all frequencies twice.
    Changes self.side to 'r' after going through all frequencies the first time.

    Yields:
        int: frequency
    """
    self.side = 'l'
    frequency = 125
    while frequency <= 8000:
        yield frequency
        frequency *= 2

    frequency = 125
    self.side = 'r'
    while frequency <= 8000:
        yield frequency
        frequency *= 2

play_one_freq

play_one_freq()

Get the next frequency and play it.

Returns:
  • bool( tuple ) –

    False if no more frequencies left

  • int( tuple ) –

    current frequency

  • float( tuple ) –

    expected SPL value in dB

Source code in app\model.py
def play_one_freq(self)->tuple:
    """Get the next frequency and play it.

    Returns:
        bool: False if no more frequencies left
        int: current frequency
        float: expected SPL value in dB
    """
    self.ap.stop()

    try:
        self.frequency = next(self.generator)
    except:
        return False, self.frequency, self.dbspl

    self.dbspl = self.level + self.retspl[self.frequency]
    print(f"Side {self.side} at {self.frequency} Hz: The SPL value should be {self.dbspl} dB.")
    self.ap.play_beep(self.frequency, self.dbhl_to_volume(self.level), self.signal_length, self.side)
    if self.frequency >= 8000 and self.side == 'r':
        return False, self.frequency, self.dbspl
    else:
        return True, self.frequency, self.dbspl

repeat_freq

repeat_freq()

Repeats the last played frequency.

Source code in app\model.py
def repeat_freq(self):
    """Repeats the last played frequency.
    """
    self.ap.stop()
    print(f" Repeating side {self.side} at {self.frequency} Hz: The SPL value should be {self.dbspl} dB.")
    self.ap.play_beep(self.frequency, self.dbhl_to_volume(self.level), self.signal_length, self.side)

set_calibration_value

set_calibration_value(measured_value)

Rights the given calibration value into temporary CSV file

Parameters:
  • measured_value (float) –

    measured SPL value in dB

Source code in app\model.py
def set_calibration_value(self, measured_value:float):
    """Rights the given calibration value into temporary CSV file

    Args:
        measured_value (float): measured SPL value in dB
    """
    value = measured_value - self.dbspl
    self.add_to_temp_csv(str(value), str(self.frequency), self.side, self.tempfile)

stop_playing

stop_playing()

Stops the audio player.

Source code in app\model.py
def stop_playing(self):
    """Stops the audio player.
    """
    self.ap.stop()

Controller

Source code in app\main.py
class Controller():

    def __init__(self):
        """Controller class (MVC architecture) that combines model and view of the Audiometer.
        """
        self.selected_program = ""
        program_functions = {"Klassisches Audiogramm" : self.start_standard_procedure,
                             "Kurzes Screening" : self.start_screen_procedure,
                             "Kalibrierung" : self.start_calibration}

        self.calibration_funcs = [self.start_calibration, self.calibration_next_freq, self.calibration_repeat_freq, self.stop_sound, self.calibration_set_level]
        self.view = setup_ui(self.start_familiarization, 
                             program_functions, self.calibration_funcs, self.get_progress)

        # helper variable for calibration
        self.button_changed = False

    def run_app(self):
        """Starts the app by running the tkinter mainloop of the view.
        """
        self.view.mainloop()

    def start_familiarization(self, id:str="", headphone:str="Sennheiser_HDA200", calibrate:bool=True, **additional_data)->bool:
        """Creates a Familiarization object and uses it to start the familiarization process.

        Args:
            id (str, optional): ID of test subject. Defaults to "".
            headphone (str, optional): Name of headphone model being used. Defaults to "Sennheiser_HDA200".
            calibrate (bool, optional): Whether to use calibration file. Defaults to True.
            **additional_data: additional key/value pairs to be stored in CSV file after procedure is done

        Returns:
            bool: Whether familiarization was successful
        """
        self.selected_program = "familiarization"
        self.familiarization = Familiarization(id=id, headphone_name=headphone, calibrate=calibrate, **additional_data)
        return self.familiarization.familiarize()

    def start_standard_procedure(self, binaural:bool=False, headphone:str="Sennheiser_HDA200", calibrate:bool=True, **additional_data):
        """Creates a StandardProcedure object and uses it to start the standard procedure.

        Args:
            binaural (bool, optional): Whether to test both ears at the same time. Defaults to False.
            headphone (str, optional): Name of headphone model being used. Defaults to "Sennheiser_HDA200".
            calibrate (bool, optional): Whether to use calibration file. Defaults to True.
            **additional_data: additional key/value pairs to be stored in CSV file after procedure is done
        """
        self.selected_program = "standard"
        self.standard_procedure = StandardProcedure(self.familiarization.get_temp_csv_filename(), headphone_name=headphone, calibrate=calibrate, **additional_data)
        self.standard_procedure.standard_test(binaural)

    def start_screen_procedure(self, binaural:bool=False, headphone:str="Sennheiser_HDA200", calibrate:bool=True, **additional_data):
        """Creates a ScreeningProcedure object and uses it to start the screening procedure.

        Args:
            binaural (bool, optional): Whether to test both ears at the same time. Defaults to False.
            headphone (str, optional): Name of headphone model being used. Defaults to "Sennheiser_HDA200".
            calibrate (bool, optional): Whether to use calibration file. Defaults to True.
            **additional_data: additional key/value pairs to be stored in CSV file after procedure is done
        """
        self.selected_program = "screening"
        self.screen_procedure = ScreeningProcedure(self.familiarization.get_temp_csv_filename(), headphone_name=headphone, calibrate=calibrate, **additional_data)
        self.screen_procedure.screen_test(binaural)

    def start_calibration(self, level:int, headphone:str="Sennheiser_HDA200")->tuple:
        """Creates a Calibration object and uses it to start calibration.

        Args:
            level (int): Level of calibration in dB HL.
            headphone (str, optional): Name of headphone model being used. Defaults to "Sennheiser_HDA200".
        """
        self.selected_program = "calibration"
        self.calibration = Calibration(startlevel=level, headphone_name=headphone)
        _, current_freq, current_spl = self.calibration_next_freq()
        return current_freq, current_spl

    def calibration_next_freq(self)->tuple:
        """Goes to next frequency in calibration process and play it.

        Returns:
            bool: Whether there are more frequencies left after this one.
            int: current frequency
            float: expected SPL value in dB
        """
        more_freqs, current_freq, current_spl = self.calibration.play_one_freq()
        if more_freqs:
            return True, current_freq, current_spl
        elif self.button_changed == False:
            self.button_changed = True
            return False, current_freq, current_spl
        else:
            self.calibration.finish_calibration()
            return False, current_freq, current_spl

    def calibration_repeat_freq(self):
        """Repeats the current frequency during calibration process.
        """
        self.calibration.repeat_freq()

    def calibration_set_level(self, spl:float):
        """Sets the measured level in dB during calbration process.

        Args:
            spl (float): Sound pressure level that was measured in dB
        """
        self.calibration.set_calibration_value(spl)

    def stop_sound(self):
        """Stops the sound during calibration process.
        """
        self.calibration.stop_playing()

    def get_progress(self)->float:
        """Gets current progress in curent procedure for progress bar.

        Returns:
            float: progress value between 0.0 and 1.0
        """
        if self.selected_program == "familiarization":
            return self.familiarization.get_progress()
        elif self.selected_program == "standard":
            return self.standard_procedure.get_progress()
        elif self.selected_program == "screening":
            return self.screen_procedure.get_progress()
        elif self.selected_program == "calibration":
            return 0.0
        else:
            return 0.0

__init__

__init__()

Controller class (MVC architecture) that combines model and view of the Audiometer.

Source code in app\main.py
def __init__(self):
    """Controller class (MVC architecture) that combines model and view of the Audiometer.
    """
    self.selected_program = ""
    program_functions = {"Klassisches Audiogramm" : self.start_standard_procedure,
                         "Kurzes Screening" : self.start_screen_procedure,
                         "Kalibrierung" : self.start_calibration}

    self.calibration_funcs = [self.start_calibration, self.calibration_next_freq, self.calibration_repeat_freq, self.stop_sound, self.calibration_set_level]
    self.view = setup_ui(self.start_familiarization, 
                         program_functions, self.calibration_funcs, self.get_progress)

    # helper variable for calibration
    self.button_changed = False

calibration_next_freq

calibration_next_freq()

Goes to next frequency in calibration process and play it.

Returns:
  • bool( tuple ) –

    Whether there are more frequencies left after this one.

  • int( tuple ) –

    current frequency

  • float( tuple ) –

    expected SPL value in dB

Source code in app\main.py
def calibration_next_freq(self)->tuple:
    """Goes to next frequency in calibration process and play it.

    Returns:
        bool: Whether there are more frequencies left after this one.
        int: current frequency
        float: expected SPL value in dB
    """
    more_freqs, current_freq, current_spl = self.calibration.play_one_freq()
    if more_freqs:
        return True, current_freq, current_spl
    elif self.button_changed == False:
        self.button_changed = True
        return False, current_freq, current_spl
    else:
        self.calibration.finish_calibration()
        return False, current_freq, current_spl

calibration_repeat_freq

calibration_repeat_freq()

Repeats the current frequency during calibration process.

Source code in app\main.py
def calibration_repeat_freq(self):
    """Repeats the current frequency during calibration process.
    """
    self.calibration.repeat_freq()

calibration_set_level

calibration_set_level(spl)

Sets the measured level in dB during calbration process.

Parameters:
  • spl (float) –

    Sound pressure level that was measured in dB

Source code in app\main.py
def calibration_set_level(self, spl:float):
    """Sets the measured level in dB during calbration process.

    Args:
        spl (float): Sound pressure level that was measured in dB
    """
    self.calibration.set_calibration_value(spl)

get_progress

get_progress()

Gets current progress in curent procedure for progress bar.

Returns:
  • float( float ) –

    progress value between 0.0 and 1.0

Source code in app\main.py
def get_progress(self)->float:
    """Gets current progress in curent procedure for progress bar.

    Returns:
        float: progress value between 0.0 and 1.0
    """
    if self.selected_program == "familiarization":
        return self.familiarization.get_progress()
    elif self.selected_program == "standard":
        return self.standard_procedure.get_progress()
    elif self.selected_program == "screening":
        return self.screen_procedure.get_progress()
    elif self.selected_program == "calibration":
        return 0.0
    else:
        return 0.0

run_app

run_app()

Starts the app by running the tkinter mainloop of the view.

Source code in app\main.py
def run_app(self):
    """Starts the app by running the tkinter mainloop of the view.
    """
    self.view.mainloop()

start_calibration

start_calibration(level, headphone='Sennheiser_HDA200')

Creates a Calibration object and uses it to start calibration.

Parameters:
  • level (int) –

    Level of calibration in dB HL.

  • headphone (str, default: 'Sennheiser_HDA200' ) –

    Name of headphone model being used. Defaults to "Sennheiser_HDA200".

Source code in app\main.py
def start_calibration(self, level:int, headphone:str="Sennheiser_HDA200")->tuple:
    """Creates a Calibration object and uses it to start calibration.

    Args:
        level (int): Level of calibration in dB HL.
        headphone (str, optional): Name of headphone model being used. Defaults to "Sennheiser_HDA200".
    """
    self.selected_program = "calibration"
    self.calibration = Calibration(startlevel=level, headphone_name=headphone)
    _, current_freq, current_spl = self.calibration_next_freq()
    return current_freq, current_spl

start_familiarization

start_familiarization(id='', headphone='Sennheiser_HDA200', calibrate=True, **additional_data)

Creates a Familiarization object and uses it to start the familiarization process.

Parameters:
  • id (str, default: '' ) –

    ID of test subject. Defaults to "".

  • headphone (str, default: 'Sennheiser_HDA200' ) –

    Name of headphone model being used. Defaults to "Sennheiser_HDA200".

  • calibrate (bool, default: True ) –

    Whether to use calibration file. Defaults to True.

  • **additional_data

    additional key/value pairs to be stored in CSV file after procedure is done

Returns:
  • bool( bool ) –

    Whether familiarization was successful

Source code in app\main.py
def start_familiarization(self, id:str="", headphone:str="Sennheiser_HDA200", calibrate:bool=True, **additional_data)->bool:
    """Creates a Familiarization object and uses it to start the familiarization process.

    Args:
        id (str, optional): ID of test subject. Defaults to "".
        headphone (str, optional): Name of headphone model being used. Defaults to "Sennheiser_HDA200".
        calibrate (bool, optional): Whether to use calibration file. Defaults to True.
        **additional_data: additional key/value pairs to be stored in CSV file after procedure is done

    Returns:
        bool: Whether familiarization was successful
    """
    self.selected_program = "familiarization"
    self.familiarization = Familiarization(id=id, headphone_name=headphone, calibrate=calibrate, **additional_data)
    return self.familiarization.familiarize()

start_screen_procedure

start_screen_procedure(binaural=False, headphone='Sennheiser_HDA200', calibrate=True, **additional_data)

Creates a ScreeningProcedure object and uses it to start the screening procedure.

Parameters:
  • binaural (bool, default: False ) –

    Whether to test both ears at the same time. Defaults to False.

  • headphone (str, default: 'Sennheiser_HDA200' ) –

    Name of headphone model being used. Defaults to "Sennheiser_HDA200".

  • calibrate (bool, default: True ) –

    Whether to use calibration file. Defaults to True.

  • **additional_data

    additional key/value pairs to be stored in CSV file after procedure is done

Source code in app\main.py
def start_screen_procedure(self, binaural:bool=False, headphone:str="Sennheiser_HDA200", calibrate:bool=True, **additional_data):
    """Creates a ScreeningProcedure object and uses it to start the screening procedure.

    Args:
        binaural (bool, optional): Whether to test both ears at the same time. Defaults to False.
        headphone (str, optional): Name of headphone model being used. Defaults to "Sennheiser_HDA200".
        calibrate (bool, optional): Whether to use calibration file. Defaults to True.
        **additional_data: additional key/value pairs to be stored in CSV file after procedure is done
    """
    self.selected_program = "screening"
    self.screen_procedure = ScreeningProcedure(self.familiarization.get_temp_csv_filename(), headphone_name=headphone, calibrate=calibrate, **additional_data)
    self.screen_procedure.screen_test(binaural)

start_standard_procedure

start_standard_procedure(binaural=False, headphone='Sennheiser_HDA200', calibrate=True, **additional_data)

Creates a StandardProcedure object and uses it to start the standard procedure.

Parameters:
  • binaural (bool, default: False ) –

    Whether to test both ears at the same time. Defaults to False.

  • headphone (str, default: 'Sennheiser_HDA200' ) –

    Name of headphone model being used. Defaults to "Sennheiser_HDA200".

  • calibrate (bool, default: True ) –

    Whether to use calibration file. Defaults to True.

  • **additional_data

    additional key/value pairs to be stored in CSV file after procedure is done

Source code in app\main.py
def start_standard_procedure(self, binaural:bool=False, headphone:str="Sennheiser_HDA200", calibrate:bool=True, **additional_data):
    """Creates a StandardProcedure object and uses it to start the standard procedure.

    Args:
        binaural (bool, optional): Whether to test both ears at the same time. Defaults to False.
        headphone (str, optional): Name of headphone model being used. Defaults to "Sennheiser_HDA200".
        calibrate (bool, optional): Whether to use calibration file. Defaults to True.
        **additional_data: additional key/value pairs to be stored in CSV file after procedure is done
    """
    self.selected_program = "standard"
    self.standard_procedure = StandardProcedure(self.familiarization.get_temp_csv_filename(), headphone_name=headphone, calibrate=calibrate, **additional_data)
    self.standard_procedure.standard_test(binaural)

stop_sound

stop_sound()

Stops the sound during calibration process.

Source code in app\main.py
def stop_sound(self):
    """Stops the sound during calibration process.
    """
    self.calibration.stop_playing()

Familiarization

Bases: Procedure

Source code in app\model.py
class Familiarization(Procedure):

    def __init__(self, startlevel:int=40, signal_length:int=1, headphone_name:str="Sennheiser_HDA200",calibrate:bool=True, id:str="", **additional_data):
        """Creates the Familiarization process.

        Args:
            startlevel (int, optional): starting level of procedure in dB HL. Defaults to 40.
            signal_length (int, optional): length of played signals in seconds. Defaults to 1.
            headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
            calibrate (bool, optional): Use calibration file. Defaults to True.
            id (str, optional): id to be stored, that will later be used for naming exported CSV file
            **additional_data: additional key/value pairs to be stored in CSV file after procedure is done
        """
        super().__init__(startlevel, signal_length, headphone_name=headphone_name, calibrate=calibrate)      
        self.fails = 0 # number of times familiarization failed
        self.tempfile = self.create_temp_csv(id=id, **additional_data) # create a temporary file to store level at frequencies

    def get_temp_csv_filename(self)->str:
        """Gets name of temp CSV file.

        Returns:
            str: name of CSV file
        """
        return self.tempfile

    def familiarize(self)->bool:
        """Main function.

        Returns:
            bool: familiarization successful
        """
        self.progress = 0.01
        while True:
            self.tone_heard = True

            # first loop (always -20dBHL)
            while self.tone_heard:
                self.play_tone()

                if self.jump_to_end == True:
                    for f in self.freq_bands:
                        self.add_to_temp_csv(20, f, 'lr', self.get_temp_csv_filename())
                    return True

                if self.tone_heard:
                    self.level -= 20

                    if self.progress < 1/5:
                        self.progress = 1/5

                else:
                    self.level += 10

            if self.progress < 1/3:
                self.progress = 1/3        

            # second loop (always +10dBHL)
            while not self.tone_heard:
                self.play_tone()
                if not self.tone_heard:
                    self.level += 10

            self.progress = 2/3

            # replay tone with same level
            self.play_tone()

            if not self.tone_heard:
                self.fails += 1
                if self.fails >= 2:
                    self.progress = 1
                    print("Familiarization unsuccessful. Please read rules and start again.")
                    return False
                else:
                    self.level = self.startlevel

            else:
                print("Familiarization successful!")
                self.progress = 1
                self.add_to_temp_csv(self.level, '1000', 'l', self.tempfile)
                return True

__init__

__init__(startlevel=40, signal_length=1, headphone_name='Sennheiser_HDA200', calibrate=True, id='', **additional_data)

Creates the Familiarization process.

Parameters:
  • startlevel (int, default: 40 ) –

    starting level of procedure in dB HL. Defaults to 40.

  • signal_length (int, default: 1 ) –

    length of played signals in seconds. Defaults to 1.

  • headphone_name (str, default: 'Sennheiser_HDA200' ) –

    Name of headphone model being used. Defaults to Sennheiser_HDA200.

  • calibrate (bool, default: True ) –

    Use calibration file. Defaults to True.

  • id (str, default: '' ) –

    id to be stored, that will later be used for naming exported CSV file

  • **additional_data

    additional key/value pairs to be stored in CSV file after procedure is done

Source code in app\model.py
def __init__(self, startlevel:int=40, signal_length:int=1, headphone_name:str="Sennheiser_HDA200",calibrate:bool=True, id:str="", **additional_data):
    """Creates the Familiarization process.

    Args:
        startlevel (int, optional): starting level of procedure in dB HL. Defaults to 40.
        signal_length (int, optional): length of played signals in seconds. Defaults to 1.
        headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
        calibrate (bool, optional): Use calibration file. Defaults to True.
        id (str, optional): id to be stored, that will later be used for naming exported CSV file
        **additional_data: additional key/value pairs to be stored in CSV file after procedure is done
    """
    super().__init__(startlevel, signal_length, headphone_name=headphone_name, calibrate=calibrate)      
    self.fails = 0 # number of times familiarization failed
    self.tempfile = self.create_temp_csv(id=id, **additional_data) # create a temporary file to store level at frequencies

familiarize

familiarize()

Main function.

Returns:
  • bool( bool ) –

    familiarization successful

Source code in app\model.py
def familiarize(self)->bool:
    """Main function.

    Returns:
        bool: familiarization successful
    """
    self.progress = 0.01
    while True:
        self.tone_heard = True

        # first loop (always -20dBHL)
        while self.tone_heard:
            self.play_tone()

            if self.jump_to_end == True:
                for f in self.freq_bands:
                    self.add_to_temp_csv(20, f, 'lr', self.get_temp_csv_filename())
                return True

            if self.tone_heard:
                self.level -= 20

                if self.progress < 1/5:
                    self.progress = 1/5

            else:
                self.level += 10

        if self.progress < 1/3:
            self.progress = 1/3        

        # second loop (always +10dBHL)
        while not self.tone_heard:
            self.play_tone()
            if not self.tone_heard:
                self.level += 10

        self.progress = 2/3

        # replay tone with same level
        self.play_tone()

        if not self.tone_heard:
            self.fails += 1
            if self.fails >= 2:
                self.progress = 1
                print("Familiarization unsuccessful. Please read rules and start again.")
                return False
            else:
                self.level = self.startlevel

        else:
            print("Familiarization successful!")
            self.progress = 1
            self.add_to_temp_csv(self.level, '1000', 'l', self.tempfile)
            return True

get_temp_csv_filename

get_temp_csv_filename()

Gets name of temp CSV file.

Returns:
  • str( str ) –

    name of CSV file

Source code in app\model.py
def get_temp_csv_filename(self)->str:
    """Gets name of temp CSV file.

    Returns:
        str: name of CSV file
    """
    return self.tempfile

Procedure

Source code in app\model.py
class Procedure:

    def __init__(self, startlevel:float, signal_length:float, headphone_name:str="Sennheiser_HDA200", calibrate:bool=True):
        """Creates the parent class for the familiarization, the main procedure, and the screening.

        Args:
            startlevel (float): starting level of procedure in dB HL
            signal_length (float): length of played signals in seconds
            headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
            calibrate (bool, optional): Use calibration file. Defaults to True.
        """
        self.ap = AudioPlayer()
        self.startlevel = startlevel
        self.level = startlevel
        self.signal_length = signal_length
        self.frequency = 1000
        self.zero_dbhl = 0.000005 # zero_dbhl in absolute numbers. This is a rough guess for uncalibrated systems and will be adjusted through the calibration file
        self.tone_heard = False
        self.freq_bands = ['125', '250', '500', '1000', '2000', '4000', '8000']
        self.freq_levels = {125: 20, 250: 20, 500: 20, 1000: 20, 2000: 20, 4000: 20, 8000: 20} # screening levels
        self.side = 'l'
        self.test_mode = False # set True to be able to skip procedures with right arrow key
        self.jump_to_end = False
        self.use_calibration = calibrate
        self.progress = 0 # value for progressbar
        self.retspl = self.get_retspl_values(headphone_name)
        self.calibration = self.get_calibration_values()
        self.save_path = self.get_save_path()  # Initialize save_path

    def get_retspl_values(self, headphone_name:str):
        """Reads the correct RETSPL values from the retspl.csv file.

        Args:
            headphone_name (str): exact name of headphone as it appears in CSV file

        Returns:
            dict of int:float : RETSPL values for each frequency band from 125 Hz to 8000 Hz
        """
        file_name = 'retspl.csv'

        # Check if the CSV file exists
        if not os.path.isfile(file_name):
            print(f"File '{file_name}' not found.")
            return

        retspl_values = {}

        try:
            with open(file_name, mode='r') as file:
                reader = csv.DictReader(file)
                for row in reader:
                    if row['headphone_model'] == headphone_name:
                        retspl_values[int(row['frequency'])] = float(row['retspl'])
        except Exception as e:
            print(f"Error reading the file: {e}")
            return

        # Check if the headphone model was found
        if not retspl_values:
            print(f"Headphone model '{headphone_name}' not found.")
            return

        print(retspl_values)
        return retspl_values

    def get_calibration_values(self)->dict:
        """Read the correct calibration values from the calibration.csv file.

        Returns:
            dict of int:float : calibration values for each frequency band from 125 Hz to 8000 Hz
        """
        file_name = 'calibration.csv'

        # Check if the CSV file exists
        if not os.path.isfile(file_name):
            print(f"File '{file_name}' not found.")
            return

        try:
            with open(file_name, mode='r') as file:
                reader = csv.DictReader(file)
                calibration_str_values_l = next(reader)
                calibration_str_values_r = next(reader)

                # convert dictionary to int:float and put into extra dictionary for left and right side
                calibration_values = {}
                calibration_values['l'] = {int(k): float(v) for k, v in calibration_str_values_l.items()}
                calibration_values['r'] = {int(k): float(v) for k, v in calibration_str_values_r.items()}

                # if both sides are used, calculate average between both sides
                calibration_values['lr'] = {}
                for k, v in calibration_values['l'].items():
                    calibration_values['lr'][k] = (10 * np.log10((10 ** (v / 10) + 10 ** (calibration_values['r'][k] / 10)) / 2))

        except Exception as e:
            print(f"Error reading the file: {e}")
            return

        print(calibration_values)
        return calibration_values

    def dbhl_to_volume(self, dbhl:float)->float:
        """Calculate dB HL into absolute numbers.

        Args:
            dbhl (float): value in dB HL

        Returns:
            float: value in absolute numbers
        """
        if self.use_calibration:
            # add RETSPL and values from calibration file at that frequency
            dbspl = dbhl + self.retspl[self.frequency] - self.calibration[self.side][self.frequency] 
        else:
            # only add RETSPL
            dbspl = dbhl + self.retspl[self.frequency] 

        return self.zero_dbhl * 10 ** (dbspl / 20) # calculate from dB to absolute numbers using the reference point self.zero_dbhl

    def key_press(self, key:keyboard.Key):
        """Function for pynputto be called on key press

        Args:
            key (keyboard.Key): key that was pressed
        """
        if key == keyboard.Key.space:
            self.tone_heard = True
            print("Tone heard!")
        elif self.test_mode and key == keyboard.Key.right:
            self.jump_to_end = True

    def play_tone(self):
        """Sets tone_heard to False, play beep, then waits 4 s (max) for keypress.
        Sets tone_heard to True if key is pressed.
        Then waits for around 1 s to 2.5 s (randomized).
        """
        self.tone_heard = False
        print(self.frequency, "Hz - playing tone at", self.level, "dBHL.")
        self.ap.play_beep(self.frequency, self.dbhl_to_volume(self.level), self.signal_length, self.side)
        listener = keyboard.Listener(on_press=self.key_press, on_release=None)
        listener.start()
        current_wait_time = 0
        max_wait_time = 4000 # in ms 
        step_size = 50 # in ms

        while current_wait_time < max_wait_time and not self.tone_heard: # wait for keypress
            time.sleep(step_size / 1000)
            current_wait_time += step_size
        listener.stop()
        self.ap.stop()

        if not self.tone_heard:
            print("Tone not heard :(")
        else:
            sleep_time = random.uniform(1, 2.5) # random wait time between 1 and 2.5
            time.sleep(sleep_time) # wait before next tone is played. #TODO test times

    def create_temp_csv(self, id:str="", **additional_data)->str:
        """Creates a temporary CSV file with the relevant frequency bands as a header
        and NaN in the second and third line as starting value for each band.
        (second line: left ear, third line: right ear)
        ID and additional data will be stored in subsequent lines in the format: key, value.

        Args:
            id (str, optional): id to be stored, that will later be used for naming exported csv file
            **additional_data: additional key/value pairs to be stored in CSV file after procedure is done

        Returns:
            str: name of temporary file
        """
        with tfile.NamedTemporaryFile(mode='w+', delete=False, newline='', suffix='.csv') as temp_file:
            # Define the CSV writer
            csv_writer = csv.writer(temp_file)

            # Write header
            csv_writer.writerow(self.freq_bands)

            # Write value NaN for each frequency in second and third row
            csv_writer.writerow(['NaN' for _ in range(len(self.freq_bands))])
            csv_writer.writerow(['NaN' for _ in range(len(self.freq_bands))])

            # Write id and additional data
            if id:
                csv_writer.writerow(["id", id])
            if additional_data:
                for key, value in additional_data.items():
                    csv_writer.writerow([key, value])

            return temp_file.name

    def add_to_temp_csv(self, value:str, frequency:str, side:str, temp_filename:str):
        """Add a value in for a specific frequency to the temporary CSV file

        Args:
            value (str): level in dB HL at specific frequency
            frequency (str): frequency where value should be added
            side (str): specify which ear ('l' or 'r')
            temp_filename (str): name of temporary CSV file
        """
        # Read all rows from the CSV file
        with open(temp_filename, mode='r', newline='') as temp_file:
            dict_reader = csv.DictReader(temp_file)
            rows = list(dict_reader)

        # Update the relevant row based on the side parameter
        if side == 'l':
            rows[0][frequency] = value
        elif side == 'r':
            rows[1][frequency] = value
        else:
            rows[0][frequency] = value
            rows[1][frequency] = value

        # Write all rows back to the CSV file
        with open(temp_filename, mode='w', newline='') as temp_file:
            dict_writer = csv.DictWriter(temp_file, fieldnames=self.freq_bands)
            dict_writer.writeheader()
            dict_writer.writerows(rows)

        print(rows[0], rows[1])

        for row in rows[2:]:
            print(row['125'], row['250'])

    def get_value_from_csv(self, frequency:str, temp_filename:str, side:str='l')->str:
        """Get the value at a specific frequency from the temporary CSV file.

        Args:
            frequency (str): frequency where value is stored
            temp_filename (str): name of temporary CSV file
            side (str, optional): specify which ear ('l' or 'r'). Defaults to 'l'.

        Returns:
            str: dB HL value at specified frequency
        """
        with open(temp_filename, mode='r', newline='') as temp_file:
            dict_reader = csv.DictReader(temp_file)
            freq_dict = next(dict_reader) # left ear
            if side == 'r': # go to next line if right side
                freq_dict = next(dict_reader)    
            return freq_dict[frequency]

    def create_final_csv_and_audiogram(self, temp_filename:str, binaural:bool=False):
        """Creates a permanent CSV file and audiogram from the temporary file.

        Args:
            temp_filename (str): Name of the temporary CSV file.
            binaural (bool): If the test is binaural.
        """
        # Read the temporary file
        with open(temp_filename, mode='r', newline='') as temp_file:
            dict_reader = csv.DictReader(temp_file)
            rows = list(dict_reader)

        # Get date and time
        now = datetime.now()
        date_str = now.strftime("%Y%m%d_%H%M%S")
        try:
            id = rows[2]['250']
        except:
            id = "missingID"

        # Create folder for the subject
        folder_name = os.path.join(self.save_path, f"{id}")
        if not os.path.exists(folder_name):
            os.makedirs(folder_name)

        final_csv_filename = os.path.join(folder_name, f"{id}_audiogramm_{date_str}.csv")

        # Write the permanent CSV file
        with open(final_csv_filename, mode='x', newline='') as final_file:
            dict_writer = csv.DictWriter(final_file, fieldnames=self.freq_bands)
            dict_writer.writeheader()
            dict_writer.writerows(rows)

        freqs = [int(x) for x in self.freq_bands]

        left_levels = [self.parse_dbhl_value(rows[0][freq]) for freq in self.freq_bands]
        right_levels = [self.parse_dbhl_value(rows[1][freq]) for freq in self.freq_bands]

        # Generate the audiogram filename
        audiogram_filename = os.path.join(folder_name, f"{id}_audiogram_{date_str}.png")
        print(left_levels, right_levels)
        create_audiogram(freqs, left_levels, right_levels, binaural=binaural, name=audiogram_filename, freq_levels=self.freq_levels)

    def parse_dbhl_value(self, value:str)->int:
        """Parses the dBHL value from the CSV file.

        Args:
            value (str): the value from the CSV file

        Returns:
            int or None: the parsed value or None if 'NH'
        """
        if value == 'NH':
            return 'NH'
        try:
            return int(value)
        except ValueError:
            return None

    def get_progress(self)->float:
        """Gets the current progress.

        Returns:
            float: progress value between 0.0 and 1.0
        """
        return self.progress

    def get_save_path(self)->str:
        """Gets selected path from settings.csv file for saving files.

        Returns:
            str: save path
        """
        file_name = 'settings.csv'

        # Check if the CSV file exists
        if not os.path.isfile(file_name):
            print(f"File '{file_name}' not found.")
            return

        save_path = ""

        try:
            with open(file_name, mode='r') as file:
                reader = csv.DictReader(file)
                settings = next(reader)
                if settings['file path']:
                    save_path = settings['file path']
                else:
                    save_path = os.getcwd()

        except Exception as e:
            print(f"Error reading the file: {e}")
            return

        return save_path

__init__

__init__(startlevel, signal_length, headphone_name='Sennheiser_HDA200', calibrate=True)

Creates the parent class for the familiarization, the main procedure, and the screening.

Parameters:
  • startlevel (float) –

    starting level of procedure in dB HL

  • signal_length (float) –

    length of played signals in seconds

  • headphone_name (str, default: 'Sennheiser_HDA200' ) –

    Name of headphone model being used. Defaults to Sennheiser_HDA200.

  • calibrate (bool, default: True ) –

    Use calibration file. Defaults to True.

Source code in app\model.py
def __init__(self, startlevel:float, signal_length:float, headphone_name:str="Sennheiser_HDA200", calibrate:bool=True):
    """Creates the parent class for the familiarization, the main procedure, and the screening.

    Args:
        startlevel (float): starting level of procedure in dB HL
        signal_length (float): length of played signals in seconds
        headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
        calibrate (bool, optional): Use calibration file. Defaults to True.
    """
    self.ap = AudioPlayer()
    self.startlevel = startlevel
    self.level = startlevel
    self.signal_length = signal_length
    self.frequency = 1000
    self.zero_dbhl = 0.000005 # zero_dbhl in absolute numbers. This is a rough guess for uncalibrated systems and will be adjusted through the calibration file
    self.tone_heard = False
    self.freq_bands = ['125', '250', '500', '1000', '2000', '4000', '8000']
    self.freq_levels = {125: 20, 250: 20, 500: 20, 1000: 20, 2000: 20, 4000: 20, 8000: 20} # screening levels
    self.side = 'l'
    self.test_mode = False # set True to be able to skip procedures with right arrow key
    self.jump_to_end = False
    self.use_calibration = calibrate
    self.progress = 0 # value for progressbar
    self.retspl = self.get_retspl_values(headphone_name)
    self.calibration = self.get_calibration_values()
    self.save_path = self.get_save_path()  # Initialize save_path

add_to_temp_csv

add_to_temp_csv(value, frequency, side, temp_filename)

Add a value in for a specific frequency to the temporary CSV file

Parameters:
  • value (str) –

    level in dB HL at specific frequency

  • frequency (str) –

    frequency where value should be added

  • side (str) –

    specify which ear ('l' or 'r')

  • temp_filename (str) –

    name of temporary CSV file

Source code in app\model.py
def add_to_temp_csv(self, value:str, frequency:str, side:str, temp_filename:str):
    """Add a value in for a specific frequency to the temporary CSV file

    Args:
        value (str): level in dB HL at specific frequency
        frequency (str): frequency where value should be added
        side (str): specify which ear ('l' or 'r')
        temp_filename (str): name of temporary CSV file
    """
    # Read all rows from the CSV file
    with open(temp_filename, mode='r', newline='') as temp_file:
        dict_reader = csv.DictReader(temp_file)
        rows = list(dict_reader)

    # Update the relevant row based on the side parameter
    if side == 'l':
        rows[0][frequency] = value
    elif side == 'r':
        rows[1][frequency] = value
    else:
        rows[0][frequency] = value
        rows[1][frequency] = value

    # Write all rows back to the CSV file
    with open(temp_filename, mode='w', newline='') as temp_file:
        dict_writer = csv.DictWriter(temp_file, fieldnames=self.freq_bands)
        dict_writer.writeheader()
        dict_writer.writerows(rows)

    print(rows[0], rows[1])

    for row in rows[2:]:
        print(row['125'], row['250'])

create_final_csv_and_audiogram

create_final_csv_and_audiogram(temp_filename, binaural=False)

Creates a permanent CSV file and audiogram from the temporary file.

Parameters:
  • temp_filename (str) –

    Name of the temporary CSV file.

  • binaural (bool, default: False ) –

    If the test is binaural.

Source code in app\model.py
def create_final_csv_and_audiogram(self, temp_filename:str, binaural:bool=False):
    """Creates a permanent CSV file and audiogram from the temporary file.

    Args:
        temp_filename (str): Name of the temporary CSV file.
        binaural (bool): If the test is binaural.
    """
    # Read the temporary file
    with open(temp_filename, mode='r', newline='') as temp_file:
        dict_reader = csv.DictReader(temp_file)
        rows = list(dict_reader)

    # Get date and time
    now = datetime.now()
    date_str = now.strftime("%Y%m%d_%H%M%S")
    try:
        id = rows[2]['250']
    except:
        id = "missingID"

    # Create folder for the subject
    folder_name = os.path.join(self.save_path, f"{id}")
    if not os.path.exists(folder_name):
        os.makedirs(folder_name)

    final_csv_filename = os.path.join(folder_name, f"{id}_audiogramm_{date_str}.csv")

    # Write the permanent CSV file
    with open(final_csv_filename, mode='x', newline='') as final_file:
        dict_writer = csv.DictWriter(final_file, fieldnames=self.freq_bands)
        dict_writer.writeheader()
        dict_writer.writerows(rows)

    freqs = [int(x) for x in self.freq_bands]

    left_levels = [self.parse_dbhl_value(rows[0][freq]) for freq in self.freq_bands]
    right_levels = [self.parse_dbhl_value(rows[1][freq]) for freq in self.freq_bands]

    # Generate the audiogram filename
    audiogram_filename = os.path.join(folder_name, f"{id}_audiogram_{date_str}.png")
    print(left_levels, right_levels)
    create_audiogram(freqs, left_levels, right_levels, binaural=binaural, name=audiogram_filename, freq_levels=self.freq_levels)

create_temp_csv

create_temp_csv(id='', **additional_data)

Creates a temporary CSV file with the relevant frequency bands as a header and NaN in the second and third line as starting value for each band. (second line: left ear, third line: right ear) ID and additional data will be stored in subsequent lines in the format: key, value.

Parameters:
  • id (str, default: '' ) –

    id to be stored, that will later be used for naming exported csv file

  • **additional_data

    additional key/value pairs to be stored in CSV file after procedure is done

Returns:
  • str( str ) –

    name of temporary file

Source code in app\model.py
def create_temp_csv(self, id:str="", **additional_data)->str:
    """Creates a temporary CSV file with the relevant frequency bands as a header
    and NaN in the second and third line as starting value for each band.
    (second line: left ear, third line: right ear)
    ID and additional data will be stored in subsequent lines in the format: key, value.

    Args:
        id (str, optional): id to be stored, that will later be used for naming exported csv file
        **additional_data: additional key/value pairs to be stored in CSV file after procedure is done

    Returns:
        str: name of temporary file
    """
    with tfile.NamedTemporaryFile(mode='w+', delete=False, newline='', suffix='.csv') as temp_file:
        # Define the CSV writer
        csv_writer = csv.writer(temp_file)

        # Write header
        csv_writer.writerow(self.freq_bands)

        # Write value NaN for each frequency in second and third row
        csv_writer.writerow(['NaN' for _ in range(len(self.freq_bands))])
        csv_writer.writerow(['NaN' for _ in range(len(self.freq_bands))])

        # Write id and additional data
        if id:
            csv_writer.writerow(["id", id])
        if additional_data:
            for key, value in additional_data.items():
                csv_writer.writerow([key, value])

        return temp_file.name

dbhl_to_volume

dbhl_to_volume(dbhl)

Calculate dB HL into absolute numbers.

Parameters:
  • dbhl (float) –

    value in dB HL

Returns:
  • float( float ) –

    value in absolute numbers

Source code in app\model.py
def dbhl_to_volume(self, dbhl:float)->float:
    """Calculate dB HL into absolute numbers.

    Args:
        dbhl (float): value in dB HL

    Returns:
        float: value in absolute numbers
    """
    if self.use_calibration:
        # add RETSPL and values from calibration file at that frequency
        dbspl = dbhl + self.retspl[self.frequency] - self.calibration[self.side][self.frequency] 
    else:
        # only add RETSPL
        dbspl = dbhl + self.retspl[self.frequency] 

    return self.zero_dbhl * 10 ** (dbspl / 20) # calculate from dB to absolute numbers using the reference point self.zero_dbhl

get_calibration_values

get_calibration_values()

Read the correct calibration values from the calibration.csv file.

Returns:
  • dict

    dict of int:float : calibration values for each frequency band from 125 Hz to 8000 Hz

Source code in app\model.py
def get_calibration_values(self)->dict:
    """Read the correct calibration values from the calibration.csv file.

    Returns:
        dict of int:float : calibration values for each frequency band from 125 Hz to 8000 Hz
    """
    file_name = 'calibration.csv'

    # Check if the CSV file exists
    if not os.path.isfile(file_name):
        print(f"File '{file_name}' not found.")
        return

    try:
        with open(file_name, mode='r') as file:
            reader = csv.DictReader(file)
            calibration_str_values_l = next(reader)
            calibration_str_values_r = next(reader)

            # convert dictionary to int:float and put into extra dictionary for left and right side
            calibration_values = {}
            calibration_values['l'] = {int(k): float(v) for k, v in calibration_str_values_l.items()}
            calibration_values['r'] = {int(k): float(v) for k, v in calibration_str_values_r.items()}

            # if both sides are used, calculate average between both sides
            calibration_values['lr'] = {}
            for k, v in calibration_values['l'].items():
                calibration_values['lr'][k] = (10 * np.log10((10 ** (v / 10) + 10 ** (calibration_values['r'][k] / 10)) / 2))

    except Exception as e:
        print(f"Error reading the file: {e}")
        return

    print(calibration_values)
    return calibration_values

get_progress

get_progress()

Gets the current progress.

Returns:
  • float( float ) –

    progress value between 0.0 and 1.0

Source code in app\model.py
def get_progress(self)->float:
    """Gets the current progress.

    Returns:
        float: progress value between 0.0 and 1.0
    """
    return self.progress

get_retspl_values

get_retspl_values(headphone_name)

Reads the correct RETSPL values from the retspl.csv file.

Parameters:
  • headphone_name (str) –

    exact name of headphone as it appears in CSV file

Returns:
  • dict of int:float : RETSPL values for each frequency band from 125 Hz to 8000 Hz

Source code in app\model.py
def get_retspl_values(self, headphone_name:str):
    """Reads the correct RETSPL values from the retspl.csv file.

    Args:
        headphone_name (str): exact name of headphone as it appears in CSV file

    Returns:
        dict of int:float : RETSPL values for each frequency band from 125 Hz to 8000 Hz
    """
    file_name = 'retspl.csv'

    # Check if the CSV file exists
    if not os.path.isfile(file_name):
        print(f"File '{file_name}' not found.")
        return

    retspl_values = {}

    try:
        with open(file_name, mode='r') as file:
            reader = csv.DictReader(file)
            for row in reader:
                if row['headphone_model'] == headphone_name:
                    retspl_values[int(row['frequency'])] = float(row['retspl'])
    except Exception as e:
        print(f"Error reading the file: {e}")
        return

    # Check if the headphone model was found
    if not retspl_values:
        print(f"Headphone model '{headphone_name}' not found.")
        return

    print(retspl_values)
    return retspl_values

get_save_path

get_save_path()

Gets selected path from settings.csv file for saving files.

Returns:
  • str( str ) –

    save path

Source code in app\model.py
def get_save_path(self)->str:
    """Gets selected path from settings.csv file for saving files.

    Returns:
        str: save path
    """
    file_name = 'settings.csv'

    # Check if the CSV file exists
    if not os.path.isfile(file_name):
        print(f"File '{file_name}' not found.")
        return

    save_path = ""

    try:
        with open(file_name, mode='r') as file:
            reader = csv.DictReader(file)
            settings = next(reader)
            if settings['file path']:
                save_path = settings['file path']
            else:
                save_path = os.getcwd()

    except Exception as e:
        print(f"Error reading the file: {e}")
        return

    return save_path

get_value_from_csv

get_value_from_csv(frequency, temp_filename, side='l')

Get the value at a specific frequency from the temporary CSV file.

Parameters:
  • frequency (str) –

    frequency where value is stored

  • temp_filename (str) –

    name of temporary CSV file

  • side (str, default: 'l' ) –

    specify which ear ('l' or 'r'). Defaults to 'l'.

Returns:
  • str( str ) –

    dB HL value at specified frequency

Source code in app\model.py
def get_value_from_csv(self, frequency:str, temp_filename:str, side:str='l')->str:
    """Get the value at a specific frequency from the temporary CSV file.

    Args:
        frequency (str): frequency where value is stored
        temp_filename (str): name of temporary CSV file
        side (str, optional): specify which ear ('l' or 'r'). Defaults to 'l'.

    Returns:
        str: dB HL value at specified frequency
    """
    with open(temp_filename, mode='r', newline='') as temp_file:
        dict_reader = csv.DictReader(temp_file)
        freq_dict = next(dict_reader) # left ear
        if side == 'r': # go to next line if right side
            freq_dict = next(dict_reader)    
        return freq_dict[frequency]

key_press

key_press(key)

Function for pynputto be called on key press

Parameters:
  • key (Key) –

    key that was pressed

Source code in app\model.py
def key_press(self, key:keyboard.Key):
    """Function for pynputto be called on key press

    Args:
        key (keyboard.Key): key that was pressed
    """
    if key == keyboard.Key.space:
        self.tone_heard = True
        print("Tone heard!")
    elif self.test_mode and key == keyboard.Key.right:
        self.jump_to_end = True

parse_dbhl_value

parse_dbhl_value(value)

Parses the dBHL value from the CSV file.

Parameters:
  • value (str) –

    the value from the CSV file

Returns:
  • int

    int or None: the parsed value or None if 'NH'

Source code in app\model.py
def parse_dbhl_value(self, value:str)->int:
    """Parses the dBHL value from the CSV file.

    Args:
        value (str): the value from the CSV file

    Returns:
        int or None: the parsed value or None if 'NH'
    """
    if value == 'NH':
        return 'NH'
    try:
        return int(value)
    except ValueError:
        return None

play_tone

play_tone()

Sets tone_heard to False, play beep, then waits 4 s (max) for keypress. Sets tone_heard to True if key is pressed. Then waits for around 1 s to 2.5 s (randomized).

Source code in app\model.py
def play_tone(self):
    """Sets tone_heard to False, play beep, then waits 4 s (max) for keypress.
    Sets tone_heard to True if key is pressed.
    Then waits for around 1 s to 2.5 s (randomized).
    """
    self.tone_heard = False
    print(self.frequency, "Hz - playing tone at", self.level, "dBHL.")
    self.ap.play_beep(self.frequency, self.dbhl_to_volume(self.level), self.signal_length, self.side)
    listener = keyboard.Listener(on_press=self.key_press, on_release=None)
    listener.start()
    current_wait_time = 0
    max_wait_time = 4000 # in ms 
    step_size = 50 # in ms

    while current_wait_time < max_wait_time and not self.tone_heard: # wait for keypress
        time.sleep(step_size / 1000)
        current_wait_time += step_size
    listener.stop()
    self.ap.stop()

    if not self.tone_heard:
        print("Tone not heard :(")
    else:
        sleep_time = random.uniform(1, 2.5) # random wait time between 1 and 2.5
        time.sleep(sleep_time) # wait before next tone is played. #TODO test times

ScreeningProcedure

Bases: Procedure

Source code in app\model.py
class ScreeningProcedure(Procedure):

    def __init__(self, temp_filename:str, signal_length:int=1, headphone_name:str="Sennheiser_HDA200", calibrate:bool=True):
        """Short screening process to check if subject can hear specific frequencies at certain levels.

        Args:
            temp_filename (str): name of temporary CSV file where starting level is stored and future values will be stored.
            signal_length (int, optional): length of played signals in seconds. Defaults to 1.
            headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
            calibrate (bool, optional): Use calibration file. Defaults to True.
        """
        super().__init__(startlevel=0, signal_length=signal_length, headphone_name=headphone_name, calibrate=calibrate)
        self.temp_filename = temp_filename
        self.freq_order = [1000, 2000, 4000, 8000, 500, 250, 125]
        self.freq_levels = {125: 20, 250: 20, 500: 20, 1000: 20, 2000: 20, 4000: 20, 8000: 20}
        self.progress_step = 1 / 14

    def screen_test(self, binaural:bool=False)->bool:
        """Main function.

        Returns:
            bool: test successful
        """
        self.progress = 0.01

        if not binaural:
            self.side = 'l'
            self.screen_one_ear()

            self.side = 'r'
            self.screen_one_ear()

            self.progress = 1

            self.create_final_csv_and_audiogram(self.temp_filename, binaural)
            return True

        if binaural:
            self.progress_step = 1 / 7
            self.side = 'lr'
            self.screen_one_ear()
            self.progress = 1

        self.create_final_csv_and_audiogram(self.temp_filename, binaural)

    def screen_one_ear(self):
        """Screening for one ear.
        """
        success = []

        for f in self.freq_order:
            print(f"Testing frequency {f} Hz")
            s = self.screen_one_freq(f)
            success.append(s)

    def screen_one_freq(self, freq:int)->bool: 
        """Screening for one frequency.

        Args:
            freq (int): frequency to be tested

        Returns:
            bool: tone heard
        """
        self.frequency = freq
        self.level = self.freq_levels[freq]
        self.tone_heard = False
        self.num_heard = 0

        for i in range(2):
            self.play_tone()

            if self.tone_heard:
                self.num_heard += 1

        if self.num_heard == 1:
            self.play_tone()

            if self.tone_heard:
                self.num_heard += 1

        if self.num_heard >= 2:
            self.add_to_temp_csv(str(self.level), str(self.frequency), self.side, self.temp_filename)
            self.progress += self.progress_step
            return

        self.add_to_temp_csv('NH', str(self.frequency), self.side, self.temp_filename)
        self.progress += self.progress_step

__init__

__init__(temp_filename, signal_length=1, headphone_name='Sennheiser_HDA200', calibrate=True)

Short screening process to check if subject can hear specific frequencies at certain levels.

Parameters:
  • temp_filename (str) –

    name of temporary CSV file where starting level is stored and future values will be stored.

  • signal_length (int, default: 1 ) –

    length of played signals in seconds. Defaults to 1.

  • headphone_name (str, default: 'Sennheiser_HDA200' ) –

    Name of headphone model being used. Defaults to Sennheiser_HDA200.

  • calibrate (bool, default: True ) –

    Use calibration file. Defaults to True.

Source code in app\model.py
def __init__(self, temp_filename:str, signal_length:int=1, headphone_name:str="Sennheiser_HDA200", calibrate:bool=True):
    """Short screening process to check if subject can hear specific frequencies at certain levels.

    Args:
        temp_filename (str): name of temporary CSV file where starting level is stored and future values will be stored.
        signal_length (int, optional): length of played signals in seconds. Defaults to 1.
        headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
        calibrate (bool, optional): Use calibration file. Defaults to True.
    """
    super().__init__(startlevel=0, signal_length=signal_length, headphone_name=headphone_name, calibrate=calibrate)
    self.temp_filename = temp_filename
    self.freq_order = [1000, 2000, 4000, 8000, 500, 250, 125]
    self.freq_levels = {125: 20, 250: 20, 500: 20, 1000: 20, 2000: 20, 4000: 20, 8000: 20}
    self.progress_step = 1 / 14

screen_one_ear

screen_one_ear()

Screening for one ear.

Source code in app\model.py
def screen_one_ear(self):
    """Screening for one ear.
    """
    success = []

    for f in self.freq_order:
        print(f"Testing frequency {f} Hz")
        s = self.screen_one_freq(f)
        success.append(s)

screen_one_freq

screen_one_freq(freq)

Screening for one frequency.

Parameters:
  • freq (int) –

    frequency to be tested

Returns:
  • bool( bool ) –

    tone heard

Source code in app\model.py
def screen_one_freq(self, freq:int)->bool: 
    """Screening for one frequency.

    Args:
        freq (int): frequency to be tested

    Returns:
        bool: tone heard
    """
    self.frequency = freq
    self.level = self.freq_levels[freq]
    self.tone_heard = False
    self.num_heard = 0

    for i in range(2):
        self.play_tone()

        if self.tone_heard:
            self.num_heard += 1

    if self.num_heard == 1:
        self.play_tone()

        if self.tone_heard:
            self.num_heard += 1

    if self.num_heard >= 2:
        self.add_to_temp_csv(str(self.level), str(self.frequency), self.side, self.temp_filename)
        self.progress += self.progress_step
        return

    self.add_to_temp_csv('NH', str(self.frequency), self.side, self.temp_filename)
    self.progress += self.progress_step

screen_test

screen_test(binaural=False)

Main function.

Returns:
  • bool( bool ) –

    test successful

Source code in app\model.py
def screen_test(self, binaural:bool=False)->bool:
    """Main function.

    Returns:
        bool: test successful
    """
    self.progress = 0.01

    if not binaural:
        self.side = 'l'
        self.screen_one_ear()

        self.side = 'r'
        self.screen_one_ear()

        self.progress = 1

        self.create_final_csv_and_audiogram(self.temp_filename, binaural)
        return True

    if binaural:
        self.progress_step = 1 / 7
        self.side = 'lr'
        self.screen_one_ear()
        self.progress = 1

    self.create_final_csv_and_audiogram(self.temp_filename, binaural)

StandardProcedure

Bases: Procedure

Source code in app\model.py
class StandardProcedure(Procedure):

    def __init__(self, temp_filename:str, signal_length:int=1, headphone_name:float="Sennheiser_HDA200", calibrate:bool=True):
        """Standard audiometer process (rising level).

        Args:
            temp_filename (str): name of temporary CSV file where starting level is stored and future values will be stored
            signal_length (int, optional): length of played signal in seconds. Defaults to 1.
            headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
            calibrate (bool, optional): Use calibration file. Defaults to True.
        """
        startlevel = int(self.get_value_from_csv('1000', temp_filename)) - 10 # 10 dB under level from familiarization
        super().__init__(startlevel, signal_length, headphone_name=headphone_name, calibrate=calibrate)
        self.temp_filename = temp_filename
        self.freq_order = [1000, 2000, 4000, 8000, 500, 250, 125] # order in which frequencies are tested

        self.progress_step = 0.95 / 14


    def standard_test(self, binaural:bool=False)->bool:
        """Main function

        Returns:
            bool: test successful
        """
        self.progress = 0.01

        if not binaural:
            self.side = 'l'

            success_l = self.standard_test_one_ear()

            if self.test_mode == True and self.jump_to_end == True:
                self.create_final_csv_and_audiogram(self.temp_filename, binaural)
                self.progress = 1
                return True

            self.side = 'r'
            success_r = self.standard_test_one_ear()

            if success_l and success_r:
                self.create_final_csv_and_audiogram(self.temp_filename, binaural)
                self.progress = 1
                return True

        if binaural:
            self.progress_step = 0.95 / 7
            self.side = 'lr'
            success_lr = self.standard_test_one_ear()

            if self.test_mode == True and self.jump_to_end == True:
                self.create_final_csv_and_audiogram(self.temp_filename, binaural)
                self.progress = 1
                return True

            if success_lr:
                self.create_final_csv_and_audiogram(self.temp_filename, binaural)
                self.progress = 1
                return True

        return False

    def standard_test_one_ear(self)->bool:
        """Audiometer for one ear.

        Returns:
            bool: test successful
        """
        success = []

        self.tone_heard = False
        self.frequency = 1000
        self.level = self.startlevel

        # Step 1 (raise tone in 5 dB steps until it is heard)
        while not self.tone_heard:
            self.play_tone()

            if self.test_mode == True and self.jump_to_end == True:
                return True

            if not self.tone_heard:
                self.level += 5

        self.startlevel = self.level
        print(f"Starting level: {self.startlevel} dBHL")

        # test every frequency
        for f in self.freq_order:
            print(f"Testing frequency {f} Hz")
            s = self.standard_test_one_freq(f)

            if self.test_mode == True and self.jump_to_end == True:
                return True

            success.append(s)

        # retest 1000 Hz (and more frequencies if discrepancy is too high)
        for f in self.freq_order:
            print(f"Retest at frequency {f} Hz")
            s = self.standard_test_one_freq(f, retest=True)
            if s:
                break

        if all(success):
            return True

        else:
            return False

    def standard_test_one_freq(self, freq:int, retest:bool=False)->bool:
        """Test for one frequency.

        Args:
            freq (int): frequency at which hearing is tested
            retest (bool, optional): this is the retest at the end of step 3 according to DIN. Defaults to False

        Returns:
            bool: test successful
        """
        self.tone_heard = True
        self.frequency = freq
        self.level = self.startlevel

        # Step 2
        answers = []
        tries = 0

        while tries < 6:
            # reduce in 10dB steps until no answer
            while self.tone_heard:
                self.level -= 10
                self.play_tone()

            # raise in 5 dB steps until answer
            while not self.tone_heard:
                self.level += 5
                self.play_tone()

            tries += 1
            answers.append(self.level)
            print(f"Try nr {tries}: level: {self.level}")

            if answers.count(self.level) >= 2:
                if retest:
                    if abs(self.level - int(self.get_value_from_csv(str(self.frequency), self.temp_filename, self.side))) > 5:
                        self.add_to_temp_csv(str(self.level), str(self.frequency), self.side, self.temp_filename)
                        return False
                    else:
                        self.add_to_temp_csv(str(self.level), str(self.frequency), self.side, self.temp_filename)
                        return True

                self.add_to_temp_csv(str(self.level), str(self.frequency), self.side, self.temp_filename)
                if self.progress < 0.95 - self.progress_step:
                    self.progress += self.progress_step
                return True

            # no two same answers in three tries
            if tries == 3:
                self.level += 10
                self.play_tone()
                answers = []

        print("Something went wrong, please try from the beginning again.")
        return False

__init__

__init__(temp_filename, signal_length=1, headphone_name='Sennheiser_HDA200', calibrate=True)

Standard audiometer process (rising level).

Parameters:
  • temp_filename (str) –

    name of temporary CSV file where starting level is stored and future values will be stored

  • signal_length (int, default: 1 ) –

    length of played signal in seconds. Defaults to 1.

  • headphone_name (str, default: 'Sennheiser_HDA200' ) –

    Name of headphone model being used. Defaults to Sennheiser_HDA200.

  • calibrate (bool, default: True ) –

    Use calibration file. Defaults to True.

Source code in app\model.py
def __init__(self, temp_filename:str, signal_length:int=1, headphone_name:float="Sennheiser_HDA200", calibrate:bool=True):
    """Standard audiometer process (rising level).

    Args:
        temp_filename (str): name of temporary CSV file where starting level is stored and future values will be stored
        signal_length (int, optional): length of played signal in seconds. Defaults to 1.
        headphone_name (str, optional): Name of headphone model being used. Defaults to Sennheiser_HDA200.
        calibrate (bool, optional): Use calibration file. Defaults to True.
    """
    startlevel = int(self.get_value_from_csv('1000', temp_filename)) - 10 # 10 dB under level from familiarization
    super().__init__(startlevel, signal_length, headphone_name=headphone_name, calibrate=calibrate)
    self.temp_filename = temp_filename
    self.freq_order = [1000, 2000, 4000, 8000, 500, 250, 125] # order in which frequencies are tested

    self.progress_step = 0.95 / 14

standard_test

standard_test(binaural=False)

Main function

Returns:
  • bool( bool ) –

    test successful

Source code in app\model.py
def standard_test(self, binaural:bool=False)->bool:
    """Main function

    Returns:
        bool: test successful
    """
    self.progress = 0.01

    if not binaural:
        self.side = 'l'

        success_l = self.standard_test_one_ear()

        if self.test_mode == True and self.jump_to_end == True:
            self.create_final_csv_and_audiogram(self.temp_filename, binaural)
            self.progress = 1
            return True

        self.side = 'r'
        success_r = self.standard_test_one_ear()

        if success_l and success_r:
            self.create_final_csv_and_audiogram(self.temp_filename, binaural)
            self.progress = 1
            return True

    if binaural:
        self.progress_step = 0.95 / 7
        self.side = 'lr'
        success_lr = self.standard_test_one_ear()

        if self.test_mode == True and self.jump_to_end == True:
            self.create_final_csv_and_audiogram(self.temp_filename, binaural)
            self.progress = 1
            return True

        if success_lr:
            self.create_final_csv_and_audiogram(self.temp_filename, binaural)
            self.progress = 1
            return True

    return False

standard_test_one_ear

standard_test_one_ear()

Audiometer for one ear.

Returns:
  • bool( bool ) –

    test successful

Source code in app\model.py
def standard_test_one_ear(self)->bool:
    """Audiometer for one ear.

    Returns:
        bool: test successful
    """
    success = []

    self.tone_heard = False
    self.frequency = 1000
    self.level = self.startlevel

    # Step 1 (raise tone in 5 dB steps until it is heard)
    while not self.tone_heard:
        self.play_tone()

        if self.test_mode == True and self.jump_to_end == True:
            return True

        if not self.tone_heard:
            self.level += 5

    self.startlevel = self.level
    print(f"Starting level: {self.startlevel} dBHL")

    # test every frequency
    for f in self.freq_order:
        print(f"Testing frequency {f} Hz")
        s = self.standard_test_one_freq(f)

        if self.test_mode == True and self.jump_to_end == True:
            return True

        success.append(s)

    # retest 1000 Hz (and more frequencies if discrepancy is too high)
    for f in self.freq_order:
        print(f"Retest at frequency {f} Hz")
        s = self.standard_test_one_freq(f, retest=True)
        if s:
            break

    if all(success):
        return True

    else:
        return False

standard_test_one_freq

standard_test_one_freq(freq, retest=False)

Test for one frequency.

Parameters:
  • freq (int) –

    frequency at which hearing is tested

  • retest (bool, default: False ) –

    this is the retest at the end of step 3 according to DIN. Defaults to False

Returns:
  • bool( bool ) –

    test successful

Source code in app\model.py
def standard_test_one_freq(self, freq:int, retest:bool=False)->bool:
    """Test for one frequency.

    Args:
        freq (int): frequency at which hearing is tested
        retest (bool, optional): this is the retest at the end of step 3 according to DIN. Defaults to False

    Returns:
        bool: test successful
    """
    self.tone_heard = True
    self.frequency = freq
    self.level = self.startlevel

    # Step 2
    answers = []
    tries = 0

    while tries < 6:
        # reduce in 10dB steps until no answer
        while self.tone_heard:
            self.level -= 10
            self.play_tone()

        # raise in 5 dB steps until answer
        while not self.tone_heard:
            self.level += 5
            self.play_tone()

        tries += 1
        answers.append(self.level)
        print(f"Try nr {tries}: level: {self.level}")

        if answers.count(self.level) >= 2:
            if retest:
                if abs(self.level - int(self.get_value_from_csv(str(self.frequency), self.temp_filename, self.side))) > 5:
                    self.add_to_temp_csv(str(self.level), str(self.frequency), self.side, self.temp_filename)
                    return False
                else:
                    self.add_to_temp_csv(str(self.level), str(self.frequency), self.side, self.temp_filename)
                    return True

            self.add_to_temp_csv(str(self.level), str(self.frequency), self.side, self.temp_filename)
            if self.progress < 0.95 - self.progress_step:
                self.progress += self.progress_step
            return True

        # no two same answers in three tries
        if tries == 3:
            self.level += 10
            self.play_tone()
            answers = []

    print("Something went wrong, please try from the beginning again.")
    return False

create_audiogram

create_audiogram(freqs, left_values=None, right_values=None, binaural=False, name='audiogram.png', freq_levels=freq_levels, subtitle=None)

Creates an audiogram based on the given frequencies and hearing threshold values with custom x-axis labels.

Parameters:
  • freqs (list of int) –

    A list of frequencies in Hz.

  • left_values (list of int, default: None ) –

    A list of hearing thresholds in dB HL for the left ear. Defaults to None.

  • right_values (list of int, default: None ) –

    A list of hearing thresholds in dB HL for the right ear. Defaults to None.

  • binaural (bool, default: False ) –

    Whether the audiogram is made from binaural test values. Defaults to False.

  • name (str, default: 'audiogram.png' ) –

    The name of the saved audiogram file. Defaults to "audiogram.png".

  • freq_levels (dict, default: freq_levels ) –

    A dictionary mapping frequencies to their target values. Defaults to freq_levels.

  • subtitle (str, default: None ) –

    A subtitle for the audiogram. Defaults to None.

Source code in app\audiogram.py
def create_audiogram(freqs:list, left_values:list=None, right_values:list=None, binaural:bool=False, name:str="audiogram.png", freq_levels:dict=freq_levels, subtitle:str=None):
    """
    Creates an audiogram based on the given frequencies and hearing threshold values with custom x-axis labels.

    Args:
        freqs (list of int): A list of frequencies in Hz.
        left_values (list of int, optional): A list of hearing thresholds in dB HL for the left ear. Defaults to None.
        right_values (list of int, optional): A list of hearing thresholds in dB HL for the right ear. Defaults to None.
        binaural (bool, optional): Whether the audiogram is made from binaural test values. Defaults to False.
        name (str, optional): The name of the saved audiogram file. Defaults to "audiogram.png".
        freq_levels (dict, optional): A dictionary mapping frequencies to their target values. Defaults to freq_levels.
        subtitle (str, optional): A subtitle for the audiogram. Defaults to None.
    """

    print("Creating audiogram with frequencies:", freqs)
    print("Left ear values:", left_values)
    print("Right ear values:", right_values)

    fig, ax = plt.subplots(figsize=(10, 6))

    ax.axhspan(-10, 20, facecolor='lightgreen', alpha=0.2)
    ax.axhspan(20, 40, facecolor='lightskyblue', alpha=0.2)
    ax.axhspan(40, 70, facecolor='yellow', alpha=0.2)
    ax.axhspan(70, 90, facecolor='orange', alpha=0.2)
    ax.axhspan(90, 120, facecolor='red', alpha=0.2)

    t1 = ax.text(6.4, 5, 'Normalhörigkeit', ha='left', va='center', fontsize=TEXT_FONT_SIZE)
    t2 = ax.text(6.4, 30, 'Leichte\nSchwerhörigkeit', ha='left', va='center', fontsize=TEXT_FONT_SIZE)
    t3 = ax.text(6.4, 55, 'Mittlere\nSchwerhörigkeit', ha='left', va='center', fontsize=TEXT_FONT_SIZE)
    t4 = ax.text(6.4, 80, 'Schwere\nSchwerhörigkeit', ha='left', va='center', fontsize=TEXT_FONT_SIZE)
    t5 = ax.text(6.4, 105, 'Hochgradige\nSchwerhörigkeit', ha='left', va='center', fontsize=TEXT_FONT_SIZE)

    x_vals = range(len(freqs))
    target_values = np.array(list(freq_levels.values()))

    nan_freqs_left = [freq for i, freq in zip(left_values, freqs) if i == 'NaN']
    nan_freqs_right = [freq for i, freq in zip(right_values, freqs) if i == 'NaN']

    nan_text = ""
    nan_t = False

    if 'NH' in left_values or 'NH' in right_values:
        heard_i_left, heard_level_left, not_heard_i_left, not_heard_level_left = split_values(x_vals, left_values, target_values)
        heard_i_right, heard_level_right, not_heard_i_right, not_heard_level_right = split_values(x_vals, right_values, target_values)

        if binaural:
            ax.plot(x_vals, target_values, linestyle='-', color=COLOR_BINAURAL)
            ax.plot(heard_i_left, heard_level_left, marker=MARKER_BINAURAL, markersize=MARKER_SIZE, linestyle='None', color=COLOR_BINAURAL, label='gehört')
            ax.plot(not_heard_i_left, not_heard_level_left, marker=NOT_HEARD_MARKER, markersize=NOT_HEARD_MARKER_SIZE, linestyle='None', color=COLOR_BINAURAL, label='nicht gehört')
        else:
            ax.plot(x_vals, target_values, linestyle='-', color=COLOR_RIGHT)
            ax.plot(heard_i_right, heard_level_right, marker=MARKER_RIGHT, markersize=MARKER_SIZE, linestyle='None', linewidth=LINE_WIDTH, color=COLOR_RIGHT, markerfacecolor='none', markeredgewidth=MARKER_EDGE_WIDTH, label='rechts gehört')
            ax.plot(not_heard_i_right, not_heard_level_right, marker=NOT_HEARD_RIGHT_MARKER, markersize=NOT_HEARD_MARKER_SIZE, linestyle='None', linewidth=LINE_WIDTH, color=COLOR_RIGHT, markeredgewidth=MARKER_EDGE_WIDTH, label='rechts nicht gehört')
            ax.plot(x_vals, target_values+SHIFT, linestyle='-', color=COLOR_LEFT)
            ax.plot(heard_i_left, heard_level_left+SHIFT, marker=MARKER_LEFT, markersize=MARKER_SIZE, linestyle='None', linewidth=LINE_WIDTH, color=COLOR_LEFT, markeredgewidth=MARKER_EDGE_WIDTH, label='links gehört')
            ax.plot(not_heard_i_left, not_heard_level_left+SHIFT, marker=NOT_HEARD_LEFT_MARKER, markersize=NOT_HEARD_MARKER_SIZE, linestyle='None', linewidth=LINE_WIDTH, color=COLOR_LEFT, markeredgewidth=MARKER_EDGE_WIDTH, label='links nicht gehört')

    else:
        x_vals_left, left_values = filter_none(x_vals, left_values)
        x_vals_right, right_values = filter_none(x_vals, right_values)

        if binaural:
            ax.plot(x_vals_left, left_values, marker=MARKER_BINAURAL, markersize=MARKER_SIZE, linestyle='-', color=COLOR_BINAURAL, label='binaural')
        else:
            ax.plot(x_vals_right, right_values, marker=MARKER_RIGHT, markersize=MARKER_SIZE, linestyle='-', linewidth=LINE_WIDTH, color=COLOR_RIGHT, markeredgewidth=MARKER_EDGE_WIDTH, markerfacecolor='none', label='rechtes Ohr')
            ax.plot(x_vals_left, left_values+SHIFT, marker=MARKER_LEFT, markersize=MARKER_SIZE, linestyle='-', linewidth=LINE_WIDTH, color=COLOR_LEFT, markeredgewidth=MARKER_EDGE_WIDTH, label='linkes Ohr')

        if nan_freqs_left or nan_freqs_right:
            and_str = ""
            nan_text = "Bei folgenden Frequenzen konnte kein Wert ermittelt werden:\n"
            print(nan_freqs_left, nan_freqs_right)
            if nan_freqs_left:
                nan_text += f"links: {', '.join(map(str, nan_freqs_left))} "
                and_str = "und "
            if nan_freqs_right:
                nan_text += f"{and_str}rechts: {', '.join(map(str, nan_freqs_right))}"
            nan_t = ax.text(0.05, -0.2, nan_text, transform=ax.transAxes, fontsize=TEXT_FONT_SIZE, ha='left', va='top', bbox=dict(facecolor='None', edgecolor='None'))

    ax.invert_yaxis()

    if subtitle:
        title = fig.suptitle('Audiogramm', fontsize=HEADER_SIZE, y=1.02) 
        ax.set_title(subtitle, fontsize=LABEL_FONT_SIZE, pad=20)
    else:
        title = fig.suptitle('Audiogramm', fontsize=HEADER_SIZE) 

    ax.set_xlabel('Frequenzen (Hz)', fontsize=LABEL_FONT_SIZE)
    ax.set_ylabel('Hörschwelle (dB HL)', fontsize=LABEL_FONT_SIZE)
    ax.set_ylim(120, -10)
    ax.set_xticks(range(len(freqs)))
    ax.set_xticklabels([f"{int(freq)}" for freq in freqs], fontsize=TICK_FONT_SIZE)
    ax.set_yticks(np.arange(0, 121, 10))
    ax.set_yticklabels(np.arange(0, 121, 10), fontsize=TICK_FONT_SIZE)
    ax.grid(True, which='both', linestyle='--', linewidth=0.5)
    lgd = ax.legend(loc='upper left', bbox_to_anchor=(1.15, 0.205), fontsize=LEGEND_FONT_SIZE, frameon=False, labelspacing=1)

    if nan_t:
        fig.savefig(name, bbox_extra_artists=(title, lgd, t1, t2, t3, t4, t5, nan_t), bbox_inches='tight')
    else:
        fig.savefig(name, bbox_extra_artists=(title, lgd, t1, t2, t3, t4, t5), bbox_inches='tight')

    plt.close(fig)