10. Ritmik Analiz#

Open In Colab

10.1. Ritmik Analiz#

Buraya kadar olan defterlerimizde temelde spektrum ve ezgi boyutlarını ele aldık. Bu defterimizde diğer önemli boyut olan ritim boyutunu ele alıyoruz.

Müzik ses sinyallerinin ritim boyutu ile ilişkili kavramların birçoğu oldukça tartışmaya açık ve kültüre özgüdürler. Burada amacımız sayısal sinyal işleme uygulamaları olduğu için mümkün olduğunca müzikolojik tartışmaların dışında kalmaya, basit sinyal öğeleri tanımlayıp kullanmaya çalışacağız. Bu defterde kullanacağımız temel kavramlar şunlar olacak:

  • Başlangıç anı (İng: onset) bir kayıttaki müzik notasının veya ses öğesinin başlangıç anına karşılık gelmektedir (bakınız: Bello et al 2005).

  • Vuruş (anları) (İng: beat): Vuruşu, müzikteki zaman organizasyonunda düzenli tekrarlayan, altta yatan temel zaman ızgarasının tik-tak’larından her biri gibi düşünebiliriz.

  • Tempo: Dakikadaki vuruş sayısı

Şimdi sırasıyla bu öğeleri sinyaller üzerinde gözleyip otomatik olarak tespit etmek için kullanılan algoritmaları ele almaya başlayalım. Önce kütüphanelerimizi kurup yükleyelim ve örnek kayıtlar indirelim.

!pip install essentia
import os, sys
import numpy as np
import matplotlib.pyplot as plt
import urllib.request
import librosa
from scipy.signal import get_window
import essentia.standard as ess
from essentia import Pool, array
from IPython.display import Audio
Requirement already satisfied: essentia in /home/baris/miniconda3/envs/KitapYazim/lib/python3.9/site-packages (2.1b6.dev857)
Requirement already satisfied: numpy>=1.8.2 in /home/baris/miniconda3/envs/KitapYazim/lib/python3.9/site-packages (from essentia) (1.22.4)
Requirement already satisfied: pyyaml in /home/baris/miniconda3/envs/KitapYazim/lib/python3.9/site-packages (from essentia) (6.0)
Requirement already satisfied: six in /home/baris/miniconda3/envs/KitapYazim/lib/python3.9/site-packages (from essentia) (1.16.0)
[   INFO   ] MusicExtractorSVM: no classifier models were configured by default

ODB verikümesinden bir grup örnek indirelim.

wav_klasor_linki = 'https://grfia.dlsi.ua.es/cm/worklines/pertusa/onset/ODB/sounds/'
etiket_klasor_linki = 'https://grfia.dlsi.ua.es/cm/worklines/pertusa/onset/ODB/ground-truth/'
ses_dosyalari = ['25-rujero.wav', 'RM-C036.wav', '3-you_think_too_muchb.wav', 'tiersen11.wav']

for dosya in ses_dosyalari:
    urllib.request.urlretrieve(wav_klasor_linki + dosya, dosya)
    etiket_dosyasi = dosya.replace('.wav','.txt')
    urllib.request.urlretrieve(etiket_klasor_linki + etiket_dosyasi, etiket_dosyasi)

print('Veriler indirildi')
Veriler indirildi

10.1.1. Başlangıç an(lar)ı tespiti#

Bir kaydın içerisindeki ses öğelerinin (notaların) başlangıç anlarının tespitinde ses öğesinin karakteri önem taşımaktadır. Genel olarak ses öğelerini şu 4 ayrı kategoriden birinde olarak düşünürüz:

  • Periyodik ve enerjisi ani değişen (İng: percussive), örnek: gitar veya piyano sesi

  • Periyodik ve enerjisi ani değişmeyen, örnek: ney sesi

  • Periyodik olmayan ve enerjisi ani değişen, örnek: davul sesi

  • Periyodik olmayan ve enerjisi ani değişmeyen, örnek: didjeridu sesi, elektronik müzikte kullanılan gürültünün filtrelenmesi ile elde edilen sesler

Her bir kategorideki ses için başlangıç anı tespit algoritmalarının etkinliği farklı olmaktadır.

Enerjisi ani değişen seslerin başlangıç anlarının tespiti için enerji sinyalini hesaplayıp kullanabileceğimizi düşündünüz sanırım. Enerji değişimini (enerjinin türevini) hesaplayan bir fonksiyon tanımlayalım ve indirdiğimiz sinyallerden birisiyle beraber görselleştirelim. Ayrıca, bu veritabanında elle işaretlenmiş başlangıç anları bilgisi de mevcut. Onları da beraber çizdirmek faydalı olacaktır.

