Utilitário compatível com Mac ou Unix para calcular e comparar o LAME MusicCRC em MP3s?

3

O codificador LAME armazena uma soma de verificação CRC16 do fluxo de áudio no cabeçalho de cada MP3 codificado. A soma de verificação de áudio 'real' pode então ser computada e comparada ao valor original em uma data posterior para verificar se o áudio foi danificado (sem ter que se preocupar com tags ID3 e similares, alterando o valor computado).

No Windows, havia um utilitário de linha de comando chamado LameTag que era capaz de calcular a soma de verificação e compará-la ao original. Infelizmente, ele é abandonado e provavelmente não é facilmente transportável para o OS X, que é claro que é o que eu uso. Eu acho que o EncSpot é capaz de fazer o mesmo, mas novamente é somente no Windows.

A minha pergunta é: Existem utilitários como este que são compatíveis com Mac, Linux, BSD ou similares?

Existem alguns que eu encontrei que podem mostrar o original CRC (como eyeD3 ), mas eles não podem computar o atual. Há também vários utilitários que afirmam verificar se há corrupção em MP3s, mas não encontrei nenhum que realmente use o quadro do MusicCRC - a maioria deles parece estar usando um método mais genérico de verificação, ou então eles usam CRCs de quadro (que estão desativados por padrão no LAME e não podem ser confiáveis).

edit:
Acho que respondi minha própria pergunta. Ao tentar pesquisar isso, me deparei com um script Python para mutagen , a biblioteca de meta-dados de áudio do QuodLibet. O script foi projetado para ler a Info Tag do LAME e, embora não lide especificamente com nenhum dos campos do CRC, eu consegui criar algo baseado em seu exemplo. Depois de algumas horas mexendo com coisas (eu sou um péssimo programador e não sei absolutamente nada sobre Python) eu finalmente consegui escrever algo que, embora sem recursos e lento, retorna os CRCs originais e computam o novos:

# 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

Eu atualizarei este post mais uma vez para adicionar um link para o script, sempre que eu trabalhar de forma mais séria.

Obrigado!

edit2:
Eu adicionei um link para o meu script abaixo (veja resposta aceita). Chama-se mp3crc e, embora não tenha um design inteligente, parece funcionar em grande parte:

link

    
por kine 18.11.2011 / 20:25

3 respostas

0

Eu respondo minha própria pergunta aqui:

Ao tentar pesquisar isso, deparei-me com um script em Python para mutagen , a biblioteca de meta-dados de áudio do QuodLibet. O script foi projetado para ler a Info Tag do LAME e, embora não lide especificamente com nenhum dos campos do CRC, eu consegui criar algo baseado em seu exemplo. Depois de algumas horas mexendo com coisas (eu sou um péssimo programador e não sei absolutamente nada sobre Python) eu finalmente consegui escrever algo que, embora sem recursos e lento, retorna os CRCs originais e computa os novos. Ainda é um pouco buggy, mas na minha própria biblioteca acabou por ser pelo menos 90% de precisão, então eu vou 'liberar', eu acho. É chamado de mp3crc e está disponível no GitHub:

link

O script deve ser executado no UNIX e no Windows, embora atualmente exista um problema Unicode somente no Windows que precisa ser corrigido. Também requer que crcmod e mutagen sejam instalados (incluo-os no repositório, mas você pode instalá-los).

Como mencionei, eu não sou um bom programador, então peço desculpas antecipadamente pelo quão embaraçoso o código provavelmente é. Mas funciona principalmente:)

    
por 23.11.2011 / 05:20
2

Aqui está uma função do shell Bash chamada lameCRC() que calcula o LAME musicCRC e CRC-16 do quadro de cabeçalho Xing / Info-LAME (conforme especificado pelo Informações de Mp3 Tag rev 1 especificações - rascunho 0 ) usando o comando afinfo da Apple e a ferramenta de linha de comando crc de Hampa Hug, link .

Se o comando afinfo da Apple não estiver disponível, dd será usado (o que levará a uma elevação de velocidade).

(Nota: eu deliberadamente evitei as funções de string internas do Bash para facilitar a portabilidade).

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\}.*//p' | tr -d '\n' | wc -c)"   # use [^ ]\{5\} ?
   plus="$(printf '%s' "$lines" | sed -n 's/^[^ ]*[ ][ ]*\([^ ]*\)LAME.*//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
}
    
por 07.12.2011 / 15:36
1

Parece haver uma porta C de LameTag_Source_0.4.1 / CRC16.pas chamada mp3_check-1.98 / crctest.c (que é uma ferramenta de linha de comando).

Aqui está uma versão hackeada de mp3_check-1.98/crctest.c que irá calcular a soma de verificação CRC16 de um determinado arquivo mp3.

/*

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);

}
    
por 21.11.2011 / 19:41