foobar2000で曲を再生する際に、曲タイトル等をSSTPにてSSP等の伺か系ソフトウェアに 送信するプラグインの開発記録です。
foo_sstp_lyricsが公開中止になったため、演奏情報を伺かで表示する手段が無くなってしまった。で、「無いなら作ればいいじゃん」ということで開発開始。 まずはおおざっぱな仕様決め。
プロジェクト名は・・・1分ほど真剣に悩んだあげく、FooToSSTP_PlayinfoSenderとし、MFCを使用したdll(ソケットサポート有り)の設定でソリューション作成。 foobarSDKの指示通り、以下の3つのプロジェクトをSDKからコピーし、ソリューションに追加。
よし、成功。というわけで、次のステップに進みます。4>ビルドに成功しました。 4> 4>経過時間 00:00:07.69 ========== すべてビルド: 4 正常終了、0 失敗、0 スキップ ==========
foobar2000にプラグインとして認識してもらわないと、SSTPのテストとかもままならないので、まずはここから着手。 SDK曰く、SDKディレクトリ内のヘッダファイルはfoobar2000.h以外#includeするな!とのことなので、仰せのとおりにする。
さて、SDKのドキュメントをサクサク読んでいく。今回のソフトに関係しそうな、コンポーネントの構造(Structure of a component)以下の項目をまとめると・・。
ちょっとしょんぼりしながら使用するインターフェースについて調べる。staticなオブジェクトとする場合はplay_callback_staticを継承し、play_callback_static_factory_tのstaticオブジェクトで生成するようだ。
これに対し、動的に生成するオブジェクトとする場合はplay_callback_impl_baseを継承して実装すれば良いようだ。
今回はどちらにしよう。・・・動的生成にしてみようか。
というわけでさくさく実装。
Fb2kToSstpPlayInfo.h #pragma once #include "foobar2000.h" class CFb2kToSstpPlayInfo : public play_callback_impl_base { public: CFb2kToSstpPlayInfo(void); ~CFb2kToSstpPlayInfo(void); };
Fb2kToSstpPlayInfo.cpp #include "StdAfx.h" #include "Fb2kToSstpPlayInfo.h" CFb2kToSstpPlayInfo::CFb2kToSstpPlayInfo(void) { } CFb2kToSstpPlayInfo::~CFb2kToSstpPlayInfo(void) { }
さてさてコンパイル。FooToSSTP_PlayinfoSender10.cpp DECLARE_COMPONENT_VERSION("foo_ToSSTP_PlayinfoSender","1.0","about message goes here"); VALIDATE_COMPONENT_FILENAME("foo_ToSSTP_PlayinfoSender10.dll");
がーん。でもよくみるとpfc等先ほど追加したプロジェクトの名前が見える。ということは、生成したlibがうまくインポートできていないのか・・・。ということで、以下を実施。Link: 1> ライブラリ Visual Studio 2010\Projects\FooToSSTP_PlayinfoSender10\Debug\FooToSSTP_PlayinfoSender10.lib とオブジェクト Visual Studio 2010\Projects\FooToSSTP_PlayinfoSender10\Debug\FooToSSTP_PlayinfoSender10.exp を作成中 1>Fb2kToSstpPlayInfo.obj : error LNK2019: 未解決の外部シンボル "void __cdecl pfc::myassert(wchar_t const *,wchar_t const *,unsigned int)" (?myassert@pfc@@YAXPB_W0I@Z) が関数 __catch$??$service_release_safe@Vmetadb_handle@@@@YAXPAVmetadb_handle@@@Z$0 で参照されました。 1>stdafx.obj : error LNK2001: 外部シンボル ""void __cdecl pfc::myassert(wchar_t const *,wchar_t const *,unsigned int)" (?myassert@pfc@@YAXPB_W0I@Z)" は未解決です。 1>Fb2kToSstpPlayInfo.obj : error LNK2019: 未解決の外部シンボル "void __cdecl _standard_api_create_internal(class service_ptr_t&,struct _GUID const &)" (?_standard_api_create_internal@@YAXAAV?$service_ptr_t@Vservice_base@@@@ABU_GUID@@@Z) が関数 "void __cdecl standard_api_create_t (class service_ptr_t &)" (??$standard_api_create_t@Vplay_callback_manager@@@@YAXAAV?$service_ptr_t@Vplay_callback_manager@@@@@Z) で参照されました。 1>Fb2kToSstpPlayInfo.obj : error LNK2001: 外部シンボル ""public: static struct _GUID const play_callback_manager::class_guid" (?class_guid@play_callback_manager@@2U_GUID@@B)" は未解決です。 1>Visual Studio 2010\Projects\FooToSSTP_PlayinfoSender10\Debug\FooToSSTP_PlayinfoSender10.dll : fatal error LNK1120: 外部参照 3 が未解決です。 1> 1>ビルドに失敗しました。
よっしゃ通った!さっそくdllをcomponentsフォルダにコピーして・・・って認識されない・・・。1>ビルドに成功しました。 1> 1>経過時間 00:00:02.08 ========== ビルド: 1 正常終了、0 失敗、3 更新不要、0 スキップ ==========
FooToSSTP_PlayinfoSender10.cpp CFooToSSTP_PlayinfoSender10App::CFooToSSTP_PlayinfoSender10App() { m_pPlayInfo=new CFb2kToSstpPlayInfo; } CFooToSSTP_PlayinfoSender10App::~CFooToSSTP_PlayinfoSender10App() { delete m_pPlayInfo; }
ぎゃあああああああああorz。現象を追っていくと、どうもfoobar本体に対するポインタを貰う前に自作オブジェクトが生成されてしまい、エラーを起こしているようだ。ということで、play_callback_staticを継承するようにコード変更。foobar2000.exe によってブレークポイントが発生しました
Fb2kToSstpPlayInfo.h #pragma once class CFb2kToSstpPlayInfo : public play_callback_static { public: CFb2kToSstpPlayInfo(void); ~CFb2kToSstpPlayInfo(void); void on_playback_starting(play_control::t_track_command p_command,bool p_paused); void on_playback_new_track(metadb_handle_ptr p_track); void on_playback_stop(play_control::t_stop_reason p_reason); void on_playback_seek(double p_time); void on_playback_pause(bool p_state); void on_playback_edited(metadb_handle_ptr p_track); void on_playback_dynamic_info(const file_info & p_info); void on_playback_dynamic_info_track(const file_info & p_info); void on_playback_time(double p_time); void on_volume_change(float p_new_val); unsigned int get_flags(void); private: unsigned int m_CallFlag; };
これはさくっと動作。ブレークポイントを使って、再生開始時にon_playback_new_trackが呼び出されていることも確認。 となると、次は演奏情報のゲットかな。Fb2kToSstpPlayInfo.cpp #include "StdAfx.h" #include "Fb2kToSstpPlayInfo.h" CFb2kToSstpPlayInfo::CFb2kToSstpPlayInfo(void) { m_CallFlag=flag_on_playback_new_track; } CFb2kToSstpPlayInfo::~CFb2kToSstpPlayInfo(void) { } void CFb2kToSstpPlayInfo::on_playback_starting(play_control::t_track_command p_command,bool p_paused) { } void CFb2kToSstpPlayInfo::on_playback_new_track(metadb_handle_ptr p_track) { } void CFb2kToSstpPlayInfo::on_playback_stop(play_control::t_stop_reason p_reason) { } void CFb2kToSstpPlayInfo::on_playback_seek(double p_time) { } void CFb2kToSstpPlayInfo::on_playback_pause(bool p_state) { } void CFb2kToSstpPlayInfo::on_playback_edited(metadb_handle_ptr p_track) { } void CFb2kToSstpPlayInfo::on_playback_dynamic_info(const file_info & p_info) { } void CFb2kToSstpPlayInfo::on_playback_dynamic_info_track(const file_info & p_info) { } void CFb2kToSstpPlayInfo::on_playback_time(double p_time) { } void CFb2kToSstpPlayInfo::on_volume_change(float p_new_val) { } unsigned int CFb2kToSstpPlayInfo::get_flags(void) { return m_CallFlag; }
VisualStudioの出力ウィンドウに出すようにしてみる。
・・・あれ。たしかfoobar2000.hにFb2kToSstpPlayInfo.cpp void CFb2kToSstpPlayInfo::on_playback_new_track(metadb_handle_ptr p_track) { try{ file_info_impl info; CString buff; p_track->get_info(info); if(info.meta_exists("TITLE")){ buff.Format(_T("Now playing:%S\n"),info.meta_get("TITLE",0)); OutputDebugString(buff); } } catch(std::exception ex) { OutputDebugString(reinterpret_cast
(ex.what())); } }
って書いてあったのに、なんでmeta_existsとかの引数はchar*なんだ?まあいいや、コンパイル通ったし。実行!foobar2000.h #ifndef UNICODE #error Only UNICODE environment supported. #endif
なぜに化けるぅぅぅ!!!!Now playing: a|?a ae a2≫
これならどうだ!?Fb2kToSstpPlayInfo.cpp void CFb2kToSstpPlayInfo::on_playback_new_track(metadb_handle_ptr p_track) { try{ file_info_impl info; t_size index; t_size u16buffsize; wchar_t *u16buff=NULL; CString buff; p_track->get_info(info); if((index=info.meta_find("TITLE"))!=pfc_infinite){ u16buffsize=MultiByteToWideChar(CP_UTF8, 0, info.meta_enum_value(index, 0), -1, NULL, 0); if(u16buffsize!=0){ u16buff=new wchar_t[u16buffsize]; if(MultiByteToWideChar(CP_UTF8, 0, info.meta_enum_value(index, 0), -1, u16buff, u16buffsize)){ buff.Format(_T("Now playing:%s\n"), u16buff); OutputDebugString(buff); } } delete[] u16buff; } } catch(std::exception ex) { OutputDebugString(reinterpret_cast
(ex.what())); } }
おっしゃあ!できた!MFCなコード(CStringね)も動いてる!Now playing: 始まり
Fb2kToSstpPlayInfo.cpp // file_infoからmetaデータを抽出し、CString型に保存する bool CFb2kToSstpPlayInfo::GetMetaInfo(const file_info &info, const char* p_MetaName, t_size p_value_number, CString *pResult) { wchar_t *u16buff=NULL; try{ t_size index; t_size u16buffsize; if((index=info.meta_find(p_MetaName))!=pfc_infinite){ // バッファサイズ取得 u16buffsize=MultiByteToWideChar(CP_UTF8, 0, info.meta_enum_value(index, p_value_number), -1, NULL, 0); if(u16buffsize!=0){ u16buff=new wchar_t[u16buffsize]; // 変換実行! if(MultiByteToWideChar(CP_UTF8, 0, info.meta_enum_value(index, p_value_number), -1, u16buff, u16buffsize)){ pResult->Format(_T("%s"), u16buff); delete[] u16buff; return true; } delete[] u16buff; } } } catch(std::exception ex) { delete[] u16buff; OutputDebugString(reinterpret_cast
(ex.what())); } return false; } void CFb2kToSstpPlayInfo::on_playback_new_track(metadb_handle_ptr p_track) { try{ file_info_impl info; CString buff; CString output; p_track->get_info(info); GetMetaInfo(info, "TITLE", 0, &buff); output.Format(_T("Now playing:%s\n"), buff); OutputDebugString(output); } catch(std::exception ex) { OutputDebugString(reinterpret_cast (ex.what())); } }
SSTPサーバーとの通信はすぐに終わる保証が無いため、別スレッドで行うことにする。
CAsyncSocketクラスで手抜きしつつ、テキストデータをSSTPに従い送受信。
テキストのフォーマットは・・・・え、UTF-8?
string_base型でデータ保持するように書き換えて、UTF-8で一貫して保持するようにするか。
さらばUTF16。君はデバッグ時だけ使うことにするよ。
細かいことはさておき、こんなんなった。ほぼ一発動作。
Fb2kToSstpPlayInfo.cpp
最低限の機能はできたけど、できればフォーマットとかを弄りたい!ということで設定ダイアログを追加します。まずはfoo_sampleをベースに。
ダイアログ部分はWTL(Windows Template Library)が必須の模様。残念、使い慣れたMFCはだめぽ。WTLはSourceForgeから入手できる。
stdafx.hをincludeしているとCStringが名前の衝突起こすようなので、ダイアログのソースからはstdafxを外し、そこだけWTLにしてみる。
よし、コンパイルは通った。あとはリンクか。そういえば、ヘルパーのプロジェクトあったから、それをソリューションに追加して、出力先やら追加ライブラリやらをいじれば大丈夫だろう。1> Pref.cpp 1> TmSchema.h is obsolete. Please include vssym32.h instead. 1>Link: 1> ライブラリ visual studio 2010\Projects\FooToSSTP_PlayinfoSender10\Debug\foo_ToSSTP_PlayinfoSender10.lib とオブジェクト visual studio 2010\Projects\FooToSSTP_PlayinfoSender10\Debug\foo_ToSSTP_PlayinfoSender10.exp を作成中 1>Pref.obj : error LNK2019: 未解決の外部シンボル "void __cdecl WIN32_OP_FAIL(void)" (?WIN32_OP_FAIL@@YAXXZ) が関数 "public: __thiscall preferences_page_instance_impl::preferences_page_instance_impl (struct HWND__ *,class service_ptr_t )" (??0?$preferences_page_instance_impl@VCPref@@@@QAE@PAUHWND__@@V?$service_ptr_t@Vpreferences_page_callback@@@@@Z) で参照されました。 1>visual studio 2010\Projects\FooToSSTP_PlayinfoSender10\Debug\foo_ToSSTP_PlayinfoSender10.dll : fatal error LNK1120: 外部参照 1 が未解決です。 1> 1>ビルドに失敗しました。
見た目改造のポイントは以下のとおり。
// These GUIDs identify the variables within our component's configuration file. // {7EAEF188-437C-4C2A-889D-88DD9CCC3413} static const GUID guid_cfg_Port = { 0x7eaef188, 0x437c, 0x4c2a, { 0x88, 0x9d, 0x88, 0xdd, 0x9c, 0xcc, 0x34, 0x13 } }; const unsigned int default_cfg_Port=9821; static cfg_uint cfg_Port(guid_cfg_Port, default_cfg_Port);
さっそくいくつか変数を作り実行したが、文字列が化けて表示される・・・orz
どうやら、SetDlgItemTextがUTF-16かASCIIしか受け付けないためのようだ。ということでUTF-8からUTF-16に変換し、無事動作。
変換関数、作っててよかった!
Pref.h
Pref.cpp
とか思ったら、なんとpfc::stringcvtに、string_utf8_from_wide_tとか、string_wide_from_utf8とかあるじゃないですか。そっち利用する方向で書き換え。
titleformatを解釈させる方法がなかなか分からなかったが、結局以下のようになった。
ここまでで、設定画面は完成。void CPref::OnChanged() { const int buffsize=1024; wchar_t buff[buffsize]; static_api_ptr_t
pbc; pfc::string8 str8res; // アクティブなコントロールが自動フォーマット処理対象なら、表示を更新 int id=::GetDlgCtrlID(GetFocus()); if(id==IDC_DEFAULTMESSAGE || id==IDC_REF0 || id==IDC_REF1 || id==IDC_REF2 || id==IDC_REF3 || id==IDC_REF4){ GetDlgItemText(id, buff, buffsize); pfc::stringcvt::string_utf8_from_wide_t <> str8buff(buff, buffsize); titleformat_object_wrapper tow(str8buff); pbc->playback_format_title(NULL, str8res, tow, NULL, playback_control::display_level_all); pfc::stringcvt::string_wide_from_utf8 wcharres(str8res); SetDlgItemText(IDC_SAMPLE, wcharres); } //tell the host that our state has changed to enable/disable the apply button appropriately. m_callback->on_state_changed(); }
コンパイルもさくっと通り、無事動作。Fb2kToSstpPlayInfo.cpp void CFb2kToSstpPlayInfo::on_playback_new_track(metadb_handle_ptr p_track) { try{ pfc::string8 str8res; static_api_ptr_t
pbc; service_ptr_t script; static_api_ptr_t ()->compile_force(script,cfg_DefaultMsg); pbc->playback_format_title(NULL, m_PlayInfo.defaultdata, script, NULL, playback_control::display_level_all); static_api_ptr_t ()->compile_force(script,cfg_Ref0); pbc->playback_format_title(NULL, m_PlayInfo.ref0, script, NULL, playback_control::display_level_all); static_api_ptr_t ()->compile_force(script,cfg_Ref1); pbc->playback_format_title(NULL, m_PlayInfo.ref1, script, NULL, playback_control::display_level_all); static_api_ptr_t ()->compile_force(script,cfg_Ref2); pbc->playback_format_title(NULL, m_PlayInfo.ref2, script, NULL, playback_control::display_level_all); static_api_ptr_t ()->compile_force(script,cfg_Ref3); pbc->playback_format_title(NULL, m_PlayInfo.ref3, script, NULL, playback_control::display_level_all); static_api_ptr_t ()->compile_force(script,cfg_Ref4); pbc->playback_format_title(NULL, m_PlayInfo.ref4, script, NULL, playback_control::display_level_all); m_Port=cfg_Port; m_targetaddr=pfc::stringcvt::string_wide_from_utf8(cfg_TargetAddr); PostSSTP(); } catch(std::exception ex) { OutputDebugString(ex.what()); } } // SSTPサーバーにメッセージを投げる void CFb2kToSstpPlayInfo::PostSSTP() { CreateThread(NULL, 0, ThrowAwayThread, this, 0, NULL); } // SSTPサーバーとの通信スレッド DWORD WINAPI CFb2kToSstpPlayInfo::ThrowAwayThread(LPVOID lpParameter) { DWORD er; try{ CFb2kToSstpPlayInfo* pObj; pObj=static_cast (lpParameter); SPlayInfo data=pObj->m_PlayInfo; pfc::string8 sendbuff, recvbuff; char buff[SSTP_MAXSIZE]; // 送信準備 CAsyncSocket socket; AfxSocketInit(NULL); if(socket.Create(0, SOCK_STREAM, 0, NULL)==0){ er=GetLastError(); throw std::exception("Failed to create socket\n"); } DWORD arg=0; if(socket.IOCtl(FIONBIO, &arg)==0){ er=GetLastError(); throw std::exception("Failed to change blocking mode\n"); } if(socket.Connect(pObj->m_targetaddr, pObj->m_Port)==0){ er=GetLastError(); throw std::exception("Failed to connect to SSTP host\n"); } // SSTPヘッダ生成 sendbuff ="NOTIFY SSTP/1.1\r\n"; sendbuff+="Sender: foobar2000ToSSTP\r\n"; sendbuff+="Event: OnMusicPlay\r\n"; sendbuff+="Charset: UTF-8\r\n"; sendbuff+="Reference0: "; sendbuff+= data.ref0; sendbuff+= "\r\n"; sendbuff+="Reference1: "; sendbuff+= data.ref1; sendbuff+= "\r\n"; sendbuff+="Reference2: "; sendbuff+= data.ref2; sendbuff+= "\r\n"; sendbuff+="Reference3: "; sendbuff+= data.ref3; sendbuff+= "\r\n"; sendbuff+="Reference4: "; sendbuff+= data.ref4; sendbuff+= "\r\n"; sendbuff+="Script: "; sendbuff+= data.defaultdata; sendbuff+= "\r\n"; sendbuff+="ScriptOption: nobreak\r\n"; sendbuff+="\r\n"; if(sendbuff.get_length()>SSTP_MAXSIZE) throw std::exception("SSTP header too large\n"); // 送信&受信 OutputDebugString(sendbuff); if(socket.Send(sendbuff.get_ptr(), sendbuff.get_length())==SOCKET_ERROR){ er=GetLastError(); throw std::exception("Failed to send data to SSTP host\n"); } if(socket.Receive(reinterpret_cast (buff), SSTP_MAXSIZE)<=0){ // 受信はするけどなにもしない er=GetLastError(); throw std::exception("Failed to recv data from SSTP host\n"); } recvbuff=buff; OutputDebugString(recvbuff.get_ptr()); socket.Close(); } catch(std::exception ex){ // 全てのstd::exceptionを受け流す LPVOID lpMsgBuf; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS, NULL, er, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0, NULL); OutputDebugString(ex.what()); OutputDebugString((LPCTSTR)lpMsgBuf); LocalFree(lpMsgBuf); return 0; } return 1; }
一部SSTPサーバーはDirectSSTPにしか対応していないので、DirectSSTPも実装することに。たしか、FMO(FileMapping Object)取得して、なんか するんだったよな・・・。調査開始。
いよいよDirectSSTPの実装。とりあえず、ゴーストもSSTPサーバーも一つずつしかないと仮定しよう。
全体の流れとしては・・・。
というわけで、コードの流れとしては・・・
あれ?mutexが成功しない・・・(そもそもロックしないコードだけど)。ちゃんとFMO名を"sakura"にしたんだけどなあ。Fb2kToSstpPlayInfo.cpp // SSTPサーバーとのDirectSSTP通信スレッド DWORD WINAPI CFb2kToSstpPlayInfo::ThrowAwayDirectSSTP(LPVOID lpParameter) { HANDLE hMutex, hFMO; CFb2kToSstpPlayInfo* pObj; pObj=static_cast
(lpParameter); SPlayInfo data=pObj->m_PlayInfo; pfc::stringcvt::string_wide_from_utf8 fmo(cfg_FMOName); LPCWSTR p=fmo; // Check if SSTP server has created FMO hMutex=OpenMutexA(0, FALSE, cfg_FMOName); if(hMutex==NULL) return 1; CloseHandle(hMutex); // Open FMO hFMO=OpenFileMapping(FILE_MAP_READ, FALSE, p); if(hFMO==NULL) return 1; // Get FMO size char *pFMO=static_cast (MapViewOfFile(hFMO, FILE_MAP_READ, 0, 0, 4)); if(pFMO==NULL){ CloseHandle(hFMO); return 1; } DWORD fmoSize=*reinterpret_cast (pFMO); CloseHandle(hFMO); return 1; }
あとはポイっとメッセージを投げるだけなんだけど、これどこかのhWndにresponseが帰ってくるんだよね。Fb2kToSstpPlayInfo.cpp // Get FMO size char *pFMO=static_cast
(MapViewOfFile(hFMO, FILE_MAP_READ, 0, 0, 4)); if(pFMO==NULL){ if(hMutex){ ReleaseMutex(hMutex); CloseHandle(hMutex); } CloseHandle(hFMO); return 0; } DWORD fmoSize=*reinterpret_cast (pFMO); // Read FMO pFMO=static_cast (MapViewOfFile(hFMO, FILE_MAP_READ, 0, 0, fmoSize)); unsigned int ptrCount=4; pfc::string8 buff, entry, field, item; int entryEnd, fieldEnd, itemEnd; HWND targetWnd; // search hWnd targetWnd=0; while(ptrCount (atoi(item)); break; } ptrCount+=itemEnd+2; } ReleaseMutex(hMutex); CloseHandle(hMutex); CloseHandle(hFMO);
以下、実行画面や結果。if(targetWnd){ CDummyDlg dlg; // ただのSSTP通信の受け手。 CFb2kToSstpPlayInfo* pObj; pfc::string8 sendbuff; pObj=static_cast
(lpParameter); dlg.Create(IDD_DUMMY); SPlayInfo data=pObj->m_PlayInfo; data.hWnd=pfc::format_int::format_int(t_int64(reinterpret_cast (dlg.m_hWnd))); sendbuff=CreateSSTPHeader(data); COPYDATASTRUCT cds; cds.cbData=sendbuff.length(); cds.lpData=static_cast (const_cast (sendbuff.get_ptr())); cds.dwData=pObj->m_Port; SendMessage(targetWnd, WM_COPYDATA, NULL, (LPARAM)(&cds)); int i; for(i=0;i<100 && !dlg.m_bRecv;i++) Sleep(10); dlg.DestroyWindow(); }
NOTIFY SSTP/1.1 Sender: foobar2000ToSSTP Event: OnMusicPlay Charset: UTF-8 HWnd: 853952 Reference0: 南の島のハメハメハ大王 Reference1: ? Reference2: NHKみんなのうた ベスト50 Reference3: 14 Reference4: 2:28 Script: NHKみんなのうた ベスト50の南の島のハメハメハ大王を演奏中だよ。 ScriptOption: nobreak SSTP/1.1 200 OK スレッド 'Win32 スレッド' (0x85c) はコード 1 (0x1) で終了しました。