<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Klakk 開発チーム</title>
    <description>The latest articles on DEV Community by Klakk 開発チーム (@klakkapp).</description>
    <link>https://dev.to/klakkapp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064933%2Fe2442ee9-3419-4fd1-a8a0-7b2051963412.png</url>
      <title>DEV Community: Klakk 開発チーム</title>
      <link>https://dev.to/klakkapp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/klakkapp"/>
    <language>en</language>
    <item>
      <title>macOSメニューバーアプリの「無効化」でAudioEngineまで止めない理由</title>
      <dc:creator>Klakk 開発チーム</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:26:52 +0000</pubDate>
      <link>https://dev.to/klakkapp/macosmeniyubaapurinowu-xiao-hua-deaudioenginemadezhi-menaili-you-17gh</link>
      <guid>https://dev.to/klakkapp/macosmeniyubaapurinowu-xiao-hua-deaudioenginemadezhi-menaili-you-17gh</guid>
      <description>&lt;p&gt;キー入力に短い効果音を付けるmacOSアプリでは、「無効化」をどこまで停止させるかが設計上の分岐になります。Event Tapだけを止めるのか、&lt;code&gt;AVAudioEngine&lt;/code&gt;とPlayer Nodeも止めるのか、準備済みPCMバッファまで破棄するのか。&lt;/p&gt;

&lt;p&gt;この記事では、有料MacアプリKlakkの現在の実装を例に、メニューバーから無効化したときに入力監視だけを停止し、Audio Engineを維持している理由と、その代わりに残る境界を整理します。&lt;/p&gt;

&lt;p&gt;私はKlakkの開発者本人です。記事末尾に自社製品へのリンクがあります。&lt;/p&gt;

&lt;h2&gt;
  
  
  無効化で止めたいものを先に分ける
&lt;/h2&gt;

&lt;p&gt;現在のKlakkでは、メニューバーの切り替えが次の3層に影響します。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;AppSettings.shared.isKlakkEnabled&lt;/code&gt; の永続状態&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KeyboardMonitor&lt;/code&gt; のEvent TapとRunLoop Source&lt;/li&gt;
&lt;li&gt;メニュー表示とAnalyticsイベント&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;一方、&lt;code&gt;AudioEngine.shared.stop()&lt;/code&gt; はこの切り替えから呼びません。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@objc&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;toggleKlakk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSMenuItem&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;AppSettings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isKlakkEnabled&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;AnalyticsService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trackKlakkToggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AppSettings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isKlakkEnabled&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kt"&gt;AppSettings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isKlakkEnabled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;KeyboardMonitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startMonitoring&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;KeyboardMonitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopMonitoring&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;statusItem&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMenu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ここでの「無効化」は、次のキー入力から新しい音をトリガーしないことを主目的にしています。&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Tapを無効化するだけでは足りない
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;KeyboardMonitor.stopMonitoring()&lt;/code&gt; は、&lt;code&gt;CGEvent.tapEnable(..., false)&lt;/code&gt;だけでは終わりません。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;stopMonitoring&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;isMonitoring&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;eventTap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;CGEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tapEnable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;eventTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kt"&gt;CFMachPortInvalidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventTap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;runLoopSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;CFRunLoopRemoveSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;CFRunLoopGetCurrent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;runLoopSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commonModes&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runLoopSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="n"&gt;isMonitoring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="n"&gt;pressedKeys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;previousModifierFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Event Tapを無効化し、Mach Portをinvalidateし、RunLoop Sourceを外します。さらに、押下中キーと修飾キーの前回状態を破棄します。&lt;/p&gt;

&lt;p&gt;最後の二つがないと、再有効化後に「前回のセッションで押されたまま」と判断し、最初の&lt;code&gt;keyDown&lt;/code&gt;を重複扱いする可能性があります。監視セッションを跨いで入力状態を持ち越さないことが重要です。&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio Engineを維持する理由
&lt;/h2&gt;

&lt;p&gt;KlakkのAudio Engineは、起動後に音声ファイルをPCMバッファへ準備し、固定の&lt;code&gt;AVAudioPlayerNode&lt;/code&gt;プールを使います。無効化のたびにEngine全体を止めて再構築すると、次の有効化で準備コストと失敗点が増えます。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;出力デバイスの再確認&lt;/li&gt;
&lt;li&gt;Audio Formatの再構築&lt;/li&gt;
&lt;li&gt;Player Nodeの再接続&lt;/li&gt;
&lt;li&gt;Engine startの失敗処理&lt;/li&gt;
&lt;li&gt;最初の一音までの遅延&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;入力監視を止めれば、新しいキーイベントはAudio Engineへ届きません。そのため、短い一時停止ではEngineと準備済みバッファを維持し、次の再開を軽くしています。&lt;/p&gt;

&lt;p&gt;アプリ終了時やAudio Engine自体のクリーンアップでは、別の&lt;code&gt;stop()&lt;/code&gt;がProperty Listener、Player Node、Engineを停止します。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaultDevicePropertyListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;AudioObjectRemovePropertyListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;AudioObjectID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kAudioObjectSystemObject&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="kc"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;defaultDevicePropertyListener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;playerNode&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;playerNodePool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;playerNode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;audioEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;メニューバーの一時無効化と、Audio System全体の終了を同じ操作にしていません。&lt;/p&gt;

&lt;h2&gt;
  
  
  「即時停止」の境界
&lt;/h2&gt;

&lt;p&gt;この設計で確実に止まるのは、新しいキーイベントによる追加再生です。&lt;/p&gt;

&lt;p&gt;一方、無効化した瞬間にすでにPlayer Nodeへscheduleされている短い音は、現在の実装ではその末尾まで再生される可能性があります。&lt;code&gt;KeyboardMonitor.stopMonitoring()&lt;/code&gt;はPlayer Nodeへ&lt;code&gt;stop()&lt;/code&gt;を送らないためです。&lt;/p&gt;

&lt;p&gt;したがって、UI文言やテストで「無効化した瞬間に全サンプルがゼロフレームで切れる」とは約束できません。必要要件が厳密なミュートなら、次の選択肢を別に設計する必要があります。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;全Player Nodeを停止する&lt;/li&gt;
&lt;li&gt;Mixer Volumeを即座に0へ下げる&lt;/li&gt;
&lt;li&gt;短いfade-outを入れる&lt;/li&gt;
&lt;li&gt;再有効化時にNodeとschedule状態を再同期する&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ただし、クリックノイズ、再開遅延、同時実行との競合が増えるため、単純な&lt;code&gt;audioEngine.stop()&lt;/code&gt;だけでは終わりません。&lt;/p&gt;

&lt;h2&gt;
  
  
  権限の撤回は別のライフサイクル
&lt;/h2&gt;

&lt;p&gt;ユーザーがInput Monitoring権限を取り消した場合は、単なる一時無効化とは違います。現在の実装は定期的に権限を確認し、撤回を検出したら監視を止めます。完全停止用の&lt;code&gt;stopMonitoringCompletely()&lt;/code&gt;では権限確認Timerも停止し、&lt;code&gt;wasMonitoring&lt;/code&gt;もリセットします。&lt;/p&gt;

&lt;p&gt;UI上の「無効」、権限の撤回、アプリ終了を一つのBooleanへ押し込まないほうが、再開条件を説明しやすくなります。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;p&gt;短いメニューバーアプリでは、無効化を「すべてのサブシステムを破棄する操作」と考えないほうが扱いやすい場合があります。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;入力監視を止めて、新しいトリガーを遮断する&lt;/li&gt;
&lt;li&gt;Event TapとRunLoop Sourceを確実に破棄する&lt;/li&gt;
&lt;li&gt;押下中キーとModifier状態をリセットする&lt;/li&gt;
&lt;li&gt;Audio EngineとPCM準備状態は短い再開のため維持する&lt;/li&gt;
&lt;li&gt;再生中サンプルの末尾は残る可能性を仕様境界として扱う&lt;/li&gt;
&lt;li&gt;権限撤回とアプリ終了は別の停止経路にする&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;KlakkはMacから追加の打鍵音を再生するアプリです。実物のキーボードのスイッチ、重さ、打鍵感、物理音は変えません。ASMRや集中しやすさには個人差があり、ADHDを含む症状の診断、治療、改善を目的とした医療製品ではありません。&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tryklakk.com/ja/#demo" rel="noopener noreferrer"&gt;ブラウザで14種類の音を試聴&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.apple.com/jp/app/id6754638652?pt=127956280&amp;amp;ct=jp-dev-disable-without-engine-stop-20260806&amp;amp;mt=12" rel="noopener noreferrer"&gt;Mac App Store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tryklakk.com/ja/privacy/" rel="noopener noreferrer"&gt;プライバシーポリシー&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;問い合わせ・訂正：&lt;a href="mailto:support@tryklakk.com"&gt;support@tryklakk.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>swift</category>
    </item>
    <item>
      <title>低遅延の打鍵音をAVAudioPlayerNodeプールで重ねる設計</title>
      <dc:creator>Klakk 開発チーム</dc:creator>
      <pubDate>Thu, 06 Aug 2026 08:05:33 +0000</pubDate>
      <link>https://dev.to/klakkapp/di-chi-yan-noda-jian-yin-woavaudioplayernodepurudezhong-nerushe-ji-2hc1</link>
      <guid>https://dev.to/klakkapp/di-chi-yan-noda-jian-yin-woavaudioplayernodepurudezhong-nerushe-ji-2hc1</guid>
      <description>&lt;p&gt;Macでキー入力に合わせて短い効果音を鳴らすとき、毎回ファイルを開いてプレイヤーを作ると、入力のホットパスにI/Oと初期化が入り込みます。この記事では、私が有料macOSアプリKlakkで使っている現在の構成を題材に、&lt;strong&gt;事前準備したPCMバッファ&lt;/strong&gt;と&lt;strong&gt;AVAudioPlayerNodeの固定プール&lt;/strong&gt;をどう分けたかを整理します。&lt;/p&gt;

&lt;h2&gt;
  
  
  1. キー入力より前に音を準備する
&lt;/h2&gt;

&lt;p&gt;再生時に必要なのは、キーコードからすでに変換済みのバッファを引くことだけにします。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;PreparedKeySound&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AVAudioPCMBuffer&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;outputGain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Float&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;preparedKeySounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PreparedKeySound&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;stateLock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;サウンドパックを読み込む段階で、ファイルの読み込み、必要区間の切り出し、出力フォーマットへの変換、先頭の無音除去、音量係数の計算まで済ませます。再生時に変換を始めないことが一番大きな分離です。&lt;/p&gt;

&lt;p&gt;辞書の入れ替えは一度組み立ててから短いロック内で行います。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;preparedSounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nf"&gt;prepareKeySounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;soundPack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;stateLock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;currentSoundPack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soundPack&lt;/span&gt;
&lt;span class="n"&gt;preparedKeySounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;preparedSounds&lt;/span&gt;
&lt;span class="n"&gt;loggedMissingKeys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;stateLock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;これなら、別のサウンドパックを読み込んでいる途中の半端な辞書を再生側が見ることはありません。&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Player Nodeを固定数だけ先に接続する
&lt;/h2&gt;

&lt;p&gt;現在の実装では、起動時に10個の &lt;code&gt;AVAudioPlayerNode&lt;/code&gt; を作り、同じ &lt;code&gt;mainMixerNode&lt;/code&gt; へ接続しています。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;playerNodePool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;AVAudioPlayerNode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;maxConcurrentSounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;currentNodeIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;maxConcurrentSounds&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AVAudioPlayerNode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;audioEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;audioEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;audioEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mainMixerNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;playerNodePool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ノードのattach/connectをキー入力のたびに行わず、入力後はラウンドロビンで次のノードを選びます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playerNodePool&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;currentNodeIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;currentNodeIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentNodeIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;maxConcurrentSounds&lt;/span&gt;

&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;masterVolume&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;preparedSound&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputGain&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;backgroundModeMultiplier&lt;/span&gt;
&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scheduleBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preparedSound&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;短い音が重なる通常のタイピングでは、一つのノードだけを使うより前の音を切りにくくなります。&lt;/p&gt;

&lt;h2&gt;
  
  
  3. 固定プールの限界も仕様にする
&lt;/h2&gt;

&lt;p&gt;この方式は無限に音を重ねません。11個目が10個の再生時間内に来れば、ラウンドロビンで戻ったノードを &lt;code&gt;stop()&lt;/code&gt; するため、そのノードの古い音は途中で終わります。&lt;/p&gt;

&lt;p&gt;これは単純さと上限の明確さを選んだ結果です。改善案としては次の選択肢があります。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;再生中でないノードを優先して探す&lt;/li&gt;
&lt;li&gt;最も古いノードだけを再利用する&lt;/li&gt;
&lt;li&gt;音の長さに応じてプール数を測定で決める&lt;/li&gt;
&lt;li&gt;同時発音数、切断数、再生遅延をテストで記録する&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;また、&lt;code&gt;currentNodeIndex&lt;/code&gt; と音量値へ複数キューから同時に触れる設計へ広げるなら、プレイヤー操作だけでなくアプリ側の状態にも直列化が必要です。「AVAudioPlayerNodeを使っているから全状態が安全」とは扱いません。&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Engine停止時は、その一音を捨てる
&lt;/h2&gt;

&lt;p&gt;出力デバイス変更などでEngineが止まっていたら、現在の実装は再起動を一度だけ要求し、そのキーの音は再生しません。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;audioEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;restartLock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shouldRestart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isRestarting&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;shouldRestart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;isRestarting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;restartLock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;shouldRestart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;restartAudioEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;停止中の入力を後からまとめて鳴らすより、次のキーから正常に戻る方が打鍵フィードバックには自然だからです。再起動中フラグをロックで守り、同時に複数の再起動を走らせないことも重要です。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;p&gt;低遅延化は「速いAPIを呼ぶ」だけではなく、キー入力後にしないことを決める作業でした。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ファイルI/O、変換、無音除去、ゲイン計算を事前に終える&lt;/li&gt;
&lt;li&gt;Player Nodeを固定数だけattach/connectしておく&lt;/li&gt;
&lt;li&gt;再生時は辞書参照、音量設定、schedule/playに限定する&lt;/li&gt;
&lt;li&gt;プール上限とEngine再起動時の欠落を隠さない&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;開発者による開示:&lt;/strong&gt; 私はKlakkの開発者です。Klakkは14種類の打鍵音を切り替えられる有料Macアプリです。ソフトウェアが追加音を再生するもので、実物のキーボードの物理音や打鍵感は変えません。ASMRや集中の感じ方には個人差があり、ADHDを含む症状を改善する医療製品ではありません。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tryklakk.com/ja/#demo" rel="noopener noreferrer"&gt;ブラウザで音を試す&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.apple.com/jp/app/id6754638652?pt=127956280&amp;amp;ct=jp-dev-player-node-pool-20260806&amp;amp;mt=12" rel="noopener noreferrer"&gt;Mac App Store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tryklakk.com/ja/privacy/" rel="noopener noreferrer"&gt;プライバシーポリシー&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;問い合わせ: &lt;a href="mailto:support@tryklakk.com"&gt;support@tryklakk.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>swift</category>
      <category>performance</category>
      <category>showdev</category>
    </item>
    <item>
      <title>macOSのInput Monitoring判定で、2つのAPIと「実際に動くか」を分けて観察する</title>
      <dc:creator>Klakk 開発チーム</dc:creator>
      <pubDate>Thu, 06 Aug 2026 03:58:33 +0000</pubDate>
      <link>https://dev.to/klakkapp/macosnoinput-monitoringpan-ding-de-2tunoapitoshi-ji-nidong-kuka-wofen-keteguan-cha-suru-4chh</link>
      <guid>https://dev.to/klakkapp/macosnoinput-monitoringpan-ding-de-2tunoapitoshi-ji-nidong-kuka-wofen-keteguan-cha-suru-4chh</guid>
      <description>&lt;p&gt;macOSでグローバルなキーイベントを受け取るアプリを作ると、権限判定を単純な &lt;code&gt;Bool&lt;/code&gt; として扱いたくなります。しかし実装と検証を進めるほど、次の3つは分けて考えた方が安全でした。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;TCC上で権限がどう見えているか&lt;/li&gt;
&lt;li&gt;ユーザーへ権限を要求した直後か&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CGEvent.tapCreate&lt;/code&gt; が実際に成功し、イベントを受け取れるか&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;この記事では、macOS向け打鍵音アプリを作る過程で使った診断方法を、製品固有のUIから切り離して整理します。&lt;/p&gt;

&lt;h2&gt;
  
  
  2つの事前確認API
&lt;/h2&gt;

&lt;p&gt;Listen-onlyのイベント監視では、次の2つを並べて観察できます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;CoreGraphics&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;IOKit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidsystem&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;InputMonitoringStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;hid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;IOHIDAccessType&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cgGranted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;looksGranted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;hid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;kIOHIDAccessTypeGranted&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;cgGranted&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;readInputMonitoringStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;InputMonitoringStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;InputMonitoringStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;hid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;IOHIDCheckAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kIOHIDRequestTypeListenEvent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nv"&gt;cgGranted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGPreflightListenEventAccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ここで重要なのは、&lt;code&gt;looksGranted&lt;/code&gt; を「監視が必ず動く」という保証にしないことです。これはあくまで事前状態の観察結果です。プロセス、署名、実行パス、権限変更直後の状態などを切り分けるために、2つの値を別々にログへ残します。&lt;/p&gt;

&lt;p&gt;ログへ記録するなら、入力内容ではなく次のような診断情報だけで十分です。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle_id=com.example.app
hid_status=granted
cg_preflight=true
event_tap_created=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;キーコード、入力文字列、前後のアプリ名まで診断ログへ入れる必要はありません。&lt;/p&gt;

&lt;h2&gt;
  
  
  権限要求は「確認」と分ける
&lt;/h2&gt;

&lt;p&gt;要求側のAPIは、確認のたびに呼ぶのではなく、初回案内やユーザーが明示的に「権限を開く」を選んだ時だけ呼びます。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;requestInputMonitoring&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;hidRequested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;IOHIDRequestAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kIOHIDRequestTypeListenEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cgRequested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGRequestListenEventAccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readInputMonitoringStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hidRequested=&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;hidRequested&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;, cgRequested=&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;cgRequested&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"looksGranted=&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;looksGranted&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;要求APIの戻り値だけで次の画面を決めると、ユーザーがシステム設定へ移動して後から許可した場合を扱いにくくなります。要求後は、アプリ側の状態を次のように分けました。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;notRequested&lt;/code&gt;: まだ案内していない&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;waitingForUser&lt;/code&gt;: システム設定での操作待ち&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;preflightGranted&lt;/code&gt;: 事前確認では許可済み&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;monitoring&lt;/code&gt;: Event Tapが作成され、監視中&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;revoked&lt;/code&gt;: 監視中に権限を失った&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  最終判定はEvent Tapの生成結果
&lt;/h2&gt;

&lt;p&gt;事前確認が通っていても、Event Tapの作成失敗は別の事実として扱います。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="kt"&gt;CGEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyDown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="kt"&gt;CGEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyUp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="kt"&gt;CGEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flagsChanged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tapCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cgSessionEventTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headInsertEventTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listenOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;eventsOfInterest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGEventMask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tap&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 権限表示だけでなく、署名・実行パス・再起動要否も切り分ける&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.listenOnly&lt;/code&gt; はイベントを書き換えない設計を表せますが、プライバシー説明の代わりにはなりません。アプリが何を処理し、何を保存せず、何を外部送信しないのかは別途明記する必要があります。&lt;/p&gt;

&lt;h2&gt;
  
  
  権限変更をポーリングする場合
&lt;/h2&gt;

&lt;p&gt;システム設定を開いた後にアプリへ戻ってもらう導線では、短い間隔で状態を再確認する実装が分かりやすいです。ただし、次の境界を決めておきます。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timerを重複生成しない&lt;/li&gt;
&lt;li&gt;監視開始後は要求用Timerを停止する&lt;/li&gt;
&lt;li&gt;権限喪失時はEvent TapとRunLoop Sourceを破棄する&lt;/li&gt;
&lt;li&gt;復旧時はユーザー設定が有効な場合だけ監視を再開する&lt;/li&gt;
&lt;li&gt;アプリ終了時はTimerも明示的に破棄する&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;「権限がある」と「監視中」を同じフラグにすると、権限喪失後のUIと実体がずれやすくなります。権限状態、Event Tapの生存状態、ユーザーが機能をオンにしているかを別々に持つ方が復旧処理を書きやすくなりました。&lt;/p&gt;

&lt;h2&gt;
  
  
  まとめ
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IOHIDCheckAccess&lt;/code&gt; と &lt;code&gt;CGPreflightListenEventAccess&lt;/code&gt; は診断のため別々に観察する&lt;/li&gt;
&lt;li&gt;要求APIは確認ループから分離し、ユーザー操作に結び付ける&lt;/li&gt;
&lt;li&gt;事前確認が通っていても、Event Tap生成結果を最終的な動作判定にする&lt;/li&gt;
&lt;li&gt;権限、監視状態、ユーザー設定を別の状態として管理する&lt;/li&gt;
&lt;li&gt;入力内容を診断ログやAnalyticsへ混ぜない&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;この記事は、macOS向け打鍵音アプリ &lt;strong&gt;Klakk&lt;/strong&gt; の開発チームが、実装中に得た知見を一般化してまとめたものです。自社の有料製品へのリンクを含みます。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ブラウザ試聴: &lt;a href="https://tryklakk.com/ja/#demo" rel="noopener noreferrer"&gt;https://tryklakk.com/ja/#demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Mac App Store: &lt;a href="https://apps.apple.com/jp/app/id6754638652?pt=127956280&amp;amp;ct=jp-dev-input-permission-20260806&amp;amp;mt=12" rel="noopener noreferrer"&gt;https://apps.apple.com/jp/app/id6754638652?pt=127956280&amp;amp;ct=jp-dev-input-permission-20260806&amp;amp;mt=12&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;プライバシーポリシー: &lt;a href="https://tryklakk.com/ja/privacy/" rel="noopener noreferrer"&gt;https://tryklakk.com/ja/privacy/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Klakkは物理キーコードと押下・解放状態をローカルで処理し、入力文字列を組み立てたり送信したりしません。ソフトウェアの音を追加する製品であり、物理キーボード自体の音や打鍵感は変えません。&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>swift</category>
      <category>security</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
