Parametric Acoustic Model for Native American Style Flutes

Tone Hole Calculator with Empirical Validation

Tony Koop · github.com/tonykoop/flutes · 2026

1. Introduction

What is a Native American style flute? A NAF has a block (often called a "fetish" or "bird"), a slow air chamber, a sound hole that the block directs air across, and a series of finger holes along the bore. The bore acts as the resonator that determines the fundamental pitch.

Why parametric. I've built 150+ instruments across the F4 to E5 range over 17 years. The recurring design problem is: given a target scale, what bore length, bore diameter, and tone-hole positions and sizes will produce it? This notebook makes that question quantitative.

Scope of this notebook. Predict fundamental and finger-hole frequencies from bore geometry; validate the predictions against my full build dataset; surface the residual patterns that suggest where the simple open-tube model is missing physics.

2. Acoustic Theory

Open-closed tube approximation. A NAF is treated as effectively open at the foot, with the slow air chamber acting as a coupled resonator. For first-order modeling, the sound chamber is approximated as an open-open tube. The slow-air-chamber coupling is documented in Section 9 as a known limitation.

Fundamental frequency: f = c / (2 · L_eff), where L_eff is the effective length including end corrections.

End correction at the open end: ΔL ≈ 0.6 · r for a tube of radius r.

Tone-hole effective length: each open hole shortens the effective length to the position of the hole, plus a hole-specific end correction depending on hole diameter and chimney height (the wall thickness at the hole).

Speed of sound, temperature-dependent: c = 331.3 · √(1 + T/273.15) m/s, with T in Celsius.

3. Core parameter functions

(* Speed of sound at temperature T in Celsius *)
soundSpeed[T_] := 331.3 * Sqrt[1 + T/273.15]

(* End correction for an open tube end *)
endCorrection[radius_] := 0.6 * radius
(* Effective length given physical length and bore radius *)
effectiveLength[L_, r_] := L + 2 * endCorrection[r]

(* Fundamental frequency of an open-open tube *)
fundamentalFreq[L_, r_, T_:20] := soundSpeed[T] / (2 * effectiveLength[L, r])

4. Tone-hole model

Standard effective-length method. Each open tone hole shortens the resonator's effective length to (hole position) + (chimney correction at that hole).

(* Effective length contribution of a tone hole *)
(* d = hole diameter, t = wall thickness (chimney height), b = bore diameter *)
toneHoleCorrection[d_, t_, b_] := Module[{te},
  te = t + 1.5 * (d/2);  (* effective chimney height including end correction *)
    te
    ]
    
    (* Predict frequency when a hole at xHole (from blowing end) is the first open hole *)
    holeFrequency[xHole_, holeDiameter_, wallThickness_, boreRadius_, T_:20] :=
      Module[{Leff, te},
          te = toneHoleCorrection[holeDiameter, wallThickness, 2*boreRadius];
              Leff = xHole + te + endCorrection[boreRadius];
                  soundSpeed[T] / (2 * Leff)
                    ]
                    
                    (* Helper convert frequency to nearest equal-tempered note name *)
                    freqToNote[f_] := Module[{midi, note, octave, names},
                      midi = Round[12 * Log2[f/440] + 69];
                        octave = Quotient[midi, 12] - 1;
                          names = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
                            names[[Mod[midi, 12] + 1]] <> ToString[octave]
                            ]

5. Interactive design tool

The showpiece. A live slider interface — set bore length, bore diameter, wall thickness, and 6 finger-hole positions/diameters; get predicted frequencies for each fingering.

Manipulate[  Module[{fundamental, holeFreqs},
    fundamental = fundamentalFreq[L, b/2];
        holeFreqs = Table[      holeFrequency[holePos[[i]], holeDia[[i]], wall, b/2],
              {i, 1, 6}
                  ];
                      Column[{
                            Style["Predicted Frequencies", Bold, 14],
                                  Grid[{
                                          {"Fingering", "Frequency (Hz)", "Note"},
                                                  {"All closed", fundamental, freqToNote[fundamental]},
                                                          Sequence @@ Table[          {"Hole " <> ToString[i] <> " open",
                                                                     holeFreqs[[i]],
                                                                                freqToNote[holeFreqs[[i]]]},
                                                                                          {i, 1, 6}
                                                                                                  ]
                                                                                                        }, Frame -> All],
                                                                                                              ListLinePlot[        {fundamental} ~Join~ holeFreqs,
                                                                                                                      PlotLabel -> "Scale steps",
                                                                                                                              AxesLabel -> {"Hole index (0 = all closed)", "Hz"}
                                                                                                                                    ]
                                                                                                                                        }]
                                                                                                                                          ],
                                                                                                                                            {{L, 0.45, "Bore length (m)"}, 0.2, 0.8, 0.005},
                                                                                                                                              {{b, 0.022, "Bore diameter (m)"}, 0.015, 0.035, 0.001},
                                                                                                                                                {{wall, 0.006, "Wall thickness (m)"}, 0.003, 0.012, 0.001},
                                                                                                                                                  Delimiter,
                                                                                                                                                    {{holePos, {0.30, 0.32, 0.34, 0.36, 0.38, 0.40},
                                                                                                                                                        "Hole positions (m)"}, ControlType -> None},
                                                                                                                                                          {{holeDia, {0.008, 0.008, 0.008, 0.008, 0.008, 0.008},
                                                                                                                                                              "Hole diameters (m)"}, ControlType -> None}
                                                                                                                                                              ]

