MCI による音楽 CD の制御
Bw-soft CD プレイヤーを作った際に調べた事をまとめました。
MP3 や ATRAC などの圧縮音楽や音楽 CD すらデジタル再生をさせてしまうのが最近の主流なので、
今更 CD プレイヤーを作る人も少ないかもしれませんが
MIDI や Wave の再生、ビデオ再生にも応用が利くので試してみてはどうでしょうか。

♪はじめに
ほとんどのパソコンに装備されている CD ドライブ。これは CD-ROM の読み出しはもちろん、音楽 CD の再生にも使えます。
ここではこの CD ドライブで音楽 CD を鳴らす方法を書いてみます。

音楽 CD を鳴らす方法はいくつかありますがここでは MCI コマンドメッセージを使った方法を紹介します。
MCI とは Media Control Interface の略で音楽や動画を再生するための命令群ですが、Windows3.1 でサポートされていたくらいなのでかなり枯れた技術です。でも音楽 CD 制御くらいにはこれで十分な気もします(不満もいっぱいあるけど)。
MCI を使うので以下の関数・メッセージを使うには mmsystem.h をインクルードし winmm.lib をリンクする事をお忘れなく。

♪実際にやってみよう
音楽 CD の制御はいたって簡単です。
MCI で始まるコマンドメッセージを mciSendCommand 関数を使ってシステムへ送ってやるだけで制御ができます。

以下に mciSendCommand 関数の説明と主な MCI コマンド(全部じゃないよ)の解説を書いてみました。簡単な機能の CD プレイヤーならこれだけで十分だと思います。

mciSendCommand
《コマンドメッセージ送信関数》
DWORD mciSendCommand(
UINTwDeviceID,// デバイス ID
UINTwMessage,// コマンドメッセージ
DWORDdwFlags,// 動作フラグ
DWORDdwParam// 拡張情報のためのポインタ
);

MCI デバイスを制御するすべてのコマンドはこの関数でやり取りを行ないます。

wDeviceID にはデバイス ID を設定します。デバイス ID は、どのドライブで再生するかを識別する番号で、MCI_OPEN メッセージを送る事によりデバイス ID を取得する事ができます。
wMessage には送信したいコマンドメッセージを書きます。
dwFlags と dwParam はコマンドメッセージの動作と拡張情報を設定します。内容は各々のコマンドメッセージにより内容が異なります。

これ以降が実際のコマンドの使い方です。
但し、すべてのコマンドや動作フラグの説明を書いていないので詳しくはヘルプファイル・ヘッダーファイル( mmsystem.h )を参照してください。特にヘッダーファイルには MCI 関連のすべてのメッセージやパラメータが宣言されていますので一度見ておくといろいろと使えそうなコマンドが見つかるかもしれません。

MCI_OPEN
《デバイスの初期化》
説明音楽 CD を制御するためにはまず初期化をする必要があります。
初期化も mciSendCommand 関数を使って行ないますが、mciSendCommand 関数の第 1 パラメータのデバイス ID は、この時点では未定なので NULL を設定します。

MCI_OPEN_PARMS 構造体に設定される wDeviceID は今後のコマンドで必ず必要になるので変数に記憶させておきましょう。

dwFlagsMCI_NOTIFY命令実行後、MM_MCINOTIFY メッセージを発生させる
MCI_WAIT命令完了まで待機する
MCI_OPEN_TYPElpstrDeviceType にデバイスタイプが書かれている事を示す
MCI_OPEN_SHAREABLEデバイスを共有する事を示す
dwParamMCI_OPEN_PARMS 構造体へのポインタ
サンプル MCI_OPEN_PARMS mop;
mop.lpstrDeviceType = "cdaudio";

mciSendCommand(NULL, MCI_OPEN, MCI_OPEN_TYPE | MCI_OPEN_SHAREABLE, (DWORD)(LPVOID)&mop);

wDeviceID = mop.wDeviceID;

MCI_OPEN_PARMS 構造体
typedef struct {
DWORDdwCallback;// MM_NOTIFY を送るウィンドウのハンドル
MCIDEVICEIDwDeviceID;// ここにデバイス ID が返される
LPCSTRlpstrDeviceType;// デバイス名(音楽 CD なら"cdaudio")
LPCSTRlpstrElementName;//
LPCSTRlpstrAlias;//
} MCI_OPEN_PARMS;

