An audio player with transport, scrubber and volume; an equalizer and a visualizer; a playlist; a cassette, cartridge and record that turn while they play and hold where they stop; piano keys, a step sequencer and a tracker you can type into; a waveform, a 16:9 video frame, an image compare, a gallery, a filmstrip, a record button and an FM tuner. input exports SlideBase, the undrawn rail its sliders share; the gallery treats exported Bases as building blocks, not components. pixel's micro font gains '#' for notes like C#4. pixel.Pad and the slide rail read input only while their base is enabled, so Inert or a fresh Disabled stops a drag or a key already sent. AGENTS records why a drawn container keeps the DISABLED description: MyGo paints a disabled subtree at half opacity.
228 lines
7.5 KiB
Go
228 lines
7.5 KiB
Go
package gallery
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/media"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
var mediaEntries = []Entry{
|
|
{346, "AudioPlayer", "音乐播放器", "media", "A whole deck in one box. Stop rewinds to the top.", demoAudioPlayer},
|
|
{347, "Transport", "播放控制", "media", "Back, play or pause, stop, skip: the four buttons every deck agrees on.", demoTransport},
|
|
{348, "Scrubber", "进度拖动", "media", "Drag to seek, or nudge with arrows. Time at both ends.", demoScrubber},
|
|
{349, "Volume", "音量", "media", "Eight bars; click one or use arrows. The speaker mutes.", demoVolume},
|
|
{350, "Equalizer", "均衡器", "media", "Five faders, ±12 dB. Bass boost at your own risk.", demoEqualizer},
|
|
{351, "Visualizer", "频谱", "media", "Bars bounce out from the middle, hottest at the tips.", demoVisualizer},
|
|
{352, "Playlist", "播放列表", "media", "♪ marks the track that plays. Enter plays the one you pick.", demoPlaylist},
|
|
{353, "Cassette", "磁带", "media", "Reels turn while it plays; watch the tape cross over.", demoCassette},
|
|
{354, "Cartridge", "卡带", "media", "A cart with a sticker. Blow on the contacts before use.", demoCartridge},
|
|
{355, "Vinyl", "黑胶", "media", "Turns in eight frames; the arm swings onto the groove.", demoVinyl},
|
|
}
|
|
|
|
type mediaDemo struct {
|
|
player media.Player
|
|
track, pick int
|
|
deck string
|
|
seek time.Duration
|
|
level int
|
|
muted bool
|
|
gains []float64
|
|
tape, spin, video bool
|
|
tapeAt, videoAt time.Duration
|
|
notes []string
|
|
beat bool
|
|
pattern [][16]bool
|
|
rows []media.Row
|
|
row, chosen int
|
|
split, mhz float64
|
|
rec bool
|
|
ran, taped, filmed time.Time
|
|
}
|
|
|
|
var tracks = []media.Track{
|
|
{Title: "Overworld Theme", Artist: "The Square Waves", Length: 2*time.Minute + 47*time.Second},
|
|
{Title: "Boss Battle", Artist: "Chip Wizards", Length: 3*time.Minute + 12*time.Second},
|
|
{Title: "Save Point Lullaby", Artist: "Triangle Wave", Length: 1*time.Minute + 58*time.Second},
|
|
{Title: "Credits Roll", Artist: "Pulse & Noise", Length: 4*time.Minute + 5*time.Second},
|
|
}
|
|
|
|
func mediaState(c *ui.Context) *mediaDemo {
|
|
return ui.Local(c.Root(), demoKey("media"), func() mediaDemo {
|
|
return mediaDemo{
|
|
player: media.Player{Level: 6}, pick: 1, deck: "STOPPED", seek: 72 * time.Second, level: 5,
|
|
gains: []float64{6, 2, 0, 3, 5},
|
|
pattern: [][16]bool{beat("x...x...x...x..."), beat("....x.......x..."), beat("x.x.x.x.x.x.x.xx"), beat("............x...")},
|
|
rows: melody(), split: 0.5, mhz: 98.3,
|
|
}
|
|
})
|
|
}
|
|
|
|
// beat reads a pattern: x hits, . rests.
|
|
func beat(s string) [16]bool {
|
|
var b [16]bool
|
|
for i, r := range s {
|
|
b[i] = r == 'x'
|
|
}
|
|
return b
|
|
}
|
|
|
|
// spin moves *at while playing, as an audio engine would.
|
|
func spin(c *ui.Context, last *time.Time, playing bool, at *time.Duration, length time.Duration) {
|
|
k := pixel.K(c)
|
|
if playing && !last.IsZero() {
|
|
*at = (*at + k.Now().Sub(*last)) % length
|
|
}
|
|
*last = time.Time{}
|
|
if playing {
|
|
*last = k.Now()
|
|
k.Tick(c, 4)
|
|
}
|
|
}
|
|
|
|
func demoAudioPlayer(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
s := mediaState(c)
|
|
t := tracks[s.track]
|
|
spin(c, &s.ran, s.player.Playing, &s.player.At, t.Length)
|
|
ui.Box(c).Width(k.D(240)).Children(func() {
|
|
res := media.AudioPlayer(c, "Now playing", t, &s.player, media.AudioPlayerOptions{})
|
|
switch {
|
|
case res.Previous:
|
|
s.track = (s.track + len(tracks) - 1) % len(tracks)
|
|
case res.Next:
|
|
s.track = (s.track + 1) % len(tracks)
|
|
}
|
|
if res.Previous || res.Next {
|
|
s.player.At = 0
|
|
log("♪ " + tracks[s.track].Title)
|
|
}
|
|
if res.Changed {
|
|
log(fmt.Sprintf("playing %v at %s, volume %d, muted %v", s.player.Playing, s.player.At.Truncate(time.Second), s.player.Level, s.player.Muted))
|
|
}
|
|
})
|
|
}
|
|
|
|
func demoTransport(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
s := mediaState(c)
|
|
ui.Column(c).Gap(k.D(6)).Children(func() {
|
|
playing := s.deck == "PLAYING"
|
|
res := media.Transport(c, "Deck", &playing, media.TransportOptions{})
|
|
switch {
|
|
case res.Stopped:
|
|
s.deck = "STOPPED"
|
|
case res.Changed && playing:
|
|
s.deck = "PLAYING"
|
|
case res.Changed:
|
|
s.deck = "PAUSED"
|
|
case res.Previous:
|
|
log("PREVIOUS")
|
|
case res.Next:
|
|
log("NEXT")
|
|
}
|
|
if res.Changed || res.Stopped {
|
|
log(s.deck)
|
|
}
|
|
pixel.Text(c, s.deck, pixel.TextOptions{Slot: pixel.Faint})
|
|
})
|
|
}
|
|
|
|
func demoScrubber(c *ui.Context, log func(string)) {
|
|
s := mediaState(c)
|
|
if media.Scrubber(c, "Seek", &s.seek, tracks[1].Length, media.ScrubberOptions{Width: 120}).Changed {
|
|
log("seek " + s.seek.String())
|
|
}
|
|
}
|
|
|
|
func demoVolume(c *ui.Context, log func(string)) {
|
|
s := mediaState(c)
|
|
if media.Volume(c, "Volume", &s.level, &s.muted, media.VolumeOptions{}).Changed {
|
|
log(fmt.Sprintf("volume %d of 8, muted %v", s.level, s.muted))
|
|
}
|
|
}
|
|
|
|
func demoEqualizer(c *ui.Context, log func(string)) {
|
|
s := mediaState(c)
|
|
bands := []string{"60", "250", "1K", "4K", "12K"}
|
|
if b := media.Equalizer(c, "Equalizer", bands, s.gains, media.EqualizerOptions{}).Band; b >= 0 {
|
|
log(fmt.Sprintf("%s Hz %+.1f dB", bands[b], s.gains[b]))
|
|
}
|
|
}
|
|
|
|
func demoVisualizer(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
// A made-up song: each bar sways at its own pace.
|
|
t := 0.0
|
|
if !k.Reduce() {
|
|
t = float64(k.Tick(c, 12)) / 12
|
|
}
|
|
levels := make([]float64, 24)
|
|
for i := range levels {
|
|
f := float64(i)
|
|
levels[i] = (0.55 + 0.45*math.Sin(t*(2+f/5)+f)) * (1 - f/32)
|
|
}
|
|
media.Visualizer(c, "Visualizer", levels, media.VisualizerOptions{Height: 40})
|
|
}
|
|
|
|
func demoPlaylist(c *ui.Context, log func(string)) {
|
|
s := mediaState(c)
|
|
res := media.Playlist(c, "Playlist", tracks, s.track, &s.pick, media.PlaylistOptions{Height: 80})
|
|
if res.Submitted {
|
|
s.track, s.player.At = s.pick, 0
|
|
log("♪ " + tracks[s.track].Title)
|
|
} else if res.Changed {
|
|
log("picked " + tracks[s.pick].Title)
|
|
}
|
|
}
|
|
|
|
func demoCassette(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
s := mediaState(c)
|
|
const side = 45 * time.Second
|
|
spin(c, &s.taped, s.tape, &s.tapeAt, side)
|
|
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
|
|
media.Cassette(c, "Mixtape, side A", media.CassetteOptions{Title: "Mixtape 86", Playing: s.tape, Progress: float64(s.tapeAt) / float64(side)})
|
|
if input.Toggle(c, "PLAY", &s.tape, input.ToggleOptions{}).Changed {
|
|
log(fmt.Sprintf("tape playing %v", s.tape))
|
|
}
|
|
})
|
|
}
|
|
|
|
func demoCartridge(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
ui.Row(c).Gap(k.D(8)).Children(func() {
|
|
media.Cartridge(c, "Pixel Quest cartridge", media.CartridgeOptions{Title: "Pixel Quest", Art: scene(pixel.Warn, pixel.Info, pixel.Good, 8, 4)})
|
|
media.Cartridge(c, "Night Run cartridge", media.CartridgeOptions{Title: "Night Run 2", Art: scene(pixel.Lit, pixel.Magic, pixel.Shade, 26, 3)})
|
|
})
|
|
}
|
|
|
|
func demoVinyl(c *ui.Context, log func(string)) {
|
|
k := pixel.K(c)
|
|
s := mediaState(c)
|
|
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
|
|
media.Vinyl(c, "Record", media.VinylOptions{Playing: s.spin})
|
|
if input.Toggle(c, "PLAY", &s.spin, input.ToggleOptions{}).Changed {
|
|
log(fmt.Sprintf("record playing %v", s.spin))
|
|
}
|
|
})
|
|
}
|
|
|
|
// scene is hills under a sky, sun at x, y.
|
|
func scene(sun, sky, hill pixel.Slot, x, y int) *pixel.Sprite {
|
|
return pixel.SpriteOf(32, 18, func(i, j int) (pixel.Slot, bool) {
|
|
switch {
|
|
case (i-x)*(i-x)+(j-y)*(j-y) <= 5:
|
|
return sun, true
|
|
case float64(j) > 12+2*math.Sin(float64(i)/4):
|
|
return hill, true
|
|
}
|
|
return sky, true
|
|
})
|
|
}
|