<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Peter Obel</title>
    <link>https://peterobel.com/</link>
    <description>Technical notes by Peter Obel.</description>
    <language>en-US</language>
    <atom:link href="https://peterobel.com/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>From a Dead Vendor App to a Cross-Platform Multi-Mic Recorder: Protocol Recovery, Native Codecs, CI and Synced Remote Takes</title>
      <link>https://peterobel.com/blog/reverse-engineered-bluetooth-mic-to-cross-platform-recorder/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/reverse-engineered-bluetooth-mic-to-cross-platform-recorder/</guid>
      <pubDate>Sat, 17 Oct 2026 09:00:00 -0700</pubDate>
      
      <description>We had several wireless lavalier mics whose vendor app had been discontinued. The mics still powered on and paired, but the only thing a phone or PC could use was the standard Bluetooth headset profile. The good audio path ran over a private serial protocol that only the vendor app spoke, and the vendor’s update and licence servers were offline.</description>
      <content:encoded><![CDATA[<h2 id="problem">Problem</h2>
<p>We had several wireless lavalier mics whose vendor app had been discontinued. The mics still powered on and paired, but the only thing a phone or PC could use was the standard Bluetooth headset profile. The good audio path ran over a private serial protocol that only the vendor app spoke, and the vendor&rsquo;s update and licence servers were offline.</p>
<p>The goal was concrete: record several of these mics at once, each as its own track, on Windows and Android, with the protocol fully under our control. Later goals added video, other microphones, a build pipeline that ships to stores, and a remote mode where one device starts and collects takes from another.</p>
<p>This post follows the path from a decompiled APK to that result, and covers the failures that shaped each step.</p>
<h2 id="production-symptoms">Production Symptoms</h2>
<ul>
<li>Through the headset profile the mic was a call microphone: 16 kHz wideband at best, and one mic at a time.</li>
<li>The vendor app no longer installed or worked on current phones. Its servers timed out, refused connections or redirected to a DNS block page.</li>
<li>No public documentation existed for the serial protocol or the audio codec.</li>
<li>Each mic has its own crystal. Two mics, a sound card and a camera run on four different clocks, so long takes drift apart.</li>
</ul>
<h2 id="step-1-recover-the-protocol-statically">Step 1: Recover the Protocol Statically</h2>
<p>The first plan assumed we would need Bluetooth HCI snoop logs and runtime hooking, because the vendor SDK shipped an &ldquo;authenticate&rdquo; native library. Neither was needed.</p>
<p>The path that worked:</p>
<ol>
<li>Decompile the APK (jadx). The SDK core was obfuscated into single-letter packages: transport, session logic, parsers, a setting-to-command map, payload builders and codec detection.</li>
<li>Map the obfuscated classes against a public sample project of an older version of the same SDK, which still had readable names.</li>
<li>Rewrite framing, commands and parameter encoding as a small Python module.</li>
<li>Check the encoder against the two session packets that the app hard-codes. Both came out byte-identical, which confirmed the checksum rule before any hardware test.</li>
</ol>
<p>The link format is a 9-byte header followed by an optional command frame. All integers are big-endian, and every checksum is the two&rsquo;s complement of the byte sum:</p>
<pre><code class="language-python">def checksum(data: bytes) -&gt; int:
    &#34;&#34;&#34;Two&#39;s complement of the byte sum: sum(data) &#43; cksum == 0 mod 256.&#34;&#34;&#34;
    return (-sum(data)) &amp; 0xFF


def build_link_header(total_len: int, seq: int, ack: int, ptype: int) -&gt; bytes:
    &#34;&#34;&#34;9-byte link header: FF 5A len16 40 seq ack type cksum.&#34;&#34;&#34;
    h = SYNC &#43; struct.pack(&#34;&gt;HBBBB&#34;, total_len, HDR_BYTE4, seq &amp; 0xFF, ack &amp; 0xFF, ptype)
    return h &#43; bytes([checksum(h)])


def build_frame(cmd: int, payload: bytes, seq: int = 0, ack: int = 0) -&gt; bytes:
    sub = b&#34;\x00\x00&#34; &#43; SUB_MAGIC &#43; struct.pack(&#34;&gt;HH&#34;, len(payload) &#43; 6, cmd)
    total = 9 &#43; len(sub) &#43; len(payload) &#43; 1
    body = sub &#43; payload
    return build_link_header(total, seq, ack, 2) &#43; body &#43; bytes([checksum(body)])</code></pre>
<p>Settings use a 6-byte parameter block (<code>00 06 | subtype16 | value16</code>). The vendor code divides by 255 instead of 256 when it splits the high byte. The reference encoder keeps that bug on purpose, because the device firmware was written against it.</p>
<p>The static pass also covered authentication. The licence library checks the app&rsquo;s own SDK licence against a server, not the device. The device-level challenge command exists, but its handler in the app is empty and no response is ever sent. On the firmware we tested, the device never sent the challenge.</p>
<h2 id="step-2-identify-the-audio-codec">Step 2: Identify the Audio Codec</h2>
<p>The audio command carries a 4-byte timestamp and then a frame with a 4-byte header. Three findings together identified the codec:</p>
<ul>
<li>The decoder library imports <code>celt_mode_create</code>, <code>celt_decoder_create</code> and a four-argument <code>celt_decode</code>. That signature exists only in CELT 0.7 to 0.10; 0.11 added a frame-size argument.</li>
<li>Its internal functions carry a prefix over upstream names. All 19 of them exist in the CELT 0.7.1 source. Pitch prediction is among them, and it was removed in 0.8, which rules out later versions.</li>
<li>The rate and frame-size tables were read straight from the library&rsquo;s read-only data section (in a disassembler, after mapping virtual addresses to file offsets).</li>
</ul>
<p>The 4-byte header is a vendor wrapper that the decoder strips before calling stock libcelt:</p>
<pre><code class="language-python">def parse_header(b):
    if len(b) &lt; 4 or b[0] != 0x9E or (b[1] &amp; 0x24):
        return None
    rate = RATES[(b[1] &gt;&gt; 6) &amp; 3]
    ch_code = (b[1] &gt;&gt; 4) &amp; 3
    if ch_code &gt; 1:
        return None
    return {&#34;rate&#34;: rate, &#34;channels&#34;: ch_code &#43; 1, &#34;frame_size&#34;: FRAME_SIZES[b[1] &amp; 3],
            &#34;flags&#34;: b[2], &#34;nbytes&#34;: b[3]}</code></pre>
<p>The proof was empirical: stock CELT 0.7.1, built from the upstream tarball, decoded the first 443 captured frames with zero errors. The stream is 44.1 kHz stereo with 512 samples and 190 bytes per frame. That is about 86 packets per second and about 131 kbit/s. Left and right are identical, because the mic has a mono capsule.</p>
<h2 id="step-3-the-proof-of-concept-on-real-hardware">Step 3: The Proof of Concept on Real Hardware</h2>
<p>The proof of concept was a Python client on a Windows ARM64 laptop. On Windows you can connect an RFCOMM socket to a service UUID with port 0, and the stack resolves the channel through SDP. That removes the need to know the channel number:</p>
<pre><code class="language-python">def open_rfcomm(mac, channel=None, timeout=10.0):
    s = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM)
    if channel:
        s.settimeout(timeout)
        s.connect((mac, channel))
    elif os.name == &#34;nt&#34;:
        s.setblocking(True)  # raw Winsock connect must not see a non-blocking socket
        _connect_by_uuid_windows(s, mac, SPP_UUID)
    ...</code></pre>
<p>The <code>setblocking(True)</code> line was the first live bug. The first probe failed with Winsock error 10035 (<code>WSAEWOULDBLOCK</code>). A timeout set earlier had made the socket non-blocking, and the raw <code>connect</code> call through ctypes does not handle that.</p>
<p>After the fix the observed sequence was:</p>
<ol>
<li>Session open at +0.5 s.</li>
<li>Device version report at +0.84 s, followed by an unprompted state dump.</li>
<li>Four rounds of clock sync (about 130 ms each).</li>
<li>Start-audio command at +1.37 s, first audio at +2.29 s.</li>
</ol>
<p>A heartbeat every 500 ms keeps the stream alive. Session open and clock sync turned out to be optional; audio streams without them.</p>
<p>The live tests answered the questions static analysis could not:</p>
<ul>
<li>Setting writes are never acknowledged, but they persist across reconnects.</li>
<li>The quality levels request 190/170/140 bytes per frame, and the device actually delivers 190/180/150.</li>
<li>The standard A2DP headphone output works while the private stream is running, so audio back to the mic needs no private protocol.</li>
<li>An early assumption from community material was that the headset profile would be narrowband (3.5 kHz). On Windows 11 it negotiates wideband mSBC at 16 kHz. It is still far below the private stream, which is full band to about 20 kHz.</li>
</ul>
<p>Firmware modification was part of the original question. It was dropped: no firmware image newer than the device&rsquo;s own was obtainable, and nothing was ever sent to or flashed onto a mic.</p>
<h3 id="the-clock-problem-shows-up">The clock problem shows up</h3>
<p>A bridge script played the decoded stream to a Windows output device. The first run used a plain FIFO. The buffer grew to 174 ms and 232 ms of audio was dropped. The mic&rsquo;s sample clock and the sound card&rsquo;s clock do not run at the same rate, and a FIFO between them either fills up or runs dry.</p>
<p>The fix was a jitter buffer that resamples by a ratio steered by its own fill level, clamped to ±0.5 % so the correction is inaudible. The same controller ended up in the app:</p>
<pre><code class="language-csharp">var err = (_count - _frac - TargetSamples) / Math.Max(TargetSamples, 1);
Ratio = 1.0 &#43; Math.Clamp(Gain * err, -MaxRatioDeviation, MaxRatioDeviation);
var lastIdx = _frac &#43; (frames - 1) * Ratio;
if (lastIdx &#43; 1 &gt;= _count)
{
    Underruns&#43;&#43;;
    _started = false;
    output.Clear();
    return false;
}</code></pre>
<p>With it, a 40 s live run decoded 3,489 frames with 0 decode errors and 0 underruns, and buffer latency stayed between 46 and 91 ms.</p>
<h2 id="step-4-from-script-to-windows-app">Step 4: From Script to Windows App</h2>
<p>Requirements for the app: several mics at once, one start/stop for all of them, native on Windows ARM64 and x64, and nothing that blocks a later macOS or Linux port.</p>
<p>.NET MAUI was the first candidate. It was dropped because it has no Linux target, and on macOS (through Mac Catalyst) it cannot use the classic Bluetooth API that RFCOMM needs. The stack became .NET 10 with Avalonia.</p>
<p>The architecture has three layers:</p>
<ul>
<li><strong>Core</strong>, with no OS APIs: framing, session handling, the CELT binding, jitter buffer and recorder. It is unit-tested against real captures.</li>
<li><strong>Platform layers</strong> per OS: RFCOMM, device discovery, audio output, and later camera and encoders.</li>
<li><strong>App heads</strong>: thin per-platform entry points. They hand the shared UI an <code>IAppHost</code> with the hub factory, recording folder, permission prompts and share/open actions.</li>
</ul>
<p>The first Windows build took minutes, not days, because Core was a direct port of the Python module that was already verified against hardware. The first CLI run on the real mic recorded 440 frames with 0 decode errors.</p>
<h3 id="native-code-was-the-hard-part">Native code was the hard part</h3>
<p>CELT is C, so every target needs its own binary. Two problems came out of that.</p>
<p>The first was loading. A Zig-built ARM64 Windows DLL failed to load with &ldquo;incorrect format&rdquo;, although its headers looked correct. Rebuilding with llvm-mingw fixed it, and Zig was dropped. Loading is now one resolver that knows each platform&rsquo;s layout:</p>
<pre><code class="language-csharp">private static IntPtr Resolve(string name, Assembly assembly, DllImportSearchPath? path)
{
    if (!Libs.TryGetValue(name, out var f))
    {
        return IntPtr.Zero;
    }
    if (OperatingSystem.IsAndroid())
    {
        return NativeLibrary.TryLoad(f.Unix, out var lib) ? lib : IntPtr.Zero;
    }
    if (OperatingSystem.IsIOS())
    {
        var framework = Path.Combine(AppContext.BaseDirectory, &#34;Frameworks&#34;, $&#34;{name}.framework&#34;, name);
        return NativeLibrary.TryLoad(framework, out var lib) ? lib : IntPtr.Zero;
    }
    var os = OperatingSystem.IsWindows() ? &#34;win&#34; : OperatingSystem.IsMacOS() ? &#34;osx&#34; : &#34;linux&#34;;
    // ... map ProcessArchitecture to arm64/x64/x86 ...
    var candidate = Path.Combine(AppContext.BaseDirectory, &#34;runtimes&#34;, $&#34;{os}-{arch}&#34;, &#34;native&#34;, file);
    if (NativeLibrary.TryLoad(candidate, out var handle))
    {
        return handle;
    }
    return NativeLibrary.TryLoad(name, assembly, path, out handle) ? handle : IntPtr.Zero;
}</code></pre>
<p>The second was thread safety, and it only showed up with several mics. Rendering store screenshots with several simulated mics crashed the process inside <code>celt_decode</code>. Without <code>-DVAR_ARRAYS</code>, CELT 0.7.1 falls back to one process-wide scratch stack, so two decoders on two threads corrupt each other. Every build script now passes the flag. A regression test that decodes the same capture on several threads at once crashed the test host on the old binary and passes on the new one.</p>
<h2 id="step-5-android">Step 5: Android</h2>
<p>The Android head went in after the shared UI was moved into a <code>UserControl</code> that both heads host. Bluetooth maps directly: <code>BluetoothSocket</code> to the same service UUID, with a fallback for stacks that refuse an authenticated link to an already-bonded device. CELT is built with NDK r29, aligned to 16 KB pages for Android 15+.</p>
<p>Audio-only recording worked on the first install. Video did not.</p>
<h3 id="eight-frames-all-at-timestamp-0">Eight frames, all at timestamp 0</h3>
<p>The first Android video take contained 8 video frames, all stamped 0 ms, next to 7.6 s of correct audio. The first guesses were a wrong clock mapping and a stalled encoder drain. A self-test that opened the camera on the phone and counted frames showed something else: the preview stopped after about five frames, and the encoder received none.</p>
<p>The cause was one line. The preview <code>ImageReader</code> image was wrapped in a C# <code>using</code>. In .NET for Android, <code>Dispose()</code> releases the managed peer but does not call the Java <code>Image.close()</code>. With <code>maxImages=2</code> the reader&rsquo;s buffers were never returned, the camera HAL timed out dequeuing new ones, and the whole repeating capture request stalled. That included the encoder&rsquo;s stream.</p>
<pre><code class="language-csharp">private void OnImage(ImageReader reader)
{
    var image = reader.AcquireLatestImage();
    if (image is null)
    {
        return;
    }
    try { ConvertPreview(image); }
    finally { image.Close(); } // Dispose() does not close the Java Image; unclosed images stall the whole capture session.
}</code></pre>
<p>After the fix the same self-test produced 89 frames in 3 s, 3 key frames, and monotonic timestamps.</p>
<h3 id="rotation-hints-are-not-reliable">Rotation hints are not reliable</h3>
<p>Portrait video played sideways. The recorder wrote the rotation as a container hint, and players handle the Matroska hint inconsistently. Our sign was also wrong. Fixing the sign was not enough, so the phone now draws each camera frame upright through OpenGL into the encoder&rsquo;s input surface, and the container hint is 0. One more trap: on the test phone, <code>SurfaceTexture.GetTransformMatrix()</code> already contained the sensor rotation, so applying it as well rotated the frames twice.</p>
<h2 id="step-6-build-and-release-pipeline">Step 6: Build and Release Pipeline</h2>
<p>The pipeline runs on a GitHub release. The tag version (<code>v1.2.3</code>) gives every store its build number: the Android version code, the Apple build number and the MSIX version.</p>
<p>Native libraries are cross-compiled on a single Linux runner:</p>
<ul>
<li>CELT and LAME are downloaded from pinned URLs and checked against pinned SHA-256 hashes.</li>
<li>llvm-mingw produces the Windows DLLs and the NDK produces the Android <code>.so</code> files.</li>
<li>The results are uploaded as artifacts and consumed by the platform jobs.</li>
</ul>
<p>There is no build matrix; the per-target loop lives in the same scripts developers run locally:</p>
<pre><code class="language-bash">for pair in aarch64:android-arm64 x86_64:android-x64; do
  arch=&#34;${pair%%:*}&#34;
  rid=&#34;${pair#*:}&#34;
  out=&#34;$here/runtimes/$rid/native&#34;
  mkdir -p &#34;$out&#34;
  (cd &#34;$src&#34; &amp;&amp; &#34;$clang&#34; --target=&#34;$arch-linux-android$api&#34; -shared -fPIC -O2 -DHAVE_LRINTF -DVAR_ARRAYS \
      -Wl,-z,max-page-size=16384 -Wl,-soname,libcelt071.so -Wno-everything \
      -o &#34;$out/libcelt071.so&#34; $files -lm)
  echo &#34;built $out/libcelt071.so&#34;
done</code></pre>
<p>A screenshot job renders the UI headlessly with simulated mics that replay a real captured session. A Python step frames those screenshots and builds every store asset. It also validates the listing text against store limits.</p>
<h3 id="what-the-first-real-releases-broke">What the first real releases broke</h3>
<ul>
<li><strong>iOS:</strong> the newest .NET iOS workload required an Xcode version the default macOS runner did not have. The fix was to move the job to a newer runner image and add an explicit Xcode selection step.</li>
<li><strong>Android:</strong> the release asked for an AAB, but only an APK came out. The project file set <code>AndroidPackageFormat</code> (singular) and CI passed <code>AndroidPackageFormats</code> (plural). The singular property won, so the CI override had no effect. The fix was renaming one property. A second fix was needed because the signed APK lands one directory above <code>publish/</code>, while the AAB lands inside it.</li>
<li><strong>Microsoft Store:</strong> the submission API has no image type for the portrait poster we generated, and the submission failed. The poster is still generated, but it is no longer uploaded.</li>
<li><strong>Signing:</strong> a missing keystore had produced a debug-signed build instead of an error. It is now a hard failure, and keystore passwords are passed as <code>file:</code> paths instead of command-line values.</li>
</ul>
<p>Google Play publishing uses no stored key. GitHub&rsquo;s OIDC token is exchanged through Google Workload Identity Federation for a short-lived service-account identity:</p>
<pre><code class="language-yaml">- name: Authenticate to Google Cloud
  uses: google-github-actions/auth@v3
  with:
    workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
    service_account: ${{ vars.GCP_PLAY_SERVICE_ACCOUNT }}
- name: Submit to Google Play
  run: &gt;
    python Tools/store/google_play.py --assets in/store-assets/android
    --package &#34;in/android/app-$VERSION-android.aab&#34; --build-number &#34;$BUILD&#34;
    --version &#34;$VERSION&#34; --track &#34;$TRACK&#34; --status &#34;$STATUS&#34;</code></pre>
<p>The submission script does everything in one Play &ldquo;edit&rdquo;: upload, check that the version code matches, update listings and images, set the track, validate and commit. If any step fails it deletes the edit, so a broken run never leaves a half-updated listing.</p>
<h2 id="step-7-remote-mode">Step 7: Remote Mode</h2>
<p>The requirement: any instance can control any other over Wi-Fi or Bluetooth, each device records its own audio and video locally, and when the take stops the controller collects everything into one package. Finding another device should be a simple search, regardless of transport.</p>
<h3 id="discovery">Discovery</h3>
<p>On Wi-Fi the searcher broadcasts a short query to the global broadcast address, a multicast group and each interface&rsquo;s subnet broadcast. It repeats the query, because single datagrams get lost on busy Wi-Fi:</p>
<pre><code class="language-csharp">async Task SendLoop()
{
    // Datagrams get lost on busy Wi-Fi: ask a few times.
    foreach (var delay in new[] { 0, 700, 1500, 3000 })
    {
        await Task.Delay(delay, ct).ConfigureAwait(false);
        foreach (var t in targets)
        {
            try { await udp.SendAsync(query, t, ct).ConfigureAwait(false); }
            catch (SocketException) { /* unreachable network */ }
        }
    }
}</code></pre>
<p>On Bluetooth each instance runs an RFCOMM service with its own UUID, and the searcher probes paired phones and computers. Results from both transports are merged by a per-install ID, and Wi-Fi is preferred. The link itself is a minimal frame format over any duplex stream: a 1-byte kind, a 4-byte length, and a JSON payload (raw bytes for file chunks). Header and payload go out in one write, because on Bluetooth each write can become its own packet. Control requires a consent prompt on the remote device.</p>
<h3 id="starting-at-the-same-instant">Starting at the same instant</h3>
<p>The controller estimates each remote&rsquo;s clock offset with an NTP-style exchange, and keeps the sample with the lowest round-trip time:</p>
<pre><code class="language-csharp">var offset = p.T2 - (p.T1 &#43; t3) / 2;
lock (_lock)
{
    _samples.Add((rtt, offset));
    // Keep ~2 minutes: during a file sync pings wait behind file data, and only the old, fast samples are accurate.
    if (_samples.Count &gt; 120)
    {
        _samples.RemoveAt(0);
    }
    var best = _samples.MinBy(s =&gt; s.Rtt);
    ClockOffset = best.Offset;
    ClockRtt = TimeSpan.FromTicks(best.Rtt);
}</code></pre>
<p>To start a take, the controller picks a start time 700 ms ahead (1,200 ms if any remote is on Bluetooth). It translates that time into each remote&rsquo;s clock and sends it. Each device sleeps until shortly before the instant and then spins until its own clock reaches it:</p>
<pre><code class="language-csharp">var wait = TimeSpan.FromTicks(startAt - MediaClock.Now);
if (wait &gt; TimeSpan.FromMilliseconds(20))
{
    await Task.Delay(wait - TimeSpan.FromMilliseconds(15));
}
while (MediaClock.Now &lt; startAt)
{
    Thread.Yield();
}
_hub.StartRecordingAll(RecordingRoot);</code></pre>
<p>Each remote reports its actual start, and the controller stores the residual offset and its uncertainty in the take manifest. Every audio track is already locked to the device&rsquo;s host clock, so an editor only has to shift each device by one constant.</p>
<h3 id="the-bug-behind-the-comment">The bug behind the comment</h3>
<p>The comment about pings waiting behind file data came from a real failure. After a long Bluetooth file sync, the manifest recorded an offset of 1.9 s ± 1.9 s. The pings sent during the transfer had queued behind 64 KiB file chunks on a link that moved only 55–80 KB/s. Their slow samples replaced the good one. Two changes fixed it: keep the best sample over about two minutes, and store the offset measured at Start instead of a later one. The retest gave 25 ms ± 4 ms.</p>
<p>Two other fixes came out of cross-platform testing:</p>
<ul>
<li>A mic name containing a colon was valid on Android and invalid as a Windows file name, so the transfer to Windows failed. All received and generated file names now go through one sanitiser.</li>
<li>A protocol level bump was nearly shipped in a way that would make older releases refuse the handshake. The wire version stays 1, and a separate <code>level</code> field announces newer messages such as pause. Old peers ignore the field and are flagged as not supporting pause.</li>
</ul>
<h2 id="runtime-impact">Runtime Impact</h2>
<ul>
<li>Decoding: 0 decode errors across every live capture, from the first 443 frames to multi-minute takes.</li>
<li>Tests: 10 tests on day one, about 350 later, run on win-arm64 and win-x64. They include bit-exact decode of a real capture and the multi-threaded CELT regression test.</li>
<li>Windows video: 1080p30 on the hardware H.264 encoder, about 10.5 Mbit/s against a 12 Mbit/s target. Recording starts in 24–40 ms with a pre-warmed encoder.</li>
<li>Remote over Wi-Fi: the peer is found in 100–161 ms, remotes start 3–19 ms from the scheduled instant, and a 5 s WAV transfers in under 0.4 s.</li>
<li>Remote over Bluetooth only: discovery takes 0.5–2.4 s, starts land within 12–25 ms (±4–6 ms clock uncertainty), and transfer is the bottleneck, at 55–80 KB/s.</li>
<li>Failure recovery: a Windows MP4 recorder killed at 8.0 s kept 8.05 s of video and 7.96 s of audio, and the file played.</li>
</ul>
<h2 id="final-takeaway">Final Takeaway</h2>
<p>The protocol work was mostly static. Matching the decompiled code against a public sample, and checking the encoder against packets the app hard-codes, gave a verified spec before the first Bluetooth connection. The expensive bugs came later and were all about state that crosses a boundary:</p>
<ul>
<li>clocks between devices;</li>
<li>buffers between Java and .NET;</li>
<li>a global scratch stack between threads;</li>
<li>an MSBuild property between a project file and CI;</li>
<li>a ping queue behind file data.</li>
</ul>
<p>Each one is now pinned by a clock-locked track, an explicit <code>Close()</code>, a compiler flag with a regression test, a single property name, and a best-sample offset estimate. Those are the class of failures this setup now prevents: silent drift, frozen capture, cross-thread corruption, missing release artifacts, and wrong alignment in multi-device takes.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to run a Remote Desktop Server (RDP) and a Domain server in one</title>
      <link>https://peterobel.com/blog/server-2012-domain-and-rdp/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/server-2012-domain-and-rdp/</guid>
      <pubDate>Thu, 22 Jan 2015 09:12:35 &#43;0000</pubDate>
      
      <description>The full article is in the works, lets on know in the comments field if we should hurry.</description>
      <content:encoded><![CDATA[<h3 id="just-because-you-can-it-is-still-not-recommended">Just because you can, it is still not recommended</h3>
<p>The full article is in the works, lets on know in the comments field if we should hurry.</p>
<p>The quick cheat sheet for success is the following steps</p>
<ul>
<li>Install the domain server on bare metal, and set it up</li>
<li>Install the Hyper-V role on the domain server</li>
<li>Install a new 2012 server in Hyper-V, which will run your RDP Licensing server</li>
<li>Install RDP licensing role to the virtual 2012 server<br>
Also install all RDP gateway services that you need:
<ul>
<li>Remote desktop Gateway</li>
<li>Remote desktop proxy</li>
<li>This is also a nice place to put VPN, etc.</li>
</ul>
</li>
</ul>
<p>For a little bit of security, you can strip the domain server of everything else, GUI, applications, etc.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to debug Windows Phone 8 apps when developing inside a Hyper-V virtual machine</title>
      <link>https://peterobel.com/blog/virtual-wp8-emulator/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/virtual-wp8-emulator/</guid>
      <pubDate>Thu, 22 Jan 2015 09:12:35 &#43;0000</pubDate>
      
      <description>The full article is in the works, lets on know in the comments field if we should hurry.</description>
      <content:encoded><![CDATA[<h3 id="hint-the-emulator-is-not-running-inside-the-virtual-machine">Hint: The emulator is not running inside the Virtual Machine</h3>
<p>The full article is in the works, lets on know in the comments field if we should hurry.</p>
<p>This requires a bit of coding, and the results might come up on this page</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Get Chrome to do High DPI Mode on Windows 8 (and 8.1)</title>
      <link>https://peterobel.com/blog/chrome-hidpi/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/chrome-hidpi/</guid>
      <pubDate>Thu, 05 Jun 2014 15:48:11 &#43;0000</pubDate>
      
      <description>The most recent version of Chrome needs the High DPI Mode to be enabled in the registry.</description>
      <content:encoded><![CDATA[<p>The most recent version of Chrome needs the High DPI Mode to be enabled in the registry.</p>
<p>This has been documented many places, among others:</p>
<ul>
<li><a href="https://groups.google.com/a/chromium.org/forum/#!topic/chromium-dev/U_K1NHuMLic">https://groups.google.com/a/chromium.org/forum/#!topic/chromium-dev/U_K1NHuMLic</a></li>
<li><a href="http://www.ubergizmo.com/how-to/how-to-google-chrome-hidpi-support-windows/">http://www.ubergizmo.com/how-to/how-to-google-chrome-hidpi-support-windows/</a></li>
</ul>
<p>However with the most recent dev version of chrome (Version 37+) you need a different (or to be safe two) registry keys. Since 64bit and 32bit windows applications can place their registry information in two places.</p>
<pre><code class="language-ini">[HKEY_CURRENT_USER\Software\Wow6432Node\Google\Chrome\Profile]</code></pre>
<p>and as previously</p>
<pre><code class="language-ini">[HKEY_CURRENT_USER\Software\Google\Chrome\Profile]</code></pre>
<p>In both these locations you should create a DWORD entry named &ldquo;high-dpi-support&rdquo; with the value of 1.</p>
<p>For convenience you can download the registry files here which will set the registry for you:</p>
<ul>
<li><a href="/downloads/chrome-hidpi/ChromeHIDPI.reg">ChromeHIDPI.reg</a></li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Making Dexpot compatible with full screen RDP and SecondMonitorToolbar</title>
      <link>https://peterobel.com/blog/rotate-cube-from-rdp/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/rotate-cube-from-rdp/</guid>
      <pubDate>Tue, 29 Jan 2013 14:32:12 &#43;0000</pubDate>
      
      <description>Running the excellent Dexpot window manager for windows, along with the opensource Dual Monitor Taskbar can cause a few issues:</description>
      <content:encoded><![CDATA[<p>Running the excellent Dexpot window manager for windows, along with the opensource Dual Monitor Taskbar can cause a few issues:</p>
<ol>
<li>It is not possible to switch virtual desktops on the client from within a remote desktop session</li>
<li>The second monitor taskbar is on-top of the remote desktop client application</li>
</ol>
<p>The remote desktop client for windows, allows ONLY the Caps-Lock key to be captured by other programs while in full screen, this is when the &ldquo;Send windows shortcuts to remote system&rdquo; is enabled.<br>
Using the autohotkey scripts below you are able to switch desktops using the Cube from Dexpot using Ctrl+CapsLock. The use case for the script is as follows:</p>
<ol>
<li>Press and hold CTRL</li>
<li>Press and RELEASE Caps-Lock</li>
<li>Use the arrow left/right to navigate available desktops</li>
<li>Release CTRL when you wish to switch to selected desktop</li>
</ol>
<p>The requirements for the setup described here, are the following applications:</p>
<ul>
<li><a href="http://dexpot.de/">Dexpot Window Manager with Cube plugin</a>
<ul>
<li>You must have mapped the Cube manual show to Alt + Win + Up Arrow</li>
</ul>
</li>
<li><a href="http://sourceforge.net/projects/dualmonitortb/">Dual Monitor Taskbar</a></li>
<li><a href="http://www.autohotkey.com/">Autohotkey</a></li>
</ul>
<p>The script can also be found, downloaded and forked at github:</p>
<pre><code>https://github.com/ecomerc/code/blob/master/RotateCube-from-RDP/rotatecube.ahk</code></pre>
<p>The code for the autohotkey is also outlined below. The implementation is:</p>
<ul>
<li>Capture the pressing of CTRL+Caps-Lock<br>
Block the capture of this (we don&rsquo;t want to enable Caps-Lock)</li>
<li>If a remote desktop client is full screen, restore it to window mode, and enlarge the window to fill the screen<br>
We do this so the cube is still covered with the RDP client</li>
<li>Send Ctrl+Win+Arrow Up to launch the Cube</li>
<li>Capture the release of Ctrl nonblocking (allow other applications to see the event as well)<br>
If a Remote Desktop Window is present on the new virtual desktop make it full screen, and make the second monitor toolbar not topmost<br>
If no Remote Desktop Window is present, make the second monitor toolbar topmost</li>
</ul>
<pre><code class="language-autohotkey">#SingleInstance force
; Usage: Press CTRL&#43;CapsLock, hold CTRL and use arrow keys to navigate desktops

RotatingCube := 0
^Capslock::
    SetKeyDelay -1 
	if (RotatingCube = 0) { 
		;Get MSTSC window (this may have to be ahk_class TSSHELLWND for some systems)
		;TODO: make multiple detection
		
		If WinActive(&#34;ahk_class TscShellContainerClass&#34;) {
			WinGet, active_id, ID, A
			WinGet, maxOrMin, MinMax, ahk_id %active_id%
			
			if (maxOrMin = 0) {
				WinGetPos, PosX, PosY, WinWidth, WinHeight, ahk_id %active_id%
				if (PosY = 0) {
					;Wait for focus to restore properly
					Sleep 50
					
					;Force the MSTSC windows to restore (unmaximize)
					PostMessage, 0x112, 0xF120,,, ahk_id %active_id%   ; 0x112 = WM_SYSCOMMAND, 0xF120 = SC_RESTORE
					Sleep 50
					
					;Move it so it fills the screen, but not 0,0 because then we would think it was maximized
					WinMove, ahk_id %active_id%,, 1, 1, WinWidth - 2, WinHeight-2
					
				}
			}
			
		}
		; Launch the Cube
		send, {LCtrl Down}{LWin Down}{Up DownTemp}
		RotatingCube := 1
	}
    return
	
~LCtrl up::
	if (RotatingCube &lt;&gt; 0) {
		send, {LWin Up}
		Sleep 50
		DetectHiddenWindows, off
		WinWait, ahk_class TscShellContainerClass, , 1
		if ErrorLevel {
			;;Did not find MSTSC
			
			;;Set SecondaryTaskbar as Always OnTop
			Winset, AlwaysOnTop, off, SecondaryTaskbar
		} else {
			;;Set SecondaryTaskbar as Not Always OnTop (it has a bug)
			Winset, AlwaysOnTop, off, SecondaryTaskbar
			
			;;Maximizing MSTSC
			WinActivate ahk_class TscShellContainerClass
			send !^{vk03sc146}
			
		}
	}
	RotatingCube := 0
    return	</code></pre>
]]></content:encoded>
    </item>
    
    <item>
      <title>GNU Syntax-Highlighter lang file for Autohotkey</title>
      <link>https://peterobel.com/blog/autohotkey-syntax-highlight/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/autohotkey-syntax-highlight/</guid>
      <pubDate>Tue, 29 Jan 2013 14:32:12 &#43;0000</pubDate>
      
      <description>The GNU Syntax-Highlighter project does not have support for Autohotkey, yet. We are running jQuery snippet on this site, and that uses converted .lang files made for SHJS.</description>
      <content:encoded><![CDATA[<p>The GNU Syntax-Highlighter project does not have support for Autohotkey, yet. We are running jQuery snippet on this site, and that uses converted .lang files made for SHJS.</p>
<p>The code is based completely on the Autohotkey language file for highligh 3.12 made by Andre Simon. The code can be downloaded, copied and forked on github.</p>
<pre><code>https://github.com/ecomerc/code/tree/master/Autohotkey-Syntax-Highlight</code></pre>
<p>Read more:</p>
<ul>
<li><a href="http://www.andre-simon.de/">http://www.andre-simon.de/</a></li>
<li><a href="http://www.gnu.org/software/src-highlite/">http://www.gnu.org/software/src-highlite/</a></li>
<li><a href="http://shjs.sourceforge.net/">http://shjs.sourceforge.net/</a></li>
<li><a href="https://web.archive.org/web/20131218000800/http://steamdev.com/snippet/">https://web.archive.org/web/20131218000800/http://steamdev.com/snippet/</a></li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to Compile MP3Gain on DD-WRT or other UNSLUG devices</title>
      <link>https://peterobel.com/blog/mp3gain/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/mp3gain/</guid>
      <pubDate>Tue, 29 Jan 2013 09:42:40 &#43;0000</pubDate>
      
      <description>It is a simple procedure, with one caveat. It requires you to have Optware, and because of space requirements it is HIGHLY recommended to have optware on a seperate disk, NOT JFFS.</description>
      <content:encoded><![CDATA[<p>It is a simple procedure, with one caveat. It requires you to have Optware, and because of space requirements it is HIGHLY recommended to have optware on a seperate disk, NOT JFFS.</p>
<ol>
<li>
<p>Install the build toolchain:</p>
<pre><code class="language-sh">ipkg install buildroot</code></pre>
<p>or</p>
<pre><code class="language-sh">ipkg install optware-devel</code></pre>
<p>and</p>
<pre><code class="language-sh">ipkg install unzip</code></pre>
</li>
<li>
<p>Download the MP3 Gain sources, pick the latest from <a href="http://sourceforge.net/projects/mp3gain/files/mp3gain/">http://sourceforge.net/projects/mp3gain/files/mp3gain/</a>.</p>
<pre><code>wget http://sourceforge.com/...</code></pre>
</li>
<li>
<p>CD to the directory and unzip the source code</p>
</li>
<li>
<p>Make the program</p>
<pre><code class="language-sh">make</code></pre>
<p>if you run into the error:</p>
<pre><code class="language-sh">cc -Wall -DHAVE_MEMCPY   -c -o mp3gain.o mp3gain.c
cc: can&#39;t resolve symbol &#39;__fsetlocking&#39;
make: *** [mp3gain.o] Error 1</code></pre>
<p>you need to point the LD_LIBRARY_PATH to your optware lib:</p>
<pre><code class="language-sh">export LD_LIBRARY_PATH=/opt/lib:$LD_LIBRARY_PATH</code></pre>
</li>
<li>
<p>I have not gotten make install working, but it should be enough to copy mp3gain into /opt/bin</p>
</li>
</ol>
]]></content:encoded>
    </item>
    
    <item>
      <title>Loading External XML Files on Microsoft SQL Server</title>
      <link>https://peterobel.com/blog/external-xml/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/external-xml/</guid>
      <pubDate>Thu, 09 Aug 2012 16:25:29 &#43;0000</pubDate>
      
      <description>Error handling and check for unauthorized remote files are completely up to the user.</description>
      <content:encoded><![CDATA[<pre><code class="language-sql">USE [PFMP]
GO

/****** Object:  UserDefinedFunction [dbo].[af_LoadRemoteFile]    Script Date: 03/10/2011 13:13:15 ******/
ALTER FUNCTION [dbo].[af_LoadRemoteFile](@url [nvarchar](255))
RETURNS [nvarchar](max) WITH EXECUTE AS CALLER
AS 
EXTERNAL NAME [LoadFile].[LoadFile].[LoadRemoteFile]
GO</code></pre>
<pre><code class="language-sql">DECLARE @xml xml
SELECT @xml=REPLACE(dbo.af_LoadRemoteFile(&#39;http://rss.cnn.com/rss/edition.rss&#39;), &#39;utf-8&#39; ,&#39;utf-16&#39;)</code></pre>
<pre><code class="language-csharp">using System;
using System.Data;
using System.Data.Sql;
using System.Data.SqlTypes;
using System.Data.SqlServer;
using Microsoft.SqlServer.Server;

public class StoredProcedures
{

    [SqlFunction(IsDeterministic = true, IsPrecise = true)]
    public static SqlString LoadRemoteFile(string remoteFile)
    {
		System.Net.WebClient remote = new System.Net.WebClient();
		byte[] myDataBuffer = remote.DownloadData (remoteFile);
		string download = System.Text.Encoding.ASCII.GetString(myDataBuffer);

        return download;
    }


};</code></pre>
<p>Error handling and check for unauthorized remote files are completely up to the user.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Launching a Shared Access Application from a Local Copy</title>
      <link>https://peterobel.com/blog/access-remote-start/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/access-remote-start/</guid>
      <pubDate>Thu, 09 Aug 2012 16:25:29 &#43;0000</pubDate>
      
      <description>This article is a reconstruction from a small batch launcher and a shortcut script. The original article file was empty. The date above is the surviving scripts’ SVN commit date, not a verified first publication date. The application names and paths below are generic.</description>
      <content:encoded><![CDATA[<h2 id="what-the-launcher-was-probably-for">What the launcher was probably for</h2>
<p>This article is a reconstruction from a small batch launcher and a shortcut
script. The original article file was empty. The date above is the surviving
scripts&rsquo; SVN commit date, not a verified first publication date. The application
names and paths below are generic.</p>
<p>The likely goal was to distribute an Access application from one shared location
without making everyone open the same front-end file over the network.</p>
<p>Each user would launch a small updater. It would copy the current client into a
directory under that user&rsquo;s application-data folder, then open the local copy.
Publishing an updated client to the share would make it available on the next
launch, without a separate installation on each workstation.</p>
<p>Here, &ldquo;remote start&rdquo; appears to mean starting an application distributed from a
remote file share. The scripts do not start a process on another computer.</p>
<h2 id="one-shared-release-one-local-copy-per-user">One shared release, one local copy per user</h2>
<p>The setup script supplies a few application-specific values to a reusable
launcher. A sanitized version of that configuration looks like this:</p>
<pre><code class="language-bat">@echo off
set &#34;name=Shared Application&#34;
set &#34;root=\\fileserver\applications\shared-app&#34;
set &#34;client=%root%\Client&#34;
set &#34;destination=%APPDATA%\SharedApplication&#34;
set &#34;batfile=%client%\%~nx0%&#34;
set &#34;icon=icon.ico&#34;

call &#34;%~dp0run.bat&#34;</code></pre>
<p>This is only the configuration wrapper, not a complete replacement updater.</p>
<p>The distinction between <code>client</code> and <code>destination</code> is the important part.
The share contains the distributable client; the user profile contains a
disposable working copy. There is no need for users to write into the release
directory, and one user&rsquo;s local front end is separate from another&rsquo;s.</p>
<h2 id="what-the-surviving-implementation-does">What the surviving implementation does</h2>
<p>The generic launcher follows this sequence:</p>
<ol>
<li>Create the local destination directory.</li>
<li>List <code>.adp</code> client files on the share in modification-date order and retain the
final filename, apparently intending to select the newest release.</li>
<li>Delete the files directly inside the local destination, then copy the selected
client and icon files from the share.</li>
<li>Write a per-user trusted-location registry entry for the local client folder.</li>
<li>Write the shared root path into a local <code>lnk.txt</code> file.</li>
<li>Locate an installed Access executable using a short list of conventional
installation paths.</li>
<li>Create or update a desktop shortcut that points back to the shared launcher.</li>
<li>Start Access with the local client file.</li>
</ol>
<p>The <code>.adp</code> extension identifies a legacy Access project rather than an ordinary
file-based database. Such a client would typically connect to a database server,
but these scripts do not establish the actual backend configuration.</p>
<p>The <code>lnk.txt</code> file probably let the client discover the shared application root.
The launcher only writes the file; no surviving application code here proves
how it was consumed.</p>
<h2 id="why-the-shortcut-points-to-the-launcher">Why the shortcut points to the launcher</h2>
<p>The shortcut helper accepts a display name, target path, and icon path. It uses
the user&rsquo;s desktop folder to create the shortcut, with the shared batch file as
its target and the copied local icon as its icon.</p>
<p>That target is deliberate. If the shortcut opened the local client directly,
later launches would bypass the update step. Returning to the shared launcher
keeps release selection in one place.</p>
<p>The trade-off is dependence on the share: a local client may exist, but this
shortcut still requires the shared launcher to be reachable.</p>
<h2 id="where-the-original-approach-is-fragile">Where the original approach is fragile</h2>
<p>The mechanism is small, but the original implementation is not a robust updater:</p>
<ul>
<li>It deletes the old local files before checking that the new copy succeeded.
A lost connection can leave the user with no usable client.</li>
<li>Its filename-selection loop does not preserve names containing spaces. There
is also no explicit handling for an empty release directory.</li>
<li>It has no release manifest, signature verification, rollback, or check for an
already-running client.</li>
<li>Executable discovery assumes particular installation directories, including
a final fallback that is not checked for existence.</li>
<li>The trusted-location entry targets one Access version even though executable
discovery considers several versions.</li>
</ul>
<p>The local destination must therefore be dedicated to replaceable application
files, never user documents or the authoritative database. A replacement should
stage and validate an update before replacing the working copy, handle failures
explicitly, and keep the previous client available for recovery.</p>
<p>Trusting the local folder also makes the release share a security boundary.
Only authorized publishers should be able to change the files that users will
copy and execute. Broadening trusted locations is not a substitute for that
control.</p>
<h2 id="takeaway">Takeaway</h2>
<p>The best reading of these scripts is a lightweight deployment scheme for a
shared Access application: publish centrally, copy locally, and always enter
through the updater. The useful idea is separating distribution from execution.
The destructive replacement step and unchecked assumptions are historical
limitations, not recommendations to reproduce unchanged.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Keep control of the Office Ribbon</title>
      <link>https://peterobel.com/blog/vba-ribbon/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/vba-ribbon/</guid>
      <pubDate>Thu, 09 Aug 2012 16:25:29 &#43;0000</pubDate>
      
      <description>Description</description>
      <content:encoded><![CDATA[<p>Description</p>
<h2 id="object-reference">Object Reference</h2>
<p>The Microsoft Office Ribbon is referenced as an IRibbonUI object. The reference is retrieved by implementing:</p>
<pre><code class="language-vb">Public gobjRibbon As IRibbonUI

Sub CallbackOnLoad(ribbon As IRibbonUI)
    Set gobjRibbon = ribbon
End Sub</code></pre>
<p>Unfortunatly the global variable gobjRibbon is only initialized once, and is lost after errors or during a reset from the VBA-IDE.</p>
<p>A user &ldquo;WernerGg&rdquo; has published a brilliant solution for Excel, based on the work of user rorya (Rory) from the MrExcel board:
<a href="http://www.mrexcel.com/forum/showthread.php?t=518629">http://www.mrexcel.com/forum/showthread.php?t=518629</a> and <a href="https://web.archive.org/web/20120707125013/http://www.thecodecage.com/forumz/top-tips/208306-excel-store-regain-iribbonui-object-reference.html">https://web.archive.org/web/20120707125013/http://www.thecodecage.com/forumz/top-tips/208306-excel-store-regain-iribbonui-object-reference.html</a>.</p>
<blockquote>
<h2 id="how-to-preserve-or-regain-the-id-of-my-custom-ribbon-ui">How to preserve or regain the Id of my custom ribbon UI?</h2>
<p>The problem is that the UI is initialized only once during load. To change enabled-state or visibility, controls must be forced to reinitialize. For that we have to store the UIs Id during load by means of the onLoad-callback.</p>
<p>But this Id can only be stored in a static variable (Private guiRibbon As IRibbonUI), which gets lost after errors or during reset from the VBA-IDE.</p>
<p>We have implemented code to store the guiRibbon at a save place (a defined name &ldquo;thisWorkbook_IRibbonUI_Ptr&rdquo; on cell H2 in this example) from where it can be retrieved when the static variable is lost. See module ObjectStore. Basically it does:</p>
<pre><code class="language-vb">Sub StoreObjRef(obj As Object)           
&#39; Store an object reference              
    Dim longObj As Long                  
    longObj = ObjPtr(obj)                
    Range(&#34;...&#34;) = longObj               
End Sub          
                        
Function RetrieveObjRef() As Object
&#39; Retrieve the object reference
   Dim longObj As Long, obj as Object
   longObj = Range(&#34;...&#34;)
   CopyMemory obj, longObj, 4
 Set RetrieveObjRef = obj
End Function
										 
&#39; where CopyMem is a Windows kernel function:
Private Declare Sub CopyMemory Lib &#34;kernel32&#34; _
    Alias &#34;RtlMoveMemory&#34; (destination As Any, source As Any, ByVal length As Long)</code></pre>
<p><em>For the entire sample and description please see the file <a href="/downloads/vba-ribbon/TestRibbonUI_3.xlsm">TestRibbonUI_3.xlsm</a> by WernerG</em></p>
</blockquote>
<h2 id="persistant-storage">Persistant Storage</h2>
<p>The method of storing values in named ranges or worksheet cells only works with Excel, for the additional Office Applications we must find another storage location.</p>
<p>On the website <a href="http://www.cpearson.com/excel/trulyglobalvariables.htm">http://www.cpearson.com/excel/trulyglobalvariables.htm</a>, Chip Pearson describes how to create truly global variables in Excel. If we combine this with the sample from WernerG, we can place the Ribbon pointer in a truly global persistant variable.</p>
<p>First we implement object storage in the truly global variable module.</p>
<pre><code class="language-vb">Public Const C_OBJ_STORAGENAME As String = &#34;AC_RIBBON_PTR&#34;

Private Declare Sub CopyMemory Lib &#34;kernel32&#34; _
    Alias &#34;RtlMoveMemory&#34; (destination As Any, source As Any, ByVal length As Long)

Public Function StoreObjRef(obj As Object) As Boolean
	&#39; Serialize and savely store an object reference
    StoreObjRef = False
    
    &#39; Serialize
    Dim longObj As Long
    longObj = ObjPtr(obj)
    
    &#39; Store into a defined name
    If Not PropertyExists(C_OBJ_STORAGENAME) Then
        SetProperty C_OBJ_STORAGENAME, longObj
        Debug.Print &#34;Save storage &#34;&#34;&#34;; C_OBJ_STORAGENAME; &#34;&#34;&#34; stored the object reference&#34;; longObj
		StoreObjRef = True
    Else
        Debug.Print &#34;Property exists&#34;
    End If
    
    &#39; Return
End Function

Public Function RetrieveObjRef() As Object
&#39; Retrieve from save storage, deserialize and return the object reference
&#39; stored with StoreObjRef

    Set RetrieveObjRef = Nothing
    
    &#39; Retrieve from a defined name
    Dim longObj As Long
    If PropertyExists(C_OBJ_STORAGENAME) Then
        If GetProperty(C_OBJ_STORAGENAME, longObj) Then
            Debug.Print &#34;Object reference&#34;; longObj; &#34;retrieved from save storage &#34;&#34;&#34;; C_OBJ_STORAGENAME; &#34;&#34;&#34;&#34;
            
            &#39; Deserialize
            Dim obj As Object
            CopyMemory obj, longObj, 4
            
            &#39; Return
            Set RetrieveObjRef = obj
            Set obj = Nothing
        Else
            Debug.Print &#34;object not retrieved&#34;
        End If
    Else
        Debug.Print &#34;object does not exist&#34;
    End If
End Function</code></pre>
<p>Then we add the call to store the reference:</p>
<pre><code class="language-vb">Public gobjRibbon As IRibbonUI

Sub CallbackOnLoad(ribbon As IRibbonUI)
    Set gobjRibbon = ribbon
    Storage.StoreObjRef ribbon
End Sub</code></pre>
<p>Additionally we add a function to retrieve the object again, for performance issues we check to see if we still have the &ldquo;local&rdquo; copy first.</p>
<pre><code class="language-vb">Function getRibbonObj() as object
    If gobjRibbon Is Nothing Then
        Set gobjRibbon = Storage.RetrieveObjRef
    End If
	set getRibbonObj = gobjRibbon
End Function</code></pre>
<p>However for use in other Office Applications the code will need a little modification. In his original file Chip has defaulted the DestHWnd to:</p>
<pre><code class="language-vb">    If HWnd &lt;= 0 Then
        DestHWnd = FindWindow(&#34;XLMAIN&#34;, Application.Caption)
    Else
        DestHWnd = HWnd
    End If</code></pre>
<p>However this is not terribly dynamic and we have changed it to:</p>
<pre><code class="language-vb">    If HWnd &lt;= 0 Then
        DestHWnd = GetDefaultHandle()
    Else
        DestHWnd = HWnd
    End If</code></pre>
<p>Additionally the GetDefaultHandle() was introduced. Obviously this should be expanded to include the main windows of the remaining Office Applications</p>
<pre><code class="language-vb">Private Function GetDefaultHandle() As Long

    If Application.name = &#34;Microsoft Access&#34; Then
        GetDefaultHandle = Application.hWndAccessApp
    ElseIf Application.name = &#34;Microsoft Excel&#34; Then
        GetDefaultHandle = FindWindow(&#34;XLMAIN&#34;, Application.Caption)
    Else
        MsgBox &#34;Undefined Application&#34;
    End If

End Function</code></pre>
<p>The code consists of two files:</p>
<ul>
<li><a href="/downloads/vba-ribbon/Storage.bas">Storage.bas</a></li>
<li><a href="/downloads/vba-ribbon/CPropType.cls">CPropType.cls</a></li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to quickly list all excel sheets in all open workbooks</title>
      <link>https://peterobel.com/blog/all-excel-sheets/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/all-excel-sheets/</guid>
      <pubDate>Thu, 09 Aug 2012 16:25:29 &#43;0000</pubDate>
      
      <description>A quick list of all the worksheets in all the open workbooks can be obtained like this.</description>
      <content:encoded><![CDATA[<p>A quick list of all the worksheets in all the open workbooks can be obtained like this.</p>
<pre><code class="language-vb">Sub SheetNames()
    Dim wb As Workbook
    Dim ws As Worksheet

    For Each wb In Application.Workbooks
        
        Debug.Print &#34;-------------&#34;
    
        Debug.Print wb.Name
    
        For Each ws In wb.Worksheets
            Debug.Print &#34; ;&#34; &amp; ws.Name
        Next
    Next
End Sub</code></pre>
<p>The &ldquo;;&rdquo; was added for quick text import where the workbook name was not in the same column as the worksheet name.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to force updates from linked directories to SkyDrive, with Microsoft SkyDrive for Windows</title>
      <link>https://peterobel.com/blog/skydrive-force-sync/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/skydrive-force-sync/</guid>
      <pubDate>Thu, 09 Aug 2012 16:25:29 &#43;0000</pubDate>
      
      <description>When you use symlinks to add directories outside your SkyDrive folder on Windows, SkyDrive will not always detect changes in these directories. This is especially noticeable if the linked folders reside on a network location.</description>
      <content:encoded><![CDATA[<p>When you use symlinks to add directories outside your SkyDrive folder on Windows, SkyDrive will not always detect changes in these directories. This is especially noticeable if the linked folders reside on a network location.</p>
<p>Just for good measure we will just show how to create the symlinks to network drives. And remember <strong>SkyDrive must not be running when creating the symlinks</strong> otherwise SkyDrive will upload them as a file, and then crash. Which can only be fixed with a reinstall.</p>
<p>This has also been an issue with the DropBox client, and here is a <a href="https://web.archive.org/web/20130629162708/http://www.dropboxwiki.com:80/Sync_Other_Folders">Good article on how this issue is handled with the DropBox client</a>. However with networked folders, you are not able to symlink from the remote location into the SkyDrive folder. In order to link a remote location to your SkyDrive, do the following:</p>
<pre><code class="language-bat">taskkill.exe /IM skydrive.exe
mklink /D &#34;Link name&#34; &#34;\\remote.server\Share\Folder&#34;</code></pre>
<p>Once the link has been made, you can restart the SkyDrive application.</p>
<h2 id="forced-syncronization">Forced Syncronization</h2>
<p>The forced syncronization is a simple procedure, that will restart the SkyDrive application. It will kill with taskkill, and start it again with the \background paramter. It is possible to force the sync simply by running SkyDrive.exe \background, however this will also bring up an explorer view of the SkyDrive folder.</p>
<pre><code class="language-bat">@echo off

set tasklist=%windir%\System32\tasklist.exe
set taskkill=%windir%\System32\taskkill.exe
set skydrive=%AppData%\..\Local\Microsoft\SkyDrive\SkyDrive.exe
set wasStopped=0
set skydriveexe=skydrive.exe

set procFound=0
set notFound_result=ERROR:
      set procName=%skydriveexe%
for /f &#34;usebackq&#34; %%A in (`%taskkill% /IM %procName%`) do (
    if NOT %%A==%notFound_result% (set procFound=1)
)
if %procFound%==0 (
  echo The process was not running.
  goto :STARTSKYDRIVE
)
set wasStopped=1
set ignore_result=INFO:
:CHECKDEAD
&#34;%windir%\system32\timeout.exe&#34; 3 /NOBREAK
for /f &#34;usebackq&#34; %%A in (`%tasklist% /nh /fi &#34;imagename eq %procName%&#34;`) do (
    if not %%A==%ignore_result% (goto :CHECKDEAD)
)

:STARTSKYDRIVE
start &#34;skydrive&#34; &#34;%skydrive%&#34; /background</code></pre>
<p>This code is based on sample code from <a href="http://stackoverflow.com/questions/7861683/from-a-batch-file-how-can-i-wait-for-a-process-to-exit-after-calling-taskkill">http://stackoverflow.com/questions/7861683/from-a-batch-file-how-can-i-wait-for-a-process-to-exit-after-calling-taskkill</a>.</p>
<p>Unfortunately when running this from the task scheduler, it will bring up a command promt, so in order to hide the command line, we use the following vbs code, and call that from the task scheduler instead:</p>
<pre><code> Set WshShell = WScript.CreateObject(&#34;WScript.Shell&#34;)
 currentDirectory = Replace(WScript.ScriptFullName,WScript.ScriptName,&#34;&#34;)
 cmd = &#34;cmd.exe /c &#34;&#34;&#34;&amp; currentDirectory &amp;&#34;SkyDriveForceSync.bat&#34;&#34;&#34;
 Return = WshShell.Run(cmd, 0, True)
 set WshShell = Nothing</code></pre>
<p>This code is based on the sample from <a href="https://web.archive.org/web/20141025231231/http://www.pcreview.co.uk:80/forums/hide-console-window-run-task-scheduler-t1468863.html">https://web.archive.org/web/20141025231231/http://www.pcreview.co.uk:80/forums/hide-console-window-run-task-scheduler-t1468863.html</a></p>
<h2 id="task-scheduling">Task Scheduling</h2>
<p>In order to have SkyDrive force a sync regularly, setup task scheduler to start the vbs script once a day, and have it repeat every hour after that for one day.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to create an Access subform for addition and viewing only</title>
      <link>https://peterobel.com/blog/access-subform-add-and-view/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/access-subform-add-and-view/</guid>
      <pubDate>Thu, 09 Aug 2012 16:25:29 &#43;0000</pubDate>
      
      <description>Facing the problem of having an access subform being used for viewing and additions only were solved the following way:</description>
      <content:encoded><![CDATA[<p>Facing the problem of having an access subform being used for viewing and additions only were solved the following way:</p>
<pre><code class="language-vb">Private Sub subfrm_Enter()
    Me.subfrm.Form.DataEntry = True
End Sub

Private Sub subfrm_Exit(Cancel As Integer)
    Me.subfrm.Form.DataEntry = False
End Sub</code></pre>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to convert m4a files to mp3</title>
      <link>https://peterobel.com/blog/itunes-convert/</link>
      <guid isPermaLink="true">https://peterobel.com/blog/itunes-convert/</guid>
      <pubDate>Thu, 09 Aug 2012 16:25:29 &#43;0000</pubDate>
      
      <description>Features:</description>
      <content:encoded><![CDATA[<p>Features:</p>
<ul>
<li>Should run on a synology ds209</li>
<li>Should transfer as many tags as possible</li>
<li>Create a &ldquo;playlist&rdquo; folder with symbolic links to the mp3 files</li>
</ul>
<p>Prerequisits</p>
<ul>
<li>Working SSH connection to the synology</li>
<li><a href="https://web.archive.org/web/20140216232649/http://forum.synology.com:80/wiki/index.php/Overview_on_modifying_the_Synology_Server,_bootstrap,_ipkg_etc#Installing_compiled.2Fbinary_programs_using_ipkg">optware</a></li>
<li>faad - ipkg install faad</li>
<li>faad - ipkg install mp3info</li>
</ul>
<pre><code class="language-sh">#!/bin/sh


#the iTunes dir is located out of the music shared folder
# because we don&#39;t want duplicates in synology.

inputdir=&#34;/volume2/media/iTunes/iTunes Media/Music&#34;
iTunesdir=&#34;/volume2/media/iTunes/&#34;

outputdir=&#34;/volume2/music/iLibrary&#34;
linkdir=&#34;/volume2/media/playlists/iPod&#34;

#When done testing set it to /dev/null
logfile=&#34;/volume1/@scripts/log/iTunes.log&#34;


#Remove all &#34;playlist&#34; links
rm $linkdir/* 


    
export IFS=$&#39;\n&#39;

playlist=$(cat $iTunesdir/iTunes\ Library.xml | grep -o iTunes%20Media.*\.m4a)

playlist=$(printf $(echo -n $playlist | sed &#39;s/\\/\\\\/g;s/\(%\)\([0-9a-fA-F][0-9a-fA-F]\)/\\x\2/g&#39;))


for i in $(find $inputdir -name &#34;*.m4a&#34; -type f)
do
    inputpath=${i#$inputdir}
    filename=$(basename $i)
    
    
    info=$(faad -i &#34;$i&#34; 2&gt;&amp;1)
    
    
    trackcount=$(echo &#34;$info&#34; | grep -c &#39;track:&#39;)
    
    if [ $trackcount -eq 1 ] ; then
        
        info=$(echo &#34;$info&#34; | sed -e /^iTunNORM:.*$/d)
        info=$(echo &#34;$info&#34; | sed -e /^iTunSMPB:.*$/d)
        
        startline=$(echo &#34;$info&#34; | grep -m 1 -n &#39;title:\|unknown:&#39;  | cut -f1 -d:)
        trackline=$(echo &#34;$info&#34; | grep -m 1 -n &#39;track:&#39;  | cut -f1 -d:)
        
        linecount=$(( $trackline - $startline ))
                
        dateline=$( echo &#34;$info&#34; | grep -m 1 -n &#39;unknown: [0-9][0-9][0-9][0-9]&#39;  | cut -f1 -d:)
        info=$(echo &#34;$info&#34; | sed -e &#34;${dateline}s/unknown: /date: /&#34;)
        
        remixline=$(echo &#34;$info&#34; | sed -e &#34;1,${trackline}d&#34; | grep -m 1 -n &#39;unknown:&#39; | cut -f1 -d:)
        if [ &#34;$remixline&#34; -ne &#34;&#34; ] ; then
            remixline=$(( $trackline &#43; $remixline ))
            info=$(echo &#34;$info&#34; | sed -e &#34;${remixline}s/unknown: /remix: /&#34;)
        fi
        
        line=$(( $startline ))
        info=$(echo &#34;$info&#34; | sed -e &#34;${line}s/unknown: /title: /&#34;)
        
        
        line=$(( $line &#43; 1 ))
        info=$(echo &#34;$info&#34; | sed -e &#34;${line}s/unknown: /artist: /&#34;)
        
        
        
        if [ $linecount -eq 3 ] ; then
            line=$(( $line &#43; 1 ))
            info=$(echo &#34;$info&#34; | sed -e &#34;${line}s/unknown: /album: /&#34;)
        fi
        
        if [ $linecount -gt 4 ] ; then
            line=$(( $line &#43; 1 ))
            info=$(echo &#34;$info&#34; | sed -e &#34;${line}s/unknown: /albumartist: /&#34;)
        fi
        if [ $linecount -gt 5 ] ; then
            line=$(( $line &#43; 1 ))
            info=$(echo &#34;$info&#34; | sed -e &#34;${line}s/unknown: /composer: /&#34;)
        fi
        
        if [ $linecount -gt 3 ] ; then
            line=$(( $line &#43; 1 ))
            info=$(echo &#34;$info&#34; | sed -e &#34;${line}s/unknown: /album: /&#34;)
            line=$(( $line &#43; 1 ))
            info=$(echo &#34;$info&#34; | sed -e &#34;${line}s/unknown: /genre: /&#34;)
        fi
        
               
        
        song=$(echo &#34;$info&#34; | grep ^title: | sed s/title:\ //g)
        artist=`echo &#34;$info&#34; | grep ^artist: | sed s/artist:\ //g`
        composer=$(echo &#34;$info&#34; | grep ^composer: | sed s/composer:\ //g)
        albumartist=`echo &#34;$info&#34; | grep ^albumartist: | sed s/albumartist:\ //g`
        album=`echo &#34;$info&#34; | grep ^album: | sed s/album:\ //g`
        genre=$(echo &#34;$info&#34; | grep ^genre: | sed s/genre:\ //g)
        track=`echo &#34;$info&#34; | grep ^track: | sed s/track:\ //g`
        year=`echo &#34;$info&#34; | grep ^date: | sed s/date:\ //g`
        remix=`echo &#34;$info&#34; | grep ^remix: | sed s/remix:\ //g`
        
        
        #Sometimes iTunes adds the remix information to the song title
        # and sometimes not, so lets check and add if it is not there
        if [ &#34;$remix&#34; != &#34;&#34; ]
        then
            if [ &#34;${song%$remix}&#34; == &#34;$song&#34; ]
            then
                song=&#34;$song $remix&#34; 
            fi
        fi
        
        # The iTunes genres are not the same as MP3 genres, and they are 
        # not consistent either
        # There is not such MP3 genre as Hip-Hop/Rap or Hip Hop/Rap
        # but there is both Hip-Hop, and Rap - so choose your favorite
        # notice that if we do not check and then use $genre as a folder, 
        # we would have unexpected subdirs
        genreorg=$genre
        case &#34;$genre&#34; in
            &#34;Hip-Hop/Rap&#34;)
                genre=&#34;Hip-Hop&#34;
                ;;
            &#34;Hip Hop/Rap&#34;)
                genre=&#34;Hip-Hop&#34;
                ;;
            &#34;R&amp;B/Soul&#34;)
                genre=&#34;R&amp;B&#34;
                ;;
            &#34;Singer/Songwriter&#34;)
                genre=&#34;Pop&#34;
                ;;
            &#34;Indie Rock&#34;)
                genre=&#34;Indie&#34;
                ;;
            &#34;Pop/Rock&#34;)
                genre=&#34;Rock&#34;
                ;;
             
            #*)  
        esac


        
        # Make sure the genre does not contain illegal characters (especially / or \
        gpath=${genre//[^a-zA-Z0-9-&amp;]/ }
        
        #Order the output after genre (or what every you want here)
        # feel free to go nuts with the tags identified above
        # below I use iTunes folder arrangement, but add genre
        # so it is actually: genre/artist/album/tracknumber - song.mp3
        
        outputfile=$outputdir/$gpath${inputpath%.m4a}.mp3
        
        #I want the link name only to be the artist and the song 
        # (when transfered to a usb stick and used in lesser than capable 
        #  mp3 players you still have the most important information)
        
        linkname=&#34;$artist - $song&#34;
        
        #Strip illegal characters from the filename (needed if files are to be used
        # in Windows, and makes sure random new dirs is not created
        
        linkname=$(echo $linkname | sed &#39;s/[][&#39;&#34;&#39;&#34;&#39;:;*?&lt;&gt;|&#34;[:cntrl:]\\\/]//g&#39;)
        linkfile=$linkdir/$linkname.mp3
           
        
        if [ ! -e &#34;$outputfile&#34; ]
        then
            echo Converting: $filename 
        
        
            mkdir -p ${outputfile%/*}
            #--ignore-tag-errors --tv &#34;TXXX=ALBUM ARTIST=String1&#34;
            # --tv &#34;TCOM=$composer&#34;
            faad -o - &#34;$i&#34;| lame -h -b 128 - &#34;$outputfile&#34; --tt &#34;$song&#34; --ty &#34;$year&#34; --tn &#34;$track&#34; --ta &#34;$artist&#34; --tg &#34;$genre&#34; --tl &#34;$album&#34; --ignore-tag-errors --tv &#34;TPE2=$albumartist&#34; --tv &#34;TCOM=$composer&#34;
        fi
        
        #Used as an additional check, to set mp3 tags for previously converted m4a files or to add tags that lame does not add correctly (is not needed anymore since lame supports --tv
        
        #if [ -e &#34;$outputfile&#34; ]
        #then
        #    echo Editing: $outputfile 
        #    mp3info -t &#34;$song&#34; -a &#34;$artist&#34; -l &#34;$album&#34; -n &#34;$track&#34; -g &#34;$genre&#34; $outputfile 
        #fi
        
        
        res=`echo $playlist | grep -i -F &#34;$filename&#34;`; 
        if [[ ${#res} &gt; 0 ]]; then
            #This file is part of the playlist, so add it to the playlist directory
            ln -s &#34;$outputfile&#34; &#34;$linkfile&#34;
        fi 
        
    fi
done

chmod -R 777 &#34;$outputdir&#34;</code></pre>
<p>The original work was based on several m4a2mp3 scripts on the interwebs. However the code was completely rewritten from scratch since iTunes (or faad) are not that good at giving proper tag names. The scripts that provided helpful inspiration were:</p>
<ul>
<li><a href="https://web.archive.org/web/20120321071346/http://www.minigeek.org/2007/07/linux-m4a-to-mp3-conversion-script">https://web.archive.org/web/20120321071346/http://www.minigeek.org/2007/07/linux-m4a-to-mp3-conversion-script</a></li>
<li><a href="https://web.archive.org/web/20120508133653/http://gnuru.org:80/article/1582/convert-m4a-mp3-script">https://web.archive.org/web/20120508133653/http://gnuru.org:80/article/1582/convert-m4a-mp3-script</a></li>
</ul>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
