To whom may interest… Finally I managed to get the spectrum of the actual 3D sound reaching each ear! For this I had to use directly the FMOD library through pyfmodex
(https://github.com/tyrylu/pyfmodex/tree/master/pyfmodex) - a very nice python bindings to FMOD library.
The code bellow is an example showing in real-time the 3D panning of a sound passing in front of your head from left to right and then from rigth to left. You will note that when the sound is going to the left side, the bars of the spectrum chart representing the right ear gradually disappear once the volume in this ear is getting low.
I tested with only one listener, once the FMOD documentation says that some DSP effects are disabled when using multiple listeners to avoid confusion (https://www.fmod.org/docs/content/generated/overview/3dsound.html):
The code to get left and right spectrums (you must have PyQt or PySide):
import sys
import pyfmodex
from pyfmodex.constants import FMOD_SOFTWARE, FMOD_LOOP_NORMAL, FMOD_3D
from PyQt5 import QtWidgets, QtGui, QtCore
LEFT_CHANNEL = 0
RIGHT_CHANNEL = 1
FMOD_DSP_FFT_WINDOW_RECT = 0
class FrequencyAnalysis(QtWidgets.QWidget):
def __init__(self, fmod, sound):
QtWidgets.QWidget.__init__(self)
self.init_ui()
# Normalization toggle and sample size
self.fmod = fmod
self.sound = sound
self.enable_normalize = False
self.sample_size = 64
def init_ui(self):
self.picture = QtWidgets.QLabel(self)
self.picture.setScaledContents(True)
layout = QtWidgets.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.picture, 0, QtCore.Qt.AlignCenter)
self.setLayout(layout)
self.setWindowTitle("FMOD 3D Frequency Analysis")
self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed))
self.resize(1600, 350)
self.show()
def keyPressEvent(self, event):
key = event.key()
print(key)
# Toggle pause
if key == QtCore.Qt.Key_P:
self.sound.TogglePause()
# Toggle normalization
if key == QtCore.Qt.Key_N:
self.enable_normalize = not self.enable_normalize
# Decrease FFT sample size
if key == QtCore.Qt.Key_1:
self.sample_size = max(self.sample_size / 2, 64)
# Increase FFT sample size
if key == QtCore.Qt.Key_2:
self.sample_size = min(self.sample_size * 2, 8192)
def paintEvent(self, event):
qp = QtGui.QPainter()
qp.begin(self)
qp.fillRect(self.rect(), QtCore.Qt.black)
# Find frequency range of each array item
hz_range = (44100 / 2) / float(self.sample_size)
# Draw display
qp.setPen(QtCore.Qt.white)
qp.setFont(QtGui.QFont("Verdana", 8.))
qp.drawText(10, 10, "Press P to toggle pause, N to toggle normalize, 1 and 2 to adjust FFT size")
qp.drawText(10, 30, "Sample size: " + str(self.sample_size) + " - Range per sample: " + str(hz_range) + "Hz")
def draw_spectrum(title, channel, start_x):
# Get spectrum for the channel
spec = self.fmod.get_spectrum(self.sample_size, channel, FMOD_DSP_FFT_WINDOW_RECT)
# Find max volume
max_vol = max(spec)
# Normalize
if self.enable_normalize and max_vol != 0:
def normalize(db):
return db / float(max_vol)
spec = [normalize(db) for db in spec]
# Draw display
qp.setPen(QtCore.Qt.white)
qp.setFont(QtGui.QFont("Verdana", 8.))
qp.drawText(start_x + 10, 70, title)
qp.drawText(start_x + 10, 80, "Max vol this frame: " + str(max_vol).format("0.000"))
# Get painter dimensions
width = (self.rect().width() / 2)
height = self.rect().height()
# VU bars
block_gap = 4 / (self.sample_size / 64)
block_width = int((float(width) * 0.8) / float(self.sample_size) - block_gap)
block_max_height = 220
# Left-hand X co-ordinate of bar, left-hand Y co-ordinate of bar, width of bar, height of bar (negative to draw upwards), paintbrush to use
for b in range(self.sample_size - 1):
rect = QtCore.QRect(start_x + int(width * 0.1 + (block_width + block_gap) * b),
height - 20,
block_width,
int(-block_max_height * spec[b]))
gradient = QtGui.QLinearGradient(rect.topLeft(), rect.bottomRight()) # Diagonal gradient from top-left to bottom-right
gradient.setColorAt(0, QtCore.Qt.green)
gradient.setColorAt(1, QtCore.Qt.red)
qp.fillRect(rect, gradient)
# Draw the spectrums perceived by each ear
draw_spectrum("LEFT EAR", LEFT_CHANNEL, start_x=0)
draw_spectrum("RIGHT EAR", RIGHT_CHANNEL, start_x=self.rect().width() / 2)
qp.end()
def main():
def change_listener(listener):
current_listener.position = listener
fmod.update()
# FMOD initialization
fmod = pyfmodex.System()
fmod.init()
# Load the sound
sound1 = fmod.create_sound("sine.wav", mode=FMOD_LOOP_NORMAL | FMOD_3D | FMOD_SOFTWARE)
# Play the sound
channel = sound1.play()
channel.volume = 0.7
channel.min_distance = 50
channel.max_distance = 10000 # Need this for sound fall off
# Create listeners positions
listener1 = (0, 0, 0)
listener2 = (0, 10, 0)
# Create a listener in the center of the scene
current_listener = fmod.listener(id=0)
change_listener(listener1)
# Open the form
app = QtWidgets.QApplication(sys.argv)
fa = FrequencyAnalysis(fmod, sound1)
# Walk the sound around your head
global x, min_x, max_x, inc
min_x = -30
max_x = 30
sound_pos = (max_x, 3, 0)
x = min_x
inc = 1
def tick():
global x, min_x, max_x, inc
if x == min_x:
inc = 1
elif x == max_x:
inc = -1
x += inc
channel.position = [x, sound_pos[1], sound_pos[2]]
print("Playing at %r" % str(channel.position))
# Update FMOD
fmod.update()
fa.repaint()
timer = QtCore.QTimer()
timer.timeout.connect(tick)
timer.start(100)
app.exec_()
sys.exit()
if __name__ == "__main__":
main()