編集履歴一覧に戻る
TakSan0のアイコン画像

TakSan0 が 2021年02月28日20時33分02秒 に編集

回路図追加して、限定から公開に変更

本文の変更

# はじめに Seeeduino XIAO というボードをご存知でしょうか? このボード 電波系のインタフェースがないので、いわゆる遠隔操作やネット連携のようなことはできませんが、とにかく小さい省エネルギー且つARM CoretexM0+ 搭載なのでスペック高めです。そして安い。 この小さいボードで、音を再生することが出来れば、おもちゃやフィギュア、小物アクセサリー等に組み込むことが出来て、色々と応用がききそうです。 「小型のものに鳴り物を仕込む実績を作りたい」というノリで作ってみました。 まずは、完成形紹介動画をご覧ください。 @[youtube](https://youtu.be/1DTtXiuE2gE) # 使用部品 |品名|型番 or SPEC|個数|参考価格|備考| |:---|---|---|---:|---| |マイコン基板|Seeed XIAO|1|729|| |マイクロSDカードアダプタ|サンハヤト CK-40|1|615|| |アンプ|JRC NJM386l156|3|204|| |スピーカー|8Ω 0.5w 15mm|1|50|ジャンク品| |LiPoバッテリ|リチウムイオンポリマー電池 150mAh|1|800|| |チップ抵抗|100Ω|1|6|| |積層セラミックコンデンサ|473|1|10|| |チップ電解コンデンサ|10uF|3|0.3|| |セラミックチップコンデンサ|1uF|3|0.3|| |電解コンデンサ|220uF10V|1|10|(10個入り特価品)| |ボリューム|10KΩ|1|50|| |M3ビス|M3x6|2|120|| |M3スペーサー|ジェラコン M3x10|2|120|| |M3ポリカビス|M3x10|3|20|| |M3ナット|M2|2|60|| |M3ポリカナット|M2|2|60|| |コネクタセット|PH2pin コネクタ+ポスト|1|30|| |たまごケース|SERIA みにくいたまごセットあひるちゃん|1|110|かなり前に購入| |下敷き|PET製※|1|5|(A4サイズをカット) 基板固定用| 参考価格で複数個入りの物は個数で割って必要数を乗算した価格です。 # ハードウェア ## 構成 卵の中は以下の構成になっています。 (1) マイコン部 (2) マイクロSD ソケット (3) アンプ部 (4) スピーカ (5) スイッチ部 (6) LiPoバッテリー ### (1) マイコン とにかく小型のものです Arduino IDE で開発でき ARM Cortex-M0 の高速処理を実現できかつ搭載メモリーも多めです。 小さいところに押し込むために、短く細い電線を直接実装する形で外付け部品を最小限にする事を意識しました。 プラ板で土台を本体にホットボンドでつけて、土台に着けたスペーサーの間にマイクロSDソケットで挟み込んで、上から半円のプラ板で抑え込んでいます。 ![固定用のプラ板を加工](https://camo.elchika.com/e29d773f31786221abae06443911b5c43e158632/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f38653264383164322d353234342d346437662d623564352d656565643335376632383362/) 半円のプラ板は後程スイッチ部を固定します。 (2) のソケットとは、プラ板を挟んで両面テープで亀の甲状態でくっつけています。 ![プラ板で](https://camo.elchika.com/e2efe07a09b7deb39c0500e3c7f78d897f689f39/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f38633232613633642d356136362d343939372d383139632d623530303830646537623931/) ### (2) マイクロSD ソケット 音楽を鳴らすにせよ、しゃべらすにせよ、鳴き声をだすにせよ、効果音をだすにせよ、 鳴り物の実績を作りには、データを ROM 内にもつと中途半端になってしまいます。 実績作りには、少し大きめのファイルをファイル単位で読めたほうがよさそうです。 XIOA はピン少なめですが SPI インタフェースはあるので マイクロSD カードに音楽データを使うことにしました。 但し、マイクロSDソケットは小型ながらも表面実装の精密なものしかありません。 しかも変換基板は600 円以上(写真左側のもの)と SDカード本体よりも高かったりします。 ですので、普段は下記右側写真の様に、余った フルサイズへの SD 変換ソケットで作った治具をブレッドボードに突き刺してやっていますが、今回は余ったSD変換ソケットがなかったのと大きすぎて、卵に入らなかったことから、やむを得なく変換基板を使用しました。 ![マイクロSDソケット](https://camo.elchika.com/e0c193f8bb825017a2c95a6854b47921f94fb729/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f66373462643534352d663833372d346438622d626234612d333630363061346631366337/) ### (3) アンプ部 マイコンの DAC 端子に直接スピーカをつけるわけにもいかないので、よく使われる手軽なアンプ 386 シリーズの JRC製の表面実装部品を使いました、 ![NJM386は表面実装品](https://camo.elchika.com/38ecde5f737b1b28f551327aabdabbb91cb48542/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f64323938386162392d326532332d346331322d383031372d636163663039633337356339/) 少しでもスペースを節約するために、表面実装品を多用して取り付けました。 ![NJM386周辺実装](https://camo.elchika.com/3cdfaafa99a842763bc31344fc21fc10f5ea0041/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f63363364326134632d323362612d343436332d383031302d663439633463316536396563/) 空中配線も多くてきたなくはありますが。 アナログ端子から出る信号は 0V ~ アナログリファレンス電圧までしか出ませんので、直流成分のオフセットが掛かっているので、入力はコンデンサで受けています。 一応カットオフ周波数あたりを計算して、10KΩボリュームに会うものが、手持ちのでは 1uF を使ったのと、出力は 8Ωでいつも使う値、220uFにしている以外は、推奨回路のままです。 はっきり言ってノイズが多すぎます。この辺はノイズ対策のチューニングが必要ですが、それはこの記事の主題ではないので、今回は断念しました。 また、卵型のケースとの相性がいいのか、最小の20倍でボリューム絞ってても、結構大きい音が出ます。 取り付けはスピーカーのすぐ裏、周りは絶縁されるので外れても大丈夫なので、軽く両面テープで止めました。 ![スピーカー裏に取り付け](https://camo.elchika.com/1cbe8c758da6b8ecbb04bd3848e5c4cd23fce89c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f30653362386237322d623137662d343931362d613163632d643430633563313363383766/) ### (4) スピーカ 小型の円形スピーカーを使いましたが、十分すぎるほどでした。 取り付け穴もないので、プラ板前後から挟みます。 ![プラ板を加工](https://camo.elchika.com/f995bcc1d1679c95f79919ee6337c88c1197de14/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f36376535303536322d336531362d343736342d393036302d653561323033363137323439/) プラ板で挟んで ![プラ板で挟んだ](https://camo.elchika.com/7467948dca3d00806379ea2ce26f576c040b4424/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f31376530343663642d376335372d343734632d383064382d346639386132313033626666/) ホットボンドで固定しました。(スピーカーの穴に合わせて穴もあけたのですが、最終的に微妙にあいませんでしたorz) ![ホットボンドで固定](https://camo.elchika.com/224cca53b05a1c8eb91c74df7fbb22cc6f00882c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f31633464326437612d626532302d346535612d396162322d636232343036353334366165/) ### (5) スイッチ部 離すととスイッチが入るタイプのマイクロスイッチがあれば簡単だったのですが、手持ちのは全て大きすぎて入りませんでしたので、自作しました。 分解した、電池式のモバイルバッテリの残骸の接点部分を利用して ![電池ボックスの残骸を接点部分に流用](https://camo.elchika.com/5148861977c5fc33952a1b5372ec02e24f748e62/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f65346538306637302d626635302d346536342d383237662d346330343065363435623438/) ユニバーサル切断して形を加工して土台にし、常に押し合っている状態に実装しました。 ![スイッチの接点部](https://camo.elchika.com/d9ce8a30d77b5ca910579e57c338cff444b5947f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f62336132303239342d303539312d346166312d393931382d306633303361373331656266/) このままでは耐久性が悪くショートする確率も高いので、最終取り付け時にはプラ板の上に載せてホットボンドで土台ごとがっちり固めました。 ![スイッチ部の土台はホットボンドでしっかり固定](https://camo.elchika.com/54663cc5ae99475efc016f1c1e00b5c374193318/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f35646265616366302d343161362d343536312d383232302d616230653865366234316434/) スイッチ遮断側は、対になる側(スピーカーとアンプ部が入っている方)プラ板をホットボンドで固定して作ります。透明なので少し見にくいですが、写真の通りです。 ![スイッチの接点遮断部のプラ板](https://camo.elchika.com/58267f5c2fcc728435079556750b5f6974d2a97f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f63306138313865652d343034632d343065392d616230622d333634386135396536386433/) ### (6) LiPoバッテリー マイコン側の土台プラ板の下に両面テープで止めて、土台をホットボンドで固定しています。 ![LiPoバッテリーの固定](https://camo.elchika.com/84469bff56e7513d2e3eaec51ec983bd2ab4056f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f33626162346539642d613238662d343662612d383330372d323466346536623639643261/) (1) ~ (6) を全て組み込んで、この写真のようになります。 ![全体を組み立て](https://camo.elchika.com/68da5121c35232014f8c5bdb3d79ad2092dd72d0/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f66326339363263622d613739622d346366392d616463382d666437626539613130346361/) ## 回路図

