{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "source": [
        "# Tap Testing, Day 3\n",
        "\n",
        "Read through this document to complete the Day 3 activity. By the time you load this file, you should have already created some audio recordings in the `.wav` file format. Make sure you have those files ready to upload - this code is going to help you analyze them using the power of computing and mathematics.\n",
        "\n",
        "### About Sound Waves\n",
        "\n",
        "Sound waves, if you have never read about them before, are regions of high and low pressure moving through the air. Simple sound waves have one **frequency** (how many times the high-pressure part happens each second). Sound waves also have an **amplitude**, or the amount of energy difference between high and low pressures. Sounds that we hear are usually much more complex than just a single frequency, however. To explore sound waves, try the tool here which allows you to blend up to 3 different frequencies together. 440 Hz, 555 Hz, and 660 Hz create an A-major chord in music! But as you investigate blending different frequencies, consider: can you tell just by looking at the plot what frequencies are represented?\n",
        "\n",
        "#### How to Use:\n",
        "\n",
        "1. **Run the Code Cell**:\n",
        "   - Hover over the cell of code below to reveal a run (play) button. Press the run button to execute the code. Adjust the sliders and press the **Show** button below them. Try the default frequencies first: with just one, then with two of them, then all three. Then, try altering the frequencies and amplitudes more. You can also play the audio created by these overlapping frequencies by pressing the play button on the audio player just below the graph.\n"
      ],
      "metadata": {
        "id": "L4CtvDXKocMf"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Import necessary libraries\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "import ipywidgets as widgets\n",
        "from IPython.display import display, Audio, clear_output\n",
        "\n",
        "# Function to generate the waveform for plotting\n",
        "def generate_waveform(num_waves, frequencies, amplitudes, duration_ms, sample_rate=44100):\n",
        "    t = np.linspace(0, duration_ms / 1000, sample_rate * duration_ms // 1000)  # Sampling rate based on duration\n",
        "    y = np.zeros_like(t)\n",
        "    for i in range(num_waves):\n",
        "        y += amplitudes[i] * np.sin(2 * np.pi * frequencies[i] * t)\n",
        "    y /= np.max(np.abs(y))  # Normalize to -1 to 1\n",
        "    return t * 1000, y  # Convert time back to milliseconds for display\n",
        "\n",
        "# Function to generate the waveform for audio\n",
        "def generate_audio_waveform(num_waves, frequencies, amplitudes, sample_rate=44100, duration=1):\n",
        "    t = np.linspace(0, duration, sample_rate * duration)  # 1-second duration\n",
        "    y = np.zeros_like(t)\n",
        "    for i in range(num_waves):\n",
        "        y += amplitudes[i] * np.sin(2 * np.pi * frequencies[i] * t)\n",
        "    y /= np.max(np.abs(y))  # Normalize to -1 to 1\n",
        "    return y\n",
        "\n",
        "# Function to update the plot\n",
        "def update_plot(num_waves, f1, f2, f3, a1, a2, a3, duration_ms):\n",
        "    frequencies = [f1, f2, f3][:num_waves]\n",
        "    amplitudes = [a1, a2, a3][:num_waves]\n",
        "    t, y = generate_waveform(num_waves, frequencies, amplitudes, duration_ms)\n",
        "\n",
        "    plt.figure(figsize=(10, 6))\n",
        "    plt.plot(t, y, label='Waveform')\n",
        "    plt.title('Blending multiple frequencies')\n",
        "    plt.xlabel('Time (ms)')\n",
        "    plt.ylabel('Amplitude')\n",
        "    plt.ylim(-1, 1)  # Set the amplitude range from -1 to 1\n",
        "    plt.grid(True)\n",
        "\n",
        "    # Generate legend text\n",
        "    legend_text = ' + '.join([f'{freq:.1f} Hz, amplitude of {amp:.1f}' for freq, amp in zip(frequencies, amplitudes)])\n",
        "    plt.figtext(0.5, -0.05, legend_text, ha='center', va='top', fontsize=15)  # Adjusted font size\n",
        "\n",
        "    plt.show()\n",
        "\n",
        "    return frequencies, amplitudes, duration_ms\n",
        "\n",
        "# Function to play the audio\n",
        "def play_audio(frequencies, amplitudes):\n",
        "    y = generate_audio_waveform(len(frequencies), frequencies, amplitudes)\n",
        "    display(Audio(y, rate=44100))\n",
        "    display(widgets.HTML(value=\"<p>Click play to listen to the resulting sound!</p>\"))\n",
        "\n",
        "# Create sliders for interactivity\n",
        "num_waves_slider = widgets.IntSlider(min=1, max=3, step=1, value=1, description='Number of Waves')\n",
        "f1_slider = widgets.FloatSlider(min=100, max=1000, step=10, value=440, description='Frequency 1 (Hz)')\n",
        "f2_slider = widgets.FloatSlider(min=100, max=1000, step=10, value=555, description='Frequency 2 (Hz)')\n",
        "f3_slider = widgets.FloatSlider(min=100, max=1000, step=10, value=660, description='Frequency 3 (Hz)')\n",
        "a1_slider = widgets.FloatSlider(min=0.1, max=1, step=0.1, value=0.5, description='Amplitude 1')\n",
        "a2_slider = widgets.FloatSlider(min=0.1, max=1, step=0.1, value=0.5, description='Amplitude 2')\n",
        "a3_slider = widgets.FloatSlider(min=0.1, max=1, step=0.1, value=0.5, description='Amplitude 3')\n",
        "duration_slider = widgets.IntSlider(min=1, max=1000, step=5, value=5, description='Duration (ms)')\n",
        "\n",
        "# Button to show plot and play audio\n",
        "show_button = widgets.Button(description=\"Show\")\n",
        "output = widgets.Output()\n",
        "\n",
        "def on_button_clicked(b):\n",
        "    with output:\n",
        "        clear_output(wait=True)\n",
        "        frequencies, amplitudes, duration_ms = update_plot(num_waves_slider.value, f1_slider.value, f2_slider.value, f3_slider.value, a1_slider.value, a2_slider.value, a3_slider.value, duration_slider.value)\n",
        "        play_audio(frequencies, amplitudes)\n",
        "\n",
        "show_button.on_click(on_button_clicked)\n",
        "\n",
        "# Combine sliders and button into a single interface\n",
        "ui = widgets.VBox([num_waves_slider, f1_slider, f2_slider, f3_slider, a1_slider, a2_slider, a3_slider, duration_slider, show_button, output])\n",
        "\n",
        "# Display the interactive plot and sliders\n",
        "display(ui)\n"
      ],
      "metadata": {
        "id": "ZzeG949tahRj"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "### Step 0: Environment Preparation\n",
        "\n",
        "In this step, we will prepare the environment by installing necessary Python libraries. These libraries provide the functionality needed to process and analyze the audio files. Each library has a specific role in the processing pipeline:\n",
        "\n",
        "- **pydub**: This library is used for audio processing tasks such as loading and manipulating audio files.\n",
        "- **scipy**: The scipy library is essential for scientific and technical computing, particularly for performing the Fast Fourier Transform (FFT).\n",
        "- **matplotlib**: This library is used for creating visualizations, such as plotting waveforms and frequency spectrums.\n",
        "- **numpy**: A fundamental package for numerical computations in Python, numpy is used for handling arrays and performing mathematical operations.\n",
        "\n",
        "#### How to Use:\n",
        "\n",
        "1. **Run the Code Cell**:\n",
        "   - Hover over the cell of code below, causing a run (play) button to appear.  Press the run button to run the code. This will ensure that all required Python libraries are installed and ready for use in subsequent steps.\n"
      ],
      "metadata": {
        "id": "AGnlgqt4Bu2p"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Run this code cell first, to ensure that appropriate Python libraries are installed\n",
        "!pip install pydub scipy matplotlib numpy"
      ],
      "metadata": {
        "id": "CVuzKi_Mx8Q_"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "### Step 1: Find Tap Segments in Audio Files\n",
        "\n",
        "In this step, we will process all `.wav` files in the current directory containing repeated sounds of someone tapping a wooden block with a hammer. Each tap sound is detected based on amplitude spikes and extracted into individual 5ms segments. The segments are then normalized and saved as separate `.wav` files along with corresponding waveform images.\n",
        "\n",
        "#### How to Use:\n",
        "\n",
        "1. **Prepare Your Directory**:\n",
        "   - Ensure your `.wav` files are placed in the current working directory of the notebook. This is typically the default directory opened in Google Colab.\n",
        "\n",
        "2. **Run the Code Cell**:\n",
        "   - Execute the code cell below to process the audio files.\n",
        "\n",
        "#### Explanation of Parameters:\n",
        "\n",
        "- **THRESHOLD**: The amplitude threshold for detecting tap sounds. Setting this value higher will make the detection stricter, only detecting louder taps.\n",
        "- **MIN_DISTANCE_MS**: The minimum distance between consecutive spikes in milliseconds. This helps to ensure that each detected tap is distinct.\n",
        "- **PRE_SPIKE_MS**: The time in milliseconds before each detected spike to begin the segment. This helps capture the initial part of the tap sound.\n",
        "- **SEGMENT_LENGTH_MS**: The length of each segment in milliseconds. This defines how long each extracted segment will be.\n",
        "\n",
        "#### Explanation of the Process:\n",
        "\n",
        "The code will first load each `.wav` file and detect the high amplitude spikes representing tap sounds. It will then extract segments starting a short time before each detected spike, normalize the segments for consistent amplitude, and save them as individual `.wav` files along with waveform images. The state of each file is tracked using a JSON file to avoid reprocessing.\n",
        "\n",
        "Normalization ensures that each audio segment has a consistent amplitude level, making it easier to compare segments and analyze the tap sounds accurately.\n"
      ],
      "metadata": {
        "id": "Uj-Nt-WxCYPo"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "KYiaL3SDjDOd"
      },
      "outputs": [],
      "source": [
        "import os\n",
        "import json\n",
        "from pydub import AudioSegment\n",
        "from scipy.io import wavfile\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "\n",
        "# Parameters for tap detection (adjust these values as needed)\n",
        "THRESHOLD = 0.6    # Amplitude threshold for detecting tap sounds\n",
        "MIN_DISTANCE_MS = 100  # Minimum distance between consecutive spikes in milliseconds\n",
        "PRE_SPIKE_MS = 0.2  # Time in milliseconds before each spike to extract segments\n",
        "SEGMENT_LENGTH_MS = 5  # Time in milliseconds for each segment\n",
        "\n",
        "# Function to load a .wav file and return the audio data and sample rate\n",
        "def load_wav(file_path):\n",
        "    sample_rate, data = wavfile.read(file_path)\n",
        "\n",
        "    # Convert stereo to mono if necessary\n",
        "    if len(data.shape) == 2:\n",
        "        data = np.mean(data, axis=1)\n",
        "\n",
        "    return sample_rate, data\n",
        "\n",
        "# Function to detect high amplitude spikes in the audio data\n",
        "def detect_taps(data, sample_rate, threshold=THRESHOLD, min_distance_ms=MIN_DISTANCE_MS):\n",
        "    min_distance = int((min_distance_ms / 1000) * sample_rate)\n",
        "    normalized_data = data / np.max(np.abs(data))\n",
        "    spikes = np.where(normalized_data > threshold)[0]\n",
        "    filtered_spikes = []\n",
        "    last_spike = -min_distance\n",
        "    for spike in spikes:\n",
        "        if spike - last_spike >= min_distance:\n",
        "            filtered_spikes.append(spike)\n",
        "            last_spike = spike\n",
        "    return filtered_spikes\n",
        "\n",
        "# Function to normalize audio segments\n",
        "def normalize_audio(segment):\n",
        "    segment = segment / np.max(np.abs(segment))\n",
        "    return np.int16(segment * 32767)  # Convert to 16-bit PCM\n",
        "\n",
        "# Function to extract segments starting a short time before each detected spike\n",
        "def extract_tap_segments(data, sample_rate, spike_indices, pre_spike_ms=PRE_SPIKE_MS, segment_ms=SEGMENT_LENGTH_MS):\n",
        "    segments = []\n",
        "    pre_spike_samples = int((pre_spike_ms / 1000) * sample_rate)\n",
        "    segment_samples = int((segment_ms / 1000) * sample_rate)\n",
        "    for spike_index in spike_indices:\n",
        "        start = max(0, spike_index - pre_spike_samples)\n",
        "        end = start + segment_samples\n",
        "        segment = data[start:end]\n",
        "        normalized_segment = normalize_audio(segment)\n",
        "        segments.append((start, normalized_segment))\n",
        "    return segments\n",
        "\n",
        "# Function to save each segment as a .wav file and plot waveform images\n",
        "def save_segments(segments, sample_rate, output_folder, base_name):\n",
        "    if not os.path.exists(output_folder):\n",
        "        os.makedirs(output_folder)\n",
        "    for i, (start, segment) in enumerate(segments):\n",
        "        segment_id = f\"{i+1:02d}\"\n",
        "        segment_audio = AudioSegment(\n",
        "            segment.tobytes(),\n",
        "            frame_rate=sample_rate,\n",
        "            sample_width=2,  # 16-bit PCM is 2 bytes\n",
        "            channels=1\n",
        "        )\n",
        "        output_path = f\"{output_folder}/{base_name}_{segment_id}.wav\"\n",
        "        segment_audio.export(output_path, format=\"wav\")\n",
        "\n",
        "        # Convert sample indices to time in milliseconds\n",
        "        times = np.arange(len(segment)) * (1000.0 / sample_rate)\n",
        "\n",
        "        # Plot waveform\n",
        "        plt.figure(figsize=(10, 4))\n",
        "        plt.plot(times, segment, label='Waveform')\n",
        "        plt.title(f'{base_name} {segment_id}: {SEGMENT_LENGTH_MS}ms of amplitude data')\n",
        "        plt.xlabel('Time (ms)')\n",
        "        plt.ylabel('Amplitude')\n",
        "        plt.legend()\n",
        "        plt.savefig(f\"{output_folder}/{base_name}_{segment_id}.png\")\n",
        "        plt.close()\n",
        "        print(f\"Tap {segment_id}: Start Sample Index {start}, Saved to {output_path}\")\n",
        "\n",
        "# Function to load the state mapping from a JSON file\n",
        "def load_state(file_path):\n",
        "    if os.path.exists(file_path):\n",
        "        with open(file_path, 'r') as file:\n",
        "            return json.load(file)\n",
        "    return {}\n",
        "\n",
        "# Function to save the state mapping to a JSON file\n",
        "def save_state(file_path, state):\n",
        "    with open(file_path, 'w') as file:\n",
        "        json.dump(state, file, indent=4)\n",
        "\n",
        "# Function to get the current state of all .wav files in the directory\n",
        "def get_wav_files_state(directory, state_file):\n",
        "    state = load_state(state_file)\n",
        "    wav_files = [f for f in os.listdir(directory) if f.endswith('.wav')]\n",
        "    for wav_file in wav_files:\n",
        "        base_name = os.path.splitext(wav_file)[0]\n",
        "        if base_name not in state:\n",
        "            state[base_name] = {'state': 'non-split'}\n",
        "    return state\n",
        "\n",
        "# Main function to loop through all .wav files and process them\n",
        "def process_directory(directory, state_file):\n",
        "    state = get_wav_files_state(directory, state_file)\n",
        "    for base_name, info in state.items():\n",
        "        if info['state'] == 'non-split':\n",
        "            subdir = os.path.join(directory, base_name)\n",
        "            if not os.path.exists(subdir):\n",
        "                os.makedirs(subdir)\n",
        "                file_path = os.path.join(directory, f\"{base_name}.wav\")\n",
        "                print(f\"Processing {file_path}...\")\n",
        "                sample_rate, data = load_wav(file_path)\n",
        "                spike_indices = detect_taps(data, sample_rate)\n",
        "                segments = extract_tap_segments(data, sample_rate, spike_indices, pre_spike_ms=PRE_SPIKE_MS, segment_ms=SEGMENT_LENGTH_MS)\n",
        "                print(f\"Extracted {len(segments)} segments.\")\n",
        "                save_segments(segments, sample_rate, subdir, base_name)\n",
        "                info['state'] = 'split'\n",
        "                info['segments'] = [f\"{i+1:02d}\" for i in range(len(segments))]\n",
        "                save_state(state_file, state)\n",
        "            else:\n",
        "                print(f\"Skipping {base_name}: Subdirectory already exists.\")\n",
        "    save_state(state_file, state)\n",
        "\n",
        "# Process this step\n",
        "directory = '.'\n",
        "state_file = 'wav_files_state.json'\n",
        "process_directory(directory, state_file)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "### Visualization after Step 1: Interactive Waveform Visualization\n",
        "\n",
        "This interactive tool allows you to explore the waveform of a selected segment by hovering over individual samples to see the amplitude and time data.\n",
        "\n",
        "#### How to Use:\n",
        "\n",
        "1. **Set the Segment Name**:\n",
        "   - Assign a value to the `SEGMENT_NAME` parameter in the code. The segment name should be in the format `base_name_segment_id`, where `base_name` is the name of the original audio file and `segment_id` is the segment number (e.g., `\"sample_01\"`).  Keep the quotation marks in the code!\n",
        "   \n",
        "   **Example**: if the segment name you wish to analyze is called `no_holes_01`, you would change the line of code involving `SEGMENT_NAME` as follows:\n",
        "```python\n",
        "SEGMENT_NAME = \"no_holes_01\"\n",
        "```\n",
        "\n",
        "2. **Run the Code**:\n",
        "   - Execute the code cell to create the interactive visualization for the specified segment.\n",
        "\n",
        "3. **View Interactive Plot**:\n",
        "   - The interactive plot of the selected segment will be displayed. You can hover over the plot to navigate through the samples to see the time (in milliseconds) and amplitude values for each sample.\n",
        "\n",
        "4. **Check Available Segments**:\n",
        "   - If the specified segment name does not exist, the code will display a message and list all available segment names. You can then choose one of the listed segments and update the `SEGMENT_NAME` parameter.\n",
        "\n",
        "By using this tool, you can gain detailed insights into the waveform of each tap sound segment, providing a comprehensive analysis of the audio data.\n",
        "\n",
        "This interactive visualization tool leverages `plotly` to provide a user-friendly interface for exploring the segment data."
      ],
      "metadata": {
        "id": "fsQIntX_btsI"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import os\n",
        "import json\n",
        "import plotly.graph_objs as go\n",
        "import plotly.express as px\n",
        "from scipy.io import wavfile\n",
        "import numpy as np\n",
        "from IPython.display import display, HTML\n",
        "\n",
        "# Set the SEGMENT_NAME parameter to the segment you want to explore\n",
        "SEGMENT_NAME = \"4C_center_01\"  # Replace with the name of the segment to visualize\n",
        "\n",
        "# Function to load the state mapping from a JSON file\n",
        "def load_state(file_path):\n",
        "    if os.path.exists(file_path):\n",
        "        with open(file_path, 'r') as file:\n",
        "            return json.load(file)\n",
        "    return {}\n",
        "\n",
        "# Function to create an interactive plot for a segment\n",
        "def create_interactive_plot(segment, base_name):\n",
        "    subdir = os.path.join('.', base_name)\n",
        "    segment_path = os.path.join(subdir, f\"{base_name}_{segment}.wav\")\n",
        "    sample_rate, data = wavfile.read(segment_path)\n",
        "\n",
        "    # Check if the data is stereo and convert to mono if necessary\n",
        "    if len(data.shape) == 2:\n",
        "        data = np.mean(data, axis=1)\n",
        "\n",
        "    # Normalize the data\n",
        "    normalized_data = data / np.max(np.abs(data))\n",
        "\n",
        "    # Ensure segment length matches expected length\n",
        "    segment_length_ms = 5\n",
        "    expected_samples = int((segment_length_ms / 1000) * sample_rate)\n",
        "    if len(normalized_data) != expected_samples:\n",
        "        print(f\"Warning: Segment length mismatch. Expected {expected_samples} samples, got {len(normalized_data)} samples.\")\n",
        "\n",
        "    # Convert sample indices to time in milliseconds\n",
        "    times = np.arange(len(normalized_data)) * (1000.0 / sample_rate)\n",
        "\n",
        "    # Create the plot\n",
        "    fig = go.Figure()\n",
        "    fig.add_trace(go.Scatter(x=times, y=normalized_data, mode='lines', name='Waveform'))\n",
        "\n",
        "    fig.update_layout(\n",
        "        title=f'{base_name} {segment}: Interactive Waveform',\n",
        "        xaxis_title='Time (ms)',\n",
        "        yaxis_title='Amplitude',\n",
        "        hovermode='x unified'\n",
        "    )\n",
        "\n",
        "    return fig\n",
        "\n",
        "# Function to list available segments\n",
        "def list_available_segments(state):\n",
        "    segments = []\n",
        "    for base_name, info in state.items():\n",
        "        if 'segments' in info and info['state'] == 'split':\n",
        "            segments.extend([f\"{base_name}_{seg}\" for seg in info['segments']])\n",
        "    return segments\n",
        "\n",
        "# Main function to create the interactive plot or list available segments\n",
        "def create_interactive_tool(state_file, segment_name):\n",
        "    state = load_state(state_file)\n",
        "    available_segments = list_available_segments(state)\n",
        "\n",
        "    if segment_name in available_segments:\n",
        "        base_name, segment_id = segment_name.rsplit('_', 1)\n",
        "        fig = create_interactive_plot(segment_id, base_name)\n",
        "        fig.show()\n",
        "    else:\n",
        "        print(f\"Segment '{segment_name}' not found.\")\n",
        "        print(\"To load a segment in this visualizer, copy one of the names below and\\npaste it between the quotes after SEGMENT_NAME = in the code,\\nthen re-run the cell.\")\n",
        "        print(\"Available segments are:\")\n",
        "        for segment in available_segments:\n",
        "            print(segment)\n",
        "\n",
        "# Process this step\n",
        "state_file = 'wav_files_state.json'\n",
        "create_interactive_tool(state_file, SEGMENT_NAME)\n"
      ],
      "metadata": {
        "id": "esko1Tg5bt_o"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "### Step 2: FFT Transformation with Frequency Binning and Top 10 Display\n",
        "\n",
        "In this step, we will perform a Fast Fourier Transform (FFT) on the segmented `.wav` files in the \"split\" state. The FFT will generate a set of amplitudes at different frequencies present in each file. We will then bin these frequencies into defined ranges and save the entire range of bins into individual JSON files for each sub-file. Additionally, we will display the top 10 frequencies in a simplified FFT graph and save these top 10 frequencies in the overall JSON file for the original `.wav` file in the root folder.\n",
        "\n",
        "The segment identifiers will be zero-padded to two digits, starting from 01. The `base_name` will be used as part of the file naming convention for subsequent files. You can adjust the `BIN_WIDTH` parameter to change the width of the frequency bins. The x-axis of the graphs will be limited to a maximum frequency of `MAX_FREQUENCY` Hz to ensure consistency across all visualizations.  It's OK to leave these parameters at their default values, which are 10 and 5000.\n",
        "\n",
        "#### Understanding FFT and Frequency Binning\n",
        "\n",
        "- **Fast Fourier Transform (FFT)**: The FFT is a mathematical algorithm that transforms a time-domain signal into its constituent frequencies. It helps us understand the frequency content of the audio segments. For a deeper understanding, students are encouraged to look up resources on Fourier Transforms and FFT, as there are many excellent explanations available.\n",
        "\n",
        "- **Frequency Binning**: After performing the FFT, the resulting frequencies are grouped into \"bins.\" Binning means combining a range of frequencies into a single value, which helps in simplifying and summarizing the data. In this context, each bin represents a small range of frequencies, and the amplitude values within this range are summed up.  You may notice that the resulting frequencies are the center-point of each bin: for a `BIN_WIDTH` of 10 Hz, the bin centers occur at 5 Hz, 15 Hz, 25 Hz and so forth.\n",
        "\n",
        "By using FFT and frequency binning, we can analyze the frequency characteristics of each tap sound, which is crucial for understanding the acoustic properties of the wooden block being tapped.\n",
        "\n",
        "By running this cell, you are transforming the audio data into the frequency domain, allowing for detailed frequency analysis and visualization of the tap sounds."
      ],
      "metadata": {
        "id": "d7qeiqsR5a0l"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import os\n",
        "import json\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "from scipy.io import wavfile\n",
        "from scipy.fft import fft, fftfreq\n",
        "\n",
        "# Adjustable parameters\n",
        "BIN_WIDTH = 10  # In Hz, how wide the \"bins\" are that subdivide the frequency spectrum\n",
        "MAX_FREQUENCY = 5000  # In Hz, what is the maximum considered frequency\n",
        "\n",
        "# Function to load the state mapping from a JSON file\n",
        "def load_state(file_path):\n",
        "    if os.path.exists(file_path):\n",
        "        with open(file_path, 'r') as file:\n",
        "            return json.load(file)\n",
        "    return {}\n",
        "\n",
        "# Function to save the state mapping to a JSON file\n",
        "def save_state(file_path, state):\n",
        "    with open(file_path, 'w') as file:\n",
        "        json.dump(state, file, indent=4)\n",
        "\n",
        "# Function to get the current state of all .wav files in the directory\n",
        "def get_wav_files_state(directory, state_file):\n",
        "    state = load_state(state_file)\n",
        "    wav_files = [f for f in os.listdir(directory) if f.endswith('.wav')]\n",
        "    for wav_file in wav_files:\n",
        "        base_name = os.path.splitext(wav_file)[0]\n",
        "        if base_name not in state:\n",
        "            state[base_name] = {'state': 'non-split'}\n",
        "    return state\n",
        "\n",
        "# Function to perform FFT and bin frequencies\n",
        "def perform_fft_and_bin(data, sample_rate, bin_width, max_frequency):\n",
        "    # Convert stereo to mono if necessary\n",
        "    if len(data.shape) == 2:\n",
        "        data = np.mean(data, axis=1)\n",
        "\n",
        "    N = len(data)\n",
        "    T = 1.0 / sample_rate\n",
        "    yf = fft(data)\n",
        "    xf = fftfreq(N, T)[:N//2]\n",
        "    amplitudes = 2.0 / N * np.abs(yf[0:N//2])\n",
        "\n",
        "    # Limit frequencies to max_frequency\n",
        "    xf = xf[xf <= max_frequency]\n",
        "    amplitudes = amplitudes[:len(xf)]\n",
        "\n",
        "    # Bin frequencies\n",
        "    bins = np.arange(0, max_frequency + bin_width, bin_width)\n",
        "    binned_amplitudes = []\n",
        "\n",
        "    for i in range(len(bins) - 1):\n",
        "        bin_mask = (xf >= bins[i]) & (xf < bins[i + 1])\n",
        "        binned_amplitudes.append({\n",
        "            'frequency': (bins[i] + bins[i + 1]) / 2,\n",
        "            'amplitude': np.sum(amplitudes[bin_mask])\n",
        "        })\n",
        "\n",
        "    return binned_amplitudes\n",
        "\n",
        "# Function to select top 10 bins\n",
        "def select_top_bins(binned_frequencies, top_n=10):\n",
        "    sorted_bins = sorted(binned_frequencies, key=lambda x: x['amplitude'], reverse=True)\n",
        "    return sorted_bins[:top_n]\n",
        "\n",
        "# Function to plot and save bar graph of top 10 binned frequencies\n",
        "def plot_top_binned_frequencies(binned_frequencies, output_path, bin_width, max_frequency, base_name, segment_id):\n",
        "    frequencies = [item[\"frequency\"] for item in binned_frequencies]\n",
        "    amplitudes = [item[\"amplitude\"] for item in binned_frequencies]\n",
        "\n",
        "    plt.figure(figsize=(10, 6))\n",
        "    bars = plt.bar(frequencies, amplitudes, width=bin_width, color=plt.cm.viridis(np.linspace(0, 1, len(frequencies))))\n",
        "    plt.xlabel('Frequency')\n",
        "    plt.ylabel('Summed Amplitudes')\n",
        "    plt.title(f'{base_name} {segment_id}: Top 10 Binned Frequencies\\n(center point of {bin_width}Hz bin)')\n",
        "    plt.xlim(0, max_frequency)\n",
        "\n",
        "    # Add frequency labels above each bar\n",
        "    for bar, freq in zip(bars, frequencies):\n",
        "        height = bar.get_height()\n",
        "        plt.text(bar.get_x() + bar.get_width() / 2, height, f'{freq:.0f}', ha='center', va='bottom')\n",
        "\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(output_path)\n",
        "    plt.close()\n",
        "\n",
        "# Main function to process the FFT for each segment in the directory\n",
        "def process_fft_and_bin(directory, state_file, bin_width, max_frequency):\n",
        "    state = get_wav_files_state(directory, state_file)\n",
        "    for base_name, info in state.items():\n",
        "        if info['state'] == 'split':\n",
        "            subdir = os.path.join(directory, base_name)\n",
        "            segment_files = [f for f in os.listdir(subdir) if f.endswith('.wav')]\n",
        "            for segment_id in info['segments']:\n",
        "                segment_file = f\"{base_name}_{segment_id}.wav\"\n",
        "                segment_path = os.path.join(subdir, segment_file)\n",
        "                sample_rate, data = wavfile.read(segment_path)\n",
        "                binned_frequencies = perform_fft_and_bin(data, sample_rate, bin_width, max_frequency)\n",
        "                top_binned_frequencies = select_top_bins(binned_frequencies, top_n=10)\n",
        "\n",
        "                # Save the entire range of bins to a .json file\n",
        "                segment_json_path_full = os.path.join(subdir, f\"{base_name}_{segment_id}_full.json\")\n",
        "                with open(segment_json_path_full, 'w') as json_file:\n",
        "                    json.dump(binned_frequencies, json_file, indent=4)\n",
        "                print(f\"Saved {segment_json_path_full}\")\n",
        "\n",
        "                # Save the top 10 binned frequencies to a .json file\n",
        "                segment_json_path_top = os.path.join(subdir, f\"{base_name}_{segment_id}.json\")\n",
        "                with open(segment_json_path_top, 'w') as json_file:\n",
        "                    json.dump(top_binned_frequencies, json_file, indent=4)\n",
        "                print(f\"Saved {segment_json_path_top}\")\n",
        "\n",
        "                # Plot and save the bar graph of the top 10 frequencies\n",
        "                plot_path = os.path.join(subdir, f\"{base_name}_{segment_id}_fft.png\")\n",
        "                plot_top_binned_frequencies(top_binned_frequencies, plot_path, bin_width, max_frequency, base_name, segment_id)\n",
        "                print(f\"Processed FFT for {segment_file}, saved plot to {plot_path}\")\n",
        "\n",
        "            info['state'] = 'fft'\n",
        "            save_state(state_file, state)\n",
        "\n",
        "# Process this step\n",
        "directory = '.'\n",
        "state_file = 'wav_files_state.json'\n",
        "process_fft_and_bin(directory, state_file, BIN_WIDTH, MAX_FREQUENCY)\n"
      ],
      "metadata": {
        "id": "bLO9e4RRxpiL"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "### Visualization After Step 2: Waveforms and Frequencies\n",
        "\n",
        "Run the code cell below to generate an interactive tool that allows you to compare two segments by selecting them from dropdown menus. The tool displays the time-amplitude waveform and FFT plot of the top 10 frequencies for the selected segments, along with a table listing the top 10 frequency bins in descending amplitude order.\n",
        "\n",
        "#### How to Use:\n",
        "\n",
        "\n",
        "\n",
        "1. **Run the Code**:\n",
        "   - Execute the code cell to generate the visualizations for the selected segments.\n",
        "\n",
        "2. **Select Segments**:\n",
        "   - Use the dropdown menus to select the segments you want to compare. The segments are listed in the format `base_name_segment_id`, where `base_name` is the name of the original audio file and `segment_id` is the segment number (e.g., `\"sample_01\"`).  Unlike the first visualization tool, running this tool does not require you to alter the code - the dropdowns allow you to select which file you want to show.\n",
        "   \n",
        "3. **View Visualizations**:\n",
        "   - The tool will display two visualizations for each selected segment:\n",
        "     - **Waveform Plot**: Shows the time-amplitude waveform, allowing you to see the overall shape and intensity of the tap sound.\n",
        "     - **FFT Plot**: Displays the top 10 frequencies present in the segment, showing the most significant frequency components of the tap sound.\n",
        "\n",
        "4. **Compare Segments**:\n",
        "   - Use the visualizations to compare the segments. Look for similarities and differences in the waveforms and frequency plots. Pay attention to:\n",
        "     - **Waveform Shape**: The shape of the waveform can indicate the nature of the tap, such as its intensity and duration.\n",
        "     - **Frequency Components**: The FFT plot reveals the dominant frequencies. Comparing these can help identify consistent patterns or anomalies between segments.\n",
        "\n",
        "#### What to Look For:\n",
        "\n",
        "- **Consistent Patterns**: Identify any recurring frequency peaks that appear in multiple segments. This can indicate common characteristics of the tap sounds.\n",
        "- **Anomalies**: Look for any unusual peaks or variations in the waveforms and frequency plots. These could be due to noise or variations in the tap sound.\n",
        "- **Amplitude Differences**: Compare the amplitude values in the waveforms and FFT plots. Variations in amplitude can provide insights into the intensity and energy of the tap sounds.\n",
        "\n",
        "By using this tool, you can gain a deeper understanding of the acoustic properties of the tap sounds and how they vary between segments. This interactive visualization helps you analyze and compare the frequency content and waveform shapes of different segments, enhancing your ability to interpret and draw conclusions from the data."
      ],
      "metadata": {
        "id": "o_iNn20Ni0sN"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import ipywidgets as widgets\n",
        "from IPython.display import display, HTML\n",
        "from PIL import Image\n",
        "\n",
        "# Function to load segment data\n",
        "def load_segment_data(segment):\n",
        "    base_name, segment_id = segment.rsplit('_', 1)\n",
        "    subdir = os.path.join('.', base_name)\n",
        "    waveform_path = os.path.join(subdir, f\"{segment}.png\")\n",
        "    fft_path = os.path.join(subdir, f\"{segment}_fft.png\")\n",
        "    json_path = os.path.join(subdir, f\"{segment}.json\")\n",
        "\n",
        "    with open(json_path, 'r') as file:\n",
        "        frequencies = json.load(file)\n",
        "\n",
        "    return waveform_path, fft_path, frequencies\n",
        "\n",
        "# Function to create the dropdown widget for selecting segments\n",
        "def create_segment_dropdown(label, segments):\n",
        "    options = [('', '')] + [(segment, segment) for segment in segments]\n",
        "    return widgets.Dropdown(options=options, description=label, style={'description_width': 'initial'})\n",
        "\n",
        "# Function to update the display with selected segment data\n",
        "def update_display(segment, output_widget):\n",
        "    if segment:\n",
        "        waveform_path, fft_path, frequencies = load_segment_data(segment)\n",
        "\n",
        "        waveform_img = Image.open(waveform_path)\n",
        "        fft_img = Image.open(fft_path)\n",
        "\n",
        "        output_widget.clear_output()\n",
        "        with output_widget:\n",
        "            display(waveform_img)\n",
        "            display(fft_img)\n",
        "\n",
        "            # Display frequency table\n",
        "            display(HTML(f'<h3>Top 10 Frequency Bins</h3>'))\n",
        "            table_html = '<table><tr>' + ''.join([f'<th>{bin[\"frequency\"]} Hz</th>' for bin in frequencies]) + '</tr></table>'\n",
        "            # table_html += '<tr>' + ''.join([f'<td>{bin[\"amplitude\"]:.2f}</td>' for bin in frequencies]) + '</tr></table>'\n",
        "            display(HTML(table_html))\n",
        "\n",
        "# Create the interactive tool\n",
        "def create_comparison_tool(state_file):\n",
        "    state = load_state(state_file)\n",
        "    segments = []\n",
        "    for base_name, info in state.items():\n",
        "        if 'segments' in info and (info['state'] == 'fft' or info['state'] == 'profiled'):\n",
        "            segments.extend([f\"{base_name}_{seg}\" for seg in info['segments']])\n",
        "\n",
        "    if not segments:\n",
        "        print(\"No segments in 'fft' or 'profiled' states found.\")\n",
        "        return\n",
        "\n",
        "    dropdown1 = create_segment_dropdown('Segment A', segments)\n",
        "    dropdown2 = create_segment_dropdown('Segment B', segments)\n",
        "\n",
        "    output1 = widgets.Output()\n",
        "    output2 = widgets.Output()\n",
        "\n",
        "    def on_dropdown1_change(change):\n",
        "        if change['type'] == 'change' and change['name'] == 'value':\n",
        "            update_display(change['new'], output1)\n",
        "\n",
        "    def on_dropdown2_change(change):\n",
        "        if change['type'] == 'change' and change['name'] == 'value':\n",
        "            update_display(change['new'], output2)\n",
        "\n",
        "    dropdown1.observe(on_dropdown1_change)\n",
        "    dropdown2.observe(on_dropdown2_change)\n",
        "\n",
        "    # Add padding between the two columns\n",
        "    box = widgets.HBox([widgets.VBox([dropdown1, output1]), widgets.HTML('<h3>vs.</h3>'), widgets.VBox([dropdown2, output2])])\n",
        "    box.layout.justify_content = 'space-between'\n",
        "\n",
        "    display(box)\n",
        "\n",
        "# Process this step\n",
        "state_file = 'wav_files_state.json'\n",
        "create_comparison_tool(state_file)\n"
      ],
      "metadata": {
        "id": "Vz4Z1hYSjCkI"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "### Step 3: Profiling Tap Sounds\n",
        "\n",
        "In this step, we will create a statistically useful \"profile\" of the tap sounds by combining information from all the segments. This involves aggregating the complete set of frequency bins from all segments, applying robust statistical measures to reduce noise and handle outliers, and then saving the full range of frequency bins from the aggregated profile for visualization and analysis.\n",
        "\n",
        "#### What is Profiling?\n",
        "\n",
        "- **Profiling**: This process involves combining data from multiple segments to create a single, representative profile. By aggregating the frequency data from all segments, we can identify consistent patterns and reduce the impact of noise or outliers.\n",
        "- **Goal**: The goal of creating a profile is to have a comprehensive understanding of the frequency characteristics of the tap sounds. This helps in analyzing the acoustic properties and identifying any common features across different segments.\n",
        "\n",
        "#### How to Use:\n",
        "\n",
        "1. **Adjust Parameters**:\n",
        "   - You can adjust the `BIN_WIDTH` and `MAX_FREQUENCY` parameters as needed. These parameters control the width of the frequency bins and the maximum frequency considered during profiling.  It's OK to leave these at their default values, which are 10 and 5000.\n",
        "\n",
        "2. **Run the Code Cell**:\n",
        "   - Execute the code cell below to process the segmented audio files and create profiles for each tap sound.\n",
        "\n",
        "3. **View Output**:\n",
        "   - The code will print a message indicating that each profile is being processed. After all profiles are created, a final message will be displayed.\n",
        "\n",
        "By running this cell, you will generate profiles that summarize the frequency characteristics of each tap sound, which will be useful for further analysis and comparison."
      ],
      "metadata": {
        "id": "G5trJ8pp-UYs"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import os\n",
        "import json\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "\n",
        "# Adjustable parameters\n",
        "BIN_WIDTH = 10  # Hz\n",
        "MAX_FREQUENCY = 5000  # Hz\n",
        "\n",
        "# Function to load the state mapping from a JSON file\n",
        "def load_state(file_path):\n",
        "    if os.path.exists(file_path):\n",
        "        with open(file_path, 'r') as file:\n",
        "            return json.load(file)\n",
        "    return {}\n",
        "\n",
        "# Function to save the state mapping to a JSON file\n",
        "def save_state(file_path, state):\n",
        "    with open(file_path, 'w') as file:\n",
        "        json.dump(state, file, indent=4)\n",
        "\n",
        "# Function to get the current state of all .wav files in the directory\n",
        "def get_wav_files_state(directory, state_file):\n",
        "    state = load_state(state_file)\n",
        "    wav_files = [f for f in os.listdir(directory) if f.endswith('.wav')]\n",
        "    for wav_file in wav_files:\n",
        "        base_name = os.path.splitext(wav_file)[0]\n",
        "        if base_name not in state:\n",
        "            state[base_name] = {'state': 'non-split'}\n",
        "    return state\n",
        "\n",
        "# Function to load frequency bins from sub-files\n",
        "def load_bins(sub_file):\n",
        "    with open(sub_file, 'r') as file:\n",
        "        bins = json.load(file)\n",
        "    return bins\n",
        "\n",
        "# Function to aggregate bins from all sub-files\n",
        "def aggregate_bins(sub_files):\n",
        "    all_bins = []\n",
        "    for sub_file in sub_files:\n",
        "        bins = load_bins(sub_file)\n",
        "        all_bins.append(bins)\n",
        "\n",
        "    aggregated_bins = {}\n",
        "    for bins in all_bins:\n",
        "        for bin in bins:\n",
        "            frequency = bin['frequency']\n",
        "            amplitude = bin['amplitude']\n",
        "            if frequency not in aggregated_bins:\n",
        "                aggregated_bins[frequency] = []\n",
        "            aggregated_bins[frequency].append(amplitude)\n",
        "\n",
        "    return aggregated_bins\n",
        "\n",
        "# Function to create a profile from aggregated bins\n",
        "def create_profile(aggregated_bins):\n",
        "    profile_bins = []\n",
        "    for frequency, amplitudes in aggregated_bins.items():\n",
        "        median_amplitude = np.median(amplitudes)\n",
        "        profile_bins.append({\n",
        "            'frequency': frequency,\n",
        "            'amplitude': median_amplitude\n",
        "        })\n",
        "    # Sort by amplitude and select top 10\n",
        "    profile_bins = sorted(profile_bins, key=lambda x: x['amplitude'], reverse=True)[:10]\n",
        "    return profile_bins\n",
        "\n",
        "# Function to save the profile\n",
        "def save_profile(profile_bins, output_file):\n",
        "    with open(output_file, 'w') as file:\n",
        "        json.dump(profile_bins, file, indent=4)\n",
        "\n",
        "# Function to plot and save bar graph of top 10 binned frequencies\n",
        "def plot_top_binned_frequencies(binned_frequencies, output_path, bin_width, max_frequency, base_name):\n",
        "    frequencies = [item[\"frequency\"] for item in binned_frequencies]\n",
        "    amplitudes = [item[\"amplitude\"] for item in binned_frequencies]\n",
        "\n",
        "    plt.figure(figsize=(10, 6))\n",
        "    bars = plt.bar(frequencies, amplitudes, width=bin_width, color=plt.cm.viridis(np.linspace(0, 1, len(frequencies))))\n",
        "    plt.xlabel('Frequency')\n",
        "    plt.ylabel('Amplitude')\n",
        "    plt.title(f'{base_name}: Top 10 Binned Frequencies')\n",
        "    plt.xlim(0, max_frequency)\n",
        "\n",
        "    # Add frequency labels above each bar\n",
        "    for bar, freq in zip(bars, frequencies):\n",
        "        height = bar.get_height()\n",
        "        plt.text(bar.get_x() + bar.get_width() / 2, height, f'{freq:.0f}', ha='center', va='bottom')\n",
        "\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(output_path)\n",
        "    plt.close()\n",
        "\n",
        "# Main function to profile the tap sounds\n",
        "def profile_tap_sounds(directory, state_file, bin_width, max_frequency):\n",
        "    state = get_wav_files_state(directory, state_file)\n",
        "    for base_name, info in state.items():\n",
        "        if info['state'] == 'fft':\n",
        "            subdir = os.path.join(directory, base_name)\n",
        "            segment_files = [os.path.join(subdir, f) for f in os.listdir(subdir) if f.endswith('_full.json')]\n",
        "\n",
        "            print(f\"Processing profile for {base_name}...\")\n",
        "            if len(segment_files) == 0:\n",
        "                print(f\"No segment JSON files found in {subdir}.\")\n",
        "                continue\n",
        "\n",
        "            aggregated_bins = aggregate_bins(segment_files)\n",
        "            profile_bins = create_profile(aggregated_bins)\n",
        "\n",
        "            # Save the profile\n",
        "            profile_path = os.path.join(subdir, f\"{base_name}_profile.json\")\n",
        "            save_profile(profile_bins, profile_path)\n",
        "\n",
        "            # Plot and save the bar graph\n",
        "            plot_path = os.path.join(subdir, f\"{base_name}_profile.png\")\n",
        "            plot_top_binned_frequencies(profile_bins, plot_path, bin_width, max_frequency, base_name)\n",
        "            print(f\"Profiled tap sounds for {base_name}, saved profile to {profile_path} and plot to {plot_path}\")\n",
        "\n",
        "            info['state'] = 'profiled'\n",
        "            save_state(state_file, state)\n",
        "\n",
        "    print(\"All profiles have been created.\")\n",
        "\n",
        "# Process this step\n",
        "directory = '.'\n",
        "state_file = 'wav_files_state.json'\n",
        "profile_tap_sounds(directory, state_file, BIN_WIDTH, MAX_FREQUENCY)\n"
      ],
      "metadata": {
        "id": "PJEPtUFk_-Bq"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "### Visualization after Step 3: Compare Profiles\n",
        "\n",
        "This interactive tool allows you to compare the frequency profile FFT images of different tap sound profiles. You can select two profiles from the dropdown menus, and the tool will display their respective FFT images side by side for easy comparison.\n",
        "\n",
        "#### How to Use:\n",
        "\n",
        "1. **Run the Code**:\n",
        "   - Execute the code cell to initialize the interactive visualization tool.\n",
        "\n",
        "2. **Select Profiles**:\n",
        "   - Use the dropdown menus labeled \"Profile A\" and \"Profile B\" to select the profiles you want to compare. The available profiles are based on the root-level audio files that have been processed and profiled.\n",
        "\n",
        "3. **View Comparison**:\n",
        "   - The FFT images of the selected profiles will be displayed side by side. This allows you to visually compare the frequency characteristics of the tap sounds from different profiles.\n",
        "\n",
        "By using this tool, you can gain insights into the similarities and differences between the frequency profiles of various tap sounds.\n"
      ],
      "metadata": {
        "id": "WQ14LcR-yW_8"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import os\n",
        "import json\n",
        "import ipywidgets as widgets\n",
        "from IPython.display import display, HTML\n",
        "from PIL import Image\n",
        "\n",
        "# Function to load profile data\n",
        "def load_profile_data(profile):\n",
        "    subdir = os.path.join('.', profile)\n",
        "    fft_path = os.path.join(subdir, f\"{profile}_profile.png\")\n",
        "\n",
        "    return fft_path\n",
        "\n",
        "# Function to create the dropdown widget for selecting profiles\n",
        "def create_profile_dropdown(label, profiles):\n",
        "    options = [('', '')] + [(profile, profile) for profile in profiles]\n",
        "    return widgets.Dropdown(options=options, description=label, style={'description_width': 'initial'})\n",
        "\n",
        "# Function to update the display with selected profile data\n",
        "def update_display(profile, output_widget):\n",
        "    if profile:\n",
        "        fft_path = load_profile_data(profile)\n",
        "\n",
        "        fft_img = Image.open(fft_path)\n",
        "\n",
        "        output_widget.clear_output()\n",
        "        with output_widget:\n",
        "            display(fft_img)\n",
        "\n",
        "# Create the interactive tool\n",
        "def create_comparison_tool(state_file):\n",
        "    state = load_state(state_file)\n",
        "    profiles = [base_name for base_name, info in state.items() if info['state'] == 'profiled']\n",
        "\n",
        "    if not profiles:\n",
        "        print(\"No profiles in 'profiled' state found.\")\n",
        "        return\n",
        "\n",
        "    dropdown1 = create_profile_dropdown('Profile A', profiles)\n",
        "    dropdown2 = create_profile_dropdown('Profile B', profiles)\n",
        "\n",
        "    output1 = widgets.Output()\n",
        "    output2 = widgets.Output()\n",
        "\n",
        "    def on_dropdown1_change(change):\n",
        "        if change['type'] == 'change' and change['name'] == 'value':\n",
        "            update_display(change['new'], output1)\n",
        "\n",
        "    def on_dropdown2_change(change):\n",
        "        if change['type'] == 'change' and change['name'] == 'value':\n",
        "            update_display(change['new'], output2)\n",
        "\n",
        "    dropdown1.observe(on_dropdown1_change)\n",
        "    dropdown2.observe(on_dropdown2_change)\n",
        "\n",
        "    # Add padding between the two columns\n",
        "    box = widgets.HBox([widgets.VBox([dropdown1, output1]), widgets.HTML('<h3>vs.</h3>'), widgets.VBox([dropdown2, output2])])\n",
        "    box.layout.justify_content = 'space-between'\n",
        "\n",
        "    display(box)\n",
        "\n",
        "# Process this step\n",
        "state_file = 'wav_files_state.json'\n",
        "create_comparison_tool(state_file)\n"
      ],
      "metadata": {
        "id": "s32VES6hdMQT"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "### Reset Environment\n",
        "\n",
        "This cell will reset the environment by deleting the subdirectories related to the `.wav` files in the root directory and also deleting the JSON state file. This allows for testing the processing cells in order without deleting and re-creating the runtime.  **Running the code cell below is optional** - it's mostly useful if you want to start over with different parameters like threshold, bin size or segment length.  Note that **this cell does not delete the original sound files** - if you want to use it to start over with a different set of sound files, you will need to delete and/or upload sound files to the root Files folder in Google Colab before re-running Step 1."
      ],
      "metadata": {
        "id": "CwMmPYyozfOq"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import os\n",
        "import shutil\n",
        "\n",
        "# Function to get the current state of all .wav files in the directory\n",
        "def get_wav_files_state(directory, state_file):\n",
        "    state = {}\n",
        "    wav_files = [f for f in os.listdir(directory) if f.endswith('.wav')]\n",
        "    for wav_file in wav_files:\n",
        "        base_name = os.path.splitext(wav_file)[0]\n",
        "        state[base_name] = {'state': 'non-split'}\n",
        "    return state\n",
        "\n",
        "# Function to reset the environment\n",
        "def reset_environment(directory, state_file):\n",
        "    state = get_wav_files_state(directory, state_file)\n",
        "    for base_name in state.keys():\n",
        "        subdir = os.path.join(directory, base_name)\n",
        "        if os.path.exists(subdir):\n",
        "            shutil.rmtree(subdir)\n",
        "            print(f\"Deleted directory: {subdir}\")\n",
        "    if os.path.exists(state_file):\n",
        "        os.remove(state_file)\n",
        "        print(f\"Deleted JSON file: {state_file}\")\n",
        "\n",
        "# Process this step\n",
        "directory = '.'\n",
        "state_file = 'wav_files_state.json'\n",
        "reset_environment(directory, state_file)\n"
      ],
      "metadata": {
        "id": "e9DwbXiqzgBD"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "## Running this Notebook in Non-Colab Environments\n",
        "\n",
        "If you need to run this notebook in an environment other than Google Colab, such as Jupyter Notebook or JupyterLab, follow these steps:\n",
        "\n",
        "### Prerequisites\n",
        "\n",
        "Ensure you have Python and Jupyter Notebook or JupyterLab installed on your system. You can install Jupyter using `pip` if it's not already installed:\n",
        "\n",
        "```sh\n",
        "pip install notebook\n",
        "```\n",
        "\n",
        "### Opening the Notebook\n",
        "\n",
        "1. **Launch Jupyter Notebook or JupyterLab:**\n",
        "   Open a terminal or command prompt and run:\n",
        "   ```sh\n",
        "   jupyter notebook\n",
        "   ```\n",
        "   or\n",
        "   ```sh\n",
        "   jupyter lab\n",
        "   ```\n",
        "\n",
        "2. **Upload the Notebook:**\n",
        "   - In the Jupyter interface, navigate to the directory where you want to work.\n",
        "   - Click the \"Upload\" button and select the `.ipynb` file (e.g., `Tap Testing Day 2 Code.ipynb`).\n",
        "   - Click \"Upload\" again to confirm.\n",
        "\n",
        "3. **Open the Notebook:**\n",
        "   - Click on the uploaded notebook file to open it.\n",
        "\n",
        "### Installing Required Libraries\n",
        "\n",
        "Google Colab comes with many pre-installed libraries, but other Jupyter environments might not have these by default. You'll need to install them manually. The necessary libraries for this notebook include `numpy`, `matplotlib`, `ipywidgets`, `IPython`, `scipy`, and `pydub`.\n",
        "\n",
        "Open a new code cell at the beginning of the notebook and run the following command to install all required libraries:\n",
        "\n",
        "```python\n",
        "!pip install numpy matplotlib ipywidgets IPython scipy pydub\n",
        "```\n",
        "\n",
        "### Running the Cells\n",
        "\n",
        "1. **Run the Setup Cell:**\n",
        "   - Ensure that the setup cell, which installs any missing libraries, is executed first to install any required packages.\n",
        "   \n",
        "2. **Run the Code Cells:**\n",
        "   - Follow the instructions within the notebook to run each cell in sequence.\n",
        "   - Adjust the sliders and parameters as instructed, and press the **Show** button to generate the visualizations and play the audio.\n",
        "\n",
        "### Differences in Library Installations\n",
        "\n",
        "- **Google Colab:**\n",
        "  - Comes pre-installed with many scientific and data analysis libraries.\n",
        "  - Automatically manages dependencies and updates.\n",
        "\n",
        "- **Jupyter Notebook/JupyterLab:**\n",
        "  - Requires manual installation of libraries using `pip` or `conda`.\n",
        "  - May need specific versions of libraries to match the notebook's requirements.\n",
        "\n",
        "By following these steps, you can successfully run the `Tap Testing Day 2 Code.ipynb` notebook in any Jupyter environment. If you encounter any issues with missing libraries, ensure they are installed using the `pip install` command mentioned above.\n"
      ],
      "metadata": {
        "id": "57MQcAV4e2DI"
      }
    }
  ]
}