Files
sanspie a46347e2c6
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 23m57s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
editor: add art studio and stage 12.5 workflows
2026-11-08 18:47:57 +03:00

144 lines
3.1 KiB
Go

package preview
import (
"bytes"
"encoding/binary"
"math"
"os"
"path/filepath"
"strconv"
"aego/asset"
"aego/audio/codec"
"aego/core/errs"
"aego/core/ids"
"aego/editor/core/tasks"
"aego/engine/i18n"
)
const peaksExt = ".peaks"
type peakKey struct {
id ids.AssetID
cols int
}
type peakItem struct {
key string
data []float32
done bool
task *tasks.Task
}
// CachedPeaks returns a current in-memory waveform without scheduling work.
// Small folder previews can use this without decoding an unopened audio file.
func (s *Service) CachedPeaks(id ids.AssetID, cols int) []float32 {
if cols <= 0 {
return nil
}
e, ok := s.entry(id)
if !ok || e.Kind != asset.ArtAudio {
return nil
}
key, ok := s.Key(e)
if !ok {
return nil
}
it := s.peaks[peakKey{id, cols}]
if it == nil || !it.done || it.key != key+"-"+strconv.Itoa(cols) {
return nil
}
return it.data
}
func (s *Service) Peaks(id ids.AssetID, cols int) ([]float32, bool) {
e, ok := s.entry(id)
if !ok || e.Kind != asset.ArtAudio || s.d.Tasks == nil || cols <= 0 {
return nil, true
}
key, ok := s.Key(e)
if !ok {
return nil, true
}
key += "-" + strconv.Itoa(cols)
if s.peaks == nil {
s.peaks = map[peakKey]*peakItem{}
}
pk := peakKey{id, cols}
it := s.peaks[pk]
if it != nil && it.key == key {
return it.data, it.done
}
if it != nil && it.task != nil {
it.task.Cancel()
}
it = &peakItem{key: key}
s.peaks[pk] = it
art, _ := e.Artifact(e.Kind)
path := filepath.Join(s.dir, key+peaksExt)
var out []float32
spec := tasks.Spec{Title: i18n.Tr("editor.preview.waveform_name", "Waveform {name}", i18n.Str("name", filepath.Base(e.Name))), Kind: "preview", Priority: s.frame}
it.task = s.d.Tasks.Work(spec, func(*tasks.Task) error {
if v, err := readPeaks(path, cols); err == nil {
out = v
return nil
}
raw, err := s.d.Pipeline.Artifacts().ReadFile(art.Path)
if err != nil {
return err
}
a, err := asset.DecodeAudio(raw)
if err != nil {
return err
}
d, err := codec.Open(bytes.NewReader(a.Data), "")
if err != nil {
return err
}
defer d.Close()
if out, err = codec.Peaks(d, cols); err != nil {
return err
}
return writePeaks(path, out)
}).Then(func(t *tasks.Task) {
if s.peaks[pk] != it {
return
}
it.task, it.done = nil, true
if t.State() == tasks.Done {
it.data = out
}
})
return nil, false
}
func readPeaks(path string, cols int) ([]float32, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if len(data) != 4*cols {
return nil, errs.New(errs.AssetBadArtifact, "the cached waveform has a wrong size")
}
out := make([]float32, cols)
for i := range out {
out[i] = math.Float32frombits(binary.LittleEndian.Uint32(data[4*i:]))
}
return out, nil
}
func writePeaks(path string, v []float32) error {
data := make([]byte, 4*len(v))
for i, x := range v {
binary.LittleEndian.PutUint32(data[4*i:], math.Float32bits(x))
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return err
}
return os.Rename(tmp, path)
}