def enerji_degisimi(x, pencere_uzunlugu, pencere_kaydirma_miktari, pencere_turu = "hann"):
  '''Verilen ses sinyalini pencerelere ayırıp her pencere için enerji hesaplayarak elde 
  edilen enerji serisinin türevini (yarım dalga doğrultma uygulandıktan sonra) döndürür.'''
  w = ess.Windowing(type = pencere_turu) # pencere ile çarpma işlemi gerçekleştirecek fonksiyon tanımı
  energy = ess.Energy() # enerji hesabı yapacak fonksiyon tanımı (6. defterde açık yazımı mevcut)
  NRG = [] # her pencere için hesaplanan enerjiyi bu listeye ekleyeceğiz
  for kesit in ess.FrameGenerator(x, frameSize = pencere_uzunlugu, hopSize = pencere_kaydirma_miktari):
    # sinyal kesitinin enerjisini hesaplayıp (yüksek değerlerin alçaklara yaklaştırılması amaçlı) log-kompresyon uyguluyoruz
    log_NRG = np.log10(energy(w(kesit))+np.finfo(float).eps)
    NRG.append(log_NRG)
  # veri türünü listeden diziye dönüştürelim
  NRG = np.array(NRG)
  # Fark/türev alma işlemi
  NRG_degisim = np.diff(NRG) 
  # Değişimin sadece pozitif kısımlarını alalım: yarım-dalga doğrultma (İng: half-wave rectification)
  NRG_degisim = (NRG_degisim + np.abs(NRG_degisim)) / 2
  # Genlik normalizasyonu
  NRG_degisim = NRG_degisim / max(NRG_degisim)
  
  return NRG_degisim

Analiz parametrelerimizi belirleyelim. Görsel incelemeyi kolaylaştırmak için sinyalin ilk 5 saniyelik kısmını kullanacağız.

ornekleme_fr = 44100
sure_saniye = 5 # saniye cinsinden ses sinyali uzunlugu
t = np.arange(sure_saniye * ornekleme_fr) / float(ornekleme_fr) # endekslere karşılık gelen saniye cinsinden zamanı taşıyan seri
pencere_uzunlugu = 2048 # ornek sayısı cinsinden pencere uzunluğu
pencere_kaydirma_miktari = 512 # ornek sayısı cinsinden pencere kaydırma miktarı

Ses sinyalini ve etiket dosyasını okutalım

dosya = '25-rujero.wav'
# Ses sinyalinin dosyadan okunması ve genlik normalizayonu
x = ess.MonoLoader(filename = dosya, sampleRate = ornekleme_fr)()
x = x[:sure_saniye * ornekleme_fr] # sinyalin ilk 5 saniyesini kullanacağız
x = x / np.max(np.abs(x))

# Elle işaretlenmiş başlangıç anlarını dosyadan okuyalım
baslangic_anlari = np.loadtxt(dosya.replace('.wav','.txt'))
baslangic_anlari = baslangic_anlari[baslangic_anlari < sure_saniye] # ilk 5 saniye içerisindeki başlangıç anlarını alalım

Ses kaydını, alttaki hücrenin çıktısının çal butonunu kullanarak dinleyiniz.

Audio(x, rate=ornekleme_fr)

Şimdi sinyal dalga formunu, enerji değişim sinyalini ve elle işaretlenmiş başlangıç noktalarını beraber çizdirebiliriz.

NRG_degisim = enerji_degisimi(x, pencere_uzunlugu, pencere_kaydirma_miktari)

f, axarr = plt.subplots(2, 1, figsize = (13, 4));
axarr[0].plot(t, x); axarr[0].set_title(dosya); axarr[0].axis('off');
axarr[0].vlines(baslangic_anlari, -1, 1, color='r');
axarr[1].plot(NRG_degisim,label = '(Log) Enerji değişimi)'); axarr[1].axis('off');axarr[1].legend(loc = 1);
../_images/10_RitmikAnaliz_17_0.png

Şekil 10.1: Ses kaydı, elle işaretlenmiş başlangıç anları (kırmızı dikey çizgiler) ve sinyalden hesaplanan enerji değişim fonksiyonu

