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
Caller Related
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
@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
@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- Requirednote_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}
}
@spec delete_note(t(), Zongzi.Timeline.SeqID.t()) :: {:ok, t()} | {:error, term()}
删除 seq_id → 墓碑(保留在链表中以维护邻接稳定性)。
@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(或相等)。
@spec gc(t(), [Zongzi.Intervention.t()], [Zongzi.Intervention.t()]) :: {:ok, t()}
回收无 intervention 引用的墓碑,将其从链表中移除。
引用判定走各 intervention 的 Anchor.Strategy.referenced_seqs/1
(strategy 为 nil 时回退 NoteTriplet)。
extra_refs 是为了使 undo 可用。
@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}}
@spec has_node?(t(), Zongzi.Timeline.SeqID.t()) :: boolean()
给定 seq_id 是否在链表中。
@spec insert_note(t(), Zongzi.Score.Note.t()) :: {:ok, t(), Zongzi.Score.Note.t()} | {:error, term()}
将音符追加到 Timeline 末尾。
@spec insert_note_after(t(), Zongzi.Score.Note.t(), Zongzi.Timeline.SeqID.t()) :: {:ok, t(), Zongzi.Score.Note.t()} | {:error, term()}
在 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 之前插入音符。
@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。
@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 位置。
Create blank Timeline.
Examples
iex> Timeline.new("Track-a")
{:ok, %Timeline{track_id: "Track-a", next_seq: 1}}
@spec node_next(t(), Zongzi.Timeline.SeqID.t()) :: Zongzi.Timeline.SeqID.t() | nil
获得给定时间线下某 SeqID 的下一个 SeqID 。
@spec node_prev(t(), Zongzi.Timeline.SeqID.t()) :: Zongzi.Timeline.SeqID.t() | nil
获得给定时间线下某 SeqID 的上一个 SeqID 。
@spec note_id_for(t(), Zongzi.Timeline.SeqID.t()) :: {:ok, Zongzi.Util.ID.t(Zongzi.Score.Note.t())} | :error
获得给定 SeqID 的 NoteID 。
@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 自动分配。
@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)。
@spec to_list(t()) :: [Zongzi.Timeline.SeqID.t()]
返回完整 seq_id 列表(含墓碑)。
Validate Timeline.