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, 264600). Це означає, що ми маємо Nchannels = 2 і Nsamples = 264600. Щоб переконатися, що наш масив numpy має правильну форму для scipy.io.wavfile.write, я спочатку транспоную масив. Пісня створена з використанням гармонічного мінору A#, діапазону другої октави та мінорної терційної гармонії.
Фото Марка Грея
А ще я збираюся завантажити файли .wav і виконати деякі додаткові маніпуляції з ними за допомогою модуля pedalboard від Spotify. Ви можете прочитати більше про бібліотеку pedalboard тут і тут.
Спочатку я повторно оброблю «Пісню води», яку показав раніше, використовуючи Compressor, Gain, Chorus, Phaser, Reverb, and a 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 для подальшої обробки. А ще — як за допомогою цього методу можна побудувати гармонії, і з цього створити більш складні, насичені та/або дивні гармонії. Тут просто безмежне поле для експерементів!
Якщо ви музикант, і вам не вистачає натхнення, спробуйте додати зображення в мій застосунок, і, можливо, ви отримаєте круту ідею, на яку можна спиратися. Ніколи не знаєш, звідки прийде натхнення!
Автор: Віктор Мурсія
Текст адаптувала Євгенія Козловська
Резиденти Дія.City сплатили до бюджету понад 8 млрд грн податків в І кварталі 2025 року.…
У Китаї закликають офісних працівників не працювати надто багато — держава сподівається, що вільний час…
Експерти звертають увагу на тривожну тенденцію: люди все частіше використовують ChatGPT, щоб визначити місцезнаходження, зображене…
Компанія JetBrains випустила нову версію мультимовного середовища розробки IntelliJ IDEA 2025.1. Оновлена IDE отримала численні…
Платформа обміну миттєвими повідомленнями Discord впроваджує функцію перевірки віку за допомогою сканування обличчя. Зараз вона…
Wikipedia намагається захистити себе від тисяч різноманітних ботів-скрейперів, які сканують дані цієї платформи для навчання…