Программирование / [Из песочницы] MP3 MusicID по аудио отпечатку файла в своей программе на С#

в 9:19, , рубрики: Audio, mp3, Программирование, метки: , , ,

Недавно у меня возникла такая необходимость — использовать распознавание музыки в собственной программе. Программа разрабатывалась на C#, это сортировщик коллекции mp3 файлов, заточенный под себя. В двух словах скажу, что к примеру, он умеет распознавать по ID3 тэгам принадлежность произведения к классической музыке, и соответственно раскладывать по папкам на диске не /Исполнитель (год-год)/Альбом (год)/Произведение, а /Композитор/Альбом/Произведение.
Изначально, мне пришла в голову идея использовать для этой цели Winamp, так как он неплохо справляется с задачей распознавания ID тэгов по отпечатку аудио. Есть идея — есть реализация! В списке треков появилась кнопочка, которая создает в %TEMP% m3u список и запускает Winamp передавая ему m3u как параметр, а приложение отслеживает ChangeFileNotification и если тэги были обновлены в Winamp — автоматом обновляет информацию в своих окошках. Однако спустя некоторое время я понял, что это неудобно, непонтово и решил вживить в код такую же фичу, как в Winamp.
Первый запрос к google по словам «MusicID develop» приводят нас на сайт gracenote.com. Здесь http://www.gracenote.com/products/musicid/ можно кратко почитать о технологии, а здесь https://doors.gracenote.com/developer/registered.html находятся SDK. Чтобы использовать SDK необходимо зарегистрироваться, что я и сделал, а также направил в gracenote дополнительное письмо, в каких целях я хочу использовать их технологии, чтобы гарантировано прислали ключики :-). На следующей день у меня был login и password для скачки SDK, а также хэши для доступа к сервису cddb и musicid.
После недолгого исследования и изучения архива MusicID-CD_SDK_2.6.206.zip я понял, что комплект мне прислали не полный :-) А именно, SDK рассчитан на использование cddb, прекрасно справляется с распознаванием по тэгам, и предлагает наиболее похожий вариант по информации, которая уже есть в тэгах, но если в mp3 нет тэгов, то по отпечатку аудио он распознать не может, а это собственно самое вкусное, то, за что я пытаюсь бороться.
После нескольких дней ковыряния dll-ок winamp, использующих gracenote методом проб и ошибок мне удалось добиться распознавания mp3 по fingerprint, и этой информацией я хочу поделиться, надеюсь окажется кому-то полезна.
Итак, что нам понадобится для достижения нашей цели:
1. Установленная студия с C#
2. Dll: CDDBControlWinamp.dll, CddbMusicIDWinamp.dll из каталога WinampPluginsGracenote
3. Библиотека alvas.audio с www.alvas.net, декодирует mp3 в wav с любой секунды, заданной длительности.
4. mp3 файл, с названием типа 0.mp3 без тэгов, который мы будем распознавать.
CDDBControlWinamp.dll, CddbMusicIDWinamp.dll являются COM-объектами, тут пояснения излишни.
Alvas.audio не бесплатна, при вызове ReadFormat() выдает занудный диалог о покупке и регистрации, но мне понравилась сама библиотека и удобство пользования, поэтому я ее пропустил через ildasm.exe, убрал показ занудного скрина, заново скомпилировал, дописал ресурсы, манифест и подписал sk.exe. Все путем.
Почему mp3 файл мы называем 0.mp3? Потому, что gracenote умеет предполагать id3 тэги даже по имени файла, а нам сейчас пока это не нужно, попробуем все сделать чисто по audio fingerprint.
Создаем проект в C#, подключаем ссылки на alvas.audio, CDDBControlWinamp.dll, CddbMusicIDWinamp.dll. У CDDBControlWinamp.dll и CddbMusicIDWinamp.dll не забываем поставить Embed Interop Types = False, чтобы использовать COM-классы.using CDDBCONTROLLibNSWinamp;
using CDDBMUSICIDLibNSWinamp;
using System.Runtime.InteropServices;
using Alvas.Audio;
Создаем нужные нам объекты:CDDBControl2Class control = new CDDBControl2Class();
CDDBNSWinampMusicIDManagerClass m = new CDDBNSWinampMusicIDManagerClass();
Инициализируем. В SetClientInfo передаем хэш доступа на сервис, присланный в письме от gracenote, версию программу, об этом можно почитал в pdf в SDK.control.SetClientInfo("XXXXXX", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "1", "");
if (control.IsRegistered(0) == 0)
control.IsRegistered(1);
m.Initialize(control);
В fil (CddbFileInfoList) вернется информация от сервера о тэгах, в fi (CddbFileInfo) мы указываем серверу, что мы хотим от него узнать, в данном случае указываем только на наш файл 1.mp3.CddbFileInfoList fil = new CddbFileInfoList();
CddbFileInfo fi = new CddbFileInfo();
fi.Filename = @"c:1.mp3";
Теперь нам нужно взять с нашего mp3 отпечаток звуковой волны. И тут есть один нюанс, до которого я доходил целый день, пока меня не осенило: отпечаток библиотека может взять только с wav-данных, то есть несжатых данных с wav-заголовком! То есть если вы хотите взять отпечаток с файла, функцияpublic virtual void GetFingerprintInternal(string Filename, out CDDBMUSICIDLibNSWinamp.CddbMusicIDFingerprint Fingerprint)
не будет работать с mp3 файлом и возвращать ошибку, работает только с wav. Значит, для достижения цели разожмем наш 1.mp3 и возьмем с него отпечаток.
Разжимаем mp3 в wav. Здесь я буду использовать промежуточный файл, но mp3 можно разжать в память и подсунуть библиотеке для снятия отпечатка.FileStream fileStream = File.OpenRead(fi.Filename);
Mp3Reader mp3 = new Mp3Reader(fileStream);
IntPtr originalAudioFormat = mp3.ReadFormat();
byte[] mp3Data = mp3.ReadData(0, 30); // с 0-ой секунды, 30 секунд
mp3.Close();
IntPtr wavFormat = AudioCompressionManager.GetCompatibleFormat(originalAudioFormat,
AudioCompressionManager.PcmFormatTag);
AcmConverter converter = new AcmConverter(originalAudioFormat, wavFormat, false);
byte[] waveData = converter.Convert(mp3Data);
FileStream of = File.Create(@"c:wav.data");
WaveWriter ww = new WaveWriter(of, AudioCompressionManager.FormatBytes(wavFormat));
ww.WriteData(waveData);
ww.Close();
Следующим кодом я покажу как создать аудио отпечаток не из файла, а из памяти. То есть прочитаем созданный файл в память и снимем отпечаток.CddbMusicIDFingerprinter finger = m.CreateFingerprinter("Cantametrix");
finger.BeginAudioStream(44100, 16, 2);
byte[] managedArray = new byte[10000000];
using (FileStream fsSource = new FileStream(@"c:wav.data",
FileMode.Open, FileAccess.Read))
{
int numBytesToRead = (int)managedArray.Length;
int numBytesRead = 0;
int n = fsSource.Read(managedArray, numBytesRead, numBytesToRead);
int size = Marshal.SizeOf(managedArray[0]) * managedArray.Length;
IntPtr pnt = Marshal.AllocHGlobal(size);
Marshal.Copy(managedArray, 0, pnt, managedArray.Length);
finger.WriteAudioData(pnt, managedArray.Length);
}
fi.Fingerprint = finger.EndAudioStream(); // взять отпечаток!
Создаем callback, куда придет инфа от сервера:int n1 = 0;// = (int)CddbMusicIDMatchCode.MUSICID_MATCH_NONE;
m.TrackIDComplete += new _ICDDBMusicIDManagerEvents_TrackIDCompleteEventHandler(cb);
void cb(int n, CddbFileInfo fi, CddbFileInfoList fil)
{
}
И распознаем:m.TrackID(fi, 0, ref n1, out fil);
В функции cb в fi.Tag имеем информацию о тэгах по аудио-отпечатку с gracenote:void cb(int n, CddbFileInfo fi, CddbFileInfoList fil)
{
fi = fil.GetFileInfo(1);
ICddbFileTag tag = fi.Tag;
}
На реализации распознавания в потоках на нескольких файлах я акцентировать внимание не буду, суть я думаю понятна.
Все вышеописанное можно использовать, допустим, в радио, или брать с микрофона входной поток и распознавать его, реализация ваша!
Спасибо за внимание, надеюсь статья будет кому-нибудь полезна!


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js