XIAO ESP32S3でドレミファソラシドを鳴らす その4

2026年1月3日土曜日

マイコン工作

https://www.notyet-maker.com/2026/01/xiao-esp32s3-3.html

の続き。


code .py を使う前に、コード停止方法を整理しておく。


REPL接続中ではないので、Ctrl+Cが使えない。

REPL接続は、USBマスストレージ(CIRCUITPY)と排他になってしまう。


なので、code .pyに、

# empty

を入力&保存して止めるのが、楽だと思う。



さて、

3. ESP32S3側でクリーン音を整え、Slash寄りにする。

の続き。


Slash寄りとは

「電子的に正確すぎる要素を、すべて弱めること」

だという。


具体的には、

---

① 高域を出しすぎない

→ ギターでは本来出ない「電子的に鋭い高音」を消すこと。

→ そのまま出すと“シンセ音”になる。


② 入力をわずかに飽和させる

→ 音量の大小が、そのまま直線的に音圧にならない状態にすること。

→ 大きく弾いても、急に音が尖らない。


③ 完全に正確な周期を避ける

→ 同じ周波数・同じ波形を、毎回まったく同じ形で出さないこと。

→ 数学的に正確だと「電子音」になる。


④ ビブラートは後段で、浅く入れる

→ 音を出した直後は揺らさない。

→ しばらく鳴ってから、周波数を少しだけ動かす。

---

とのこと。


synthio で出せる「エレキギターっぽさ」の実質的な限界の

ドレミファソラシドは↓。

///

import time, math, array

import board, audiobusio, synthio


SAMPLE_RATE = 18000

AMP = 0.10

ATTACK = 0.01

DECAY = 0.20

SUSTAIN = 0.25

RELEASE = 0.25

NOTE_ON_SEC = 0.35

NOTE_GAP_SEC = 0.25


PRE_SIL_MS = 4

BROKEN_MS = 12


JITTER_CENTS = 2.0

JITTER_TAU = 0.01


DRIVE_BASE = 0.6

DRIVE_BROKEN = 1.0


ODD3_BASE = 1.0

ODD5_BASE = 0.25

ODD3_JIT = 0.08

ODD5_JIT = 0.03


PHASE_WARP_BASE = 0.00

PHASE_WARP_BROKEN = 0.18


VIB_ON = True

VIB_DELAY = 0.10

VIB_RATE = 5.0

VIB_CENTS = 5.0


UPDATE_STEP = 0.006


LP_FC_NORMAL = 4500.0

LP_FC_BROKEN = 6000.0

SAT_DRIVE = 0.6

POST_GAIN = 1.8


WAVE_LEN = 128

MAX_I16 = 32767


NOTES = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25]


i2s = audiobusio.I2SOut(board.D6, board.D3, board.D7)

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 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.array("h", [0] * WAVE_LEN)

    lp_alpha = alpha_from_fc(lp_fc)

    lp_y = 0.0

    for i in range(WAVE_LEN):

        phase = 2 * 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


def play_note(freq):

    odd3 = ODD3_BASE + (rand_unit() * 2.0 - 1.0) * ODD3_JIT

    odd5 = ODD5_BASE + (rand_unit() * 2.0 - 1.0) * ODD5_JIT

    drive_base = DRIVE_BASE + (rand_unit() * 0.05)

    drive_broken = DRIVE_BROKEN + (rand_unit() * 0.08)


    wave_normal = make_wave(drive_base, odd3, odd5, PHASE_WARP_BASE, LP_FC_NORMAL)

    wave_broken = make_wave(drive_broken, odd3, odd5, PHASE_WARP_BROKEN, LP_FC_BROKEN)


    note = synthio.Note(frequency=freq, envelope=env, amplitude=AMP, waveform=wave_normal)

    synth.press(note)


    start = time.monotonic()

    pre_sil = PRE_SIL_MS / 1000.0

    broken = BROKEN_MS / 1000.0

    broken_end = pre_sil + broken


    while time.monotonic() - start < NOTE_ON_SEC:

        now = time.monotonic()

        t = now - start


        if t < pre_sil:

            note.amplitude = AMP * 0.2

            note.waveform = wave_normal

        elif t < broken_end:

            note.amplitude = AMP

            note.waveform = wave_broken

            jitter = (rand_unit() * 2.0 - 1.0) * JITTER_CENTS * math.exp(-(t - pre_sil) / JITTER_TAU)

            note.frequency = freq * cents_to_ratio(jitter)

        else:

            note.amplitude = AMP

            note.waveform = wave_normal

            note.frequency = freq


        if VIB_ON and t > VIB_DELAY and t > broken_end:

            vib = cents_to_ratio(math.sin(2 * math.pi * VIB_RATE * now) * VIB_CENTS)

            note.frequency = note.frequency * vib


        time.sleep(UPDATE_STEP)


    synth.release(note)

    time.sleep(NOTE_GAP_SEC)


while True:

    for f in NOTES:

        play_note(f)

///


が、エレキギターっぽくない....


ChatGPTに問いただすと、

---

synthioの上に「後段処理(アンプ+キャビ相当)」を足せると思っていた

具体的には、音が出たあとにローパスや歪み段を挿せる前提で話を進めていましたが、synthio+audiobusioにはそれを差し込む入口がありませんでした。


synthioの範囲で「Slash風の歪み」まで到達できると思っていた

実際は、synthioでできるのは「ギターを意識したシンセ音(クリーン〜軽いクランチ寄り)」までで、アンプで潰して押し出すような歪みの質感は射程外でした。


最初から synthio を使わず、RawSample(ストリーム処理)で行くべきでした。

---

という誤認をしていたらしい。


なかなか進まない。

続きは次回。


このブログを検索

ブログ アーカイブ

曲がったことが嫌い

祖父は俳句をしていて、竹風という俳名を持っていた。 ググったら、 西祭すみし大堰のうすにごり という俳句が見つかった。 Geminiに入力すると、説明してくれたのでビックリ。(ここには説明を掲載しない。) 竹風という俳名は、竹を割ったような性格、つまり曲がったことが大嫌い、という...

QooQ