Given one pair of headphones, can we make them sound like another, perhaps higher quality pair of headphones? In this instructable, we will build a device that attempts to do just that. Our automatic headphone equalizer will measure the frequency response of one pair of headphones, and then adjust the six frequency bands to get a new a frequency response that is as close as possible to a pre-measured target pair. The quality of the hardware, the limitation on the number of bands and the gain in each band, and our restriction to only one audio channel (mono) will prevent us from perfectly making your $5 "cereal box" headphones to $1,500 headphones, but we can certainly try to make them sound closer!
Built by Jack Zhu and Paul Quigley over 5 weeks for the Stanford Class EE122A - Analog Circuits Laboratory. We've learned a lot throughout the design, build, and programming process, and our write-up definitely will have some errors. Please let us know what you would like to see us fix!
Step 1: Bill of Materials
We summarize our bill of materials below. See the attached spreadsheet for specific components values:
9 x LT1056 Op-amp
Adafruit 128*64 OLED Display
Arduino Mega 2560 Microcontroller
CEM-C9745 Electret Microphone
2 x 3.3 mm Audio Jack
AD5206 Digital Potentiometer
The following materials will also be helpful for construction and testing:
Digilent Electronics Explorer
2 x small breadboard
1 x small PC board
4 x aluminum standoffs
Step 2: Designing the Equalizer
Many well-established audio equalizer designs are widely available online. This 3 Band equalizer shows a low-pass, band-pass, and high-pass filter summed at an op amp. We opted for a solution that would allow us to add many similar stages while only varying component parts.
This RLC circuit equalizer lab is a good source for the fundamentals of the design we selected (first 2 images above are borrowed from this source, we would encourage you to read it!). The first image shows how a simple volume adjuster would work: the potentiometer with its wiper grounded changes the feedback resistance, thereby changing the gain. If, however, the value of "r" varied by frequency, we could have this volume adjustment only at a selected frequency. The next image shows just this: RLC band-pass circuits break input sound into various frequencies, with the potentiometers adjusting the gain of that RLC circuit's particular frequency band.
In order to get down to the lower frequencies while maintaining quality factor, you'll find that some large inductors (~1 Henry) are going to become necessary. We can replace the inductor with a op-amp in feed back, the "gyrator" simulates a RL circuit (image 3 above), using a much more compact op-amp configuration.
We designed the simplest configuration we could construct, while maintaining functionality and performance, then modeled the system in LTSpice. We see that the input of each filter is connected to the wiper of potentiometer, pictured above as resistors. The B sides of the potentiometers are connected to the positive rail of a summing amp, while the A sides of the potentiometer are connected to the negative rail of a summing amp. Moving the position of the wiper down increases the resistance before the gyrator, and thus lowers the gain on that band.
The bands we chose aren't perfect; we encourage you to build more experiment and change capacitor values to give you the best results! See the appendix on design calculations for help designing a band.
NOTE: The circuit pictured we built does not include a buffer stage, while the LTSpice model that we have a attached does. We recommend that you include a pre-amp in the circuit you include this, or at least a resistor, in your equalizer so as to avoid damaging audio source components.
Two example calculations that may be useful when designing your equalizer are given below.
Centering a band:
The center frequency of an RLC filter is given by
f0 = 1/(2pi sqrt(LC))
If we substitute in using the equivalent inductance of our gyrator, we have
L = RR'C' => f0 = 1/(2pi sqrt(CC'RR'))
For this band, we target a center frequency of around 100 Hz. Substituting C = .5uF, C' = 20nF, R = 220kOhm, and R' = 1kOhm We get
f0 = 107 Hz
Which meets our design specification.
Note that we arrived at the values we used through a combination of simulation, referring to other sources, and tuning. These calculations can simply help to get you started.
Step 3: Building and Testing the Equalizer Stage
We soldered our equalizer to in a compact design that you can see in the schematic above. Notice that in our final design we put the capacitor into header pins so that we could swap them out if we wanted to change adjust our bands.
Before we add digital control it is time to test the basic configuration of our amplifier. Connect the output of each filter to wipers of potentiometers as show in schematic of the previous step. Use a network analyzer, such as the one on the Digilent Electronics Explorer to test your equalizer. Does fully suppressing every band except one produce "humps" as shown in the network analyzer schematic above? If so you have successfully built a six band equalizer. See pictures above for each of our frequency band responses.
Step 4: Connecting to the Digital Potentiometer
Now that we have our equalizer constructed, we need a way of controlling the gain on each band programatically. An easy way to do this is with the AD5206 digital potentiometer. It's essentially six digital potentiometers in one, perfect for our six band equalizer! See this tutorial for more information.
An easy way to configure the potentiometer on our system is to connect the A side of each potentiometer to the the positve rail of a breadboard, the B side to the negative rail, and the wiper to the input of the appropriate filter.
When this is done, you can control the the gain on each band using the Arduino code attached. On the serial input, enter a string of the form "band,suppression" where band is between 0 and 5, and suppression is between 0 and 255. Once again you can test for the presence of distinct "humps" when you use the network analyzer and fully suppress all bands except one.
Step 5: Eliminate Noise Sources
While building this circuit, we often found that unexpected sources injected noise into our system. At this point, you should probe the output of your equalizer on sinewave inputs that fall in each of your designed bands. If you are not getting a clean sinewave output, some possible noise sources include:
- Op-amps. Different models of op-amps will have different slew-rates, which will in turn, affect the amount of high-frequency noise present in the circuit. We found that when we swapped the LME49710, which we originally using, for an LT1056 that we had a much cleaner signal.
- Power supplies. We found that powering off the Digilent Electronics Explorer had the potential to inject large amounts of 500kHz noise into our system. Switching to a bench power supply for the equalizer stage greatly improved our signal.
- Digital noise sources. Digital devices include a high-frequency clock that can inject serious noise into analog circuits. To reduce this, we power our digital pot from the Arduino, which is on USB power, and completely de-coupled from the analog power supply. We also found the analog noise injected into the ground pin of the digital pot was causing some SPI errors.
- We also found that putting a 220uF capacitor between power and ground greatly reduced high frequency noise, as well as changing the cutoff frequency of our summing amplifier with a small capacitor (220 pf)
Step 6: Microphone Pre-Amp
We chose the CEM-C9745 Electret Microphone for it's flat frequency response. See data sheet for details. Most electret microphones have flat frequency responses and use fairly low power
We tried several configurations of microphone pre-amps and found the best performance with a design based on the following blog post: http://lowvoltage.wordpress.com/2011/05/21/lm358-mic-amp/
In order to characterize our pair of headphones, we need to accurately measure that pair's frequency response. We will use an electret microphone, since it has a flat pass-band response. We first amplify the microphone according to the schematic above.
The op-amp is simply in an inverting configuration with 100x gain.
Our output is connected to a peak detector with a 10Hz cutoff, that allows us to estimate the amplitude of our output without having to do an FFT in software.
Here is an example calculation that you can use to tune your peak detector to meet the requirements of your bands:
For our peak detector, we want an RC time constant that is approximately (by rule of thumb) 5 times the lowest design frequency, in our case, around 10 Hz.
So we want RC = 5 * .1s = 500 ms
We choose R = 50kOhm and C = 10uF and we get Tau = 500 ms. Which meets our design specification.
Connect the output of this stage to pin A0 of your Arduino, so that we can use this mic to characterize our headphones.
Step 7: Audio Input/output
Thus far, we have been testing only using network analyzers. Before we close the loop, we need to test that our equalizer performs reasonably with actual audio signals. Simply connect the input of your equalizer to a 3.5mm audio jack, and plug this into a music playing device of your choice.
As shown, our equalizer only connects to the left channel. If you built two equalizers, you can can connect one to each channel for full stereo sound.
The output of our equalizer cannot power our headphones directly, since the op-amps will not supply the necessary current. We connected the output of the equalizer to a portable headphone amplifier via a 3.5mm audiojack http://www.amazon.com/E6-Fujiyama-Portable-Headpho..., and connect our headphones to this amplifier
We wanted to focus on the equalizing stage rather than amplifying stage. However, if you wanted to build your own class AB or class D amplifier, this could be an interesting addition to the project and a great learning experience.
Step 8: OLED Display and Buttons for I/O
We chose to include an Adafruit 128 x 64 OLED display which gives users instructions on how to calibrate their headphones, and allows them to scroll through various tuning options. A complete guide to using Adafruit's OLEDs can be found here: https://learn.adafruit.com/monochrome-oled-breako...
Note that we chose to you use the I2C protocol to communicate with our OLED, so if you wish to run our code, be sure to solder the jumpers on the back of the OLED and use the following pin configuration:
GND goes to ground
Vin goes to 5V
Data to I2C SDA (Pin 20 on the Mega)
Clk to I2C SCL ( A5 on the Mega it is 21 and on the Leonardo digital 3)
RST to digital 4
We shall allow the user to scroll through tuning options that we shall display on the OLED. Scrolling shall be done using two push buttons connected to pins 8 and 9. See this tutorial for more details on the buttons.
Step 9: Putting the Stages Together
In this step, we will take a step back to see how the different stages we have built fit together.
-- Our input audio signal is connected to the input of our equalizer.
-- The input is buffered with a pre-amp, then separated into six bands using RLC-equivalent gyrator filters.
-- A digital potentiometer controls the gain on each band.
-- The bands are summed together, and passed through a low pass filter to reduce high frequency noise.
-- The output of our equalizer is goes into a headphone amplifier, which provides power, so this signal is played by our headphones
Now, closing the loop
-- The output of our headphones is played into an electret microphone.
-- The output of the electret microphone is amplified, and passed through a peak detector.
-- This peak is then measured by our Arduino's analogRead function.
-- The Arduino writes to the digital potentiometer in order to adjust the gains and match whichever target headphone the user has selected
We mounted everything on a 3/8" acrylic sheet with adhesives and aluminum standoffs, but any platform will do.
We have not yet said how the Arduino calculates the values to write to the digital potentiometer. Let's explore that in the next step.
Step 10: Least Squares Fitting
In this step we describe the method which we will use to the software we shall use in future steps. We fit by method of least squares, and we summarize this below, for curious readers:
We want to set the gains on each band such that our input pair shall sound like our target pair. We can attempt this by assuming that the frequency response of our entire system will be a linear function of the gain on each band and the frequency response of the stages individually. That is we assume that it is of the form y=Ax, where y is a vector with our output frequency of size n, where n is the number of measurements we made while characterizing the headphones. A is a matrix whose columns represent the frequency response of each band (n x 6), and x is vector of size 6 which represents the gain on each stage.
We want to solve for X. A is a skinny matrix, so it cannot be inverted, but we can take it's pseudo inverse, (A^T A)^-1 A^T, and multiply it by our target frequency response. This will give us an x such that Ax is as close as possible to our target. Note that x may have values which are either negative, or greater than the maximum gain we can achieve. We will simply set the latter to 0 and the former to our max gain. In MATLAB, this is all done by "A \ x". We also had to handle some data inconsistencies such as interpolating to make sure that all samples are the same length. See our code for details.
Note that we must make one measurement with no sound playing to get a baseline, and subtract it from all future measurements in order for our assumption of linearity to possibly be valid.
We believe that their may be methods that give a tighter fit, such as stochastic gradient descent, which assumes no linear model. However, stochastic gradient descent would require us to repeatedly re-characterize our input headphones, which would be much slower, but possibly have a better fit.
Step 11: Arduino Client
We use the Arduino to communicate with a MATLAB server which sending frequency responses and requests for tuning to the Arduino.
Note that the software we have uploaded is dependent on the Adafruit OLED library.
Some more detail for those who are curious about implementation:
The Arduino can send the following messages to MATLAB:
- Begin frequency characterization
- analogRead values
- Request for tuning to a specific option (i.e. Bass Boost or fit to Sennheiser)
MATLAB can send the following messages to Arduino:
- Begin sending analogRead values
- Stop sending analogRead values
- Write value [0-255] to potentiometer [0-5]
The Arduino is essentially essentially a state machine. The state of the buttons and a set of booleans determine the action of the Arduino on each pass through loop. The booleans are changed by Serial commands sent from MATLAB.
Step 12: MATLAB Server
Our MATLAB server communicates via the protocol that we outlined in the last step. 5 files exist:
runserver.m Is the main server loop. It continually listens to a serial input, processes the commands sent to/from the Arduino. It also handles sending and receiving different pre-calibrated headphone setups. By default, our code connects on port "COM8". This may be different for any computer that you use.
build_basis.m gets called once for a run of the server. It builds the "basis vectors" of whatever headphone is currently on the microphone. It does this by setting the value of all digital pots to 255 (all 0 gain), except one, and obtaining the transfer function of the headphones with that band removed. It does this for all 6 channels, and then builds a "basis" matrix A that will be used for the least squares fitting step.
networkAnalyze_chirp runs a frequency sweep, in the form of a lograithmic chirp function, and plays the resulting sound from the computer speakers. It then grabs the value of the peak detector (being broadcast by the Arduino), and returns the amplitudes and the offset of the response.
target_fit takes a basis matrix A, and fits it to a target headphone A via least squares. It then sets the digial pots to the fitted values, and performs a final frequency sweep on the headphones for verification.
write_pots publishes the values of chosen (an array of values, length 6, values 0 - 255) to the Arduino. it pauses between each send by an amount specified by delay in order to ensure the Arduino is finished writing to SPI.
Please download and play around with the code!
Note: This can also be implemented on the Arduino -- we challenge the advanced Arduino user to do so!
Step 13: Measuring the Results
Above, we can see the frequency response plot generated by MATLAB when we characterize our HD 439 Sennheiser headphones. We see that it shares several distinctive features with the official transfer function on headroom.com, which makes us more confident in our system's performance.
We also show some examples of the Sennheiser basis functions (image 3), and fitting the Sennheiser headphones.
Image 4 shows fitting the Sennheiser earphones to itself, given two different runs and conditions. The runs had different amounts of force on the ear pad, the volumes of the audio, and the noise in the environment, but we see that they are relatively similar.
Image 5 shows fitting the Sennheiser earphones to a flat band. The norm decreased, but as we can see, the fit is closer, but not perfect.
Image 6 shows fitting the Sennheiser earphones to the Beats earphones. Again, we see a clear qualitative shift from the original Sennheiser transfer function to the fitted transfer function.
Several of the reasons we don't get perfect (or better) fits:
We are limited to a limited range of resistance values in the digital pots. We handle negative values of fitted weights by saturating them to zero, and values greater than 255 by saturating them to 255. This results in a possible non-optimal solution. to get closer, we could implement one of many iterative optimization schemes, or use multiple objective functions. We challenge you to do this!
The bands we picked ended up being very narrow (see Image 3). This leaves large variations in the values of bands that least squares can tune to. We also see that the singular values of the basis (Image 7) are small(er) after about the 3rd value. Ideally, we would have wider bands, and higher singular values, which would correspond to higher control dimensionality and possibly the ability to match more target headphone transfer functions.
Step 14: Conclusions
We're pretty happy with our final equalizer, and have had some fun trying to turn one brand of headphones into another. We do, however, want to reiterate drawbacks, and possible improvements to our design:
- We only equalize one sound channel (If you want stereo equalization, build two equalizers and two digital pots and attach them to the left and right channels).
- Our bands ended up being a bit too narrow, and left some gaps. Our next version will have wider, and possibly more bands.
- We assumed a linear approximation of the human auditory system, which can definitely be improved. We could have fit our signal to the log of the input signal, which may be a better model to how the human ear detects volume.
- Our fitting step is one-shot, we could use iterative methods to approach a better fit given our saturation limits
- Even at the highest gain, we never reach much more than 20 dB (10x amplification in magnitude) gain. We can change this by having higher potentiometer resistances or use dedicated amplifier components with variable gain.
Future versions may also replace all analog components with Digital filters (DSP), and a smaller package.
- We'd like to thank our Professor Gregory Kovacs and our TA Bill Esposito.