4

I want to rip/archive all my music collection to a lossless but compressed file format, i.e. the files should all be perfect, lossless representations of the original data but should consume less space than uncompressed WAV(E).

WAV(E) is a no-go, since it's non-free (proprietary Microsoft stuff), cross-platform compression is cumbersome or not possible and the file size is limited to 4 GB. Therefore I choose FLAC (Free Lossless Audio Codec).

Since digitizing a whole collection is a mammoth task and FLAC offers 9 compression levels (0 to 8), there comes the golden question:

Which compression level should I wisely choose?

  • this question doesn't address sound design at all but it does touch upon a choice that some sound designers face, which is, how to best handle our ever-growing libraries of recordings. Personally, I'm going FLAC over WAVE simply because of the storage issue, but I'm afraid I don't have any insight as to the compression level. – Jay Jennings Aug 29 '17 at 23:26
  • Interestingly I posted this on Music first but the people there recommended moving it to Sound Design. – Suuuehgi Aug 30 '17 at 1:10
8

FLAC compression levels are (only) a trade of between encoding time and file size. The decoding time is pretty much independent of compression rate. In the following I will refer to the compression levels 0, ..., 8 as FLAC-0, ..., FLAC-8.

In short: I recommend FLAC-4!


The Easy Solutions

Obviously:

  • If I don't care about encoding time and since space is money, I take the highest compression level FLAC-8.

  • If I don't care about space but want to get behind this as fast as possible, I take the lowest compression level FLAC-0.


The Difficult Solution

Where is the right middle between file size and encoding time? I stumbled upon Nathan Zachary's article on this question but he compares just two files, encodes them just once (encoding time hugely varies according to the sideload of the computer) and tables are hard to read compared to graphs.

So, inspired by this, I redid his measurements with five complete albums each in a different genre and encoded each file/track 10 times.

Procedure:

  • Rip album with abcde and proper cdparanoia settings to uncompressed WAV.
  • Convert every file 10 times for each compression level (FLAC-0 to FLAC-8) and take the mean encoding time relative to FLAC-0 and the file size relative to FLAC-0.
    • For this I disabled internet connection, all periodic jobs (cronjobs) and nearly everything else so that really mostly the compression runs and as less as possible interfers.

This measure should be pretty much independent of the used hardware. I used flac version 1.3.2 on Arch Linux using flac <infile> --compression-level-X -f -o flacX.flac.

Efficiency

If you multiply the relative size with the relative encoding/compression time, you get a value for the badness. But since this badness is mostly governed by the relative time, the graphs would vastly overlap. So, to clear the graph up, I just mirrored the badness into a goodness, I call efficiency here.

Findings

From FLAC-4 on, the compression time explodes BUT there are two surprises:

  1. There is a significant reduction in file size between FLAC-3 and FLAC-4 depending on the music genre: Classical music has a way lower compression using FLAC-4. I assume that this is because FLAC uses a linear prediction model for compression that does less well with more complex (less linear) music.

  2. For non-classical music, FLAC-3 is even significantly worse than FLAC-2 in term of file size.

flac_rank

Recommendations

I recommend using compression level FLAC-4.

Going higher significantly increases encoding time with marginal improvement in file size reduction (mean reduction from FLAC-4 to FLAC-8 in this test is 1.2 % with a 182 % increase in mean compression time).


Appendix

Albums

I just took the first five random CDs (listed below) that I thought of represent different field of music. The links are intentionally going to Amazon to provide an easy possibility to take a glimpse into the music/to get an idea of the music since it does make a significant difference in the compression.

Program

For this task I wrote a python program that goes through all sub-folders (the albums) in a given folder (--directory <folder>) to test all .wav files and group/plot them by their sub-folder name.

<folder>
    Album 1
    Album 2
    ...

The analysis will be saved in a file --outfile <file1>. To plot, use --infile <file1> and --outfile <file2>.

#!/usr/bin/python3
#encoding=utf8

import os, sys, subprocess, argparse
from datetime import datetime, timedelta
from os.path import isfile, isdir, join
import numpy as np
import matplotlib.pyplot as plt
import pickle as pkl

parser = argparse.ArgumentParser(description='Analyse flac compression and conversion time')