6. Empirical validation against 150+ build data

The section that makes this notebook different from any other published flute-acoustic model. Validates predictions against my full build dataset. TODO: export build data to CSV (target columns: buildID, key, boreLength, boreDiameter, wallThickness, measuredFundamental, holePositions, holeDiameters) and upload to Wolfram Cloud.

(* Import build data — CSV with columns described above *)
buildData = Import[CloudObject["flute_builds.csv"], "Dataset", "HeaderLines" -> 1];

(* Compute predicted vs measured for each build *)
predictions = buildData[All, <|
  "predicted" -> fundamentalFreq[#boreLength, #boreDiameter/2] &,
    "measured" -> #measuredFundamental &,
      "error_cents" -> 1200 * Log2[#measuredFundamental /
                        fundamentalFreq[#boreLength, #boreDiameter/2]] &
                        |>];
                        
                        (* Scatter plot: predicted vs measured *)
                        ListPlot[
                          predictions[All, {"predicted", "measured"}] // Normal,
                            AxesLabel -> {"Predicted (Hz)", "Measured (Hz)"},
                              PlotStyle -> PointSize[Medium],
                                Epilog -> {Red, Line[{{200, 200}, {800, 800}}]},
                                  PlotLabel -> "Model vs. Measurement, n=150+"
                                  ]
                                  
                                  (* Histogram of error in cents — the killer chart *)
                                  Histogram[  predictions[All, "error_cents"] // Normal,
                                    PlotLabel -> "Prediction error (cents)",
                                      AxesLabel -> {"Error (cents)", "Count"}
                                      ]

7. Residual analysis and model refinement

TODO after Section 6 runs. Examine residuals for correlation with: key (frequency-dependent term?), bore diameter (end-correction refinement?), wall thickness (chimney correction?). For each correlated residual, fit a simple correction term and re-plot. Document each iteration. This section is what turns the notebook from "I built a calculator" into "I learned something."

8. Design table generator (reverse direction)

Given a target key, output a build spec.

designSpec[targetFundamental_, boreDia_:0.022, wall_:0.006] := Module[{L},
  L = soundSpeed[20] / (2 * targetFundamental) - 2 * endCorrection[boreDia/2];
    <|
        "targetHz" -> targetFundamental,
            "boreLengthM" -> L,
                "boreLengthIn" -> L * 39.3701,
                    "boreDiameterM" -> boreDia,
                        "wallThicknessM" -> wall,
                            "estimatedHoleSpacing" ->
                                  "see Section 5 widget for tone-hole layout"
                                    |>
                                    ]
                                    
                                    (* Generate a design table for G3 to G5 *)
                                    keys = {"G3", "A3", "B3", "C4", "D4", "E4", "F#4", "G4",
                                            "A4", "B4", "C5", "D5", "E5", "F#5", "G5"};
                                            keyFreqs = {196.00, 220.00, 246.94, 261.63, 293.66, 329.63,
                                                        369.99, 392.00, 440.00, 493.88, 523.25, 587.33,
                                                                    659.25, 739.99, 783.99};
                                                                    
                                                                    designTable = Dataset[  AssociationThread[{"key", "spec"} -> #] & /@
                                                                        Transpose[{keys, designSpec /@ keyFreqs}]
                                                                        ]

9. Discussion and limitations

What this model does NOT capture: Slow-air-chamber coupling — treated here as a simple open-end approximation; in reality the chamber resonance interacts with the bore. Finger occlusion variability — real-world half-holing and partial covers are not modeled. Wood species damping — negligible for fundamental frequency, audible for timbre/decay characteristics. Temperature and humidity drift in the playing environment — the temperature parameter handles the first-order effect but not humidity.