switch to pcm driven stanza selection

This commit is contained in:
Boaz Sender 2025-04-21 11:38:42 -07:00
parent 5b2e71c317
commit d9400fdb64
9 changed files with 1699 additions and 1503 deletions

View file

@ -1,21 +1,52 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import LCD from "raspberrypi-liquid-crystal"; import LCD from "raspberrypi-liquid-crystal";
const prisma = new PrismaClient(); import fs from "node:fs";
const TICK = 250; import * as WavFileDecoder from "wav-file-decoder";
const WAIT = 10;
const lcd = new LCD(1, 0x27, 16, 2);
lcd.beginSync();
lcd.clearSync();
const timer = (ms) => new Promise((res) => setTimeout(res, ms)); const timer = (ms) => new Promise((res) => setTimeout(res, ms));
const getStanza = async () => { const prisma = new PrismaClient();
const stanzaCount = await prisma.stanza.count(); const getStanza = async (register) => {
const stanzaCount = await prisma.stanza.count({ where: { register } });
const skip = Math.floor(Math.random() * stanzaCount); const skip = Math.floor(Math.random() * stanzaCount);
const randomStanza = await prisma.stanza.findMany({ const randomStanza = await prisma.stanza.findMany({
skip: skip, skip: skip,
take: 1, take: 1,
where: {
register,
},
}); });
return randomStanza[0].text; return randomStanza[0].text;
}; };
// Play portal wave PCM data and set bands
const fileData = fs.readFileSync("/home/grace/blackportal1000.wav");
const wavFileInfo = WavFileDecoder.getWavFileInfo(fileData);
const audioData = WavFileDecoder.decodeWavFile(fileData);
const totalSamples = wavFileInfo.chunkInfo.filter((ci) => ci.chunkId === "data")[0].dataLength;
let sample = 0;
let register = "high";
let count = 0;
setInterval(() => {
const absSample = Math.abs(audioData.channelData[0][count]);
if (count > totalSamples) {
count = 0;
}
else {
count++;
}
register = "high";
if (absSample < 0.01) {
register = "mid";
}
if (absSample < 0.001) {
register = "low";
}
sample = absSample;
}, 1);
const lcd = new LCD(1, 0x27, 16, 2);
lcd.beginSync();
lcd.clearSync();
const TICK = 250;
const WAIT = 10;
// Play bottom line
const tag = " Black Portal 4856 E Davison, Detroit "; const tag = " Black Portal 4856 E Davison, Detroit ";
let tagCharacterLocation = 0; let tagCharacterLocation = 0;
setInterval(() => { setInterval(() => {
@ -28,25 +59,27 @@ setInterval(() => {
tagCharacterLocation = 0; tagCharacterLocation = 0;
} }
}, TICK); }, TICK);
while (true) { // Play top line
let stanza = await getStanza(); const playNewStanza = async () => {
let stanzaOuterInterval; let stanza = await getStanza(register);
let stanzaInnerInterval; let stanzaWaitTimeout;
let stanzaCharacterLocation = 0; let stanzaCharacterLocation = 0;
stanzaOuterInterval = setInterval(async () => { const scrollStanza = async () => {
lcd.printLineSync(0, " "); lcd.printLineSync(0, " ");
stanzaInnerInterval = setTimeout(() => { stanzaWaitTimeout = setTimeout(() => {
lcd.printLineSync(0, `${16 - stanzaCharacterLocation > 0 lcd.printLineSync(0, `${16 - stanzaCharacterLocation > 0
? Array(16 - stanzaCharacterLocation).join(" ") ? Array(16 - stanzaCharacterLocation).join(" ")
: ""}${stanza}`.substring(stanzaCharacterLocation, 16 + stanzaCharacterLocation)); : ""}${stanza}`.substring(stanzaCharacterLocation, stanza.length));
}, WAIT); }, WAIT);
await timer(TICK);
stanzaCharacterLocation++; stanzaCharacterLocation++;
if (stanzaCharacterLocation > stanza.length) { if (stanzaCharacterLocation === stanza.length) {
stanza = await getStanza(); playNewStanza();
stanzaCharacterLocation = 0;
} }
}, TICK); else {
await timer((16 + stanza.length) * (WAIT + TICK)); scrollStanza();
clearInterval(stanzaOuterInterval);
clearInterval(stanzaInnerInterval);
} }
};
scrollStanza();
};
playNewStanza();

111
buildingsound.js Normal file
View file

@ -0,0 +1,111 @@
import fs from "node:fs";
import exec from "node:child_process";
import _ from "underscore";
import PCMPlayer from 'pcm-player'
import pcm from "pcm";
var min = 1.0;
var max = -1.0;
const pcmData = []
await pcm.getPcmData('/home/boazsender/Downloads/BlackPortal recording April 6 2025.wav', { stereo: true, sampleRate: 44100 },
function(sample, channel) {
// Sample is from [-1.0...1.0], channel is 0 for left and 1 for right
pcmData.push(sample)
min = Math.min(min, sample);
max = Math.max(max, sample);
},
function(err, output) {
if (err)
throw new Error(err);
console.log('min=' + min + ', max=' + max);
}
);
console.log(pcmData)
/**
* [findPeaks Naive algo to identify peaks in the audio data, and wave]
* @param {[type]} pcmdata [description]
* @param {[type]} samplerate [description]
* @return {[type]} [description]
*/
function findPeaks(pcmdata, samplerate) {
const interval = 0.05 * 1000;
index = 0;
const step = Math.round(samplerate * (interval / 1000));
const max = 0;
const prevmax = 0;
const prevdiffthreshold = 0.3;
//loop through song in time with sample rate
const samplesound = setInterval(
function () {
if (index >= pcmdata.length) {
clearInterval(samplesound);
console.log("finished sampling sound");
return;
}
for (const i = index; i < index + step; i++) {
max = pcmdata[i] > max ? pcmdata[i].toFixed(1) : max;
}
// Spot a significant increase? Potential peak
bars = getbars(max);
if (max - prevmax >= prevdiffthreshold) {
bars = bars + " == peak == ";
}
// Print out mini equalizer on commandline
console.log(bars, max);
prevmax = max;
max = 0;
index += step;
},
interval,
pcmdata
);
}
/**
* TBD
* @return {[type]} [description]
*/
function detectBeats() {}
/**
* [getbars Visualize image sound using bars, from average pcmdata within a sample interval]
* @param {[Number]} val [the pcmdata point to be visualized ]
* @return {[string]} [a set of bars as string]
*/
function getbars(val) {
bars = "";
for (const i = 0; i < val * 50 + 2; i++) {
bars = bars + "|";
}
return bars;
}
/**
* [Plays a sound file]
* @param {[string]} soundfile [file to be played]
* @return {[type]} [void]
*/
function playsound(soundfile) {
// linux or raspi
// const create_audio = exec('aplay'+soundfile, {maxBuffer: 1024 * 500}, function (error, stdout, stderr) {
const create_audio = exec(
"mplayer -loop 0 " + soundfile,
{ maxBuffer: 1024 * 500 },
function (error, stdout, stderr) {
if (error !== null) {
console.log("exec error: " + error);
} else {
//console.log(" finshed ");
//micInstance.resume();
}
}
);
}

9
package-lock.json generated
View file

@ -10,7 +10,8 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@prisma/client": "^6.5.0", "@prisma/client": "^6.5.0",
"raspberrypi-liquid-crystal": "^1.20.0" "raspberrypi-liquid-crystal": "^1.20.0",
"wav-file-decoder": "^1.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
@ -826,6 +827,12 @@
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
},
"node_modules/wav-file-decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/wav-file-decoder/-/wav-file-decoder-1.0.3.tgz",
"integrity": "sha512-j+yFh6Ux5zMjYmhhRyP2Q574Bc1VwuvJC2SlFPz2BqoEJ6FlNjvJ1XlDiy10NBbEQW+a+3jvUfT5AOBhR4Eavg==",
"license": "MIT"
} }
} }
} }

View file

