Psytrance Tool

Psytrance Toolは Psytrance 向けフレーズをお手軽に生成するためのツール (ピアノロール・スクリプト) です。


インストール方法と使い方

以下の添付ファイル (.pyscript) を「[ユーザー]/Documents/Image-Line/FL Studio/Settings/Piano roll scripts」フォルダにコピーします。

使い方
Psytrace Toolスクリプトをインストールすると、ピアノロールのスパナアイコンから「Script」カテゴリに「Psytrance v100」と表示されるのでこれを選びます。

すると「Psytrance Tool」が表示されます。

なお、これはPiano roll script全般に言えることですが、入力ボックスにカーソルが当たっていると SPACEキーで再生できないことがあります。その場合は何もないところをクリックしてフォーカスを解除すると SPACEキーで再生ができるようになります。

パラメータ説明
パラメータの説明は以下のとおりです。
パラメータ名 説明
Root ルートの音程
Octave ルートのオクターブ
Scale 生成するフレーズのスケール。基本的に Phrygianで問題ないです
4bar x 生成するフレーズの小節数
Pattern 生成するリズムのパターン
Melody 生成するメロディの種類
Rotation リズムのローテーション (左右へのシフト)
Melody Rot メロディのローテーション (左右へのシフト)
Velocity 音の強さ
Duration 音の長さ
Mod X Modulation X。またはカットオフ (FL標準シンセのみ有効)
Mod Y Modulation Y。またはレゾナンス (FL標準シンセのみ有効)
Fill -1 oct リズムの休符を1オクターブ下の音で埋めるときの音の長さ
Triplet 3連符の設定。有効にする場合は特定のリズムステップのノートをスキップします
Mute ミュートする拍数
Muted pitch drop ミュートした拍に挿入するピッチダウンノートの設定

スクリプト拡張の手引き

編集方法
テキストエディタで添付のスクリプト "PsytranceTool v###.pyscript" を開いて編集します。
文字コードはUTF-8なので、エディタはサクラエディタなどをオススメします。

なおスクリプトは Python で記述されているので、Pythonの言語知識が多少必要となります。
スケールの追加方法
スケールは "SCALE_TBL" に辞書型で追加します。
# スケールの定義。要素数は "7音階" 固定.
SCALE_TBL = {
  ...
 
コメントに書かれているように、スケールは「七音音階」としているため、要素数を必ず「7つ」にする必要があります。これはメロディ生成に影響するためです。
もしペンタトニックスケールなど、5音階のスケールを使用する場合は、6番目と7番目を5音階の値とすると良いかもしれません。
リズムの追加方法
リズムは "PATTERN_TBL" に辞書型で追加します。
# リズムの定義 (0:休符 1:ルート音 8:+1オクターブ)
PATTERN_TBL = {
  ...
 
基本的に16分音符を基準としているため、16の要素を定義します。
値は「0」が休符(無音)、「1」がルート恩、「8」が1オクターブ上、「-7」が1オクターブ下となります。
メロディの追加方法
メロディは "MELODY_TBL" に辞書型で追加します。
MELODY_TBL = {
  ...
 
パターンで指定した音に対する相対値となり、0がルート (1度)、1が2度、2が3度、3が4度、-1が1度下 (1オクターブ下の7度)となります。

スクリプト

+ スクリプトコード
# ==========================================================
# @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
 
 
最終更新:2025年04月01日 07:33