Skip to content

Device shells: <recorder> mic capture + engine-drawn <audio> control - #18

Merged
andrevanzuydam merged 5 commits into
mainfrom
feature/device-shells-ios-recorder
Sep 17, 2026
Merged

andrevanzuydam merged 5 commits into
mainfrom
feature/device-shells-ios-recorder

Conversation

@andrevanzuydam

Copy link
Copy Markdown
Owner

Completes the on-device <recorder> work and fixes two device bugs found while testing on a real iPhone and the Android emulator.

What's in here

  • feat(ios/android): <recorder> mic capture — iOS AVAudioRecorder (app-layer Obj-C, since FPC's -Tios has no AVFoundation unit) and Android MediaRecorder over JNI, both to an AAC .m4a. TinaSetRecording routes the clip into an <audio id="rec"> player and fires onrecord.
  • fix(android): notify.show SIGSEGV — a real FPC -O2 miscompile: the notify unit's small parameterless Env() helper was folded away and env aliased the first string param, so env^.FindClass dereferenced the notification title as a JNIEnv. Diagnosed from the tombstone (fault addr = the ASCII of "Saved a recording") + objdump of a byte-identical rebuild. Fix = fetch the JNIEnv inline in AndroidNotify.
  • feat(audio): engine-drawn <audio controls> — replaces the shell-overlaid native player (a black AVPlayerViewController on iOS) with a control the engine draws itself: a light bar with a ▶/⏸ button and a progress track (ckAudio + PaintAudioControl). A tap returns TINA_AUDIO_TOGGLE; the engine API TinaAudioSrc/TinaAudioWantPlay/TinaSetAudioProgress + shell contract AudioPlay/AudioPause/AudioProgress drive playback. iOS/macOS AVAudioPlayer, Android MediaPlayer. Looks identical on every platform.

Verification

  • test_dom ✅ · compliance 219/219 ✅ · leakcheck clean ✅ · test_interact 39/39
  • macOS renderer: audio control draws correctly (idle ▶, playing ⏸ + progress)
  • Android live: recorder-stop posts its notification (no crash); audio control plays a tone, bar advances, pause + end-reset work
  • iOS: builds, deploys and launches on device; on-device audio playback pending a manual tap (iOS live-input isn't wired in the tooling)

🤖 Generated with Claude Code

andrevanzuydam and others added 5 commits September 17, 2026 16:54
Ports the <recorder> control to iOS. Since FPC's iOS target has no
AVFoundation unit, the capture is app-layer Obj-C (like ImageLoader.m),
not a Pascal objcclass shell:

- tina4ios.pas: new exported `tina4_set_recording(path)` → TinaSetRecording
  (the core stamps the control, routes the clip into <audio id="rec">,
  fires onrecord; '' rolls a failed start back to idle). tina4.h gains
  TINA_RECORD_START/STOP and the decl.
- Tina4View.m: on TINA_RECORD_START/STOP it starts/stops an AVAudioRecorder
  (AAC .m4a in the temp dir) after requesting the mic permission via
  AVAudioSession (PlayAndRecord); the file path (or '') is handed back.
- Info.plist: NSMicrophoneUsageDescription.
- showcase.html: a <recorder> + <audio id="rec"> demo section.

Verified on device (iPhone 12 Pro): tap-record → speak → tap-stop saves
a real tina4-rec-*.m4a and loads it into the audio player (no rollback,
so capture + permission + hand-off all succeeded).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
syncVideos built the player URL with URLWithString:, which returns a
scheme-less URL for a local file path — so the <recorder>'s temp-dir
.m4a saved but the <audio id="rec"> player couldn't open it (no
playback). A src starting with '/' now goes through fileURLWithPath:.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports the <recorder> control to Android:
- tina4jni.pas: new exported JNI `Tina4View_nativeSetRecording` →
  TinaSetRecording ('' rolls a failed capture back to idle).
- MainActivity: startRecording/stopRecording drive a MediaRecorder
  (MIC → AAC .m4a in filesDir), requesting RECORD_AUDIO at runtime
  (starts on grant, rolls back on denial). AndroidManifest gains
  RECORD_AUDIO.
- Tina4View: touch codes 6/7 (TINA_RECORD_START/STOP) delegate to
  MainActivity; onRecordingDone hands the path back. syncVideos now
  builds a file:// Uri for a local path so the recorded clip plays in
  the <audio id="rec"> player (same local-file fix as iOS).
- showcase.html: a <recorder> + <audio id="rec"> demo section.

Build-verified: libtina4.so (arm64-v8a + armeabi-v7a) and the debug APK
both compile. On-device run pending an adb-visible emulator
(`adb install -r android/tina4pascal-debug.apk`, then tap the recorder).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A separate parameterless Env() helper in the Android notify unit was
elided by -O2 and `env` aliased to AndroidNotify's first string param, so
`env^.FindClass` dereferenced the notification title ("Saved a recording")
as a JNIEnv vtable and crashed (SEGV_MAPERR, fault addr = the string
bytes). This fired on <recorder> stop, whose onrecord runs notify.show.

Inline the GetEnv/AttachCurrentThread acquisition into AndroidNotify so
there is no small function for the optimiser to fold away; env is now a
real interface pointer. Verified on emulator-5554: recorder stop posts the
"Saved a recording" notification, process stays alive, no SIGSEGV.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
<audio controls> was a light placeholder box with a shell-owned native
player laid over it — on iOS that player was an AVPlayerViewController, a
black video surface with no usable controls. Replace it with a control the
engine draws itself, so it looks the same everywhere and shows a real
play/stop button.

Core: a new ckAudio control kind; PaintAudioControl draws a light bar with
a round play/pause button and a progress track (reads the 'playing' and
'progress' attributes). A tap returns TINA_AUDIO_TOGGLE; <audio> is no
longer collected as a native embed. New engine API TinaAudioSrc /
TinaAudioWantPlay / TinaSetAudioProgress lets a shell drive playback and
push the elapsed fraction + sounding state back each frame.

Contract: AudioPlay / AudioPause / AudioProgress on TTina4Shell (safe
defaults → a shell with no audio out just draws an idle bar).

Shells: iOS + macOS play through AVAudioPlayer, Android through MediaPlayer
(a relative asset src is resolved against the asset base); the desktop
htmlviewer harness drives it via the shell contract and polls progress in
its tick loop. iOS also drops the recorder's separate auto-play — the clip
now lands in the control and the user taps play, one playback path.

Verified: test_dom, compliance 219/219, leakcheck clean, test_interact
39/39; the control renders on the macOS renderer and plays live on Android
(tap plays a tone, the bar advances, pause works, resets at end, no crash).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@andrevanzuydam
andrevanzuydam merged commit c343c52 into main Sep 17, 2026
3 checks passed
@andrevanzuydam
andrevanzuydam deleted the feature/device-shells-ios-recorder branch September 17, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant