💡 NB: If you want just the script, skip to the last part.
💡 NB2: I'll be using Python. If you're a bash fan, this might not be for you.
I began collecting a personal library of music files back in the era of infrared file transfers. I've always loved the connection this kind of arrangement offers me to my library, something that the streaming era has not been able to provide. So, I thought, it's time to transfer a bunch of files to my phone.
On my previous Android phones, this would have been a trivial endeavour. Android has a relatively permissive file system and its apps are ready to play FLAC files without much fuss. On iOS, you have to transfer files via iTunes (on Windows). And iTunes has a limited compatiblity for audio files. (It doesn't even play FLAC!)
In this situation, MP3 would be the easy choice; they're popular and they're easy to work with. But, due to a number of reasons like audio quality and compression rate, AAC is marginally better than MP3. Of course, this comes at a cost: Converting from FLAC to AAC exactly the way I want can be a pain in the ass.
Is it worth it? If you're the kind of person who thinks in ROI and numbers, no. Converting to MP3 might even be easier. But I think scripting and audio codecs are a fun pasttime. If you're that kind of person, too, let's move on to the next section.
Here's what I want to do: Convert all the FLAC and MP3 files in a given directory into AAC files into a separate directory. In this conversion process, the file structure of the first directory must be replicated in the second directory. It will be outside of the scope, but I do this so I can transfer the second directory into iTunes, which will allow me to sync those file into my iPhone and play them via Apple Music, despite not having a subscription.
In a perfect world, I would simply write a script to loop through my entire directory,
convert it via FFmpeg,
and be on my merry way.
Right now [2023-08-28 20-24 PST (UTC+0800)],
while FFmpeg does come with an AAC encoder,
it's not the best one available.
That honour belongs to macOS's.
(Apparently, there is a way to make ffmpeg
use the macOS encoder on Windows.
Maybe for a future project?)
For now, I'll be using
the The Fraunhofer FDK AAC encoder,
or libfdk_aac
,
which is compatible with FFmpeg.
Now FFmpeg does not have libfdk_aac
due to licence issues.
You're going to have to compile it yourself.
Lucky for us, FFmpeg does have a page
about doing it.
Just follow it for your operating system.
Now that you have a binary that can use libfdk_aac
,
the script should work now.
Here is it in full:
import os
import shutil
import subprocess
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm
# Variables. These would need to be replaced in a case-to-case basis.
source_directory = '/home/username/Music' # Path to the source directory containing audio files.
destination_directory = '/home/username/Music_copy' # Path to the destination directory where processed files will be stored.
log_file_path = '/tmp/ffmpeg.log' # Path to the log file for FFmpeg output.
# Function to convert audio using FFmpeg
def convert_audio(input_path, output_path):
command = [
'ffmpeg', # FFmpeg command
'-i', input_path, # Input file path
'-c:a', 'libfdk_aac', # Audio codec to use (AAC)
'-c:v', 'copy', # Copy video codec
'-vbr', '3', # Audio quality
'-n', # No overwrite
output_path # Output file path
]
output_dir = os.path.dirname(output_path)
os.makedirs(output_dir, exist_ok=True) # Create output directory if not exists
with open(log_file_path, 'w') as log_file:
subprocess.run(command, stdout=log_file, stderr=subprocess.STDOUT) # Run FFmpeg command and write output to log file
# Function to copy a file from source to destination
def copy_file(input_path, output_path):
output_dir = os.path.dirname(output_path)
os.makedirs(output_dir, exist_ok=True) # Create output directory if not exists
shutil.copy(input_path, output_path) # Copy the file from source to destination
print(f"Copied: {input_path} -> {output_path}") # Print a message indicating the copy
# Function to process a single file
def process_file(input_path, dest_dir):
relative_path = os.path.relpath(input_path, source_directory) # Get the relative path of the input file
output_path = os.path.join(dest_dir, relative_path) # Create the output path in the destination directory
# Check if the input file has an audio format extension
if input_path.lower().endswith(('.flac', '.mp3', '.wav')):
output_path = os.path.splitext(output_path)[0] + '.m4a' # Change the extension to .m4a (Apple Lossless)
convert_audio(input_path, output_path) # Convert audio using FFmpeg
print(f"Converted: {input_path} -> {output_path}") # Print a message indicating the conversion
else:
copy_file(input_path, output_path) # If not an audio file, simply copy it to the destination
# Function to process all files in a directory
def process_directory(source_dir, dest_dir):
for root, _, files in tqdm(os.walk(source_dir)): # Walk through the source directory
print(f"Processing {files}") # Print a message indicating the processing
for filename in tqdm(files):
input_path = os.path.join(root, filename)
process_file(input_path, dest_dir) # Process each file using the defined function
# Function to process all files in a directory in parallel
def process_directory_parallel(source_dir, dest_dir):
def process_file_wrapper(args):
input_path, dest_dir = args
process_file(input_path, dest_dir)
file_paths = []
for root, _, files in os.walk(source_dir):
file_paths.extend([(os.path.join(root, filename), dest_dir) for filename in files])
with ThreadPoolExecutor() as executor: # Use ThreadPoolExecutor for parallel processing
list(tqdm(executor.map(process_file_wrapper, file_paths), total=len(file_paths)))
if __name__ == '__main__':
process_directory_parallel(source_directory, destination_directory) # Start parallel processing of the source directory's files