diff --git a/gigs/thomaslawson.com/work-fia-edits/mu-current/ac-wrapper-links.php b/gigs/thomaslawson.com/work-fia-edits/mu-current/ac-wrapper-links.php new file mode 100644 index 0000000000..bbc74231bd --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/mu-current/ac-wrapper-links.php @@ -0,0 +1,61 @@ + + (function() { + var links = $json; + Object.keys(links).forEach(function(id) { + var el = document.querySelector('[data-id=\"' + id + '\"]'); + if (el) { + el.style.cursor = 'pointer'; + el.addEventListener('click', function(e) { + if (e.target.closest('a')) return; + window.location.href = links[id]; + }); + } + }); + })(); + \n"; +} + +function ac_find_wrapper_links($elements, &$links) { + foreach ($elements as $el) { + $settings = $el['settings'] ?? []; + if (!empty($settings['enable_wrapper_link']) && $settings['enable_wrapper_link'] === 'yes') { + $url = $settings['wrapper_link']['url'] ?? ''; + if ($url && !empty($el['id'])) { + // Update http to https + $url = str_replace('http://', 'https://', $url); + $links[$el['id']] = $url; + } + } + if (!empty($el['elements'])) { + ac_find_wrapper_links($el['elements'], $links); + } + } +} diff --git a/gigs/thomaslawson.com/work-fia-edits/mu-current/automation-by-installatron.php b/gigs/thomaslawson.com/work-fia-edits/mu-current/automation-by-installatron.php new file mode 100644 index 0000000000..c2718c1b13 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/mu-current/automation-by-installatron.php @@ -0,0 +1,25 @@ + (after Astra inline CSS and the external + * Elementor stylesheets) so the !important overrides reliably win. + */ +add_action('wp_head', 'tl_no_animations_css', PHP_INT_MAX); + +function tl_no_animations_css() { + ?> + + + + + + + + About – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

I make art, and think about art, to better understand life. Over the years this work has taken many forms, but a consideration of painting has always remained central. But in chasing these questions about art and life I have also written essays and reviews, published and edited magazines, organized exhibitions, and devoted a lot of time and energy to helping younger artists figure out what they need to do.

+
+
+
+

“More than any artist/writer of his generation, Thomas Lawson has the make up of a true journalist. He is an embedded correspondent, a polemical editorialist, sending his dispatches to a radically independent media that happens to be his own (or that he makes his own). Lawson always writes in the first person, from the vantage point of someone who is as much an artist, publisher, and occasional curator as he is a writer. Reacting to the ambient hegemony of late modernism and its avant-garde myths, Lawson nevertheless manages to uphold a real oppositional modus operandi, a determination to take sides, to make critical distinctions, attempting along the way to enunciate a progressive position from the very heart of an increasingly reactionary milieu.”

+

+

From introductory essay, Mining for Gold; Thomas Lawson, Selected Writings (1979-1996), eds., Lionel Bovier and Fabrice Stroun, JRP/Ringier, Zurich 2004.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1975 - 1980

+
+
+
+

I moved to New York in 1975 in search of a community that would sharpen my thinking and support my growth. Within a few years I was publishing reviews and essays in Art in America and Flash Art, and participating in group shows in various non-profit spaces around the city. In early 1979 Susan Morgan and I began publishing REALLIFE Magazine, which sought to be a forum for younger artists working to reinvestigate the power of imagery following a decade of art’s dematerialization into idea and action.

+
+
+
+
+
+
+
+
+
+
+
+ Gold_Dog
+
+
+
+
GOLD DOG
+
+
+
+
1978
+
+
+
+
+
+
+
+ REALLIFE
+
+
+
+
REALLIFE COVER
+
+
+
+
-
+
+
+
+
+
+
+
+ FORMANESQUE
+
+
+
+
FORMANESQUE DRAWING WALL
+
+
+
+
1977
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1980 – 1990

+
+
+
+

During this decade I established a public voice as both a painter and a writer. I exhibited annually, between MetroPictures in New York, Richard Khulenschmidt Gallery in Los Angeles, and Anthony Reynolds Gallery in London. I also published a stream of essays in REALLIFE Magazine and Artforum, seeking to further develop my ideas about the moment in art history. In the late 80s I began to look for ways to expand the audience for my painting, and began a series of public works, mostly temporary murals in New York and various cities across the UK.

+
+
+
+
+
+
+
+
+
+
+
+ RED SHOE
+
+
+
+
RED SHOE
+
+
+
+
1979
+
+
+
+
+
+
+
+ LAST EXIT
+
+
+
+
LAST EXIT
+
+
+
+
-
+
+
+
+
+
+
+
+ SHOT FOR A BIKE
+
+
+
+
SHOT FOR A BIKE
+
+
+
+
1981
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1990 – 2000

+
+
+
+

I had been teaching as an adjunct at various art schools during the 80s, and in 1990 made the decision to seek a fulltime position. Following an unexpected invitation, I began my long tenure as dean of the Art School at CalArts in January 1991. The year before I had mounted a survey exhibition at the Third Eye Center in Glasgow, and took that as an opportunity to reevaluate my studio practice. At the same time, influenced by my new perspective as an art teacher, I began thinking about the work of a younger generation. This work generated a number of exhibitions, interviews, and essays, many focusing on the new work coming out of London and Glasgow at that time. Thanks to a sabbatical, I was able to spend some time in Scotland during the 1998 referendum on devolution, and began research on a multi-year project concerning Scottish republicanism during the time of the French Revolution, a project which resulted in several series of paintings and installations, and several small chapbooks.

+
+
+
+
+
+
+
+
+
+
+
+ BRITISH ART SHOW
+
+
+
+
THE BRITISH ART SHOW
+
+
+
+
-
+
+
+
+
+
+
+
+ BROAD STUDIOS
+
+
+
+
BROAD STUDIOS
+
+
+
+
-
+
+
+
+
+
+
+
+ LOS ANGELES PAINTING
+
+
+
+
LOS ANGELES PAINTING​
+
+
+
+
1996
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2000 – 2010

+
+
+
+

The Scottish project reignited my interest in painting, and I moved into a larger studio. By the end of the decade, I was showing in Los Angeles and New York, and was recognized for the work by an award from the John Simon Guggenheim Memorial Foundation. I also published several essays considering a range of painters, from Laura Owens and Richard Wright, to Patrick Caulfield and Gerard Hemsworth. In 2002 I became a co-editor of the London-based journal, Afterall, and continued with that collaboration until 2009.

+
+
+
+
+
+
+
+
+
+
+
+ SUBURBAN INSTALL
+
+
+
+
SUBURBAN INSTALL
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
PATRICK CAUFIELD ESSAY
+
+
+
+
-
+
+
+
+
+
+
+
+ PARTICIPANT INSTALL
+
+
+
+
PARTICIPANT INSTALL
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2010 – 2020

+
+
+
+

After a year of design work, I launched East of Borneo, an online magazine and archive dedicated to documenting art as seen from Los Angeles. This gave me a dynamic platform from which to explore the ongoing histories and contributions of CalArts to that larger discourse, through commissioning essays, and writing my own. The platform also provided support for two ambitious projects, a reading exhibition about the experimental pedagogies tried in the early years of CalArts, and a book-centered place making project located in several communities in South Los Angeles.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
THE JOURNEY WEST
+
+
+
+
2010
+
+
+
+
+
+
+
+
+
+
+
+
PATTERN OF THOUGHT
+
+
+
+
2011
+
+
+
+
+
+
+
+
+
+
+
+
DREAMS OF THE ARROGANT PRINCE
+
+
+
+
2017
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2020 onwards

+
+
+
+

The new decade began with a worldwide pandemic, closing down so much social and cultural life. Nevertheless, I continued painting and writing. We hired a new team at East of Borneo, and it continues to thrive. This section will highlight new developments as they occur, and we shall see if a theme arises.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
STUDIO
+
+
+
+
2021
+
+
+
+
+
+
+
+
+
+
+
+
MICHAEL ASHER
+
+
+
+
2022
+
+
+
+
+
+
+
+
+
+
+
+
DISCARDED PROTECTION
+
+
+
+
2022
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/beyond-the-studio-v2.html b/gigs/thomaslawson.com/work-fia-edits/pages/beyond-the-studio-v2.html new file mode 100644 index 0000000000..dc26635b74 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/beyond-the-studio-v2.html @@ -0,0 +1,818 @@ + + + + + + + Beyond the Studio – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Beyond the Studio

+
+
+
+
+ + +
+
+
+
+
+

Studio work is by its very nature private, a personal exploration of ideas and themes. This offers tremendous freedom, but limits exposure to the relatively privileged world of galleries and museums.  This section is about various projects and proposals to address a wider public, to bring the work to the streets. The challenge is to condense and simplify, without losing complexity.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Painted Installations
+
+
+
+
+
+
+
+ + +
+
+
+
+
Early New York
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Dark Installations
+
+
+
+
+
+
+
+ + +
+
+
+
+
Temporary Murals
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Glasgow Projects
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Theatre, Dance & Fashion
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Los Angeles
+
+
+
+
+
+
+
+ + +
+
+
+
+
The Scottish Project​
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/beyond-the-studio.html b/gigs/thomaslawson.com/work-fia-edits/pages/beyond-the-studio.html new file mode 100644 index 0000000000..cb4ef38b81 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/beyond-the-studio.html @@ -0,0 +1,586 @@ + + + + + + + Beyond the Studio – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Beyond the Studio

+
+
+
+
+ + +
+
+
+
+
+

Studio work is by its very nature private, a personal exploration of ideas and themes. This offers tremendous freedom, but limits exposure to the relatively privileged world of galleries and museums.  This section is about various projects and proposals to address a wider public, to bring the work to the streets. The challenge is to condense and simplify, without losing complexity.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Painted Installations
+
+
+
+
+
+
+
+ + +
+
+
+
+
Early New York
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Dark Installations
+
+
+
+
+
+
+
+ + +
+
+
+
+
Temporary Murals
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Glasgow Projects
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Theatre, Dance & Fashion
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Los Angeles
+
+
+
+
+
+
+
+ + +
+
+
+
+
The Scottish Project​
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/bookshelf-v2.html b/gigs/thomaslawson.com/work-fia-edits/pages/bookshelf-v2.html new file mode 100644 index 0000000000..13409e0d24 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/bookshelf-v2.html @@ -0,0 +1,855 @@ + + + + + + + Bookshelf – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Bookshelf

+
+
+
+
+ + +
+
+
+
+
+

The studio is a filter, a space in which experience is considered, and through a material process, transformed into art. I work in series, and each series is a response to a particular set of circumstances in my life and in the culture around me. I begin by collecting images; sometimes these are photographs I take, but more often they are already circulating in various print and digital media. Once a collection reaches a certain density I start to edit, crop, and combine. I decide on an appropriate scale, and then translate the imagery into paint on canvas, with all the attendant decisions about color and brushmark, representational style and painterly approach.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Publications

+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+

Other writings

+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+

Miscellaneous

+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/bookshelf.html b/gigs/thomaslawson.com/work-fia-edits/pages/bookshelf.html new file mode 100644 index 0000000000..20ef9ae219 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/bookshelf.html @@ -0,0 +1,623 @@ + + + + + + + Bookshelf – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Bookshelf

+
+
+
+
+ + +
+
+
+
+
+

The studio is a filter, a space in which experience is considered, and through a material process, transformed into art. I work in series, and each series is a response to a particular set of circumstances in my life and in the culture around me. I begin by collecting images; sometimes these are photographs I take, but more often they are already circulating in various print and digital media. Once a collection reaches a certain density I start to edit, crop, and combine. I decide on an appropriate scale, and then translate the imagery into paint on canvas, with all the attendant decisions about color and brushmark, representational style and painterly approach.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Publications

+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+

Other writings

+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+

Miscellaneous

+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/home-v2.html b/gigs/thomaslawson.com/work-fia-edits/pages/home-v2.html new file mode 100644 index 0000000000..02ffbd8ff0 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/home-v2.html @@ -0,0 +1,777 @@ + + + + + + + Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

Notes

+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

In the Studio

+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

Beyond the Studio

+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

Art in a Broader Context

+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

Bookshelf

+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/home.html b/gigs/thomaslawson.com/work-fia-edits/pages/home.html new file mode 100644 index 0000000000..e3927188ae --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/home.html @@ -0,0 +1,545 @@ + + + + + + + Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

Notes

+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

In the Studio

+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

Beyond the Studio

+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

Art in a Broader Context

+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+

Bookshelf

+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/in-the-studio-v2.html b/gigs/thomaslawson.com/work-fia-edits/pages/in-the-studio-v2.html new file mode 100644 index 0000000000..bf6e6f85fb --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/in-the-studio-v2.html @@ -0,0 +1,1444 @@ + + + + + + + In The Studio – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

In the Studio

+
+
+
+
+ + +
+
+
+
+
+

The studio is a filter, a space in which experience is considered, and through a material process, transformed into art. I work in series, and each series is a response to a particular set of circumstances in my life and in the culture around me. I begin by collecting images; sometimes these are photographs I take, but more often they are already circulating in various print and digital media. Once a collection reaches a certain density I start to edit, crop, and combine. I decide on an appropriate scale, and then translate the imagery into paint on canvas, with all the attendant decisions about color and brushmark, representational style and painterly approach.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1977

+
+
+
+
+ + +
+
+
+
+
+

1979

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BABY PINK ON BLUE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1980

+
+
+
+
+ + +
+
+
+
+
+

1982

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1983

+
+
+
+
+ + +
+
+
+
+
+

1987

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1987

+
+
+
+
+ + +
+
+
+
+
+

1990

+
+
+
+
+
+
+
+ INSTALLATION KUHLENSCHMIDT 2
+
+
+
+
+
+
+
+
+
+
+
+ Arm Juggling Light
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1991

+
+
+
+
+ + +
+
+
+
+
+

1993

+
+
+
+
+
+
+
+ OUT OF THE DARK NIGHT
+
+
+
+
+
+
+
+
+
+
+
+ FIERY SKY
+
+
+
+
+
+
+
+ SUBURBAN DREAMS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1994

+
+
+
+
+ + +
+
+
+
+
+

1998

+
+
+
+
+
+
+
+ NIGHTMARE INN
+
+
+
+
+
+
+
+
+
+
+
+ VIENNESE PAINTING 7
+
+
+
+
+
+
+
+ DEAD CALM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1999

+
+
+
+
+ + +
+
+
+
+
+

2006

+
+
+
+
+
+
+
+ WEATHER SYSTEM
+
+
+
+
+
+
+
+
+
+
+
+ DOUBLE WORLD
+
+
+
+
+
+
+
+ UNTITLED ORANGE-PINK-GREEN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2006

+
+
+
+
+ + +
+
+
+
+
+

2010

+
+
+
+
+
+
+
+ TREE
+
+
+
+
+
+
+
+
+
+
+
+ HARLEQUIN
+
+
+
+
+
+
+
+ THE NEW WORLD TWINS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2010

+
+
+
+
+ + +
+
+
+
+
+

2015

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2015

+
+
+
+
+ + +
+
+
+
+
+

2016

+
+
+
+
+
+
+
+ DISILLUSIONMENT
+
+
+
+
+
+
+
+
+
+
+
+ DISLOCATION
+
+
+
+
+
+
+
+ DISORDER
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2017

+
+
+
+
+ + +
+
+
+
+
+

2020

+
+
+
+
+
+
+
+ HEAD IN HANDS
+
+
+
+
+
+
+
+
+
+
+
+ WHITE ON WHITE
+
+
+
+
+
+
+
+ STORMY WEATHER
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/in-the-studio.html b/gigs/thomaslawson.com/work-fia-edits/pages/in-the-studio.html new file mode 100644 index 0000000000..a1b87908e9 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/in-the-studio.html @@ -0,0 +1,1212 @@ + + + + + + + In The Studio – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

In the Studio

+
+
+
+
+ + +
+
+
+
+
+

The studio is a filter, a space in which experience is considered, and through a material process, transformed into art. I work in series, and each series is a response to a particular set of circumstances in my life and in the culture around me. I begin by collecting images; sometimes these are photographs I take, but more often they are already circulating in various print and digital media. Once a collection reaches a certain density I start to edit, crop, and combine. I decide on an appropriate scale, and then translate the imagery into paint on canvas, with all the attendant decisions about color and brushmark, representational style and painterly approach.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1977

+
+
+
+
+ + +
+
+
+
+
+

1979

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BABY PINK ON BLUE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1980

+
+
+
+
+ + +
+
+
+
+
+

1982

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1983

+
+
+
+
+ + +
+
+
+
+
+

1987

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1987

+
+
+
+
+ + +
+
+
+
+
+

1990

+
+
+
+
+
+
+
+ INSTALLATION KUHLENSCHMIDT 2
+
+
+
+
+
+
+
+
+
+
+
+ Arm Juggling Light
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1991

+
+
+
+
+ + +
+
+
+
+
+

1993

+
+
+
+
+
+
+
+ OUT OF THE DARK NIGHT
+
+
+
+
+
+
+
+
+
+
+
+ FIERY SKY
+
+
+
+
+
+
+
+ SUBURBAN DREAMS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1994

+
+
+
+
+ + +
+
+
+
+
+

1998

+
+
+
+
+
+
+
+ NIGHTMARE INN
+
+
+
+
+
+
+
+
+
+
+
+ VIENNESE PAINTING 7
+
+
+
+
+
+
+
+ DEAD CALM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

1999

+
+
+
+
+ + +
+
+
+
+
+

2006

+
+
+
+
+
+
+
+ WEATHER SYSTEM
+
+
+
+
+
+
+
+
+
+
+
+ DOUBLE WORLD
+
+
+
+
+
+
+
+ UNTITLED ORANGE-PINK-GREEN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2006

+
+
+
+
+ + +
+
+
+
+
+

2010

+
+
+
+
+
+
+
+ TREE
+
+
+
+
+
+
+
+
+
+
+
+ HARLEQUIN
+
+
+
+
+
+
+
+ THE NEW WORLD TWINS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2010

+
+
+
+
+ + +
+
+
+
+
+

2015

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2015

+
+
+
+
+ + +
+
+
+
+
+

2016

+
+
+
+
+
+
+
+ DISILLUSIONMENT
+
+
+
+
+
+
+
+
+
+
+
+ DISLOCATION
+
+
+
+
+
+
+
+ DISORDER
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

2017

+
+
+
+
+ + +
+
+
+
+
+

2020

+
+
+
+
+
+
+
+ HEAD IN HANDS
+
+
+
+
+
+
+
+
+
+
+
+ WHITE ON WHITE
+
+
+
+
+
+
+
+ STORMY WEATHER
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/inthestudio_1977-1979.html b/gigs/thomaslawson.com/work-fia-edits/pages/inthestudio_1977-1979.html new file mode 100644 index 0000000000..e8b7a8a904 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/inthestudio_1977-1979.html @@ -0,0 +1,730 @@ + + + + + + + InTheStudio_1977-1979 – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

1977

+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+

1979

+
+
+
+
+
+
+
+
+
+
+
+ FORMANESQUE
+
+
+
+
Formanesque, 1977
+oilstick and graphite on paper
+110 X 150 in. (25 sheets, 22 X 30 in. each)
+
+
+
+
+
+
+
+ FIGURES FORMANESQUE
+
+
+
+
Running Figures (detail from Formanesque), 1977
+oilstick and graphite on paper
+22 x 30 in.
+
+
+
+
+
+
+
+
+
+
+
+ AEROPLANES SEVERAL
+
+
+
+
Several Aeroplanes (detail from Formanesque), 1977
+oilstick and graphite on paper
+22 x 30 in. +
+
+
+
+
+
+
+
+ ONE PIPER
+
+
+
+
Single Piper, 1977
+oilstick and graphite on paper
+22 x 30 in.
+
+
+
+
+
+
+
+
+
+
+
+ HIGHLAND GAMES
+
+
+
+
Highland Games, 1977
+oilstick and graphite on paper
+22 x 30 in.
+
+
+
+
+
+
+
+ LONE PIPER
+
+
+
+
Lone Piper, 1977
+oilstick and enamel paint on canvas
+54 x 54 in. +
+
+
+
+
+
+
+
+
+
+
+
+ BABY PINK ON BLUE
+
+
+
+
Pink Baby on Blue, 1978
+oilstick and spray enamel on paper
+44 x 30 in. +
+
+
+
+
+
+
+
+
+
+
+
+
Jumping Baby, 1978
+oilstick and enamel on canvas
+52 x 42 in. +
+
+
+
+
+
+
+
+
+
+
+
+ TINY DOG
+
+
+
+
Tiny Dog, 1978
+oilstick on paper
+diptych, 30 x 45 in.
+
+
+
+
+
+
+
+ GOLD DOG
+
+
+
+
Good Dog, 1978
+oilstick on paper
+diptych, 30 x 45 in.
+
+
+
+
+
+
+
+
+
+
+
+ GOLD DOG
+
+
+
+
Gold Dog, 1978
+oilstick and enamel paint on canvas, with painted wooden frame
+53 x 43 in.
+
+
+
+
+
+
+
+ TWO BOXERS
+
+
+
+
Mirror Boxers, 1978
+oilstick on paper
+diptych, 30 x 45 in.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The Joy of Sex, 1979
+ink on paper
+10 x 8 in.
+
+
+
+
+
+
+
+
Joy of Sex III, 1979
+oil and enamel paint on canvas
+43 x 25 in. +
+
+
+
+
+
+
+
+
+
+
+
+ TWO DOGS BURGUNDY
+
+
+
+
Mirror Bassets, 1978
+oilstick on paper
+diptych, 30 x 45 in.
+
+
+
+
+
+
+
+ RED SHOE
+
+
+
+
Red Shoe, 1979
+oilstick and enamel paint on canvas
+36 x 36 in. +
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/inthestudio_1980-1982.html b/gigs/thomaslawson.com/work-fia-edits/pages/inthestudio_1980-1982.html new file mode 100644 index 0000000000..e785238566 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/inthestudio_1980-1982.html @@ -0,0 +1,800 @@ + + + + + + + InTheStudio_1980-1982 – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

1980

+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+

1982

+
+
+
+
+
+
+
+
+
+
+
+ GREED
+
+
+
+
GREED
+
+
+
+
1980
+
+
+
+
+
+
+
+ HE DIED LIKE INNOCENT VICTIMS
+
+
+
+
HE DIED LIKE INNOCENT VICTIMS
+
+
+
+
1980
+
+
+
+
+
+
+
+
+
+
+
+ WILD IN THE STREETS
+
+
+
+
WILD IN THE STREETS
+
+
+
+
1980
+
+
+
+
+
+
+
+ A NEW BEGGINING
+
+
+
+
A NEW BEGGINING
+
+
+
+
1981
+
+
+
+
+
+
+
+
+
+
+
+ BATTERED BODY FOUND IN FREEZER
+
+
+
+
BATTERED BODY FOUND IN FREEZER
+
+
+
+
1981
+
+
+
+
+
+
+
+ DON'T HIT HER AGAIN
+
+
+
+
DON'T HIT HER AGAIN
+
+
+
+
1981
+
+
+
+
+
+
+
+
+
+
+
+ BEATEN TO DEATH
+
+
+
+
BEATEN TO DEATH
+
+
+
+
1981
+
+
+
+
+
+
+
+ SHOT FOR A BIKE
+
+
+
+
SHOT FOR A BIKE
+
+
+
+
1981
+
+
+
+
+
+
+
+
+
+
+
+ BURN BURN BURN
+
+
+
+
BURN BURN BURN
+
+
+
+
1982
+
+
+
+
+
+
+
+ NEW YORK'S FINEST II
+
+
+
+
NEW YORK'S FINEST II
+
+
+
+
1982
+
+
+
+
+
+
+
+
+
+
+
+ THEIR BROKEN HEARTS
+
+
+
+
THEIR BROKEN HEARTS
+
+
+
+
1982
+
+
+
+
+
+
+
+ SAVED
+
+
+
+
SAVED
+
+
+
+
1982
+
+
+
+
+
+
+
+
+
+
+
+ GUILTY
+
+
+
+
GUILTY
+
+
+
+
1982
+
+
+
+
+
+
+
+ HE SHOT HIS BEST BUDDY
+
+
+
+
HE SHOT HIS BEST BUDDY
+
+
+
+
1982
+
+
+
+
+
+
+
+
+
+
+
+ HE SHOT BEST BUDDY (PENCIL)
+
+
+
+
HE SHOT BEST BUDDY (PENCIL)
+
+
+
+
1982
+
+
+
+
+
+
+
+ HE SHOT HIS BEST BUDDY (SCALED)
+
+
+
+
HE SHOT HIS BEST BUDDY (SCALED)
+
+
+
+
1982
+
+
+
+
+
+
+
+
+
+
+
+ HE SHOT BEST BUDDY (DETAIL)
+
+
+
+
HE SHOT BEST BUDDY (DETAIL)
+
+
+
+
1982
+
+
+
+
+
+
+
+ REPLACEMENT
+
+
+
+
REPLACEMENT
+
+
+
+
1982
+
+
+
+
+
+
+
+
+
+
+
+ SHE SLEW SCHOOLMATE
+
+
+
+
SHE SLEW SCHOOLMATE
+
+
+
+
1982
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/notes-v2.html b/gigs/thomaslawson.com/work-fia-edits/pages/notes-v2.html new file mode 100644 index 0000000000..8dfa1ecba4 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/notes-v2.html @@ -0,0 +1,780 @@ + + + + + + + Notes – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Notes

+
+
+
+
+ + +
+
+
+
+
+

This section is a grab bag of new and old. New work and work in progress, possibly notes for future projects. Recent work that hasn’t been migrated to one of the more permanent sections. Old work recently rediscovered, excavated from storage, found in a drawer, lost on a shelf. Eventually all these items will move elsewhere.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
Rabkin Prize
+
+
+
+
+
+
+
+

Thomas Lawson: Sunny and Warm at Chez Max et Dorothea, Los Angeles

+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
The Studio Reader
+
+
+
+
+
+
+
+ + +
+
+
+
+
Thomas Lawson: Attending to the Bats at AF Projects, Los Angeles
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Anthology for Unseen
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/notes.html b/gigs/thomaslawson.com/work-fia-edits/pages/notes.html new file mode 100644 index 0000000000..66def51d59 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/notes.html @@ -0,0 +1,548 @@ + + + + + + + Notes – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Notes

+
+
+
+
+ + +
+
+
+
+
+

This section is a grab bag of new and old. New work and work in progress, possibly notes for future projects. Recent work that hasn’t been migrated to one of the more permanent sections. Old work recently rediscovered, excavated from storage, found in a drawer, lost on a shelf. Eventually all these items will move elsewhere.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
Rabkin Prize
+
+
+
+
+
+
+
+

Thomas Lawson: Sunny and Warm at Chez Max et Dorothea, Los Angeles

+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
The Studio Reader
+
+
+
+
+
+
+
+ + +
+
+
+
+
Thomas Lawson: Attending to the Bats at AF Projects, Los Angeles
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Anthology for Unseen
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_afterall.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_afterall.html new file mode 100644 index 0000000000..e0b7a8bc39 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_afterall.html @@ -0,0 +1,813 @@ + + + + + + + Bookshelf_Afterall – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Publications

+
+
+
+
Afterall
+
+
+
+
+
+
+
+
+
+
+
+ + AFTERALL 6 +
+
+ +
+
+
AFTERALL 6, 2000
+
+
+
+
+
+
+
+ + AFTERALL 7 +
+
+ +
+
+
AFTERALL 7, 2003
+
+
+
+
+
+
+
+
+
+
+
+ + AFTERALL 9 +
+
+ +
+
+
AFTERALL 9, 2004
+
+
+
+
+
+
+
+ + AFTERALL 11 +
+
+ +
+
+
AFTERALL 11, 2005
+
+
+
+
+
+
+
+
+
+
+
+ + AFTERALL 12 +
+
+ +
+
+
AFTERALL 12, 2005
+
+
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
AFTERALL 13
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_artforum.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_artforum.html new file mode 100644 index 0000000000..6a8164d159 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_artforum.html @@ -0,0 +1,1309 @@ + + + + + + + Bookshelf_Artforum – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Publications

+
+
+
+
Artforum
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum November 1980 +
+
+ +
+
+
Artforum, November 1980
+
+
+
+
+
+
+
+ + Artforum March 1981 +
+
+ +
+
+
Artforum, March 1981
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum April 1981 +
+
+ +
+
+
Artforum, April 1981
+
+
+
+
+
+
+
+ + Artforum, May 1981 +
+
+ +
+
+
Artforum, May 1981
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, September 1981 +
+
+ +
+
+
Artforum, September 1981
+
+
+
+
+
+
+
+ + Artforum, October 1981 +
+
+ +
+
+
Artforum, October 1981
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, December 1981 +
+
+ +
+
+
Artforum, December 1981
+
+
+
+
+
+
+
+ + Artforum, January 1982 +
+
+ +
+
+
Artforum, January 1982
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, May 1982 +
+
+ +
+
+
Artforum, May 1982
+
+
+
+
+
+
+
+ + Artforum, Summer 1982 +
+
+ +
+
+
Artforum, Summer 1982
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, October 1982 +
+
+ +
+
+
Artforum, October 1982
+
+
+
+
+
+
+
+ + Artforum, November 1982 +
+
+ +
+
+
Artforum, November 1982
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, February 1983 +
+
+ +
+
+
Artforum, February 1983
+
+
+
+
+
+
+
+ + Artforum, March 1983 +
+
+
+ +
+
+
+
Artforum, March 1983
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, Summer 1983 +
+
+ +
+
+
Artforum, Summer 1983
+
+
+
+
+
+
+
+ + Artforum, September 1983 +
+
+ +
+
+
Artforum, September 1983
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, January 1984 +
+
+ +
+
+
Artforum, January 1984
+
+
+
+
+
+
+
+ + Artforum, May 1984 +
+
+ +
+
+
Artforum, May 1984
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, Summer 1984 +
+
+ +
+
+
Artforum, Summer 1984
+
+
+
+
+
+
+
+ + Artforum, September 1984 +
+
+ +
+
+
Artforum, September 1984
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, November 1984 +
+
+ +
+
+
Artforum, November 1984
+
+
+
+
+
+
+
+ + Artforum, March 1986 +
+
+
+ +
+
+
+
Artforum, March 1986
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, January 1988 +
+
+
+ +
+
+
+
Artforum, January 1988
+
+
+
+
+
+
+
+ + Artforum, October 2004 +
+
+ +
+
+
Artforum, October 2004
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, April 2005 +
+
+ +
+
+
Artforum, April 2005
+
+
+
+
+
+
+
+ + Arforum, January 2006 +
+
+ +
+
+
Arforum, January 2006
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, September 2006 +
+
+ +
+
+
Artforum, April 2005
+
+
+
+
+
+
+
+ + Artforum, November 2006 +
+
+ +
+
+
Artforum, November 2006
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, December 2006 +
+
+ +
+
+
Artforum, December 2006
+
+
+
+
+
+
+
+ + Arforum, April 2007 +
+
+ +
+
+
Arforum, April 2007
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, December 2012 +
+
+ +
+
+
Artforum, December 2012
+
+
+
+
+
+
+
+ + Artforum, October 2014 +
+
+
+ +
+
+
+
Artforum, October 2014
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf-anthologies.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf-anthologies.html new file mode 100644 index 0000000000..db43e83618 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf-anthologies.html @@ -0,0 +1,971 @@ + + + + + + + Bookshelf – Anthologies – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

OTHER

+
+
+
+
Anthologies
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Art After Modernism: Rethinking Representation
+
+
+
+
Brian Wallis ed., New Museum of Contemporary Art, NY, 1984, reprinted 1992
+
+
+
+
+
+
+
+
+
+
+
+
Theories of Contemporary Art
+
+
+
+
Richard Hertz, ed., Prentice-Hall, NJ, 1985
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Infotainment
+
+
+
+
Peter Nagy and David Robbins, eds., Reichard/Livet and J. Berg Press, NY, 1985
+
+
+
+
+
+
+
+
+
+
+
+
Individuals: A Selected History of Contemporary Art 1945-1986
+
+
+
+
Howard Singerman, ed., MoCA Los Angeles and Abbeville Press, NY, 1986
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Modern Dreams: The Rise and Fall of Pop
+
+
+
+
Ed Leffingwell and Karen Marta, eds., MIT Press, Cambridge, MA 1988
+
+
+
+
+
+
+
+
+
+
+
+
Blasted Allegories: An Anthology of Writing by Contemporary Artists
+
+
+
+
Brian Wallis, ed., New Museum of Contemporary Art, NY and MIT Press, Cambridge M, 1989
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Flash Art: Two Decades of History
+
+
+
+
Giancarlo Politi and Helena Kontova, eds., MIT Press, Cambridge, MA 1990
+
+
+
+
+
+
+
+
+
+
+
+
Postmodern Perspectives: Issues in Contemporary Art
+
+
+
+
Howard Risatti ed., Prentice-Hall., NJ, 1990, 1998
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Between Artists: Twelve Artists Interview Twelve Artists,
+
+
+
+
Barnes, Barossh, Bartman, Sappington, eds., A.R.T. Press, Los Angeles, 1996
+
+
+
+
+
+
+
+
+
+
+
+
Thomas Lawson: Mining for Gold, Selected Writings 1979-1996
+
+
+
+
Lionel Bovier and Fabrice Stroun, eds., JRP/Ringier, Zurich, 2004
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
REALLIFE Magazine; Selected Writings and Projects 1979-1994
+
+
+
+
Miriam Katzeff, Thomas Lawson, Susan Morgan, eds., Primary Information, NY, 2006
+
+
+
+
+
+
+
+
+
+
+
+
12 The Beat and the Buzz: Inside the L.A. Art World
+
+
+
+
Richard Hertz, ed., Mineola Press, CA 2009
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Excursus I-IV
+
+
+
+
Alex Klein, ed., ICA, University of Pennsylvania, 2014
+
+
+
+
+
+
+
+
+
+
+
+
Akademie X Lessons in Art and Life
+
+
+
+
Craig Garret, et al, eds., Phaidon, London and NY, 2015
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
Remote/Control: astral Projection in Higher Ed
+
+
+
+
Emma kemp and Adriana Widdoes, eds., EastofBorneo Books, LA 2020
+
+
+
+
+
+
+
+
+
+
+
+
Artists at Work
+
+
+
+
Adriana Widdoes, ed., EastofBorneo Books, LA, 2023
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf-reallife.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf-reallife.html new file mode 100644 index 0000000000..8a1a6137b0 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf-reallife.html @@ -0,0 +1,1053 @@ + + + + + + + Bookshelf – REALLIFE – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Publications

+
+
+
+
Reallife
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_afterall.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_afterall.html new file mode 100644 index 0000000000..e0b7a8bc39 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_afterall.html @@ -0,0 +1,813 @@ + + + + + + + Bookshelf_Afterall – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Publications

+
+
+
+
Afterall
+
+
+
+
+
+
+
+
+
+
+
+ + AFTERALL 6 +
+
+ +
+
+
AFTERALL 6, 2000
+
+
+
+
+
+
+
+ + AFTERALL 7 +
+
+ +
+
+
AFTERALL 7, 2003
+
+
+
+
+
+
+
+
+
+
+
+ + AFTERALL 9 +
+
+ +
+
+
AFTERALL 9, 2004
+
+
+
+
+
+
+
+ + AFTERALL 11 +
+
+ +
+
+
AFTERALL 11, 2005
+
+
+
+
+
+
+
+
+
+
+
+ + AFTERALL 12 +
+
+ +
+
+
AFTERALL 12, 2005
+
+
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
AFTERALL 13
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_artforum.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_artforum.html new file mode 100644 index 0000000000..6a8164d159 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_artforum.html @@ -0,0 +1,1309 @@ + + + + + + + Bookshelf_Artforum – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Publications

+
+
+
+
Artforum
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum November 1980 +
+
+ +
+
+
Artforum, November 1980
+
+
+
+
+
+
+
+ + Artforum March 1981 +
+
+ +
+
+
Artforum, March 1981
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum April 1981 +
+
+ +
+
+
Artforum, April 1981
+
+
+
+
+
+
+
+ + Artforum, May 1981 +
+
+ +
+
+
Artforum, May 1981
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, September 1981 +
+
+ +
+
+
Artforum, September 1981
+
+
+
+
+
+
+
+ + Artforum, October 1981 +
+
+ +
+
+
Artforum, October 1981
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, December 1981 +
+
+ +
+
+
Artforum, December 1981
+
+
+
+
+
+
+
+ + Artforum, January 1982 +
+
+ +
+
+
Artforum, January 1982
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, May 1982 +
+
+ +
+
+
Artforum, May 1982
+
+
+
+
+
+
+
+ + Artforum, Summer 1982 +
+
+ +
+
+
Artforum, Summer 1982
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, October 1982 +
+
+ +
+
+
Artforum, October 1982
+
+
+
+
+
+
+
+ + Artforum, November 1982 +
+
+ +
+
+
Artforum, November 1982
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, February 1983 +
+
+ +
+
+
Artforum, February 1983
+
+
+
+
+
+
+
+ + Artforum, March 1983 +
+
+
+ +
+
+
+
Artforum, March 1983
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, Summer 1983 +
+
+ +
+
+
Artforum, Summer 1983
+
+
+
+
+
+
+
+ + Artforum, September 1983 +
+
+ +
+
+
Artforum, September 1983
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, January 1984 +
+
+ +
+
+
Artforum, January 1984
+
+
+
+
+
+
+
+ + Artforum, May 1984 +
+
+ +
+
+
Artforum, May 1984
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, Summer 1984 +
+
+ +
+
+
Artforum, Summer 1984
+
+
+
+
+
+
+
+ + Artforum, September 1984 +
+
+ +
+
+
Artforum, September 1984
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, November 1984 +
+
+ +
+
+
Artforum, November 1984
+
+
+
+
+
+
+
+ + Artforum, March 1986 +
+
+
+ +
+
+
+
Artforum, March 1986
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, January 1988 +
+
+
+ +
+
+
+
Artforum, January 1988
+
+
+
+
+
+
+
+ + Artforum, October 2004 +
+
+ +
+
+
Artforum, October 2004
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, April 2005 +
+
+ +
+
+
Artforum, April 2005
+
+
+
+
+
+
+
+ + Arforum, January 2006 +
+
+ +
+
+
Arforum, January 2006
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, September 2006 +
+
+ +
+
+
Artforum, April 2005
+
+
+
+
+
+
+
+ + Artforum, November 2006 +
+
+ +
+
+
Artforum, November 2006
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, December 2006 +
+
+ +
+
+
Artforum, December 2006
+
+
+
+
+
+
+
+ + Arforum, April 2007 +
+
+ +
+
+
Arforum, April 2007
+
+
+
+
+
+
+
+
+
+
+
+ + Artforum, December 2012 +
+
+ +
+
+
Artforum, December 2012
+
+
+
+
+
+
+
+ + Artforum, October 2014 +
+
+
+ +
+
+
+
Artforum, October 2014
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_eastofborneo.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_eastofborneo.html new file mode 100644 index 0000000000..c952c67dea --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_eastofborneo.html @@ -0,0 +1,1069 @@ + + + + + + + Bookshelf_EastofBorneo​ – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

Publications

+
+
+
+
East of Borneo
+
+
+
+
+
+
+
+
+
+
+
+ + The Journey West +
+
+ +
+
+
October 10, 2010
+
+
+
+
+
+
+
+ + Interview with Andrea Bowers +
+
+ +
+
+
January 20, 2011
+
+
+
+
+
+
+
+
+
+
+
+ + Institutional Whitewash +
+
+ +
+
+
February 11, 2011
+
+
+
+
+
+
+
+ + Interview with Liz Glynn +
+
+ +
+
+
September 6, 2012
+
+
+
+
+
+
+
+
+
+
+
+ + Michael Asher, 1943 – 2012 +
+
+ +
+
+
October 15, 2012
+
+
+
+
+
+
+
+ + 4 Taxis +
+
+
+ +
+
+
+
February 5, 2013
+
+
+
+
+
+
+
+
+
+
+
+ + Rhapsody in Pink: Steven Prina Paints +
+
+ +
+
+
April 11, 2013
+
+
+
+
+
+
+
+ + Allan Sekula, 1951-2013 +
+
+ +
+
+
August 11, 2013
+
+
+
+
+
+
+
+
+
+
+
+ + Interview with Fiona Connor +
+
+ +
+
+
July 18, 2017
+
+
+
+
+
+
+
+ + A Schoolboy visits Man Ray +
+
+ +
+
+
August 29, 2017
+
+
+
+
+
+
+
+
+
+
+
+ + Walter Hopps discovers Joseph Cornell +
+
+ +
+
+
August 29, 2017
+
+
+
+
+
+
+
+ + Marcel Duchamp cover for Beatrice Wood catalogue +
+
+ +
+
+
August 29, 2017
+
+
+
+
+
+
+
+
+
+
+
+ + Walter Hopps visits the Arensberg Collection +
+
+ +
+
+
October 1, 2017
+
+
+
+
+
+
+
+ + Cesar Pelli, 1926-2019 +
+
+ +
+
+
July 21, 2019
+
+
+
+
+
+
+
+
+
+
+
+ + Remembering John Baldessari +
+
+ +
+
+
March 2, 2020
+
+
+
+
+
+
+
+ + Betye Saar, The Liberation of Aunt Jemima, 1972 +
+
+ +
+
+
June 19, 2020
+
+
+
+
+
+
+
+ +
+
+
+
+ + A CalArts Story +
+
+ +
+
+
September 7, 2021
+
+
+
+
+
+
+
+
+
+
+
+ + Luciano Perna, 1958-2021 +
+
+ +
+
+
January 21, 2022
+
+
+
+
+ +
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_writingsabouttl.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_writingsabouttl.html new file mode 100644 index 0000000000..423b02ae37 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_bookshelf_writingsabouttl.html @@ -0,0 +1,1029 @@ + + + + + + + Bookshelf_WritingsAboutTL – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

OTHER

+
+
+
+
Writings About Thomas Lawson
+
+
+
+
+
+
+
+
+
+
+
+ + Thomas Lawson at LAXART +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Christopher Miles Review +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Donald Kuspit +
+
+ +
+
+
1982
+
+
+
+
+
+
+
+ + Ingrid Sischy +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Jeanne Silverthorn +
+
+ +
+
+
1990
+
+
+
+
+
+
+
+ + Jeanne Silverthorn +
+
+ +
+
+
1985
+
+
+
+
+
+
+
+
+
+
+
+ + Joan Casademont +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Judith Russi +
+
+
+ +
+
+
+
May 1983
+
+
+
+
+
+
+
+
+
+
+
+ + Kate Linker +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+ + Kuspit on Drawing +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Lisa Liebman +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+ + Lobel Singerman +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Nichtsagend +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+ + Ron Jones - Decay of Living +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Ron Jones - Seeing is Believing +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Schjeldahl +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Sydney Biennale +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Travis Deihl +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_elementor-1796.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_elementor-1796.html new file mode 100644 index 0000000000..632797b2b1 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_elementor-1796.html @@ -0,0 +1,761 @@ + + + + + + + Bookshelf-Interviews – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_elementor-395.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_elementor-395.html new file mode 100644 index 0000000000..e8fd7e97e7 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_elementor-395.html @@ -0,0 +1,1049 @@ + + + + + + + Bookshelf – Considering Other Artists – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

OTHER

+
+
+
+
Considering Other Artists
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + Empire +
+
+
+
+
Empire
+
+
+
+
-
+
+
+
+
+
+
+
+ + Laura Owens +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Richard Wright +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Michael Hurson +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Paint +
+
+
+
+
Paint
+
+
+
+
-
+
+
+
+
+
+
+
+ + Gary Hume +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Some Thoughts on Painting +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Stills +
+
+
+
+
Stills
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Gerard Hemsworth +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Gretchen Bender +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Infotainment Review +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + LA at Pompidou +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Lucy McKenzie +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Mario Merz +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Georgina Starr +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Chema Cobo +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Don Dudley +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/pages/shelf_writingsabouttl.html b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_writingsabouttl.html new file mode 100644 index 0000000000..423b02ae37 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/pages/shelf_writingsabouttl.html @@ -0,0 +1,1029 @@ + + + + + + + Bookshelf_WritingsAboutTL – Thomas Lawson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+
+
+
+

OTHER

