// BEGIN_RECORD
    private void onBeginRecordMessage(Dictionary <string, object> args)
    {
        Logger.Log("onBeginRecordMessage");
        int sentenceIndex = StorybookStateManager.GetState().evaluatingSentenceIndex;

        this.taskQueue.Enqueue(this.recordAudioForCurrentSentence(sentenceIndex));
    }
Пример #2
0
    // Send a message representing storybook state to the controller, in a new thread.
    // Doesn't need to return Action because it's only used as a timer elapsed handler.
    private void sendStorybookState(object _, System.Timers.ElapsedEventArgs __)
    {
        Dictionary <string, object> publish = new Dictionary <string, object>();

        publish.Add("topic", Constants.STORYBOOK_STATE_TOPIC);
        publish.Add("op", "publish");

        // Note that this is protected by a lock, so although ROS messages could
        // send out of order, the information within them will be consistent.
        // And if the sending rate isn't too high, the likelihood of out of order messages
        // is low, and inconsequential for the controller anyway.
        // TODO: should devise a better scheme to make sure states are sent in order.
        // Can also use the sequence numbers provided in the header.
        // Or use a lock in this class so that only one state message can be sent at a time.
        Dictionary <string, object> data = StorybookStateManager.GetRosMessageData();

        data.Add("header", RosbridgeUtilities.GetROSHeader());
        // Don't allow audio_file to be null, ROS will get upset.
        if (data["audio_file"] == null)
        {
            data["audio_file"] = "";
        }
        publish.Add("msg", data);

        bool success = this.rosClient.SendMessage(Json.Serialize(publish));

        if (!success)
        {
            // Logger.Log("Failed to send StorybookState message: " + Json.Serialize((publish)));
        }
    }
    // Send a message representing storybook state to the controller.
    // Doesn't need to return Action because it's only used as a timer elapsed handler.
    private void sendStorybookState(object _, System.Timers.ElapsedEventArgs __)
    {
        Dictionary <string, object> publish = new Dictionary <string, object>();

        publish.Add("topic", Constants.STORYBOOK_STATE_TOPIC);
        publish.Add("op", "publish");

        // TODO: could devise a better scheme to make sure states are sent in order.
        // Can also use the sequence numbers provided in the header. Probably overkill.
        Dictionary <string, object> data = StorybookStateManager.GetRosMessageData();

        data.Add("header", RosbridgeUtilities.GetROSHeader());
        // Don't allow audio_file to be null, ROS will get upset.
        if (data["audio_file"] == null)
        {
            data["audio_file"] = "";
        }
        publish.Add("msg", data);

        bool success = this.rosClient.SendMessage(Json.Serialize(publish));

        if (!success)
        {
            // Logger.Log("Failed to send StorybookState message: " + Json.Serialize((publish)));
        }
    }
 private Action showNextSentence(bool childTurn, bool shouldRecord)
 {
     return(() => {
         Logger.Log("Showing next sentence from inside task queue");
         if (StorybookStateManager.GetState().evaluatingSentenceIndex + 1 <
             this.storyManager.stanzaManager.GetNumSentences())
         {
             StorybookStateManager.IncrementEvaluatingSentenceIndex();
             int newIndex = StorybookStateManager.GetState().evaluatingSentenceIndex;
             Color color = new Color();
             if (childTurn)
             {
                 color = Constants.CHILD_READ_TEXT_COLOR;
             }
             else
             {
                 color = Constants.JIBO_READ_TEXT_COLOR;
             }
             this.storyManager.stanzaManager.GetSentence(newIndex).FadeIn(color);
             if (newIndex - 1 >= 0)
             {
                 this.storyManager.stanzaManager.GetSentence(newIndex - 1).ChangeTextColor(Constants.GREY_TEXT_COLOR);
             }
             if (shouldRecord)
             {
                 Logger.Log("shouldRecord is true in showNextSentence");
                 this.recordAudioForCurrentSentence(newIndex).Invoke();
             }
         }
         else
         {
             throw new Exception("Cannot show sentence, index out of range");
         }
     });
 }
 private Action setStorybookMode(int mode)
 {
     return(() => {
         // Convert to StorybookMode.
         StorybookMode newMode = (StorybookMode)mode;
         StorybookStateManager.SetStorybookMode(newMode);
     });
 }
    // Helper function to wrap together two actions:
    // (1) loading a page and (2) sending the StorybookPageInfo message over ROS.
    private void loadPageAndSendRosMessage(SceneDescription sceneDescription)
    {
        // Load the page.
        this.storyManager.LoadPage(sceneDescription);

        // Send the ROS message to update the controller about what page we're on now.
        StorybookPageInfo updatedInfo = new StorybookPageInfo();

        updatedInfo.storyName  = this.currentStory.GetName();
        updatedInfo.pageNumber = this.currentPageNumber;
        updatedInfo.sentences  = this.storyManager.stanzaManager.GetAllSentenceTexts();

        // Update state (will get automatically sent to the controller.
        StorybookStateManager.SetStorySelected(this.currentStory.GetName(),
                                               this.currentStory.GetNumPages());

        // Gather information about scene objects.
        StorybookSceneObject[] sceneObjects =
            new StorybookSceneObject[sceneDescription.sceneObjects.Length];
        for (int i = 0; i < sceneDescription.sceneObjects.Length; i++)
        {
            SceneObject          so  = sceneDescription.sceneObjects[i];
            StorybookSceneObject sso = new StorybookSceneObject();
            sso.id          = so.id;
            sso.label       = so.label;
            sso.inText      = so.inText;
            sceneObjects[i] = sso;
        }
        updatedInfo.sceneObjects = sceneObjects;

        // Gather information about tinker texts.
        StorybookTinkerText[] tinkerTexts =
            new StorybookTinkerText[this.storyManager.tinkerTexts.Count];
        for (int i = 0; i < this.storyManager.tinkerTexts.Count; i++)
        {
            TinkerText          tt  = this.storyManager.tinkerTexts[i].GetComponent <TinkerText>();
            StorybookTinkerText stt = new StorybookTinkerText();
            stt.word           = tt.word;
            stt.hasSceneObject = false;
            stt.sceneObjectId  = -1;
            tinkerTexts[i]     = stt;
        }
        foreach (Trigger trigger in sceneDescription.triggers)
        {
            if (trigger.type == TriggerType.CLICK_TINKERTEXT_SCENE_OBJECT)
            {
                tinkerTexts[trigger.args.textId].hasSceneObject = true;
                tinkerTexts[trigger.args.textId].sceneObjectId  = trigger.args.sceneObjectId;
            }
        }
        updatedInfo.tinkerTexts = tinkerTexts;

        // Send the message.
        if (Constants.USE_ROS)
        {
            this.rosManager.SendStorybookPageInfoAction(updatedInfo);
        }
    }
Пример #7
0
    // Use Update for handling when to trigger actions in other objects.
    private void Update()
    {
        if (this.audioSource.isPlaying)
        {
            // Check our current timestamp, and compare against timestamps of
            // the triggers we have, in order to cause specific actions to happen.
            this.currentTimestamp = this.audioSource.time;
            float maxCutoffTime = this.currentTimestamp;
            float minCutoffTime = Math.Max(this.lastTimestamp, this.startTimestamp);
            // Watch for special case where the audio has finished and we need to
            // make sure we call any outstanding triggers.
            if (this.currentTimestamp < this.lastTimestamp)
            {
                maxCutoffTime = float.MaxValue;
            }
            foreach (KeyValuePair <float, List <AudioTrigger> > trigger in this.triggers)
            {
                // TODO: need a special case for first one? but not for hungry toad
                if (trigger.Key >= minCutoffTime &&
                    trigger.Key <= maxCutoffTime)
                {
                    // Invoke this trigger's action.
                    foreach (AudioTrigger t in trigger.Value)
                    {
                        if (!t.disallowInvokePastStop)
                        {
                            t.action();
                        }
                        else
                        {
                            // Only invoke if current time has not past stop time.
                            if (this.currentTimestamp <= this.stopTimestamp)
                            {
                                t.action();
                            }
                            else
                            {
                                Logger.Log("don't do trigger action because first in stanza");
                            }
                        }
                    }
                }
            }
            if (this.currentTimestamp > this.stopTimestamp)
            {
                Logger.Log("stopping because current is " + this.currentTimestamp + " and stop is " + this.stopTimestamp);
                this.StopAudio();
            }
        }
        this.lastTimestamp = this.currentTimestamp;

        // Update audio state so that StorybookState ROS messages are accurate.
        bool playing = this.audioSource.isPlaying;

        StorybookStateManager.SetAudioState(playing, this.audioFileName);
    }
    // Constructor.
    public RosManager(string rosIP, string portNum, GameController gameController)
    {
        Logger.Log("RosManager constructor");
        this.gameController        = gameController;
        this.storybookStateManager = gameController.GetStorybookStateManager();

        this.rosClient = new RosbridgeWebSocketClient(rosIP, portNum);
        this.rosClient.receivedMsgEvent += this.onMessageReceived;
        this.commandHandlers             = new Dictionary <StorybookCommand, Action <Dictionary <string, object> > >();
    }
 private void finishStory()
 {
     this.storyManager.ClearPage();
     this.storyManager.audioManager.StopAudio();
     this.storyManager.ShowTheEndPage(false);
     this.currentPageNumber = 0;
     this.setLandscapeOrientation();
     this.showLibraryPanel(true);
     StorybookStateManager.SetStoryExited();
 }
    void Start()
    {
        // Set up all UI elements. (SetActive, GetComponent, etc.)
        // Get references to objects if necessary.
        Logger.Log("Game Controller start");
        this.landscapeNextButton.interactable = true;
        this.landscapeNextButton.onClick.AddListener(this.onNextButtonClick);
        this.portraitNextButton.interactable = true;
        this.portraitNextButton.onClick.AddListener(this.onNextButtonClick);

        this.landscapeBackButton.interactable = true;
        this.landscapeBackButton.onClick.AddListener(this.onBackButtonClick);
        this.portraitBackButton.interactable = true;
        this.portraitBackButton.onClick.AddListener(this.onBackButtonClick);

        this.landscapeFinishButton.interactable = true;
        this.landscapeFinishButton.onClick.AddListener(this.onFinishButtonClick);
        this.portraitFinishButton.interactable = true;
        this.portraitFinishButton.onClick.AddListener(this.onFinishButtonClick);

        this.rosConnectButton.onClick.AddListener(this.onRosConnectButtonClicked);
        this.enterLibraryButton.onClick.AddListener(this.onEnterLibraryButtonClicked);
        this.startStoryButton.onClick.AddListener(this.onStartStoryClicked);

        this.landscapeToggleAudioButton.onClick.AddListener(this.toggleAudio);
        this.portraitToggleAudioButton.onClick.AddListener(this.toggleAudio);

        // Update the sizing of all of the panels depending on the actual
        // screen size of the device we're on.
        this.resizePanelsOnStartup();

        this.storyPages = new List <SceneDescription>();

        this.storybookStateManager = new StorybookStateManager();

        this.storyManager     = GetComponent <StoryManager>();
        this.assetManager     = GetComponent <AssetManager>();
        this.audioRecorder    = GetComponent <AudioRecorder>();
        this.speechAceManager = GetComponent <SpeechAceManager>();

        this.stories = new List <StoryMetadata>();
        this.initStories();

        // Either show the rosPanel to connect to ROS, or wait to go into story selection.

        if (Constants.USE_ROS)
        {
            this.setupRosScreen();
        }

        // TODO: figure out when to actually set this, should be dependent on game mode.
        this.storyManager.SetAutoplay(false);
    }
 // SHOW_NEXT_SENTENCE
 private void onShowNextSentenceMessage(Dictionary <string, object> args)
 {
     // Assert that we are highlighting the appropriate sentence.
     // Need to cast better.
     Logger.Log("onShowNextSentenceMessage");
     if (Convert.ToInt32(args["index"]) != StorybookStateManager.GetState().evaluatingSentenceIndex + 1)
     {
         Logger.LogError("Sentence index doesn't match " + args["index"] + " " +
                         StorybookStateManager.GetState().evaluatingSentenceIndex + 1);
         throw new Exception("Sentence index doesn't match, fail fast");
     }
     this.taskQueue.Enqueue(this.showNextSentence((bool)args["child_turn"], (bool)args["record"]));
 }
 private void goToNextPage()
 {
     this.currentPageNumber += 1;
     if (this.currentPageNumber > StorybookStateManager.GetState().numPages)
     {
         throw new Exception("Cannot go forward anymore, already at end " + this.currentPageNumber + " " + StorybookStateManager.GetState().numPages);
     }
     this.storyManager.ClearPage();
     StorybookStateManager.ResetEvaluatingSentenceIndex();
     // Explicitly send the state to make sure it gets sent before the page info does.
     if (Constants.USE_ROS)
     {
         this.rosManager.SendStorybookState();
     }
     this.loadPageAndSendRosMessage(this.storyPages[this.currentPageNumber]);
 }
    private int currentPageNumber = 0; // 0-indexed, index into this.storyPages, 0 is title page.

    void Awake()
    {
        // Enforce singleton pattern.
        if (instance == null)
        {
            instance = this;
        }
        else if (instance != this)
        {
            Logger.Log("duplicate GameController, destroying");
            Destroy(gameObject);
        }
        DontDestroyOnLoad(gameObject);

        // Do this in Awake() to avoid null references.
        StorybookStateManager.Init();
    }
    // =================
    // Helpers.
    // =================

    // Separate the logic of showing buttons from actually moving pages.
    // In evaluate mode, we want to be able to instruct the tablet to navigate the pages
    // without the child needing to press any buttons.
    private void goToPrevPage()
    {
        this.currentPageNumber -= 1;
        if (this.currentPageNumber < 0)
        {
            // Fail fast.
            throw new Exception("Cannot go back any farther, already at beginning");
        }
        this.storyManager.ClearPage();
        StorybookStateManager.ResetEvaluatingSentenceIndex();
        // Explicitly send the state to make sure it gets sent before the page info does.
        if (Constants.USE_ROS)
        {
            this.rosManager.SendStorybookState();
        }
        this.loadPageAndSendRosMessage(this.storyPages[this.currentPageNumber]);
    }
 private void loadFirstPage()
 {
     if (Constants.USE_ROS)
     {
         this.rosManager.SendStorybookLoaded().Invoke();
     }
     this.loadPageAndSendRosMessage(this.storyPages[this.currentPageNumber]);
     this.showLibraryPanel(false);
     this.hideElement(this.loadingBar);
     this.showElement(this.nextButton.gameObject);
     this.showElement(this.toggleAudioButton.gameObject);
     this.setOrientationView(this.orientation);
     // If in evaluate mode, don't show any navigation buttons.
     if (StorybookStateManager.GetState().storybookMode == StorybookMode.Evaluate)
     {
         this.showNavigationButtons(false);
     }
 }
    public StorybookStateManager()
    {
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            throw new Exception("Cannot attempt to create multiple StorybookStateManagers");
        }

        this.stateLock = new Object();

        // Set default values for start of interaction.
        this.currentState = new StorybookState {
            audioPlaying          = false,
            audioFile             = "",
            storybookMode         = StorybookMode.NotReading,
            currentStory          = "",
            numPages              = 0,
            evaluatingStanzaIndex = -1,
        };
    }
Пример #17
0
    // Upload an audio file to the collected child audio bucket in S3.
    // Argument audioPath should be the same as what was passed into SaveAudioAtPath in AudioRecorder.
    public void S3UploadChildAudio(string audioPath)
    {
        // Use a prefix that includes story, page number, first 2 words of stanza, and date.
        string s3Path = DateTime.Now.ToString("yyyy-MM-dd") + "/" + Constants.PARTICIPANT_ID + "/" +
                        StorybookStateManager.GetState().currentStory + "/" +
                        StorybookStateManager.GetState().storybookMode + "/" +
                        DateTime.Now.ToString("HH:mm:ss") + "_" + audioPath;
        PutObjectRequest request = new PutObjectRequest {
            BucketName = Constants.S3_CHILD_AUDIO_BUCKET,
            Key        = s3Path,
            FilePath   = Application.persistentDataPath + "/" + audioPath
        };

        this.s3Client.PutObjectAsync(request, (responseObj) => {
            if (responseObj.Exception == null)
            {
                Logger.Log("Successful upload " + s3Path);
            }
            else
            {
                Logger.Log("Upload failed");
            }
        });
    }
    // When child is done speaking, get SpeechACE results and save the recording.
    private void stopRecordingAndDoSpeechace()
    {
        int    sentenceIndex = StorybookStateManager.GetState().evaluatingSentenceIndex;
        string text          = this.storyManager.stanzaManager.GetSentence(sentenceIndex).GetSentenceText();
        string tempFileName  = this.currentPageNumber + "_" + sentenceIndex + ".wav";

        this.audioRecorder.EndRecording((clip) => {
            if (clip == null)
            {
                Logger.Log("Got null clip, means user pressed stop recording when no recording was active");
                return;
            }
            Logger.Log("Done recording, getting speechACE results and uploading file to S3...");
            // Tell controller we're done recording!
            if (Constants.USE_ROS)
            {
                this.rosManager.SendRecordAudioComplete(sentenceIndex).Invoke();
            }
            // TODO: should also delete these audio files after we don't need them anymore.
            AudioRecorder.SaveAudioAtPath(tempFileName, clip);
            float duration = clip.length;
            StartCoroutine(this.speechAceManager.AnalyzeTextSample(
                               tempFileName, text, (speechAceResult) => {
                if (Constants.USE_ROS)
                {
                    this.rosManager.SendSpeechAceResultAction(sentenceIndex, text,
                                                              duration, speechAceResult).Invoke();
                }
                // If we want to replay for debugging, uncomment this.
                // AudioClip loadedClip = AudioRecorder.LoadAudioLocal(fileName);
                // this.storyManager.audioManager.LoadAudio(loadedClip);
                // this.storyManager.audioManager.PlayAudio();
                this.assetManager.S3UploadChildAudio(tempFileName);
            }));
        });
    }
Пример #19
0
 public void SetStorybookStateManager(StorybookStateManager manager) {
     this.storybookStateManager = manager;
 }
Пример #20
0
    // Main function to be called by GameController.
    // Passes in a description received over ROS or hardcoded.
    // LoadScene is responsible for loading all resources and putting them in
    // place, and attaching callbacks to created GameObjects, where these
    // callbacks involve functions from SceneManipulatorAPI.
    public void LoadPage(SceneDescription description)
    {
        this.setDisplayModeFromSceneDescription(description);
        this.resetPanelSizes();

        // Only allow swiping in explore mode. TODO: maybe should change this if there's a need.
        Stanza.ALLOW_SWIPE = StorybookStateManager.GetState().storybookMode == StorybookMode.Explore;

        // Load audio.
        this.audioManager.LoadAudio(description.audioFile, this.assetManager.GetAudioClip(description.audioFile));

        if (description.isTitle)
        {
            // Show only the title panel.
            this.titlePanel.SetActive(true);
            this.readerPanel.SetActive(false);

            // Special case for title page.
            // No TinkerTexts, and image takes up a larger space.
            this.loadTitlePage(description);
        }
        else
        {
            // Show only the text and graphics panels.
            this.titlePanel.SetActive(false);
            this.readerPanel.SetActive(true);

            // Load image.
            this.loadImage(description.storyImageFile);

            List <string> textWords =
                new List <string>(description.text.Split(' '));
            // Need to remove any empty or only punctuation words.
            textWords.RemoveAll(String.IsNullOrEmpty);
            List <string> filteredTextWords = new List <string>();
            foreach (string word in textWords)
            {
                if (Util.WordHasNoAlphanum(word))
                {
                    filteredTextWords[filteredTextWords.Count - 1] += word;
                }
                else
                {
                    filteredTextWords.Add(word);
                }
            }
            if (filteredTextWords.Count != description.timestamps.Length)
            {
                Logger.LogError("textWords doesn't match timestamps length " +
                                filteredTextWords.Count + " " + description.timestamps.Length);
            }
            // Depending on how many words there are, update the sizing and spacing heuristically.
            this.resizeSpacingAndFonts(filteredTextWords.Count);

            for (int i = 0; i < filteredTextWords.Count; i++)
            {
                this.loadTinkerText(i, filteredTextWords[i], description.timestamps[i],
                                    i == filteredTextWords.Count - 1);
            }

            // After all TinkerTexts and Stanzas have been formatted, set up all the sentences and
            // set the stanza swipe handlers.
            this.stanzaManager.SetupSentences();
            // If we are in evaluate mode, all stanzas should be hidden by default.
            if (StorybookStateManager.GetState().storybookMode == StorybookMode.Evaluate)
            {
                this.stanzaManager.HideAllSentences();
            }
            // This will send StorybookEvent ROS messages to the controller when stanzas are swiped.
            if (Constants.USE_ROS)
            {
                this.stanzaManager.SetSentenceSwipeHandlers();
            }

            // Load audio triggers for TinkerText.
            this.loadAudioTriggers();
        }

        // Load all scene objects.
        foreach (SceneObject sceneObject in description.sceneObjects)
        {
            this.loadSceneObject(sceneObject);
        }

        // Sort scene objects by size (smallest on top).
        this.sortSceneObjectLayering();

        // Load triggers.
        foreach (Trigger trigger in description.triggers)
        {
            this.loadTrigger(trigger);
        }

        if (this.autoplayAudio)
        {
            this.audioManager.PlayAudio();
        }
    }