はじめに
配信で声の雰囲気を少し変えたいときや、BGM と声のキー感を揃えたいときに、ピッチシフトは分かりやすい手段です。一方で OBS プラグイン開発の観点では、音をどう変えるかより先に、音声がどの粒度で届き、どのバッファを返すべきか を理解していないと破綻します。本記事ではアルゴリズム解説を最小限にし、OBS Studio 32.0.4 の audio filter API で動く土台を作ります。
前提として、過去 3 本の記事で扱った内容を既知として進めます。
成果物の仕様
- フィルタ表示名:
Test Pitch Shift/検証用ピッチシフト - フィルタ ID:
test_pitchshift_filter - プロパティ名:
Semitone/半音 - デフォルト値:
0 - サンプルレート: 44.1 kHz
- 方針: レイテンシ優先、音質は後回し
YouTube 公式のガイドライン にて、ステレオ音声の場合は 44.1 KHz という推奨値が案内されているため、今回はこれにあわせます。
検証環境
- OS: Windows 11
- OBS Studio: 32.0.4
- Visual Studio: Visual Studio 2022 Community
- Windows 11 SDK: 10.0.22621.0
- MSVC ビルドツール: v143
- CMake: 4.2.1
- Git: 2.52.0.windows.1
Part 0: 日本語を含む C++ のビルド
CMake 側で /utf-8 を渡し、ソースと実行文字セットを UTF-8 として扱わせます。MSVC の既定コードページ次第で、日本語コメントや日本語文字列が原因でビルドが崩れるのを避けます。
CMakeLists.txt に次を追加します。
target_compile_features(${CMAKE_PROJECT_NAME} PRIVATE cxx_std_17)
+if (MSVC)
+ target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE /utf-8)
+endif()
/utf-8 はコンパイラに「ソースを UTF-8 として解釈する」指定ですが、環境によってはエディタ側で UTF-8 と認識されず警告が出ることがあります。そこで .editorconfig を次のように変更し、 C++ ファイルを utf-8-bom として保存するようにします。
root = true
[*]
insert_final_newline = true
end_of_line = lf
charset = utf-8
[*.{c,cc,cpp,h,hpp,inl}]
charset = utf-8-bom
合わせて、今回追加する .cpp / .h は UTF-8 で保存してください。
動作確認
- C++ ファイルにコメント行など日本語追加
- Visual Studio でソリューションのリビルド
Part 1: フィルタを登録して OBS に出す
まずは「OBS にフィルタが出る」部分だけを作ります。この段階では locale もプロパティ UI も未実装で構いません。
1. src/test-pitchshift-filter.cpp を追加する
Git Bash でプロジェクトフォルダーに移動します。
cd <プロジェクトフォルダーのパス> # 例 `/c/Users/AllegroMoltoV/test-plugin`
下記のコマンドでファイルを新規作成します。
touch src/test-pitchshift-filter.cpp
これまでの設定だと src/*.h が Visual Studio の Solution Explorer に表示されません。後で DSP を分割していくので、見通しのために src/*.h も拾うようにします。CMakeLists.txt に行を追加します。
# src/ と data/ を自動で拾う (追加のたびに CMakeLists.txt を編集しないため)
file(
GLOB PLUGIN_SOURCES
CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.c"
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
)
Visual Studio でソリューションファイルを開き、 src/test-pitchshift-filter.cpp の内容を次のようにします。
ソリューションエクスプローラー上でファイルが見つからなければ、リビルドののち「すべて再読み込み」を選択します。
#include <obs-module.h>
namespace {
constexpr const char *kFilterId = "test_pitchshift_filter";
constexpr const char *kTextFilterName = "TestPitchShiftFilterName";
static const char *test_pitchshift_get_name(void *)
{
return obs_module_text(kTextFilterName);
}
static obs_audio_data *test_pitchshift_filter_audio(void *, obs_audio_data *audio)
{
// まだ加工しない
return audio;
}
} // namespace
void register_test_pitchshift_filter(void)
{
obs_source_info info = {};
info.id = kFilterId;
info.type = OBS_SOURCE_TYPE_FILTER;
info.output_flags = OBS_SOURCE_AUDIO;
info.get_name = test_pitchshift_get_name;
info.filter_audio = test_pitchshift_filter_audio;
obs_register_source(&info);
}
2. src/plugin-main.cpp から登録関数を呼ぶ
+void register_test_pitchshift_filter(void);
extern "C" bool obs_module_load(void)
{
// 既存の登録
// 以前の記事で追加した register_tint_filter(); などがここにある想定
+ register_test_pitchshift_filter();
obs_log(LOG_INFO, "plugin loaded successfully (version %s)", PLUGIN_VERSION);
return true;
}
動作確認
- リビルドしてインストールし OBS を起動
- 任意の音声付きソース (マイク入力など) を選び「音声フィルタ」から追加
- フィルタ一覧に
TestPitchShiftFilterNameが出ればOK
Part 2: locale を入れて表示名を整える
Part 1 ではキー文字列が見えていました。ここで表示名を整えます。
data/locale/en-US.ini
TestPitchShiftFilterName=Test Pitch Shift
data/locale/ja-JP.ini
TestPitchShiftFilterName=検証用ピッチシフト
動作確認
- OBS でフィルタ名が
Test Pitch Shift/検証用ピッチシフトと表示されることを確認
Part 3: 標準プロパティ UI とインスタンス化
src/test-pitchshift-filter.cpp に次の実装を追加します。
1. 定数の追加
constexpr const char *kPropSemitone = "TestPitchShiftSemitone";
constexpr const char *kSettingSemitone = "semitone";
constexpr int kSemitoneMin = -12;
constexpr int kSemitoneMax = 12;
2. フィルタ状態を導入する
OBS のプロパティ UI は、フィルタをインスタンス化した上で get_properties を呼び出します。create を未設定のまま get_properties だけ追加すると、有効なプロパティがありません という表示になることがあります。
#include <atomic>
struct TestPitchShiftFilter final {
obs_source_t *context = nullptr;
std::atomic<int> semitone{0}; // Part 4 で使うので先に入れておく
};
3. create / destroy を追加する
static void *test_pitchshift_create(obs_data_t *settings, obs_source_t *source)
{
auto *f = new TestPitchShiftFilter();
f->context = source;
const int s = (int)obs_data_get_int(settings, kSettingSemitone);
f->semitone.store(s, std::memory_order_relaxed);
return f;
}
static void test_pitchshift_destroy(void *data)
{
delete static_cast<TestPitchShiftFilter *>(data);
}
4. get_defaults / get_properties を追加する
static void test_pitchshift_get_defaults(obs_data_t *settings)
{
obs_data_set_default_int(settings, kSettingSemitone, 0);
}
static obs_properties_t *test_pitchshift_get_properties(void *data)
{
// data は create が返したフィルタ状態
(void)data;
obs_properties_t *props = obs_properties_create();
obs_properties_add_int_slider(
props,
kSettingSemitone,
obs_module_text(kPropSemitone),
kSemitoneMin,
kSemitoneMax,
1);
return props;
}
5. register_test_pitchshift_filter に create/destroy/get_defaults/get_properties を追加する
info.get_name = test_pitchshift_get_name;
+info.create = test_pitchshift_create;
+info.destroy = test_pitchshift_destroy;
+info.get_defaults = test_pitchshift_get_defaults;
+info.get_properties = test_pitchshift_get_properties;
info.filter_audio = test_pitchshift_filter_audio;
6. locale にラベルを追加する
data/locale/en-US.ini
TestPitchShiftSemitone=Semitone
data/locale/ja-JP.ini
TestPitchShiftSemitone=半音
動作確認
- フィルタのプロパティに「半音」スライダーが表示される
- スライダーを動かして音が変わらなくても OK
Part 4: update で値を保持し、音声スレッドから読む土台を作る
TestPitchShiftFilter に update を追加します。
static void test_pitchshift_update(void *data, obs_data_t *settings)
{
auto *f = static_cast<TestPitchShiftFilter *>(data);
const int s = (int)obs_data_get_int(settings, kSettingSemitone);
f->semitone.store(s, std::memory_order_relaxed);
}
register_test_pitchshift_filter に追加します。
info.update = test_pitchshift_update;
動作確認
- ビルドが通る
- スライダー操作ができる
- (任意)
updateにobs_log(LOG_INFO, ...)を一時的に入れて値更新を確認
Part 5: filter_audio の返り値バッファ寿命を満たす
音声フィルタは filter_audio コールバックで音声ブロックを受け取り、加工後の音声を返します。
重要なのは、filter_audio が返す obs_audio_data * の中身が、次の filter_audio 呼び出しまで有効である必要がある点です。つまり、ローカル配列を返すのは危険です。フィルタ状態にバッファを保持し、そこへ書いて返す必要があります。
この Part では「音は変えないが、返すバッファだけ自前にする」ことで、土台を確立します。
1. 出力バッファをフィルタ状態に持つ
#include <vector>
struct TestPitchShiftFilter final {
obs_source_t *context = nullptr;
std::atomic<int> semitone{0};
std::vector<float> outbuf[2];
obs_audio_data out_audio = {};
};
2. create で capacity を確保する
音声スレッド内での動的確保を避けるため、先に確保します。
static void *test_pitchshift_create(obs_data_t *settings, obs_source_t *source)
{
// ...
for (int ch = 0; ch < 2; ch++) {
f->outbuf[ch].reserve(4096);
}
}
3. filter_audio で outbuf にコピーして返す
#include <algorithm>
static obs_audio_data *test_pitchshift_filter_audio(void *data, obs_audio_data *audio)
{
auto *f = static_cast<TestPitchShiftFilter *>(data);
// Part 6 ではここに処理を追加する
// semitone が 0 のときは従来通りバイパスでも良いが、
// ここでは検証のため、あえて outbuf にコピーして返しても良い。
const uint32_t frames = audio->frames;
for (int ch = 0; ch < 2; ch++) {
f->outbuf[ch].resize(frames);
const float *in = reinterpret_cast<const float *>(audio->data[ch]);
float *out = f->outbuf[ch].data();
std::copy_n(in, frames, out);
f->out_audio.data[ch] = reinterpret_cast<uint8_t *>(out);
}
for (size_t ch = 2; ch < MAX_AV_PLANES; ch++) {
f->out_audio.data[ch] = nullptr;
}
f->out_audio.frames = frames;
f->out_audio.timestamp = audio->timestamp;
return &f->out_audio;
}
動作確認
- OBS の 設定 → 音声 → チャンネル を ステレオにして確認
- 配信や録画で音が途切れずに出る
- フィルタを付けても音が変わらない
Part 6: 44.1 kHz 固定と 2ch 前提のガードを入れる
ここで仕様をコードに落とします。
- サンプルレートは 44.1 kHz 固定
- チャンネルは 2ch 固定
- 不一致ならバイパスし、警告ログは 1 回だけ
constexpr uint32_t kExpectedSampleRate = 44100;
struct TestPitchShiftFilter final {
// ...
bool warned_sample_rate = false;
};
static obs_audio_data *test_pitchshift_filter_audio(void *data, obs_audio_data *audio)
{
auto *f = static_cast<TestPitchShiftFilter *>(data);
// ここから追加
const uint32_t sr = audio_output_get_sample_rate(obs_get_audio());
if (sr != kExpectedSampleRate) {
if (!f->warned_sample_rate) {
obs_log(LOG_WARNING,
"[test_pitchshift_filter] sample rate must be %u Hz, but actual is %u Hz. bypassing.",
kExpectedSampleRate, sr);
f->warned_sample_rate = true;
}
return audio;
}
const size_t channels = audio_output_get_channels(obs_get_audio());
if (channels != 2) {
return audio;
}
// ここから先は Part 5 のコピー処理
}
動作確認
- OBS の音声サンプルレートを 44.1 kHz にして動く
- (任意) OBS 側を 48 kHz にして、警告ログが 1 回だけ出てバイパスする
Part 7: 低遅延ピッチシフタを組み込む
ここからが「音を変える」部分です。ただし本記事の目的はアルゴリズムではなく OBS の音声フィルタ土台です。そこで割り切ります。
- 低遅延のため、短いディレイラインとクロスフェードで近似
- 音質劣化やアーティファクトは許容
- 半音スライダーで挙動確認できることを優先
1. DSP を src/low-latency-pitch-shift.* に切り出す
フィルタ登録やスレッド境界の話と、DSP の話を分離します。
-
src/low-latency-pitch-shift.h: クラス宣言 -
src/low-latency-pitch-shift.cpp: 実装
全文は付録にまとめておくので参考にしてください。
2. test-pitchshift-filter.cpp に reset_requested と shifter を追加する
#include "low-latency-pitch-shift.h"
struct TestPitchShiftFilter final {
obs_source_t *context = nullptr;
std::atomic<int> semitone{0};
std::atomic<bool> reset_requested{false};
bool warned_sample_rate = false;
LowLatencyPitchShift shifter[2];
std::vector<float> outbuf[2];
obs_audio_data out_audio = {};
};
update でリセット要求を立てます。
static void test_pitchshift_update(void *data, obs_data_t *settings)
{
auto *f = static_cast<TestPitchShiftFilter *>(data);
const int s = (int)obs_data_get_int(settings, kSettingSemitone);
f->semitone.store(s, std::memory_order_relaxed);
f->reset_requested.store(true, std::memory_order_release);
}
create で Prepare() します。
static void *test_pitchshift_create(obs_data_t *settings, obs_source_t *source)
{
auto *f = new TestPitchShiftFilter();
f->context = source;
const int s = (int)obs_data_get_int(settings, kSettingSemitone);
f->semitone.store(s, std::memory_order_relaxed);
for (auto &sh : f->shifter) {
sh.Prepare();
}
for (int ch = 0; ch < 2; ch++) {
f->outbuf[ch].reserve(4096);
}
return f;
}
3. filter_audio で pitch_factor を作って適用する
#include <cmath>
static obs_audio_data *test_pitchshift_filter_audio(void *data, obs_audio_data *audio)
{
auto *f = static_cast<TestPitchShiftFilter *>(data);
// 無効化時は内部バッファを捨てて即バイパス
if (!obs_source_enabled(f->context)) {
for (auto &sh : f->shifter) {
sh.Reset();
}
return audio;
}
const uint32_t sr = audio_output_get_sample_rate(obs_get_audio());
if (sr != kExpectedSampleRate) {
if (!f->warned_sample_rate) {
obs_log(LOG_WARNING,
"[test_pitchshift_filter] sample rate must be %u Hz, but actual is %u Hz. bypassing.",
kExpectedSampleRate, sr);
f->warned_sample_rate = true;
}
return audio;
}
const size_t channels = audio_output_get_channels(obs_get_audio());
if (channels != 2) {
return audio;
}
const int semitone = f->semitone.load(std::memory_order_relaxed);
if (semitone == 0) {
return audio;
}
if (f->reset_requested.exchange(false, std::memory_order_acq_rel)) {
for (auto &sh : f->shifter) {
sh.Reset();
}
}
const double pitch_factor = std::pow(2.0, (double)semitone / 12.0);
const uint32_t frames = audio->frames;
for (int ch = 0; ch < 2; ch++) {
f->outbuf[ch].resize(frames);
const float *in = reinterpret_cast<const float *>(audio->data[ch]);
float *out = f->outbuf[ch].data();
f->shifter[ch].ProcessBlock(in, out, frames, pitch_factor);
f->out_audio.data[ch] = reinterpret_cast<uint8_t *>(out);
}
for (size_t ch = 2; ch < MAX_AV_PLANES; ch++) {
f->out_audio.data[ch] = nullptr;
}
f->out_audio.frames = frames;
f->out_audio.timestamp = audio->timestamp;
return &f->out_audio;
}
動作確認
- 「半音」を動かすと、声の高さが変わることを確認
- 本記事では音質劣化は許容。レイテンシの低さを優先
遅延の見積り
本実装は「最大ディレイ長」を上限として遅延を見積もれます。
maxDelay = baseDelay + rangemaxDelay = 256 + 128 = 384 samples- 44.1 kHz のとき
384 / 44100 ≒ 8.7 ms
まとめ
OBS の音声フィルタ開発では、アルゴリズムより先に 返り値バッファ寿命とスレッド境界を押さえる必要があります。返す obs_audio_data のメモリは、次の filter_audio 呼び出しまで有効である必要があります。本記事では音質を後回しにし、半音スライダーで動作確認できるところまでを順序立てて解説しました。
本実装は下記リポジトリでも公開しています。参考にしてください。
参考
- /utf-8 (ソースと実行の文字セットを UTF-8に設定) | Microsoft Learn
- ライブ エンコーダの設定、ビットレート、解像度を選択する - YouTube ヘルプ
- Properties API Reference (obs_properties_t) — OBS Studio 32.0.4 documentation
- Source API Reference (obs_source_t) — OBS Studio 32.0.4 documentation
付録: ソース全文
src/low-latency-pitch-shift.h
#pragma once
#include <cstdint>
#include <vector>
class LowLatencyPitchShift final {
public:
void Prepare();
void Reset();
void ProcessBlock(const float *in, float *out, uint32_t frames, double pitch_factor);
private:
float ReadTap(double phase01, double pitch_factor) const;
std::vector<float> buf_;
int write_pos_ = 0;
double phase_ = 0.0;
};
src/low-latency-pitch-shift.cpp
#include "low-latency-pitch-shift.h"
#include <algorithm>
#include <cmath>
namespace {
// レイテンシ優先パラメータ (44.1 kHz 前提での目安は本文参照)
constexpr int kBaseDelaySamples = 256;
constexpr int kRangeSamples = 128;
constexpr int kDelayLineLength = 1024;
inline double clamp_double(double v, double lo, double hi)
{
return (v < lo) ? lo : (v > hi) ? hi : v;
}
inline double frac(double x)
{
return x - std::floor(x);
}
inline float tri(double phase01)
{
// 0..1..0 の三角窓 (クロスフェード用)
const double f = frac(phase01);
const double t = 1.0 - std::fabs(2.0 * f - 1.0);
return (float)clamp_double(t, 0.0, 1.0);
}
} // namespace
void LowLatencyPitchShift::Prepare()
{
buf_.assign(kDelayLineLength, 0.0f);
Reset();
}
void LowLatencyPitchShift::Reset()
{
std::fill(buf_.begin(), buf_.end(), 0.0f);
write_pos_ = 0;
phase_ = 0.0;
}
void LowLatencyPitchShift::ProcessBlock(const float *in, float *out, uint32_t frames, double pitch_factor)
{
// pitch_factor = 2^(semitone/12)
// 変調ディレイ方式の近似で、低遅延を優先する。
const double slope = 1.0 - pitch_factor; // D'[n] (samples / sample)
const double phase_step = std::fabs(slope) / (double)kRangeSamples;
for (uint32_t i = 0; i < frames; i++) {
buf_[write_pos_] = in[i];
write_pos_ = (write_pos_ + 1) % (int)buf_.size();
const double p1 = phase_;
const double p2 = phase_ + 0.5;
const float w1 = tri(p1);
const float w2 = tri(p2);
const float s1 = ReadTap(p1, pitch_factor);
const float s2 = ReadTap(p2, pitch_factor);
out[i] = s1 * w1 + s2 * w2;
phase_ += phase_step;
phase_ = frac(phase_);
}
}
float LowLatencyPitchShift::ReadTap(double phase01, double pitch_factor) const
{
const double f = frac(phase01);
double delay = 0.0;
if (pitch_factor >= 1.0) {
delay = (double)kBaseDelaySamples + (double)kRangeSamples * (1.0 - f);
} else {
delay = (double)kBaseDelaySamples + (double)kRangeSamples * f;
}
const double len = (double)buf_.size();
double pos = (double)write_pos_ - delay;
pos = std::fmod(pos + len, len);
const int i0 = (int)pos;
const int i1 = (i0 + 1) % (int)buf_.size();
const double frac01 = pos - (double)i0;
const float a = buf_[i0];
const float b = buf_[i1];
return (float)((1.0 - frac01) * (double)a + frac01 * (double)b);
}
src/test-pitchshift-filter.cpp
#include <obs-module.h>
#include <plugin-support.h>
#include <atomic>
#include <vector>
#include <algorithm>
#include <cmath>
#include "low-latency-pitch-shift.h"
namespace {
constexpr const char *kFilterId = "test_pitchshift_filter";
constexpr const char *kTextFilterName = "TestPitchShiftFilterName";
constexpr const char *kPropSemitone = "TestPitchShiftSemitone";
constexpr const char *kSettingSemitone = "semitone";
constexpr int kSemitoneMin = -12;
constexpr int kSemitoneMax = 12;
constexpr uint32_t kExpectedSampleRate = 44100;
struct TestPitchShiftFilter final {
obs_source_t *context = nullptr;
std::atomic<int> semitone{0};
std::atomic<bool> reset_requested{false};
bool warned_sample_rate = false;
LowLatencyPitchShift shifter[2];
std::vector<float> outbuf[2];
obs_audio_data out_audio = {};
};
static void *test_pitchshift_create(obs_data_t *settings, obs_source_t *source)
{
auto *f = new TestPitchShiftFilter();
f->context = source;
const int s = (int)obs_data_get_int(settings, kSettingSemitone);
f->semitone.store(s, std::memory_order_relaxed);
for (auto &sh : f->shifter) {
sh.Prepare();
}
for (int ch = 0; ch < 2; ch++) {
f->outbuf[ch].reserve(4096);
}
return f;
}
static void test_pitchshift_update(void *data, obs_data_t *settings)
{
auto *f = static_cast<TestPitchShiftFilter *>(data);
const int s = (int)obs_data_get_int(settings, kSettingSemitone);
f->semitone.store(s, std::memory_order_relaxed);
f->reset_requested.store(true, std::memory_order_release);
}
static void test_pitchshift_destroy(void *data)
{
delete static_cast<TestPitchShiftFilter *>(data);
}
static const char *test_pitchshift_get_name(void *)
{
return obs_module_text(kTextFilterName);
}
static void test_pitchshift_get_defaults(obs_data_t *settings)
{
obs_data_set_default_int(settings, kSettingSemitone, 0);
}
static obs_properties_t *test_pitchshift_get_properties(void *data)
{
// data は create が返したフィルタ状態
(void)data;
obs_properties_t *props = obs_properties_create();
obs_properties_add_int_slider(props, kSettingSemitone, obs_module_text(kPropSemitone), kSemitoneMin,
kSemitoneMax, 1);
return props;
}
static obs_audio_data *test_pitchshift_filter_audio(void *data, obs_audio_data *audio)
{
auto *f = static_cast<TestPitchShiftFilter *>(data);
// 無効化時は内部バッファを捨てて即バイパス
if (!obs_source_enabled(f->context)) {
for (auto &sh : f->shifter) {
sh.Reset();
}
return audio;
}
const uint32_t sr = audio_output_get_sample_rate(obs_get_audio());
if (sr != kExpectedSampleRate) {
if (!f->warned_sample_rate) {
obs_log(LOG_WARNING,
"[test_pitchshift_filter] sample rate must be %u Hz, but actual is %u Hz. bypassing.",
kExpectedSampleRate, sr);
f->warned_sample_rate = true;
}
return audio;
}
const size_t channels = audio_output_get_channels(obs_get_audio());
if (channels != 2) {
return audio;
}
const int semitone = f->semitone.load(std::memory_order_relaxed);
if (semitone == 0) {
return audio;
}
if (f->reset_requested.exchange(false, std::memory_order_acq_rel)) {
for (auto &sh : f->shifter) {
sh.Reset();
}
}
const double pitch_factor = std::pow(2.0, (double)semitone / 12.0);
const uint32_t frames = audio->frames;
for (int ch = 0; ch < 2; ch++) {
f->outbuf[ch].resize(frames);
const float *in = reinterpret_cast<const float *>(audio->data[ch]);
float *out = f->outbuf[ch].data();
f->shifter[ch].ProcessBlock(in, out, frames, pitch_factor);
f->out_audio.data[ch] = reinterpret_cast<uint8_t *>(out);
}
for (size_t ch = 2; ch < MAX_AV_PLANES; ch++) {
f->out_audio.data[ch] = nullptr;
}
f->out_audio.frames = frames;
f->out_audio.timestamp = audio->timestamp;
return &f->out_audio;
}
} // namespace
void register_test_pitchshift_filter(void)
{
obs_source_info info = {};
info.id = kFilterId;
info.type = OBS_SOURCE_TYPE_FILTER;
info.output_flags = OBS_SOURCE_AUDIO;
info.get_name = test_pitchshift_get_name;
info.create = test_pitchshift_create;
info.update = test_pitchshift_update;
info.destroy = test_pitchshift_destroy;
info.get_defaults = test_pitchshift_get_defaults;
info.get_properties = test_pitchshift_get_properties;
info.filter_audio = test_pitchshift_filter_audio;
obs_register_source(&info);
}




