Skip to content

Instantly share code, notes, and snippets.

@noahdominic
Last active September 13, 2023 10:02
Show Gist options
  • Save noahdominic/83e253672f15306dfb708ce5886de79b to your computer and use it in GitHub Desktop.
Save noahdominic/83e253672f15306dfb708ce5886de79b to your computer and use it in GitHub Desktop.
How I converted my FLAC/MP3 library to AAC for iOS

Converting My FLAC/MP3 Library to AAC for iOS

💡 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.

Why AAC?

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.

Prerequisites

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.

FFmpeg with libfdk_aac

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.

The Script

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment