コマンドラインで録音する 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