diff --git a/python-programming/fm_harmonics_music_generator.py b/python-programming/fm_harmonics_music_generator.py new file mode 100644 index 0000000..f35d0c2 --- /dev/null +++ b/python-programming/fm_harmonics_music_generator.py @@ -0,0 +1,170 @@ +# fm_harmonics_music_generator.py +# +# Created at 2025-01-01 +# +# Most of the code is AI generated, but the script works just fine. +# +# Unfortunately, I don't have enough time to add more typing now. +# +# Author: Intel A80486DX2-66 +# License: Creative Commons Zero 1.0 Universal or Unlicense +# SPDX-License-Identifier: CC0-1.0 OR Unlicense + +import numpy as np +from scipy.io.wavfile import write +from random import randint, seed, getrandbits, uniform + +def extend(l: list, n: int) -> list: + extended_list = [] # Create a new list to hold the extended values + for value in l: + extended_list.extend([value] * n) # Append the value n times + return extended_list + + +def generate_fm_wave(carrier_freq, modulator_freq, duration, sample_rate=44100, modulation_index=1.0): + """Generate a sine wave using FM synthesis.""" + + t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False) + modulating_signal = np.sin(2 * np.pi * modulator_freq * t) # Modulator signal + wave = 0.5 * np.sin(2 * np.pi * carrier_freq * t + modulation_index * modulating_signal) # FM synthesis + return wave + + +def apply_envelope(wave, attack_time, decay_time, sustain_level, release_time, sample_rate=44100): + """Apply an ADSR envelope to the wave.""" + + total_samples = len(wave) + attack_samples = int(attack_time * sample_rate) + decay_samples = int(decay_time * sample_rate) + release_samples = int(release_time * sample_rate) + + # Create the envelope + envelope = np.zeros(total_samples) + + # Attack + envelope[:attack_samples] = np.linspace(0, 1, attack_samples) + + # Decay + if attack_samples + decay_samples < total_samples: + envelope[attack_samples:attack_samples + decay_samples] = np.linspace(1, sustain_level, decay_samples) + + # Sustain + if attack_samples + decay_samples < total_samples: + envelope[attack_samples + decay_samples:total_samples - release_samples] = sustain_level + + # Release + if total_samples - release_samples > 0: + envelope[total_samples - release_samples:] = np.linspace(sustain_level, 0, release_samples) + + return wave * envelope + + +def generate_sine_wave(frequencies, duration, sample_rate=44100): + """Generate a sine wave for a list of frequencies.""" + + t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False) + wave = np.zeros_like(t) + for freq in frequencies: + wave += np.sin(2 * np.pi * freq * t) + return wave + + +def create_sound_file(patterns, repetitions, tempo, output_file, carrier, modulator, modulation_index): + """ + Create a sound file with specified frequency patterns, repetitions, and tempo. + + :param patterns: List of lists containing frequencies (in Hz) for each pattern. + :param repetitions: Number of times to repeat each pattern. + :param tempo: Tempo in beats per minute (BPM). + :param output_file: Name of the output WAV file. + """ + sample_rate = 44100 # Standard sample rate + beat_duration = 60 / tempo # Duration of one beat in seconds + sound_wave = np.array([]) # Initialize an empty array for the sound wave + + pattern_extension = 10 + extended_patterns = extend(patterns, pattern_extension) + + t = 0 + for i in range(repetitions): # Repeat the entire pattern + for pattern in extended_patterns: + actual_beat_duration = beat_duration / pattern_extension + + # Generate a sine wave for the current pattern + sine_wave = generate_sine_wave(pattern, actual_beat_duration) + + ## carrier=int(frequency / randint(1, 2)) + ## modulator=randint(int(frequency / 4), int(frequency / 2)) # uniform(frequency / 4, frequency / 2) + + fm_wave = generate_fm_wave(carrier, modulator, actual_beat_duration, modulation_index=modulation_index) + + # Combine the sine wave and FM wave + combined_wave = sine_wave + fm_wave + + # Apply envelope to the combined wave + ## combined_wave = apply_envelope(combined_wave, attack_time=0.01, decay_time=8, sustain_level=0.7, release_time=0.2, sample_rate=sample_rate) + + # Append the combined wave to the sound wave + sound_wave = np.concatenate((sound_wave, combined_wave)) + + t += 1 + + # Normalize the sound wave to the range of int16 + sound_wave = np.int16(sound_wave / np.max(np.abs(sound_wave)) * 32767) + + # Write the sound wave to a WAV file + write(output_file, sample_rate, sound_wave) + + +def harmonics(frequency, count: int, step: int = 1) -> list: + return [frequency * i for i in range(1, count, step)] + + +if __name__ == "__main__": + ## seed(0) + + tempo_multiplier = 4 + + randbool = lambda: getrandbits(1) == 1 + + min_frequency = 35 + max_frequency = 120 + frequency_limit_diff = max_frequency - min_frequency + frequency = randint(min_frequency, max_frequency) + n = 2**randint(1, 5) # 4/4 beat + frequency_subtract = frequency // frequency_limit_diff + + negative_frequency_drift = randbool() + precision = 2 + precision_machine = 10**precision + frequency_drift_toggle = randbool() + frequency_drift_max = randint(0, 1) if frequency_drift_toggle else 0 + frequency_drift_value = lambda: randint(-frequency_drift_max * precision_machine if negative_frequency_drift else 0, frequency_drift_max * precision_machine) / precision_machine + frequency_drift = lambda: frequency_drift_value() if randbool() else 0 + + carrier = uniform(frequency / 4, frequency * 2) + modulator = randint(0, frequency) + modulation_index = randint(5, 50) + + patterns = [ + harmonics( + frequency + frequency_drift(), + randint( + 1, + 16), + randint(1, 2)) + + for i in range(n) + ] + repetitions = randint(2, 8) # 4/4 beat + tempo = randint(60, 160) + output_file = "output.wav" + + print(f"Base: {frequency} Hz, frequency drift: {'on' if frequency_drift_toggle else 'off'}") + print(f"Carrier: {carrier} Hz") + print(f"Modulator: {modulator} Hz") + print(f"Modulation index: {modulation_index}") + print(f"Tempo: {tempo} BPM") + create_sound_file(patterns, repetitions, tempo * tempo_multiplier, output_file, carrier, modulator, modulation_index) + + print("Done!")