5. Müzik Sinyalleri frekans spektrum analizi#

Open In Colab

5.1. Spektrum, frekans uzayının parçalara bölünmesi, nota frekansları, ezgi ve tonalite#

Bu defterimizden itibaren müzik sinyal işleme konularını ele almaya başlayacağız. Bir önceki defterde sinyal spektrumunu ele almıştık. Burada bıraktığımız yerden başlayıp adım adım önce müzik kayıtları içerisinde ezgi boyutunu, daha sonra (ilerleyen defterlerde) ritim boyutunu analiz etmek için kodlar yazacağız.

Bir önceki defterimizde bir sinyal içerisindeki sinüzoidal bir bileşenin spektrumda (üzerine pencere fonksiyonunun spektrumu kopyalanmış olarak) öne çıktığını ve tepe noktasının pozisyonundan bileşenin frekansını kestirebileceğimizi görmüştük. Zaman-frekans gösterimi olan spektrogramda da harmonik izlerinin kayıttaki müzikal ezgi ile doğrudan bağlantılı olduğunu görmüştük. Aşağıda bir müzik sinyali için spektrogram çizdirip bu konuyu daha detaylı ele almaya başlayalım.

Gerekli kütüphaneleri yükleyelim.

import sys
import os
import numpy as np
import matplotlib.pyplot as plt
import soundfile as sf
from scipy import signal
import urllib.request
from IPython.display import Audio

Bir saksafon kaydını indirip incelemeye başlayalım.

url = 'https://github.com/MTG/sms-tools/raw/master/sounds/sax-phrase-short.wav'
urllib.request.urlretrieve(url,'sax-phrase-short.wav')
muzik_sinyali, ornekleme_fr = sf.read('sax-phrase-short.wav')

fig = plt.figure(figsize=(15,3))
plt.plot(muzik_sinyali)
plt.title('saksafon sinyali örneği');
plt.xlabel('zaman endeksi (n)');
plt.ylabel('x[n]');
Audio(muzik_sinyali, rate=ornekleme_fr)
../_images/05_MuzikSinyalleriSpektrumu_7_1.png

Şekil 5.1: Örnek müzik sinyali dalga formu

Spektrum/spektrogram çizimleri oluşturmak için daha önceki defterlerde kullanıdığımız fonksiyonları kullanacağız. Aşağıya o fonksiyonları kopyalayalım.

# Genlik spektrumu hesaplayan fonksiyon 
def genlik_spektrumu(x, fft_N):
  '''Verilen x sinyalinin dB skalasında genlik spektrumunu hesaplar ve 
  spektrumun ilk yarısını([0, örnekleme-frekansı/2] aralığını) döndürür

  Parametreler
  ----------
  x : numpy.array
      Giriş sinyali
  fft_N : int
      Spektrum hesabında kullanılan nokta sayısı

  Döndürülenler
  -------
  numpy.array
      Genlik spektrumu (pozitif frekans bölgesi)
  '''
  X = np.fft.fft(x, fft_N)
  genlikX = np.abs(X[:int(fft_N / 2)])
  genlikX[genlikX < np.finfo(float).eps] = np.finfo(float).eps # log operasyonundan önce önlem
  return 20 * np.log10(genlikX)
def spektrogram_cizdir(x, pencereFonksiyonAdi, pencere_genisligi, pencere_kaydirma_miktari, fft_N, ornekleme_fr, eksen_limitleri={}):
  '''Sinyalin küçük kesitlerinin spektrumlarını genlik_spektrumu() fonksiyonu ile hesaplayıp 
  yanyana dizerek spektrogram zaman-frekans temsilini hesaplar ve çizdirir'''
  pencere_fonksiyonu = signal.get_window(pencereFonksiyonAdi, pencere_genisligi)

  # Pencerelerin baslangic noktalarını hesaplayalım
  pencere_baslangiclari = np.arange(0, x.size - pencere_genisligi, pencere_kaydirma_miktari, dtype = int)
  pencereSayisi = pencere_baslangiclari.size

  spktrgrm = np.array([]).reshape(0, int(fft_N / 2)) # spektogram boş bir dizi olarak başlatılıyor
  for k in range(pencereSayisi):
      pencere_baslangici = pencere_baslangiclari[k]
      x_w = x[pencere_baslangici:pencere_baslangici + pencere_genisligi] * pencere_fonksiyonu
      spktrgrm = np.vstack((spktrgrm, genlik_spektrumu(x_w, fft_N))) # k. sinyal kesitinin spektrumunun eklenmesi

  # Spektrogram matrisinin çizdirilmesi
  zamanEkseni = np.arange(spktrgrm.shape[0]) * pencere_kaydirma_miktari / float(ornekleme_fr)
  frekansEkseniHz = np.arange(spktrgrm.shape[1]) * float(ornekleme_fr) / float(fft_N)

  fig = plt.figure(figsize=(15,4))
  plt.pcolormesh(zamanEkseni, frekansEkseniHz, np.transpose(spktrgrm))
  plt.xlim([0, zamanEkseni[-1]])
  plt.ylim([0, frekansEkseniHz[-1]])
  plt.title('Genlik Spektrogramı (dB)')
  plt.ylabel('frekans(Hz)')
  plt.xlabel('zaman(saniye)')
  if 'x' in eksen_limitleri:
    plt.xlim(eksen_limitleri['x'])
  if 'y' in eksen_limitleri:
    plt.ylim(eksen_limitleri['y'])
  plt.show();

Bir önceki defterde dar-bant spektrogramında harmonikler için yatay çizgiler gözlediğimizi ve ilk harmoniğin izinin ezginin frekansının zamanla değişim eğrisi olduğunu da görmüştük. Bu örnek için bu gözlemi tekrar yapalım.

# Dar-bant spektrumu çizdirelim (zamanda kullandığımız pencere genişliği çok küçük olmamalı ki harmonik izlerini gözleyebilelim) 
eksen_limitleri = {'y':(0, 2000)} # Hz cinsinden eksen sınırları
spektrogram_cizdir(muzik_sinyali, pencereFonksiyonAdi='blackman', pencere_genisligi=2048, pencere_kaydirma_miktari=1024, fft_N=2048, ornekleme_fr=ornekleme_fr, eksen_limitleri=eksen_limitleri)
../_images/05_MuzikSinyalleriSpektrumu_13_0.png

Şekil 5.2: Yakınlaştırılmış dar-bant genlik spektrogramı

Kaydı dinlediğinizde saksafon ezgisinin 6 notadan oluştuğunu işiteceksiniz. Spektrogramımızda 500Hz çevresinde gözlediğimiz kesikli yatay çizgiler müzikal ezgimizin frekansının zamanla değişimini gösteren bir çizim olarak düşünülebilir. Örneği kaydın ortalarına (1.5 saniye) denk gelen uzun notanın bir kısmı için genlik spektrogramını çizdirip inceleyelim. O bölgede 600 Hz civarında bir yatay çizgi gözlüyoruz. Bu bölgenin spektrumunu incelemek istiyoruz.

# 1.5 saniye çevresinde 0.5 saniyelik bir kesit alalım
baslangic_endeks = int((1.5 - 0.25) * ornekleme_fr)
bitis_endeks = int((1.5 + 0.25) * ornekleme_fr)
sinyal_kesidi = muzik_sinyali[baslangic_endeks:bitis_endeks]
sinyal_kesidi = sinyal_kesidi * signal.get_window('hann', sinyal_kesidi.size)

fft_N = ornekleme_fr # fft_N tercihimizi çizimimizde endeksler Hz cinsinden frekansa denk gelecek şekilde yaptık
genlik_spektrum = genlik_spektrumu(sinyal_kesidi, fft_N)

fig = plt.figure(figsize=(16,3))
plt.subplot(1,3,1)
plt.plot(sinyal_kesidi)
plt.title('Zaman uzayındaki pencerelenmiş sinyal kesidi');
plt.xlabel('Zaman (endeksi n)')
plt.subplot(1,3,2)
plt.title('Genlik spektrumu');
plt.plot(genlik_spektrum)
plt.xlabel('Frekans(Hz)')
plt.ylabel('Genlik(dB)')
plt.subplot(1,3,3)
plt.plot(genlik_spektrum[:2000]) # yakınlaştırılmış çizim için ilk 2000Hz'lik spektrumu alalım
plt.xlabel('Frekans(Hz)')
plt.ylabel('Genlik(dB)')
plt.title('Genlik spektrumu (yakınlaştırılmış)');
../_images/05_MuzikSinyalleriSpektrumu_16_0.png

Şekil 5.3: Pencerelenmiş sinyal dalga formu ve genlik spektrumu

Orta bölgeden aldığımız kesitin spektrumunda sinyalin temel titreşim frekansını ve harmoniklerin frekansını tepeler olarak gözleyebiliyoruz. İlk tepeyi 600 Hz civarında gözlüyoruz (temel titreşim frekansına karşılık geliyor). Bu frekansın tam katlarında (\(k*600\) Hz) da diğer harmoniklerin tepelerini görebiliyoruz. Harmonik izleri spektrogramda (Şekil 5.2) (yine tam katlarda) yatay izler olarak gözleniyor. Bu durumda spektrumdan yola çıkarak birçok ezgi analizi işlemi gerçekleştirebiliriz. Aşağıda bu yönde adımlar atacağız. Ancak öncelikle frekans uzayı ile ilgili önemli konuyu daha ele almalıyız: insanın diğer birçok algısı gibi frekans algısı da logaritmiktir (Bakınız). Müzikal notaların frekansları frekans uzayı logaritmik bölünerek yerleştirilmiştir. Bir işitme testi planlayıp küçük bir demo yapalım.

5.1.1. Müzikal frekans uzayının bölünmesi#

İşitsel test: frekans uzayının doğrusal ve logaritmik bölünmesi: Öncelikle ses sentezlemek için bir fonksiyon tanımlayacağız, daha sonra doğrusal ve logaritmik bölünme ile elde edilen frekanslarda ses sentezleyip dinleyeceğiz.

Verilen bir dizi temel titreşim frekans değerine sahip sinüzoidi arka arkaya dizerek ses sentezleyen bir fonksiyon tanımlayalım. Temel titreşim frekans dizimizi doğrusal ve logaritmik oluşturup elde ettiğimiz sesleri dinleyip karşılaştıracağız.

def sinus_dizisi_sentezle(frekans_dizisi, ornekleme_frekansi=16000,sure_saniye=0.75):
  ''' frekans_dizisi'nde verilen frekanslarda sinüzoidler sentezleyen fonksiyon'''
  t = np.arange(0, sure_saniye, 1/ornekleme_frekansi)
  w = signal.tukey(t.size, 0.05) # sinyal kesitleri arası geçişlerin keskin olmaması için her kesiti tukey penceresi ile çarpacağız
  sinyal = np.array([]) # sinyali boş oluşturup sağına sinyal parçaları ekleyeceğiz
  for frekans in frekans_dizisi:
    sinyal = np.concatenate((sinyal, np.cos(2 * np.pi * frekans * t) * w)) # bir sinüzoid oluşturup sinyalin devamına ekleniyor
  return sinyal

Şimdi frekans serilerimizi oluşturalım. Frekans uzayımızı doğrusal ve logaritmik parçalara bölüp frekans değerleri seçerek iki dizi oluşturacağız.

# Frekans bandı sınırları ve kaç parçaya bölüneceğini keyfi olarak seçelim
baslangic_frekansi = 220 # Hz cinsinden başlangıç frekans değeri
bitis_frekansi = 440 # Hz cinsinden bitiş frekans değeri
parca_sayisi = 12

# Doğrusal bölme: 13 adet frekans değeri aralık doğrusal olarak bölünerek elde ediliyor
delta = (bitis_frekansi - baslangic_frekansi) / parca_sayisi
frekanslar_dogrusal_bolunmus = np.arange(baslangic_frekansi, bitis_frekansi+delta/2, delta)
print("Doğrusal eşit bölünme ile seçilen frekanslar (Hz):\n", frekanslar_dogrusal_bolunmus)

# Logaritmik bölme: 13 adet frekans değeri aralık logaritmik olarak bölünerek elde ediliyor
# Frekans uzayını log2 döşümüne tabi tutup eşit parçalara ayırıp değerler seçelim 
delta = (np.log2(bitis_frekansi) - np.log2(baslangic_frekansi)) / parca_sayisi
frekanslar_log2Hz = np.arange(np.log2(baslangic_frekansi), np.log2(bitis_frekansi)+delta/2, delta)
# log2-Hz frekans değerlerimizi tekrar Hz'e döndürelim
frekanslar_log_bolunmus = np.power(2, frekanslar_log2Hz) 
print("\nLogaritmik eşit bölünme ile seçilen frekanslar (Hz):\n", frekanslar_log_bolunmus)
Doğrusal eşit bölünme ile seçilen frekanslar (Hz):
 [220.         238.33333333 256.66666667 275.         293.33333333
 311.66666667 330.         348.33333333 366.66666667 385.
 403.33333333 421.66666667 440.        ]

Logaritmik eşit bölünme ile seçilen frekanslar (Hz):
 [220.         233.08188076 246.94165063 261.6255653  277.18263098
 293.66476792 311.12698372 329.62755691 349.22823143 369.99442271
 391.99543598 415.30469758 440.        ]

Şimdi bu frekans dizileri için ses sentezleyip dinleyelim. Dinlerken kendimize şu soruyu sormalıyız: hangi dizide sesler birbirini takip ederken frekans değişimleri eşit miktarda artmış hissi veriyor? (Birden fazla kez dinleyiniz.)

# Doğrusal bölünme ile elde edilen frekans dizisinin sentezi
ses_dogrusal_bolunme = sinus_dizisi_sentezle(frekanslar_dogrusal_bolunmus)
Audio(ses_dogrusal_bolunme, rate=16000)
# Logaritmik bölünme ile elde edilen frekans dizisinin sentezi
ses_logaritmik_bolunme = sinus_dizisi_sentezle(frekanslar_log_bolunmus)
Audio(ses_logaritmik_bolunme, rate=16000)

Birçok insan, logaritmik bölünme ile elde edilen sesleri birbirini eşit miktar ve daha düzenli frekans artışlarıyla artıyor şeklinde algılıyor. Birçok insanla yapılan dinleme testlerinden yola çıkarak, algılanan frekans ile ölçülen frekans arasındaki ilişkiyi ortaya koyan bir dönüşüm formülü mevcut: Mel-skalası (İng: Mel scale). Mel-skalası müzik sinyal işlemede frekans uzayının temsilinde yaygın olarak kullanılıyor.

Yukarıda yaptığımız deney kısa geldi ise yukarıdaki frekans aralıklarını ve nota sayılarını değiştirerek kendiniz için farklı örnekler sentezleyip dinleyebilirsiniz.

5.1.1.1. Eşit yedirimli bölme#

Frekans eksenini eşit yedirimli bölme (İng: equal temperament): Üstteki logaritmik bölme işlemi, günümüz Batı Klasik ve Pop Müziği için tercih edilen notalar için frekans belirleme yöntemi ile aynıdır: 220Hz - 440Hz arasını (1 oktavı) 12 eşit parçaya bölerek (nota) frekans değerlerini seçmiş olduk. Diğer oktavlardaki nota frekans karşılıklarını bulmak için de bu elde ettiğimiz değerleri \(2^{k}\) (\(k\) tamsayı) ile çarpmamız yeterli olacaktır. Müzik kuramında, frekans uzayını bu şekilde logaritmik bölerek elde edilen ses frekans dizilerine “eşit yedirimli” (İng: TET, tone equal temperament) adı verilir (örneğin oktavın 19 logaritmik eşit parçaya bölünmesiyle elde edilen nota frekansları dizisi kısaca 19TET olarak adlandırılır). Buradaki yedirim işlemi, başka bir işlem sonucu oluşan bir farkın dağıtılmasından/yedirilmesinden gelse de sonuç olarak logaritmik eşit bölünmeye karşılık gelmektedir. Batı müziği için kullanılan diğer ses sistemleri ile ilgili bakınız.

Batı klasik ve pop müziği için yaygın olarak kullanılan nota frekansları artık bir standart haline gelmiş ve birçok müzik teknolojisi aracının tasarımında varsayılan değerler olarak kullanılmaktadır. Örneğin frekans ekseninin logaritmik 12 eşit parçaya bölünmesi(12TET) ve 440 Hz’in 4. oktavdaki La sesi olarak seçilmesi ile elde edilen nota frekans tablosu tamsayı değerlere de eşlenerek MIDI (Musical Instrument Digital Interface) protokolü tanımlanmıştır. Aşağıda, piyano notası, nota isimleri, MIDI numaraları ve karşılık gelen frekans değerleri tablosunu bulabilirsiniz. (220Hz-440Hz arasındaki değerlerin yukarıda logaritmik eşit bölmeyle elde ettiğimiz değerlerle aynı olduğunu görebilirsiniz)

Şekil 5.4: Batı müziği standart nota frekansları, nota isimleri ve MIDI numaraları eşleme tablosu

MIDI numaraları klavyedeki tuşlara eşlenmiş tamsayılar olup bu numaraları doğrudan bu tuşların endeksleri olarak da düşünebiliriz (A4: 69. tuş, B4: 71. tuş,.. gibi). A4 notasının 440Hz’e eşlendiği ve oktavın 12 eşit logaritmik parçaya bölündüğü düşünülerek Hz cinsinden frekans değeri ile MIDI numaraları arasında dönüşüm fonksiyonları aşağıdaki gibi yazılabilir.

def hz_den_midi(frekans_hz):
  midi_no = 69 + np.log2(frekans_hz/440) * 12
  return midi_no

def midi_den_hz(midi_no):
  frekans_hz = pow(2, ( midi_no - 69 ) / 12 ) * 440
  return frekans_hz

Bu fonksiyonları test etmek için bazı MIDI numaraları seçelim, tablomuzdaki değerlerle karşılaştıralım

midi_numaralari = [0, 57, 60, 69] # C-1, A3, C4, A4 notalarının MIDI numaraları
for midi_no in midi_numaralari:
  print('MIDI#',midi_no, ' karşılığı =', midi_den_hz(midi_no), 'Hz')

frekanslar_hz = [110, 220, 440] # A2, A3, A4 notalarının frekansları (Hz)
for frekans_hz in frekanslar_hz:
  print(frekans_hz, ' Hz karşılığı midi = ', hz_den_midi(frekans_hz))
MIDI# 0  karşılığı = 8.175798915643707 Hz
MIDI# 57  karşılığı = 220.0 Hz
MIDI# 60  karşılığı = 261.6255653005986 Hz
MIDI# 69  karşılığı = 440.0 Hz
110  Hz karşılığı midi =  45.0
220  Hz karşılığı midi =  57.0
440  Hz karşılığı midi =  69.0

Tabloda verilmiş değerlerle hesapladığımız değerleri karşılaştırdığınızda uyumlu olduklarını göreceksiniz.

Tablodaki nota isimlerinin karşılık geldiği MIDI numaralarına veya frekanslara bu defterin geri kalan kısmında ihtiyaç duyacağız. Bu bilgiyi bir tablodan okuyup bir Python kütüphanesi içinde saklayalım.

import pandas as pd # csv dosyasını okumak için pandas kütüphanesini kullanalım
nota_fr_tablo = pd.read_csv('https://github.com/barisbozkurt/dataDumpForCourses/raw/master/noteFrequencies.csv')
# Tablodaki ilk 10 öğeyi listeleyelim
nota_fr_tablo.head(10)
notes frequencies
0 - 0.00
1 _ 0.00
2 s 0.00
3 C0 16.35
4 C#0 17.32
5 Db0 17.32
6 D0 18.35
7 D#0 19.45
8 Eb0 19.45
9 E0 20.60
# Tabloyu pandas-dataFrame'den kütüphaneye dönüştürelim
nota_frekanslari = {nota_fr_tablo.iloc[ind]['notes']:nota_fr_tablo.iloc[ind]['frequencies'] for ind in range(3,nota_fr_tablo.shape[0])}
print(nota_frekanslari)
{'C0': 16.35, 'C#0': 17.32, 'Db0': 17.32, 'D0': 18.35, 'D#0': 19.45, 'Eb0': 19.45, 'E0': 20.6, 'F0': 21.83, 'F#0': 23.12, 'Gb0': 23.12, 'G0': 24.5, 'G#0': 25.96, 'Ab0': 25.96, 'A0': 27.5, 'A#0': 29.14, 'Bb0': 29.14, 'B0': 30.87, 'C1': 32.7, 'C#1': 34.65, 'Db1': 34.65, 'D1': 36.71, 'D#1': 38.89, 'Eb1': 38.89, 'E1': 41.2, 'F1': 43.65, 'F#1': 46.25, 'Gb1': 46.25, 'G1': 49.0, 'G#1': 51.91, 'Ab1': 51.91, 'A1': 55.0, 'A#1': 58.27, 'Bb1': 58.27, 'B1': 61.74, 'C2': 65.41, 'C#2': 69.3, 'Db2': 69.3, 'D2': 73.42, 'D#2': 77.78, 'Eb2': 77.78, 'E2': 82.41, 'F2': 87.31, 'F#2': 92.5, 'Gb2': 92.5, 'G2': 98.0, 'G#2': 103.83, 'Ab2': 103.83, 'A2': 110.0, 'A#2': 116.54, 'Bb2': 116.54, 'B2': 123.47, 'C3': 130.81, 'C#3': 138.59, 'Db3': 138.59, 'D3': 146.83, 'D#3': 155.56, 'Eb3': 155.56, 'E3': 164.81, 'F3': 174.61, 'F#3': 185.0, 'Gb3': 185.0, 'G3': 196.0, 'G#3': 207.65, 'Ab3': 207.65, 'A3': 220.0, 'A#3': 233.08, 'Bb3': 233.08, 'B3': 246.94, 'C4': 261.63, 'C#4': 277.18, 'Db4': 277.18, 'D4': 293.66, 'D#4': 311.13, 'Eb4': 311.13, 'E4': 329.63, 'F4': 349.23, 'F#4': 369.99, 'Gb4': 369.99, 'G4': 392.0, 'G#4': 415.3, 'Ab4': 415.3, 'A4': 440.0, 'A#4': 466.16, 'Bb4': 466.16, 'B4': 493.88, 'C5': 523.25, 'C#5': 554.37, 'Db5': 554.37, 'D5': 587.33, 'D#5': 622.25, 'Eb5': 622.25, 'E5': 659.25, 'F5': 698.46, 'F#5': 739.99, 'Gb5': 739.99, 'G5': 783.99, 'G#5': 830.61, 'Ab5': 830.61, 'A5': 880.0, 'A#5': 932.33, 'Bb5': 932.33, 'B5': 987.77, 'C6': 1046.5, 'C#6': 1108.73, 'Db6': 1108.73, 'D6': 1174.66, 'D#6': 1244.51, 'Eb6': 1244.51, 'E6': 1318.51, 'F6': 1396.91, 'F#6': 1479.98, 'Gb6': 1479.98, 'G6': 1567.98, 'G#6': 1661.22, 'Ab6': 1661.22, 'A6': 1760.0, 'A#6': 1864.66, 'Bb6': 1864.66, 'B6': 1975.53, 'C7': 2093.0, 'C#7': 2217.46, 'Db7': 2217.46, 'D7': 2349.32, 'D#7': 2489.02, 'Eb7': 2489.02, 'E7': 2637.02, 'F7': 2793.83, 'F#7': 2959.96, 'Gb7': 2959.96, 'G7': 3135.96, 'G#7': 3322.44, 'Ab7': 3322.44, 'A7': 3520.0, 'A#7': 3729.31, 'Bb7': 3729.31, 'B7': 3951.07, 'C8': 4186.01, 'C#8': 4434.92, 'Db8': 4434.92, 'D8': 4698.63, 'D#8': 4978.03, 'Eb8': 4978.03, 'E8': 5274.04, 'F8': 5587.65, 'F#8': 5919.91, 'Gb8': 5919.91, 'G8': 6271.93, 'G#8': 6644.88, 'Ab8': 6644.88, 'A8': 7040.0, 'A#8': 7458.62, 'Bb8': 7458.62, 'B8': 7902.13}

