Skip to content

feat(#23): now-playing アプリの PID を NowPlayingInfo まで伝搬#294

Merged
GeneralD merged 2 commits into
mainfrom
feat/23-nowplaying-pid-pipeline
Jul 3, 2026
Merged

feat(#23): now-playing アプリの PID を NowPlayingInfo まで伝搬#294
GeneralD merged 2 commits into
mainfrom
feat/23-nowplaying-pid-pipeline

Conversation

@GeneralD

@GeneralD GeneralD commented Jul 3, 2026

Copy link
Copy Markdown
Owner

agent type breaking scope diff files tests relates

概要

スペクトラムアナライザ (#23) の前提工事として、now-playing アプリの PID を MediaRemote helper から NowPlayingInfo まで伝搬する。CoreAudio process tap(macOS 14.2+ の AudioHardwareCreateProcessTap)は対象プロセスの PID を要求するため、このパイプラインが tap 実装の入口になる。

この PR は #23 を close しない — アナライザ本体(AudioTapDataSource / SpectrumInteractor / Presenter / View / config)は後続 PR で実装する。

変更内容

media-remote-helper → MediaRemoteDataSource → NowPlaying (Entity) → TrackHandler → NowPlayingInfo
モジュール 変更
media-remote-helper.swift MRMediaRemoteGetNowPlayingApplicationPID任意解決で追加。シンボル欠落時は pid なしペイロードに縮退し helper 自体は死なない。PID 0(セッション所有アプリなし)は nil に正規化
MediaRemoteDataSourceImpl JSON ペイロードの pid フィールドをデコード
Entity/NowPlaying pid: Int? 追加(内部エンティティ)
Entity/NowPlayingInfo pid: Int? 追加 — lyra track の JSON 出力に pid が現れる
TrackHandlerImpl lyrics あり/なし両パスで pid をパススルー

設計メモ

テスト

  • MediaRemoteDataSourceImplTests: pid ありペイロード → surfaced、pid なし → nil
  • TrackHandlerImplTests: デフォルトクエリ/lyrics パス両方で passthrough を検証
  • 全 978 テストパス
実機 JSON デモを載せていない理由

now-playing セッション不在の環境では lyra track が出力せず待ち続けるため、実出力を採取できなかった。変更前の brew 2.16.0 でも同一症状(コントロールテスト済み)で、この diff とは無関係の既存挙動 → #295 に起票済み。helper 単体(/usr/bin/swift <bundle>/media-remote-helper.swift)は本変更後も {"has_info":false} を即時継続出力しており、helper 側の縮退は正常。

TODO(後続)

Relates to #23

GeneralD added 2 commits July 3, 2026 12:23
スペクトラムアナライザ (#23) の CoreAudio process tap が音源プロセスを
特定できるよう、MediaRemote helper で
MRMediaRemoteGetNowPlayingApplicationPID を任意解決し、
Entity → DataSource → Handler の 5 モジュールを通して pid を伝搬する。

- helper はシンボル欠落時に pid なしへ縮退(helper 全体は死なない)
- PID 0(セッション所有アプリなし)は nil に正規化
- lyra track の JSON 出力に pid フィールドが追加される
Copilot AI review requested due to automatic review settings July 3, 2026 03:36
@GeneralD GeneralD self-assigned this Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an optional pid property to NowPlaying and NowPlayingInfo entities, threads it through the media-remote-helper CLI (resolving via dlsym), fixes a missing separator in MediaRemoteDataSourceImpl.poll(), propagates pid through TrackHandlerImpl, adds tests, and bumps the version file.

Changes

PID propagation

Layer / File(s) Summary
Entity pid property
Sources/Entity/NowPlaying.swift, Sources/Entity/NowPlayingInfo.swift
Adds an optional pid: Int? property and initializer parameter (default nil) to both NowPlaying and NowPlayingInfo.
Helper PID resolution and payload
Sources/MediaRemoteDataSource/Resources/media-remote-helper.swift
Resolves MRMediaRemoteGetNowPlayingApplicationPID via dlsym, splits printing into printInfo(event:pid:), and reworks fetchAndPrint to include pid in the emitted JSON.
Data source pid mapping and tests
Sources/MediaRemoteDataSource/MediaRemoteDataSourceImpl.swift, Tests/MediaRemoteDataSourceTests/MediaRemoteDataSourceImplTests.swift
Fixes the NowPlaying initializer call so pid is correctly parsed after timestamp, extends the jsonLine test helper with an optional pid, and adds a test asserting pid decoding across polls.
TrackHandler pid passthrough and tests
Sources/TrackHandler/TrackHandlerImpl.swift, Tests/TrackHandlerTests/TrackHandlerImplTests.swift
Reformats NowPlayingInfo initializer calls to include pid in both no-lyrics and lyrics paths, extends NowPlaying.stub with pid, and adds passthrough tests for both paths.
Version bump
Sources/VersionHandler/Resources/version.txt
Updates version from 2.16.0 to 2.17.0.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Timer
  participant fetchAndPrint
  participant GetPIDFn
  participant printInfo
  participant stdout

  Timer->>fetchAndPrint: event (.trackChange/.tick)
  fetchAndPrint->>GetPIDFn: resolve pid via dlsym
  GetPIDFn-->>fetchAndPrint: pid or nil (0 mapped to nil)
  fetchAndPrint->>printInfo: event, pid
  printInfo->>stdout: JSON payload with pid field
Loading

Related issues: None specified.

Related PRs: None specified.

Suggested labels: enhancement

Suggested reviewers: GeneralD

🐰 A hop, a skip, a pid appended,
through helper, handler, all extended,
from zero mapped to gentle nil,
the tracks now carry process still.
Version bumped, the tests all pass—
this rabbit's build is first in class! 🥕

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: propagating the now-playing app PID to NowPlayingInfo.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/23-nowplaying-pid-pipeline

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prepares the now-playing pipeline for the upcoming spectrum analyzer work (#23) by propagating the now-playing application PID from the MediaRemote helper output through the DataSource and Entity layers into NowPlayingInfo (surfaced by lyra track output).

Changes:

  • Extend media-remote-helper.swift to (optionally) resolve MRMediaRemoteGetNowPlayingApplicationPID, normalize PID 0 to nil, and emit "pid" in the helper JSON payload.
  • Decode the "pid" field in MediaRemoteDataSourceImpl, add pid: Int? to NowPlaying / NowPlayingInfo, and pass it through TrackHandlerImpl for both non-lyrics and lyrics paths.
  • Add unit tests covering PID decoding and PID passthrough, and bump the app version.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
Sources/MediaRemoteDataSource/Resources/media-remote-helper.swift Optionally fetches now-playing app PID and includes it in helper JSON output (PID 0 → nil).
Sources/MediaRemoteDataSource/MediaRemoteDataSourceImpl.swift Decodes "pid" from the helper payload into NowPlaying.
Sources/Entity/NowPlaying.swift Adds pid: Int? to the internal now-playing entity.
Sources/TrackHandler/TrackHandlerImpl.swift Passes pid through into NowPlayingInfo on both code paths.
Sources/Entity/NowPlayingInfo.swift Adds pid: Int? to the public Codable output entity.
Tests/MediaRemoteDataSourceTests/MediaRemoteDataSourceImplTests.swift Tests that PID is surfaced when present and nil when absent.
Tests/TrackHandlerTests/TrackHandlerImplTests.swift Tests PID passthrough for default and lyrics queries.
Sources/VersionHandler/Resources/version.txt Bumps version to 2.17.0.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@GeneralD GeneralD merged commit a70f555 into main Jul 3, 2026
5 checks passed
@GeneralD GeneralD deleted the feat/23-nowplaying-pid-pipeline branch July 3, 2026 07:55
GeneralD added a commit that referenced this pull request Jul 3, 2026
![agent](https://img.shields.io/badge/agent-claude-D97757)
![type](https://img.shields.io/badge/type-feat-blue)
![breaking](https://img.shields.io/badge/breaking-no-green)
![scope](https://img.shields.io/badge/scope-spectrum-blue)
![diff](https://img.shields.io/badge/diff-+4458%20--221-green)
![files](https://img.shields.io/badge/files-94%20files-red)
![tests](https://img.shields.io/badge/tests-added-green)
![review](https://img.shields.io/badge/review-thorough-yellow)


![スペクトラムアナライザ動作中](https://github.com/user-attachments/assets/e71e48be-26c5-4a84-b212-c10989971cb2)

## 概要

now-playing アプリの音声を CoreAudio process tap
で捕捉し、オーバーレイにリアルタイムのスペクトラムアナライザ(バーグラフ)を描画する。Closes #23

## 変更内容

- **Entity**: `SpectrumConfig` / `SpectrumStyle` / `SpectrumPlacement` /
`AudioSourceState` を追加。`[spectrum]` TOML セクション(bar_count / bar_color /
placement / decay_rate / fft_size 等、全項目デフォルトあり・既定 disabled)
- **NowPlayingRepository**: `stream()` を multicast 化(単一ポンプ +
全購読者ブロードキャスト + 直近値 replay)。MediaRemote ヘルパーのパイプは単一消費者だが、任意のレイヤーが
`observeNowPlaying()` を自由に購読できる
- **AudioTapDataSource**: `ProcessTapEngine`(macOS 14.4+ API、14.0
ターゲット向けに availability erasure)+ swift-atomics による SPSC
ロックフリーリングバッファ。IOProc コールバックは RT-safe(アロケーション・ロック・Swift concurrency なし)
- **タップ対象はプロセスサブツリー全体**: Chromium 系ブラウザは音声をヘルパープロセスが出すため、メイン pid
のみのタップでは無音になる(Arc の Browser Helper
で実測)。`kAudioHardwarePropertyProcessObjectList` を ppid ウォークでフィルタして全対象を
`CATapDescription` に渡す
- **FrequencyAnalyzer**: Hann 窓 → vDSP FFT → dB 正規化 → bar
変換の純粋モジュール(依存ゼロ)
- **SpectrumUseCase / AudioCaptureRepository**: Interactor → UseCase →
Repository → DataSource の層を完全に通す新モジュール。`SpectrumUseCase`
がサンプル取得(Repository 経由)と FrequencyAnalyzer
による周波数変換を組み合わせ、`AudioCaptureRepository` がタップ DataSource を抽象化する(層飛ばしゼロ)
- **SpectrumInteractor**: `PlaybackUseCase.observeNowPlaying()` を単一の
`for await` で直接消費(Interactor 間依存なし)。直列消費により pause/play
連打でもタップ生成/破棄が交錯せず、pid/再生状態の重複排除も自前で行う。排他は
`OSAllocatedUnfairLock(state:)`。キャプチャ操作と magnitudes 取得は
`SpectrumUseCase` へ委譲
- **SpectrumPresenter / SpectrumView**: DisplayLink tick
で指数減衰マージ、`binHeights()` は読み取り専用(Canvas 描画中の @published 変更なし)。#252/#258
のゼロアイドルコストパターン(条件付き include + `TimelineView(paused:)`)
- **Info.plist**: `NSAudioCaptureUsageDescription` を追加(システムオーディオ録音 TCC)
- バージョン 2.17.0 → 2.18.0(minor)

## 背景・動機

issue #23 の本体実装。前提工事の PID 伝搬は #294 で完了済み。設計判断(14.4 フロア / ブラウザはそのままタップ /
swift-atomics)はセッション内で確認済み。

## テスト計画

- [x] `swift test` 全 1027 件パス(新規: FrequencyAnalyzer / SampleRingBuffer /
AudioTapDataSourceImpl / SpectrumInteractorImpl / SpectrumUseCaseImpl /
AudioCaptureRepositoryImpl / SpectrumPresenter / `[spectrum]` デコード /
NowPlayingRepository multicast)
- [x] `make lint` パス
- [x] 実機検証: launchd 経由で debug ビルドを起動し、Arc 再生中にバー描画をスクリーンショットで確認(上図)
- [x] CI-safe: タップ系テストは不明 pid で TCC 手前で失敗するため CI でプロンプトは出ない

## 備考

- **既知の制限(設計判断)**: タップはプロセスツリー単位。ブラウザ再生時は全タブの音声が混ざる(README に記載)
- **開発時の TCC 注意**: ターミナル起動のデーモンは TCC
責任プロセスがターミナルになり、プロンプトが出ずに無音になる。launchd(LaunchAgent)経由で起動すること(CLAUDE.md
に記載)
- スペクトラム機能は macOS 14.4+ が必要(14.0〜14.3 は startTap が false を返し無効化)

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants