https://bitbucket.org/daniel_fort/magic-lantern
Raw File
Tip revision: 555b6fca702731271fb4866a612fca3b662b7c00 authored by Daniel Fort on 14 November 2017, 05:06:31 UTC
Looks like this might be the one that's off.
Tip revision: 555b6fc
ettr.c
/**
 * Auto ETTR (Expose To The Right).
 * 
 * Optimize the exposure for raw shooting (photo + video).
 */

#include <module.h>
#include <dryos.h>
#include <property.h>
#include <bmp.h>
#include <menu.h>
#include <config.h>
#include <raw.h>
#include <lens.h>
#include <math.h>
#include <zebra.h>
#include <shoot.h>
#include <fps.h>
#include <focus.h>
#include <beep.h>
#include <histogram.h>
#include <console.h>

/* interface with dual ISO */
#include "../dual_iso/dual_iso.h" 

static CONFIG_INT("auto.ettr", auto_ettr, 0);
static CONFIG_INT("auto.ettr.trigger", auto_ettr_trigger, 2);
static CONFIG_INT("auto.ettr.ignore", auto_ettr_ignore, 1);
static CONFIG_INT("auto.ettr.level", auto_ettr_target_level, -1);
static CONFIG_INT("auto.ettr.max.tv", auto_ettr_max_shutter, 88);
static CONFIG_INT("auto.ettr.clip", auto_ettr_clip, 0);
static CONFIG_INT("auto.ettr.mode", auto_ettr_adjust_mode, 0);
static CONFIG_INT("auto.ettr.midtone.snr", auto_ettr_midtone_snr_limit, 6);
static CONFIG_INT("auto.ettr.shadow.snr", auto_ettr_shadow_snr_limit, 2);
static CONFIG_INT("auto.ettr.dual.iso", auto_ettr_dual_iso_link, 1);
static CONFIG_INT("auto.ettr.allow.beeps", auto_ettr_allow_beeps, 1);

static int debug_info = 0;
static int show_metered_areas = 0;

#define AUTO_ETTR_TRIGGER_ALWAYS_ON (auto_ettr_trigger == 0 || is_intervalometer_running())
#define AUTO_ETTR_TRIGGER_AUTO_SNAP (auto_ettr_trigger == 1)
#define AUTO_ETTR_TRIGGER_PHOTO (AUTO_ETTR_TRIGGER_ALWAYS_ON || AUTO_ETTR_TRIGGER_AUTO_SNAP)
#define AUTO_ETTR_TRIGGER_BY_SET (auto_ettr_trigger == 2)
#define AUTO_ETTR_TRIGGER_BY_HALFSHUTTER_DBLCLICK (auto_ettr_trigger == 3)

#define IS_EOS_M (camera_model_id == MODEL_EOS_M)

/* status codes */
#define ETTR_EXPO_PRECOND_TIMEOUT -2
#define ETTR_EXPO_LIMITS_REACHED -1
#define ETTR_NEED_MORE_SHOTS 0
#define ETTR_SETTLED 1

/** Some cameras do not have raw liveview **/
extern WEAK_FUNC(ret_0) void raw_lv_request();
extern WEAK_FUNC(ret_0) void raw_lv_release();
extern WEAK_FUNC(ret_0) int  raw_lv_is_enabled();

/* optional beeps */
static void ettr_beep()
{
    if (auto_ettr_allow_beeps)
    {
        beep();
    }
}

static void ettr_beep_times(int n)
{
    if (auto_ettr_allow_beeps)
    {
        beep_times(n);
    }
}

static int ettr_get_current_long_exposure_time()
{
    int seconds = menu_get_value_from_script("Bulb Timer", "Exposure duration");
    return seconds;
}

static int ettr_get_current_raw_shutter()
{
    if (is_bulb_mode())
    {
        return shutterf_to_raw(ettr_get_current_long_exposure_time());
    }
    else
    {
        return lens_info.raw_shutter;
    }
}

static int auto_ettr_get_long_exposure_time(int raw_shutter)
{
    /* full-stops will be rounded to minutes, otherwise we get funny times like 91 seconds */
    int seconds = (int)roundf(30.0 * powf(2.0, (16.0 - raw_shutter)/8.0));
    
    /* things like 21 or 19 get rounded */
    int s = (seconds % 60) % 10;
    if (s == 1 || s == 3) {
        seconds--;
    } else if (s == 7 || s == 9) {
        seconds++;
    }
    
    return seconds;
}

static const char * ettr_format_shutter(int raw_shutter)
{
    if (raw_shutter >= SHUTTER_30s)
    {
        return lens_format_shutter(raw_shutter);
    }
    else
    {
        int seconds = auto_ettr_get_long_exposure_time(raw_shutter);
        return format_time_hours_minutes_seconds(seconds);
    }
}


static char* get_current_exposure_settings()
{
    static char msg[50];
    int iso1 = lens_info.iso_analog_raw;
    snprintf(msg, sizeof(msg), "ISO %d", raw2iso(iso1));
    int iso2 = dual_iso_get_recovery_iso();
    if (iso2 && iso2 != iso1)
    {
        STR_APPEND(msg, "/%d", raw2iso(iso2));
    }
    
    if (is_bulb_mode())
    {
        /* note: using ettr_format_shutter here introduces roundoff errors of a few seconds */
        int seconds = ettr_get_current_long_exposure_time();
        STR_APPEND(msg, " %s", format_time_hours_minutes_seconds(seconds));
    }
    else
    {
        STR_APPEND(msg, " %s", lens_format_shutter(lens_info.raw_shutter));
    }
    return msg;
}

static int extra_snr_needed = 0;

/* metering on dual ISO images can be affected by black level delta */
/* ideally, ev_hi = ev_lo + ev_delta, so we'll try to find a black level delta that matches this */
/* => solve this: raw_to_ev(raw_value_hi - black_delta) = raw_to_ev(raw_value_lo + black_delta) + ev_delta */
static int guess_black_delta(int raw_value_lo, int raw_value_hi, float ev_delta)
{
    float best_err = 100000;
    int best_black_delta = 0;
    for (int black_delta = -40; black_delta <= 40; black_delta++)
    {
        float err = ABS(raw_to_ev(raw_value_hi - black_delta) - raw_to_ev(raw_value_lo + black_delta) - ev_delta);
        if (err < best_err)
        {
            best_err = err;
            best_black_delta = black_delta;
        }
    }
    return best_black_delta;
}