Yukarıdaki tabloda MIDI numaralarının piyanoda tuşlara (ve dolayısıyla notalara) karşılık geldiğini görmüştük. Bu durumda bir müzik parçasını hangi zamanda hangi tuşa basıldığı bilgisi ile (örneğin tamsayı dizileri ile) ifade edebiliriz. Tuştan elin ne zaman çekildiği, dolayısıyla notanın ne kadar sürdüğü bilgisini de eklediğimizde nota içeriğini (dinamikler dışında) tamamen temsil edebiliriz.

Otomatik çalan piyanoların tasarımında bu fikir kullanılmış ve bugün yaygın olarak kullanılan piano-roll temsili elde edilmiştir. Spektrograma benzer bir zaman-frekans temsili olan piyano rulosu için aşağıdaki örneği inceleyiniz. \(y\) ekseni MIDI numaralarını temsil etmektedir.

from IPython.display import YouTubeVideo
YouTubeVideo('E2j-frfK-yg', width=800, height=300)

5.2. Spektrogramdan piyano rulosu benzeri bir temsil elde etmek: Kromagram#

Spektrogramdan piyano rulosu benzeri bir temsil elde etmek: Kromagram (İng: Chromagram):

Spektrogram ve piyano rulosu, ikisi de zaman-frekans temsilleri olup gerçekten benzer özelliklere sahiptirler. Farkları:

  • Spektrogramda frekans ekseni doğrusal Hz değerleriyle temsil edilmiştir, piyano rulosunda logaritmik dönüşüm sonucu elde edilen (ve piyano tuşlarının endekslerine karşılık gelen) tam-sayı değerlerle

  • Spektrogramda tekil bir notanın harmonikleri de (tam kat frekanslarda) bileşen olarak bulunmaktadır. Piyano rulosunda harmonikler için bilgi bulunmamaktadır.

Verilen bir ses kaydına karşılık gelen piyano rulosu temsilinin otomatik bulunması işlemine otomatik notaya dökme (İng: automatic music transcription) ismi verilir. Müzik ses işleme alanının zor problemlerinden birisi olarak bilinir. Bu alanda güncel çalışmaların özetlendiği bir makale için bakınız: Benetos et al, 2018, “Automatic Music Transcription: An Overview”.

Spektrogramdan yola çıkarak piyano rulosu elde etmek zor bir araştırma problemi olmakla beraber, spektrogramı piyano rulosuna benzer bir temsil olan kromagram’a dönüştürmek görece daha kolay bir işlemdir ve otomatik akor tespiti gibi birçok uygulamada kromagram temsili yaygın olarak kullanılmaktadır.

Kromagram, her notaya düşen toplam enerjiyi yansıtan bir zaman-frekans gösterimidir. Müzikteki kroma terimi renk-kromasına benzer şekilde kullanılır.

Bir notanın temelde iki boyutta bilgi içerdiğini düşünebiliriz:

  • notanın bulunduğu oktav

  • notanın ismi

Örneğin A4 için oktav = 4, nota = A (La)

Oktav bilgisini dışarıda bıraktığımızda elimizde kroma bilgisi (nota ismi) kalır. Aşağıda renklerle de ilişki kuran nota uzayı temsilini inceleyiniz. Nota isimleri renklerle ilişkilendirilmiş ve kroma ismi verilmiştir.

Şekil 5.5: Müzikal frekans uzayının iki boyutu: ton yüksekliği/oktav bilgisi/tizlik ve renklere eşlenerek gösterilen kroma kategorileri

# Kroma isimlerini ileride kullanmak üzere bir listeye yazalım
kroma_isimleri = ['C', 'C#/Db', 'D', 'D#/Eb', 'E', 'F', 'F#/Gb', 'G', 'G#/Ab','A', 'A#/Bb', 'B']

Kroma özniteliği ve kromagram: Kroma özniteliği bir sinyal kesiti için her bir 12 kromaya/notaya karşılık toplam enerji sevilerini içeren bir temsildir (12 boyutlu bir vektör). Spektrogramda olduğu gibi; ardışık kesitler için kromaları hesaplayıp yanyana dizdiğimizde kromagramı elde ediyoruz.

Bu durumda, spektrogramdan kromagram elde etmek istediğimizde spektrogramdaki genlik spektrumunu 12 boyutlu bir temsile indirmemiz gerekmekte. Bunun için gerekli adımları detaylı olarak ele alan bir Jupyter defteri için bakınız. Bu dönüşüm Librosa kütüphanesi içerisinde bir fonksiyon olarak da mevcut: librosa.feature.chroma_stft. Spektrogramdan kromagram elde etme işlemi sırasında kromagram temsilini temiz bir nota dizisi temsiline benzetmek için çeşitli ek adımlar dikkate alınabilir. Bu yönde çabalar birçok farklı kromagram dönüşüm işleminin tanımlanmasına yol açmıştır. Kromagram hesabının çeşitli varyantları için bakınız. Birçok uygulamada etkinliği raporlanmış bir kroma hesaplama aracı için bakınız.

Ne kadar hazır fonksiyonlara erişimimiz olsa da, kendi başımıza spektrumdan kroma elde eden bir fonksiyon yazmamızda fayda var. Bu, iki temsil arasındaki ilişkileri öğrenmemize ve elimizdeki temsil vektörlerin özelliklerini tanımamıza yardımcı olacaktır.

Şekil 5.6: Spektrogram ve piyano rulosu

Şu şekilde bir dönüşüm uygulayacağız: Genlik spektrumunun her örneğinin karşılık geldiği Hz cinsinden frekansı ve bu değerden yola çıkarak MIDI numarasını bulacağız. Daha sonra oktav bilgisini çıkartmak için MIDI numarasının 12’ye göre modunu alacağız. Bu bize, genlik spektrumundan aldığımız örneğin hangi kromaya karşılık geldiğini verdiği için bunu o kromanın toplam genlik değerine ekleyeceğiz. Tüm genlik spektrumu noktaları için bu adımları işlettiğimizde her bir kromaya düşen toplam genlik değerini hesaplamış olacağız. Bu işlemi yaparken bütün frekans bandı yerine düşük frekans bölgesindeki bir bandı alacağız çünkü spektrumda yüksek frekans bölgelerinde harmonik enerjileri düşüktür, o bölgeden bileşenleri toplarsak harmonik dışında diğer bileşenleri de temsilimize gereksiz bir şekilde eklemiş oluruz.

def kromagram_cizdir(x, pencereFonksiyonAdi, pencere_genisligi, pencere_kaydirma_miktari, fft_N, min_frekans, max_frekans, ornekleme_fr, ikili_deger_kullan = False):
  # Sessiz bölgeleri tespit edip sıfırlamakta fayda var
  # Keyfi seçilmiş bir eşik değeri kullanacağız. Kromada max değer bu değerin altında ise 
  # elimizdeki kromanın sessiz bölüme karşılık geldiğini varsayıp sıfırlayacağız
  sessizlik_esik_degeri = 0.2 
  
  min_frekans_endeks = round(min_frekans * fft_N / ornekleme_fr) # Hz -> endeks dönüşümü
  max_frekans_endeks = round(max_frekans * fft_N / ornekleme_fr)

  # Sinyalimizi pencerelemek için pencere fonksiyonumuzu tanımlayalım
  w = signal.get_window(pencereFonksiyonAdi, pencere_genisligi)

  # Pencere başlangıç endeksilerini bir diziye toplayalım
  baslangic_endeksleri = np.arange(0, x.size - pencere_genisligi, pencere_kaydirma_miktari, dtype = int)
  pencere_sayisi = baslangic_endeksleri.size

  # Her pencere için genlik spektrumu hesaplatıp bunu kromaya dönüştüreceğiz
  # ve bu kromaları arka arkaya dizerek kromagram oluşturacağız
  kromagram = []
  for k in range(pencere_sayisi):
      basla_endeks = baslangic_endeksleri[k]
      x_w = x[basla_endeks:basla_endeks + pencere_genisligi] * w
      genlik_spekt = np.power(10, genlik_spektrumu(x_w, fft_N) /20 ) # dB yerine doğrusal değer toplamayı tercih edeceğiz
      bant_enerjileri = np.zeros((12,))
      for frekans_endeks in np.arange(min_frekans_endeks, max_frekans_endeks):
          # Frekans uzayında endeksin karşılık geldiği Hz değerini ve bundan da MIDI numarasını elde edeceğiz
          frekans_endeksHz = frekans_endeks * ornekleme_fr / fft_N
          binMidi = int(np.round(hz_den_midi(frekans_endeksHz)))
          bant_enerjileri[np.mod(binMidi,12)] += genlik_spekt[frekans_endeks]
      # Sinyalin kendisinin toplam enerjisinin kroma temsilinde önemi yok, normalizasyon uygulayarak bu etkiyi kaldıralım
      bant_enerjileri = bant_enerjileri / np.sum(bant_enerjileri)

      # Kromaları 0-1 arası değerlerle temsil etmek yerine maksimum değer 1, diğerleri 0 olarak temsil etmeyi tercih edebiliriz: 
      # Bu temsili "ikili değerli"(binary valued) olarak düşünebiliriz
      if ikili_deger_kullan:
        max_deger = np.max(bant_enerjileri)
        max_endeks = np.argmax(bant_enerjileri)
        bant_enerjileri[:] = 0
        if max_deger > sessizlik_esik_degeri:
          bant_enerjileri[max_endeks] = 1
      # İlgili pencere için hesaplanan kromayı kromagramımıza ekle(mle)yelim
      kromagram.append(bant_enerjileri)

  kromagram = np.array(kromagram) 
  # Kroma isimlerini batı müziği ses sisteminden alacağız: C: do, D: re, ... B: si
  kroma_isimleri = ['C', 'C#/Db', 'D', 'D#/Eb', 'E', 'F', 'F#/Gb', 'G', 'G#/Ab','A', 'A#/Bb', 'B']

  fig = plt.figure(figsize = (18, 4))
  plt.imshow(kromagram.transpose())
  #ax.tick_params(axis = 'x',which = 'both',top = 'off',bottom = 'off',labelbottom = 'off') 
  plt.title('Kromagram')
  plt.yticks(np.arange(12), kroma_isimleri)
  plt.ylabel('Kroma ismi')
  plt.xlabel('Pencere endeksi')
  plt.axis('tight');
  return kromagram

Şimdi saksafon ses sinyalimizin kromagramını oluşturup çizdirelim.

# Kromagram çizdirme
pencere_genisligi = 4096
pencere_kaydirma_miktari = 2048
fft_N = 4096
# Kroma hesabında kullanılacak frekans bandı sınırları
min_frekans = 100
max_frekans = 4000

kromagram_cizdir(muzik_sinyali, 'hann', pencere_genisligi, pencere_kaydirma_miktari, fft_N, min_frekans, max_frekans, ornekleme_fr);
../_images/05_MuzikSinyalleriSpektrumu_54_0.png

Şekil 5.7: Kromagram: saksafon kaydı örneği

Bu kayda karşılık gelen nota yazıldığında, icra edilen nota dizisinin [C5, B4, C5, D5, A4, Bb4] olduğu görülecektir. Kromagramda enerjilerin yüksek olduğu bölgeleri bu bilgi ışığında inceleyiniz.

En yüksek enerjinin gözlendiği kromaya ek olarak çoğunlukla o kromanın 5’lisinde (o kroma ile başlayan majör gamda 5. ses, örneğin C için G) bir miktar enerji gözlemeyi bekliyoruz. Çünkü ses sinyali temel tireşim frekansının tam katlarında bileşenler (harmonikler) içeriyor. Genelde ilk 7-8 harmonikten sonra enerji düştüğü için ilk harmoniklerin enerjilerinin yansımasını kromagramda gözlüyoruz. Sırayla yazmak istersek, örneğin C kromasında bir sesin harmonikleri, frekansları ve karşılık geldiği kromalar şöyle listelenebilir:

    1. harmonik: C, \(f_{harmonik} = f_0 * 1\),

    1. harmonik: C, \(f_{harmonik} = f_0 * 2\), (1 üst oktavda aynı kroma),

    1. harmonik: G, \(f_{harmonik} = f_0 * 3\), (1 üst oktavda farklı kroma)

    1. harmonik: C, \(f_{harmonik} = f_0 * 4\), (2 üst oktavda aynı kroma),

    1. harmonik: E, \(f_{harmonik} = f_0 * 5\), (2 üst oktavda farklı kroma)

Bir nota üzerinden örneklendirelim. Örneğin C2 notasının çalındığı ve tüm harmonikleri barındıran bir enstrüman kullanıldığında ilk 5 harmonik için frekanslar ve daha önce okuduğumuz nota frekansları tablolarında karşılık gelen notaları listeleyelim:

# C2 notasının ilk 5 harmoniğinin frekanslarının listelenmesi 
f0 = nota_frekanslari['C2']
print('C2 notası için: f0 = {} Hz'.format(f0))
for k in range(1,6):
  print('{}.harmonik frekansı: {} Hz'.format(k, f0 * k))
print('Nota frekansları:')

# Daha önce dosyadan okuduğumuz standart nota frekans tablosundan 
#  yukarıda bahsi geçen notaları frekanslarıyla beraber listeleyelim
for nota in ['C2', 'C3', 'G3', 'C4', 'E4']:
  print('{}: {} Hz'.format(nota, nota_frekanslari[nota]))
C2 notası için: f0 = 65.41 Hz
1.harmonik frekansı: 65.41 Hz
2.harmonik frekansı: 130.82 Hz
3.harmonik frekansı: 196.23 Hz
4.harmonik frekansı: 261.64 Hz
5.harmonik frekansı: 327.04999999999995 Hz
Nota frekansları:
C2: 65.41 Hz
C3: 130.81 Hz
G3: 196.0 Hz
C4: 261.63 Hz
E4: 329.63 Hz

Sonuç olarak, C kromalı bir nota icrası içeren bir sinyalde 3. ve 5. harmonikler sebebiyle G ve E kromalarında da bir miktar enerji gözlemeyi bekliyoruz. Örneğin yukarıda elde ettiğimiz kromagramda ilk nota C5 olup en yüksek enerji C kromasında gözlenmekle beraber G kromasında da bir miktar enerji olduğunu görebiliyoruz. (benzer gözlemleri ikinci nota olan B4 için F# kromasında, dördüncü nota olan D5 için A kromasında yapabiliyoruz).

Monofonik (tek enstrüman sesinin olduğu) kayıtların kromagramını oluştururken harmoniklerin etkisini gözardı etmek istersek izleyebileceğimiz basit yollardan birisi sadece en yüksek enerjili kromayı 1, diğerlerini 0 ile temsil etmek. Bunu fonksiyonumuzun son parametresi (ikili_deger_kullan) ile kontrol ediyoruz. Deneyelim:

kromagram_cizdir(muzik_sinyali, 'hann', pencere_genisligi, pencere_kaydirma_miktari, fft_N, min_frekans, max_frekans, ornekleme_fr, ikili_deger_kullan=True);
../_images/05_MuzikSinyalleriSpektrumu_59_0.png

Şekil 5.8: İkili(İng: binary) kromagram: saksafon kaydı örneği

Bu işlem, bu sinyal için ana kromanın kalıp harmonik kaynaklı kroma bilgisinin dışarıda bırakılmasını sağladı ve piyano rulosuna (nota dizilimine) çok yakın bir temsil elde ettik.

Birkaç başka kayıt için kromagram çizimlerine bir göz atalım. Dosyaları internetten indirerek çizimleri oluşturacağız.

%%capture
# "UIOWA:MIS dataset" kayıtlarından örnekler indirip kullanalım
import urllib.request
import zipfile
import os, sys,shutil

# UIOWA:MIS dataset örnekleri adresleri
linkler = {'flute':'http://theremin.music.uiowa.edu/sound%20files/MIS/Woodwinds/flute/flute.nonvib.ff.zip',
         'clarinet':'http://theremin.music.uiowa.edu/sound%20files/MIS/Woodwinds/Ebclarinet/EbClar.ff.zip'}

veri_klasoru = 'enstruman'
if not os.path.exists(veri_klasoru):
  os.mkdir(veri_klasoru) # klasörü yarat
for enstruman, url in linkler.items():
    print(enstruman, 'dosyalarını indiriyor')
    hedef_klasor = os.path.join(veri_klasoru,enstruman)
    if not os.path.exists(hedef_klasor): # yoksa klasörü yarat
        os.mkdir(hedef_klasor)
    dosya_ismi = url.split('/')[-1]
    urllib.request.urlretrieve(url,dosya_ismi)
    # zip pakedinin açılması
    zip_ref = zipfile.ZipFile(dosya_ismi, 'r')
    zip_ref.extractall(hedef_klasor)
    zip_ref.close()
    os.remove(dosya_ismi) # zip dosyasının silinmesi
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
/tmp/ipykernel_23292/3992857142.py in <module>
     17         os.mkdir(hedef_klasor)
     18     dosya_ismi = url.split('/')[-1]
---> 19     urllib.request.urlretrieve(url,dosya_ismi)
     20     # zip pakedinin açılması
     21     zip_ref = zipfile.ZipFile(dosya_ismi, 'r')

~/miniconda3/envs/KitapYazim/lib/python3.9/urllib/request.py in urlretrieve(url, filename, reporthook, data)
    266 
    267             while True:
--> 268                 block = fp.read(bs)
    269                 if not block:
    270                     break

~/miniconda3/envs/KitapYazim/lib/python3.9/http/client.py in read(self, amt)
    461             # Amount is given, implement using readinto
    462             b = bytearray(amt)
--> 463             n = self.readinto(b)
    464             return memoryview(b)[:n].tobytes()
    465         else:

~/miniconda3/envs/KitapYazim/lib/python3.9/http/client.py in readinto(self, b)
    505         # connection, and the user is reading more bytes than will be provided
    506         # (for example, reading in 1k chunks)
--> 507         n = self.fp.readinto(b)
    508         if not n and b:
    509             # Ideally, we would raise IncompleteRead if the content-length

~/miniconda3/envs/KitapYazim/lib/python3.9/socket.py in readinto(self, b)
    702         while True:
    703             try:
--> 704                 return self._sock.recv_into(b)
    705             except timeout:
    706                 self._timeout_occurred = True

~/miniconda3/envs/KitapYazim/lib/python3.9/ssl.py in recv_into(self, buffer, nbytes, flags)
   1239                   "non-zero flags not allowed in calls to recv_into() on %s" %
   1240                   self.__class__)
-> 1241             return self.read(nbytes, buffer)
   1242         else:
   1243             return super().recv_into(buffer, nbytes, flags)

~/miniconda3/envs/KitapYazim/lib/python3.9/ssl.py in read(self, len, buffer)
   1097         try:
   1098             if buffer is not None:
-> 1099                 return self._sslobj.read(len, buffer)
   1100             else:
   1101                 return self._sslobj.read(len)

KeyboardInterrupt: 
# Şimdi bazı örnek sinyaller kullanalım (klasörlerde birçok başka kayıt bulunmakta, 
#  dosya isimini değiştirerek onlar için de aynı işlemleri deneyebilirsiniz)
muzik_sinyali, ornekleme_fr = sf.read('enstruman/flute/flute.nonvib.ff.C5B5.aiff')
Audio(muzik_sinyali, rate=ornekleme_fr)

Kaydı dinlerseniz bu örnekte flütün kromatik gamı (sırayla bir oktavlık aralıktaki tüm ardışık notaları) çaldığını duyacaksınız. Kroma dizisi olarak beklentimiz: [C, C#, D, D#, E, F, … B]

# Uzun bir dosya olduğu için görünümün anlaşılırlığını artırmak için penceremizi geniş seçeceğiz
pencere_genisligi = 4096 * 4
pencere_kaydirma_miktari = 2048 * 4
kromagram_cizdir(muzik_sinyali, 'hann', pencere_genisligi, pencere_kaydirma_miktari, fft_N, min_frekans, max_frekans, ornekleme_fr);
../_images/05_MuzikSinyalleriSpektrumu_65_0.png

Şekil 5.9: Kromagram: flüt kaydı örneği

Beklentimizi karşılayan bir kromagram elde ettik. Notalar sırasıyla birer adım (endeks bir artarak; kromatik) ilerliyor. İkili değerler kullanılan kromagramı da çizdirelim:

kromagram_cizdir(muzik_sinyali, 'hann', pencere_genisligi, pencere_kaydirma_miktari, fft_N, min_frekans, max_frekans, ornekleme_fr, ikili_deger_kullan=True);
../_images/05_MuzikSinyalleriSpektrumu_68_0.png

Şekil 5.10: İkili kromagram: flüt kaydı örneği

İkili değerler kullandığımızda yine oldukça temiz, piyano rulosuna yakın bir temsil elde ettik. Bu örnek sinyal işleme açısından görece kolaydı. Şimdi başka bir örnek ele alalım. Tuşlu çalgılarda yüksek frekanstaki harmoniklerin enerjileri daha yüksek olduğu için icra edilen notanın harmoniklerinin de kromagrama karmaşıklaştırıcı bir katkısı olduğunu göreceğiz.

%%capture
urllib.request.urlretrieve('https://d33wubrfki0l68.cloudfront.net/076d85706126547b668e9159d819af5c59a18b34/905dd/mp3/c-major-scale.mp3','c-major-scale.mp3')
# mp3'ten wav'a dönüştürme: terminalde ffmpeg aracı çalıştırılıyor
os.system('ffmpeg -i c-major-scale.mp3 -vn -acodec pcm_s16le -ac 1 -ar 44100 -f wav c-major-scale.wav')
muzik_sinyali, ornekleme_fr = sf.read('c-major-scale.wav')
Audio(muzik_sinyali, rate=ornekleme_fr)

Bu kayıtta da Do Majör (C major) dizi çıkıcı ve inici olarak icra edilmiş (kromagram gösterimimizde endeks sıfır en üstte olduğu için hareket ters olarak gözlenecek: yukarıdan aşağıya ve aşağıdan yukarıya). İşitsel deneyimden yola çıkarak beklenen kroma dizisi: [C, D, E, F, G, A, B, C, B, A, G, F, E, D, C]

pencere_genisligi = 4096
pencere_kaydirma_miktari = 2048
kromagram_cizdir(muzik_sinyali, 'hann', pencere_genisligi, pencere_kaydirma_miktari, fft_N, min_frekans, max_frekans, ornekleme_fr);
../_images/05_MuzikSinyalleriSpektrumu_74_0.png

Şekil 5.11: Kromagram: piyano kaydı örneği

Bu örnekte harmoniklerden kaynaklı bileşenlerin enerjilerinin daha fazla olduğunu görebiliyoruz. Örneğin ilk nota olan C için 3. harmonik olan G’de oldukça yüksek, hatta notanın ikinci yarısında değeri C’ninkini geçen bir bileşen olduğunu görüyoruz.

İkili kroma temsilini oluşturup inceleyelim:

kromagram_cizdir(muzik_sinyali, 'hann', pencere_genisligi, pencere_kaydirma_miktari, fft_N, min_frekans, max_frekans, ornekleme_fr, ikili_deger_kullan=True);
../_images/05_MuzikSinyalleriSpektrumu_78_0.png

Şekil 5.12: İkili kromagram: piyano kaydı örneği

Birinci notada bir hata görüyoruz: C’nin 3. harmoniği olan G kromasının enerjisi 1. harmoniğin enerjisini geçip öne çıktı. Diğer notalar için temsil sorunsuz görünüyor. İkili temsilin bu çeşit bir sorun içerebileceğini aklımızda tutalım.

Buraya kadar kendi kroma hesaplama fonksiyonlarımızı yazıp kullandık. Bu örnek için diğer kütüphane fonksiyonlarından birinin kullanımını da örnekleyelim:

# Librosa ile kromagram hesabı
import librosa
S = np.abs(librosa.stft(muzik_sinyali, n_fft=4096))**2
chroma = librosa.feature.chroma_stft(S=S, sr=ornekleme_fr)


fig = plt.figure(figsize = (18, 4))
plt.imshow(chroma)
plt.title('Kromagram')
plt.yticks(np.arange(12), kroma_isimleri)
plt.axis('tight')
plt.ylabel('Kroma ismi');
../_images/05_MuzikSinyalleriSpektrumu_81_0.png

Şekil 5.13: Librosa aracı ile hesaplanan kromagram: piyano kaydı örneği

Kendi yazdığımız fonksiyonla elde ettiğimiz kromagrama büyük oranda benzeyen bir temsil elde ettik.

Kroma özniteliği ve kromagram temsilini bazı ses örnekleriyle beraber ele almış olduk. Şimdi kromagram temsilini bir bilgi erişim probleminde kullanalım: otomatik anahtar tespiti/kestirimi.

5.3. Otomatik anahtar tespiti#

Otomatik anahtar tespiti (İng: Automatic key detection)

Müzik eserleri notaya alınırken, eserin bir ana tonalitesini temsil eden bir anahtar kullanılır. Otomatik notaya dökme işleminin adımlarından biri olarak öncelikle anahtarın tespit edilmesine ihtiyaç vardır. Anahtar/tonalite bilgisine birçok uygulamada ihtiyaç duyulmaktadır. Örneğin DJ-Mixing yöntemleri arasında Harmonic mixing için eldeki her kaydın tonalite bilgisine ihtiyaç vardır. Aşağıda kromagram temsilini kullanarak otomatik tonalite tespiti uygulaması yapacağız. Çoğu batı pop ve klasik müziği eserleri 12 majör veya 12 minör tonaliteden birisinde kategorize edilir. “Mode” kavramı çok daha geniş ve çeşitli olmakla beraber biz de eğitim için hazırlanmış bu defterde bu 24 tonaliteyi sınıflarımız olarak ele alacağız ve elimizdeki kaydı bu 24 kategoriden birisine eşlemeye çalışacağız.

Şekil 5.14: Tonaliteler ve anahtarlar

Elimizdeki bir otomatik sınıflandırma problemi olduğu için varolan birçok makine öğrenmesi algoritmasından birisini kullanabiliriz. Burada müzik dışındaki alanlarda görece nadir kullanılan ancak bu problem için yaygın kullanılan bir yöntem olan şablon eşleştirmeyi kullanacağız. Bu algoritma şu temel yöntemi uygulamaktadır: her bir kategori bir şablonla temsil edilir ve eldeki kayıt bütün şablonlarla teker teker karşılaştırılıp hangi şablona en yakın olduğu bulunur ve hangi şablona yakın ise kayıt o kategoriye atanır. Bu işlemi nota dağılımları üzerinden gerçekleştiren bir yöntem için bakınız:

Temperley, David. “What’s key for key? The Krumhansl-Schmuckler key-finding algorithm reconsidered.” Music Perception 17.1 (1999): 65-100.

Anahtar tespiti için şablon eşleştirme nota dağılımlarından yapılabildiği gibi kromagram ortalaması üzerinden de gerçekleştirilebilir. Nota dağılımlarında her bir kromadan ne sıklıkta ya da ne uzunlukta icra olduğu, kromagram ortalamasında ise her bir kromada toplam ne kadar genlik/enerji bulunduğu temsil edilmektedir ve bu ikisi ilişkili temsiller olarak düşünülebilir. Bu iki temsilin ilişkili olduğunu kabul ederek ilerleyelim. Yukarıdaki makalede 68. sayfa Figür 1’de C majör ve C minör için birer şablon verilmiş. Bunları ortalama kroma ile ilişkili kabul edelim ve birer vektör içine yazıp ortalama kroma şablonları olarak kullanmayı deneyelim. Diğer 11 majör ve 11 minör tonalite şablonunu bu şablonu dairesel çevirerek (nota uzayında kaydırarak) elde edeceğiz.

def sablon_cizdir(sablon):
  fig = plt.figure(figsize=(8,3))
  plt.stem(sablon, use_line_collection=True)
  plt.xticks(np.arange(12), kroma_isimleri)
  plt.xlabel('Kroma isimleri')

major_sablon = np.array([6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88])
major_sablon = major_sablon / np.sum(major_sablon) # değerleri toplam 1 olacak şekilde normalize edelim
sablon_cizdir(major_sablon)
plt.title('Major tonalite şablonu');
../_images/05_MuzikSinyalleriSpektrumu_89_0.png

Şekil 5.15: Krumhansl-Schmuckler majör tonalite nota dağılım şablonu

minor_sablon = np.array([6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17])
minor_sablon = minor_sablon / np.sum(minor_sablon) # değerleri toplam 1 olacak şekilde normalize edelim

sablon_cizdir(minor_sablon)
plt.title('Minör anahtar şablonu');
../_images/05_MuzikSinyalleriSpektrumu_91_0.png

Şekil 5.16: Krumhansl-Schmuckler minör tonalite nota dağılım şablonu

%%capture
# Internetten bazı kayıtlar indirip rahat okuyabilmek için wav formatına dönüştürelim
url = "https://archive.org/download/50_best_loved_works_by_vivaldi/013%20Mandolin%20Concerto%20In%20C%20Major%2C%20RV%20425%201-Allegro.mp3"
urllib.request.urlretrieve(url,'Cmajor_sample.mp3');
url = "https://archive.org/download/50_best_loved_works_by_vivaldi/031%20Concerto%20For%202%20Celli%20And%20Strings%20In%20G%20Minor%20Rv%20531g_2-%20Largo.mp3"
urllib.request.urlretrieve(url,'Gminor_sample.mp3');
# mp3 -> wav dönüşümü
os.system('ffmpeg -i Cmajor_sample.mp3 -vn -acodec pcm_s16le -ac 1 -ar 44100 -f wav Cmajor_sample.wav');
os.system('ffmpeg -i Gminor_sample.mp3 -vn -acodec pcm_s16le -ac 1 -ar 44100 -f wav Gminor_sample.wav');
# Daha sonra denemek isterseniz diğer örnekler:
#url = 'https://archive.org/download/cd_melody-in-f_anton-rubinstein-antonn-dvok-christian-sin/disc1/03.%20Felix%20Mendelssohn%20-%20Songs%20Without%20Words%2C%20E%20major%2C%20op.%2019%20no.%201_sample.mp3'
#urllib.request.urlretrieve(url,'Mendelssohn_Emajor_sample.mp3')
#url = 'https://archive.org/download/cd_the-sonatas-and-partitas-for-violin-solo_johann-sebastian-bach-gidon-kremer/disc1/01.05.%20Johann%20Sebastian%20Bach%20-%20Partita%20no.%201%20for%20Violin%20Solo%20in%20B%20minor%2C%20BWV%201002%20-%20I.%20Allemanda_sample.mp3'
#urllib.request.urlretrieve(url,'ViolinSolo_Bminor_sample.mp3')
#os.system('ffmpeg -i Mendelssohn_Emajor_sample.mp3 -vn -acodec pcm_s16le -ac 1 -ar 44100 -f wav Mendelssohn_Emajor_sample.wav')
#os.system('ffmpeg -i ViolinSolo_Bminor_sample.mp3 -vn -acodec pcm_s16le -ac 1 -ar 44100 -f wav ViolinSolo_Bminor_sample.wav')

Şablonlarla karşılaştırmak için kayıtların kroma(gram) ortalamalarına ihtiyaç duyacağız. Bir fonksiyonla hesaplayalım.

def kroma_ortalama_hesapla(x):
  pencere_genisligi = 4096 * 4
  pencere_kaydirma_miktari = 2048 * 4
  min_frekans = 100
  max_frekans = 2000
  x_kroma = kromagram_cizdir(x, 'hann', pencere_genisligi, pencere_kaydirma_miktari, fft_N, min_frekans, max_frekans, fs);
  x_kroma_toplam = x_kroma.sum(axis=0)
  return x_kroma_toplam / np.sum(x_kroma_toplam) # toplam değer bir olacak şekilde normalizasyon

Şablonla karşılaştırma sırasında ihtiyacımız olacak fark fonksiyonunu ve şablonla karşılaştırmayı gerçekleştirip hesaplanan en düşük 5 farkı ilgili anahtarlarla beraber listeleyen bir fonksiyon yazalım:

def L1_fark(x, y):
  '''Diziler arasındaki farkın L1 uzunluğunu hesaplar (https://mathworld.wolfram.com/L1-Norm.html)'''
  return np.sum(np.abs(x-y))

def karsilastirilan_kromalari_cizdir(kroma1, kroma2, etiketler, baslik):
  '''Kromaları üstüste çizdirme fonksiyonu (kullanımı opsiyonel)'''
  fig = plt.figure(figsize=(4,3))
  plt.stem(kroma1, markerfmt='C0o', use_line_collection=True, label=etiketler[0])
  plt.stem(kroma2, markerfmt='C1o', use_line_collection=True, label=etiketler[1])
  plt.xticks(np.arange(12), kroma_isimleri)
  plt.xlabel('Kroma isimleri')
  plt.title(baslik); plt.legend();

def en_yakin_tonalite_bul(kayit_kroma, major_sablon, minor_sablon, kroma_isimleri, sekil_ciz=False):
  '''kayıt kromasını dairesel kaydırarak tüm şablonlarla farkını hesaplar ve en düşük farkları listeler'''
  
  # Anahtarlar listesi
  anahtarlar = [n+' majör' for n in kroma_isimleri] + [n+' minör' for n in kroma_isimleri]

  farklar = []
  # Majör şablondan farkların hesaplanması
  for kaydirma_adimi in range(12):
    if kaydirma_adimi > 0:
      kaydirilmis_kroma = np.concatenate((kayit_kroma[kaydirma_adimi:], kayit_kroma[:kaydirma_adimi]))
    else:
      kaydirilmis_kroma = kayit_kroma
    fark = L1_fark(major_sablon, kaydirilmis_kroma)
    if sekil_ciz:
      karsilastirilan_kromalari_cizdir(major_sablon, kaydirilmis_kroma, ['majör şablon','kayıt'], 'Majör şablon ve {} adım kaydırılmış kayıt: fark: {}'.format(kaydirma_adimi, fark))
    farklar.append(fark)
  
  # Minör şablondan farkların hesaplanması
  for kaydirma_adimi in range(12):
    if kaydirma_adimi > 0:
      kaydirilmis_kroma = np.concatenate((kayit_kroma[kaydirma_adimi:], kayit_kroma[:kaydirma_adimi]))
    else:
      kaydirilmis_kroma = kayit_kroma    
    fark = L1_fark(minor_sablon, kaydirilmis_kroma)
    if sekil_ciz:
      karsilastirilan_kromalari_cizdir(minor_sablon, kaydirilmis_kroma, ['minör şablon','kayıt'], 'Minör şablon ve {} adım kaydırılmış kayıt: fark: {}'.format(kaydirma_adimi, fark))
    farklar.append(fark)

  # En düşük farkların listelenmesi/yazdırılması
  for ind in np.argsort(farklar)[:5]:
    print(anahtarlar[ind], 'şablonundan farkı:\t', farklar[ind])
  print('-------------------------------')

Şimdi artık ses dosyasını okuyup anahtarını tespit etmeye çalışabiliriz. Do (C) majör örneğinden başlayalım.

Kaydın ilk 0.5 dakikalık kısmından bir analiz yapalım. Hem analizimiz daha kısa sürecektir, hem de eser ilerledikçe başka tonalitelere geçiş yapma riski artacaktır. Kaydın tümüne şu linkten erişebilirsiniz.

x, fs = sf.read('Cmajor_sample.wav')
x = x[: int(fs*60*0.5)] # ilk 0.5 saniyelik kısmı alalım
x = x / np.max(np.abs(x))
Audio(x, rate=fs)
ortalama_kroma = kroma_ortalama_hesapla(x)
../_images/05_MuzikSinyalleriSpektrumu_102_0.png

Şekil 5.17: Kayıt kromagramı

sablon_cizdir(ortalama_kroma)
plt.title('Kayıt ortalama kroma');
../_images/05_MuzikSinyalleriSpektrumu_104_0.png

Şekil 5.18: Do (C) majör tonalitesinde bir kaydın ortalama kroma değerleri

Şimdi kaydın ortalama kromasını kullanarak tonatlite buldurma işlemini gerçekleştirebiliriz. Aşağıdaki fonksiyonun her adımda kaşılaştırılan vektörleri gösteren bir çizim oluşturmasını isterseniz sona “sekil_ciz=True” opsiyonunu ekleyebilirsiniz.

en_yakin_tonalite_bul(ortalama_kroma, major_sablon, minor_sablon, kroma_isimleri)
A minör şablonundan farkı:	 0.20756946554659533
C majör şablonundan farkı:	 0.21488364357400336
G majör şablonundan farkı:	 0.2268429599004791
E minör şablonundan farkı:	 0.2292163388591332
D majör şablonundan farkı:	 0.3106722761901595
-------------------------------

Parçayı en çok A minör şablonuna yakın buldu. C majör ikinci sırada geliyor. A minör, C majör’ün ilgili minörü olduğu için kabul edilebilir bir sonuç olarak düşünülebilir. Şimdi sol(G) minör tonalitedeki diğer örneğimizle deneyelim.

Yine kaydın ilk 0.5 dakikalık kısmından bir analiz yapalım. Kaydın tümüne ulaşmak isterseniz tıklayınız.

x, fs = sf.read('Gminor_sample.wav')
x = x[: int(fs*60*0.5)]
x = x / np.max(np.abs(x))
Audio(x, rate=fs)
ortalama_kroma = kroma_ortalama_hesapla(x)
../_images/05_MuzikSinyalleriSpektrumu_111_0.png

Şekilde 5.19: Kayıt kromagramı

sablon_cizdir(ortalama_kroma)
plt.title('Kayıt ortalama kroma');
../_images/05_MuzikSinyalleriSpektrumu_113_0.png

Şekil 5.20: Sol(G) minör tonalitesinde bir kaydın ortalama kroma değerleri

en_yakin_tonalite_bul(ortalama_kroma, major_sablon, minor_sablon, kroma_isimleri)
D minör şablonundan farkı:	 0.20779963660861234
G minör şablonundan farkı:	 0.21858068300176903
D majör şablonundan farkı:	 0.22947840818246046
G majör şablonundan farkı:	 0.23219320448507397
F majör şablonundan farkı:	 0.3183343248818621
-------------------------------

Bu örnekte de doğru sonuç ikinci sırada geldi. D minör G minör’ün 5’li ötesinde olduğu için bu da tipik otomatik tonalite bulma hataları arasında. Başarıyı etkileyen çok sayıda faktör bulunmakta. Bunların bazılarını şöyle sıralayabiliriz.

Kararı etkileyen faktörler:

  • Bir makaledeki nota dağılımlarını kroma ortalaması şablonları olarak kullandık. Şablonları verilerden öğrenmek daha sağlıklı bir yöntem olurdu

  • Kayıtta kullanılan referans frekansı A4=440 Hz’den farklı olabilir

  • Kroma içerisinde harmonik enerjileri bir miktar gürültü yaratıyor

  • Şablon eşleştirme algoritmasının sınırlılıkları (kullanılan fark fonksiyonu, vb)

  • Kroma hesaplama yöntemi

Bu defterimizde sinyal spektrumunu bir oktavlık bölgeye katlayarak ve bir oktavlık bölgede logaritmik bölünmeyle elde edilmiş 12 bölüme düşen toplam genliği/enerjiyi bularak hesapladığımız kroma özniteliğini ve bu özniteliğin kullanıldığı bir uygulamayı ele aldık. Sinyalin frekansını kestirmedik ama frekans dağılımına benzer bir temsile erişmiş olduk. Basitleştirilmiş kroma hesaplama ve şablon eşleştirme fonksiyonlarımızla iki gerçek kayıt üzerinden yaptığımız testte doğru sonuçlara ulaşamasak da çok yakın sonuçlara ulaşabildik (doğru sınıf iki testte de 2. sırada geldi).

Kroma özniteliği; otomatik akor kestirimi, ses-nota hizalaması gibi çok popüler uygulamalarda kullanılan temel öznitelik olması sebebiyle önemlidir. Bu defter, tekniğin son durumunu temsil eden bir kod sunmaktan çok konunun özünün anlaşılmasını kolaylaştırmak amacıyla karmaşıklığı az olacak şekilde hazırlanmıştır. Konu ilginizi çektiyse alttaki kaynakları incelemenizi öneririz.

Bir sonraki defterimizde sinyalin frekans ve enerji gibi düşük seviyeli özelliklerinin kestirimi ve bu özelliklerin müzik işleme uygulamalarında kullanımı üzerinde duracağız.

Kaynakça:

Önerilen diğer kaynaklar:

Yazar: Barış Bozkurt, editör: Ahmet Uysal