mirror of
https://github.com/Khendi1/PVS.git
synced 2026-02-11 13:10:42 +01:00
193 lines
7.6 KiB
Python
193 lines
7.6 KiB
Python
|
|
import random
|
|
import math
|
|
from enum import Enum
|
|
import numpy as np
|
|
from config import params
|
|
import noise
|
|
from param import Param
|
|
|
|
def map_value(value, from_min, from_max, to_min, to_max, round_down=True):
|
|
"""
|
|
Maps a value from one range to another.
|
|
|
|
Args:
|
|
value: The value to map.
|
|
from_min: The minimum value of the original range.
|
|
from_max: The maximum value of the original range.
|
|
to_min: The minimum value of the target range.
|
|
to_max: The maximum value of the target range.
|
|
Returns:
|
|
The mapped value in the target range, rounded down to the nearest integer.
|
|
"""
|
|
# Calculate the proportion of the value within the original range
|
|
proportion = (value - from_min) / (from_max - from_min)
|
|
|
|
# Map the proportion to the target range
|
|
mapped_value = to_min + proportion * (to_max - to_min)
|
|
|
|
if round_down:
|
|
return math.floor(mapped_value)
|
|
else:
|
|
return mapped_value # Return the mapped value without rounding
|
|
|
|
class OscillatorShape(Enum):
|
|
NONE = 0
|
|
SINE = 1
|
|
SQUARE = 2
|
|
TRIANGLE = 3
|
|
SAWTOOTH = 4
|
|
PERLIN = 5
|
|
|
|
@classmethod
|
|
def from_value(cls, value):
|
|
if isinstance(value, str):
|
|
value = value.lower()
|
|
if value == "sine":
|
|
return cls.SINE
|
|
elif value == "square":
|
|
return cls.SQUARE
|
|
elif value == "triangle":
|
|
return cls.TRIANGLE
|
|
elif value == "sawtooth":
|
|
return cls.SAWTOOTH
|
|
elif value == "perlin":
|
|
return cls.PERLIN
|
|
return cls(value)
|
|
|
|
class Oscillator:
|
|
def __init__(self, name, frequency, amplitude, phase, shape, seed=0, linked_param_name=None, max_amplitude=100, min_amplitude=-100):
|
|
self.name = name
|
|
self.param_max = max_amplitude
|
|
self.param_min = min_amplitude
|
|
self.frequency = params.add(f"{name}_frequency", 0, 2, frequency)
|
|
self.amplitude = params.add(f"{name}_amplitude", -100, 100, amplitude)
|
|
# self.amplitude = params.add(f"{name}_amplitude", self.param_min, self.param_max, amplitude)
|
|
self.phase = params.add(f"{name}_phase", 0, 360, phase)
|
|
self.seed = params.add(f"{name}_seed", 0, 100, seed) # TODO: Change ambiguous name to something more descriptive
|
|
self.shape = params.add(f"{name}_shape", 0, len(OscillatorShape)-1, shape)
|
|
|
|
self.noise_octaves = params.add(f"{name}_noise_octaves", 1, 10, 6)
|
|
self.noise_persistence = params.add(f"{name}_noise_persistence", 0.1, 1.0, 0.5)
|
|
self.noise_lacunarity = params.add(f"{name}_noise_lacunarity", 1.0, 2.0, 2.0)
|
|
self.noise_repeat = params.add(f"{name}_noise_repeat", 1, 1000, 100)
|
|
self.noise_base = params.add(f"{name}_noise_base", 0, 1000, 456)
|
|
|
|
self.sample_rate = 30
|
|
self.direction = 1
|
|
self.oscillator = self.create_oscillator()
|
|
self.linked_param = None
|
|
self.value = 0
|
|
|
|
# Link to a parameter if provided
|
|
if linked_param_name is not None:
|
|
self.linked_param = params.get(linked_param_name)
|
|
if self.linked_param is None:
|
|
raise ValueError(f"Linked parameter '{linked_param_name}' not found in params.")
|
|
self.link_param(self.linked_param)
|
|
|
|
def get_next_value(self, map=False):
|
|
"""
|
|
Gets the next value from the current waveform generator.
|
|
|
|
Returns:
|
|
The next sample value.
|
|
"""
|
|
self.value = next(self.oscillator)
|
|
return self.value
|
|
|
|
def _scale_value(self, param: Param, value: int | float, in_min=-1.0, in_max=1.0):
|
|
"""Scales a value from the noise range to the oscillator's param_min/max."""
|
|
return (value - in_min) * ((param.max - param.min) / (in_max - in_min)) + param.min
|
|
|
|
def create_oscillator(self):
|
|
"""
|
|
Creates a waveform oscillator using time instead of phase.
|
|
|
|
Returns:
|
|
generator: A generator that yields the next sample of the waveform.
|
|
"""
|
|
t = 0.0
|
|
|
|
while True:
|
|
freq = self.frequency.value
|
|
amp = self.amplitude.value
|
|
phase_offset = self.phase.value
|
|
seed = self.seed.value
|
|
shape = int(self.shape.value)
|
|
direction = self.direction
|
|
|
|
# Calculate the argument for the waveform functions
|
|
arg = 2 * np.pi * freq * t + np.deg2rad(phase_offset)
|
|
|
|
if shape == 0: # null wave
|
|
pass
|
|
elif shape == 1: # sine wave
|
|
sample = amp * np.sin(arg) + seed
|
|
sample *= direction
|
|
elif shape == 2: # square wave
|
|
sample = amp * np.sign(np.sin(arg)) + seed
|
|
elif shape == 3: # triangle wave
|
|
sample = amp * (2 / np.pi) * np.arcsin(np.sin(arg)) + seed
|
|
sample *= direction
|
|
elif shape == 4: # Sawtooth wave
|
|
sample = amp * (2 * (t * freq - np.floor(t * freq + 0.5))) + seed
|
|
sample *= direction
|
|
elif shape == 5: # Perlin noise
|
|
|
|
# noise_input_x = self._time / 10.0 # Adjust divisor for desired speed/smoothness
|
|
noise_input_x = (t * self.frequency.value) # % self.noise_repeat
|
|
|
|
sample = noise.pnoise1(
|
|
noise_input_x,
|
|
octaves=self.noise_octaves.value,
|
|
persistence=self.noise_persistence.value,
|
|
lacunarity=self.noise_lacunarity.value,
|
|
repeat=self.noise_repeat.value, # This ensures the noise repeats after 'noise_repeat' units
|
|
base=self.noise_base.value
|
|
)
|
|
|
|
else:
|
|
raise ValueError(f"Invalid shape value. Must be 0 (sine), 1 (square), 2 (triangle), or 3 (sawtooth). got shape={shape}")
|
|
|
|
if self.linked_param is not None:
|
|
if shape == 5:
|
|
# TODO: handle perlin noise mapping using the map_value method
|
|
mapped_sample = self._scale_value(self.linked_param, sample, in_min=-1.0, in_max=1.0) * self.amplitude.value
|
|
elif isinstance(self.linked_param.default_val, float):
|
|
# mapped_sample = self._scale_value(self.linked_param, sample, in_min=-1.0, in_max=1.0) #* #self.amplitude.value
|
|
mapped_sample = map_value(round(sample, 5), self.param_min, self.param_max, self.linked_param.min, self.linked_param.max, round_down=False)
|
|
elif isinstance(self.linked_param.default_val, int):
|
|
mapped_sample = map_value(sample, self.param_min, self.param_max, self.linked_param.min, self.linked_param.max)
|
|
|
|
self.linked_param.value = mapped_sample
|
|
# print(f'{sample} mapped to {mapped_sample} for linked param {self.linked_param.name}')
|
|
yield sample
|
|
t += 1 / self.sample_rate # Increment time by sample period
|
|
|
|
def link_param(self, param: Param):
|
|
"""
|
|
Links the oscillator parameters to a parameter object.
|
|
|
|
Args:
|
|
param (Param): The parameter object to link to.
|
|
"""
|
|
print(f"Linking {self.name} to {param.name}")
|
|
self.linked_param = param
|
|
self.amplitude.max = param.max
|
|
self.amplitude.min = param.min
|
|
self.phase.max = param.max
|
|
self.phase.min = param.min
|
|
self.seed.max = param.max
|
|
self.seed.min = param.min
|
|
|
|
def unlink_param(self):
|
|
"""
|
|
Unlinks the oscillator parameters from the parameter object.
|
|
"""
|
|
self.linked_param = None
|
|
|
|
# @property
|
|
# def value(self):
|
|
# """Returns the current calculated value of the oscillator."""
|
|
# return self._current_value |