switch to pcm driven stanza selection
This commit is contained in:
parent
5b2e71c317
commit
d9400fdb64
9 changed files with 1699 additions and 1503 deletions
|
|
@ -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
111
buildingsound.js
Normal 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
9
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2868
prisma/lyrics.csv
2868
prisma/lyrics.csv
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -12,23 +12,25 @@ 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])
|
||||
id String @id @default(cuid())
|
||||
text String
|
||||
songId String
|
||||
song Song @relation(fields: [songId], references: [id])
|
||||
register String
|
||||
}
|
||||
|
||||
model Song {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
artistId String
|
||||
artist Artist @relation(fields: [artistId], references: [id])
|
||||
stanzas Stanza[]
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
artistId String
|
||||
artist Artist @relation(fields: [artistId], references: [id])
|
||||
stanzas Stanza[]
|
||||
}
|
||||
|
||||
model Artist {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
songs Song[]
|
||||
}
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
songs Song[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
97
screen.ts
97
screen.ts
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue