Compare commits

..

No commits in common. "main" and "v0.0.1" have entirely different histories.
main ... v0.0.1

20 changed files with 112 additions and 879 deletions

View file

@ -3,12 +3,8 @@ name: Python package
on:
push:
branches: [ "main" ]
paths-ignore:
- "README.md"
pull_request:
branches: [ "main" ]
release:
types: [published]
jobs:
build-ubuntu:
@ -206,7 +202,6 @@ jobs:
runs-on: ubuntu-latest
name: "Upload wheels to PyPI"
if: github.event_name == 'release' && github.event.action == 'published'
# if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/download-artifact@v4
with:

1
.gitignore vendored
View file

@ -39,4 +39,3 @@ dataset
*.egg-info*
vita/LICENSE
dist
headless/builds/VisualStudio2022/x64

View file

@ -1,8 +1,6 @@
PYTHON := $(shell which python)
PYTHONINCLUDEPATH := $(shell python3.10 -c "import sysconfig; print(sysconfig.get_path('include'))")
PYTHONINCLUDEPATH := $(shell $(PYTHON) -c "import sysconfig; print(sysconfig.get_path('include'))")
PYTHONLIBPATH := $(shell $(PYTHON) -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))")
PYTHONLIBPATH := $(shell python3.10 -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))")
ifndef CONFIG
CONFIG=Release
@ -10,7 +8,7 @@ endif
ifndef LIBDIR
# LIBDIR=/usr/lib/
LIBDIR=$(shell $(PYTHON) -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))")
LIBDIR=$(shell python3.10 -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))")
endif
BUILD_DATE="$(shell date +'%Y %m %d %H %M')"

View file

