// title: Overtone Explorer // author: prko // description: // [related forum post 1](https://scsynth.org/t/how-to-avoid-exception-in-graphdef-load-exceeded-number-of-interconnect-buffers-how-to-make-certain-sliders-of-multislider-read-only/8827/3) // // [related forum post 2](https://scsynth.org/t/newbie-cannot-get-new-code-to-compile/8840/3?u=prko) // // [related forum post 3](https://scsynth.org/t/gui-interactivity-limitations-or-the-problem-of-the-code-itself/8897) // code: ( s.options.numWireBufs = 128; // 256; ~numSliders = 64; // Tested at up to 120 ~mul = 0.1; ~freq = 200; // 393.44 s.waitForBoot { var numSliders, indexThumbSize, indexWidth, centre, upperHeight, lowerHeight, multiSliderHeight, width, win; var freqBox, multiSliderFreq, multiSliderPhase, phases, phaseButtons, ampNum, ampSlider, freqScope; var lable, lables, waveformView, waveform, interactivePhaseButtonsUpdate; var lablesHeight, interactiveLableFreq, interactiveLablePhase, interactiveLableUpdate, antiAliasing; numSliders = ~numSliders; indexThumbSize = 16; indexWidth = indexThumbSize + 1; centre = [Window.screenBounds.width, Window.screenBounds.height] / 2; width = if(numSliders <= 36) { 36 * indexWidth + 6 + 255; } { numSliders * indexWidth + 6 }; win = Window("overtone explorer", Rect(centre[0] - (width / 2), centre[1] - 325, width, 650)); win.front.onClose_{ x.free; freqScope.kill }.acceptsMouseOver_(true); lowerHeight = 250; upperHeight = win.bounds.height - lowerHeight; multiSliderHeight = upperHeight - 50 / 2; lable = { |parent, left, top, width, height, align, fontSize, string, colour, background| StaticText(parent, Rect(left, top, width, height)) .align_(align) .font_(Font("Arial", fontSize)) .stringColor_(colour) .string_(string) .background_(background) }; lablesHeight = 36; antiAliasing = { |freqBase| numSliders.do { |index| var thisFreq; thisFreq = index + 1 * freqBase; interactiveLableFreq.string_(""); if(thisFreq < (s.sampleRate / 2)) { lables[index].size.do { |i| lables[index][i].stringColor_(Color.black) } } { multiSliderFreq.value_(multiSliderFreq.value.put(index, 0)); lables[index].size.do { |i| lables[index][i].stringColor_(Color.grey(0.6)) } } } }; multiSliderFreq = if(numSliders < 36) { MultiSliderView(win, Rect(1, 0, numSliders * indexWidth + 6, multiSliderHeight)); } { MultiSliderView(win, Rect(1, 0, width, multiSliderHeight)); }; multiSliderFreq.value_((0 ! numSliders)) .showIndex_(true) .indexThumbSize_(indexThumbSize) .action_{ |sliders| var numNotZero = ~numSliders - sliders.value.occurrencesOf(0); x.set(\numNotZero, numNotZero, \overtoneLast, ~numSliders, \amplitudes, sliders.value) } .mouseOverAction_{ |view, x, y| var mouseOverIndex; mouseOverIndex = (x / indexWidth).round; interactiveLableUpdate.(x, y, multiSliderFreq, interactiveLableFreq, "Hz;\n", 100, "%", "\n0 % to prevent aliasing"); interactiveLableFreq.stringColor_(if(mouseOverIndex * freqBox.value < (s.sampleRate / 2)) { Color.blue(1, 0.5) } { Color.grey(0.5) }); } .mouseMoveAction_{ |view, x, y| var mouseMoveIndex; mouseMoveIndex = (x / indexWidth).round; interactiveLableUpdate.(x, y, multiSliderFreq, interactiveLableFreq, "Hz;\n", 100, "%", "\n0 % to prevent aliasing"); interactiveLableFreq.stringColor_(if(mouseMoveIndex * freqBox.value < (s.sampleRate / 2)) { Color.blue(1, 0.5) } { Color.grey(0.5) }); } .mouseUpAction_{ |view, x, y| antiAliasing.(freqBox.value); interactiveLableUpdate.(x, y, multiSliderFreq, interactiveLableFreq, "Hz;\n", 100, "%", "\n0 % to prevent aliasing") } .mouseLeaveAction_{ interactiveLableFreq.string_("Amplitudes") .bounds_(Rect( multiSliderFreq.bounds.extent.x - 120 / 2, multiSliderFreq.bounds.extent.y - 20 / 2, 120, 25)); multiSliderFreq.showIndex_(false) } .mouseEnterAction_{ multiSliderFreq.showIndex_(true) } .background_(Color.grey(0.9, 0.1)) .colors_(Color.grey(0.3, 0.5), Color.grey(0.3, 0.5)); interactiveLableFreq = lable.(multiSliderFreq, multiSliderFreq.bounds.extent.x - 120 / 2, multiSliderFreq.bounds.extent.y - 20 / 2, 120, 25, \left, 24, "Amplitudes", Color.blue(1, 0.5), Color.grey(0.9, 0.7)); lables = numSliders.collect { |i| var string, left; string = (i + 1).asString.padLeft(3, "0"); left = i * indexWidth + 7; [ lable.(win, left, multiSliderHeight + 2, 10, 10, \center, 11, string[0], Color.black, Color.clear), lable.(win, left, multiSliderHeight + 14, 10, 10, \center, 11, string[1], Color.black, Color.clear), lable.(win, left, multiSliderHeight + 26, 10, 10, \center, 11, string[2], Color.black, Color.clear) ] }; phases = 0 ! ~numSliders; phaseButtons = ~numSliders.collect { |i| Button(win, Rect( indexThumbSize + 1 * i + 4, upperHeight / 2 - 15 + 26, indexThumbSize, indexThumbSize )) .states_([["O", Color.black], ["Ø", Color.green(0.6)]]) .action = { |view| var phase = if(view.value == 0) { 0 } { pi }; if(phaseButtons[i].states.size == 1) { phaseButtons[i] .states_([["O", Color.black], ["Ø", Color.green(0.6)]]) }; multiSliderPhase.value = multiSliderPhase.value.put(i, phase.linlin(0, 2pi, 0, 1)); phases.put(i, phase); x.set(\phases, phases) } }; multiSliderPhase = if(numSliders < 36) { MultiSliderView(win, Rect( 1, multiSliderHeight + lablesHeight + indexThumbSize, numSliders * indexWidth + 6, multiSliderHeight )); } { MultiSliderView(win, Rect( 1, multiSliderHeight + lablesHeight + indexThumbSize, width, multiSliderHeight )); }; multiSliderPhase.value_(((0 ! numSliders))) .showIndex_(true) .indexThumbSize_(indexThumbSize) .action_{ |sliders| var phases = sliders.value.linlin(0, 1, 0, 2pi); interactivePhaseButtonsUpdate.(sliders.index); x.set(\phases, phases) } .mouseOverAction_{ |view, x, y| var mouseOverIndex; mouseOverIndex = (x / indexWidth).round; interactiveLableUpdate.(x, y, multiSliderPhase, interactiveLablePhase, "Hz;\n", 360, "°", ""); interactiveLablePhase.stringColor_(if(mouseOverIndex * freqBox.value < (s.sampleRate / 2)) { Color.blue(1, 0.5) } { Color.grey(0.5) }) } .mouseMoveAction_{ |view, x, y| var mouseMoveIndex; mouseMoveIndex = (x / indexWidth).round.clip(0, ~numSliders - 1); interactivePhaseButtonsUpdate.(mouseMoveIndex); interactiveLableUpdate.(x, y, multiSliderPhase, interactiveLablePhase, "Hz;\n", 360, "°", ""); interactiveLablePhase.stringColor_(if(mouseMoveIndex * freqBox.value < (s.sampleRate / 2)) { Color.blue(1, 0.5) } { Color.grey(0.5) }) } .mouseUpAction_{ |view, x, y| interactiveLableUpdate.(x, y, multiSliderPhase, interactiveLablePhase, "Hz;\n", 360, "°", "") } .mouseLeaveAction_{ interactiveLablePhase.string_("Phases") .bounds_(Rect( multiSliderPhase.bounds.extent.x - 100 / 2, multiSliderPhase.bounds.extent.y - 20 / 2, 100, 25)); multiSliderPhase.showIndex_(false) } .mouseEnterAction_{ multiSliderPhase.showIndex_(true) } .background_(Color.grey(0.9, 0.1)) .colors_(Color.grey(0.3, 0.5), Color.grey(0.3, 0.5)); interactiveLablePhase = lable.(multiSliderPhase, multiSliderPhase.bounds.extent.x - 100 / 2, multiSliderPhase.bounds.extent.y - 20 / 2, 100, 25, \left, 24, "Phases", Color.blue(1, 0.5), Color.grey(0.9, 0.7)); interactiveLableUpdate = { |x, y, where, which, hzORphase, how, unit, antialiasing| var index, indexLable, indexLableSize, value, cps; index =(x / indexWidth).ceil.clip(1, ~numSliders); indexLable = index.asInteger.asString; indexLableSize = indexLable.size; value = y.linlin(0, where.bounds.extent.y - 1, how, 0).asInteger; cps = index * freqBox.value; which.string_( indexLable ++ ":" + cps ++ hzORphase + (" " ! (indexLableSize + 2)).join + value ++ unit + (if(s.sampleRate / 2 <= cps) { antialiasing } { "" }) ); which.bounds = which.bounds.size_(which.sizeHint); which.bounds_( x = if(x + which.bounds.width < where.bounds.extent.x) { x } { x - which.bounds.width }; y = where.bounds.extent.y - which.bounds.height / 2; Rect(x, y, which.bounds.width, which.bounds.height); ) }; interactivePhaseButtonsUpdate = { |buttonIndex| if(phases[buttonIndex] == 0.0 || (phases[buttonIndex] == 180.0)) { phaseButtons[buttonIndex] .states_([["O", Color.black], ["Ø", Color.green(0.6)]]); phaseButtons[buttonIndex].value = (phases[buttonIndex] % 180) } { phaseButtons[buttonIndex] .states_([["↓", Color.blue(1, 0.5)]]) } }; lable.(win, 10, upperHeight + 3, 300, 20, \left, 12, "Base frequency (Hz): (test with 393.44)", Color.black, Color.clear); freqBox = NumberBox(win, Rect(130, upperHeight + 3, 50, 20)) .font_(Font("Arial", 12)); freqBox.value_(~freq) .action_{ |numb| var freqBase = numb.value; x.set(\freq, numb.value); waveform.cycle_(s.sampleRate / numb.value); antiAliasing.(freqBase) }; freqScope = FreqScopeView(win, Rect(2, upperHeight + 3 + 22, 511, lowerHeight - 28)); freqScope.active_(true); ListView(win, Rect(2 + 511 - 30, upperHeight + 3 + 22, 30, 34)) .items_(["lin", "log"]) .action_{ |menu| freqScope.freqMode_(menu.value); }; ampNum = NumberBox(win, Rect(3 + 511, upperHeight + 3, 29, 20)) .font_(Font("Arial", 12)) .value_(0.1) .action_{ |numb| ampSlider.value_(numb.value); x.set(\mul, numb.value); }; ampSlider = Slider(win, Rect(2 + 511, upperHeight + 2 + 22, 31, lowerHeight - 26)) .value_(~mul) .action_{ x.set(\mul, ampSlider.value); ampNum.value_(ampSlider.value); }; ServerMeterView(s, win, (2 + 511 + 30) @ (upperHeight + 18), 0, 2); waveformView = View(win, Rect( 2 + 511 + 30 + ServerMeterView.getWidth(0, 1, s), upperHeight + 1, 255, 255 )); waveform = Stethoscope(s, 1, view: waveformView); SynthDef(\overtone_additive, { |overtoneFirst = 1, overtoneLast = 1, numNotZero = nil| var freq, phases, mul, numOvertones, maxLayers, amplitudes, temp, signalArray, sum; mul = \mul.kr(~mul); freq = \freq.kr(~freq); maxLayers = ~numSliders; phases = \phases.kr(0 ! maxLayers); amplitudes = \amplitudes.kr(0 ! maxLayers); numOvertones = if(numNotZero == nil) { overtoneLast - overtoneFirst + 1 } { numNotZero - overtoneFirst + 1 }.clip(1, ~numSliders); signalArray = maxLayers.collect { |i| var nthOvertone, minDetect, maxDetect, aliasingDetect, switch; nthOvertone = i + 1 * freq; minDetect = overtoneFirst <= (i + 1); maxDetect = overtoneLast >= (i + 1); aliasingDetect = nthOvertone < (SampleRate.ir / 2); switch = (minDetect * maxDetect * aliasingDetect).lag(0.2); SinOsc.ar(nthOvertone, phases[i].lag(0.2)) * switch * numOvertones.reciprocal.sqrt.lag(0.2) }; signalArray = signalArray * amplitudes.lag(0.2); sum = signalArray.sum * mul.lag(0.2); Out.ar(0, sum ! 2); }).add; s.sync; waveform.cycle_(s.sampleRate / ~freq); x = Synth(\overtone_additive); } ) x = Synth(\overtone_additive, [\amplitudes, 1 ! ~numSliders, \mul, 0.1]); x.set(\overtoneFirst, 2, \overtoneLast, 2); x.set(\overtoneFirst, 1, \overtoneLast, 1); x.set(\overtoneFirst, 1, \overtoneLast, 2); x.set(\overtoneFirst, 1, \overtoneLast, 3); x.set(\overtoneFirst, 4, \overtoneLast, 7); x.free