Spaces:
Sleeping
Sleeping
Create app.py
Browse files
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.')
|