Zongzi.Timeline (zongzi v0.3.0)

Copy Markdown

The source of truth of the note sequences, only records the relationships between them.

Implemented using a doubly linked list.

独立于 Note 的生命周期——Note 被 split/merge/drag 后, Timeline 维护的 seq_id 序列始终反映最新的全序关系。

Data Fields

  • head / tail — 链表首尾指针(nil 表示空链表)
  • nodes%{SeqID.t() => {prev :: SeqID.t() | nil, next :: SeqID.t() | nil}},双向链表

  • seq_map — seq_id → note_id 的反向查找
  • tombstones — 已删除的 seq_id,保留在链表中以维护邻接稳定性

Record Score Update

For single note:

  • 在末尾添加音符
  • 在序列的特定位置添加音符(基于目前存在的音符序列的位置)
  • 切开/合并特定音符
  • 可能需要变更音符排序的拖拽音符
  • Delete note

For batch operations:

  • Append notes one time
  • 批量在某音符前/后插入一堆音符
  • Delete several notes

Query Primitives

See Zongzi.Timeline.Query.

Timeline not contain Note.

写操作后 Caller 侧 note 快照(notes_by_seq)的同步需要单独实现。

Summary

Functions

Rebuild Timeline within serialized arguments.

删除 seq_id → 墓碑(保留在链表中以维护邻接稳定性)。

批量删除 from_seq..to_seq 范围内的所有 seq_id(标记墓碑,不断链)。

回收无 intervention 引用的墓碑,将其从链表中移除。

基于 Timeline 自持的计数器生成新 SeqID。

给定 seq_id 是否在链表中。

将音符追加到 Timeline 末尾。

在 target_seq 之后插入音符。

在 target_seq 之前插入音符。

Merge two notes(timeline level), invoke Zongzi.Score.Note.merge/4 inside.

拖拽 seq 到 target_seq 的 before/after 位置。

Create blank Timeline.

获得给定时间线下某 SeqID 的下一个 SeqID 。

获得给定时间线下某 SeqID 的上一个 SeqID 。

获得给定 SeqID 的 NoteID 。

批量 splice:把一组 notes 整体接到 target_seq 之后。O(n) 建子链 + O(1) splice。

split_tick 处切开音符,返回前后两个 Note。

返回完整 seq_id 列表(含墓碑)。

Validate Timeline.

Types

t()

@type t() :: %Zongzi.Timeline{
  head: Zongzi.Timeline.SeqID.t() | nil,
  next_seq: pos_integer(),
  nodes: %{
    required(Zongzi.Timeline.SeqID.t()) =>
      {prev_seq_id :: Zongzi.Timeline.SeqID.t() | nil,
       next_seq_id :: Zongzi.Timeline.SeqID.t() | nil}
  },
  seq_map: %{
    required(Zongzi.Timeline.SeqID.t()) => Zongzi.Util.ID.t(Zongzi.Score.Note)
  },
  tail: Zongzi.Timeline.SeqID.t() | nil,
  tombstones: MapSet.t(Zongzi.Timeline.SeqID.t()),
  track_id: Zongzi.Util.ID.t()
}

Functions

build(attrs)

@spec build(%{
  :track_id => Zongzi.Util.ID.t(),
  :note_order => [Zongzi.Timeline.SeqID.t()],
  optional(:seq_map) => %{
    required(Zongzi.Timeline.SeqID.t()) => Zongzi.Util.ID.t(Zongzi.Score.Note)
  },
  optional(:tombstones) => [Zongzi.Timeline.SeqID.t()],
  optional(:next_seq) => pos_integer()
}) :: {:ok, t()}

Rebuild Timeline within serialized arguments.

note_order → 链表,O(n)。

Attributes

  • track_id - Required
  • note_order - [SeqID.t()],有序列表(含墓碑)
  • seq_map — 可选,默认 %{}
  • tombstones[SeqID.t()],可选,默认 []
  • next_seq — 可选,默认 max(note_order) + 1

Examples

iex> Timeline.build(
...>   %{track_id: "t1", note_order: [],
...>   seq_map: %{}, tombstones: []})
{:ok, %Timeline{track_id: "t1", next_seq: 1}}

iex> Timeline.build(
...>   %{track_id: "t1", note_order: [1, 2],
...>   seq_map: %{1 => "N_a", 2 => "N_b"}, tombstones: [2]})
{:ok, %Timeline{
  track_id: "t1",
  head: 1, tail: 2,
  nodes: %{1 => {nil, 2}, 2 => {1, nil}},
  seq_map: %{1 => "N_a", 2 => "N_b"},
  tombstones: MapSet.new([2]), next_seq: 3}
}

delete_note(timeline, seq_id)

@spec delete_note(t(), Zongzi.Timeline.SeqID.t()) :: {:ok, t()} | {:error, term()}

删除 seq_id → 墓碑(保留在链表中以维护邻接稳定性)。

delete_range(timeline, from_seq, to_seq)

@spec delete_range(t(), Zongzi.Timeline.SeqID.t(), Zongzi.Timeline.SeqID.t()) ::
  {:ok, t()} | {:error, term()}

批量删除 from_seq..to_seq 范围内的所有 seq_id(标记墓碑,不断链)。

from_seq 必须链表中先于 to_seq(或相等)。

gc(timeline, interventions, extra_refs \\ [])

@spec gc(t(), [Zongzi.Intervention.t()], [Zongzi.Intervention.t()]) :: {:ok, t()}

回收无 intervention 引用的墓碑,将其从链表中移除。

引用判定走各 intervention 的 Anchor.Strategy.referenced_seqs/1 (strategy 为 nil 时回退 NoteTriplet)。

extra_refs 是为了使 undo 可用。

generate(timeline)

@spec generate(t()) :: {Zongzi.Timeline.SeqID.t(), t()}

基于 Timeline 自持的计数器生成新 SeqID。

Examples

iex> Timeline.new("Track-a") |> elem(1) |> Timeline.generate()
{1, %Zongzi.Timeline{track_id: "Track-a", next_seq: 2}}

iex> %Zongzi.Timeline{track_id: "Track-b", next_seq: 2} |> Timeline.generate()
{2, %Zongzi.Timeline{track_id: "Track-b", next_seq: 3}}

has_node?(timeline, seq_id)

@spec has_node?(t(), Zongzi.Timeline.SeqID.t()) :: boolean()

给定 seq_id 是否在链表中。

insert_note(timeline, note)

@spec insert_note(t(), Zongzi.Score.Note.t()) ::
  {:ok, t(), Zongzi.Score.Note.t()} | {:error, term()}

将音符追加到 Timeline 末尾。

insert_note_after(timeline, note, target_seq)

@spec insert_note_after(t(), Zongzi.Score.Note.t(), Zongzi.Timeline.SeqID.t()) ::
  {:ok, t(), Zongzi.Score.Note.t()} | {:error, term()}

在 target_seq 之后插入音符。

insert_note_before(timeline, note, target_seq)

@spec insert_note_before(t(), Zongzi.Score.Note.t(), Zongzi.Timeline.SeqID.t()) ::
  {:ok, t(), Zongzi.Score.Note.t()} | {:error, term()}

在 target_seq 之前插入音符。

merge_notes(timeline, note_a, note_b, merged_id)

@spec merge_notes(
  t(),
  Zongzi.Score.Note.t(),
  Zongzi.Score.Note.t(),
  Zongzi.Util.ID.t(Zongzi.Score.Note.t())
) :: {:ok, t(), Zongzi.Score.Note.t()} | {:error, term()}

Merge two notes(timeline level), invoke Zongzi.Score.Note.merge/4 inside.

seq_id_2 变墓碑,seq_id_1 保留并指向 merged_note_id。

move_note(timeline, seq_id, target_seq, where)

@spec move_note(
  t(),
  selected_seq_id :: Zongzi.Timeline.SeqID.t(),
  target_seq_id :: Zongzi.Timeline.SeqID.t(),
  :before | :after
) :: {:ok, t()} | {:error, term()}

拖拽 seq 到 target_seq 的 before/after 位置。

new(track_id)

Create blank Timeline.

Examples

iex> Timeline.new("Track-a")
{:ok, %Timeline{track_id: "Track-a", next_seq: 1}}

node_next(timeline, seq_id)

@spec node_next(t(), Zongzi.Timeline.SeqID.t()) :: Zongzi.Timeline.SeqID.t() | nil

获得给定时间线下某 SeqID 的下一个 SeqID 。

node_prev(timeline, seq_id)

@spec node_prev(t(), Zongzi.Timeline.SeqID.t()) :: Zongzi.Timeline.SeqID.t() | nil

获得给定时间线下某 SeqID 的上一个 SeqID 。

note_id_for(timeline, seq_id)

@spec note_id_for(t(), Zongzi.Timeline.SeqID.t()) ::
  {:ok, Zongzi.Util.ID.t(Zongzi.Score.Note.t())} | :error

获得给定 SeqID 的 NoteID 。

splice_after(timeline, notes, target_seq)

@spec splice_after(t(), [Zongzi.Score.Note.t()], Zongzi.Timeline.SeqID.t()) ::
  {:ok, t(), [Zongzi.Score.Note.t()]} | {:error, term()}

批量 splice:把一组 notes 整体接到 target_seq 之后。O(n) 建子链 + O(1) splice。

notes 按传入顺序依次链接,seq_id 自动分配。

split_note(timeline, note, split_tick, new_id, attrs \\ [])

@spec split_note(
  t(),
  Zongzi.Score.Note.t(),
  non_neg_integer(),
  Zongzi.Util.ID.t(Zongzi.Score.Note.t()),
  map() | keyword()
) :: {:ok, t(), Zongzi.Score.Note.t(), Zongzi.Score.Note.t()} | {:error, term()}

split_tick 处切开音符,返回前后两个 Note。

后半音符自动分配新 seq_id 并 splice 到原音符后。 attrs 可选,透传给 Note.split/4(如给后半音符不同的 lyric)。

to_list(timeline)

@spec to_list(t()) :: [Zongzi.Timeline.SeqID.t()]

返回完整 seq_id 列表(含墓碑)。

validate(timeline)

Validate Timeline.