Skip to content

エフェクター改造 | PIC | C/C++

DOD FX-96のトレイルバイパス化

WARNING

正しい情報を記載するよう努めていますが、それはそれとして当方は一切の責任を負いかねます。

いかなる作業も自己責任で行っていただくようお願いいたします。

はじめに

ご依頼を受けてDOD FX96 [Echo FX] のトレイルバイパス化を行ったので、その工程を簡単にまとめておきます。

ちょっと動画自体の音が小さいかもしれません。

当初は手前味噌ながらModule-02なるトレイルバイパスモジュールを使う予定だったのですが、より合理的な方法が見つかったので出番なしでした。

方針

このエフェクターでは、エフェクトON/OFFの切り替え時に論理回路で3つの動作を行います。

エフェクトONエフェクトOFF
① MIXノブ有効化無効化
② Wet信号(ディレイ音)ミュート解除ミュート
③ LED点灯消灯

今回のトレイルバイパス化にあたっては、これら3つの動作をマイコンで肩代わりしたうえで、さらに4つ目の動作を追加します。

エフェクトONエフェクトOFF
ディレイ回路に入る直前のDry信号(原音)ミュート解除ミュート

以上4つの動作を回路図上で番号ごとに示したのが次の図です。元の回路図はこの記事の執筆時点でググると出てくるもので、実機と見比べて異なる点は見つかりませんでした。

ImageGroup - big

部品の取り外し

以下の部品を取り外します。

リファレンス番号
Q2J201
Q32N5088
U74007A
C410.22u
R401M
R4610k

TIP

ついでに全ての電解コンデンサを同等の新品と交換しました。体感的に、「エフェクトかかりません」を理由にジャンクになっている古い揺れ/空間系エフェクターの大半は電解コンデンサの寿命が原因であるように思います。もっともジャンクを漁る者としてはそこが狙い目な訳ですが…

新しい回路の取り付け

実態配線図

部品を取り外した跡地めがけて以下の基板を繋ぎます。マイコンは在庫が沢山あるという理由でPIC12F675を使っています。SW1(フットスイッチ)およびD1(LED)は別に取り外していないので、足に絡めてはんだ付けします。

ImageGroup - big

Q2とC41

回路が部分的にそっくり挿げ替えられているのがQ2(方針①)およびC41(方針④)の部分で、上の実態配線図ではQ2が左の基板に、C41が右の基板の上部に対応しています。これらは回路図で見るとそれぞれ次のようになっています。

← Q2 | C41 →← Q2 | C41 →

← Q2 | C41 →

TIP

エフェクター全体の駆動電圧が+9Vで固定ならばQ2は取り外さずとも事足ります。このときバイアス電圧は+4.5Vなので、マイコンからQ2に+5V(HIGH)/0V(LOW)を与えるだけでQ2のON/OFFを制御できるからです。しかしこの方式でエフェクター全体の駆動電圧が+10Vを超えると、バイアス電圧が+5V以上になるのでQ2で常にVSVGとなってしまい制御が効きません。

このエフェクターでは駆動電圧がディレイ音に大きく影響するので、+9Vより大きい電圧で駆動することにかなり意義があります。従って、どうせならQ2を取り外す今回の方式の方がベターかなと思います。

ちなみにディレイ用チップたちの受けられる電圧が-18Vまで(※これらのICは内部的には負電源で動かしている)なので、エフェクターとしての実質的な耐圧は+18V弱になると思われます。

ソースコード

電源投入時のオプション設定(オルタネイト/モーメンタリなど)を含めてしまっているので量が多くなってしまっていますが、重要な処理はTURN()scanModeSw()あたりでしょうか。具体的に何が起こっているのかの説明はコメントアウトに託しました。

c
/**
 * @file        main.c
 * @author      aSumo
 * @version     1.0
 * @date        2025-08-03
 * @copyright   (c) 2025 aSumo
 * @brief       FX96のトレイルバイパス用プログラム
 * @details     PIC12F6XX以外を使う場合はPIC12F6XX.cを適宜書き換えること
 */

#include "header.h"

/**
 * @fn      void main(void)
 * @brief   サボり上司
 */
int main(void) {
  disAnalog();         // アナログ機能を備えている場合は無効化
  CMCON = 0b00000111;  // コンパレータ不使用
  TRISIO = 0b00011000; // GP3とGP4が入力、他は出力
  GPIO = 0;            // 全てのピンをLOWに設定してから
  MUTE_wet = true;     // エフェクトOFFなのでWet音をミュート

  INTCON |= 0b10001000; // GIE=1, GPIE=1 (GPIO割り込み有効化)
  IOC3 = 1;             // FootSwのピン変化割り込みを有効化
  di();                 // とは言ったものの、まだ割り込み禁止

  initConfig(); // 電源投入時の設定を行う
  scanModeSw(); // 一旦モードスイッチに合わせてから

  if (initState) { // 初期状態を合わせる
    TURN();
  }

  ei(); // ここでようやく割り込みを解禁

  while (true) {
    scanModeSw(); // モードスイッチを見張りながらフットスイッチの割り込み待ち
  }

  return 0;
}
c
#include <stdbool.h>
#include <stdint.h>
#include <xc.h>

#define _XTAL_FREQ 4000000 //!< クロック周波数 4MHz

#pragma config FOSC = INTRCIO // 内部クロック          使用
#pragma config WDTE = OFF     // Watch-Dogタイマー     不使用
#pragma config PWRTE = ON     // パワーアップタイマー  使用
#pragma config MCLRE = OFF    // リセットピン          不使用
#pragma config BOREN = ON     // Brown-Out検出         使用
#pragma config CP = ON        // プログラム保護        使用
#pragma config CPD = ON       // データ保護            使用

// ピンアウト
#define LED GP0      //!< LEDへの出力
#define MUTE_dry GP1 //!< Dry音ミュート用のフォトカプラへの出力
#define MUTE_wet GP2 //!< Wet音ミュート用のトランジスタへの出力
#define SW_foot GP3  //!< フットスイッチからの入力
#define SW_trail GP4 //!< トレイルスイッチからの入力
#define ENABLE_mix GP5 //!< Mixノブ用のFET用のリレーへの出力

// 状態変数など
bool isTrail = false;   //!< バイパスモード: トレイル / ノーマル
bool state = false;     //!< エフェクト: ON / OFF
bool initState = false; //!< ペダルの初期状態
bool momentary = false; //!< モーメンタリ動作の是非
bool timing = false;    //!< オルタネイト動作時、いつ反転するか

/**
 * エフェクトON or OFFの切り替えマクロ
 * 割り込み中に同じ関数を複数回呼ぶと良くないらしいのでマクロ化している
 */
#define TURN()                                                                 \
  do {                                                                         \
    ENABLE_mix = !ENABLE_mix;                                                  \
    if (isTrail) {                                                             \
      MUTE_dry = !MUTE_dry;                                                    \
    } else {                                                                   \
      MUTE_wet = !MUTE_wet;                                                    \
    }                                                                          \
    LED = !LED;                                                                \
    state = !state;                                                            \
  } while (false)

// その他
#define EEPADR_initState 0x00 //!< EEPROM内のアドレスその1
#define EEPADR_momentary 0x02 //!< EEPROM内のアドレスその2
#define EEPADR_timing 0x04    //!< EEPROM内のアドレスその3
#define LED_SHORT_MS 50       //!< LED用のインターバル
#define LED_LONG_MS 250       //!< LED用のインターバル

// 関数のプロトタイプ宣言
uint8_t eepRead(uint8_t adress);
void eepWrite(uint8_t adress, uint8_t myData);
void flashLED(uint8_t interval, uint8_t times);
void disAnalog(void);
void initConfig(void);
void scanModeSw(void);
c
/**
 * @file        common.c
 * @author      aSumo
 * @version     1.0
 * @date        2025-08-03
 * @copyright   (c) 2025 aSumo
 * @brief       マイコンによらず共通な関数を定義するファイル
 */

#include "header.h"

/**
 * @fn      void flashLED(uint8_t interval_type, uint8_t times)
 * @param   types   点滅の間隔
 * @param   times   点滅の回数
 * @brief   LEDを点滅させる関数
 */
void flashLED(uint8_t interval_type, uint8_t times) {
  for (uint8_t i = 0; i < times; i++) {
    switch (interval_type) {
    case 1:
      LED = true;
      __delay_ms(LED_SHORT_MS);
      LED = false;
      __delay_ms(LED_SHORT_MS);
    case 2:
      LED = true;
      __delay_ms(LED_LONG_MS);
      LED = false;
      __delay_ms(LED_LONG_MS);
    default:
      break;
    }
  }
}

/**
 * @fn     void initConfig(void)
 * @brief  EEPROMの値を読み書きして初期状態を設定する関数
 */
void initConfig(void) {
  uint16_t flag_i = 0; // 踏まれている時間をカウントする変数

  // EEPROMからuint8_t型をbool型に変換して読み込む(0:false / 1:true)
  initState = (bool)(eepRead(EEPADR_initState));
  momentary = (bool)(eepRead(EEPADR_momentary));
  timing = (bool)(eepRead(EEPADR_timing));

  // 踏みながら電源を入れてモード切り替え
  if (!SW_foot) {
    flashLED(1, 3);

    // Alternate or Momentary
    for (flag_i = 0; flag_i < 300; flag_i++) { // flag_i*10秒間待ってやる
      if (!SW_foot) {
        __delay_ms(10);         // 踏まれている間は待ち続ける
      } else {                  // 離されたら
        momentary = !momentary; // 設定値を更新
        eepWrite(EEPADR_momentary, (uint8_t)momentary);
        break;
      }
    }

    // 電源投入時のエフェクトON or OFF
    if (flag_i > 275) {
      // initStateの設定に入る
      initState = !initState; // 設定値を更新
      eepWrite(EEPADR_initState, (uint8_t)initState);
      flashLED(2, 3);
      while (!SW_foot) {
        ; // 離されるまでキープ
      };
    }
  }
}

