SPRESENSEでデジタルエフェクターを実現するには、エレキギターの音をキャプチャーして信号処理をして出力するまで短時間で行う必要があります。でないと楽器とスピーカーの音が大きくずれてしまい、まともな演奏ができません。

ヤマハが行った実験によると、30ミリ秒だと遅延があると認識され、50ミリ秒以上の遅延では演奏が困難になるらしいです。





SPRESENSEはデフォルトで48000bpsで音をキャプチャしているので、最大1440サンプル(1440/48000=0.03)を1フレームとして処理できそうです。ただ、より遅延感をなくすために、その半分の720サンプル単位で処理できるようにしたいと思います。

SPRESENSEは短時間で入力から出力するためライブラリが用意されています。FrontEnd ライブラリとOutputMixerライブラリです。実装をしてみたスケッチを示します。

このスケッチのポイントは、"frontend_pcm_cb"関数です。この関数はFrontEndライブラリがデータを取得すると呼ばれるコールバック関数です。この関数の中で、入力処理→信号処理→出力処理を行います。入力がモノラル信号で出力はステレオ信号になることに注意してください。

  • 入力処理:"frontend_signal_input"関数

  • 信号処理:"signal_process"関数

  • 出力処理:"mixer_stereo_output"関数


  • その他見るべきポイントは" setup"関数と"loop"関数です。setup関数で FroneEndライブラリとOuputMixerライブラリの初期化。loop関数でエラー状態の監視を行っています。



    #include <FrontEnd.h>
    #include <OutputMixer.h>
    #include <MemoryUtil.h>
    #include <arch/board/board.h>

    #define SAMPLE_SIZE (720)

    FrontEnd *theFrontEnd;
    OutputMixer *theMixer;

    const int32_t channel_num = AS_CHANNEL_MONO;
    const int32_t bit_length = AS_BITLENGTH_16;
    const int32_t sample_size = SAMPLE_SIZE;
    const int32_t frame_size = sample_size * (bit_length / 8) * channel_num;
    bool isErr = false;

    void frontend_attention_cb(const ErrorAttentionParam *param) {
    Serial.println("ERROR: Attention! Something happened at FrontEnd");
    if (param->error_code >= AS_ATTENTION_CODE_WARNING) isErr = true;
    }

    void mixer_attention_cb(const ErrorAttentionParam *param){
    Serial.println("ERROR: Attention! Something happened at Mixer");
    if (param->error_code >= AS_ATTENTION_CODE_WARNING) isErr = true;
    }

    static bool frontend_done_cb(AsMicFrontendEvent ev, uint32_t result, uint32_t detail){
    UNUSED(ev); UNUSED(result); UNUSED(detail);
    return true;
    }

    static void outputmixer_done_cb(MsgQueId requester_dtq, MsgType reply_of, AsOutputMixDoneParam* done_param) {
    UNUSED(requester_dtq); UNUSED(reply_of); UNUSED(done_param);
    return;
    }

    static void outputmixer0_send_cb(int32_t identifier, bool is_end) {
    UNUSED(identifier); UNUSED(is_end);
    return;
    }

    static void frontend_pcm_cb(AsPcmDataParam pcm) {
    static uint8_t mono_input[frame_size];
    static uint8_t stereo_output[frame_size*2];
    static const bool time_measurement = true;
    if (time_measurement) {
    static uint32_t last_time = 0;
    uint32_t current_time = micros();
    uint32_t duration = current_time - last_time;
    last_time = current_time;
    Serial.println("duration = " + String(duration));
    }

    frontend_signal_input(pcm, mono_input, frame_size);
    signal_process((int16_t*)mono_input, (int16_t*)stereo_output, sample_size);
    mixer_stereo_output(stereo_output, frame_size);
    return;
    }

    void frontend_signal_input(AsPcmDataParam pcm, uint8_t* input, uint32_t frame_size) {
    /* clean up the input buffer */
    memset(input, 0, frame_size);

    if (!pcm.is_valid) {
    Serial.println("WARNING: Invalid data! @frontend_signal_input");
    return;
    }

    if (pcm.size > frame_size) {
    Serial.print("WARNING: Captured size is too big! -");
    Serial.print(String(pcm.size));
    Serial.println("- @frontend_signal_input");
    pcm.size = frame_size;
    }

    /* copy the signal to signal_input buffer */
    if (pcm.size != 0) {
    memcpy(input, pcm.mh.getPa(), pcm.size);
    } else {
    Serial.println("WARNING: Captured size is zero! @frontend_signal_input");
    }
    }

    void signal_process(int16_t* mono_input, int16_t* stereo_output, uint32_t sample_size) {
    /* clean up the output buffer */
    memset(stereo_output, 0, sizeof(int16_t)*sample_size*2);
    /* copy the signal to output buffer */
    for (int n = SAMPLE_SIZE-1; n >= 0; --n) {
    stereo_output[n*2] = stereo_output[n*2+1] = mono_input[n];
    }
    return true;
    }

    void mixer_stereo_output(uint8_t* stereo_output, uint32_t frame_size) {

    /* Alloc MemHandle */
    AsPcmDataParam pcm_param;
    if (pcm_param.mh.allocSeg(S0_REND_PCM_BUF_POOL, frame_size) != ERR_OK) {
    Serial.println("ERROR: Cannot allocate memory @mixer_stereo_output");
    isErr = false;
    return;
    }

    /* Set PCM parameters */
    pcm_param.is_end = false;
    pcm_param.identifier = OutputMixer0;
    pcm_param.callback = 0;
    pcm_param.bit_length = bit_length;
    pcm_param.size = frame_size*2;
    pcm_param.sample = frame_size;
    pcm_param.is_valid = true;
    memcpy(pcm_param.mh.getPa(), stereo_output, pcm_param.size);

    int err = theMixer->sendData(OutputMixer0, outputmixer0_send_cb, pcm_param);
    if (err != OUTPUTMIXER_ECODE_OK) {
    Serial.println("ERROR: sendData -" + String(err) + "- @mixer_stereo_output");
    isErr = true;
    }
    }

    void setup() {
    Serial.begin(115200);

    /* Initialize memory pools and message libs */
    initMemoryPools();
    createStaticPools(MEM_LAYOUT_RECORDINGPLAYER);

    /* setup FrontEnd and Mixer */
    theFrontEnd = FrontEnd::getInstance();
    theMixer = OutputMixer::getInstance();

    /* begin FrontEnd and OuputMixer */
    theFrontEnd->begin(frontend_attention_cb);
    theMixer->begin();
    Serial.println("Setup: FrontEnd and OutputMixer began");

    /* activate FrontEnd and Mixer */
    theFrontEnd->setCapturingClkMode(FRONTEND_CAPCLK_NORMAL);
    theFrontEnd->activate(frontend_done_cb);
    theMixer->create(mixer_attention_cb);
    theMixer->activate(OutputMixer0, outputmixer_done_cb);
    delay(100); /* waiting for Mic startup */
    Serial.println("Setup: FrontEnd and OutputMixer activated");

    /* Initialize FrontEnd */
    AsDataDest dst;
    dst.cb = frontend_pcm_cb;
    theFrontEnd->init(channel_num, bit_length, sample_size, AsDataPathCallback, dst);
    Serial.println("Setup: FrontEnd initialized");

    /* Set rendering volume */
    theMixer->setVolume(0, 0, 0);

    /* Unmute */
    board_external_amp_mute_control(false);
    theFrontEnd->start();
    Serial.println("Setup: FrontEnd started");
    }

    void loop() {
    if (isErr == true) {
    board_external_amp_mute_control(true);
    theFrontEnd->stop();
    theFrontEnd->deactivate();
    theMixer->deactivate(OutputMixer0);
    theFrontEnd->end();
    theMixer->end();
    Serial.println("Capturing Process Terminated");
    while(1) {};
    }
    }



    入出力までの時間の計測結果は次のようになりました。縦軸はマイクロ秒です。多少ばらつきはありますが、狙った通り15ミリ秒で処理ができているようです。




    エフェクトは、"signal_process"関数内に記述していけば出力に反映されます。次から入力信号にエフェクトをかけてみたいと思います!
    (^^)/~

    「SPRESENSEでデジタルエフェクターを作ろう!」ープリアンプの製作編


    「SPRESENSEでデジタルエフェクターを作ろう!」ー低遅延入出力の実現ー

    「SPRESENSEでデジタルエフェクターを作ろう!」ーコンプレッサーとディストーションの実現ー

    「SPRESENSEでデジタルエフェクターを作ろう!」ー空間系エフェクターの実現ー

    「SPRESENSEでデジタルエフェクターを作ろう!」ーモジュレーション系エフェクターの実現ー



    SPRESENSEではじめるローパワーエッジAI (Make: PROJECTS)

    • 出版社/メーカー: オライリージャパン
    • 発売日: 2022/02/28
    • メディア: 単行本(ソフトカバー)



    SONY SPRESENSE メインボード CXD5602PWBMAIN1

    • 出版社/メーカー: スプレッセンス(Spresense)
    • メディア: Tools & Hardware



    SONY SPRESENSE 拡張ボード CXD5602PWBEXT1

    • 出版社/メーカー: スプレッセンス(Spresense)
    • メディア: Tools & Hardware