Author SHA1 Message Date
nilsandClaude Opus 5.5 fd38c37ebe App 2.43.4: the Halloween pumpkin's light follows the music
The bass from Android's audio analyser drives the glow and brightens the
carving on the beat, only while the style is on screen. Yellow and green and
the embers keep their meaning; without a signal, as with the radio, the light
stays as it was. Levels only. Tried on the emulator. Not tried on the car.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
2026-11-01 10:07:01 +01:00
nilsandClaude Opus 5.5 ea9763e1de SPECS: the sound check does not hear the radio
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
2026-10-31 23:55:56 +01:00
nilsandClaude Opus 5.5 97e37c8a3c SPECS: app 2.43.3 published; the radio finding put right
The owner only meant today's check had no radio, so the 29 October one
stands: the radio's session played with music not active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
2026-10-31 17:10:27 +01:00
9 changed files with 255 additions and 16 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
| Tab | Contents |
|---|---|
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car, top-down: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`; the map is `MapLayerView`, a hardware layer drawn once and moved and turned by the GPU every display frame), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`), Día de Muertos (`MuertosView`, an ofrenda: papel picado, candles, a calavera in a ring of marigolds), Halloween (`HalloweenView`, a jack-o'-lantern, a potion flask, a tombstone on a graveyard hill), both counting with `Tally`; the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire, Día de Muertos, Halloween; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car, top-down: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`; the map is `MapLayerView`, a hardware layer drawn once and moved and turned by the GPU every display frame), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`), Día de Muertos (`MuertosView`, an ofrenda: papel picado, candles, a calavera in a ring of marigolds), Halloween (`HalloweenView`, a jack-o'-lantern, a potion flask, a tombstone on a graveyard hill), both counting with `Tally`, the pumpkin's light following the music on screen (`MusicTap`, `MusicPulse`: Android's audio analyser, levels only; Bluetooth and CarPlay, not the radio); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire, Día de Muertos, Halloween; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
| Consumption / Charging | period totals, daily log, charging sessions and curve |
| Live data | on/off, ABRP API key and user token, upload frequency |
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
+4 -1
View File
@@ -140,7 +140,10 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
- 2026-10-31 — Music checks from the car: the radio publishes a session while it plays (`com.saicmotor.radio`, 29 October, DAB: TITLE the station, ARTIST the programme text, AUTHOR the band), with `isMusicActive` false; whether the radio could be heard then is not known (the owner did not have it playing in the check they looked at), so this says nothing yet about whether the audio analyser hears it. To be tested with the radio audibly playing. CarPlay playing (31 October, 2.43.2) has `isMusicActive` true, like Bluetooth on 28 October. The sound check itself has not run on the car yet: 2.43.3 was not published and those reports came from 2.43.2's music-sources check.
- 2026-11-01 — App 2.43.4: **the Halloween pumpkin's light follows the music** (`MusicTap`, `MusicPulse`; owner: "make the light in the pumpkin react to the equalizer/sound of the music"). The bass, 25 times a second off the main thread, drives the glow and brightens the carving on the beat; yellow and green still mean drawing and regenerating, the embers still the power, and no signal leaves the light as it was. It listens only with the app in front, on the Overview, in Halloween. The owner reports the sound check worked with Bluetooth and CarPlay; those two reports never reached the server (why not is unexplained), so their levels are unseen and the pulse's feel is untuned for the car. Tested: unit tests, lint, emulator (a tone already playing when the analyser starts: the carving and glow move with it; no sound: unchanged; the analyser is released on another tab and when the app leaves the front; no crash). Not tried on the car; how it looks there, and its effect on the head unit over a long drive, are unknown.
- 2026-10-31 — Sound check on the car (2.43.3), radio: `com.saicmotor.radio` playing, `isMusicActive` false, analyser enabled without error and silent for all 10 seconds (peak 0.00, -9600 mB). The audio analyser does not hear the radio. The owner ran Bluetooth and CarPlay too; those reports have not reached the server yet (the outbox sends oldest first and stops at the first failure, retrying while the service runs).
- 2026-10-31 — App 2.43.3 published (`car-2.43.3`).
- 2026-10-31 — Music checks from the car: the radio publishes a session while it plays (`com.saicmotor.radio`, 29 October, DAB: TITLE the station, ARTIST the programme text, AUTHOR the band), with `isMusicActive` false, so its sound may not pass through Android's mix and the audio analyser may not hear it. The sound check with the radio playing will tell. CarPlay playing (31 October, 2.43.2) has `isMusicActive` true, like Bluetooth on 28 October. The sound check itself has not run on the car yet: 2.43.3 was not published and those reports came from 2.43.2's music-sources check.
- 2026-10-31 — App 2.43.3: **sound check** on the Logs tab (`SoundProbe`, `SoundLevels`), asked for before the Halloween pumpkin's light follows the music (owner: "make the light in the pumpkin react to the equalizer/sound of the music"). One tap: 10 seconds of levels from `Visualizer` on session 0 (peak, bass, mid, treble, rms), saved and sent like a recording; no sound kept, no microphone. New in the car build's manifest: RECORD_AUDIO and MODIFY_AUDIO_SETTINGS. Grounds for trying: Bluetooth played with `isMusicActive` true (28 October check), `libCarPlayPlatformAudio.so` uses AudioTrack and no direct PCM. Whether the analyser hears anything on the car is what this is to find out. Tested: unit tests, lint, emulator (silence says nothing was playing; a pulsing 110 Hz + 1 kHz test tone from another app is heard, peak 0.27–0.91, bass strongest, the tone kept playing, no prompt, no crash). Not tried on the car.
- 2026-10-31 — App 2.43.2 published (`car-2.43.2`).
- 2026-10-31 — App 2.43.2: Halloween's teeth are triangles hanging from the grin's upper lip, each in the colour of the rib behind it (the right one was the darker orange on the lighter rib; owner: "the color on the right side is off"). Two screenshots redone. Tested: unit tests, lint, emulator with sample values (drawing in panel and full screen, regenerating in full screen) and with no data. Not tried on the car.
+3 -1
View File
@@ -262,7 +262,9 @@ the range ("here lies … km"), and the bats are the range too, one per 50 km; P
gravestones, the one engaged lit. The moon holds the clock, a sign hanging from a cobweb has trip,
usage, climate and odometer, and the hill along the bottom is the last two minutes of power. While
you drive the bats fly, the potion bubbles and the ghost floats (where there is room for it, as on
this screen). The app turns the pumpkin's orange with it.
this screen). With Bluetooth or CarPlay music playing, the pumpkin's glow swells and its carving
brightens on the beat; the radio cannot be heard, so it burns as usual then. The app turns the
pumpkin's orange with it.
![Halloween with the navigation](screenshots/themes/halloween-panel.png)
![Halloween, full screen](screenshots/themes/halloween.png)
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
versionCode 156
versionName "2.43.3"
versionCode 157
versionName "2.43.4"
// Nothing about the server ships in the APK: its URL and ingest token are entered in
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
@@ -25,10 +25,13 @@ import java.util.Locale;
* tombstone reads the range ("here lies … km"); P R N D are four small gravestones, the one engaged
* lit. The bats are the range too, one per 50 km. The hill along the bottom is the last two minutes
* of power, rising with it, in orange or, where it regenerated, green. Unread values show "—" and
* light nothing.
* light nothing. With music playing, the pumpkin's glow swells and its carving brightens on the
* beat ({@link MusicTap}); colours and the embers keep their meaning, and with no music it burns as
* before.
*
* The night sky is drawn once into a bitmap per size; only the rest goes over it. Bats, bubbles and
* the ghost move (about 30 frames a second) only while the car moves and the view is on screen.
* the ghost move (about 30 frames a second) only while the car moves and the view is on screen;
* the light's pulse keeps it drawing while there is one.
*/
final class HalloweenView extends ClusterCanvasView {
@@ -56,6 +59,7 @@ final class HalloweenView extends ClusterCanvasView {
private int skyW, skyH;
private float time;
private long lastFrame;
private MusicTap music;
HalloweenView(Context context) {
super(context);
@@ -63,6 +67,11 @@ final class HalloweenView extends ClusterCanvasView {
line.setStrokeCap(Paint.Cap.ROUND);
}
/** Where the pumpkin's light hears the music; without it, the light does not follow any. */
void setMusic(MusicTap tap) {
music = tap;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
@@ -91,12 +100,13 @@ final class HalloweenView extends ClusterCanvasView {
drawGhost(c, w, u);
drawHill(c, w, h, u);
drawEmbers(c, w, u);
drawPumpkin(c, w, u);
float pulse = music == null ? 0f : music.pulse();
drawPumpkin(c, w, u, pulse);
drawSign(c, u);
drawFlask(c, u);
drawTombstone(c, w, u);
drawGears(c, w, u);
keepAnimating(moving, 33);
keepAnimating(moving || pulse > 0f, 33);
}
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
@@ -289,16 +299,18 @@ final class HalloweenView extends ClusterCanvasView {
// ---------- The jack-o'-lantern: the speed ----------
private void drawPumpkin(Canvas c, int w, float u) {
private void drawPumpkin(Canvas c, int w, float u, float pulse) {
float cx = w / 2f, cy = 348f * u, rx = 200f * u, ry = 158f * u;
boolean regen = regen();
int burn = regen ? FIRE : CANDLE;
// The light it throws, stronger the more power.
// The carving brightens on the beat, gently, and stays yellow or green.
int burn = brighter(regen ? FIRE : CANDLE, pulse * 0.35f);
// The light it throws, stronger the more power, swelling with the music.
float share = v.powerKw == null ? 0f : Math.min(1f, Math.abs(v.powerKw) / 80f);
glow.setShader(new RadialGradient(cx, cy, rx * 1.6f,
new int[] { AccentColor.withAlpha(regen ? FIRE : ORANGE, 0x30 + (int) (share * 0x50)),
float reach = rx * (1.6f + pulse * 0.2f);
glow.setShader(new RadialGradient(cx, cy, reach,
new int[] { AccentColor.withAlpha(regen ? FIRE : ORANGE, 0x30 + (int) (share * 0x50) + (int) (pulse * 0x60)),
AccentColor.withAlpha(regen ? FIRE : ORANGE, 0) }, null, Shader.TileMode.CLAMP));
c.drawCircle(cx, cy, rx * 1.6f, glow);
c.drawCircle(cx, cy, reach, glow);
// The stem.
path.reset();
path.moveTo(cx - 22f * u, cy - ry + 12f * u);
@@ -350,6 +362,13 @@ final class HalloweenView extends ClusterCanvasView {
label(c, power, cx, cy + ry + 44f * u, 34f * u, regen ? FIRE : BONE, face, Paint.Align.CENTER);
}
/** The colour moved {@code share} of the way to white. */
private static int brighter(int colour, float share) {
int r = (colour >> 16) & 0xFF, g = (colour >> 8) & 0xFF, b = colour & 0xFF;
return 0xFF000000 | (r + (int) ((255 - r) * share)) << 16 | (g + (int) ((255 - g) * share)) << 8
| (b + (int) ((255 - b) * share));
}
private void triangle(Canvas c, float x1, float y1, float x2, float y2, float x3, float y3, int burn, float u) {
path.reset();
path.moveTo(x1, y1);
@@ -77,6 +77,7 @@ public class MainActivity extends AppCompatActivity {
private boolean inFront;
/** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */
private View[] stylePanes;
private MusicTap musicTap;
private View consumptionPane;
private View chargingPane;
/** Display, battery, charging price and Wi-Fi. */
@@ -195,6 +196,7 @@ public class MainActivity extends AppCompatActivity {
@Override public void onPageSelected(int position) {
markCurrentPage(position);
applyOverviewFullScreen(position);
updateMusicTap();
}
});
findViewById(R.id.edge_tab).setOnClickListener(v -> leaveOverviewFullScreen());
@@ -353,9 +355,12 @@ public class MainActivity extends AppCompatActivity {
mapRoads = new MapRoads(this);
gridMapView = new GridView(this, mapRoads);
visorView = new VisorView(this);
HalloweenView halloweenView = new HalloweenView(this);
musicTap = new MusicTap(this);
halloweenView.setMusic(musicTap);
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
new NixieView(this), new ChronographView(this), gridView, gridMapView, new TimeCircuitsView(this),
new QuestView(this), visorView, new EmpireView(this), new MuertosView(this), new HalloweenView(this));
new QuestView(this), visorView, new EmpireView(this), new MuertosView(this), halloweenView);
View[] canvasPanes = new View[canvasStyles.size()];
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
// Grid with map: its map layer goes under it, in the same pane.
@@ -506,6 +511,7 @@ public class MainActivity extends AppCompatActivity {
uiHandler.post(overviewRefresh);
inFront = true;
updateMapLocation();
updateMusicTap();
}
@Override
@@ -555,6 +561,7 @@ public class MainActivity extends AppCompatActivity {
uiHandler.removeCallbacks(overviewRefresh);
inFront = false;
updateMapLocation();
updateMusicTap();
}
@Override
@@ -1751,6 +1758,14 @@ public class MainActivity extends AppCompatActivity {
// The accent choices depend on the style ("Original colours" for Retro and Terminal).
if (settingsPane != null) bindAccentColor();
updateMapLocation();
updateMusicTap();
}
/** The pumpkin listens only while it can be seen: the app in front, on the Overview, in Halloween. */
private void updateMusicTap() {
ViewPager2 pager = findViewById(R.id.content);
musicTap.setRunning(inFront && overviewStyle() == OverviewStyle.HALLOWEEN
&& pager != null && pager.getCurrentItem() == 0);
}
/**
@@ -0,0 +1,38 @@
package com.evsuite.abrp;
/**
* How strongly the Halloween pumpkin's light answers the music at this moment, from 0 to 1, out of
* the bass the audio analyser reports. What counts is the beat, not the loudness: each reading is
* placed between the bass's recent average and its recent peak, so quiet and loud songs both
* pulse and a steady hum settles at nothing. It rises fast and falls slowly, as a flame does. No
* reading or silence falls back to 0, so without music the light stays as it is. Android-free and
* unit-tested.
*/
final class MusicPulse {
/** Below this the bass counts as silence. */
static final float SILENT = 0.005f;
private static final float RISE_S = 0.03f, FALL_S = 0.25f, AVERAGE_S = 2f, PEAK_S = 4f;
private float average, peak, pulse;
/** One reading, {@code seconds} after the last; null when it failed. Returns the new pulse. */
float update(Float bass, float seconds) {
float target = 0f;
if (bass == null || bass.isNaN() || bass < SILENT) {
average = 0f;
peak = 0f;
} else {
average = average == 0f ? bass : average + (bass - average) * share(seconds, AVERAGE_S);
peak = Math.max(bass, peak - peak * share(seconds, PEAK_S));
if (peak > average) target = Math.max(0f, Math.min(1f, (bass - average) / (peak - average)));
}
pulse += (target - pulse) * share(seconds, target > pulse ? RISE_S : FALL_S);
if (pulse < 0.005f) pulse = 0f;
return pulse;
}
private static float share(float seconds, float timeConstant) {
return Math.min(1f, Math.max(0f, seconds) / timeConstant);
}
}
@@ -0,0 +1,91 @@
package com.evsuite.abrp;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.audiofx.Visualizer;
/**
* Listens to what the head unit plays for the Halloween pumpkin's light: Android's audio analyser
* on the output mix, its bass read 25 times a second on a thread of its own, made into a
* {@link MusicPulse}. Runs only while MainActivity says so (the style on screen, the app in
* front). Levels only: no sound is kept and no microphone used. Where the analyser hears nothing
* (the radio) or is not available, the pulse stays 0 and the pumpkin burns as usual.
*/
final class MusicTap {
private static final int READ_MS = 40, GIVE_UP_AFTER_FAILURES = 50;
private final Context app;
private volatile Thread worker;
private volatile float pulse;
MusicTap(Context context) {
app = context.getApplicationContext();
}
/** From 0 to 1; safe to read from any thread. */
float pulse() {
return pulse;
}
void setRunning(boolean run) {
if (run == (worker != null)) return;
if (!run) {
Thread stopping = worker;
worker = null;
pulse = 0f;
stopping.interrupt();
return;
}
if (app.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) return;
Thread thread = new Thread(this::listen, "music-tap");
thread.setDaemon(true);
worker = thread;
thread.start();
}
/** All analyser calls live here, off the main thread; it is always let go. */
private void listen() {
Thread me = Thread.currentThread();
Visualizer analyser = null;
try {
analyser = new Visualizer(0);
analyser.setCaptureSize(Visualizer.getCaptureSizeRange()[1]);
analyser.setScalingMode(Visualizer.SCALING_MODE_NORMALIZED);
if (analyser.setEnabled(true) != Visualizer.SUCCESS) return;
int rateHz = analyser.getSamplingRate() / 1000;
byte[] fft = new byte[analyser.getCaptureSize()];
MusicPulse music = new MusicPulse();
long last = System.nanoTime();
int failures = 0;
while (worker == me && failures < GIVE_UP_AFTER_FAILURES) {
Thread.sleep(READ_MS);
boolean read = analyser.getFft(fft) == Visualizer.SUCCESS;
failures = read ? 0 : failures + 1;
long now = System.nanoTime();
float value = music.update(read ? SoundLevels.band(fft, rateHz, 20f, 250f) : null, (now - last) / 1e9f);
last = now;
if (worker == me) pulse = value;
}
} catch (InterruptedException e) {
// Told to stop.
} catch (RuntimeException e) {
// No analyser here: the pumpkin burns as usual.
} finally {
if (worker == me) pulse = 0f;
if (analyser != null) {
try {
analyser.setEnabled(false);
} catch (RuntimeException ignored) {
// Released next either way.
}
try {
analyser.release();
} catch (RuntimeException ignored) {
// Nothing more can be done from here.
}
}
}
}
}
@@ -0,0 +1,71 @@
package com.evsuite.abrp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class MusicPulseTest {
private static final float STEP = 0.04f;
@Test
public void noReadingAndSilenceLightNothing() {
MusicPulse p = new MusicPulse();
assertEquals(0f, p.update(null, STEP), 0f);
assertEquals(0f, p.update(Float.NaN, STEP), 0f);
assertEquals(0f, p.update(0f, STEP), 0f);
}
@Test
public void aBeatLightsAndFadesSlowerThanItRises() {
MusicPulse p = new MusicPulse();
for (int i = 0; i < 50; i++) p.update(0.05f, STEP);
float beat = p.update(0.5f, STEP);
assertTrue("a beat lights it, got " + beat, beat > 0.8f);
float after = p.update(0.05f, STEP);
assertTrue("it fades, got " + after, after < beat);
assertTrue("slowly, got " + after, after > beat * 0.7f);
}
@Test
public void aSteadyHumSettlesAtNothing() {
MusicPulse p = new MusicPulse();
float last = 1f;
for (int i = 0; i < 400; i++) last = p.update(0.3f, STEP);
assertEquals(0f, last, 0f);
}
@Test
public void quietAndLoudSongsPulseAlike() {
assertEquals(beatOf(0.5f), beatOf(0.05f), 0.02f);
}
private static float beatOf(float scale) {
MusicPulse p = new MusicPulse();
float top = 0f;
for (int i = 0; i < 500; i++) {
top = Math.max(top * 0.99f, p.update(i % 12 == 0 ? scale : scale * 0.2f, STEP));
}
return top;
}
@Test
public void whenTheMusicStopsItGoesOut() {
MusicPulse p = new MusicPulse();
for (int i = 0; i < 100; i++) p.update(i % 12 == 0 ? 0.5f : 0.1f, STEP);
float last = 1f;
for (int i = 0; i < 100; i++) last = p.update(null, STEP);
assertEquals(0f, last, 0f);
}
@Test
public void itStaysBetweenZeroAndOne() {
MusicPulse p = new MusicPulse();
for (int i = 0; i < 2000; i++) {
Float bass = i % 11 == 0 ? null : Float.valueOf(i % 7 == 0 ? 1f : 0.1f);
float v = p.update(bass, i % 5 == 0 ? 2f : STEP);
assertTrue(v >= 0f && v <= 1f);
}
}
}