Skip to content

Instantly share code, notes, and snippets.

@ychalier
Created December 22, 2024 11:55
Show Gist options
  • Select an option

  • Save ychalier/fd171da5e2c9c65b9ad8a3bfc8b45b95 to your computer and use it in GitHub Desktop.

Select an option

Save ychalier/fd171da5e2c9c65b9ad8a3bfc8b45b95 to your computer and use it in GitHub Desktop.
Granular Synthesis Draft
import argparse
import os
import pathlib
import random
import numpy
import soundfile
import tqdm
def generate(
input_path: str,
output_path: str | None,
output_duration: float = 30,
layer_count: int = 1000,
sigma: float = .5,
spread: float = .3,
max_duration: float = .05,
stop_count: int = 10,
window: str = "gaussian",
seed: int | None = None
):
if seed is None:
seed = random.randint(0, 2**32 - 1)
random.seed(seed)
data, sample_rate = soundfile.read(input_path)
output_duration = int(sample_rate * output_duration)
spread_frames = int(spread * sample_rate)
max_frame_duration = int(max_duration * sample_rate)
window_function = {
"gaussian": lambda x: numpy.exp(-.5*(x/(2*sigma))**2),
"triangular": lambda x: 1.0 - numpy.abs(x),
"lines": lambda x: numpy.clip(2-2*numpy.abs(x), 0, 1),
"parabola": lambda x: 1 - numpy.pow(x, 2),
"half-lines": lambda x: numpy.clip(2-2*numpy.abs(x), .5, 1),
"sine": lambda x: numpy.cos(x*numpy.pi/2),
"none": lambda x: numpy.ones_like(x),
}[window]
out = numpy.zeros(output_duration)
stops = [random.randint(0, data.shape[0] - 1) for _ in range(stop_count)]
for _ in tqdm.tqdm(range(layer_count), unit="layer", desc="Generating"):
i = 0
while i < output_duration - 1:
stop = stops[int(i * len(stops) / output_duration)]
duration = min(int(random.random() * max_frame_duration), output_duration - i)
start = random.randint(stop - spread_frames, stop + spread_frames) - duration // 2
sample = data[start:start+duration]
x = numpy.linspace(-1, 1, sample.shape[0])
window_array = window_function(x)
grain = numpy.multiply(sample[:,0], window_array)
out[i:i+duration] += grain
i += duration
out /= numpy.max(numpy.abs(out))
if output_path is None:
p = pathlib.Path(input_path)
output_path = p\
.with_stem(p.stem + f"_{window}_{layer_count}_{spread}_{max_duration}_{seed}")\
.with_suffix(".wav")
soundfile.write(output_path, out, sample_rate)
if os.path.isfile(output_path):
os.startfile(output_path)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"input",
type=str,
help="Input wav file")
parser.add_argument(
"output",
type=str, default=None, nargs="?",
help="Output wav file")
parser.add_argument(
"-d", "--duration",
type=int, default=30,
help="Output duration in seconds")
parser.add_argument(
"-l", "--layers",
type=int, default=100,
help="Number of layers")
parser.add_argument(
"-si", "--sigma",
type=float, default=0.5,
help="Sigma for Gaussian window")
parser.add_argument(
"-sp", "--spread",
type=float, default=0.3,
help="Spread for stop points")
parser.add_argument(
"-md", "--max-duration",
type=float, default=0.05,
help="Maximum grain duration")
parser.add_argument(
"-sc", "--stop-count",
type=int, default=1,
help="Number of stop points")
parser.add_argument(
"-w", "--window",
type=str, default="gaussian",
choices=["gaussian", "triangular", "lines", "parabola", "half-lines",
"sine", "none"],
help="Window function")
parser.add_argument(
"-se", "--seed",
type=int, default=None,
help="Random seed")
args = parser.parse_args()
generate(args.input, args.output, args.duration, args.layers, args.sigma,
args.spread, args.max_duration, args.stop_count, args.window,
args.seed)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment