/** * Copyright 2019 The Google Research Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.googleresearch.capturesync; import android.os.Handler; import android.util.Log; import com.googleresearch.capturesync.softwaresync.phasealign.PhaseAligner; import com.googleresearch.capturesync.softwaresync.phasealign.PhaseConfig; import com.googleresearch.capturesync.softwaresync.phasealign.PhaseResponse; /** * Calculates and adjusts camera phase by inserting frames of varying exposure lengths. * * <p>Phase alignment is an iterative process. Running for more iterations results in higher * accuracy up to the stability of the camera and the accuracy of the phase alignment configuration * values. */ public class PhaseAlignController { public static final String INJECT_FRAME = "injection_frame"; private static final String TAG = "PhaseAlignController"; // Maximum number of phase alignment iteration steps in the alignment process. // TODO(samansari): Make this a parameter that you pass in to this class. Then make the class that // constructs this pass the constant in. private static final int MAX_ITERATIONS = 5; // Delay after an alignment step to wait for phase to settle before starting the next iteration. private static final int PHASE_SETTLE_DELAY_MS = 400; private final MainActivity context; private final Handler handler; private final Object lock = new Object(); private boolean inAlignState = false; private final PhaseAligner phaseAligner; private PhaseResponse latestResponse; public PhaseAlignController(PhaseConfig config, MainActivity context) { handler = new Handler(); phaseAligner = new PhaseAligner(config); Log.v(TAG, "Loaded phase align config."); this.context = context; } /** * Update the latest phase response from the latest frame timestamp to keep track of phase. * * <p>The timestamp is nanoseconds in the synchronized leader clock domain. * * @return phase of timestamp in nanoseconds in the same domain as given. */ public long updateCaptureTimestamp(long timestampNs) { // TODO(samansaari) : Rename passTimestamp -> updateCaptureTimestamp or similar in softwaresync. latestResponse = phaseAligner.passTimestamp(timestampNs); // TODO (samansari) : Pull this into an interface/callback. context.runOnUiThread(() -> context.updatePhaseTextView(latestResponse.diffFromGoalNs())); return latestResponse.phaseNs(); } /** Submit an frame with a specific exposure to offset future frames and align phase. */ private void doPhaseAlignStep() { Log.i( TAG, String.format( "Current Phase: %.3f ms, Diff: %.3f ms, inserting frame exposure %.6f ms, lower bound" + " %.6f ms.", latestResponse.phaseNs() * 1e-6f, latestResponse.diffFromGoalNs() * 1e-6f, latestResponse.exposureTimeToShiftNs() * 1e-6f, phaseAligner.getConfig().minExposureNs() * 1e-6f)); // TODO(samansari): Make this an interface. context.injectFrame(latestResponse.exposureTimeToShiftNs()); } public void startAlign() { synchronized (lock) { if (inAlignState) { Log.i(TAG, "startAlign() called while already aligning."); return; } inAlignState = true; // Start inserting frames every {@code PHASE_SETTLE_DELAY_MS} ms to try and push the phase to // the goal phase. Stop after aligned to threshold or after {@code MAX_ITERATIONS}. handler.post(() -> work(MAX_ITERATIONS)); } } private void work(int iterationsLeft) { // Check if Aligned / Not Aligned but able to iterate / Ran out of iterations. if (latestResponse.isAligned()) { // Aligned. Log.i( TAG, String.format( "Reached: Current Phase: %.3f ms, Diff: %.3f ms", latestResponse.phaseNs() * 1e-6f, latestResponse.diffFromGoalNs() * 1e-6f)); synchronized (lock) { inAlignState = false; } Log.d(TAG, "Aligned."); } else if (!latestResponse.isAligned() && iterationsLeft > 0) { // Not aligned but able to run another alignment iteration. doPhaseAlignStep(); Log.v(TAG, "Queued another phase align step."); // TODO (samansari) : Replace this brittle delay-based solution to a response-based one. handler.postDelayed( () -> work(iterationsLeft - 1), PHASE_SETTLE_DELAY_MS); // Try again after it settles. } else { // Reached max iterations before aligned. Log.i( TAG, String.format( "Failed to Align, Stopping at: Current Phase: %.3f ms, Diff: %.3f ms", latestResponse.phaseNs() * 1e-6f, latestResponse.diffFromGoalNs() * 1e-6f)); synchronized (lock) { inAlignState = false; } Log.d(TAG, "Finishing alignment, reached max iterations."); } } }