Не знаю, сколько там прошло времени с моего последнего захода сюда, но за этот период я снова что-то экскрементировал, и поскольку прошлый раз мне делали намёк по поводу "изложить в виде статей", я и подумал - можно ведь и вернуться, и что-то изложить. Не уверен, что здешней публике (т.е. настоящим кодерам) это будет интересно, но у меня есть пара знакомых, которые (возможно) это прочли бы, и проще им дать ссылку на одно и то же место (если модераторы, которые и здесь должны быть, не удалят). Словом, это будет нечто вроде статьи.
Какая мне придумалась в этот раз задача? Предположим, ко мне, как говорят, "привязалась мелодия" - я её бубню полдня, насвистываю и всё такое, но, хоть тресни, не могу вспомнить - ни как эта песня называется, ни чья она, ни откуда она... Хорошо бы, если б можно было напеть в компьютерный микрофон пару первых её фраз (такта четы, допустим) - а компьютер её бы распознал и дал мне ответ - название, аффтар и вот это всё.
Нет, как человек, кое-что знающий за музыку, я понимаю, что мы с моим компьютером тут попадаем в такую область, где компьютер (не обязательно мой, а даже и тот, который у Каспарова выигрывал) не только никогда не сравняется с человеческим умом, но даже и не приблизится к уму мили, допустим, на три. Однако, нельзя ли сделать хотя бы шажок в этом направлении, попробовать, хотя бы, саму гипотетически-теоретическую возможность? Вот я и попробовал. Что нужно для экскримента?
Двигаемся поступательно. Ну, во-первых, нужен какой-нибудь рекордер в браузер. И это вообще не проблема - вот он, лежит готовенький:
https://github.com...
забираем файл, прикручиваем к нему незатейливый интерфейс (см. картинко с незатейливым интерфейсом) - работает, как часы. Он должен записать звук и, при нажатии на кнопку stop, отправить blob формата WAV на сервер, где к нему будет прикручено случайное имя с соответствующим расширением - blabla.wav - и положено в нужную папку.
Дальше. Теперь из этого WAV нужно сделать MIDI - и вот это уже проблема. Во-первых, известные преобразовалки wav-to-mid предназначены совсем не для этого - к примеру, я подсовывал напетую голосом муз-фразочку известному AmazingMIDI, и он вообще не понимает, что с этим делать - он не понимает, за что "хвататься". И во-вторых, они все - десктопные, а мне нужна консольная программа. Короче, пара дней поисков в этих ваших интернетах вывели меня вот на эту лабу какого-то испанского университета и их утилитку:
https://www.upf.edu/...
Утилитка представляет собой готовый бинарный файлик; поскольку у меня дома Windows - это файлик dll, и ещё кое-что при нём. В Виндоусе она довольно капризная (для Линукса, сказано, более покладистая) - к примеру, просит обязательно 32-битный Python27. Ещё нужно добавить пачку пакетов расширений для Python, и ещё клонировать с Гитхаба вот эту прграммку, написанную одним из авторов этой утилитки - собсна, она и "подхватывает", умеет использовать искомую утилитку:
https://github.com/justinsalamon...
Программка, как видите, на Питоне. Но она консольная - то, что надо. Вот так выглядит строка в cmd
python c:/audio_to_midi_melodia/audio_to_midi_melodia.py --smooth 0.50 --minduration 0.2 file.wav file.mid --jams 60
Всем известно, что запустить любую консольную программку в качестве "побочной" в Ноде (да в любой платформе, вообще-то) - дело пустяковое. Я, к примеру, запилил для этого такой смешной и почти универсальный модулёк:
var execFile = require('child_process').execFile;
function childProcessExecFile(p, ...args) {
return new Promise(function(resolve, reject) {
var pr = execFile(p, args, function(err) {
if(err) resolve(err);
var out = args[args.length-4].split('.');
resolve(out[0]);
});
});
}
module.exports = childProcessExecFile;
и в него можно подсовывать любую консольную команду - хоть ffmpeg, хоть graphicsmagic, хоть другую команду от node, а хоть и, вот, python. В обработчике же - это выглядит так:
req.on('end', function () {
childProcessExec('python', 'c:/audio_to_midi_melodia/audio_to_midi_melodia.py', '--smooth', '0.50',
'--minduration', '0.2', filesPath + '.wav', filesPath + '.mid', '--jams', '60')
.then( path => {
var midBuffer = fs.readFileSync(path + '.mid');
var wavBuffer = synth.midiToWav(midBuffer).toBuffer();
fs.writeFileSync(path + '.wav', wavBuffer, {encoding: 'binary'});
var arrS = path.split('\\');
//console.log(path);
res.set('content-type', 'text/html');
res.send('/melodia_lib/' + arrS[arrS.length-1] + '.wav');
})
.catch( err => {
console.log(err);
});
});
это, как видите, та же строка из Cygwin, только распиленная запятыми на аргументы промисифицированной функции. Программка берёт отправленный рекордером WAV и "экстрактит" из него MID. И это вполне сносный MID (я в конце скажу - насколько сносный). Но приведённый выше код немного забежал вперёд изложения. Видите, в then там ещё чего-то происходит. Дальше-то нужно вот что сделать: нужно дать гипотетическому "клиенту" возможность услышать - что там компьютер понял-то из его напевания или насвистывания, или, там... надинькивания мелодии по гитарным струнам? И тут проблемка в том, что браузеры не обслуживают файлы MIDI (они понимают, что это стандартный MIME-тип audio/midi, но вставлять его в тег <audio> бесполезно - играть не будет). Т.е. надо взять уже готовый MID, "синтезировать" из него обратно WAV - и ссылку на него вернуть "клиенту". Я сначала пробовал Timidity - её, тоже, можно юзать в консоли, - но она, как оказалось, для данной задачки, опять, не годится. Уж думал, что придётся такой "синтезатор" самому пилить, что совсем не трудно, только возьни много (даже статью годную нашёл в подспорье), но, слава богу, отыскался готовый модульчик для Ноды:
https://www.npmjs.com...
он всё делает, и делает очень хорошо - берёт MIDI-данные и синтезит из них кондово-синтетические тоны, причём, каждое "событие" акцентирует, как бы сказать, "атакой", т.е. точно слышно количество тонов (нет ли лишних? - что критически важно с точки зрения последующего "угадывания мелодии"). Ну вот, "клиент" теперь может услышать, как оно получилось - и если не вполне удовлетворительно, то перепеть (перединькать). Если же норм - нажать кнопку Look for. Собсна, дело за малым - осталось научить компьютер угадывать мелодии.
Дальнейшее изложение будет иметь больше отношения к музыке, чем к компьютерам, и если кто понимает за сольфеджио - он поймёт, а если не понимает... - всё равно поймёт. Видите ли, чтобы компьютер мог "угадывать", его нужно снярядить специальной библиотечкой таких штук, которые я условно назвал "реляционная формула мелодии". Тут фишка в том, что мелодия - это не последовательность тонов (ну, типа "нот") разной высоты и длительности. Мелодия - это последовательность отношений соседних тонов (в сольфеджио эти отношения называются "интервалы") и последовательность отношений длительностей (последовательность не длительностей - а их отношений). Именно поэтому не имеет значения, во-первых, в какой тональности звучит мелодия, и во-вторых, в каком темпе она звучит - она всё равно остаётся "той же самой мелодией" (т.е. "узнаётся").
Что это значит в преспективе текущей задачи? - А вот что. Когда испанская утилитка делает из WAV - MID, на выходе получается два файла: один - собственно blabla.mid, а другой - blabla.jams: флаг --jams в command-line как раз и является указанием - такой файл заодно делать. Аффтары программки посчитали, видимо, что будет остроумным прикручивать к этим файлам такое расширение - оно, как не трудно догадаться, сокращение от "джем-сейшин", - но вообще-то, это JSON-строка. Тут ведь, думаю, все понимают, что у файла *.json совсем не оязательно должно быть такое расширение - его может хоть вообще не быть, и если его не реквайрить Нодой (кажется, с 8-й версии она умеет так делать), а читать методом fs.readFile('blabla.jams'), то прочитается строка и, затем, распарсится в обычный советский javascript-объект. И в этом объекте мы, в том числе, увидим вот такие примерно данные:
"data": {
"duration": [
0.41505668934240364,
0.39764172335600906,
0.3947392290249433,
//...
],
//...
"value": [
57.0,
60.0,
64.0,
//...
],
"time": [
1.3380498866213153,
1.7531065759637188,
2.1507482993197278,
//...
]
}
Из этих данных и делается "реляционная формула мелодии", как я это называю. Нужны только массивы value и time. Файл MIDI ведь устроен донельзя примитивно: value - это тупо пронумерованные клавиши фортепиано по полу-тонам (к примеру, 60.0 - это "до" первой октавы, 61.0 - "до-диез", 62.0 - "ре", 63.0 - "ре-диез" и т.д.), а time - это время "событий" от самого начала (т.е. когда каждый из тонов начинал звучать). Из этих двух массивов и можно посчитать все отношения простым арифметическим методом вычитания. 60-57=3 полутона - "малая терция", 64-60=4 полутона - "большая терция" и т.д. Остаётся лишь покорпеть на примитивнейшей функцией:
function makeRelationTones(arr) {
var result = [];
for(var i=0; i<arr.length-1; i++) {
result.push(relationTones(arr[i+1], arr[i]));
}
return result;
}
function relationTones(next, previous) {
switch(next - previous) {
case 0:
return '1-1';
case 1:
return 'm-2-UP';
case 2:
return 'b-2-UP';
case 3:
return 'm-3-UP';
//...
//...
case -9:
return 'b-6-DN';
case -10:
return 'm-7-DN';
case -11:
return 'b-7-DN';
case -12:
return 'c-8-DN';
default:
return '0-0-0';
}
}
(окончание в каменте)