AyushS9020 commited on
Commit
c33a694
·
verified ·
1 Parent(s): 5f8f660

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +281 -0
app.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import streamlit as st
3
+ import librosa
4
+ import numpy as np
5
+ from concurrent.futures import ProcessPoolExecutor
6
+ from docx import Document
7
+ from datetime import datetime
8
+ from io import BytesIO
9
+ import matplotlib.pyplot as plt
10
+ import librosa.display
11
+ import tempfile
12
+ from docx.shared import Inches
13
+ import os
14
+
15
+ # Function to find the offset between master and sample segments
16
+ def find_offset(master_segment, sample_segment, sr):
17
+ correlation = np.correlate(master_segment, sample_segment, mode='full')
18
+ max_corr_index = np.argmax(correlation)
19
+ offset_samples = max_corr_index - len(sample_segment) + 1
20
+ offset_ms = (offset_samples / sr) * 1000 # Convert to milliseconds
21
+ return offset_ms
22
+
23
+ # Process segment data function
24
+ def process_segment_data(args):
25
+ interval, master, sample, sr_master, segment_length = args
26
+ start = interval * sr_master
27
+ end = start + segment_length * sr_master # Segment of defined length
28
+ if end <= len(master) and end <= len(sample):
29
+ master_segment = master[start:end]
30
+ sample_segment = sample[start:end]
31
+ offset = find_offset(master_segment, sample_segment, sr_master)
32
+ return (interval // 60, offset)
33
+ return None
34
+
35
+ # Function to generate DOCX with results
36
+ def generate_docx(results, intervals, dropouts, plots):
37
+ doc = Document()
38
+
39
+ # Add introductory text with today's date
40
+ specified_date = datetime.now().strftime("%Y-%m-%d")
41
+ doc.add_heading(f"Audio Sync Results - {specified_date}", 0)
42
+
43
+ # List the names of the devices
44
+ device_names = [name for name in results.keys()]
45
+ doc.add_paragraph(f"Devices compared: {', '.join(device_names)}\n")
46
+
47
+ # Add a table with results
48
+ table = doc.add_table(rows=1, cols=len(device_names) + 1)
49
+ hdr_cells = table.rows[0].cells
50
+ hdr_cells[0].text = 'Time (mins)'
51
+ for i, device_name in enumerate(device_names):
52
+ hdr_cells[i + 1].text = device_name
53
+
54
+ # Fill the table with intervals and results
55
+ for interval in intervals:
56
+ row_cells = table.add_row().cells
57
+ row_cells[0].text = f"{interval // 60} mins" # Convert seconds to minutes
58
+ for i, sample_name in enumerate(device_names):
59
+ result = next((offset for (intv, offset) in results[sample_name] if intv == interval // 60), None)
60
+ row_cells[i + 1].text = f"{result:.2f} ms" if result is not None else "N/A"
61
+
62
+ # Add a section for dropouts
63
+ doc.add_heading("Detected Dropouts", 1)
64
+ for device_name, device_dropouts in dropouts.items():
65
+ doc.add_paragraph(f"Dropouts for {device_name}:")
66
+ for dropout in device_dropouts:
67
+ start, end, duration_ms = dropout
68
+ doc.add_paragraph(f"Start: {format_time(start)} | End: {format_time(end)} | Duration: {duration_ms:.0f} ms")
69
+
70
+ # Add the plot
71
+ doc.add_picture(plots[device_name], width=Inches(6))
72
+
73
+ # Add a comments section
74
+ doc.add_paragraph("\nResults and Comments:\n")
75
+
76
+ return doc
77
+
78
+ # Function to format time in HH:MM:SS
79
+ def format_time(seconds):
80
+ hours = int(seconds // 3600)
81
+ minutes = int((seconds % 3600) // 60)
82
+ secs = seconds % 60
83
+ return f'{hours:02d}:{minutes:02d}:{secs:06.3f}'
84
+
85
+ # Function to detect audio dropouts
86
+ def detect_dropouts(file_path, dropout_db_threshold=-20, min_duration_ms=100):
87
+ # Load the audio file
88
+ y, sr = librosa.load(file_path, sr=None)
89
+
90
+ # Improved time resolution by reducing hop length
91
+ hop_length = 256 # Reduced hop length for better time resolution
92
+ frame_length = hop_length / sr * 1000 # ms per frame
93
+
94
+ # Convert the signal to decibels
95
+ rms = librosa.feature.rms(y=y, frame_length=hop_length, hop_length=hop_length)
96
+ rms_db = librosa.power_to_db(rms, ref=np.max)
97
+
98
+ # Threshold to find dropouts (segments below the dropout_db_threshold)
99
+ dropout_frames = rms_db[0] < dropout_db_threshold
100
+
101
+ # Detect contiguous frames of dropouts lasting at least min_duration_ms
102
+ min_frames = int(min_duration_ms / frame_length)
103
+ dropouts = []
104
+ start = None
105
+
106
+ for i, is_dropout in enumerate(dropout_frames):
107
+ if is_dropout and start is None:
108
+ start = i # Start of a dropout
109
+ elif not is_dropout and start is not None:
110
+ if i - start >= min_frames:
111
+ start_time = start * hop_length / sr
112
+ end_time = i * hop_length / sr
113
+ duration_ms = (end_time - start_time) * 1000 # Convert duration to milliseconds
114
+ dropouts.append((start_time, end_time, duration_ms))
115
+ start = None
116
+
117
+ # Handle the case where dropout extends to the end of the file
118
+ if start is not None and len(dropout_frames) - start >= min_frames:
119
+ start_time = start * hop_length / sr
120
+ end_time = len(dropout_frames) * hop_length / sr
121
+ duration_ms = (end_time - start_time) * 1000
122
+ dropouts.append((start_time, end_time, duration_ms))
123
+
124
+ return dropouts
125
+
126
+ # Function to plot waveform with dropouts
127
+ def plot_waveform_with_dropouts(y, sr, dropouts, file_name):
128
+ plt.figure(figsize=(12, 6))
129
+ librosa.display.waveshow(y, sr=sr, alpha=0.6)
130
+ plt.title('Waveform with Detected Dropouts')
131
+ plt.xlabel('Time (seconds)')
132
+ plt.ylabel('Amplitude')
133
+
134
+ # Highlight dropouts
135
+ for dropout in dropouts:
136
+ start_time, end_time, _ = dropout
137
+ plt.axvspan(start_time, end_time, color='red', alpha=0.5, label='Dropout' if 'Dropout' not in plt.gca().get_legend_handles_labels()[1] else "")
138
+
139
+ plt.legend()
140
+ plt.savefig(file_name, bbox_inches='tight')
141
+ plt.close()
142
+
143
+ def get_or_create_extraction_folder() :
144
+
145
+ if 'extraction_folder' not in st.session_state :
146
+
147
+ current_datetime = datetime.now().strftime("%Y%m%d_%H%M%S")
148
+
149
+ folder_name = f'extracted_tracks_{current_datetime}'
150
+ full_path = os.path.abspath(folder_name)
151
+
152
+ os.makedirs(full_path , exist_ok = True)
153
+ st.session_state.extraction_folder = full_path
154
+
155
+ return st.session_state.extraction_folder
156
+
157
+ st.title('CineWav Audio processing Hub')
158
+
159
+ uploaded_file = st.file_uploader('Choose an M4A file' , type = ['m4a'])
160
+
161
+ channels = {
162
+ 'Front Left (FL)' : 'FL' ,
163
+ 'Front Right (FR)' : 'FR' ,
164
+ 'Center (FC)' : 'FC' ,
165
+ 'Subwoofer (LFE)' : 'LFE' ,
166
+ 'Back Left (BL)' : 'BL' ,
167
+ 'Back Right (BR)' : 'BR' ,
168
+ 'Side Left (SL)' : 'SL' ,
169
+ 'Side Right (SR)' : 'SR'
170
+ }
171
+
172
+ if uploaded_file is not None :
173
+
174
+ extraction_folder = get_or_create_extraction_folder()
175
+ st.write(f'Using extraction folder: {extraction_folder}')
176
+
177
+ with tempfile.NamedTemporaryFile(delete = False , suffix = '.m4a') as temp_file :
178
+
179
+ temp_file.write(uploaded_file.getbuffer())
180
+ input_file = temp_file.name
181
+
182
+ st.write('File uploaded successfully. Identifying and extracting audio channels...')
183
+
184
+ output_files = []
185
+
186
+ for name , channel in channels.items() :
187
+
188
+ output_file = os.path.join(extraction_folder, f"{name.replace(' ', '_').lower()}.wav")
189
+
190
+ if not os.path.exists(output_file) :
191
+
192
+ command = f'ffmpeg -y -i "{input_file}" -filter_complex "pan=mono|c0={channel}" "{output_file}"'
193
+
194
+ subprocess.run(command , shell = True)
195
+
196
+ st.write(f'Extracted {name} to {os.path.basename(output_file)}')
197
+
198
+ output_files.append(output_file)
199
+
200
+ st.write('Extraction complete. Download your files below:')
201
+
202
+ for output_file in output_files:
203
+ with open(output_file, "rb") as f:
204
+ st.download_button(label=f"Download {os.path.basename(output_file)}", data=f, file_name=os.path.basename(output_file), mime="audio/wav")
205
+
206
+ # Step 4: Audio Sync Offset Finder and Dropout Detection
207
+ st.subheader("Audio Sync Offset Finder and Dropout Detection")
208
+ st.write("Select a master track and one or more sample tracks to compare.")
209
+
210
+ # File selection
211
+ master_file = st.selectbox("Select Master Track", output_files, format_func=lambda x: os.path.basename(x))
212
+ sample_files = st.multiselect("Select Sample Tracks", output_files, default=[output_file for output_file in output_files if output_file != master_file], format_func=lambda x: os.path.basename(x))
213
+ # Sampling rate and segment settings
214
+ low_sr = st.slider("Select lower sampling rate for faster processing", 4000, 16000, 4000)
215
+ segment_length = st.slider("Segment length (seconds)", 2, 120, 10)
216
+ intervals = st.multiselect("Select intervals (in seconds)", options=[60, 900, 1800, 2700, 3600, 4500, 5400, 6300], default=[60, 900, 1800, 2700, 3600])
217
+
218
+ # Add a "Process" button
219
+ if st.button("Process"):
220
+ if master_file and sample_files:
221
+ st.write("Processing started...")
222
+
223
+ # Load the master track
224
+ master, sr_master = librosa.load(master_file, sr=low_sr)
225
+
226
+ all_results = {}
227
+ all_dropouts = {}
228
+ all_plots = {}
229
+
230
+ for sample_file in sample_files:
231
+ sample, sr_sample = librosa.load(sample_file, sr=low_sr)
232
+
233
+ # Resample if the sampling rates do not match
234
+ if sr_master != sr_sample:
235
+ sample = librosa.resample(sample, sr_sample, sr_master)
236
+
237
+ args = [(interval, master, sample, sr_master, segment_length) for interval in intervals]
238
+
239
+ with ProcessPoolExecutor() as executor:
240
+ results = list(filter(None, executor.map(process_segment_data, args)))
241
+
242
+ all_results[sample_file] = results
243
+
244
+ # Detect dropouts in the sample track
245
+ dropouts = detect_dropouts(sample_file)
246
+ all_dropouts[sample_file] = dropouts
247
+
248
+ # Plot waveform with dropouts
249
+ plot_file_name = f"{sample_file}_plot.png"
250
+ plot_waveform_with_dropouts(sample, sr_master, dropouts, plot_file_name)
251
+ all_plots[sample_file] = plot_file_name
252
+
253
+ st.write("Processing completed.")
254
+
255
+ # Display results
256
+ for sample_name, results in all_results.items():
257
+ file_name = os.path.basename(sample_name)
258
+ st.subheader(f"Results for {file_name}:")
259
+ for interval, offset in results:
260
+ st.write(f"At {interval // 60} mins: Offset = {offset:.2f} ms") # Update interval display if necessary
261
+
262
+ # Display dropouts
263
+ for sample_name, dropouts in all_dropouts.items():
264
+ file_name = os.path.basename(sample_name)
265
+ st.subheader(f"Detected Dropouts for {file_name}:")
266
+ if dropouts:
267
+ for dropout in dropouts:
268
+ start, end, duration_ms = dropout
269
+ st.write(f"Start: {format_time(start)} | End: {format_time(end)} | Duration: {duration_ms:.0f} ms")
270
+ else:
271
+ st.write("No significant dropouts detected.")
272
+
273
+ # Generate DOCX and provide download option
274
+ doc = generate_docx(all_results, intervals, all_dropouts, all_plots)
275
+ doc_buffer = BytesIO()
276
+ doc.save(doc_buffer)
277
+ doc_buffer.seek(0)
278
+ st.download_button("Download Results as DOCX", data=doc_buffer.getvalue(), file_name="audio_sync_results.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
279
+ else:
280
+ st.warning("Please select a master track and at least one sample track to begin processing.")
281
+ else : st.warning('Please upload an M4A file to begin.')