{"id":2631,"date":"2026-03-14T18:44:52","date_gmt":"2026-03-14T16:44:52","guid":{"rendered":"https:\/\/bio-me.bio\/?page_id=2631"},"modified":"2026-03-14T18:46:07","modified_gmt":"2026-03-14T16:46:07","slug":"cpr-compression-rate-trainer","status":"publish","type":"page","link":"https:\/\/bio-me.bio\/?page_id=2631","title":{"rendered":"CPR Compression Rate Trainer"},"content":{"rendered":"\n<div class=\"eco-tool wp-block-group\" id=\"eco-tool-cpr-2c6d9\">\n  <div class=\"eco-tool__header\">\n    <h2 class=\"eco-tool__title\">CPR Compression Rate Trainer<\/h2>\n    <p class=\"eco-tool__lead\">\n      Train a steady chest-compression rhythm (informational training aid). For real emergencies, follow local guidance and call emergency services.\n    <\/p>\n  <\/div>\n\n  <form class=\"eco-tool__form\" id=\"eco-cpr-form-2c6d9\" novalidate>\n    <div class=\"eco-tool__grid3\">\n      <!-- BPM -->\n      <div class=\"eco-tool__field\">\n        <label class=\"eco-tool__label\" for=\"eco-cpr-bpm-2c6d9\">Target rate (BPM)<\/label>\n        <input class=\"eco-tool__input\" id=\"eco-cpr-bpm-2c6d9\" type=\"number\" min=\"60\" max=\"180\" step=\"1\" value=\"110\" inputmode=\"numeric\" \/>\n        <div class=\"eco-tool__hint\">Common training range: 100\u2013120 BPM.<\/div>\n      <\/div>\n\n      <!-- Duration -->\n      <div class=\"eco-tool__field\">\n        <label class=\"eco-tool__label\" for=\"eco-cpr-duration-2c6d9\">Session length<\/label>\n        <select class=\"eco-tool__input\" id=\"eco-cpr-duration-2c6d9\">\n          <option value=\"30\">30 seconds<\/option>\n          <option value=\"60\" selected>1 minute<\/option>\n          <option value=\"120\">2 minutes<\/option>\n          <option value=\"180\">3 minutes<\/option>\n          <option value=\"300\">5 minutes<\/option>\n          <option value=\"0\">Custom\u2026<\/option>\n        <\/select>\n        <div class=\"eco-tool__hint\">Pick a preset or set custom below.<\/div>\n      <\/div>\n\n      <!-- Custom duration -->\n      <div class=\"eco-tool__field eco-tool__hidden\" id=\"eco-cpr-custom-wrap-2c6d9\">\n        <label class=\"eco-tool__label\" for=\"eco-cpr-custom-2c6d9\">Custom length (seconds)<\/label>\n        <input class=\"eco-tool__input\" id=\"eco-cpr-custom-2c6d9\" type=\"number\" min=\"10\" max=\"1800\" step=\"5\" value=\"90\" inputmode=\"numeric\" \/>\n        <div class=\"eco-tool__hint\">Example: 90 seconds.<\/div>\n      <\/div>\n\n      <!-- Sound -->\n      <div class=\"eco-tool__field\">\n        <label class=\"eco-tool__label\" for=\"eco-cpr-sound-2c6d9\">Metronome sound<\/label>\n        <select class=\"eco-tool__input\" id=\"eco-cpr-sound-2c6d9\">\n          <option value=\"off\" selected>Off (visual only)<\/option>\n          <option value=\"beep\">Beep (simple)<\/option>\n          <option value=\"click\">Click (soft)<\/option>\n        <\/select>\n        <div class=\"eco-tool__hint\">Sound starts only after pressing Start.<\/div>\n      <\/div>\n\n      <!-- Count method -->\n      <div class=\"eco-tool__field\">\n        <label class=\"eco-tool__label\" for=\"eco-cpr-count-2c6d9\">Count display<\/label>\n        <select class=\"eco-tool__input\" id=\"eco-cpr-count-2c6d9\">\n          <option value=\"beats\" selected>Beats count<\/option>\n          <option value=\"cycles\">30-compression cycles<\/option>\n        <\/select>\n        <div class=\"eco-tool__hint\">Useful for practice routines.<\/div>\n      <\/div>\n\n     \n      <\/div>\n    <\/div>\n\n    <div class=\"eco-tool__actions\">\n      <button type=\"button\" class=\"wp-element-button eco-tool__btn\" id=\"eco-cpr-start-2c6d9\">Start<\/button>\n      <button type=\"button\" class=\"wp-element-button eco-tool__btn eco-tool__btn--ghost\" id=\"eco-cpr-stop-2c6d9\" disabled>Stop<\/button>\n      <button type=\"button\" class=\"wp-element-button eco-tool__btn eco-tool__btn--ghost\" id=\"eco-cpr-reset-2c6d9\">Reset<\/button>\n      <div class=\"eco-tool__error\" id=\"eco-cpr-error-2c6d9\" aria-live=\"polite\"><\/div>\n    <\/div>\n  <\/form>\n\n  <div class=\"eco-tool__result\" id=\"eco-cpr-result-2c6d9\">\n    <h3 class=\"eco-tool__subtitle\">Session<\/h3>\n\n    <div class=\"eco-tool__cards\">\n      <div class=\"eco-tool__card\">\n        <div class=\"eco-tool__metric-label\">Tempo<\/div>\n        <div class=\"eco-tool__metric-value\" id=\"eco-cpr-tempo-2c6d9\">110 BPM<\/div>\n        <div class=\"eco-tool__metric-sub\" id=\"eco-cpr-interval-2c6d9\">Beat every 0.55s<\/div>\n      <\/div>\n\n      <div class=\"eco-tool__card\">\n        <div class=\"eco-tool__metric-label\">Time left<\/div>\n        <div class=\"eco-tool__metric-value\" id=\"eco-cpr-left-2c6d9\">1:00<\/div>\n        <div class=\"eco-tool__metric-sub\" id=\"eco-cpr-status-2c6d9\">Ready<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"eco-tool__card eco-tool__card--wide\">\n      <div class=\"eco-tool__metric-label\">Count<\/div>\n      <div class=\"eco-tool__bigcount\" id=\"eco-cpr-countval-2c6d9\">0<\/div>\n      <div class=\"eco-tool__metric-sub\" id=\"eco-cpr-countsub-2c6d9\">beats<\/div>\n\n      <div class=\"eco-tool__pulsewrap\" aria-hidden=\"true\">\n        <div class=\"eco-tool__pulse\" id=\"eco-cpr-pulse-2c6d9\"><\/div>\n      <\/div>\n    <\/div>\n\n    <p class=\"eco-tool__note\">\n      Training aid only. In a real emergency, call local emergency services and follow dispatcher instructions.\n    <\/p>\n  <\/div>\n\n  <details class=\"eco-tool__details\">\n    <summary class=\"eco-tool__summary\">How it works<\/summary>\n    <div class=\"eco-tool__details-body\">\n      <p class=\"eco-tool__text\">\n        The trainer uses a simple metronome. Set BPM, choose duration, then press Start.\n        The visual pulse marks each beat; optional sound is generated in the browser.\n      <\/p>\n    <\/div>\n  <\/details>\n<\/div>\n\n<style>\n.eco-tool{ border: 1px solid rgba(0,0,0,.12); border-radius: 12px; padding: 16px; }\n.eco-tool__header{ margin-bottom: 12px; }\n.eco-tool__title{ margin: 0 0 8px; }\n.eco-tool__lead{ margin: 0; opacity: .9; }\n.eco-tool__form{ margin-top: 12px; }\n\n.eco-tool__grid3{ display: grid; grid-template-columns: 1fr; gap: 16px; }\n@media (min-width: 860px){ .eco-tool__grid3{ grid-template-columns: 1fr 1fr 1fr; } }\n\n.eco-tool__field{ display: flex; flex-direction: column; gap: 6px; }\n.eco-tool__label{ font-weight: 600; }\n\n.eco-tool__input{\n  width: 100%;\n  height: 44px;\n  padding: 0 12px;\n  border: 1px solid rgba(0,0,0,.20);\n  border-radius: 10px;\n  background: #fff;\n  box-sizing: border-box;\n  font: inherit;\n}\n.eco-tool select.eco-tool__input{\n  appearance: none;\n  -webkit-appearance: none;\n  line-height: 44px;\n  padding-right: 40px;\n  background-image:\n    linear-gradient(45deg, transparent 50%, rgba(0,0,0,.60) 50%),\n    linear-gradient(135deg, rgba(0,0,0,.60) 50%, transparent 50%);\n  background-position:\n    calc(100% - 18px) 50%,\n    calc(100% - 12px) 50%;\n  background-size: 6px 6px;\n  background-repeat: no-repeat;\n}\n.eco-tool__static{\n  display: flex;\n  align-items: center;\n  background: rgba(0,0,0,.02);\n}\n\n.eco-tool__hint{ font-size: .92em; opacity: .78; min-height: 38px; }\n\n.eco-tool__actions{ display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 16px; }\n.eco-tool__btn{ padding: 10px 22px; }\n\n.eco-tool__btn--ghost{ background: transparent !important; border: 1px solid rgba(0,0,0,.20) !important; }\n.eco-tool__btn--ghost:hover,.eco-tool__btn--ghost:focus{ background: rgba(0,0,0,.06) !important; border-color: rgba(0,0,0,.35) !important; }\n.eco-tool__btn:disabled{ opacity: .5; cursor: not-allowed; }\n\n.eco-tool__error{ min-height: 1.2em; font-weight: 600; flex: 1 1 240px; }\n\n.eco-tool__result{ margin-top: 16px; }\n.eco-tool__subtitle{ margin: 0 0 10px; }\n\n.eco-tool__cards{ display: grid; gap: 10px; grid-template-columns: 1fr; }\n@media (min-width: 860px){ .eco-tool__cards{ grid-template-columns: 1fr 1fr; } }\n\n.eco-tool__card{ border: 1px solid rgba(0,0,0,.12); border-radius: 12px; padding: 12px; }\n.eco-tool__card--wide{ margin-top: 10px; }\n\n.eco-tool__metric-label{ opacity: .85; font-weight: 600; }\n.eco-tool__metric-value{ font-size: 1.6em; font-weight: 800; margin-top: 6px; line-height: 1.1; }\n.eco-tool__metric-sub{ opacity: .85; margin-top: 6px; }\n\n.eco-tool__bigcount{\n  font-size: 3.0em;\n  font-weight: 900;\n  line-height: 1;\n  margin-top: 10px;\n}\n\n.eco-tool__pulsewrap{\n  margin-top: 12px;\n  height: 12px;\n  border: 1px solid rgba(0,0,0,.12);\n  border-radius: 999px;\n  background: rgba(0,0,0,.03);\n  overflow: hidden;\n}\n.eco-tool__pulse{\n  height: 100%;\n  width: 0%;\n  background: rgba(0,0,0,.25);\n  transition: width 80ms linear;\n}\n\n.eco-tool__note{ margin: 10px 0 0; opacity: .9; }\n\n.eco-tool__details{ margin-top: 14px; }\n.eco-tool__summary{ cursor: pointer; font-weight: 700; }\n.eco-tool__details-body{ margin-top: 10px; }\n.eco-tool__text{ margin: 0 0 10px; }\n\n.eco-tool__hidden{ display: none !important; }\n<\/style>\n\n<script>\n(function(){\n  const S = \"2c6d9\";\n  const el = (id) => document.getElementById(id + \"-\" + S);\n\n  const bpmEl = el(\"eco-cpr-bpm\");\n  const durationEl = el(\"eco-cpr-duration\");\n  const customWrap = el(\"eco-cpr-custom-wrap\");\n  const customEl = el(\"eco-cpr-custom\");\n  const soundEl = el(\"eco-cpr-sound\");\n  const countEl = el(\"eco-cpr-count\");\n\n  const startBtn = el(\"eco-cpr-start\");\n  const stopBtn = el(\"eco-cpr-stop\");\n  const resetBtn = el(\"eco-cpr-reset\");\n  const errorEl = el(\"eco-cpr-error\");\n\n  const tempoEl = el(\"eco-cpr-tempo\");\n  const intervalEl = el(\"eco-cpr-interval\");\n  const leftEl = el(\"eco-cpr-left\");\n  const statusEl = el(\"eco-cpr-status\");\n  const countValEl = el(\"eco-cpr-countval\");\n  const countSubEl = el(\"eco-cpr-countsub\");\n  const pulseEl = el(\"eco-cpr-pulse\");\n\n  let timerId = null;\n  let tickId = null;\n  let endAt = 0;\n  let beats = 0;\n\n  \/\/ WebAudio (created only after user action)\n  let audioCtx = null;\n\n  function setError(msg){ errorEl.textContent = msg || \"\"; }\n  function clamp(n,a,b){ return Math.max(a, Math.min(b,n)); }\n\n  function showCustom(){\n    customWrap.classList.toggle(\"eco-tool__hidden\", durationEl.value !== \"0\");\n  }\n\n  function getDurationSec(){\n    if (durationEl.value === \"0\") return Number(customEl.value);\n    return Number(durationEl.value);\n  }\n\n  function fmtTime(sec){\n    sec = Math.max(0, Math.floor(sec));\n    const m = Math.floor(sec\/60);\n    const s = sec % 60;\n    return `${m}:${String(s).padStart(2,\"0\")}`;\n  }\n\n  function beatIntervalMs(bpm){\n    return 60000 \/ bpm;\n  }\n\n  function updateMeta(){\n    const bpm = Number(bpmEl.value);\n    const interval = beatIntervalMs(bpm) \/ 1000;\n    tempoEl.textContent = `${Math.round(bpm)} BPM`;\n    intervalEl.textContent = `Beat every ${interval.toFixed(2)}s`;\n  }\n\n  function setButtons(running){\n    startBtn.disabled = running;\n    stopBtn.disabled = !running;\n  }\n\n  function pulse(){\n    pulseEl.style.width = \"100%\";\n    setTimeout(() => { pulseEl.style.width = \"0%\"; }, 90);\n  }\n\n  function playSound(type){\n    if (type === \"off\") return;\n\n    if (!audioCtx){\n      audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n    }\n    const t = audioCtx.currentTime;\n\n    const osc = audioCtx.createOscillator();\n    const gain = audioCtx.createGain();\n\n    if (type === \"beep\"){\n      osc.frequency.value = 880;\n      gain.gain.setValueAtTime(0.0001, t);\n      gain.gain.exponentialRampToValueAtTime(0.15, t + 0.01);\n      gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.08);\n      osc.type = \"sine\";\n    } else {\n      osc.frequency.value = 220;\n      gain.gain.setValueAtTime(0.0001, t);\n      gain.gain.exponentialRampToValueAtTime(0.08, t + 0.005);\n      gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.05);\n      osc.type = \"square\";\n    }\n\n    osc.connect(gain);\n    gain.connect(audioCtx.destination);\n    osc.start(t);\n    osc.stop(t + 0.09);\n  }\n\n  function updateCount(){\n    if (countEl.value === \"beats\"){\n      countValEl.textContent = String(beats);\n      countSubEl.textContent = \"beats\";\n    } else {\n      const cycles = Math.floor(beats \/ 30);\n      const inCycle = beats % 30;\n      countValEl.textContent = String(inCycle);\n      countSubEl.textContent = `in current set \u00b7 completed sets: ${cycles}`;\n    }\n  }\n\n  function start(){\n    setError(\"\");\n\n    const bpm = Number(bpmEl.value);\n    const dur = Number(getDurationSec());\n\n    if (!Number.isFinite(bpm) || bpm < 60 || bpm > 180){\n      setError(\"Please enter a valid BPM between 60 and 180.\");\n      return;\n    }\n    if (!Number.isFinite(dur) || dur < 10 || dur > 1800){\n      setError(\"Please enter a valid duration (10\u20131800 seconds).\");\n      return;\n    }\n\n    beats = 0;\n    updateCount();\n    updateMeta();\n\n    endAt = Date.now() + dur * 1000;\n    statusEl.textContent = \"Running\";\n    setButtons(true);\n\n    const intervalMs = beatIntervalMs(bpm);\n\n    \/\/ Beat loop\n    tickId = setInterval(() => {\n      beats += 1;\n      pulse();\n      playSound(soundEl.value);\n      updateCount();\n    }, intervalMs);\n\n    \/\/ Timer loop\n    timerId = setInterval(() => {\n      const left = Math.max(0, Math.ceil((endAt - Date.now()) \/ 1000));\n      leftEl.textContent = fmtTime(left);\n      if (left <= 0){\n        stop(true);\n      }\n    }, 200);\n\n    leftEl.textContent = fmtTime(dur);\n  }\n\n  function stop(finished){\n    if (tickId) clearInterval(tickId);\n    if (timerId) clearInterval(timerId);\n    tickId = null;\n    timerId = null;\n\n    setButtons(false);\n    statusEl.textContent = finished ? \"Done\" : \"Stopped\";\n  }\n\n  function reset(){\n    setError(\"\");\n    bpmEl.value = \"110\";\n    durationEl.value = \"60\";\n    customEl.value = \"90\";\n    soundEl.value = \"off\";\n    countEl.value = \"beats\";\n\n    showCustom();\n    updateMeta();\n\n    beats = 0;\n    updateCount();\n    leftEl.textContent = \"1:00\";\n    statusEl.textContent = \"Ready\";\n    pulseEl.style.width = \"0%\";\n\n    stop(false);\n  }\n\n  \/\/ init\n  showCustom();\n  updateMeta();\n  updateCount();\n\n  durationEl.addEventListener(\"change\", showCustom);\n  bpmEl.addEventListener(\"input\", updateMeta);\n\n  startBtn.addEventListener(\"click\", start);\n  stopBtn.addEventListener(\"click\", () => stop(false));\n  resetBtn.addEventListener(\"click\", reset);\n})();\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>CPR Compression Rate Trainer Train a steady chest-compression rhythm (informational training aid). For real emergencies, follow local guidance and call emergency services. Target rate (BPM) Common training range: 100\u2013120 BPM.&hellip;<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":2462,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_sitemap_exclude":false,"_sitemap_priority":"","_sitemap_frequency":"","footnotes":""},"_links":{"self":[{"href":"https:\/\/bio-me.bio\/index.php?rest_route=\/wp\/v2\/pages\/2631"}],"collection":[{"href":"https:\/\/bio-me.bio\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/bio-me.bio\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/bio-me.bio\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/bio-me.bio\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2631"}],"version-history":[{"count":2,"href":"https:\/\/bio-me.bio\/index.php?rest_route=\/wp\/v2\/pages\/2631\/revisions"}],"predecessor-version":[{"id":2633,"href":"https:\/\/bio-me.bio\/index.php?rest_route=\/wp\/v2\/pages\/2631\/revisions\/2633"}],"up":[{"embeddable":true,"href":"https:\/\/bio-me.bio\/index.php?rest_route=\/wp\/v2\/pages\/2462"}],"wp:attachment":[{"href":"https:\/\/bio-me.bio\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2631"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}