-
Notifications
You must be signed in to change notification settings - Fork 1
/
sounds.go
148 lines (119 loc) · 2.88 KB
/
sounds.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package sounds
import (
"embed"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
"github.com/diamondburned/gotk4/pkg/core/glib"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/diamondburned/gotkit/app"
"github.com/pkg/errors"
)
//go:embed *.opus
var sounds embed.FS
// Sound IDs.
const (
Bell = "bell"
Message = "message"
)
var (
fileExistsCache sync.Map
soundsLastPlayed sync.Map
)
const soundDebounce = 200 * time.Millisecond
// Play plays the given sound ID. It first uses Canberra, falling back to
// ~/.cache/gotktrix/{id}.opus, then the embedded audio (if any), then
// display.Beep() otherwise.
//
// Play is asynchronous; it returning does not mean the audio has successfully
// been played to the user.
func Play(app *app.Application, id string) {
go play(app, id)
}
func play(app *app.Application, id string) {
now := time.Now()
if t, ok := soundsLastPlayed.Load(id); ok {
t := t.(time.Time)
if now.Sub(t) < soundDebounce {
return
}
}
soundsLastPlayed.Store(id, now)
canberra := exec.Command("canberra-gtk-play", "--id", id)
if err := canberra.Run(); err == nil {
return
} else {
log.Println("canberra error:", err)
}
name := id
if filepath.Ext(name) == "" {
name += ".opus"
}
dst := app.CachePath("sounds", name)
var fileExists bool
if b, ok := fileExistsCache.Load(dst); ok && b.(bool) {
fileExists = true
} else {
_, err := os.Stat(dst)
if err != nil {
if err := copyToFS(dst, name); err != nil {
log.Printf("cannot copy sound %q: %v", id, err)
glib.IdleAdd(beep)
return
}
}
fileExists = true
fileExistsCache.Store(id, fileExists)
}
glib.IdleAdd(func() {
media := gtk.NewMediaFileForFilename(dst)
mediaWeak := glib.NewWeakRef(media)
media.NotifyProperty("error", func() {
media := mediaWeak.Get()
fileExistsCache.Delete(id)
playEmbedError(id, media.Error())
})
media.Play()
})
}
func playEmbedError(name string, err error) {
log.Printf("error playing embedded %s.opus: %v", name, err)
beep()
}
func beep() {
log.Println("using beep() instead")
disp := gdk.DisplayGetDefault()
disp.Beep()
}
func copyToFS(dst string, name string) error {
src, err := sounds.Open(name)
if err != nil {
return err
}
defer src.Close()
dir := filepath.Dir(dst)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return errors.Wrap(err, "cannot mkdir sounds/")
}
f, err := os.CreateTemp(dir, ".tmp.*")
if err != nil {
return errors.Wrap(err, "cannot mktemp in cache dir")
}
defer os.Remove(f.Name())
defer f.Close()
if _, err := io.Copy(f, src); err != nil {
return errors.Wrap(err, "cannot write audio to disk")
}
if err := f.Close(); err != nil {
return errors.Wrap(err, "cannot close written audio")
}
if err := os.Rename(f.Name(), dst); err != nil {
return errors.Wrap(err, "cannot commit written audio")
}
return nil
}