////////////////////////////////////////////// // // // KippleGEN or, The Empathy Box // // 2024 // // Daniel L. Lythgoe // // // ////////////////////////////////////////////// // This is a script for an (ambient) wavetable synth and sequencer with GUI. // Place the desired sound files in a subfolder called "/Samples/", relative to the folder the Supercollider document is located in. // This works best with very short sound files; longer ones can sometimes break the interpreter. // You can create your own .wav files, but a library of single cycle wave forms that can be used with the synth can be downloaded here: // https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/ // I recommend the OVERTONE pack to start // Special thanks for Reflectives on Youtube and his excellent video "Wave Terrainish adventures with VOsc" // Boot the server s.boot; // Directory for recordings Platform.recordingsDir; ( // Run once to convert and resample wavetable files. This may take a few seconds. Refer to the Post Window for confirmation of completion. // After "Conversion successfully completed", the active buffers and their corresponding symbols will be posted. var paths, file, data, n, newData, outFile, bufLenChecker, shortestBuf, dataList; dataList = List.new; paths = (thisProcess.nowExecutingPath.dirname +/+ "/Samples/*.wav").pathMatch; // <---- search relative path for all .wav files, excluding other file types // paths = "MYPATH/Samples/*.wav".pathMatch; // <-- if the relative path doesn't work, replace the MYPATH string in this line to hard code the search path ////////////////////////////////////// // // // Reading and Resampling // // // ////////////////////////////////////// Routine({ Routine({ // Step 1: Check for the shortest provided sample. paths.do { |it, i| file = SoundFile.openRead(paths[i]); data = Signal.newClear(file.numFrames); dataList.add(data.size); shortestBuf = dataList.minItem({|item, i| item}); // shortestBuf.postln; //<--- Troubleshooting }; // Step 2: Set wavetable frame size "n". By default set to 4096. // Since VOsc requires the length of the wavetable to be a power of 2, "shortestBuf" is always converted to the next power of two greater than or equal to the receiver. 1.do { n = 4096; // n.postln; //<--- Troubleshooting // if (shortestBuf < 4096) {n = shortestBuf.nextPowerOfTwo}; if (shortestBuf > 4096) {n = shortestBuf.nextPowerOfTwo}; if (shortestBuf > 8192) {n = 8192}; ("Wavetable Frame Size is " ++ n ++ ".").postln; }; // Step 3: Convert the samples to wavetables paths.do { |it, i| // 'protect' guarantees the file objects will be closed in case of error protect { // Read original size of data file = SoundFile.openRead(paths[i]); data = Signal.newClear(file.numFrames); file.readData(data); 0.1.wait; // Here we use the new frame size "n" that we calculated in Step 2 to resample the .wav files to the desired equal length newData = data.resamp1(n); 0.1.wait; // Convert the resampled signal into a Wavetable. // resamp1 outputs an Array, so we have to reconvert to Signal newData = newData.as(Signal).asWavetable; 0.1.wait; // save to disk. outFile = SoundFile(paths[i] ++ "_4096.wtable") .headerFormat_("WAV") .sampleFormat_("float") .numChannels_(1) .sampleRate_(44100); if(outFile.openWrite.notNil) { outFile.writeData(newData); 0.1.wait; } { "Couldn't write output file".warn; }; } { file.close; if(outFile.notNil) { outFile.close }; }; }; "Conversion successfully completed.".postln; // 0.1.wait; }).play; // Wait for the previous routine to finish (paths.size * 0.41 + 0.1).wait; "...".postln; 0.1.wait; ////////////////////////////////////////////////////// // // // Loading the Wavetables to Buffer Memory // // // ////////////////////////////////////////////////////// Routine({ var wtsize, wtpaths, wtbuffers; "-----------wavetables begin-----------".postln; wtsize = n; // Read back all the wavetables we created above wtpaths = (thisProcess.nowExecutingPath.dirname +/+ "/Samples/*.wtable").pathMatch; // Allocate double the wavetable frame size to the buffer to be safe wtbuffers = Buffer.allocConsecutive(wtpaths.size, s, wtsize * 2, 1, ); // Here we add each wavetable to the Buffers we created above. // We assign each buffer a name as a Symbol for easy access later on. This name is equivalent to the file name of the sample. // The buffer number and name are added to a global variable called "~bufferReference" for easy reference. This is however not needed for the synth in this file. wtpaths.do { |it, i| wtbuffers[i].read(wtpaths[i])}; ~wtbufnums = List[]; ~wavetables = (); ~bufferReference = List.new; wtpaths.do { |it i| var name, buffer; name = wtbuffers[i].path.basename.findRegexp(".*\.wav")[0][1].splitext[0]; buffer = wtbuffers[i].bufnum; ~wavetables[name.asSymbol] = buffer; ~wtbufnums.add(buffer); buffer.postln; name.postln; ~bufferReference.add((buffer + "=" + name).asString); }; // ~bufferReference.postln; "-----------wavetables end-----------".postln; };).play; };).play; ) // For reference ~wtbufnums; //<------------ Returns an array of the loaded buffer channels ~bufferReference; //<------ Returns an array of the loaded buffers and their respective names, i.e. Symbols ////////////////////////////////////////////////////// // // // Deckard's Synthdef // // // ////////////////////////////////////////////////////// ( var voices; // Specify the number of chorusing oscillators voices = 8; //Specify the number of channels ~numChans = 2; SynthDef.new(\vosc1, {|out=0, buf=0, numBufs=2, freq=200, amp=0.2, bufRate=0.01, bitRate = 44100.0, bitDepth = 24.0,atk=12,sus=6,rel=20, timeScale=1.0, detuneAmt=0.2,shaperWet=0.0, decimWet = 1.0, hpf=10,lpf=20000| var sig, bufpos, detuneSig, env, envG, shaperDry, decimDry; shaperWet = shaperWet.fold2(0.8).squared.sqrt; shaperDry = 1-shaperWet; decimDry = 1-decimWet; env = Env([0,0.5,1,1,0.5,0],[atk/2,atk/2,sus,rel/2,rel/2], curve: [5,-5,0,4,-3]); env.asArray.size.postln; env = EnvGen.kr(env,timeScale:timeScale, doneAction:2); detuneSig = LFNoise1.kr(0.2!voices).bipolar(detuneAmt).midiratio; bufpos = buf + LFNoise1.kr(bufRate).range(0, numBufs-1); sig = VOsc.ar(bufpos, freq*detuneSig); sig = (Decimator.ar(sig, Lag.kr(bitRate,5.0), Lag.kr(bitDepth,5.0))*decimWet) + (sig*decimDry); sig = (Shaper.ar(~wtbufnums[rrand(~wtbufnums.size-1,~wtbufnums[0])],sig)*shaperWet) + (sig*shaperDry); sig = HPF.ar(sig, Lag2.kr(hpf,8.0)); sig = LPF.ar(sig, Lag2.kr(lpf,8.0)); sig = SplayAz.ar(~numChans, sig); sig = LeakDC.ar(sig) * env * amp; Out.ar(out, sig); }).add ) ////////////////////////////////////////////////////// // // // Sequencer // // // ////////////////////////////////////////////////////// ( var sequencer, dictRoutine; ~voices = [1,1,1,1]; ~degrees = [0,7,9,10]; ~atk = 12; ~sus = 6; ~rel = 20; ~timeScale=1.0; ~dDict = Dictionary.newFrom([ \buf, ~wtbufnums[0], \numBufs, ~wtbufnums.size-1, \bufRate, 0.02, \degree1, 0, \degree2, 7, \degree3, 9, \degree4, 10, \freq, rrand(24,72).nearestInScale(~degrees, 12).postln.midicps, \amp, 0.2, \atk, 12, \sus, 6, \rel, 20, \timeScale, 1.0, \detuneAmt, 0.17, \decimWet, 1.0, \bitRate, 44100, \bitDepth, 24, \shaperWet, 0.0, \hpf, 10, \lpf, 20000]); ~dDictReset = Routine({1.do{ ~dDict[\buf] = ~wtbufnums[0]; ~dDict[\numBufs] = ~wtbufnums.size-1; ~dDict[\bufRate] = 0.02; ~dDict[\amp] = 0.2; ~dDict[\atk] = 12; ~dDict[\sus] = 6; ~dDict[\rel] = 20; ~dDict[\timeScale] = 1.0; ~dDict[\detuneAmt] = 0.17; ~dDict[\decimWet] = 1.0; ~dDict[\bitRate] = 44100; ~dDict[\bitDepth] = 24; ~dDict[\shaperWet] = 0.0; ~dDict[\hpf] = 20; ~dDict[\lpf] = 20000; }}); ~sequencer = Routine({ inf.do{ ///// VOICE 1 ///// 1.do{ if (~voices[0] == 1) { |it, i| ~v1 = Synth (\vosc1, [ \buf, ~dDict[\buf], \numBufs, ~dDict[\numBufs], \bufRate, ~dDict[\bufRate], \freq, rrand(24,72).nearestInScale(~degrees, 12).postln.midicps, \amp, ~dDict[\amp], \atk, ~dDict[\atk], \sus, ~dDict[\sus], \rel, ~dDict[\rel], \timeScale, ~dDict[\timeScale], \detuneAmt, ~dDict[\detuneAmt], \decimWet, ~dDict[\decimWet], \bitRate, ~dDict[\bitRate], \bitDepth, ~dDict[\bitDepth], \shaperWet, ~dDict[\shaperWet], \hpf, ~dDict[\hpf], \lpf, ~dDict[\lpf]] );}}; ///// VOICE 2 ///// 1.do{ if (~voices[1] == 1) { |it, i| ~v2 = Synth (\vosc1, [ \buf, ~dDict[\buf], \numBufs, ~dDict[\numBufs], \bufRate, ~dDict[\bufRate], \freq, rrand(24,72).nearestInScale(~degrees, 12).postln.midicps, \amp, ~dDict[\amp], \atk, ~dDict[\atk], \sus, ~dDict[\sus], \rel, ~dDict[\rel], \timeScale, ~dDict[\timeScale], \detuneAmt, ~dDict[\detuneAmt], \decimWet, ~dDict[\decimWet], \bitRate, ~dDict[\bitRate], \bitDepth, ~dDict[\bitDepth], \shaperWet, ~dDict[\shaperWet], \hpf, ~dDict[\hpf], \lpf, ~dDict[\lpf]] );}}; ///// VOICE 3 ///// 1.do{ if (~voices[2] == 1) { |it, i| ~v3 = Synth (\vosc1, [ \buf, ~dDict[\buf], \numBufs, ~dDict[\numBufs], \bufRate, ~dDict[\bufRate], \freq, rrand(24,72).nearestInScale(~degrees, 12).postln.midicps, \amp, ~dDict[\amp], \atk, ~dDict[\atk], \sus, ~dDict[\sus], \rel, ~dDict[\rel], \timeScale, ~dDict[\timeScale], \detuneAmt, ~dDict[\detuneAmt], \decimWet, ~dDict[\decimWet], \bitRate, ~dDict[\bitRate], \bitDepth, ~dDict[\bitDepth], \shaperWet, ~dDict[\shaperWet], \hpf, ~dDict[\hpf], \lpf, ~dDict[\lpf]] );}}; ///// VOICE 4 ///// 1.do{ if (~voices[3] == 1) { |it, i| ~v4 = Synth (\vosc1, [ \buf, ~dDict[\buf], \numBufs, ~dDict[\numBufs], \bufRate, ~dDict[\bufRate], \freq, rrand(24,72).nearestInScale(~degrees, 12).postln.midicps, \amp, ~dDict[\amp], \atk, ~dDict[\atk], \sus, ~dDict[\sus], \rel, ~dDict[\rel], \timeScale, ~dDict[\timeScale], \detuneAmt, ~dDict[\detuneAmt], \decimWet, ~dDict[\decimWet], \bitRate, ~dDict[\bitRate], \bitDepth, ~dDict[\bitDepth], \shaperWet, ~dDict[\shaperWet], \hpf, ~dDict[\hpf], \lpf, ~dDict[\lpf]] );}}; //4.wait; //<------ For troubleshooting purposes, ignore ((~dDict[\atk]+~dDict[\sus]+(~dDict[\rel]/7))*~dDict[\timeScale]).wait; }}); ) // The following controls for the "~sequencer" Routine are included in the GUI and are only included as text for troubleshooting purposes ~sequencer.reset; ~sequencer.play; ~sequencer.stop; ////////////////////////////////////////////////////// // // // GUI // // // ////////////////////////////////////////////////////// ( Routine({ var displayWindow = { var synth, ampSpec, intervalSpec, window, winX=550, winY=450; var knobAtk, knobSus, knobRel, knobTimeScale, knobDetune, knobDecim, knobBitRate, knobBitDepth, knobShaper, knobBufRate, knobHPF, knobLPF; var column2, column3, column4, row1, row2,row3,row4,row5, row6, row7,row8,row9,row10,row11,row12; var vs1,vs2,vs3; //vs -> vertical spacing ~voices = [1,1,1,1]; ~degrees = [0,7,9,10]; // Resets the dictionary to its default values ~dDictReset.reset; ~dDictReset.play; 0.05.wait; vs1 = 28; vs2 = 20; vs3 = 45; row1 = 30; row2 = (row1+vs2); row3 = (row2+vs3); row4 = (row3+vs1); row5 = (row4+vs2); row6 = (row5+vs3); row7 = (row6+vs1); row8 = (row7+vs2); row9 = (row8+vs3); row10 = (row9+vs1); row11 = (row10+vs2); row12 = (row11+vs3); column2 = 320; column3 = column2+60; column4 = column3+60; window = Window("Do Androids Dream of Electric Sheep?", Rect(100,200,winX,winY)); window.background = Color.fromHexString("#222A2D"); //#A96E6E intervalSpec = ControlSpec(0, 12,'lin',1); // CONTROLS FOR THE 4 VOICES -> interval, on/off 4.do {|i| var slider, text, offset, number, onOff; i = i + 1; offset = 60 * i; number = NumberBox(window, Rect(offset,255,50,20)); number.value = ~degrees[i-1]; slider = Slider(window, Rect(offset,row2,50,200)); slider.knobColor = Color.fromHexString("#B0BDE0"); slider.value = intervalSpec.unmap(~degrees[i-1]); slider.action = {|v| var val=intervalSpec.map(v.value); ~degrees[i-1] = val.asInteger; number.value=val }; text = StaticText(window,Rect(offset,30,50,20)); text.string = "Interval" ++ i; text.stringColor = Color.white; text.align = \center; onOff = Button(window, Rect(offset, 280,50,21)) .states_([["", Color.fromHexString("#B0BDE0"), Color.fromHexString("#B0BDE0")], ["", Color.white, Color.white]]) .action_({|view| if (view.value == 0) {~voices[i-1]=1} {~voices[i-1]=0} }); }; // ATTACK 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.1,30,'lin'); text = StaticText(window,Rect(column2,row1,40,20)); text.string = "Atk"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column2,row3,41,20)); knobAtk =Knob.new(window, Rect(column2,row2,40,40)); knobAtk.value = spec.unmap(~dDict[\atk]); number.value= spec.map(knobAtk.value); knobAtk.action = {|v| var val = ~dDict[\atk]; val = spec.map(v.value); ~dDict[\atk] = val; ~v1.set(\atk,val); ~v2.set(\atk,val); ~v3.set(\atk,val); ~v4.set(\atk,val); number.value=val; }}; // SUSTAIN 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.1,30,'lin'); text = StaticText(window,Rect(column2,row4,40,20)); text.string = "Sus"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column2,row6,41,20)); knobSus =Knob.new(window, Rect(column2,row5,40,40)); knobSus.value = spec.unmap(~dDict[\sus]); number.value= spec.map(knobSus.value); knobSus.action = {|v| var val = ~dDict[\sus]; val = spec.map(v.value); ~dDict[\sus] = val; ~v1.set(\sus,val); ~v2.set(\sus,val); ~v3.set(\sus,val); ~v4.set(\sus,val); number.value=val; }}; // RELEASE 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.1,30,'lin'); text = StaticText(window,Rect(column2,row7,40,20)); text.string = "Rel"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column2,row9,41,20)); knobRel =Knob.new(window, Rect(column2,row8,40,40)); knobRel.value = spec.unmap(~dDict[\rel]); number.value= spec.map(knobRel.value); knobRel.action = {|v| var val = ~dDict[\rel]; val = spec.map(v.value); ~dDict[\rel] = val; ~v1.set(\rel,val); ~v2.set(\rel,val); ~v3.set(\rel,val); ~v4.set(\rel,val); number.value=val; }}; // TIMESCALE 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.1,30,'exponential'); text = StaticText(window,Rect(column2,row10,40,20)); text.string = "tScale"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column2,row12,41,20)); knobTimeScale =Knob.new(window, Rect(column2,row11,40,40)); knobTimeScale.value = spec.unmap(~dDict[\timeScale]); number.value= spec.map(knobTimeScale.value); knobTimeScale.action = {|v| var val = ~dDict[\timeScale]; val = spec.map(v.value); ~dDict[\timeScale] = val; ~v1.set(\timeScale,val); ~v2.set(\timeScale,val); ~v3.set(\timeScale,val); ~v4.set(\timeScale,val); number.value=val; }}; // DETUNE 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.0,1,'lin'); text = StaticText(window,Rect(column3,row1,40,20)); text.string = "Detune"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column3,row3,41,20)); knobDetune =Knob.new(window, Rect(column3,row2,40,40)); knobDetune.value = spec.unmap(~dDict[\detuneAmt]); number.value= spec.map(knobDetune.value); knobDetune.action = {|v| var val = ~dDict[\detuneAmt]; val = spec.map(v.value); ~dDict[\detuneAmt] = val; ~v1.set(\detuneAmt,val); ~v2.set(\detuneAmt,val); ~v3.set(\detuneAmt,val); ~v4.set(\detuneAmt,val); number.value=val; }}; // DECIMATE 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.0,1,'lin'); text = StaticText(window,Rect(column3,row4,40,20)); text.string = "dcmWet"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column3,row6,41,20)); knobDecim =Knob.new(window, Rect(column3,row5,40,40)); knobDecim.value = spec.unmap(~dDict[\decimWet]); number.value= spec.map(knobDecim.value); knobDecim.action = {|v| var val = ~dDict[\decimWet]; val = spec.map(v.value); ~dDict[\decimWet] = val; ~v1.set(\decimWet,val); ~v2.set(\decimWet,val); ~v3.set(\decimWet,val); ~v4.set(\decimWet,val); number.value=val; }}; // BITRATE 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.0,44100,'lin'); text = StaticText(window,Rect(column3,row7,40,20)); text.string = "Bitrate"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column3,row9,41,20)); knobBitRate =Knob.new(window, Rect(column3,row8,40,40)); knobBitRate.value = spec.unmap(~dDict[\bitRate]); number.value= spec.map(knobBitRate.value); knobBitRate.action = {|v| var val = ~dDict[\bitRate]; val = spec.map(v.value); ~dDict[\bitRate] = val; ~v1.set(\bitRate,val); ~v2.set(\bitRate,val); ~v3.set(\bitRate,val); ~v4.set(\bitRate,val); number.value=val; }}; // BITDEPTH 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.0,24,'lin'); text = StaticText(window,Rect(column3,row10,40,20)); text.string = "Bitdepth"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column3,row12,41,20)); knobBitDepth =Knob.new(window, Rect(column3,row11,40,40)); knobBitDepth.value = spec.unmap(~dDict[\bitDepth]); number.value= spec.map(knobBitDepth.value); knobBitDepth.action = {|v| var val = ~dDict[\bitDepth]; val = spec.map(v.value); ~dDict[\bitDepth] = val; ~v1.set(\bitDepth,val); ~v2.set(\bitDepth,val); ~v3.set(\bitDepth,val); ~v4.set(\bitDepth,val); number.value=val; }}; // SHAPER 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.0,1,'lin'); text = StaticText(window,Rect(column4,row1,40,20)); text.string = "Shaper"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column4,row3,41,20)); knobShaper =Knob.new(window, Rect(column4,row2,40,40)); knobShaper.value = spec.unmap(~dDict[\shaperWet]); number.value= spec.map(knobShaper.value); knobShaper.action = {|v| var val = ~dDict[\shaperWet]; val = spec.map(v.value); ~dDict[\shaperWet] = val; ~v1.set(\shaperWet,val); ~v2.set(\shaperWet,val); ~v3.set(\shaperWet,val); ~v4.set(\shaperWet,val); number.value=val; }}; // BUFRATE 1.do{|i| var knob, text, number,spec; spec = ControlSpec(0.0,1,'lin'); text = StaticText(window,Rect(column4,row4,40,20)); text.string = "Bufrate"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column4,row6,41,20)); knobBufRate =Knob.new(window, Rect(column4,row5,40,40)); knobBufRate.value = spec.unmap(~dDict[\bufRate]); number.value= spec.map(knobBufRate.value); knobBufRate.action = {|v| var val = ~dDict[\bufRate]; val = spec.map(v.value).postln; ~dDict[\bufRate] = val; ~v1.set(\bufRate,val); ~v2.set(\bufRate,val); ~v3.set(\bufRate,val); ~v4.set(\bufRate,val); number.value=val; }}; // HIGH-PASS FILTER 1.do{|i| var knob, text, number,spec; spec = ControlSpec(10,20000,'exponential'); text = StaticText(window,Rect(column4,row7,40,20)); text.string = "HPF"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column4,row9,41,20)); knobHPF =Knob.new(window, Rect(column4,row8,40,40)); knobHPF.value = spec.unmap(~dDict[\hpf]); number.value= spec.map(knobHPF.value); knobHPF.action = {|v| var val = ~dDict[\hpf]; val = spec.map(v.value).postln; ~dDict[\hpf] = val; ~v1.set(\hpf,val); ~v2.set(\hpf,val); ~v3.set(\hpf,val); ~v4.set(\hpf,val); number.value=val; }}; // LOW-PASS FILTER 1.do{|i| var knob, text, number,spec; spec = ControlSpec(10,20000,'exponential'); text = StaticText(window,Rect(column4,row10,40,20)); text.string = "LPF"; text.stringColor = Color.white; text.align = \center; number = NumberBox(window, Rect(column4,row12,41,20)); knobLPF =Knob.new(window, Rect(column4,row11,40,40)); knobLPF.value = spec.unmap(~dDict[\lpf]); number.value= spec.map(knobLPF.value); knobLPF.action = {|v| var val = ~dDict[\lpf]; val = spec.map(v.value).postln; ~dDict[\lpf] = val; ~v1.set(\lpf,val); ~v2.set(\lpf,val); ~v3.set(\lpf,val); ~v4.set(\lpf,val); number.value=val; }}; // BUTTON TO START/STOP SEQUENCER Button(window, Rect(60,(row10+6),110,83)) .states_([["START", Color.black, Color.white], ["STOP", Color.white, Color.fromHexString("#8DB38B")]]) //#70AE6E .action_({|view| view.value.postln; if (view.value == 1) {~sequencer.reset;~sequencer.play} {~sequencer.stop} }); // BUTTON TO START/STOP RECORDING Button(window, Rect(180,(row10+6),110,83)) .states_([["REC", Color.black, Color.white], ["STOP REC", Color.white, Color.fromHexString("#D72638")]]) .action_({|view| view.value.postln; if (view.value == 1) {s.record(numChannels: ~numChans)} {s.stopRecording} }); //Notes 1.do{var text1, text2; text1 = StaticText.new(window, Rect(60,row10+92,200,20)); text1.string = "KippleGEN or, The Empathy Box (2024)"; text1.stringColor = Color.new(1,1,1,0.4); text1.align = \left; text2 = StaticText.new(window, Rect(60,row10+107,200,20)); text2.string = "Daniel L. Lythgoe"; text2.stringColor = Color.new(1,1,1,0.4); text2.align = \left; }; window.front; window.alwaysOnTop = true; }; displayWindow.value(); }).play(AppClock))