adjust speed with slider, or R to toggle run/pause, S to step while paused
xxxxxxxxxx
/*
engineer: jWilliam Dunn 2023.0825
how a Harley-Davidson engine work?
the following sketch is a study in the
geometry of the 45-degree v-twin engine
and its distinctive rhythmic sound
background: en.wikipedia.org/wiki/V-twin_engine
and en.wikipedia.org/wiki/Four-stroke_engine
reference: youtu.be/Qjrd8aaKTbQ and youtu.be/8dAbcbAJRw8?t=189
see also: patents.google.com/patent/US9856817B2
see also: en.wikipedia.org/wiki/Harley-Davidson
"The first cylinder fires, the second (rear) cylinder
fires 315° later, then there is a 405° gap until the
first cylinder fires again, giving the engine its
unique sound."
note: this sketch does not include the cylinder heads,
valves, air chambers, cams, pushrods, cooling fins, etc.
an image of an M8 is sized/positioned accurately in
the background and validated against the reverse side.
the sound heard from an engine is the exhaust exiting
each cylinder when the valve opens and releases the
intensely compressed gases following the combustion
the sound sample for this sketch is from a 720° engine
cycle at around 01:37 of this video: youtu.be/S4vHterh_Co
it sounds about right when the sketch speed is set to
approx 70 and synth set to 0
the synthetic sound is a mix of noise fadeout to simulate
the exhaust pulse, combined with a 81 Hz tone for the left
and 101 Hz for the right to emulate the shortshot staggered
exhaust pipes (the frequencies extracted from several
spectrum analysis results)
a blue rectangle represents an opened intake valve
an orange rectangle represents an opened exhaust valve
Note: p5.sound v1.4.0 - v1.7.0 produce a glitchy
distortion in sound after a minute, due to an open
issue in p5.js: github.com/processing/p5.js-sound/issues/635
...as a workaround, this sketch uses the older v0.6.0
which runs about five minutes before glitching
adjust speed with slider, or R to toggle run/pause, S to step while paused
observations
1. flywheel mass alone pushes each cylinder through compression
2. the longer exhaust period of 405 degrees is produced by the left (rear) cylinder
next level:
github.com/Engine-Simulator
*/
let p1, p2, phase, vol=0.5, soft=0.5, run=false, scl=1,
exhaustsoundL1, exhaustsoundR1, bg1, bg2,
exhaustsoundL2, exhaustsoundR2,
x=0, y=20, f, rpm=0,
// angle of the cylinders
// [range from 45 to 90. Most Harley's have a 45-degree angle
// Harley V-Rod angle is 60 on a common crankpin while
// Revolution Max angle is 60 with a 30 degree crankpin offset
// example: youtu.be/6Gqm3KO15LE?t=281 ]
vangle=45,
// crankpin offset [most Harleys share the same crankpin]
/* [Honda Shadow VT750C 45° V-twin had a vibration-canceling
offset dual-pin crankshaft at 90°. exhaust sound is more
unequal, firing at 225° and 495° intervals.
en.wikipedia.org/wiki/Honda_Shadow#VT750C_45%C2%B0_V-twin
example: youtube.com/watch?v=7TUyL18136E ] */
cpoffset=0, // [range from 0 to 90]
// ratio of bore to stroke is determined by the 2023 H-D Breakout engine
// 4.075in bore and a 4.5in stroke, thus bore/stroke=0.90556
strok=29*4, bore=strok*0.90556,
toRad = Math.PI/180;
debug=false;
let spinX = (t)=> strok*Math.cos(t*toRad)/2,
spinY = (t)=> strok*Math.sin(t*toRad)/2;
//assuming frameRate of 120,
//speed is the number of degrees of rotation per frame
OPC.slider('speed', 1, 1,100,1); // 100 degrees per frame = 2000 rpm
OPC.slider('synth', 50, 0,100,1); // amount of sampled vs synthetic exhaust
OPC.slider('dimension', 21, 1,100,1); // scale 0.8 to 1.8
OPC.slider('visibility', 110, 0,255,1); // alpha of background image
OPC.toggle('photo', false); // photo vs line
OPC.button('run_stop', "run/stop");
OPC.button('step', "step");
function parameterChanged(variableName, newValue) {
// undefined
//console.log(frameRate());
if (variableName == 'synth') {
vol = newValue/100;
exhaustsoundL1.amp((1-vol)*soft); // soften the sampled exhaust
exhaustsoundR1.amp((1-vol)*soft);
exhaustsoundL2.amp(vol);
exhaustsoundR2.amp(vol);
}
if (variableName == 'dimension') {
scl = map(newValue, 1,100, 0.8,1.8);
}
}
function buttonReleased(variableName) {
if (variableName == 'run_stop') {
run = !run;
}
if (variableName == 'step') {
if(!run){p1+=speed;p2+=speed;}
}
}
function preload() {
f = loadFont('https://cdnjs.cloudflare.com/ajax/libs/ink/3.1.10/fonts/Roboto/roboto-regular-webfont.ttf');
soundFormats('mp3');
exhaustsoundL1 = loadSound('hdexhaustL3.mp3');
exhaustsoundR1 = loadSound('hdexhaustR3.mp3');
exhaustsoundL2 = loadSound('exhaustSynthL.mp3');
exhaustsoundR2 = loadSound('exhaustSynthR.mp3');
bg1 = loadImage("m8.jpg");
bg2 = loadImage("tc.jpg");
}
function setup() {
createCanvas(windowWidth, windowHeight);
pixelDensity(2);
rectMode(CENTER);
angleMode(DEGREES);
//outputVolume(vol); // not supported in p5.sound 1.3.0
exhaustsoundL1.amp((1-vol)*soft); // soften the sampled exhaust
exhaustsoundR1.amp((1-vol)*soft);
exhaustsoundL1.playMode('restart');
exhaustsoundR1.playMode('restart');
exhaustsoundL1.pan(-0.67);
exhaustsoundR1.pan(0.67);
exhaustsoundL2.amp(vol);
exhaustsoundR2.amp(vol);
exhaustsoundL2.playMode('restart');
exhaustsoundR2.playMode('restart');
exhaustsoundL2.pan(-0.67);
exhaustsoundR2.pan(0.67);
phase=[4,1]; // 4,2
p1 = 562; // 720
p2 = p1-vangle-360-cpoffset; // offset the phases
frameRate(120);
textFont(f);
textSize(16);
x=width/2 - 200;
}
function draw() {
background(0);
if(debug) {
if(frameCount%50==0)rpm=(frameRate()*speed/6+rpm)/2;
// Render the background box for the HUD
noStroke();
fill(50,50,52, 200); // add some transparency
rect(x+20+200,y+15, 400,70);
// Render the labels
fill(69,161,255);
text("piston 1:",x+35,y); text("piston 2:",x+235,y);
text("angle p1:",x+35,y+20); text("angle p2:",x+235,y+20);
text("rpm:",x+35,y+40);
// Render the data
fill(69,161,255);
let p="";
if(phase[0]==1)p="intake";
if(phase[0]==2)p="compress";
if(phase[0]==3)p="power";
if(phase[0]==4)p="exhaust";
text(p,x+105,y);
if(phase[1]==1)p="intake";
if(phase[1]==2)p="compress";
if(phase[1]==3)p="power";
if(phase[1]==4)p="exhaust";
text(p,x+305,y);
text(p1, x+105,y+20);
text(p2, x+305,y+20);
text(round(run?rpm:0), x+105,y+40);
}
translate(width/2,height/2+150);
rotate(-90-vangle/2);
scale(scl);
// render the background engine image
if(vangle===45) {
push();
rotate(90+vangle/2);
scale(0.87); // 0.855
tint(255, visibility);
image(photo?bg1:bg2, -381,-555); //-383,-564
pop();
}
strokeCap(ROUND);
fill(0);
stroke(160);
//////////////////////////////////////////////////////////////////
// cylinder 1 (left)
//////////////////////////////////////////////////////////////////
strokeWeight(2);
stroke(80);
line(37*4,-bore/2-3, 80*4,-bore/2-3);
line(37*4,bore/2+3, 80*4,bore/2+3);
line(80*4,bore/2+3, 80*4,-bore/2-3);
strokeWeight(24);
stroke(80);
line(spinX(p1),spinY(p1), 55*4+spinX(p1),0); //rod 1
stroke(160);
strokeWeight(4);
strokeCap(SQUARE);
//noFill();
rect(55*4+spinX(p1),0, 13*4,bore); //piston 1
stroke(160);
noFill();
strokeWeight(0.125*4); // piston 1 pin
ellipse(55*4+spinX(p1),0, 6*4,6*4);
strokeWeight(4);
fill(0);
//////////////////////////////////////////////////////////////////
// cylinder 2 (right)
//////////////////////////////////////////////////////////////////
rotate(vangle);
strokeCap(ROUND);
strokeWeight(2);
stroke(80);
line(37*4,-bore/2-3, 80*4,-bore/2-3);
line(37*4,bore/2+3, 80*4,bore/2+3);
line(80*4,bore/2+3, 80*4,-bore/2-3);
strokeWeight(24);
stroke(80);
line(spinX(p2),spinY(p2), 55*4+spinX(p2),0); //rod 2
stroke(160);
strokeWeight(4);
strokeCap(SQUARE);
rect(55*4+spinX(p2),0, 13*4,bore); //piston 2
stroke(160);
noFill();
strokeWeight(0.125*4); // piston 2 pin
ellipse(55*4+spinX(p2),0, 6*4,6*4);
//////////////////////////////////////////////////////////////////
// flywheel
//////////////////////////////////////////////////////////////////
// the diameter of the flywheel in a stock Harley-Davidson
// Milwaukee Eight engine is 8 3/8 inches
// ratio=1.861111 X strok = 54
stroke(80);
strokeWeight(2);
fill(0, 240);
ellipse(0,0, 54*4,54*4);
noFill();
strokeWeight(0.125*4);
stroke(160);
//fill(160);
ellipse(spinX(p2),spinY(p2), 9*4,9*4); // crankpin
push();
rotate(-vangle);
noStroke();
fill(80,0,0);
rotate(p1);
rect(-23.5*4,0, 6*4,4); // red counter-weight marker
pop();
//fill(0);
//stroke(160);
//////////////////////////////////////////////////////////////////
// phase management
//////////////////////////////////////////////////////////////////
/* standard four-stroke cycle is two revolutions
phase 1: intake 0 to 180 degrees
phase 2: compression 180 to 360
phase 3: combustion 360 to 540
phase 4: exhaust 540 to 720
*/
if(run)p1+=speed;
if(p1>720) {
p1 -= 720;
phase[0] = 1; // reset phase of piston 1
}
// within the intake phase (1)
if(p1>=0 && p1<180) {
push(); // render intake valve
rotate(-vangle);
noStroke();
fill(0,80,190);
translate(80*4,9*4);
rect(0,0, 4*4,8*4);
pop();
}
// within the compression phase (2)
if(p1>=180 && p1<360) {
if(phase[0]==1)
phase[0]=2;// transition to compression 1 -> 2
if(phase[0]==2) { //compress
push(); //render the compression stroke
rotate(-vangle);
noStroke();
fill(0,0,180, (15*4+spinX(p1))*2);
translate(71*4+spinX(p1)/2,0);
rect(-0.5,0, 17*4-spinX(p1)+2.5,bore+3.5);
pop();
}
}
// within the combustion phase (3)
if(p1>=360 && p1<540) {
if(phase[0]==3) { //combustion
push(); //render the power stroke
rotate(-vangle);
noStroke();
fill(180,0,0, 255-(15*4-spinX(p1))*2);
translate(71*4+spinX(p1)/2,0);
rect(-0.5,0, 17*4-spinX(p1)+2.5,bore+3.5);
pop();
}
if(phase[0]==2) { // transition phase 2 -> 3
phase[0]=3;//ignition
push(); // render spark on top of the power stroke (one frame)
rotate(-vangle);
noStroke();
fill(190,190,0);
translate(79*4,0);
rotate(45);
rect(0,0, 4*4,4*4);
pop();
}
}
// within the exhaust phase (4)
if(p1>=540 && p1<720) {
if(phase[0]==3) {
phase[0]=4; // transition 3 -> 4
exhaustsoundL1.play(); // trigger the exhaust sound
exhaustsoundL2.play();
}
push(); // render exhaust valve
rotate(-vangle);
noStroke();
fill(190,80,0);
translate(80*4,-9*4);
rect(0,0, 4*4,8*4);
pop();
}
if(run)p2+=speed;
if(p2>720) {
p2 -= 720;
phase[1] = 1; // reset phase of piston 2
}
if(p2>=0 && p2<180) {
push(); //intake valve
noStroke();
fill(0,80,190);
translate(80*4,-9*4);
rect(0,0, 4*4,8*4);
pop();
}
if(p2>=180 && p2<360) {
if(phase[1]==1)
phase[1]=2;// transition to compression
if(phase[1]==2) { //compress
push(); //render the compression stroke
noStroke();
fill(0,0,180, (15*4+spinX(p2))*2);
translate(71*4+spinX(p2)/2,0);
rect(-0.5,0, 17*4-spinX(p2)+2.5,bore+3.5);
pop();
}
}
if(p2>=360 && p2<540) {
if(phase[1]==3) { //combustion
push(); //power
noStroke();
fill(180,0,0, 255-(15*4-spinX(p2))*2);
translate(71*4+spinX(p2)/2,0);
rect(-0.5,0, 17*4-spinX(p2)+2.5,bore+3.5);
pop();
}
if(phase[1]==2) {
phase[1]=3;//ignition
push(); // render spark
noStroke();
fill(190,190,0);
translate(79*4,0);
rotate(45);
rect(0,0, 4*4,4*4);
pop();
}
}
if(p2>=540 && p2<720) {
if(phase[1]==3) {
phase[1]=4;//exhaust sound
exhaustsoundR1.play();
exhaustsoundR2.play();
}
push(); //exhaust valve
noStroke();
fill(190,80,0);
translate(80*4,9*4);
rect(0,0, 4*4,8*4);
pop();
}
}
function keyPressed() {
if(key=="r")run=!run;
if(key=="s" && !run){p1+=speed;p2+=speed;}
}
// starting code forked from sketch/382576