Ошибка

wavcrc32: Программа для проверки CRC аудиоданных, или о том, как EAC считает CRC.

Тема закрыта
 
Автор Сообщение

Shamik

Пол: Пол:Муж

Стаж: 15 лет

Сообщений: 642

Кокосовые острова
Рейтинг

post 05-Мар-2010 17:21 [-]0[+]

Quote

Внимание: текст статьи пока НЕ адаптирован на массового читателя, если Вы что-то не поняли -- это НЕ СТРАШНО!
Огромное спасибо BakLAN, siro. и DrStandBy за замечания и предложения, а также найденные недочёты

Как мы все хорошо знаем, EAC при рипании дисков подсчитывает контрольную сумму аудиоданных, и записывает её в LOG-файл. При этом он умеет делать это двумя способами:
1) при подсчёте учитываются все семплы (в т.ч. и нулевые)
2) при подсчёте учитываются семплы, за исключением нулевых
Выбор способа контролируется галкой EAC->EAC Options->1-я вкладка->No use nul samples in CRC calculations

Зачем оно туда пишется? Да так, чисто ради контроля. Кроме того, оно бывает полезно при склейке потрековых релизов, которые были нарезаны из целикового образа. Да и вообще, ну мало ли зачем бывает нужна контрольная сумма... Ну а как же её проверить? Ну не писать же на блин, чтобы рипнуть. Да нет, есть способ проще -- Tools->Process WAV и там в углу высвечивается контрольная сумма.

И всё бы было ничего, если бы в EAC в этом самом месте не было бы ошибки. А именно, если галка выключена, то всё хорошо -- и аудиоредактор, и грабилка, считают контрольную сумму одинаковым алгоритмом, и значения получаются одни и те же. Таким образом, если рип был сграблен без галки, то в логе будет значение CRC "без галки", то есть с учётом всех сэмплов, и аудиоредактор подсчитает его же. А вот если галка при рипании была включена, то вот тут начинаются чудеса. Если этот рип потом пропустить через аудиоредактор, не меняя состояния галки, то мы НЕ увидим того значения CRC, которое у нас в логе. А причина этого в том, что алгоритм, подсчитывающий CRC в аудиоредакторе EAC, содержит ошибку.

Примечание. Факт несовпадения контрольных сумм хорошо известен в соответствующих кругах. Однако я не знаю, обладает ли кто-либо ещё знанием про то, откуда это несоответствие берётся.

Так вот, уважаемые дамы и господа, мне удалось путём анализа кода EAC, установить, где именно в этом алгоритме имеется ошибка. Более того, я написал программу, которая считает для заданного WAV-файла все три возможные значения контрольной суммы. Таким образом, теперь, если мы имеем рип и в логе указана какая-то контрольная сумма, то мы имеем возможность пересчитать её для образа и проверить, что она соответствует тому, что там написано в логе. Конечно, это не спасает от тех, кто подделывал логи -- от таких вообще не спрячешься, подделать можно всё.

На 99% уверен, что это именно ошибка, а не сознательно допущенное разногласие. Хотелось бы, чтобы эта ошибка была исправлена в последней версии EAC.

Ошибка заключается в том, что EAC учитывает данные только по одному каналу при подсчёте CRC в аудиоредакторе при включённой галке. Не буду приводить соответствующий фрагмент ассемблерного листинга, укажу лишь, что достаточно сравнить код процедур по адресу .45E590 и .45E520. Первая из них работает правильно, вторая -- ошибочна. Аналогичные процедуры для подсчёта CRC при граблении не содержат ошибок. Кому очень интересно, могу выдать листинг из IDA (кстати говоря, я безмерно признателен автору этого поистине гениального средства для исследования чужого кода!).

В заключение я привожу текст программы на C/C++, которая вычисляет CRC всеми нужными способами (и правильными, и неправильными). Программа может быть скомпилирована любым компилятором (например gcc, visual c++, borland c++, ...) в виде (консольного) приложения, и, хочется верить, является платформенно-независимой.

Код:

/*
----------------------------------------------------------
wavcrc32.cpp
Version 0.22
----------------------------------------------------------
A program for counting audiodata CRC checksums
that used in EAC (Exact Audio Copy by Andre Wiethoff)
----------------------------------------------------------
This program is freeware and open-source, you may
freely redistribute or modify it, or use code in your
applications.
----------------------------------------------------------
I hope this code can be compiled with any C/C++ compiler
like microsoft visual c++, gcc, open-watcom c/c++, ...
If you experience any problems with compiling this program
you may feel free to ask me, what's the hell it is, maybe
I can help you.

At this time I've checked that
* gcc/g++ 3.4.4 @ cygwin
* msvc 8.0
* open watcom c/c++ 1.6
work fine with this code!
----------------------------------------------------------
(c) ]DichlofoS[ Systems, 2007
mailto:dmvn@mccme.ru
----------------------------------------------------------
*/

