Example #1
0
static void add_event_node_to_audio_clip(AudioGraph *ag, AudioGraphClip *clip) {
    assert(!clip->event_node_descr);
    assert(!clip->event_node);

    const char *name = "audio_clip_events";
    char *description = create_formatted_str("Events for Audio Clip: %s",
            clip->audio_clip->name.encode().raw());
    GenesisNodeDescriptor *node_descr =
        ok_mem(genesis_create_node_descriptor(ag->pipeline, 1, name, description));
    free(description);
    description = nullptr;

    genesis_node_descriptor_set_userdata(node_descr, clip);

    struct GenesisPortDescriptor *events_out_port = genesis_node_descriptor_create_port(
            node_descr, 0, GenesisPortTypeEventsOut, "events_out");

    if (!events_out_port)
        panic("unable to create ports");

    genesis_node_descriptor_set_run_callback(node_descr, audio_clip_event_node_run);
    genesis_node_descriptor_set_seek_callback(node_descr, audio_clip_event_node_seek);
    genesis_node_descriptor_set_create_callback(node_descr, audio_clip_event_node_create);
    genesis_node_descriptor_set_destroy_callback(node_descr, audio_clip_event_node_destroy);

    clip->event_node_descr = node_descr;
    clip->event_node = ok_mem(genesis_node_descriptor_create_node(node_descr));
}
Example #2
0
static AudioGraph *audio_graph_create_common(Project *project, GenesisContext *genesis_context,
        double latency)
{
    GenesisPipeline *pipeline;
    ok_or_panic(genesis_pipeline_create(genesis_context, &pipeline));

    genesis_pipeline_set_latency(pipeline, latency);
    genesis_pipeline_set_sample_rate(pipeline, project->sample_rate);
    genesis_pipeline_set_channel_layout(pipeline, &project->channel_layout);

    AudioGraph *ag = ok_mem(create_zero<AudioGraph>());
    ag->project = project;
    ag->pipeline = pipeline;
    ag->play_head_pos = 0.0;
    ag->is_playing = false;
    ag->play_head_changed_flag.clear();

    ag->resample_descr = genesis_node_descriptor_find(ag->pipeline, "resample");
    if (!ag->resample_descr)
        panic("unable to find resampler");

    genesis_pipeline_set_underrun_callback(pipeline, underrun_callback, ag);

    project->events.attach_handler(EventProjectAudioClipsChanged,
            on_project_audio_clips_changed, ag);
    project->events.attach_handler(EventProjectAudioClipSegmentsChanged,
            on_project_audio_clip_segments_changed, ag);


    refresh_audio_clips(ag);
    refresh_audio_clip_segments(ag);

    return ag;

}
Example #3
0
static void refresh_audio_clips(AudioGraph *ag) {
    Project *project = ag->project;
    int ag_i = 0;
    int project_i = 0;
    for (;;) {
        AudioGraphClip *ag_clip = nullptr;
        AudioClip *project_clip = nullptr;
        if (ag_i < ag->audio_clip_list.length())
            ag_clip = ag->audio_clip_list.at(ag_i);
        if (project_i < project->audio_clip_list.length())
            project_clip = project->audio_clip_list.at(project_i);
        if (ag_clip && project_clip && ag_clip->audio_clip == project_clip) {
            ag_i += 1;
            project_i += 1;
        } else if (project_clip && !ag_clip) {
            ag_clip = ok_mem(create_zero<AudioGraphClip>());
            ag_clip->audio_clip = project_clip;
            ag_clip->audio_graph = ag;
            add_nodes_to_audio_clip(ag, ag_clip);
            ok_or_panic(ag->audio_clip_list.append(ag_clip));
            ag_i += 1;
            project_i += 1;
        } else if (!project_clip && ag_clip) {
            panic("TODO destroy audio graph clip");
            ag->audio_clip_list.swap_remove(ag_i);
        } else if (!project_clip && !ag_clip) {
            break;
        } else {
            panic("TODO replace nodes");
        }
    }
}
Example #4
0
static int init_playback_node(AudioGraph *ag) {
    MixerLine *master_mixer_line = ag->project->mixer_line_list.at(0);
    Effect *first_effect = master_mixer_line->effects.at(0);

    assert(first_effect->effect_type == EffectTypeSend);
    EffectSend *effect_send = &first_effect->effect.send;

    assert(effect_send->send_type == EffectSendTypeDevice);
    EffectSendDevice *send_device = &effect_send->send.device;

    SoundIoDevice *audio_device = get_device_for_id(ag, (DeviceId)send_device->device_id);
    if (!audio_device) {
        return GenesisErrorDeviceNotFound;
    }

    GenesisNodeDescriptor *playback_node_descr;
    int err;
    if ((err = genesis_audio_device_create_node_descriptor(ag->pipeline,
                    audio_device, &playback_node_descr)))
    {
        return err;
    }

    assert(!ag->master_node);
    ag->master_node = ok_mem(genesis_node_descriptor_create_node(playback_node_descr));

    soundio_device_unref(audio_device);

    return 0;
}
Example #5
0
PngImage::PngImage(const ByteBuffer &compressed_bytes) {
    if (png_sig_cmp((png_bytep)compressed_bytes.raw(), 0, 8))
        panic("not png file");

    png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png_ptr)
        panic("unable to create png read struct");

    png_infop info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr)
        panic("unable to create png info struct");

    // don't call any png_* functions outside of this function D:
    if (setjmp(png_jmpbuf(png_ptr)))
        panic("libpng has jumped the shark");

    png_set_sig_bytes(png_ptr, 8);

    PngIo png_io = {8, (unsigned char *)compressed_bytes.raw(), compressed_bytes.length()};
    png_set_read_fn(png_ptr, &png_io, read_png_data);

    png_read_info(png_ptr, info_ptr);

    _width  = png_get_image_width(png_ptr, info_ptr);
    _height = png_get_image_height(png_ptr, info_ptr);

    if (_width <= 0 || _height <= 0)
        panic("spritesheet image has no pixels");

    // bits per channel (not per pixel)
    int bits_per_channel = png_get_bit_depth(png_ptr, info_ptr);
    if (bits_per_channel != 8)
        panic("expected 8 bits per channel");

    int channel_count = png_get_channels(png_ptr, info_ptr);
    if (channel_count != 4)
        panic("expected 4 channels");

    int color_type = png_get_color_type(png_ptr, info_ptr);
    if (color_type != PNG_COLOR_TYPE_RGBA)
        panic("expected RGBA");

    _pitch = _width * bits_per_channel * channel_count / 8;
    _image_data.resize(_height * _pitch);
    png_bytep *row_ptrs = ok_mem(allocate_zero<png_bytep>(_height));

    for (int i = 0; i < _height; i++) {
        png_uint_32 q = (_height - i - 1) * _pitch;
        row_ptrs[i] = (png_bytep)_image_data.raw() + q;
    }

    png_read_image(png_ptr, row_ptrs);
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    destroy(row_ptrs, _height);
}
Example #6
0
File: gui.cpp Project: EQ4/genesis
Gui::Gui(GenesisContext *context, ResourceBundle *resource_bundle) :
    _running(true),
    _focus_window(nullptr),
    _utility_window(create_utility_window()),
    _resource_bundle(resource_bundle),
    _spritesheet(this, "spritesheet"),
    img_entry_dir(_spritesheet.get_image_info("font-awesome/folder.png")),
    img_entry_dir_open(_spritesheet.get_image_info("font-awesome/folder-open.png")),
    img_entry_file(_spritesheet.get_image_info("font-awesome/file.png")),
    img_plus(_spritesheet.get_image_info("font-awesome/plus-square.png")),
    img_minus(_spritesheet.get_image_info("font-awesome/minus-square.png")),
    img_microphone(_spritesheet.get_image_info("font-awesome/microphone.png")),
    img_volume_up(_spritesheet.get_image_info("font-awesome/volume-up.png")),
    img_check(_spritesheet.get_image_info("font-awesome/check.png")),
    img_caret_right(_spritesheet.get_image_info("font-awesome/caret-right.png")),
    img_caret_down(_spritesheet.get_image_info("font-awesome/caret-down.png")),
    img_arrow_up(_spritesheet.get_image_info("font-awesome/arrow-up.png")),
    img_arrow_down(_spritesheet.get_image_info("font-awesome/arrow-down.png")),
    img_arrow_left(_spritesheet.get_image_info("font-awesome/arrow-left.png")),
    img_arrow_right(_spritesheet.get_image_info("font-awesome/arrow-right.png")),
    img_music(_spritesheet.get_image_info("font-awesome/music.png")),
    img_plug(_spritesheet.get_image_info("font-awesome/plug.png")),
    img_exclamation_circle(_spritesheet.get_image_info("font-awesome/exclamation-circle.png")),
    img_null(_spritesheet.get_image_info("img/null.png")),
    img_play_head(_spritesheet.get_image_info("img/play_head.png")),
    _genesis_context(context),
    dragging(false),
    drag_data(nullptr),
    drag_window(nullptr)
{

    ft_ok(FT_Init_FreeType(&_ft_library));
    _resource_bundle->get_file_buffer("font.ttf", _default_font_buffer);
    ft_ok(FT_New_Memory_Face(_ft_library, (FT_Byte*)_default_font_buffer.raw(),
                _default_font_buffer.length(), 0, &_default_font_face));

    cursor_default = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
    cursor_ibeam = glfwCreateStandardCursor(GLFW_IBEAM_CURSOR);
    cursor_hresize = glfwCreateStandardCursor(GLFW_HRESIZE_CURSOR);
    cursor_vresize = glfwCreateStandardCursor(GLFW_VRESIZE_CURSOR);

    genesis_set_audio_device_callback(_genesis_context, audio_device_callback, this);
    genesis_set_midi_device_callback(_genesis_context, midi_device_callback, this);

    genesis_set_sound_backend_disconnect_callback(_genesis_context, sound_backend_disconnect_callback, this);

    genesis_flush_events(_genesis_context);
    genesis_refresh_midi_devices(_genesis_context);

    gui_mutex = ok_mem(os_mutex_create());

    os_mutex_lock(gui_mutex);
}
Example #7
0
static void add_audio_node_to_audio_clip(AudioGraph *ag, AudioGraphClip *clip) {
    assert(!clip->node_descr);
    assert(!clip->node);

    ok_or_panic(project_ensure_audio_asset_loaded(ag->project, clip->audio_clip->audio_asset));
    GenesisAudioFile *audio_file = clip->audio_clip->audio_asset->audio_file;

    const struct SoundIoChannelLayout *channel_layout =
        genesis_audio_file_channel_layout(audio_file);
    int sample_rate = genesis_audio_file_sample_rate(audio_file);

    const char *name = "audio_clip";
    char *description = create_formatted_str("Audio Clip: %s",
            clip->audio_clip->name.encode().raw());
    GenesisNodeDescriptor *node_descr = ok_mem(genesis_create_node_descriptor(ag->pipeline, 2, name, description));
    free(description); description = nullptr;

    genesis_node_descriptor_set_userdata(node_descr, clip);

    struct GenesisPortDescriptor *audio_out_port = genesis_node_descriptor_create_port(
            node_descr, 0, GenesisPortTypeAudioOut, "audio_out");

    struct GenesisPortDescriptor *events_in_port = genesis_node_descriptor_create_port(
            node_descr, 1, GenesisPortTypeEventsIn, "events_in");

    if (!audio_out_port || !events_in_port)
        panic("unable to create ports");

    genesis_audio_port_descriptor_set_channel_layout(audio_out_port, channel_layout, true, -1);
    genesis_audio_port_descriptor_set_sample_rate(audio_out_port, sample_rate, true, -1);

    genesis_node_descriptor_set_run_callback(node_descr, audio_clip_node_run);
    genesis_node_descriptor_set_seek_callback(node_descr, audio_clip_node_seek);
    genesis_node_descriptor_set_create_callback(node_descr, audio_clip_node_create);
    genesis_node_descriptor_set_destroy_callback(node_descr, audio_clip_node_destroy);

    clip->node_descr = node_descr;
    clip->node = ok_mem(genesis_node_descriptor_create_node(node_descr));
}
Example #8
0
void os_spawn_process(const char *exe, const List<ByteBuffer> &args, bool detached) {
    pid_t pid = fork();
    if (pid == -1)
        panic("fork failed");
    if (pid != 0)
        return;
    if (detached) {
        if (setsid() == -1)
            panic("process detach failed");
    }

    const char **argv = ok_mem(allocate_zero<const char *>(args.length() + 2));
    argv[0] = exe;
    argv[args.length() + 1] = nullptr;
    for (int i = 0; i < args.length(); i += 1) {
        argv[i + 1] = args.at(i).raw();
    }
    execvp(exe, const_cast<char * const *>(argv));
    panic("execvp failed: %s", strerror(errno));
}
Example #9
0
int audio_graph_create_render(Project *project, GenesisContext *genesis_context,
        const GenesisExportFormat *export_format, const ByteBuffer &out_path,
        AudioGraph **out_audio_graph)
{
    AudioGraph *ag = audio_graph_create_common(project, genesis_context, 0.10);

    ag->render_export_format = *export_format;
    ag->render_out_path = out_path;

    ag->render_frame_index = 0;
    ag->render_frame_count = project_get_duration_frames(project);
    ag->render_cond = ok_mem(os_cond_create());
    ag->is_playing = true;

    ag->render_descr = genesis_create_node_descriptor(ag->pipeline,
            1, "render", "Master render node.");
    if (!ag->render_descr)
        panic("unable to create render node descriptor");

    genesis_node_descriptor_set_userdata(ag->render_descr, ag);
    genesis_node_descriptor_set_run_callback(ag->render_descr, render_node_run);
    genesis_node_descriptor_set_activate_callback(ag->render_descr, render_node_activate);
    ag->render_port_descr = genesis_node_descriptor_create_port(
            ag->render_descr, 0, GenesisPortTypeAudioIn, "audio_in");
    if (!ag->render_port_descr)
        panic("unable to create render port descriptor");
    genesis_audio_port_descriptor_set_channel_layout(ag->render_port_descr,
            &project->channel_layout, true, -1);
    genesis_audio_port_descriptor_set_sample_rate(ag->render_port_descr,
            export_format->sample_rate, true, -1);

    ag->master_node = ok_mem(genesis_node_descriptor_create_node(ag->render_descr));

    ag->render_stream = ok_mem(genesis_audio_file_stream_create(ag->pipeline->context));

    int render_sample_rate = genesis_audio_file_codec_best_sample_rate(export_format->codec,
            export_format->sample_rate);

    genesis_audio_file_stream_set_sample_rate(ag->render_stream, render_sample_rate);
    genesis_audio_file_stream_set_channel_layout(ag->render_stream, &project->channel_layout);

    ByteBuffer encoded;
    encoded = project->tag_title.encode();
    genesis_audio_file_stream_set_tag(ag->render_stream, "title", -1, encoded.raw(), encoded.length());

    encoded = project->tag_artist.encode();
    genesis_audio_file_stream_set_tag(ag->render_stream, "artist", -1, encoded.raw(), encoded.length());

    encoded = project->tag_album_artist.encode();
    genesis_audio_file_stream_set_tag(ag->render_stream, "album_artist", -1, encoded.raw(), encoded.length());

    encoded = project->tag_album.encode();
    genesis_audio_file_stream_set_tag(ag->render_stream, "album", -1, encoded.raw(), encoded.length());

    // TODO looks like I messed up the year tag; it should actually be ISO 8601 "date"
    // TODO so we need to write the date tag here, not year.

    genesis_audio_file_stream_set_export_format(ag->render_stream, export_format);

    int err;
    if ((err = genesis_audio_file_stream_open(ag->render_stream, out_path.raw(),
                    out_path.length())))
    {
        audio_graph_destroy(ag);
        return err;
    }

    *out_audio_graph = ag;
    return 0;
}
Example #10
0
void audio_graph_start_pipeline(AudioGraph *ag) {
    int err;

    ag->start_play_head_pos = ag->play_head_pos;

    if (genesis_pipeline_is_running(ag->pipeline))
        return;

    int target_sample_rate = genesis_pipeline_get_sample_rate(ag->pipeline);
    SoundIoChannelLayout *target_channel_layout = genesis_pipeline_get_channel_layout(ag->pipeline);

    int audio_file_node_count = ag->audio_file_port_descr ? 1 : 0;

    if (audio_file_node_count >= 1) {

        if (ag->audio_file_node) {
            genesis_node_destroy(ag->audio_file_node);
            ag->audio_file_node = nullptr;
        }

        if (ag->preview_audio_file) {
            // Set channel layout
            const struct SoundIoChannelLayout *channel_layout =
                genesis_audio_file_channel_layout(ag->preview_audio_file);
            genesis_audio_port_descriptor_set_channel_layout(
                    ag->audio_file_port_descr, channel_layout, true, -1);

            // Set sample rate
            int sample_rate = genesis_audio_file_sample_rate(ag->preview_audio_file);
            genesis_audio_port_descriptor_set_sample_rate(ag->audio_file_port_descr, sample_rate, true, -1);

        } else {
            genesis_audio_port_descriptor_set_channel_layout(
                    ag->audio_file_port_descr, target_channel_layout, true, -1);
            genesis_audio_port_descriptor_set_sample_rate(
                    ag->audio_file_port_descr, target_sample_rate, true, -1);
        }
        ag->audio_file_node = ok_mem(genesis_node_descriptor_create_node(ag->audio_file_descr));
    }


    int resample_audio_out_index = genesis_node_descriptor_find_port_index(ag->resample_descr, "audio_out");
    assert(resample_audio_out_index >= 0);

    // one for each of the audio clips and one for the sample file preview node
    int mix_port_count = audio_file_node_count + ag->audio_clip_list.length();

    ok_or_panic(create_mixer_descriptor(ag->pipeline, mix_port_count, &ag->mixer_descr));
    ag->mixer_node = ok_mem(genesis_node_descriptor_create_node(ag->mixer_descr));

    ok_or_panic(genesis_connect_audio_nodes(ag->mixer_node, ag->master_node));

    // We start on mixer port index 1 because index 0 is the audio out. Index 1 is
    // the first audio in.
    int next_mixer_port = 1;
    if (audio_file_node_count >= 1) {
        int audio_out_port_index = genesis_node_descriptor_find_port_index(ag->audio_file_descr, "audio_out");
        if (audio_out_port_index < 0)
            panic("port not found");

        GenesisPort *audio_out_port = genesis_node_port(ag->audio_file_node, audio_out_port_index);
        GenesisPort *audio_in_port = genesis_node_port(ag->mixer_node, next_mixer_port++);

        if ((err = genesis_connect_ports(audio_out_port, audio_in_port))) {
            if (err == GenesisErrorIncompatibleChannelLayouts ||
                err == GenesisErrorIncompatibleSampleRates)
            {
                ag->resample_node = ok_mem(genesis_node_descriptor_create_node(ag->resample_descr));
                ok_or_panic(genesis_connect_audio_nodes(ag->audio_file_node, ag->resample_node));

                GenesisPort *audio_out_port = genesis_node_port(ag->resample_node, resample_audio_out_index);
                ok_or_panic(genesis_connect_ports(audio_out_port, audio_in_port));
            } else {
                ok_or_panic(err);
            }
        }
    }

    for (int i = 0; i < ag->audio_clip_list.length(); i += 1) {
        AudioGraphClip *clip = ag->audio_clip_list.at(i);

        int audio_out_port_index = genesis_node_descriptor_find_port_index(clip->node_descr, "audio_out");
        if (audio_out_port_index < 0)
            panic("port not found");

        GenesisPort *audio_out_port = genesis_node_port(clip->node, audio_out_port_index);
        GenesisPort *audio_in_port = genesis_node_port(ag->mixer_node, next_mixer_port++);

        if ((err = genesis_connect_ports(audio_out_port, audio_in_port))) {
            if (err == GenesisErrorIncompatibleChannelLayouts ||
                err == GenesisErrorIncompatibleSampleRates)
            {
                clip->resample_node = ok_mem(genesis_node_descriptor_create_node(ag->resample_descr));
                ok_or_panic(genesis_connect_audio_nodes(clip->node, clip->resample_node));

                GenesisPort *audio_out_port = genesis_node_port(clip->resample_node,
                        resample_audio_out_index);
                ok_or_panic(genesis_connect_ports(audio_out_port, audio_in_port));
            } else {
                ok_or_panic(err);
            }
        }

        GenesisPort *events_in_port = genesis_node_port(clip->node, 1);
        GenesisPort *events_out_port = genesis_node_port(clip->event_node, 0);

        ok_or_panic(genesis_connect_ports(events_out_port, events_in_port));
    }


    fprintf(stderr, "\nStarting pipeline...\n");
    genesis_debug_print_pipeline(ag->pipeline);

    double start_time = ag->play_head_pos;

    assert(next_mixer_port == mix_port_count + 1);
    if ((err = genesis_pipeline_start(ag->pipeline, start_time)))
        panic("unable to start pipeline: %s", genesis_strerror(err));
}
Example #11
0
SettingsFile *settings_file_open(const ByteBuffer &path) {
    SettingsFile *sf = ok_mem(create_zero<SettingsFile>());

    sf->path = path;

    // default settings
    sf->open_project_id = uint256::zero();
    sf->user_name = "";
    sf->user_id = uint256::zero();

    FILE *f = fopen(path.raw(), "rb");
    if (!f) {
        if (errno == ENOENT) {
            // no settings file, leave everything at default
            return sf;
        }
        panic("unable to open settings file: %s", strerror(errno));
    }

    sf->state = SettingsFileStateStart;

    LaxJsonContext *json = ok_mem(lax_json_create());
    sf->json = json;

    json->userdata = sf;
    json->string = on_string;
    json->number = on_number;
    json->primitive = on_primitive;
    json->begin = on_begin;
    json->end = on_end;

    struct stat st;
    if (fstat(fileno(f), &st))
        panic("fstat failed");

    ByteBuffer buf;
    buf.resize(st.st_size);

    int amt_read = fread(buf.raw(), 1, buf.length(), f);

    if (fclose(f))
        panic("fclose error");

    if (amt_read != buf.length())
        panic("error reading settings file");

    handle_parse_error(sf, lax_json_feed(json, amt_read, buf.raw()));
    handle_parse_error(sf, lax_json_eof(json));

    for (int i = 0; i < sf->open_windows.length(); i += 1) {
        SettingsFileOpenWindow *open_window = &sf->open_windows.at(i);
        if (open_window->perspective_index < 0 || open_window->perspective_index >= sf->perspectives.length()) {
            panic("window %d perspective index out of bounds: %d", i + 1, open_window->perspective_index);
        }
    }

    sf->json = nullptr;
    lax_json_destroy(json);

    return sf;
}
Example #12
0
static int on_begin(struct LaxJsonContext *json, enum LaxJsonType type) {
    SettingsFile *sf = (SettingsFile *) json->userdata;
    switch (sf->state) {
        default:
            return parse_error(sf, (type == LaxJsonTypeObject) ? "unexpected object" : "unexpected array");
        case SettingsFileStateStart:
            if (type != LaxJsonTypeObject)
                return parse_error(sf, "expected object");
            sf->state = SettingsFileStateReadyForProp;
            break;
        case SettingsFileStateExpectSampleDirs:
            if (type != LaxJsonTypeArray)
                return parse_error(sf, "expected array");
            sf->state = SettingsFileStateSampleDirsItem;
            break;
        case SettingsFileStatePerspectives:
            if (type != LaxJsonTypeArray)
                return parse_error(sf, "expected array");
            sf->state = SettingsFileStatePerspectivesItem;
            break;
        case SettingsFileStatePerspectivesItem:
            if (type != LaxJsonTypeObject)
                return parse_error(sf, "expected object");
            ok_or_panic(sf->perspectives.add_one());
            sf->current_perspective = &sf->perspectives.last();
            sf->state = SettingsFileStatePerspectivesItemProp;
            break;
        case SettingsFileStatePerspectivesItemPropDock:
            if (type != LaxJsonTypeObject)
                return parse_error(sf, "expected object");
            sf->state = SettingsFileStateDockItemProp;
            break;
        case SettingsFileStateDockItemPropTabs:
            if (type != LaxJsonTypeArray)
                return parse_error(sf, "expected array");
            sf->state = SettingsFileStateTabName;
            break;
        case SettingsFileStateDockItemPropChildA:
            ok_or_panic(sf->dock_stack.append(sf->current_dock));
            sf->current_dock->child_a = ok_mem(create_zero<SettingsFileDock>());
            sf->current_dock = sf->current_dock->child_a;
            sf->state = SettingsFileStateDockItemProp;
            break;
        case SettingsFileStateDockItemPropChildB:
            ok_or_panic(sf->dock_stack.append(sf->current_dock));
            sf->current_dock->child_b = ok_mem(create_zero<SettingsFileDock>());
            sf->current_dock = sf->current_dock->child_b;
            sf->state = SettingsFileStateDockItemProp;
            break;
        case SettingsFileStateOpenWindows:
            if (type != LaxJsonTypeArray)
                return parse_error(sf, "expected array");
            sf->state = SettingsFileStateOpenWindowItem;
            break;
        case SettingsFileStateOpenWindowItem:
            if (type != LaxJsonTypeObject)
                return parse_error(sf, "expected object");
            ok_or_panic(sf->open_windows.add_one());
            sf->current_open_window = &sf->open_windows.last();
            sf->state = SettingsFileStateOpenWindowItemProp;
            break;
    }
    return 0;
}