MCI_CLOSE
《デバイスの終了》
説明デバイスを使用し終わったら解放する必要があります。
これを忘れると……どうなるんでしょう?
dwFlagsMCI_NOTIFY命令実行後、MM_MCINOTIFY メッセージを発生させる
MCI_WAIT命令完了まで待機する
dwParamMCI_GENERIC_PARAMS 構造体へのポインタ
サンプル mciSendCommand(wDeviceID, MCI_CLOSE, 0, NULL);
MCI_GENERIC_PARAMS 構造体
typedef struct {
DWORDdwCallback;// MM_NOTIFY を送るウィンドウのハンドル
} MCI_GENERIC_PARAMS;

MCI_SET
《デバイス状態の設定》
説明再生開始位置など位置を指示する場合に、その位置情報の形式を設定します。
そのほかドライブトレイの開閉もこのコマンドで行います。
dwFlagsMCI_NOTIFY命令実行後、MM_MCINOTIFY メッセージを発生させる
MCI_WAIT命令完了まで待機する
MCI_SET_DOOR_CLOSED CD トレイを閉じる
MCI_SET_DOOR_OPEN CD トレイを開ける
MCI_SET_TIME_FORMATdwTimeFormat に時刻形式が設定してあることを示す
dwParamMCI_SET_PARMS 構造体へのポインタ
サンプル // CD トレイを閉じる
mciSendCommand(wDeviceID, MCI_SET, MCI_SET_DOOR_CLOSED, NULL);

// CD トレイを開ける
mciSendCommand(wDeviceID, MCI_SET, MCI_SET_DOOR_OPEN, NULL);

// 時刻形式指定のサンプルは MCI_PLAY を参照

MCI_SET_PARMS 構造体
typedef struct {
DWORDdwCallback;// MM_NOTIFY を送るウィンドウのハンドル
DWORDdwTimeFormat;// 時刻形式を設定する
DWORDdwAudio;//
} MCI_SET_PARMS;

dwTimeFormat の設定値
MCI_FORMAT_MILLISECONDSミリ秒
MCI_FORMAT_MSF分/秒/フレーム
MCI_FORMAT_TMSFトラック/分/秒/フレーム

MCI_PLAY
《演奏の開始》
説明音楽 CD を演奏するのがこのコマンドです。
演奏開始位置と終了位置を MCI_PLAY_PARMS 構造体で指定します。
MCI_FROM を省略すると現在位置からの再生、MCI_TO を省略すると CD の最後までを演奏します。
演奏位置の時刻形式はあらかじめ MCI_SET_PARMS 構造体で指定しておいてください。
dwFlagsMCI_NOTIFY命令実行後、MM_MCINOTIFY メッセージを発生させる
MCI_WAIT命令完了まで待機する
MCI_FROMdwFrom の位置から再生をする (省略時は現在位置から再生)
MCI_TOdwTo まで再生する (省略時は CD の最後まで)
dwParamMCI_PLAY_PARMS 構造体へのポインタ
サンプル MCI_SET_PARMS set;
MCI_PLAY_PARMS play;

// 時間形式を TMFS (トラック/分/秒/フレーム)の形式にする
set.dwTimeFormat = MCI_FORMAT_TMSF;
mciSendCommand(wDeviceID, MCI_SET, MCI_SET_TIME_FORMAT, (DWORD)(LPVOID)&set);

// 5 トラック目から最後までを再生し
// 再生を終えたら MM_MCINOTIFY メッセージを送る

play.dwCallback = (DWORD)hWnd;
play.dwFrom = MCI_MAKE_TMSF(5, 0, 0, 0);
mciSendCommand(wDeviceID, MCI_PLAY, MCI_FROM | MCI_NOTIFY, (DWORD)(LPVOID)&play);

MCI_PLAY_PARMS 構造体
typedef struct {
DWORDdwCallback;// MM_NOTIFY を送るウィンドウのハンドル
DWORDdwFrom;// 演奏開始位置を指定
DWORDdwTo;// 演奏終了位置を指定
} MCI_PLAY_PARMS;