#if _MSC_VER >= 1400
  // this option is for Safe STDLIB fopen_s, etc functions
  #define MSC_SAFECODE 1
#else
  #undef MSC_SAFECODE
#endif

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

#if _MSC_VER >= 1400
  #include <windows.h>
#endif

#define SAMPLE_BUFFER_SIZE 1024

// Function prototypes
void InitCRC32Table();
unsigned int Reflect(unsigned int ref, char ch);
bool CheckDWORD(FILE* f, unsigned int nOffset, unsigned int nProperValue, const char* szMessage);
unsigned int CheckRIFFHeader(FILE* f);

unsigned int CRC32Table[256];

// Call this function only once to initialize the CRC table
void InitCRC32Table()
{
  unsigned int ulPolynomial = 0x04c11db7;

  // 256 values representing ASCII character codes.
  for(int i = 0; i <= 0xFF; i++)
  {
    CRC32Table[i] = Reflect(i, 8) << 24;
    for (int j = 0; j < 8; j++)
      CRC32Table[i] = (CRC32Table[i] << 1) ^ (CRC32Table[i] & (1 << 31) ? ulPolynomial : 0);
    CRC32Table[i] = Reflect(CRC32Table[i], 32);
  }
}

/*
  Reflection is a requirement for the official CRC-32 standard.
  You can create CRCs without it, but they won't conform to the standard.
  Used only by InitCRC32Table()
*/
unsigned int Reflect(unsigned int ref, char ch)
{
  unsigned int value(0);

  // Swap bit 0 for bit 7
  // bit 1 for bit 6, etc.
  for (int i = 1; i < (ch + 1); i++)
  {
    if (ref & 1)
      value |= 1 << (ch - i);
    ref >>= 1;
  }
  return value;
}

/*
  Checks DWORD in WAV file on validness
*/
bool CheckDWORD(FILE* f, unsigned int nOffset, unsigned int nProperValue, const char* szMessage)
{
  unsigned int dwCheck = 0;
  fseek(f, nOffset, SEEK_SET);
  fread(&dwCheck, 1, 4, f);
  if (dwCheck != nProperValue)
  {
    printf("%s\n", szMessage);
    return false;
  }
  return true;
}
/*
  Checks RIFF header in WAV file on validness
*/
unsigned int CheckRIFFHeader(FILE* f)
{
  if (!CheckDWORD(f, 0x00, 0x46464952, "*** non-RIFF format!")) return 0;
  if (!CheckDWORD(f, 0x08, 0x45564157, "*** non-WAVE format!")) return 0;
  if (!CheckDWORD(f, 0x0C, 0x20746d66, "*** cannot find format chunk!")) return 0;
  if (!CheckDWORD(f, 0x10, 0x00000010, "*** invalid format chunk size!")) return 0;
  if (!CheckDWORD(f, 0x14, 0x00020001, "*** invalid audio format!")) return 0;
  if (!CheckDWORD(f, 0x24, 0x61746164, "*** cannot find data chunk!")) return 0;

  unsigned int dwDataSize = 0;
  fseek(f, 0x28, SEEK_SET);
  fread(&dwDataSize, 1, 4, f);
  return dwDataSize;
}

