diff --git a/README.md b/README.md index 02e7092..3d1cc9f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,79 @@ -# vocal-mastering -The Vocal Mastering Application is a Django-based web application designed to facilitate the uploading and processing of vocal audio files. It utilizes advanced audio processing techniques to enhance the quality of vocal recordings, making it suitable for musicians, producers, and audio engineers. + +--- + +# Vocal Mastering Application + +## Overview +The Vocal Mastering Application is a Django-based web application designed to facilitate the uploading and processing of vocal audio files. It utilizes advanced audio processing techniques to enhance the quality of vocal recordings, making it suitable for musicians, producers, and audio engineers. + +## Features +- **File Upload:** Users can upload vocal audio files in various formats (WAV, MP3, M4A). +- **Asynchronous Processing:** Audio processing is handled in the background using Celery, allowing users to continue using the application while their files are being processed. +- **Advanced Audio Processing:** The application employs a range of audio processing techniques, including noise reduction, dynamic compression, equalization, and loudness normalization. +- **Job Status Tracking:** Users can check the status of their audio processing jobs and download the mastered audio once completed. + +## Technologies Used +- **Django:** A high-level Python web framework for building web applications. +- **Celery:** An asynchronous task queue/job queue based on distributed message passing. +- **Librosa:** A Python package for music and audio analysis. +- **NumPy:** A library for numerical computations in Python. +- **SoundFile:** A library for reading and writing sound files. +- **Pyloudnorm:** A library for loudness normalization. + +## Installation +To set up the Vocal Mastering Application locally, follow these steps: + +### Clone the Repository: +```bash +git clone https://github.com/yourusername/vocal-mastering.git +cd vocal-mastering +``` + +### Create a Virtual Environment: +```bash +python -m venv venv +source venv/bin/activate # On Windows use `venv\Scripts\activate` +``` + +### Install Dependencies: +```bash +pip install -r requirements.txt +``` + +### Set Up the Database: +```bash +python manage.py migrate +``` + +### Run the Development Server: +```bash +python manage.py runserver +``` + +### Access the Application: +Open your web browser and navigate to [http://127.0.0.1:8000/](http://127.0.0.1:8000/). + +## Usage +1. **Upload Vocal:** Navigate to the upload page and select an audio file to upload. +2. **Processing:** After uploading, the application will process the audio file in the background. You will be redirected to a job status page. +3. **Download Mastered Audio:** Once processing is complete, you can download the mastered audio file. + +## Code Structure +The application is organized into several key components: +- `forms.py`: Contains the form for uploading audio files. +- `models.py`: Defines the data models for storing audio files and processing jobs. +- `processors.py`: Implements the audio processing logic. +- `tasks.py`: Contains the Celery tasks for asynchronous processing. +- `views.py`: Handles the web requests and responses. +- `urls.py`: Defines the URL routing for the application. + +## Contributing +Contributions are welcome! If you have suggestions for improvements or new features, please open an issue or submit a pull request. + +## License +This project is licensed under the MIT License. See the LICENSE file for details. + +## Sample Screenshot +[![result.png](https://i.postimg.cc/3NDJPFcs/result.png)](https://postimg.cc/sGsrW7ym) + +--- \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..2bda2b1 Binary files /dev/null and b/db.sqlite3 differ diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..996b709 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vocal_mastering.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/mastering/__init__.py b/mastering/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mastering/__pycache__/__init__.cpython-312.pyc b/mastering/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e307da5 Binary files /dev/null and b/mastering/__pycache__/__init__.cpython-312.pyc differ diff --git a/mastering/__pycache__/admin.cpython-312.pyc b/mastering/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..6eebacc Binary files /dev/null and b/mastering/__pycache__/admin.cpython-312.pyc differ diff --git a/mastering/__pycache__/advanced_vocal_processor.cpython-312.pyc b/mastering/__pycache__/advanced_vocal_processor.cpython-312.pyc new file mode 100644 index 0000000..c09a332 Binary files /dev/null and b/mastering/__pycache__/advanced_vocal_processor.cpython-312.pyc differ diff --git a/mastering/__pycache__/apps.cpython-312.pyc b/mastering/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..8619bf4 Binary files /dev/null and b/mastering/__pycache__/apps.cpython-312.pyc differ diff --git a/mastering/__pycache__/forms.cpython-312.pyc b/mastering/__pycache__/forms.cpython-312.pyc new file mode 100644 index 0000000..2a08d34 Binary files /dev/null and b/mastering/__pycache__/forms.cpython-312.pyc differ diff --git a/mastering/__pycache__/models.cpython-312.pyc b/mastering/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..0b6e57e Binary files /dev/null and b/mastering/__pycache__/models.cpython-312.pyc differ diff --git a/mastering/__pycache__/processors.cpython-312.pyc b/mastering/__pycache__/processors.cpython-312.pyc new file mode 100644 index 0000000..83b14f0 Binary files /dev/null and b/mastering/__pycache__/processors.cpython-312.pyc differ diff --git a/mastering/__pycache__/tasks.cpython-312.pyc b/mastering/__pycache__/tasks.cpython-312.pyc new file mode 100644 index 0000000..41f7a02 Binary files /dev/null and b/mastering/__pycache__/tasks.cpython-312.pyc differ diff --git a/mastering/__pycache__/urls.cpython-312.pyc b/mastering/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..b4aecef Binary files /dev/null and b/mastering/__pycache__/urls.cpython-312.pyc differ diff --git a/mastering/__pycache__/views.cpython-312.pyc b/mastering/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..1383da5 Binary files /dev/null and b/mastering/__pycache__/views.cpython-312.pyc differ diff --git a/mastering/admin.py b/mastering/admin.py new file mode 100644 index 0000000..ea5d68b --- /dev/null +++ b/mastering/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/mastering/apps.py b/mastering/apps.py new file mode 100644 index 0000000..b167ab5 --- /dev/null +++ b/mastering/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MasteringConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "mastering" diff --git a/mastering/forms.py b/mastering/forms.py new file mode 100644 index 0000000..b3b51ca --- /dev/null +++ b/mastering/forms.py @@ -0,0 +1,10 @@ +from django import forms +from .models import VocalMastering + +class VocalUploadForm(forms.ModelForm): + class Meta: + model = VocalMastering + fields = ['original_audio'] + widgets = { + 'original_audio': forms.FileInput(attrs={'accept': 'audio/*'}) + } \ No newline at end of file diff --git a/mastering/migrations/0001_initial.py b/mastering/migrations/0001_initial.py new file mode 100644 index 0000000..c565826 --- /dev/null +++ b/mastering/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 5.1.4 on 2024-12-10 09:10 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="VocalProcessingJob", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("original_file", models.FileField(upload_to="original_vocals/")), + ( + "processed_file", + models.FileField( + blank=True, null=True, upload_to="processed_vocals/" + ), + ), + ( + "status", + models.CharField( + choices=[ + ("pending", "Pending"), + ("processing", "Processing"), + ("completed", "Completed"), + ("failed", "Failed"), + ], + default="pending", + max_length=20, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("completed_at", models.DateTimeField(blank=True, null=True)), + ("error_message", models.TextField(blank=True, null=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/mastering/migrations/0002_vocalmastering_delete_vocalprocessingjob.py b/mastering/migrations/0002_vocalmastering_delete_vocalprocessingjob.py new file mode 100644 index 0000000..0b691a6 --- /dev/null +++ b/mastering/migrations/0002_vocalmastering_delete_vocalprocessingjob.py @@ -0,0 +1,37 @@ +# Generated by Django 5.1.4 on 2024-12-10 09:46 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("mastering", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="VocalMastering", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("original_audio", models.FileField(upload_to="vocals/")), + ( + "mastered_audio", + models.FileField(blank=True, null=True, upload_to="mastered/"), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.DeleteModel( + name="VocalProcessingJob", + ), + ] diff --git a/mastering/migrations/0003_vocalmastering_reference_audio.py b/mastering/migrations/0003_vocalmastering_reference_audio.py new file mode 100644 index 0000000..3c2b6ca --- /dev/null +++ b/mastering/migrations/0003_vocalmastering_reference_audio.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2024-12-12 11:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("mastering", "0002_vocalmastering_delete_vocalprocessingjob"), + ] + + operations = [ + migrations.AddField( + model_name="vocalmastering", + name="reference_audio", + field=models.FileField(blank=True, null=True, upload_to="references/"), + ), + ] diff --git a/mastering/migrations/0004_remove_vocalmastering_reference_audio.py b/mastering/migrations/0004_remove_vocalmastering_reference_audio.py new file mode 100644 index 0000000..c07d543 --- /dev/null +++ b/mastering/migrations/0004_remove_vocalmastering_reference_audio.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.4 on 2024-12-12 11:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("mastering", "0003_vocalmastering_reference_audio"), + ] + + operations = [ + migrations.RemoveField( + model_name="vocalmastering", + name="reference_audio", + ), + ] diff --git a/mastering/migrations/__init__.py b/mastering/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mastering/migrations/__pycache__/0001_initial.cpython-312.pyc b/mastering/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000..fa0313b Binary files /dev/null and b/mastering/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/mastering/migrations/__pycache__/0002_vocalmastering_delete_vocalprocessingjob.cpython-312.pyc b/mastering/migrations/__pycache__/0002_vocalmastering_delete_vocalprocessingjob.cpython-312.pyc new file mode 100644 index 0000000..3238d2c Binary files /dev/null and b/mastering/migrations/__pycache__/0002_vocalmastering_delete_vocalprocessingjob.cpython-312.pyc differ diff --git a/mastering/migrations/__pycache__/0003_vocalmastering_reference_audio.cpython-312.pyc b/mastering/migrations/__pycache__/0003_vocalmastering_reference_audio.cpython-312.pyc new file mode 100644 index 0000000..564a04f Binary files /dev/null and b/mastering/migrations/__pycache__/0003_vocalmastering_reference_audio.cpython-312.pyc differ diff --git a/mastering/migrations/__pycache__/0004_remove_vocalmastering_reference_audio.cpython-312.pyc b/mastering/migrations/__pycache__/0004_remove_vocalmastering_reference_audio.cpython-312.pyc new file mode 100644 index 0000000..e02bc3c Binary files /dev/null and b/mastering/migrations/__pycache__/0004_remove_vocalmastering_reference_audio.cpython-312.pyc differ diff --git a/mastering/migrations/__pycache__/__init__.cpython-312.pyc b/mastering/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c4ba495 Binary files /dev/null and b/mastering/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/mastering/models.py b/mastering/models.py new file mode 100644 index 0000000..dc0c226 --- /dev/null +++ b/mastering/models.py @@ -0,0 +1,11 @@ +import uuid +from django.db import models + +class VocalMastering(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + original_audio = models.FileField(upload_to='vocals/') + mastered_audio = models.FileField(upload_to='mastered/', null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Vocal Mastering - {self.id}" \ No newline at end of file diff --git a/mastering/processors.py b/mastering/processors.py new file mode 100644 index 0000000..204ce2b --- /dev/null +++ b/mastering/processors.py @@ -0,0 +1,309 @@ +import os +import numpy as np +import librosa +import soundfile as sf +import pyloudnorm as pyln +from scipy import signal +from django.conf import settings +import noisereduce as nr +from scipy.fftpack import fft, ifft + +class SmarterVocalMasteringProcessor: + def __init__( + self, + target_loudness=-14, + sample_rate=44100, + dynamic_range_threshold=6 + ): + self.target_loudness = target_loudness + self.sample_rate = sample_rate + self.dynamic_range_threshold = dynamic_range_threshold + + # 1. Noise Profiling and Adaptive Noise Reduction + def noise_reduction(self, audio): + try: + # Ensure non-zero audio values to avoid computational issues + audio = np.clip(audio, a_min=1e-10, a_max=None) + + # Extract a noise sample from the first 1 second for a more representative profile + noise_duration = int(1.0 * self.sample_rate) # 1 second + noise_sample = audio[:noise_duration] + + # Check if the noise sample has sufficient variability + if np.std(noise_sample) < 1e-3: + print("Warning: Noise sample may not represent actual noise. Proceeding with caution.") + + # Perform noise reduction with dynamic parameters + reduced_audio = nr.reduce_noise( + y=audio, + y_noise=noise_sample, + sr=self.sample_rate, + prop_decrease=0.5, # Slightly higher suppression for better noise removal + stationary=False # Non-stationary mode for fluctuating noise + ) + + # Smooth transitions to avoid artifacts + reduced_audio = np.clip(reduced_audio, -1.0, 1.0) # Normalize after processing + + return reduced_audio + except Exception as e: + print(f"Noise reduction error: {e}") + return audio + + # 2. Dynamic Lookahead Compression + def dynamic_compression(self, audio): + try: + threshold = -18 # dBFS + ratio = 4 # Compression ratio + attack = 0.01 # Seconds + release = 0.1 # Seconds + + audio_db = 20 * np.log10(np.maximum(np.abs(audio), 1e-5)) + gain = np.ones_like(audio) + + above_threshold = audio_db > threshold + gain[above_threshold] = 10 ** ((threshold - audio_db[above_threshold]) * (1 - 1 / ratio) / 20) + + # Smooth the gain using a moving average + window_size = int(self.sample_rate * (attack + release)) + if window_size > 1: + smoothed_gain = np.convolve(gain, np.ones(window_size) / window_size, mode='same') + else: + smoothed_gain = gain + + return audio * smoothed_gain + except Exception as e: + print(f"Compression error: {e}") + return audio + + def equalize(self, audio): + try: + fft_data = fft(audio) + freqs = np.fft.fftfreq(len(audio), 1 / self.sample_rate) + perceptual_curve = np.interp(np.abs(freqs), [100, 1000, 5000, 20000], [1.0, 1.2, 1.0, 0.8]) + return ifft(fft_data * perceptual_curve).real + except Exception as e: + print(f"Equalization error: {e}") + return audio + + # 3. Phase Alignment + def phase_alignment(self, audio): + try: + if audio.ndim > 1: + left, right = audio[0], audio[1] + delay = np.correlate(left, right, mode='full') + alignment_offset = np.argmax(delay) - len(right) + 1 + if alignment_offset > 0: + right = np.pad(right, (alignment_offset, 0), mode='constant')[:len(left)] + else: + left = np.pad(left, (-alignment_offset, 0), mode='constant')[:len(right)] + return np.stack([left, right]) + return audio + except Exception as e: + print(f"Phase alignment error: {e}") + return audio + + # 4. Psychoacoustic Modeling + def psychoacoustic_enhancement(self, audio): + try: + spectrum = np.fft.rfft(audio) + freqs = np.fft.rfftfreq(len(audio), 1 / self.sample_rate) + masking_curve = np.ones_like(freqs) + masking_curve[(freqs >= 2000) & (freqs <= 5000)] *= 1.2 + return np.fft.irfft(spectrum * masking_curve) + except Exception as e: + print(f"Psychoacoustic enhancement error: {e}") + return audio + + # 5. Transient Shaping + def transient_shaping(self, audio): + try: + # Calculate RMS envelope with a frame length of 2048 samples + frame_length = 2048 + hop_length = frame_length // 2 # Overlap for smoother envelope + rms = librosa.feature.rms(y=audio, frame_length=frame_length, hop_length=hop_length)[0] + + # Normalize the RMS envelope to the range [0.8, 1.2] for gain control + gain = np.interp(rms, (rms.min(), rms.max()), (0.8, 1.2)) + + # Upsample gain to match the length of the audio + gain_upsampled = np.interp( + np.arange(len(audio)), + np.linspace(0, len(audio), len(gain)), + gain + ) + + # Apply gain to audio for transient shaping + shaped_audio = audio * gain_upsampled + return shaped_audio + except Exception as e: + print(f"Transient shaping error: {e}") + return audio + + # 6. Mid/Side (M/S) Processing Refinement + def mid_side_processing(self, audio): + try: + if audio.ndim > 1: + left, right = audio[0], audio[1] + mid = (left + right) / 2 + side = (left - right) / 2 + side *= 1.2 # Widen high frequencies + return np.stack([mid + side, mid - side]) + return audio + except Exception as e: + print(f"M/S processing error: {e}") + return audio + + # 7. Advanced De-Essing + def de_essing(self, audio): + try: + D = librosa.stft(audio) + mag, phase = np.abs(D), np.angle(D) + high_freq_mask = mag > np.percentile(mag, 95) + attenuation = np.clip(1 - (mag / mag.max()), 0.5, 1) + mag[high_freq_mask] *= attenuation[high_freq_mask] + return librosa.istft(mag * np.exp(1j * phase)) + except Exception as e: + print(f"De-essing error: {e}") + return audio + + # 8. Intelligent Harmonic Exciter + def harmonic_exciter(self, audio): + try: + harmonic_audio = audio + 0.01 * np.tanh(audio * 2) + return harmonic_audio + except Exception as e: + print(f"Harmonic exciter error: {e}") + return audio + + # 9. Multiband Compression + def multiband_compression(self, audio): + try: + bands = [(20, 200), (200, 1000), (1000, 5000), (5000, 20000)] + compressed_audio = np.zeros_like(audio) + for low, high in bands: + sos = signal.butter(4, [low, high], btype='band', fs=self.sample_rate, output='sos') + band = signal.sosfilt(sos, audio) + gain = 0.9 # Example fixed gain, could be adaptive + compressed_audio += band * gain + return compressed_audio + except Exception as e: + print(f"Multiband compression error: {e}") + return audio + + # 10. Loudness Range Optimization + def loudness_normalization(self, audio): + try: + meter = pyln.Meter(self.sample_rate) + loudness = meter.integrated_loudness(audio) + return pyln.normalize.loudness(audio, loudness, self.target_loudness) + except Exception as e: + print(f"Loudness normalization error: {e}") + return audio + + # 11. Stereo Enhancement + def stereo_enhancement(self, audio): + try: + # Ensure audio is stereo + if audio.ndim == 1: + raise ValueError("Input audio must be stereo for stereo enhancement.") + + # Separate mid (center) and side (stereo) channels + mid = (audio[0, :] + audio[1, :]) / 2 # Mono channel (mid) + side = (audio[0, :] - audio[1, :]) / 2 # Stereo difference (side) + + # Amplify the side channel for stereo widening + side_widened = side * 1.2 # Adjust factor for widening effect + + # Recombine mid and side channels + left = mid + side_widened + right = mid - side_widened + + # Normalize to avoid clipping + left = np.clip(left, -1.0, 1.0) + right = np.clip(right, -1.0, 1.0) + + # Combine left and right channels into stereo + enhanced_audio = np.vstack([left, right]) + + return enhanced_audio + except Exception as e: + print(f"Stereo enhancement error: {e}") + return audio + + def normalize_vocal_loudness(self, audio): + try: + # Calculate the loudness of the audio + audio_db = librosa.amplitude_to_db(np.abs(audio)) + target_db=-20 + # Identify vocal segments (this is a simplified approach) + # You might want to use a more sophisticated method to isolate vocals + vocal_mask = audio_db > (np.mean(audio_db) + 10) # Adjust threshold as needed + vocal_segments = audio[vocal_mask] + + # Calculate the current loudness of the vocal segments + current_loudness = librosa.amplitude_to_db(np.mean(np.abs(vocal_segments))) + + # Calculate the gain needed to reach the target loudness + gain = target_db - current_loudness + + # Apply gain to the vocal segments + normalized_audio = audio * (10 ** (gain / 20)) + + return normalized_audio + + except Exception as e: + print(f"Error in loudness normalization: {e}") + return audio + + # 12. Export and Delivery Optimization + def export_audio(self, audio, output_path): + try: + # sf.write(output_path, audio, self.sample_rate, subtype="PCM_16") + + # Convert the audio to 24-bit integer format + audio_24bit = (audio * (2**23 - 1)).astype(np.int32) + sf.write(output_path, audio, self.sample_rate, subtype="PCM_24") + print(f"Audio exported successfully") + except Exception as e: + print(f"Export error: {e}") + + def process_vocal(self, input_path, output_path=None): + try: + if not os.path.exists(input_path): + raise FileNotFoundError(f"Input file not found: {input_path}") + + audio, sr = librosa.load(input_path, sr=self.sample_rate) + if audio.ndim > 1: + audio = librosa.to_mono(audio) + + steps = [ + self.normalize_vocal_loudness, + self.phase_alignment, + self.de_essing, + self.dynamic_compression, + self.equalize, + self.psychoacoustic_enhancement, + self.multiband_compression, + self.transient_shaping, + self.harmonic_exciter, + self.mid_side_processing, + # self.stereo_enhancement, + self.loudness_normalization, + ] + + for step in steps: + audio = step(audio) + + if output_path is None: + output_path = os.path.join( + settings.MEDIA_ROOT, + 'processed_vocals', + f'processed_{os.path.basename(input_path)}' + ) + os.makedirs(os.path.dirname(output_path), exist_ok=True) + self.export_audio(audio, output_path) + return output_path + except Exception as e: + print(f"Processing error: {e}") + return None \ No newline at end of file diff --git a/mastering/tasks.py b/mastering/tasks.py new file mode 100644 index 0000000..ff00cb4 --- /dev/null +++ b/mastering/tasks.py @@ -0,0 +1,29 @@ +from celery import shared_task +from django.core.files import File +from .models import VocalMastering +from .processors import SmarterVocalMasteringProcessor +from django.utils import timezone +import os + +@shared_task +def process_vocal_track(job_id): + try: + job = VocalMastering.objects.get(id=job_id) + processor = SmarterVocalMasteringProcessor() + processed_file_path = processor.process_vocal(job.original_audio.path) + + if processed_file_path: + with open(processed_file_path, 'rb') as f: + job.mastered_audio.save(os.path.basename(processed_file_path), File(f)) + job.status = 'completed' + job.completed_at = timezone.now() + else: + job.status = 'failed' + job .error_message = "Processing failed" + + job.save() + + except Exception as e: + job.status = 'failed' + job.error_message = str(e) + job.save() \ No newline at end of file diff --git a/mastering/tests.py b/mastering/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/mastering/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/mastering/urls.py b/mastering/urls.py new file mode 100644 index 0000000..66b1c52 --- /dev/null +++ b/mastering/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.upload_vocal, name='upload_vocal'), + path('job//status/', views.job_status, name='job_status'), + path('job//download/', views.download_mastered_audio, name='download_mastered_audio'), +] \ No newline at end of file diff --git a/mastering/views.py b/mastering/views.py new file mode 100644 index 0000000..4a583e6 --- /dev/null +++ b/mastering/views.py @@ -0,0 +1,85 @@ +import logging +from django.shortcuts import render, redirect +from django.http import JsonResponse, FileResponse, Http404 +from django.views.decorators.http import require_http_methods +from django.contrib import messages +from django.core.exceptions import ValidationError +from .models import VocalMastering +from .forms import VocalUploadForm +from .processors import SmarterVocalMasteringProcessor + +logger = logging.getLogger(__name__) + +@require_http_methods(["GET", "POST"]) +def upload_vocal(request): + if request.method == 'POST': + form = VocalUploadForm(request.POST, request.FILES) + + if form.is_valid(): + audio_file = request.FILES.get('original_audio') + _validate_audio_file(audio_file) + job = VocalMastering.objects.create(original_audio=audio_file) + _process_vocal_async(job) + return redirect('job_status', job_id=job.id) + + messages.error(request, "Invalid form submission") + + else: + form = VocalUploadForm() + + return render(request, 'mastering/upload.html', {'form': form}) + +def _validate_audio_file(audio_file): + if not audio_file: + raise ValidationError("No audio file uploaded") + + max_size = 50 * 1024 * 1024 + if audio_file.size > max_size: + raise ValidationError(f"File too large. Maximum size is {max_size/1024/1024}MB") + + allowed_types = ['audio/wav', 'audio/mpeg', 'audio/mp3', 'audio/x-wav', 'audio/x-m4a'] + if audio_file.content_type not in allowed_types: + raise ValidationError("Invalid file type. Supported: WAV, MP3, M4A") + +def _process_vocal_async(job): + from threading import Thread + + def process_task(): + try: + processor = SmarterVocalMasteringProcessor() + processed_path = processor.process_vocal(job.original_audio.path) + if processed_path: + with open(processed_path, 'rb') as f: + job.mastered_audio.save(f'mastered_{job.id}.wav', f) + job.save() + except Exception as e: + logger.error(f"Processing error for job {job.id}: {e}") + + Thread(target=process_task, daemon=True).start() + +def job_status(request, job_id): + try: + job = VocalMastering.objects.get(id=job_id) + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return JsonResponse({ + 'status': 'COMPLETED' if job.mastered_audio else 'PROCESSING', + 'mastered_audio_url': job.mastered_audio.url if job.mastered_audio else None + }) + return render(request, 'mastering/job_status.html', {'job': job}) + + except VocalMastering.DoesNotExist: + messages.error(request, "Job not found") + return redirect('upload_vocal') + +def download_mastered_audio(request, job_id): + try: + job = VocalMastering.objects.get(id=job_id) + if not job.mastered_audio: + raise Http404("Mastered audio file not found") + + response = FileResponse(job.mastered_audio.open('rb'), as_attachment=True, filename=f'mastered_{job.id}.wav') + response['Content-Type'] = 'audio/wav' + return response + + except VocalMastering.DoesNotExist: + raise Http404("Job not found") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6d62c84 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +Django==5.1.4 +celery==5.4.0 +redis==5.2.1 +numba==0.60.0 +numpy==2.0.2 +librosa==0.10.2.post1 +soundfile==0.12.1 +pyloudnorm==0.1.1 +scipy==1.14.1 +noisereduce==3.0.3 \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..4881be7 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,20 @@ + + + + + Vocal Mastering + + + +
+ {% block content %}{% endblock %} +
+ + \ No newline at end of file diff --git a/templates/mastering/job_status.html b/templates/mastering/job_status.html new file mode 100644 index 0000000..4b928be --- /dev/null +++ b/templates/mastering/job_status.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Job Status

+ + {% if job.mastered_audio %} +
+

Processing Complete

+ + Download Mastered Audio + +
+ {% else %} +
+

Processing in progress...

+ +
+ + + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/mastering/result.html b/templates/mastering/result.html new file mode 100644 index 0000000..58a4a0f --- /dev/null +++ b/templates/mastering/result.html @@ -0,0 +1,67 @@ +{% extends 'base.html' %} + +{% block content %} +
+ {% if processing_success %} +

Vocal Mastering Completed

+ +
+

Original Audio

+ +
+ +
+

Mastered Audio

+ +
+ + + {% else %} +

Processing Failed

+

We were unable to process your audio file. Please try again.

+ {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/mastering/upload.html b/templates/mastering/upload.html new file mode 100644 index 0000000..591fd4b --- /dev/null +++ b/templates/mastering/upload.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block content %} +

Upload Vocal for Mastering

+
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..119e05a --- /dev/null +++ b/vercel.json @@ -0,0 +1,15 @@ +{ + "builds": [ + { + "src": "vocal_mastering/wsgi.py", + "use": "@vercel/python", + "config": {"runtime": "python3.12" } + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "/vocal_mastering/wsgi.py" + } + ] +} diff --git a/vocal_mastering/__init__.py b/vocal_mastering/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vocal_mastering/__pycache__/__init__.cpython-312.pyc b/vocal_mastering/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..f04c02b Binary files /dev/null and b/vocal_mastering/__pycache__/__init__.cpython-312.pyc differ diff --git a/vocal_mastering/__pycache__/settings.cpython-312.pyc b/vocal_mastering/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000..decfc5d Binary files /dev/null and b/vocal_mastering/__pycache__/settings.cpython-312.pyc differ diff --git a/vocal_mastering/__pycache__/urls.cpython-312.pyc b/vocal_mastering/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..0cc254a Binary files /dev/null and b/vocal_mastering/__pycache__/urls.cpython-312.pyc differ diff --git a/vocal_mastering/__pycache__/wsgi.cpython-312.pyc b/vocal_mastering/__pycache__/wsgi.cpython-312.pyc new file mode 100644 index 0000000..c0ecba6 Binary files /dev/null and b/vocal_mastering/__pycache__/wsgi.cpython-312.pyc differ diff --git a/vocal_mastering/asgi.py b/vocal_mastering/asgi.py new file mode 100644 index 0000000..f44855b --- /dev/null +++ b/vocal_mastering/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vocal_mastering.settings") + +application = get_asgi_application() diff --git a/vocal_mastering/settings.py b/vocal_mastering/settings.py new file mode 100644 index 0000000..5e8a46e --- /dev/null +++ b/vocal_mastering/settings.py @@ -0,0 +1,116 @@ +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-9jm(g5oib)a56s&up_ey)*ed!!n7a-g7k7p_j$0k48+2+2ixfg" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['.vercel.app', '127.0.0.1'] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "mastering" +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = 'vocal_mastering.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = "vocal_mastering.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +STATIC_URL = '/static/' +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/vocal_mastering/urls.py b/vocal_mastering/urls.py new file mode 100644 index 0000000..a939fa9 --- /dev/null +++ b/vocal_mastering/urls.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('mastering.urls')), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/vocal_mastering/wsgi.py b/vocal_mastering/wsgi.py new file mode 100644 index 0000000..f6758a0 --- /dev/null +++ b/vocal_mastering/wsgi.py @@ -0,0 +1,9 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vocal_mastering.settings") + +application = get_wsgi_application() + +app = application