LECTURE 11 簡単なボーカルキャンセラ

○ はじめに

音楽 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 の方法では必ずボーカルをキャンセル出来るわけではありませんが、それでもいろんなデータで遊んでみると結構面白いと思います。ぜひ試してみて下さい。