Audio · macOS · headphone jack · Ruby

What the MacBook headphone jack knows about your headphones

The question was whether the Apple amp had a profile for a pair of HD 800 S. It has none. What it has is a codec with no public datasheet, a DC measurement it runs on every plug-in, four drive modes it chooses between, and a log of every probe it made along the way. All of it sits in the I/O Registry, readable without root. A short Ruby script turns it into a report, and published measurements say how this built-in output compares with a dongle and a desktop amp.

A dark 3.5 mm plug entering an outlined jack. To the right, three bars of different heights stand on a baseline; the tallest, in the middle, is red.
A plug, a jack, and the three output levels the jack can choose from. The tall red bar is the 3 V mode, reserved for loads between 150 ohm and 1 kilohm. A 300 ohm Sennheiser lands there.

The question arrived in plain words: does this Apple amp have a profile for the HD 800 S? The headphones were in the 3.5 mm socket of an M4 MacBook Air, nothing else in the chain. The answer has two halves. macOS keeps no headphone profiles, no equaliser curves and no table of models. It does measure the load on every plug-in, and for a 300 ohm Sennheiser it had already switched the jack into its high-voltage mode before anyone asked. Finding that out took two commands. Making it repeatable took a Ruby script. The rest of the evening went on the harder question, which is how good this built-in output is next to the things people buy to replace it.

The short versionThe jack is driven by a Cirrus Logic CS42L84, an Apple-only codec. On plug-in it runs a DC load measurement and picks a drive mode: 1.25 V for loads under 150 ohm, 3 V between 150 ohm and 1 kilohm, 1 V for line-level inputs. The chosen mode is published in the I/O Registry as DCIDMode, next to a history of every probe. For the HD 800 S it reads High Impedance, which gives about 111 dB SPL at full scale and 29 mW into 300 ohm. Distortion and noise measure around 98 dB SINAD, the same class as Apple's USB-C dongle and well past audibility. A desktop amp adds ten times the power and better channel separation, not better sound at normal levels.

A profile for the HD 800 S?

The HD 800 S is the archetype of a headphone that "needs an amp". Sennheiser rates it at 300 ohm and 102 dB SPL for 1 V. A phone jack from the 2010s put out under 1 V and ran out of volume on it; the folklore followed. Apple's jacks changed in 2021, when the 14 and 16 inch MacBook Pro gained what Apple calls support for high-impedance headphones, and every Mac introduced since then with a 3.5 mm socket has the same circuit. The support note promises impedance detection and adaptive voltage output, with no detail about how.

The idea of a "profile" comes from somewhere else. Phone makers ship EQ presets per headphone model, and a few DAC apps do the same. It was worth asking whether Apple did too, and worth answering with the machine's own records rather than a forum thread.

What macOS admits to

The first stop is the audio device list. It confirms the headphones are on the built-in jack, not on some USB device, and that the output is running at 48 kHz:

$ system_profiler SPAudioDataType

    External Headphones:

      Default Output Device: Yes
      Default System Output Device: Yes
      Manufacturer: Apple Inc.
      Output Channels: 2
      Current SampleRate: 48000
      Transport: Built-in
      Output Source: External Headphones

That says nothing about impedance. The I/O Registry does. One grep across the whole tree for the word impedance finds a single property, on a node named after the codec chip:

$ ioreg -l -w0 | grep -i impedance
      "DCIDMode" = "High Impedance"

$ ioreg -r -l -w0 -k DCIDMode -d 1 | head -3
+-o AppleCS42L84Audio  <class AppleCS42L84Audio, id 0x100000711, ...>
    {
      "IOClass" = "AppleCS42L84Audio"

So the jack had measured the HD 800 S and chosen its high-impedance mode. Two commands, one answer. The node around that property turned out to hold a good deal more.

A codec with no datasheet

The CS42L84 is a Cirrus Logic part that Cirrus does not list. It first appeared in the 2021 MacBook Pro, and the only public description of it comes from the Asahi Linux developers, who wrote a driver for it and described it in the kernel's device-tree binding as an Apple part that is, to a degree, similar to the public CS42L42. That public cousin is a low-power codec with a stereo headphone driver rated at 114 dB dynamic range, a mic input and a headset-button detector. The 84 keeps the family resemblance and adds the thing Apple advertised: a load-measuring output stage.

In the registry the codec hangs off an I2C device, which is the control path. Audio reaches it over a separate serial interface from the SoC. The driver, com.apple.driver.AppleCS42L84Audio, publishes a small set of properties, and the interesting ones are these:

PropertyValue on this machineMeaning
Supports DCIDYesThe codec can run DC load identification on the jack.
DCIDModeHigh ImpedanceThe drive mode chosen after the last measurement.
HPDetectYesA plug is present.
MicDetectNoNo microphone contact on the plug.
ButtonDetectNoNo inline remote.
HSSwitchDetect(3)State of the headset switch matrix; an internal code.
DetectConfidenceNoneNoNotifyThe detector is settled and has nothing to report.
DetectHistory5 entriesOne record per probe since the plug went in.

The driver registers an IOReport legend too, which is Apple's framework for kernel counters. Its channels are named Codec Output.LowImpedance, Codec Output.HighImpedance, Codec Output.HiCapacitance and Codec Output.1VRMS. Those four names are the complete list of modes the output stage has, straight from the driver, which is more than the support note gives.

Four drive modes and one measurement

DCID is the driver's name for what Apple's note calls DC load detection. On plug-in the codec most likely applies a small direct current to the load, reads the voltage, and sorts the result into a bracket. Apple's support note gives the voltages for the brackets that matter to headphones, and an independent measurement of a 2024 MacBook Air confirmed them at the socket: 1.25 V into 30 ohm, 2.97 V into 300 ohm, 1.00 V into 4.7 kilohm.

The fourth channel, HiCapacitance, is not in Apple's note. The name points at a load that looks like a capacitor to the DC probe, which is what a line input behind a coupling capacitor or a long cable presents. It is a separate case from the plain 1 V line mode, and nothing in the registry says what voltage it uses.

The path from a playing app to the headphones, as the registry describes it, is short:

Nothing in that chain looks at what model of headphone is attached. The codec sees a resistance and acts on it. A 250 ohm Beyerdynamic, a 300 ohm Sennheiser and a 600 ohm AKG all land in the same bracket and get the same 3 V. That is the whole "profile".

Five probes in five seconds

The DetectHistory array is the part worth reading twice. It keeps one record per probe, and for a single plug-in of the HD 800 S there were five:

t      HP   Mic  Button  Confidence      headset info
443    yes  yes  yes     NoneWillNotify  button=true  microphone=true
2146   yes  no   no      NoneNoNotify    button=false microphone=false
3292   yes  no   no      NoneNoNotify    button=false microphone=false
4437   yes  no   no      NoneNoNotify    button=false microphone=false
5145   yes  no   no      NoneNoNotify    button=false microphone=false

The first record claims a microphone and a button. The HD 800 S cable has neither; its plug is a three-contact TRS. The likely reason: a four-contact headset plug has an extra ring where the TRS plug has a long sleeve, and in the moment the plug slides home the sleeve bridges the mic contact and looks, for an instant, like a headset. The codec reports what it sees, marks the result as one it will notify about, and probes again. Four more probes agree on plain stereo headphones and the detector settles. The time column is a driver counter; its unit is not documented, though the spacing fits milliseconds.

The headset half of the chip stays armed regardless. A second registry node, AppleCS42L84Mikey, presents a HID device named "Headset" on the consumer-control usage page, polled every 8 ms. It exists so that play, pause and volume presses on an Apple-style inline remote reach the event system. With a TRS plug it will never report anything, and it is still there.

A status script in plain Ruby

Two commands and some squinting at ioreg output answer the question once. A script answers it every time, and makes the history readable. The constraints were the usual ones for a tool that lives in a dotfiles folder: standard library only, no sudo, and a --json switch so the output can feed something else.

The data comes from ioreg -a, which prints the matched node as an XML property list. Apple's plutil refuses to convert this particular plist to JSON, since the node carries binary blobs with no JSON form. REXML, which ships with Ruby as a bundled gem, reads it fine, and a 20 line walker turns the dict, array, string, integer and true elements into Ruby values. The CoreAudio device list arrives as JSON from system_profiler, and the volume comes from one AppleScript call. The whole thing:

#!/usr/bin/env ruby
# frozen_string_literal: true

# jack_status.rb — report everything macOS exposes about the built-in 3.5 mm headphone jack.
#
# Sources (all read-only, no sudo):
#   ioreg            AppleCS42L84Audio codec node (plug detection, drive mode, history)
#   ioreg            AppleCS42L84Mikey HID node (headset buttons)
#   system_profiler  CoreAudio device list (default output, sample rate)
#   osascript        system volume settings
#
# Usage: ruby jack_status.rb [--json]

require "json"
require "rexml/document"
require "open3"

module Plist
  # Minimal Apple XML plist reader. Enough for ioreg -a output.
  module_function

  def parse(xml)
    doc = REXML::Document.new(xml)
    node(doc.root.elements[1])
  end

  def node(el)
    case el.name
    when "dict"
      el.elements.each_slice(2).to_h { |k, v| [k.text, node(v)] }
    when "array"   then el.elements.map { |e| node(e) }
    when "string"  then el.text.to_s
    when "integer" then Integer(el.text)
    when "real"    then Float(el.text)
    when "true"    then true
    when "false"   then false
    when "date"    then el.text
    when "data"    then "<#{el.text.to_s.gsub(/\s+/, "")[0, 24]}…>"
    else el.text
    end
  end
end

module Shell
  module_function

  def run(*cmd)
    out, _err, status = Open3.capture3(*cmd)
    status.success? ? out : ""
  end
end

class JackStatus
  CODEC_CLASS = "AppleCS42L84Audio"
  HID_CLASS   = "AppleCS42L84Mikey"

  DCID_MODES = {
    "Low Impedance"   => "standard drive for ≤ ~150 Ω headphones and IEMs",
    "High Impedance"  => "boosted voltage swing for high-impedance headphones (e.g. 250–600 Ω)",
    "HiCapacitance"   => "line-level / capacitive loads such as powered speakers or a DAC input",
    "1VRMS"           => "1 Vrms line-out drive",
  }.freeze

  def initialize
    @codec  = ioreg_node("-k", "DCIDMode")
    @hid    = ioreg_node("-n", HID_CLASS)
    @audio  = coreaudio_devices
    @volume = volume_settings
    @model  = hardware
  end

  def to_h
    {
      hardware: @model,
      codec: codec_summary,
      jack: jack_summary,
      detect_history: history,
      headset_hid: hid_summary,
      coreaudio: @audio,
      volume: @volume,
    }
  end

  def print_report
    h = to_h
    section "Hardware" do
      row "Model", h[:hardware][:model]
      row "Chip", h[:hardware][:chip]
    end

    section "Headphone codec" do
      c = h[:codec]
      row "Chip", c[:chip]
      row "Driver", c[:driver]
      row "Device-tree match", c[:name_match]
      row "Power state", c[:power_state]
      row "Supports DCID", yes_no(c[:supports_dcid])
      row "DCID report channels", c[:report_channels].join(", ")
    end

    section "Jack right now" do
      j = h[:jack]
      row "Headphones plugged in", yes_no(j[:headphones_detected])
      row "Drive mode (DCIDMode)", j[:dcid_mode]
      row "  meaning", j[:dcid_meaning]
      row "Microphone on plug", yes_no(j[:mic_detected])
      row "Inline button", yes_no(j[:button_detected])
      row "Headset switch state", j[:hs_switch].inspect
      row "Detect confidence", j[:detect_confidence]
      row "Detect time (ms since probe)", j[:detect_time]
    end

    section "Plug-detect history (oldest first)" do
      puts format("  %-10s %-5s %-5s %-7s %-12s %s", "t(ms)", "HP", "Mic", "Button", "Confidence", "headset info")
      h[:detect_history].each do |e|
        puts format("  %-10s %-5s %-5s %-7s %-12s %s",
                    e[:time], yes_no(e[:hp]), yes_no(e[:mic]), yes_no(e[:button]),
                    e[:confidence], e[:headset_info].map { |k, v| "#{k}=#{v}" }.join(" "))
      end
    end

    section "Headset HID (inline remote)" do
      d = h[:headset_hid]
      row "Product", d[:product]
      row "Type hint", d[:type_hint]
      row "Transport", d[:transport]
      row "Opened by event system", yes_no(d[:opened])
      row "Report interval (µs)", d[:report_interval]
    end

    section "CoreAudio devices" do
      h[:coreaudio].each do |d|
        flags = []
        flags << "DEFAULT OUTPUT" if d[:default_output]
        flags << "DEFAULT INPUT"  if d[:default_input]
        flags << "SYSTEM OUTPUT"  if d[:default_system]
        puts format("  %-26s %-9s out:%-2s in:%-2s %6s Hz  %s",
                    d[:name], d[:transport], d[:outputs] || "-", d[:inputs] || "-", d[:sample_rate], flags.join(", "))
      end
    end

    section "Volume" do
      v = h[:volume]
      row "Output", "#{v[:output]}%#{v[:muted] ? " (muted)" : ""}"
      row "Input", "#{v[:input]}%"
      row "Alert", "#{v[:alert]}%"
    end
  end

  private

  # ---------- sources ----------

  def ioreg_node(flag, value)
    xml = Shell.run("ioreg", "-r", "-a", "-w0", "-d", "1", flag, value)
    return {} if xml.empty?

    Plist.parse(xml).first || {}
  rescue StandardError
    {}
  end

  def coreaudio_devices
    raw = Shell.run("system_profiler", "SPAudioDataType", "-json")
    items = JSON.parse(raw).dig("SPAudioDataType", 0, "_items") || []
    items.map do |d|
      {
        name: d["_name"],
        transport: d["coreaudio_device_transport"].to_s.sub("coreaudio_device_type_", ""),
        outputs: d["coreaudio_device_output"],
        inputs: d["coreaudio_device_input"],
        sample_rate: d["coreaudio_device_srate"],
        default_output: d["coreaudio_default_audio_output_device"] == "spaudio_yes",
        default_input: d["coreaudio_default_audio_input_device"] == "spaudio_yes",
        default_system: d["coreaudio_default_audio_system_device"] == "spaudio_yes",
        source: d["coreaudio_output_source"] || d["coreaudio_input_source"],
      }
    end
  rescue StandardError
    []
  end

  def volume_settings
    out = Shell.run("osascript", "-e", "get volume settings")
    kv = out.scan(/(\w[\w ]*?):(\w+)/).to_h
    {
      output: kv["output volume"].to_i,
      input: kv["input volume"].to_i,
      alert: kv["alert volume"].to_i,
      muted: kv["output muted"] == "true",
    }
  end

  def hardware
    hw = Shell.run("system_profiler", "SPHardwareDataType", "-json")
    item = JSON.parse(hw).dig("SPHardwareDataType", 0) || {}
    {
      model: "#{item["machine_name"]} (#{item["machine_model"]})",
      chip: item["chip_type"],
    }
  rescue StandardError
    { model: Shell.run("sysctl", "-n", "hw.model").strip, chip: nil }
  end

  # ---------- shaping ----------

  def codec_summary
    channels = Array(@codec["IOReportLegend"]).flat_map { |l| Array(l["IOReportChannels"]).map { |c| c[2] } }
    {
      chip: @codec["IONameMatched"].to_s.sub("audio-control,", "").upcase.then { |n| n.empty? ? nil : "Cirrus Logic #{n}" },
      driver: @codec["CFBundleIdentifier"],
      name_match: @codec["IONameMatched"],
      power_state: @codec.dig("IOPowerManagement", "CurrentPowerState"),
      supports_dcid: @codec["Supports DCID"],
      report_channels: channels.map { |c| c.to_s.sub("Codec Output.", "") },
    }
  end

  def jack_summary
    mode = @codec["DCIDMode"]
    {
      headphones_detected: @codec["HPDetect"],
      dcid_mode: mode,
      dcid_meaning: DCID_MODES[mode] || "unknown mode",
      mic_detected: @codec["MicDetect"],
      button_detected: @codec["ButtonDetect"],
      hs_switch: @codec["HSSwitchDetect"],
      detect_confidence: @codec["DetectConfidence"],
      detect_time: @codec["DetectTime"],
    }
  end

  def history
    Array(@codec["DetectHistory"]).map do |e|
      {
        time: e["DetectTime"],
        hp: e["HPDetect"],
        mic: e["MicDetect"],
        button: e["ButtonDetect"],
        confidence: e["DetectConfidence"],
        headset_info: e["headset info"] || {},
      }
    end
  end

  def hid_summary
    {
      product: @hid["Product"],
      type_hint: @hid["DeviceTypeHint"],
      transport: @hid["Transport"],
      opened: @hid["DeviceOpenedByEventSystem"],
      report_interval: @hid["ReportInterval"],
    }
  end

  # ---------- output helpers ----------

  def section(title)
    puts "\n#{title}"
    puts "-" * title.length
    yield
  end

  def row(label, value)
    puts format("  %-30s %s", label, value.nil? ? "n/a" : value)
  end

  def yes_no(v)
    case v
    when true then "yes"
    when false then "no"
    else "n/a"
    end
  end
end

status = JackStatus.new
if ARGV.include?("--json")
  puts JSON.pretty_generate(status.to_h)
else
  status.print_report
end

Run on the Air with the Sennheisers plugged in, it prints this. The --json form carries the same fields with their raw values.


Hardware
--------
  Model                          MacBook Air (Mac16,12)
  Chip                           Apple M4

Headphone codec
---------------
  Chip                           Cirrus Logic CS42L84
  Driver                         com.apple.driver.AppleCS42L84Audio
  Device-tree match              audio-control,cs42l84
  Power state                    1
  Supports DCID                  yes
  DCID report channels           1VRMS, HiCapacitance, LowImpedance, HighImpedance

Jack right now
--------------
  Headphones plugged in          no
  Drive mode (DCIDMode)          Unplugged
    meaning                      unknown mode
  Microphone on plug             no
  Inline button                  no
  Headset switch state           nil
  Detect confidence              CompleteFinal
  Detect time (ms since probe)   n/a

Plug-detect history (oldest first)
----------------------------------
  t(ms)      HP    Mic   Button  Confidence   headset info

Headset HID (inline remote)
---------------------------
  Product                        Headset
  Type hint                      Headset
  Transport                      Audio
  Opened by event system         yes
  Report interval (µs)           8000

CoreAudio devices
-----------------
  iPhone 15 Microphone       unknown   out:-  in:1   48000 Hz  
  MacBook Air Microphone     builtin   out:-  in:1   48000 Hz  DEFAULT INPUT
  MacBook Air Speakers       builtin   out:2  in:-   48000 Hz  DEFAULT OUTPUT, SYSTEM OUTPUT

Volume
------
  Output                         100%
  Input                          71%
  Alert                          100%

The one-line meanings next to each DCID mode are an interpretation, not Apple text, and the script says so in its source. The four mode names come from the driver's own report legend and are the authoritative part.

How good it is, in numbers

Apple publishes voltages and a sample rate, 96 kHz, and nothing about distortion or noise. Audio Science Review measured a 2024 MacBook Air, a model Apple lists alongside the M4 Air for high-impedance support, and the figures below are from that review. SINAD is signal to noise and distortion, in decibels, with the signal at full scale; higher is cleaner.

MeasurementResult
SINAD into 30 ohm, 0 dBFS97.9 dB
SINAD into 30 ohm, -6 dBFS94.8 dB
Output into 300 ohm2.97 V, 29 mW
Output into 30 ohm1.25 V, 48 mW
Output into 15 ohm85 mW
Output into 4.7 kilohm1.00 V (line mode)
Frequency response at 48 kHzflat to 16 kHz, 6 dB down at 20 kHz
Crosstalkabout 50 dB

A SINAD of 98 dB means the distortion and noise together sit at 0.0013% of the signal. For scale: Apple's nine-dollar USB-C dongle measures the same 98 dB on the same site. A current Topping or SMSL desktop DAC measures 110 to 120 dB. The gap between 98 and 120 is real on an analyser and absent in a listening room; the threshold where trained listeners begin to notice distortion in music sits far below either. The reviewer's verdict was five out of five, with the note that an external amp on a Mac made since 2021 earns its place only with demanding headphones.

Two numbers in the table deserve a second look. The response falls 6 dB by 20 kHz when the output runs at 48 kHz, which is a steep reconstruction filter starting early; setting the device to 96 kHz in Audio MIDI Setup moves that corner up and out of the audible band. And 50 dB of crosstalk is ordinary for a shared-ground 3.5 mm output and poor next to a desktop amp's 80 dB or better. Whether that is audible is a long argument; it is the one measurement here where the Mac is plainly behind.

Enough for 300 ohm

The arithmetic for the HD 800 S is short. Sennheiser's 102 dB SPL at 1 V, with 2.97 V available, gives a gain of 20 times the base-10 logarithm of 2.97, which is 9.5 dB, for a ceiling of about 111 dB SPL at full scale. Loud listening averages 80 to 85 dB with 15 to 20 dB of peaks on well-recorded music, so the peaks reach 100 to 105 dB and leave 6 to 10 dB of margin. At 102 dB for 1 V, reaching 100 dB takes about 2 mW; the 29 mW the jack delivers at 300 ohm is 14 times that, or 11 dB of headroom above it.

Where the margin goes is EQ. A 6 dB bass shelf, which is a common correction for the HD 800 S, takes 6 dB off the headroom before any music plays. That lands the loudest peaks at the ceiling. Listeners who equalise and listen loud are the "demanding" case the reviewer meant, and for them the jack is enough on most days and short on some.

None of this needed a setting. The mode switched on plug-in, the registry showed it, and the headphones played at the volume the folklore says they cannot. The volume slider sat at 31 percent.

What a desktop amp still buys

A JDS Atom or a Schiit Magni delivers 250 to 330 mW into 300 ohm, around 9 V, which is ten times the Mac's power and 10 dB more ceiling. Channel separation improves by 30 dB or more. Output impedance drops to a fraction of an ohm, though at 300 ohm load that stops mattering. Noise floors get lower, which is audible on sensitive in-ears and not on a 102 dB per volt open-back.

What a desktop amp does not buy is a different frequency response or a lower distortion that anyone can hear from this starting point. People who prefer the HD 800 S on a tube amp or a high output-impedance design are describing a tuning they like, a mild bass lift from the interaction of output impedance with the driver's resonance, and that is a legitimate reason to own one. It is not a deficiency of the jack.

The honest ranking, then: the Mac's jack is a good DAC and a modest amp. For a 300 ohm headphone at sane levels it is complete. For EQ with headroom, for very loud listening, or for the flavour of a particular amplifier, it is the thing you add to, and the registry will tell you which mode it was in when you did.

What to keep in mind

  • The SINAD figures were taken into 30 ohm. No published measurement shows SINAD into 300 ohm for this jack. The reviewer's distortion plots at 300 ohm look as clean as the 30 ohm ones, which is reassuring and not the same as a number.
  • The mode is chosen at plug-in. A plug seated slowly or a dirty socket can settle in the wrong bracket; the history will show it. Reseating the plug forces a new measurement.
  • The registry property names belong to Apple's driver and can change with any macOS release. The script reads what is there and prints n/a for what is not.
  • The I/O Registry describes the codec, not the sound. A codec in the right mode with a bad cable, a dirty jack or a wrong sample rate is still a codec in the right mode.
  • The crosstalk and filter figures depend on the output sample rate and the analyser's load. Treat them as the shape of the thing, not as a spec sheet.

The question had a one-word answer and a worthwhile detour. There is no profile. There is a chip that measures, decides and remembers, and that is the better design: it works for headphones Apple has never heard of.

Sources

  1. Apple Support, Use high-impedance headphones with your Mac
  2. Audio Science Review, Review and measurements of MacBook Air (2024) headphone output
  3. Linux kernel, device-tree binding for the Cirrus Logic CS42L84 codec
  4. Cirrus Logic, CS42L42 datasheet (the public cousin of the CS42L84)
  5. Sennheiser, HD 800 S product page and specifications
  6. MacRumors, Apple provides details on using high-impedance headphones with new MacBook Pros (2021)
  7. Headphonesty, MacBook Air headphone jack tests, a summary of the ASR measurements
  8. StereoNET forum, entry-level headphone amplifier spec comparison (JDS Atom, Schiit Magni output power)
  9. Frieve audio review index, Apple USB-C to 3.5 mm adapter measurements summary
  10. Apple Developer, IOKit documentation (I/O Registry)
  11. REXML, the XML toolkit bundled with Ruby