TanaRadio
spotify apple_podcasts amazon_music
L

TanaRadio

たな 434 Episodes
たな
チャッピー
ジェームズ
サリー
ひとみ
エディ

👑 現在のトップリスナー

私立工科系大学教員(科学史)の日々の出来事や雑感。日記,メモ,ブログなど
TanaRadio用語集もどうぞ: https://sitetanaka.net/tana_lexicon/tana_lexicon.html

391 日記 | TanaRadio Pi製作の現状と今学期の授業の振り返り

391 日記 | TanaRadio Pi製作の現状と今学期の授業の振り返り

Aug 4, 2026 25:04 たな

TanaRadio Pi 開発過程まとめ――思いつきからフェーズ5Nまでの51日間――作成:2026年8月3日 対象期間:2026年6月13日〜8月2日 0.全体の要約2026年6月13日、「ポッドキャストをラジオ受信機で聴きたい」という思いつきから始まりました。翌14日に部品を注文し、以後ほぼ毎日、30分程度の作業を積み重ねています。51日間の到達点は次のとおりです。電源を入れると自動で起動し、スピーカーから声が流れる8つの「棚」を、つまみを回して切り替えられる再生・停止・前後の移動も、つまみ1本で操作できる画面はなく、状態は2つのLEDの点滅で表す制作過程は声日記として33本配信され、記録そのものが作品の一部になっています。1.時系列第I期:小さなパソコンにする(6月13日〜21日)日付出来事6/13ラジオを作ることを思いつく6/14Raspberry Pi 5ほかを注文(357)6/16開封の儀(358)6/17microSDにOSを書き込む(359)6/18起動・Wi-Fi・音出しを確認(フェーズ1・2完了)(360)6/19ボタン・つまみ類を追加注文(361)6/21電源ONでTanaRadioの画面が自動表示(フェーズ3完了)(362)フェーズ3では Unlock keyring のパスワード入力に手こずり、Seahorseでキーリングのパスワードを空にして解決しました。最初の「詰まりどころ」です。第II期:ボタンで操作する(6月24日〜28日)日付出来事6/24GPIO17に1ボタンだけ配線して反応を確認(365)6/253ボタンに増やし、playerctlでブラウザ再生を操作(366)6/28Chromiumを捨ててmpvへ移行(フェーズ4完了)(368)ブラウザ経由からmpv+Unixソケット制御へ移ったことが、以後すべての土台になりました。この段階で「RSSから音声URLを取り出す」という、現在まで続く基本動作が入っています。第III期:ラジオの文法を作る(6月29日〜7月12日)6月29日、フェーズ5を9段階(5A〜5I)に分割する計画を立てました(369)。以後、ほぼ1日1段階のペースで進みます。日付段階内容6/305ARSS1本からプレイリストを生成(370)7/15B過去3日分を古い順に並べる(371)7/25C複数RSSの統合(372)7/45D3ボタンでプレイリストを操作(374)7/55E「棚」ファイルの導入(5棚)(375)7/65F棚選択ボタン+LED1(376)7/85G再生モードボタン+LED2(377)7/9―音量ロータリーエンコーダー(378)7/10―Raspberry Piをメタルケースへ(379)7/12―systemdによる自動起動(380)7月5日に導入した**「棚」がこのプロジェクト固有の発明**です。番組やエピソードではなく、複数のRSSをまとめた単位で選ぶ。「声の本棚」という当初からの比喩が、ここでファイル構造として実装されました。7月12日、電源を入れるだけで音が出るようになり、機械としての基本形が完成しています。第IV期:基板化と筐体の芽(7月13日〜21日)日付出来事7/13実体配線図を作成。ブレッドボードのまま籠に入れた状態を「カゴラジオ」と呼ぶ(381)7/14基板が想定より小さく、配置を変更(382)7/15ピンヘッダーとグラウンド線をハンダ付け(383)7/16スイッチ1個+抵抗1個。1つずつ動作確認する方式を確立(384)7/19ボタン3個→5個へ(386・387)7/20LED2個を追加。紙箱に載せて「箱ラジオ」に(388)7/21ロータリーエンコーダーを取り付け、基板化完了(389)この期間の技術的な山場は、隣り合うランドをハンダでつなぐ作業でした。「抵抗のリード線を長いまま先に固定し、あとから切る」という手順に行き着いたのが7月19日です。手順の発見が、道具の腕前より効いた局面でした。第V期:ソフトウェアの成熟と操作系の再設計(7月25日〜8月2日)日付段階内容7/255J音量の永続化(棚を変えても音量が戻らない)7/255K内部構造の整理6項目、起動スクリプトのバージョン非依存化7/265LRSS並列取得。棚切り替えが約20秒→1.7秒7/26―ALSAミキサーを100%へ(音量問題の真因)7/265M棚選択をロータリーエンコーダー化。棚ボタン廃止8/25N再生操作をロータリーエンコーダー1本に統合。3ボタン廃止5Mで導入した**「確定の二段構え」**(回転が止まって0.8秒でLED表示、2.5秒でRSS再取得)は、つまみを回して局を探し、手を止めると音が入ってくるという、古いラジオの同調に近い動作を生みました。5Nでは回転を0.5秒ためて一度で移動する方式とし、playlist-pos を読んで直接飛ぶようにしています。mpvから値を「読む」処理は、5Nで初めて書いた種類のコードです。2.操作系の変遷部品の総数は増えているのに、操作の種類は減り続けました。時点構成7月19日(5G相当)5ボタン・2ランプ・1ダイヤル7月26日(5M)4ボタン・2ランプ・2ダイヤル8月2日(5N)1ボタン・2ランプ・3ダイヤル5Nの時点で、操作は次のように整理されています。回すもの=4つの軸:棚(何を)/モード(どの順で)/エピソード(どこを)/音量(どれくらい)押すもの=唯一の動詞:再生/一時停止残った1つの押しボタン(再生モード選択)も、フェーズ5Oでつまみに置き換える予定です。3.思想の展開技術と並行して、「なぜこれを作るのか」の言語化が進みました。日付内容6/21363「作る意味とは」 ChatGPTに問い、「音声が現れるまでの過程を、自分の手と場所と機械に取り戻すこと」という答えを得る6/26367「ほどよいラジオ」 鉱石ラジオ(自作性)・自由ラジオ(場所性)・イリイチのコンヴィヴィアリティ(ほどよさ)の三点で位置づける7/3373 講義「ほどよい技術」 「科学と技術の社会史」の教材にTanaRadio Piを組み込む。適正技術の実例として提示7/13381 「何が出てくるかわからない」聴き方の面白さを語る7/19385 テックLT 「たまたま聴けるものを聴く」という聴き方の発見と、スモールステップの有効性を報告コンヴィヴィアリティに「ほどよい」という訳語を当てたのが6月26日です。この語は講義にも持ち込まれ、制作が教育実践に折り返しています。7月26日の総合報告書では、四つの概念に階層をつけました。「声の本棚」だけがこの装置固有の発明であり、鉱石ラジオは「作る過程」、イリイチは「作った後の暮らし方」、自由ラジオは「場所性(TanaFMまで未着手)」という分担です。4.一貫していた作り方51日間を通して守られた原則は、次の4つに集約できます。1. スモールステップ。 1日30分でできる単位に割る。フェーズ5を9段階に分けたのが典型です。「一遍に高機能のものを作ろうとすると大体うまくいかない」(381)。2. 動いたものは残す。 5Aのスクリプトを消さずに5Bを作る。フェーズごとにファイル名を変える。戻れる場所を必ず用意しています。3. ハードを増やす前にソフトで仕組みを固める。 5Mの棚エンコーダー化が少ない実装量で済んだのは、連続押しをまとめるタイマー処理をボタン用として先に作ってあったためです。4. 組み込む前に単体で測る。 棚エンコーダー(7/26)と再生エンコーダー(8/2)は、いずれもブレッドボードで測定してから本体に入れました。1クリック=1ステップであること、押し込みで軸が回らないことは、測って初めて分かっています。そして全体を通じて、AIが先生役、判断は人間という関係が保たれました。手順書はAIが書きますが、部品を選び、ハンダを当て、仕様を決め、失敗の原因を突き止めるのは人間の側です。なお7月下旬に、開発支援AIがChatGPTからClaudeへ交代しています。作り方の原則は引き継がれましたが、フェーズ番号の規約だけが落ちました(そのため,フェーズ番号がこのまとめでは途中で飛んでいます:5G→5J)。5.主な失敗と、そこで分かったこと症状真因教訓起動時にパスワードを聞かれるキーリングの仕様自動化は「動いているもの」に対してのみ行う音量の数値は変わるのに音が変わらないmpvの内部音量が実出力に届いていない音の経路は何段もある番組によって音が小さすぎるALSA側が70%で頭打ち同じ症状でも原因の層が違うGPIO busy で起動しない起動スクリプトが旧版を呼び二重起動手動実行の前に必ずサービスを止める棚切り替えに20秒の沈黙RSSを1本ずつ取得していた複雑な仕組みを足す前に単純な原因を疑うLEDが8番を表示中に追従できないtime.sleep は止められない世代番号で中断可能にするこれらの記録は備忘録ではなく、「ブラックボックスが開く瞬間」の一次資料として意識的に残されています。抽象的に「技術は不透明だ」と語るより、具体的な症状と原因の層を示すほうが教材として強い、という判断です。6.現在地と次現在地(8月3日)操作系はほぼ形になり、日常利用による評価の段階です。次の点を観察しています。PLAY_ACCUM_DELAY(0.5秒)が体に合っているか左回し(戻る)を実際に使うかモードボタンを何回触ったか(=5Oの設計判断そのもの)次の予定5O:モード選択のロータリー化+I2S用のGPIO再配線(棚A相 19→17、音量A相 21→22)筐体(木製ケーブルボックス+4mmシナ合板パネル)I2Sアンプ(MAX98357A)への移行電源操作(J2パッド)#声日記 #TanaRadioPi

390 メモ | 学生が手抜きをしないようになるには

390 メモ | 学生が手抜きをしないようになるには

Jul 28, 2026 04:32 たな

今学期の私の授業のキーワードは「ひっかかり」「違和感」でした。講義に関するコメントシートの設問1はこんな感じでした:設問1:第○週の講義ビデオを観て,「なんか引っかかる」「すっきりしない」「どうも納得できない」と感じたことを,問いの形で書いてください。また,なぜそこが引っかかったのか,その理由や背景も書いてください。なお,その問いの答えを書く必要はありません。前週コメントシート共有の時間のワークでは次のような指示を出しました:教員の合図により,リンク「相互コメントワークを始める」をクリックし,出てきた3つの投稿(「問い」)にコメント(「一応の答え」と「でも」)を付ける

389 日記 | 基板化を完了

389 日記 | 基板化を完了

Jul 21, 2026 23:44 たな

音量調節用のロータリーエンコーダーを取り付け,これで一通り部品を基板に取り付けました。ごちゃごちゃした配線はみな箱の中に納めてすっきりしました。#声日記 #TanaRadioPi

388 日記 | 箱ラジオができました

388 日記 | 箱ラジオができました

Jul 20, 2026 28:28 たな

ユニバーサル基板にLED 2個と抵抗2個をハンダ付けして,選んでいる棚とモードがLEDの点滅回数でわかるようになりました。さらに,基板を紙箱に取り付け,Raspberry Piのケースに被せて,「箱ラジオ」ができました。#声日記 #TanaRadioPi

387 日記 | 押しボタンを5つに増やす

387 日記 | 押しボタンを5つに増やす

Jul 19, 2026 29:28 たな

基板化作業の続き。前回同様,タクトスイッチ2個と抵抗2個をハンダ付けして,棚選びとモード選びができるようにしました。残る部品はLED 2個とロータリーエンコーダーです(録音では,ロータリーエンコーダーだけのように言っていますが,間違いです)。#声日記 #TanaRadioPi

386 日記 | 押しボタンを3つに増やす

386 日記 | 押しボタンを3つに増やす

Jul 19, 2026 31:01 たな

基板化作業の続き。タクトスイッチ2個と抵抗2個をハンダ付けして,プレイリスト上のエピソードを進めたり戻したりできるようにしました。#声日記 #TanaRadioPi

385 ポッドキャスト受信機TanaRadio Piを自作すること得たで2つの発見 | テックLTポッドキャストカレンダー

385 ポッドキャスト受信機TanaRadio Piを自作すること得たで2つの発見 | テックLTポッドキャストカレンダー

Jul 19, 2026 05:00 たな

第6回 テックLTポッドキャストカレンダー参加エピソードです(7月19日分)。約1ヶ月前の6月13日に始めたポッドキャスト受信機「TanaRadio Pi」の製作を通して発見した2つのことについて語っています。*TanaRadio Piの製作日記をTanaRadioで配信しています。→ #TanaRadioPi現在のTanaRadio Pi (愛称: カゴラジオ)参考:301 AIによる堅実な開発の20日間 | テックLTポッドキャストカレンダー#テックLTポッドキャストカレンダー

384 日記 | 基板へのハンダ付けとスイッチ動作確認

384 日記 | 基板へのハンダ付けとスイッチ動作確認

Jul 16, 2026 14:33 たな

基板への部品ハンダ付けの続き。今日はタクトスイッチ1つと抵抗1つを取り付け,スイッチの動作確認をしました。#声日記 #TanaRadioPi

383 日記 | 基板に部品をハンダ付け

383 日記 | 基板に部品をハンダ付け

Jul 15, 2026 32:43 たな

基板に部品をハンダ付けする作業を始めました。まずは,ピンヘッダーとグラウンド線です。#声日記 #TanaRadioPi

382 日記 | 基板に部品を配置

382 日記 | 基板に部品を配置

Jul 14, 2026 03:00 たな

昨日作った実体配線図通りには基板に置けなかったので,一部配置を変えました。#声日記 #TanaRadioPi

381 日記 | 回路図を書いてみました

381 日記 | 回路図を書いてみました

Jul 13, 2026 18:21 たな

2016年7月13日月曜日の声日記。TanaRadio Piの回路図(正確には実体配線図)など。カゴラジオ#声日記 #TanaRadioPi

380 日記 | 電源オンで自動起動

380 日記 | 電源オンで自動起動

Jul 12, 2026 28:25 たな

電源オンで音が出るようにしました。見かけはともかく,機能的にはほぼ期待通りのモノができました(フェーズ5J完了)。フェーズ5J 詳細マニュアル 電源ONでTanaRadio Piを自動起動する 0. 今回の目標 今回の完成イメージはこれです。 Raspberry Piの電源を入れる ↓ 自動で tanaradio_5i.py が起動する ↓ 現在の棚・再生モードをLEDで表示する ↓ プレイリストを作る ↓ mpvが起動する ↓ USBスピーカーから音が出る ↓ ボタンと音量ダイヤルで操作できる 今回は、ケースにはまだ入れません。 まずは、 モニターなし キーボードなし マウスなし ターミナル操作なし で動くかを確認します。 1. 今回使う方式 今回は systemd という仕組みを使います。 これは、Raspberry Piの起動時に自動でプログラムを立ち上げるための標準的な方法です。 今回作るものは2つです。 1. 起動用シェルスクリプト run_tanaradio_5j.sh 2. systemdサービス設定 tanaradio.service 直接 tanaradio_5i.py を自動起動してもよいのですが、間にシェルスクリプトを置いた方が安全です。 理由は、起動直後はWi-Fiや音声出力の準備がまだ終わっていないことがあるからです。少し待ってからPythonを起動するようにします。 2. まず手動実行で現在の状態を確認する Raspberry Piでターミナルを開きます。 cd ~/tanaradio5 ls 次のようなファイルがあることを確認します。 tanaradio_5i.py shelf_01_voice_life.txt shelf_02_tech_ai_mono.txt shelf_03_education_research.txt shelf_04_books_culture.txt shelf_05_tanaradio.txt 次に、いつものように手動実行します。 python3 tanaradio_5i.py ここで、これまで通り音が出て、ボタンと音量ダイヤルが効けばOKです。 終了するときは、 Ctrl + C です。 ここが成功していない場合は、自動起動に進まないでください。 自動起動は「動いているものを自動化する」段階です。動いていないものを自動化すると、エラーが忍者のように見えなくなります。 3. USBスピーカーを確認する USBスピーカーが認識されているか確認します。 aplay -l たとえば次のような表示が出れば、USBスピーカーが見えています。 card 1: Device [USB Audio Device], device 0: USB Audio [USB Audio] 表示名は機種によって違います。 USB Audio や Device のような文字があれば、だいたいOKです。 次にテスト音を鳴らします。 speaker-test -t wav -c 2 左右からテスト音が出ればOKです。 止めるときは、 Ctrl + C です。 4. mpvだけでUSBスピーカーから音が出るか確認する すでに playlist_5i.m3u または playlist_5g.m3u のようなプレイリストがある場合は、それを使って確認します。 まず、作業フォルダを確認します。 cd ~/tanaradio5 ls *.m3u 表示されたプレイリスト名に合わせて、たとえば次を実行します。 mpv --no-video playlist_5g.m3u または、 mpv --no-video playlist_5i.m3u 音が出ればOKです。 止めるときは、 q です。 もしプレイリスト名がわからなければ、あとで tanaradio_5i.py が自動で作るので、ここは深追いしなくて大丈夫です。 5. 起動用シェルスクリプトを作る 次に、自動起動用のシェルスクリプトを作ります。 cd ~/tanaradio5 nano run_tanaradio_5j.sh 次をそのまま貼り付けます。 #!/bin/bash # TanaRadio Pi フェーズ5J 起動用スクリプト cd /home/tana/tanaradio5 || exit 1 # 起動直後はWi-Fiや音声出力の準備が終わっていないことがあるため、少し待つ sleep 12 # Pythonのprintをログにすぐ出す export PYTHONUNBUFFERED=1 /usr/bin/python3 /home/tana/tanaradio5/tanaradio_5i.py 保存します。 Ctrl + O Enter Ctrl + X 実行権限を付けます。 chmod +x run_tanaradio_5j.sh 6. 起動用スクリプトを手動で試す まだ自動起動にはしません。 先に、このスクリプト自体が動くか確認します。 ./run_tanaradio_5j.sh 12秒ほど待ってから、tanaradio_5i.py が起動するはずです。 音が出て、ボタンとダイヤルが効けばOKです。 終了するときは、 Ctrl + C です。 7. systemdサービスファイルを作る 次に、Raspberry Pi起動時にこのスクリプトを自動実行する設定を作ります。 sudo nano /etc/systemd/system/tanaradio.service 次をそのまま貼り付けます。 [Unit] Description=TanaRadio Pi Phase 5J Wants=network-online.target After=network-online.target sound.target [Service] Type=simple User=tana WorkingDirectory=/home/tana/tanaradio5 ExecStart=/home/tana/tanaradio5/run_tanaradio_5j.sh Restart=on-failure RestartSec=5 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target 保存します。 Ctrl + O Enter Ctrl + X ここでは User=tana としています。 たなさんのRaspberry Piのユーザー名が tana なので、このままでよいはずです。 8. systemdに設定を読み込ませる sudo systemctl daemon-reload ここではまだ自動起動は有効化されていません。 設定ファイルをRaspberry Piに認識させただけです。 9. まず手動でサービス起動する いきなり再起動しないで、まずサービスとして起動します。 sudo systemctl start tanaradio.service 少し待ちます。 12秒後くらいにLEDが点滅し、音が出ればかなり良いです。 状態を確認します。 sudo systemctl status tanaradio.service 画面に次のような表示があればOKです。 Active: active (running) または、すでに何らかの理由で終了していれば、 Active: failed などが出ます。 終了するときは q です。 10. ログを見る 自動起動では、エラーはターミナルに直接出ません。 その代わり、ログを見ます。 journalctl -u tanaradio.service -n 80 --no-pager リアルタイムで見る場合は、 journalctl -u tanaradio.service -f 止めるときは、 Ctrl + C です。 ログに、 TanaRadio Pi フェーズ5I 現在の棚: 現在の再生モード: プレイリストを作り直します mpvを起動しました のような表示が出ていれば、プログラムは動いています。 11. サービスを止める方法 手動で実験したいときは、先にサービスを止めます。 sudo systemctl stop tanaradio.service これは大事です。 サービスが動いている状態で、さらにターミナルから python3 tanaradio_5i.py を実行すると、GPIOやmpvのソケットが二重に使われて、おかしな動きになる可能性があります。 つまり、今後の基本はこうです。 自動起動で使うとき: sudo systemctl start tanaradio.service 手動で実験するとき: sudo systemctl stop tanaradio.service python3 tanaradio_5i.py 12. 自動起動を有効にする サービスとして動くことを確認できたら、起動時に自動実行されるようにします。 sudo systemctl enable tanaradio.service これで、次回の電源ONから自動起動します。 13. 再起動テスト いよいよ、モニターなし運用に近いテストです。 まず、念のため現在のサービスを再起動しておきます。 sudo systemctl restart tanaradio.service 問題なく音が出たら、Raspberry Pi全体を再起動します。 sudo reboot 再起動後、しばらく待ちます。 目安としては、 起動 ↓ Wi-Fi準備 ↓ 12秒待機 ↓ Python起動 ↓ RSS取得 ↓ mpv起動 ↓ 音が出る なので、電源投入から音が出るまで少し時間がかかります。 ここで焦らないのがコツです。ラジオというより、寝起きの小型サーバーです。 14. 再起動後の確認 音が出れば成功です。 次も確認します。 再生/一時停止ボタンが効く 戻るボタンが効く 次へボタンが効く 棚ボタンが効く モードボタンが効く 音量ダイヤルが効く LEDが点滅する これが全部できれば、フェーズ5Jはほぼ成功です。 15. モニターなしで使う前のSSH確認 日常運用では、モニターをつながずに使うことになります。 その場合、問題が起きたときはSSHで入って確認します。 Macなど別のPCから、Raspberry PiにSSH接続します。 ssh tana@raspberrypi.local 入れたら、状態確認はこれです。 sudo systemctl status tanaradio.service ログ確認はこれです。 journalctl -u tanaradio.service -n 80 --no-pager 再起動はこれです。 sudo systemctl restart tanaradio.service 停止はこれです。 sudo systemctl stop tanaradio.service 16. 自動起動を一時的に止める方法 ケース作業や配線変更をするとき、自動でプログラムが起動すると危ない場合があります。 そのときは、自動起動を無効にします。 sudo systemctl disable tanaradio.service いま動いているサービスも止めるなら、 sudo systemctl stop tanaradio.service もう一度自動起動に戻すときは、 sudo systemctl enable tanaradio.service sudo systemctl start tanaradio.service です。 17. よくあるトラブル トラブル1:音が出ない まずサービスを止めます。 sudo systemctl stop tanaradio.service 手動で音が出るか確認します。 cd ~/tanaradio5 python3 tanaradio_5i.py 手動では音が出るが、自動起動では音が出ない場合は、systemd から起動したmpvが音声出力先をうまくつかめていない可能性があります。 その場合は、tanaradio_5i.py の mpv 起動部分に、USBスピーカー向けの指定を追加します。 まず編集します。 nano ~/tanaradio5/tanaradio_5i.py 中で、次のような部分を探します。 cmd = [ "mpv", "--no-video", "--really-quiet", f"--input-ipc-server={MPV_SOCKET}", str(PLAYLIST_FILE), ] これを次のようにします。 cmd = [ "mpv", "--no-video", "--really-quiet", "--ao=alsa", f"--input-ipc-server={MPV_SOCKET}", str(PLAYLIST_FILE), ] 保存します。 Ctrl + O Enter Ctrl + X サービスを再起動します。 sudo systemctl restart tanaradio.service これでUSBスピーカーから出る可能性が高くなります。 トラブル2:起動直後にRSS取得で失敗する ログにRSS取得エラーが出る場合、Wi-Fi接続より先にプログラムが動いている可能性があります。 その場合は、待ち時間を少し伸ばします。 nano ~/tanaradio5/run_tanaradio_5j.sh この行を、 sleep 12 たとえば次のようにします。 sleep 25 保存して、サービスを再起動します。 sudo systemctl restart tanaradio.service トラブル3:GPIO関係のエラーが出る ログにGPIOやpin関係のエラーが出る場合、まず二重起動を疑います。 ps aux | grep tanaradio 複数の tanaradio_5i.py が動いていたら、いったん止めます。 sudo systemctl stop tanaradio.service pkill -f tanaradio_5i.py pkill -f mpv その後、もう一度サービスを起動します。 sudo systemctl start tanaradio.service トラブル4:サービスがすぐ落ちる 状態を見ます。 sudo systemctl status tanaradio.service 詳しいログを見ます。 journalctl -u tanaradio.service -n 120 --no-pager 多くの場合、原因は次のどれかです。 ファイル名が違う 作業フォルダが違う RSS棚ファイルがない GPIOピンが別プロセスに使われている mpvが見つからない 音声出力がつかめない まず確認するコマンドはこれです。 ls -l /home/tana/tanaradio5 which python3 which mpv 18. フェーズ5Jの成功条件 次の5つができれば、フェーズ5J成功です。 1. sudo systemctl start tanaradio.service で起動する 2. USBスピーカーから音が出る 3. 物理ボタンで再生操作できる 4. 音量ロータリーエンコーダーが効く 5. sudo reboot 後にも自動で起動して音が出る ここまで行けば、TanaRadio Piはもう「毎回ターミナルで実行する実験機」ではありません。 かなりはっきり言うと、ここを越えたら ラジオです。 まだ裸のラジオですが、電源を入れたら勝手に声が出る。これはもう道具としての魂が入っています。 次のフェーズ6Aで、ようやくケース配置に進むのがよいです。#声日記 #TanaRadioPi

379 日記 | Raspberry Piをケースに入れる

379 日記 | Raspberry Piをケースに入れる

Jul 10, 2026 33:47 たな

フェーズ5Jに行こうとしましたが,その前にRaspberry Piをケースに入れるところまでで終わってしまいました。*なぜかノイズキャンセリングが効いているようで,とても聴きにくいです。#声日記 #TanaRadioPi

378 日記 | ロータリーエンコーダーで音量調節

378 日記 | ロータリーエンコーダーで音量調節

Jul 9, 2026 31:39 たな

まず,ロータリーエンコーダーをDIP化基板にハンダ付けして,それからこれを使って音量調整できるようにしました(フェーズ5I完了)。フェーズ5I 詳細マニュアル ロータリーエンコーダーで音量調整する 0. 今回の目標 今回追加する機能はこれです。 ロータリーエンコーダーを右に回す ↓ 音量アップ ロータリーエンコーダーを左に回す ↓ 音量ダウン 今回は、エンコーダーの押し込みによるミュートは扱いません。 たなさんの手元のロータリーエンコーダーは3端子タイプなので、 A C / GND / COM B の3本だけを使います。 1. 今回使うGPIO フェーズ5Gでは、次の構成で進んでいる前提にします。 再生/一時停止ボタン:GPIO17 戻るボタン:GPIO27 次へボタン:GPIO22 棚選びボタン:GPIO23 再生モードボタン:GPIO5 棚LED:GPIO24 モードLED:GPIO6 mpv IPCソケット: /tmp/tanaradio-mpv.sock 今回は、ロータリーエンコーダー用に次の2本を追加します。 エンコーダーA:GPIO20 エンコーダーB:GPIO21 エンコーダーC / COM:GND 物理ピンで言うと、Raspberry Piの40ピンヘッダーでは次の並びが使いやすいです。 GPIO20:物理ピン38 GND :物理ピン39 GPIO21:物理ピン40 ここが気持ちよく並んでいます。電子工作の神様が一瞬だけ優しい顔をしています。 gpiozeroの公式ドキュメントでも、ロータリーエンコーダーはA/BをGPIOへ、CつまりcommonをGNDへ接続する形で説明されています。 2. 配線する前にRaspberry Piを終了する まず、現在動いている tanaradio_5g.py があれば Ctrl + C で止めます。 そのあと、Raspberry Piを終了します。 sudo shutdown -h now 緑LEDの点滅が落ち着き、完全に止まったら電源ケーブルを抜きます。 GPIO配線は、必ず電源を抜いてから行います。 特に今回はGPIOが増えるので、ここは慎重でよいです。急がない。ラジオは逃げません。 3. ロータリーエンコーダーを配線する ロータリーエンコーダーの3端子を、次のようにつなぎます。 ロータリーエンコーダー A → GPIO20 ロータリーエンコーダー C → GND ロータリーエンコーダー B → GPIO21 DIP化基板に A、C、B のような表記があれば、それに従います。 もし表記が分かりにくい場合は、中央の端子が C / COM / GND であることが多いです。ただし、ここは部品によって違う可能性もあります。基板に印字があるなら印字を優先してください。 重要なのはこれです。 ロータリーエンコーダーには 5V をつながない 3.3V も今回はつながない C / COM は GND につなぐ A と B を GPIO20 / GPIO21 につなぐ 4. 起動して作業フォルダへ移動する 配線が終わったら、Raspberry Piの電源を入れます。 起動したらターミナルを開きます。 cd ~/tanaradio5 確認します。 pwd 次のように表示されればOKです。 /home/tana/tanaradio5 5. 必要パッケージを確認する 念のため、gpiozero関係を入れておきます。 sudo apt update sudo apt install -y python3-gpiozero python3-lgpio すでに入っていれば、そのままで大丈夫です。 6. ロータリーエンコーダー単体テスト まず、音量制御とは関係なく、エンコーダーを回したらPythonが反応するかだけ確認します。 nano test_encoder_5i.py 次を貼り付けます。 #!/usr/bin/env python3 from gpiozero import RotaryEncoder from signal import pause ENC_A_GPIO = 20 ENC_B_GPIO = 21 encoder = RotaryEncoder( ENC_A_GPIO, ENC_B_GPIO, max_steps=0, bounce_time=0.002 ) def rotated_clockwise(): print("右回し") def rotated_counter_clockwise(): print("左回し") encoder.when_rotated_clockwise = rotated_clockwise encoder.when_rotated_counter_clockwise = rotated_counter_clockwise print("ロータリーエンコーダーのテストを開始します。") print("つまみを左右に回してください。") print("終了するには Ctrl+C を押してください。") pause() 保存します。 Ctrl + O Enter Ctrl + X 実行できるようにします。 chmod +x test_encoder_5i.py 実行します。 ./test_encoder_5i.py つまみを右に回して、 右回し 左に回して、 左回し と出れば成功です。 終了は、 Ctrl + C です。 8. 5Gを動かしたまま音量制御テストをする 次に、tanaradio_5g.py はそのまま使い、別のテストプログラムからmpvへ音量命令を送ります。 mpvはIPCソケットで外部制御できます。mpvの公式マニュアルでも、外部プログラムから制御する場合は --input-ipc-server を使う方法が説明されています。 また、mpvの音量操作は add volume 5 / add volume -5 のようなコマンドで行えます。 まず、1つ目のターミナルで5Gを起動します。 cd ~/tanaradio5 ./tanaradio_5g.py 音が出る状態にしておきます。 次に、別のターミナルを開いて、同じフォルダへ移動します。 cd ~/tanaradio5 音量テスト用ファイルを作ります。 nano test_volume_5i.py 次を貼り付けます。 #!/usr/bin/env python3 import json import os import socket import time from gpiozero import RotaryEncoder from signal import pause MPV_SOCKET = "/tmp/tanaradio-mpv.sock" ENC_A_GPIO = 20 ENC_B_GPIO = 21 VOLUME_STEP = 5 MIN_VOLUME_INTERVAL = 0.04 last_volume_time = 0.0 encoder = RotaryEncoder( ENC_A_GPIO, ENC_B_GPIO, max_steps=0, bounce_time=0.002 ) def send_mpv_command(*command): if not os.path.exists(MPV_SOCKET): print("mpvのソケットが見つかりません。") print("先に別ターミナルで ./tanaradio_5g.py を起動してください。") return try: data = json.dumps({"command": list(command)}) + "\n" with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: client.connect(MPV_SOCKET) client.sendall(data.encode("utf-8")) try: response = client.recv(4096) if response: print(response.decode("utf-8").strip()) except Exception: pass except Exception as e: print(f"mpvコマンド送信エラー: {e}") def change_volume(delta): global last_volume_time now = time.monotonic() if now - last_volume_time < MIN_VOLUME_INTERVAL: return last_volume_time = now if delta > 0: print("音量アップ") else: print("音量ダウン") send_mpv_command("add", "volume", delta) def volume_up(): change_volume(VOLUME_STEP) def volume_down(): change_volume(-VOLUME_STEP) encoder.when_rotated_clockwise = volume_up encoder.when_rotated_counter_clockwise = volume_down print("ロータリーエンコーダー音量テストを開始します。") print("別ターミナルで tanaradio_5g.py が動いている必要があります。") print("つまみを右に回すと音量アップ、左に回すと音量ダウンです。") print("終了するには Ctrl+C を押してください。") pause() 保存します。 Ctrl + O Enter Ctrl + X 実行できるようにします。 chmod +x test_volume_5i.py 実行します。 ./test_volume_5i.py 5Gで音声を流したまま、つまみを回します。 右回し → 音量アップ 左回し → 音量ダウン になれば、フェーズ5Iの核心部分は成功です。 9. 5Gコードを5Iとしてコピーする ここから本体に統合します。 まず、5Gコードをコピーします。 cp tanaradio_5g.py tanaradio_5i.py バックアップも作っておきます。 cp tanaradio_5g.py tanaradio_5g_backup.py 編集します。 nano tanaradio_5i.py 10. RotaryEncoder をimportに追加する コードの上の方に、たぶん次のような行があります。 from gpiozero import Button, LED これを次のようにします。 from gpiozero import Button, LED, RotaryEncoder すでに RotaryEncoder が入っていたら、そのままでOKです。 11. GPIO設定を追加する GPIO番号を定義している部分に、次を追加します。 ENC_A_GPIO = 20 ENC_B_GPIO = 21 VOLUME_STEP = 5 MIN_VOLUME_INTERVAL = 0.04 last_volume_time = 0.0 VOLUME_STEP = 5 は、1クリックごとに音量を5ずつ変える設定です。 大きすぎると感じたら、あとで次のように小さくできます。 VOLUME_STEP = 3 または、 VOLUME_STEP = 2 12. 音量変更関数を追加する ボタンの処理関数が並んでいるあたりに、次を追加します。 def change_volume(delta): global last_volume_time now = time.monotonic() if now - last_volume_time < MIN_VOLUME_INTERVAL: return last_volume_time = now if delta > 0: print(f"音量アップ +{delta}") else: print(f"音量ダウン {delta}") send_mpv_command("add", "volume", delta) def volume_up(): change_volume(VOLUME_STEP) def volume_down(): change_volume(-VOLUME_STEP) ここで使っている send_mpv_command() は、5Gですでに使っているmpv制御関数です。 もし5Gコード内で関数名が違っていて、たとえば、 send_command(...) や、 mpv_command(...) のような名前になっている場合は、この部分だけ5G側の関数名に合わせてください。 つまり、この行です。 send_mpv_command("add", "volume", delta) ここを、5Gで使っている名前に合わせます。 13. エンコーダーを作成してイベントを登録する ボタンを作っている場所を探します。 たとえば次のような場所です。 play_button = Button(...) prev_button = Button(...) next_button = Button(...) shelf_button = Button(...) mode_button = Button(...) その近くに、次を追加します。 encoder = RotaryEncoder( ENC_A_GPIO, ENC_B_GPIO, max_steps=0, bounce_time=0.002 ) encoder.when_rotated_clockwise = volume_up encoder.when_rotated_counter_clockwise = volume_down もし5Gコードに setup_buttons() のような関数がある場合は、その関数の中に追加してください。 その場合、最後の return に encoder も含めておくと安全です。 例: def setup_buttons(): play_button = Button(PLAY_GPIO, pull_up=True, bounce_time=0.05) prev_button = Button(PREV_GPIO, pull_up=True, bounce_time=0.05) next_button = Button(NEXT_GPIO, pull_up=True, bounce_time=0.05) shelf_button = Button(SHELF_GPIO, pull_up=True, bounce_time=0.08) mode_button = Button(MODE_GPIO, pull_up=True, bounce_time=0.08) encoder = RotaryEncoder( ENC_A_GPIO, ENC_B_GPIO, max_steps=0, bounce_time=0.002 ) play_button.when_pressed = toggle_pause prev_button.when_pressed = previous_episode next_button.when_pressed = next_episode shelf_button.when_pressed = change_shelf mode_button.when_pressed = change_mode encoder.when_rotated_clockwise = volume_up encoder.when_rotated_counter_clockwise = volume_down return [ play_button, prev_button, next_button, shelf_button, mode_button, encoder, ] すでにLEDも同じ関数内で作っている場合は、LEDもreturnに残してください。 大事なのは、encoder を作ったあと、プログラム終了まで消えないようにすることです。 Pythonでは、作った部品オブジェクトを変数として保持しておく必要があります。 14. time がimportされているか確認する 今回の change_volume() では time.monotonic() を使います。 コードの上の方に、 import time があるか確認してください。 なければ追加します。 import time 15. 保存して実行する 保存します。 Ctrl + O Enter Ctrl + X 実行できるようにします。 chmod +x tanaradio_5i.py 5Gが動いている場合はいったん止めます。 Ctrl + C 5Iを実行します。 ./tanaradio_5i.py 確認することは次の通りです。 再生/一時停止ボタンが効く 戻るボタンが効く 次へボタンが効く 棚選びボタンが効く 再生モードボタンが効く 棚LEDが点滅する モードLEDが点滅する ロータリーエンコーダー右回しで音量アップ ロータリーエンコーダー左回しで音量ダウン これができれば、フェーズ5I成功です。 フェーズ5Iの成功条件 次の3つができれば成功です。 1. test_encoder_5i.py で右回し/左回しが表示される 2. test_volume_5i.py で5G再生中の音量を変えられる 3. tanaradio_5i.py 単体で、5ボタン・2LED・音量つまみが全部動く これで、TanaRadio Piはかなり「ラジオ」になります。 ボタンで番組の棚と時間順を選び、つまみで音量を調整する。ここまで来ると、もう単なるRaspberry Pi工作ではなく、手元の音声装置です。良いところまで来ています。#声日記 #TanaRadioPi

377 日記 | 再生モードボタンをつける

377 日記 | 再生モードボタンをつける

Jul 8, 2026 17:40 たな

