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 LCD from "raspberrypi-liquid-crystal";
const prisma = new PrismaClient();
const TICK = 250;
const WAIT = 10;
const lcd = new LCD(1, 0x27, 16, 2);
lcd.beginSync();
lcd.clearSync();
import fs from "node:fs";
import * as WavFileDecoder from "wav-file-decoder";
const timer = (ms) => new Promise((res) => setTimeout(res, ms));
const getStanza = async () => {
const stanzaCount = await prisma.stanza.count();
const prisma = new PrismaClient();
const getStanza = async (register) => {
const stanzaCount = await prisma.stanza.count({ where: { register } });
const skip = Math.floor(Math.random() * stanzaCount);
const randomStanza = await prisma.stanza.findMany({
skip: skip,
take: 1,
where: {
register,
},
});
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 ";
let tagCharacterLocation = 0;
setInterval(() => {
@ -28,25 +59,27 @@ setInterval(() => {
tagCharacterLocation = 0;
}
}, TICK);
while (true) {
let stanza = await getStanza();
let stanzaOuterInterval;
let stanzaInnerInterval;
// Play top line
const playNewStanza = async () => {
let stanza = await getStanza(register);
let stanzaWaitTimeout;
let stanzaCharacterLocation = 0;
stanzaOuterInterval = setInterval(async () => {
const scrollStanza = async () => {
lcd.printLineSync(0, " ");
stanzaInnerInterval = setTimeout(() => {
stanzaWaitTimeout = setTimeout(() => {
lcd.printLineSync(0, `${16 - stanzaCharacterLocation > 0
? Array(16 - stanzaCharacterLocation).join(" ")
: ""}${stanza}`.substring(stanzaCharacterLocation, 16 + stanzaCharacterLocation));
: ""}${stanza}`.substring(stanzaCharacterLocation, stanza.length));
}, WAIT);
await timer(TICK);
stanzaCharacterLocation++;
if (stanzaCharacterLocation > stanza.length) {
stanza = await getStanza();
stanzaCharacterLocation = 0;
if (stanzaCharacterLocation === stanza.length) {
playNewStanza();
}
}, TICK);
await timer((16 + stanza.length) * (WAIT + TICK));
clearInterval(stanzaOuterInterval);
clearInterval(stanzaInnerInterval);
}
else {
scrollStanza();
}
};
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",
"dependencies": {
"@prisma/client": "^6.5.0",
"raspberrypi-liquid-crystal": "^1.20.0"
"raspberrypi-liquid-crystal": "^1.20.0",
"wav-file-decoder": "^1.0.3"
},
"devDependencies": {
"@types/node": "^22.13.14",
@ -826,6 +827,12 @@
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"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",
"dependencies": {
"@prisma/client": "^6.5.0",
"raspberrypi-liquid-crystal": "^1.20.0"
"raspberrypi-liquid-crystal": "^1.20.0",
"wav-file-decoder": "^1.0.3"
},
"devDependencies": {
"@types/node": "^22.13.14",
@ -29,5 +30,8 @@
},
"prisma": {
"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"
url = env("DATABASE_URL")
}
model Stanza {
id String @id @default(cuid())
text String
songId String
song Song @relation(fields: [songId], references: [id])
register String
}
model Song {

View file

@ -3,13 +3,15 @@ import fs from "node:fs";
import { parse } from "csv";
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());
for await (const record of parser) {
console.log(record[3]);
await prisma.stanza.create({
data: {
text: record[3],
register: record[4],
song: {
connectOrCreate: {
where: {

View file

@ -1,30 +1,68 @@
import { PrismaClient, Stanza } from "@prisma/client";
import { PrismaClient } from "@prisma/client";
import LCD from "raspberrypi-liquid-crystal";
const prisma = new PrismaClient();
const TICK = 250;
const WAIT = 10;
const lcd = new LCD(1, 0x27, 16, 2);
lcd.beginSync();
lcd.clearSync();
import fs from "node:fs";
import * as WavFileDecoder from "wav-file-decoder";
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 randomStanza = await prisma.stanza.findMany({
skip: skip,
take: 1,
where: {
register,
},
});
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 =
" Black Portal 4856 E Davison, Detroit ";
let tagCharacterLocation = 0;
setInterval(() => {
lcd.printLineSync(1, " ");
setTimeout(() => {
@ -40,36 +78,35 @@ setInterval(() => {
}
}, TICK);
while (true) {
let stanza = await getStanza();
// Play top line
let stanzaOuterInterval;
let stanzaInnerInterval;
const playNewStanza = async () => {
let stanza = await getStanza(register);
let stanzaWaitTimeout;
let stanzaCharacterLocation = 0;
stanzaOuterInterval = setInterval(async () => {
const scrollStanza = async () => {
lcd.printLineSync(0, " ");
stanzaInnerInterval = setTimeout(() => {
stanzaWaitTimeout = setTimeout(() => {
lcd.printLineSync(
0,
`${
16 - stanzaCharacterLocation > 0
? Array(16 - stanzaCharacterLocation).join(" ")
: ""
}${stanza}`.substring(
stanzaCharacterLocation,
16 + stanzaCharacterLocation
)
}${stanza}`.substring(stanzaCharacterLocation, stanza.length)
);
}, WAIT);
await timer(TICK);
stanzaCharacterLocation++;
if (stanzaCharacterLocation > stanza.length) {
stanza = await getStanza();
stanzaCharacterLocation = 0;
if (stanzaCharacterLocation === stanza.length) {
playNewStanza();
} else {
scrollStanza();
}
}, TICK);
};
scrollStanza();
};
await timer((16 + stanza.length) * (WAIT + TICK));
clearInterval(stanzaOuterInterval);
clearInterval(stanzaInnerInterval);
}
playNewStanza();