任天堂おしゃべりフラワーのしゃべりを網羅する?

いよいよトライするかね 一応今回はCopilot君にお伺いをたててみようかと…..。

この記事の続編です。WASAPIベースでなくて、WINMM APIベースを使います。今回はCopilot君の専用モードで意見を聞きました。というかほぼ言うがまま。(笑)なんか数日単位で成長しているかもしれません。といっても、いつものように100%エラーフリーではなく、手は入れなければなりませんでしたがね。Copilot君とのやり取りの一部だけ掲載しようと思います。

まず、しばらく前からCopilot君は割り込んでこなくなりました。

windows mic level capture winmmapi vox

というような検索ワードでEdgeからサーチすると、下図のようにCOPILOT君は一歩引いたところに待機しています。

ここで”COPILOT”をクリックすると、

このような画面になります。サインインしているので名前を呼んできます。ここから対話していきます。まずは検索ワードがそのまま入っていますから、上向き矢印でおけですね。ちなみにVOXというのは

これはGemini君の方の検索ですね。自動録音・自動停止よりは理解してもらい易いような気がします。AI君も勉強しているので。まさにこの機能を実現したいのでありました。

というので当然チェック入れます。で、次

褒められちゃいました。つぼにはまった、ないしはめられたということか?ここから色々と細部を詰めていくと、細かいパラメータも決めてと言ってきます。さらに進めると、

じゃあ、一気に全部いくね。
RMS=0.03 / 無音1.5秒 / プリバッファ500ms / WinMM waveIn / vox_001.wav 形式で保存するフル VOX 録音プロジェクトです。
________________________________________
フォルダ構成
ProjectVox/
 ├─ ProjectVox.cbproj
 ├─ ProjectVox.cpp
 ├─ Unit1.h
 ├─ Unit1.cpp
 └─ Unit1.dfm
________________________________________
ProjectVox.cpp
#include <vcl.h>
#pragma hdrstop
#include <tchar.h>

とか言ってくるので、言われる通りにプロジェクトを作ります。どうせならば、C++ Builder CEのプロジェクト一式をzipでダウンロードさせてくれればいいのですが、そうは行きませんでした。そぶりは見せるけど、ちゃんとしたzipは提供されませでした。今回はね。(近々に可能になるような気がする。)

まずフォームは、

TLabel x 1, TButton x 2のみ。

Unit1.hは、

//---------------------------------------------------------------------------

#ifndef Unit1H
#define Unit1H
//---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>

#include <mmsystem.h>
#include <vector>
#include <deque>
#include <atomic>

//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:	// IDE で管理されるコンポーネント
	TButton *Start;
	TLabel *LabelStatus;
	TButton *Stop;
	void __fastcall FormDestroy(TObject *Sender);
	void __fastcall StartClick(TObject *Sender);
	void __fastcall StopClick(TObject *Sender);
private:	// ユーザー宣言
public:		// ユーザー宣言
	__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
// WinMM
extern HWAVEIN hWaveIn;
extern WAVEHDR WaveHdr[2];
extern short Buffer1[4096];
extern short Buffer2[4096];

// VOX
extern std::vector<short> gCurrentPcm;      // 現在の録音バッファ
extern std::deque<short> gPreBuffer;        // プリバッファ(500ms)
extern std::atomic<bool> gRunning;          // waveIn 動作中
extern std::atomic<bool> gVoxRecording;     // VOX 録音中
extern int gSampleRate;                     // サンプリングレート
extern int gSilenceSamplesNeeded;           // 無音判定に必要なサンプル数
extern int gCurrentSilenceSamples;          // 現在の無音継続サンプル数
extern int gFileIndex;                      // vox_XXX.wav の番号

#endif

Unit1.cppは、

//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------

// WinMM
HWAVEIN hWaveIn;
WAVEHDR WaveHdr[2];
short Buffer1[4096];
short Buffer2[4096];

// VOX
std::vector<short> gCurrentPcm;
std::deque<short> gPreBuffer;
std::atomic<bool> gRunning = false;
std::atomic<bool> gVoxRecording = false;
int gSampleRate = 44100;
int gSilenceSamplesNeeded = 0;
int gCurrentSilenceSamples = 0;
int gFileIndex = 1;

