の続き。
!注意!
この記事でも、エレキギターっぽい音に到達しなかった。
ただし、記録として公開しておく。
ChatGPTと会話した結果、
---
MX MIDI Guitarを参照するのは「演奏解釈」までに止め、
音は synthio で作り切るのが正解
---
ということになった。
MX MIDI Guitarから参照する「演奏解釈」とは下記の通り。
---
MX MIDI Guitar がやっているのは、**連続するアナログ入力の時間変化から、
note / velocity / timing を確定する演奏解釈だけ**です。
音を鳴らす処理は一切ありません。
具体的には、
* 入力の生値ではなく**差分(変化量)**を見る
* 差分が閾値を超えた瞬間を「ピック候補」とする
* 数 ms〜十数 ms の短い窓で**ピーク値**を取り、ノイズか有効かを判定
* ピーク値を正規化して **velocity** を決定
* ごく短時間に起きた複数弦の反応を**同一ストラム**として束ねる
* 弦番号と押弦情報から **note** を確定し、そこで初めて `note_on` を出す
* 入力が弱まったら、同様に閾値判定で `note_off` を出す
重要なのは、
* 音色・波形・歪み・エンベロープは扱わない
* 「ギターっぽさ」は**アタック直後の検出精度とタイミング解釈**で作っている
* 出力は常に **演奏イベント(note, velocity, timing)だけ**
あなたの構成では、
MX MIDI Guitar の `note_on(note, velocity)` を
`synthio` の `play_note(freq, velocity)` に置き換えるだけで、
**演奏解釈ロジックはそのまま前段として使えます**。
これが、MX MIDI Guitar の演奏解釈をコード準拠で言語化した最小要約です。
---
下記のコードで、MX MIDI Guitarの「演奏解釈」のうち ピック検出と強さまでは
再現できたようだが、エレキギターっぽい音じゃない。
///
import math
import time
from array import array
import audiobusio
import board
import synthio
# --- Hardware / runtime ---
SAMPLE_RATE = 18000
I2S_BCLK = board.D6
I2S_LRCLK = board.D3
I2S_DATA = board.D7
# --- Interpretation layer (MX MIDI Guitar style, no sound) ---
PICK_DIFF_THRESHOLD = 0.25
PICK_WINDOW_MS = 12
PEAK_MIN = 0.30
PEAK_MAX = 1.00
STRUM_HOLD_MS = 60
OFF_HOLD_MS = 80
SIGNAL_DT_MS = 1
SIGNAL_LEN_MS = 2600
# Dummy notes (C D E F G A B C)
NOTE_SEQUENCE = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25]
# --- Sound layer (synthio only) ---
AMP_MIN = 0.05
AMP_MAX = 0.15
ATTACK = 0.002
DECAY = 0.12
SUSTAIN = 0.10
RELEASE = 0.08
MIN_NOTE_ON_SEC = 0.25
DRIVE_BASE = 0.6
ODD3_BASE = 1.0
ODD5_BASE = 0.25
PHASE_WARP_BASE = 0.00
UPDATE_STEP = 0.006
ATTACK_WAVE_MS = 12
ATTACK_PITCH_MS = 20
PITCH_CENTS = 3.0
LP_FC_NORMAL = 4500.0
SAT_DRIVE = 0.6
POST_GAIN = 1.8
WAVE_LEN = 128
MAX_I16 = 32767
NOTE_GAP_SEC = 0.2
i2s = audiobusio.I2SOut(I2S_BCLK, I2S_LRCLK, I2S_DATA)
synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE)
i2s.play(synth)
env = synthio.Envelope(
attack_time=ATTACK,
decay_time=DECAY,
sustain_level=SUSTAIN,
release_time=RELEASE,
)
rng = 1
def rand_unit():
global rng
rng = (rng * 1103515245 + 12345) & 0x7FFFFFFF
return rng / 0x7FFFFFFF
def clamp01(value):
if value < 0.0:
return 0.0
if value > 1.0:
return 1.0
return value
def cents_to_ratio(cents):
return 2 ** (cents / 1200.0)
def alpha_from_fc(fc):
dt = 1.0 / SAMPLE_RATE
rc = 1.0 / (2.0 * math.pi * fc)
return dt / (rc + dt)
def soft_sat(x, drive):
y = x + drive * (x * x * x)
if y > 1.0:
y = 1.0
elif y < -1.0:
y = -1.0
return y
def make_wave(drive, odd3, odd5, phase_warp, lp_fc):
w = array("h", [0] * WAVE_LEN)
lp_alpha = alpha_from_fc(lp_fc)
lp_y = 0.0
for i in range(WAVE_LEN):
phase = 2.0 * math.pi * i / WAVE_LEN
phase = phase + phase_warp * math.sin(phase)
x = math.sin(phase)
x = x + (odd3 * drive) * (x * x * x) + (odd5 * drive) * (x * x * x * x * x)
x = soft_sat(x, SAT_DRIVE)
lp_y = lp_y + lp_alpha * (x - lp_y)
z = lp_y * POST_GAIN
if z > 1.0:
z = 1.0
elif z < -1.0:
z = -1.0
w[i] = int(z * MAX_I16)
return w
# -----------------------------
# Interpretation layer
# -----------------------------
interp_last_value = 0.0
interp_last_time = 0.0
interp_state = "idle"
interp_peak_max = 0.0
interp_peak_end = 0.0
interp_hold_until = 0.0
interp_below_ms = 0.0
interp_current_note = None
interp_note_index = 0
interp_next_pick_time = 0.0
def interpret_step(now, note_sequence):
global interp_last_value, interp_last_time, interp_state
global interp_peak_max, interp_peak_end, interp_hold_until
global interp_below_ms, interp_current_note, interp_note_index
global interp_next_pick_time
if interp_last_time == 0.0:
interp_last_time = now
interp_next_pick_time = now + 0.2
return None
dt = now - interp_last_time
if dt <= 0:
return None
interp_last_time = now
noise = (rand_unit() - 0.5) * 0.02
spike = 0.0
if now >= interp_next_pick_time:
spike = 0.6
interp_next_pick_time = now + 0.25 + (rand_unit() * 0.05)
value = (interp_last_value * 0.98) + noise + spike
diff = abs(value - interp_last_value)
interp_last_value = value
if interp_state == "idle":
if now < interp_hold_until:
return None
if diff > PICK_DIFF_THRESHOLD:
interp_state = "peak"
interp_peak_max = diff
interp_peak_end = now + (PICK_WINDOW_MS / 1000.0)
return None
if interp_state == "peak":
if diff > interp_peak_max:
interp_peak_max = diff
if now >= interp_peak_end:
if interp_peak_max < PEAK_MIN:
interp_state = "idle"
return None
velocity = clamp01((interp_peak_max - PEAK_MIN) / (PEAK_MAX - PEAK_MIN))
interp_current_note = note_sequence[interp_note_index % len(note_sequence)]
interp_note_index += 1
interp_hold_until = now + (STRUM_HOLD_MS / 1000.0)
interp_state = "wait_reset"
interp_below_ms = 0.0
return ("on", interp_current_note, velocity, now)
return None
if interp_state == "wait_reset":
if diff < PICK_DIFF_THRESHOLD:
interp_below_ms += dt * 1000.0
if interp_below_ms >= OFF_HOLD_MS:
interp_state = "idle"
return ("off", interp_current_note, 0.0, now)
else:
interp_below_ms = 0.0
return None
# -----------------------------
# Sound layer
# -----------------------------
WAVEFORM = make_wave(DRIVE_BASE, ODD3_BASE, ODD5_BASE, PHASE_WARP_BASE, LP_FC_NORMAL)
def play_event(active, freq, velocity, start_time):
amp = AMP_MIN + (AMP_MAX - AMP_MIN) * velocity
drive = DRIVE_BASE + (0.6 * velocity)
wave_normal = make_wave(drive, ODD3_BASE, ODD5_BASE, PHASE_WARP_BASE, LP_FC_NORMAL)
wave_attack = make_wave(drive * 1.4, ODD3_BASE * 1.3, ODD5_BASE * 1.2, PHASE_WARP_BASE, LP_FC_NORMAL)
note = synthio.Note(frequency=freq, envelope=env, amplitude=amp, waveform=wave_attack)
synth.press(note)
active[freq] = {
"note": note,
"start": start_time,
"amp": amp,
"freq": freq,
"wave_normal": wave_normal,
"wave_attack": wave_attack,
"release_at": None,
}
def update_active(active, now):
to_release = []
for freq, state in active.items():
note = state["note"]
t = now - state["start"]
if t < (ATTACK_WAVE_MS / 1000.0):
note.waveform = state["wave_attack"]
else:
note.waveform = state["wave_normal"]
if t < (ATTACK_PITCH_MS / 1000.0):
decay = math.exp(-t / 0.01)
note.frequency = state["freq"] * cents_to_ratio(PITCH_CENTS * decay)
else:
note.frequency = state["freq"]
release_at = state["release_at"]
if release_at is not None and now >= release_at:
to_release.append(freq)
for freq in to_release:
synth.release(active[freq]["note"])
del active[freq]
active_notes = {}
while True:
now = time.monotonic()
event = interpret_step(now, NOTE_SEQUENCE)
if event:
ev_type, freq, velocity, _t = event
if ev_type == "on":
if freq in active_notes:
synth.release(active_notes[freq]["note"])
del active_notes[freq]
play_event(active_notes, freq, velocity, now)
else:
if freq in active_notes:
start_time = active_notes[freq]["start"]
hold_until = start_time + MIN_NOTE_ON_SEC
if now >= hold_until:
synth.release(active_notes[freq]["note"])
del active_notes[freq]
else:
active_notes[freq]["release_at"] = hold_until
update_active(active_notes, now)
time.sleep(UPDATE_STEP)
///
で、何時間もかけて、
---
エレキギターっぽさに近づく作業ではありましたが、そのままでは“本物っぽい
音色”には到達しにくい作業でした。理由は、いまやっているのが主に MX MIDI
Guitar の「演奏解釈(いつ・どれだけ強く弾いたか)」の再現で、音色の核
(ピックノイズ、高域の瞬間成分、弦ごとの倍音減衰、アンプ/スピーカーの癖)
は別レイヤーです。
---
という結論になってしまった。
次のアクションは、
エレキギターっぽい音を目指し、発音側を変更する。
・ピック直後5〜15msだけノイズ成分を重ねる
・15ms後に「明るい波形→暗い波形」に切り替える
・ドレミファソラシドで鳴らして、ノイズなし版と聴き比べる
0 件のコメント:
コメントを投稿