/**
 * @fn      void scanModeSw(void)
 * @brief   モードスイッチをスキャンして、適宜モードを切り替える関数
 */
void scanModeSw(void) {
  // トレイル → ノーマル
  if (SW_trail && isTrail) {
    if (!state) { // エフェクトOFFならば(ONのときは状態が共通)
      MUTE_wet = true;  // Wet音をミュート
      MUTE_dry = false; // Dry音をミュート解除
    }
    isTrail = false;
  }

  // ノーマル → トレイル
  else if (!SW_trail && !isTrail) {
    if (!state) { // エフェクトOFFならば(ONのときは状態が共通)
      MUTE_dry = true;  // Dry音をミュート
      MUTE_wet = false; // Wet音をミュート解除
    }
    isTrail = true;

    // 環境によっては最初の切り替えだけポップノイズが発生するので、
    // ここで予め消化しておく
    TURN();
    TURN();
  }
}
c
/**
 * @file        PIC12F675.c
 * @author      aSumo
 * @version     1.0
 * @date        2025-08-03
 * @copyright   (c) 2025 aSumo
 * @brief       PIC12F675に特有の関数を定義するファイル
 */

#include "header.h"

//! EEPROM(0x00~0x07)の初期値を定義するマクロ
__EEPROM_DATA(0, 0, 0, 0, 0, 0, 0, 0);

/**
 * @fn      uint8_t eepRead(uint8_t adress)
 * @brief   EEPROMからデータを読み込む関数
 * @param   adress  データを読み込む番地
 * @return  読み込んだデータ
 */
uint8_t eepRead(uint8_t adress) {
  EEADR = adress; // アドレスを指定
  RD = 1;         // 読み込みを開始
  while (RD) {
    ;
  }; // RD==0まで待つ
  return EEDATA; // 読み込んだ値を返却
}

/**
 * @fn      uint8_t eepWrite(uint8_t adress, uint8_t myData)
 * @brief   EEPROMにデータを書き込む関数
 * @param   adress  データの書き込み番地
 * @param   myData  書き込むデータ(少なくとも数値型であること!)
 */
void eepWrite(uint8_t adress, uint8_t myData) {
  WREN = 1;        // 書き込みを許可
  EEADR = adress;  // アドレスを指定
  EEDATA = myData; // 書き込みたいデータ
  EECON2 = 0x55;   // 暗号その1
  EECON2 = 0xAA;   // 暗号その2
  WR = 1;          // 書き込みをセット
  while (WR) {     // WR==0まで待つ
    ;
  };
  WREN = 0; // 書き込みを禁止
}

/**
 * @fn      void disAnalog(void)
 * @brief   予約語の宣言の有無からチップのアナログ機能の有無を判別する関数
 * @details 少なくともPIC12F629とPIC12F675の両方で使えるようにしている
 */
void disAnalog(void) {
#ifdef ANSEL
  ANSEL = 0x00;
#endif
#ifdef ADCON0
  ADCON0 = 0x00;
#endif
};

/**
 * @fn      void __interrupt isr(void)
 * @brief   bypass_interrupt()の無限whileループ中に呼び出される
 * @details 割り込み処理の中で関数を呼ぶとあんまり良くないらしい
 */
void __interrupt() isr(void) {
  if (GPIF) {      // GPIO変化割り込みフラグが立っているか?
    __delay_ms(5); // チャタリング対策

    if (!SW_foot) {    // 踏まれているか?
      if (momentary) { // モーメンタリ動作ならば
        TURN();        // ひっくり返す
        while (!SW_foot) {
          ; // 離されるまでキープ
        }
        TURN(); // 離されたらもう一度ひっくり返す
      } else {  // オルタネイト動作ならば
        if (timing) { // タイミングの指定によっては離されるまで待つ
          while (!SW_foot) {
            ; // 離されるまでキープ
          }
        }
        TURN(); // ひっくり返す
      }
    }

    GPIF = 0; // 割り込みフラグをクリア
  }
}

完成

ImageGroup - doubleImageGroup - double

トグルスイッチはLEDの横に増設しましたが、見た目に拘らないのであればもう一段上のノブと同じステージに増設した方が作業しやすいです。

おわりに

ポップノイズと格闘する時間が長かったのですが、全く聞こえないレベルに落ち着いて良かったです。

ただのトゥルーバイパス化だと大半は元のエフェクターの外側にループスイッチャーを増設するような作業になるので画一化が可能ですが、トレイルバイパス化は機種ごとにベストプラクティスが異なってくるな…という気づきがありました。冒頭で述べたようなモジュール基板ではあまり太刀打ちできないのかもしれません。

CC BY-SA 4.0