MCI_STOP
《演奏の停止》
説明演奏を停止します。
dwFlagsMCI_NOTIFY命令実行後、MM_MCINOTIFY メッセージを発生させる
MCI_WAIT命令完了まで待機する
dwParamMCI_GENERIC_PARAMS 構造体へのポインタ (MCI_CLOSE 参照)
サンプル mciSendCommand(wDeviceID, MCI_STOP, MCI_WAIT, NULL);

MCI_PAUSE
《演奏の一時停止》
説明演奏を一時停止します。
MCI_PLAY を送れば演奏を再開します。
dwFlagsMCI_NOTIFY命令実行後、MM_MCINOTIFY メッセージを発生させる
MCI_WAIT命令完了まで待機する
dwParamMCI_GENERIC_PARAMS 構造体へのポインタ (MCI_CLOSE 参照)
サンプル mciSendCommand(wDeviceID, MCI_PAUSE, 0, NULL);

MCI_SEEK
《演奏開始位置の移動》
説明 CD の読み込み位置を移動させます。
次に MCI_PLAY を送った場合、ここで指定した位置から演奏が開始されます(MCI_FROM 省略時)。
dwFlagsMCI_NOTIFY命令実行後、MM_MCINOTIFY メッセージを発生させる
MCI_WAIT命令完了まで待機する
MCI_SEEK_TO_END音楽 CD の最後へ移動
MCI_SEEK_TO_START音楽 CD の先頭へ移動
MCI_TO移動先を dwToで指定する
dwParamMCI_SEEK_PARMS 構造体へのポインタ
サンプル // 先頭に移動させる
mciSendCommand(wDeviceID, MCI_SEEK, MCI_SEEK_TO_START | MCI_WAIT, NULL);
MCI_SEEK_PARMS 構造体
typedef struct {
DWORDdwCallback;// MM_NOTIFY を送るウィンドウのハンドル
DWORDdwTo;// 移動先を指定
} MCI_SEEK_PARMS;

MCI_STATUS
《各種情報の取得》
説明音楽 CD の全トラック数、トラック長、再生中の時間などなど各種情報を得ます。
dwFlagsMCI_NOTIFY命令実行後、MM_MCINOTIFY メッセージを発生させる
MCI_WAIT命令完了まで待機する
MCI_STATUS_ITEMdwItem に取得したい情報を指定していることを示す
dwParamMCI_STATUS_PARMS 構造体へのポインタ
サンプル // 総トラック数を得る
MCI_STATUS_PARMS status;

status.dwItem = MCI_STATUS_NUMBER_OF_TRACKS;
mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM, (DWORD)(LPVOID)&status);

int nNumberOfTracks = (int)status.dwReturn;

// トラック長を得る
MCI_SET_PARMS set;
MCI_STATUS_PARMS status;

set.dwTimeFormat = MCI_FORMAT_MSF;
mciSendCommand(wDeviceID, MCI_SET, MCI_SET_TIME_FORMAT, (DWORD)(LPVOID)&set);

status.dwItem = MCI_STATUS_LENGTH;
status.dwTrack = 1; // 1 トラック目の長さを得る場合
mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_TRACK, (DWORD)(LPVOID)&status);

int nTime = MCI_MSF_MINUTE(status.dwReturn) * 60 + MCI_MSF_SECOND(status.dwReturn); // 秒数に変換

// 現在位置を得る
MCI_SET_PARMS set;
MCI_STATUS_PARMS status;

set.dwTimeFormat = MCI_FORMAT_TMSF;
mciSendCommand(wDeviceID, MCI_SET, MCI_SET_TIME_FORMAT, (DWORD)(LPVOID)&set);

status.dwItem = MCI_STATUS_POSITION;
mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM, (DWORD)(LPVOID)&status);

DWORD dwPosition = status.dwReturn; // 現在位置が TMSF 形式で得られる

// ディスクの有無を得る
MCI_STATUS_PARMS status;

status.dwItem = MCI_STATUS_MEDIA_PRESENT;
mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM, (DWORD)(LPVOID)&status);

BOOL bExist = (BOOL)status.dwReturn; // ディスクの有無が BOOL 値で得られる

MCI_STATUS_PARMS 構造体
typedef struct {
DWORDdwCallback;// MM_NOTIFY を送るウィンドウのハンドル
DWORDdwReturn;// 取得した情報がここに返る
DWORDdwItem;// 取得したい情報を指定する
DWORDdwTrack;// トラックの情報を得る場合にトラック番号を指定する
} MCI_STATUS_PARMS;

dwItem の設定値
MCI_STATUS_LENGTHメディア長を dwReturn に返す
MCI_STATUS_NUMBER_OF_TRACKS全トラック数を dwReturn に返す
MCI_STATUS_POSITION現在位置を dwReturn に返す
MCI_STATUS_MEDIA_PRESENTディスクの有無を dwReturn に返す
MCI_TRACKトラックの情報を得ることを示す

♪実践的なテクニック
ここまでに書いたコマンドを使えば音楽 CD の制御ができます。
簡単な機能の CD プレイヤーであればこれくらいで十分作ることが可能だと思いますが、少しでも機能追加をしようとすると自力で実装しないといけないのが MCI コマンドの厄介なところです。

§ランダム再生を実装しよう

MCI コマンド最大の問題点はトラックの変わり目を検出できないことだと思います。
なぜ検出する必要があるのでしょうか? CD の最初から終わりまでを順に演奏する分には検出できなくても問題ありません。
必要となるのはランダム再生やプログラム再生を実装する場合です。トラックが終わるたびにあちこちのトラックに移るわけですが、トラックの変わり目が検出できないことには移るタイミングが判りません。という訳で検出する必要が出てくるわけです。

実際、検出しようと思えば出来ないことも無いです(というよりも出来る)。まず一番簡単なやり方は、WM_TIMER メッセージを発生させてその度に MCI_STATUS_POSITION で現在位置を得る方法です。前回の WM_TIMER 発生時とトラック番号が変わっていればその時にトラックが変わった事が判ります。WM_TIMER も 10 回/秒ほど発生させたら問題ない程度に検出できるはずです。

でも、こんなやり方じゃダメだと思いませんか? だいたい WM_TIMER が 10 回/秒発生したところで 0.1 秒は余計なトラックが再生される可能性があるのですから。
それにトラックの変わり目を調べるためだけに 10 回/秒も WM_TIMER が発生するっていうのもダサいです。

次のやり方は MM_MCINOTIFY メッセージによってトラックの変わり目を通知してもらう方法です。このやり方だとかなり思い通りの動作をしてくれます。
MM_MCINOTIFY とは、各種 MCI コマンドが完了したり異常終了した場合、このメッセージで通知されます。
通知される内容は以下の通りです。

MM_MCINOTIFY メッセージのパラメータ
wParamMCI_NOTIFY_ABORTEDコマンドが中断された
MCI_NOTIFY_FAILUREデバイスエラーが発生した
MCI_NOTIFY_SUCCESSFULコマンドが正常終了した
MCI_NOTIFY_SUPERSEDEDコマンドが二重に発生した(?)
lParamメッセージを発したデバイスの ID

この MM_MCINOTIFY メッセージを発生させるには 各種 MCI コマンドの dwFlags に MCI_NOTIFY を指定するだけです。

トラックの変わり目を検出したい場合、MCI_PLAY コマンドの時に MCI_NOTIFY を指定してやれば指定したトラックの演奏を終えたときに通知が来ます。
問題は全 10 トラックの音楽 CD を演奏しても MCI_NOTIFY (の MCI_NOTIFY_SUCCESSFUL)が通知されるのは 10 トラック終了時の 1 回だけという点です。コマンド完了時に通知が来るという仕様上、そうなります。

すべてのトラック終了時に通知を受け取るためには MCI_PLAY コマンドで 1 トラックずつ演奏するという方法があります。

MCI_PLAY_PARMS play;

play.dwCallback = (DWORD)hWnd;
play.dwFrom = MCI_MAKE_TMSF(1, 0, 0, 0); // トラック 1 の最初から
play.dwTo = MCI_MAKE_TMSF(2, 0, 0, 0); // トラック 2 の最初(=トラック 1 の最後)までを演奏
mciSendCommand(wDeviceID, MCI_PLAY, MCI_FROM | MCI_TO | MCI_NOTIFY, (DWORD)(LPVOID)&play);