int main(int argc, char** argv)
{
  InitCRC32Table();

#if _MSC_VER >= 1400
  DWORD StartTime = GetTickCount();
#endif

  printf("EAC wavcrc32 v0.22 (c) ]DichlofoS[ Systems, 2007\nthanx to: BakLAN, siro and other beta-testers/contributors!\n");
  if (argc != 2)
  {
    printf("*** invalid cmdline arguments!\nsyntax: wavcrc32 filename.wav\n");
    return -1;
  }
  const char* sFileName = argv[1];

#ifdef MSC_SAFECODE
  FILE* f = 0;
  fopen_s(&f, sFileName, "rb");
#else
  FILE* f = fopen(sFileName, "rb");
#endif

  if (!f)
  {
    printf("*** cannot open file!\n");
    return -1;
  }
  printf("file opened ok\n");

  // check header
  unsigned int nDataSize = CheckRIFFHeader(f);
  if (!nDataSize)
  {
    fclose(f);
    return -1;
  }
  printf("RIFF header checked ok\n");

  unsigned char chBuffer[4*SAMPLE_BUFFER_SIZE];

  unsigned int nSampleCount = nDataSize >> 2;

  unsigned int crc(0xffffffff);
  unsigned int crcns(0xffffffff);
  unsigned int crcnsl(0xffffffff);
  unsigned int crcnslr(0xffffffff);
  unsigned int crcnb(0xffffffff);

  printf("please wait while couning checksums...\n");
  printf("processing %d samples...\n", nSampleCount);

  unsigned int i = 0;
  while (i < nSampleCount)
  {
    unsigned int nCurrentBlockSize = nSampleCount - i;
    if (nCurrentBlockSize > SAMPLE_BUFFER_SIZE) nCurrentBlockSize = SAMPLE_BUFFER_SIZE;
    if (fread(chBuffer, 1, 4*nCurrentBlockSize, f) != 4*nCurrentBlockSize)
    {
      printf("*** cannot read from input file!\n");
      fclose(f);
      printf("file closed ok\n");
      return -1;
    }
    i += nCurrentBlockSize;
    for (unsigned int k = 0; k < nCurrentBlockSize; k++)
    {
      unsigned char nSample[4];
      *(unsigned int*)nSample = *((unsigned int*)chBuffer+k);
      // calculate checksum skipping nulsamples
      if (*(unsigned int*)nSample)
        for (int j = 0; j < 4; j++)
          crcns = (crcns >> 8) ^ CRC32Table[(crcns & 0xFF) ^ nSample[j]];
      // calculate checksum skipping nulsamples on left/right channel
      if (nSample[0] || nSample[1])
        for (int j = 0; j < 2; j++)
          crcnslr = (crcnslr >> 8) ^ CRC32Table[(crcnslr & 0xFF) ^ nSample[j]];
      if (nSample[2] || nSample[3])
        for (int j = 2; j < 4; j++)
          crcnslr = (crcnslr >> 8) ^ CRC32Table[(crcnslr & 0xFF) ^ nSample[j]];
      // calculate non-zero-bytes checksum on left channel
      if (nSample[0] || nSample[1])
        for (int j = 0; j < 2; j++)
          crcnsl = (crcnsl >> 8) ^ CRC32Table[(crcnsl & 0xFF) ^ nSample[j]];
      // calculate non-zero-bytes checksum
      for (int j = 0; j < 4; j++)
        if (nSample[j])
          crcnb = (crcnb >> 8) ^ CRC32Table[(crcnb & 0xFF) ^ nSample[j]];

      // calculate standard checksum
      for (int j = 0; j < 4; j++)
        crc = (crc >> 8) ^ CRC32Table[(crc & 0xFF) ^ nSample[j]];
    }
  }
  fclose(f);
  printf("file closed ok\n");

  crc = crc^0xffffffff;
  crcns = crcns^0xffffffff;
  crcnsl = crcnsl^0xffffffff;
  crcnslr = crcnslr^0xffffffff;
  crcnb = crcnslr^0xffffffff;

  printf("--- used in EAC:\n");
  printf("generic: grabbing, \"no use...\" off [all bytes]            : %08X.\n", crc);
  printf("generic: grabbing, \"no use...\" on [word nulsamples l/r]   : %08X.\n", crcnslr);
  printf("waveditor [word nulsamples l]                             : %08X.\n", crcnsl);
  printf("--- no used in EAC, only for reference:\n");
  printf("byte ignore zeros                                         : %08X.\n", crcnb);
  printf("dword nulsamples                                          : %08X.\n", crcns);

#if _MSC_VER >= 1400
  printf("---\ntime elapsed: %d ms\n", GetTickCount()-StartTime);
#endif
  return 0;
}
В случае возникновения каких-либо замечаний или предложений, просьба писать либо мне в личку, либо в этот топик. Да, сразу скажу: я писал этот код максимально прозрачным, чтобы не затмевать суть дела.

Кроме того, по инициативе некоторых уважаемых граждан, была разработана GUI-версия данной программы для Windows (впрочем, запустить её под wine не составляет никакого труда).
CON 0.1 Первая публичная версия
CON 0.2 Добавлена обработка ошибок (в том числе анализ RIFF-заголовка), прооптимизирован код.
CON 0.21 Исправлены мелкие недочёты в выводе (10x siro.), включена оптимизация в watcom-версии (10x BakLAN), код избавлен от компиляторной зависимости.
CON 0.22 Незначительные исправления, добавлен (windows-only) код вычисления времени подсчёта.

GUI 0.1 Первая публичная версия
GUI 0.11 Исправлено несколько ляпов в коде, рекомендуется обновить
Желающие протестировать данный программный продукт приветствуются! Если Вы не можете скомпилировать консольную версию под свою ОС, пожалуйста, обращайтесь, попробуем Вам помочь. В приложении к данному посту имеются архивы с обеими версиями программы для Windows (бинарники + четыре dll-ки, которые нужно кинуть в %WINDIR%\System32, если их там ещё нету + исходник для сборки под *nix).

Возможные проблемы при запуске консольной версии: если не запускается программа wavcrc32.exe, попробуйте wavcrc32-watcom.exe. Если есть MSVC и опыт работы в нём, можете попробовать пересобрать программу под Windows (исходник кросплатформенный, проблем при компиляции быть не должно).

Если не нужна консольная версия, Вы можете отдельно взять только GUI-версию программы для Windows (приложена отдельным архивом вторым моим постом в теме).

Спасибо за внимание!

Статью поготовил и написал исходный код © dmvn, Огромное спасибо ему!
info Profile PM
Показать сообщения:    
Тема закрыта

Текущее время: 21-Ноя 13:32

Часовой пояс: GMT + 4



Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы