<?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>低遅延の打鍵音を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>
