XIAO ESP32S3+MAX98357Aでエレキギターの音にトライ

2026年2月1日日曜日

マイコン工作

https://www.notyet-maker.com/2026/01/max98357a.html
の続き。

前回、ノイズはなくせたので、今度は、エレキギターっぽい音にトライ。

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 だけを使って音の立ち上がりと減衰を調整しました。

リアルなギター再現ではなく、 **「それっぽく聴こえる条件だけを拾った」**というのが今回のポイントです。
---

次は、よりエレキギターっぽい音を目標にする。

このブログを検索

ブログ アーカイブ

XIAO ESP32S3+MAX98357Aでエレキギターの音にトライ

https://www.notyet-maker.com/2026/01/max98357a.html の続き。 前回、ノイズはなくせたので、今度は、エレキギターっぽい音にトライ。 YouTubeにアップロードしたところ、ジージーした音しか聴こえないが、 実物は、ゴッドファーザー...

QooQ