Documentation
¶
Overview ¶
Package webcore is the Web UI's core session: the document state the browser edits, behind the coarse boundary the front end speaks. It owns canonical state and validation; the WASM glue in web/wasm only translates calls. Errors carry stable codes that cross the boundary as structured envelopes, mirrored by the TypeScript contract in web/app/src/boundary.
Index ¶
- Constants
- type AreaSnapshot
- type Audition
- type BankSnapshot
- type DiskSnapshot
- type EffectsSnapshot
- type EnvelopeSnapshot
- type Error
- type FileSnapshot
- type ImportEstimate
- type InstrumentSnapshot
- type InstrumentVoice
- type LoopSnapshot
- type SFZResult
- type SchemaField
- type Session
- func (s *Session) AddArea(bank, voiceSlot int) (Snapshot, *Error)
- func (s *Session) AddBank(data []byte, slot int) (Snapshot, *Error)
- func (s *Session) AddVoice(fzvData []byte) (Snapshot, *Error)
- func (s *Session) AuditionPCM(fileName string) (*Audition, *Error)
- func (s *Session) AuditionSlot(slot int) (*Audition, *Error)
- func (s *Session) BeginGesture()
- func (s *Session) Close() (Snapshot, *Error)
- func (s *Session) CommitGesture() bool
- func (s *Session) DeleteArea(bank, area int) (Snapshot, *Error)
- func (s *Session) DeleteFile(name string) (Snapshot, *Error)
- func (s *Session) DuplicateArea(bank, area int) (Snapshot, *Error)
- func (s *Session) EstimateImport(files map[string][]byte, rate uint32, channel string) (*ImportEstimate, *Error)
- func (s *Session) ExportImage() ([]byte, *Error)
- func (s *Session) ExportImageAt(index int) ([]byte, *Error)
- func (s *Session) ExtractFile(name string) ([]byte, *Error)
- func (s *Session) ExtractVoiceSlot(slot int, format string) ([]byte, string, *Error)
- func (s *Session) ImportSFZ(files map[string][]byte, sfzPath string, rate int, fitToDisk, split bool, ...) (SFZResult, *Error)
- func (s *Session) ImportWAV(filename string, wavData []byte, rate uint32, channel string) (Snapshot, *Error)
- func (s *Session) ImportWAVFolder(files map[string][]byte, rate int, fitToDisk bool, channel string) (SFZResult, *Error)
- func (s *Session) ImportWAVToInstrument(filename string, wavData []byte, rate uint32, channel string) (Snapshot, *Error)
- func (s *Session) LoadFZF(data []byte) (Snapshot, *Error)
- func (s *Session) MapVoice(voiceSlot int) (Snapshot, *Error)
- func (s *Session) NewDisk(label string) (Snapshot, *Error)
- func (s *Session) NewInstrument(name string) (Snapshot, *Error)
- func (s *Session) OpenImage(data []byte) (Snapshot, *Error)
- func (s *Session) OpenImagePair(a, b []byte) (Snapshot, *Error)
- func (s *Session) Peaks(fileName string, startFrame, endFrame, buckets int) ([]int16, *Error)
- func (s *Session) Redo() (Snapshot, *Error)
- func (s *Session) RenameBank(bank int, name string) (Snapshot, *Error)
- func (s *Session) RenameDisk(label string) (Snapshot, *Error)
- func (s *Session) RenameVoiceSlot(slot int, name string) (Snapshot, *Error)
- func (s *Session) SetAreaField(bank, area int, field string, value int) (Snapshot, *Error)
- func (s *Session) SetBendRange(value int) (Snapshot, *Error)
- func (s *Session) SetEffectCell(controller, target, value int) (Snapshot, *Error)
- func (s *Session) SetEnvelope(fileName, which string, sustain, end int, rates, stops []int) (Snapshot, *Error)
- func (s *Session) SetGeneration(fileName string, startFrame, endFrame int) (Snapshot, *Error)
- func (s *Session) SetLoop(fileName string, index, startFrame, endFrame int) (Snapshot, *Error)
- func (s *Session) SetLoopSelect(fileName string, sustain, release int) (Snapshot, *Error)
- func (s *Session) SetParamNumber(fileName, fieldID string, value int) (Snapshot, *Error)
- func (s *Session) SetParamOption(fileName, fieldID, option string) (Snapshot, *Error)
- func (s *Session) SetSlotEnvelope(slot int, which string, sustain, end int, rates, stops []int) (Snapshot, *Error)
- func (s *Session) SetSlotGeneration(slot, startFrame, endFrame int) (Snapshot, *Error)
- func (s *Session) SetSlotLoop(slot, index, startFrame, endFrame int) (Snapshot, *Error)
- func (s *Session) SetSlotLoopAttr(slot, index, xf, tm int) (Snapshot, *Error)
- func (s *Session) SetSlotLoopSelect(slot, sustain, release int) (Snapshot, *Error)
- func (s *Session) SetSlotParamNumber(slot int, fieldID string, value int) (Snapshot, *Error)
- func (s *Session) SetSlotParamOption(slot int, fieldID, option string) (Snapshot, *Error)
- func (s *Session) SlotPeaks(slot, startFrame, endFrame, buckets int) ([]int16, *Error)
- func (s *Session) Snapshot() Snapshot
- func (s *Session) SwapAreas(bank, a, b int) (Snapshot, *Error)
- func (s *Session) Undo() (Snapshot, *Error)
- type Snapshot
- type VoiceDetail
Constants ¶
const ( ExtractFZV = "fzv" ExtractWAV = "wav" )
Extract formats for ExtractVoiceSlot.
const ( VerdictFits = "fits" VerdictSplits = "splits" VerdictWontFit = "wont-fit" ReasonSampleMemory = "sample-memory" ReasonDiskRoom = "disk-room" ReasonVoiceLimit = "voice-limit" )
Import estimate verdicts and refusal reasons. The verdict names what the matching import call would do with the same files, rate, and channel; the reason narrows a refusal to the constraint that bites first.
const ( ChannelLeft = "left" ChannelRight = "right" ChannelMix = "mix" )
Channel names accepted by ImportWAV, mirrored by the TypeScript contract.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AreaSnapshot ¶
type AreaSnapshot struct {
VoiceSlot int `json:"voiceSlot"`
VoiceName string `json:"voiceName"`
KeyLow int `json:"keyLow"`
KeyHigh int `json:"keyHigh"`
Root int `json:"root"`
VelLow int `json:"velLow"`
VelHigh int `json:"velHigh"`
MidiChannel int `json:"midiChannel"`
Output int `json:"output"`
OutputLabel string `json:"outputLabel"`
Volume int `json:"volume"`
}
AreaSnapshot is one key split in a bank: the fields R12 makes editable. MidiChannel speaks the display scale (1 to 16) like the CLI; Output carries the raw gchn bitmask plus its rendered label.
type Audition ¶
type Audition struct {
SampleRate int `json:"sampleRate"`
Root int `json:"root"`
PCM []int16 `json:"-"`
}
Audition carries one voice's PCM for the preview path (R20 to R22): the declared sample rate, the root key the pitch maths centres on, and the decoded samples. The PCM crosses the boundary as a transferable; it stays out of JSON.
type BankSnapshot ¶
type BankSnapshot struct {
Name string `json:"name"`
Areas []AreaSnapshot `json:"areas"`
}
BankSnapshot is one bank sector: its name and its areas in vp order.
type DiskSnapshot ¶
type DiskSnapshot struct {
Label string `json:"label"`
UsedBytes int `json:"usedBytes"`
CapacityBytes int `json:"capacityBytes"`
Disks int `json:"disks"`
MissingDisk int `json:"missingDisk,omitempty"`
Files []FileSnapshot `json:"files"`
Instrument *InstrumentSnapshot `json:"instrument,omitempty"`
}
DiskSnapshot describes the open disk set for the UI. Instrument is the parsed full dump when the disk carries one. Disks is 1 or 2: a split instrument spans a pair and the capacity figures cover both (R23). MissingDisk names the absent half when one image of a pair was opened alone (R5): 2 means "this is disk 1, disk 2 is missing".
type EffectsSnapshot ¶
EffectsSnapshot is the instrument's global effect block (R19): pitch bend range plus the 3 by 7 controller modulation matrix. Rows are mod wheel, foot pedal, aftertouch; columns are LFO pitch, LFO amp, LFO filter, LFO resonance, DCA offset, DCF offset, and DCQ offset, in R19's order.
type EnvelopeSnapshot ¶
type EnvelopeSnapshot struct {
Sustain int `json:"sustain"`
End int `json:"end"`
Rates []int `json:"rates"`
Stops []int `json:"stops"`
}
EnvelopeSnapshot carries one envelope in the hardware display scale (0 to 99 for rates and stops), the same units the setters accept.
type Error ¶
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
// Item names the offending file, voice, or field where one
// exists, the way the spec's contract section promises.
Item string `json:"item,omitempty"`
// Detail carries the technical reason for a bug report; the
// message above stays the one a musician reads.
Detail string `json:"detail,omitempty"`
}
Error is a boundary error envelope: a stable machine code plus a human message. It never wraps a panic; the glue recovers those.
type FileSnapshot ¶
type FileSnapshot struct {
Name string `json:"name"`
Type string `json:"type"`
SizeBytes int `json:"sizeBytes"`
Params map[string]any `json:"params,omitempty"`
Voice *VoiceDetail `json:"voice,omitempty"`
}
FileSnapshot describes one directory entry for the UI. Voice files carry their editable parameter values keyed by schema field ID, plus the bespoke-editor detail (waveform extent, loops, envelopes).
type ImportEstimate ¶
type ImportEstimate struct {
// Bytes is the document's full dump growth: sector padded audio
// plus any voice header sectors the new slots need, plus the
// dump base when the import brings the instrument into being.
Bytes int `json:"bytes"`
// Seconds is the batch's play time at the target rate.
Seconds float64 `json:"seconds"`
// RoomSeconds is how much more mono audio the document holds at
// the target rate within its current disk count, bounded by real
// free sectors and the sampler's sample memory. A larger import
// may still land by splitting; the verdict reports that.
RoomSeconds float64 `json:"roomSeconds"`
Verdict string `json:"verdict"`
// Reason narrows VerdictWontFit; empty otherwise.
Reason string `json:"reason,omitempty"`
// AnyStereo is set when at least one file carries two channels,
// which is what makes the left, right, or mix question worth
// asking.
AnyStereo bool `json:"anyStereo"`
// OverCapFile names the first file over the sampler's memory at
// the target rate, with its play time in FileSeconds and the
// longest play time that memory loads at this rate in CapSeconds.
OverCapFile string `json:"overCapFile,omitempty"`
FileSeconds float64 `json:"fileSeconds,omitempty"`
CapSeconds float64 `json:"capSeconds,omitempty"`
// FitsAtRates lists the rates at which the whole batch would be
// accepted, for a refusal's way out; empty when none would.
FitsAtRates []int `json:"fitsAtRates,omitempty"`
}
ImportEstimate is the authoritative pre-flight answer for a WAV import: what the conversion would add and whether it can land. It shares fzutil.ResampledLen with the conversion itself, so the figures cannot drift from what an accepted import then produces.
type InstrumentSnapshot ¶
type InstrumentSnapshot struct {
FileName string `json:"fileName"`
Banks []BankSnapshot `json:"banks"`
Voices []InstrumentVoice `json:"voices"`
Effects *EffectsSnapshot `json:"effects,omitempty"`
}
InstrumentSnapshot is the disk's full dump as the UI edits it: banks of areas plus the voice list with unreferenced voices flagged.
type InstrumentVoice ¶
type InstrumentVoice struct {
Slot int `json:"slot"`
Name string `json:"name"`
Referenced bool `json:"referenced"`
Params map[string]any `json:"params,omitempty"`
Voice *VoiceDetail `json:"voice,omitempty"`
// velocity switch clones a voice header rather than the samples,
// so it costs no disk space. The UI says so instead of charging
// the user twice for one sound.
SharesAudio bool `json:"sharesAudio,omitempty"`
// AudioKey identifies the samples this slot plays: where they
// start and how many there are. It changes only when the audio
// changes, so a caller can cache decoded PCM and peaks against it
// instead of re-decoding on every parameter edit.
AudioKey string `json:"audioKey,omitempty"`
}
InstrumentVoice is one audible voice slot with its R13 flag, plus the editable surface the voice editor renders: schema params and the bespoke detail (loops, envelopes), parsed from the slot's header bytes. Loop positions arrive voice-relative; the dump's absolute addresses rebase here, mirroring the slot setters.
type LoopSnapshot ¶
type LoopSnapshot struct {
Start int `json:"start"`
End int `json:"end"`
XF int `json:"xf"`
Tm int `json:"tm"`
}
LoopSnapshot is one row of a voice's 8-loop table, in sample frames with the spec's flag bits masked.
type SFZResult ¶
SFZResult reports an import that ran the conversion pipeline: the new document snapshot plus the sample rate the core actually used, which fit to disk may have stepped down from the request (R9). The UI warns when Rate is below the asked rate.
type SchemaField ¶
type SchemaField struct {
ID string `json:"id"`
Label string `json:"label"`
Group string `json:"group"`
Kind string `json:"kind"`
Min int `json:"min"`
Max int `json:"max"`
Options []string `json:"options,omitempty"`
}
SchemaField declares one editable voice parameter: the UI renders a control from this declaration and nothing else (R14). Kind is one of "knob", "stepper", "note" (a stepper displayed as a note name), or "select". Numeric kinds carry Min and Max; select carries Options.
func Schema ¶
func Schema() []SchemaField
Schema returns the editable parameter schema the UI renders from.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session holds one open disk set. Not safe for concurrent use; the worker serialises calls.
func (*Session) AddArea ¶
AddArea appends an area playing an existing voice slot, with the container's default full key and velocity ranges, the voice's own root key, and every generator as its output.
func (*Session) AddBank ¶
AddBank places a .fzb per the placement matrix (R7). With an instrument open, the dump's bank sector lands at bank index slot (replacing that bank's mapping, or appending when slot is the first unused index); voice slots keep their meaning by index, the hardware bank load semantics. Without an instrument the .fzb itself becomes the instrument: bank plus voice headers, audio to be imported later.
func (*Session) AddVoice ¶
AddVoice places an .fzv per the placement matrix (R7): with no disk open it becomes a one voice instrument on a fresh disk; with no instrument it becomes a one voice instrument on the open disk; otherwise it joins the instrument's voice list. The join lands with a fresh area carrying the voice's own key range: the FZ format sizes a dump's voice area from bank references, so a voice no bank references would vanish on the next parse.
func (*Session) AuditionPCM ¶
AuditionPCM decodes a voice file's audio for preview, the same code path as the CLI's fzv extract.
func (*Session) AuditionSlot ¶
AuditionSlot decodes an instrument voice slot's audio for preview, through the same unpack path as the CLI's fzf unpack.
func (*Session) BeginGesture ¶
func (s *Session) BeginGesture()
BeginGesture opens an undo bracket: edits until CommitGesture coalesce into one history entry.
func (*Session) Close ¶
Close discards the open document and its history: back to the start state, nothing written (N3: durable output is the exported file, and the UI confirms unexported changes first).
func (*Session) CommitGesture ¶
CommitGesture closes the bracket, landing the coalesced entry. It reports whether the gesture changed anything: a press and release with no movement lands no entry, and the UI must not call such a document dirty.
func (*Session) DeleteArea ¶
DeleteArea removes an area, shifting later areas down (R11), and gives its voice slot back to the voice area with it: the format sizes a dump's voice area from the banks' area counts, so a voice no area references has nowhere to live. The freed voice's samples stay in the audio area until the next rebuild, and undo restores the area and the voice together.
func (*Session) DeleteFile ¶
DeleteFile removes a directory file from the disk. Deleting the full dump deletes the instrument; on a split pair the second disk goes with it.
func (*Session) DuplicateArea ¶
DuplicateArea appends a clone of an area: the velocity switch workflow (R11). The clone's voice header is copied into a fresh slot at the end of the voice area (growing it by a sector when full), so the two areas share audio and the PCM footprint is unchanged.
func (*Session) EstimateImport ¶
func (s *Session) EstimateImport(files map[string][]byte, rate uint32, channel string) (*ImportEstimate, *Error)
EstimateImport reports what importing files at rate would do to the document, without touching it: the size and play time the batch becomes, the room left, and whether the import fits, splits the instrument across two disks, or is refused. It reads every file the way the conversion would, so an unreadable WAV is refused here with the same error the import would give.
func (*Session) ExportImage ¶
ExportImage returns a copy of disk 1's image bytes. Opening then exporting is byte identical.
func (*Session) ExportImageAt ¶
ExportImageAt returns a copy of one image of the document: index 0 is disk 1, index 1 is disk 2 of a split pair (R25).
func (*Session) ExtractFile ¶
ExtractFile returns a copy of a directory file's bytes for saving. The full dump of a split pair comes back stitched, the whole instrument in one file.
func (*Session) ExtractVoiceSlot ¶
ExtractVoiceSlot returns one instrument voice as a standalone file: the unpacked .fzv (the CLI's fzf unpack output), or a 16-bit mono WAV at the voice's declared rate (the CLI's fzv extract output). The returned name is the voice's, for the download filename.
func (*Session) ImportSFZ ¶
func (s *Session) ImportSFZ(files map[string][]byte, sfzPath string, rate int, fitToDisk, split bool, channel string) (SFZResult, *Error)
ImportSFZ converts an SFZ instrument (its folder crossing the boundary as path to bytes pairs) through the CLI's pipeline and places it as the document's instrument, replacing any existing one (R7, R9). sfzPath names the .sfz inside files; empty means the one .sfz the folder holds. With split, the CLI's two disk build runs and the document becomes an image pair; otherwise fitToDisk steps the rate down to fit one disk, and a dump that still outgrows one disk splits automatically (R10 rejects only past two).
channel is ChannelLeft, ChannelRight, or ChannelMix, as it is for ImportWAVFolder: the UI asks once and the answer covers every stereo sample the instrument references.
func (*Session) ImportWAV ¶
func (s *Session) ImportWAV(filename string, wavData []byte, rate uint32, channel string) (Snapshot, *Error)
ImportWAV converts a WAV to an FZ voice and adds it to the open disk: the browser side of J1. The voice name derives from the filename the way the CLI derives it. channel is ChannelLeft, ChannelRight, or ChannelMix and only matters for stereo input.
func (*Session) ImportWAVFolder ¶
func (s *Session) ImportWAVFolder(files map[string][]byte, rate int, fitToDisk bool, channel string) (SFZResult, *Error)
ImportWAVFolder converts a folder of WAVs through the CLI's zero SFZ pipeline (sorted names, sequential keys up the keyboard, R8) and places the result as the document's instrument. channel is ChannelLeft, ChannelRight, or ChannelMix; the UI asks once and the answer covers every file in the batch.
func (*Session) ImportWAVToInstrument ¶
func (s *Session) ImportWAVToInstrument(filename string, wavData []byte, rate uint32, channel string) (Snapshot, *Error)
ImportWAVToInstrument converts a WAV through the CLI's importer and places the voice with AddVoice's matrix behaviour: the instrument grows by one voice, or comes into being when absent.
func (*Session) LoadFZF ¶
LoadFZF places a full dump per the placement matrix (R7): with no disk open it becomes the instrument of a fresh disk; otherwise it replaces the open disk's instrument. A dump too large for one disk splits into a two disk document (R25). The UI owns the replace prompt; the core just places.
func (*Session) MapVoice ¶
MapVoice maps a voice into the first bank with room, in one action (R13). The new area arrives with the container's playable defaults.
func (*Session) NewInstrument ¶
NewInstrument creates an empty instrument on the open disk (R4), or on a fresh disk when none is open. The format cannot express a dump with no voices (a dump's voice area is sized from bank references), so the empty instrument is one bank whose single area references a silent placeholder slot; the first voice that joins fills it.
func (*Session) OpenImage ¶
OpenImage replaces the session's disk with the given image after validating that the core can parse it. The bytes are copied, so the caller's buffer (often a transferable from the UI) is never aliased.
func (*Session) OpenImagePair ¶
OpenImagePair opens a two disk split instrument as one document (R5). The images may arrive in either order: the directory entry's disk number byte says which is which.
func (*Session) Peaks ¶
Peaks returns interleaved min/max sample pairs for the requested frame window of a voice, bucketed for display (R17). The window clamps to the voice's frame count; buckets must be positive. The PCM decode is the same code path as the CLI's fzv extract.
func (*Session) RenameBank ¶
RenameBank sets a bank's 12-character printable ASCII name (R11).
func (*Session) RenameDisk ¶
RenameDisk sets the open disk's 12-character printable ASCII label. On a split pair, disk 1 carries the document's name; disk 2 keeps the derived label it was formatted with.
func (*Session) RenameVoiceSlot ¶
RenameVoiceSlot sets a slot's 12-character printable ASCII name.
func (*Session) SetAreaField ¶
SetAreaField edits one Area field (R12). Numeric fields clamp to their ranges; midiChannel speaks the display scale (1 to 16); voiceSlot re-points the area at another voice.
func (*Session) SetBendRange ¶
SetBendRange writes the pitch bend range, clamped to the block's 0..127 byte.
func (*Session) SetEffectCell ¶
SetEffectCell writes one modulation matrix cell (R19): controller 0 to 2 (mod wheel, foot pedal, aftertouch) to target 0 to 6 in R19's destination order, clamped to 0..127.
func (*Session) SetEnvelope ¶
func (s *Session) SetEnvelope(fileName, which string, sustain, end int, rates, stops []int) (Snapshot, *Error)
SetEnvelope sets a whole envelope (which is "dca" or "dcf"): the sustain and end stage designations plus all eight rates and stops in the hardware display scale, clamped (R16).
func (*Session) SetGeneration ¶
SetGeneration sets a voice file's generation window (R14's generation start and end) in sample frames, clamped to the voice's own extent. A standalone voice starts at sample zero, so the cells already hold the frames the boundary speaks and no rebase applies.
func (*Session) SetLoop ¶
SetLoop sets loop index's start and end on a voice, clamped to the voice's frame count. The snapshot reports the frames the core confirmed (R17).
func (*Session) SetLoopSelect ¶
SetLoopSelect sets the sustain and release loop designations, clamped to 0..8 where 8 means none.
func (*Session) SetParamNumber ¶
SetParamNumber sets a numeric schema field on a voice file, clamping the value to the field's declared range (R14).
func (*Session) SetParamOption ¶
SetParamOption sets a select schema field on a voice file.
func (*Session) SetSlotEnvelope ¶
func (s *Session) SetSlotEnvelope(slot int, which string, sustain, end int, rates, stops []int) (Snapshot, *Error)
SetSlotEnvelope sets a whole envelope on an instrument voice slot: the same display-scale contract as the loose-file setter (R16).
func (*Session) SetSlotGeneration ¶
SetSlotGeneration sets an instrument voice slot's generation window (R14's generation start and end) in voice-relative frames, clamped to the slot's own frame count. The cells hold absolute addresses, so the write rebases; without that a window written as frames would address another voice's samples.
func (*Session) SetSlotLoop ¶
SetSlotLoop sets loop index's start and end on an instrument voice slot, in voice-relative frames clamped to the slot's frame count; the cells store absolute addresses, so the write rebases (R17).
func (*Session) SetSlotLoopAttr ¶
SetSlotLoopAttr sets loop index's cross-fade and multi-loop time on an instrument voice slot, clamped to the format's ranges.
func (*Session) SetSlotLoopSelect ¶
SetSlotLoopSelect sets the sustain and release loop designations on an instrument voice slot, clamped to 0..8 where 8 means none.
func (*Session) SetSlotParamNumber ¶
SetSlotParamNumber sets a numeric schema field on an instrument voice slot, clamping to the field's declared range (R14).
func (*Session) SetSlotParamOption ¶
SetSlotParamOption sets a select schema field on an instrument voice slot.
func (*Session) SlotPeaks ¶
SlotPeaks returns interleaved min/max sample pairs for a frame window of an instrument voice slot, the slot-addressed sibling of Peaks (R17).
type Snapshot ¶
type Snapshot struct {
Revision int `json:"revision"`
Disk *DiskSnapshot `json:"disk"`
CanUndo bool `json:"canUndo"`
CanRedo bool `json:"canRedo"`
}
Snapshot is the state the UI renders from. Revision is a monotonic per-session token; a changed revision means changed state.
type VoiceDetail ¶
type VoiceDetail struct {
Frames int `json:"frames"`
SampleRate int `json:"sampleRate"`
GenStart int `json:"genStart"`
GenEnd int `json:"genEnd"`
LoopSustain int `json:"loopSustain"`
LoopRelease int `json:"loopRelease"`
Loops []LoopSnapshot `json:"loops"`
Dca EnvelopeSnapshot `json:"dca"`
Dcf EnvelopeSnapshot `json:"dcf"`
}
VoiceDetail is the bespoke-editor surface of a voice: the waveform extent, the generation window, the loop table, and both envelopes (R14's Sample group, R16, R17).
GenStart and GenEnd are R14's generation start and end, in voice-relative sample frames like every other position the boundary carries. They sit here rather than in the schema because their bounds are this voice's own frame count: a schema field declares one static range for every voice, and a gened past waved would play the samples of whatever follows in the audio area.