app.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. // Package app is the Wails service the frontend calls. It owns the current
  2. // project and returns a fresh State after every call; the frontend keeps no
  3. // project data of its own (build plan 4.6 and 5).
  4. package app
  5. import (
  6. "fmt"
  7. "sync"
  8. "git.monoexp.com/bajdev/mpckit/internal/project"
  9. )
  10. // Version is set at build time with
  11. // -ldflags "-X git.monoexp.com/bajdev/mpckit/internal/app.Version=v1.0.0".
  12. var Version = "dev"
  13. // DefaultName is the name of a new project.
  14. const DefaultName = "NewKit"
  15. // Pad is one pad as the frontend shows it.
  16. type Pad struct {
  17. Slot int `json:"slot"`
  18. Label string `json:"label"` // "A01" .. "H16"
  19. Name string `json:"name"` // sample name; empty for an empty pad
  20. File string `json:"file"` // pool file name
  21. Coarse int `json:"coarse"`
  22. Fine int `json:"fine"`
  23. MuteGroup int `json:"muteGroup"`
  24. HasNotes bool `json:"hasNotes"`
  25. }
  26. // Sequence is one existing sequence.
  27. type Sequence struct {
  28. Slot int `json:"slot"`
  29. Label string `json:"label"` // slot label, "A01" .. "H16"
  30. Name string `json:"name"`
  31. BPM float64 `json:"bpm"`
  32. Bars int `json:"bars"`
  33. HasNotes bool `json:"hasNotes"`
  34. }
  35. // Limits are the ranges the UI offers, from the device checks.
  36. type Limits struct {
  37. Pads int `json:"pads"`
  38. MaxSamples int `json:"maxSamples"`
  39. MaxSequences int `json:"maxSequences"`
  40. MinCoarse int `json:"minCoarse"`
  41. MaxCoarse int `json:"maxCoarse"`
  42. MinFine int `json:"minFine"`
  43. MaxFine int `json:"maxFine"`
  44. MaxMuteGroup int `json:"maxMuteGroup"`
  45. MinBPM float64 `json:"minBPM"`
  46. MaxBPM float64 `json:"maxBPM"`
  47. MaxBars int `json:"maxBars"`
  48. }
  49. // State is everything the frontend shows about the project.
  50. type State struct {
  51. Name string `json:"name"`
  52. Dirty bool `json:"dirty"`
  53. Pads []Pad `json:"pads"` // all 128, in slot order
  54. Sequences []Sequence `json:"sequences"` // existing ones, in slot order
  55. SampleCount int `json:"sampleCount"`
  56. SequenceCount int `json:"sequenceCount"`
  57. Limits Limits `json:"limits"`
  58. }
  59. // About is the text for the About dialog.
  60. type About struct {
  61. Version string `json:"version"`
  62. License string `json:"license"`
  63. Repo string `json:"repo"`
  64. }
  65. // Service is registered with Wails; its exported methods become the
  66. // frontend bindings.
  67. type Service struct {
  68. mu sync.Mutex
  69. p *project.Project
  70. }
  71. // NewService returns a service holding a new, empty project.
  72. func NewService() *Service {
  73. s := &Service{}
  74. s.reset()
  75. return s
  76. }
  77. func (s *Service) reset() {
  78. p, err := project.New(DefaultName)
  79. if err != nil {
  80. panic(err) // the embedded template and a constant name
  81. }
  82. s.p = p
  83. }
  84. // New replaces the current project with a new, empty one.
  85. func (s *Service) New() State {
  86. s.mu.Lock()
  87. defer s.mu.Unlock()
  88. s.reset()
  89. return s.state()
  90. }
  91. // State returns the current project's state.
  92. func (s *Service) State() State {
  93. s.mu.Lock()
  94. defer s.mu.Unlock()
  95. return s.state()
  96. }
  97. // Version returns the app version.
  98. func (s *Service) Version() string { return Version }
  99. // About returns the About dialog text.
  100. func (s *Service) About() About {
  101. return About{
  102. Version: Version,
  103. License: "MIT License, copyright (c) 2026 bajdev",
  104. Repo: "https://git.monoexp.com/bajdev/mpckit",
  105. }
  106. }
  107. // Label returns the label the UI and the device use for a pad or sequence
  108. // slot: bank letter and two-digit pad number, e.g. 0 -> "A01", 17 -> "B02".
  109. func Label(slot int) string {
  110. return fmt.Sprintf("%c%02d", 'A'+slot/16, slot%16+1)
  111. }
  112. func (s *Service) state() State {
  113. p := s.p
  114. st := State{
  115. Name: p.Name,
  116. Dirty: p.Dirty,
  117. Pads: make([]Pad, 0, project.Pads),
  118. Sequences: []Sequence{},
  119. SampleCount: p.SampleCount(),
  120. Limits: Limits{
  121. Pads: project.Pads,
  122. MaxSamples: project.MaxSamples,
  123. MaxSequences: project.MaxSequences,
  124. MinCoarse: project.MinCoarse,
  125. MaxCoarse: project.MaxCoarse,
  126. MinFine: project.MinFine,
  127. MaxFine: project.MaxFine,
  128. MaxMuteGroup: project.MaxMuteGroup,
  129. MinBPM: project.MinBPM,
  130. MaxBPM: project.MaxBPM,
  131. MaxBars: project.MaxBars,
  132. },
  133. }
  134. for slot := 0; slot < project.Pads; slot++ {
  135. v, err := p.Pad(slot)
  136. if err != nil {
  137. // A loaded file with an odd pad; show it as empty.
  138. st.Pads = append(st.Pads, Pad{Slot: slot, Label: Label(slot)})
  139. continue
  140. }
  141. pad := Pad{Slot: slot, Label: Label(slot), Name: v.SampleName, File: v.File,
  142. Coarse: v.Coarse, Fine: v.Fine, MuteGroup: v.MuteGroup}
  143. if !v.Empty() {
  144. pad.HasNotes = p.PadHasNotes(slot)
  145. }
  146. st.Pads = append(st.Pads, pad)
  147. }
  148. for _, q := range p.Sequences() {
  149. st.Sequences = append(st.Sequences, Sequence{Slot: q.Slot, Label: Label(q.Slot),
  150. Name: q.Name, BPM: q.BPM, Bars: q.Bars, HasNotes: q.HasNotes})
  151. }
  152. st.SequenceCount = len(st.Sequences)
  153. return st
  154. }