として、トラック 1 の演奏を終え MM_MCINOTIFY が届いたら トラック 2 だけを演奏する。トラック 2 が終わり MM_MCINOTIFY が届いたら次はトラック 3 だけを演奏する。これを繰り返せばトラックが変わるごとに通知が届きます。
しかしこのやり方には、トラックとトラックの間で一瞬音が途切れてしまう欠点があります。CD プレイヤーとしては致命的な問題です。これでは使い物になりません。

では、どうやればいいのでしょうか?

基本的には後者の MM_MCINOTIFY メッセージを受け取る方法ですが、違うのは連続したトラックを演奏する場合には MCI_PLAY_PARMS 構造体の dwTo に連続している最後のトラックの終端を指定する点です。

たとえば全 9 トラックの音楽 CD があったとします。通常再生時は 1 から 9 までを順に再生すればいいので

再生する順番

と、こんな感じです。この場合、dwFrom にはトラック 1、dwTo は省略(=ディスクの最後まで)してやればいいです。
トラック 1 から 8 までは MM_MCINOTIFY は通知されませんが順に演奏するだけなので通知される必要もありません。

次にランダム再生時です。ポイントは再生する順番をあらかじめ決めておき、それを配列に格納しておくことです。
ランダムで再生する順番が以下のようになったとします。

再生する順番

まず、最初に演奏するトラック 7。次のトラックは 5 なので連続していません。よって dwFrom と dwTo にはトラック 7 の最初と最後を指定します。
トラック 7 の演奏を終えると MM_MCINOTIFY が届きます。次はトラック 5 を再生です。しかもその次はトラック 6 なので連続しています。ここでは dwFrom にトラック 5 の先頭、dwTo にトラック 6 の最後を指定してやればいいわけです。こうすればトラック 5 からトラック 6 へ変わる瞬間にも音は途切れません。この時、MM_MCINOTIFY の通知も来ませんが、続けて再生するだけなので通知が来る必要もありません。
次はトラック 9。その次がトラック 2 で連続していないのでトラック 9 のみ演奏します。その次はトラック 2 からトラック 4 まで連続しているので dwFrom にトラック 2 の先頭、dwTo にトラック 4 の最後を指定します。
こうしてやれば必要な通知を受けつつ、音が途切れることなくランダム再生・プログラム再生が可能になります。

長々と書いたわりには当たり前の手法な気がします (もちろんこれが最良かは知りませんが)。
そもそもはじめから トラックが変わるたびに通知メッセージが届くような仕様になっていたらこんな処理は不要なんですが……。

もっといいやり方を知っている人、誰か教えて!

§ディスクの識別番号を取得しよう

データ CD や音楽 CD はディスクを識別するための一意の番号を持っています(実際にはディスク自体に番号は書き込まれておらず、何らか(トラック数や時間)の情報を元に MCI が数値を決めていると思われます)。
この番号があれば現在どの CD が挿入されているかを判断する事ができるので、判断した後はその CD の曲名を記録させたり、特定の CD は吐き出すなど、いろいろと応用できます。

これまでと同様に識別番号の取得にも MCI コマンドを使います。識別番号取得用バッファをあらかじめ確保しておき、dwFlags に MCI_INFO_MEDIA_IDENTITY をセットして MCI_INFO を送ればバッファに識別番号が書き込まれます。識別番号と言っても返ってくるのは数値ではなく文字列であることに注意してください。
識別番号を得るサンプルです。

MCI_INFO_PARMS info;
char buff[16];

ZeroMemory(buff, sizeof(buff));
info.lpstrReturn = buff;
info.dwRetSize = sizeof(buff);
mciSendCommand(wDeviceID, MCI_INFO, MCI_INFO_MEDIA_IDENTITY, (DWORD)(LPVOID)&info);

ためしに何枚かの CD を入れて番号を調べてみた結果です。

タイトル種類時間トラック数識別番号
中島みゆき「臨月」アルバム45m55s9 11007515
ティファニー「二人の世界」シングル7m57s2 246299
Windows2000 ProfessionalCD-ROM (Win)44m11s1 199407
DanceDanceRevolution 2nd RemixCD-ROM (PS)60m26s36 72836323

§音楽 CD なのか?データ CD なのか?の判断

CD プレイヤーは音楽 CD を演奏するためのものです。しかしこの世には音楽 CD だけではなく、データが記録されている CD-ROM や CD-G、Photo-CD などさまざまなメディアが存在します。
これらはどれも 12cm のディスクなのでドライブに挿入する事が可能です。さて、これらのデータ CD を演奏しようとするとどうなるでしょうか?実際にやったことはありませんがスピーカーやヘッドホンを痛めるそうです。それならデータ CD なら演奏できないようにしてしまいましょう。

注意してほしいのは この音楽 CD か?データ CD か?っていう判断はディスク単位ではなくトラック単位ということです。
プレイステーションやサターンの CD を思い出してほしいのですが、これらの CD は 1 トラック目にデータ(プログラム)が書き込まれていて、それ以降のトラックにゲーム中で演奏される音楽が録音されています。つまり 1 枚のディスクにデータ CD と音楽 CD が同居しているようなもんです。よってこの判断はトラック単位で行う必要があります。

判断の仕方ですが、MCI_STATUS_PARMS 構造体の dwItem に MCI_CDA_STATUS_TYPE_TRACK を、dwTrack に判断するトラックを設定して mciSendCommand 関数で MCI_STATUS を送ればオーケーです。
コマンド送信後、dwReturn に MCI_CDA_TRACK_AUDIO が設定されていればそのトラックは音楽トラック、MCI_CDA_TRACK_OTHER が設定されていればデータトラックだと判断できます。

サンプルはこんな感じです。

MCI_STATUS_PARMS status;

status.dwItem = MCI_CDA_STATUS_TYPE_TRACK;
status.dwTrack = 1; // 1 トラック目を判定する
mciSendCommand(wDeviceID, MCI_STATUS, MCI_STATUS_ITEM | MCI_TRACK, (DWORD)(LPVOID)&status);

switch(status.dwReturn){
case MCI_CDA_TRACK_AUDIO:
// 音楽 CD だった場合の処理
break;
case MCI_CDA_TRACK_OTHER:
// データ CD だった場合の処理
break;
}

§トレイの開閉を感知する

次はトレイ開閉の状態を調べる方法です。

やり方のひとつとして、、、MCI_STATUS コマンドでディスクの有無を検査する方法があります。検査した結果が前回と違っていたら、例えば前回あったディスクが今回無くなっていたらそれはディスクが取り出されたと判断し、空だったトレイにディスクが見つかればディスクが挿入されたと判断するわけです。
1 秒間に 3 回くらい WM_TIMER が飛んでくるようにしてメッセージが来るたびに検査と比較をすればいいでしょう。

しかし、このやり方、やはりダサいです。まず、このやり方だとタイマーを使わないとだめです。1 分間に何度もディスクが出し入れされるわけでもないのに、ディスクの有無を検査するために何回も何回も何回も何回もメッセージが飛んできます。無駄だと思いませんか?

実はトレイの開閉は WM_DEVICECHANGE メッセージで感知できます。これは MCI とは違い、通常のウィンドウメッセージです。このメッセージを受けた時、wParam が DBT_DEVICEREMOVECOMPLETE ならばトレイがオープンされた、DBT_DEVICEARRIVAL ならば閉じられたと判断できます。
しかし、このメッセージは CD ドライブの開閉を教えてくれるメッセージではなく、デバイス状態の変化によって通知されるそうです。だから CD ドライブでなくても、例えば USB 端子に外付け FDD を繋げたり、ネットワークドライブを接続・切断してもこのメッセージは発生します。どれもデバイス状態の変化ですので。ネットワークドライブを切断したのを CD トレイが開いたのと勘違いしないようにプログラムをする必要があります。

もっと問題なのはこのメッセージ、通知されるのが異常に遅い点です。CD ドライブを閉じてから 5 秒くらい経ってから のほほんとメッセージが飛んできます。なんでこんなに遅いのかなぁ、と嘆いたところでそういう仕様なんだからどうしようもありませんが、とにかく遅いです。ほかにもっと感度のいいメッセージはないのでしょうか?
そういう意味では、ダサいダサいといいつつも WM_TIMER をばんばん発生させてディスクの有無をチェックするほうが正解かもしれません。

そうそう、この WM_DEVICECHANGE メッセージを利用するには dbt.h をインクルードする必要があるのでお忘れなく。