Javascript.RU

Создать новую тему Ответ
 
Опции темы Искать в теме
  #1 (permalink)  
Старый 22.01.2020, 13:38
Профессор
Отправить личное сообщение для Audaxviator Посмотреть профиль Найти все сообщения от Audaxviator
 
Регистрация: 28.04.2017
Сообщений: 214

Угадай мелодию
Не знаю, сколько там прошло времени с моего последнего захода сюда, но за этот период я снова что-то экскрементировал, и поскольку прошлый раз мне делали намёк по поводу "изложить в виде статей", я и подумал - можно ведь и вернуться, и что-то изложить. Не уверен, что здешней публике (т.е. настоящим кодерам) это будет интересно, но у меня есть пара знакомых, которые (возможно) это прочли бы, и проще им дать ссылку на одно и то же место (если модераторы, которые и здесь должны быть, не удалят). Словом, это будет нечто вроде статьи.

Какая мне придумалась в этот раз задача? Предположим, ко мне, как говорят, "привязалась мелодия" - я её бубню полдня, насвистываю и всё такое, но, хоть тресни, не могу вспомнить - ни как эта песня называется, ни чья она, ни откуда она... Хорошо бы, если б можно было напеть в компьютерный микрофон пару первых её фраз (такта четы, допустим) - а компьютер её бы распознал и дал мне ответ - название, аффтар и вот это всё.

Нет, как человек, кое-что знающий за музыку, я понимаю, что мы с моим компьютером тут попадаем в такую область, где компьютер (не обязательно мой, а даже и тот, который у Каспарова выигрывал) не только никогда не сравняется с человеческим умом, но даже и не приблизится к уму мили, допустим, на три. Однако, нельзя ли сделать хотя бы шажок в этом направлении, попробовать, хотя бы, саму гипотетически-теоретическую возможность? Вот я и попробовал. Что нужно для экскримента?

Двигаемся поступательно. Ну, во-первых, нужен какой-нибудь рекордер в браузер. И это вообще не проблема - вот он, лежит готовенький:
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';
	}
}


(окончание в каменте)
Изображения:
Тип файла: jpg intfs.jpg (79.9 Кб, 2 просмотров)
Вложения:
Тип файла: zip Desktop.zip (767.0 Кб, 0 просмотров)

Последний раз редактировалось Audaxviator, 22.01.2020 в 14:10.
Ответить с цитированием
  #2 (permalink)  
Старый 22.01.2020, 13:39
Профессор
Отправить личное сообщение для Audaxviator Посмотреть профиль Найти все сообщения от Audaxviator
 
Регистрация: 28.04.2017
Сообщений: 214

Обозначения для интервалов я придумал "от фонаря" - чтоб мне понятно только было, чтоб не запутаться. Допустим, "с-4-UP" обозначает "чистая кварта вверх", а "m-3-DN" - "малая терция вниз". Аналогично есть функция, высчитывающая все отношения длительностей - только, она берёт разницу первого и нулевого элементов массива time, какбэ держит её в уме, а все остальные разницы между соседними в массие числами - относительно первой; и результаты по-возможности правильно округляет. (как говаривал старик Эйнштейн, "всё в мире относительно") Ну и всё, получается "реляционная формула мелодии". Для примера - вот "формула" старинной немецной песенки "Ах, мой милый Августин":
'b-2-UPb-2-DNb-2-DNm-2-DNb-3-DNb-2-UPc-5-DNb-6-UPb-3-DNc-5-UP': {
    reltones: [
      'b-2-UP', 'b-2-DN', 'b-2-DN',
      'm-2-DN', 'b-3-DN', 'b-2-UP',
      'c-5-DN', 'b-6-UP', 'b-3-DN',
      'c-5-UP', 'b-2-UP', 'b-2-DN',
      'b-2-DN', 'm-2-DN', 'b-3-DN',
      'b-2-UP', 'c-5-DN', 'c-4-UP'
    ],
    reldurat: [
       0.5, 0.25,  0.5, 0.5, 1,
       0.5,    1,  0.5,   1, 1,
      0.25, 0.25, 0.25, 0.5, 1,
       0.5,    1
    ],
    musicdata: 'Ах, мой милый Августин'
  }

