Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ephemer committed Mar 10, 2020
0 parents commit 7fc132f
Show file tree
Hide file tree
Showing 13 changed files with 684 additions and 0 deletions.
90 changes: 90 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

.build/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# Accio dependency management
Dependencies/
.accio/

# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/
29 changes: 29 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "WavReader",
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "WavReader",
targets: ["WavReader"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "WavReader",
dependencies: ["CWavHeader"]),
.target(name: "CWavHeader"),
.testTarget(
name: "WavReaderTests",
dependencies: ["WavReader"]),
]
)
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# WavReader (SwiftPM)

This example needs a wav file sitting next to the executable file to work.

Open the project in Xcode, build the project, then right click on `WavReader` under Products and choose `Show In Finder`. Add a file called `example.wav` in that directory and then you can run from within Xcode.
Empty file added Sources/CWavHeader/dummy.c
Empty file.
4 changes: 4 additions & 0 deletions Sources/CWavHeader/include/module.modulemap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module CWavHeader {
header "wavHeader.h"
export *
}
28 changes: 28 additions & 0 deletions Sources/CWavHeader/include/wavHeader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// wavHeader.h
// WavReader
//
// Created by Geordie Jay on 03.03.20.
// Copyright © 2020 flowkey. All rights reserved.
//

#ifndef wavHeader_h
#define wavHeader_h

struct FmtChunk {
unsigned char fmtChunkString[4];
unsigned int lengthOfFmtSection;
unsigned short formatType; // 1 == PCM is the only supported format
unsigned short channelCount;
unsigned int sampleRate;
unsigned int byteRate;
unsigned short blockAlignment;
unsigned short bitsPerSample;
};

struct DataChunk {
unsigned char dataChunkString[4];
unsigned int dataSize;
};

#endif /* wavHeader_h */
162 changes: 162 additions & 0 deletions Sources/WavReader/WavReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
//
// WavReader.swift
// WavReader
//
// Created by Geordie Jay on 03.03.20.
// Copyright © 2020 flowkey. All rights reserved.
//

import Foundation
import CWavHeader

public struct WavReader: Sequence {
public let sampleRate: Int
public let numFrames: Int
public let numChannels: Int
public let bitsPerSample: Int
public let bytesPerSample: Int

public let bytesPerFrame: Int
public let numSamples: Int
public let blockSize: Int // in frames

fileprivate let bytes: Data
fileprivate let offsetToWavData: Int

public init(bytes: Data, blockSize: Int = 1024) throws {
self.blockSize = blockSize

guard
let indexOfFormatSection = bytes.firstOccurence(of: "fmt", maxSearchBytes: 512),
let fmtChunk = bytes.withUnsafeBytes({ buffer -> FmtChunk? in
buffer.baseAddress?.advanced(by: indexOfFormatSection).bindMemory(to: FmtChunk.self, capacity: 1).pointee
})
else {
preconditionFailure("Invalid wav header: could not find fmt section")
}

print(fmtChunk)

sampleRate = Int(fmtChunk.sampleRate)
numChannels = Int(fmtChunk.channelCount)
bitsPerSample = Int(fmtChunk.bitsPerSample)
bytesPerSample = bitsPerSample / 8
bytesPerFrame = numChannels * bytesPerSample

guard
let indexOfDataSection = bytes.firstOccurence(of: "data", maxSearchBytes: 512),
let dataChunk = bytes.withUnsafeBytes({ buffer -> DataChunk? in
buffer.baseAddress?.advanced(by: indexOfDataSection).bindMemory(to: DataChunk.self, capacity: 1).pointee
})
else {
preconditionFailure("Invalid wav header: could not find data section")
}

numFrames = Int(dataChunk.dataSize) / bytesPerFrame
numSamples = numFrames * numChannels
offsetToWavData = indexOfDataSection + MemoryLayout<DataChunk>.size

self.bytes = bytes
precondition(bitsPerSample == 16, "The algo currently only supports 16bit")
}

public init(filename: String = "example.wav", blockSize: Int = 1024) throws {
let wavData = try Data(contentsOf: URL(fileURLWithPath: filename), options: [.mappedIfSafe])
try self.init(bytes: wavData, blockSize: blockSize)
}

public func makeIterator() -> WavReader.Iterator {
return Iterator(self)
}
}

extension WavReader {
public class Iterator: IteratorProtocol {
private var floatBuffer: [Float]
private var wavFile: WavReader

private var frameIndex = 0

fileprivate init(_ wavFile: WavReader) {
self.wavFile = wavFile
floatBuffer = [Float](repeating: 0.0, count: wavFile.blockSize * wavFile.numChannels)
}

public func next() -> [Float]? {
let offsetToWavData = wavFile.offsetToWavData

// it's cheaper to do multiplication than division, so divide
// here once and multiply each sample by this number:
let floatFactor = Float(1.0) / Float(Int16.max)

var framesRead = 0

return wavFile.bytes.withUnsafeBytes { bufferPointer in
let int16buffer = UnsafeBufferPointer<Int16>(
start: bufferPointer.baseAddress!.advanced(by: offsetToWavData).assumingMemoryBound(to: Int16.self),
count: wavFile.numSamples
)

while frameIndex < wavFile.numFrames {
for channel in 0 ..< wavFile.numChannels {
let dataArrayIndex = frameIndex + channel
let sample = int16buffer[dataArrayIndex]
floatBuffer[framesRead] = Float(sample) * floatFactor
}

framesRead += 1
self.frameIndex += 1

if framesRead == wavFile.blockSize {
return floatBuffer
}
}

if framesRead > 0 {
// We're at EOF

// Empty the rest of the float buffer, otherwise it
// will contain the previous frame's data:
(framesRead ..< wavFile.blockSize).forEach { i in
floatBuffer[i] = 0.0
}

return floatBuffer
}

return nil
}
}
}
}

fileprivate extension Data {
/// Search for the first occurence of the given string in our Data's buffer.
/// Optionally you can provide `maxSearchBytes` which stops the search after reaching the given byte count
/// This allows us to stop searching the entire Data buffer if the String was not found quickly.
func firstOccurence(of string: String, maxSearchBytes: Int? = nil) -> Int? {
let stringLength = string.utf8CString.count - 1
if stringLength == 0 { return nil }

let maxSearchBytes = (maxSearchBytes ?? self.count) - stringLength
if maxSearchBytes <= 0 { return nil }

return self.withUnsafeBytes { rawBufferPointer -> Int? in
let buffer = rawBufferPointer.bindMemory(to: CChar.self)

return string.withCString { stringBuffer in
buffer.indices.first { index in
if index >= maxSearchBytes { return false }

for stringIndex in 0 ..< stringLength {
if buffer[index + stringIndex] != stringBuffer[stringIndex] {
return false
}
}

return true
}
}
}
}
}
7 changes: 7 additions & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import XCTest

import WavFileTests

var tests = [XCTestCaseEntry]()
tests += WavFileTests.allTests()
XCTMain(tests)
15 changes: 15 additions & 0 deletions Tests/WavReaderTests/WavFileTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import XCTest
@testable import WavFile

final class WavFileTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(true, true)
}

static var allTests = [
("testExample", testExample),
]
}
9 changes: 9 additions & 0 deletions Tests/WavReaderTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import XCTest

#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(WavFileTests.allTests),
]
}
#endif
Loading

0 comments on commit 7fc132f

Please sign in to comment.