# Loudspeaker Analysis and Experiments: Part I

#### Part I: Thiele-Small Parameters, Impedance, and Resonance

A modern loudspeaker is a crossover technology comprised of electrical and mechanical components. And while this design helps humans enjoy the analog world using digitization, it also creates a complex problem that encompasses the fields of fluid dynamics and electrical engineering. In order to demystify the loudspeaker, two engineers: Neville Thiele and Richard H. Small derived relationships between the physical parameters of a loudspeaker and its acoustic performance. These parameters, called the Thiele-Small Parameters, are still used today to design audio systems and remain a cornerstone for quantifying the performance of such a system [read more about Thiele-Small Parameters here and here, and design aspects here and here].

In this tutorial, a loudspeaker will be analyzed by calculating the Thiele-Small parameters from impedance measurements using an inexpensive USB data acquisition system (minimum sampling rate of 44.1 kHz). The methods used in this project will educate the user on multiple engineering topics ranging from: data acquisition, electronics, acoustics, signal processing, and computer programming.

#### Thiele-Small Parameters and Background

A few crutial Thiele-Small pararmeters are cited below based on the Thiele and Small papers:

The parameters above give information about the speaker’s performance and limitations, which define some physical characteristics of the loudspeaker. These characteristics are difficult to quantify once the loudspeaker is assembled, so we are forced to approximate the following physical characteristics using experiments:

A few notes on the definitions above: r represents a parameter calculated at the loudspeaker’s resonance frequency. Additionally, e represents a parameter in the electrical domain; m represents a parameter in the mechanical domain, and a represents the acoustic domain. From this point onward, I will use each parameter to define another parameter using specific equation derived by Thiele and Small.

#### Parts List for Experiments

This is a fairly involved experiment in that it requires the following core parts: a computer, a USB acquisition device (at least 44.1 kHz sampling rate), a loudspeaker driver, and small calibration weights. These components will allow us to fully characterize the loudspeaker using the Thiele-Small parameters. Below is the full parts list used in my method of calculating the parameters:

1. Boss Audio 80 Watt Loudspeaker - $10.79 [Amazon] 2. Behringer UCA202 USB Audio Interface -$29.99 [Amazon]

3. Calibration Weights - $6.99 [Amazon] 4. Audio Amplifier 15 Watt -$8.99 [Amazon]

5. Speaker Wire - $8.49 [Amazon] 6. Multimeter with AC Voltmeter -$37.97 [Amazon]

7. Alligator Clips - $6.39 [Amazon] 8. Jumper Wires -$6.49 [Amazon]

9. Resistor Kit - $7.99 [Amazon] 10. Breadboard -$7.99 [Amazon]

11. 3.5 mm cable - $5.10 [Amazon] Boss Audio Full Range 80W Driver #### Measuring the DC Coil Resistance The first step in the process is to measure the speaker’s electrical resistance, which should always be less than the nominal resistance. Start by measuring the resistance of the leads on your multimeter, then subtract that from the resistance measured across the terminals of the speaker. I measured the resistance to be 3.3 Ohms: This value will be necessary for calculating the electrical damping factor, Qer , and efficiency η0 . #### Wiring for Impedance Measurement The wiring for this experiment is somewhat involved, however, the root of the wiring method is based on a voltage divider with the speaker acting as the second resistor. The full diagram is shown below: The process flow for the wiring above is as follows: 1. A smartphone app called ‘Audio Function Generator’ is used to generate a sine wave sweep or constant frequency into the amplifier’s 3.5 mm input 2. The amplified signal is wired across the voltage divider 3. The voltage across the amplified signal is inputted into the Behringer USB acquisition device 4. The voltage across the loudspeaker terminals is inputted into the Behringer USB acquisition device 5. The USB stereo input is read by Python on a computer (Raspberry Pi in my case) Using the wiring method above, we will be able to approximate the resonance frequency of the driver where the impedance is maximum. We will also be able to find parameters relating to the electrical and mechanical properties of the driver. #### Identifying the Resonance Region Using a Full-Spectrum Frequency Sweep We’ll be using the Behringer UCA202 USB audio interface to sample voltage readings at 44.1 kHz on a Raspberry Pi. The general setup for finding the resonance frequency starts by wiring the loudspeaker in series with a resistor to measure voltage across the speaker. And since the voltage varies when we excite the speaker using a sinusoidal function, we need to approximate the impedance across the loudspeaker. The voltage divider equation can be rewritten specifically for our scenario: where Vspeaker is the voltage measured across the loudspeaker, Vsupply is the input voltage applied to the system, R1 is the resistance of the chosen resistor (I used 10 Ohms), and the approximate impedance here is Rspeaker , which can be solved for: This result is important for many reasons. It allows us to approximate the maximum impedance, Zmax (Rspeaker,max ), which allows us to cite the resonance frequency and also solve for other crutial variables like the damping and efficiency of the system. Therefore, once the resonance is discovered we can begin to calculate other parameters necessary to characterizing the perforamnce of a loudspeaker. An impedance plot of the loudspeaker that I used is shown below for a frequency sweep from 20 Hz - 20,000 Hz. I sampled the impedance over 180 seconds. It was done in three pieces and then stitched together. I used the ranges of 20 Hz - 120 Hz, 120 Hz - 2,000 Hz, and 2,000 Hz - 20,000 Hz. This was done to avoid diminishing the peak and the transition zone between high and low frequencies. I used a sampling period of 1 second, which resulted in a frequency resolution of 1 Hz. Impedance Plot Identifying Resonance Region Several observations can be made regarding the behavior of the impedance plot above. The first is - there is a clear peak which we can identify as the resonance frequency. For the loudspeaker I'm using, the resonance is located around 86 Hz, but the plot above shows about 93 Hz. In the narrower plot below I show a better approximation of the resonance. The full spectrum sweep identifies the resonance region. There are also a few extraneous peaks that likely represent some sort of noise in the input signal. The noise is likely due to the sampling interval and how the smartphone app handles the sweep. Also - the user may notice an increase in the impedance as the frequency increases toward infinity. This is often cited as an artifact of the speaker's inductance, which can act as a frequency filter (hence the increase in impedance with frequency). The important thing to remember is that we have identified the resonance region of the loudspeaker, which we will further explore in the next section when we discuss phase behavior and approximating the amplitude of the impedance at resonance. #### Impedance Magnitude and Phase Relation to Resonance There are multiple ways of identifying the actual resonance frequency of a loudspeaker. Many manufacturers will put the impedance response curve on their datasheet, and fewer will include the phase measurement, which is often correlated to the derivative of the impedance. This means that if we plot both the impedance response and the phase, we should see a phase zero-crossing around the resonance frequency. The response curves for phase and impedance can be seen below calculated for the loudspeaker used in this project (44.1kHz, 10 sec recording, 20 Hz - 220 Hz sweep): The full code to replicate the figures above is also given below import pyaudio import numpy as np import matplotlib.pyplot as plt plt.style.use('ggplot') def fft_calc(data_vec): fft_data_raw = (np.fft.fft(data_vec)) fft_data = (fft_data_raw[0:int(np.floor(len(data_vec)/2))])/len(data_vec) fft_data[1:] = 2*fft_data[1:] return fft_data form_1 = pyaudio.paInt16 # 16-bit resolution chans = 2 # 1 channel samp_rate = 44100 # 44.1kHz sampling rate record_secs = 10 # seconds to record dev_index = 2 # device index found by p.get_device_info_by_index(ii) chunk = 44100*record_secs # 2^12 samples for buffer R_1 = 9.8 # measured resistance from voltage divider resistor R_dc = 3.3 # measured dc resistance of loudspeaker freq_sweep_range = (40.0,200.0) # range of frequencies in sweep (plus or minus a few to avoid noise at the ends) audio = pyaudio.PyAudio() # create pyaudio instantiation # create pyaudio stream stream = audio.open(format = form_1,rate = samp_rate,channels = chans, \ input_device_index = dev_index,input = True, \ frames_per_buffer=chunk) stream.stop_stream() # pause stream so that user can control the recording input("Click to Record") data,chan0_raw,chan1_raw = [],[],[] # loop through stream and append audio chunks to frame array stream.start_stream() # start recording for ii in range(0,int((samp_rate/chunk)*record_secs)): data.append(np.fromstring(stream.read(chunk),dtype=np.int16)) # stop, close stream, and terminate pyaudio instantiation stream.stop_stream() stream.close() audio.terminate() print("finished recording\n------------------") # loop through recorded data to extract each channel for qq in range(0,(np.shape(data))): curr_dat = data[qq] chan0_raw.append(curr_dat[::2]) chan1_raw.append((curr_dat[1:])[::2]) # conversion from bits chan0_raw = np.divide(chan0_raw,(2.0**15)-1.0) chan1_raw = np.divide(chan1_raw,(2.0**15)-1.0) # Calculating FFTs and phases spec_array_0_noise,spec_array_1_noise,phase_array,Z_array = [],[],[],[] for mm in range(0,(np.shape(chan0_raw))): Z_0 = ((fft_calc(chan0_raw[mm]))) Z_1 = ((fft_calc(chan1_raw[mm]))) phase_array.append(np.subtract(np.angle(Z_0,deg=True), np.angle(Z_1,deg=True))) spec_array_0_noise.append(((np.abs(Z_0[1:])))) spec_array_1_noise.append(((np.abs(Z_1[1:])))) # frequency values for FFT f_vec = samp_rate*np.arange(chunk/2)/chunk # frequency vector plot_freq = f_vec[1:] # avoiding f = 0 for logarithmic plotting # calculating Z Z_mean = np.divide(R_1*np.nanmean(spec_array_0_noise,0), np.subtract(np.nanmean(spec_array_1_noise,0),np.nanmean(spec_array_0_noise,0))) # setting minimum frequency locations based on frequency sweep f_min_loc = np.argmin(np.abs(plot_freq-freq_sweep_range)) f_max_loc = np.argmin(np.abs(plot_freq-freq_sweep_range)) max_f_loc = np.argmax(Z_mean[f_min_loc:f_max_loc])+f_min_loc f_max = plot_freq[max_f_loc] # print out impedance found from phase zero-crossing print('Resonance at Z-based Maximum:') print('f = {0:2.1f}, Z = {1:2.1f}'.format(f_max,np.max(Z_mean[f_min_loc:f_max_loc]))) print('------------------') # smoothing out the phase data by averaging large spikes smooth_width = 10 # width of smoothing window for phase phase_trimmed = (phase_array)[f_min_loc:f_max_loc] phase_diff = np.append(0,np.diff(phase_trimmed)) for yy in range(smooth_width-1,len(phase_diff)-smooth_width): for mm in range(0,smooth_width): if np.abs(phase_diff[yy]) > 100.0: phase_trimmed[yy] = (phase_trimmed[yy-mm]+phase_trimmed[yy+mm])/2.0 phase_diff[yy] = (phase_diff[yy-mm]+phase_diff[yy+mm])/2.0 if np.abs(phase_diff[yy]) > 100.0: continue else: break if np.abs(phase_diff[yy]) > 100.0: phase_trimmed[yy] = np.nan ##### plotting algorithms for impedance and phase #### fig,ax = plt.subplots() fig.set_size_inches(12,8) # Logarithm plots in x-axis p1, = ax.semilogx(plot_freq[f_min_loc:f_max_loc],Z_mean[f_min_loc:f_max_loc],label='$Z$',color='#7CAE00') ax2 = ax.twinx() # mirror axis for phase p2, = ax2.semilogx(plot_freq[f_min_loc:f_max_loc],phase_trimmed,label='$\phi$',color='#F8766D') # plot formatting subplot_vec = [p1,p2] ax2.legend(subplot_vec,[l.get_label() for l in subplot_vec],fontsize=20) ax.yaxis.label.set_color(p1.get_color()) ax2.yaxis.label.set_color(p2.get_color()) ax.set_ylabel('Impedance [$\Omega$]',fontsize=16) ax2.set_ylabel('Phase [Degrees]',fontsize=16) ax2.grid(False) ax.spines["right"].set_edgecolor(p1.get_color()) ax2.spines["right"].set_edgecolor(p2.get_color()) ax.tick_params(axis='y', colors=p1.get_color()) ax2.tick_params(axis='y', colors=p2.get_color()) ax.set_xlabel('Frequency [Hz]',fontsize=16) peak_width = 70.0 # approx width of peak in Hz ax.set_xlim([f_max-(peak_width/2.0),f_max+(peak_width/2.0)]) ax.set_ylim([np.min(Z_mean[f_min_loc:f_max_loc]),np.max(Z_mean[f_min_loc:f_max_loc])+0.5]) ax2.set_xlim([f_max-(peak_width/2.0),f_max+(peak_width/2.0)]) ax2.set_ylim([-45,45]) ax.set_xticks([],minor=True) ax2.set_xticks(np.arange(f_max-(peak_width/2.0),f_max+(peak_width/2.0),10)) ax2.set_xticklabels(['{0:2.0f}'.format(ii) for ii in np.arange(f_max-(peak_width/2.0),f_max+(peak_width/2.0),10)]) # locating phase and Z maximums to annotate the figure Z_max_text = ' = {0:2.1f}$\Omega$'.format(np.max(Z_mean[f_min_loc:f_max_loc])) f_max_text = ' = {0:2.1f} Hz'.format(plot_freq[np.argmax(Z_mean[f_min_loc:f_max_loc])+f_min_loc]) ax.annotate('$f_{max}$'+f_max_text+',$Z_{max}$'+Z_max_text,xy=(plot_freq[np.argmax(Z_mean[f_min_loc:f_max_loc])+f_min_loc], np.max(Z_mean[f_min_loc:f_max_loc])),\ xycoords='data',xytext=(-300,-50),size=14,textcoords='offset points', arrowprops=dict(arrowstyle='simple', fc='0.6',ec='none')) # from phase phase_f_min = np.argmin(np.abs(np.subtract(f_max-(peak_width/2.0),plot_freq))) phase_f_max = np.argmin(np.abs(np.subtract(f_max+(peak_width/2.0),plot_freq))) phase_min_loc = np.argmin(np.abs(phase_array[phase_f_min:phase_f_max]))+phase_f_min Z_max_text_phase = ' = {0:2.1f}$\Omega$'.format(Z_mean[phase_min_loc]) f_max_text_phase = ' = {0:2.1f} Hz'.format(plot_freq[phase_min_loc]) ax2.annotate('$\phi_{min}$'+' ={0:2.1f}$^\circ$'.format(np.abs(phase_array[phase_min_loc]))+',$f_{max}$= '+f_max_text_phase+\ '\n$Z_{max}\$'+Z_max_text_phase,
xy=(plot_freq[phase_min_loc],phase_array[phase_min_loc]),\
xycoords='data',xytext=(-120,-150),size=14,textcoords='offset points',arrowprops=dict(arrowstyle='simple',
fc='0.6',ec='none'))
# print out impedance found from phase zero-crossing
print('Resonance at Phase Zero-Crossing:')
print('f = {0:2.1f}, Z = {1:2.1f}'.format(plot_freq[phase_min_loc],Z_mean[phase_min_loc]))

# uncomment to save plot
##plt.savefig('Z_sweep_with_phase.png',dpi=300,facecolor=[252/255,252/255,252/255])

plt.show()


The code above and all the codes for this project can be found on the project’s GitHub page:

### https://github.com/engineersportal/thiele_small_parameters.git

Therefore, for the case of our loudspeaker - we can say that its resonance frequency is about 86 Hz. And by using the actual RMS values of steady frequency inputs, I have another plot that shows just how close this is to the likely resonance:

Plot of impedance and phase around resonance

This method is done by hand, not using an FFT. We can still see similar behavior using this method, indicating that the FFT method is accurate (and time-saving).

The manual method shown directly above can be used to approximate the resonance, however, I will use the quicker and nearly as accurate FFT method. And in the next entry, we will be doing multiple measurements of resonance at different mass loadings - so the manual method would really time quite a bit of time.

#### Conclusion and Continuation

In this first entry into the loudspeaker analysis series, I discussed the Thiele-Small parameters and the notion of impedance and resonance. The complex nature of loudspeakers makes this series an educational and diverse topic in engineering. I explored how to find the resonance frequency of a speaker driver and how to use both phase and magnitude to approximate the frequency of the resonance and the magnitude of the impedance at resonance. Both of these values will become instrumental in characterizing loudspeakers and audio systems for use in real-world applications.

In the next entry, I discuss how to find the remaining mechanical and electrical properties of a loudspeaker and the applications that they open up in terms of design in acoustic environments.

See More in Acoustics and Engineering: