# ==========================================================
# @brief パーカッション生成ツール.
# @note 機能概要
#
# @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
import random
from typing import List
MAX_VELOCITY = 10
# リズムの定義 (Velocityの倍率。最大は10)
PATTERN_TBL = {
# ----------------------1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
"4x4": [10, 0, 0, 0] * 4,
"OFF BEAT": [0, 0, 10, 0] * 4,
"HI-HAT 0011": [0, 0, 10, 7] * 4,
"HI-HAT 0111": [0, 7, 10, 7] * 4,
"HI-HAT 1011": [7, 0, 10, 7] * 4,
"HI-HAT 1111": [3, 7, 10, 7] * 4,
"332 x2": [10, 0, 0, 10, 0, 0, 10, 0] * 2,
"333322": [10, 0, 0, 10, 0, 0, 10, 0, 0, 10, 0, 0, 10, 0, 10, 0],
"334 24": [10, 0, 0, 10, 0, 0, 10, 0, 0, 0, 10, 0, 10, 0, 0, 0],
"JERSEY CLUB": [10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 10, 0, 0, 10, 0],
"FILL 8": [10, 0] * 8,
"FILL 16": [10] * 16,
"FILL 3": [10, 10, 0, 10, 10, 0, 10, 10, 0, 10, 10, 0, 10, 10, 0, 10],
}
# ピッチ.
PITCH_C2 = 12 * 2
PITCH_C3 = 12 * 3
PITCH_C4 = 12 * 4
PITCH_C5 = 12 * 5
PITCH_C6 = 12 * 6
PITCH_C7 = 12 * 7
PITCH_C8 = 12 * 8
# 音の長さ (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
# ピッチオフセット
MAX_PITCH_OFS = 120 # 最大.
MIN_PITCH_OFS = -120 # 最小.
# ラベル.
LABEL_PITCH = 'Pitch'
LABEL_BEATS = '4 beats x'
LABEL_PATTERN = 'Pattern'
LABEL_PATTERN_ROT = 'Rotation'
LABEL_VELOCITY = 'Velocity'
LABEL_DURATION = 'Duration'
LABEL_MOD_X = 'Mod X'
LABEL_MOD_Y = 'Mod Y'
LABEL_RND_VEL = 'Random Velocity'
LABEL_RND_PAN = 'Random Pan'
LABEL_RND_NOTE = 'Random Note'
LABEL_RND_NOTE_VEL = 'Rnd Note Velocity'
LABEL_TRIPLETS = 'Triplets'
# 抽選する.
def rand_choice(v:float) -> bool:
""" vの確率でTrueを返す """
if v <= 0.0:
return False # 確率0%はFalse.
if v >= 1.0:
return True # 確率100%はTrue.
# 確率計算.
return random.random() < v
def rand_int(a:int, b:int) -> int:
""" a <= rnd <= b のランダムな整数値を返す """
return random.randint(a, b)
def rand_range(a:float, b:float) -> float:
""" a <= rnd <= b のランダムな小数を返す """
return random.uniform(a, b)
def clamp(v, a, b):
""" a <= v <= b の範囲になるように clampする """
return a if v < a else b if v > b else v
# 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 Pattern:
def __init__(self, vel:float):
self.vel = 1.0 * vel / MAX_VELOCITY # 0.0〜1.0に正規化.
def is_valid(self) -> bool:
# velocityが0より大きければ有効.
return self.vel > 0
def createDialog():
""" ダイアログ生成"""
form = ScriptDialog('Percussion tool', '')
# ボタン追加.
form.AddInputKnobInt(LABEL_PITCH, PITCH_C5, PITCH_C2, PITCH_C8)
form.AddInputKnobInt(LABEL_BEATS, 1, 1, 5)
form.AddInputCombo(LABEL_PATTERN, PATTERN_TBL.keys(), 3)
form.AddInputKnobInt(LABEL_PATTERN_ROT, 0, -15, 15)
form.AddInputKnob(LABEL_VELOCITY, 0.8, 0.0, 1.0)
form.AddInputKnob(LABEL_DURATION, 0.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_RND_VEL, 0.0, 0.0, 1.0)
form.AddInputKnob(LABEL_RND_PAN, 0.0, 0.0, 1.0)
form.AddInputKnob(LABEL_RND_NOTE, 0.0, 0.0, 1.0)
form.AddInputKnob(LABEL_RND_NOTE_VEL, 0.4, 0.0, 1.0)
form.AddInputCombo(LABEL_TRIPLETS, TRIPLETS_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:int) -> List[int]:
""" 辞書型からインデックス指定で値を取得する (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 PercNote:
def __init__(self, max_bars):
self.note = Note()
self.max_bars = max_bars
def duplicate(self):
""" 複製を返す """
ret = PercNote(self.max_bars)
ret.note = self.note.clone()
return ret
# Percussion Tool.
class Perc:
def __init__(self, form):
self.form = form
self.max_steps = self.getInputValue(LABEL_BEATS) * 16
def next_step(self):
# 次のステップに進む.
pass
def getInputValue(self, s:str):
""" form から値を取得 """
return self.form.GetInputValue(s)
def create_note(self, step:int, ptn:Pattern) -> PercNote:
""" ノート生成 """
if self.is_skip_triplets(step):
return None # スキップするステップ.
tick = self.getInputValue(LABEL_DURATION)
max_bars = self.getInputValue(LABEL_BEATS)
# ノート生成.
n = PercNote(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) * ptn.vel
n.note.fcut = self.getInputValue(LABEL_MOD_X)
n.note.fres = self.getInputValue(LABEL_MOD_Y)
# ランダム処理.
rnd_vel = self.getInputValue(LABEL_RND_VEL)
if rnd_vel > 0:
# ランダムVelocity.
v = n.note.velocity + 0.5 * rand_range(-1.0, 1.0) * rnd_vel
n.note.velocity = clamp(v, 0.0, 1.0)
rnd_pan = self.getInputValue(LABEL_RND_PAN)
if rnd_pan > 0:
# ランダムPAN.
n.note.pan = 0.5 + 0.5 * rand_range(-1.0, 1.0) * rnd_pan
return n
def interval_to_pitch(self, idx:int) -> int:
""" ピッチの取得 """
pitch = self.getInputValue(LABEL_PITCH)
return pitch
def get_pattern(self) -> List[Pattern]:
""" パターンの取得 """
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の値だけ繰り返す.
bar_mul = self.form.GetInputValue(LABEL_BEATS)
# ランダムノートの出現割合.
rnd_note = self.getInputValue(LABEL_RND_NOTE)
rnd_note_vel = self.getInputValue(LABEL_RND_NOTE_VEL)
# Patternデータに変換
ret:List[Pattern] = []
for d in pattern * bar_mul:
if d == 0:
# 休符.
if rand_choice(rnd_note):
# ランダムノートの確率で休符をノートにする.
d = int(MAX_VELOCITY * rnd_note_vel)
ptn = Pattern(d)
ret.append(ptn)
return ret
def is_triplets(self) -> bool:
""" 3連符モードかどうか """
triplets = self.form.GetInputValue(LABEL_TRIPLETS)
if triplets == Triplets.NONE:
return False # 3連符モードでない.
return True # 3連符モード.
def is_skip_triplets(self, step:int) -> bool:
""" トリプレットのステップ数かどうか """
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()
perc = Perc(form)
for step, ptn in enumerate(perc.get_pattern()):
# ノート生成.
n = perc.create_note(step, ptn)
if n is None:
# スキップノート.
perc.next_step()
continue
# ピッチを設定.
n.note.number = perc.getInputValue(LABEL_PITCH)
if ptn.is_valid():
# 休符でない
n.note.number = perc.interval_to_pitch(step)
score.addNote(n.note) # スコアに追加.
else:
# 休符.
pass
perc.next_step()