Commit 4bcb27e6 by PLN (Algolia)

feat(parvagues): bio, credibility strip, featured video, track metadata

- Add BioSection (livecoding explainer + genre tags) after hero
- Add AsSeenAt strip (TOPLAP, Algorave GZ, Opal, Cookie, 38C3, 39C3)
- Featured Algorave GZ25 video with neon badge
- New releases: CosmicFest album + Ton Numéro single with covers
- Fix Bandcamp/Spotify/YouTube streaming embeds + privacy consent
- Fix social links: remove Bluesky, GitLab at nech.pl/Tidal
- New live events: 38C3 House of Tea, Chaos Music Club, 39C3 sets
- Track metadata with Tidal code ingredients for 3 pilot gigs
- Poster support on live subpages (Algorave GZ25 poster)
- Expandable track breakdowns with code snippets on subpages
parent 6253c752
const credentials = [
{ name: 'TOPLAP', detail: '20 Years', url: 'https://toplap.org/' },
{ name: 'Algorave', detail: 'GZ Lyon', url: 'https://algorave.com/' },
{ name: 'Opal Festival', url: 'https://www.opal-music.com/' },
{ name: 'Cookie Collective', url: 'https://www.cookie.paris/' },
{ name: '38C3', detail: 'House of Tea', url: 'https://soundcloud.com/parvagues/live-38c3-house-of-tea' },
{ name: '39C3', detail: 'House of Tea', url: 'https://soundcloud.com/parvagues/live-39c3' },
];
export default function AsSeenAt() {
return (
<div className="py-10 border-y border-white/[0.06]">
<div className="max-w-5xl mx-auto px-6">
<p
className="text-[10px] tracking-[0.25em] uppercase text-center mb-6"
style={{ color: 'var(--text-muted)' }}
>
As seen at
</p>
<div className="flex flex-wrap justify-center items-center gap-x-8 gap-y-4">
{credentials.map(({ name, detail, url }) => (
<a
key={name}
href={url}
target="_blank"
rel="noopener noreferrer"
className="group flex items-baseline gap-1.5 transition-colors duration-200"
>
<span
className="font-display font-bold text-sm md:text-base tracking-wide text-white/40 group-hover:text-[var(--neon-high)] transition-colors duration-200"
>
{name}
</span>
{detail && (
<span className="text-[10px] tracking-wider text-white/20 group-hover:text-white/40 transition-colors duration-200">
{detail}
</span>
)}
</a>
))}
</div>
</div>
</div>
);
}
export default function BioSection() {
return (
<section className="reveal py-16 md:py-24">
<div className="max-w-3xl mx-auto px-6 text-center">
<p
className="text-base md:text-lg leading-relaxed"
style={{ color: 'var(--text-primary)' }}
>
<strong className="font-display">ParVagues</strong> est un projet de{' '}
<em>livecoding</em> solo — musique électronique composée et jouée en
temps réel par le code. Algorave, techno, drum &amp; bass, breakbeat :
chaque set est unique, joué sur scène avec{' '}
<a
href="https://tidalcycles.org/"
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 decoration-[var(--neon-high)]/40 hover:decoration-[var(--neon-high)] transition-colors"
>
TidalCycles
</a>{' '}
+ MIDI.
</p>
<div className="flex flex-wrap justify-center gap-2 mt-8">
{['Algorave', 'Techno', 'DnB', 'Breakbeat', 'Livecoding'].map((tag) => (
<span
key={tag}
className="px-3 py-1 text-[11px] tracking-widest uppercase rounded-full border border-white/10 text-[var(--text-muted)]"
>
{tag}
</span>
))}
</div>
</div>
</section>
);
}
...@@ -3,8 +3,8 @@ import Link from 'next/link'; ...@@ -3,8 +3,8 @@ import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { Syne } from 'next/font/google'; import { Syne } from 'next/font/google';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { FaEnvelope, FaInstagram, FaYoutube, FaGithub } from 'react-icons/fa'; import { FaEnvelope, FaInstagram, FaYoutube, FaGitlab } from 'react-icons/fa';
import { SiBluesky, SiMastodon } from 'react-icons/si'; import { SiMastodon } from 'react-icons/si';
const syne = Syne({ const syne = Syne({
subsets: ['latin'], subsets: ['latin'],
...@@ -15,8 +15,7 @@ const syne = Syne({ ...@@ -15,8 +15,7 @@ const syne = Syne({
const socials = [ const socials = [
{ href: 'https://instagram.com/parvagues.mp3', icon: FaInstagram, label: 'Instagram' }, { href: 'https://instagram.com/parvagues.mp3', icon: FaInstagram, label: 'Instagram' },
{ href: 'https://bsky.app/profile/nech.pl', icon: SiBluesky, label: 'Bluesky' }, { href: 'http://nech.pl/Tidal', icon: FaGitlab, label: 'GitLab' },
{ href: 'https://github.com/parvagues', icon: FaGithub, label: 'GitHub' },
{ href: 'https://youtube.com/@parvagues', icon: FaYoutube, label: 'YouTube' }, { href: 'https://youtube.com/@parvagues', icon: FaYoutube, label: 'YouTube' },
{ href: 'https://chaos.social/@PixelNoir', icon: SiMastodon, label: 'Mastodon' }, { href: 'https://chaos.social/@PixelNoir', icon: SiMastodon, label: 'Mastodon' },
]; ];
......
...@@ -15,6 +15,22 @@ function DeezerIcon({ className }) { ...@@ -15,6 +15,22 @@ function DeezerIcon({ className }) {
const albums = [ const albums = [
{ {
id: '2025_cosmicfest',
title: 'Live @ CosmicFest',
image: '/images/parvagues/albums/2025_cosmicfest/cover.jpg',
links: [
{ platform: 'Bandcamp', url: 'https://parvagues.bandcamp.com/album/live-cosmicfest' },
],
},
{
id: '2025_ton_numero',
title: 'Ton Numéro',
image: '/images/parvagues/albums/2025_ton_numero/cover.jpg',
links: [
{ platform: 'Bandcamp', url: 'https://parvagues.bandcamp.com/track/ton-num-ro' },
],
},
{
id: '2024_opal', id: '2024_opal',
title: 'Livecoding (Opal Festival 2024)', title: 'Livecoding (Opal Festival 2024)',
image: '/images/parvagues/albums/2024_opal/cover.jpg', image: '/images/parvagues/albums/2024_opal/cover.jpg',
...@@ -55,7 +71,7 @@ const streamingPlatforms = [ ...@@ -55,7 +71,7 @@ const streamingPlatforms = [
label: 'Bandcamp', label: 'Bandcamp',
icon: FaBandcamp, icon: FaBandcamp,
color: '#1da0c3', color: '#1da0c3',
embedUrl: 'https://bandcamp.com/EmbeddedPlayer/album=3869867806/size=large/bgcol=333333/linkcol=a700d1/tracklist=false/transparent=true/', embedUrl: 'https://bandcamp.com/EmbeddedPlayer/album=644862623/size=large/bgcol=0a0a0a/linkcol=a700d1/tracklist=true/transparent=true/',
embedHeight: 450, embedHeight: 450,
profileUrl: 'https://parvagues.bandcamp.com/', profileUrl: 'https://parvagues.bandcamp.com/',
}, },
...@@ -67,16 +83,20 @@ const streamingPlatforms = [ ...@@ -67,16 +83,20 @@ const streamingPlatforms = [
embedUrl: 'https://open.spotify.com/embed/artist/0kznTQnx5QRhMwktmZboX4?utm_source=generator&theme=0', embedUrl: 'https://open.spotify.com/embed/artist/0kznTQnx5QRhMwktmZboX4?utm_source=generator&theme=0',
embedHeight: 450, embedHeight: 450,
profileUrl: 'https://open.spotify.com/artist/0kznTQnx5QRhMwktmZboX4', profileUrl: 'https://open.spotify.com/artist/0kznTQnx5QRhMwktmZboX4',
isPrivacy: true,
privacyProvider: 'Spotify',
}, },
{ {
id: 'youtube', id: 'youtube',
label: 'YouTube', label: 'YouTube',
icon: FaYoutube, icon: FaYoutube,
color: '#ff0000', color: '#ff0000',
embedUrl: 'https://www.youtube.com/embed?listType=user_uploads&list=@parvagues', embedUrl: 'https://www.youtube-nocookie.com/embed/videoseries?list=OLAK5uy_l4MF3OCIXcdPMpsHGVX2Q9MiX6oU1zT6g',
embedHeight: 400, embedHeight: 400,
profileUrl: 'https://www.youtube.com/@parvagues/videos', profileUrl: 'https://www.youtube.com/@parvagues/videos',
isVideo: true, isVideo: true,
isPrivacy: true,
privacyProvider: 'YouTube (Google)',
}, },
{ {
id: 'deezer', id: 'deezer',
...@@ -131,12 +151,17 @@ function AlbumCard({ album }) { ...@@ -131,12 +151,17 @@ function AlbumCard({ album }) {
} }
function StreamingEmbed({ platform, onClose }) { function StreamingEmbed({ platform, onClose }) {
if (platform.isPrivacy) { const [consent, setConsent] = useState(false);
const Icon = platform.icon;
const providerName = platform.privacyProvider || platform.label;
// Privacy-only platforms (no embed, just redirect)
if (platform.isPrivacy && !platform.embedUrl) {
return ( return (
<div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-12 text-center"> <div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-12 text-center">
<FaInstagram className="w-10 h-10 text-[var(--text-muted)] mx-auto mb-4" /> <Icon className="w-10 h-10 text-[var(--text-muted)] mx-auto mb-4" />
<p className="text-sm text-[var(--text-muted)] mb-6 max-w-sm mx-auto"> <p className="text-sm text-[var(--text-muted)] mb-6 max-w-sm mx-auto">
Le contenu Instagram se connecte aux serveurs de Meta et peut suivre votre activité. Le contenu {providerName} se connecte à des serveurs tiers et peut suivre votre activité.
</p> </p>
<a <a
href={platform.profileUrl} href={platform.profileUrl}
...@@ -144,12 +169,41 @@ function StreamingEmbed({ platform, onClose }) { ...@@ -144,12 +169,41 @@ function StreamingEmbed({ platform, onClose }) {
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-display font-semibold tracking-wider rounded-full border border-white/20 hover:bg-white hover:text-[var(--surface)] transition-all duration-300" className="inline-flex items-center gap-2 px-6 py-3 text-sm font-display font-semibold tracking-wider rounded-full border border-white/20 hover:bg-white hover:text-[var(--surface)] transition-all duration-300"
> >
Voir sur Instagram Voir sur {platform.label}
</a>
</div>
);
}
// Privacy consent gate (has embed but needs user approval)
if (platform.isPrivacy && !consent) {
return (
<div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-12 text-center">
<Icon className="w-10 h-10 text-[var(--text-muted)] mx-auto mb-4" />
<p className="text-sm text-[var(--text-muted)] mb-6 max-w-sm mx-auto">
Le contenu {providerName} se connecte à des serveurs tiers et peut suivre votre activité.
</p>
<div className="flex flex-wrap justify-center gap-4">
<button
onClick={() => setConsent(true)}
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-display font-semibold tracking-wider rounded-full border border-white/20 hover:bg-white hover:text-[var(--surface)] transition-all duration-300"
>
Charger le lecteur
</button>
<a
href={platform.profileUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-display tracking-wider rounded-full text-[var(--text-muted)] hover:text-white transition-all duration-300"
>
Voir sur {platform.label}
</a> </a>
</div> </div>
</div>
); );
} }
// Embed loaded (either no privacy gate, or consent given)
return ( return (
<div className="bg-white/[0.03] border border-white/[0.06] rounded-xl overflow-hidden"> <div className="bg-white/[0.03] border border-white/[0.06] rounded-xl overflow-hidden">
<div style={{ height: platform.isVideo ? undefined : platform.embedHeight }}> <div style={{ height: platform.isVideo ? undefined : platform.embedHeight }}>
......
import { FaInstagram, FaTwitter, FaGithub } from 'react-icons/fa'; import { FaInstagram, FaGitlab } from 'react-icons/fa';
import { SiBluesky, SiMastodon } from 'react-icons/si';
export default function SocialCTA() { export default function SocialCTA() {
return ( return (
<div className="flex flex-wrap justify-center gap-6 py-12"> <div className="flex flex-wrap justify-center gap-6 py-12">
<a <a
href="https://instagram.com/parvagues" href="https://instagram.com/parvagues.mp3"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-3 bg-gradient-to-br from-purple-600 to-pink-600 px-8 py-4 rounded-full text-white font-bold text-lg hover:from-purple-500 hover:to-pink-500 transition-all transform hover:scale-105 shadow-lg hover:shadow-purple-500/50" className="flex items-center gap-3 bg-gradient-to-br from-purple-600 to-pink-600 px-8 py-4 rounded-full text-white font-bold text-lg hover:from-purple-500 hover:to-pink-500 transition-all transform hover:scale-105 shadow-lg hover:shadow-purple-500/50"
...@@ -15,23 +14,13 @@ export default function SocialCTA() { ...@@ -15,23 +14,13 @@ export default function SocialCTA() {
</a> </a>
<a <a
href="https://bsky.app/profile/nech.pl" href="http://nech.pl/Tidal"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 bg-gray-800 hover:bg-gray-700 px-8 py-4 rounded-full text-white font-bold text-lg transition-all transform hover:scale-105 shadow-lg border border-white/10 hover:border-white/30"
>
<SiBluesky className="text-2xl" />
<span>Bluesky</span>
</a>
<a
href="https://github.com/parvagues"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-3 bg-black hover:bg-gray-900 px-8 py-4 rounded-full text-white font-bold text-lg transition-all transform hover:scale-105 shadow-lg border border-white/30 hover:border-white" className="flex items-center gap-3 bg-black hover:bg-gray-900 px-8 py-4 rounded-full text-white font-bold text-lg transition-all transform hover:scale-105 shadow-lg border border-white/30 hover:border-white"
> >
<FaGithub className="text-2xl" /> <FaGitlab className="text-2xl" />
<span>GitHub</span> <span>GitLab</span>
</a> </a>
</div> </div>
); );
......
...@@ -4,14 +4,16 @@ import { FaPlay, FaYoutube } from 'react-icons/fa'; ...@@ -4,14 +4,16 @@ import { FaPlay, FaYoutube } from 'react-icons/fa';
// source: 'archive' (default) | 'youtube' // source: 'archive' (default) | 'youtube'
// For YouTube: id is the YouTube video ID // For YouTube: id is the YouTube video ID
// For archive.org: id is the archive identifier // For archive.org: id is the archive identifier
const videos = [ const featuredVideo = {
{
id: 'bS5jcIJfU6c', id: 'bS5jcIJfU6c',
source: 'youtube', source: 'youtube',
title: 'Algorave GZ25', title: 'Algorave GZ25',
subtitle: 'w/ Pérégrine · GZ Algorave', subtitle: 'w/ Pérégrine · Algorave Lyon — European-wide annual event',
date: 'Fév 2025', date: 'Fév 2025',
}, featured: true,
};
const videos = [
{ {
id: 'toplap-fromscratch-dec2025-parvagues', id: 'toplap-fromscratch-dec2025-parvagues',
title: 'From Scratch: Jungle 🐅', title: 'From Scratch: Jungle 🐅',
...@@ -169,6 +171,16 @@ export default function VideoSection() { ...@@ -169,6 +171,16 @@ export default function VideoSection() {
</h2> </h2>
<div className="h-px bg-white/10 mt-4 mb-12" /> <div className="h-px bg-white/10 mt-4 mb-12" />
{/* Featured video — full width */}
<div className="mb-12">
<div className="relative">
<span className="absolute -top-3 left-4 z-10 px-3 py-0.5 text-[10px] tracking-[0.2em] uppercase font-display font-bold rounded-full bg-[var(--neon-high)] text-white">
Featured
</span>
<VideoCard video={featuredVideo} />
</div>
</div>
<div className="grid sm:grid-cols-2 gap-8 md:gap-10"> <div className="grid sm:grid-cols-2 gap-8 md:gap-10">
{videos.map((video) => ( {videos.map((video) => (
<VideoCard key={video.id} video={video} /> <VideoCard key={video.id} video={video} />
......
---
title: "[38C3] Chaos Music Club"
date: "2024-12-29"
time: "23:00"
location: "Hamburg, Germany"
address: "CCH, Hamburg"
description: "Livecoding set at the Chaos Music Club, 38C3 Chaos Communication Congress."
ctaURL: "https://soundcloud.com/parvagues/live-38c3-chaos-music-club"
ctaText: "Écouter le set"
video: ""
audio: "https://soundcloud.com/parvagues/live-38c3-chaos-music-club"
archive: ""
tags: ["livecoding", "ccc", "38c3", "chaos-music-club", "live"]
---
Livecoding set at the Chaos Music Club, 38C3 edition of the Chaos Communication Congress.
---
title: "[38C3] House of Tea"
date: "2024-12-29"
time: "22:00"
location: "Hamburg, Germany"
address: "CCH, Hamburg"
description: "Livecoding set at the House of Tea, 38C3 Chaos Communication Congress."
ctaURL: "https://soundcloud.com/parvagues/live-38c3-house-of-tea"
ctaText: "Écouter le set"
video: ""
audio: "https://soundcloud.com/parvagues/live-38c3-house-of-tea"
archive: ""
tags: ["livecoding", "ccc", "38c3", "house-of-tea", "live"]
---
Livecoding set at the House of Tea, 38C3 edition of the Chaos Communication Congress.
{
"gig": "38c3-toilet",
"title": "[38C3] Secret Toilet Rave",
"date": "2024-12-28",
"bpmRange": [124, 170],
"styleDistribution": {
"acid-techno": 2,
"punk-dnb": 2,
"downtempo": 1
},
"samplePacks": [
{ "name": "bassWarsaw", "usage": "primary bass synth in Ghosts and L'or Bleu" },
{ "name": "ghost", "usage": "namesake haunted samples cycled through 4 variations" },
{ "name": "rhadamanthe_vocal", "usage": "'self-destruct sequence' vocal drops in Ghosts" },
{ "name": "breaks165 / jungle_breaks", "usage": "breakbeat backbone across multiple tracks" },
{ "name": "vec1_acid / vec2_synth_acid", "usage": "triple-layered acid bass in Acidulé" },
{ "name": "FMRhodes1", "usage": "tremolo piano lead in Acidulé" },
{ "name": "punk", "usage": "punk guitar chops in Nouveau Punk" },
{ "name": "meth_bass", "usage": "dub-delay bass slices in Pitbul Punk" },
{ "name": "moog", "usage": "chromatic lead in Pitbul Punk" },
{ "name": "cpluck", "usage": "'Violoncelle Manouche punko-magique' in Nouveau Punk" },
{ "name": "90s_synatm", "usage": "atmospheric pads with triple destruction bus" },
{ "name": "suns_keys / suns_guitar", "usage": "Leifur James remix samples in L'or Bleu" }
],
"tracks": [
{
"name": "Ghosts in the Toilets",
"file": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"bpm": 160,
"style": "dnb",
"ingredients": [
{ "type": "structure", "code": "setcps (160/60/4)", "description": "160 BPM, fast DNB-adjacent tempo" },
{ "type": "sample", "code": "\"ghost\" # n (slow 4 $ \"<0 1 2 3>\")", "description": "Ghost samples cycling through 4 variations — the track's namesake" },
{ "type": "technique", "code": "slice 16 \"[0 .. 7] . <[0 .. 7]!3 [0 1 . [2 3]]>\"", "description": "Breakbeat slicer on breaks165 with variable resequencing" },
{ "type": "effect", "code": "# octersubsub 2 # octersub 2 # sz 0.01 # room 1.0", "description": "Heavy octave sub-bass doubling with tight reverb" },
{ "type": "pattern", "code": "note (\"<e3 fs3 <gs3 d4> <a3 df4>>\" - 12) # \"bassWarsaw\"", "description": "Dark Warsaw bass with diminished/augmented interval cycle" }
]
},
{
"name": "Nouveau Punk",
"file": "live/collab/raph/nouveau_punk.tidal",
"bpm": 155,
"style": "punk-dnb",
"ingredients": [
{ "type": "structure", "code": "setcps (155/60/4)", "description": "155 BPM — punk energy territory" },
{ "type": "sample", "code": "chop 4 $ \"punk:0/4\" # crushbus 41 (range 16 1.5 \"^53\")", "description": "Punk guitar chopped with MIDI-controlled bitcrush" },
{ "type": "pattern", "code": "note \"<e g a <b4 b4>>\" # \"meth_bass:3\" # hpf 2000", "description": "High-passed meth_bass following E-G-A-B progression" },
{ "type": "technique", "code": "struct \"t(<3!3 5>,8,<0!7 1>)\" $ arp \"up\" ... # \"cpluck:2\"", "description": "Euclidean-rhythmed arpeggiated 'Violoncelle Manouche punko-magique'" },
{ "type": "effect", "code": "# crushbus 52 # octersubbus 51 # octerbus 53", "description": "Triple MIDI-controlled destruction on 90s_synatm synth" }
]
},
{
"name": "Pitbul Punk",
"file": "live/collab/raph/punkachien.tidal",
"bpm": 170,
"style": "punk-dnb",
"ingredients": [
{ "type": "structure", "code": "setcps (170/60/4)", "description": "170 BPM — full DNB speed, fastest track" },
{ "type": "pattern", "code": "note (\"d*<3 4 3 2> . ds ds df\" + 2) # \"vec1_acid:11\"", "description": "Acid bassline with variable-density note patterns" },
{ "type": "technique", "code": "superimpose ((|* gain \"^17\") . (# n 10) . (# legato 0.8))", "description": "Layered acid superimposition with MIDI gain crossfade" },
{ "type": "sample", "code": "loopAt 2 $ slice 2 $ \"meth_bass:9\" # delay ... # delayfb ...", "description": "Meth bass with building dub-delay wash (0 to 0.8 feedback)" },
{ "type": "pattern", "code": "note \"<a as <a a df5 df5> <d df>>\" # \"moog\"", "description": "Moog lead with chromatic A-A#-A/Db-D progression" }
]
},
{
"name": "Acidulé",
"file": "live/collab/raph/acidule.tidal",
"bpm": 135,
"style": "acid-techno",
"ingredients": [
{ "type": "structure", "code": "setcps (135/60/4)", "description": "135 BPM acid techno groove" },
{ "type": "pattern", "code": "n \"<30!16 31!16>\" # \"vec2_synth_acid\"", "description": "Triple-layered acid bass stack: vec2 alternating + two vec1 lines" },
{ "type": "technique", "code": ">| note (\"[b d6 b cs6 d6 b cs6 b]*2\" - \"<0!32 12!32>\")", "description": "Acid sequence with octave drop every 32 cycles" },
{ "type": "sample", "code": "note (\"b ~ ~ b ~ ~ <d!7 e6> <~ d>\") # \"FMRhodes1\" # tremdp 4 # tremr 1.8", "description": "FM Rhodes 'Piano lead acidulé' with tremolo droplet texture" },
{ "type": "pattern", "code": "note (\"<b5!3 <g d>>@7 <c6 c6 cs6 a6>\") # \"moog:0\"", "description": "Caring bass — Moog with chromatic C-C#-A movement" }
]
},
{
"name": "L'or Bleu",
"file": "live/collab/mousquetaires/blue_gold.tidal",
"bpm": 124,
"style": "downtempo",
"ingredients": [
{ "type": "structure", "code": "setcps(124/60/4)", "description": "124 BPM — slowest, most melodic track" },
{ "type": "sample", "code": "\"suns_keys\" # n \"<0 <1 2> 3 3>\"", "description": "Suns of Gold keyboard stabs with tremolo and euclidean slicing" },
{ "type": "technique", "code": "fast (4/3) $ note (\"<[fs4 <a5!2 gs!4 fs5!4 ...>@3]*4>\")", "description": "Polymetric (4/3) Warsaw bass — triplets against 4/4 grid" },
{ "type": "sample", "code": "slice 8 \"<0 1 2 3>(3,8)\" $ \"suns_guitar:2\" # room 0.8", "description": "Euclidean-sliced guitar with lush reverb and L/R panning" },
{ "type": "effect", "code": "# crushbus 73 (range 16 2.9 \"^55\") # lpfbus 72", "description": "'CRASH/BURN THIS GUITAR' — MIDI-driven total destruction" }
]
}
]
}
{
"gig": "ccc-live",
"title": "CCC LIVE - Cookie Collective",
"date": "2024-10-01",
"bpmRange": [120, 160],
"styleDistribution": {
"nu-jazz": 3,
"dnb": 2,
"acid-techno": 1,
"breakbeat": 1,
"hybrid": 2
},
"samplePacks": [
{ "name": "nujazz_bass120", "usage": "bass foundation for nu-jazz tracks (Cafe Glace, Salut Nu)" },
{ "name": "nujazz_bass125", "usage": "bass foundation for Cafe Tiede and Force Motrice" },
{ "name": "nujazz_keys120/125", "usage": "jazz chord progressions across 4 tracks" },
{ "name": "jungle_breaks", "usage": "breakbeat layers in Cafe Glace (#22, #79, #19)" },
{ "name": "vec1_acid / vec2_synth_acid", "usage": "acid bass stack in L'ACID d'abord" },
{ "name": "suns_keys / suns_guitar", "usage": "Leifur James remix samples in Blue Gold" },
{ "name": "FMRhodes1/2", "usage": "FM synthesis piano, used in 3+ tracks" },
{ "name": "bassWarsaw", "usage": "primary bass synth across multiple tracks" },
{ "name": "fbass", "usage": "chopped bass in Alerte Verte" },
{ "name": "bass_commodore", "usage": "granular bass in Nuit Agitee" },
{ "name": "movie_vitality", "usage": "'I WANT TO GO TO MARS' vocal in Nass Revient" },
{ "name": "giorgio_syn", "usage": "Moroder tribute synth in Force Motrice" },
{ "name": "cbow", "usage": "cello bow with stutter delay in Force Motrice" },
{ "name": "meth_bass", "usage": "glitch bass layer in Cafe Glace" }
],
"tracks": [
{
"name": "L'ACID d'abord <3",
"file": "live/collab/raph/acidule.tidal",
"bpm": 135,
"style": "acid-techno",
"ingredients": [
{ "type": "structure", "code": "setcps (135/60/4)", "description": "135 BPM, acid techno tempo" },
{ "type": "sample", "code": "n \"<30!16 31!16>\" # \"vec2_synth_acid\"", "description": "Layered acid synth voices from vec2 bank, alternating presets every 16 cycles" },
{ "type": "sample", "code": "\"vec1_acid:40\" # hpf 200 # gain (range 0.6 1.1 \"^53\")", "description": "Secondary acid layer with high-pass filter, MIDI-controlled gain crossfade" },
{ "type": "technique", "code": ">| note (\"[b d6 b cs6 d6 b cs6 b]*2\" - \"<0!32 12!32>\")", "description": "Overriding note pattern with octave drop alternance every 32 cycles" },
{ "type": "effect", "code": "# tremdp 4 # tremr 1.8", "description": "Tremolo on the FM Rhodes lead creating a bouncy droplet texture" }
]
},
{
"name": "Alerte Verte",
"file": "live/midi/nova/dnb/alerte_verte.tidal",
"bpm": 160,
"style": "dnb",
"ingredients": [
{ "type": "structure", "code": "setcps (160/60/4)", "description": "160 BPM drum & bass tempo" },
{ "type": "sample", "code": "$ \"fbass:25\"", "description": "Chopped bass sample sliced into 16 parts via loopAt 4" },
{ "type": "technique", "code": "$ whenmod 16 12 (struct \"t*<4 4 8 16>\")", "description": "Snare rush building from quarter to 16th notes every 16 cycles" },
{ "type": "effect", "code": "# leslie \"^34\" # lsize (1 + (slow 16 perlin * 2)) # lrate 6.7", "description": "MIDI-controlled Leslie speaker with perlin-modulated cabinet size" },
{ "type": "sample", "code": "$ \"[808lc,<~!4 808mc!4>]\" # speed (range 0 2 \"^55\")", "description": "808 clave pattern with MIDI-pitchable speed" }
]
},
{
"name": "Blue Gold",
"file": "live/collab/ccc/ccc0.tidal",
"bpm": 140,
"style": "hybrid",
"ingredients": [
{ "type": "structure", "code": "setcps(140/60/4)", "description": "140 BPM, faster CCC live version of the Leifur James remix" },
{ "type": "sample", "code": "$ \"suns_keys\" # n \"<0 <1 2> 3 3>\"", "description": "Sampled keys with slice manipulation (8 slices, euclidean patterns)" },
{ "type": "technique", "code": "$ fast (4/3) $ note (\"<[fs4 <a5!2 gs!4 fs5!4 ...>@3]*4>\")", "description": "Triplet-feel bassline (4/3 speed) with complex ascending/descending notes" },
{ "type": "sample", "code": "$ \"suns_guitar:0/2\" # octersubbus 71", "description": "Sampled guitar with MIDI-controlled sub-octave destruction" },
{ "type": "effect", "code": "# crushbus 73 (range 16 2.9 \"^55\")", "description": "MIDI-controlled bitcrush from clean 16-bit to destroyed 2.9-bit" }
]
},
{
"name": "Nuit Agitee",
"file": "live/midi/nova/breaks/nuit_agitee.tidal",
"bpm": 160,
"style": "breakbeat",
"ingredients": [
{ "type": "structure", "code": "setcps (160/60/4)", "description": "160 BPM breakbeat tempo" },
{ "type": "technique", "code": "let scale = getScale (scaleTable ++ [(\"gameboy\", [1,2,4,5,7,9,11])])", "description": "Custom 'gameboy' scale definition for melodic content" },
{ "type": "sample", "code": "$ loopAt 8 $ chop 64 $ \"bass_commodore:7\"", "description": "Commodore bass chopped into 64 slices — granular bass texture" },
{ "type": "effect", "code": "# crushbus 41 # octersubbus 42 # lesliebus 43", "description": "Triple simultaneous bus effects on bass, all MIDI-controlled" },
{ "type": "sample", "code": "# \"harpsichord2\" # distortbus 51", "description": "Harpsichord lead using gameboy scale with MIDI distortion" }
]
},
{
"name": "Nass Revient de Mars!",
"file": "live/dnb/nass_revient.tidal",
"bpm": 140,
"style": "hybrid",
"ingredients": [
{ "type": "structure", "code": "setcps (140/60/4)", "description": "140 BPM hybrid techno/DNB" },
{ "type": "technique", "code": "$ superimpose (|+| note \"~ 7(<3!3 5>,8)\")", "description": "Supersaw bassline with euclidean 5th-interval superimposition" },
{ "type": "sample", "code": "$ \"forgan:24\" # crushbus 5", "description": "Massive organ sample with perlin-noise-driven bitcrush" },
{ "type": "technique", "code": "arp \"up\" \"<[c'maj'4 c'maj'3]!12 ...>\"", "description": "FM Rhodes arpeggios using chord inversions" },
{ "type": "sample", "code": "$ \"movie_vitality:10\"", "description": "'I WANT TO GO TO MARS' — movie sample with expanding window" }
]
},
{
"name": "Atelier de Force Motrice",
"file": "live/midi/nova/dnb/force_motrice.tidal",
"bpm": 125,
"style": "nu-jazz",
"ingredients": [
{ "type": "structure", "code": "setcps (125/60/4)", "description": "125 BPM, nu-jazz/breaks territory" },
{ "type": "sample", "code": "$ chop 16 $ loopAt 4 $ \"nujazz_bass125:1\"", "description": "Nu-jazz bass chopped into 16 slices with MIDI crush and octave" },
{ "type": "technique", "code": "# modIndex (range 0 2 \"^34\") # \"FMRhodes1\"", "description": "FM Rhodes with MIDI-controlled FM synthesis depth" },
{ "type": "sample", "code": "$ \"giorgio_syn:22\"", "description": "Giorgio synth (Moroder tribute) with crushable bus effect" },
{ "type": "effect", "code": "$ stut 4 0.65 \"e\" $ note ... # \"cbow:2\"", "description": "Cello bow with 4-step stutter delay creating ghostly trails" }
]
},
{
"name": "Cafe Tiede",
"file": "live/midi/nova/nujazz/cafe_tiede.tidal",
"bpm": 125,
"style": "nu-jazz",
"ingredients": [
{ "type": "structure", "code": "setcps(125/60/4)", "description": "125 BPM nu-jazz groove" },
{ "type": "sample", "code": "$ chop 8 $ loopAt 4 $ \"nujazz_bass125:1\"", "description": "Nu-jazz bass with 4-bar loop chopped into 8 slices" },
{ "type": "sample", "code": "$ chop 8 $ loopAt 2 $ \"nujazz_keys125\" # n \"<56 56 57 ...>\"", "description": "Nu-jazz keys with 9 preset rotations creating chord progressions" },
{ "type": "effect", "code": "# roombus 110 (slow 16 $ range 0 0.9 saw)", "description": "Slowly building reverb over 16 cycles combined with sine LPF sweep" },
{ "type": "technique", "code": "$ slice 4 \"<<[0 1 2 3] [0 1 0 1 2 3 2 3]> ...>\"", "description": "Keys with alternating simple/doubled slice patterns" }
]
},
{
"name": "Cafe Glace",
"file": "live/midi/nova/nujazz/cafe_glace.tidal",
"bpm": 120,
"style": "nu-jazz",
"ingredients": [
{ "type": "structure", "code": "setcps(120/60/4)", "description": "120 BPM, cooler than Cafe Tiede" },
{ "type": "sample", "code": "$ \"nujazz_bass120:4\"", "description": "Nu-jazz bass at 120 BPM with MIDI-controlled bitcrush" },
{ "type": "technique", "code": "$ midiOn \"^58\" ((# freeze 0.2))", "description": "Spectral freeze effect triggered by MIDI on keys" },
{ "type": "sample", "code": "$ \"meth_bass\" # crush 8 # lpf (slow 32 $ range 4000 400 sine)", "description": "'UN GLITCH DANS MON CAFE!!?!' — glitch bass with closing filter" },
{ "type": "technique", "code": "$ midiOn \"^55\" (loopAt 1 . (|> \"jungle_breaks:19\"))", "description": "MIDI-switchable jungle break layers with variable slice density" }
]
},
{
"name": "Salut Nu",
"file": "live/midi/nova/nujazz/salut_nu.tidal",
"bpm": 120,
"style": "nu-jazz",
"ingredients": [
{ "type": "structure", "code": "setcps(120/60/4)", "description": "120 BPM nu-jazz encore" },
{ "type": "technique", "code": "juxBy 0.9 (mask ... . rev . (|+ note 12) . (|+ pan 0.2))", "description": "MIDI-triggered stereo mirror: reversed, octave-up, panned piano reflection" },
{ "type": "sample", "code": "$ slice 4 ... $ \"nujazz_keys120\" # n \"<54!16 53!16>\"", "description": "Piano with 16-step slice choreography between two presets" },
{ "type": "sample", "code": "note ... # \"bassWarsaw\" # octersub 0.5 # squizbus 42", "description": "Warsaw bass with sub-octave and MIDI-controlled squiz distortion" },
{ "type": "technique", "code": "$ jux rev $ note ... # \"moogBass\" # chorus 0.2 # room 0.8", "description": "'La chorale du jugement' — Moog bass with stereo call-and-response" }
]
}
]
}
---
title: "[39C3] House of Tea"
date: "2025-12-28"
time: "22:00"
location: "Hamburg, Germany"
address: "CCH, Hamburg"
description: "Livecoding set at the House of Tea, 39C3 Chaos Communication Congress."
ctaURL: "https://soundcloud.com/parvagues/live-39c3"
ctaText: "Écouter le set"
video: ""
audio: "https://soundcloud.com/parvagues/live-39c3"
archive: ""
tags: ["livecoding", "ccc", "39c3", "house-of-tea", "live"]
---
Livecoding set at the House of Tea, 39C3 edition of the Chaos Communication Congress.
---
title: "[39C3] Toilet Rave"
date: "2025-12-29"
time: "01:00"
location: "Hamburg, Germany"
address: "CCH, Hamburg"
description: "Livecoding set at the Toilet Rave, 39C3 Chaos Communication Congress."
ctaURL: "https://soundcloud.com/parvagues/39c3_toiletrave_master"
ctaText: "Écouter le set"
video: ""
audio: "https://soundcloud.com/parvagues/39c3_toiletrave_master"
archive: ""
tags: ["livecoding", "ccc", "39c3", "toilet-rave", "live"]
---
Livecoding set at the Toilet Rave, 39C3 edition of the Chaos Communication Congress.
--- ---
title: "AlgoRave Lyon 2025" title: "AlgoRave Lyon 2025"
date: "2025-05-25" date: "2025-05-24"
time: "XX:XX" time: "18:00"
location: "GR TBD" location: "Vaulx-en-Velin, France"
address: " GRRRND ZERO VAULX, 56 - 60 Avenue Bohlen, Vaulx-en-Velin" address: " GRRRND ZERO VAULX, 56 - 60 Avenue Bohlen, Vaulx-en-Velin"
description: "AlgoRave 18h-06h avec musique installations et performances audiovisuelles." description: "AlgoRave 18h-06h avec musique installations et performances audiovisuelles."
ctaURL: "https://www.grrrndzero.org/index.php/2672-sam-24-05-algorave" ctaURL: "https://www.grrrndzero.org/index.php/2672-sam-24-05-algorave"
...@@ -36,8 +36,9 @@ teasing3: | ...@@ -36,8 +36,9 @@ teasing3: |
Tonight @ Lyon Tonight @ Lyon
video: "" video: ""
audio: "" audio: "https://parvagues.bandcamp.com/track/live-algorave-ground-zero-2025"
archive: "" archive: ""
poster: "/images/parvagues/lives/2025/algorave-lyon/poster.jpg"
tags: ["livecoding", "algorave", "lyon", "tidal"] tags: ["livecoding", "algorave", "lyon", "tidal"]
--- ---
......
---
title: "CosmicFest v0"
date: "2025-06-21"
time: "20:00"
location: "Biarritz region, France"
address: ""
description: "Live set at CosmicFest v0."
ctaURL: "https://me.nech.pl/cosmicfest"
ctaText: "Voir l'event"
video: ""
audio: "https://parvagues.bandcamp.com/album/live-cosmicfest"
archive: ""
tags: ["livecoding", "cosmicfest", "live"]
---
Live set at CosmicFest v0.
...@@ -8,7 +8,7 @@ description: "Afterparty set for Raise Summit." ...@@ -8,7 +8,7 @@ description: "Afterparty set for Raise Summit."
ctaURL: "" ctaURL: ""
ctaText: "" ctaText: ""
video: "" video: ""
audio: "" audio: "https://parvagues.bandcamp.com/track/ton-num-ro"
archive: "" archive: ""
tags: ["livecoding", "raise", "afterparty", "techno"] tags: ["livecoding", "raise", "afterparty", "techno"]
--- ---
......
...@@ -60,6 +60,21 @@ export async function getLiveData(slug) { ...@@ -60,6 +60,21 @@ export async function getLiveData(slug) {
throw new Error(`Live with slug "${slug}" not found`); throw new Error(`Live with slug "${slug}" not found`);
} }
export function getLiveTracks(slug) {
const years = fs.readdirSync(livesDirectory).filter(item =>
fs.statSync(path.join(livesDirectory, item)).isDirectory()
);
for (const year of years) {
const tracksPath = path.join(livesDirectory, year, slug, 'tracks.json');
if (fs.existsSync(tracksPath)) {
return JSON.parse(fs.readFileSync(tracksPath, 'utf8'));
}
}
return null;
}
export function getLivesImages(slug) { export function getLivesImages(slug) {
const years = fs.readdirSync(livesDirectory).filter(item => const years = fs.readdirSync(livesDirectory).filter(item =>
fs.statSync(path.join(livesDirectory, item)).isDirectory() fs.statSync(path.join(livesDirectory, item)).isDirectory()
......
import { getAllLives } from '@/lib/livesData'; import { getAllLives } from '@/lib/livesData';
import Layout from '@/components/parvagues/Layout'; import Layout from '@/components/parvagues/Layout';
import Hero from '@/components/parvagues/Hero'; import Hero from '@/components/parvagues/Hero';
import BioSection from '@/components/parvagues/BioSection';
import AsSeenAt from '@/components/parvagues/AsSeenAt';
import TourTimeline from '@/components/parvagues/TourTimeline'; import TourTimeline from '@/components/parvagues/TourTimeline';
import MusicSection from '@/components/parvagues/MusicSection'; import MusicSection from '@/components/parvagues/MusicSection';
import VideoSection from '@/components/parvagues/VideoSection'; import VideoSection from '@/components/parvagues/VideoSection';
...@@ -10,6 +12,8 @@ export default function ParVagues({ lives }) { ...@@ -10,6 +12,8 @@ export default function ParVagues({ lives }) {
return ( return (
<Layout title="ParVagues - Musique Algorithmique"> <Layout title="ParVagues - Musique Algorithmique">
<Hero /> <Hero />
<BioSection />
<AsSeenAt />
<TourTimeline lives={lives} /> <TourTimeline lives={lives} />
<MusicSection /> <MusicSection />
<VideoSection /> <VideoSection />
......
import { useState, useEffect } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import { getAllLives, getLiveData, getLivesImages, getLiveTracks } from '@/lib/livesData';
import Layout from '@/components/parvagues/Layout';
import Countdown from '@/components/parvagues/Countdown';
import ImageGallery from '@/components/parvagues/ImageGallery';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
import ReactMarkdown from 'react-markdown';
import Prism from 'prismjs';
import 'prismjs/components/prism-haskell';
import 'prismjs/themes/prism-tomorrow.css';
function TrackIngredient({ ingredient }) {
return (
<div className="bg-black/30 rounded-lg p-3 border border-white/5">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[10px] tracking-widest uppercase px-2 py-0.5 rounded-full border border-[var(--neon-high)]/20 text-[var(--neon-high)]">
{ingredient.type}
</span>
</div>
<code className="block text-xs text-[var(--text-muted)] font-mono leading-relaxed mb-1 overflow-x-auto">
{ingredient.code}
</code>
<p className="text-[11px] text-[var(--text-muted)]">{ingredient.description}</p>
</div>
);
}
function TracksSection({ tracks }) {
if (!tracks) return null;
return (
<div className="mb-16">
<div className="flex items-center gap-4 mb-6">
<h2 className="font-display text-2xl font-bold text-white">
<span className="text-[var(--neon-high)]">#</span> Tracks
</h2>
<div className="h-px bg-[var(--neon-high)]/20 flex-grow" />
</div>
{tracks.styleDistribution && (
<div className="flex flex-wrap gap-2 mb-8">
{Object.entries(tracks.styleDistribution).map(([style, count]) => (
<span key={style} className="px-3 py-1 text-[11px] tracking-widest uppercase rounded-full border border-white/10 text-[var(--text-muted)]">
{style} ×{count}
</span>
))}
{tracks.bpmRange && (
<span className="px-3 py-1 text-[11px] tracking-widest uppercase rounded-full border border-[var(--neon-high)]/20 text-[var(--neon-high)]">
{tracks.bpmRange[0]}{tracks.bpmRange[1]} BPM
</span>
)}
</div>
)}
<div className="space-y-6">
{tracks.tracks.map((track, i) => (
<details key={track.name} className="group bg-white/[0.02] border border-white/[0.06] rounded-xl overflow-hidden">
<summary className="flex items-center gap-4 px-5 py-4 cursor-pointer hover:bg-white/[0.03] transition-colors">
<span className="font-mono text-xs text-[var(--text-muted)] w-6">{String(i + 1).padStart(2, '0')}</span>
<span className="font-display font-semibold text-sm flex-grow">{track.name}</span>
{track.bpm && <span className="text-[11px] text-[var(--text-muted)] font-mono">{track.bpm} BPM</span>}
{track.style && <span className="text-[10px] tracking-wider uppercase text-[var(--text-muted)]">{track.style}</span>}
</summary>
<div className="px-5 pb-5 pt-2 space-y-3 border-t border-white/[0.04]">
{track.ingredients.map((ing, j) => (
<TrackIngredient key={j} ingredient={ing} />
))}
{track.file && (
<p className="text-[10px] text-[var(--text-muted)] font-mono mt-2">
src: {track.file}
</p>
)}
</div>
</details>
))}
</div>
</div>
);
}
export default function LiveEvent({ live, images, tracks }) {
const [phase, setPhase] = useState('early'); // early, J-14, J-7, J-3, live, post
const [isPostEvent, setIsPostEvent] = useState(false);
useEffect(() => {
// Highlight code blocks
Prism.highlightAll();
}, [live, phase]);
useEffect(() => {
const checkDate = () => {
const now = new Date();
const eventDate = new Date(live.date);
if (now > eventDate) {
setIsPostEvent(true);
setPhase('post');
}
};
checkDate();
const interval = setInterval(checkDate, 60000); // Check every minute
return () => clearInterval(interval);
}, [live.date]);
// Determine which content to show based on phase
const getTeasingContent = () => {
if (isPostEvent) return null;
// Logic to select teasing content from frontmatter based on phase
// Assuming frontmatter has fields like teasing_j7, teasing_j3 etc.
// For now, we'll just show the main description + countdown
return (
<div className="text-center mb-12">
<div className="inline-block p-6 bg-black/50 backdrop-blur-md rounded-xl border border-purple-500/30 shadow-[0_0_30px_rgba(137,0,179,0.2)]">
<h3 className="text-purple-400 mb-4 font-mono text-sm">&gt; INITIALIZING_EVENT</h3>
<Countdown targetDate={live.date} onPhaseChange={setPhase} />
</div>
</div>
);
};
return (
<Layout title={`${live.title} - ParVagues`}>
<div className="max-w-4xl mx-auto">
{/* Header Info */}
<div className="text-center mb-16">
<div className="inline-block px-3 py-1 mb-4 text-xs font-mono text-purple-300 border border-purple-500/30 rounded-full">
{(() => {
const date = new Date(live.date);
return isNaN(date.getTime()) ? 'Date TBD' : format(date, 'dd MMMM yyyy', { locale: fr });
})()}
</div>
<h1 className="text-4xl md:text-6xl font-bold mb-4 bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent">
{live.title}
</h1>
<p className="text-xl text-gray-400 flex items-center justify-center gap-2">
<span className="w-2 h-2 bg-purple-500 rounded-full animate-pulse"></span>
{live.location}
</p>
</div>
{/* Poster */}
{live.frontmatter.poster && (
<div className="mb-16 rounded-2xl overflow-hidden border border-white/[0.06]">
<Image
src={live.frontmatter.poster}
alt={`${live.title} poster`}
width={1200}
height={1710}
className="w-full h-auto"
priority
/>
</div>
)}
{/* Countdown / Teasing */}
{!isPostEvent && getTeasingContent()}
{/* Main Content */}
<div className="prose prose-invert prose-purple max-w-none mb-16 bg-black/30 p-8 rounded-2xl border border-white/5">
<ReactMarkdown>{live.content}</ReactMarkdown>
</div>
{/* Tracks & Code */}
<TracksSection tracks={tracks} />
{/* Gallery (Post-event or if images exist) */}
{images && images.length > 0 && (
<div className="mb-16">
<div className="flex items-center gap-4 mb-8">
<h2 className="text-2xl font-bold text-white">
<span className="text-purple-500">#</span> Galerie
</h2>
<div className="h-px bg-purple-500/30 flex-grow"></div>
</div>
<ImageGallery images={images} />
</div>
)}
</div>
</Layout>
);
}
export async function getStaticPaths() {
const lives = getAllLives();
const paths = lives.map((live) => ({
params: { slug: live.slug },
}));
return { paths, fallback: false };
}
export async function getStaticProps({ params }) {
const live = await getLiveData(params.slug);
const images = getLivesImages(params.slug);
const tracks = getLiveTracks(params.slug);
return {
props: {
live,
images,
tracks,
},
revalidate: 60,
};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment