Take the 2-minute tour ×
Super User is a question and answer site for computer enthusiasts and power users. It's 100% free, no registration required.

The LAME encoder stores a CRC16 checksum of the audio stream in the header of every MP3 it encodes. The 'actual' audio checksum can then be computed and compared to the original value at a later date to verify whether the audio has been damaged (without having to worry about ID3 tags and the like changing the computed value).

On Windows, there was a command-line utility called LameTag that was capable of computing the checksum and comparing it to the original. Unfortunately, it's abandoned and probably not easily portable to OS X, which of course is what I use. I think EncSpot is capable of doing the same, but again it is Windows-only.

My question is: Are there any utilities like this that are compatible with Mac, Linux, BSD, or similar?

There are a few I've found that can show the original CRC (like eyeD3), but they can't compute the current one. There are also several utilities that claim to check for corruption in MP3s, but I haven't found any that actually use the MusicCRC frame — most of them seem to be using a more generic method of checking, or else they use frame CRCs (which are disabled by default in LAME and can't be relied on).

edit:
I guess i've answered my own question. In trying to research this, i stumbled across a Python script for mutagen, QuodLibet's audio meta-data library. The script is designed to read LAME's Info Tag, and although it does not deal with either of the CRC fields specifically, i was able to create something that does based off of its example. After a few hours of messing with things (i'm a terrible programmer and know absolutely nothing about Python) i finally managed to write something that, although featureless and slow, does return the original CRCs and compute the new ones:

# Known good track
kapche-lanka:test % ../mp3crc.py "10 - CLAW FINGER.mp3"
10 - CLAW FINGER.mp3:
    Original MusicCRC:     8171
    Computed MusicCRC:     8171
    Original Info Tag CRC: AEFD
    Computed Info Tag CRC: AEFD

# Known bad track
kapche-lanka:test % ../mp3crc.py "10 - Griffons Never Die.mp3"
10 - Griffons Never Die.mp3:
    Original MusicCRC:     2014
    Computed MusicCRC:     BCF1
    Original Info Tag CRC: DF02
    Computed Info Tag CRC: DF02

I will update this post one more time to add a link to the script, whenever i get it working in a more serious fashion.

Thanks!

edit2:
I've added a link to my script below (see accepted answer). It's called mp3crc and, although not expertly designed, it seems to work for the most part:

https://github.com/ohkine/mp3crc

share|improve this question
    
Recommend you port that tool: phwip.wordpress.com/home/audio it is unfortunately written in Pascal. –  dlamblin Nov 18 '11 at 20:01
    
Sadly it is far beyond my ability to port Delphi applications (or probably any others) to UNIX. :/ –  kine Nov 18 '11 at 20:06
    
Yeah I tried the Free Pascal Complier in Delphi mode and it told me it doesn't support APPTYPE on Linux and it couldn't find TnTClasses. –  dlamblin Nov 18 '11 at 20:20
    
You'll probably need to remove the $APPTYPE declaration, and TnTClasses is part of the TNT Unicode Controls. –  afrazier Nov 18 '11 at 21:34
    
@afrazier well, you'd need to replace the TntSystem and TntSysUtils with something providing the same functionality. We could port this to C, D, Perl, Python, Java? Or just drop in replacements somehow. TntClasses requires Windows which isn't there, granted LameTag.dpr requires Windows too. –  dlamblin Nov 21 '11 at 6:09
show 1 more comment

3 Answers

Here's a Bash shell function called lameCRC() that calculates the LAME musicCRC and CRC-16 of the Xing/Info-LAME header frame (as specified by the Mp3 Info Tag rev 1 specifications - draft 0) by using Apple's afinfo command and the crc command line tool by Hampa Hug, http://www.hampa.ch/misc-utils/index.html.

If Apple's afinfo command is not available, dd will be used (which will lead to a speed bump though).

(Note: I deliberately avoided Bash's internal string functions to make portability easier).

lameCRC() {     # Bash shell function

   # lameCRC() uses the crc command line tool from http://www.hampa.ch/misc-utils/index.html.
   # lameCRC() is partly inspired by the output of Apple's afinfo command and 
   # the C source code of Audio-Scan-0.93 and MP3-Cut-Gapless-0.03 by Andy Grundman:
   # https://metacpan.org/author/AGRUNDMA
   # Audio-Scan-0.93/src/mp3.c          (GNU General Public License Version 2, June 1991 or later)
   # Audio-Scan-0.93/include/mp3.h      ( ditto )
   # MP3-Cut-Gapless-0.03/src/mp3cut.c  ( ditto )

   # usage: lameCRC lame.mp3

   # Basic information: 
   # Mp3 Info Tag rev 1 specifications, http://gabriel.mp3-tech.org/mp3infotag.html
   # LAME info header zone A has a length of 120 bytes (or 240 chars in xxd hex output).
   # The "LAMEx." string is followed by 30 bytes (or 60 chars in xxd hex output) according to the 
   # "Suggested Info Tag extension fields + layout" in the Mp3 Info Tag rev 1 specifications.

   local n n1 n2 lines plus crcs hexchar lame_start_idx xinginfo_start_idx PATH

   PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin

   [[ ! -x '/usr/local/bin/crc' ]] && 
      { printf '%s\n' 'No crc command line tool in /usr/local/bin!' 'See: http://www.hampa.ch/misc-utils/index.html' 1>&2; }

   # get Xing|Info|LAME strings and their offsets in binary file
   lines="$(strings -a -n 4 -t d "$1" | grep -E --line-buffered 'Xing|Info|LAME.\.' | head -n 2)" 

   [[ $(echo "$lines" | grep -E -c 'Xing|Info') -ne 1 ]] ||
   [[ $(echo "$lines" | grep -E -c 'LAME[^ ]{5}') -ne 1 ]] && {
      echo 'No Xing|Info string or correct LAME encoder version string (e.g. LAME3.98r) found!' 1>&2; 
      echo "$lines" 1>&2;
      return 1; 
   }

   # get offset index numbers of Xing|Info|LAME strings
   lame_start_idx="$(printf '%s' "$lines" | awk '/LAME/{print $1}' )"
   xinginfo_start_idx="$(printf '%s' "$lines" | awk '/Xing|Info/{print $1}' )"

   # get possible offset of LAME string in output of strings command
   # LAME version string should consist of 9 chars, but may have a prefix in output of strings command
   # example:  9LAME3.98r         instead of   LAME3.98r
   # example:  7LAME3.88 (beta)   instead of   LAME3.88 (beta)
   #plus="$(printf '%s' "$lines" | sed -n 's/^[^ ]*[ ][ ]*\([^ ]*\)LAME[^ ]\{5\}.*/\1/p' | tr -d '\n' | wc -c)"   # use [^ ]\{5\} ?
   plus="$(printf '%s' "$lines" | sed -n 's/^[^ ]*[ ][ ]*\([^ ]*\)LAME.*/\1/p' | tr -d '\n' | wc -c)"

   lame_start_idx=$(( $lame_start_idx + $plus ))

   [[ $(( $lame_start_idx - $xinginfo_start_idx )) -ne 120 ]] && 
      { echo 'No 120 bytes between Xing / Info and LAME string. Exiting ...' 1>&2; return 1; }

   # get entire LAME info tag
   #dd if="$1" bs=1 skip="$lame_start_idx" count=36 2>/dev/null |  LC_ALL=C od -A n -cv; return 0

   # get bytes $BC-$BD (MusicCRC) and bytes $BE-$BF (CRC-16 of Info Tag) (as described in http://gabriel.mp3-tech.org/mp3infotag.html)
   crcs="$(dd if="$1" bs=1 skip="$(( $lame_start_idx + 32 ))" count=4 2>/dev/null | xxd -p | tr -d '\n')"

   [[ -z "$crcs" ]] && { echo 'No LAME musicCRC and CRC-16 of Info Tag found!' 1>&2; return 1; }

   lameMusicLengthPlusCRCs="$(dd if="$1" bs=1 skip=$(( $lame_start_idx + 28 )) count=8 2>/dev/null | xxd -p | tr -d '\n')"
   lameMusicLength="$(echo "$lameMusicLengthPlusCRCs" | cut -b 1-8 )"
   lameMusicCRC1="$(echo "$lameMusicLengthPlusCRCs" | cut -b 9-10 )"   # cf. http://gabriel.mp3-tech.org/mp3infotag#musiccrc
   lameMusicCRC2="$(echo "$lameMusicLengthPlusCRCs" | cut -b 11-12 )"
   lameInfoTagCRC16="$(echo "$lameMusicLengthPlusCRCs" | cut -b 13-16 )"

   # LAME MusicLength consists of: 
   # [LAME Tag frame][complete mp3 music data]
   lameMusicLengthByteSize=$(printf '%d' "0x${lameMusicLength}")

   [[ $lameMusicLengthByteSize -le 0 ]] && { echo 'lameMusicLengthByteSize <= 0. Exiting ...' 1>&2; return 1; }


   if [[ -x '/usr/bin/sw_vers' ]] && [[ "$(/usr/bin/sw_vers -productName)" == "Mac OS X" ]] && [[ -x '/usr/bin/afinfo' ]]; then

      # get audio_bytes, i. e. [complete mp3 music data] - [LAME Tag frame]

      #id3v2 --delete-all "$1" 1>/dev/null  # for testing purposes; edits file in-place!
      # afinfo seems to be only available on Mac OS X 
      # afinfo alternative: Perl module Audio-Scan-0.93 by Andy Grundman 
      # perl -e 'use Audio::Scan; my $offset = Audio::Scan->find_frame($ARGV[1],0); print "$offset\n";' _ file.mp3
      audioinfo="$(afinfo "$1")"  
      audio_bytes="$(echo "$audioinfo" | awk -F" " '/audio bytes:/{print $NF}' )"
      audio_data_file_offset="$(echo "$audioinfo" | awk -F" " '/audio data file offset:/{print $NF}')"
      xingInfoLameTagFrameSize=$(( lameMusicLengthByteSize - audio_bytes ))

      [[ $audio_bytes -le 0 ]] && { echo 'audio_bytes <= 0. Exiting ...' 1>&2; return 1; }

      # 0..xingInfoLameTagFrameOffset (match first 0xff byte in mp3 file)
      n=0
      hexchar=""
      until [[ "$hexchar" == 'ff' ]]; do
         hexchar="$(dd if="$1" bs=1 skip=$n count=1 2>/dev/null | xxd -p)"
         n=$(( n + 1))
      done
      xingInfoLameTagFrameOffset=$(( n - 1 ))

   else   # dd speed bump

      # get xingInfoLameTagFrameSize
      # for mp3 magic numbers (\xFF\xFB) see: 
      # http://www.digitalpreservation.gov/formats/fdd/fdd000105.shtml

      # n1
      # count bytes from: 0xff...<--...$xinginfo_start_idx
      hexchar=""
      n=$xinginfo_start_idx
      until [[ "$hexchar" == 'ff' ]]; do
         n=$(( n - 1))
         hexchar="$(dd if="$1" bs=1 skip=$n count=1 2>/dev/null | xxd -p)"
      done
      xingInfoLameTagFrameOffset=$n
      n1=$(( xinginfo_start_idx - n ))

      # n2
      # count bytes from: $xinginfo_start_idx+120+36...-->...0xff
      hexchar=""
      n=$((xinginfo_start_idx + 120 + 36))
      until [[ "$hexchar" == 'ff' ]]; do
         hexchar="$(dd if="$1" bs=1 skip=$n count=1 2>/dev/null | xxd -p)"
         n=$(( n + 1))
      done
      n2=$(( n - xinginfo_start_idx - 120 - 36 - 1 ))   # - 1 because the trailing 0xff got counted by $n

      xingInfoLameTagFrameSize=$(( $n1 + $n2 + 120 + 36 ))
      audio_data_file_offset=$((xingInfoLameTagFrameOffset + xingInfoLameTagFrameSize))

      # get audio_bytes, i. e. [complete mp3 music data] - [LAME Tag frame]
      audio_bytes=$( printf "%s\n" "scale = 0; ${lameMusicLengthByteSize} - ${xingInfoLameTagFrameSize}" | bc )

   fi

   old_lameInfoTagCRC16="$lameInfoTagCRC16"
   new_lameInfoTagCRC16="$(head -c $(( xingInfoLameTagFrameOffset + xingInfoLameTagFrameSize )) "$1" | 
          tail -c ${xingInfoLameTagFrameSize} | head -c 190 | crc -R -r -g crc16)"

   old_lameMusicCRC16="${lameMusicCRC1}${lameMusicCRC2}"
   new_lameMusicCRC16="$(head -c $(( ${audio_data_file_offset} + ${audio_bytes} )) "$1" | 
          tail -c ${audio_bytes} | crc -R -r -g crc16)"

   echo
   printf '%s\n' "old_lameInfoTagCRC16: ${old_lameInfoTagCRC16}" "new_lameInfoTagCRC16: ${new_lameInfoTagCRC16}"
   echo
   printf '%s\n' "old_lameMusicCRC16: ${old_lameMusicCRC16}" "new_lameMusicCRC16: ${new_lameMusicCRC16}"
   echo

   return 0
}
share|improve this answer
add comment
up vote 0 down vote accepted