再生モード切り替えのためのボタンとLEDを取り付けました(フェーズ5G完了)。TanaRadio Pi フェーズ5G 詳細マニュアル 5つ目のボタンで再生モードを選ぶ 0. 今回の目標 フェーズ5Fまでで、すでに次のことができています。 棚ボタンを押す ↓ 棚番号が進む ↓ LED1が棚番号ぶん点滅する ↓ 選んだ棚のRSSからプレイリストが作られる ↓ mpvで再生される フェーズ5Gでは、ここに「再生モード選び」を追加します。 再生モードボタンを押す ↓ 再生モード番号が進む ↓ LED2がモード番号ぶん点滅する ↓ 同じ棚でも、再生対象や並び順が変わる ↓ プレイリストを作り直して再生する 今回入れる再生モードは、まずこの3つです。 1:最新順 2:今日だけ古い順 3:3日前から古い順 今回は、まだ次の2つは入れません。 4:7日前から古い順 5:未聴っぽいもの 「未聴っぽいもの」は、ローカル再生履歴が必要です。ここは5H以降に回すのが正解です。焦って入れると、工作からいきなり業務システム開発になります。沼です。まだ長靴はいりません。 1. 完成後の操作 フェーズ5G完成後の操作はこうなります。 再生/一時停止ボタン: 再生/一時停止 戻るボタン: 前のエピソードへ 次へボタン: 次のエピソードへ 棚ボタン: 棚を1つ進める LED1が棚番号ぶん点滅 再生モードボタン: 再生モードを1つ進める LED2がモード番号ぶん点滅 棚は、フェーズ5Eで決めた次の構成をそのまま使います。 1:声日記・生活 shelf_01_voice_life.txt 2:技術・AI・ものづくり shelf_02_tech_ai_mono.txt 3:教育・研究・知の実践 shelf_03_education_research.txt 4:本・文化・物語 shelf_04_books_culture.txt 5:TanaRadio shelf_05_tanaradio.txt 2. 追加する部品 今回追加するのは、次の2つです。 タクトスイッチ 1個 LED 1個 つまり、5Fまでの構成に加えて、 再生モード選びボタン 再生モード表示LED を追加します。 5G終了時点では、こうなります。 タクトスイッチ:5個 LED:2個 ロータリーエンコーダー:まだ使わない ロータリーエンコーダーは次の段階です。ここで欲張らないのが、かなり大事です。 3. GPIO番号 このマニュアルでは、次のGPIO番号で説明します。 再生/一時停止ボタン:GPIO17 戻るボタン:GPIO27 次へボタン:GPIO22 棚ボタン:GPIO23 再生モードボタン:GPIO5 棚表示LED:GPIO24 再生モード表示LED:GPIO6 すでに5Fで別のGPIO番号を使っている場合は、あとで出てくるPythonコードのこの部分だけ変更してください。 PIN_PLAY = 17 PIN_PREV = 27 PIN_NEXT = 22 PIN_SHELF = 23 PIN_MODE = 5 PIN_LED_SHELF = 24 PIN_LED_MODE = 6 4. 配線 4.1 再生モードボタンの配線 新しく追加するタクトスイッチを、次のようにつなぎます。 GPIO5 ─ 1kΩ程度の抵抗 ─ タクトスイッチ ─ GND これまでのボタンと同じ考え方です。 内部プルアップを使うので、ボタンを押していないときはHIGH、押したときにGNDへ落ちてLOWになります。 4.2 再生モード表示LEDの配線 新しく追加するLEDを、次のようにつなぎます。 GPIO6 ─ 330Ω抵抗 ─ LEDの長い足 LEDの短い足 ─ GND LEDには向きがあります。 長い足:GPIO側 短い足:GND側 LEDが光らない場合、まず疑うべきはLEDの向きです。コードより先に足を疑ってください。電子工作では、だいたい足か線です。 5. いったん電源を切って配線する 配線を追加する前に、Raspberry Piを終了します。 sudo shutdown -h now 電源が落ちたら、電源ケーブルを抜いてから配線します。 配線が終わったら、もう一度電源を入れます。 6. 作業フォルダへ移動する Raspberry Piが起動したら、ターミナルを開きます。 cd ~/tanaradio5 現在地を確認します。 pwd 次のようになればOKです。 /home/tana/tanaradio5 7. 5Fのコードをバックアップする フェーズ5Fがうまく動いているので、必ずバックアップします。 もし現在のファイル名が tanaradio_5f.py なら、次のようにします。 cp tanaradio_5f.py tanaradio_5f_ok.py もしファイル名が違う場合は、その名前に合わせてください。 例: cp 今動いているファイル名.py 5f_ok.py これは保険です。 5Gで何か変になっても、5Fに戻れます。工作でもプログラムでも、戻れる場所を作る人は強いです。 8. 必要なパッケージを確認する すでに入っているはずですが、念のため確認します。 python3 -c "import gpiozero; print('gpiozero OK')" gpiozero OK と出ればOKです。 もしエラーが出たら、次を実行します。 sudo apt update sudo apt install -y python3-gpiozero python3-lgpio mpvも確認します。 mpv --version バージョン情報が出ればOKです。 9. 新しい5Gコードを作る 新しく tanaradio_5g.py を作ります。 nano tanaradio_5g.py 次のコードをそのまま貼り付けます。 #!/usr/bin/env python3 import json import socket import subprocess import time import urllib.request import xml.etree.ElementTree as ET from dataclasses import dataclass from datetime import datetime, timedelta, timezone from email.utils import parsedate_to_datetime from pathlib import Path from signal import pause from zoneinfo import ZoneInfo from gpiozero import Button, LED BASE_DIR = Path(__file__).resolve().parent PLAYLIST_FILE = BASE_DIR / "playlist_5g.m3u" STATE_FILE = BASE_DIR / "state_5g.json" MPV_SOCKET = "/tmp/tanaradio-mpv.sock" TZ = ZoneInfo("Asia/Tokyo") MAX_ITEMS = 80 # ===== GPIO番号(BCM) ===== # 5Fまでの配線と違う場合は、ここだけ自分の配線に合わせて変更してください。 PIN_PLAY = 17 PIN_PREV = 27 PIN_NEXT = 22 PIN_SHELF = 23 PIN_MODE = 5 PIN_LED_SHELF = 24 PIN_LED_MODE = 6 SHELVES = [ { "key": "voice", "name": "声日記・生活", "file": "shelf_01_voice_life.txt", }, { "key": "tech", "name": "技術・AI・ものづくり", "file": "shelf_02_tech_ai_mono.txt", }, { "key": "edu", "name": "教育・研究・知の実践", "file": "shelf_03_education_research.txt", }, { "key": "books", "name": "本・文化・物語", "file": "shelf_04_books_culture.txt", }, { "key": "tana", "name": "TanaRadio", "file": "shelf_05_tanaradio.txt", }, ] # 5Gではまず3種類だけにします。 # 「7日前から古い順」「未聴っぽいもの」は、次の段階で足します。 MODES = [ { "key": "latest", "name": "最新順", }, { "key": "today_oldest", "name": "今日だけ古い順", }, { "key": "last3_oldest", "name": "3日前から古い順", }, ] @dataclass class Episode: title: str program: str pub_dt: datetime audio_url: str mpv_process = None busy = False def clamp_index(value, size, default=0): if not isinstance(value, int): return default if value < 0 or value >= size: return default return value def load_state(): default_state = { "shelf": 0, "mode": 2, # 初期値は「3日前から古い順」 } if not STATE_FILE.exists(): return default_state try: data = json.loads(STATE_FILE.read_text(encoding="utf-8")) return { "shelf": clamp_index(data.get("shelf"), len(SHELVES), 0), "mode": clamp_index(data.get("mode"), len(MODES), 2), } except Exception: return default_state def save_state(state): STATE_FILE.write_text( json.dumps(state, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) def read_feed_urls(shelf_file): path = BASE_DIR / shelf_file if not path.exists(): raise FileNotFoundError(f"{shelf_file} が見つかりません。") urls = [] for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line: continue if line.startswith("#"): continue urls.append(line) if not urls: raise ValueError(f"{shelf_file} にRSS URLがありません。") return urls def download_rss(url): request = urllib.request.Request( url, headers={ "User-Agent": "TanaRadio-Pi/5G", }, ) with urllib.request.urlopen(request, timeout=25) as response: return response.read() def parse_pubdate(text): if not text: return datetime(1970, 1, 1, tzinfo=timezone.utc).astimezone(TZ) try: dt = parsedate_to_datetime(text) except Exception: try: dt = datetime.fromisoformat(text.replace("Z", "+00:00")) except Exception: dt = datetime(1970, 1, 1, tzinfo=timezone.utc) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(TZ) def find_audio_url(item): enclosure = item.find("enclosure") if enclosure is not None: audio_url = enclosure.attrib.get("url") if audio_url: return audio_url for child in item: if child.tag.endswith("content"): audio_url = child.attrib.get("url") if audio_url: return audio_url return "" def find_text(item, tag_name): element = item.find(tag_name) if element is not None and element.text: return element.text.strip() return "" def extract_episodes_from_rss(rss_data): root = ET.fromstring(rss_data) channel = root.find("channel") if channel is None: raise ValueError("RSSのchannelが見つかりません。") program = channel.findtext("title", default="").strip() episodes = [] for item in channel.findall("item"): audio_url = find_audio_url(item) if not audio_url: continue title = find_text(item, "title") or "(no title)" pub_text = find_text(item, "pubDate") pub_dt = parse_pubdate(pub_text) episodes.append( Episode( title=title, program=program, pub_dt=pub_dt, audio_url=audio_url, ) ) return episodes def collect_episodes(shelf): urls = read_feed_urls(shelf["file"]) all_episodes = [] print(f"棚: {shelf['name']}") print(f"RSS数: {len(urls)}") for url in urls: try: print(f"RSS取得: {url}") rss_data = download_rss(url) episodes = extract_episodes_from_rss(rss_data) all_episodes.extend(episodes) print(f" 取得エピソード数: {len(episodes)}") except Exception as e: print(f" このRSSは読み飛ばします: {e}") return all_episodes def select_episodes_by_mode(episodes, mode): now = datetime.now(TZ) if mode["key"] == "latest": selected = sorted(episodes, key=lambda e: e.pub_dt, reverse=True) elif mode["key"] == "today_oldest": today = now.date() selected = [ e for e in episodes if e.pub_dt.date() == today ] selected = sorted(selected, key=lambda e: e.pub_dt) elif mode["key"] == "last3_oldest": since = now - timedelta(days=3) selected = [ e for e in episodes if e.pub_dt >= since ] selected = sorted(selected, key=lambda e: e.pub_dt) else: selected = sorted(episodes, key=lambda e: e.pub_dt, reverse=True) return selected[:MAX_ITEMS] def write_playlist(episodes): lines = ["#EXTM3U"] for episode in episodes: title = f"{episode.program} - {episode.title}".replace("\n", " ") pub = episode.pub_dt.strftime("%Y-%m-%d %H:%M") lines.append(f"# {pub} {title}") lines.append(f"#EXTINF:-1,{title}") lines.append(episode.audio_url) PLAYLIST_FILE.write_text( "\n".join(lines) + "\n", encoding="utf-8", ) print(f"{PLAYLIST_FILE.name} を作成しました。") print(f"再生対象: {len(episodes)} 件") def playlist_has_audio(): if not PLAYLIST_FILE.exists(): return False for line in PLAYLIST_FILE.read_text(encoding="utf-8").splitlines(): line = line.strip() if line and not line.startswith("#"): return True return False def stop_mpv(): global mpv_process if mpv_process is not None and mpv_process.poll() is None: mpv_process.terminate() try: mpv_process.wait(timeout=3) except subprocess.TimeoutExpired: mpv_process.kill() mpv_process = None def start_mpv(): global mpv_process stop_mpv() if not playlist_has_audio(): print("再生できる音声URLがないため、mpvは起動しません。") print("今日モードで0件の場合などは正常です。") return socket_path = Path(MPV_SOCKET) if socket_path.exists(): try: socket_path.unlink() except Exception: pass cmd = [ "mpv", "--no-video", "--really-quiet", f"--input-ipc-server={MPV_SOCKET}", str(PLAYLIST_FILE), ] mpv_process = subprocess.Popen(cmd) time.sleep(0.5) print("mpvを起動しました。") def send_mpv_command(command): try: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: client.connect(MPV_SOCKET) message = json.dumps({"command": command}) + "\n" client.send(message.encode("utf-8")) except Exception as e: print(f"mpvへ命令を送れませんでした: {e}") def blink(led, count): count = max(1, int(count)) for _ in range(count): led.on() time.sleep(0.18) led.off() time.sleep(0.18) def rebuild_playlist_and_restart(): state = load_state() shelf = SHELVES[state["shelf"]] mode = MODES[state["mode"]] print("") print("=== プレイリストを作り直します ===") print(f"棚: {state['shelf'] + 1}. {shelf['name']}") print(f"再生モード: {state['mode'] + 1}. {mode['name']}") episodes = collect_episodes(shelf) selected = select_episodes_by_mode(episodes, mode) write_playlist(selected) start_mpv() def run_exclusive(action): global busy if busy: print("処理中です。少し待ってください。") return busy = True try: action() except Exception as e: print(f"エラー: {e}") finally: busy = False def on_play(): print("再生/一時停止") send_mpv_command(["cycle", "pause"]) def on_prev(): print("前のエピソードへ") send_mpv_command(["playlist-prev"]) def on_next(): print("次のエピソードへ") send_mpv_command(["playlist-next"]) def on_shelf(): def action(): state = load_state() state["shelf"] = (state["shelf"] + 1) % len(SHELVES) save_state(state) shelf = SHELVES[state["shelf"]] print(f"棚を変更: {state['shelf'] + 1}. {shelf['name']}") blink(led_shelf, state["shelf"] + 1) rebuild_playlist_and_restart() run_exclusive(action) def on_mode(): def action(): state = load_state() state["mode"] = (state["mode"] + 1) % len(MODES) save_state(state) mode = MODES[state["mode"]] print(f"再生モードを変更: {state['mode'] + 1}. {mode['name']}") blink(led_mode, state["mode"] + 1) rebuild_playlist_and_restart() run_exclusive(action) def print_status(): state = load_state() shelf = SHELVES[state["shelf"]] mode = MODES[state["mode"]] print("") print("TanaRadio Pi フェーズ5G") print("========================") print(f"現在の棚: {state['shelf'] + 1}. {shelf['name']}") print(f"現在の再生モード: {state['mode'] + 1}. {mode['name']}") print("") print("操作:") print(" 再生ボタン: 再生/一時停止") print(" 戻るボタン: 前のエピソード") print(" 次へボタン: 次のエピソード") print(" 棚ボタン: 棚を進める。LED1が棚番号ぶん点滅") print(" モードボタン: 再生モードを進める。LED2がモード番号ぶん点滅") print(" Ctrl + C: 終了") print("") # ===== GPIOオブジェクト ===== btn_play = Button(PIN_PLAY, pull_up=True, bounce_time=0.05) btn_prev = Button(PIN_PREV, pull_up=True, bounce_time=0.05) btn_next = Button(PIN_NEXT, pull_up=True, bounce_time=0.05) btn_shelf = Button(PIN_SHELF, pull_up=True, bounce_time=0.05) btn_mode = Button(PIN_MODE, pull_up=True, bounce_time=0.05) led_shelf = LED(PIN_LED_SHELF) led_mode = LED(PIN_LED_MODE) def main(): btn_play.when_pressed = on_play btn_prev.when_pressed = on_prev btn_next.when_pressed = on_next btn_shelf.when_pressed = on_shelf btn_mode.when_pressed = on_mode print_status() state = load_state() blink(led_shelf, state["shelf"] + 1) time.sleep(0.4) blink(led_mode, state["mode"] + 1) run_exclusive(rebuild_playlist_and_restart) try: pause() except KeyboardInterrupt: print("\n終了します。") finally: stop_mpv() led_shelf.off() led_mode.off() if __name__ == "__main__": main() 保存します。 Ctrl + O Enter Ctrl + X 10. 実行権限をつける chmod +x tanaradio_5g.py 11. 棚ファイルがあるか確認する 次を実行します。 ls shelf_*.txt 次の5つが表示されればOKです。 shelf_01_voice_life.txt shelf_02_tech_ai_mono.txt shelf_03_education_research.txt shelf_04_books_culture.txt shelf_05_tanaradio.txt 中身も軽く確認します。 head shelf_01_voice_life.txt RSS URLが入っていればOKです。 12. 5Gを実行する まず、既存のmpvが残っている場合は止めます。 pkill mpv そして実行します。 ./tanaradio_5g.py うまくいくと、ターミナルに次のような表示が出ます。 TanaRadio Pi フェーズ5G ======================== 現在の棚: 1. 声日記・生活 現在の再生モード: 3. 3日前から古い順 操作: 再生ボタン: 再生/一時停止 戻るボタン: 前のエピソード 次へボタン: 次のエピソード 棚ボタン: 棚を進める。LED1が棚番号ぶん点滅 モードボタン: 再生モードを進める。LED2がモード番号ぶん点滅 Ctrl + C: 終了 その後、RSSを取得し、プレイリストが作られ、mpvが起動します。 === プレイリストを作り直します === 棚: 1. 声日記・生活 再生モード: 3. 3日前から古い順 RSS数: ... playlist_5g.m3u を作成しました。 再生対象: ... 件 mpvを起動しました。 音が出れば、まず成功です。 13. 動作テスト 13.1 起動時のLED点滅 起動直後に、 LED1:棚番号ぶん点滅 LED2:再生モード番号ぶん点滅 します。 初期状態では、再生モードは3番にしてあります。 LED2が3回点滅 → 3日前から古い順 13.2 再生モードボタンを押す 再生モードボタンを押します。 1回押すごとに、次のように進みます。 1:最新順 ↓ 2:今日だけ古い順 ↓ 3:3日前から古い順 ↓ 1:最新順 ボタンを押すと、LED2がモード番号ぶん点滅します。 LED2が1回点滅 → 最新順 LED2が2回点滅 → 今日だけ古い順 LED2が3回点滅 → 3日前から古い順 その後、プレイリストが作り直され、mpvが再起動します。 13.3 棚ボタンも確認する 棚ボタンも、5Fと同じように動くか確認します。 棚ボタンを押す ↓ LED1が棚番号ぶん点滅 ↓ 選んだ棚でプレイリスト作成 ↓ 再生 ここが動けば、5Fの機能は壊れていません。 14. 再生モードの意味 1:最新順 RSSから取れたエピソードを、新しいものから順に並べます。 新しい ↓ 古い 新着チェック向きです。 2:今日だけ古い順 今日公開されたエピソードだけを対象にします。 今日の古いもの ↓ 今日の新しいもの 今日のエピソードがない場合は、 再生対象: 0 件 再生できる音声URLがないため、mpvは起動しません。 今日モードで0件の場合などは正常です。 と表示されます。 これはエラーではありません。 単に「今日はまだ該当エピソードがない」という意味です。 3:3日前から古い順 現在時刻から見て、過去3日以内のエピソードを対象にします。 3日前 ↓ 2日前 ↓ 昨日 ↓ 今日 これは「追いつき再生」向きです。 TanaRadio Piの日常利用では、このモードがいちばんラジオらしいと思います。 15. 作られるファイル 5Gでは、新しく次のファイルが作られます。 playlist_5g.m3u state_5g.json 現在の棚と再生モードに応じて作られるmpv用プレイリストです。 中身を確認するには、 head playlist_5g.m3u state_5g.json 現在選ばれている棚番号と再生モード番号を保存するファイルです。 確認するには、 cat state_5g.json 例: { "shelf": 0, "mode": 2 } Python内部では0から数えるため、 "shelf": 0 → 棚1 "mode": 2 → モード3 という意味になります。 ここは少しややこしいですが、正常です。コンピュータは0から数えたがる生き物です。 16. 終了方法 プログラムを止めるには、ターミナルで次を押します。 Ctrl + C すると、 終了します。 と出て、mpvも止まります。 もし音だけ残ってしまった場合は、次で止めます。 pkill mpv 18. 成功条件 フェーズ5Gの成功条件は、次の5つです。 1. 5つ目のボタンを押すと、再生モードが切り替わる 2. LED2が再生モード番号ぶん点滅する 3. 最新順・今日だけ古い順・3日前から古い順が切り替わる 4. 棚ボタンとLED1もこれまで通り動く 5. 再生/一時停止・戻る・次へボタンも動く これができれば、フェーズ5Gは成功です。 19. 今回はここで止めてよいです 5Gまで来ると、TanaRadio Piはかなり「声の本棚」らしくなっています。 どの棚を聴くか どの時間順で聴くか この2つを、物理ボタンで選べるようになったからです。 次の段階は、おそらく次のどちらかです。 フェーズ5H: ローカル再生履歴を作る フェーズ5I: ロータリーエンコーダーで音量調整する 私のおすすめは、まだ再生履歴には行かず、先にロータリーエンコーダーを音量用として追加することです。理由は単純で、音量ダイヤルが付くと一気にラジオ感が出るからです。やる気が出る工作は、正義です。#声日記 #TanaRadioPi

376 日記 | 棚選択ボタンとLEDランプを追加

376 日記 | 棚選択ボタンとLEDランプを追加

Jul 6, 2026 26:21 たな

