diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a7a86840..3728912a 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 }} @@ -142,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 @@ -153,6 +161,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 +231,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..cd07d546 100644 --- a/media/launch/index.html +++ b/media/launch/index.html @@ -143,6 +143,15 @@
+ + + +
@@ -297,6 +306,13 @@ tl.to(state, { v: value, duration: dur, ease: "power2.out", onUpdate: () => { el.textContent = Math.round(state.v).toLocaleString("en-US"); } }, at); } + // 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); + // 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..0e9b48da --- /dev/null +++ b/media/launch/tools/fetch-music.sh @@ -0,0 +1,51 @@ +#!/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='78ef3c248c376997d0175ef534ab3243677e4323f4a8ff3cb07f698a3441cd7a' # of the zip, 2024-10-27 + +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" +# 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 +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