@ -1,6 +1,6 @@
# Vita
Vita is a Python module for interacting with the [Vital Synthesizer](https://github.com/mtytel/vital). **It is not an official product related to Vital**. Vita uses [Effort-based versioning](https://jacobtomlinson.dev/effver/).
Vita is a Python module for interacting with the [Vital Synthesizer](https://github.com/mtytel/vital). **It is not an official product related to Vital**.
## Installation
@ -16,7 +16,8 @@ pip install vita
from scipy.io import wavfile
import vita
sample_rate = 44100
SAMPLE_RATE = 44_100
bpm = 120.0
note_dur = 1.0
render_dur = 3.0
@ -26,7 +27,6 @@ velocity = 0.7 # [0.0 to 1.0]
synth = vita.Synth()
# The initial preset is loaded by default.
synth.set_sample_rate(sample_rate)
synth.set_bpm(bpm)
# Let's make a custom modulation using
@ -35,42 +35,29 @@ synth.set_bpm(bpm)
print("potential sources:", vita.get_modulation_sources())
print("potential destinations:", vita.get_modulation_destinations())
# "lfo_1" is a potential source,
# and "filter_1_cutoff" is a potential destination.
# "lfo_1" and "filter_1_cutoff" are potential sources and destinations.
assert synth.connect_modulation("lfo_1", "filter_1_cutoff")
controls = synth.get_controls()
controls["modulation_1_amount"].set(1.0)
controls["filter_1_on"].set(1.0)
val = controls["filter_1_on"].value()
controls["lfo_1_tempo"].set(vita.constants.SyncedFrequency.k1_16)
# Use normalized parameter control (0-1 range, VST-style)
controls["filter_1_cutoff"].set_normalized(0.5) # Set knob to 50%
print(controls["filter_1_cutoff"].get_normalized()) # Get normalized value
# Get parameter details and display text
info = synth.get_control_details("delay_style")
print(f"Options: {info.options}") # ["Mono", "Stereo", "Ping Pong", "Mid Ping Pong"]
print(f"Current: {synth.get_control_text('delay_style')}") # e.g., "Stereo"
# Render audio to numpy array shaped (2, NUM_SAMPLES)
audio = synth.render(pitch, velocity, note_dur, render_dur)
wavfile.write("generated_preset.wav", sample_rate, audio.T)
wavfile.write("generated_preset.wav", SAMPLE_RATE, audio.T)
# Dump current state to JSON text
preset_path = "generated_preset.vital"
json_text = synth.to_json()
with open(preset_path, "w") as f:
f.write(json_text)
# Load JSON text
with open(preset_path, "r") as f:
json_text = f.read()
assert synth.load_json(json_text)
# Or load directly from file
@ -82,8 +69,6 @@ synth.load_init_preset()
synth.clear_modulations()
```
Documentation is not yet automated. Please browse [bindings.cpp](https://github.com/DBraun/Vita/blob/main/src/headless/bindings.cpp) to get a sense of how the code works.
### Issues
If you find any issues with the code, report them at https://github.com/DBraun/Vita.

View file

@ -1,7 +1,3 @@
# export LIBDIR=/usr/lib/python3.10
# export PYTHONLIBPATH=/usr/lib/python3.10
# export PYTHONINCLUDEPATH=/usr/include/python3.10
echo "PYTHONLIBPATH: $PYTHONLIBPATH"
echo "PYTHONINCLUDEPATH: $PYTHONINCLUDEPATH"
echo "LIBDIR: $LIBDIR"
@ -20,6 +16,3 @@ cd ../..
make headless_server
echo "build_linux.sh is done!"
# to build a wheel:
# python3 -m build --wheel

View file

@ -1 +0,0 @@
output

View file

@ -1,14 +0,0 @@
# Vita - Multiprocessing
This script demonstrates how to use [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) to efficiently generate one-shots. The number of workers is by default `multiprocessing.cpu_count()`. Each worker has a persistent synthesizer instance. Each worker consumes paths of presets from a multiprocessing [Queue](https://docs.python.org/3/library/multiprocessing.html#pipes-and-queues). For each preset, the worker renders out audio for a configurable MIDI pitch range. The output audio path includes the pitch and preset name.
Example usage:
```bash
python main.py --preset-dir "path/to/vital_presets"
```
To see all available parameters:
```bash
python main.py --help
```

View file

@ -1,183 +0,0 @@
# This file is part of the Vita distribution (https://github.com/DBraun/Vita).
# Copyright (c) 2025 David Braun.
import argparse
from collections import namedtuple
import logging
import multiprocessing
import os
from pathlib import Path
import time
import traceback
# extra libraries to install with pip
import vita
import numpy as np
from scipy.io import wavfile
from tqdm import tqdm
Item = namedtuple("Item", "preset_path")
class Worker:
def __init__(
self,
queue: multiprocessing.Queue,
bpm: float = 120.0,
note_duration: float = 2.0,
render_duration: float = 5.0,
pitch_low: int = 60,
pitch_high: int = 72,
velocity: int = 100,
output_dir="output",
):
self.queue = queue
self.bpm = bpm
self.note_duration = note_duration
self.render_duration = render_duration
self.pitch_low, self.pitch_high = pitch_low, pitch_high
self.velocity = velocity
self.output_dir = Path(output_dir)
def startup(self):
synth = vita.Synth()
synth.set_bpm(self.bpm)
self.synth = synth
def process_item(self, item: Item):
preset_path = item.preset_path
self.synth.load_preset(preset_path)
basename = os.path.basename(preset_path)
for pitch in range(self.pitch_low, self.pitch_high + 1):
audio = self.synth.render(
pitch, self.velocity, self.note_duration, self.note_duration
)
output_path = self.output_dir / f"{pitch}_{basename}.wav"
wavfile.write(str(output_path), 44_100, audio.transpose())
def run(self):
try:
self.startup()
while True:
try:
item = self.queue.get_nowait()
self.process_item(item)
except multiprocessing.queues.Empty:
break
except Exception as e:
return traceback.format_exc()
def main(
preset_dir,
bpm: float = 120.0,
note_duration: float = 2.0,
render_duration: float = 4.0,
pitch_low: int = 60,
pitch_high: int = 60,
num_workers=None,
output_dir="output",
logging_level="INFO",
):
# Create logger
logging.basicConfig()
logger = logging.getLogger("vita")
logger.setLevel(logging_level.upper())
# Glob all the preset file paths
preset_paths = list(Path(preset_dir).rglob("*.vital"))
# Get num items so that the progress bar works well
num_items = len(preset_paths)
# Create a Queue and add items
input_queue = multiprocessing.Manager().Queue()
for preset_path in preset_paths:
input_queue.put(Item(str(preset_path)))
# Create a list to hold the worker processes
workers = []
# The number of workers to spawn
num_processes = num_workers or multiprocessing.cpu_count()
# Log info
logger.info(f"Note duration: {note_duration}")
logger.info(f"Render duration: {render_duration}")
logger.info(f"Using num workers: {num_processes}")
logger.info(f"Pitch low: {pitch_low}")
logger.info(f"Pitch high: {pitch_high}")
logger.info(f"Output directory: {output_dir}")
os.makedirs(output_dir, exist_ok=True)
# Create a multiprocessing Pool
with multiprocessing.Pool(processes=num_processes) as pool:
# Create and start a worker process for each CPU
for i in range(num_processes):
worker = Worker(
input_queue,
bpm=bpm,
note_duration=note_duration,
render_duration=render_duration,
pitch_low=pitch_low,
pitch_high=pitch_high,
output_dir=output_dir,
)
async_result = pool.apply_async(worker.run)
workers.append(async_result)
# Use tqdm to track progress. Update the progress bar in each iteration.
pbar = tqdm(total=num_items)
while True:
incomplete_count = sum(1 for w in workers if not w.ready())
pbar.update(
num_items - input_queue.qsize() - pbar.n
) # not perfectly accurate.
if incomplete_count == 0:
break
time.sleep(0.1)
pbar.close()
# Check for exceptions in the worker processes
for i, worker in enumerate(workers):
exception = worker.get()
if exception is not None:
logger.error(f"Exception in worker {i}:\n{exception}")
logger.info("All done!")
if __name__ == "__main__":
# We're using multiprocessing.Pool, so our code MUST be inside __main__.
# See https://docs.python.org/3/library/multiprocessing.html
# fmt: off
parser = argparse.ArgumentParser()
parser.add_argument("--preset-dir", required=True, help="Directory path of Vital presets.")
parser.add_argument("--bpm", default=120.0, type=float, help="Beats per minute for the Render Engine.")
parser.add_argument("--note-duration", default=1, type=float, help="Note duration in seconds.")
parser.add_argument("--pitch-low", default=60, type=int, help="Lowest MIDI pitch to be used (inclusive).")
parser.add_argument("--pitch-high", default=60, type=int, help="Highest MIDI pitch to be used (inclusive).")
parser.add_argument("--render-duration", default=1, type=float, help="Render duration in seconds.")
parser.add_argument("--num-workers", default=None, type=int, help="Number of workers to use.")
parser.add_argument("--output-dir", default=os.path.join(os.path.dirname(__file__), "output"), help="Output directory.")
parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NOTSET"], help="Logger level.")
# fmt: on
args = parser.parse_args()
main(
args.preset_dir,
args.bpm,
args.note_duration,
args.render_duration,
args.pitch_low,
args.pitch_high,
args.num_workers,
args.output_dir,
args.log_level,
)

View file

@ -1,93 +0,0 @@
"""
Demo: VST-style normalized parameter control
This example shows how to control Vital synthesizer parameters using
normalized 0-1 values, similar to how VST/AU plugins work in DAWs.
"""
import vita
from scipy.io import wavfile
SAMPLE_RATE = 44_100
def main():
synth = vita.Synth()
synth.set_bpm(120.0)
# Direct control - linear interpolation of actual values
controls = synth.get_controls()
# Turn the filter on
controls["filter_1_on"].set(1.0)
# Example 1: Comparing direct vs normalized control (Filter 1 Cutoff)
print("=== Comparing Direct vs Normalized Control ===")
# Get info about the filter 1 cutoff parameter
info = synth.get_control_details("filter_1_cutoff")
print(f"Parameter: {info.display_name}")
print(f"Range: {info.min} to {info.max}")
print(f"Scale: {str(info.scale)}")
print()
print("Direct control (linear interpolation):")
for pct in [0.0, 0.25, 0.5, 0.75, 1.0]:
value = info.min + pct * (info.max - info.min)
controls["filter_1_cutoff"].set(value)
print(f" {pct*100:3.0f}% → value={value:.3f}")
print("\nNormalized control (VST-style):")
for norm in [0.0, 0.25, 0.5, 0.75, 1.0]:
controls["filter_1_cutoff"].set_normalized(norm)
actual = controls["filter_1_cutoff"].value()
display_text = synth.get_control_text("filter_1_cutoff")
print(f" {norm*100:3.0f}% → value={actual:.3f} (displays as {display_text})")
# Render a short note
audio = synth.render(36, 0.8, 0.9, 1) # C2, velocity 0.8, 0.9s note, 1.0s render
filename = f"filter_sweep_{int(norm*100):03d}.wav"
wavfile.write(filename, SAMPLE_RATE, audio.T)
print(f"Rendered {filename} with filter at {norm*100:.0f}%")
# Example 2: Comparing direct vs normalized control (Env 1 Delay)
print("\n=== Comparing Direct vs Normalized Control ===")
# Get info about the Env 1 Delay parameter
info = synth.get_control_details("env_1_delay")
print(f"Parameter: {info.display_name}")
print(f"Range: {info.min} to {info.max}")
print(f"Scale: {str(info.scale)}")
print()
print("Direct control (linear interpolation):")
for pct in [0.0, 0.25, 0.5, 0.75, 1.0]:
value = info.min + pct * (info.max - info.min)
controls["env_1_delay"].set(value)
print(f" {pct*100:3.0f}% → value={value:.3f}")
print("\nNormalized control (VST-style):")
print("(Note: knob position has quartic relationship to display value)")
for norm in [0.0, 0.25, 0.5, 0.75, 1.0]:
controls["env_1_delay"].set_normalized(norm)
actual = controls["env_1_delay"].value()
display_text = synth.get_control_text("env_1_delay")
expected_display = 4.0 * (norm ** 4)
print(f" {norm*100:3.0f}% → value={actual:.3f} (displays as {display_text}, expected ~{expected_display:.2f}s)")
# Example 3: Discrete parameter control (Delay Style)
print("\n=== Discrete Parameter Control ===")
info = synth.get_control_details("delay_style")
print(f"Delay styles: {info.options}")
# Cycle through delay styles using normalized values
num_styles = len(info.options)
for i in range(num_styles):
normalized = i / (num_styles - 1) if num_styles > 1 else 0
controls["delay_style"].set_normalized(normalized)
style = controls["delay_style"].get_text()
print(f"Normalized {normalized:.2f}{style}")
if __name__ == "__main__":
main()

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>

View file

@ -5,7 +5,7 @@
# Then in the `dist` directory, `pip install vita`
import setuptools
from setuptools import setup
from setuptools import setup, Extension
from setuptools.dist import Distribution
import os
import os.path

View file

@ -16,7 +16,6 @@
#include "synth_base.h"
#include <nanobind/nanobind.h>
#include "sample_source.h"
#include "sound_engine.h"
#include "load_save.h"
@ -433,12 +432,11 @@ bool SynthBase::loadFromString(std::string json_text) {
//setPresetName(preset.getFileNameWithoutExtension()); // todo:
// note: dbraun commented this out since we're running headless anyway.
//SynthGuiInterface* gui_interface = getGuiInterface();
//if (gui_interface) {
// gui_interface->updateFullGui();
// gui_interface->notifyFresh();
//}
SynthGuiInterface* gui_interface = getGuiInterface();
if (gui_interface) {
gui_interface->updateFullGui();
gui_interface->notifyFresh();
}
return true;
}
@ -448,13 +446,8 @@ void SynthBase::pySetBPM(float bpm) {
engine_->setBpm(bpm);
};
// src/plugin/synth_plugin.cpp Line 129-133 prepareToPlay
void SynthBase::setSampleRate(double sample_rate) {
engine_->setSampleRate(sample_rate);
midi_manager_->setSampleRate(sample_rate);
}
void SynthBase::renderAudioToFile(File file, std::vector<int> notes, float velocity, float note_dur, float render_dur, bool render_images) {
static constexpr int kSampleRate = 44100;
static constexpr int kPreProcessSamples = 44100;
static constexpr int kFadeSamples = 200;
static constexpr int kBufferSize = 64;
@ -469,11 +462,11 @@ void SynthBase::renderAudioToFile(File file, std::vector<int> notes, float veloc
engine_->allSoundsOff(); // note: dbraun added this
processModulationChanges();
engine_->setSampleRate(kSampleRate);
// engine_->setBpm(bpm);
engine_->updateAllModulationSwitches();
int kSampleRate = getSampleRate();
double sample_time = 1.0 / kSampleRate;
double sample_time = 1.0 / getSampleRate();
double current_time = -kPreProcessSamples * sample_time;
for (int samples = 0; samples < kPreProcessSamples; samples += kBufferSize) {
@ -580,23 +573,21 @@ void SynthBase::renderAudioToFile(File file, std::vector<int> notes, float veloc
}
nb::ndarray<float, nb::shape<2, -1>, nb::numpy> SynthBase::renderAudioToNumpy(const int& midi_note, float velocity, float note_dur, float render_dur) {
static constexpr int kSampleRate = 44100;
static constexpr int kFadeSamples = 200;
static constexpr int kBufferSize = 64;
static constexpr int kPreProcessSamples = 256; // note: dbraun decreased this from 44100.
// Release GIL for the performance-critical section
// nb::gil_scoped_release gil_release; // TODO:
ScopedLock lock(getCriticalSection());
engine_->allSoundsOff(); // note: dbraun added this
processModulationChanges();
engine_->setSampleRate(kSampleRate);
engine_->updateAllModulationSwitches();
int kSampleRate = getSampleRate();
// Preprocess modulation
double sample_time = 1.0 / kSampleRate;
double sample_time = 1.0 / getSampleRate();
double current_time = -kPreProcessSamples * sample_time;
for (int samples = 0; samples < kPreProcessSamples; samples += kBufferSize) {
@ -616,6 +607,8 @@ nb::ndarray<float, nb::shape<2, -1>, nb::numpy> SynthBase::renderAudioToNumpy(co
static_cast<size_t>(total_samples * 2); // stereo: 2 channels
auto* data = new float[total_frames](); // Zero-initialized
auto capsule = nb::capsule(
data, [](void* p) noexcept { delete[] static_cast<float*>(p); });
int baseSample = 0;
@ -641,15 +634,9 @@ nb::ndarray<float, nb::shape<2, -1>, nb::numpy> SynthBase::renderAudioToNumpy(co
}
}
// Re-acquire GIL before creating numpy array
// nb::gil_scoped_acquire gil_acquire; // TODO:
// Create capsule with the data
nb::capsule owner(data, [](void* p) noexcept { delete[] (float*)p; });
// Return the data as a NumPy array
return nb::ndarray<float, nb::shape<2, -1>, nb::numpy>(
data, {2, static_cast<size_t>(total_samples)}, owner);
data, {2, static_cast<size_t>(total_samples)}, capsule);
}
bool SynthBase::renderAudioToFile2(const std::string& output_path, const int& midi_note, float velocity, float note_dur, float render_dur) {

View file

@ -139,7 +139,6 @@ class SynthBase : public MidiManager::Listener {
Tuning* getTuning() { return &tuning_; }
void pySetBPM(float bpm);
void setSampleRate(double sample_rate);
struct ValueChangedCallback : public CallbackMessage {
ValueChangedCallback(std::shared_ptr<SynthBase*> listener, std::string name, vital::mono_float val) :

View file

@ -18,10 +18,6 @@
#include "value.h"
#include "voice_handler.h"
#include "wave_frame.h"
#include <stdexcept>
#include "synth_parameters.h"
#include <cmath>
#include <string>
namespace nb = nanobind;
using namespace vital;
@ -104,165 +100,6 @@ nb::list get_modulation_sources() {
return result;
}
// Get formatted display text for a control (with scaling & units).
static std::string get_control_text(HeadlessSynth &synth, const std::string &name) {
auto &controls = synth.getControls();
auto it = controls.find(name);
if (it == controls.end())
throw std::runtime_error("No control: " + name);
mono_float raw = it->second->value();
const auto &details = Parameters::getDetails(name);
// Discrete/indexed parameters
if (details.string_lookup) {
int count = static_cast<int>(details.max - details.min + 1);
int idx = static_cast<int>(std::lround(raw - details.min));
if (idx < 0) idx = 0;
else if (idx >= count) idx = count - 1;
return details.string_lookup[idx];
}
// Continuous parameters: apply scaling
float skewed;
switch (details.value_scale) {
case ValueDetails::kQuadratic: skewed = raw * raw; break;
case ValueDetails::kCubic: skewed = raw * raw * raw; break;
case ValueDetails::kQuartic: skewed = raw * raw; skewed = skewed * skewed; break;
case ValueDetails::kExponential:
if (details.display_invert)
skewed = 1.0f / std::pow(2.0f, raw);
else
skewed = std::pow(2.0f, raw);
break;
case ValueDetails::kSquareRoot: skewed = std::sqrt(raw); break;
default: skewed = raw; break;
}
float display_val = details.display_multiply * skewed + details.post_offset;
return std::to_string(display_val) + details.display_units;
}
// Wrapper class for Value that knows its parameter name
class ControlValue {
private:
vital::Value* value_;
std::string name_;
HeadlessSynth* synth_;
public:
ControlValue(vital::Value* value, const std::string& name, HeadlessSynth* synth)
: value_(value), name_(name), synth_(synth) {}
// Delegate existing Value methods
float value() const { return value_->value(); }
void set(double v) { value_->set(poly_float(static_cast<float>(v))); }
void set(int v) { value_->set(poly_float(static_cast<float>(v))); }
// Normalized control methods
void set_normalized(double normalized) {
// Clamp to 0-1
normalized = std::max(0.0, std::min(1.0, normalized));
const auto &details = Parameters::getDetails(name_);
float value;
if (details.value_scale == ValueDetails::kIndexed) {
// For indexed parameters, quantize to discrete values
int num_options = static_cast<int>(details.max - details.min + 1);
int index = static_cast<int>(std::round(normalized * (num_options - 1)));
value = details.min + index;
} else {
// For non-linear parameters, normalized represents the display position
// We need to convert: normalized -> display value -> internal value
float value_normalized = static_cast<float>(normalized);
switch (details.value_scale) {
case ValueDetails::kQuadratic:
// Display = internal^2, so internal = sqrt(display)
// normalized maps to display range
value = details.min + std::sqrt(value_normalized) * (details.max - details.min);
break;
case ValueDetails::kCubic:
// Display = internal^3, so internal = display^(1/3)
value = details.min + std::pow(value_normalized, 1.0f/3.0f) * (details.max - details.min);
break;
case ValueDetails::kQuartic: {
// VST behavior: knob position represents quartic of display position
// When knob is at 50%, we want display = 4 * 0.5^4 = 0.25 seconds
// Since display = internal^4 and max_display = max_internal^4
// We want internal = (normalized^4 * max_display)^(1/4) = normalized * max_internal
value = details.min + value_normalized * (details.max - details.min);
break;
}
case ValueDetails::kExponential:
// For exponential, normalized maps directly to the exponent
if (details.display_invert)
value = details.min + (1.0f / std::pow(2.0f, value_normalized)) * (details.max - details.min);
else
value = details.min + std::pow(2.0f, value_normalized) * (details.max - details.min);
break;
case ValueDetails::kSquareRoot:
// Display = sqrt(internal), so internal = display^2
value = details.min + (value_normalized * value_normalized) * (details.max - details.min);
break;
default: // Linear
value = details.min + value_normalized * (details.max - details.min);
break;
}
}
value_->set(value);
}
double get_normalized() const {
const auto &details = Parameters::getDetails(name_);
float raw = value_->value();
if (details.value_scale == ValueDetails::kIndexed) {
// For indexed parameters, normalize based on index
int num_options = static_cast<int>(details.max - details.min + 1);
int index = static_cast<int>(std::round(raw - details.min));
return static_cast<double>(index) / (num_options - 1);
} else {
// Normalize to 0-1 range within parameter bounds
float normalized_internal = (raw - details.min) / (details.max - details.min);
float normalized;
// Convert internal value to display position (0-1)
switch (details.value_scale) {
case ValueDetails::kQuadratic:
// Display = internal^2
normalized = normalized_internal * normalized_internal;
break;
case ValueDetails::kCubic:
// Display = internal^3
normalized = normalized_internal * normalized_internal * normalized_internal;
break;
case ValueDetails::kQuartic:
// VST behavior: normalized position = internal position
normalized = normalized_internal;
break;
case ValueDetails::kExponential:
// For exponential, need to account for the range
if (details.display_invert)
normalized = -std::log2(normalized_internal + 1e-10f);
else
normalized = std::log2(normalized_internal + 1e-10f);
break;
case ValueDetails::kSquareRoot:
// Display = sqrt(internal)
normalized = std::sqrt(normalized_internal);
break;
default: // Linear
normalized = normalized_internal;
break;
}
return std::max(0.0, std::min(1.0, static_cast<double>(normalized)));
}
}
std::string get_text() const {
return get_control_text(*synth_, name_);
}
};
NB_MODULE(vita, m) {
@ -275,14 +112,17 @@ NB_MODULE(vita, m) {
auto m_constants = m.def_submodule("constants", "Submodule containing constants and enums");
// Expose Enums
nb::enum_<constants::SourceDestination>(m_constants, "SourceDestination", nb::is_arithmetic())
nb::enum_<constants::SourceDestination>(m_constants, "SourceDestination")
.value("Filter1", constants::SourceDestination::kFilter1)
.value("Filter2", constants::SourceDestination::kFilter2)
.value("DualFilters", constants::SourceDestination::kDualFilters)
.value("Effects", constants::SourceDestination::kEffects)
.value("DirectOut", constants::SourceDestination::kDirectOut);
.value("DirectOut", constants::SourceDestination::kDirectOut)
.def("__int__", [](constants::SourceDestination self) {
return static_cast<int>(self);
});
nb::enum_<constants::Effect>(m_constants, "Effect", nb::is_arithmetic())
nb::enum_<constants::Effect>(m_constants, "Effect")
.value("Chorus", constants::Effect::kChorus)
.value("Compressor", constants::Effect::kCompressor)
.value("Delay", constants::Effect::kDelay)
@ -291,9 +131,11 @@ NB_MODULE(vita, m) {
.value("FilterFx", constants::Effect::kFilterFx)
.value("Flanger", constants::Effect::kFlanger)
.value("Phaser", constants::Effect::kPhaser)
.value("Reverb", constants::Effect::kReverb);
.value("Reverb", constants::Effect::kReverb)
.def("__int__",
[](constants::Effect self) { return static_cast<int>(self); });
nb::enum_<constants::FilterModel>(m_constants, "FilterModel", nb::is_arithmetic())
nb::enum_<constants::FilterModel>(m_constants, "FilterModel")
.value("Analog", constants::FilterModel::kAnalog)
.value("Dirty", constants::FilterModel::kDirty)
.value("Ladder", constants::FilterModel::kLadder)
@ -301,51 +143,20 @@ NB_MODULE(vita, m) {
.value("Diode", constants::FilterModel::kDiode)
.value("Formant", constants::FilterModel::kFormant)
.value("Comb", constants::FilterModel::kComb)
.value("Phase", constants::FilterModel::kPhase);
nb::enum_<constants::RetriggerStyle>(m_constants, "RetriggerStyle", nb::is_arithmetic())
.value("Free", constants::RetriggerStyle::kFree)
.value("Retrigger", constants::RetriggerStyle::kRetrigger)
.value("SyncToPlayHead", constants::RetriggerStyle::kSyncToPlayHead);
// Parameter value scaling types
nb::enum_<vital::ValueDetails::ValueScale>(m_constants, "ValueScale", nb::is_arithmetic())
.value("Indexed", vital::ValueDetails::kIndexed)
.value("Linear", vital::ValueDetails::kLinear)
.value("Quadratic", vital::ValueDetails::kQuadratic)
.value("Cubic", vital::ValueDetails::kCubic)
.value("Quartic", vital::ValueDetails::kQuartic)
.value("SquareRoot", vital::ValueDetails::kSquareRoot)
.value("Exponential", vital::ValueDetails::kExponential);
// ControlInfo provides metadata for each parameter
nb::class_<vital::ValueDetails>(m, "ControlInfo")
.def(nb::init<>())
.def_ro("name", &vital::ValueDetails::name)
.def_ro("min", &vital::ValueDetails::min)
.def_ro("max", &vital::ValueDetails::max)
.def_ro("default_value", &vital::ValueDetails::default_value)
.def_ro("version_added", &vital::ValueDetails::version_added)
.def_ro("post_offset", &vital::ValueDetails::post_offset)
.def_ro("display_multiply", &vital::ValueDetails::display_multiply)
.def_ro("scale", &vital::ValueDetails::value_scale)
.def_ro("display_units", &vital::ValueDetails::display_units)
.def_ro("display_name", &vital::ValueDetails::display_name)
.def_prop_ro("is_discrete",
[](const vital::ValueDetails &d) {
return d.value_scale == vital::ValueDetails::kIndexed;
})
.def_prop_ro("options", [](const vital::ValueDetails &d) {
nb::list opts;
if (d.value_scale == vital::ValueDetails::kIndexed && d.string_lookup) {
int count = static_cast<int>(d.max - d.min + 1);
for (int i = 0; i < count; ++i)
opts.append(std::string(d.string_lookup[i]));
}
return opts;
.value("Phase", constants::FilterModel::kPhase)
.def("__int__", [](constants::FilterModel self) {
return static_cast<int>(self);
});
nb::enum_<SynthOscillator::SpectralMorph>(m_constants, "SpectralMorph", nb::is_arithmetic())
nb::enum_<constants::RetriggerStyle>(m_constants, "RetriggerStyle")
.value("Free", constants::RetriggerStyle::kFree)
.value("Retrigger", constants::RetriggerStyle::kRetrigger)
.value("SyncToPlayHead", constants::RetriggerStyle::kSyncToPlayHead)
.def("__int__", [](constants::RetriggerStyle self) {
return static_cast<int>(self);
});
nb::enum_<SynthOscillator::SpectralMorph>(m_constants, "SpectralMorph")
.value("NoSpectralMorph", SynthOscillator::SpectralMorph::kNoSpectralMorph)
.value("Vocode", SynthOscillator::SpectralMorph::kVocode)
.value("FormScale", SynthOscillator::SpectralMorph::kFormScale)
@ -357,9 +168,12 @@ NB_MODULE(vita, m) {
.value("HighPass", SynthOscillator::SpectralMorph::kHighPass)
.value("PhaseDisperse", SynthOscillator::SpectralMorph::kPhaseDisperse)
.value("ShepardTone", SynthOscillator::SpectralMorph::kShepardTone)
.value("Skew", SynthOscillator::SpectralMorph::kSkew);
.value("Skew", SynthOscillator::SpectralMorph::kSkew)
.def("__int__", [](SynthOscillator::SpectralMorph self) {
return static_cast<int>(self);
});
nb::enum_<SynthOscillator::DistortionType>(m_constants, "DistortionType", nb::is_arithmetic())
nb::enum_<SynthOscillator::DistortionType>(m_constants, "DistortionType")
.value("None", SynthOscillator::DistortionType::kNone)
.value("Sync", SynthOscillator::DistortionType::kSync)
.value("Formant", SynthOscillator::DistortionType::kFormant)
@ -372,9 +186,12 @@ NB_MODULE(vita, m) {
.value("FmSample", SynthOscillator::DistortionType::kFmSample)
.value("RmOscillatorA", SynthOscillator::DistortionType::kRmOscillatorA)
.value("RmOscillatorB", SynthOscillator::DistortionType::kRmOscillatorB)
.value("RmSample", SynthOscillator::DistortionType::kRmSample);
.value("RmSample", SynthOscillator::DistortionType::kRmSample)
.def("__int__", [](SynthOscillator::DistortionType self) {
return static_cast<int>(self);
});
nb::enum_<SynthOscillator::UnisonStackType>(m_constants, "UnisonStackType", nb::is_arithmetic())
nb::enum_<SynthOscillator::UnisonStackType>(m_constants, "UnisonStackType")
.value("Normal", SynthOscillator::UnisonStackType::kNormal)
.value("CenterDropOctave", SynthOscillator::UnisonStackType::kCenterDropOctave)
.value("CenterDropOctave2", SynthOscillator::UnisonStackType::kCenterDropOctave2)
@ -386,54 +203,75 @@ NB_MODULE(vita, m) {
.value("MinorChord", SynthOscillator::UnisonStackType::kMinorChord)
.value("HarmonicSeries", SynthOscillator::UnisonStackType::kHarmonicSeries)
.value("OddHarmonicSeries",
SynthOscillator::UnisonStackType::kOddHarmonicSeries);
SynthOscillator::UnisonStackType::kOddHarmonicSeries)
.def("__int__", [](SynthOscillator::UnisonStackType self) {
return static_cast<int>(self);
});
nb::enum_<RandomLfo::RandomType>(m_constants, "RandomLFOStyle", nb::is_arithmetic())
nb::enum_<RandomLfo::RandomType>(m_constants, "RandomLFOStyle")
.value("Perlin", RandomLfo::RandomType::kPerlin)
.value("SampleAndHold", RandomLfo::RandomType::kSampleAndHold)
.value("SinInterpolate", RandomLfo::RandomType::kSinInterpolate)
.value("LorenzAttractor", RandomLfo::RandomType::kLorenzAttractor);
.value("LorenzAttractor", RandomLfo::RandomType::kLorenzAttractor)
.def("__int__",
[](RandomLfo::RandomType self) { return static_cast<int>(self); });
nb::enum_<VoiceHandler::VoicePriority>(m_constants, "VoicePriority", nb::is_arithmetic())
nb::enum_<VoiceHandler::VoicePriority>(m_constants, "VoicePriority")
.value("Newest", VoiceHandler::VoicePriority::kNewest)
.value("Oldest", VoiceHandler::VoicePriority::kOldest)
.value("Highest", VoiceHandler::VoicePriority::kHighest)
.value("Lowest", VoiceHandler::VoicePriority::kLowest)
.value("RoundRobin", VoiceHandler::VoicePriority::kRoundRobin);
.value("RoundRobin", VoiceHandler::VoicePriority::kRoundRobin)
.def("__int__", [](VoiceHandler::VoicePriority self) {
return static_cast<int>(self);
});
nb::enum_<VoiceHandler::VoiceOverride>(m_constants, "VoiceOverride", nb::is_arithmetic())
nb::enum_<VoiceHandler::VoiceOverride>(m_constants, "VoiceOverride")
.value("Kill",VoiceHandler::VoiceOverride::kKill)
.value("Steal", VoiceHandler::VoiceOverride::kSteal);
.value("Steal", VoiceHandler::VoiceOverride::kSteal)
.def("__int__", [](VoiceHandler::VoiceOverride self) {
return static_cast<int>(self);
});
nb::enum_<PredefinedWaveFrames::Shape>(m_constants, "WaveShape", nb::is_arithmetic())
nb::enum_<PredefinedWaveFrames::Shape>(m_constants, "WaveShape")
.value("Sin", PredefinedWaveFrames::kSin)
.value("SaturatedSin", PredefinedWaveFrames::kSaturatedSin)
.value("Triangle", PredefinedWaveFrames::kTriangle)
.value("Square", PredefinedWaveFrames::kSquare)
.value("Pulse", PredefinedWaveFrames::kPulse)
.value("Saw", PredefinedWaveFrames::kSaw);
.value("Saw", PredefinedWaveFrames::kSaw)
.def("__int__", [](PredefinedWaveFrames::Shape self) {
return static_cast<int>(self);
});
nb::enum_<SynthLfo::SyncType>(m_constants, "SynthLFOSyncType", nb::is_arithmetic())
nb::enum_<SynthLfo::SyncType>(m_constants, "SynthLFOSyncType")
.value("Trigger", SynthLfo::SyncType::kTrigger)
.value("Sync", SynthLfo::SyncType::kSync)
.value("Envelope", SynthLfo::SyncType::kEnvelope)
.value("SustainEnvelope", SynthLfo::SyncType::kSustainEnvelope)
.value("LoopPoint", SynthLfo::SyncType::kLoopPoint)
.value("LoopHold", SynthLfo::SyncType::kLoopHold);
.value("LoopHold", SynthLfo::SyncType::kLoopHold)
.def("__int__",
[](SynthLfo::SyncType self) { return static_cast<int>(self); });
nb::enum_<MultibandCompressor::BandOptions>(m_constants, "CompressorBandOption", nb::is_arithmetic())
nb::enum_<MultibandCompressor::BandOptions>(m_constants, "CompressorBandOption")
.value("Multiband", MultibandCompressor::BandOptions::kMultiband)
.value("LowBand", MultibandCompressor::BandOptions::kLowBand)
.value("HighBand", MultibandCompressor::BandOptions::kHighBand)
.value("SingleBand", MultibandCompressor::BandOptions::kSingleBand);
.value("SingleBand", MultibandCompressor::BandOptions::kSingleBand)
.def("__int__", [](MultibandCompressor::BandOptions self) {
return static_cast<int>(self);
});
nb::enum_<SynthFilter::Style>(m_constants, "SynthFilterStyle", nb::is_arithmetic())
nb::enum_<SynthFilter::Style>(m_constants, "SynthFilterStyle")
.value("k12Db", SynthFilter::Style::k12Db)
.value("k24Db", SynthFilter::Style::k24Db)
.value("NotchPassSwap", SynthFilter::Style::kNotchPassSwap)
.value("DualNotchBand", SynthFilter::Style::kDualNotchBand)
.value("BandPeakNotch", SynthFilter::Style::kBandPeakNotch)
.value("Shelving", SynthFilter::Style::kShelving);
.value("Shelving", SynthFilter::Style::kShelving)
.def("__int__",
[](SynthFilter::Style self) { return static_cast<int>(self); });
// https://github.com/mtytel/vital/blob/636ca0ef517a4db087a6a08a6a8a5e704e21f836/src/interface/look_and_feel/synth_strings.h#L174
enum SyncedFrequencyName {
@ -451,7 +289,7 @@ NB_MODULE(vita, m) {
k1_64
};
nb::enum_<SyncedFrequencyName>(m_constants, "SyncedFrequency", nb::is_arithmetic())
nb::enum_<SyncedFrequencyName>(m_constants, "SyncedFrequency")
.value("k32_1", SyncedFrequencyName::k32_1)
.value("k16_1", SyncedFrequencyName::k16_1)
.value("k8_1", SyncedFrequencyName::k8_1)
@ -463,7 +301,9 @@ NB_MODULE(vita, m) {
.value("k1_8", SyncedFrequencyName::k1_8)
.value("k1_16", SyncedFrequencyName::k1_16)
.value("k1_32", SyncedFrequencyName::k1_32)
.value("k1_64", SyncedFrequencyName::k1_64);
.value("k1_64", SyncedFrequencyName::k1_64)
.def("__int__",
[](SyncedFrequencyName self) { return static_cast<int>(self); });
// https://github.com/mtytel/vital/blob/636ca0ef517a4db087a6a08a6a8a5e704e21f836/src/synthesis/modulators/synth_lfo.h#L58C1-L65C9
enum SyncOption {
@ -474,12 +314,13 @@ NB_MODULE(vita, m) {
kKeytrack,
};
nb::enum_<SyncOption>(m_constants, "SynthLFOSyncOption", nb::is_arithmetic())
nb::enum_<SyncOption>(m_constants, "SynthLFOSyncOption")
.value("Time", SyncOption::kTime)
.value("Tempo", SyncOption::kTempo)
.value("DottedTempo", SyncOption::kDottedTempo)
.value("TripletTempo", SyncOption::kTripletTempo)
.value("Keytrack", SyncOption::kKeytrack);
.value("Keytrack", SyncOption::kKeytrack)
.def("__int__", [](SyncOption self) { return static_cast<int>(self); });
// Binding for poly_float
nb::class_<vital::poly_float>(m, "poly_float")
@ -533,32 +374,11 @@ NB_MODULE(vita, m) {
return "<CRValue value=" + std::to_string(v.value()) + ">";
});
// Bind the ControlValue wrapper class
nb::class_<ControlValue>(m, "ControlValue")
.def("value", &ControlValue::value)
.def("set", nb::overload_cast<double>(&ControlValue::set), nb::arg("value"))
.def("set", nb::overload_cast<int>(&ControlValue::set), nb::arg("value"))
.def("set_normalized", &ControlValue::set_normalized, nb::arg("value"),
"Set control value using normalized 0-1 range")
.def("get_normalized", &ControlValue::get_normalized,
"Get control value as normalized 0-1 range")
.def("get_text", &ControlValue::get_text,
"Get formatted display text for the control");
// Expose the SynthBase class, specifying ProcessorRouter as its base
nb::class_<HeadlessSynth>(m, "Synth")
.def(nb::init<>()) // Ensure there's a default constructor or adjust
// accordingly
// Bind the first overload of connectModulation
.def("__getstate__", [](HeadlessSynth &synth) {
return const_cast<HeadlessSynth &>(synth).pyToJson(); // Removes const safely
})
.def("__setstate__", [](HeadlessSynth &synth, const std::string &json) {
new (&synth) HeadlessSynth();
synth.loadFromString(json);
})
.def("connect_modulation",
nb::overload_cast<const std::string &, const std::string &>(
&HeadlessSynth::pyConnectModulation),
@ -572,7 +392,6 @@ NB_MODULE(vita, m) {
"Disconnects a modulation source from a destination by name.")
.def("set_bpm", &HeadlessSynth::pySetBPM, nb::arg("bpm"))
.def("set_sample_rate", &HeadlessSynth::setSampleRate, nb::arg("sample_rate"))
.def("render_file", &HeadlessSynth::renderAudioToFile2,
nb::arg("output_path"), nb::arg("midi_note"),
@ -611,21 +430,5 @@ NB_MODULE(vita, m) {
.def("load_init_preset", &HeadlessSynth::loadInitPreset, "Load the initial preset.")
.def("clear_modulations", &HeadlessSynth::clearModulations)
.def("get_controls", [](HeadlessSynth &synth) {
nb::dict result;
auto &controls = synth.getControls();
for (const auto &[name, value] : controls) {
result[name.c_str()] = ControlValue(value, name, &synth);
}
return result;
}, nb::rv_policy::reference_internal)
.def("get_control_details", [](HeadlessSynth &synth, const std::string &name) {
// Validate control name
if (!vital::Parameters::isParameter(name))
throw std::runtime_error("No metadata for control: " + name);
// Return parameter metadata
return vital::Parameters::getDetails(name);
}, nb::arg("name"), "Get metadata for a control")
.def("get_control_text", get_control_text)
;
.def("get_controls", &HeadlessSynth::getControls, nb::rv_policy::reference);
}

View file

@ -1,144 +0,0 @@
import pytest
import vita
from vita.constants import ValueScale
def test_normalized_linear_parameter():
"""Test normalized control on a linear parameter"""
synth = vita.Synth()
# Get a linear parameter (most parameters are linear by default)
info = synth.get_control_details("filter_1_cutoff")
if info.scale != ValueScale.Linear:
pytest.skip("filter_1_cutoff is not linear in this version")
# Test boundary values
controls = synth.get_controls()
controls["filter_1_cutoff"].set_normalized(0.0)
assert abs(controls["filter_1_cutoff"].get_normalized() - 0.0) < 0.001
assert abs(controls["filter_1_cutoff"].value() - info.min) < 0.001
controls["filter_1_cutoff"].set_normalized(1.0)
assert abs(controls["filter_1_cutoff"].get_normalized() - 1.0) < 0.001
assert abs(controls["filter_1_cutoff"].value() - info.max) < 0.001
# Test middle value
controls["filter_1_cutoff"].set_normalized(0.5)
assert abs(controls["filter_1_cutoff"].get_normalized() - 0.5) < 0.001
expected_mid = info.min + 0.5 * (info.max - info.min)
assert abs(controls["filter_1_cutoff"].value() - expected_mid) < 0.001
def test_normalized_quartic_parameter():
"""Test normalized control on a quartic parameter like env_1_delay"""
synth = vita.Synth()
# Verify env_1_delay uses quartic scaling
info = synth.get_control_details("env_1_delay")
assert info.scale == ValueScale.Quartic
# Test boundary values
controls = synth.get_controls()
controls["env_1_delay"].set_normalized(0.0)
assert abs(controls["env_1_delay"].get_normalized() - 0.0) < 0.001
controls["env_1_delay"].set_normalized(1.0)
assert abs(controls["env_1_delay"].get_normalized() - 1.0) < 0.001
# Test that normalized 0.5 maps to 0.25 seconds display (0.5^4 * 4.0)
# For quartic parameters in VST, normalized maps linearly to internal range
# but the knob DISPLAY position has a quartic relationship
controls["env_1_delay"].set_normalized(0.5)
expected_value = info.min + 0.5 * (info.max - info.min) # Linear mapping
assert abs(controls["env_1_delay"].value() - expected_value) < 0.001
assert abs(controls["env_1_delay"].get_normalized() - 0.5) < 0.001
actual_value = controls["env_1_delay"].value()
linear_mid = info.min + 0.5 * (info.max - info.min)
# With this implementation, normalized maps linearly to internal range
assert abs(actual_value - linear_mid) < 0.001
def test_normalized_indexed_parameter():
"""Test normalized control on an indexed/discrete parameter"""
synth = vita.Synth()
# delay_style is an indexed parameter
info = synth.get_control_details("delay_style")
assert info.scale == ValueScale.Indexed
assert info.is_discrete
num_options = len(info.options)
# Test each discrete value
controls = synth.get_controls()
for i in range(num_options):
normalized = i / (num_options - 1) if num_options > 1 else 0
controls["delay_style"].set_normalized(normalized)
# Check it maps to the correct index
actual_index = int(controls["delay_style"].value() - info.min)
assert actual_index == i
# Check text matches
assert controls["delay_style"].get_text() == info.options[i]
def test_normalized_vst_style_automation():
"""Demonstrate VST-style parameter automation using normalized values"""
synth = vita.Synth()
# Simulate a VST automation curve for filter cutoff
# This would be how a DAW sends parameter changes
automation_points = [
(0.0, 0.0), # Start closed
(0.25, 0.5), # Open to middle
(0.5, 1.0), # Full open
(0.75, 0.3), # Partially close
(1.0, 0.0), # Close again
]
controls = synth.get_controls()
for time, normalized_value in automation_points:
controls["filter_1_cutoff"].set_normalized(normalized_value)
# Verify the normalized value is preserved
retrieved = controls["filter_1_cutoff"].get_normalized()
assert abs(retrieved - normalized_value) < 0.001
# In a real scenario, you would render audio here
# audio = synth.render(60, 0.8, 0.1, 0.1)
def test_normalized_clipping():
"""Test that normalized values are properly clipped to 0-1 range"""
synth = vita.Synth()
# Test values outside 0-1 range
controls = synth.get_controls()
controls["filter_1_cutoff"].set_normalized(-0.5)
assert controls["filter_1_cutoff"].get_normalized() == 0.0
controls["filter_1_cutoff"].set_normalized(1.5)
assert controls["filter_1_cutoff"].get_normalized() == 1.0
if __name__ == "__main__":
# Run basic tests
test_normalized_linear_parameter()
print("✓ Linear parameter test passed")
test_normalized_quartic_parameter()
print("✓ Quartic parameter test passed")
test_normalized_indexed_parameter()
print("✓ Indexed parameter test passed")
test_normalized_vst_style_automation()
print("✓ VST-style automation test passed")
test_normalized_clipping()
print("✓ Clipping test passed")
print("\nAll normalized parameter tests passed!")

View file

@ -20,14 +20,15 @@ from vita.constants import (
SyncedFrequency,
)
SAMPLE_RATE = 44_100
def test_render(bpm=120.0, sample_rate=48000, note_dur=1.0, render_dur=3.0, pitch=36, velocity=0.7):
def test_render(bpm=120.0, note_dur=1.0, render_dur=3.0, pitch=36, velocity=0.7):
synth = vita.Synth()
# The initial preset is laoded by default.
synth.set_bpm(bpm)
synth.set_sample_rate(sample_rate)
assert vita.get_modulation_sources()
assert vita.get_modulation_destinations()
@ -37,14 +38,12 @@ def test_render(bpm=120.0, sample_rate=48000, note_dur=1.0, render_dur=3.0, pitc
controls = synth.get_controls()
controls["modulation_1_amount"].set(1.0)
controls["filter_1_on"].set(1.0)
assert 1.0 == controls["filter_1_on"].value()
controls["lfo_1_tempo"].set(SyncedFrequency.k1_16)
# Render audio to numpy array shaped (2, NUM_SAMPLES)
audio = synth.render(pitch, velocity, note_dur, render_dur)
assert sample_rate == int(audio.shape[1] / render_dur) # assume int
wavfile.write("generated_preset.wav", sample_rate, audio.T)
wavfile.write("generated_preset.wav", SAMPLE_RATE, audio.T)
# Dump current state to json text
json_text = synth.to_json()
@ -54,10 +53,8 @@ def test_render(bpm=120.0, sample_rate=48000, note_dur=1.0, render_dur=3.0, pitc
# Load JSON text
with open(preset_path, "r") as f:
json_text1 = f.read()
assert synth.load_json(json_text1)
assert json_text == json_text1
json_text = f.read()
assert synth.load_json(json_text)
# Or load directly from file:
assert synth.load_preset(preset_path)
@ -66,42 +63,3 @@ def test_render(bpm=120.0, sample_rate=48000, note_dur=1.0, render_dur=3.0, pitc
synth.load_init_preset()
# Or just clear modulations.
synth.clear_modulations()
info = synth.get_control_details("delay_style")
print("min:", info.min)
print("max:", info.max)
print("default_value:", info.default_value)
print("scale:", info.scale) # e.g. ValueScale.Indexed
print("discrete:", info.is_discrete) # True
print("options: ", info.options) # ["Mono","Stereo","Ping Pong","Mid Ping Pong"]
print("display_name:", info.display_name)
print("display_units:", info.display_units)
controls["delay_style"].set(0)
text = synth.get_control_text("delay_style")
assert text == info.options[0]
print("text: ", text)
controls["delay_style"].set(1)
text = synth.get_control_text("delay_style")
assert text == info.options[1]
print("text: ", text)
assert info.is_discrete
for i in range(int(info.min), int(info.max)+1):
controls["delay_style"].set(i)
info = synth.get_control_details("env_1_delay")
print("min:", info.min)
print("max:", info.max)
print("default_value:", info.default_value)
print("scale:", info.scale) # e.g. ValueScale.Quartic
print("discrete:", info.is_discrete) # False
print("options: ", info.options) # [""]
assert not info.is_discrete
steps = 100
for i in range(steps):
pct = i/(steps-1)
# linearly interpolate
y = info.min + pct * (info.max-info.min)
controls["env_1_delay"].set(y)

@ -1 +1 @@
Subproject commit 109af19428a8784aa02fed55a5b3e21617de93ef
Subproject commit efbeb8afd95b210d9e2e06f719ddc1336a12b9d7

View file

@ -1,9 +1,2 @@
from .vita import Synth, constants, get_modulation_sources, get_modulation_destinations
from .version import __version__
__ALL__ = [
"Synth",
"constants",
"get_modulation_sources",
"get_modulation_destinations",
]
from .vita import *
from .version import *

View file

@ -1,39 +1 @@
from ..vita.constants import (
SourceDestination,
Effect,
FilterModel,
RetriggerStyle,
SpectralMorph,
DistortionType,
UnisonStackType,
RandomLFOStyle,
SynthFilterStyle,
WaveShape,
CompressorBandOption,
VoiceOverride,
VoicePriority,
SynthLFOSyncOption,
SynthLFOSyncType,
SyncedFrequency,
ValueScale,
)
__ALL__ = [
"SourceDestination",
"Effect",
"FilterModel",
"RetriggerStyle",
"SpectralMorph",
"DistortionType",
"UnisonStackType",
"RandomLFOStyle",
"SynthFilterStyle",
"WaveShape",
"CompressorBandOption",
"VoiceOverride",
"VoicePriority",
"SynthLFOSyncOption",
"SynthLFOSyncType",
"SyncedFrequency",
"ValueScale",
]
from ..vita.constants import *

View file

@ -1 +1 @@
__version__ = "0.0.5"
__version__ = "0.0.1"