├── icon.ico ├── requirements.txt ├── .gitignore ├── demo ├── Audio-Capture-App-v1.0.0_M34NcRNW4H.png └── Audio-Capture-App-v1.0.0_M34NcRNW4K.PNG ├── LICENSE.md ├── README.md └── app.py /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfrBernard/audio-capture-app/HEAD/icon.ico -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfrBernard/audio-capture-app/HEAD/requirements.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | *.wav 4 | .vscode/ 5 | .env 6 | venv/ 7 | config.txt 8 | build/ 9 | dist/ 10 | app.spec -------------------------------------------------------------------------------- /demo/Audio-Capture-App-v1.0.0_M34NcRNW4H.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfrBernard/audio-capture-app/HEAD/demo/Audio-Capture-App-v1.0.0_M34NcRNW4H.png -------------------------------------------------------------------------------- /demo/Audio-Capture-App-v1.0.0_M34NcRNW4K.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfrBernard/audio-capture-app/HEAD/demo/Audio-Capture-App-v1.0.0_M34NcRNW4K.PNG -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | ### Copyright (c) 2024 C. Bernard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Audio Capture Tool 2 | 3 | This tool was developed to help with **organizing**, **sorting**, and **capturing improvised audio recordings**. It allows users to listen to audio input from a selected device, visualize it in real-time with a spectrometer, and easily capture short audio snippets with user-defined filenames and durations. It also provides a convenient way to manage saved captures and configure the save directory. 4 | 5 | [**Download the latest version here**](https://github.com/cfrBernard/audio-capture-app/releases) 6 | 7 | [![Version](https://img.shields.io/badge/version-v1.0.0-blue)](https://github.com/cfrBernard/audio-capture-app/releases) 8 | [![License](https://img.shields.io/github/license/cfrBernard/MaskMapWizard)](./LICENSE.md) 9 | 10 | ## Features 11 | 12 | - **Device Selection**: Easily select an input audio device from a dropdown menu. 13 | - **Listening Control**: Start and stop audio input listening with a single click. 14 | - **Real-Time Visualization**: Display audio levels visually using a dynamic spectrometer. 15 | - **Audio Capture**: 16 | - Capture audio snippets with a custom filename. 17 | - Configure capture duration (1–300 seconds). 18 | - Avoid overwriting existing files with filename validation. 19 | - **Save Directory Configuration**: 20 | - Choose a directory for saving audio captures. 21 | - Save the configuration persistently across sessions. 22 | - **Error Handling**: Intuitive error messages and feedback for invalid input or missing configurations. 23 | 24 | --- 25 | 26 |
27 | 28 |

29 | Screenshot 1 30 |                 31 | Screenshot 2 32 |

33 | 34 |
35 | 36 | --- 37 | 38 | ## 🛠️ Getting Started 39 | 40 | ### Prerequisites 41 | 42 | - **Python 3.8+** 43 | - **Required Libraries**: 44 | - `tkinter`: GUI framework (comes pre-installed with Python). 45 | - `sounddevice`: For audio input. 46 | - `numpy`: For numerical operations. 47 | - `wave`: To save audio files in WAV format. 48 | 49 | You can install the dependencies with the following command: 50 | 51 | ```bash 52 | pip install -r requirements.txt 53 | ``` 54 | 55 | > Note: Using a .venv is highly recommended. 56 | 57 | ### Running the Application 58 | 59 | 1. Clone this repository or download the source code. 60 | 2. Open a terminal and navigate to the project directory. 61 | 3. Run the application using: 62 | 63 | ```bash 64 | python app.py 65 | ``` 66 | 67 | --- 68 | 69 | ## How to Use 70 | 71 | ### Selecting an Input Device 72 | 73 | - Choose an input device from the dropdown list at the top of the application window. 74 | - Only devices with input capabilities are listed. 75 | - Press **Start Listening** to begin monitoring the audio input. 76 | 77 | ### Capturing Audio 78 | 79 | 1. Click the **Capture** button to open the capture configuration dialog. 80 | 2. Enter a filename (without the extension). If no filename is provided, the app generates one automatically. 81 | 3. Specify the duration of the capture (1–300 seconds). 82 | 4. The file is saved in the configured save directory. 83 | 84 | ### Managing the Save Directory 85 | 86 | - The current save directory is displayed on the **Save Directory** button. 87 | - Click the button to change the directory. 88 | - The selection is saved persistently in a `config.txt` file. 89 | 90 | ### Real-Time Visualization 91 | 92 | - A spectrometer displays the audio input levels in real-time. 93 | 94 | --- 95 | 96 | ## File Structure 97 | 98 | ```bash 99 | . 100 | ├── app.py # Main application code 101 | ├── config.txt # Configuration file for save directory 102 | ├── requirements.txt # List of dependencies 103 | ├── LICENSE.md # MIT License 104 | └── README.md # Documentation for the project 105 | ``` 106 | 107 | --- 108 | 109 | ## ⚠️ Troubleshooting 110 | 111 | ### No Input Devices Listed: 112 | 113 | - Ensure that your audio input device (e.g., microphone) is connected and enabled. 114 | - Restart the application. 115 | 116 | ### Save Directory Issues: 117 | 118 | - The app defaults to the current directory if no directory is selected. 119 | - Ensure you have write permissions for the chosen directory. 120 | 121 | --- 122 | 123 | ## 🔮 Future Enhancements 124 | 125 | - Add MIDI support (for input & export). 126 | - Add support for stereo audio input. 127 | - Enhance the spectrometer with frequency domain analysis. 128 | - Implement additional audio file formats (e.g., MP3). 129 | - Add playback functionality for captured files directly within the app. 130 | 131 | ## License 132 | 133 | This project is licensed under the MIT License. See the [LICENSE](./LICENSE.md) file for details. 134 | 135 | --- 136 | 137 | > **Note**: A MacOS version will be released in the future. 138 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk, simpledialog, messagebox, filedialog 3 | import sounddevice as sd 4 | import numpy as np 5 | import wave 6 | import os 7 | from datetime import datetime 8 | 9 | # Filepath to the config file 10 | config_file_path = 'config.txt' 11 | 12 | # Default save directory (the current directory of the application) 13 | default_save_directory = os.getcwd() 14 | 15 | # Function to read the config file and load the save directory 16 | def load_config(): 17 | global save_directory 18 | save_directory = default_save_directory # Set default directory at the beginning 19 | 20 | if os.path.exists(config_file_path): 21 | with open(config_file_path, 'r') as f: 22 | lines = f.readlines() 23 | for line in lines: 24 | if line.startswith('save_directory='): 25 | save_directory = line.strip().split('=')[1] 26 | break 27 | 28 | # Update the label to display the correct path 29 | save_directory_button.config(text=f"Save Directory: {save_directory}") 30 | 31 | # Function to write the save directory to the config file 32 | def save_config(): 33 | with open(config_file_path, 'w') as f: 34 | f.write(f"save_directory={save_directory}\n") 35 | 36 | # Function to retrieve valid input devices 37 | def get_input_devices(): 38 | devices = sd.query_devices() 39 | input_devices = [] 40 | 41 | # Add only valid devices 42 | for device in devices: 43 | if device['max_input_channels'] > 0 and "Microsoft Sound Mapper - Input" not in device['name']: 44 | input_devices.append(device['name']) # Add only valid devices 45 | return input_devices 46 | 47 | # Global variables 48 | listening = False 49 | circular_buffer = None 50 | stream = None 51 | save_directory = None 52 | 53 | # Function to log messages 54 | def log_message(message): 55 | log_text.config(state=tk.NORMAL) # Enable text box to insert text 56 | log_text.insert(tk.END, message + '\n') 57 | log_text.yview(tk.END) # Scroll to the latest message 58 | log_text.config(state=tk.DISABLED) # Disable text box after insertion 59 | 60 | # Function to start listening 61 | def start_listening(): 62 | global listening, circular_buffer, stream 63 | # Check if a device is selected 64 | if not device_combobox.get(): 65 | log_message("Error: Please select an input device.") 66 | return 67 | 68 | try: 69 | listening = True 70 | pastille_canvas.itemconfig(pastille, fill="red") # Visual indicator (red dot) 71 | selected_device = device_combobox.get() 72 | device_index = next(i for i, d in enumerate(sd.query_devices()) if d['name'] == selected_device) 73 | sd.default.device = device_index 74 | sd.default.samplerate = 44100 75 | sd.default.channels = 1 76 | 77 | buffer_size = int(44100 * 300) # 300 seconds 78 | circular_buffer = np.zeros(buffer_size, dtype=np.float32) 79 | 80 | def audio_callback(indata, frames, time, status): 81 | global circular_buffer 82 | if status: 83 | log_message(f"Status: {status}") 84 | # Add new data to the circular buffer 85 | circular_buffer[:-frames] = circular_buffer[frames:] 86 | circular_buffer[-frames:] = indata[:, 0] 87 | 88 | # Start the audio stream 89 | stream = sd.InputStream(callback=audio_callback) 90 | stream.start() 91 | log_message("Listening has started.") 92 | except Exception as e: 93 | log_message(f"Error starting listening: {e}") 94 | 95 | # Function to stop listening 96 | def stop_listening(): 97 | global listening, stream 98 | if listening: 99 | listening = False 100 | pastille_canvas.itemconfig(pastille, fill="gray") # Reset visual indicator (gray dot) 101 | if stream is not None: 102 | stream.stop() # Stop the stream 103 | stream.close() # Close the stream 104 | log_message("Listening has stopped.") 105 | else: 106 | log_message("No listening session is currently active.") 107 | 108 | # Function to select the directory for saving captures 109 | def select_save_directory(): 110 | global save_directory 111 | directory = filedialog.askdirectory(title="Select Folder for Captures") 112 | if directory: # Only update if the user selects a directory 113 | save_directory = directory 114 | save_directory_button.config(text=f"Save Directory: {save_directory}") 115 | log_message(f"Capture files will be saved to: {save_directory}") 116 | save_config() # Save the new directory to the config file 117 | 118 | def capture_audio(): 119 | global listening, circular_buffer, stream, save_directory 120 | if not listening: 121 | log_message("Error: Listening has not started.") 122 | return 123 | 124 | # Pause listening while configuring the capture 125 | log_message("Pausing listening for capture configuration...") 126 | stop_listening() 127 | 128 | # Prompt for the filename 129 | filename = simpledialog.askstring("File Name", "Enter the file name:", 130 | initialvalue=f"capture_{datetime.now().strftime('%Y%m%d_%H%M%S')}") 131 | if not filename: # Cancel pressed or empty input 132 | log_message("Capture canceled by the user.") 133 | start_listening() 134 | return 135 | 136 | # Ensure the extension 137 | if not filename.lower().endswith('.wav'): 138 | filename += '.wav' 139 | 140 | # Check save directory 141 | if not save_directory: 142 | save_directory = default_save_directory 143 | log_message(f"No directory selected. Using default directory: {save_directory}") 144 | 145 | # Generate the full file path 146 | file_path = os.path.join(save_directory, filename) 147 | 148 | # Check for overwrites 149 | if os.path.exists(file_path): 150 | overwrite = messagebox.askyesno("File exists", f"The file {filename} already exists. Do you want to overwrite it?") 151 | if not overwrite: 152 | log_message("Capture aborted: file not overwritten.") 153 | start_listening() 154 | return 155 | 156 | # Prompt for duration 157 | duration = simpledialog.askinteger("Capture Duration", "Capture duration (in seconds, 1-300):", 158 | minvalue=1, maxvalue=300) 159 | if not duration: # Cancel pressed 160 | log_message("Capture canceled.") 161 | start_listening() 162 | return 163 | 164 | # Retrieve and save data 165 | try: 166 | sample_rate = 44100 167 | num_samples = duration * sample_rate 168 | captured_audio = circular_buffer[-num_samples:] 169 | 170 | with wave.open(file_path, 'wb') as wf: 171 | wf.setnchannels(1) 172 | wf.setsampwidth(2) 173 | wf.setframerate(sample_rate) 174 | wf.writeframes((captured_audio * 32767).astype(np.int16)) 175 | log_message(f"Capture saved as {file_path}") 176 | except Exception as e: 177 | log_message(f"Error saving the recording: {e}") 178 | 179 | # Resume listening after capture dialog closes 180 | log_message("Resuming listening...") 181 | start_listening() 182 | 183 | 184 | # Function to update the visual spectrum (simple amplitude-based display) 185 | def update_spectrometer(): 186 | global circular_buffer 187 | if listening and circular_buffer is not None: 188 | # Get the latest data from the circular buffer 189 | data = circular_buffer[-2048:] # Take the last 2048 samples for better sensitivity 190 | 191 | # Calculate the amplitude (root mean square of the signal) 192 | amplitude = np.sqrt(np.mean(data**2)) 193 | 194 | # Update the spectrometer bars based on the amplitude 195 | # Adjust the threshold for each bar based on the intensity 196 | for i in range(len(spectrometer_bars)): 197 | # More sensitive display 198 | if amplitude > (i + 1) * 0.005: # Lower threshold for even greater sensitivity 199 | spectrometer_bars[i].config(height=10) # Show bar (height 10) 200 | else: 201 | spectrometer_bars[i].config(height=1) # Hide bar (height 1) 202 | 203 | # Call the function again after 50ms to update the spectrometer in real-time 204 | root.after(50, update_spectrometer) 205 | 206 | # Setup the main window 207 | root = tk.Tk() 208 | root.title("Audio capture app") 209 | root.geometry("350x500") 210 | 211 | # Title label 212 | label = tk.Label(root, text="Select an Audio Input Device", font=("Arial", 13)) 213 | label.pack(pady=10) 214 | 215 | # Frame for input device selection 216 | device_frame = tk.Frame(root) 217 | device_frame.pack(pady=10) 218 | 219 | # ComboBox for selecting the device 220 | device_combobox = ttk.Combobox(device_frame, width=35) 221 | device_combobox.grid(row=0, column=0, padx=10) 222 | 223 | # Initialize the list of devices 224 | input_devices = get_input_devices() 225 | device_combobox['values'] = input_devices 226 | if input_devices: 227 | device_combobox.current(0) 228 | 229 | # Frame for listening control 230 | control_frame = tk.Frame(root) 231 | control_frame.pack(pady=20) 232 | 233 | # Start Listening button 234 | start_button = tk.Button(control_frame, text="Start Listening", command=start_listening) 235 | start_button.grid(row=0, column=0, padx=10) 236 | 237 | # Canvas for visual indicator (red dot) 238 | pastille_canvas = tk.Canvas(control_frame, width=20, height=20) 239 | pastille = pastille_canvas.create_oval(5, 5, 15, 15, fill="gray") 240 | pastille_canvas.grid(row=0, column=1, padx=10) 241 | 242 | # Stop Listening button 243 | stop_button = tk.Button(control_frame, text="Stop Listening", command=stop_listening) 244 | stop_button.grid(row=0, column=2, padx=10) 245 | 246 | # Spectrometer container frame with fixed dimensions 247 | spectrometer_container = tk.Frame(root, height=25, width=350,) 248 | spectrometer_container.pack_propagate(False) # Prevents resizing 249 | spectrometer_container.pack(pady=20) 250 | 251 | # Inner canvas for the spectrometer 252 | spectrometer_canvas = tk.Canvas(spectrometer_container, height=25, width=350) 253 | spectrometer_canvas.pack(fill=tk.BOTH, expand=True) 254 | 255 | # Inner frame for spectrometer bars 256 | spectrometer_frame = tk.Frame(spectrometer_canvas, height=25, width=350) 257 | spectrometer_frame.pack_propagate(False) # Prevent the frame from resizing to its children 258 | spectrometer_canvas.create_window((0, 0), window=spectrometer_frame, anchor="nw") 259 | 260 | # Define the size and layout for bars 261 | spectrometer_bars = [] 262 | num_bars = 80 263 | bar_width = 5 264 | bar_spacing = 1 265 | bar_height = 1 # Minimum height 266 | 267 | for i in range(num_bars): 268 | bar = tk.Canvas( 269 | spectrometer_frame, 270 | width=bar_width, 271 | height=bar_height, 272 | bg="black", 273 | highlightthickness=0 # Remove border 274 | ) 275 | bar.place(x=i * (bar_width + bar_spacing), y=0) # Position bar 276 | spectrometer_bars.append(bar) 277 | 278 | # Add capture button and save directory button 279 | capture_button = tk.Button(root, text="Capture", command=capture_audio) 280 | capture_button.pack(pady=0) 281 | 282 | save_directory_button = tk.Button(root, text="Save Directory: ", command=select_save_directory) 283 | save_directory_button.pack(pady=30) 284 | 285 | # Log window for feedback 286 | log_text = tk.Text(root, height=10, width=60, state=tk.DISABLED) 287 | log_text.pack(pady=10) 288 | 289 | # Initialize spectrometer update loop 290 | update_spectrometer() 291 | 292 | # Load the configuration file 293 | load_config() 294 | 295 | # Start the Tkinter main loop 296 | root.mainloop() 297 | --------------------------------------------------------------------------------