このスクリプトは、macOSの外付けディスプレイのHDRをワンクリックで切り替えるためのもので、HDR映像を視聴する際にシステム設定で手動でオン/オフを切り替える手間を解消する。サードパーティ製ツールやroot権限、UI自動化には依存せず、CoreGraphicsでディスプレイ情報を読み取り、非公開のMonitorPanel.frameworkを呼び出してHDR状態を変更する。ロジックは以下の通り。まず現在のモードでHDRを直接オンにする。対応しておらずリフレッシュレートが約120Hzより高い場合は、解像度とスケーリングは変更せずに約120Hzへ下げる。それでも対応しない場合は元のモードに戻してエラーを返す。スクリプトは毎回一意の外付けディスプレイを動的に識別し、状態ファイルと実行ロックで中断や同時実行を処理するとともに、元のモードを記録してHDRオフ時に復元できるようにする。実行にはXcodeまたはCommand Line Toolsが必要で、初回実行時には組み込みのObjective-Cヘルパーをコンパイルし、結果をキャッシュする。非公開インターフェースはシステムアップデート後に無効になる可能性があるため、実行前にチェックが行われる。
やりたいこと
miniLEDの外部ディスプレイを使っているので、動画をダウンロードするときは、たいていDV版かHDR版を選びます。MacBookの内蔵ディスプレイなら、普段はEDRを使って明るい部分をより明るく表示できます。一方、手持ちの他社製ディスプレイでHDR動画をきちんと表示するには、「ディスプレイ」設定でHDRをオンにする必要があります。
厳密には、EDRはApple製ディスプレイ専用ではなく、対応する外部ディスプレイでも使える場合があります。ここではあくまで自分の使い方について書きます。動画を見る前にHDRをオンにし、見終わったらSDRに戻します。
手動で切り替えるのは難しくありません。「システム設定」を開いて「ディスプレイ」に進み、外部ディスプレイを選んで「ハイダイナミックレンジ」をオン/オフするだけです。ただ、動画を見るたびに操作が必要で、見終わったらまた戻さなければなりません。途中で別の作業をする場合も、いちいち切り替えることになります。何度も繰り返すうちに、だんだん面倒になってきました。
以前から何度かおすすめしているSpace Launcherなら、ショートカットキーでスクリプトを実行できます。そこで、いちばん手軽な方法として、.command.commandファイルを作り、1回押せばHDRをオン、もう1回押せばオフにすることにしました。
もう一つ厄介なのが、仕事をする場所が一定せず、接続するディスプレイも毎回同じとは限らないことです。自宅では4K 160 Hz、別の場所では別の4K 144 Hzディスプレイを使うこともあります。端子やケーブル、表示モードによっては、こうした高リフレッシュレートとHDRを同時に使えない場合があります。
そこで、このスクリプトにはいくつかルールを設けました。
特に最後の条件があるため、「システム設定」をUI自動化で操作する方法はほぼ候補から外れました。
シェルコマンドだけでは実現できない理由
Appleの公式説明では、HDRの切り替えは「システム設定 → ディスプレイ」にあります。また、外部ディスプレイでHDRを使えるかどうかは、Mac、HDR10対応ディスプレイ、ポート、ケーブル、変換アダプタ、ディスプレイのファームウェアにも左右されると説明されています。AppleのHDRに関する説明
公開されているCoreGraphics APIからは、接続中のディスプレイ、内蔵ディスプレイの判別、現在の解像度やリフレッシュレートなど、さまざまな情報を取得できます。たとえば、CGGetOnlineDisplayListは接続中のディスプレイの一覧取得に、CGDisplayIsBuiltinは内蔵ディスプレイの判別に使えます。CGDisplayModeは現在の表示モードを読み取れます。
しかし、Appleの公開APIをひと通り調べても、「ハイダイナミックレンジ」のオン/オフを直接変更するインターフェースは見つかりませんでした。つまり、ディスプレイ情報は公開APIで読み取れても、HDRを切り替える方法は用意されていないのです。
「システム設定」をosascriptで操作する方法も検討しました。これなら他社製ツールは不要ですが、アクセシビリティとオートメーションの権限が必要で、実行時にウインドウが開いて前面に出てきます。さらに、システム設定の画面構成や表示文言に依存するため、macOSのアップデートで動かなくなる可能性があります。ショートカットキーを押したら、裏で静かに切り替わるようにしたかったので、この方法は使いませんでした。
最終的に使ったのは、macOSに標準搭載されているものの、公開ドキュメントのないMonitorPanel.frameworkです。現在の表示モードを読み取り、そのモードでHDRが使えるかを判定し、HDRの状態を変更できます。実装にあたっては主にToggleHDR.swiftを参考にしました。同じフレームワークを使っているので、少なくとも実現可能な方向性だと分かります。
もちろん、プライベートAPIの最大の問題は、今後も使える保証がないことです。macOSのアップデートでクラス名やメソッド名、呼び出し方が変わったり、同じメソッドでも動作が変わったりする可能性があります。そのためスクリプトは起動時に毎回チェックし、インターフェースが想定と違っていたら、表示設定には触れずに終了します。
スクリプトの動作
基本的な流れはシンプルです。現在のモードでHDRを使えるならそのままオンにし、使えなければ約120 Hzを試します。それでも駄目なら元に戻します。以下は引数なしで実行した場合の流れです。--onと--offを指定すると、目標の状態をオン/オフに固定できます。
1. 対象のディスプレイを特定する
スクリプトは「前回どのディスプレイを操作したか」を記憶せず、実行のたびにあらためて探します。
最終的に外部ディスプレイが1台だけでなければなりません。外部ディスプレイが接続されていない場合や、2台同時に接続されている場合は、見つかったディスプレイを一覧表示して終了します。今回は切り替えないほうが、別のディスプレイのHDRを誤ってオンにするより安全です。
対象を選ぶ際にUUIDを使わないのは意図的です。「現在接続中の外部ディスプレイが1台だけ」という条件だけで決めます。UUIDは対象を決めた後、切り替え中に同じディスプレイを再特定したり、そのディスプレイ固有の復元情報と結び付けたりするために使います。こうすれば別の場所に移ったとき、前のディスプレイの記録を新しいディスプレイに適用してしまうこともありません。
2. そのままHDRを使えるなら、リフレッシュレートは変えない
まず現在の表示モードcurrentModeを読み取り、次にhasHDRModesを確認します。この項目が示すのは、MonitorPanelが「現在のモードでHDRを使える」と判定しているかどうかです。
現在のモードでHDRを使えるなら、そのままHDRをオンにします。設定を書き込んだ後も、APIの戻り値だけを信頼せず、ディスプレイの状態を読み直します。MonitorPanelで解像度と拡大率が変わっていないことを確認し、CoreGraphicsで実際のリフレッシュレートが変わっていないことを確認します。2回続けて同じ結果が得られて、初めて切り替え成功とみなします。
つまり、4K 160/144 HzでHDRを使えるなら、安全のために先に120 Hzへ下げることはありません。逆にHDRをオンにした際、システムが表示モードまで変えてしまった場合も検出できるので、切り替え成功として扱うことはありません。
3. HDRを使えない場合は、120 Hzを試す
スクリプトが120 Hzのモードを探すのは、現在のモードがHDR非対応でリフレッシュレートが約120 Hzを超えている場合か、VRR/ProMotionのような可変リフレッシュレートモードを使っている場合に限ります。
候補となるモードは、さらに次の条件をすべて満たす必要があります。
これらの条件をすべて満たす必要があります。「3840 × 2160」という物理解像度だけを比べても不十分です。同じ物理解像度でも、拡大率の異なるモードが複数ある場合があるからです。リフレッシュレートが一時的に下がるのは構いませんが、デスクトップの拡大率が変わるのは困ります。
候補が複数ある場合は、120 Hzとの差が最も小さいものを選びます。差が同じなら、リフレッシュレートの高いほうを優先します。適切な120 Hzモードが見つからなければ、その時点で失敗として終了し、100 Hzや60 Hzまで試すことはありません。HDRのために少しリフレッシュレートを下げたいだけで、表示環境全体を崩したいわけではないからです。
4. HDRをオフにしたら、元のリフレッシュレートに戻す
HDRをオンにする際、実際に160/144 Hzから約120 Hzへ下げた場合は、切り替え前後の表示モードを記録します。再接続後に変わる可能性のあるモード番号だけでなく、解像度、リフレッシュレート、HiDPI、拡大率、ピクセル形式に加え、ディスプレイのUUID、vendor、model、serialなども保存します。
HDRをオフにするときは、まず同じディスプレイが接続されていること、現在のモードが以前切り替えた約120 Hzモードであることを確認し、問題がなければ元のリフレッシュレートに戻します。ディスプレイをつなぎ直すと、元のモード番号が変わっていることがあります。その場合は解像度、リフレッシュレート、拡大率などを使ってモードを探し直します。一意に特定できた場合だけ復元し、見つからなかったり候補が複数あったりした場合は何もしません。
HDRがオンの間に手動でリフレッシュレートや拡大率を変えた場合は、古い設定に戻しません。HDRをオフにするだけにして、その後手動で選んだモードはそのまま残します。
初回実行時にコンパイルが必要な理由
ShellからObjective-Cのプライベートフレームワークを直接呼び出すことはできません。すべてを1つのファイルにまとめるため、toggle-external-hdr.commandtoggle-external-hdr.commandにはObjective-Cのヘルパーソースコードを埋め込んであります。
初回実行時、外側のzshスクリプトがシステム標準のxcrun clangを呼び出してヘルパーをコンパイルし、生成物を次の場所にキャッシュします。
キャッシュはmacOSのビルド番号、CPUアーキテクチャ、埋め込みソースコードのハッシュごとに分けられます。システムやヘルパーのソースが変われば再コンパイルし、変更がなければ次回以降はキャッシュを使うので、毎回コンパイルを待つ必要はありません。
キャッシュディレクトリとバイナリの所有者が現在のユーザーか、権限に問題がないか、不審なシンボリックリンクやハードリンクではないかも確認します。チェックに通らなければ終了し、コンパイルに失敗しても古いバージョンを代わりに実行することはありません。
他社製のディスプレイソフトをインストールする必要はありませんが、AppleのXcodeまたはCommand Line Toolsがあらかじめインストールされている必要があります。
表示モードの切り替え中にスクリプトが中断されると、画面が数秒暗くなったり、ディスプレイの内部IDが変わったりすることがあります。さらに厄介なのは、リフレッシュレートを120 Hzに下げた後、HDRをオンにする前に処理が中断されるケースです。
そのため、スクリプトはどこまで処理が進んだかを記録します。状態は次の4種類です。
状態
意味
`pending`
元のモードを保存済み。リフレッシュレートの切り替え待ち。
`enabling`
フォールバックモードに切り替え済み。HDRをオンにする前。
`active`
HDRがオンで、復元情報が有効。
`disabling`
HDRをオフにしているところ。次に元のモードへ戻す。
次回実行時には、前回の処理が途中で終わっていないかを確認します。現在のモードがスクリプトによって変更されたものだと確認できれば、そのまま復元を続けます。確認できなければ停止し、「似ている」というだけのモードで現在の設定を上書きすることはありません。状態がすでにactiveなのに、その後手動でモードを変えていた場合は、古い復元情報を削除してHDRをオフにします。次回、期限切れのリフレッシュレート設定に戻されるのを防ぐためです。
実行ロックも設けています。別のプロセスが切り替え中なら、後から起動したほうは待ち続けずに終了します。ショートカットキーを素早く2回押したり、スクリプトをダブルクリックしたり、2つのターミナルから同時に実行したりしても、2つのプロセスが同じディスプレイを同時に変更することはありません。
使い方
スクリプトには実行権限を設定済みです。スクリプトのあるディレクトリに移動したら、まず現在の状態を確認しておくとよいでしょう。
普段は引数なしで実行すれば大丈夫です。オン/オフを明示して実行することもできます。
.commandという拡張子なので、.commandFinderからダブルクリックして実行することもできます。さらに便利なのは、Space Launcherでショートカットキーを割り当てる方法です。ダウンロードやコピーの際に実行権限が失われた場合は、次のようにして付け直せます。
--statusを実行すると、スクリプトが検出したディスプレイ、現在の解像度とリフレッシュレート、HDRの対応状況とオン/オフ、次に行う処理の概要が表示されます。HDRの切り替えや表示モードの変更は行いません。ただし初回実行時は、ヘルパーのコンパイルに伴ってキャッシュが書き込まれることがあります。
前回の実行が途中で中断されていた場合、次に実際の処理を実行すると、まず残った状態を処理します。そのため、--statusに表示される「次の処理」はあくまで目安で、実際に行われる処理と異なることがあります。
実際の動作は、おおむね次のとおりです。
まとめ
このスクリプトでできるのはHDRと表示モードの切り替えだけで、ポートやケーブルの帯域を増やすことはできません。4K、120/144/160 Hz、HDR、色深度を同時に使えるかどうかは、Mac、ケーブルや変換アダプタ、ディスプレイが報告する能力、OSDの設定、ファームウェア次第です。Appleによると、拡大表示時の解像度によって、選択できるリフレッシュレートやHDRモードが変わることもあります。Appleの外部ディスプレイに関する説明
現時点でこのスクリプトが対応しているのは、一般的な外部ディスプレイ1台だけです。DisplayLinkや仮想ディスプレイなど特殊な接続方式はまだ対象外です。そのような環境では、まず--statusを実行してディスプレイが正しく検出されているか確認し、切り替えるかどうか判断してください。
また、MonitorPanel.frameworkはあくまでプライベートAPIです。macOSのメジャーアップデート後は、まず--statusを実行し、画面を確認しながら、いつでも手動で設定を戻せる状況で一度切り替えを試すのがおすすめです。--statusが正常に動いても、読み取り用インターフェースが残っていると分かるだけで、書き込み動作がアップデート前とまったく同じとは限りません。
最初は、HDR切り替え用のインターフェースさえ見つかれば、ほぼ完成だと思っていました。実際に書いてみると、切り替え自体はむしろ簡単でした。それより、正しいディスプレイを見つけること、いつ120 Hzに下げるか、拡大率を保つこと、途中で失敗したときに設定を元に戻すことのほうが大変でした。
こうした点をきちんと処理できれば、普段の使い方は簡単で、安心してスクリプトに任せられます。動画を見る前にショートカットキーを押し、見終わったらもう一度押す。安全に切り替えられると判断できれば実行し、判断がつかなければ何も変えません。