pico-os-app 1 1194 api-test 1.0.0 動作確認 動作確認 45266 71e5655523d59486d0f8b6b4fa6c539c6752cbd11bd99d4907160f5f77f938b9 notify,background,microphone Lua APIの動作確認をまとめたアプリ。部品・描画・画面の受け渡し・コントローラー・音・通知・マイクを1画面ずつ試せる cdae7ba29d76143cb10ee4c5bf4c9c78c3a55dfd5d6cd0df25ff0cbacad30c5d kimu1109 - ed25519 cdae7ba29d76143cb10ee4c5bf4c9c78c3a55dfd5d6cd0df25ff0cbacad30c5d d2ca58aaa17093c273aaf8d17c2ae1cd75a96602b6cfeb9222c122e0e2d27141fe98eb925255eef86f50c06a62f1a708c906293b322457f47fbb0f5ab77cda01 app.cfg 583 91467a89dd147ddb969f4cc5e25597f5f0ff849c0d2a84f2a62b01d06960ec8b background.lua 1180 30814181ef3d7830429ca7ece8f0bad15b01805f94c0b6c2103fef7992ea5f3c basics.lua 5761 7dd8508e22089a3fe3b837d5abc38c2db2b26cb1f357da59b08509583350a1bc demo.mml 766 4f4a43696b9da2adcf3c804b292967c28303c40f2a350549cf8e294b85b031bb draw.lua 3183 a889aa58c97f74538803bf35447e609a560f4e1e846cff7a91a58d46ce7ec693 extra.lua 6320 9399e76b28a320f9514a5f28bf6a3d3389ab9b6ca856bb12eda740df4ed61f8d icon.pimg 139 31d06e450dc126c9778e7e46cd8bba0d930359f7c758f15b4ad93ba5cc6f137e main.lua 2602 158d08ce9a1e74647a4604f4e16c5faad8c0cd7f835772d03343b88bb1ddcad2 mic.lua 4686 626247bb01c1cdde991a252dabc14d16f6e772d58529142bef768bdcb4742b30 notify.lua 3241 1e879f440da6ada2e3d61776e6fff8bc8a037a2cc3f5f53ce55542e0dc57314c pad.lua 5081 aa7d799e88b5fcd23a73c3d0d14e11297c6cfe2eb16e058150ae6587cce41c15 picker.lua 1036 a7ce75b832284ceb727b6855d038fd69bb030ee55d432c3c13f17aa0490b751a scene.lua 4294 5bf04c4f847a1e5be41c5d74a4597233d49650c953cedbc4b2817aad4878f1c7 sound.lua 6108 9828f952194d80d8b09bfbcf892fa484d48cbdaea38e0e094c9ce2e6fd8682d8 util.lua 286 e17df345a6cdde662c38a19816ae411338f19fdfbb62dd764e0b6f66c019299c name=動作確認 id=api-test description=Lua APIの動作確認をまとめたアプリ。部品・描画・画面の受け渡し・コントローラー・音・通知・マイクを1画面ずつ試せる version=1.0.0 icon=icon.pimg permission_network=false permission_sd_outside_app_dir=false permission_notify=true # マイクのページ(mic.lua)で聞く・録音する permission_microphone=true # 閉じている間も60秒ごとに background.lua を動かす(通知のページの「BG通知」) permission_background=true background=background.lua background_interval=60 -- バックグラウンドスクリプトの動作確認(app.cfg の background)。 -- アプリを閉じている間も background_interval(60秒)ごとに、小さな使い捨てのエンジンで上から1回動く。 -- 画面の pico.*(create 等)は無い。回をまたぐ値は settings_* で持ち越す(グローバル変数は残らない)。 -- 通知のページの「BG通知」を入れておくと、動くたびに通知を出す。 local runs = tonumber(pico.settings_get("bg_runs") or "0") + 1 local t = pico.get_time() pico.settings_set("bg_runs", tostring(runs)) pico.settings_set("bg_last", string.format("%02d:%02d:%02d", t.hour, t.min, t.sec)) pico.log("動作確認(バックグラウンド): " .. runs .. "回目 is_background=" .. tostring(pico.is_background()) .. " create=" .. tostring(pico.create)) if pico.settings_get("bg_notify") == "true" then -- タイマーが残っている間はエンジンが生きている(1秒後に出して終わる) pico.after(1000, function() pico.notify{ title = "バックグラウンド", body = runs .. "回目に動きました", tag = "bg", data = "bg", sound = false } end) end -- 基本の部品: pico.create で作る汎用部品と、その固有イベントの確認。 -- 「入力」タブ … Textbox(text_changed) / NumberInput / Checkbox(checked_changed) / -- DropdownMenu(list_add + dropdown_changed) / LayoutContainer(add_child / remove_child) / pico.beep -- 「図形」タブ … 図形ウィジェット(Rect / Ellipse / Line / Triangle)。Canvasと違い、置けば値が変わるまで残る -- タブごとの表示の切り替えは pico.tab_link に任せる。 local x, y, w, h = pico.content_rect() local m = 8 local top = y + 32 local function label(text, lx, ly) local l = pico.create("Label") pico.set(l, "x", lx) pico.set(l, "y", ly) pico.set(l, "font_size", 0) pico.set(l, "text", text) return l end local function button(text, bx, by, bw, fn) local b = pico.create("Button") pico.set(b, "font_size", 0) pico.set(b, "text", text) pico.set(b, "x", bx) pico.set(b, "y", by) pico.set(b, "w", bw) pico.set(b, "h", 30) pico.on(b, "press_end", fn) return b end local tabs = pico.create("TabBar") pico.set(tabs, "x", x) pico.set(tabs, "y", y) pico.set(tabs, "w", w) pico.set(tabs, "h", 26) pico.tab_add(tabs, "入力") pico.tab_add(tabs, "図形") local function link(index, ...) for _, id in ipairs({ ... }) do pico.tab_link(tabs, index, id) end end -- ---- 入力 ---- local textbox = pico.create("Textbox") pico.set(textbox, "x", x + m) pico.set(textbox, "y", top) pico.set(textbox, "max_width", w - m * 2) pico.set(textbox, "max_height", 26) pico.set(textbox, "is_single_line", true) pico.set(textbox, "placeholder", "タップして入力") local text_label = label("text: (未入力)", x + m, top + 30) pico.on(textbox, "text_changed", function(id) pico.set(text_label, "text", "text: " .. pico.get(id, "text")) end) local number = pico.create("NumberInput") pico.set(number, "x", x + m) pico.set(number, "y", top + 54) local number_label = label("数: ", x + m + 72, top + 58) local checkbox = pico.create("Checkbox") pico.set(checkbox, "x", x + m) pico.set(checkbox, "y", top + 126) pico.set(checkbox, "text", "チェック") local check_label = label("checked: false", x + 130, top + 130) pico.on(checkbox, "checked_changed", function(id) pico.set(check_label, "text", "checked: " .. tostring(pico.get(id, "checked"))) end) -- コンテナへの出し入れ: 子を作って足し、もう一度押すと破棄せず外してから消す local container = pico.create("LayoutContainer") pico.set(container, "x", x + m) pico.set(container, "y", top + 158) pico.set(container, "w", w - m * 2) pico.set(container, "h", 24) local child = nil local toggle toggle = button("子を追加", x + m, top + 186, 110, function() if child == nil then child = pico.create("Label") pico.set(child, "text", "追加された子") pico.add_child(container, child) pico.set(toggle, "text", "子を外す") else pico.remove_child(container, child) pico.destroy(child) child = nil pico.set(toggle, "text", "子を追加") end end) local beep = button("テスト音", x + 130, top + 186, 100, function() pico.beep(880, 300) end) -- ---- 図形 ---- local shapes = {} local function shape(kind, props) local s = pico.create(kind) for k, v in pairs(props) do pico.set(s, k, v) end shapes[#shapes + 1] = s return s end shape("Rect", { x = x + m, y = top + 8, w = 50, h = 34, color = 9 }) shape("Rect", { x = x + m + 60, y = top + 8, w = 50, h = 34, color = 12, filled = false, thickness = 3 }) shape("Ellipse", { x = x + m + 120, y = top + 8, w = 34, h = 34, color = 10 }) shape("Ellipse", { x = x + m + 164, y = top + 10, w = 46, h = 30, color = 5, filled = false }) local line = shape("Line", { x1 = x + m, y1 = top + 60, x2 = x + m + 60, y2 = top + 90, color = 0, thickness = 3 }) local tri = shape("Triangle", { x1 = x + m + 80, y1 = top + 90, x2 = x + m + 100, y2 = top + 60, x3 = x + m + 120, y3 = top + 90, color = 14 }) shape("Triangle", { x1 = x + m + 140, y1 = top + 90, x2 = x + m + 160, y2 = top + 60, x3 = x + m + 180, y3 = top + 90, color = 8, filled = false, thickness = 2 }) -- x/y は形全体の平行移動 local moved = false local move = button("x/yで平行移動", x + m, top + 120, 140, function() local dy = moved and -20 or 20 pico.set(line, "y", pico.get(line, "y") + dy) pico.set(tri, "y", pico.get(tri, "y") + dy) moved = not moved end) -- ---- 戻る ---- local back = button("", x + m, y + h - 34, 80, function() pico.pop() end) pico.set(back, "icon_id", 30) -- ArrowLeft -- 開いた一覧が下の部品に重なるので、ドロップダウンは最後に作る(後に作ったものが上に描かれる) local dropdown = pico.create("DropdownMenu") pico.set(dropdown, "x", x + m) pico.set(dropdown, "y", top + 86) pico.set(dropdown, "w", 120) pico.list_add(dropdown, "りんご") pico.list_add(dropdown, "みかん") pico.list_add(dropdown, "ぶどう") local dropdown_label = label("未選択", x + 140, top + 94) pico.on(dropdown, "dropdown_changed", function(id) pico.set(dropdown_label, "text", "選択: " .. pico.get(id, "selected_index")) end) link(0, textbox, text_label, number, number_label, checkbox, check_label, container, toggle, beep, dropdown, dropdown_label) link(1, move, table.unpack(shapes)) -- NumberInput は変化のイベントを持たないので、毎フレーム読んで変わったら出す local last_num = nil function loop() local n = pico.get(number, "text") if n ~= last_num then last_num = n pico.set(number_label, "text", "数: " .. n) end end ; pico-os MML のサンプル(docs/MUSIC_FORMAT.md)。「動作確認」アプリの「音」のデモ曲と同じもの #title デモ曲 #composer pico-os #tempo 200 ; ドラム。マクロの中で変えた波形や音量は、抜けると元に戻る #macro k = @noise o6 v13 E-1 q8 c16 r16 #macro h = @noise_short o8 v4 q8 c32 r16. #macro s = @noise o8 v10 E-1 q8 c8 A @pulse25 v12 E-6 o5 l4 L e g a g8 e8 | d e c2 | e g a > c < | b g a2 B @pulse12 v6 E-2 o4 l8 L c e g e < a > c e c | < f a > c < a g b > d < b > B c e g e < a > c e c | < f a g b > c e g e C @triangle v15 o3 l4 L c c < a a > | < f f g g > | c c < a a > | < f g > c2 D L [ $k $h $s $h ]8 ; 8分×4 = 1小節を8回 -- 直接描画: Canvasのrenderコールバックで描く描画API・入力APIの確認。 -- pico.draw_ellipse/fill_ellipse/draw_triangle/fill_triangle/draw_polygon/fill_polygon/draw_arc/fill_arc -- pico.draw_line(…, width) / pico.text_width / pico.draw_image_ex / pico.on_key / pico.battery / pico.millis -- 「戻る」ボタン以外は全部Canvasのrenderコールバックの中で描く(直接描画の約束)。 local cx0, cy0, cw, ch = pico.content_rect() local back = pico.create("Button") pico.set(back, "x", cx0 + 4) pico.set(back, "y", cy0 + 2) pico.set(back, "icon_id", 30) -- ArrowLeft pico.on(back, "press_end", function() pico.pop() end) local canvas = pico.create("Canvas") pico.set(canvas, "x", cx0) pico.set(canvas, "y", cy0 + 34) pico.set(canvas, "w", cw) pico.set(canvas, "h", ch - 34) local last_key = "(キー未入力)" local angle = 0 local t0 = pico.millis() pico.on_key(function(key, mods) last_key = key .. (mods.ctrl and " +Ctrl" or "") .. (mods.shift and " +Shift" or "") pico.invalidate(canvas) return true end) local img = pico.image_load(pico.path_join(pico.app_dir(), "icon.pimg")) pico.on(canvas, "render", function() local x, y = cx0, cy0 + 34 -- 太い線と輪郭 pico.draw_line(x + 8, y + 8, x + 70, y + 30, 0, 1) pico.draw_line(x + 8, y + 18, x + 70, y + 40, 12, 4) pico.draw_line(x + 8, y + 28, x + 70, y + 50, 9, 8) -- 楕円 pico.fill_ellipse(x + 110, y + 28, 28, 16, 10) pico.draw_ellipse(x + 110, y + 28, 36, 22, 0) -- 三角形 pico.fill_triangle(x + 170, y + 50, x + 200, y + 8, x + 228, y + 50, 5) pico.draw_triangle(x + 170, y + 50, x + 200, y + 8, x + 228, y + 50, 0, 2) -- 多角形(凹んだ星) local star = {} for i = 0, 9 do local r = (i % 2 == 0) and 30 or 13 local a = -math.pi / 2 + i * math.pi / 5 star[#star + 1] = math.floor(x + 40 + r * math.cos(a)) star[#star + 1] = math.floor(y + 110 + r * math.sin(a)) end pico.fill_polygon(star, 11) pico.draw_polygon(star, 0, 2) -- 円弧と扇形(パイチャート) pico.fill_arc(x + 130, y + 110, 30, 0, math.pi * 0.8, 12) pico.fill_arc(x + 130, y + 110, 30, math.pi * 0.8, math.pi * 1.5, 9) pico.fill_arc(x + 130, y + 110, 30, math.pi * 1.5, math.pi * 2, 10) pico.draw_arc(x + 130, y + 110, 36, 0, math.pi * 1.5, 0, 3) -- 画像の拡大・回転・反転 if img then local w, h = pico.image_size(img) pico.draw_image_ex(img, x + 40, y + 190, 0, 1, 1, w / 2, h / 2) pico.draw_image_ex(img, x + 100, y + 190, angle, 1.5, 1.5, w / 2, h / 2) pico.draw_image_ex(img, x + 170, y + 190, 0, -1, 1, w / 2, h / 2) pico.draw_image_ex(img, x + 215, y + 190, 0, 0.5, 2, w / 2, h / 2) end -- 文字: 右寄せ local label = "キー: " .. last_key pico.draw_text(x + cw - pico.text_width(label, 0) - 4, y + 248, label, 0, 0) local p, v, usb = pico.battery() if p then pico.draw_text(x + 4, y + 232, string.format("電池%d%% %.2fV%s", p, v, usb and " USB" or ""), 0, 0) end end) function loop(dt) angle = (pico.millis() - t0) / 1000 pico.invalidate(canvas) end -- 追加の部品: 専用ウィジェットと補助モジュールの確認 -- ・Luaから作れるようになったウィジェット(ProgressBar / AnalogClock / DurationPicker / MonthGrid / -- MarkdownView / TextView / ImageView) -- ・pico.ui(宣言的な組み立て)・pico.tab_link(タブごとの表示切り替え)・pico.tween -- ・ジェスチャー(swipe / long_press / double_tap)・pico.toast・pico.show_date・pico.encrypt local ui = require("pico.ui") local tween = require("pico.tween") local x, y, w, h = pico.content_rect() local top = y + 32 local panel_h = h - 32 - 36 -- ---- タブ ---- local tabs = pico.create("TabBar") pico.set(tabs, "x", x) pico.set(tabs, "y", y) pico.set(tabs, "w", w) pico.set(tabs, "h", 26) for _, name in ipairs({ "表示", "文書", "日付", "暗号" }) do pico.tab_add(tabs, name) end local function link(index, ...) for _, id in ipairs({ ... }) do pico.tab_link(tabs, index, id) end end -- ---- タブ0 表示: ProgressBar / AnalogClock / DurationPicker / ImageView ---- local bar_label = ui.Label{ x = x + 8, y = top, font_size = 0, text = "ProgressBar" } local bar = ui.ProgressBar{ x = x + 8, y = top + 20, w = w - 16, h = 14, max_value = 100, color = 10 } local clock = ui.AnalogClock{ x = x + 8, y = top + 44, w = 84, h = 84 } local picker = ui.DurationPicker{ x = x + 100, y = top + 44, w = 132, h = 60, total_ms = 90000, name = "picker", font_size = 0, on_duration_changed = function(id, ms) pico.set(bar, "value", math.min(100, ms // 1000)) end } local pic_label = ui.Label{ x = x + 100, y = top + 108, font_size = 0, text = "ImageView" } local pic = ui.ImageView{ x = x + 100, y = top + 126, w = 60, h = 40, path = pico.path_join(pico.app_dir(), "icon.pimg") } local fill = ui.Button{ x = x + 8, y = top + 140, font_size = 0, text = "ゲージ", w = 84, h = 28, on_press_end = function() tween.start{ from = 0, to = 100, duration = 800, ease = "ease_out", on_update = function(v) pico.set(bar, "value", v) end, on_done = function() pico.toast("おわり") end } end } link(0, bar_label, bar, clock, picker, pic_label, pic, fill) -- 時計は1秒ごとに現在時刻を流し込む pico.every(1000, function() local t = pico.get_time() pico.set(clock, "hour", t.hour) pico.set(clock, "minute", t.min) pico.set(clock, "second", t.sec) end) -- ---- タブ1 文書: MarkdownView ---- local md = ui.MarkdownView{ x = x + 4, y = top, w = w - 8, h = panel_h, on_link_tap = function(id, path) pico.toast("リンク: " .. path) end } pico.text_set(md, [[ # MarkdownView Luaから作れるようになった。 - **太字**や*斜体* - 長い文書も渡せる - [リンク](docs/a.md) ## TextView 見えている行だけを描く軽い表示欄も作れる。 ]]) link(1, md) -- ---- タブ2 日付: MonthGrid ---- local today = pico.get_time() local year, month = today.year, today.month local month_label = ui.Label{ x = x + 8, y = top, font_size = 0, text = "" } local grid = ui.MonthGrid{ x = x + 8, y = top + 28, w = w - 16, h = 136, today = today.day, selected = today.day, on_day_selected = function(id, day) pico.set(month_label, "text", string.format("%d年%d月%d日", year, month, day)) end } local function show_month() pico.set(grid, "year", year) pico.set(grid, "month", month) pico.set(grid, "today", (year == today.year and month == today.month) and today.day or 0) pico.set_dots(grid, { [3] = { 12 }, [10] = { 9, 10 }, [20] = { 12, 9, 10 } }) pico.set(month_label, "text", string.format("%d年%d月", year, month)) end local function shift(d) month = month + d if month < 1 then month = 12; year = year - 1 elseif month > 12 then month = 1; year = year + 1 end show_month() end pico.on(grid, "swipe", function(id, dir) if dir == "left" then shift(1) elseif dir == "right" then shift(-1) end end) pico.on(grid, "long_press", function() pico.toast("長押し") end) pico.on(grid, "double_tap", function() year, month = today.year, today.month; show_month() end) local pick_date = ui.Button{ x = x + 8, y = top + 172, w = 150, h = 28, font_size = 0, text = "日付ダイアログ", on_press_end = function() local d = pico.show_date("日付を選ぶ", year, month, 1) pico.on(d, "closed", function(_, ok, v) if ok then pico.toast(v) end end) end } show_month() link(2, month_label, grid, pick_date) local hint = ui.Label{ x = x + 8, y = top + 204, font_size = 0, max_width = w - 16, text = "左右にスワイプで月を送る\nダブルタップで今月" } link(2, hint) -- ---- タブ3 暗号: encrypt / decrypt ---- local input = ui.Textbox{ x = x + 8, y = top, max_width = w - 16, placeholder = "ここに文字を入れる" } local out = ui.TextView{ x = x + 8, y = top + 80, w = w - 16, h = 110 } local PASSWORD = "demo-password" local last_cipher = nil local enc_btn = ui.Button{ x = x + 8, y = top + 38, w = 100, h = 28, font_size = 0, text = "暗号化", on_press_end = function() local text = pico.get(input, "text") or "" local c, why = pico.encrypt(text, PASSWORD) if not c then pico.toast(tostring(why)) return end last_cipher = c pico.text_set(out, c) end } local dec_btn = ui.Button{ x = x + 124, y = top + 38, w = 100, h = 28, font_size = 0, text = "復号", on_press_end = function() if not last_cipher then pico.toast("先に暗号化") return end local p, why = pico.decrypt(last_cipher, PASSWORD) pico.text_set(out, p and ("復号: " .. p) or ("失敗: " .. tostring(why))) end } local bad_btn = ui.Button{ x = x + 8, y = top + 196, w = 150, h = 28, font_size = 0, text = "違うパスワード", on_press_end = function() if not last_cipher then pico.toast("先に暗号化") return end local p, why = pico.decrypt(last_cipher, "wrong") pico.text_set(out, p and ("復号: " .. p) or ("失敗: " .. tostring(why))) end } link(3, input, out, enc_btn, dec_btn, bad_btn) -- ---- 戻る ---- local back = ui.Button{ x = x + 8, y = y + h - 30, w = 80, h = 26, font_size = 0, icon_id = 30, on_press_end = function() pico.go_back() end } pico.on_back(function() pico.pop() end) pico.tab_link(tabs, 0, bar) -- 同じ連動を重ねても1つ 00ÿ ÿ • . + ( ) ) ) ( ( ( )           !  #  $ & ' ) +  ÿ ÿ [ -- 動作確認: Lua APIの動作確認用の画面をまとめたアプリ。 -- このメニューから各ページ(同じフォルダの *.lua)を pico.push_scene で開き、 -- 各ページの「戻る」(pico.pop)でここへ戻る。権限(app.cfg)は開いたページへ引き継がれる。 -- -- basics.lua … 基本の部品(TabBar / DropdownMenu / Textbox / NumberInput / Checkbox / 図形 / コンテナの出し入れ / テスト音) -- extra.lua … 追加の部品(ProgressBar / AnalogClock / DurationPicker / MonthGrid / MarkdownView / TextView / ImageView、 -- pico.ui / pico.tween / ジェスチャー / 暗号化) -- draw.lua … Canvasへの直接描画(図形・太線・画像の拡大/回転/反転・文字幅・on_key・電池) -- scene.lua … require / タッチ座標 / 画面の受け渡し(picker.lua) / JSON / タイマー / store -- pad.lua … 外部コントローラー -- sound.lua … チップチューン音源の鍵盤とデモ曲(demo.mml) -- notify.lua … 通知(pico.notify)。通知から開かれたときは、このメニューが通知のページへ送る -- mic.lua … マイク(pico.mic_*)。メーター・録音・再生・増幅/周波数 local x, y, w, h = pico.content_rect() local dir = pico.app_dir() local function open(file, args) pico.push_scene(pico.path_join(dir, file), args) end local PAGES = { { "基本の部品", "basics.lua" }, { "追加の部品", "extra.lua" }, { "直接描画", "draw.lua" }, { "画面とデータ", "scene.lua" }, { "コントローラー", "pad.lua" }, { "音", "sound.lua" }, { "通知", "notify.lua" }, { "マイク", "mic.lua" }, } local BTN_H = 28 local GAP = 3 local top = y + 4 for i, p in ipairs(PAGES) do local b = pico.create("Button") pico.set(b, "text", p[1]) pico.set(b, "x", x + 8) pico.set(b, "y", top + (i - 1) * (BTN_H + GAP)) pico.set(b, "w", w - 16) pico.set(b, "h", BTN_H) pico.on(b, "press_end", function() open(p[2]) end) end local back = pico.create("Button") pico.set(back, "icon_id", 30) -- ArrowLeft pico.set(back, "x", x + 8) pico.set(back, "y", top + #PAGES * (BTN_H + GAP) + 4) pico.set(back, "w", 80) pico.set(back, "h", BTN_H) pico.on(back, "press_end", function() pico.go_back() end) pico.on_back(function() pico.pop() end) -- 通知から開かれた(launch_reasonは1回きり)ときは通知のページへ local tag, data = pico.launch_reason() if tag then open("notify.lua", { tag = tag, data = data }) end -- マイク(pico.mic_*)の動作確認。I2Sマイク SPH0645(GP26=BCLK / GP27=LRCLK / GP28=DOUT)。 -- 開いている間はメーターで聞き続け、[録音]でこのアプリのフォルダの rec_NNNN.wav へ録る。 -- [再生]で最後に録ったものを pico.wav_play で鳴らす。増幅・周波数はこのアプリを閉じると元へ戻る(mic.cfgへは書かない)。 -- 権限 permission_microphone(app.cfg)が要る。PCビルドは PICOOS_MIC=tone(既定)で440Hzの試験音が入る。 local x, y, w, h = pico.content_rect() local M = 8 local BLACK, WHITE, GREY, RED, GREEN, YELLOW = 0, 15, 7, 12, 10, 14 local last = nil -- 最後に録ったファイル local level = 0 -- 0〜1(落ちるときはゆっくり) local db = -96 local function button(text, bx, by, bw, fn) local b = pico.create("Button") pico.set(b, "text", text) pico.set(b, "x", bx) pico.set(b, "y", by) pico.set(b, "w", bw) pico.set(b, "h", 32) pico.set(b, "font_size", 0) pico.on(b, "press_end", fn) return b end local function label(ly, lh) local l = pico.create("Label") pico.set(l, "x", x + M) pico.set(l, "y", ly) pico.set(l, "font_size", 0) pico.set(l, "max_width", w - M * 2) if lh then pico.set(l, "max_height", lh) end pico.set(l, "text", "") return l end button("戻る", x + M, y + 4, 60, function() pico.go_back() end) pico.on_back(function() pico.mic_stop(); pico.wav_stop(); pico.pop() end) local status = label(y + 42, 34) local info = label(y + 78, 34) -- メーター(-60〜0dBFS) local MY = y + 116 local meter = pico.create("Canvas") pico.set(meter, "x", x + M) pico.set(meter, "y", MY) pico.set(meter, "w", w - M * 2) pico.set(meter, "h", 16) pico.on(meter, "render", function() local mw = w - M * 2 pico.draw_rect(x + M, MY, mw, 16, BLACK) local fill = math.floor((mw - 2) * level) if fill > 0 then local c = (db >= -1 and RED) or (db >= -12 and YELLOW) or GREEN pico.fill_rect(x + M + 1, MY + 1, fill, 14, c) end end) local BY = MY + 26 local half = (w - M * 3) // 2 local rec_btn rec_btn = button("録音", x + M, BY, half, function() if pico.mic_recording() then pico.mic_stop() else local p, err = pico.mic_start() if p then last = p else pico.toast("録音できません: " .. tostring(err)) end end end) button("再生", x + M * 2 + half, BY, half, function() if pico.mic_recording() then return end if not last then pico.toast("まだ録っていません"); return end local ok, err = pico.wav_play(last) if not ok then pico.toast("鳴らせません: " .. tostring(err)) end end) local third = (w - M * 4) // 3 button("増幅-", x + M, BY + 38, third, function() pico.mic_set_gain(pico.mic_gain() - 6) end) button("増幅+", x + M * 2 + third, BY + 38, third, function() pico.mic_set_gain(pico.mic_gain() + 6) end) button("周波数", x + M * 3 + third * 2, BY + 38, third, function() local nxt = { [16000] = 24000, [24000] = 48000, [48000] = 16000 } local ok, err = pico.mic_set_rate(nxt[pico.mic_rate()] or 16000) if not ok then pico.toast(tostring(err)) end end) local ok, err = pico.mic_monitor(true) if not ok then pico.set(status, "text", "メーターを入れられません: " .. tostring(err)) end local shown_rec = nil pico.every(50, function() local v, d = pico.mic_level() local nv = math.max(0, math.min(1, (d + 60) / 60)) if v <= 0 then nv = 0 end local before = level level = math.max(nv, level - 0.03) db = d if math.floor(before * 100) ~= math.floor(level * 100) then pico.invalidate(meter) end end) pico.every(250, function() local i = pico.mic_info() local rec = i.recording if rec ~= shown_rec then pico.set(rec_btn, "text", i.finishing and "保存中" or (rec and "停止" or "録音")) shown_rec = rec end local s if i.input == "failed" then s = "マイクのI2Sを開始できません" elseif i.input == "running" and not i.signal then s = "マイクの信号がありません(配線を確認)" elseif rec then s = string.format("録音中 %d.%d秒", i.ms // 1000, (i.ms % 1000) // 100) if i.dropped > 0 then s = s .. string.format(" 取りこぼし%d", i.dropped) end else s = "待機中 (" .. i.input .. ")" end pico.set(status, "text", s) -- Label は "_" を下線の印として消すので、ファイル名は空白に置き換えて見せる local name = last and last:match("[^/]+$"):gsub("_", " ") or "" pico.set(info, "text", string.format("%dHz +%ddB %s", i.rate, i.gain, name)) end) -- 通知: pico.notify の確認。 -- 予約した通知はこのアプリを閉じても出る(見張るのはOS)。トーストか通知センターで -- タップするとこのアプリが開き、main.lua が pico.launch_reason() で「どの通知から開いたか」を受け取って -- このページへ args で渡す(launch_reason は起動した最初の画面で1回だけ読める)。 -- 通知を使うには app.cfg に permission_notify=true が要る。 local x, y, w, h = pico.content_rect() local m = 8 local function button(text, bx, by, fn) local b = pico.create("Button") pico.set(b, "text", text) pico.set(b, "x", bx) pico.set(b, "y", by) pico.on(b, "press_end", fn) return b end local function label(text, lx, ly) local l = pico.create("Label") pico.set(l, "x", lx) pico.set(l, "y", ly) pico.set(l, "font_size", 0) pico.set(l, "text", text) return l end pico.set(button("", x + m, y + m, function() pico.pop() end), "icon_id", 30) -- ArrowLeft label("通知", x + 70, y + m + 6) local status = label("", x + m, y + 200) local reason = label("", x + m, y + 255) local function refresh(msg) pico.set(status, "text", (msg and (msg .. "\n") or "") .. "予約: " .. #pico.notify_list() .. "件") end local function result(id, err) if id == nil then refresh("失敗: " .. tostring(err)) else refresh("OK (id=" .. id .. ")") end end local row = y + 50 button("すぐ通知", x + m, row, function() result(pico.notify{ title = "テスト通知", body = "すぐ出した通知です", tag = "now", data = "now" }) end) button("10秒後", x + 120, row, function() result(pico.notify{ title = "10秒経ちました", body = "閉じていても出ます", delay_ms = 10000, tag = "later", data = "later" }) end) row = row + 45 button("1分ごと", x + m, row, function() result(pico.notify{ title = "1分ごとの通知", every_ms = 60000, tag = "every", data = "every", sound = false }) end) button("取り消し", x + 120, row, function() refresh("取り消し: " .. pico.notify_cancel() .. "件") end) row = row + 45 button("電池20%未満", x + m, row, function() result(pico.notify{ title = "電池が少なくなりました", when = "battery_low", below = 20, tag = "battery" }) end) -- バックグラウンド(background.lua)が動くたびに通知を出すか。動いた回数は background.lua が settings へ書く local bg_button local function bg_text() local on = pico.settings_get("bg_notify") == "true" return "BG通知:" .. (on and "入" or "切") end bg_button = button(bg_text(), x + 120, row, function() local on = pico.settings_get("bg_notify") == "true" pico.settings_set("bg_notify", on and "false" or "true") pico.set(bg_button, "text", bg_text()) end) local bg_label = label("", x + m, y + 230) pico.set(bg_label, "text", "バックグラウンド: " .. (pico.settings_get("bg_runs") or "0") .. "回(最後 " .. (pico.settings_get("bg_last") or "-") .. ")") local from = pico.args() if from and from.tag then pico.set(reason, "text", "通知から: " .. from.tag .. " / " .. tostring(from.data)) else pico.set(reason, "text", "メニューから開いた") end refresh() -- 外部コントローラーの動作確認(SUMMARY.md #10)。 -- 今押しているボタンを図で出し、十字キーで点を動かす(斜め = 2方向の同時押しの確認)。 -- 実物のコントローラーが無い間は、PCで script/pad_serial.py を動かすとキーボードがコントローラーになる。 -- HOMEか「戻る」でメニューへ戻る。 -- Buttonのw/hは枠・立体表示を含めた全体の大きさ。文字/アイコンの領域はこれより9px小さい local BUTTON_FRAME = 9 local x, y, w, h = pico.content_rect() local margin = 8 local BLACK, WHITE, GREY, RED, BLUE = 0, 15, 7, 12, 9 local status = pico.create("Label") pico.set(status, "x", x + margin) pico.set(status, "y", y + margin) pico.set(status, "font_size", 0) pico.set(status, "text", "") -- ---- ボタンの図 ---- local PAD_Y = y + 36 local PAD_H = 118 local pad = pico.create("Canvas") pico.set(pad, "x", x) pico.set(pad, "y", PAD_Y) pico.set(pad, "w", w) pico.set(pad, "h", PAD_H) local function key(px, py, bw, bh, name, label) local down = pico.pad_down(name) if down then pico.fill_rect(px, py, bw, bh, BLACK) else pico.draw_rect(px, py, bw, bh, BLACK) end pico.draw_text(px + 3, py + (bh - 16) // 2, label, down and WHITE or BLACK, 0) end local function round(cx, cy, r, name, label) local down = pico.pad_down(name) if down then pico.fill_circle(cx, cy, r, BLACK) else pico.draw_circle(cx, cy, r, BLACK) end pico.draw_text(cx - 4, cy - 8, label, down and WHITE or BLACK, 0) end pico.on(pad, "render", function() local ox, oy = x, PAD_Y -- 肩のボタン key(ox + 4, oy + 2, 34, 20, "zl", "ZL") key(ox + 40, oy + 2, 26, 20, "l", "L") key(ox + w - 66, oy + 2, 26, 20, "r", "R") key(ox + w - 38, oy + 2, 34, 20, "zr", "ZR") -- 十字キー local cx, cy, s = ox + 48, oy + 70, 22 key(cx - s // 2, cy - s - s // 2, s, s, "up", "") key(cx - s // 2, cy + s // 2, s, s, "down", "") key(cx - s - s // 2, cy - s // 2, s, s, "left", "") key(cx + s // 2, cy - s // 2, s, s, "right", "") -- ABXY(Wiiクラシックの並び: 右A 下B 左Y 上X) local bx, by, d = ox + w - 50, oy + 70, 24 round(bx + d, by, 12, "a", "A") round(bx, by + d, 12, "b", "B") round(bx - d, by, 12, "y", "Y") round(bx, by - d, 12, "x", "X") -- 真ん中 key(ox + 86, oy + 94, 30, 20, "select", "-") key(ox + 105, oy + 66, 30, 20, "home", "H") key(ox + 124, oy + 94, 30, 20, "start", "+") end) -- ---- 十字キーで動く点 ---- local FIELD_Y = PAD_Y + PAD_H + 6 local FIELD_H = h - (FIELD_Y - y) - 40 local field = pico.create("Canvas") pico.set(field, "x", x + margin) pico.set(field, "y", FIELD_Y) pico.set(field, "w", w - margin * 2) pico.set(field, "h", FIELD_H) local R = 6 local dot_x, dot_y = (w - margin * 2) / 2, FIELD_H / 2 local SPEED = 90 -- px/秒 pico.on(field, "render", function() local fx, fy = x + margin, FIELD_Y pico.draw_rect(fx, fy, w - margin * 2, FIELD_H, GREY) local color = BLACK if pico.pad_down("a") then color = RED elseif pico.pad_down("b") then color = BLUE end pico.fill_circle(fx + math.floor(dot_x), fy + math.floor(dot_y), R, color) end) local back = pico.create("Button") pico.set(back, "x", x + margin) pico.set(back, "y", y + h - 32) pico.set(back, "w", 80 + BUTTON_FRAME) pico.set(back, "h", 26 + BUTTON_FRAME) pico.set(back, "icon_id", 30) -- ArrowLeft pico.on(back, "press_start", function() pico.go_back() end) -- EscキーとコントローラーのHOMEでも戻る(HOMEは下のキー図にも出る) pico.on_back(function() pico.pop() end) -- ---- 毎フレーム ---- local NAMES = { "up", "down", "left", "right", "a", "b", "x", "y", "l", "r", "zl", "zr", "start", "select", "home" } local last_key = nil local last_connected = nil function loop(dt) local connected = pico.pad_connected() if connected ~= last_connected then last_connected = connected pico.set(status, "text", connected and "コントローラー: 接続中" or "コントローラー: 未接続") end -- 押しているものが変わったときだけ図を描き直す local k = "" for _, n in ipairs(NAMES) do if pico.pad_down(n) then k = k .. n .. " " end end if k ~= last_key then last_key = k pico.invalidate(pad) pico.invalidate(field) end local vx, vy = 0, 0 if pico.pad_down("left") then vx = vx - 1 end if pico.pad_down("right") then vx = vx + 1 end if pico.pad_down("up") then vy = vy - 1 end if pico.pad_down("down") then vy = vy + 1 end if vx ~= 0 or vy ~= 0 then local step = SPEED * dt / 1000 if vx ~= 0 and vy ~= 0 then step = step * 0.7071 end dot_x = math.max(R + 1, math.min(w - margin * 2 - R - 2, dot_x + vx * step)) dot_y = math.max(R + 1, math.min(FIELD_H - R - 2, dot_y + vy * step)) pico.invalidate(field) end end -- push_scene の子の画面: pico.args() で親から値を受け取り、pico.pop(result) で結果を返す local args = pico.args() or {} local x, y, w, h = pico.content_rect() local title = pico.create("Label") pico.set(title, "x", x + 10) pico.set(title, "y", y + 10) pico.set(title, "text", args.title or "選んでください") local colors = { "red", "green", "blue" } for i, name in ipairs(colors) do local b = pico.create("Button") pico.set(b, "x", x + 10) pico.set(b, "y", y + 40 + (i - 1) * 40) pico.set(b, "w", 120) pico.set(b, "h", 34) pico.set(b, "text", name) pico.on(b, "press_end", function() pico.pop({ color = name, index = i }) -- 親の on_result に届く end) end local cancel = pico.create("Button") pico.set(cancel, "x", x + 10) pico.set(cancel, "y", y + 170) pico.set(cancel, "w", 120) pico.set(cancel, "h", 34) pico.set(cancel, "text", "やめる") pico.on(cancel, "press_end", function() pico.pop() end) -- 結果なし: 親の on_result は呼ばれない -- 画面とデータ: 次の5つの確認 -- 1. require("util") … モジュールの分割 -- 2. press_start(id, x, y, lx, ly, dx, dy) … タッチ座標の引数(盤面のマス目を逆算) -- 3. push_scene(path, args) / pico.args() / pico.pop(result) / on_suspend・on_resume・on_result / store_load・store_save -- 4. json_decode / json_encode(HTTPは権限が要るのでここでは使わない) -- 5. pico.every / pico.after / pico.cancel local util = require("util") local x, y, w, h = pico.content_rect() local save = pico.store_load() or { taps = 0 } local seconds = 0 local picked = "-" local last_cell = "-" local function label(text, lx, ly) local l = pico.create("Label") pico.set(l, "x", lx) pico.set(l, "y", ly) pico.set(l, "font_size", 0) -- 小さい文字(16px) pico.set(l, "text", text) return l end local title = label("画面とデータ", x + 8, y + 4) local clock_label = label("経過 0:00", x + 8, y + 24) local cell_label = label("マス: -", x + 8, y + 44) local drag_label = label("ドラッグ: 0,0", x + 8, y + 64) local json_label = label("", x + 8, y + 84) local pick_label = label("選んだ色: -", x + 8, y + 104) local tap_label = label("保存済みタップ数: " .. save.taps, x + 8, y + 124) -- 5. タイマー: 1秒ごとに経過時間(util.clock)を更新 pico.every(1000, function() seconds = seconds + 1 pico.set(clock_label, "text", "経過 " .. util.clock(seconds)) end) -- 2. 盤面: 108x72の枠を 36x24 のマスに分け、lx,ly から (列,行) を出す local board = pico.create("Canvas") pico.set(board, "x", x + 8) pico.set(board, "y", y + 148) pico.set(board, "w", 108) pico.set(board, "h", 72) local sel_col, sel_row = nil, nil pico.on(board, "render", function() local ax, ay = pico.get_draw_area() local bx = x + 8 local by = y + 148 pico.fill_rect(bx, by, 108, 72, 15) for c = 0, 3 do pico.draw_line(bx + c * 36, by, bx + c * 36, by + 71, 0) end for r = 0, 2 do pico.draw_line(bx, by + r * 24, bx + 107, by + r * 24, 0) end if sel_col then pico.fill_rect(bx + sel_col * 36 + 1, by + sel_row * 24 + 1, 35, 23, 8) end end) pico.on(board, "press_start", function(id, px, py, lx, ly) sel_col, sel_row = lx // 36, ly // 24 last_cell = sel_col .. "," .. sel_row pico.set(cell_label, "text", "マス: " .. last_cell) pico.invalidate(board) save.taps = save.taps + 1 pico.set(tap_label, "text", "保存済みタップ数: " .. save.taps) pico.store_save(save) -- アプリのフォルダの store.json に保存 end) local drag_x, drag_y = 0, 0 pico.on(board, "press_move", function(id, px, py, lx, ly, dx, dy) drag_x, drag_y = drag_x + dx, drag_y + dy pico.set(drag_label, "text", "ドラッグ: " .. drag_x .. "," .. drag_y) end) -- 4. JSON: 往復して表示 local round = pico.json_decode(pico.json_encode({ name = "たろう", scores = { 10, 20.5 }, ok = true })) pico.set(json_label, "text", "JSON: " .. round.name .. " " .. round.scores[2] .. " " .. tostring(round.ok)) -- 3. 画面の受け渡し local pick_btn = pico.create("Button") pico.set(pick_btn, "x", x + 8) pico.set(pick_btn, "y", y + 232) pico.set(pick_btn, "w", 130) pico.set(pick_btn, "h", 34) pico.set(pick_btn, "text", "色を選ぶ") pico.on(pick_btn, "press_end", function() pico.push_scene(pico.path_join(pico.app_dir(), "picker.lua"), { title = "色を選ぶ" }) end) local back = pico.create("Button") pico.set(back, "x", x + 150) pico.set(back, "y", y + 232) pico.set(back, "w", 80) pico.set(back, "h", 34) pico.set(back, "icon_id", 30) -- ArrowLeft pico.on(back, "press_end", function() pico.pop() end) -- 離れる前の状態を持ち越す(Pop で戻るとこのスクリプトは最初から実行し直される) function on_suspend() return { seconds = seconds, picked = picked, cell = last_cell } end function on_resume(s) seconds = s.seconds picked = s.picked pico.set(clock_label, "text", "経過 " .. util.clock(seconds)) pico.set(pick_label, "text", "選んだ色: " .. picked) end function on_result(r) picked = r.color pico.set(pick_label, "text", "選んだ色: " .. picked .. " (#" .. r.index .. ")") end -- 音: チップチューン音源の確認。 -- 鍵盤(1オクターブ+1音)を押すとチャンネル1で鳴る。波形と減衰はボタンで切り替え。 -- 「デモ曲」は同じフォルダの demo.mml(pico-os MML、docs/MUSIC_FORMAT.md)を pico.music_play で鳴らす。 -- 曲を鳴らしながら鍵盤を押すと、鍵盤の音(効果音)がチャンネル1を借り、離すと曲へ返す。 -- アンプがつながっていなくても同じように動く(音が出ないだけ。上の1行で分かる)。 -- Buttonのw/hは枠・立体表示を含めた全体の大きさ。文字/アイコンの領域はこれより9px小さい local BUTTON_FRAME = 9 local cx, cy, cw, ch = pico.content_rect() local WAVES = { "pulse50", "pulse25", "pulse12", "triangle", "saw", "noise", "noise_short" } local wave_index = 1 local decay = true -- ---- 上の段: 戻る / 波形 / 減衰 ---- local function button(x, y, w, text, fn) local b = pico.create("Button") -- 文字を先に決める(後から文字を変えると箱の大きさが測り直される) pico.set(b, "font_size", 0); pico.set(b, "text", text) pico.set(b, "x", x); pico.set(b, "y", y) pico.set(b, "w", w + BUTTON_FRAME); pico.set(b, "h", 18 + BUTTON_FRAME) pico.on(b, "press_start", fn) return b end pico.set(button(cx + 2, cy + 2, 40, "", function() pico.pop() end), "icon_id", 30) -- ArrowLeft local wave_btn wave_btn = button(cx + 46, cy + 2, 104, "波形: " .. WAVES[wave_index], function() wave_index = wave_index % #WAVES + 1 pico.set(wave_btn, "text", "波形: " .. WAVES[wave_index]) end) local decay_btn decay_btn = button(cx + 154, cy + 2, cw - 156, "減衰: あり", function() decay = not decay pico.set(decay_btn, "text", decay and "減衰: あり" or "減衰: なし") end) -- "h"は影を含むボタン全体の高さ。そこから1px空けて下を並べる local row_bottom = cy + 2 + pico.get(wave_btn, "h") + 1 local status = pico.create("Label") pico.set(status, "x", cx + 4); pico.set(status, "y", row_bottom + 4) pico.set(status, "font_size", 0) -- ---- 鍵盤 ---- -- C4〜C5の白鍵8つ。黒鍵はC#/D#/F#/G#/A# local KEY_TOP = row_bottom + 24 local KEY_H = 120 local BLACK_H = 72 local WHITE_W = math.floor(cw / 8) local BLACK_W = math.floor(WHITE_W * 0.6) local WHITE_NOTES = { 60, 62, 64, 65, 67, 69, 71, 72 } -- 黒鍵: {白鍵の何番目の右端にあるか, ノート番号} local BLACK_KEYS = { {1, 61}, {2, 63}, {4, 66}, {5, 68}, {6, 70} } local NAMES = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" } local pressed_note = nil local function blackRect(i) local k = BLACK_KEYS[i] return cx + k[1] * WHITE_W - math.floor(BLACK_W / 2), KEY_TOP, BLACK_W, BLACK_H end -- 座標 → ノート番号(黒鍵が上に重なっているので先に見る) local function noteAt(x, y) if y < KEY_TOP or y >= KEY_TOP + KEY_H then return nil end if y < KEY_TOP + BLACK_H then for i = 1, #BLACK_KEYS do local bx, _, bw = blackRect(i) if x >= bx and x < bx + bw then return BLACK_KEYS[i][2] end end end local i = math.floor((x - cx) / WHITE_W) + 1 return WHITE_NOTES[i] end local keys = pico.create("Canvas") pico.set(keys, "x", cx); pico.set(keys, "y", KEY_TOP) pico.set(keys, "w", WHITE_W * 8); pico.set(keys, "h", KEY_H) pico.on(keys, "render", function() for i = 1, 8 do local x = cx + (i - 1) * WHITE_W if pressed_note == WHITE_NOTES[i] then pico.fill_rect(x, KEY_TOP, WHITE_W, KEY_H, 7) end pico.draw_rect(x, KEY_TOP, WHITE_W + 1, KEY_H, 0) end for i = 1, #BLACK_KEYS do local bx, by, bw, bh = blackRect(i) pico.fill_rect(bx, by, bw, bh, pressed_note == BLACK_KEYS[i][2] and 8 or 0) end end) local note_label = pico.create("Label") pico.set(note_label, "x", cx + 4); pico.set(note_label, "y", KEY_TOP + KEY_H + 6) pico.set(note_label, "font_size", 0) pico.set(note_label, "text", "鍵盤を押すと鳴ります") local function playKey(note) if note == pressed_note then return end pressed_note = note pico.invalidate(keys) if not note then -- 減衰なしは押している間だけ鳴らす。減衰ありは離しても消えるまで鳴らしておく if not decay then pico.sound_stop(1) end return end local wave = WAVES[wave_index] local freq = pico.note_freq(note) -- ノイズは周波数がそのまま「ザー」の粗さになるので、高めに振っておく if wave == "noise" or wave == "noise_short" then freq = freq * 16 end pico.sound_play(1, freq, 0, { wave = wave, volume = 15, envelope = decay and -2 or 0 }) pico.set(note_label, "text", string.format("%s%d %.1fHz", NAMES[note % 12 + 1], note // 12 - 1, pico.note_freq(note))) end local function onTouch(_, x, y) playKey(noteAt(x, y)) end pico.on(keys, "press_start", onTouch) pico.on(keys, "press_move", onTouch) pico.on(keys, "press_end", function() playKey(nil) end) pico.on(keys, "press_out", function() playKey(nil) end) -- ---- デモ曲 ---- -- 曲は2コア目のシーケンサーが鳴らすので、ここでは始めて/止めるだけ(テンポも揺れない) local song_btn local function refreshSongButton() pico.set(song_btn, "text", pico.music_playing() and "デモ曲を止める" or "デモ曲を再生") end song_btn = button(cx + 2, KEY_TOP + KEY_H + 26, 130, "デモ曲を再生", function() if pico.music_playing() then pico.music_stop() else local ok, err = pico.music_play(pico.path_join(pico.app_dir(), "demo.mml")) if not ok then pico.show_error("デモ曲を読めません\n" .. err) end end refreshSongButton() end) local status_timer = 1000 function loop(dt) status_timer = status_timer + dt if status_timer >= 500 then status_timer = 0 pico.set(status, "text", pico.sound_available() and "音: 鳴らせます" or "音: アンプ未接続(鳴りません)") refreshSongButton() end end -- require のデモ用モジュール(scene.lua から require("util") で読まれる) local M = {} function M.pad2(n) return string.format("%02d", n) end -- 秒数を "m:ss" にする function M.clock(sec) return string.format("%d:%02d", sec // 60, sec % 60) end return M