半年以上からMIDIコントローラーを買ったりしていたものの何もしないまま歳を越してしまったわけだが、とうとう軽く触ってみることを始めた。
とりあえず『ゼルダの伝説 時のオカリナ』の"時の歌"を打ち込んでlofiっぽい感じにして遊んでみた。
ゼル伝『時の歌』lofiっぽいやつ その2 pic.twitter.com/utqUrLJRfm
— nao_y (@NaoY_py) 2024年2月16日
MIDIはファイルとして管理されているのだからPythonで記述できるのでは、と思い探してみるとmidoというライブラリが見つかった。
midoの基礎
midを使うと簡単にMIDIファイルを作成できる。以下のコード例は、ドの音の四分音符を一つ鳴らすだけのシンプルすぎる例。
import mido from mido import Message, MidiFile, MidiTrack, MetaMessage mid = MidiFile() track = MidiTrack() mid.tracks.append(track) # BPM120のMIDIトラックを作成 track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(120))) # BPMを120に設定 # C3(ド)の音の四分音符をMIDIトラックに追加 (C3はいわゆる真ん中の高さのド) track.append(Message('note_on', note=60, velocity=64, time=0)) track.append(Message('note_off', note=60, time=480)) # MIDIファイルを保存 mid.save("new_song.mid")
C3が60で表されることがわかれば、他の音もわかる。
60: "C", 61: "C#", 62: "D", 63: "D#", 64: "E", 65: "F", 66: "F#", 67: "G", 68: "G#", 69: "A", 70: "A#", 71: "B",
オクターブを変えたいときはそれぞれの値に±12をすればよい。
自由研究
俺はメロディを思いつくことがほぼできないので、自動化しようと思い立った。
初めはなんらかのAPIで取れる連続的なレスポンスをどうにかしてメロディに変換しようと思ったが、天気系のAPIの使い方を調べているうちに面倒になり、まずはシンプルにテキストの長さを音の並びに変換しようという考えになった。
というわけで、手始めにWebエンジニアの多くが見たことがあるであろう"Lorum ipsum ..."を題材とした。
Lorum ipsum ... is 何
Webデザインなどにおいて、文章を入れる箇所に仮置きするダミーテキスト。
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
処理の流れ
- テキストをスペースで区切って、単語に分ける。カンマやピリオドは削除しておく
- 単語の長さの最小値、最大値を取得する
- 最小値<=n<=最大値の範囲の数列を、スケールの構成音の数で分割する
- それぞれの単語の長さをスケールの構成音に変換する
- MIDIトラックに音を追加していく
- MIDIファイルに保存する
Cメジャースケールでの例
Lorem ipsum dolor sit amet, . . .
↓
["Lorem", "ipsum", "dolor", "sit", "amet"]
メジャースケールの構成音は「1, 3, 5, 6, 8, 10, 12, 13」なので、分割数は8となる。
ダミーテキストの単語の最小値は2, 最大値は17なので、この範囲を8分割する。
[array([2, 3]), array([4, 5]), array([6, 7]), array([8, 9]), array([10, 11]), array([12, 13]), array([14, 15]), array([16, 17])]
各単語の長さを、この配列に当てはめて音を決定する。
[5, 5, 5, 3, 4, . . .]
↓
[1, 1, 1, 0, 1, ...]
となる。
キーがC(ド)の場合「レ, レ, レ, ド, レ」となる。
各音をMIDIトラックに追加、ファイルを保存して完了となる。
音の長さと強さ
音の長さは4分音符、2分音符、全音符をランダムに割り振っている。
音の強さ(ベロシティ)も64を基準に±10の範囲でランダムに設定している。
スケールの構成音の取得
スケール名と構成音の一覧を記載しているWebサイトをスクレイピングして一覧用のファイルを作成した。
スクリプト実行時にスケール名のIDを入力して、使用するスケールを決定する。
琉球音階を使った楽曲の例
Web系の人の多くが見たことある"Lorem ipsum ..."から生成した琉球音階メロディに空間系エフェクトを掛けて琉球ゲイズみたいなものを作ってみた。
— nao_y (@NaoY_py) 2024年2月20日
コードは雑に手打ち。ドラム音源はSpliceから。 pic.twitter.com/8rE5ZU7458
今後の展望
- 青空文庫にある文学作品からメロディを生成する
- スケールの構成音からコードを生成する
付録
scraping.py
import csv from pathlib import Path import re from bs4 import BeautifulSoup as bs import requests URL = "https://scale-player.vercel.app/scale_type" PAT = re.compile(r"([\d]){1,2}") res = requests.get(URL) soup = bs(res.content, "html.parser") h3_tags = soup.find_all("h3") scales = [] for h3 in h3_tags: try: scales.append(h3["id"]) except KeyError: pass with Path("scales.csv").open("w") as f: fieldnames = ("idx", "scaleName", "notes") writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() idx = 1 for scale in scales: h3 = soup.find("h3", id=scale) parent = h3.parent dl = parent.css.select("dl > dd") for elem in dl: detail = elem.find("p").get_text("\n").split("\n")[0] notes = detail.replace("構成音:", "") scale_name = elem.find("img")["alt"] writer.writerow({"idx": f"{idx:02d}", "scaleName": scale_name, "notes": notes}) idx += 1
main.py
from collections import defaultdict import csv import numpy as np from pathlib import Path import random import mido from mido import Message, MidiFile, MidiTrack, MetaMessage PATH = Path("scales.csv") KEY_MESSAGE = ( "- C3 : 60\n" "- C#3: 61\n" "- D3 : 62\n" "- D#3: 63\n" "- E3 : 64\n" "- F3 : 65\n" "- F#3: 66\n" "- G3 : 67\n" "- G#3: 68\n" "- A3 : 69\n" "- A#3: 70\n" "- B3 : 71\n" ) KEY_DICT = { 60: "C", 61: "C#", 62: "D", 63: "D#", 64: "E", 65: "F", 66: "F#", 67: "G", 68: "G#", 69: "A", 70: "A#", 71: "B", } PITCH_RANGE = 12 SAMPLE_SENTENCE = ( "Lorem ipsum dolor sit amet, " "consectetur adipiscing elit, " "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." ) def load_scales(): scales = defaultdict() with PATH.open("r") as f: fieldnames = ("idx", "scaleName", "notes") reader = csv.DictReader(f, fieldnames=fieldnames) next(reader) for row in reader: idx = row["idx"] scales[idx] = row["notes"].split(",") return scales def fetch_sentence_lengths(): sentences = SAMPLE_SENTENCE.split(" ") sentences = [sentence.replace(",", "").replace(".", "") for sentence in sentences] lengths = [len(sentence) for sentence in sentences] return lengths def length_to_scale_notes(lengths, scales): print(lengths) scale_length = len(scales) split_base = np.array_split( list(range(min(lengths), max(lengths) + 1)), scale_length ) print(split_base) scale_positions = [] for length in lengths: for idx, base in enumerate(split_base): if base[0] <= length <= base[-1]: scale_positions.append(idx) scale_notes = [int(scales[idx])-1 for idx in scale_positions] return scale_notes random.seed() scales_map = load_scales() bpm = int(input("BPM: ")) scale_id = input("Scale Id: ") scales = scales_map[scale_id] print(KEY_MESSAGE) key = int(input("Key: ")) key_name = KEY_DICT[key] pitch = input("Pitch(+/-): ") if not pitch: pitch = 0 pitch_shift = int(pitch) key += (pitch_shift * PITCH_RANGE) lengths = fetch_sentence_lengths() scale_notes = length_to_scale_notes(lengths, scales) print(scale_notes) mid = MidiFile() track = MidiTrack() mid.tracks.append(track) track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(bpm))) for scale_note in scale_notes: rand_time = 480 * random.choice([1, 2, 4]) rand_velocity = 64 + random.choice(list(range(-10, 11))) track.append(Message('note_on', note=key+scale_note, velocity=rand_velocity, time=0)) track.append(Message('note_off', note=key+scale_note, time=rand_time)) filename = f"{key_name}_{bpm}_scaleId{scale_id}.mid" mid.save(filename)