Examples
While we strive to keep the number of external dependencies required to run the examples below small (ideally: zero), many of the examples require the curses
library.
On Windows, this unfortunately requires the installation of an extra module, for example windows-curses.
Device detection
This is a sample script pretty printing the audio and recording devices detected by the FMOD Engine on your system.
1"""Sample code to list identification information about all sound devices
2(audio out and audio in) detected by FMOD Engine on a system.
3"""
4
5import re
6
7import pyfmodex
8from pyfmodex.enums import RESULT, SPEAKERMODE
9from pyfmodex.exceptions import FmodError
10from pyfmodex.flags import DRIVER_STATE
11
12system = pyfmodex.System()
13system.init()
14
15
16def _pp_driverinfo(driverinfo, indent=1):
17 """Pretty print driverinfo.
18
19 Lists all keys in the given pyfmodex.structobject with their values,
20 indented by the given number times four spaces.
21
22 .. todo:: Figure out how the GUID structure works exactly.
23 """
24 for key in driverinfo.keys():
25 value = driverinfo[key]
26 if isinstance(value, bytes):
27 value = value.decode()
28 elif isinstance(value, pyfmodex.structures.GUID):
29 continue
30 elif key == "system_rate":
31 value = f"{value} kHz"
32 elif key == "speaker_mode":
33 value = SPEAKERMODE(value).name
34 elif key == "state":
35 value = re.sub(r"^DRIVER_STATE.|\)$", "", str(DRIVER_STATE(value))).replace(
36 "|", ", "
37 )
38 print(4 * " " * indent, end="")
39 print(f"{key}: {value}")
40 print()
41
42
43def list_drivers(title, meth):
44 """List and prettyprint information about drivers returned by the given
45 method.
46 """
47 print(title)
48 print("-" * len(title))
49 counter = 0
50 while True:
51 try:
52 driverinfo = meth(counter)
53 except FmodError as fmoderr:
54 if fmoderr.result == RESULT.INVALID_PARAM:
55 break
56 raise fmoderr
57 print(f"Index {counter}:")
58 _pp_driverinfo(driverinfo)
59 counter += 1
60
61
62list_drivers("Detected audio OUT devices", system.get_driver_info)
63list_drivers("Detected audio IN devices", system.get_record_driver_info)
64
65system.release()
3D sound positioning
This is a sample script demonstrating the very basics of 3D sound positioning.
(Adapted from sample code shipped with FMOD Engine.)
1"""Sample code to show basic positioning of 3D sounds."""
2
3import curses
4import sys
5import time
6from math import sin
7
8import pyfmodex
9from pyfmodex.flags import MODE
10
11INTERFACE_UPDATETIME = 50
12DISTANCEFACTOR = 1
13MIN_FMOD_VERSION = 0x00020108
14
15# Create system object and initialize
16system = pyfmodex.System()
17VERSION = system.version
18if VERSION < MIN_FMOD_VERSION:
19 print(
20 f"FMOD lib version {VERSION:#08x} doesn't meet "
21 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
22 )
23 sys.exit(1)
24
25system.init(maxchannels=3)
26
27THREED_SETTINGS = system.threed_settings
28THREED_SETTINGS.distance_factor = DISTANCEFACTOR
29
30# Load some sounds
31sound1 = system.create_sound("media/drumloop.wav", mode=MODE.THREED)
32sound1.min_distance = 0.5 * DISTANCEFACTOR
33sound1.max_distance = 5000 * DISTANCEFACTOR
34sound1.mode = MODE.LOOP_NORMAL
35
36sound2 = system.create_sound("media/jaguar.wav", mode=MODE.THREED)
37sound2.min_distance = 0.5 * DISTANCEFACTOR
38sound2.max_distance = 5000 * DISTANCEFACTOR
39sound2.mode = MODE.LOOP_NORMAL
40
41sound3 = system.create_sound("media/swish.wav")
42
43# Play sounds at certain positions
44channel1 = system.play_sound(sound1, paused=True)
45channel1.position = (-10 * DISTANCEFACTOR, 0, 0)
46channel1.paused = False
47
48channel2 = system.play_sound(sound2, paused=True)
49channel2.position = (15 * DISTANCEFACTOR, 0, 0)
50channel2.paused = False
51
52# Main loop
53def main(stdscr):
54 """Draw a simple TUI, grab keypresses and let the user manipulate a simple
55 environment with a listener and some sounds.
56 """
57 listener = system.listener(0)
58 pos_ch1 = int((channel1.position[0]) / DISTANCEFACTOR) + 25
59 pos_ch2 = int((channel2.position[0]) / DISTANCEFACTOR) + 25
60
61 stdscr.clear()
62 stdscr.nodelay(True)
63
64 # Create small visual display
65 stdscr.addstr(
66 "===========\n"
67 "3D Example.\n"
68 "===========\n"
69 "\n"
70 "Press 1 to toggle sound 1 (16bit Mono 3D)\n"
71 "Press 2 to toggle sound 2 (8bit Mono 3D)\n"
72 "Press 3 to play a sound (16bit Stereo 2D)\n"
73 "Press h or l to move listener (when in still mode)\n"
74 "Press space to toggle listener still mode\n"
75 "Press q to quit"
76 )
77
78 listener_automove = True
79 listener_prevposx = 0
80 listener_velx = 0
81 clock = 0
82 while True:
83 tic = time.time()
84
85 listener_posx = listener.position[0]
86 environment = list("|" + 48 * "." + "|")
87 environment[pos_ch1 - 1 : pos_ch1 + 2] = list("<1>")
88 environment[pos_ch2 - 1 : pos_ch2 + 2] = list("<2>")
89 environment[int(listener_posx / DISTANCEFACTOR) + 25] = "L"
90
91 stdscr.addstr(11, 0, "".join(environment))
92 stdscr.addstr("\n")
93
94 # Listen to the user
95 try:
96 keypress = stdscr.getkey()
97 if keypress == "1":
98 channel1.paused = not channel1.paused
99 elif keypress == "2":
100 channel2.paused = not channel2.paused
101 elif keypress == "3":
102 system.play_sound(sound3)
103 elif keypress == " ":
104 listener_automove = not listener_automove
105 elif keypress == "q":
106 break
107
108 if not listener_automove:
109 if keypress == "h":
110 listener_posx = max(
111 -24 * DISTANCEFACTOR, listener_posx - DISTANCEFACTOR
112 )
113 elif keypress == "l":
114 listener_posx = min(
115 23 * DISTANCEFACTOR, listener_posx + DISTANCEFACTOR
116 )
117 except curses.error as cerr:
118 if cerr.args[0] != "no input":
119 raise cerr
120
121 # Update the listener
122 if listener_automove:
123 listener_posx = sin(clock * 0.05) * 24 * DISTANCEFACTOR
124 listener_velx = (listener_posx - listener_prevposx) * (
125 1000 / INTERFACE_UPDATETIME
126 )
127
128 listener.position = (listener_posx, 0, 0)
129 listener.velocity = (listener_velx, 0, 0)
130 listener_prevposx = listener_posx
131
132 clock += 30 * (1 / INTERFACE_UPDATETIME)
133 system.update()
134
135 toc = time.time()
136 time.sleep(max(0, INTERFACE_UPDATETIME / 1000 - (toc - tic)))
137
138
139curses.wrapper(main)
140
141# Shut down
142sound1.release()
143sound2.release()
144sound3.release()
145
146system.release()
Channel groups
This is sample script showing how to put channels into channel groups.
(Adapted from sample code shipped with FMOD Engine.)
1"""Sample code to show how to put channels into channel groups."""
2
3import curses
4import sys
5import time
6
7import pyfmodex
8from pyfmodex.flags import MODE
9
10MIN_FMOD_VERSION = 0x00020108
11
12# Create a System object and initialize
13system = pyfmodex.System()
14VERSION = system.version
15if VERSION < MIN_FMOD_VERSION:
16 print(
17 f"FMOD lib version {VERSION:#08x} doesn't meet "
18 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
19 )
20 sys.exit(1)
21
22system.init(maxchannels=6)
23
24
25# Load some sounds
26sounds = []
27sounds.append(system.create_sound("media/drumloop.wav", mode=MODE.LOOP_NORMAL))
28sounds.append(system.create_sound("media/jaguar.wav", mode=MODE.LOOP_NORMAL))
29sounds.append(system.create_sound("media/swish.wav", mode=MODE.LOOP_NORMAL))
30sounds.append(system.create_sound("media/c.ogg", mode=MODE.LOOP_NORMAL))
31sounds.append(system.create_sound("media/d.ogg", mode=MODE.LOOP_NORMAL))
32sounds.append(system.create_sound("media/e.ogg", mode=MODE.LOOP_NORMAL))
33
34group_a = system.create_channel_group("Group A")
35group_b = system.create_channel_group("Group B")
36group_master = system.master_channel_group
37
38# Instead of being independent, set the group A and B to be children of the
39# master group
40group_master.add_group(group_a)
41group_master.add_group(group_b)
42
43# Start all the sounds
44for idx, sound in enumerate(sounds):
45 system.play_sound(sound, channel_group=group_a if idx < 3 else group_b)
46
47# Change the volume of each group, just because we can! (reduce overall noise)
48group_a.volume = 0.5
49group_b.volume = 0.5
50
51# Main loop
52def main(stdscr):
53 """Draw a simple TUI, grab keypresses and let the user manipulate the
54 channel groups.
55 """
56 stdscr.clear()
57 stdscr.nodelay(True)
58
59 # Create small visual display
60 stdscr.addstr(
61 "=======================\n"
62 "Channel Groups Example.\n"
63 "=======================\n"
64 "\n"
65 "Group A : drumloop.wav, jaguar.wav, swish.wav\n"
66 "Group B : c.ogg, d.ogg, e.ogg\n"
67 "\n"
68 "Press a to mute/unmute group A\n"
69 "Press b to mute/unmute group B\n"
70 "Press m to mute/unmute master group\n"
71 "Press q to quit"
72 )
73
74 while True:
75 stdscr.addstr(
76 12, 0, f"Channels playing: {system.channels_playing['channels']}\n"
77 )
78
79 # Listen to the user
80 try:
81 keypress = stdscr.getkey()
82 if keypress == "a":
83 group_a.mute = not group_a.mute
84 elif keypress == "b":
85 group_b.mute = not group_b.mute
86 elif keypress == "m":
87 group_master.mute = not group_master.mute
88 elif keypress == "q":
89 break
90 except curses.error as cerr:
91 if cerr.args[0] != "no input":
92 raise cerr
93
94 system.update()
95 time.sleep(50 / 1000)
96
97 # A little fade out
98 if not (group_master.mute or group_a.mute and group_b.mute):
99 pitch = 1.0
100 volume = 1.0
101
102 fadeout_sec = 3
103 for _ in range(10 * fadeout_sec):
104 group_master.pitch = pitch
105 group_master.volume = volume
106
107 volume -= 1 / (10 * fadeout_sec)
108 pitch -= 0.25 / (10 * fadeout_sec)
109
110 system.update()
111 time.sleep(0.1)
112
113
114curses.wrapper(main)
115
116# Shut down
117for sound in sounds:
118 sound.release()
119
120group_a.release()
121group_b.release()
122
123system.release()
Convolution reverb
This is a sample script showing how to set up a convolution reverb DSP and work with it.
(Adapted from sample code shipped with FMOD Engine.)
1"""Sample code to demonstrate how to set up a convolution reverb DSP and work
2with it.
3"""
4
5import curses
6import sys
7import time
8from ctypes import c_short, sizeof
9
10import pyfmodex
11from pyfmodex.enums import (CHANNELCONTROL_DSP_INDEX, DSP_CONVOLUTION_REVERB,
12 DSP_TYPE, DSPCONNECTION_TYPE, SOUND_FORMAT,
13 TIMEUNIT)
14from pyfmodex.flags import MODE
15
16MIN_FMOD_VERSION = 0x00020108
17
18# Create a System object and initialize
19system = pyfmodex.System()
20VERSION = system.version
21if VERSION < MIN_FMOD_VERSION:
22 print(
23 f"FMOD lib version {VERSION:#08x} doesn't meet "
24 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
25 )
26 sys.exit(1)
27
28system.init(maxchannels=1)
29
30# Create a new channel group to hold the convolution DSP unit
31reverbgroup = system.create_channel_group("reverb")
32
33# Create a new channel group to hold all the channels and process the dry path
34maingroup = system.create_channel_group("main")
35
36# Create the convolution DSP unit and set it as the tail of the channel group
37reverbunit = system.create_dsp_by_type(DSP_TYPE.CONVOLUTIONREVERB)
38reverbgroup.add_dsp(CHANNELCONTROL_DSP_INDEX.TAIL, reverbunit)
39
40# Open the impulse response wav file, but use FMOD_OPENONLY as we want to read
41# the data into a seperate buffer
42irsound = system.create_sound("media/standrews.wav", mode=MODE.DEFAULT | MODE.OPENONLY)
43
44# For simplicity of the example, if the impulse response is the wrong format
45# just display an error
46if irsound.format.format != SOUND_FORMAT.PCM16:
47 print(
48 "Impulse Response file is the wrong audio format. It should be 16bit"
49 " integer PCM data."
50 )
51 sys.exit(1)
52
53# The reverb unit expects a block of data containing a single 16 bit int
54# containing the number of channels in the impulse response, followed by PCM 16
55# data
56short_size = sizeof(c_short)
57irsound_channels = irsound.format.channels
58irsound_data_length = irsound.get_length(TIMEUNIT.PCMBYTES)
59irdata = (c_short * (1 + irsound_data_length))()
60irsound_data = irsound.read_data(irsound_data_length)[0]
61
62irdata[0] = irsound_channels
63irdata[1:] = list(irsound_data)
64
65reverbunit.set_parameter_data(DSP_CONVOLUTION_REVERB.PARAM_IR, irdata)
66
67# Don't pass any dry signal from the reverb unit, instead take the dry part of
68# the mix from the main signal path
69reverbunit.set_parameter_float(DSP_CONVOLUTION_REVERB.PARAM_DRY, -80)
70
71# We can now release the sound object as the reverb unit has created its
72# internal data
73irsound.release()
74
75# Load up and play a sample clip recorded in an anechoic chamber
76sound = system.create_sound("media/singing.wav", mode=MODE.THREED | MODE.LOOP_NORMAL)
77channel = system.play_sound(sound, channel_group=maingroup, paused=True)
78
79# Create a send connection between the channel head and the reverb unit
80channel_head = channel.get_dsp(CHANNELCONTROL_DSP_INDEX.HEAD)
81reverb_connection = reverbunit.add_input(channel_head, DSPCONNECTION_TYPE.SEND)
82
83channel.paused = False
84
85# Main loop
86def main(stdscr):
87 """Draw a simple TUI, grab keypresses and let the user manipulate the
88 reverb connection.
89 """
90 wet_volume = 1
91 dry_volume = 1
92
93 stdscr.clear()
94 stdscr.nodelay(True)
95
96 # Create small visual display
97 stdscr.addstr(
98 "====================\n"
99 "Convolution Example.\n"
100 "====================\n"
101 "\n"
102 "Press k and j to change dry mix\n"
103 "Press h and l to change wet mix\n"
104 "Press q to quit"
105 )
106
107 while True:
108 stdscr.addstr(8, 0, f"wet mix [{wet_volume:.2f}] | dry mix [{dry_volume:.2f}]")
109
110 # Listen to the user
111 try:
112 keypress = stdscr.getkey()
113 if keypress == "h":
114 wet_volume = max(wet_volume - 0.05, 0)
115 elif keypress == "l":
116 wet_volume = min(wet_volume + 0.05, 1)
117 elif keypress == "j":
118 dry_volume = max(dry_volume - 0.05, 0)
119 elif keypress == "k":
120 dry_volume = min(dry_volume + 0.05, 1)
121 elif keypress == "q":
122 break
123 except curses.error as cerr:
124 if cerr.args[0] != "no input":
125 raise cerr
126
127 reverb_connection.mix = wet_volume
128 maingroup.volume = dry_volume
129
130 system.update()
131 time.sleep(50 / 1000)
132
133
134curses.wrapper(main)
135
136# Shut down
137sound.release()
138maingroup.release()
139reverbgroup.remove_dsp(reverbunit)
140reverbunit.disconnect_all(inputs=True, outputs=True)
141reverbunit.release()
142reverbgroup.release()
143system.release()
DSP effect per speaker
This is a sample script showing how to manipulate a DSP network and as an example, creating two DSP effects, splitting a single sound into two audio paths, which then gets filtered seperately.
To only have each audio path come out of one speaker each, set_mix_matrix()
is used just before the two branches merge back together again.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to demonstrate how to manipulate DSP network to have two
2different effects on seperately filtered, different audio paths from a single
3sound.
4"""
5
6import curses
7import sys
8import time
9
10import pyfmodex
11from pyfmodex.enums import (CHANNELCONTROL_DSP_INDEX, DSP_MULTIBAND_EQ,
12 DSP_MULTIBAND_EQ_FILTER_TYPE, DSP_TYPE,
13 SPEAKERMODE)
14from pyfmodex.flags import MODE
15from pyfmodex.structobject import Structobject
16
17MIN_FMOD_VERSION = 0x00020108
18
19# Create a System object and initialize
20system = pyfmodex.System()
21VERSION = system.version
22if VERSION < MIN_FMOD_VERSION:
23 print(
24 f"FMOD lib version {VERSION:#08x} doesn't meet "
25 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
26 )
27 sys.exit(1)
28
29# In this special case we want to use stereo output and not worry about varying
30# matrix sizes depending on user speaker mode
31software_format = Structobject(
32 sample_rate=48000, speaker_mode=SPEAKERMODE.STEREO, raw_speakers=0
33)
34system.software_format = software_format
35
36# Initialize FMOD
37system.init(maxchannels=1)
38
39sound = system.create_sound("media/drumloop.wav", mode=MODE.LOOP_NORMAL)
40channel = system.play_sound(sound)
41
42# Create the DSP effects
43dsplowpass = system.create_dsp_by_type(DSP_TYPE.MULTIBAND_EQ)
44dsplowpass.set_parameter_int(
45 DSP_MULTIBAND_EQ.A_FILTER, DSP_MULTIBAND_EQ_FILTER_TYPE.LOWPASS_24DB
46)
47dsplowpass.set_parameter_float(DSP_MULTIBAND_EQ.A_FREQUENCY, 1000)
48dsplowpass.set_parameter_float(DSP_MULTIBAND_EQ.A_Q, 4)
49
50dsphighpass = system.create_dsp_by_type(DSP_TYPE.MULTIBAND_EQ)
51dsphighpass.set_parameter_int(
52 DSP_MULTIBAND_EQ.A_FILTER, DSP_MULTIBAND_EQ_FILTER_TYPE.HIGHPASS_24DB
53)
54dsphighpass.set_parameter_float(DSP_MULTIBAND_EQ.A_FREQUENCY, 4000)
55dsphighpass.set_parameter_float(DSP_MULTIBAND_EQ.A_Q, 4)
56
57# Connect up the DSP network
58
59# When a sound is played, a subnetwork is set up in the DSP network which looks
60# like this (wavetable is the drumloop sound, and it feeds its data from right
61# to left):
62#
63# [DSPHEAD]<---[DSPCHANNELMIXER]<---[CHANNEL HEAD]<---[WAVETABLE - DRUMLOOP.WAV]
64group_master = system.master_channel_group
65dsphead = group_master.get_dsp(CHANNELCONTROL_DSP_INDEX.HEAD)
66dspchannelmixer, _ = dsphead.get_input(0)
67
68# Now disconnect channeldsp head from the wavetable to make it look like this:
69#
70# [DSPHEAD] [DSPCHANNELMIXER]<---[CHANNEL HEAD]<---[WAVETABLE - DRUMLOOP.WAV]
71dsphead.disconnect_from(dspchannelmixer)
72
73# Now connect the two effects to channeldsp head and store the two connections
74# this makes so we can set their matrix later
75
76# [DSPLOWPASS]
77# /x
78# [DSPHEAD] [DSPCHANNELMIXER]<---[CHANNEL HEAD]<---[WAVETABLE - DRUMLOOP.WAV]
79# \y
80# [DSPHIGHPASS]
81dsplowpassconnection = dsphead.add_input(dsplowpass) # x
82dsphighpassconnection = dsphead.add_input(dsphighpass) # y
83
84# Now connect the channelmixer to the 2 effects
85# [DSPLOWPASS]
86# /x \
87# [DSPHEAD] [DSPCHANNELMIXER]<---[CHANNEL HEAD]<---[WAVETABLE - DRUMLOOP.WAV]
88# \y /
89# [DSPHIGHPASS]
90
91dsplowpass.add_input(dspchannelmixer) # Ignore connection - we dont care about it.
92dsphighpass.add_input(dspchannelmixer) # Ignore connection - we dont care about it.
93
94# Now the drumloop will be twice as loud, because it is being split into 2,
95# then recombined at the end. What we really want is to only feed the
96# dsphead<-dsplowpass through the left speaker for that effect, and
97# dsphead<-dsphighpass to the right speaker for that effect. We can do that
98# simply by setting the pan, or speaker matrix of the connections
99
100# [DSPLOWPASS]
101# /x=1,0 \
102# [DSPHEAD] [DSPCHANNELMIXER]<---[CHANNEL HEAD]<---[WAVETABLE - DRUMLOOP.WAV]
103# \y=0,1 /
104# [DSPHIGHPASS]
105
106lowpassmatrix = [
107 1, 0, # output to front left: take front left input signal at 1
108 0, 0, # output to front right: silence
109]
110highpassmatrix = [
111 0, 0, # output to front left: silence
112 0, 1, # output to front right: take front right input signal at 1
113]
114
115# Upgrade the signal coming from the channel mixer from mono to stereo
116# Otherwise the lowpass and highpass will get mono signals
117dspchannelmixer.channel_format = Structobject(
118 channel_mask=0, num_channels=0, source_speaker_mode=SPEAKERMODE.STEREO
119)
120
121# Now set the above matrices
122dsplowpassconnection.set_mix_matrix(lowpassmatrix, 2, 2)
123dsphighpassconnection.set_mix_matrix(highpassmatrix, 2, 2)
124
125dsplowpass.bypass = True
126dsphighpass.bypass = True
127
128dsplowpass.active = True
129dsphighpass.active = True
130
131# Main loop
132def main(stdscr):
133 """Draw a simple TUI, grab keypresses and let the user manipulate the
134 DSP states.
135 """
136 pan = 0
137
138 stdscr.clear()
139 stdscr.nodelay(True)
140
141 # Create small visual display
142 stdscr.addstr(
143 "==============================\n"
144 "DSP Effect Per Speaker Sample.\n"
145 "==============================\n"
146 "\n"
147 "Press 1 to toggle lowpass (left speaker)\n"
148 "Press 2 to toggle highpass (right speaker)\n"
149 "Press h and l to pan sound\n"
150 "Press q to quit"
151 )
152
153 while True:
154 stdscr.addstr(
155 10,
156 0,
157 f"Lowpass (left) is {'inactive' if dsplowpass.bypass else 'active '}",
158 )
159 stdscr.addstr(
160 11,
161 0,
162 f"Highpass (right) is {'inactive' if dsphighpass.bypass else 'active '}",
163 )
164 stdscr.addstr(12, 0, f"Pan is {pan:.1f} ")
165
166 # Listen to the user
167 try:
168 keypress = stdscr.getkey()
169 if keypress == "1":
170 dsplowpass.bypass = not dsplowpass.bypass
171 elif keypress == "2":
172 dsphighpass.bypass = not dsphighpass.bypass
173 elif keypress == "h":
174 pan = max(pan - 0.1, -1)
175 channel.set_pan(pan)
176 elif keypress == "l":
177 pan = min(pan + 0.1, 1)
178 channel.set_pan(pan)
179 elif keypress == "q":
180 break
181 except curses.error as cerr:
182 if cerr.args[0] != "no input":
183 raise cerr
184
185 system.update()
186 time.sleep(50 / 1000)
187
188
189curses.wrapper(main)
190
191# Shut down
192sound.release()
193dsplowpass.release()
194dsphighpass.release()
195system.release()
Effects
This is a sample script showing how to apply some of the built in software effects to sounds by applying them to the master channel group.
All software sounds played here would be filtered in the same way.
To filter per channel, and not have other channels affected, simply aply the same function to the Channel
instead of the ChannelGroup
.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to apply some built in software effects to sounds.
2"""
3
4import curses
5import sys
6import time
7
8import pyfmodex
9from pyfmodex.enums import (DSP_MULTIBAND_EQ, DSP_MULTIBAND_EQ_FILTER_TYPE,
10 DSP_TYPE)
11
12MIN_FMOD_VERSION = 0x00020108
13
14# Create a System object and initialize.
15system = pyfmodex.System()
16VERSION = system.version
17if VERSION < MIN_FMOD_VERSION:
18 print(
19 f"FMOD lib version {VERSION:#08x} doesn't meet "
20 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
21 )
22 sys.exit(1)
23
24system.init(maxchannels=1)
25
26mastergroup = system.master_channel_group
27sound = system.create_sound("media/drumloop.wav")
28channel = system.play_sound(sound)
29
30# Create some effects to play with
31dsplowpass = system.create_dsp_by_type(DSP_TYPE.MULTIBAND_EQ)
32dsphighpass = system.create_dsp_by_type(DSP_TYPE.MULTIBAND_EQ)
33dspecho = system.create_dsp_by_type(DSP_TYPE.ECHO)
34dspflange = system.create_dsp_by_type(DSP_TYPE.FLANGE)
35
36# Configure multiband_eq DSPs to create lowpass and highpass filters
37dsplowpass.set_parameter_int(
38 DSP_MULTIBAND_EQ.A_FILTER, DSP_MULTIBAND_EQ_FILTER_TYPE.LOWPASS_24DB
39)
40dsplowpass.set_parameter_float(DSP_MULTIBAND_EQ.A_FREQUENCY, 1000)
41dsplowpass.set_parameter_float(DSP_MULTIBAND_EQ.A_Q, 4)
42
43dsphighpass.set_parameter_int(
44 DSP_MULTIBAND_EQ.A_FILTER, DSP_MULTIBAND_EQ_FILTER_TYPE.HIGHPASS_24DB
45)
46dsphighpass.set_parameter_float(DSP_MULTIBAND_EQ.A_FREQUENCY, 4000)
47dsphighpass.set_parameter_float(DSP_MULTIBAND_EQ.A_Q, 4)
48
49# Add them to the master channel group. Each time an effect is added (to
50# position 0) it pushes the others down the list.
51mastergroup.add_dsp(0, dsplowpass)
52mastergroup.add_dsp(0, dsphighpass)
53mastergroup.add_dsp(0, dspecho)
54mastergroup.add_dsp(0, dspflange)
55
56# By default, bypass all effects. This means let the original signal go
57# through without processing. It will sound 'dry' until effects are enabled by
58# the user.
59dsplowpass.bypass = True
60dsphighpass.bypass = True
61dspecho.bypass = True
62dspflange.bypass = True
63
64# Main loop
65def main(stdscr):
66 """Draw a simple TUI, grab keypresses and let the user manipulate the
67 DSP states.
68 """
69 stdscr.clear()
70 stdscr.nodelay(True)
71
72 # Create small visual display
73 stdscr.addstr(
74 "================\n"
75 "Effects Example.\n"
76 "================\n"
77 "\n"
78 "Press SPACE to pause/unpause sound\n"
79 "Press 1 to toggle dsplowpass effect\n"
80 "Press 2 to toggle dsphighpass effect\n"
81 "Press 3 to toggle dspecho effect\n"
82 "Press 4 to toggle dspflange effect\n"
83 "Press q to quit"
84 )
85
86 while True:
87 stdscr.addstr(
88 11,
89 0,
90 "%-8s: lowpass[%s] highpass[%s] echo [%s] flange[%s]"
91 % (
92 "Paused" if channel.paused else "Playing",
93 " " if dsplowpass.bypass else "x",
94 " " if dsphighpass.bypass else "x",
95 " " if dspecho.bypass else "x",
96 " " if dspflange.bypass else "x",
97 ),
98 )
99
100 # Listen to the user
101 try:
102 keypress = stdscr.getkey()
103 if keypress == " ":
104 channel.paused = not channel.paused
105 elif keypress == "1":
106 dsplowpass.bypass = not dsplowpass.bypass
107 elif keypress == "2":
108 dsphighpass.bypass = not dsphighpass.bypass
109 elif keypress == "3":
110 dspecho.bypass = not dspecho.bypass
111 elif keypress == "4":
112 dspflange.bypass = not dspflange.bypass
113 elif keypress == "q":
114 break
115 except curses.error as cerr:
116 if cerr.args[0] != "no input":
117 raise cerr
118
119 system.update()
120 time.sleep(50 / 1000)
121
122
123curses.wrapper(main)
124
125# Shut down
126mastergroup.remove_dsp(dsplowpass)
127mastergroup.remove_dsp(dsphighpass)
128mastergroup.remove_dsp(dspecho)
129mastergroup.remove_dsp(dspflange)
130
131dsplowpass.release()
132dsphighpass.release()
133dspecho.release()
134dspflange.release()
135
136sound.release()
137system.release()
Gapless playback
This is a sample script showing how to schedule channel playback into the future with sample accuracy. It uses several scheduled channels to synchronize two or more sounds.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to schedule channel playback into the future with
2sample accuracy. Uses several scheduled channels to synchronize two or more
3sounds.
4"""
5
6import curses
7import sys
8import time
9from enum import IntEnum
10
11import pyfmodex
12from pyfmodex.enums import TIMEUNIT
13from pyfmodex.structobject import Structobject
14
15MIN_FMOD_VERSION = 0x00020108
16
17
18# pylint: disable=too-few-public-methods
19class Note(IntEnum):
20 """The notes we need to play our song."""
21
22 C = 0
23 D = 1
24 E = 2
25
26
27SONG = [
28 Note.E, # Ma-
29 Note.D, # ry
30 Note.C, # had
31 Note.D, # a
32 Note.E, # lit-
33 Note.E, # tle
34 Note.E, # lamb,
35 Note.E, # .....
36 Note.D, # lit-
37 Note.D, # tle
38 Note.D, # lamb,
39 Note.D, # .....
40 Note.E, # lit-
41 Note.E, # tle
42 Note.E, # lamb,
43 Note.E, # .....
44 Note.E, # Ma-
45 Note.D, # ry
46 Note.C, # had
47 Note.D, # a
48 Note.E, # lit-
49 Note.E, # tle
50 Note.E, # lamb,
51 Note.E, # its
52 Note.D, # fleece
53 Note.D, # was
54 Note.E, # white
55 Note.D, # as
56 Note.C, # snow.
57 Note.C, # .....
58 Note.C, # .....
59 Note.C, # .....
60]
61
62# Create a System object and initialize.
63system = pyfmodex.System()
64VERSION = system.version
65if VERSION < MIN_FMOD_VERSION:
66 print(
67 f"FMOD lib version {VERSION:#08x} doesn't meet "
68 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
69 )
70 sys.exit(1)
71
72system.init(maxchannels=len(SONG))
73
74# Get information needed later for scheduling: the mixer block size, and the
75# output rate of the mixer
76
77dsp_block_len = system.dsp_buffer_size.size
78output_rate = system.software_format.sample_rate
79
80# Load our sounds - these are just sine wave tones at different frequencies.
81sounds = [None] * len(Note)
82sounds[Note.C] = system.create_sound("media/c.ogg")
83sounds[Note.D] = system.create_sound("media/d.ogg")
84sounds[Note.E] = system.create_sound("media/e.ogg")
85
86# Create a channelgroup that the channels will play on. We can use this
87# channelgroup as our clock reference. It also means we can pause and pitch
88# bend the channelgroup, without affecting the offsets of the delays, because
89# the channelgroup clock which the channels feed off, will be pausing and
90# speeding up/slowing down and still keeping the children in sync.
91channelgroup = system.create_channel_group("Parent")
92
93# Play all the sounds at once! Space them apart with set delay though so that
94# they sound like they play in order.
95CLOCK_START = 0
96for note in SONG:
97
98 # Pick a note from our tune
99 sound = sounds[note]
100
101 # Play the sound on the channelgroup we want to use as the parent clock
102 # reference (for `delay` further down)
103 channel = system.play_sound(sound, channelgroup, paused=True)
104
105 if not CLOCK_START:
106 CLOCK_START = channel.dsp_clock.parent_clock
107
108 # Start the sound into the future, by two mixer blocks worth. Should be
109 # enough to avoid the mixer catching up and hitting the clock value
110 # before we've finished setting up everything. Alternatively the
111 # channelgroup we're basing the clock on could be paused to stop it
112 # ticking.
113 CLOCK_START += dsp_block_len * 2
114 else:
115 # Get the length of the sound in samples
116 sound_len = sound.get_length(TIMEUNIT.PCM)
117
118 # Get the default frequency that the sound was recorded at
119 freq = sound.default_frequency
120
121 # Convert the length of the sound to 'output samples' for the output
122 # timeline
123 sound_len = int(sound_len / freq * output_rate)
124
125 # Place the sound clock start time to this value after the last one
126 CLOCK_START += sound_len
127
128 # Schedule the channel to start in the future at the newly calculated
129 # channelgroup clock value
130 channel.delay = Structobject(dsp_start=CLOCK_START, dsp_end=0, stop_channels=False)
131
132 # Unpause the sound. Note that you won't hear the sounds, they are
133 # scheduled into the future.
134 channel.paused = False
135
136# Main loop
137def main(stdscr):
138 """Draw a simple TUI, grab keypresses and let the user manipulate the
139 channel parameters.
140 """
141 stdscr.clear()
142 stdscr.nodelay(True)
143
144 # Create small visual display
145 stdscr.addstr(
146 "=========================\n"
147 "Gapless Playback example.\n"
148 "=========================\n"
149 "\n"
150 "Press SPACE to toggle pause\n"
151 "Press k to increase pitch\n"
152 "Press j to decrease pitch\n"
153 "Press q to quit"
154 )
155
156 while True:
157 paused_state = "Paused" if channelgroup.paused else "Playing"
158
159 stdscr.move(9, 0)
160 stdscr.clrtoeol()
161 stdscr.addstr(
162 f"Channels Playing {system.channels_playing.channels} : {paused_state}"
163 )
164
165 # Listen to the user
166 try:
167 keypress = stdscr.getkey()
168 if keypress == " ":
169 # Pausing the channelgroup, as the clock parent will pause any
170 # scheduled sounds from continuing. If you paused the channel,
171 # this would not stop the clock it is delayed against from
172 # ticking, and you'd have to recalculate the delay for the
173 # channel into the future again before it was unpaused.
174 channelgroup.paused = not channelgroup.paused
175 elif keypress == "k":
176 for _ in range(50):
177 channelgroup.pitch += 0.01
178 system.update()
179 time.sleep(10 / 1000)
180 elif keypress == "j":
181 for _ in range(50):
182 if channelgroup.pitch > 0.1:
183 channelgroup.pitch -= 0.01
184 system.update()
185 time.sleep(10 / 1000)
186 elif keypress == "q":
187 break
188 except curses.error as cerr:
189 if cerr.args[0] != "no input":
190 raise cerr
191
192 system.update()
193 time.sleep(50 / 1000)
194
195
196curses.wrapper(main)
197
198# Shut down
199for sound in sounds:
200 sound.release()
201system.release()
Generate tone
This is a sample script showing how to play generated tones using play_dsp()
instead of manually connecting and disconnecting DSP units.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to play generated tones using System.play_dsp
2instead of manually connecting and disconnecting DSP units.
3"""
4
5import curses
6import sys
7import time
8
9import pyfmodex
10from pyfmodex.enums import DSP_OSCILLATOR, DSP_TYPE
11
12MIN_FMOD_VERSION = 0x00020108
13
14# Create a System object and initialize
15system = pyfmodex.System()
16VERSION = system.version
17if VERSION < MIN_FMOD_VERSION:
18 print(
19 f"FMOD lib version {VERSION:#08x} doesn't meet "
20 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
21 )
22 sys.exit(1)
23
24system.init(maxchannels=1)
25
26# Create an oscillator DSP unit for the tone
27dsp = system.create_dsp_by_type(DSP_TYPE.OSCILLATOR)
28dsp.set_parameter_float(DSP_OSCILLATOR.RATE, 440) # Musical note 'A'
29
30# Main loop
31def main(stdscr):
32 """Draw a simple TUI, grab keypresses and let the user manipulate the
33 DSP parameters.
34 """
35 stdscr.clear()
36 stdscr.nodelay(True)
37
38 # Create small visual display
39 stdscr.addstr(
40 "======================\n"
41 "Generate Tone Example.\n"
42 "======================\n"
43 "\n"
44 "Press 1 to play a sine wave\n"
45 "Press 2 to play a sqaure wave\n"
46 "Press 3 to play a saw wave\n"
47 "Press 4 to play a triangle wave\n"
48 "Press SPACE to stop the channel\n"
49 "Press q to quit\n"
50 "Press k and j to change volume\n"
51 "Press h and l to change frequency"
52 )
53
54 channel = None
55 while True:
56 if channel:
57 playing = "playing" if channel.is_playing else "stopped"
58 volume = channel.volume
59 frequency = channel.frequency
60 else:
61 playing = "stopped"
62 volume = 0
63 frequency = 0
64
65 stdscr.move(13, 0)
66 stdscr.clrtoeol()
67 stdscr.addstr(f"Channel is {playing}")
68
69 stdscr.move(14, 0)
70 stdscr.clrtoeol()
71 stdscr.addstr(f"Volume {volume:.2f}")
72
73 stdscr.move(15, 0)
74 stdscr.clrtoeol()
75 stdscr.addstr(f"Frequency {frequency}")
76
77 # Listen to the user
78 try:
79 keypress = stdscr.getkey()
80 if keypress == "1":
81 if channel:
82 channel.stop()
83 channel = system.play_dsp(dsp, paused=True)
84 channel.volume = 0.5
85 dsp.set_parameter_int(DSP_OSCILLATOR.TYPE, 0)
86 channel.paused = False
87 elif keypress == "2":
88 if channel:
89 channel.stop()
90 channel = system.play_dsp(dsp, paused=True)
91 channel.volume = 0.125
92 dsp.set_parameter_int(DSP_OSCILLATOR.TYPE, 1)
93 channel.paused = False
94 elif keypress == "3":
95 if channel:
96 channel.stop()
97 channel = system.play_dsp(dsp, paused=True)
98 channel.volume = 0.125
99 dsp.set_parameter_int(DSP_OSCILLATOR.TYPE, 2)
100 channel.paused = False
101 elif keypress == "4":
102 if channel:
103 channel.stop()
104 channel = system.play_dsp(dsp, paused=True)
105 channel.volume = 0.5
106 dsp.set_parameter_int(DSP_OSCILLATOR.TYPE, 4)
107 channel.paused = False
108 elif keypress == " ":
109 if channel:
110 channel.stop()
111 channel = None
112 elif keypress == "q":
113 break
114
115 if not channel:
116 raise curses.error("no input")
117
118 if keypress == "h":
119 channel.frequency = max(channel.frequency - 500, 0)
120 elif keypress == "j":
121 channel.volume = max(channel.volume - 0.1, 0)
122 elif keypress == "k":
123 channel.volume = min(channel.volume + 0.1, 1)
124 elif keypress == "l":
125 channel.frequency = channel.frequency + 500
126 except curses.error as cerr:
127 if cerr.args[0] != "no input":
128 raise cerr
129
130 system.update()
131 time.sleep(50 / 1000)
132
133
134curses.wrapper(main)
135
136# Shut down
137dsp.release()
138system.release()
Granular synthesis
This is a sample script showing how to play a string of sounds together without gaps, using delay
to produce a granular synthesis style truck engine effect.
The basic operation is:
Play two sounds initially at the same time, the first sound immediately, and the second sound with a delay calculated by the length of the first sound.
Set delay to initiate the delayed playback. The delay is sample accurate and uses output samples as the time frame, not source samples. These samples are a fixed amount per second regardless of the source sound format, for example 48000 samples per second if FMOD is initialized to 48khz output.
Output samples are calculated from source samples with a simple source-to-output sample rate conversion.
When the first sound finishes, the second one should have automatically started. This is a good oppurtunity to queue up the next sound. Repeat step two.
Make sure the framerate is high enough to queue up a new sound before the other one finishes otherwise you will get gaps.
These sounds are not limited by format, channel count or bit depth and can also be modified to allow for overlap, by reducing the delay from the first sound playing to the second by the overlap amount.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to play a string of sounds together without gaps,
2using `delay`, to produce a granular synthesis style trick engine effect.
3"""
4
5import curses
6import random
7import sys
8import time
9
10import pyfmodex
11from pyfmodex.enums import RESULT, TIMEUNIT
12from pyfmodex.exceptions import FmodError
13from pyfmodex.flags import MODE
14from pyfmodex.structobject import Structobject
15
16MIN_FMOD_VERSION = 0x00020108
17
18soundnames = (
19 "media/granular/truck_idle_off_01.wav",
20 "media/granular/truck_idle_off_02.wav",
21 "media/granular/truck_idle_off_03.wav",
22 "media/granular/truck_idle_off_04.wav",
23 "media/granular/truck_idle_off_05.wav",
24 "media/granular/truck_idle_off_06.wav",
25)
26
27# Create a System object and initialize.
28system = pyfmodex.System()
29VERSION = system.version
30if VERSION < MIN_FMOD_VERSION:
31 print(
32 f"FMOD lib version {VERSION:#08x} doesn't meet "
33 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
34 )
35 sys.exit(1)
36
37system.init(maxchannels=2)
38
39output_rate = system.software_format.sample_rate
40dsp_block_len = system.dsp_buffer_size.size
41master_channel_group = system.master_channel_group
42
43sounds = []
44for soundname in soundnames:
45 sounds.append(system.create_sound(soundname, mode=MODE.IGNORETAGS))
46
47
48def queue_next_sound(playingchannel=None):
49 """Queue the next sound."""
50 newsound = sounds[random.randrange(0, len(sounds))]
51 newchannel = system.play_sound(newsound, paused=True)
52
53 start_delay = 0
54 if playingchannel:
55 # Get the start time of the playing channel
56 start_delay = playingchannel.delay.dsp_start
57
58 # Grab the length of the playing sound, and its frequency, so we can
59 # calculate where to place the new sound on the time line
60 sound_len = playingchannel.current_sound.get_length(TIMEUNIT.PCM)
61 freq = playingchannel.frequency
62
63 # Now calculate the length of the sound in 'output samples'. For
64 # instance, if a 44khz sound is 22050 samples long, and the output
65 # rate is 48khz, then we want to delay by 24000 output samples
66 sound_len = int(sound_len / freq * output_rate)
67
68 # Add output rate adjusted sound length to the clock value of the
69 # sound that is currently playing
70 start_delay += sound_len
71 else:
72 start_delay = newchannel.dsp_clock.parent_clock
73 start_delay += 2 * dsp_block_len
74
75 # Set the delay of the new sound to the end of the old sound
76 newchannel.delay = Structobject(
77 dsp_start=start_delay, dsp_end=0, stop_channels=False
78 )
79
80 # Randomize pitch/volume to make it sound more realistic / random
81 newchannel.frequency *= (
82 1 + random.uniform(-1, 1) * .02
83 ) # @22khz, range fluctuates from 21509 to 22491
84
85 newchannel.volume *= 1 - random.random() * 0.2 # 0.8 to 1.0
86
87 newchannel.paused = False
88
89 return newchannel
90
91
92# Main loop
93def main(stdscr):
94 """Draw a simple TUI, grab keypresses and let the user manipulate the
95 channel paused state.
96 """
97 stdscr.clear()
98 stdscr.nodelay(True)
99
100 # Create small visual display
101 stdscr.addstr(
102 "====================================\n"
103 "Granular Synthesis SetDelay Example.\n"
104 "====================================\n"
105 "\n"
106 "Press SPACE to toggle pause\n"
107 "Press q to quit"
108 )
109
110 # Kick off the first two sounds. First one is immediate, second one will be
111 # triggered to start after the first one.
112 channels = []
113 channels.append(queue_next_sound())
114 channels.append(queue_next_sound(channels[0]))
115
116 slot = 0
117 while True:
118 paused_state = "paused" if master_channel_group.paused else "playing"
119
120 stdscr.move(7, 0)
121 stdscr.clrtoeol()
122 stdscr.addstr(f"Channels are {paused_state}")
123
124 # Replace the sound that just finished with a new sound, to create
125 # endless seamless stitching!
126 try:
127 is_playing = channels[slot].is_playing
128 except FmodError as fmoderror:
129 if fmoderror.result != RESULT.INVALID_HANDLE:
130 raise fmoderror
131
132 if not is_playing and not master_channel_group.paused:
133 # Replace sound that just ended with a new sound, queued up to
134 # trigger exactly after the other sound ends
135 channels[slot] = queue_next_sound(channels[1 - slot])
136 slot = 1 - slot # flip
137
138 # Listen to the user
139 try:
140 keypress = stdscr.getkey()
141 if keypress == " ":
142 master_channel_group.paused = not master_channel_group.paused
143 elif keypress == "q":
144 break
145 except curses.error as cerr:
146 if cerr.args[0] != "no input":
147 raise cerr
148
149 system.update()
150 # If you wait too long (longer than the length of the shortest sound),
151 # you will get gaps.
152 time.sleep(10 / 1000)
153
154
155curses.wrapper(main)
156
157# Shut down
158for sound in sounds:
159 sound.release()
160system.release()
Load from memory
This is a sample script showing how to use the OPENMEMORY
mode flag whe creating sounds to load the data into memory.
This example is simply a variant of the Play sound example, but it loads the data into memory and then uses the load from memory feature of create_sound()
.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to load data into memory and read it from there."""
2
3import curses
4import mmap
5import sys
6import time
7from pathlib import Path
8
9import pyfmodex
10from pyfmodex.enums import RESULT, TIMEUNIT
11from pyfmodex.exceptions import FmodError
12from pyfmodex.flags import MODE
13from pyfmodex.structures import CREATESOUNDEXINFO
14
15MIN_FMOD_VERSION = 0x00020108
16
17mediadir = Path("media")
18soundnames = (
19 mediadir / "drumloop.wav",
20 mediadir / "jaguar.wav",
21 mediadir / "swish.wav",
22)
23
24# Create a System object and initialize
25system = pyfmodex.System()
26VERSION = system.version
27if VERSION < MIN_FMOD_VERSION:
28 print(
29 f"FMOD lib version {VERSION:#08x} doesn't meet "
30 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
31 )
32 sys.exit(1)
33
34system.init()
35
36sounds = []
37for filename in soundnames:
38 with open(filename, mode="rb") as file_obj:
39 with mmap.mmap(
40 file_obj.fileno(), length=0, access=mmap.ACCESS_READ
41 ) as mmap_obj:
42 sounds.append(
43 system.create_sound(
44 mmap_obj.read(),
45 mode=MODE.OPENMEMORY | MODE.LOOP_OFF,
46 exinfo=CREATESOUNDEXINFO(length=mmap_obj.size()),
47 )
48 )
49
50# Main loop
51def main(stdscr):
52 """Draw a simple TUI, grab keypresses and let the user play the sounds."""
53 stdscr.clear()
54 stdscr.nodelay(True)
55
56 # Create small visual display
57 stdscr.addstr(
58 "=========================\n"
59 "Load From Memory Example.\n"
60 "=========================\n"
61 "\n"
62 f"Press 1 to play a mono sound ({soundnames[0].stem})\n"
63 f"Press 2 to play a mono sound ({soundnames[1].stem})\n"
64 f"Press 3 to play a stero sound ({soundnames[2].stem})\n"
65 "Press q to quit"
66 )
67
68 channel = None
69 currentsound = None
70 while True:
71 is_playing = False
72 position = 0
73 length = 0
74 if channel:
75 try:
76 is_playing = channel.is_playing
77 position = channel.get_position(TIMEUNIT.MS)
78 currentsound = channel.current_sound
79 if currentsound:
80 length = currentsound.get_length(TIMEUNIT.MS)
81
82 except FmodError as fmoderror:
83 if fmoderror.result not in (
84 RESULT.INVALID_HANDLE,
85 RESULT.CHANNEL_STOLEN,
86 ):
87 raise fmoderror
88
89 stdscr.move(9, 0)
90 stdscr.clrtoeol()
91 stdscr.addstr(
92 "Time %02d:%02d:%02d/%02d:%02d:%02d : %s"
93 % (
94 position / 1000 / 60,
95 position / 1000 % 60,
96 position / 10 % 100,
97 length / 1000 / 60,
98 length / 1000 % 60,
99 length / 10 % 100,
100 "Playing" if is_playing else "Stopped",
101 ),
102 )
103 stdscr.addstr(10, 0, f"Channel Playing {system.channels_playing.channels:-2d}")
104
105 # Listen to the user
106 try:
107 keypress = stdscr.getkey()
108 if keypress in "123":
109 channel = system.play_sound(sounds[int(keypress) - 1])
110 elif keypress == "q":
111 break
112 except curses.error as cerr:
113 if cerr.args[0] != "no input":
114 raise cerr
115
116 system.update()
117 time.sleep(50 / 1000)
118
119
120curses.wrapper(main)
121
122# Shut down
123for sound in sounds:
124 sound.release()
125system.release()
Multiple speakers
This is a sample script showing how to play sounds on multiple speakers, and also how to assign sound subchannels (like in stereo sound) to different, individual speakers.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to play sounds on multiple speakers."""
2
3import curses
4import sys
5import time
6
7import pyfmodex
8from pyfmodex.enums import RESULT, SPEAKERMODE, TIMEUNIT
9from pyfmodex.exceptions import FmodError
10from pyfmodex.flags import MODE
11
12MIN_FMOD_VERSION = 0x00020108
13
14CHOICES = (
15 "Mono from front left speaker",
16 "Mono from front right speaker",
17 "Mono from center speaker",
18 "Mono from surround left speaker",
19 "Mono from surround right speaker",
20 "Mono from rear left speaker",
21 "Mono from rear right speaker",
22 "Stereo from front speakers",
23 "Stereo from front speakers (channels swapped)",
24 "Stereo (right only) from center speaker",
25)
26
27# Create a System object and initialize
28system = pyfmodex.System()
29VERSION = system.version
30if VERSION < MIN_FMOD_VERSION:
31 print(
32 f"FMOD lib version {VERSION:#08x} doesn't meet "
33 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
34 )
35 sys.exit(1)
36
37system.init(maxchannels=len(CHOICES))
38
39speaker_mode = SPEAKERMODE(system.software_format.speaker_mode)
40
41sound_mono = system.create_sound("media/drumloop.wav", mode=MODE.TWOD | MODE.LOOP_OFF)
42sound_stereo = system.create_sound("media/stereo.ogg", mode=MODE.TWOD | MODE.LOOP_OFF)
43
44
45def is_choice_available(choice_idx):
46 """Is the given configuration choice available in the current speakermode?"""
47 if speaker_mode in (SPEAKERMODE.MONO, SPEAKERMODE.STEREO):
48 return choice_idx not in (2, 3, 4, 5, 6, 9)
49 if speaker_mode == SPEAKERMODE.QUAD:
50 return choice_idx not in (2, 5, 6, 9)
51 if speaker_mode in (SPEAKERMODE.SURROUND, SPEAKERMODE.FIVEPOINTONE):
52 return choice_idx not in (5, 6)
53
54 return True
55
56
57def play_sound(choice_idx):
58 """Play a sound in the given configuration choice.
59
60 Returns the created channel.
61 """
62 channel = None
63 if choice_idx == 0: # Mono front left
64 channel = system.play_sound(sound_mono, paused=True)
65 channel.set_mix_levels_output(1, 0, 0, 0, 0, 0, 0, 0)
66 channel.paused = False
67 elif choice_idx == 1: # Mono front right
68 channel = system.play_sound(sound_mono, paused=True)
69 channel.set_mix_levels_output(0, 1, 0, 0, 0, 0, 0, 0)
70 channel.paused = False
71 elif choice_idx == 2: # Mono centre
72 channel = system.play_sound(sound_mono, paused=True)
73 channel.set_mix_levels_output(0, 0, 1, 0, 0, 0, 0, 0)
74 channel.paused = False
75 elif choice_idx == 3: # Mono surround left
76 channel = system.play_sound(sound_mono, paused=True)
77 channel.set_mix_levels_output(0, 0, 0, 0, 1, 0, 0, 0)
78 channel.paused = False
79 elif choice_idx == 4: # Mono surround right
80 channel = system.play_sound(sound_mono, paused=True)
81 channel.set_mix_levels_output(0, 0, 0, 0, 0, 1, 0, 0)
82 channel.paused = False
83 elif choice_idx == 5: # Mono read left
84 channel = system.play_sound(sound_mono, paused=True)
85 channel.set_mix_levels_output(0, 0, 0, 0, 0, 0, 1, 0)
86 channel.paused = False
87 elif choice_idx == 6: # Mono read right
88 channel = system.play_sound(sound_mono, paused=True)
89 channel.set_mix_levels_output(0, 0, 0, 0, 0, 0, 0, 1)
90 channel.paused = False
91 elif choice_idx == 7: # Stereo format
92 channel = system.play_sound(sound_stereo)
93 elif choice_idx == 8: # Stereo front channel swapped
94 matrix = [0, 1,
95 1, 0]
96 channel = system.play_sound(sound_stereo, paused=True)
97 channel.set_mix_matrix(matrix, 2, 2)
98 channel.paused = False
99 elif choice_idx == 8: # Stereo (right only) center
100 matrix = [0, 0,
101 0, 0,
102 0, 1]
103 channel = system.play_sound(sound_stereo, paused=True)
104 channel.set_mix_matrix(matrix, 3, 2)
105 channel.paused = False
106 return channel
107
108
109# Main loop
110def main(stdscr):
111 """Draw a simple TUI, grab keypresses and let the user play the sounds."""
112 stdscr.clear()
113 stdscr.nodelay(True)
114
115 # Create small visual display
116 all_opts = speaker_mode.value >= SPEAKERMODE.SEVENPOINTONE.value
117 stdscr.addstr(
118 "=========================\n"
119 "Multiple Speaker Example.\n"
120 "=========================\n"
121 "\n"
122 "Press j or k to select mode\n"
123 "Press SPACE to play the sound\n"
124 "Press q to quit\n"
125 "\n"
126 f"Speaker mode is set to {speaker_mode.name}"
127 " causing some speaker options to be unavailale"
128 if not all_opts
129 else ""
130 )
131
132 channel = None
133 currentsound = None
134 choice_idx = 0
135 while True:
136 stdscr.move(10, 0)
137 for idx, choice in enumerate(CHOICES):
138 available = is_choice_available(idx)
139 sel = "-" if not available else "X" if choice_idx == idx else " "
140 stdscr.addstr(f"[{sel}] {choice}\n")
141
142 is_playing = False
143 position = 0
144 length = 0
145 if channel:
146 try:
147 is_playing = channel.is_playing
148 position = channel.get_position(TIMEUNIT.MS)
149 currentsound = channel.current_sound
150 if currentsound:
151 length = currentsound.get_length(TIMEUNIT.MS)
152
153 except FmodError as fmoderror:
154 if fmoderror.result not in (
155 RESULT.INVALID_HANDLE,
156 RESULT.CHANNEL_STOLEN,
157 ):
158 raise fmoderror
159
160 stdscr.move(11 + len(CHOICES), 0)
161 stdscr.clrtoeol()
162 stdscr.addstr(
163 "Time %02d:%02d:%02d/%02d:%02d:%02d : %s\n"
164 % (
165 position / 1000 / 60,
166 position / 1000 % 60,
167 position / 10 % 100,
168 length / 1000 / 60,
169 length / 1000 % 60,
170 length / 10 % 100,
171 "Playing" if is_playing else "Stopped",
172 ),
173 )
174 stdscr.addstr(f"Channels playing: {system.channels_playing.channels:-2d}")
175
176 # Listen to the user
177 try:
178 keypress = stdscr.getkey()
179 if keypress == "k":
180 old_idx = choice_idx
181 while True:
182 choice_idx = max(choice_idx - 1, 0)
183 if is_choice_available(choice_idx):
184 break
185 if choice_idx == 0:
186 choice_idx = old_idx
187 break
188 elif keypress == "j":
189 old_idx = choice_idx
190 while True:
191 choice_idx = min(choice_idx + 1, len(CHOICES) - 1)
192 if is_choice_available(choice_idx):
193 break
194 if choice_idx == len(CHOICES) - 1:
195 choice_idx = old_idx
196 break
197 elif keypress == " ":
198 channel = play_sound(choice_idx)
199 elif keypress == "q":
200 break
201 except curses.error as cerr:
202 if cerr.args[0] != "no input":
203 raise cerr
204
205 system.update()
206 time.sleep(50 / 1000)
207
208
209curses.wrapper(main)
210
211# Shut down
212sound_mono.release()
213sound_stereo.release()
214system.release()
Multiple systems
This example shows how to play sounds on two different output devices from the same application. It creates two System
objects, selects a different sound device for each, then allows the user to play one sound on each device.
Note that sounds created on device A cannot be played on device B and vice versa.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to play sounds on two diferent output devices from
2the same application.
3"""
4
5import curses
6import sys
7import time
8
9import pyfmodex
10from pyfmodex.enums import OUTPUTTYPE
11from pyfmodex.flags import MODE
12
13MIN_FMOD_VERSION = 0x00020108
14
15
16def fetch_driver(stdscr, system, name=""):
17 """Draw a simple TUI, grab keypresses and let the user select a driver."""
18 num_drivers = system.num_drivers
19 if not num_drivers:
20 system.output = OUTPUTTYPE.NOSOUND
21 return 0
22
23 selected_idx = 0
24 drivers = [system.get_driver_info(idx).name.decode() for idx in range(num_drivers)]
25 while True:
26 stdscr.addstr(4, 0, "Choose a device for system ")
27 stdscr.addstr(name, curses.A_BOLD)
28 stdscr.addstr(
29 "\n"
30 "\n"
31 "Use j and k to select\n"
32 "Press SPACE to confirm\n"
33 "\n"
34 )
35
36 for idx in range(num_drivers):
37 sel = "X" if selected_idx == idx else " "
38 stdscr.addstr(f"[{sel}] - {idx}. {drivers[idx]}\n")
39
40 # Listen to the user
41 try:
42 keypress = stdscr.getkey()
43 if keypress == "k":
44 selected_idx = max(selected_idx - 1, 0)
45 elif keypress == "j":
46 selected_idx = min(selected_idx + 1, num_drivers - 1)
47 elif keypress == " ":
48 return selected_idx
49 except curses.error as cerr:
50 if cerr.args[0] != "no input":
51 raise cerr
52
53 time.sleep(50 / 1000)
54
55
56# Main loop
57def main(stdscr):
58 """Draw a simple TUI, grab keypresses and let the user play some sounds."""
59 stdscr.clear()
60 stdscr.nodelay(True)
61
62 # Create small visual display
63 stdscr.addstr(
64 "========================\n"
65 "Multiple System Example.\n"
66 "========================"
67 )
68
69 # Create Sound Card A
70 system_a = pyfmodex.System()
71 version = system_a.version
72 if version < MIN_FMOD_VERSION:
73 print(
74 f"FMOD lib version {version:#08x} doesn't meet "
75 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
76 )
77 sys.exit(1)
78
79 system_a.driver = fetch_driver(stdscr, system_a, "System A")
80 system_a.init()
81
82 # Create Sound Card B
83 system_b = pyfmodex.System()
84 system_b.driver = fetch_driver(stdscr, system_b, "System B")
85 system_b.init()
86
87 # Load one sample into each sound card
88 sound_a = system_a.create_sound("media/drumloop.wav", mode=MODE.LOOP_OFF)
89 sound_b = system_b.create_sound("media/jaguar.wav")
90
91 stdscr.move(4, 0)
92 stdscr.clrtobot()
93 stdscr.addstr(
94 "Press 1 to play a sound on device A\n"
95 "Press 2 to play a sound on device B\n"
96 "Press q to quit"
97 )
98 while True:
99 stdscr.move(8, 0)
100 stdscr.clrtobot()
101 stdscr.addstr(
102 f"Channels playing on A: {system_a.channels_playing.channels}\n"
103 f"Channels playing on B: {system_b.channels_playing.channels}"
104 )
105
106 # Listen to the user
107 try:
108 keypress = stdscr.getkey()
109 if keypress == "1":
110 system_a.play_sound(sound_a)
111 elif keypress == "2":
112 system_b.play_sound(sound_b)
113 elif keypress == "q":
114 break
115 except curses.error as cerr:
116 if cerr.args[0] != "no input":
117 raise cerr
118
119 system_a.update()
120 system_b.update()
121 time.sleep(50 / 1000)
122
123 # Shut down
124 sound_a.release()
125 system_a.release()
126
127 sound_b.release()
128 system_b.release()
129
130
131curses.wrapper(main)
Net stream
This example shows how to play streaming audio from an Internet source.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to play streaming audio from an Internet source."""
2
3import ctypes
4import curses
5import sys
6import time
7
8import pyfmodex
9from pyfmodex.enums import OPENSTATE, RESULT, TAGDATATYPE, TAGTYPE, TIMEUNIT
10from pyfmodex.exceptions import FmodError
11from pyfmodex.flags import MODE
12from pyfmodex.structobject import Structobject
13from pyfmodex.structures import CREATESOUNDEXINFO
14
15URL = "https://focus.stream.publicradio.org/focus.mp3"
16
17MIN_FMOD_VERSION = 0x00020108
18
19# Create a System object and initialize
20system = pyfmodex.System()
21VERSION = system.version
22if VERSION < MIN_FMOD_VERSION:
23 print(
24 f"FMOD lib version {VERSION:#08x} doesn't meet "
25 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
26 )
27 sys.exit(1)
28
29system.init(maxchannels=1)
30
31# Increase the file buffer size a little bit to account for Internet lag
32system.stream_buffer_size = Structobject(size=64 * 1024, unit=TIMEUNIT.RAWBYTES)
33
34# Increase the default file chunk size to handle seeking inside large playlist
35# files that may be over 2kb.
36exinfo = CREATESOUNDEXINFO(filebuffersize=1024 * 16)
37
38tags = {}
39
40
41def show_tags(stdscr, sound, channel):
42 """Read and print any tags that have arrived. This could, for example,
43 happen if a radio station switches to a new song.
44 """
45 stdscr.move(11, 0)
46 stdscr.addstr("Tags:\n")
47 while True:
48 try:
49 tag = sound.get_tag(-1)
50 except FmodError:
51 break
52
53 if tag.datatype == TAGDATATYPE.STRING.value:
54 tag_data = ctypes.string_at(tag.data).decode()
55 tags[tag.name.decode()] = (tag_data, tag.datalen)
56 if tag.type == TAGTYPE.PLAYLIST.value and not tag.name == "FILE":
57 # data point to sound owned memory, copy it before the
58 # sound is released
59 sound.release()
60 sound = system.create_sound(
61 tag.data,
62 mode=MODE.CREATESTREAM | MODE.NONBLOCKING,
63 exinfo=exinfo,
64 )
65 elif tag.type == TAGTYPE.FMOD.value:
66 # When a song changes, the sample rate may also change, so
67 # compensate here
68 if tag.name.decode() == "Sample Rate Change" and channel:
69 channel.frequency = float(ctypes.string_at(tag.data).decode())
70
71 stdscr.move(12, 0)
72 stdscr.clrtobot()
73 for name, value in tags.items():
74 stdscr.addstr(f"{name} = '{value[0]}' ({value[1]} bytes)\n")
75
76
77# Main loop
78def main(stdscr):
79 """Draw a simple TUI, grab keypresses and let the user control playback."""
80 stdscr.clear()
81 stdscr.nodelay(True)
82
83 # Create small visual display
84 stdscr.addstr(
85 "===================\n"
86 "Net Stream Example.\n"
87 "===================\n"
88 "\n"
89 "Press SPACE to toggle pause\n"
90 "Press q to quit\n"
91 )
92
93 sound = system.create_sound(
94 URL, mode=MODE.CREATESTREAM | MODE.NONBLOCKING, exinfo=exinfo
95 )
96
97 channel = None
98 while True:
99 open_state = sound.open_state
100
101 is_playing = False
102 position = 0
103 paused = False
104 if channel:
105 try:
106 is_playing = channel.is_playing
107 position = channel.get_position(TIMEUNIT.MS)
108 paused = channel.paused
109
110 # Silence the stream until we have sufficient data for smooth
111 # playback
112 channel.mute = open_state.starving
113 except FmodError as fmoderror:
114 if fmoderror.result not in (
115 RESULT.INVALID_HANDLE,
116 RESULT.CHANNEL_STOLEN,
117 ):
118 raise fmoderror
119 else:
120 try:
121 channel = system.play_sound(sound)
122 except FmodError:
123 # This may fail if the stream isn't ready yet, so don't check
124 # for errors
125 pass
126
127 state = ""
128 if open_state.state == OPENSTATE.BUFFERING:
129 state = "Buffering..."
130 elif open_state.state == OPENSTATE.CONNECTING:
131 state = "Connecting..."
132 elif paused:
133 state = "Paused"
134 elif is_playing:
135 state = "Playing"
136
137 if open_state.starving:
138 state += " (STARVING)"
139
140 stdscr.move(7, 0)
141 stdscr.clrtoeol()
142 stdscr.addstr(
143 "Time = %02d:%02d:%02d\n"
144 % (
145 position / 1000 / 60,
146 position / 1000 % 60,
147 position / 10 % 100,
148 ),
149 )
150 stdscr.addstr(
151 f"State = {state}\n"
152 f"Buffer Percentage = {open_state.percent_buffered}%"
153 )
154
155 show_tags(stdscr, sound, channel)
156
157 # Listen to the user
158 try:
159 keypress = stdscr.getkey()
160 if keypress == " ":
161 if channel:
162 channel.paused = not channel.paused
163 elif keypress == "q":
164 break
165 except curses.error as cerr:
166 if cerr.args[0] != "no input":
167 raise cerr
168
169 system.update()
170 time.sleep(50 / 1000)
171
172 if channel:
173 channel.stop()
174
175 stdscr.clear()
176 stdscr.addstr("Waiting for sound to finish opening before trying to release it...")
177 stdscr.refresh()
178 while True:
179 if sound.open_state.state == OPENSTATE.READY:
180 break
181 system.update()
182 time.sleep(50 / 1000)
183
184 sound.release()
185
186
187curses.wrapper(main)
188
189# Shut down
190system.release()
Play sound
This example shows how to simply load and play multiple sounds, the simplest usage of FMOD.
By default FMOD will decode the entire file into memory when it loads.
If the sounds are big and possibly take up a lot of RAM it would be better to use the CREATESTREAM
flag, as this will stream the file in realtime as it plays (see Play stream).
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to simply load and play multiple sounds, the
2simplest usage of FMOD.
3"""
4
5import curses
6import sys
7import time
8
9from pathlib import Path
10
11import pyfmodex
12from pyfmodex.enums import RESULT, TIMEUNIT
13from pyfmodex.exceptions import FmodError
14from pyfmodex.flags import MODE
15
16MIN_FMOD_VERSION = 0x00020108
17
18mediadir = Path("media")
19soundnames = (
20 mediadir / "drumloop.wav",
21 mediadir / "jaguar.wav",
22 mediadir / "swish.wav",
23)
24
25# Create a System object and initialize
26system = pyfmodex.System()
27VERSION = system.version
28if VERSION < MIN_FMOD_VERSION:
29 print(
30 f"FMOD lib version {VERSION:#08x} doesn't meet "
31 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
32 )
33 sys.exit(1)
34
35system.init()
36
37sounds = []
38for filename in soundnames:
39 # drumloop.wav has embedded loop points which automatically turns on
40 # looping so we turn it off (for all) here.
41 sounds.append(system.create_sound(str(filename), mode=MODE.LOOP_OFF))
42
43
44# Main loop
45def main(stdscr):
46 """Draw a simple TUI, grab keypresses and let the user play the sounds."""
47 stdscr.clear()
48 stdscr.nodelay(True)
49
50 # Create small visual display
51 stdscr.addstr(
52 "===================\n"
53 "Play Sound Example.\n"
54 "===================\n"
55 "\n"
56 f"Press 1 to play a mono sound ({soundnames[0].stem})\n"
57 f"Press 2 to play a mono sound ({soundnames[1].stem})\n"
58 f"Press 3 to play a stereo sound ({soundnames[2].stem})\n"
59 "Press q to quit"
60 )
61
62 channel = None
63 currentsound = None
64 while True:
65 is_playing = False
66 position = 0
67 length = 0
68 if channel:
69 try:
70 is_playing = channel.is_playing
71 position = channel.get_position(TIMEUNIT.MS)
72 currentsound = channel.current_sound
73 if currentsound:
74 length = currentsound.get_length(TIMEUNIT.MS)
75
76 except FmodError as fmoderror:
77 if fmoderror.result not in (
78 RESULT.INVALID_HANDLE,
79 RESULT.CHANNEL_STOLEN,
80 ):
81 raise fmoderror
82
83 stdscr.move(9, 0)
84 stdscr.clrtoeol()
85 stdscr.addstr(
86 "Time %02d:%02d:%02d/%02d:%02d:%02d : %s"
87 % (
88 position / 1000 / 60,
89 position / 1000 % 60,
90 position / 10 % 100,
91 length / 1000 / 60,
92 length / 1000 % 60,
93 length / 10 % 100,
94 "Playing" if is_playing else "Stopped",
95 ),
96 )
97 stdscr.addstr(10, 0, f"Channel Playing {system.channels_playing.channels:-2d}")
98
99 # Listen to the user
100 try:
101 keypress = stdscr.getkey()
102 if keypress in "123":
103 channel = system.play_sound(sounds[int(keypress) - 1])
104 elif keypress == "q":
105 break
106 except curses.error as cerr:
107 if cerr.args[0] != "no input":
108 raise cerr
109
110 system.update()
111 time.sleep(50 / 1000)
112
113
114curses.wrapper(main)
115
116# Shut down
117for sound in sounds:
118 sound.release()
119system.release()
Play stream
This example shows how to simply play a stream such as an MP3 or WAV.
The stream behaviour is achieved by specifying CREATESTREAM
in the call to create_sound()
.
This makes FMOD decode the file in realtime as it plays, instead of loading it all at once which uses far less memory in exchange for a small runtime CPU hit.
Note that pyfmodex does this automatically through the convenience method create_stream()
.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to simply play a stream such as an MP3 or WAV."""
2
3import curses
4import sys
5import time
6from pathlib import Path
7
8import pyfmodex
9from pyfmodex.enums import RESULT, TIMEUNIT
10from pyfmodex.exceptions import FmodError
11from pyfmodex.flags import MODE
12
13MIN_FMOD_VERSION = 0x00020108
14
15mediadir = Path("media")
16soundnames = (
17 mediadir / "drumloop.wav",
18 mediadir / "jaguar.wav",
19 mediadir / "swish.wav",
20)
21
22# Create a System object and initialize
23system = pyfmodex.System()
24VERSION = system.version
25if VERSION < MIN_FMOD_VERSION:
26 print(
27 f"FMOD lib version {VERSION:#08x} doesn't meet "
28 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
29 )
30 sys.exit(1)
31
32system.init()
33
34# This example uses an FSB file, which is a preferred pack format for fmod
35# containing multiple sounds. This could just as easily be exchanged with a
36# wav/mp3/ogg file for example, but in that case you wouldn't need to check for
37# subsounds. Because of the check below, this example would work with both
38# types of sound file (packed vs single).
39sound = system.create_stream("media/wave_vorbis.fsb", mode=MODE.LOOP_NORMAL)
40
41sound_to_play = sound
42if sound.num_subsounds:
43 sound_to_play = sound.get_subsound(0)
44
45# Main loop
46def main(stdscr):
47 """Draw a simple TUI, grab keypresses and let the user control playback."""
48 stdscr.clear()
49 stdscr.nodelay(True)
50
51 # Create small visual display
52 stdscr.addstr(
53 "====================\n"
54 "Play Stream Example.\n"
55 "====================\n"
56 "\n"
57 "Press SPACE to toggle pause\n"
58 "Press q to quit"
59 )
60
61 # Play the sound
62 channel = sound_to_play.play()
63
64 while True:
65 is_playing = False
66 position = 0
67 length = 0
68 try:
69 is_playing = channel.is_playing
70 position = channel.get_position(TIMEUNIT.MS)
71 length = sound_to_play.get_length(TIMEUNIT.MS)
72 except FmodError as fmoderror:
73 if not fmoderror.result is RESULT.INVALID_HANDLE:
74 raise fmoderror
75
76 stdscr.move(7, 0)
77 stdscr.clrtoeol()
78 stdscr.addstr(
79 "Time %02d:%02d:%02d/%02d:%02d:%02d : %s"
80 % (
81 position / 1000 / 60,
82 position / 1000 % 60,
83 position / 10 % 100,
84 length / 1000 / 60,
85 length / 1000 % 60,
86 length / 10 % 100,
87 "Paused" if channel.paused else "Playing" if is_playing else "Stopped",
88 ),
89 )
90
91 # Listen to the user
92 try:
93 keypress = stdscr.getkey()
94 if keypress == " ":
95 channel.paused = not channel.paused
96 elif keypress == "q":
97 break
98 except curses.error as cerr:
99 if cerr.args[0] != "no input":
100 raise cerr
101
102 system.update()
103 time.sleep(50 / 1000)
104
105
106curses.wrapper(main)
107
108# Shut down
109sound_to_play.release()
110system.release()
Record enumeration
This example shows how to enumerate the available recording drivers on a device. It demonstrates how the enumerated list changes as microphones are attached and detached. It also shows that you can record from multi mics at the same time (if your audio subsystem supports that).
Please note: to minimize latency, care should be taken to control the number of samples between the record position and the play position. Check Record for details on this process.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to enumerate the available recording drivers on
2this device and work with them.
3"""
4
5import curses
6import sys
7import time
8from collections import defaultdict
9from ctypes import c_int, c_short, sizeof
10
11import pyfmodex
12from pyfmodex.enums import RESULT, SOUND_FORMAT
13from pyfmodex.exceptions import FmodError
14from pyfmodex.flags import DRIVER_STATE, MODE, SYSTEM_CALLBACK_TYPE
15from pyfmodex.structures import CREATESOUNDEXINFO
16
17MIN_FMOD_VERSION = 0x00020108
18MAX_DRIVERS_IN_VIEW = 3
19MAX_DRIVERS = 16
20
21# Create a System object and initialize
22system = pyfmodex.System()
23VERSION = system.version
24if VERSION < MIN_FMOD_VERSION:
25 print(
26 f"FMOD lib version {VERSION:#08x} doesn't meet "
27 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
28 )
29 sys.exit(1)
30
31system.init()
32
33# Setup a callback so we can be notified if the record list has changed
34def record_list_changed_callback( # pylint: disable=unused-argument
35 mysystem, callback_type, commanddata1, comanddata2, userdata
36):
37 """Increase a counter referenced by userdata."""
38 _record_list_changed_count = c_int.from_address(userdata)
39 _record_list_changed_count.value += 1
40
41 return RESULT.OK.value
42
43
44record_list_changed_count = c_int(0)
45system.user_data = record_list_changed_count
46system.set_callback(
47 record_list_changed_callback, SYSTEM_CALLBACK_TYPE.RECORDLISTCHANGED
48)
49
50
51recordings = [defaultdict(bool) for _ in range(MAX_DRIVERS)]
52
53
54def show_record_drivers(stdscr, selected_driver_idx, num_drivers):
55 """Show an overview of detected record drivers."""
56 row, _ = stdscr.getyx()
57 for i in range(min(MAX_DRIVERS_IN_VIEW, num_drivers)):
58 idx = (selected_driver_idx - MAX_DRIVERS_IN_VIEW // 2 + i) % num_drivers
59 row += 2
60 if idx == selected_driver_idx:
61 stdscr.addstr(row, 0, ">")
62 driver_info = system.get_record_driver_info(idx)
63 statechar = "(*) " if DRIVER_STATE(driver_info.state) & DRIVER_STATE.DEFAULT else ""
64 stdscr.addstr(row, 2, f"{idx}. {statechar}{driver_info.name.decode():41s}")
65 row += 1
66 stdscr.addstr(row, 2, f"{driver_info.system_rate/1000:2.1f}KHz")
67 stdscr.addstr(row, 10, f"{driver_info.speaker_mode_channels}ch")
68 data4 = driver_info.guid.data4.zfill(8).decode()
69 stdscr.addstr(
70 row,
71 13,
72 "{%08X-%04X-%04X-%04X-%02X%02X%02X%02X%02X%02X}"
73 % (
74 driver_info.guid.data1,
75 driver_info.guid.data2,
76 driver_info.guid.data3,
77 int(data4[0]) << 8 | int(data4[1]),
78 int(data4[2]),
79 int(data4[3]),
80 int(data4[4]),
81 int(data4[5]),
82 int(data4[6]),
83 int(data4[7]),
84 ),
85 )
86 row += 1
87 stdscr.addstr(
88 row,
89 2,
90 "(%s) (%s) (%s)"
91 % (
92 "Connected"
93 if (DRIVER_STATE(driver_info.state) & DRIVER_STATE.CONNECTED)
94 else "Unplugged",
95 "Recording" if system.is_recording(idx) else "Not recoding",
96 "Playing"
97 if recordings[idx]["channel"] and recordings[idx]["channel"].is_playing
98 else "Not playing",
99 ),
100 )
101
102
103# Main loop
104def main(stdscr):
105 """Draw a simple TUI, grab keypresses and let the user control recording
106 and playback.
107 """
108 stdscr.clear()
109 stdscr.nodelay(True)
110
111 # Create small visual display
112 stdscr.addstr(
113 "===========================\n"
114 "Record Enumeration Example.\n"
115 "==========================="
116 )
117
118 selected_driver_idx = 0
119 cur_y, _ = stdscr.getyx()
120 while True:
121 stdscr.move(cur_y + 2, 0)
122 stdscr.clrtobot()
123 stdscr.addstr(
124 f"Record list has updated {record_list_changed_count.value} time(s)\n"
125 f"Currently, {system.record_num_drivers.connected} recording device(s) are plugged in\n"
126 "\n"
127 "Press j and k to scroll list\n"
128 "Press q to quit\n"
129 "\n"
130 "Press 1 to start/stop recording\n"
131 "Press 2 to start/stop playback"
132 )
133
134 # Clamp the reported number of drivers to simplify this example
135 num_drivers = min(system.record_num_drivers.drivers, MAX_DRIVERS)
136
137 subwin = stdscr.subwin(cur_y + 9, 0)
138 show_record_drivers(subwin, selected_driver_idx, num_drivers)
139
140 # Listen to the user
141 try:
142 keypress = stdscr.getkey()
143 if keypress == "j":
144 selected_driver_idx = (selected_driver_idx + 1) % num_drivers
145 elif keypress == "k":
146 selected_driver_idx = (selected_driver_idx - 1) % num_drivers
147 elif keypress == "q":
148 break
149 elif keypress == "1":
150 if system.is_recording(selected_driver_idx):
151 system.record_stop(selected_driver_idx)
152 else:
153 # Clean up previous record sound
154 if recordings[selected_driver_idx]["sound"]:
155 recordings[selected_driver_idx]["sound"].release()
156
157 # Query device native settings and start a recording
158 record_driver_info = system.get_record_driver_info(
159 selected_driver_idx
160 )
161 exinfo = CREATESOUNDEXINFO(
162 numchannels=record_driver_info.speaker_mode_channels,
163 format=SOUND_FORMAT.PCM16.value,
164 defaultfrequency=record_driver_info.system_rate,
165 # one second buffer; size here does not change the latency
166 length=record_driver_info.system_rate
167 * sizeof(c_short)
168 * record_driver_info.speaker_mode_channels,
169 )
170 sound = system.create_sound(
171 0, mode=MODE.LOOP_NORMAL | MODE.OPENUSER, exinfo=exinfo
172 )
173 recordings[selected_driver_idx]["sound"] = sound
174 try:
175 system.record_start(selected_driver_idx, sound, loop=True)
176 except FmodError as fmoderror:
177 if fmoderror.result != RESULT.RECORD_DISCONNECTED:
178 raise fmoderror
179 elif keypress == "2":
180 channel = recordings[selected_driver_idx]["channel"]
181 sound = recordings[selected_driver_idx]["sound"]
182 if channel and channel.is_playing:
183 channel.stop()
184 recordings[selected_driver_idx]["channel"] = False
185 elif sound:
186 recordings[selected_driver_idx]["channel"] = sound.play()
187
188 except curses.error as cerr:
189 if cerr.args[0] != "no input":
190 raise cerr
191
192 system.update()
193 time.sleep(50 / 1000)
194
195
196curses.wrapper(main)
197
198# Shut down
199for recorder in recordings:
200 if recorder["sound"]:
201 recorder["sound"].release()
202system.release()
Record
This example shows how to record continuously and play back the same data while keeping a specified latency between the two. This is achieved by delaying the start of playback until the specified number of milliseconds has been recorded. At runtime the playback speed will be slightly altered to compensate for any drift in either play or record drivers.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to record continuously and play back the same data
2while keeping a specified latency between the two.
3"""
4
5import curses
6import sys
7import time
8from ctypes import c_short, sizeof
9
10import pyfmodex
11from pyfmodex.enums import RESULT, SOUND_FORMAT, TIMEUNIT
12from pyfmodex.exceptions import FmodError
13from pyfmodex.flags import MODE
14from pyfmodex.reverb_presets import REVERB_PRESET
15from pyfmodex.structures import CREATESOUNDEXINFO, REVERB_PROPERTIES
16
17MIN_FMOD_VERSION = 0x00020108
18
19# Some devices will require higher latency to avoid glitches
20LATENCY_MS = 50
21DRIFT_MS = 1
22RECORD_DEVICE_INDEX = 0
23
24# Create a System object and initialize
25system = pyfmodex.System()
26VERSION = system.version
27if VERSION < MIN_FMOD_VERSION:
28 print(
29 f"FMOD lib version {VERSION:#08x} doesn't meet "
30 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
31 )
32 sys.exit(1)
33
34system.init()
35
36if not system.record_num_drivers:
37 print("No recording devices found/plugged in! Aborting.")
38 sys.exit(1)
39
40# Determine latency in samples
41record_driver_info = system.get_record_driver_info(RECORD_DEVICE_INDEX)
42
43# The point where we start compensating for drift
44drift_threshold = record_driver_info.system_rate * DRIFT_MS / 1000
45# User specified latency
46desired_latency = record_driver_info.system_rate * LATENCY_MS / 1000
47
48# Create user sound to record into, then start recording
49exinfo = CREATESOUNDEXINFO(
50 numchannels=record_driver_info.speaker_mode_channels,
51 format=SOUND_FORMAT.PCM16.value,
52 defaultfrequency=record_driver_info.system_rate,
53 # one second buffer; size here does not change the latency
54 length=record_driver_info.system_rate
55 * sizeof(c_short)
56 * record_driver_info.speaker_mode_channels,
57)
58sound = system.create_sound(0, mode=MODE.LOOP_NORMAL | MODE.OPENUSER, exinfo=exinfo)
59system.record_start(RECORD_DEVICE_INDEX, sound, loop=True)
60sound_len = sound.get_length(TIMEUNIT.PCM)
61
62# Main loop
63def main(stdscr):
64 """Draw a simple TUI, grab keypresses and let the user control playback."""
65 stdscr.clear()
66 stdscr.nodelay(True)
67
68 dsp_enabled = False
69
70 # Create small visual display
71 stdscr.addstr(
72 "===============\n"
73 "Record Example.\n"
74 "===============\n"
75 "\n"
76 "(Adjust LATENCY_MS in the source to compensate for stuttering)\n"
77 f"(Current value is {LATENCY_MS}ms)"
78 )
79
80 reverb_on = REVERB_PROPERTIES(*REVERB_PRESET.CONCERTHALL.value)
81 reverb_off = REVERB_PROPERTIES(*REVERB_PRESET.OFF.value)
82
83 # User specified latency adjusted for driver update granularity
84 adjusted_latency = desired_latency
85 # Latency measured once playback begins (smoothened for jitter)
86 actual_latency = desired_latency
87
88 last_record_pos = 0
89 last_play_pos = 0
90 samples_recorded = 0
91 samples_played = 0
92 min_record_delta = sound_len
93 channel = None
94 while True:
95 stdscr.move(7, 0)
96 stdscr.clrtoeol()
97 stdscr.addstr(
98 f"Press SPACE to {'disable' if dsp_enabled else 'enable'} DSP effect\n"
99 "Press q to quit"
100 )
101
102 # Determine how much has been recorded since we last checked
103 record_pos = 0
104 try:
105 record_pos = system.get_record_position(RECORD_DEVICE_INDEX)
106 except FmodError as fmoderror:
107 if fmoderror.result != RESULT.RECORD_DISCONNECTED:
108 raise fmoderror
109
110 record_delta = (
111 record_pos - last_record_pos
112 if record_pos >= last_record_pos
113 else record_pos + sound_len - last_record_pos
114 )
115 last_record_pos = record_pos
116 samples_recorded += record_delta
117
118 if record_delta and record_delta < min_record_delta:
119 # Smallest driver granularity seen so far
120 min_record_delta = record_delta
121 # Adjust our latency if driver granularity is high
122 adjusted_latency = max(desired_latency, record_delta)
123
124 # Delay playback until our desired latency is reached
125 if not channel and samples_recorded >= adjusted_latency:
126 channel = sound.play()
127
128 if channel:
129 # Stop playback if recording stops
130 if not system.is_recording(RECORD_DEVICE_INDEX):
131 channel.paused = True
132
133 # Determine how much has been played since we last checked
134 play_pos = channel.get_position(TIMEUNIT.PCM)
135 play_delta = (
136 play_pos - last_play_pos
137 if play_pos >= last_play_pos
138 else play_pos + sound_len - last_play_pos
139 )
140 last_play_pos = play_pos
141 samples_played += play_delta
142
143 # Compensate for any drift
144 latency = samples_recorded - samples_played
145 actual_latency = 0.97 * actual_latency + 0.03 * latency
146
147 playbackrate = record_driver_info.system_rate
148 if actual_latency < adjusted_latency - drift_threshold:
149 # Play position is catching up to the record position, slow
150 # playback down by 2%
151 playbackrate -= playbackrate / 50
152 elif actual_latency > adjusted_latency + drift_threshold:
153 # Play position is falling behind the record position, speed
154 # playback up by 2%
155 playbackrate += playbackrate / 50
156 channel.frequency = playbackrate
157
158 adjusted_latency_ms = int(
159 adjusted_latency * 1000 / record_driver_info.system_rate
160 )
161 actual_latency_ms = int(actual_latency * 1000 / record_driver_info.system_rate)
162 samples_recorded_s = int(samples_recorded / record_driver_info.system_rate)
163 samples_played_s = int(samples_played / record_driver_info.system_rate)
164
165 stdscr.move(10, 0)
166 stdscr.clrtobot()
167 stdscr.addstr(
168 f"Adjusted latency: {adjusted_latency:4.0f} ({adjusted_latency_ms}ms)\n"
169 f"Actual latency: {actual_latency:4.0f} ({actual_latency_ms}ms)\n"
170 "\n"
171 f"Recorded: {samples_recorded:5d} ({samples_recorded_s}s)\n"
172 f"Played: {samples_played:5d} ({samples_played_s}s)"
173 )
174
175 # Listen to the user
176 try:
177 keypress = stdscr.getkey()
178 if keypress == " ":
179 # Add a DSP effect -- just for fun
180 dsp_enabled = not dsp_enabled
181 system.set_reverb_properties(
182 0, reverb_on if dsp_enabled else reverb_off
183 )
184 elif keypress == "q":
185 break
186 except curses.error as cerr:
187 if cerr.args[0] != "no input":
188 raise cerr
189
190 system.update()
191 time.sleep(10 / 1000)
192
193
194curses.wrapper(main)
195
196# Shut down
197sound.release()
198system.release()
User Created Sound
This example shows how create a sound with data filled by the user. It shows a user created static sample, followed by a user created stream. The former allocates all memory needed for the sound and is played back as a static sample, while the latter streams the data in chunks as it plays, using far less memory.
(Adapted from sample code shipped with FMOD Engine.)
1"""Example code to show how to create a sound with data filled by the user."""
2
3import curses
4import sys
5import time
6from ctypes import c_float, c_short, sizeof
7from math import sin
8
9import pyfmodex
10from pyfmodex.callback_prototypes import (SOUND_PCMREADCALLBACK,
11 SOUND_PCMSETPOSCALLBACK)
12from pyfmodex.enums import RESULT, SOUND_FORMAT, TIMEUNIT
13from pyfmodex.exceptions import FmodError
14from pyfmodex.flags import MODE
15from pyfmodex.structures import CREATESOUNDEXINFO
16
17MIN_FMOD_VERSION = 0x00020108
18
19# Create a System object and initialize
20system = pyfmodex.System()
21VERSION = system.version
22if VERSION < MIN_FMOD_VERSION:
23 print(
24 f"FMOD lib version {VERSION:#08x} doesn't meet "
25 f"minimum requirement of version {MIN_FMOD_VERSION:#08x}"
26 )
27 sys.exit(1)
28
29system.init()
30
31# pylint: disable=invalid-name
32# Using names common in mathematics
33t1, t2 = c_float(0), c_float(0) # time
34v1, v2 = c_float(0), c_float(0) # velocity
35
36
37def pcmread_callback(sound_p, data_p, datalen_i): # pylint: disable=unused-argument
38 """Read callback used for user created sounds.
39
40 Generates smooth noise.
41 """
42
43 # >>2 = 16bit stereo (4 bytes per sample)
44 for _ in range(datalen_i >> 2):
45 # left channel
46 stereo16bitbuffer_left = int(sin(t1.value) * 32767)
47 c_short.from_address(data_p).value = stereo16bitbuffer_left
48 data_p += sizeof(c_short)
49
50 # right channel
51 stereo16bitbuffer_right = int(sin(t2.value) * 32767)
52 c_short.from_address(data_p).value = stereo16bitbuffer_right
53 data_p += sizeof(c_short)
54
55 t1.value += 0.01 + v1.value
56 t2.value += 0.0142 + v2.value
57 v1.value += sin(t1.value) * 0.002
58 v2.value += sin(t2.value) * 0.002
59
60 return RESULT.OK.value
61
62
63def pcmsetpos_callback(
64 sound, subsound, position, timeunit
65): # pylint: disable=unused-argument
66 """Set position callback for user created sounds or to intercept FMOD's
67 decoder during an API setPositon call.
68
69 This is useful if the user calls set_position on a channel and you want to
70 seek your data accordingly.
71 """
72 return RESULT.OK.value
73
74
75# Main loop
76def main(stdscr):
77 """Draw a simple TUI, grab keypresses and let the user select a sound
78 generation method.
79 """
80 stdscr.clear()
81 stdscr.nodelay(True)
82
83 # Create small visual display
84 stdscr.addstr(
85 "===========================\n"
86 "User Created Sound Example.\n"
87 "==========================="
88 )
89 stdscr.refresh()
90
91 subwin = stdscr.derwin(4, 0)
92 subwin.nodelay(True)
93 subwin.addstr(
94 "Sound played here is generated in realtime. It will either play as a "
95 "stream which means it is continually filled as it is playing, or it "
96 "will play as a static sample, which means it is filled once as the "
97 "sound is created, then, when played, it will just play that short "
98 "loop of data.\n"
99 "\n"
100 "Press 1 to play an generated infinite stream\n"
101 "Press 2 to play a static looping sample\n"
102 "Press q to quit"
103 )
104
105 mode = MODE.OPENUSER | MODE.LOOP_NORMAL
106 while True:
107 # Listen to the user
108 try:
109 keypress = subwin.getkey()
110 if keypress == "1":
111 mode |= MODE.CREATESTREAM
112 break
113 if keypress == "2":
114 break
115 if keypress == "q":
116 return
117 except curses.error as cerr:
118 if cerr.args[0] != "no input":
119 raise cerr
120
121 time.sleep(50 / 1000)
122
123 # Create and play the sound
124 numchannels = 2
125 defaultfrequency = 44100
126 exinfo = CREATESOUNDEXINFO(
127 # Number of channels in the sound
128 numchannels=numchannels,
129 # Default playback rate of the sound
130 defaultfrequency=defaultfrequency,
131 # Chunk size of stream update in samples. This will be the amount of
132 # data passed to the user callback.
133 decodebuffersize=44100,
134 # Length of PCM data in bytes of whole sound (for sound.get_length)
135 length=defaultfrequency * numchannels * sizeof(c_short) * 5,
136 # Data format of sound
137 format=SOUND_FORMAT.PCM16.value,
138 # User callback to reading
139 pcmreadcallback=SOUND_PCMREADCALLBACK(pcmread_callback),
140 # User callback to seeking
141 pcmsetposcallback=SOUND_PCMSETPOSCALLBACK(pcmsetpos_callback),
142 )
143 sound = system.create_sound(0, mode=mode, exinfo=exinfo)
144 channel = sound.play()
145
146 subwin.clear()
147 subwin.addstr("Press SPACE to toggle pause\n" "Press q to quit")
148 row, _ = subwin.getyx()
149 while True:
150 is_playing = False
151 paused = False
152 position = 0
153 length = 0
154 if channel:
155 try:
156 is_playing = channel.is_playing
157 paused = channel.paused
158 position = channel.get_position(TIMEUNIT.MS)
159 length = sound.get_length(TIMEUNIT.MS)
160
161 except FmodError as fmoderror:
162 if not fmoderror.result is RESULT.INVALID_HANDLE:
163 raise fmoderror
164
165 subwin.move(row + 2, 0)
166 subwin.clrtoeol()
167 subwin.addstr(
168 "Time %02d:%02d:%02d/%02d:%02d:%02d : %s"
169 % (
170 position / 1000 / 60,
171 position / 1000 % 60,
172 position / 10 % 100,
173 length / 1000 / 60,
174 length / 1000 % 60,
175 length / 10 % 100,
176 "Paused" if paused else "Playing" if is_playing else "Stopped",
177 ),
178 )
179
180 # Listen to the user
181 try:
182 keypress = subwin.getkey()
183 if keypress == " ":
184 channel.paused = not channel.paused
185 elif keypress == "q":
186 break
187 except curses.error as cerr:
188 if cerr.args[0] != "no input":
189 raise cerr
190
191 system.update()
192 time.sleep(50 / 1000)
193
194 sound.release()
195
196
197curses.wrapper(main)
198
199# Shut down
200system.release()