Data Scientist (а по совместительству — физик и химик) Виктор Мурсия в своем блоге на Medium пишет, что музыка была с ним всегда. Он слушает ее каждый день и играет на гитаре больше 20 лет. Перед тем, как начать свое академическое физико-химическое путешествие, Виктор задумывался о музыкальной карьере. Передаем ему слово.
И хотя я не выбрал этот путь, я наслаждаюсь музыкой и могу делать с ней крутые вещи. Например, я решил создать программу, которая бы «проигрывала» изображение. Я видел такие попытки, но, по моему мнению, результаты были не слишком благозвучны.
Здесь я покажу свою версию программы и результаты ее работы. Если вы хотите просмотреть полный код, вы сможете найти его в репозитории GitHub. Я создал программу с помощью Streamlit.
Содержание
Моя стратегия и ход мыслей были таковы:
Таким образом подразделение цветового пространства может быть привязано к конкретной ноте в музыкальной гамме. Тогда у этой ноты будет частота, связанная с ней.
Давайте попробуем!
HSV — это цветовое пространство, контролируемое тремя значениями: оттенок, насыщенность и яркость.
HSV-цилиндр / Источник: Wikipedia
Оттенок — это уровень, к которому раздражитель может быть описан как похожий или отличный от раздражителей, которые описаны как красный, оранжевый, желтый, зеленый, синий, фиолетовый. По сути, оттенок — это цвет.
Насыщенность определяют как красочность области, оцененной пропорционально ее яркости. Другими словами, насыщенность означает количество белого в этом цвете.
Яркость определяется как восприятие, вызванное яркостью визуальной цели. Другими словами, насыщенность означает количество черного в этом цвете.
Оттеночные значения основных цветов:
Я буду работать в цветовом пространстве HSV, поскольку оно уже разделено, поэтому дальнейшее отображение частот будет более интуитивным.
Вот пример сравнения между цветовыми пространствами для изображения и кодом для их создания:
#Need function that reads pixel hue value hsv = cv2.cvtColor(ori_img, cv2.COLOR_BGR2HSV) #Plot the image fig, axs = plt.subplots(1, 3, figsize = (15,15)) names = ['BGR','RGB','HSV'] imgs = [ori_img, img, hsv] i = 0 for elem in imgs: axs[i].title.set_text(names[i]) axs[i].imshow(elem) axs[i].grid(False) i += 1 plt.show()
Цветовые пространства / Источник: оригинальное изображение RGB от agsandrew. Авторские изображения — Victor Murcia
Теперь, когда у нас есть наше изображение в HSV, давайте вытащим значение оттенка (H
) из каждого пикселя. Это можно сделать с помощью вложенного цикла for
по высоте и ширине изображения:
i=0 ; j=0 #Initialize array the will contain Hues for every pixel in image hues = [] for i in range(height): for j in range(width): hue = hsv[i][j][0] #This is the hue value at pixel coordinate (i,j) hues.append(hue)
Теперь, когда у меня есть массив, содержащий значение H
для каждого пикселя, я размещу этот результат в pandas dataframe
. Каждая строка dataframe
— это пиксель, и, следовательно, каждый столбец будет содержать информацию об этом пикселе. Я назову этот dataframe pixels_df
.
Вот он:
Сейчас dataframe
состоит из одного столбца под названием «оттенки», где каждая строка представляет H
канал для каждого пикселя на изображении, которое я загрузил.
Моя начальная идея по преобразованию значения оттенка в частоту предполагала отображение между заранее определенным набором частот и значением H
. Функция отображения показана ниже:
#Define frequencies that make up A-Harmonic Minor Scale scale_freqs = [220.00, 246.94 ,261.63, 293.66, 329.63, 349.23, 415.30] def hue2freq(h,scale_freqs): thresholds = [26 , 52 , 78 , 104, 128 , 154 , 180] note = scale_freqs[0] if (h <= thresholds[0]): note = scale_freqs[0] elif (h > thresholds[0]) & (h <= thresholds[1]): note = scale_freqs[1] elif (h > thresholds[1]) & (h <= thresholds[2]): note = scale_freqs[2] elif (h > thresholds[2]) & (h <= thresholds[3]): note = scale_freqs[3] elif (h > thresholds[3]) & (h <= thresholds[4]): note = scale_freqs[4] elif (h > thresholds[4]) & (h <= thresholds[5]): note = scale_freqs[5] elif (h > thresholds[5]) & (h <= thresholds[6]): note = scale_freqs[6] else: note = scale_freqs[0] return note
Функция принимает значение H
и массив, содержащий частоты для отображения H
как inputs
. В примере используется массив под названием scale_freqs
для определения частот. Используемые в частоте scale_freqs
соответствуют минорной гармонической гамме.
Затем определяется массив пороговых значений для H
. Этот массив можно использовать для преобразования H
в частоту scale_freqs
с помощью лямбда-функции:
pixels_df['notes'] = pixels_df.apply(lambda row : hue2freq(row['hues'],scale_freqs), axis = 1)
А теперь, когда у меня есть массив частот, я конвертирую столбец notes
в массив numpy
, который называется frequencies
, поскольку потом я могу использовать это для создания аудиофайла, который можно воспроизвести.
Для этого я могу использовать функцию wavfile.write
, которая встроена в scipy
, и убедиться, что я использую соответствующее преобразование типа данных (для 1D-массивов это np.float32
):
frequencies = pixels_df['notes'].to_numpy() song = np.array([]) sr = 22050 # sample rate T = 0.1 # 0.1 second duration t = np.linspace(0, T, int(T*sr), endpoint=False) # time variable #Make a song with numpy array :] #nPixels = int(len(frequencies))#All pixels in image nPixels = 60 for i in range(nPixels): val = frequencies[i] note = 0.5*np.sin(2*np.pi*val*t) #Represent each note as a sign wave song = np.concatenate([song, note]) #Add notes into song array to make song ipd.Audio(song, rate=sr) # load a NumPy array as audio
Послушайте песню, которую я создал, используя первые 60 пикселей из изображения ниже (я мог бы попытаться использовать все 230 400 пикселей этой картинки, но песня будет длиться несколько часов):
Классно вышло! Но я бы еще немного ее усовершенствовал.
Я решил добавить эффект октав (т.е. сделать так, чтобы ноты звучали выше или ниже) в процесс создания песни. Октава, которая будет использована для определенной ноты, выбирается случайным образом из массива:
song = np.array([]) octaves = np.array([0.5,1,2]) sr = 22050 # sample rate T = 0.1 # 0.1 second duration t = np.linspace(0, T, int(T*sr), endpoint=False) # time variable #Make a song with numpy array :] #nPixels = int(len(frequencies))#All pixels in image nPixels = 60 for i in range(nPixels): octave = random.choice(octaves) val = octave * frequencies[i] note = 0.5*np.sin(2*np.pi*val*t) song = np.concatenate([song, note]) ipd.Audio(song, rate=sr) # load a NumPy array
Давайте послушаем!
Прекрасно! Теперь звучит интереснее. Но у нас есть все эти пиксели: попробуем использовать их, выбирая частоты из случайных пикселей?
song = np.array([]) octaves = np.array([1/2,1,2]) sr = 22050 # sample rate T = 0.1 # 0.1 second duration t = np.linspace(0, T, int(T*sr), endpoint=False) # time variable #Make a song with numpy array :] #nPixels = int(len(frequencies))#All pixels in image nPixels = 60 for i in range(nPixels): octave = random.choice(octaves) val = octave * random.choice(frequencies) note = 0.5*np.sin(2*np.pi*val*t) song = np.concatenate([song, note]) ipd.Audio(song, rate=sr) # load a NumPy array
Мне нравится: теперь это фактически генератор песен, с которым можно играться сколько угодно!
Я знаю, что это мем, но это «математический рок»? 🙂
Я показал, как можно создавать музыку из изображений с помощью ля-гармонической минорной гаммы. Но было бы неплохо получить большее разнообразие в начальной ноте (тонике) нашей гаммы и других интервалов, кроме тех, которые определены структурой гармонического минорного звукоряда. Это позволит нашей программе создавать более разнообразные мелодии.
Для этого мне нужен способ процедурной генерации частот для любой тонической ноты, которую мы хотим использовать. Кэти Хэ написала отличную статью, где провела тщательное исследование Python и музыки. Я адаптировал одну из ее функций для работы, чтобы сопоставлять ноты фортепиано с частотами, как показано ниже:
def get_piano_notes(): # White keys are in Uppercase and black keys (sharps) are in lowercase octave = ['C', 'c', 'D', 'd', 'E', 'F', 'f', 'G', 'g', 'A', 'a', 'B'] base_freq = 440 #Frequency of Note A4 keys = np.array([x+str(y) for y in range(0,9) for x in octave]) # Trim to standard 88 keys start = np.where(keys == 'A0')[0][0] end = np.where(keys == 'C8')[0][0] keys = keys[start:end+1] note_freqs = dict(zip(keys, [2**((n+1-49)/12)*base_freq for n in range(len(keys))])) note_freqs[''] = 0.0 # stop return note_freqs
Эта функция служит отправной точкой для моей процедуры создания песни/шкалы, и ее можно использовать для создания словаря, отображающего музыкальные ноты, соответствующие 88 клавишам стандартного пианино, на частоты в единицах Гц:
#Load note dictionary note_freqs = get_piano_notes()
Затем нам нужно определить интервалы шкалы в терминах тонов, чтобы мы могли индексировать наши ноты:
#Define tones. Upper case are white keys in piano. Lower case are black keys scale_intervals = ['A','a','B','C','c','D','d','E','F','f','G','g']
А теперь мы можем найти индекс гаммы в предыдущем списке тонов. Это нужно, потому что я потом переиндексирую список, чтобы он начинался с нужной нам тоники:
#Find index of desired key index = scale_intervals.index(whichKey) #Redefine scale interval so that scale intervals begins with whichKey new_scale = scale_intervals[index:12] + scale_intervals[:index]
После этого я могу определить группу разных массивов шкалы, где каждый элемент соответствует индексу из массива повторного индексирования, который я только что сделал выше:
#Choose scale if whichScale == 'AEOLIAN': scale = [0, 2, 3, 5, 7, 8, 10] elif whichScale == 'BLUES': scale = [0, 2, 3, 4, 5, 7, 9, 10, 11] elif whichScale == 'PHYRIGIAN': scale = [0, 1, 3, 5, 7, 8, 10] elif whichScale == 'CHROMATIC': scale = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] elif whichScale == 'DORIAN': scale = [0, 2, 3, 5, 7, 9, 10] elif whichScale == 'HARMONIC_MINOR': scale = [0, 2, 3, 5, 7, 8, 11] elif whichScale == 'LYDIAN': scale = [0, 2, 4, 6, 7, 9, 11] elif whichScale == 'MAJOR': scale = [0, 2, 4, 5, 7, 9, 11] elif whichScale == 'MELODIC_MINOR': scale = [0, 2, 3, 5, 7, 8, 9, 10, 11] elif whichScale == 'MINOR': scale = [0, 2, 3, 5, 7, 8, 10] elif whichScale == 'MIXOLYDIAN': scale = [0, 2, 4, 5, 7, 9, 10] elif whichScale == 'NATURAL_MINOR': scale = [0, 2, 3, 5, 7, 8, 10] elif whichScale == 'PENTATONIC': scale = [0, 2, 4, 7, 9] else: print('Invalid scale name')
Почти готово! Я также определю здесь интервалы для использования:
#Make harmony dictionary (i.e. fundamental, perfect fifth, major third, octave) #unison = U0 #semitone = ST #major second = M2 #minor third = m3 #major third = M3 #perfect fourth = P4 #diatonic tritone = DT #perfect fifth = P5 #minor sixth = m6 #major sixth = M6 #minor seventh = m7 #major seventh = M7 #octave = O8 harmony_select = {'U0' : 1, 'ST' : 16/15, 'M2' : 9/8, 'm3' : 6/5, 'M3' : 5/4, 'P4' : 4/3, 'DT' : 45/32, 'P5' : 3/2, 'm6': 8/5, 'M6': 5/3, 'm7': 9/5, 'M7': 15/8, 'O8': 2 }
И теперь я могу взять результаты предыдущих шагов и создать песню!
#Get length of scale (i.e., how many notes in scale) nNotes = len(scale) #Initialize arrays freqs = [] #harmony = [] #harmony_val = harmony_select[makeHarmony] for i in range(nNotes): note = new_scale[scale[i]] + str(whichOctave) freqToAdd = note_freqs[note] freqs.append(freqToAdd) #harmony.append(harmony_val*freqToAdd)
Супер! Сейчас у меня много параметров, которые я могу использовать, чтобы создавать песни. Я могу установить тональность, октаву, гармонию, количество пикселей, выбор пикселей случайным образом и продолжительность каждой ноты.
Давайте проверим это на нескольких изображениях. Частота дискретизации для всех изображений составляет 22050 Гц, если не указано иное.
Я подумал, что можно использовать пиксельное искусство. Вот одна из песен, вышедших в результате использования этого замечательного произведения @Matej ‘Retro’ Jan в качестве исходного изображения. Эта песня была создана с использованием третьей октавы в качестве основы в тональности ля мажор. Достаточно мило, не правда ли?
Пиксельные китайские горы / Источник: Matej ‘Retro’ Jan
Эта песня создана с использованием E Dorian и третьей октавы:
Источник: Anna Om
Эта песня была создана с использованием B Lydian и диапазона второй октавы. Моя любимая! Будет звучать очень круто как гитарный риф.
Автор фото: John Salatas
Я обожаю свою кошку Катерину. Эта песня создана с использованием гармонического минора А и диапазона третьей октавы. Звучит очень хорошо, как по мне.
То, что я делал выше, позволяет добавить гармонии нашей песне. Пользователь может определить, какую гармонию использовать, а затем выводится правильный интервал ноты с помощью определенного процесса.
Ниже я покажу пример песни, созданной из изображения и соответствующей гармонии, объединенной в single .wav file с помощью двумерного массива numpy.
Согласно документации для scipy.io.wavfile.write, если я хочу записать 2D-массив в файл .wav, у 2D-массива должны быть размеры в форме (Nsamples, Nchannels).
Обратите внимание, какова сейчас форма нашего массива (2, 264 600). Это означает, что у нас есть Nchannels = 2 и Nsamples = 264600. Чтобы убедиться, что у нашего numpy-массива правильная форма для scipy.io.wavfile.write, я сначала транспонирую массив. Песня создана с использованием гармонического минора A#, диапазона второй октавы и минорной терционной гармонии.
Фото Марка Грея
А еще я собираюсь загрузить файлы .wav и выполнить некоторые дополнительные манипуляции с ними с помощью pedalboard-модуля от Spotify. Вы можете прочитать больше о библиотеке pedalboard здесь и здесь.
Сначала я повторно обработаю «Песню воды», которую показал ранее, используя Compressor, Gain, Chorus, Phaser, Reverb и Ladder Filter.
Вот результат:
Теперь я повторно обработаю песню «Катерина», используя Ladder Filter, Delay, Reverb и PitchShift:
Звучит отлично!
И еще поиграюсь с песней, созданной из пейзажа. Использую LadderFilter, Delay, Reverb, Chorus, PitchShift и Phaser:
Librosa — отличный пакет, позволяющий выполнять различные операции над звуковыми данными. Попробуйте его обязательно! Здесь я использовал его, чтобы преобразовывать частоты в Notes и Midi Numbers.
Файлы цифрового интерфейса музыкальных инструментов (MIDI) используются как формат файла, который можно подключать к различным электронным музыкальным инструментам, компьютерам и другим аудиоустройствам.
Если мы сохраним песню в этом формате, это позволит другим музыкантам или программистам использовать ее для экспериментов.
Ниже я показываю функции, которые можно использовать для отображения сгенерированных частот, чтобы получить соответствующие музыкальные ноты и midi_numbers
для нашей песни:
#Convert frequency to a note catterina_df['notes'] = catterina_df.apply(lambda row : librosa.hz_to_note(row['frequencies']), axis = 1) #Convert note to a midi number catterina_df['midi_number'] = catterina_df.apply(lambda row : librosa.note_to_midi(row['notes']), axis = 1)
Теперь, когда я создал dataframe
, содержащий частоты, ноты и миди-номера, я могу создать из него MIDI-файл. Затем я мог бы использовать этот MIDI-файл для создания нот для нашей песни.
Чтобы создать MIDI-файл, я воспользуюсь пакетом midiutil
. Этот пакет позволяет создавать MIDI-файлы из массива MIDI-номеров. Вы можете настроить свой файл разными способами, настроив громкость, темп и дорожки. Пока я сделаю только однодорожный MIDI-файл:
#Convert midi number column to a numpy array midi_number = catterina_df['midi_number'].to_numpy() degrees = list(midi_number) # MIDI note number track = 0 channel = 0 time = 0 # In beats duration = 1 # In beats tempo = 240 # In BPM volume = 100 # 0-127, as per the MIDI standard MyMIDI = MIDIFile(1) # One track, defaults to format 1 (tempo track # automatically created) MyMIDI.addTempo(track,time, tempo) for pitch in degrees: MyMIDI.addNote(track, channel, pitch, time, duration, volume) time = time + 1 with open("catterina.mid", "wb") as output_file: MyMIDI.writeFile(output_file)
Я показал, как можно создавать музыку из картинок, и как эти песни можно экспортировать в файлы .wav для дальнейшей обработки. А еще как с помощью этого метода можно построить гармонии, и из этого создать более сложные, насыщенные и/или странные гармонии. Тут просто безграничное поле для экспериментов!
Если вы музыкант, и вам не хватает вдохновения, попробуйте добавить изображение в мое приложение и, возможно, вы получите крутую идею, на которую можно опираться. Никогда не знаешь, откуда придет вдохновение!
Автор: Виктор Мурсия
Текст адаптировала Евгения Козловская
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…