-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerate_starter_sound.py
More file actions
270 lines (231 loc) · 12.1 KB
/
generate_starter_sound.py
File metadata and controls
270 lines (231 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
#!/usr/bin/env python3
"""
Procedural Electric Starter Motor Sound Generator
Rewritten to match p5.js acoustic character using clean harmonic oscillators:
- Three-oscillator architecture (mainOsc, gearOsc, humOsc)
- Sine wave fundamental (40→400 Hz scaled to 12V specs)
- Triangle wave 2x gear harmonic (metallic character)
- Sine wave 0.5x sub-harmonic hum (deep resonance)
- Smooth frequency ramping (no aliasing artifacts)
- 42% broadband mechanical noise (unchanged, working well)
- 12V automotive parameters with voltage sag and load pulses
"""
import numpy as np
from scipy.io import wavfile
from scipy.signal import sawtooth
import subprocess
import sys
# ============ PARAMETERS ============
SAMPLE_RATE = 48000 # Hz
DURATION = 5.5 # seconds
# Motor speed progression - 12V automotive starter specifications
# Reference: Typical car starters (Nippondenso, Bosch, Hitachi)
RPM_START = 0 # Off state
RPM_ENGAGE = 30 # Initial solenoid engagement kick (still spinning up)
RPM_TARGET = 250 # Cranking speed - typical 12V starter on loaded engine
ENGAGE_TIME = 0.15 # Solenoid click duration (150ms, electromagnetic pull-in)
RAMPUP_TIME = 2.5 # Time to reach target from engagement
# ============ THREE-OSCILLATOR FREQUENCY MAPPING ============
# Key insight: Starter motor shaft spins at ~3,600 RPM, not engine cranking speed
# Engine cranking: 200-300 RPM (what we see in engine-sim)
# Starter motor shaft: ~3,600 RPM (14.4:1 gear reduction from Hitachi/Bosch specs)
# The high motor speed creates the high-pitched whine we hear
#
# Gear ratio calculation: 3600 RPM motor ÷ 250 RPM engine = 14.4x
# This explains the acoustic character: motor shaft harmonic dominates
# Scaling: Starter motor shaft RPM (not engine RPM)
# Engine cranking RPM -> Motor shaft RPM: multiply by gear ratio (14.4)
# At 250 RPM engine: motor runs at 3,600 RPM
# At 30 RPM engine: motor runs at 432 RPM
#
# New frequency mapping:
# - mainOsc at 250 RPM engine = motor at 3600 RPM
# - Old base was 100 Hz @ 250 RPM engine
# - New base should reflect actual motor: ~580 Hz @ 3600 RPM (proper starter whine)
# - This gives us: f_main = 580 * (motor_rpm / 3600)
MOTOR_GEAR_RATIO = 14.4 # Starter motor shaft speed / engine speed ratio
MOTOR_RPM_TARGET = 3600.0 # Typical starter motor shaft speed at full load
MAIN_OSC_BASE = 580.0 # Hz at 3600 RPM motor shaft (high-pitched whine characteristic)
GEAR_OSC_RATIO = 2.0 # 2x fundamental = metallic gear-teeth effect
HUM_OSC_RATIO = 0.5 # 0.5x fundamental = deep sub-harmonic resonance
# Oscillator amplitude mix (from p5.js reference normalized)
MAIN_OSC_AMP = 0.35 # Fundamental whine (clean sine)
GEAR_OSC_AMP = 0.25 # Metallic 2x harmonic (triangle wave = harsh)
HUM_OSC_AMP = 0.18 # Deep sub-harmonic (clean sine, adds richness)
# Total harmonic energy: 0.78 (leaves 0.22 for broadband noise component)
# Load variations - compression strokes on engine vary RPM
# 4-cylinder at ~250 RPM cranking = ~8 Hz effective load frequency
LOAD_VARIATION_FREQ = 8.0 # Hz (compression pulse rate for 4-cylinder)
LOAD_VARIATION_DEPTH = 0.20 # 20% RPM dip under compression load
# Broadband mechanical noise - keeps the working well-tuned element
# Real motors are dominated by noise, not clean harmonics
NOISE_AMOUNT = 0.42 # 42% broadband noise (unchanged, verified working)
# ============ ELECTRICAL SPECIFICATIONS (12V DC STARTER) ============
# Simulates realistic voltage sag under load
VOLTAGE_NOMINAL = 12.0 # V
VOLTAGE_INITIAL = 11.5 # V (realistic cold start with battery resistance)
VOLTAGE_MIN_SAG = 10.8 # V (max sag under 400A load)
VOLTAGE_SAG_TIME = 1.0 # Voltage sag duration (1 second into cranking)
# ============ PHASE TRACKING FOR SMOOTH FREQUENCY RAMPING ============
# Key insight: Use phase accumulation for smooth, alias-free sweeps
main_phase = 0.0
gear_phase = 0.0
hum_phase = 0.0
dt = 1.0 / SAMPLE_RATE
# ============ GENERATION ============
print("Generating starter motor sound scaled by ACTUAL MOTOR SHAFT RPM (not engine RPM)...")
print(f"Duration: {DURATION}s @ {SAMPLE_RATE} Hz")
print(f"\nThree-Oscillator Architecture (Motor shaft speed based):")
print(f" mainOsc: sine 83→580 Hz (fundamental motor whine, motor-scaled)")
print(f" gearOsc: triangle 166→1160 Hz (2x metallic gear-teeth)")
print(f" humOsc: sine 41→290 Hz (0.5x deep sub-harmonic)")
total_samples = int(SAMPLE_RATE * DURATION)
audio = np.zeros(total_samples)
# Pre-allocate noise state for colored noise
noise_state = 0.0
for i in range(total_samples):
t = i / SAMPLE_RATE
# ---- RPM PROGRESSION ----
# Phase 1: Solenoid engagement (0-0.15s) - click/initial kick
if t < ENGAGE_TIME:
rpm = RPM_ENGAGE
# Soft start into engagement
engagement_progress = t / ENGAGE_TIME
engagement_envelope = np.sin(engagement_progress * np.pi / 2) ** 0.5
# Phase 2: Spin-up ramp (0.15s - 2.65s) - struggling under load
elif t < ENGAGE_TIME + RAMPUP_TIME:
progress = (t - ENGAGE_TIME) / RAMPUP_TIME
# Cubic easing for more natural acceleration
progress_eased = progress ** 1.5
rpm = RPM_ENGAGE + (RPM_TARGET - RPM_ENGAGE) * progress_eased
engagement_envelope = 1.0
# Phase 3: Steady cranking (2.65s+)
else:
rpm = RPM_TARGET
engagement_envelope = 1.0
# ---- VOLTAGE SAG SIMULATION (12V ELECTRICAL EFFECT) ----
# Battery voltage sags under high current draw of starter motor
if t < VOLTAGE_SAG_TIME:
# Voltage sag during heavy load phase
sag_progress = t / VOLTAGE_SAG_TIME
voltage_sag = np.cos(sag_progress * np.pi / 2) ** 1.5 # Exponential recovery
voltage = VOLTAGE_INITIAL + (VOLTAGE_MIN_SAG - VOLTAGE_INITIAL) * (1 - voltage_sag)
else:
# Voltage stabilizes after initial load phase
voltage = VOLTAGE_INITIAL + (VOLTAGE_NOMINAL - VOLTAGE_INITIAL) * 0.5
# Voltage affects motor performance (normalized 0-1, where 1.0 = 12V)
voltage_factor = voltage / VOLTAGE_NOMINAL # 0.9-1.0 typically
# ---- FREQUENCY MODULATION BY LOAD ----
# Pure cranking phase (0-3s): smooth, no load pulses for clean baseline
# Optional: load effects after firing (3+s) could be added here
# For now: keep smooth throughout for clean electrical whine character
load_mod = 1.0 # No 8 Hz load pulses during cranking phase
# Convert engine RPM to motor shaft RPM using gear ratio
motor_rpm = rpm * MOTOR_GEAR_RATIO
# Voltage sag reduces effective RPM (weaker motor under low voltage)
freq_modulated = (motor_rpm / MOTOR_RPM_TARGET) * MAIN_OSC_BASE * load_mod * (0.95 + 0.05 * voltage_factor)
# ---- GENERATE THREE-OSCILLATOR SIGNAL ----
# Smooth phase accumulation for alias-free frequency ramping
# mainOsc: Fundamental sine wave (1.0x ratio)
# Scales from ~16 Hz (at 0 RPM) to 100 Hz (at 250 RPM)
main_freq = freq_modulated
main_phase += 2 * np.pi * main_freq * dt
main_osc = np.sin(main_phase)
# gearOsc: Triangle wave at 2.0x fundamental (gear-teeth metallic character)
# Scales from ~32 Hz to 200 Hz - harsh metallic sound
gear_freq = freq_modulated * GEAR_OSC_RATIO
gear_phase += 2 * np.pi * gear_freq * dt
# Triangle wave from -pi to pi phase: sawtooth(x) produces triangle with proper band-limiting
# sawtooth(phase, width=0.5) gives symmetric triangle wave
gear_osc = sawtooth(gear_phase, width=0.5)
# humOsc: Sub-harmonic sine at 0.5x fundamental (deep resonance)
# Scales from ~8 Hz to 50 Hz - adds richness and body
hum_freq = freq_modulated * HUM_OSC_RATIO
hum_phase += 2 * np.pi * hum_freq * dt
hum_osc = np.sin(hum_phase)
# Combine three oscillators with their mixing ratios
harmonic_signal = (
MAIN_OSC_AMP * main_osc +
GEAR_OSC_AMP * gear_osc +
HUM_OSC_AMP * hum_osc
)
# ---- BROADBAND MECHANICAL NOISE ----
# White noise filtered for mechanical character (42% - unchanged, verified working)
noise_white = np.random.uniform(-1, 1)
# Colored noise by running sum (simple low-pass filter = pink-ish)
noise_colored = noise_white * 0.5 + noise_state * 0.5
noise_state = noise_colored
noise = noise_colored
# ---- COMBINE HARMONIC OSCILLATORS + BROADBAND NOISE ----
# Normalize harmonic energy (0.78) + broadband noise (0.42)
signal = (
harmonic_signal * (1.0 - NOISE_AMOUNT) + # Harmonics at ~58%
noise * NOISE_AMOUNT # Broadband noise at ~42%
)
# Voltage sag reduces amplitude (weaker electromagnetic force under low voltage)
signal *= (0.92 + 0.08 * voltage_factor) # 8% amplitude reduction at min voltage
# ---- ENVELOPE & AMPLITUDE SHAPING ----
# Fade in from solenoid engagement
if t < 0.2:
envelope = t / 0.2 # 200ms gentle fade in
elif t > DURATION - 0.3:
envelope = max(0.0, (DURATION - t) / 0.3) # 300ms fade out
else:
envelope = 1.0
# Apply engagement envelope (solenoid kick)
envelope *= engagement_envelope
# Amplitude ramp during spin-up (motor gets "louder" as it engages)
if t < ENGAGE_TIME + RAMPUP_TIME:
spin_up_progress = min(1.0, (t - ENGAGE_TIME) / (RAMPUP_TIME * 0.7))
amplitude_ramp = 0.3 + 0.7 * spin_up_progress # Start quiet, ramp to full
else:
amplitude_ramp = 1.0
audio[i] = signal * envelope * amplitude_ramp
# ---- POST-PROCESSING ----
# Normalize to 0.9 of max range (avoid clipping)
max_val = np.max(np.abs(audio))
if max_val > 0:
audio = audio / max_val * 0.85
# Convert to int16
audio_int16 = (audio * 32767).astype(np.int16)
# Save WAV
output_path = "/Users/danielsinclair/vscode/engine-sim-cli/generate_starter_sound.wav"
wavfile.write(output_path, SAMPLE_RATE, audio_int16)
print(f"\n✓ Generated: {output_path}")
print(f"\n12V AUTOMOTIVE STARTER SPECIFICATIONS:")
print(f" - Cranking RPM: {RPM_TARGET} RPM (typical 200-300 range for loaded engine)")
print(f" - Engagement: {ENGAGE_TIME*1000:.0f}ms solenoid click + spin-up")
print(f" - Ramp-up: {RAMPUP_TIME}s under load (engine compression resistance)")
print(f"\nELECTRICAL CHARACTERISTICS (12V DC):")
print(f" - Nominal voltage: {VOLTAGE_NOMINAL}V")
print(f" - Initial battery: {VOLTAGE_INITIAL}V (cold start with internal resistance)")
print(f" - Min sag under load: {VOLTAGE_MIN_SAG}V (typical 400A current draw)")
print(f" - Sag duration: {VOLTAGE_SAG_TIME}s (recovery as load stabilizes)")
print(f"\nFREQUENCY COMPONENTS (MOTOR SHAFT RPM BASED):")
print(f" - Motor gear ratio: {MOTOR_GEAR_RATIO}:1 (shaft speed ÷ engine speed)")
print(f" - Motor target: {MOTOR_RPM_TARGET:.0f} RPM @ {RPM_TARGET} RPM engine")
print(f" - mainOsc (sine): {MAIN_OSC_BASE:.0f} Hz @ {MOTOR_RPM_TARGET:.0f} RPM motor")
print(f" Range: ~{MAIN_OSC_BASE*30*MOTOR_GEAR_RATIO/MOTOR_RPM_TARGET:.0f} Hz (slow) to {MAIN_OSC_BASE:.0f} Hz (full cranking)")
print(f" - gearOsc (triangle, 2x): {MAIN_OSC_BASE*GEAR_OSC_RATIO:.0f} Hz @ {MOTOR_RPM_TARGET:.0f} RPM motor (metallic gear-teeth)")
print(f" - humOsc (sine, 0.5x): {MAIN_OSC_BASE*HUM_OSC_RATIO:.0f} Hz @ {MOTOR_RPM_TARGET:.0f} RPM motor (deep sub-harmonic)")
print(f"\nSOUND MIX (P5.JS ACOUSTIC ARCHITECTURE):")
print(f" - mainOsc (fundamental): {MAIN_OSC_AMP*100:.0f}% amplitude")
print(f" - gearOsc (metallic 2x): {GEAR_OSC_AMP*100:.0f}% amplitude")
print(f" - humOsc (deep 0.5x): {HUM_OSC_AMP*100:.0f}% amplitude")
print(f" - Broadband noise: {NOISE_AMOUNT*100:.0f}% (mechanical background)")
print(f" - Total harmonic: {(MAIN_OSC_AMP+GEAR_OSC_AMP+HUM_OSC_AMP)*100:.0f}%")
print(f"\nACOUSTIC CHARACTER:")
print(f" - Smooth frequency ramping (no aliasing artifacts)")
print(f" - Clean oscillator objects (sine + triangle waves)")
print(f" - Simple harmonic ratios (0.5x, 1.0x, 2.0x)")
print(f" - Realistic 12V automotive parameters with voltage sag")
print(f" - Pure cranking phase: NO 8 Hz load pulses (clean baseline)")
print(f" - SPL: 70-90 dB @ 1 meter (typical automotive starter)")
# Auto-play using afplay
try:
print(f"\nPlaying: {output_path}")
subprocess.run(["afplay", output_path], check=True)
except FileNotFoundError:
print("Warning: afplay not found. Install by running: brew install --formula sox")
except subprocess.CalledProcessError as e:
print(f"Error playing audio: {e}")