Steven Jewel Blog RSS Feed

07 Nov 2013

Detecting Headphone Presence

On my desktop computer at work I have both speakers and headphones. When I'm wearing the headphones I want the speakers to be muted. Like most computers, when you plug the headphones in the front, it mutes the line-out on the back, but unfortunately my desktop is under the desk and it'd be impractical to reach under and mute it.

Since I switch between wearing them and not wearing them quite frequently, I wanted something that would automatically mute the speakers when I put the headphones on. As a bonus, I'd like my music to pause or unpause automatically. (I share an office, so leaving the music playing all the time won't work.)

Instead of creating a keyboard shortcut to mute or unmute the speakers, I got a little carried away. I have a webcam, so I thought that I could attach a QR code on the top of the headphones. Then, looking at myself in the webcam, I realized that the color of the headphones might be enough.

Wearing Headphones

Perhaps it'd be possible to analyze the webcam image and determine whether or not I was wearing them by counting green pixels.

I tried grabbing a frame from the webcam with mplayer, but it turned out that the image was too dark until the camera had a chance to adjust itself. It's just as easy to read video frames.

# Count number of green pixels.
#
# "image" is a string containing the RGB data
def count_green image
  count = 0
  image.chars.each_slice(3) do |r,g,b|
    count += 1 if g.ord > 64 && g.ord*2 > r.ord*3 && g.ord*2 > r.ord*3
  end
end

width = 640
height = 480
dim = "#{width}x#{height}"
video = IO.popen( "avconv -f video4linux -s #{dim} -i /dev/video0 -pix_fmt rgb24 -f rawvideo -vcodec rawvideo -s #{dim} -", 'rb' )

while image = video.read( width * height * 3 )
  headphones_present = count_green(image) > 0

  if headphones_present
    system "mute-speakers"
  else
    system "unmute-speakers"
  end
end

Matches

It turns out that it works perfectly. In the example above, 938 pixels match. When I'm not wearing the headphones, less than a dozen match!

To be safe, I've added in some extra logic to reduce the amount of switches, which adds a slight delay. After using it for a few weeks I'll have the values tuned to better values.

Of course, sampling nearly a million pixels every second is not something that ruby best suited for (it takes 137ms/frame on an i7). Luckily, it's easy to replace just that portion with C code using the RubyInline gem.

require 'inline'

class GreenCounter
  inline do |builder|
    builder.add_compile_flags '-O3 -Wall'
    builder.c_singleton File.read('green.c')
  end
end

And the C code:

int green( VALUE image_str) {
  int i = 0;
  char* image = StringValuePtr( image_str );
  int size = RSTRING_LEN( image_str );
  int count = 0;
  unsigned char r, g, b;
  for( i = 0; i < size; i += 3 ) {
    r = image[i+0];
    g = image[i+1];
    b = image[i+2];
    if( g > 64 && g*2 > r*3 && g*2 > b*3 )
      count++;
  }
  return count;
}

The C version is able to scan an image in 0.4ms, a 340× speedup!

Full code is available on github.