Spinorama manual

Published

May 7, 2025

Abstract

How to use the library step by step!

How to use the library?

How to load the data?

Start by loading some classical libraries:

import numpy as np
import pandas as pd
import plotly as plt

Then some specific functions from Spinorama:

from spinorama.load_spl_hv_txt import parse_graph_spl_hv_txt
from spinorama.load import filter_graphs
from spinorama.constant_paths import MEAN_MIN, MEAN_MAX, DEFAULT_FREQ_RANGE

Loading a dataset in text format

The parser expect to find 72 files in this directory:

  • name of the files matches *_H angle.txt for horizontal measurements with angle between -170 and 180 in 10 degrees increment
  • name of the files matches *_V angle.txt for vertical measurements with angle between -170 and 180 in 10 degrees increment
speaker = 'Ascend Acoustics Sierra-2EX V2'
dir = f'../datas/measurements/{speaker}/vendor'
mformat='spl_hv_txt'

# read horizontal and vertical data
# spl_H and spl_V are dataframe
_, spl_H = parse_graph_spl_hv_txt(dir, 'H')
_, spl_V = parse_graph_spl_hv_txt(dir, 'V')

# put them in a convenient dictionnary of dataframe
df = filter_graphs(speaker, spl_H, spl_V, MEAN_MIN, MEAN_MAX, mformat=mformat, mdistance=1)

How to load all the precomputed data in one call

import logging
import sys, os

sys.path.append(os.path.expanduser("../src"))
sys.path.append(os.path.expanduser(".."))

from generate_common import cache_load_seq, get_custom_logger
import datas.metadata as metadata

Set logging level:

level = logging.DEBUG
logger = get_custom_logger(level=level, duplicate=True)

Load the data:

datas = cache_load_seq(filters={}, smoke_test=True)
(loaded 11 speakers)

If you use smoke_test set to True, you only load a few speakers. If set to False, you load all of them, it can take one or two minutes to load the files. It returs a dictionnary organised by speaker, then by origin then by version of the measurement.

List of speakers:

print(datas.keys())
dict_keys(['DIYSG HT-10', 'Infinity Interlude IL10', 'Mackie CR4', 'Monoprice MM-3', 'Tekton Troubadour', 'JBL AC25', 'JBL LSR305', 'Midiplus MI5 II', 'Nexo PS10', 'Optimal Audio Cuboid 15', 'Vandersteen 2c'])

List of graphs for a speaker:

print(datas['DIYSG HT-10'].keys())
print(datas['DIYSG HT-10']['Misc'].keys())
print(datas['DIYSG HT-10']['Misc']['misc-mtg90'].keys())
dict_keys(['Misc'])
dict_keys(['misc-mtg90', 'misc-mtg90_eq'])
dict_keys(['CEA2034', 'CEA2034 Normalized', 'CEA2034 Normalized_unmelted', 'CEA2034_unmelted', 'Early Reflections', 'Early Reflections_unmelted', 'Estimated In-Room Response', 'Estimated In-Room Response Normalized', 'Estimated In-Room Response Normalized_unmelted', 'Estimated In-Room Response_unmelted', 'Horizontal Reflections', 'Horizontal Reflections_unmelted', 'On Axis', 'On Axis_unmelted', 'SPL Horizontal', 'SPL Horizontal_normalized_unmelted', 'SPL Horizontal_unmelted', 'SPL Vertical', 'SPL Vertical_normalized_unmelted', 'SPL Vertical_unmelted', 'Vertical Reflections', 'Vertical Reflections_unmelted', 'sensitivity', 'sensitivity_1m', 'sensitivity_distance'])

CEA2034 for the same speaker:

print(datas['DIYSG HT-10']['Misc']['misc-mtg90']['CEA2034'][0:10])
      Freq Measurements         dB
0  20.0000      On Axis -36.443836
1  20.2909      On Axis -35.744836
2  20.5860      On Axis -35.070836
3  20.8855      On Axis -34.417836
4  21.1893      On Axis -33.764836
5  21.4975      On Axis -33.148836
6  21.8102      On Axis -32.646836
7  22.1274      On Axis -32.139836
8  22.4492      On Axis -31.704836
9  22.7758      On Axis -31.222836

CEA2034 for the same speaker in a different format:

print(datas['DIYSG HT-10']['Misc']['misc-mtg90']['CEA2034_unmelted'][0:10])
      Freq    On Axis  Listening Window  Sound Power  Early Reflections  \
0  20.0000 -36.443836        -37.854667   -36.872278         -36.993473   
1  20.2909 -35.744836        -37.124235   -36.158043         -36.277658   
2  20.5860 -35.070836        -36.451606   -35.486504         -35.605859   
3  20.8855 -34.417836        -35.731935   -34.784833         -34.891403   
4  21.1893 -33.764836        -35.088759   -34.161523         -34.273449   
5  21.4975 -33.148836        -34.448149   -33.500303         -33.620336   
6  21.8102 -32.646836        -33.915044   -32.981184         -33.098504   
7  22.1274 -32.139836        -33.400733   -32.496287         -32.602930   
8  22.4492 -31.704836        -32.947137   -32.060434         -32.160116   
9  22.7758 -31.222836        -32.395652   -31.524028         -31.625929   

   Early Reflections DI  Sound Power DI  DI offset  
0             -0.861194       -0.982389          0  
1             -0.846576       -0.966192          0  
2             -0.845747       -0.965102          0  
3             -0.840532       -0.947102          0  
4             -0.815309       -0.927236          0  
5             -0.827813       -0.947846          0  
6             -0.816540       -0.933861          0  
7             -0.797804       -0.904447          0  
8             -0.787021       -0.886704          0  
9             -0.769724       -0.871625          0  

Computing with the data

Computing the spinorama (CEA2034)

from spinorama.compute_cea2034 import compute_cea2034
# compute the spin
spin = compute_cea2034(df['SPL Horizontal_unmelted'], df['SPL Vertical_unmelted'])

Computing classical values for this speaker

from spinorama.compute_estimates import estimates
# compute the spin
spin = compute_cea2034(df['SPL Horizontal_unmelted'], df['SPL Vertical_unmelted'])
properties = estimates(spin, df['SPL Horizontal_unmelted'], df['SPL Vertical_unmelted'])
print('Reference point (Hz) at which the SPL value dropped by 3 dB with respect to the average of the on-axis measurement between [{}, {}]: {:5.1f} Hz'.format(
         properties['ref_from'],
         properties['ref_to'],
         properties['ref_3dB'],
))
print('Directivity in degrees at which the SPL value dropped by 6 d with respect to the on-axis measurementB: {:5.1f} deg'.format(properties['directivity_horizontal_avg']))
properties
Reference point (Hz) at which the SPL value dropped by 3 dB with respect to the average of the on-axis measurement between [300.0, 5000.0]:  58.6 Hz
Directivity in degrees at which the SPL value dropped by 6 d with respect to the on-axis measurementB:  75.0 deg
{'ref_3dB': np.float64(58.6),
 'ref_6dB': np.float64(51.3),
 'ref_9dB': np.float64(43.9),
 'ref_12dB': np.float64(39.6),
 'ref_from': 300.0,
 'ref_to': 5000.0,
 'ref_level': np.float64(-0.3),
 'ref_band': np.float64(2.7),
 'sensitivity_delta': np.float64(-0.26316630303028626),
 'directivity_horizontal_pos': 80.0,
 'directivity_horizontal_neg': -70.0,
 'directivity_horizontal_avg': 75.0,
 'directivity_vertical_pos': 10.0,
 'directivity_vertical_neg': -20.0,
 'directivity_vertical_avg': 15.0}

Compute the harmann/olive score

Compute the PIR (Predicted In-Room Response)

from spinorama.compute_cea2034 import estimated_inroom_hv
pir = estimated_inroom_hv(df['SPL Horizontal_unmelted'], df['SPL Vertical_unmelted'])

Compute the PIR (Predicted In-Room Response)

from spinorama.load import graph_melt, graph_unmelt
from spinorama.compute_cea2034 import compute_cea2034, estimated_inroom_hv
from spinorama.compute_scores import speaker_pref_rating
spin = compute_cea2034(df['SPL Horizontal_unmelted'], df['SPL Vertical_unmelted'])
pir = estimated_inroom_hv(df['SPL Horizontal_unmelted'], df['SPL Vertical_unmelted'])
scores = speaker_pref_rating(graph_melt(spin), graph_melt(pir), rounded=True)
scores
{'nbd_on_axis': 0.359,
 'nbd_listening_window': 0.321,
 'nbd_sound_power': 0.297,
 'nbd_pred_in_room': 0.282,
 'sm_pred_in_room': 0.876,
 'sm_sound_power': 0.943,
 'pref_score_wsub': 7.97,
 'lfx_hz': 43,
 'lfq': 0.829,
 'pref_score': 5.9,
 'aad_on_axis': 0.454}

Plotting the data

Example of the parameters you can change to adapt the layout to your liking. See plotly documentation for all the options

my_layout = dict(
    width=700,
    height=400,
    title=dict(
        x=0.5,
        y=1.0,
        xanchor="center",
        yanchor="top",
        text=speaker,
        font=dict(
            size=18,
        ),
    ),
    legend=dict(
        x=1.2,
        y=1,
        xanchor="center",
        orientation="v",
        font=dict(
            size=10,
        ),
    ),
    font=dict(
        size=10
    ),
    margin=dict(
    l=0,
        r=0,
        b=30,
        t=30,
        pad=4
    ),
)

All the plot functions are in:

import spinorama.plot as plot

CEA2034 plot (aka spinorama plot)

plot_spin = plot.plot_spinorama(
    spin=spin,
    params=plot.plot_params_default,
    minmax_slopes=None,
    is_normalized=False,
    valid_freq_range=DEFAULT_FREQ_RANGE
)
plot_spin.update_layout(my_layout)
plot_spin

Normalized CEA2034 plot

plot_spin = plot.plot_spinorama(
    spin=spin,
    params=plot.plot_params_default,
    minmax_slopes=None,
    is_normalized=True,
    valid_freq_range=DEFAULT_FREQ_RANGE
)
plot_spin.update_layout(my_layout)
plot_spin

On Axis plot

plot_onaxis = plot.plot_graph(df['On Axis_unmelted'], plot.plot_params_default, DEFAULT_FREQ_RANGE)
plot_onaxis.update_layout(my_layout)
plot_onaxis

If you also want to see regression lines:

plot_onaxis = plot.plot_graph_flat(df['On Axis_unmelted'], "On Axis", plot.plot_params_default, DEFAULT_FREQ_RANGE)
plot_onaxis.update_layout(my_layout)
plot_onaxis

Early reflection plot

plot_er = plot.plot_graph(df['Early Reflections_unmelted'], plot.plot_params_default, DEFAULT_FREQ_RANGE)
plot_er.update_layout(my_layout)
plot_er

Predicted In-Room Response (aka PIR) plot

plot_pir = plot.plot_graph_regression(
  df=df['Estimated In-Room Response_unmelted'],
  measurement='Estimated In-Room Response',
  params=plot.plot_params_default,
  minmax_slopes=None,
  is_normalized=False,
  valid_freq_range=DEFAULT_FREQ_RANGE
)
plot_pir.update_layout(my_layout)
plot_pir

Directivity plots

There are various ways to represent directivity plots:

Contour plots

plot_contour_h = plot.plot_contour(df['SPL Horizontal_unmelted'], plot.contour_params_default, DEFAULT_FREQ_RANGE)
plot_contour_h.update_layout(my_layout)
plot_contour_h

The same one, normalized to on axis:

plot_contour_h = plot.plot_contour(df['SPL Horizontal_normalized_unmelted'], plot.contour_params_default, DEFAULT_FREQ_RANGE)
plot_contour_h.update_layout(my_layout)
plot_contour_h

3D Contour plots

plot_contour_h = plot.plot_contour_3d(df['SPL Horizontal_unmelted'], plot.contour_params_default, DEFAULT_FREQ_RANGE)
plot_contour_h.update_layout(my_layout)
plot_contour_h

The same one, normalized to on axis:

plot_contour_h = plot.plot_contour_3d(df['SPL Horizontal_normalized_unmelted'], plot.contour_params_default, DEFAULT_FREQ_RANGE)
plot_contour_h.update_layout(my_layout)
plot_contour_h

Radar plots

plot_contour_h = plot.plot_radar(df['SPL Horizontal_unmelted'], plot.radar_params_default, DEFAULT_FREQ_RANGE)
plot_contour_h.update_layout(my_layout)
plot_contour_h
plot_contour_h = plot.plot_radar(df['SPL Vertical_unmelted'], plot.radar_params_default, DEFAULT_FREQ_RANGE)
plot_contour_h.update_layout(my_layout)
plot_contour_h