|
○ はじめに
音楽 CD を聞いていると、”伴奏の部分だけのデータにして歌の練習をしたいなあ”などと思うことはありませんか? その様な用途に使える様に、ここではボーカルキャンセラを作ってみたいと思います。ボーカルキャンセラといっても、カラオケマシンの様な完全な伴奏曲を作るのではなく、”うまくいけばボーカルの部分があまり聞こえなくなる”といった程度の簡単なものです。
音楽 CD の曲は通常ステレオ ( 2 チャンネル ) です。ステレオ曲の場合右と左で異なる音が再生されますが、多くの場合ボーカルは右と左に同じ波形で格納されています ( もちろん、右と左のバランスを変化させる曲もたくさんあります。なので、”うまくいけば”です。。。)。この場合、左右のチャンネルに同じ波形の PCM データとしてボーカルが格納されているので、片方のチャンネルの位相を反転して両チャンネルを加算する ( 要は、片方のチャンネルからもう一方のチャンネルの値を引けば良いです )と、ボーカルが消えるという仕組みです。左右のチャンネルを合成するので、結果として得られるデータはモノラルとなります。
うまくいけば、ボーカルがかなり小さくなりほとんど聞こえなくなると思います。ただし、ボーカル以外に左右に同じ波形で格納されている 音がある場合 ( 低音域の音が多い ) は、それらの音も小さくなってしまいますのであしからず。。。
○ サンプルプログラム
指定されたステレオ WAVE ファイルからボーカルを消すサンプルプログラム vocancel.c です。はじめに のページから、全てのサンプルプログラムを固めた tar ボールをダウンロードすることもできます。
gcc -o vocancel vocancel.c
としてコンパイルします。コンパイルオプションは各自の環境などに応じて修正して下さい。
引数に入力 WAVE ファイルのパスと出力 WAVE ファイルのパスを指定して実行します。vocancel.c では、16ビットステレオの WAVE ファイルのみを処理対象とします。結果として得られる WAVE ファイルは 16ビットモノラルとなります。それ以外の WAVE ファイルを入力に指定した場合は、エラーを表示して終了します。また、入力ファイルの何 % を処理したかの表示は随時更新されます。
--- vocancel.c ---
1 #include <fcntl.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <sys/ioctl.h> 6 #include <unistd.h> 7 #include <linux/soundcard.h> 8 #include <limits.h> 9 10 11 /* WAVE ファイルの情報を格納する構造体 */ 12 typedef struct { 13 FILE* fp; /* ファイル構造体 */ 14 short is_pcm; /* PCM フォーマットの場合は 1、それ以外は 0 */ 15 short channel; /* モノラルの場合は 1、ステレオの場合は 2 */ 16 int rate; /* サンプリング周波数 */ 17 short bits; /* 量子化ビット数 */ 18 long offset; /* ファイル先頭から PCM データまでのオフセット */ 19 int len; /* PCM データ部の長さ */ 20 } 21 WAVE; 22 23 24 /* 関数プロトタイプ */ 25 static int vocancel_read_file( char* fname, WAVE* wave ); 26 static int vocancel_mix_and_write( char* fname, WAVE* inwave ); 27 static void vocancel_progress( int* processed, int current, WAVE* wave ); 28 29 30 int 31 main( int argc, char** argv ) 32 { 33 WAVE wave; 34 35 if ( argc != 3 ) { 36 fprintf( stderr, "usage : vocancel filetoread filetowrite\n" ); 37 return 1; 38 } 39 40 /* WAVE ファイルヘッダの読み込み */ 41 if ( vocancel_read_file( argv[1], &wave ) != 0 ) { 42 fprintf( stderr, "Failed to read specified WAVE file" 43 ": %s\n", argv[1] ); 44 return 1; 45 } 46 47 48 /* ステレオ => モノラル変換 */ 49 printf( "Now canceling center sound ...\n" ); 50 51 if ( vocancel_mix_and_write( argv[2], &wave ) != 0 ) { 52 fprintf( stderr, "Failed to write monoral WAVE file" 53 ": %s\n", argv[2] ); 54 fclose( wave.fp ); 55 return 1; 56 } 57 58 fclose( wave.fp ); 59 printf( "\ndone.\n" ); 60 return 0; 61 } 62 63 64 /* 65 * WAVE ファイル情報を読み込む 66 */ 67 static int 68 vocancel_read_file( char* fname, WAVE* wave ) 69 { 70 char buf[32]; 71 int len; 72 73 if ( ( wave->fp = fopen( fname, "r" ) ) == NULL ) { 74 fprintf( stderr, "Failed to open %s\n", fname ); 75 return -1; 76 } 77 78 /* 79 * 先頭 4 バイトが "RIFF" であることを確認 80 * 更に 4 バイトスキップしておく 81 */ 82 fread( buf, 8, 1, wave->fp ); 83 84 if ( strncmp( buf, "RIFF", 4 ) != 0 ) { 85 fprintf( stderr, "Specified file is not RIFF file.\n" ); 86 fclose( wave->fp ); 87 return -1; 88 } 89 90 91 /* 次の 4 バイトが "WAVE" であることを確認 */ 92 fread( buf, 4, 1, wave->fp ); 93 94 if ( strncmp( buf, "WAVE", 4 ) != 0 ) { 95 fprintf( stderr, "Specified file is not WAVE file.\n" ); 96 fclose( wave->fp ); 97 return -1; 98 } 99 100 /* fmt チャンクを探す */ 101 while ( 1 ) { 102 fread( buf, 8, 1, wave->fp ); 103 len = *( int* )( &buf[4] ); 104 105 if ( strncmp( buf, "fmt ", 4 ) != 0 ) { 106 if ( fseek( wave->fp, len, SEEK_CUR ) == -1 ) { 107 fprintf( stderr, "Failed to find fmt chunk.\n" ); 108 fclose( wave->fp ); 109 return -1; 110 } 111 } 112 else { 113 break; 114 } 115 } 116 117 118 /* WAVE フォーマットを読み込む */ 119 fread( buf, len, 1, wave->fp ); 120 121 wave->is_pcm = *( ( short* )( &buf[0] ) ); 122 wave->channel = *( ( short* )( &buf[2] ) ); 123 wave->rate = *( ( int* )( &buf[4] ) ); 124 wave->bits = *( ( short* )( &buf[14] ) ); 125 126 if ( wave->is_pcm != 1 ) { 127 wave->is_pcm = 0; 128 } 129 130 /* data チャンクを探す */ 131 while ( 1 ) { 132 fread( buf, 8, 1, wave->fp ); 133 len = *( int* )( &buf[4] ); 134 135 if ( strncmp( buf, "data", 4 ) != 0 ) { 136 if ( fseek( wave->fp, len, SEEK_CUR ) == -1 ) { 137 fprintf( stderr, "Failed to find data chunk.\n" ); 138 fclose( wave->fp ); 139 return -1; 140 } 141 } 142 else { 143 break; 144 } 145 } 146 147 wave->len = len; 148 149 if ( ( wave->offset = ftell( wave->fp ) ) == -1 ) { 150 fprintf( stderr, "Failed to find offset of PCM data.\n" ); 151 fclose( wave->fp ); 152 return -1; 153 } 154 155 return 0; 156 } 157 158 159 #define BUFSIZE 128000 160 161 /* 162 * 指定ファイルに、左右のチャンネルを合成したモノラル WAVE データを出力する。 163 */ 164 static int 165 vocancel_mix_and_write( char* fname, WAVE* in ) 166 { 167 /* 168 * wave_len は WAVE ファイルの先頭から 4 バイト目のデータ 169 * head_len は WAVE ファイルの先頭から 16 バイト目のデータ 170 * data_len は PCM データ部のデータ長 171 */ 172 int wave_len; 173 int head_len; 174 int data_len; 175 int byte_sec; 176 int byte_dat; 177 178 int tmpval; 179 int processed = 0; 180 int len = 0; 181 int i; 182 183 char ibuf[ BUFSIZE ]; 184 char obuf[ BUFSIZE ]; 185 186 short channel = 1; 187 short out; 188 short in_l; 189 short in_r; 190 191 FILE* fp; 192 193 if ( ( fp = fopen( fname, "w" ) ) == NULL ) { 194 fprintf( stderr, "Failed to open %s\n", fname ); 195 return -1; 196 } 197 198 199 /* ステレオでない場合は、左右のチャンネルの合成処理が出来ないため終了 */ 200 if ( in->channel != 2 ) { 201 fprintf( stderr, "Specified WAVE file is not stereo.\n" ); 202 fclose( fp ); 203 return -1; 204 } 205 206 207 /* 16 ビットのデータかどうか確認 */ 208 if ( in->bits != 16 ) { 209 fprintf( stderr, "Specified WAVE file is not 16 bits data.\n" ); 210 fclose( fp ); 211 return -1; 212 } 213 214 data_len = in->len / 2; 215 head_len = 18; 216 wave_len = ( data_len + 8 ) + head_len + 12; 217 byte_sec = in->rate * in->bits / 8; 218 byte_dat = in->bits / 8; 219 220 221 /* PCM データの直前までのデータを書き込む */ 222 fwrite( "RIFF", strlen( "RIFF" ), 1, fp ); 223 fwrite( &wave_len, 4, 1, fp ); 224 fwrite( "WAVE", strlen( "WAVE" ), 1, fp ); 225 fwrite( "fmt ", strlen( "fmt " ), 1, fp ); 226 fwrite( &head_len, 4, 1, fp ); 227 fwrite( &in->is_pcm, 2, 1, fp ); 228 fwrite( &channel, 2, 1, fp ); 229 fwrite( &in->rate, 4, 1, fp ); 230 fwrite( &byte_sec, 4, 1, fp ); 231 fwrite( &byte_dat, 2, 1, fp ); 232 fwrite( &in->bits, 2, 1, fp ); 233 tmpval = ( short )head_len; 234 fwrite( &tmpval, 2, 1, fp ); 235 fwrite( "data", strlen( "data" ), 1, fp ); 236 fwrite( &data_len, 4, 1, fp ); 237 238 239 /* PCM データを合成しながら書き込む */ 240 fflush( stdout ); 241 vocancel_progress( &processed, 0, in ); 242 243 while ( 1 ) { 244 len = fread( ibuf, 1, BUFSIZE, in->fp ); 245 246 if ( len < BUFSIZE && ferror( in->fp ) ) { 247 fprintf( stderr, "Error occured.\n" ); 248 fclose( fp ); 249 return -1; 250 } 251 252 for ( i = 0; i < len; i += 4 ) { 253 memcpy( &in_l, &ibuf[i], 2 ); 254 memcpy( &in_r, &ibuf[i + 2], 2 ); 255 256 tmpval = in_l - in_r; 257 258 if ( tmpval > 0 && tmpval > SHRT_MAX ) { 259 out = SHRT_MAX; 260 } 261 else if ( tmpval < 0 && tmpval < SHRT_MIN ) { 262 out = SHRT_MIN; 263 } 264 else { 265 out = tmpval; 266 } 267 268 memcpy( &obuf[ i / 2 ], &out, 2 ); 269 } 270 271 fwrite( &obuf, len / 2, 1, fp ); 272 vocancel_progress( &processed, len, in ); 273 274 if ( len < BUFSIZE ) { 275 break; 276 } 277 } 278 279 fclose( fp ); 280 return 0; 281 } 282 283 284 /* 285 * 進捗率の表示 286 */ 287 static void 288 vocancel_progress( int* processed, int current, WAVE* wave ) 289 { 290 int progress; 291 292 *processed += current; 293 progress = (int)( ( ( double )*processed / ( double )wave->len ) * 100 ); 294 printf( "\r%3d%% processed.", progress ); 295 fflush( stdout ); 296 } 297 298 299 /* End of vocancel.c */
○ サンプルプログラムの説明
では、サンプルプログラムについて説明していきます。
- 11 行目 〜 21 行目
WAVE 構造体の定義については、LECTURE 10 WAVE ファイルを再生する と同じです。
- 24 〜 27 行目
内部関数のプロトタイプ宣言です。vocancel_read_file() で入力 WAVE ファイルのヘッダを読み込みます。 vocancel_mix_and_write() で、入力 WAVE ファイルの左右のチャンネルを合成しつつ出力 WAVE ファイルにデータを書き込みます。vocancel_progress() で WAVE ファイル処理中に進捗率を表示します。
- 30 〜 61 行目 ( main() )
main() 関数では、まず vocancel_read_file() で指定された WAVE ファイルの情報を読み込み、WAVE 型変数に保存しておきます。
次に vocancel_mix_and_write() では、読み込んだ WAVE ファイルの情報を使用して、 ボーカルキャンセル処理した WAVE データを出力ファイルに書きこみます。
- 64 〜 156 行目 ( vocancel_read_file() )
WAVE ファイルを読み込んで、WAVE 型変数に情報を格納する関数です。LECTURE 10 WAVE ファイルを再生する と同じです。
- 159 行目
入力ファイルから一度に読み込んで処理するデータサイズを定義しています。
- 161 〜 281 行目 ( vocancel_mix_and_write() )
入力ファイルからデータを読み込み、左右のチャンネルを合成してモノラルデータを出力する関数です。
- 167 〜 176 行目
出力 WAVE ファイルのヘッダを書き込むために使用する変数を宣言しています。
- 183 〜 184 行目
ibuf に入力 WAVE ファイルから読み込んだデータを格納し、obuf に出力 WAVE ファイルのデータを格納します。
- 199 〜 212 行目
入力 WAVE ファイルが、16ビットステレオかどうかを確認しています。もし 16ビットステレオでない場合は、 エラー終了します。
- 214 〜 218 行目
出力 WAVE ファイルのヘッダ構築に必要な値を計算しています。data_len は出力 WAVE ファイルの PCM データ長であり、 入力 WAVE ファイルの半分になります ( 左右のチャンネルを合成するため )。head_len は WAVE ファイルの先頭 20 バイトに続くヘッダ部のデータ長で、wave_len は WAVE ファイルから先頭 8 バイトを除いた部分のデータ長です。 byte_sec は 1 秒間に使用するバイト数であり、サンプリング周波数 * byte_dat ( 1サンプルのバイト数 ) で計算できます。これらの値は、221 〜 236 行目の PCM データ直前までのデータを書き込む際に使用します。
- 221 〜 236 行目
PCM データ直前までのデータを書き込みます。LECTURE 10 WAVE ファイルを再生する に書いてある WAVE ファイルの形式と比較しながら、どの変数がどういうデータなのか確認してみて下さい。
- 243 〜 277 行目
出力データを作成する while ループです。まず、入力 WAVE ファイルから一定長のデータをバッファに読み込みます。 バッファのデータから左右のチャンネルを 1 サンプルずつ取り出し、左のサンプル値から右のサンプル値を引いた値を 出力用バッファに書き込みます ( 右のサンプル値から左のサンプル値を引いても良い )。その際、符号付き 16 ビット値の範囲を越えていないかどうかチェックし、越えている場合は限界値を設定します。
入力バッファの全データを処理したところで、出力バッファの内容をファイルに書き込むと共に進捗率の表示を更新します。
- 284 〜 296 行目 ( vocancel_progress() )
WAVE ファイルの処理中に、コンソール画面に現在の進捗率を表示します。処理内容は LECTURE 10 WAVE ファイルを再生する の wave_progress() と同じです。
以上で vocancel.c の説明は終わりです。vocancel.c の方法では必ずボーカルをキャンセル出来るわけではありませんが、それでもいろんなデータで遊んでみると結構面白いと思います。ぜひ試してみて下さい。