6

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
11

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)
| improve this answer | |
  • 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 '19 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.

| improve this answer | |
0

As a followup to Suuuehgi's answer, I'd also like to add that if you are starting from a CD and ripping it directly to FLAC, encoding time may not matter at all because you have to first rip the music, which takes time.

Here's what I tried:

Using dbPowerAmp CD Ripper, I ripped my copy of Mariah Carey's "Merry Christmas" album. I ripped it once at FLAC compression level 8, once at level 5 (dbPowerAmps default), and once at level 0.

Here's the total times for each rip, from clicking start, to end with all FLAC files done:

Level 0 = 6:19

Level 5 = 6:18

Level 8 = 6:23

As you can see, the variance between all 3 is minimal, within < 5 seconds of each other. As I watched it rip and encode, the encoding status was a mere flash on the screen, barely registered. And watching the file system as it was ripping, it appeared to be encoding on the fly as it was ripping. YMMV on slower systems however.

As for file sizes, here are the file sizes produced:

Level 0 = 278 MB

Level 5 = 257 MB

Level 8 = 256 MB

enter image description here

While the total rip and encode times were basically the same, the file sizes were not, HOWEVER, there is definitely diminishing returns in the later compression levels (as Suuuehgi's answer alludes to).

To me, it seems that if you are starting from CD's and have a decent PC, the time it takes to rip and encode won't change much based on the FLAC compression level. The file size does change however. I think dbPowerAmps suggestion of FLAC level 5 as a default is a good one. Only 1 MB difference between FLAC 5 and FLAC 8, where-as if you go FLAC 0, my example shows 21 MB in excess storage that could be saved. That may not seem like much, but when you are ripping vast collections, it adds up quick (a single FLAC song can be around that size.)

This was done on a desktop with a USB 2 DVD drive, ripping on average at 7x speed. My desktop PC specs are a Intel Core i5-6500 CPU @ 3.2Ghz, 16GB of RAM, and a Samsung 860 EVO Sata SSD drive.

| improve this answer | |

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.