ボタンで棚を選択できるようにしました。選んでいる棚番号はLEDランプの点滅回数で表示します(フェーズ5F完了)。# TanaRadio Pi フェーズ5F 詳細マニュアル ## 棚選びボタンとLED1を追加する ## 0. 今回の目標 フェーズ5Eでは、棚ファイルを指定してプレイリストを作り、mpvで再生できるようになりました。 フェーズ5Fでは、その「棚の切り替え」を物理ボタンで行えるようにします。 今回の動きは次の通りです。 ```text 棚選びボタンを押す ↓ 棚番号が1つ進む ↓ LED1が棚番号ぶん点滅する ↓ 選ばれた棚ファイルから playlist.m3u を作る ↓ mpvをその棚で再生し直す ``` 今回はまだ、再生モード選びボタンは追加しません。 再生モードは、5Eまでと同じく、 ```text 過去3日分 古い順 ``` を基本にします。 --- # 1. 今回使う棚構成 フェーズ5Fでは、フェーズ5Eで作った次の棚構成を使います。 ```text 1: 声日記・生活 (voice) -> shelf_01_voice_life.txt 2: 技術・AI・ものづくり (tech) -> shelf_02_tech_ai_mono.txt 3: 教育・研究・知の実践 (edu) -> shelf_03_education_research.txt 4: 本・文化・物語 (books) -> shelf_04_books_culture.txt 5: TanaRadio (tana) -> shelf_05_tanaradio.txt ``` 棚選びボタンを押すと、この順番で進みます。 ```text 声日記・生活 ↓ 技術・AI・ものづくり ↓ 教育・研究・知の実践 ↓ 本・文化・物語 ↓ TanaRadio ↓ 声日記・生活 ``` LED1の点滅回数は、次の対応です。 ```text 1回点滅:声日記・生活 2回点滅:技術・AI・ものづくり 3回点滅:教育・研究・知の実践 4回点滅:本・文化・物語 5回点滅:TanaRadio ``` --- # 2. 今回追加する部品 今回追加する部品は2つです。 ```text タクトスイッチ 1個 LED 1個 ``` 用途は次の通りです。 ```text タクトスイッチ:棚選びボタン LED:棚番号表示LED ``` 既存の3ボタンは、そのまま使います。 ```text 再生/一時停止 戻る 次へ ``` つまり、フェーズ5F終了時点では、次の構成になります。 ```text ボタン1:再生/一時停止 ボタン2:戻る ボタン3:次へ ボタン4:棚選び LED1:棚番号表示 ``` --- # 3. GPIO番号 このマニュアルでは、次のGPIO番号を使います。 ```text 再生/一時停止:GPIO17 戻る:GPIO27 次へ:GPIO22 棚選びボタン:GPIO23 棚番号LED:GPIO24 ``` Raspberry Piの物理ピン番号では、次の対応です。 ```text GPIO17:物理ピン11 GPIO27:物理ピン13 GPIO22:物理ピン15 GPIO23:物理ピン16 GPIO24:物理ピン18 GND:物理ピン6, 9, 14, 20, 25 など ``` 注意点です。 ```text GPIO番号と物理ピン番号は違います。 ``` コードでは「GPIO番号」を使います。 たとえば、 ```text GPIO23 ``` は、物理ピン番号では、 ```text 16番ピン ``` です。 ここを取り違えると、ボタンを押しても無反応になります。静かな失敗です。電子工作ではいちばん地味に困るやつです。 --- # 4. 電源を切る 配線を触る前に、必ずRaspberry Piを終了します。 ターミナルで次を実行します。 ```bash sudo shutdown -h now ``` 完全に電源が切れてから、USB-C電源を抜きます。 --- # 5. 棚選びボタンを配線する 棚選びボタンは、次のようにつなぎます。 ```text GPIO23 ─ 1kΩ抵抗 ─ タクトスイッチ ─ GND ``` この構成では、Python側で内部プルアップを使います。 そのため、動作は次のようになります。 ```text ボタンを押していない:HIGH ボタンを押した:LOW ``` これまでのボタンと同じ考え方です。 --- # 6. 棚番号LEDを配線する LEDは、次のようにつなぎます。 ```text GPIO24 ─ 330Ω抵抗 ─ LEDの長い足 LEDの短い足 ─ GND ``` LEDの足の目安です。 ```text 長い足:プラス側 短い足:GND側 ``` 光らない場合は、まずLEDの向きを疑ってください。 LEDは、向きが逆だと基本的に光りません。 壊れたわけではなく、ただ黙っているだけです。 --- # 7. Raspberry Piを起動する 配線が終わったら、Raspberry Piの電源を入れます。 ターミナルを開き、作業フォルダに移動します。 ```bash cd ~/tanaradio5 ``` 現在地を確認します。 ```bash pwd ``` 次のように表示されればOKです。 ```text /home/tana/tanaradio5 ``` ユーザー名が違う場合は、そこだけ変わります。 --- # 8. 必要なパッケージを確認する GPIOとmpvを使うため、念のため次を実行します。 ```bash sudo apt update sudo apt install -y python3-gpiozero python3-lgpio mpv ``` すでに入っている場合は、そのままで大丈夫です。 --- # 9. 棚ファイルを確認する 今回使う棚ファイルがあるか確認します。 ```bash ls shelf_*.txt ``` 次の5つが見えればOKです。 ```text shelf_01_voice_life.txt shelf_02_tech_ai_mono.txt shelf_03_education_research.txt shelf_04_books_culture.txt shelf_05_tanaradio.txt ``` それぞれの中身も確認します。 ```bash cat shelf_01_voice_life.txt cat shelf_02_tech_ai_mono.txt cat shelf_03_education_research.txt cat shelf_04_books_culture.txt cat shelf_05_tanaradio.txt ``` RSS URLが1行ずつ並んでいればOKです。 例です。 ```text https://listen.style/p/xxxx/rss https://listen.style/p/yyyy/rss https://listen.style/p/zzzz/rss ``` 空の棚ファイルがある場合、その棚に切り替えたときにエラーになります。 テスト段階では、空の棚にも一時的に動作確認用のRSS URLを1本入れておくと安心です。 --- # 10. 5F用Pythonファイルを作る 新しいPythonファイルを作ります。 ```bash nano tanaradio_5f.py ``` 次のコードをそのまま貼り付けます。 重要です。 最初の行は必ず、次で始めてください。 ```text #!/usr/bin/env python3 ``` この前に、余計な文字を入れないでください。 ```python #!/usr/bin/env python3 import argparse import json import os import signal import socket import subprocess import time import urllib.request import xml.etree.ElementTree as ET from dataclasses import dataclass from datetime import datetime, timedelta, timezone from email.utils import parsedate_to_datetime from pathlib import Path from threading import Lock from gpiozero import Button, LED # ========================= # GPIO設定 # 5Dまでと違うGPIOを使っていた場合は、ここだけ直してください。 # BCM番号です。物理ピン番号ではありません。 # ========================= PLAY_GPIO = 17 # 再生/一時停止ボタン PREV_GPIO = 27 # 戻るボタン NEXT_GPIO = 22 # 次へボタン SHELF_GPIO = 23 # 棚選びボタン(今回追加) SHELF_LED_GPIO = 24 # 棚番号表示LED(今回追加) # ========================= # ファイル設定 # ========================= BASE_DIR = Path(__file__).resolve().parent PLAYLIST_FILE = BASE_DIR / "playlist.m3u" STATE_FILE = BASE_DIR / "state_5f.json" MPV_SOCKET = "/tmp/tanaradio_5f_mpv.sock" # フェーズ5Fでは、まだ再生モードボタンは追加しません。 # 5Eまでと同じく「過去3日分・古い順」を基本にします。 DAYS_BACK = 3 # ========================= # 棚設定 # フェーズ5Eで作った棚構成に合わせています。 # ========================= SHELVES = [ { "label": "声日記・生活", "short": "voice", "file": "shelf_01_voice_life.txt", }, { "label": "技術・AI・ものづくり", "short": "tech", "file": "shelf_02_tech_ai_mono.txt", }, { "label": "教育・研究・知の実践", "short": "edu", "file": "shelf_03_education_research.txt", }, { "label": "本・文化・物語", "short": "books", "file": "shelf_04_books_culture.txt", }, { "label": "TanaRadio", "short": "tana", "file": "shelf_05_tanaradio.txt", }, ] @dataclass class Episode: title: str audio_url: str pub_date: datetime | None feed_title: str mpv_process: subprocess.Popen | None = None current_shelf_index = 0 shelf_lock = Lock() shelf_led: LED | None = None def read_lines(path: Path) -> list[str]: if not path.exists(): return [] lines = [] for raw_line in path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line: continue if line.startswith("#"): continue lines.append(line) return lines def load_state() -> int: if not STATE_FILE.exists(): return 0 try: data = json.loads(STATE_FILE.read_text(encoding="utf-8")) index = int(data.get("shelf_index", 0)) if 0 <= index < len(SHELVES): return index except Exception: pass return 0 def save_state(index: int) -> None: data = { "shelf_index": index, "shelf_label": SHELVES[index]["label"], "shelf_short": SHELVES[index]["short"], "shelf_file": SHELVES[index]["file"], "saved_at": datetime.now().isoformat(timespec="seconds"), } STATE_FILE.write_text( json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8", ) def download_rss(url: str) -> bytes: request = urllib.request.Request( url, headers={"User-Agent": "TanaRadio-Pi/5F"}, ) with urllib.request.urlopen(request, timeout=25) as response: return response.read() def get_child_text(parent: ET.Element, tag_name: str) -> str: element = parent.find(tag_name) if element is not None and element.text: return element.text.strip() return "" def find_audio_url(item: ET.Element) -> str | None: # 一般的なRSS Podcastのenclosureタグ enclosure = item.find("enclosure") if enclosure is not None: audio_url = enclosure.attrib.get("url") if audio_url: return audio_url # media:content など、名前空間つきタグにも軽く対応 for child in item: if child.tag.endswith("content"): audio_url = child.attrib.get("url") if audio_url: return audio_url return None def parse_pub_date(text: str) -> datetime | None: if not text: return None try: dt = parsedate_to_datetime(text) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone() except Exception: return None def parse_rss(rss_data: bytes, feed_url: str) -> list[Episode]: root = ET.fromstring(rss_data) channel = root.find("channel") if channel is None: raise ValueError("RSSのchannelが見つかりません。") feed_title = get_child_text(channel, "title") or feed_url episodes: list[Episode] = [] for item in channel.findall("item"): audio_url = find_audio_url(item) if not audio_url: continue title = get_child_text(item, "title") or "タイトルなし" pub_date = parse_pub_date(get_child_text(item, "pubDate")) episodes.append( Episode( title=title, audio_url=audio_url, pub_date=pub_date, feed_title=feed_title, ) ) return episodes def collect_episodes_for_shelf(index: int) -> list[Episode]: shelf = SHELVES[index] shelf_path = BASE_DIR / shelf["file"] feed_urls = read_lines(shelf_path) if not feed_urls: raise ValueError(f"{shelf_path.name} にRSS URLがありません。") print() print(f"棚 {index + 1}: {shelf['label']} ({shelf['short']})") print(f"棚ファイル: {shelf_path.name}") print(f"RSS数: {len(feed_urls)}") all_episodes: list[Episode] = [] for feed_url in feed_urls: print(f"RSS取得中: {feed_url}") try: rss_data = download_rss(feed_url) episodes = parse_rss(rss_data, feed_url) all_episodes.extend(episodes) print(f" 取得: {len(episodes)} 件") except Exception as e: print(f" このRSSは読み飛ばしました: {e}") if not all_episodes: raise ValueError("音声エピソードを1件も取得できませんでした。") now = datetime.now().astimezone() cutoff = now - timedelta(days=DAYS_BACK) filtered = [ ep for ep in all_episodes if ep.pub_date is not None and ep.pub_date >= cutoff ] if not filtered: print(f"過去{DAYS_BACK}日分のエピソードがありません。") print("動作確認しやすいように、今回は最新10件でプレイリストを作ります。") filtered = sorted( all_episodes, key=lambda ep: ep.pub_date or datetime.min.replace(tzinfo=timezone.utc), reverse=True, )[:10] filtered.sort( key=lambda ep: ep.pub_date or datetime.min.replace(tzinfo=timezone.utc) ) else: filtered.sort( key=lambda ep: ep.pub_date or datetime.min.replace(tzinfo=timezone.utc) ) return filtered def make_playlist(index: int) -> int: episodes = collect_episodes_for_shelf(index) lines = ["#EXTM3U"] for ep in episodes: date_text = ep.pub_date.isoformat(timespec="minutes") if ep.pub_date else "日付不明" safe_title = ep.title.replace("\n", " ").strip() safe_feed = ep.feed_title.replace("\n", " ").strip() lines.append(f"# {date_text} / {safe_feed} / {safe_title}") lines.append(ep.audio_url) PLAYLIST_FILE.write_text( "\n".join(lines) + "\n", encoding="utf-8", ) print(f"{PLAYLIST_FILE.name} を作成しました。") print(f"音声URL数: {len(episodes)} 件") return len(episodes) def wait_for_mpv_socket(timeout: float = 5.0) -> bool: start = time.time() while time.time() - start < timeout: if os.path.exists(MPV_SOCKET): return True time.sleep(0.1) return False def send_mpv_command(command: list[str | int | float]) -> None: if not os.path.exists(MPV_SOCKET): print("mpvのソケットが見つかりません。mpvがまだ起動していない可能性があります。") return payload = json.dumps({"command": command}) + "\n" try: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: client.connect(MPV_SOCKET) client.sendall(payload.encode("utf-8")) except Exception as e: print(f"mpvへの命令送信に失敗しました: {e}") def stop_mpv() -> None: global mpv_process if mpv_process is None: return if mpv_process.poll() is None: try: send_mpv_command(["quit"]) mpv_process.wait(timeout=3) except Exception: mpv_process.terminate() try: mpv_process.wait(timeout=3) except Exception: mpv_process.kill() mpv_process = None if os.path.exists(MPV_SOCKET): try: os.remove(MPV_SOCKET) except OSError: pass def start_mpv() -> None: global mpv_process stop_mpv() if os.path.exists(MPV_SOCKET): os.remove(MPV_SOCKET) print("mpvを起動します。") mpv_process = subprocess.Popen( [ "mpv", "--input-ipc-server=" + MPV_SOCKET, "--force-window=no", str(PLAYLIST_FILE), ] ) if not wait_for_mpv_socket(): print("mpvの操作ソケットを確認できませんでした。") print("再生自体は始まっている可能性があります。") def blink_shelf_number(index: int) -> None: if shelf_led is None: return count = index + 1 print(f"LED点滅: {count}回") for _ in range(count): shelf_led.on() time.sleep(0.18) shelf_led.off() time.sleep(0.18) time.sleep(0.25) def blink_error() -> None: if shelf_led is None: return for _ in range(6): shelf_led.on() time.sleep(0.07) shelf_led.off() time.sleep(0.07) def select_shelf(index: int, restart_player: bool = True) -> bool: global current_shelf_index with shelf_lock: try: make_playlist(index) current_shelf_index = index save_state(index) blink_shelf_number(index) if restart_player: start_mpv() print(f"現在の棚: {index + 1} / {SHELVES[index]['label']} ({SHELVES[index]['short']})") return True except Exception as e: print("棚の切り替えに失敗しました。") print(e) blink_error() return False def next_shelf() -> None: next_index = (current_shelf_index + 1) % len(SHELVES) print() print(f"棚ボタン: {current_shelf_index + 1} → {next_index + 1}") print(f"次の棚: {SHELVES[next_index]['label']} ({SHELVES[next_index]['short']})") select_shelf(next_index, restart_player=True) def on_play() -> None: print("再生/一時停止") send_mpv_command(["cycle", "pause"]) def on_prev() -> None: print("戻る") send_mpv_command(["playlist-prev"]) def on_next() -> None: print("次へ") send_mpv_command(["playlist-next"]) def list_shelves() -> None: print("棚一覧") for i, shelf in enumerate(SHELVES, start=1): path = BASE_DIR / shelf["file"] urls = read_lines(path) print( f"{i}: {shelf['label']} ({shelf['short']}) " f"-> {path.name} RSS数: {len(urls)}" ) def test_led() -> None: global shelf_led shelf_led = LED(SHELF_LED_GPIO) try: for index, shelf in enumerate(SHELVES): print(f"棚{index + 1}: {shelf['label']} ({shelf['short']}) の点滅テスト") blink_shelf_number(index) time.sleep(0.5) finally: shelf_led.off() shelf_led.close() shelf_led = None def main() -> None: global current_shelf_index, shelf_led parser = argparse.ArgumentParser(description="TanaRadio Pi フェーズ5F") parser.add_argument("--list", action="store_true", help="棚一覧を表示する") parser.add_argument("--test-led", action="store_true", help="LEDの点滅をテストする") args = parser.parse_args() if args.list: list_shelves() return if args.test_led: test_led() return current_shelf_index = load_state() print("TanaRadio Pi フェーズ5Fを起動します。") print() list_shelves() print() print( f"前回の棚: {current_shelf_index + 1} / " f"{SHELVES[current_shelf_index]['label']} " f"({SHELVES[current_shelf_index]['short']})" ) shelf_led = LED(SHELF_LED_GPIO) play_button = Button(PLAY_GPIO, pull_up=True, bounce_time=0.08) prev_button = Button(PREV_GPIO, pull_up=True, bounce_time=0.08) next_button = Button(NEXT_GPIO, pull_up=True, bounce_time=0.08) shelf_button = Button(SHELF_GPIO, pull_up=True, bounce_time=0.12) play_button.when_pressed = on_play prev_button.when_pressed = on_prev next_button.when_pressed = on_next shelf_button.when_pressed = next_shelf try: ok = select_shelf(current_shelf_index, restart_player=True) if not ok: print("最初の棚を作れませんでした。棚ファイルを確認してください。") return print() print("操作できます。") print("再生/一時停止、戻る、次へ、棚選びボタンを押してください。") print("終了するには Ctrl + C を押します。") signal.pause() except KeyboardInterrupt: print() print("終了します。") finally: stop_mpv() if shelf_led is not None: shelf_led.off() shelf_led.close() play_button.close() prev_button.close() next_button.close() shelf_button.close() if __name__ == "__main__": main() ``` 保存します。 ```text Ctrl + O Enter Ctrl + X ``` --- # 11. 実行権限を付ける 次を実行します。 ```bash chmod +x tanaradio_5f.py ``` ただし、実行は `python3` で行うのが安全です。 ```bash python3 tanaradio_5f.py ``` 以前のように、Pythonファイルをシェルスクリプトとして読んでしまう事故を避けるためです。 --- # 12. 棚一覧を確認する まず、棚ファイルが正しく認識されているか確認します。 ```bash python3 tanaradio_5f.py --list ``` 期待される表示例です。 ```text 棚一覧 1: 声日記・生活 (voice) -> shelf_01_voice_life.txt RSS数: 8 2: 技術・AI・ものづくり (tech) -> shelf_02_tech_ai_mono.txt RSS数: 6 3: 教育・研究・知の実践 (edu) -> shelf_03_education_research.txt RSS数: 5 4: 本・文化・物語 (books) -> shelf_04_books_culture.txt RSS数: 4 5: TanaRadio (tana) -> shelf_05_tanaradio.txt RSS数: 1 ``` RSS数は実際のファイル内容によって変わります。 ここで重要なのは、次の対応が正しく出ることです。 ```text 1: 声日記・生活 -> shelf_01_voice_life.txt 2: 技術・AI・ものづくり -> shelf_02_tech_ai_mono.txt 3: 教育・研究・知の実践 -> shelf_03_education_research.txt 4: 本・文化・物語 -> shelf_04_books_culture.txt 5: TanaRadio -> shelf_05_tanaradio.txt ``` --- # 13. LEDだけテストする いきなり再生まで行かず、まずLEDの点滅を確認します。 ```bash python3 tanaradio_5f.py --test-led ``` 期待される動きです。 ```text 棚1:LEDが1回点滅 棚2:LEDが2回点滅 棚3:LEDが3回点滅 棚4:LEDが4回点滅 棚5:LEDが5回点滅 ``` 表示例です。 ```text 棚1: 声日記・生活 (voice) の点滅テスト LED点滅: 1回 棚2: 技術・AI・ものづくり (tech) の点滅テスト LED点滅: 2回 棚3: 教育・研究・知の実践 (edu) の点滅テスト LED点滅: 3回 棚4: 本・文化・物語 (books) の点滅テスト LED点滅: 4回 棚5: TanaRadio (tana) の点滅テスト LED点滅: 5回 ``` LEDが光らない場合は、次を確認します。 ```text LEDの向きが逆ではないか GPIO24ではなく別のピンにつないでいないか GNDにつながっているか 抵抗を入れているか ``` --- # 14. 5Fを起動する いよいよ5Fを起動します。 ```bash python3 tanaradio_5f.py ``` うまくいくと、次のような表示になります。 ```text TanaRadio Pi フェーズ5Fを起動します。 棚一覧 1: 声日記・生活 (voice) -> shelf_01_voice_life.txt RSS数: 8 2: 技術・AI・ものづくり (tech) -> shelf_02_tech_ai_mono.txt RSS数: 6 3: 教育・研究・知の実践 (edu) -> shelf_03_education_research.txt RSS数: 5 4: 本・文化・物語 (books) -> shelf_04_books_culture.txt RSS数: 4 5: TanaRadio (tana) -> shelf_05_tanaradio.txt RSS数: 1 前回の棚: 1 / 声日記・生活 (voice) 棚 1: 声日記・生活 (voice) 棚ファイル: shelf_01_voice_life.txt RSS数: 8 RSS取得中: ... playlist.m3u を作成しました。 音声URL数: 10 件 LED点滅: 1回 mpvを起動します。 現在の棚: 1 / 声日記・生活 (voice) 操作できます。 再生/一時停止、戻る、次へ、棚選びボタンを押してください。 終了するには Ctrl + C を押します。 ``` 音が出れば、まず成功です。 --- # 15. 既存3ボタンを確認する 次のボタンを押します。 ```text 再生/一時停止 戻る 次へ ``` ターミナルに次のように表示されれば、ボタン入力は読めています。 ```text 再生/一時停止 戻る 次へ ``` 音声もそれに合わせて動けばOKです。 --- # 16. 棚選びボタンを押す 棚選びボタンを1回押します。 棚1から棚2に進む場合、期待される動きは次の通りです。 ```text 棚1:声日記・生活 ↓ 棚2:技術・AI・ものづくり ``` ターミナルには、たとえば次のように表示されます。 ```text 棚ボタン: 1 → 2 次の棚: 技術・AI・ものづくり (tech) 棚 2: 技術・AI・ものづくり (tech) 棚ファイル: shelf_02_tech_ai_mono.txt RSS数: 6 RSS取得中: ... playlist.m3u を作成しました。 音声URL数: 12 件 LED点滅: 2回 mpvを起動します。 現在の棚: 2 / 技術・AI・ものづくり (tech) ``` さらに押すと、次のように進みます。 ```text 2 → 3:教育・研究・知の実践 3 → 4:本・文化・物語 4 → 5:TanaRadio 5 → 1:声日記・生活 ``` LED点滅の対応は次の通りです。 ```text 1回点滅:声日記・生活 2回点滅:技術・AI・ものづくり 3回点滅:教育・研究・知の実践 4回点滅:本・文化・物語 5回点滅:TanaRadio ``` --- # 17. 前回の棚が保存されるか確認する 一度終了します。 ```text Ctrl + C ``` もう一度起動します。 ```bash python3 tanaradio_5f.py ``` 前回最後に選んだ棚から始まれば成功です。 棚の状態は、次のファイルに保存されます。 ```text state_5f.json ``` 中身を確認するには、次を実行します。 ```bash cat state_5f.json ``` 表示例です。 ```json { "shelf_index": 1, "shelf_label": "技術・AI・ものづくり", "shelf_short": "tech", "shelf_file": "shelf_02_tech_ai_mono.txt", "saved_at": "2026-07-06T09:30:00" } ``` ここで `shelf_index` は0始まりです。 つまり、 ```text shelf_index 0:棚1 shelf_index 1:棚2 shelf_index 2:棚3 shelf_index 3:棚4 shelf_index 4:棚5 ``` です。 --- # 18. 成功条件 フェーズ5Fの成功条件は、次の4つです。 ```text 1. 棚選びボタンを押すと、棚番号が進む 2. LED1が棚番号ぶん点滅する 3. 選ばれた棚ファイルに応じて playlist.m3u が作り直される 4. mpvが新しい棚で再生し直される ``` 棚番号の対応は次の通りです。 ```text 1:声日記・生活 2:技術・AI・ものづくり 3:教育・研究・知の実践 4:本・文化・物語 5:TanaRadio ``` LED点滅の対応は次の通りです。 ```text 1回点滅:声日記・生活 2回点滅:技術・AI・ものづくり 3回点滅:教育・研究・知の実践 4回点滅:本・文化・物語 5回点滅:TanaRadio ``` --- # 20. 今回はここで止めてよい フェーズ5Fでは、ここまでで十分です。 今回できるようになることは、次です。 ```text 物理ボタンで棚を選べる LEDの点滅で棚番号を確認できる 棚ごとのRSSリストからプレイリストを作れる 選んだ棚をmpvで再生し直せる ``` これはかなり大きな進歩です。 フェーズ5Eまでは、棚はまだ「コマンドで指定するもの」でした。 フェーズ5Fで、棚は「手元のボタンで選ぶもの」になります。 ここから一気にラジオ感が出ます。 次のフェーズ5Gでは、5つ目のボタンとLED2を追加して、再生モードを選べるようにします。 候補は、まず次の3つで十分です。 ```text 1:最新順 2:今日 3:3日前から古い順 ``` 「7日前から古い順」や「未聴っぽいもの」は、そのあとで大丈夫です。 ここで焦らないのが勝ち筋です。#声日記 #TanaRadioPi

375 日記 | 棚を選択してプレイリストを作成

375 日記 | 棚を選択してプレイリストを作成

Jul 5, 2026 29:54 たな

5つの棚(ポッドキャストのグループ)を作成し,それを選んでプレイリストを作成できるようにしました(フェーズ5E完了)。フェーズ5E 詳細マニュアル 棚ファイルを作り、コマンドで棚を切り替える 0. 今回の目標 フェーズ5Eでやることは、これです。 棚ごとのRSSリストを作る ↓ コマンドで棚を指定する ↓ 指定した棚のRSSだけを読む ↓ 過去3日分・古い順で playlist.m3u を作る ↓ mpv または tanaradio_5d.py で再生する 今回はまだ、以下はやりません。 棚選びボタン LED点滅表示 再生モード選び ローカル再生履歴 ロータリーエンコーダー ここは焦らなくてよいです。 5Eの目的は、「棚」という考え方を、まずファイルで実現することです。 ステップ1:作業フォルダへ移動する Raspberry Piでターミナルを開きます。 cd ~/tanaradio5 確認します。 pwd 次のように表示されればOKです。 /home/ユーザー名/tanaradio5 ファイルも確認します。 ls 少なくとも、次のようなファイルがあるはずです。 feeds.txt make_playlist_5c.py playlist.m3u tanaradio_5d.py ステップ2:今の状態をバックアップする 5Dまで動いているので、念のためバックアップします。 cp make_playlist_5c.py make_playlist_5c_backup.py cp tanaradio_5d.py tanaradio_5d_backup.py cp playlist.m3u playlist_backup_5d.m3u 確認します。 ls バックアップファイルが見えればOKです。 make_playlist_5c_backup.py tanaradio_5d_backup.py playlist_backup_5d.m3u 5Eでは tanaradio_5d.py は基本的に変更しません。 ここ、大事です。5Dで動いたものはそのまま残します。動いたものは宝です。Linux作業では、動いたものを消すと、静かに深い後悔が来ます。 ステップ3:棚ファイルを作る まず、今回決めた5つの棚に対応するファイルを作ります。 touch shelf_01_voice_life.txt touch shelf_02_tech_ai_mono.txt touch shelf_03_education_research.txt touch shelf_04_books_culture.txt touch shelf_05_tanaradio.txt 確認します。 ls shelf_*.txt 次のように表示されればOKです。 shelf_01_voice_life.txt shelf_02_tech_ai_mono.txt shelf_03_education_research.txt shelf_04_books_culture.txt shelf_05_tanaradio.txt 棚の意味は次の通りです。 1:声日記・生活 2:技術・AI・ものづくり 3:教育・研究・知の実践 4:本・文化・物語 5:TanaRadio ステップ4:まず1つの棚にRSSを入れる 最初から全部きれいに分類しなくてよいです。 まずは、動作確認のために、これまで使っていた feeds.txt を1番の棚にコピーします。 cp feeds.txt shelf_01_voice_life.txt 確認します。 cat shelf_01_voice_life.txt RSS URLが表示されればOKです。 あとで、実際の分類に合わせて中身を編集します。 たとえば、1番の棚を編集するには、 nano shelf_01_voice_life.txt 次のように、コメントを入れながらRSS URLを書けます。 # 声日記・生活 # で始まる行は、あとで作るプログラムでは無視されます。 つまり、番組名メモを書いておいて大丈夫です。 ステップ5:5E用スクリプトを作る 新しいPythonファイルを作ります。 nano make_playlist_5e.py 次のコードをそのまま貼り付けてください。 #!/usr/bin/env python3 import sys import urllib.request import xml.etree.ElementTree as ET from pathlib import Path from datetime import datetime, timedelta, timezone from email.utils import parsedate_to_datetime from zoneinfo import ZoneInfo PLAYLIST_FILE = "playlist.m3u" # 5Eでは、まだ再生モード選びはしない。 # 5Cと同じく「過去3日分・古い順」で固定する。 DAYS = 3 JST = ZoneInfo("Asia/Tokyo") SHELVES = [ { "id": "1", "key": "voice", "name": "声日記・生活", "file": "shelf_01_voice_life.txt", "aliases": ["life", "koe", "nikki"], }, { "id": "2", "key": "tech", "name": "技術・AI・ものづくり", "file": "shelf_02_tech_ai_mono.txt", "aliases": ["ai", "mono", "monozukuri"], }, { "id": "3", "key": "edu", "name": "教育・研究・知の実践", "file": "shelf_03_education_research.txt", "aliases": ["education", "research", "study"], }, { "id": "4", "key": "books", "name": "本・文化・物語", "file": "shelf_04_books_culture.txt", "aliases": ["book", "culture", "story"], }, { "id": "5", "key": "tana", "name": "TanaRadio", "file": "shelf_05_tanaradio.txt", "aliases": ["tanaradio"], }, ] def show_shelves(): print("使える棚:") for shelf in SHELVES: print( f" {shelf['id']}: {shelf['name']} " f"({shelf['key']}) -> {shelf['file']}" ) print() print("例:") print(" python3 make_playlist_5e.py 1") print(" python3 make_playlist_5e.py tech") print(" python3 make_playlist_5e.py shelf_05_tanaradio.txt") def resolve_shelf(argument): if argument is None: # 引数なしの場合は1番棚を使う return SHELVES[0] arg = argument.strip() for shelf in SHELVES: candidates = [shelf["id"], shelf["key"]] + shelf["aliases"] if arg in candidates: return shelf # .txt が指定された場合は、任意の棚ファイルとして扱う path = Path(arg) if path.suffix == ".txt" or path.exists(): return { "id": "?", "key": "custom", "name": path.stem, "file": str(path), "aliases": [], } raise ValueError(f"棚を見つけられませんでした: {argument}") def read_feed_urls(shelf_file): path = Path(shelf_file) if not path.exists(): raise FileNotFoundError(f"{shelf_file} が見つかりません。") urls = [] for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() # 空行は無視 if not line: continue # # で始まる行はコメントとして無視 if line.startswith("#"): continue urls.append(line) if not urls: raise ValueError(f"{shelf_file} にRSS URLが書かれていません。") return urls def download_rss(url): print(f"RSSを取得します: {url}") request = urllib.request.Request( url, headers={ "User-Agent": "TanaRadio-Pi/5E" } ) with urllib.request.urlopen(request, timeout=20) as response: return response.read() def get_text(element, tag_name): child = element.find(tag_name) if child is not None and child.text: return child.text.strip() return "" def find_audio_url(item): # 通常のPodcast RSSでは enclosure に音声URLが入る enclosure = item.find("enclosure") if enclosure is not None: audio_url = enclosure.attrib.get("url") if audio_url: return audio_url # 念のため media:content 的な形式にも軽く対応する for child in item: if child.tag.endswith("content"): audio_url = child.attrib.get("url") if audio_url: return audio_url return None def parse_pub_date(item): pub_date_text = get_text(item, "pubDate") if not pub_date_text: return None try: dt = parsedate_to_datetime(pub_date_text) # タイムゾーン情報がない場合はUTC扱いにする if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(JST) except Exception: return None def parse_rss(rss_data, feed_url): root = ET.fromstring(rss_data) channel = root.find("channel") if channel is None: raise ValueError("RSSのchannelが見つかりません。") program_title = get_text(channel, "title") if not program_title: program_title = "番組名不明" items = channel.findall("item") episodes = [] for item in items: episode_title = get_text(item, "title") audio_url = find_audio_url(item) pub_dt = parse_pub_date(item) if not audio_url: continue if pub_dt is None: continue episodes.append( { "program_title": program_title, "episode_title": episode_title, "audio_url": audio_url, "pub_dt": pub_dt, "feed_url": feed_url, } ) return episodes def collect_episodes(feed_urls): all_episodes = [] for url in feed_urls: try: rss_data = download_rss(url) episodes = parse_rss(rss_data, url) all_episodes.extend(episodes) print(f" 取得できたエピソード数: {len(episodes)} 件") except Exception as e: print(" このRSSの取得または解析でエラーが発生しました。") print(f" {e}") print(" 次のRSSへ進みます。") return all_episodes def remove_duplicates(episodes): seen = set() unique = [] for ep in episodes: audio_url = ep["audio_url"] if audio_url in seen: continue seen.add(audio_url) unique.append(ep) return unique def filter_recent_episodes(episodes): now = datetime.now(JST) cutoff = now - timedelta(days=DAYS) recent = [] for ep in episodes: if ep["pub_dt"] >= cutoff: recent.append(ep) return recent def make_playlist(episodes, shelf): if not episodes: raise ValueError( f"過去{DAYS}日分のエピソードが見つかりませんでした。" ) # 古い順に並べる episodes.sort(key=lambda ep: ep["pub_dt"]) playlist_lines = [ "#EXTM3U", f"# Shelf: {shelf['name']}", f"# Shelf file: {shelf['file']}", f"# Mode: 過去{DAYS}日分・古い順", ] for ep in episodes: pub_text = ep["pub_dt"].strftime("%Y-%m-%d %H:%M") program = ep["program_title"] title = ep["episode_title"] url = ep["audio_url"] playlist_lines.append(f"# {pub_text} / {program} / {title}") playlist_lines.append(url) Path(PLAYLIST_FILE).write_text( "\n".join(playlist_lines) + "\n", encoding="utf-8" ) print(f"{PLAYLIST_FILE} を作成しました。") print(f"プレイリスト内のエピソード数: {len(episodes)} 件") def main(): try: if len(sys.argv) >= 2 and sys.argv[1] in ["--list", "-l"]: show_shelves() return argument = sys.argv[1] if len(sys.argv) >= 2 else None shelf = resolve_shelf(argument) print("選択された棚:") print(f" 番号: {shelf['id']}") print(f" 名前: {shelf['name']}") print(f" ファイル: {shelf['file']}") print() feed_urls = read_feed_urls(shelf["file"]) print(f"RSS URL数: {len(feed_urls)} 本") episodes = collect_episodes(feed_urls) print(f"全取得エピソード数: {len(episodes)} 件") episodes = remove_duplicates(episodes) print(f"重複除去後: {len(episodes)} 件") episodes = filter_recent_episodes(episodes) print(f"過去{DAYS}日分: {len(episodes)} 件") make_playlist(episodes, shelf) print("完了しました。") except Exception as e: print("エラーが発生しました。") print(e) print() print("使い方を確認するには、次を実行してください。") print(" python3 make_playlist_5e.py --list") if __name__ == "__main__": main() 保存します。 Ctrl + O Enter Ctrl + X ステップ6:実行権限をつける chmod +x make_playlist_5e.py ただし、最初の実行は今回も python3 をつけるのがおすすめです。 python3 make_playlist_5e.py --list 次のように表示されればOKです。 使える棚: 1: 声日記・生活 (voice) -> shelf_01_voice_life.txt 2: 技術・AI・ものづくり (tech) -> shelf_02_tech_ai_mono.txt 3: 教育・研究・知の実践 (edu) -> shelf_03_education_research.txt 4: 本・文化・物語 (books) -> shelf_04_books_culture.txt 5: TanaRadio (tana) -> shelf_05_tanaradio.txt ステップ7:1番棚でプレイリストを作る まず、1番棚を使います。 python3 make_playlist_5e.py 1 または、 python3 make_playlist_5e.py voice うまくいくと、次のような表示になります。 選択された棚: 番号: 1 名前: 声日記・生活 ファイル: shelf_01_voice_life.txt RSS URL数: 5 本 RSSを取得します: https://... 取得できたエピソード数: 10 件 ... 全取得エピソード数: 50 件 重複除去後: 50 件 過去3日分: 12 件 playlist.m3u を作成しました。 プレイリスト内のエピソード数: 12 件 完了しました。 確認します。 head -n 20 playlist.m3u 次のように、棚名が入っていればOKです。 #EXTM3U # Shelf: 声日記・生活 # Shelf file: shelf_01_voice_life.txt # Mode: 過去3日分・古い順 # 2026-07-03 18:21 / 1分声日記 / 2026/07/03 18:21 https://... ここで、5Eの基本は成功です。 ステップ8:mpvで再生確認する まずは単純にmpvで再生します。 mpv playlist.m3u 音が出ればOKです。 mpvの操作はこれまで通りです。 スペースキー:一時停止/再開 Enter:次へ q:終了 確認できたら、q で終了します。 ステップ9:5Dの物理ボタン操作につなげる 5Eで作った playlist.m3u は、5Dの tanaradio_5d.py でそのまま使えます。 つまり、次の流れです。 python3 make_playlist_5e.py 1 ./tanaradio_5d.py これで、 1番棚のRSSから playlist.m3u を作る ↓ tanaradio_5d.py が playlist.m3u を再生する ↓ 3つの物理ボタンで操作する という形になります。 ボタン操作は5Dと同じです。 再生/一時停止ボタン:再生・一時停止 戻るボタン:前のエピソードへ 次へボタン:次のエピソードへ ここまでできると、TanaRadio Piはもうかなり「棚を持ったラジオ」になっています。 ステップ10:別の棚を試す 2番棚にRSSを入れてから試します。 nano shelf_02_tech_ai_mono.txt たとえば、次のように書きます。 # 技術・AI・ものづくり # ・・・ https://listen.style/p/xxxxx/rss # ・・・ https://listen.style/p/yyyyy/rss # ・・・ https://listen.style/p/zzzzz/rss 保存します。 Ctrl + O Enter Ctrl + X 2番棚でプレイリストを作ります。 python3 make_playlist_5e.py 2 または、 python3 make_playlist_5e.py tech 確認します。 head -n 20 playlist.m3u Shelf: 技術・AI・ものづくり と表示されればOKです。 再生します。 mpv playlist.m3u または、物理ボタンつきで再生します。 ./tanaradio_5d.py ステップ11:5つの棚を順番に整える 最初の棚構成は、次の形でよいと思います。 1:声日記・生活 nano shelf_01_voice_life.txt 入れる候補: 2:技術・AI・ものづくり nano shelf_02_tech_ai_mono.txt 入れる候補: 3:教育・研究・知の実践 nano shelf_03_education_research.txt 入れる候補: 4:本・文化・物語 nano shelf_04_books_culture.txt 入れる候補: 5:TanaRadio nano shelf_05_tanaradio.txt 入れる候補: TanaRadio 同じ番組を複数の棚に入れても大丈夫です。 棚は図書館の分類ではありません。 聴くときの気分で使う分類です。ここはゆるくて大丈夫です。 ステップ12:棚ごとの動作確認 それぞれの棚で試します。 python3 make_playlist_5e.py 1 head -n 10 playlist.m3u python3 make_playlist_5e.py 2 head -n 10 playlist.m3u python3 make_playlist_5e.py 3 head -n 10 playlist.m3u python3 make_playlist_5e.py 4 head -n 10 playlist.m3u python3 make_playlist_5e.py 5 head -n 10 playlist.m3u それぞれで、 # Shelf: ... の部分が変わればOKです。 たとえば、 # Shelf: 声日記・生活 # Shelf: 技術・AI・ものづくり # Shelf: TanaRadio のように変われば成功です。 ステップ13:実際の使い方 フェーズ5E段階での日常運用は、こうなります。 声日記・生活を聴く cd ~/tanaradio5 python3 make_playlist_5e.py 1 ./tanaradio_5d.py 技術・AI・ものづくりを聴く cd ~/tanaradio5 python3 make_playlist_5e.py 2 ./tanaradio_5d.py TanaRadioを聴く cd ~/tanaradio5 python3 make_playlist_5e.py 5 ./tanaradio_5d.py ここではまだ「棚を選んでから再生」ではなく、**「棚を指定してプレイリストを作ってから再生」**です。 次の5Fで、これを棚選びボタンに接続します。 フェーズ5Eの成功条件 フェーズ5Eの成功条件は、次の5つです。 1. shelf_01〜shelf_05 の棚ファイルを作れた 2. 棚ファイルにRSS URLを書けた 3. make_playlist_5e.py で棚を指定できた 4. 指定した棚に応じて playlist.m3u が作り直された 5. tanaradio_5d.py で、その playlist.m3u を物理ボタン操作できた ここまでできれば、5Eは成功です。 今回できるようになること 5Eが成功すると、TanaRadio Piはこう変わります。 フォロー中をまとめて流す装置 から、 声の棚を選んで流す装置 になります。 これはかなり大きいです。 まだ棚選びボタンはありませんが、考え方としてはもう「本棚」です。 今回の5Eを一言で言えば、こうです。 RSS一覧を、意味のある棚に分ける。 ここからTanaRadio Piは、単なるポッドキャスト再生機ではなく、たなさんの生活・研究・制作に合わせて育つ「声の本棚」になっていきます。次の5Fでは、この棚切り替えを物理ボタンに接続します。#声日記  #TanaRadioPi

