Browse Source

Merge branch 'v0.6' of https://github.com/VCVRack/Rack into v0.6

tags/v0.6.2b
Andrew Belt 6 years ago
parent
commit
6f2550197d
22 changed files with 547 additions and 360 deletions
  1. +8
    -0
      CHANGELOG.md
  2. +2
    -2
      Makefile
  3. +2
    -2
      README.md
  4. +4
    -0
      include/asset.hpp
  5. +1
    -38
      include/dsp/decimator.hpp
  6. +27
    -12
      include/dsp/digital.hpp
  7. +22
    -20
      include/dsp/fir.hpp
  8. +1
    -0
      include/dsp/functions.hpp
  9. +55
    -15
      include/dsp/ode.hpp
  10. +174
    -0
      include/dsp/resampler.hpp
  11. +1
    -98
      include/dsp/samplerate.hpp
  12. +130
    -0
      include/helpers.hpp
  13. +1
    -81
      include/rack.hpp
  14. +2
    -2
      include/util/common.hpp
  15. +6
    -0
      include/util/math.hpp
  16. +6
    -4
      installer.nsi
  17. +17
    -17
      res/ComponentLibrary/TL1105_0.svg
  18. +17
    -17
      res/ComponentLibrary/TL1105_1.svg
  19. +1
    -1
      src/Core/AudioInterface.cpp
  20. +1
    -1
      src/app/ModuleWidget.cpp
  21. +62
    -49
      src/asset.cpp
  22. +7
    -1
      src/main.cpp

+ 8
- 0
CHANGELOG.md View File

@@ -1,4 +1,12 @@


### 0.6.2 (2018-07-13)

- Added module presets
- Added [command line arguments](https://vcvrack.com/manual/Installing.html#command-line-usage) for setting Rack directories
- Improved UI/engine communication stability
- [VCV Bridge](https://vcvrack.com/manual/Bridge.html) 0.6.2
- Added VST FX plugin

### 0.6.1 (2018-06-17) ### 0.6.1 (2018-06-17)


- Added gamepad MIDI driver - Added gamepad MIDI driver


+ 2
- 2
Makefile View File

@@ -1,5 +1,5 @@
RACK_DIR ?= . RACK_DIR ?= .
VERSION = 0.6.1
VERSION = 0.6.2


FLAGS += \ FLAGS += \
-Iinclude \ -Iinclude \
@@ -121,7 +121,7 @@ endif
ifdef ARCH_WIN ifdef ARCH_WIN
mkdir -p dist/Rack mkdir -p dist/Rack
mkdir -p dist/Rack/Bridge mkdir -p dist/Rack/Bridge
cp Bridge/VST/dist/VCV-Bridge-{32,64,-fx-32,-fx-64}.dll dist/Rack/Bridge/
cp Bridge/VST/dist/VCV-Bridge-{32,64,fx-32,fx-64}.dll dist/Rack/Bridge/
cp -R LICENSE* res dist/Rack/ cp -R LICENSE* res dist/Rack/
cp $(TARGET) dist/Rack/ cp $(TARGET) dist/Rack/
$(STRIP) -s dist/Rack/$(TARGET) $(STRIP) -s dist/Rack/$(TARGET)


+ 2
- 2
README.md View File

@@ -97,8 +97,8 @@ All **source code** in this repository is licensed under [BSD-3-Clause](LICENSE.


**Component Library graphics** in `res/ComponentLibrary` are licensed under [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) by [Grayscale](http://grayscale.info/). Commercial plugins must request a commercial license to use Component Library graphics by emailing contact@vcvrack.com. **Component Library graphics** in `res/ComponentLibrary` are licensed under [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) by [Grayscale](http://grayscale.info/). Commercial plugins must request a commercial license to use Component Library graphics by emailing contact@vcvrack.com.


**Core** panel graphics in `res/Core` are copyright © 2017 Grayscale. You may not create derivative works of Core panels.
**Core panel graphics** in `res/Core` are copyright © 2017 Grayscale. You may not create derivative works of Core panels.


The **VCV logo and icon** are copyright © 2017 Andrew Belt and may not be used in derivative works. The **VCV logo and icon** are copyright © 2017 Andrew Belt and may not be used in derivative works.


The **"VCV" name** is trademarked and may not be used for unofficial products. However, it is acceptable to use the phrase "for VCV Rack" for promotion of your plugin. For all other purposes, email contact@vcvrack.com.
The **"VCV" name** is trademarked and may not be used for unofficial products. However, it is acceptable to use the phrase "for VCV Rack" for promotion of your Rack plugin. For all other purposes, email contact@vcvrack.com.

+ 4
- 0
include/asset.hpp View File

@@ -16,4 +16,8 @@ std::string assetLocal(std::string filename);
std::string assetPlugin(Plugin *plugin, std::string filename); std::string assetPlugin(Plugin *plugin, std::string filename);




extern std::string assetGlobalDir;
extern std::string assetLocalDir;


} // namespace rack } // namespace rack

+ 1
- 38
include/dsp/decimator.hpp View File

@@ -1,39 +1,2 @@
#pragma once #pragma once

#include "string.h"
#include "dsp/ringbuffer.hpp"
#include "dsp/fir.hpp"


namespace rack {

template<int OVERSAMPLE, int QUALITY>
struct Decimator {
DoubleRingBuffer<float, OVERSAMPLE*QUALITY> inBuffer;
float kernel[OVERSAMPLE*QUALITY];

Decimator(float cutoff = 0.9) {
boxcarFIR(kernel, OVERSAMPLE*QUALITY, cutoff * 0.5 / OVERSAMPLE);
blackmanHarrisWindow(kernel, OVERSAMPLE*QUALITY);
// The sum of the kernel should be 1
float sum = 0.0;
for (int i = 0; i < OVERSAMPLE*QUALITY; i++) {
sum += kernel[i];
}
for (int i = 0; i < OVERSAMPLE*QUALITY; i++) {
kernel[i] /= sum;
}
// Zero input buffer
memset(inBuffer.data, 0, sizeof(inBuffer.data));
}
/** `in` must be OVERSAMPLE floats long */
float process(float *in) {
memcpy(inBuffer.endData(), in, OVERSAMPLE*sizeof(float));
inBuffer.endIncr(OVERSAMPLE);
float out = convolve(inBuffer.endData() + OVERSAMPLE*QUALITY, kernel, OVERSAMPLE*QUALITY);
// Ignore the ring buffer's start position
return out;
}
};

} // namespace rack
#include "resampler.hpp"

+ 27
- 12
include/dsp/digital.hpp View File

@@ -6,7 +6,7 @@
namespace rack { namespace rack {




/** Turns high when value reaches 1, turns low when value reaches 0 */
/** Turns HIGH when value reaches 1.f, turns LOW when value reaches 0.f. */
struct SchmittTrigger { struct SchmittTrigger {
// UNKNOWN is used to represent a stable state when the previous state is not yet set // UNKNOWN is used to represent a stable state when the previous state is not yet set
enum State { enum State {
@@ -14,7 +14,14 @@ struct SchmittTrigger {
LOW, LOW,
HIGH HIGH
}; };
State state = UNKNOWN;
State state;

SchmittTrigger() {
reset();
}
void reset() {
state = UNKNOWN;
}
/** Updates the state of the Schmitt Trigger given a value. /** Updates the state of the Schmitt Trigger given a value.
Returns true if triggered, i.e. the value increases from 0 to 1. Returns true if triggered, i.e. the value increases from 0 to 1.
If different trigger thresholds are needed, use If different trigger thresholds are needed, use
@@ -48,25 +55,33 @@ struct SchmittTrigger {
bool isHigh() { bool isHigh() {
return state == HIGH; return state == HIGH;
} }
void reset() {
state = UNKNOWN;
}
}; };




/** When triggered, holds a high value for a specified time before going low again */ /** When triggered, holds a high value for a specified time before going low again */
struct PulseGenerator { struct PulseGenerator {
float time = 0.f;
float pulseTime = 0.f;
float time;
float triggerDuration;

PulseGenerator() {
reset();
}
/** Immediately resets the state to LOW */
void reset() {
time = 0.f;
triggerDuration = 0.f;
}
/** Advances the state by `deltaTime`. Returns whether the pulse is in the HIGH state. */
bool process(float deltaTime) { bool process(float deltaTime) {
time += deltaTime; time += deltaTime;
return time < pulseTime;
return time < triggerDuration;
} }
void trigger(float pulseTime) {
// Keep the previous pulseTime if the existing pulse would be held longer than the currently requested one.
if (time + pulseTime >= this->pulseTime) {
/** Begins a trigger with the given `triggerDuration`. */
void trigger(float triggerDuration) {
// Keep the previous triggerDuration if the existing pulse would be held longer than the currently requested one.
if (time + triggerDuration >= this->triggerDuration) {
time = 0.f; time = 0.f;
this->pulseTime = pulseTime;
this->triggerDuration = triggerDuration;
} }
} }
}; };


+ 22
- 20
include/dsp/fir.hpp View File

@@ -5,34 +5,36 @@


namespace rack { namespace rack {


/** Perform a direct convolution
x[-len + 1] to x[0] must be defined
*/
inline float convolve(const float *x, const float *kernel, int len) {
float y = 0.0;
/** Performs a direct sum convolution */
inline float convolveNaive(const float *in, const float *kernel, int len) {
float y = 0.f;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
y += x[-i] * kernel[i];
y += in[len - 1 - i] * kernel[i];
} }
return y; return y;
} }


inline void blackmanHarrisWindow(float *x, int n) {
const float a0 = 0.35875;
const float a1 = 0.48829;
const float a2 = 0.14128;
const float a3 = 0.01168;
for (int i = 0; i < n; i++) {
x[i] *= a0
- a1 * cosf(2 * M_PI * i / (n - 1))
+ a2 * cosf(4 * M_PI * i / (n - 1))
- a3 * cosf(6 * M_PI * i / (n - 1));
/** Computes the impulse response of a boxcar lowpass filter */
inline void boxcarLowpassIR(float *out, int len, float cutoff = 0.5f) {
for (int i = 0; i < len; i++) {
float t = i - (len - 1) / 2.f;
out[i] = 2 * cutoff * sinc(2 * cutoff * t);
} }
} }


inline void boxcarFIR(float *x, int n, float cutoff) {
for (int i = 0; i < n; i++) {
float t = (float)i / (n - 1) * 2.0 - 1.0;
x[i] = sinc(t * n * cutoff);
inline void blackmanHarrisWindow(float *x, int len) {
// Constants from https://en.wikipedia.org/wiki/Window_function#Blackman%E2%80%93Harris_window
const float a0 = 0.35875f;
const float a1 = 0.48829f;
const float a2 = 0.14128f;
const float a3 = 0.01168f;
float factor = 2*M_PI / (len - 1);
for (int i = 0; i < len; i++) {
x[i] *=
+ a0
- a1 * cosf(1*factor * i)
+ a2 * cosf(2*factor * i)
- a3 * cosf(3*factor * i);
} }
} }




+ 1
- 0
include/dsp/functions.hpp View File

@@ -6,6 +6,7 @@
namespace rack { namespace rack {




/** The normalized sinc function. https://en.wikipedia.org/wiki/Sinc_function */
inline float sinc(float x) { inline float sinc(float x) {
if (x == 0.f) if (x == 0.f)
return 1.f; return 1.f;


+ 55
- 15
include/dsp/ode.hpp View File

@@ -2,47 +2,87 @@




namespace rack { namespace rack {
namespace ode {


typedef void (*stepCallback)(float x, const float y[], float dydt[]);
/** The callback function `f` in each of these stepping functions must have the signature


/** Solve an ODE system using the 1st order Euler method */
inline void stepEuler(stepCallback f, float x, float dx, float y[], int len) {
void f(float t, const float x[], float dxdt[])

A capturing lambda is ideal for this.
For example, the following solves the system x''(t) = -x(t) using a fixed timestep of 0.01 and initial conditions x(0) = 1, x'(0) = 0.

float x[2] = {1.f, 0.f};
float dt = 0.01f;
for (float t = 0.f; t < 1.f; t += dt) {
rack::ode::stepRK4(t, dt, x, 2, [&](float t, const float x[], float dxdt[]) {
dxdt[0] = x[1];
dxdt[1] = -x[0];
});
printf("%f\n", x[0]);
}

*/

/** Solves an ODE system using the 1st order Euler method */
template<typename F>
void stepEuler(float t, float dt, float x[], int len, F f) {
float k[len]; float k[len];


f(x, y, k);
f(t, x, k);
for (int i = 0; i < len; i++) {
x[i] += dt * k[i];
}
}

/** Solves an ODE system using the 2nd order Runge-Kutta method */
template<typename F>
void stepRK2(float t, float dt, float x[], int len, F f) {
float k1[len];
float k2[len];
float yi[len];

f(t, x, k1);

for (int i = 0; i < len; i++) {
yi[i] = x[i] + k1[i] * dt / 2.f;
}
f(t + dt / 2.f, yi, k2);

for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
y[i] += dx * k[i];
x[i] += dt * k2[i];
} }
} }


/** Solve an ODE system using the 4th order Runge-Kutta method */
inline void stepRK4(stepCallback f, float x, float dx, float y[], int len) {
/** Solves an ODE system using the 4th order Runge-Kutta method */
template<typename F>
void stepRK4(float t, float dt, float x[], int len, F f) {
float k1[len]; float k1[len];
float k2[len]; float k2[len];
float k3[len]; float k3[len];
float k4[len]; float k4[len];
float yi[len]; float yi[len];


f(x, y, k1);
f(t, x, k1);


for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
yi[i] = y[i] + k1[i] * dx / 2.0;
yi[i] = x[i] + k1[i] * dt / 2.f;
} }
f(x + dx / 2.0, yi, k2);
f(t + dt / 2.f, yi, k2);


for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
yi[i] = y[i] + k2[i] * dx / 2.0;
yi[i] = x[i] + k2[i] * dt / 2.f;
} }
f(x + dx / 2.0, yi, k3);
f(t + dt / 2.f, yi, k3);


for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
yi[i] = y[i] + k3[i] * dx;
yi[i] = x[i] + k3[i] * dt;
} }
f(x + dx, yi, k4);
f(t + dt, yi, k4);


for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
y[i] += dx * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]) / 6.0;
x[i] += dt * (k1[i] + 2.f * k2[i] + 2.f * k3[i] + k4[i]) / 6.f;
} }
} }


} // namespace ode
} // namespace rack } // namespace rack

+ 174
- 0
include/dsp/resampler.hpp View File

@@ -0,0 +1,174 @@
#pragma once

#include <assert.h>
#include <string.h>
#include <speex/speex_resampler.h>
#include "frame.hpp"
#include "ringbuffer.hpp"
#include "fir.hpp"


namespace rack {

template<int CHANNELS>
struct SampleRateConverter {
SpeexResamplerState *st = NULL;
int channels = CHANNELS;
int quality = SPEEX_RESAMPLER_QUALITY_DEFAULT;
int inRate = 44100;
int outRate = 44100;

SampleRateConverter() {
refreshState();
}
~SampleRateConverter() {
if (st) {
speex_resampler_destroy(st);
}
}

/** Sets the number of channels to actually process. This can be at most CHANNELS. */
void setChannels(int channels) {
assert(channels <= CHANNELS);
if (channels == this->channels)
return;
this->channels = channels;
refreshState();
}

/** From 0 (worst, fastest) to 10 (best, slowest) */
void setQuality(int quality) {
if (quality == this->quality)
return;
this->quality = quality;
refreshState();
}

void setRates(int inRate, int outRate) {
if (inRate == this->inRate && outRate == this->outRate)
return;
this->inRate = inRate;
this->outRate = outRate;
refreshState();
}

void refreshState() {
if (st) {
speex_resampler_destroy(st);
st = NULL;
}

if (channels > 0 && inRate != outRate) {
int err;
st = speex_resampler_init(channels, inRate, outRate, quality, &err);
assert(st);
assert(err == RESAMPLER_ERR_SUCCESS);

speex_resampler_set_input_stride(st, CHANNELS);
speex_resampler_set_output_stride(st, CHANNELS);
}
}

/** `in` and `out` are interlaced with the number of channels */
void process(const Frame<CHANNELS> *in, int *inFrames, Frame<CHANNELS> *out, int *outFrames) {
assert(in);
assert(inFrames);
assert(out);
assert(outFrames);
if (st) {
// Resample each channel at a time
spx_uint32_t inLen;
spx_uint32_t outLen;
for (int i = 0; i < channels; i++) {
inLen = *inFrames;
outLen = *outFrames;
int err = speex_resampler_process_float(st, i, ((const float*) in) + i, &inLen, ((float*) out) + i, &outLen);
assert(err == RESAMPLER_ERR_SUCCESS);
}
*inFrames = inLen;
*outFrames = outLen;
}
else {
// Simply copy the buffer without conversion
int frames = min(*inFrames, *outFrames);
memcpy(out, in, frames * sizeof(Frame<CHANNELS>));
*inFrames = frames;
*outFrames = frames;
}
}
};


template<int OVERSAMPLE, int QUALITY>
struct Decimator {
float inBuffer[OVERSAMPLE*QUALITY];
float kernel[OVERSAMPLE*QUALITY];
int inIndex;

Decimator(float cutoff = 0.9f) {
boxcarLowpassIR(kernel, OVERSAMPLE*QUALITY, cutoff * 0.5f / OVERSAMPLE);
blackmanHarrisWindow(kernel, OVERSAMPLE*QUALITY);
reset();
}
void reset() {
inIndex = 0;
memset(inBuffer, 0, sizeof(inBuffer));
}
/** `in` must be length OVERSAMPLE */
float process(float *in) {
// Copy input to buffer
memcpy(&inBuffer[inIndex], in, OVERSAMPLE*sizeof(float));
// Advance index
inIndex += OVERSAMPLE;
inIndex %= OVERSAMPLE*QUALITY;
// Perform naive convolution
float out = 0.f;
for (int i = 0; i < OVERSAMPLE*QUALITY; i++) {
int index = inIndex - 1 - i;
index = (index + OVERSAMPLE*QUALITY) % (OVERSAMPLE*QUALITY);
out += kernel[i] * inBuffer[index];
}
return out;
}
};


template<int OVERSAMPLE, int QUALITY>
struct Upsampler {
float inBuffer[QUALITY];
float kernel[OVERSAMPLE*QUALITY];
int inIndex;

Upsampler(float cutoff = 0.9f) {
boxcarLowpassIR(kernel, OVERSAMPLE*QUALITY, cutoff * 0.5f / OVERSAMPLE);
blackmanHarrisWindow(kernel, OVERSAMPLE*QUALITY);
reset();
}
void reset() {
inIndex = 0;
memset(inBuffer, 0, sizeof(inBuffer));
}
/** `out` must be length OVERSAMPLE */
void process(float in, float *out) {
// Zero-stuff input buffer
inBuffer[inIndex] = OVERSAMPLE * in;
// Advance index
inIndex++;
inIndex %= QUALITY;
// Naively convolve each sample
// TODO replace with polyphase filter hierarchy
for (int i = 0; i < OVERSAMPLE; i++) {
float y = 0.f;
for (int j = 0; j < QUALITY; j++) {
int index = inIndex - 1 - j;
index = (index + QUALITY) % QUALITY;
int kernelIndex = OVERSAMPLE * j + i;
y += kernel[kernelIndex] * inBuffer[index];
}
out[i] = y;
}
}
};


} // namespace rack

+ 1
- 98
include/dsp/samplerate.hpp View File

@@ -1,99 +1,2 @@
#pragma once #pragma once

#include <assert.h>
#include <string.h>
#include <speex/speex_resampler.h>
#include "frame.hpp"


namespace rack {

template<int CHANNELS>
struct SampleRateConverter {
SpeexResamplerState *st = NULL;
int channels = CHANNELS;
int quality = SPEEX_RESAMPLER_QUALITY_DEFAULT;
int inRate = 44100;
int outRate = 44100;

SampleRateConverter() {
refreshState();
}
~SampleRateConverter() {
if (st) {
speex_resampler_destroy(st);
}
}

/** Sets the number of channels to actually process. This can be at most CHANNELS. */
void setChannels(int channels) {
assert(channels <= CHANNELS);
if (channels == this->channels)
return;
this->channels = channels;
refreshState();
}

/** From 0 (worst, fastest) to 10 (best, slowest) */
void setQuality(int quality) {
if (quality == this->quality)
return;
this->quality = quality;
refreshState();
}

void setRates(int inRate, int outRate) {
if (inRate == this->inRate && outRate == this->outRate)
return;
this->inRate = inRate;
this->outRate = outRate;
refreshState();
}

void refreshState() {
if (st) {
speex_resampler_destroy(st);
st = NULL;
}

if (channels > 0 && inRate != outRate) {
int err;
st = speex_resampler_init(channels, inRate, outRate, quality, &err);
assert(st);
assert(err == RESAMPLER_ERR_SUCCESS);

speex_resampler_set_input_stride(st, CHANNELS);
speex_resampler_set_output_stride(st, CHANNELS);
}
}

/** `in` and `out` are interlaced with the number of channels */
void process(const Frame<CHANNELS> *in, int *inFrames, Frame<CHANNELS> *out, int *outFrames) {
assert(in);
assert(inFrames);
assert(out);
assert(outFrames);
if (st) {
// Resample each channel at a time
spx_uint32_t inLen;
spx_uint32_t outLen;
for (int i = 0; i < channels; i++) {
inLen = *inFrames;
outLen = *outFrames;
int err = speex_resampler_process_float(st, i, ((const float*) in) + i, &inLen, ((float*) out) + i, &outLen);
assert(err == RESAMPLER_ERR_SUCCESS);
}
*inFrames = inLen;
*outFrames = outLen;
}
else {
// Simply copy the buffer without conversion
int frames = min(*inFrames, *outFrames);
memcpy(out, in, frames * sizeof(Frame<CHANNELS>));
*inFrames = frames;
*outFrames = frames;
}
}
};

} // namespace rack
#include "resampler.hpp"

+ 130
- 0
include/helpers.hpp View File

@@ -0,0 +1,130 @@
#include "plugin.hpp"
#include "engine.hpp"
#include "app.hpp"


namespace rack {


template <class TModule, class TModuleWidget, typename... Tags>
Model *createModel(std::string author, std::string slug, std::string name, Tags... tags) {
struct TModel : Model {
Module *createModule() override {
TModule *module = new TModule();
return module;
}
ModuleWidget *createModuleWidget() override {
TModule *module = new TModule();
TModuleWidget *moduleWidget = new TModuleWidget(module);
moduleWidget->model = this;
return moduleWidget;
}
ModuleWidget *createModuleWidgetNull() override {
TModuleWidget *moduleWidget = new TModuleWidget(NULL);
moduleWidget->model = this;
return moduleWidget;
}
};
Model *model = new TModel();
model->author = author;
model->slug = slug;
model->name = name;
model->tags = {tags...};
return model;
}

template <class TWidget>
TWidget *createWidget(Vec pos) {
TWidget *w = new TWidget();
w->box.pos = pos;
return w;
}

/** Deprecated. Use createWidget<TScrew>() instead */
template <class TScrew>
DEPRECATED TScrew *createScrew(Vec pos) {
return createWidget<TScrew>(pos);
}

template <class TParamWidget>
TParamWidget *createParam(Vec pos, Module *module, int paramId, float minValue, float maxValue, float defaultValue) {
TParamWidget *param = new TParamWidget();
param->box.pos = pos;
param->module = module;
param->paramId = paramId;
param->setLimits(minValue, maxValue);
param->setDefaultValue(defaultValue);
return param;
}

template <class TParamWidget>
TParamWidget *createParamCentered(Vec pos, Module *module, int paramId, float minValue, float maxValue, float defaultValue) {
TParamWidget *param = new TParamWidget();
param->box.pos = pos.minus(param->box.size.div(2));
param->module = module;
param->paramId = paramId;
param->setLimits(minValue, maxValue);
param->setDefaultValue(defaultValue);
return param;
}

template <class TPort>
TPort *createInput(Vec pos, Module *module, int inputId) {
TPort *port = new TPort();
port->box.pos = pos;
port->module = module;
port->type = Port::INPUT;
port->portId = inputId;
return port;
}

template <class TPort>
TPort *createInputCentered(Vec pos, Module *module, int inputId) {
TPort *port = new TPort();
port->box.pos = pos.minus(port->box.size.div(2));
port->module = module;
port->type = Port::INPUT;
port->portId = inputId;
return port;
}

template <class TPort>
TPort *createOutput(Vec pos, Module *module, int outputId) {
TPort *port = new TPort();
port->box.pos = pos;
port->module = module;
port->type = Port::OUTPUT;
port->portId = outputId;
return port;
}

template <class TPort>
TPort *createOutputCentered(Vec pos, Module *module, int outputId) {
TPort *port = new TPort();
port->box.pos = pos.minus(port->box.size.div(2));
port->module = module;
port->type = Port::OUTPUT;
port->portId = outputId;
return port;
}

template <class TModuleLightWidget>
TModuleLightWidget *createLight(Vec pos, Module *module, int firstLightId) {
TModuleLightWidget *light = new TModuleLightWidget();
light->box.pos = pos;
light->module = module;
light->firstLightId = firstLightId;
return light;
}

template <class TModuleLightWidget>
TModuleLightWidget *createLightCentered(Vec pos, Module *module, int firstLightId) {
TModuleLightWidget *light = new TModuleLightWidget();
light->box.pos = pos.minus(light->box.size.div(2));
light->module = module;
light->firstLightId = firstLightId;
return light;
}


} // namespace rack

+ 1
- 81
include/rack.hpp View File

@@ -9,84 +9,4 @@
#include "app.hpp" #include "app.hpp"
#include "ui.hpp" #include "ui.hpp"
#include "componentlibrary.hpp" #include "componentlibrary.hpp"


namespace rack {


////////////////////
// helpers
////////////////////

/** Deprecated, use Model::create<TModule, TModuleWidget>(...) instead */
template <class TModuleWidget, typename... Tags>
DEPRECATED Model *createModel(std::string author, std::string slug, std::string name, Tags... tags) {
struct TModel : Model {
ModuleWidget *createModuleWidget() override {
ModuleWidget *moduleWidget = new TModuleWidget();
moduleWidget->model = this;
return moduleWidget;
}
};
Model *model = new TModel();
model->author = author;
model->slug = slug;
model->name = name;
model->tags = {tags...};
return model;
}

/** Deprecated, use Widget::create<TScrew>() instead */
template <class TScrew>
DEPRECATED TScrew *createScrew(Vec pos) {
TScrew *screw = new TScrew();
screw->box.pos = pos;
return screw;
}

/** Deprecated, use ParamWidget::create<TParamWidget>() instead */
template <class TParamWidget>
DEPRECATED TParamWidget *createParam(Vec pos, Module *module, int paramId, float minValue, float maxValue, float defaultValue) {
TParamWidget *param = new TParamWidget();
param->box.pos = pos;
param->module = module;
param->paramId = paramId;
param->setLimits(minValue, maxValue);
param->setDefaultValue(defaultValue);
return param;
}

/** Deprecated, use Port::create<TPort>(..., Port::INPUT, ...) instead */
template <class TPort>
DEPRECATED TPort *createInput(Vec pos, Module *module, int inputId) {
TPort *port = new TPort();
port->box.pos = pos;
port->module = module;
port->type = Port::INPUT;
port->portId = inputId;
return port;
}

/** Deprecated, use Port::create<TPort>(..., Port::OUTPUT, ...) instead */
template <class TPort>
DEPRECATED TPort *createOutput(Vec pos, Module *module, int outputId) {
TPort *port = new TPort();
port->box.pos = pos;
port->module = module;
port->type = Port::OUTPUT;
port->portId = outputId;
return port;
}

/** Deprecated, use ModuleLightWidget::create<TModuleLightWidget>() instead */
template<class TModuleLightWidget>
DEPRECATED TModuleLightWidget *createLight(Vec pos, Module *module, int firstLightId) {
TModuleLightWidget *light = new TModuleLightWidget();
light->box.pos = pos;
light->module = module;
light->firstLightId = firstLightId;
return light;
}


} // namespace rack
#include "helpers.hpp"

+ 2
- 2
include/util/common.hpp View File

@@ -116,14 +116,14 @@ Example:
fclose(file); fclose(file);
}); });
*/ */
template <typename F>
template<typename F>
struct DeferWrapper { struct DeferWrapper {
F f; F f;
DeferWrapper(F f) : f(f) {} DeferWrapper(F f) : f(f) {}
~DeferWrapper() { f(); } ~DeferWrapper() { f(); }
}; };


template <typename F>
template<typename F>
DeferWrapper<F> deferWrapper(F f) { DeferWrapper<F> deferWrapper(F f) {
return DeferWrapper<F>(f); return DeferWrapper<F>(f);
} }


+ 6
- 0
include/util/math.hpp View File

@@ -297,6 +297,12 @@ struct Rect {
r.size = size.plus(delta.mult(2.f)); r.size = size.plus(delta.mult(2.f));
return r; return r;
} }
Rect shrink(Vec delta) {
Rect r;
r.pos = pos.plus(delta);
r.size = size.minus(delta.mult(2.f));
return r;
}
}; };






+ 6
- 4
installer.nsi View File

@@ -33,13 +33,13 @@ RequestExecutionLevel admin
Var VST_64_DIR Var VST_64_DIR
!define MUI_DIRECTORYPAGE_VARIABLE $VST_64_DIR !define MUI_DIRECTORYPAGE_VARIABLE $VST_64_DIR
!define MUI_DIRECTORYPAGE_TEXT_TOP "Bridge VST 64-bit plugin install directory"
!define MUI_DIRECTORYPAGE_TEXT_TOP "Bridge 64-bit VST plugin install directory"
!define MUI_PAGE_CUSTOMFUNCTION_PRE VST_64_DIR_PRE !define MUI_PAGE_CUSTOMFUNCTION_PRE VST_64_DIR_PRE
!insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_DIRECTORY
Var VST_32_DIR Var VST_32_DIR
!define MUI_DIRECTORYPAGE_VARIABLE $VST_32_DIR !define MUI_DIRECTORYPAGE_VARIABLE $VST_32_DIR
!define MUI_DIRECTORYPAGE_TEXT_TOP "Bridge VST 32-bit plugin install directory"
!define MUI_DIRECTORYPAGE_TEXT_TOP "Bridge 32-bit VST plugin install directory"
!define MUI_PAGE_CUSTOMFUNCTION_PRE VST_32_DIR_PRE !define MUI_PAGE_CUSTOMFUNCTION_PRE VST_32_DIR_PRE
!insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_DIRECTORY
@@ -79,17 +79,19 @@ Section "VCV Rack" VCVRACK
SectionEnd SectionEnd
Section "Bridge VST 64-bit plugin" VST_64
Section "Bridge 64-bit VST plugin" VST_64
StrCpy $OUTDIR $VST_64_DIR StrCpy $OUTDIR $VST_64_DIR
CreateDirectory $OUTDIR CreateDirectory $OUTDIR
File "dist\Rack\Bridge\VCV-Bridge-64.dll" File "dist\Rack\Bridge\VCV-Bridge-64.dll"
File "dist\Rack\Bridge\VCV-Bridge-fx-64.dll"
SectionEnd SectionEnd
Section "Bridge VST 32-bit plugin" VST_32
Section "Bridge 32-bit VST plugin" VST_32
StrCpy $OUTDIR $VST_32_DIR StrCpy $OUTDIR $VST_32_DIR
CreateDirectory $OUTDIR CreateDirectory $OUTDIR
File "dist\Rack\Bridge\VCV-Bridge-32.dll" File "dist\Rack\Bridge\VCV-Bridge-32.dll"
File "dist\Rack\Bridge\VCV-Bridge-fx-32.dll"
SectionEnd SectionEnd


+ 17
- 17
res/ComponentLibrary/TL1105_0.svg View File

@@ -9,12 +9,12 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="15.999999"
height="15.997643"
viewBox="0 0 4.2333331 4.2327099"
width="5.4186664mm"
height="5.4178686mm"
viewBox="0 0 5.4186663 5.4178686"
version="1.1" version="1.1"
id="svg12484" id="svg12484"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="TL1105_0.svg"> sodipodi:docname="TL1105_0.svg">
<defs <defs
id="defs12478" /> id="defs12478" />
@@ -25,9 +25,9 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="44.8"
inkscape:cx="1.1769543"
inkscape:cy="7.1864792"
inkscape:zoom="11.2"
inkscape:cx="6.3388991"
inkscape:cy="5.196676"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:current-layer="g12430" inkscape:current-layer="g12430"
showgrid="false" showgrid="false"
@@ -35,12 +35,12 @@
fit-margin-left="0" fit-margin-left="0"
fit-margin-right="0" fit-margin-right="0"
fit-margin-bottom="0" fit-margin-bottom="0"
inkscape:window-width="2560"
inkscape:window-height="1422"
inkscape:window-width="1600"
inkscape:window-height="882"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="18" inkscape:window-y="18"
inkscape:window-maximized="0" inkscape:window-maximized="0"
units="px" />
units="mm" />
<metadata <metadata
id="metadata12481"> id="metadata12481">
<rdf:RDF> <rdf:RDF>
@@ -57,13 +57,13 @@
inkscape:label="Layer 1" inkscape:label="Layer 1"
inkscape:groupmode="layer" inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(-87.434409,-107.81432)">
transform="translate(-86.841742,-107.22174)">
<g <g
transform="matrix(0.15583204,0,0,-0.15583204,44.616575,295.36332)" transform="matrix(0.15583204,0,0,-0.15583204,44.616575,295.36332)"
id="g12430" id="g12430"
style="stroke-width:2.26383328"> style="stroke-width:2.26383328">
<g <g
transform="translate(301.93513,1189.951)"
transform="matrix(1.28,0,0,1.28,305.73837,1189.9507)"
id="g5959-5" id="g5959-5"
style="stroke-width:2.26383328"> style="stroke-width:2.26383328">
<path <path
@@ -73,19 +73,19 @@
d="m 0,0 c 0,-7.5 -6.083,-13.58 -13.584,-13.58 -7.5,0 -13.582,6.08 -13.582,13.58 0,7.503 6.082,13.582 13.582,13.582 C -6.083,13.582 0,7.503 0,0" /> d="m 0,0 c 0,-7.5 -6.083,-13.58 -13.584,-13.58 -7.5,0 -13.582,6.08 -13.582,13.58 0,7.503 6.082,13.582 13.582,13.582 C -6.083,13.582 0,7.503 0,0" />
</g> </g>
<circle <circle
style="opacity:1;vector-effect:none;fill:#676967;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.26383328;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
style="opacity:1;vector-effect:none;fill:#676967;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.89770651;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="circle12436" id="circle12436"
cx="288.35214" cx="288.35214"
cy="-1190.6586"
r="11.277138"
cy="-1190.8564"
r="14.434736"
transform="scale(1,-1)" /> transform="scale(1,-1)" />
<circle <circle
transform="scale(1,-1)" transform="scale(1,-1)"
r="11.277138"
r="14.434736"
cy="-1189.952" cy="-1189.952"
cx="288.35214" cx="288.35214"
id="path12411" id="path12411"
style="opacity:1;vector-effect:none;fill:#2b2d2b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.26383328;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
style="opacity:1;vector-effect:none;fill:#2b2d2b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.89770651;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g> </g>
</g> </g>
</svg> </svg>

+ 17
- 17
res/ComponentLibrary/TL1105_1.svg View File

@@ -9,12 +9,12 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="15.999999"
height="15.997643"
viewBox="0 0 4.2333331 4.2327099"
width="5.4186664mm"
height="5.4178686mm"
viewBox="0 0 5.4186663 5.4178686"
version="1.1" version="1.1"
id="svg12484" id="svg12484"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="TL1105_1.svg"> sodipodi:docname="TL1105_1.svg">
<defs <defs
id="defs12478" /> id="defs12478" />
@@ -25,9 +25,9 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="44.8"
inkscape:cx="5.4698259"
inkscape:cy="8.0360204"
inkscape:zoom="15.839192"
inkscape:cx="5.770688"
inkscape:cy="6.8891413"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:current-layer="g12476" inkscape:current-layer="g12476"
showgrid="false" showgrid="false"
@@ -35,12 +35,12 @@
fit-margin-left="0" fit-margin-left="0"
fit-margin-right="0" fit-margin-right="0"
fit-margin-bottom="0" fit-margin-bottom="0"
inkscape:window-width="2560"
inkscape:window-height="1422"
inkscape:window-width="1600"
inkscape:window-height="882"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="18" inkscape:window-y="18"
inkscape:window-maximized="0" inkscape:window-maximized="0"
units="px" />
units="mm" />
<metadata <metadata
id="metadata12481"> id="metadata12481">
<rdf:RDF> <rdf:RDF>
@@ -57,14 +57,14 @@
inkscape:label="Layer 1" inkscape:label="Layer 1"
inkscape:groupmode="layer" inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(-155.97001,-114.87325)">
transform="translate(-155.37735,-114.28067)">
<g <g
id="g12476" id="g12476"
transform="matrix(0.15583204,0,0,-0.15583204,113.15218,302.42225)" transform="matrix(0.15583204,0,0,-0.15583204,113.15218,302.42225)"
style="stroke-width:2.26383328"> style="stroke-width:2.26383328">
<g <g
id="g12468" id="g12468"
transform="translate(301.93513,1189.951)"
transform="matrix(1.28,0,0,1.28,305.73837,1189.9507)"
style="stroke-width:2.26383328"> style="stroke-width:2.26383328">
<path <path
d="m 0,0 c 0,-7.5 -6.083,-13.58 -13.584,-13.58 -7.5,0 -13.582,6.08 -13.582,13.58 0,7.503 6.082,13.582 13.582,13.582 C -6.083,13.582 0,7.503 0,0" d="m 0,0 c 0,-7.5 -6.083,-13.58 -13.584,-13.58 -7.5,0 -13.582,6.08 -13.582,13.58 0,7.503 6.082,13.582 13.582,13.582 C -6.083,13.582 0,7.503 0,0"
@@ -74,17 +74,17 @@
</g> </g>
<circle <circle
transform="scale(1,-1)" transform="scale(1,-1)"
r="11.277138"
cy="-1189.2383"
r="14.434736"
cy="-1189.0385"
cx="288.35214" cx="288.35214"
id="circle12472" id="circle12472"
style="opacity:1;vector-effect:none;fill:#676967;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.26383328;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
style="opacity:1;vector-effect:none;fill:#676967;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.89770651;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<circle <circle
style="opacity:1;vector-effect:none;fill:#141514;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.26383328;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
style="opacity:1;vector-effect:none;fill:#141514;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.89770651;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="circle12474" id="circle12474"
cx="288.35214" cx="288.35214"
cy="-1189.952" cy="-1189.952"
r="11.277138"
r="14.434736"
transform="scale(1,-1)" /> transform="scale(1,-1)" />
</g> </g>
</g> </g>


+ 1
- 1
src/Core/AudioInterface.cpp View File

@@ -6,7 +6,7 @@
#include <condition_variable> #include <condition_variable>
#include "Core.hpp" #include "Core.hpp"
#include "audio.hpp" #include "audio.hpp"
#include "dsp/samplerate.hpp"
#include "dsp/resampler.hpp"
#include "dsp/ringbuffer.hpp" #include "dsp/ringbuffer.hpp"






+ 1
- 1
src/app/ModuleWidget.cpp View File

@@ -295,7 +295,7 @@ void ModuleWidget::draw(NVGcontext *vg) {
nvgScissor(vg, 0, 0, box.size.x, box.size.y); nvgScissor(vg, 0, 0, box.size.x, box.size.y);
Widget::draw(vg); Widget::draw(vg);


// CPU meter
// Power meter
if (module && gPowerMeter) { if (module && gPowerMeter) {
nvgBeginPath(vg); nvgBeginPath(vg);
nvgRect(vg, nvgRect(vg,


+ 62
- 49
src/asset.cpp View File

@@ -23,73 +23,86 @@
namespace rack { namespace rack {




static std::string globalDir;
static std::string localDir;
std::string assetGlobalDir;
std::string assetLocalDir;




void assetInit(bool devMode) { void assetInit(bool devMode) {
if (devMode) {
// Use current working directory if running in development mode
globalDir = ".";
localDir = ".";
return;
}

if (assetGlobalDir.empty()) {
if (devMode) {
assetGlobalDir = ".";
}
else {
#if ARCH_MAC #if ARCH_MAC
CFBundleRef bundle = CFBundleGetMainBundle();
assert(bundle);
CFURLRef resourcesUrl = CFBundleCopyResourcesDirectoryURL(bundle);
char resourcesBuf[PATH_MAX];
Boolean success = CFURLGetFileSystemRepresentation(resourcesUrl, TRUE, (UInt8*) resourcesBuf, sizeof(resourcesBuf));
assert(success);
CFRelease(resourcesUrl);
globalDir = resourcesBuf;

// Get home directory
struct passwd *pw = getpwuid(getuid());
assert(pw);
localDir = pw->pw_dir;
localDir += "/Documents/Rack";
CFBundleRef bundle = CFBundleGetMainBundle();
assert(bundle);
CFURLRef resourcesUrl = CFBundleCopyResourcesDirectoryURL(bundle);
char resourcesBuf[PATH_MAX];
Boolean success = CFURLGetFileSystemRepresentation(resourcesUrl, TRUE, (UInt8*) resourcesBuf, sizeof(resourcesBuf));
assert(success);
CFRelease(resourcesUrl);
assetGlobalDir = resourcesBuf;
#endif #endif
#if ARCH_WIN #if ARCH_WIN
char moduleBuf[MAX_PATH];
DWORD length = GetModuleFileName(NULL, moduleBuf, sizeof(moduleBuf));
assert(length > 0);
PathRemoveFileSpec(moduleBuf);
globalDir = moduleBuf;

// Get "My Documents" folder
char documentsBuf[MAX_PATH];
HRESULT result = SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, documentsBuf);
assert(result == S_OK);
localDir = documentsBuf;
localDir += "/Rack";
char moduleBuf[MAX_PATH];
DWORD length = GetModuleFileName(NULL, moduleBuf, sizeof(moduleBuf));
assert(length > 0);
PathRemoveFileSpec(moduleBuf);
assetGlobalDir = moduleBuf;
#endif #endif
#if ARCH_LIN #if ARCH_LIN
// TODO For now, users should launch Rack from their terminal in the global directory
globalDir = ".";

// Get home directory
const char *homeBuf = getenv("HOME");
if (!homeBuf) {
struct passwd *pw = getpwuid(getuid());
assert(pw);
homeBuf = pw->pw_dir;
// TODO For now, users should launch Rack from their terminal in the global directory
assetGlobalDir = ".";
#endif
}
} }
localDir = homeBuf;
localDir += "/.Rack";

if (assetLocalDir.empty()) {
if (devMode) {
assetLocalDir = ".";
}
else {
#if ARCH_WIN
// Get "My Documents" folder
char documentsBuf[MAX_PATH];
HRESULT result = SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, documentsBuf);
assert(result == S_OK);
assetLocalDir = documentsBuf;
assetLocalDir += "/Rack";
#endif #endif
systemCreateDirectory(localDir);
#if ARCH_MAC
// Get home directory
struct passwd *pw = getpwuid(getuid());
assert(pw);
assetLocalDir = pw->pw_dir;
assetLocalDir += "/Documents/Rack";
#endif
#if ARCH_LIN
// Get home directory
const char *homeBuf = getenv("HOME");
if (!homeBuf) {
struct passwd *pw = getpwuid(getuid());
assert(pw);
homeBuf = pw->pw_dir;
}
assetLocalDir = homeBuf;
assetLocalDir += "/.Rack";
#endif
}
}

systemCreateDirectory(assetGlobalDir);
systemCreateDirectory(assetLocalDir);
} }




std::string assetGlobal(std::string filename) { std::string assetGlobal(std::string filename) {
return globalDir + "/" + filename;
return assetGlobalDir + "/" + filename;
} }




std::string assetLocal(std::string filename) { std::string assetLocal(std::string filename) {
return localDir + "/" + filename;
return assetLocalDir + "/" + filename;
} }






+ 7
- 1
src/main.cpp View File

@@ -26,11 +26,17 @@ int main(int argc, char* argv[]) {
// Parse command line arguments // Parse command line arguments
int c; int c;
opterr = 0; opterr = 0;
while ((c = getopt(argc, argv, "d")) != -1) {
while ((c = getopt(argc, argv, "dg:l:")) != -1) {
switch (c) { switch (c) {
case 'd': { case 'd': {
devMode = true; devMode = true;
} break; } break;
case 'g': {
assetGlobalDir = optarg;
} break;
case 'l': {
assetLocalDir = optarg;
} break;
default: break; default: break;
} }
} }


Loading…
Cancel
Save