classdef theremin_ui < matlab.apps.AppBase

    % Properties that correspond to app components
    properties (Access = public)
        UIFigure                    matlab.ui.Figure
        ThereminLabel               matlab.ui.control.Label
        BaseVolumeKnobLabel         matlab.ui.control.Label
        BaseVolumeKnob              matlab.ui.control.Knob
        StartButton                 matlab.ui.control.Button
        Panel                       matlab.ui.container.Panel
        FilenameEditFieldLabel      matlab.ui.control.Label
        FilenameEditField           matlab.ui.control.EditField
        SaveButton                  matlab.ui.control.Button
        MinFrequencyEditFieldLabel  matlab.ui.control.Label
        MinFrequencyEditField       matlab.ui.control.NumericEditField
        TopFrequencyEditFieldLabel  matlab.ui.control.Label
        TopFrequencyEditField       matlab.ui.control.NumericEditField
        ExitButton                  matlab.ui.control.Button
    end

    
    properties (Access = private)
        % everything UI related
        state; % str: 'paused', 'running'
        base_volume; % volume factor by UI
        file_name; % file name for exporting audio
        min_freq;
        max_freq;
        
        % theremine related, taken from theremin.m
        const_frames_per_second;
        const_Fs;
        const_te;
        const_samples_per_frame;
        const_sample_range;
        const_fade_speed;
        signal_f;
        zero_hands;
        one_hand;
        two_hands;
        deviceWriter;
        complete_signal;
    end
    
    methods(Access = private)
        function [sound,offset] = get_theremin_sound_bit(app, x, y, offset, generator)
            volume = y / 1300;
            frequency = max(3, (x + 155) * pi);
            frequency = max(app.min_freq, (frequency - 3)/1500 * app.max_freq);
            base = generator(frequency) + offset;
            offset = base(end);
            sound = sin(base) .* volume * (app.base_volume * 2);
        end
    end

    % Callbacks that handle component events
    methods (Access = private)

        % Code that executes after component creation
        function startupFcn(app)
            app.state = 'paused';
            app.base_volume = 0.5;
            app.file_name = "Theremin.wav";
            app.min_freq = 3;
            app.max_freq = 1500;
            
            app.const_frames_per_second = 100;
            app.const_Fs = 44100;
            app.const_te = 1/app.const_frames_per_second;
            app.const_samples_per_frame = ceil(app.const_Fs * app.const_te);
            app.const_sample_range = 0:app.const_samples_per_frame-1;
            app.const_fade_speed = 0.975;
            app.signal_f = @(freq) (freq ./ app.const_Fs .* 2 .* pi .* app.const_sample_range);
            app.zero_hands = size(NaN(0,0));
            app.one_hand   = size(NaN(1,1));
            app.two_hands  = size(NaN(1,2));
            app.deviceWriter = audioDeviceWriter('SampleRate', app.const_Fs, ...
                'SupportVariableSizeInput', true, 'BufferSize', 3 * app.const_samples_per_frame);
        end

        % Value changed function: BaseVolumeKnob
        function BaseVolumeKnobValueChanged(app, event)
            app.base_volume = app.BaseVolumeKnob.Value;
        end

        % Value changed function: MinFrequencyEditField
        function MinFrequencyEditFieldValueChanged(app, event)
            app.min_freq = app.MinFrequencyEditField.Value;
        end

        % Value changed function: TopFrequencyEditField
        function TopFrequencyEditFieldValueChanged(app, event)
            app.max_freq = app.TopFrequencyEditField.Value;
        end

        % Button pushed function: StartButton
        function StartButtonPushed(app, event)
            if (strcmp(app.state, 'paused'))
                app.state = 'running';
                app.StartButton.Text = 'Pause';

                P = NaN(1000000,3);
                count = 1;
                
                frequency_pos = 0;
                height_pos = 0;
                
                offset = 0;
                
                signal = sin(app.signal_f(0));
                
                app.complete_signal = signal;
                
                gesture_count = 0;

            % main loop
            while strcmp(app.state, "running") && gesture_count < app.const_frames_per_second
                frame = matleap_frame;
                handCount = size(frame.hands);
                
                % slowly decrease volume
                height_pos = height_pos * app.const_fade_speed;
                
                if isequal(app.one_hand, handCount)        
                    %pos = frame.hands(1).palm.position;
                    pos = frame.hands(1).palm.stabilized_position;
                    
                    frequency_pos = pos(1);
                    height_pos = max(150, height_pos);
                    
                    P(count, 1:3) = pos;
                    count = count + 1;
                
                    if frame.gesture > 0
                        gesture_count = gesture_count + 1;
                    else
                        gesture_count = 0;
                    end
                elseif isequal(app.two_hands, handCount)
                    gesture_count = 0;
                    
                    %pos = frame.hands(1).palm.position;
                    pos = frame.hands(1).palm.stabilized_position;
                    
                    frequency_pos = pos(1);
                    %y_one = pos(2);
                    
                    P(count, 1:3) = pos;
                    count = count + 1;
                    
                    %pos = frame.hands(2).palm.position;
                    pos = frame.hands(2).palm.stabilized_position;
            
                    %x_two = pos(1);
                    height_pos = pos(2);
                else
                    % no hands, do nothing (or more than 2 (how?! :D))
                    gesture_count = 0;
                end
                
                % play current sound
                [signal, offset] = get_theremin_sound_bit(app, frequency_pos, height_pos, offset, app.signal_f);
                
                buffer_under_flow = app.deviceWriter(signal(:)); 
                
                %if buffer_under_flow ~= 0
                %    disp("Buffer ran empty!");
                %end
                
                app.complete_signal = [app.complete_signal signal];
            end
            else
                app.state = 'paused';
                app.StartButton.Text = 'Resume';
            end
        end

        % Value changed function: FilenameEditField
        function FilenameEditFieldValueChanged(app, event)
            app.file_name = app.FilenameEditField.Value;
        end

        % Button pushed function: SaveButton
        function SaveButtonPushed(app, event)
            audiowrite(app.file_name, app.complete_signal, app.const_Fs);
        end

        % Button pushed function: ExitButton
        function ExitButtonPushed(app, event)
            closereq();
        end
    end

    % Component initialization
    methods (Access = private)

        % Create UIFigure and components
        function createComponents(app)

            % Create UIFigure and hide until all components are created
            app.UIFigure = uifigure('Visible', 'off');
            app.UIFigure.Position = [100 100 540 341];
            app.UIFigure.Name = 'MATLAB App';

            % Create ThereminLabel
            app.ThereminLabel = uilabel(app.UIFigure);
            app.ThereminLabel.FontSize = 21;
            app.ThereminLabel.FontWeight = 'bold';
            app.ThereminLabel.Position = [224 281 95 32];
            app.ThereminLabel.Text = 'Theremin';

            % Create BaseVolumeKnobLabel
            app.BaseVolumeKnobLabel = uilabel(app.UIFigure);
            app.BaseVolumeKnobLabel.HorizontalAlignment = 'center';
            app.BaseVolumeKnobLabel.Position = [78 124 70 22];
            app.BaseVolumeKnobLabel.Text = 'Base Volume';

            % Create BaseVolumeKnob
            app.BaseVolumeKnob = uiknob(app.UIFigure, 'continuous');
            app.BaseVolumeKnob.Limits = [0 1];
            app.BaseVolumeKnob.ValueChangedFcn = createCallbackFcn(app, @BaseVolumeKnobValueChanged, true);
            app.BaseVolumeKnob.Position = [83 177 60 60];
            app.BaseVolumeKnob.Value = 0.5;

            % Create StartButton
            app.StartButton = uibutton(app.UIFigure, 'push');
            app.StartButton.ButtonPushedFcn = createCallbackFcn(app, @StartButtonPushed, true);
            app.StartButton.Position = [341 184 100 25];
            app.StartButton.Text = 'Start';

            % Create Panel
            app.Panel = uipanel(app.UIFigure);
            app.Panel.Position = [261 39 260 107];

            % Create FilenameEditFieldLabel
            app.FilenameEditFieldLabel = uilabel(app.Panel);
            app.FilenameEditFieldLabel.HorizontalAlignment = 'right';
            app.FilenameEditFieldLabel.Position = [46 61 52 22];
            app.FilenameEditFieldLabel.Text = 'Filename';

            % Create FilenameEditField
            app.FilenameEditField = uieditfield(app.Panel, 'text');
            app.FilenameEditField.ValueChangedFcn = createCallbackFcn(app, @FilenameEditFieldValueChanged, true);
            app.FilenameEditField.Tooltip = {'Save audio to...'};
            app.FilenameEditField.Position = [113 61 100 22];
            app.FilenameEditField.Value = 'Theremin.wav';

            % Create SaveButton
            app.SaveButton = uibutton(app.Panel, 'push');
            app.SaveButton.ButtonPushedFcn = createCallbackFcn(app, @SaveButtonPushed, true);
            app.SaveButton.Position = [80 23 100 25];
            app.SaveButton.Text = 'Save';

            % Create MinFrequencyEditFieldLabel
            app.MinFrequencyEditFieldLabel = uilabel(app.UIFigure);
            app.MinFrequencyEditFieldLabel.HorizontalAlignment = 'right';
            app.MinFrequencyEditFieldLabel.Position = [17 81 78 22];
            app.MinFrequencyEditFieldLabel.Text = 'Min Frequency';

            % Create MinFrequencyEditField
            app.MinFrequencyEditField = uieditfield(app.UIFigure, 'numeric');
            app.MinFrequencyEditField.Limits = [3 100];
            app.MinFrequencyEditField.ValueChangedFcn = createCallbackFcn(app, @MinFrequencyEditFieldValueChanged, true);
            app.MinFrequencyEditField.Position = [110 81 100 22];
            app.MinFrequencyEditField.Value = 3;

            % Create TopFrequencyEditFieldLabel
            app.TopFrequencyEditFieldLabel = uilabel(app.UIFigure);
            app.TopFrequencyEditFieldLabel.HorizontalAlignment = 'right';
            app.TopFrequencyEditFieldLabel.Position = [16 50 79 22];
            app.TopFrequencyEditFieldLabel.Text = 'Top Frequency';

            % Create TopFrequencyEditField
            app.TopFrequencyEditField = uieditfield(app.UIFigure, 'numeric');
            app.TopFrequencyEditField.Limits = [100 2000];
            app.TopFrequencyEditField.ValueChangedFcn = createCallbackFcn(app, @TopFrequencyEditFieldValueChanged, true);
            app.TopFrequencyEditField.Position = [110 50 100 22];
            app.TopFrequencyEditField.Value = 1500;

            % Create ExitButton
            app.ExitButton = uibutton(app.UIFigure, 'push');
            app.ExitButton.ButtonPushedFcn = createCallbackFcn(app, @ExitButtonPushed, true);
            app.ExitButton.Position = [209 3 100 26];
            app.ExitButton.Text = 'Exit';

            % Show the figure after all components are created
            app.UIFigure.Visible = 'on';
        end
    end

    % App creation and deletion
    methods (Access = public)

        % Construct app
        function app = theremin_ui

            runningApp = getRunningApp(app);

            % Check for running singleton app
            if isempty(runningApp)

                % Create UIFigure and components
                createComponents(app)

                % Register the app with App Designer
                registerApp(app, app.UIFigure)

                % Execute the startup function
                runStartupFcn(app, @startupFcn)
            else

                % Focus the running singleton app
                figure(runningApp.UIFigure)

                app = runningApp;
            end

            if nargout == 0
                clear app
            end
        end

        % Code that executes before app deletion
        function delete(app)

            % Delete UIFigure when app is deleted
            delete(app.UIFigure)
        end
    end
end