+
+
+
+
Writings About Thomas Lawson
+
+
+
+
+
+
+
+
+
+
+
+ + Thomas Lawson at LAXART +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Christopher Miles Review +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Donald Kuspit +
+
+ +
+
+
1982
+
+
+
+
+
+
+
+ + Ingrid Sischy +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Jeanne Silverthorn +
+
+ +
+
+
1990
+
+
+
+
+
+
+
+ + Jeanne Silverthorn +
+
+ +
+
+
1985
+
+
+
+
+
+
+
+
+
+
+
+ + Joan Casademont +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Judith Russi +
+
+
+ +
+
+
+
May 1983
+
+
+
+
+
+
+
+
+
+
+
+ + Kate Linker +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+ + Kuspit on Drawing +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Lisa Liebman +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+ + Lobel Singerman +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Nichtsagend +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+ + Ron Jones - Decay of Living +
+
+ +
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Ron Jones - Seeing is Believing +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Schjeldahl +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ + Sydney Biennale +
+
+ +
+
+
-
+
+
+
+
+
+
+
+ + Travis Deihl +
+
+
+ +
+
+
+
-
+
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + diff --git a/gigs/thomaslawson.com/work-fia-edits/zzz-tl-fia-polish.php b/gigs/thomaslawson.com/work-fia-edits/zzz-tl-fia-polish.php new file mode 100644 index 0000000000..d33c74e117 --- /dev/null +++ b/gigs/thomaslawson.com/work-fia-edits/zzz-tl-fia-polish.php @@ -0,0 +1,385 @@ + + + list of cover image URLs + '7f476f6' => [ + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/1-ARtforum-November-1980-817x1024.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/2-Artforum-Marchh-1981-872x1024.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/3-Artforum-April-1981-886x1024.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/4-Artforum-May-1981-925x1024.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/5-Artforum-September-1981-956x1024.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/6-Artforum-October-1981-960x1024.png', + ], + 'be9922f' => [ + 'https://www.thomaslawson.com/wp-content/uploads/2023/01/Afterall6-cover-654x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/01/Afterall7-642x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/01/Afterall9-cover-644x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/01/Afterall11-cover-647x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/01/Afterall12-cover-642x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2024/01/Afterall13-656x1024.jpg', + ], + '2fe6730' => [ + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/The-Journey-West-1024x716.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/Andrea-Bowers-Interview-1024x592.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/Institutional-Whitewash-1024x712.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/Liz-Glynn-1024x640.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/Michael-Asher-obit.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/02/4-Taxis-1024x712.png', + ], + '624f2b7' => [ + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/REALLIFE-1-cover.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/REALLIFE-2-cover.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/REALLIFE-3-cover.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/654-Real-Life-review-of-anthology_Page_1-scaled.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/683-Real-Life-Show_Page_1-682x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/Pfeil-Magazine-cover-and-contents_Page_1-745x1024.jpg', + ], + '148197c' => [ + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/403-Video-Data-Bank-Profile-1-674x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/497-Robbins-Interview-with-TL-1-scaled.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/620-Robert-Rooney-Pool-Side-1-740x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/793-CAE-Art-Papers-1-scaled.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/Cover-for-Christopher-Howard-interview-1024x589.png', + 'https://www.thomaslawson.com/wp-content/uploads/2023/12/Cover-for-McEvilly-Sischy-interview-990x1024.png', + ], + '91e3759' => [ + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/650-Thomas-Lawson-at-LAXART-1.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/Christopher-Miles.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/Donald-Kuspit-1024x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/Ingrid-Sischy-1014x1024.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/Jeane-Silverthorne-1990.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/Jeanne-Silverthorn-Summer-1985.jpg', + ], + 'f2f992c' => [ + 'https://www.thomaslawson.com/wp-content/uploads/2026/03/spike-71-couples-cover-web_f4b47d70-c6c8-4448-ba6c-5ab99feb149d-794x1024.jpeg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/359-Empire-1.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/439-Laura-Owens-1-scaled.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/643-Richard-Wright-1-scaled.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/648-Michael-Hurson-1-scaled.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2023/04/652-Paint-1-scaled.jpg', + ], + '7c79d3f' => [ + 'https://www.thomaslawson.com/wp-content/uploads/2024/03/1-Art-After-Modernism-Rethinking-Representation-721x1024.png', + 'https://www.thomaslawson.com/wp-content/uploads/2024/03/2-Theories-of-Contemporary-Art-666x1024.jpeg', + 'https://www.thomaslawson.com/wp-content/uploads/2024/03/3-Infotainment.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2024/03/4-Individuals.jpg', + 'https://www.thomaslawson.com/wp-content/uploads/2024/03/5-Modern-Dreams.png', + 'https://www.thomaslawson.com/wp-content/uploads/2024/03/6-Blasted-allegories-731x1024.jpeg', + ], + ]; + $json = wp_json_encode($shelves); + ?> + + DUR - REL) env *= Math.max(0, (DUR - t) / REL); + // Smooth chord boundary so we don't click + const tInChord = t - chordIdx * CHORD_LEN_S - Math.floor(t / (CHORD_LEN_S * PROG.length)) * (CHORD_LEN_S * PROG.length); + const xfadeStart = CHORD_LEN_S - 0.15; // 150 ms boundary smooth + if (tInChord > xfadeStart) { + const k = (tInChord - xfadeStart) / 0.15; + env *= (1 - k * 0.4); // dip a touch, then re-rise next chord + } + const ph1 = 2 * Math.PI * fSub * t; + const ph2 = 2 * Math.PI * fSub2 * t; + const s = (Math.sin(ph1) + Math.sin(ph2)) * 0.5 * env; + L[i] += s * 0.9; + R[i] += s * 0.9; + } +} + +// ── voice: sine PAD (3-osc chord) ─────────────────────────────────── +{ + for (let i = 0; i < N; i++) { + const t = i / SR; + const chordIdx = Math.floor(t / CHORD_LEN_S) % PROG.length; + const c = chordMidis(PROG[chordIdx]); + const fRoot = midiToHz(c.root); + const fFifth = midiToHz(c.fifth); + const fOct = midiToHz(c.oct); + // Slow tremolo via amp LFO @ 0.18 Hz + const lfo = 0.85 + 0.15 * Math.sin(2 * Math.PI * 0.18 * t); + let env = 0.08 * lfo; + const ATT = 10.0; + const REL = 14.0; + if (t < ATT) env *= t / ATT; + if (t > DUR - REL) env *= Math.max(0, (DUR - t) / REL); + const s1 = Math.sin(2 * Math.PI * fRoot * t); + const s2 = Math.sin(2 * Math.PI * fFifth * t); + const s3 = Math.sin(2 * Math.PI * fOct * t); + // Wide stereo: root center, fifth slightly left, octave slightly right. + const sL = s1 * 0.5 + s2 * 0.7 + s3 * 0.35; + const sR = s1 * 0.5 + s2 * 0.35 + s3 * 0.7; + L[i] += sL * env; + R[i] += sR * env; + } +} + +// ── voice: sine SPARKLE — pinged bells on the downbeat of each chord +// Two-octave-up bell, ~4 s exponential decay. +{ + const totalChords = Math.ceil(DUR / CHORD_LEN_S); + for (let k = 0; k < totalChords; k++) { + const tStart = k * CHORD_LEN_S; + const chordIdx = k % PROG.length; + const c = chordMidis(PROG[chordIdx]); + const fSpark = midiToHz(c.root + 24); // two octaves up + const fSpark2 = midiToHz(c.fifth + 24); // fifth two octaves up + const decay = 4.0; + const startSample = Math.floor(tStart * SR); + // Only ring during the "active" portion — skip the silent + // intro lead-in and outro tail so the sparkle doesn't appear too + // early. Keep sparkles between t=14 and t=DUR-16. + if (tStart < 14 || tStart > DUR - 16) continue; + const endSample = Math.min(N, startSample + Math.floor(decay * SR)); + for (let i = startSample; i < endSample; i++) { + const dt = (i - startSample) / SR; + const env = 0.06 * Math.exp(-dt * 1.4); + const s1 = Math.sin(2 * Math.PI * fSpark * dt); + const s2 = Math.sin(2 * Math.PI * fSpark2 * dt); + // Pan: sparkle one left, fifth one right + L[i] += s1 * env * 0.8 + s2 * env * 0.3; + R[i] += s1 * env * 0.3 + s2 * env * 0.8; + } + } +} + +// ── light tape-like saturation (soft-clip via tanh) ────────────────── +let peak = 0; +for (let i = 0; i < N; i++) { + L[i] = Math.tanh(L[i] * 1.1); + R[i] = Math.tanh(R[i] * 1.1); + peak = Math.max(peak, Math.abs(L[i]), Math.abs(R[i])); +} +// Peak-normalize to -3 dB headroom +const target = Math.pow(10, -3 / 20); +const gain = peak > 0 ? target / peak : 1; +for (let i = 0; i < N; i++) { L[i] *= gain; R[i] *= gain; } +console.log(`▸ synth peak ${peak.toFixed(3)} → gain ${gain.toFixed(3)} (target -3 dB)`); + +// ── encode WAV (16-bit PCM stereo, 44.1 kHz) ──────────────────────── +const nFrames = N; +const dataSize = nFrames * 2 * 2; // 2 ch × 2 bytes +const buf = Buffer.alloc(44 + dataSize); +buf.write("RIFF", 0); +buf.writeUInt32LE(36 + dataSize, 4); +buf.write("WAVE", 8); +buf.write("fmt ", 12); +buf.writeUInt32LE(16, 16); // PCM chunk size +buf.writeUInt16LE(1, 20); // PCM format +buf.writeUInt16LE(2, 22); // channels +buf.writeUInt32LE(SR, 24); +buf.writeUInt32LE(SR * 2 * 2, 28); // byte rate +buf.writeUInt16LE(4, 32); // block align +buf.writeUInt16LE(16, 34); // bits/sample +buf.write("data", 36); +buf.writeUInt32LE(dataSize, 40); +let off = 44; +for (let i = 0; i < N; i++) { + buf.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(L[i] * 32767))), off); off += 2; + buf.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(R[i] * 32767))), off); off += 2; +} + +writeFileSync(OUT, buf); +console.log(`✓ wrote ${OUT} (${(dataSize/1024/1024).toFixed(1)} MB · ${DUR}s · ${SR}Hz · stereo)`); diff --git a/pop/big-pictures/bin/finalize-amazing-cover.sh b/pop/big-pictures/bin/finalize-amazing-cover.sh new file mode 100755 index 0000000000..cf3f037fcd --- /dev/null +++ b/pop/big-pictures/bin/finalize-amazing-cover.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# finalize-amazing-cover.sh — upscale the 1024² cover illy to a 3000² +# DistroKid-ready jpg, mirror to ~/Desktop and the durable Working +# Desktop dir, then re-embed it into the brightened release mp3. +# +# Usage: bash finalize-amazing-cover.sh [VARIANT] (default v1) +set -euo pipefail +VAR="${1:-v1}" +SECROOT="$HOME/Documents/Working Desktop/gens/amazing-grace-sections" +DUR_DIR="$HOME/Documents/Working Desktop/amazing-grace" +DESK="$HOME/Desktop" +SRC="$SECROOT/cover/gens/${VAR}.png" +COVER3000="$DUR_DIR/amazing-grace-cover-3000.jpg" +mkdir -p "$DUR_DIR" + +if [[ ! -f "$SRC" ]]; then + echo "✗ missing $SRC — run gen-amazing-illys.sh first" + exit 1 +fi + +echo "▸ upscaling cover ${VAR} → 3000²" +magick "$SRC" -filter Lanczos -resize 3000x3000^ -gravity center -extent 3000x3000 \ + -quality 92 "$COVER3000" +cp "$COVER3000" "$DESK/amazing-grace-cover-3000.jpg" +ls -lh "$COVER3000" "$DESK/amazing-grace-cover-3000.jpg" + +echo "▸ re-embedding 3000² cover into release mp3" +MASTER_MP3="$DUR_DIR/amazing-grace.mp3" +TMP_MP3="$DUR_DIR/amazing-grace.mp3.tmp" +ffmpeg -y -hide_banner -loglevel error \ + -i "$MASTER_MP3" -i "$COVER3000" \ + -map 0:a -map 1 -c copy -id3v2_version 3 \ + -metadata:s:v title="Album cover" -metadata:s:v comment="Cover (front)" \ + -disposition:v attached_pic \ + "$TMP_MP3" +mv "$TMP_MP3" "$MASTER_MP3" +cp "$MASTER_MP3" "$DESK/amazing-grace.mp3" +ls -lh "$MASTER_MP3" "$DESK/amazing-grace.mp3" + +echo "✓ cover staged → $COVER3000" +echo "✓ release mp3 → $MASTER_MP3 (with 3000² cover embedded)" diff --git a/pop/big-pictures/bin/gen-amazing-illys.sh b/pop/big-pictures/bin/gen-amazing-illys.sh new file mode 100755 index 0000000000..e43570f161 --- /dev/null +++ b/pop/big-pictures/bin/gen-amazing-illys.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# gen-amazing-illys.sh — regenerate the amazing-grace rooftop hymn-circle +# set for a given variant tag. 7 portrait IG illys (1024×1536) + 1 +# square COVER illy (1024×1024). Concurrency 3 (8 GB machine cap). +# Mirrors gen-illys-penta.sh. +# usage: bash gen-amazing-illys.sh [VARIANT] (default v1) +set -u +VAR="${1:-v1}" +REPO="/Users/jas/aesthetic-computer" +SECROOT="$HOME/Documents/Working Desktop/gens/amazing-grace-sections" +LOG="$HOME/Documents/Working Desktop/gens/amazing-grace-genillys-${VAR}.log" +: > "$LOG" +GEN="$REPO/marketing/bin/gen-promo.mjs" + +jobs=( + "verse1|${VAR}|1024x1536" + "verse2|${VAR}|1024x1536" + "verse3|${VAR}|1024x1536" + "verse4|${VAR}|1024x1536" + "verse5|${VAR}|1024x1536" + "verse6|${VAR}|1024x1536" + "verse7|${VAR}|1024x1536" + "cover|${VAR}|1024x1024" +) + +run_one() { + local sec="$1" var="$2" size="$3" + echo "▸ START $sec $var $size $(date +%H:%M:%S)" >> "$LOG" + node "$GEN" "$SECROOT/$sec" --variant "$var" --size "$size" --force --no-mirror \ + >> "$LOG" 2>&1 \ + && echo "✓ DONE $sec $var $(date +%H:%M:%S)" >> "$LOG" \ + || echo "✗ FAIL $sec $var (exit $?) $(date +%H:%M:%S)" >> "$LOG" +} + +i=0 +for spec in "${jobs[@]}"; do + IFS='|' read -r sec var size <<< "$spec" + run_one "$sec" "$var" "$size" & + i=$((i+1)) + if (( i % 3 == 0 )); then wait -n 2>/dev/null || wait; fi +done +wait +echo "=== ALL GENS COMPLETE $(date +%H:%M:%S) ===" >> "$LOG" +grep -E '✓ DONE|✗ FAIL' "$LOG" diff --git a/pop/big-pictures/bin/gen-amazing-prompts.mjs b/pop/big-pictures/bin/gen-amazing-prompts.mjs new file mode 100644 index 0000000000..b717ef912f --- /dev/null +++ b/pop/big-pictures/bin/gen-amazing-prompts.mjs @@ -0,0 +1,98 @@ +#!/usr/bin/env node +// gen-amazing-prompts.mjs — writes cover-prompt.txt for the +// amazing-grace **church / notepat / Neo laptop** scene. +// +// Concept (jas, 2026-05-20): +// In a SMALL OLD CHURCH SANCTUARY at golden hour, jeffrey sits +// among the congregation (NOT on a pulpit), CRADLING HIS CHARTREUSE +// MACBOOK NEO in his lap, SINGING amazing grace while PLAYING the +// notepat keyboard interface on the open laptop screen. The +// pixsies + AC freaks sit in the pews around him, listening, +// joining softly. Warm honey window light through the stained +// glass. Peer-horizontal — jeffrey is one of the congregation, +// never centered as preacher. +// +// Photoreal candid phone snapshot; portrait 1024×1536 for verses, +// square 1024×1024 for the cover. +// +// Usage: node pop/big-pictures/bin/gen-amazing-prompts.mjs [--dry] + +import { writeFileSync, existsSync, copyFileSync } from "node:fs"; +import { join } from "node:path"; +import { homedir } from "node:os"; + +const DRY = process.argv.includes("--dry"); +const ROOT = `${homedir()}/Documents/Working Desktop/gens/amazing-grace-sections`; + +const MEDIUM = +`A CANDID PHOTOGRAPH — true-to-life color, real phone/camera snapshot taken inside a small old church sanctuary in late afternoon. Soft natural light from tall stained-glass windows; very fine grain; air a touch dust-mote-hazy. PHOTOREAL, not an illustration, not a painting, not colored-pencil, not gouache, not cinematic-glossy, not neon. Deadpan, tender, peer-horizontal: jeffrey shares the same eye-line as the congregation, nobody centered as a hero. NOBODY acknowledges the camera — all of them are inside the song. Tall vertical portrait orientation, composed so it also crops cleanly to a centered square. ABSOLUTELY NO app/phone/story UI chrome (no bars, avatars, handles, icons, send bars).`; + +const SETTING = +`SETTING — the interior of a small old Protestant-style church sanctuary: warm wood pews in two short rows, faded red velvet seat cushions, a worn dark-wood center aisle, a low modest wooden pulpit at the very back (small, far, not the focus), an upright piano off to the side untouched, a tall arched window of stained glass throwing colored light, a small wooden cross on the back wall (modest, not large). Honey golden light slanting in. Hand-lettered hymn-board on the wall reading nothing readable. Plain wood-and-plaster walls, a couple of cheap metal chairs along the aisle, dust motes in the light beams. NO readable real-world brand wordmarks anywhere; signage generic and illegible. The hush of a Sunday-afternoon practice or a small open-door evening service.`; + +const JEFFREY = +`JEFFREY (recognizable from the jeffrey reference photographs — medium-length brown hair, beard, his real face), REAL person, photoreal skin and hair: he sits in a PEW like everyone else — NOT at the pulpit, NOT centered as a preacher — third pew from the back on the aisle side, his green laptop OPEN ACROSS HIS LAP. He is SINGING — mouth gently open mid-word, head tilted slightly down toward the laptop screen, eyes half-closed in the music, a faint half-smile. His RIGHT HAND rests on the laptop keyboard, fingers gently pressing keys (he is PLAYING the on-screen notepat piano — keystrokes drive the notes, not a separate physical keyboard). His outfit from the references: a soft pale-blue faint-pinstripe long-sleeve button-down with a small embroidered yellow bear emblem low on the chest, a bright YELLOW PEN clipped at the chest pocket, soft RED plastic glasses on a thin cord around his neck, wide-leg medium-cobalt trousers, worn black canvas low-top sneakers. He looks tender, plain, fully inside the song. A peer among the congregation.`; + +const NEO = +`JEFFREY'S LAPTOP — exactly ONE laptop in the frame: jeffrey's CITRUS-GREEN MacBook Neo, open across his lap. The lid faces back so the screen is partly visible to viewers behind him — but the SCREEN CONTENT is the simple notepat piano interface (a clean horizontal row of black + white piano keys, two octaves wide, on a dark slate background, with a single white-glowing key marking the note being played; NO app chrome, no menus, no logos). The closed-side of the lid (behind the hinge, facing the camera) carries a TORN WHITE-PAPER SCRAP taped where the Apple logo would be; on that scrap, hand-penned in thick black ink, is a single whistlegraph BUTTERFLY (use the attached whistlegraph-butterfly.png — exactly that shape, NOT the Apple logo, NOT the PALS logo). No second laptop anywhere; the congregation hold none.`; + +const CONGREGATION = +`THE CONGREGATION — about 8-12 people in the pews around and behind jeffrey, a quiet mixed audience listening: AC FREAKS (the human peers) + PIXSIES (small synthetic-but-passing-as-human folk) of varied ages, ages mixed from kids to elders. Most sit in the pews two or three to a bench, hands folded in laps or holding a small printed paper hymn-sheet. A few of them sing softly along with jeffrey — mouths gently parted, eyes a little closed. An older pixsie at the back has both hands cupped quietly in their lap. One small child-sized pixsie peers over the pew-back, fascinated by jeffrey's laptop. Real outfits — button-downs, hoodies, printed plain tees, cardigans, sweaters, flannels, the occasional light jacket; never tank tops, never costumes. NONE of the congregation are turned toward the camera. NONE hold laptops or phones.`; + +const PIXSIES = +`PIXSIES — the small adorable folk mixed throughout the pews look almost entirely HUMAN, child-sized to adult-sized, an adorable diverse mix (varied faces, skin tones, builds, ages, kids to elders), passable as human with only SUBTLE synthetic tells: a tiny LED faintly glowing under the skin at a temple, behind an ear, in an eye; maybe one hairline seam — easy to miss. NOT clunky robots, NOT mannequins. natural human ears — never pointed/elf/animal.`; + +const PALETTE = +`PALETTE — warm late-afternoon honey light through stained glass: soft amber + ruby + lavender beams across the pews, dust motes catching in the slanting beams, the wood and worn red velvet glowing warm, the back of the sanctuary cool and dim in shadow. jeffrey's pale denim-blues + buttery yellow pen + small red glasses-cord all readable in the warm light. The chartreuse-green Neo lid glows a touch warmer here. Real soft photographic light, low and tender, never neon, never punchy.`; + +const AVOID = +`AVOID — any illustration / drawing / colored-pencil / gouache / painterly look (this is a PHOTOGRAPH); cinematic-glossy or neon glow; lasers; chrome/UI overlays of any kind (no Story bars / send bars / app icons / handles); ANY visible second laptop or phone; large readable brand wordmarks anywhere (hymn-board, screen, hoodies, lid); a hero-pose centering on jeffrey or a preacher-at-pulpit framing; figures looking at or acknowledging the camera; pointed elf ears on pixsies; tank tops, costumes, choir robes, or stage outfits; an apple-logo on the green Neo lid; ornate cathedral grandeur (this is a SMALL plain church, not a basilica); any prosperity-megachurch staging; tangled cornucopias of stuff in the frame.`; + +const NOTEPAT_NOTE = +`THE LAPTOP-SCREEN DETAIL — the visible part of the notepat interface looks like a clean simple row of black-and-white piano keys (a 2-octave horizontal keyboard) on a dark navy / slate background, with ONE key softly highlighted in white where jeffrey is pressing right now. No menus, no logos, no chrome, no second window. NEVER the surrounding scene reflected on the screen (no recursive screens — [[feedback_imagegen_no_recursive_screens]]).`; + +const SECTIONS = { + verse1: +`SECTION — VERSE 1 ("amazing grace, how sweet the sound"): the FIRST and only verse for this single-edit cover. jeffrey just settles in the pew, opens the Neo across his lap; the congregation has turned softly toward him in their seats; he begins to sing the first phrase, his right hand on the keyboard playing the first notepat keys; the warmest honey light is just landing across the pews. Two or three of the singers smile softly toward him. Mood: arrival, breath, the gentle first note of the hymn. Frame is intimate but wide enough to see ~6-8 of the congregation around him in pews.`, + verse2: +`SECTION — VERSE 2 ("twas grace that taught my heart to fear"): the song has settled. The congregation now leans slightly in, heads tilted gently, eyes closing. jeffrey's eyes mostly closed, fully inside the chord. Light a touch warmer.`, + verse3: +`SECTION — VERSE 3 ("through many dangers toils and snares"): a swell — singers join more audibly. An AC freak in the next pew rests a hand on the shoulder of the person beside them; one pixsie wipes the corner of an eye with a knuckle. Light still warm honey.`, + verse4: +`SECTION — VERSE 4 ("the lord has promised good to me"): hand-clasping starts. Two people across the aisle reach across and squeeze hands once; a child-sized pixsie leans softly against an older pew-mate. jeffrey calm and steady on the notepat. Honey light deepening to amber.`, + verse5: +`SECTION — VERSE 5 ("yea when this flesh and heart shall fail"): the most reverent verse. The sanctuary hushed. jeffrey leans a touch into the laptop, the chord held longer. Light raking long shadows across the pews. Mood: held breath; not sad, just deep.`, + verse6: +`SECTION — VERSE 6 ("the earth shall soon dissolve like snow"): the stained-glass light is going cooler — amber turning to dusty rose. A few pew-overhead pendant lamps switching on, warming faces from above. jeffrey's eyes closed, the Neo screen casting a faint glow up on his face from the notepat keys. Mood: vastness, smallness.`, + verse7: +`SECTION — VERSE 7 ("when weve been there ten thousand years"): final verse. Light has dropped — only the warm pendant lamps, a low last-color slip through the stained glass. The congregation faces lit warm from above. A few singers have stood quietly in their pews. jeffrey is still seated with the Neo across his lap, hands resting on the keys for the final hold. Mood: gentle release, the song finishing, no applause yet.`, + cover: +`SECTION — COVER (square 1024×1024 hero crop): center the frame on jeffrey mid-pew with the chartreuse Neo open across his lap, his right hand on the keyboard, mouth gently open singing. Two or three of the congregation visible to his left and right in the pew and behind him, also gently singing or listening. The stained-glass window beam catches him from the side. The Neo's white-paper whistlegraph-butterfly scrap on the closed-side of the lid is clearly visible. The notepat 2-octave keyboard on the screen is partly visible from this angle, one key softly highlighted. Composition allows a small clear band at the top of the frame (church wall + light) suitable for a release wordmark later. Peer-horizontal, no hero-centering.`, +}; + +function build(section) { + return [ + MEDIUM, SETTING, JEFFREY, NEO, NOTEPAT_NOTE, CONGREGATION, PIXSIES, + SECTIONS[section], PALETTE, AVOID, + ].join("\n\n"); +} + +let wrote = 0; +for (const section of Object.keys(SECTIONS)) { + const dir = join(ROOT, section); + const p = join(dir, "cover-prompt.txt"); + const body = build(section); + if (DRY) { + console.log(`── ${section} (${body.length} chars) ──`); + console.log(body.slice(0, 200) + "…\n"); + continue; + } + if (existsSync(p)) { + const stamp = new Date().toISOString().replace(/[:.]/g, "-"); + copyFileSync(p, p.replace(/\.txt$/, `.${stamp}.bak`)); + } + writeFileSync(p, body + "\n"); + wrote++; + console.log(`✓ wrote ${section}/cover-prompt.txt (${body.length} chars)`); +} +if (!DRY) console.log(`\n${wrote} prompt(s) written under\n ${ROOT}`); diff --git a/pop/big-pictures/bin/sacred-drone.mjs b/pop/big-pictures/bin/sacred-drone.mjs new file mode 100644 index 0000000000..177c9e5086 --- /dev/null +++ b/pop/big-pictures/bin/sacred-drone.mjs @@ -0,0 +1,121 @@ +#!/usr/bin/env node +// sacred-drone.mjs — Sacred-Harp-style 3-voice human-hum-like drone: +// • Root G3 (≈196 Hz), 5th D4 (≈293.66 Hz), Octave G4 (≈392 Hz) +// • No chord changes — the drone holds the tonic throughout +// • Each voice = 2 sine oscillators detuned ±4 cents for natural +// "two singers on the same note" chorus thickness +// • Each voice has its OWN slow amp-LFO (≈0.11 / 0.17 / 0.23 Hz) +// so the three never bloom in phase — gives the soft swell-and- +// fade of a hummed congregation breath +// • Soft stereo spread (root center, 5th slightly left, octave +// slightly right) so the chord opens up across the field +// • Long attack (4 s) + long release (15 s); soft tanh saturation; +// peak-normalize to -3 dB headroom +// +// Output: stereo 16-bit WAV at 44.1 kHz. +// +// Usage: node pop/big-pictures/bin/sacred-drone.mjs +// --duration 84 --root-midi 55 +// --out /tmp/amazing-sacred-drone.wav + +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const argv = process.argv.slice(2); +const flags = {}; +for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith("--")) { + const next = argv[i + 1]; + if (next !== undefined && !next.startsWith("--")) { flags[a.slice(2)] = next; i++; } + else flags[a.slice(2)] = true; + } +} + +const DUR = Number(flags.duration ?? 84); +const ROOT = Number(flags["root-midi"] ?? 55); // G3 +const SR = 44100; +const OUT = resolve(String(flags.out || "/tmp/amazing-sacred-drone.wav").replace(/^~/, process.env.HOME)); +const N = Math.floor(DUR * SR); + +const midiToHz = (m) => 440 * Math.pow(2, (m - 69) / 12); +const cents = (c) => Math.pow(2, c / 1200); + +// 3 voices: root, 5th, octave (each with 2 detuned sub-oscs). +// Gains tuned so the drone sits CLEARLY under a sung lead — strong +// enough to read as a continuous "instrument" the whole song, never +// disappearing behind the voice. +const voices = [ + { midi: ROOT, panL: 0.62, panR: 0.62, lfoHz: 0.11, lfoPhase: 0.0, gain: 0.95 }, + { midi: ROOT + 7, panL: 0.78, panR: 0.42, lfoHz: 0.17, lfoPhase: Math.PI * 0.6, gain: 0.75 }, + { midi: ROOT + 12, panL: 0.42, panR: 0.78, lfoHz: 0.23, lfoPhase: Math.PI * 1.3, gain: 0.60 }, +]; + +const L = new Float32Array(N); +const R = new Float32Array(N); + +const ATTACK = 4.0; +const RELEASE = 15.0; + +for (const v of voices) { + const f1 = midiToHz(v.midi) * cents(-4); + const f2 = midiToHz(v.midi) * cents(+4); + // Per-voice phase init = small random offset so the two detuned + // oscs don't start in phase (would emphasize the beat frequency). + // Use a deterministic offset derived from the midi # so the file + // is reproducible. + const ph1Off = (v.midi % 17) / 17 * Math.PI * 2; + const ph2Off = (v.midi % 13) / 13 * Math.PI * 2; + for (let i = 0; i < N; i++) { + const t = i / SR; + // global envelope + let env = v.gain; + if (t < ATTACK) env *= t / ATTACK; + if (t > DUR - RELEASE) env *= Math.max(0, (DUR - t) / RELEASE); + // per-voice slow amp-LFO (gentle breath swell). Range tightened + // to [0.92, 1.0] so the drone never drops out audibly under the + // vocal — it still breathes, but it's always present. + const lfo = 0.92 + 0.08 * Math.sin(2 * Math.PI * v.lfoHz * t + v.lfoPhase); + env *= lfo; + const s = (Math.sin(2 * Math.PI * f1 * t + ph1Off) + + Math.sin(2 * Math.PI * f2 * t + ph2Off)) * 0.5 * env; + L[i] += s * v.panL; + R[i] += s * v.panR; + } +} + +// Soft saturation + peak-norm to -3 dB +let peak = 0; +for (let i = 0; i < N; i++) { + L[i] = Math.tanh(L[i] * 1.05); + R[i] = Math.tanh(R[i] * 1.05); + peak = Math.max(peak, Math.abs(L[i]), Math.abs(R[i])); +} +const target = Math.pow(10, -3 / 20); +const gain = peak > 0 ? target / peak : 1; +for (let i = 0; i < N; i++) { L[i] *= gain; R[i] *= gain; } +console.log(`▸ synth peak ${peak.toFixed(3)} → gain ${gain.toFixed(3)} (target -3 dB)`); + +// 16-bit PCM stereo WAV +const dataSize = N * 2 * 2; +const buf = Buffer.alloc(44 + dataSize); +buf.write("RIFF", 0); +buf.writeUInt32LE(36 + dataSize, 4); +buf.write("WAVE", 8); +buf.write("fmt ", 12); +buf.writeUInt32LE(16, 16); +buf.writeUInt16LE(1, 20); +buf.writeUInt16LE(2, 22); +buf.writeUInt32LE(SR, 24); +buf.writeUInt32LE(SR * 2 * 2, 28); +buf.writeUInt16LE(4, 32); +buf.writeUInt16LE(16, 34); +buf.write("data", 36); +buf.writeUInt32LE(dataSize, 40); +let off = 44; +for (let i = 0; i < N; i++) { + buf.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(L[i] * 32767))), off); off += 2; + buf.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(R[i] * 32767))), off); off += 2; +} +writeFileSync(OUT, buf); +console.log(`✓ wrote ${OUT} (${(dataSize/1024/1024).toFixed(1)} MB · ${DUR}s · ${SR}Hz · stereo)`); diff --git a/pop/big-pictures/bin/vertical-amazing.mjs b/pop/big-pictures/bin/vertical-amazing.mjs new file mode 100644 index 0000000000..8f49ceab70 --- /dev/null +++ b/pop/big-pictures/bin/vertical-amazing.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node +// vertical-amazing.mjs — 1080×1920 vertical IG cut for amazing-grace +// (the 1:24 single edit: verse-1-only with cool sine intro/outro). +// +// Inputs (must already exist): +// ~/Documents/Working Desktop/gens/amazing-grace-sections/verse{1,6}/gens/v1.png +// ~/Desktop/amazing-grace.mp3 (the 1:24 brightened master) +// +// Output: +// ~/Desktop/amazing-grace-vertical.mp4 (1080×1920, H.264, AAC) +// +// Two-illy slow-blend, ken-burns drift across 1:24. verse1 illy holds +// the first ~46 s (the golden-hour arrival + the vocal verse), then a +// 6-second xfade into verse6 (the deeper-dusk pew-light frame) for +// the long sine-only outro. Title PNG (pre-rendered via magick) fades +// in over 0-3 s. +// +// Note: brew ffmpeg 8.1 here ships without libass / drawtext, so we +// pre-render the title text via ImageMagick and composite it via the +// `overlay` filter. No lyric overlay in this cut. + +import { existsSync, mkdirSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const HOME = homedir(); +void HERE; +const SECROOT = `${HOME}/Documents/Working Desktop/gens/amazing-grace-sections`; +const AUDIO = `${HOME}/Desktop/amazing-grace.mp3`; +const OUT = `${HOME}/Desktop/amazing-grace-vertical.mp4`; +const W = 1080; +const H = 1920; +const FPS = 30; + +const TOTAL = 84.0; // single duration +const SEG1 = 50.0; // verse1 illy holds for 50 s +const SEG2 = 40.0; // verse6 illy holds for last 40 s +const XFADE = 6.0; // long slow blend +const SEG1_END = SEG1; // xfade begins at 50.0 → ends 56.0 + +const ILLY1 = `${SECROOT}/verse1/gens/v2.png`; +const ILLY2 = `${SECROOT}/verse6/gens/v2.png`; +for (const p of [ILLY1, ILLY2]) { + if (!existsSync(p)) { console.error(`✗ missing illy ${p}`); process.exit(1); } +} +if (!existsSync(AUDIO)) { console.error(`✗ missing ${AUDIO}`); process.exit(1); } + +const tmp = `/tmp/amazing-vertical-${process.pid}`; +mkdirSync(tmp, { recursive: true }); + +// ── render title PNG via ImageMagick ──────────────────────────────────── +const TITLE_PNG = `${tmp}/title.png`; +{ + const args = [ + "-size", `${W}x600`, "xc:none", + "-gravity", "center", + "-fill", "white", + "-font", "/System/Library/Fonts/Helvetica.ttc", + "-pointsize", "150", + "-stroke", "#101218", "-strokewidth", "8", + "-annotate", "+0-60", "amazing grace", + "-stroke", "none", + "-fill", "#FFF4E0", + "-font", "/System/Library/Fonts/Helvetica.ttc", + "-pointsize", "44", + "-annotate", "+0+90", "aesthetic dot computer · pixsies", + TITLE_PNG, + ]; + const r = spawnSync("magick", args, { stdio: ["ignore", "inherit", "inherit"] }); + if (r.status !== 0) { console.error("✗ magick title PNG failed"); process.exit(1); } +} +console.log(`▸ title PNG → ${TITLE_PNG}`); + +// ── ffmpeg filter graph ──────────────────────────────────────────────── +// Each illy gets a slow ken-burns zoom over its own segment duration. +const seg1Frames = Math.round(SEG1 * FPS) + Math.round(XFADE * FPS); +const seg2Frames = Math.round(SEG2 * FPS) + Math.round(XFADE * FPS); +const seg1Dur = (SEG1 + XFADE).toFixed(3); +const seg2Dur = (SEG2 + XFADE).toFixed(3); + +const illyInputs = [ + "-loop","1","-t", seg1Dur, "-r", String(FPS), "-i", ILLY1, + "-loop","1","-t", seg2Dur, "-r", String(FPS), "-i", ILLY2, +]; +const titleInput = ["-loop","1","-t","3.5","-i", TITLE_PNG]; + +const TITLE_IDX = 2; +const AUDIO_IDX = 3; + +const filters = []; +filters.push( + `[0:v]scale=${Math.floor(W*1.18)}:-2,` + + `zoompan=z='min(zoom+0.00035,1.10)':d=${seg1Frames}:s=${W}x${H}:fps=${FPS},` + + `setsar=1,format=yuv420p[v0]` +); +filters.push( + `[1:v]scale=${Math.floor(W*1.18)}:-2,` + + `zoompan=z='min(zoom+0.00030,1.09)':d=${seg2Frames}:s=${W}x${H}:fps=${FPS},` + + `setsar=1,format=yuv420p[v1]` +); +// xfade at SEG1_END for XFADE seconds +filters.push(`[v0][v1]xfade=transition=fade:duration=${XFADE}:offset=${SEG1_END.toFixed(3)}[vbase]`); +// title overlay (0 → 3 s, fade in 0.4 s, fade out 0.6 s) +filters.push( + `[${TITLE_IDX}:v]format=rgba,fade=t=in:st=0:d=0.4:alpha=1,fade=t=out:st=2.4:d=0.6:alpha=1[title]` +); +filters.push( + `[vbase][title]overlay=x=(W-w)/2:y=820:enable='between(t,0,3)'[vfinal]` +); + +const filterComplex = filters.join(";"); + +const args = [ + "-y", "-hide_banner", "-loglevel", "warning", "-stats", + ...illyInputs, + ...titleInput, + "-i", AUDIO, + "-filter_complex", filterComplex, + "-map", "[vfinal]", + "-map", `${AUDIO_IDX}:a`, + "-c:v", "libx264", "-profile:v", "high", "-level", "4.0", + "-pix_fmt", "yuv420p", "-r", String(FPS), + "-preset", "medium", "-crf", "20", + "-c:a", "aac", "-b:a", "192k", "-ar", "44100", + "-movflags", "+faststart", + "-t", String(TOTAL), + "-shortest", + OUT, +]; + +console.log(`▸ ffmpeg: verse1 illy (0-${SEG1_END.toFixed(1)}s) + ${XFADE}s xfade + verse6 illy → 1:24 IG cut`); +console.log(` → ${OUT}`); +const t0 = Date.now(); +const r = spawnSync("ffmpeg", args, { stdio: "inherit" }); +if (r.status !== 0) { console.error("✗ ffmpeg failed"); process.exit(r.status || 1); } +const dt = ((Date.now() - t0) / 1000).toFixed(1); +console.log(`\n✓ ${OUT} (${dt}s)`); diff --git a/pop/big-pictures/cli.mjs b/pop/big-pictures/cli.mjs index b8068e7299..b62487e4a2 100644 --- a/pop/big-pictures/cli.mjs +++ b/pop/big-pictures/cli.mjs @@ -303,8 +303,10 @@ const VOCAL_ALIGN = `${OUT}/${SLUG}-vocal.mp3.alignment.json`; step("1 · say (jeffrey-pvc TTS)", () => { // Try /with-timestamps first; if the server isn't patched (returns // audio/mpeg instead of JSON), fall back to plain TTS + whisper. + const STABILITY = String(flags.stability ?? "0.6"); + const SIMILARITY = String(flags.similarity ?? "0.9"); const baseArgs = ["bin/say.mjs", `big-pictures/${SLUG}.txt`, - "--stability", "0.6", "--similarity", "0.9", + "--stability", STABILITY, "--similarity", SIMILARITY, "--out", `big-pictures/out/${SLUG}-vocal.mp3`]; const argsWithTs = [...baseArgs, "--timestamps"]; if (FORCE) { baseArgs.push("--force"); argsWithTs.push("--force"); } diff --git a/pop/bin/play.mjs b/pop/bin/play.mjs new file mode 100644 index 0000000000..bdb8f0f8e6 --- /dev/null +++ b/pop/bin/play.mjs @@ -0,0 +1,278 @@ +#!/usr/bin/env node +// play.mjs — pop play. quick "notes → instrument → fx → wav → open" from /pop. +// +// Usage: +// node pop/bin/play.mjs --menu +// node pop/bin/play.mjs --notes "C4*1 E4*1 G4*1 C5*2" --instrument supersaw --preset pad --open +// node pop/bin/play.mjs --notes "A3*1 C4*1 E4*2" --instrument hoover --preset whoop --fx flange --open +// node pop/bin/play.mjs --np pop/dance/trance-hook.np --instrument supersaw --bpm 138 --fx wobble:rate=2,depth=0.7 --fx bitcrush:bits=6 --open +// +// Note format (inline): "*" tokens, space-separated. +// NOTE = C4 / C#4 / Bb3 / etc. (octave 4 = middle). *beats default 1. +// "r*1" = rest of 1 beat. +// +// .np: pop's notepat score format. Multi-syllable cells share their start +// time; this CLI extracts the first NOTE token per cell (one note per +// syllable), which is enough for instrumental playback. +// +// Defaults: 120 BPM, 48 kHz mono, normalized to −3 dB. WAV out to /tmp. +// --open hands the file to QuickTime Player (mp3/audio convention). + +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { spawn } from "node:child_process"; +import { tmpdir } from "node:os"; + +import { MENU, loadInstrument, loadFx } from "../lib/menu.mjs"; +import { resolveLfoHz } from "../dance/synths/skrill.mjs"; + +// ── arg parsing ──────────────────────────────────────────────────────── +function parseArgs(argv) { + const flags = { fx: [] }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--fx") { flags.fx.push(argv[++i]); continue; } + if (a.startsWith("--")) { + const key = a.slice(2); + const next = argv[i + 1]; + if (next !== undefined && !next.startsWith("--")) { flags[key] = next; i++; } + else flags[key] = true; + } + } + return flags; +} + +// Split on `sep` at bracket depth 0 — so breakpoint lists `[t:v,t:v]` +// embedded as a value don't break the param-list comma. +function splitTopLevel(s, sep) { + const out = []; + let depth = 0, cur = ""; + for (const ch of s) { + if (ch === "[") depth++; + else if (ch === "]") depth--; + if (ch === sep && depth === 0) { out.push(cur); cur = ""; } + else cur += ch; + } + if (cur !== "") out.push(cur); + return out; +} + +// fx spec: "wobble" · "wobble:rate=2,depth=0.7" · "wobble:rate=1/8,depth=[0:0,2:0.7,4:0]" +// "1/N", "1/Nt" (triplet), "1/Nd" (dotted) → Hz (needs --bpm) +// "[t0:v0,t1:v1,...]" → breakpoint env: [{time:t0, [k]:v0}, ...] +function parseFxSpec(spec, bpm) { + // Only split on the FIRST colon — breakpoint envs use `:` as time:value sep. + const i = spec.indexOf(":"); + const name = i < 0 ? spec : spec.slice(0, i); + const paramStr = i < 0 ? "" : spec.slice(i + 1); + const opts = {}; + if (!paramStr) return { name, opts }; + for (const kv of splitTopLevel(paramStr, ",")) { + const eq = kv.indexOf("="); + if (eq < 0) continue; + const k = kv.slice(0, eq).trim(); + const v = kv.slice(eq + 1).trim(); + if (v.startsWith("[") && v.endsWith("]")) { + // breakpoint env: [t0:v0,t1:v1,...] + const inner = v.slice(1, -1); + const points = []; + for (const pair of splitTopLevel(inner, ",")) { + const [tStr, vStr] = pair.split(":"); + const time = Number(tStr); + const val = Number(vStr); + if (Number.isFinite(time) && Number.isFinite(val)) { + points.push({ time, [k]: val }); + } + } + opts[k] = points; + } else if (/^1\/\d+(\.\d+)?[td]?$/.test(v)) { + opts[k] = resolveLfoHz(v, bpm); + } else { + const num = Number(v); + opts[k] = Number.isFinite(num) ? num : v; + } + } + return { name, opts }; +} + +// ── notes → events ──────────────────────────────────────────────────── +const STEP = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 }; + +function noteNameToMidi(name) { + if (name === "r" || name === "R") return null; // rest + const m = name.match(/^([A-Ga-g])([#b]?)(-?\d+)$/); + if (!m) return NaN; + let n = STEP[m[1].toUpperCase()]; + if (m[2] === "#") n += 1; + else if (m[2] === "b") n -= 1; + return n + (parseInt(m[3], 10) + 1) * 12; +} + +function parseInlineNotes(s, bpm) { + const beatSec = 60 / bpm; + const events = []; + let t = 0; + for (const tok of s.trim().split(/\s+/)) { + const [n, b] = tok.split("*"); + const beats = b ? parseFloat(b) : 1; + const midi = noteNameToMidi(n); + if (midi === null) { t += beats * beatSec; continue; } + if (!Number.isFinite(midi)) continue; + events.push({ startSec: t, midi, durSec: beats * beatSec * 0.95, gain: 0.9 }); + t += beats * beatSec; + } + return { events, totalSec: t }; +} + +// .np: `NOTE:syllable*beats` cells, space-separated, advancing time. +// Lines starting with `#`, `verse`, or blank are skipped. We take one +// note per cell (the first one) — enough for instrumental preview. +function parseNpFile(path, bpm) { + const beatSec = 60 / bpm; + const raw = readFileSync(path, "utf8"); + const events = []; + let t = 0; + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + if (trimmed.startsWith("#")) continue; + if (/^verse\b/i.test(trimmed)) continue; + for (const tok of trimmed.split(/\s+/)) { + const m = tok.match(/^([A-G][#b]?-?\d+):[^*\s]*\*([\d.]+)$/); + if (!m) continue; + const midi = noteNameToMidi(m[1]); + const beats = parseFloat(m[2]); + if (!Number.isFinite(midi) || !Number.isFinite(beats)) continue; + events.push({ startSec: t, midi, durSec: beats * beatSec * 0.95, gain: 0.9 }); + t += beats * beatSec; + } + } + return { events, totalSec: t }; +} + +// ── menu print ──────────────────────────────────────────────────────── +function printMenu() { + console.log("\npop menu — pickable building blocks across /pop\n"); + for (const [section, items] of Object.entries(MENU)) { + console.log(`${section}:`); + if (Array.isArray(items)) { console.log(` ${items.join(", ")}`); console.log(""); continue; } + if (section === "scales") { + for (const [k, v] of Object.entries(items)) console.log(` ${k.padEnd(14)} [${v.join(", ")}]`); + console.log(""); continue; + } + if (section === "percussion") { + console.log(` voices: ${items.voices.join(", ")}`); + console.log(` ${items.note}`); + console.log(""); continue; + } + for (const [name, def] of Object.entries(items)) { + const presets = def.presets ? ` (${def.presets.join(", ")})` : ""; + const lane = def.lane ? ` · ${def.lane}` : ""; + const blurb = def.blurb || ""; + const inlineMark = def.inline && !def.file ? " [inline]" : ""; + console.log(` ${(name + presets).padEnd(32)}${lane.padEnd(18)} — ${blurb}${inlineMark}`); + } + console.log(""); + } + console.log("usage:"); + console.log(" play.mjs --notes \"C4*1 E4*1 G4*2\" --instrument supersaw --preset pad --fx wobble --open"); + console.log(" play.mjs --np path/to/score.np --instrument hoover --bpm 138 --fx flange --open"); +} + +// ── WAV writer (mono, 16-bit PCM) ───────────────────────────────────── +function writeWavMono16(path, float32, sampleRate) { + const n = float32.length; + const bytes = n * 2; + const header = Buffer.alloc(44); + header.write("RIFF", 0); + header.writeUInt32LE(36 + bytes, 4); + header.write("WAVE", 8); + header.write("fmt ", 12); + header.writeUInt32LE(16, 16); + header.writeUInt16LE(1, 20); // PCM + header.writeUInt16LE(1, 22); // channels + header.writeUInt32LE(sampleRate, 24); + header.writeUInt32LE(sampleRate * 2, 28); + header.writeUInt16LE(2, 32); // block align + header.writeUInt16LE(16, 34); + header.write("data", 36); + header.writeUInt32LE(bytes, 40); + const pcm = Buffer.alloc(bytes); + for (let i = 0; i < n; i++) { + let s = float32[i]; + if (s > 1) s = 1; else if (s < -1) s = -1; + pcm.writeInt16LE((s * 32767) | 0, i * 2); + } + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, Buffer.concat([header, pcm])); +} + +// ── main ────────────────────────────────────────────────────────────── +async function main() { + const flags = parseArgs(process.argv.slice(2)); + + if (flags.menu || flags.help) { printMenu(); return; } + + const SAMPLE_RATE = Number(flags.sr ?? 48000); + const bpm = Number(flags.bpm ?? 120); + + let parsed; + if (flags.np) parsed = parseNpFile(resolve(process.cwd(), flags.np), bpm); + else if (flags.notes) parsed = parseInlineNotes(flags.notes, bpm); + else { + console.error("provide --notes \"C4*1 E4*1 G4*2\" or --np path/to/score.np (or --menu)"); + process.exit(1); + } + + if (parsed.events.length === 0) { console.error("no notes parsed"); process.exit(1); } + + const instName = flags.instrument || flags.inst || "supersaw"; + const inst = MENU.instruments[instName]; + if (!inst) { console.error(`unknown instrument "${instName}". try --menu`); process.exit(1); } + + const mixEvent = await loadInstrument(instName); + + const tailSec = Number(flags.tail ?? 1.8); + const totalSamples = Math.ceil((parsed.totalSec + tailSec) * SAMPLE_RATE); + const out = new Float32Array(totalSamples); + + const instOpts = { sampleRate: SAMPLE_RATE }; + if (flags.preset) instOpts.preset = flags.preset; + if (flags.detune !== undefined) instOpts.detuneCents = Number(flags.detune); + if (flags.voices !== undefined) instOpts.voices = Number(flags.voices); + + const presetTag = flags.preset ? `/${flags.preset}` : ""; + console.log(`→ ${instName}${presetTag} · bpm=${bpm} · ${parsed.events.length} notes · ${(parsed.totalSec + tailSec).toFixed(2)}s`); + for (const ev of parsed.events) mixEvent(ev, out, instOpts); + + for (const spec of flags.fx) { + const { name, opts } = parseFxSpec(spec, bpm); + let apply; + try { apply = await loadFx(name); } catch (e) { console.error(`✗ ${e.message}`); continue; } + const tag = Object.keys(opts).length ? " " + JSON.stringify(opts) : ""; + console.log(` fx · ${name}${tag}`); + apply(out, { sampleRate: SAMPLE_RATE, ...opts }); + } + + // peak-normalize to −3 dB + let peak = 0; + for (let i = 0; i < out.length; i++) { const a = Math.abs(out[i]); if (a > peak) peak = a; } + if (peak > 0) { + const norm = 0.707 / peak; + for (let i = 0; i < out.length; i++) out[i] *= norm; + } + + const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + const outPath = flags.out + ? resolve(process.cwd(), flags.out) + : resolve(tmpdir(), `pop-${instName}${flags.preset ? "-" + flags.preset : ""}-${stamp}.wav`); + + writeWavMono16(outPath, out, SAMPLE_RATE); + console.log(`✓ ${outPath}`); + + if (flags.open) { + spawn("open", ["-a", "QuickTime Player", outPath], { stdio: "ignore", detached: true }).unref(); + } +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/pop/dance/bin/bake-trancepenta.sh b/pop/dance/bin/bake-trancepenta.sh index 546d1a0001..0e34b71ebb 100755 --- a/pop/dance/bin/bake-trancepenta.sh +++ b/pop/dance/bin/bake-trancepenta.sh @@ -67,8 +67,27 @@ run "finalize-penta-vocal (--vox-db -3 · scratch master + vocal bus)" \ --scr "$SCR" --vox "$VOX" --vox-db -3 \ --out "$FINAL" -# mp3 for build.mjs (matches audioSrc dir) -ffmpeg -y -i "$FINAL" -codec:a libmp3lame -b:a 320k "$MP3" 2>/dev/null +# mp3 for build.mjs + DistroKid asset pack — 320 k with the cover art +# embedded as ID3v2 attached_pic + full ID3 tags (title/artist/album/ +# year/genre). Most modern players + Spotify/Apple/DistroKid all read +# ID3v2.3. +COVER="$REPO/system/public/assets/pop/trancepenta.jpg" +if [ -f "$COVER" ]; then + ffmpeg -y -loglevel error -i "$FINAL" -i "$COVER" \ + -map 0:a -map 1:v -c:a libmp3lame -b:a 320k -c:v copy \ + -id3v2_version 3 \ + -metadata title="trancepenta" \ + -metadata artist="Aesthetic Dot Computer" \ + -metadata album="pixsies" \ + -metadata date="2026" \ + -metadata genre="Dance" \ + -metadata:s:v title="Album cover" \ + -metadata:s:v comment="Cover (front)" \ + "$MP3" 2>/dev/null +else + echo "[bake-trancepenta] ⚠ cover missing at $COVER — mp3 will lack embedded art" + ffmpeg -y -i "$FINAL" -codec:a libmp3lame -b:a 320k "$MP3" 2>/dev/null +fi rm -f "$SCR" "$VOX" echo "[bake-trancepenta] done → $FINAL" diff --git a/pop/gecs/bin/render.mjs b/pop/gecs/bin/render.mjs new file mode 100755 index 0000000000..699702ef43 --- /dev/null +++ b/pop/gecs/bin/render.mjs @@ -0,0 +1,959 @@ +#!/usr/bin/env node +// gecs/bin/render.mjs — the gecs bed builder, v2. Same node-only / no- +// Web-Audio posture as pop/hippyhayzard/bin/render.mjs: every sample +// synthesized here from AC instruments. No Suno. Bottom-up (pop/SCORE.md). +// +// Lane: 100-gecs / hyperpop crossed with TRAP swing. The arrangement +// mirrors pop/gecs/gecs.np and clocks ~84s at 165 BPM: +// +// intro 4 · v1 12 · ch1 8 · v2 8 · ch2 8 · break 6 · ch3 8 · outro 4 +// = 58 bars × 1.454s ≈ 84.4s +// +// Design notes vs v1 (the "general MIDI" critique): +// • STEREO bus + a real algorithmic plate reverb send. Voices live in +// space, not stacked dry on the same point. +// • Trap drum patterns: kick on 1 + the 'and of 3', snare on 3, hi- +// hats 1/16 with strong (~26%) swing + occasional 1/32 rolls. No +// more 4-on-the-floor in the verses. +// • SPARSER LAYERING: each section drops one or two voices so the +// space breathes. Verse = just trap kit + screech-skrill + a single +// stab. Chorus = supersaw chord stack + hoover honk + reverb tail. +// Break = ska upstrokes alone over half-time trap kit. +// • Slight timing humanization (±6ms jitter) so it's not gridlocked. +// • Filter sweeps on chorus supersaw (time-varying lowpass) instead +// of static-bright stacks. +// • Bit-crush stays on the master + kick, but lighter than v1 so the +// reverb tails actually read. +// +// Usage: +// node pop/gecs/bin/render.mjs # → out/gecs.mp3 +// node pop/gecs/bin/render.mjs --bpm 168 --out ~/g.mp3 + +import { writeFileSync, mkdirSync, unlinkSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; +import { homedir } from "node:os"; + +import { mixEventSupersaw } from "../../dance/synths/supersaw.mjs"; +import { mixEventSkrill } from "../../dance/synths/skrill.mjs"; +import { mixEventHoover } from "../../hippyhayzard/synths/hoover.mjs"; + +const SR = 48_000; +const HERE = dirname(fileURLToPath(import.meta.url)); + +const argv = process.argv.slice(2); +const flags = {}; +for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith("--")) { + const k = a.slice(2), n = argv[i + 1]; + if (n !== undefined && !n.startsWith("--")) { flags[k] = n; i++; } else flags[k] = true; + } +} +function expandHome(p) { + if (!p) return p; + if (p === "~") return homedir(); + if (p.startsWith("~/")) return resolve(homedir(), p.slice(2)); + return p; +} + +const BPM = Number(flags.bpm ?? 165); +const beat = 60 / BPM; +const bar = beat * 4; +const sx = beat / 4; + +// Deterministic per-event jitter (humanization, ±jitMs around the grid). +function jit(seedStr, jitMs = 6) { + let s = 2166136261 >>> 0; + for (let i = 0; i < seedStr.length; i++) { s ^= seedStr.charCodeAt(i); s = Math.imul(s, 16777619); } + s = (s >>> 0) || 1; + s ^= s << 13; s >>>= 0; s ^= s >>> 17; s >>>= 0; s ^= s << 5; s >>>= 0; + return ((s / 0xffffffff) * 2 - 1) * (jitMs / 1000); +} + +// ── inline drums ───────────────────────────────────────────────────── +function addStereo(L, R, idx, vL, vR) { + if (idx >= 0 && idx < L.length) { L[idx] += vL; R[idx] += vR; } +} +function crush(x, levels = 12) { + const s = x >= 0 ? 1 : -1; + const q = Math.round(Math.abs(x) * levels) / levels; + return s * Math.min(1, q); +} + +// 808-style trap kick — long sub tail (~0.35s) + crushed click on top. +function trapKick(L, R, startSec, g = 1.0, crushLvls = 14) { + const dur = 0.36, n = Math.floor(dur * SR), s0 = Math.floor(startSec * SR); + let ph = 0; + for (let i = 0; i < n; i++) { + const t = i / SR; + const f = 42 + (140 - 42) * Math.exp(-t * 22); // smoother slide + ph += (2 * Math.PI * f) / SR; + const body = Math.sin(ph) * Math.exp(-t * 6); // long sub tail + const click = i < SR * 0.003 ? (Math.random() * 2 - 1) * 0.8 * (1 - i / (SR * 0.003)) : 0; + const v = crush((body + click * 0.6) * g, crushLvls); + addStereo(L, R, s0 + i, v, v); // mono center + } +} + +// Sharp trap snare — short, snappy, pan slightly right +function trapSnare(L, R, startSec, g = 0.85) { + const dur = 0.11, n = Math.floor(dur * SR), s0 = Math.floor(startSec * SR); + let prev = 0, ph = 0; + for (let i = 0; i < n; i++) { + const t = i / SR; + const nz = Math.random() * 2 - 1; + const hp = nz - prev; prev = nz; + ph += (2 * Math.PI * 195) / SR; + const tone = Math.sin(ph) * Math.exp(-t * 38) * 0.5; + const v = crush((hp * Math.exp(-t * 30) + tone) * g, 10); + addStereo(L, R, s0 + i, v * 0.92, v * 1.08); // hint right + } +} + +// Stacked clap layered with snare (gecs hit) +function trapClap(L, R, startSec, g = 0.7) { + for (let k = 0; k < 4; k++) { + const t0 = startSec + k * 0.009; + const dur = 0.06, n = Math.floor(dur * SR), s0 = Math.floor(t0 * SR); + let prev = 0; + for (let i = 0; i < n; i++) { + const nz = Math.random() * 2 - 1; + const hp = nz - prev; prev = nz; + const v = crush(hp * Math.exp(-(i / SR) * 50) * g * (k === 3 ? 1.3 : 0.55), 10); + addStereo(L, R, s0 + i, v * 1.05, v * 0.95); // hint left + } + } +} + +// Hi-hat: closed, short, can be panned across the stereo field +function hat(L, R, startSec, g = 0.32, len = 0.022, pan = 0) { + const n = Math.floor(len * SR), s0 = Math.floor(startSec * SR); + let prev = 0; + const pL = Math.cos((pan + 1) * Math.PI / 4); // equal-power pan + const pR = Math.sin((pan + 1) * Math.PI / 4); + for (let i = 0; i < n; i++) { + const nz = Math.random() * 2 - 1; + const hp = nz - prev; prev = nz; + const v = hp * Math.exp(-(i / SR) * 260) * g; + addStereo(L, R, s0 + i, v * pL * 1.414, v * pR * 1.414); + } +} + +// Open hat — same color, longer tail +function openHat(L, R, startSec, g = 0.22, pan = 0) { + hat(L, R, startSec, g, 0.18, pan); +} + +function riser(L, R, startSec, durSec, g = 0.55) { + const n = Math.floor(durSec * SR), s0 = Math.floor(startSec * SR); + let lp = 0; + for (let i = 0; i < n; i++) { + const w = i / n; + const nz = Math.random() * 2 - 1; + const a = 0.02 + 0.5 * w; + lp += a * (nz - lp); + const v = (nz - lp) * Math.pow(w, 1.4) * g; + // riser sweeps L→R as it climbs + const pan = w * 2 - 1; + const pL = Math.cos((pan + 1) * Math.PI / 4); + const pR = Math.sin((pan + 1) * Math.PI / 4); + addStereo(L, R, s0 + i, v * pL * 1.414, v * pR * 1.414); + } +} + +// ── algorithmic plate reverb (Schroeder-style: 4 comb + 2 allpass) ─── +// Process a mono send bus into a stereo wet bus. Cheap but musical — +// gives us the "deep space" the v1 bed was missing. +function reverbToStereo(send, wetL, wetR, opts = {}) { + const room = opts.room ?? 0.78; // 0..1 — feedback amount + const damp = opts.damp ?? 0.35; // 0..1 — HF damping per pass + const wet = opts.wet ?? 0.42; + // four parallel comb filters w/ prime-ish delay (in samples @ 48k) + const combDelL = [1557, 1617, 1491, 1422]; + const combDelR = [1580, 1640, 1514, 1445]; + const combL = combDelL.map((d) => ({ buf: new Float32Array(d), idx: 0, lp: 0 })); + const combR = combDelR.map((d) => ({ buf: new Float32Array(d), idx: 0, lp: 0 })); + // two series allpass per channel + const apDel = [225, 556]; + const apL = apDel.map((d) => ({ buf: new Float32Array(d), idx: 0 })); + const apR = apDel.map((d) => ({ buf: new Float32Array(d), idx: 0 })); + const apGain = 0.5; + + for (let i = 0; i < send.length; i++) { + const x = send[i]; + let sumL = 0, sumR = 0; + for (let c = 0; c < combL.length; c++) { + const cL = combL[c], cR = combR[c]; + // L + const outL = cL.buf[cL.idx]; + cL.lp = outL * (1 - damp) + cL.lp * damp; + cL.buf[cL.idx] = x + cL.lp * room; + cL.idx = (cL.idx + 1) % cL.buf.length; + sumL += outL; + // R + const outR = cR.buf[cR.idx]; + cR.lp = outR * (1 - damp) + cR.lp * damp; + cR.buf[cR.idx] = x + cR.lp * room; + cR.idx = (cR.idx + 1) % cR.buf.length; + sumR += outR; + } + let yL = sumL * 0.25, yR = sumR * 0.25; + for (const ap of apL) { + const d = ap.buf[ap.idx]; + const in_ = yL + d * apGain; + ap.buf[ap.idx] = in_; + yL = d - in_ * apGain; + ap.idx = (ap.idx + 1) % ap.buf.length; + } + for (const ap of apR) { + const d = ap.buf[ap.idx]; + const in_ = yR + d * apGain; + ap.buf[ap.idx] = in_; + yR = d - in_ * apGain; + ap.idx = (ap.idx + 1) % ap.buf.length; + } + wetL[i] += yL * wet; + wetR[i] += yR * wet; + } +} + +// Mix a mono synth buffer into the stereo bus with a pan + send level. +// pan in [-1, 1]. sendLevel goes to the reverb input bus. +function placeStereo(mono, busL, busR, send, pan = 0, sendLevel = 0.3) { + const pL = Math.cos((pan + 1) * Math.PI / 4); + const pR = Math.sin((pan + 1) * Math.PI / 4); + for (let i = 0; i < mono.length; i++) { + const v = mono[i]; + busL[i] += v * pL * 1.414; + busR[i] += v * pR * 1.414; + send[i] += v * sendLevel; + } +} + +// ── chord progressions (F#m verse · A major chorus) ────────────────── +const VERSE_PROG = [ + { bass: 42, voicing: [54, 57, 61] }, // F#m + { bass: 38, voicing: [54, 57, 62] }, // D + { bass: 40, voicing: [52, 56, 59] }, // E + { bass: 35, voicing: [54, 57, 59] }, // Bm +]; +const CHORUS_PROG = [ + { bass: 45, voicing: [57, 61, 64] }, // A + { bass: 38, voicing: [57, 62, 66] }, // D + { bass: 42, voicing: [57, 61, 66] }, // F#m + { bass: 40, voicing: [56, 59, 64] }, // E +]; + +// CHORUS MELODY — hand-transcribed from gecs.np (the chorus block). The +// instruments DOUBLE this line (the user feedback: "harmonizes on the +// instruments"). Format: [startBeat, midi, durBeats]. Spans 8 bars +// (32 beats) — same shape as the sung chorus. +// +// "ker-nel pa-nic ker-nel pa-nic blue light scream" +// "ker-nel pa-nic ker-nel pa-nic wake up from the dream" +// "ev-ry-thing is o-kay ev-ry-thing is fine" +// "ker-nel pa-nic on the nine-ty ninth time" +const CHORUS_MELODY = [ + // bars 1-2 — blue light scream + [0, 69, 0.5], [0.5, 69, 0.5], [1, 73, 0.5], [1.5, 73, 0.5], + [2, 69, 0.5], [2.5, 69, 0.5], [3, 73, 0.5], [3.5, 73, 0.5], + [4, 76, 1.0], [5, 73, 1.0], [6, 69, 2.0], + // bars 3-4 — wake up from the dream + [8, 69, 0.5], [8.5, 69, 0.5], [9, 73, 0.5], [9.5, 73, 0.5], + [10, 69, 0.5], [10.5, 69, 0.5], [11, 73, 0.5], [11.5, 73, 0.5], + [12, 76, 0.5], [12.5, 76, 0.5], [13, 73, 0.5], [13.5, 73, 0.5], + [14, 69, 2.0], + // bars 5-6 — everything is okay / everything is fine + [16, 73, 0.5], [16.5, 73, 0.5], [17, 69, 0.5], [17.5, 69, 0.5], + [18, 76, 0.5], [18.5, 76, 1.5], + [20, 73, 0.5], [20.5, 73, 0.5], [21, 69, 0.5], [21.5, 69, 0.5], + [22, 78, 2.0], + // bars 7-8 — kernel panic on the ninety ninth time + [24, 69, 0.5], [24.5, 69, 0.5], [25, 73, 0.5], [25.5, 73, 0.5], + [26, 69, 0.5], [26.5, 69, 0.5], [27, 76, 0.5], [27.5, 76, 0.5], + [28, 73, 1.0], [29, 69, 3.0], +]; + +// Diatonic 3rd-above (A major scale) — counter-line for the harmony layer. +const AMAJ = [0, 2, 4, 5, 7, 9, 11]; // semitone offsets from A +function third_above(midi) { + const semi = midi - 69; + const octs = Math.floor(semi / 12); + const pc = ((semi % 12) + 12) % 12; + let idx = AMAJ.indexOf(pc); + if (idx < 0) { idx = 0; for (let k = 0; k < AMAJ.length; k++) if (AMAJ[k] <= pc) idx = k; } + const pos = octs * 7 + idx + 2; // up a diatonic third + const nOct = Math.floor(pos / 7); + const nIdx = ((pos % 7) + 7) % 7; + return 69 + nOct * 12 + AMAJ[nIdx]; +} + +// ── section renderers ──────────────────────────────────────────────── +// Each section gets its own mono synth scratch buffer that we pan + +// send into reverb. Drums are rendered straight into the stereo bus. + +function renderIntro(busL, busR, send, drmL, drmR, t0, bars, N) { + const scratch = new Float32Array(N); + for (let b = 0; b < bars; b++) { + const bt = t0 + b * bar; + // sub drone holds the key — pan slight L, wet on send + mixEventSkrill( + { startSec: bt, midi: 30, durSec: bar * 0.95, gain: 0.30, preset: "sub", lfo: "1/2" }, + scratch, { sampleRate: SR, bpm: BPM }, + ); + // single hoover whoop on beat 1 of bar 2 + bar 4 — ambient, distant + if (b === 1 || b === 3) { + const tmp = new Float32Array(N); + mixEventHoover( + { startSec: bt, midi: 57, durSec: bar * 0.8, gain: 0.24 }, + tmp, { sampleRate: SR, preset: "whoop" }, + ); + placeStereo(tmp, busL, busR, send, b === 1 ? -0.6 : 0.6, 0.7); + } + // sparse trap hats — only the 'and' of 2 and 4 + hat(drmL, drmR, bt + beat * 1.5, 0.15, 0.025, -0.4); + hat(drmL, drmR, bt + beat * 3.5, 0.15, 0.025, 0.4); + } + placeStereo(scratch, busL, busR, send, 0, 0.5); + // big riser across the last 2 bars (sweeps L→R) + pickup kick + riser(drmL, drmR, t0 + (bars - 2) * bar, 2 * bar, 0.55); + trapKick(drmL, drmR, t0 + bars * bar - sx, 0.9); +} + +function renderVerse(busL, busR, send, drmL, drmR, kt, t0, bars, prog, sectionName) { + const screech = new Float32Array(N); + const subB = new Float32Array(N); + const stab = new Float32Array(N); + for (let b = 0; b < bars; b++) { + const bt = t0 + b * bar; + const ch = prog[b % prog.length]; + // screech-skrill on 1/16 (the gecs wobble) — narrower than chorus + mixEventSkrill( + { startSec: bt, midi: ch.bass + 12, durSec: bar * 0.95, gain: 0.34, + preset: "screech", lfo: "1/16" }, + screech, { sampleRate: SR, bpm: BPM }, + ); + // 808 sub track — only on beat 1 and 3.5 (the trap kick spots) + mixEventSkrill( + { startSec: bt, midi: ch.bass - 12, durSec: beat * 1.4, gain: 0.36, preset: "sub" }, + subB, { sampleRate: SR, bpm: BPM }, + ); + mixEventSkrill( + { startSec: bt + beat * 2.5, midi: ch.bass - 12, durSec: beat * 1.2, gain: 0.32, preset: "sub" }, + subB, { sampleRate: SR, bpm: BPM }, + ); + // a single supersaw stab on the 'and' of 4 (lead-in to next bar) + for (const m of ch.voicing) { + mixEventSupersaw( + { startSec: bt + beat * 3.5, midi: m, durSec: beat * 0.4, gain: 0.18 }, + stab, { sampleRate: SR, preset: "stab" }, + ); + } + // TRAP drum pattern, humanized: + // beat 1 — kick (the big 808 hit) + // beat 3 — snare + clap + // beat 3.5 — kick (the trap stutter) + // 16th hats with strong swing + 1/32 roll on the last 8th of bar 4 of every 4-bar phrase + const seedB = `${sectionName}:${b}:`; + trapKick(drmL, drmR, bt + jit(seedB + "k1"), 0.95); + kt.push(bt); + trapSnare(drmL, drmR, bt + beat * 2 + jit(seedB + "s3"), 0.72); + trapClap(drmL, drmR, bt + beat * 2 + jit(seedB + "c3"), 0.40); + trapKick(drmL, drmR, bt + beat * 2.5 + jit(seedB + "k35"), 0.70); + kt.push(bt + beat * 2.5); + // 16th hats (strong swing — off-16ths delayed 26% of sx) + for (let e = 0; e < 16; e++) { + const sw = e % 2 === 1 ? sx * 0.26 : 0; + const ht = bt + e * sx + sw + jit(seedB + "h" + e, 3); + const pan = (e % 4 === 0) ? 0 : ((e % 4 === 2) ? 0.3 : (e % 2 ? -0.3 : 0.15)); + hat(drmL, drmR, ht, e % 4 === 0 ? 0.24 : e % 2 ? 0.14 : 0.18, 0.022, pan); + } + // 1/32 roll on the last 8th of bar 4 in each 4-bar phrase (trap tic) + if (b % 4 === 3) { + for (let r = 0; r < 4; r++) { + hat(drmL, drmR, bt + beat * 3.5 + r * (sx / 2), 0.18 - r * 0.03, 0.018, r * 0.4 - 0.6); + } + } + // open hat ghost on the 'and of 4' for breath + if (b % 2 === 1) openHat(drmL, drmR, bt + beat * 3.5, 0.14, 0.4); + } + // Place voices in space with reverb sends + placeStereo(screech, busL, busR, send, -0.45, 0.32); // screech sits left + placeStereo(subB, busL, busR, send, 0.0, 0.10); // sub centered, dry + placeStereo(stab, busL, busR, send, 0.55, 0.45); // stab right, wet +} + +function renderChorus(busL, busR, send, drmL, drmR, kt, t0, bars, prog, isFinal, sectionName) { + const supL = new Float32Array(N); // supersaw chord pad (panned wide) + const supR = new Float32Array(N); + const lead = new Float32Array(N); // SUPERSAW LEAD doubling the vocal melody + const leadOct = new Float32Array(N); // octave-up doubling of the lead + const harm = new Float32Array(N); // diatonic-3rd-above harmony counter-line + const honkMel = new Float32Array(N); // hoover playing the melody too (the gecs honk wall) + const subB = new Float32Array(N); // 808 sub + + for (let b = 0; b < bars; b++) { + const bt = t0 + b * bar; + const ch = prog[b % prog.length]; + // Lighter chord pad now — the lead carries melody, pad is bedding + ch.voicing.forEach((m, vi) => { + const target = vi === 0 ? supL : vi === 2 ? supR : (vi % 2 ? supR : supL); + mixEventSupersaw( + { startSec: bt, midi: m, durSec: bar * 0.96, gain: isFinal ? 0.14 : 0.12 }, + target, { sampleRate: SR, preset: "lead" }, + ); + }); + // 808 sub on beat 1 and beat 3.5 (trap kick spots) + mixEventSkrill( + { startSec: bt, midi: ch.bass - 12, durSec: beat * 2.2, gain: 0.40, preset: "sub" }, + subB, { sampleRate: SR, bpm: BPM }, + ); + mixEventSkrill( + { startSec: bt + beat * 2.5, midi: ch.bass - 12, durSec: beat * 1.5, gain: 0.36, preset: "sub" }, + subB, { sampleRate: SR, bpm: BPM }, + ); + // CHORUS LEAD — supersaw + hoover + harmonized 3rd play the same + // melody the vocal sings. Notes for this bar come from CHORUS_MELODY + // (filter to events that start within this bar's [b*4, (b+1)*4) beats). + const barStartBeats = b * 4; + const barEndBeats = (b + 1) * 4; + for (const [sb, m, db] of CHORUS_MELODY) { + if (sb < barStartBeats || sb >= barEndBeats) continue; + const tNote = bt + (sb - barStartBeats) * beat; + const dNote = db * beat; + // primary lead (supersaw, the vocal-doubled hyperpop wall) + mixEventSupersaw( + { startSec: tNote, midi: m, durSec: dNote * 0.95, gain: isFinal ? 0.46 : 0.40 }, + lead, { sampleRate: SR, preset: "lead" }, + ); + // octave-up doubling (the chipmunk shimmer that locks with vocal) + mixEventSupersaw( + { startSec: tNote, midi: m + 12, durSec: dNote * 0.6, gain: 0.18 }, + leadOct, { sampleRate: SR, preset: "stab" }, + ); + // diatonic 3rd above — gecs-style harmonized counter-line + mixEventSupersaw( + { startSec: tNote, midi: third_above(m), durSec: dNote * 0.95, gain: isFinal ? 0.28 : 0.22 }, + harm, { sampleRate: SR, preset: "lead" }, + ); + // hoover honk doubling the melody an octave down (only on + // sustained notes ≥ 1 beat — keeps the wall feel without mud) + if (db >= 1) { + mixEventHoover( + { startSec: tNote, midi: m - 12, durSec: dNote * 0.85, gain: isFinal ? 0.40 : 0.34 }, + honkMel, { sampleRate: SR, preset: "stab" }, + ); + } + } + // TRAP chorus kit — same skeleton as verse but harder, with double-time hat fills on bar 4 + const seedB = `${sectionName}:${b}:`; + trapKick(drmL, drmR, bt + jit(seedB + "k1"), isFinal ? 1.05 : 0.98); + kt.push(bt); + trapSnare(drmL, drmR, bt + beat * 2 + jit(seedB + "s3"), 0.78); + trapClap(drmL, drmR, bt + beat * 2 + jit(seedB + "c3"), 0.52); + trapKick(drmL, drmR, bt + beat * 2.5 + jit(seedB + "k35"), 0.75); + kt.push(bt + beat * 2.5); + for (let e = 0; e < 16; e++) { + const sw = e % 2 === 1 ? sx * 0.26 : 0; + const ht = bt + e * sx + sw + jit(seedB + "h" + e, 3); + const pan = (e % 4 === 0) ? 0 : ((e % 4 === 2) ? 0.35 : (e % 2 ? -0.35 : 0.15)); + hat(drmL, drmR, ht, e % 4 === 0 ? 0.26 : e % 2 ? 0.15 : 0.20, 0.022, pan); + } + // 1/32 hat roll on the last beat of bar 4 in every 4-bar phrase + if (b % 4 === 3) { + for (let r = 0; r < 8; r++) { + hat(drmL, drmR, bt + beat * 3 + r * (sx / 2), 0.22 - r * 0.020, 0.018, r * 0.25 - 0.85); + } + } + } + + // Filter sweep on the chord pad (time-varying lowpass via 1-pole) — + // opens across each 4-bar phrase, snaps back. Kills the static-GM feel. + function sweepLP(buf, baseAlpha = 0.06, peakAlpha = 0.45, phraseBars = 4) { + const phraseSamp = Math.floor(phraseBars * bar * SR); + let lp = 0; + for (let i = 0; i < buf.length; i++) { + const w = (i % phraseSamp) / phraseSamp; + const a = baseAlpha + (peakAlpha - baseAlpha) * w; + lp += a * (buf[i] - lp); + buf[i] = lp; + } + } + sweepLP(supL); sweepLP(supR); + sweepLP(leadOct, 0.12, 0.60); + + // Place voices in stereo space. + // Chord pad — kept wide L/R as background bedding. + for (let i = 0; i < N; i++) { + busL[i] += supL[i] * 1.0; busR[i] += supR[i] * 1.0; + send[i] += (supL[i] + supR[i]) * 0.20; + } + // Lead doubles vocal — center (where the vocal sits), modest reverb + placeStereo(lead, busL, busR, send, 0.00, 0.30); + placeStereo(leadOct, busL, busR, send, 0.10, 0.50); // octave shimmer slightly right + placeStereo(harm, busL, busR, send, -0.40, 0.45); // 3rd-above harmony to the left + placeStereo(honkMel, busL, busR, send, 0.40, 0.55); // hoover melody-double on the right + placeStereo(subB, busL, busR, send, 0.00, 0.06); +} + +function renderBreak(busL, busR, send, drmL, drmR, kt, t0, bars, sectionName) { + // ska/skank flip: half-time trap kit + off-beat upstroke supersaw chords. + // first 4 bars stripped (no bass), last 2 bars riser → ch3. + const ch = { bass: 42, voicing: [54, 57, 61] }; + const upstroke = new Float32Array(N); + const subB = new Float32Array(N); + + for (let b = 0; b < bars; b++) { + const bt = t0 + b * bar; + if (b >= 4) { + mixEventSkrill( + { startSec: bt, midi: ch.bass - 12, durSec: bar * 0.95, gain: 0.34, preset: "sub" }, + subB, { sampleRate: SR, bpm: BPM }, + ); + } + // off-beat ska upstrokes — supersaw stab on every 'and' + for (let q = 0; q < 4; q++) { + const stabT = bt + q * beat + beat * 0.5 + jit(`${sectionName}:${b}:up${q}`, 4); + for (const m of ch.voicing) { + mixEventSupersaw( + { startSec: stabT, midi: m + 12, durSec: beat * 0.3, gain: 0.16 }, + upstroke, { sampleRate: SR, preset: "stab" }, + ); + } + } + // half-time TRAP kit (much sparser than verse/chorus) + const seedB = `${sectionName}:${b}:`; + trapKick(drmL, drmR, bt + jit(seedB + "k1"), 0.92); + kt.push(bt); + trapSnare(drmL, drmR, bt + beat * 2 + jit(seedB + "s3"), 0.78); + trapClap(drmL, drmR, bt + beat * 2 + jit(seedB + "c3"), 0.52); + // sparse open hat on each beat (let it ring — that's the "space") + for (let q = 0; q < 4; q++) { + openHat(drmL, drmR, bt + q * beat + jit(seedB + "oh" + q, 4), 0.16, q % 2 ? 0.4 : -0.4); + } + } + // riser through the last 2 bars (sweeps L→R) + riser(drmL, drmR, t0 + (bars - 2) * bar, 2 * bar, 0.62); + + // Place upstrokes hard-stereo bouncing — quintessential ska feel + // (alternating L/R per upstroke would need per-event placement; + // here we just split mono → L+R wide and drench in reverb) + for (let i = 0; i < N; i++) { + const v = upstroke[i]; + // alternate L/R via a 1Hz-ish toggle (~one full L↔R per 2 bars) + const pan = Math.sin(2 * Math.PI * (i / SR) * (BPM / 60) * 0.5); + const pL = Math.cos((pan + 1) * Math.PI / 4); + const pR = Math.sin((pan + 1) * Math.PI / 4); + busL[i] += v * pL * 1.414; + busR[i] += v * pR * 1.414; + send[i] += v * 0.55; + } + placeStereo(subB, busL, busR, send, 0, 0.10); +} + +function renderOutro(busL, busR, send, drmL, drmR, t0, bars) { + // Unresolved tail: A major pad + a single low hoover, fade. Drenched + // in reverb — "the song ends but the room keeps ringing". + const pad = new Float32Array(N); + const whoop = new Float32Array(N); + for (const m of [57, 61, 64]) { + mixEventSupersaw( + { startSec: t0, midi: m, durSec: bars * bar, gain: 0.14 }, + pad, { sampleRate: SR, preset: "pad" }, + ); + } + mixEventHoover( + { startSec: t0, midi: 45, durSec: bars * bar, gain: 0.24 }, + whoop, { sampleRate: SR, preset: "whoop" }, + ); + placeStereo(pad, busL, busR, send, -0.3, 0.75); + placeStereo(whoop, busL, busR, send, 0.3, 0.65); + // one trap kick on the 1 + trapKick(drmL, drmR, t0, 0.95); + // sparse hats fading out + for (let b = 0; b < bars; b++) { + for (let q = 0; q < 4; q++) { + const g = 0.10 * (1 - b / bars); + hat(drmL, drmR, t0 + b * bar + q * beat + beat * 0.5, g, 0.025, q % 2 ? -0.4 : 0.4); + } + } +} + +// ── arrangement ───────────────────────────────────────────────────── +const SECTIONS = [ + { name: "intro", bars: 4, kind: "intro" }, + { name: "v1", bars: 12, kind: "verse", prog: VERSE_PROG }, + { name: "ch1", bars: 8, kind: "chorus", prog: CHORUS_PROG }, + { name: "v2", bars: 8, kind: "verse", prog: VERSE_PROG }, + { name: "ch2", bars: 8, kind: "chorus", prog: CHORUS_PROG }, + { name: "break", bars: 6, kind: "break" }, + { name: "ch3", bars: 8, kind: "chorus", prog: CHORUS_PROG, final: true }, + { name: "outro", bars: 4, kind: "outro" }, +]; + +const totalBars = SECTIONS.reduce((a, s) => a + s.bars, 0); +const totalSec = totalBars * bar; +const N = Math.ceil(totalSec * SR); + +const busL = new Float32Array(N), busR = new Float32Array(N); +const drmL = new Float32Array(N), drmR = new Float32Array(N); +const send = new Float32Array(N); // reverb input bus +const wetL = new Float32Array(N), wetR = new Float32Array(N); +const kickTimes = []; + +// Compute section start times in seconds — needed for meta-FX placement. +const sectionStarts = {}; +{ + let bb = 0; + for (const sec of SECTIONS) { + sectionStarts[sec.name] = { startSec: bb * bar, bars: sec.bars, endSec: (bb + sec.bars) * bar }; + bb += sec.bars; + } +} + +let cursor = 0; +for (const sec of SECTIONS) { + const t0 = cursor * bar; + if (sec.kind === "intro") renderIntro(busL, busR, send, drmL, drmR, t0, sec.bars, N); + if (sec.kind === "verse") renderVerse(busL, busR, send, drmL, drmR, kickTimes, t0, sec.bars, sec.prog, sec.name); + if (sec.kind === "chorus") renderChorus(busL, busR, send, drmL, drmR, kickTimes, t0, sec.bars, sec.prog, !!sec.final, sec.name); + if (sec.kind === "break") renderBreak(busL, busR, send, drmL, drmR, kickTimes, t0, sec.bars, sec.name); + if (sec.kind === "outro") renderOutro(busL, busR, send, drmL, drmR, t0, sec.bars); + cursor += sec.bars; +} + +// ── META-FX layer: section transitions ───────────────────────────── +// These run AFTER all sections are rendered. They modify the already- +// rendered buffers in place (tape stops, stutter chops) and add new +// signal at transition points (reverse swells). + +// (1) Reverse cymbal swell — 1 bar before each chorus (ch1 + ch2). +// We render a noise crescendo forward then time-reverse it in place. +function reverseCymbalSwell(L, R, sendBus, startSec, durSec, g = 0.45) { + const n = Math.floor(durSec * SR); + const s0 = Math.floor(startSec * SR); + // forward swell: pink-ish noise w/ exponential growth + filter opening + const tmpL = new Float32Array(n), tmpR = new Float32Array(n); + let lpL = 0, lpR = 0; + for (let i = 0; i < n; i++) { + const w = i / n; + const a = 0.05 + 0.55 * w; + const nzL = Math.random() * 2 - 1; + const nzR = Math.random() * 2 - 1; + lpL += a * (nzL - lpL); + lpR += a * (nzR - lpR); + const env = Math.pow(w, 1.4); + tmpL[i] = (nzL - lpL) * env * g; + tmpR[i] = (nzR - lpR) * env * g; + } + // reverse in place + for (let i = 0; i < n; i++) { + if (s0 + i < N) { + L[s0 + i] += tmpL[n - 1 - i]; + R[s0 + i] += tmpR[n - 1 - i]; + sendBus[s0 + i] += (tmpL[n - 1 - i] + tmpR[n - 1 - i]) * 0.5; // drench + } + } +} +reverseCymbalSwell(drmL, drmR, send, sectionStarts.ch1.startSec - bar, bar, 0.50); +reverseCymbalSwell(drmL, drmR, send, sectionStarts.ch2.startSec - bar, bar, 0.45); + +// (2) Stutter chop on the last beat of v2 — replace last 4 sixteenths +// with 4 copies of the first sixteenth (the classic "1/16 chop"), then +// the final 1/32 gets repeated twice + silence. Operates on bus + drums. +function stutterChop(L, R, drmL_, drmR_, startSec, beatsToChop = 1) { + const sliceBeats = beatsToChop / 4; // 4 sixteenths + const sliceSec = sliceBeats * beat; + const sliceN = Math.floor(sliceSec * SR); + const s0 = Math.floor(startSec * SR); + // grab the FIRST slice (the "ka" at startSec) from each buffer + const cap = (buf) => { + const out = new Float32Array(sliceN); + for (let i = 0; i < sliceN && s0 + i < N; i++) out[i] = buf[s0 + i]; + return out; + }; + const sL = cap(L), sR = cap(R), sDL = cap(drmL_), sDR = cap(drmR_); + // overwrite the next 3 slices with that same captured slice (chop ×4) + for (let k = 1; k < 4; k++) { + const off = s0 + k * sliceN; + for (let i = 0; i < sliceN && off + i < N; i++) { + // taper amplitude across repeats so it doesn't sound like a stuck CD + const g = 1.0 - k * 0.15; + L[off + i] = sL[i] * g; + R[off + i] = sR[i] * g; + drmL_[off + i] = sDL[i] * g; + drmR_[off + i] = sDR[i] * g; + } + } +} +stutterChop(busL, busR, drmL, drmR, sectionStarts.v2.endSec - beat * 1, 1); + +// (3) Tape stop into break — last 0.4s of ch2. Slow pitch-down via +// progressively-stretched read of the source, then fade. +function tapeStop(L, R, drmL_, drmR_, sendBus, startSec, durSec = 0.45) { + const n = Math.floor(durSec * SR); + const s0 = Math.floor(startSec * SR); + // Capture the source region first (so we can overwrite it). + const capLen = n; + const srcL = new Float32Array(capLen); + const srcR = new Float32Array(capLen); + const srcDL = new Float32Array(capLen); + const srcDR = new Float32Array(capLen); + for (let i = 0; i < capLen; i++) { + if (s0 + i < N) { + srcL[i] = L[s0 + i]; + srcR[i] = R[s0 + i]; + srcDL[i] = drmL_[s0 + i]; + srcDR[i] = drmR_[s0 + i]; + } + } + // Read pointer starts at 0, advances at rate = 1.0 → 0.0 (quadratic). + let readPos = 0; + for (let i = 0; i < n; i++) { + const w = i / n; + const rate = 1.0 - Math.pow(w, 1.6); // 1 → 0 + const env = Math.pow(1 - w, 1.4); // amplitude fade + const idx = Math.floor(readPos); + const frac = readPos - idx; + const lerp = (b) => { + if (idx >= capLen - 1) return 0; + return b[idx] * (1 - frac) + b[idx + 1] * frac; + }; + if (s0 + i < N) { + L[s0 + i] = lerp(srcL) * env; + R[s0 + i] = lerp(srcR) * env; + drmL_[s0 + i] = lerp(srcDL) * env; + drmR_[s0 + i] = lerp(srcDR) * env; + sendBus[s0 + i] *= env; // also fade send to silence + } + readPos += rate; + } +} +tapeStop(busL, busR, drmL, drmR, send, sectionStarts.break.startSec - 0.45, 0.45); + +// ── reverb (deep plate, the missing space) ────────────────────────── +// Reverb wet level is automated per section (more wet in break + outro, +// less wet in chorus where the dry hooks need to punch through). +reverbToStereo(send, wetL, wetR, { room: 0.80, damp: 0.32, wet: 0.45 }); + +// Apply per-section reverb wet automation (gain on the wet buffers). +function wetEnv(sec) { + switch (sec.kind) { + case "intro": return 1.35; + case "verse": return 0.85; + case "chorus": return 0.75; + case "break": return 1.55; // BIG space in the break + case "outro": return 1.50; + default: return 1.0; + } +} +for (const sec of SECTIONS) { + const { startSec, endSec } = sectionStarts[sec.name]; + const g = wetEnv(sec); + const s0 = Math.floor(startSec * SR), s1 = Math.min(N, Math.floor(endSec * SR)); + // smooth crossfade over 50ms at boundaries to avoid clicks + const xfN = Math.floor(0.05 * SR); + for (let i = s0; i < s1; i++) { + let mul = g; + if (i - s0 < xfN) { + const w = (i - s0) / xfN; + mul = 1.0 * (1 - w) + g * w; + } else if (s1 - i < xfN) { + const w = (s1 - i) / xfN; + mul = g * w + 1.0 * (1 - w); + } + wetL[i] *= mul; + wetR[i] *= mul; + } +} + +// ── sidechain (heavy pump on the synth bus, NOT the reverb) ───────── +// Duck depth automated per section: deeper in chorus (the pump), lighter +// in verse, OFF in break (so the ska upstrokes breathe). +function duckDepth(secKind) { + switch (secKind) { + case "verse": return 0.55; // dip to .55 → recover + case "chorus": return 0.32; // dip to .32 → recover (hard pump) + case "break": return 0.85; // light, almost off + case "intro": return 0.70; + case "outro": return 0.65; + } + return 0.45; +} +function kickSecKind(t) { + for (const sec of SECTIONS) { + const s = sectionStarts[sec.name]; + if (t >= s.startSec && t < s.endSec) return sec.kind; + } + return "verse"; +} +const duck = new Float32Array(N).fill(1); +for (const kt of kickTimes) { + const dipFloor = duckDepth(kickSecKind(kt)); + const s0 = Math.floor(kt * SR), len = Math.floor(0.20 * SR); + for (let i = 0; i < len && s0 + i < N; i++) { + const w = i / len; + const d = dipFloor + (1.0 - dipFloor) * w; + if (d < duck[s0 + i]) duck[s0 + i] = d; + } +} + +// ── MASTER FX ENVELOPE CHAIN ──────────────────────────────────────── +// Time-varying processing on the (synth bus + wet reverb + drums) mix +// BEFORE saturation/crush. The envelope morphs across sections to give +// the song meta-level shape — kills the static GM feel. +// +// - lowpass alpha (a=1 → bypass, a=0 → DC). Drops between sections to +// create a momentary "muffle" then snaps open. +// - highpass alpha (the 1-pole HP). Rises in the break — opens up the +// low end for the kick to thump, also that "filter ramp" gecs trick. +// - saturation drive — gentle in verse, hot in chorus (the wall). +// - bitcrush level — harsher in chorus + final chorus. +// +// All envelopes are stored as keyframes [time, value] and lerp'd per-sample. + +const songEnd = totalSec; +function env(keyframes) { + return (t) => { + // binary-search would be cleaner but keyframes are short + let prev = keyframes[0], next = keyframes[keyframes.length - 1]; + for (let i = 0; i < keyframes.length - 1; i++) { + if (t >= keyframes[i][0] && t <= keyframes[i + 1][0]) { + prev = keyframes[i]; next = keyframes[i + 1]; break; + } + } + if (t <= prev[0]) return prev[1]; + if (t >= next[0]) return next[1]; + const w = (t - prev[0]) / (next[0] - prev[0]); + return prev[1] * (1 - w) + next[1] * w; + }; +} + +const T = sectionStarts; +// LP alpha — 1.0 = wide open. Dip to 0.35 at section boundaries for the +// "filter close" gecs effect, then snap back open across the first bar. +const lpEnv = env([ + [0, 1.00], + [T.v1.startSec, 0.50], + [T.v1.startSec + bar, 1.00], + [T.ch1.startSec - 0.15, 0.35], + [T.ch1.startSec + bar * 0.5, 1.00], + [T.v2.startSec - 0.15, 0.45], + [T.v2.startSec + bar * 0.5, 1.00], + [T.ch2.startSec - 0.15, 0.35], + [T.ch2.startSec + bar * 0.5, 1.00], + [T.break.startSec - 0.05, 0.25], // tape stop already pulls it down + [T.break.startSec + bar, 0.85], + [T.ch3.startSec - 0.15, 0.30], + [T.ch3.startSec + bar * 0.3, 1.00], // CH3 SLAM OPEN + [T.outro.startSec, 1.00], + [songEnd, 0.55], +]); +// HP alpha — 0 = bypass; rises = more low-cut. Climb during break for +// the "filter ramp" thinning, then drop back at ch3. +const hpEnv = env([ + [0, 0.0], + [T.break.startSec, 0.05], + [T.break.startSec + bar * 3, 0.35], // peak thin in middle of break + [T.break.endSec - bar, 0.10], + [T.ch3.startSec, 0.0], + [songEnd, 0.0], +]); +// Saturation drive — gentle (1.0) verse, hotter (1.45) chorus, off in break. +const driveEnv = env([ + [0, 1.00], + [T.v1.startSec, 1.05], + [T.ch1.startSec, 1.35], + [T.ch1.endSec, 1.10], + [T.v2.startSec, 1.10], + [T.ch2.startSec, 1.40], + [T.ch2.endSec, 1.40], + [T.break.startSec, 0.90], // break breathes + [T.break.endSec, 1.30], + [T.ch3.startSec, 1.55], // final chorus hottest + [T.outro.startSec, 1.10], + [songEnd, 0.90], +]); +// Bitcrush levels — fewer levels = crunchier. 256 ≈ transparent. +const crushEnv = env([ + [0, 256], + [T.v1.startSec, 160], + [T.ch1.startSec, 96], + [T.v2.startSec, 160], + [T.ch2.startSec, 80], + [T.break.startSec, 256], // clean for the break + [T.ch3.startSec, 64], // CH3 cranks the crush + [T.outro.startSec, 160], + [songEnd, 256], +]); + +// Apply master FX in-place — 1-pole LP + 1-pole HP + drive + crush. +const masterL = new Float32Array(N), masterR = new Float32Array(N); +{ + let lpStateL = 0, lpStateR = 0; + let hpStateL = 0, hpStateR = 0, hpInPrevL = 0, hpInPrevR = 0; + for (let i = 0; i < N; i++) { + const t = i / SR; + const a = lpEnv(t); // 1-pole LP coefficient + const h = hpEnv(t); // HP amount (0=off) + const drv = driveEnv(t); + const lvls = crushEnv(t); + + let xL = busL[i] * duck[i] + wetL[i] * 0.85 + drmL[i]; + let xR = busR[i] * duck[i] + wetR[i] * 0.85 + drmR[i]; + + // LP + lpStateL += a * (xL - lpStateL); + lpStateR += a * (xR - lpStateR); + let yL = lpStateL, yR = lpStateR; + + // HP (1-pole HP = x - x_lp; blend by h) + if (h > 0.001) { + // simple HP: y[n] = (1-h)*(y[n-1] + x[n] - x[n-1]) + const k = 1 - h; + hpStateL = k * (hpStateL + yL - hpInPrevL); + hpStateR = k * (hpStateR + yR - hpInPrevR); + hpInPrevL = yL; hpInPrevR = yR; + yL = hpStateL; yR = hpStateR; + } else { + hpInPrevL = yL; hpInPrevR = yR; + hpStateL = yL; hpStateR = yR; + } + + // Drive + crush + masterL[i] = crush(Math.tanh(yL * drv), lvls); + masterR[i] = crush(Math.tanh(yR * drv), lvls); + } +} + +// ── master out ─────────────────────────────────────────────────────── +const mix = new Float32Array(N * 2); +let peak = 0; +for (let i = 0; i < N; i++) { + mix[i * 2] = masterL[i]; + mix[i * 2 + 1] = masterR[i]; + if (Math.abs(masterL[i]) > peak) peak = Math.abs(masterL[i]); + if (Math.abs(masterR[i]) > peak) peak = Math.abs(masterR[i]); +} +if (peak > 0) { const nrm = 0.82 / peak; for (let i = 0; i < mix.length; i++) mix[i] *= nrm; } + +const outPath = expandHome(flags.out) || resolve(HERE, "../out/gecs.mp3"); +mkdirSync(dirname(outPath), { recursive: true }); +const rawPath = `${outPath}.f32.raw`; +const b = Buffer.alloc(mix.length * 4); +for (let i = 0; i < mix.length; i++) b.writeFloatLE(mix[i], i * 4); +writeFileSync(rawPath, b); +console.log( + `→ gecs [bed v3 · trap+space+meta-fx+harmony] · ${BPM} BPM · ${totalBars} bars · ${(totalBars * bar).toFixed(1)}s · ` + + `stereo + reverb + envelopes · intro → v1 → ch1 → v2 → ch2 → BREAK(ska) → ch3 → outro`, +); +const ff = spawnSync("ffmpeg", [ + "-hide_banner", "-y", "-loglevel", "error", + "-f", "f32le", "-ar", String(SR), "-ac", "2", "-i", rawPath, + "-c:a", "libmp3lame", "-q:a", "2", outPath, +], { stdio: "inherit" }); +try { unlinkSync(rawPath); } catch {} +if (ff.status !== 0) { console.error("✗ ffmpeg failed"); process.exit(1); } +console.log(`✓ ${outPath}`); diff --git a/pop/gecs/bin/sing.mjs b/pop/gecs/bin/sing.mjs new file mode 100755 index 0000000000..b95de42a38 --- /dev/null +++ b/pop/gecs/bin/sing.mjs @@ -0,0 +1,282 @@ +#!/usr/bin/env node +// gecs/bin/sing.mjs — jeffrey-pvc sung vocal for kernel panic. Same +// proven pipeline as pop/hippyhayzard/bin/sing.mjs (say → align → +// score-pitch → score-stretch) but tuned HOT for 100-gecs hyperpop: +// +// • TTS: stability 0.55 (still ≥0.5 — clone stays jeffrey; per the +// jeffrey_pvc_settings memory anything lower drifts), style 0.55 +// (more bratty / less measured), speed 1.10 (rapid delivery). +// • score-pitch: heavier vibrato (6.5Hz × 22c) for the autotune-wobble +// gecs vocal tic; the .np already does the chipmunk lift in the +// chorus by writing the melody up at A4-F#5. +// • score-stretch: 168 BPM so the sung line packs tight inside the +// 84.4s bed (162 left some chorus held notes ringing past the kit). +// • mix: aggressive comp + presence EQ (+5dB ~3.5kHz) + light tape- +// style saturation; no reverb (gecs vox sit dry on top of the +// bit-crushed bed). Bed hard-ducked under the vocal (ratio 10). +// +// Word order MUST match pop/gecs/gecs.txt strictly (175 words). +// +// Usage: +// node pop/gecs/bin/sing.mjs # full sung song +// node pop/gecs/bin/sing.mjs --force # bypass say cache + +import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const argv = process.argv.slice(2); +const flags = {}; +for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith("--")) { + const k = a.slice(2), n = argv[i + 1]; + if (n !== undefined && !n.startsWith("--")) { flags[k] = n; i++; } else flags[k] = true; + } +} + +const HERE = dirname(fileURLToPath(import.meta.url)); +const G = resolve(HERE, ".."); +const POP = resolve(G, ".."); +const REL = (p) => p.replace(POP + "/", ""); + +const SCORE = `${G}/gecs.np`; +const LYRIC = `${G}/gecs.txt`; +const OUT_DIR = `${G}/out`; +const TMP = `${OUT_DIR}/.tmp`; +mkdirSync(TMP, { recursive: true }); + +const VOCAL = `${OUT_DIR}/gecs-vocal.mp3`; +const VOCAL_ALIGN = `${VOCAL}.alignment.json`; +const WORDS = `${OUT_DIR}/gecs-vocal-words.json`; +const PITCHED = `${OUT_DIR}/gecs-pitched.mp3`; +const PITCHED_ALIGN = `${OUT_DIR}/gecs-pitched-alignment.json`; +const STRETCHED = `${OUT_DIR}/gecs-stretched.mp3`; +const BED = `${OUT_DIR}/gecs.mp3`; +const FINAL = resolve(process.cwd(), flags.out || `${OUT_DIR}/gecs-song.mp3`); +const FORCE = !!flags.force; + +for (const [p, what] of [[SCORE, "score"], [LYRIC, "lyric"]]) { + if (!existsSync(p)) { console.error(`✗ ${what} missing: ${p}`); process.exit(1); } +} +if (!existsSync(BED)) { + console.error(`✗ bed missing: ${BED}\n run: node pop/gecs/bin/render.mjs`); + process.exit(1); +} + +function run(cmd, args, cwd = POP, soft = false) { + const r = spawnSync(cmd, args, { cwd, stdio: ["ignore", "inherit", "inherit"] }); + if (r.status !== 0 && !soft) { + console.error(`✗ ${cmd} ${args.slice(0, 3).join(" ")}… failed (exit ${r.status})`); + process.exit(1); + } + return r.status === 0; +} +function step(name, fn) { + const t0 = Date.now(); + process.stdout.write(`▸ ${name}\n`); + fn(); + console.log(` ${((Date.now() - t0) / 1000).toFixed(1)}s`); +} + +console.log(`━━━ gecs · sing (jeffrey-pvc hyperpop) ━━━\n`); + +// ── 1. say × N — one ElevenLabs call PER SECTION with section-specific +// voice settings. Then concatenate the per-section mp3s into VOCAL and +// merge per-section alignment JSONs into VOCAL_ALIGN (with cumulative +// time offsets) so steps 2-5 see a single contiguous vocal as before. +// +// verse — stability 0.72, style 0.30, speed 1.10 (solid + rapid) +// chorus — stability 0.58, style 0.65, speed 1.05 (excited, big) +// breakdown — stability 0.50, style 0.85, speed 0.95 (shouted, loose) +// +// say.mjs already content-hash-caches per call body, so unchanged +// sections skip the API on re-run. `--force` invalidates all. + +// Parse gecs.txt into ORDERED sections (chorus appears 3× — sing each). +function parseSections(txt) { + const out = []; + let cur = null; + for (const raw of txt.split("\n")) { + const t = raw.trim(); + if (!t) continue; + const m = t.match(/^(verse \d+|chorus|breakdown|hook|intro|outro|bridge)$/i); + if (m) { + if (cur) out.push(cur); + const name = m[1].toLowerCase(); + const kind = name.startsWith("verse") ? "verse" + : name === "chorus" ? "chorus" + : name === "breakdown" ? "breakdown" : name; + cur = { name, kind, lines: [] }; + } else if (cur) { + cur.lines.push(t); + } + } + if (cur) out.push(cur); + return out; +} + +const SECTION_VOICE = { + verse: { stability: "0.72", style: "0.30", speed: "1.10" }, + chorus: { stability: "0.58", style: "0.65", speed: "1.05" }, + breakdown: { stability: "0.50", style: "0.85", speed: "0.95" }, +}; + +const SECTIONS_TXT = parseSections(readFileSync(LYRIC, "utf8")); +const SECTION_DIR = `${TMP}/sections`; +mkdirSync(SECTION_DIR, { recursive: true }); + +step(`1 · say × ${SECTIONS_TXT.length} (per-section voice settings)`, () => { + // Number each section occurrence so the temp file paths are unique + // (3 chorus calls → chorus-1.txt, chorus-2.txt, chorus-3.txt etc). + const counts = {}; + for (const sec of SECTIONS_TXT) { + counts[sec.name] = (counts[sec.name] || 0) + 1; + sec.idx = counts[sec.name]; + sec.slug = sec.name.replace(/\s+/g, ""); // "verse 1" → "verse1" + sec.txtPath = `${SECTION_DIR}/${sec.slug}-${sec.idx}.txt`; + sec.mp3Path = `${SECTION_DIR}/${sec.slug}-${sec.idx}.mp3`; + sec.alignPath = `${sec.mp3Path}.alignment.json`; + writeFileSync(sec.txtPath, sec.lines.join("\n") + "\n"); + } + // Fire each section sequentially (parallel hammers the API + machine). + for (const sec of SECTIONS_TXT) { + const v = SECTION_VOICE[sec.kind] || SECTION_VOICE.verse; + const args = ["bin/say.mjs", REL(sec.txtPath), + "--stability", v.stability, "--similarity", "0.92", "--style", v.style, + "--speed", v.speed, "--out", REL(sec.mp3Path), "--timestamps"]; + if (FORCE) args.push("--force"); + process.stdout.write(` · ${sec.name}(${sec.idx}) [${sec.kind}: stab ${v.stability} · sty ${v.style} · spd ${v.speed}]\n`); + const r = spawnSync("node", args, { cwd: POP, stdio: ["ignore", "inherit", "inherit"] }); + if (r.status !== 0) { console.error(`✗ say failed for ${sec.name}(${sec.idx})`); process.exit(1); } + } +}); + +// ── 2. concat per-section mp3s + merge alignments → VOCAL + WORDS ──── +const lyricWords = readFileSync(LYRIC, "utf8") + .split("\n") + .filter((l) => l.trim() && !/^(verse \d+|hook|intro|outro|bridge|chorus|breakdown)$/i.test(l.trim())) + .join(" ") + .split(/\s+/).map((w) => w.replace(/[^a-zA-Z0-9]/g, "")).filter(Boolean); +const expected = lyricWords.length; + +step(`2 · concat sections + merge alignments (target ${expected} words)`, () => { + // Convert each section mp3 → 48k wav, measure exact duration, and + // stitch with ffmpeg concat. Wav avoids the mp3-concat priming gap. + const wavList = []; + let offsetSec = 0; + const mergedWords = []; + for (const sec of SECTIONS_TXT) { + const wav = `${SECTION_DIR}/${sec.slug}-${sec.idx}.wav`; + run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", + "-i", sec.mp3Path, "-ar", "48000", "-ac", "1", wav]); + wavList.push(wav); + // word offsets: shift this section's words by cumulative offset + if (!existsSync(sec.alignPath)) { + console.error(`✗ alignment missing for ${sec.name}(${sec.idx})`); process.exit(1); + } + const a = JSON.parse(readFileSync(sec.alignPath, "utf8")); + const offMs = Math.round(offsetSec * 1000); + for (const w of (a.words || [])) { + mergedWords.push({ + text: w.text.replace(/[^a-zA-Z0-9]/g, ""), + fromMs: w.fromMs + offMs, + toMs: w.toMs + offMs, + }); + } + const probe = spawnSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", wav], { encoding: "utf8" }); + offsetSec += parseFloat(probe.stdout.trim()) || 0; + } + // ffmpeg concat demuxer requires a manifest + const manifest = `${SECTION_DIR}/manifest.txt`; + writeFileSync(manifest, wavList.map((p) => `file '${p}'`).join("\n") + "\n"); + const combinedWav = `${SECTION_DIR}/combined.wav`; + run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", + "-f", "concat", "-safe", "0", "-i", manifest, + "-ar", "48000", "-ac", "1", combinedWav]); + // combined wav → mp3 for VOCAL (so the downstream pipeline gets mp3) + run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", + "-i", combinedWav, "-c:a", "libmp3lame", "-q:a", "2", VOCAL]); + // Synthesize a combined alignment json (matches format of single say.mjs) + writeFileSync(VOCAL_ALIGN, JSON.stringify({ + source: "elevenlabs/with-timestamps/multi-section", + sections: SECTIONS_TXT.map((s) => ({ name: s.name, idx: s.idx, kind: s.kind })), + words: mergedWords, + }, null, 2)); + writeFileSync(WORDS, JSON.stringify(mergedWords, null, 0)); + console.log(` ✓ ${mergedWords.length} words across ${SECTIONS_TXT.length} sections · combined ${offsetSec.toFixed(1)}s vocal`); +}); + +// ── 3. score-pitch (WORLD f0 → the .np melody, heavy hyperpop vibrato) ── +step("3 · score-pitch (WORLD · hyperpop wobble 6.5Hz/22c)", () => { + run("node", ["bin/score-pitch.mjs", + "--slug", "gecs", "--section", "all", + "--score", REL(SCORE), "--vocal", REL(VOCAL), "--words", REL(WORDS), + "--transpose", "0", "--vibrato-hz", "6.5", "--vibrato-cents", "22", + "--out", REL(PITCHED)]); +}); + +// ── 4. score-stretch (pack into the 84.4s bed) ──────────────────────── +step("4 · score-stretch (rubberband · 168 BPM → fits 84.4s bed)", () => { + run("node", ["bin/score-stretch.mjs", + "--slug", "gecs", "--section", "all", + "--score", REL(SCORE), "--in", REL(PITCHED), "--alignment", REL(PITCHED_ALIGN), + "--bpm", "168", "--max-stretch", "8.0", "--out", REL(STRETCHED)]); +}); + +// ── 5. hyperpop mix: dry, hot, in-your-face. Lay over the bed. ──────── +step("5 · treat + mix onto the bed (intro = 4 bars @ 165 BPM)", () => { + const lead = `${TMP}/lead.wav`; + run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", "-i", STRETCHED, + "-ar", "48000", "-ac", "1", lead]); + + // intro = 4 bars @ 165 BPM = 4 * (60/165*4) = 5.818 s + const introMs = Math.round(4 * (60 / 165 * 4) * 1000); + + const bp = spawnSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", BED], { encoding: "utf8" }); + const bedDur = parseFloat(bp.stdout.trim()) || 84.4; + const fadeStart = Math.max(0, bedDur - 1.0); + + // VOCAL chain (less airy): + // highpass 80Hz only (was 110Hz — that was eating chest warmth) + // acompressor (thresh -18dB, ratio 4, gentle attack — keep body) + // equalizer +2.5dB 250Hz (RESTORE chest, not de-mud — was -3dB) + // equalizer +2dB ~2.5kHz (presence cut-through, not the harsh 3.5k +5dB) + // equalizer -2dB 6kHz (KILL the airy/breathy top end) + // + // BED stays at parity (0.85 vol) and only gets light ducking (ratio 3, + // higher threshold) — earlier ratio-10/thresh-0.015 was making the bed + // disappear the moment any vocal hit. Now bed is heard THROUGH vocal. + const fc = + `[1:a]highpass=f=80,` + + `acompressor=threshold=-18dB:ratio=4:attack=8:release=160:makeup=4:knee=4,` + + `equalizer=f=250:t=q:w=1.0:g=2.5,` + + `equalizer=f=2500:t=q:w=1.4:g=2,` + + `equalizer=f=6000:t=q:w=1.4:g=-2,` + + `acompressor=threshold=-10dB:ratio=2.5:attack=4:release=80:makeup=1.5,` + + `volume=1.35,` + + `adelay=${introMs}|${introMs},` + + `apad=whole_dur=${bedDur.toFixed(3)}[vox];` + + `[vox]asplit=2[voxM][voxK];` + + `[0:a]volume=0.95[bedlo];` + + `[bedlo][voxK]sidechaincompress=threshold=0.08:ratio=3:attack=10:release=200:makeup=1[bedd];` + + `[bedd][voxM]amix=inputs=2:duration=first:normalize=0,` + + `atrim=duration=${bedDur.toFixed(3)},` + + `afade=t=out:st=${fadeStart.toFixed(3)}:d=1.0,` + + `loudnorm=I=-11:TP=-1.0:LRA=8,aresample=48000[out]`; + + run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", + "-i", BED, "-i", lead, + "-filter_complex", fc, "-map", "[out]", + "-c:a", "libmp3lame", "-q:a", "2", FINAL]); +}); + +mkdirSync(dirname(FINAL), { recursive: true }); +const probe = spawnSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", FINAL], { encoding: "utf8" }); +const sz = (readFileSync(FINAL).length / 1024).toFixed(0); +console.log(`\n✓ ${FINAL} (${sz} KB · ${parseFloat(probe.stdout || 0).toFixed(1)}s · jeffrey-pvc hyperpop sung on the bed)`); +copyFileSync(FINAL, `${OUT_DIR}/gecs-song.mp3`); diff --git a/pop/gecs/gecs.np b/pop/gecs/gecs.np new file mode 100644 index 0000000000..332342580b --- /dev/null +++ b/pop/gecs/gecs.np @@ -0,0 +1,80 @@ +# gecs — kernel panic. hyperpop / 100-gecs style score for jeffrey-pvc. +# verses sit in F#m, mid-low rap pocket (MIDI 54-66); chorus jumps an +# octave into A major (MIDI 69-79) for the chipmunk lift. break is a +# single-pitch shouted/spat section. notes track A-major chord tones in +# the chorus + relative-minor (F#m) chord tones in the verses so it +# stays consonant against the supersaw / skrill bed in bin/render.mjs. +# notation: NOTE:syllable*beats ('-' continues a held/melisma word) +# WORD ORDER strictly matches pop/gecs/gecs.txt. +# bin/sing.mjs splits the score on `verse N` headers only — chorus / +# breakdown blocks live INSIDE verse 1 / verse 2 inline. + +verse 1 +# verse 1 — tabs open / fan on max / fingers cramping on the keys ... +F#3:tabs*1 A3:o-*0.5 A3:-pen*0.5 F#3:fan*1 F#3:on*0.5 A3:max*1.5 +F#3:fin-*0.5 F#3:-gers*0.5 A3:cramp-*0.5 A3:-ing*0.5 F#3:on*0.5 F#3:the*0.5 C#4:keys*2 +# compile error cant recover / pushed the branch and lost a cover +F#3:com-*0.5 F#3:-pile*0.5 A3:er-*0.5 A3:-ror*0.5 F#3:cant*0.5 F#3:re-*0.5 A3:-cov-*0.5 A3:-er*0.5 +F#3:pushed*0.5 F#3:the*0.5 A3:branch*0.5 F#3:and*0.5 F#3:lost*0.5 F#3:a*0.5 C#4:cov-*1 A3:-er*1 +# lith is down / oven choked / cdn cache still revoked +A3:lith*0.5 A3:is*0.5 F#3:down*1 +A3:o-*0.5 A3:-ven*0.5 F#3:choked*1 +A3:cdn*0.5 A3:cache*0.5 F#3:still*0.5 F#3:re-*0.5 C#4:-voked*2 +# thirty four percent left / wrote a poem that i kept +F#3:thir-*0.5 F#3:-ty*0.5 A3:four*0.5 A3:per-*0.5 F#3:-cent*0.5 F#3:left*1.5 +A3:wrote*0.5 A3:a*0.5 F#3:po-*0.5 F#3:-em*0.5 A3:that*0.5 A3:i*0.5 C#4:kept*3 + +# CHORUS 1 — pitched UP to A4-E5 chord tones (the chipmunk lift) +# kernel panic kernel panic blue light scream +A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 +E5:blue*1 C#5:light*1 A4:scream*2 +# kernel panic kernel panic wake up from the dream +A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 +E5:wake*0.5 E5:up*0.5 C#5:from*0.5 C#5:the*0.5 A4:dream*3 +# everything is okay / everything is fine +C#5:ev-*0.5 C#5:-ry-*0.5 A4:-thing*0.5 A4:is*0.5 E5:o-*0.5 E5:-kay*1.5 +C#5:ev-*0.5 C#5:-ry-*0.5 A4:-thing*0.5 A4:is*0.5 F#5:fine*3 +# kernel panic on the ninety ninth time +A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 A4:on*0.5 A4:the*0.5 +E5:nine-*0.5 E5:-ty*0.5 C#5:ninth*1 A4:time*3 + +verse 2 +# verse 2 — fia walked in / coffee in her hand / asking why my laptop wont land +F#3:fi-*0.5 F#3:-a*0.5 A3:walked*0.5 A3:in*1.5 +F#3:cof-*0.5 F#3:-fee*0.5 A3:in*0.5 A3:her*0.5 C#4:hand*2 +F#3:ask-*0.5 F#3:-ing*0.5 A3:why*0.5 A3:my*0.5 F#3:lap-*0.5 F#3:-top*0.5 +A3:wont*1 C#4:land*3 +# papers in the printer / line is dim / dossier glowing at the rim +F#3:pa-*0.5 F#3:-pers*0.5 A3:in*0.5 A3:the*0.5 F#3:prin-*0.5 F#3:-ter*0.5 +A3:line*0.5 A3:is*0.5 C#4:dim*2 +F#3:dos-*0.5 F#3:-si-*0.5 A3:-er*0.5 A3:glow-*0.5 A3:-ing*0.5 F#3:at*0.5 F#3:the*0.5 C#4:rim*3 + +# CHORUS 2 — identical melody to chorus 1 +A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 +E5:blue*1 C#5:light*1 A4:scream*2 +A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 +E5:wake*0.5 E5:up*0.5 C#5:from*0.5 C#5:the*0.5 A4:dream*3 +C#5:ev-*0.5 C#5:-ry-*0.5 A4:-thing*0.5 A4:is*0.5 E5:o-*0.5 E5:-kay*1.5 +C#5:ev-*0.5 C#5:-ry-*0.5 A4:-thing*0.5 A4:is*0.5 F#5:fine*3 +A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 A4:on*0.5 A4:the*0.5 +E5:nine-*0.5 E5:-ty*0.5 C#5:ninth*1 A4:time*3 + +# BREAKDOWN — single-pitch shout/spat. F#3 monotone, no melody. +# re boot re boot re boot +F#3:re*0.5 F#3:boot*0.5 F#3:re*0.5 F#3:boot*0.5 F#3:re*0.5 F#3:boot*1 +# re load re load re load +F#3:re*0.5 F#3:load*0.5 F#3:re*0.5 F#3:load*0.5 F#3:re*0.5 F#3:load*1 +# type the password with my hands (slight lift to A3 on "hands") +F#3:type*0.5 F#3:the*0.5 F#3:pass-*0.5 F#3:-word*0.5 F#3:with*0.5 F#3:my*0.5 A3:hands*1 +# neo boot loop never lands (lift into the next chorus) +F#3:ne-*0.5 F#3:-o*0.5 A3:boot*0.5 A3:loop*0.5 C#4:nev-*0.5 C#4:-er*0.5 E4:lands*1 + +# CHORUS 3 — final, same melody (sing.mjs will double an octave up here) +A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 +E5:blue*1 C#5:light*1 A4:scream*2 +A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 +E5:wake*0.5 E5:up*0.5 C#5:from*0.5 C#5:the*0.5 A4:dream*3 +C#5:ev-*0.5 C#5:-ry-*0.5 A4:-thing*0.5 A4:is*0.5 E5:o-*0.5 E5:-kay*1.5 +C#5:ev-*0.5 C#5:-ry-*0.5 A4:-thing*0.5 A4:is*0.5 F#5:fine*3 +A4:ker-*0.5 A4:-nel*0.5 C#5:pa-*0.5 C#5:-nic*0.5 A4:on*0.5 A4:the*0.5 +E5:nine-*0.5 E5:-ty*0.5 C#5:ninth*1 A4:time*5 diff --git a/pop/gecs/gecs.txt b/pop/gecs/gecs.txt new file mode 100644 index 0000000000..c4cb092131 --- /dev/null +++ b/pop/gecs/gecs.txt @@ -0,0 +1,35 @@ +verse 1 +tabs. open. fan. on. max. fingers. cramping. on. the. keys. +compile. error. cant. recover. pushed. the. branch. and. lost. a. cover. +lith. is. down. oven. choked. cdn. cache. still. revoked. +thirty. four. percent. left. wrote. a. poem. that. i. kept. + +chorus +kernel. panic. kernel. panic. blue. light. scream. +kernel. panic. kernel. panic. wake. up. from. the. dream. +everything. is. okay. everything. is. fine. +kernel. panic. on. the. ninety. ninth. time. + +verse 2 +fia. walked. in. coffee. in. her. hand. +asking. why. my. laptop. wont. land. +papers. in. the. printer. line. is. dim. +dossier. glowing. at. the. rim. + +chorus +kernel. panic. kernel. panic. blue. light. scream. +kernel. panic. kernel. panic. wake. up. from. the. dream. +everything. is. okay. everything. is. fine. +kernel. panic. on. the. ninety. ninth. time. + +breakdown +re. boot. re. boot. re. boot. +re. load. re. load. re. load. +type. the. password. with. my. hands. +neo. boot. loop. never. lands. + +chorus +kernel. panic. kernel. panic. blue. light. scream. +kernel. panic. kernel. panic. wake. up. from. the. dream. +everything. is. okay. everything. is. fine. +kernel. panic. on. the. ninety. ninth. time. diff --git a/pop/hellsine/bin/bake.mjs b/pop/hellsine/bin/bake.mjs index bb040870a2..111b7a903c 100644 --- a/pop/hellsine/bin/bake.mjs +++ b/pop/hellsine/bin/bake.mjs @@ -61,20 +61,19 @@ if (SCRATCH_ON) { } // 3 — finalize for Spotify (Spotify normalises to -14 on playback) -// Brightening polish — pure-sine mixes render very dark (lows ~31 dB -// over the air band on the first hellsine cut). Hardcore wants it -// bright + aggressive, so this is heavier than the trance note: a big -// high-shelf for air, presence + sparkle bells, and a mud trim. +// The new mix (halftime hole-kick + 8-partial pads + sparkle + steam) +// is inherently brighter than the original dark gabber-punch cut, so +// the brightening pass is now a LIGHT polish. The old +8 dB shelf + +// dual presence/sparkle boosts piled into the 2-12 kHz range and read +// as buzz hash. Keep just a gentle air lift + warm the low end. run("ffmpeg", ["-y", "-i", mix, "-af", - "highshelf=f=7500:g=8," + - "equalizer=f=4200:t=q:w=1.1:g=4," + - "equalizer=f=12000:t=q:w=1.4:g=4," + - "equalizer=f=200:t=q:w=1.0:g=-2.5," + + "highshelf=f=9000:g=2," + + "equalizer=f=150:t=q:w=1.0:g=2," + // warmth back (kick is subby now) "acompressor=threshold=-18dB:ratio=2:attack=15:release=240:makeup=1:knee=8," + "loudnorm=I=-14:TP=-1.5:LRA=11," + "alimiter=limit=0.94:attack=6:release=110:level=disabled," + - "afade=t=out:st=104.5:d=4", - "-ar", "44100", "-sample_fmt", "s16", finalWav], "finalize → -14 LUFS (bright)"); + "afade=t=out:st=162.5:d=4", + "-ar", "44100", "-sample_fmt", "s16", finalWav], "finalize → -14 LUFS (light polish)"); run("ffmpeg", ["-y", "-i", finalWav, "-codec:a", "libmp3lame", "-b:a", "320k", finalMp3], "320k mp3"); spawnSync("rm", ["-f", scr]); diff --git a/pop/hellsine/bin/hellsine.mjs b/pop/hellsine/bin/hellsine.mjs index 6ae4133325..3752828c62 100644 --- a/pop/hellsine/bin/hellsine.mjs +++ b/pop/hellsine/bin/hellsine.mjs @@ -94,16 +94,17 @@ const CHORD = { const cyc = (arr, n) => arr[n % arr.length]; // ── arrangement: ordered sections, each N bars ──────────────────────── -// "study music under a film arc" — continuous, no dead air. The kick is -// the hardcore spine; it thins to a pulse in the bridge (the study -// zone) and is fully absent only in the overture / coda tail. +// Longer sections + halftime beat = each kick lands like a HOLE, not a +// punch. Spread out + heavy ducking = the negative space carries the +// hardcore weight. "Study music under a film arc" still — continuous, +// no dead air, more sine voices layered for body. const PLAN = [ - { name: "overture", bars: 8, kick: "none", drive: 0, chords: ["Dm","Dm","Bb","Bb","F","F","C","A"], theme: "soft" }, - { name: "statement", bars: 16, kick: "four", drive: HELL, chords: ["Dm","Dm","Bb","Bb","F","F","C","C"], theme: "brass" }, - { name: "bridge", bars: 16, kick: "pulse", drive: HELL*0.7, chords: ["Gm","Gm","Dm","Dm","C","C","Bb","A"], theme: "bsoft" }, - { name: "develop", bars: 16, kick: "four", drive: HELL*1.15, chords: ["Dm","Dm","F","F","Gm","Gm","A","A","Bb","Bb","C","C","Dm","A","Dm","A"], theme: "frag" }, - { name: "climax", bars: 16, kick: "hard", drive: HELL*1.3, transpose: 2, chords: ["Dm","Dm","Bb","Bb","F","F","C","C"], theme: "brass" }, - { name: "coda", bars: 8, kick: "fade", drive: HELL*0.6, chords: ["Dm","Dm","Gm","Gm","C","C","Dm","Dm"], theme: "dissolve" }, + { name: "overture", bars: 12, kick: "none", drive: 0, chords: ["Dm","Dm","Bb","Bb","F","F","C","A","Dm","Bb","F","C"], theme: "soft" }, + { name: "statement", bars: 24, kick: "halftime", drive: HELL, chords: ["Dm","Dm","Bb","Bb","F","F","C","C"], theme: "brass" }, + { name: "bridge", bars: 24, kick: "pulse", drive: HELL*0.7, chords: ["Gm","Gm","Dm","Dm","C","C","Bb","A"], theme: "bsoft" }, + { name: "develop", bars: 24, kick: "halftime", drive: HELL*1.15, chords: ["Dm","Dm","F","F","Gm","Gm","A","A","Bb","Bb","C","C","Dm","A","Dm","A"], theme: "frag" }, + { name: "climax", bars: 24, kick: "halfhard", drive: HELL*1.3, transpose: 2, chords: ["Dm","Dm","Bb","Bb","F","F","C","C"], theme: "brass" }, + { name: "coda", bars: 16, kick: "fade", drive: HELL*0.6, chords: ["Dm","Dm","Gm","Gm","C","C","Dm","Dm"], theme: "dissolve" }, ]; const TOTAL_BARS = PLAN.reduce((a, s) => a + s.bars, 0); const TAIL = 3.2; @@ -124,34 +125,82 @@ function write(t, fn, dur) { } } -// The gabber/Rotterdam kick: ONE sine, fast exponential pitch drop, then -// tanh-saturated past its skin. The distorted sine = the kick. +// The HOLE kick — no punch, no click, no transient. A slow-blooming +// sub-only sine pitch-drop that lives mostly through what it removes +// from the surrounding mix. Each hit is a vacuum carved by the duck +// (15 ms slam-shut → 450 ms slow re-open); the kick itself is just the +// bass pressure filling the hole. Still pure sine, still saturated for +// hardcore body, just stripped of its bright attack. function kick(t, drive = HELL, gain = 1) { - const dur = 0.26, pStart = 240, pEnd = 47, pT = 0.034; + const dur = 0.55; // longer body (was 0.26) + const pStart = 180, pEnd = 28; // start lower, end deep-sub (was 240→47) + const pT = 0.080; // slow pitch sweep (was 0.034) let ph = 0; - // mark sidechain duck - const di = Math.floor(t * SR), dN = Math.floor(0.20 * SR); + // The HOLE: instant slam-shut (15 ms), then slow re-open over 450 ms. + // Ducks the sub/pad/strings/theme to 8% — they all vacuum out around + // each kick, which is what makes the kick FEEL like a hole. + const di = Math.floor(t * SR); + const dN = Math.floor(0.46 * SR); + const closeN = Math.floor(0.015 * SR); // 15 ms slam + const openN = dN - closeN; // 445 ms re-open for (let k = 0; k < dN && di + k < N; k++) { - const env = 0.32 + 0.68 * (1 - Math.exp(-k / (0.16 * SR))); + let env; + if (k < closeN) { + env = 1 - (k / closeN) * 0.92; // 1.0 → 0.08 over 15 ms + } else { + const p = (k - closeN) / openN; + env = 0.08 + 0.92 * (1 - Math.exp(-p * 3.2)); // smooth re-open + } if (env < DUCK[di + k]) DUCK[di + k] = env; } write(t, (lt) => { const f = pEnd + (pStart - pEnd) * Math.exp(-lt / pT); ph += (TAU * f) / SR; - const amp = Math.exp(-lt / 0.10) * (1 - Math.exp(-lt / 0.0008)); // click→thump + // Slow bloom (12 ms attack — was 0.8 ms!) + long body + const atk = 1 - Math.exp(-lt / 0.012); + const decay = Math.exp(-lt / 0.22); // longer tail + const amp = atk * decay; let x = Math.sin(ph); - x = Math.tanh(x * (drive * (0.7 + 0.3 * Math.exp(-lt / 0.05)))); // the hell - x = Math.max(-0.97, Math.min(0.97, x * 1.06)); // skin - // gabber transient — a bright, ~4 ms high sine SNAP at the attack. - // This is the genre's HF "tick"; pure sine, just fast. Without it a - // sine-only mix is far too dark for hardcore. - const click = Math.sin(TAU * 3400 * lt) * Math.exp(-lt / 0.0016) * 0.5 - + Math.sin(TAU * 1700 * lt) * Math.exp(-lt / 0.0042) * 0.35; - const v = (x * amp + click) * 0.9 * gain; + x = Math.tanh(x * (drive * 0.45)); // gentler saturation (was full) + const v = x * amp * 0.95 * gain; return [v, v]; }, dur); } +// ── steam release — additive-sine "white noise" breath ─────────────── +// THE LAW HOLDS: a dense bank of detuned sines summed IS broadband noise +// (Fourier identity). Frequencies are log-spaced + jittered across the +// air band so the result reads as steam/breath, not as pitched chord. +// Stays inside the all-sine constraint and gives the track the hiss the +// pure-sine mix otherwise lacks. +function steam(t, dur, gain = 0.12, opt = {}) { + const nVoices = opt.voices || 48; + const fMin = opt.fMin || 400; // more body, less hiss + const fMax = opt.fMax || 5500; // pulled out of the buzz band (was 9500) + const atk = opt.atk ?? 0.6; // slow steam release + const rel = opt.rel ?? 1.2; + const breathRate = opt.breathRate || 0.5; // gentle steam pulsing + const breathDepth = opt.breathDepth ?? 0.35; + const freqs = new Float64Array(nVoices); + const phases = new Float64Array(nVoices); + for (let i = 0; i < nVoices; i++) { + const u = (i + rng()) / nVoices; + freqs[i] = fMin * Math.pow(fMax / fMin, u); + phases[i] = rng() * TAU; + } + const norm = 1 / Math.sqrt(nVoices); + write(t, (lt) => { + let env = Math.min(1, lt / atk); + if (lt > dur - rel) env *= Math.max(0, (dur - lt) / rel); + const breath = (1 - breathDepth) + breathDepth * Math.sin(TAU * breathRate * lt); + let x = 0; + for (let i = 0; i < nVoices; i++) x += Math.sin(TAU * freqs[i] * lt + phases[i]); + x = Math.tanh(x * norm * 1.1); + const v = x * env * breath * gain; + return [v * 0.92, v * 1.00]; // slight pan offset → stereo hiss + }, dur + rel); +} + // Sub — low sine at the chord root, rides the duck (the pump). function sub(t, dur, midi, gain = 0.5) { const f = m2f(midi - 12); @@ -258,43 +307,78 @@ for (const sec of PLAN) { sectionRanges.push({ name: sec.name, startBar: bar, endBar: bar + sec.bars, startSec, endSec: (bar + sec.bars) * SPBAR }); - // chords / sub / pad + // chords / sub / pad — more sine voices for body. Each chord tone gets + // an 8-partial pad voice (was 5); we add a high octave-up sparkle pad + // (5 partials) for shimmer, and a low octave-down body voice on root + // only. Total pad sine count per chord ≈ 4 voices × 8 partials = 32 + // sines, plus 5 sparkle partials + 5 body partials = ~42 sines/chord. for (let b = 0; b < sec.bars; b++) { const tBar = (bar + b) * SPBAR; const ch = CHORD[cyc(sec.chords, b)]; const root = ch.root + tr; - // pad = sine triad (strings), rides the duck + const padGain = sec.name === "overture" || sec.name === "coda" ? 0.085 : 0.072; + // pad = sine triad with mid-weight partials (no upper-mid sizzle — + // dropped partials 7+8, tamed 5+6 to keep the wash smooth, not buzzy) for (const semi of ch.q) { - voice(tBar + hum(0.004), SPBAR * 0.98, root + semi + 12, - sec.name === "overture" || sec.name === "coda" ? 0.10 : 0.085, + voice(tBar + hum(0.004), SPBAR * 0.98, root + semi + 12, padGain, { atk: 0.35, rel: 0.4, vibD: 0.003, - parts: [[1, 1], [2, 0.4], [3, 0.22], [4, 0.12], [5, 0.07]] }); + parts: [[1, 1], [2, 0.45], [3, 0.22], [4, 0.12], [5, 0.06]] }); + } + // high sparkle pad — octave up, much quieter + fewer partials so it + // doesn't pile into the 2-4 kHz buzz band + if (sec.name !== "overture") { + voice(tBar + hum(0.005), SPBAR * 0.98, root + 24, padGain * 0.22, + { atk: 0.65, rel: 0.65, vibR: 4.2, vibD: 0.004, + parts: [[1, 1], [2, 0.25], [3, 0.10]] }); } + // body voice — low root, fattens the chord under the sub (warmth) + voice(tBar + hum(0.003), SPBAR * 0.98, root, padGain * 0.55, + { atk: 0.25, rel: 0.35, parts: [[1, 1], [2, 0.35], [3, 0.14], [4, 0.06]] }); if (sec.kick !== "none" || sec.name === "coda") sub(tBar, SPBAR * 0.99, root, sec.name === "bridge" ? 0.42 : 0.52); } - // kick + perc grid + // steam-release: long breath voice spanning the whole section. Quiet + // backbone hiss + an extra crescendo blast across the LAST 4 bars + // into the next section (the "release"). Coda gets a long dissolve. + const secDur = sec.bars * SPBAR; + steam(startSec, secDur, + sec.name === "bridge" ? 0.030 : sec.name === "overture" ? 0.040 : 0.022, + { atk: sec.name === "overture" ? 2.4 : 1.4, rel: 1.8 }); + if (sec.name !== "coda") { + steam(startSec + Math.max(0, secDur - 4 * SPBAR), 4 * SPBAR + 0.5, + sec.name === "develop" ? 0.065 : 0.045, + { atk: 3.0, rel: 0.6, breathRate: 0.8, breathDepth: 0.45 }); + } else { + // coda: long final steam dissipation under the tail + steam(startSec + 6 * SPBAR, 10 * SPBAR + TAIL, 0.035, + { atk: 3.5, rel: 4.5, breathRate: 0.35, breathDepth: 0.55 }); + } + + // kick + perc grid — HALFTIME by default (beats 1+3 only) so each hit + // lands like a hole with space around it. Climax adds a sparse ghost + // on the "and-of-4" of every other bar; that's the only density bump. for (let b = 0; b < sec.bars; b++) { const tBar = (bar + b) * SPBAR; const dr = sec.drive; const lastBar = b === sec.bars - 1; - if (sec.kick === "four" || sec.kick === "hard") { - for (let beat = 0; beat < 4; beat++) { + if (sec.kick === "halftime" || sec.kick === "halfhard") { + for (const beat of [0, 2]) { // beats 1 + 3 const tk = tBar + beat * SPB + hum(0.003); kick(tk, dr, 1); kickEvents.push({ t: +tk.toFixed(4) }); - if (sec.kick === "hard" && beat === 3) { // climax: offbeat double - kick(tk + SPB * 0.5, dr * 0.9, 0.9); kickEvents.push({ t: +(tk + SPB * 0.5).toFixed(4) }); - } } - if (lastBar && (sec.name === "develop")) { // 16th roll into climax - for (let r = 0; r < 8; r++) { - const tk = tBar + 3 * SPB + r * (SPB / 2); - kick(tk, dr * (0.7 + r * 0.05), 0.85); + if (sec.kick === "halfhard" && b % 2 === 1) { // climax: ghost on every other bar + const tk = tBar + 3.5 * SPB + hum(0.003); + kick(tk, dr * 0.7, 0.7); kickEvents.push({ t: +tk.toFixed(4) }); + } + if (lastBar && sec.name === "develop") { // pre-climax: 4 kicks across the last bar (was 16th roll) + for (let r = 0; r < 4; r++) { + const tk = tBar + r * SPB; + kick(tk, dr * (0.65 + r * 0.10), 0.85); kickEvents.push({ t: +tk.toFixed(4) }); } } - } else if (sec.kick === "pulse") { // study zone: half-time + } else if (sec.kick === "pulse") { // study zone: halftime (same as default now) for (const beat of [0, 2]) { const tk = tBar + beat * SPB; kick(tk, dr, 0.85); kickEvents.push({ t: +tk.toFixed(4) }); @@ -303,13 +387,13 @@ for (const sec of PLAN) { if (b < 4) { const tk = tBar; kick(tk, dr * (1 - b / 5), 0.8); kickEvents.push({ t: +tk.toFixed(4) }); } } - // perc tick — offbeat 8ths; sparser in the study bridge + // perc tick — also widened: 8th-positions 1 + 5 only (the "and-of-1" and "and-of-3") + // so the hi-hats mirror the halftime kicks instead of fighting them. if (sec.kick !== "none" && sec.name !== "coda") { - const every = sec.name === "bridge" ? 2 : 1; - for (let s8 = 1; s8 < 8; s8 += 2 * every) { - tick(tBar + s8 * (SPB / 2), sec.name === "bridge" ? 0.16 : 0.24); - } - tick(tBar + 3.5 * SPB, 0.26, true); // metallic open hat + const bridgeSparse = sec.name === "bridge"; + tick(tBar + 1 * (SPB / 2), bridgeSparse ? 0.14 : 0.20); // and-of-1 + tick(tBar + 5 * (SPB / 2), bridgeSparse ? 0.14 : 0.20); // and-of-3 + if (!bridgeSparse) tick(tBar + 3.5 * SPB, 0.22, true); // metallic open hat snareEvents.push({ t: +(tBar + SPB).toFixed(4) }); snareEvents.push({ t: +(tBar + 3 * SPB).toFixed(4) }); } diff --git a/pop/lib/menu.mjs b/pop/lib/menu.mjs new file mode 100644 index 0000000000..7d9dd2aeac --- /dev/null +++ b/pop/lib/menu.mjs @@ -0,0 +1,173 @@ +// menu.mjs — the pop menu. Single declarative registry of every +// instrument, fx, percussion voice, bed, vocal pipeline, scale, and form +// that lives in /pop. Adding an entry here makes it pickable from the +// `pop play` CLI and any future track scaffold. +// +// Categories: +// instruments — tonal voices. signature: mixEventX(ev, out, opts) +// where ev = { startSec, midi, durSec, gain } +// fx — buffer transforms. signature: applyX(buf, opts) +// (mutates buf in place; supports startSec/endSec opts) +// percussion — AC percussion kit (one-shot triggers) +// beds — continuous textures (still inline per-lane) +// master — bus/master chain stages (reverb, sidechain, softclip) +// pitch_time — offline pitch/time transforms (autotune, rubberband) +// vocal — TTS + alignment + per-word placement pipelines +// scales — interval sets keyed by tonic +// forms — arrangement templates (trance-bbd, chorale-16, …) +// +// The CLI loader only handles `instruments` + `fx` today; the rest is +// here so the menu stays the single source of truth as we wire more. + +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const POP = resolve(HERE, ".."); + +export const MENU = { + instruments: { + supersaw: { + file: "dance/synths/supersaw.mjs", export: "mixEventSupersaw", + presets: ["lead", "pad", "stab"], lane: "dance", + params: ["voices", "detuneCents", "attack", "decay"], + blurb: "JP-8000 detuned-saw stack (7 voices)", + }, + sinepower: { + file: "dance/synths/sinepower.mjs", export: "mixEventSinePower", + presets: ["lead", "pad", "stab"], lane: "dance", + params: ["partials", "attack", "decay"], + blurb: "stacked harmonic sines, no aliasing", + }, + skrill: { + file: "dance/synths/skrill.mjs", export: "mixEventSkrill", + presets: ["growl", "talk", "reese", "screech", "sub"], lane: "dance", + params: ["modRatio", "fmIndex", "vowelA", "vowelB", "lfo", "q", "drive", "crush", "subGain"], + blurb: "FM talking-bass w/ swept formants", + }, + hoover: { + file: "hippyhayzard/synths/hoover.mjs", export: "mixEventHoover", + presets: ["whoop", "stab", "hazard", "pad"], lane: "hippyhayzard", + params: ["voices", "detune", "whoop", "cutoff", "q", "drive", "subGain"], + blurb: "Alpha-Juno 'mentasm' w/ pitch whoop", + }, + zitar: { + file: "hippyhayzard/synths/zitar.mjs", export: "mixEventZitar", + presets: ["sitar", "lead", "drone", "dry"], lane: "hippyhayzard", + params: ["stretch", "damp", "jawari", "taraf", "symGain", "chikari"], + blurb: "Karplus-Strong sitar + jawari buzz", + }, + }, + + fx: { + wobble: { + file: "dance/synths/fx.mjs", export: "applyWobble", + params: ["target", "rate", "depth", "baseCutoffHz", "waveform"], + blurb: "LFO on amp or filter (trance/dubstep wub)", + }, + bitcrush: { + file: "dance/synths/fx.mjs", export: "applyBitcrush", + params: ["bits", "downsample", "mix"], + blurb: "bit-depth + sample-rate reduction", + }, + flange: { + file: "dance/synths/fx.mjs", export: "applyFlange", + params: ["rate", "depthMs", "baseDelayMs", "feedback", "mix"], + blurb: "LFO-swept delay line w/ feedback", + }, + }, + + // ── declared but not yet wired into the play CLI ───────────────────── + + percussion: { + note: "AC percussion kit — `playPercussion(sound, letter, opts)`. Lives in system/public/aesthetic.computer/lib/percussion.mjs.", + voices: ["kick", "snare", "clap", "snap", "hat-c", "hat-o", "ride", "crash", "splash", "cowbell", "block", "tambo"], + }, + + beds: { + ocean: { lane: "chillwave", blurb: "pink-noise LP w/ slow 8s wave LFO", inline: "chillwave/bin/render.mjs" }, + "noise-sweep": { lane: "chillwave", blurb: "very slow white-noise filter sweep", inline: "chillwave/bin/render.mjs" }, + "sacred-drone": { lane: "big-pictures", blurb: "3-voice human-hum drone w/ breath LFO", inline: "big-pictures/bin/sacred-drone.mjs" }, + "cool-sine": { lane: "big-pictures", blurb: "sub + pad + sparkle sine layer", inline: "big-pictures/bin/cool-sine-layer.mjs" }, + sinebells: { lane: "shared", blurb: "struck bell partials, T60 decay", inline: "bin/melody-bells.mjs" }, + }, + + master: { + "stereo-reverb": { lane: "jungle", blurb: "Freeverb-lite (comb + allpass)", inline: "jungle/bin/render.mjs" }, + sidechain: { blurb: "kick-triggered duck on sub/pad", inline: "per-lane (implicit envelope gate)" }, + softclip: { file: "dance/synths/fx.mjs", export: "softClip", params: ["drive"], blurb: "tanh saturation glue" }, + }, + + pitch_time: { + autotune: { file: "bin/autotune.py", blurb: "WORLD f0 snap (note/frame)", params: ["key", "scale", "strength", "preserve", "glide-ms"] }, + "score-pitch": { file: "bin/score-pitch.mjs", blurb: "MIDI-driven rubberband pitch shift" }, + "score-stretch": { file: "bin/score-stretch.mjs", blurb: "rubberband time-fit (no pitch shift)" }, + "pitchsnap-world": { file: "bin/pitchsnap_world.py", blurb: "WORLD pitch correction variant" }, + }, + + vocal: { + "jeffrey-pvc": { file: "bin/say.mjs", blurb: "ElevenLabs jeffrey-pvc TTS (stability ≥ 0.5)", params: ["speed", "style", "stability", "similarity"] }, + "score-render": { file: "bin/score-render.mjs", blurb: "per-word vocal placement onto a .np score" }, + "mfa-align": { file: "bin/mfa-align.mjs", blurb: "Montreal Forced Aligner word/phoneme timing" }, + "sacred-hum": { file: "big-pictures/bin/sacred-drone.mjs", blurb: "3-voice non-vocal human-hum drone" }, + }, + + scales: { + minor: [0, 2, 3, 5, 7, 8, 10], + major: [0, 2, 4, 5, 7, 9, 11], + "minor-pent": [0, 3, 5, 7, 10], + "major-pent": [0, 2, 4, 7, 9], + dorian: [0, 2, 3, 5, 7, 9, 10], + phrygian: [0, 1, 3, 5, 7, 8, 10], + chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + }, + + forms: { + "trance-bbd": { lane: "dance", blurb: "breakdown → build → drop (138 bpm minor)" }, + "chorale-16": { lane: "hippyhayzard", blurb: "16-bar Bach chorale, borrowed iv + deceptive cadence" }, + "ballad-32": { lane: "hippyhayzard", blurb: "32-bar nightcore-ballad form (~1:28)" }, + "williams-aba": { lane: "hellsine", blurb: "ABA' heroic-leitmotif arc, climax key lift" }, + "jungle-16": { lane: "jungle", blurb: "16-bar chopped-break (jungletón / raggasol / rodando)" }, + }, + + modulation: { + "ad-env": { blurb: "linear attack → exponential decay (AC sound.synth contract)" }, + "lfo": { blurb: "sine/tri/square · free Hz or beat-synced (1/8, 1/4, 1/8t, 1/8d)" }, + "pitch-env": { blurb: "bend-down → spring-up (the hoover 'whoop')", file: "hippyhayzard/synths/hoover.mjs" }, + "breakpoint-env": { blurb: "[{time, ...params}] linear-interp ramps; honored by fx + beds", file: "dance/synths/fx.mjs" }, + "vibrato": { blurb: "sine pitch mod, rate + depth (cents)" }, + }, + + score: { + ".np": { blurb: "notepat — `NOTE:syllable*beats` cells, space-separated; sections via `verse N` / `#`" }, + "midi-to-np": { file: "bin/midi_to_np.py", blurb: "MIDI file → .np converter" }, + "musicxml-to-np": { file: "bin/musicxml_to_np.py", blurb: "MusicXML → .np converter (preferred for hymn imports)" }, + "os-to-np": { file: "bin/os_to_np.py", blurb: "Open Score → .np converter" }, + ".txt": { blurb: "paired lyric file, one line per .np section" }, + }, +}; + +async function loadByEntry(entry) { + if (!entry?.file || !entry?.export) return null; + if (!entry.file.endsWith(".mjs")) return null; + const mod = await import(resolve(POP, entry.file)); + return mod[entry.export] ?? null; +} + +export async function loadInstrument(name) { + const e = MENU.instruments[name]; + if (!e) throw new Error(`unknown instrument: ${name}`); + const fn = await loadByEntry(e); + if (!fn) throw new Error(`instrument ${name} not loadable from ${e.file}:${e.export}`); + return fn; +} + +export async function loadFx(name) { + const e = MENU.fx[name] || MENU.master[name]; + if (!e) throw new Error(`unknown fx: ${name}`); + const fn = await loadByEntry(e); + if (!fn) throw new Error(`fx ${name} not loadable from ${e.file}:${e.export}`); + // softClip has signature (buf, drive); wrap to the (buf, opts) contract. + if (name === "softclip") return (buf, opts = {}) => fn(buf, opts.drive ?? 1.0); + return fn; +}