infer-vst / back /generators /generator.py
Yann
push backend
86694c3
import argparse
# ParamValue = Tuple[str,float,List[float]]
import os
import os.path
from typing import List
import h5py
import numpy as np
from scipy.io.wavfile import write as write_wav
from generators.parameters import *
"""
This is a base class to derive different kinds of sound generator from (e.g.
custom synthesis, VST plugins)
"""
class SoundGenerator:
"""
This is now a wrapper round the 'real' generation function
to handle normalising and saving
"""
def generate(
self,
parameters: dict,
filename: str,
length: float,
sample_rate: int,
extra: dict,
normalise: bool = True,
) -> np.ndarray:
audio = self.do_generate(parameters, filename, length, sample_rate, extra)
if normalise:
max = np.max(np.absolute(audio))
if max > 0:
audio = audio / max
if not self.creates_wave_file():
self.write_file(audio, filename, sample_rate)
def do_generate(
self,
parameters: dict,
filename: str,
length: float,
sample_rate: int,
extra: dict,
) -> np.ndarray:
print(
"Someone needs to write this method! Generating silence in {} with parameters:{}".format(
filename, str(parameters)
)
)
return np.zeros(int(length * sample_rate))
def creates_wave_file(self) -> bool:
return False
# Assumes that the data is -1..1 floating point
def write_file(self, data: np.ndarray, filename: str, sample_rate: int):
# REVIEW: is this needed?
# int_data = (data * np.iinfo(np.int16).max).astype(int)
write_wav(filename, sample_rate, data)
"""
This class runs through a parameter set, gets it to generate parameter settings
then runs the sound generator over it.
"""
class DatasetCreator:
def __init__(
self,
name: str,
dataset_dir: str,
wave_file_dir: str,
parameters: ParameterSet,
normalise: bool = True,
):
self.name = name
self.parameters = parameters
self.dataset_dir = dataset_dir
self.wave_file_dir = wave_file_dir
self.normalise = normalise
os.makedirs(dataset_dir, exist_ok=True)
os.makedirs(f"{wave_file_dir}/{name}", exist_ok=True)
def create_parameters(
self,
max: int = 2000,
method: str = "complete",
extra: dict = {},
force_create=False,
) -> str:
filename = self.get_dataset_filename("data", "hdf5")
if os.path.isfile(filename) and not force_create:
print(
"Parameter file exists, not recreating (use --regenerate_samples if you want to force)"
)
return filename
print("+" * 40)
print(f"Generating Dataset {self.name}, {max} examples")
print(f"Datasets: {self.dataset_dir}")
print("+" * 40)
# Save out the parameters first
self.save_parameters()
# Generate the set of samples (could switch to generators,
# but need to figure out arbitrary size arrays in HDF5)
dataset: List[Sample] = []
# if method == "complete":
# dataset = self.parameters.recursively_generate_all()
# else:
dataset = self.parameters.sample_space(sample_size=max)
# Create the data file and add all the points to it
with h5py.File(filename, "w") as datafile:
# Figure out the sizes to store
records = len(dataset)
param_size = len(dataset[0].encode())
# Add columns to it
filenames = datafile.create_dataset(
"files", (records,), dtype=h5py.string_dtype()
)
parameters = datafile.create_dataset(
"parameters", (records,), dtype=h5py.string_dtype()
)
labels = datafile.create_dataset("labels", (records, param_size))
audio_exists = datafile.create_dataset(
"audio_exists", (records,), dtype=np.bool_
)
# Generate the sample points
for index, point in enumerate(dataset):
params = self.parameters.to_settings(point)
filenames[index] = self.get_wave_filename(index)
labels[index] = point.encode()
parameters[index] = json.dumps(params)
audio_exists[index] = False
if index % 1000 == 0:
print("Generating parameters for example {}".format(index))
datafile.flush()
datafile.close()
return filename
def generate_audio(
self,
sound_generator: SoundGenerator,
length: float = 1,
sample_rate: int = 16384,
extra: dict = {},
dataset_filename=None,
force_generate=True,
):
if dataset_filename is None:
dataset_filename = self.get_dataset_filename("data", "hdf5")
print("+" * 40)
print(
f"Generating Audio for Dataset {self.name} ({dataset_filename}), with {length}s at {sample_rate}/s"
)
print(f"Output waves: {self.wave_file_dir}, datasets: {self.dataset_dir}")
print("+" * 40)
with h5py.File(dataset_filename, "r+") as datafile:
for name, value in datafile.items():
print(f"{name}: {value}")
# Get the columns
filenames = datafile.get("files")
print(filenames)
parameters = datafile.get("parameters")
print(parameters)
audio_exists = datafile.get("audio_exists")
print(audio_exists)
for index, filename in enumerate(filenames):
if (
audio_exists[index]
and os.path.isfile(filename)
and not force_generate
):
print(f"Audio exists for index {index} ({filename})")
else:
print(f"Generating Audio for index {index} ({filename})")
params = json.loads(parameters[index])
audio = sound_generator.generate(
params,
filename,
length,
sample_rate,
extra,
normalise=self.normalise,
)
audio_exists[index] = bool(audio)
datafile.flush()
if index % 1000 == 0:
print("Generating example {}".format(index))
def save_parameters(self):
self.parameters.save_json(self.get_dataset_filename("params", "json"))
self.parameters.save(self.get_dataset_filename("params", "pckl"))
def get_dataset_filename(self, type: str, extension: str = "txt") -> str:
return f"{self.dataset_dir}/{self.name}_{type}.{extension}"
def get_wave_filename(self, index: int) -> str:
return f"{self.wave_file_dir}/{self.name}/{self.name}_{index:05d}.wav"
def default_generator_argparse():
parser = argparse.ArgumentParser(description="Process some integers.")
parser.add_argument(
"--num_examples",
type=int,
dest="samples",
action="store",
default=20000,
help="Number of examples to create",
)
parser.add_argument(
"--name",
type=str,
dest="name",
default="InverSynth",
help="Name of datasets to create",
)
parser.add_argument(
"--dataset_directory",
type=str,
dest="data_dir",
default="test_datasets",
help="Directory to put datasets",
)
parser.add_argument(
"--wavefile_directory",
type=str,
dest="wave_dir",
default="test_waves",
help="Directory to put wave files. Will have the dataset name appended automatically",
)
parser.add_argument(
"--length",
type=float,
dest="length",
default=1.0,
help="Length of each sample in seconds",
)
parser.add_argument(
"--sample_rate",
type=int,
dest="sample_rate",
default=16384,
help="Sample rate (Samples/second)",
)
parser.add_argument(
"--sampling_method",
type=str,
dest="method",
default="random",
choices=["random"],
help="Method to use for generating examples. Currently only random, but may include whole space later",
)
parser.add_argument(
"--regenerate_samples",
action="store_true",
help="Regenerate the set of points to explore if it exists (will also force regenerating audio)",
)
parser.add_argument(
"--regenerate_audio",
action="store_true",
help="Regenerate audio files if they exists",
)
parser.add_argument(
"--normalise", action="store_true", help="Regenerate audio files if they exists"
)
return parser
def generate_examples(
gen: SoundGenerator, parameters: ParameterSet, args=None, extra={}
):
if not args:
parser = default_generator_argparse()
args = parser.parse_args()
g = DatasetCreator(
name=args.name,
dataset_dir=args.data_dir,
wave_file_dir=args.wave_dir,
parameters=parameters,
normalise=args.normalise,
)
g.create_parameters(
max=args.samples, method=args.method, force_create=True
)
g.generate_audio(
sound_generator=gen,
length=args.length,
sample_rate=args.sample_rate,
extra=extra,
force_generate=args.regenerate_audio | args.regenerate_samples,
)
if __name__ == "__main__":
gen = SoundGenerator()
parameters = ParameterSet(
[
Parameter("p1", [100, 110, 120, 130, 140]),
Parameter("p2", [200, 220, 240, 260, 280]),
]
)
g = DatasetCreator(
"example_generator",
dataset_dir="test_datasets",
wave_file_dir="test_waves/example/",
parameters=parameters,
)
g.generate_examples(sound_generator=gen, parameters=parameters)