🥁

即興演奏のためのオーディオビジュアルセットを作った

2024/09/17に公開

2024年の9月14日に岡千穂さん、野本直輝さんらからComputer Music Partyというイベントにお誘いいただき、かなり久し振りにソロでの即興演奏、オーディオビジュアルのライブをしました。出演者全員やってることがみんな違ってて、全員何をしているのか全然わからなくて素晴らしいイベントでした。。
Calum Gunn, moxus, Naoki Nomoto & Utah Kawasaki, Satoru Higa, okachiho - Tokyo Gig Guide

本記事はそのイベントの中で何をしていたか、どう作ったか、についての内容となります

実際の演奏中の様子はこちら
https://www.youtube.com/watch?v=pF1u29JSG_g

モチベーション

大学生のころ、専攻ではコンピューターミュージックを学ぶかたわら放課後はジャズ研でドラムを演奏するといった学生生活を送っていました

卒業して早20年程度が経過した今、久しぶりにドラム触りたいけどな~ となったりしたのですが、おいそれと生のドラムセットを購入して演奏するというのは、騒音の問題、住居内スペースの問題から日本の住宅事情だとかなり難しい、というのが現状です。(まあ、練習スタジオに行けばいんですがね…)

そんな訳でMPCなどパッドタイプのサンプラーを買ってみたりもしたんですが、楽器としてのドラムの感覚とは違いすぎてフラストレーションを蓄積させていきました。なんで足が使えないんだよ。叩いた時のニュアンス違い全然出なくないか? などと

打楽器になじみのない人も多いと思うのですが、たとえばスネアドラムを例にとるだけでも色々な音色の変化があります

ドラムのヘッド(天面の皮の部分)の中心部を叩くと詰まった音、ヘッドのはじっこの部分を叩くとどんどん高音の成分が強くなっていきますし、リムと呼ばれる金属製のフレームとヘッドを同時に叩けばいわゆるリムショットと呼ばれるヘッドの膜の振動と木製のボディ部分の共鳴があわさった音になります

他にもリムの部分だけを叩けば金属質の音がしますし、スティックをヘッドに押し付けながらリムを叩くクローズドリム、押し付けたスティックをさらに別のスティックで叩く、などなど… とにかく様々な音、おもしろい音が出る方法が色々とあります

そのような奏法の違いがあり、叩く対象もスネアドラムだけでなく色々な径のドラム、大小シンバル類があり、スティックを振る強さ、当たる角度、ひじや肩の動かし肩でも音がかわっていきます

そんな多数のパラメーターを想像しながら物理的に物を殴るというのが打楽器な訳なのですが、パッドタイプのサンプラーとしてこれを立ち上げるとどうなるか。単音の255段階の音量、またはサンプルの変化になる訳です。これはドラムかもしれないが、打楽器ではないだろうよ… というのがテンションが上がらない大きな部分としてありました

アイディア

具体的に何をしようとしていたのかは忘れてしまったのですが、デジタルフィルタの仕組みを調べていた所でIIRフィルタとFIRフィルタというのがある、といった記事を読みました

ざっくりと言うとIIRフィルタはローパスやハイパスなどの特性を持ったフィルタを作りたい時に使う、FIRフィルタはもっと複雑なフィルタリング処理をしたい時に使う、みたいなことだったりする思うのですが、FIRフィルタはいわゆるコンボリューションリバーブ、インパルスレスポンスリバーブ、サンプリングリバーブとなど呼ばれるオーディオのプラグインにも使用されています

この技術を使うと、たとえば有名なホールの部屋鳴りをサンプリングして、自分の楽曲があたかもその場で再生されているかのような残響を付加できるというようなことができ、そのような製品が多数発売されています。近年では大抵のDAWにも同様の機能を持ったプラグインが同梱されています

こういったコンボリューションリバーブの単純化した理屈としては:

  1. 残響成分を収録したい場所に行ってスピーカーとマイクを配置する
  2. スピーカーからインパルス(ごく短期間で0→1→0の変化をする信号)を出力して、それをマイクで収録する
  3. 収録した音声が残響成分ということになるので、その音声を使ってエフェクトをかけたい音声に対して畳み込み演算を行う

といったプロセスで残響を付加しているようです。要は特定の入力(インパルス)に対して、部屋の環境(残響成分)がどう反応するか、というのを計測して後付けしまっせ、ということです

ということは、理屈としてはコンボリューションリバーブに対してインパルスの信号を入力するとロードされた残響成分をサンプリングした波形が帰ってくる、ということになります。

ということは、ロードするサンプルを残響成分のファイルではなく適当なオーディオファイルにしたらサンプルレベルのサンプラーになるのでは…? というアイディアがおぼろげながら浮かんできたんです

実験

というわけで実験をしました。その当時動画を撮っていたわけではなかったので再現ビデオを収録しました

https://www.youtube.com/watch?v=-xl0UWS1pGY

単純にピエゾの音をDAWに入力して、ドラムのサンプルをロードしたコンボリューションリバーブをかけているだけですが、もうこれだけでMIDIのパッドとは段違いの再現力。。。これはいける… という所まで確認して、このアイディアは約1年ほど塩漬けの状態になります

打楽器としての能力を実装する

AD変換

時は流れて、前述のComputer Music Partyというイベントにお誘いいただいたので寝かしていたあのネタをやる時がついに来たか… ということになりました

コンセプト実証的な所まではできていたのですが、打楽器は基本的にドラムやシンバルといった要素を複数組み合わせて演奏することが多いです。なのでマルチチャンネルのオーディオ入力インターフェイスを作ることにしました(当然ですね)

複数入力系統のあるオーディオインターフェイスを使うのが楽は楽なのですが、ゆくゆくRasPiなどでスタンドアロンの楽器にしたいなという野望があったので一旦茨の道を選んでいきます

秋月電子商会を眺めていい感じのコンポーネントがないか探してみた所、MCP3208というADコンバーターがSPI通信で8ch出るらしい、というのと、FT232HというチップでSPIとUSBの変換ができるらしい、というのでとりあえず購入しました


12bit 8ch ADコンバーター MCP3208-CI/P: 半導体 秋月電子通商-電子部品・ネット通販


FT232H使用USB⇔GPIO+SPI+I2C変換モジュール: 半導体 秋月電子通商-電子部品・ネット通販

SPI通信も初めてだったので、FT232HでSPI通信ができるとされている公式から提供されていたライブラリ(libmpsse)を試してみました

結果、所動くには動くけどADC 1サンプリングあたり1ms程度しか出なくない…? となり、それでいくと1フレームあたり8サンプル = サンプリングレート125Hz程度の動作しかできませんでした。125Hzだと最高でも60Hz程度の周波数までしかとれないことになり、検証してないのでわからないけどそれはさすがに低すぎないか…? となりました

MCP3204のデータシート的には100Kサンプル/秒で動くと書いてあったので、それでいくと1フレームあたり8サンプルとってもスペック的には12.5KHzでサンプリングできるはずなのだが… と思って色々やって、結局libmpsseを使わずに、もう1階層下のレイヤーのftd2xxというライブラリを使う方法を試しました

インターネットを駆使しながらコピペコピペを繰り返し、めでたくサンプリングレート9KHz弱まで上げられたので一旦理論値を追うのはやめてここで手打ちにしました。ピエゾからの音声入力程度なら9KHz弱で十分でしょ という見込みです

ブレッドボードに組んでいって、とりあえずこんなかんじに。アナログ回路的な部分は正直自信がなさすぎますが、、、

というわけで、材料費としては全部でざっくり1万円以内で12bit 8KHz 8chのピエゾADコンバーターができました。やったね!

以下、必要になる人がいるかもしれないのでサンプルコードを貼っておきます。(とりあえず動いた!!!ってところで止めているので、汚かったりバグがあったりするはずです)

MCP3208 + FT232Hを動かすサンプルコード

MCP3208.h

#pragma once

#include <vector>
#include <deque>
#include <thread>
#include <atomic>
#include <mutex>

class MCP3208
{
public:

    MCP3208();
    ~MCP3208();

    void start();
    void stop();

    void get_buffer(std::vector<int16_t>& buffer);

protected:

    std::thread thread_;
    std::atomic_bool running_ = false;

    std::deque<uint8_t> stream_buffer_ {};

    std::mutex channel_data_mutex_;
    std::deque<int16_t> channel_data_ {};

};

MCP3208.cpp

#include "MCP3208.h"

#include <array>
#include <cassert>
#include <string>
#include <iostream>

#define FTD2XX_STATIC

extern "C" {
#include <ftd2xx.h>
}

std::string to_string(FT_STATUS status)
{
    switch (status)
    {
    case FT_OK: return "FT_OK";
    case FT_INVALID_HANDLE: return "FT_INVALID_HANDLE";
    case FT_DEVICE_NOT_FOUND: return "FT_DEVICE_NOT_FOUND";
    case FT_DEVICE_NOT_OPENED: return "FT_DEVICE_NOT_OPENED";
    case FT_IO_ERROR: return "FT_IO_ERROR";
    case FT_INSUFFICIENT_RESOURCES: return "FT_INSUFFICIENT_RESOURCES";
    case FT_INVALID_PARAMETER: return "FT_INVALID_PARAMETER";
    case FT_INVALID_BAUD_RATE: return "FT_INVALID_BAUD_RATE";
    case FT_DEVICE_NOT_OPENED_FOR_ERASE: return "FT_DEVICE_NOT_OPENED_FOR_ERASE";
    case FT_DEVICE_NOT_OPENED_FOR_WRITE: return "FT_DEVICE_NOT_OPENED_FOR_WRITE";
    case FT_FAILED_TO_WRITE_DEVICE: return "FT_FAILED_TO_WRITE_DEVICE";
    case FT_EEPROM_READ_FAILED: return "FT_EEPROM_READ_FAILED";
    case FT_EEPROM_WRITE_FAILED: return "FT_EEPROM_WRITE_FAILED";
    case FT_EEPROM_ERASE_FAILED: return "FT_EEPROM_ERASE_FAILED";
    case FT_EEPROM_NOT_PRESENT: return "FT_EEPROM_NOT_PRESENT";
    case FT_EEPROM_NOT_PROGRAMMED: return "FT_EEPROM_NOT_PROGRAMMED";
    case FT_INVALID_ARGS: return "FT_INVALID_ARGS";
    case FT_NOT_SUPPORTED: return "FT_NOT_SUPPORTED";
    case FT_OTHER_ERROR: return "FT_OTHER_ERROR";
    case FT_DEVICE_LIST_NOT_READY: return "FT_DEVICE_LIST_NOT_READY";
    default: return "Unknown";
    }
}

#define FT_CHECK(CODE)\
    do {\
        FT_STATUS status = CODE;\
        if (status != FT_OK) {\
            std::cerr << "Error: " << to_string(status) << std::endl;\
            assert(false);\
        }\
    } while (0)

FT_HANDLE ft;

#define USB_INPUT_BUFFER_SIZE 65536
#define USB_OUTPUT_BUFFER_SIZE 65536

/* Shifting commands IN MPSSE Mode*/
#define MPSSE_WRITE_NEG 0x01   /* Write TDI/DO on negative TCK/SK edge*/
#define MPSSE_BITMODE   0x02   /* Write bits, not bytes */
#define MPSSE_READ_NEG  0x04   /* Sample TDO/DI on negative TCK/SK edge */
#define MPSSE_LSB       0x08   /* LSB first */
#define MPSSE_DO_WRITE  0x10   /* Write TDI/DO */
#define MPSSE_DO_READ   0x20   /* Read TDO/DI */
#define MPSSE_WRITE_TMS 0x40   /* Write TMS/CS */

/* FTDI MPSSE commands */
#define SET_BITS_LOW   0x80
/*BYTE DATA*/
/*BYTE Direction*/
#define SET_BITS_HIGH  0x82
/*BYTE DATA*/
/*BYTE Direction*/
#define GET_BITS_LOW   0x81
#define GET_BITS_HIGH  0x83
#define LOOPBACK_START 0x84
#define LOOPBACK_END   0x85
#define TCK_DIVISOR    0x86
/* H Type specific commands */
#define DIS_DIV_5       0x8a
#define EN_DIV_5        0x8b
#define EN_3_PHASE      0x8c
#define DIS_3_PHASE     0x8d
#define CLK_BITS        0x8e
#define CLK_BYTES       0x8f
#define CLK_WAIT_HIGH   0x94
#define CLK_WAIT_LOW    0x95
#define EN_ADAPTIVE     0x96
#define DIS_ADAPTIVE    0x97
#define CLK_BYTES_OR_HIGH 0x9c
#define CLK_BYTES_OR_LOW  0x9d
/*FT232H specific commands */
#define DRIVE_OPEN_COLLECTOR 0x9e
/* Value Low */
/* Value HIGH */ /*rate is 12000000/((1+value)*2) */
#define DIV_VALUE(rate) (rate > 6000000)?0:((6000000/rate -1) > 0xffff)? 0xffff: (6000000/rate -1)

/* Commands in MPSSE and Host Emulation Mode */
#define SEND_IMMEDIATE 0x87
#define WAIT_ON_HIGH   0x88
#define WAIT_ON_LOW    0x89

/* Commands in Host Emulation Mode */
#define READ_SHORT     0x90
/* Address_Low */
#define READ_EXTENDED  0x91
/* Address High */
/* Address Low  */
#define WRITE_SHORT    0x92
/* Address_Low */
#define WRITE_EXTENDED 0x93
/* Address High */
/* Address Low  */

namespace Pin {
    // enumerate the AD bus for conveniance.
    enum bus_t {
        SK = 0x01, // ADBUS0, SPI data clock
        DO = 0x02, // ADBUS1, SPI data out
        DI = 0x04, // ADBUS2, SPI data in
        CS = 0x08, // ADBUS3, SPI chip select
        L0 = 0x10, // ADBUS4, general-ourpose i/o, GPIOL0
        L1 = 0x20, // ADBUS5, general-ourpose i/o, GPIOL1
        L2 = 0x40, // ADBUS6, general-ourpose i/o, GPIOL2
        l3 = 0x80  // ADBUS7, general-ourpose i/o, GPIOL3
     };
}

static FT_STATUS FT_Write(FT_HANDLE handle, std::vector<uint8_t> data)
{
    DWORD bytesWritten;
    // std::vector<uint8_t> data(list);
    return FT_Write(handle, data.data(), data.size(), &bytesWritten);
}

static FT_STATUS FT_Read(FT_HANDLE handle, std::vector<uint8_t>& out_data)
{
    DWORD bytesInInputBuf = 0;
    FT_CHECK(FT_GetQueueStatus(ft, &bytesInInputBuf));

    out_data.resize(bytesInInputBuf);
    return FT_Read(handle, out_data.data(), bytesInInputBuf, &bytesInInputBuf);
}

void append(std::vector<uint8_t>& data, std::initializer_list<uint8_t> list)
{
    data.insert(data.end(), list.begin(), list.end());
}

void append(std::vector<uint8_t>& data, std::initializer_list<int> list)
{
    data.insert(data.end(), list.begin(), list.end());
}

MCP3208::MCP3208()
{
}

MCP3208::~MCP3208()
{
    stop();
}

void MCP3208::start()
{
    DWORD bytesWritten;

    DWORD numChannels = 0;
    FT_CHECK(FT_CreateDeviceInfoList(&numChannels));
    std::cout << "Number of channels: " << numChannels << std::endl;

    std::vector<FT_DEVICE_LIST_INFO_NODE> devList(numChannels);
    FT_CHECK(FT_GetDeviceInfoList(devList.data(), &numChannels));

    for (DWORD channel = 0; channel < numChannels; channel++)
    {
        auto dev = devList[channel];
        printf("Device %d:\n", channel);
        printf("\tVID/PID: 0x%04x/0x%04x\n", dev.ID >> 16, dev.ID & 0xFFFF);
        printf("\tSerial: %s\n", dev.SerialNumber);
        printf("\tDescription: %s\n", dev.Description);
    }

    assert(numChannels > 0);

    FT_CHECK(FT_Open(0, &ft));

    FT_ResetDevice(ft);
    FT_SetUSBParameters(ft, USB_INPUT_BUFFER_SIZE, USB_OUTPUT_BUFFER_SIZE);
    FT_SetChars(ft, false, 0, false, 0);

    FT_SetLatencyTimer(ft, 1);
    FT_SetTimeouts(ft, 1000, 1000);

    FT_SetBitMode(ft, 0x00, FT_BITMODE_RESET);

    // Set all pin to input
    FT_SetBitMode(ft, (UCHAR)0, FT_BITMODE_MPSSE);

    {
        uint32_t clock = 2000000;
        uint8_t ENABLE_CLOCK_DIVIDE = 0x8B;
        FT_Write(ft, {ENABLE_CLOCK_DIVIDE});

        FT_Write(ft, {EN_DIV_5});

        uint8_t SET_CLOCK_FREQUENCY_CMD = 0x86;
        clock = (6000000/clock) - 1;
        uint8_t clockL = (uint8_t)clock;
        uint8_t clockH = (uint8_t)(clock>>8);

        FT_Write(ft, {TCK_DIVISOR, clockL, clockH});
    }

    const unsigned char pinInitialState = Pin::CS;
    const unsigned char pinDirection = Pin::SK | Pin::DO | Pin::CS;

    FT_CHECK(FT_Write(ft, {SET_BITS_LOW, pinInitialState, pinDirection}));
    FT_CHECK(FT_Write(ft, {LOOPBACK_END}));
    FT_CHECK(FT_Write(ft, {DIS_ADAPTIVE}));

    // Empty device input buffer
    DWORD bytesInInputBuf = 0;
    FT_CHECK(FT_GetQueueStatus(ft, &bytesInInputBuf));
    assert(bytesInInputBuf == 0);

    running_ = true;
    thread_ = std::thread([this, pinInitialState, pinDirection]()
    {
        std::array<uint16_t, 8> chan {};

        while (running_)
        {
            std::vector<uint8_t> out_buffer {};

            for (int i = 0; i < 8; i++)
            {
                uint8_t channel = i;

                append(out_buffer, {SET_BITS_LOW, pinInitialState & ~Pin::CS, pinDirection});

                uint8_t nbytes = 3;
                append(out_buffer, {MPSSE_DO_WRITE | MPSSE_WRITE_NEG | MPSSE_DO_READ, nbytes-1, 0});
                append(out_buffer, {0x06 | (channel >> 2), (channel & 0x03) << 6, 0x00});

                append(out_buffer, {SET_BITS_LOW, pinInitialState | Pin::CS, pinDirection});
            }

            FT_CHECK(FT_Write(ft, out_buffer));

            std::vector<uint8_t> data {};
            FT_CHECK(FT_Read(ft, data));

            if (!data.empty())
            {
                std::lock_guard<std::mutex> lock(channel_data_mutex_);
                stream_buffer_.insert(stream_buffer_.end(), data.begin(), data.end());

                while (stream_buffer_.size() >= 24) // 3byte * 8channel
                {
                    auto it = stream_buffer_.begin();

                    for (int i = 0; i < 8; i++)
                    {
                        auto in0 = *it++;
                        auto in1 = *it++;
                        auto in2 = *it++;

                        int value = (in1 & 0x0F);
                        value <<= 8;
                        value += in2;

                        chan[i] = value;
                    }

                    {
                        channel_data_.insert(channel_data_.end(), chan.begin(), chan.end());
                    }

                    stream_buffer_.erase(stream_buffer_.begin(), stream_buffer_.begin() + 24);
                }
            }
        }
    });
}

void MCP3208::stop()
{
    running_ = false;
    if (thread_.joinable())
    {
        thread_.join();
    }
}

void MCP3208::get_buffer(std::vector<int16_t>& buffer)
{
    std::lock_guard<std::mutex> lock(channel_data_mutex_);
    buffer.assign(channel_data_.begin(), channel_data_.end());
    channel_data_.clear();
}