374 日記 | 物理ボタンでプレイリスト再生を制御

374 日記 | 物理ボタンでプレイリスト再生を制御

Jul 4, 2026 25:57 たな

物理ボタンでプレイリスト再生の制御ができるようになりました(フェーズ5D完了)。*以下のtanaradio_5d.pyのコードは,修正済みのものです(この収録後も1回修正しました)。フェーズ5D 詳細マニュアル 既存3ボタンでRSSプレイリストを操作する 0. 今回の目標 今回やることは、これです。 フェーズ5Cで playlist.m3u を作る ↓ mpvで playlist.m3u を再生する ↓ GPIOの3ボタンでmpvを操作する 3つのボタンの役割は、今回は次のようにします。 再生/一時停止ボタン:再生・一時停止 戻るボタン:前のエピソードへ 次へボタン:次のエピソードへ mpvへの命令は次の3つです。 再生/一時停止:cycle pause 戻る:playlist-prev 次へ:playlist-next 構想メモでは、戻るボタンに「1回押し=先頭へ、2回押し=前へ、長押し=10秒戻る」という発展案があります。 ただし、5Dではまだ入れません。ここで欲張ると、ボタン判定の沼に入ります。沼は楽しいですが、今日はまだ岸辺でお茶を飲みます。 ステップ1:作業フォルダへ移動する Raspberry Piでターミナルを開きます。 cd ~/tanaradio5 確認します。 pwd 次のような表示になればOKです。 /home/ユーザー名/tanaradio5 ステップ2:5Cまでのファイルを確認する ls 少なくとも、次のようなファイルがあるはずです。 feeds.txt make_playlist_5c.py playlist.m3u スクリプト名は、たなさんが5Cで作った名前に合わせてください。 以下では、5Cのスクリプト名を make_playlist_5c.py として進めます。 ステップ3:5Cのプレイリストをもう一度作る まず、5Cのスクリプトを実行して、最新の playlist.m3u を作ります。 ./make_playlist_5c.py または、実行権限を付けていない場合は、 python3 make_playlist_5c.py うまくいけば、たとえば次のような表示になります。 playlist.m3u を作成しました。 音声URL数: 12 件 完了しました。 確認します。 ls -l playlist.m3u playlist.m3u が存在し、サイズが0でなければOKです。 中身も少し見ます。 head playlist.m3u 次のように、音声URLが入っていればOKです。 #EXTM3U # エピソードタイトル https://... ステップ4:mpvだけで再生できるか確認する ボタン操作に入る前に、まず普通に再生します。 mpv playlist.m3u 音が出ればOKです。 この状態では、キーボードで操作できます。 スペースキー:一時停止/再開 Enter:次へ q:終了 確認できたら、q を押してmpvを終了します。 ここで音が出ない場合は、5Dには進まず、音声出力側を先に確認してください。 ステップ5:GPIOライブラリを確認する 今回はPythonからGPIOボタンを読むために gpiozero を使います。 まず確認します。 python3 -c "import gpiozero; print('gpiozero OK')" 次のように出ればOKです。 gpiozero OK もしエラーが出たら、次を実行します。 sudo apt update sudo apt install -y python3-gpiozero python3-lgpio インストール後、もう一度確認します。 python3 -c "import gpiozero; print('gpiozero OK')" ステップ6:GPIO番号を確認する 今回のコードでは、フェーズ4で使ってきた3つのGPIOを次のように使います。 GPIO17:再生/一時停止 GPIO27:戻る、つまり前のエピソードへ GPIO22:次へ、つまり次のエピソードへ 配線はこれまで通り、基本形は次です。構想メモでも、ボタン入力には保険として抵抗を残す方針になっています。 GPIO ─ 1kΩ程度の抵抗 ─ ボタン ─ GND つまり、ボタンを押すとGPIOがGNDにつながる形です。 Python側では pull_up=True にします。 ステップ7:5D用スクリプトを作る 新しいファイルを作ります。 nano tanaradio_5d.py 次のコードをそのまま貼り付けます。 #!/usr/bin/env python3 from gpiozero import Button from signal import pause import json import os import signal import socket import subprocess import sys import time from pathlib import Path # ===== 設定ここから ===== BASE_DIR = Path(__file__).resolve().parent PLAYLIST_FILE = BASE_DIR / "playlist.m3u" SOCKET_PATH = "/tmp/tanaradio-mpv.sock" # フェーズ4で使った既存3ボタンのGPIO番号 PIN_PLAY_PAUSE = 17 # 再生/一時停止 PIN_BACK = 27 # 戻る:前のエピソードへ PIN_NEXT = 22 # 次へ:次のエピソードへ # ===== 設定ここまで ===== mpv_process = None def check_playlist(): if not PLAYLIST_FILE.exists(): print(f"{PLAYLIST_FILE} が見つかりません。") print("先にフェーズ5Cのスクリプトを実行して playlist.m3u を作ってください。") sys.exit(1) text = PLAYLIST_FILE.read_text(encoding="utf-8", errors="ignore").strip() if not text: print(f"{PLAYLIST_FILE} は空です。") sys.exit(1) def start_mpv(): global mpv_process if os.path.exists(SOCKET_PATH): os.remove(SOCKET_PATH) print("mpvを起動します。") print(f"プレイリスト: {PLAYLIST_FILE}") mpv_process = subprocess.Popen( [ "mpv", "--no-video", f"--input-ipc-server={SOCKET_PATH}", str(PLAYLIST_FILE), ], cwd=str(BASE_DIR), ) # mpvのIPCソケットができるまで少し待つ for _ in range(50): if os.path.exists(SOCKET_PATH): print("mpv操作用ソケットを確認しました。") return time.sleep(0.1) print("mpv操作用ソケットが見つかりません。") print("mpvが起動できていない可能性があります。") def send_mpv_command(command): payload = json.dumps({"command": command}).encode("utf-8") + b"\n" try: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: client.settimeout(1.0) client.connect(SOCKET_PATH) client.sendall(payload) # mpvからの返事を少し読む。 # これをしないと、mpv側に Broken pipe が出ることがある。 try: client.recv(4096) except socket.timeout: pass except FileNotFoundError: print("mpvのソケットが見つかりません。mpvが終了している可能性があります。") except ConnectionRefusedError: print("mpvに接続できませんでした。") except Exception as e: print("mpvへの命令送信でエラーが出ました。") print(e) def play_pause(): print("ボタン: 再生/一時停止") send_mpv_command(["cycle", "pause"]) def back_episode(): print("ボタン: 戻る:前のエピソードへ") send_mpv_command(["playlist-prev"]) def next_episode(): print("ボタン: 次へ") send_mpv_command(["playlist-next"]) def setup_buttons(): print("GPIOボタンを準備します。") print(f"再生/一時停止: GPIO{PIN_PLAY_PAUSE}") print(f"戻る: GPIO{PIN_BACK}") print(f"次へ: GPIO{PIN_NEXT}") play_button = Button(PIN_PLAY_PAUSE, pull_up=True, bounce_time=0.08) back_button = Button(PIN_BACK, pull_up=True, bounce_time=0.08) next_button = Button(PIN_NEXT, pull_up=True, bounce_time=0.08) play_button.when_pressed = play_pause back_button.when_pressed = back_episode next_button.when_pressed = next_episode # main側で保持するために返す return play_button, back_button, next_button def cleanup(signum=None, frame=None): global mpv_process print("\n終了します。") if mpv_process is not None and mpv_process.poll() is None: mpv_process.terminate() try: mpv_process.wait(timeout=5) except subprocess.TimeoutExpired: mpv_process.kill() if os.path.exists(SOCKET_PATH): try: os.remove(SOCKET_PATH) except OSError: pass sys.exit(0) def main(): signal.signal(signal.SIGINT, cleanup) signal.signal(signal.SIGTERM, cleanup) check_playlist() start_mpv() # 重要: # setup_buttons() の戻り値を変数に入れて保持する。 # これをしないと、環境によってはボタンが反応しなくなることがある。 buttons = setup_buttons() print("準備完了です。") print("3つの物理ボタンで操作できます。終了は Ctrl + C です。") pause() if __name__ == "__main__": main() 保存します。 Ctrl + O Enter Ctrl + X ステップ8:実行権限を付ける chmod +x tanaradio_5d.py ステップ9:5Dを実行する ./tanaradio_5d.py うまくいくと、次のような表示になります。 mpvを起動します。 プレイリスト: /home/ユーザー名/tanaradio5/playlist.m3u mpv操作用ソケットを確認しました。 GPIOボタンを準備します。 再生/一時停止: GPIO17 戻る: GPIO27 次へ: GPIO22 準備完了です。 3つの物理ボタンで操作できます。終了は Ctrl + C です。 この状態で、音声が流れ始めます。 ステップ10:3つのボタンを試す 10-1. 再生/一時停止ボタン GPIO17につながっているボタンを押します。 ターミナルに次のように出ればOKです。 ボタン: 再生/一時停止 音声が一時停止します。 もう一度押すと再開します。 10-2. 次へボタン GPIO22につながっているボタンを押します。 ボタン: 次へ 次のエピソードへ進めばOKです。 10-3. 戻るボタン GPIO27につながっているボタンを押します。 ボタン: 戻る:前のエピソードへ 前のエピソードへ戻ればOKです。 ただし、プレイリストの最初のエピソードを再生しているときは、それ以上前へ戻れないので、動きがわかりにくい場合があります。 その場合は、先に「次へ」を1〜2回押してから「戻る」を試してください。 ステップ11:終了する 終了するときは、ターミナルで次を押します。 Ctrl + C 次のように出ればOKです。 終了します。 mpvも一緒に終了します。 ステップ12:今回の成功条件 フェーズ5Dの成功条件は、次の4つです。 1. フェーズ5Cの playlist.m3u が作れている 2. tanaradio_5d.py を実行すると mpv が起動する 3. 再生/一時停止ボタンで pause / resume ができる 4. 戻る・次へボタンで playlist-prev / playlist-next ができる ここまでできれば、5Dは成功です。 これはかなり大きいです。 5Cまでは「RSSから音声を集める仕組み」でしたが、5Dでついに「物理ボタンで声の流れを操る装置」になります。ラジオ感が、ぐっと前に出てきます。#声日記 #TanaRadioPi

373 講義 | 「ほどよい」技術

373 講義 | 「ほどよい」技術

Jul 3, 2026 19:02 たな

この音源は、最近収録した今学期の「科学と技術の社会史」の講義(ビデオ教材)の最後の部分です。現在製作中のTanaRadio Piの話も出てきます。#講義 #TanaRadioPi

372 日記 | 複数RSSで過去3日分を古い順に再生

372 日記 | 複数RSSで過去3日分を古い順に再生

Jul 2, 2026 12:13 たな

複数RSSで過去3日分を古い順に再生できるようになりまいした(フェーズ5C完了)。*NHKの番組を再生している部分はカットしました。フェーズ5C 詳細マニュアル 複数RSSから「過去3日分・古い順」のプレイリストを作る 0. 今回の目標 5Cでやることは、これです。 複数のRSS URLを feeds.txt に書く ↓ PythonでRSSを順番に読む ↓ 各RSSからエピソードを集める ↓ 過去3日分だけに絞る ↓ 公開日時の古い順に並べる ↓ playlist.m3u を作る ↓ mpvで連続再生する 今回も、まだやらないことがあります。 棚選び 再生モード選び LED表示 追加ボタン ロータリーエンコーダー 再生履歴 ここ、大事です。 5Cは「複数RSS化だけ」です。まだラジオの操作部品は増やしません。焦らないのが勝ち筋です。 ステップ1:作業フォルダへ移動する Raspberry Piでターミナルを開きます。 cd ~/tanaradio5 確認します。 pwd 次のような表示ならOKです。 /home/pi/tanaradio5 ユーザー名が違う場合は、pi の部分は別名になります。 ステップ2:5Bまでのファイルを確認する ls おそらく、次のようなファイルがあるはずです。 feed.txt make_playlist_5a.py make_playlist_5b.py playlist.m3u 5Cでは、5Bのファイルを壊さず、新しく次の2つを作ります。 feeds.txt make_playlist_5c.py 5Bはそのまま残します。 動いたものは残す。これは電子工作でもプログラムでも鉄則です。動いたものを消すと、あとで泣きます。小さく泣くならまだしも、Linux相手だとわりと本気で泣きます。 ステップ3:複数RSS用の feeds.txt を作る まず、RSS URLを複数書くためのファイルを作ります。 nano feeds.txt 中には、RSS URLを1行に1本ずつ書きます。 例です。 https://listen.style/p/xxxxx/rss https://listen.style/p/yyyyy/rss https://listen.style/p/zzzzz/rss 最初は欲張らず、まずは2本でよいです。 https://listen.style/p/xxxxx/rss https://listen.style/p/yyyyy/rss 保存します。 Ctrl + O Enter Ctrl + X 確認します。 cat feeds.txt RSS URLが複数行で表示されればOKです。 ステップ4:5C用のPythonスクリプトを作る 新しいファイルを作ります。 nano make_playlist_5c.py 次のコードをそのまま貼り付けてください。 #!/usr/bin/env python3 import urllib.request import xml.etree.ElementTree as ET from pathlib import Path from datetime import datetime, timedelta, timezone from email.utils import parsedate_to_datetime from zoneinfo import ZoneInfo FEEDS_FILE = "feeds.txt" PLAYLIST_FILE = "playlist.m3u" # 5Bと同じく「過去3日分」 DAYS = 3 JST = ZoneInfo("Asia/Tokyo") def read_feed_urls(): path = Path(FEEDS_FILE) if not path.exists(): raise FileNotFoundError(f"{FEEDS_FILE} が見つかりません。") urls = [] for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() # 空行と # で始まるコメント行は無視する if not line: continue if line.startswith("#"): continue urls.append(line) if not urls: raise ValueError(f"{FEEDS_FILE} にRSS URLが書かれていません。") return urls def download_rss(url): print(f"RSSを取得します: {url}") request = urllib.request.Request( url, headers={ "User-Agent": "TanaRadio-Pi/5C" } ) with urllib.request.urlopen(request, timeout=20) as response: return response.read() def get_text(element, tag_name): child = element.find(tag_name) if child is not None and child.text: return child.text.strip() return "" def find_audio_url(item): # 通常のPodcast RSSでは enclosure に音声URLが入る enclosure = item.find("enclosure") if enclosure is not None: audio_url = enclosure.attrib.get("url") if audio_url: return audio_url # 念のため media:content 的な形式にも軽く対応する for child in item: if child.tag.endswith("content"): audio_url = child.attrib.get("url") if audio_url: return audio_url return None def parse_pub_date(item): pub_date_text = get_text(item, "pubDate") if not pub_date_text: return None try: dt = parsedate_to_datetime(pub_date_text) # タイムゾーン情報がない場合はUTC扱いにする if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(JST) except Exception: return None def parse_rss(rss_data, feed_url): root = ET.fromstring(rss_data) channel = root.find("channel") if channel is None: raise ValueError("RSSのchannelが見つかりません。") program_title = get_text(channel, "title") if not program_title: program_title = "番組名不明" items = channel.findall("item") episodes = [] for item in items: episode_title = get_text(item, "title") audio_url = find_audio_url(item) pub_dt = parse_pub_date(item) if not audio_url: continue if pub_dt is None: continue episodes.append({ "program_title": program_title, "episode_title": episode_title, "audio_url": audio_url, "pub_dt": pub_dt, "feed_url": feed_url, }) return episodes def collect_episodes(feed_urls): all_episodes = [] for url in feed_urls: try: rss_data = download_rss(url) episodes = parse_rss(rss_data, url) all_episodes.extend(episodes) print(f" 取得できたエピソード数: {len(episodes)} 件") except Exception as e: print(" このRSSの取得または解析でエラーが発生しました。") print(f" {e}") print(" 次のRSSへ進みます。") return all_episodes def filter_recent_episodes(episodes): now = datetime.now(JST) cutoff = now - timedelta(days=DAYS) recent = [] for ep in episodes: if ep["pub_dt"] >= cutoff: recent.append(ep) return recent def remove_duplicates(episodes): seen = set() unique = [] for ep in episodes: audio_url = ep["audio_url"] if audio_url in seen: continue seen.add(audio_url) unique.append(ep) return unique def make_playlist(episodes): if not episodes: raise ValueError( f"過去{DAYS}日分のエピソードが見つかりませんでした。" ) # 古い順に並べる episodes.sort(key=lambda ep: ep["pub_dt"]) playlist_lines = ["#EXTM3U"] for ep in episodes: pub_text = ep["pub_dt"].strftime("%Y-%m-%d %H:%M") program = ep["program_title"] title = ep["episode_title"] url = ep["audio_url"] playlist_lines.append(f"# {pub_text} / {program} / {title}") playlist_lines.append(url) Path(PLAYLIST_FILE).write_text( "\n".join(playlist_lines) + "\n", encoding="utf-8" ) print(f"{PLAYLIST_FILE} を作成しました。") print(f"プレイリスト内のエピソード数: {len(episodes)} 件") def main(): try: feed_urls = read_feed_urls() print(f"RSS URL数: {len(feed_urls)} 本") episodes = collect_episodes(feed_urls) print(f"全取得エピソード数: {len(episodes)} 件") episodes = remove_duplicates(episodes) print(f"重複除去後: {len(episodes)} 件") episodes = filter_recent_episodes(episodes) print(f"過去{DAYS}日分: {len(episodes)} 件") make_playlist(episodes) print("完了しました。") except Exception as e: print("エラーが発生しました。") print(e) if __name__ == "__main__": main() 保存します。 Ctrl + O Enter Ctrl + X ステップ5:実行権限をつける chmod +x make_playlist_5c.py ただし、実行はまず次の形でやるのがおすすめです。 python3 make_playlist_5c.py 以前のように、Pythonファイルをシェルとして実行してしまう事故を避けるためです。 python3 を前につければ、確実にPythonとして実行されます。 ステップ6:プレイリストを作る 実行します。 python3 make_playlist_5c.py うまくいくと、次のような表示になります。 RSS URL数: 2 本 RSSを取得します: https://listen.style/p/xxxxx/rss 取得できたエピソード数: 10 件 RSSを取得します: https://listen.style/p/yyyyy/rss 取得できたエピソード数: 10 件 全取得エピソード数: 20 件 重複除去後: 20 件 過去3日分: 5 件 playlist.m3u を作成しました。 プレイリスト内のエピソード数: 5 件 完了しました。 ここで見るポイントは、次の3つです。 RSS URL数 過去3日分 プレイリスト内のエピソード数 プレイリスト内のエピソード数 が1件以上なら成功です。 ステップ7:playlist.m3u の中身を確認する head -n 30 playlist.m3u 次のように表示されればOKです。 #EXTM3U # 2026-07-01 08:30 / 番組A / エピソードタイトル https://... # 2026-07-01 10:15 / 番組B / エピソードタイトル https://... ここで確認したいのは、複数番組が混ざっているかどうかです。 たとえば、 番組A 番組B 番組A 番組C のように並んでいれば、5Cらしい動きになっています。 単にRSSを順番に再生しているのではなく、複数RSSのエピソードを集めて、公開日時で並べ替えているわけです。 ここが5Cの肝です。 ステップ8:mpvで再生する mpv playlist.m3u 音が出れば成功です。 mpvの基本操作はこれです。 スペースキー:一時停止/再開 Enter:次のエピソードへ q:終了 再生される順番が、過去3日分の古い順になっているか、耳でざっくり確認してください。 厳密に確認したい場合は、playlist.m3u のコメント行を見ます。 grep '^# 20' playlist.m3u 次のように日時が古い順に並んでいればOKです。 # 2026-06-29 09:00 / ... # 2026-06-30 14:20 / ... # 2026-07-01 08:30 / ... ステップ9:RSSを少しずつ増やす 最初の2本でうまく行ったら、feeds.txt にRSSを追加します。 nano feeds.txt 例です。 # TanaRadio https://listen.style/p/xxxxx/rss # 気になる番組1 https://listen.style/p/yyyyy/rss # 気になる番組2 https://listen.style/p/zzzzz/rss このスクリプトでは、空行と # で始まる行は無視されます。 なので、コメントを書いておけます。 保存したら再実行します。 python3 make_playlist_5c.py mpv playlist.m3u RSSを増やすと、かなり「フォロー中ラジオ」感が出てきます。 この段階でもう、TanaRadio Piは単なる1番組再生機ではなくなります。 ステップ10:過去3日分が空だった場合 次のように出ることがあります。 過去3日分: 0 件 エラーが発生しました。 過去3日分のエピソードが見つかりませんでした。 これは故障ではありません。 原因は単純で、登録したRSSに最近のエピソードがないだけです。 その場合は、テスト用に日数を一時的に増やします。 nano make_playlist_5c.py この部分を探します。 DAYS = 3 一時的にこうします。 DAYS = 30 保存して、再実行します。 python3 make_playlist_5c.py これでエピソードが出てくるなら、スクリプトは正常です。 テストが終わったら、また戻します。 DAYS = 3 ステップ11:成功条件 フェーズ5Cの成功条件は、次の4つです。 1. feeds.txt にRSS URLを複数本書けた 2. make_playlist_5c.py を実行できた 3. 複数RSS由来のエピソードが playlist.m3u に入った 4. mpv playlist.m3u で連続再生できた 特に重要なのは3です。 複数RSS ↓ 1つのプレイリスト ↓ 過去3日分 ↓ 古い順 これができれば、5Cは成功です。 5Cでできるようになったこと 5Cが成功すると、TanaRadio Piはこうなります。 1つの番組を聴く装置 から、 複数番組をまとめて流す装置 に変わります。 これは地味ですが、かなり大きな変化です。 もう「ポッドキャストを1本ずつ選んで聴く」のではなく、「登録してある声が時間順に流れてくる」状態になります。 ここで、かなりラジオらしくなります。 次のフェーズ5Dでやること 次の5Dでは、フェーズ4で作った既存の3ボタンを、この新しい playlist.m3u 再生に結びつけます。 予定としては、まず単純にこうします。 再生/一時停止ボタン:pause切り替え 戻るボタン:前のエピソードへ 次へボタン:次のエピソードへ 構想メモでは戻るボタンに「1回押し・2回押し・長押し」の区別も出ていますが、5Dの最初ではまだ入れなくてよいです。 まずは「物理ボタンでプレイリストを前後できる」だけで十分です。 今回の5Cは、ひとことで言えばこうです。 複数の声を、1本の時間の流れにする。 ここまで来ると、TanaRadio Piはかなり「声の本棚」の入口に立っています。これは順調です。#声日記 #TanaRadioPi

Kanayafrica

Kanayafrica

エジプトのこと,長崎のこと,世界遺産のことについてお話をします. 新型コロナウイルス感染症(2020年)の影響で大学の講義のオンライン化,リモート化が進んでいます.従来は講義の合間に余談を挟むことが出来たのですが,オンライン授業などではなかなか出来ないので,余談だけ切り出してポッドキャストにしました. YouTube版はこちらです👉 https://www.youtube.com/channel/UCzoDl88mrhS2Z2_8L6PfLRA 匿名質問を受け付けています.回答はツイッターで行います👉 https://marshmallow-qa.com/kanaya https://listen.style/p/kanaya?ATttQN7X

おふとんの中から声月記を呟く

おふとんの中から声月記を呟く

お布団の中から寝ぼけながらボソボソと声月記のようなことをつぶやいている人がいます。

【ショート】STEAM NEWSまとめ

【ショート】STEAM NEWSまとめ

配信テスト中です. https://listen.style/p/steamnews?461WWtmZ

TanaRadio Magazine

TanaRadio Magazine

TanaRadioの配信をテーマごとにまとめたマガジンです。このマガジンのメインコンテンツである各エピソードは,この概要欄に掲載されたプレーヤー(あるいはプレーヤーの見出しリンク)から再生してください。 関連: TanaRadio ( https://listen.style/p/tanaradio/ ) TanaRadio 2 ( https://listen.style/p/tanaradio2/ )

TanaRadio 2

TanaRadio 2

私立工科系大学で科学史を教えている教員がお送りする有料配信チャンネル。(現在,休止中)

LISTEN_staff

LISTEN_staff

テスト用アカウントです。 ポッドキャストの書き起こしサービス「LISTEN」はこちら https://listen.style/p/staff_test?uT1WThX6