/* also used for display on histogram */
static int auto_ettr_get_correction()
{
    static int last_value = INT_MIN;
    
    /* this is kinda slow, don't run it very often */
    static int aux = INT_MIN;
    if (!lv && !should_run_polling_action(100, &aux) && last_value != INT_MIN)
        return last_value;
    
    int gray_proj = 
        auto_ettr_clip == 0 ? GRAY_PROJECTION_MAX_RGB :
        auto_ettr_clip == 1 ? GRAY_PROJECTION_MAX_RB :
        auto_ettr_clip == 2 ? GRAY_PROJECTION_MEDIAN_RGB : -1;
    
    /* compute the raw levels for more percentile values; will help if the image is overexposed */
    /* if it's not, we'll use only the first value (the one from menu) */
    int percentiles[13] = {(1000 - auto_ettr_ignore), 950, 900, 800, 750, 700, 600, 500, 300, 200, 150, 100, 50};

    int raw_values[COUNT(percentiles)];
    static float diff_from_lower_percentiles[COUNT(percentiles)-1] = {0};

    int speed = 1; /* 1 = examine each LiveView pixel (720x480); 2 = downsample by 2 and so on */
    if (lv)
    {
        /* if highlight ignore is off, we have to look carefully */
        /* otherwise, the meter is not that sensitive and can be a little faster */
        speed = auto_ettr_ignore ? 4 : 2;
    }

    int ok = raw_hist_get_percentile_levels(percentiles, raw_values, COUNT(percentiles), gray_proj | GRAY_PROJECTION_DARK_ONLY, speed);
    
    if (ok != 1)
    {
        last_value = INT_MIN;
        return last_value;
    }
    
    float ev = raw_to_ev(raw_values[0]);
    int raw_median_lo = raw_values[7];  /* 50th percentile (median) */
    int raw_shadow_lo = raw_values[12]; /* 5th percentile */
    int raw_highlight_lo = raw_values[0]; /* "highlight ignore" percentile */
    float ev_median_lo = raw_to_ev(raw_median_lo);
    float ev_shadow_lo = raw_to_ev(raw_shadow_lo);
    
    int dual_iso = auto_ettr_dual_iso_link && dual_iso_is_active();
    float ev_median_hi = ev_median_lo;
    float ev_shadow_hi = ev_shadow_lo; /* for dual ISO: for the bright exposure */
    
    if (dual_iso)
    {
        /* for dual ISO only:*/
        /* we have metered the dark exposure (since ETTR is pushing that to the right), now meter the bright one too */

        /* EV difference between the two ISOs (from settings) */
        float dual_iso_spacing = ABS(dual_iso_get_recovery_iso() - lens_info.iso_analog_raw) / 8.0;

        if (lv && !is_movie_mode())
        {
            /* photo LV (only one exposure) */
            /* estimate it from settings */
            int rec_iso = dual_iso_get_recovery_iso();
            if (rec_iso > (int)lens_info.iso_analog_raw) /* we are looking at the dark exposure */
            {
                ev_median_hi = MIN(ev_median_lo + dual_iso_spacing, 0); /* you can't get whiter than white */
                ev_shadow_hi = MIN(ev_shadow_lo + dual_iso_spacing, 0);
            }
            else /* we are looking at the bright exposure */
            {
                ev_median_hi = ev_median_lo - dual_iso_spacing;
                ev_shadow_hi = ev_shadow_lo - dual_iso_spacing;
                float aux = ev_median_hi; ev_median_hi = ev_median_lo; ev_median_lo = aux;
                aux = ev_shadow_hi; ev_shadow_hi = ev_shadow_lo; ev_shadow_lo = aux;
            }
        }
        else
        {
            /* photo non-LV and movie */
            int percentiles_hi[2] = {500, 50};
            int raw_values_hi[2];
            raw_hist_get_percentile_levels(percentiles_hi, raw_values_hi, COUNT(percentiles_hi), gray_proj | GRAY_PROJECTION_BRIGHT_ONLY, 4);
            int raw_median_hi = raw_values_hi[0];  /* 50th percentile (median) */
            int raw_shadow_hi = raw_values_hi[1]; /* 5th percentile */

            /* signal level for the higher exposure must be equal to signal level for the lower exposure plus dual ISO spacing (EV) */
            /* if it's not, it's very likely to be a large black level difference messing with our formulas */
            /* let's try to fight it! */

            /* compute it from shadow levels, because this is where black delta has the largest effect */
            /* if you compute it from median, shadow may be still wrong by 1-2 EV */
            /* if you compute it from shadow, median may be wrong by only 0.1 - 0.2 EV - much better! */
            int black_delta = guess_black_delta(raw_shadow_lo, raw_shadow_hi, dual_iso_spacing);

            ev_median_lo = raw_to_ev(raw_median_lo + black_delta);
            ev_shadow_lo = raw_to_ev(raw_shadow_lo + black_delta);

            ev_median_hi = raw_to_ev(raw_median_hi - black_delta);
            ev_shadow_hi = raw_to_ev(raw_shadow_hi - black_delta);

            if (debug_info)
            {
                int gap_med = (ev_median_hi - ev_median_lo) * 100;
                int gap_shad = (ev_shadow_hi - ev_shadow_lo) * 100;
                printf("Black delta  : %d (EV gap mid:%s%d.%02d shad:%s%d.%02d)\n", black_delta, FMT_FIXEDPOINT2(gap_med), FMT_FIXEDPOINT2(gap_shad));
            }
        }
    }

    if (show_metered_areas)
    {
        /* show where exactly are those percentiles */
        bmp_printf(FONT(FONT_SMALL, COLOR_WHITE, COLOR_BLUE),   0, 20, "Shadows    5%%   ");
        bmp_printf(FONT(FONT_SMALL, COLOR_WHITE, COLOR_ORANGE), 0, 32, "Midtones   50%%  ");
        int hp = (1000 - auto_ettr_ignore);
        bmp_printf(FONT(FONT_SMALL, COLOR_WHITE, COLOR_RED),    0, 44, "Highlights%3d.%d%%", hp/10, hp%10);
        zebra_highlight_raw_advanced(
            (struct raw_highlight_info [])
            {
                {
                    .raw_level_lo = 0,
                    .raw_level_hi = raw_shadow_lo,
                    .color = COLOR_BLUE,
                    .line_type = ZEBRA_LINE_SIMPLE,
                    .fill_type = ZEBRA_FILL_DIAG,
                    .gray_projection = gray_proj | GRAY_PROJECTION_DARK_ONLY,
                },
                {
                    .raw_level_lo = raw_median_lo,
                    .raw_level_hi = raw_median_lo,
                    .color = COLOR_ORANGE,
                    .line_type = ZEBRA_LINE_SIMPLE,
                    .gray_projection = gray_proj | GRAY_PROJECTION_DARK_ONLY,
                },
                {
                    .raw_level_lo = raw_highlight_lo,
                    .raw_level_hi = 16384,
                    .color = COLOR_RED,
                    .line_type = ZEBRA_LINE_SIMPLE,
                    .fill_type = ZEBRA_FILL_DIAG,
                    .gray_projection = gray_proj | GRAY_PROJECTION_DARK_ONLY,
                },
                RAW_HIGHLIGHT_END
            }
        );
    }

    //~ bmp_printf(FONT_MED, 50, 200, "%d ", MEMX(0xc0f08030));
    float target = MIN(auto_ettr_target_level, -0.5);
    float correction = target - ev;
    float overexposed_percentage = 0;
    if (ev < -0.1)
    {
        /* cool, we know exactly how much to correct, we'll return "correction" */
        
        /* save data for helping with future overexposed shots */
        for (int k = 0; k < COUNT(percentiles)-1; k++)
            diff_from_lower_percentiles[k] = ev - raw_to_ev(raw_values[k+1]);
        
        if (debug_info) printf("overexposure hints: %d %d %d\n", (int)(diff_from_lower_percentiles[0] * 100), (int)(diff_from_lower_percentiles[1] * 100), (int)(diff_from_lower_percentiles[2] * 100));
    }
    else
    {
        /* image is overexposed */
        /* and we don't know how much to go back in order to fix the overexposure */

        /* we can find out how many pixels are clipped, but this doesn't help much in knowing how many stops we should go back */
        overexposed_percentage = raw_hist_get_overexposure_percentage(GRAY_PROJECTION_AVERAGE_RGB | GRAY_PROJECTION_DARK_ONLY) / 100.0;
        if (debug_info) printf("overexposure area: %s%d.%d%%\n", FMT_FIXEDPOINT2((int)(overexposed_percentage * 100)));

        /* from the previous shot, we know where the highlights were, compared to some lower percentiles */
        /* let's assume this didn't change; meter at those percentiles and extrapolate the result */

        int num = 0;
        float sum = 0;
        float min = 100000;
        float max = -100000;
        for (int k = 0; k < COUNT(percentiles)-1; k++)
        {
            if (diff_from_lower_percentiles[k] > 0)
            {
                float lower_ev = raw_to_ev(raw_values[k+1]);
                if (lower_ev < -0.1)
                {
                    /* if the scene didn't change, we should be spot on */
                    /* don't update the correction hints, since we don't know exactly where we are */
                    ev = lower_ev + diff_from_lower_percentiles[k];
                    
                    /* we need to get a stronger correction than with the overexposed metering */
                    /* otherwise, the scene probably changed */
                    if (target - ev < correction)
                    {
                        float corr = target - ev;
                        min = MIN(min, corr);
                        max = MAX(max, corr);
                        
                        /* first estimations are more reliable, weight them a bit more */
                        sum += corr * (COUNT(percentiles) - k);
                        num += (COUNT(percentiles) - k);
                        //~ msleep(500);
                        printf("overexposure fix: k=%d diff=%d ev=%d corr=%d\n", k, (int)(diff_from_lower_percentiles[k] * 100), (int)(ev * 100), (int)(corr * 100));
                    }
                }
            }
        }

        /* use the average value for correction */
        correction = sum / num;
        
        if (num < 3 || max - correction > 1 || correction - min > 1 || correction > -1)
        {
            /* scene changed? measurements from previous shot not confirmed or vary too much?
             * 
             * we'll use a heuristic: for 1% of blown out image, go back 1EV, for 100% go back 13EV */
            printf("fail info: (%d %d %d %d) (%d %d %d)\n", raw_values[0], raw_values[1], raw_values[2], raw_values[3], (int)(diff_from_lower_percentiles[0] * 100), (int)(diff_from_lower_percentiles[1] * 100), (int)(diff_from_lower_percentiles[2] * 100));
            float corr = - log2f(1 + overexposed_percentage*overexposed_percentage);
            
            /* with dual ISO, the cost of underexposing is not that high, so prefer it to improve convergence */
            if (dual_iso)
                corr *= 3;
            
            correction = MIN(correction, corr);
            
            /* we can't really meter more than 10 EV */
            correction = MAX(correction, -10);
        }
    }

    int iso1 = lens_info.iso_analog_raw;
    int iso2 = iso1;
    if (dual_iso) iso2 = dual_iso_get_recovery_iso();
    int iso_hi = MAX(iso1, iso2);
    int iso_lo = MIN(iso1, iso2);
    float dr_lo = get_dxo_dynamic_range(iso_lo) / 100.0;
    float dr_hi = get_dxo_dynamic_range(iso_hi) / 100.0;

    if (debug_info)
    {
        if (dual_iso)
        {
            float midtone_snr_lo = dr_lo + ev_median_lo;
            float shadow_snr_lo = dr_lo + ev_shadow_lo;
            int mid_snr_lo = (int)roundf(midtone_snr_lo * 10);
            int shad_snr_lo = (int)roundf(shadow_snr_lo * 10);
            float midtone_snr_hi = dr_hi + ev_median_hi;
            float shadow_snr_hi = dr_hi + ev_shadow_hi;
            int mid_snr_hi = (int)roundf(midtone_snr_hi * 10);
            int shad_snr_hi = (int)roundf(shadow_snr_hi * 10);
            printf("Midtone SNR  : %s%d.%d / %s%d.%d EV\n", FMT_FIXEDPOINT1(mid_snr_lo), FMT_FIXEDPOINT1(mid_snr_hi));
            printf("Shadows SNR  : %s%d.%d / %s%d.%d EV\n", FMT_FIXEDPOINT1(shad_snr_lo), FMT_FIXEDPOINT1(shad_snr_hi));
        }
        else
        {
            float midtone_snr = dr_lo + ev_median_lo;
            float shadow_snr = dr_lo + ev_shadow_lo;
            int mid_snr = (int)roundf(midtone_snr * 10);
            int shad_snr = (int)roundf(shadow_snr * 10);
            printf("Midtone SNR  : %s%d.%d EV\n", FMT_FIXEDPOINT1(mid_snr));
            printf("Shadows SNR  : %s%d.%d EV\n", FMT_FIXEDPOINT1(shad_snr));
        }
        int clipped = raw_hist_get_overexposure_percentage(GRAY_PROJECTION_AVERAGE_RGB | GRAY_PROJECTION_DARK_ONLY);
        printf("Clipped highs: %s%d.%02d%%\n", FMT_FIXEDPOINT2(clipped));
    }

    if (overexposed_percentage > 0 && (auto_ettr_midtone_snr_limit || auto_ettr_shadow_snr_limit) && !dual_iso)
    {
        /* if the image is overexposed and we have SNR limits, we could meter for those instead */
        /* don't underexpose by more than 2 EV in one step though */
        correction -= 2;
    }

    /* are we underexposing too much? */
    float correction0 = correction;
    if (lens_info.raw_iso && (auto_ettr_midtone_snr_limit || auto_ettr_shadow_snr_limit))
    {
        float midtone_snr = dr_lo + ev_median_lo;
        float shadow_snr = dr_lo + ev_shadow_lo;

        if (auto_ettr_midtone_snr_limit)
        {
            float midtone_expected_snr = midtone_snr + correction0;
            int midtone_desired_snr = auto_ettr_midtone_snr_limit;

            if (midtone_expected_snr < midtone_desired_snr)
            {
                correction = MAX(correction, correction0 + midtone_desired_snr - midtone_expected_snr);
            }
        }

        if (auto_ettr_shadow_snr_limit)
        {
            float shadow_expected_snr = shadow_snr + correction0;
            int shadow_desired_snr = auto_ettr_shadow_snr_limit;

            if (shadow_expected_snr < shadow_desired_snr)
            {
                correction = MAX(correction, correction0 + shadow_desired_snr - shadow_expected_snr);
            }
        }
    }
    
    /* exposure difference with and without SNR limits */
    int expo_delta_snr = (correction - correction0) * 100.0;
    
    if (debug_info)
    {
        int expo_hi = correction0 * 100.0;
        int expo_snr = correction * 100.0;
        printf("Expo highlight: %s%d.%02d EV\n", FMT_FIXEDPOINT2S(expo_hi));
        printf("Expo SNR limit: %s%d.%02d EV\n", FMT_FIXEDPOINT2S(expo_snr));
        printf("Expo delta SNR: %s%d.%02d EV\n", FMT_FIXEDPOINT2S(expo_delta_snr));
    }

    /* exposure correction so it doesn't clip anything more than allowed by highlight ignore */
    int corr_without_clipping = (int)(correction * 100) - expo_delta_snr;

    if (dual_iso)
    {
        /* with dual ISO: expose without clipping */
        /* auto_ettr_work will have to do something and recover the SNR */
        last_value = corr_without_clipping;
        extra_snr_needed = expo_delta_snr;
    }
    else
    {
        /* without dual ISO: expose with clipping in order to meet the SNR */
        /* no more SNR correction needed */
        last_value = corr_without_clipping + expo_delta_snr;
        extra_snr_needed = 0;
    }
    
    if (debug_info)
    {
        printf("Expo correction: %s%d.%02d EV\n", FMT_FIXEDPOINT2S(last_value));
    }
    return last_value;
}

int auto_ettr_export_correction(int* out)
{
    int value = auto_ettr_get_correction();
    if (value == INT_MIN) return -1;
    if (out) *out = value;
    return 1;
}

static char prev_exposure_settings[50];

/* returns: 0 = nothing changed, 1 = OK, -1 = exposure limits reached */
static int auto_ettr_work(int corr)
{
    if (debug_info) printf("\nauto_ettr_work(%d)\n", corr);
    /* wait until shutter speed is reported by Canon firmware */
    int iter = 0;
    while (lens_info.raw_shutter == 0)
    {
        if (iter > 100)
        {
            return ETTR_EXPO_PRECOND_TIMEOUT;
        }
        msleep(50);
        iter += 50;
    }
    
    /* save initial exposure settings so we can print them */
    char* expo_settings = get_current_exposure_settings();
    snprintf(prev_exposure_settings, sizeof(prev_exposure_settings), "%s", expo_settings);
    
    int tv = ettr_get_current_raw_shutter();
    int iso = lens_info.raw_iso;
    
    /* to detect whether it settled or not */
    int tv_before = tv;
    int iso_before = iso;
    int iso2_before = dual_iso_get_recovery_iso();
    
    if (!tv || !iso) return 0;
    //~ int old_expo = tv - iso;

    /* note: expo compensation will not clip with dual ISO, but will clip highlights without it */
    int dual_iso = auto_ettr_dual_iso_link && dual_iso_is_active();
    int delta = -corr * 8 / 100;
    
    int expected_expo = tv - iso + delta;               /* will clip without dual ISO */

    static int prev_tv = 0;
    if (auto_ettr_adjust_mode == 1)
    {
        if (prev_tv != tv)
        {
            auto_ettr_max_shutter = tv;
            if (lv)
            {
                NotifyBox(2000, "ETTR: Tv <= %s ", lens_format_shutter(tv));
                prev_tv = tv;
                return 0; /* wait for next iteration */
            }
            else
            {
                msleep(1000);
                bmp_printf(FONT_MED, 0, os.y0, "ETTR: Tv <= %s ", lens_format_shutter(tv));
            }
        }
    }
    else
    {
        if (lv && prev_tv != tv && AUTO_ETTR_TRIGGER_ALWAYS_ON)
        {
            prev_tv = tv;
            return 0; /* small pause when you change exposure manually */
        }
    }

    int shutter_lim = auto_ettr_max_shutter;

    /* if intervalometer is enabled, limit longest exposures
     * to interval time minus 2 seconds */
    if (is_intervalometer_running())
    {
        int intervalometer_lim = MAX(200, 1000 * (get_interval_time() - 2));
        shutter_lim = MAX(shutter_lim, shutter_ms_to_raw(intervalometer_lim));
    }

    /* can't go slower than 1/fps in movie mode */
    if (is_movie_mode())
    {
        shutter_lim = MAX(shutter_lim, shutter_ms_to_raw(1000 / video_mode_fps));
        if (!expo_override_active())
        {
            /* without expo override, in movie mode we can't set exposures longer than 1/30 */
            shutter_lim = MAX(shutter_lim, SHUTTER_1_30);
        }
    }

    /* apply exposure correction */
    tv += delta;

    if (debug_info) printf("expo after comp: %d\n", tv - iso);

    /* use the lowest ISO for which we can get shutter = shutter_lim or higher */
    int offset = MIN(tv - shutter_lim, iso - MIN_ISO);
    tv -= offset;
    iso -= offset;

    /* some shutter values are not accepted by Canon firmware */
    int tvr = (MIN(tv, shutter_lim) >= SHUTTER_30s)
        ? round_shutter(tv, shutter_lim)
        : MAX(tv, shutter_lim);
    
    iso += tvr - tv;

    if (debug_info) printf("tv rounding: %d -> %d limit=%d\n", tv, tvr, shutter_lim);
    
    /* analog iso can be only in 1 EV increments */
    /* prefer rounding towards lower ISOs */
    int max_auto_iso = auto_iso_range & 0xFF;
    int isor = COERCE(iso / 8 * 8, MIN_ISO, max_auto_iso);
    if (debug_info) printf("iso rounding: %d -> %d (expo %d -> %d)\n", iso, isor, tvr - iso, tvr - isor);
    
    /* can we use dual ISO to recover the highlights? (HR = highlight recovery) */
    if (dual_iso)
    {
        int base_iso = isor;
        int recovery_iso = base_iso;

        /* bring back the SNR */
        int snr_delta = -extra_snr_needed;
        while (snr_delta < 0 && recovery_iso < max_auto_iso)
        {
            int old_rec_iso = recovery_iso;
            recovery_iso += 8;
            int dr_gained = dual_iso_calc_dr_improvement(old_rec_iso, recovery_iso);
            snr_delta += dr_gained;
        }
        
        if (snr_delta + extra_snr_needed < 100) /* snr_delta + extra_snr_needed = SUM(dr_gained) */
        {
            /* too little gain? just shoot at base ISO */
            recovery_iso = base_iso;
            snr_delta = -extra_snr_needed;
        }
        else if (base_iso > MIN_ISO)
        {
            /* shooting at high ISO? go back one stop to protect some more highlights, because the cost is next to none */
            base_iso -= 8;
            expected_expo += 8;
        }

        /* apply dual ISO settings */
        isor = base_iso;
        dual_iso_set_recovery_iso(recovery_iso);
        extra_snr_needed = -snr_delta;
    }

    /* apply the new settings */
    int oki = 0, oks = 0;
    if (tvr < SHUTTER_30s)
    {
        /* use BULB for long exposures */
        ensure_bulb_mode();
        int seconds = auto_ettr_get_long_exposure_time(tvr);
        
        if (is_intervalometer_running())
        {
            /* in BULB mode, limit longest exposures to interval time minus 3 seconds */
            int intervalometer_lim = MAX(1, get_interval_time() - 3);
            seconds = MIN(seconds, intervalometer_lim);
        }
        
        /* configure bulb timer with the new exposure */
        menu_set_value_from_script("Bulb Timer", "Exposure duration", seconds);
        oks = 1;

        /* set ISO */
        oki = hdr_set_rawiso(isor);
    }
    else
    {
        if (is_bulb_mode())
        {
            /* back from BULB */
            set_shooting_mode(SHOOTMODE_M);
        }
        
        oki = lens_set_rawiso(isor);    /* for expo overide */
        oks = lens_set_rawshutter(tvr);
        if (!expo_override_active())
        {
            oks = hdr_set_rawshutter(tvr);  /* for confirmation and retrying if needed */
            oki = hdr_set_rawiso(isor);
        }
    }

    /* don't let expo lock undo our changes */
    expo_lock_update_value();

    if (debug_info)
    {
        printf("Adjusted expo: %s (SNR lost: %s%d.%02d)\n", get_current_exposure_settings(), FMT_FIXEDPOINT2(extra_snr_needed));
    }

    /* to know when the user changed shutter speed */
    prev_tv = ettr_get_current_raw_shutter();
    
    /* did it converge or not? */
    int tv_after = prev_tv;
    int iso_after = lens_info.raw_iso;
    int new_expo = tv_after - iso_after;

    if (dual_iso)
    {
        int iso2_after = dual_iso_get_recovery_iso();
        int dr2_before = dual_iso_calc_dr_improvement(iso_before, iso2_before);
        int dr2_after = dual_iso_calc_dr_improvement(iso_after, iso2_after);

        if (debug_info)
        {
            printf( 
                "iso2 %d->%d dr %d->%d\n",
                raw2iso(iso2_before), raw2iso(iso2_after), dr2_before, dr2_after
            );
        }

        if (ABS(dr2_after - dr2_before) >= 40)
            return ETTR_NEED_MORE_SHOTS;
        
        //~ if (highlight_headroom_needed > 50)
            //~ return ETTR_EXPO_LIMITS_REACHED;
    }

    if (debug_info)
    {
        printf(
            "iso %d->%d %s\ntv %d->%d %s\nexpo expected %d got %d\n",
            raw2iso(iso_before), raw2iso(iso_after), oki ? "OK" : "err",
            tv_before, tv_after, oks ? "OK" : "err",
            expected_expo, new_expo
        );
    }
    
    /* anything changed? consider it OK, better than nothing */
    if (ABS(tv_before - tv_after) >= 4)
        return ETTR_NEED_MORE_SHOTS;

    if (ABS(iso_before - iso_after) >= 4)
        return ETTR_NEED_MORE_SHOTS;

    /* did we fully correct the exposure? */
    if (ABS(new_expo - expected_expo) > 8)
        return ETTR_EXPO_LIMITS_REACHED;

    return oks && oki ? ETTR_SETTLED : ETTR_EXPO_LIMITS_REACHED;
}

static volatile int auto_ettr_running = 0;
static volatile int ettr_pics_took = 0;

static void auto_ettr_step_task(int corr)
{
    lens_wait_readytotakepic(64);
    int status = auto_ettr_work(corr);
    
    if (status == ETTR_SETTLED)
    {
        /* cool, we got the ideal exposure */
        ettr_beep();
        ettr_pics_took = 0;
        
        msleep(1000);
        bmp_printf(FONT_MED, 0, os.y0, "ETTR: settled at %s", get_current_exposure_settings());

        //~ int blown_highlights = (highlight_headroom_needed - highlight_headroom_recovered) / 10;
        //~ if (blown_highlights > 2)
        //~ {
            //~ msleep(1000);
            //~ bmp_printf(FONT_MED, 0, os.y0, "ETTR: clipped %s%d.%d EV of highlights", FMT_FIXEDPOINT1(blown_highlights));
        //~ }
    }
    else if (ettr_pics_took >= 3)
    {
        /* I give up */
        ettr_beep_times(3);
        ettr_pics_took = 0;
        msleep(1000);
        bmp_printf(FONT_MED, 0, os.y0, "ETTR: giving up\n%s", get_current_exposure_settings());
    }
    else if (status == ETTR_EXPO_LIMITS_REACHED)
    {
        ettr_beep_times(3);
        ettr_pics_took = 0;
        msleep(1000);
        bmp_printf(FONT_MED, 0, os.y0, "ETTR: expo limits reached\n%s", get_current_exposure_settings());
    }
    else if (status == ETTR_EXPO_PRECOND_TIMEOUT)
    {
        ettr_beep_times(3);
        ettr_pics_took = 0;
        msleep(1000);
        bmp_printf(FONT_MED, 0, os.y0, "ETTR: timeout while waiting for preconditions\n");
    }
    else if (AUTO_ETTR_TRIGGER_AUTO_SNAP)
    {
        /* take another pic */
        auto_ettr_running = 0;
        schedule_remote_shot();
        ettr_pics_took++;
    }
    else if (AUTO_ETTR_TRIGGER_ALWAYS_ON)
    {
        ettr_beep_times(2);
        msleep(1000);
        bmp_printf(FONT_MED, 0, os.y0, "ETTR: next %s (was %s)", get_current_exposure_settings(), prev_exposure_settings);

        //~ int blown_highlights = (highlight_headroom_needed - highlight_headroom_recovered) / 10;
        //~ if (blown_highlights > 2)
        //~ {
            //~ bmp_printf(FONT_MED, 0, os.y0+20, "Clipped %s%d.%d EV of highlights", FMT_FIXEDPOINT1(blown_highlights));
        //~ }
    }
    auto_ettr_running = 0;
}

/* photo mode only, no LV */
static void auto_ettr_step()
{
    if (!auto_ettr) return;
    if (shooting_mode != SHOOTMODE_M && !is_movie_mode() && !is_bulb_mode()) return;
    if (lens_info.raw_iso == 0) return;
    if (auto_ettr_running) return;
    if (is_hdr_bracketing_enabled() && !AUTO_ETTR_TRIGGER_BY_SET) return;

    if (!raw_update_params())
    {
        NotifyBox(5000, "Raw error");
        return;
    }

    int corr = auto_ettr_get_correction();
    if (corr != INT_MIN)
    {
        /* we'd better not change expo settings from prop task (we won't get correct confirmations) */
        auto_ettr_running = 1;
        task_create("ettr_task", 0x1c, 0x1000, auto_ettr_step_task, (void*) corr);
    }
}

static int auto_ettr_check_pre_lv()
{
    if (!auto_ettr) return 0;
    if (shooting_mode != SHOOTMODE_M && !is_movie_mode()) return 0;
    if (lens_info.raw_iso == 0) return 0;
    if (lens_info.raw_shutter == 0) return 0;
    if (is_hdr_bracketing_enabled() && !AUTO_ETTR_TRIGGER_BY_SET) return 0;
    int raw = is_movie_mode() ? raw_lv_is_enabled() : pic_quality & 0x60000;
    return raw;
}

static int auto_ettr_check_in_lv()
{
    if (AUTO_ETTR_TRIGGER_ALWAYS_ON && !get_expsim()) return 0;
    if (AUTO_ETTR_TRIGGER_ALWAYS_ON && lv_dispsize != 1) return 0;
    if (LV_PAUSED) return 0;
    if (!liveview_display_idle()) return 0;
    return 1;
}

static int auto_ettr_check_lv()
{
    if (!auto_ettr_check_pre_lv()) return 0;
    if (!auto_ettr_check_in_lv()) return 0;
    return 1;
}

static volatile int auto_ettr_vsync_active = 0;
static volatile int auto_ettr_vsync_delta = 0;
static volatile int auto_ettr_vsync_counter = 0;

/* instead of changing settings via properties, we can override them very quickly */
static unsigned int auto_ettr_vsync_cbr(unsigned int ctx)
{
    auto_ettr_vsync_counter++;

    if (auto_ettr_vsync_active)
    {
        int delta = auto_ettr_vsync_delta;
        int current_iso = get_frame_iso();
        int current_shutter = get_frame_shutter_timer();
        int altered_iso = current_iso;
        int altered_shutter = current_shutter;

        int max_shutter = get_max_shutter_timer();
        if (current_shutter > max_shutter) max_shutter = current_shutter;

        /* first increase shutter speed, since it gives the cleanest signal */
        while (delta > 0 && altered_shutter * 2 <= max_shutter)
        {
            altered_shutter *= 2;
            delta -= 8;
        }

        int max_iso = get_max_analog_iso();

        /* then try to increase ISO if we need more */
        while (delta > 0 && altered_iso + 8 <= max_iso)
        {
            altered_iso += 8;
            delta -= 8;
        }

        /* then try to decrease ISO until ISO 100, raw 72 (even with HTP, FRAME_ISO goes to 100) */
        while (delta < -8 && altered_iso - 8 >= 72)
        {
            altered_iso -= 8;
            delta += 8;
        }

        /* commit iso */
        set_frame_iso(altered_iso);

        /* adjust shutter with the remaining delta */
        altered_shutter = COERCE((int)roundf(powf(2, delta/8.0) * (float)altered_shutter), 2, max_shutter);
        
        /* commit shutter */
        set_frame_shutter_timer(altered_shutter);
        
        //~ bmp_printf(FONT_MED, 50, 70, "delta %d iso %d->%d shutter %d->%d max %d ",  auto_ettr_vsync_delta, current_iso, altered_iso, current_shutter, altered_shutter, get_max_shutter_timer());
        return 1;
    }
    
    return 0;
}

static int auto_ettr_wait_lv_frames(int num_frames)
{
    auto_ettr_vsync_counter = 0;
    int count = 0;
    int frame_duration = 1000000 / fps_get_current_x1000();
    while (auto_ettr_vsync_counter < num_frames)
    {
        frame_duration = MAX(frame_duration, 1000000 / fps_get_current_x1000());
        msleep(20);
        count++;
        if (count > num_frames * frame_duration * 2 / 20)
        {
            /* timeout */
            if (debug_info) printf("wait_lv_frames: timeout\n");
            return 0;
        }
        if (!lv)
        {
            /* outside lv */
            if (debug_info) printf("wait_lv_frames: LV closed\n");
            return 0;
        }
    }
    return 1;
}

/* wait until LiveView exposure changes from the old values to something else (with timeout on number of frames) */
static int auto_ettr_wait_lv_expo_change(int max_frames, int old_iso, int old_shutter)
{
    /* todo: also look at aperture changes */
    for (int i = 0; i < max_frames; i++)
    {
        int current_iso = get_frame_iso();
        int current_shutter = get_frame_shutter_timer();
        if (debug_info) printf("wait lv expo change: %x %x\n", current_iso, current_shutter);
        if (current_iso != old_iso || current_shutter != old_shutter)
        {
            if (debug_info) printf("exposure changed to: %x %x\n", current_iso, current_shutter);
            /* exposure changed */
            return 1;
        }
        if (!auto_ettr_wait_lv_frames(1))
        {
            /* whoops */
            return 0;
        }
    }

    /* timeout */
    if (debug_info) printf("lv expo change timeout\n");
    return 0;
}

/* wait until LiveView exposure settles (identical on two consecutive frames) */
static int auto_ettr_wait_lv_expo_settle(int max_frames)
{
    /* todo: also look at aperture changes */
    int old_iso = -1;
    int old_shutter = -1;
    for (int i = 0; i < max_frames; i++)
    {
        int current_iso = get_frame_iso();
        int current_shutter = get_frame_shutter_timer();
        if (debug_info) printf("wait lv expo settle: %x %x\n", current_iso, current_shutter);
        if (current_iso == old_iso && current_shutter == old_shutter)
        {
            /* looks like it settled */
            if (debug_info) printf("lv expo maybe settled at: %x %x\n", current_iso, current_shutter);
            
            /* wait one more frame, just in case */
            if (auto_ettr_wait_lv_frames(2) == 0)
            {
                return 0;
            }
            
            current_iso = get_frame_iso();
            current_shutter = get_frame_shutter_timer();
            if (current_iso == old_iso && current_shutter == old_shutter)
            {
                if (debug_info) printf("lv expo settled at: %x %x\n", current_iso, current_shutter);
                /* looks like it did settle */
                return 1;
            }
        }
        if (!auto_ettr_wait_lv_frames(1))
        {
            /* whoops */
            return 0;
        }
        old_iso = current_iso;
        old_shutter = current_shutter;
    }
    
    /* timeout */
    if (debug_info) printf("lv expo settle timeout\n");
    return 0;
}

static int auto_ettr_prepare_lv(int reset, int force_expsim_and_zoom)
{
    static int was_in_lv = 1;
    static int old_expsim = -1;
    static int old_zoom = -1;
    static int should_clear_bv = 0;
    
    if (!reset)
    {
        was_in_lv = lv;
        old_expsim = -1;

        if (!lv) force_liveview();
        if (!lv) return 0; /* fail */

        /* force 1x zoom */
        if (force_expsim_and_zoom && lv_dispsize != 1)
        {
            old_zoom = lv_dispsize;
            set_lv_zoom(1);
            if (!auto_ettr_wait_lv_frames(10)) return 0;
        }

        /* temporarily enable get_expsim() while metering */
        if (force_expsim_and_zoom)
        {
            if (shooting_mode == SHOOTMODE_M && !lens_info.name[0])
            {
                /* workaround for Canon's manual lens underexposure bug */
                /* use expo override instead of ExpSim */
                extern int bv_auto;
                if (!bv_auto)
                {
                    should_clear_bv = 1;
                    bv_toggle(0, 1);
                    if (!auto_ettr_wait_lv_frames(10)) return 0;
                }
            }
            else if (!get_expsim())
            {
                /* ExpSim should work well */
                old_expsim = get_expsim();
                set_expsim(1);
                if (!auto_ettr_wait_lv_frames(10)) return 0;
            }
        }
    }
    else /* undo all that stuff */
    {
        if (should_clear_bv)
        {
            extern int bv_auto;
            if (bv_auto)
            {
                bv_toggle(0, -1);
                auto_ettr_wait_lv_frames(5);
            }
            should_clear_bv = 0;
        }
        
        if (old_expsim >= 0)
        {
            set_expsim(old_expsim);
            old_expsim = -1;
        }
        
        if (old_zoom > 0)
        {
            set_lv_zoom(old_zoom);
            old_zoom = -1;
        }
        
        if (lv && !was_in_lv)
        {
            msleep(200);
            close_liveview();
            was_in_lv = 1;
        }
    }
    return 1; /* ok */
}