group = parser.add_mutually_exclusive_group()
group.add_argument('-d', '--directory', help="Input folder", type=str)
group.add_argument('-if', '--infile', help="Plot saved stats (pickle file)", type=str)

parser.add_argument('-of', '--outfile', help="Output file", type=str, required=True)
parser.add_argument('-c', '--cycles', help="Number of cycles for each file", type=int, default=5)
parser.add_argument('-C', '--maxcompression', help="Max compression level", type=int, default=8)

args = parser.parse_args()

args.maxcompression += 1

############################################################

xlabel = 'FLAC Compression Factor'
ylabel_size = 'Size Relative to FLAC-0'
ylabel_time = 'Mean Compression Time\nOver {} Cycles Relative to FLAC-0 [s]'.format(args.cycles)
ylabel_efficiency = r'Efficiency: $(-1)\cdot$ Fraction Time $\cdot$ Fraction Size $+ 2$'

############################################################

# Analyse and write mode
if not args.infile:

    if isdir(args.directory):
        mypath = args.directory
    else:
        raise ValueError('Folder {} does not exist!'.format(args.directory))

    folders = [f for f in os.listdir(mypath) if isdir(join(mypath, f))]
    print('Found folders: {}'.format(folders))

    # Create temporary working folder
    temp_folder = 'temp_{}'.format(os.getpid())

    if not os.path.exists(temp_folder):
        os.makedirs(temp_folder)

    # Every analysis will be storen in stats
    stats = {}
    remove = []

    for folder in folders:

        stats[folder] = {}
        stats[folder]['files'] = [f for f in os.listdir(mypath+folder) if isfile(join(mypath+folder, f)) and f.endswith('.wav')]

        if len(stats[folder]['files']) == 0:
            print('No .wav files found in {}. Skipping.'.format(folder))
            remove.append(folder)
            stats.pop(folder, None)
        else:
            stats[folder]['stats'] = np.empty([len(stats[folder]['files']),args.maxcompression], dtype=object)

    # Remove empty (no .wav) folders from list
    for folder in remove:
        folders.remove(folder)

    totalfiles = []

    for folder in folders:
        totalfiles += stats[folder]['files']

    totalfiles = len(totalfiles)

    if totalfiles == 0:
        raise RuntimeError('No .wav files found!')

    totalcycles = totalfiles * args.cycles * args.maxcompression

    counter_cycles = 0
    time_start = datetime.strptime(str(datetime.now()), "%Y-%m-%d %H:%M:%S.%f")

    for folder in folders:
        # i: 0..Nfiles
        # n: 0..8
        files = stats[folder]['files']

        for i in range(len(files)):

            infile = '{}/{}'.format(mypath+folder,files[i])

            for n in range(args.maxcompression):

                Dtime = []

                for j in range(args.cycles):

                    time1 = datetime.strptime(str(datetime.now()), "%Y-%m-%d %H:%M:%S.%f")
                    subprocess.run(['flac', infile, '--compression-level-{}'.format(n),
                                    '-f', '-o', '{}/flac{}.flac'.format(temp_folder,n)])
                    time2 = datetime.strptime(str(datetime.now()), "%Y-%m-%d %H:%M:%S.%f")
                    Dtime.append((time2-time1).total_seconds())

                    counter_cycles += 1

                    # Percentage of totalcycles
                    status = counter_cycles/totalcycles
                    remain_factor = (1 - status)/status

                    time_current = datetime.strptime(str(datetime.now()), "%Y-%m-%d %H:%M:%S.%f")
                    time_elapsed = (time_current - time_start).total_seconds()

                    print('========================================')
                    print('Status: {} %'.format(int(100*status)))
                    print('Estimated remaining time: {}'.format(str(timedelta(seconds=int(remain_factor * time_elapsed)))))
                    print('========================================')

                Dtime = np.mean(Dtime)

                size = os.path.getsize('{}/flac{}.flac'.format(temp_folder,n))

                # Array if size (regarded as constat) and mean compression time
                # (file1, FLAC0)(file1, FLAC1)...(file1, FLACmaxcompression) 
                # (file2, FLAC0)(file2, FLAC1)...(file2, FLACmaxcompression) 
                # ...
                stats[folder]['stats'][i,n] = (size, Dtime)

    for folder in folders:

        # Taking columnwise (for each compression level) means of size...
        stats[folder]['ploty_size'] = [np.mean([e[0] for e in stats[folder]['stats'][:,col]])
                                       for col in range(np.shape(stats[folder]['stats'])[1])]
        # (relative to FLAC-0)
        stats[folder]['ploty_size'] = [i/stats[folder]['ploty_size'][0] for i in stats[folder]['ploty_size']]

        # ... and mean time.
        stats[folder]['ploty_time'] = [np.mean([e[1] for e in stats[folder]['stats'][:,col]])
                                       for col in range(np.shape(stats[folder]['stats'])[1])]
        # (relative to FLAC-0)
        stats[folder]['ploty_time'] = [i/stats[folder]['ploty_time'][0] for i in stats[folder]['ploty_time']]

        # Rough 'effectivity' estimation -size*time + 2
        # Expl.: Starts at (0,1), therefore flipping with (-1) requires
        #        + 2. Without (-1) would be 'badness'
        stats[folder]['ploty_eff'] = [ 2 + (-1) * stats[folder]['ploty_size'][i] * stats[folder]['ploty_time'][i]
                                      for i in range(len(stats[folder]['ploty_size']))]

    with open(args.outfile, 'wb') as of:
        data = {}
        data['stats'] = stats
        data['folders'] = folders
        data['cycles'] = args.cycles
        data['maxcompression'] = args.maxcompression
        pkl.dump(data, of, protocol=pkl.HIGHEST_PROTOCOL)

    if os.path.isdir(temp_folder): subprocess.run(['rm', '-r', temp_folder])

