跳到正文
Hacker News· smig0·· 10 天前AI 评分12

Pebble Time 2 手表 64k 演示 demo 发布

64k demo for the Pebble Time 2

AI 导读

开发者发布了一款运行在 Pebble Time 2 手表上的 64k demo,使用 Mad Tea Synth 插件在 DAW 中制作,代码语言为 Tea++。

正文 · 原文

A 64k demo for the Pebble Time 2 Watch and MicroW8

If you have the watch, you can install it from the official pebble site

Mad Tea Synth

The demo is made in Mad Tea Synth, which is a plug-in used inside in a music software/DAW such as Reaper or Bitwig. It’s basically a list of events, like a kick drum, a lead synth or a visual effect.

Each event is mapped to a midi note-range, and then it is just a code box (or two code boxes for events with both sound and visuals)

In each codebox you have access to things like midi velocity vel, song position in beats beat, seconds since note-on t and relative sample rate rate etc

Then there are some basic functions to draw circles, lines, rects and text. On the audio side there’s support for a sample slot per event which is streamed using the function smp.

The codeboxes use a custom language (Tea++). It’s imperative with inferred types. I put effort into making it feel like casual scripting (but with support to compile to C and output fixed point for maths)

Music / Concept

There is a famous voice sample from the intro of Howdy Doody, a 1950s kids television show, which was later sampled in 80s/90s hiphop/dance scenes. The whole thing goes “Say kids, what time is it”, and that last part seemed useful…

I could hear the melody in my head, and I cut and tuned the words accordingly. For other samples, I reused a stab from Daydreamer and the dirty electric bass from RE-FORM

Then there’s just a lot of time-related concepts to draw from, not sure I managed to pick well though…

No synth in the synth?

The plan was to have an api for sound design, partly derived from the RE-FORM Synth. That never happened so there was nothing included except playing mono samples.

I reached for an experiment from 2013. The idea was:

  • A pulse-width waveform is just animating a single bit-flip linearly from 0 to 1, -what if we combine multiple animated sign-flips using xor?

I tried many alternatives back then, and I really liked one which sounded like a bass with a lot of phaser fx:

pw = (x, pw) => {  // normal pw, for reference
  x -= floor(x)    
  return x < pw ? -1 : 1         
}

phaserpw = (x, pw) => {
  x -= floor(x)
  return ( (x < 1/8 - pw * 1/8) 
         ^ (x < 2/8 - pw * 2/8) 
         ^ (x < 5/8 - pw * 2/8) 
         ^ (x < 6/8 + pw * 2/8) 
         ^ (x < 7/8 + pw * 1/8)
    ) ? -1 : 1  
} 

So for this demo I rewrote that with a boxcar-filter, which was then used for almost all of the synth sounds in the demo. Play around / listen in Mad Tea Lab.

Ascii Rendering

Similar to some of the scenes in PRINT, but on a much smaller scale…

The implementation first render to a small grayscale buffer. It samples 3 grayscale pixels vertically, and then the difference of the 2 outer samples determines which of 2 lookup tables should be used for middle point. (The dual-lut makes edges smoother)

upper_heavy_lut = "  '`:""--**!^+)<?vcYCFos35EH90BNW" 
lower_heavy_lut = "  ..,:;-__*!!+(>=Jc7lnIaedK$6Q&MW" 

Note: actual luts in demo are 255 chars long

The greetings text changes the stroke weight of each character based on the (interpolated) grayscale pixel value instead of swapping the glyph.

The grayscale source image is mostly single color with antialiased edges, so to make it more interesting, I apply vinjetting, this also gives some nice stability of filled areas when animating. Most ascii-render scenes also use 2 size levels, so larger glyphs alternate with 2x2 of half the size.

Hourglass scene

This scene was made to contrast the overall monochrome style of the demo and showcase the color display. The display supports 64 colors (4 shades per RGB channel), and I felt like I evaluated all relevant 7-color warm-to-cold palettes…

The e-paper display looks best in sunlight and warm lighting so that’s what I used for the video recording.

Outline prototyped in Mad Tea Labs func-plot, then just combined into a single function

Animating with code

I prefer stateless implementations for non-interactive/fixed animation, and utility functions that output normalized ranges.

Movement / Transition / Hits

The renderman/glsl smoothstep api is great for animating, I use these functions:

linearstep   (start_time, end_time, time)
smoothstep   (start_time, end_time, time)
smootherstep (start_time, end_time, time)

Integrated versions (a == area under the curve)

linearstepa  (start_time, end_time, time)
smoothstepa  (start_time, end_time, time)
smootherstepa(start_time, end_time, time)

The last ones are handy for instance when doing interpolation/glide between two notes, or if you want graphics to transition between stationary and spinning; instead of doing:

cur_spin_speed = smoothstep(0, 1, t) * max_speed
rotation += cur_spin_speed

Skip the state and do:

rotation = smoothstepa(0, 1, t) * max_speed

For visually syncing “hits” like kicks, snares and stabs, I again use linearstep, but with an exponent to control the curve.

Rhythmic Time Warp

Time warping is a nice way to apply rhytmic sync to something that is already animated, like the dot-tunnel scene. I usually just pass time though a function like this one:

rhythmic_time_warp = (x, amount, len = 0.18) => {
    var steps = fract(x + 0.5)
    return x + amount * (smootherstep(-len, len, steps - 0.5) - steps)
}

The function is designed so that time is always running the fastest exactly at integers, however, I tend to have to offset the input time slightly depending on the context to get the right feel.

Rhythmic Bounce

Simply 4 * frac(x) * frac(-x)

Tooling failure

As I was working on the visuals for the demo, I felt it was generally nicer to just use beats(global time) with a manual offset, rather than t, becuase when using t you cannot start previewing in the middle of an event. But that means having to update the offset if I move the scene around, so a key promise of visual editing in the DAW fell flat, which was disappointing…

Going 64k

Actual hardware seem capable, but for better battery life the Pebble SDK sets limits: 128kb RAM, 256kb flash storage, and no floating point support. There was also a bug in the pebble SDK limiting the binary to 64k. I felt I would not have the time to make good use of the flash storage anyway so I went for 64k for everything.

To really take advantage of the hardware, you’d have to change the firmware/OS. But this would make it much more cumbersome for people to actually run the demo…

Improving watch spreaker quality

The watch support audio playback at 16-bit 16 khz, but the challange is the tiny speaker. I suspected I could improve audio quality by just applying a few band/notch filters (reducing the annoying resonant high frequencies). This turned out to be harder than expected, as distortion quickly appeared when trying to equalize the frequency profile. After a few iterations I settled on 3 boosted peak filters + gain reduction. Just tweaked by ear, and already sacrificing a lot of volume for better quality. You can toggle this on/off using the down button on the watch.

Show-time!

I worked hard to finish this for Evoke 2026, however I’d failed to notice that the party had adopted a hard anti-ai stance. So the demo was disqualified as LLMs were used during tools development. Instead I submitted it to Xenium a week later, but as a remote entry unfortunately as I did not have energy for more travel. Disqualification had some good outcomes, as the delay gave time to do a MicroW8 port and improve the video edit.

Demo scope vs tool scope

Making this demo helped drive the development of the tool, however the demo scope is small compared to the tooling. Feel free to continue reading about the Mad Tea Synth Project

来源:Hacker News · teadrinker.net