From 14030b515a39896b18c0d12578580ec1fc411e74 Mon Sep 17 00:00:00 2001 From: Largo Date: Fri, 2 Oct 2026 00:12:18 +0200 Subject: [PATCH 1/3] Launch video: In the Hall of the Mountain King Grieg's piece, played by the Czech National Symphony Orchestra for Musopen and marked public domain (CC PDM 1.0) on classicals.de, which is where media/launch/tools/fetch-music.sh downloads it from before a render; the repository does not carry it. The quiet opening plays under the 2011 scene, the film cuts into the full orchestra at the title, and the piece's last chord is meant to fall on the end card. --profile prints the recording's loudness every half second, to place that cut without listening. release.yml fetches the music before the render, and run by hand with video_for: renders only the video and replaces it, and its SHA256SUMS line, on a release that is already out. The SHA-256 of the download and the offset of the cut are not pinned yet: this change's own CI run reports both. --- .github/workflows/release.yml | 41 ++++++++++++++++++++++++- Docs/releases/v4.0.0.md | 4 +++ media/launch/.gitignore | 1 + media/launch/README.md | 24 ++++++++++++--- media/launch/index.html | 12 ++++++++ media/launch/tools/fetch-music.sh | 50 +++++++++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 5 deletions(-) create mode 100755 media/launch/tools/fetch-music.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a7a86840..a675f9cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,8 @@ name: Release # tag v* publishes them as a GitHub release (a pre-release while the version has a # suffix such as -preview1), with Docs/releases/.md as the notes when there is one; # running the workflow by hand, or a pull request that changes the packaging or the -# video, only keeps them as workflow artifacts. +# video, only keeps them as workflow artifacts. Run by hand with video_for set to a tag, +# it renders only the video and replaces it, and its SHA256SUMS line, on that release. on: push: tags: ['v*'] @@ -15,6 +16,11 @@ on: - Util/package-release.sh - media/launch/** workflow_dispatch: + inputs: + video_for: + description: Render only the launch video and replace it on this release (a tag, such as v4.0.0) + required: false + default: '' permissions: contents: read @@ -31,6 +37,7 @@ env: jobs: package: name: ${{ matrix.rid }} + if: github.event_name != 'workflow_dispatch' || inputs.video_for == '' runs-on: ${{ matrix.os }} # macOS is untested (README, Platforms): a failure there does not hold the release back. continue-on-error: ${{ matrix.experimental }} @@ -153,6 +160,10 @@ jobs: - name: FFmpeg run: sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg + # The music is not in the repository; the script fetches the pinned recording. + - name: Music + run: bash media/launch/tools/fetch-music.sh --profile + # lint is static and must pass; check also audits layout and contrast in the # browser and only warns here. The version is pinned in package.json. - name: Check and render @@ -219,3 +230,31 @@ jobs: gh release create "$GITHUB_REF_NAME" dist/* \ --repo "$GITHUB_REPOSITORY" --title "IronRuby ${GITHUB_REF_NAME#v}" \ "${notes[@]}" --generate-notes $prerelease + + # By hand: a new render of the video on a release that is already out. Only the video + # and its line in SHA256SUMS change; the archives stay as they were published. + replace-video: + name: replace the video on ${{ inputs.video_for }} + if: github.event_name == 'workflow_dispatch' && inputs.video_for != '' + needs: video + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v8 + with: + name: ironruby-launch-video + path: dist + + - name: Upload + working-directory: dist + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.video_for }} + run: | + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern SHA256SUMS + sum=$(sha256sum ironruby-4-launch.mp4) + { grep -v ' ironruby-4-launch.mp4$' SHA256SUMS; echo "$sum"; } > SHA256SUMS.new + mv SHA256SUMS.new SHA256SUMS + cat SHA256SUMS + gh release upload "$TAG" ironruby-4-launch.mp4 SHA256SUMS --clobber --repo "$GITHUB_REPOSITORY" diff --git a/Docs/releases/v4.0.0.md b/Docs/releases/v4.0.0.md index 97ab7116..85dfd1be 100644 --- a/Docs/releases/v4.0.0.md +++ b/Docs/releases/v4.0.0.md @@ -48,3 +48,7 @@ database drivers), and macOS is built but untested. The Thanks to everyone who built IronRuby at Microsoft and in IronLanguages, and to the prism and ruby/spec projects, without which none of this would be measurable. + +*Music in the launch video: Edvard Grieg, In the Hall of the Mountain King, played by the +Czech National Symphony Orchestra for Musopen; public domain, from +[classicals.de](https://www.classicals.de/grieg-peer-gynt).* diff --git a/media/launch/.gitignore b/media/launch/.gitignore index 6bc1a607..dcb20cfc 100644 --- a/media/launch/.gitignore +++ b/media/launch/.gitignore @@ -2,3 +2,4 @@ renders/ snapshots/ node_modules/ .hyperframes/ +assets/music/ diff --git a/media/launch/README.md b/media/launch/README.md index a142efba..d6009e10 100644 --- a/media/launch/README.md +++ b/media/launch/README.md @@ -13,7 +13,22 @@ the title frame. | `assets/fonts/` | Inter and JetBrains Mono (SIL Open Font License, the licences beside them), so a render needs no network for fonts | | `assets/images/notepad.png` | [`Samples/Notepad`](../../Samples/Notepad) editing its own source, captured on Windows 11 at 200 % | | `poster.jpg` | The title frame with a play button | -| `teaser.webp` | 23 s of the film at 960x540 and 10 fps: the title, the irb session, .NET and the Notepad, the end card. Frames seeked from `index.html` in Chrome, encoded with ImageMagick (`magick -delay 10 -loop 0 f*.png -resize 960x540 -quality 72 teaser.webp`), 1.3 MB | +| `teaser.webp` | 23 s of the film at 960x540 and 10 fps: the title, the irb session, .NET and the Notepad, the end card. Frames seeked from `index.html` in Chrome, encoded with ImageMagick (`magick -delay 10 -loop 0 f*.png -resize 960x540 -quality 72 teaser.webp`), 1.3 MB; silent, as images are | +| `tools/fetch-music.sh` | Fetches the music into `assets/music/` (not in git), checked against a pinned SHA-256 | + +## Music + +Edvard Grieg, *In the Hall of the Mountain King* (Peer Gynt Suite No. 1, Op. 46, IV), +played by the Czech National Symphony Orchestra for [Musopen](https://musopen.org), from +[classicals.de](https://www.classicals.de/grieg-peer-gynt). The recording is in the public +domain (Creative Commons Public Domain Mark 1.0), so it needs no licence; it is credited +here and in the release notes all the same. + +The repository does not carry it: `tools/fetch-music.sh` downloads it before a render. +The quiet opening plays under the 2011 scene; at the title the film cuts into the full +orchestra (`data-media-start` on `#music-main`), placed so that the piece's last chord +falls on the end card. `tools/fetch-music.sh --profile` prints the recording's loudness +every half second, which is how the cut was placed. Every figure on screen comes from the project README (ruby/spec counts, benchmark ratios, versions), and every line of code runs on IronRuby 4.0. Change the README, change the film. @@ -25,6 +40,7 @@ pinned in `package.json`. ```sh cd media/launch +tools/fetch-music.sh # the music, into assets/music/ npm run lint npm run check # also audits layout and contrast in the browser npm run render # renders/ironruby-4-launch.mp4, 60 fps @@ -32,6 +48,6 @@ npm run preview # the HyperFrames studio, to scrub and edit ``` `.github/workflows/release.yml` does the same on Ubuntu for every `v*` tag (and for pull -requests that touch this folder) and attaches the MP4 to the release. - -The film is silent: it is meant to play muted in a feed. +requests that touch this folder) and attaches the MP4 to the release. Run by hand with +`video_for: v4.0.0`, it renders only the film and replaces it on that release, along with +its line in `SHA256SUMS`. diff --git a/media/launch/index.html b/media/launch/index.html index c982383d..0c6dce7f 100644 --- a/media/launch/index.html +++ b/media/launch/index.html @@ -143,6 +143,12 @@
+ + + +
@@ -297,6 +303,12 @@ tl.to(state, { v: value, duration: dur, ease: "power2.out", onUpdate: () => { el.textContent = Math.round(state.v).toLocaleString("en-US"); } }, at); } + // Music: the opening fades out under the cut, the orchestra is there with the title. + tl.fromTo("#music-intro", { volume: 0 }, { volume: 1, duration: 0.8, ease: "none" }, 0); + tl.to("#music-intro", { volume: 0, duration: 0.6, ease: "none" }, 5.3); + tl.fromTo("#music-main", { volume: 0 }, { volume: 1, duration: 0.25, ease: "none" }, 5.3); + tl.to("#music-main", { volume: 0, duration: 1.2, ease: "none" }, 53.8); + // The backdrop drifts through the whole film. tl.fromTo("#glow-ruby", { x: 0, y: 0 }, { x: 260, y: 140, duration: 55, ease: "sine.inOut" }, 0); tl.fromTo("#glow-violet", { x: 0, y: 0 }, { x: -320, y: -180, duration: 55, ease: "sine.inOut" }, 0); diff --git a/media/launch/tools/fetch-music.sh b/media/launch/tools/fetch-music.sh new file mode 100755 index 00000000..c74e1bca --- /dev/null +++ b/media/launch/tools/fetch-music.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Fetches the launch video's music: Edvard Grieg, "In the Hall of the Mountain King" +# (Peer Gynt Suite No. 1, Op. 46, IV), played by the Czech National Symphony Orchestra +# for Musopen, from classicals.de. The recording is in the public domain, marked +# CC PDM 1.0 (https://www.classicals.de/grieg-peer-gynt), so it may be used in the video +# without conditions; the release notes credit it all the same. +# +# media/launch/tools/fetch-music.sh # -> media/launch/assets/music/mountain-king.mp3 +# media/launch/tools/fetch-music.sh --profile # and print its loudness every half second +# +# The repository does not carry the recording: the render fetches it, and the checksum +# pins the exact file, so a different download cannot slip into a release unnoticed. +# --profile is how the cuts in index.html were placed (data-media-start) without +# listening: it prints the momentary loudness (EBU R128, LUFS) over time. +set -euo pipefail + +URL='https://www.quantumdigitalmedia.de/Classicals-Music/Music/MO%20Collection/Classicals.de%20-%20Grieg%20-%20Peer%20Gynt%20Suite%20No.%201%2C%20Op.%2046%20-%20IV.%20In%20the%20Hall%20Of%20The%20Mountain%20King.zip' +SHA256='' # of the zip; empty until the first download has been checked + +here=$(cd "$(dirname "$0")/.." && pwd) +out="$here/assets/music/mountain-king.mp3" +mkdir -p "$(dirname "$out")" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +curl -fsSL --retry 3 -o "$tmp/music.zip" "$URL" +actual=$(sha256sum "$tmp/music.zip" | cut -d' ' -f1) +if [ -z "$SHA256" ]; then + echo "fetch-music.sh: no checksum pinned yet; this download is sha256 $actual" >&2 +elif [ "$actual" != "$SHA256" ]; then + echo "fetch-music.sh: the download is sha256 $actual, not the pinned $SHA256" >&2 + exit 1 +fi + +unzip -l "$tmp/music.zip" +mp3=$(unzip -Z1 "$tmp/music.zip" | grep -i '\.mp3$' | head -1) +if [ -z "$mp3" ]; then + echo "fetch-music.sh: no MP3 in the download" >&2 + exit 1 +fi +unzip -p "$tmp/music.zip" "$mp3" > "$out" +echo "fetch-music.sh: $mp3 -> $out ($(wc -c < "$out") bytes)" + +if [ "${1:-}" = --profile ]; then + ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1 "$out" + # ebur128 reports every 100 ms; keep every fifth report. + ffmpeg -nostats -hide_banner -i "$out" -af ebur128=metadata=1,ametadata=print:key=lavfi.r128.M -f null - 2>&1 | + awk '/pts_time/ { split($0, a, "pts_time:"); t = a[2] + 0 } + /lavfi.r128.M=/ { split($0, b, "="); if (n++ % 5 == 0) printf "%7.1f s %6.1f LUFS\n", t, b[2] }' +fi From 841a6d25b8332eff3b4f8a750e805f82f0d46a15 Mon Sep 17 00:00:00 2001 From: Largo Date: Fri, 2 Oct 2026 00:23:42 +0200 Subject: [PATCH 2/3] Launch video: pin the music's checksum, place the cut by its loudness From the first run's fetch-music.sh --profile: the recording is 154.1 s, the opening near -40 LUFS, the full orchestra entering at 101 s, a general pause at 147.5-148.3 s and the final chords at 148.4-150.4 s. The film now cuts in at 104.1 s with the title, which puts the pause just before the end card and the final chord on it (49.6 s); the opening under 2011 plays 11 dB up. The zip's SHA-256 is pinned, and the script skips the __MACOSX resource fork the zip also carries. --- media/launch/index.html | 13 ++++++++----- media/launch/tools/fetch-music.sh | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/media/launch/index.html b/media/launch/index.html index 0c6dce7f..3d8aa6ea 100644 --- a/media/launch/index.html +++ b/media/launch/index.html @@ -144,10 +144,13 @@
- - + it for the render). The recording is 154.1 s; from its loudness (fetch-music.sh + --profile): the opening is near -40 LUFS, the full orchestra enters at 101 s, the + general pause is 147.5-148.3 s, the final chords 148.4-150.4 s. So the opening + plays under 2011, 11 dB up, and the film cuts in at 104.1 s with the title: the + pause falls just before the end card, and the final chord on it (49.6 s). --> + +
@@ -304,7 +307,7 @@ } // Music: the opening fades out under the cut, the orchestra is there with the title. - tl.fromTo("#music-intro", { volume: 0 }, { volume: 1, duration: 0.8, ease: "none" }, 0); + tl.fromTo("#music-intro", { volume: 0 }, { volume: 3.5, duration: 0.8, ease: "none" }, 0); tl.to("#music-intro", { volume: 0, duration: 0.6, ease: "none" }, 5.3); tl.fromTo("#music-main", { volume: 0 }, { volume: 1, duration: 0.25, ease: "none" }, 5.3); tl.to("#music-main", { volume: 0, duration: 1.2, ease: "none" }, 53.8); diff --git a/media/launch/tools/fetch-music.sh b/media/launch/tools/fetch-music.sh index c74e1bca..0e9b48da 100755 --- a/media/launch/tools/fetch-music.sh +++ b/media/launch/tools/fetch-music.sh @@ -15,7 +15,7 @@ set -euo pipefail URL='https://www.quantumdigitalmedia.de/Classicals-Music/Music/MO%20Collection/Classicals.de%20-%20Grieg%20-%20Peer%20Gynt%20Suite%20No.%201%2C%20Op.%2046%20-%20IV.%20In%20the%20Hall%20Of%20The%20Mountain%20King.zip' -SHA256='' # of the zip; empty until the first download has been checked +SHA256='78ef3c248c376997d0175ef534ab3243677e4323f4a8ff3cb07f698a3441cd7a' # of the zip, 2024-10-27 here=$(cd "$(dirname "$0")/.." && pwd) out="$here/assets/music/mountain-king.mp3" @@ -33,7 +33,8 @@ elif [ "$actual" != "$SHA256" ]; then fi unzip -l "$tmp/music.zip" -mp3=$(unzip -Z1 "$tmp/music.zip" | grep -i '\.mp3$' | head -1) +# The zip also holds a macOS resource fork (__MACOSX/._.mp3), which is not audio. +mp3=$(unzip -Z1 "$tmp/music.zip" | grep -v '^__MACOSX/' | grep -i '\.mp3$' | head -1) if [ -z "$mp3" ]; then echo "fetch-music.sh: no MP3 in the download" >&2 exit 1 From 57f5c498f3be2bde3c19934617bc8612b4ffadc5 Mon Sep 17 00:00:00 2001 From: Largo Date: Fri, 2 Oct 2026 01:11:02 +0200 Subject: [PATCH 3/3] Launch video: no volume tween above 1, a 20-minute limit on the render A volume tween sets the media element's volume, which only goes from 0 to 1: tweening the opening to 3.5 failed the render (IndexSizeError), and hyperframes then waited until the job's 45-minute limit. The opening keeps its +11 dB as a static data-volume, which the mixer applies, and needs no fade: the recording begins in silence and the cut into the orchestra covers its end. --- .github/workflows/release.yml | 3 ++- media/launch/index.html | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a675f9cd..3728912a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -149,7 +149,8 @@ jobs: name: launch video runs-on: ubuntu-latest continue-on-error: true - timeout-minutes: 45 + # A render takes about 7 minutes; a failed one can leave hyperframes waiting. + timeout-minutes: 20 steps: - uses: actions/checkout@v7 diff --git a/media/launch/index.html b/media/launch/index.html index 3d8aa6ea..cd07d546 100644 --- a/media/launch/index.html +++ b/media/launch/index.html @@ -306,9 +306,10 @@ tl.to(state, { v: value, duration: dur, ease: "power2.out", onUpdate: () => { el.textContent = Math.round(state.v).toLocaleString("en-US"); } }, at); } - // Music: the opening fades out under the cut, the orchestra is there with the title. - tl.fromTo("#music-intro", { volume: 0 }, { volume: 3.5, duration: 0.8, ease: "none" }, 0); - tl.to("#music-intro", { volume: 0, duration: 0.6, ease: "none" }, 5.3); + // Music: the orchestra is there with the title, and fades out at the very end. The + // opening keeps its static data-volume (3.5, +11 dB): a volume tween is a media + // element's volume, which only goes from 0 to 1, and the render fails beyond that. + // It needs no fade either: the recording begins in silence, and the cut covers its end. tl.fromTo("#music-main", { volume: 0 }, { volume: 1, duration: 0.25, ease: "none" }, 5.3); tl.to("#music-main", { volume: 0, duration: 1.2, ease: "none" }, 53.8);