@ -18,7 +18,8 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@prisma/client": "^6.5.0", "@prisma/client": "^6.5.0",
"raspberrypi-liquid-crystal": "^1.20.0" "raspberrypi-liquid-crystal": "^1.20.0",
"wav-file-decoder": "^1.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
@ -29,5 +30,8 @@
}, },
"prisma": { "prisma": {
"seed": "tsx prisma/seed.ts" "seed": "tsx prisma/seed.ts"
},
"volta": {
"node": "23.11.0"
} }
} }

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -12,11 +12,13 @@ datasource db {
provider = "sqlite" provider = "sqlite"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model Stanza { model Stanza {
id String @id @default(cuid()) id String @id @default(cuid())
text String text String
songId String songId String
song Song @relation(fields: [songId], references: [id]) song Song @relation(fields: [songId], references: [id])
register String
} }
model Song { model Song {

View file

@ -3,13 +3,15 @@ import fs from "node:fs";
import { parse } from "csv"; import { parse } from "csv";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
//Artist,Album,Song,Stanza // lyrics from https://docs.google.com/spreadsheets/d/1b8gANkghKpJKPzsPEigggxOydGgM8ntX0EAI4rPwbSU/edit?gid=0#gid=0
// csv header: Artist,Album,Song,Stanza
const parser = fs.createReadStream(`prisma/lyrics.csv`).pipe(parse()); const parser = fs.createReadStream(`prisma/lyrics.csv`).pipe(parse());
for await (const record of parser) { for await (const record of parser) {
console.log(record[3]); console.log(record[3]);
await prisma.stanza.create({ await prisma.stanza.create({
data: { data: {
text: record[3], text: record[3],
register: record[4],
song: { song: {
connectOrCreate: { connectOrCreate: {
where: { where: {

View file

@ -1,30 +1,68 @@
import { PrismaClient, Stanza } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import LCD from "raspberrypi-liquid-crystal"; import LCD from "raspberrypi-liquid-crystal";
const prisma = new PrismaClient(); import fs from "node:fs";
import * as WavFileDecoder from "wav-file-decoder";
const TICK = 250;
const WAIT = 10;
const lcd = new LCD(1, 0x27, 16, 2);
lcd.beginSync();
lcd.clearSync();
const timer = (ms: number) => new Promise((res) => setTimeout(res, ms)); const timer = (ms: number) => new Promise((res) => setTimeout(res, ms));
const getStanza = async () => {
const stanzaCount = await prisma.stanza.count(); const prisma = new PrismaClient();
const getStanza = async (register: string) => {
const stanzaCount = await prisma.stanza.count({ where: { register } });
const skip = Math.floor(Math.random() * stanzaCount); const skip = Math.floor(Math.random() * stanzaCount);
const randomStanza = await prisma.stanza.findMany({ const randomStanza = await prisma.stanza.findMany({
skip: skip, skip: skip,
take: 1, take: 1,
where: {
register,
},
}); });
return randomStanza[0].text as string; return randomStanza[0].text as string;
}; };
// Play portal wave PCM data and set bands
const fileData = fs.readFileSync("/home/grace/blackportal1000.wav");
const wavFileInfo = WavFileDecoder.getWavFileInfo(fileData);
const audioData = WavFileDecoder.decodeWavFile(fileData);
const totalSamples = wavFileInfo.chunkInfo.filter(
(ci) => ci.chunkId === "data"
)[0].dataLength;
let sample = 0;
let register = "high";
let count = 0;
setInterval(() => {
const absSample = Math.abs(audioData.channelData[0][count]);
if (count > totalSamples) {
count = 0;
} else {
count++;
}
register = "high";
if (absSample < 0.01) {
register = "mid";
}
if (absSample < 0.001) {
register = "low";
}
sample = absSample;
}, 1);
const lcd = new LCD(1, 0x27, 16, 2);
lcd.beginSync();
lcd.clearSync();
const TICK = 250;
const WAIT = 10;
// Play bottom line
const tag = const tag =
" Black Portal 4856 E Davison, Detroit "; " Black Portal 4856 E Davison, Detroit ";
let tagCharacterLocation = 0; let tagCharacterLocation = 0;
setInterval(() => { setInterval(() => {
lcd.printLineSync(1, " "); lcd.printLineSync(1, " ");
setTimeout(() => { setTimeout(() => {
@ -40,36 +78,35 @@ setInterval(() => {
} }
}, TICK); }, TICK);
while (true) { // Play top line
let stanza = await getStanza();
let stanzaOuterInterval; const playNewStanza = async () => {
let stanzaInnerInterval; let stanza = await getStanza(register);
let stanzaWaitTimeout;
let stanzaCharacterLocation = 0; let stanzaCharacterLocation = 0;
stanzaOuterInterval = setInterval(async () => {
const scrollStanza = async () => {
lcd.printLineSync(0, " "); lcd.printLineSync(0, " ");
stanzaInnerInterval = setTimeout(() => { stanzaWaitTimeout = setTimeout(() => {
lcd.printLineSync( lcd.printLineSync(
0, 0,
`${ `${
16 - stanzaCharacterLocation > 0 16 - stanzaCharacterLocation > 0
? Array(16 - stanzaCharacterLocation).join(" ") ? Array(16 - stanzaCharacterLocation).join(" ")
: "" : ""
}${stanza}`.substring( }${stanza}`.substring(stanzaCharacterLocation, stanza.length)
stanzaCharacterLocation,
16 + stanzaCharacterLocation
)
); );
}, WAIT); }, WAIT);
await timer(TICK);
stanzaCharacterLocation++; stanzaCharacterLocation++;
if (stanzaCharacterLocation === stanza.length) {
if (stanzaCharacterLocation > stanza.length) { playNewStanza();
stanza = await getStanza(); } else {
stanzaCharacterLocation = 0; scrollStanza();
} }
}, TICK); };
scrollStanza();
};
await timer((16 + stanza.length) * (WAIT + TICK)); playNewStanza();
clearInterval(stanzaOuterInterval);
clearInterval(stanzaInnerInterval);
}