-- draft default tip
authorhh
Fri, 22 Nov 2019 09:40:16 +0100
changeset 0 16509f98f301
--
android/michelson.studio/build.gradle
android/michelson.studio/lib031-release/build.gradle
android/michelson.studio/local.properties
android/michelson.studio/michelson/build.gradle
android/michelson.studio/michelson/src/main/AndroidManifest.xml
android/michelson.studio/michelson/src/main/java/hh/michelson/A.java
android/michelson.studio/michelson/src/main/java/hh/michelson/C.java
android/michelson.studio/michelson/src/main/java/hh/michelson/GraphView.java
android/michelson.studio/michelson/src/main/java/hh/michelson/Handle.java
android/michelson.studio/michelson/src/main/java/hh/michelson/RotatedLv.java
android/michelson.studio/michelson/src/main/java/hh/michelson/Wave.java
android/michelson.studio/michelson/src/main/java/hh/michelson/WavesFragment.java
android/michelson.studio/michelson/src/main/java/hh/michelson/Windrover.java
android/michelson.studio/michelson/src/main/res/layout/handles_row.xml
android/michelson.studio/michelson/src/main/res/layout/header.xml
android/michelson.studio/michelson/src/main/res/layout/list_view.xml
android/michelson.studio/michelson/src/main/res/layout/main.xml
android/michelson.studio/michelson/src/main/res/layout/wave.xml
android/michelson.studio/michelson/src/main/res/layout/wave_list_row.xml
android/michelson.studio/michelson/src/main/res/layout/wave_params_row.xml
android/michelson.studio/michelson/src/main/res/values/dimens.xml
android/michelson.studio/michelson/src/main/res/values/strings.xml
android/michelson.studio/settings.gradle
python/michelson.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/build.gradle	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,17 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+    repositories {
+        jcenter()
+        google()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0-alpha09'
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+        google()
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/lib031-release/build.gradle	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,2 @@
+configurations.maybeCreate("default")
+artifacts.add("default", file('lib031-release.aar'))
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/local.properties	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,10 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file should *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/home/L/_HH/obsah/TECHNO/Android/sdk
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/build.gradle	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,45 @@
+apply plugin: 'com.android.application'
+
+android {
+	signingConfigs {
+		hh {
+			keyAlias 'androiddebugkey'
+			keyPassword 'android'
+			storeFile file('/home/hh/.android/debug.keystore')
+			storePassword 'android'
+		}
+	}
+	
+	compileSdkVersion 28
+
+	defaultConfig {
+		applicationId 'hh.michelson'
+		minSdkVersion 22
+		targetSdkVersion 26
+		versionCode 1
+		versionName "1.0"
+		testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+		signingConfig signingConfigs.hh
+	}
+	buildTypes {
+		release {
+			signingConfig signingConfigs.hh
+		}
+		debug {
+			debuggable false
+		}
+	}
+	productFlavors {
+	}
+}
+
+dependencies {
+	implementation project(':lib031-release')
+	implementation fileTree(include: ['*.jar'], dir: 'libs')
+    implementation 'com.android.support:support-v4:28.0.0'
+    implementation 'com.android.support:appcompat-v7:28.0.0'
+    implementation 'com.android.support:preference-v7:28.0.0'
+    implementation 'com.android.support:coordinatorlayout:28.0.0'
+    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+    implementation 'com.android.support:design:28.0.0'
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/AndroidManifest.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="hh.michelson"
+    >
+
+    <application
+        android:label="@string/app_name"
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:theme="@android:style/Theme.Holo.Light.NoActionBar"
+        >
+
+        <activity
+            android:name=".A"
+            >
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+
+        </activity>
+
+    </application>
+
+</manifest> 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/java/hh/michelson/A.java	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,462 @@
+package hh.michelson;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.ListView;
+import android.widget.SeekBar;
+
+import hh.lib.D;
+import hh.lib.DA;
+import hh.ui.FlatButton;
+
+public class A extends DA implements WavesFragment.OnFragmentInteractionListener, Handler.Callback {
+    /**
+     * ----------------------------------------------------------
+     * main activity template with uninstall in menu
+     * ----------------------------------------------------------
+     */
+
+    WavesFragment waveList;
+    boolean playing = C.playingDefault;
+    Wave wave = null;
+    double phase = 0;
+    boolean node = true;
+
+	volatile double freq = C.freqDef.Default;
+	volatile double freqProgr = C.freqProgrDef.Default;
+	volatile double targetFreq, deltaFreq, dt;
+	final static int UPDFREQ = 0;
+
+    double amplitude;
+    volatile double amplKoef = 0;
+    double maxY = 0;
+    boolean mute = false;
+
+	double pulseSecs = C.pulseDef.Default;
+	final double pulseStep = (Math.log10(C.pulseDef.Max) - Math.log10(C.pulseDef.Min)) / C.maxBarProgress;    /* in secs */
+    int pulseCycles;     /* pulse in cycles */
+	boolean isInPulse = true;
+
+	double pauseSecs = C.pauseDef.Default;
+	final double pauseStep = (Math.log10(C.pauseDef.Max) - Math.log10(C.pauseDef.Min)) / C.maxBarProgress;    /* in secs */
+    int pauseCycles;     /* pause in cycles */
+    volatile double pulsePausePhase = 0;
+
+	double slope = C.slopeDef.Default;
+    final double slopeStep = (Math.log10(C.slopeDef.Max) - Math.log10(C.slopeDef.Min)) / C.maxBarProgress;
+    double slopeKoef = 1d;
+	final static int SETSLOPE = 1;
+
+	Handle amplBar;
+	Handle pulseBar;
+	Handle pauseBar;
+	Handle slopeBar;
+	Handle freqBar;
+	Handle freqProgressBar;
+	Handle[] handles;
+
+	AudioTrack audioTrack;
+	int buffsize;
+	class Samples {
+		short[] samples;
+		boolean ready;
+
+		Samples(int n) {
+			samples = new short[n];
+		}
+	}
+	Samples[] s;
+	Thread audioThread = null;
+	boolean audioIsActive = true;
+
+	Handler h = new Handler(this);
+
+	class AmplHandle extends Handle {
+
+		AmplHandle(D d, double actual) { super(d, C.amplitudeDef, actual); }
+
+		@Override
+		public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
+			super.onProgressChanged(bar, progress, fromUser);
+			getAmpl();
+		}
+
+		@Override
+		public boolean onLongClick(View v) {
+			final boolean r = super.onLongClick(v);
+			getAmpl();
+			return r;
+		}
+	}
+
+	class PulseHandle extends Handle {
+
+		PulseHandle(D d, double actual) { super(d, C.pulseDef, actual); }
+
+		@Override
+		double progress2value(int progress) { return progress == 0 ? 0d : Math.pow(10, progress * pulseStep - 1); }
+
+		@Override
+		int value2progress(double value) { return value == 0 ? 0 : (int)((Math.log10(value) + 1) / pulseStep); }
+	}
+
+	class PauseHandle extends Handle {
+
+		PauseHandle(D d, double actual) { super(d, C.pauseDef, actual); }
+
+		PauseHandle(D d, String label, double min, double max, double def, double actual, String key) {
+			super(d, label, min, max, def, actual, key);
+		}
+
+		@Override
+		double progress2value(int progress) { return progress == 0 ? 0d : Math.pow(10, progress * pulseStep - 1); }
+
+		@Override
+		int value2progress(double value) { return value == 0 ? 0 : (int)((Math.log10(value) + 1) / pauseStep); }
+	}
+
+	class SlopeHandle extends Handle {
+
+		SlopeHandle(D d, double actual) { super(d, C.slopeDef, actual); }
+
+		SlopeHandle(D d, String label, double min, double max, double def, double actual, String key) {
+			super(d, label, min, max, def, actual, key);
+		}
+
+		@Override
+		double progress2value(int progress) { return progress == 0 ? 0d : Math.pow(10, (progress + 1) * slopeStep); }
+
+		@Override
+		int value2progress(double value) { return value == 0 ? 0 : (int)(Math.log10(value)/slopeStep - 1); }
+	}
+
+	@Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+		setContentView(R.layout.main);
+
+	    Point size = new Point();
+	    ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getSize(size);
+	    C.windowWidth = size.x;
+
+		getValues();
+	    setMenu();
+	    findViewById(R.id.dispWave).setOnClickListener(new View.OnClickListener() {
+		    public void onClick(View v) {
+			    showListUI();
+		    }
+	    });
+		setControlHandles();
+	    findViewById(R.id.mute).setOnClickListener(new View.OnClickListener() {
+		    public void onClick(View v) {
+			    flipMute();
+		    }
+	    });
+	    setMute();
+	    allocAudio();
+
+		if(savedInstanceState != null) playing = savedInstanceState.getBoolean(C.playingKey, C.playingDefault);
+	    if(playing)startAudio();
+	    else showListUI();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) { outState.putBoolean(C.playingKey, playing); }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+	    stopAudio();
+	    audioTrack.release();
+    }
+
+    @Override
+	public boolean handleMessage(Message msg) {
+	    switch(msg.what) {
+		    case UPDFREQ:
+			    updateFreq();
+			    return true;
+		    case SETSLOPE:
+			    setSlopeBar();
+			    return true;
+		    default:
+			    return d.handleMessage(msg);
+	    }
+    }
+
+	@Override
+	public void setNewWave(int position) {
+		if(fm.getBackStackEntryCount() > 0) {
+			fm.popBackStackImmediate();
+			fm.beginTransaction().detach(waveList).commit();
+		}
+		if(position >= 0) {
+			wave = Enum.valueOf(Wave.class, Wave.names[position]);
+			sp.edit().putString(C.waveKey, wave.name()).apply();
+		}
+		playing = true;
+		startAudio();
+	}
+
+	void startAudio() {
+	    wave.init(this);
+		if(audioThread != null) return;
+		audioThread = new Thread() {
+			public void run() {
+	            setPriority(Thread.MAX_PRIORITY);
+				play();
+			}
+		};
+		s[0].ready = false;
+		s[1].ready = false;
+		audioTrack.play();
+		audioThread.start();
+		audioIsActive = true;
+
+		new Thread() { public void run() { synthesize(); } }.start();
+	}
+
+	void stopAudio() {
+		if(audioThread == null) return;
+		audioIsActive = false;
+		try { audioThread.join(); } catch (InterruptedException e) { e.printStackTrace(); }
+		audioThread = null;
+		audioTrack.stop();
+	}
+
+	void getValues() {
+		wave = Enum.valueOf(Wave.class, sp.getString(C.waveKey, C.waveDefault.name()));
+		amplitude = sp.getFloat(C.amplitudeDef.Key, Double.valueOf(C.amplitudeDef.Default).floatValue());
+		mute = sp.getBoolean(C.muteKey, C.muteDefault);
+		pulseSecs = sp.getFloat(C.pulseDef.Key, Double.valueOf(C.pulseDef.Default).floatValue());
+		pauseSecs = sp.getFloat(C.pauseDef.Key, Double.valueOf(C.pauseDef.Default).floatValue());
+		slope = sp.getFloat(C.slopeDef.Key, Double.valueOf(C.slopeDef.Default).floatValue());
+		freq = sp.getFloat(C.freqDef.Key, Double.valueOf(C.freqDef.Default).floatValue());
+		if(freq < C.freqDef.Min) freq = C.freqDef.Min;
+		freqProgr = sp.getFloat(C.freqProgrDef.Key, Double.valueOf(C.freqProgrDef.Default).floatValue());
+	}
+
+    void setControlHandles() {
+	    amplBar = new AmplHandle(d, amplitude);
+        pulseBar = new PulseHandle(d, pulseSecs);
+	    pauseBar = new PauseHandle(d, pauseSecs);
+	    slopeBar = new SlopeHandle(d, slope);
+	    freqBar = new Handle(d, C.freqDef, freq);
+	    freqProgressBar = new Handle(d, C.freqProgrDef, freqProgr);
+
+	    handles = new Handle[] { amplBar, pulseBar, pauseBar, slopeBar, freqBar, freqProgressBar };
+	    ((ListView)findViewById(R.id.handles)).setAdapter(new Windrover(d, "Controls", this, R.layout.handles_row, handles));
+    }
+
+	void showListUI() {
+		stopAudio();
+		playing = false;
+		waveList = new WavesFragment();
+		fm.beginTransaction()
+				.add(R.id.main, waveList)
+				.addToBackStack(null)
+				.commit();
+	}
+
+	void getAmpl() {
+		synchronized(this) {
+			amplitude = amplBar.getActual();
+			setAmplKoef();
+		}
+    }
+
+    void setAmplKoef() {
+        amplKoef = amplitude;
+        if(maxY > 1d) amplKoef = amplKoef / maxY;
+    }
+
+    void setMaxY(double maxY) {
+		this.maxY = maxY;
+		setAmplKoef();
+	}
+
+	void updateFreq() {
+        /* zatím se volá z nitě syntetizátoru (netřeba synchronizovat), který zajišťuje pravidelný rytmus */
+		double nextTargFreq = targetFreq;
+
+		final double barFreq = freqBar.getActual();
+        final double fProgress = freqProgressBar.getActual();
+
+        if(targetFreq != barFreq) nextTargFreq = barFreq;     // změna freqBar má přednost
+        else if(fProgress != 0d) {
+        	nextTargFreq = targetFreq + fProgress;
+            if(nextTargFreq > C.freqDef.Max) nextTargFreq = C.freqDef.Max;
+            if(nextTargFreq < C.freqDef.Min) nextTargFreq = C.freqDef.Min;
+			freqBar.setActual(nextTargFreq);
+        }
+
+		if(nextTargFreq != targetFreq) {
+        	synchronized(this) {
+        		targetFreq = nextTargFreq;
+		        deltaFreq = (targetFreq - freq) / 10;
+	        }
+			if(ll(5)) l(String.format(lo, "freq=%f, targetFreq=%f, deltaFreq=%f",
+					freq, targetFreq, deltaFreq));
+		}
+    }
+
+	void getPulse() {
+		pulseCycles = dur2len(pulseBar.getActual());
+		pauseCycles = dur2len(pauseBar.getActual());
+		Message.obtain(h, SETSLOPE).sendToTarget();
+	}
+
+    void setSlopeBar() {
+	    /* slope can't exceed pulse/2 */
+        int p = (int)Math.floor(pulseCycles /2);
+        if(pulseCycles > 0 && slopeBar.getActual() > p) slopeBar.setActual(p);
+	}
+
+    int dur2len(double duration) { return (int)Math.round(duration * freq); }
+
+    void flipMute() {
+        mute = !mute;
+        setMute();
+    }
+
+	void setMute() {
+		((FlatButton)findViewById(R.id.mute)).setText(mute ? "unmute" : "mute");
+		sp.edit().putBoolean(C.muteKey, mute).apply();
+	}
+
+	void allocAudio() {
+		final int audioSessionId = ((AudioManager)getSystemService(Context.AUDIO_SERVICE)).generateAudioSessionId();
+		buffsize = AudioTrack.getMinBufferSize(
+				C.SR,
+				AudioFormat.CHANNEL_OUT_MONO,
+				AudioFormat.ENCODING_PCM_16BIT);
+		l(4, String.format(lo, "buf size=%d", buffsize));
+		audioTrack = new AudioTrack(
+				new AudioAttributes.Builder()
+						.setUsage(AudioAttributes.USAGE_UNKNOWN)
+						.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
+						.build(),
+				new AudioFormat.Builder()
+						.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+						.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+						.build(),
+				buffsize,
+				AudioTrack.MODE_STREAM,
+				audioSessionId);
+		s = new Samples[] {new Samples(buffsize), new Samples(buffsize)};
+	}
+
+	void play() {
+		while(audioIsActive) {
+			try {
+				playSamples(0);
+				if(audioIsActive) playSamples(1);
+			} catch(InterruptedException e) {}
+		}
+		l(4,"exiting playing thread");
+	}
+
+	void playSamples(int y) throws InterruptedException {
+		synchronized(s[y]) {
+			while(!s[y].ready && audioIsActive) {
+				if(ll(7)) l("waiting for samples " + y);
+				s[y].wait(555);
+			}
+		}
+		if(!audioIsActive) return;
+		if(ll(7)) l(String.format(lo, "playing samples %d", y));
+		int offset = 0, size = buffsize, put;
+		while((put = audioTrack.write(s[y].samples, offset, size)) < size) {
+			offset += put;
+			size -= put;
+		}
+		synchronized(s[y]) {
+			s[y].ready = false;
+			s[y].notify();
+		}
+	}
+
+    void synthesize() {
+	    phase = 0;
+	    freq = targetFreq = freqBar.getActual();
+	    dt = C.twoPI * freq / C.SR;
+	    while(audioIsActive) {
+		    try {
+			    compute(0);
+		        if(audioIsActive) compute(1);
+		    } catch(InterruptedException e) {}
+		    // pravidelný interval pro snímání změny frekvence; samotná změna frekvence s provede postupně, když je průběh vlny v uzlu
+		    Message.obtain(h, UPDFREQ).sendToTarget();
+	    }
+    }
+
+    void compute(int i) throws InterruptedException {
+	    synchronized(s[i]) {
+		    while(s[i].ready && audioIsActive) s[i].wait();
+	    }
+		if(!audioIsActive) return;
+	    computeSamples(i);
+	    synchronized(s[i]) {
+		    s[i].ready = true;
+		    s[i].notify();
+	    }
+    }
+
+	void computeSamples(int i) {
+		for(int j = 0; j < buffsize; j++) {
+			synchronized(this) {    // každý vzorek se synchronizuje zvlášť
+				if(node) adjustInNode();
+				final double k = (mute ? 0 : 1) * slopeKoef * amplKoef;
+
+				s[i].samples[j] = (k != 0 ? (short)(k * wave.value(phase)) : 0);
+
+				phase += dt;
+				if(phase > C.twoPI) {
+					phase -= C.twoPI;
+					node = true;
+				}
+				else node = false;
+
+				if(node) {
+					++pulsePausePhase;
+					if(pulsePausePhase > (pulseCycles + pauseCycles)) pulsePausePhase = 0;
+				}
+			}
+		}
+	}
+
+	void adjustInNode() {
+		getPulse();
+		if(pauseCycles == 0) isInPulse = true;
+		else isInPulse = ((pulseCycles != 0) && (pulsePausePhase <= pulseCycles));
+		if(isInPulse) {
+			slopeKoef = 1d;
+			setSlopeBar();
+			final double slope = slopeBar.getActual();
+			if(slope > 0 && pauseCycles > 0) {
+				if(pulsePausePhase < slope)
+					slopeKoef = (pulsePausePhase + 1d) / (slope + 1d);
+				if(pulsePausePhase > (pulseCycles - 1 - slope))
+					slopeKoef = (pulseCycles - pulsePausePhase) / (slope + 1d);
+			}
+		}
+		else slopeKoef = 0;
+
+		if(freq != targetFreq) {
+			if(Math.abs(freq - targetFreq) < Math.abs(deltaFreq)) freq = targetFreq;
+			else freq += deltaFreq;
+			dt = C.twoPI * freq / C.SR;
+			getPulse();
+		}
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/java/hh/michelson/C.java	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,46 @@
+package hh.michelson;
+
+public class C {
+	/**----------------------------------------------------------
+	* constant vector
+	* ----------------------------------------------------------*/
+
+	static final int		SR = 44100;
+	/*static final double PI = 4. * Math.atan(1.);*/
+	static final double		PI = Math.PI;
+	static final double		twoPI = 2. * Math.PI;
+	static final double		quadPI = Math.pow(Math.PI, 2);
+	static final int		maxBarProgress = 999;
+	static int				windowWidth;
+
+	static final String playingKey = "playing";
+	static final boolean playingDefault = false;
+
+	static final String waveKey = "WAVE";
+	static final Wave waveDefault = Wave.F1;
+
+	static final HandleDef pulseDef = new HandleDef("PULSE", "pulse", 0.1, 3, 0);
+	static final HandleDef pauseDef = new HandleDef("PAUSE", "pause", 0.1, 3, 0);
+	static final HandleDef slopeDef = new HandleDef("SLOPE", "slope", 1, 30, 0);
+	static final HandleDef freqDef = new HandleDef("FREQ", "frequence", 220, 880, 220);
+	static final HandleDef freqProgrDef = new HandleDef("FREQPROGR", "freq progress", -33, 33, 0);
+	static final HandleDef amplitudeDef = new HandleDef("AMPLITUDE", "amplitude", 0, 10000, 5000);
+
+	static final String		muteKey		    = "MUTE";
+	static final boolean	muteDefault		= false;
+}
+
+class HandleDef {
+	String Key;
+	String Label;
+	double Min;
+	double Max;
+	double Default;
+	HandleDef(String Key, String Label, double Min, double Max, double Default) {
+		this.Key = Key;
+		this.Label = Label;
+		this.Min = Min;
+		this.Max = Max;
+		this.Default = Default;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/java/hh/michelson/GraphView.java	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,93 @@
+package hh.michelson;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import hh.lib.D;
+
+public class GraphView extends View {
+	D d;
+	Context context;
+	Paint mPaint;
+	float[] graphValues = new float[] {0f};
+	float[] graphLines = null;
+	int W = 0, H, Y0;
+	final int maxH = 600;
+	double maxY = 0d;
+	double dx;
+	double kY;
+
+	/*GraphView(Context context) {
+		super(context);
+		init(context); }*/
+
+	public GraphView(Context context, AttributeSet a) {
+		super(context, a);
+		init(context); }
+
+	/*GraphView(Context context, AttributeSet a, int defStyleAttr) {
+		super(context, a, defStyleAttr);
+		init(context); }
+
+	GraphView(Context context, AttributeSet a, int defStyleAttr, int defStyleRes) {
+		super(context, a, defStyleAttr, defStyleRes);
+		init(context); }*/
+
+	void init(Context context) {
+		this.d = ((A)context).d.klon(this);
+		this.context = context;
+		mPaint = new Paint();
+		mPaint.setAntiAlias(true);
+		mPaint.setStyle(Paint.Style.FILL);
+		mPaint.setColor(Color.RED);}
+
+	public void onDraw(Canvas canvas) {
+		d.l(4, "graph.onDraw");
+		canvas.drawLine(0, Y0, W, Y0, mPaint);
+		fillGraphArray();
+		if(graphLines != null) canvas.drawLines(graphLines, mPaint);
+	}
+
+	void setGraphValues(float[] graphValues) { this.graphValues = graphValues; }
+
+    void setMaxY(double maxY) { this.maxY = maxY; }
+
+	protected void onMeasure(int w, int h) {
+		W = MeasureSpec.getSize(w);
+		H = MeasureSpec.getSize(h) > maxH ? maxH : MeasureSpec.getSize(h);
+		super.onMeasure(w, h);
+		setMeasuredDimension(W, H);
+		Y0 = H / 2;
+		if(d.ll(4)) d.l(String.format(A.lo, "graph, onMeasure, w=%d, W=%d, h=%d, H=%d, Y0=%d",
+				MeasureSpec.getSize(w), W, MeasureSpec.getSize(h), H, Y0));
+	}
+
+	void fillGraphArray() {
+		if(d.ll(4)) d.l(String.format(A.lo, "fillGraphArray, W=%d, graphValues.length=%d", W, graphValues.length));
+		graphLines = null;
+		if(W > 0 && graphValues != null) {
+			dx = (double)W / graphValues.length;
+			kY = 4.0 * Y0 / (5.0 * maxY);
+			graphLines = new float[4 * graphValues.length];
+			graphLines[0] = 0;
+			graphLines[1] = Y0;
+			if(d.ll(4)) d.l(String.format(A.lo, "W=%d, H=%d, Y0=%d, graphLines.length=%d", W, H, Y0, graphLines.length));
+			int i = 0;
+			for(double v: graphValues) {
+				if(i > 0) {
+					graphLines[i] = graphLines[i-2];
+					graphLines[i+1] = graphLines[i-1];
+				}
+				graphLines[i+2] = graphLines[i] + Double.valueOf(dx).floatValue();
+				graphLines[i+3] = Double.valueOf(Y0 - kY * v).floatValue();
+				i += 4;
+			}
+			graphLines[0] = graphLines[2];
+			graphLines[1] = graphLines[3];
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/java/hh/michelson/Handle.java	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,113 @@
+package hh.michelson;
+
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import hh.lib.D;
+import hh.lib.DA;
+
+class Handle implements SeekBar.OnSeekBarChangeListener, View.OnLongClickListener {
+	static final String labelDefault = "rowViewRes";
+	static final double minDefault = 0f;
+	static final double maxDefault = 100f;
+
+	String key;
+	String label = labelDefault;
+	double min = minDefault;
+	double max = maxDefault;
+	double def = min;
+	volatile double actual = def;
+
+	View handleView;
+	SeekBar bar;
+	TextView actualDisp;
+	TextView labelView;
+	int listPosition = -1;
+
+	D d;
+
+	Handle(D d, HandleDef def, double actual) { this(d, def.Label, def.Min, def.Max, def.Default, actual, def.Key); }
+
+	Handle(D d, String label, double min, double max, double def, double actual, String key) {
+		this.d = d.klon(this);
+		this.label = label;
+		this.min = min;
+		this.max = max;
+		this.def = def;
+		this.actual = actual;
+		this.key = key;
+	}
+
+	/* listener implementation */
+	@Override
+	public void onStopTrackingTouch(SeekBar bar) { saveActual(); }
+
+	@Override
+	public void onStartTrackingTouch(SeekBar bar) { }
+
+	@Override
+	public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
+		if(fromUser) { actual = progress2value(bar.getProgress()); }
+		dispActual();
+	}
+
+	@Override
+	public boolean onLongClick(View v) {
+		reset();
+		return true;
+	}
+
+	void dispActual() {
+		actualDisp.setText(String.format(A.lo, "%.2f", actual));
+		actualDisp.invalidate();
+	}
+
+	double getActual() { return actual; }
+
+	int getActualInt() { return (int)Math.round(actual); }
+
+	void adjustBar() {
+//		d.l(String.format("+++ adjustBar, key=%s, actual=%f, bar=%d", key, actual, bar.hashCode()));
+		bar.setProgress(value2progress(actual));
+		bar.invalidate();
+	}
+
+	void setActual(double actual) {
+		this.actual = actual;
+		if(isInflated()) adjustBar();
+		saveActual();
+	}
+
+	void reset() { setActual(def); }
+
+	void saveActual() {
+		DA.sp.edit().putFloat(key, Double.valueOf(actual).floatValue()).apply(); }
+
+	boolean isInflated() {
+		return (handleView != null && handleView.getId() == listPosition); }
+
+	double progress2value(int progress) { return min + (max - min) * progress / C.maxBarProgress; }
+
+	int value2progress(double value) { return (int)Math.round(C.maxBarProgress * (value - min) / (max - min)); }
+
+	void infillView(View handleView, int listPosition) {
+		this.listPosition = listPosition;
+		this.handleView = handleView;
+		handleView.setId(listPosition);
+		labelView = ((TextView)handleView.findViewById(R.id.label));
+		labelView.setText(label);
+		labelView.setOnLongClickListener(this);
+		final TextView tmin = (TextView)handleView.findViewById(R.id.min);
+		if(tmin != null) tmin.setText(String.format(A.lo, "%.1f", min));
+		final TextView tmax = (TextView)handleView.findViewById(R.id.max);
+		if(tmax != null) tmax.setText(String.format(A.lo, "%.1f", max));
+		bar = (SeekBar)handleView.findViewById(R.id.bar);
+		bar.setMax(C.maxBarProgress);
+		adjustBar();
+		actualDisp = handleView.findViewById(R.id.actual);
+		dispActual();
+		bar.setOnSeekBarChangeListener(this);
+		actualDisp.setOnLongClickListener(this);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/java/hh/michelson/RotatedLv.java	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,31 @@
+package hh.michelson;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.widget.ListView;
+
+import java.util.Locale;
+
+import hh.lib.D;
+
+public class RotatedLv extends ListView {
+    static String tag;
+    D d;
+
+    public RotatedLv(Context c) { this(c, null); }
+
+    public RotatedLv(Context c, AttributeSet a) {
+        super(c, a);
+        d = new D(c, getClass().getSimpleName() + "." + getId());
+    }
+
+    protected void onMeasure(int w, int h) {
+        super.onMeasure(w, h);
+	    Resources r = d.c.getResources();
+	    int W = r.getDimensionPixelSize(R.dimen.graph_width);
+	    int H = C.windowWidth - r.getDimensionPixelSize(R.dimen.graph_heigth);
+        setMeasuredDimension(W, H);
+        d.l(4, String.format(Locale.getDefault(), "onMeasure, adjusted to width=%d, heigth=%d", W, H));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/java/hh/michelson/Wave.java	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,296 @@
+package hh.michelson;
+
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import hh.lib.D;
+import hh.lib.DA;
+
+enum Wave {
+	SINE {
+		@Override
+		double value(double x) {
+			return Math.sin(x);
+		}
+	},
+	SAW {
+		@Override
+		double value(double x) {
+			return x / C.PI - 1;
+		}
+	},
+	TRIAN {
+		@Override
+		double value(double x) {
+			return (x < C.PI ? 2*x/ C.PI - 1 : 2*(1 - x/ C.PI) + 1);
+		}
+	},
+	SQUARE {
+		@Override
+		double value(double x) {
+			return x < C.PI ? 1 : -1;
+		}
+	},
+	F1 {
+		final HandleDef[] F1Def = new HandleDef[] {
+				new HandleDef(name() + "K00", "N", 1, 30, 3),
+				new HandleDef(name() + "K01", "K", 0, 2, 1)
+		};
+
+		@Override
+		public String toString() {
+			return "SUM(N) sin(N * x) / pow(N, K)";
+		}
+
+		@Override
+		void setHandleDef() { paramsDef = F1Def; }
+
+		@Override
+		void adjustParams() {
+			n = (int)Math.round(params[0]);
+			k1 = new double[n];
+			for(int i = 1; i < n; i += 2) k1[i] = Math.pow(i, params[1]);
+		}
+
+		@Override
+		double value(double x) {
+			double y = 0;
+			for(int i = 1; i < n; i += 2) y += Math.sin(i * x) / k1[i];
+			return y;
+		}
+	},
+	F2 {
+		@Override public String toString () { return "sin(x)+cos(x) + (sin(2x)+cos(2x))/2 + (sin(4x)+cos(4x))/4"; }
+
+		@Override
+		double value(double x) {
+			return (Math.sin(x) + Math.cos(x) + 0.5 * Math.sin(2*x) + 0.5 * Math.cos(2*x) +
+					0.25 * Math.sin(4*x) + 0.25 * Math.cos(4*x));
+		}
+	},
+	F3 {
+		@Override
+		public String toString () { return "cos(ln(x))"; }
+
+		@Override
+		double value(double x) { return Math.cos(Math.log(x)); }
+	},
+	F4 {
+		@Override public String toString () { return "cos(x^3))"; }
+
+		@Override
+		double value(double x) { return Math.cos(Math.pow(x, 3)); }
+	},
+	F5 {
+		@Override public String toString () { return "cos(sin(x)))"; }
+
+		@Override
+		double value(double x) { return Math.cos(Math.sin(x)); }
+	},
+	F6 {
+		@Override public String toString () { return "sin(cos(x)))"; }
+
+		@Override
+		double value(double x) { return Math.sin(Math.cos(x)); }
+	},
+	F7 {
+		@Override public String toString () { return "sin(sin(x)))"; }
+
+		@Override
+		double value(double x) { return Math.sin(Math.sin(x)); }
+	},
+	F8 {
+		@Override public String toString () { return "cos(cos(x)))"; }
+
+		@Override
+		double value(double x) { return Math.cos(Math.cos(x)); }
+	},
+	F9 {
+		@Override public String toString () { return "ln(sin(x))"; }
+
+		@Override
+		double value(double x) { return Math.log(Math.sin(x)); }
+	},
+	F10 {
+		@Override public String toString () { return "tan(sin(x)+cos(x))"; }
+
+		@Override
+		double value(double x) { return Math.tan(Math.sin(x) + Math.cos(x)); }
+	},
+	MICHELSON {
+		final int numOfWaves = 20;
+
+		@Override public String toString () { return "SUMn (Kn * sin(n*x))"; }
+
+		@Override
+		void setHandleDef() {
+			paramsDef = new HandleDef[numOfWaves];
+			for(int i = 0; i<paramsDef.length; i++) paramsDef[i] = new HandleDef(
+					String.format("%sK%02d", Wave.MICHELSON.name(), i), String.format("%02d", i+1), -10, 10, 0);
+		}
+
+		@Override
+		double value(double x) {
+			double y = 0;
+			for (int i = 0; i < params.length; i++) y += params[i] * Math.sin((i+1) * x);
+			return y;
+		}
+	};
+
+	static String[] names, titles;
+
+	static {
+		final List<String> t = new ArrayList<>();
+		final List<String> n = new ArrayList<>();
+		for (Wave w : Wave.values()) { t.add(w.toString()); n.add(w.name()); }
+		titles = new String[t.size()];
+		t.toArray(titles);
+		names = new String[n.size()];
+		n.toArray(names);
+	}
+
+	class ParamHandle extends Handle implements SeekBar.OnSeekBarChangeListener {
+
+		ParamHandle(D d, int i) { super(d, paramsDef[i], params[i]); }
+
+		@Override
+		public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
+			super.onProgressChanged(bar, progress, fromUser);
+			if(fromUser) {
+				synchronized(context) {
+					setParam(listPosition, actual);
+					adjust();
+				}
+				graphView.invalidate();
+			}
+		}
+
+		@Override
+		public boolean onLongClick(View v) {
+			final boolean r = super.onLongClick(v);
+			synchronized(context) {
+				setParam(listPosition, actual);
+				adjust();
+			}
+			graphView.invalidate();
+			return r;
+		}
+
+		@Override
+		void infillView(View handleView, int listPosition) {
+			super.infillView(handleView, listPosition);
+			labelView.setOnLongClickListener(this);
+			bar.setOnSeekBarChangeListener(this);
+		}
+	}
+
+	String tag = getClass().getName();
+
+	D d;
+	A context;
+	int n;
+	double k1[];
+	double[] params = null;
+	HandleDef[] paramsDef = null;
+	ParamHandle[] paramHandles;
+	RotatedLv paramView;
+
+	GraphView graphView;
+	float[] graphValues;
+	double maxY = 0d;
+
+	double value(double x) {
+		return 0.0d;
+	}
+
+	boolean hasParams() { return params != null; }
+
+	void init(A context) {
+		this.context = context;
+		this.d = context.d.klon(this);
+		((TextView)context.findViewById(R.id.dispWave)).setText(String.format("%s", this));
+		paramView = context.findViewById(R.id.params);
+		graphView = context.findViewById(R.id.graph);
+		graphValues = new float[(int)Math.ceil(A.rs.getDimension(R.dimen.graph_width))];
+        /*graphValues = new double[1 + (int)Math.ceil(2 * D.SR / freq)];*/
+		graphView.setGraphValues(graphValues);
+		initParams();
+		adjust();
+		setParamsView();
+		graphView.invalidate();
+	}
+
+	void adjust() {
+		adjustParams();
+		computeWaveGraph();
+	}
+
+	void setHandleDef() {}
+
+	void initParams() {
+		if(params == null) {
+			setHandleDef();
+			if(paramsDef != null) {
+				params = new double[paramsDef.length];
+				for(int i = 0; i < params.length; i++)
+					params[i] = DA.sp.getFloat(paramsDef[i].Key, Double.valueOf(paramsDef[i].Default).floatValue());
+			}
+		}
+	}
+
+	void adjustParams() {}  // předzpracování parametrů vlny po změně nastavitelného parametru
+
+	void setParam(int i, double value) { params[i] = value; }
+
+	void resetParams() {
+		if(hasParams()) {
+			for(int i=0; i < params.length; i++) paramHandles[i].reset();
+			synchronized(context) {
+				for(int i = 0; i < params.length; i++) setParam(i, paramsDef[i].Default);
+				adjust();
+			}
+			graphView.invalidate();
+		}
+	}
+
+	void setParamsView() {
+		final View reset = context.findViewById(R.id.reset);
+		if(hasParams()) {
+			reset.setVisibility(View.VISIBLE);
+			reset.setOnClickListener(new View.OnClickListener() {
+				public void onClick(View v) {
+					resetParams();
+				}
+			});
+			setParamHandles();
+			paramView.setAdapter(new Windrover(d, "Params", context, R.layout.wave_params_row, paramHandles));
+		}
+		else {
+			reset.setVisibility(View.GONE);
+			paramView.setAdapter(null);
+		}
+	}
+
+	void setParamHandles() {
+		paramHandles = new ParamHandle[params.length];
+		for(int i = 0; i < params.length; i++) { paramHandles[i] = new ParamHandle(d, i); }
+	}
+
+	void computeWaveGraph() {
+		final double  dt = 2 * C.twoPI / graphValues.length;       /*dt = D.twoPI * freq / D.SR,*/
+		double  t = 0d;
+		maxY = 0d;
+		for (int i=0; i<graphValues.length; i++) {
+			final double v = value(t % C.twoPI);
+			if(Math.abs(v) > maxY) maxY = Math.abs(v);
+			graphValues[i] = Double.valueOf(v).floatValue();
+			t += dt;
+		}
+		graphView.setMaxY(maxY);
+		context.setMaxY(maxY);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/java/hh/michelson/WavesFragment.java	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,45 @@
+package hh.michelson;
+
+import android.app.Activity;
+import android.app.ListFragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+public class WavesFragment extends ListFragment {
+	OnFragmentInteractionListener mListener;
+	boolean selected = false;
+
+	@Override
+	public void onAttach(Activity context) {
+		super.onAttach(context);
+		setListAdapter(new ArrayAdapter<>(getActivity(), R.layout.wave_list_row, Wave.titles));
+		mListener = (OnFragmentInteractionListener)context;
+		setRetainInstance(true);    // zachovat fragment při otáčení obrazovky
+	}
+
+	@Override
+	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+		return inflater.inflate(R.layout.list_view, container, false);
+	}
+
+	@Override
+	public void onListItemClick(ListView l, View v, int position, long id) {
+		mListener.setNewWave(position);
+		selected = true;
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		if(!selected) mListener.setNewWave(-1);
+		mListener = null;
+	}
+
+	public static interface OnFragmentInteractionListener {
+		public void setNewWave(int position);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/java/hh/michelson/Windrover.java	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,45 @@
+package hh.michelson;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import hh.lib.D;
+
+class Windrover extends BaseAdapter implements ListAdapter {
+	D d;
+	Context c;
+	int rowViewRes;
+	Handle[] handles;
+
+	Windrover(D d, String label, Context c, int rowViewRes, Handle[] handles) {
+		this.d = d.klon(getClass().getSimpleName() + "." + label);
+		this.c = c;
+		this.rowViewRes = rowViewRes;
+		this.handles = handles;
+	}
+
+	public Object getItem(int pos) {
+		if(d.ll(4)) d.l(String.format("getItem=%s", handles[pos]));
+		return handles[pos];
+	}
+
+	public long getItemId(int pos) {
+		if(d.ll(4)) d.l(String.format(A.lo, "getItemId=%d", pos));
+		return pos;
+	}
+
+	public int getCount() {
+		if(d.ll(4)) d.l(String.format(A.lo, "getCount=%d", handles.length));
+		return handles.length;
+	}
+
+	public View getView(int pos, View handleView, ViewGroup parent) {
+		View v = ((LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(rowViewRes, null);
+		handles[pos].infillView(v, pos);
+		return v;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/res/layout/handles_row.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+	>
+
+    <TextView
+        android:id="@+id/min"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+		android:layout_alignParentTop="true"
+		android:layout_alignParentLeft="true"
+	    />
+
+	<RelativeLayout
+	        android:layout_width="wrap_content"
+	        android:layout_height="wrap_content"
+			android:layout_alignParentTop="true"
+	        android:layout_centerHorizontal="true"
+	        >
+
+		<TextView
+			android:id="@+id/label"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignParentTop="true"
+			android:layout_alignParentLeft="true"
+			android:layout_marginRight="5dip"
+			android:textStyle="bold"
+			/>
+
+	    <TextView
+	        android:id="@+id/actual"
+	        android:layout_width="wrap_content"
+	        android:layout_height="wrap_content"
+			android:layout_alignParentTop="true"
+	        android:layout_toRightOf="@+id/label"/>
+
+	</RelativeLayout>
+
+    <TextView
+	    android:id="@+id/max"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+		android:layout_alignParentTop="true"
+		android:layout_alignParentRight="true"
+	    />
+
+    <SeekBar
+        android:id="@+id/bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+	    android:layout_below="@+id/min"
+        android:longClickable="true"
+        android:focusableInTouchMode="false"
+	    />
+
+</RelativeLayout>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/res/layout/header.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	>
+
+	<include
+		android:id="@+id/menu"
+		layout="@layout/menu"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		/>
+
+	<TextView
+		android:id="@+id/dispWave"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_toRightOf="@+id/menu"
+		android:layout_toLeftOf="@id/reset"
+		android:gravity="center"
+		android:paddingEnd="6dp"
+		android:singleLine="true"
+		android:ellipsize="middle"
+		/>
+
+	<hh.ui.FlatButton
+		android:id="@+id/reset"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_toLeftOf="@id/mute"
+		android:layout_centerVertical="true"
+		android:paddingEnd="5dip"
+		android:textSize="11sp"
+		android:text="reset"
+		/>
+
+	<hh.ui.FlatButton
+		android:id="@+id/mute"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_alignParentEnd="true"
+		android:layout_centerVertical="true"
+		android:paddingEnd="5dip"
+		android:textSize="11sp"
+		android:text="mute"
+		/>
+
+	<TextView
+		android:id="@+id/tag"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_alignParentLeft="true"
+		android:layout_below="@+id/menu"
+		android:singleLine="true"
+		android:text="@string/app_name"
+		/>
+
+</RelativeLayout>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/res/layout/list_view.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ListView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@id/android:list"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:textAlignment="gravity"
+    android:background="@android:color/white"
+    />
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/res/layout/main.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	android:id="@+id/main"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	android:layout_marginLeft="4dp"
+	>
+
+	<include
+		android:id="@+id/header"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_alignParentTop="true"
+		layout="@layout/header"
+		/>
+
+	<include
+		android:id="@+id/wave"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_below="@id/header"
+		layout="@layout/wave"
+		/>
+
+	<Space
+		android:id="@+id/place_holder"
+		android:visibility="invisible"
+
+		android:layout_width="@dimen/graph_width"
+		android:layout_height="@dimen/graph_heigth"
+		android:layout_below="@id/header"
+		android:layout_alignParentStart="true"
+		/>
+
+	<ListView
+		android:id="@+id/handles"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		android:layout_below="@id/place_holder"
+		android:layout_above="@+id/hint"
+		android:layout_alignParentStart="true"
+		android:layout_marginRight="4dp"
+		android:dividerHeight="0dp"
+		android:divider="@android:color/transparent"
+		/>
+
+	<TextView
+		android:id="@+id/hint"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_alignParentBottom="true"
+		android:text="long click on bar label to reset"
+		android:textStyle="italic"
+		/>
+
+</RelativeLayout>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/res/layout/wave.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/transparent"
+	>
+
+	<!--wave graph-->
+	<hh.michelson.GraphView
+		android:id="@+id/graph"
+		android:layout_width="@dimen/graph_width"
+		android:layout_height="@dimen/graph_heigth"
+		/>
+
+	<!--wave parameters as rotaded list view-->
+	<hh.michelson.RotatedLv
+		android:id="@+id/params"
+
+		android:layout_width="@dimen/graph_heigth"
+		android:layout_height="wrap_content"
+		android:layout_alignParentTop="true"
+		android:layout_alignEnd="@id/graph"
+
+		android:transformPivotX="@dimen/graph_heigth"
+		android:transformPivotY="0px"
+		android:rotation="270"
+        android:divider="@android:color/transparent"
+		android:dividerHeight="0dp"
+		/>
+
+</RelativeLayout>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/res/layout/wave_list_row.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/wave_entry"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center_vertical|center_horizontal|fill_vertical"
+    android:paddingTop="10sp"
+    android:paddingBottom="10sp"
+    android:minHeight="30sp"
+    android:singleLine="false">
+
+</TextView>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/res/layout/wave_params_row.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="@dimen/row_heigth"
+	>
+
+	<Space
+		android:id="@+id/place_holder"
+		android:visibility="invisible"
+
+		android:layout_width="@dimen/value_box_heigth"
+		android:layout_height="@dimen/row_heigth"
+		android:layout_alignParentTop="true"
+		android:layout_alignParentLeft="true"
+		/>
+
+	<SeekBar
+		android:id="@+id/bar"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_toRightOf="@+id/place_holder"
+		android:layout_toLeftOf="@+id/label"
+		android:background="@android:color/transparent"
+		/>
+
+	<TextView
+		android:id="@+id/actual"
+
+		android:layout_width="@dimen/row_heigth"
+		android:layout_height="wrap_content"
+		android:layout_alignLeft="@+id/bar"
+		android:layout_alignTop="@+id/bar"
+
+		android:transformPivotX="0px"
+		android:transformPivotY="0px"
+		android:rotation="90"
+
+		android:clickable="true"
+		android:gravity="center_horizontal|top"
+		android:textSize="@dimen/value_font_heigth"
+		android:background="@android:color/transparent"
+		/>
+
+	<TextView
+		android:id="@+id/label"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_alignParentRight="true"
+		android:layout_centerVertical="true"
+		android:rotation="90"
+		android:background="@android:color/transparent"
+		/>
+
+</RelativeLayout>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/res/values/dimens.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<dimen name="graph_heigth">320px</dimen>
+	<dimen name="graph_width">320px</dimen>
+	<dimen name="row_heigth">60px</dimen>
+	<dimen name="value_font_heigth">15px</dimen>
+	<dimen name="value_box_heigth">20px</dimen>
+</resources>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/michelson/src/main/res/values/strings.xml	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">.michelson.17</string>
+</resources>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/android/michelson.studio/settings.gradle	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,1 @@
+include ':michelson', ':lib031-release'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/python/michelson.py	Fri Nov 22 09:40:16 2019 +0100
@@ -0,0 +1,103 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+import sys, math
+from PyQt5.QtWidgets import QWidget, QApplication, QSlider, QVBoxLayout, QPushButton
+from PyQt5.QtGui import QPainter, QPen, QStaticText
+from PyQt5.QtCore import Qt
+
+pos = (300, 300)
+size = 300
+margin = 10
+
+class Michelson(QWidget):
+		
+	def __init__(self):
+		super().__init__()
+		
+		self.setGeometry(pos[0], pos[1], size + 20 * 40 + 0, size)
+		self.setWindowTitle('Michelson')
+		
+		self.s = []
+		for i in range(0, 20):
+			self.s.append(SliderK(self, i))
+			self.s[i].setGeometry(size + i*40, margin, 40, size - 2*margin)
+		
+		self.show()
+		
+	def paintEvent(self, e):
+		qp = QPainter()
+		qp.begin(self)
+		self.drawGraph(qp)
+		qp.end()
+		
+	def drawGraph(self, qp):
+		vmax, v = self.values()
+		y0 = (size - 2*margin) / 2 + margin
+		yk = (y0 - margin) / vmax if vmax > 0 else 0
+		qp.setPen(QPen(Qt.black, 1, Qt.SolidLine))
+		qp.drawLine(0, y0, size, y0)
+		qp.setPen(QPen(Qt.green, 2, Qt.SolidLine))
+		for i in range(0, len(v) - 1):
+			qp.drawLine(margin + i, y0 - yk * v[i], margin + i + 1, y0 - yk * v[i+1])
+		
+	def values(self):
+		vmax = 0
+		v = []
+		x = 0
+		dx = 2 * math.pi / (size - 2 * margin)
+		while x <= 2 * math.pi:
+			y = 0
+			for i in range(0, 20):
+				y = y + self.s[i].v * math.sin((i + 1) * x)
+# 			y = math.sqrt(math.pi * math.pi - (x-math.pi) * (x-math.pi))
+			if math.fabs(y) > vmax: vmax = math.fabs(y)
+			v.append(y)
+			x = x + dx
+		return (vmax, v)
+	
+	
+class SliderK(QWidget):
+		
+	def __init__(self, w, i):
+		super().__init__(w)
+		
+		self.w = w
+		self.i = i
+		self.init = 0
+		self.v = 0
+		
+		label = QPushButton(str(i+1))
+		
+		self.val = QPushButton(str(self.init))
+		self.val.clicked.connect(self.resetK)
+		
+		self.sld = QSlider(Qt.Vertical, self.w)
+		self.sld.setFocusPolicy(Qt.NoFocus)
+		self.sld.setMaximum(20)
+		self.sld.setMinimum(-20)		
+		self.sld.setValue(self.init)
+		self.sld.valueChanged[int].connect(self.changeK)
+		
+		vbox = QVBoxLayout()
+		
+		vbox.addWidget(label)
+		vbox.addWidget(self.val)
+		vbox.addWidget(self.sld)
+		
+		self.setLayout(vbox)
+	
+	def changeK(self, v):
+# 		print("changeK({})={}".format(self.i, v))
+		self.v = v
+		self.val.setText(str(v))
+		self.w.update()
+		
+	def resetK(self):
+		self.sld.setValue(self.init)
+	
+			
+if __name__ == '__main__':	
+	app = QApplication(sys.argv)
+	widget = Michelson()
+	sys.exit(app.exec_())