@@ -49,13 +49,16 @@ LDFLAGS += \ | |||||
-L$(HOME)/pkg/portaudio-r1891-build/lib/x64/ReleaseMinDependency -lportaudio_x64 \ | -L$(HOME)/pkg/portaudio-r1891-build/lib/x64/ReleaseMinDependency -lportaudio_x64 \ | ||||
-Wl,--export-all-symbols,--out-implib,libRack.a -mwindows | -Wl,--export-all-symbols,--out-implib,libRack.a -mwindows | ||||
TARGET = Rack.exe | TARGET = Rack.exe | ||||
OBJECTS = Rack.res | |||||
all: $(TARGET) | |||||
# OBJECTS = Rack.res | |||||
%.res: %.rc | %.res: %.rc | ||||
windres $^ -O coff -o $@ | windres $^ -O coff -o $@ | ||||
endif | endif | ||||
all: $(TARGET) | |||||
clean: | |||||
rm -rf $(TARGET) build | |||||
include Makefile.inc | include Makefile.inc |
@@ -10,30 +10,37 @@ namespace rack { | |||||
struct KnobDavies1900h : SpriteKnob { | struct KnobDavies1900h : SpriteKnob { | ||||
KnobDavies1900h() { | KnobDavies1900h() { | ||||
box.size = Vec(36, 36); | |||||
spriteOffset = Vec(-8, -8); | |||||
spriteSize = Vec(64, 64); | |||||
minIndex = 44; | |||||
maxIndex = -46; | |||||
// box.size = Vec(36, 36); | |||||
// spriteOffset = Vec(-8, -8); | |||||
// spriteSize = Vec(64, 64); | |||||
// minIndex = 44; | |||||
// maxIndex = -46; | |||||
// spriteCount = 120; | |||||
box.size = Vec(42, 42); | |||||
spriteOffset = Vec(-9, -9); | |||||
spriteSize = Vec(60, 60); | |||||
minIndex = 0; | |||||
maxIndex = 119; | |||||
spriteCount = 120; | spriteCount = 120; | ||||
spriteImage = Image::load("res/Black Plastic small 01.png"); | |||||
} | } | ||||
}; | }; | ||||
struct KnobDavies1900hWhite : KnobDavies1900h { | struct KnobDavies1900hWhite : KnobDavies1900h { | ||||
KnobDavies1900hWhite() { | KnobDavies1900hWhite() { | ||||
spriteImage = Image::load("res/ComponentLibrary/Davies1900hWhite.png"); | |||||
// spriteImage = Image::load("res/ComponentLibrary/Davies1900hWhite.png"); | |||||
} | } | ||||
}; | }; | ||||
struct KnobDavies1900hBlack : KnobDavies1900h { | struct KnobDavies1900hBlack : KnobDavies1900h { | ||||
KnobDavies1900hBlack() { | KnobDavies1900hBlack() { | ||||
spriteImage = Image::load("res/ComponentLibrary/Davies1900hBlack.png"); | |||||
// spriteImage = Image::load("res/ComponentLibrary/Davies1900hBlack.png"); | |||||
} | } | ||||
}; | }; | ||||
struct KnobDavies1900hRed : KnobDavies1900h { | struct KnobDavies1900hRed : KnobDavies1900h { | ||||
KnobDavies1900hRed() { | KnobDavies1900hRed() { | ||||
spriteImage = Image::load("res/ComponentLibrary/Davies1900hRed.png"); | |||||
// spriteImage = Image::load("res/ComponentLibrary/Davies1900hRed.png"); | |||||
} | } | ||||
}; | }; | ||||
@@ -43,10 +50,14 @@ struct KnobDavies1900hRed : KnobDavies1900h { | |||||
struct PJ301M : SpriteWidget { | struct PJ301M : SpriteWidget { | ||||
PJ301M() { | PJ301M() { | ||||
box.size = Vec(24, 24); | |||||
spriteOffset = Vec(-10, -10); | |||||
spriteSize = Vec(48, 48); | |||||
spriteImage = Image::load("res/ComponentLibrary/PJ301M.png"); | |||||
// box.size = Vec(24, 24); | |||||
// spriteOffset = Vec(-10, -10); | |||||
// spriteSize = Vec(48, 48); | |||||
// spriteImage = Image::load("res/ComponentLibrary/PJ301M.png"); | |||||
box.size = Vec(26, 26); | |||||
spriteOffset = Vec(-16, -16); | |||||
spriteSize = Vec(56, 56); | |||||
spriteImage = Image::load("res/port.png"); | |||||
} | } | ||||
}; | }; | ||||
struct InputPortPJ301M : InputPort, PJ301M {}; | struct InputPortPJ301M : InputPort, PJ301M {}; | ||||
@@ -54,10 +65,14 @@ struct OutputPortPJ301M: OutputPort, PJ301M {}; | |||||
struct PJ3410 : SpriteWidget { | struct PJ3410 : SpriteWidget { | ||||
PJ3410() { | PJ3410() { | ||||
box.size = Vec(31, 31); | |||||
spriteOffset = Vec(-9, -9); | |||||
spriteSize = Vec(54, 54); | |||||
spriteImage = Image::load("res/ComponentLibrary/PJ3410.png"); | |||||
// box.size = Vec(31, 31); | |||||
// spriteOffset = Vec(-9, -9); | |||||
// spriteSize = Vec(54, 54); | |||||
// spriteImage = Image::load("res/ComponentLibrary/PJ3410.png"); | |||||
box.size = Vec(26, 26); | |||||
spriteOffset = Vec(-12, -12); | |||||
spriteSize = Vec(56, 56); | |||||
spriteImage = Image::load("res/port.png"); | |||||
} | } | ||||
}; | }; | ||||
struct InputPortPJ3410 : InputPort, PJ3410 {}; | struct InputPortPJ3410 : InputPort, PJ3410 {}; | ||||
@@ -65,10 +80,15 @@ struct OutputPortPJ3410: OutputPort, PJ3410 {}; | |||||
struct CL1362 : SpriteWidget { | struct CL1362 : SpriteWidget { | ||||
CL1362() { | CL1362() { | ||||
box.size = Vec(33, 29); | |||||
spriteOffset = Vec(-10, -10); | |||||
spriteSize = Vec(57, 54); | |||||
spriteImage = Image::load("res/ComponentLibrary/CL1362.png"); | |||||
// box.size = Vec(33, 29); | |||||
// spriteOffset = Vec(-10, -10); | |||||
// spriteSize = Vec(57, 54); | |||||
// spriteImage = Image::load("res/ComponentLibrary/CL1362.png"); | |||||
box.size = Vec(26, 26); | |||||
spriteOffset = Vec(-12, -12); | |||||
spriteSize = Vec(56, 56); | |||||
spriteImage = Image::load("res/port.png"); | |||||
} | } | ||||
}; | }; | ||||
struct InputPortCL1362 : InputPort, CL1362 {}; | struct InputPortCL1362 : InputPort, CL1362 {}; | ||||
@@ -251,11 +251,11 @@ struct SampleRateConverter { | |||||
data.src_ratio = r; | data.src_ratio = r; | ||||
} | } | ||||
/** `in` and `out` are interlaced with the number of channels */ | /** `in` and `out` are interlaced with the number of channels */ | ||||
void process(const float *in, int *inFrames, float *out, int *outFrames) { | |||||
// The const cast is okay since src_process does not modify it | |||||
data.data_in = const_cast<float*>(in); | |||||
void process(const Frame<CHANNELS> *in, int *inFrames, Frame<CHANNELS> *out, int *outFrames) { | |||||
// Old versions of libsamplerate use float* here instead of const float* | |||||
data.data_in = (float*) in; | |||||
data.input_frames = *inFrames; | data.input_frames = *inFrames; | ||||
data.data_out = out; | |||||
data.data_out = (float*) out; | |||||
data.output_frames = *outFrames; | data.output_frames = *outFrames; | ||||
src_process(state, &data); | src_process(state, &data); | ||||
*inFrames = data.input_frames_used; | *inFrames = data.input_frames_used; | ||||
@@ -8,6 +8,8 @@ | |||||
#include <assert.h> | #include <assert.h> | ||||
#include <string> | #include <string> | ||||
#include <condition_variable> | |||||
#include <mutex> | |||||
namespace rack { | namespace rack { | ||||
@@ -33,4 +35,36 @@ std::string stringf(const char *format, ...); | |||||
/** Truncates and adds "..." to a string, not exceeding `len` characters */ | /** Truncates and adds "..." to a string, not exceeding `len` characters */ | ||||
std::string ellipsize(std::string s, size_t len); | std::string ellipsize(std::string s, size_t len); | ||||
/** Threads which obtain a VIPLock will cause wait() to block for other less important threads. | |||||
This does not provide the VIPs with an exclusive lock. That should be left up to another mutex shared between the less important thread. | |||||
*/ | |||||
struct VIPMutex { | |||||
int count = 0; | |||||
std::condition_variable cv; | |||||
std::mutex countMutex; | |||||
/** Blocks until there are no remaining VIPLocks */ | |||||
void wait() { | |||||
std::unique_lock<std::mutex> lock(countMutex); | |||||
while (count > 0) | |||||
cv.wait(lock); | |||||
} | |||||
}; | |||||
struct VIPLock { | |||||
VIPMutex &m; | |||||
VIPLock(VIPMutex &m) : m(m) { | |||||
std::unique_lock<std::mutex> lock(m.countMutex); | |||||
m.count++; | |||||
} | |||||
~VIPLock() { | |||||
std::unique_lock<std::mutex> lock(m.countMutex); | |||||
m.count--; | |||||
lock.unlock(); | |||||
m.cv.notify_all(); | |||||
} | |||||
}; | |||||
} // namespace rack | } // namespace rack |
@@ -1,5 +1,6 @@ | |||||
#include <assert.h> | #include <assert.h> | ||||
#include <mutex> | #include <mutex> | ||||
#include <thread> | |||||
#include <portaudio.h> | #include <portaudio.h> | ||||
#include "core.hpp" | #include "core.hpp" | ||||
#include "dsp.hpp" | #include "dsp.hpp" | ||||
@@ -40,7 +41,8 @@ struct AudioInterface : Module { | |||||
int numInputs = 0; | int numInputs = 0; | ||||
// Used because the GUI thread and Rack thread can both interact with this class | // Used because the GUI thread and Rack thread can both interact with this class | ||||
std::mutex mutex; | |||||
std::mutex bufferMutex; | |||||
bool streamRunning; | |||||
SampleRateConverter<2> inputSrc; | SampleRateConverter<2> inputSrc; | ||||
SampleRateConverter<2> outputSrc; | SampleRateConverter<2> outputSrc; | ||||
@@ -54,6 +56,7 @@ struct AudioInterface : Module { | |||||
AudioInterface(); | AudioInterface(); | ||||
~AudioInterface(); | ~AudioInterface(); | ||||
void step(); | void step(); | ||||
void stepStream(const float *input, float *output, int numFrames); | |||||
int getDeviceCount(); | int getDeviceCount(); | ||||
std::string getDeviceName(int deviceId); | std::string getDeviceName(int deviceId); | ||||
@@ -84,10 +87,23 @@ AudioInterface::~AudioInterface() { | |||||
} | } | ||||
void AudioInterface::step() { | void AudioInterface::step() { | ||||
std::lock_guard<std::mutex> lock(mutex); | |||||
if (!stream) | if (!stream) | ||||
return; | return; | ||||
// Read/write stream if we have enough input, OR the output buffer is empty if we have no input | |||||
if (numOutputs > 0) { | |||||
while (inputSrcBuffer.size() >= blockSize && streamRunning) { | |||||
std::this_thread::sleep_for(std::chrono::duration<float>(100e-6)); | |||||
} | |||||
} | |||||
else if (numInputs > 0) { | |||||
while (outputBuffer.empty() && streamRunning) { | |||||
std::this_thread::sleep_for(std::chrono::duration<float>(100e-6)); | |||||
} | |||||
} | |||||
std::lock_guard<std::mutex> lock(bufferMutex); | |||||
// Get input and pass it through the sample rate converter | // Get input and pass it through the sample rate converter | ||||
if (numOutputs > 0) { | if (numOutputs > 0) { | ||||
if (!inputBuffer.full()) { | if (!inputBuffer.full()) { | ||||
@@ -98,63 +114,63 @@ void AudioInterface::step() { | |||||
} | } | ||||
// Once full, sample rate convert the input | // Once full, sample rate convert the input | ||||
// inputBuffer -> SRC -> inputSrcBuffer | |||||
if (inputBuffer.full()) { | if (inputBuffer.full()) { | ||||
inputSrc.setRatio(sampleRate / gSampleRate); | inputSrc.setRatio(sampleRate / gSampleRate); | ||||
int inLen = inputBuffer.size(); | int inLen = inputBuffer.size(); | ||||
int outLen = inputSrcBuffer.capacity(); | int outLen = inputSrcBuffer.capacity(); | ||||
inputSrc.process((const float*) inputBuffer.startData(), &inLen, (float*) inputSrcBuffer.endData(), &outLen); | |||||
inputSrc.process(inputBuffer.startData(), &inLen, inputSrcBuffer.endData(), &outLen); | |||||
inputBuffer.startIncr(inLen); | inputBuffer.startIncr(inLen); | ||||
inputSrcBuffer.endIncr(outLen); | inputSrcBuffer.endIncr(outLen); | ||||
} | } | ||||
} | } | ||||
// Read/write stream if we have enough input, OR the output buffer is empty if we have no input | |||||
bool streamReady = (numOutputs > 0) ? (inputSrcBuffer.size() >= blockSize) : (outputBuffer.empty()); | |||||
if (streamReady) { | |||||
// printf("%p\t%d\t%d\n", this, inputSrcBuffer.size(), outputBuffer.size()); | |||||
PaError err; | |||||
// Read output from input stream | |||||
// (for some reason, if you write the output stream before you read the input stream, PortAudio can segfault on Windows.) | |||||
if (numInputs > 0) { | |||||
Frame<2> *buf = new Frame<2>[blockSize]; | |||||
err = Pa_ReadStream(stream, (float*) buf, blockSize); | |||||
if (err) { | |||||
// Ignore buffer underflows | |||||
if (err == paInputOverflowed) { | |||||
fprintf(stderr, "Audio input buffer underflow\n"); | |||||
} | |||||
} | |||||
// Set output | |||||
if (!outputBuffer.empty()) { | |||||
Frame<2> f = outputBuffer.shift(); | |||||
setf(outputs[AUDIO1_OUTPUT], 5.0 * f.samples[0]); | |||||
setf(outputs[AUDIO2_OUTPUT], 5.0 * f.samples[1]); | |||||
} | |||||
} | |||||
// Pass output through sample rate converter | |||||
outputSrc.setRatio(gSampleRate / sampleRate); | |||||
int inLen = blockSize; | |||||
int outLen = outputBuffer.capacity(); | |||||
outputSrc.process((float*) buf, &inLen, (float*) outputBuffer.endData(), &outLen); | |||||
outputBuffer.endIncr(outLen); | |||||
delete[] buf; | |||||
void AudioInterface::stepStream(const float *input, float *output, int numFrames) { | |||||
if (numOutputs > 0) { | |||||
// Wait for enough input before proceeding | |||||
while (inputSrcBuffer.size() < numFrames) { | |||||
if (!streamRunning) | |||||
return; | |||||
std::this_thread::sleep_for(std::chrono::duration<float>(100e-6)); | |||||
} | } | ||||
} | |||||
// Write input to output stream | |||||
if (numOutputs > 0) { | |||||
assert(inputSrcBuffer.size() >= blockSize); | |||||
err = Pa_WriteStream(stream, (const float*) inputSrcBuffer.startData(), blockSize); | |||||
inputSrcBuffer.startIncr(blockSize); | |||||
if (err) { | |||||
// Ignore buffer underflows | |||||
if (err == paOutputUnderflowed) { | |||||
fprintf(stderr, "Audio output buffer underflow\n"); | |||||
} | |||||
std::lock_guard<std::mutex> lock(bufferMutex); | |||||
// input stream -> output buffer | |||||
if (numInputs > 0) { | |||||
Frame<2> inputFrames[numFrames]; | |||||
for (int i = 0; i < numFrames; i++) { | |||||
for (int c = 0; c < 2; c++) { | |||||
inputFrames[i].samples[c] = (numInputs > c) ? input[i*numInputs + c] : 0.0; | |||||
} | } | ||||
} | } | ||||
// Pass output through sample rate converter | |||||
outputSrc.setRatio(gSampleRate / sampleRate); | |||||
int inLen = numFrames; | |||||
int outLen = outputBuffer.capacity(); | |||||
outputSrc.process(inputFrames, &inLen, outputBuffer.endData(), &outLen); | |||||
outputBuffer.endIncr(outLen); | |||||
} | } | ||||
// Set output | |||||
if (!outputBuffer.empty()) { | |||||
Frame<2> f = outputBuffer.shift(); | |||||
setf(outputs[AUDIO1_OUTPUT], 5.0 * f.samples[0]); | |||||
setf(outputs[AUDIO2_OUTPUT], 5.0 * f.samples[1]); | |||||
// input buffer -> output stream | |||||
if (numOutputs > 0) { | |||||
for (int i = 0; i < numFrames; i++) { | |||||
if (inputSrcBuffer.empty()) | |||||
break; | |||||
Frame<2> f = inputSrcBuffer.shift(); | |||||
for (int c = 0; c < numOutputs; c++) { | |||||
output[i*numOutputs + c] = f.samples[c]; | |||||
} | |||||
} | |||||
} | } | ||||
} | } | ||||
@@ -170,9 +186,15 @@ std::string AudioInterface::getDeviceName(int deviceId) { | |||||
return stringf("%s: %s (%d in, %d out)", apiInfo->name, info->name, info->maxInputChannels, info->maxOutputChannels); | return stringf("%s: %s (%d in, %d out)", apiInfo->name, info->name, info->maxInputChannels, info->maxOutputChannels); | ||||
} | } | ||||
static int paCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) { | |||||
AudioInterface *p = (AudioInterface *) userData; | |||||
p->stepStream((const float *) inputBuffer, (float *) outputBuffer, framesPerBuffer); | |||||
return paContinue; | |||||
} | |||||
void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { | void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { | ||||
closeDevice(); | closeDevice(); | ||||
std::lock_guard<std::mutex> lock(mutex); | |||||
std::lock_guard<std::mutex> lock(bufferMutex); | |||||
this->sampleRate = sampleRate; | this->sampleRate = sampleRate; | ||||
this->blockSize = blockSize; | this->blockSize = blockSize; | ||||
@@ -207,7 +229,7 @@ void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { | |||||
err = Pa_OpenStream(&stream, | err = Pa_OpenStream(&stream, | ||||
numInputs == 0 ? NULL : &inputParameters, | numInputs == 0 ? NULL : &inputParameters, | ||||
numOutputs == 0 ? NULL : &outputParameters, | numOutputs == 0 ? NULL : &outputParameters, | ||||
sampleRate, blockSize, paNoFlag, NULL, NULL); | |||||
sampleRate, blockSize, paNoFlag, paCallback, this); | |||||
if (err) { | if (err) { | ||||
fprintf(stderr, "Failed to open audio stream: %s\n", Pa_GetErrorText(err)); | fprintf(stderr, "Failed to open audio stream: %s\n", Pa_GetErrorText(err)); | ||||
return; | return; | ||||
@@ -218,6 +240,8 @@ void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { | |||||
fprintf(stderr, "Failed to start audio stream: %s\n", Pa_GetErrorText(err)); | fprintf(stderr, "Failed to start audio stream: %s\n", Pa_GetErrorText(err)); | ||||
return; | return; | ||||
} | } | ||||
// This should go after Pa_StartStream because sometimes it will call the callback once synchronously, and that time it should return early | |||||
streamRunning = true; | |||||
// Correct sample rate | // Correct sample rate | ||||
const PaStreamInfo *streamInfo = Pa_GetStreamInfo(stream); | const PaStreamInfo *streamInfo = Pa_GetStreamInfo(stream); | ||||
@@ -227,10 +251,16 @@ void AudioInterface::openDevice(int deviceId, float sampleRate, int blockSize) { | |||||
} | } | ||||
void AudioInterface::closeDevice() { | void AudioInterface::closeDevice() { | ||||
std::lock_guard<std::mutex> lock(mutex); | |||||
std::lock_guard<std::mutex> lock(bufferMutex); | |||||
if (stream) { | if (stream) { | ||||
PaError err; | PaError err; | ||||
streamRunning = false; | |||||
err = Pa_StopStream(stream); | |||||
if (err) { | |||||
fprintf(stderr, "Failed to stop audio stream: %s\n", Pa_GetErrorText(err)); | |||||
} | |||||
err = Pa_CloseStream(stream); | err = Pa_CloseStream(stream); | ||||
if (err) { | if (err) { | ||||
fprintf(stderr, "Failed to close audio stream: %s\n", Pa_GetErrorText(err)); | fprintf(stderr, "Failed to close audio stream: %s\n", Pa_GetErrorText(err)); | ||||
@@ -5,47 +5,16 @@ | |||||
#include <set> | #include <set> | ||||
#include <chrono> | #include <chrono> | ||||
#include <thread> | #include <thread> | ||||
#include <mutex> | |||||
#include <condition_variable> | |||||
#include <xmmintrin.h> | #include <xmmintrin.h> | ||||
#include "engine.hpp" | #include "engine.hpp" | ||||
#include "util.hpp" | |||||
namespace rack { | namespace rack { | ||||
float gSampleRate; | float gSampleRate; | ||||
/** Threads which obtain a VIPLock will cause wait() to block for other less important threads. | |||||
This does not provide the VIPs with an exclusive lock. That should be left up to another mutex shared between the less important thread. | |||||
*/ | |||||
struct VIPMutex { | |||||
int count = 0; | |||||
std::condition_variable cv; | |||||
std::mutex countMutex; | |||||
/** Blocks until there are no remaining VIPLocks */ | |||||
void wait() { | |||||
std::unique_lock<std::mutex> lock(countMutex); | |||||
while (count > 0) | |||||
cv.wait(lock); | |||||
} | |||||
}; | |||||
struct VIPLock { | |||||
VIPMutex &m; | |||||
VIPLock(VIPMutex &m) : m(m) { | |||||
std::unique_lock<std::mutex> lock(m.countMutex); | |||||
m.count++; | |||||
} | |||||
~VIPLock() { | |||||
std::unique_lock<std::mutex> lock(m.countMutex); | |||||
m.count--; | |||||
lock.unlock(); | |||||
m.cv.notify_all(); | |||||
} | |||||
}; | |||||
static bool running = false; | static bool running = false; | ||||
@@ -20,7 +20,7 @@ int main() { | |||||
assert(success); | assert(success); | ||||
CFRelease(bundleURL); | CFRelease(bundleURL); | ||||
// chdir(dirname(path)); | |||||
chdir(dirname(path)); | |||||
} | } | ||||
#endif | #endif | ||||
@@ -4,7 +4,7 @@ | |||||
namespace rack { | namespace rack { | ||||
std::string gApplicationName = "VCV Rack"; | std::string gApplicationName = "VCV Rack"; | ||||
std::string gApplicationVersion = "v0.2.0 alpha"; | |||||
std::string gApplicationVersion = "v0.1.1 alpha"; | |||||
RackWidget *gRackWidget = NULL; | RackWidget *gRackWidget = NULL; | ||||
@@ -47,9 +47,9 @@ struct FileChoice : ChoiceButton { | |||||
openItem->text = "Open"; | openItem->text = "Open"; | ||||
menu->pushChild(openItem); | menu->pushChild(openItem); | ||||
MenuItem *saveItem = new SaveItem(); | |||||
saveItem->text = "Save"; | |||||
menu->pushChild(saveItem); | |||||
// MenuItem *saveItem = new SaveItem(); | |||||
// saveItem->text = "Save"; | |||||
// menu->pushChild(saveItem); | |||||
MenuItem *saveAsItem = new SaveItem(); | MenuItem *saveAsItem = new SaveItem(); | ||||
saveAsItem->text = "Save As"; | saveAsItem->text = "Save As"; | ||||
@@ -76,13 +76,17 @@ static NVGcolor wireColors[8] = { | |||||
nvgRGB(0x88, 0x88, 0x88), // light gray | nvgRGB(0x88, 0x88, 0x88), // light gray | ||||
nvgRGB(0xaa, 0xaa, 0xaa), // white | nvgRGB(0xaa, 0xaa, 0xaa), // white | ||||
}; | }; | ||||
static int nextWireColorId = 1; | |||||
static int lastWireColorId = -1; | |||||
WireWidget::WireWidget() { | WireWidget::WireWidget() { | ||||
color = wireColors[nextWireColorId]; | |||||
nextWireColorId = (nextWireColorId + 1) % 8; | |||||
int wireColorId; | |||||
do { | |||||
wireColorId = randomu32() % 8; | |||||
} while (wireColorId == lastWireColorId); | |||||
lastWireColorId = wireColorId; | |||||
color = wireColors[wireColorId]; | |||||
} | } | ||||
WireWidget::~WireWidget() { | WireWidget::~WireWidget() { | ||||