Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻譯準備中
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Jogo diário de adivinhar música, com dois modos: • Trecho — você ouve um piscar de olho. Cada erro ou pulo libera mais tempo: 0,2s → 0,5s → 2s → 4s → 8s → 15s (a mesma curva do Songless). O ponto de partida é sorteado dentro da música, não o começo da prévia — que as APIs já entregam cortada no refrão. O sorteio é determinístico (semente = id da música + número do puzzle), então o desafio do dia é o mesmo para todo mundo e o trecho não muda entre as tentativas; começos mudos são descartados por energia do sinal (lib/audio/startPoint.ts).檢視完整 README ↗
Jogo diário de adivinhar música, com dois modos:
0,2s → 0,5s → 2s → 4s → 8s → 15s (a mesma curva do Songless). O ponto de partida é sorteado dentro da música, não o começo da prévia — que as APIs já entregam cortada no refrão. O sorteio é determinístico (semente = id da música + número do puzzle), então o desafio do dia é o mesmo para todo mundo e o trecho não muda entre as tentativas; começos mudos são descartados por energia do sinal (lib/audio/startPoint.ts).bateria → baixo → guitarra → teclado → outros → vocal) quando a música tem trilhas isoladas;graves → médios-graves → médios → médios-agudos → agudos → brilho) quando não tem — que é o caso das prévias de streaming. Em ambos dá para ligar/desligar cada camada já liberada; na revelação final o clipe toca limpo, sem filtro.São 6 tentativas por modo, um puzzle novo por dia (meia-noite UTC), estatísticas locais e resultado compartilhável em emojis.
Cada modo tem duas variantes:
/trecho, /banda) — um puzzle por dia, com sequência, estatísticas e compartilhamento./livre/trecho, /livre/banda) — rodadas ilimitadas, sem esperar o dia virar. Sorteia uma música na hora, dá um bom descanso à música e ao artista que acabaram de sair (lib/game/rotation.ts) e mantém um placar da sessão. Gênero, dificuldade e ponto de partida ficam num controle recolhido que mostra o que está valendo ("Todos · Impossível · Sorteado") — abertos são onze botões, e num celular isso empurra o play para fora da primeira tela. Não grava nada e não mexe na sequência do diário.O multijogador usa esta pontuação: acertar no trecho de 0,2s vale 6 pontos, e cada degrau liberado vale um ponto a menos — 6, 5, 4, 3, 2, 1, e zero para quem não acertou. Como errar e pular consomem o mesmo degrau, custam a mesma coisa; pular só serve para não perder tempo, nunca é vantagem. Não grava nada nem mexe na sequência do diário.
/duelo) — de 2 a 4 pessoas em um aparelho só, disputando a mesma música. A primeira ouve 0,2s e chuta; se errar ou pular, a vez passa para a próxima, que ouve 0,5s mas já vale só 5 pontos. Quem acertar leva os pontos daquele degrau e a rodada acaba. Quem abre a rodada roda a cada rodada, porque começar é vantagem e desvantagem ao mesmo tempo: vale mais, mas entrega o degrau seguinte de graça a quem vem depois.
No duelo dá para filtrar o sorteio por gênero, dificuldade, época e artista — tudo múltipla escolha, com a contagem de músicas ao vivo. Como cruzar filtros esvazia o catálogo rápido (k-pop antes de 1990 são zero músicas), a tela avisa quando as músicas vão repetir e trava quando não dá para jogar.Ao fim de qualquer rodada — no diário ou no livre — o botão Desafiar um amigo copia um link. Quem abre cai numa partida com a mesma música e exatamente o mesmo trecho, e vê a marca a bater antes de chutar: "acertou em 0,5s". No fim, o jogo diz quem ganhou. Vence o degrau menor; quem não acertou perde para qualquer acerto, e dois que não acertaram empatam.
É o único formato multijogador assíncrono do jogo: o duelo exige o mesmo sofá, mas o link você manda às 9h e a pessoa abre às 15h. E não precisa de infraestrutura nenhuma — o catálogo inteiro já vai no bundle, então o id da música basta e a página resolve o resto sozinha.
O que viaja na URL é a partida inteira, codificada (lib/game/challenge.ts): id da música, modo, ponto de partida, o degrau de quem desafiou e a semente. A semente é o detalhe que não dá para esquecer: o trecho é sorteado a partir dela, então sem ela o amigo ouviria outro pedaço da mesma música e a comparação viraria mentira.
O link não pode entregar a resposta, e isso vale em três lugares: o código é opaco (não é criptografia — quem quiser decodificar decodifica; é para a música não aparecer de graça na barra de endereço), o cartão de previsualização que o WhatsApp e o Discord desdobram é o mesmo para todo desafio, e a tela de entrada não mostra capa, título nem artista — só a marca a bater. Um código cortado pelo aplicativo de mensagem cai numa tela explicando, e não num 404.
O desafio não grava nada: nem estatística, nem sequência, nem placar de sessão. O único resultado é o veredito.
O bloco de áudio é uma barra de trecho fina com as marcas dos seis degraus, o botão de tocar e o volume — nessa ordem, de cima para baixo. O botão é o ponto de foco: fica maior que o resto e tem um brilho difuso atrás, para não competir em peso com a barra.
A escala da barra é linear no tempo, então os primeiros degraus são fatias finas — 0,2s de 15s é 1,3% da largura. As marcas de cada degrau existem justamente para dar referência onde a fatia sozinha não daria.
Os ajustes da rodada (dificuldade, gênero e ponto de partida) ficam num painel recolhido, e cada controle tem a forma do dado que representa: a dificuldade é uma escala ordenada, então os cinco níveis ficam encostados numa trilha contínua que sobe da esquerda para a direita, em escala de calor — verde, âmbar, laranja, vermelho, roxo. O gênero é um conjunto sem ordem, então são pílulas soltas. O ponto de partida é binário, então é um alternador de duas metades. Desenhados todos iguais, os três pareciam a mesma pergunta feita três vezes.
O "Todos" da dificuldade fica fora da trilha e sem cor de nível: ele não é um degrau da escala. Os cinco níveis mantêm cor mesmo desmarcados — se só o escolhido tivesse cor, a rampa apareceria justamente quando já não serve.
Reconhecer uma música em 0,2s não depende de gênero nem de época: depende de quantas vezes você já ouviu aquilo. Por isso o catálogo é repartido em cinco níveis — fácil, médio, difícil, raro, impossível — pela popularidade da faixa no Deezer (lib/game/tiers.ts), e o nível entra no filtro como mais um eixo, ao lado de gênero e época.
Os cortes são percentis do próprio catálogo, não números fixos de streams. Com número fixo, importar um preset novo moveria o chão inteiro e poderia esvaziar um nível sem ninguém perceber; com percentil os cinco níveis têm sempre mais ou menos o mesmo tamanho, que é o que faz o filtro continuar jogável. O nível fica gravado em data/songs.json e não é calculado na hora justamente porque a fatia depende de todas as outras músicas: recalculado em cima de um catálogo já filtrado, "fácil" viraria "fácil dentro do k-pop", e quem escolheu fácil ouviria uma faixa que nunca ouviu na vida.
Música que nunca foi consultada não tem nível: continua sendo sorteada normalmente, mas nunca responde a um filtro de nível. E enquanto o catálogo inteiro estiver sem popularidade, os botões de dificuldade nem aparecem — um filtro que só sabe zerar o sorteio seria pior que nenhum.
No modo livre e no duelo dá para escolher entre sorteado (um ponto qualquer dentro da prévia) e do começo. Não existe "início da música": as prévias de 30s do Deezer e do iTunes já vêm recortadas do meio da faixa, quase sempre no refrão — então começar do início da prévia é o modo fácil, e a tela diz isso. Prévia que abre em fade tem o silêncio pulado (previewStart), porque 0,2s de nada não é desafio, é bug.
O diário não aceita a escolha: lá o trecho tem que sair igual para todo mundo, senão a grade de emojis compara partidas diferentes.
npm install
npm run dev
Abra http://localhost:3000. Não precisa de backend, banco nem arquivo de áudio: o catálogo vem com 8 faixas geradas proceduralmente (Web Audio puro), então o jogo é jogável no primeiro npm run dev.
| Comando | O que faz |
|---|---|
npm run dev | Servidor de desenvolvimento |
npm run build | Valida o catálogo e faz o build de produção |
npm start | Sobe o build de produção |
npm test | Testes da lógica pura e do gerador de áudio (Vitest) |
npm run lint | ESLint (eslint-config-next) |
npm run catalog:check | Valida data/songs.json sozinho |
npm run catalog:import | Importa músicas reais das APIs do Deezer/iTunes |
npm run catalog:featuring | Preenche os artistas convidados de cada música |
npm run catalog:rank | Puxa a popularidade e recalcula os níveis |
npm run format | Prettier |
strict, alias @/*)prefers-color-schemelocalStorage<audio>) e os efeitos de interface, gerados na hora em vez de servidos como arquivo. A prévia começa a baixar assim que a música é sorteada, não no clique: resolver a URL assinada no Deezer custa ~300 ms e o download mais ~150 ms, e fazer isso depois do toque no play fazia o jogo parecer travado bem na hora em que devia responder. O AudioContext fica de fora do adiantamento de propósito — ele só pode nascer dentro de um gesto do usuário, e decodificar são dezenas de milissegundosapp/ rotas (menu, /trecho, /banda, /duelo, /desafio) + /api
components/ UI (client components)
lib/game/ lógica pura: sorteio diário, rotação do modo livre, palpites, máquina de estados, duelo, desafio, pontos, filtro, níveis, stats, share
lib/audio/ gerador procedural, provedor plugável e motor de reprodução
lib/puzzle/ resolução do puzzle do dia (local ou via API)
store/ Zustand (jogo, duelo + preferências)
data/songs.json catálogo
scripts/ validação do catálogo (roda antes do build)
Em vez de baixar e servir mp3, dá pra puxar prévias oficiais de 30s de APIs públicas e gratuitas — sem chave, sem OAuth:
npm run catalog:import -- --fonte deezer --preset classicos # ~47 artistas consagrados
npm run catalog:import -- --fonte deezer --preset kpop --juntar # acrescenta K-pop
npm run catalog:import -- --fonte deezer --limite 50 # chart do Brasil
npm run catalog:import -- --fonte deezer --busca "rock nacional" --limite 30 --juntar
npm run catalog:import -- --preset crista --por-artista 6 --ao-vivo --titulo-unico --juntar
| Opção | Padrão | O que faz |
|---|---|---|
--fonte | deezer | deezer (1 chamada, já traz a prévia) ou itunes (chart + busca, mais lento) |
--preset | — | classicos, rock, pop, hiphop, eletronica, kpop ou crista: listas curadas de artistas em scripts/import-catalog.ts |
--por-artista | 3 | Faixas por artista no preset |
--pais | br | País do chart |
--limite | 50 | Quantas músicas importar (chart/busca) |
--busca | — | Importa de uma busca em vez do chart |
--juntar | — | Acrescenta ao catálogo atual em vez de substituir |
--ao-vivo | — | Aceita gravações ao vivo (e apaga o "- Ao Vivo" do título) |
--titulo-unico | — | Uma gravação por título, mesmo de artistas diferentes |
--saida | data/songs.json | Arquivo de destino |
O importador descarta remix, karaokê e afins (atrapalham o reconhecimento) e evita que a mesma música entre duas vezes por causa de "Remastered".
Gravação ao vivo também fica de fora por padrão, mas com --ao-vivo ela entra e a marca some do título — em gêneros onde o ao vivo é a gravação de referência (o louvor católico, por exemplo, em que o Frei Gilson tem 19 das 20 faixas mais tocadas ao vivo) o padrão esvaziaria o preset. Nesses gêneros vale somar --titulo-unico: quando várias comunidades gravam o mesmo louvor, duas gravações do mesmo título viram duas respostas certas para o mesmo áudio, e quem escolhesse a outra levaria erro. Vence a primeira, ou seja, a ordem do preset.
Depois rode npm run catalog:featuring e npm run catalog:check.
O --genero grava uma categoria por importação, mas o campo é lista (genres): uma música pode estar em dois gêneros ao mesmo tempo e aparece no filtro dos dois. É o caso do disco solo das integrantes do BLACKPINK, que é K-pop e pop ocidental ao mesmo tempo — o segundo gênero é curadoria, aplicada depois da importação.
Um preset também aceita o id do artista no Deezer ('LISA#145068682'), para os nomes que a busca não resolve: "LISA" não aparece nem entre os dez primeiros resultados dela mesma, e o desempate por fãs entregaria uma homônima.
O Deezer é inconsistente com participações: "Uptown Funk (feat. Bruno Mars)" vem com o crédito no título, mas "Sua Cara" vem seco — e aí ninguém acha a música procurando por "Anitta". Os convidados só existem em /track/{id}, que os endpoints de busca e de chart não trazem, então buscá-los faria o importador demorar o dobro. Por isso é um passo à parte:
npm run catalog:featuring # preenche só o que falta
npm run catalog:featuring -- --forcar # refaz tudo
O script só acrescenta o campo featuring: não mexe em id, na ordem do array nem em nenhum valor existente. Isso importa porque o sorteio do puzzle diário deriva da lista de músicas — mudar a ordem faria todo mundo perder a partida do dia. Os convidados entram na busca e valem como "artista certo" no palpite.
O catálogo não tinha nenhum sinal de dificuldade: gênero e ano não dizem se a música já entrou no seu ouvido. O Deezer entrega um rank de 0 a ~1.000.000 por faixa, que também só existe em /track/{id} — então, como o featuring, é um passo à parte:
npm run catalog:rank # preenche só o que falta
npm run catalog:rank -- --forcar # reconsulta tudo
npm run catalog:rank -- --so-niveis # só recalcula os níveis, sem ir na rede
Mesma regra do outro: só acrescenta campo, preservando ordem e valores. O rank é o fato que veio da API; o tier é derivado dele por percentil sobre o catálogo inteiro. Por isso o recálculo roda sempre no fim e sobre todas as músicas, mesmo com --limite: uma música nova muda a fatia de todas as outras. E é por isso que existe o --so-niveis — para retocar os cortes sem gastar 2205 chamadas de rede.
O catalog:check recusa tier sem rank: um nível editado à mão não sobreviveria ao próximo catalog:rank, e falhar no build é melhor que descobrir jogando.
Duas coisas que o importador resolve por você:
"previewId": "deezer:123"), não o link; /api/preview resolve a URL fresca a cada partida. As do iTunes são estáveis e ficam gravadas em clip./api/preview/stream, que só aceita hosts da allowlist.Prévia não tem trilhas isoladas, mas isso não deixa a música de fora do modo Banda: sem stems, a revelação acontece por faixa de frequência (lib/audio/layers.ts), aplicada com filtros do Web Audio no próprio navegador.
Na tela de revelação, músicas com id do Deezer ganham o widget oficial da faixa (components/FullTrackPlayer.tsx) — o mesmo papel que o embed do SoundCloud cumpre no Songless, e também sem chave de API. O widget toca 30s para quem não está logado no Deezer, e a faixa inteira para quem está.
Testado em 14/08/2026:
| SoundCloud | Deezer / iTunes | |
|---|---|---|
| Busca sem chave | 401 na API pública e na api-v2 interna | 200, sem cadastro |
| Descobrir a faixa por artista/título | sem caminho público | /search/artist + /artist/{id}/top |
| oEmbed sem chave | funciona, mas exige já saber a URL da faixa | — |
| Bytes do áudio | resolve → 401; iframe cross-origin bloqueia | mp3 baixável e decodificável |
| Música inteira deslogado | sim (vantagem real dele) | 30s sem login |
O jogo precisa do AudioBuffer, não de um player: sem as amostras não dá para cortar em 0,2s com fade, nem para aplicar os filtros do modo Banda. Um widget em iframe de outra origem não entrega isso — o que também explica por que o Songless, que usa SoundCloud, não tem um modo estilo Bandle.
Não — e por bastante tempo. Nem para o multijogador: o Duelo é local e o link de desafio cabe inteiro na URL, porque o catálogo já vai no bundle de todo mundo. Fora as estatísticas globais (opcionais), o jogo não fala com servidor nenhum.
O catálogo é um JSON lido em tempo de build:
| Catálogo | songs.json | Peso no bundle (gzip) |
|---|---|---|
| 236 músicas | 69 KB | ~13 KB |
| 2.205 músicas (atual) | 740 KB | ~98 KB |
Regra prática: até uns poucos milhares de músicas o JSON resolve. O áudio nunca pesa, porque mora no CDN da fonte.
Vale migrar para banco (Supabase — a rota /api/stats já está preparada) quando aparecer alguma destas necessidades:
Antes disso, existe um degrau mais barato: manter o JSON como fonte e servi-lo por um Route Handler com cache, tirando o catálogo do bundle — sem banco nenhum.
Edite data/songs.json. Cada entrada aceita três fontes de áudio (source):
local — seus arquivos em public/audio{
"id": "song-100",
"title": "Nome da Música",
"artist": "Artista",
"year": 1997,
"cover": "/covers/song-100.jpg",
"source": "local",
"clip": "/audio/song-100/clip.mp3",
"stems": {
"drums": "/audio/song-100/drums.mp3",
"bass": "/audio/song-100/bass.mp3",
"guitar": "/audio/song-100/guitar.mp3",
"keys": "/audio/song-100/keys.mp3",
"other": "/audio/song-100/other.mp3",
"vocals": "/audio/song-100/vocals.mp3"
}
}
O clip alimenta o modo Trecho; as stems alimentam o modo Banda. Sem as 6 stems a música só aparece no modo Trecho — npm run catalog:check avisa quando isso acontece e quebra o build se um arquivo declarado não existir.
preview — prévia oficial de 30sNormalmente vem do catalog:import, mas dá pra escrever à mão:
{
"id": "deezer-3135556",
"title": "...",
"artist": "...",
"year": 2001,
"source": "preview",
"previewId": "deezer:3135556"
}
/api/preview resolve a prévia — por previewId (exato) ou, sem ele, por busca de artista + título — e /api/preview/stream repassa o mp3 pelo seu domínio. Prévias não têm trilhas separadas, então só servem o modo Trecho.
synth — faixa gerada (padrão do repo){
"id": "song-001",
"title": "...",
"artist": "...",
"year": 2019,
"source": "synth",
"synth": { "bpm": 92, "root": 45, "mode": "minor", "progression": [0, 8, 3, 10], "seed": 1001 }
}
root é uma nota MIDI, progression são semitons relativos à tônica (um acorde por compasso) e seed deixa o resultado determinístico. O gerador (lib/audio/synth.ts) monta as 6 trilhas separadas, então músicas synth funcionam nos dois modos.
Campos opcionais úteis: cover (sem ele, uma capa em gradiente é gerada a partir do id), aliases (grafias alternativas aceitas na busca — a busca já ignora acentos e maiúsculas), featuring (artistas convidados, preenchido por catalog:featuring) e o par rank / tier (popularidade e nível de dificuldade, preenchidos por catalog:rank).
lib/audio/provider.ts define a interface AudioProvider (loadClip / loadStem). Há quatro implementações: SynthProvider, LocalFileProvider, PreviewApiProvider e CompositeProvider (escolhe música a música pelo campo source — é o padrão).
Para forçar um provedor, use NEXT_PUBLIC_AUDIO_PROVIDER (auto | synth | local | preview). Para plugar outra fonte (S3, CDN própria, outra API), implemente a interface e devolva-a em createProvider() — nem a UI nem o motor de reprodução mudam.
Copie .env.example para .env.local.
Puzzle do dia. Por padrão (NEXT_PUBLIC_PUZZLE_SOURCE=local) tudo é resolvido no cliente a partir do catálogo — como no Wordle, a resposta é descobrível no devtools. Com NEXT_PUBLIC_PUZZLE_SOURCE=api, o puzzle vem de /api/puzzle, que usa a data do servidor e não aceita parâmetro de data — não há como pedir a resposta de amanhã. Se a rota falhar, o cliente cai no modo local.
Estatísticas globais. Desligadas por padrão. Preencha SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY e ligue NEXT_PUBLIC_GLOBAL_STATS=on para que /api/stats registre e agregue os resultados do dia (a chave de serviço nunca sai do servidor):
create table results (
id bigint generated always as identity primary key,
mode text not null check (mode in ('trecho','banda')),
puzzle_number int not null,
attempts int not null check (attempts between 0 and 6),
won boolean not null,
created_at timestamptz not null default now()
);
create index results_lookup on results (mode, puzzle_number);
alter table results enable row level security; -- só a service role escreve
As estatísticas pessoais continuam em localStorage (gtm:v1:*, com migração segura de schema) e funcionam offline mesmo com o backend ligado.
O código é seu; a música não vem junto. As 8 faixas que acompanham o repositório são geradas por síntese (originais, sem terceiros envolvidos) justamente para o jogo rodar sem material de terceiros.
Para publicar com músicas reais, use apenas conteúdo que você pode usar:
O modo Banda não exige separação de stems: com prévias ele revela por faixa de frequência, que é só filtragem em tempo real do áudio autorizado, sem gerar nem distribuir derivado. Se você quiser a revelação instrumento a instrumento de verdade, aí sim precisa de multipistas próprias, material licenciado para esse uso, ou separação por IA (Demucs, Spleeter) aplicada somente a faixas que você tem direito de processar e distribuir — separar stems de música comercial e publicá-las não é coberto por uso legítimo.
Navegação completa por teclado (Espaço toca/para, Enter envia, setas percorrem o autocomplete, Esc fecha), foco visível, aria-live anunciando cada tentativa, combobox com role/aria-activedescendant, alvos de toque ≥44px, contraste AA e prefers-reduced-motion respeitado.
Nada depende só de som: os efeitos sonoros duplicam informação que já está na tela, e o botão de mudo no cabeçalho desliga todos.