├── export └── .keep ├── favicon.ico ├── .gitignore ├── src ├── assets.pyxres ├── patterns.json ├── tones.json ├── bdf.py ├── generator.json ├── sounds.py ├── rhythm.json └── generator.py ├── 8bit-bgm-gen.pyxapp ├── play.py └── readme.md /export/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiromofufactory/8bit-bgm-generator/HEAD/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .git 3 | dev 4 | public 5 | export/* 6 | *.sh 7 | firebase.json 8 | .firebaserc -------------------------------------------------------------------------------- /src/assets.pyxres: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiromofufactory/8bit-bgm-generator/HEAD/src/assets.pyxres -------------------------------------------------------------------------------- /8bit-bgm-gen.pyxapp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiromofufactory/8bit-bgm-generator/HEAD/8bit-bgm-gen.pyxapp -------------------------------------------------------------------------------- /play.py: -------------------------------------------------------------------------------- 1 | import pyxel 2 | import json 3 | 4 | MUSIC_FILE = "export/music" 5 | 6 | 7 | class App: 8 | def __init__(self): 9 | pyxel.init(160, 120, title="8bit bgm player") 10 | with open(f"./{MUSIC_FILE}.json", "rt") as fin: 11 | self.music = json.loads(fin.read()) 12 | pyxel.run(self.update, self.draw) 13 | 14 | def update(self): 15 | if pyxel.btnp(pyxel.KEY_SPACE): 16 | if pyxel.play_pos(0) is None: 17 | for ch, sound in enumerate(self.music): 18 | pyxel.sound(ch).set(*sound) 19 | pyxel.play(ch, ch, loop=True) 20 | else: 21 | pyxel.stop() 22 | if pyxel.btnp(pyxel.KEY_ESCAPE): 23 | pyxel.quit() 24 | 25 | def draw(self): 26 | pyxel.cls(0) 27 | pyxel.text(20, 52, "Press [SPACE] to play / stop.", 7) 28 | pyxel.text(20, 64, "Press [ESC] to exit.", 7) 29 | 30 | 31 | App() 32 | -------------------------------------------------------------------------------- /src/patterns.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": ":1", 4 | "abbr": "BD", 5 | "name": "Bass Drum", 6 | "wave": "N", 7 | "notes": [36, 24], 8 | "decay": 8, 9 | "sustain": 0, 10 | "velocity": 100 11 | }, 12 | { 13 | "key": ":2", 14 | "abbr": "SD", 15 | "name": "Snare Drum", 16 | "wave": "N", 17 | "notes": [46], 18 | "decay": 16, 19 | "sustain": 0, 20 | "velocity": 100 21 | }, 22 | { 23 | "key": ":3", 24 | "abbr": "HH", 25 | "name": "HiHat", 26 | "wave": "N", 27 | "notes": [58], 28 | "decay": 10, 29 | "sustain": 0, 30 | "velocity": 30 31 | }, 32 | { 33 | "key": ":5", 34 | "abbr": "LT", 35 | "name": "Low Tam", 36 | "wave": "T", 37 | "notes": [21, 19, 18, 17, 16, 15, 14, 13, 12], 38 | "sustain": 0, 39 | "decay": 16, 40 | "velocity": 100 41 | }, 42 | { 43 | "key": ":6", 44 | "abbr": "MT", 45 | "name": "Middle Tam", 46 | "wave": "T", 47 | "notes": [27, 25, 24, 23, 22, 21, 20, 19, 18], 48 | "sustain": 0, 49 | "decay": 16, 50 | "velocity": 100 51 | }, 52 | { 53 | "key": ":7", 54 | "abbr": "HT", 55 | "name": "Hi Tam", 56 | "wave": "T", 57 | "notes": [33, 31, 30, 29, 28, 27, 26, 25, 24], 58 | "sustain": 0, 59 | "decay": 16, 60 | "velocity": 100 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /src/tones.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "pure triangle", 4 | "wave": "T", 5 | "attack": 0, 6 | "decay": 0, 7 | "sustain": 100, 8 | "release": 0, 9 | "vibrato": 0 10 | }, 11 | { 12 | "name": "normal lead", 13 | "wave": "P", 14 | "attack": 0, 15 | "decay": 30, 16 | "sustain": 50, 17 | "release": 10, 18 | "vibrato": 60 19 | }, 20 | { 21 | "name": "soft lead", 22 | "wave": "P", 23 | "attack": 20, 24 | "decay": 20, 25 | "sustain": 70, 26 | "release": 10, 27 | "vibrato": 60 28 | }, 29 | { 30 | "name": "strings", 31 | "wave": "P", 32 | "attack": 40, 33 | "decay": 0, 34 | "sustain": 100, 35 | "release": 20, 36 | "vibrato": 90 37 | }, 38 | { 39 | "name": "flute", 40 | "wave": "S", 41 | "attack": 15, 42 | "decay": 60, 43 | "sustain": 50, 44 | "release": 10, 45 | "vibrato": 90 46 | }, 47 | { 48 | "name": "e_piano", 49 | "wave": "S", 50 | "attack": 0, 51 | "decay": 30, 52 | "sustain": 30, 53 | "release": 10, 54 | "vibrato": 0 55 | }, 56 | { 57 | "name": "harp", 58 | "wave": "S", 59 | "attack": 0, 60 | "decay": 15, 61 | "sustain": 10, 62 | "release": 20, 63 | "vibrato": 0 64 | }, 65 | { 66 | "name": "base", 67 | "wave": "T", 68 | "attack": 0, 69 | "decay": 0, 70 | "sustain": 100, 71 | "release": 0, 72 | "vibrato": 60 73 | }, 74 | { 75 | "name": "harpsicord", 76 | "wave": "P", 77 | "attack": 0, 78 | "decay": 40, 79 | "sustain": 20, 80 | "release": 10, 81 | "vibrato": 0 82 | }, 83 | { 84 | "name": "ocarina", 85 | "wave": "T", 86 | "attack": 15, 87 | "decay": 60, 88 | "sustain": 60, 89 | "release": 10, 90 | "vibrato": 0 91 | }, 92 | { 93 | "name": "square lead", 94 | "wave": "S", 95 | "attack": 0, 96 | "decay": 60, 97 | "sustain": 80, 98 | "release": 10, 99 | "vibrato": 0 100 | }, 101 | { 102 | "name": "thick lead", 103 | "wave": "P", 104 | "attack": 0, 105 | "decay": 60, 106 | "sustain": 80, 107 | "release": 10, 108 | "vibrato": 0 109 | }, 110 | { 111 | "name": "no data", 112 | "wave": "T", 113 | "attack": 0, 114 | "decay": 0, 115 | "sustain": 0, 116 | "release": 0, 117 | "vibrato": 0 118 | }, 119 | { 120 | "name": "no data", 121 | "wave": "T", 122 | "attack": 0, 123 | "decay": 0, 124 | "sustain": 0, 125 | "release": 0, 126 | "vibrato": 0 127 | }, 128 | { 129 | "name": "no data", 130 | "wave": "T", 131 | "attack": 0, 132 | "decay": 0, 133 | "sustain": 0, 134 | "release": 0, 135 | "vibrato": 0 136 | }, 137 | { 138 | "name": "noise drums", 139 | "wave": "N", 140 | "attack": 0, 141 | "decay": 12, 142 | "sustain": 0, 143 | "release": 0, 144 | "vibrato": 0 145 | } 146 | ] 147 | -------------------------------------------------------------------------------- /src/bdf.py: -------------------------------------------------------------------------------- 1 | import pyxel as px 2 | 3 | 4 | # 日本語フォント表示 5 | class BDFRenderer: 6 | BORDER_DIRECTIONS = [ 7 | (-1, -1), 8 | (0, -1), 9 | (1, -1), 10 | (-1, 0), 11 | (1, 0), 12 | (-1, 1), 13 | (0, 1), 14 | (1, 1), 15 | ] 16 | 17 | def __init__(self, bdf_filename): 18 | self.fontboundingbox = [0, 0, 0, 0] 19 | self.fonts = self._parse_bdf(bdf_filename) 20 | self.screen_ptr = px.screen.data_ptr() 21 | self.screen_width = px.width 22 | 23 | def _parse_bdf(self, bdf_filename): 24 | fonts = {} 25 | code = None 26 | bitmap = None 27 | dwidth = 0 28 | with open(bdf_filename, "r") as f: 29 | for line in f: 30 | if line.startswith("ENCODING"): 31 | code = int(line.split()[1]) 32 | elif line.startswith("DWIDTH"): 33 | dwidth = int(line.split()[1]) 34 | elif line.startswith("BBX"): 35 | bbx_data = list(map(int, line.split()[1:])) 36 | font_width, font_height, offset_x, offset_y = ( 37 | bbx_data[0], 38 | bbx_data[1], 39 | bbx_data[2], 40 | bbx_data[3], 41 | ) 42 | elif line.startswith("BITMAP"): 43 | bitmap = [] 44 | elif line.startswith("ENDCHAR"): 45 | fonts[code] = ( 46 | dwidth, 47 | font_width, 48 | font_height, 49 | offset_x, 50 | offset_y, 51 | bitmap, 52 | ) 53 | bitmap = None 54 | elif line.startswith("FONTBOUNDINGBOX"): 55 | # 0:width 1:height 2:offset_x 3:offset_y 56 | self.fontboundingbox = list(map(int, line.split()[1:])) 57 | elif bitmap is not None: 58 | hex_string = line.strip() 59 | bin_string = bin(int(hex_string, 16))[2:].zfill(len(hex_string) * 4) 60 | bitmap.append(int(bin_string[::-1], 2)) 61 | return fonts 62 | 63 | def _draw_font(self, x, y, font, color): 64 | dwidth, font_width, font_height, offset_x, offset_y, bitmap = font 65 | screen_ptr = self.screen_ptr 66 | screen_width = self.screen_width 67 | x = x + self.fontboundingbox[2] + offset_x 68 | y = ( 69 | y 70 | + self.fontboundingbox[1] 71 | + self.fontboundingbox[3] 72 | - font_height 73 | - offset_y 74 | ) 75 | for j in range(font_height): 76 | for i in range(font_width): 77 | if (bitmap[j] >> i) & 1: 78 | screen_ptr[(y + j) * screen_width + x + i] = color 79 | 80 | def text(self, x, y, text, color=7, border_color=None, spacing=0): 81 | for char in text: 82 | code = ord(char) 83 | if code not in self.fonts: 84 | continue 85 | font = self.fonts[code] 86 | if border_color is not None: 87 | for dx, dy in self.BORDER_DIRECTIONS: 88 | self._draw_font( 89 | x + dx, 90 | y + dy, 91 | font, 92 | border_color, 93 | ) 94 | self._draw_font(x, y, font, color) 95 | x += font[0] + spacing 96 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 8bit BGM generator 2 | 3 | 8bit BGM generator は、「作曲スキルがないけど、Pyxel の自作ゲームに BGM を入れたい」という方のための BGM 自動生成ツールです。 4 | 5 | Pyxel をお持ちの方でなくても、ブラウザ版で体験いただくことができます。また、生成した曲を mp3 などの音声ファイルとして録音したり、MIDI ファイルとして出力した上で加工することで、Pyxel 以外のレトロゲーム開発や動画の BGM 等に使っていただくことも可能です(詳しくは後述)。 6 | 7 | 問題点を発見したり、要望がある方は、Twitter [frenchbread](https://twitter.com/frenchbread1222) (@frenchbread1222) まで DM 等でお知らせください。 8 | 9 | ## 自動生成の仕様 10 | 11 | 8 小節、3 または 4 パートの曲を生成します。パート編成は以下から選べます。 12 | 13 | - メロディ+リバーブ(ディレイ)+ベース 14 | - メロディ+ベース+ドラム 15 | - メロディ+サブメロディ+ベース 16 | - メロディ+サブメロディ+ベース+ドラム 17 | 18 | 3 チャンネル編成で生成した曲を Pyxel で取り込んだ場合、残りの 1 チャンネルを効果音用に自由に使えます。(4 チャンネル編成にする場合も、効果音の再生時に一時的に曲を止めることによって BGM と効果音を両立できます。) 19 | 20 | テンポ、コード進行、ベースとドラムのリズムなどは登録されているパターンから指定します。メロディは自動生成ですが、パラメータによりある程度調整することもできます。 21 | 22 | よくわからないという方は、とりあえず「きほん」タブの「プリセット」を適当に選択してみてください。その後でテンポを変えたり、リズムを差し替えることで、好みの曲調に近づけることができると思います。 23 | 24 | ## ツールの使い方 25 | 26 | - ブラウザ上で動かす場合: https://retro-bgm-generator.web.app/ (スマートフォンでも動きますが、画面が小さく操作しづらいのと、作成した曲をエクスポートするのが困難なので、お試し用途以外は PC からアクセスください。) 27 | - ローカル環境で動かす場合: このリポジトリをダウンロードして、`pyxel play 8bit-bgm-gen` を実行してください。また、MIDI ファイルの出力をしたい場合、事前に `pip install mido` コマンドを実行して mido をインストールしてください。 28 | 29 | ## 自動生成した曲を使うには? 30 | 31 | ### ①Pyxel で使う場合 32 | 33 | Pyxel のサウンド仕様に沿ったテキストデータが json ファイルとして出力されるので、これをゲーム本体のスクリプトで読み込んで再生する形になります。(リポジトリ内の play.py 参照。) 34 | 35 | ### ②Pyxel 以外で使いたい場合 36 | 37 | 以下の 2 つの方法があります。 38 | 39 | - ver 1.30でwavファイルの出力に対応しましたので、このファイルを再生用にお使いください。 40 | - ローカル実行時に限りMIDIファイルを出力できますので(要mido)、この MIDI ファイルを直接プログラムから再生したり、お使いの DAW などで オーディオ形式のファイル(mp3, wav etc)を生成することで、多様な環境でお使いいただけます。ただし、8bit BGM generator で設定した音色は MIDI ファイルには反映されませんので、お使いの環境にあわせて音源を設定したり、音域をオクターブ単位で調整したりすることを推奨します。 41 | 42 | ## チュートリアル動画 43 | 44 | [こちらをご覧ください(Youtube が開きます)](https://youtu.be/aacS2atOeQ4) 45 | 46 | ※この動画は ver1.00 時点のものなので現在のバージョンと少し仕様が異なっています。ご了承ください。 47 | 48 | ## カスタマイズ方法 49 | 50 | generator.json をカスタマイズすることで、登録されているコード進行・リズム(ベース・ドラム)以外のパターンの曲を生成することもできます。 51 | 52 | ローカルダウンロードの上、自己責任でご使用ください。(形式が正しくない場合、正常動作しない可能性が高いです。) 53 | 54 | ### コード進行 55 | 56 | ``` 57 | "chords": [ 58 | { 59 | "description": "Ⅰ - Ⅶ♭", 60 | "progression": [ 61 | { "loc": 0, "notes": "209019030909" }, 62 | { "loc": 16, "notes": "901093090920" }, 63 | { "loc": 32, "notes": "209019030909" }, 64 | { "loc": 48, "notes": "901093090920" }, 65 | { "loc": 64, "repeat": 0 }, 66 | { "loc": 80, "repeat": 1 }, 67 | : 68 | ``` 69 | 70 | - "description" は画面上に表示する説明です。曲には影響しません。 71 | - "progression.loc" は曲のロケーションを指します。16 ごとに 1 小節ですので、1 小節ごとにコードを変える場合は、上の例のように 0, 16... と指定します。 72 | - "progression.notes" はコードやメロディの構成音を示します。12 個の数字は、C, C#, D .. に対応します。 73 | - "0" は使わない音です。 74 | - "1" は「コードを構成するベース以外の必須音」です。"1"の音はコード内で必ず使います。 75 | - "2" はベース音です。メロディでも使いますが、必須にはなりません。 76 | - "3" は「コードを構成するが省略可能な音」です。この音はメロディに登場しない場合もあります。 77 | - "9" はコード構成音以外で、メロディに使える音です。 78 | - たとえば C メジャースケジュールのコード C の場合、C が"2"、E が"1"、G が"3"、DFAB が"9"となり、"notes"として指定する文字列は"209019030909"です。 79 | - 上の例において、G を"1"と指定することも可能ですが、特にコードの長さが少ない場合、"1" の数が多いと曲の生成に時間がかかったり、フリーズする場合があります。"1" の数は 2 つまで、1/2 小節ごとにコードを切り替える場合は 1 つのみとすることを推奨します。 80 | - "progression.repeat" は、progression 要素の n 番目(1 つ目は n=0)と同じコード・メロディを繰り返す指定です。"repeat"を指定する場合、"notes"は指定しないでください。 81 | 82 | ### ベースパターン 83 | 84 | ``` 85 | "base": [ 86 | : 87 | { 88 | "basic": "0.2.4.2.0.2.4.2.", 89 | "final": "0.2.4.2.2.4.2.4." 90 | }, 91 | : 92 | ``` 93 | 94 | - "final"は 8 小節目、"basic"はそれ以外の小節に適用するパターンで、フォーマットは同じです。 95 | - 1 つの文字が 16 分音符に相当しますので、文字列の長さは必ず 16 文字にしてください。 96 | - "0"は休符です。 97 | - "1"は 5 度の音、"2"はルート音、"3"は"1"より 1 オクターブ高い 5 度の音、"4"は"2"より 1 オクターブ高いルート音を示します。 98 | - "."で前の音を伸ばします。たとえば "4.4." や "2.2." であれば、8 分音符 x 2 を示します。"00"と"0."は同じ結果になります。 99 | 100 | ### ドラムパターン 101 | 102 | ``` 103 | "drums": [ 104 | : 105 | { 106 | "basic": "1033203310332033", 107 | "final": "1033203310332220" 108 | } 109 | ], 110 | ``` 111 | 112 | - "final"は 4, 8 小節目、"basic"はそれ以外の小節に適用するパターンで、フォーマットは同じです。 113 | - 1 つの文字が 16 分音符に相当しますので、文字列の長さは必ず 16 文字にしてください。 114 | - "0"は休符です。 115 | - "1"はバスドラム、"2"はスネアドラム、"3"はハイハット、"5"はロータム、"6"はミドルタム、"7"はハイタムです。(patterns.json の"key"に対応しますので、音色を追加・変更することも可能です。) 116 | 117 | ## 更新履歴 118 | 119 | 2025/01/28 ver1.30 wavファイル出力に対応 120 | 121 | 2025/01/24 ver1.24 Windows/Web版におけるjson出力時の不具合改善、typo修正 122 | 123 | 2023/10/14 Ver1.21 mido インストール時にエラーとなる問題を修正 124 | 125 | 2023/10/10 Ver1.20 MIDI 出力機能をサポート 126 | 127 | 2023/09/23 Ver1.10 メロディ生成アルゴリズム改善、サブメロディ生成に対応 128 | 129 | 2023/09/09 Ver1.02 ブラウザ版のエクスポートに対応 130 | 131 | 2023/06/01 Ver1.01 Windows でローカル実行時にファイル保存できないバグを修正 132 | 133 | 2023/05/08 Ver1.00 初版リリース 134 | -------------------------------------------------------------------------------- /src/generator.json: -------------------------------------------------------------------------------- 1 | { 2 | "chords": [ 3 | { 4 | "description": "Ⅰ - Ⅶ♭", 5 | "progression": [ 6 | { "loc": 0, "notes": "209019030909" }, 7 | { "loc": 16, "notes": "901093090920" }, 8 | { "loc": 32, "notes": "209019030909" }, 9 | { "loc": 48, "notes": "901093090920" }, 10 | { "loc": 64, "repeat": 0 }, 11 | { "loc": 80, "repeat": 1 }, 12 | { "loc": 96, "notes": "209019030909" }, 13 | { "loc": 112, "notes": "901093090920" }, 14 | { "loc": 120, "notes": "901099010902" } 15 | ] 16 | }, 17 | { 18 | "description": "Ⅳ - Ⅴ/Ⅳ", 19 | "progression": [ 20 | { "loc": 0, "notes": "309092090109" }, 21 | { "loc": 16, "notes": "903092010901" }, 22 | { "loc": 32, "notes": "309092090109" }, 23 | { "loc": 48, "notes": "903092010901" }, 24 | { "loc": 64, "notes": "909209030930" }, 25 | { "loc": 80, "notes": "309201090190" }, 26 | { "loc": 96, "notes": "909209030930" }, 27 | { "loc": 112, "notes": "309201090190" } 28 | ] 29 | }, 30 | { 31 | "description": "Ⅰ - Ⅴ - Ⅵ♭ - Ⅶ♭", 32 | "progression": [ 33 | { "loc": 0, "notes": "209019030909" }, 34 | { "loc": 16, "notes": "903099020901" }, 35 | { "loc": 32, "notes": "109309092090" }, 36 | { "loc": 48, "notes": "901903099020" }, 37 | { "loc": 64, "repeat": 0 }, 38 | { "loc": 80, "repeat": 1 }, 39 | { "loc": 96, "notes": "109309092090" }, 40 | { "loc": 104, "notes": "901903099020" }, 41 | { "loc": 112, "notes": "209091030909" }, 42 | { "loc": 120, "notes": "209019030909" } 43 | ] 44 | }, 45 | { 46 | "description": "ⅣM7 - ⅢM7 - Ⅵ7 - Ⅰ", 47 | "progression": [ 48 | { "loc": 0, "notes": "309012090109" }, 49 | { "loc": 16, "notes": "901020901903" }, 50 | { "loc": 32, "notes": "109039010209" }, 51 | { "loc": 48, "notes": "209019030909" }, 52 | { "loc": 64, "repeat": 0 }, 53 | { "loc": 80, "repeat": 1 }, 54 | { "loc": 96, "notes": "901039090209" }, 55 | { "loc": 112, "notes": "019039010209" } 56 | ] 57 | }, 58 | { 59 | "description": "Ⅵm - Ⅴ - Ⅳ - Ⅴ", 60 | "progression": [ 61 | { "loc": 0, "notes": "109039090209" }, 62 | { "loc": 16, "notes": "903099020901" }, 63 | { "loc": 32, "notes": "309092090109" }, 64 | { "loc": 48, "notes": "903099020901" }, 65 | { "loc": 64, "repeat": 0 }, 66 | { "loc": 80, "repeat": 1 }, 67 | { "loc": 96, "repeat": 2 }, 68 | { "loc": 112, "notes": "903099020901" }, 69 | { "loc": 120, "notes": "903090902901" } 70 | ] 71 | }, 72 | { 73 | "description": "Ⅵm - Ⅳ - Ⅴ - Ⅰ", 74 | "progression": [ 75 | { "loc": 0, "notes": "109039090209" }, 76 | { "loc": 16, "notes": "309092090109" }, 77 | { "loc": 32, "notes": "903099020901" }, 78 | { "loc": 48, "notes": "209019030909" }, 79 | { "loc": 56, "notes": "109019030902" }, 80 | { "loc": 64, "repeat": 0 }, 81 | { "loc": 80, "repeat": 1 }, 82 | { "loc": 96, "repeat": 2 }, 83 | { "loc": 112, "notes": "209019030909" }, 84 | { "loc": 120, "notes": "903093090302" }, 85 | { "loc": 124, "notes": "903029030903" } 86 | ] 87 | }, 88 | { 89 | "description": "Ⅵm - Ⅱ", 90 | "progression": [ 91 | { "loc": 0, "notes": "109030909209" }, 92 | { "loc": 16, "notes": "902090109309" }, 93 | { "loc": 32, "notes": "109030909209" }, 94 | { "loc": 48, "notes": "902090109309" }, 95 | { "loc": 64, "repeat": 0 }, 96 | { "loc": 80, "repeat": 1 }, 97 | { "loc": 96, "notes": "109030909209" }, 98 | { "loc": 112, "notes": "902090109309" }, 99 | { "loc": 120, "notes": "909020901903" } 100 | ] 101 | }, 102 | { 103 | "description": "Ⅲm7 - ⅣM7 - Ⅴ6 - Ⅵm9", 104 | "progression": [ 105 | { "loc": 0, "notes": "901029010903" }, 106 | { "loc": 16, "notes": "309012090109" }, 107 | { "loc": 32, "notes": "903019020901" }, 108 | { "loc": 48, "notes": "109039030201" }, 109 | { "loc": 64, "repeat": 0 }, 110 | { "loc": 80, "repeat": 1 }, 111 | { "loc": 96, "notes": "903019020901" }, 112 | { "loc": 112, "notes": "019030909209" } 113 | ] 114 | } 115 | ], 116 | "base": [ 117 | { 118 | "basic": "4.....4.....0.44", 119 | "final": "4.....4.....44.." 120 | }, 121 | { 122 | "basic": "2.3.4.3.2.3.4.3.", 123 | "final": "2.3.4.3.2...4..." 124 | }, 125 | { 126 | "basic": "440...440...2...", 127 | "final": "440...440...442." 128 | }, 129 | { 130 | "basic": "0.2.4.2.0.2.4.2.", 131 | "final": "0.2.4.2.2.4.2.4." 132 | }, 133 | { 134 | "basic": "2.402.402.402.40", 135 | "final": "2.402.402.40242." 136 | }, 137 | { 138 | "basic": "2034203420342034", 139 | "final": "2034203420444440" 140 | }, 141 | { 142 | "basic": "2044204420442044", 143 | "final": "2044204420442.4." 144 | }, 145 | { 146 | "basic": "4.444.444.444.44", 147 | "final": "4.444.444.442.22" 148 | } 149 | ], 150 | "drums": [ 151 | { 152 | "basic": "1000000000001000", 153 | "final": "1000000000001030" 154 | }, 155 | { 156 | "basic": "1000001000001000", 157 | "final": "1000001000007750" 158 | }, 159 | { 160 | "basic": "3030003330300033", 161 | "final": "3030003330303013" 162 | }, 163 | { 164 | "basic": "1000001000003330", 165 | "final": "1000001000003310" 166 | }, 167 | { 168 | "basic": "1000301010003013", 169 | "final": "1000301010003333" 170 | }, 171 | { 172 | "basic": "3033303330333033", 173 | "final": "3033303330336655" 174 | }, 175 | { 176 | "basic": "3033203330332033", 177 | "final": "3033203330332032" 178 | }, 179 | { 180 | "basic": "1033203310332033", 181 | "final": "1033203310332220" 182 | } 183 | ], 184 | "preset": [ 185 | { 186 | "speed": 216, 187 | "chord": 0, 188 | "base": 4, 189 | "base_quantize": 14, 190 | "drums": 4, 191 | "melo_tone": 0, 192 | "sub_tone": 0, 193 | "melo_lowest_note": 28, 194 | "melo_density": 2, 195 | "melo_use16": true, 196 | "instrumentation": 3 197 | }, 198 | { 199 | "speed": 216, 200 | "chord": 1, 201 | "base": 6, 202 | "base_quantize": 12, 203 | "drums": 5, 204 | "melo_tone": 3, 205 | "sub_tone": 3, 206 | "melo_lowest_note": 28, 207 | "melo_density": 4, 208 | "melo_use16": true, 209 | "instrumentation": 3 210 | }, 211 | { 212 | "speed": 312, 213 | "chord": 2, 214 | "base": 1, 215 | "base_quantize": 15, 216 | "drums": 0, 217 | "melo_tone": 5, 218 | "sub_tone": 5, 219 | "melo_lowest_note": 30, 220 | "melo_density": 2, 221 | "melo_use16": false, 222 | "instrumentation": 0 223 | }, 224 | { 225 | "speed": 276, 226 | "chord": 3, 227 | "base": 2, 228 | "base_quantize": 15, 229 | "drums": 3, 230 | "melo_tone": 4, 231 | "sub_tone": 4, 232 | "melo_lowest_note": 28, 233 | "melo_density": 0, 234 | "melo_use16": false, 235 | "instrumentation": 3 236 | }, 237 | { 238 | "speed": 240, 239 | "chord": 4, 240 | "base": 0, 241 | "base_quantize": 14, 242 | "drums": 2, 243 | "melo_tone": 0, 244 | "sub_tone": 1, 245 | "melo_lowest_note": 29, 246 | "melo_density": 2, 247 | "melo_use16": false, 248 | "instrumentation": 3 249 | }, 250 | { 251 | "speed": 216, 252 | "chord": 5, 253 | "base_quantize": 14, 254 | "base": 3, 255 | "drums": 4, 256 | "melo_tone": 1, 257 | "sub_tone": 1, 258 | "melo_lowest_note": 30, 259 | "melo_density": 2, 260 | "melo_use16": true, 261 | "instrumentation": 2 262 | }, 263 | { 264 | "speed": 192, 265 | "chord": 6, 266 | "base": 5, 267 | "base_quantize": 13, 268 | "drums": 6, 269 | "melo_tone": 0, 270 | "sub_tone": 0, 271 | "melo_lowest_note": 28, 272 | "melo_density": 4, 273 | "melo_use16": true, 274 | "instrumentation": 1 275 | }, 276 | { 277 | "speed": 168, 278 | "chord": 7, 279 | "base": 7, 280 | "base_quantize": 15, 281 | "drums": 7, 282 | "melo_tone": 3, 283 | "sub_tone": 3, 284 | "melo_lowest_note": 28, 285 | "melo_density": 4, 286 | "melo_use16": true, 287 | "instrumentation": 3 288 | } 289 | ] 290 | } 291 | -------------------------------------------------------------------------------- /src/sounds.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import math 3 | 4 | try: 5 | import mido 6 | from mido import MidiFile, MidiTrack, Message, MetaMessage 7 | except: 8 | pass 9 | 10 | list_notes = ("c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b") 11 | 12 | 13 | def putNotes(note_len, state, tones, result): 14 | if len(result.keys()) == 0: 15 | result["note"] = "" 16 | result["tone"] = "" 17 | result["volume"] = "" 18 | result["effect"] = "" 19 | tone = tones[state["tone"]] 20 | pattern = state["pattern"] 21 | decay = pattern["decay"] if pattern and "decay" in pattern else tone["decay"] 22 | sustain = ( 23 | pattern["sustain"] if pattern and "sustain" in pattern else tone["sustain"] 24 | ) 25 | velocity = pattern["velocity"] / 100 if pattern else 1.0 26 | loops = int((note_len + state["tick"]) / 48) - int(state["tick"] / 48) 27 | for _ in range(loops): 28 | level = 0 29 | state["duration"] += 1 30 | duration = state["duration"] 31 | if ( 32 | not state["is_rest"] 33 | and duration > note_len / 48 * state["note_cnt"] * state["quantize"] 34 | ): 35 | state["is_rest"] = True 36 | state["duration"] = 0 37 | duration = 1 38 | if state["is_rest"]: 39 | release = tone["release"] if "release" in tone else 0 40 | if duration < release: 41 | level = sustain / 100 * (1 - duration / release) 42 | else: 43 | level = 0 44 | state["note"] = -1 45 | else: 46 | if duration < tone["attack"]: 47 | level = duration / tone["attack"] 48 | elif duration < tone["attack"] + decay: 49 | level = 1 - (1 - sustain / 100) * ((duration - tone["attack"]) / decay) 50 | else: 51 | level = sustain / 100 52 | if pattern: 53 | notes = pattern["notes"] 54 | note = notes[min(duration, len(notes)) - 1] 55 | state["note"] = note 56 | note_str = list_notes[note % 12] + str(note // 12) 57 | elif state["note"] is None or state["note"] < 0: 58 | note_str = "r" 59 | else: 60 | note = state["note"] 61 | note_str = list_notes[note % 12] + str(note // 12) 62 | skipVib = tone["wave"] in ["S", "T"] and duration % 2 != 0 63 | vibrato = tone["vibrato"] if "vibrato" in tone else 0 64 | effect = "n" if vibrato == 0 or duration < vibrato or skipVib else "v" 65 | volume = min(math.ceil(level * state["volume"] * velocity), 7) 66 | result["note"] += note_str 67 | result["tone"] += pattern["wave"] if pattern else tone["wave"] 68 | result["volume"] += str(volume) 69 | result["effect"] += effect 70 | 71 | 72 | # Pyxel再生データの生成 73 | def compile(src, tones, patterns): 74 | speed = 240 75 | note_len = 48 76 | states = [] 77 | results = [] 78 | for _ in range(4): 79 | states.append( 80 | { 81 | "note_cnt": 0, 82 | "tone": 0, 83 | "volume": 7, 84 | "quantize": 1.0, 85 | "duration": 0, 86 | "note": -1, 87 | "is_rest": True, 88 | "pattern": None, 89 | "tick": 0, 90 | } 91 | ) 92 | results.append({}) 93 | for row, item in enumerate(src): 94 | if not item[0] is None: 95 | old_speed = speed 96 | speed = item[0] 97 | note_len = note_len / old_speed * speed 98 | if not item[2] is None: 99 | note_len = speed * item[2] 100 | for ch in range(4): 101 | state = states[ch] 102 | item_idx = 3 + ch * 4 103 | if not item[item_idx] is None: 104 | state["tone"] = item[item_idx] 105 | if not item[item_idx + 1] is None: 106 | state["volume"] = item[item_idx + 1] 107 | if not item[item_idx + 2] is None: 108 | state["quantize"] = item[item_idx + 2] / 16 109 | note = item[item_idx + 3] 110 | if not note is None: 111 | state["pattern"] = None 112 | for pattern in patterns: 113 | if pattern["key"] == note: 114 | state["pattern"] = pattern 115 | state["duration"] = 0 116 | if note == -1: 117 | state["is_rest"] = True 118 | else: 119 | state["is_rest"] = False 120 | state["note"] = note if state["pattern"] is None else None 121 | note_cnt = 0 122 | while True: 123 | note_cnt += 1 124 | if row + note_cnt >= len(src): 125 | break 126 | nextItem = src[row + note_cnt] 127 | if not nextItem[item_idx + 3] is None: 128 | break 129 | state["note_cnt"] = note_cnt 130 | putNotes(note_len, state, tones, results[ch]) 131 | state["tick"] += note_len 132 | sounds = [] 133 | for ch in range(4): 134 | sound = results[ch] 135 | if sound["note"]: 136 | sounds.append( 137 | [ 138 | sound["note"], 139 | shorten(sound["tone"]), 140 | shorten(sound["volume"]), 141 | shorten(sound["effect"]), 142 | 1, 143 | ] 144 | ) 145 | else: 146 | sounds.append(None) 147 | return sounds 148 | 149 | 150 | # Pyxel再生データの生成 151 | def make_midi(src, outPath): 152 | mid = MidiFile() 153 | tracks = [None, None, None, None] 154 | has_note = [False, False, False, False] 155 | tones = [None, None, None, None] 156 | notes = [-1, -1, -1, -1] 157 | note_time = [0, 0, 0, 0] 158 | rest_time = [0, 0, 0, 0] 159 | volumes = [7, 7, 7, 7] 160 | quantize = [15, 15, 15, 15] 161 | bpm = 120 162 | note_len = 480 163 | 164 | def make_track(ch): 165 | if tracks[ch] is None: 166 | tracks[ch] = MidiTrack() 167 | tracks[ch].append(MetaMessage("set_tempo", tempo=bpm)) 168 | 169 | def put_note(ch, new_note): 170 | make_track(ch) 171 | note = notes[ch] 172 | if note_time[ch] and note != -1: 173 | midi_time = (note_time[ch] * quantize[ch]) // 16 174 | tracks[ch].append( 175 | Message( 176 | "note_off", 177 | note=note, 178 | velocity=64, 179 | time=midi_time, 180 | channel=ch, 181 | ) 182 | ) 183 | rest_time[ch] = note_time[ch] - midi_time 184 | if type(new_note) is str and new_note[0] == ":": 185 | midi_note = { 186 | ":1": 36, 187 | ":2": 38, 188 | ":3": 42, 189 | ":5": 45, 190 | ":6": 47, 191 | ":7": 50, 192 | }[new_note] 193 | tracks[ch].append( 194 | Message( 195 | "note_on", 196 | note=midi_note, 197 | velocity=volumes[ch] * 16, 198 | time=rest_time[ch], 199 | channel=9, 200 | ) 201 | ) 202 | tracks[ch].append( 203 | Message( 204 | "note_off", 205 | note=midi_note, 206 | velocity=64, 207 | time=40, 208 | channel=9, 209 | ) 210 | ) 211 | has_note[ch] = True 212 | rest_time[ch] = -40 213 | notes[ch] = -1 214 | elif new_note == -1: 215 | notes[ch] = -1 216 | else: 217 | midi_note = 36 + new_note 218 | tracks[ch].append( 219 | Message( 220 | "note_on", 221 | note=midi_note, 222 | velocity=volumes[ch] * 16, 223 | time=rest_time[ch], 224 | channel=ch, 225 | ) 226 | ) 227 | has_note[ch] = True 228 | notes[ch] = midi_note 229 | note_time[ch] = 0 230 | 231 | for item in src: 232 | if not item[0] is None: 233 | # TODO:テンポ変更はいったん考慮しない 234 | bpm = mido.bpm2tempo(28800 // item[0]) 235 | if not item[2] is None: 236 | note_len = item[2] * 40 237 | for ch in range(4): 238 | idx = (0, 8, 4, 12)[ch] 239 | if not item[idx + 3] is None: # 音色 240 | make_track(ch) 241 | tone = item[idx + 3] 242 | tones[ch] = tone 243 | if tone != 15: 244 | tracks[ch].append(Message("program_change", program=6, channel=ch)) 245 | if not item[idx + 4] is None: # ボリューム 246 | make_track(ch) 247 | volumes[ch] = item[idx + 4] 248 | if not item[idx + 5] is None: # クオンタイズ 249 | make_track(ch) 250 | quantize[ch] = item[idx + 5] 251 | note = item[idx + 6] # ノート。-1休符、None継続、0〜実音 252 | 253 | if notes[ch] == -1: 254 | if not note is None and note != -1: 255 | put_note(ch, note) 256 | else: 257 | if not note is None: 258 | put_note(ch, note) 259 | if notes[ch] == -1: 260 | rest_time[ch] += note_len 261 | else: 262 | note_time[ch] += note_len 263 | for ch in range(4): 264 | put_note(ch, -1) 265 | if has_note[ch]: 266 | mid.tracks.append(tracks[ch]) 267 | mid.save(outPath) 268 | 269 | 270 | def shorten(s): 271 | head = s[0:1] 272 | for idx in range(len(s)): 273 | if s[idx : idx + 1] != head: 274 | return s 275 | return head 276 | 277 | 278 | def raise_error(msg): 279 | print(msg) 280 | sys.exit() 281 | -------------------------------------------------------------------------------- /src/rhythm.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | 0, 4 | null, 5 | null, 6 | null, 7 | null, 8 | 0, 9 | 0, 10 | 0, 11 | 0, 12 | null, 13 | null, 14 | null, 15 | null, 16 | null, 17 | null, 18 | null 19 | ], 20 | [ 21 | null, 22 | null, 23 | null, 24 | 0, 25 | null, 26 | null, 27 | 0, 28 | null, 29 | 0, 30 | null, 31 | null, 32 | null, 33 | 0, 34 | null, 35 | null, 36 | null 37 | ], 38 | [ 39 | 0, 40 | null, 41 | null, 42 | 0, 43 | null, 44 | null, 45 | 0, 46 | null, 47 | 0, 48 | null, 49 | null, 50 | null, 51 | null, 52 | null, 53 | null, 54 | null 55 | ], 56 | [ 57 | -1, 58 | null, 59 | null, 60 | null, 61 | null, 62 | null, 63 | null, 64 | null, 65 | -1, 66 | null, 67 | null, 68 | null, 69 | null, 70 | null, 71 | null, 72 | null 73 | ], 74 | [ 75 | 0, 76 | null, 77 | null, 78 | null, 79 | null, 80 | 0, 81 | 0, 82 | 0, 83 | 0, 84 | null, 85 | null, 86 | null, 87 | null, 88 | null, 89 | null, 90 | null 91 | ], 92 | [ 93 | null, 94 | null, 95 | null, 96 | 0, 97 | null, 98 | null, 99 | 0, 100 | null, 101 | 0, 102 | null, 103 | null, 104 | 0, 105 | null, 106 | null, 107 | 0, 108 | null 109 | ], 110 | [ 111 | 0, 112 | null, 113 | null, 114 | null, 115 | null, 116 | null, 117 | 0, 118 | null, 119 | null, 120 | null, 121 | 0, 122 | 0, 123 | 0, 124 | null, 125 | null, 126 | null 127 | ], 128 | [ 129 | 0, 130 | null, 131 | null, 132 | null, 133 | null, 134 | null, 135 | null, 136 | null, 137 | null, 138 | null, 139 | null, 140 | null, 141 | null, 142 | null, 143 | null, 144 | null 145 | ], 146 | [ 147 | 0, 148 | null, 149 | null, 150 | null, 151 | null, 152 | 0, 153 | 0, 154 | 0, 155 | 0, 156 | null, 157 | null, 158 | null, 159 | null, 160 | null, 161 | null, 162 | null 163 | ], 164 | [ 165 | null, 166 | null, 167 | null, 168 | null, 169 | 0, 170 | null, 171 | null, 172 | null, 173 | 0, 174 | null, 175 | null, 176 | null, 177 | 0, 178 | null, 179 | null, 180 | null 181 | ], 182 | [ 183 | 0, 184 | null, 185 | null, 186 | null, 187 | null, 188 | null, 189 | null, 190 | 0, 191 | null, 192 | null, 193 | null, 194 | 0, 195 | 0, 196 | 0, 197 | null, 198 | null, 199 | null 200 | ], 201 | [ 202 | 0, 203 | null, 204 | null, 205 | null, 206 | null, 207 | null, 208 | null, 209 | null, 210 | null, 211 | null, 212 | 0, 213 | null, 214 | 0, 215 | 0, 216 | null, 217 | null 218 | ], 219 | [ 220 | 0, 221 | null, 222 | null, 223 | null, 224 | null, 225 | null, 226 | null, 227 | null, 228 | null, 229 | null, 230 | 0, 231 | null, 232 | 0, 233 | null, 234 | 0, 235 | null 236 | ], 237 | [ 238 | 0, 239 | null, 240 | null, 241 | null, 242 | null, 243 | null, 244 | 0, 245 | null, 246 | null, 247 | null, 248 | 0, 249 | 0, 250 | 0, 251 | null, 252 | null, 253 | null 254 | ], 255 | [ 256 | 0, 257 | null, 258 | null, 259 | null, 260 | null, 261 | null, 262 | null, 263 | null, 264 | null, 265 | null, 266 | null, 267 | null, 268 | null, 269 | null, 270 | null, 271 | null 272 | ], 273 | [ 274 | -1, 275 | null, 276 | null, 277 | null, 278 | null, 279 | null, 280 | null, 281 | null, 282 | -1, 283 | null, 284 | 0, 285 | null, 286 | 0, 287 | null, 288 | 0, 289 | null 290 | ], 291 | [ 292 | 0, 293 | null, 294 | null, 295 | null, 296 | null, 297 | null, 298 | 0, 299 | null, 300 | null, 301 | null, 302 | null, 303 | null, 304 | 0, 305 | null, 306 | null, 307 | null 308 | ], 309 | [ 310 | 0, 311 | null, 312 | null, 313 | 0, 314 | null, 315 | null, 316 | 0, 317 | null, 318 | null, 319 | null, 320 | null, 321 | 0, 322 | 0, 323 | null, 324 | null, 325 | null 326 | ], 327 | [ 328 | 0, 329 | null, 330 | null, 331 | null, 332 | null, 333 | null, 334 | 0, 335 | null, 336 | null, 337 | null, 338 | null, 339 | null, 340 | 0, 341 | null, 342 | null, 343 | null 344 | ], 345 | [null, null, null, null, null, null, null, null, -1, null, 0, 0, 0, 0, 0, 0], 346 | [ 347 | 0, 348 | null, 349 | null, 350 | 0, 351 | null, 352 | null, 353 | 0, 354 | null, 355 | 0, 356 | null, 357 | null, 358 | null, 359 | 0, 360 | null, 361 | null, 362 | null 363 | ], 364 | [ 365 | 0, 366 | null, 367 | null, 368 | 0, 369 | null, 370 | null, 371 | 0, 372 | null, 373 | 0, 374 | null, 375 | null, 376 | 0, 377 | null, 378 | null, 379 | 0, 380 | null 381 | ], 382 | [ 383 | 0, 384 | null, 385 | null, 386 | null, 387 | null, 388 | null, 389 | null, 390 | null, 391 | null, 392 | null, 393 | null, 394 | null, 395 | null, 396 | null, 397 | null, 398 | null 399 | ], 400 | [null, null, null, null, null, null, null, null, -1, 0, 0, 0, 0, 0, 0, 0], 401 | [ 402 | 0, 403 | null, 404 | 0, 405 | null, 406 | 0, 407 | null, 408 | null, 409 | null, 410 | null, 411 | null, 412 | null, 413 | null, 414 | null, 415 | null, 416 | null, 417 | null 418 | ], 419 | [ 420 | -1, 421 | null, 422 | null, 423 | null, 424 | null, 425 | null, 426 | 0, 427 | null, 428 | 0, 429 | null, 430 | 0, 431 | null, 432 | 0, 433 | null, 434 | 0, 435 | null 436 | ], 437 | [ 438 | 0, 439 | null, 440 | 0, 441 | null, 442 | 0, 443 | null, 444 | null, 445 | null, 446 | null, 447 | null, 448 | null, 449 | null, 450 | null, 451 | null, 452 | null, 453 | null 454 | ], 455 | [ 456 | -1, 457 | null, 458 | null, 459 | null, 460 | null, 461 | null, 462 | 0, 463 | null, 464 | 0, 465 | null, 466 | 0, 467 | null, 468 | 0, 469 | null, 470 | 0, 471 | null 472 | ], 473 | [ 474 | 0, 475 | null, 476 | null, 477 | null, 478 | null, 479 | null, 480 | null, 481 | null, 482 | null, 483 | null, 484 | 0, 485 | null, 486 | 0, 487 | null, 488 | 0, 489 | null 490 | ], 491 | [ 492 | 0, 493 | null, 494 | null, 495 | null, 496 | 0, 497 | null, 498 | null, 499 | null, 500 | 0, 501 | null, 502 | null, 503 | null, 504 | null, 505 | null, 506 | 0, 507 | null 508 | ], 509 | [ 510 | 0, 511 | null, 512 | null, 513 | null, 514 | null, 515 | null, 516 | null, 517 | null, 518 | 0, 519 | null, 520 | null, 521 | null, 522 | 0, 523 | null, 524 | null, 525 | null 526 | ], 527 | [ 528 | 0, 529 | null, 530 | 0, 531 | null, 532 | 0, 533 | null, 534 | null, 535 | null, 536 | null, 537 | null, 538 | null, 539 | null, 540 | null, 541 | null, 542 | null, 543 | null 544 | ], 545 | [ 546 | -1, 547 | null, 548 | null, 549 | null, 550 | null, 551 | null, 552 | 0, 553 | null, 554 | 0, 555 | null, 556 | 0, 557 | null, 558 | 0, 559 | null, 560 | 0, 561 | null 562 | ], 563 | [ 564 | 0, 565 | null, 566 | 0, 567 | null, 568 | 0, 569 | null, 570 | null, 571 | null, 572 | null, 573 | null, 574 | null, 575 | null, 576 | null, 577 | null, 578 | null, 579 | null 580 | ], 581 | [ 582 | -1, 583 | null, 584 | null, 585 | null, 586 | null, 587 | null, 588 | 0, 589 | null, 590 | 0, 591 | null, 592 | 0, 593 | null, 594 | 0, 595 | null, 596 | 0, 597 | null 598 | ], 599 | [ 600 | 0, 601 | null, 602 | null, 603 | null, 604 | null, 605 | null, 606 | null, 607 | null, 608 | null, 609 | null, 610 | 0, 611 | null, 612 | 0, 613 | null, 614 | 0, 615 | null 616 | ], 617 | [ 618 | 0, 619 | null, 620 | null, 621 | null, 622 | 0, 623 | null, 624 | null, 625 | null, 626 | 0, 627 | null, 628 | null, 629 | null, 630 | null, 631 | null, 632 | 0, 633 | null 634 | ], 635 | [ 636 | 0, 637 | null, 638 | null, 639 | null, 640 | null, 641 | null, 642 | null, 643 | null, 644 | null, 645 | null, 646 | null, 647 | null, 648 | null, 649 | null, 650 | null, 651 | null 652 | ], 653 | [ 654 | -1, 655 | null, 656 | null, 657 | null, 658 | null, 659 | null, 660 | null, 661 | null, 662 | null, 663 | null, 664 | null, 665 | null, 666 | null, 667 | null, 668 | null, 669 | null 670 | ], 671 | [ 672 | 0, 673 | null, 674 | null, 675 | 0, 676 | null, 677 | null, 678 | 0, 679 | null, 680 | 0, 681 | null, 682 | null, 683 | null, 684 | null, 685 | null, 686 | null, 687 | null 688 | ], 689 | [ 690 | -1, 691 | null, 692 | null, 693 | null, 694 | null, 695 | null, 696 | 0, 697 | 0, 698 | 0, 699 | null, 700 | null, 701 | 0, 702 | null, 703 | null, 704 | 0, 705 | null 706 | ], 707 | [ 708 | 0, 709 | null, 710 | null, 711 | 0, 712 | null, 713 | null, 714 | 0, 715 | null, 716 | 0, 717 | null, 718 | null, 719 | null, 720 | null, 721 | null, 722 | null, 723 | null 724 | ], 725 | [ 726 | null, 727 | null, 728 | null, 729 | 0, 730 | null, 731 | null, 732 | 0, 733 | null, 734 | 0, 735 | null, 736 | null, 737 | null, 738 | 0, 739 | null, 740 | null, 741 | null 742 | ], 743 | [ 744 | 0, 745 | null, 746 | null, 747 | null, 748 | null, 749 | null, 750 | 0, 751 | null, 752 | null, 753 | null, 754 | null, 755 | null, 756 | 0, 757 | null, 758 | null, 759 | null 760 | ], 761 | [0, null, 0, null, 0, null, 0, null, null, null, 0, 0, 0, null, null, null], 762 | [ 763 | 0, 764 | null, 765 | null, 766 | 0, 767 | null, 768 | null, 769 | 0, 770 | null, 771 | 0, 772 | null, 773 | null, 774 | null, 775 | null, 776 | null, 777 | null, 778 | null 779 | ], 780 | [ 781 | -1, 782 | null, 783 | null, 784 | null, 785 | null, 786 | null, 787 | 0, 788 | 0, 789 | 0, 790 | null, 791 | null, 792 | 0, 793 | null, 794 | null, 795 | 0, 796 | null 797 | ], 798 | [ 799 | 0, 800 | null, 801 | null, 802 | 0, 803 | null, 804 | null, 805 | 0, 806 | null, 807 | 0, 808 | null, 809 | null, 810 | null, 811 | null, 812 | null, 813 | null, 814 | null 815 | ], 816 | [ 817 | null, 818 | null, 819 | null, 820 | 0, 821 | null, 822 | null, 823 | 0, 824 | null, 825 | 0, 826 | null, 827 | null, 828 | null, 829 | 0, 830 | null, 831 | null, 832 | null 833 | ], 834 | [ 835 | 0, 836 | null, 837 | null, 838 | null, 839 | null, 840 | null, 841 | 0, 842 | null, 843 | null, 844 | null, 845 | null, 846 | null, 847 | 0, 848 | null, 849 | null, 850 | null 851 | ], 852 | [0, null, 0, null, 0, null, 0, null, null, null, 0, 0, 0, null, null, null], 853 | [ 854 | 0, 855 | null, 856 | null, 857 | null, 858 | null, 859 | null, 860 | 0, 861 | 0, 862 | 0, 863 | null, 864 | null, 865 | null, 866 | null, 867 | null, 868 | 0, 869 | 0 870 | ], 871 | [ 872 | 0, 873 | null, 874 | 0, 875 | 0, 876 | 0, 877 | null, 878 | null, 879 | null, 880 | null, 881 | null, 882 | null, 883 | null, 884 | 0, 885 | null, 886 | 0, 887 | null 888 | ], 889 | [ 890 | 0, 891 | null, 892 | null, 893 | null, 894 | null, 895 | null, 896 | 0, 897 | 0, 898 | 0, 899 | null, 900 | null, 901 | null, 902 | null, 903 | null, 904 | 0, 905 | 0 906 | ], 907 | [ 908 | 0, 909 | null, 910 | 0, 911 | 0, 912 | 0, 913 | null, 914 | null, 915 | null, 916 | null, 917 | null, 918 | null, 919 | null, 920 | null, 921 | null, 922 | null, 923 | null 924 | ], 925 | [ 926 | 0, 927 | null, 928 | null, 929 | null, 930 | null, 931 | null, 932 | 0, 933 | 0, 934 | 0, 935 | null, 936 | null, 937 | null, 938 | null, 939 | null, 940 | 0, 941 | 0 942 | ], 943 | [ 944 | 0, 945 | null, 946 | 0, 947 | 0, 948 | 0, 949 | null, 950 | null, 951 | null, 952 | null, 953 | null, 954 | null, 955 | null, 956 | 0, 957 | null, 958 | 0, 959 | null 960 | ], 961 | [ 962 | 0, 963 | null, 964 | null, 965 | null, 966 | null, 967 | null, 968 | 0, 969 | 0, 970 | 0, 971 | null, 972 | null, 973 | null, 974 | null, 975 | null, 976 | 0, 977 | 0 978 | ], 979 | [ 980 | 0, 981 | null, 982 | 0, 983 | 0, 984 | 0, 985 | null, 986 | null, 987 | null, 988 | null, 989 | null, 990 | null, 991 | null, 992 | null, 993 | null, 994 | null, 995 | null 996 | ], 997 | [0, 0, 0, 0, 0, 0, 0, 0, 0, null, null, null, null, null, null, null], 998 | [0, 0, 0, 0, 0, 0, 0, 0, 0, null, null, null, null, null, null, null], 999 | [0, 0, 0, 0, 0, null, null, null, 0, 0, 0, 0, 0, null, null, null], 1000 | [ 1001 | 0, 1002 | null, 1003 | null, 1004 | null, 1005 | null, 1006 | null, 1007 | 0, 1008 | null, 1009 | null, 1010 | null, 1011 | null, 1012 | null, 1013 | 0, 1014 | null, 1015 | null, 1016 | null 1017 | ] 1018 | ] 1019 | -------------------------------------------------------------------------------- /src/generator.py: -------------------------------------------------------------------------------- 1 | # title: 8bit BGM generator 2 | # author: frenchbread 3 | # desc: A Pyxel music auto-generation tool 4 | # site: https://github.com/shiromofufactory/8bit-bgm-generator 5 | # license: MIT 6 | # version: 1.30 7 | import pyxel as px 8 | import json 9 | import sounds 10 | import os 11 | from bdf import BDFRenderer 12 | 13 | SUBMELODY_DIFF = 0 14 | SUB_RHYTHM = [0, None, 0, None, 0, None, 0, None, 0, None, 0, None, 0, None, 0, None] 15 | 16 | LOCAL = False 17 | try: 18 | from js import Blob, URL, document, window, _savePyxelFile 19 | except: 20 | LOCAL = True 21 | 22 | # カラー定義 23 | COL_BACK_PRIMARY = 7 24 | COL_BACK_SECONDARY = 12 25 | COL_BTN_BASIC = 5 26 | COL_BTN_SELECTED = 6 27 | COL_BTN_DISABLED = 13 28 | COL_TEXT_BASIC = 1 29 | COL_TEXT_MUTED = 5 30 | COL_SHADOW = 0 31 | 32 | # 生成する曲の小節数(8固定) 33 | BARS_NUMBERS = 8 34 | 35 | # パラメータ指定用 36 | list_instrumentation = [ 37 | (0, "Melo(with reverb) & Bass"), 38 | (1, "Melo & Bass & Drums"), 39 | (2, "Melo & Bass & Sub"), 40 | (3, "Full (Melo & Bass & Sub & Drums)"), 41 | ] 42 | list_tones = [ 43 | (11, "Pulse solid"), 44 | (8, "Pulse thin"), 45 | (2, "Pulse soft"), 46 | (10, "Square solid"), 47 | (6, "Square thin (Harp)"), 48 | (4, "Square soft (Flute)"), 49 | ] 50 | list_melo_lowest_note = [ 51 | (28, "E2"), 52 | (29, "F2"), 53 | (30, "F#2"), 54 | (31, "G2"), 55 | (32, "G#2"), 56 | (33, "A2"), 57 | ] 58 | list_melo_use16 = [(True, "Yes"), (False, "No")] 59 | list_melo_density = [(0, "less"), (2, "normal"), (4, "more")] 60 | 61 | 62 | # 部品 63 | class Element: 64 | def __init__(self, x, y, w, h): 65 | self.x = x 66 | self.y = y 67 | self.w = w 68 | self.h = h 69 | 70 | def mouse_in(self): 71 | mx = px.mouse_x 72 | my = px.mouse_y 73 | return ( 74 | mx >= self.x 75 | and mx < self.x + self.w 76 | and my >= self.y 77 | and my < self.y + self.h 78 | ) 79 | 80 | 81 | # タブ 82 | class Tab(Element): 83 | def __init__(self, idx, x, y, text): 84 | super().__init__(x, y, 64, 12) 85 | self.idx = idx 86 | self.text = text 87 | 88 | def draw(self, app): 89 | active = self.idx == app.tab 90 | rect_c = COL_BACK_PRIMARY if active else COL_BACK_SECONDARY 91 | text_c = COL_TEXT_BASIC if active else COL_TEXT_MUTED 92 | px.rect(self.x, self.y, self.w, self.h, rect_c) 93 | text_info = app.get_text(self.text) 94 | x = int(self.x + self.w / 2 - text_info[1]) 95 | y = int(self.y + self.h / 2 - 4) 96 | app.text(x, y, self.text, text_c) 97 | 98 | 99 | # アイコン 100 | class Icon(Element): 101 | def __init__(self, id, x, y): 102 | super().__init__(x, y, 16, 12) 103 | self.id = id 104 | self.state = 0 105 | 106 | def draw(self, app): 107 | state = 0 108 | if self.id == 0: 109 | state = 1 if px.play_pos(0) else 0 110 | elif self.id == 1: 111 | state = 1 if not px.play_pos(0) else 0 112 | elif self.id == 2: 113 | state = 1 if app.loop else 0 114 | elif self.id == 3: 115 | state = 1 if app.show_export else 0 116 | px.blt(self.x, self.y, 0, self.id * 16, state * 16, self.w, self.h, 0) 117 | self.state = state 118 | 119 | 120 | # ボタン 121 | class Button(Element): 122 | def __init__(self, tab, type, key, x, y, w, text, disable_cond=0): 123 | super().__init__(x, y, w, 10) 124 | self.tab = tab 125 | self.type = type 126 | self.key = key 127 | self.x = x 128 | self.y = y 129 | self.w = w 130 | self.text = text 131 | self.disable_cond = disable_cond 132 | self.selected = False 133 | 134 | def draw(self, app): 135 | if not self.visible(app): 136 | return 137 | text_s = str(self.text) 138 | if self.disabled(app): 139 | rect_c = COL_BTN_DISABLED 140 | elif app.parm[self.type] == self.key: 141 | rect_c = COL_BTN_SELECTED 142 | else: 143 | rect_c = COL_BTN_BASIC 144 | text_c = COL_TEXT_BASIC 145 | px.rect(self.x, self.y, self.w - 1, self.h - 1, rect_c) 146 | px.text( 147 | self.x + self.w / 2 - len(text_s) * 2, 148 | self.y + self.h / 2 - 3, 149 | text_s, 150 | text_c, 151 | ) 152 | 153 | def visible(self, app): 154 | return self.tab is None or app.tab == self.tab 155 | 156 | def disabled(self, app): 157 | org = app.parm["instrumentation"] 158 | return (self.disable_cond == 1 and org in (0, 2)) or ( 159 | self.disable_cond == 2 and org in (0, 1) 160 | ) 161 | 162 | 163 | # アプリ 164 | class App: 165 | def __init__(self): 166 | output_path = os.path.abspath(".") 167 | if output_path.endswith("src"): 168 | self.output_path = output_path + "/../export" 169 | else: 170 | self.output_path = output_path + "/export" 171 | self.output_json = "music.json" 172 | self.output_wav = "music.wav" 173 | self.output_midi = "music.mid" 174 | self.failed_export_midi = False 175 | px.init(256, 256, title="8bit BGM generator", quit_key=px.KEY_NONE) 176 | px.load("assets.pyxres") 177 | self.bdf = BDFRenderer("misaki_gothic.bdf") 178 | self.parm = { 179 | "preset": 0, 180 | "transpose": 0, 181 | "language": 1, 182 | "base_highest_note": 26, # ベース(ルート)最高音 183 | "melo_density": 4, # メロディ濃度(0-4) 184 | } 185 | self.loop = True 186 | with open("tones.json", "rt", encoding="utf-8") as fin: 187 | self.tones = json.loads(fin.read()) 188 | with open("patterns.json", "rt", encoding="utf-8") as fin: 189 | self.patterns = json.loads(fin.read()) 190 | with open("generator.json", "rt", encoding="utf-8") as fin: 191 | self.generator = json.loads(fin.read()) 192 | with open("rhythm.json", "rt", encoding="utf-8") as fin: 193 | self.melo_rhythm = json.loads(fin.read()) 194 | # タブ、共通ボタン、アイコン 195 | self.tabs = [] 196 | self.buttons = [] 197 | self.icons = [] 198 | list_tab = (0, 1, 2) 199 | list_language = ("Japanese", "English") 200 | for i, elm in enumerate(list_tab): 201 | self.set_tab(i, i * 64 + 4, 20, elm) 202 | for i in range(4 if LOCAL else 5): 203 | self.set_icon(i, 4 + i * 20, 4) 204 | for i, elm in enumerate(list_language): 205 | self.set_btn(None, "language", i, 116 + 48 * i, 6, 48, elm) 206 | # 基本タブ 207 | for i, elm in enumerate(self.generator["preset"]): 208 | self.set_btn(0, "preset", i, 8 + 24 * i, 50, 24, i + 1) 209 | for i in range(12): 210 | key = (i + 6) % 12 - 11 211 | self.set_btn(0, "transpose", key, 8 + 20 * i, 114, 20, i - 5) 212 | for i, elm in enumerate(list_instrumentation): 213 | self.set_btn(0, "instrumentation", elm[0], 8, 144 + i * 10, 144, elm[1]) 214 | # コードとリズムタブ 215 | list_speed = [360, 312, 276, 240, 216, 192, 168, 156] 216 | list_base_quantize = [12, 13, 14, 15] 217 | for i, elm in enumerate(list_speed): 218 | self.set_btn(1, "speed", elm, 8 + 24 * i, 50, 24, int(28800 / elm)) 219 | for i, elm in enumerate(self.generator["chords"]): 220 | self.set_btn(1, "chord", i, 8 + 24 * i, 80, 24, i + 1) 221 | for i, elm in enumerate(self.generator["base"]): 222 | self.set_btn(1, "base", i, 8 + 24 * i, 110, 24, i + 1) 223 | for i, elm in enumerate(list_base_quantize): 224 | quantize = str(int(elm * 100 / 16)) + "%" 225 | self.set_btn(1, "base_quantize", elm, 8 + 24 * i, 140, 24, quantize) 226 | for i, elm in enumerate(self.generator["drums"]): 227 | self.set_btn(1, "drums", i, 8 + 24 * i, 170, 24, i + 1, 1) 228 | # メロディータブ 229 | for i, elm in enumerate(list_tones): 230 | self.set_btn(2, "melo_tone", i, 8 + 24 * i, 50, 24, i + 1) 231 | for i, elm in enumerate(list_tones): 232 | self.set_btn(2, "sub_tone", i, 8 + 24 * i, 80, 24, i + 1, 2) 233 | for i, elm in enumerate(list_melo_lowest_note): 234 | self.set_btn(2, "melo_lowest_note", elm[0], 8 + 24 * i, 110, 24, elm[1]) 235 | for i, elm in enumerate(list_melo_density): 236 | self.set_btn(2, "melo_density", elm[0], 8 + 48 * i, 140, 48, elm[1]) 237 | for i, elm in enumerate(list_melo_use16): 238 | self.set_btn(2, "melo_use16", elm[0], 8 + 24 * i, 170, 24, elm[1]) 239 | self.items = [] 240 | self.set_preset(self.parm["preset"]) 241 | self.play() 242 | self.saved_playkey = [-1, -1, -1] 243 | self.show_export = None 244 | self.downloading = False 245 | self.tab = 0 246 | px.mouse(True) 247 | px.run(self.update, self.draw) 248 | 249 | @property 250 | def total_len(self): 251 | return BARS_NUMBERS * 16 252 | 253 | @property 254 | def with_submelody(self): 255 | return self.parm["instrumentation"] in (2, 3) 256 | 257 | @property 258 | def with_drum(self): 259 | return self.parm["instrumentation"] in (1, 3) 260 | 261 | def set_tab(self, *args): 262 | self.tabs.append(Tab(*args)) 263 | 264 | def set_icon(self, *args): 265 | self.icons.append(Icon(*args)) 266 | 267 | def set_btn(self, *args): 268 | self.buttons.append(Button(*args)) 269 | 270 | def update(self): 271 | if not px.btnp(px.MOUSE_BUTTON_LEFT): 272 | return 273 | if self.show_export: 274 | self.show_export = None 275 | return 276 | for tab in self.tabs: 277 | if tab.mouse_in(): 278 | self.tab = tab.idx 279 | for icon in self.icons: 280 | if icon.mouse_in(): 281 | if icon.id == 0 and icon.state == 0: 282 | self.play() 283 | elif icon.id == 1 and icon.state == 0: 284 | px.stop() 285 | elif icon.id == 2: 286 | self.loop = not self.loop 287 | if px.play_pos(0): 288 | self.play() 289 | elif icon.id == 3: 290 | px.musics[0].set([0], [1], [2], [3]) 291 | if LOCAL: 292 | with open( 293 | f"{self.output_path}/{self.output_json}", "wt" 294 | ) as fout: 295 | fout.write(json.dumps(self.music)) 296 | px.musics[0].save(f"{self.output_path}/{self.output_wav}", 1) 297 | try: 298 | sounds.make_midi( 299 | self.items, f"{self.output_path}/{self.output_midi}" 300 | ) 301 | except: 302 | self.failed_export_midi = True 303 | print( 304 | "MIDIファイルを出力できませんでした。midoをインストールしてください。" 305 | ) 306 | else: 307 | if not self.downloading: 308 | self.downloading = True 309 | blob = Blob.new( 310 | [json.dumps(self.music)], {"type": "text/plain"} 311 | ) 312 | blob_url = URL.createObjectURL(blob) 313 | a = document.createElement("a") 314 | a.href = blob_url 315 | a.download = self.output_json 316 | document.body.appendChild(a) 317 | a.click() 318 | document.body.removeChild(a) 319 | URL.revokeObjectURL(blob_url) 320 | px.musics[0].save(self.output_wav, 1) 321 | _savePyxelFile(self.output_wav) 322 | self.downloading = False 323 | self.show_export = True 324 | elif icon.id == 4: 325 | window.open( 326 | "https://github.com/shiromofufactory/8bit-bgm-generator#readme" 327 | ) 328 | for button in self.buttons: 329 | if button.visible(self) and button.mouse_in(): 330 | prev_value = self.parm[button.type] 331 | self.parm[button.type] = button.key 332 | if button.type == "language": 333 | return 334 | if button.type == "preset": 335 | self.set_preset(button.key) 336 | else: 337 | make_melody = button.type in [ 338 | "transpose", 339 | "instrumentation", 340 | "chord", 341 | "base", 342 | "melo_lowest_note", 343 | "melo_density", 344 | "melo_use16", 345 | ] 346 | # サブ音色変更時、Beforeがサブなしだったらメロディ再生成する 347 | if button.type == "sub_tone": 348 | if (prev_value < 0 and button.key >= 0) or ( 349 | prev_value >= 0 and button.key < 0 350 | ): 351 | make_melody = True 352 | self.generate_music(make_melody) 353 | self.play() 354 | 355 | def draw(self): 356 | px.cls(COL_BACK_SECONDARY) 357 | px.rect(4, 32, 248, 184, COL_BACK_PRIMARY) 358 | px.text(220, 8, "ver 1.30", COL_TEXT_MUTED) 359 | if self.tab == 0: 360 | self.text(8, 40, 3, COL_TEXT_BASIC) 361 | px.rectb(8, 64, 240, 32, COL_TEXT_MUTED) 362 | self.text(16, 68, 4, COL_TEXT_MUTED) 363 | self.text(16, 76, 5, COL_TEXT_MUTED) 364 | self.text(16, 84, 6, COL_TEXT_MUTED) 365 | self.text(8, 104, 7, COL_TEXT_BASIC) 366 | self.text(8, 134, 8, COL_TEXT_BASIC) 367 | px.rectb(8, 188, 240, 24, COL_TEXT_MUTED) 368 | self.text(16, 192, 29, COL_TEXT_MUTED) 369 | self.text(16, 200, 30, COL_TEXT_MUTED) 370 | elif self.tab == 1: 371 | self.text(8, 40, 9, COL_TEXT_BASIC) 372 | self.text(8, 70, 10, COL_TEXT_BASIC) 373 | chord_name = self.generator["chords"][self.parm["chord"]]["description"] 374 | self.text(80, 70, chord_name, COL_TEXT_MUTED) 375 | self.text(8, 100, 11, COL_TEXT_BASIC) 376 | self.text(8, 130, 12, COL_TEXT_BASIC) 377 | self.text(8, 160, 13, COL_TEXT_BASIC) 378 | elif self.tab == 2: 379 | self.text(8, 40, 16, COL_TEXT_BASIC) 380 | melo_tone_name = list_tones[self.parm["melo_tone"]][1] 381 | self.text(88, 40, melo_tone_name, COL_TEXT_MUTED) 382 | self.text(8, 70, 17, COL_TEXT_BASIC) 383 | if self.with_submelody: 384 | sub_tone_name = list_tones[self.parm["sub_tone"]][1] 385 | else: 386 | sub_tone_name = "-" 387 | self.text(88, 70, sub_tone_name, COL_TEXT_MUTED) 388 | self.text(8, 100, 18, COL_TEXT_BASIC) 389 | self.text(8, 130, 19, COL_TEXT_BASIC) 390 | self.text(8, 160, 20, COL_TEXT_BASIC) 391 | # タブ、ボタン、モーダル 392 | for tab in self.tabs: 393 | tab.draw(self) 394 | for button in self.buttons: 395 | button.draw(self) 396 | for icon in self.icons: 397 | icon.draw(self) 398 | if self.show_export: 399 | h = 12 * 5 + 18 400 | y = 72 401 | px.rect(20, y + 4, 224, h, COL_SHADOW) 402 | px.rect(16, y, 224, h, COL_BTN_SELECTED) 403 | px.rectb(16, y, 224, h, COL_BTN_BASIC) 404 | if self.failed_export_midi: 405 | list_mes = (22, 25, 26, 27, 28) 406 | else: 407 | list_mes = (22, 23, 24, 27, 28) 408 | for i in range(5): 409 | self.text(20, y + 4 + 12 * i, list_mes[i], COL_TEXT_BASIC) 410 | # 鍵盤 411 | sx = 8 412 | sy = 234 413 | px.rect(sx, sy, 5 * 42 - 1, 16, 7) 414 | for x in range(5 * 7 - 1): 415 | px.line(sx + 5 + x * 6, sy, sx + 5 + x * 6, sy + 15, 0) 416 | for o in range(5): 417 | px.rect(sx + 3 + o * 42, sy, 5, 9, 0) 418 | px.rect(sx + 9 + o * 42, sy, 5, 9, 0) 419 | px.rect(sx + 21 + o * 42, sy, 5, 9, 0) 420 | px.rect(sx + 27 + o * 42, sy, 5, 9, 0) 421 | px.rect(sx + 33 + o * 42, sy, 5, 9, 0) 422 | # 音域バー 423 | mln = self.parm["melo_lowest_note"] 424 | bhn = self.parm["base_highest_note"] 425 | (x1, _) = self.get_piano_xy(mln) 426 | (x2, _) = self.get_piano_xy(mln + 16) 427 | px.rect(x1, 231, x2 - x1 + 3, 2, 11) 428 | if self.with_submelody: 429 | (x1, _) = self.get_piano_xy(bhn - 20) 430 | (x2, _) = self.get_piano_xy(mln + 13) 431 | px.rect(x1, 228, x2 - x1 + 3, 2, 14) 432 | (x1, _) = self.get_piano_xy(bhn - 23) 433 | (x2, _) = self.get_piano_xy(bhn) 434 | px.rect(x1, 231, x2 - x1 + 3, 2, 10) 435 | # 再生インジケータ 436 | if px.play_pos(0): 437 | pos = px.play_pos(0)[1] 438 | ticks = self.parm["speed"] / 16 439 | loc = int(pos // ticks) 440 | bars = loc // 16 + 1 441 | beats = (loc // 4) % 4 + 1 442 | else: 443 | return 444 | px.text(8, 220, f"bars: {bars}/{BARS_NUMBERS}", COL_TEXT_BASIC) 445 | px.text(56, 220, f"beats: {beats}/{4}", COL_TEXT_BASIC) 446 | item = self.items[loc] 447 | # 演奏情報 448 | self.draw_playkey(0, item[6], 11) 449 | self.draw_playkey(1, item[10], 10) 450 | if self.with_submelody: 451 | self.draw_playkey(2, item[14], 14) 452 | for i, elm in enumerate(self.patterns): 453 | y = i // 3 454 | x = i % 3 455 | c = COL_TEXT_BASIC if elm["key"] in (item[14], item[18]) else COL_TEXT_MUTED 456 | px.text(220 + x * 10, 235 + y * 8, elm["abbr"], c) 457 | 458 | def draw_playkey(self, key, input, c): 459 | value = input 460 | if value is None: 461 | value = self.saved_playkey[key] 462 | else: 463 | self.saved_playkey[key] = value 464 | if value < 0: 465 | return 466 | (x, y) = self.get_piano_xy(value) 467 | px.rect(x, y, 3, 4, c) 468 | 469 | def get_piano_xy(self, value): 470 | note12 = value % 12 471 | oct = value // 12 472 | x = 8 + (1, 4, 7, 10, 13, 19, 22, 25, 28, 31, 34, 37)[note12] + oct * 42 473 | y = 234 + (2 if note12 in [1, 3, 6, 8, 10] else 10) 474 | return x, y 475 | 476 | def play(self): 477 | for ch, sound in enumerate(self.music): 478 | px.sounds[ch].set(*sound) 479 | px.play(ch, ch, loop=self.loop) 480 | 481 | def set_preset(self, value): 482 | preset = self.generator["preset"][value] 483 | for key in preset: 484 | self.parm[key] = preset[key] 485 | self.generate_music() 486 | 487 | def text(self, x, y, value, c): 488 | if type(value) is int: 489 | self.bdf.text(x, y, self.get_text(value)[0], c) 490 | else: 491 | self.bdf.text(x, y, value, c) 492 | 493 | def get_text(self, value): 494 | list_text = [ 495 | ("きほん", "Basic"), 496 | ("コードとリズム", "Chord & Rhythm"), 497 | ("メロディ", "Melody"), 498 | ("プリセット", "Preset"), 499 | ( 500 | "「コードとリズム」「メロディ」の オススメせっていを", 501 | "The recommended settings for 'Chord and Rhythm' and", 502 | ), 503 | ( 504 | "とうろくしてあります。 はじめてのかたは", 505 | "'Melody' are registered. If you are a first time user,", 506 | ), 507 | ( 508 | "プリセットをもとに きょくをつくってみましょう。", 509 | "create a song based on the presets.", 510 | ), 511 | ("トランスポーズ", "Transpose"), 512 | ("へんせい", "Instrumentation"), 513 | ("テンポ", "Tempo"), 514 | ("コードしんこう", "Chord Progression"), 515 | ("ベース パターン", "Bass Patterns"), 516 | ("ベース クオンタイズ", "Base Quantize"), 517 | ("ドラム パターン", "Drums Patterns"), 518 | ( 519 | "「No drums」をせんたくすると ドラムパートのかわりに", 520 | "When 'No drums' is selected, ", 521 | ), 522 | ( 523 | "メロディにリバーブがかかります。", 524 | "reverb is applied to the melody instead of the drum part.", 525 | ), 526 | ("ねいろ(メイン)", "Tone (1st channel)"), 527 | ("ねいろ(サブ)", "Tone (2nd channel)"), 528 | ("おとのたかさ(さいていおん)", "Sound Height (lowest note)"), 529 | ("おんぷのかず", "Number of notes"), 530 | ("16ぶおんぷをつかう?", "Use 16th notes?"), 531 | ("", ""), 532 | ( 533 | "【ローカルでうごかしているばあい】", 534 | "[When running in a local environment]", 535 | ), 536 | ( 537 | " exportフォルダに music.json/wav/mid を", 538 | " 'music.json/wav/mid' in the export folder.", 539 | ), 540 | (" ほぞんしました。", ""), 541 | ( 542 | " exportフォルダに music.json/wav をほぞんしました。", 543 | " 'music.json/wav' in the export folder.", 544 | ), 545 | ("", ""), 546 | ("【ブラウザでうごかしているばあい】", "[When running in a browser]"), 547 | ( 548 | " music.json/wav がダウンロードされます。", 549 | " 'music.json/wav' will be downloaded.", 550 | ), 551 | ( 552 | "フルへんせいのばあいは4チャンネル、", 553 | "Use 4 channels for full instrumentation,", 554 | ), 555 | ( 556 | "それいがいは3チャンネルをつかいます。", 557 | "3 channels for everything else.", 558 | ), 559 | ] 560 | lang = self.parm["language"] 561 | text = list_text[value][lang] 562 | width = 4 if lang == 0 else 2 563 | return text, len(text) * width 564 | 565 | def generate_music(self, make_melody=True): 566 | px.stop() 567 | parm = self.parm 568 | base = self.generator["base"][parm["base"]] 569 | drums = self.generator["drums"][parm["drums"]] 570 | # コードリスト準備 571 | self.set_chord_lists() 572 | # バッキング生成 573 | items = [] 574 | self.base_notes = [] 575 | self.cur_chord_idx = -1 576 | for loc in range(self.total_len): 577 | items.append([None for _ in range(19)]) 578 | (chord_idx, _) = self.get_chord(loc) 579 | if chord_idx > self.cur_chord_idx: 580 | chord_list = self.chord_lists[chord_idx] 581 | self.cur_chord_idx = chord_idx 582 | self.cur_chord_loc = loc 583 | item = items[loc] 584 | tick = loc % 16 # 拍(0-15) 585 | if loc == 0: # 最初の行(セットアップ) 586 | item[0] = parm["speed"] # テンポ 587 | item[1] = 48 # 4/4拍子 588 | item[2] = 3 # 16分音符 589 | item[3] = list_tones[parm["melo_tone"]][0] # メロディ音色 590 | item[4] = 6 # メロディ音量 591 | item[5] = 14 # メロディ音長 592 | item[7] = 7 # ベース音色 593 | item[8] = 7 # ベース音量 594 | item[9] = parm["base_quantize"] # ベース音長 595 | if self.with_submelody: 596 | item[11] = list_tones[parm["sub_tone"]][0] 597 | item[12] = 4 598 | item[13] = 15 599 | if self.with_drum: 600 | item[15] = 15 601 | item[16] = 5 602 | item[17] = 15 603 | elif self.with_drum: 604 | item[11] = 15 605 | item[12] = 5 606 | item[13] = 15 607 | else: # リバーブ 608 | item[11] = item[3] 609 | item[12] = 2 610 | item[13] = item[5] 611 | 612 | # ベース音設定 613 | if not chord_list["repeat"] is None: 614 | repeat_loc = self.chord_lists[chord_list["repeat"]]["loc"] 615 | target_loc = repeat_loc + loc - self.cur_chord_loc 616 | item[10] = items[target_loc][10] 617 | else: 618 | pattern = "basic" if loc // 16 < 7 else "final" 619 | base_str = base[pattern][tick] 620 | if base_str == ".": 621 | base_note = None 622 | elif base_str == "0": 623 | base_note = -1 624 | else: 625 | highest = parm["base_highest_note"] 626 | pattern = "basic" if loc // 16 < 7 else "final" 627 | base_root = 12 + parm["transpose"] + chord_list["base"] 628 | while base_root + 24 > highest: 629 | base_root -= 12 630 | notes = chord_list["notes_origin"] 631 | adjust_list = [0, -1, 1, -2, 2, -3, 3] 632 | adjust_idx = 0 633 | base_add = {"1": 7, "2": 12, "3": 19, "4": 24}[base_str] 634 | while notes: 635 | adjust = adjust_list[adjust_idx] 636 | base_note = base_root + base_add + adjust 637 | tmp = (base_note + parm["transpose"]) % 12 638 | # print(notes, tmp, notes[tmp]) 639 | if notes[(base_note + parm["transpose"]) % 12] in [1, 2, 3]: 640 | break 641 | adjust_idx += 1 642 | item[10] = base_note 643 | self.base_notes.append(base_note) 644 | # ドラム音設定 645 | if self.with_drum: 646 | pattern = "basic" if (loc // 16) % 4 < 3 else "final" 647 | idx = 18 if self.with_submelody else 14 648 | drum_str = drums[pattern][tick] 649 | if drum_str == "0": 650 | item[idx] = None 651 | else: 652 | item[idx] = ":" + drum_str 653 | # メロディー生成 654 | failure_cnt = 0 655 | while make_melody: 656 | self.generate_melody() 657 | if self.check_melody(): 658 | break 659 | failure_cnt += 1 660 | self.set_chord_lists() 661 | # print("--------失敗---------") 662 | # print("失敗回数", failure_cnt) 663 | # メロディ・サブとリバーブの音符を設定 664 | for loc in range(self.total_len): 665 | item = items[loc] 666 | item[6] = self.melody_notes[loc] 667 | if self.with_submelody: 668 | item[14] = self.submelody_notes[loc] 669 | elif not self.with_drum: # リバーブ 670 | item[14] = self.melody_notes[ 671 | (loc + self.total_len - 1) % self.total_len 672 | ] 673 | # 完了処理 674 | self.music = sounds.compile(items, self.tones, self.patterns) 675 | self.items = items 676 | 677 | # self.chord_listsを生成 678 | def set_chord_lists(self): 679 | chord = self.generator["chords"][self.parm["chord"]] 680 | self.chord_lists = [] 681 | for progression in chord["progression"]: 682 | chord_list = { 683 | "loc": progression["loc"], 684 | "base": 0, 685 | "no_root": False, 686 | "notes": [], 687 | "notes_origin": [], 688 | "repeat": progression["repeat"] if "repeat" in progression else None, 689 | } 690 | if "repeat" in progression: 691 | chord_list["base"] = self.chord_lists[progression["repeat"]]["base"] 692 | if "notes" in progression: 693 | notes = [int(s) for s in progression["notes"]] 694 | chord_list["notes_origin"] = notes 695 | note_chord_cnt = 0 696 | # ベース音設定 697 | for idx in range(12): 698 | if notes[idx] == 2: 699 | chord_list["base"] = idx 700 | if notes[idx] in [1, 2, 3]: 701 | note_chord_cnt += 1 702 | chord_list["no_root"] = note_chord_cnt > 3 703 | # レンジを決める 704 | if self.with_submelody: 705 | chord_list["notes"] = self.make_chord_notes(notes, SUBMELODY_DIFF) 706 | else: 707 | chord_list["notes"] = self.make_chord_notes(notes) 708 | self.chord_lists.append(chord_list) 709 | 710 | # コードリストの音域設定 711 | def make_chord_notes(self, notes, tone_up=0): 712 | parm = self.parm 713 | note_highest = None 714 | idx = 0 715 | results = [] 716 | while True: 717 | note_type = int(notes[idx % 12]) 718 | note = 12 + idx + parm["transpose"] 719 | if note >= parm["melo_lowest_note"] + tone_up: 720 | if note_type in [1, 2, 3, 9]: 721 | results.append((note, note_type)) 722 | if note_highest is None: 723 | note_highest = note + 15 724 | if note_highest and note >= note_highest: 725 | break 726 | idx += 1 727 | return results 728 | 729 | # メロディ生成 730 | def generate_melody(self): 731 | self.melody_notes = [-2 for _ in range(self.total_len)] 732 | self.submelody_notes = [-2 for _ in range(self.total_len)] 733 | # メインメロディ 734 | # print("=== MAIN START ===") 735 | rhythm_main_list = [] 736 | for _ in range(5): 737 | rhythm_main_list.append(self.get_rhythm_set()) 738 | rhythm_main_list.sort(key=len) 739 | rhythm_main = rhythm_main_list[self.parm["melo_density"]] 740 | for loc in range(self.total_len): 741 | # すでに埋まっていたらスキップ 742 | if self.melody_notes[loc] != -2: 743 | continue 744 | # 1セットの音を追加 745 | notesets = self.get_next_notes(rhythm_main, loc) 746 | if notesets is None: # repeat 747 | repeat_loc = self.chord_lists[self.chord_list["repeat"]]["loc"] 748 | target_loc = repeat_loc + loc - self.cur_chord_loc 749 | repeat_note = self.melody_notes[target_loc] 750 | self.put_melody(loc, repeat_note, 1) 751 | repeat_subnote = self.submelody_notes[target_loc] 752 | self.submelody_notes[loc] = repeat_subnote 753 | else: 754 | notesets_len = 0 755 | for noteset in notesets: 756 | self.put_melody(noteset[0], noteset[1], noteset[2]) 757 | notesets_len += noteset[2] 758 | self.put_submelody(loc, -2, notesets_len) 759 | # サブメロディ 760 | # print("=== SUB START ===") 761 | rhythm_sub = self.get_rhythm_set(True) 762 | prev_note_loc = -1 763 | for loc in range(self.total_len): 764 | note = self.submelody_notes[loc] 765 | if not note is None and note >= 0: 766 | prev_note_loc = loc 767 | self.prev_note = note 768 | elif loc - prev_note_loc >= 4 and loc % 4 == 0: 769 | notesets = self.get_next_notes(rhythm_sub, loc, True) 770 | if not notesets is None: 771 | for noteset in notesets: 772 | self.put_submelody(noteset[0], noteset[1], noteset[2]) 773 | prev_note_loc = loc 774 | 775 | # メロディのリズムを取得 776 | def get_rhythm_set(self, is_sub=False): 777 | self.cur_chord_idx = -1 # 現在のコード(self.chord_listsのインデックス) 778 | self.cur_chord_loc = -1 # 現在のコードの開始位置 779 | self.is_repeat = False # リピートモード 780 | self.chord_list = [] 781 | self.prev_note = -1 # 直前のメロディー音 782 | self.first_in_chord = True # コード切り替え後の最初のノート 783 | results = [] 784 | used16 = False 785 | while True: 786 | for bar in range(BARS_NUMBERS): 787 | if is_sub: 788 | pat_line = SUB_RHYTHM 789 | else: 790 | while True: 791 | pat_line = self.melo_rhythm[ 792 | px.rndi(0, len(self.melo_rhythm) - 1) 793 | ] 794 | # 16分音符回避設定 795 | if self.has_16th_note(pat_line): 796 | if not self.parm["melo_use16"]: 797 | continue 798 | used16 = True 799 | # 先頭が持続音のものは避ける(暫定) 800 | if not pat_line[0] is None: 801 | break 802 | for idx, pat_one in enumerate(pat_line): 803 | loc = bar * 16 + idx 804 | if not pat_one is None: 805 | results.append((loc, pat_one)) 806 | if is_sub or not self.parm["melo_use16"] or used16: 807 | break 808 | for _ in range(2): 809 | results.append((self.total_len, -1)) 810 | return results 811 | 812 | # 16分音符が含まれるか 813 | def has_16th_note(self, line): 814 | prev_str = None 815 | for i in line: 816 | if i == 0 and prev_str == 0: 817 | return True 818 | prev_str = i 819 | return False 820 | 821 | def get_next_notes(self, rhythm_set, loc, is_sub=False): 822 | pat = None 823 | for pat_idx, rhythm in enumerate(rhythm_set): 824 | if loc == rhythm[0]: 825 | pat = rhythm[1] 826 | break 827 | elif loc < rhythm[0]: 828 | break 829 | note_len = rhythm_set[pat_idx + 1][0] - loc 830 | # コード切替判定 831 | change_code = False 832 | premonitory = False 833 | (next_chord_idx, next_chord_loc) = self.get_chord(loc) 834 | if next_chord_idx > self.cur_chord_idx: 835 | change_code = True 836 | elif not self.is_repeat and loc + note_len > next_chord_loc: 837 | (next_chord_idx, next_chord_loc) = self.get_chord(loc + note_len) 838 | change_code = True 839 | premonitory = True 840 | # print(loc, note_len, "先取音発生") 841 | if change_code: 842 | self.chord_list = self.chord_lists[next_chord_idx] 843 | self.cur_chord_idx = next_chord_idx 844 | self.cur_chord_loc = loc 845 | self.first_in_chord = True 846 | self.is_repeat = not self.chord_list["repeat"] is None 847 | # 小節単位の繰り返し 848 | if self.is_repeat: 849 | # print(loc, "repeat") 850 | return [] if premonitory else None 851 | if pat == -1: # 休符 852 | # print(loc, "休符") 853 | return [(loc, -1, note_len)] 854 | # 初期処理 855 | self.chord_notes = self.chord_list["notes"] 856 | next_idx = self.get_target_note(is_sub, loc) 857 | # 連続音を何個置けるか(コード維持&4分音符以下) 858 | following = [] 859 | prev_loc = loc 860 | while True: 861 | pat_loc = rhythm_set[pat_idx + 1 + len(following)][0] 862 | no_next = pat_loc >= next_chord_loc or pat_loc - prev_loc > 4 863 | if not following or not no_next: 864 | following.append((prev_loc, pat_loc - prev_loc)) 865 | if no_next: 866 | break 867 | prev_loc = pat_loc 868 | loc, note_len = following[0] 869 | # 直前のメロディーのインデックスを今のコードリストと照合(構成音から外れていたらNone) 870 | cur_idx = None 871 | if not premonitory: 872 | for idx, note in enumerate(self.chord_notes): 873 | if self.prev_note == note[0]: 874 | cur_idx = idx 875 | break 876 | # 初音(直前が休符 or コード構成音から外れた場合は、コード構成音を取得) 877 | if self.prev_note < 0 or cur_idx is None: 878 | # print(loc, "初音") 879 | note = self.chord_notes[next_idx][0] 880 | return [(loc, note, note_len)] 881 | # 各種変数準備 882 | results = [] 883 | diff = abs(next_idx - cur_idx) 884 | direction = 1 if next_idx > cur_idx else -1 885 | # 刺繍音/同音 886 | if diff == 0: 887 | cnt = len(following) // 2 888 | if cnt and px.rndi(0, 1) and not is_sub: 889 | # print(loc, "刺繍音", cnt * 2) 890 | for i in range(cnt): 891 | while next_idx == cur_idx: 892 | next_idx = self.get_target_note() 893 | direction = 1 if next_idx > cur_idx else -1 894 | # print(cur_idx, next_idx, self.chord_notes) 895 | note = self.chord_notes[cur_idx + direction][0] 896 | prev_note = self.prev_note 897 | note_follow = following[i * 2] 898 | results.append((note_follow[0], note, note_follow[1])) 899 | note_follow = following[i * 2 + 1] 900 | results.append((note_follow[0], prev_note, note_follow[1])) 901 | return results 902 | else: 903 | # print(loc, "同音") 904 | return [(loc, self.prev_note, note_len)] 905 | # ステップに必要な長さが足りない/跳躍量が大きい/割合で跳躍音採用 906 | if abs(next_idx - cur_idx) > len(following): 907 | note = self.chord_notes[next_idx][0] 908 | # print(loc, "跳躍") 909 | return [(loc, note, note_len)] 910 | # ステップ 911 | # print(loc, "ステップ", abs(next_idx - cur_idx)) 912 | i = 0 913 | while next_idx != cur_idx: 914 | cur_idx += direction 915 | note = self.chord_notes[cur_idx][0] 916 | note_follow = following[i] 917 | results.append((note_follow[0], note, note_follow[1])) 918 | i += 1 919 | return results 920 | 921 | # メロディ検査(コード中の重要構成音が入っているか) 922 | def check_melody(self): 923 | cur_chord_idx = -1 924 | need_notes_list = [] 925 | for loc in range(self.total_len): 926 | (next_chord_idx, _) = self.get_chord(loc) 927 | if next_chord_idx > cur_chord_idx: 928 | # need_notes_listが残っている=重要構成音が満たされていない 929 | if len(need_notes_list) > 0: 930 | return False 931 | cur_chord_idx = next_chord_idx 932 | notes_list = self.chord_lists[cur_chord_idx]["notes"] 933 | need_notes_list = [] 934 | for chord in notes_list: 935 | note = chord[0] % 12 936 | if chord[1] == 1 and not note in need_notes_list: 937 | need_notes_list.append(note) 938 | note = self.melody_notes[loc] 939 | if not note is None and note >= 0 and note % 12 in need_notes_list: 940 | need_notes_list.remove(note % 12) 941 | if self.with_submelody: 942 | note = self.submelody_notes[loc] 943 | if not note is None and note >= 0 and note % 12 in need_notes_list: 944 | need_notes_list.remove(note % 12) 945 | return True 946 | 947 | # コードリスト取得(locがchords_listsの何番目のコードか、次のコードの開始位置を返す) 948 | def get_chord(self, loc): 949 | chord_lists_cnt = len(self.chord_lists) 950 | next_chord_loc = 16 * BARS_NUMBERS 951 | for rev_idx in range(chord_lists_cnt): 952 | idx = chord_lists_cnt - rev_idx - 1 953 | if loc >= self.chord_lists[idx]["loc"]: 954 | break 955 | else: 956 | next_chord_loc = self.chord_lists[idx]["loc"] 957 | return idx, next_chord_loc 958 | 959 | # 跳躍音の跳躍先を決定 960 | def get_target_note(self, is_sub=False, loc=None): 961 | no_root = self.first_in_chord or self.chord_list["no_root"] 962 | allowed_types = [1, 3] if no_root else [1, 2, 3] 963 | notes = self.get_subnotes(loc) if is_sub else self.chord_list["notes"] 964 | # 跳躍先候補が1オクターブ超のみの場合、最低音を選択 965 | hightest_note = 0 966 | hightest_idx = 0 967 | for idx, noteset in enumerate(notes): 968 | if noteset[0] > hightest_note and noteset[1] in allowed_types: 969 | hightest_note = noteset[0] 970 | hightest_idx = idx 971 | if self.prev_note - hightest_note > 12: 972 | return hightest_idx 973 | while True: 974 | idx = px.rndi(0, len(notes) - 1) 975 | if not notes[idx][1] in allowed_types: 976 | continue 977 | note = notes[idx][0] 978 | if self.prev_note >= 0: 979 | diff = abs(self.prev_note - note) 980 | if diff > 12: 981 | continue 982 | factor = diff if diff != 12 else diff - 6 983 | # 近い音ほど出やすい(オクターブ差は補正、サブはそうではない) 984 | if px.rndi(0, 15) < factor and not is_sub: 985 | continue 986 | return idx 987 | 988 | # メロディのトーンを配置 989 | def put_melody(self, loc, note, note_len=1): 990 | for idx in range(note_len): 991 | self.melody_notes[loc + idx] = note if idx == 0 else None 992 | if note is not None: 993 | self.prev_note = note 994 | self.first_in_chord = False 995 | 996 | # サブメロディのトーンを配置 997 | def put_submelody(self, loc, note, note_len=1): 998 | master_note = None 999 | subnote = note 1000 | master_loc = loc 1001 | while master_loc >= 0: 1002 | master_note = self.melody_notes[master_loc] 1003 | if not master_note is None and master_note >= 0: 1004 | prev_note = master_note if note == -2 else note 1005 | subnote = self.search_downer_note(prev_note, master_note, master_loc) 1006 | break 1007 | master_loc -= 1 1008 | prev_subnote = None 1009 | for idx in range(note_len): 1010 | if ( 1011 | not self.melody_notes[loc + idx] is None 1012 | and self.melody_notes[loc + idx] >= 0 1013 | ): 1014 | master_note = self.melody_notes[loc + idx] 1015 | duplicate = ( 1016 | not master_note is None 1017 | and not subnote is None 1018 | and (abs(subnote > master_note) < 3) 1019 | ) 1020 | if duplicate: 1021 | subnote = self.search_downer_note(subnote, master_note, loc + idx) 1022 | self.submelody_notes[loc + idx] = ( 1023 | subnote if subnote != prev_subnote else None 1024 | ) 1025 | prev_subnote = subnote 1026 | 1027 | # メロの下ハモを探す 1028 | def search_downer_note(self, prev_note, master_note, loc): 1029 | if self.with_submelody and master_note >= 0: 1030 | notes = self.get_subnotes(loc) 1031 | if not prev_note is None and abs(prev_note - master_note) >= 3: 1032 | return prev_note 1033 | cur_note = master_note - 3 1034 | while cur_note >= self.parm["melo_lowest_note"]: 1035 | for n in notes: 1036 | if n[0] == cur_note and n[1] in [1, 2, 3]: 1037 | return cur_note 1038 | cur_note -= 1 1039 | return -1 1040 | 1041 | # サブパートの許容音域を取得 1042 | def get_subnotes(self, start_loc): 1043 | master_note = None 1044 | base_note = None 1045 | loc = start_loc 1046 | while master_note is None or base_note is None: 1047 | if master_note is None and self.melody_notes[loc] != -1: 1048 | master_note = self.melody_notes[loc] 1049 | if base_note is None and self.base_notes[loc] != -1: 1050 | base_note = self.base_notes[loc] 1051 | loc = (loc + self.total_len - 1) % self.total_len 1052 | notes = self.chord_list["notes_origin"].copy() 1053 | results = [] 1054 | has_important_tone = False 1055 | idx = 0 1056 | while notes: 1057 | note_type = notes[idx % 12] 1058 | if note_type in [1, 2, 3, 9]: 1059 | note = 12 + idx + self.parm["transpose"] 1060 | # ベース+3〜メイン-3までを許可する 1061 | if note > master_note - 3 and has_important_tone: 1062 | break 1063 | if note >= base_note + 3: 1064 | results.append((note, note_type)) 1065 | if note_type in [1, 3]: 1066 | has_important_tone = True 1067 | idx += 1 1068 | self.chord_notes = results 1069 | return results 1070 | 1071 | 1072 | App() 1073 | --------------------------------------------------------------------------------