[{"content":"(the post is automatically translated by AI)\nBackground I\u0026rsquo;ve been learning ROS, but I don\u0026rsquo;t have a robot at hand. My idea: use the iPad I already have as a ROS node and receive data on my Mac — sensor data like IMU, Camera, Battery, etc. — to understand how ROS nodes collect and publish data.\nIn this article, we\u0026rsquo;ll:\nSet up the MacBook as a ROS node (using Docker) Turn the iPad into a ROS node (using Conduit) Receive the iPad\u0026rsquo;s sensor data on the MacBook Setting Up the MacBook as a ROS Node (Docker) The ROS documentation mentions installing ROS2 natively on macOS, but in practice this runs into many deprecated packages and version dependency issues. After a few rounds of debugging with Claude Code, I decided to skip native installation — my goal at this stage was to see the data format ROS2 produces, not fight with the toolchain.\nThe Docker image approach is the simplest way to get started.\nCreate a directory ros2-ipad/ with a config/ subdirectory. Prepare three files:\nros2-ipad |__ Dockerfile |__ docker-compose.yml |__ config/zenoh_router.json5 Dockerfile FROM --platform=linux/arm64 ros:jazzy RUN apt-get update \u0026amp;\u0026amp; apt-get install -y \\ ros-jazzy-rmw-zenoh-cpp \\ \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* RUN echo \u0026#34;/opt/ros/jazzy/opt/zenoh_cpp_vendor/lib\u0026#34; \u0026gt; /etc/ld.so.conf.d/zenoh.conf \u0026amp;\u0026amp; ldconfig Uses the Jazzy release of ROS. If you\u0026rsquo;re on an Intel Mac, change linux/arm64 to linux/amd64. Installs the ros-jazzy-rmw-zenoh-cpp package. Fixes the library path. docker-compose.yml services: zenoh_router: build: . container_name: zenoh_router ports: - \u0026#34;7447:7447/tcp\u0026#34; - \u0026#34;7447:7447/udp\u0026#34; stdin_open: true tty: true environment: - ROS_DOMAIN_ID=0 - RMW_IMPLEMENTATION=rmw_zenoh_cpp - LD_LIBRARY_PATH=/opt/ros/jazzy/opt/zenoh_cpp_vendor/lib - ZENOH_ROUTER_CONFIG_URI=./config/zenoh_router.json5 volumes: - ./config:/config:ro command: \u0026gt; bash -c \u0026#34;source /opt/ros/jazzy/setup.bash \u0026amp;\u0026amp; ros2 run rmw_zenoh_cpp rmw_zenohd\u0026#34; Field Description build: . Builds the image from the local Dockerfile container_name Fixed container name zenoh_router ports Maps host port 7447 to the container (TCP/UDP), allowing external devices like the iPad to connect stdin_open / tty Keeps the terminal open for interactive use ROS_DOMAIN_ID=0 ROS 2 domain ID; nodes must share the same domain to communicate RMW_IMPLEMENTATION Specifies Zenoh as the ROS 2 middleware LD_LIBRARY_PATH Ensures libzenohc.so is found ZENOH_ROUTER_CONFIG_URI Points to the Zenoh router config file volumes Mounts the local ./config directory into the container (read-only) command Sources the ROS 2 setup and starts the Zenoh router config/zenoh_router.json5 { mode: \u0026#34;router\u0026#34;, listen: { endpoints: [ \u0026#34;tcp/0.0.0.0:7447\u0026#34; ], }, } Field Value Description mode \u0026quot;router\u0026quot; Starts in router mode, relaying messages between nodes listen.endpoints \u0026quot;tcp/0.0.0.0:7447\u0026quot; Listens on all network interfaces on port 7447 Since Docker doesn\u0026rsquo;t have a fixed IP, we use 0.0.0.0 to listen on all interfaces.\nRun It Inside the ros2-ipad/ directory, run:\ndocker compose build docker compose up Turning the iPad into a ROS Node (Conduit) Search for and install Conduit, powered by ROS from the App Store.\nConfigure ROS Topics On the Conduit home screen, select the topics you want to publish — options include IMU, GPS, Camera, and Battery.\nSet the IP Address On your Mac, go to System Settings \u0026gt; Wi-Fi and find the MacBook\u0026rsquo;s current IP address on your Wi-Fi network.\nBack in Conduit on the iPad, tap the settings icon in the top-right. Set Zenoh Router \u0026gt; Router Address to the MacBook\u0026rsquo;s IP address.\nStart Conduit Tap the run button in the bottom-right of Conduit to start streaming.\nReceiving iPad Sensor Data on the MacBook Make sure the iPad and MacBook are on the same Wi-Fi network.\nAfter starting Conduit on the iPad, go back to the MacBook.\nFirst, check the running container ID with docker ps:\nEnter the container with bash:\ndocker exec -it 2e76462241cb bash Source the ROS 2 environment:\nsource /opt/ros/jazzy/setup.bash export RMW_IMPLEMENTATION=rmw_zenoh_cpp List available topics:\nroot@2e76462241cb:/# ros2 topic list /conduit/camera/front/camera_info /conduit/camera/front/image_raw/compressed /parameter_events /rosout /tf_static Echo a topic to view its data:\nroot@2e76462241cb:/# ros2 topic echo /conduit/camera/front/image_raw/compressed Camera data received successfully!\nConclusion In this article, we documented how to use an iPad as a ROS2 sensor and receive its data on a Mac. The data flow looks like this:\niPad ↓ tcp/\u0026lt;MacBook IP\u0026gt;:7447 ↓ Docker port mapping 7447 → 7447 (container) ↓ Zenoh listening on 0.0.0.0:7447 This was a great hands-on way to learn ROS2 commands and understand what ROS node sensor data looks like in practice.\n","permalink":"https://blog.gannipiece.tw/en/posts/how-to-connect-ipad-and-macbook-by-ros2/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"background\"\u003eBackground\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve been learning ROS, but I don\u0026rsquo;t have a robot at hand. My idea: use the iPad I already have as a ROS node and receive data on my Mac — sensor data like IMU, Camera, Battery, etc. — to understand how ROS nodes collect and publish data.\u003c/p\u003e\n\u003cp\u003eIn this article, we\u0026rsquo;ll:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSet up the MacBook as a ROS node (using Docker)\u003c/li\u003e\n\u003cli\u003eTurn the iPad into a ROS node (using Conduit)\u003c/li\u003e\n\u003cli\u003eReceive the iPad\u0026rsquo;s sensor data on the MacBook\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"setting-up-the-macbook-as-a-ros-node-docker\"\u003eSetting Up the MacBook as a ROS Node (Docker)\u003c/h2\u003e\n\u003cp\u003eThe ROS documentation mentions \u003ca href=\"https://docs.ros.org/en/kilted/Installation/Alternatives/macOS-Development-Setup.html\" target=\"_blank\" rel=\"noopener\"\u003einstalling ROS2 natively on macOS\u003c/a\u003e, but in practice this runs into many deprecated packages and version dependency issues. After a few rounds of debugging with Claude Code, I decided to skip native installation — my goal at this stage was to see the data format ROS2 produces, not fight with the toolchain.\u003c/p\u003e","title":"How to Use an iPad as a ROS2 Sensor and Receive Data on a Mac"},{"content":"(the post is automatically translated by AI)\nIn early 2026, I left my senior software engineer position at a large tech company to join a startup as a tech lead. Still figuring out both the technical and managerial sides, I\u0026rsquo;m hoping this series of journal entries will help me look back years later and see how I\u0026rsquo;ve grown.\nIn the previous entry, we unified the deployment process across all environments, giving us more confidence in changes and tests going forward. Now it was finally time to look at the system\u0026rsquo;s behavior under traffic spikes.\nThe system runs on an AKS Kubernetes cluster. In the past, when the system received sudden bursts of traffic, the auto-scaling mechanism failed to kick in in time, causing customers to complain about long wait times.\nMy first step was to understand the baseline: what does \u0026ldquo;peak load\u0026rdquo; actually look like in terms of concurrent users? What is the auto-scaling mechanism expected to achieve? And how long is the expected end-to-end processing time per request?\nBased on current requirements, a traffic spike means roughly 100–200 simultaneous user requests. Consumer pods are responsible for processing these requests, and a mechanism called the \u0026ldquo;hot standby job\u0026rdquo; is responsible for auto-scaling before requests pile up. Ideally, since one end-to-end request is expected to take about six minutes, we want all users\u0026rsquo; wait times to fall within that window.\n200 concurrent requests isn\u0026rsquo;t a lot. With an auto-scaling mechanism in place and a maximum replica count of 200, you\u0026rsquo;d expect it to handle this gracefully. So why was there still delay?\nI ran tests using k6 to simulate concurrent users. Without even reaching 200 users, a run with just 100 users took 50 minutes total. Worse, the per-request processing time increased as the number of users grew.\nThere was clearly a bottleneck in the system.\nBut before addressing the bottleneck, I noticed something more serious: the auto-scaling mechanism wasn\u0026rsquo;t actually scaling consumers as expected. Even under elevated load with K8s resources still available, the number of consumers only reached around 10. With so few workers, slow throughput was inevitable.\nSo I had to look at exactly how the \u0026ldquo;hot standby job\u0026rdquo; was doing its scaling. The mechanism works like this: a monitor-like worker periodically checks the status of each consumer. When a consumer enters \u0026ldquo;working\u0026rdquo; state, the monitor calculates how many additional workers are needed and calls the K8s API to increase the consumer count.\nThis sounds reasonable on the surface, but there were a few failure points. First, consumers save their state to Redis during state transitions; the monitor worker reads Redis to decide whether to scale. The two operate at different polling frequencies, which creates a timing gap. Second, the original implementation didn\u0026rsquo;t account for all state transition scenarios — so even after a request was done, the consumer state could linger in Redis.\nThe result was a fairly complex system. Which raised the question: did we actually need that complexity?\nCommon Kubernetes auto-scaling approaches include HPA and KEDA. I asked the engineer why neither of these was used and why the hot standby job mechanism was built from scratch.\nThe answer: customers had complained about slow processing, so the goal was to keep a few \u0026ldquo;pre-warmed\u0026rdquo; consumers on standby so they could accept new tasks immediately, skipping image pull and initialization time.\nI later measured: that initialization time was around 200 milliseconds — negligible. This is exactly why data should drive decision-making.\nSince the existing design couldn\u0026rsquo;t reliably meet our needs, we needed a better-maintained and more stable solution for auto-scaling. After evaluating options, KEDA was the right fit: its event-driven scaling model is better suited than HPA for our use case of monitoring RabbitMQ queue depth. We decided to migrate to KEDA.\nThe result: after switching to KEDA, the system became significantly more stable and easier to track. Peak traffic processing time dropped from nearly an hour to under 20 minutes — roughly a 30% improvement. While we haven\u0026rsquo;t hit the target window yet, the auto-scaling consumer count now reaches the expected level, and the system itself is simpler and more stable. A solid first step.\n","permalink":"https://blog.gannipiece.tw/en/posts/startup-journey-0002-autoscaling-deep-dive/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eIn early 2026, I left my senior software engineer position at a large tech company to join a startup as a tech lead. Still figuring out both the technical and managerial sides, I\u0026rsquo;m hoping this series of journal entries will help me look back years later and see how I\u0026rsquo;ve grown.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIn the previous entry, we unified the deployment process across all environments, giving us more confidence in changes and tests going forward. Now it was finally time to look at the system\u0026rsquo;s behavior under traffic spikes.\u003c/p\u003e","title":"Startup Journey 0002 | Why Isn't Auto-Scaling Working? The System Just Crashes"},{"content":"(the post is automatically translated by AI)\nIn early 2026, I left my senior software engineer position at a large tech company to join a startup as a tech lead. Still figuring out both the technical and managerial sides, I\u0026rsquo;m hoping this series of journal entries will help me look back years later and see how I\u0026rsquo;ve grown.\nMy first task after joining was to look back at the current system. This system had long struggled with auto-scaling, causing delays under sudden or sustained high traffic.\nSo on day one I started working with the existing developer to get the environment set up. The first problem surfaced immediately: since this system had been developed almost entirely by a single engineer, reproducing it locally — or on the cloud K8s cluster — turned out to be impossible. Environment variable mismatches between setups were a major culprit.\nAs I dug deeper into the infrastructure, a more serious issue emerged that needed immediate attention: each environment\u0026rsquo;s deployment is maintained separately. In other words, while there appears to be a \u0026ldquo;dev/test,\u0026rdquo; \u0026ldquo;staging,\u0026rdquo; and \u0026ldquo;production\u0026rdquo; environment split, they are not consistent with each other. The Jenkins CI/CD consists of three separate pipelines — one per environment.\nWhat does that mean in practice? It means that any fix or test we do on dev or staging cannot be guaranteed to hold in production, because the three pipelines are independent processes.\nThe environments seem to exist, but also kind of don\u0026rsquo;t.\nI knew that if we wanted meaningful performance testing on staging, we\u0026rsquo;d first need to unify the deployment process across all environments. So while I was drafting the next release plan in the first week, I was simultaneously studying the system architecture and working to unify the deployment process without touching the existing codebase.\nThe three environments currently differ in their cluster setups: dev runs on our own self-hosted machines, while staging and production run in the cloud. The biggest difference is that the cloud uses Istio for ingress routing, while the local environment uses a plain ingress controller. Additionally, the staging infrastructure code hadn\u0026rsquo;t been updated in a long time — the previous workflow was basically: develop and test on dev, then deploy directly to production. Staging was only occasionally used for performance testing.\nSince staging was already neglected, I decided not to try to fix it. My plan: take the infrastructure currently used in production and migrate it to staging, then adjust the environment variables for staging-specific settings.\nWhat about dev? Normally you\u0026rsquo;d test on dev before promoting to staging, then production. But the dev environment had engineers actively building the next release. It was more efficient to work in parallel — I\u0026rsquo;d handle staging, and we\u0026rsquo;d sync dev later once there was a stable checkpoint.\nSo the migration plan became:\nComplete the staging infra reproduction Unify the Jenkins pipeline so all environments use the same deployment process Deploy to staging with the unified process to confirm no regressions Deploy to production with the unified process to confirm no regressions (Optional) Migrate the unified Jenkins flow to GitLab CI/CD Deploy to dev with the unified process Step 5 is partly personal preference — Jenkins Groovy syntax is less readable than GitLab CI/CD YAML, and since the Jenkins config doesn\u0026rsquo;t live in the codebase it\u0026rsquo;s outside version control. I\u0026rsquo;d also rather not manage multiple toolchains; keeping CI/CD co-located with the code repo reduces operational overhead.\nAfter about a week, we finally had a unified deployment across all environments — a first step in the right direction. Now it\u0026rsquo;s time to tackle the actual traffic bottleneck.\n","permalink":"https://blog.gannipiece.tw/en/posts/startup-journey-0001-environment-consistency/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eIn early 2026, I left my senior software engineer position at a large tech company to join a startup as a tech lead. Still figuring out both the technical and managerial sides, I\u0026rsquo;m hoping this series of journal entries will help me look back years later and see how I\u0026rsquo;ve grown.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eMy first task after joining was to look back at the current system. This system had long struggled with auto-scaling, causing delays under sudden or sustained high traffic.\u003c/p\u003e","title":"Startup Journey 0001 | It's There, But Also Not There"},{"content":"(the post is automatically translated by AI)\nIn early 2026, I left my senior software engineer position at a large tech company to join a startup as a tech lead. Still figuring out both the technical and managerial sides, I\u0026rsquo;m hoping this series of journal entries will help me look back years later and see how I\u0026rsquo;ve grown.\nIn early 2025, a former lab senior — also a co-founder from our first startup attempt — reached out again with an invitation to build something together. At that point, I hadn\u0026rsquo;t been a senior engineer for very long, and my risk tolerance was fairly low. So I declined.\nAfter that, they reached out multiple times — true \u0026ldquo;three visits to the thatched cottage\u0026rdquo; persistence. Having settled into the senior engineer role for a while, I started feeling the itch to try something different. When the next invitation came, I decided to take the leap and join the startup.\nTo be honest, we had already tried this once before in 2022. Back then, I was still a master\u0026rsquo;s student, and the founders had just graduated from the lab with limited industry experience. Our lack of clarity on requirements and market direction had us running in circles. Knowing my own limitations and how much I still needed to learn from the industry, I stepped away first. The convenient timing of military service — specifically the R\u0026amp;D alternative service program — gave me a two-year window to work in the industry. I figured I\u0026rsquo;d come back afterward. Little did I know \u0026ldquo;afterward\u0026rdquo; would be three years later. The team composition hasn\u0026rsquo;t changed much, but the company has.\nThis time, joining the startup is also a way to test myself against the years that have passed. What can I bring to a company just getting off the ground? Can I have a bigger impact and help more people? Can I gain more experience leading a team? These three questions are the most important reasons driving my decision.\nThe company\u0026rsquo;s current state: there\u0026rsquo;s one relatively mature product, a new domain they want to explore, and some scattered client work. Most team members are each handling their own project. My first task after joining is to assess the stability and architecture of the existing mature product — and determine whether to replace or retain it in preparation for high-traffic scenarios.\nGiven that everyone is essentially a one-person team, I anticipated that software development processes might not be fully established — things like CI/CD and separate testing environments.\nSure enough, within the first week I discovered that while there is a basic CI/CD pipeline on Jenkins, each environment is configured independently. The configuration differences between environments are significant, which means there\u0026rsquo;s actually very little confidence in the test-to-production reliability. In other words, the current workflow makes it nearly impossible to catch in staging what might still break in production.\nI\u0026rsquo;ll document that more in the next entry.\nFor now, the ship has sailed and the plunge has been taken. Let\u0026rsquo;s see how far I can go.\n","permalink":"https://blog.gannipiece.tw/en/posts/startup-journey-0000-to-join-or-not/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eIn early 2026, I left my senior software engineer position at a large tech company to join a startup as a tech lead. Still figuring out both the technical and managerial sides, I\u0026rsquo;m hoping this series of journal entries will help me look back years later and see how I\u0026rsquo;ve grown.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIn early 2025, a former lab senior — also a co-founder from our first startup attempt — reached out again with an invitation to build something together. At that point, I hadn\u0026rsquo;t been a senior engineer for very long, and my risk tolerance was fairly low. So I declined.\u003c/p\u003e","title":"Startup Journey 0000 | To Join or Not To Join"},{"content":"(the post is automatically translated by AI)\nTable of Contents About Sushi DAW Installation Method 1: Using the Official Pre-built Binary Method 2: Build from Source Verifying the Installation: Playing a Synth Arpeggio Conclusion Troubleshooting Issue 1: required Xcode 9 or newer References About Sushi DAW Sushi is the plugin host and DAW for the Elk Audio OS [1]. Key features include:\nConfiguration via JSON files Runtime control via OSC (Open Sound Control) or Google\u0026rsquo;s gRPC protocol A configurable logging system for third-party integrations gRPC supports multiple languages — Python, JavaScript, Lua, C/C++, and more — which allows rapid prototyping with elkpy in Python and production optimization with elkcpp in C/C++.\nWhile Sushi has long supported Linux, since version 1.0 it also supports macOS. This article documents how to install it on macOS and verify the setup.\nInstallation Method 1: Using the Official Pre-built Binary The Releases page provides official pre-built binaries for macOS and Linux. Download the file matching your macOS architecture (Intel or Apple Silicon).\nAfter downloading and extracting, open a terminal in that folder and run:\n$ xattr -rc sushi Method 2: Build from Source Clone the repository from GitHub: $ git clone https://github.com/elk-audio/sushi Update all submodules: $ git submodule update --init --recursive Create a build directory and navigate into it: $ mkdir build \u0026amp;\u0026amp; cd build Generate the Makefile with CMake (see Troubleshooting #1 if you hit issues): $ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../third-party/vcpkg/scripts/buildsystems/vcpkg.cmake .. Build: $ make Verifying the Installation: Playing a Synth Arpeggio First, locate the sushi executable. For Method 1, it\u0026rsquo;s in the top-level of the extracted folder; for Method 2, it\u0026rsquo;s deep inside the build directory.\nIn misc/config_files, there are sample JSON config files. Find play_brickworks_synth.json and run:\n$ ./sushi --coreaudio -c config_files/play_brickworks_synth.json If you hear a synthesizer arpeggio, the installation and setup are complete!\nConclusion In this article, we briefly introduced Sushi — a headless DAW — and documented how to install it on macOS. In upcoming articles, we\u0026rsquo;ll demonstrate how to control it via gRPC for real-time performance control.\nTroubleshooting Issue 1: required Xcode 9 or newer Error snippet: Vst3 macOS detected -- [SMTG] Check C++ compiler CMake Error at third-party/vst3sdk/cmake/modules/SMTG_DetectPlatform.cmake:55 (message): [SMTG] XCode 9 or newer is required Fix:\nInstall Xcode from the App Store\nAdd the path to ~/.zshrc:\nexport PATH=\u0026#34;/Applications/Xcode.app/Contents/Developer/usr/bin:$PATH\u0026#34; References [1] elk-audio/sushi: Elk Audio OS Plugin host and DAW, GitHub, https://github.com/elk-audio/sushi\n[2] OSC index, https://ccrma.stanford.edu/groups/osc/index.html\n[3] gRPC, https://grpc.io/\n","permalink":"https://blog.gannipiece.tw/en/posts/install-sushi-daw-on-macos/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003cbr\u003e\n\u003cdetails open\u003e\n    \u003csummary\u003eTable of Contents\u003c/summary\u003e\n    \u003cnav id=\"TableOfContents\"\u003e\n  \u003cul\u003e\n    \u003cli\u003e\u003ca href=\"#about-sushi-daw\"\u003eAbout Sushi DAW\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#installation\"\u003eInstallation\u003c/a\u003e\n      \u003cul\u003e\n        \u003cli\u003e\u003ca href=\"#method-1-using-the-official-pre-built-binary\"\u003eMethod 1: Using the Official Pre-built Binary\u003c/a\u003e\u003c/li\u003e\n        \u003cli\u003e\u003ca href=\"#method-2-build-from-source\"\u003eMethod 2: Build from Source\u003c/a\u003e\u003c/li\u003e\n      \u003c/ul\u003e\n    \u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#verifying-the-installation-playing-a-synth-arpeggio\"\u003eVerifying the Installation: Playing a Synth Arpeggio\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#conclusion\"\u003eConclusion\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#troubleshooting\"\u003eTroubleshooting\u003c/a\u003e\n      \u003cul\u003e\n        \u003cli\u003e\u003ca href=\"#issue-1-required-xcode-9-or-newer\"\u003eIssue 1: \u003ccode\u003erequired Xcode 9 or newer\u003c/code\u003e\u003c/a\u003e\u003c/li\u003e\n      \u003c/ul\u003e\n    \u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#references\"\u003eReferences\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/nav\u003e\n\u003c/details\u003e\n\u003cbr\u003e\n\n\u003ch2 id=\"about-sushi-daw\"\u003eAbout Sushi DAW\u003c/h2\u003e\n\u003cp\u003eSushi is the plugin host and DAW for the \u003ca href=\"https://github.com/elk-audio/sushi\" target=\"_blank\" rel=\"noopener\"\u003eElk Audio OS\u003c/a\u003e [1]. Key features include:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eConfiguration via JSON files\u003c/li\u003e\n\u003cli\u003eRuntime control via \u003ca href=\"https://ccrma.stanford.edu/groups/osc/index.html\" target=\"_blank\" rel=\"noopener\"\u003eOSC\u003c/a\u003e (Open Sound Control) or Google\u0026rsquo;s \u003ca href=\"https://grpc.io/\" target=\"_blank\" rel=\"noopener\"\u003egRPC\u003c/a\u003e protocol\u003c/li\u003e\n\u003cli\u003eA configurable logging system for third-party integrations\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003egRPC supports multiple languages — Python, JavaScript, Lua, C/C++, and more — which allows rapid prototyping with \u003ca href=\"https://github.com/elk-audio/elkpy\" target=\"_blank\" rel=\"noopener\"\u003eelkpy\u003c/a\u003e in Python and production optimization with \u003ca href=\"https://github.com/elk-audio/elkcpp\" target=\"_blank\" rel=\"noopener\"\u003eelkcpp\u003c/a\u003e in C/C++.\u003c/p\u003e","title":"How to Install Sushi DAW on macOS"},{"content":"(the post is automatically translated by AI)\nTable of Contents Introduction Migrating the GitHub Repo to GitLab Configuring the GitLab CI/CD Pipeline Scheduled Deployment (Auto-Deploy on a Schedule) Pointing the Gandi Domain to GitLab Pages Configure GitLab Pages Configure Gandi DNS Conclusion References Introduction My personal website was previously hosted on GitHub, deployed via GitHub Actions to GitHub Pages, with a custom domain pointed to it. That workflow was convenient — once set up, I only had to focus on content and not worry about deployment.\nHowever, one drawback is that the repository hosting GitHub Pages must be public. Making it private is possible, but it costs money. GitLab Pages, on the other hand, still supports deploying private repositories for free. So I decided to migrate the workflow to GitLab.\nThis guide is divided into three phases:\nMigrating the GitHub repo to GitLab Configuring the GitLab CI/CD pipeline Pointing the Gandi domain to GitLab Pages Migrating the GitHub Repo to GitLab On GitLab, click New Project Select Import project Choose GitHub import Connect your GitHub account Select the GitHub project and import it into GitLab Confirm the repository has the Hugo file structure Done Configuring the GitLab CI/CD Pipeline Create a .gitlab-ci.yml file in the project root to configure CI/CD.\nOn the repo page, click + to add a New file Name it .gitlab-ci.yml Add the following configuration: yaml # To contribute improvements to CI/CD templates, please follow the Development guide at: # https://docs.gitlab.com/ee/development/cicd/templates.html --- # All available Hugo versions are listed here: # https://gitlab.com/pages/hugo/container_registry default: image: \u0026#34;${CI_TEMPLATE_REGISTRY_HOST}/pages/hugo:latest\u0026#34; variables: GIT_SUBMODULE_STRATEGY: recursive test: script: - hugo rules: - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH pages: script: - hugo artifacts: paths: - public rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH environment: production This GitLab Hugo template has three parts: environment config, test, and deploy:\nEnvironment config: sets the Docker image and enables recursive submodule updates Test: runs on any branch that is not the default branch Deploy: runs on the default branch only Commit the file Go to Build \u0026gt; Pipelines and confirm the job was triggered successfully Scheduled Deployment (Auto-Deploy on a Schedule) In my case, I want the site to redeploy every day so that future-scheduled posts go live automatically without manual action.\nGo to Build \u0026gt; Pipeline schedules 1 Click New schedule Set a description Choose Interval Pattern — I use 0 0 * * * (midnight every day) under Custom Set the Cron 2 timezone to UTC+8 (Taiwan) Select the branch or tag to run Check Activated Click Create pipeline schedule Done Pointing the Gandi Domain to GitLab Pages The final step is to point your Gandi domain to GitLab Pages. You\u0026rsquo;ll need to purchase a domain from Gandi first.\nConfigure GitLab Pages Go to Settings, expand Visibility, project features, permissions Set Project visibility to Private Set Pages permissions from Only Project Members to Everyone Go to Deploy \u0026gt; Pages Check Use unique domain and save Click New Domain and enter your Gandi domain Confirm the page info — before DNS verification, Verified should show Not verified Note the DNS and Verification status info — you\u0026rsquo;ll need these in the next step Configure Gandi DNS Log into gandi.net and go to Domains \u0026gt; DNS Records Click Add record and create an A type record Set the name to your domain (e.g., blog.gannipiece.tw) Set the IPv4 address to 35.185.44.232 — this is the current (2023) IP for all GitLab Pages Go back and add another record of type TXT Use the same domain name Set the Text value to the full string after TXT in the Verification status (not just the code portion) Save and wait about 30 minutes for DNS propagation and verification Conclusion This article documents how to migrate from GitHub Pages to GitLab Pages and point a Gandi domain to the auto-deployed GitLab page. It also shows how to use GitLab CI/CD to schedule automatic Hugo deployments.\nA few things worth noting: DNS configuration for GitLab Pages differs from GitHub Pages. With GitHub Pages, you typically set a CNAME record; with GitLab, you set an A record pointing to 35.185.44.232. Also, for the TXT verification record, you need to paste the entire string after TXT in the Verification status field — not just the code portion.\nReferences scheduler https://docs.gitlab.com/ee/ci/pipelines/schedules.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nCron https://docs.gitlab.com/ee/topics/cron/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://blog.gannipiece.tw/en/posts/deploy-hugo-to-gitlab-with-gandi/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003cbr\u003e\n\u003cdetails open\u003e\n    \u003csummary\u003eTable of Contents\u003c/summary\u003e\n    \u003cnav id=\"TableOfContents\"\u003e\n  \u003cul\u003e\n    \u003cli\u003e\u003ca href=\"#introduction\"\u003eIntroduction\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#migrating-the-github-repo-to-gitlab\"\u003eMigrating the GitHub Repo to GitLab\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#configuring-the-gitlab-cicd-pipeline\"\u003eConfiguring the GitLab CI/CD Pipeline\u003c/a\u003e\n      \u003cul\u003e\n        \u003cli\u003e\u003ca href=\"#scheduled-deployment-auto-deploy-on-a-schedule\"\u003eScheduled Deployment (Auto-Deploy on a Schedule)\u003c/a\u003e\u003c/li\u003e\n      \u003c/ul\u003e\n    \u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#pointing-the-gandi-domain-to-gitlab-pages\"\u003ePointing the Gandi Domain to GitLab Pages\u003c/a\u003e\n      \u003cul\u003e\n        \u003cli\u003e\u003ca href=\"#configure-gitlab-pages\"\u003eConfigure GitLab Pages\u003c/a\u003e\u003c/li\u003e\n        \u003cli\u003e\u003ca href=\"#configure-gandi-dns\"\u003eConfigure Gandi DNS\u003c/a\u003e\u003c/li\u003e\n      \u003c/ul\u003e\n    \u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#conclusion\"\u003eConclusion\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#references\"\u003eReferences\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/nav\u003e\n\u003c/details\u003e\n\u003cbr\u003e\n\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eMy personal website was previously hosted on GitHub, deployed via GitHub Actions to GitHub Pages, with a custom domain pointed to it. That workflow was convenient — once set up, I only had to focus on content and not worry about deployment.\u003c/p\u003e","title":"How to Deploy Hugo to GitLab Pages and Point It to a Gandi Domain"},{"content":"(the post is automatically translated by AI)\nIntroduction MediaPipe is a collection of ML solutions maintained and developed by Google for various computer vision tasks, including detecting multiple body parts 1 — face, hands, torso, hair, and more (Figure 1).\nFigure 1 One of the most common applications is using the Holistic 2 solution for full-body skeleton detection. Since it captures both facial and body landmark data, it can substitute for traditional motion capture hardware (e.g., MoCap suits) — especially useful for Vtuber applications that don\u0026rsquo;t require high-precision motion.\nOnce we have the MediaPipe skeleton data, we can map it to a personal character model (e.g., a VRM model) and drive its skeleton in real time through Unity or other engines.\nHowever, while there are YouTube demos showing this mapping in action, the underlying code and approach are rarely shared — often because of commercial software constraints.\nIn this article, we explain how we map a MediaPipe skeleton to a Unity humanoid skeleton, and we open-source the conversion code in hopes that the community can help improve it.\nGanniPiece/MetU: A mapping from Mediapipe skeleton to Unity humanoid skeleton. (github.com)\nIf you\u0026rsquo;re already familiar with coordinate system conversions and 3D rotation, you can jump directly to the Implementation section.\nCoordinate System Conversion MediaPipe uses a right-handed coordinate system 3, while Unity uses a left-handed one 4. Additionally, MediaPipe\u0026rsquo;s origin is at the top-right of the image, whereas Unity\u0026rsquo;s world origin is at the bottom-left.\nFigure 2 Left: MediaPipe coordinate system. Right: Unity world coordinate system.\nNote: MediaPipe\u0026rsquo;s world pose landmark and pose landmark are both right-handed, but differ slightly. The former uses the person\u0026rsquo;s hip as the origin; the latter uses the top-left corner of the image.\n3D Rotation Computing rotations using dynamic Euler angles in 3D space is prone to Gimbal Lock 5. Using static (world-space) Euler angles avoids this problem.\nWhat\u0026rsquo;s the difference? In dynamic Euler angles, three rotation matrices $R_x$, $R_y$, $R_z$ are applied sequentially. This means when point $p$ is rotated about $y$ by 90°, the $z$-axis rotation aligns with the $y$-axis, losing one degree of freedom — this is Gimbal Lock.\nFigure 3 5 The solution is to use a fixed world coordinate system for all rotations. In Unity, this can be done with:\nusing UnityEngine; Vector3.FromToRotation(fromDirection_a, toDirection_b); FromToRotation returns a Quaternion representing the rotation from vector $a$ to vector $b$. Append .eulerAngles to get Euler angles.\nImplementation With these two prerequisites understood, here\u0026rsquo;s how we map the two skeletons. The main steps are:\nConvert the coordinate axis Set the rotation angle From Figure 2, the Unity world coordinate system and MediaPipe\u0026rsquo;s world pose landmark differ by a sign flip on every axis. So after retrieving MediaPipe landmark positions, we negate all coordinates to convert to Unity\u0026rsquo;s world space:\n$$P_{Unity}(x,y,z) = -\\alpha \\cdot P_{MediaPipe}(x,y,z) = \\alpha \\cdot P_{MediaPipe}(-x, -y, -z)$$\nwhere $\\alpha$ is a positive constant representing the scale ratio between the two coordinate systems.\nFor example, Unity\u0026rsquo;s positive $y$-axis (green arrow) corresponds to MediaPipe\u0026rsquo;s negative $y$-axis, and similarly for $x$ and $z$. So a MediaPipe point at $(-1, -1, -1)$ maps to $\\alpha(1, 1, 1)$ in Unity.\nAfter coordinate conversion, we calculate the rotation between the Unity skeleton vector and the corresponding MediaPipe vector. For instance, to set the upper arm rotation, we compute the (shoulder → elbow) vector in MediaPipe, compute the same vector in the Unity model, apply FromToRotation to get the rotation, and rotate the Unity bone accordingly.\nThe pseudocode for each joint:\nMappingBoneFromMediaPipeToUnity (mediaPipeVec, unityVec, target): var mediaPipeVecInUnity = -mediaPipeVec var rotation = FromToRotation(unityVec, mediaPipeVecInUnity) target.Rotate(rotation, Space.World) Conclusion In this article, we introduced MediaPipe as a real-time skeleton detection solution and provided a simple, understandable method for mapping its skeleton to Unity\u0026rsquo;s humanoid skeleton. We\u0026rsquo;ve also open-sourced the conversion code at GanniPiece/MetU — feel free to explore and contribute.\nReferences ML Solutions in MediaPipe: https://google.github.io/mediapipe/#ml-solutions-in-mediapipe\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nHolistic Solution: https://google.github.io/mediapipe/solutions/holistic.html\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nRight-handed coordinate system: https://learn.microsoft.com/en-us/windows/uwp/graphics-concepts/coordinate-systems\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nLeft-handed coordinate system: https://learn.microsoft.com/en-us/windows/uwp/graphics-concepts/coordinate-systems\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nGimbal Lock: https://en.wikipedia.org/wiki/Gimbal_lock\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://blog.gannipiece.tw/en/posts/mediapipe-skeleton-to-unity-humanoid/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://mediapipe.dev\" target=\"_blank\" rel=\"noopener\"\u003eMediaPipe\u003c/a\u003e is a collection of ML solutions maintained and developed by Google for various computer vision tasks, including detecting multiple body parts \u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e — face, hands, torso, hair, and more (Figure 1).\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e\u003cimg src=\"https://i.imgur.com/CPapaFy.png\" alt=\"Figure 1\" loading=\"lazy\"\u003e\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eFigure 1\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eOne of the most common applications is using the Holistic \u003csup id=\"fnref:2\"\u003e\u003ca href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e2\u003c/a\u003e\u003c/sup\u003e solution for full-body skeleton detection. Since it captures both facial and body landmark data, it can substitute for traditional motion capture hardware (e.g., MoCap suits) — especially useful for Vtuber applications that don\u0026rsquo;t require high-precision motion.\u003c/p\u003e","title":"How to Map MediaPipe Skeleton to Unity Humanoid"},{"content":"(the post is automatically translated by AI)\nIntroduction In the previous article, Why Coroutine? (Part 1) — Is Multithreading Not Enough?, we covered the underlying principles of Coroutines and listed the interface we need to implement one in C. In this article, we\u0026rsquo;ll walk through the code line by line.\nSource code: GanniPiece/SimpleCCoroutine: A simple coroutine example implemented using C (github.com)\nBackground Knowledge Before we begin, if you\u0026rsquo;re not familiar with Coroutines, please read the previous article first. In it we mentioned that we can manipulate ucontext_t [1] using four operations defined in \u0026lt;ucontext.h\u0026gt;:\nmakecontext [2] setcontext [3] getcontext [4] swapcontext [5] If you\u0026rsquo;re already familiar with these, skip ahead to the Implementation section.\nucontext_t ucontext_t is defined in \u0026lt;ucontext.h\u0026gt; — its full name is \u0026ldquo;user context.\u0026rdquo; The header uses typedef to define mcontext_t, and also defines the ucontext_t struct, which contains the following members:\nucontext_t *uc_link pointer to the context that will be resumed when this context returns sigset_t uc_sigmask the set of signals that are blocked when this context is active stack_t uc_stack the stack used by this context mcontext_t uc_mcontext a machine-specific representation of the saved context uc_link: Pointer to the context to return to when this context exits. uc_sigmask: Signals blocked while this context is active. uc_stack: Stores the function stack and program counter. uc_mcontext: Machine-specific context representation. getcontext / setcontext #include \u0026lt;ucontext.h\u0026gt; int getcontext(ucontext_t *ucp); int setcontext(const ucontext_t *ucp); getcontext() initializes the struct pointed to by ucp with the current thread\u0026rsquo;s context. setcontext() restores the context pointed to by ucp.\nmakecontext / swapcontext #include \u0026lt;ucontext.h\u0026gt; void makecontext(ucontext_t *ucp, (void *func)(), int argc, ...); int swapcontext(ucontext_t *oucp, const ucontext_t *ucp); makecontext() specifies the function func for the context pointed to by ucp. When this context is resumed via swapcontext() or setcontext(), argc arguments are passed and func is executed.\nImplementation Step 0: Preprocessor Directives and Macro Definitions #define _XOPEN_SOURCE #include \u0026lt;ucontext.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #define handle_error(msg) \\ do {perror(msg); exit(EXIT_FAILURE); } while(0) Step 1: Define the Consumer A consumer has its own task to complete and may be interrupted (yield) during execution.\ntypedef struct Coroutine { ucontext_t *ctx; char func_stack[16384]; void* param; bool is_finished; struct Coroutine *next; } coroutine_t; Step 2: Define the Producer The producer is the main Coroutine — it can register consumers and schedule them.\ntypedef struct Registry { ucontext_t *ctx; char func_stack[16384]; coroutine_t *cur_coroutine; // currently running coroutine coroutine_t *coroutines; // list of coroutines } co_registry_t; Step 3: Define the Producer\u0026rsquo;s Task static co_registry_t *registry; static ucontext_t main_ctx; static void main_ctx_function () { printf(\u0026#34;main context: get control\\n\u0026#34;); if (!registry-\u0026gt;cur_coroutine) return; // scheduling while (1) { coroutine_t* cur = registry-\u0026gt;cur_coroutine; coroutine_t* ptr = cur-\u0026gt;next; while (ptr-\u0026gt;is_finished) { ptr = ptr-\u0026gt;next; if (ptr == cur) break; } if ((cur-\u0026gt;is_finished) \u0026amp;\u0026amp; (ptr == cur)) break; registry-\u0026gt;cur_coroutine = ptr; printf(\u0026#34;main coroutine: switch\\n\u0026#34;); if (swapcontext(registry-\u0026gt;ctx, registry-\u0026gt;cur_coroutine-\u0026gt;ctx) == -1) handle_error(\u0026#34;swapcontext\u0026#34;); } } Step 4: Create the Producer\u0026rsquo;s Context co_registry_t * create_producer () { co_registry_t *p; p = malloc(sizeof(co_registry_t)); p-\u0026gt;ctx = malloc(sizeof(ucontext_t)); if (getcontext(p-\u0026gt;ctx) == -1) handle_error(\u0026#34;getcontext\u0026#34;); p-\u0026gt;ctx-\u0026gt;uc_stack.ss_sp = p-\u0026gt;func_stack; p-\u0026gt;ctx-\u0026gt;uc_stack.ss_size = sizeof(p-\u0026gt;func_stack); makecontext(p-\u0026gt;ctx, main_ctx_function, 0); return p; } Step 5: Create a Consumer\u0026rsquo;s Context /* create a new coroutine for a new task */ coroutine_t * create_coroutine(coroutine_body_t func, void *param) { coroutine_t *p; p = malloc(sizeof(coroutine_t)); p-\u0026gt;ctx = malloc(sizeof(ucontext_t)); if (getcontext(p-\u0026gt;ctx) == -1) handle_error(\u0026#34;getcontext\u0026#34;); p-\u0026gt;ctx-\u0026gt;uc_stack.ss_sp = p-\u0026gt;func_stack; p-\u0026gt;ctx-\u0026gt;uc_stack.ss_size = sizeof(p-\u0026gt;func_stack); p-\u0026gt;param = param; p-\u0026gt;is_finished = false; p-\u0026gt;next = NULL; makecontext(p-\u0026gt;ctx, func, 0); return p; } Step 6: Implement yield /* return control to the producer, which will * determine the next context to run. */ void yield() { swapcontext(registry-\u0026gt;cur_coroutine-\u0026gt;ctx, registry-\u0026gt;ctx); } Step 7: Register a Coroutine void register_coroutine(coroutine_t *coroutine) { if (!registry-\u0026gt;coroutines) { registry-\u0026gt;coroutines = coroutine; registry-\u0026gt;cur_coroutine = coroutine; } else { coroutine_t *ptr = registry-\u0026gt;coroutines; while (ptr-\u0026gt;next != NULL) ptr = ptr-\u0026gt;next; ptr-\u0026gt;next = coroutine; } } Step 8: Main Program int main () { registry = create_co_registry(); printf(\u0026#34;creating coroutines...\\n\u0026#34;); coroutine_t *co_1 = create_coroutine(func1, (void*) 0); coroutine_t *co_2 = create_coroutine(func2, (void*) 0); printf(\u0026#34;registering coroutines...\\n\u0026#34;); register_coroutine(co_1); register_coroutine(co_2); co_2-\u0026gt;next = co_1; printf(\u0026#34;starting coroutines...\\n\u0026#34;); if (swapcontext(\u0026amp;main_ctx, registry-\u0026gt;ctx) == -1) handle_error(\u0026#34;swapcontext\u0026#34;); free(registry); free(co_1); free(co_2); } Conclusion In this article, we continued from Part 1 and walked through how to implement a Coroutine in C with actual code. The source code is available at GanniPiece/SimpleCCoroutine — feel free to download and explore it.\nReferences ","permalink":"https://blog.gannipiece.tw/en/posts/why-coroutine-part-2-implementing-in-c/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eIn the previous article, \u003ca href=\"https://blog.gannipiece.tw/en/posts/why-coroutine-part-1-is-multithreading-bad/\"\u003eWhy Coroutine? (Part 1) — Is Multithreading Not Enough?\u003c/a\u003e, we covered the underlying principles of Coroutines and listed the interface we need to implement one in C. In this article, we\u0026rsquo;ll walk through the code line by line.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSource code: \u003ca href=\"https://github.com/GanniPiece/SimpleCCoroutine\" target=\"_blank\" rel=\"noopener\"\u003eGanniPiece/SimpleCCoroutine: A simple coroutine example implemented using C (github.com)\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"background-knowledge\"\u003eBackground Knowledge\u003c/h2\u003e\n\u003cp\u003eBefore we begin, if you\u0026rsquo;re not familiar with Coroutines, please read the previous article first. In it we mentioned that we can manipulate \u003ccode\u003eucontext_t\u003c/code\u003e [1] using four operations defined in \u003ccode\u003e\u0026lt;ucontext.h\u0026gt;\u003c/code\u003e:\u003c/p\u003e","title":"Why Coroutine? (Part 2) — Implementing a Coroutine in C"},{"content":"(the post is automatically translated by AI)\nIntroduction Macros and inline functions are two techniques for code reuse and expansion. Unlike regular function calls, both avoid the overhead of subroutine push/pop operations at runtime, which can speed up execution.\nThe key difference between them is when the expansion happens: macros are substituted by the preprocessor before compilation, while inline functions are expanded by the compiler during compilation. However, both result in larger binary sizes compared to using regular functions, since the code is duplicated at every call site.\nIn this article, we\u0026rsquo;ll explain the principles behind macros and inline functions, and compare them with practical examples.\nMacro A macro is expanded by the preprocessor through text substitution. As mentioned in the article #: The Language of the Preprocessor, the #define directive lets us define function-like macros 1.\nA quick refresher on macro syntax:\n#define identifier(parameters, ...) replacement-list Every occurrence of identifier in the code is replaced by replacement-list at the preprocessing stage. A simple example:\n#define MAX(x, y) (x) \u0026gt; (y) ? (x) : (y) After the preprocessor runs, every MAX(x, y) is replaced with (x) \u0026gt; (y) ? (x) : (y).\nCommon Pitfalls Accidentally adding = or ;\n#define MAX(x, y) (x) \u0026gt; (y) ? (x) : (y) // correct #define MAX(x, y) = (x) \u0026gt; (y) ? (x) : (y) // error 1(a): stray = #define MAX(x, y) (x) \u0026gt; (y) ? (x) : (y); // error 1(b): stray ; Forgetting to wrap variables in parentheses\n#define TWICE(x) 2 * x int M = TWICE(3 + 5) This expands to M = 2 * 3 + 5, giving 11 instead of the expected 16. Always protect operands with parentheses.\nInline Function An inline function (内嵌函式) embeds the function body directly at the call site. There are two ways to use it:\nUsing the inline keyword — the compiler expands the function at compile time. Compiler auto-inlining — the compiler may automatically inline short functions for performance. Example using the inline keyword:\ninline int max(x, y) { if (x \u0026gt;= y) return x; else return y; } Observing Macro and Inline with GCC Observing Macros Use the -E flag to see what the preprocessor produces. Given:\n/* test_macro_01.c */ #define MAX(x, y) x \u0026gt; y? x: y int main () { MAX(2, 3); } After gcc -E -o test_macro.i test_macro.c:\n/* test_macro_01.i */ ... int main() { 2 \u0026gt; 3 ? 2 : 3; } If we use a post-increment operator:\n/* test_macro_02.c */ #define MAX(x, y) x \u0026gt; y? x: y int main () { int i=4, j=5; MAX(i++, j); } The expanded result:\n/* test_macro_02.i */ ... int main() { int i=4, j=5; i++ \u0026gt; j ? i++ : j; } Because the macro is plain text substitution, i is incremented twice, ending up as 6 instead of 5.\nObserving Inline Use gcc -S to view the generated assembly. Comparing a function with and without inline:\n/* test_inline.c */ inline int twice (int x) { return 2 * x; } int main () { twice(3); } /* test_no_inline.c */ int twice (int x) { return 2 * x; } int main () { twice (3); } From the assembly output, the inline version merges twice directly into _main, eliminating the extra push/pop overhead. The non-inline version keeps a separate _twice subroutine that is called via callq.\nMacro vs Inline Function Function Macro Inline Execution Time Runtime Preprocessing Compiling Usage type function (arguments) #define inline Declaration Position Anywhere Must be declared at the top Inside or outside a class Termination Closing brace } Newline (use \\ for multi-line) Closing brace } Mechanism push / pop text substitution function substitution Debugging Easy Difficult Easy Automation No Must be explicitly defined Short class functions may be auto-inlined by the compiler Expansion No Always Can be disabled with compiler flags Speed Slower Faster Faster Space Usage Smaller Larger Larger Conclusion In this article, we introduced two code reuse techniques beyond regular functions: macros and inline functions. We gave examples of how each works and common pitfalls (such as the macro definition errors 2). We then used GCC 3 to observe each at a different compilation stage and compared them in the table above.\nIf you\u0026rsquo;re curious about preprocessors in general, check out the earlier article #: The Language of the Preprocessor.\nReferences Replacing text macros - cppreference.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nDon\u0026rsquo;t underestimate the preprocessor\u0026#160;\u0026#x21a9;\u0026#xfe0e;\ngcc(1) - Linux manual page (man7.org)\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://blog.gannipiece.tw/en/posts/macro-vs-inline-function/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eMacros and inline functions are two techniques for code reuse and expansion. Unlike regular function calls, both avoid the overhead of subroutine push/pop operations at runtime, which can speed up execution.\u003c/p\u003e\n\u003cp\u003eThe key difference between them is \u003cstrong\u003ewhen\u003c/strong\u003e the expansion happens: macros are substituted by the preprocessor before compilation, while inline functions are expanded by the compiler during compilation. However, both result in larger binary sizes compared to using regular functions, since the code is duplicated at every call site.\u003c/p\u003e","title":"Code Expansion: Macro vs Inline Function"},{"content":"(the post is automatically translated by AI)\nIntroduction The preprocessor runs before the compilation phase. As a pre-compilation step, it can handle several types of tasks, including conditional compilation (e.g., #if, #ifndef), file inclusion, and macro definitions. In this article, we\u0026rsquo;ll look at each of these preprocessor features one by one.\nThe Language of the Preprocessor To communicate with the preprocessor, we first need to understand its language. A standard preprocessor directive consists of three parts:\nThe # character A series of standard-defined commands or user-defined preprocessing commands A newline In other words, any code line starting with # is a directive for the preprocessor!\nPreprocessor: \u0026ldquo;Anything that doesn\u0026rsquo;t start with # — I don\u0026rsquo;t recognize it!\u0026rdquo;\nHow many types of preprocessor language are there? The reference documentation 1 clearly lists several categories:\nConditional inclusion 2 Replacing text macros 3 Source file inclusion 4 Diagnostic directive 5 Implementation defined behavior control 6 Filename and line information 7 Conditional Inclusion Conditional preprocessing begins with #if, #ifdef, or #ifndef, and can include any number of #elif, #elifdef, or #elifndef directives, followed by an optional #else. The block is terminated with #endif.\nExample: we first define ABCD and map it to 2. Then we check whether ABCD is defined and print accordingly.\n#define ABCD 2 #include \u0026lt;iostream\u0026gt; int main () { #ifdef ABCD std::cout \u0026lt;\u0026lt; \u0026#34;1: yes\\n\u0026#34; \u0026lt;\u0026lt; std::endl; #else std::cout \u0026lt;\u0026lt; \u0026#34;1: no\\n\u0026#34; \u0026lt;\u0026lt; std::endl; #endif } Replacing Text Macros // Object-like macros #define identifier replacement-list // Function-like macros #define identifier(parameters, ...) replacement-list Macros substitute text using #define. Once defined, the compiler replaces every occurrence of identifier in the code with the replacement-list. The example #define ABCD 2 in the previous section is a simple object-like macro.\nThere are two forms: Object-like macros and Function-like macros.\nSource File Inclusion #include \u0026lt;h-chr-sequence\u0026gt; new-line #include \u0026#34;q-char-sequence\u0026#34; new-line This is the familiar header file inclusion. #include \u0026lt;stdio.h\u0026gt;, for instance, replaces the directive with the contents of stdio.h.\nDiagnostic Directive #error diagnostic-message #warning diagnostic-message Implementation Defined Behavior Control #pragma pragma-params pragma directives control compiler-specific behaviors, such as suppressing warnings. The ISO C standard 8 does not mandate specific pragma implementations, but several are common: #pragma STDC, #pragma unpack, #pragma once, etc.\npragma once Most modern compilers support this 9. When seen in a header file, it means the file will only be parsed once during compilation, even if included from multiple places. This achieves the same goal as include guards (#ifndef ... #define ... #endif).\nHowever, there are some caveats: #pragma once cannot distinguish between files with the same name in different directories, nor can it guard against system-level header conflicts.\npragma pack #pragma pack(arg) #pragma pack() #pragma pack(push) #pragma pack(push, arg) #pragma pack(pop) This family of directives sets the memory alignment for user-defined contiguous data structures. Setting arg determines the alignment size in bytes.\nFilename and Line Information #line lineno #line lineno \u0026#34;filename\u0026#34; Used to change the current preprocessor\u0026rsquo;s filename (__FILE__) and line number (__LINE__).\n#include \u0026lt;cassert\u0026gt; #define FNAME \u0026#34;test.cc\u0026#34; int main() { #line 777 FNAME assert(2+2 == 5); } // Output: // test: test.cc:777: int main(): Assertion `2+2 == 5\u0026#39; failed. Conclusion In this article, we examined several preprocessor directive types: file inclusion, replacing text macros, conditional inclusion, and more. These preprocessing techniques let us write more flexible and efficient code, and transform the source before the compilation phase begins.\nReferences Preprocessor - cppreference.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nReplacing text macros - cppreference.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nConditional inclusion - cppreference.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nSource file inclusion - cppreference.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nDiagnostic directives - cppreference.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nImplementation defined behavior control - cppreference.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nFilename and line information - cppreference.com\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nISO/IEC 9899:2018 - Information technology - Programming languages - C (ansi.org)\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nPragma once - Wikipedia\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://blog.gannipiece.tw/en/posts/preprocessor-language/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eThe preprocessor runs before the compilation phase. As a pre-compilation step, it can handle several types of tasks, including conditional compilation (e.g., \u003ccode\u003e#if\u003c/code\u003e, \u003ccode\u003e#ifndef\u003c/code\u003e), file inclusion, and macro definitions. In this article, we\u0026rsquo;ll look at each of these preprocessor features one by one.\u003c/p\u003e\n\u003ch2 id=\"the-language-of-the-preprocessor\"\u003eThe Language of the Preprocessor\u003c/h2\u003e\n\u003cp\u003eTo communicate with the preprocessor, we first need to understand its language. A standard preprocessor directive consists of three parts:\u003c/p\u003e","title":"#: The Language of the Preprocessor"},{"content":"(the post is automatically translated by AI)\nIntroduction Many beginners in music production hesitate to buy a MIDI keyboard because of the cost, even though having a MIDI controller can significantly streamline the composition workflow. Wouldn\u0026rsquo;t it be convenient if the tablet you already own could act as a MIDI controller?\nIn this article, we\u0026rsquo;ll cover how to turn a mobile device (specifically an iPad) into a wireless MIDI controller. We\u0026rsquo;ll start by introducing some iPad MIDI apps, then explain how to configure the computer side, and finally do a quick test to confirm everything works.\niPad Setup App Download You\u0026rsquo;ll need an app that can send MIDI signals. Here are a few options:\nApp Platform Price Description WiFiMIDI iOS/iPadOS Free A MIDI controller with keyboard, drum pads, and control signal sending. Transmits MIDI over the same Wi-Fi network to DAWs like GarageBand or Reason. Lemur iOS/iPadOS $24.99 USD A MIDI/OSC controller for iOS. TouchOSC iOS/iPadOS $4.99 USD Lets you add knobs and custom controls on a touch interface. Compatible with iOS 5.1.1 and later. The following guide uses WiFiMIDI (free) as the example.\nComputer Setup Make sure your iPad and computer are on the same Wi-Fi network before proceeding!\nmacOS Step Screenshot 1. Open Audio MIDI Setup 2. Go to Window \u0026gt; Show MIDI Studio 3. In the MIDI Studio window, click the 🌐 button in the top-right to open MIDI Network Setup 4. Click + in the top-left to add a new session, then check it 5. Select your device (iPad) and click Connect 6. Configure the MIDI input device in your DAW Windows Step Screenshot 1. Download and install rtpMIDI 2. Open rtpMIDI: using Apple Bonjour 3. Click + in the top-left to create a new session and check it 4. Click + in the bottom-left to add a destination. Enter the iPad\u0026rsquo;s IP address (found under Wi-Fi settings) and set Port to 5004 4.1 Note: check the iPad\u0026rsquo;s IP address under Wi-Fi settings 5. Click Connect 6. Configure the MIDI input device in your DAW Same as macOS Step 6 Testing Test Environment Devices: MacBook Pro 2020 (13-inch, 16GB) / iPad Air 4 OS: macOS Monterey v12.4 / iPadOS 15.5 DAW: Reaper v6.57 Result iPad MacBook (Reaper) Screenshot Description Sending MIDI via WiFiMIDI DAW receiving MIDI signal (yellow bar) Conclusion In this article, we showed how to turn an iPad into a wireless MIDI controller using the rtpMIDI protocol. We tested the method on both Windows and macOS and confirmed it works on both platforms.\nAlthough this guide focuses on iPad, the same approach applies to Android devices by swapping out the iPad app for an Android equivalent (just replace the app in the iPad setup section with an Android MIDI app).\n","permalink":"https://blog.gannipiece.tw/en/posts/ipad-as-wireless-midi-controller/","summary":"In this article, we document how to turn an iPad into a MIDI keyboard and receive its signals on both macOS and Windows.","title":"How to Use an iPad as a Wireless MIDI Controller"},{"content":"(the post is automatically translated by AI)\nIntroduction When using Git [1] for version control, it\u0026rsquo;s easy to accidentally make a typo in a commit message — or simply realize later that it could be worded better. In those cases, you need to go back and fix the commit message.\nThe basic approach is git commit --amend [2], which corrects the most recent commit message. In this article, I\u0026rsquo;ll document how to go back to an older commit — not just the last one — and edit its message for future reference.\nProblem Scenario Suppose the first five commits added files a, b, c, d, and e. However, as shown below, the third commit message accidentally says Add d.txt instead of Add c.txt:\ngit log --oneline 13d43cf (HEAD -\u0026gt; master) Add e.txt 72dc4bd Add d.txt 20f1770 Add d. txt ← should be \u0026#34;Add c.txt\u0026#34; 899c8fa Add b.txt 383b0b Add a. txt Since HEAD is already at 13d43cf, we can\u0026rsquo;t use git commit --amend directly to fix 20f1770.\nHow do we edit the commit message of 20f1770?\nSolution We\u0026rsquo;ll use git rebase in interactive mode [3], triggered with the -i or --interactive flag. To modify commit 20f1770, we need to rebase starting from the commit before it.\nSteps:\nRun this command in the terminal: git rebase -i 899c8fa # the commit before the one you want to edit A list like this will appear: pick 20f1770 Add d.txt pick 72dc4bd Add d.txt pick 13d43cf Add e.txt # Rebase 899c8fa..13d43cf onto 899c8fa (3 commands) # ... # r, reword \u0026lt;commit\u0026gt; = use commit, but edit the commit message # e, edit \u0026lt;commit\u0026gt; = use commit, but stop for amending # ... Change pick to edit for the commit you want to modify: edit 20f1770 Add d.txt pick 72dc4bd Add d.txt pick 13d43cf Add e.txt Save and exit (Esc + :wq)\nUse --amend to edit the message:\ngit commit --amend Change the message: Add c.txt # Changed from \u0026#34;Add d.txt\u0026#34; to \u0026#34;Add c.txt\u0026#34; Save and exit (Esc + :wq)\nContinue the rebase:\ngit rebase --continue Done! Running git log --oneline now shows: 6ff9253 (HEAD -\u0026gt; master) Add e.txt 155afa9 Add d.txt 93c8fff Add c.txt 899c8fa Add b.txt 38e3b0b Add a.txt Conclusion In this article, I documented how to edit older commit messages, including commits several steps back. The key tools are:\ngit rebase -i git commit --amend Used together, they let you cleanly rewrite any past commit message.\nReferences Git - https://git-scm.com git commit \u0026ndash;amend - https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---amend Interactive mode - https://git-scm.com/docs/git-rebase#_interactive_mode ","permalink":"https://blog.gannipiece.tw/en/posts/how-to-edit-commit-message/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eWhen using \u003ca href=\"https://git-scm.com\" target=\"_blank\" rel=\"noopener\"\u003eGit\u003c/a\u003e [1] for version control, it\u0026rsquo;s easy to accidentally make a typo in a commit message — or simply realize later that it could be worded better. In those cases, you need to go back and fix the commit message.\u003c/p\u003e\n\u003cp\u003eThe basic approach is \u003ccode\u003egit commit --amend\u003c/code\u003e [2], which corrects the most recent commit message. In this article, I\u0026rsquo;ll document how to go back to an older commit — not just the last one — and edit its message for future reference.\u003c/p\u003e","title":"How to Edit a Specific Commit Message in Git"},{"content":"(the post is automatically translated by AI)\nIntroduction While working with UniVRM [1], I noticed that many Unity development scenarios rely on Coroutines — for example, opening a system file dialog or loading resources. In these situations, Coroutines are chosen to prevent background tasks from disrupting the user experience, such as when loading a large asset causes the window to freeze and the application becomes unresponsive.\nIn this article, I\u0026rsquo;ll explain what a Coroutine is and compare it with Threads. I\u0026rsquo;ll then go into more detail about how Coroutines work and wrap up with a brief conclusion. If you\u0026rsquo;re interested in implementation, see the follow-up article: Why Coroutine? (Part 2) — Implementing a Coroutine in C.\nBody A Coroutine (共常式 in Mandarin) is literally a \u0026ldquo;cooperative subroutine.\u0026rdquo; Think of it as multiple subroutines sharing a single resource, taking turns using it. Among these subroutines, one acts as the producer — a special member whose job is to manage and schedule the other consumer subroutines. When a consumer finishes its turn, it yields control back to the producer, which then passes the resource to the next consumer. Consumers never hand the resource directly to each other.\nCoroutine flow diagram An analogy: imagine you and your friends order a bowl of shaved ice on a hot summer day, but the shop only gives you one spoon. To be fair, you agree to take turns — one scoop each. The bowl (resource) is shared among everyone\u0026rsquo;s task of eating ice. However, if one friend hogs the spoon and refuses to pass it, the whole system breaks down. This is exactly the pitfall of Cooperative Multitasking.\nThis is the core problem with Cooperative Multitasking [2], which is typically used when a single CPU core handles multiple tasks. If any consumer refuses to yield, the producer can never dispatch the next task.\nAnother common alternative is Multi-threading. With multi-threading, multiple tasks can run \u0026ldquo;simultaneously\u0026rdquo; note 1 on a single core, and each thread has its own context — similar to Coroutines. The key difference is that in multi-threading, the OS decides when to switch between threads (context switch), which means shared data must be protected. We need mechanisms like mutexes and signals to guard critical sections.\nComparison:\nCoroutine Thread Switching User decides where to yield OS decides context switch time Data protection Not required Required Independent context Yes Yes Other Runs inside a thread Note 1: \u0026ldquo;Simultaneously\u0026rdquo; is in quotes because the CPU rapidly switches between tasks, creating an illusion of parallelism.\nHow It Works To implement a Coroutine, we need four core operations: create, assign, retrieve, and switch between subroutines. First, we create a producer, which can register consumers and schedule them. Then, for each consumer, we create its own context. Each time a consumer yields, its state is saved into its context, and control returns to the producer for the next dispatch. This continues until all consumers complete their tasks.\nSummary of the required components and methods:\nName Type Description producer object Can register and schedule consumers consumer object Has its own task; may be interrupted (yield) yield() method Suspends the current consumer and returns control to the producer create_producer() method Creates a producer create_consumer() method Creates a consumer register_consumer() method Registers a new consumer with the producer schedule() method Producer\u0026rsquo;s scheduling logic The overall flow:\ncreate_producer() create_consumer() register_consumer() Start running the producer Producer dispatches resource to a consumer Consumer runs Consumer calls yield() Consumer returns resource to the producer If there are unfinished consumers, go back to step 5. Otherwise, continue. Producer finishes or stands by Conclusion In this article, we explained how Coroutines work and made a brief comparison with Threads. We then took a closer look at the methods and flow required to implement a Coroutine. In the next article, Why Coroutine? (Part 2) — Implementing a Coroutine in C, we\u0026rsquo;ll implement the Coroutine described here using C.\nReferences UniVRM - Github page - https://github.com/vrm-c/UniVRM Cooperative Multitasking - https://en.wikipedia.org/wiki/Cooperative_multitask System V-like - Wikipedia - https://zh.wikipedia.org/zh-tw/UNIX_System_V C/C++ coroutine (fiber) implementation - http://zevoid.blogspot.com/2017/11/cc-coroutine-fiber.html ","permalink":"https://blog.gannipiece.tw/en/posts/why-coroutine-part-1-is-multithreading-bad/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eWhile working with \u003ca href=\"https://github.com/vrm-c/UniVRM\" target=\"_blank\" rel=\"noopener\"\u003eUniVRM\u003c/a\u003e \u003csup\u003e[1]\u003c/sup\u003e, I noticed that many Unity development scenarios rely on Coroutines — for example, opening a system file dialog or loading resources. In these situations, Coroutines are chosen to prevent background tasks from disrupting the user experience, such as when loading a large asset causes the window to freeze and the application becomes unresponsive.\u003c/p\u003e\n\u003cp\u003eIn this article, I\u0026rsquo;ll explain what a Coroutine is and compare it with Threads. I\u0026rsquo;ll then go into more detail about how Coroutines work and wrap up with a brief conclusion. If you\u0026rsquo;re interested in implementation, see the follow-up article: \u003ca href=\"https://blog.gannipiece.tw/en/posts/why-coroutine-part-2-implementing-in-c/\"\u003eWhy Coroutine? (Part 2) — Implementing a Coroutine in C\u003c/a\u003e.\u003c/p\u003e","title":"Why Coroutine? (Part 1) — Is Multithreading Not Enough?"},{"content":"(the post is automatically translated by AI)\nIntroduction A memory leak occurs when allocated memory is never freed. While it doesn\u0026rsquo;t necessarily cause immediate crashes, a leak gradually reduces the available memory, degrading system performance over time. In severe cases, it can lead to unpredictable errors or even security vulnerabilities 1.\nMemory: \u0026ldquo;Hmm? Something smells.\u0026rdquo;\nOne of the most common causes of memory leaks is forgetting to free dynamically allocated memory — for instance, calling new on the heap and never calling delete. The unpredictable timing of when this causes problems makes debugging quite difficult.\nIn Unity, when a memory leak occurs, the console typically shows:\nA Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to get more details. Without further information, this error message alone is not very useful for pinpointing the source of the leak.\nHow to Enable Full Stack Traces Following the hint in the Unity console, we need to enable Full StackTraces for more details.\nInstall the Jobs package\nGo to Window \u0026gt; Package Manager Set the package source to Unity Registry Search for the Jobs package Click Install After installation, a Jobs menu should appear in the menu bar. If it doesn\u0026rsquo;t, go back and confirm the installation succeeded. Enable leak detection\nSelect Jobs \u0026gt; Leak Detection from the menu bar Change the setting from the default On to Full Stack Traces (Expensive) Re-run your project\nNow, when a memory leak occurs, the Unity console will show the exact location of the leak, making it much easier to track down and fix.\nConclusion Memory leaks stem from improper memory management and can cause unpredictable errors that are hard to debug — especially in code you didn\u0026rsquo;t write yourself. This article provides a quick method to pinpoint memory leaks in Unity. Hopefully, the next time you encounter this issue, you\u0026rsquo;ll be able to resolve it faster!\n","permalink":"https://blog.gannipiece.tw/en/posts/detect-memory-leak-in-unity/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eA memory leak occurs when allocated memory is never freed. While it doesn\u0026rsquo;t necessarily cause immediate crashes, a leak gradually reduces the available memory, degrading system performance over time. In severe cases, it can lead to unpredictable errors or even security vulnerabilities \u003ca href=\"https://owasp.org/www-community/vulnerabilities/Memory_leak\" target=\"_blank\" rel=\"noopener\"\u003e1\u003c/a\u003e.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eMemory: \u0026ldquo;Hmm? Something smells.\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eOne of the most common causes of memory leaks is forgetting to free dynamically allocated memory — for instance, calling \u003ccode\u003enew\u003c/code\u003e on the heap and never calling \u003ccode\u003edelete\u003c/code\u003e. The unpredictable timing of when this causes problems makes debugging quite difficult.\u003c/p\u003e","title":"How to Enable and Detect Memory Leaks in the Unity Editor"},{"content":"(the post is automatically translated by AI)\nIntroduction Interested in the Vtuber ecosystem and how it works? The best way to learn is by doing. In this article, I\u0026rsquo;ll walk through how to quickly create a Vtuber model using VRoid Studio, export it as a VRM file, import it into Unity, and apply Mixamo animations.\nWe\u0026rsquo;ll follow these steps:\nModel creation — create a model in VRoid Studio and export it File conversion — use Blender to convert VRM to FBX Import into Unity Import Mixamo animations Apply the animation Import the model Model Creation VRoid Studio is a free, beginner-friendly tool for quickly creating character models.\nDownload and install it from the official site (select macOS if you\u0026rsquo;re on a Mac).\nOpen VRoid Studio. You\u0026rsquo;ll see the main screen:\nClick Create New to start a new project. Customize the character as you like — VRoid Studio offers many options for hairstyle, outfit, height, and more.\nWhen you\u0026rsquo;re happy with your model, click the export icon ⬆️ in the top-right and choose Export as VRM. A VRM Settings window will appear. Fill in the required Title and Creator fields, then click Export.\nFile Conversion Converting VRM to FBX allows us to apply Mixamo animations directly. If you only want to import the model into Unity without animations, skip to the Import into Unity section.\nWe\u0026rsquo;ll use Blender for the conversion. Install two plugins: CATS Blender Plugin and VRM Add-on for Blender. Click the GitHub links to download each as a ZIP.\nPlugin Installation Open Blender and go to Edit \u0026gt; Preferences \u0026gt; Add-ons (or press Cmd + , on Mac).\nClick Install (⬇️) in the top-right and install both ZIP files: cats-blender-plugin-master.zip and VRM_Addon_for_Blender-release.zip. After installation, click the Refresh (🔄) icon — you should see:\n3D View: Cats Blender Plugin Import-Export: VRM format If these don\u0026rsquo;t appear, make sure Community is selected in the filter tab.\nConversion In: VRM / Out: FBX\nGo to File \u0026gt; Import \u0026gt; VRM (.vrm) and select the model created earlier. You\u0026rsquo;ll see the character in the main viewport. Remove the default cube from the Scene Collection on the right.\nAt this stage the character has no texture. In the right-side settings panel, switch to the CATS tab, find Fix Model, click the 🔧 settings icon, keep only Fix Materials, then click Fix Model. You\u0026rsquo;ll now see the character with its texture.\nFinally, go to File \u0026gt; Export \u0026gt; FBX (.fbx) to export the model. The FBX file will be generated — this stage is complete.\nImport into Unity After all that work, we\u0026rsquo;re finally at the last stage: importing the FBX model into Unity and applying Mixamo animations.\nInstall UniVRM Plugin To quickly load a VRM file directly in Unity, install the UniVRM plugin. If you converted to FBX in the previous step, you can skip this.\nUniVRM is a Unity package for importing and exporting VRM files.\nOn the UniVRM Releases page, download the .unitypackage file (not the Samples package). In Unity, right-click in the Project panel and select Import Package to add it.\nAfter importing, you should see a VRM0 option in the menu bar, with VRM0 \u0026gt; Import to import VRM models directly.\nMixamo Animations Mixamo is a lifesaver for those without artistic or modeling skills. It has a large library of ready-made animations you can download and use.\nOnline Application If you have an FBX file, you can apply animations directly on Mixamo online (requires a Mixamo account). Upload your FBX character via Animations \u0026gt; UPLOAD CHARACTERS, then browse and preview different animations.\nWhen you find an animation you like, download it as FBX and import it into Unity.\nApplying in Unity To apply animations inside Unity, import the Mixamo FBX into Unity\u0026rsquo;s Assets. Select your character model in the Project panel, then in the Inspector, click Add Component \u0026gt; Animation and set Animation to the imported Mixamo FBX.\nAnd that\u0026rsquo;s it — the model is imported and the animation is applied!\nConclusion This article walked through the full process of creating a Vtuber model in VRoid Studio, converting it for Unity, and adding Mixamo animations. The next step is to hook this up with a motion capture tool and try going live!\n","permalink":"https://blog.gannipiece.tw/en/posts/import-vroid-model-unity-mixamo/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eInterested in the Vtuber ecosystem and how it works? The best way to learn is by doing. In this article, I\u0026rsquo;ll walk through how to quickly create a Vtuber model using \u003ca href=\"https://vroid.com\" target=\"_blank\" rel=\"noopener\"\u003eVRoid Studio\u003c/a\u003e, export it as a VRM file, import it into Unity, and apply Mixamo animations.\u003c/p\u003e\n\u003cp\u003eWe\u0026rsquo;ll follow these steps:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eModel creation\u003c/strong\u003e — create a model in VRoid Studio and export it\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFile conversion\u003c/strong\u003e — use \u003ca href=\"https://www.blender.org\" target=\"_blank\" rel=\"noopener\"\u003eBlender\u003c/a\u003e to convert VRM to FBX\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eImport into \u003ca href=\"https://unity.com\" target=\"_blank\" rel=\"noopener\"\u003eUnity\u003c/a\u003e\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003eImport \u003ca href=\"https://www.mixamo.com/#/\" target=\"_blank\" rel=\"noopener\"\u003eMixamo\u003c/a\u003e animations\u003c/li\u003e\n\u003cli\u003eApply the animation\u003c/li\u003e\n\u003cli\u003eImport the model\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"model-creation\"\u003eModel Creation\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://vroid.com\" target=\"_blank\" rel=\"noopener\"\u003eVRoid Studio\u003c/a\u003e is a free, beginner-friendly tool for quickly creating character models.\u003c/p\u003e","title":"How to Import a VRoid Studio Model into Unity with Mixamo Animations"},{"content":"(the post is automatically translated by AI)\nIntroduction The XY Pad is a common UI element in audio plugins — it lets you control two independent parameters at once. For example, you could set the X-axis to pan position and the Y-axis to volume level. Moving the thumb on the pad then simulates a sound source moving around the listener, which is a very intuitive control surface.\nA basic XY Pad consists of two parts: a Pad (the canvas) and a Thumb (the draggable indicator). Here\u0026rsquo;s an example from Cabbage Audio Forum: the white area is the Pad, the green circle is the Thumb. The Thumb\u0026rsquo;s position maps to the X and Y parameter values.\nThis article assumes you already have a basic understanding of JUCE and won\u0026rsquo;t repeat project setup in detail. For setup, refer to JUCE: Tutorial: Projucer Part 1.\nImplementation Project Setup This is a brief overview. For a full guide on setting up a JUCE project, see a dedicated setup tutorial.\nOpen Projucer and create a Plug-In project (Projucer \u0026gt; Plug-In \u0026gt; Basic). Use the default settings or add modules as needed (e.g., juce_dsp). Click Create Project, then verify that File Explorer \u0026gt; Source contains four files: PluginProcessor.h, PluginProcessor.cpp, PluginEditor.h, PluginEditor.cpp.\nAdd the exporters for your platform (e.g., Xcode, Linux Makefile) under the left-side Exporters section.\nProcessorEditor For this article, we only need to implement the XY Pad UI, so we focus on ProcessorEditor, which inherits from juce::AudioProcessorEditor (which itself inherits juce::Component). The two virtual methods to override are paint() and resized().\njuce::Component::paint() virtual void Component::paint(Graphics\u0026amp; g) Every object inheriting Component can override paint() to define its own rendering. It\u0026rsquo;s called when the component needs to be redrawn — either via repaint(), or when the window is refreshed. Child paint() calls take priority over parent ones. To paint on top of children, implement paintOverChildren() instead.\nCalling repaint() marks the component as \u0026ldquo;dirty\u0026rdquo; and schedules a deferred repaint on the message thread — meaning UI updates in JUCE are asynchronous, as in most modern UI frameworks.\njuce::Component::resized() virtual void Component::resized() Called whenever the component\u0026rsquo;s size changes. Use it to position child components. Unlike repaint(), calling setBounds() or setSize() triggers resized() synchronously.\nBuilding the Components Pad Create a class inheriting juce::Component and override paint() and resized():\nclass Pad: public juce::Component { public: Pad(); ~Pad(); void paint(juce::Graphics\u0026amp; g); void resized(); }; In paint(), draw a white rounded rectangle:\nvoid Pad::paint(juce::Graphics\u0026amp; g) { auto cornerSize = 10.0f; g.setColour(juce::Colours::white); g.fillRoundedRectangle(getLocalBounds().toFloat(), cornerSize); } Thumb Create another class inheriting juce::Component. Override paint() for a circular shape, and also handle mouseDown and mouseDrag to move the Thumb. When dragging starts, invoke the moveCallback.\nFor std::function callback usage, see the article How to Use std::function to Write a Callback Function\nclass Thumb: public juce::Component { public: Thumb(); void paint(juce::Graphics\u0026amp; g) override; void mouseDown(const juce::MouseEvent\u0026amp; event) override; void mouseDrag(const juce::MouseEvent\u0026amp; event) override; int getSize(); std::function\u0026lt;void(juce::Point\u0026lt;float\u0026gt;)\u0026gt; moveCallback; private: static constexpr int thumbSize = 20; juce::ComponentDragger dragger; juce::ComponentBoundsConstrainer constrainer; }; Constructor — configure the constrainer:\nThumb::Thumb() { constrainer.setMinimumOnscreenAmounts(thumbSize, thumbSize, thumbSize, thumbSize); } Override paint():\nvoid Thumb::paint(juce::Graphics\u0026amp; g) { g.setColour(juce::Colours::green); g.fillEllipse(getLocalBounds().toFloat()); } Override mouseDown() and mouseDrag():\nvoid Thumb::mouseDown(const juce::MouseEvent\u0026amp; event) { dragger.startDraggingComponent(this, event); } void Thumb::mouseDrag(const juce::MouseEvent\u0026amp; event) { dragger.dragComponent(this, event, \u0026amp;constrainer); if (moveCallback) moveCallback(getPosition().toFloat()); } Assembling the Components The Pad contains the Thumb.\nAdd a Thumb member to Pad and make it visible in the constructor:\nclass Pad: public juce::Component { ... private: Thumb thumb; ... }; void Pad::Pad() { addAndMakeVisible(thumb); } Position the Thumb inside the Pad in resized():\nvoid Pad::resized() { juce::Rectangle\u0026lt;int\u0026gt; centre = getLocalBounds().withSizeKeepingCentre(thumb.getSize(), thumb.getSize()); thumb.setBounds(centre); thumb.setCentrePosition(getLocalBounds().proportionOfWidth(0.5), getLocalBounds().proportionOfHeight(0.3)); } (Optional) Define the Thumb\u0026rsquo;s move callback — e.g., to update a panner or volume value:\nvoid Pad::Pad() { ... thumb.moveCallback = [\u0026amp;](juce::Point\u0026lt;float\u0026gt; pos) { // do something with pos }; } Add the Pad to PluginEditor:\nclass XYPadTutorialEditor : public juce::AudioProcessorEditor { public: XYPadTutorialEditor(); ... private: ... Pad pad; } void XYPadTutorialEditor::XYPadTutorialEditor() { addAndMakeVisible(pad); ... } void XYPadTutorialEditor::resized() { pad.setBounds(getLocalBounds().reduced(20)); } Done! That\u0026rsquo;s the XY Pad implemented in JUCE — fully draggable!\n","permalink":"https://blog.gannipiece.tw/en/posts/juce-xypad-implementation/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eThe XY Pad is a common UI element in audio plugins — it lets you control two independent parameters at once. For example, you could set the X-axis to pan position and the Y-axis to volume level. Moving the thumb on the pad then simulates a sound source moving around the listener, which is a very intuitive control surface.\u003c/p\u003e\n\u003cp\u003eA basic XY Pad consists of two parts: a \u003cstrong\u003ePad\u003c/strong\u003e (the canvas) and a \u003cstrong\u003eThumb\u003c/strong\u003e (the draggable indicator). Here\u0026rsquo;s an example from \u003ca href=\"https://forum.cabbageaudio.com/t/chnset-and-xypad/321\" target=\"_blank\" rel=\"noopener\"\u003eCabbage Audio Forum\u003c/a\u003e: the white area is the Pad, the green circle is the Thumb. The Thumb\u0026rsquo;s position maps to the X and Y parameter values.\u003c/p\u003e","title":"How to Implement an XY Pad with JUCE"},{"content":"(the post is automatically translated by AI)\nIntroduction With the plugin development mostly done, the next task is to merge two parallel branches of work. Git provides two approaches for this: merge and rebase.\nIn this article, I\u0026rsquo;ll explain how rebase works and illustrate it with a practical example from our development workflow.\nHow It Works git rebase replays commits from one branch on top of another, using the target branch as the new base. Suppose you have two branches A and B with diverging histories — you need to integrate their differences.\nIf you want to use branch A as the base, first switch to branch A: ❯ git checkout A Confirm HEAD is on branch A: ❯ git status On branch A nothing to commit, working tree clean Rebase B onto A (run this from branch B): ❯ # (on branch B) ❯ git rebase A If there are conflicts, resolve them one by one following git\u0026rsquo;s output messages.\nAfter resolving each conflict, stage the file:\n❯ git add \u0026lt;modified file name\u0026gt; Repeat steps 4–5, then continue the rebase until all conflicts are resolved: ❯ git rebase --continue Example Suppose my partner and I are each working on an independent feature, me on branch A and them on branch B. Their feature A is done and I want to include it in my branch B. I can use rebase to bring their work into my current branch.\nA simplified example: the develop branch has d.cpp. In separate branches, I add a.cpp to branch A and b.cpp to branch B.\nOriginal d.cpp (branch develop): #include \u0026lt;iostream\u0026gt; int main() { std::cout \u0026lt;\u0026lt; \u0026#34;D\u0026#34; \u0026lt;\u0026lt; std::endl; } d.cpp on branch A: #include \u0026lt;iostream\u0026gt; int main() { std::cout \u0026lt;\u0026lt; \u0026#34;A\u0026#34; \u0026lt;\u0026lt; std::endl; } d.cpp on branch B: #include \u0026lt;iostream\u0026gt; int main() { std::cout \u0026lt;\u0026lt; \u0026#34;B\u0026#34; \u0026lt;\u0026lt; std::endl; } Rebasing B onto A causes a conflict in d.cpp:\nAuto-merging d.cpp CONFLICT (content): Merge conflict in d.cpp error: could not apply 1b2cb82... Update B Resolve all conflicts manually, mark them as resolved with \u0026#34;git add/rm \u0026lt;conflicted_files\u0026gt;\u0026#34;, then run \u0026#34;git rebase --continue\u0026#34;. You can instead skip this commit: run \u0026#34;git rebase --skip\u0026#34;. To abort and get back to the state before \u0026#34;git rebase\u0026#34;, run \u0026#34;git rebase --abort\u0026#34;. Could not apply 1b2cb82... Update B Open d.cpp and resolve the conflict: #include \u0026lt;iostream\u0026gt; int main() { \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD std::cout \u0026lt;\u0026lt; \u0026#34;A\u0026#34; \u0026lt;\u0026lt; std::endl; ======= std::cout \u0026lt;\u0026lt; \u0026#34;B\u0026#34; \u0026lt;\u0026lt; std::endl; \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; 1b2cb82 (Update B) } Keep the version you want, then stage the file. Run git rebase --continue and repeat step 5 until all conflicts are resolved. Done. ","permalink":"https://blog.gannipiece.tw/en/posts/git-rebase-merge-branches/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eWith the plugin development mostly done, the next task is to merge two parallel branches of work. \u003ca href=\"https://git-scm.com\" title=\"git\" target=\"_blank\" rel=\"noopener\"\u003eGit\u003c/a\u003e provides two approaches for this: \u003cstrong\u003emerge\u003c/strong\u003e and \u003cstrong\u003erebase\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eIn this article, I\u0026rsquo;ll explain how \u003ccode\u003erebase\u003c/code\u003e works and illustrate it with a practical example from our development workflow.\u003c/p\u003e\n\u003ch2 id=\"how-it-works\"\u003eHow It Works\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003egit rebase\u003c/code\u003e replays commits from one branch on top of another, using the target branch as the new base. Suppose you have two branches \u003cstrong\u003eA\u003c/strong\u003e and \u003cstrong\u003eB\u003c/strong\u003e with diverging histories — you need to integrate their differences.\u003c/p\u003e","title":"How to Use git rebase to Integrate Branches"},{"content":"(the post is automatically translated by AI)\nIntroduction std::function is a C++11 feature defined in the \u0026lt;functional\u0026gt; header. It\u0026rsquo;s similar to a function pointer in C, but more general: any CopyConstructible Callable object can be stored, copied, and invoked through it.\nExamples of such objects include functions, lambda expressions, bind expressions, as well as pointers to member functions and member data.\nThe stored callable object is referred to as the std::function\u0026rsquo;s target. If no target is assigned, the std::function is called empty, and invoking it will throw std::bad_function_call.\nA callback function is a function passed as an argument to another function, to be called at some later point.\nHow to Use It First, let\u0026rsquo;s look at the std::function class template:\ntemplate\u0026lt;class R, class... args\u0026gt; class function\u0026lt;R(args)\u0026gt; To use function, you specify the argument type(s) args and the return type R at initialization. These correspond to the parameters and return type of the Callable. Use void for no return value or no arguments.\nExample:\nstd::function\u0026lt;void(int, float)\u0026gt; callback; This defines a std::function named callback that takes an int and a float and returns nothing.\nTo assign a target, std::function overloads operator=. If you have a function func that matches the signature, assign it directly:\nvoid func(int a, float b); callback = func; Example Program #include \u0026lt;iostream\u0026gt; #include \u0026lt;functional\u0026gt; std::function\u0026lt;void(int, float)\u0026gt; callback; void add(int a, float b) { std::cout \u0026lt;\u0026lt; a + b \u0026lt;\u0026lt; std::endl; } void sub(int a, float b) { std::cout \u0026lt;\u0026lt; a - b \u0026lt;\u0026lt; std::endl; } void func(int a, float b, std::function\u0026lt;void(int, float)\u0026gt; c) { c(a, b); } int main() { int a = 5; float b = 0.3; // 5 + 0.3 callback = add; func(a, b, callback); // 5 - 0.3 callback = sub; func(a, b, callback); } ","permalink":"https://blog.gannipiece.tw/en/posts/cpp-std-function-callback/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://en.cppreference.com/w/cpp/utility/functional/function\" target=\"_blank\" rel=\"noopener\"\u003e\u003ccode\u003estd::function\u003c/code\u003e\u003c/a\u003e is a C++11 feature defined in the \u003ccode\u003e\u0026lt;functional\u0026gt;\u003c/code\u003e header. It\u0026rsquo;s similar to a function pointer in C, but more general: any \u003ca href=\"https://en.cppreference.com/w/cpp/named_req/CopyConstructible\" target=\"_blank\" rel=\"noopener\"\u003e\u003cem\u003eCopyConstructible\u003c/em\u003e\u003c/a\u003e \u003ca href=\"https://en.cppreference.com/w/cpp/named_req/Callable\" target=\"_blank\" rel=\"noopener\"\u003e\u003cem\u003eCallable\u003c/em\u003e\u003c/a\u003e object can be stored, copied, and invoked through it.\u003c/p\u003e\n\u003cp\u003eExamples of such objects include functions, lambda expressions, bind expressions, as well as pointers to member functions and member data.\u003c/p\u003e\n\u003cp\u003eThe stored callable object is referred to as the \u003ccode\u003estd::function\u003c/code\u003e\u0026rsquo;s \u003cstrong\u003etarget\u003c/strong\u003e. If no target is assigned, the \u003ccode\u003estd::function\u003c/code\u003e is called \u003cstrong\u003eempty\u003c/strong\u003e, and invoking it will throw \u003ccode\u003estd::bad_function_call\u003c/code\u003e.\u003c/p\u003e","title":"How to Use std::function to Write a Callback Function in C++"},{"content":"This is GanniPiece\u0026rsquo;s personal blog — a home for literary writing (in Chinese), technical articles, and notes from everyday life.\nAbout Me Backend software engineer with 3+ years of experience in Python, C#, and cloud systems (AWS/Azure) and system design. Skilled in automated and E2E testing, DevOps practices (Terraform, Opsgenie), and scalable backend design. Passionate about the intersection of code and sound, with research experience in signal processing and music information retrieval.\nContact me mailto: gannipiece.tw@gmail.com Github: github.com/GanniPiece ","permalink":"https://blog.gannipiece.tw/en/about/","summary":"\u003cp\u003eThis is GanniPiece\u0026rsquo;s personal blog — a home for literary writing (in Chinese), technical articles, and notes from everyday life.\u003c/p\u003e\n\u003ch2 id=\"about-me\"\u003eAbout Me\u003c/h2\u003e\n\u003cp\u003eBackend software engineer with 3+ years of experience in Python, C#, and cloud systems (AWS/Azure) and system design. Skilled in automated and E2E testing, DevOps practices (Terraform, Opsgenie), and scalable backend design. Passionate about the intersection of code and sound, with research experience in signal processing and music information retrieval.\u003c/p\u003e","title":"About"},{"content":"(the post is automatically translated by AI)\nIntroduction While you can run a GitLab Runner installed directly on your machine, GitLab also provides Docker images [[1]] that let you run GitLab Runner inside a Docker container.\nThis article follows the official documentation [[2]] — starting with installing Docker, then creating a container, and finally running GitLab Runner inside it.\nInstalling Docker macOS $ brew cask install docker Linux (Ubuntu) [[3]] If this is your first Docker installation:\nUpdate apt and allow it to use a repository over HTTPS:\n$ sudo apt-get update $ sudo apt-get install \\ ca-certificates \\ curl \\ gnupg \\ lsb-release Add Docker\u0026rsquo;s official GPG key:\n$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyring.gpg Set up the stable repository (you can substitute stable with nightly or test):\n$ echo \\ \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \\ $(lsb_release -cs) stable\u0026#34; | sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null Install Docker Engine:\nUpdate apt and install Docker Engine and containerd:\n$ sudo apt-get update $ sudo apt-get install docker-ce docker-ce-cli containerd.io Verify the installation with hello-world:\n$ sudo docker run hello-world Uninstall Docker Engine:\nRemove the packages:\n$ sudo apt-get purge docker-ce docker-ce-cli containerd.io Manually remove images, containers, volumes, and config files:\n$ sudo rm -rf /var/lib/docker $ sudo rm -rf /var/lib/containerd Running GitLab Runner in a Container Use docker run to both create and start the container in one step. Two options are available to ensure the GitLab Runner config persists across container restarts:\nOption 1: Mount a local system volume\n$ docker run -d --name gitlab-runner --restart always \\ -v /srv/gitlab-runner/config:/etc/gitlab-runner \\ -v /var/run/docker.sock:/var/run/docker.sock \\ gitlab/gitlab-runner:latest Option 2: Use a Docker volume\nCreate a Docker volume:\n$ docker volume create gitlab-runner-config Start the runner using that volume:\n$ docker run -d --name gitlab-runner --restart always \\ -v /var/run/docker.sock:/var/run/docker.sock \\ -v gitlab-runner-config:/etc/gitlab-runner \\ gitlab/gitlab-runner:latest With either option, GitLab Runner is now running inside Docker.\nAdditional Notes Updating the container after editing config.toml:\n$ docker restart gitlab-runner config.toml is typically found at the location configured when mounting (e.g., /srv/gitlab-runner/config/ for Option 1).\nReading GitLab Runner logs:\n$ docker logs [container name] References https://docs.gitlab.com/runner/install/docker.html#docker-images Run GitLab Runner in a container | GitLab Install Docker Engine on Ubuntu | Docker Documentation ","permalink":"https://blog.gannipiece.tw/en/posts/run-gitlab-runner-in-docker/","summary":"\u003cp\u003e\u003cem\u003e(the post is automatically translated by AI)\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eWhile you can run a GitLab Runner installed directly on your machine, GitLab also provides \u003ca href=\"https://docs.gitlab.com/runner/install/docker.html#docker-images\" target=\"_blank\" rel=\"noopener\"\u003eDocker images\u003c/a\u003e [[1]] that let you run GitLab Runner inside a Docker container.\u003c/p\u003e\n\u003cp\u003eThis article follows the \u003ca href=\"https://docs.gitlab.com/runner/install/docker.html\" target=\"_blank\" rel=\"noopener\"\u003eofficial documentation\u003c/a\u003e [[2]] — starting with installing Docker, then creating a container, and finally running GitLab Runner inside it.\u003c/p\u003e\n\u003ch2 id=\"installing-docker\"\u003eInstalling Docker\u003c/h2\u003e\n\u003ch3 id=\"macos\"\u003emacOS\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e$ brew cask install docker\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"linux-ubuntu-3\"\u003eLinux (Ubuntu) [[3]]\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eIf this is your first Docker installation:\u003c/strong\u003e\u003c/p\u003e","title":"How to Run GitLab Runner in Docker"}]