arcadesynth 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Martin Richardson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ # MANIFEST.in
2
+
3
+ include src/arcadesynth/*.toml
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcadesynth
3
+ Version: 1.0.1
4
+ Summary: Programmatically synthesize retro arcade sound effects
5
+ Author-email: "C. Martin Richardson" <c.mart.richardson@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 C. Martin Richardson
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/martianmr/arcadesynth
29
+ Keywords: arcade,synth,retro
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Programming Language :: Python
32
+ Classifier: Programming Language :: Python :: 3
33
+ Requires-Python: >=3.9
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: scipy>=1.17.1
37
+ Requires-Dist: tomli; python_version < "3.11"
38
+ Provides-Extra: dev
39
+ Requires-Dist: pygame; extra == "dev"
40
+ Requires-Dist: bumpver; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # arcadesynth 1.0.1
44
+
45
+ Programmatically synthesize retro arcade sound effects with arcadesynth
46
+
47
+ This utility can be used to generate your own custom sound effects in code as an alternative to using sound assets. It also provides some hand-crafted sounds from the Golden age of arcade video games.
48
+
49
+ The idea was inspired by sound generation on 1980s 8-bit microcomputers which sometimes had fairly sophisticated synthesizer chips which could be programmed at a low level with a bit of know-how. The algorithms are kept fairly simple but are capable of approximating early arcade, console and computer game sounds that are generated with specialized chips or custom electronics.
50
+
51
+ ## Installation
52
+
53
+ arcadesynth can be installed via pip
54
+
55
+ ```bash
56
+ pip install arcadesynth
57
+ ```
58
+
59
+ ## Modules
60
+
61
+ - synth - functions to build your own effects
62
+ - synth_sample - some simple basic sounds
63
+ - synth_example - some generic retro sound effects
64
+ - synth_asteroids - sounds from the 1979 Atari game Asteroids
65
+ - synth_spaceinvaders - sounds from the 1978 Taito game Asteroids
66
+
67
+ ## Synth
68
+
69
+ This module can be used to create custom sound effects by generating waves or white noise and then manipulating the signal with envelopes, filters and more.
70
+
71
+ E.g. synthesize a retro gunshot sound in pygame.
72
+
73
+ ```python
74
+ import pygame
75
+ from arcadesynth import synth
76
+
77
+ pygame.init()
78
+ pygame.mixer.init()
79
+ ms = 500 # Length of buffer will be 500 milliseconds
80
+ buf = synth.make_white(ms, 1.0) # Fill buffer with white noise
81
+ buf = synth.lowpass_filter(buf, 1200.0, order=4) # Apply a filter to remove higher frequencies
82
+ synth.noisalise(buf, gain=0.5) # Even out the amplitude of the noise
83
+ synth.envelope(buf, 20, 30, 0.2, ms - 10) # Apply an envelope with a short attack and release and then fade off from 20% amplitude
84
+ sound = pygame.mixer.Sound(synth.tobytes(buf))
85
+ sound.play()
86
+ ```
87
+
88
+ ## Space Invaders
89
+
90
+ There are some built-in hand-crafted synthesized sounds including those from the 1978 classic Space Invaders by Taito. Sounds in the arcade machine are produced on a Texas Instruments SN76477 'complex sound generator' chip supported by custom electronics.
91
+
92
+ E.g. synthesize the sound made by the flying saucer blowing up in Space Invaders.
93
+
94
+ ```python
95
+ import pygame
96
+ from arcadesynth import synth_spaceinvaders as si
97
+
98
+ pygame.init()
99
+ pygame.mixer.init()
100
+ sound = pygame.mixer.Sound(si.si_ufodie())
101
+ sound.play()
102
+ ```
103
+
104
+ ## LICENSE
105
+
106
+ MIT License — See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,64 @@
1
+ # arcadesynth 1.0.1
2
+
3
+ Programmatically synthesize retro arcade sound effects with arcadesynth
4
+
5
+ This utility can be used to generate your own custom sound effects in code as an alternative to using sound assets. It also provides some hand-crafted sounds from the Golden age of arcade video games.
6
+
7
+ The idea was inspired by sound generation on 1980s 8-bit microcomputers which sometimes had fairly sophisticated synthesizer chips which could be programmed at a low level with a bit of know-how. The algorithms are kept fairly simple but are capable of approximating early arcade, console and computer game sounds that are generated with specialized chips or custom electronics.
8
+
9
+ ## Installation
10
+
11
+ arcadesynth can be installed via pip
12
+
13
+ ```bash
14
+ pip install arcadesynth
15
+ ```
16
+
17
+ ## Modules
18
+
19
+ - synth - functions to build your own effects
20
+ - synth_sample - some simple basic sounds
21
+ - synth_example - some generic retro sound effects
22
+ - synth_asteroids - sounds from the 1979 Atari game Asteroids
23
+ - synth_spaceinvaders - sounds from the 1978 Taito game Asteroids
24
+
25
+ ## Synth
26
+
27
+ This module can be used to create custom sound effects by generating waves or white noise and then manipulating the signal with envelopes, filters and more.
28
+
29
+ E.g. synthesize a retro gunshot sound in pygame.
30
+
31
+ ```python
32
+ import pygame
33
+ from arcadesynth import synth
34
+
35
+ pygame.init()
36
+ pygame.mixer.init()
37
+ ms = 500 # Length of buffer will be 500 milliseconds
38
+ buf = synth.make_white(ms, 1.0) # Fill buffer with white noise
39
+ buf = synth.lowpass_filter(buf, 1200.0, order=4) # Apply a filter to remove higher frequencies
40
+ synth.noisalise(buf, gain=0.5) # Even out the amplitude of the noise
41
+ synth.envelope(buf, 20, 30, 0.2, ms - 10) # Apply an envelope with a short attack and release and then fade off from 20% amplitude
42
+ sound = pygame.mixer.Sound(synth.tobytes(buf))
43
+ sound.play()
44
+ ```
45
+
46
+ ## Space Invaders
47
+
48
+ There are some built-in hand-crafted synthesized sounds including those from the 1978 classic Space Invaders by Taito. Sounds in the arcade machine are produced on a Texas Instruments SN76477 'complex sound generator' chip supported by custom electronics.
49
+
50
+ E.g. synthesize the sound made by the flying saucer blowing up in Space Invaders.
51
+
52
+ ```python
53
+ import pygame
54
+ from arcadesynth import synth_spaceinvaders as si
55
+
56
+ pygame.init()
57
+ pygame.mixer.init()
58
+ sound = pygame.mixer.Sound(si.si_ufodie())
59
+ sound.play()
60
+ ```
61
+
62
+ ## LICENSE
63
+
64
+ MIT License — See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,56 @@
1
+ # pyproject.toml
2
+
3
+ [build-system]
4
+ requires = ["setuptools>=61.0.0", "wheel"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "arcadesynth"
9
+ version = "1.0.1"
10
+ description = "Programmatically synthesize retro arcade sound effects"
11
+ readme = "README.md"
12
+ authors = [{ name = "C. Martin Richardson", email = "c.mart.richardson@gmail.com" }]
13
+ license = { file = "LICENSE" }
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python",
17
+ "Programming Language :: Python :: 3",
18
+ ]
19
+ keywords = ["arcade", "synth", "retro"]
20
+ dependencies = [
21
+ "scipy >= 1.17.1",
22
+ 'tomli; python_version < "3.11"',
23
+ ]
24
+ requires-python = ">=3.9"
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["pygame", "bumpver"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/martianmr/arcadesynth"
31
+
32
+ [tool.bumpver]
33
+ current_version = "1.0.1"
34
+ version_pattern = "MAJOR.MINOR.PATCH"
35
+ commit_message = "bump version {old_version} -> {new_version}"
36
+ tag_message = "{new_version}"
37
+ tag_scope = "default"
38
+ pre_commit_hook = ""
39
+ post_commit_hook = ""
40
+ commit = true
41
+ tag = true
42
+ push = true
43
+ allowed_branches = "main,develop,release*"
44
+
45
+ [tool.bumpver.file_patterns]
46
+ "pyproject.toml" = [
47
+ 'current_version = "{version}"',
48
+ 'version = "{version}"',
49
+ ]
50
+ "README.md" = [
51
+ "{version}",
52
+ ]
53
+ "src/arcadesynth/__init__.py" = [
54
+ "{version}"
55
+ ]
56
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ # __init__.py
2
+
3
+ from importlib import resources
4
+
5
+ # Version of the arcadesynth package
6
+ __version__ = "1.0.1"
@@ -0,0 +1 @@
1
+ # config.toml
@@ -0,0 +1,327 @@
1
+ """synth - programmatically synthesize retro arcade sound effects
2
+ """
3
+
4
+ import array
5
+ import math
6
+ import random
7
+ import numpy as np
8
+ from scipy.signal import butter, filtfilt, medfilt
9
+
10
+ DEFAULT_FREQUENCY = 44100
11
+ DEFAULT_CHANNELS = 2
12
+ MAX_LEN = 3600000
13
+ SAMPLE_RATE = DEFAULT_FREQUENCY * DEFAULT_CHANNELS
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Internal functions
17
+ # ---------------------------------------------------------------------------
18
+
19
+
20
+ def _limit(d, limit = 1.0, llimit = None):
21
+ """Force value to a limit"""
22
+ return min(max(d, (-limit if llimit is None else llimit)), limit)
23
+
24
+
25
+ def _butterworth_filter(buf, cutoff, type='lowpass', order=2):
26
+ """A butterworth filter to reduce unwanted frequencies, order 1 is lowest effect, 6 is more dramatic"""
27
+ nyq = 0.5 * SAMPLE_RATE
28
+ normal_cutoff = cutoff / nyq
29
+ b, a = butter(order, normal_cutoff, btype=type)
30
+ buf = array.array("d", filtfilt(b, a, buf))
31
+ return buf
32
+
33
+
34
+ def _samplept(func, i, f, harmonics=0, p=[], special=None):
35
+ """Calculate amplitude of a sample for a buffer position
36
+
37
+ Given a function to calculate amplitude of a wave (func) for a buffer position (i)
38
+ and frequency (f) calculate and return the amplitude.
39
+ If harmonics is not zero also add in the amplitude of the given order of harmonics.
40
+ If frequency is not constant throughout the buffer, track the current phase during the
41
+ building process and pass the current phase of the primary and any harmonics through
42
+ the list (p), otherwise phase can be calculated on the fly.
43
+ """
44
+ amp = 0.0
45
+ scaling = math.pow(2, harmonics) / (math.pow(2, harmonics + 1) - 1)
46
+ for h in range(harmonics + 1):
47
+ freq = f * (h + 1)
48
+ vol = (1.0 / (h + 1)) * scaling
49
+ dp = 2 * math.pi * freq / SAMPLE_RATE
50
+ if len(p) == 0:
51
+ phase = dp * i
52
+ else:
53
+ phase = p[h] = p[h] + dp
54
+ a = func(phase, special)
55
+ amp += a * vol
56
+ return amp if len(p) == 0 else (amp, p)
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Functions to calculate amplitude depending on phase
61
+ # ---------------------------------------------------------------------------
62
+
63
+
64
+ def sawtooth(phase, special = None):
65
+ """Calculate amplitude for a sawtooth wave at the given phase"""
66
+ return (2.0 / math.pi) * ((phase / 2.0) % math.pi) - 1.0
67
+
68
+
69
+ def triangle(phase, special = None):
70
+ """Calculate amplitude for a triangle wave at the given phase"""
71
+ return ((2.0 / math.pi) * (math.pi - abs(phase % (2.0 * math.pi) - math.pi)) - 1.0)
72
+
73
+
74
+ def square(phase, special = None):
75
+ """Calculate amplitude for a square wave at the given phase"""
76
+ cutoff = 0.0 if special is None else special
77
+ return 1.0 if math.sin(phase) > cutoff else -1.0
78
+
79
+
80
+ def sin(phase, special = None):
81
+ """Calculate amplitude for a sine wave at the given phase"""
82
+ return math.sin(phase)
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Functions to manipulate buffers and return a new buffer
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ def init(frequency=DEFAULT_FREQUENCY, channels=DEFAULT_CHANNELS):
91
+ """Set up sample rate, do this first to avoid using defaults
92
+
93
+ E.g. get these values from pygame with 'frequency, _, channels = pygame.mixer.get_init()'
94
+ """
95
+ global SAMPLE_RATE
96
+ SAMPLE_RATE = frequency * channels
97
+ return
98
+
99
+
100
+ def get_init():
101
+ return SAMPLE_RATE
102
+
103
+
104
+ def tobytes(buf):
105
+ """Convert buffer with range -1.0 to 1.0 to short signed integers then output as a byte stream
106
+
107
+ Use this to convert a sound held in internal format to a byte list for external processing, e.g. by
108
+ pygame.
109
+ """
110
+ a = array.array("h")
111
+ for i in range(len(buf)):
112
+ a.append(int(_limit(buf[i]) * 32767))
113
+ return a.tobytes()
114
+
115
+
116
+ def boost(buf, gain):
117
+ """Apply gain to buffer"""
118
+ nparray = np.array(buf)
119
+ npnew = nparray * gain
120
+ buf = array.array("d", npnew)
121
+ return buf
122
+
123
+
124
+ def reverse(buf):
125
+ """Return backwards copy of the buffer"""
126
+ nparray = np.array(buf)
127
+ npnew = np.flip(nparray)
128
+ buf = array.array("d", npnew)
129
+ return buf
130
+
131
+ def combine(buf1, buf2):
132
+ """Combine 2 buffers giving average amplitude for each sample
133
+
134
+ N.b. buffers must be of identical length"""
135
+ nparray1 = np.array(buf1)
136
+ nparray2 = np.array(buf2)
137
+ npsummed = nparray1 + nparray2 / 2
138
+ buf = array.array("d", npsummed)
139
+ return buf
140
+
141
+
142
+ def constrain(buf, lower, upper):
143
+ """Clip a buffer to an upper and lower constraint"""
144
+ nparray = np.array(buf)
145
+ np.clip(nparray, lower, upper, nparray)
146
+ buf = array.array("d", nparray)
147
+ return buf
148
+
149
+
150
+ def vmove(buf, amount):
151
+ """Shift samples in the Y axis"""
152
+ nparray = np.array(buf)
153
+ nparray += amount
154
+ buf = array.array("d", nparray)
155
+ return buf
156
+
157
+
158
+ def fade(buf, a1, a2):
159
+ """Change start and end amplitude and scale smoothly between"""
160
+ buf = boost(buf, a1)
161
+ ms = (len(buf) / SAMPLE_RATE) * 1000.0
162
+ envelope(buf, 0, ms, a2 / a1, 0)
163
+ return buf
164
+
165
+
166
+ def noisalise(buf, vol = 1.0, gain = 1.0):
167
+ """Apply gain to buffer proportional to mean amplitude, useful for restoring a constant RMS after processing noise"""
168
+ data = np.array(buf)
169
+ av = (data[data > 0].mean() + -data[data < 0].mean()) / 2.0
170
+ r = gain / (av / 2.0)
171
+ for i in range(len(buf)):
172
+ buf[i] = _limit(buf[i], limit=vol / gain) * r * vol
173
+ return
174
+
175
+
176
+ def slice(buf, start, end):
177
+ """Extract a section of a buffer with start and end times in ms"""
178
+ s = int(SAMPLE_RATE * start / 1000.0)
179
+ e = min(len(buf), int(SAMPLE_RATE * end / 1000.0))
180
+ return buf[s:e]
181
+
182
+
183
+ def median_filter(buf):
184
+ """Perform a median filter"""
185
+ return array.array("d", medfilt(buf, kernel_size=5))
186
+
187
+
188
+ def lowpass_filter(buf, cutoff, order=6):
189
+ """Apply a filter that progressively removes frequencies above the cutoff"""
190
+ return _butterworth_filter(buf, cutoff, type='lowpass', order=order)
191
+
192
+
193
+ def highpass_filter(buf, cutoff, order=6):
194
+ """Apply a filter that progressively removes frequencies below the cutoff"""
195
+ return _butterworth_filter(buf, cutoff, type='highpass', order=order)
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Functions to create new buffers
200
+ # ---------------------------------------------------------------------------
201
+
202
+
203
+ def make_sawtooth(ms, vol=0.2, freq=140.0):
204
+ """Make a sawtooth wave"""
205
+ n = int(SAMPLE_RATE * ms / 1000.0)
206
+ buf = array.array("d")
207
+ for i in range(n):
208
+ buf.append(_samplept(sawtooth, i, freq) * vol)
209
+ return buf
210
+
211
+
212
+ def make_triangle(ms, vol=0.2, freq=140.0):
213
+ """Make a triangle wave"""
214
+ n = int(SAMPLE_RATE * ms / 1000.0)
215
+ buf = array.array("d")
216
+ for i in range(n):
217
+ buf.append(_samplept(triangle, i, freq) * vol)
218
+ return buf
219
+
220
+
221
+ def make_square(ms, vol=0.2, freq=140.0):
222
+ """Make a square wave"""
223
+ n = int(SAMPLE_RATE * ms / 1000.0)
224
+ buf = array.array("d")
225
+ for i in range(n):
226
+ buf.append(_samplept(square, i, freq) * vol)
227
+ return buf
228
+
229
+
230
+ def make_sin(ms=600, vol=0.4, freq=50.0, harmonics=0):
231
+ """Make a sine wave"""
232
+ n = int(SAMPLE_RATE * ms / 1000.0)
233
+ buf = array.array("d")
234
+ for i in range(n):
235
+ a = _samplept(sin, i, freq, harmonics)
236
+ buf.append(a * vol)
237
+ return buf
238
+
239
+
240
+ def make_white(ms=600, vol=0.4, smooth=False):
241
+ """Fill a buffer with random amplitudes to produce white noise
242
+
243
+ If smooth is False use values of 1.0 or -1.0 randomly to produce white noise similar to the SN76477 Complex Sound Generator chip,
244
+ otherwise generate purely random amplitudes which gives a more natural noise.
245
+ """
246
+ n = int(SAMPLE_RATE * ms / 1000.0)
247
+ buf = array.array("d")
248
+ random.seed(0)
249
+ rng = random.Random()
250
+ for _ in range(n):
251
+ r = rng.uniform(-1.0, 1.0) * vol
252
+ buf.append(r if smooth else 1.0 if r > 0.0 else -1.0)
253
+ return buf
254
+
255
+
256
+ def make_brown(ms=600, vol=0.1, freq=150.0, order=1):
257
+ """Apply a lowpass filter to white noise to produce brown noise"""
258
+ buf = make_white(ms, vol)
259
+ buf = lowpass_filter(buf, freq, order)
260
+ return buf
261
+
262
+
263
+ def make_sweep(ms=80, startf=1500.0, endf=400.0, shape=triangle, vol=1.0, repeat=0, harmonics=0, special=None):
264
+ """Create a buffer that starts at frequency startf and ends at endf
265
+
266
+ If repeat is not zero the cycle goes from startf to endf and back to startf in repeat milliseconds and
267
+ continues to cycle until ms milliseconds have been filled.
268
+ """
269
+ n = int(SAMPLE_RATE * ms / 1000.0)
270
+ loop = n * 2 if repeat == 0 else int(SAMPLE_RATE * repeat / 1000.0)
271
+ period = loop // 2
272
+ buf = array.array("d")
273
+ phase = [0.0 for _ in range(harmonics + 1)]
274
+ for i in range(n):
275
+ x = i % loop
276
+ if x > period:
277
+ x = loop - x
278
+ else:
279
+ pass
280
+ f = (startf - ((startf - endf) * (x / period) ))
281
+ a, phase = _samplept(shape, i, f, harmonics, phase, special)
282
+ buf.append(a * vol)
283
+ return buf
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # Functions to manipulate buffers in place
288
+ # ---------------------------------------------------------------------------
289
+
290
+
291
+ def tremolo(buf, freq, intensity, shape=sin):
292
+ """Modify a buffer in place so that amplitude increases and decreases according to shape and intensity"""
293
+ for i in range(len(buf)):
294
+ phase = i * math.pi * freq / SAMPLE_RATE
295
+ buf[i] *= (1.0 - intensity) + abs(((shape(phase) + 1.0) / 2.0) * intensity)
296
+ return
297
+
298
+
299
+ def envelope(buf, attack, decay, sustain, release, start=0, end=MAX_LEN):
300
+ """Modify a buffer in place with an ADSR envelope"""
301
+ attack_start = int(SAMPLE_RATE * start / 1000.0)
302
+ attack_end = min(len(buf), int(SAMPLE_RATE * (start + attack) / 1000.0))
303
+ decay_end = min(len(buf), int(SAMPLE_RATE * decay / 1000.0) + attack_end)
304
+ release_end = min(len(buf), int(SAMPLE_RATE * end / 1000.0))
305
+ sustain_end = min(len(buf), release_end - int(SAMPLE_RATE * release / 1000.0))
306
+ if attack != 0.0:
307
+ for i in range(attack_start, attack_end):
308
+ buf[i] *= (i - attack_start) / (attack_end - attack_start)
309
+ for i in range(attack_end, decay_end):
310
+ buf[i] *= ((decay_end - i) / (decay_end - attack_end)) * (1.0 - sustain) + sustain
311
+ for i in range(decay_end, sustain_end):
312
+ buf[i] *= sustain
313
+ for i in range(sustain_end, release_end):
314
+ buf[i] *= ((release_end - i) / (release_end - sustain_end)) * sustain
315
+ return
316
+
317
+
318
+ def echo(buf, ms, vol):
319
+ """Modify buffer in place to add a short delay to the sound"""
320
+ delay = int(SAMPLE_RATE * ms / 1000.0)
321
+ for _ in range(delay):
322
+ buf.append(0.0)
323
+ for i in range(len(buf)):
324
+ buf[i] *= 1 - vol
325
+ if i >= delay:
326
+ buf[i] += buf[i - delay] * vol
327
+ return
@@ -0,0 +1,75 @@
1
+ """Asteroids sound effects"""
2
+ from .synth import *
3
+
4
+
5
+ def as_playerfire():
6
+ """Asteroids player firing sound"""
7
+ ms = 260
8
+ buf = make_sweep(ms, 820, 100, square, 1.0, 0, special=-0.8)
9
+ envelope(buf, 0, ms, 0.2, 0)
10
+ envelope(buf, 0, 100, 0.5, 0, 0, 100)
11
+ envelope(buf, 0, 0, 0.5, 0, 100, ms)
12
+ return tobytes(buf)
13
+
14
+
15
+ def as_ufofire():
16
+ """Asteroids flying saucer firing sound"""
17
+ ms = 260
18
+ buf = make_sweep(ms, 820, 650, square, 1.0, 0, special=-0.8)
19
+ return tobytes(buf)
20
+
21
+
22
+ def as_bump(freq = 81.516):
23
+ """Asteroids periodic heartbeat bump
24
+
25
+ Expected frequencies in Hz, 81.516, 87.500
26
+ """
27
+ buf = make_square(50, 1.0, freq)
28
+ return tobytes(buf)
29
+
30
+
31
+ def as_explode(freq = 200.0):
32
+ """Asteroids things blowing up
33
+
34
+ Estimated frequencies:
35
+ Large rock 170 Hz
36
+ Medium Rock 220 Hz
37
+ Small Rock 270 Hz
38
+
39
+ """
40
+ ms = 1000
41
+ buf = make_white(ms, 1.0)
42
+ buf = lowpass_filter(buf, freq, order=1)
43
+ noisalise(buf, gain=0.1)
44
+ envelope(buf, 20, 0, 1.0, ms)
45
+ return tobytes(buf)
46
+
47
+
48
+ def as_ufo(ms, low, high):
49
+ """Asteroids flying saucer on screen noise
50
+
51
+ Large 180, 600, 1400
52
+ Small 120, 800, 1600
53
+ """
54
+ buf = make_sweep(ms, low, high, triangle, 0.5, ms)
55
+ return tobytes(buf)
56
+
57
+
58
+ def as_thrust():
59
+ """Asteroids player ship thrusting"""
60
+ buf = make_brown(1000, 0.5, 30, 1)
61
+ noisalise(buf, gain=0.12)
62
+ buf = median_filter(buf)
63
+ reversebuf = reverse(buf)
64
+ buf = buf + reversebuf
65
+ return tobytes(buf)
66
+
67
+
68
+ def as_extralife():
69
+ """Asteroids extra life notification"""
70
+ buf = make_sin(1300, 0.5, 2940)
71
+ tremolo(buf, 16.8, 1.0, square)
72
+ return tobytes(buf)
73
+
74
+
75
+
@@ -0,0 +1,30 @@
1
+ """Generic retro sound effects"""
2
+ from .synth import *
3
+
4
+
5
+ def eg_gunshot():
6
+ """Retro gunshot sound"""
7
+ ms = 500
8
+ buf = make_white(ms, 1.0)
9
+ buf = lowpass_filter(buf, 1200.0, order=4)
10
+ noisalise(buf, gain=0.5)
11
+ envelope(buf, 20, 30, 0.2, ms - 10)
12
+ return tobytes(buf)
13
+
14
+
15
+ def eg_boom():
16
+ """A booming explosion"""
17
+ ms = 1500
18
+ buf = make_white(ms, 1.0)
19
+ buf = lowpass_filter(buf, 500.0, order=4)
20
+ noisalise(buf, gain=0.5)
21
+ envelope(buf, 20, 30, 0.2, ms - 10)
22
+ return tobytes(buf)
23
+
24
+
25
+ def eg_zap():
26
+ """The sound of a Kill-o-Zap gun, probably"""
27
+ ms = 200
28
+ buf = make_sweep(ms, 1000, 200, triangle, 1.0, 0)
29
+ envelope(buf, 20, 30, 0.2, ms - 10)
30
+ return tobytes(buf)
@@ -0,0 +1,58 @@
1
+ """Basic sample sounds"""
2
+ from .synth import *
3
+
4
+
5
+ def sawtooth_wave(ms=1600, vol=1.0, freq=150.0):
6
+ """Return bytes representing a simple sawtooth wave"""
7
+ buf = make_sawtooth(ms, vol, freq)
8
+ return tobytes(buf)
9
+
10
+
11
+ def triangle_wave(ms=1600, vol=1.0, freq=150.0):
12
+ """Return bytes representing a simple triangle wave"""
13
+ buf = make_triangle(ms, vol, freq)
14
+ return tobytes(buf)
15
+
16
+
17
+ def sine_wave(ms=600, vol=1.0, freq=150.0):
18
+ """Return bytes representing a simple sine wave"""
19
+ buf = make_sin(ms, vol, freq)
20
+ return tobytes(buf)
21
+
22
+
23
+ def square_wave(ms=90, vol=0.4, freq=144.0):
24
+ """Return bytes representing a simple square wave"""
25
+ buf = make_square(ms, vol, freq)
26
+ return tobytes(buf)
27
+
28
+
29
+ def brown_noise(ms=600, vol=0.4, freq=150.0):
30
+ """Return bytes representing brown noise"""
31
+ buf = make_brown(ms, vol, freq)
32
+ return tobytes(buf)
33
+
34
+
35
+ def white_noise(ms=600, vol=0.4, smooth=False):
36
+ """Return bytes representing white noise"""
37
+ buf = make_white(ms, vol, smooth)
38
+ return tobytes(buf)
39
+
40
+
41
+ def sweep(ms=80, startf=1500.0, endf=100.0, shape=sin, vol=1.0, repeat=0):
42
+ """Return bytes representing a frequency sweep from startf to endf"""
43
+ buf = make_sweep(ms, startf, endf, shape, vol, repeat)
44
+ return tobytes(buf)
45
+
46
+
47
+ def chord(ms=600, vol=0.4, f1=261.6256, f2=329.6276):
48
+ """Return bytes representing a 2 note chord"""
49
+ buf1 = make_square(ms, vol, freq=f1)
50
+ buf2 = make_square(ms, vol, freq=f2)
51
+ buf1 = combine(buf1, buf2)
52
+ return tobytes(buf1)
53
+
54
+
55
+ def fadeup(ms=999, a1=0.2, a2=0.8):
56
+ buf = make_triangle(ms, 1.0, 261.6256)
57
+ buf = fade(buf, a1, a2)
58
+ return tobytes(buf)
@@ -0,0 +1,120 @@
1
+ """Space Invaders sound effects"""
2
+ from .synth import *
3
+
4
+
5
+ def si_extralife():
6
+ """Space Invaders extra life notification"""
7
+ ms = 1360
8
+ buf1 = make_triangle(ms, 1.0, 490)
9
+ buf2 = make_sawtooth(ms, 1.0, 490)
10
+ buf = combine(buf1, buf2)
11
+ tremolo(buf, 120.0, 0.9, shape=triangle)
12
+ buf = boost(buf, 5.0)
13
+ envelope(buf, 20, 0, 1.0, 20, end=200)
14
+ for i in range(8):
15
+ s = i * 145 + 200
16
+ e = s + 135
17
+ mute = e + 10
18
+ envelope(buf, 30, 0, 0.9, 30, start=s, end=e)
19
+ envelope(buf, 0, 0, 0.0, 0, start=e, end=mute)
20
+ echo(buf, 30, 0.4)
21
+ return tobytes(buf)
22
+
23
+
24
+ def si_ufodie():
25
+ """Space Invaders UFO exploding sound"""
26
+ low = 150.0
27
+ high = 1320.0
28
+ buf = make_sweep(2210, low, high, triangle, 0.5, 170)
29
+ envelope(buf, 10, 0, 1.0, 350)
30
+ echo(buf, 20, 0.3)
31
+ return tobytes(buf)
32
+
33
+
34
+ def si_ufo():
35
+ """Space Invaders UFO on screen sound"""
36
+ low = 400.0
37
+ high = 1550.0
38
+ buf = make_sweep(170, low, high, square, 0.4, 170)
39
+ return tobytes(buf)
40
+
41
+
42
+ def si_aliendie():
43
+ """Space Invaders enemy exploding sound"""
44
+ buf = array.array("d")
45
+ params = {
46
+ "amplitude" : (0.7, 0.7, 0.4, 0.3, 0.25),
47
+ "length" : (80, 80, 80, 100, 120)
48
+ }
49
+ for i in range(len(params["amplitude"])):
50
+ s = 1500.0 if i < len(params["amplitude"]) - 1 else 1900.0
51
+ f = 200.0 if 1 < len(params["amplitude"]) - 1 else 1100.0
52
+ tempbuf = make_sweep(params["length"][i], s, f, triangle, params["amplitude"][i], 0)
53
+ envelope(tempbuf, 5, 0, 1.0, 5)
54
+ buf += tempbuf
55
+ envelope(buf, 5, 0, 1.0, 300)
56
+ echo(buf, 20, 0.3)
57
+ return tobytes(buf)
58
+
59
+
60
+ def si_explosion():
61
+ """Space Invaders player exploding sound"""
62
+ ms = 1600
63
+ buf = make_white(ms, 1.0)
64
+ buf = lowpass_filter(buf, 350.0, order=1)
65
+ noisalise(buf, gain=0.35)
66
+ buf = lowpass_filter(buf, 2600.0)
67
+ buf = median_filter(buf)
68
+ buf = lowpass_filter(buf, 2600.0)
69
+ envelope(buf, 20, 0, 1.0, 20, start=0, end=100)
70
+ envelope(buf, 20, 0, 1.0, 20, start=100, end=180)
71
+ envelope(buf, 5, 0, 0.1, 5, start=180, end=200)
72
+ envelope(buf, 20, 0, 1.0, 20, start=200, end=250)
73
+ envelope(buf, 20, 0, 1.0, 20, start=250, end=300)
74
+ envelope(buf, 20, 0, 0.6, 20, start=300, end=400)
75
+ envelope(buf, 20, 0, 1.0, 20, start=400, end=450)
76
+ envelope(buf, 20, 0, 1.0, 20, start=450, end=500)
77
+ envelope(buf, 20, 20, 0.6, 500, start=500)
78
+ echo(buf, 80, 0.5)
79
+ return tobytes(buf)
80
+
81
+
82
+ def si_playerfire():
83
+ """Space Invaders player firing sound"""
84
+ ms = 400
85
+ freq = 2490.0
86
+ buf = make_white(ms, 1.0)
87
+ start = slice(buf, 0, 300)
88
+ for _ in range(5):
89
+ start = lowpass_filter(start, freq, order=6)
90
+ start = highpass_filter(start, freq, order=6)
91
+ mid = slice(buf, 300, 350)
92
+ for _ in range(5):
93
+ mid = lowpass_filter(mid, freq - 75, order=6)
94
+ mid = highpass_filter(mid, freq - 75, order=6)
95
+ end = slice(buf, 350, 400)
96
+ for _ in range(5):
97
+ end = lowpass_filter(end, freq - 150, order=6)
98
+ end = highpass_filter(end, freq - 150, order=6)
99
+ buf = start + mid + end
100
+ noisalise(buf, gain=0.15)
101
+ envelope(buf, 10, 0, 1.0, 100)
102
+ echo(buf, 40, 0.4)
103
+ return tobytes(buf)
104
+
105
+
106
+ def si_march(freq=53.650):
107
+ """Space Invaders low-frequency marching sound
108
+
109
+ Expected frequencies in Hz, 53.650, 50.114, 46.178, 62.465
110
+ """
111
+ ms = 80
112
+ buf = make_sin(ms, 1.0, freq)
113
+ buf = constrain(buf, 0.5, 1.0)
114
+ buf = vmove(buf, -0.6)
115
+ buf = median_filter(buf)
116
+ envelope(buf, 5, 0, 1.0, 5)
117
+ echo(buf, 20, 0.3)
118
+ return tobytes(buf)
119
+
120
+
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcadesynth
3
+ Version: 1.0.1
4
+ Summary: Programmatically synthesize retro arcade sound effects
5
+ Author-email: "C. Martin Richardson" <c.mart.richardson@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 C. Martin Richardson
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/martianmr/arcadesynth
29
+ Keywords: arcade,synth,retro
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Programming Language :: Python
32
+ Classifier: Programming Language :: Python :: 3
33
+ Requires-Python: >=3.9
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: scipy>=1.17.1
37
+ Requires-Dist: tomli; python_version < "3.11"
38
+ Provides-Extra: dev
39
+ Requires-Dist: pygame; extra == "dev"
40
+ Requires-Dist: bumpver; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # arcadesynth 1.0.1
44
+
45
+ Programmatically synthesize retro arcade sound effects with arcadesynth
46
+
47
+ This utility can be used to generate your own custom sound effects in code as an alternative to using sound assets. It also provides some hand-crafted sounds from the Golden age of arcade video games.
48
+
49
+ The idea was inspired by sound generation on 1980s 8-bit microcomputers which sometimes had fairly sophisticated synthesizer chips which could be programmed at a low level with a bit of know-how. The algorithms are kept fairly simple but are capable of approximating early arcade, console and computer game sounds that are generated with specialized chips or custom electronics.
50
+
51
+ ## Installation
52
+
53
+ arcadesynth can be installed via pip
54
+
55
+ ```bash
56
+ pip install arcadesynth
57
+ ```
58
+
59
+ ## Modules
60
+
61
+ - synth - functions to build your own effects
62
+ - synth_sample - some simple basic sounds
63
+ - synth_example - some generic retro sound effects
64
+ - synth_asteroids - sounds from the 1979 Atari game Asteroids
65
+ - synth_spaceinvaders - sounds from the 1978 Taito game Asteroids
66
+
67
+ ## Synth
68
+
69
+ This module can be used to create custom sound effects by generating waves or white noise and then manipulating the signal with envelopes, filters and more.
70
+
71
+ E.g. synthesize a retro gunshot sound in pygame.
72
+
73
+ ```python
74
+ import pygame
75
+ from arcadesynth import synth
76
+
77
+ pygame.init()
78
+ pygame.mixer.init()
79
+ ms = 500 # Length of buffer will be 500 milliseconds
80
+ buf = synth.make_white(ms, 1.0) # Fill buffer with white noise
81
+ buf = synth.lowpass_filter(buf, 1200.0, order=4) # Apply a filter to remove higher frequencies
82
+ synth.noisalise(buf, gain=0.5) # Even out the amplitude of the noise
83
+ synth.envelope(buf, 20, 30, 0.2, ms - 10) # Apply an envelope with a short attack and release and then fade off from 20% amplitude
84
+ sound = pygame.mixer.Sound(synth.tobytes(buf))
85
+ sound.play()
86
+ ```
87
+
88
+ ## Space Invaders
89
+
90
+ There are some built-in hand-crafted synthesized sounds including those from the 1978 classic Space Invaders by Taito. Sounds in the arcade machine are produced on a Texas Instruments SN76477 'complex sound generator' chip supported by custom electronics.
91
+
92
+ E.g. synthesize the sound made by the flying saucer blowing up in Space Invaders.
93
+
94
+ ```python
95
+ import pygame
96
+ from arcadesynth import synth_spaceinvaders as si
97
+
98
+ pygame.init()
99
+ pygame.mixer.init()
100
+ sound = pygame.mixer.Sound(si.si_ufodie())
101
+ sound.play()
102
+ ```
103
+
104
+ ## LICENSE
105
+
106
+ MIT License — See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/arcadesynth/__init__.py
6
+ src/arcadesynth/config.toml
7
+ src/arcadesynth/synth.py
8
+ src/arcadesynth/synth_asteroids.py
9
+ src/arcadesynth/synth_example.py
10
+ src/arcadesynth/synth_sample.py
11
+ src/arcadesynth/synth_spaceinvaders.py
12
+ src/arcadesynth.egg-info/PKG-INFO
13
+ src/arcadesynth.egg-info/SOURCES.txt
14
+ src/arcadesynth.egg-info/dependency_links.txt
15
+ src/arcadesynth.egg-info/requires.txt
16
+ src/arcadesynth.egg-info/top_level.txt
@@ -0,0 +1,8 @@
1
+ scipy>=1.17.1
2
+
3
+ [:python_version < "3.11"]
4
+ tomli
5
+
6
+ [dev]
7
+ pygame
8
+ bumpver
@@ -0,0 +1 @@
1
+ arcadesynth