diff --git a/sfx.py b/sfx.py index c8ceca5..df297ec 100644 --- a/sfx.py +++ b/sfx.py @@ -163,6 +163,25 @@ class Synth: self._build_music() # ------------------------------------------------------------------ music + # A real melody! Public-domain "Twinkle Twinkle Little Star" (Mozart's + # variations), arranged as a gentle 4-bar loop with a soft bass line. + # Fits the star-lane theme; unmistakably a tune. + # Each bar is 8 eighth-note slots; attacks are (note, n_slots, octave). + MELODY = [ + # Twinkle, twinkle, little star + [("C", 1, 4), ("C", 1, 4), ("G", 1, 4), ("G", 1, 4), + ("A", 1, 4), ("A", 1, 4), ("G", 2, 4)], + # how I wonder what you are + [("F", 1, 4), ("F", 1, 4), ("E", 1, 4), ("E", 1, 4), + ("D", 1, 4), ("D", 1, 4), ("C", 2, 4)], + # up above the world so high / like a diamond in the sky + [("G", 1, 4), ("G", 1, 4), ("F", 1, 4), ("F", 1, 4), + ("E", 1, 4), ("E", 1, 4), ("D", 2, 4)], + [("G", 1, 4), ("G", 1, 4), ("F", 1, 4), ("F", 1, 4), + ("E", 1, 4), ("E", 1, 4), ("D", 2, 4)], + ] + BASS_ROOTS = ["C", "F", "C", "C"] # harmony for each of the 4 bars + def _music_buffer(self, tracks, total_s): """Render time-placed notes (start_s, note, octave, dur_s, vol, shape) into one softly-mixed, normalized buffer.""" @@ -179,53 +198,54 @@ class Synth: sc = 28000.0 / peak if peak > 28000 else 1.0 return array.array("h", (int(v * sc) for v in buf)) - def _build_music(self): - """Two short generative loops, deliberately sparse & soft: a calm menu - loop and a gentle bouncy play loop. (Square waves and hats are used in - SFX only - never in the music - to keep it from getting harsh.)""" - - # ---- menu: dreamy, mostly rests, C - Am - F - G ------------------------ - seq = [ - (("C", 2), ["C", "E", "G", "D"], "E"), - (("A", 2), ["A", "C", "E", "B"], "C"), - (("F", 2), ["F", "A", "C", "G"], "A"), - (("G", 2), ["G", "B", "D", "A"], "B"), - ] + def _render_loop(self, eighth_s, lead_vol, lead_shape, bass_vol, + offset_s=0.0, bars=None, melody_oct=4, with_bass=True): + """Schedule the MELODY (per-bar eighth attacks) plus a soft oom-pah + bass. `offset_s` shifts the start (used for B sections / echoes); + `bars` picks which MELODY bars to play in order.""" + bars = bars if bars is not None else list(range(4)) tracks = [] - CH = 1.8 - for ci, (bass, arp, twinkle) in enumerate(seq): - t = ci * CH - tracks.append((t, bass[0], bass[1], CH * 0.72, 0.12, "tri")) # bass - for k in range(4): # arpeggio - oc = 4 if k < 2 else 5 - tracks.append((t + 0.35 + k * 0.26, arp[k], oc, 0.24, 0.08, "tri")) - tracks.append((t + 1.3, twinkle, 5, 0.55, 0.045, "tri")) # twinkle - menu_total = 4 * CH + 0.8 + for idx, bi in enumerate(bars): + bar_start = offset_s + idx * 8 * eighth_s + if with_bass: + root = self.BASS_ROOTS[bi % len(self.BASS_ROOTS)] + fifth = "G" if root == "C" else "C" + tracks.append((bar_start, root, 2, eighth_s * 4.2, bass_vol, "tri")) + tracks.append((bar_start + eighth_s * 4, fifth, 2, + eighth_s * 4.2, bass_vol, "tri")) + slot = 0 + for (nm, nslots, oc) in self.MELODY[bi % len(self.MELODY)]: + t = bar_start + slot * eighth_s + tracks.append((t + eighth_s * 0.08, nm, oc + melody_oct - 4, + eighth_s * nslots * 0.9, lead_vol, lead_shape)) + slot += nslots + return tracks + + def _build_music(self): + """A real tune: public-domain "Twinkle Twinkle Little Star" arranged + in C - soft sine lullaby on the title, bouncy triangle version during + play. Gentle oom-pah bass, nothing harsh, no percussion.""" + + # ---- menu: slow lullaby ---------------------------------------------- + e = 0.30 # one bar = 8 * 0.30 = 2.4 s + tracks = self._render_loop(e, 0.085, "sine", 0.055) + menu_total = 4 * 8 * e + 2.6 # 4 bars + tail self._menu_music = self._music_buffer(tracks, menu_total) - # ---- play: bouncy but gentle, 16 eighth-notes @ ~0.30s ---------------- - step = 0.30 - bass_seq = [("C", 2), ("C", 2), ("G", 2), ("G", 2), - ("A", 2), ("A", 2), ("F", 2), ("G", 2), - ("C", 2), ("C", 2), ("G", 2), ("G", 2), - ("A", 2), ("A", 2), ("G", 2), ("C", 2)] - lead_seq = [None, None, ("E", 4), None, ("G", 4), None, ("E", 4), None, - ("D", 4), None, ("F", 4), None, ("A", 4), None, ("G", 4), None] - tracks = [] - for i in range(16): - t = i * step - if i % 2 == 0: # bass on the beat - bn, bo = bass_seq[i] - tracks.append((t, bn, bo, step * 1.25, 0.15, "tri")) - if lead_seq[i]: # light melody - nm, oc = lead_seq[i] - tracks.append((t + 0.02, nm, oc, step * 0.85, 0.085, "tri")) - play_total = 16 * step + 1.2 + # ---- play: bouncy version, with a brighter octave-up repeat ---------- + e2 = 0.225 # ~130 bpm eighth feel + tracks = self._render_loop(e2, 0.115, "tri", 0.07) + # B section: same tune one octave up, lighter, then the loop restarts + tracks += self._render_loop(e2, 0.045, "sine", 0.0, + offset_s=4 * 8 * e2, bars=[0, 1, 2, 3], + melody_oct=5, with_bass=False) + play_total = 2 * 4 * 8 * e2 + 1.6 # two passes, ~16 s of music self._play_music = self._music_buffer(tracks, play_total) + self._menu = pygame.mixer.Sound(buffer=self._menu_music.tobytes()) self._play = pygame.mixer.Sound(buffer=self._play_music.tobytes()) - self._menu.set_volume(0.40) - self._play.set_volume(0.30) + self._menu.set_volume(0.42) + self._play.set_volume(0.34) # ---- playback -------------------------------------------------------- def play(self, name, vol=1.0, pitch_var=0):