DAWにつなげる

さて、めでたくAD変換ができたのですが、このままだとC++のプログラム内でデータがとれた、という状態なので最終的に音が出る系統と繋げなければなりません

今回はDAWとして Bitwig Studio を使っていたのですが、愛用している理由のひとつとして JACK Audio Connection Kit にネイティブ対応している、というのがあります

JACKとはは何かというと、PCの中で立ち上がっているソフトウェア間でオーディオ信号をむすぶバーチャルパッチベイのようなユーティリティで、Linuxなどではよく使われているらしいのですがWindows環境にも移植されています

これを使うと比較的簡単にオーディオをDAWに立ち上げられるなと思ったので、ADしたサンプルをオーディオのサンプリングレートに線形補完でリサンプルしながら流すコードを書きました

AD変換したデータをJACKに流す部分のサンプルコード

main.cpp

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <signal.h>

#ifndef WIN32
#include <unistd.h>
#endif

#include <iostream>

#include <jack/jack.h>

#include "MCP3208.h"

MCP3208 device;

#define NUM_CHANNELS 8

jack_port_t* output_ports[NUM_CHANNELS];
jack_client_t* client;

int INPUT_SAMPLE_RATE = 8775;
int OUTPUT_SAMPLE_RATE = 44100;
#define BUFFER_LEN 512

static void signal_handler(int sig)
{
    jack_client_close(client);
    fprintf(stderr, "signal received, exiting ...\n");
    exit(0);
}

float rate = 0;

int process(jack_nframes_t nframes, void* arg)
{
    jack_default_audio_sample_t* out[NUM_CHANNELS];

    for (int i = 0; i < NUM_CHANNELS; i++)
    {
        out[i] = (jack_default_audio_sample_t*)jack_port_get_buffer(output_ports[i], nframes);
    }


    std::vector<int16_t> buffer;
    device.get_buffer(buffer);

    const size_t NUM_INPUT_FRAMES = buffer.size() / NUM_CHANNELS;

    if (!buffer.empty())
    {
        std::vector<float> input_buffer(buffer.size());
        std::vector<float> output_buffer(nframes * NUM_CHANNELS);

        for (int i = 0; i < buffer.size(); i++)
        {
            const auto NOISE_FLOOR = 100;

            buffer[i] -= NOISE_FLOOR;
            if (buffer[i] < 0)
            {
                buffer[i] = 0;
            }

            input_buffer[i] = (float)buffer[i] / (4096.0f - NOISE_FLOOR);

            input_buffer[i] = pow(input_buffer[i], 1.0);
        }

        for (int n = 0; n < NUM_CHANNELS; n++)
        {
            float* src = input_buffer.data() + n;
            float* dst = output_buffer.data() + n;

            for (int i = 0; i < nframes; i++)
            {
                float d = (float)i / nframes;
                float j = (float)(d * (NUM_INPUT_FRAMES-1));
                int ji = floor(j);
                float jf = j - ji;

                // linear interpolation
                dst[i * NUM_CHANNELS] =
                    src[ji * NUM_CHANNELS] * (1 - jf)
                    + src[(ji + 1) * NUM_CHANNELS] * jf;
            }
        }

        for (int i = 0; i < NUM_CHANNELS; i++)
        {
            auto src = output_buffer.data() + i;
            auto dst = out[i];

            for (int n = 0; n < nframes; n++)
            {
                *dst++ = *src;
                src += NUM_CHANNELS;
            }
        }
    }

    return 0;
}

void jack_shutdown(void* arg)
{
    exit(1);
}

