Podcast thumbnail for TanaRadio

by たな

401 episodes
Updated Daily
Accepts GuestsHas SponsorsLocation 🇯🇵

Podcast Overview

私立工科系大学教員(科学史)の日々の出来事や雑感。日記,メモ,ブログなど TanaRadio Blog ( https://tanakahi.hatenablog.com/ )

Language

🇯🇵

Publishing Since

11/6/2023

1 verified contact email on file for TanaRadio

Pitch yourself as a guest, propose sponsorships, or reach out directly to the host.

Recent Episodes

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

July 10, 2026

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

<p>フェーズ5Jに行こうとしましたが,その前にRaspberry Piをケースに入れるところまでで終わってしまいました。</p><p>*なぜかノイズキャンセリングが効いているようで,とても聴きにくいです。</p><p>#声日記 #TanaRadioPi</p> <a href="https://listen.style/p/tanaradio/ozkli56i">LISTENで開く</a>

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

July 9, 2026

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

<p>まず,ロータリーエンコーダーをDIP化基板にハンダ付けして,それからこれを使って音量調整できるようにしました(フェーズ5I完了)。</p><pre><code class="language-plaintext">フェーズ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 &lt; MIN_VOLUME_INTERVAL: return last_volume_time = now if delta &gt; 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 &lt; MIN_VOLUME_INTERVAL: return last_volume_time = now if delta &gt; 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工作ではなく、手元の音声装置です。良いところまで来ています。</code></pre><p>#声日記 #TanaRadioPi</p> <a href="https://listen.style/p/tanaradio/gly2e5wr">LISTENで開く</a>

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

July 8, 2026

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

<p>再生モード切り替えのためのボタンとLEDを取り付けました(フェーズ5G完了)。</p><pre><code class="language-plaintext">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 &lt; 0 or value &gt;= 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 &gt;= 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: ロータリーエンコーダーで音量調整する 私のおすすめは、まだ再生履歴には行かず、先にロータリーエンコーダーを音量用として追加することです。理由は単純で、音量ダイヤルが付くと一気にラジオ感が出るからです。やる気が出る工作は、正義です。</code></pre><p>#声日記 #TanaRadioPi</p> <a href="https://listen.style/p/tanaradio/ncwd7c23">LISTENで開く</a>

401 total episodes available

Deep-dive analytics for TanaRadio

Frequently asked questions

Have a different question and can't find the answer you're looking for? Reach out to our support team by sending us an email and we'll get back to you as soon as we can.

What is TanaRadio?

私立工科系大学教員(科学史)の日々の出来事や雑感。日記,メモ,ブログなど TanaRadio Blog ( https://tanakahi.hatenablog.com/ )

How often does this podcast release new episodes?

This podcast updates daily.

Where can I listen to this podcast?

This podcast is available on 8 platforms including Apple Podcasts, Spotify, and more. You can also use the RSS feed directly.

Does this podcast accept guests?

Yes, this podcast regularly features guests.

Legal Disclaimer

Pod Engine is not affiliated with, endorsed by, or officially connected with any of the podcasts displayed on this platform. We operate independently as a podcast discovery and analytics service.

All podcast artwork, thumbnails, and content displayed on this page are the property of their respective owners and are protected by applicable copyright laws. This includes, but is not limited to, podcast cover art, episode artwork, show descriptions, episode titles, transcripts, audio snippets, and any other content originating from the podcast creators or their licensors.

We display this content under fair use principles and/or implied license for the purpose of podcast discovery, information, and commentary. We make no claim of ownership over any podcast content, artwork, or related materials shown on this platform. All trademarks, service marks, and trade names are the property of their respective owners.

While we strive to ensure all content usage is properly authorized, if you are a rights holder and believe your content is being used inappropriately or without proper authorization, please contact us immediately at hey@podengine.ai for prompt review and appropriate action, which may include content removal or proper attribution.

By accessing and using this platform, you acknowledge and agree to respect all applicable copyright laws and intellectual property rights of content owners. Any unauthorized reproduction, distribution, or commercial use of the content displayed on this platform is strictly prohibited.