Это она предназначена - для сравнения с ней, т.е. взята из готовой "библитеки формул", каковая представляет собой, конечно же, JSON-файлик. Ключ, как видите, сконкатенированный в строку массив reltones - это, как я теоретически предполагал, для быстрого предварительного поиска. Когда "клиент" (ну, то есть я) напел или надинькал WAV, получил в ответ другой WAV (кондово-синтетический) и удостоверился, что оно нормально, он жмёт кнопку Look for - это сигнал серверу, что нужно взять соответствующий blabla.jams и запилить из него подобную же "формулу" (тем же самым алгорифмом, каким запиливаются и все "формулы" для "библиотеки"). Потом он делает такой же "конкатенат" из первых пяти интервалов и бежит с ним по ключам "библиотеки", проверяя их на предмет такой подстроки - и когда (и если) находит подходящий ключ, то потом "для верности" ещё и сравнивает оба массива (оттуда и оттуда). Но, честно сказать, над "алгорифмом" я сильно не парился - всё равно в реальности такой типа "сервис" сделать нельзя. Потому что все мелодии в мире уникальны, их существует больше, чем слов в любом языке - чтобы сделать хоть мало-мальски "библиотеку" таких "формул", это надо чтобы какой-нибудь институт культуры месяц полным составом только этим занимался.

Но как-то же я должен был проверить, оно вообще работает или нет? Загнал в "библиотеку" три "формулы" - кроме упомянутой "Ах, мой милый Августин", ещё "Подмосковные вечера" и "Cheek to cheek" Ирвина Берлина. Я это делал - играя одним пальцем мелодии на пианине, подключённой к компьютеру шнуром, т.е. исходный WAV записывается в идеальном виде (специальную кнопку для добавления формулы в библиотеку видно на кортинко с интерфейсом). И потом уж пробовал напевать или наигрывать.

Скажу так. С голосом, всё равно, получается не очень - но это именно из-за голоса. Увы, я не оперный певец и не могу пропевать фразочку чисто и ровно - голос у меня ставчески-дрожащий, связки давно пропиты... Методом научного тыка выяснил лишь, что если не утыкаться носом в микрофон (тогда уж вся пропитость и дрожание налицо), а отодвинуться, а лучше, вообще отойти на метр-полтора - тогда примерно через раз получается. А вот если проигрывать фразочку на пианине, которая аж в двух метрах от компьютера, то всё работает норм. Получается, что WAV отдаётся с низкой амплитудой, с кучей лишнего шума - но ей этого достаточно, потому что - главное - пианина тоны играет чисто и ровно (в прикрученном ZIP-файле для понятности два WAV - исходный и возвращённый "синтетический"). Из какой мелодии первую фразочку наигрываю (разумеется, в любой тональности и темпе) - такое она мне из "библиотеки" (одно из трёх имеющихся) название и выводит.

Извените за внимание.
Ответить с цитированием
Ответ



Опции темы Искать в теме
Искать в теме:

Расширенный поиск


Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Угадай число. Помогите написать скрипт и по возможности объяснить, как работает Vadim Zhizherin Общие вопросы Javascript 3 04.05.2018 21:09
Игра угадай число forzz Общие вопросы Javascript 5 26.10.2017 16:17
игра угадай число. что я сделала не так? 2 раза выбираю число и он выкидывает... olikbel2017 Общие вопросы Javascript 8 07.10.2017 01:41
Угадай число GTG Общие вопросы Javascript 17 22.04.2013 16:23
Угадай мелодию Владлен Events/DOM/Window 1 07.04.2013 16:44