diff options
Diffstat (limited to 'mpdDisplay.go')
-rw-r--r-- | mpdDisplay.go | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/mpdDisplay.go b/mpdDisplay.go new file mode 100644 index 0000000..93fb552 --- /dev/null +++ b/mpdDisplay.go @@ -0,0 +1,249 @@ +// Copyright 2022 Benjamin Barenblat +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +package main + +import ( + "math" + "time" + + "golang.org/x/text/transform" + + "benjamin.barenblat.name/audiotrond/cfa635" +) + +var ( + encoder = cfa635.NewEncoder() +) + +func encode(s string) []byte { + r, _, err := transform.String(encoder, s) + if err != nil { + panic(err) + } + return []byte(r) +} + +func setPlaybackIcon(model *model, lcdState *cfa635.LCDState) { + switch model.State { + case stopped: + lcdState[0][19] = 0xd0 + case playing: + lcdState[0][19] = 0x10 + case paused: + lcdState[0][19] = pauseIconSprite + } +} + +func rotate(s []byte, width int, start, now time.Time) []byte { + const delay int = 10 + const rotationRate float64 = 1 / float64(333*time.Millisecond) + + if len(s) <= width { + return s + } + + ticks := int(float64(now.Sub(start).Nanoseconds()) * rotationRate) + + if len(s) < 2*width { + // Just scroll back and forth. + period := len(s) - width + delay + ticks %= 2 * period + + i := 0 + if ticks <= delay { + } else if ticks <= period { + i = ticks - delay + } else if ticks <= period+delay { + i = len(s) - width + } else { + i = 2*period - ticks + } + return s[i : i+width] + } + + // Actually rotate. + for i := 0; i < 7; i++ { + s = append(s, byte(' ')) + } + + period := len(s) + delay + ticks %= period + + i := 0 + if ticks > delay { + i = ticks - delay + } + return append(s[i:], s[:i]...)[:width] +} + +func setTrackInfo(model *model, now time.Time, lcdState *cfa635.LCDState) { + // Cut off the track one character short so we don't overwrite the + // playback icon. + copy(lcdState[0][:], rotate(encode(model.Track), 19, model.LastTrackInfoUpdate, now)) + copy(lcdState[1][:], rotate(encode(model.Artist), 20, model.LastTrackInfoUpdate, now)) + copy(lcdState[2][:], rotate(encode(model.Album), 20, model.LastTrackInfoUpdate, now)) +} + +func setTimeElapsed(model *model, lcdState *cfa635.LCDState) int { + elapsed := fmtTime(model.Elapsed, model.Duration) + copy(lcdState[3][:], elapsed) + return len(elapsed) +} + +func setTimeRemaining(model *model, lcdState *cfa635.LCDState) int { + remaining := fmtTime(model.Elapsed-model.Duration, model.Duration) + copy(lcdState[3][20-len(remaining):], remaining) + return len(remaining) +} + +func setProgressBar(model *model, barStart, barEnd int, lcdState *cfa635.LCDState) { + fraction := float64(model.Elapsed) / float64(model.Duration) + + // Convert the fraction played to the number of columns that should be + // colored in the bar. Each cell has 6 columns; leave one extra column + // free at the left of the bar so it doesn't crash into the time + // elapsed. + c := int(math.Round(fraction * (6*float64(barEnd-barStart) - 1))) + if c == 0 { + return + } + + // There are precomposed block glyphs of widths 1-5 that leave a blank + // column at the beginning. Use one of those in the first cell. + w := c + if w > 5 { + w = 5 + } + lcdState[3][barStart] = byte(0xdb - w) + c -= w + if c == 0 { + return + } + + // Fill in the full cells in the bar. + x := barStart + 1 + for ; c >= 6; x, c = x+1, c-6 { + lcdState[3][x] = progressBarSpriteFull + } + if c == 0 { + return + } + + // Fill in the last, partial bar. + lcdState[3][x] = byte(progressBarSprite1 - 1 + c) +} + +const ( + ellipsisSprite = iota + pauseIconSprite + progressBarSprite1 + progressBarSprite2 + progressBarSprite3 + progressBarSprite4 + progressBarSprite5 + progressBarSpriteFull +) + +func initializeMPDDisplay(lcd *cfa635.Module) error { + // Load sprites. + + if err := lcd.SetCharacter(ellipsisSprite, &[...]byte{ + 0b00_000000, + 0b00_000000, + 0b00_000000, + 0b00_000000, + 0b00_000000, + 0b00_000000, + 0b00_010101, + 0b00_000000, + }); err != nil { + return err + } + + if err := lcd.SetCharacter(pauseIconSprite, &[...]byte{ + 0b00_000000, + 0b00_011011, + 0b00_011011, + 0b00_011011, + 0b00_011011, + 0b00_011011, + 0b00_000000, + 0b00_000000, + }); err != nil { + return err + } + + for w := 1; w <= 6; w++ { + r := byte((1<<w - 1) << (6 - w)) + if err := lcd.SetCharacter(progressBarSprite1-1+w, &[...]byte{ + r, + r, + r, + r, + r, + r, + r, + 0b00_000000, + }); err != nil { + return err + } + } + + return nil +} + +func brightnessStep(pm float64, then, now time.Time, old float64) float64 { + const brightnessRampRate float64 = 20.0 / float64(500*time.Millisecond) + + dt := float64(now.Sub(then).Nanoseconds()) + return old + pm*dt*brightnessRampRate +} + +func setBrightness(model *model, now time.Time, old *view) float64 { + var z time.Time + if old.Mtime == z { + return old.DisplayBrightness + } + + if model.State == playing || now.Sub(model.LastStateChange).Seconds() < 15 { + if old.DisplayBrightness >= 20 { + return 20 + } + return brightnessStep(+1, old.Mtime, now, old.DisplayBrightness) + } else { + if old.DisplayBrightness == 0 { + return 0 + } + return math.Max(0, brightnessStep(-1, old.Mtime, now, old.DisplayBrightness)) + } +} + +func mpdView(model *model, now time.Time, old *view) *view { + var new view + new.LCD = cfa635.ClearedLCDState() + setPlaybackIcon(model, new.LCD) + setTrackInfo(model, now, new.LCD) + if model.Duration > 0 { + barStart := setTimeElapsed(model, new.LCD) + barEnd := 20 - setTimeRemaining(model, new.LCD) + setProgressBar(model, barStart, barEnd, new.LCD) + } + + new.DisplayBrightness = setBrightness(model, now, old) + + new.Mtime = now + + return &new +} |