int main(int argc, char* argv[])
{
    const char* client_name = "PiezoDriver";
    const char* server_name = NULL;
    jack_options_t options = JackNullOption;
    jack_status_t status;

    client = jack_client_open(client_name, options, &status, server_name);
    if (client == NULL)
    {
        fprintf(stderr, "jack_client_open() failed, "
                "status = 0x%2.0x\n", status);
        if (status & JackServerFailed)
        {
            fprintf(stderr, "Unable to connect to JACK server\n");
        }
        exit(1);
    }
    if (status & JackServerStarted)
    {
        fprintf(stderr, "JACK server started\n");
    }
    if (status & JackNameNotUnique)
    {
        client_name = jack_get_client_name(client);
        fprintf(stderr, "unique name `%s' assigned\n", client_name);
    }

    jack_set_process_callback(client, process, nullptr);
    jack_on_shutdown(client, jack_shutdown, 0);

    for (int i = 0; i < NUM_CHANNELS; i++)
    {
        char port_name[32];
        sprintf(port_name, "output%d", i);
        output_ports[i] = jack_port_register(client, port_name,
                                          JACK_DEFAULT_AUDIO_TYPE,
                                          JackPortIsOutput, 0);
        if (output_ports[i] == NULL)
        {
            fprintf(stderr, "no more JACK ports available\n");
            exit(1);
        }
    }

    if (jack_activate(client))
    {
        fprintf(stderr, "cannot activate client");
        exit(1);
    }

#ifdef WIN32
    signal(SIGINT, signal_handler);
    signal(SIGABRT, signal_handler);
    signal(SIGTERM, signal_handler);
#else
	signal(SIGQUIT, signal_handler);
	signal(SIGTERM, signal_handler);
	signal(SIGHUP, signal_handler);
	signal(SIGINT, signal_handler);
#endif

    device.start();

    while (1)
    {
#ifdef WIN32
        Sleep(1000);
#else
		sleep (1);
#endif
    }

    jack_client_close(client);
    exit(0);
}

何はともあれ、これで8chのオーディオインターフェイス的なものができました!!!
めでたしめでたし! …ではなくて、音を鳴らす部分を作っていく必要がありますね

発音部分を作る

(中略) できました! 実際に演奏している様子はこちら↓

https://www.youtube.com/watch?v=qccUGOXomQE

だいぶ端折ったように見えるかもしれませんが、オーディオの入力をいい感じのインパルスっぽい波形になるようにハイパスをかけて加工する → コンボリューションリバーブをかける → 出音の調整のためのEQ等々をかける という手順を各チャンネル分やっているだけなのであまり書くことがないのでした…

音楽的なミキシングの部分とかは完全に素人なのでこれからもっと勉強していく必要性をひしひしと感じています。。

ビジュアルの部分を作る

ビジュアルについては以前にビジュアルパフォーマンスでやったシステムを踏襲して、UEでレンダリング、SpoutでTDに送ってカラコレ、のような構成で作っていました

そういった内容については過去の記事にまとめています ↓
https://zenn.dev/satoruhiga/articles/148e670e648d9a

登場するオブジェクトとしてはマイクスタンド + SM57が沢山 (配線を頑張りました)

シネマティックな雰囲気になるかなと思って、大昔に買ったARRIのスタジオライトセットのアセットからいくつか

などなどを投入しております

今回新たに追加した要素としては、音の担当も僕だったためにいつも使っている音解析的なオーディオリアクティブの仕組みのほかに、DAWの各チャンネルにインサートしてトラックごとの音量をOSCで送る部分というのもJUCEを使って作っていました。(これは後程plugdataを使ったらVSTプラグインまで一瞬でできたのでは?ということが発覚します)

TDBitwigにもトラックのボリュームは存在するのですが、RMSの音量しかとれなかったので音反応の要素として使うにはゆっくりとしすぎててダメでした

まとめ

今回作ったデジタル打楽器の仕組みの部分は個人的にはかなり可能性を感じていて、これからも継続的にアップデートしていきたいと思っています

一旦DAW上で出音的に納得いく所まで作れたら、

  • それをどうにかRasPi的なミニコンピューターで動くようにしてポータブルな単体楽器として自立させたいな… とかだったり、
  • ハイハットのオープンクローズがないとやっぱ演奏でできる事が一個減るよな~ みたいな所、
  • 現状スネアもリムショット的な表現はできないのである程度以上の強さになるとやっぱリムの音をまぜるべきか?

などなど、アイディアは尽きませんが、本記事は以上といった所で締めとしたいと思います

面白いと思ってくださった方がいたら、ライブのオファーお待ちしております!

Discussion