#author("2024-02-28T04:52:24+00:00","","") #author("2024-02-29T00:38:20+00:00","","") [[技術資料]] #Contents **【目的】 [#c242d311] メロディーのリアルタイム解析を通じて即興演奏や耳コピを支援を行う.高度な音楽的なスキルがなくても演奏ができるようにするために,わかりやすい指板に抑える場所をマークしてあるコード譜を生成し,出力を行う.これにより,ギターの初心者が簡単に練習に取り組めることを目指す. **【使用するファイル全部】 [#t4aaeb52] #ref(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') **【参考資料】 [#eda9093f] 音階度数(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 **【ギターの解説】 [#h6c9cf1d] ギターとは ギターは、弦を振動させることによって音を出す楽器です。 ヴァイオリンやチェロのように弦を擦って音を出すのではなく、弦を弾いて音を出すので、撥弦(はつげん)楽器に分類されます。 学問的にはリュート属に分類されます。リュートとは、棹(さお)と共鳴胴をもち、弦を弾くことによって音を出す楽器を指します。 演奏方法は、指板上のフレット(指板にある隆起)を指で押さえ、指やピックで弦を弾くというのが基本です。 #ref(ギター.png) ・ヘッド ギターの頭部で弦の端を保持する役割があります。ギターの種類やギターメーカーによって形・角度はさまざまです。 ・ペグ ヘッド部分に付いている部品で、弦を巻き取ることで弦にテンションをかけてチューニングを行います。 POINT テンション:弦の張り具合のこと。 チューニング:弦の音の高さを合わせること。 ・弦 音を鳴らすために欠かせないパーツです。弦はブリッジからペグまで渡っています。 種類は豊富で、素材とゲージ(太さ)によって音色や弾き心地が異なります。 ・ナット ヘッド付近で弦を支えるパーツです。弦高や音色を左右する重要なパーツでもあります。 ナットも種類が多くあります。ギターは本体のみならずカスタムパーツが豊富で、ハマれば沼ですね!! ・ネック ネックとは首のことで、ボディから長く伸びたパーツです。表面には指板が貼られていています。 弦のテンションに耐えうる硬い素材が使われています。 ネックは経年劣化や環境の変化などで反ってしまうことがあります。そうすると、弦のテンションや弦高が変わってしまい演奏に支障がでます。定期的にメンテナンスすることが大切ですね! ・指板(フィンガーボード) 押弦する際に触るパーツです。硬い素材が使われています。 アコギでは平たい形状のもの、エレキでは丸みを帯びている形状のものが主流です。 表面にはフレットと呼ばれる金属のパーツがついていて、指板はフレットを固定し安定させる役割があります。 ・フレット 指板に固定されている隆起しているパーツです。フレットの箇所を指で押さえると、目的の音を出すことができます。 ・ポジションマーク 指板に付けられているマークのことです。このマークのお陰でフレット数が数えやすくなります。 3、5、7、9、12・・・と決まったフレットに付けられています。 12フレットはちょうど開放弦の音のオクターブ上のため、他のマークとは区別されたものが付けられます。 最低限の知識をつけてね~↓参考にしてね↓ ギターってどんな楽器?https://koujun.ac/music-theory-basic-003/ **【音楽理論の定義】 [#f1eaeb78] ***コードの音符を取得する [#s5b22b80] 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特定のルート音のインスタンスを作成し、ルートから始まる半音スケールをキーとして、ルートからの距離 (半音単位) を値として持つ辞書を構築します。 ***半音ごとに定義 [#oc9830c0] 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 一般的なコードを定義する。 ***コードごとの定義 [#w748a38d] 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 クラスに保存されます。 ***フレットボード上で音符を見つける [#y36fdc2e] 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その逆を行い、音名の文字列を取得し、各弦で演奏できるすべての可能な位置を (弦番号、フレット番号) のタプルの形式で返します。 ***コードシェイプの取得 [#x3307a86] 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 つの条件に従って、再生不可能なシェイプをフィルタリングするメソッドを呼び出します。 ***オープン弦とミュート弦の識別 [#we416003] 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オブジェクトを作成してリストに追加します。 #ref(gaga.jpg) **【実装の流れ】 [#m51f4c2e] 1. ユーザーが演奏を行いたいコードの種類を選択しそれに合ったコードを生成できるよ うにする. 2. 入力された音声をリアルタイムで取得し FFT を用い,音高判定を行う. 3. 判定された音高に合わせ,ギターの指版に抑える場所を〇や×で示したコード譜を作 成して画像としてプロットする. 4. 作成されたコード譜にあったタブ譜の情報を保存し共有できるようにする. ***入力された音声の音高推定 [#g5835e23] 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平均律の音階に基づいて周波数に対応する音階を算出します。 #ref(音高.png) import matplotlib.pyplot as plt import pyaudio as pa import numpy as np import cv2 import csv from PIL import Image, ImageFont, ImageDraw from core.plot_chords import ChordShapePlot from core.chord_shapes import ChordDiagram, FingerPosition from core.chord_name import ChordNameGenerator # 定数の設定 RATE = 44100 BUFFER_SIZE = 16384 HEIGHT = 300 WIDTH = 400 SCALE = [ 'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#' ] # 音量の閾値を設定 VOLUME_THRESHOLD = 10000 # この値を調整して閾値を設定 # ストリームの準備 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) # ウィンドウを表示するための関数 def show_window(image): cv2.imshow('sample', image) cv2.waitKey(1) # ウィンドウを閉じるための関数 def close_window(): cv2.destroyAllWindows() # 判定された音階を格納するリスト HANTEI = [] # CSVファイルのヘッダー csv_header = ['raw_open_strings', 'shape', 'played_strings', 'open_strings', 'muted_strings'] # ファイルを追記モードで開く with open('chord_data.csv', 'a', newline='') as csvfile: writer = csv.writer(csvfile) # ヘッダーを書き込む(ファイルが空の場合のみ) if csvfile.tell() == 0: writer.writerow(csv_header) current_chord = None # 現在表示中の和音 plot_index = 0 # 画像の保存先ディレクトリパスを指定 save_directory = r'C:\Users\tarku\Desktop\chord_visualisation-main tyukanmade\chord_visualisation-main\src\chord_visualisation\figure' while True: try: # ストリームからデータを取得 audio_data = stream.read(BUFFER_SIZE) data = np.frombuffer(audio_data, dtype='int16') # 音量の振幅を計算 amplitude = np.max(np.abs(data)) # 音量が閾値を超えた場合にのみ音階判定を実行 if amplitude > VOLUME_THRESHOLD: 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 if val == 0: scale_num = 0 # ゼロに近い場合、デフォルトのスケールを設定 else: scale_num = int(np.log2((val / 440.0) ** 12) + offset) % len(SCALE) HANTEI.append(SCALE[scale_num]) # 描画準備 canvas = np.full((HEIGHT, WIDTH, 3), 255, np.uint8) # 日本語を描画するのは少し手間がかかる # 自身の環境に合わせてフォントへのpathを指定する font = ImageFont.truetype('C:\\Windows\\Fonts\\Arial.ttf', 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) # 判定結果を描画 show_window(canvas) data_buffer = np.append(data_buffer[BUFFER_SIZE:], data) # 五回までの音階を使用してChordShapePlotを初期化してプロット if len(HANTEI) >= 8: for i in range(len(HANTEI) - 8, len(HANTEI)): if current_chord is not None: plt.close() # 現在のウィンドウを閉じる current_chord = HANTEI[i] try: # 新しい和音ダイアグラムをプロット cp = ChordShapePlot(current_chord, 'major') # 例として 'minor' を使用 cp.plot_by_idx(25) # 新しい和音のデータを取得 raw_open_strings = cp.raw_open_strings shape = cp.shape played_strings = cp.played_strings open_strings = cp.open_strings muted_strings = cp.muted_strings # CSVファイルにデータを書き込む writer.writerow([raw_open_strings, shape, played_strings, open_strings, muted_strings]) # 画像の保存先パスを指定 save_path = f'{save_directory}\\chord_plot_{i}.png' # 新しい和音をプロットし、画像として保存 plt.pause(2) # 2秒待 plt.savefig(save_path) plt.close() except Exception as e: print(f"Error processing chord {current_chord}: {e}") except KeyboardInterrupt: # ctrl+c で終了 close_window() # ウィンドウを閉じる break # 後処理 stream.stop_stream() stream.close() audio.terminate() print("HANTEI:", HANTEI) 実際に実行するプログラム