@@ -5,11 +5,12 @@ import (
55 "encoding/hex"
66 "fmt"
77 "os"
8+ "slices"
89 "strings"
910 "time"
1011
1112 "github.com/gofrs/uuid"
12- "gopkg.in/yaml.v3"
13+ yaml "gopkg.in/yaml.v3"
1314)
1415
1516var fileFormats = []string {"mp3" , "m4a" , "webm" }
@@ -22,8 +23,8 @@ func New(filename string) (*Agenda, error) {
2223 }
2324
2425 a := new (Agenda )
25- err = yaml . Unmarshal ( data , a )
26- if err != nil {
26+
27+ if err := yaml . Unmarshal ( data , a ); err != nil {
2728 return nil , fmt .Errorf ("failed to read YAML: %w" , err )
2829 }
2930
@@ -104,12 +105,7 @@ func (a *Agenda) AllTracks() (out []*Track) {
104105 // Track already-seen audio files so that we do not list them twice
105106 var seen []string
106107 unseen := func (id string ) bool {
107- for _ , i := range seen {
108- if i == id {
109- return false
110- }
111- }
112- return true
108+ return ! slices .Contains (seen , id )
113109 }
114110
115111 // Load Announcements first
@@ -203,6 +199,9 @@ type Room struct {
203199 // played.
204200 Sources []* Source `json:"sources" yaml:"sources"`
205201
202+ // PointsOfInterest are non-source points in the room which are made available as targets to screen accessibility tooling.
203+ PointsOfInterest []* PointOfInterest `json:"pointsOfInterest" yaml:"pointsOfInterest"`
204+
206205 // RoomTracks is a list of audio tracks to be played in a room, sourced from
207206 // everywhere. This is generally exclusive with Sources.
208207 RoomTracks []* Track `json:"roomTracks" yaml:"roomTracks"`
@@ -223,6 +222,12 @@ func (r *Room) generateIDs(a *Agenda) error {
223222 return err
224223 }
225224
225+ for _ , p := range r .PointsOfInterest {
226+ if err := p .generateID (); err != nil {
227+ return fmt .Errorf ("failed to generate ID for point of interest: %w" , err )
228+ }
229+ }
230+
226231 for _ , s := range r .Sources {
227232 err = s .generateIDs (a )
228233 if err != nil {
@@ -281,12 +286,7 @@ func (r *Room) AllTracks() (out []*Track) {
281286 // Track already-seen audio files so that we do not list them twice
282287 var seen []string
283288 unseen := func (id string ) bool {
284- for _ , i := range seen {
285- if i == id {
286- return false
287- }
288- }
289- return true
289+ return ! slices .Contains (seen , id )
290290 }
291291
292292 // Iterate Sources first
@@ -313,13 +313,13 @@ func (r *Room) AllTracks() (out []*Track) {
313313// Dimensions describe the dimensions of a space.
314314type Dimensions struct {
315315 // Width is the left-to-right dimension.
316- Width float64 `json:"width" yaml:"width"`
316+ Width float64 `json:"width" yaml:"width"`
317317
318318 // Height is the up-to-down dimension (the height of a room).
319319 Height float64 `json:"height" yaml:"height"`
320320
321321 // Depth is the forward-to-backward dimension.
322- Depth float64 `json:"depth" yaml:"depth"`
322+ Depth float64 `json:"depth" yaml:"depth"`
323323}
324324
325325// Surfaces describe the surface material of a room.
@@ -334,9 +334,8 @@ type Surfaces struct {
334334 Up string `json:"up" yaml:"up"`
335335}
336336
337- // Source describes a unique audio sequence and location
338- type Source struct {
339-
337+ // PointOfInterest describes a point of interest.
338+ type PointOfInterest struct {
340339 // ID is the generated unique identifier
341340 ID string `json:"id" yaml:"-"`
342341
@@ -346,34 +345,72 @@ type Source struct {
346345 // Location indicates a specific 3-dimensional coordinate in the room from
347346 // which the audio of this source emanates
348347 Location Point `json:"location" yaml:"location"`
348+ }
349+
350+ func (p * PointOfInterest ) generateID () error {
351+ // If we don't have a name, generate one
352+ if p .Name == "" {
353+ p .Name = uuid .Must (uuid .NewV1 ()).String ()
354+ }
355+
356+ p .ID = hashString (fmt .Sprintf ("poi-%s" , p .Name ))
357+
358+ return nil
359+ }
360+
361+ // Source describes a unique audio sequence and location
362+ type Source struct {
363+ PointOfInterest `json:",inline" yaml:",inline"`
364+
365+ // AutoTracks defines a pattern by which cue-track mappings are generated.
366+ // NOTE: either RoomTracks or AutoTracks should be defined, but not both.
367+ AutoTracks * AutoTracks `json:"autoTracks" yaml:"autoTracks"`
349368
350369 // Tracks is the list of audio tracks which should be played upon reaching a
351370 // particular cue
352371 Tracks []* Track `json:"tracks" yaml:"tracks"`
353372}
354373
355374func (s * Source ) generateIDs (a * Agenda ) error {
356- err := s .generateID ()
357- if err != nil {
375+ if err := s .generateID (); err != nil {
358376 return err
359377 }
360378
361- for _ , t := range s .Tracks {
362- if err = t .generateID (a ); err != nil {
363- return err
379+ if s .AutoTracks != nil {
380+ var autoTracks []* Track
381+
382+ for _ , c := range a .Cues {
383+ if slices .Contains (s .AutoTracks .IgnoreCues , c .Name ) {
384+ continue
385+ }
386+
387+ t := & Track {
388+ AudioFilePrefix : s .AutoTracks .Prefix + c .Name ,
389+ Cue : c .Name ,
390+ }
391+
392+ if err := t .generateID (a ); err != nil {
393+ if s .AutoTracks .IgnoreMissing {
394+ continue
395+ }
396+
397+ return fmt .Errorf ("failed to generate track %s for cue %s in source %s: %w" ,
398+ t .AudioFilePrefix , t .Cue , s .Name , err )
399+ }
400+
401+ autoTracks = append (autoTracks , t )
364402 }
365- }
366403
367- return nil
368- }
404+ s .Tracks = autoTracks
369405
370- func (s * Source ) generateID () error {
371- // If we don't have a name, generate one
372- if s .Name == "" {
373- s .Name = uuid .Must (uuid .NewV1 ()).String ()
406+ return nil
374407 }
375408
376- s .ID = hashString (fmt .Sprintf ("source-%s" , s .Name ))
409+ for _ , t := range s .Tracks {
410+ if err := t .generateID (a ); err != nil {
411+ return err
412+ }
413+ }
377414
378415 return nil
379416}
@@ -460,6 +497,20 @@ func (t *Track) generateID(a *Agenda) error {
460497 return nil
461498}
462499
500+ // AutoTracks defines a method by which cue-track mappings may be autogenerated.
501+ type AutoTracks struct {
502+ // Prefix defines a prefix which should be added to each auto-generated audio filename.
503+ // Note that this does NOT presume a directory separation between the prefix and the filename.
504+ // That is, a prefix of "alpha/beta" and a cue name of "gamma" would result in a file name "alpha/betagamma.webm".
505+ Prefix string `json:"prefix" yaml:"prefix"`
506+
507+ // IgnoreMissing indicates that any tracks which do not exist should simply be ignored.
508+ IgnoreMissing bool `json:"ignoreMissing" yaml:"ignoreMissing"`
509+
510+ // IgnoreCues provides a list of cues for which no track should be bound.
511+ IgnoreCues []string
512+ }
513+
463514// Point is a 3-dimensional point in space
464515type Point struct {
465516 X float64 `json:"x" yaml:"x"`
0 commit comments