I'll answer my own question here:

In trying to research this, i stumbled across a Python script for mutagen, QuodLibet's audio meta-data library. The script is designed to read LAME's Info Tag, and although it does not deal with either of the CRC fields specifically, i was able to create something that does based off of its example. After a few hours of messing with things (i'm a terrible programmer and know absolutely nothing about Python) i finally managed to write something that, although featureless and slow, does return the original CRCs and compute the new ones. It's still a little buggy, but on my own library it turned out to be at least 90% accurate, so i'll 'release' it i guess. It's called mp3crc and is available on GitHub:

https://github.com/ohkine/mp3crc

The script should run on UNIX and Windows, although there is currently a Windows-only Unicode issue that needs fixed. It also requires crcmod and mutagen to be installed (i include them in the repository but you can install them however).

As mentioned i'm not a very good programmer, so i apologise in advance for how embarrassing the code probably is. But it mostly works :)

share|improve this answer
add comment

There seems to be a C port of LameTag_Source_0.4.1/CRC16.pas called mp3_check-1.98/crctest.c (which is a command line tool).

Here's a hacked version of mp3_check-1.98/crctest.c that that will calculate the CRC16 checksum of a given mp3 file.

/*

modified version of source code taken from:
mp3_check-1.98/crctest.c,
http://sourceforge.net/projects/mp3check/

NOTE:
compare mp3_check-1.98/crctest.c with
LameTag_Source_0.4.1/CRC16.pas from
http://phwip.wordpress.com/home/audio/ 

See also: 
mp3check - check mp3 files for integrity,
http://jo.ath.cx/soft/mp3check/

gcc -Wall -Wextra -03 -o crctest crctest.c

./crctest *.mp3
printf '%d\n' $(./crctest *.mp3)

*/

#include <stdio.h>
#include <stdlib.h>

int
crcbuf(crc, len, buf)
    register int    crc;    /* running CRC value */
    register unsigned long  len;
    register char *buf;
{

    static short crc_table[] =  {
            0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
            0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
            0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
            0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
            0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
            0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
            0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
            0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
            0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
            0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
            0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
            0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
            0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
            0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
            0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
            0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
            0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
            0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
            0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
            0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
            0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
            0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
            0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
            0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
            0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
            0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
            0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
            0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
            0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
            0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
            0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
            0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
    };

    register unsigned long  i;

    for (i=0; i<len; i++)
            crc = ((crc >> 8) & 0xff) ^ crc_table[(crc ^ *buf++) & 0xff];

    return (crc);
}

int main (int argc, char * argv []) 

{

    if (argc != 2) return(1);

    int crc = 0;
    int newcrc = 0;

    // cf. http://www.linuxquestions.org/questions/programming-9/c-howto-read-binary-file-into-buffer-172985/
    char *name = argv[1];
    FILE *file;
    char *buffer;
    unsigned long fileLen;

    //Open file
    file = fopen(name, "rb");
    if (!file)
    {
            fprintf(stderr, "Unable to open file %s", name);
            return(1);
    }

    //Get file length
    fseek(file, 0, SEEK_END);
    fileLen=ftell(file);
    fseek(file, 0, SEEK_SET);

    //Allocate memory
    buffer=(char *)malloc(fileLen+1);
    if (!buffer)
    {
            fprintf(stderr, "Memory error!");
                            fclose(file);
            return(1);
    }

    //Read file contents into buffer
    fread(buffer, fileLen, 1, file);
    fclose(file);

    newcrc = crcbuf(crc, fileLen, buffer);

    printf("0x%x\n", newcrc);

    free(buffer);

    return(0);

}
share|improve this answer
    
Thank you! However, i don't think i can consider that the answer, because that isn't quite how MusicCRC works. It computes the CRC16 of the audio stream, not the file, which is the great thing about it — if you do the whole file, the CRC will always change due to changes in ID3 tags and so on. The audio stream should always stay the same, though, no matter what (unless you intentionally mess it up with MP3Gain or something). I think i might have answered my own question though: See the edit to my post! –  kine Nov 21 '11 at 23:14
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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