{"action":"created","comment":{"url":"https://api.github.com/repos/nemomobile/ngfd/pulls/comments/29669551","id":29669551,"diff_hunk":"@@ -0,0 +1,801 @@\n+/*************************************************************************\n+This file is part of tone-generator\n+\n+Copyright (C) 2010 Nokia Corporation.\n+\n+This library is free software; you can redistribute\n+it and/or modify it under the terms of the GNU Lesser General Public\n+License as published by the Free Software Foundation\n+version 2.1 of the License.\n+\n+This library is distributed in the hope that it will be useful,\n+but WITHOUT ANY WARRANTY; without even the implied warranty of\n+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+Lesser General Public License for more details.\n+\n+You should have received a copy of the GNU Lesser General Public\n+License along with this library; if not, write to the Free Software\n+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301\n+USA.\n+*************************************************************************/\n+\n+#define _GNU_SOURCE\n+\n+#include <stdlib.h>\n+#include <string.h>\n+#include <time.h>\n+#include <sys/time.h>\n+#include <errno.h>\n+\n+#include <pulse/pulseaudio.h>\n+\n+#include <ngf/log.h>\n+#include <trace/trace.h>\n+\n+#include \"ausrv.h\"\n+#include \"stream.h\"\n+\n+#define TRACE(f, args...) trace_write(trctx, trflags, trkeys, f, ##args)\n+#define LOG_CAT \"tonegen-stream: \"\n+\n+static void state_callback(pa_stream *, void *);\n+static void underflow_callback(pa_stream *, void *);\n+static void suspended_callback(pa_stream *, void *);\n+static void write_callback(pa_stream *, size_t, void *);\n+static void flush_callback(pa_stream *, int, void *);\n+static void drain_callback(pa_stream *, int, void *);\n+static void write_samples(struct stream *, int16_t *,size_t, uint32_t *);\n+\n+static uint32_t default_rate     = 48000;\n+static int      print_statistics = 0;\n+static int      target_buflen    = 1000; /* 1000msec ie. 1sec */\n+static int      min_bufreq       = 200;  /* 200msec */\n+\n+int stream_init(void)\n+{\n+    return 0;\n+}\n+\n+\n+void stream_set_default_samplerate(uint32_t rate)\n+{\n+    default_rate = rate;\n+}\n+\n+void stream_print_statistics(int print)\n+{\n+    print_statistics = print;\n+}\n+\n+void stream_buffering_parameters(int tlen, int minreq)\n+{\n+    if (!tlen && !minreq) {\n+        target_buflen = 0;\n+        min_bufreq = 0;\n+    }\n+    else {\n+        if (tlen < 20 || minreq < 10 || minreq > tlen - 10) {\n+            N_ERROR(LOG_CAT \"Ignoring invalid buffering parameters %d %d\",\n+                      tlen, minreq);\n+        }\n+        else {\n+            target_buflen = tlen;\n+            min_bufreq = minreq;\n+        }\n+    }\n+}\n+\n+struct stream *stream_create(struct ausrv *ausrv,\n+                             char         *name,\n+                             char         *sink,\n+                             uint32_t      sample_rate,\n+                             uint32_t    (*write)(struct stream*,int16_t*,int),\n+                             void        (*destroy)(void*),\n+                             void         *proplist,\n+                             void         *data)\n+{\n+    struct stream      *stream;\n+    pa_buffer_attr      battr;\n+    pa_stream_flags_t   flags;\n+    pa_sample_spec      spec;\n+    struct timeval      tv;\n+    uint64_t            start;\n+    struct stream_stat *stat;\n+    uint32_t            bufsize;\n+    uint32_t            tlength;\n+    char                tlstr[32];\n+    char                bfstr[32];\n+\n+    if (!ausrv->connected) {\n+        N_ERROR(LOG_CAT \"Can't create stream '%s': no server connected\", name);\n+        return NULL;\n+    }\n+\n+    if (name == NULL)\n+        name = \"generated tone\";\n+\n+    if (sample_rate == 0)\n+        sample_rate = default_rate;\n+\n+    memset(&spec, 0, sizeof(spec));\n+    spec.format   = PA_SAMPLE_S16LE;\n+    spec.rate     = sample_rate;\n+    spec.channels = 1;          /* e.g. MONO */\n+\n+    \n+    if (min_bufreq > 0)\n+        bufsize = pa_usec_to_bytes(min_bufreq * PA_USEC_PER_MSEC, &spec);\n+    else\n+        bufsize = (uint32_t)-1;\n+\n+    if (target_buflen > 0)\n+        tlength = pa_usec_to_bytes(target_buflen * PA_USEC_PER_MSEC, &spec);\n+    else\n+        tlength = (uint32_t)-1;\n+\n+    gettimeofday(&tv, NULL);\n+    start = (uint64_t)tv.tv_sec * (uint64_t)1000000 + (uint64_t)tv.tv_usec;\n+\n+    if ((stream = (struct stream *)malloc(sizeof(*stream))) == NULL) {\n+        N_ERROR(LOG_CAT \"%s(): Can't allocate memory\", __FUNCTION__);\n+        return NULL;\n+    }\n+    memset(stream, 0, sizeof(*stream));\n+\n+    stream->next    = ausrv->streams;\n+    stream->ausrv   = ausrv;\n+    stream->id      = ausrv->nextid++;\n+    stream->name    = strdup(name);\n+    stream->rate    = sample_rate;\n+    stream->pastr   = pa_stream_new_with_proplist(ausrv->context, name,\n+                                                  &spec, NULL,\n+                                                  (pa_proplist *)proplist);\n+    stream->start   = start;\n+    stream->flush   = TRUE;\n+    stream->bufsize = bufsize;\n+    stream->write   = write;\n+    stream->destroy = destroy;\n+    stream->data    = data;\n+\n+    if (print_statistics) {\n+        stat = &stream->stat;\n+        stat->wrtime  = start;\n+        stat->minbuf  = -1;\n+        stat->mingap  = -1;\n+        stat->mincalc = -1;\n+    }\n+\n+\n+    if (stream->pastr == NULL) {\n+        free(stream->name);\n+        \n+        free(stream);\n+\n+        TRACE(\"%s(): stream creation of '%s' failed\", __FUNCTION__, name);\n+\n+        return NULL;    \n+    }\n+\n+    /* these are for the 48Khz mono 16bit streams */\n+    battr.maxlength = -1;                /* default (4MB) */\n+    battr.tlength   = tlength;\n+    battr.minreq    = bufsize;\n+    battr.prebuf    = -1;                /* default (tlength) */\n+    battr.fragsize  = -1;                /* default (tlength) */\n+\n+    flags = PA_STREAM_ADJUST_LATENCY;\n+\n+    pa_stream_set_state_callback(stream->pastr, state_callback,(void*)stream);\n+    pa_stream_set_underflow_callback(stream->pastr, underflow_callback,\n+                                     (void *)stream);\n+    pa_stream_set_suspended_callback(stream->pastr, suspended_callback,\n+                                     (void *)stream);\n+    pa_stream_set_write_callback(stream->pastr, write_callback,(void *)stream);\n+    pa_stream_connect_playback(stream->pastr, sink, &battr, flags, NULL, NULL);\n+\n+    ausrv->streams = stream;\n+\n+    TRACE(\"%s(): stream '%s' created\", __FUNCTION__, stream->name);\n+\n+    if (print_statistics) {\n+        if (battr.tlength == (uint32_t)-1)\n+            snprintf(tlstr, sizeof(tlstr), \"<default>\");\n+        else\n+            snprintf(tlstr, sizeof(tlstr), \"%u\", battr.tlength);\n+\n+        if (battr.minreq == (uint32_t)-1)\n+            snprintf(bfstr, sizeof(tlstr), \"<default>\");\n+        else\n+            snprintf(bfstr, sizeof(tlstr), \"%u\", battr.minreq);\n+\n+        TRACE(\"Requested buffer attributes:\\n\"\n+              \"   tlength  %s\\n\"\n+              \"   minreq   %s\",\n+              tlstr, bfstr);\n+    }\n+\n+    return stream;\n+}\n+\n+void stream_destroy(struct stream *stream)\n+{\n+    struct ausrv         *ausrv = stream->ausrv;\n+    struct stream        *prev;\n+    pa_stream            *pastr;\n+    const pa_buffer_attr *battr;\n+    struct stream_stat   *stat;\n+    pa_operation         *oper;\n+#ifdef ENABLE_TRACE\n+    struct timeval        tv;\n+    uint64_t              stop;\n+    double                upt;\n+    double                strt;\n+    double                dur;\n+    double                freq;\n+    double                flow;\n+    uint32_t              avbuf;\n+    uint32_t              avcalc;\n+    uint32_t              avcpu;\n+    uint32_t              avgap;\n+#endif\n+\n+    TRACE(\"%s(): destroying stream '%s'\", __FUNCTION__, stream->name);\n+\n+\n+    if (stream->killed) {\n+        TRACE(\"%s(): stream is already killed\", __FUNCTION__);\n+        return;\n+    }\n+\n+#ifdef ENABLE_TRACE\n+    gettimeofday(&tv, NULL);\n+    stop = (uint64_t)tv.tv_sec * (uint64_t)1000000 + (uint64_t)tv.tv_usec;\n+#endif\n+\n+\n+    for (prev=(struct stream *)&ausrv->streams;  prev->next;  prev=prev->next){\n+        if (prev->next == stream) {\n+            pastr = stream->pastr;\n+            battr = pa_stream_get_buffer_attr(pastr);\n+            stat  = &stream->stat;\n+\n+            if (stream->flush)\n+                oper = pa_stream_flush(pastr, flush_callback, (void *)stream);\n+            else\n+                oper = pa_stream_drain(pastr, drain_callback, (void *)stream);\n+\n+            if (oper == NULL)\n+                return;\n+\n+            pa_operation_unref(oper);\n+\n+            prev->next = stream->next;\n+            stream->next   = NULL;\n+            stream->killed = TRUE;\n+\n+            if (stream->destroy != NULL)\n+                stream->destroy(stream->data);\n+\n+            stream->ausrv  = NULL;\n+\n+            free(stream->buf.samples);\n+\n+            pa_stream_set_write_callback(pastr, NULL,NULL);\n+\n+            if (print_statistics && stat->wrcnt > 0) {\n+                if (battr != NULL) {\n+                    TRACE(\"Buffer attributes:\\n\"\n+                          \"   maxlength %u\\n\"\n+                          \"   tlength   %u\\n\"\n+                          \"   prebuf    %u\\n\"\n+                          \"   minreq    %u\",\n+                          battr->maxlength, battr->tlength,\n+                          battr->prebuf, battr->minreq);\n+                }\n+\n+#ifdef ENABLE_TRACE\n+                upt  = (double)(stop - stream->start) / 1000000.0;\n+                strt = (double)(stream->time) / 1000000.0;\n+                dur  = (double)(stat->wrtime - stat->firstwr)/1000000.0 + 0.01;\n+                freq = (double)stat->wrcnt / dur;\n+                flow = (double)stream->bcnt / dur;\n+\n+                avbuf  = stream->bcnt / stat->wrcnt;\n+                avcpu  = (stat->cpucalc / stat->wrcnt) / 1000;\n+                avcalc = (uint32_t)(stat->sumcalc/(uint64_t)stat->wrcnt)/1000; \n+                avgap  = (uint32_t)(stat->sumgap /(uint64_t)stat->wrcnt)/1000; \n+#endif\n+\n+                TRACE(\"stream '%s' killed. Statistics:\\n\"\n+                      \"   up %.3lfsec tone %.3lfsec\\n\"\n+                      \"   flow %.0lf byte/sec (excluding pre-buffering)\\n\"\n+                      \"   write freq %.2lf buf/sec (every %.0lf msec)\\n\"\n+                      \"   bufsize %u - %u - %u\\n\"\n+                      \"   calc.time %u - %u - %u msec\\n\"\n+                      \"   avarage cpu / buffer %u msec\\n\"\n+                      \"   cpu load for all buffer calculation %.2lf%%\\n\"\n+                      \"   gaps %u - %u - %u msec\\n\"\n+                      \"   underflows %u\\n\"\n+                      \"   %u buffer was late out of %u (%u%%)\",\n+                      stream->name, upt, strt, flow, freq, 1000.0/freq,\n+                      stat->minbuf, avbuf, stat->maxbuf,\n+                      stat->mincalc / 1000, avcalc, stat->maxcalc / 1000,\n+                      avcpu, ((double)avcpu * freq) / 10.0,\n+                      stat->mingap / 1000, avgap, stat->maxgap / 1000,\n+                      stat->underflows, stat->late, stat->wrcnt,\n+                      (stat->late * 100) / stat->wrcnt);\n+            }\n+\n+            return;\n+        }\n+    }\n+\n+    N_ERROR(LOG_CAT \"%s(): Can't find stream '%s'\", __FUNCTION__, stream->name);\n+}\n+\n+void stream_set_timeout(struct stream *stream, uint32_t timeout)\n+{\n+    if (timeout == 0)\n+        stream->end = 0;\n+    else\n+        stream->end = stream->time + timeout;\n+}\n+\n+void stream_kill_all(struct ausrv *ausrv)\n+{\n+    struct stream *stream;\n+\n+    while ((stream = ausrv->streams) != NULL) {\n+        ausrv->streams = stream->next;\n+\n+        stream->next   = NULL;\n+        stream->killed = TRUE;\n+\n+        if (stream->destroy != NULL)\n+            stream->destroy(stream->data);\n+\n+        stream->ausrv  = NULL;\n+\n+        pa_stream_set_state_callback(stream->pastr, NULL,NULL);\n+        pa_stream_set_underflow_callback(stream->pastr, NULL,NULL);\n+        pa_stream_set_suspended_callback(stream->pastr, NULL,NULL);\n+        pa_stream_set_write_callback(stream->pastr, NULL,NULL);\n+\n+        free(stream->name);\n+        free(stream);\n+    }\n+}\n+\n+void stream_clean_buffer(struct stream *stream)\n+{\n+    struct timeval  tv;\n+    uint64_t        now;\n+    uint32_t        bcnt;\n+    uint32_t        dcnt;\n+    size_t          offs;\n+    size_t          len;\n+    uint32_t        i,j;\n+    int32_t         sample;\n+\n+    gettimeofday(&tv, NULL);\n+    now  = (uint64_t)tv.tv_sec * (uint64_t)1000000 + (uint64_t)tv.tv_usec;\n+    bcnt = ((now - stream->start) * (uint64_t)stream->rate) / 1000000ULL;\n+    bcnt *= 2;\n+\n+    dcnt = (10000ULL * (uint64_t)stream->rate) / 1000000ULL;\n+\n+    if (stream->buf.samples != NULL) {\n+        offs = bcnt >= stream->bcnt ? bcnt - stream->bcnt : 0;\n+\n+        if (offs < stream->buf.buflen) {\n+            len = stream->buf.buflen - offs;\n+\n+            if (len < dcnt * 2) {\n+                TRACE(\"%s(): resetting %u bytes in write-ahead-buffer\",\n+                      __FUNCTION__, len);\n+                memset((char *)stream->buf.samples + offs, 0, len);\n+            }\n+            else {\n+                for (i = 0, j = offs / 2;  i < dcnt;  i++, j++) {\n+                    sample = stream->buf.samples[j];\n+                    sample = (sample * ((int32_t)dcnt - (int32_t)i - 1)) / (int32_t)dcnt;\n+\n+                    if (sample > 32767)\n+                        stream->buf.samples[j] = 32767;\n+                    else if (sample < -32767)\n+                        stream->buf.samples[j] = -32767;\n+                    else\n+                        stream->buf.samples[j] = sample;\n+                }\n+\n+                len  -= dcnt * 2;\n+                offs += dcnt * 2;\n+\n+                TRACE(\"%s(): ramping down %u and resetting %u bytes in \"\n+                      \"write-ahead-buffer\", __FUNCTION__, dcnt * 2, len);\n+\n+                if (len > 0)\n+                    memset((char *)stream->buf.samples + offs, 0, len);\n+            }\n+        }\n+    }\n+}\n+\n+struct stream *stream_find(struct ausrv *ausrv, char *name)\n+{\n+    struct stream *stream;\n+\n+    for (stream = ausrv->streams;   stream != NULL;   stream = stream->next) {\n+        if (!strcmp(name, stream->name))\n+            break;\n+    }\n+\n+    return stream;\n+}\n+\n+static pa_proplist *parse_properties(pa_proplist *proplist, const char *propstring)\n+{\n+    const char  *key, *val, *next;\n+    char         keybuf[128];\n+    int          keylen, vallen;\n+\n+    if (propstring == NULL)\n+        return NULL;\n+\n+    key = propstring;\n+    while (key != NULL) {\n+        if ((val = strchr(key, '=')) == NULL) {\n+            N_ERROR(LOG_CAT \"%s(): Invalid property string '%s'\", __FUNCTION__,\n+                      propstring);\n+            goto error;\n+        }\n+\n+        keylen = val - key;\n+        if (keylen >= (int)sizeof(keybuf)) {\n+            N_ERROR(LOG_CAT \"%s(): property key '%*.*s' too long\", __FUNCTION__,\n+                      keylen, keylen, key);\n+            goto error;\n+        }\n+        strncpy(keybuf, key, keylen);\n+        keybuf[keylen] = '\\0';\n+\n+        val++;\n+        if ((next = strchr(val, ',')) != NULL) {\n+            vallen = next - val;\n+            next++;\n+        }\n+        else\n+            vallen = strlen(val);\n+\n+        pa_proplist_setf(proplist, keybuf, \"%*.*s\", vallen, vallen, val);\n+\n+        key = next;\n+    }\n+\n+    return proplist;\n+    \n+ error:\n+    pa_proplist_free(proplist);\n+    return NULL;\n+}\n+\n+void *stream_parse_properties(const char *propstring)\n+{\n+    pa_proplist *p;\n+\n+    if ((p = pa_proplist_new()) == NULL) {\n+        N_ERROR(LOG_CAT \"%s(): Failed to allocate property list\", __FUNCTION__);\n+        return NULL;\n+    }\n+\n+    return (void *) parse_properties(p, propstring);\n+}\n+\n+void *stream_merge_properties(void *proplist, const char *extra_properties)\n+{\n+    pa_proplist *p = (pa_proplist *) proplist;\n+\n+    if (!p)\n+        return stream_parse_properties(extra_properties);\n+    else\n+        p = pa_proplist_copy(p);\n+\n+    return (void *) parse_properties(p, extra_properties);\n+}\n+\n+void stream_free_properties(void *proplist)\n+{\n+    if (proplist)\n+        pa_proplist_free((pa_proplist *)proplist);\n+}\n+\n+static void state_callback(pa_stream *pastr, void *userdata)\n+{\n+    struct stream *stream = (struct stream *)userdata;\n+\n+    if (!stream || stream->pastr != pastr) {\n+        N_ERROR(LOG_CAT \"%s(): confused with data structures\", __FUNCTION__);\n+        return;\n+    }\n+\n+    switch (pa_stream_get_state(pastr)) {\n+        case PA_STREAM_UNCONNECTED:\n+            TRACE(\"%s(): stream '%s' unconnected\", __FUNCTION__, stream->name);\n+            break;\n+\n+        case PA_STREAM_CREATING:\n+            TRACE(\"%s(): stream '%s' creating\", __FUNCTION__, stream->name);\n+            break;\n+\n+        case PA_STREAM_READY:\n+            TRACE(\"%s(): stream '%s' ready\", __FUNCTION__, stream->name);\n+            break;\n+\n+        case PA_STREAM_TERMINATED:\n+            TRACE(\"%s(): stream '%s' terminated\", __FUNCTION__, stream->name);\n+            free(stream->name);\n+            free(stream);\n+            break;\n+\n+        default:\n+        case PA_STREAM_FAILED:\n+            N_ERROR(LOG_CAT \"%s(): Stream error: %s\", __FUNCTION__,\n+                      pa_strerror(pa_context_errno(pa_stream_get_context(pastr))));\n+            break;\n+    }\n+}\n+\n+\n+static void underflow_callback(pa_stream *pastr, void *userdata)\n+{\n+    (void)pastr;\n+\n+    struct stream *stream = (struct stream *)userdata;\n+\n+    if (!stream || !stream->name) \n+        N_ERROR(LOG_CAT \"Stream underflow\");\n+    else {\n+        N_ERROR(LOG_CAT \"Stream '%s' underflow\", stream->name);\n+\n+        stream->stat.underflows++;\n+\n+        stream_destroy(stream);\n+    }\n+}\n+\n+static void suspended_callback(pa_stream *pastr, void *userdata)\n+{\n+    (void)pastr;\n+\n+    struct stream *stream = (struct stream *)userdata;\n+\n+    if (!stream || !stream->name) \n+        N_ERROR(LOG_CAT \"Stream suspended\");\n+    else {\n+        N_ERROR(LOG_CAT \"Stream '%s' suspended\", stream->name);\n+\n+    }\n+}\n+\n+static void write_callback(pa_stream *pastr, size_t bytes, void *userdata)\n+{\n+    struct stream        *stream = (struct stream *)userdata;\n+    struct stream_stat   *stat   = &stream->stat;\n+    const pa_buffer_attr *battr;\n+    int16_t              *samples;\n+    size_t                buflen;\n+    int16_t              *extra;\n+    size_t                extlen;\n+    struct timeval        tv;\n+    uint32_t              start = 0;\n+    uint32_t              gap = 0;\n+    uint32_t              calcend;\n+    uint32_t              calc;\n+    uint32_t              period;\n+    uint32_t              cpu;\n+\n+\n+    if (!stream || stream->pastr != pastr) {\n+        N_ERROR(LOG_CAT \"%s(): Confused with data structures\", __FUNCTION__);\n+        return;\n+    }\n+\n+    if (stream->killed)\n+        return;\n+\n+    if (print_statistics) {\n+        gettimeofday(&tv, NULL);\n+        start = (uint64_t)tv.tv_sec * (uint64_t)1000000 + (uint64_t)tv.tv_usec;\n+        gap   = start - stat->wrtime;\n+    }\n+\n+#if 0","path":"src/plugins/tonegen/stream.c","position":612,"original_position":612,"commit_id":"322bdfa2fe2d5c776883e1d64b935d400e548485","original_commit_id":"322bdfa2fe2d5c776883e1d64b935d400e548485","user":{"login":"thp","id":135241,"avatar_url":"https://avatars.githubusercontent.com/u/135241?v=3","gravatar_id":"","url":"https://api.github.com/users/thp","html_url":"https://github.com/thp","followers_url":"https://api.github.com/users/thp/followers","following_url":"https://api.github.com/users/thp/following{/other_user}","gists_url":"https://api.github.com/users/thp/gists{/gist_id}","starred_url":"https://api.github.com/users/thp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thp/subscriptions","organizations_url":"https://api.github.com/users/thp/orgs","repos_url":"https://api.github.com/users/thp/repos","events_url":"https://api.github.com/users/thp/events{/privacy}","received_events_url":"https://api.github.com/users/thp/received_events","type":"User","site_admin":false},"body":"Remove this if it isn't used.","created_at":"2015-05-05T13:53:28Z","updated_at":"2015-05-05T13:53:28Z","html_url":"https://github.com/nemomobile/ngfd/pull/46#discussion_r29669551","pull_request_url":"https://api.github.com/repos/nemomobile/ngfd/pulls/46","_links":{"self":{"href":"https://api.github.com/repos/nemomobile/ngfd/pulls/comments/29669551"},"html":{"href":"https://github.com/nemomobile/ngfd/pull/46#discussion_r29669551"},"pull_request":{"href":"https://api.github.com/repos/nemomobile/ngfd/pulls/46"}}},"pull_request":{"url":"https://api.github.com/repos/nemomobile/ngfd/pulls/46","id":23981381,"html_url":"https://github.com/nemomobile/ngfd/pull/46","diff_url":"https://github.com/nemomobile/ngfd/pull/46.diff","patch_url":"https://github.com/nemomobile/ngfd/pull/46.patch","issue_url":"https://api.github.com/repos/nemomobile/ngfd/issues/46","number":46,"state":"open","locked":false,"title":"Merge tonegend as NGFD tonegen plugin.","user":{"login":"jusa","id":184568,"avatar_url":"https://avatars.githubusercontent.com/u/184568?v=3","gravatar_id":"","url":"https://api.github.com/users/jusa","html_url":"https://github.com/jusa","followers_url":"https://api.github.com/users/jusa/followers","following_url":"https://api.github.com/users/jusa/following{/other_user}","gists_url":"https://api.github.com/users/jusa/gists{/gist_id}","starred_url":"https://api.github.com/users/jusa/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jusa/subscriptions","organizations_url":"https://api.github.com/users/jusa/orgs","repos_url":"https://api.github.com/users/jusa/repos","events_url":"https://api.github.com/users/jusa/events{/privacy}","received_events_url":"https://api.github.com/users/jusa/received_events","type":"User","site_admin":false},"body":"","created_at":"2014-11-06T13:50:56Z","updated_at":"2015-05-05T13:53:28Z","closed_at":null,"merged_at":null,"merge_commit_sha":"29bf973998dab4a3422b27cc95b523fa71663d06","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/nemomobile/ngfd/pulls/46/commits","review_comments_url":"https://api.github.com/repos/nemomobile/ngfd/pulls/46/comments","review_comment_url":"https://api.github.com/repos/nemomobile/ngfd/pulls/comments{/number}","comments_url":"https://api.github.com/repos/nemomobile/ngfd/issues/46/comments","statuses_url":"https://api.github.com/repos/nemomobile/ngfd/statuses/322bdfa2fe2d5c776883e1d64b935d400e548485","head":{"label":"jusa:tone-generator","ref":"tone-generator","sha":"322bdfa2fe2d5c776883e1d64b935d400e548485","user":{"login":"jusa","id":184568,"avatar_url":"https://avatars.githubusercontent.com/u/184568?v=3","gravatar_id":"","url":"https://api.github.com/users/jusa","html_url":"https://github.com/jusa","followers_url":"https://api.github.com/users/jusa/followers","following_url":"https://api.github.com/users/jusa/following{/other_user}","gists_url":"https://api.github.com/users/jusa/gists{/gist_id}","starred_url":"https://api.github.com/users/jusa/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jusa/subscriptions","organizations_url":"https://api.github.com/users/jusa/orgs","repos_url":"https://api.github.com/users/jusa/repos","events_url":"https://api.github.com/users/jusa/events{/privacy}","received_events_url":"https://api.github.com/users/jusa/received_events","type":"User","site_admin":false},"repo":{"id":6756978,"name":"ngfd","full_name":"jusa/ngfd","owner":{"login":"jusa","id":184568,"avatar_url":"https://avatars.githubusercontent.com/u/184568?v=3","gravatar_id":"","url":"https://api.github.com/users/jusa","html_url":"https://github.com/jusa","followers_url":"https://api.github.com/users/jusa/followers","following_url":"https://api.github.com/users/jusa/following{/other_user}","gists_url":"https://api.github.com/users/jusa/gists{/gist_id}","starred_url":"https://api.github.com/users/jusa/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jusa/subscriptions","organizations_url":"https://api.github.com/users/jusa/orgs","repos_url":"https://api.github.com/users/jusa/repos","events_url":"https://api.github.com/users/jusa/events{/privacy}","received_events_url":"https://api.github.com/users/jusa/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/jusa/ngfd","description":"Non-graphical feedback daemon.","fork":true,"url":"https://api.github.com/repos/jusa/ngfd","forks_url":"https://api.github.com/repos/jusa/ngfd/forks","keys_url":"https://api.github.com/repos/jusa/ngfd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jusa/ngfd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jusa/ngfd/teams","hooks_url":"https://api.github.com/repos/jusa/ngfd/hooks","issue_events_url":"https://api.github.com/repos/jusa/ngfd/issues/events{/number}","events_url":"https://api.github.com/repos/jusa/ngfd/events","assignees_url":"https://api.github.com/repos/jusa/ngfd/assignees{/user}","branches_url":"https://api.github.com/repos/jusa/ngfd/branches{/branch}","tags_url":"https://api.github.com/repos/jusa/ngfd/tags","blobs_url":"https://api.github.com/repos/jusa/ngfd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jusa/ngfd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jusa/ngfd/git/refs{/sha}","trees_url":"https://api.github.com/repos/jusa/ngfd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jusa/ngfd/statuses/{sha}","languages_url":"https://api.github.com/repos/jusa/ngfd/languages","stargazers_url":"https://api.github.com/repos/jusa/ngfd/stargazers","contributors_url":"https://api.github.com/repos/jusa/ngfd/contributors","subscribers_url":"https://api.github.com/repos/jusa/ngfd/subscribers","subscription_url":"https://api.github.com/repos/jusa/ngfd/subscription","commits_url":"https://api.github.com/repos/jusa/ngfd/commits{/sha}","git_commits_url":"https://api.github.com/repos/jusa/ngfd/git/commits{/sha}","comments_url":"https://api.github.com/repos/jusa/ngfd/comments{/number}","issue_comment_url":"https://api.github.com/repos/jusa/ngfd/issues/comments{/number}","contents_url":"https://api.github.com/repos/jusa/ngfd/contents/{+path}","compare_url":"https://api.github.com/repos/jusa/ngfd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jusa/ngfd/merges","archive_url":"https://api.github.com/repos/jusa/ngfd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jusa/ngfd/downloads","issues_url":"https://api.github.com/repos/jusa/ngfd/issues{/number}","pulls_url":"https://api.github.com/repos/jusa/ngfd/pulls{/number}","milestones_url":"https://api.github.com/repos/jusa/ngfd/milestones{/number}","notifications_url":"https://api.github.com/repos/jusa/ngfd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jusa/ngfd/labels{/name}","releases_url":"https://api.github.com/repos/jusa/ngfd/releases{/id}","created_at":"2012-11-19T08:17:47Z","updated_at":"2014-06-13T10:05:58Z","pushed_at":"2015-05-04T08:41:19Z","git_url":"git://github.com/jusa/ngfd.git","ssh_url":"git@github.com:jusa/ngfd.git","clone_url":"https://github.com/jusa/ngfd.git","svn_url":"https://github.com/jusa/ngfd","homepage":null,"size":1654,"stargazers_count":0,"watchers_count":0,"language":"C","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"nemomobile:master","ref":"master","sha":"9eb88180f0e3516b9f23e21fe8ef90ca33bdc583","user":{"login":"nemomobile","id":1088947,"avatar_url":"https://avatars.githubusercontent.com/u/1088947?v=3","gravatar_id":"","url":"https://api.github.com/users/nemomobile","html_url":"https://github.com/nemomobile","followers_url":"https://api.github.com/users/nemomobile/followers","following_url":"https://api.github.com/users/nemomobile/following{/other_user}","gists_url":"https://api.github.com/users/nemomobile/gists{/gist_id}","starred_url":"https://api.github.com/users/nemomobile/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nemomobile/subscriptions","organizations_url":"https://api.github.com/users/nemomobile/orgs","repos_url":"https://api.github.com/users/nemomobile/repos","events_url":"https://api.github.com/users/nemomobile/events{/privacy}","received_events_url":"https://api.github.com/users/nemomobile/received_events","type":"Organization","site_admin":false},"repo":{"id":6747379,"name":"ngfd","full_name":"nemomobile/ngfd","owner":{"login":"nemomobile","id":1088947,"avatar_url":"https://avatars.githubusercontent.com/u/1088947?v=3","gravatar_id":"","url":"https://api.github.com/users/nemomobile","html_url":"https://github.com/nemomobile","followers_url":"https://api.github.com/users/nemomobile/followers","following_url":"https://api.github.com/users/nemomobile/following{/other_user}","gists_url":"https://api.github.com/users/nemomobile/gists{/gist_id}","starred_url":"https://api.github.com/users/nemomobile/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nemomobile/subscriptions","organizations_url":"https://api.github.com/users/nemomobile/orgs","repos_url":"https://api.github.com/users/nemomobile/repos","events_url":"https://api.github.com/users/nemomobile/events{/privacy}","received_events_url":"https://api.github.com/users/nemomobile/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/nemomobile/ngfd","description":"Non-graphical feedback daemon.","fork":false,"url":"https://api.github.com/repos/nemomobile/ngfd","forks_url":"https://api.github.com/repos/nemomobile/ngfd/forks","keys_url":"https://api.github.com/repos/nemomobile/ngfd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nemomobile/ngfd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nemomobile/ngfd/teams","hooks_url":"https://api.github.com/repos/nemomobile/ngfd/hooks","issue_events_url":"https://api.github.com/repos/nemomobile/ngfd/issues/events{/number}","events_url":"https://api.github.com/repos/nemomobile/ngfd/events","assignees_url":"https://api.github.com/repos/nemomobile/ngfd/assignees{/user}","branches_url":"https://api.github.com/repos/nemomobile/ngfd/branches{/branch}","tags_url":"https://api.github.com/repos/nemomobile/ngfd/tags","blobs_url":"https://api.github.com/repos/nemomobile/ngfd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nemomobile/ngfd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nemomobile/ngfd/git/refs{/sha}","trees_url":"https://api.github.com/repos/nemomobile/ngfd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nemomobile/ngfd/statuses/{sha}","languages_url":"https://api.github.com/repos/nemomobile/ngfd/languages","stargazers_url":"https://api.github.com/repos/nemomobile/ngfd/stargazers","contributors_url":"https://api.github.com/repos/nemomobile/ngfd/contributors","subscribers_url":"https://api.github.com/repos/nemomobile/ngfd/subscribers","subscription_url":"https://api.github.com/repos/nemomobile/ngfd/subscription","commits_url":"https://api.github.com/repos/nemomobile/ngfd/commits{/sha}","git_commits_url":"https://api.github.com/repos/nemomobile/ngfd/git/commits{/sha}","comments_url":"https://api.github.com/repos/nemomobile/ngfd/comments{/number}","issue_comment_url":"https://api.github.com/repos/nemomobile/ngfd/issues/comments{/number}","contents_url":"https://api.github.com/repos/nemomobile/ngfd/contents/{+path}","compare_url":"https://api.github.com/repos/nemomobile/ngfd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nemomobile/ngfd/merges","archive_url":"https://api.github.com/repos/nemomobile/ngfd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nemomobile/ngfd/downloads","issues_url":"https://api.github.com/repos/nemomobile/ngfd/issues{/number}","pulls_url":"https://api.github.com/repos/nemomobile/ngfd/pulls{/number}","milestones_url":"https://api.github.com/repos/nemomobile/ngfd/milestones{/number}","notifications_url":"https://api.github.com/repos/nemomobile/ngfd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nemomobile/ngfd/labels{/name}","releases_url":"https://api.github.com/repos/nemomobile/ngfd/releases{/id}","created_at":"2012-11-18T14:19:10Z","updated_at":"2014-07-23T12:35:40Z","pushed_at":"2015-05-04T08:41:20Z","git_url":"git://github.com/nemomobile/ngfd.git","ssh_url":"git@github.com:nemomobile/ngfd.git","clone_url":"https://github.com/nemomobile/ngfd.git","svn_url":"https://github.com/nemomobile/ngfd","homepage":null,"size":1911,"stargazers_count":0,"watchers_count":0,"language":"C","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":12,"mirror_url":null,"open_issues_count":2,"forks":12,"open_issues":2,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/nemomobile/ngfd/pulls/46"},"html":{"href":"https://github.com/nemomobile/ngfd/pull/46"},"issue":{"href":"https://api.github.com/repos/nemomobile/ngfd/issues/46"},"comments":{"href":"https://api.github.com/repos/nemomobile/ngfd/issues/46/comments"},"review_comments":{"href":"https://api.github.com/repos/nemomobile/ngfd/pulls/46/comments"},"review_comment":{"href":"https://api.github.com/repos/nemomobile/ngfd/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/nemomobile/ngfd/pulls/46/commits"},"statuses":{"href":"https://api.github.com/repos/nemomobile/ngfd/statuses/322bdfa2fe2d5c776883e1d64b935d400e548485"}}}}