|
5 | 5 | import subsequence.chords |
6 | 6 | import subsequence.constants |
7 | 7 | import subsequence.constants.velocity |
| 8 | +import subsequence.easing |
8 | 9 | import subsequence.groove |
9 | 10 | import subsequence.intervals |
10 | 11 | import subsequence.pattern |
@@ -895,6 +896,95 @@ def velocity_shape (self, low: int = subsequence.constants.velocity.VELOCITY_SHA |
895 | 896 | note.velocity = int(low + (high - low) * vdc_value) |
896 | 897 | return self |
897 | 898 |
|
| 899 | + def duck_map ( |
| 900 | + self, |
| 901 | + steps: typing.Iterable[int], |
| 902 | + floor: float = 0.0, |
| 903 | + grid: typing.Optional[int] = None, |
| 904 | + ) -> typing.List[float]: |
| 905 | + |
| 906 | + """ |
| 907 | + Build a per-step velocity multiplier list for sidechain-style ducking. |
| 908 | +
|
| 909 | + Returns a list of floats, one per grid step: ``floor`` at each trigger |
| 910 | + step in ``steps``, ``1.0`` everywhere else. Pass the result to |
| 911 | + ``p.data`` for another pattern to read, then apply with |
| 912 | + ``p.scale_velocities()``. |
| 913 | +
|
| 914 | + Parameters: |
| 915 | + steps: Grid indices that trigger ducking (e.g. kick hit positions). |
| 916 | + floor: Multiplier written at trigger steps. ``0.0`` = full silence, |
| 917 | + ``1.0`` = no effect. Values in between give partial ducking. |
| 918 | + grid: Grid resolution (defaults to ``p.grid``). |
| 919 | +
|
| 920 | + Returns: |
| 921 | + ``List[float]`` of length ``grid``. |
| 922 | +
|
| 923 | + Example:: |
| 924 | +
|
| 925 | + # Full duck on kick hits |
| 926 | + p.data["kick_sc"] = p.duck_map(kick_steps) |
| 927 | +
|
| 928 | + # Softer duck |
| 929 | + p.data["kick_sc"] = p.duck_map(kick_steps, floor=0.3) |
| 930 | +
|
| 931 | + # Velocity-proportional: deeper duck for harder kicks |
| 932 | + p.data["kick_sc"] = p.duck_map(kick_steps, floor=1.0 - (velocity / 127)) |
| 933 | + """ |
| 934 | + |
| 935 | + if grid is None: |
| 936 | + grid = self._default_grid |
| 937 | + |
| 938 | + trigger = set(steps) |
| 939 | + return [floor if s in trigger else 1.0 for s in range(grid)] |
| 940 | + |
| 941 | + def scale_velocities ( |
| 942 | + self, |
| 943 | + factors: typing.Sequence[float], |
| 944 | + grid: typing.Optional[int] = None, |
| 945 | + ) -> "PatternBuilder": |
| 946 | + |
| 947 | + """ |
| 948 | + Scale note velocities by a per-step multiplier list. |
| 949 | +
|
| 950 | + Each note's velocity is multiplied by the factor at the corresponding |
| 951 | + grid step index. A factor of ``1.0`` leaves the velocity unchanged; |
| 952 | + ``0.0`` silences the note; ``0.5`` halves it. |
| 953 | +
|
| 954 | + Parameters: |
| 955 | + factors: Per-step multipliers, one float per grid step. |
| 956 | + Values outside ``[0.0, 1.0]`` are valid — result is clamped to |
| 957 | + ``[0, 127]`` after scaling. |
| 958 | + grid: Grid resolution (defaults to ``p.grid``). Must match the |
| 959 | + length of ``factors``. |
| 960 | +
|
| 961 | + Returns: |
| 962 | + ``self`` for fluent chaining. |
| 963 | +
|
| 964 | + Example:: |
| 965 | +
|
| 966 | + # Sidechain ducking: silence bass on kick steps, full volume elsewhere. |
| 967 | + kick_steps = {0, 4, 8, 12} |
| 968 | + p.data["kick_sc"] = [0.0 if s in kick_steps else 1.0 for s in range(p.grid)] |
| 969 | +
|
| 970 | + # In the bass pattern: |
| 971 | + p.scale_velocities(p.data.get("kick_sc", [1.0] * p.grid)) |
| 972 | + """ |
| 973 | + |
| 974 | + if grid is None: |
| 975 | + grid = self._default_grid |
| 976 | + |
| 977 | + step_duration = self._pattern.length / grid |
| 978 | + pulses_per_step = step_duration * subsequence.constants.MIDI_QUARTER_NOTE |
| 979 | + |
| 980 | + for pulse, step in self._pattern.steps.items(): |
| 981 | + idx = int(round(pulse / pulses_per_step)) |
| 982 | + if 0 <= idx < len(factors): |
| 983 | + for note in step.notes: |
| 984 | + note.velocity = max(0, min(127, int(note.velocity * factors[idx]))) |
| 985 | + |
| 986 | + return self |
| 987 | + |
898 | 988 | def randomize ( |
899 | 989 | self, |
900 | 990 | timing: float = 0.03, |
|
0 commit comments