static void auto_ettr_on_request_task_fast()
{
    ettr_beep();
    int raw_requested = 0;
    
    char* err_msg = "ETTR failed";
    
    /* requires LiveView and ExpSim */
    if (!auto_ettr_prepare_lv(0, 1)) goto err;
    if (!auto_ettr_check_lv()) goto err;
    
    if (get_halfshutter_pressed())
    {
        msleep(500);
        if (get_halfshutter_pressed()) goto err;
    }

#undef AUTO_ETTR_DEBUG
#ifdef AUTO_ETTR_DEBUG
    auto_ettr_vsync_active = 1;
    int raw0 = raw_hist_get_percentile_level(500, GRAY_PROJECTION_GREEN, 2);
    float ev0 = raw_to_ev(raw0);
    int y0 = 100 - ev0 * 20;
    for (int i = 0; i < 100; i++)
    {
        int delta = rand() % 160 - 80;
        auto_ettr_vsync_delta = delta;
        if (!auto_ettr_wait_lv_frames(2)) break;
        
        int raw = raw_hist_get_percentile_level(500, GRAY_PROJECTION_GREEN, 2);
        float ev = raw_to_ev(raw);
        int x = 360 + delta * 3;
        int y = 100 - ev * 24; /* multiplier must be 8 x the one from delta */
        dot(x-16, y-16, COLOR_BLUE, 3);
        draw_angled_line(360, y0, 300, 1800-450, COLOR_RED);
        draw_angled_line(360, y0, 300, -450, COLOR_RED);
        draw_angled_line(0, 100, 720, 0, COLOR_RED);
    }
    auto_ettr_vsync_delta = 0;
    auto_ettr_wait_lv_frames(100);
#endif


    NotifyBox(100000, "ETTR...");
    raw_lv_request(); raw_requested = 1;

    if (!raw_update_params())
    {
        err_msg = "Raw error";
        goto err;
    }

    for (int i = 0; i < 5; i++)
    {
        NotifyBox(100000, "ETTR (%d)...", i+1);

        /* make sure the LiveView exposure is settled before reading */
        if (!auto_ettr_wait_lv_expo_settle(30)) break;

        if (fps_get_shutter_speed_shift(160) == 0)
        {
            auto_ettr_vsync_active = 1;
            auto_ettr_vsync_delta = 0;
            for (int k = 0; k < 5; k++)
            {
                if (debug_info) printf("ETTR (%d.%d)\n", i+1, k+1);
                
                /* see how far we are from the ideal exposure */
                int corr = auto_ettr_get_correction();
                if (corr == INT_MIN) break;
                
                /* override the liveview parameters via auto_ettr_vsync_cbr (much faster than via properties) */
                auto_ettr_vsync_delta += corr * 8 / 100;

                /* I'm confident the last iteration was accurate */
                if (corr >= -20 && corr <= 200)
                    break;
                
                /* wait for 2 frames before trying again */
                if (!auto_ettr_wait_lv_frames(2)) goto err;
            }
            auto_ettr_vsync_active = 0;
        }
        else /* FPS override is messing up our plans? fall back to the slow method */
        {
            int corr = auto_ettr_get_correction();
            if (corr == INT_MIN) break;
            auto_ettr_vsync_delta = corr * 8 / 100;
        }

        /* apply the correction via properties */
        int corr = auto_ettr_vsync_delta * 100 / 8;
        int old_iso = get_frame_iso();
        int old_shutter = get_frame_shutter_timer();
        int status = auto_ettr_work(corr);
    
        if (status == ETTR_SETTLED)
        {
            /* looks like it settled */
            break;
        }
        else
        {
            if (i < 4 && status != ETTR_EXPO_LIMITS_REACHED)
            {
                /* here we go again... */
                if (!auto_ettr_wait_lv_expo_change(30, old_iso, old_shutter)) goto err;
            }
            else
            {
                /* or... not? */
                err_msg = status == ETTR_EXPO_LIMITS_REACHED ? "Expo limits reached" : "Whoops";
                goto err;
            }
        }
    }

/* ok: */
    ettr_beep();
    NotifyBoxHide();
    goto cleanup;

err:
    ettr_beep();
    ettr_beep();
    NotifyBox(2000, err_msg);
    goto cleanup;

cleanup:
    auto_ettr_running = 0;
    auto_ettr_vsync_active = 0;
    if (raw_requested) raw_lv_release();
    auto_ettr_prepare_lv(1, 1);
}

static void auto_ettr_step_lv_fast()
{
    if (!auto_ettr || !AUTO_ETTR_TRIGGER_ALWAYS_ON)
        return;
    
    if (!auto_ettr_prepare_lv(0, 0))
        goto end;
    
    if (!auto_ettr_check_lv())
        goto end;
    
    if (get_halfshutter_pressed())
        goto end;

    /* only poll exposure once per second */
    static int aux = INT_MIN;
    if (!should_run_polling_action(1000, &aux))
        goto end;
    
    raw_lv_request();

    if (!raw_update_params())
    {
        NotifyBox(5000, "Raw error");
        goto skip;
    }

    /* make sure the LiveView exposure is settled before reading */
    if (!auto_ettr_wait_lv_expo_settle(30)) goto skip;

    /* get exposure correction */
    int corr = auto_ettr_get_correction();
    
    /* only correct if the image is overexposed by more than 0.2 EV or underexposed by more than 1 EV */
    if (corr != INT_MIN && (corr < -20 || corr > 100))
    {
        if (fps_get_shutter_speed_shift(160) == 0)
        {
            auto_ettr_vsync_active = 1;
            auto_ettr_vsync_delta = 0;
            int k;
            for (k = 0; k < 5; k++)
            {
                /* see how far we are from the ideal exposure */
                if (k > 0) corr = auto_ettr_get_correction();
                if (corr == INT_MIN) break;
                
                /* override the liveview parameters via auto_ettr_vsync_cbr (much faster than via properties) */
                auto_ettr_vsync_delta += corr * 8 / 100;

                /* I'm confident the last iteration was accurate */
                if (corr >= -20 && corr <= 200)
                    break;
                
                /* wait for 3 frames before trying again */
                if (!auto_ettr_wait_lv_frames(2)) break;
            }
            auto_ettr_vsync_active = 0;
        }
        else /* FPS override is messing up our plans? fall back to the slow method */
        {
            auto_ettr_vsync_delta = corr * 8 / 100;
        }

        /* apply the final correction via properties */
        int old_iso = get_frame_iso();
        int old_shutter = get_frame_shutter_timer();

        auto_ettr_work(auto_ettr_vsync_delta * 100 / 8);

        auto_ettr_wait_lv_expo_change(30, old_iso, old_shutter);
    }

skip:
    raw_lv_release();
    
end:
    auto_ettr_prepare_lv(1, 0);
}

static void auto_ettr_on_request_task_slow()
{
    ettr_beep();
    char* err_msg = "ETTR failed";
    
    /* requires LiveView and ExpSim */
    if (!auto_ettr_prepare_lv(0, 1)) goto err;
    if (!auto_ettr_check_lv()) goto err;

    if (get_halfshutter_pressed())
    {
        msleep(500);
        if (get_halfshutter_pressed()) goto err;
    }

    NotifyBox(100000, "ETTR...");
    for (int k = 0; k < 5; k++)
    {
        msleep(500);
        
        raw_lv_request();

        if (!raw_update_params())
        {
            err_msg = "Raw error";
            goto err;
        }

        int corr = auto_ettr_get_correction();
        raw_lv_release();

        if (corr == INT_MIN)
            break;
        
        int status = auto_ettr_work(corr);
        msleep(1000);
        
        if (status == ETTR_SETTLED)
            break;
        
        if (k == 4 || status == ETTR_EXPO_LIMITS_REACHED)
        {
            err_msg = status == ETTR_EXPO_LIMITS_REACHED ? "Expo limits reached" : "Whoops";
            goto err;
        }
    }

/* ok: */
    ettr_beep();
    NotifyBoxHide();
    goto cleanup;

err:
    ettr_beep();
    ettr_beep();
    NotifyBox(2000, err_msg);
    goto cleanup;

cleanup:
    auto_ettr_prepare_lv(1, 1);
    auto_ettr_running = 0;
}

static void auto_ettr_step_lv_slow()
{
    if (!auto_ettr || !AUTO_ETTR_TRIGGER_ALWAYS_ON)
        return;
    
    if (!auto_ettr_check_lv())
        return;
    
    if (get_halfshutter_pressed())
        return;

    /* only update once per 1.5 seconds, so the exposure has a chance to be updated on the LCD */
    static int aux = INT_MIN;
    if (!should_run_polling_action(1500, &aux))
        return;
    
    int corr = INT_MIN;
    raw_lv_request();
    
    if (raw_update_params())
    {
        corr = auto_ettr_get_correction();
    }
    
    raw_lv_release();
    
    if (corr == INT_MIN)
        return;
    
    /* only correct if the image is overexposed by more than 0.2 EV or underexposed by more than 1 EV */
    if (corr >= -20 && corr < 100)
        return;

    int status = auto_ettr_work(corr);

    if (status == ETTR_SETTLED)
    {
        ettr_beep();
    }
}

static void auto_ettr_step_lv()
{
    if (can_set_frame_iso() && can_set_frame_shutter_timer())
        auto_ettr_step_lv_fast();
    else
        auto_ettr_step_lv_slow();
}

static void auto_ettr_on_request_task(int unused)
{
    if (can_set_frame_iso() && can_set_frame_shutter_timer())
        auto_ettr_on_request_task_fast();
    else
        auto_ettr_on_request_task_slow();
}

static unsigned int auto_ettr_keypress_cbr(unsigned int key)
{
    if (!auto_ettr) return 1;
    if (AUTO_ETTR_TRIGGER_PHOTO) return 1;
    if (!display_idle()) return 1;
    if (!auto_ettr_check_pre_lv()) return 1;
    if (lv && !auto_ettr_check_in_lv()) return 1;
    
    if (
            (IS_EOS_M && AUTO_ETTR_TRIGGER_BY_SET && detect_double_click(key, MODULE_KEY_TOUCH_1_FINGER, MODULE_KEY_UNTOUCH_1_FINGER)) ||
            (!IS_EOS_M && AUTO_ETTR_TRIGGER_BY_SET && key == MODULE_KEY_PRESS_SET) ||
            (AUTO_ETTR_TRIGGER_BY_HALFSHUTTER_DBLCLICK && detect_double_click(key, MODULE_KEY_PRESS_HALFSHUTTER, MODULE_KEY_UNPRESS_HALFSHUTTER)) ||
       0)
    {
        if (!auto_ettr_running)
        {
            auto_ettr_running = 1;
            task_create("ettr_task", 0x1c, 0x1000, auto_ettr_on_request_task, (void*) 0);
        }
        if (AUTO_ETTR_TRIGGER_BY_SET) return 0;
    }
    return 1;
}

static MENU_UPDATE_FUNC(auto_ettr_update)
{
    if (lv && ((void*)&raw_lv_request == (void*)&ret_0))
        MENU_SET_WARNING(MENU_WARN_NOT_WORKING, "Auto ETTR Does not work in LV on this camera.");

    if (shooting_mode != SHOOTMODE_M && !is_movie_mode() && !is_bulb_mode())
        MENU_SET_WARNING(MENU_WARN_NOT_WORKING, "Auto ETTR only works in M, BULB and RAW MOVIE modes.");

    if (lens_info.raw_iso == 0)
        MENU_SET_WARNING(MENU_WARN_NOT_WORKING, "Auto ETTR requires manual ISO.");

    if (!lv && !can_use_raw_overlays_photo() && AUTO_ETTR_TRIGGER_PHOTO)
        MENU_SET_WARNING(MENU_WARN_NOT_WORKING, "Photo RAW data not available, try in LiveView.");

    menu_checkdep_raw(entry, info);

    if (image_review_time == 0 && AUTO_ETTR_TRIGGER_PHOTO)
        MENU_SET_WARNING(MENU_WARN_NOT_WORKING, "Enable image review from Canon menu.");

    if (is_hdr_bracketing_enabled() && !AUTO_ETTR_TRIGGER_BY_SET)
        MENU_SET_WARNING(MENU_WARN_NOT_WORKING, "Not compatible with HDR bracketing. Use trigger mode SET.");

    if (lv && AUTO_ETTR_TRIGGER_ALWAYS_ON && !get_expsim())
        MENU_SET_WARNING(MENU_WARN_NOT_WORKING, "In LiveView, this requires ExpSim enabled.");
    
    if (is_continuous_drive() && AUTO_ETTR_TRIGGER_PHOTO)
        MENU_SET_WARNING(MENU_WARN_ADVICE, "Not fully compatible with continuous drive.");

    if (auto_ettr)
    {
        MENU_SET_VALUE(
            AUTO_ETTR_TRIGGER_ALWAYS_ON ? "Always ON" : 
            AUTO_ETTR_TRIGGER_AUTO_SNAP ? "Auto Snap" : 
            AUTO_ETTR_TRIGGER_BY_SET && IS_EOS_M ? "Screen DblTap" :
            AUTO_ETTR_TRIGGER_BY_SET && !IS_EOS_M ? "Press SET" :
            AUTO_ETTR_TRIGGER_BY_HALFSHUTTER_DBLCLICK ? "HalfS DBC" : "err"
        );
    }

    if (!AUTO_ETTR_TRIGGER_PHOTO)
    {
        MENU_SET_HELP("Press the shortcut key to optimize the exposure (ETTR).");
    }
    else if (AUTO_ETTR_TRIGGER_ALWAYS_ON)
    {
        if (lv) MENU_SET_HELP("In LiveView, just wait for exposure to settle, then shoot.");
        else MENU_SET_HELP("Take a test picture (underexposed). Next pic will be ETTR.");
    }
    else if (AUTO_ETTR_TRIGGER_AUTO_SNAP)
    {
        MENU_SET_HELP("Press shutter once. ML will take a pic and retry if needed.");
    }
    
    /* recommended: move AF to back button */
    if (auto_ettr && AUTO_ETTR_TRIGGER_BY_HALFSHUTTER_DBLCLICK && !is_manual_focus())
        entry->works_best_in = DEP_CFN_AF_BACK_BUTTON;
    else
        entry->works_best_in = 0;
}

static MENU_UPDATE_FUNC(auto_ettr_max_shutter_update)
{
    MENU_SET_VALUE("%s", ettr_format_shutter(auto_ettr_max_shutter));
    
    if (auto_ettr_max_shutter < SHUTTER_30s)
    {
        MENU_SET_RINFO("BULB");
        MENU_SET_WARNING(MENU_WARN_INFO, "For long exposures, enable bulb timer (maybe also intervalometer).");
    }
    
    if (is_intervalometer_running())
    {
        MENU_SET_WARNING(MENU_WARN_INFO, "Slowest shutter will be limited by interval time minus 2 seconds.");
    }
    
    if (auto_ettr_adjust_mode == 1)
    {
        MENU_SET_WARNING(MENU_WARN_NOT_WORKING, "Adjust shutter speed from top scrollwheel, outside menu.");
    }
}

static MENU_SELECT_FUNC(auto_ettr_max_shutter_toggle)
{
    if (auto_ettr_adjust_mode == 0)
    {
        /* adjust in 0.5 EV steps, from 1/4096 to 4096 seconds */
        const int tv_max = SHUTTER_1_4000;
        const int tv_min = SHUTTER_1s - EXPO_FULL_STOP * 12;
        auto_ettr_max_shutter = MOD(auto_ettr_max_shutter/4*4 - tv_min + delta * 4, tv_max - tv_min + 4) + tv_min;
    }
}

PROP_HANDLER(PROP_GUI_STATE)
{
    if (buf[0] == GUISTATE_QR)
    {
        if (AUTO_ETTR_TRIGGER_PHOTO)
            auto_ettr_step();
    }
}

MENU_UPDATE_FUNC(auto_ettr_intervalometer_warning)
{
    if (auto_ettr && image_review_time == 0 && AUTO_ETTR_TRIGGER_PHOTO)
        MENU_SET_WARNING(MENU_WARN_NOT_WORKING, "Auto ETTR: enable image review from Canon menu.");
}

void auto_ettr_intervalometer_wait()
{
    if (auto_ettr && image_review_time)
    {
        /* make sure auto ETTR has a chance to run (it's triggered by prop handler on QR mode) */
        /* timeout: a bit more than exposure time, to handle long expo noise reduction */
        for (int i = 0; i < raw2shutter_ms(ettr_get_current_raw_shutter())/100; i++)
        {
            if (gui_state == GUISTATE_PLAYMENU || gui_state == GUISTATE_QR) break;
            msleep(150);
        }
        msleep(500);
    }
}

static unsigned int auto_ettr_polling_cbr()
{
    if (lv && NOT_RECORDING && ((void*)&raw_lv_request != (void*)&ret_0))
        auto_ettr_step_lv();
    return 0;
}

static MENU_SELECT_FUNC(debug_info_toggle)
{
    debug_info = !debug_info;
    /* fixme: kinda ugly */
    if (debug_info) console_show();
    else console_hide();
}

static struct menu_entry ettr_menu[] =
{
    {
        .name = "Auto ETTR", 
        .priv = &auto_ettr, 
        .update = auto_ettr_update,
        .max = 1,
        .help  = "Auto expose to the right when you shoot RAW.",
        .submenu_width = 710,
        .children =  (struct menu_entry[]) {
            {
                .name = "Trigger mode",
                .priv = &auto_ettr_trigger,
                .max = 3, // NOTE: Modifed by the module init task to disable ETTR in LV if not supported
                // choices is set in module init because it is dynamic now
                //~ .choices =
                .help  = "When should the exposure be adjusted for ETTR:",
                // help2 is set in module init because it is dynamic now
                //~ .help2 =
            },
            {
                .name = "Slowest shutter",
                .priv = &auto_ettr_max_shutter,
                .select = auto_ettr_max_shutter_toggle,
                .update = auto_ettr_max_shutter_update,
                .min = 16,
                .max = 152,
                .icon_type = IT_PERCENT,
                .help = "Slowest shutter speed for ETTR (longest exposure time)."
            },
            {
                .name = "Exposure target",
                .priv = &auto_ettr_target_level,
                .min = -4,
                .max = 0,
                .choices = CHOICES("-4 EV", "-3 EV", "-2 EV", "-1 EV", "-0.5 EV"),
                .help = "Exposure target for ETTR. Recommended: -0.5 or -1 EV.",
                .advanced = 1,
            },
            {
                .name = "Highlight ignore",
                .priv = &auto_ettr_ignore,
                .min = 0,
                .max = 500,
                .unit = UNIT_PERCENT_x10,
                .icon_type = IT_PERCENT,
                .help  = "How many bright pixels are allowed above the target level.",
                .help2 = "Use this to allow spec(ta)cular highlights to be clipped.",
            },
            {
                .name = "Allow clipping",
                .priv = &auto_ettr_clip,
                .max = 2,
                .choices = CHOICES("OFF", "Green channel", "Any channel"),
                .help = "Choose what color channels are allowed to be clipped.",
                .advanced = 1,
            },
            {
                .name = "Midtone SNR limit",
                .priv = &auto_ettr_midtone_snr_limit,
                .min = 0,
                .max = 8,
                .choices = CHOICES("OFF", "1 EV", "2 EV", "3 EV", "4 EV", "5 EV", "6 EV", "7 EV", "8 EV"),
                .help  = "Stop underexposing when at least half of the image gets",
                .help2 = "noisier than selected SNR => will clip more highlights.",
                .depends_on = DEP_MANUAL_ISO,
            },
            {
                .name = "Shadow SNR limit",
                .priv = &auto_ettr_shadow_snr_limit,
                .min = 0,
                .max = 6,
                .choices = CHOICES("OFF", "1 EV", "2 EV", "3 EV", "4 EV", "5 EV", "6 EV"),
                .help  = "Stop underexposing when at least 5% of the image gets",
                .help2 = "noisier than selected SNR => will clip more highlights.",
                .depends_on = DEP_MANUAL_ISO,
            },
            {
                .name = "Link to Canon shutter",
                .priv = &auto_ettr_adjust_mode,
                .max = 1,
                .help = "Hack to adjust slowest shutter from main dial.",
                .advanced = 1,
            },
            {
                .name = "Link to Dual ISO",
                .priv = &auto_ettr_dual_iso_link,
                .max = 1,
                .help  = "Let ETTR change DualISO settings so you get the SNR values",
                .help2 = "in mids & shadows. It will disable dual ISO if not needed.",
            },
            {
                .name = "Show metered areas",
                .priv = &show_metered_areas,
                .max = 1,
                .help =  "Show where the white point and the SNR levels are metered",
                .help2 = "(what exactly is considered highlight, midtone and shadow).",
                .advanced = 1,
            },
            {
                .name = "Allow beeps",
                .priv = &auto_ettr_allow_beeps,
                .max = 1,
                .help =  "Make status beeps (1 = OK, 2 = need more pictures, 3 = error).",
                .advanced = 1,
            },
            {
                .name = "Show debug info",
                .priv = &debug_info,
                .select = debug_info_toggle,
                .max = 1,
                .help = "For camera nerds.",
                .advanced = 1,
            },
            MENU_ADVANCED_TOGGLE,
            MENU_EOL,
        },
    },
};

static const char * trigger_choices_eosm[] = {"Always ON", "Auto Snap", "Screen DblTap", "HalfS DblClick"};
static const char * trigger_choices_others[] = {"Always ON", "Auto Snap", "Press SET", "HalfS DblClick"};

static const char * trigger_help_eosm = 
    "Always ON: when you take a pic, or continuously in LiveView\n"
    "Auto Snap: after u take a pic,trigger another pic if needed\n"
    "Screen DblTap: meter for ETTR when you tap the screen twice\n"
    "HalfS DblClick: meter for ETTR when pressing halfshutter 2x\n";

static const char * trigger_help_others = 
    "Always ON: when you take a pic, or continuously in LiveView\n"
    "Auto Snap: after u take a pic,trigger another pic if needed\n"
    "Press SET: meter for ETTR when you press SET (LiveView)\n"
    "HalfS DblClick: meter for ETTR when pressing halfshutter 2x\n";

static unsigned int ettr_init()
{
    if ((void*)&raw_lv_request == (void*)&ret_0)
    {
        auto_ettr_trigger  = auto_ettr_trigger > 1 ? 0 : auto_ettr_trigger;
        ettr_menu[0].children[0].max = 1;
    }

    // Modify menu for the EOS M
    if (IS_EOS_M)
    {
        ettr_menu[0].children[0].choices = trigger_choices_eosm;
        ettr_menu[0].children[0].help2 = trigger_help_eosm;
    }
    else
    {
        ettr_menu[0].children[0].choices = trigger_choices_others;
        ettr_menu[0].children[0].help2 = trigger_help_others;
    }

    menu_add("Expo", ettr_menu, COUNT(ettr_menu));

    return 0;
}

static unsigned int ettr_deinit()
{
    return 0;
}

MODULE_INFO_START()
    MODULE_INIT(ettr_init)
    MODULE_DEINIT(ettr_deinit)
MODULE_INFO_END()

MODULE_CBRS_START()
    MODULE_CBR(CBR_VSYNC_SETPARAM, auto_ettr_vsync_cbr, 0)
    MODULE_CBR(CBR_KEYPRESS, auto_ettr_keypress_cbr, 0)
    MODULE_CBR(CBR_SHOOT_TASK, auto_ettr_polling_cbr, 0)
MODULE_CBRS_END()

MODULE_PROPHANDLERS_START()
    MODULE_PROPHANDLER(PROP_GUI_STATE)
MODULE_PROPHANDLERS_END()

MODULE_CONFIGS_START()
    MODULE_CONFIG(auto_ettr)
    MODULE_CONFIG(auto_ettr_trigger)
    MODULE_CONFIG(auto_ettr_ignore)
    MODULE_CONFIG(auto_ettr_target_level)
    MODULE_CONFIG(auto_ettr_max_shutter)
    MODULE_CONFIG(auto_ettr_clip)
    MODULE_CONFIG(auto_ettr_adjust_mode)
    MODULE_CONFIG(auto_ettr_midtone_snr_limit)
    MODULE_CONFIG(auto_ettr_shadow_snr_limit)
    MODULE_CONFIG(auto_ettr_dual_iso_link)
    MODULE_CONFIG(auto_ettr_allow_beeps)
MODULE_CONFIGS_END()
back to top