spatialaudio / python-sounddevice

:sound: Play and Record Sound with Python :snake:
https://python-sounddevice.readthedocs.io/
MIT License
1.02k stars 149 forks source link

Is it possible to set a user-inputted value to sd.playrec() for modifying the recording duration? #432

Open creeksflowing opened 2 years ago

creeksflowing commented 2 years ago

I am building a web application for the determination of room impulse responses. I'm currently using streamlit for the GUI part, this is an extract of my code relevant to sounddevice:

from pathlib import Path
import streamlit as st
import numpy as np
import sounddevice as sd
from numba import jit
from scipy import signal
from scipy.io import wavfile

def app_room_measurements():
    audio_files_path = r"data/audio_files"
    sweep_string = ""
    inv_filter_string = ""
    ir_string = ""

    @jit(nopython=True)
    def fade(data, gain_start, gain_end):
        """
        Create a fade on an input object

        Parameters
        ----------
        :param data: The input array
        :param gain_start: The fade starting point
        :param gain_end: The fade ending point

        Returns
        -------
        data : object
            An input array with the fade applied
        """
        gain = gain_start
        delta = (gain_end - gain_start) / (len(data) - 1)
        for i in range(len(data)):
            data[i] = data[i] * gain
            gain = gain + delta

        return data

    @jit(nopython=True)
    def generate_exponential_sweep(
        sweep_duration, sr, starting_frequency, ending_frequency
    ):
        """
        Generate an exponential sweep using Farina's log sweep theory

        Parameters
        ----------
        :param sweep_duration: The duration of the excitement signal (in seconds)
        :param sr: The sampling frequency
        :param starting_frequency: The starting frequency of the excitement signal
        :param ending_frequency: The ending frequency of the excitement signal

        Returns
        -------
        exponential_sweep : array
            An array with the fade() function applied
        """
        time_in_samples = sweep_duration * sr
        exponential_sweep = np.zeros(time_in_samples, dtype=np.double)
        for n in range(time_in_samples):
            t = n / sr
            exponential_sweep[n] = np.sin(
                (2.0 * np.pi * starting_frequency * sweep_duration)
                / np.log(ending_frequency / starting_frequency)
                * (
                    np.exp(
                        (t / sweep_duration)
                        * np.log(ending_frequency / starting_frequency)
                    )
                    - 1.0
                )
            )

        number_of_samples = 50
        exponential_sweep[-number_of_samples:] = fade(
            exponential_sweep[-number_of_samples:], 1, 0
        )

        return exponential_sweep

    @jit(nopython=True)
    def generate_inverse_filter(
        sweep_duration, sr, exponential_sweep, starting_frequency, ending_frequency
    ):
        """
        Generate an inverse filter using Farina's log sweep theory

        Parameters
        ----------
        :param sweep_duration: The duration of the excitement signal (in seconds)
        :param sr: The sampling frequency
        :param exponential_sweep: The resulting array of the generate_exponential_sweep() function
        :param starting_frequency: The starting frequency of the excitement signal
        :param ending_frequency: The ending frequency of the excitement signal

        Returns
        -------
        inverse_filter : array
             The array resulting from applying an amplitude envelope to the exponential_sweep array
        """
        time_in_samples = sweep_duration * sr
        amplitude_envelope = np.zeros(time_in_samples, dtype=np.double)
        inverse_filter = np.zeros(time_in_samples, dtype=np.double)
        for n in range(time_in_samples):
            amplitude_envelope[n] = pow(
                10,
                (
                    (-6 * np.log2(ending_frequency / starting_frequency))
                    * (n / time_in_samples)
                )
                * 0.05,
            )
            inverse_filter[n] = exponential_sweep[-n] * amplitude_envelope[n]

        return inverse_filter

    sample_rate_option = st.selectbox("Select the desired sample rate", (44100, 48000))
    sweep_duration_option = st.selectbox("Select the duration of the sweep", (3, 7, 14))
    max_reverb_option = st.selectbox(
        "Select the expected maximum reverb decay time", (1, 2, 3, 5, 10)
    )

    st.caption(
        """
                Note that longer sweeps provide more accuracy,
                but even short sweeps can be used to measure long decays
                """
    )

    def write_wav_file(file_name, rate, data):
        save_file_path = os.path.join(audio_files_path, file_name)
        wavfile.write(save_file_path, rate, data)
        st.success(f"File successfully written to audio_files_path as:>> {file_name}")

    def playrec_sweep(wavefile_name):
        read_file_path = os.path.join(audio_files_path, wavefile_name)
        sample_rate, data = wavfile.read(read_file_path)

        stop_button = st.button("Stop")

        if "stop_button_state" not in st.session_state:
            st.session_state.stop_button_state = False

        user_sweep = sd.playrec(data, sample_rate, channels=1, blocking=True)

        if stop_button or st.session_state.stop_button_state:
            st.session_state.stop_button_state = True

            sd.stop()

        else:
            write_wav_file(
                file_name=user_sweep_string, rate=sample_rate_option, data=user_sweep
            )
            print("Sweep done playing")

    user_input = str(st.text_input("Name your file: "))

    if user_input:
        sweep_string = user_input + "_exponential_sweep.wav"
        inv_filter_string = user_input + "_inverse_filter.wav"
        user_sweep_string = user_input + "_user_exponential_sweep.wav"

        st.write(sweep_string)

        play_button = st.button("Play")

        if "play_button_state" not in st.session_state:
            st.session_state.play_button_state = False

        if play_button or st.session_state.play_button_state:
            st.session_state.play_button_state = True

            sweep = generate_exponential_sweep(
                sweep_duration_option, sample_rate_option, 20, 24000
            )
            inv_filter = generate_inverse_filter(
                sweep_duration_option, sample_rate_option, sweep, 20, 24000
            )

            write_wav_file(file_name=sweep_string, rate=sample_rate_option, data=sweep)

            playrec_sweep(sweep_string)

In short: I let the user choose the desired sample rate, the desired duration of the excitement signal (the sweep) and the maximum expected reverb decay time. After that the user can name the file and start the simultaneous playback and recording of the created file with sd.playrec(). The problem is: I would like to extend the duration of the recording by adding the user-inputted parameter max_reverb_option to the duration value, as the recording should include the tail of the reverb, but apparently sd.playrec()does not accept a duration parameter. How can I do it? Are there other options that I'm missing?

HaHeho commented 2 years ago

As a quick idea without having looked at the code: Append the generated sweep with zeros of the specified length. For safety, it might also be good to have a short period of silence before the sweep starts,

creeksflowing commented 2 years ago

As a quick idea without having looked at the code: Append the generated sweep with zeros of the specified length. For safety, it might also be good to have a short period of silence before the sweep starts,

That's actually a very good idea, thanks! I still have to find a way to apply the fade function to avoid clicks, let's see!

HaHeho commented 2 years ago

Yes, clicks in the beginning and end of the sweep can be tricky as well. Feel free to take inspiration (or code 👍 ) from our examples which implement playrec here (mind the latest versions of the examples and processing code is in a subbranch).