-

準備中(現在 使い始めたばかりのEagle と格闘中です)

+

![キャプションを入力できます](https://camo.elchika.com/08c300be4bbd20e8aa025ccce5f7de8995a42058/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f66316634373662372d333466642d346630342d626631652d656662656336613265656432/)

# ソフトウェア ## 開発環境 Arduino-IDE を使用 使用ライブラリ -Adafruit_ZeroTimer ## 音楽データ 簡単にするために今回は MP3 ではなく、PCMファイル(.WAV) を使用しました。 デモで使用しているのは、ライセンスフリーな音楽素材がおいていあるサイト [クラシック名曲サウンドライブラリ](http://andotowa.quu.cc/) から持ってきたものです。 MP3 なので、 FFMPEG で変換して使っています。 また、サンプリング周波数が高いと処理が間に合わなかったので、少しダウンサンプリングしています。 (22.05KHz) ![MP3->WAV + ダウンサンプリング](https://camo.elchika.com/14a2e33ccf240d06cff9e27a9e826d43644ec989/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f39636634393335312d326365662d343030322d383838372d3962356664656261383735662f66343966623936362d306538622d346565342d393063392d396239303761373138396337/) > ffmpeg.exe -i Mozart-EineKleine-1st.mp3 -ac 1 -ar 22050 -acodec pcm_s16le -f wav EineKlei.wav 変換したファイルを ルートに作成した、 wav_pcm というフォルダーに入れています。 ## 音楽ファイルフォーマット XIAO で PCMファイルを再生するものが探しきれなかったので、ファイルフォーマット解析の部分を含め全て再生部は全て自作しています。 全てのファイルを保証するわけではありませんが、ffmpega が吐き出すフォーマットはいくつか試した限りでは全部再生できました。 ## 処理の流れ 再生部分は、メイン側のファイル読み出しと、タイマによる定期割り込みによる、DAC制御の2部構成です。 それを、キューを使って連携しています。 ### (1) ファイル読み出し 読み出し側は、SDカードから読み取ったファイル内のデータを1バイト単位でただひたすら、キューに入れていく処理です。 ある程度キューにデータがたまってから、再生を開始するような仕組みを実装してあります。 バッファが足りなくなってしまったらエラーとなり、その場合中間値を出力していますが、ノイズの原因の一つになっているかもしれません。 ### (2) DAC制御 DAC制御側は、サンプリング周期にて割り込みが掛かり処理が始まりますが、これが結構早すぎるので、メイン側のファイル読み出しが間に合わないのでデータが必要な時に読めていないことがあります。 ある程度キューにデータがたまってから、再生するようにしているのはそのためで、キューにため込むことで、必要な時にデータがそろっていない状況を減らしています。 22050 Hz の WAV データではその確率もかなり低いのですが、 CD温室である 44100Hz にした場合は音が識別できないほどひどくなります。 性能限界かと思います。 ```C:XIAO_MusicEgg.ino // [[[ インクルード ]]] #include <SPI.h> #include <SD.h> #include <Adafruit_ZeroTimer.h> #include "TakSanQueue.h" // キューバッファ // [[[ マクロ定義 ]]] // [PIN定義] #define PIN_ERROR_LED LED_BUILTIN // デバッグ用のLED #define PIN_DAC A0 // アナログ DAC出力端子 #define PIN_SDCARD_CS 2 // [ADC関連定義] // 論理上は 1023 諧調あるが、クリップするので少し小さく(Headroom?) #define ANALOG_MAX 682 // 1023 // Analog値の最大値 #define ANALOG_CENTER (ANALOG_MAX>>1) // Analog値の中央(無音値) // [ボリューム定義] #define DIGITAL_VOLUME_STEP 50 // ボリューム値 (0 - 63) #define DIGITAL_VOLUME_STEP_MAX 63 // ボリューム値 (0 - 63) #define DIGITAL_VOLUME_BITS 6 // 64 なので 6BIT シフト // テスト用サイン波定義 //#define SIN_WAVE_DEBUG // これのコメントを外すとサイン波でデバッグできる #ifdef SIN_WAVE_DEBUG #define SIN_SAMPLE_FREQ (44100<<1) // サイン波のサンプリング周波数 #define SIN_WAVE_FREQ (440) // 発生させるサイン波の周波数 (A4) #define SIN_TABLE_SIZE (SIN_SAMPLE_FREQ/(SIN_WAVE_FREQ)) //サインテーブルのバッファサイズ #endif // SIN_WAVE_DEBUG // [WAV ファイル関連] // Wave File Chunk ID #define CHUNK_ID_TO_INT(a,b,c,d) ((a<<0)|(b<<8)|(c<<16)|(d<<24)) #define CHUNK_ID_RIFF CHUNK_ID_TO_INT('R','I','F','F') #define CHUNK_ID_WAVE CHUNK_ID_TO_INT('W','A','V','E') #define CHUNK_ID_FORMAT CHUNK_ID_TO_INT('f','m','t',' ') #define CHUNK_ID_DATA CHUNK_ID_TO_INT('d','a','t','a') // Wave File Spec #define PCM_CENTER (0x8000) #define PCM_CENTER_M (PCM_CENTER) #define PCM_CENTER_S (((PCM_CENTER_M)<<1)) #define PCM_BIT_SHIFT_M (14) #define PCM_BIT_SHIFT_S (PCM_BIT_SHIFT_M+1) // [キュー関連] #define QUEUE_BUFFER_SIZE 0x1000 #define QUEUE_BUFFER_MIN_BUFFERING 0x0C00 // [[[ 型定義 ]]] // [WAV ファイル関連] struct chunk_inf_t // チャンク構造の共用体 { struct { uint32_t id; uint32_t size; uint32_t fmt_id; } riff_inf; struct { uint32_t id; uint32_t size; uint16_t format; uint16_t channels; uint32_t sample_freq; uint32_t bytes_per_sec; uint16_t block_size; uint16_t bits_per_sample; } fmt_inf; struct { uint32_t id; uint32_t size; } data_inf; } chunk_inf; // [キュー関連] typedef uint16_t queue_data_t; // キューデータの型 // [[[ 変数 ]]] bool reading_wav_file = false; Adafruit_ZeroTimer zero_timer(3); // デバッグ用の サイン波作成 #ifdef SIN_WAVE_DEBUG uint32_t sinWaveFreq = SIN_SAMPLE_FREQ*2; uint16_t sin_table[SIN_TABLE_SIZE]; #endif // SIN_WAVE_DEBUG // [キュー関連] TakSanQueue<queue_data_t,QUEUE_BUFFER_SIZE> queue; uint32_t QueuePutErrorCount = 0; uint32_t QueueGetErrorCount = 0; uint32_t QueueGetCount = 0; // [SD-Card関連] Sd2Card card; SdVolume volume; SdFile root; File myFile; // [[[ 関数プロトタイプ ]]] void TC3_Handler(); void setup_zero_timer(float freq); void sd_card_info(void); void setup(); void loop(); void play_wav_file(char* file_name); void WavePlay_Handler(void); #ifdef SIN_WAVE_DEBUG void SinWavePlay_Handler(void); void make_sin_table(void); #endif // SIN_WAVE_DEBUG void TC3_Handler(){ Adafruit_ZeroTimer::timerHandler(3); } // 割り込みタイマーの設定 (Adafruit ライブラリのサンプルソース) // https://github.com/adafruit/Adafruit_ZeroTimer/blob/master/examples/timer_callback/timer_callback.ino // を参考にしている。 void setup_zero_timer(float freq) { // Set up the flexible divider/compare uint8_t divider = 1; uint16_t compare = 0; tc_clock_prescaler prescaler = TC_CLOCK_PRESCALER_DIV1; if ((freq < 24000000) && (freq > 800)) { divider = 1; prescaler = TC_CLOCK_PRESCALER_DIV1; compare = 48000000/freq; } else if (freq > 400) { divider = 2; prescaler = TC_CLOCK_PRESCALER_DIV2; compare = (48000000/2)/freq; } else if (freq > 200) { divider = 4; prescaler = TC_CLOCK_PRESCALER_DIV4; compare = (48000000/4)/freq; } else if (freq > 100) { divider = 8; prescaler = TC_CLOCK_PRESCALER_DIV8; compare = (48000000/8)/freq; } else if (freq > 50) { divider = 16; prescaler = TC_CLOCK_PRESCALER_DIV16; compare = (48000000/16)/freq; } else if (freq > 12) { divider = 64; prescaler = TC_CLOCK_PRESCALER_DIV64; compare = (48000000/64)/freq; } else if (freq > 3) { divider = 256; prescaler = TC_CLOCK_PRESCALER_DIV256; compare = (48000000/256)/freq; } else if (freq >= 0.75) { divider = 1024; prescaler = TC_CLOCK_PRESCALER_DIV1024; compare = (48000000/1024)/freq; } else { Serial.println("Invalid frequency"); while (1) delay(10); } Serial.print("Divider:"); Serial.println(divider); Serial.print("Compare:"); Serial.println(compare); Serial.print("Final freq:"); Serial.println((int)(48000000/compare)); zero_timer.enable(false); zero_timer.configure(prescaler, // prescaler TC_COUNTER_SIZE_16BIT, // bit width of timer/counter TC_WAVE_GENERATION_MATCH_PWM // frequency or PWM mode ); zero_timer.setCompare(0, compare); } // 初期化関数 void setup() { Serial.begin(115200); pinMode(PIN_ERROR_LED,OUTPUT); //this configures the LED pin, you can remove this it's just example code pinMode(PIN_DAC,OUTPUT); digitalWrite(PIN_ERROR_LED, LOW); delay(500); Serial.println("initialization Start!"); if (!SD.begin(PIN_SDCARD_CS)) { Serial.println("initialization failed!"); while (1); } Serial.println("initialization done."); } // 定期処理ループ void loop() { #ifdef SIN_WAVE_DEBUG play_sin_wave(); delay(500); #else // SIN_WAVE_DEBUG play_wav_file("wav_pcm/EineKlei.wav"); delay(500); play_wav_file("wav_pcm/Saikai.wav"); delay(500); play_wav_file("wav_pcm/GirlsJst.wav"); delay(500); play_wav_file("wav_pcm/SnMik22S.wav"); delay(500); play_wav_file("wav_pcm/I_LV_22M.WAV"); delay(500); play_wav_file("wav_pcm/CanonInD.wav"); delay(500); play_wav_file("wav_pcm/HotelCal.wav"); delay(500); #endif // SIN_WAVE_DEBUG } // WAV ファイルの再生 void play_wav_file(char* file_name) { bool file_ok = true; uint32_t ReadSize; int available; uint32_t ChunkID; int16_t Data[2]; // キューエラー情報を初期化しておく queue_data_t QueueData; QueuePutErrorCount = 0; QueueGetErrorCount = 0; QueueGetCount = 0; Serial.printf("Playing WAV PCM file:%s\r\n", file_name); myFile = SD.open(file_name, FILE_READ); // ファイルの読み出し処理 do { if (NULL == myFile) { Serial.printf("Can't open file : %s\r\n", file_name); file_ok = false; break;} memset(&chunk_inf, 0x00, sizeof(chunk_inf)); available = myFile.available(); if (0 == available) { Serial.printf("RIFF No data: remain_size=%d\r\n", available); file_ok = false; break;} // [RIFF Header] myFile.read(&chunk_inf.riff_inf, sizeof(chunk_inf.riff_inf)); if (chunk_inf.riff_inf.id != CHUNK_ID_RIFF) { Serial.printf("RIFF ID err: %xh\r\n", chunk_inf.riff_inf.id); file_ok = false; break;} if (chunk_inf.riff_inf.fmt_id != CHUNK_ID_WAVE) { Serial.printf("WAVE ID err: %xh\r\n", chunk_inf.riff_inf.fmt_id); file_ok = false; break;} // ファイルをチャンク単位で読んでいく do { myFile.read(&ChunkID, sizeof(ChunkID)); switch(ChunkID) { // フォーマットデータの場合は、整合性チェックとフォーマットの情報表示をする case CHUNK_ID_FORMAT: chunk_inf.fmt_inf.id = ChunkID; myFile.read(&chunk_inf.fmt_inf.size, (sizeof(chunk_inf.fmt_inf) - sizeof(ChunkID) )); if (chunk_inf.fmt_inf.size != 0x0010) { Serial.printf("FORMAT not PCM err: %xh\r\n", chunk_inf.fmt_inf.size); file_ok = false; break;} if (chunk_inf.fmt_inf.format != 0x0001) { Serial.printf("FORMAT not PCM err: %xh\r\n", chunk_inf.fmt_inf.format); file_ok = false; break;} if ((chunk_inf.fmt_inf.channels != 0x0001)&&(chunk_inf.fmt_inf.channels != 0x0002)) { Serial.printf("FORMAT channel err: %d\r\n", chunk_inf.fmt_inf.channels); file_ok = false; break;} if (chunk_inf.fmt_inf.sample_freq > 44100) { Serial.printf("FORMAT sample freq err: %d\r\n", chunk_inf.fmt_inf.sample_freq); file_ok = false; break;} if ((chunk_inf.fmt_inf.block_size != 2)&&(chunk_inf.fmt_inf.block_size != 4)) { Serial.printf("FORMAT block size err: %d\r\n", chunk_inf.fmt_inf.block_size); file_ok = false; break;} if (chunk_inf.fmt_inf.bits_per_sample != 16) { Serial.printf("FORMAT bits per sample err: %d\r\n", chunk_inf.fmt_inf.bits_per_sample); file_ok = false; break;} break; // チャンクの種類がデータなら、ファイルをチャンクのサイズ分読み出して終了 case CHUNK_ID_DATA: chunk_inf.data_inf.id = ChunkID; myFile.read(&chunk_inf.data_inf.size, (sizeof(chunk_inf.data_inf) - sizeof(ChunkID) )); break; default: myFile.read(&ReadSize, sizeof(ReadSize)); Serial.printf("Skip Invalid Chunk :%c%c%c%c(size=%xh)", (ChunkID>>0)&0xff, (ChunkID>>8)&0xff, (ChunkID>>16)&0xff, (ChunkID>>24)&0xff, ReadSize); myFile.seek(myFile.position()+ReadSize); break; } if ( false == file_ok ) break; } while( (chunk_inf.fmt_inf.id != CHUNK_ID_FORMAT) || (chunk_inf.data_inf.id != CHUNK_ID_DATA) ); available = myFile.available(); if (0 == available) { Serial.printf("DATA No data: %xh expected %xh\r\n", available, chunk_inf.data_inf.size); /*break;*/} } while(0); // ファイルを正常に読み出せた場合再生処理開始する if (file_ok) { Serial.println("WAV file Header OK!"); setup_zero_timer((float)chunk_inf.fmt_inf.sample_freq); zero_timer.setCallback(true, TC_CALLBACK_CC_CHANNEL0, WavePlay_Handler); // set DAC in the callback Serial.println("WAV start buffering!"); reading_wav_file = true; // モノラルデータの再生 if (1==chunk_inf.fmt_inf.channels) { // 再生が途切れないようにまずはキューにバッファリング ReadSize = min(QUEUE_BUFFER_MIN_BUFFERING, (myFile.size() >> 1)); for (int i = 0; (i<ReadSize && myFile.available()); i++) { // ファイル読み出し側は読み出したら次々とデータをキューに入れていくのみ。 // 再生が途切れない程度まではバッファリングするだけで、割り込みはまだ開始しない。 myFile.read(&Data, (sizeof(Data)>>1)); QueueData = (uint16_t)( ( (ANALOG_CENTER) * ( (long int)Data[0] - PCM_CENTER) * DIGITAL_VOLUME_STEP ) >> (PCM_BIT_SHIFT_M + DIGITAL_VOLUME_BITS) ); if ( false == queue.Put(&QueueData)) { digitalWrite(PIN_ERROR_LED, HIGH); QueuePutErrorCount++; } else digitalWrite(PIN_ERROR_LED, LOW); } // バッファリングできたら再生しながら読み出し Serial.println("WAV start playing!"); zero_timer.enable(true); while (myFile.available()) { // ファイル読み出し側は読み出したら次々とデータをキューに入れていくのみ。 // 割り込み側で定期的にキューから取得してDACに出力することで再生する仕組み。 myFile.read(&Data, (sizeof(Data)>>1)); QueueData = (uint16_t)( ( (ANALOG_CENTER) * ( (long int)Data[0] - PCM_CENTER) * DIGITAL_VOLUME_STEP ) >> (PCM_BIT_SHIFT_M + DIGITAL_VOLUME_BITS) ); while ( false == queue.Put(&QueueData)) { QueuePutErrorCount++; digitalWrite(PIN_ERROR_LED, HIGH); //Serial.print("x"); delay(10); } } } // ステレオデータの再生 else if (2==chunk_inf.fmt_inf.channels) { // 再生が途切れないようにまずはキューにバッファリング ReadSize = min(QUEUE_BUFFER_MIN_BUFFERING, (myFile.size() >> 2)); for (int i = 0; (i<ReadSize && myFile.available()); i++) { // ファイル読み出し側は読み出したら次々とデータをキューに入れていくのみ。 // 再生が途切れない程度まではバッファリングするだけで、割り込みはまだ開始しない。 myFile.read(&Data, sizeof(Data)); QueueData = (uint16_t)( ( (ANALOG_CENTER) * ( (long int)Data[0] + (long int)Data[1] - ( 2 * PCM_CENTER) ) * DIGITAL_VOLUME_STEP ) >> (PCM_BIT_SHIFT_S + DIGITAL_VOLUME_BITS) ); if ( false == queue.Put(&QueueData)) { QueuePutErrorCount++; digitalWrite(PIN_ERROR_LED, HIGH); } else digitalWrite(PIN_ERROR_LED, LOW); } // バッファリングできたら再生しながら読み出し Serial.println("WAV start playing!"); zero_timer.enable(true); while (myFile.available()) { // ファイル読み出し側は読み出したら次々とデータをキューに入れていくのみ。 // 割り込み側で定期的にキューから取得してDACに出力することで再生する仕組み。 myFile.read(&Data, sizeof(Data)); QueueData = (uint16_t)( ( (ANALOG_CENTER) * ( (long int)Data[0] + (long int)Data[1] - ( 2 * PCM_CENTER) ) * DIGITAL_VOLUME_STEP ) >> (PCM_BIT_SHIFT_S + DIGITAL_VOLUME_BITS) ); while ( false == queue.Put(&QueueData)) { QueuePutErrorCount++; digitalWrite(PIN_ERROR_LED, HIGH); //Serial.print("x"); delay(10); } } } reading_wav_file = false; Serial.println("PCM file read end: waiting wave play finish!"); delay(100); while( queue.DataExists()) { delay(100); // Serial.print("."); } zero_timer.enable(false); Serial.println("Done!"); if (QueuePutErrorCount) Serial.printf("Put Err = %xh\r\n", QueuePutErrorCount); if (QueueGetErrorCount) Serial.printf("Get Err = %xh\r\n", QueueGetErrorCount); if (QueueGetCount) Serial.printf("Get = %xh\r\n\r\n", QueueGetCount); myFile.close(); } } // Wave 再生の割り込み側の処理 void WavePlay_Handler(void) { queue_data_t Data; // キューから取り出して、DACに食わせるだけ() if (false == queue.Get(&Data)) { // キューが空になってしまったらセンター値にして無音にする(ここに来るケースは読み出しが追いついていない) analogWrite(PIN_DAC, ANALOG_CENTER); if (false == reading_wav_file) { QueueGetErrorCount++; digitalWrite(PIN_ERROR_LED, HIGH); } } else { // 正常にキューから読めたら、DACに食わせる analogWrite(PIN_DAC, Data); } } #ifdef SIN_WAVE_DEBUG // サイン波再生のために、先にテーブルを計算しておく(浮動小数点演算のリアルタイムでの演算はサンプリング周期に追いつかない為) void make_sin_table(void) { int i; float level; for (i = 0; i < SIN_TABLE_SIZE; i++) { // level = (float)((ANALOG_CENTER) + ( (float)(ANALOG_MAX>>1) * (float)sin( 3.1415926f * 2.0f * (float)i / (float)SIN_TABLE_SIZE)))/1.5f; level = (float)((ANALOG_CENTER) + ( ( ((float)ANALOG_CENTER * (float)DIGITAL_VOLUME_STEP / (float)DIGITAL_VOLUME_STEP_MAX ) * ( (float)(ANALOG_MAX>>1) * (float)sin( 3.1415926f * 2.0f * (float)i / (float)SIN_TABLE_SIZE))))); sin_table[i]= (uint16_t) level; } } // サイン波の再生 void play_sin_wave(void) { Serial.println("Making SIN Table"); make_sin_table(); zero_timer.configure(TC_CLOCK_PRESCALER_DIV1, // prescaler TC_COUNTER_SIZE_16BIT, // bit width of timer/counter TC_WAVE_GENERATION_MATCH_PWM // match style ); zero_timer.setCompare(0, (uint16_t)((float)48000000.0f/(float)sinWaveFreq)); // 1 match, channel 0 zero_timer.setCallback(true, TC_CALLBACK_CC_CHANNEL0, SinWavePlay_Handler); // set DAC in the callback zero_timer.enable(true); Serial.println("Playing SIN WAV"); delay(5000); zero_timer.enable(false); } // サイン波の再生の割り込み側の処理 void SinWavePlay_Handler(void) { static int counter = 0; analogWrite(PIN_DAC, sin_table[counter]); if (counter < ( SIN_TABLE_SIZE -1 ) ) counter++; else counter = 0; } #endif // SIN_WAVE_DEBUG ``` ```C:TakSanQueue.h #ifndef TakSanQueue_h #define TakSanQueue_h template <class DataType, int _Size = 1024> class TakSanQueue { public: TakSanQueue(void) { Clear(); } void Clear(void) { _PutPos = 0; _GetPos = 0; memset(_DataBuffer, 0, sizeof(_DataBuffer)); } bool Put(DataType* data) { unsigned int NextPos = (_PutPos < _Size) ? _PutPos + 1 : 0; if (NextPos == _GetPos) return (false); _DataBuffer[_PutPos] = *data; _PutPos = NextPos; return (true); } bool Get(DataType* data) { unsigned int NextPos; if (_GetPos == _PutPos) return (false); NextPos = (_GetPos < _Size) ? _GetPos + 1 : 0; *data = _DataBuffer[_GetPos]; _GetPos = NextPos; return (true); } bool DataExists(void) { if (_GetPos == _PutPos) return (false); return (true); } private: unsigned int _PutPos; unsigned int _GetPos; // unsigned int _PutErrorCount = 0; // unsigned int _GetErrorCount = 0; // unsigned int _GetCount = 0; DataType _DataBuffer[_Size]; }; #endif //TakSanQueue_h ``` # 最後に 何とか小さいも卵というものに、いい感じに組み込めました。 基板事作成すればかなり小型化でき、様々なものに組み込めるかと思います。 そういう意味では「小型のものに鳴り物を仕込む実績を作りたい」という当初の目的は果たせたと思います。 参考にして何かつくるのであれば、同じように卵に仕込む必要はないと思います。色々考えてみてください。 おもちゃとか、100円ショップのグッズとかに鳴り物を組み込んでみようとおもっています。