の続き。
前回、ノイズはなくせたので、今度は、エレキギターっぽい音にトライ。
YouTubeにアップロードしたところ、ジージーした音しか聴こえないが、
実物は、ゴッドファーザーのテーマが鳴っている。
コードは、ChatGPTのThinkingモードで生成した。
///
# XIAO ESP32S3 + MAX98357A / CircuitPython 10.3
# Goal: less "jii-jii" (fixed high whine) by band-limited wavetable,
# then add thickness (detune) + tiny pick noise burst.
# No audiomixer/audiodelays. No allocations in loop. gc.disable.
import gc
import math
import time
from array import array
import audiobusio
import board
import synthio
# ---- Hardware ----
SAMPLE_RATE = 22050
I2S_BCLK = board.D6
I2S_LRCLK = board.D3
I2S_DATA = board.D7
# ---- Sequencer (same melody skeleton; you can change) ----
BEAT_SEC = 60 / 84
OCTAVE_MULT = 0.25 # if "key too high", keep 0.25; if too low, try 0.5
E_BASE = 329.63
A_BASE = 440.00
B_BASE = 493.88
C_BASE = 523.25
F_BASE = 349.23
G_BASE = 392.00
E = E_BASE * OCTAVE_MULT
A = A_BASE * OCTAVE_MULT
B = B_BASE * OCTAVE_MULT
C = C_BASE * OCTAVE_MULT
F = F_BASE * OCTAVE_MULT
G = G_BASE * OCTAVE_MULT
NOTE_SEQUENCE_ABSOLUTE = [
(0, E, 1),
(1, A, 1),
(2, C, 2),
(4, B, 1),
(5, A, 2),
(7, C, 1),
(8, A, 1),
(9, B, 1),
(10, A, 1),
(11, F, 1),
(12, G, 1),
(13, E, 3),
]
LOOP_STEPS = 16
# ---- Tone design ----
# 핵심: 高次倍音を削る(= band-limit)ことで「ジー」を消す
WAVE_LEN = 1024 # 256だと倍音が荒くなりやすい
MAX_HARM = 18 # 少なすぎると細い / 多すぎるとジーが出やすい
EVEN_MIX = 0.12 # 偶数倍音を少しだけ(ギターの色気)
ODD_TILT = 0.95 # 奇数の減衰の仕方(小さいほど暗い)
SOFTCLIP = 1.8 # 波形整形(大きいほど歪むがジーも出やすいので控えめ)
NOTE_AMPL = 0.12
DETUNE_CENTS = -4.0
DETUNE_LEVEL = 0.28
ATTACK = 0.004
DECAY = 0.10
SUSTAIN = 0.55
RELEASE = 0.10
# pick noise
NOISE_MS_MIN = 5
NOISE_MS_MAX = 8
NOISE_LEVEL = 0.10 # NOTE_AMPL に対する比率
NOISE_LP_FC = 1800.0 # ノイズも高域を落として耳障り回避
# tiny pitch drift (random-walk) to avoid static synth feel (optional)
DRIFT_MAX_CENTS = 0.6
DRIFT_STEP_CENTS = 0.08
DRIFT_UPDATE_SEC = 0.02
MIN_SLEEP_SEC = 0.001
NOTE_GAP_SEC = 0.003
# ---- helpers ----
rng = 1
def rand_unit():
global rng
rng = (rng * 1103515245 + 12345) & 0x7FFFFFFF
return rng / 0x7FFFFFFF
def cents_to_ratio(c):
return 2 ** (c / 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 clamp_i16(x):
if x > 32767:
return 32767
if x < -32768:
return -32768
return int(x)
def softclip(x, k):
# simple soft clip (tanh-ish without tanh)
# x in [-1,1]
y = x * (1.0 + k)
if y > 1.0:
y = 1.0
if y < -1.0:
y = -1.0
# cubic smoothing near edges
return y - (y * y * y) * 0.20
def build_bandlimited_wave():
w = array("h", [0] * WAVE_LEN)
for i in range(WAVE_LEN):
ph = 2.0 * math.pi * i / WAVE_LEN
s = 0.0
# "guitar-ish" harmonic recipe:
# - strong fundamental
# - odd harmonics dominant, even small
# - amplitude falls with harmonic number
for n in range(1, MAX_HARM + 1):
amp = 1.0 / n
if (n & 1) == 0:
amp *= EVEN_MIX
else:
amp *= (ODD_TILT ** (n - 1))
s += amp * math.sin(n * ph)
# normalize-ish
s *= 0.80
# mild asymmetry = pick bite without too much high whine
s = s + 0.06 * (s * s)
# soft clip to add controlled harmonics
s = softclip(s, SOFTCLIP)
w[i] = clamp_i16(s * 32767)
return w
def build_noise_wave():
# low-passed noise wavetable
w = array("h", [0] * WAVE_LEN)
a = alpha_from_fc(NOISE_LP_FC)
y = 0.0
for i in range(WAVE_LEN):
x = (rand_unit() * 2.0) - 1.0
y += a * (x - y)
w[i] = clamp_i16(y * 32767)
return w
# ---- audio init ----
i2s = audiobusio.I2SOut(I2S_BCLK, I2S_LRCLK, I2S_DATA)
synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE)
i2s.play(synth)
main_wave = build_bandlimited_wave()
noise_wave = build_noise_wave()
env = synthio.Envelope(attack_time=ATTACK, decay_time=DECAY, sustain_level=SUSTAIN, release_time=RELEASE)
noise_env = synthio.Envelope(attack_time=0.001, decay_time=0.006, sustain_level=0.0, release_time=0.008)
note_main = synthio.Note(frequency=E, waveform=main_wave, envelope=env, amplitude=NOTE_AMPL)
note_det = synthio.Note(frequency=E, waveform=main_wave, envelope=env, amplitude=NOTE_AMPL * DETUNE_LEVEL)
note_noise = synthio.Note(frequency=440.0, waveform=noise_wave, envelope=noise_env, amplitude=0.0)
# ---- state ----
note_pressed = False
det_pressed = False
noise_active = False
note_press_time = 0.0
noise_off_time = 0.0
event_index = 0
loop_offset = 0
start_time = time.monotonic() + 1.0
note_off_time = 0.0
current_freq = None
next_press_time = start_time
# drift
drift_val = 0.0
drift_tgt = 0.0
next_drift_time = 0.0
try:
gc.collect()
gc.disable()
while True:
now = time.monotonic()
# schedule next event (catch-up)
if current_freq is None:
while True:
step, f, h = NOTE_SEQUENCE_ABSOLUTE[event_index]
next_time = start_time + (loop_offset + step) * BEAT_SEC
if now < next_time:
break
target_off = next_time + (BEAT_SEC * h)
if now > target_off:
event_index += 1
if event_index >= len(NOTE_SEQUENCE_ABSOLUTE):
event_index = 0
loop_offset += LOOP_STEPS
continue
current_freq = f
note_off_time = target_off
next_press_time = max(next_time, now) + NOTE_GAP_SEC
event_index += 1
if event_index >= len(NOTE_SEQUENCE_ABSOLUTE):
event_index = 0
loop_offset += LOOP_STEPS
break
# note off
if current_freq is not None and now >= note_off_time:
if note_pressed:
synth.release(note_main)
note_pressed = False
if det_pressed:
synth.release(note_det)
det_pressed = False
if noise_active:
synth.release(note_noise)
noise_active = False
current_freq = None
# note on
if current_freq is not None and (not note_pressed) and now >= next_press_time:
note_press_time = now
base = current_freq
detf = base * cents_to_ratio(DETUNE_CENTS)
note_main.frequency = base
note_det.frequency = detf
synth.press(note_main)
note_pressed = True
synth.press(note_det)
det_pressed = True
# reset drift
drift_val = 0.0
drift_tgt = 0.0
next_drift_time = now + DRIFT_UPDATE_SEC
# noise burst (pick)
burst_ms = NOISE_MS_MIN + rand_unit() * (NOISE_MS_MAX - NOISE_MS_MIN)
note_noise.amplitude = NOTE_AMPL * NOISE_LEVEL
synth.press(note_noise)
noise_active = True
noise_off_time = now + (burst_ms / 1000.0)
# noise off
if noise_active and now >= noise_off_time:
synth.release(note_noise)
noise_active = False
# tiny drift (random-walk) to avoid static tone
if note_pressed:
if now >= next_drift_time:
drift_tgt += ((rand_unit() - 0.5) * 2.0 * DRIFT_STEP_CENTS)
if drift_tgt > DRIFT_MAX_CENTS:
drift_tgt = DRIFT_MAX_CENTS
elif drift_tgt < -DRIFT_MAX_CENTS:
drift_tgt = -DRIFT_MAX_CENTS
next_drift_time = now + DRIFT_UPDATE_SEC
drift_val += (drift_tgt - drift_val) * 0.18
r = cents_to_ratio(drift_val)
# apply drift to both
note_main.frequency = current_freq * r
note_det.frequency = (current_freq * cents_to_ratio(DETUNE_CENTS)) * r
# sleep planner
next_wake = now + MIN_SLEEP_SEC
if current_freq is not None and now < next_press_time:
next_wake = min(next_wake, next_press_time)
if current_freq is not None and now < note_off_time:
next_wake = min(next_wake, note_off_time)
if noise_active and now < noise_off_time:
next_wake = min(next_wake, noise_off_time)
time.sleep(max(MIN_SLEEP_SEC, next_wake - now))
except Exception:
i2s.stop()
i2s.deinit()
raise
///
いまいち理解できないが、ChatGPTの説明は下記。
---
「ギターっぽく聴こえる最低条件」だけを、既存作例から借りました。
参考にしたのは、この作例です。 https://learn.adafruit.com/guitar-synth-with-circuitpython-synthio/code-the-guitar
この作例から使った考え方は、次の3点だけです。
基音はシンプルに鳴らす
まず synthio で素直な波形を出し、最初から作り込みすぎない。
ピッキング感は最初の一瞬で作る
アタック時だけノイズや倍音を足し、すぐに引っ込める。
高域を落としてシンセ臭さを消す
フィルタで上を削り、「弦+アンプっぽい帯域」に寄せる。
MAX98357A+I2Sで「確実に音が出る」土台を先に固め、 その上で synthio だけを使って音の立ち上がりと減衰を調整しました。
リアルなギター再現ではなく、 **「それっぽく聴こえる条件だけを拾った」**というのが今回のポイントです。
---
次は、よりエレキギターっぽい音を目標にする。
0 件のコメント:
コメントを投稿