diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45cd33a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.Thumb.db + +node_modules diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fab4916 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2023-present IRCAM – Centre Pompidou (France, Paris) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the IRCAM nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..859c4f8 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Ircam | create + +[![npm version](https://badge.fury.io/js/@ircam%2Fcreate.svg)](https://badge.fury.io/js/@ircam%2Fcreate) + +Interactive command line tools for scaffolding simple Web Audio test apps and demos. + +## Usage + +```sh +npx @ircam/create@latest [dirname] +``` + +## Credits + +[https://soundworks.dev/credits.html](https://soundworks.dev/credits.html) + +## License + +[BSD-3-Clause](./LICENSE) diff --git a/create.js b/create.js new file mode 100755 index 0000000..a62bb66 --- /dev/null +++ b/create.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import * as url from 'node:url'; + +import chalk from 'chalk'; +import { mkdirp } from 'mkdirp'; +import prompts from 'prompts'; +import readdir from 'recursive-readdir'; + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); +const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'))); + +export function toValidPackageName(name) { + return name + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/^[._]/, '') + .replace(/[^a-z0-9~.-]+/g, '-'); +} + +console.log(`\ +${chalk.gray(`[@ircam/create#v${version}]`)} +`); + +let targetDir; +if (process.argv[2]) { + targetDir = process.argv[2]; +} else { + targetDir = '.'; +} + +if (targetDir === '.') { + const result = await prompts([ + { + type: 'text', + name: 'dir', + message: 'Where should we create your project?\n (leave blank to use current directory)', + }, + ]); + + if (result.dir) { + targetDir = result.dir; + } +} + +const targetWorkingDir = path.normalize(path.join(process.cwd(), targetDir)); + +if (fs.existsSync(targetWorkingDir) && fs.readdirSync(targetWorkingDir).length > 0) { + console.log(chalk.red(`> "${targetDir}" directory exists and is not empty, aborting...`)); + process.exit(1); +} + +const templateDir = path.join(__dirname, 'templates', 'simple-online'); + +const ignoreFiles = ['.DS_Store', 'Thumbs.db']; +const files = await readdir(templateDir, ignoreFiles); + +await mkdirp(targetWorkingDir); + + +console.log(''); +console.log(`> scaffolding app in:`, targetWorkingDir); + +for (let src of files) { + const file = path.relative(templateDir, src); + const dest = path.join(targetWorkingDir, file); + + await mkdirp(path.dirname(dest)); + + switch (file) { + case 'package.json': { + const pkg = JSON.parse(fs.readFileSync(src)); + pkg.name = toValidPackageName(options.name); + + fs.writeFileSync(dest, JSON.stringify(pkg, null, 2)); + break; + } + case 'README.md': + case 'index.html': + case 'main.js': { + let content = fs.readFileSync(src).toString(); + content = content.replace(/\[app_name\]/mg, targetDir); + fs.writeFileSync(dest, content); + break; + } + // just copy the file without modification + default: { + fs.copyFileSync(src, dest); + break; + } + } +} + +console.log(chalk.yellow('> your project is ready!')); + +console.log('') +console.log(chalk.yellow('> next steps:')); +let i = 1; + +const relative = path.relative(process.cwd(), targetWorkingDir); +if (relative !== '') { + console.log(` ${i++}: ${chalk.cyan(`cd ${relative}`)}`); +} + +console.log(` ${i++}: ${chalk.cyan('npx serve')}`); + +console.log('') +console.log(`- to close the dev server, press ${chalk.bold(chalk.cyan('Ctrl-C'))}`); diff --git a/package.json b/package.json new file mode 100644 index 0000000..59da8d9 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "@ircam/create", + "version": "1.0.0", + "description": "Interactive command line tools for scaffolding simple Web Audio test apps and demos.", + "authors": [ + "Benjamin Matuszewski" + ], + "license": "BSD-3-Clause", + "type": "module", + "bin": { + "create": "create.js" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/ircam-create/ircam-create" + }, + "bugs": { + "url": "https://github.com/ircam-create/ircam-create/issues" + }, + "homepage": "https://github.com/ircam-create/ircam-create", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "chalk": "^5.3.0", + "commander": "^11.1.0", + "mkdirp": "^3.0.1", + "prompts": "^2.4.2", + "recursive-readdir": "^2.2.3" + } +} diff --git a/templates/simple-online/README.md b/templates/simple-online/README.md new file mode 100644 index 0000000..d92901e --- /dev/null +++ b/templates/simple-online/README.md @@ -0,0 +1,10 @@ +# [app_name] + +Simple template for Web Audio demos and tests + +## Usage + +``` +cd path/to/dir +npx serve +``` diff --git a/templates/simple-online/assets/sample.wav b/templates/simple-online/assets/sample.wav new file mode 100755 index 0000000..aa10036 Binary files /dev/null and b/templates/simple-online/assets/sample.wav differ diff --git a/templates/simple-online/index.html b/templates/simple-online/index.html new file mode 100644 index 0000000..2e8d4e8 --- /dev/null +++ b/templates/simple-online/index.html @@ -0,0 +1,13 @@ + + + + + + [app_name] + + + + + + + diff --git a/templates/simple-online/lib/load-audio-buffer.js b/templates/simple-online/lib/load-audio-buffer.js new file mode 100644 index 0000000..d9bb88f --- /dev/null +++ b/templates/simple-online/lib/load-audio-buffer.js @@ -0,0 +1,17 @@ +const contexts = new Map(); + +export default async function loadAudioBuffer(pathname, sampleRate = 48000) { + if (!contexts.has(sampleRate)) { + const context = new OfflineAudioContext(1, 1, sampleRate); + console.log(context.sampleRate); + contexts.set(sampleRate, context); + } + + const response = await fetch(pathname); + const arrayBuffer = await response.arrayBuffer(); + + const context = contexts.get(sampleRate); + const audioBuffer = await context.decodeAudioData(arrayBuffer); + + return audioBuffer; +} diff --git a/templates/simple-online/lib/resume-audio-context.js b/templates/simple-online/lib/resume-audio-context.js new file mode 100644 index 0000000..bd47083 --- /dev/null +++ b/templates/simple-online/lib/resume-audio-context.js @@ -0,0 +1,15 @@ +import { html, render } from 'https://unpkg.com/lit-html'; + +export default async function resumeAudioContext(audioContext) { + return new Promise(resolve => { + render(html` + { + await audioContext.resume(); + resolve(); + }} + >Resume context + `, document.body); + }); +} diff --git a/templates/simple-online/main.js b/templates/simple-online/main.js new file mode 100644 index 0000000..4ba24af --- /dev/null +++ b/templates/simple-online/main.js @@ -0,0 +1,22 @@ +import { html, render } from 'https://unpkg.com/lit-html'; +import 'https://unpkg.com/@ircam/sc-components@latest'; + +import resumeAudioContext from './lib/resume-audio-context.js'; +import loadAudioBuffer from './lib/load-audio-buffer.js'; + +const audioContext = new AudioContext(); +await resumeAudioContext(audioContext); + +const buffer = await loadAudioBuffer('./assets/sample.wav', audioContext.sampleRate); + +render(html` +

[app_name]

+ { + const src = audioContext.createBufferSource(); + src.connect(audioContext.destination); + src.buffer = buffer; + src.start(); + }} + > +`, document.body); diff --git a/templates/simple-online/styles.css b/templates/simple-online/styles.css new file mode 100644 index 0000000..d98197f --- /dev/null +++ b/templates/simple-online/styles.css @@ -0,0 +1,27 @@ +:root { + --background-color: #181817; + --font-color: #ffffff; + --font-family: Consolas, monaco, monospace; + --font-size: 62.5%; // such that 1rem == 10px +} + +* { + box-sizing: border-box; + font-family: var(--font-family); +} + +html, body { + width: 100%; + min-height: 100vh; + background-color: var(--background-color); + color: var(--font-color); +} + +html { + font-size: var(--font-size); +} + +body { + padding: 20px; + margin: 0; +}