# ==========================================================
# @brief Psytrance生成ツール.
# @note 機能概要
# - ルートの音程と音階を指定してPiano rollのノートを生成します
# - Patternはリズム、Melodyはリズムに対する相対的なスケールの値です
# - リズムは1小節を16ステップ(16分音符)として構成します
# - リズムとメロディは "Rotation" により回転することができます
# - 3連符は1拍の特定の音を間引くことで実現しています
# - 特定の小節をミュートできます
# - またミュートした小節にピッチダウンを追加できます
#
# @author FL Studio 非公式 wiki (https://w.atwiki.jp/flstudio2/)
# @license MIT
# 再配布は自由です。
# ただし Image-Line社 (https://www.image-line.com) 以外が
# 有料の販売物に含めることは禁止します
# ==========================================================
from flpianoroll import *
from enum import IntEnum
# スケールの定義。要素数は "7音階" 固定.
SCALE_TBL = {
# --------------------- 1 2 3 4 5 6 7
"Phrygian": [0, 1, 3, 5, 7, 8, 10],
"Phrygian Dominant": [0, 1, 4, 5, 7, 8, 10],
"Minor Hungarian": [0, 2, 3, 6, 7, 8, 11],
"Japanese Insen": [0, 1, 5, 7, 10, 10, 10], # 5音階
"Arabic": [0, 1, 4, 5, 7, 8, 11],
}
# リズムの定義 (0:休符 1:ルート音 8:+1オクターブ)
PATTERN_TBL = {
"OFF BEAT": [0, 0, 1, 0] * 4,
"GALLOP 1": [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1],
"GALLOP 2": [0, 0, 1, 0, 0, 0, 1, 1] * 2,
"GALLOP 3": [0, 0, 1, 1] * 4,
"ROLLING 1": [0, 1, 1, 1] * 4,
"ROLLING 2": [0, 1, 8, 8] * 4,
"ROLLING 3": [0, 1, 8, 1, 0, 1, 8, 8] * 2,
"ROLLING 4": [0, 1, 8, 1, 0, 8, 1, 8] * 2,
"BOUNCE 1": [0, 0, 0, 1, 0, 0, 1, 0] * 2,
"BOUNCE 2": [0, 0, 1, 0, 0, 1, 0, 1] * 2,
"332 x2 (6 steps)": [1, 0, 0, 1, 0, 0, 1, 0] * 2,
"332 x2 (8 steps)": [1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0],
"333322 (6 steps)" : [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0],
"333322 (10 steps)": [1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0],
"33334": [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0],
"JERSEY CLUB 1": [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0],
"JERSEY CLUB 2": [1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0],
"JERSEY CLUB 3": [0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0],
}
# メロディの定義 (値はリズムからの相対値).
MELODY_TBL = {
# ---------------- 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
"None": [0] * 16,
"Bass 1 (6 x2)": [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, -1],
"Bass 2 (6 x2)": [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -1],
"Bass 3 (6 x2)": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1],
"Bass 4 (6 x4)": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1],
"Bass 5 (4 x2)": [0, 0, 0, 1, 0, 0, 0, -1],
"Bass 6 (4 x2)": [0, 0, 0, 1, 0, 0, -3, -1],
"Bass 7 (5 x2)": [0, 0, 0, 0, 1, 0, 0, 0, 0, -1],
"Lead 1 (6 x2)": [0, 0, 0, 1, 0, -1, 0, 0, 0, 1, 2, 1],
"Lead 2 (6 x2)": [0, 0, 0, 1, 2, 3, 0, 0, 0, 1, -1, -3],
"Lead 1 (5 x2)": [0, 0, 1, 0, -1, 0, 0, 1, 2, 1],
"Lead 2 (5 x2)": [0, 0, 1, 2, 3, 0, 0, 1, -1, -3],
"Lead 1 (8 x2)": [0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, -1, -2, -1],
"Lead 2 (8 x2)": [0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 0, 0, 0, -1, -3, -1],
"Lead 3 (8 x2)": [0, 0, 0, 0, 0, 2, 3, 1, 0, 0, 0, 0, 0, -1, 1, 0],
}
# 音の長さ (FLは1小節あたり384).
TICK_PPQ = 96
TICK_4 = TICK_PPQ // 1
TICK_8 = TICK_PPQ // 2
TICK_12 = TICK_PPQ // 3 # Triplets
TICK_16 = TICK_PPQ // 4
TICK_32 = TICK_PPQ // 8
TICK_64 = TICK_PPQ // 16
TICK_128 = TICK_PPQ // 32
# ラベル.
LABEL_ROOT = 'Root'
LABEL_OCTAVE = 'Octave'
LABEL_SCALE = 'Scale'
LABEL_BEATS = '4 beats x'
LABEL_PATTERN = 'Pattern'
LABEL_MELODY = 'Melody'
LABEL_PATTERN_ROT = 'Rotation'
LABEL_MELODY_ROT = 'Melody Rot'
LABEL_VELOCITY = 'Velocity'
LABEL_DURATION = 'Duration'
LABEL_MOD_X = 'Mod X'
LABEL_MOD_Y = 'Mod Y'
LABEL_FILL = 'Fill -1 oct'
LABEL_TRIPLETS = 'Triplets'
LABEL_MUTE = 'Mute'
LABEL_MUTE_BEND = 'Muted pitch drop'
# 12音階.
NOTE_LIST = ["C", "C#", "D", "D#", "E", "F", "G", "G#", "A", "A#", "B"]
# 3連符の種別.
class Triplets(IntEnum):
NONE = 0
SKIP_1 = 1
SKIP_2 = 2
SKIP_3 = 3
SKIP_4 = 4
TRIPLETS_LIST = ["NONE", "SKIP 1", "★SKIP 2", "SKIP 3", "SKIP 4"]
# 特定の拍を無音にする.
class Mute(IntEnum):
NONE = 0 # 無効.
BEAT_1ST = 1 # 1拍目をミュート.
BEAT_4TH = 2 # 4拍目をミュート.
BEAT_1_2 = 3 # 1〜2拍目をミュート.
def is_mute_step(self, step, max_bars):
""" 指定のステップがミュートに該当するかどうか """
if self == Mute.NONE:
return False # 常にミュート無効.
# 拍と小節数を判定.
beat = (step // 4)%4 + 1
bar = ((step // 4) // 4) + 1
match self:
case Mute.BEAT_1ST: # 1小節目・1拍目をミュート.
if bar != 1:
return False # 1小節目でない.
return beat == 1
case Mute.BEAT_4TH: # 4拍目をミュート.
if bar != max_bars:
return False # 最後の小節でない.
return beat == 4
case Mute.BEAT_1_2: # 1,2拍目をミュート.
if bar != 1:
return False # 1小節目でない.
return beat in [1, 2]
case _:
return False # 該当なしはいったん無効.
MUTE_LIST = ["NONE", "1ST BEAT", "4TH BEAT", "1ST+2ND BEAT"]
# ミュートした小節のピッチ下げるノートの設定.
class MuteBend(IntEnum):
NONE = 0 # 無効
DOWN_1_OCT_NOTE_16 = 1 # 1オクターブ下げる。16分音符.
DOWN_1_OCT_NOTE_8 = 2 # 1オクターブ下げる。8分音符.
DOWN_1_OCT_NOTE_4 = 3 # 1オクターブ下げる。4分音符.
DOWN_2_OCT_NOTE_16 = 4 # 2オクターブ下げる。16分音符.
DOWN_2_OCT_NOTE_8 = 5 # 2オクターブ下げる。8分音符.
DOWN_2_OCT_NOTE_4 = 6 # 2オクターブ下げる。4分音符.
def is_except(self, step, mute_type, max_bars):
""" 除外判定. """
if self == MuteBend.NONE:
return True # 無効なので除外.
if step % 4 != 0:
return True # 拍の頭でない.
# 拍と小節数を判定.
beat = (step // 4) + 1
bar = ((step // 4) // 4) + 1
if mute_type == Mute.BEAT_1_2:
if beat != 1:
return True # 1小節目ではない.
if mute_type == Mute.BEAT_4TH:
if bar != max_bars:
return True # 最後の小節ではない.
return False
def get_oct(self):
""" 下げるオクターブ数を取得する """
if self in [MuteBend.DOWN_1_OCT_NOTE_16, MuteBend.DOWN_1_OCT_NOTE_8, MuteBend.DOWN_1_OCT_NOTE_4]:
return 1
if self in [MuteBend.DOWN_2_OCT_NOTE_16, MuteBend.DOWN_2_OCT_NOTE_8, MuteBend.DOWN_2_OCT_NOTE_4]:
return 2
# 該当なし.
return 0
def get_tick(self):
if self in [MuteBend.DOWN_1_OCT_NOTE_16, MuteBend.DOWN_2_OCT_NOTE_16]:
return TICK_16
if self in [MuteBend.DOWN_1_OCT_NOTE_8, MuteBend.DOWN_2_OCT_NOTE_8]:
return TICK_8
if self in [MuteBend.DOWN_1_OCT_NOTE_4, MuteBend.DOWN_2_OCT_NOTE_4]:
return TICK_4
# 該当なし.
return 0
MUTE_BEND_LIST = ["None", "-1 oct / 16 note", "-1 oct / 8 note", "-1 oct / 4 note", "-2 oct / 16 note", "-2 oct / 8 note", "-2 oct / 4 note", ]
def createDialog():
""" ダイアログ生成"""
form = ScriptDialog('Psytrance tool', '')
# ボタン追加.
form.AddInputCombo(LABEL_ROOT, NOTE_LIST, 1)
form.AddInputKnobInt(LABEL_OCTAVE, 4, 2, 7)
form.AddInputCombo(LABEL_SCALE, SCALE_TBL.keys(), 0)
form.AddInputKnobInt(LABEL_BEATS, 1, 1, 5)
form.AddInputCombo(LABEL_PATTERN, PATTERN_TBL.keys(), 0)
form.AddInputCombo(LABEL_MELODY, MELODY_TBL.keys(), 0)
form.AddInputKnobInt(LABEL_PATTERN_ROT, 0, -15, 15)
form.AddInputKnobInt(LABEL_MELODY_ROT, 0, -32, 32)
form.AddInputKnob(LABEL_VELOCITY, 0.8, 0.0, 1.0)
form.AddInputKnob(LABEL_DURATION, 16/64.0, 0.0, 1.0)
form.AddInputKnob(LABEL_MOD_X, 0.5, 0.0, 1.0)
form.AddInputKnob(LABEL_MOD_Y, 0.5, 0.0, 1.0)
form.AddInputKnob(LABEL_FILL, 0.0, 0.0, 1.0)
form.AddInputCombo(LABEL_TRIPLETS, TRIPLETS_LIST, 0)
form.AddInputCombo(LABEL_MUTE, MUTE_LIST, 0)
form.AddInputCombo(LABEL_MUTE_BEND, MUTE_BEND_LIST, 0)
return form
def rotate_list(l, v):
""" リストをvの値で回転する """
v = v%len(l) * -1
return l[v:] + l[:v]
def get_table_value_from_idx(table, idx):
""" 辞書型からインデックス指定で値を取得する (Python3.6以降はこれでOK) """
return list(table.values())[idx]
def normalize_to_range(v, a, b):
"""
a <= v <= b となるように正規化する
Note:
- a > b の場合、範囲を入れ替えて正規化します
- a == b の場合、値は固定値 a になります
"""
d = b - a
if d < 0:
a, b = b, a # a > b の場合は値を入れ替えます
elif d == 0:
return a # a = b の場合は固定値.
return ((v - a) % d) + a
def normalize_to_symmetric_range(v, a):
""" -a <= v <= a となるように正規化する """
return normalize_to_range(v, -a, a)
# ツール用にNoteクラスを拡張
class PsyNote:
def __init__(self, mute_type, mute_bend, max_bars):
self.note = Note()
self.mute_type = Mute(mute_type) # Mute型に変換.
self.mute_bend = MuteBend(mute_bend) # MuteBend型に変換.
self.max_bars = max_bars
def duplicate(self):
""" 複製を返す """
ret = PsyNote(self.mute_type, self.mute_bend, self.max_bars)
ret.note = self.note.clone()
return ret
def is_mute(self, step):
""" 指定のステップがミュートに該当するかどうか """
return self.mute_type.is_mute_step(step, self.max_bars)
def add_bend(self, step):
"""
ベンド処理が必要であればノートを追加
is_mute()で不要なステップが除外されている前提.
"""
# ベンド除外条件.
if self.mute_bend.is_except(step, self.mute_type, self.max_bars):
return # 除外.
tick = self.mute_bend.get_tick()
octave = self.mute_bend.get_oct()
# もとのノートを追加.
self.note.length = tick
score.addNote(self.note)
# ベンドノートを追加.
bend = self.duplicate() # コピーを生成.
bend.note.slide = True # スライドノート.
bend.note.number -= octave * 12 # オクターブ下げる.
score.addNote(bend.note)
# Psytrance Tool.
class Psy:
def __init__(self, form):
self.form = form
self.melody_step = self.getInputValue(LABEL_MELODY_ROT) * -1
def getInputValue(self, s):
""" form から値を取得 """
return self.form.GetInputValue(s)
def create_note(self, step):
""" ノート生成 """
if self.is_skip_triplets(step):
return None # スキップするステップ.
tick = self.getInputValue(LABEL_DURATION)
mute = self.getInputValue(LABEL_MUTE)
mute_bend = self.getInputValue(LABEL_MUTE_BEND)
max_bars = self.getInputValue(LABEL_BEATS)
# ノート生成.
n = PsyNote(mute, mute_bend, max_bars)
# 基本情報を設定.
n.note.time = step * TICK_16
if self.is_triplets():
n.note.time = (step * 3 // 4) * TICK_12 # 3連符.
n.note.length = int(TICK_4 * tick) # 4分音符に対する割合.
n.note.velocity = self.getInputValue(LABEL_VELOCITY)
n.note.fcut = self.getInputValue(LABEL_MOD_X)
n.note.fres = self.getInputValue(LABEL_MOD_Y)
return n
def getScale(self):
""" スケールリストの取得 """
idx = self.getInputValue(LABEL_SCALE)
return get_table_value_from_idx(SCALE_TBL, idx)
def getMelody(self):
""" メロディリストの取得 """
idx = self.form.GetInputValue(LABEL_MELODY)
return get_table_value_from_idx(MELODY_TBL, idx)
def adjust_pitch(self, ptn_idx):
""" 指定の音程に対応するピッチを決める """
# まずはスケールリストを取得.
scale = self.getScale()
size = len(scale)
pitch = 0
# "スケールの数 = 1オクターブ" に正規化.
while ptn_idx >= size:
# 1オクターブ上
pitch += 12
ptn_idx -= size
while ptn_idx < 0:
# 1オクターブ下
pitch -= 12
ptn_idx += size
pitch += scale[ptn_idx]
return pitch
def interval_to_pitch(self, ptn_idx, idx, octave):
""" ピッチの取得 """
ptn_idx -= 1 # パターンは1以上が有効なので-1.
melody = self.getMelody()
melody_len = len(melody)
# メロディのステップを正規化.
self.melody_step = normalize_to_symmetric_range(self.melody_step, melody_len)
ptn_idx += melody[self.melody_step] # 基本ピッチにメロディでオフセット
self.melody_step += 1 # 次の処理で丸める.
# スケールの範囲に合わせてクリップしてピッチを変化させます.
pitch = self.adjust_pitch(ptn_idx)
# オクターブを加算.
return pitch + (12 * octave)
def get_pattern(self):
""" パターンの取得 """
idx = self.form.GetInputValue(LABEL_PATTERN)
rotation = self.form.GetInputValue(LABEL_PATTERN_ROT)
pattern = get_table_value_from_idx(PATTERN_TBL, idx)
if rotation != 0:
# パターンを回転
pattern = rotate_list(pattern, rotation)
bar_mul = self.form.GetInputValue(LABEL_BEATS)
return pattern * bar_mul
def is_triplets(self):
""" 3連符モードかどうか """
triplets = self.form.GetInputValue(LABEL_TRIPLETS)
if triplets == Triplets.NONE:
return False # 3連符モードでない.
return True # 3連符モード.
def is_skip_triplets(self, step):
""" トリプレットのステップ数かどうか """
triplets = self.form.GetInputValue(LABEL_TRIPLETS)
if triplets == Triplets.NONE:
return False # 3連符無効.
if triplets != (step % 4) + 1: # 1拍4ステップで1始まり.
return False # 該当しない.
# 3連符のスキップステップ.
return True
def apply(form):
""" ノートの設定 """
# ノートをすべて消去.
score.clear()
psy = Psy(form)
root = form.GetInputValue(LABEL_ROOT) # ルート音.
octave = form.GetInputValue(LABEL_OCTAVE) # オクターブ数.
fill_length = form.GetInputValue(LABEL_FILL) # 休符を埋める音の長さ.
for step, ptn_idx in enumerate(psy.get_pattern()):
# ノート生成.
n = psy.create_note(step)
if n is None:
# スキップノート.
continue
# ひとまずルートを設定.
n.note.number = root + (octave * 12)
if n.is_mute(step):
# ミュートノート.
# ベンド処理があれば行う.
n.add_bend(step)
continue
if ptn_idx != 0:
# 休符でない
n.note.number = root + psy.interval_to_pitch(ptn_idx, step, octave)
score.addNote(n.note) # スコアに追加.
elif fill_length > 0:
# 休符を埋める.
# ルートの1オクターブ下に追加
n.note.number = root + (octave - 1) * 12
n.note.length = int(TICK_16 * fill_length) # 16分音符に対する割合.
score.addNote(n.note) # スコアに追加.
else:
# 休符.
pass