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.
- arcadesynth-1.0.1/LICENSE +21 -0
- arcadesynth-1.0.1/MANIFEST.in +3 -0
- arcadesynth-1.0.1/PKG-INFO +106 -0
- arcadesynth-1.0.1/README.md +64 -0
- arcadesynth-1.0.1/pyproject.toml +56 -0
- arcadesynth-1.0.1/setup.cfg +4 -0
- arcadesynth-1.0.1/src/arcadesynth/__init__.py +6 -0
- arcadesynth-1.0.1/src/arcadesynth/config.toml +1 -0
- arcadesynth-1.0.1/src/arcadesynth/synth.py +327 -0
- arcadesynth-1.0.1/src/arcadesynth/synth_asteroids.py +75 -0
- arcadesynth-1.0.1/src/arcadesynth/synth_example.py +30 -0
- arcadesynth-1.0.1/src/arcadesynth/synth_sample.py +58 -0
- arcadesynth-1.0.1/src/arcadesynth/synth_spaceinvaders.py +120 -0
- arcadesynth-1.0.1/src/arcadesynth.egg-info/PKG-INFO +106 -0
- arcadesynth-1.0.1/src/arcadesynth.egg-info/SOURCES.txt +16 -0
- arcadesynth-1.0.1/src/arcadesynth.egg-info/dependency_links.txt +1 -0
- arcadesynth-1.0.1/src/arcadesynth.egg-info/requires.txt +8 -0
- arcadesynth-1.0.1/src/arcadesynth.egg-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
arcadesynth
|