else:
    with open(args.infile, 'rb') as f:
        data = pkl.load(f)
        stats = data['stats']
        folders = data['folders']
        args.maxcompression = data['maxcompression']
        args.cycles = data['cycles']

    fig = plt.figure()

    plotx = range(args.maxcompression)
    pos = range(len(plotx))

    ax_size = fig.add_subplot(111)
    ax_size.set_xticks(pos)
    ax_size.set_xticklabels(plotx)
    ax_size.set_title('FLAC compression comparison')

    ax_time = ax_size.twinx()
    ax_efficiency = ax_size.twinx()

    colorfracs = [i / (len(folders)-0.9) if i > 0 else 0 for i in range(len(folders))]

    # Actual plotting
    lns = []
    for cfrac, folder in zip(colorfracs, folders):

        color = plt.cm.viridis(cfrac)
        l_size, = ax_size.plot(plotx, stats[folder]['ploty_size'],
                               color=color, linestyle=':',
                               label='Size Ratio: {}'.format(folder))
        l_time, = ax_time.plot(plotx, stats[folder]['ploty_time'],
                               color=color, linestyle='--',
                               label='Time Ratio: {}'.format(folder))
        l_eff, = ax_efficiency.plot(plotx, stats[folder]['ploty_eff'],
                               color=color, linestyle='-',
                               label='Efficiency: {}'.format(folder))
        lns.append(l_size)
        lns.append(l_time)
        lns.append(l_eff)

    ax_efficiency.spines['right'].set_position(('outward', 60))

    ax_size.xaxis.grid(color='.85', linestyle='-', linewidth=.5)

    ax_size.set_xlabel(xlabel)
    ax_size.set_ylabel(ylabel_size)
    ax_efficiency.set_ylabel(ylabel_efficiency)
    ax_time.set_ylabel(ylabel_time)

    lgd = ax_time.legend(handles=lns, loc='upper center',
                   bbox_to_anchor=(0.5, -.15), facecolor='#FFFFFF',
                         prop={'family': 'monospace','size': 'small'})

    fig.savefig(args.outfile, bbox_inches='tight', dpi=300)
  • Whoa... this is an awesome quantitative analysis you did there! I really appreciate that you took the time to do all this. Couldn't have been fast, but really great results. Thanks! – pepoluan Mar 9 at 4:25
0

Flac 0. Storage is so cheap these days, seems like a no brainer to me... also Flac 0 is less likely to hiccup on a slower system since decoding it is less demanding to decode.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.