{"id":2286,"date":"2025-12-27T13:42:48","date_gmt":"2025-12-27T04:42:48","guid":{"rendered":"https:\/\/rfsec.ddns.net\/db\/?p=2286"},"modified":"2025-12-29T12:29:45","modified_gmt":"2025-12-29T03:29:45","slug":"%e3%82%b7%e3%83%b3%e3%82%bb%e3%82%b5%e3%82%a4%e3%82%b6%e3%83%bc%e3%82%92raspberry-pi4%e3%81%ab%e5%ae%9f%e8%a3%85%e3%81%99%e3%82%8b","status":"publish","type":"post","link":"https:\/\/rfsec.ddns.net\/db\/?p=2286","title":{"rendered":"\u30b7\u30f3\u30bb\u30b5\u30a4\u30b6\u30fc\u3092raspberry Pi4\u306b\u5b9f\u88c5\u3059\u308b"},"content":{"rendered":"\n<p>FM\u97f3\u6e90\u65b9\u5f0f<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">FM\u97f3\u6e90\u306e\u30ec\u30b7\u30d4\u306e\u30b3\u30c4\uff08\u81ea\u5206\u3067\u4f5c\u308b\u5834\u5408\uff09<\/h3>\n\n\n\n<p>\u3053\u306e\u30b7\u30f3\u30bb\u30b5\u30a4\u30b6\u30fc\u306b\u304a\u3051\u308b\u5404\u30d1\u30e9\u30e1\u30fc\u30bf\u306e\u97f3\u3078\u306e\u5f71\u97ff\u306f\u4ee5\u4e0b\u306e\u3068\u304a\u308a\u3067\u3059\u3002<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>Mod Ratio (\u500d\u97f3\u69cb\u6210)<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>1.0, 2.0, 3.0...<\/code> (\u6574\u6570): \u304d\u308c\u3044\u306a\u548c\u97f3\u3001\u697d\u5668\u7684\u306a\u97f3\u306b\u306a\u308a\u307e\u3059\u3002<\/li>\n\n\n\n<li><code>1.41, 2.5, 3.14...<\/code> (\u975e\u6574\u6570): \u91d1\u5c5e\u97f3\u3001\u9418\u3001\u30ce\u30a4\u30ba\u3063\u307d\u3044\u97f3\u306b\u306a\u308a\u307e\u3059\u3002<\/li>\n\n\n\n<li><code>0.5<\/code>: 1\u30aa\u30af\u30bf\u30fc\u30d6\u4e0b\u306e\u97f3\u304c\u6df7\u3056\u308a\u3001\u592a\u304f\u306a\u308a\u307e\u3059\u3002<\/li>\n\n\n\n<li><code>1.5<\/code>: \u300c\u30c9\u300d\u306b\u5bfe\u3057\u3066\u300c\u30bd\u300d\u304c\u6df7\u3056\u308a\u3001\u30d1\u30ef\u30fc\u30b3\u30fc\u30c9\u306e\u3088\u3046\u306a\u97ff\u304d\u306b\u306a\u308a\u307e\u3059\u3002<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Mod Index (\u97f3\u306e\u660e\u308b\u3055\u30fb\u6fc0\u3057\u3055)<\/strong>\n<ul class=\"wp-block-list\">\n<li><code>0.0<\/code>: \u7d14\u7c8b\u306a\u30b5\u30a4\u30f3\u6ce2\uff08\u30dd\u30fc\u3068\u3044\u3046\u6642\u5831\u306e\u3088\u3046\u306a\u97f3\uff09\u3002<\/li>\n\n\n\n<li><code>1.0 ~ 3.0<\/code>: \u5fc3\u5730\u3088\u3044FM\u30c8\u30fc\u30f3\uff08\u30a8\u30ec\u30d4\u3084\u30d9\u30fc\u30b9\uff09\u3002<\/li>\n\n\n\n<li><code>5.0 ~ 10.0<\/code>: \u30ae\u30e9\u30ae\u30e9\u3057\u305f\u97f3\u3001\u30d3\u30e8\u30fc\u30f3\u3068\u3044\u3046\u97f3\u3002<\/li>\n\n\n\n<li><code>10.0\u4ee5\u4e0a<\/code>: \u30ce\u30a4\u30ba\u306b\u8fd1\u3044\u7834\u58ca\u7684\u306a\u97f3\uff08Gain\u3092\u4e0b\u3052\u306a\u3044\u3068\u8033\u304c\u75db\u304f\u306a\u308a\u307e\u3059\uff09\u3002<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Attack \/ Release<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>Pad\u7cfb<\/strong>: Attack\u3092 <code>0.5<\/code> \u4ee5\u4e0a\u306b\u3059\u308b\u3068\u3001\u3075\u308f\u3063\u3068\u7acb\u3061\u4e0a\u304c\u308a\u307e\u3059\u3002<\/li>\n\n\n\n<li><strong>Bass\/Bell\u7cfb<\/strong>: Attack\u3092 <code>0.01<\/code> (\u6700\u901f) \u306b\u3057\u3066\u3001Release\u3067\u4f59\u97fb\u3092\u8abf\u6574\u3057\u307e\u3059\u3002<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<figure class=\"wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex\">\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"641\" height=\"526\" data-id=\"2303\" src=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-29-120718.png\" alt=\"\" class=\"wp-image-2303\" srcset=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-29-120718.png 641w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-29-120718-300x246.png 300w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-29-120718-624x512.png 624w\" sizes=\"auto, (max-width: 641px) 100vw, 641px\" \/><\/figure>\n<\/figure>\n\n\n\n<figure class=\"wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-2 is-layout-flex wp-block-gallery-is-layout-flex\">\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"629\" height=\"830\" data-id=\"2296\" src=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-141516.png\" alt=\"\" class=\"wp-image-2296\" srcset=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-141516.png 629w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-141516-227x300.png 227w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-141516-624x823.png 624w\" sizes=\"auto, (max-width: 629px) 100vw, 629px\" \/><\/figure>\n<\/figure>\n\n\n<div class=\"wp-block-ub-content-toggle wp-block-ub-content-toggle-block\" id=\"ub-content-toggle-block-1f677232-11f7-426f-9019-0ba50c6d91f8\" data-mobilecollapse=\"false\" data-desktopcollapse=\"true\" data-preventcollapse=\"false\" data-showonlyone=\"false\">\n<div class=\"wp-block-ub-content-toggle-accordion\" style=\"border-color: #f1f1f1;\" id=\"ub-content-toggle-panel-block-\">\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-title-wrap\" style=\"background-color: #f1f1f1;\" aria-controls=\"ub-content-toggle-panel-0-1f677232-11f7-426f-9019-0ba50c6d91f8\" tabindex=\"0\">\n\t\t\t<p class=\"wp-block-ub-content-toggle-accordion-title ub-content-toggle-title-1f677232-11f7-426f-9019-0ba50c6d91f8\" style=\"color: #000000; \">\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u3000\u30a2\u30ca\u30ed\u30b0+Soundfont\u7248(Python)<\/p>\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-toggle-wrap right\" style=\"color: #000000;\"><span class=\"wp-block-ub-content-toggle-accordion-state-indicator wp-block-ub-chevron-down\"><\/span><\/div>\n\t\t<\/div>\n\t\t\t<div role=\"region\" aria-expanded=\"false\" class=\"wp-block-ub-content-toggle-accordion-content-wrap ub-hide\" id=\"ub-content-toggle-panel-0-1f677232-11f7-426f-9019-0ba50c6d91f8\">\n\n<p><\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code># \u30b3\u30fc\u30c9\u540d: py_synth_v43_midi_monitor\n# \u30d0\u30fc\u30b8\u30e7\u30f3: 43.0 (MIDI Monitor Edition)\n# Description: \u53d7\u4fe1\u3057\u305fMIDI\u30c7\u30fc\u30bf\u3092\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u3067\u30ed\u30b0\u8868\u793a\u3059\u308b\u30e2\u30cb\u30bf\u30fc\u6a5f\u80fd\u3092\u8ffd\u52a0\u3002\u97f3\u5207\u308c\u9632\u6b62\u30fb\u30d9\u30ed\u30b7\u30c6\u30a3\u88dc\u6b63\u6e08\u307f\u3002\n\nimport os\nos.environ['OMP_NUM_THREADS'] = '1'\n\nimport sys\nimport time\nimport threading\nimport gc\nimport math\nimport numpy as np\nimport rtmidi\nimport fluidsynth\nimport tkinter as tk\nfrom tkinter import ttk\nfrom tkinter import filedialog\nfrom contextlib import contextmanager\n\n# --- \u30e9\u30a4\u30d6\u30e9\u30ea ---\ntry:\n    import sounddevice as sd\nexcept ImportError:\n    sys.exit(\"Error: pip install sounddevice\")\ntry:\n    import mido\nexcept ImportError:\n    sys.exit(\"Error: pip install mido\")\ntry:\n    from scipy.signal import lfilter\nexcept ImportError:\n    sys.exit(\"Error: pip install scipy\")\n\n# --- \u8a2d\u5b9a ---\nTARGET_DEVICE_KEYWORD = \"USB\"\nSAMPLE_RATE = 48000\nBLOCK_SIZE = 4096\nCHANNELS = 2\nDTYPE = 'int16'\nMASTER_GAIN_DEFAULT = 0.5\nSF2_PATH = \"\/usr\/share\/sounds\/sf2\/FluidR3_GM.sf2\"\n\nis_running = True\nAUDIO_DEVICE_NAME = \"Searching...\"\nAUDIO_DEVICE_ID = None\nMIDI_PORT_NAME = \"Searching...\"\n\n# --- GM\u697d\u5668\u540d ---\nGM_INSTRUMENTS = [\n    \"Grand Piano\", \"Bright Piano\", \"E.Grand Piano\", \"Honky-tonk\", \"E.Piano 1\", \"E.Piano 2\", \"Harpsichord\", \"Clavinet\",\n    \"Celesta\", \"Glockenspiel\", \"Music Box\", \"Vibraphone\", \"Marimba\", \"Xylophone\", \"Tubular Bells\", \"Dulcimer\",\n    \"Drawbar Organ\", \"Percussive Organ\", \"Rock Organ\", \"Church Organ\", \"Reed Organ\", \"Accordion\", \"Harmonica\", \"Tango Accordion\",\n    \"Guit(Nylon)\", \"Guit(Steel)\", \"Guit(Jazz)\", \"Guit(Clean)\", \"Guit(Muted)\", \"Guit(Overdrive)\", \"Guit(Distortion)\", \"Guit(Harmonics)\",\n    \"Acoustic Bass\", \"E.Bass(Finger)\", \"E.Bass(Pick)\", \"Fretless Bass\", \"Slap Bass 1\", \"Slap Bass 2\", \"Synth Bass 1\", \"Synth Bass 2\",\n    \"Violin\", \"Viola\", \"Cello\", \"Contrabass\", \"Tremolo Strings\", \"Pizzicato Strings\", \"Orchestral Harp\", \"Timpani\",\n    \"Strings 1\", \"Strings 2\", \"SynthStrings 1\", \"SynthStrings 2\", \"Choir Aahs\", \"Voice Oohs\", \"Synth Voice\", \"Orchestra Hit\",\n    \"Trumpet\", \"Trombone\", \"Tuba\", \"Muted Trumpet\", \"French Horn\", \"Brass Section\", \"Synth Brass 1\", \"Synth Brass 2\",\n    \"Soprano Sax\", \"Alto Sax\", \"Tenor Sax\", \"Baritone Sax\", \"Oboe\", \"English Horn\", \"Bassoon\", \"Clarinet\",\n    \"Piccolo\", \"Flute\", \"Recorder\", \"Pan Flute\", \"Blown Bottle\", \"Shakuhachi\", \"Whistle\", \"Ocarina\",\n    \"Lead 1 (square)\", \"Lead 2 (saw)\", \"Lead 3 (calliope)\", \"Lead 4 (chiff)\", \"Lead 5 (charang)\", \"Lead 6 (voice)\", \"Lead 7 (fifths)\", \"Lead 8 (bass+lead)\",\n    \"Pad 1 (new age)\", \"Pad 2 (warm)\", \"Pad 3 (polysynth)\", \"Pad 4 (choir)\", \"Pad 5 (bowed)\", \"Pad 6 (metallic)\", \"Pad 7 (halo)\", \"Pad 8 (sweep)\",\n    \"FX 1 (rain)\", \"FX 2 (soundtrack)\", \"FX 3 (crystal)\", \"FX 4 (atmosphere)\", \"FX 5 (brightness)\", \"FX 6 (goblins)\", \"FX 7 (echoes)\", \"FX 8 (sci-fi)\",\n    \"Sitar\", \"Banjo\", \"Shamisen\", \"Koto\", \"Kalimba\", \"Bag pipe\", \"Fiddle\", \"Shanai\",\n    \"Tinkle Bell\", \"Agogo\", \"Steel Drums\", \"Woodblock\", \"Taiko Drum\", \"Melodic Tom\", \"Synth Drum\", \"Reverse Cymbal\",\n    \"Guit Fret Noise\", \"Breath Noise\", \"Seashore\", \"Bird Tweet\", \"Telephone Ring\", \"Helicopter\", \"Applause\", \"Gunshot\"\n]\n\n@contextmanager\ndef ignore_stderr():\n    try:\n        devnull = os.open(os.devnull, os.O_WRONLY)\n        old_stderr = os.dup(2)\n        sys.stderr.flush()\n        os.dup2(devnull, 2)\n        os.close(devnull)\n        try: yield\n        finally: os.dup2(old_stderr, 2); os.close(old_stderr)\n    except: yield\n\nclass SynthParams:\n    def __init__(self):\n        self.engine_mode = \"ANALOG\" \n        self.master_gain = MASTER_GAIN_DEFAULT\n        self.waveform = \"sawtooth\"\n        self.lfo_rate = 5.0; self.lfo_depth = 0.0\n        self.filter_mode = \"LPF\"; self.cutoff_base = 1000.0; self.resonance = 0.5; self.vcf_env_amt = 4000.0\n        self.vcf_attack = 0.1; self.vcf_decay = 0.3; self.vcf_sustain = 0.4; self.vcf_release = 0.5\n        self.amp_attack = 0.05; self.amp_decay = 0.2; self.amp_sustain = 0.6; self.amp_release = 0.5\n        self.sf_program = 0; self.sf_vol = 1.0\n        self.digital_filter_on = True\n\nparams = SynthParams()\n\nclass FluidManager:\n    def __init__(self):\n        self.fs = None; self.ready = False\n    def init_synth(self):\n        try:\n            self.fs = fluidsynth.Synth()\n            self.fs.setting('synth.sample-rate', float(SAMPLE_RATE))\n            self.fs.setting('synth.gain', 1.0)\n            self.fs.setting('synth.polyphony', 32)\n            self.fs.setting('synth.reverb.active', 0)\n            self.fs.setting('synth.chorus.active', 0)\n            sfid = self.fs.sfload(SF2_PATH)\n            self.fs.program_select(0, sfid, 0, 0)\n            self.ready = True\n            print(\"FluidSynth Ready.\")\n        except Exception as e:\n            print(f\"FluidSynth Error: {e}\"); self.ready = False\n    def get_samples(self, n):\n        if not self.ready: return np.zeros(n * 2, dtype=np.int16)\n        return np.array(self.fs.get_samples(n), dtype=np.int16)\n    def note_on(self, n, v):\n        if self.ready: self.fs.noteon(0, n, v)\n    def note_off(self, n):\n        if self.ready: self.fs.noteoff(0, n)\n    def change_program(self, p):\n        if self.ready: self.fs.program_change(0, int(p))\n\nfluid_engine = FluidManager()\n\ndef design_biquad(mode, fc, q, fs):\n    w0=2*math.pi*fc\/fs; alpha=math.sin(w0)\/(2*q); c=math.cos(w0); a0=1+alpha\n    if mode==\"LPF\": b=[(1-c)\/2, 1-c, (1-c)\/2]\n    elif mode==\"HPF\": b=[(1+c)\/2, -(1+c), (1+c)\/2]\n    elif mode==\"BPF\": b=[alpha, 0, -alpha]\n    else: return [1,0,0],[1,0,0]\n    return np.array(b)\/a0, np.array([1+alpha,-2*c,1-alpha])\/a0\n\nclass ADSREnvelope:\n    def __init__(self, is_amp=True):\n        self.state='IDLE'; self.level=0.0; self.is_amp=is_amp\n    def trigger(self): self.state='ATTACK'\n    def release(self): self.state='RELEASE'\n    def get_val(self):\n        if self.is_amp: a,d,s,r = params.amp_attack,params.amp_decay,params.amp_sustain,params.amp_release\n        else: a,d,s,r = params.vcf_attack,params.vcf_decay,params.vcf_sustain,params.vcf_release\n        step = float(BLOCK_SIZE)\/SAMPLE_RATE\n        if self.state=='ATTACK':\n            self.level+=step\/max(a,0.001); \n            if self.level&gt;=1.0: self.level,self.state=1.0,'DECAY'\n        elif self.state=='DECAY':\n            self.level-=(step\/max(d,0.001))*(1.0-s); \n            if self.level&lt;=s: self.level,self.state=s,'SUSTAIN'\n        elif self.state=='SUSTAIN': self.level=s\n        elif self.state=='RELEASE':\n            self.level-=step\/max(r,0.001); \n            if self.level&lt;=0.0: self.level,self.state=0.0,'IDLE'\n        return self.level\n\nclass StereoMasterFilter:\n    def __init__(self): self.zi = np.zeros((2, 2)) \n    def process(self, sig_stereo_int16, cut, res):\n        sig_float = sig_stereo_int16.astype(np.float32) \/ 32768.0\n        fc = max(50.0, min(cut, SAMPLE_RATE * 0.45)); q = 0.7 + (res * 9.0)\n        b, a = design_biquad(params.filter_mode, fc, q, SAMPLE_RATE)\n        sig_reshaped = sig_float.reshape(-1, 2)\n        out_reshaped, self.zi = lfilter(b, a, sig_reshaped, axis=0, zi=self.zi)\n        return np.clip(out_reshaped.flatten() * 32768.0, -32768, 32767).astype(np.int16)\n\nclass Oscillator:\n    def __init__(self): self.p=0.0; self.f=0.0\n    def set_f(self,f): self.f=f\n    def get(self,n,lfo):\n        if self.f==0: return np.zeros(n)\n        mf=self.f+(lfo*params.lfo_depth*10.0)\n        ph=self.p+np.cumsum(np.full(n,1.0))*(mf\/SAMPLE_RATE)\n        self.p=ph[-1]%1.0; ph%=1.0\n        w=params.waveform\n        if w=='sawtooth': return 2.0*(ph-0.5)\n        elif w=='square': return np.sign(np.sin(2*np.pi*ph))\n        elif w=='sine': return np.sin(2*np.pi*ph)\n        return 2.0*np.abs(2.0*(ph-0.5))-1.0\n\nclass LFO:\n    def __init__(self): self.p=0.0\n    def get(self,n):\n        t=(np.arange(n)+self.p)\/SAMPLE_RATE; self.p+=n\n        if self.p&gt;SAMPLE_RATE: self.p-=SAMPLE_RATE\n        return np.sin(2*np.pi*params.lfo_rate*t)\n\nvco=Oscillator(); lfo=LFO(); amp=ADSREnvelope(True); vcf=ADSREnvelope(False)\nmaster_filter = StereoMasterFilter() \nactive_note=None\n\n# --- GUI Manager with MIDI Log ---\nclass GUIManager:\n    def __init__(self):\n        self.root = None; self.btn_engine = None; self.instr_label = None; self.btn_filter = None\n        self.disp_a=None; self.disp_d=None; self.disp_s=None; self.disp_r=None\n        self.var_vol=None; self.var_cut=None; self.var_res=None\n        self.gate_lamp=None; self.scope_canvas=None\n        self.lbl_filename=None; self.lbl_wave=None\n        self.var_dig_flt = None; self.btn_scope = None \n        self.scope_mode = \"OFF\"; self.last_scope_time = 0\n        self.log_text = None # MIDI Log Widget\n\n    def set_gate(self, on_off):\n        if self.root and self.gate_lamp:\n            color = \"#FF0000\" if on_off else \"#440000\"\n            try: self.root.after_idle(lambda: self.gate_lamp.itemconfig(\"lamp\", fill=color))\n            except: pass\n    def update_slider(self, target_var, value):\n        if self.root and target_var:\n            try: self.root.after_idle(lambda: target_var.set(value))\n            except: pass\n    def update_wave_label(self, name):\n        if self.root and self.lbl_wave:\n            try: self.root.after_idle(lambda: self.lbl_wave.config(text=f\"WAVE: {name.upper()}\"))\n            except: pass\n    def update_filename(self, text):\n        if self.root and self.lbl_filename:\n            try: self.root.after_idle(lambda: self.lbl_filename.config(text=text))\n            except: pass\n            \n    # \u2605 MIDI\u30ed\u30b0\u8ffd\u52a0\u7528\u30e1\u30bd\u30c3\u30c9\n    def log_midi(self, text):\n        if self.root and self.log_text:\n            try: self.root.after_idle(lambda: self._append_log(text))\n            except: pass\n\n    def _append_log(self, text):\n        if not self.log_text: return\n        self.log_text.config(state='normal')\n        self.log_text.insert(tk.END, text + \"\\n\")\n        self.log_text.see(tk.END)\n        # \u884c\u6570\u5236\u9650\uff08\u91cd\u304f\u306a\u308b\u306e\u3092\u9632\u3050\u305f\u3081\u6700\u65b050\u884c\uff09\n        if int(self.log_text.index('end-1c').split('.')[0]) &gt; 50:\n            self.log_text.delete('1.0', '2.0')\n        self.log_text.config(state='disabled')\n\n    def toggle_engine(self):\n        if params.engine_mode == \"ANALOG\": params.engine_mode = \"DIGITAL\"; self.btn_engine.config(text=\"MODE: DIGITAL (SoundFont)\", bg=\"#44AAEE\")\n        else: params.engine_mode = \"ANALOG\"; self.btn_engine.config(text=\"MODE: ANALOG (Scipy)\", bg=\"#EEAA44\")\n    def toggle_filter(self):\n        modes = [\"LPF\", \"BPF\", \"HPF\"]; current = params.filter_mode\n        next_mode = modes[(modes.index(current) + 1) % 3]; params.filter_mode = next_mode\n        if self.btn_filter: self.btn_filter.config(text=f\"FILTER: {next_mode}\")\n    def toggle_scope(self):\n        if self.scope_mode == \"OFF\": self.scope_mode = \"WAVE\"; self.btn_scope.config(text=\"SCOPE: WAVE\", bg=\"black\", fg=\"#00FF00\")\n        elif self.scope_mode == \"WAVE\": self.scope_mode = \"FFT\"; self.btn_scope.config(text=\"SCOPE: FFT\", bg=\"#224422\", fg=\"#00FF00\")\n        else: self.scope_mode = \"OFF\"; self.btn_scope.config(text=\"SCOPE: OFF (Stable)\", bg=\"#444444\", fg=\"#AAAAAA\"); \n        if self.scope_canvas: self.scope_canvas.delete(\"all\")\n    def draw_scope_scheduled(self, data):\n        if self.scope_mode == \"OFF\" or not self.root or not self.scope_canvas: return\n        try:\n            w, h = 250, 100; mid = h\/2; self.scope_canvas.delete(\"all\"); disp_data = data \/ 32768.0\n            if self.scope_mode == \"WAVE\":\n                self.scope_canvas.create_line(0, mid, w, mid, fill=\"#003300\", dash=(2, 4))\n                frames = len(disp_data) \/\/ 2; step = max(1, frames \/\/ w); pts = []\n                for i in range(w):\n                    idx = int(i * step) * 2 \n                    if idx &lt; len(disp_data): pts.extend([i, mid - disp_data[idx] * 40.0])\n                if len(pts) &gt; 2: self.scope_canvas.create_line(pts, fill=\"#00FF00\", width=1.5)\n            else:\n                window = np.hanning(len(disp_data)); fft_res = np.abs(np.fft.rfft(disp_data * window)); fft_res = fft_res \/ len(disp_data) * 100.0; pts = []\n                num_bins = min(len(fft_res), 300)\n                for i in range(num_bins):\n                    x = i * (w \/ num_bins); mag = np.clip(fft_res[i] * h * 0.8, 0, h); pts.extend([x, h - mag])\n                if len(pts) &gt; 2: self.scope_canvas.create_line(pts, fill=\"#00FFFF\", width=1.5)\n        except: pass\n    def set_digital_filter(self):\n        if self.var_dig_flt: params.digital_filter_on = self.var_dig_flt.get()\n    def change_instr_relative(self, delta):\n        new_prog = (params.sf_program + delta) % 128; params.sf_program = new_prog; fluid_engine.change_program(new_prog)\n        name = GM_INSTRUMENTS[new_prog]\n        if self.instr_label: self.instr_label.config(text=f\"{new_prog}: {name}\", fg=\"cyan\")\n\ngui = GUIManager()\n\n# --- Audio Callback ---\nlast_audio_data = np.zeros(1024, dtype=np.int16)\n\ndef sd_callback(outdata, frames, time_info, status):\n    if status: print(f\"Stat: {status}\", file=sys.stderr)\n    if not is_running: outdata.fill(0); return\n\n    if params.engine_mode == \"ANALOG\":\n        raw = vco.get(frames, np.mean(lfo.get(frames)))\n        an_mono = raw * amp.get_val()\n        an_stereo_int16 = (np.repeat(an_mono, 2) * 0.8 * 32767).astype(np.int16)\n    else: an_stereo_int16 = np.zeros(frames * 2, dtype=np.int16)\n\n    dig_stereo_int16 = fluid_engine.get_samples(frames)\n    if params.engine_mode == \"ANALOG\": dig_stereo_int16 *= 0\n    else: an_stereo_int16 *= 0\n    \n    mixed_int16 = np.clip(an_stereo_int16.astype(np.int32) + dig_stereo_int16.astype(np.int32), -32768, 32767).astype(np.int16)\n\n    apply_filter = False\n    if params.engine_mode == \"ANALOG\": apply_filter = True\n    elif params.engine_mode == \"DIGITAL\": apply_filter = params.digital_filter_on\n\n    if apply_filter:\n        v_env = vcf.get_val()\n        cut = params.cutoff_base + (params.vcf_env_amt * v_env)\n        final_out = master_filter.process(mixed_int16, cut, params.resonance)\n    else:\n        vcf.get_val(); final_out = mixed_int16\n\n    final_out = (final_out * params.master_gain).astype(np.int16)\n    \n    global last_audio_data\n    if len(last_audio_data) != len(final_out): last_audio_data = np.zeros_like(final_out)\n    last_audio_data[:] = final_out\n    outdata[:] = final_out.reshape(-1, 2)\n\ndef gui_update_loop():\n    if is_running and gui.root:\n        gui.draw_scope_scheduled(last_audio_data)\n        gui.root.after(100, gui_update_loop)\n\ndef handle_cc(n, v):\n    norm=v\/127.0\n    if n==8: params.master_gain=norm; gui.update_slider(gui.var_vol,norm); return\n    if n==7: w=[\"sawtooth\",\"square\",\"sine\",\"triangle\"][min(3,int(norm*4))]; params.waveform=w; gui.update_wave_label(w); return\n    if n==1: params.lfo_rate=0.1+norm*19.9\n    elif n==2: params.lfo_depth=norm\n    elif n==3: params.vcf_attack=0.01+norm*2; gui.update_slider(gui.disp_a, params.vcf_attack)\n    elif n==4: params.vcf_decay=0.01+norm*2; gui.update_slider(gui.disp_d, params.vcf_decay)\n    elif n==5: params.vcf_sustain=norm; gui.update_slider(gui.disp_s, params.vcf_sustain)\n    elif n==6: params.vcf_release=0.01+norm*3; gui.update_slider(gui.disp_r, params.vcf_release)\n\ndef midi_loop(midi_in):\n    while is_running:\n        msg = midi_in.get_message()\n        if msg:\n            m, _ = msg\n            if len(m) &gt;= 2: process_midi_event(m[0], m[1], m[2] if len(m)&gt;2 else 0)\n        time.sleep(0.001)\n\ndef get_note_name(note_num):\n    notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']\n    octave = note_num \/\/ 12 - 1\n    name = notes[note_num % 12]\n    return f\"{name}{octave}\"\n\ndef process_midi_event(status, data1, data2):\n    global active_note\n    st = status &amp; 0xF0\n    \n    # --- \u2605MIDI LOGGING ---\n    log_msg = \"\"\n    \n    if st == 0x90 and data2 &gt; 0: # Note On\n        n, v = data1, data2\n        v = v + 40\n        # Velocity Correction\n        if v &gt; 127 : v =127\n        \n        log_msg = f\"Note On : {get_note_name(n)} ({n}) vel:{v}\"\n        \n        if params.engine_mode == \"ANALOG\": \n            vco.set_f(440.0*(2**((n-69)\/12.0))); amp.trigger(); vcf.trigger(); active_note=n; gui.set_gate(True)\n        else: \n            fluid_engine.note_on(n, v); vcf.trigger(); gui.set_gate(True)\n            \n    elif (st == 0x80) or (st == 0x90 and data2 == 0): # Note Off\n        n = data1\n        log_msg = f\"Note Off: {get_note_name(n)} ({n})\"\n        \n        if params.engine_mode == \"ANALOG\": \n            if n == active_note: amp.release(); vcf.release(); active_note=None; gui.set_gate(False)\n        else: \n            fluid_engine.note_off(n); vcf.release(); gui.set_gate(False)\n            \n    elif st == 0xB0: # CC\n        log_msg = f\"Control : #{data1} val:{data2}\"\n        handle_cc(data1, data2)\n        \n    elif st == 0xC0: # PC\n        log_msg = f\"Prog Chg: {data1} ({GM_INSTRUMENTS[data1] if data1&lt;128 else '?'})\"\n        if params.engine_mode == \"DIGITAL\":\n            params.sf_program = data1; fluid_engine.change_program(data1)\n            name = GM_INSTRUMENTS[data1] if data1 &lt; len(GM_INSTRUMENTS) else \"Unknown\"\n            if gui.root: gui.instr_label.config(text=f\"{data1}: {name}\", fg=\"cyan\")\n            \n    # GUI Log Update\n    if log_msg:\n        # Append hex raw data\n        raw_str = f\"[{status:02X} {data1:02X} {data2:02X}]\"\n        gui.log_midi(f\"{raw_str} {log_msg}\")\n\nclass MidiPlayer:\n    def __init__(self): self.filename = None; self.is_playing = False; self.thread = None\n    def load_file(self):\n        f = filedialog.askopenfilename(filetypes=[(\"MIDI Files\", \"*.mid\"), (\"All Files\", \"*.*\")])\n        if f: self.filename = f; short_name = os.path.basename(f); gui.update_filename(f\"File: {short_name}\"); print(f\"Loaded: {f}\")\n    def play(self):\n        if not self.filename or self.is_playing: return\n        self.is_playing = True; self.thread = threading.Thread(target=self._play_thread, daemon=True); self.thread.start()\n    def stop(self):\n        self.is_playing = False; process_midi_event(0xB0, 120, 0)\n        if params.engine_mode == \"DIGITAL\":\n            for i in range(128): fluid_engine.note_off(i)\n    def _play_thread(self):\n        try:\n            mid = mido.MidiFile(self.filename, clip=True); print(f\"Start playing: {self.filename}\")\n            for msg in mid.play():\n                if not self.is_playing or not is_running: break\n                if msg.type == 'note_on': process_midi_event(0x90, msg.note, msg.velocity)\n                elif msg.type == 'note_off': process_midi_event(0x80, msg.note, msg.velocity)\n                elif msg.type == 'control_change': process_midi_event(0xB0, msg.control, msg.value)\n                elif msg.type == 'program_change': process_midi_event(0xC0, msg.program, 0)\n            self.is_playing = False; print(\"Finished.\")\n        except Exception as e: print(f\"Error: {e}\"); self.is_playing = False\n\nmidi_player = MidiPlayer()\n\ndef cleanup_and_exit():\n    global is_running\n    print(\"\\nShutting down...\")\n    is_running = False \n    midi_player.stop()\n    if 'stream' in globals(): stream.stop(); stream.close()\n    if 'mi' in globals(): mi.close_port()\n    if gui.root: gui.root.destroy()\n    print(\"Bye!\")\n    sys.exit()\n\ndef create_panel():\n    root=tk.Tk(); gui.root=root\n    root.title(\"Synth v43 (MIDI Monitor)\"); root.geometry(\"640x820\"); style=ttk.Style(); style.theme_use('clam')\n    root.protocol(\"WM_DELETE_WINDOW\", cleanup_and_exit)\n    tk.Label(root, text=f\"Audio: {AUDIO_DEVICE_NAME} (ID:{AUDIO_DEVICE_ID}) | MIDI: {MIDI_PORT_NAME}\", bg=\"#333333\", fg=\"white\", font=(\"Arial\", 9)).pack(fill=tk.X, side=tk.TOP)\n    \n    gui.disp_a=tk.DoubleVar(value=params.vcf_attack); gui.disp_d=tk.DoubleVar(value=params.vcf_decay)\n    gui.disp_s=tk.DoubleVar(value=params.vcf_sustain); gui.disp_r=tk.DoubleVar(value=params.vcf_release)\n    gui.var_vol=tk.DoubleVar(value=params.master_gain); gui.var_cut=tk.DoubleVar(value=params.cutoff_base)\n    gui.var_res=tk.DoubleVar(value=params.resonance); gui.var_dig_flt = tk.BooleanVar(value=params.digital_filter_on)\n\n    top=tk.Frame(root,bg=\"black\",pady=5); top.pack(fill=tk.X)\n    cv=tk.Canvas(top,width=30,height=30,bg=\"black\",highlightthickness=0); cv.pack(side=tk.LEFT,padx=10)\n    gui.gate_lamp=cv; cv.create_oval(5,5,25,25,fill=\"#440000\",outline=\"gray\",tags=\"lamp\")\n    gui.btn_engine=tk.Button(top, text=\"MODE: ANALOG (Scipy)\", bg=\"#EEAA44\", fg=\"white\", font=(\"Arial\",12,\"bold\"), command=gui.toggle_engine); gui.btn_engine.pack(side=tk.LEFT, padx=100)\n    tk.Button(top, text=\"QUIT\", bg=\"#FF4444\", fg=\"white\", font=(\"Arial\",10,\"bold\"), command=cleanup_and_exit).pack(side=tk.RIGHT, padx=10)\n\n    main=ttk.Frame(root,padding=5); main.pack(fill=tk.BOTH,expand=True)\n    l=ttk.LabelFrame(main,text=\"Global Filter &amp; Scope\",padding=5); l.pack(side=tk.LEFT,fill=tk.BOTH,expand=True)\n    sc_frame = tk.Frame(l, bg=\"black\", bd=2, relief=tk.SUNKEN); sc_frame.pack(pady=5)\n    gui.scope_canvas=tk.Canvas(sc_frame,width=250,height=100,bg=\"black\",highlightthickness=0); gui.scope_canvas.pack()\n    gui.lbl_wave = tk.Label(l, text=\"WAVE: SAWTOOTH\", bg=\"#222222\", fg=\"cyan\", font=(\"Arial\", 10, \"bold\")); gui.lbl_wave.pack(fill=tk.X)\n    gui.btn_scope = tk.Button(l, text=\"SCOPE: OFF (Stable)\", bg=\"#444444\", fg=\"#AAAAAA\", font=(\"Arial\", 8), command=gui.toggle_scope); gui.btn_scope.pack(fill=tk.X, pady=2)\n    gui.btn_filter=tk.Button(l, text=\"FILTER: LPF\", bg=\"#4444AA\", fg=\"white\", font=(\"Arial\",10,\"bold\"), command=gui.toggle_filter); gui.btn_filter.pack(fill=tk.X, pady=5)\n    \n    def ms(p,l,v,mx,at):\n        f=ttk.Frame(p); f.pack(fill=tk.X); ttk.Label(f,text=l,width=5).pack(side=tk.LEFT)\n        ttk.Scale(f,from_=50 if at==\"cutoff_base\" else 0, to=mx,variable=v,command=lambda x: setattr(params,at,float(x))).pack(side=tk.LEFT,fill=tk.X,expand=True)\n    ms(l,\"Cut\",gui.var_cut,5000,\"cutoff_base\"); ms(l,\"Res\",gui.var_res,0.9,\"resonance\")\n\n    r=ttk.LabelFrame(main,text=\"Digital \/ Player\",padding=5); r.pack(side=tk.RIGHT,fill=tk.BOTH,expand=True)\n    gui.instr_label=tk.Label(r,text=\"0: Grand Piano\", font=(\"Arial\",14,\"bold\"), fg=\"cyan\", bg=\"#222222\", width=20); gui.instr_label.pack(pady=5)\n    instr_btns = ttk.Frame(r); instr_btns.pack(fill=tk.X, pady=2)\n    tk.Button(instr_btns, text=\"&lt; Prev\", width=10, command=lambda: gui.change_instr_relative(-1)).pack(side=tk.LEFT, padx=5, expand=True)\n    tk.Button(instr_btns, text=\"Next &gt;\", width=10, command=lambda: gui.change_instr_relative(1)).pack(side=tk.LEFT, padx=5, expand=True)\n    fk = ttk.Frame(r); fk.pack(fill=tk.X, pady=5)\n    cb = ttk.Checkbutton(fk, text=\"Filter Enable (Digital Mode)\", variable=gui.var_dig_flt, command=gui.set_digital_filter); cb.pack(anchor=tk.CENTER)\n    mp = ttk.LabelFrame(r, text=\"MIDI File Player\", padding=5); mp.pack(fill=tk.X, pady=5)\n    gui.lbl_filename = tk.Label(mp, text=\"No File Selected\", font=(\"Arial\", 8)); gui.lbl_filename.pack(fill=tk.X)\n    btn_box = ttk.Frame(mp); btn_box.pack(fill=tk.X)\n    tk.Button(btn_box, text=\"Load\", command=midi_player.load_file, bg=\"#DDDDDD\").pack(side=tk.LEFT, fill=tk.X, expand=True)\n    tk.Button(btn_box, text=\"Play\", command=midi_player.play, bg=\"#AAFFAA\").pack(side=tk.LEFT, fill=tk.X, expand=True)\n    tk.Button(btn_box, text=\"Stop\", command=midi_player.stop, bg=\"#FFAAAA\").pack(side=tk.LEFT, fill=tk.X, expand=True)\n    ttk.Label(r,text=\"Filter ADSR\").pack(pady=2)\n    def me(l,v,c):\n        f=ttk.Frame(r); f.pack(fill=tk.X); ttk.Label(f,text=l,width=3).pack(side=tk.LEFT)\n        ttk.Scale(f,from_=0.01,to=2.0,variable=v,command=lambda x: setattr(params,f\"vcf_{c}\",float(x))).pack(side=tk.LEFT,fill=tk.X,expand=True)\n    me(\"A\",gui.disp_a,\"attack\"); me(\"D\",gui.disp_d,\"decay\"); me(\"S\",gui.disp_s,\"sustain\"); me(\"R\",gui.disp_r,\"release\")\n    ttk.Label(r,text=\"Vol (K8)\").pack(pady=2)\n    ttk.Scale(r,from_=0,to=1,variable=gui.var_vol,command=lambda x: handle_cc(8,float(x)*127)).pack(fill=tk.X)\n    \n    # \u2605MIDI Monitor Frame\n    log_f = ttk.LabelFrame(root, text=\"MIDI Monitor (Recent 50)\", padding=5)\n    log_f.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)\n    gui.log_text = tk.Text(log_f, height=6, state='disabled', bg=\"black\", fg=\"#00FF00\", font=(\"Consolas\", 9))\n    gui.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)\n    sb = ttk.Scrollbar(log_f, orient=\"vertical\", command=gui.log_text.yview)\n    sb.pack(side=tk.RIGHT, fill=tk.Y)\n    gui.log_text.config(yscrollcommand=sb.set)\n\n    gui.root.after(100, gui_update_loop)\n    return root\n\nif __name__ == \"__main__\":\n    fluid_engine.init_synth()\n    mi=rtmidi.MidiIn()\n    ports = mi.get_ports(); found_idx = -1\n    for i, p_name in enumerate(ports):\n        if \"Midi Through\" not in p_name: found_idx = i; break\n    if found_idx != -1: mi.open_port(found_idx); print(f\"MIDI Connected: {ports[found_idx]}\")\n    else: mi.open_virtual_port(\"PySynth\"); print(\"MIDI: Virtual Port\")\n    threading.Thread(target=midi_loop,args=(mi,),daemon=True).start()\n    \n    print(\"\\n--- Audio Hardware Scan ---\")\n    devs = sd.query_devices()\n    target_id = None\n    \n    for i, d in enumerate(devs):\n        if d['max_output_channels'] &gt; 0 and TARGET_DEVICE_KEYWORD in d['name']:\n            target_id = i\n            AUDIO_DEVICE_NAME = d['name']\n            AUDIO_DEVICE_ID = i\n            print(f\"--&gt; FOUND TARGET: {d['name']} (ID: {i})\")\n            break\n            \n    if target_id is None:\n        target_id = sd.default.device[1]\n        AUDIO_DEVICE_NAME = \"Default (Target not found)\"\n        print(\"--&gt; Using Default Device\")\n\n    stream = sd.OutputStream(\n        device=target_id,\n        samplerate=SAMPLE_RATE,\n        channels=CHANNELS,\n        dtype=DTYPE,\n        blocksize=BLOCK_SIZE,\n        latency='high',\n        callback=sd_callback\n    )\n    \n    stream.start()\n    print(f\"Stream Started: {SAMPLE_RATE}Hz \/ {BLOCK_SIZE} frames\")\n    \n    root=create_panel()\n    try: root.mainloop()\n    except: pass\n<\/code><\/pre><\/div>\n\n<\/div>\n\t\t<\/div>\n<\/div>\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\">\n<figure class=\"wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-3 is-layout-flex wp-block-gallery-is-layout-flex\">\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"610\" height=\"495\" data-id=\"2305\" src=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-29-120940.png\" alt=\"\" class=\"wp-image-2305\" srcset=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-29-120940.png 610w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-29-120940-300x243.png 300w\" sizes=\"auto, (max-width: 610px) 100vw, 610px\" \/><\/figure>\n<\/figure>\n<\/div>\n<\/div>\n\n\n<div class=\"wp-block-ub-content-toggle wp-block-ub-content-toggle-block\" id=\"ub-content-toggle-block-e269d5f9-35f3-4405-9045-58d7a4bb2330\" data-mobilecollapse=\"false\" data-desktopcollapse=\"true\" data-preventcollapse=\"false\" data-showonlyone=\"false\">\n<div class=\"wp-block-ub-content-toggle-accordion\" style=\"border-color: #f1f1f1;\" id=\"ub-content-toggle-panel-block-\">\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-title-wrap\" style=\"background-color: #f1f1f1;\" aria-controls=\"ub-content-toggle-panel-0-e269d5f9-35f3-4405-9045-58d7a4bb2330\" tabindex=\"0\">\n\t\t\t<p class=\"wp-block-ub-content-toggle-accordion-title ub-content-toggle-title-e269d5f9-35f3-4405-9045-58d7a4bb2330\" style=\"color: #000000; \">ST7789\u7248\uff08Python\uff09<\/p>\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-toggle-wrap right\" style=\"color: #000000;\"><span class=\"wp-block-ub-content-toggle-accordion-state-indicator wp-block-ub-chevron-down\"><\/span><\/div>\n\t\t<\/div>\n\t\t\t<div role=\"region\" aria-expanded=\"false\" class=\"wp-block-ub-content-toggle-accordion-content-wrap ub-hide\" id=\"ub-content-toggle-panel-0-e269d5f9-35f3-4405-9045-58d7a4bb2330\">\n\n<p><\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code># \u30b3\u30fc\u30c9\u540d: py_synth_v60_arpeggiator\n# \u30d0\u30fc\u30b8\u30e7\u30f3: 60.0 (Chord Arpeggiator)\n# Description: Page 8\u306b\u30a2\u30eb\u30da\u30b8\u30a8\u30fc\u30bf\u30fc\u6a5f\u80fd(BPM\/Pattern)\u3092\u8ffd\u52a0\u3002\n\nimport os\nos.environ['OMP_NUM_THREADS'] = '1'\n\nimport sys\nimport time\nimport threading\nimport math\nimport subprocess\nimport random\nimport numpy as np\nimport rtmidi\nimport fluidsynth\nimport digitalio\nimport board\nimport mido \n\nfrom PIL import Image, ImageDraw, ImageFont\nfrom adafruit_rgb_display import st7789\n\n# --- \u30e9\u30a4\u30d6\u30e9\u30ea\u30c1\u30a7\u30c3\u30af ---\ntry:\n    import sounddevice as sd\nexcept ImportError:\n    sys.exit(\"Error: pip install sounddevice\")\ntry:\n    from scipy.signal import lfilter\nexcept ImportError:\n    sys.exit(\"Error: pip install scipy\")\n\n# --- \u8a2d\u5b9a ---\nTARGET_DEVICE_KEYWORD = \"USB\" \nSAMPLE_RATE = 48000\nBLOCK_SIZE = 256\nCHANNELS = 2\nDTYPE = 'int16'\nMASTER_GAIN_DEFAULT = 0.5\nSF2_PATH = \"\/usr\/share\/sounds\/sf2\/FluidR3_GM.sf2\"\nMIDI_DIR = \".\/midi\"\n\n# LCD Pins\nCS_PIN = board.D8\nDC_PIN = board.D27\nRST_PIN = board.D17\nBAUDRATE = 24000000\n\nis_running = True\nlast_audio_data = np.zeros(BLOCK_SIZE, dtype=np.int16)\nmidi_logs = [] \nlast_drum_name = \"\" \n\n# --- \u30b3\u30fc\u30c9\u5b9a\u7fa9 ---\nCHORD_TYPES = [\n    {\"name\": \"OFF\",      \"offsets\": [0]},\n    {\"name\": \"Major\",    \"offsets\": [0, 4, 7]},\n    {\"name\": \"Minor\",    \"offsets\": [0, 3, 7]},\n    {\"name\": \"Maj7\",     \"offsets\": [0, 4, 7, 11]},\n    {\"name\": \"Min7\",     \"offsets\": [0, 3, 7, 10]},\n    {\"name\": \"Sus4\",     \"offsets\": [0, 5, 7]},\n    {\"name\": \"5th(Pwr)\", \"offsets\": [0, 7]},\n    {\"name\": \"Octave\",   \"offsets\": [0, 12]}\n]\n\n# --- \u30a2\u30eb\u30da\u30b8\u30aa\u30d1\u30bf\u30fc\u30f3 ---\nARP_PATTERNS = [\"OFF\", \"UP\", \"DOWN\", \"RAND\"]\n\n# --- GM\u697d\u5668\u540d ---\nGM_INSTRUMENTS = [\n    \"Grand Piano\", \"Bright Piano\", \"E.Grand Piano\", \"Honky-tonk\", \"E.Piano 1\", \"E.Piano 2\", \"Harpsichord\", \"Clavinet\",\n    \"Celesta\", \"Glockenspiel\", \"Music Box\", \"Vibraphone\", \"Marimba\", \"Xylophone\", \"Tubular Bells\", \"Dulcimer\",\n    \"Drawbar Organ\", \"Percussive Organ\", \"Rock Organ\", \"Church Organ\", \"Reed Organ\", \"Accordion\", \"Harmonica\", \"Tango Accordion\",\n    \"Guit(Nylon)\", \"Guit(Steel)\", \"Guit(Jazz)\", \"Guit(Clean)\", \"Guit(Muted)\", \"Guit(Overdrive)\", \"Guit(Distortion)\", \"Guit(Harmonics)\",\n    \"Acoustic Bass\", \"E.Bass(Finger)\", \"E.Bass(Pick)\", \"Fretless Bass\", \"Slap Bass 1\", \"Slap Bass 2\", \"Synth Bass 1\", \"Synth Bass 2\",\n    \"Violin\", \"Viola\", \"Cello\", \"Contrabass\", \"Tremolo Strings\", \"Pizzicato Strings\", \"Orchestral Harp\", \"Timpani\",\n    \"Strings 1\", \"Strings 2\", \"SynthStrings 1\", \"SynthStrings 2\", \"Choir Aahs\", \"Voice Oohs\", \"Synth Voice\", \"Orchestra Hit\",\n    \"Trumpet\", \"Trombone\", \"Tuba\", \"Muted Trumpet\", \"French Horn\", \"Brass Section\", \"Synth Brass 1\", \"Synth Brass 2\",\n    \"Soprano Sax\", \"Alto Sax\", \"Tenor Sax\", \"Baritone Sax\", \"Oboe\", \"English Horn\", \"Bassoon\", \"Clarinet\",\n    \"Piccolo\", \"Flute\", \"Recorder\", \"Pan Flute\", \"Blown Bottle\", \"Shakuhachi\", \"Whistle\", \"Ocarina\",\n    \"Lead 1 (square)\", \"Lead 2 (saw)\", \"Lead 3 (calliope)\", \"Lead 4 (chiff)\", \"Lead 5 (charang)\", \"Lead 6 (voice)\", \"Lead 7 (fifths)\", \"Lead 8 (bass+lead)\",\n    \"Pad 1 (new age)\", \"Pad 2 (warm)\", \"Pad 3 (polysynth)\", \"Pad 4 (choir)\", \"Pad 5 (bowed)\", \"Pad 6 (metallic)\", \"Pad 7 (halo)\", \"Pad 8 (sweep)\",\n    \"FX 1 (rain)\", \"FX 2 (soundtrack)\", \"FX 3 (crystal)\", \"FX 4 (atmosphere)\", \"FX 5 (brightness)\", \"FX 6 (goblins)\", \"FX 7 (echoes)\", \"FX 8 (sci-fi)\",\n    \"Sitar\", \"Banjo\", \"Shamisen\", \"Koto\", \"Kalimba\", \"Bag pipe\", \"Fiddle\", \"Shanai\",\n    \"Tinkle Bell\", \"Agogo\", \"Steel Drums\", \"Woodblock\", \"Taiko Drum\", \"Melodic Tom\", \"Synth Drum\", \"Reverse Cymbal\",\n    \"Guit Fret Noise\", \"Breath Noise\", \"Seashore\", \"Bird Tweet\", \"Telephone Ring\", \"Helicopter\", \"Applause\", \"Gunshot\"\n]\n\n# --- \u30c9\u30e9\u30e0\u30de\u30c3\u30d7 ---\nDRUM_MAP = {\n    36: \"Bass Drum 1\", 37: \"Side Stick\", 38: \"Acoustic Snare\", 39: \"Hand Clap\",\n    40: \"Electric Snare\", 41: \"Low Floor Tom\", 42: \"Closed Hi-Hat\", 43: \"High Floor Tom\",\n    44: \"Pedal Hi-Hat\", 45: \"Low Tom\", 46: \"Open Hi-Hat\", 47: \"Low-Mid Tom\",\n    48: \"Hi-Mid Tom\", 49: \"Crash Cymbal 1\", 50: \"High Tom\", 51: \"Ride Cymbal 1\"\n}\n\nclass SynthParams:\n    def __init__(self):\n        self.engine_mode = \"ANALOG\" \n        self.master_gain = MASTER_GAIN_DEFAULT\n        self.waveform = \"sawtooth\"\n        self.lfo_rate = 5.0; self.lfo_depth = 0.0\n        self.filter_mode = \"LPF\"; self.cutoff_base = 1000.0; self.resonance = 0.5; self.vcf_env_amt = 4000.0\n        self.vcf_attack = 0.1; self.vcf_decay = 0.3; self.vcf_sustain = 0.4; self.vcf_release = 0.5\n        self.amp_attack = 0.05; self.amp_decay = 0.2; self.amp_sustain = 0.6; self.amp_release = 0.5\n        self.sf_program = 0; self.sf_vol = 1.0\n        self.reverb_send = 40 \n        self.chorus_send = 0\n        \n        # Chord &amp; Arp\n        self.chord_type_idx = 0 \n        self.arp_pattern_idx = 0 # 0=OFF\n        self.arp_bpm = 120.0\n        \n        self.page_index = 0 \n        self.gate_status = False\n\nparams = SynthParams()\n\n# --- Helper Functions for System Info ---\ndef get_ip_address():\n    try:\n        cmd = \"hostname -I | cut -d' ' -f1\"\n        return subprocess.check_output(cmd, shell=True).decode(\"utf-8\").strip()\n    except: return \"Unknown\"\n\ndef get_cpu_temp():\n    try:\n        with open(\"\/sys\/class\/thermal\/thermal_zone0\/temp\", \"r\") as f:\n            temp = float(f.read()) \/ 1000.0\n        return temp\n    except: return 0.0\n\ndef get_disk_usage():\n    try:\n        cmd = \"df -h \/ | awk 'NR==2{print $3 \\\"\/\\\" $2 \\\" (\\\" $5 \\\")\\\"}'\"\n        return subprocess.check_output(cmd, shell=True).decode(\"utf-8\").strip()\n    except: return \"?\"\n\ndef get_ram_usage():\n    try:\n        cmd = \"free -m | awk 'NR==2{printf \\\"%s\/%sMB (%.0f%%)\\\", $3,$2,$3*100\/$2 }'\"\n        return subprocess.check_output(cmd, shell=True).decode(\"utf-8\").strip()\n    except: return \"?\"\n\n# --- MIDI Player Class ---\nclass MidiPlayer:\n    def __init__(self):\n        self.files = []\n        self.selected_idx = 0\n        self.is_playing = False\n        self.stop_signal = False\n        self.thread = None\n        self.scan_files()\n\n    def scan_files(self):\n        self.files = []\n        if os.path.exists(MIDI_DIR):\n            for f in os.listdir(MIDI_DIR):\n                if f.lower().endswith(\".mid\"):\n                    self.files.append(f)\n            self.files.sort()\n        else:\n            try:\n                os.makedirs(MIDI_DIR)\n                print(f\"Created {MIDI_DIR}\")\n            except: pass\n        if not self.files:\n            self.files = [\"(No Files)\"]\n\n    def select_file(self, norm_val):\n        count = len(self.files)\n        self.selected_idx = min(count - 1, int(norm_val * count))\n\n    def play(self):\n        if self.is_playing: return\n        if self.files[0] == \"(No Files)\": return\n        self.stop_signal = False\n        self.is_playing = True\n        self.thread = threading.Thread(target=self._play_thread, daemon=True)\n        self.thread.start()\n\n    def stop(self):\n        if self.is_playing:\n            self.stop_signal = True\n            for ch in range(16):\n                process_midi_event(0xB0 + ch, 120, 0)\n                process_midi_event(0xB0 + ch, 123, 0)\n\n    def _play_thread(self):\n        filename = os.path.join(MIDI_DIR, self.files[self.selected_idx])\n        try:\n            mid = mido.MidiFile(filename)\n            print(f\"Playing: {filename}\")\n            if params.engine_mode == \"ANALOG\":\n                params.engine_mode = \"DIGITAL1\"\n            for msg in mid.play():\n                if self.stop_signal or not is_running: break\n                if msg.type == 'note_on': process_midi_event(0x90 | msg.channel, msg.note, msg.velocity)\n                elif msg.type == 'note_off': process_midi_event(0x80 | msg.channel, msg.note, msg.velocity)\n                elif msg.type == 'control_change': process_midi_event(0xB0 | msg.channel, msg.control, msg.value)\n                elif msg.type == 'program_change': process_midi_event(0xC0 | msg.channel, msg.program, 0)\n            print(\"Playback Finished.\")\n        except Exception as e:\n            print(f\"MIDI Error: {e}\")\n        self.is_playing = False\n        self.stop_signal = False\n\nmidi_player = MidiPlayer()\n\nclass FluidManager:\n    def __init__(self):\n        self.fs = None; self.ready = False\n    def init_synth(self):\n        try:\n            self.fs = fluidsynth.Synth()\n            self.fs.setting('synth.sample-rate', float(SAMPLE_RATE))\n            self.fs.setting('synth.gain', 1.0)\n            self.fs.setting('synth.polyphony', 64)\n            self.fs.setting('synth.reverb.active', 'yes')\n            self.fs.setting('synth.chorus.active', 'yes')\n            \n            sfid = self.fs.sfload(SF2_PATH)\n            self.fs.program_select(0, sfid, 0, 0)\n            res = self.fs.program_select(9, sfid, 128, 0)\n            if res != 0: self.fs.program_select(9, sfid, 120, 0)\n            self.fs.program_change(9, 0)\n            \n            self.fs.cc(0, 91, params.reverb_send)\n            self.fs.cc(0, 93, params.chorus_send)\n\n            self.ready = True\n            print(\"FluidSynth Ready (Reverb\/Chorus ON).\")\n        except Exception as e:\n            print(f\"FluidSynth Error: {e}\"); self.ready = False\n            \n    def get_samples(self, n):\n        if not self.ready: return np.zeros(n * 2, dtype=np.int16)\n        return np.array(self.fs.get_samples(n), dtype=np.int16)\n    \n    def note_on(self, ch, n, v):\n        if self.ready: self.fs.noteon(ch, n, v)\n    def note_off(self, ch, n):\n        if self.ready: self.fs.noteoff(ch, n)\n    def change_program(self, ch, p):\n        if self.ready: self.fs.program_change(ch, int(p))\n    \n    def send_cc(self, ch, cc, val):\n        if self.ready: self.fs.cc(ch, cc, int(val))\n\nfluid_engine = FluidManager()\n\n# --- Arpeggiator ---\nclass Arpeggiator:\n    def __init__(self):\n        self.current_notes = [] # [(note, vel)]\n        self.playing_note = None\n        self.step_idx = 0\n        self.last_step_time = 0\n        threading.Thread(target=self._loop, daemon=True).start()\n    \n    def set_notes(self, notes_with_vel):\n        # notes_with_vel: list of (n, v)\n        self.current_notes = notes_with_vel\n        self.current_notes.sort(key=lambda x: x[0]) # Default sort UP\n        \n    def _loop(self):\n        while is_running:\n            mode = ARP_PATTERNS[params.arp_pattern_idx]\n            \n            # ARP OFF: Do nothing (Logic handled in process_midi)\n            if mode == \"OFF\" or not self.current_notes:\n                if self.playing_note is not None:\n                    fluid_engine.note_off(0, self.playing_note)\n                    self.playing_note = None\n                time.sleep(0.01)\n                continue\n\n            # ARP ON\n            step_duration = 60.0 \/ params.arp_bpm \/ 2.0 # 8th notes\n            now = time.time()\n            \n            if now - self.last_step_time &gt;= step_duration:\n                # Stop prev\n                if self.playing_note is not None:\n                    fluid_engine.note_off(0, self.playing_note)\n                \n                # Select Next Note\n                notes = self.current_notes[:] # Copy\n                if not notes: continue\n\n                if mode == \"UP\":\n                    pass # Sorted\n                elif mode == \"DOWN\":\n                    notes.reverse()\n                elif mode == \"RAND\":\n                    random.shuffle(notes)\n                \n                # Safe idx\n                self.step_idx %= len(notes)\n                n, v = notes[self.step_idx]\n                \n                fluid_engine.note_on(0, n, v)\n                self.playing_note = n\n                \n                if mode == \"RAND\":\n                    self.step_idx = random.randint(0, len(notes)-1)\n                else:\n                    self.step_idx += 1\n                \n                self.last_step_time = now\n            \n            time.sleep(0.005)\n\narp = Arpeggiator()\n\ndef design_biquad(mode, fc, q, fs):\n    w0=2*math.pi*fc\/fs; alpha=math.sin(w0)\/(2*q); c=math.cos(w0); a0=1+alpha\n    if mode==\"LPF\": b=[(1-c)\/2, 1-c, (1-c)\/2]\n    elif mode==\"HPF\": b=[(1+c)\/2, -(1+c), (1+c)\/2]\n    elif mode==\"BPF\": b=[alpha, 0, -alpha]\n    else: return [1,0,0],[1,0,0]\n    return np.array(b)\/a0, np.array([1+alpha,-2*c,1-alpha])\/a0\n\nclass ADSREnvelope:\n    def __init__(self, is_amp=True):\n        self.state='IDLE'; self.level=0.0; self.is_amp=is_amp\n    def trigger(self): self.state='ATTACK'\n    def release(self): self.state='RELEASE'\n    def get_val(self):\n        if self.is_amp: a,d,s,r = params.amp_attack,params.amp_decay,params.amp_sustain,params.amp_release\n        else: a,d,s,r = params.vcf_attack,params.vcf_decay,params.vcf_sustain,params.vcf_release\n        step = float(BLOCK_SIZE)\/SAMPLE_RATE\n        if self.state=='ATTACK':\n            self.level+=step\/max(a,0.001);\n            if self.level&gt;=1.0: self.level,self.state=1.0,'DECAY'\n        elif self.state=='DECAY':\n            self.level-=(step\/max(d,0.001))*(1.0-s);\n            if self.level&lt;=s: self.level,self.state=s,'SUSTAIN'\n        elif self.state=='SUSTAIN': self.level=s\n        elif self.state=='RELEASE':\n            self.level-=step\/max(r,0.001);\n            if self.level&lt;=0.0: self.level,self.state=0.0,'IDLE'\n        return self.level\n\nclass StereoMasterFilter:\n    def __init__(self): self.zi = np.zeros((2, 2))\n    def process(self, sig_stereo_int16, cut, res):\n        sig_float = sig_stereo_int16.astype(np.float32) \/ 32768.0\n        fc = max(50.0, min(cut, SAMPLE_RATE * 0.45)); q = 0.7 + (res * 9.0)\n        b, a = design_biquad(params.filter_mode, fc, q, SAMPLE_RATE)\n        sig_reshaped = sig_float.reshape(-1, 2)\n        out_reshaped, self.zi = lfilter(b, a, sig_reshaped, axis=0, zi=self.zi)\n        return np.clip(out_reshaped.flatten() * 32768.0, -32768, 32767).astype(np.int16)\n\nclass Oscillator:\n    def __init__(self): self.p=0.0; self.f=0.0\n    def set_f(self,f): self.f=f\n    def get(self,n,lfo):\n        if self.f==0: return np.zeros(n)\n        mf=self.f+(lfo*params.lfo_depth*10.0)\n        ph=self.p+np.cumsum(np.full(n,1.0))*(mf\/SAMPLE_RATE)\n        self.p=ph[-1]%1.0; ph%=1.0\n        w=params.waveform\n        if w=='sawtooth': return 2.0*(ph-0.5)\n        elif w=='square': return np.sign(np.sin(2*np.pi*ph))\n        elif w=='sine': return np.sin(2*np.pi*ph)\n        return 2.0*np.abs(2.0*(ph-0.5))-1.0\n\nclass LFO:\n    def __init__(self): self.p=0.0\n    def get(self,n):\n        t=(np.arange(n)+self.p)\/SAMPLE_RATE; self.p+=n\n        if self.p&gt;SAMPLE_RATE: self.p-=SAMPLE_RATE\n        return np.sin(2*np.pi*params.lfo_rate*t)\n\nvco=Oscillator(); lfo=LFO(); amp=ADSREnvelope(True); vcf=ADSREnvelope(False)\nmaster_filter = StereoMasterFilter()\nactive_note=None\n\n# --- Audio Callback ---\ndef sd_callback(outdata, frames, time_info, status):\n    global last_audio_data\n    if not is_running: outdata.fill(0); return\n\n    if params.engine_mode == \"ANALOG\":\n        raw = vco.get(frames, np.mean(lfo.get(frames)))\n        an_mono = raw * amp.get_val()\n        an_stereo_int16 = (np.repeat(an_mono, 2) * 0.8 * 32767).astype(np.int16)\n    else:\n        an_stereo_int16 = np.zeros(frames * 2, dtype=np.int16)\n\n    dig_stereo_int16 = fluid_engine.get_samples(frames)\n    mixed_int16 = np.clip(an_stereo_int16.astype(np.int32) + dig_stereo_int16.astype(np.int32), -32768, 32767).astype(np.int16)\n\n    apply_filter = False\n    if params.engine_mode == \"ANALOG\": apply_filter = True\n    elif params.engine_mode == \"DIGITAL1\": apply_filter = True\n\n    if apply_filter:\n        v_env = vcf.get_val()\n        cut = params.cutoff_base + (params.vcf_env_amt * v_env)\n        final_out = master_filter.process(mixed_int16, cut, params.resonance)\n    else:\n        vcf.get_val() \n        final_out = mixed_int16\n\n    final_out = (final_out * params.master_gain).astype(np.int16)\n    \n    if len(last_audio_data) != len(final_out):\n        last_audio_data = np.zeros_like(final_out)\n    last_audio_data[:] = final_out\n        \n    outdata[:] = final_out.reshape(-1, 2)\n\n# --- Display Manager ---\nclass DisplayManager:\n    def __init__(self):\n        spi = board.SPI()\n        cs = digitalio.DigitalInOut(CS_PIN)\n        dc = digitalio.DigitalInOut(DC_PIN)\n        rst = digitalio.DigitalInOut(RST_PIN)\n        self.disp = st7789.ST7789(\n            spi, cs=cs, dc=dc, rst=rst,\n            baudrate=BAUDRATE,\n            width=240, height=320,\n            x_offset=0, y_offset=0,\n            rotation=0\n        )\n        self.width = 320\n        self.height = 240\n        self.image = Image.new(\"RGB\", (self.width, self.height))\n        self.draw = ImageDraw.Draw(self.image)\n        try:\n            self.font_s = ImageFont.truetype(\"\/usr\/share\/fonts\/truetype\/dejavu\/DejaVuSans.ttf\", 14)\n            self.font_m = ImageFont.truetype(\"\/usr\/share\/fonts\/truetype\/dejavu\/DejaVuSans.ttf\", 18)\n            self.font_l = ImageFont.truetype(\"\/usr\/share\/fonts\/truetype\/dejavu\/DejaVuSans-Bold.ttf\", 24)\n        except:\n            self.font_s = ImageFont.load_default()\n            self.font_m = ImageFont.load_default()\n            self.font_l = ImageFont.load_default()\n        \n        self.last_sys_update = 0\n        self.cache_ip = \"\"; self.cache_disk = \"\"; self.cache_ram = \"\"\n\n    def update(self):\n        self.draw.rectangle((0, 0, self.width, self.height), outline=0, fill=0)\n        \n        gate_color = (255, 0, 0) if params.gate_status else (50, 0, 0)\n        self.draw.ellipse((5, 5, 20, 20), fill=gate_color)\n        page_names = [\"MAIN\", \"LFO\", \"ADSR\/FLT\", \"VISUAL\", \"LOG\", \"PLAYER\", \"SYSTEM\", \"EFFECT\", \"CHORD\"]\n        title = f\"{page_names[params.page_index]} (PG:{params.page_index+1}\/9)\"\n        self.draw.text((30, 5), title, font=self.font_m, fill=(200, 200, 200))\n        self.draw.line((0, 30, self.width, 30), fill=(100, 100, 100))\n\n        if params.page_index == 0: self._draw_page_main()\n        elif params.page_index == 1: self._draw_page_lfo()\n        elif params.page_index == 2: self._draw_page_adsr()\n        elif params.page_index == 3: self._draw_page_scope()\n        elif params.page_index == 4: self._draw_page_log()\n        elif params.page_index == 5: self._draw_page_player()\n        elif params.page_index == 6: self._draw_page_system()\n        elif params.page_index == 7: self._draw_page_effect()\n        elif params.page_index == 8: self._draw_page_chord()\n            \n        self.disp.image(self.image.rotate(90, expand=True))\n\n    def _draw_page_main(self):\n        if params.engine_mode == \"ANALOG\":\n            mode_color = (255, 165, 0)\n            txt = f\"WAVE: {params.waveform.upper()}\"\n        elif params.engine_mode == \"DIGITAL1\":\n            mode_color = (0, 191, 255)\n            p = params.sf_program\n            name = GM_INSTRUMENTS[p] if p &lt; len(GM_INSTRUMENTS) else \"?\"\n            txt = f\"PROG {p}: {name}\"\n        elif params.engine_mode == \"DIGITAL2\":\n            mode_color = (100, 255, 100)\n            p = params.sf_program\n            name = GM_INSTRUMENTS[p] if p &lt; len(GM_INSTRUMENTS) else \"?\"\n            txt = f\"PROG {p}: {name}\"\n        else: \n            mode_color = (255, 100, 255)\n            p = params.sf_program\n            name = GM_INSTRUMENTS[p] if p &lt; len(GM_INSTRUMENTS) else \"?\"\n            txt = f\"PROG {p}: {name}\"\n        self.draw.text((10, 40), f\"MODE: {params.engine_mode}\", font=self.font_l, fill=mode_color)\n        self.draw.text((10, 80), txt, font=self.font_m, fill=(255, 255, 255))\n        if \"DIGITAL\" in params.engine_mode:\n            self.draw.text((10, 110), \"Use K3 to Select Prog\", font=self.font_s, fill=(100, 100, 255))\n        if params.engine_mode == \"DIGITAL2\":\n             self.draw.text((180, 45), \"(Filt OFF)\", font=self.font_s, fill=(100, 255, 100))\n        elif params.engine_mode == \"DIGITAL3\":\n             self.draw.text((180, 45), \"(Fixed Vel)\", font=self.font_s, fill=(255, 100, 255))\n        if last_drum_name:\n            self.draw.text((10, 135), f\"Drum: {last_drum_name}\", font=self.font_m, fill=(255, 100, 100))\n        vol_pct = int(params.master_gain * 100)\n        self.draw.text((10, 160), f\"VOL: {vol_pct}%\", font=self.font_m, fill=(0, 255, 0))\n        bar_w = 180\n        self.draw.rectangle((10, 185, 10+bar_w, 200), outline=(0, 255, 0), fill=None)\n        self.draw.rectangle((10, 185, 10+int(bar_w*params.master_gain), 200), fill=(0, 255, 0))\n\n    def _draw_page_lfo(self):\n        self.draw.text((10, 40), f\"RATE:  {params.lfo_rate:.1f} Hz\", font=self.font_l, fill=(255, 255, 0))\n        self.draw.text((10, 70), f\"DEPTH: {int(params.lfo_depth*100)} %\", font=self.font_l, fill=(0, 255, 255))\n        self.draw.text((10, 100), \"Knob K1: Rate\", font=self.font_s, fill=(200, 200, 200))\n        self.draw.text((10, 120), \"Knob K2: Depth\", font=self.font_s, fill=(200, 200, 200))\n        cx, cy = 160, 180\n        w, h = 280, 60\n        t = time.time()\n        phase = t * params.lfo_rate * 2.0 * math.pi\n        points = []\n        for i in range(w):\n            x = i\n            amp = (params.lfo_depth * 25.0) + 2.0 \n            y = math.sin(phase + (i * 0.1)) * amp\n            points.append((20 + x, cy + y))\n        if len(points) &gt; 1:\n            self.draw.line(points, fill=(255, 0, 255), width=2)\n            self.draw.rectangle((20, cy-30, 20+w, cy+30), outline=(50, 50, 50))\n\n    def _draw_page_adsr(self):\n        self.draw.text((10, 40), f\"A:{params.vcf_attack:.2f}  D:{params.vcf_decay:.2f}\", font=self.font_m, fill=(255, 255, 255))\n        self.draw.text((10, 65), f\"S:{params.vcf_sustain:.2f}  R:{params.vcf_release:.2f}\", font=self.font_m, fill=(255, 255, 255))\n        self.draw.text((10, 90), f\"CUT: {int(params.cutoff_base)}\", font=self.font_m, fill=(255, 255, 0))\n        self.draw.text((160, 90), f\"RES: {params.resonance:.1f}\", font=self.font_m, fill=(255, 0, 255))\n        self.draw.text((10, 115), \"K1:A K2:D K3:S K4:R\", font=self.font_s, fill=(150, 150, 150))\n        self.draw.text((10, 130), \"K5:Cut K7:Res\", font=self.font_s, fill=(150, 150, 150))\n        gx, gy, gw, gh = 20, 220, 280, 80\n        total_t = params.vcf_attack + params.vcf_decay + params.vcf_release + 0.5\n        scale_x = gw \/ max(total_t, 1.0)\n        x0, y0 = gx, gy\n        xa = x0 + (params.vcf_attack * scale_x)\n        ya = gy - gh\n        xd = xa + (params.vcf_decay * scale_x)\n        ys = gy - (params.vcf_sustain * gh)\n        hold_w = 0.5 * scale_x\n        xr_start = xd + hold_w\n        xr_end = xr_start + (params.vcf_release * scale_x)\n        pts = [(x0, y0), (xa, ya), (xd, ys), (xr_start, ys), (xr_end, y0)]\n        self.draw.line(pts, fill=(0, 255, 0), width=3)\n        self.draw.line((gx, gy, gx+gw, gy), fill=(100, 100, 100))\n\n    def _draw_page_scope(self):\n        step = 4\n        scale_y = 60.0 \n        mid_y = 80     \n        points = []\n        data = last_audio_data[::2] \n        for x in range(0, self.width, step):\n            idx = int((x \/ self.width) * len(data))\n            if idx &lt; len(data):\n                val = data[idx] \/ 32768.0\n                y = mid_y - (val * scale_y)\n                points.append((x, y))\n        if len(points) &gt; 1:\n            self.draw.line(points, fill=(0, 255, 0), width=2)\n        \n        self.draw.text((5, 140), \"Waveform (Up) \/ Spectrum (Down)\", font=self.font_s, fill=(150, 150, 150))\n\n        # FFT Spectrum\n        try:\n            fft_data = np.abs(np.fft.rfft(data)) \n            fft_len = len(fft_data)\n            display_len = min(fft_len, 64) \n            bar_width = self.width \/ display_len\n            bottom_y = 230\n            max_height = 80.0\n            \n            for i in range(display_len):\n                mag = fft_data[i]\n                if i == 0: mag = 0 \n                h = min(max_height, (mag \/ 400000.0) * max_height)\n                \n                x1 = i * bar_width\n                y1 = bottom_y - h\n                x2 = x1 + bar_width - 1\n                y2 = bottom_y\n                r = int(255 * (i \/ display_len))\n                b = 255 - r\n                self.draw.rectangle((x1, y1, x2, y2), fill=(r, 100, b))\n        except: pass\n\n    def _draw_page_log(self):\n        y = 40\n        recent = midi_logs[-8:]\n        for line in recent:\n            self.draw.text((5, y), line, font=self.font_s, fill=(0, 255, 255))\n            y += 20\n\n    def _draw_page_player(self):\n        self.draw.text((10, 40), \"MIDI File Player\", font=self.font_l, fill=(0, 255, 255))\n        status_txt = \"PLAYING &gt;&gt;\" if midi_player.is_playing else \"STOPPED ||\"\n        status_col = (0, 255, 0) if midi_player.is_playing else (255, 100, 100)\n        self.draw.text((10, 70), status_txt, font=self.font_l, fill=status_col)\n        self.draw.text((10, 110), \"K1: Select File\", font=self.font_s, fill=(200, 200, 200))\n        self.draw.text((10, 130), \"K2: Play(Right)\/Stop(Left)\", font=self.font_s, fill=(200, 200, 200))\n        start_y = 160\n        files = midi_player.files\n        idx = midi_player.selected_idx\n        if idx &lt; 1: window_start = 0\n        elif idx &gt;= len(files) - 1: window_start = len(files) - 3\n        else: window_start = idx - 1\n        window_start = max(0, window_start)\n        for i in range(window_start, min(len(files), window_start + 3)):\n            fname = files[i]\n            y = start_y + (i - window_start) * 25\n            prefix = \"&gt; \" if i == idx else \"  \"\n            col = (255, 255, 0) if i == idx else (200, 200, 200)\n            if len(fname) &gt; 20: fname = fname[:18] + \"..\"\n            self.draw.text((10, y), prefix + fname, font=self.font_m, fill=col)\n\n    def _draw_page_system(self):\n        now = time.time()\n        if now - self.last_sys_update &gt; 2.0:\n            self.cache_ip = get_ip_address()\n            self.cache_disk = get_disk_usage()\n            self.cache_ram = get_ram_usage()\n            self.last_sys_update = now\n        self.draw.text((10, 40), \"IP Address:\", font=self.font_m, fill=(200, 200, 200))\n        self.draw.text((10, 60), self.cache_ip, font=self.font_l, fill=(0, 255, 255))\n        temp = get_cpu_temp()\n        t_col = (0, 255, 0)\n        if temp &gt; 60: t_col = (255, 255, 0)\n        if temp &gt; 70: t_col = (255, 0, 0)\n        self.draw.text((10, 90), f\"CPU Temp: {temp:.1f} C\", font=self.font_m, fill=t_col)\n        self.draw.text((10, 120), \"RAM Usage:\", font=self.font_s, fill=(200, 200, 200))\n        self.draw.text((10, 135), self.cache_ram, font=self.font_m, fill=(255, 255, 255))\n        self.draw.text((10, 160), \"Disk Usage:\", font=self.font_s, fill=(200, 200, 200))\n        self.draw.text((10, 175), self.cache_disk, font=self.font_m, fill=(255, 255, 255))\n\n    def _draw_page_effect(self):\n        self.draw.text((10, 40), \"Global Effects\", font=self.font_l, fill=(255, 0, 255))\n        self.draw.text((10, 80), f\"Reverb Send: {params.reverb_send}\", font=self.font_m, fill=(255, 255, 0))\n        bar_w = 200\n        self.draw.rectangle((10, 105, 10+bar_w, 115), outline=(100, 100, 100))\n        self.draw.rectangle((10, 105, 10+int(bar_w*(params.reverb_send\/127.0)), 115), fill=(255, 255, 0))\n        self.draw.text((10, 140), f\"Chorus Send: {params.chorus_send}\", font=self.font_m, fill=(0, 255, 255))\n        self.draw.rectangle((10, 165, 10+bar_w, 175), outline=(100, 100, 100))\n        self.draw.rectangle((10, 165, 10+int(bar_w*(params.chorus_send\/127.0)), 175), fill=(0, 255, 255))\n        self.draw.text((10, 200), \"K1: Reverb  K2: Chorus\", font=self.font_s, fill=(200, 200, 200))\n\n    def _draw_page_chord(self):\n        self.draw.text((10, 40), \"Chord &amp; Arpeggiator\", font=self.font_l, fill=(255, 100, 100))\n        \n        # 1. Type\n        idx = params.chord_type_idx\n        c_name = CHORD_TYPES[idx][\"name\"]\n        col = (100, 100, 100) if idx == 0 else (255, 255, 0)\n        self.draw.text((10, 70), f\"TYPE: {c_name}\", font=self.font_m, fill=col)\n        \n        # 2. BPM\n        self.draw.text((10, 100), f\"BPM:  {int(params.arp_bpm)}\", font=self.font_m, fill=(0, 255, 255))\n        \n        # 3. Pattern\n        p_idx = params.arp_pattern_idx\n        p_name = ARP_PATTERNS[p_idx]\n        col_p = (100, 100, 100) if p_idx == 0 else (255, 0, 255)\n        self.draw.text((10, 130), f\"ARP:  {p_name}\", font=self.font_m, fill=col_p)\n\n        self.draw.text((10, 180), \"K1\/Pad:Type  K2:BPM\", font=self.font_s, fill=(150, 150, 150))\n        self.draw.text((10, 200), \"K3:Arp Pattern\", font=self.font_s, fill=(150, 150, 150))\n\n\ndisp_mgr = None\n\n# --- MIDI Handling ---\ndef handle_cc(n, v):\n    norm = v \/ 127.0\n    # K6: Page Select (9 Pages now)\n    if n == 6:\n        pg = int(norm * 9) \n        if pg &gt; 8: pg = 8\n        if params.page_index != pg: params.page_index = pg\n        return\n    if n == 8: params.master_gain = norm; return\n\n    if params.page_index == 0:\n        if n == 7:\n            idx = min(6, int(norm * 7))\n            if idx == 4: params.engine_mode = \"DIGITAL1\"\n            elif idx == 5: params.engine_mode = \"DIGITAL2\"\n            elif idx == 6: params.engine_mode = \"DIGITAL3\"\n            else:\n                params.engine_mode = \"ANALOG\"\n                params.waveform = [\"sawtooth\", \"square\", \"sine\", \"triangle\"][idx]\n            return\n        if n == 3 and \"DIGITAL\" in params.engine_mode:\n            prog = v\n            if params.sf_program != prog:\n                params.sf_program = prog\n                fluid_engine.change_program(0, prog)\n            return\n\n    elif params.page_index == 1:\n        if n == 1: params.lfo_rate = 0.1 + norm * 19.9\n        elif n == 2: params.lfo_depth = norm\n        return\n\n    elif params.page_index == 2:\n        if n == 1: params.vcf_attack = 0.01 + norm * 2\n        elif n == 2: params.vcf_decay = 0.01 + norm * 2\n        elif n == 3: params.vcf_sustain = norm\n        elif n == 4: params.vcf_release = 0.01 + norm * 3 \n        elif n == 5: params.cutoff_base = 50.0 + norm * 5000.0\n        elif n == 7: params.resonance = norm * 0.95\n        return\n        \n    elif params.page_index == 5:\n        if n == 1: midi_player.select_file(norm)\n        elif n == 2:\n            if v &gt; 64: midi_player.play()\n            else: midi_player.stop()\n\n    elif params.page_index == 7: # EFFECT\n        if n == 1: \n            params.reverb_send = v\n            fluid_engine.send_cc(0, 91, v)\n        elif n == 2: \n            params.chorus_send = v\n            fluid_engine.send_cc(0, 93, v)\n        elif n == 3: \n            params.master_gain = norm\n            \n    elif params.page_index == 8: # CHORD &amp; ARP\n        if n == 1:\n            idx = int(norm * len(CHORD_TYPES))\n            if idx &gt;= len(CHORD_TYPES): idx = len(CHORD_TYPES) - 1\n            params.chord_type_idx = idx\n        elif n == 2: # BPM\n            params.arp_bpm = 60.0 + (norm * 240.0) # 60-300\n        elif n == 3: # Pattern\n            idx = int(norm * len(ARP_PATTERNS))\n            if idx &gt;= len(ARP_PATTERNS): idx = len(ARP_PATTERNS) - 1\n            params.arp_pattern_idx = idx\n\ndef get_note_name(note_num):\n    notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']\n    octave = note_num \/\/ 12 - 1\n    name = notes[note_num % 12]\n    return f\"{name}{octave}\"\n\ndef add_log(msg):\n    midi_logs.append(msg)\n    if len(midi_logs) &gt; 20: midi_logs.pop(0)\n\ndef process_midi_event(status, data1, data2):\n    global active_note, last_drum_name\n    st = status &amp; 0xF0\n    ch = status &amp; 0x0F\n    log_str = \"\"\n\n    # Note On\n    if st == 0x90 and data2 &gt; 0: \n        n, v = data1, data2\n        \n        # Pad Chord Select (Only in Chord Page)\n        if params.page_index == 8:\n            if 36 &lt;= n &lt;= 43:\n                idx = n - 36\n                params.chord_type_idx = idx\n                log_str = f\"Chord Type: {CHORD_TYPES[idx]['name']}\"\n                add_log(log_str)\n                return \n            elif 44 &lt;= n &lt;= 51:\n                idx = n - 44\n                params.chord_type_idx = idx\n                log_str = f\"Chord Type: {CHORD_TYPES[idx]['name']}\"\n                add_log(log_str)\n                return \n        \n        # Drum Check\n        is_drum = (ch == 9) or (36 &lt;= n &lt;= 51 and ch == 0)\n        if is_drum:\n            target_ch = 9\n            v_drum = max(v, 64)\n            fluid_engine.note_on(target_ch, n, v_drum)\n            d_name = DRUM_MAP.get(n, \"Drum\")\n            last_drum_name = d_name\n            log_str = f\"Ch{ch} Pad: {d_name}\"\n            add_log(log_str)\n            return\n\n        # Normal Note\n        if ch == 0 and params.engine_mode == \"DIGITAL3\": v = 102\n        else: v = min(127, v + 40)\n        \n        log_str = f\"Ch{ch} NoteOn: {n}\"\n        \n        # Chord Mode Processing (Only for Ch0)\n        if ch == 0:\n            # 1. Generate Notes\n            notes_to_play = []\n            if params.chord_type_idx &gt; 0:\n                offsets = CHORD_TYPES[params.chord_type_idx][\"offsets\"]\n                for off in offsets:\n                    chord_n = n + off\n                    if 0 &lt;= chord_n &lt;= 127: notes_to_play.append((chord_n, v))\n            else:\n                notes_to_play.append((n, v))\n            \n            # 2. Check ARP status\n            is_arp = (params.arp_pattern_idx &gt; 0)\n            \n            if is_arp:\n                arp.set_notes(notes_to_play)\n                # For Analog Scope visual only\n                if params.engine_mode == \"ANALOG\": active_note=n; params.gate_status=True\n                else: params.gate_status=True\n            else:\n                # Normal Chord Play\n                for (cn, cv) in notes_to_play:\n                    fluid_engine.note_on(ch, cn, cv)\n                \n                if params.engine_mode == \"ANALOG\":\n                    vco.set_f(440.0*(2**((n-69)\/12.0))); amp.trigger(); vcf.trigger(); active_note=n; params.gate_status=True\n                else:\n                    vcf.trigger(); params.gate_status=True\n\n    # Note Off\n    elif (st == 0x80) or (st == 0x90 and data2 == 0): \n        n = data1\n        if params.page_index == 8 and (36 &lt;= n &lt;= 51): return\n\n        is_drum = (ch == 9) or (36 &lt;= n &lt;= 51 and ch == 0)\n        if is_drum:\n            fluid_engine.note_off(9, n)\n            return\n        log_str = f\"Ch{ch} NoteOff: {n}\"\n        \n        if ch == 0:\n            is_arp = (params.arp_pattern_idx &gt; 0)\n            if is_arp:\n                # Stop Arp\n                arp.set_notes([])\n                if params.engine_mode == \"ANALOG\":\n                     if n == active_note: amp.release(); vcf.release(); active_note=None; params.gate_status=False\n                else:\n                    vcf.release(); params.gate_status=False\n            else:\n                # Normal Note Off\n                if params.chord_type_idx &gt; 0:\n                    offsets = CHORD_TYPES[params.chord_type_idx][\"offsets\"]\n                    for off in offsets:\n                        chord_n = n + off\n                        if 0 &lt;= chord_n &lt;= 127: fluid_engine.note_off(ch, chord_n)\n                else:\n                    fluid_engine.note_off(ch, n)\n                \n                if params.engine_mode == \"ANALOG\":\n                     if n == active_note: amp.release(); vcf.release(); active_note=None; params.gate_status=False\n                else:\n                    vcf.release(); params.gate_status=False\n\n    elif st == 0xB0:\n        if ch == 0:\n            handle_cc(data1, data2)\n            log_str = f\"CC #{data1} Val:{data2}\"\n        else:\n            fluid_engine.send_cc(ch, data1, data2)\n\n    elif st == 0xC0:\n        if \"DIGITAL\" in params.engine_mode or ch != 0:\n            params.sf_program = data1\n            fluid_engine.change_program(ch, data1)\n            log_str = f\"Ch{ch} PC: {data1}\"\n    \n    if log_str and ch == 0: add_log(log_str)\n\ndef midi_loop(midi_in):\n    while is_running:\n        msg = midi_in.get_message()\n        if msg:\n            m, _ = msg\n            if len(m) &gt;= 2: process_midi_event(m[0], m[1], m[2] if len(m)&gt;2 else 0)\n        time.sleep(0.001)\n\n# --- Main ---\nif __name__ == \"__main__\":\n    print(\"Initializing Synth (v60: Arpeggiator)...\")\n    \n    disp_mgr = DisplayManager()\n    \n    print(\"Searching for audio device with keyword: 'USB'...\")\n    devs = sd.query_devices()\n    usb_idx = None\n    for i, d in enumerate(devs):\n        if TARGET_DEVICE_KEYWORD in d['name'] and d['max_output_channels'] &gt; 0:\n            usb_idx = i\n            print(f\"Found Target Device: {d['name']} (Index: {i})\")\n            break\n            \n    if usb_idx is None:\n        print(\"Warning: USB Device not found. Using system default.\")\n    \n    fluid_engine.init_synth()\n    \n    mi=rtmidi.MidiIn()\n    ports = mi.get_ports(); found_idx = -1\n    for i, p_name in enumerate(ports):\n        if \"Midi Through\" not in p_name: found_idx = i; break\n    if found_idx != -1: mi.open_port(found_idx); print(f\"MIDI Connected: {ports[found_idx]}\")\n    else: mi.open_virtual_port(\"PySynth\"); print(\"MIDI: Virtual Port\")\n    threading.Thread(target=midi_loop,args=(mi,),daemon=True).start()\n\n    print(\"Starting Audio Stream...\")\n    \n    stream = sd.OutputStream(\n        device=usb_idx,\n        channels=CHANNELS,\n        samplerate=SAMPLE_RATE,\n        dtype=DTYPE,\n        blocksize=BLOCK_SIZE,\n        latency='high',\n        callback=sd_callback\n    )\n    stream.start()\n\n    print(\"System Running.\")\n    \n    try:\n        while True:\n            start = time.time()\n            disp_mgr.update()\n            elapsed = time.time() - start\n            if elapsed &lt; 0.05:\n                time.sleep(0.05 - elapsed)\n                \n    except KeyboardInterrupt:\n        print(\"\\nStopping...\")\n        is_running = False\n        midi_player.stop()\n        stream.stop()\n        mi.close_port()\n        print(\"Bye!\")\n\n<\/code><\/pre><\/div>\n\n<\/div>\n\t\t<\/div>\n<\/div>\n\n\n<figure class=\"wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-4 is-layout-flex wp-block-gallery-is-layout-flex\">\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"942\" height=\"776\" data-id=\"2294\" src=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-140223.png\" alt=\"\" class=\"wp-image-2294\" srcset=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-140223.png 942w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-140223-300x247.png 300w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-140223-768x633.png 768w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-140223-624x514.png 624w\" sizes=\"auto, (max-width: 942px) 100vw, 942px\" \/><\/figure>\n<\/figure>\n\n\n<div class=\"wp-block-ub-content-toggle wp-block-ub-content-toggle-block\" id=\"ub-content-toggle-block-f0187ce7-e7e6-4b4a-a503-0525f961ad1c\" data-mobilecollapse=\"false\" data-desktopcollapse=\"true\" data-preventcollapse=\"false\" data-showonlyone=\"false\">\n<div class=\"wp-block-ub-content-toggle-accordion\" style=\"border-color: #f1f1f1;\" id=\"ub-content-toggle-panel-block-\">\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-title-wrap\" style=\"background-color: #f1f1f1;\" aria-controls=\"ub-content-toggle-panel-0-f0187ce7-e7e6-4b4a-a503-0525f961ad1c\" tabindex=\"0\">\n\t\t\t<p class=\"wp-block-ub-content-toggle-accordion-title ub-content-toggle-title-f0187ce7-e7e6-4b4a-a503-0525f961ad1c\" style=\"color: #000000; \">\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u7248\u3000\u30dd\u30ea\u30d5\u30a9\u30cb\u30c3\u30af\u30a2\u30ca\u30ed\u30b0\u30b7\u30f3\u30bb\u30b5\u30a4\u30b6\u30fc(Python)<\/p>\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-toggle-wrap right\" style=\"color: #000000;\"><span class=\"wp-block-ub-content-toggle-accordion-state-indicator wp-block-ub-chevron-down\"><\/span><\/div>\n\t\t<\/div>\n\t\t\t<div role=\"region\" aria-expanded=\"false\" class=\"wp-block-ub-content-toggle-accordion-content-wrap ub-hide\" id=\"ub-content-toggle-panel-0-f0187ce7-e7e6-4b4a-a503-0525f961ad1c\">\n\n<p><\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code># code_name: raspi4_polysynth_v9_effects\n# Version: 9.0.0\n# Description: Multicore + MIDI Player + FX (Chorus\/Delay)\n# Hardware: Raspberry Pi 4, USB DAC, AKAI MPK Mini (CC 1-8)\n\nimport tkinter as tk\nfrom tkinter import ttk\nfrom tkinter import filedialog\nimport numpy as np\nimport sounddevice as sd\nimport mido\nimport threading\nimport time\nimport sys\nimport multiprocessing\nimport queue\nimport os\n\n# --- Visualization Imports ---\nimport matplotlib\nmatplotlib.use(\"TkAgg\")\nfrom matplotlib.backends.backend_tkagg import FigureCanvasTkAgg\nfrom matplotlib.figure import Figure\n\n# --- System Config ---\nSAMPLE_RATE = 48000\nBLOCK_SIZE = 1024\nPOLYPHONY = 6\nCHANNELS = 2\nVIZ_FPS = 30\n\n# --- Shared Default Parameters ---\nDEFAULT_PARAMS = {\n    'waveform': 'square',\n    'pulse_width': 0.5,\n    'cutoff': 1.0, \n    'resonance': 0.0,\n    'amp_attack': 0.05,\n    'amp_release': 0.5,\n    'lfo_rate': 5.0,\n    'lfo_amount': 0.0,\n    'volume': 0.8,\n    'velocity_sense': 1.0,\n    'chorus_mix': 0.0,  # 0.0 - 1.0\n    'delay_mix': 0.0    # 0.0 - 1.0\n}\n\n# --- MIDI CC Mapping (FX\u7528\u306b\u5909\u66f4) ---\nCC_MAP = {\n    1: 'cutoff', 2: 'resonance', 3: 'amp_attack', 4: 'amp_release',\n    5: 'pulse_width', \n    6: 'chorus_mix', # Old: lfo_rate -&gt; New: Chorus\n    7: 'delay_mix',  # Old: lfo_amount -&gt; New: Delay\n    8: 'volume'\n}\n\n# ==========================================\n#  DSP Logic: Synthesis\n# ==========================================\nclass ADSR:\n    IDLE, ATTACK, RELEASE = 0, 1, 4\n    def __init__(self):\n        self.state = self.IDLE\n        self.level = 0.0\n    def trigger(self): self.state = self.ATTACK\n    def release(self): self.state = self.RELEASE\n    def process_block(self, size, attack_t, release_t):\n        env = np.zeros(size)\n        atk_s = max(1, int(attack_t * SAMPLE_RATE))\n        rel_s = max(1, int(release_t * SAMPLE_RATE))\n        step_atk = 1.0 \/ atk_s\n        step_rel = 1.0 \/ rel_s\n        for i in range(size):\n            if self.state == self.ATTACK:\n                self.level += step_atk\n                if self.level &gt;= 1.0: self.level = 1.0\n            elif self.state == self.RELEASE:\n                self.level -= step_rel\n                if self.level &lt;= 0.0:\n                    self.level = 0.0\n                    self.state = self.IDLE\n            env[i] = self.level\n        return env\n\nclass SynthVoice:\n    def __init__(self):\n        self.active = False\n        self.note = 0\n        self.phase = 0.0\n        self.freq = 440.0\n        self.velocity = 0\n        self.amp_env = ADSR()\n\n    def trigger(self, note, velocity):\n        self.active = True\n        self.note = note\n        self.velocity = velocity \/ 127.0\n        self.freq = 440.0 * (2.0 ** ((note - 69) \/ 12.0))\n        self.phase = 0.0\n        self.amp_env.trigger()\n\n    def release(self):\n        self.amp_env.release()\n\n    def get_audio_block(self, size, p, lfo_val):\n        if not self.active: return np.zeros(size)\n        t = np.arange(size)\n        phase_inc = self.freq \/ SAMPLE_RATE\n        phases = self.phase + t * phase_inc\n        phases_wrapped = phases % 1.0\n        self.phase = (self.phase + size * phase_inc) % 1.0\n        \n        w_type = p['waveform']\n        if w_type == 'square':\n            pw = p['pulse_width']\n            mod_pw = np.clip(pw + (lfo_val * p['lfo_amount'] * 0.2), 0.05, 0.95)\n            osc = np.where(phases_wrapped &lt; mod_pw, 1.0, -1.0)\n        elif w_type == 'saw':\n            osc = 2.0 * phases_wrapped - 1.0\n        elif w_type == 'tri':\n            osc = 2.0 * np.abs(2.0 * phases_wrapped - 1.0) - 1.0\n        elif w_type == 'sine':\n            osc = np.sin(2.0 * np.pi * phases_wrapped)\n        else:\n            osc = 2.0 * phases_wrapped - 1.0\n\n        cutoff_mod = np.clip(p['cutoff'] + (lfo_val * p['lfo_amount'] * 0.5), 0.1, 1.0)\n        tone_gain = 0.6 + (0.4 * cutoff_mod) # Gain Boosted\n        \n        if p['resonance'] &gt; 0.1:\n            osc += (osc * osc * osc * p['resonance'] * 2.0)\n            osc = np.clip(osc, -1.5, 1.5)\n\n        amp = self.amp_env.process_block(size, p['amp_attack'], p['amp_release'])\n        if self.amp_env.state == ADSR.IDLE: self.active = False\n        return osc * amp * self.velocity * tone_gain\n\nclass PolySynth:\n    def __init__(self):\n        self.voices = [SynthVoice() for _ in range(POLYPHONY)]\n        self.lfo_phase = 0.0\n    def note_on(self, note, vel):\n        for v in self.voices:\n            if not v.active:\n                v.trigger(note, vel); return\n    def note_off(self, note):\n        for v in self.voices:\n            if v.active and v.note == note: v.release()\n    def get_block(self, frames, current_params):\n        lfo_inc = current_params['lfo_rate'] \/ SAMPLE_RATE\n        lfo_phases = self.lfo_phase + np.arange(frames) * lfo_inc\n        lfo_out = np.sin(2.0 * np.pi * lfo_phases)\n        self.lfo_phase = (self.lfo_phase + frames * lfo_inc) % 1.0\n        \n        mix = np.zeros(frames)\n        vc = 0\n        for v in self.voices:\n            if v.active:\n                mix += v.get_audio_block(frames, current_params, lfo_out)\n                vc += 1\n        \n        # Note: Master Volume and Limiter moved to EffectProcessor for consistency\n        return mix\n\n# ==========================================\n#  DSP Logic: Effects (New)\n# ==========================================\nclass EffectProcessor:\n    def __init__(self):\n        # Chorus State\n        self.chorus_buffer_size = 2048\n        self.chorus_buffer = np.zeros(self.chorus_buffer_size)\n        self.chorus_ptr = 0\n        self.chorus_lfo_phase = 0.0\n        \n        # Delay State\n        self.delay_len = int(0.4 * SAMPLE_RATE) # 400ms delay\n        self.delay_buffer = np.zeros(self.delay_len)\n        self.delay_ptr = 0\n\n    def process(self, input_signal, params):\n        # 1. Chorus Effect (Simple Modulated Delay)\n        c_mix = params['chorus_mix']\n        output = input_signal.copy()\n        \n        if c_mix &gt; 0.01:\n            # Write to ring buffer\n            n = len(input_signal)\n            indices = np.arange(n) + self.chorus_ptr\n            np.put(self.chorus_buffer, indices % self.chorus_buffer_size, input_signal)\n            \n            # LFO for Delay Time modulation\n            lfo_rate = 1.5 # Hz (Fixed for Chorus)\n            lfo_depth_samples = 150 # ~3ms\n            base_delay = 300 # ~6ms\n            \n            lfo_inc = lfo_rate \/ SAMPLE_RATE\n            phases = self.chorus_lfo_phase + np.arange(n) * lfo_inc\n            mod = np.sin(2.0 * np.pi * phases) * lfo_depth_samples\n            self.chorus_lfo_phase = (self.chorus_lfo_phase + n * lfo_inc) % 1.0\n            \n            # Calculate Read Pointers (Integer indexing for speed on Pi)\n            read_pos = (indices - base_delay - mod).astype(int)\n            delayed_sig = np.take(self.chorus_buffer, read_pos % self.chorus_buffer_size)\n            \n            # Mix: Dry + Wet\n            output = output * (1.0 - 0.5 * c_mix) + delayed_sig * c_mix\n            \n            # Update pointer\n            self.chorus_ptr = (self.chorus_ptr + n) % self.chorus_buffer_size\n\n        # 2. Delay Effect (Simple Feedback Echo)\n        d_mix = params['delay_mix']\n        if d_mix &gt; 0.01:\n            n = len(output)\n            # Read from delay buffer (Feedback loop)\n            # Current time reading\n            read_indices = (np.arange(n) + self.delay_ptr) % self.delay_len\n            delay_sig = np.take(self.delay_buffer, read_indices)\n            \n            # Mix Wet signal into Output\n            output = output + delay_sig * d_mix\n            \n            # Write Output back to delay buffer (with Feedback decay)\n            feedback = 0.5\n            to_write = input_signal + (delay_sig * feedback) # Simple feed-forward + feedback\n            np.put(self.delay_buffer, read_indices, to_write)\n            \n            self.delay_ptr = (self.delay_ptr + n) % self.delay_len\n\n        # 3. Master Volume &amp; Limiter\n        output *= params['volume']\n        np.clip(output, -0.95, 0.95, out=output)\n        \n        # Stereo Expansion (Simple duplicate for now)\n        return np.column_stack((output, output)).astype(np.float32)\n\n# ==========================================\n#  AUDIO PROCESS\n# ==========================================\ndef audio_engine_process(control_queue, viz_queue, feedback_queue, device_id):\n    print(f\"[Audio Process] PID: {multiprocessing.current_process().pid} Started.\")\n    \n    synth = PolySynth()\n    fx = EffectProcessor() # \u2605 FX Engine\n    current_params = DEFAULT_PARAMS.copy()\n    \n    def apply_note_on(note, velocity):\n        vel = velocity\n        if current_params['velocity_sense'] &lt; 0.5:\n            vel = 127\n        synth.note_on(note, vel)\n\n    def midi_listener():\n        ports = mido.get_input_names()\n        target = None\n        for p in ports:\n            if any(x in p for x in [\"MPK\", \"Midi Through\", \"AKAI\", \"USB Audio\"]): target = p\n        if not target and ports: target = ports[0]\n        \n        if target:\n            print(f\"[Audio Process] MIDI Connected: {target}\")\n            with mido.open_input(target) as inport:\n                for msg in inport:\n                    if msg.type == 'note_on' and msg.velocity &gt; 0:\n                        apply_note_on(msg.note, msg.velocity)\n                    elif msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity==0):\n                        synth.note_off(msg.note)\n                    elif msg.type == 'control_change':\n                        cc = msg.control\n                        val = msg.value \/ 127.0\n                        if cc in CC_MAP:\n                            key = CC_MAP[cc]\n                            # FX params usually linear 0-1\n                            if key in ['chorus_mix', 'delay_mix', 'volume', 'resonance', 'lfo_amount']:\n                                new_val = val\n                            # Others need scaling\n                            elif key == 'cutoff': new_val = 0.1 + (val * 0.9)\n                            elif key == 'lfo_rate': new_val = 0.1 + val * 20.0\n                            elif key == 'pulse_width': new_val = 0.05 + (val * 0.9)\n                            elif key == 'amp_attack': new_val = 0.01 + val * 2.0\n                            elif key == 'amp_release': new_val = 0.1 + val * 5.0\n                            else: new_val = val\n                            \n                            current_params[key] = new_val\n                            feedback_queue.put(('midi_update', key, new_val))\n\n    t_midi = threading.Thread(target=midi_listener, daemon=True)\n    t_midi.start()\n\n    def callback(outdata, frames, time_info, status):\n        try:\n            while True:\n                msg = control_queue.get_nowait()\n                cmd = msg[0]\n                if cmd == 'set_param': current_params[msg[1]] = msg[2]\n                elif cmd == 'note_on': apply_note_on(msg[1], msg[2])\n                elif cmd == 'note_off': synth.note_off(msg[1])\n                elif cmd == 'stop': raise sd.CallbackAbort\n        except queue.Empty: pass\n\n        # 1. Synthesize (Mono)\n        raw_sig = synth.get_block(frames, current_params)\n        \n        # 2. Apply Effects &amp; Stereo Out\n        final_sig = fx.process(raw_sig, current_params)\n        \n        if len(final_sig) &lt; len(outdata):\n            outdata[:len(final_sig)] = final_sig\n            outdata[len(final_sig):] = 0\n        else:\n            outdata[:] = final_sig\n        \n        try:\n            # Visualize L channel\n            viz_queue.put_nowait(final_sig[:, 0].copy())\n        except queue.Full: pass\n\n    try:\n        with sd.OutputStream(device=device_id, channels=CHANNELS, \n                             callback=callback, samplerate=SAMPLE_RATE, \n                             blocksize=BLOCK_SIZE):\n            while True: time.sleep(1)\n    except KeyboardInterrupt: pass\n    except Exception as e: print(f\"[Audio] Error: {e}\")\n\n# ==========================================\n#  GUI PROCESS\n# ==========================================\nclass SynthGUI:\n    def __init__(self, root, control_queue, viz_queue, feedback_queue):\n        self.root = root\n        self.control_queue = control_queue\n        self.viz_queue = viz_queue\n        self.feedback_queue = feedback_queue\n        self.params = DEFAULT_PARAMS.copy()\n        self.suppress_send = False\n        \n        self.midi_thread = None\n        self.midi_playing = False\n        self.midi_file_path = \"\"\n        \n        self.root.title(\"Raspi4 PolySynth v9.0 - FX Edition\")\n        self.root.geometry(\"950x580\") # \u5c11\u3057\u9ad8\u3055\u3092\u78ba\u4fdd\n        self.root.configure(bg=\"#2b2b2b\")\n        self.root.protocol(\"WM_DELETE_WINDOW\", self.on_close)\n        \n        self.sliders = {}\n        self.view_mode = \"SCOPE\"\n\n        self.setup_ui()\n        self.setup_plots()\n        self.update_plot()\n        self.check_feedback_loop()\n\n    def setup_ui(self):\n        style = ttk.Style()\n        style.theme_use('clam')\n        style.configure(\"TLabelframe\", background=\"#2b2b2b\", foreground=\"white\")\n        style.configure(\"TLabelframe.Label\", background=\"#2b2b2b\", foreground=\"#00ff00\")\n        style.configure(\"TCheckbutton\", background=\"#2b2b2b\", foreground=\"white\")\n        \n        main_container = tk.Frame(self.root, bg=\"#2b2b2b\")\n        main_container.pack(fill=\"both\", expand=True)\n        \n        left_frame = tk.Frame(main_container, bg=\"#2b2b2b\", width=380)\n        left_frame.pack(side=\"left\", fill=\"y\", padx=5, pady=5)\n        self.right_frame = tk.Frame(main_container, bg=\"#101010\")\n        self.right_frame.pack(side=\"right\", fill=\"both\", expand=True, padx=5, pady=5)\n\n        self.create_controls(left_frame)\n        self.create_player_controls(left_frame)\n\n        btn_frame = tk.Frame(left_frame, bg=\"#2b2b2b\")\n        btn_frame.pack(side=\"bottom\", fill=\"x\", pady=5)\n        \n        self.view_btn = tk.Button(btn_frame, text=\"VIEW\", command=self.toggle_view,\n                                  bg=\"#0066cc\", fg=\"white\", font=(\"Arial\", 9, \"bold\"), height=1)\n        self.view_btn.pack(side=\"left\", fill=\"x\", expand=True, padx=2)\n        \n        exit_btn = tk.Button(btn_frame, text=\"EXIT\", command=self.on_close,\n                  bg=\"#cc0000\", fg=\"white\", font=(\"Arial\", 9, \"bold\"), height=1)\n        exit_btn.pack(side=\"right\", fill=\"x\", expand=True, padx=2)\n\n    def create_controls(self, parent):\n        groups = [\n            (\"OSCILLATOR [K5]\", [(\"Pulse Width\", \"pulse_width\", 0.05, 0.95)]),\n            (\"FILTER \/ TONE [K1, K2]\", [(\"Cutoff\", \"cutoff\", 0.1, 1.0), (\"Resonance\", \"resonance\", 0, 1.0)]),\n            (\"MODULATION [Slider]\", [(\"Rate\", \"lfo_rate\", 0.1, 20.0), (\"Amount\", \"lfo_amount\", 0.0, 1.0)]),\n            (\"ENVELOPE [K3, K4]\", [(\"Attack\", \"amp_attack\", 0.01, 2.0), (\"Release\", \"amp_release\", 0.1, 5.0)]),\n            # \u2605 New Effects Section\n            (\"EFFECTS [K6, K7]\", [(\"Chorus Mix\", \"chorus_mix\", 0.0, 1.0), (\"Delay Mix\", \"delay_mix\", 0.0, 1.0)]),\n            (\"MASTER [K8]\", [(\"Volume\", \"volume\", 0.0, 1.0)])\n        ]\n\n        osc_frame = ttk.LabelFrame(parent, text=\"OSCILLATOR [K5]\")\n        osc_frame.pack(fill=\"x\", pady=2)\n        wave_sub = tk.Frame(osc_frame, bg=\"#2b2b2b\")\n        wave_sub.pack(side=\"top\", fill=\"x\", pady=2)\n        self.wave_var = tk.StringVar(value=self.params['waveform'])\n        for label, val in [(\"Sqr\", \"square\"), (\"Saw\", \"saw\"), (\"Tri\", \"tri\"), (\"Sin\", \"sine\")]:\n            ttk.Radiobutton(wave_sub, text=label, value=val, variable=self.wave_var,\n                            command=self.on_wave_change).pack(side=\"left\", padx=5)\n        \n        for g_name, ctrls in groups:\n            if g_name.startswith(\"OSC\"): frame = osc_frame\n            elif g_name.startswith(\"MASTER\"):\n                frame = ttk.LabelFrame(parent, text=g_name)\n                frame.pack(fill=\"x\", pady=2)\n                self.vel_sense_var = tk.IntVar(value=int(self.params['velocity_sense']))\n                cb = ttk.Checkbutton(frame, text=\"Velocity Sense\", variable=self.vel_sense_var,\n                                     command=self.on_vel_sense_change)\n                cb.pack(anchor=\"w\", padx=5, pady=0)\n            else:\n                frame = ttk.LabelFrame(parent, text=g_name)\n                frame.pack(fill=\"x\", pady=2)\n            \n            for label, key, min_v, max_v in ctrls:\n                f = tk.Frame(frame, bg=\"#2b2b2b\")\n                f.pack(fill=\"x\", padx=5, pady=0)\n                tk.Label(f, text=label, width=12, anchor=\"w\", bg=\"#2b2b2b\", fg=\"white\", font=(\"Arial\", 8)).pack(side=\"left\")\n                s = tk.Scale(f, from_=min_v, to=max_v, orient=\"horizontal\", resolution=0.01, \n                             bg=\"#404040\", fg=\"white\", highlightthickness=0, width=10,\n                             command=lambda v, k=key: self.on_slider_change(k, v))\n                s.set(self.params[key])\n                s.pack(side=\"right\", fill=\"x\", expand=True)\n                self.sliders[key] = s\n\n    def create_player_controls(self, parent):\n        player_frame = ttk.LabelFrame(parent, text=\"MIDI PLAYER\")\n        player_frame.pack(fill=\"x\", pady=5)\n        \n        self.lbl_file = tk.Label(player_frame, text=\"No file loaded\", bg=\"#2b2b2b\", fg=\"#aaaaaa\", anchor=\"w\", font=(\"Arial\", 8))\n        self.lbl_file.pack(fill=\"x\", padx=5, pady=0)\n        \n        btn_box = tk.Frame(player_frame, bg=\"#2b2b2b\")\n        btn_box.pack(fill=\"x\", pady=2)\n        \n        tk.Button(btn_box, text=\"LOAD\", command=self.load_midi_file,\n                  bg=\"#444444\", fg=\"white\", width=8, font=(\"Arial\", 8)).pack(side=\"left\", padx=2)\n        tk.Button(btn_box, text=\"PLAY\", command=self.play_midi,\n                  bg=\"#008800\", fg=\"white\", width=6, font=(\"Arial\", 8)).pack(side=\"left\", padx=2)\n        tk.Button(btn_box, text=\"STOP\", command=self.stop_midi,\n                  bg=\"#880000\", fg=\"white\", width=6, font=(\"Arial\", 8)).pack(side=\"left\", padx=2)\n\n    def load_midi_file(self):\n        path = filedialog.askopenfilename(filetypes=[(\"MIDI Files\", \"*.mid\"), (\"All Files\", \"*.*\")])\n        if path:\n            self.midi_file_path = path\n            self.lbl_file.config(text=os.path.basename(path))\n\n    def play_midi(self):\n        if not self.midi_file_path: return\n        if self.midi_playing: return\n        self.midi_playing = True\n        self.midi_thread = threading.Thread(target=self._midi_worker, daemon=True)\n        self.midi_thread.start()\n\n    def stop_midi(self):\n        self.midi_playing = False\n\n    def _midi_worker(self):\n        try:\n            mid = mido.MidiFile(self.midi_file_path)\n            for msg in mid.play():\n                if not self.midi_playing: break\n                if msg.type == 'note_on' and msg.velocity &gt; 0:\n                    self.control_queue.put(('note_on', msg.note, msg.velocity))\n                elif msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0):\n                    self.control_queue.put(('note_off', msg.note))\n        except Exception: pass\n        self.midi_playing = False\n\n    def setup_plots(self):\n        self.fig = Figure(figsize=(5, 3.2), dpi=100, facecolor=\"#101010\")\n        self.ax1 = self.fig.add_subplot(211)\n        self.ax2 = self.fig.add_subplot(212)\n        self.canvas = FigureCanvasTkAgg(self.fig, master=self.right_frame)\n        self.canvas.get_tk_widget().pack(fill=\"both\", expand=True)\n        self.line1, = self.ax1.plot([], [], color=\"#00ff00\", lw=1)\n        self.line2, = self.ax2.plot([], [], color=\"#00ffff\", lw=1)\n        self.x_wave = np.arange(0, BLOCK_SIZE, 4)\n        self.x_freq = np.fft.rfftfreq(BLOCK_SIZE, 1\/SAMPLE_RATE)\n        self.setup_scope_view()\n\n    def setup_scope_view(self):\n        self.ax1.clear(); self.ax2.clear()\n        self.ax1.set_facecolor(\"#000000\"); self.ax2.set_facecolor(\"#000000\")\n        self.ax1.set_title(\"Waveform\", color=\"white\", fontsize=8)\n        self.ax2.set_title(\"Spectrum\", color=\"white\", fontsize=8)\n        self.ax1.tick_params(labelsize=6)\n        self.ax2.tick_params(labelsize=6)\n        self.ax1.set_ylim(-1.1, 1.1); self.ax1.set_xlim(0, BLOCK_SIZE)\n        self.ax2.set_ylim(0, 0.5); self.ax2.set_xlim(0, 5000)\n        self.ax1.grid(True, color=\"#333333\"); self.ax2.grid(True, color=\"#333333\")\n        self.line1, = self.ax1.plot(self.x_wave, np.zeros(len(self.x_wave)), color=\"#00ff00\", lw=1)\n        self.line2, = self.ax2.plot(self.x_freq, np.zeros(len(self.x_freq)), color=\"#00ffff\", lw=1)\n        self.fig.tight_layout()\n        self.canvas.draw()\n\n    def setup_param_view(self):\n        self.ax1.clear(); self.ax2.clear()\n        self.ax1.set_facecolor(\"#000000\"); self.ax2.set_facecolor(\"#000000\")\n        self.ax1.set_title(\"ADSR Setting\", color=\"white\", fontsize=8)\n        self.ax2.set_title(\"VCF Response\", color=\"white\", fontsize=8)\n        self.ax1.tick_params(labelsize=6)\n        self.ax2.tick_params(labelsize=6)\n        self.ax1.set_ylim(0, 1.1); self.ax1.set_xlim(0, 3.0)\n        self.ax2.set_ylim(0, 1.2); self.ax2.set_xlim(20, 10000)\n        self.ax2.set_xscale('log')\n        self.ax1.grid(True, color=\"#333333\"); self.ax2.grid(True, color=\"#333333\")\n        self.line1, = self.ax1.plot([], [], color=\"#ffcc00\", lw=2)\n        self.line2, = self.ax2.plot([], [], color=\"#ff00ff\", lw=2)\n        self.fig.tight_layout()\n        self.canvas.draw()\n\n    def on_slider_change(self, key, value):\n        if self.suppress_send: return\n        val = float(value)\n        self.params[key] = val\n        self.control_queue.put(('set_param', key, val))\n\n    def on_wave_change(self):\n        val = self.wave_var.get()\n        self.params['waveform'] = val\n        self.control_queue.put(('set_param', 'waveform', val))\n\n    def on_vel_sense_change(self):\n        val = float(self.vel_sense_var.get())\n        self.params['velocity_sense'] = val\n        self.control_queue.put(('set_param', 'velocity_sense', val))\n\n    def toggle_view(self):\n        if self.view_mode == \"SCOPE\":\n            self.view_mode = \"PARAM\"\n            self.setup_param_view()\n        else:\n            self.view_mode = \"SCOPE\"\n            self.setup_scope_view()\n\n    def check_feedback_loop(self):\n        try:\n            while not self.feedback_queue.empty():\n                msg = self.feedback_queue.get_nowait()\n                if msg[0] == 'midi_update':\n                    key, val = msg[1], msg[2]\n                    self.suppress_send = True \n                    if key in self.sliders:\n                        self.sliders[key].set(val)\n                    self.params[key] = val\n                    self.suppress_send = False\n        except queue.Empty: pass\n        self.root.after(50, self.check_feedback_loop)\n\n    def update_plot(self):\n        if self.view_mode == \"SCOPE\":\n            try:\n                raw_audio = None\n                while not self.viz_queue.empty():\n                    raw_audio = self.viz_queue.get_nowait()\n                if raw_audio is not None:\n                    ds_audio = raw_audio[::4]\n                    if len(ds_audio) == len(self.x_wave):\n                        self.line1.set_ydata(ds_audio * 3.0)\n                    window = np.hanning(len(raw_audio))\n                    fft_res = np.fft.rfft(raw_audio * window)\n                    fft_mag = np.abs(fft_res) \/ len(raw_audio) * 15.0\n                    if len(fft_mag) == len(self.x_freq):\n                        self.line2.set_ydata(fft_mag)\n                    self.canvas.draw_idle()\n            except queue.Empty: pass\n        else:\n            att, rel = self.params['amp_attack'], self.params['amp_release']\n            t_adsr = np.linspace(0, 3.0, 300)\n            y_adsr = np.zeros_like(t_adsr)\n            hold = 0.5\n            for i, t in enumerate(t_adsr):\n                if t &lt; att: y_adsr[i] = t\/att if att&gt;0 else 1\n                elif t &lt; att+hold: y_adsr[i] = 1\n                elif t &lt; att+hold+rel: y_adsr[i] = 1 - (t-(att+hold))\/rel\n            self.line1.set_data(t_adsr, y_adsr)\n            cutoff = self.params['cutoff']\n            res = self.params['resonance']\n            fc = 50 + cutoff * 8000\n            f_vcf = np.logspace(np.log10(20), np.log10(10000), 200)\n            q = 0.7 + res * 4.0\n            ratio = f_vcf \/ fc\n            y_vcf = np.clip(1.0 \/ np.sqrt((1-ratio**2)**2 + (ratio\/q)**2), 0, 1.2)\n            self.line2.set_data(f_vcf, y_vcf)\n            self.canvas.draw_idle()\n        self.root.after(int(1000\/VIZ_FPS), self.update_plot)\n\n    def on_close(self):\n        self.midi_playing = False\n        self.control_queue.put(('stop', None, None))\n        self.root.quit()\n        self.root.destroy()\n\ndef find_audio_device_safe():\n    devices = sd.query_devices()\n    print(\"\\n--- Audio Devices ---\")\n    target_id = None\n    for i, dev in enumerate(devices):\n        if dev['max_output_channels'] &gt; 0:\n            print(f\"ID {i}: {dev['name']}\")\n            if \"USB\" in dev['name']: target_id = i\n    return target_id if target_id is not None else sd.default.device[1]\n\nif __name__ == \"__main__\":\n    multiprocessing.freeze_support()\n    control_q = multiprocessing.Queue()\n    viz_q = multiprocessing.Queue(maxsize=2)\n    feedback_q = multiprocessing.Queue()\n    dev_id = find_audio_device_safe()\n    print(f\"\\nTarget Audio Device ID: {dev_id}\")\n\n    p_audio = multiprocessing.Process(\n        target=audio_engine_process,\n        args=(control_q, viz_q, feedback_q, dev_id),\n        daemon=True\n    )\n    p_audio.start()\n\n    try:\n        root = tk.Tk()\n        app = SynthGUI(root, control_q, viz_q, feedback_q)\n        root.mainloop()\n    except KeyboardInterrupt:\n        pass\n    finally:\n        print(\"Terminating...\")\n        p_audio.terminate()\n        p_audio.join()\n        sys.exit(0)\n<\/code><\/pre><\/div>\n\n<\/div>\n\t\t<\/div>\n<\/div>\n\n\n<figure class=\"wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-5 is-layout-flex wp-block-gallery-is-layout-flex\">\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"939\" height=\"724\" data-id=\"2292\" src=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-135708.png\" alt=\"\" class=\"wp-image-2292\" srcset=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-135708.png 939w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-135708-300x231.png 300w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-135708-768x592.png 768w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2025\/12\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2025-12-27-135708-624x481.png 624w\" sizes=\"auto, (max-width: 939px) 100vw, 939px\" \/><\/figure>\n<\/figure>\n\n\n<div class=\"wp-block-ub-content-toggle wp-block-ub-content-toggle-block\" id=\"ub-content-toggle-block-0f0c4ae4-121b-4b8b-a525-345cd33fb763\" data-mobilecollapse=\"false\" data-desktopcollapse=\"true\" data-preventcollapse=\"false\" data-showonlyone=\"false\">\n<div class=\"wp-block-ub-content-toggle-accordion\" style=\"border-color: #f1f1f1;\" id=\"ub-content-toggle-panel-block-\">\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-title-wrap\" style=\"background-color: #f1f1f1;\" aria-controls=\"ub-content-toggle-panel-0-0f0c4ae4-121b-4b8b-a525-345cd33fb763\" tabindex=\"0\">\n\t\t\t<p class=\"wp-block-ub-content-toggle-accordion-title ub-content-toggle-title-0f0c4ae4-121b-4b8b-a525-345cd33fb763\" style=\"color: #000000; \">FM\u97f3\u6e90\u7248\uff08\u672a\u5b8c\u6210 Python)<\/p>\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-toggle-wrap right\" style=\"color: #000000;\"><span class=\"wp-block-ub-content-toggle-accordion-state-indicator wp-block-ub-chevron-down\"><\/span><\/div>\n\t\t<\/div>\n\t\t\t<div role=\"region\" aria-expanded=\"false\" class=\"wp-block-ub-content-toggle-accordion-content-wrap ub-hide\" id=\"ub-content-toggle-panel-0-0f0c4ae4-121b-4b8b-a525-345cd33fb763\">\n\n<p><\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code># py_fm_synth_rpi4_v12.1_sync.py\n# Version: 12.1.0\n# Description: 4-OP FM with full GUI-Preset-Operator synchronization.\n\nimport sounddevice as sd\nimport numpy as np\nimport threading\nimport tkinter as tk\nfrom tkinter import ttk\nimport mido\nimport time\nimport sys\nimport multiprocessing\nfrom queue import Empty\n\n# --- \u8a2d\u5b9a ---\nSAMPLE_RATE = 48000\nBLOCK_SIZE = 1024\nCHANNELS = 2\nMAX_VOICES = 3 \nPLOT_INTERVAL_MS = 100\n\n# --- 4-OP \u30d7\u30ea\u30bb\u30c3\u30c8 ---\nPRESETS = {\n    \"4-OP Grand Piano\": {\n        'algo': 1, 'master_gain': 0.55, 'fb': 0.4,\n        'ops': [\n            {'idx': 1.0, 'ratio': 1.0, 'atk': 0.01, 'rel': 1.2}, # Op1\n            {'idx': 2.2, 'ratio': 1.003, 'atk': 0.01, 'rel': 0.5}, # Op2\n            {'idx': 0.8, 'ratio': 7.0, 'atk': 0.01, 'rel': 0.2}, # Op3\n            {'idx': 2.0, 'ratio': 14.0, 'atk': 0.01, 'rel': 0.1} # Op4\n        ]\n    },\n    \"4-OP Trumpet\": {\n        'algo': 1, 'master_gain': 0.4, 'fb': 0.7,\n        'ops': [\n            {'idx': 1.0, 'ratio': 1.0, 'atk': 0.08, 'rel': 0.2},\n            {'idx': 3.5, 'ratio': 1.0, 'atk': 0.1, 'rel': 0.2},\n            {'idx': 1.0, 'ratio': 2.0, 'atk': 0.1, 'rel': 0.2},\n            {'idx': 0.5, 'ratio': 3.0, 'atk': 0.1, 'rel': 0.2}\n        ]\n    },\n    \"4-OP Pipe Organ\": {\n        'algo': 3, 'master_gain': 0.4, 'fb': 0.0,\n        'ops': [\n            {'idx': 1.0, 'ratio': 1.0, 'atk': 0.05, 'rel': 0.2},\n            {'idx': 0.5, 'ratio': 2.0, 'atk': 0.05, 'rel': 0.2},\n            {'idx': 0.3, 'ratio': 4.0, 'atk': 0.05, 'rel': 0.2},\n            {'idx': 0.2, 'ratio': 0.5, 'atk': 0.05, 'rel': 0.2}\n        ]\n    }\n}\n\n# \u30d1\u30e9\u30e1\u30fc\u30bf\u30ec\u30f3\u30b8\u5b9a\u7fa9\nRANGE = {\n    'idx': (0.0, 20.0), 'ratio': (0.5, 15.0), 'atk': (0.01, 2.0), 'rel': (0.01, 2.0),\n    'fb': (0.0, 0.95), 'master_gain': (0.0, 1.0)\n}\n\n# ==========================================\n#  Audio Process (Independent Core)\n# ==========================================\n\nclass FMVoice4Op:\n    def __init__(self, sample_rate, block_size):\n        self.sample_rate = sample_rate\n        self.block_size = block_size\n        self.active, self.gate, self.finished = False, False, True\n        self.note = 0; self.velocity = 0.0\n        self.phases = np.zeros(4); self.env_levels = np.zeros(4)\n        self.prev_fb_out = 0.0; self.carrier_buf = np.zeros(block_size, dtype=np.float32)\n\n    def trigger(self, note, velocity):\n        self.note, self.velocity = note, velocity\n        self.gate, self.active, self.finished = True, True, False\n\n    def add_to_buffer(self, mix_buffer, params):\n        if self.finished: return\n        target_v = (params['fixed_vel_val'] if params['use_fixed_vel'] else self.velocity) if self.gate else 0.0\n        \n        envs = []\n        for i in range(4):\n            p = params['ops'][i]\n            rate = p['atk'] if self.gate else p['rel']\n            step = (target_v - self.env_levels[i]) * (self.block_size \/ (self.sample_rate * rate))\n            e = np.linspace(self.env_levels[i], self.env_levels[i] + step, self.block_size)\n            self.env_levels[i] = e[-1]; envs.append(e)\n\n        if not self.gate and self.env_levels[0] &lt; 0.001:\n            self.active, self.finished = False, True; return\n\n        f0 = 440.0 * (2 ** ((self.note - 69) \/ 12.0))\n        t = np.arange(self.block_size) \/ self.sample_rate\n        algo = params['algo']\n        op_outs = [np.zeros(self.block_size) for _ in range(4)]\n        \n        for i in reversed(range(4)):\n            p = params['ops'][i]; freq = f0 * p['ratio']\n            mod_in = np.zeros(self.block_size)\n            if algo == 1:\n                if i &lt; 3: mod_in = op_outs[i+1] * params['ops'][i+1]['idx'] * envs[i+1]\n            elif algo == 2:\n                if i == 2: mod_in = op_outs[3] * params['ops'][3]['idx'] * envs[3]\n                if i == 0: mod_in = op_outs[1] * params['ops'][1]['idx'] * envs[1]\n            \n            fb_val = (self.prev_fb_out * params['fb'] * 2.0) if i == 3 else 0.0\n            sig = np.sin(2 * np.pi * freq * t + self.phases[i] + mod_in + fb_val)\n            op_outs[i] = sig\n            if i == 3: self.prev_fb_out = sig[-1]\n            self.phases[i] = (self.phases[i] + 2 * np.pi * freq * self.block_size \/ self.sample_rate) % (2 * np.pi)\n\n        final_out = np.zeros(self.block_size)\n        if algo == 1: final_out = op_outs[0] * params['ops'][0]['idx'] * envs[0]\n        elif algo == 2: final_out = (op_outs[0] * envs[0] + op_outs[2] * envs[2]) * 0.5\n        elif algo == 3:\n            for i in range(4): final_out += op_outs[i] * envs[i] * 0.25\n            \n        np.add(mix_buffer, final_out, out=mix_buffer)\n\ndef audio_process_entry(cmd_q, mon_q):\n    from copy import deepcopy\n    # \u521d\u671f\u5316\n    current_params = deepcopy(PRESETS[\"4-OP Grand Piano\"])\n    current_params.update({'use_fixed_vel': False, 'fixed_vel_val': 1.0})\n    voices = [FMVoice4Op(SAMPLE_RATE, BLOCK_SIZE) for _ in range(MAX_VOICES)]\n    mix_buffer = np.zeros(BLOCK_SIZE, dtype=np.float32)\n\n    devices = sd.query_devices()\n    target_id = next((i for i, d in enumerate(devices) if d['max_output_channels'] &gt; 0 and \"USB\" in d['name']), sd.default.device[1])\n            \n    def callback(outdata, frames, time_info, status):\n        try:\n            while True:\n                cmd, key, val = cmd_q.get_nowait()\n                if cmd == 'note_on': \n                    for v in voices:\n                        if not v.active: v.trigger(key, val); break\n                elif cmd == 'note_off': \n                    for v in voices:\n                        if v.note == key: v.gate = False\n                elif cmd == 'param': \n                    if '.' in key: # ops.0.idx\n                        _, op_i, p_k = key.split('.'); current_params['ops'][int(op_i)][p_k] = val\n                    else: current_params[key] = val\n                elif cmd == 'preset':\n                    # \u97f3\u8272\u3092\u6df1\u304f\u30b3\u30d4\u30fc\u3057\u3066\u53cd\u6620\n                    new_p = deepcopy(PRESETS[key])\n                    for k in new_p: current_params[k] = new_p[k]\n        except Empty: pass\n        \n        mix_buffer.fill(0.0); active = False\n        for v in voices:\n            if v.active: v.add_to_buffer(mix_buffer, current_params); active = True\n        np.multiply(mix_buffer, current_params['master_gain'], out=mix_buffer)\n        np.clip(mix_buffer, -1.0, 1.0, out=mix_buffer)\n        outdata[:, 0] = mix_buffer; outdata[:, 1] = mix_buffer\n        if mon_q.empty() and active: mon_q.put_nowait(mix_buffer[::8].copy())\n\n    with sd.OutputStream(device=target_id, channels=CHANNELS, samplerate=SAMPLE_RATE, blocksize=BLOCK_SIZE, latency='high', callback=callback):\n        while True: time.sleep(1)\n\n# ==========================================\n#  Main Process (GUI &amp; MIDI)\n# ==========================================\n\nclass SynthGUI:\n    def __init__(self, root, cmd_q, mon_q):\n        self.root, self.cmd_q, self.mon_q = root, cmd_q, mon_q\n        self.root.title(\"FM Synth v12.1 (Full Sync)\")\n        \n        # \u73fe\u5728\u306e\u30d1\u30e9\u30e1\u30fc\u30bf\u72b6\u614b\u3092GUI\u5074\u3067\u3082\u4fdd\u6301\uff08\u540c\u671f\u7528\uff09\n        from copy import deepcopy\n        self.gui_params = deepcopy(PRESETS[\"4-OP Grand Piano\"])\n\n        # --- Layout ---\n        self.top = ttk.Frame(root, padding=5); self.top.pack(side=tk.TOP, fill=tk.X)\n        self.mid = ttk.Frame(root, padding=5); self.mid.pack(side=tk.TOP, fill=tk.X)\n        self.left = ttk.Frame(root, padding=10); self.left.pack(side=tk.LEFT, fill=tk.Y)\n        self.right = ttk.Frame(root, padding=10); self.right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)\n\n        # Op Selector\n        op_sel_frame = ttk.LabelFrame(self.top, text=\"Edit Operator Select\", padding=5); op_sel_frame.pack(side=tk.LEFT, padx=5)\n        self.op_var = tk.IntVar(value=0)\n        for i in range(4):\n            ttk.Radiobutton(op_sel_frame, text=f\"Op{i+1}\", variable=self.op_var, value=i, command=self.sync_gui_to_current_op).pack(side=tk.LEFT, padx=10)\n\n        # Global\n        glob_frame = ttk.LabelFrame(self.top, text=\"Global Settings\", padding=5); glob_frame.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)\n        self.algo_var = tk.DoubleVar(value=1.0)\n        ttk.Label(glob_frame, text=\"Algo:\").pack(side=tk.LEFT)\n        ttk.Scale(glob_frame, from_=1, to=3, variable=self.algo_var, command=self.on_algo_change).pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)\n\n        # Knobs\n        k_frame = ttk.LabelFrame(self.mid, text=\"K1-K8 Knobs Sync\", padding=5); k_frame.pack(fill=tk.X)\n        self.knob_vars = {}; self.knob_labels = {}\n        for i in range(1, 9):\n            f = ttk.Frame(k_frame); f.pack(side=tk.LEFT, padx=10, expand=True)\n            ttk.Label(f, text=f\"K{i}\", font=('Arial', 8, 'bold')).pack()\n            v = tk.DoubleVar(value=0)\n            s = ttk.Scale(f, from_=1.0, to=0.0, orient=tk.VERTICAL, length=120, variable=v)\n            s.pack(pady=2); self.knob_vars[i] = v\n            lbl = ttk.Label(f, text=\"-\", font=('Arial', 8), foreground=\"blue\"); lbl.pack(); self.knob_labels[i] = lbl\n\n        # Settings\n        s_frame = ttk.LabelFrame(self.left, text=\"Settings\", padding=5); s_frame.pack(fill=tk.X)\n        self.preset_var = tk.StringVar(value=\"4-OP Grand Piano\")\n        self.combo = ttk.Combobox(s_frame, textvariable=self.preset_var, values=list(PRESETS.keys()), state=\"readonly\")\n        self.combo.pack(fill=tk.X, pady=5); self.combo.bind(\"&lt;&lt;ComboboxSelected&gt;&gt;\", self.on_preset_select)\n        \n        self.v_fix = tk.BooleanVar(value=False)\n        ttk.Checkbutton(s_frame, text=\"Velocity Fix\", variable=self.v_fix, command=lambda: self.cmd_q.put(('param', 'use_fixed_vel', self.v_fix.get()))).pack(anchor=tk.W)\n        ttk.Button(self.left, text=\"EXIT\", command=sys.exit).pack(pady=40)\n\n        # Visuals\n        from matplotlib.figure import Figure\n        from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg\n        self.fig = Figure(figsize=(5, 3), dpi=80); self.ax_w = self.fig.add_subplot(211); self.ax_f = self.fig.add_subplot(212)\n        self.fig.subplots_adjust(hspace=0.6); self.line_w, = self.ax_w.plot(np.zeros(BLOCK_SIZE\/\/8))\n        self.ax_w.set_ylim(-1.1, 1.1); self.line_f, = self.ax_f.plot(np.zeros(BLOCK_SIZE\/\/16 + 1))\n        self.ax_f.set_ylim(0, 100); self.canvas = FigureCanvasTkAgg(self.fig, master=self.right); self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)\n        \n        self.sync_gui_to_current_op()\n        self.update_plot()\n\n    def on_algo_change(self, v):\n        val = int(float(v))\n        self.gui_params['algo'] = val\n        self.cmd_q.put(('param', 'algo', val))\n\n    def on_preset_select(self, e):\n        name = self.preset_var.get()\n        from copy import deepcopy\n        self.gui_params = deepcopy(PRESETS[name])\n        self.cmd_q.put(('preset', name, 0))\n        # \u753b\u9762\u306e\u5168\u30b9\u30e9\u30a4\u30c0\u30fc\u3068Algo\u3092\u66f4\u65b0\n        self.sync_gui_to_current_op()\n        self.algo_var.set(float(self.gui_params['algo']))\n\n    def sync_gui_to_current_op(self):\n        \"\"\" \u30aa\u30da\u30ec\u30fc\u30bf\u5207\u308a\u66ff\u3048\u3084\u30d7\u30ea\u30bb\u30c3\u30c8\u5909\u66f4\u6642\u306bGUI\u30b9\u30e9\u30a4\u30c0\u30fc\u3092\u9023\u52d5\u3055\u305b\u308b \"\"\"\n        op_idx = self.op_var.get()\n        p = self.gui_params['ops'][op_idx]\n        \n        # \u30e9\u30d9\u30eb\u66f4\u65b0\n        labels = {1: f\"Op{op_idx+1} Idx\", 2: f\"Op{op_idx+1} Ratio\", 3: f\"Op{op_idx+1} Atk\", 4: f\"Op{op_idx+1} Rel\", 5: \"Feedback\", 6: \"Algo\", 7: \"-\", 8: \"Master Gain\"}\n        for i, text in labels.items(): self.knob_labels[i].config(text=text)\n\n        # \u30b9\u30e9\u30a4\u30c0\u30fc\u5024\u306e\u9006\u63db\u7b97\u8a2d\u5b9a\n        def norm(val, key): return (val - RANGE[key][0]) \/ (RANGE[key][1] - RANGE[key][0])\n        \n        self.knob_vars[1].set(norm(p['idx'], 'idx'))\n        self.knob_vars[2].set(norm(p['ratio'], 'ratio'))\n        self.knob_vars[3].set(norm(p['atk'], 'atk'))\n        self.knob_vars[4].set(norm(p['rel'], 'rel'))\n        self.knob_vars[5].set(norm(self.gui_params['fb'], 'fb'))\n        self.knob_vars[6].set(norm(self.gui_params['algo'], 'master_gain')) # Algo\u306f\u4fbf\u5b9c\u4e0a0-1\u306b\u30de\u30c3\u30d7\n        self.knob_vars[8].set(norm(self.gui_params['master_gain'], 'master_gain'))\n\n    def update_plot(self):\n        try:\n            data = None\n            while not self.mon_q.empty(): data = self.mon_q.get_nowait()\n            if data is not None:\n                self.line_wave.set_ydata(data)\n                fft = 20 * np.log10(np.abs(np.fft.rfft(data * np.hanning(len(data)))) + 1e-6) + 60\n                self.line_fft.set_ydata(fft); self.canvas.draw_idle()\n        except: pass\n        self.root.after(PLOT_INTERVAL_MS, self.update_plot)\n\ndef midi_loop(cmd_q, gui):\n    ports = mido.get_input_names()\n    target = next((p for p in ports if \"AKAI\" in p or \"MIDI\" in p), None)\n    if not target: return\n    with mido.open_input(target) as port:\n        for msg in port:\n            if msg.type == 'note_on' and msg.velocity &gt; 0: cmd_q.put(('note_on', msg.note, msg.velocity\/127.0))\n            elif msg.type in ['note_off', 'note_on']: cmd_q.put(('note_off', msg.note, 0))\n            elif msg.type == 'control_change' and 1 &lt;= msg.control &lt;= 8:\n                cc = msg.control; norm = msg.value \/ 127.0; op = gui.op_var.get()\n                # \u30d1\u30e9\u30e1\u30fc\u30bf\u8a08\u7b97\n                def denorm(n, key): return RANGE[key][0] + n * (RANGE[key][1] - RANGE[key][0])\n                \n                if cc == 1: \n                    val = denorm(norm, 'idx'); gui.gui_params['ops'][op]['idx'] = val\n                    cmd_q.put(('param', f'ops.{op}.idx', val))\n                elif cc == 2: \n                    val = denorm(norm, 'ratio'); gui.gui_params['ops'][op]['ratio'] = val\n                    cmd_q.put(('param', f'ops.{op}.ratio', val))\n                elif cc == 3: \n                    val = denorm(norm, 'atk'); gui.gui_params['ops'][op]['atk'] = val\n                    cmd_q.put(('param', f'ops.{op}.atk', val))\n                elif cc == 4: \n                    val = denorm(norm, 'rel'); gui.gui_params['ops'][op]['rel'] = val\n                    cmd_q.put(('param', f'ops.{op}.rel', val))\n                elif cc == 5: \n                    val = denorm(norm, 'fb'); gui.gui_params['fb'] = val\n                    cmd_q.put(('param', 'fb', val))\n                elif cc == 6: \n                    val = int(1 + norm * 2); gui.gui_params['algo'] = val\n                    cmd_q.put(('param', 'algo', val))\n                    gui.root.after(0, lambda v=val: gui.algo_var.set(float(v)))\n                elif cc == 8: \n                    val = norm; gui.gui_params['master_gain'] = val\n                    cmd_q.put(('param', 'master_gain', val))\n                \n                gui.root.after(0, lambda c=cc, n=norm: gui.knob_vars[c].set(n))\n\nif __name__ == \"__main__\":\n    multiprocessing.set_start_method('spawn', force=True)\n    cmd_q, mon_q = multiprocessing.Queue(), multiprocessing.Queue()\n    proc = multiprocessing.Process(target=audio_process_entry, args=(cmd_q, mon_q), daemon=True); proc.start()\n    root = tk.Tk(); root.geometry(\"950x700\")\n    app = SynthGUI(root, cmd_q, mon_q)\n    threading.Thread(target=midi_loop, args=(cmd_q, app), daemon=True).start(); root.mainloop()\n<\/code><\/pre><\/div>\n\n<\/div>\n\t\t<\/div>\n<\/div>\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>FM\u97f3\u6e90\u65b9\u5f0f FM\u97f3\u6e90\u306e\u30ec\u30b7\u30d4\u306e\u30b3\u30c4\uff08\u81ea\u5206\u3067\u4f5c\u308b\u5834\u5408\uff09 \u3053\u306e\u30b7\u30f3\u30bb\u30b5\u30a4\u30b6\u30fc\u306b\u304a\u3051\u308b\u5404\u30d1\u30e9\u30e1\u30fc\u30bf\u306e\u97f3\u3078\u306e\u5f71\u97ff\u306f\u4ee5\u4e0b\u306e\u3068\u304a\u308a\u3067\u3059\u3002<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-2286","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"featured_image_src":null,"author_info":{"display_name":"mars","author_link":"https:\/\/rfsec.ddns.net\/db\/?author=1"},"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/posts\/2286","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2286"}],"version-history":[{"count":9,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/posts\/2286\/revisions"}],"predecessor-version":[{"id":2311,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/posts\/2286\/revisions\/2311"}],"wp:attachment":[{"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2286"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2286"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2286"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}