Files
2024-11-24 18:55:43 -08:00

153 lines
5.3 KiB
Python

import torch
import numpy as np
import ast
from typing import Tuple, Dict
class BytebeatSynth:
@classmethod
def INPUT_TYPES(cls) -> Dict:
# (t * (t >> 5 & 7 | t >> 10) & 63) + (t >> 7 & 127)
# (t * (t >> 9 | t >> 13) & 16) & 255
return {
"required": {
"samplerate": ("INT", {"default": 8000, "min": 8000, "max": 96000, "step": 1}),
"duration": ("FLOAT", {"default": 15.0, "min": 1.0, "max": 60.0, "step": 0.1}),
"expression": ("STRING", {"default": "(t * (t >> 9 | t >> 13) & 16) & 255", "multiline": False, "hint": "Provide a C-like expression using t, bitwise operators, and arithmetic."})
}
}
RETURN_TYPES = ("AUDIO", "INT")
RETURN_NAMES = ("audio", "sample_rate")
CATEGORY = "Synthesizer"
FUNCTION = "process"
def process(
self,
samplerate: int,
duration: float,
expression: str,
) -> Tuple[Dict[str, torch.Tensor]]:
"""
Generates audio using the Bytebeat synthesis method.
Parameters:
samplerate (int): Sampling rate in Hz.
duration (float): Duration of the audio in seconds.
expression (str): Bytebeat expression as a C-like string.
Returns:
Tuple[Dict[str, torch.Tensor]]: Generated audio waveform and sample rate.
"""
# Validate inputs
if samplerate < 8000 or samplerate > 96000:
raise ValueError("Sample rate must be between 8000 and 96000 Hz.")
if duration < 1.0 or duration > 60.0:
raise ValueError("Duration must be between 1.0 and 60.0 seconds.")
# Calculate total number of samples
total_samples = int(samplerate * duration)
# Create an array of time indices
t = np.arange(total_samples, dtype=np.uint32)
# Safely evaluate the Bytebeat expression
try:
samples = self.safe_eval_expression(expression, t)
except Exception as e:
raise ValueError(f"Error evaluating expression: {e}")
# Ensure samples are in 8-bit range [0, 255]
samples = samples.astype(np.int16)
samples = (samples - 128) * 256 # Normalize to 16-bit range
# Convert to PyTorch tensor
waveform = torch.from_numpy(samples).float()
# Normalize waveform to prevent clipping
max_val = torch.max(torch.abs(waveform))
if max_val > 0:
waveform = waveform / max_val * 0.9 # Scale to 90% to prevent clipping
# Add channel dimension (mono)
waveform = waveform.unsqueeze(0) # Shape: [1, samples]
# Add batch dimension if required by the framework
waveform = waveform.unsqueeze(0) # Shape: [1, 1, samples]
# Prepare the return dictionary
return {"waveform": waveform, "sample_rate": samplerate}, samplerate
def safe_eval_expression(self, expr: str, t: np.ndarray) -> np.ndarray:
"""
Safely evaluate a Bytebeat expression.
Parameters:
expr (str): Bytebeat expression as a string.
t (np.ndarray): Array of time indices.
Returns:
np.ndarray: Evaluated audio samples.
"""
# Define allowed names and functions
allowed_names = {
't': t,
'np': np,
'sin': np.sin,
'cos': np.cos,
'tan': np.tan,
'exp': np.exp,
'sqrt': np.sqrt,
'log': np.log,
'abs': np.abs,
'min': np.minimum,
'max': np.maximum,
# Add more numpy functions if needed
}
# Parse the expression to ensure it's safe
try:
node = ast.parse(expr, mode='eval')
except SyntaxError as e:
raise ValueError(f"Syntax error in expression: {e}")
# Recursively validate the AST nodes
for subnode in ast.walk(node):
if isinstance(subnode, ast.Call):
if not isinstance(subnode.func, ast.Name) or subnode.func.id not in allowed_names:
raise ValueError(f"Function '{subnode.func.id}' is not allowed.")
elif isinstance(subnode, ast.Name):
if subnode.id not in allowed_names:
raise ValueError(f"Use of name '{subnode.id}' is not allowed.")
elif isinstance(subnode, (ast.BinOp, ast.UnaryOp, ast.Num, ast.Expression,
ast.Load, ast.Add, ast.Sub, ast.Mult, ast.Div,
ast.Mod, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor,
ast.RShift, ast.LShift, ast.USub, ast.UAdd)):
continue
else:
raise ValueError(f"Unsupported expression element: {type(subnode).__name__}")
# Compile and evaluate the expression
try:
compiled_expr = compile(node, '<string>', 'eval')
result = eval(compiled_expr, {"__builtins__": None}, allowed_names)
except Exception as e:
raise ValueError(f"Error during evaluation: {e}")
if not isinstance(result, np.ndarray):
raise ValueError("The expression must return a NumPy array.")
return result
NODE_CLASS_MAPPINGS = {
"BytebeatSynth":BytebeatSynth
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BytebeatSynth": "ByteBeat Synth"
}