技術資料

【目的】 

メロディーのリアルタイム解析を通じて即興演奏や耳コピを支援を行う.高度な音楽的なスキルがなくても演奏ができるようにするために,わかりやすい指板に抑える場所をマークしてあるコード譜を生成し,出力を行う.これにより,ギターの初心者が簡単に練習に取り組めることを目指す.

【使用するファイル全部】 

111.png

基本的な音楽知識と音楽理論,ギターの知識がないとプログラムの中身がわからないと思う...

#leaf_rotation=0がラベルの向き、orientation="top"がクラスタリングの図の方向
   #color_threshold=xでユークリッド平方距離がx以上を同色で表示
   #above_threshold_color="color"でユークリッド平方距離がx以上を"color"色に染める
   ##最新ラベルとラベルされているデータの距離が2000以内の場合同じ場所として判別#####
   dendrogram(Z, labels=df_label,leaf_rotation='vertical',leaf_font_size=16,color_threshold=2000,above_threshold_color='gray')
   #各データのクラスター番号出力
   group = fcluster(Z, 2000, criterion='distance')

【参考資料】 

音階度数(Degree):https://meloko-support.com/beginning/degree

周波数一覧: https://www.petitmonte.com/javascript/musical_scale_frequency.html

コード印象の違い:https://er-music.jp/theory/145/

マイナーコード構成:https://www.rakusta.jp/blog/code-minor

Amコード:https://guitar-concierge.com/chord/minor/a-minor/

リアルタイム判定:https://skimie.com/articles/6a3bfa82712f59cb6b5a6c10d7

広い音階:https://qiita.com/T1210Taichi/items/4daaeb9cec8765add0e4

【ギターの解説】 

ギターとは ギターは、弦を振動させることによって音を出す楽器です。 ヴァイオリンやチェロのように弦を擦って音を出すのではなく、弦を弾いて音を出すので、撥弦(はつげん)楽器に分類されます。 学問的にはリュート属に分類されます。リュートとは、棹(さお)と共鳴胴をもち、弦を弾くことによって音を出す楽器を指します。 演奏方法は、指板上のフレット(指板にある隆起)を指で押さえ、指やピックで弦を弾くというのが基本です。

ギター.png

・ヘッド ギターの頭部で弦の端を保持する役割があります。ギターの種類やギターメーカーによって形・角度はさまざまです。

・ペグ ヘッド部分に付いている部品で、弦を巻き取ることで弦にテンションをかけてチューニングを行います。

POINT テンション:弦の張り具合のこと。 チューニング:弦の音の高さを合わせること。

・弦 音を鳴らすために欠かせないパーツです。弦はブリッジからペグまで渡っています。 種類は豊富で、素材とゲージ(太さ)によって音色や弾き心地が異なります。

・ナット ヘッド付近で弦を支えるパーツです。弦高や音色を左右する重要なパーツでもあります。 ナットも種類が多くあります。ギターは本体のみならずカスタムパーツが豊富で、ハマれば沼ですね!!

・ネック ネックとは首のことで、ボディから長く伸びたパーツです。表面には指板が貼られていています。 弦のテンションに耐えうる硬い素材が使われています。 ネックは経年劣化や環境の変化などで反ってしまうことがあります。そうすると、弦のテンションや弦高が変わってしまい演奏に支障がでます。定期的にメンテナンスすることが大切ですね!

・指板(フィンガーボード) 押弦する際に触るパーツです。硬い素材が使われています。 アコギでは平たい形状のもの、エレキでは丸みを帯びている形状のものが主流です。 表面にはフレットと呼ばれる金属のパーツがついていて、指板はフレットを固定し安定させる役割があります。

・フレット 指板に固定されている隆起しているパーツです。フレットの箇所を指で押さえると、目的の音を出すことができます。

・ポジションマーク 指板に付けられているマークのことです。このマークのお陰でフレット数が数えやすくなります。 3、5、7、9、12・・・と決まったフレットに付けられています。 12フレットはちょうど開放弦の音のオクターブ上のため、他のマークとは区別されたものが付けられます。

最低限の知識をつけてね~↓参考にしてね↓

ギターってどんな楽器?https://koujun.ac/music-theory-basic-003/

【音楽理論の定義】 

コードの音符を取得する 

class ChromaticScale:
   notes = ('A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#')
   
   def __init__(self, root_note: str):
       self.root_note = root_note
   
   @property
   def scale(self) -> dict[str, int]:
       """Generate a scale for a specific key."""
              
       starting_idx = list(self.notes).index(self.root_note)
       notes: list[str] = []
       for idx in range(len(self.notes)):
           new_index = (starting_idx + idx) % len(self.notes)
           notes.append(self.notes[new_index])
       return dict(zip(notes, list(range(len(notes)))))

半音スケール、音程、さまざまな式を表すオブジェクトを追加する必要があります。このクラスは、ChromaticScale特定のルート音のインスタンスを作成し、ルートから始まる半音スケールをキーとして、ルートからの距離 (半音単位) を値として持つ辞書を構築します。

半音ごとに定義 

class Intervals(Enum):
   """Standard intervals in western music in number of half-steps"""
   P1 = 0  # perfect unison
   m2 = 3  # minor second
   M2 = 4  # major second
   m3 = 3  # minor third
   M3 = 4  # major third
   P4 = 5  # perfect fourth
   TT = 6  # tritone
   d5 = 6  # diminished fifth
   P5 = 7  # perfect fifth
   m6 = 8  # minor sixth
   M6 = 9  # major sixth
   A5 = 8   # augmented 5
   m7 = 10  # minor seventh
   M7 = 11  # major seventh
   P8 = 12  # octave
   M9 = 14  # Major ninth

一般的なコードを定義する。

コードごとの定義 

 class ChordFormula(Enum):

   major = Intervals.P1.value, Intervals.M3.value, Intervals.P5.value
   minor = Intervals.P1.value, Intervals.m3.value, Intervals.P5.value
   aug = Intervals.P1.value, Intervals.m3.value, Intervals.A5.value
   dim = Intervals.P1.value, Intervals.m3.value, Intervals.d5.value
   sus4 = Intervals.P1.value, Intervals.P4.value, Intervals.P5.value
   sus2 = Intervals.P1.value, Intervals.M2.value, Intervals.P5.value
   major7 = Intervals.P1.value, Intervals.M3.value, Intervals.P5.value, Intervals.M7.value
   dom7 = Intervals.P1.value, Intervals.M3.value, Intervals.P5.value, Intervals.m7.value
   minor7 = Intervals.P1.value, Intervals.m3.value, Intervals.P5.value, Intervals.m7.value
   minor7_flat5 = Intervals.P1.value, Intervals.m3.value, Intervals.d5.value, Intervals.m7.value
   dim7 = Intervals.P1.value, Intervals.m3.value, Intervals.d5.value, Intervals.M6.value
   major9 = Intervals.P1.value, Intervals.M3.value, Intervals.P5.value, Intervals.M7.value, Intervals.M9.value
   dom9 = Intervals.P1.value, Intervals.M3.value, Intervals.P5.value, Intervals.m7.value, Intervals.M9.value

さまざまな音程があり、その中には同じ数の半音差を表すものもありますが、異なる文脈で使用されます。したがって、物事を単純化するために、すべての間隔は、標準の略語と半音ステップ数を使用して Enum クラスに保存されます。

フレットボード上で音符を見つける 

from chromatic import ChromaticScale

class FretboardNotes:

   chromatic_scale = ChromaticScale.notes
   
   def __init__(
       self, 
       tuning: tuple[str, ...] = ('E', 'B', 'G', 'D', 'A', 'E'),
       max_frets: int = 13
       ) -> None:        
       self._tuning = tuning
       self._max_frets = max_frets
       self._populate_all_strings()
       
   def _populate_all_strings(self):
       string_notes: dict[str, list[str]] = {}
       for open_note in self._tuning:
           string_notes[open_note] = []
           for fret in range(self._max_frets + 1):
               string_notes[open_note].append(
                   self.chromatic_scale[(self.chromatic_scale.index(open_note) + fret) % 12])
       self.string_notes = string_notes
           
   def get_note(self, string: int, fret: int) -> str:
       open_note = self._tuning[string - 1]
       return self.chromatic_scale[(self.chromatic_scale.index(open_note) + fret) % 12]
   
   def get_fret_position_from_note(self, note: str):
       positions: list[tuple[int, int]] = []
       for string_no, open_note in enumerate(self.string_notes.keys()):
           frets = [idx for idx, i in enumerate(self.string_notes[open_note]) if i == note]
           positions.extend([(string_no + 1, f) for f in frets])
           if string_no == 0:
               positions.extend([(6, f) for f in frets])
       return positions

_populate_all_stringsこのクラスには、初期化中に実行されるメソッドがあります。パラメータまでのすべてのフレットのノートを計算しますmax_frets。ここでは、アコースティック ギターで楽器のほこりっぽい端でコードを演奏するのはあまり快適ではないため、値を 13 に設定しましたが、これはエレキ ギターにも拡張できます。

最初のパブリック メソッドは、get_note弦番号とフレット番号を受け取り、音名の文字列を返します。2 番目のメソッドはget_fret_position_from_noteその逆を行い、音名の文字列を取得し、各弦で演奏できるすべての可能な位置を (弦番号、フレット番号) のタプルの形式で返します。

コードシェイプの取得 

class ChordShapes:    
   def __init__(
       self, 
       tuning: tuple[str, ...] = ('E', 'B', 'G', 'D', 'A', 'E'),
       ) -> None:  
       self.tuning = tuning
       self.fretboard = FretboardNotes(tuning)  
   def parse_chord_string(self, chord_string: str) -> tuple[str, str]:
       if len(chord_string) == 1:
           return chord_string, "major"
       if chord_string[1] in ["#", "♭", "b"]:
           root_note = chord_string[:2]
           raw_quality = chord_string[2:]
       else:
           root_note = chord_string[0] 
           raw_quality = chord_string[1:] 
       quality = self._parse_quality_string(raw_quality)
       return root_note, quality
       
   def _parse_quality_string(self, raw_quality: str) -> str:
       if raw_quality == 'm':
           return "minor"
       elif raw_quality == 'm7':
           return 'minor7'
       else:
           return raw_quality
   
   def get_chord_notes(self, key: str, quality: str) -> list[str]:
       c = Chords(key, quality)
       return c.notes
   
   def get_chord_diagram(self, chord_string: str) -> list[ChordDiagram]:
       key, quality = self.parse_chord_string(chord_string)
       raw_shapes = self._get_shapes(key, quality)
       filtered_shapes = self._filter_shapes(raw_shapes)
       return self._check_open_strings(filtered_shapes, key, quality)     
       
   def _get_shapes(self, root: str, quality: str
                   ) -> list[tuple[tuple[int, int], ...]]:       
       # get chord notes translate flats to alternative names
       notes = [note_translator.get(n) or n 
                for n in self.get_chord_notes(root, quality)]
       chord_notes: dict[int, list[tuple[int, int]]] = {
           idx: self.fretboard.get_fret_position_from_note(n)
           for idx, n in enumerate(notes)
       }
       # now get all possible combinations
       return list(product(*chord_notes.values()))
   
   def _filter_shapes(self, raw_shapes: list[tuple[tuple[int, int], ...]]):        
       # filter the resulting combinations such that:
       #  - notes of a chord are no more than 3 frets apart
       #  - all notes have to be on separate strings
       unplayable_idx: list[int] = []
       fret_ranges = list(map(lambda x: 
           max(f[1] for f in x) - min(f[1] for f in x), raw_shapes))
       unplayable_idx = [idx for idx, i 
                         in enumerate(fret_ranges) if i >= 3]
       # identify combinations where different 
       # notes are played on the same string
       different_strings = [
           idx for idx, i in enumerate(raw_shapes) 
           if len({x[0] for x in i}) != len(i)
       ]
       unplayable_idx.extend(different_strings)
       comb_filtered = [i for idx, i in enumerate(raw_shapes) 
                        if idx not in unplayable_idx]
       # sort results in the order of strings
       comb_filtered = [sorted(i, key=lambda x: x[0]) 
                        for i in comb_filtered]
       return comb_filtered

多くの音符で 1 つの弦上に 2 つの異なる位置を設定できるという事実を考慮すると、可能な運指の組み合わせの総数は膨大になります。ただし、いくつかのルールを追加して、物理的に再生でき、音もおかしくないものだけを取得することができます。

音符の間隔は 4 フレットを超えてはいけません。そうしないと、コードを演奏するのが非常に難しくなるか、不可能になってしまいます。 一度に発音できる音は 1 つだけであるため、コードのすべての音は別の弦に配置する必要があります。

ここでの主なメソッドは、get_chord_diagram()コード名の文字列を入力として受け取り、オブジェクト"Am"のリストを返すものです。ChordDiagramまず、parse_chord_string入力文字列を解析し、ルート音とコードの質を識別するメソッドを呼び出します。

次に、メソッドを使用して生の運指形状を識別します_get_shapes。このメソッドは、考えられるすべての指の位置を計算し、各コードの音符の形式における 1 つの特定の運指の組み合わせであるタプルのリストを返します(string_number, fret)。

filter_shapes最後に、このセクションの冒頭に挙げた 2 つの条件に従って、再生不可能なシェイプをフィルタリングするメソッドを呼び出します。

オープン弦とミュート弦の識別 

def _check_open_strings(
       self, 
       raw_shapes: list[list[tuple[int, int]]],
       root: str,
       quality: str
       ) -> list[ChordDiagram]:
       s = MusicScale(root)   
       scale = s.scale(ScalePatterns.__getitem__(quality).value)
       print(f"{scale=}")
       
       res: list[ChordDiagram] = []
       
       for shape in raw_shapes:
           # get root note string
           root_string = self.fretboard.get_root_note_string_from_chord_shape(shape, root)
           
           played_strings: list[int] = [i[0] for i in shape]            
           raw_open_strings: list[int] = [i for i in range(1, 7) 
                                          if i not in played_strings]
										   
           # if any of the notes are played on 0th fret
           # remove those from played strings and add to
           # open strings.
           raw_open_strings.extend([i[0] for i in shape if i[1] == 0])
           played_strings = [i[0] for i in shape if i[1] != 0]
			
           # if the open string is lower register than the root note
           # then it will need to get muted
           muted_strings: list[int] = [i for i in raw_open_strings 
                                       if i > root_string]    
										
           # go through tuning and if a note is not depressed and
           # it is still in the correct scale then add it to open_strings
           open_strings = [idx+1 for idx, i in enumerate(self.tuning) 
                           if i in scale 
                           and idx+1 in raw_open_strings 
                           and idx+1 not in muted_strings]
           muted_strings = [i for i in raw_open_strings 
                            if i not in open_strings]
           res.append(ChordDiagram(
               shape=shape, 
               open_strings=open_strings, 
               muted_strings=muted_strings
               )
                      )
       return res

事前にフィルタリングされたすべてのコード形状を確認し、それぞれについて、まず の形式の各タプルの最初の要素をチェックすることで、フレットのある文字列を特定します(string_no, fret_no)。次に、フレットのない弦をチェックするだけで、開放弦の初期値を特定します。

場合によっては、コード スキーマに 0 フレットでフレットを付ける弦が含まれることがありますが、これは開放弦を演奏するのと同じであるため、これらの弦を開放弦のリストに追加し、フレット付き弦のリストから削除します。

次に、上で説明したように、提案された開放弦がルート音より低い音域である場合、ミュートするか演奏しない必要があるため、それらの弦をmuted_stringsリストに追加します。

次に、生成された音が正しいスケールでミュートされていないことを確認して、元の開放弦リストをフィルターします。

最後に、ChordDiagramオブジェクトを作成してリストに追加します。

gaga.jpg

【実装の流れ】 

1. ユーザーが演奏を行いたいコードの種類を選択しそれに合ったコードを生成できるよ うにする.

2. 入力された音声をリアルタイムで取得し FFT を用い,音高判定を行う.

3. 判定された音高に合わせ,ギターの指版に抑える場所を〇や×で示したコード譜を作 成して画像としてプロットする.

4. 作成されたコード譜にあったタブ譜の情報を保存し共有できるようにする.

入力された音声の音高推定 

import matplotlib.pyplot as plt
import pyaudio as pa
import numpy as np
import cv2
from PIL import Image, ImageFont, ImageDraw
RATE=44100
BUFFER_SIZE=16384
HEIGHT=300
WIDTH=400
SCALE=[
	'ラ', 'ラ#', 'シ', 'ド', 'ド#', 'レ',
	'レ#', 'ミ', 'ファ', 'ファ#','ソ', 'ソ#'
]
## ストリーム準備
audio = pa.PyAudio()
stream = audio.open( rate=RATE,
		channels=1,
		format=pa.paInt16,
		input=True,
		frames_per_buffer=BUFFER_SIZE)
## 波形プロット用のバッファ				
data_buffer = np.zeros(BUFFER_SIZE*16, int)		
## 二つのプロットを並べて描画
fig  = plt.figure()
fft_fig = fig.add_subplot(2,1,1)		
wave_fig = fig.add_subplot(2,1,2)
while True:
	try:
		## ストリームからデータを取得
		audio_data=stream.read(BUFFER_SIZE)
		data=np.frombuffer(audio_data,dtype='int16')
		fd = np.fft.fft(data)
		fft_data = np.abs(fd[:BUFFER_SIZE//2])
		freq=np.fft.fftfreq(BUFFER_SIZE, d=1/RATE)
		## スペクトルで最大の成分を取得
		val=freq[np.argmax(fft_data)]
		offset = 0.5 if val >= 440 else -0.5
		scale_num=int(np.log2((val/440.0)**12)+offset)%len(SCALE)
		
		## 描画準備
		canvas = np.full((HEIGHT,WIDTH,3),255,np.uint8)
		
		## 日本語を描画するのは少し手間がかかる
		### 自身の環境に合わせてフォントへのpathを指定する
		font = ImageFont.truetype(
			'/System/Library/Fonts/ヒラギノ角ゴシック W5.ttc',
			120)
		canvas = Image.fromarray(canvas)
		draw = ImageDraw.Draw(canvas)
		draw.text((20, 100),
			SCALE[scale_num],
			font=font,
			fill=(0, 0, 0, 0))
		canvas = np.array(canvas)
		## 判定結果を描画
		cv2.imshow('sample',canvas)
		## プロット
		data_buffer = np.append(data_buffer[BUFFER_SIZE:],data)
		wave_fig.plot(data_buffer)
		fft_fig.plot(freq[:BUFFER_SIZE//20],fft_data[:BUFFER_SIZE//20])
		wave_fig.set_ylim(-10000,10000)
		plt.pause(0.0001)
		fft_fig.cla()
		wave_fig.cla()
		
	except KeyboardInterrupt: ## ctrl+c で終了
		break
## 後始末

stream.stop_stream() stream.close() audio.terminate()

FFT (Fast Fourier Transform) を用いた周波数解析:

オーディオデータをFFTを使って周波数領域に変換します。 FFTを実行することで、波形データを周波数スペクトルに変換し、それぞれの周波数成分の振幅を取得します。 周波数スペクトルの解析:

FFTを実行することで得られた周波数スペクトルから、特定の周波数成分を抽出します。 最大振幅を持つ周波数成分を見つけ、その周波数を基に音高を推定します。 周波数から音階の算出:

抽出された最大振幅の周波数を基に、音階を推定します。 音階は周波数に対して対応するように定義されており、440 HzのA(ラ)を基準にして、12平均律の音階に基づいて周波数に対応する音階を算出します。

音高.png

トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS