将关卡BGM改为温暖无节拍氛围循环
This commit is contained in:
@@ -1,3 +1,14 @@
|
||||
## 2026-10-23:温暖无节拍关卡BGM试听版
|
||||
|
||||
版本:2026-10-23-warm-ambient-bgm。用户选择试用温暖、无节拍、弱旋律方向;本轮仅替换关卡音乐,选关音乐保持原文件哈希。
|
||||
|
||||
- 原创32秒循环:低中音区柔和和声缓慢交叠,没有鼓点、规律敲击或突出旋律;采用周期波形与平滑交叉渐变,避免循环接缝。
|
||||
- PCM均方根约0.035,峰值小于0.17,关卡播放音量0.14;选关音量仍为0.16。保留音乐/音效/振动独立开关及后台、中断恢复。
|
||||
- 节点确认音、通关和弦、缩放、触觉、存档、选关标题及200关规则保留。之前节点跟手与立体标题发布包保留。
|
||||
- 验收:80项模块测试、200关触摸回归;生成资源的接缝、包络突变、音量及开关恢复检查。实际听感与真机扬声器/耳机效果需用户试听。
|
||||
|
||||
试听资源:music/play.wav。发布包:../releases/NumberSwipe-main-温暖无节拍BGM试听版-20261023.zip。
|
||||
|
||||
## 2026-10-23:节点反馈与立体标题
|
||||
|
||||
版本:2026-10-23-responsive-nodes-sticker-header。开发分支:codex/node-feedback-responsive。以当前正式主工程为基线;没有从历史副本覆盖文件。
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
## 2026-10-23:温暖无节拍关卡BGM试听版
|
||||
|
||||
版本:2026-10-23-warm-ambient-bgm。用户选择试用温暖、无节拍、弱旋律方向;本轮仅替换关卡音乐,选关音乐保持原文件哈希。
|
||||
|
||||
- 原创32秒循环:低中音区柔和和声缓慢交叠,没有鼓点、规律敲击或突出旋律;采用周期波形与平滑交叉渐变,避免循环接缝。
|
||||
- PCM均方根约0.035,峰值小于0.17,关卡播放音量0.14;选关音量仍为0.16。保留音乐/音效/振动独立开关及后台、中断恢复。
|
||||
- 节点确认音、通关和弦、缩放、触觉、存档、选关标题及200关规则保留。之前节点跟手与立体标题发布包保留。
|
||||
- 验收:80项模块测试、200关触摸回归;生成资源的接缝、包络突变、音量及开关恢复检查。实际听感与真机扬声器/耳机效果需用户试听。
|
||||
|
||||
试听资源:music/play.wav。发布包:../releases/NumberSwipe-main-温暖无节拍BGM试听版-20261023.zip。
|
||||
|
||||
## 2026-10-23:节点反馈与立体标题
|
||||
|
||||
版本:2026-10-23-responsive-nodes-sticker-header。开发分支:codex/node-feedback-responsive。以当前正式主工程为基线;没有从历史副本覆盖文件。
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
## 2026-10-23:温暖无节拍关卡BGM试听版
|
||||
|
||||
版本:2026-10-23-warm-ambient-bgm。用户选择试用温暖、无节拍、弱旋律方向;本轮仅替换关卡音乐,选关音乐保持原文件哈希。
|
||||
|
||||
- 原创32秒循环:低中音区柔和和声缓慢交叠,没有鼓点、规律敲击或突出旋律;采用周期波形与平滑交叉渐变,避免循环接缝。
|
||||
- PCM均方根约0.035,峰值小于0.17,关卡播放音量0.14;选关音量仍为0.16。保留音乐/音效/振动独立开关及后台、中断恢复。
|
||||
- 节点确认音、通关和弦、缩放、触觉、存档、选关标题及200关规则保留。之前节点跟手与立体标题发布包保留。
|
||||
- 验收:80项模块测试、200关触摸回归;生成资源的接缝、包络突变、音量及开关恢复检查。实际听感与真机扬声器/耳机效果需用户试听。
|
||||
|
||||
试听资源:music/play.wav。发布包:../releases/NumberSwipe-main-温暖无节拍BGM试听版-20261023.zip。
|
||||
|
||||
## 2026-10-23:节点反馈与立体标题
|
||||
|
||||
版本:2026-10-23-responsive-nodes-sticker-header。开发分支:codex/node-feedback-responsive。以当前正式主工程为基线;没有从历史副本覆盖文件。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,22 @@
|
||||
import fs from 'node:fs';import path from 'node:path';
|
||||
// Original sparse pentatonic loops. Each damped note wraps its tail across the seam.
|
||||
export function buildMusic(root){fs.mkdirSync(path.join(root,'music'),{recursive:true});const rate=22050,seconds=16,n=rate*seconds;
|
||||
for(const [name,notes]of Object.entries({select:[60,67,72,64,69,67,64,62],play:[48,55,60,55,57,52,55,50]})){
|
||||
const samples=new Float64Array(n);for(let k=0;k<notes.length;k++){const f=440*2**((notes[k]-69)/12),offset=k*2;for(let j=0;j<rate*4;j++){const t=j/rate,env=(1-Math.exp(-t/.025))*Math.exp(-t/.65)*Math.min(1,(4-t)/.15);samples[(offset*rate+j)%n]+=(Math.sin(2*Math.PI*f*t)+.12*Math.sin(4*Math.PI*f*t))*env*.24;}}
|
||||
const b=Buffer.alloc(44+n*2);b.write('RIFF');b.writeUInt32LE(b.length-8,4);b.write('WAVEfmt ',8);b.writeUInt32LE(16,16);b.writeUInt16LE(1,20);b.writeUInt16LE(1,22);b.writeUInt32LE(rate,24);b.writeUInt32LE(rate*2,28);b.writeUInt16LE(2,32);b.writeUInt16LE(16,34);b.write('data',36);b.writeUInt32LE(n*2,40);for(let i=0;i<n;i++)b.writeInt16LE(Math.round(samples[i]*32767),44+i*2);fs.writeFileSync(path.join(root,'music',name+'.wav'),b);
|
||||
}}
|
||||
const rate=22050;
|
||||
function writeWav(root,name,samples){const n=samples.length,b=Buffer.alloc(44+n*2);b.write('RIFF');b.writeUInt32LE(b.length-8,4);b.write('WAVEfmt ',8);b.writeUInt32LE(16,16);b.writeUInt16LE(1,20);b.writeUInt16LE(1,22);b.writeUInt32LE(rate,24);b.writeUInt32LE(rate*2,28);b.writeUInt16LE(2,32);b.writeUInt16LE(16,34);b.write('data',36);b.writeUInt32LE(n*2,40);for(let i=0;i<n;i++)b.writeInt16LE(Math.round(Math.max(-1,Math.min(1,samples[i]))*32767),44+i*2);fs.writeFileSync(path.join(root,'music',name+'.wav'),b);}
|
||||
export function buildMusic(root){
|
||||
fs.mkdirSync(path.join(root,'music'),{recursive:true});
|
||||
// Selection music is byte-for-byte unchanged.
|
||||
const select=new Float64Array(rate*16),notes=[60,67,72,64,69,67,64,62];
|
||||
for(let k=0;k<notes.length;k++){const f=440*2**((notes[k]-69)/12),offset=k*2;for(let j=0;j<rate*4;j++){const t=j/rate,env=(1-Math.exp(-t/.025))*Math.exp(-t/.65)*Math.min(1,(4-t)/.15);select[(offset*rate+j)%select.length]+=(Math.sin(2*Math.PI*f*t)+.12*Math.sin(4*Math.PI*f*t))*env*.24;}}
|
||||
writeWav(root,'select',select);
|
||||
// Original 32-second warm pad: no percussion, note attacks or recurring beat.
|
||||
// Integer cycles and periodic crossfades preserve both waveform and slope at wrap.
|
||||
const seconds=32,samples=new Float64Array(rate*seconds),chords=[[48,55,60,64],[45,52,55,60],[48,55,62,67]];
|
||||
const voices=chords.map((notes,k)=>notes.map((midi,j)=>({f:Math.round(440*2**((midi-69)/12)*seconds)/seconds,phase:(k*1.7+j*.9),gain:j===0?.75:1})));
|
||||
let sum=0,peak=0;
|
||||
for(let i=0;i<samples.length;i++){const t=i/rate;let value=0;
|
||||
for(let k=0;k<voices.length;k++){const weight=(1-Math.cos(2*Math.PI*(t/seconds-k/voices.length)))/2;
|
||||
for(const v of voices[k]){const phase=2*Math.PI*v.f*t+v.phase;value+=weight*v.gain*(Math.sin(phase)+.055*Math.sin(2*phase)+.012*Math.sin(3*phase));}}
|
||||
samples[i]=value;sum+=value*value;peak=Math.max(peak,Math.abs(value));
|
||||
}
|
||||
const gain=Math.min(.035/Math.sqrt(sum/samples.length),.16/peak);for(let i=0;i<samples.length;i++)samples[i]*=gain;
|
||||
writeWav(root,'play',samples);
|
||||
}
|
||||
|
||||
@@ -5,3 +5,6 @@ test('old arrows retain distance, rush to endpoint and fresh arrows move at base
|
||||
test('settings have exactly three independent preferences, and hint has its own cue',()=>{const ui=boot();ui.select(3);ui.button('设置');assert.deepEqual(Array.from(ui.api.buttons(),b=>b.label),['音乐:开','音效开','振动:开','完成']);ui.button('完成');ui.button('指路');assert.equal(ui.played.at(-1),'audio/hint.wav');ui.button('选关');assert.ok(ui.played.includes('music/select.wav'));ui.button('下一页');assert.equal(ui.played.at(-1),'audio/ui.wav');});
|
||||
test('actual fixed numeric node uses checkpoint pitch, final cell uses complete chord',()=>{const pack=JSON.parse(fs.readFileSync(new URL('./src/upstream/levels.json',import.meta.url)));const levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));const i=levels.findIndex(l=>l.cells.some(c=>c.clue>1&&c.clue<l.cells.length));const l=levels[i],ui=boot();ui.select(i);const route=solve(l)[0];for(const id of route){ui.advance(80);ui.cell(id);const e=ui.api.snapshot().feedback.events.at(-1);if(e.kind==='checkpoint')assert.equal(ui.played.at(-1),'audio/checkpoint-'+e.step+'.wav');}assert.equal(ui.played.at(-1),'audio/complete.wav');assert.ok(ui.api.snapshot().feedback.events.some(e=>e.kind==='checkpoint'));});
|
||||
test('two original music loops are quiet, nonempty and have a smooth loop seam',()=>{for(const name of ['select','play']){const b=fs.readFileSync(new URL('./music/'+name+'.wav',import.meta.url));assert.equal(b.readUInt32LE(24),22050);assert.ok(b.length>600000);assert.ok(Math.abs(b.readInt16LE(44)-b.readInt16LE(b.length-2))<100);}});
|
||||
|
||||
test('warm gameplay ambient is 32 seconds, softly leveled and free from percussive envelope jumps',()=>{const b=fs.readFileSync(new URL('./music/play.wav',import.meta.url)),n=(b.length-44)/2,frames=[];assert.equal(n/22050,32);let sum=0,peak=0;for(let i=0;i<n;i++){const v=b.readInt16LE(44+i*2)/32767;sum+=v*v;peak=Math.max(peak,Math.abs(v));}assert.ok(Math.sqrt(sum/n)>.03&&Math.sqrt(sum/n)<.04);assert.ok(peak<.17);for(let i=0;i<n;i+=2205){let energy=0,count=0;for(let j=i;j<Math.min(i+2205,n);j++){const v=b.readInt16LE(44+j*2)/32767;energy+=v*v;count++;}frames.push(Math.sqrt(energy/count));}assert.ok(frames.every((v,i)=>!i||Math.abs(v-frames[i-1])<.006));assert.ok(Math.abs(b.readInt16LE(44)-b.readInt16LE(b.length-2))<50);});
|
||||
test('gameplay ambient uses lower scene volume and keeps independent mute and resume behavior',()=>{const store=new Map(),api={getStorageSync:k=>store.get(k),setStorageSync:(k,v)=>store.set(k,v),createInnerAudioContext:()=>({stop(){},play(){this.plays=(this.plays||0)+1;},onError(){}})},m=new Music(api);m.scene('select');assert.equal(m.audio.volume,.16);m.scene('play');assert.equal(m.audio.volume,.14);assert.equal(m.audio.loop,true);const plays=m.audio.plays;m.scene('play');assert.equal(m.audio.plays,plays);m.suspend();m.resume();assert.equal(m.audio.src,'music/play.wav');assert.equal(m.audio.volume,.14);m.toggle();assert.equal(new Music(api).enabled,false);});
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,106 @@
|
||||
|
||||
> numberswipe-main@1.0.0 check
|
||||
> npm run build && npm test && npm run smoke
|
||||
|
||||
|
||||
> numberswipe-main@1.0.0 build
|
||||
> node build.mjs
|
||||
|
||||
Built campaign-200-r2: 200 levels
|
||||
|
||||
> numberswipe-main@1.0.0 test
|
||||
> node --test *.test.mjs
|
||||
|
||||
✔ 手机没有浏览器音频接口时,原生播放器仍能播放全部 36 步和六类反馈音 (4.2496ms)
|
||||
✔ 关闭音效停止播放;再次开启会试听并保存偏好;后台中断停止所有播放器 (0.4588ms)
|
||||
✔ 延迟通关音在静音或进入后台时取消,不在返回后突然播放 (58.1285ms)
|
||||
✔ 音频错误可诊断,不再静默吞掉 (1.3962ms)
|
||||
✔ 所有音频均为非静音 PCM;原版第13步比第1步高一个八度 (41.1368ms)
|
||||
✔ 教学和组合规则用具体动作及奇偶例子解释,没有未解释的术语 (13.1034ms)
|
||||
✔ 过关提示同步使用实际播放起点,非请求时间 (0.7765ms)
|
||||
✔ 过关和弦收尾持续衰减,不在后半段重新起音 (2.9054ms)
|
||||
✔ merged irregular outlines retain holes and never draw internal unit edges (68.7005ms)
|
||||
✔ all 200 boards fit four screen budgets with stable safe anchors and real shared entrances (570.0971ms)
|
||||
✔ guides through every irregular block stay inside its occupied shape (30.9901ms)
|
||||
✔ visited, conflicting and hinted category cells retain their material identity (7.9789ms)
|
||||
✔ five font subsets match their manifest and fit the package budget (3.2457ms)
|
||||
✔ upstream adapter fails closed if the inspected render seams change (0.7175ms)
|
||||
✔ procedural cushion pixels preserve holes and light the upper bevel without a border (15.739ms)
|
||||
✔ visible glyph bounds are centered independently of font ascent and side bearings (1.4517ms)
|
||||
✔ stage with zero depth casts alpha shadows without painting a solid backing (6.6306ms)
|
||||
✔ native width-only TextMetrics still centers the rasterized glyph ink (2.1955ms)
|
||||
✔ fixed channels remain anchored at their block centers after number enlargement (780.5029ms)
|
||||
✔ checkpoint ribbons trigger on correct arrival only and never duplicate per frame (2.334ms)
|
||||
✔ checkpoint pulse is small, centered in time and settles without looping (0.2499ms)
|
||||
✔ event batches preserve every landing and sequence but commit only highest audio and haptic (3.9613ms)
|
||||
✔ completion waits at most 120ms and ignores late audio callbacks (0.818ms)
|
||||
✔ light haptics are limited, checkpoint replaces light, and completion adds only one delayed short pulse (0.7328ms)
|
||||
✔ unsupported graded haptic falls back to one short pulse, never long vibration (0.5939ms)
|
||||
✔ haptic disable, preview and lifecycle cancellation suppress delayed or expired work (0.5278ms)
|
||||
✔ late failed haptic requests cannot revive old pulses and ungraded completion falls back once (0.359ms)
|
||||
✔ rectangles, concave shapes and hexagons require geometric inward distance without treating internal seams as walls (1.3903ms)
|
||||
✔ one high-speed native move records all intermediate cells and completion without duplicate final node (125.9551ms)
|
||||
✔ legal but off-solution moves use public rules, while invalid clues only produce error (65.1831ms)
|
||||
✔ drag near a previous-block boundary cannot undo; deeper entry can, and undo button remains immediate (47.9813ms)
|
||||
✔ tutorial, redraw, hint and restored saved routes do not generate player feedback (71.153ms)
|
||||
✔ pressure and rebound have bounded amplitude and duration (0.3831ms)
|
||||
✔ interpolation cannot duplicate the same failed-cell event within one native movement (0.4001ms)
|
||||
✔ nodes are driven by explicit events even when multiple arrive before rendering; at most two ribbons survive (24.6483ms)
|
||||
✔ audio fast moves merge to latest pitch and completion stops the whole four-channel pool (1.6418ms)
|
||||
✔ settings persist haptic preference independently and chapter cards always use three columns (67.065ms)
|
||||
✔ original audio is preserved and high pitches lose energy rather than growing sharper (9.7041ms)
|
||||
✔ empty, partial and wrong routes reveal the first incomplete full segment (2.5105ms)
|
||||
✔ completion hides the hint; repeat clicks cannot skip; undo and restart return to earlier segments (0.6003ms)
|
||||
✔ all 200 levels reveal consecutive segments without changing the player path (1327.0448ms)
|
||||
✔ hint draws skip player edges and mask visited cells on their own layer; compositing is restored (0.7515ms)
|
||||
✔ parity is a segment anchor; colour is not; flow moves continuously across cells (0.3026ms)
|
||||
✔ coral teaching ribbon preserves visited masks and offers direction in reduced motion (1.3771ms)
|
||||
✔ hint rounds shared guide junctions and keeps separate runs disconnected (0.3138ms)
|
||||
✔ 实际教学关动画:basic (4.3763ms)
|
||||
✔ 实际教学关动画:walls (0.859ms)
|
||||
✔ 实际教学关动画:straight (2.2734ms)
|
||||
✔ 实际教学关动画:vertical (3.0943ms)
|
||||
✔ 实际教学关动画:corner (0.9779ms)
|
||||
✔ 实际教学关动画:colour (0.7234ms)
|
||||
✔ 实际教学关动画:parity (0.6127ms)
|
||||
✔ 实际教学关动画:rhythm-door (0.9466ms)
|
||||
✔ 实际教学关动画:hex (0.8683ms)
|
||||
✔ 实际教学关动画:unknown-start (2.3708ms)
|
||||
✔ 200关的组合规则均能找到对应真实教学关 (99.1989ms)
|
||||
✔ 教学减速,结束停留1秒后自动循环 (3.14ms)
|
||||
✔ music loops independently, persists mute and cancels in background (3.5682ms)
|
||||
✔ old arrows retain distance, rush to endpoint and fresh arrows move at base speed (1.237ms)
|
||||
✔ settings have exactly three independent preferences, and hint has its own cue (126.7275ms)
|
||||
✔ actual fixed numeric node uses checkpoint pitch, final cell uses complete chord (101.1832ms)
|
||||
✔ two original music loops are quiet, nonempty and have a smooth loop seam (1.4324ms)
|
||||
✔ warm gameplay ambient is 32 seconds, softly leveled and free from percussive envelope jumps (12.8053ms)
|
||||
✔ gameplay ambient uses lower scene volume and keeps independent mute and resume behavior (0.3121ms)
|
||||
✔ all 200 levels: valid numeric, parity and direction arrivals play checkpoint pitch; final arrival only completes (25674.1428ms)
|
||||
✔ drag back and undo button both vibrate lightly; disabling vibration suppresses both (48.3745ms)
|
||||
✔ haptic cadence is approximately 15 percent denser without changing light or medium grades (1.2116ms)
|
||||
✔ regular and special scale settle at 90ms with a larger special peak (0.221ms)
|
||||
✔ rendered undo target and nonnumeric checkpoint animate immediately without adding ribbons (5.8198ms)
|
||||
✔ quick undo cancels removed node rebound, including last-step removal (2.0522ms)
|
||||
✔ first launch goes directly to gameplay (84.5281ms)
|
||||
✔ resume unfinished route after exit from gameplay (109.8065ms)
|
||||
✔ resume unfinished route after exit from selection (100.4282ms)
|
||||
✔ completed level resumes next unfinished, including leaving from cleared screen (67.2269ms)
|
||||
✔ replaying a cleared level does not replace the unfinished save (74.6262ms)
|
||||
✔ legacy saves, malformed data and full completion have valid startup targets (50.4716ms)
|
||||
✔ selection pages expose fifteen cards and fit phone safe areas (225.6503ms)
|
||||
✔ hint-only frames reuse static UI, and player input invalidates it (40.9483ms)
|
||||
✔ hint curves are cached and distance lookup is stable at boundaries (1.2217ms)
|
||||
✔ new-mechanic lesson reuses the board while the continuous cursor advances (38.2212ms)
|
||||
ℹ tests 80
|
||||
ℹ suites 0
|
||||
ℹ pass 80
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 25971.4086
|
||||
|
||||
> numberswipe-main@1.0.0 smoke
|
||||
> node smoke.mjs
|
||||
|
||||
PASS 200/200: r2源文件哈希一致、唯一解、微信发布包触摸通关、撤销/重开/指路、存档/音效设置恢复、前后台切换。
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"channel": "main",
|
||||
"version": "2026-10-23-responsive-nodes-sticker-header",
|
||||
"version": "2026-10-23-warm-ambient-bgm",
|
||||
"date": "2026-10-23",
|
||||
"baseVersion": "2026-10-22-resume-chapter-lessons",
|
||||
"baseMainSHA256": "bb35d5edf8d96eb3253c78052d219b32f9a4241efd9971537f6c9c8896188125",
|
||||
"package": "NumberSwipe-main-节点跟手与立体标题-20261023.zip",
|
||||
"package": "NumberSwipe-main-温暖无节拍BGM试听版-20261023.zip",
|
||||
"promotedFrom": "2026-10-22-layered-feedback-music-flow",
|
||||
"previousVersion": "2026-10-23-main-layered-feedback"
|
||||
"previousVersion": "2026-10-23-responsive-nodes-sticker-header"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export class Music{
|
||||
constructor(api){this.api=api;this.enabled=true;this.paused=false;try{this.enabled=api.getStorageSync('number-swipe:music:v1')!=='off';}catch{}}
|
||||
scene(name){this.name=name;if(!this.enabled||this.paused||this.playing===name)return;try{if(!this.audio){this.audio=this.api.createInnerAudioContext();this.audio.loop=true;this.audio.volume=.16;this.audio.obeyMuteSwitch=false;this.audio.onError?.(()=>{this.playing=null;this.failed=true;});}if(this.failed)return;this.audio.stop();this.audio.src='music/'+name+'.wav';this.audio.play();this.playing=name;}catch{this.failed=true;}}
|
||||
scene(name){this.name=name;if(!this.enabled||this.paused||this.playing===name)return;try{if(!this.audio){this.audio=this.api.createInnerAudioContext();this.audio.loop=true;this.audio.volume=.16;this.audio.obeyMuteSwitch=false;this.audio.onError?.(()=>{this.playing=null;this.failed=true;});}if(this.failed)return;this.audio.stop();this.audio.volume=name==='play'?.14:.16;this.audio.src='music/'+name+'.wav';this.audio.play();this.playing=name;}catch{this.failed=true;}}
|
||||
stop(){this.audio?.stop();this.playing=null;}
|
||||
suspend(){this.paused=true;this.stop();}
|
||||
resume(){this.paused=false;this.failed=false;if(this.name)this.scene(this.name);}
|
||||
|
||||
Reference in New Issue
Block a user