/** * Drag the mouse over the pattern sliders to create sound. * Add or remove sliders via the "steps" control. *

Try setting the number of steps to an interesting metrical unit — say "8" — * and use the pattern as a fractal step sequencer. *

If the UI freezes, try reloading the page. *

If you like the sounds, you can download and run the app locally to save out files. *

Mac OS X version
* Windows version
* Linux version

* Back to Raintone.com *

*

Thanks to Terran Olson's work on audio fractals that inspired this sketch. * Thanks to Krister Olsson's Ess library for the sound support, and Andreas Schlegel's controlP5 library for UI. *

*/ // Fractal Wavetables // March 2009 // jdn (at) raintone.com // // change history: // v1 - basic algorithm and mono saving // v2 - added drag-and-drop import of fw_xxxx.aif files // v3 - code cleanup // v4 - switched to controlP5 library for most of GUI. // refactored code -- FWAudio could become class // v5 - added a, b, stereo a+b and morph a->b modes // // import krister.Ess.*; // nice simple sound library import sojamo.drop.*; import controlP5.*; // holy shit this library rocks /* * Model */ // Important constants final int SR = 44100; final int MAX_SLIDERS = 80; final int LEFT_MARGIN = 100; // fractal data final int NUM_PATTERNS = 2; FloatFract[] fract = new FloatFract[NUM_PATTERNS]; // to support stereo int numPatternsActive = 1; int patternOffset = 1; boolean morphing = false; public boolean update = true; // pattern size / timing data float curDuration = 0; int targetIteration = 0; FWAudio playback; /* * Control */ SDrop drop; // for dropping saved files back into the program to load as "presets" public ControlP5 controlP5; Slider stepsSlider; Slider durationSlider; FWSliderPool[] patternSliders = new FWSliderPool[NUM_PATTERNS]; IterationView[] fractView = new IterationView[NUM_PATTERNS]; ArrayList mouseListeners = new ArrayList(); // create and wire the entire GUI... void setup() { Ess.start(this); playback = new FWAudio(); // drag-and-drop handler for file imports drop = new SDrop(this); size(800, 600); colorMode(HSB, 1); background(0); // setup UI controller controlP5 = new ControlP5(this); // mode buttons int buttonWidth = 77; int buttonHeight = 20; int buttonY = 14; int b = 1; controlP5.addButton("a",1,LEFT_MARGIN,buttonY,buttonWidth,buttonHeight); controlP5.addButton("b",1,LEFT_MARGIN+(b++)*(buttonWidth+10),buttonY,buttonWidth,buttonHeight); controlP5.addButton("stereo",1,LEFT_MARGIN+(b++)*(buttonWidth+10),buttonY,buttonWidth,buttonHeight).setLabel("stereo"); controlP5.addButton("morph",1,LEFT_MARGIN+(b++)*(buttonWidth+10),buttonY,buttonWidth,buttonHeight); controlP5.addButton("swap",1,LEFT_MARGIN+(b++)*(buttonWidth+10),buttonY,buttonWidth,buttonHeight).setLabel("swap a<->b"); // save button -- only if we're running as an application if(!online) { controlP5.addButton("save",1,LEFT_MARGIN+(b++)*(buttonWidth+10),buttonY,buttonWidth,buttonHeight).setLabel("Save Audio"); } // play toggles buttonWidth = 20; buttonHeight = 20; buttonY = 300; b = 0; controlP5.addToggle("mute",false,LEFT_MARGIN+(b++)*(buttonWidth+10),buttonY,buttonWidth,buttonHeight); controlP5.addToggle("looping",true,LEFT_MARGIN+(b++)*(buttonWidth+10),buttonY,buttonWidth,buttonHeight).setLabel("loop"); controlP5.addToggle("update",true,LEFT_MARGIN+(1+b++)*(buttonWidth+10),buttonY,buttonWidth,buttonHeight).setLabel("autoupdate"); // horizontal sliders stepsSlider = controlP5.addSlider("steps",2,MAX_SLIDERS,3,LEFT_MARGIN,245,width-200,10); stepsSlider.setLabel(""); durationSlider = controlP5.addSlider("duration",0.25,30,4.0,LEFT_MARGIN,275,width-200,10); durationSlider.setLabel("seconds"); durationSlider.setDecimalPrecision(2); // labels controlP5.addTextlabel("patternLabel", "pattern", LEFT_MARGIN-43, 121); controlP5.addTextlabel("stepsLabel", "steps", LEFT_MARGIN-33, 246); controlP5.addTextlabel("durationLabel", "duration", LEFT_MARGIN-47, 276); controlP5.addTextlabel("1", "1", (LEFT_MARGIN + width-200)+10, 50); controlP5.addTextlabel("0", "0", (LEFT_MARGIN + width-200)+10, 123); controlP5.addTextlabel("-1", "-1", (LEFT_MARGIN + width-200)+8, 193); // loop through to create the pattern-specific bits of the system // since for stereo and morphs we need multiples of all of this for(int i=0; i < NUM_PATTERNS; i++) { // the fractal model fract[i] = new FloatFract(); // bank of vertical pattern sliders that can work as a "draw" area patternSliders[i] = new FWSliderPool(round(stepsSlider.value()), MAX_SLIDERS, LEFT_MARGIN, 0, width-200, 0); // set initial values for pattern sliders float[] vals = { 1, 0.5, 1 }; if(i == 0) vals[1] = 0.1; for (int j = 0; j < patternSliders[i].size(); j++) patternSliders[i].slider(j).setValue(vals[j]); // fractal iteration viewer fractView[i] = new IterationView(fract[i].getSegments(), 10, 0, 0, width, 0); mouseListeners.add(patternSliders[i]); } stereo(1); } /** * controlP5 event callbacks */ public void a(float val) { numPatternsActive = 1; patternOffset = 0; morphing = false; setSingleView(0); } public void b(float val) { numPatternsActive = 1; patternOffset = 1; morphing = false; setSingleView(1); } public void stereo(float val) { numPatternsActive = 2; patternOffset = 0; morphing = false; setDoubleView(); } public void morph(float val) { numPatternsActive = 2; patternOffset = 0; morphing = true; setDoubleView(); } public void looping(boolean val) { playback.setLooping(val); } public void mute(boolean val) { playback.setMute(val); } public void setSingleView(int num) { patternSliders[num].clear(); patternSliders[1-num].clear(); patternSliders[num].setHeight(160); patternSliders[num].setY(50); patternSliders[num].show(); patternSliders[1-num].hide(); fractView[num].setY(height-30); fractView[num].setHeight(-230); fractView[num].show(); fractView[1-num].hide(); playback.waveDirty = true; updateFractalSettings(); } public void setDoubleView() { for(int p = 0; p < numPatternsActive; p++) { patternSliders[p].clear(); patternSliders[p].setY(50+(p*(10+150/NUM_PATTERNS))); patternSliders[p].setHeight(150/NUM_PATTERNS); patternSliders[p].show(); fractView[p].clear(); fractView[p].setY(height-150); fractView[p].setHeight((p==0?-1:1)*230/NUM_PATTERNS); fractView[p].show(); } playback.waveDirty = true; updateFractalSettings(); } public void swap(float val) { for (int i = 0; i < patternSliders[0].size(); i++) { float tempVal = patternSliders[0].slider(i).value(); patternSliders[0].slider(i).setValue(patternSliders[1].slider(i).value()); patternSliders[1].slider(i).setValue(tempVal); } playback.waveDirty = true; updateFractalSettings(); } public void steps(float val) { stepsSlider.setValueLabel(""+round(val)); checkNumSliders(); } public void duration(float val) { // this will get polled in mouseReleased() } public void save(float val) { doSave(); } /** * applet events */ public void stop() { Ess.stop(); super.stop(); } public void draw() { for(int p = 0; p < numPatternsActive; p++) patternSliders[p+patternOffset].draw(); playback.drawPlayhead(); // process any updates if(!update) return; boolean stillIterating = false; for(int p = 0; p < numPatternsActive; p++) { int pat = p + patternOffset; if(fract[pat].iteration() < targetIteration) { fractView[pat].draw(); fract[pat].iterate(); fractView[pat].setNextIteration(fract[pat].getSegments()); stillIterating = true; } } if (!stillIterating && playback.waveDirty && playback.audioPlaying) { playback.stopAudio(); if (numPatternsActive == 2) { playback.writeStereoAudio(fract[0].getSegments(), fract[1].getSegments()); } else { for(int p = 0; p < numPatternsActive; p++) { int pat = p + patternOffset; playback.writeAudio(fract[pat].getSegments(), p); } } playback.playAudio(); } } void mousePressed() { for(int i = 0; i < mouseListeners.size(); i++) ((MouseListener)mouseListeners.get(i)).mousePressed(); } void mouseReleased() { for(int i = 0; i < mouseListeners.size(); i++) ((MouseListener)mouseListeners.get(i)).mouseReleased(); checkNumSliders(); checkDuration(); updateFractalSettings(); } void dropEvent(DropEvent theDropEvent) { if(theDropEvent.isFile()) { // for further information see // http://java.sun.com/j2se/1.4.2/docs/api/java/io/File.html File myFile = theDropEvent.file(); if(myFile.isFile()) { // attempt to parse filename String fileName = myFile.getName(); doRestore(fileName); } } } /* * Control Functions */ void doSave() { playback.stopAudio(); String defaultFilename = ""; for(int p = 0; p < numPatternsActive; p++) { int pat = p + patternOffset; defaultFilename = "fw_"+fract[pat].toString(); if(morphing) defaultFilename += "->"+fract[1-pat].toString(); if(defaultFilename.length() > 250) { defaultFilename = defaultFilename.substring(0, 250); } defaultFilename += ".aif"; String path = "" + defaultFilename; print("Save path: " + path + "\n"); playback.writeSoundFile(path, pat); } playback.playAudio(); } void doRestore(String fileName) { if(fileName.endsWith(".aif") && fileName.startsWith("fw_")) { // looks OK-ish fileName = fileName.substring(3, fileName.length()-4); // strip pre- and suffix //print("restoring seed:" + fileName); playback.stopAudio(); playback.invalidateAudio(); FloatFract newFract = new FloatFract(fileName); // copy seed onto UI sliders stepsSlider.setValue(newFract.pattern().size()); checkNumSliders(); checkDuration(); for(int i = 0; i < patternSliders[patternOffset].size(); i++) patternSliders[patternOffset].slider(i).setValue(((Double)newFract.pattern().get(i)).floatValue()); updateFractalSettings(); playback.playAudio(); } } void checkNumSliders() { for(int p = 0; p < NUM_PATTERNS; p++) { if(patternSliders[p].size() != round(stepsSlider.value())) { playback.stopAudio(); patternSliders[p].setSize((round(stepsSlider.value()))); Runtime.getRuntime().gc(); } } } void checkDuration() { if(curDuration != durationSlider.value()) { curDuration = durationSlider.value(); if(targetIteration != calculateIterationBounds(0)) playback.waveDirty = true; } } void updateFractalSettings() { boolean needsUpdate = false; boolean waveWasDirty = playback.waveDirty; ArrayList[] newPattern = new ArrayList[numPatternsActive]; for(int p = 0; p < numPatternsActive; p++) { if(!morphing) fract[p+patternOffset].setMorphPattern(null); newPattern[p] = new ArrayList(patternSliders[p+patternOffset].size()); for(int i = 0; i < patternSliders[p+patternOffset].size(); i++) { newPattern[p].add(new Double(patternSliders[p+patternOffset].slider(i).value())); } if(waveWasDirty || (!patternsSame(newPattern[p], fract[p+patternOffset].pattern()))) { needsUpdate = true; } } if(needsUpdate) { targetIteration = calculateIterationBounds(0); for(int p = 0; p < numPatternsActive; p++) { fract[p+patternOffset].setPattern(newPattern[p]); if(morphing) { fract[p+patternOffset].setMorphPattern(newPattern[1-p]); } fractView[p+patternOffset].reset(this); fractView[p+patternOffset].setNextIteration(fract[p+patternOffset].getSegments()); } playback.invalidateAudio(); Runtime.getRuntime().gc(); playback.playAudio(); } } int calculateIterationBounds(int p) { // numSliders^iteration = total samples float targetLength = durationSlider.value(); float numIterations = log(SR*targetLength) / log(patternSliders[p].size()); int targetIteration = ceil(numIterations); // favor longer clips // unless it would be too long if (pow(patternSliders[p].size(), targetIteration) >= SR*targetLength*4) targetIteration--; return targetIteration; }