From d9f12e44dc789384b3f8987c9b9033f96057ab32 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 12 Aug 2016 11:31:06 -0700 Subject: [PATCH 01/19] Add resampler support for downsampling with variable-input constant-output buffering mode --- libraries/audio/src/AudioSRC.cpp | 18 ++++++++++++++++++ libraries/audio/src/AudioSRC.h | 2 ++ 2 files changed, 20 insertions(+) diff --git a/libraries/audio/src/AudioSRC.cpp b/libraries/audio/src/AudioSRC.cpp index 5dba63b349..44326e5bc4 100644 --- a/libraries/audio/src/AudioSRC.cpp +++ b/libraries/audio/src/AudioSRC.cpp @@ -1030,3 +1030,21 @@ int AudioSRC::getMaxInput(int outputFrames) { return (int)(((int64_t)outputFrames * _step) >> 32); } } + +// the input frames that will produce exactly outputFrames +int AudioSRC::getExactInput(int outputFrames) { + // + // For upsampling, a correct implementation is more complicated + // because it requires early exit of the multirate filter. + // This is not currently supported. + // + if (_upFactor > _downFactor) { + return -1; + } + if (_step == 0) { + int64_t offset = ((int64_t)_phase * _downFactor) % _upFactor; + return (int)(((int64_t)outputFrames * _downFactor + offset) / _upFactor); + } else { + return (int)(((int64_t)outputFrames * _step + _offset) >> 32); + } +} diff --git a/libraries/audio/src/AudioSRC.h b/libraries/audio/src/AudioSRC.h index f7becc22c3..5ae5318b5e 100644 --- a/libraries/audio/src/AudioSRC.h +++ b/libraries/audio/src/AudioSRC.h @@ -48,6 +48,8 @@ public: int getMinInput(int outputFrames); int getMaxInput(int outputFrames); + int getExactInput(int outputFrames); + private: float* _polyphaseFilter; int* _stepTable; From 2326696d5430cdadfec6d85f575160a75c35415b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 16 Aug 2016 16:53:51 -0700 Subject: [PATCH 02/19] Disabled the continuous polling for audio device changes on Windows. It necessitated extra buffering to prevent periodic dropouts in the audio stream. --- libraries/audio-client/src/AudioClient.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 398f4bb27c..0be91ab5fe 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1362,8 +1362,8 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { } void AudioClient::checkDevices() { -# ifdef Q_OS_LINUX - // on linux, this makes the audio stream hiccup +# if defined(Q_OS_LINUX) || defined (Q_OS_WIN) + // on Windows and Linux, this causes dropouts in the audio stream # else QVector inputDevices = getDeviceNames(QAudio::AudioInput); QVector outputDevices = getDeviceNames(QAudio::AudioOutput); From 7202d132c57b312fba19dac959abfd114f30236e Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 16 Aug 2016 16:57:28 -0700 Subject: [PATCH 03/19] Remove 22050 from the preferred list of back-end sample rates. --- libraries/audio-client/src/AudioClient.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0be91ab5fe..3a049e0c38 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -334,20 +334,16 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, qCDebug(audioclient) << "The desired format for audio I/O is" << desiredAudioFormat; const int FORTY_FOUR = 44100; + const int FORTY_EIGHT = 48000; adjustedAudioFormat = desiredAudioFormat; #ifdef Q_OS_ANDROID adjustedAudioFormat.setSampleRate(FORTY_FOUR); #else - const int HALF_FORTY_FOUR = FORTY_FOUR / 2; - - if (audioDevice.supportedSampleRates().contains(AudioConstants::SAMPLE_RATE * 2)) { - // use 48, which is a simple downsample, upsample - adjustedAudioFormat.setSampleRate(AudioConstants::SAMPLE_RATE * 2); - } else if (audioDevice.supportedSampleRates().contains(HALF_FORTY_FOUR)) { - // use 22050, resample but closer to 24 - adjustedAudioFormat.setSampleRate(HALF_FORTY_FOUR); + if (audioDevice.supportedSampleRates().contains(FORTY_EIGHT)) { + // use 48000, which is a simple downsample, upsample + adjustedAudioFormat.setSampleRate(FORTY_EIGHT); } else if (audioDevice.supportedSampleRates().contains(FORTY_FOUR)) { // use 44100, resample adjustedAudioFormat.setSampleRate(FORTY_FOUR); From f80304d68e9e3233ed54bc60314026483e32385b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 18 Aug 2016 08:27:20 -0700 Subject: [PATCH 04/19] Change audio pipelines to process in exactly 10ms blocks (240 samples instead of 256). This produces an integral number of samples when resampled to 44.1k or 48k, allowing a 44.1k back-end to work correctly without extra buffering or dynamic buffers sizes. --- libraries/audio/src/AudioConstants.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio/src/AudioConstants.h b/libraries/audio/src/AudioConstants.h index 9271323498..b8ad94e669 100644 --- a/libraries/audio/src/AudioConstants.h +++ b/libraries/audio/src/AudioConstants.h @@ -28,9 +28,9 @@ namespace AudioConstants { const int MAX_CODEC_NAME_LENGTH = 30; const int MAX_CODEC_NAME_LENGTH_ON_WIRE = MAX_CODEC_NAME_LENGTH + sizeof(uint32_t); - const int NETWORK_FRAME_BYTES_STEREO = 1024; + const int NETWORK_FRAME_BYTES_STEREO = 960; const int NETWORK_FRAME_SAMPLES_STEREO = NETWORK_FRAME_BYTES_STEREO / sizeof(AudioSample); - const int NETWORK_FRAME_BYTES_PER_CHANNEL = 512; + const int NETWORK_FRAME_BYTES_PER_CHANNEL = NETWORK_FRAME_BYTES_STEREO / 2; const int NETWORK_FRAME_SAMPLES_PER_CHANNEL = NETWORK_FRAME_BYTES_PER_CHANNEL / sizeof(AudioSample); const float NETWORK_FRAME_SECS = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL / float(AudioConstants::SAMPLE_RATE)); const float NETWORK_FRAME_MSECS = NETWORK_FRAME_SECS * 1000.0f; From 46c94a938ef5840268e22d011f7886329d4b4a1e Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 18 Aug 2016 08:29:32 -0700 Subject: [PATCH 05/19] Updated HRTF to process in 240-sample blocks --- libraries/audio/src/AudioHRTF.cpp | 62 +++++++++++++++---------------- libraries/audio/src/AudioHRTF.h | 2 +- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index 378e2154c1..5984187203 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -31,38 +31,36 @@ // Valimaki, Laakso. "Elimination of Transients in Time-Varying Allpass Fractional Delay Filters" // static const float crossfadeTable[HRTF_BLOCK] = { - 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 0.9999611462f, 0.9998445910f, 0.9996503524f, - 0.9993784606f, 0.9990289579f, 0.9986018986f, 0.9980973490f, 0.9975153877f, 0.9968561049f, 0.9961196033f, 0.9953059972f, - 0.9944154131f, 0.9934479894f, 0.9924038765f, 0.9912832366f, 0.9900862439f, 0.9888130845f, 0.9874639561f, 0.9860390685f, - 0.9845386431f, 0.9829629131f, 0.9813121235f, 0.9795865307f, 0.9777864029f, 0.9759120199f, 0.9739636731f, 0.9719416652f, - 0.9698463104f, 0.9676779344f, 0.9654368743f, 0.9631234783f, 0.9607381059f, 0.9582811279f, 0.9557529262f, 0.9531538935f, - 0.9504844340f, 0.9477449623f, 0.9449359044f, 0.9420576968f, 0.9391107867f, 0.9360956322f, 0.9330127019f, 0.9298624749f, - 0.9266454408f, 0.9233620996f, 0.9200129616f, 0.9165985472f, 0.9131193872f, 0.9095760221f, 0.9059690029f, 0.9022988899f, - 0.8985662536f, 0.8947716742f, 0.8909157412f, 0.8869990541f, 0.8830222216f, 0.8789858616f, 0.8748906015f, 0.8707370778f, - 0.8665259359f, 0.8622578304f, 0.8579334246f, 0.8535533906f, 0.8491184090f, 0.8446291692f, 0.8400863689f, 0.8354907140f, - 0.8308429188f, 0.8261437056f, 0.8213938048f, 0.8165939546f, 0.8117449009f, 0.8068473974f, 0.8019022052f, 0.7969100928f, - 0.7918718361f, 0.7867882182f, 0.7816600290f, 0.7764880657f, 0.7712731319f, 0.7660160383f, 0.7607176017f, 0.7553786457f, - 0.7500000000f, 0.7445825006f, 0.7391269893f, 0.7336343141f, 0.7281053287f, 0.7225408922f, 0.7169418696f, 0.7113091309f, - 0.7056435516f, 0.6999460122f, 0.6942173981f, 0.6884585998f, 0.6826705122f, 0.6768540348f, 0.6710100717f, 0.6651395310f, - 0.6592433251f, 0.6533223705f, 0.6473775872f, 0.6414098993f, 0.6354202341f, 0.6294095226f, 0.6233786988f, 0.6173287002f, - 0.6112604670f, 0.6051749422f, 0.5990730716f, 0.5929558036f, 0.5868240888f, 0.5806788803f, 0.5745211331f, 0.5683518042f, - 0.5621718523f, 0.5559822381f, 0.5497839233f, 0.5435778714f, 0.5373650468f, 0.5311464151f, 0.5249229428f, 0.5186955971f, - 0.5124653459f, 0.5062331573f, 0.5000000000f, 0.4937668427f, 0.4875346541f, 0.4813044029f, 0.4750770572f, 0.4688535849f, - 0.4626349532f, 0.4564221286f, 0.4502160767f, 0.4440177619f, 0.4378281477f, 0.4316481958f, 0.4254788669f, 0.4193211197f, - 0.4131759112f, 0.4070441964f, 0.4009269284f, 0.3948250578f, 0.3887395330f, 0.3826712998f, 0.3766213012f, 0.3705904774f, - 0.3645797659f, 0.3585901007f, 0.3526224128f, 0.3466776295f, 0.3407566749f, 0.3348604690f, 0.3289899283f, 0.3231459652f, - 0.3173294878f, 0.3115414002f, 0.3057826019f, 0.3000539878f, 0.2943564484f, 0.2886908691f, 0.2830581304f, 0.2774591078f, - 0.2718946713f, 0.2663656859f, 0.2608730107f, 0.2554174994f, 0.2500000000f, 0.2446213543f, 0.2392823983f, 0.2339839617f, - 0.2287268681f, 0.2235119343f, 0.2183399710f, 0.2132117818f, 0.2081281639f, 0.2030899072f, 0.1980977948f, 0.1931526026f, - 0.1882550991f, 0.1834060454f, 0.1786061952f, 0.1738562944f, 0.1691570812f, 0.1645092860f, 0.1599136311f, 0.1553708308f, - 0.1508815910f, 0.1464466094f, 0.1420665754f, 0.1377421696f, 0.1334740641f, 0.1292629222f, 0.1251093985f, 0.1210141384f, - 0.1169777784f, 0.1130009459f, 0.1090842588f, 0.1052283258f, 0.1014337464f, 0.0977011101f, 0.0940309971f, 0.0904239779f, - 0.0868806128f, 0.0834014528f, 0.0799870384f, 0.0766379004f, 0.0733545592f, 0.0701375251f, 0.0669872981f, 0.0639043678f, - 0.0608892133f, 0.0579423032f, 0.0550640956f, 0.0522550377f, 0.0495155660f, 0.0468461065f, 0.0442470738f, 0.0417188721f, - 0.0392618941f, 0.0368765217f, 0.0345631257f, 0.0323220656f, 0.0301536896f, 0.0280583348f, 0.0260363269f, 0.0240879801f, - 0.0222135971f, 0.0204134693f, 0.0186878765f, 0.0170370869f, 0.0154613569f, 0.0139609315f, 0.0125360439f, 0.0111869155f, - 0.0099137561f, 0.0087167634f, 0.0075961235f, 0.0065520106f, 0.0055845869f, 0.0046940028f, 0.0038803967f, 0.0031438951f, - 0.0024846123f, 0.0019026510f, 0.0013981014f, 0.0009710421f, 0.0006215394f, 0.0003496476f, 0.0001554090f, 0.0000388538f, + 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, + 0.9999545513f, 0.9998182135f, 0.9995910114f, 0.9992729863f, 0.9988641959f, 0.9983647147f, 0.9977746334f, 0.9970940592f, + 0.9963231160f, 0.9954619438f, 0.9945106993f, 0.9934695553f, 0.9923387012f, 0.9911183425f, 0.9898087010f, 0.9884100149f, + 0.9869225384f, 0.9853465419f, 0.9836823120f, 0.9819301512f, 0.9800903780f, 0.9781633270f, 0.9761493483f, 0.9740488082f, + 0.9718620885f, 0.9695895868f, 0.9672317161f, 0.9647889052f, 0.9622615981f, 0.9596502542f, 0.9569553484f, 0.9541773705f, + 0.9513168255f, 0.9483742335f, 0.9453501294f, 0.9422450630f, 0.9390595988f, 0.9357943158f, 0.9324498078f, 0.9290266826f, + 0.9255255626f, 0.9219470843f, 0.9182918983f, 0.9145606690f, 0.9107540747f, 0.9068728075f, 0.9029175730f, 0.8988890902f, + 0.8947880914f, 0.8906153223f, 0.8863715413f, 0.8820575200f, 0.8776740426f, 0.8732219061f, 0.8687019198f, 0.8641149055f, + 0.8594616970f, 0.8547431402f, 0.8499600930f, 0.8451134248f, 0.8402040169f, 0.8352327617f, 0.8302005629f, 0.8251083354f, + 0.8199570049f, 0.8147475079f, 0.8094807915f, 0.8041578130f, 0.7987795403f, 0.7933469510f, 0.7878610328f, 0.7823227830f, + 0.7767332084f, 0.7710933251f, 0.7654041585f, 0.7596667428f, 0.7538821211f, 0.7480513449f, 0.7421754743f, 0.7362555775f, + 0.7302927306f, 0.7242880178f, 0.7182425305f, 0.7121573680f, 0.7060336363f, 0.6998724488f, 0.6936749255f, 0.6874421931f, + 0.6811753847f, 0.6748756396f, 0.6685441031f, 0.6621819261f, 0.6557902652f, 0.6493702826f, 0.6429231452f, 0.6364500251f, + 0.6299520991f, 0.6234305485f, 0.6168865589f, 0.6103213199f, 0.6037360251f, 0.5971318716f, 0.5905100601f, 0.5838717943f, + 0.5772182810f, 0.5705507299f, 0.5638703530f, 0.5571783649f, 0.5504759820f, 0.5437644228f, 0.5370449075f, 0.5303186576f, + 0.5235868960f, 0.5168508463f, 0.5101117333f, 0.5033707820f, 0.4966292180f, 0.4898882667f, 0.4831491537f, 0.4764131040f, + 0.4696813424f, 0.4629550925f, 0.4562355772f, 0.4495240180f, 0.4428216351f, 0.4361296470f, 0.4294492701f, 0.4227817190f, + 0.4161282057f, 0.4094899399f, 0.4028681284f, 0.3962639749f, 0.3896786801f, 0.3831134411f, 0.3765694515f, 0.3700479009f, + 0.3635499749f, 0.3570768548f, 0.3506297174f, 0.3442097348f, 0.3378180739f, 0.3314558969f, 0.3251243604f, 0.3188246153f, + 0.3125578069f, 0.3063250745f, 0.3001275512f, 0.2939663637f, 0.2878426320f, 0.2817574695f, 0.2757119822f, 0.2697072694f, + 0.2637444225f, 0.2578245257f, 0.2519486551f, 0.2461178789f, 0.2403332572f, 0.2345958415f, 0.2289066749f, 0.2232667916f, + 0.2176772170f, 0.2121389672f, 0.2066530490f, 0.2012204597f, 0.1958421870f, 0.1905192085f, 0.1852524921f, 0.1800429951f, + 0.1748916646f, 0.1697994371f, 0.1647672383f, 0.1597959831f, 0.1548865752f, 0.1500399070f, 0.1452568598f, 0.1405383030f, + 0.1358850945f, 0.1312980802f, 0.1267780939f, 0.1223259574f, 0.1179424800f, 0.1136284587f, 0.1093846777f, 0.1052119086f, + 0.1011109098f, 0.0970824270f, 0.0931271925f, 0.0892459253f, 0.0854393310f, 0.0817081017f, 0.0780529157f, 0.0744744374f, + 0.0709733174f, 0.0675501922f, 0.0642056842f, 0.0609404012f, 0.0577549370f, 0.0546498706f, 0.0516257665f, 0.0486831745f, + 0.0458226295f, 0.0430446516f, 0.0403497458f, 0.0377384019f, 0.0352110948f, 0.0327682839f, 0.0304104132f, 0.0281379115f, + 0.0259511918f, 0.0238506517f, 0.0218366730f, 0.0199096220f, 0.0180698488f, 0.0163176880f, 0.0146534581f, 0.0130774616f, + 0.0115899851f, 0.0101912990f, 0.0088816575f, 0.0076612988f, 0.0065304447f, 0.0054893007f, 0.0045380562f, 0.0036768840f, + 0.0029059408f, 0.0022253666f, 0.0016352853f, 0.0011358041f, 0.0007270137f, 0.0004089886f, 0.0001817865f, 0.0000454487f, }; // diff --git a/libraries/audio/src/AudioHRTF.h b/libraries/audio/src/AudioHRTF.h index 63d1712980..08ae82b854 100644 --- a/libraries/audio/src/AudioHRTF.h +++ b/libraries/audio/src/AudioHRTF.h @@ -19,7 +19,7 @@ static const int HRTF_TAPS = 64; // minimum-phase FIR coefficients static const int HRTF_TABLES = 25; // number of HRTF subjects static const int HRTF_DELAY = 24; // max ITD in samples (1.0ms at 24KHz) -static const int HRTF_BLOCK = 256; // block processing size +static const int HRTF_BLOCK = 240; // block processing size static const float HRTF_GAIN = 1.0f; // HRTF global gain adjustment From 87d5ec2437a550b2fc92ef8008ed1323fad0f8e4 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 18 Aug 2016 08:31:43 -0700 Subject: [PATCH 06/19] Bump protocol version. Audio packets are now exactly 10ms (240/480 samples). --- libraries/networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e9d61a827a..b852eed4d9 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -77,7 +77,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::InjectAudio: case PacketType::MicrophoneAudioNoEcho: case PacketType::MicrophoneAudioWithEcho: - return static_cast(AudioVersion::CodecNameInAudioPackets); + return static_cast(AudioVersion::Exactly10msAudioPackets); default: return 17; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 40524e2288..9b70c17a98 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -217,7 +217,8 @@ enum class DomainListVersion : PacketVersion { enum class AudioVersion : PacketVersion { HasCompressedAudio = 17, - CodecNameInAudioPackets + CodecNameInAudioPackets, + Exactly10msAudioPackets }; #endif // hifi_PacketHeaders_h From 3583bdecbb0d2bd6ba4e282bdbbd53772e0e5123 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 18 Aug 2016 16:56:26 -0700 Subject: [PATCH 07/19] Fix for calculateDeviceToNetworkInputRatio() which was completely broken. Running the audio back-end at 44.1k now works correctly. --- libraries/audio-client/src/AudioClient.cpp | 13 ++----------- libraries/audio-client/src/AudioClient.h | 1 - 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 3a049e0c38..cf43730258 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -748,12 +748,11 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { } void AudioClient::handleAudioInput() { - const float inputToNetworkInputRatio = calculateDeviceToNetworkInputRatio(); - const int inputSamplesRequired = (int)((float)AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * inputToNetworkInputRatio); + // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output + const int inputSamplesRequired = _inputFormat.channelCount() * _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); QByteArray inputByteArray = _inputDevice->readAll(); - handleLocalEchoAndReverb(inputByteArray); _inputRingBuffer.writeData(inputByteArray.data(), inputByteArray.size()); @@ -1261,14 +1260,6 @@ int AudioClient::calculateNumberOfInputCallbackBytes(const QAudioFormat& format) return numInputCallbackBytes; } -float AudioClient::calculateDeviceToNetworkInputRatio() const { - float inputToNetworkInputRatio = (int)((_numInputCallbackBytes - * CALLBACK_ACCELERATOR_RATIO - / AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL) + 0.5f); - - return inputToNetworkInputRatio; -} - int AudioClient::calculateNumberOfFrameSamples(int numBytes) const { int frameSamples = (int)(numBytes * CALLBACK_ACCELERATOR_RATIO + 0.5f) / sizeof(int16_t); return frameSamples; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 171014edda..9db2cc0489 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -298,7 +298,6 @@ private: // Callback acceleration dependent calculations int calculateNumberOfInputCallbackBytes(const QAudioFormat& format) const; int calculateNumberOfFrameSamples(int numBytes) const; - float calculateDeviceToNetworkInputRatio() const; quint16 _outgoingAvatarAudioSequenceNumber; From 4b2778f02dbc932c90a673fcf95f98c2df923b54 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 20 Aug 2016 12:22:47 -0700 Subject: [PATCH 08/19] Re-implement the continuous detection of device changes for all platforms, using a background thread. This prevents the glitches caused by calling checkDevices() on the audio processing thread. --- libraries/audio-client/src/AudioClient.cpp | 41 +++++++++++++++++----- libraries/audio-client/src/AudioClient.h | 3 +- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index cf43730258..8f8d17d4ad 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -67,6 +67,33 @@ Setting::Handle windowSecondsForDesiredReduction("windowSecondsForDesiredRe DEFAULT_WINDOW_SECONDS_FOR_DESIRED_REDUCTION); Setting::Handle repetitionWithFade("repetitionWithFade", DEFAULT_REPETITION_WITH_FADE); +// background thread that continuously polls for device changes +class CheckDevicesThread : public QThread { +public: + const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000; + + CheckDevicesThread(AudioClient* audioClient) + : _audioClient(audioClient) { + + connect(qApp, &QCoreApplication::aboutToQuit, [this] { + _quit = true; + }); + } + + void run() override { + while (!_quit) { + + //qDebug() << "Checking for audio device changes..."; + _audioClient->checkDevices(); + + QThread::msleep(DEVICE_CHECK_INTERVAL_MSECS); + } + } + + AudioClient* _audioClient { nullptr }; + bool _quit { false }; +}; + AudioClient::AudioClient() : AbstractAudioInterface(), _audioInput(NULL), @@ -121,14 +148,14 @@ AudioClient::AudioClient() : connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioClient::handleMismatchAudioFormat); - _inputDevices = getDeviceNames(QAudio::AudioInput); _outputDevices = getDeviceNames(QAudio::AudioOutput); - const qint64 DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000; - QTimer* updateTimer = new QTimer(this); - connect(updateTimer, &QTimer::timeout, this, &AudioClient::checkDevices); - updateTimer->start(DEVICE_CHECK_INTERVAL_MSECS); + // start a thread to detect any device changes + QThread* checkDevicesThread = new CheckDevicesThread(this); + checkDevicesThread->setObjectName("CheckDevices Thread"); + checkDevicesThread->setPriority(QThread::LowPriority); + checkDevicesThread->start(); configureReverb(); @@ -1349,9 +1376,6 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { } void AudioClient::checkDevices() { -# if defined(Q_OS_LINUX) || defined (Q_OS_WIN) - // on Windows and Linux, this causes dropouts in the audio stream -# else QVector inputDevices = getDeviceNames(QAudio::AudioInput); QVector outputDevices = getDeviceNames(QAudio::AudioOutput); @@ -1361,7 +1385,6 @@ void AudioClient::checkDevices() { emit deviceChanged(); } -# endif } void AudioClient::loadSettings() { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 9db2cc0489..1ee4b92a0c 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -139,6 +139,8 @@ public: QVector& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; } + void checkDevices(); + static const float CALLBACK_ACCELERATOR_RATIO; #ifdef Q_OS_WIN @@ -312,7 +314,6 @@ private: QVector _inputDevices; QVector _outputDevices; - void checkDevices(); bool _hasReceivedFirstPacket = false; From 8fbf5a9e8baf7a85493ceefcd8570ff76fa2067e Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 24 Aug 2016 10:16:43 -0700 Subject: [PATCH 09/19] PR feedback --- libraries/audio-client/src/AudioClient.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 8f8d17d4ad..1e324e7bec 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -90,6 +90,7 @@ public: } } +private: AudioClient* _audioClient { nullptr }; bool _quit { false }; }; From b4d143fe130f3318e5ab4d5dbbdbc6f70be8526f Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 24 Aug 2016 10:19:09 -0700 Subject: [PATCH 10/19] PR feedback --- libraries/audio-client/src/AudioClient.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1e324e7bec..131c55e6d1 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -82,10 +82,7 @@ public: void run() override { while (!_quit) { - - //qDebug() << "Checking for audio device changes..."; _audioClient->checkDevices(); - QThread::msleep(DEVICE_CHECK_INTERVAL_MSECS); } } From c0caea1ffd72265660567910c18882a1823567b5 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 24 Aug 2016 10:32:29 -0700 Subject: [PATCH 11/19] PR feedback --- libraries/audio-client/src/AudioClient.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 131c55e6d1..089ef75095 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1373,6 +1373,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { return bytesWritten; } +// now called from a background thread, to keep blocking operations off the audio thread void AudioClient::checkDevices() { QVector inputDevices = getDeviceNames(QAudio::AudioInput); QVector outputDevices = getDeviceNames(QAudio::AudioOutput); From 1ab35797b16c6c1b6b4cf636b20756d0aeebc42a Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 25 Aug 2016 16:09:25 -0700 Subject: [PATCH 12/19] Drastically reduce the audio capture buffering on Windows. With device detection on another thread and WASAPI back-end, the large capture buffer (resulting from CALLBACK_ACCELERATOR_RATIO) is no longer needed. --- libraries/audio-client/src/AudioClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 089ef75095..f16b662428 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1265,7 +1265,7 @@ int AudioClient::setOutputBufferSize(int numFrames, bool persist) { // proportional to the accelerator ratio. #ifdef Q_OS_WIN -const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 0.1f; +const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 1.0f; #endif #ifdef Q_OS_MAC From 996bf0d763b9c4d7ac2328f97d238d950869ffc4 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sun, 28 Aug 2016 20:12:59 -0700 Subject: [PATCH 13/19] Replace the Qt multimedia audio back-end on the Windows build. Adds an external project (wasapi) to download a low-latency audio plugin for Qt, as a precompiled DLL. Adds a post-build command to use this new plugin instead of the default Qt audio plugin. --- cmake/externals/wasapi/CMakeLists.txt | 24 +++++++++++++++++++ .../PackageLibrariesForDeployment.cmake | 10 ++++++++ interface/CMakeLists.txt | 1 + 3 files changed, 35 insertions(+) create mode 100644 cmake/externals/wasapi/CMakeLists.txt diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt new file mode 100644 index 0000000000..87c3f4b0e4 --- /dev/null +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -0,0 +1,24 @@ +if (WIN32) + + set(EXTERNAL_NAME wasapi) + string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + + include(ExternalProject) + ExternalProject_Add( + ${EXTERNAL_NAME} + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi.zip + URL_MD5 11c8a7728d6eda7223df800e10b70723 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 + ) + + # Hide this external target (for ide users) + set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + + ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) + + set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${SOURCE_DIR} CACHE FILEPATH "Location of wasapi DLL") + +endif() diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index 050cea9fe1..7f826c1f8c 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -41,5 +41,15 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) POST_BUILD COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" ) + + set(QTAUDIO_PATH $/audio) + + # if present, replace qtaudio_windows.dll with qtaudio_wasapi.dll + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E remove ${QTAUDIO_PATH}/qtaudio_windows.dll && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.pdb ${QTAUDIO_PATH} ) + ) + endif () endmacro() diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 414fafe705..27cba520bb 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -126,6 +126,7 @@ if (WIN32) add_dependency_external_projects(sdl2) add_dependency_external_projects(OpenVR) add_dependency_external_projects(neuron) + add_dependency_external_projects(wasapi) endif() # disable /OPT:REF and /OPT:ICF for the Debug builds From 678ea3ecbfbf08f00213310b696e58eb28fb42fc Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Mon, 29 Aug 2016 13:22:48 -0700 Subject: [PATCH 14/19] PR feedback --- cmake/externals/wasapi/CMakeLists.txt | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt index 87c3f4b0e4..019920df77 100644 --- a/cmake/externals/wasapi/CMakeLists.txt +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -1,24 +1,24 @@ if (WIN32) - set(EXTERNAL_NAME wasapi) - string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + set(EXTERNAL_NAME wasapi) + string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) - include(ExternalProject) - ExternalProject_Add( - ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi.zip - URL_MD5 11c8a7728d6eda7223df800e10b70723 - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" - LOG_DOWNLOAD 1 - ) + include(ExternalProject) + ExternalProject_Add( + ${EXTERNAL_NAME} + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi.zip + URL_MD5 11c8a7728d6eda7223df800e10b70723 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 + ) - # Hide this external target (for ide users) - set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + # Hide this external target (for ide users) + set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") - ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) + ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) - set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${SOURCE_DIR} CACHE FILEPATH "Location of wasapi DLL") + set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${SOURCE_DIR} CACHE FILEPATH "Location of wasapi DLL") endif() From d2b33e00bdbccabcc06497be51c018239ad850a1 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 30 Aug 2016 10:13:08 -0700 Subject: [PATCH 15/19] Fix audio startup failures. Directly calling the static method QAudioDeviceInfo::availableDevices() was not thread-safe. --- libraries/audio-client/src/AudioClient.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index f16b662428..02b9cdfd39 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -82,8 +82,8 @@ public: void run() override { while (!_quit) { - _audioClient->checkDevices(); QThread::msleep(DEVICE_CHECK_INTERVAL_MSECS); + _audioClient->checkDevices(); } } @@ -195,10 +195,17 @@ void AudioClient::audioMixerKilled() { emit disconnected(); } +// thread-safe +QList getAvailableDevices(QAudio::Mode mode) { + static std::mutex mutex; + std::lock_guard lock(mutex); + + return QAudioDeviceInfo::availableDevices(mode); +} QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& deviceName) { QAudioDeviceInfo result; - foreach(QAudioDeviceInfo audioDevice, QAudioDeviceInfo::availableDevices(mode)) { + foreach(QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) { if (audioDevice.deviceName().trimmed() == deviceName.trimmed()) { result = audioDevice; break; @@ -262,7 +269,7 @@ QString AudioClient::friendlyNameForAudioDevice(wchar_t* guid) { QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { #ifdef __APPLE__ - if (QAudioDeviceInfo::availableDevices(mode).size() > 1) { + if (getAvailableDevices(mode).size() > 1) { AudioDeviceID defaultDeviceID = 0; uint32_t propertySize = sizeof(AudioDeviceID); AudioObjectPropertyAddress propertyAddress = { @@ -292,7 +299,7 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { if (!getPropertyError && propertySize) { // find a device in the list that matches the name we have and return it - foreach(QAudioDeviceInfo audioDevice, QAudioDeviceInfo::availableDevices(mode)) { + foreach(QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) { if (audioDevice.deviceName() == CFStringGetCStringPtr(deviceName, kCFStringEncodingMacRoman)) { return audioDevice; } @@ -601,7 +608,7 @@ QString AudioClient::getDefaultDeviceName(QAudio::Mode mode) { QVector AudioClient::getDeviceNames(QAudio::Mode mode) { QVector deviceNames; - foreach(QAudioDeviceInfo audioDevice, QAudioDeviceInfo::availableDevices(mode)) { + foreach(QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) { deviceNames << audioDevice.deviceName().trimmed(); } return deviceNames; From 9187b4aa7d77e01bc015b73186b865feb6dc2eb7 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 30 Aug 2016 12:57:29 -0700 Subject: [PATCH 16/19] More complete fix for audio threading failures. Qt keeps an internal Q_GLOBAL_STATIC list of devices, which is referenced at device start() but clobbered by availableDevices(). Now protected by a mutex. --- libraries/audio-client/src/AudioClient.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 02b9cdfd39..db61ecd5f1 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -67,6 +67,11 @@ Setting::Handle windowSecondsForDesiredReduction("windowSecondsForDesiredRe DEFAULT_WINDOW_SECONDS_FOR_DESIRED_REDUCTION); Setting::Handle repetitionWithFade("repetitionWithFade", DEFAULT_REPETITION_WITH_FADE); +// protect the Qt internal device list +using Mutex = std::mutex; +using Lock = std::unique_lock; +static Mutex _deviceMutex; + // background thread that continuously polls for device changes class CheckDevicesThread : public QThread { public: @@ -197,9 +202,8 @@ void AudioClient::audioMixerKilled() { // thread-safe QList getAvailableDevices(QAudio::Mode mode) { - static std::mutex mutex; - std::lock_guard lock(mutex); - + // NOTE: availableDevices() clobbers the Qt internal device list + Lock lock(_deviceMutex); return QAudioDeviceInfo::availableDevices(mode); } @@ -741,7 +745,11 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { if (!_loopbackOutputDevice && _loopbackAudioOutput) { // we didn't have the loopback output device going so set that up now + + // NOTE: device start() uses the Qt internal device list + Lock lock(_deviceMutex); _loopbackOutputDevice = _loopbackAudioOutput->start(); + lock.unlock(); if (!_loopbackOutputDevice) { return; @@ -1133,7 +1141,10 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn int numFrameSamples = calculateNumberOfFrameSamples(_numInputCallbackBytes); _inputRingBuffer.resizeForFrameSize(numFrameSamples); + // NOTE: device start() uses the Qt internal device list + Lock lock(_deviceMutex); _inputDevice = _audioInput->start(); + lock.unlock(); if (_inputDevice) { connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleAudioInput())); @@ -1229,7 +1240,11 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); _audioOutputIODevice.start(); + + // NOTE: device start() uses the Qt internal device list + Lock lock(_deviceMutex); _audioOutput->start(&_audioOutputIODevice); + lock.unlock(); qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / sizeof(int16_t) / (float)_outputFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << From cb5bded7c16c67e6e43f58b19b73ceb84e676e28 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 30 Aug 2016 14:37:33 -0700 Subject: [PATCH 17/19] Add support for more back-end sample rates --- libraries/audio-client/src/AudioClient.cpp | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index db61ecd5f1..84396fa079 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -365,24 +365,26 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, const QAudioFormat& desiredAudioFormat, QAudioFormat& adjustedAudioFormat) { - // FIXME: directly using 24khz has a bug somewhere that causes channels to be swapped. - // Continue using our internal resampler, for now. qCDebug(audioclient) << "The desired format for audio I/O is" << desiredAudioFormat; - const int FORTY_FOUR = 44100; - const int FORTY_EIGHT = 48000; adjustedAudioFormat = desiredAudioFormat; #ifdef Q_OS_ANDROID - adjustedAudioFormat.setSampleRate(FORTY_FOUR); + adjustedAudioFormat.setSampleRate(44100); #else - if (audioDevice.supportedSampleRates().contains(FORTY_EIGHT)) { - // use 48000, which is a simple downsample, upsample - adjustedAudioFormat.setSampleRate(FORTY_EIGHT); - } else if (audioDevice.supportedSampleRates().contains(FORTY_FOUR)) { - // use 44100, resample - adjustedAudioFormat.setSampleRate(FORTY_FOUR); + // + // Attempt to set the device sample rate in decreasing order of preference. + // On Windows, using WASAPI shared mode, only a match with the hardware sample rate will succeed. + // + if (audioDevice.supportedSampleRates().contains(48000)) { + adjustedAudioFormat.setSampleRate(48000); + } else if (audioDevice.supportedSampleRates().contains(44100)) { + adjustedAudioFormat.setSampleRate(44100); + } else if (audioDevice.supportedSampleRates().contains(32000)) { + adjustedAudioFormat.setSampleRate(32000); + } else if (audioDevice.supportedSampleRates().contains(24000)) { + adjustedAudioFormat.setSampleRate(24000); } #endif From 1a6af49b3d28f4af2337396184c820ad5a0a529e Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 30 Aug 2016 16:10:11 -0700 Subject: [PATCH 18/19] Support for even more back-end sample rates --- libraries/audio-client/src/AudioClient.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 84396fa079..7fdda5a118 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -374,7 +374,7 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, #else // - // Attempt to set the device sample rate in decreasing order of preference. + // Attempt the device sample rate in decreasing order of preference. // On Windows, using WASAPI shared mode, only a match with the hardware sample rate will succeed. // if (audioDevice.supportedSampleRates().contains(48000)) { @@ -385,6 +385,16 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, adjustedAudioFormat.setSampleRate(32000); } else if (audioDevice.supportedSampleRates().contains(24000)) { adjustedAudioFormat.setSampleRate(24000); + } else if (audioDevice.supportedSampleRates().contains(16000)) { + adjustedAudioFormat.setSampleRate(16000); + } else if (audioDevice.supportedSampleRates().contains(96000)) { + adjustedAudioFormat.setSampleRate(96000); + } else if (audioDevice.supportedSampleRates().contains(192000)) { + adjustedAudioFormat.setSampleRate(192000); + } else if (audioDevice.supportedSampleRates().contains(88200)) { + adjustedAudioFormat.setSampleRate(88200); + } else if (audioDevice.supportedSampleRates().contains(176400)) { + adjustedAudioFormat.setSampleRate(176400); } #endif From 72535ce237aaf8d28ebcfcd1ce96ff9676388f56 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 31 Aug 2016 13:09:06 -0700 Subject: [PATCH 19/19] Fix reverbTest.js which was broken due to missing helper script --- scripts/developer/utilities/tools/cookies.js | 1536 ++++++++++++++++++ 1 file changed, 1536 insertions(+) create mode 100644 scripts/developer/utilities/tools/cookies.js diff --git a/scripts/developer/utilities/tools/cookies.js b/scripts/developer/utilities/tools/cookies.js new file mode 100644 index 0000000000..0e08f80d0e --- /dev/null +++ b/scripts/developer/utilities/tools/cookies.js @@ -0,0 +1,1536 @@ +// +// cookies.js +// +// version 2.0 +// +// Created by Sam Gateau, 4/1/2015 +// A simple ui panel that present a list of porperties and the proper widget to edit it +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; + +var SLIDER_RANGE_INCREMENT_SCALE = 1 / 1000; +var THUMB_COLOR = { + red: 150, + green: 150, + blue: 150 +}; +var THUMB_HIGHLIGHT = { + red: 255, + green: 255, + blue: 255 +}; +var CHECK_MARK_COLOR = { + red: 70, + green: 70, + blue: 90 +}; + +// The Slider class +(function() { + var Slider = function(x, y, width, thumbSize) { + + this.background = Overlays.addOverlay("text", { + backgroundColor: { + red: 200, + green: 200, + blue: 255 + }, + x: x, + y: y, + width: width, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true + }); + this.thumb = Overlays.addOverlay("text", { + backgroundColor: THUMB_COLOR, + x: x, + y: y, + width: thumbSize, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 1.0, + visible: true + }); + + this.thumbSize = thumbSize; + this.thumbHalfSize = 0.5 * thumbSize; + + this.minThumbX = x + this.thumbHalfSize; + this.maxThumbX = x + width - this.thumbHalfSize; + this.thumbX = this.minThumbX; + + this.minValue = 0.0; + this.maxValue = 1.0; + + this.y = y; + + this.clickOffsetX = 0; + this.isMoving = false; + this.visible = true; + }; + + Slider.prototype.updateThumb = function() { + var thumbTruePos = this.thumbX - 0.5 * this.thumbSize; + Overlays.editOverlay(this.thumb, { + x: thumbTruePos + }); + }; + + Slider.prototype.isClickableOverlayItem = function(item) { + return (item == this.thumb) || (item == this.background); + }; + + + Slider.prototype.onMousePressEvent = function(event, clickedOverlay) { + if (!this.isClickableOverlayItem(clickedOverlay)) { + this.isMoving = false; + return; + } + this.highlight(); + this.isMoving = true; + var clickOffset = event.x - this.thumbX; + if ((clickOffset > -this.thumbHalfSize) && (clickOffset < this.thumbHalfSize)) { + this.clickOffsetX = clickOffset; + } else { + this.clickOffsetX = 0; + this.thumbX = event.x; + this.updateThumb(); + this.onValueChanged(this.getValue()); + } + }; + + Slider.prototype.onMouseMoveEvent = function(event) { + if (this.isMoving) { + var newThumbX = event.x - this.clickOffsetX; + if (newThumbX < this.minThumbX) { + newThumbX = this.minThumbX; + } + if (newThumbX > this.maxThumbX) { + newThumbX = this.maxThumbX; + } + this.thumbX = newThumbX; + this.updateThumb(); + this.onValueChanged(this.getValue()); + } + }; + + Slider.prototype.onMouseReleaseEvent = function(event) { + this.isMoving = false; + this.unhighlight(); + }; + + Slider.prototype.updateWithKeys = function(direction) { + this.range = this.maxThumbX - this.minThumbX; + this.thumbX += direction * (this.range * SCALE); + this.updateThumb(); + this.onValueChanged(this.getValue()); + }; + + Slider.prototype.highlight = function() { + if (this.highlighted) { + return; + } + Overlays.editOverlay(this.thumb, { + backgroundColor: { + red: 255, + green: 255, + blue: 255 + } + }); + this.highlighted = true; + }; + + Slider.prototype.unhighlight = function() { + if (!this.highlighted) { + return; + } + Overlays.editOverlay(this.thumb, { + backgroundColor: THUMB_COLOR + }); + this.highlighted = false; + }; + + Slider.prototype.setNormalizedValue = function(value) { + if (value < 0.0) { + this.thumbX = this.minThumbX; + } else if (value > 1.0) { + this.thumbX = this.maxThsumbX; + } else { + this.thumbX = value * (this.maxThumbX - this.minThumbX) + this.minThumbX; + } + this.updateThumb(); + }; + Slider.prototype.getNormalizedValue = function() { + return (this.thumbX - this.minThumbX) / (this.maxThumbX - this.minThumbX); + }; + + Slider.prototype.setValue = function(value) { + var normValue = (value - this.minValue) / (this.maxValue - this.minValue); + this.setNormalizedValue(normValue); + }; + Slider.prototype.getValue = function() { + return this.getNormalizedValue() * (this.maxValue - this.minValue) + this.minValue; + }; + + Slider.prototype.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + + + Slider.prototype.setMinValue = function(minValue) { + var currentValue = this.getValue(); + this.minValue = minValue; + this.setValue(currentValue); + }; + Slider.prototype.getMinValue = function() { + return this.minValue; + }; + Slider.prototype.setMaxValue = function(maxValue) { + var currentValue = this.getValue(); + this.maxValue = maxValue; + this.setValue(currentValue); + }; + Slider.prototype.getMaxValue = function() { + return this.maxValue; + }; + + Slider.prototype.onValueChanged = function(value) {}; + + Slider.prototype.getHeight = function() { + if (!this.visible) { + return 0; + } + return 1.5 * this.thumbSize; + }; + + Slider.prototype.moveUp = function(newY) { + Overlays.editOverlay(this.background, { + y: newY + }); + Overlays.editOverlay(this.thumb, { + y: newY + }); + }; + + Slider.prototype.moveDown = function() { + Overlays.editOverlay(this.background, { + y: this.y + }); + Overlays.editOverlay(this.thumb, { + y: this.y + }); + }; + + this.setThumbColor = function(color) { + Overlays.editOverlay(this.thumb, { + backgroundColor: { + red: color.x * 255, + green: color.y * 255, + blue: color.z * 255 + } + }); + }; + this.setBackgroundColor = function(color) { + Overlays.editOverlay(this.background, { + backgroundColor: { + red: color.x * 255, + green: color.y * 255, + blue: color.z * 255 + } + }); + }; + + Slider.prototype.hide = function() { + Overlays.editOverlay(this.background, { + visible: false + }); + Overlays.editOverlay(this.thumb, { + visible: false + }); + this.visible = false; + }; + + Slider.prototype.show = function() { + Overlays.editOverlay(this.background, { + visible: true + }); + Overlays.editOverlay(this.thumb, { + visible: true + }); + this.visible = true; + }; + + Slider.prototype.destroy = function() { + Overlays.deleteOverlay(this.background); + Overlays.deleteOverlay(this.thumb); + }; + + this.Slider = Slider; + + // The Checkbox class + var Checkbox = function(x, y, width, thumbSize) { + + this.thumb = Overlays.addOverlay("text", { + backgroundColor: THUMB_COLOR, + textFontSize: 10, + x: x, + y: y, + width: thumbSize, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 1.0, + visible: true + }); + + + this.thumbSize = thumbSize; + var checkX = x + (0.25 * thumbSize); + var checkY = y + (0.25 * thumbSize); + this.y = y; + this.boxCheckStatus = true; + this.clickedBox = false; + this.visible = true; + + + this.checkMark = Overlays.addOverlay("text", { + backgroundColor: CHECK_MARK_COLOR, + x: checkX, + y: checkY, + width: thumbSize / 2.0, + height: thumbSize / 2.0, + alpha: 1.0, + visible: true + }); + }; + + Checkbox.prototype.updateThumb = function() { + Overlays.editOverlay(this.checkMark, { + visible: this.boxCheckStatus + }); + }; + + Checkbox.prototype.isClickableOverlayItem = function(item) { + return (item == this.thumb) || (item == this.checkMark); + }; + + Checkbox.prototype.onMousePressEvent = function(event, clickedOverlay) { + if (!this.isClickableOverlayItem(clickedOverlay)) { + return; + } + this.boxCheckStatus = !this.boxCheckStatus; + this.onValueChanged(this.getValue()); + this.updateThumb(); + }; + + Checkbox.prototype.onMouseReleaseEvent = function(event) {}; + + Checkbox.prototype.updateWithKeys = function() { + this.boxCheckStatus = !this.boxCheckStatus; + this.onValueChanged(this.getValue()); + this.updateThumb(); + }; + + Checkbox.prototype.highlight = function() { + Overlays.editOverlay(this.thumb, { + backgroundColor: THUMB_HIGHLIGHT + }); + this.highlighted = true; + }; + + Checkbox.prototype.unhighlight = function() { + Overlays.editOverlay(this.thumb, { + backgroundColor: THUMB_COLOR + }); + this.highlighted = false; + }; + + Checkbox.prototype.setValue = function(value) { + this.boxCheckStatus = value; + }; + + Checkbox.prototype.setterFromWidget = function(value) { + this.updateThumb(); + }; + + Checkbox.prototype.getValue = function() { + return this.boxCheckStatus; + }; + + Checkbox.prototype.reset = function(resetValue) { + this.setValue(resetValue); + }; + + Checkbox.prototype.onValueChanged = function(value) {}; + + Checkbox.prototype.getHeight = function() { + if (!this.visible) { + return 0; + } + return 1.5 * this.thumbSize; + }; + + Checkbox.prototype.moveUp = function(newY) { + Overlays.editOverlay(this.background, { + y: newY + }); + Overlays.editOverlay(this.thumb, { + y: newY + }); + Overlays.editOverlay(this.checkMark, { + y: newY + (0.25 * this.thumbSize) + }); + Overlays.editOverlay(this.unCheckMark, { + y: newY + (0.25 * this.thumbSize) + }); + }; + + Checkbox.prototype.moveDown = function() { + Overlays.editOverlay(this.background, { + y: this.y + }); + Overlays.editOverlay(this.thumb, { + y: this.y + }); + Overlays.editOverlay(this.checkMark, { + y: this.y + (0.25 * this.thumbSize) + }); + Overlays.editOverlay(this.unCheckMark, { + y: this.y+ (0.25 * this.thumbSize) + }); + }; + + Checkbox.prototype.hide = function() { + Overlays.editOverlay(this.background, { + visible: false + }); + Overlays.editOverlay(this.thumb, { + visible: false + }); + Overlays.editOverlay(this.checkMark, { + visible: false + }); + Overlays.editOverlay(this.unCheckMark, { + visible: false + }); + this.visible = false; + } + + Checkbox.prototype.show = function() { + Overlays.editOverlay(this.background, { + visible: true + }); + Overlays.editOverlay(this.thumb, { + visible: true + }); + Overlays.editOverlay(this.checkMark, { + visible: true + }); + Overlays.editOverlay(this.unCheckMark, { + visible: true + }); + this.visible = true; + } + + Checkbox.prototype.destroy = function() { + Overlays.deleteOverlay(this.background); + Overlays.deleteOverlay(this.thumb); + Overlays.deleteOverlay(this.checkMark); + Overlays.deleteOverlay(this.unCheckMark); + }; + + this.Checkbox = Checkbox; + + // The ColorBox class + var ColorBox = function(x, y, width, thumbSize) { + var self = this; + + var slideHeight = thumbSize / 3; + var sliderWidth = width; + this.red = new Slider(x, y, width, slideHeight); + this.green = new Slider(x, y + slideHeight, width, slideHeight); + this.blue = new Slider(x, y + 2 * slideHeight, width, slideHeight); + this.red.setBackgroundColor({ + x: 1, + y: 0, + z: 0 + }); + this.green.setBackgroundColor({ + x: 0, + y: 1, + z: 0 + }); + this.blue.setBackgroundColor({ + x: 0, + y: 0, + z: 1 + }); + + this.red.onValueChanged = this.setterFromWidget; + this.green.onValueChanged = this.setterFromWidget; + this.blue.onValueChanged = this.setterFromWidget; + + this.visible = true; + }; + + ColorBox.prototype.isClickableOverlayItem = function(item) { + return this.red.isClickableOverlayItem(item) || this.green.isClickableOverlayItem(item) || this.blue.isClickableOverlayItem(item); + }; + + ColorBox.prototype.onMousePressEvent = function(event, clickedOverlay) { + this.red.onMousePressEvent(event, clickedOverlay); + if (this.red.isMoving) { + return; + } + + this.green.onMousePressEvent(event, clickedOverlay); + if (this.green.isMoving) { + return; + } + + this.blue.onMousePressEvent(event, clickedOverlay); + }; + + ColorBox.prototype.onMouseMoveEvent = function(event) { + this.red.onMouseMoveEvent(event); + this.green.onMouseMoveEvent(event); + this.blue.onMouseMoveEvent(event); + }; + + ColorBox.prototype.onMouseReleaseEvent = function(event) { + this.red.onMouseReleaseEvent(event); + this.green.onMouseReleaseEvent(event); + this.blue.onMouseReleaseEvent(event); + }; + + ColorBox.prototype.updateWithKeys = function(direction) { + this.red.updateWithKeys(direction); + this.green.updateWithKeys(direction); + this.blue.updateWithKeys(direction); + } + + ColorBox.prototype.highlight = function() { + this.red.highlight(); + this.green.highlight(); + this.blue.highlight(); + + this.highlighted = true; + }; + + ColorBox.prototype.unhighlight = function() { + this.red.unhighlight(); + this.green.unhighlight(); + this.blue.unhighlight(); + + this.highlighted = false; + }; + + ColorBox.prototype.setterFromWidget = function(value) { + var color = this.getValue(); + this.onValueChanged(color); + this.updateRGBSliders(color); + }; + + ColorBox.prototype.onValueChanged = function(value) {}; + + ColorBox.prototype.updateRGBSliders = function(color) { + this.red.setThumbColor({ + x: color.x, + y: 0, + z: 0 + }); + this.green.setThumbColor({ + x: 0, + y: color.y, + z: 0 + }); + this.blue.setThumbColor({ + x: 0, + y: 0, + z: color.z + }); + }; + + // Public members: + ColorBox.prototype.setValue = function(value) { + this.red.setValue(value.x); + this.green.setValue(value.y); + this.blue.setValue(value.z); + this.updateRGBSliders(value); + }; + + ColorBox.prototype.getValue = function() { + var value = { + x: this.red.getValue(), + y: this.green.getValue(), + z: this.blue.getValue() + }; + return value; + }; + + ColorBox.prototype.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + + + ColorBox.prototype.getHeight = function() { + if (!this.visible) { + return 0; + } + return 1.5 * this.thumbSize; + }; + + ColorBox.prototype.moveUp = function(newY) { + this.red.moveUp(newY); + this.green.moveUp(newY); + this.blue.moveUp(newY); + }; + + ColorBox.prototype.moveDown = function() { + this.red.moveDown(); + this.green.moveDown(); + this.blue.moveDown(); + }; + + ColorBox.prototype.hide = function() { + this.red.hide(); + this.green.hide(); + this.blue.hide(); + this.visible = false; + } + + ColorBox.prototype.show = function() { + this.red.show(); + this.green.show(); + this.blue.show(); + this.visible = true; + } + + ColorBox.prototype.destroy = function() { + this.red.destroy(); + this.green.destroy(); + this.blue.destroy(); + }; + + this.ColorBox = ColorBox; + + // The DirectionBox class + var DirectionBox = function(x, y, width, thumbSize) { + var self = this; + + var slideHeight = thumbSize / 2; + var sliderWidth = width; + this.yaw = new Slider(x, y, width, slideHeight); + this.pitch = new Slider(x, y + slideHeight, width, slideHeight); + + + this.yaw.setThumbColor({ + x: 1, + y: 0, + z: 0 + }); + this.yaw.minValue = -180; + this.yaw.maxValue = +180; + + this.pitch.setThumbColor({ + x: 0, + y: 0, + z: 1 + }); + this.pitch.minValue = -1; + this.pitch.maxValue = +1; + + this.yaw.onValueChanged = this.setterFromWidget; + this.pitch.onValueChanged = this.setterFromWidget; + + this.visible = true; + }; + + DirectionBox.prototype.isClickableOverlayItem = function(item) { + return this.yaw.isClickableOverlayItem(item) || this.pitch.isClickableOverlayItem(item); + }; + + DirectionBox.prototype.onMousePressEvent = function(event, clickedOverlay) { + this.yaw.onMousePressEvent(event, clickedOverlay); + if (this.yaw.isMoving) { + return; + } + this.pitch.onMousePressEvent(event, clickedOverlay); + }; + + DirectionBox.prototype.onMouseMoveEvent = function(event) { + this.yaw.onMouseMoveEvent(event); + this.pitch.onMouseMoveEvent(event); + }; + + DirectionBox.prototype.onMouseReleaseEvent = function(event) { + this.yaw.onMouseReleaseEvent(event); + this.pitch.onMouseReleaseEvent(event); + }; + + DirectionBox.prototype.updateWithKeys = function(direction) { + this.yaw.updateWithKeys(direction); + this.pitch.updateWithKeys(direction); + }; + + DirectionBox.prototype.highlight = function() { + this.pitch.highlight(); + this.yaw.highlight(); + + this.highlighted = true; + }; + + DirectionBox.prototype.unhighlight = function() { + this.pitch.unhighlight(); + this.yaw.unhighlight(); + + this.highlighted = false; + }; + + DirectionBox.prototype.setterFromWidget = function(value) { + var yawPitch = this.getValue(); + this.onValueChanged(yawPitch); + }; + + DirectionBox.prototype.onValueChanged = function(value) {}; + + DirectionBox.prototype.setValue = function(direction) { + var flatXZ = Math.sqrt(direction.x * direction.x + direction.z * direction.z); + if (flatXZ > 0.0) { + var flatX = direction.x / flatXZ; + var flatZ = direction.z / flatXZ; + var yaw = Math.acos(flatX) * 180 / Math.PI; + if (flatZ < 0) { + yaw = -yaw; + } + this.yaw.setValue(yaw); + } + this.pitch.setValue(direction.y); + }; + + DirectionBox.prototype.getValue = function() { + var dirZ = this.pitch.getValue(); + var yaw = this.yaw.getValue() * Math.PI / 180; + var cosY = Math.sqrt(1 - dirZ * dirZ); + var value = { + x: cosY * Math.cos(yaw), + y: dirZ, + z: cosY * Math.sin(yaw) + }; + return value; + }; + + DirectionBox.prototype.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + + DirectionBox.prototype.getHeight = function() { + if (!this.visible) { + return 0; + } + return 1.5 * this.thumbSize; + }; + + DirectionBox.prototype.moveUp = function(newY) { + this.pitch.moveUp(newY); + this.yaw.moveUp(newY); + }; + + DirectionBox.prototype.moveDown = function(newY) { + this.pitch.moveDown(); + this.yaw.moveDown(); + }; + + DirectionBox.prototype.hide = function() { + this.pitch.hide(); + this.yaw.hide(); + this.visible = false; + } + + DirectionBox.prototype.show = function() { + this.pitch.show(); + this.yaw.show(); + this.visible = true; + } + + DirectionBox.prototype.destroy = function() { + this.yaw.destroy(); + this.pitch.destroy(); + }; + + this.DirectionBox = DirectionBox; + + var textFontSize = 12; + + var CollapsablePanelItem = function(name, x, y, textWidth, height) { + this.name = name; + this.height = height; + this.y = y; + this.isCollapsable = true; + + var topMargin = (height - 1.5 * textFontSize); + + this.thumb = Overlays.addOverlay("image", { + color: { + red: 255, + green: 255, + blue: 255 + }, + imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/expand-ui.svg', + x: x, + y: y, + width: rawHeight, + height: rawHeight, + alpha: 1.0, + visible: true + }); + + this.title = Overlays.addOverlay("text", { + backgroundColor: { + red: 255, + green: 255, + blue: 255 + }, + x: x + rawHeight * 1.5, + y: y, + width: textWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: " " + name, + font: { + size: textFontSize + }, + topMargin: topMargin + }); + }; + + CollapsablePanelItem.prototype.destroy = function() { + Overlays.deleteOverlay(this.title); + Overlays.deleteOverlay(this.thumb); + }; + + CollapsablePanelItem.prototype.editTitle = function(opts) { + Overlays.editOverlay(this.title, opts); + }; + + CollapsablePanelItem.prototype.hide = function() { + Overlays.editOverlay(this.title, { + visible: false + }); + Overlays.editOverlay(this.thumb, { + visible: false + }); + + if (this.widget != null) { + this.widget.hide(); + } + }; + + CollapsablePanelItem.prototype.show = function() { + Overlays.editOverlay(this.title, { + visible: true + }); + Overlays.editOverlay(this.thumb, { + visible: true + }); + + if (this.widget != null) { + this.widget.show(); + } + }; + + CollapsablePanelItem.prototype.moveUp = function(newY) { + Overlays.editOverlay(this.title, { + y: newY + }); + Overlays.editOverlay(this.thumb, { + y: newY + }); + + if (this.widget != null) { + this.widget.moveUp(newY); + } + } + + CollapsablePanelItem.prototype.moveDown = function() { + Overlays.editOverlay(this.title, { + y: this.y + }); + Overlays.editOverlay(this.thumb, { + y: this.y + }); + + if (this.widget != null) { + this.widget.moveDown(); + } + } + this.CollapsablePanelItem = CollapsablePanelItem; + + var PanelItem = function(name, setter, getter, displayer, x, y, textWidth, valueWidth, height) { + //print("creating panel item: " + name); + + this.isCollapsable = false; + this.name = name; + this.y = y; + this.isCollapsed = false; + + this.displayer = typeof displayer !== 'undefined' ? displayer : function(value) { + if (value == true) { + return "On"; + } else if (value == false) { + return "Off"; + } + return value.toFixed(2); + }; + + var topMargin = (height - 1.5 * textFontSize); + this.title = Overlays.addOverlay("text", { + backgroundColor: { + red: 255, + green: 255, + blue: 255 + }, + x: x, + y: y, + width: textWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: " " + name, + font: { + size: textFontSize + }, + topMargin: topMargin + }); + + this.value = Overlays.addOverlay("text", { + backgroundColor: { + red: 255, + green: 255, + blue: 255 + }, + x: x + textWidth, + y: y, + width: valueWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: this.displayer(getter()), + font: { + size: textFontSize + }, + topMargin: topMargin + }); + + this.getter = getter; + this.resetValue = getter(); + + this.setter = function(value) { + + setter(value); + + Overlays.editOverlay(this.value, { + text: this.displayer(getter()) + }); + + if (this.widget) { + this.widget.setValue(value); + } + + //print("successfully set value of widget to " + value); + }; + this.setterFromWidget = function(value) { + setter(value); + // ANd loop back the value after the final setter has been called + var value = getter(); + + if (this.widget) { + this.widget.setValue(value); + } + Overlays.editOverlay(this.value, { + text: this.displayer(value) + }); + }; + + this.widget = null; + }; + + PanelItem.prototype.hide = function() { + Overlays.editOverlay(this.title, { + visible: false + }); + Overlays.editOverlay(this.value, { + visible: false + }); + + if (this.widget != null) { + this.widget.hide(); + } + }; + + + PanelItem.prototype.show = function() { + Overlays.editOverlay(this.title, { + visible: true + }); + Overlays.editOverlay(this.value, { + visible: true + }); + + if (this.widget != null) { + this.widget.show(); + } + + }; + + PanelItem.prototype.moveUp = function(newY) { + + Overlays.editOverlay(this.title, { + y: newY + }); + Overlays.editOverlay(this.value, { + y: newY + }); + + if (this.widget != null) { + this.widget.moveUp(newY); + } + + }; + + PanelItem.prototype.moveDown = function() { + + Overlays.editOverlay(this.title, { + y: this.y + }); + Overlays.editOverlay(this.value, { + y: this.y + }); + + if (this.widget != null) { + this.widget.moveDown(); + } + + }; + + PanelItem.prototype.destroy = function() { + Overlays.deleteOverlay(this.title); + Overlays.deleteOverlay(this.value); + + if (this.widget != null) { + this.widget.destroy(); + } + }; + this.PanelItem = PanelItem; + + var textWidth = 180; + var valueWidth = 100; + var widgetWidth = 300; + var rawHeight = 20; + var rawYDelta = rawHeight * 1.5; + var outerPanel = true; + var widgets; + + + var Panel = function(x, y) { + + if (outerPanel) { + widgets = []; + } + outerPanel = false; + + this.x = x; + this.y = y; + this.nextY = y; + + print("creating panel at x: " + this.x + " y: " + this.y); + + this.widgetX = x + textWidth + valueWidth; + + this.items = new Array(); + this.activeWidget = null; + this.visible = true; + this.indentation = 30; + + }; + + Panel.prototype.mouseMoveEvent = function(event) { + if (this.activeWidget) { + this.activeWidget.onMouseMoveEvent(event); + } + }; + + Panel.prototype.mousePressEvent = function(event) { + // Make sure we quitted previous widget + if (this.activeWidget) { + this.activeWidget.onMouseReleaseEvent(event); + } + this.activeWidget = null; + + var clickedOverlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); + + this.handleCollapse(clickedOverlay); + + // If the user clicked any of the slider background then... + for (var i in this.items) { + var item = this.items[i]; + var widget = this.items[i].widget; + + if (widget.isClickableOverlayItem(clickedOverlay)) { + this.activeWidget = widget; + this.activeWidget.onMousePressEvent(event, clickedOverlay); + break; + + } + } + }; + + Panel.prototype.mouseReleaseEvent = function(event) { + if (this.activeWidget) { + this.activeWidget.onMouseReleaseEvent(event); + } + this.activeWidget = null; + }; + + // Reset panel item upon double-clicking + Panel.prototype.mouseDoublePressEvent = function(event) { + var clickedOverlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); + this.handleReset(clickedOverlay); + }; + + + Panel.prototype.handleReset = function(clickedOverlay) { + for (var i in this.items) { + + var item = this.items[i]; + var widget = item.widget; + + if (item.isSubPanel && widget) { + widget.handleReset(clickedOverlay); + } + + if (clickedOverlay == item.title) { + item.activeWidget = widget; + item.activeWidget.reset(item.resetValue); + break; + } + } + }; + + Panel.prototype.handleCollapse = function(clickedOverlay) { + for (var i in this.items) { + + var item = this.items[i]; + var widget = item.widget; + + if (item.isSubPanel && widget) { + widget.handleCollapse(clickedOverlay); + } + + if (!item.isCollapsed && item.isCollapsable && clickedOverlay == item.thumb) { + Overlays.editOverlay(item.thumb, { + imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/expand-right.svg' + }); + this.collapse(clickedOverlay); + item.isCollapsed = true; + } else if (item.isCollapsed && item.isCollapsable && clickedOverlay == item.thumb) { + Overlays.editOverlay(item.thumb, { + imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/expand-ui.svg' + }); + this.expand(clickedOverlay); + item.isCollapsed = false; + } + } + }; + + Panel.prototype.collapse = function(clickedOverlay) { + var keys = Object.keys(this.items); + + for (var i = 0; i < keys.length; ++i) { + var item = this.items[keys[i]]; + + if (item.isCollapsable && clickedOverlay == item.thumb) { + var panel = item.widget; + panel.hide(); + break; + } + } + + // Now recalculate new heights of subsequent widgets + for (var j = i + 1; j < keys.length; ++j) { + this.items[keys[j]].moveUp(this.getCurrentY(keys[j])); + } + }; + + Panel.prototype.expand = function(clickedOverlay) { + var keys = Object.keys(this.items); + + for (var i = 0; i < keys.length; ++i) { + var item = this.items[keys[i]]; + + if (item.isCollapsable && clickedOverlay == item.thumb) { + var panel = item.widget; + panel.show(); + break; + } + } + // Now recalculate new heights of subsequent widgets + for (var j = i + 1; j < keys.length; ++j) { + this.items[keys[j]].moveDown(); + } + }; + + + Panel.prototype.onMousePressEvent = function(event, clickedOverlay) { + for (var i in this.items) { + var item = this.items[i]; + if (item.widget.isClickableOverlayItem(clickedOverlay)) { + item.activeWidget = item.widget; + item.activeWidget.onMousePressEvent(event, clickedOverlay); + } + } + }; + + Panel.prototype.onMouseMoveEvent = function(event) { + for (var i in this.items) { + var item = this.items[i]; + if (item.activeWidget) { + item.activeWidget.onMouseMoveEvent(event); + } + } + }; + + Panel.prototype.onMouseReleaseEvent = function(event, clickedOverlay) { + for (var i in this.items) { + var item = this.items[i]; + if (item.activeWidget) { + item.activeWidget.onMouseReleaseEvent(event); + } + item.activeWidget = null; + } + }; + + Panel.prototype.onMouseDoublePressEvent = function(event, clickedOverlay) { + for (var i in this.items) { + var item = this.items[i]; + if (item.activeWidget) { + item.activeWidget.onMouseDoublePressEvent(event); + } + } + }; + + var tabView = false; + var tabIndex = 0; + + Panel.prototype.keyPressEvent = function(event) { + if (event.text == "TAB" && !event.isShifted) { + tabView = true; + if (tabIndex < widgets.length) { + if (tabIndex > 0 && widgets[tabIndex - 1].highlighted) { + // Unhighlight previous widget + widgets[tabIndex - 1].unhighlight(); + } + widgets[tabIndex].highlight(); + tabIndex++; + } else { + widgets[tabIndex - 1].unhighlight(); + //Wrap around to front + tabIndex = 0; + widgets[tabIndex].highlight(); + tabIndex++; + } + } else if (tabView && event.isShifted) { + if (tabIndex > 0) { + tabIndex--; + if (tabIndex < widgets.length && widgets[tabIndex + 1].highlighted) { + // Unhighlight previous widget + widgets[tabIndex + 1].unhighlight(); + } + widgets[tabIndex].highlight(); + } else { + widgets[tabIndex].unhighlight(); + //Wrap around to end + tabIndex = widgets.length - 1; + widgets[tabIndex].highlight(); + } + } else if (event.text == "LEFT") { + for (var i = 0; i < widgets.length; i++) { + // Find the highlighted widget, allow control with arrow keys + if (widgets[i].highlighted) { + var k = -1; + widgets[i].updateWithKeys(k); + break; + } + } + } else if (event.text == "RIGHT") { + for (var i = 0; i < widgets.length; i++) { + // Find the highlighted widget, allow control with arrow keys + if (widgets[i].highlighted) { + var k = 1; + widgets[i].updateWithKeys(k); + break; + } + } + } + }; + + // Widget constructors + Panel.prototype.newSlider = function(name, minValue, maxValue, setValue, getValue, displayValue) { + this.nextY = this.y + this.getHeight(); + + var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); + var slider = new Slider(this.widgetX, this.nextY, widgetWidth, rawHeight); + slider.minValue = minValue; + slider.maxValue = maxValue; + widgets.push(slider); + + item.widget = slider; + item.widget.onValueChanged = function(value) { + item.setterFromWidget(value); + }; + item.setter(getValue()); + this.items[name] = item; + + }; + + Panel.prototype.newCheckbox = function(name, setValue, getValue, displayValue) { + var display; + if (displayValue == true) { + display = function() { + return "On"; + }; + } else if (displayValue == false) { + display = function() { + return "Off"; + }; + } + + this.nextY = this.y + this.getHeight(); + + var item = new PanelItem(name, setValue, getValue, display, this.x, this.nextY, textWidth, valueWidth, rawHeight); + + var checkbox = new Checkbox(this.widgetX, this.nextY, widgetWidth, rawHeight); + widgets.push(checkbox); + + item.widget = checkbox; + item.widget.onValueChanged = function(value) { + item.setterFromWidget(value); + }; + item.setter(getValue()); + this.items[name] = item; + + //print("created Item... checkbox=" + name); + }; + + Panel.prototype.newColorBox = function(name, setValue, getValue, displayValue) { + this.nextY = this.y + this.getHeight(); + + var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); + + var colorBox = new ColorBox(this.widgetX, this.nextY, widgetWidth, rawHeight); + widgets.push(colorBox); + + item.widget = colorBox; + item.widget.onValueChanged = function(value) { + item.setterFromWidget(value); + }; + item.setter(getValue()); + this.items[name] = item; + + // print("created Item... colorBox=" + name); + }; + + Panel.prototype.newDirectionBox = function(name, setValue, getValue, displayValue) { + this.nextY = this.y + this.getHeight(); + + var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); + + var directionBox = new DirectionBox(this.widgetX, this.nextY, widgetWidth, rawHeight); + widgets.push(directionBox); + + item.widget = directionBox; + item.widget.onValueChanged = function(value) { + item.setterFromWidget(value); + }; + item.setter(getValue()); + this.items[name] = item; + + // print("created Item... directionBox=" + name); + }; + + Panel.prototype.newSubPanel = function(name) { + + this.nextY = this.y + this.getHeight(); + + var item = new CollapsablePanelItem(name, this.x, this.nextY, textWidth, rawHeight); + item.isSubPanel = true; + + this.nextY += 1.5 * item.height; + + var subPanel = new Panel(this.x + this.indentation, this.nextY); + + item.widget = subPanel; + this.items[name] = item; + return subPanel; + // print("created Item... subPanel=" + name); + }; + + Panel.prototype.onValueChanged = function(value) { + for (var i in this.items) { + this.items[i].widget.onValueChanged(value); + } + }; + + + Panel.prototype.set = function(name, value) { + var item = this.items[name]; + if (item != null) { + return item.setter(value); + } + return null; + }; + + Panel.prototype.get = function(name) { + var item = this.items[name]; + if (item != null) { + return item.getter(); + } + return null; + }; + + Panel.prototype.getWidget = function(name) { + var item = this.items[name]; + if (item != null) { + return item.widget; + } + return null; + }; + + Panel.prototype.update = function(name) { + var item = this.items[name]; + if (item != null) { + return item.setter(item.getter()); + } + return null; + }; + + Panel.prototype.isClickableOverlayItem = function(item) { + for (var i in this.items) { + if (this.items[i].widget.isClickableOverlayItem(item)) { + return true; + } + } + return false; + }; + + Panel.prototype.getHeight = function() { + var height = 0; + + for (var i in this.items) { + height += this.items[i].widget.getHeight(); + if (this.items[i].isSubPanel && this.items[i].widget.visible) { + height += 1.5 * rawHeight; + } + } + + return height; + }; + + Panel.prototype.moveUp = function() { + for (var i in this.items) { + this.items[i].widget.moveUp(); + } + }; + + Panel.prototype.moveDown = function() { + for (var i in this.items) { + this.items[i].widget.moveDown(); + } + }; + + Panel.prototype.getCurrentY = function(key) { + var height = 0; + var keys = Object.keys(this.items); + + for (var i = 0; i < keys.indexOf(key); ++i) { + var item = this.items[keys[i]]; + + height += item.widget.getHeight(); + + if (item.isSubPanel) { + height += 1.5 * rawHeight; + + } + } + return this.y + height; + }; + + + Panel.prototype.hide = function() { + for (var i in this.items) { + if (this.items[i].isSubPanel) { + this.items[i].widget.hide(); + } + this.items[i].hide(); + } + this.visible = false; + }; + + Panel.prototype.show = function() { + for (var i in this.items) { + if (this.items[i].isSubPanel) { + this.items[i].widget.show(); + } + this.items[i].show(); + } + this.visible = true; + }; + + + Panel.prototype.destroy = function() { + for (var i in this.items) { + + if (this.items[i].isSubPanel) { + this.items[i].widget.destroy(); + } + this.items[i].destroy(); + } + }; + this.Panel = Panel; +})(); + + +Script.scriptEnding.connect(function scriptEnding() { + Controller.releaseKeyEvents({ + text: "left" + }); + Controller.releaseKeyEvents({ + key: "right" + }); +}); + + +Controller.captureKeyEvents({ + text: "left" +}); +Controller.captureKeyEvents({ + text: "right" +});