Elle yerleştirilmiş başlangıç noktalarının enerji değişim sinyali tepeleriyle büyük oranda örtüştüğünü görebiliyoruz. Enerji değişim sinyalinin tepeleri üzerinden otomatik başlangıç noktası tespiti yapan bir fonksiyon yazmanız gerektiğini düşünün. Görece düşük genlikli bazı tepelerin gözardı edilmesi, bazılarının ise dikkate alınması gerekecektir. Bu tür durumda ilkin akla eşik değeri kullanmak (belirli genliğin altında olan tepeleri gözardı etmek) gelir. Buradaki örneği detaylı incelerseniz sabit bir eşik değeri belirlemenin zor olduğunu göreceksiniz. Bu tür durumlarda sinyalin özelliğine göre dinamik değişen lokal eşik değeri kullanabilirsiniz (Örneğin enerji değişim sinyalinin kesiti içerisindeki ortalama değerle orantılı bir eşik değeri kullanılabilir). Bu da tüm sorunları çözmeyecek, belki bir adım iyileşme sağlayacaktır.

Bu tür problemlerde, kullanabileceğimiz diğer akustik parametreleri de ele almak faydalı olacaktır. Birden fazla akustik parametrenin birarada kullanıldığı durumlarda performansta bir adım daha iyileşme gözleme ihtimali yüksektir. Şimdi bu tip (enerjisi ani değişen) sinyallerin başlangıç noktası tespitinde yaygın kullanılan diğer parametrelere geçelim.

  1. Spektral akı (İng: Spectral flux):

Spektral akı, genlik spektrumunun değişimini parametrize etmeye yönelik tasarlanmıştır: ardışık pencerelerden elde edilen genlik spektrumlarının farklarının toplamı şeklinde tanımlanır. \(k\) spektrumdaki örnek endeksi, \(|X_t[k]|\) \(t\). penceredeki genlik spektrumu olmak üzere \(t\) penceresindeki spektral akı \(SF_t\): $\( SF_t = \sum_{k=0}^{N/2} H( |X_t[k]| - |X_{(t-1)}[k])| ) \)\( olarak hesaplanabilir. Burada \)H(x)\( yarım-dalga doğrultma fonksiyonudur: \)\( H(x) = \frac{x+|x|}{2} \)$

  1. Yüksek frekans içeriği (İng: High frequency content): Yüksek frekans içeriği, frekans ile ağırlıklandırılmış genlik spektrumu toplamına karşılık gelir. Bu toplam işleminde, frekans arttıkça karşılık gelen genliğin ağırlığı artmakta, diğer bir deyişle yüksek frekans bileşenlerinin ağırlıklandırmada öne çıkarıldığı bir genlik toplamı hesaplanmaktadır. \(k\) spektrumdaki örnek endeksi, \(|X_t[k]|\) \(t\). penceredeki genlik spektrumu olmak üzere \(t\) penceresindeki yüksek frekans içeriği \(HFC_t:\)

\[ HFC_t = \sum_{k=0}^{N/2} k*|X_t[k]| \]

olarak hesaplanabilir.

Başlangıç anı tespiti için parametrelerde ani değişimin olduğu noktaların bulunması hedeflenir. Bu amaçla oluşturulan serilere de bu sebeple “yenilik/değişiklik fonksiyonlarını” (İng: Novelty functions) adı verilir. Yukarıda tanımladığımız yenilik/değişiklik fonksiyonlarını hesaplamak için Python fonksiyonları tanımlayalım ve örneğimiz üzerinde test edelim.

def spektral_aki(x, pencere_uzunlugu, pencere_kaydirma_miktari, pencere_turu = "hann"):
  '''Girdi olarak verilen x sinyalinin spektral akısını hesaplayıp döndürür'''
  w = ess.Windowing(type = pencere_turu)
  spectrum = ess.Spectrum(size = pencere_uzunlugu)

  # Genlik spektrum farkını hesaplamak için önceki sinyal penceresinin genlik spektrumunu alttaki seride tutacağız 
  onceki_genlik_spektrumu = np.zeros((1 + int(pencere_uzunlugu / 2),))
  sf = [] # her pencere için hesaplanan sf değerini bu listeye ekleyeceğiz
  for kesit in ess.FrameGenerator(x, frameSize = pencere_uzunlugu, hopSize = pencere_kaydirma_miktari):
      genlik_spektrumu = spectrum(w(kesit))
      spektral_fark = genlik_spektrumu - onceki_genlik_spektrumu
      h = (spektral_fark + np.abs(spektral_fark)) / 2
      sf.append(np.sum(h))
      onceki_genlik_spektrumu = genlik_spektrumu # bir sonraki döngü için eldeki spektrumu önceki spektrum olarak kaydediyoruz

  SF = np.array(sf[1:]) # ilk spektral fark sıfırdan çıkartılarak elde edildi, dışarıda bırakalım
  return SF / np.max(SF)

def yuksek_frekans_icerigi(x, pencere_uzunlugu, pencere_kaydirma_miktari, pencere_turu = "hann"):
  '''Girdi olarak verilen x sinyalinin yuksek frekans içeriğini hesaplayıp döndürür'''
  w = ess.Windowing(type = pencere_turu)
  spectrum = ess.Spectrum(size = pencere_uzunlugu)

  hfc = [] # her pencere için hesaplanan hfc değerini bu listeye ekleyeceğiz
  for kesit in ess.FrameGenerator(x, frameSize = pencere_uzunlugu, hopSize = pencere_kaydirma_miktari):
      genlik_spektrumu = spectrum(w(kesit))
      # ağırlıklı toplam işlemi dot-product olarak yapabiliriz, k değerleri de np.arange ile 0 ile N-1 arasında oluşturulabilir
      hfc_t = np.dot(genlik_spektrumu, np.arange(genlik_spektrumu.size))
      hfc.append(hfc_t) 

  hfc = np.array(hfc)
  return hfc / np.max(hfc)

Şimdi aynı sinyal için parametrelerimizi hesaplayıp birarada çizdirelim:

sf = spektral_aki(x, pencere_uzunlugu, pencere_kaydirma_miktari)
hfc = yuksek_frekans_icerigi(x, pencere_uzunlugu, pencere_kaydirma_miktari)

# hfc'ye, içindeki değişimi ön-plana çıkarmak için opsiyonel olarak türev alma ve yarım-dalga doğrultama uygulanabilir 
hfc = np.diff(hfc)
hfc = (hfc + np.abs(hfc)) / 2

# Çizdirme adımları
f, axarr = plt.subplots(4, 1, figsize = (13, 6))
axarr[0].plot(t, x); axarr[0].set_title(dosya); axarr[0].axis('off')
axarr[0].vlines(baslangic_anlari, -1, 1, color='r')
axarr[1].plot(NRG_degisim,label = '(Log) Enerji değişimi)'); axarr[1].axis('off');axarr[1].legend(loc = 1)
axarr[2].plot(sf,label = 'Spektral akı)'); axarr[2].axis('off');axarr[2].legend(loc = 1)
axarr[3].plot(hfc,label = 'Yüksek frekans içeriği değişimi'); axarr[3].axis('off');axarr[3].legend(loc = 1)
<matplotlib.legend.Legend at 0x7fee982b2310>
../_images/10_RitmikAnaliz_25_1.png

Şekil 10.2: Ses kaydı, elle işaretlenmiş başlangıç anları (kırmızı dikey çizgiler) ve sinyalden hesaplanan yenilik/değişim fonksiyonları

Üç akustik parametrenin de benzer değişimler ve tepeler içerdiğini görebiliyoruz. Başlangıç noktası tespiti için üçünü birarada kullanan bir karar mekanizmasının tek parametre kullanan bir mekanizmaya göre daha iyi sonuç verip vermediğini test etmek isteyebilirsiniz. Bunu egzersiz olarak size bırakalım.

Elimizdeki kayıt örneği, enerjisi aniden değişen sinyal kategorisindeydi. Şimdi enerjisi aniden değişmeyen bir kaydı ele alalım. Bu tür bir örneği hazır veri kümesinde bulamadığımız için kendimiz hazırladık (kesit belirleme ve notaların işaretlenmesini elle yaptık ve altta diziler içerisine başlangıç anlarını yazdık)

dosya = 'neyTaksim.mp3'
link = 'https://archive.org/download/cd_ferahnak_bekir-ahin-balolu-and-nurullah-kank/disc1/04.%20Bekir%20%C5%9Eahin%20Balo%C4%9Flu%20And%20Nurullah%20Kan%C4%B1k%20-%20Ney%20taksim_sample.mp3'
urllib.request.urlretrieve(link, dosya);

# Ses sinyalinin dosyadan okunması ve genlik normalizayonu
x = ess.MonoLoader(filename = dosya, sampleRate = ornekleme_fr)()
baslangic_saniye = 14; bitis_saniye = baslangic_saniye + sure_saniye
x = x[baslangic_saniye * ornekleme_fr: bitis_saniye * ornekleme_fr] # sinyalin 5 saniyesini kullanacağız
x = x / np.max(np.abs(x))

# Yazar(bizler) tarafından elle işaretlenmiş başlangıç noktaları
baslangic_anlari = [0.838752, 1.038687, 1.145969, 2.725943, 3.413524, 4.340052]

Audio(x, rate=ornekleme_fr)