🌐 한국어

    ADAS API#

    Oasis는 차선 감지, 앞차 출발, 앞차 충돌 경고, 차선 이탈 등의 ADAS 기능을 지원합니다.

    헤더 파일#

    OasisMedia.h

    설명#

    통합 녹화 또는 개별 녹화 객체 생성시 channel<N>-enable-adaschannel<N>-adas-inference-model-names 키 값을 지정하여 ADAS를 활성화합니다.

    주행 중 차 속도는 setVehicleSpeedsetMultiChannelVehicleSpeed 함수를 호출하여 실시간으로 반영됩니다.

    createInferenceEngine을 호출하여 모델 파일을 로딩한 후 ADAS를 사용할 수 있습니다.

    예제#

    아래는 통합녹화 객체를 생성하여 한 채널에 대해서 ADAS를 사용하는 예입니다.

    Oasis Inference Engine을 초기화 합니다. nef_path는 모델 파일 경로입니다. 모델 파일은 각 칩셋의 NPU에 탑재되어 사용됩니다.

    parameters["model-path"] = nef_path;
    createInferenceEngine(parameters);
    

    통합 녹화 객체 생성시 패러미터에 ADAS를 활성화합니다.

    parameters["channel-count"] = "1";
    
    parameters["channel1-camera-id"] = std::to_string(camera_id);
    parameters["channel1-ise-id"] = "-1";
    parameters["channel1-resolution"] = "1080p";
    parameters["channel1-bitrate"] = "8000000";
    parameters["channel1-fps"] = "30";
    parameters["channel1-file-framerate"] = "30";
    parameters["channel1-venc-framerate"] = "30";
    parameters["channel1-venc-keyframe-interval"] = "30";
    parameters["channel1-vencoder-type"] = "h265";
    parameters["channel1-h264-profile"] = "main";
    
    parameters["channel1-enable-adas"] = "1";
    parameters["channel1-adas-inference-model-names"] = "ufld,yolov5";
    

    MediaObserver에서 유도된 사용자 정의 인터페이스에 ADAS 관련 콜백 함수를 정의합니다.

    virtual void onAdasForwardVehicleCollisionWarning(void *user_data) {
      fprintf(stdout, ">>> Adas collision warning!\n");
    }
    
    virtual void onAdasForwardVehicleDepartueWarning(void *user_data) {
      fprintf(stdout, ">>> Adas vehicle departure warning!\n");
    }
    
    virtual void onAdasLaneDepartureWarning(void *user_data, int32_t land_side) {
      fprintf(stdout, ">>> Adas %s lane departure warning!\n", land_side==0?"left":"right");
    }
    
    virtual void onAdasForwardPedestrianWarning(void *user_data) {
      fprintf(stdout, ">>> Adas pedestrian warning!\n");
    }
    

    타이머 쓰레드에서 주기적으로 속도를 설정합니다. 실제 사용할 경우, GPS 등 데이터로 부터 속도를 얻어내어 설정합니다.

    setVehicleSpeed(recorder, 60.f);
    

    아래는 전체 코드입니다.

    #include "OasisAPI.h"
    #include "OasisLog.h"
    #include "OasisMedia.h"
    #include "OasisFS.h"
    #include "OasisUtil.h"
    #include "OasisMotion.h"
    #include "OasisInference.h"
    
    #include "common.h"
    
    
    #include <thread>
    #include <mutex>
    #include <memory>
    #include <condition_variable>
    
    #include <signal.h>
    
    #define DLOG_THIS   0x00010000
    #define DLOG_RECORD 0x00020000
    #define DLOG_FRAME  0x00040000
    #define DLOG_TRACE  0x00080000
    
    #undef DLOG_FLAGS
    #define  DLOG_FLAGS (DLOG_FLAGS_DEFAULT|DLOG_RECORD|DLOG_TRACE|DLOG_THIS/*|DLOG_FRAME*/)
    
    #undef DLOG_TAG
    #define DLOG_TAG "ADAS"
    
    #define OFFS_QUEUE_SIZE_KBYTES  (20*1024)
    #define OFFS_CACHE_SIZE_KBYTES  (1024)
    #define OFFS_WRITE_ALIGNMENT_BYTES  8192
    #define MEDIA_CACHE_SIZE_KBYTES (40*1024)
    
    
    extern "C" int getch(const char *prompt);
    
    using namespace oasis;
    
    static bool timer_running = true;
    static bool continue_recording = true;
    
    enum {
        kStartEventRecording = 1,
        kRecorderExitNow = 2,
    };
    
    struct RecorderCommand {
        int32_t cmd_;
        RecorderCommand() = default;
        RecorderCommand(int32_t cmd) : cmd_(cmd) {}
        RecorderCommand(const RecorderCommand& src) : cmd_(src.cmd_) {}
        RecorderCommand& operator=(const RecorderCommand& src) {
            if(this != &src) {
                cmd_ = src.cmd_;
            }
            return *this;
        }
    };
    
    
    static std::condition_variable cmdq_cond_;
    static std::mutex cmdq_lock_;
    static std::list<RecorderCommand> cmdq_;
    
    
    static void postCommand(int32_t cmd) {
        try {
            std::lock_guard<std::mutex> _l(cmdq_lock_);
            cmdq_.emplace_back(cmd);
            cmdq_cond_.notify_one();
        } catch(...) {
    
        }
    }
    
    static uint8_t strd_data_header_[24] = {0, };
    
    class MyRecordingObserver : public MediaObserver {
        public:
        virtual void onStarted(void* user_data, const char* file_path) {
            DLOG(DLOG_RECORD|DLOG_INFO, "Recording started, file path \"%s\"\r\n", file_path);
        }
    
        virtual void onStopped(void* user_data, media_report_reason_t reason, void* details) {
            DLOG(DLOG_RECORD|DLOG_INFO, "Recording stopped, reason: %d(%s)\r\n", reason, mediaReportReasonString(reason));
            if(reason != kMediaReportNoError) {
                continue_recording = false;
            }
        }
    
        virtual void onFileChanged(void* user_data, const char* new_file_path) {
            DLOG(DLOG_RECORD|DLOG_INFO, "New file used: \"%s\"\r\n", new_file_path);
        }
    
        virtual void onFileDeleted(void* user_data, const char* deleted_file_path) {
            DLOG(DLOG_RECORD|DLOG_INFO, "Oldest file used: \"%s\"\r\n", deleted_file_path);
        }
    
        virtual void onError(void* user_data, media_report_reason_t reason, void* details) {
            if(reason == kRecordingErrorMediaDataTimeout) {
                int32_t camera_id = (intptr_t)details;
                DLOG(DLOG_RECORD|DLOG_ERROR, "Recording error, reason: %d(%s), camera<%d>\r\n", reason, mediaReportReasonString(reason), camera_id);
            } else if(reason == kRecordingErrorMediaCacheReadErrorBegin) {
                DLOG(DLOG_RECORD|DLOG_ERROR, "Recording error, reason: %d(%s) ===> stopping\r\n", reason, mediaReportReasonString(reason));
            } else if(reason == kRecordingErrorMediaCacheReadErrorEnd) {
                DLOG(DLOG_RECORD|DLOG_ERROR, "Recording error, reason: %d(%s)\r\n", reason, mediaReportReasonString(reason));
            }
    
            continue_recording = false;
        }
    
        virtual void onInfo(void* user_data, MediaInfo* info) {
            DLOG(DLOG_RECORD, "Recording duration %lld.%06lld sec\r\n", info->durationUs / 1000000ll, info->durationUs % 1000000ll);
        }
    
        virtual void onInfoEx(void* user_data, MediaInfoEx* info) {
            dumpInfoEx(info);
        }
    
        //player paused and resumed
        virtual void onPaused(void *user_data, MediaInfo *info) {}
        virtual void onResumed(void *user_data, MediaInfo *info) {}
        virtual void onEventRecordingStarted(void* user_data, media_report_reason_t reason, const char* file_path) {
            DLOG0(DLOG_THIS, "Event Recording started @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
        }
        virtual void onEventRecordingCompleted(void* user_data, media_report_reason_t reason, const char* file_path) {
            DLOG0(DLOG_THIS, "Event Recording completed @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
        }
        virtual void onMotionRecordingStarted(void *user_data, media_report_reason_t reason, const char *file_path) {
            DLOG0(DLOG_THIS, "Motion Recording started @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
        }
        virtual void onMotionRecordingCompleted(void *user_data, media_report_reason_t reason, const char *file_path) {
            DLOG0(DLOG_THIS, "Motion Recording completed @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
        }
    
        virtual void queryNewFilePath(void* user_data, oasis::recording_mode_t file_type, bool rear_camera_on, bool sound_on, std::string& file_path) {}
        virtual void onSnapshotCompleted(void *user_data, uint32_t snapshot_id, int32_t camera_id, int error, const std::vector<char> &image_data, const struct timeval &timestamp) {}
    
        virtual void onAdasForwardVehicleCollisionWarning(void *user_data) {
            fprintf(stdout, ">>> Adas collision warning!\n");
        }
    
        virtual void onAdasForwardVehicleDepartueWarning(void *user_data) {
            fprintf(stdout, ">>> Adas vehicle departure warning!\n");
        }
    
        virtual void onAdasLaneDepartureWarning(void *user_data, int32_t land_side) {
            fprintf(stdout, ">>> Adas %s lane departure warning!\n", land_side==0?"left":"right");
        }
    
        virtual void onAdasForwardPedestrianWarning(void *user_data) {
            fprintf(stdout, ">>> Adas pedestrian warning!\n");
        }
    
    };
    
    
    void print_usage(const char *pname)
    {
        DLOG0(DLOG_INFO, "USAGE: %s [-n <nef path>] [<-o|-t> <dir>]\n", pname);
    }
    
    void cancel_handler(int signum)
    {
        timer_running = false;
        continue_recording = false;
    }
    
    #if 0
    #define SND_PATH "default"
    #else
    #define SND_PATH "hw:0,0"
    #endif
    
    int main(int argc, char* argv[])
    {
        int32_t err;
        int32_t i, c, n;
        std::thread t1, t2, t3;
        int32_t camera_id = 0;
        bool use_offs = false;
        std::string offs_dir = "/mnt/sd";
        std::string tmp_dir = "/tmp";
        const char *forward_url = nullptr;
    
        oasis::key_value_map_t parameters;
    
        bool nef_path_valid = false;
        std::string nef_path = "/mnt/sd/model.nef";
    
        srand(time(NULL));
    
        opterr = 0;
        while ((c = getopt(argc, argv, "n:o:t:x:h")) != -1) {
            switch (c) {
            case 'n':
                if(optarg == nullptr) {
                    fprintf(stderr, "error: bad option argument '%c'\n", optopt);
                    return -1;
                }
                if(access(optarg, F_OK)) {
                    fprintf(stderr, "nef file not found: %s\n", optarg);
                    return -1;
                }
                nef_path = optarg;
                nef_path_valid = true;
                break;
            case 'x':
                if(optarg == nullptr) {
                    print_usage(argv[0]);
                    return -1;
                }
                forward_url = optarg;
                break;
            case 'o':
                if(optarg == nullptr) {
                    fprintf(stderr, "error: bad option argument '%c'\n", optopt);
                    return -1;
                }
                offs_dir = optarg;
                if(!isDirectory(offs_dir.c_str())) {
                    fprintf(stderr, "error: bad option argument '%c'\n", optopt);
                    print_usage(argv[0]);
                    return -1;
                }
                use_offs = true;
                break;
            case 't':
                if(optarg == nullptr) {
                    fprintf(stderr, "error: bad option argument '%c'\n", optopt);
                    return -1;
                }
                tmp_dir = optarg;
                if(!isDirectory(tmp_dir.c_str())) {
                    fprintf(stderr, "error: bad option argument '%c'\n", optopt);
                    print_usage(argv[0]);
                    return -1;
                }
                use_offs = false;
                break;
            case 'h':
            default:
                print_usage(argv[0]);
                return -1;
            }
        }
    
    
        DLOG0(DLOG_INFO, "using %s: %s\n", use_offs?"OFFS":"non-OFFS", use_offs?offs_dir.c_str():tmp_dir.c_str());
        DLOG0(DLOG_INFO, "camera id: %d\n", camera_id);
    
        signal(SIGINT, cancel_handler);
    
         ////////////////////////////////////////////////////////////////////////////////////////////
        // init
    
        parameters["offs-qsize-max"] = std::to_string(OFFS_QUEUE_SIZE_KBYTES);
        parameters["offs-overwrite-if-exist"] = "1";
        parameters["offs-cache-size"] = std::to_string(OFFS_CACHE_SIZE_KBYTES);
        parameters["media-cache-size"] = std::to_string(MEDIA_CACHE_SIZE_KBYTES);
        parameters["oasis-log-flags"] = std::to_string(OASIS_LOG_DEBUG/*|OASIS_LOG_ENCODE_BITRATE*/);
    
        //enableLogLocalTime(true);
    
        if(oasis::initialize(parameters) < 0) {
            DLOG(DLOG_ERROR, "Oasis init failed\n");
            return -1;
        }
    
        //non-offs, use system fs
        if(!use_offs) {
            fs::offsConfigLocalFormatInfo((tmp_dir + "/DRIVING").c_str(), 24*1024*1024);
            fs::offsConfigLocalFormatInfo((tmp_dir + "/EVENT").c_str(), 12*1024*1024);
        }
    
        ////////////////////////////////////////////////////////////////////////////////////////////
        // audio
        parameters.clear();
    
        //parameters["types"]="source,sink";
        parameters["types"]="source,sink";
        parameters["path"]=SND_PATH;
        parameters["always-on"]="0";
        parameters["channels"]="2";
        parameters["aec-disabled"]="1";
        parameters["denoise-enabled"]="1";
        parameters["snd-input-channels"]="2";
        parameters["snd-input-sample-size"]="16";
        parameters["snd-input-sampling-duration-msec"]="40";
        parameters["snd-input-sampling-rate"]="48000";
        //parameters["snd-input-sampling-rate"]="22050";
        parameters["snd-input-mono-channel-index"] = "0";
    
        createAudioDevice(parameters);
    
        ////////////////////////////////////////////////////////////////////////////////////////////
        // inference
        if(nef_path_valid) {
            parameters.clear();
            if(access(nef_path.c_str(), F_OK)) {
                nef_path_valid = false;
                fprintf(stdout, "file not found: %s\n", nef_path.c_str());
                fprintf(stdout, "inference disabled\n");
            } else {
                fprintf(stdout, "Inference model path: %s\n", nef_path.c_str());
                parameters["model-path"] = nef_path;
                createInferenceEngine(parameters);
            }
        }
    
        ////////////////////////////////////////////////////////////////////////////////////////////
        // sources
        parameters.clear();
    
        parameters["source-count"] = "1";
    
        parameters["source1-camera-id"] = "0";
        parameters["source1-isp-id"] = "0";
        parameters["source1-isp-wdr-mode"] = "0";
        parameters["source1-capture-format"] = "YUV420";
        parameters["source1-capture-buffers"] = "5";
        parameters["source1-fps"] = "30";
        parameters["source1-subchannel-rotation"] = "0";
        parameters["source1-loc"] = "front";
        parameters["source1-capture-resolution"] = "1080p";
        parameters["source1-sensor-config"] = "./Resource/VIC/0/tp2855_1920x1080_ch0.cfg";
        parameters["source1-autoscene-config"] = "./Resource/AutoScene/autoscene_conf.cfg";
        parameters["source1-resource-dir"] = "./Resource/";
    
    
        configCameras(parameters);
    
        ////////////////////////////////////////////////////////////////////////////////////////////
        //recorer settings
        parameters.clear();
    
        parameters["file-prefix"] =  "oasis-";
        parameters["file-extension"] = "mp4";
        //parameters["file-extension"] = "avi";
        parameters["file-duration-secs"] = "60";
        if(use_offs) {
            parameters["normal-folder-path"] = offs_dir + "/DRIVING";
            parameters["event-folder-path"] = offs_dir + "/EVENT";
            parameters["motion-folder-path"] = offs_dir + "/EVENT";
        } else {
            parameters["normal-folder-path"] = tmp_dir + "/DRIVING";
            parameters["event-folder-path"] = tmp_dir + "/EVENT";
            parameters["motion-folder-path"] = tmp_dir + "/EVENT";
        }
    
        parameters["event-pre-recording-seconds"] = "10";
        parameters["event-post-recording-seconds"] = "10";
        parameters["motion-pre-recording-seconds"] = "10";
        parameters["motion-post-recording-seconds"] = "10";
        parameters["disable-event-recording"] = "0";
        parameters["disable-offs-recording"] = "0";
        if(use_offs) {
            parameters["max-files"] = "0";
        } else {
            parameters["max-files"] = "5";
        }
        parameters["delete-oldest-file-on-max-files"] = "1";
        parameters["recording-size-limit-threshold-seconds"] = "0";
        parameters["report-media-info-ex"] = "1";
    
        parameters["avi-strd-size-max"] = "65536";
        parameters["mp4-udta-size-max"] = "65536";
        parameters["enable-persistent-cache"] = "0";
        parameters["recording-file-header-write-interval-secs"] = "1";
    
        parameters["snd-path"] = "hw:0,0";
        parameters["snd-input-channels"] = "2";
        parameters["snd-input-sample-size"] = "16";
        parameters["snd-input-sampling-duration-msec"] = "120";
        //parameters["snd-input-sampling-rate"] = "44100";
        parameters["snd-input-sampling-rate"] = "48000";
        if(forward_url) {
            parameters["snd-forward-url"] = forward_url;
        }
    
        parameters["aencoder-type"] = "raw"; //aac, raw, mp3
        parameters["aencoder-bitrate"] = "128000";
    
        parameters["osd-font-size"] = "0"; // disable (default)
        parameters["osd-font-face"] = "Consolas";
        parameters["osd-text-color"] = "255,255,255";
        parameters["osd-use-text-color"] = "1";
        parameters["osd-use-bg-color"] = "0";
        parameters["osd-bg-color"] = "0,0,0";
        parameters["osd-use-outline-color"] = "1";
        parameters["osd-outline-color"] = "255,255,255";
        parameters["osd-horz-align"] = "left";
        parameters["osd-vert-align"] = "bottom";
        parameters["osd-font-path"] = "/mnt/flash/leipzig/consola.ttf";
    
        parameters["channel-count"] = "1";
    
        parameters["channel1-camera-id"] = std::to_string(camera_id);
        parameters["channel1-ise-id"] = "-1";
        parameters["channel1-resolution"] = "1080p";
        parameters["channel1-bitrate"] = "8000000";
        parameters["channel1-fps"] = "30";
        parameters["channel1-file-framerate"] = "30";
        parameters["channel1-venc-framerate"] = "30";
        parameters["channel1-venc-keyframe-interval"] = "30";
    #if 0  
        parameters["channel1-vencoder-type"] = "h264";
        parameters["channel1-h264-profile"] = "high";
        parameters["channel1-h264-level"] = "level51";
        parameters["channel1-h264-min-qp"] = "10";
        parameters["channel1-h264-max-qp"] = "31";
        parameters["channel1-h264-enable-fixqp"] = "0";
        parameters["channel1-h264-fix-iqp"] = "10";
        parameters["channel1-h264-fix-pqp"] = "20";
    #else
        parameters["channel1-vencoder-type"] = "h265";
        parameters["channel1-h264-profile"] = "main";
    #endif
    
        if(nef_path_valid) {
            parameters["channel1-enable-adas"] = "1";
            parameters["channel1-adas-inference-model-names"] = "ufld,yolov5";
        }
    
        parameters["channel1-media-wait-timeout-secs"] = "3";
        parameters["channel1-media-wait-timeout-notify-oneshot"] = "1";
        parameters["channel1-osd-font-size"] = "0"; //disable;
    
        std::shared_ptr<MyRecordingObserver> recording_observer = std::make_shared<MyRecordingObserver>();
    
        RecorderRef recorder = createRecorder(parameters);
        if(recorder == nullptr) {
            DLOG(DLOG_ERROR, "recorder creation failed!\r\n");
            oasis::finalize();
            return -1;
        }
    
    
        memset(&strd_data_header_, 0, sizeof(strd_data_header_));
    
    
        t3 = std::thread([&]() {
    
            RecorderCommand cmd;
    
            do {
                {
                    std::unique_lock<std::mutex> _l(cmdq_lock_);
                    while(cmdq_.empty()) {
                        cmdq_cond_.wait(_l);
                    }
                    cmd = cmdq_.front();
                    cmdq_.pop_front();
                }
    
                if(cmd.cmd_ == kRecorderExitNow) {
                    DLOG(DLOG_TRACE, "got kRecorderExitNow\r\n");
                    break;
                }
    
                DLOG(DLOG_INFO, "cmd %d\n", cmd.cmd_);
    
                switch(cmd.cmd_) {
                    case kStartEventRecording: {
                        if (recorderIsRunning(recorder) && isEventRecording(recorder) == false) {
                            time_t t;
                            struct tm tm_t;
                            char path[PATH_MAX];
    
                            time(&t);
                            localtime_r(&t, &tm_t);
    
                            if(use_offs) {
                                sprintf(path, "%s/EVENT/EVT_%04d-%02d-%02d-%02d-%02d-%02d.mp4", offs_dir.c_str(), tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
                            } else {
                                sprintf(path, "%s/EVENT/EVT_%04d-%02d-%02d-%02d-%02d-%02d.mp4", tmp_dir.c_str(), tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
                            }
                            startEventRecording(recorder, path, nullptr, 0);
                        } else {
                            fprintf(stdout, "Recorder not started (%d) or busy in recording an motion (%d).\n", recorderIsRunning(recorder), isMotionRecording(recorder));
                        }
                        break;
                    }
                default:
                        break;
                }
    
            } while(1);
    
            {
                std::lock_guard<std::mutex> _l(cmdq_lock_);
                cmdq_.clear();
            }
        });
    
    
        ////////////////////////////////////////////////////////////////////////////////////////////
        t2 = std::thread([&]() {
            time_t t;
            struct tm tm_t;
            uint64_t usec, preview_usec;
            std::string title;
            uint32_t count = 0;
    
            char stream_data[512] = { 0, };
    
            preview_usec = systemTime();
    
            do {
    
                usec = systemTime();
    
                time(&t);
                localtime_r(&t, &tm_t);
    
                if (isRecorderRecording(recorder)) {
                    sprintf(stream_data, "OASIS %4d/%02d/%02d %02d:%02d:%02d", tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
                    addRecordingVideoStreamData(recorder, (void*)stream_data, strlen(stream_data) + 1);
                }
    
                if(/*count == 0 &&*/ isRecorderRecording(recorder)) {
                    title = oasis::format("OASIS %4d/%02d/%02d %02d:%02d:%02d\n", tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
                    //title += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero.";
                    setRecordingText(recorder, kTextTrackOsd, title, usec);
                    count++;
                }
    
                setVehicleSpeed(recorder, 60.f);
    
                //30 msec
                usleep(40000);
    
            } while (timer_running);
        });
    
        err = startRecording(recorder, recording_observer, nullptr, false, &strd_data_header_, sizeof(strd_data_header_));
        if(err < 0) {
            DLOG(DLOG_ERROR, "start recording failed\r\n");
            goto done;
        }  
    
        do {
            c = getch("Ctrl+C to exit...");
            if(c < 0 || continue_recording == false) {
                break;
            }
            if(c != '\n') {
                fprintf(stdout, "%c", c);
            }
            fprintf(stdout, "\n");
    
            if(c == 'e') {
                postCommand(kStartEventRecording);
            }
    
        } while (continue_recording == true);
    
    done:
        timer_running = false;
        if (t2.joinable()) {
            t2.join();
        }
    
        postCommand(kRecorderExitNow);
        if (t3.joinable()) {
            t3.join();
        }
    
        if(recorder) {
            destroyRecorder(recorder);
        }
    
        oasis::finalize();
    
        printf("goodbye.\n");
    
        return 0;
    }