// ---------------- RMS 計算 ----------------
double CalcRms(const short* buf, int samples)
{
    if (samples <= 0) return 0.0;

    double sumSq = 0.0;
    for (int i = 0; i < samples; ++i) {
        double v = buf[i] / 32768.0;
        sumSq += v * v;
    }
    double meanSq = sumSq / samples;
    return std::sqrt(meanSq);
}

// ---------------- WAV 保存 ----------------
void SaveWav(const AnsiString& filename, const std::vector<short>& pcm, int sampleRate)
{
    if (pcm.empty()) return;

    FILE* fp = fopen(filename.c_str(), "wb");
    if (!fp) return;

    int dataSize = pcm.size() * sizeof(short);
    int fmtSize = 16;
    short audioFormat = 1;
    short numChannels = 1;
    short bitsPerSample = 16;
    int byteRate = sampleRate * numChannels * bitsPerSample / 8;
    short blockAlign = numChannels * bitsPerSample / 8;

    fwrite("RIFF", 1, 4, fp);
    int fileSize = 36 + dataSize;
    fwrite(&fileSize, 4, 1, fp);
    fwrite("WAVE", 1, 4, fp);

    fwrite("fmt ", 1, 4, fp);
    fwrite(&fmtSize, 4, 1, fp);
    fwrite(&audioFormat, 2, 1, fp);
    fwrite(&numChannels, 2, 1, fp);
    fwrite(&sampleRate, 4, 1, fp);
    fwrite(&byteRate, 4, 1, fp);
    fwrite(&blockAlign, 2, 1, fp);
    fwrite(&bitsPerSample, 2, 1, fp);

    fwrite("data", 1, 4, fp);
    fwrite(&dataSize, 4, 1, fp);
    fwrite(pcm.data(), 1, dataSize, fp);

    fclose(fp);
}

// ---------------- waveIn コールバック ----------------
void CALLBACK waveInProc(
    HWAVEIN hwi, UINT msg, DWORD_PTR instance,
    DWORD_PTR param1, DWORD_PTR param2)
{
    if (msg == WIM_DATA) {
        WAVEHDR* hdr = (WAVEHDR*)param1;
        int samples = hdr->dwBytesRecorded / 2;
        short* p = (short*)hdr->lpData;

        if (gRunning) {
            // プリバッファ更新(常に最新 500ms を保持)
            int preSamplesMax = gSampleRate / 2; // 500ms
            for (int i = 0; i < samples; ++i) {
                gPreBuffer.push_back(p[i]);
                if ((int)gPreBuffer.size() > preSamplesMax) {
                    gPreBuffer.pop_front();
                }
            }

            // RMS 計算
            double rms = CalcRms(p, samples);

            const double VOX_THRESHOLD = 0.03; // しきい値
            if (!gVoxRecording) {
                // まだ録音開始していない → しきい値超えたら開始
                if (rms >= VOX_THRESHOLD) {
                    gVoxRecording = true;
                    gCurrentPcm.clear();

                    // プリバッファを先頭にコピー
                    gCurrentPcm.insert(gCurrentPcm.end(),
                                       gPreBuffer.begin(), gPreBuffer.end());

                    gCurrentSilenceSamples = 0;
                }
            }

            if (gVoxRecording) {
                // 録音中 → 現在バッファを追加
                gCurrentPcm.insert(gCurrentPcm.end(), p, p + samples);

                // 無音判定
                if (rms < VOX_THRESHOLD) {
                    gCurrentSilenceSamples += samples;
                } else {
                    gCurrentSilenceSamples = 0;
                }

                if (gCurrentSilenceSamples >= gSilenceSamplesNeeded) {
                    // 無音が一定時間続いた → 録音終了 & 保存
                    gVoxRecording = false;
                    gCurrentSilenceSamples = 0;

                    String fname = Format(L"vox_%03d.wav", ARRAYOFCONST((gFileIndex)));
                    gFileIndex++;

                    SaveWav(fname, gCurrentPcm, gSampleRate);

                    // UI 更新はメインスレッドで
                    TThread::Queue(nullptr, [fname]() {
						Form1->LabelStatus->Caption =
                            L"Saved: " + fname;
                    });

                    gCurrentPcm.clear();
                }
            }
        }

        waveInAddBuffer(hwi, hdr, sizeof(WAVEHDR));
    }
}

// ---------------- フォーム ----------------
__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
}



// ---------------- Stop ----------------
void __fastcall TForm1::StopClick(TObject *Sender)
{
    gRunning = false;
    gVoxRecording = false;

    waveInStop(hWaveIn);
    waveInReset(hWaveIn);

    waveInUnprepareHeader(hWaveIn, &WaveHdr[0], sizeof(WAVEHDR));
    waveInUnprepareHeader(hWaveIn, &WaveHdr[1], sizeof(WAVEHDR));

    waveInClose(hWaveIn);

    LabelStatus->Caption = L"Stopped";
}

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
	gRunning = false;
	gVoxRecording = false;

}
//---------------------------------------------------------------------------
// ---------------- Start ----------------

//---------------------------------------------------------------------------
void __fastcall TForm1::StartClick(TObject *Sender)
{
	gSampleRate = 44100;
	gSilenceSamplesNeeded = (int)(gSampleRate * 1.5); // 無音 1.5 秒
	gCurrentSilenceSamples = 0;
	gFileIndex = 1;

	gPreBuffer.clear();
	gCurrentPcm.clear();
	gRunning = true;
	gVoxRecording = false;

	WAVEFORMATEX wf = {0};
	wf.wFormatTag = WAVE_FORMAT_PCM;
	wf.nChannels = 1;
	wf.nSamplesPerSec = gSampleRate;
	wf.wBitsPerSample = 16;
	wf.nBlockAlign = wf.nChannels * wf.wBitsPerSample / 8;
	wf.nAvgBytesPerSec = wf.nSamplesPerSec * wf.nBlockAlign;

	MMRESULT res = waveInOpen(&hWaveIn, WAVE_MAPPER, &wf,
							  (DWORD_PTR)waveInProc, 0, CALLBACK_FUNCTION);
	if (res != MMSYSERR_NOERROR) {
		ShowMessage(L"waveInOpen failed");
		return;
	}

	ZeroMemory(&WaveHdr[0], sizeof(WAVEHDR));
	ZeroMemory(&WaveHdr[1], sizeof(WAVEHDR));

	WaveHdr[0].lpData = (LPSTR)Buffer1;
	WaveHdr[0].dwBufferLength = sizeof(Buffer1);
	WaveHdr[1].lpData = (LPSTR)Buffer2;
	WaveHdr[1].dwBufferLength = sizeof(Buffer2);

	waveInPrepareHeader(hWaveIn, &WaveHdr[0], sizeof(WAVEHDR));
	waveInPrepareHeader(hWaveIn, &WaveHdr[1], sizeof(WAVEHDR));

	waveInAddBuffer(hWaveIn, &WaveHdr[0], sizeof(WAVEHDR));
	waveInAddBuffer(hWaveIn, &WaveHdr[1], sizeof(WAVEHDR));

	waveInStart(hWaveIn);

	LabelStatus->Caption = L"VOX running...";

}
//---------------------------------------------------------------------------

です。Copilot君が吐き出した(書いた)コードは1行だけエラーが出ていて、44行目。

void SaveWav(const AnsiString& filename, const std::vector<short>& pcm, int sampleRate)

とすべきところを、Copilot君は

void SaveWav(const String& filename, const std::vector<short>& pcm, int sampleRate)

と受けていて、これは、

[bcc32c エラー] Unit1.cpp(48): no matching function for call to 'fopen'
  stdio.h(274): candidate function not viable: no known conversion from 'System::WideChar *' (aka 'wchar_t *') to 'const char *' for 1st argument

というありがちなエラーになりますから、示したように仮引数filenameの型をString->AnsiStringにすればおけです。というかfopen使うんかい。動作画面は、

のようになり、そのまま”おしゃべりフラワー”君にしゃべらせると、

のように連番でしゃべりが自動的に切り取られています。機会を見てフルモニター(設定は朝7時から夜11時までの16時間)してみようと思います。いくつか再生してみましょう。

1行の修正だけでおおよそ動くプログラムができてしますのは驚異ですね。

コメント