コマンドラインで録音する Macで。

Macに乗り換えてはや一年。
だいぶMac流の流儀(LaunchCtrlとか)にも慣れ始めた今日この頃。
とはいえまだまだわからんことも。

Macってコマンドラインから録音できないんですか?
というか /dev/dsp ないんすか?
呟いてもにっちもさっちもだったので自分で作ってみた。

portaudioを使って。


portaudioというのはMacだけじゃなくてWinやLinuxなどのaudio周りをラップしてくれるライブラリらしい。
にゃる。


ということは、これで作ればLinuxでも動くのか。
触ってみて損のあるライブラリではなさそう。


とりあえず設計としては超シンプルに。
録音形式はRAWでいいや。
いざとなりゃsoxで変換すればいいし。


とか思って作り始めたのだが...
C++ってこんな難しかったっけ?
っていうか、書き方が思い出せない...orz
た、タプルはどこ?


ということで、以下Boostたっぷりなソース。
もうBoostなしでC++を書けないと認識した今日この頃。そしてclassってなんだっけ(苦笑
"class Hoge where"ですね、わかります!
getoptもBoostのProgram_Options使おうと思ったのだけど謎すぎたのでこいつと portaudio だけ.hをinclude。

#include <getopt.h>
#include <portaudio.h>

#include <vector>
#include <fstream>
#include <iostream>
#include <iterator>
#include <boost/format.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/tuple/tuple.hpp>

static const int DEFAULT_CHANNELS = -1;
static const double DEFAULT_SAMPLE_RATE = -1;
static const PaSampleFormat DEFAULT_SAMPLE_FORMAT = paInt16;
static const int DEFAULT_SECONDS = -1;

typedef boost::tuple< PaStreamParameters *, std::ostream *, int64_t, int64_t > userdata_t;

char* getSampleFormatName( PaSampleFormat sampleFormat )
{
	switch( sampleFormat ) {
	case paFloat32:
		return "32bit Float";
	case paInt32:
		return "32bit Signed Int";
	case paInt16:
		return "16bit Signed Int";
	case paInt8:
		return "8bit Signed Int";
	case paUInt8:
		return "8bit Unsigned Int";
	default:
		return NULL;
	}
}

size_t getSampleFormatSize( PaSampleFormat sampleFormat )
{
	switch( sampleFormat ) {
	case paFloat32:
		return sizeof( float );
	case paInt32:
		return sizeof( int32_t );
	case paInt16:
		return sizeof( int16_t );
	case paInt8:
		return sizeof( int8_t );
	case paUInt8:
		return sizeof( uint8_t );
	default:
		std::cerr << boost::format( "unknown sample format %08x" ) % sampleFormat << std::endl;
		exit( EXIT_FAILURE );
	}
}

PaStreamCallback stream_callback;

void dumpError( PaError err )
{
	std::cerr << boost::format( "Error %1$d: %2$s" ) % err % Pa_GetErrorText( err ) << std::endl;
}

void version( int argc, char **argv )
{
	std::cout << boost::format( "PortAudio version: %1$d ( %2$s )" ) % Pa_GetVersion() % Pa_GetVersionText() << std::endl;
}

void usage( int argc, char **argv )
{
	PaDeviceIndex defaultInputDeviceIndex = Pa_GetDefaultInputDevice();
	const PaDeviceInfo *defaultInputDeviceInfo = Pa_GetDeviceInfo( defaultInputDeviceIndex );

	std::cout << boost::format( "usage: %1$s [OPTION]... FILE" ) % argv[0] << std::endl;
	std::cout << boost::format( "recode raw sound to FILE." ) << std::endl;
	std::cout << boost::format( "" ) << std::endl;
	std::cout << boost::format( "Mandatory arguments to long options are mandatory for short options too." ) << std::endl;
	
	std::cout << boost::format( "  -d,--device=INDEX          input device index [=%1$d(%2$s)]." ) % defaultInputDeviceIndex % defaultInputDeviceInfo->name << std::endl;
	std::cout << boost::format( "  -c,--channels=CHANNELS     channel count [=%1$d]." ) % DEFAULT_CHANNELS << std::endl;
	std::cout << boost::format( "                             <0: max channel count of input device. see --list." ) << std::endl;
	std::cout << boost::format( "  -r,--rate=RATE             sample rate[=%1$g]." ) % DEFAULT_SAMPLE_RATE << std::endl;
	std::cout << boost::format( "                             <0: default sample rate of input device. see --list." ) << std::endl;
	std::cout << boost::format( "  -f,--format=FORMAT         sample format[=%1$d]" ) % DEFAULT_SAMPLE_FORMAT << std::endl;
	std::cout << boost::format( "                             %1$2d: %2$s") % paFloat32 % getSampleFormatName( paFloat32 ) << std::endl;
	std::cout << boost::format( "                             %1$2d: %2$s") % paInt32   % getSampleFormatName( paInt32   ) << std::endl;
	std::cout << boost::format( "                             %1$2d: %2$s") % paInt16   % getSampleFormatName( paInt16   ) << std::endl;
	std::cout << boost::format( "                             %1$2d: %2$s") % paInt8    % getSampleFormatName( paInt8    ) << std::endl;
	std::cout << boost::format( "                             %1$2d: %2$s") % paUInt8   % getSampleFormatName( paUInt8   ) << std::endl;
	std::cout << boost::format( "  -t,--time=SEC              recode SEC seconds[=%1$d]" ) % DEFAULT_SECONDS << std::endl;
	std::cout << boost::format( "                             <0: infinite. stop recoding to press any key." ) << std::endl;
	std::cout << boost::format( "  -l,--list                  print input/output devices list and exit." ) << std::endl;
	std::cout << boost::format( "     --help                  print this message and exit." ) << std::endl;
	std::cout << boost::format( "     --version               print version information and exit." ) << std::endl;
}

void dumpDeviceList()
{
	PaDeviceIndex defaultInputDeviceIndex = Pa_GetDefaultInputDevice();
	PaDeviceIndex defaultOutputDeviceIndex = Pa_GetDefaultOutputDevice();
	int numDevices = Pa_GetDeviceCount();

	std::cout << boost::format( " #. # of INPUT CHANNELS / # of OUTPUT CHANNELS, DEFAULT SAMPLING RATE: DEVICE NAME(HOST API)" ) << std::endl;
	for( PaDeviceIndex i = 0; i < numDevices; ++i ) {
		const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo( i );
		const PaHostApiInfo *hostInfo = Pa_GetHostApiInfo( deviceInfo->hostApi );
		boost::format fmt = boost::format(
				"%1$2d. "
				"%2$c %3$2d input channels / "
				"%4$c %5$2d output channels, "
				"Default %6$8lg kHz : "
				"%7$s(%8$s)"
				);
		fmt % i;
		fmt % (defaultInputDeviceIndex == i ? '*' : ' ');
		fmt % deviceInfo->maxInputChannels;
		fmt % (defaultOutputDeviceIndex == i ? '*' : ' ');
		fmt % deviceInfo->maxOutputChannels;
		fmt % deviceInfo->defaultSampleRate;
		fmt % deviceInfo->name;
		fmt % hostInfo->name;
		std::cout << fmt << std::endl;
	}
}

int stream_callback( const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData )
{
	userdata_t *tUserData = static_cast<userdata_t *>( userData );
	PaStreamParameters *iParam = tUserData->get<0>();
	std::ostream *oStream = tUserData->get<1>();
	int64_t &writtenFrames = tUserData->get<2>();
	int64_t totalFrames = tUserData->get<3>();

	int unit = iParam->channelCount * getSampleFormatSize( iParam->sampleFormat );

	if( !(totalFrames < 0) && writtenFrames + frameCount > (unsigned)totalFrames ) {
		frameCount = totalFrames - writtenFrames;
	}

	oStream->write( static_cast<const char *>( input ), frameCount * unit );
	writtenFrames += frameCount;

	if( totalFrames < 0 || writtenFrames < totalFrames ) {
		return paContinue;
	} else {
		return paComplete;
	}
}

int main( int argc, char **argv )
{
	PaError err;
	if( (err = Pa_Initialize()) != paNoError ) {
		dumpError( err );
		Pa_Terminate();
		exit( EXIT_FAILURE );
	}

	PaDeviceIndex iDeviceIndex = Pa_GetDefaultInputDevice();
	int nChannels = DEFAULT_CHANNELS;
	double sampleRate = DEFAULT_SAMPLE_RATE;
	PaSampleFormat sampleFormat = DEFAULT_SAMPLE_FORMAT;
	int nSeconds = DEFAULT_SECONDS;
	std::ostream *oStream;

	while( 1 ) {
		static struct option longopts[] = {
			{ "device", required_argument, NULL, 'd' },
			{ "rate", required_argument, NULL, 'r' },
			{ "format", required_argument, NULL, 'f' },
			{ "time", required_argument, NULL, 't' },
			{ "list", no_argument, NULL, 'l' },
			{ "help", no_argument, NULL, '?' },
			{ "version", no_argument, NULL, '.' },
			{ 0, 0, 0, 0 }
		};

		int c = getopt_long( argc, argv, "d:c:r:f:t:l?.", longopts, NULL );
		if( c == -1 ) {
			break;
		}

		switch( c ) {
		case 'd':
			iDeviceIndex = boost::lexical_cast<PaDeviceIndex>( optarg );
			break;
		case 'c':
			nChannels = boost::lexical_cast<int>( optarg );
			break;
		case 'r':
			sampleRate = boost::lexical_cast<double>( optarg );
			break;
		case 'f':
			sampleFormat = boost::lexical_cast<PaSampleFormat>( optarg );
			break;
		case 't':
			nSeconds = boost::lexical_cast<int>( optarg );
			break;
		case 'l':
			dumpDeviceList();
			exit( EXIT_SUCCESS );
		case '?':
			usage( argc, argv );
			exit( EXIT_SUCCESS );
		case '.':
			version( argc, argv );
			exit( EXIT_SUCCESS );
		default:
			usage( argc, argv );
			exit( EXIT_FAILURE );
		}
	}
	if( optind < argc ) {
		if( strcmp( "-", argv[optind] ) == 0 ) {
			oStream = new std::iostream( std::cout.rdbuf() );
		} else {
			oStream = new std::ofstream( argv[optind], std::ios::out );
		}
	} else {
		usage( argc, argv );
		exit( EXIT_FAILURE );
	}

	const PaDeviceInfo *iDeviceInfo = Pa_GetDeviceInfo( iDeviceIndex );
	if( iDeviceInfo == NULL ) {
		dumpError( paInvalidDevice );
		Pa_Terminate();
		exit( EXIT_FAILURE );
	}

	if( nChannels < 0 ) {
		nChannels = iDeviceInfo->maxInputChannels;
	}
	if( sampleRate < 0 ) {
		sampleRate = iDeviceInfo->defaultSampleRate;
	}

	PaStream* stream = NULL;
	PaStreamParameters iParam;

	memset( &iParam, 1, sizeof( PaStreamParameters ) );
	iParam.device = iDeviceIndex;
	iParam.channelCount = nChannels;
	iParam.sampleFormat = sampleFormat;
	iParam.suggestedLatency = iDeviceInfo->defaultLowInputLatency;
	iParam.hostApiSpecificStreamInfo = NULL;

	std::cout << "Input Device : " << iDeviceInfo->name << std::endl;
	std::cout << "Sample Format: " << getSampleFormatName( sampleFormat ) << std::endl;
	std::cout << "Sample Rate  : " << sampleRate << std::endl;
	std::cout << "Channel Count: " << nChannels << std::endl;

	userdata_t userData = boost::make_tuple( &iParam, oStream, 0, nSeconds * sampleRate );
	if( (err = Pa_OpenStream( &stream, &iParam, NULL, sampleRate, 256, paClipOff, stream_callback, &userData )) != paNoError ) {
		dumpError( err );
		Pa_Terminate();
		exit( EXIT_FAILURE );
	}

	Pa_StartStream( stream );
	while( Pa_IsStreamActive( stream ) ) {
		Pa_Sleep( 1 );
	}
	Pa_StopStream( stream );

	Pa_CloseStream( stream );
	Pa_Terminate();

	delete oStream;
	return 0;
}

ただしいAPIの書き方だとか例外処理だとかそんなものはしらん!!
動きゃいいんだ動けば!!
ソースは運がよければ http://unite.goth-wrist-cut.operaunite.com/file_sharing/content/ このあたりにある...かも。
Opera Uniteおもしろいよね!常接マシンじゃないけどね!!!
大学のWebServerは最近システム入れ替えあって、未だ環境を再設定していないので...そのうち。


名称はpaRec(仮)。recはsoxが使ってるっぽいので、PortAudioRECcodeでpaRec。(仮
とりあえず使い方としては、まず、"paRec --list"でデバイス一覧を取得。

$ paRec --list
 #. # of INPUT CHANNELS / # of OUTPUT CHANNELS, DEFAULT SAMPLING RATE: DEVICE NAME(HOST API)
 0. *  2 input channels /    0 output channels, Default    44100 kHz : Built-in Microphone(Core Audio)
 1.    2 input channels /    0 output channels, Default    44100 kHz : Built-in Input(Core Audio)
 2.    0 input channels / *  2 output channels, Default    44100 kHz : Built-in Output(Core Audio)
 3.    1 input channels /    1 output channels, Default     8000 kHz : AXS-02(Core Audio)
 4.    2 input channels /    2 output channels, Default    44100 kHz : Soundflower (2ch)(Core Audio)
 5.   16 input channels /   16 output channels, Default    44100 kHz : Soundflower (16ch)(Core Audio)

うちの環境だとこんな感じ。
一番左の列がデバイスのインデックス。こいつで使いたいデバイスを指定します。
次が最大の入力チャンネルと出力チャンネル。"*"がついているのが入出力それぞれのデフォルト。
...出力は...意味ないね...うん。
次いでカンマの後ろがデフォルトのサンプリングレートで、コロンに続けて名称と。
Built-in なんたら、ってのがMacの標準の入出力で、AXS-02はBTヘッドセット。
Soundflowerは仮想AudioデバイスSkypeとかの音をUstしたりなんやりするときに便利らしいSomething。


録音したい場合は"paRec -d デバイスインデックス -c チャンネル数 -r サンプリングレート -f フォーマット -t 録音時間 ファイル名"となる。
チャンネル数、サンプリングレートに関しては省略するとそのデバイスのデフォルト値、フォーマットは省略するとsigned int 16が使われる。
録音時間を省略するとCtrl-CでKillするまで録音。
フォーマットの指定に関しては1,2,8,16,32から指定。どれが何に対応するかは--helpしてみてくだしあ。
指定方法が変なのはportaudioの内部の数字そのまま使っているからさ!
あと24bitって何よ?ってことで4は指定できないのさ!
最初はsoxのように-1,-2,-4,-8でサイズ、-s,-i,-fとかで型、みたくしようと思ったのだけどめんどくさくて...orz
...怠惰ですいません。
...知識なくてすいません。
...生きていてすいません。
ファイル名は"hoge.raw"みたいな形にしておくといいかも。
あとraw形式なのでヘッダ情報を持っていません。
どんなオプションを指定したかを忘れないうちにsoxでwavなりなんなりに変換することをおすすめします。はい。
チャンネル数とサンプリングレートに関しては sox や play のオプションにあわせてあるので、"-traw"とフォーマットに会わせたオプションを指定すればokです。
paRec(仮)で-f32(8bit uint)なら"-1 -u"、-f8(16bit int)なら"-2 -s"、-f1(32bit float)なら"-4 -f"という感じ。
全体では、"sox -c2 -r44100 -2 -s -traw hoge.raw -twav hoge.wav"という感じでwavを作れます。


まぁ、そんなこんなです。
最初は入出力ともにデバイス/ファイルを指定できて、デバイス->ファイルで録音、ファイル->デバイスで再生、デバイス->デバイスサウンドのバイパス、ファイル->ファイルでフォーマット変換みたいなのを作るつもりだったのですが、めんどくさくなって...。
どうせ sox なり play なりでできるしね!!
...生きていてすいません。


カーネルハカーさんとか、デバドラ屋さんとかだと /dev/dsp 相当の何かを作れるのでしょうけど、あいにく自分は論理屋なので作れません。
だれかMac用の /dev/dsp 相当のなにか作ってくれないかな!!!!(他力本願
あ、あと見ての通りソースがひどいでの作り直して、かつ、洗練してくださる方も大募集♪


...あとさ、そもそもコマンドラインで録音できるよ、みたいなオチ...ないですよね...?


--追記100402
getoptのwhile中のlexical_castがひどかったので修正...orz